Skip to content

Commit

Permalink
Merge pull request #9 from Shopify/@cartogram/cli-page-command
Browse files Browse the repository at this point in the history
  • Loading branch information
cartogram authored Nov 5, 2021
2 parents 60bbfca + 7c6a478 commit 28165a8
Show file tree
Hide file tree
Showing 10 changed files with 157 additions and 37 deletions.
1 change: 1 addition & 0 deletions packages/cli/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
39 changes: 3 additions & 36 deletions packages/cli/src/commands/create/component/component.ts
Original file line number Diff line number Diff line change
@@ -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',
});
Expand Down Expand Up @@ -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('.');
}
1 change: 1 addition & 0 deletions packages/cli/src/commands/create/page/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export {page as default} from './page';
44 changes: 44 additions & 0 deletions packages/cli/src/commands/create/page/page.ts
Original file line number Diff line number Diff line change
@@ -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)
),
})
);
}
9 changes: 9 additions & 0 deletions packages/cli/src/commands/create/page/templates/page-jsx.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export default function ({name, path}: {name: string; path: string}) {
return `
export default function ${name}({request, response, ...serverState}) {
return (
<div>${name} component at \`${path}\`</div>
);
}
`;
}
59 changes: 59 additions & 0 deletions packages/cli/src/commands/create/page/tests/page.test.ts
Original file line number Diff line number Diff line change
@@ -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 (
<div>Products component at \`src/pages/products.server.jsx\`</div>
);
}
`
);
});
});

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 (
<div>Collections component at \`src/pages/collections.server.tsx\`</div>
);
}
`
);
});
});

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 (
<div>ProductDetails component at \`src/pages/products/[handle].server.tsx\`</div>
);
}
`
);
});
});
});
2 changes: 1 addition & 1 deletion packages/cli/src/testing/testing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, string | boolean | null>;

interface App {
Expand Down
6 changes: 6 additions & 0 deletions packages/cli/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@ export interface Env<Context = {}> {
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[];
Expand Down
1 change: 1 addition & 0 deletions packages/cli/src/utilities/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
32 changes: 32 additions & 0 deletions packages/cli/src/utilities/react.ts
Original file line number Diff line number Diff line change
@@ -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.`;
}

0 comments on commit 28165a8

Please sign in to comment.