|
| 1 | +import FunctionTypegen from './typegen.js' |
| 2 | +import {chooseFunction} from '../../../services/function/common.js' |
| 3 | +import {localAppContext} from '../../../services/app-context.js' |
| 4 | +import {testApp, testFunctionExtension, testProject} from '../../../models/app/app.test-data.js' |
| 5 | +import {functionTypegenJsonOutputSchema} from '../../../services/function/typegen/types.js' |
| 6 | +import {Config} from '@oclif/core' |
| 7 | +import {afterEach, expect, test, vi} from 'vitest' |
| 8 | +import {inTemporaryDirectory, readFile, writeFile} from '@shopify/cli-kit/node/fs' |
| 9 | +import {joinPath} from '@shopify/cli-kit/node/path' |
| 10 | +import {withCapturedStandardStreams} from '@shopify/cli-kit/node/testing/output' |
| 11 | +import {runWithCommandEventsForCommand} from '@shopify/cli-kit/node/command-events' |
| 12 | +import {ExternalError, AbortError, handler} from '@shopify/cli-kit/node/error' |
| 13 | +import {exec} from '@shopify/cli-kit/node/system' |
| 14 | +import {packageManagerBinaryCommandForDirectory} from '@shopify/cli-kit/node/node-package-manager' |
| 15 | +import {unstyled} from '@shopify/cli-kit/node/output' |
| 16 | + |
| 17 | +// eslint-disable-next-line no-restricted-imports |
| 18 | +import {resolve} from 'node:path' |
| 19 | + |
| 20 | +vi.mock('../../../services/app-context.js') |
| 21 | +vi.mock('@shopify/cli-kit/node/system', async (importOriginal) => ({ |
| 22 | + ...(await importOriginal<typeof import('@shopify/cli-kit/node/system')>()), |
| 23 | + exec: vi.fn(), |
| 24 | +})) |
| 25 | +vi.mock('@shopify/cli-kit/node/node-package-manager', async (importOriginal) => ({ |
| 26 | + ...(await importOriginal<typeof import('@shopify/cli-kit/node/node-package-manager')>()), |
| 27 | + packageManagerBinaryCommandForDirectory: vi.fn(), |
| 28 | +})) |
| 29 | +vi.mock('../../../services/function/common.js', async (importOriginal) => ({ |
| 30 | + ...(await importOriginal<typeof import('../../../services/function/common.js')>()), |
| 31 | + chooseFunction: vi.fn(), |
| 32 | +})) |
| 33 | +afterEach(() => vi.unstubAllEnvs()) |
| 34 | + |
| 35 | +async function command(argv: string[], directory: string, custom = false) { |
| 36 | + const app = testApp({directory}) |
| 37 | + const extension = await testFunctionExtension({dir: directory, entryPath: 'src/index.js'}) |
| 38 | + if (custom) extension.configuration.build!.typegen_command = 'custom-codegen --target test' |
| 39 | + vi.mocked(localAppContext).mockResolvedValue({app, project: testProject(), activeConfig: {} as never}) |
| 40 | + vi.mocked(chooseFunction).mockResolvedValue(extension) |
| 41 | + vi.mocked(packageManagerBinaryCommandForDirectory).mockResolvedValue({ |
| 42 | + command: 'codegen', |
| 43 | + args: ['--config', 'package.json'], |
| 44 | + }) |
| 45 | + return {instance: new FunctionTypegen(argv, await Config.load()), extension, app} |
| 46 | +} |
| 47 | + |
| 48 | +test.each([ |
| 49 | + {custom: true, argv: ['--json', '--no-input']}, |
| 50 | + {custom: true, argv: ['--json']}, |
| 51 | + {custom: false, argv: ['--json', '--no-input']}, |
| 52 | + {custom: false, argv: ['--json']}, |
| 53 | + {custom: true, argv: ['-jcstaging']}, |
| 54 | + {custom: false, argv: ['-jcstaging']}, |
| 55 | + {custom: true, argv: ['-j']}, |
| 56 | + {custom: false, argv: ['-j']}, |
| 57 | +])('emits one result and codegen side events: %j', async ({custom, argv}) => { |
| 58 | + await inTemporaryDirectory(async (directory) => { |
| 59 | + const {instance, extension, app} = await command(argv, directory, custom) |
| 60 | + vi.mocked(exec).mockImplementation(async (_command, _args, options) => { |
| 61 | + if (options?.stdout && options.stdout !== 'inherit') options.stdout.write('Generating types\n') |
| 62 | + if (options?.stderr && options.stderr !== 'inherit') options.stderr.write('Codegen warning\n') |
| 63 | + await writeFile(joinPath(directory, 'generated.ts'), 'export type Input = {}\n') |
| 64 | + }) |
| 65 | + await withCapturedStandardStreams(async ({stdout, stderr}) => { |
| 66 | + await expect(runWithCommandEventsForCommand(argv, () => instance.run())).resolves.toEqual({app}) |
| 67 | + expect(JSON.parse(stdout())).toEqual({ |
| 68 | + status: 'success', |
| 69 | + function: {handle: extension.configuration.handle ?? null, name: extension.name, directory: resolve(directory)}, |
| 70 | + }) |
| 71 | + const events = stderr() |
| 72 | + .trim() |
| 73 | + .split('\n') |
| 74 | + .map((line) => JSON.parse(line)) |
| 75 | + expect(events).toContainEqual( |
| 76 | + expect.objectContaining({type: 'diagnostic', message: expect.stringContaining('Codegen warning')}), |
| 77 | + ) |
| 78 | + if (custom) |
| 79 | + expect(events).toContainEqual( |
| 80 | + expect.objectContaining({type: 'diagnostic', message: expect.stringContaining('Generating types')}), |
| 81 | + ) |
| 82 | + expect(stderr()).not.toContain('GraphQL types generated successfully.') |
| 83 | + }) |
| 84 | + await expect(readFile(joinPath(directory, 'generated.ts'))).resolves.toBe('export type Input = {}\n') |
| 85 | + expect(exec).toHaveBeenCalledWith( |
| 86 | + custom ? 'custom-codegen' : 'codegen', |
| 87 | + custom ? ['--target', 'test'] : ['--config', 'package.json'], |
| 88 | + expect.objectContaining({cwd: directory}), |
| 89 | + ) |
| 90 | + }) |
| 91 | +}) |
| 92 | + |
| 93 | +test.each([[], ['--no-input']])('keeps custom codegen text output and the success banner: %j', async (...argv) => { |
| 94 | + await inTemporaryDirectory(async (directory) => { |
| 95 | + const {instance} = await command(argv, directory, true) |
| 96 | + vi.mocked(exec).mockImplementation(async (_command, _args, options) => { |
| 97 | + if (options?.stdout && options.stdout !== 'inherit') options.stdout.write('Native codegen output\n') |
| 98 | + }) |
| 99 | + await withCapturedStandardStreams(async ({stdout, stderr}) => { |
| 100 | + await instance.run() |
| 101 | + expect(stdout()).toBe('Native codegen output\n') |
| 102 | + expect(unstyled(stderr())).toContain('GraphQL types generated successfully.') |
| 103 | + }) |
| 104 | + }) |
| 105 | +}) |
| 106 | + |
| 107 | +test('emits one fatal document when the custom generator fails', async () => { |
| 108 | + vi.stubEnv('SHOPIFY_FLAG_JSON', '1') |
| 109 | + await inTemporaryDirectory(async (directory) => { |
| 110 | + const {instance} = await command(['--json'], directory, true) |
| 111 | + vi.mocked(exec).mockRejectedValue(new ExternalError('Generator failed', 'custom-codegen', [])) |
| 112 | + await withCapturedStandardStreams(async ({stdout}) => { |
| 113 | + await runWithCommandEventsForCommand(instance.argv, async () => { |
| 114 | + try { |
| 115 | + await instance.run() |
| 116 | + expect.unreachable('Expected typegen to fail') |
| 117 | + } catch (error) { |
| 118 | + if (!(error instanceof ExternalError)) throw error |
| 119 | + await handler(error) |
| 120 | + } |
| 121 | + }) |
| 122 | + expect(JSON.parse(stdout())).toMatchObject({error: {type: 'external', message: 'Generator failed'}}) |
| 123 | + expect(stdout()).not.toContain('"status":"success"') |
| 124 | + }) |
| 125 | + }) |
| 126 | +}) |
| 127 | + |
| 128 | +test('keeps the missing custom generator error for non-JavaScript functions', async () => { |
| 129 | + await inTemporaryDirectory(async (directory) => { |
| 130 | + const {instance, extension} = await command(['--json'], directory) |
| 131 | + extension.entrySourceFilePath = 'src/main.rs' |
| 132 | + await withCapturedStandardStreams(async ({stdout}) => { |
| 133 | + await expect(runWithCommandEventsForCommand(instance.argv, () => instance.run())).rejects.toThrow(AbortError) |
| 134 | + expect(stdout()).toBe('') |
| 135 | + }) |
| 136 | + expect(exec).not.toHaveBeenCalled() |
| 137 | + }) |
| 138 | +}) |
| 139 | + |
| 140 | +test('exposes the result schema and retains inherited flags', () => { |
| 141 | + expect(FunctionTypegen.flags.json).toBeDefined() |
| 142 | + expect(FunctionTypegen.flags['no-input']).toBeDefined() |
| 143 | + expect(FunctionTypegen.jsonOutputSchema).toBe(functionTypegenJsonOutputSchema) |
| 144 | + expect(FunctionTypegen.descriptionForHelp()).toContain('FunctionTypegenResult') |
| 145 | +}) |
0 commit comments