@shopify/cli-kit 0.0.0-snapshot-20261013164607 → 0.0.0-snapshot-20261013171044
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/private/node/testing/json-command-events.d.ts +2 -0
- package/dist/private/node/testing/json-command-events.js +17 -0
- package/dist/private/node/testing/json-command-events.js.map +1 -0
- package/dist/public/common/version.d.ts +1 -1
- package/dist/public/common/version.js +1 -1
- package/dist/public/common/version.js.map +1 -1
- package/dist/public/node/is-global.js +1 -2
- package/dist/public/node/is-global.js.map +1 -1
- package/dist/public/node/testing/json-command.d.ts +44 -0
- package/dist/public/node/testing/json-command.js +151 -0
- package/dist/public/node/testing/json-command.js.map +1 -0
- package/dist/public/node/testing/output.d.ts +2 -2
- package/dist/public/node/testing/output.js +12 -9
- package/dist/public/node/testing/output.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { commandEventOutputSchema } from '../../../public/node/command-events.js';
|
|
2
|
+
import { expect } from 'vitest';
|
|
3
|
+
export function expectCommandEvents(stderr, expectedEvents) {
|
|
4
|
+
const events = stderr.trim()
|
|
5
|
+
? stderr
|
|
6
|
+
.trim()
|
|
7
|
+
.split('\n')
|
|
8
|
+
.map((line) => commandEventOutputSchema.validate(JSON.parse(line)))
|
|
9
|
+
: [];
|
|
10
|
+
if (typeof expectedEvents === 'function') {
|
|
11
|
+
expectedEvents(events);
|
|
12
|
+
}
|
|
13
|
+
else {
|
|
14
|
+
expect(events).toMatchObject(expectedEvents);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
//# sourceMappingURL=json-command-events.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"json-command-events.js","sourceRoot":"","sources":["../../../../src/private/node/testing/json-command-events.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,wBAAwB,EAAC,MAAM,wCAAwC,CAAA;AAC/E,OAAO,EAAC,MAAM,EAAC,MAAM,QAAQ,CAAA;AAI7B,MAAM,UAAU,mBAAmB,CAAC,MAAc,EAAE,cAAuC;IACzF,MAAM,MAAM,GAAG,MAAM,CAAC,IAAI,EAAE;QAC1B,CAAC,CAAC,MAAM;aACH,IAAI,EAAE;aACN,KAAK,CAAC,IAAI,CAAC;aACX,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,wBAAwB,CAAC,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC;QACvE,CAAC,CAAC,EAAE,CAAA;IACN,IAAI,OAAO,cAAc,KAAK,UAAU,EAAE,CAAC;QACzC,cAAc,CAAC,MAAM,CAAC,CAAA;IACxB,CAAC;SAAM,CAAC;QACN,MAAM,CAAC,MAAM,CAAC,CAAC,aAAa,CAAC,cAAc,CAAC,CAAA;IAC9C,CAAC;AACH,CAAC","sourcesContent":["import {commandEventOutputSchema} from '../../../public/node/command-events.js'\nimport {expect} from 'vitest'\n\nexport type CommandEventExpectation = Record<string, unknown>[] | ((events: Record<string, unknown>[]) => void)\n\nexport function expectCommandEvents(stderr: string, expectedEvents: CommandEventExpectation): void {\n const events = stderr.trim()\n ? stderr\n .trim()\n .split('\\n')\n .map((line) => commandEventOutputSchema.validate(JSON.parse(line)))\n : []\n if (typeof expectedEvents === 'function') {\n expectedEvents(events)\n } else {\n expect(events).toMatchObject(expectedEvents)\n }\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const CLI_KIT_VERSION = "0.0.0-snapshot-
|
|
1
|
+
export declare const CLI_KIT_VERSION = "0.0.0-snapshot-20261013171044";
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export const CLI_KIT_VERSION = '0.0.0-snapshot-
|
|
1
|
+
export const CLI_KIT_VERSION = '0.0.0-snapshot-20261013171044';
|
|
2
2
|
//# sourceMappingURL=version.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"version.js","sourceRoot":"","sources":["../../../src/public/common/version.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,eAAe,GAAG,+BAA+B,CAAA","sourcesContent":["export const CLI_KIT_VERSION = '0.0.0-snapshot-
|
|
1
|
+
{"version":3,"file":"version.js","sourceRoot":"","sources":["../../../src/public/common/version.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,eAAe,GAAG,+BAA+B,CAAA","sourcesContent":["export const CLI_KIT_VERSION = '0.0.0-snapshot-20261013171044'\n"]}
|
|
@@ -48,10 +48,9 @@ export function currentProcessIsGlobal(argv = process.argv) {
|
|
|
48
48
|
export async function installGlobalShopifyCLI(packageManager) {
|
|
49
49
|
const { outputInfo } = await import('./output.js');
|
|
50
50
|
const { exec } = await import('./system.js');
|
|
51
|
-
const { jsonOutputEnabled } = await import('./environment.js');
|
|
52
51
|
const args = packageManager === 'yarn' ? ['global', 'add', '@shopify/cli@latest'] : ['install', '-g', '@shopify/cli@latest'];
|
|
53
52
|
outputInfo(`Running ${packageManager} ${args.join(' ')}...`);
|
|
54
|
-
await exec(packageManager, args,
|
|
53
|
+
await exec(packageManager, args, { stdio: 'inherit' });
|
|
55
54
|
}
|
|
56
55
|
/**
|
|
57
56
|
* Prompts the user to install the global CLI.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"is-global.js","sourceRoot":"","sources":["../../../src/public/node/is-global.ts"],"names":[],"mappings":"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","sourcesContent":["import {cwd, dirname, isSubpath, joinPath, sniffForPath} from './path.js'\nimport {isUnitTest} from './context/local.js'\nimport {findPathUpSync, globSync} from './fs.js'\nimport {realpathSync} from 'fs'\nimport type {PackageManager} from './node-package-manager.js'\n\nlet _isGlobal: boolean | undefined\n\n/**\n * Returns true if the current process is running in a global context.\n *\n * @param argv - The arguments passed to the process.\n * @returns `true` if the current process is running in a global context.\n */\nexport function currentProcessIsGlobal(argv = process.argv): boolean {\n // If we are running tests, we need to disable the cache\n try {\n if (_isGlobal !== undefined && !isUnitTest()) return _isGlobal\n\n // Path where the current project is (app/hydrogen)\n const path = sniffForPath() ?? cwd()\n\n const projectDir = getProjectDir(path)\n if (!projectDir) {\n return true\n }\n\n // From node docs: \"The second element [of the array] will be the path to the JavaScript file being executed\"\n const binDir = argv[1] ?? ''\n if (!binDir) {\n return true\n }\n\n // If binDir lives inside projectDir, we are running a local CLI.\n // Use isSubpath (pathe.relative under the hood) instead of a raw\n // string startsWith: projectDir flows through normalizePath and is\n // forward-slash on every platform, while argv[1] is OS-native, so on\n // Windows it arrives backslash-separated and a naive startsWith would\n // misclassify a local install as global.\n const isLocal = isSubpath(projectDir.trim(), binDir)\n\n _isGlobal = !isLocal\n return _isGlobal\n // eslint-disable-next-line no-catch-all/no-catch-all\n } catch (error) {\n return false\n }\n}\n\n/**\n * Installs the global Shopify CLI, using the provided package manager.\n *\n * @param packageManager - The package manager to use.\n */\nexport async function installGlobalShopifyCLI(packageManager: PackageManager): Promise<void> {\n const {outputInfo} = await import('./output.js')\n const {exec} = await import('./system.js')\n const {jsonOutputEnabled} = await import('./environment.js')\n const args =\n packageManager === 'yarn' ? ['global', 'add', '@shopify/cli@latest'] : ['install', '-g', '@shopify/cli@latest']\n outputInfo(`Running ${packageManager} ${args.join(' ')}...`)\n await exec(packageManager, args, jsonOutputEnabled() ? {} : {stdio: 'inherit'})\n}\n\nexport interface InstallGlobalCLIPromptResult {\n install: boolean\n alreadyInstalled: boolean\n}\n/**\n * Prompts the user to install the global CLI.\n *\n * @returns `true` if the user has installed the global CLI.\n */\nexport async function installGlobalCLIPrompt(): Promise<InstallGlobalCLIPromptResult> {\n const {terminalSupportsPrompting} = await import('./system.js')\n if (!terminalSupportsPrompting()) return {install: false, alreadyInstalled: false}\n const {globalCLIVersion} = await import('./version.js')\n if (await globalCLIVersion()) {\n return {install: false, alreadyInstalled: true}\n }\n const {renderSelectPrompt} = await import('./ui.js')\n const result = await renderSelectPrompt({\n message: 'We recommend installing Shopify CLI globally in your system. Would you like to install it now?',\n choices: [\n {value: 'yes', label: 'Yes'},\n {value: 'no', label: 'No, just for this project'},\n ],\n })\n\n return {install: result === 'yes', alreadyInstalled: false}\n}\n\n/**\n * Infers the package manager used by the global CLI.\n *\n * @param argv - The arguments passed to the process.\n * @param env - The environment variables of the process.\n * @returns The package manager used by the global CLI.\n */\nexport function inferPackageManagerForGlobalCLI(argv = process.argv, env = process.env): PackageManager {\n if (!currentProcessIsGlobal(argv)) return 'unknown'\n\n // Check for Homebrew first (most reliable via environment variable)\n if (env.SHOPIFY_HOMEBREW_FORMULA) {\n return 'homebrew'\n }\n\n const processArgv = argv[1] ?? ''\n const symlinkPath = processArgv.toLowerCase()\n\n // Resolve symlinks to get the real path of the binary.\n let realPath = symlinkPath\n try {\n realPath = realpathSync(processArgv).toLowerCase()\n // eslint-disable-next-line no-catch-all/no-catch-all\n } catch (error) {\n // fall back to using the original path for detection\n }\n\n // Inspect both the (unresolved) symlink path and the resolved real path. Some\n // package managers — notably bun (`~/.bun/bin/<name>`) — install global binaries\n // as symlinks pointing into a generic `node_modules` directory whose real path\n // no longer contains the package manager name. The original symlink under the\n // PM's bin dir is the most reliable signal in that case.\n const matches = (needle: string) => realPath.includes(needle) || symlinkPath.includes(needle)\n\n if (matches('yarn')) return 'yarn'\n if (matches('pnpm')) return 'pnpm'\n if (matches('bun')) return 'bun'\n\n // Check for Homebrew via Cellar path (resolved symlink)\n if (realPath.includes('/cellar/')) return 'homebrew'\n\n return 'npm'\n}\n\n/**\n * Returns the project directory for the given path.\n *\n * @param directory - The path to search upward from.\n * @returns The project root directory, or undefined if not found.\n */\nexport function getProjectDir(directory: string): string | undefined {\n const configFiles = ['shopify.app{,.*}.toml', 'hydrogen.config.js', 'hydrogen.config.ts']\n const existsConfigFile = (directory: string) => {\n const configPaths = globSync(configFiles.map((file) => joinPath(directory, file)))\n return configPaths.length > 0 ? configPaths[0] : undefined\n }\n try {\n const configFile = findPathUpSync(existsConfigFile, {\n cwd: directory,\n type: 'file',\n })\n if (configFile) return dirname(configFile)\n // eslint-disable-next-line no-catch-all/no-catch-all\n } catch (error) {\n return undefined\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"is-global.js","sourceRoot":"","sources":["../../../src/public/node/is-global.ts"],"names":[],"mappings":"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","sourcesContent":["import {cwd, dirname, isSubpath, joinPath, sniffForPath} from './path.js'\nimport {isUnitTest} from './context/local.js'\nimport {findPathUpSync, globSync} from './fs.js'\nimport {realpathSync} from 'fs'\nimport type {PackageManager} from './node-package-manager.js'\n\nlet _isGlobal: boolean | undefined\n\n/**\n * Returns true if the current process is running in a global context.\n *\n * @param argv - The arguments passed to the process.\n * @returns `true` if the current process is running in a global context.\n */\nexport function currentProcessIsGlobal(argv = process.argv): boolean {\n // If we are running tests, we need to disable the cache\n try {\n if (_isGlobal !== undefined && !isUnitTest()) return _isGlobal\n\n // Path where the current project is (app/hydrogen)\n const path = sniffForPath() ?? cwd()\n\n const projectDir = getProjectDir(path)\n if (!projectDir) {\n return true\n }\n\n // From node docs: \"The second element [of the array] will be the path to the JavaScript file being executed\"\n const binDir = argv[1] ?? ''\n if (!binDir) {\n return true\n }\n\n // If binDir lives inside projectDir, we are running a local CLI.\n // Use isSubpath (pathe.relative under the hood) instead of a raw\n // string startsWith: projectDir flows through normalizePath and is\n // forward-slash on every platform, while argv[1] is OS-native, so on\n // Windows it arrives backslash-separated and a naive startsWith would\n // misclassify a local install as global.\n const isLocal = isSubpath(projectDir.trim(), binDir)\n\n _isGlobal = !isLocal\n return _isGlobal\n // eslint-disable-next-line no-catch-all/no-catch-all\n } catch (error) {\n return false\n }\n}\n\n/**\n * Installs the global Shopify CLI, using the provided package manager.\n *\n * @param packageManager - The package manager to use.\n */\nexport async function installGlobalShopifyCLI(packageManager: PackageManager): Promise<void> {\n const {outputInfo} = await import('./output.js')\n const {exec} = await import('./system.js')\n const args =\n packageManager === 'yarn' ? ['global', 'add', '@shopify/cli@latest'] : ['install', '-g', '@shopify/cli@latest']\n outputInfo(`Running ${packageManager} ${args.join(' ')}...`)\n await exec(packageManager, args, {stdio: 'inherit'})\n}\n\nexport interface InstallGlobalCLIPromptResult {\n install: boolean\n alreadyInstalled: boolean\n}\n/**\n * Prompts the user to install the global CLI.\n *\n * @returns `true` if the user has installed the global CLI.\n */\nexport async function installGlobalCLIPrompt(): Promise<InstallGlobalCLIPromptResult> {\n const {terminalSupportsPrompting} = await import('./system.js')\n if (!terminalSupportsPrompting()) return {install: false, alreadyInstalled: false}\n const {globalCLIVersion} = await import('./version.js')\n if (await globalCLIVersion()) {\n return {install: false, alreadyInstalled: true}\n }\n const {renderSelectPrompt} = await import('./ui.js')\n const result = await renderSelectPrompt({\n message: 'We recommend installing Shopify CLI globally in your system. Would you like to install it now?',\n choices: [\n {value: 'yes', label: 'Yes'},\n {value: 'no', label: 'No, just for this project'},\n ],\n })\n\n return {install: result === 'yes', alreadyInstalled: false}\n}\n\n/**\n * Infers the package manager used by the global CLI.\n *\n * @param argv - The arguments passed to the process.\n * @param env - The environment variables of the process.\n * @returns The package manager used by the global CLI.\n */\nexport function inferPackageManagerForGlobalCLI(argv = process.argv, env = process.env): PackageManager {\n if (!currentProcessIsGlobal(argv)) return 'unknown'\n\n // Check for Homebrew first (most reliable via environment variable)\n if (env.SHOPIFY_HOMEBREW_FORMULA) {\n return 'homebrew'\n }\n\n const processArgv = argv[1] ?? ''\n const symlinkPath = processArgv.toLowerCase()\n\n // Resolve symlinks to get the real path of the binary.\n let realPath = symlinkPath\n try {\n realPath = realpathSync(processArgv).toLowerCase()\n // eslint-disable-next-line no-catch-all/no-catch-all\n } catch (error) {\n // fall back to using the original path for detection\n }\n\n // Inspect both the (unresolved) symlink path and the resolved real path. Some\n // package managers — notably bun (`~/.bun/bin/<name>`) — install global binaries\n // as symlinks pointing into a generic `node_modules` directory whose real path\n // no longer contains the package manager name. The original symlink under the\n // PM's bin dir is the most reliable signal in that case.\n const matches = (needle: string) => realPath.includes(needle) || symlinkPath.includes(needle)\n\n if (matches('yarn')) return 'yarn'\n if (matches('pnpm')) return 'pnpm'\n if (matches('bun')) return 'bun'\n\n // Check for Homebrew via Cellar path (resolved symlink)\n if (realPath.includes('/cellar/')) return 'homebrew'\n\n return 'npm'\n}\n\n/**\n * Returns the project directory for the given path.\n *\n * @param directory - The path to search upward from.\n * @returns The project root directory, or undefined if not found.\n */\nexport function getProjectDir(directory: string): string | undefined {\n const configFiles = ['shopify.app{,.*}.toml', 'hydrogen.config.js', 'hydrogen.config.ts']\n const existsConfigFile = (directory: string) => {\n const configPaths = globSync(configFiles.map((file) => joinPath(directory, file)))\n return configPaths.length > 0 ? configPaths[0] : undefined\n }\n try {\n const configFile = findPathUpSync(existsConfigFile, {\n cwd: directory,\n type: 'file',\n })\n if (configFile) return dirname(configFile)\n // eslint-disable-next-line no-catch-all/no-catch-all\n } catch (error) {\n return undefined\n }\n}\n"]}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { type CommandEventExpectation } from '../../../private/node/testing/json-command-events.js';
|
|
2
|
+
import type BaseCommand from '../base-command.js';
|
|
3
|
+
import type { JsonOutputSchema } from '../json-output-schema.js';
|
|
4
|
+
type JsonCommandResult = Record<string, unknown> | ReadonlyArray<unknown>;
|
|
5
|
+
type TestCleanup = () => void | Promise<void>;
|
|
6
|
+
interface TextOutput {
|
|
7
|
+
stdout: string;
|
|
8
|
+
stderr: string;
|
|
9
|
+
}
|
|
10
|
+
interface JsonCommandTestOptions {
|
|
11
|
+
command: typeof BaseCommand & {
|
|
12
|
+
readonly jsonOutputSchema: JsonOutputSchema;
|
|
13
|
+
};
|
|
14
|
+
commandId: string;
|
|
15
|
+
moduleURL: string;
|
|
16
|
+
args: string[] | (() => string[]);
|
|
17
|
+
expectedResult: JsonCommandResult | (() => JsonCommandResult);
|
|
18
|
+
expectedEvents?: CommandEventExpectation;
|
|
19
|
+
expectedSchema: Record<string, unknown>;
|
|
20
|
+
setup: () => void | TestCleanup | Promise<void | TestCleanup>;
|
|
21
|
+
assertExecuted: (format: 'text' | 'json') => void;
|
|
22
|
+
assertNotExecuted: () => void;
|
|
23
|
+
textOutput: TextOutput | ((output: TextOutput) => void);
|
|
24
|
+
diagnostics?: {
|
|
25
|
+
setup: () => void | Promise<void>;
|
|
26
|
+
expectedEvents: CommandEventExpectation;
|
|
27
|
+
};
|
|
28
|
+
failure?: {
|
|
29
|
+
setup: () => void | Promise<void>;
|
|
30
|
+
expectedError: Record<string, unknown>;
|
|
31
|
+
expectedEvents?: CommandEventExpectation;
|
|
32
|
+
exitCode: number;
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Registers shared JSON contract tests for a finite command using its real launcher and output writers.
|
|
37
|
+
* Supply independent expected results and callbacks that check the command's service calls.
|
|
38
|
+
* Keep domain-specific input, schema, prompt, and file tests beside the command.
|
|
39
|
+
* This suite captures process globals and must not run concurrently with other tests.
|
|
40
|
+
*
|
|
41
|
+
* @param options - The command, fixtures, expected output, and service assertions.
|
|
42
|
+
*/
|
|
43
|
+
export declare function describeJsonCommand(options: JsonCommandTestOptions): void;
|
|
44
|
+
export {};
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { withCapturedStandardStreams, mockAndCaptureOutput } from './output.js';
|
|
2
|
+
import { launchCLI } from '../cli-launcher.js';
|
|
3
|
+
import { ShopifyConfig } from '../custom-oclif-loader.js';
|
|
4
|
+
import { expectCommandEvents } from '../../../private/node/testing/json-command-events.js';
|
|
5
|
+
import { unstyled } from '../output.js';
|
|
6
|
+
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
|
|
7
|
+
/**
|
|
8
|
+
* Registers shared JSON contract tests for a finite command using its real launcher and output writers.
|
|
9
|
+
* Supply independent expected results and callbacks that check the command's service calls.
|
|
10
|
+
* Keep domain-specific input, schema, prompt, and file tests beside the command.
|
|
11
|
+
* This suite captures process globals and must not run concurrently with other tests.
|
|
12
|
+
*
|
|
13
|
+
* @param options - The command, fixtures, expected output, and service assertions.
|
|
14
|
+
*/
|
|
15
|
+
export function describeJsonCommand(options) {
|
|
16
|
+
const { command, commandId, moduleURL } = options;
|
|
17
|
+
describe.sequential(`${commandId} JSON contract`, () => {
|
|
18
|
+
let originalExitCode;
|
|
19
|
+
let restoreMocks;
|
|
20
|
+
let cleanup;
|
|
21
|
+
beforeEach(async () => {
|
|
22
|
+
originalExitCode = process.exitCode;
|
|
23
|
+
process.exitCode = undefined;
|
|
24
|
+
vi.stubEnv('CI', '1');
|
|
25
|
+
vi.stubEnv('SHOPIFY_CLI_NO_ANALYTICS', '1');
|
|
26
|
+
vi.stubEnv('SHOPIFY_FLAG_JSON', '0');
|
|
27
|
+
vi.stubEnv('SHOPIFY_FLAG_JSON_SCHEMA', '0');
|
|
28
|
+
vi.stubEnv('SHOPIFY_FLAG_NO_INPUT', '0');
|
|
29
|
+
const hooks = vi.spyOn(ShopifyConfig.prototype, 'runHook').mockResolvedValue({ successes: [], failures: [] });
|
|
30
|
+
const exit = vi.spyOn(process, 'exit').mockReturnValue(undefined);
|
|
31
|
+
restoreMocks = () => {
|
|
32
|
+
hooks.mockRestore();
|
|
33
|
+
exit.mockRestore();
|
|
34
|
+
};
|
|
35
|
+
cleanup = undefined;
|
|
36
|
+
cleanup = await options.setup();
|
|
37
|
+
});
|
|
38
|
+
afterEach(async () => {
|
|
39
|
+
try {
|
|
40
|
+
await cleanup?.();
|
|
41
|
+
}
|
|
42
|
+
finally {
|
|
43
|
+
restoreMocks();
|
|
44
|
+
process.exitCode = originalExitCode;
|
|
45
|
+
vi.unstubAllEnvs();
|
|
46
|
+
mockAndCaptureOutput().clear();
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
const run = async (flags, commandArgs = typeof options.args === 'function' ? options.args() : options.args) => {
|
|
50
|
+
const argv = [...commandId.split(':'), ...commandArgs, ...flags];
|
|
51
|
+
const originalArgv = process.argv;
|
|
52
|
+
const restoreArgv = () => {
|
|
53
|
+
process.argv = originalArgv;
|
|
54
|
+
};
|
|
55
|
+
// Error handling and some output helpers read process.argv instead of the launcher's argv option.
|
|
56
|
+
process.argv = [...originalArgv.slice(0, 2), ...argv];
|
|
57
|
+
try {
|
|
58
|
+
await launchCLI({ moduleURL, argv, lazyCommandLoader: async () => command });
|
|
59
|
+
}
|
|
60
|
+
finally {
|
|
61
|
+
restoreArgv();
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
const expectResult = (stdout) => {
|
|
65
|
+
const expectedResult = typeof options.expectedResult === 'function' ? options.expectedResult() : options.expectedResult;
|
|
66
|
+
expect(stdout).toBe(`${JSON.stringify(expectedResult, null, 2)}\n`);
|
|
67
|
+
command.jsonOutputSchema.validate(JSON.parse(stdout));
|
|
68
|
+
options.assertExecuted('json');
|
|
69
|
+
expect(process.exit).not.toHaveBeenCalled();
|
|
70
|
+
expect(process.exitCode ?? 0).toBe(0);
|
|
71
|
+
};
|
|
72
|
+
const expectEvents = (stderr, expectedEvents = options.expectedEvents ?? []) => {
|
|
73
|
+
expectCommandEvents(stderr, expectedEvents);
|
|
74
|
+
};
|
|
75
|
+
test.each(['--json', '-j'])('writes one result with %s', async (flag) => {
|
|
76
|
+
await withCapturedStandardStreams(async ({ stdout, stderr }) => {
|
|
77
|
+
await run([flag]);
|
|
78
|
+
expectResult(stdout());
|
|
79
|
+
expectEvents(stderr());
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
test('supports the JSON environment flag', async () => {
|
|
83
|
+
vi.stubEnv('SHOPIFY_FLAG_JSON', '1');
|
|
84
|
+
await withCapturedStandardStreams(async ({ stdout, stderr }) => {
|
|
85
|
+
await run([]);
|
|
86
|
+
expectResult(stdout());
|
|
87
|
+
expectEvents(stderr());
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
const supportsNoInput = 'no-input' in (command.flags ?? {}) || 'no-input' in command.baseFlags;
|
|
91
|
+
test.each([false, true])(supportsNoInput ? 'keeps no-input independent from JSON: %s' : 'selects text and JSON output independently: %s', async (json) => {
|
|
92
|
+
await withCapturedStandardStreams(async ({ stdout, stderr }) => {
|
|
93
|
+
await run([...(supportsNoInput ? ['--no-input'] : []), ...(json ? ['--json'] : [])]);
|
|
94
|
+
if (json) {
|
|
95
|
+
expectResult(stdout());
|
|
96
|
+
expectEvents(stderr());
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
const textOutput = { stdout: stdout(), stderr: unstyled(stderr()) };
|
|
100
|
+
if (typeof options.textOutput === 'function') {
|
|
101
|
+
options.textOutput(textOutput);
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
expect(textOutput).toEqual(options.textOutput);
|
|
105
|
+
}
|
|
106
|
+
options.assertExecuted('text');
|
|
107
|
+
expect(process.exit).not.toHaveBeenCalled();
|
|
108
|
+
expect(process.exitCode ?? 0).toBe(0);
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
test('exposes the result schema in help', () => {
|
|
113
|
+
expect(command.description).toContain(`Output from \`--json\` conforms to the \`${command.jsonOutputSchema.name}\` schema.`);
|
|
114
|
+
});
|
|
115
|
+
test('discovers the schema without required inputs, hooks, or service calls', async () => {
|
|
116
|
+
await withCapturedStandardStreams(async ({ stdout, stderr }) => {
|
|
117
|
+
await run(['--json-schema'], []);
|
|
118
|
+
expect(JSON.parse(stdout()).definitions.Result).toMatchObject(options.expectedSchema);
|
|
119
|
+
expect(stderr()).toBe('');
|
|
120
|
+
expect(ShopifyConfig.prototype.runHook).not.toHaveBeenCalled();
|
|
121
|
+
options.assertNotExecuted();
|
|
122
|
+
expect(process.exit).not.toHaveBeenCalled();
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
const diagnostics = options.diagnostics;
|
|
126
|
+
if (diagnostics) {
|
|
127
|
+
test('writes diagnostics to stderr separately from the result', async () => {
|
|
128
|
+
await diagnostics.setup();
|
|
129
|
+
await withCapturedStandardStreams(async ({ stdout, stderr }) => {
|
|
130
|
+
await run(['--json']);
|
|
131
|
+
expectResult(stdout());
|
|
132
|
+
expectEvents(stderr(), diagnostics.expectedEvents);
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
const failure = options.failure;
|
|
137
|
+
if (failure) {
|
|
138
|
+
test('writes only the fatal error and preserves the failure exit code', async () => {
|
|
139
|
+
await failure.setup();
|
|
140
|
+
await withCapturedStandardStreams(async ({ stdout, stderr }) => {
|
|
141
|
+
await run(['--json']);
|
|
142
|
+
expect(JSON.parse(stdout())).toEqual(failure.expectedError);
|
|
143
|
+
expectEvents(stderr(), failure.expectedEvents ?? []);
|
|
144
|
+
options.assertExecuted('json');
|
|
145
|
+
expect(process.exit).toHaveBeenCalledExactlyOnceWith(failure.exitCode);
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
//# sourceMappingURL=json-command.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"json-command.js","sourceRoot":"","sources":["../../../../src/public/node/testing/json-command.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,2BAA2B,EAAE,oBAAoB,EAAC,MAAM,aAAa,CAAA;AAC7E,OAAO,EAAC,SAAS,EAAC,MAAM,oBAAoB,CAAA;AAC5C,OAAO,EAAC,aAAa,EAAC,MAAM,2BAA2B,CAAA;AACvD,OAAO,EAAC,mBAAmB,EAA+B,MAAM,sDAAsD,CAAA;AACtH,OAAO,EAAC,QAAQ,EAAC,MAAM,cAAc,CAAA;AACrC,OAAO,EAAC,SAAS,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,EAAE,EAAC,MAAM,QAAQ,CAAA;AAmCxE;;;;;;;GAOG;AACH,MAAM,UAAU,mBAAmB,CAAC,OAA+B;IACjE,MAAM,EAAC,OAAO,EAAE,SAAS,EAAE,SAAS,EAAC,GAAG,OAAO,CAAA;IAE/C,QAAQ,CAAC,UAAU,CAAC,GAAG,SAAS,gBAAgB,EAAE,GAAG,EAAE;QACrD,IAAI,gBAAyC,CAAA;QAC7C,IAAI,YAAwB,CAAA;QAC5B,IAAI,OAA2B,CAAA;QAE/B,UAAU,CAAC,KAAK,IAAI,EAAE;YACpB,gBAAgB,GAAG,OAAO,CAAC,QAAQ,CAAA;YACnC,OAAO,CAAC,QAAQ,GAAG,SAAS,CAAA;YAC5B,EAAE,CAAC,OAAO,CAAC,IAAI,EAAE,GAAG,CAAC,CAAA;YACrB,EAAE,CAAC,OAAO,CAAC,0BAA0B,EAAE,GAAG,CAAC,CAAA;YAC3C,EAAE,CAAC,OAAO,CAAC,mBAAmB,EAAE,GAAG,CAAC,CAAA;YACpC,EAAE,CAAC,OAAO,CAAC,0BAA0B,EAAE,GAAG,CAAC,CAAA;YAC3C,EAAE,CAAC,OAAO,CAAC,uBAAuB,EAAE,GAAG,CAAC,CAAA;YACxC,MAAM,KAAK,GAAG,EAAE,CAAC,KAAK,CAAC,aAAa,CAAC,SAAS,EAAE,SAAS,CAAC,CAAC,iBAAiB,CAAC,EAAC,SAAS,EAAE,EAAE,EAAE,QAAQ,EAAE,EAAE,EAAC,CAAC,CAAA;YAC3G,MAAM,IAAI,GAAG,EAAE,CAAC,KAAK,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC,eAAe,CAAC,SAAkB,CAAC,CAAA;YAC1E,YAAY,GAAG,GAAG,EAAE;gBAClB,KAAK,CAAC,WAAW,EAAE,CAAA;gBACnB,IAAI,CAAC,WAAW,EAAE,CAAA;YACpB,CAAC,CAAA;YACD,OAAO,GAAG,SAAS,CAAA;YACnB,OAAO,GAAG,MAAM,OAAO,CAAC,KAAK,EAAE,CAAA;QACjC,CAAC,CAAC,CAAA;QAEF,SAAS,CAAC,KAAK,IAAI,EAAE;YACnB,IAAI,CAAC;gBACH,MAAM,OAAO,EAAE,EAAE,CAAA;YACnB,CAAC;oBAAS,CAAC;gBACT,YAAY,EAAE,CAAA;gBACd,OAAO,CAAC,QAAQ,GAAG,gBAAgB,CAAA;gBACnC,EAAE,CAAC,aAAa,EAAE,CAAA;gBAClB,oBAAoB,EAAE,CAAC,KAAK,EAAE,CAAA;YAChC,CAAC;QACH,CAAC,CAAC,CAAA;QAEF,MAAM,GAAG,GAAG,KAAK,EACf,KAAe,EACf,WAAW,GAAG,OAAO,OAAO,CAAC,IAAI,KAAK,UAAU,CAAC,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,IAAI,EAChF,EAAE;YACF,MAAM,IAAI,GAAG,CAAC,GAAG,SAAS,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,GAAG,WAAW,EAAE,GAAG,KAAK,CAAC,CAAA;YAChE,MAAM,YAAY,GAAG,OAAO,CAAC,IAAI,CAAA;YACjC,MAAM,WAAW,GAAG,GAAG,EAAE;gBACvB,OAAO,CAAC,IAAI,GAAG,YAAY,CAAA;YAC7B,CAAC,CAAA;YACD,kGAAkG;YAClG,OAAO,CAAC,IAAI,GAAG,CAAC,GAAG,YAAY,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,GAAG,IAAI,CAAC,CAAA;YACrD,IAAI,CAAC;gBACH,MAAM,SAAS,CAAC,EAAC,SAAS,EAAE,IAAI,EAAE,iBAAiB,EAAE,KAAK,IAAI,EAAE,CAAC,OAAO,EAAC,CAAC,CAAA;YAC5E,CAAC;oBAAS,CAAC;gBACT,WAAW,EAAE,CAAA;YACf,CAAC;QACH,CAAC,CAAA;QAED,MAAM,YAAY,GAAG,CAAC,MAAc,EAAE,EAAE;YACtC,MAAM,cAAc,GAClB,OAAO,OAAO,CAAC,cAAc,KAAK,UAAU,CAAC,CAAC,CAAC,OAAO,CAAC,cAAc,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,cAAc,CAAA;YAClG,MAAM,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,SAAS,CAAC,cAAc,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,CAAA;YACnE,OAAO,CAAC,gBAAgB,CAAC,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAA;YACrD,OAAO,CAAC,cAAc,CAAC,MAAM,CAAC,CAAA;YAC9B,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,gBAAgB,EAAE,CAAA;YAC3C,MAAM,CAAC,OAAO,CAAC,QAAQ,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;QACvC,CAAC,CAAA;QAED,MAAM,YAAY,GAAG,CAAC,MAAc,EAAE,cAAc,GAAG,OAAO,CAAC,cAAc,IAAI,EAAE,EAAE,EAAE;YACrF,mBAAmB,CAAC,MAAM,EAAE,cAAc,CAAC,CAAA;QAC7C,CAAC,CAAA;QAED,IAAI,CAAC,IAAI,CAAC,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC,CAAC,2BAA2B,EAAE,KAAK,EAAE,IAAI,EAAE,EAAE;YACtE,MAAM,2BAA2B,CAAC,KAAK,EAAE,EAAC,MAAM,EAAE,MAAM,EAAC,EAAE,EAAE;gBAC3D,MAAM,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,CAAA;gBACjB,YAAY,CAAC,MAAM,EAAE,CAAC,CAAA;gBACtB,YAAY,CAAC,MAAM,EAAE,CAAC,CAAA;YACxB,CAAC,CAAC,CAAA;QACJ,CAAC,CAAC,CAAA;QAEF,IAAI,CAAC,oCAAoC,EAAE,KAAK,IAAI,EAAE;YACpD,EAAE,CAAC,OAAO,CAAC,mBAAmB,EAAE,GAAG,CAAC,CAAA;YACpC,MAAM,2BAA2B,CAAC,KAAK,EAAE,EAAC,MAAM,EAAE,MAAM,EAAC,EAAE,EAAE;gBAC3D,MAAM,GAAG,CAAC,EAAE,CAAC,CAAA;gBACb,YAAY,CAAC,MAAM,EAAE,CAAC,CAAA;gBACtB,YAAY,CAAC,MAAM,EAAE,CAAC,CAAA;YACxB,CAAC,CAAC,CAAA;QACJ,CAAC,CAAC,CAAA;QAEF,MAAM,eAAe,GAAG,UAAU,IAAI,CAAC,OAAO,CAAC,KAAK,IAAI,EAAE,CAAC,IAAI,UAAU,IAAI,OAAO,CAAC,SAAS,CAAA;QAC9F,IAAI,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC,CACtB,eAAe,CAAC,CAAC,CAAC,0CAA0C,CAAC,CAAC,CAAC,gDAAgD,EAC/G,KAAK,EAAE,IAAI,EAAE,EAAE;YACb,MAAM,2BAA2B,CAAC,KAAK,EAAE,EAAC,MAAM,EAAE,MAAM,EAAC,EAAE,EAAE;gBAC3D,MAAM,GAAG,CAAC,CAAC,GAAG,CAAC,eAAe,CAAC,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAA;gBACpF,IAAI,IAAI,EAAE,CAAC;oBACT,YAAY,CAAC,MAAM,EAAE,CAAC,CAAA;oBACtB,YAAY,CAAC,MAAM,EAAE,CAAC,CAAA;gBACxB,CAAC;qBAAM,CAAC;oBACN,MAAM,UAAU,GAAG,EAAC,MAAM,EAAE,MAAM,EAAE,EAAE,MAAM,EAAE,QAAQ,CAAC,MAAM,EAAE,CAAC,EAAC,CAAA;oBACjE,IAAI,OAAO,OAAO,CAAC,UAAU,KAAK,UAAU,EAAE,CAAC;wBAC7C,OAAO,CAAC,UAAU,CAAC,UAAU,CAAC,CAAA;oBAChC,CAAC;yBAAM,CAAC;wBACN,MAAM,CAAC,UAAU,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,UAAU,CAAC,CAAA;oBAChD,CAAC;oBACD,OAAO,CAAC,cAAc,CAAC,MAAM,CAAC,CAAA;oBAC9B,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,gBAAgB,EAAE,CAAA;oBAC3C,MAAM,CAAC,OAAO,CAAC,QAAQ,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;gBACvC,CAAC;YACH,CAAC,CAAC,CAAA;QACJ,CAAC,CACF,CAAA;QAED,IAAI,CAAC,mCAAmC,EAAE,GAAG,EAAE;YAC7C,MAAM,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC,SAAS,CACnC,4CAA4C,OAAO,CAAC,gBAAgB,CAAC,IAAI,YAAY,CACtF,CAAA;QACH,CAAC,CAAC,CAAA;QAEF,IAAI,CAAC,uEAAuE,EAAE,KAAK,IAAI,EAAE;YACvF,MAAM,2BAA2B,CAAC,KAAK,EAAE,EAAC,MAAM,EAAE,MAAM,EAAC,EAAE,EAAE;gBAC3D,MAAM,GAAG,CAAC,CAAC,eAAe,CAAC,EAAE,EAAE,CAAC,CAAA;gBAChC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC,aAAa,CAAC,OAAO,CAAC,cAAc,CAAC,CAAA;gBACrF,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;gBACzB,MAAM,CAAC,aAAa,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC,GAAG,CAAC,gBAAgB,EAAE,CAAA;gBAC9D,OAAO,CAAC,iBAAiB,EAAE,CAAA;gBAC3B,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,gBAAgB,EAAE,CAAA;YAC7C,CAAC,CAAC,CAAA;QACJ,CAAC,CAAC,CAAA;QAEF,MAAM,WAAW,GAAG,OAAO,CAAC,WAAW,CAAA;QACvC,IAAI,WAAW,EAAE,CAAC;YAChB,IAAI,CAAC,yDAAyD,EAAE,KAAK,IAAI,EAAE;gBACzE,MAAM,WAAW,CAAC,KAAK,EAAE,CAAA;gBACzB,MAAM,2BAA2B,CAAC,KAAK,EAAE,EAAC,MAAM,EAAE,MAAM,EAAC,EAAE,EAAE;oBAC3D,MAAM,GAAG,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAA;oBACrB,YAAY,CAAC,MAAM,EAAE,CAAC,CAAA;oBACtB,YAAY,CAAC,MAAM,EAAE,EAAE,WAAW,CAAC,cAAc,CAAC,CAAA;gBACpD,CAAC,CAAC,CAAA;YACJ,CAAC,CAAC,CAAA;QACJ,CAAC;QAED,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,CAAA;QAC/B,IAAI,OAAO,EAAE,CAAC;YACZ,IAAI,CAAC,iEAAiE,EAAE,KAAK,IAAI,EAAE;gBACjF,MAAM,OAAO,CAAC,KAAK,EAAE,CAAA;gBACrB,MAAM,2BAA2B,CAAC,KAAK,EAAE,EAAC,MAAM,EAAE,MAAM,EAAC,EAAE,EAAE;oBAC3D,MAAM,GAAG,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAA;oBACrB,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,aAAa,CAAC,CAAA;oBAC3D,YAAY,CAAC,MAAM,EAAE,EAAE,OAAO,CAAC,cAAc,IAAI,EAAE,CAAC,CAAA;oBACpD,OAAO,CAAC,cAAc,CAAC,MAAM,CAAC,CAAA;oBAC9B,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,+BAA+B,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAA;gBACxE,CAAC,CAAC,CAAA;YACJ,CAAC,CAAC,CAAA;QACJ,CAAC;IACH,CAAC,CAAC,CAAA;AACJ,CAAC","sourcesContent":["import {withCapturedStandardStreams, mockAndCaptureOutput} from './output.js'\nimport {launchCLI} from '../cli-launcher.js'\nimport {ShopifyConfig} from '../custom-oclif-loader.js'\nimport {expectCommandEvents, type CommandEventExpectation} from '../../../private/node/testing/json-command-events.js'\nimport {unstyled} from '../output.js'\nimport {afterEach, beforeEach, describe, expect, test, vi} from 'vitest'\nimport type BaseCommand from '../base-command.js'\nimport type {JsonOutputSchema} from '../json-output-schema.js'\n\ntype JsonCommandResult = Record<string, unknown> | ReadonlyArray<unknown>\ntype TestCleanup = () => void | Promise<void>\ninterface TextOutput {\n stdout: string\n stderr: string\n}\n\ninterface JsonCommandTestOptions {\n command: typeof BaseCommand & {readonly jsonOutputSchema: JsonOutputSchema}\n commandId: string\n moduleURL: string\n args: string[] | (() => string[])\n expectedResult: JsonCommandResult | (() => JsonCommandResult)\n expectedEvents?: CommandEventExpectation\n expectedSchema: Record<string, unknown>\n setup: () => void | TestCleanup | Promise<void | TestCleanup>\n assertExecuted: (format: 'text' | 'json') => void\n assertNotExecuted: () => void\n textOutput: TextOutput | ((output: TextOutput) => void)\n diagnostics?: {\n setup: () => void | Promise<void>\n expectedEvents: CommandEventExpectation\n }\n failure?: {\n setup: () => void | Promise<void>\n expectedError: Record<string, unknown>\n expectedEvents?: CommandEventExpectation\n exitCode: number\n }\n}\n\n/**\n * Registers shared JSON contract tests for a finite command using its real launcher and output writers.\n * Supply independent expected results and callbacks that check the command's service calls.\n * Keep domain-specific input, schema, prompt, and file tests beside the command.\n * This suite captures process globals and must not run concurrently with other tests.\n *\n * @param options - The command, fixtures, expected output, and service assertions.\n */\nexport function describeJsonCommand(options: JsonCommandTestOptions): void {\n const {command, commandId, moduleURL} = options\n\n describe.sequential(`${commandId} JSON contract`, () => {\n let originalExitCode: typeof process.exitCode\n let restoreMocks: () => void\n let cleanup: void | TestCleanup\n\n beforeEach(async () => {\n originalExitCode = process.exitCode\n process.exitCode = undefined\n vi.stubEnv('CI', '1')\n vi.stubEnv('SHOPIFY_CLI_NO_ANALYTICS', '1')\n vi.stubEnv('SHOPIFY_FLAG_JSON', '0')\n vi.stubEnv('SHOPIFY_FLAG_JSON_SCHEMA', '0')\n vi.stubEnv('SHOPIFY_FLAG_NO_INPUT', '0')\n const hooks = vi.spyOn(ShopifyConfig.prototype, 'runHook').mockResolvedValue({successes: [], failures: []})\n const exit = vi.spyOn(process, 'exit').mockReturnValue(undefined as never)\n restoreMocks = () => {\n hooks.mockRestore()\n exit.mockRestore()\n }\n cleanup = undefined\n cleanup = await options.setup()\n })\n\n afterEach(async () => {\n try {\n await cleanup?.()\n } finally {\n restoreMocks()\n process.exitCode = originalExitCode\n vi.unstubAllEnvs()\n mockAndCaptureOutput().clear()\n }\n })\n\n const run = async (\n flags: string[],\n commandArgs = typeof options.args === 'function' ? options.args() : options.args,\n ) => {\n const argv = [...commandId.split(':'), ...commandArgs, ...flags]\n const originalArgv = process.argv\n const restoreArgv = () => {\n process.argv = originalArgv\n }\n // Error handling and some output helpers read process.argv instead of the launcher's argv option.\n process.argv = [...originalArgv.slice(0, 2), ...argv]\n try {\n await launchCLI({moduleURL, argv, lazyCommandLoader: async () => command})\n } finally {\n restoreArgv()\n }\n }\n\n const expectResult = (stdout: string) => {\n const expectedResult =\n typeof options.expectedResult === 'function' ? options.expectedResult() : options.expectedResult\n expect(stdout).toBe(`${JSON.stringify(expectedResult, null, 2)}\\n`)\n command.jsonOutputSchema.validate(JSON.parse(stdout))\n options.assertExecuted('json')\n expect(process.exit).not.toHaveBeenCalled()\n expect(process.exitCode ?? 0).toBe(0)\n }\n\n const expectEvents = (stderr: string, expectedEvents = options.expectedEvents ?? []) => {\n expectCommandEvents(stderr, expectedEvents)\n }\n\n test.each(['--json', '-j'])('writes one result with %s', async (flag) => {\n await withCapturedStandardStreams(async ({stdout, stderr}) => {\n await run([flag])\n expectResult(stdout())\n expectEvents(stderr())\n })\n })\n\n test('supports the JSON environment flag', async () => {\n vi.stubEnv('SHOPIFY_FLAG_JSON', '1')\n await withCapturedStandardStreams(async ({stdout, stderr}) => {\n await run([])\n expectResult(stdout())\n expectEvents(stderr())\n })\n })\n\n const supportsNoInput = 'no-input' in (command.flags ?? {}) || 'no-input' in command.baseFlags\n test.each([false, true])(\n supportsNoInput ? 'keeps no-input independent from JSON: %s' : 'selects text and JSON output independently: %s',\n async (json) => {\n await withCapturedStandardStreams(async ({stdout, stderr}) => {\n await run([...(supportsNoInput ? ['--no-input'] : []), ...(json ? ['--json'] : [])])\n if (json) {\n expectResult(stdout())\n expectEvents(stderr())\n } else {\n const textOutput = {stdout: stdout(), stderr: unstyled(stderr())}\n if (typeof options.textOutput === 'function') {\n options.textOutput(textOutput)\n } else {\n expect(textOutput).toEqual(options.textOutput)\n }\n options.assertExecuted('text')\n expect(process.exit).not.toHaveBeenCalled()\n expect(process.exitCode ?? 0).toBe(0)\n }\n })\n },\n )\n\n test('exposes the result schema in help', () => {\n expect(command.description).toContain(\n `Output from \\`--json\\` conforms to the \\`${command.jsonOutputSchema.name}\\` schema.`,\n )\n })\n\n test('discovers the schema without required inputs, hooks, or service calls', async () => {\n await withCapturedStandardStreams(async ({stdout, stderr}) => {\n await run(['--json-schema'], [])\n expect(JSON.parse(stdout()).definitions.Result).toMatchObject(options.expectedSchema)\n expect(stderr()).toBe('')\n expect(ShopifyConfig.prototype.runHook).not.toHaveBeenCalled()\n options.assertNotExecuted()\n expect(process.exit).not.toHaveBeenCalled()\n })\n })\n\n const diagnostics = options.diagnostics\n if (diagnostics) {\n test('writes diagnostics to stderr separately from the result', async () => {\n await diagnostics.setup()\n await withCapturedStandardStreams(async ({stdout, stderr}) => {\n await run(['--json'])\n expectResult(stdout())\n expectEvents(stderr(), diagnostics.expectedEvents)\n })\n })\n }\n\n const failure = options.failure\n if (failure) {\n test('writes only the fatal error and preserves the failure exit code', async () => {\n await failure.setup()\n await withCapturedStandardStreams(async ({stdout, stderr}) => {\n await run(['--json'])\n expect(JSON.parse(stdout())).toEqual(failure.expectedError)\n expectEvents(stderr(), failure.expectedEvents ?? [])\n options.assertExecuted('json')\n expect(process.exit).toHaveBeenCalledExactlyOnceWith(failure.exitCode)\n })\n })\n }\n })\n}\n"]}
|
|
@@ -23,7 +23,7 @@ export interface CapturedStandardStreams {
|
|
|
23
23
|
*
|
|
24
24
|
* The callback receives accessors instead of the function returning captured output so that
|
|
25
25
|
* assertions remain possible when the callback throws (for example commands that abort).
|
|
26
|
-
* Streams, console
|
|
26
|
+
* Streams, console and unit-test detection are restored afterwards.
|
|
27
27
|
* Not safe for concurrent tests.
|
|
28
28
|
*
|
|
29
29
|
* @param run - Callback receiving accessors for the captured stdout and stderr.
|
|
@@ -31,7 +31,7 @@ export interface CapturedStandardStreams {
|
|
|
31
31
|
*/
|
|
32
32
|
export declare function withCapturedStandardStreams<T>(run: (streams: CapturedStandardStreams) => T | Promise<T>): Promise<T>;
|
|
33
33
|
/**
|
|
34
|
-
* Captures writes to stdout and stderr, including console
|
|
34
|
+
* Captures writes to stdout and stderr, including console output intercepted by Vitest.
|
|
35
35
|
* Call restore in a finally block. This replaces process globals and must not be used in concurrent tests.
|
|
36
36
|
* Prefer withCapturedStandardStreams, which also disables unit-test output suppression while it runs.
|
|
37
37
|
*
|
|
@@ -9,7 +9,7 @@ import { Console } from 'node:console';
|
|
|
9
9
|
*
|
|
10
10
|
* The callback receives accessors instead of the function returning captured output so that
|
|
11
11
|
* assertions remain possible when the callback throws (for example commands that abort).
|
|
12
|
-
* Streams, console
|
|
12
|
+
* Streams, console and unit-test detection are restored afterwards.
|
|
13
13
|
* Not safe for concurrent tests.
|
|
14
14
|
*
|
|
15
15
|
* @param run - Callback receiving accessors for the captured stdout and stderr.
|
|
@@ -39,7 +39,7 @@ function setUnitTestVariable(value) {
|
|
|
39
39
|
}
|
|
40
40
|
}
|
|
41
41
|
/**
|
|
42
|
-
* Captures writes to stdout and stderr, including console
|
|
42
|
+
* Captures writes to stdout and stderr, including console output intercepted by Vitest.
|
|
43
43
|
* Call restore in a finally block. This replaces process globals and must not be used in concurrent tests.
|
|
44
44
|
* Prefer withCapturedStandardStreams, which also disables unit-test output suppression while it runs.
|
|
45
45
|
*
|
|
@@ -48,17 +48,14 @@ function setUnitTestVariable(value) {
|
|
|
48
48
|
export function mockAndCaptureStandardStreams() {
|
|
49
49
|
const stdout = captureStream(process.stdout);
|
|
50
50
|
const stderr = captureStream(process.stderr);
|
|
51
|
-
// Vitest intercepts console
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
// eslint-disable-next-line no-console
|
|
55
|
-
console.warn = new Console(process.stdout, process.stderr).warn;
|
|
51
|
+
// Vitest intercepts console output before it reaches the streams; use Node's console to exercise the writers.
|
|
52
|
+
const originalConsole = globalThis.console;
|
|
53
|
+
globalThis.console = Object.assign(new Console(process.stdout, process.stderr), { Console });
|
|
56
54
|
return {
|
|
57
55
|
stdout: stdout.output,
|
|
58
56
|
stderr: stderr.output,
|
|
59
57
|
restore: () => {
|
|
60
|
-
|
|
61
|
-
console.warn = originalWarn;
|
|
58
|
+
globalThis.console = originalConsole;
|
|
62
59
|
stdout.restore();
|
|
63
60
|
stderr.restore();
|
|
64
61
|
},
|
|
@@ -67,6 +64,7 @@ export function mockAndCaptureStandardStreams() {
|
|
|
67
64
|
function captureStream(stream) {
|
|
68
65
|
const chunks = [];
|
|
69
66
|
const originalWrite = stream.write;
|
|
67
|
+
const originalDrainListeners = new Set(stream.rawListeners('drain'));
|
|
70
68
|
stream.write = (chunk, encodingOrCallback, callback) => {
|
|
71
69
|
const encoding = typeof encodingOrCallback === 'string' ? encodingOrCallback : 'utf8';
|
|
72
70
|
chunks.push(typeof chunk === 'string' ? Buffer.from(chunk, encoding) : Buffer.from(chunk));
|
|
@@ -79,6 +77,11 @@ function captureStream(stream) {
|
|
|
79
77
|
output: () => Buffer.concat(chunks).toString('utf8'),
|
|
80
78
|
restore: () => {
|
|
81
79
|
stream.write = originalWrite;
|
|
80
|
+
// Captured writes always succeed, so Oclif's flush listener never receives a drain event.
|
|
81
|
+
for (const listener of stream.rawListeners('drain')) {
|
|
82
|
+
if (!originalDrainListeners.has(listener))
|
|
83
|
+
stream.removeListener('drain', listener);
|
|
84
|
+
}
|
|
82
85
|
},
|
|
83
86
|
};
|
|
84
87
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"output.js","sourceRoot":"","sources":["../../../../src/public/node/testing/output.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,aAAa,EAAE,kBAAkB,EAAC,MAAM,cAAc,CAAA;AAC9D,OAAO,EAAC,uBAAuB,EAAC,MAAM,qBAAqB,CAAA;AAC3D,OAAO,EAAC,oBAAoB,EAAC,MAAM,oCAAoC,CAAA;AACvE,mDAAmD;AACnD,OAAO,EAAC,OAAO,EAAC,MAAM,cAAc,CAAA;AAwBpC;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,KAAK,UAAU,2BAA2B,CAC/C,GAAyD;IAEzD,4GAA4G;IAC5G,MAAM,wBAAwB,GAAG,OAAO,CAAC,GAAG,CAAC,oBAAoB,CAAC,QAAQ,CAAC,CAAA;IAC3E,mBAAmB,CAAC,GAAG,CAAC,CAAA;IACxB,uBAAuB,EAAE,CAAA;IACzB,MAAM,OAAO,GAAG,6BAA6B,EAAE,CAAA;IAC/C,IAAI,CAAC;QACH,OAAO,MAAM,GAAG,CAAC,EAAC,MAAM,EAAE,OAAO,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,MAAM,EAAC,CAAC,CAAA;IACpE,CAAC;YAAS,CAAC;QACT,OAAO,CAAC,OAAO,EAAE,CAAA;QACjB,mBAAmB,CAAC,wBAAwB,CAAC,CAAA;QAC7C,uBAAuB,EAAE,CAAA;IAC3B,CAAC;AACH,CAAC;AAED,SAAS,mBAAmB,CAAC,KAAyB;IACpD,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;QACxB,OAAO,OAAO,CAAC,GAAG,CAAC,oBAAoB,CAAC,QAAQ,CAAC,CAAA;IACnD,CAAC;SAAM,CAAC;QACN,OAAO,CAAC,GAAG,CAAC,oBAAoB,CAAC,QAAQ,CAAC,GAAG,KAAK,CAAA;IACpD,CAAC;AACH,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,6BAA6B;IAC3C,MAAM,MAAM,GAAG,aAAa,CAAC,OAAO,CAAC,MAAM,CAAC,CAAA;IAC5C,MAAM,MAAM,GAAG,aAAa,CAAC,OAAO,CAAC,MAAM,CAAC,CAAA;IAC5C,
|
|
1
|
+
{"version":3,"file":"output.js","sourceRoot":"","sources":["../../../../src/public/node/testing/output.ts"],"names":[],"mappings":"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","sourcesContent":["import {collectedLogs, clearCollectedLogs} from '../output.js'\nimport {resetMemoizedIsUnitTest} from '../context/local.js'\nimport {environmentVariables} from '../../../private/node/constants.js'\n// eslint-disable-next-line n/prefer-global/console\nimport {Console} from 'node:console'\n\ninterface OutputMock {\n output: () => string\n info: () => string\n debug: () => string\n success: () => string\n completed: () => string\n warn: () => string\n error: () => string\n clear: () => void\n}\n\ninterface StandardStreamsMock {\n stdout: () => string\n stderr: () => string\n restore: () => void\n}\n\nexport interface CapturedStandardStreams {\n stdout: () => string\n stderr: () => string\n}\n\n/**\n * Runs a callback with process stdout/stderr captured and unit-test output suppression disabled,\n * so tests can assert on what a command actually writes to the standard streams.\n *\n * The callback receives accessors instead of the function returning captured output so that\n * assertions remain possible when the callback throws (for example commands that abort).\n * Streams, console and unit-test detection are restored afterwards.\n * Not safe for concurrent tests.\n *\n * @param run - Callback receiving accessors for the captured stdout and stderr.\n * @returns The value returned by the callback.\n */\nexport async function withCapturedStandardStreams<T>(\n run: (streams: CapturedStandardStreams) => T | Promise<T>,\n): Promise<T> {\n // Computed access on purpose: Vitest compile-replaces the literal process.env.SHOPIFY_UNIT_TEST expression.\n const previousUnitTestVariable = process.env[environmentVariables.unitTest]\n setUnitTestVariable('0')\n resetMemoizedIsUnitTest()\n const streams = mockAndCaptureStandardStreams()\n try {\n return await run({stdout: streams.stdout, stderr: streams.stderr})\n } finally {\n streams.restore()\n setUnitTestVariable(previousUnitTestVariable)\n resetMemoizedIsUnitTest()\n }\n}\n\nfunction setUnitTestVariable(value: string | undefined): void {\n if (value === undefined) {\n delete process.env[environmentVariables.unitTest]\n } else {\n process.env[environmentVariables.unitTest] = value\n }\n}\n\n/**\n * Captures writes to stdout and stderr, including console output intercepted by Vitest.\n * Call restore in a finally block. This replaces process globals and must not be used in concurrent tests.\n * Prefer withCapturedStandardStreams, which also disables unit-test output suppression while it runs.\n *\n * @returns Captured output and a function to restore the original writers.\n */\nexport function mockAndCaptureStandardStreams(): StandardStreamsMock {\n const stdout = captureStream(process.stdout)\n const stderr = captureStream(process.stderr)\n // Vitest intercepts console output before it reaches the streams; use Node's console to exercise the writers.\n const originalConsole = globalThis.console\n globalThis.console = Object.assign(new Console(process.stdout, process.stderr), {Console})\n\n return {\n stdout: stdout.output,\n stderr: stderr.output,\n restore: () => {\n globalThis.console = originalConsole\n stdout.restore()\n stderr.restore()\n },\n }\n}\n\nfunction captureStream(stream: NodeJS.WriteStream) {\n const chunks: Buffer[] = []\n const originalWrite = stream.write\n const originalDrainListeners = new Set(stream.rawListeners('drain'))\n stream.write = (\n chunk: string | Uint8Array,\n encodingOrCallback?: BufferEncoding | ((error?: Error | null) => void),\n callback?: (error?: Error | null) => void,\n ) => {\n const encoding = typeof encodingOrCallback === 'string' ? encodingOrCallback : 'utf8'\n chunks.push(typeof chunk === 'string' ? Buffer.from(chunk, encoding) : Buffer.from(chunk))\n const onWrite = typeof encodingOrCallback === 'function' ? encodingOrCallback : callback\n if (onWrite) queueMicrotask(() => onWrite())\n return true\n }\n\n return {\n output: () => Buffer.concat(chunks).toString('utf8'),\n restore: () => {\n stream.write = originalWrite\n // Captured writes always succeed, so Oclif's flush listener never receives a drain event.\n for (const listener of stream.rawListeners('drain')) {\n if (!originalDrainListeners.has(listener)) stream.removeListener('drain', listener as () => void)\n }\n },\n }\n}\n\n/**\n * Returns a set of functions to get the outputs ocurred during a test run.\n *\n * @returns An mock object with all the output functions.\n */ export function mockAndCaptureOutput(): OutputMock {\n return {\n output: () => (collectedLogs.output ?? []).join('\\n'),\n info: () => (collectedLogs.info ?? []).join('\\n'),\n debug: () => (collectedLogs.debug ?? []).join('\\n'),\n success: () => (collectedLogs.success ?? []).join('\\n'),\n completed: () => (collectedLogs.completed ?? []).join('\\n'),\n warn: () => (collectedLogs.warn ?? []).join('\\n'),\n error: () => (collectedLogs.error ?? []).join('\\n'),\n clear: () => {\n clearCollectedLogs()\n // output.collectedLogs = {}\n },\n }\n}\n"]}
|