From c18e3020df94e5cf8bf6bad3e8bc16994dd20645 Mon Sep 17 00:00:00 2001 From: Gonzalo Riestra Date: Mon, 28 Sep 2026 16:44:59 +0200 Subject: [PATCH] Add typed JSON output to Hydrogen setup commands --- .../cli/json-output-command-exceptions.cjs | 4 - packages/cli/oclif.manifest.json | 46 +++++- packages/cli/src/commands/hydrogen/setup.ts | 78 ++++++--- .../cli/src/commands/hydrogen/setup/css.ts | 87 +++++++--- .../hydrogen/setup/json-output.test.ts | 150 ++++++++++++++++++ .../src/commands/hydrogen/setup/markets.ts | 48 +++++- .../cli/src/commands/hydrogen/setup/vite.ts | 57 ++++++- packages/cli/src/lib/setups/types.ts | 61 +++++++ 8 files changed, 465 insertions(+), 66 deletions(-) create mode 100644 packages/cli/src/commands/hydrogen/setup/json-output.test.ts create mode 100644 packages/cli/src/lib/setups/types.ts diff --git a/packages/cli/json-output-command-exceptions.cjs b/packages/cli/json-output-command-exceptions.cjs index 834ed51025..f3954da425 100644 --- a/packages/cli/json-output-command-exceptions.cjs +++ b/packages/cli/json-output-command-exceptions.cjs @@ -2,10 +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/setup.ts', - 'packages/cli/src/commands/hydrogen/setup/css.ts', - 'packages/cli/src/commands/hydrogen/setup/markets.ts', - 'packages/cli/src/commands/hydrogen/setup/vite.ts', 'packages/cli/src/commands/hydrogen/shortcut.ts', 'packages/cli/src/commands/hydrogen/upgrade.ts', diff --git a/packages/cli/oclif.manifest.json b/packages/cli/oclif.manifest.json index 646f5f900c..02aab4ef9c 100644 --- a/packages/cli/oclif.manifest.json +++ b/packages/cli/oclif.manifest.json @@ -1733,7 +1733,7 @@ "hydrogen:setup": { "aliases": [], "args": {}, - "description": "Scaffold routes and core functionality.", + "description": "Scaffold routes and core functionality.\n\nUse `--json-schema` to print the result, error, and event schemas.\n\nOutput from `--json` conforms to the `HydrogenSetupResult` schema.\n\n```json\n{\n \"type\": \"object\",\n \"properties\": {\n \"directory\": {\n \"type\": \"string\"\n },\n \"name\": {\n \"type\": \"string\"\n },\n \"location\": {\n \"type\": \"string\"\n },\n \"i18n\": {\n \"type\": \"string\",\n \"enum\": [\n \"subfolders\",\n \"domains\",\n \"subdomains\"\n ]\n },\n \"routes\": {\n \"type\": \"object\",\n \"additionalProperties\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n }\n },\n \"shortcut\": {\n \"type\": \"boolean\"\n }\n },\n \"required\": [\n \"directory\",\n \"name\",\n \"location\",\n \"shortcut\"\n ],\n \"additionalProperties\": false,\n \"title\": \"HydrogenSetupResult\",\n \"$schema\": \"http://json-schema.org/draft-07/schema#\"\n}\n```", "flags": { "json-schema": { "description": "Print the command's JSON schemas.", @@ -1742,6 +1742,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" + }, "path": { "description": "The path to the directory of the Hydrogen storefront. Defaults to the current directory where the command is run.", "env": "SHOPIFY_HYDROGEN_FLAG_PATH", @@ -1789,6 +1798,7 @@ "pluginType": "core", "strict": true, "enableJsonFlag": false, + "descriptionWithMarkdown": "Scaffold routes and core functionality.", "isESM": true, "relativePath": [ "dist", @@ -1811,7 +1821,7 @@ ] } }, - "description": "Setup CSS strategies for your project.", + "description": "Adds support for certain CSS strategies to your project.\n\nUse `--json-schema` to print the result, error, and event schemas.\n\nOutput from `--json` conforms to the `HydrogenSetupCssResult` schema.\n\n```json\n{\n \"type\": \"object\",\n \"properties\": {\n \"status\": {\n \"type\": \"string\",\n \"enum\": [\n \"configured\",\n \"built-in\",\n \"cancelled\"\n ]\n },\n \"directory\": {\n \"type\": \"string\"\n },\n \"strategy\": {\n \"type\": \"string\",\n \"enum\": [\n \"tailwind\",\n \"vanilla-extract\",\n \"css-modules\",\n \"postcss\"\n ]\n },\n \"files\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n },\n \"dependenciesInstalled\": {\n \"type\": \"boolean\"\n },\n \"needsNpmReinstall\": {\n \"type\": \"boolean\"\n }\n },\n \"required\": [\n \"status\",\n \"directory\",\n \"strategy\",\n \"files\",\n \"dependenciesInstalled\",\n \"needsNpmReinstall\"\n ],\n \"additionalProperties\": false,\n \"title\": \"HydrogenSetupCssResult\",\n \"$schema\": \"http://json-schema.org/draft-07/schema#\"\n}\n```", "flags": { "json-schema": { "description": "Print the command's JSON schemas.", @@ -1820,6 +1830,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" + }, "path": { "description": "The path to the directory of the Hydrogen storefront. Defaults to the current directory where the command is run.", "env": "SHOPIFY_HYDROGEN_FLAG_PATH", @@ -1875,7 +1894,7 @@ ] } }, - "description": "Setup support for multiple markets in your project.", + "description": "Adds support for multiple \"markets\" (https://shopify.dev/docs/custom-storefronts/hydrogen/markets) to your project by using the URL structure.\n\nUse `--json-schema` to print the result, error, and event schemas.\n\nOutput from `--json` conforms to the `HydrogenSetupMarketsResult` schema.\n\n```json\n{\n \"type\": \"object\",\n \"properties\": {\n \"directory\": {\n \"type\": \"string\"\n },\n \"strategy\": {\n \"type\": \"string\",\n \"enum\": [\n \"subfolders\",\n \"domains\",\n \"subdomains\"\n ]\n },\n \"serverEntryPoint\": {\n \"type\": \"string\"\n }\n },\n \"required\": [\n \"directory\",\n \"strategy\"\n ],\n \"additionalProperties\": false,\n \"title\": \"HydrogenSetupMarketsResult\",\n \"$schema\": \"http://json-schema.org/draft-07/schema#\"\n}\n```", "flags": { "json-schema": { "description": "Print the command's JSON schemas.", @@ -1884,6 +1903,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" + }, "path": { "description": "The path to the directory of the Hydrogen storefront. Defaults to the current directory where the command is run.", "env": "SHOPIFY_HYDROGEN_FLAG_PATH", @@ -1914,7 +1942,7 @@ "hydrogen:setup:vite": { "aliases": [], "args": {}, - "description": "EXPERIMENTAL: Upgrades the project to use Vite.", + "description": "EXPERIMENTAL: Upgrades the project to use Vite.\n\nUse `--json-schema` to print the result, error, and event schemas.\n\nOutput from `--json` conforms to the `HydrogenSetupViteResult` schema.\n\n```json\n{\n \"type\": \"object\",\n \"properties\": {\n \"directory\": {\n \"type\": \"string\"\n },\n \"viteConfig\": {\n \"type\": \"string\"\n },\n \"serverEntryPoint\": {\n \"type\": \"string\"\n },\n \"dependenciesInstalled\": {\n \"type\": \"boolean\",\n \"const\": true\n },\n \"needsMdxSetup\": {\n \"type\": \"boolean\"\n }\n },\n \"required\": [\n \"directory\",\n \"viteConfig\",\n \"serverEntryPoint\",\n \"dependenciesInstalled\",\n \"needsMdxSetup\"\n ],\n \"additionalProperties\": false,\n \"title\": \"HydrogenSetupViteResult\",\n \"$schema\": \"http://json-schema.org/draft-07/schema#\"\n}\n```", "flags": { "json-schema": { "description": "Print the command's JSON schemas.", @@ -1923,6 +1951,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" + }, "path": { "description": "The path to the directory of the Hydrogen storefront. Defaults to the current directory where the command is run.", "env": "SHOPIFY_HYDROGEN_FLAG_PATH", @@ -1940,6 +1977,7 @@ "pluginType": "core", "strict": true, "enableJsonFlag": false, + "descriptionWithMarkdown": "EXPERIMENTAL: Upgrades the project to use Vite.", "isESM": true, "relativePath": [ "dist", diff --git a/packages/cli/src/commands/hydrogen/setup.ts b/packages/cli/src/commands/hydrogen/setup.ts index 838619c398..139dd9f296 100644 --- a/packages/cli/src/commands/hydrogen/setup.ts +++ b/packages/cli/src/commands/hydrogen/setup.ts @@ -1,3 +1,6 @@ +import {writeJsonResult} from '../../lib/json-output.js'; +import {jsonFlag} from '@shopify/cli-kit/node/cli'; +import {setupJsonOutputSchema} from '../../lib/setups/types.js'; import Command from '@shopify/cli-kit/node/base-command'; import {AbortController} from '@shopify/cli-kit/node/abort'; import {renderTasks} from '../../lib/ui.js'; @@ -24,9 +27,16 @@ import {ALIAS_NAME, getCliCommand} from '../../lib/shell.js'; import {getTemplateAppFile} from '../../lib/build.js'; export default class Setup extends Command { - static description = 'Scaffold routes and core functionality.'; + static get jsonOutputSchema(): typeof setupJsonOutputSchema { + return setupJsonOutputSchema; + } + + static descriptionWithMarkdown = 'Scaffold routes and core functionality.'; + + static description = this.descriptionForHelp(); static flags = { + ...jsonFlag, ...commonFlags.path, ...commonFlags.force, ...commonFlags.markets, @@ -40,10 +50,13 @@ export default class Setup extends Command { const {flags} = await this.parse(Setup); const directory = flags.path ? resolvePath(flags.path) : process.cwd(); - await runSetup({ - ...flagsToCamelObject(flags), - directory, - }); + await runSetup( + { + ...flagsToCamelObject(flags), + directory, + }, + flags.json, + ); } } @@ -54,7 +67,7 @@ type RunSetupOptions = { shortcut?: boolean; }; -export async function runSetup(options: RunSetupOptions) { +export async function executeSetup(options: RunSetupOptions) { const controller = new AbortController(); const {rootDirectory, appDirectory} = await getRemixConfig(options.directory); @@ -135,11 +148,25 @@ export async function runSetup(options: RunSetupOptions) { options.shortcut, ); - if (!i18n && !createShortcut) return; + if (!i18n && !createShortcut) { + await backgroundWorkPromise; + return { + directory: rootDirectory, + name: location, + location, + i18n, + routes, + shortcut: false, + cliCommand, + showSummary: false, + }; + } + let shortcut = false; if (createShortcut) { backgroundWorkPromise = backgroundWorkPromise.then(async () => { if (await createShortcut()) { + shortcut = true; cliCommand = ALIAS_NAME; } }); @@ -149,18 +176,31 @@ export async function runSetup(options: RunSetupOptions) { await renderTasks(tasks); - await renderProjectReady( - { - location, - name: location, - directory: rootDirectory, - }, - { - cliCommand, + return { + directory: rootDirectory, + name: location, + location, + i18n, + routes, + shortcut, + cliCommand, + showSummary: true, + }; +} + +export async function runSetup(options: RunSetupOptions, json?: boolean) { + const result = await executeSetup(options); + if ( + !writeJsonResult(setupJsonOutputSchema, result, json) && + result.showSummary + ) { + await renderProjectReady(result, { + cliCommand: result.cliCommand, depsInstalled: true, packageManager: 'npm', - i18n, - routes, - }, - ); + i18n: result.i18n, + routes: result.routes, + }); + } + return result; } diff --git a/packages/cli/src/commands/hydrogen/setup/css.ts b/packages/cli/src/commands/hydrogen/setup/css.ts index 1cb74bea47..acb181aa76 100644 --- a/packages/cli/src/commands/hydrogen/setup/css.ts +++ b/packages/cli/src/commands/hydrogen/setup/css.ts @@ -1,3 +1,6 @@ +import {writeJsonResult} from '../../../lib/json-output.js'; +import {jsonFlag} from '@shopify/cli-kit/node/cli'; +import {setupCssJsonOutputSchema} from '../../../lib/setups/types.js'; import {resolvePath} from '@shopify/cli-kit/node/path'; import { commonFlags, @@ -23,12 +26,17 @@ import {getViteConfig} from '../../../lib/vite-config.js'; import {AbortError} from '@shopify/cli-kit/node/error'; export default class SetupCSS extends Command { + static get jsonOutputSchema(): typeof setupCssJsonOutputSchema { + return setupCssJsonOutputSchema; + } + static descriptionWithMarkdown = 'Adds support for certain CSS strategies to your project.'; - static description = 'Setup CSS strategies for your project.'; + static description = this.descriptionForHelp(); static flags = { + ...jsonFlag, ...commonFlags.path, ...commonFlags.force, ...overrideFlag(commonFlags.installDeps, {'install-deps': {default: true}}), @@ -46,15 +54,18 @@ export default class SetupCSS extends Command { const {flags, args} = await this.parse(SetupCSS); const directory = flags.path ? resolvePath(flags.path) : process.cwd(); - await runSetupCSS({ - ...flagsToCamelObject(flags), - strategy: args.strategy as CssStrategy, - directory, - }); + await runSetupCSS( + { + ...flagsToCamelObject(flags), + strategy: args.strategy as CssStrategy, + directory, + }, + flags.json, + ); } } -export async function runSetupCSS({ +export async function executeSetupCSS({ strategy: flagStrategy, directory, force = false, @@ -64,7 +75,7 @@ export async function runSetupCSS({ directory: string; force?: boolean; installDeps: boolean; -}) { +}): Promise { const viteConfig = await getViteConfig(directory).catch(() => null); if (!viteConfig) { throw new AbortError( @@ -76,17 +87,17 @@ export async function runSetupCSS({ const strategy = flagStrategy ? flagStrategy : await renderCssPrompt(); - if (strategy === 'css-modules' || strategy === 'postcss') { - renderSuccess({ - headline: `Vite works out of the box with ${CSS_STRATEGY_NAME_MAP[strategy]}.`, - body: `See the Vite documentation for more information:\n${CSS_STRATEGY_HELP_URL_MAP[strategy]}`, - }); - - return; - } - + const result = { + directory, + strategy, + files: [] as string[], + dependenciesInstalled: false, + needsNpmReinstall: false, + }; + if (strategy === 'css-modules' || strategy === 'postcss') + return {...result, status: 'built-in'}; const setupOutput = await setupCssStrategy(strategy, remixConfig, force); - if (!setupOutput) return; + if (!setupOutput) return {...result, status: 'cancelled'}; const {workPromise, generatedAssets, needsInstallDeps} = setupOutput; @@ -123,6 +134,39 @@ export async function runSetupCSS({ await renderTasks(tasks); + return { + ...result, + status: 'configured', + files: generatedAssets, + dependenciesInstalled: Boolean(installDeps && needsInstallDeps), + needsNpmReinstall: Boolean( + needsInstallDeps && isNpm && strategy === 'tailwind', + ), + }; +} + +export async function runSetupCSS( + options: Parameters[0], + json?: boolean, +) { + const result = await executeSetupCSS(options); + if (!writeJsonResult(setupCssJsonOutputSchema, result, json)) + renderSetupCSS(result); + return result; +} + +export function renderSetupCSS( + result: import('../../../lib/setups/types.js').SetupCssResult, +) { + const {strategy, files: generatedAssets} = result; + if (result.status === 'cancelled') return; + if (result.status === 'built-in') { + renderSuccess({ + headline: `Vite works out of the box with ${CSS_STRATEGY_NAME_MAP[strategy]}.`, + body: `See the Vite documentation for more information:\n${CSS_STRATEGY_HELP_URL_MAP[strategy]}`, + }); + return; + } renderSuccess({ headline: `${CSS_STRATEGY_NAME_MAP[strategy]} setup complete.`, body: @@ -133,16 +177,11 @@ export async function runSetupCSS({ : '') + `\nFor more information, visit ${CSS_STRATEGY_HELP_URL_MAP[strategy]}`, }); - - // Due to a bug in NPM related to optional dependencies in Tailwind, - // we need to reinstall dependencies to fix node_modules: - // https://github.com/npm/cli/issues/4828 - if (needsInstallDeps && isNpm && strategy === 'tailwind') { + if (result.needsNpmReinstall) renderWarning({ body: [ 'Due to a bug in NPM, you might need to reinstall dependencies again.\nRun', {command: 'npm install'}, ], }); - } } diff --git a/packages/cli/src/commands/hydrogen/setup/json-output.test.ts b/packages/cli/src/commands/hydrogen/setup/json-output.test.ts new file mode 100644 index 0000000000..abdda0735a --- /dev/null +++ b/packages/cli/src/commands/hydrogen/setup/json-output.test.ts @@ -0,0 +1,150 @@ +import {beforeEach, expect, it, vi} from 'vitest'; +import {captureJsonOutput} from '../../../../tests/output.js'; +import {getViteConfig, hasViteConfig} from '../../../lib/vite-config.js'; +import {getRemixConfig} from '../../../lib/remix-config.js'; +import { + setupCssStrategy, + renderCssPrompt, +} from '../../../lib/setups/css/index.js'; +import {setupI18nStrategy} from '../../../lib/setups/i18n/index.js'; +import Setup from '../setup.js'; +import SetupCSS, {runSetupCSS} from './css.js'; +import SetupMarkets, {runSetupMarkets} from './markets.js'; +import SetupVite, {runSetupVite, presentSetupVite} from './vite.js'; + +vi.mock('../../../lib/vite-config.js'); +vi.mock('../../../lib/remix-config.js'); +vi.mock('../../../lib/setups/css/index.js', async (original) => ({ + ...(await original()), + setupCssStrategy: vi.fn(), + renderCssPrompt: vi.fn(), +})); +vi.mock('../../../lib/setups/i18n/index.js', async (original) => ({ + ...(await original()), + setupI18nStrategy: vi.fn(), +})); + +beforeEach(() => { + vi.clearAllMocks(); + vi.mocked(getViteConfig).mockResolvedValue({ + remixConfig: {rootDirectory: '/project'}, + } as any); + vi.mocked(getRemixConfig).mockResolvedValue({ + rootDirectory: '/project', + serverEntryPoint: '/project/server.ts', + } as any); + vi.mocked(setupCssStrategy).mockResolvedValue({ + workPromise: Promise.resolve(), + generatedAssets: ['tailwind.css'], + needsInstallDeps: false, + }); +}); + +it('reports configured CSS files and puts task progress on stderr', async () => { + const {stdout, stderr} = await captureJsonOutput(() => + runSetupCSS({ + directory: '/project', + strategy: 'tailwind', + installDeps: false, + }), + ); + expect(JSON.parse(stdout)).toEqual({ + directory: '/project', + status: 'configured', + strategy: 'tailwind', + files: ['tailwind.css'], + dependenciesInstalled: false, + needsNpmReinstall: false, + }); + expect( + stderr + .trim() + .split('\n') + .map((line) => JSON.parse(line)), + ).toEqual( + expect.arrayContaining([ + expect.objectContaining({type: 'progress', status: 'completed'}), + ]), + ); +}); + +it('preserves CSS strategy prompts in JSON mode and handles built-in support', async () => { + vi.mocked(renderCssPrompt).mockResolvedValue('css-modules'); + const {stdout} = await captureJsonOutput(() => + runSetupCSS({directory: '/project', installDeps: false}), + ); + expect(renderCssPrompt).toHaveBeenCalled(); + expect(JSON.parse(stdout)).toMatchObject({ + status: 'built-in', + strategy: 'css-modules', + files: [], + }); + expect(setupCssStrategy).not.toHaveBeenCalled(); +}); + +it('reports a cancelled CSS setup without claiming files were written', async () => { + vi.mocked(setupCssStrategy).mockResolvedValue(undefined); + const {stdout} = await captureJsonOutput(() => + runSetupCSS({ + directory: '/project', + strategy: 'tailwind', + installDeps: false, + }), + ); + expect(JSON.parse(stdout)).toMatchObject({ + status: 'cancelled', + files: [], + dependenciesInstalled: false, + }); +}); + +it('reports the selected markets strategy after applying it', async () => { + const {stdout} = await captureJsonOutput(() => + runSetupMarkets({directory: '/project', strategy: 'domains'}), + ); + expect(JSON.parse(stdout)).toEqual({ + directory: '/project', + strategy: 'domains', + serverEntryPoint: '/project/server.ts', + }); + expect(setupI18nStrategy).toHaveBeenCalledWith( + 'domains', + expect.objectContaining({rootDirectory: '/project'}), + ); +}); + +it('encodes Vite migration results through the real presenter and writer', async () => { + const result = { + directory: '/project', + viteConfig: '/project/vite.config.ts', + serverEntryPoint: '/project/server.ts', + dependenciesInstalled: true as const, + needsMdxSetup: false, + }; + const {stdout, stderr} = await captureJsonOutput(() => + presentSetupVite(result), + ); + expect(JSON.parse(stdout)).toEqual(result); + expect(stderr).toBe(''); + expect(() => + SetupVite.jsonOutputSchema.encode({...result, viteConfig: null} as any), + ).toThrow(); +}); + +it('does not encode success when the project already uses Vite', async () => { + vi.mocked(hasViteConfig).mockResolvedValue(true); + const {stdout} = await captureJsonOutput(async () => { + await expect(runSetupVite({directory: '/project'})).rejects.toThrow( + 'already has a Vite config', + ); + }); + expect(stdout).toBe(''); +}); + +it.each([Setup, SetupCSS, SetupMarkets, SetupVite])( + 'exposes JSON flags and discoverable schemas: %s', + (command) => { + expect(command.flags.json).toBeDefined(); + expect(command.description).toContain(command.jsonOutputSchema.name); + }, +); diff --git a/packages/cli/src/commands/hydrogen/setup/markets.ts b/packages/cli/src/commands/hydrogen/setup/markets.ts index d41d5d2b96..0cafb2c3cf 100644 --- a/packages/cli/src/commands/hydrogen/setup/markets.ts +++ b/packages/cli/src/commands/hydrogen/setup/markets.ts @@ -1,3 +1,6 @@ +import {writeJsonResult} from '../../../lib/json-output.js'; +import {jsonFlag} from '@shopify/cli-kit/node/cli'; +import {setupMarketsJsonOutputSchema} from '../../../lib/setups/types.js'; import {resolvePath} from '@shopify/cli-kit/node/path'; import {commonFlags, flagsToCamelObject} from '../../../lib/flags.js'; import Command from '@shopify/cli-kit/node/base-command'; @@ -13,12 +16,17 @@ import { } from '../../../lib/setups/i18n/index.js'; export default class SetupMarkets extends Command { + static get jsonOutputSchema(): typeof setupMarketsJsonOutputSchema { + return setupMarketsJsonOutputSchema; + } + static descriptionWithMarkdown = 'Adds support for multiple [markets](https://shopify.dev/docs/custom-storefronts/hydrogen/markets) to your project by using the URL structure.'; - static description = 'Setup support for multiple markets in your project.'; + static description = this.descriptionForHelp(); static flags = { + ...jsonFlag, ...commonFlags.path, }; @@ -34,15 +42,18 @@ export default class SetupMarkets extends Command { const {flags, args} = await this.parse(SetupMarkets); const directory = flags.path ? resolvePath(flags.path) : process.cwd(); - await runSetupMarkets({ - ...flagsToCamelObject(flags), - strategy: args.strategy as I18nStrategy, - directory, - }); + await runSetupMarkets( + { + ...flagsToCamelObject(flags), + strategy: args.strategy as I18nStrategy, + directory, + }, + flags.json, + ); } } -export async function runSetupMarkets({ +export async function executeSetupMarkets({ strategy: flagStrategy, directory, }: { @@ -64,12 +75,33 @@ export async function runSetupMarkets({ }, ]); + return { + directory: remixConfig.rootDirectory, + strategy, + serverEntryPoint: remixConfig.serverEntryPoint, + }; +} + +export async function runSetupMarkets( + options: Parameters[0], + json?: boolean, +) { + const result = await executeSetupMarkets(options); + if (!writeJsonResult(setupMarketsJsonOutputSchema, result, json)) + renderSetupMarkets(result); + return result; +} + +export function renderSetupMarkets({ + strategy, + serverEntryPoint, +}: import('../../../lib/setups/types.js').SetupMarketsResult) { renderSuccess({ headline: `Markets support setup complete with strategy ${I18N_STRATEGY_NAME_MAP[ strategy ].toLowerCase()}.`, body: `You can now modify the supported locales in ${ - remixConfig.serverEntryPoint ?? 'your server entry file.' + serverEntryPoint ?? 'your server entry file.' }\n`, }); } diff --git a/packages/cli/src/commands/hydrogen/setup/vite.ts b/packages/cli/src/commands/hydrogen/setup/vite.ts index bbdc015403..c37239332c 100644 --- a/packages/cli/src/commands/hydrogen/setup/vite.ts +++ b/packages/cli/src/commands/hydrogen/setup/vite.ts @@ -1,3 +1,6 @@ +import {writeJsonResult} from '../../../lib/json-output.js'; +import {jsonFlag} from '@shopify/cli-kit/node/cli'; +import {setupViteJsonOutputSchema} from '../../../lib/setups/types.js'; import {joinPath, resolvePath} from '@shopify/cli-kit/node/path'; import Command from '@shopify/cli-kit/node/base-command'; import {renderSuccess, renderTasks} from '../../../lib/ui.js'; @@ -24,9 +27,17 @@ import {AbortError} from '@shopify/cli-kit/node/error'; import {outputNewline} from '@shopify/cli-kit/node/output'; export default class SetupVite extends Command { - static description = 'EXPERIMENTAL: Upgrades the project to use Vite.'; + static get jsonOutputSchema(): typeof setupViteJsonOutputSchema { + return setupViteJsonOutputSchema; + } + + static descriptionWithMarkdown = + 'EXPERIMENTAL: Upgrades the project to use Vite.'; + + static description = this.descriptionForHelp(); static flags = { + ...jsonFlag, ...commonFlags.path, }; @@ -34,10 +45,13 @@ export default class SetupVite extends Command { const {flags} = await this.parse(SetupVite); const directory = flags.path ? resolvePath(flags.path) : process.cwd(); - await runSetupVite({ - ...flagsToCamelObject(flags), - directory, - }); + await runSetupVite( + { + ...flagsToCamelObject(flags), + directory, + }, + flags.json, + ); } } @@ -49,7 +63,7 @@ const tailwindPostCSSConfig = `export default { }; `; -export async function runSetupVite({directory}: {directory: string}) { +export async function executeSetupVite({directory}: {directory: string}) { outputNewline(); if (await hasViteConfig(directory)) { throw new AbortError('This project already has a Vite config file.'); @@ -398,14 +412,43 @@ export async function runSetupVite({directory}: {directory: string}) { }, ]); + return { + directory, + viteConfig: resolvePath(directory, 'vite.config.' + fileExt.slice(0, 2)), + serverEntryPoint: resolvePath(directory, serverEntry), + dependenciesInstalled: true as const, + needsMdxSetup: Boolean(rawRemixConfig.mdx), + }; +} + +export async function runSetupVite( + options: {directory: string}, + json?: boolean, +) { + const result = await executeSetupVite(options); + presentSetupVite(result, json); + return result; +} + +export function renderSetupVite({ + needsMdxSetup, +}: import('../../../lib/setups/types.js').SetupViteResult) { renderSuccess({ headline: `Your Vite project is ready!`, body: `We've modified your project to use Vite.\nPlease use Git to review the changes.`, nextSteps: [ - rawRemixConfig.mdx + needsMdxSetup ? 'Setup MDX support in Vite: https://remix.run/docs/en/main/future/vite#add-mdx-plugin' : '', `See more information about Vite in Remix at https://remix.run/docs/en/main/future/vite`, ].filter(Boolean), }); } + +export function presentSetupVite( + result: import('../../../lib/setups/types.js').SetupViteResult, + json?: boolean, +) { + if (!writeJsonResult(setupViteJsonOutputSchema, result, json)) + renderSetupVite(result); +} diff --git a/packages/cli/src/lib/setups/types.ts b/packages/cli/src/lib/setups/types.ts new file mode 100644 index 0000000000..1b276dc66b --- /dev/null +++ b/packages/cli/src/lib/setups/types.ts @@ -0,0 +1,61 @@ +import { + defineJsonOutputSchema, + type InferJsonOutputSchema, +} from '@shopify/cli-kit/node/json-output-schema'; +import {zod} from '@shopify/cli-kit/node/schema'; + +export const setupJsonOutputSchema = defineJsonOutputSchema({ + name: 'HydrogenSetupResult', + schema: zod.object({ + directory: zod.string(), + name: zod.string(), + location: zod.string(), + i18n: zod.enum(['subfolders', 'domains', 'subdomains']).optional(), + routes: zod.record(zod.array(zod.string())).optional(), + shortcut: zod.boolean(), + }), +}); +export type SetupResult = InferJsonOutputSchema; +export const setupCssJsonOutputSchema = defineJsonOutputSchema({ + name: 'HydrogenSetupCssResult', + schema: zod.object({ + status: zod.enum(['configured', 'built-in', 'cancelled']), + directory: zod.string(), + strategy: zod.enum([ + 'tailwind', + 'vanilla-extract', + 'css-modules', + 'postcss', + ]), + files: zod.array(zod.string()), + dependenciesInstalled: zod.boolean(), + needsNpmReinstall: zod.boolean(), + }), +}); +export type SetupCssResult = InferJsonOutputSchema< + typeof setupCssJsonOutputSchema +>; +export const setupMarketsJsonOutputSchema = defineJsonOutputSchema({ + name: 'HydrogenSetupMarketsResult', + schema: zod.object({ + directory: zod.string(), + strategy: zod.enum(['subfolders', 'domains', 'subdomains']), + serverEntryPoint: zod.string().optional(), + }), +}); +export type SetupMarketsResult = InferJsonOutputSchema< + typeof setupMarketsJsonOutputSchema +>; +export const setupViteJsonOutputSchema = defineJsonOutputSchema({ + name: 'HydrogenSetupViteResult', + schema: zod.object({ + directory: zod.string(), + viteConfig: zod.string(), + serverEntryPoint: zod.string(), + dependenciesInstalled: zod.literal(true), + needsMdxSetup: zod.boolean(), + }), +}); +export type SetupViteResult = InferJsonOutputSchema< + typeof setupViteJsonOutputSchema +>;