From 7ae2684b225168a60ed7c214456709d2528ad841 Mon Sep 17 00:00:00 2001 From: Tobias Binna Date: Fri, 28 Aug 2026 23:14:28 +0800 Subject: [PATCH] fix(executors): launch Forge without shell interpretation Closes #209 --- .github/workflows/ci-pr.yml | 70 +++++++- package.json | 2 + packages/nx-forge/package.json | 1 + .../src/executors/deploy/executor.spec.ts | 14 +- .../src/executors/register/executor.spec.ts | 62 +++++++ .../src/executors/register/executor.ts | 2 +- .../src/utils/forge/async-commands.spec.ts | 160 ++++++++++++++++++ .../src/utils/forge/async-commands.ts | 22 ++- pnpm-lock.yaml | 113 ++++++++----- pnpm-workspace.yaml | 2 + 10 files changed, 378 insertions(+), 70 deletions(-) create mode 100644 packages/nx-forge/src/executors/register/executor.spec.ts create mode 100644 packages/nx-forge/src/utils/forge/async-commands.spec.ts diff --git a/.github/workflows/ci-pr.yml b/.github/workflows/ci-pr.yml index e8d8342..60bd3aa 100644 --- a/.github/workflows/ci-pr.yml +++ b/.github/workflows/ci-pr.yml @@ -4,15 +4,76 @@ on: paths-ignore: - 'docs/**' +permissions: + actions: read + contents: read + jobs: - pr: + quality: + name: Quality + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + with: + ref: ${{ github.event.pull_request.head.sha }} + repository: ${{github.event.pull_request.head.repo.full_name}} + fetch-depth: 0 + persist-credentials: false + - uses: nrwl/nx-set-shas@v5 + name: Derive appropriate SHAs for base and head for `nx affected` commands + - uses: pnpm/action-setup@v6 + with: + run_install: false + - uses: actions/setup-node@v5 + with: + node-version: 'lts/*' + check-latest: true + package-manager-cache: false + - run: pnpm install --frozen-lockfile + - run: pnpm exec commitlint --from ${{ github.event.pull_request.base.sha }} --to ${{ github.event.pull_request.head.sha }} --verbose + - run: pnpm nx affected --target=format:check --parallel=2 + - run: pnpm nx affected --target=lint --parallel=2 + + build-and-test: + name: Build and test (${{ matrix.os }}) + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, windows-latest] + steps: + - uses: actions/checkout@v5 + with: + ref: ${{ github.event.pull_request.head.sha }} + repository: ${{github.event.pull_request.head.repo.full_name}} + fetch-depth: 0 + persist-credentials: false + - uses: nrwl/nx-set-shas@v5 + name: Derive appropriate SHAs for base and head for `nx affected` commands + - uses: pnpm/action-setup@v6 + with: + run_install: false + - uses: actions/setup-node@v5 + with: + node-version: 'lts/*' + check-latest: true + cache: pnpm + - run: pnpm install --frozen-lockfile + - run: pnpm nx affected --target=build --parallel=3 + - run: pnpm nx affected --target=test --parallel=2 + env: + NODE_OPTIONS: '--throw-deprecation' + + e2e: + name: E2E + needs: [quality, build-and-test] runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 with: - ref: ${{ github.event.pull_request.head.ref }} + ref: ${{ github.event.pull_request.head.sha }} repository: ${{github.event.pull_request.head.repo.full_name}} fetch-depth: 0 + persist-credentials: false - uses: nrwl/nx-set-shas@v5 name: Derive appropriate SHAs for base and head for `nx affected` commands - uses: pnpm/action-setup@v6 @@ -31,11 +92,6 @@ jobs: echo ATLASSIAN_SITE_URL=${{ vars.ATLASSIAN_SITE_URL }} >> e2e/nx-forge-e2e/.env echo DEVELOPER_SPACE_ID=${{ secrets.DEVELOPER_SPACE_ID }} >> e2e/nx-forge-e2e/.env - run: pnpm install --frozen-lockfile - - run: pnpm exec commitlint --from ${{ github.event.pull_request.base.sha }} --to ${{ github.event.pull_request.head.sha }} --verbose - - run: pnpm nx affected --target=format:check --parallel=2 - - run: pnpm nx affected --target=lint --parallel=2 - - run: pnpm nx affected --target=build --parallel=3 - - run: pnpm nx affected --target=test --parallel=2 - run: pnpm nx affected --target=e2e --parallel=2 --no-silent env: NODE_OPTIONS: '--no-deprecation' diff --git a/package.json b/package.json index e2ff14d..a7dde18 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "@swc-node/register": "catalog:", "@swc/cli": "catalog:", "@swc/core": "catalog:", + "@types/cross-spawn": "catalog:", "@types/fs-extra": "catalog:", "@types/jest": "catalog:", "@types/js-yaml": "catalog:", @@ -69,6 +70,7 @@ }, "dependencies": { "@forge/manifest": "catalog:", + "cross-spawn": "catalog:", "fs-extra": "catalog:", "js-yaml": "catalog:", "jsonata": "catalog:" diff --git a/packages/nx-forge/package.json b/packages/nx-forge/package.json index 133509f..7e45c33 100644 --- a/packages/nx-forge/package.json +++ b/packages/nx-forge/package.json @@ -18,6 +18,7 @@ "@nx/jest": "catalog:nx", "@nx/js": "catalog:nx", "@nx/webpack": "catalog:nx", + "cross-spawn": "catalog:", "fs-extra": "catalog:", "js-yaml": "catalog:", "jsonata": "catalog:", diff --git a/packages/nx-forge/src/executors/deploy/executor.spec.ts b/packages/nx-forge/src/executors/deploy/executor.spec.ts index 331acd9..bc1df84 100644 --- a/packages/nx-forge/src/executors/deploy/executor.spec.ts +++ b/packages/nx-forge/src/executors/deploy/executor.spec.ts @@ -62,22 +62,20 @@ describe('deploy executor', () => { }); it('forwards multiple approval rules to Forge', async () => { + // Forge documents MAJOR_VERSION_RULE and accepts multiple server-provided rule names: + // https://developer.atlassian.com/platform/forge/cli-reference/deploy/#pre-approval + const approvals = ['MAJOR_VERSION_RULE', 'LICENSE_RULE']; + await runExecutor( { ...defaultOptions, - approve: ['MAJOR_VERSION_RULE', 'LICENSE_RULE'], + approve: approvals, }, context ); expect(runForgeCommandAsyncMock).toHaveBeenCalledWith( - [ - 'deploy', - '--environment=development', - '--approve', - 'MAJOR_VERSION_RULE', - 'LICENSE_RULE', - ], + ['deploy', '--environment=development', '--approve', ...approvals], { cwd: 'dist/apps/example' } ); }); diff --git a/packages/nx-forge/src/executors/register/executor.spec.ts b/packages/nx-forge/src/executors/register/executor.spec.ts new file mode 100644 index 0000000..4751325 --- /dev/null +++ b/packages/nx-forge/src/executors/register/executor.spec.ts @@ -0,0 +1,62 @@ +import { resolve } from 'path'; +import { ExecutorContext } from '@nx/devkit'; +import { runForgeCommandAsync } from '../../utils/forge/async-commands'; +import runExecutor from './executor'; +import { patchManifestYml } from './lib/patch-manifest-yml'; +import { RegisterExecutorOptions } from './schema'; + +jest.mock('../../utils/forge/async-commands', () => ({ + runForgeCommandAsync: jest.fn().mockResolvedValue(undefined), +})); +jest.mock('./lib/patch-manifest-yml', () => ({ + patchManifestYml: jest.fn().mockResolvedValue(undefined), +})); + +const runForgeCommandAsyncMock = jest.mocked(runForgeCommandAsync); +const patchManifestYmlMock = jest.mocked(patchManifestYml); + +describe('register executor', () => { + const context = { + root: '/workspace', + projectName: 'example', + projectsConfigurations: { + version: 2, + projects: { + example: { + root: 'apps/example', + sourceRoot: 'apps/example/src', + }, + }, + }, + } as unknown as ExecutorContext; + + const defaultOptions: RegisterExecutorOptions = { + outputPath: 'dist/apps/example', + appName: 'Example app', + verbose: false, + developerSpaceId: '', + acceptTerms: false, + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('forwards the developer space flag and value as separate arguments', async () => { + const developerSpaceId = 'space id;$(untrusted)'; + + await runExecutor( + { + ...defaultOptions, + developerSpaceId, + }, + context + ); + + expect(runForgeCommandAsyncMock).toHaveBeenCalledWith( + ['register', '--developer-space-id', developerSpaceId, 'Example app'], + { cwd: resolve('/workspace', 'dist/apps/example') } + ); + expect(patchManifestYmlMock).toHaveBeenCalledTimes(1); + }); +}); diff --git a/packages/nx-forge/src/executors/register/executor.ts b/packages/nx-forge/src/executors/register/executor.ts index 7975b20..7bcd016 100644 --- a/packages/nx-forge/src/executors/register/executor.ts +++ b/packages/nx-forge/src/executors/register/executor.ts @@ -29,7 +29,7 @@ export default async function runExecutor( const args = [ 'register', ...(options.developerSpaceId - ? [`--developer-space-id ${options.developerSpaceId}`] + ? ['--developer-space-id', options.developerSpaceId] : []), ...(options.acceptTerms === true ? ['--accept-terms'] : []), ...(options.verbose === true ? ['--verbose'] : []), diff --git a/packages/nx-forge/src/utils/forge/async-commands.spec.ts b/packages/nx-forge/src/utils/forge/async-commands.spec.ts new file mode 100644 index 0000000..cdf8dfc --- /dev/null +++ b/packages/nx-forge/src/utils/forge/async-commands.spec.ts @@ -0,0 +1,160 @@ +import { + chmodSync, + existsSync, + mkdtempSync, + mkdirSync, + readFileSync, + realpathSync, + rmSync, + writeFileSync, +} from 'fs'; +import { tmpdir } from 'os'; +import { delimiter, join } from 'path'; +import { getPackageManagerCommand } from '@nx/devkit'; +import { runForgeCommandAsync } from './async-commands'; + +jest.mock('@nx/devkit', () => ({ + ...jest.requireActual('@nx/devkit'), + getPackageManagerCommand: jest.fn(), +})); + +const getPackageManagerCommandMock = jest.mocked(getPackageManagerCommand); + +describe('runForgeCommandAsync', () => { + let testRoot: string; + let binPath: string; + let invocationPath: string; + let sideEffectPath: string; + + beforeEach(() => { + testRoot = mkdtempSync(join(tmpdir(), 'nx-forge-command-')); + binPath = join(testRoot, 'bin'); + invocationPath = join(testRoot, 'invocation.json'); + sideEffectPath = join(testRoot, 'shell-side-effect'); + mkdirSync(binPath); + + createPackageManagerShim(binPath, 'pnpm'); + mockPackageManagerCommand('pnpm exec'); + }); + + afterEach(() => { + rmSync(testRoot, { recursive: true, force: true }); + jest.clearAllMocks(); + }); + + it('preserves Forge arguments without shell interpretation', async () => { + const injectedCommand = + process.platform === 'win32' + ? "approval value&node -e \"require('fs').writeFileSync(process.env.FORGE_SIDE_EFFECT,'created')\"" + : "approval value;node -e \"require('fs').writeFileSync(process.env.FORGE_SIDE_EFFECT,'created')\""; + const forgeArgs = [ + 'deploy', + '--approve', + injectedCommand, + '--major-version', + '2', + ]; + + await runForgeCommandAsync(forgeArgs, { + cwd: testRoot, + env: createEnvironment(binPath, invocationPath, { + FORGE_SIDE_EFFECT: sideEffectPath, + FORGE_TEST_ENV: 'forwarded', + }), + }); + + expect(JSON.parse(readFileSync(invocationPath, 'utf8'))).toEqual({ + args: ['exec', 'forge', ...forgeArgs], + cwd: realpathSync(testRoot), + environmentValue: 'forwarded', + }); + expect(existsSync(sideEffectPath)).toBe(false); + }); + + it.each([ + { exec: 'npx', executable: 'npx', prefixArgs: [] }, + { exec: 'pnpm exec', executable: 'pnpm', prefixArgs: ['exec'] }, + { exec: 'yarn', executable: 'yarn', prefixArgs: [] }, + { exec: 'bun', executable: 'bun', prefixArgs: [] }, + ])('supports the $exec package-manager command shape', async (command) => { + createPackageManagerShim(binPath, command.executable); + mockPackageManagerCommand(command.exec); + + await runForgeCommandAsync(['version'], { + cwd: testRoot, + env: createEnvironment(binPath, invocationPath), + }); + + expect(JSON.parse(readFileSync(invocationPath, 'utf8')).args).toEqual([ + ...command.prefixArgs, + 'forge', + 'version', + ]); + }); + + it('rejects when Forge exits with a non-zero code', async () => { + await expect( + runForgeCommandAsync(['lint'], { + cwd: testRoot, + env: createEnvironment(binPath, invocationPath, { + FORGE_EXIT_CODE: '7', + }), + }) + ).rejects.toThrow('Exit with error code: 7'); + }); +}); + +function mockPackageManagerCommand(exec: string): void { + getPackageManagerCommandMock.mockReturnValue({ + exec, + } as ReturnType); +} + +function createEnvironment( + binPath: string, + invocationPath: string, + extraEnvironment: NodeJS.ProcessEnv = {} +): NodeJS.ProcessEnv { + const pathKey = + Object.keys(process.env).find((key) => key.toLowerCase() === 'path') ?? + 'PATH'; + + return { + [pathKey]: `${binPath}${delimiter}${process.env[pathKey] ?? ''}`, + FORGE_INVOCATION_PATH: invocationPath, + ...extraEnvironment, + }; +} + +function createPackageManagerShim(directory: string, command: string): void { + const scriptPath = join(directory, 'fake-package-manager.cjs'); + writeFileSync( + scriptPath, + [ + "const { writeFileSync } = require('fs');", + 'writeFileSync(', + ' process.env.FORGE_INVOCATION_PATH,', + ' JSON.stringify({', + ' args: process.argv.slice(2),', + ' cwd: process.cwd(),', + ' environmentValue: process.env.FORGE_TEST_ENV,', + ' })', + ');', + 'process.exitCode = Number(process.env.FORGE_EXIT_CODE ?? 0);', + ].join('\n') + ); + + if (process.platform === 'win32') { + writeFileSync( + join(directory, `${command}.cmd`), + `@echo off\r\n"${process.execPath}" "%~dp0\\fake-package-manager.cjs" %*\r\n` + ); + } else { + const executablePath = join(directory, command); + writeFileSync( + executablePath, + `#!/usr/bin/env node\nrequire('./fake-package-manager.cjs');\n` + ); + chmodSync(executablePath, 0o755); + } +} diff --git a/packages/nx-forge/src/utils/forge/async-commands.ts b/packages/nx-forge/src/utils/forge/async-commands.ts index 2361de8..cb830cf 100644 --- a/packages/nx-forge/src/utils/forge/async-commands.ts +++ b/packages/nx-forge/src/utils/forge/async-commands.ts @@ -1,6 +1,9 @@ -import { spawn } from 'child_process'; import { getPackageManagerCommand } from '@nx/devkit'; +// Node cannot directly launch npm/pnpm/yarn `.cmd` shims on Windows. +// cross-spawn resolves those shims while preserving each Forge argument. +import spawn from 'cross-spawn'; + /** * Runs the given Forge CLI command asynchronously. * @@ -12,13 +15,16 @@ export const runForgeCommandAsync = ( opts: { env?: NodeJS.ProcessEnv; cwd?: string } = {} ): Promise => { const pmc = getPackageManagerCommand(); - const forgeCmd = ['forge', ...args].join(' '); - const cliProcess = spawn(`${pmc.exec} ${forgeCmd}`, { - cwd: opts.cwd, - env: { ...process.env, ...opts.env }, - stdio: [process.stdin, process.stdout, process.stderr], - shell: true, - }); + const [executable, ...packageManagerArgs] = pmc.exec.trim().split(/\s+/); + const cliProcess = spawn( + executable, + [...packageManagerArgs, 'forge', ...args], + { + cwd: opts.cwd, + env: { ...process.env, ...opts.env }, + stdio: [process.stdin, process.stdout, process.stderr], + } + ); return new Promise((resolve, reject) => { cliProcess.once('exit', (code: number) => { if (code === 0) { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 68146a9..e4a7c6b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -36,6 +36,9 @@ catalogs: '@swc/core': specifier: 1.15.8 version: 1.15.8 + '@types/cross-spawn': + specifier: 6.0.6 + version: 6.0.6 '@types/fs-extra': specifier: ^9.0.13 version: 9.0.13 @@ -54,6 +57,9 @@ catalogs: '@typescript-eslint/parser': specifier: ^8.40.0 version: 8.62.0 + cross-spawn: + specifier: 7.0.6 + version: 7.0.6 dotenv: specifier: ^16.0.3 version: 16.6.1 @@ -177,6 +183,9 @@ importers: '@forge/manifest': specifier: 'catalog:' version: 13.3.1(typescript@5.9.3) + cross-spawn: + specifier: 'catalog:' + version: 7.0.6 fs-extra: specifier: 'catalog:' version: 10.1.0 @@ -241,6 +250,9 @@ importers: '@swc/core': specifier: 'catalog:' version: 1.15.8 + '@types/cross-spawn': + specifier: 'catalog:' + version: 6.0.6 '@types/fs-extra': specifier: 'catalog:' version: 9.0.13 @@ -361,6 +373,9 @@ importers: '@nx/webpack': specifier: catalog:nx version: 23.0.1(@babel/traverse@7.29.7)(@swc-node/register@1.11.1(@swc/core@1.15.8)(@swc/types@0.1.27)(typescript@5.9.3))(@swc/cli@0.8.1(@swc/core@1.15.8)(chokidar@5.0.0))(@swc/core@1.15.8)(html-webpack-plugin@5.6.7(webpack@5.108.0))(nx@23.0.1(@swc-node/register@1.11.1(@swc/core@1.15.8)(@swc/types@0.1.27)(typescript@5.9.3))(@swc/core@1.15.8))(typescript@5.9.3)(verdaccio@6.7.4(typanion@3.14.0))(webpack-cli@7.0.3)(webpack-dev-server@5.2.5)(webpack@5.108.0) + cross-spawn: + specifier: 'catalog:' + version: 7.0.6 fs-extra: specifier: 'catalog:' version: 10.1.0 @@ -3004,6 +3019,9 @@ packages: '@types/connect@3.4.38': resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} + '@types/cross-spawn@6.0.6': + resolution: {integrity: sha512-fXRhhUkG4H3TQk5dBhQ7m/JDdSNHKwR2BBia62lhwEIq9xGiQKLxd6LymNhn47SjXhsUEPmxi+PKw2OkW4LLjA==} + '@types/esquery@1.5.4': resolution: {integrity: sha512-yYO4Q8H+KJHKW1rEeSzHxcZi90durqYgWVfnh5K6ZADVBjBv2e1NEveYX5yT2bffgN7RqzH3k9930m+i2yBoMA==} @@ -11274,7 +11292,7 @@ snapshots: '@jest/console@30.3.0': dependencies: '@jest/types': 30.3.0 - '@types/node': 18.19.21 + '@types/node': 26.0.1 chalk: 4.1.2 jest-message-util: 30.3.0 jest-util: 30.3.0 @@ -11283,7 +11301,7 @@ snapshots: '@jest/console@30.4.1': dependencies: '@jest/types': 30.4.1 - '@types/node': 18.19.21 + '@types/node': 26.0.1 chalk: 4.1.2 jest-message-util: 30.4.1 jest-util: 30.4.1 @@ -11297,14 +11315,14 @@ snapshots: '@jest/test-result': 30.3.0 '@jest/transform': 30.3.0 '@jest/types': 30.3.0 - '@types/node': 18.19.21 + '@types/node': 26.0.1 ansi-escapes: 4.3.2 chalk: 4.1.2 ci-info: 4.4.0 exit-x: 0.2.2 graceful-fs: 4.2.11 jest-changed-files: 30.3.0 - jest-config: 30.3.0(@types/node@18.19.21)(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@swc/core@1.15.8)(@types/node@18.19.21)(typescript@5.9.3)) + jest-config: 30.3.0(@types/node@26.0.1)(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@swc/core@1.15.8)(@types/node@18.19.21)(typescript@5.9.3)) jest-haste-map: 30.3.0 jest-message-util: 30.3.0 jest-regex-util: 30.0.1 @@ -11332,14 +11350,14 @@ snapshots: '@jest/test-result': 30.3.0 '@jest/transform': 30.3.0 '@jest/types': 30.3.0 - '@types/node': 18.19.21 + '@types/node': 26.0.1 ansi-escapes: 4.3.2 chalk: 4.1.2 ci-info: 4.4.0 exit-x: 0.2.2 graceful-fs: 4.2.11 jest-changed-files: 30.3.0 - jest-config: 30.3.0(@types/node@18.19.21)(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@swc/core@1.15.8)(@types/node@26.0.1)(typescript@5.9.3)) + jest-config: 30.3.0(@types/node@26.0.1)(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@swc/core@1.15.8)(@types/node@26.0.1)(typescript@5.9.3)) jest-haste-map: 30.3.0 jest-message-util: 30.3.0 jest-regex-util: 30.0.1 @@ -11372,7 +11390,7 @@ snapshots: '@jest/fake-timers': 30.0.5 '@jest/types': 30.0.5 '@types/jsdom': 21.1.7 - '@types/node': 18.19.21 + '@types/node': 26.0.1 jest-mock: 30.0.5 jest-util: 30.0.5 jsdom: 26.1.0 @@ -11381,21 +11399,21 @@ snapshots: dependencies: '@jest/fake-timers': 30.0.5 '@jest/types': 30.0.5 - '@types/node': 18.19.21 + '@types/node': 26.0.1 jest-mock: 30.0.5 '@jest/environment@30.3.0': dependencies: '@jest/fake-timers': 30.3.0 '@jest/types': 30.3.0 - '@types/node': 18.19.21 + '@types/node': 26.0.1 jest-mock: 30.3.0 '@jest/environment@30.4.1': dependencies: '@jest/fake-timers': 30.4.1 '@jest/types': 30.4.1 - '@types/node': 18.19.21 + '@types/node': 26.0.1 jest-mock: 30.4.1 '@jest/expect-utils@30.3.0': @@ -11424,7 +11442,7 @@ snapshots: dependencies: '@jest/types': 30.0.5 '@sinonjs/fake-timers': 13.0.5 - '@types/node': 18.19.21 + '@types/node': 26.0.1 jest-message-util: 30.0.5 jest-mock: 30.0.5 jest-util: 30.0.5 @@ -11433,7 +11451,7 @@ snapshots: dependencies: '@jest/types': 30.3.0 '@sinonjs/fake-timers': 15.4.0 - '@types/node': 18.19.21 + '@types/node': 26.0.1 jest-message-util: 30.3.0 jest-mock: 30.3.0 jest-util: 30.3.0 @@ -11442,7 +11460,7 @@ snapshots: dependencies: '@jest/types': 30.4.1 '@sinonjs/fake-timers': 15.4.0 - '@types/node': 18.19.21 + '@types/node': 26.0.1 jest-message-util: 30.4.1 jest-mock: 30.4.1 jest-util: 30.4.1 @@ -11471,7 +11489,7 @@ snapshots: '@jest/pattern@30.0.1': dependencies: - '@types/node': 18.19.21 + '@types/node': 26.0.1 jest-regex-util: 30.0.1 '@jest/pattern@30.4.0': @@ -11487,7 +11505,7 @@ snapshots: '@jest/transform': 30.3.0 '@jest/types': 30.3.0 '@jridgewell/trace-mapping': 0.3.31 - '@types/node': 18.19.21 + '@types/node': 26.0.1 chalk: 4.1.2 collect-v8-coverage: 1.0.3 exit-x: 0.2.2 @@ -11515,7 +11533,7 @@ snapshots: '@jest/transform': 30.4.1 '@jest/types': 30.4.1 '@jridgewell/trace-mapping': 0.3.31 - '@types/node': 18.19.21 + '@types/node': 26.0.1 chalk: 4.1.2 collect-v8-coverage: 1.0.3 exit-x: 0.2.2 @@ -11635,7 +11653,7 @@ snapshots: '@jest/schemas': 30.0.5 '@types/istanbul-lib-coverage': 2.0.6 '@types/istanbul-reports': 3.0.4 - '@types/node': 18.19.21 + '@types/node': 26.0.1 '@types/yargs': 17.0.35 chalk: 4.1.2 @@ -11645,7 +11663,7 @@ snapshots: '@jest/schemas': 30.0.5 '@types/istanbul-lib-coverage': 2.0.6 '@types/istanbul-reports': 3.0.4 - '@types/node': 18.19.21 + '@types/node': 26.0.1 '@types/yargs': 17.0.35 chalk: 4.1.2 @@ -13067,17 +13085,21 @@ snapshots: '@types/bonjour@3.5.13': dependencies: - '@types/node': 18.19.21 + '@types/node': 26.0.1 '@types/connect-history-api-fallback@1.5.4': dependencies: '@types/express-serve-static-core': 4.19.8 - '@types/node': 18.19.21 + '@types/node': 26.0.1 '@types/connect@3.4.38': dependencies: '@types/node': 26.0.1 + '@types/cross-spawn@6.0.6': + dependencies: + '@types/node': 26.0.1 + '@types/esquery@1.5.4': dependencies: '@types/estree': 1.0.9 @@ -13114,7 +13136,7 @@ snapshots: '@types/http-proxy@1.17.17': dependencies: - '@types/node': 18.19.21 + '@types/node': 26.0.1 '@types/istanbul-lib-coverage@2.0.6': {} @@ -13135,7 +13157,7 @@ snapshots: '@types/jsdom@21.1.7': dependencies: - '@types/node': 18.19.21 + '@types/node': 26.0.1 '@types/tough-cookie': 4.0.5 parse5: 7.3.0 @@ -13174,7 +13196,7 @@ snapshots: '@types/responselike@1.0.0': dependencies: - '@types/node': 18.19.21 + '@types/node': 26.0.1 '@types/retry@0.12.2': {} @@ -13199,7 +13221,7 @@ snapshots: '@types/sockjs@0.3.36': dependencies: - '@types/node': 18.19.21 + '@types/node': 26.0.1 '@types/stack-utils@2.0.3': {} @@ -13211,7 +13233,7 @@ snapshots: '@types/ws@8.18.1': dependencies: - '@types/node': 18.19.21 + '@types/node': 26.0.1 '@types/yargs-parser@21.0.3': {} @@ -16712,7 +16734,7 @@ snapshots: '@jest/expect': 30.3.0 '@jest/test-result': 30.3.0 '@jest/types': 30.3.0 - '@types/node': 18.19.21 + '@types/node': 26.0.1 chalk: 4.1.2 co: 4.6.0 dedent: 1.7.2(babel-plugin-macros@3.1.0) @@ -16738,7 +16760,7 @@ snapshots: '@jest/expect': 30.4.1 '@jest/test-result': 30.4.1 '@jest/types': 30.4.1 - '@types/node': 18.19.21 + '@types/node': 26.0.1 chalk: 4.1.2 co: 4.6.0 dedent: 1.7.2(babel-plugin-macros@3.1.0) @@ -16829,7 +16851,7 @@ snapshots: - babel-plugin-macros - supports-color - jest-config@30.3.0(@types/node@18.19.21)(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@swc/core@1.15.8)(@types/node@26.0.1)(typescript@5.9.3)): + jest-config@30.3.0(@types/node@26.0.1)(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@swc/core@1.15.8)(@types/node@18.19.21)(typescript@5.9.3)): dependencies: '@babel/core': 7.29.7 '@jest/get-type': 30.1.0 @@ -16855,12 +16877,11 @@ snapshots: slash: 3.0.0 strip-json-comments: 3.1.1 optionalDependencies: - '@types/node': 18.19.21 - ts-node: 10.9.1(@swc/core@1.15.8)(@types/node@26.0.1)(typescript@5.9.3) + '@types/node': 26.0.1 + ts-node: 10.9.1(@swc/core@1.15.8)(@types/node@18.19.21)(typescript@5.9.3) transitivePeerDependencies: - babel-plugin-macros - supports-color - optional: true jest-config@30.3.0(@types/node@26.0.1)(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@swc/core@1.15.8)(@types/node@26.0.1)(typescript@5.9.3)): dependencies: @@ -17024,7 +17045,7 @@ snapshots: '@jest/environment': 30.3.0 '@jest/fake-timers': 30.3.0 '@jest/types': 30.3.0 - '@types/node': 18.19.21 + '@types/node': 26.0.1 jest-mock: 30.3.0 jest-util: 30.3.0 jest-validate: 30.3.0 @@ -17034,7 +17055,7 @@ snapshots: '@jest/environment': 30.4.1 '@jest/fake-timers': 30.4.1 '@jest/types': 30.4.1 - '@types/node': 18.19.21 + '@types/node': 26.0.1 jest-mock: 30.4.1 jest-util: 30.4.1 jest-validate: 30.4.1 @@ -17042,7 +17063,7 @@ snapshots: jest-haste-map@30.3.0: dependencies: '@jest/types': 30.3.0 - '@types/node': 18.19.21 + '@types/node': 26.0.1 anymatch: 3.1.3 fb-watchman: 2.0.2 graceful-fs: 4.2.11 @@ -17133,19 +17154,19 @@ snapshots: jest-mock@30.0.5: dependencies: '@jest/types': 30.0.5 - '@types/node': 18.19.21 + '@types/node': 26.0.1 jest-util: 30.0.5 jest-mock@30.3.0: dependencies: '@jest/types': 30.3.0 - '@types/node': 18.19.21 + '@types/node': 26.0.1 jest-util: 30.3.0 jest-mock@30.4.1: dependencies: '@jest/types': 30.4.1 - '@types/node': 18.19.21 + '@types/node': 26.0.1 jest-util: 30.4.1 jest-pnp-resolver@1.2.3(jest-resolve@30.3.0): @@ -17196,7 +17217,7 @@ snapshots: '@jest/test-result': 30.3.0 '@jest/transform': 30.3.0 '@jest/types': 30.3.0 - '@types/node': 18.19.21 + '@types/node': 26.0.1 chalk: 4.1.2 emittery: 0.13.1 exit-x: 0.2.2 @@ -17223,7 +17244,7 @@ snapshots: '@jest/test-result': 30.4.1 '@jest/transform': 30.4.1 '@jest/types': 30.4.1 - '@types/node': 18.19.21 + '@types/node': 26.0.1 chalk: 4.1.2 emittery: 0.13.1 exit-x: 0.2.2 @@ -17252,7 +17273,7 @@ snapshots: '@jest/test-result': 30.3.0 '@jest/transform': 30.3.0 '@jest/types': 30.3.0 - '@types/node': 18.19.21 + '@types/node': 26.0.1 chalk: 4.1.2 cjs-module-lexer: 2.2.0 collect-v8-coverage: 1.0.3 @@ -17279,7 +17300,7 @@ snapshots: '@jest/test-result': 30.4.1 '@jest/transform': 30.4.1 '@jest/types': 30.4.1 - '@types/node': 18.19.21 + '@types/node': 26.0.1 chalk: 4.1.2 cjs-module-lexer: 2.2.0 collect-v8-coverage: 1.0.3 @@ -17361,7 +17382,7 @@ snapshots: jest-util@30.3.0: dependencies: '@jest/types': 30.3.0 - '@types/node': 18.19.21 + '@types/node': 26.0.1 chalk: 4.1.2 ci-info: 4.4.0 graceful-fs: 4.2.11 @@ -17407,7 +17428,7 @@ snapshots: dependencies: '@jest/test-result': 30.3.0 '@jest/types': 30.3.0 - '@types/node': 18.19.21 + '@types/node': 26.0.1 ansi-escapes: 4.3.2 chalk: 4.1.2 emittery: 0.13.1 @@ -17418,7 +17439,7 @@ snapshots: dependencies: '@jest/test-result': 30.4.1 '@jest/types': 30.4.1 - '@types/node': 18.19.21 + '@types/node': 26.0.1 ansi-escapes: 4.3.2 chalk: 4.1.2 emittery: 0.13.1 @@ -17427,13 +17448,13 @@ snapshots: jest-worker@27.5.1: dependencies: - '@types/node': 18.19.21 + '@types/node': 26.0.1 merge-stream: 2.0.0 supports-color: 8.1.1 jest-worker@30.3.0: dependencies: - '@types/node': 18.19.21 + '@types/node': 26.0.1 '@ungap/structured-clone': 1.3.2 jest-util: 30.3.0 merge-stream: 2.0.0 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 9aa44d4..08dd7a8 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -23,12 +23,14 @@ catalog: '@swc-node/register': '1.11.1' '@swc/cli': '0.8.1' '@swc/core': '1.15.8' + '@types/cross-spawn': '6.0.6' '@types/fs-extra': '^9.0.13' '@types/jest': '30.0.0' '@types/js-yaml': '^4.0.5' '@types/node': '18.19.21' '@typescript-eslint/eslint-plugin': '^8.40.0' '@typescript-eslint/parser': '^8.40.0' + cross-spawn: '7.0.6' dotenv: '^16.0.3' eslint: '^9.8.0' eslint-config-prettier: '10.1.8'