diff --git a/packages/create-sitecore-jss/src/templates/nextjs/package.json b/packages/create-sitecore-jss/src/templates/nextjs/package.json index 4cd2a11f67..938f3c5211 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..48bc088f01 --- /dev/null +++ b/packages/create-sitecore-jss/src/templates/nextjs/scripts/scaffold.ts @@ -0,0 +1,13 @@ +/* eslint-disable @typescript-eslint/no-var-requires */ +import { scaffoldCommand } from './scaffold/components'; +const { program } = require('commander'); + +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..17a4ab204e --- /dev/null +++ b/packages/create-sitecore-jss/src/templates/nextjs/scripts/scaffold/components/config.ts @@ -0,0 +1,15 @@ +/* eslint-disable */ +import { Config } from './utils'; + +import { componentTemplate } from './templates/component-src'; + +const componentsFolderConfig: Config = { + directories: [ + { 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 new file mode 100644 index 0000000000..3385b8572c --- /dev/null +++ b/packages/create-sitecore-jss/src/templates/nextjs/scripts/scaffold/components/index.ts @@ -0,0 +1,156 @@ +import inquirer from 'inquirer'; +import chalk from 'chalk'; +import fs from 'fs'; +import path from 'path'; +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 +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) { + return content.replace(/\r|\n/gm, '\r\n'); +} + +export const scaffoldCommand = async (componentName: string | undefined): Promise => { + // Get the name + 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` + ) + ); + componentName = undefined; + } + } + + // 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; + }); + } + + directories.push(directory); + prompt = `subdirectory of ${chalk.yellow(directory.name)}`; + directoryOptions = directory.directories || []; + } + + let templateArgs: ExpandedTemplateArgs = { + componentName, + directories, + }; + + // Questions + if (config.questions) { + await inquirer.prompt(config.questions).then((answers) => { + templateArgs = { + ...templateArgs, + ...answers, + }; + }); + } + + // 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, + }; + } + + // 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; + for (const directory of directories) { + outputDir = path.join(outputDir, directory.path); + } + + if (Object.keys(templates).length > 1) { + outputDir = path.join(outputDir, componentName); + } + + fs.mkdirSync(outputDir, { recursive: true }); + + // Generate files + const created: string[] = []; + for (const name of Object.keys(templates)) { + const fileName = name.replace('[name]', componentName); + const filePath = path.join(outputDir, fileName); + + 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'); + 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/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..3e5e35d8d9 --- /dev/null +++ b/packages/create-sitecore-jss/src/templates/nextjs/scripts/scaffold/components/utils.ts @@ -0,0 +1,27 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { QuestionCollection } from 'inquirer'; + +// Template types +export interface TemplateArgs { + componentName: string; + directories: ConfigDirectory[]; +} + +export type Template = (args: T) => string; + +// Configuration Types +export type Config = { + directories: ConfigDirectory[]; + templates: ConfigTemplate; + questions?: QuestionCollection; +}; + +export type ConfigDirectory = { + name: string; + path: string; + directories?: ConfigDirectory[]; + templates?: ConfigTemplate; + [name: string]: unknown; +}; + +export type ConfigTemplate = Record>;