Skip to content
Draft
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
3 changes: 0 additions & 3 deletions packages/cli/json-output-command-exceptions.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,6 @@
const commandExceptions = [
// Existing finite commands awaiting migration. Remove entries as they adopt typed JSON output.
// Do not add new finite commands to this section.
'packages/cli/src/commands/hydrogen/g.ts',
'packages/cli/src/commands/hydrogen/generate/route.ts',
'packages/cli/src/commands/hydrogen/generate/routes.ts',
'packages/cli/src/commands/hydrogen/init.ts',
'packages/cli/src/commands/hydrogen/link.ts',
'packages/cli/src/commands/hydrogen/list.ts',
Expand Down
23 changes: 21 additions & 2 deletions packages/cli/oclif.manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -1045,7 +1045,7 @@
"required": true
}
},
"description": "Generates a standard Shopify route.",
"description": "Generates a set of default routes from the starter template.\n\nUse `--json-schema` to print the result, error, and event schemas.\n\nOutput from `--json` conforms to the `HydrogenGenerateRoutesResult` schema.\n\n```json\n{\n \"type\": \"object\",\n \"properties\": {\n \"routes\": {\n \"type\": \"array\",\n \"items\": {\n \"$ref\": \"#/definitions/Route\"\n }\n },\n \"routeGroups\": {\n \"type\": \"object\",\n \"additionalProperties\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n }\n },\n \"isTypescript\": {\n \"type\": \"boolean\"\n }\n },\n \"required\": [\n \"routes\",\n \"routeGroups\",\n \"isTypescript\"\n ],\n \"additionalProperties\": false,\n \"title\": \"HydrogenGenerateRoutesResult\",\n \"definitions\": {\n \"Route\": {\n \"type\": \"object\",\n \"properties\": {\n \"sourceRoute\": {\n \"type\": \"string\"\n },\n \"destinationRoute\": {\n \"type\": \"string\"\n },\n \"operation\": {\n \"type\": \"string\",\n \"enum\": [\n \"created\",\n \"skipped\",\n \"replaced\"\n ]\n }\n },\n \"required\": [\n \"sourceRoute\",\n \"destinationRoute\",\n \"operation\"\n ],\n \"additionalProperties\": false\n }\n },\n \"$schema\": \"http://json-schema.org/draft-07/schema#\"\n}\n```",
"flags": {
"json-schema": {
"description": "Print the command's JSON schemas.",
Expand All @@ -1054,6 +1054,15 @@
"allowNo": false,
"type": "boolean"
},
"json": {
"char": "j",
"description": "Output the result as JSON. Automatically disables color output.",
"env": "SHOPIFY_FLAG_JSON",
"hidden": false,
"name": "json",
"allowNo": false,
"type": "boolean"
},
"adapter": {
"description": "React Router adapter used in the route. The default is `react-router`.",
"env": "SHOPIFY_HYDROGEN_FLAG_ADAPTER",
Expand Down Expand Up @@ -1115,7 +1124,7 @@
"hydrogen:generate:routes": {
"aliases": [],
"args": {},
"description": "Generates all supported standard shopify routes.",
"description": "Generates all supported standard shopify routes.\n\nUse `--json-schema` to print the result, error, and event schemas.\n\nOutput from `--json` conforms to the `HydrogenGenerateRoutesResult` schema.\n\n```json\n{\n \"type\": \"object\",\n \"properties\": {\n \"routes\": {\n \"type\": \"array\",\n \"items\": {\n \"$ref\": \"#/definitions/Route\"\n }\n },\n \"routeGroups\": {\n \"type\": \"object\",\n \"additionalProperties\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n }\n },\n \"isTypescript\": {\n \"type\": \"boolean\"\n }\n },\n \"required\": [\n \"routes\",\n \"routeGroups\",\n \"isTypescript\"\n ],\n \"additionalProperties\": false,\n \"title\": \"HydrogenGenerateRoutesResult\",\n \"definitions\": {\n \"Route\": {\n \"type\": \"object\",\n \"properties\": {\n \"sourceRoute\": {\n \"type\": \"string\"\n },\n \"destinationRoute\": {\n \"type\": \"string\"\n },\n \"operation\": {\n \"type\": \"string\",\n \"enum\": [\n \"created\",\n \"skipped\",\n \"replaced\"\n ]\n }\n },\n \"required\": [\n \"sourceRoute\",\n \"destinationRoute\",\n \"operation\"\n ],\n \"additionalProperties\": false\n }\n },\n \"$schema\": \"http://json-schema.org/draft-07/schema#\"\n}\n```",
"flags": {
"json-schema": {
"description": "Print the command's JSON schemas.",
Expand All @@ -1124,6 +1133,15 @@
"allowNo": false,
"type": "boolean"
},
"json": {
"char": "j",
"description": "Output the result as JSON. Automatically disables color output.",
"env": "SHOPIFY_FLAG_JSON",
"hidden": false,
"name": "json",
"allowNo": false,
"type": "boolean"
},
"adapter": {
"description": "React Router adapter used in the route. The default is `react-router`.",
"env": "SHOPIFY_HYDROGEN_FLAG_ADAPTER",
Expand Down Expand Up @@ -1172,6 +1190,7 @@
"pluginType": "core",
"strict": true,
"enableJsonFlag": false,
"descriptionWithMarkdown": "Generates all supported standard shopify routes.",
"isESM": true,
"relativePath": [
"dist",
Expand Down
2 changes: 2 additions & 0 deletions packages/cli/src/commands/hydrogen/g.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import Command from '@shopify/cli-kit/node/base-command';
import GenerateRoute from './generate/route.js';

// Hidden compatibility alias; the JSON contract belongs to generate route.
// eslint-disable-next-line @shopify/cli/command-json-output
export default class GenerateRouteShortcut extends Command {
static description =
'Shortcut for `hydrogen generate`. See `hydrogen generate --help` for more information.';
Expand Down
77 changes: 76 additions & 1 deletion packages/cli/src/commands/hydrogen/generate/route.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import GenerateRoute, {runGenerate} from './route.js';
import GenerateRoutes from './routes.js';
import {captureJsonOutput} from '../../../../tests/output.js';
import {describe, it, expect, vi, beforeEach, afterEach} from 'vitest';
import {mockAndCaptureOutput} from '@shopify/cli-kit/node/testing/output';
import {runGenerate} from './route.js';
import {generateRoutes} from '../../../lib/setups/routes/generate.js';

describe('runGenerate', () => {
Expand Down Expand Up @@ -48,3 +50,76 @@ describe('runGenerate', () => {
expect(outputMock.info()).toMatch(/2 of 3 routes/i);
});
});

it.each([GenerateRoute, GenerateRoutes])(
'exposes JSON on route commands: %s',
(command) => {
expect(command.flags.json).toBeDefined();
expect(command.description).toContain(command.jsonOutputSchema.name);
},
);

it('encodes created, replaced and skipped routes without the success banner', async () => {
const result = {
isTypescript: true,
routeGroups: {cart: ['cart.tsx']},
routes: [
{
sourceRoute: 'cart.tsx',
destinationRoute: 'app/routes/cart.tsx',
operation: 'created' as const,
},
{
sourceRoute: 'account.tsx',
destinationRoute: 'app/routes/account.tsx',
operation: 'replaced' as const,
},
{
sourceRoute: 'search.tsx',
destinationRoute: 'app/routes/search.tsx',
operation: 'skipped' as const,
},
],
};
vi.mocked(generateRoutes).mockResolvedValue({
...result,
formatOptions: {} as any,
});
const {stdout, stderr} = await captureJsonOutput(() =>
runGenerate({routeName: 'all', directory: '/project'}),
);
expect(JSON.parse(stdout)).toEqual(result);
expect(stderr).toBe('');
expect(() =>
GenerateRoute.jsonOutputSchema.encode({
...result,
isTypescript: 'yes',
} as any),
).toThrow();
});

it('forwards all-route flags without reconstructing false boolean arguments', async () => {
vi.mocked(generateRoutes).mockResolvedValue({
routes: [],
routeGroups: {},
isTypescript: false,
formatOptions: {} as any,
});
const command = new GenerateRoutes([], {} as any);
const parse = vi
.spyOn(command as any, 'parse')
.mockResolvedValue({flags: {json: true, path: '/project', force: false}});
try {
const {stdout} = await captureJsonOutput(() => command.run());
expect(JSON.parse(stdout)).toEqual({
routes: [],
routeGroups: {},
isTypescript: false,
});
expect(generateRoutes).toHaveBeenLastCalledWith(
expect.objectContaining({routeName: 'all', force: false}),
);
} finally {
parse.mockRestore();
}
});
55 changes: 39 additions & 16 deletions packages/cli/src/commands/hydrogen/generate/route.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import {writeJsonResult} from '../../../lib/json-output.js';
import {jsonFlag} from '@shopify/cli-kit/node/cli';
import {generateRoutesJsonOutputSchema} from '../../../lib/setups/routes/types.js';
import Command from '@shopify/cli-kit/node/base-command';
import {resolvePath} from '@shopify/cli-kit/node/path';
import {renderSuccess} from '../../../lib/ui.js';
Expand All @@ -15,9 +18,14 @@ import {
import {isV1RouteConventionInstalled} from '../../../lib/remix-version-interop.js';

export default class GenerateRoute extends Command {
static get jsonOutputSchema(): typeof generateRoutesJsonOutputSchema {
return generateRoutesJsonOutputSchema;
}

static descriptionWithMarkdown = `Generates a set of default routes from the starter template.`;
static description = 'Generates a standard Shopify route.';
static description = this.descriptionForHelp();
static flags = {
...jsonFlag,
adapter: Flags.string({
description:
'React Router adapter used in the route. The default is `react-router`.',
Expand Down Expand Up @@ -56,28 +64,43 @@ export default class GenerateRoute extends Command {

const directory = flags.path ? resolvePath(flags.path) : process.cwd();

await runGenerate({
...flags,
directory,
routeName,
localePrefix: flags['locale-param'],
});
await runGenerate(
{
...flags,
directory,
routeName,
localePrefix: flags['locale-param'],
},
flags.json,
);
}
}

export async function runGenerate(options: {
routeName: string;
directory: string;
adapter?: string;
typescript?: boolean;
force?: boolean;
localePrefix?: string;
}) {
const {routes} = await generateRoutes({
export async function runGenerate(
options: {
routeName: string;
directory: string;
adapter?: string;
typescript?: boolean;
force?: boolean;
localePrefix?: string;
},
json?: boolean,
) {
const {routes, routeGroups, isTypescript} = await generateRoutes({
...options,
v1RouteConvention: isV1RouteConventionInstalled(),
});

const result = {routes, routeGroups, isTypescript};
if (!writeJsonResult(generateRoutesJsonOutputSchema, result, json))
renderGeneratedRoutes(result);
return result;
}

export function renderGeneratedRoutes({
routes,
}: import('../../../lib/setups/routes/types.js').GenerateRoutesResult) {
const padEnd =
3 +
routes.reduce(
Expand Down
36 changes: 20 additions & 16 deletions packages/cli/src/commands/hydrogen/generate/routes.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,29 @@
import GenerateRoute from './route.js';
import {jsonFlag} from '@shopify/cli-kit/node/cli';
import {resolvePath} from '@shopify/cli-kit/node/path';
import GenerateRoute, {runGenerate} from './route.js';
import Command from '@shopify/cli-kit/node/base-command';
import {generateRoutesJsonOutputSchema} from '../../../lib/setups/routes/types.js';

export default class GenerateRoutes extends Command {
static description = 'Generates all supported standard shopify routes.';

static get jsonOutputSchema(): typeof generateRoutesJsonOutputSchema {
return generateRoutesJsonOutputSchema;
}
static descriptionWithMarkdown =
'Generates all supported standard shopify routes.';
static description = this.descriptionForHelp();
static hidden: true;

static flags = GenerateRoute.flags;
static flags = {...GenerateRoute.flags, ...jsonFlag};

async run(): Promise<void> {
const {flags} = await this.parse(GenerateRoutes);

await GenerateRoute.run([
'all',
...Object.entries(flags).map(([name, value]) => {
if (value === true) {
return `--${name}`;
}

return `--${name}=${value}`;
}),
]);
await runGenerate(
{
...flags,
directory: flags.path ? resolvePath(flags.path) : process.cwd(),
routeName: 'all',
localePrefix: flags['locale-param'],
},
flags.json,
);
}
}
23 changes: 23 additions & 0 deletions packages/cli/src/lib/setups/routes/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import {
defineJsonOutputSchema,
type InferJsonOutputSchema,
} from '@shopify/cli-kit/node/json-output-schema';
import {zod} from '@shopify/cli-kit/node/schema';

const Route = zod.object({
sourceRoute: zod.string(),
destinationRoute: zod.string(),
operation: zod.enum(['created', 'skipped', 'replaced']),
});
export const generateRoutesJsonOutputSchema = defineJsonOutputSchema({
name: 'HydrogenGenerateRoutesResult',
schema: zod.object({
routes: zod.array(Route),
routeGroups: zod.record(zod.array(zod.string())),
isTypescript: zod.boolean(),
}),
definitions: {Route},
});
export type GenerateRoutesResult = InferJsonOutputSchema<
typeof generateRoutesJsonOutputSchema
>;
Loading