From 7c6a478a7d91350d9fbc41a54cf28430224bc40c Mon Sep 17 00:00:00 2001 From: Matt Seccafien Date: Fri, 5 Nov 2021 08:27:39 +0100 Subject: [PATCH] feat: cli page command --- packages/cli/CHANGELOG.md | 1 + .../commands/create/component/component.ts | 39 +----------- .../cli/src/commands/create/page/index.ts | 1 + packages/cli/src/commands/create/page/page.ts | 44 ++++++++++++++ .../create/page/templates/page-jsx.ts | 9 +++ .../commands/create/page/tests/page.test.ts | 59 +++++++++++++++++++ packages/cli/src/testing/testing.ts | 2 +- packages/cli/src/types.ts | 6 ++ packages/cli/src/utilities/index.ts | 1 + packages/cli/src/utilities/react.ts | 32 ++++++++++ 10 files changed, 157 insertions(+), 37 deletions(-) create mode 100644 packages/cli/src/commands/create/page/index.ts create mode 100644 packages/cli/src/commands/create/page/page.ts create mode 100644 packages/cli/src/commands/create/page/templates/page-jsx.ts create mode 100644 packages/cli/src/commands/create/page/tests/page.test.ts create mode 100644 packages/cli/src/utilities/react.ts diff --git a/packages/cli/CHANGELOG.md b/packages/cli/CHANGELOG.md index cb67f7b15a..9c34861a6b 100644 --- a/packages/cli/CHANGELOG.md +++ b/packages/cli/CHANGELOG.md @@ -7,6 +7,7 @@ and adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). ## Unreleased +- Add create page command [#810](https://github.com/Shopify/hydrogen/pull/810) - Add create component command [#806](https://github.com/Shopify/hydrogen/pull/806) - Add init command [#791](https://github.com/Shopify/hydrogen/pull/791) diff --git a/packages/cli/src/commands/create/component/component.ts b/packages/cli/src/commands/create/component/component.ts index fec127b7f1..9b931b3ffa 100644 --- a/packages/cli/src/commands/create/component/component.ts +++ b/packages/cli/src/commands/create/component/component.ts @@ -1,19 +1,12 @@ -import {pascalCase} from 'change-case'; -import {Env} from '../../../types'; - -export enum ComponentType { - Client = 'React client component', - Shared = 'React shared component', - Server = 'React server component', -} - +import {Env, ComponentType} from '../../../types'; +import {componentName, validComponentName} from '../../../utilities'; /** * Scaffold a new React component. */ export async function component(env: Env) { const {ui, fs, workspace} = env; const name = await ui.ask('What do you want to name this component?', { - validate: validateComponentName, + validate: validComponentName, default: 'ProductCard', name: 'name', }); @@ -41,29 +34,3 @@ export async function component(env: Env) { }) ); } - -function validateComponentName(name: string) { - const suggested = pascalCase(name); - if (name === suggested) { - return true; - } - - return `Invalid component name. Try ${suggested} instead.`; -} - -function getReactComponentTypeSuffix(component: ComponentType) { - switch (component) { - case ComponentType.Client: - return 'client'; - case ComponentType.Server: - return 'server'; - default: - return null; - } -} - -function componentName(name: string, type: ComponentType, extension: string) { - return [name, getReactComponentTypeSuffix(type), extension] - .filter((fp) => fp) - .join('.'); -} diff --git a/packages/cli/src/commands/create/page/index.ts b/packages/cli/src/commands/create/page/index.ts new file mode 100644 index 0000000000..7bbd8e6670 --- /dev/null +++ b/packages/cli/src/commands/create/page/index.ts @@ -0,0 +1 @@ +export {page as default} from './page'; diff --git a/packages/cli/src/commands/create/page/page.ts b/packages/cli/src/commands/create/page/page.ts new file mode 100644 index 0000000000..0604c69ed1 --- /dev/null +++ b/packages/cli/src/commands/create/page/page.ts @@ -0,0 +1,44 @@ +import {Env, ComponentType} from '../../../types'; +import {componentName, validComponentName} from '../../../utilities'; + +const PAGES_DIRECTORY = 'src/pages'; + +/** + * Scaffold a new Hydrogen page. + */ +export async function page(env: Env) { + const {ui, fs, workspace} = env; + + const name = await ui.ask('What do you want to name this page?', { + validate: validComponentName, + default: 'Products', + name: 'name', + }); + + const url = await ui.ask('What is the url path to this page?', { + default: '/products', + name: 'name', + }); + + const urlSegments = url.split('/'); + const lastUrlSegment = urlSegments.pop() || ''; + const extension = (await workspace.isTypeScript) ? 'tsx' : 'jsx'; + + const path = fs.join( + workspace.root(), + PAGES_DIRECTORY, + ...urlSegments, + componentName(lastUrlSegment, ComponentType.Server, extension) + ); + + fs.write( + path, + (await import('./templates/page-jsx')).default({ + name, + path: fs.join( + PAGES_DIRECTORY, + componentName(url, ComponentType.Server, extension) + ), + }) + ); +} diff --git a/packages/cli/src/commands/create/page/templates/page-jsx.ts b/packages/cli/src/commands/create/page/templates/page-jsx.ts new file mode 100644 index 0000000000..ab0b65d08b --- /dev/null +++ b/packages/cli/src/commands/create/page/templates/page-jsx.ts @@ -0,0 +1,9 @@ +export default function ({name, path}: {name: string; path: string}) { + return ` +export default function ${name}({request, response, ...serverState}) { + return ( +
${name} component at \`${path}\`
+ ); +} +`; +} diff --git a/packages/cli/src/commands/create/page/tests/page.test.ts b/packages/cli/src/commands/create/page/tests/page.test.ts new file mode 100644 index 0000000000..d04609c8a6 --- /dev/null +++ b/packages/cli/src/commands/create/page/tests/page.test.ts @@ -0,0 +1,59 @@ +import {withCli} from '../../../../testing'; + +describe('page', () => { + it('scaffolds a basic JSX page with a name', async () => { + await withCli(async ({run, fs}) => { + await run('create page', { + name: 'Products', + url: 'products', + }); + + expect(await fs.read('src/pages/products.server.jsx')).toBe( + `export default function Products({request, response, ...serverState}) { + return ( +
Products component at \`src/pages/products.server.jsx\`
+ ); +} +` + ); + }); + }); + + it('scaffolds a basic TSX page with a name when a tsconfig exists', async () => { + await withCli(async ({run, fs}) => { + await fs.write('tsconfig.json', JSON.stringify({}, null, 2)); + await run('create page', { + name: 'Collections', + url: 'collections', + }); + + expect(await fs.read('src/pages/collections.server.tsx')).toBe( + `export default function Collections({request, response, ...serverState}) { + return ( +
Collections component at \`src/pages/collections.server.tsx\`
+ ); +} +` + ); + }); + }); + + it('supports nested and dynamic components', async () => { + await withCli(async ({run, fs}) => { + await fs.write('tsconfig.json', JSON.stringify({}, null, 2)); + await run('create page', { + name: 'ProductDetails', + url: 'products/[handle]', + }); + + expect(await fs.read('src/pages/products/[handle].server.tsx')).toBe( + `export default function ProductDetails({request, response, ...serverState}) { + return ( +
ProductDetails component at \`src/pages/products/[handle].server.tsx\`
+ ); +} +` + ); + }); + }); +}); diff --git a/packages/cli/src/testing/testing.ts b/packages/cli/src/testing/testing.ts index 7322f46de8..68c92328ad 100644 --- a/packages/cli/src/testing/testing.ts +++ b/packages/cli/src/testing/testing.ts @@ -24,7 +24,7 @@ import getPort from 'get-port'; const INPUT_TIMEOUT = 500; const execPromise = promisify(exec); -type Command = 'create' | 'create component'; +type Command = 'create' | 'create component' | 'create page'; type Input = Record; interface App { diff --git a/packages/cli/src/types.ts b/packages/cli/src/types.ts index 9480dacc53..507c76aa5b 100644 --- a/packages/cli/src/types.ts +++ b/packages/cli/src/types.ts @@ -11,6 +11,12 @@ export interface Env { context?: Context; } +export enum ComponentType { + Client = 'React client component', + Shared = 'React shared component', + Server = 'React server component', +} + export interface TemplateOptions { ifFeature(feature: Feature, output: string): string; features: Feature[]; diff --git a/packages/cli/src/utilities/index.ts b/packages/cli/src/utilities/index.ts index a10f34f716..e1e2aaafa4 100644 --- a/packages/cli/src/utilities/index.ts +++ b/packages/cli/src/utilities/index.ts @@ -5,6 +5,7 @@ import minimist from 'minimist'; export * from './error'; export * from './feature'; export {merge} from './merge'; +export {componentName, validComponentName} from './react'; const DEFAULT_SUBCOMMANDS = { create: 'app', diff --git a/packages/cli/src/utilities/react.ts b/packages/cli/src/utilities/react.ts new file mode 100644 index 0000000000..561d97feed --- /dev/null +++ b/packages/cli/src/utilities/react.ts @@ -0,0 +1,32 @@ +import {pascalCase} from 'change-case'; +import {ComponentType} from '../types'; + +function getReactComponentTypeSuffix(component: ComponentType) { + switch (component) { + case ComponentType.Client: + return 'client'; + case ComponentType.Server: + return 'server'; + default: + return null; + } +} + +export function componentName( + name: string, + type: ComponentType, + extension: string +) { + return [name, getReactComponentTypeSuffix(type), extension] + .filter((fp) => fp) + .join('.'); +} + +export function validComponentName(name: string) { + const suggested = pascalCase(name); + if (name === suggested) { + return true; + } + + return `Invalid component name. Try ${suggested} instead.`; +}