From 8d9f6758fbdf0a53f246a16705961ea72eab4a1c Mon Sep 17 00:00:00 2001 From: Convly Date: Mon, 17 Feb 2025 16:15:38 +0100 Subject: [PATCH 1/4] chore(scripts): add demo.sh script for streamlined demo management --- package.json | 8 +- scripts/demo.sh | 195 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 196 insertions(+), 7 deletions(-) create mode 100755 scripts/demo.sh diff --git a/package.json b/package.json index b6437a4..2bb3f70 100644 --- a/package.json +++ b/package.json @@ -48,13 +48,7 @@ "build": "rollup --config rollup.config.mjs --failAfterWarnings", "build:clean": "pnpm run clean && pnpm run build", "clean": "pnpm exec rimraf ./dist", - "demo:build": "pnpm run build && pnpm -C demo/node-typescript build", - "demo:env": "test -f demo/.strapi-app/.env || cp demo/.strapi-app/.env.example demo/.strapi-app/.env", - "demo:install": "pnpm install && pnpm -C demo/.strapi-app install && pnpm -C demo/node-typescript install && pnpm -C demo/node-javascript install", - "demo:seed": "pnpm -C demo/.strapi-app seed:example", - "demo:seed:clean": "pnpm exec rimraf demo/.strapi-app/.tmp/data.db && pnpm run demo:seed", - "demo:setup": "pnpm demo:install && pnpm run demo:env && pnpm run demo:build && pnpm run demo:seed:clean", - "demo:start": "pnpm -C demo/.strapi-app develop", + "demo": "pnpm exec scripts/demo.sh", "lint": "eslint .", "lint:fix": "eslint . --fix", "lint:fix:dry": "eslint . --fix-dry-run", diff --git a/scripts/demo.sh b/scripts/demo.sh new file mode 100755 index 0000000..6b28046 --- /dev/null +++ b/scripts/demo.sh @@ -0,0 +1,195 @@ +#!/bin/bash + +# Color codes as constants +COLOR_BLUE="34" +COLOR_RED="31" +COLOR_CYAN="36" +COLOR_YELLOW="33" +COLOR_GREEN="32" + +# Folder where demo applications reside +DEMO_FOLDER="demo" + +# Package manager used for managing dependencies and commands +PACKAGE_MANAGER="pnpm" + +# Name of the hidden folder for the Strapi demo app +STRAPI_APP_NAME=".strapi-app" + +# Full path to the Strapi demo app within the demo folder +STRAPI_APP_PATH="$DEMO_FOLDER/$STRAPI_APP_NAME" + +# Dynamically find subfolders in the $DEMO_FOLDER that do not start with a dot (hidden folders) +# This helps to list all demo applications except for the Strapi app or other hidden artifacts +STRAPI_DEMO_FOLDERS=$(find "$DEMO_FOLDER" -mindepth 1 -maxdepth 1 -type d ! -name ".*") + +# Function to print a message in a specified color +colored_echo() { + local message=$1 + local color=$2 + + echo -e "\033[1;${color}m${message}\033[0m" +} + +execute() { + colored_echo "$1" "$COLOR_BLUE" + + # Execute the command and handle any potential errors + eval "$1" || { + colored_echo "Error: Command failed - $1" "$COLOR_RED" + exit 1 + } +} + +demo_for_each() { + # Loop through each application folder found in the STRAPI_DEMO_FOLDERS + while read -r demo_folder; do + # Display the folder currently being processed + colored_echo "Executing in context of \"$demo_folder\"" "$COLOR_CYAN" + + # Execute the given command, passing the current demo folder as an argument + "$@" "$demo_folder" + done <<< "$STRAPI_DEMO_FOLDERS" +} + +# Install dependencies for the main Strapi app and each demo app +install() { + app_install # Strapi app + demo_for_each "demo_install" # Install dependencies for each demo +} + +# Build the main Strapi app and each demo app +build() { + app_build # Strapi app + demo_for_each "demo_build" # Build each demo +} + +# Build a specific demo app folder if a build script is present in its package.json +demo_build() { + local demo_folder=$1 + + # Run the build script for the demo app + execute "$PACKAGE_MANAGER -C \"$demo_folder\" run --if-present build" +} + +# Install dependencies for a specific demo app folder +demo_install() { + local demo_folder=$1 + + # Run the package manager install command in the demo folder + execute "$PACKAGE_MANAGER -C \"$demo_folder\" install" +} + +# Install dependencies for the Strapi app +app_install() { + execute "$PACKAGE_MANAGER -C \"$STRAPI_APP_PATH\" install" +} + +# Build the Strapi app +app_build() { + execute "$PACKAGE_MANAGER -C \"$STRAPI_APP_PATH\" run build" +} + +# Setup the .env file for the main Strapi app +app_env_setup() { + local env_file=".env" + local env_example_file=".env.example" + + # Define paths for the .env file and the .env.example file + local env_path="$STRAPI_APP_PATH/$env_file" + local env_example_path="$STRAPI_APP_PATH/$env_example_file" + + # Check if the .env file does not exist + if [ ! -e "$env_path" ]; then + colored_echo "⚠ No $env_file file found in the Strapi demo app ($env_path), proceeding with the app env setup" "$COLOR_YELLOW" + + # Create the .env file by copying from the .env.example file + execute "cp $env_example_path $env_path" + + colored_echo "✔ \"$env_file\" has been successfully generated from \"$env_example_file\"" "$COLOR_GREEN" + elif [ -e "$env_path" ] && [ ! -f "$env_path" ]; then + # Error if the path exists but is not a file + colored_echo "✖ The path exists but is not a file ($env_path), something is off" "$COLOR_RED" + else + # If the .env file already exists, skip setup + colored_echo "✔ Found an $env_file file in the Strapi demo app ($STRAPI_APP_PATH), skipping the app env setup" "$COLOR_GREEN" + fi +} + +# Seed the Strapi app, optionally cleaning the database first +app_seed() { + local should_clean=$1 + local database_path="$STRAPI_APP_PATH/.tmp/data.db" + + # If "clean" argument is passed, remove the database before seeding + if [ "$should_clean" = "clean" ]; then + colored_echo "Cleaning up the Strapi app database before seeding" "$COLOR_CYAN" + execute "$PACKAGE_MANAGER exec rimraf $database_path" + fi + + # Run the seed script for the main Strapi app + execute "$PACKAGE_MANAGER -C $STRAPI_APP_PATH seed:example" +} + +# Start the main Strapi app in development mode +app_start() { + execute "$PACKAGE_MANAGER -C $STRAPI_APP_PATH develop" +} + +# Complete setup script: installs dependencies, sets up .env, builds, and seeds the database +setup() { + colored_echo "Starting setup process..." "$COLOR_CYAN" + + colored_echo "Step 1: Installing all project dependencies..." "$COLOR_YELLOW" + install || { + colored_echo "✖ Failed to install dependencies. Exiting setup process." "$COLOR_RED" + exit 1 + } + + colored_echo "Step 2: Setting up the environment configuration..." "$COLOR_YELLOW" + app_env_setup || { + colored_echo "✖ Failed to set up the environment. Exiting setup process." "$COLOR_RED" + exit 1 + } + + colored_echo "Step 3: Building the projects..." "$COLOR_YELLOW" + build || { + colored_echo "✖ Failed to build the projects. Exiting setup process." "$COLOR_RED" + exit 1 + } + + colored_echo "Step 4: Cleaning and seeding the database..." "$COLOR_YELLOW" + app_seed "clean" || { + colored_echo "✖ Failed to seed the database. Exiting setup process." "$COLOR_RED" + exit 1 + } + + colored_echo "✔ Setup completed successfully!" "$COLOR_GREEN" +} + +# Display a help message for using the script +show_help() { + echo "Usage: ./demo.sh [command]" + echo + echo "Commands:" + echo " setup Complete setup: install, env, build, and seed-clean" + echo " build Build the main Strapi app and demo applications" + echo " install Install all dependencies for the main app and demo apps" + echo " app:env:setup Set up the environment configuration for the Strapi app" + echo " app:start Start the Strapi demo app in development mode" + echo " app:seed Seed the Strapi demo app" + echo " app:seed:clean Clean and seed the Strapi demo app" + echo " help Show this help message" +} + +# Command handler: runs the appropriate function based on the input command +case "$1" in +"setup") setup ;; +"build") build ;; +"install") install ;; +"app:env") app_env_setup ;; +"app:start") app_start ;; +"app:seed") app_seed ;; +"app:seed:clean") app_seed "clean" ;; +"help" | *) show_help ;; +esac From 8555019bb9a45943c2e670dc18982ca0226bf1f8 Mon Sep 17 00:00:00 2001 From: Convly Date: Mon, 17 Feb 2025 16:16:47 +0100 Subject: [PATCH 2/4] chore(demo): remove unnecessary blank line --- demo/next-server-components/src/app/layout.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/demo/next-server-components/src/app/layout.tsx b/demo/next-server-components/src/app/layout.tsx index 6268456..31df8c6 100644 --- a/demo/next-server-components/src/app/layout.tsx +++ b/demo/next-server-components/src/app/layout.tsx @@ -5,7 +5,6 @@ import type { Metadata } from 'next'; import './globals.css'; - const geistSans = Geist({ variable: '--font-geist-sans', subsets: ['latin'], From 6498696dea0454ac52bb83aee739afd93e127bf6 Mon Sep 17 00:00:00 2001 From: Convly Date: Mon, 17 Feb 2025 16:18:22 +0100 Subject: [PATCH 3/4] chore(demo): refactor and modularize seeding logic --- demo/.strapi-app/scripts/seed.js | 675 +++++++++++++++++++++---------- 1 file changed, 456 insertions(+), 219 deletions(-) diff --git a/demo/.strapi-app/scripts/seed.js b/demo/.strapi-app/scripts/seed.js index 96d8aa6..7490fdc 100644 --- a/demo/.strapi-app/scripts/seed.js +++ b/demo/.strapi-app/scripts/seed.js @@ -1,211 +1,467 @@ 'use strict'; -const fs = require('fs-extra'); -const path = require('path'); +const path = require('node:path'); + +const fse = require('fs-extra'); const mime = require('mime-types'); + const { categories, authors, articles, global, about } = require('../data/data.json'); -async function seedExampleApp(app) { - const shouldImportSeedData = await isFirstRun(); +/** + * Types Definition + * @typedef {import('@strapi/strapi').Core.Strapi} Strapi + * @typedef {{ filepath: string; mimetype: string; originalFileName: string; size: number }} FileMetadata + */ + +// CONSTANTS +const DEMO_FOLDER_PATH = path.join(__dirname, '..', '..'); +const UP_ROLE_UID = 'plugin::users-permissions.role'; +const UP_PERMISSION_UID = 'plugin::users-permissions.permission'; +const FILE_UID = 'plugin::upload.file'; + +// FILES MANIPULATION + +/** + * A utility class for handling file operations including path resolution, + * file metadata extraction, size calculation, and file uploads. + */ +class FileHandler { + static DEFAULT_UPLOAD_FOLDER_PATH = path.join(__dirname, '..', 'data', 'uploads'); - if (shouldImportSeedData) { - try { - console.log('Setting up the template...'); - await importSeedData(); + uploadsPath; + + constructor(uploadsPath = FileHandler.DEFAULT_UPLOAD_FOLDER_PATH) { + this.uploadsPath = uploadsPath; + } + + /** + * Constructs the full path by combining the upload directory path with the given filename. + * + * @param {string} filename - The name of the file to append to the uploads path. + * @return {string} The full path to the file. + */ + filePath(filename) { + return path.join(this.uploadsPath, filename); + } + + /** + * Retrieves the size of a specified file in bytes. + * + * @param {string} filename - The name of the file to determine the size for. + * @return {number} The size of the file in bytes. + */ + fileSize(filename) { + const filePath = this.filePath(filename); + const { size } = fse.statSync(filePath); + + return size; + } - console.log('Creating api token...'); - await createApiTokens(app); + /** + * Retrieves metadata information for a given file. + * + * @param {string} filename - The name of the file for which metadata is to be retrieved. + * @return {FileMetadata} An object containing metadata of the file. + */ + fileMetadata(filename) { + const filepath = this.filePath(filename); + const size = this.fileSize(filename); + const ext = path.extname(filename).slice(1); + const mimetype = mime.lookup(ext || '') || ''; + + return { filepath, mimetype, originalFileName: filename, size }; + } + + /** + * Uploads a file to the upload service with associated metadata. + * + * @param {FileMetadata} file - The file to be uploaded. + * @param {string} name - The name of the file used for metadata such as alternative text, caption, and filename. + * @return {Promise} - A promise resolving to the response from the upload service. + */ + static async upload(file, name) { + return app + .plugin('upload') + .service('upload') + .upload({ + files: file, + data: { + fileInfo: { + alternativeText: `An image uploaded to Strapi called ${name}`, + caption: name, + name, + }, + }, + }); + } + + /** + * Synchronizes a list of files by checking whether they already exist and uploading them if they do not. + * + * @param {string[]} files - An array of file names to be synchronized. + * @return {Object|Array} - Returns a single file object if only one file is processed, + * otherwise an array of processed file objects containing both existing and newly uploaded files. + */ + async sync(files) { + const existingFiles = []; + const uploadedFiles = []; + + const filesBackup = [...files]; + + for (const filename of filesBackup) { + // Check if the file already exists in Strapi + const match = await strapi + .query(FILE_UID) + // Try finding the file by name without its extension + .findOne({ + where: { + name: filename.replace(/\..*$/, ''), + }, + }); + + // File exists, don't upload it + if (match) { + existingFiles.push(match); + } + + // File doesn't exist, upload it + else { + const metadata = this.fileMetadata(filename); + const filenameWithoutExtension = filename.replace(/\.[^.]*$/, ''); - console.log('Ready to go'); - } catch (error) { - console.log('Could not import seed data'); - console.error(error); + const [file] = await FileHandler.upload(metadata, filenameWithoutExtension); + + uploadedFiles.push(file); + } } - } else { + + const processedFiles = [...existingFiles, ...uploadedFiles]; + + // If there is only one file, then return only that file + return processedFiles.length === 1 ? processedFiles[0] : processedFiles; + } +} + +/** + * Initializes and configures a Strapi app. + * It compiles the Strapi app, loads it, seeds the database with initial data, and performs cleanup before exiting. + * + * @overview + * This function is responsible for: + * - Compiling and initializing a Strapi app. + * - Setting the logging level of the app. + * - Seeding the app's database with example content, if it is the first run. + * - Cleaning up and shutting down the app gracefully. + * + * @async + * + * @example + * // Run the main function to set up and configure the Strapi app + * main() + * .then(() => console.log("Setup completed successfully")) + * .catch((error) => console.error("An error occurred during initialization:", error)); + * + * @throws {Error} If the Strapi app fails to load, data seeding fails, or the cleanup process encounters an error. + */ +async function main() { + const { createStrapi, compileStrapi } = require('@strapi/strapi'); + + const appContext = await compileStrapi(); + app = await createStrapi(appContext).load(); + + app.log.level = 'error'; + + await seedExampleApp(); + + await app.destroy(); + + process.exit(0); +} + +// UTILS + +/** + * Seeds the example app by importing seed data and initializing API tokens. + * This operation is performed only if it is the first run. + * + * @return {Promise} A promise that resolves when the seeding process is complete. + */ +async function seedExampleApp() { + const shouldImportSeedData = await isFirstRun(); + + if (!shouldImportSeedData) { console.log( - 'Seed data has already been imported. We cannot reimport unless you clear your database first.' + 'Seed data has already been imported. It cannot be imported again without clearing the database first.' ); + process.exit(0); + } + + try { + console.log('Importing the seed data...'); + await importSeedData(); + + console.log('Initializing API tokens...'); + await createDefaultAPITokens(); + + console.log('The app has been seeded successfully'); + } catch (error) { + console.error('Could not import seed data'); + console.error(error); } } +/** + * Checks if this is the first execution of the "init" setup process using the core store. + * + * @note If the setup has not been previously run, it updates the store to mark the setup as initialized. + * + * @return {Promise} A promise that resolves to `true` if it is the first run, or `false` otherwise. + */ async function isFirstRun() { - const pluginStore = strapi.store({ - environment: strapi.config.environment, + const pluginStore = app.store({ + environment: app.config.environment, type: 'type', name: 'setup', }); + const initHasRun = await pluginStore.get({ key: 'initHasRun' }); - await pluginStore.set({ key: 'initHasRun', value: true }); + + if (!initHasRun) { + await pluginStore.set({ key: 'initHasRun', value: true }); + } + return !initHasRun; } -async function setPublicPermissions(newPermissions) { +// PERMISSIONS + +/** + * Sets permissions for the public role by updating the Strapi permissions model with the provided actions. + * + * @param {Record} permissions An object defining permissions where keys refer to a controller name and values to an array of its action + * @return {Promise} A promise that resolves when all permissions have been successfully created. + * + * @throws {Error} Throw an error if the public role is not found. + */ +async function setPublicPermissions(permissions) { // Find the ID of the public role - const publicRole = await strapi.query('plugin::users-permissions.role').findOne({ - where: { - type: 'public', - }, - }); + const publicRole = await app.db.query(UP_ROLE_UID).findOne({ where: { type: 'public' } }); + + if (!publicRole) { + throw new Error('Public role not found, exiting...'); + } + + const { id: publicRoleID } = publicRole; // Create the new permissions and link them to the public role - const allPermissionsToCreate = []; - Object.keys(newPermissions).map((controller) => { - const actions = newPermissions[controller]; - const permissionsToCreate = actions.map((action) => { - return strapi.query('plugin::users-permissions.permission').create({ - data: { - action: `api::${controller}.${controller}.${action}`, - role: publicRole.id, - }, + const createPublicPermissionQueries = []; + + Object.keys(permissions).map((controller) => { + // create, update, delete, etc + const actions = permissions[controller]; + + const queries = actions + // Map actions to IDs + .map((action) => `api::${controller}.${controller}.${action}`) + // Create the permission + .map((action) => { + return app.db.query(UP_PERMISSION_UID).create({ data: { action, role: publicRoleID } }); }); - }); - allPermissionsToCreate.push(...permissionsToCreate); + + createPublicPermissionQueries.push(...queries); }); - await Promise.all(allPermissionsToCreate); -} -function getFileSizeInBytes(filePath) { - const stats = fs.statSync(filePath); - const fileSizeInBytes = stats['size']; - return fileSizeInBytes; + await Promise.all(createPublicPermissionQueries); } -function getFileData(fileName) { - const filePath = path.join('data', 'uploads', fileName); - // Parse the file metadata - const size = getFileSizeInBytes(filePath); - const ext = fileName.split('.').pop(); - const mimeType = mime.lookup(ext || '') || ''; - - return { - filepath: filePath, - originalFileName: fileName, - size, - mimetype: mimeType, - }; -} +/** + * Creates API tokens for specified access types and updates the .env files in demo directories with the generated tokens. + * + * @return {Promise} A promise resolving when the API tokens have been created and .env files have been updated. + */ +async function createDefaultAPITokens() { + const apiTokenService = app.service('admin::api-token'); -async function uploadFile(file, name) { - return strapi - .plugin('upload') - .service('upload') - .upload({ - files: file, - data: { - fileInfo: { - alternativeText: `An image uploaded to Strapi called ${name}`, - caption: name, - name, - }, - }, - }); -} + const tokenTypes = [ + { name: 'Full Access Token', type: 'full-access' }, + { name: 'Read Only Token', type: 'read-only' }, + ]; -// Create an entry and attach files if there are any -async function createEntry({ model, entry }) { - try { - // Actually create the entry in Strapi - await strapi.documents(`api::${model}.${model}`).create({ - data: entry, + const demoDirectories = findDemoDirectories(); + const envPaths = demoDirectories.map((dir) => path.join(dir, '.env')); + + for (const tokenType of tokenTypes) { + // Create the token + let token = await apiTokenService.create({ + name: tokenType.name, + description: `Token for ${tokenType.type} access`, + type: tokenType.type, + lifespan: null, }); - } catch (error) { - console.error({ model, entry, error }); + + console.log( + `Generated a new API Token named "${token.name}" (${token.type}): ${token.accessKey}` + ); + + // Update .env files + const envKey = `${tokenType.type.toUpperCase().replace('-', '_')}_TOKEN`; + + for (const envPath of envPaths) { + updateEnvFile(envPath, envKey, token.accessKey); + } } } -async function checkFileExistsBeforeUpload(files) { - const existingFiles = []; - const uploadedFiles = []; - const filesCopy = [...files]; +// DATA - for (const fileName of filesCopy) { - // Check if the file already exists in Strapi - const fileWhereName = await strapi.query('plugin::upload.file').findOne({ - where: { - name: fileName.replace(/\..*$/, ''), - }, - }); +/** + * Imports seed data into the app by setting public permissions + * for specific models and creating initial entries for them + * + * @return {Promise} Resolves when all seed data has been successfully imported. + */ +async function importSeedData() { + // Allow public "find one" queries on every model + await setPublicPermissions({ + article: ['findOne'], + category: ['findOne'], + author: ['findOne'], + global: ['findOne'], + about: ['findOne'], + }); - if (fileWhereName) { - // File exists, don't upload it - existingFiles.push(fileWhereName); - } else { - // File doesn't exist, upload it - const fileData = getFileData(fileName); - const fileNameNoExtension = fileName.split('.').shift(); - const [file] = await uploadFile(fileData, fileNameNoExtension); - uploadedFiles.push(file); - } + // Create all entries + await importCategories(); + await importAuthors(); + await importArticles(); + await importGlobal(); + await importAbout(); +} + +/** + * Creates a new entry for the specified model in the database. + * + * @param {Object} options - The options for creating the entry. + * @param {string} options.model - The name of the model for which the entry is created. + * @param {Object} options.entry - The data object representing the entry to be created. + * + * @return {Promise} A promise that resolves when the entry has been successfully created, or rejects with an error if the operation fails. + */ +async function createEntry({ model, entry }) { + const uid = `api::${model}.${model}`; + + try { + await app.documents(uid).create({ data: entry }); + } catch (error) { + console.error({ model, entry, error }); } - const allFiles = [...existingFiles, ...uploadedFiles]; - // If only one file then return only that file - return allFiles.length === 1 ? allFiles[0] : allFiles; } +/** + * Updates the given blocks (dynamic zone) by synchronizing files associated with them and modifying their structures accordingly. + * + * Handles specific types of blocks like media and slider, ensuring relevant files are updated or uploaded. + * + * Blocks not recognized by the function are returned as-is. + * + * @param {Array} blocks - An array of block objects to be processed. Each block contains a `__component` property indicating its type. + * + * @return {Promise>} A promise resolving to an array of updated block objects with synchronized file data. + */ async function updateBlocks(blocks) { const updatedBlocks = []; + for (const block of blocks) { - if (block.__component === 'shared.media') { - const uploadedFiles = await checkFileExistsBeforeUpload([block.file]); - // Copy the block to not mutate directly - const blockCopy = { ...block }; - // Replace the file name on the block with the actual file - blockCopy.file = uploadedFiles; - updatedBlocks.push(blockCopy); - } else if (block.__component === 'shared.slider') { - // Get files already uploaded to Strapi or upload new files - const existingAndUploadedFiles = await checkFileExistsBeforeUpload(block.files); - // Copy the block to not mutate directly - const blockCopy = { ...block }; - // Replace the file names on the block with the actual files - blockCopy.files = existingAndUploadedFiles; - // Push the updated block - updatedBlocks.push(blockCopy); - } else { - // Just push the block as is - updatedBlocks.push(block); + switch (block.__component) { + case 'shared.media': + const uploadedFiles = await fileHandler.sync([block.file]); + + // Copy the block to not mutate directly + const clonedMedia = { ...block }; + + // Replace the filename on the block with the actual file + clonedMedia.file = uploadedFiles; + + updatedBlocks.push(clonedMedia); + break; + case 'shared.slider': + // Get files already uploaded to Strapi or upload new files + const existingAndUploadedFiles = await fileHandler.sync(block.files); + + // Copy the block to not mutate directly + const clonedSlider = { ...block }; + + // Replace the file names on the block with the actual files + clonedSlider.files = existingAndUploadedFiles; + + // Push the updated block + updatedBlocks.push(clonedSlider); + break; + default: + // Push the block as is + updatedBlocks.push(block); + break; } } return updatedBlocks; } +/** + * Imports articles by processing their blocks, synchronizing their cover images, + * and creating new entries with the necessary updates. + * + * @return {Promise} Resolves when all articles have been successfully imported. + */ async function importArticles() { for (const article of articles) { - const cover = await checkFileExistsBeforeUpload([`${article.slug}.jpg`]); + const cover = await fileHandler.sync([`${article.slug}.jpg`]); const updatedBlocks = await updateBlocks(article.blocks); await createEntry({ model: 'article', entry: { ...article, - cover, + // Overrides blocks: updatedBlocks, - // Make sure it's not a draft + cover, + // Make sure it is not a draft publishedAt: Date.now(), }, }); } } +/** + * Imports and synchronizes global settings including favicon and default share image. + * + * Retrieves the global "favicon" and "share" image, synchronizes them, + * and creates or updates the global entry with the modified SEO and publishing settings. + * + * @return {Promise} A promise that resolves to the created or updated global entry object. + */ async function importGlobal() { - const favicon = await checkFileExistsBeforeUpload(['favicon.png']); - const shareImage = await checkFileExistsBeforeUpload(['default-image.png']); + const favicon = await fileHandler.sync(['favicon.png']); + const shareImage = await fileHandler.sync(['default-image.png']); + return createEntry({ model: 'global', entry: { ...global, + // Overrides + defaultSeo: { ...global.defaultSeo, shareImage }, favicon, - // Make sure it's not a draft + // Make sure it is not a draft publishedAt: Date.now(), - defaultSeo: { - ...global.defaultSeo, - shareImage, - }, }, }); } +/** + * Imports and updates the "about" entry by modifying its blocks and ensuring the entry is not in draft status. + * + * @return {Promise} A promise that resolves when the "about" entry has been successfully imported and updated. + */ async function importAbout() { const updatedBlocks = await updateBlocks(about.blocks); @@ -213,126 +469,107 @@ async function importAbout() { model: 'about', entry: { ...about, + // Overrides blocks: updatedBlocks, - // Make sure it's not a draft + // Make sure it is not a draft publishedAt: Date.now(), }, }); } +/** + * Imports a list of categories by creating entries for each category. + * + * @return {Promise} A promise that resolves when all categories have been imported. + */ async function importCategories() { for (const category of categories) { await createEntry({ model: 'category', entry: category }); } } +/** + * Imports authors into the system by processing their data, synchronizing their avatar files, + * and creating entries for each author in the specified model. + * + * @return {Promise} A promise that resolves when all authors have been successfully processed and imported. + */ async function importAuthors() { for (const author of authors) { - const avatar = await checkFileExistsBeforeUpload([author.avatar]); + const avatar = await fileHandler.sync([author.avatar]); - await createEntry({ - model: 'author', - entry: { - ...author, - avatar, - }, - }); + await createEntry({ model: 'author', entry: { ...author, avatar } }); } } -async function importSeedData() { - // Allow read of application content types - await setPublicPermissions({ - article: ['find', 'findOne'], - category: ['find', 'findOne'], - author: ['find', 'findOne'], - global: ['find', 'findOne'], - about: ['find', 'findOne'], - }); - - // Create all entries - await importCategories(); - await importAuthors(); - await importArticles(); - await importGlobal(); - await importAbout(); -} - -async function createApiTokens(app) { - const apiTokenService = app.service('admin::api-token'); - - const tokenTypes = [ - { name: 'Full Access Token', type: 'full-access' }, - { name: 'Read Only Token', type: 'read-only' }, - ]; - - const envPaths = [ - path.join(__dirname, '../../next-server-components/.env'), - path.join(__dirname, '../../node-javascript/.env'), - path.join(__dirname, '../../node-typescript/.env'), - ]; - - for (const tokenType of tokenTypes) { - const tokenDetails = { - name: tokenType.name, - description: `Token for ${tokenType.type} access`, - type: tokenType.type, - lifespan: null, - }; - - // Create the token - let token = await apiTokenService.create({ - ...tokenDetails, - }); +// ENV + +/** + * Updates or adds a key-value pair in the specified environment (.env) file. + * + * If the key already exists, its value is updated; else it is appended at the end of the file. + * + * @param {string} filePath - The path to the environment file. + * @param {string} key - The environment variable key to update or add. + * @param {string} value - The value to set for the environment variable key. + * @return {void} + */ +function updateEnvFile(filePath, key, value) { + const fileExists = fse.existsSync(filePath); - console.log( - `Created API Token ${token.name} (${token.type}) with access key: ${token.accessKey}` + if (!fileExists) { + console.error( + `Couldn't add the ${key} ENV variable to "${filePath}" because the file doesn't exist` ); - - // Update .env files - const envKey = tokenType.type.toUpperCase().replace('-', '_') + '_TOKEN'; - for (const envPath of envPaths) { - updateEnvFile(envPath, envKey, token.accessKey); - } + return; } -} -function updateEnvFile(filePath, key, value) { - let envContent = ''; - if (fs.existsSync(filePath)) { - envContent = fs.readFileSync(filePath, 'utf8'); - } + const env = fse.readFileSync(filePath, 'utf8'); + const envLines = env.split('\n'); - const envLines = envContent.split('\n'); const keyIndex = envLines.findIndex((line) => line.startsWith(`${key}=`)); + // The key already exists, update it if (keyIndex !== -1) { - // Update existing key envLines[keyIndex] = `${key}=${value}`; - } else { - // Add new key + } + // The key doesn't exist, create it + else { envLines.push(`${key}=${value}`); } - fs.writeFileSync(filePath, envLines.join('\n'), 'utf8'); + // Save the modified env content back to the file + const updatedEnv = envLines.join('\n'); + fse.writeFileSync(filePath, updatedEnv, 'utf8'); } -async function main() { - const { createStrapi, compileStrapi } = require('@strapi/strapi'); - - const appContext = await compileStrapi(); - const app = await createStrapi(appContext).load(); - - app.log.level = 'error'; +// UTILS + +/** + * Finds and returns the paths of all demo directories within the demo root folder + * + * Only includes directories that aren't hidden + * + * @return {string[]} An array of relative paths to the demo directories. + */ +function findDemoDirectories() { + return fse + .readdirSync(DEMO_FOLDER_PATH, { withFileTypes: true }) + .filter((dirent) => dirent.isDirectory()) + .filter((dirent) => !dirent.name.startsWith('.')) + .map((dirent) => path.join(DEMO_FOLDER_PATH, dirent.name)); +} - await seedExampleApp(app); +// ENTRYPOINT - await app.destroy(); +/** @type {Strapi} */ +let app; - process.exit(0); -} +const fileHandler = new FileHandler(); -main().catch((error) => { - console.error(error); - process.exit(1); -}); +main() + .then(() => process.exit(0)) + .catch((error) => { + console.error(error); + process.exit(1); + }); From 91374a37b82574a00e612a5b72bb2751ddfc9644 Mon Sep 17 00:00:00 2001 From: Convly Date: Mon, 17 Feb 2025 16:36:51 +0100 Subject: [PATCH 4/4] docs(demo): update README with new demo commands and structure --- README.md | 93 +++++++++++++++++++++++++++++++++++++------------------ 1 file changed, 63 insertions(+), 30 deletions(-) diff --git a/README.md b/README.md index 0c66617..077ee6c 100644 --- a/README.md +++ b/README.md @@ -254,73 +254,106 @@ Below is a list of available namespaces to use: ## 🚀 Demo Projects -This repository includes demo projects located in the `/demo` directory to help you get started with using the client. The actual Strapi application is located in the `.strapi-app` directory. +This repository includes demo projects located in the `/demo` directory to help you get started with using the client. The actual Strapi app is located in the `.strapi-app` directory. ### Demo Structure -- **`.strapi-app`**: This is the main Strapi application used for the demo projects. -- **`demo/node-typescript`**: A Node.js project using TypeScript. -- **`demo/node-javascript`**: A Node.js project using JavaScript. +- **`.strapi-app`**: the main Strapi app used for the demo projects. +- **`demo/node-typescript`**: a Node.js project using TypeScript. +- **`demo/node-javascript`**: a Node.js project using JavaScript. +- **`demo/next-server-components`**: a Next.js project using TypeScript and server components. -### Using Demo Scripts +### Using Demo Commands -The `package.json` includes several scripts to help you manage and run the demo projects. These scripts are designed to streamline the process of setting up and running the demo projects, making it easier for developers to test and interact with the client. +The repository supports running demo-related commands directly using the format `pnpm demo `. -The most important basic commands to get started are: +To display the entire list of available commands, use `pnpm demo help` -- **`demo:setup`**: A comprehensive setup command that installs dependencies, sets up the environment, builds the projects, and seeds the demo application. This is a one-stop command to prepare everything needed to run the demos. +#### Comprehensive Setup: + +- **`pnpm demo setup`** + A complete setup command that installs dependencies, sets up the environment, + builds the projects, and seeds the database with initial data for the demo app. + + It is a one-stop command for preparing everything. ```bash - pnpm run demo:setup + pnpm demo setup ``` -- **`demo:run`**: Runs the Strapi application server in development mode. This command is useful for testing and developing. +#### Development: + +- **`pnpm demo app:start`** + Starts the Strapi demo app in development mode. This is useful for testing and making changes to the Strapi backend. ```bash - pnpm run demo:run + pnpm demo app:start ``` -- **`demo:seed:clean`**: Cleans the existing data and re-seeds the Strapi demo application. This command is helpful for resetting the demo data to its initial state. +#### Database Seeding: + +- **`pnpm demo app:seed`** + Seeds the Strapi app with sample data. Use this when you want to populate your Strapi app with default content. ```bash - pnpm run demo:seed:clean + pnpm demo app:seed ``` -The following scripts shouldn't need to be used generally, but are also available to run individual parts of the setup or reset process: - -- **`demo:build`**: Builds the main project and the TypeScript and JavaScript demo projects. This command ensures that all necessary build steps are completed for the demo projects to run. +- **`pnpm demo app:seed:clean`** + Cleans the existing database and re-seeds the Strapi demo app. + This is helpful if you want to reset the demo data to its initial state. ```bash - pnpm run demo:build + pnpm demo app:seed:clean ``` -- **`demo:install`**: Installs all dependencies for the main project and all demo projects, including TypeScript, JavaScript, HTML, and Next.js demos. This command is essential to prepare the environment for running the demos. +#### Build and Install: + +- **`pnpm demo build`** + Builds the main Strapi app and all demo projects. + + Use this to prepare the projects for use, ensuring all components are compiled and ready. ```bash - pnpm run demo:install + pnpm demo build ``` -- **`demo:env`**: Sets up the environment for the Strapi demo application by copying the example environment file if it doesn't already exist. +- **`pnpm demo install`** + Installs dependencies for the main Strapi app and all demo applications. + + This command ensures that all required packages are downloaded and ready to go. ```bash - pnpm run demo:env + pnpm demo install ``` -- **`demo:seed`**: Seeds the Strapi application with example data. This script also generates `.env` files with API tokens for the `node-typescript` and `node-javascript` projects. +#### Environment Setup: + +- **`pnpm demo app:env:setup`** + Sets up the `.env` file for the main Strapi app by copying the example `.env.example` file if no `.env` file exists. + + This ensures the environment is configured appropriately. ```bash - pnpm run demo:seed + pnpm demo app:env:setup ``` -### Adding New Projects +--- -If you add new projects to the `/demo` directory, you will need to update the seed script located at `/demo/.strapi-app/scripts/seed.js` with the new project paths to ensure they are properly configured and seeded. +#### Adding New Projects -### Future Plans +New projects added to the `/demo` directory are automatically picked up by the demo scripts. +Thus, no explicit configuration updates are required for these commands to work with new demo directories. -We plan to expand the demo projects to include: +**Note:** if a project needs to be built to be used, add a `build` script +to its `package.json` so that the demo scripts automatically run it. -- A basic HTML project. -- A Next.js project. +--- + +#### Future Plans + +We plan to expand the demo projects to include: -These additions will provide more examples of how to integrate the client into different types of applications. +- A basic HTML project +- A Vue.js project (with or without server components) +- A Svelte project