Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add cli command for page creation #9

Merged
merged 1 commit into from
Nov 5, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.`;
}