From ec37c3861a8a89b250d472b80816d86b2def24a0 Mon Sep 17 00:00:00 2001 From: Mike Jarosch Date: Mon, 14 Nov 2022 17:44:53 -0600 Subject: [PATCH 1/2] First pass of enhanced scafolding --- .../src/templates/nextjs/package.json | 6 +- ...onent.ts => scaffold-component.ts.default} | 0 .../src/templates/nextjs/scripts/scaffold.ts | 12 ++ .../scripts/scaffold/components/config.ts | 19 ++ .../scripts/scaffold/components/index.ts | 106 ++++++++++ .../components}/templates/component-src.ts | 8 +- .../scripts/scaffold/components/utils.ts | 188 ++++++++++++++++++ 7 files changed, 335 insertions(+), 4 deletions(-) rename packages/create-sitecore-jss/src/templates/nextjs/scripts/{scaffold-component.ts => scaffold-component.ts.default} (100%) create mode 100644 packages/create-sitecore-jss/src/templates/nextjs/scripts/scaffold.ts create mode 100644 packages/create-sitecore-jss/src/templates/nextjs/scripts/scaffold/components/config.ts create mode 100644 packages/create-sitecore-jss/src/templates/nextjs/scripts/scaffold/components/index.ts rename packages/create-sitecore-jss/src/templates/nextjs/scripts/{ => scaffold/components}/templates/component-src.ts (82%) create mode 100644 packages/create-sitecore-jss/src/templates/nextjs/scripts/scaffold/components/utils.ts diff --git a/packages/create-sitecore-jss/src/templates/nextjs/package.json b/packages/create-sitecore-jss/src/templates/nextjs/package.json index 118a8bc60a..5fd75c319e 100644 --- a/packages/create-sitecore-jss/src/templates/nextjs/package.json +++ b/packages/create-sitecore-jss/src/templates/nextjs/package.json @@ -46,6 +46,7 @@ "@graphql-codegen/typescript-resolvers": "^1.17.10", "@graphql-typed-document-node/core": "^3.1.0", "@sitecore-jss/sitecore-jss-cli": "^21.1.0-canary", + "@types/inquirer": "^8.1.3", "@types/node": "^14.6.4", "@types/react": "^18.0.12", "@types/react-dom": "^18.0.5", @@ -54,6 +55,7 @@ "axios": "^0.21.1", "chalk": "~2.4.2", "chokidar": "~3.1.1", + "commander": "^9.4.1", "constant-case": "^3.0.4", "cross-env": "~6.0.3", "dotenv": "^16.0.0", @@ -63,6 +65,7 @@ "eslint-plugin-prettier": "^3.1.4", "eslint-plugin-yaml": "^0.2.0", "graphql-let": "^0.16.2", + "inquirer": "^8.2.0", "next-transpile-modules": "^9.0.0", "npm-run-all": "~4.1.5", "prettier": "^2.1.2", @@ -79,7 +82,8 @@ "next:build": "next build", "next:dev": "cross-env NODE_OPTIONS='--inspect' next dev", "next:start": "next start", - "scaffold": "ts-node --project tsconfig.scripts.json scripts/scaffold-component.ts", + "scaffold": "ts-node --project tsconfig.scripts.json scripts/scaffold.ts", + "scaffold:component": "npm run scaffold -- component", "start:connected": "npm-run-all --serial bootstrap --parallel next:dev start:watch-components", "start:production": "npm-run-all --serial bootstrap next:build next:start", "start:watch-components": "ts-node --project tsconfig.scripts.json scripts/generate-component-factory.ts --watch" diff --git a/packages/create-sitecore-jss/src/templates/nextjs/scripts/scaffold-component.ts b/packages/create-sitecore-jss/src/templates/nextjs/scripts/scaffold-component.ts.default similarity index 100% rename from packages/create-sitecore-jss/src/templates/nextjs/scripts/scaffold-component.ts rename to packages/create-sitecore-jss/src/templates/nextjs/scripts/scaffold-component.ts.default diff --git a/packages/create-sitecore-jss/src/templates/nextjs/scripts/scaffold.ts b/packages/create-sitecore-jss/src/templates/nextjs/scripts/scaffold.ts new file mode 100644 index 0000000000..84ca56f252 --- /dev/null +++ b/packages/create-sitecore-jss/src/templates/nextjs/scripts/scaffold.ts @@ -0,0 +1,12 @@ +const { program } = require('commander'); +import { scaffoldCommand } from './scaffold/components'; + +program + .command('component') + .description('create template files for components') + .argument('[component-name]') + .action((componentName: string) => { + scaffoldCommand(componentName); + }); + +program.parse(process.argv); diff --git a/packages/create-sitecore-jss/src/templates/nextjs/scripts/scaffold/components/config.ts b/packages/create-sitecore-jss/src/templates/nextjs/scripts/scaffold/components/config.ts new file mode 100644 index 0000000000..291f87d841 --- /dev/null +++ b/packages/create-sitecore-jss/src/templates/nextjs/scripts/scaffold/components/config.ts @@ -0,0 +1,19 @@ +import { ComponentsFolderConfig } from './utils'; + +import { componentTemplate } from './templates/component-src'; + +// theme: {}, +// site: {}, +// helper: {}, + +const componentsFolderConfig: ComponentsFolderConfig = { + levels: [ { name: 'Shared', short: 'Components used by everyone' }, ], + directories: [ + { name: 'default', short: 'The default component folder', path: '' } + ], + templates: [ + { name: 'default', short: 'The default component template', fileName: '[name].tsx', template: componentTemplate } + ], +}; + +export default componentsFolderConfig; diff --git a/packages/create-sitecore-jss/src/templates/nextjs/scripts/scaffold/components/index.ts b/packages/create-sitecore-jss/src/templates/nextjs/scripts/scaffold/components/index.ts new file mode 100644 index 0000000000..6ebc9700b9 --- /dev/null +++ b/packages/create-sitecore-jss/src/templates/nextjs/scripts/scaffold/components/index.ts @@ -0,0 +1,106 @@ +import inquirer, { Question } from 'inquirer'; +import chalk from 'chalk'; +import fs from 'fs'; +import path from 'path'; +import { ComponentsFolder } from './utils'; +import componentConfig from './config'; + +// Matches component names that start with a capital letter, and contain only letters, number, +// underscores, or dashes. Optionally, the component name can be preceded by a relative path +const nameParamFormat = new RegExp(/^((?:[\w-]+\/)*)([A-Z][\w-]+)$/); + +const componentRootPath = 'src/components'; + +/** + * Force to use `crlf` line endings, we are using `crlf` across the project. + * Replace: `lf` (\n), `cr` (\r) + * @param {string} content + */ + function editLineEndings(content: string) { + return content.replace(/\r|\n/gm, '\r\n'); +} + +export const scaffoldCommand = async (componentName: string | undefined) => { + let answers: any = { + componentName + }; + + // Get the name + while (!answers.componentName) { + await inquirer.prompt({ + name: 'componentName', + message: 'The name for the component to generate?', + }, answers); + + const regExResult = nameParamFormat.exec(answers.componentName as string); + if (regExResult === null) { + console.log(chalk.red(`Component name should start with an uppercase letter and contain only letters, numbers, dashes, or underscores. If specifying a path, it must be relative to src/components`)); + componentName = undefined; + } + } + + let componentsFolder = new ComponentsFolder(componentConfig); + + // Get the level + if (componentsFolder.levels.length > 1) { + await inquirer.prompt({ + name: 'level', + type: 'list', + message: 'What level should the component be created at?', + choices: componentsFolder.levels + }); + } else { + answers.level = componentsFolder.levels[0].name; + } + const level = componentsFolder.levels.find(_ => _.name === answers.level) || componentsFolder.levels[0]; + + // Get the directory + if (level.directories.length > 1) { + await inquirer.prompt({ + name: 'directory', + type: 'list', + message: 'What directory should the component be created in?', + choices: level.directories + }); + } else { + answers.directory = level.directories[0].name; + } + const directory = level.directories.find(_ => _.name === answers.directory) || level.directories[0]; + + // Validate overwrite + + // Questions + let questions: Question[] = []; + + if (componentsFolder.questions.length > 0) { + questions.concat(componentsFolder.questions); + } + if (level.questions.length > 0) { + questions.concat(level.questions); + } + if (directory.questions.length > 0) { + questions.concat(directory.questions); + } + if (questions.length > 1) { + await inquirer.prompt(questions, answers); + } + + // Output directory + let outputDir = componentRootPath; + if (level.path) { + outputDir = path.join(componentRootPath, level.path); + } + outputDir = path.join(outputDir, directory.path); + fs.mkdirSync(outputDir, { recursive: true }); + + // Generate files + for (const template of directory.templates) { + const fileName = template.fileName.replace('[name]', answers.componentName); + const filePath = path.join(outputDir, fileName); + + const templateContent = editLineEndings(template.template(answers)); + + fs.writeFileSync(filePath, templateContent, 'utf8'); + console.log(chalk.green(`File ${fileName} has been scaffolded.`)); + } +} diff --git a/packages/create-sitecore-jss/src/templates/nextjs/scripts/templates/component-src.ts b/packages/create-sitecore-jss/src/templates/nextjs/scripts/scaffold/components/templates/component-src.ts similarity index 82% rename from packages/create-sitecore-jss/src/templates/nextjs/scripts/templates/component-src.ts rename to packages/create-sitecore-jss/src/templates/nextjs/scripts/scaffold/components/templates/component-src.ts index 632799ce27..4fac224114 100644 --- a/packages/create-sitecore-jss/src/templates/nextjs/scripts/templates/component-src.ts +++ b/packages/create-sitecore-jss/src/templates/nextjs/scripts/scaffold/components/templates/component-src.ts @@ -1,9 +1,13 @@ +import { Template, TemplateArgs } from '../utils'; + /** * Generates React boilerplate for a component under `src/components` * @param componentName - the component name * @returns component src boilerplate as a string */ -function generateComponentSrc(componentName: string): string { +export const componentTemplate: Template = ({ + componentName, +}): string => { return `import { Text, Field, withDatasourceCheck } from '@sitecore-jss/sitecore-jss-nextjs'; import { ComponentProps } from 'lib/component-props'; @@ -23,5 +27,3 @@ const ${componentName} = (props: ${componentName}Props): JSX.Element => ( export default withDatasourceCheck()<${componentName}Props>(${componentName}); `; } - -export default generateComponentSrc; diff --git a/packages/create-sitecore-jss/src/templates/nextjs/scripts/scaffold/components/utils.ts b/packages/create-sitecore-jss/src/templates/nextjs/scripts/scaffold/components/utils.ts new file mode 100644 index 0000000000..99a2af0581 --- /dev/null +++ b/packages/create-sitecore-jss/src/templates/nextjs/scripts/scaffold/components/utils.ts @@ -0,0 +1,188 @@ +import { Question } from 'inquirer'; + +// Template types +export interface TemplateArgs { + componentName: string; + directory: string; +} + +export type Template = (args: T) => string; + +// Configuration Types +type ComponentsFolderLevels = 'Shared' | 'Theme' | 'Site' | 'Helper'; + +export type ComponentsFolderConfig = { + levels: ComponentConfigLevel[]; + directories: ComponentConfigDirectory[]; + templates: ComponentConfigTemplate[]; + questions?: Question[]; +}; + +type ComponentConfigLevel = { + name: ComponentsFolderLevels; + short: string; + path?: string; + directories?: ComponentConfigOverwrite | ComponentConfigPatch | undefined; + templates?: ComponentConfigOverwrite | ComponentConfigPatch | undefined; + questions?: Question[] | undefined; +}; + +type ComponentConfigDirectory = { + name: string; + short: string; + path: string; + templates?: ComponentConfigOverwrite | ComponentConfigPatch | undefined; + questions?: Question[] | undefined; +}; + +type ComponentConfigTemplate = { + name: string; + short: string; + fileName: string; + template: Template; + questions?: Question[] | undefined; +}; + +type ComponentConfigPatch = { + kind: "patch"; + add: Type[] | undefined; + remove: string[] | undefined; + replace: Type[] | undefined; +}; + +type ComponentConfigOverwrite = { + kind: "overwrite"; + overwrite: Type[]; +}; + +// Types used for the configuration after applying any overrides +type ComponentsFolderLevel = { + name: ComponentsFolderLevels; + short: string; + path?: string; + directories: ComponentsFolderDirectory[]; + questions: Question[]; +}; + +type ComponentsFolderDirectory = { + name: string; + short: string; + path: string; + templates: ComponentsFolderTemplate[]; + questions: Question[]; +}; + +type ComponentsFolderTemplate = { + name: string; + short: string; + fileName: string; + template: Template; + questions: Question[]; +}; + +export class ComponentsFolder { + readonly levels: ComponentsFolderLevel[]; + readonly questions: Question[]; + + constructor(protected configuration: ComponentsFolderConfig) { + if (configuration.levels.length === 0) { + throw `No levels configured`; + } + + this.questions = configuration.questions || []; + + this.levels = []; + for (const levelConfig of configuration.levels) { + const level = this.buildLevel(levelConfig, configuration); + this.levels.push(level); + } + } + + private buildLevel(levelConfig: ComponentConfigLevel, rootConfig: ComponentsFolderConfig): ComponentsFolderLevel { + // The allowed directories is a combination of the root and level directories + let directoryConfigs = this.applyOverrides(rootConfig.directories, levelConfig.directories); + if (directoryConfigs.length === 0) { + throw `No directories configured for level ${levelConfig.name}`; + } + + const directories: ComponentsFolderDirectory[] = []; + for (const directoryConfig of directoryConfigs) { + const directory = this.buildDirectory(directoryConfig, levelConfig, rootConfig); + directories.push(directory); + } + + let level: ComponentsFolderLevel = { + name: levelConfig.name, + short: levelConfig.short, + path: levelConfig.path, + questions: levelConfig.questions || [], + directories, + }; + + return level; + } + + private buildDirectory(directoryConfig: ComponentConfigDirectory, levelConfig: ComponentConfigLevel, rootConfig: ComponentsFolderConfig): ComponentsFolderDirectory { + // The allowed templates is a combination of the root, level and directories + let templateConfigs = this.applyOverrides(rootConfig.templates, levelConfig.templates); + templateConfigs = this.applyOverrides(templateConfigs, directoryConfig.templates); + if (templateConfigs.length === 0) { + throw `No templates configured for level ${levelConfig.name} and directory ${directoryConfig.name}`; + } + + const templates: ComponentsFolderTemplate[] = []; + for (const templateConfig of templateConfigs) { + const template = this.buildTemplate(templateConfig); + templates.push(template); + } + + let directory: ComponentsFolderDirectory = { + name: directoryConfig.name, + short: directoryConfig.short, + path: directoryConfig.path, + questions: directoryConfig.questions || [], + templates, + }; + + return directory; + } + + private buildTemplate(templateConfig: ComponentConfigTemplate): ComponentsFolderTemplate { + let template: ComponentsFolderTemplate = { + name: templateConfig.name, + short: templateConfig.short, + fileName: templateConfig.fileName, + template: templateConfig.template, + questions: templateConfig.questions || [], + }; + + return template; + } + + private applyOverrides(base: Type[], override: ComponentConfigOverwrite | ComponentConfigPatch | undefined) { + let result = base; + if (override) { + if (override === undefined) { + // Do nothing + } else if (override.kind === "overwrite") { + result = override.overwrite; + } else if (override.kind === "patch") { + if (override.add) { + result.concat(override.add); + } + if (override.remove) { + const remove = override.remove; + result = result.filter((_) => !remove.indexOf(_.name)) + } + if (override.replace) { + const replace = override.replace; + result = result.map(_ => { + const index = replace.findIndex(__ => __.name === _.name); + return index >= 0 ? replace[index] : _; + }); + } + } + } + return result; + } +} From db41ef09ed6390228509938638ed591886a369e9 Mon Sep 17 00:00:00 2001 From: Mike Jarosch Date: Tue, 15 Nov 2022 19:52:35 -0600 Subject: [PATCH 2/2] Simplify the scaffolding config and process. --- .../src/templates/nextjs/scripts/scaffold.ts | 3 +- .../scripts/scaffold/components/config.ts | 18 +- .../scripts/scaffold/components/index.ts | 174 ++++++++++------ .../scripts/scaffold/components/utils.ts | 185 ++---------------- 4 files changed, 133 insertions(+), 247 deletions(-) diff --git a/packages/create-sitecore-jss/src/templates/nextjs/scripts/scaffold.ts b/packages/create-sitecore-jss/src/templates/nextjs/scripts/scaffold.ts index 84ca56f252..48bc088f01 100644 --- a/packages/create-sitecore-jss/src/templates/nextjs/scripts/scaffold.ts +++ b/packages/create-sitecore-jss/src/templates/nextjs/scripts/scaffold.ts @@ -1,5 +1,6 @@ -const { program } = require('commander'); +/* eslint-disable @typescript-eslint/no-var-requires */ import { scaffoldCommand } from './scaffold/components'; +const { program } = require('commander'); program .command('component') diff --git a/packages/create-sitecore-jss/src/templates/nextjs/scripts/scaffold/components/config.ts b/packages/create-sitecore-jss/src/templates/nextjs/scripts/scaffold/components/config.ts index 291f87d841..17a4ab204e 100644 --- a/packages/create-sitecore-jss/src/templates/nextjs/scripts/scaffold/components/config.ts +++ b/packages/create-sitecore-jss/src/templates/nextjs/scripts/scaffold/components/config.ts @@ -1,19 +1,15 @@ -import { ComponentsFolderConfig } from './utils'; +/* eslint-disable */ +import { Config } from './utils'; import { componentTemplate } from './templates/component-src'; -// theme: {}, -// site: {}, -// helper: {}, - -const componentsFolderConfig: ComponentsFolderConfig = { - levels: [ { name: 'Shared', short: 'Components used by everyone' }, ], +const componentsFolderConfig: Config = { directories: [ - { name: 'default', short: 'The default component folder', path: '' } - ], - templates: [ - { name: 'default', short: 'The default component template', fileName: '[name].tsx', template: componentTemplate } + { name: 'default', path: '' } ], + templates: { + '[name].tsx': componentTemplate, + }, }; export default componentsFolderConfig; diff --git a/packages/create-sitecore-jss/src/templates/nextjs/scripts/scaffold/components/index.ts b/packages/create-sitecore-jss/src/templates/nextjs/scripts/scaffold/components/index.ts index 6ebc9700b9..3385b8572c 100644 --- a/packages/create-sitecore-jss/src/templates/nextjs/scripts/scaffold/components/index.ts +++ b/packages/create-sitecore-jss/src/templates/nextjs/scripts/scaffold/components/index.ts @@ -1,9 +1,9 @@ -import inquirer, { Question } from 'inquirer'; +import inquirer from 'inquirer'; import chalk from 'chalk'; import fs from 'fs'; import path from 'path'; -import { ComponentsFolder } from './utils'; -import componentConfig from './config'; +import { TemplateArgs, ConfigDirectory } from './utils'; +import config from './config'; // Matches component names that start with a capital letter, and contain only letters, number, // underscores, or dashes. Optionally, the component name can be preceded by a relative path @@ -11,96 +11,146 @@ const nameParamFormat = new RegExp(/^((?:[\w-]+\/)*)([A-Z][\w-]+)$/); const componentRootPath = 'src/components'; +interface ExpandedTemplateArgs extends TemplateArgs { + [name: string]: unknown; +} + /** * Force to use `crlf` line endings, we are using `crlf` across the project. * Replace: `lf` (\n), `cr` (\r) * @param {string} content */ - function editLineEndings(content: string) { +function editLineEndings(content: string) { return content.replace(/\r|\n/gm, '\r\n'); } -export const scaffoldCommand = async (componentName: string | undefined) => { - let answers: any = { - componentName - }; - +export const scaffoldCommand = async (componentName: string | undefined): Promise => { // Get the name - while (!answers.componentName) { - await inquirer.prompt({ - name: 'componentName', - message: 'The name for the component to generate?', - }, answers); - - const regExResult = nameParamFormat.exec(answers.componentName as string); + while (!componentName) { + await inquirer + .prompt({ + name: 'componentName', + message: 'The name for the component to generate?', + }) + .then((answers) => { + componentName = answers.componentName; + }); + + const regExResult = nameParamFormat.exec(componentName || ''); if (regExResult === null) { - console.log(chalk.red(`Component name should start with an uppercase letter and contain only letters, numbers, dashes, or underscores. If specifying a path, it must be relative to src/components`)); + console.log( + chalk.red( + `Component name should start with an uppercase letter and contain only letters, numbers, dashes, or underscores. If specifying a path, it must be relative to src/components` + ) + ); componentName = undefined; } } - let componentsFolder = new ComponentsFolder(componentConfig); + // Get the directory chain + let directoryOptions: ConfigDirectory[] = config.directories; + const directories: ConfigDirectory[] = []; + let prompt = 'directory'; + + while (directoryOptions.length > 0) { + let directory = directoryOptions[0]; + + if (directoryOptions.length > 1) { + await inquirer + .prompt({ + name: 'directory', + type: 'list', + message: `What ${prompt} should the component be created in?`, + choices: directoryOptions, + }) + .then((answers) => { + directory = directoryOptions?.find((_) => _.name === answers.directory) || directory; + }); + } - // Get the level - if (componentsFolder.levels.length > 1) { - await inquirer.prompt({ - name: 'level', - type: 'list', - message: 'What level should the component be created at?', - choices: componentsFolder.levels - }); - } else { - answers.level = componentsFolder.levels[0].name; + directories.push(directory); + prompt = `subdirectory of ${chalk.yellow(directory.name)}`; + directoryOptions = directory.directories || []; } - const level = componentsFolder.levels.find(_ => _.name === answers.level) || componentsFolder.levels[0]; - - // Get the directory - if (level.directories.length > 1) { - await inquirer.prompt({ - name: 'directory', - type: 'list', - message: 'What directory should the component be created in?', - choices: level.directories - }); - } else { - answers.directory = level.directories[0].name; - } - const directory = level.directories.find(_ => _.name === answers.directory) || level.directories[0]; - // Validate overwrite + let templateArgs: ExpandedTemplateArgs = { + componentName, + directories, + }; // Questions - let questions: Question[] = []; - - if (componentsFolder.questions.length > 0) { - questions.concat(componentsFolder.questions); - } - if (level.questions.length > 0) { - questions.concat(level.questions); + if (config.questions) { + await inquirer.prompt(config.questions).then((answers) => { + templateArgs = { + ...templateArgs, + ...answers, + }; + }); } - if (directory.questions.length > 0) { - questions.concat(directory.questions); + + // Pass any additional directory parameters to the templates + for (const directory of directories) { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { name, path, directories, templates, ...rest } = directory; + templateArgs = { + ...templateArgs, + ...rest, + }; } - if (questions.length > 1) { - await inquirer.prompt(questions, answers); + + // Add any additional templates defined at the directory level + let templates = { + ...config.templates, + }; + for (const directory of directories) { + templates = { + ...templates, + ...directory.templates, + }; } // Output directory let outputDir = componentRootPath; - if (level.path) { - outputDir = path.join(componentRootPath, level.path); + for (const directory of directories) { + outputDir = path.join(outputDir, directory.path); } - outputDir = path.join(outputDir, directory.path); + + if (Object.keys(templates).length > 1) { + outputDir = path.join(outputDir, componentName); + } + fs.mkdirSync(outputDir, { recursive: true }); // Generate files - for (const template of directory.templates) { - const fileName = template.fileName.replace('[name]', answers.componentName); + const created: string[] = []; + for (const name of Object.keys(templates)) { + const fileName = name.replace('[name]', componentName); const filePath = path.join(outputDir, fileName); - const templateContent = editLineEndings(template.template(answers)); + if (fs.existsSync(filePath)) { + const { overwrite } = await inquirer.prompt({ + name: 'overwrite', + type: 'confirm', + message: `The file ${chalk.yellow(fileName)} already exists, overwrite?`, + }); + + if (!overwrite) { + continue; + } + } + + const templateContent = editLineEndings(templates[name](templateArgs)); fs.writeFileSync(filePath, templateContent, 'utf8'); - console.log(chalk.green(`File ${fileName} has been scaffolded.`)); + created.push(filePath); + console.log(`Scaffolding of ${chalk.green(fileName)} complete.`); } -} + + if (created.length > 0) { + console.log(''); + console.log(chalk.green('Next steps:')); + for (const file of created) { + console.log(`* Implement ${chalk.green(file)}`); + } + } +}; diff --git a/packages/create-sitecore-jss/src/templates/nextjs/scripts/scaffold/components/utils.ts b/packages/create-sitecore-jss/src/templates/nextjs/scripts/scaffold/components/utils.ts index 99a2af0581..3e5e35d8d9 100644 --- a/packages/create-sitecore-jss/src/templates/nextjs/scripts/scaffold/components/utils.ts +++ b/packages/create-sitecore-jss/src/templates/nextjs/scripts/scaffold/components/utils.ts @@ -1,188 +1,27 @@ -import { Question } from 'inquirer'; +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { QuestionCollection } from 'inquirer'; // Template types export interface TemplateArgs { componentName: string; - directory: string; + directories: ConfigDirectory[]; } export type Template = (args: T) => string; // Configuration Types -type ComponentsFolderLevels = 'Shared' | 'Theme' | 'Site' | 'Helper'; - -export type ComponentsFolderConfig = { - levels: ComponentConfigLevel[]; - directories: ComponentConfigDirectory[]; - templates: ComponentConfigTemplate[]; - questions?: Question[]; -}; - -type ComponentConfigLevel = { - name: ComponentsFolderLevels; - short: string; - path?: string; - directories?: ComponentConfigOverwrite | ComponentConfigPatch | undefined; - templates?: ComponentConfigOverwrite | ComponentConfigPatch | undefined; - questions?: Question[] | undefined; +export type Config = { + directories: ConfigDirectory[]; + templates: ConfigTemplate; + questions?: QuestionCollection; }; -type ComponentConfigDirectory = { +export type ConfigDirectory = { name: string; - short: string; path: string; - templates?: ComponentConfigOverwrite | ComponentConfigPatch | undefined; - questions?: Question[] | undefined; -}; - -type ComponentConfigTemplate = { - name: string; - short: string; - fileName: string; - template: Template; - questions?: Question[] | undefined; -}; - -type ComponentConfigPatch = { - kind: "patch"; - add: Type[] | undefined; - remove: string[] | undefined; - replace: Type[] | undefined; -}; - -type ComponentConfigOverwrite = { - kind: "overwrite"; - overwrite: Type[]; -}; - -// Types used for the configuration after applying any overrides -type ComponentsFolderLevel = { - name: ComponentsFolderLevels; - short: string; - path?: string; - directories: ComponentsFolderDirectory[]; - questions: Question[]; -}; - -type ComponentsFolderDirectory = { - name: string; - short: string; - path: string; - templates: ComponentsFolderTemplate[]; - questions: Question[]; -}; - -type ComponentsFolderTemplate = { - name: string; - short: string; - fileName: string; - template: Template; - questions: Question[]; + directories?: ConfigDirectory[]; + templates?: ConfigTemplate; + [name: string]: unknown; }; -export class ComponentsFolder { - readonly levels: ComponentsFolderLevel[]; - readonly questions: Question[]; - - constructor(protected configuration: ComponentsFolderConfig) { - if (configuration.levels.length === 0) { - throw `No levels configured`; - } - - this.questions = configuration.questions || []; - - this.levels = []; - for (const levelConfig of configuration.levels) { - const level = this.buildLevel(levelConfig, configuration); - this.levels.push(level); - } - } - - private buildLevel(levelConfig: ComponentConfigLevel, rootConfig: ComponentsFolderConfig): ComponentsFolderLevel { - // The allowed directories is a combination of the root and level directories - let directoryConfigs = this.applyOverrides(rootConfig.directories, levelConfig.directories); - if (directoryConfigs.length === 0) { - throw `No directories configured for level ${levelConfig.name}`; - } - - const directories: ComponentsFolderDirectory[] = []; - for (const directoryConfig of directoryConfigs) { - const directory = this.buildDirectory(directoryConfig, levelConfig, rootConfig); - directories.push(directory); - } - - let level: ComponentsFolderLevel = { - name: levelConfig.name, - short: levelConfig.short, - path: levelConfig.path, - questions: levelConfig.questions || [], - directories, - }; - - return level; - } - - private buildDirectory(directoryConfig: ComponentConfigDirectory, levelConfig: ComponentConfigLevel, rootConfig: ComponentsFolderConfig): ComponentsFolderDirectory { - // The allowed templates is a combination of the root, level and directories - let templateConfigs = this.applyOverrides(rootConfig.templates, levelConfig.templates); - templateConfigs = this.applyOverrides(templateConfigs, directoryConfig.templates); - if (templateConfigs.length === 0) { - throw `No templates configured for level ${levelConfig.name} and directory ${directoryConfig.name}`; - } - - const templates: ComponentsFolderTemplate[] = []; - for (const templateConfig of templateConfigs) { - const template = this.buildTemplate(templateConfig); - templates.push(template); - } - - let directory: ComponentsFolderDirectory = { - name: directoryConfig.name, - short: directoryConfig.short, - path: directoryConfig.path, - questions: directoryConfig.questions || [], - templates, - }; - - return directory; - } - - private buildTemplate(templateConfig: ComponentConfigTemplate): ComponentsFolderTemplate { - let template: ComponentsFolderTemplate = { - name: templateConfig.name, - short: templateConfig.short, - fileName: templateConfig.fileName, - template: templateConfig.template, - questions: templateConfig.questions || [], - }; - - return template; - } - - private applyOverrides(base: Type[], override: ComponentConfigOverwrite | ComponentConfigPatch | undefined) { - let result = base; - if (override) { - if (override === undefined) { - // Do nothing - } else if (override.kind === "overwrite") { - result = override.overwrite; - } else if (override.kind === "patch") { - if (override.add) { - result.concat(override.add); - } - if (override.remove) { - const remove = override.remove; - result = result.filter((_) => !remove.indexOf(_.name)) - } - if (override.replace) { - const replace = override.replace; - result = result.map(_ => { - const index = replace.findIndex(__ => __.name === _.name); - return index >= 0 ? replace[index] : _; - }); - } - } - } - return result; - } -} +export type ConfigTemplate = Record>;