diff --git a/.changeset/quiet-hooks-register.md b/.changeset/quiet-hooks-register.md new file mode 100644 index 000000000..d946ef223 --- /dev/null +++ b/.changeset/quiet-hooks-register.md @@ -0,0 +1,5 @@ +--- +'@openfn/runtime': patch +--- + +Use `module.registerHooks()` for the ESM resolve hook where Node supports it diff --git a/.changeset/worker-node-26-bookworm.md b/.changeset/worker-node-26-bookworm.md new file mode 100644 index 000000000..c6917e7d0 --- /dev/null +++ b/.changeset/worker-node-26-bookworm.md @@ -0,0 +1,5 @@ +--- +'@openfn/ws-worker': minor +--- + +Run the worker image on Node 26 and Debian bookworm diff --git a/.changeset/worker-sentry-11.md b/.changeset/worker-sentry-11.md new file mode 100644 index 000000000..11edb0d48 --- /dev/null +++ b/.changeset/worker-sentry-11.md @@ -0,0 +1,5 @@ +--- +'@openfn/ws-worker': patch +--- + +Upgrade Sentry to v11 to stop a Node deprecation warning at startup, and deduplicate repeated identical errors diff --git a/.circleci/config.yml b/.circleci/config.yml index 3c836c967..dd5651a7d 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -9,6 +9,22 @@ commands: name: install CLI globally command: sudo npm install -g file:dist/openfn-cli.tgz + check_node_version: + parameters: + node_version: + type: string + default: '' + steps: + - run: + name: Check Node versions match .tool-versions + command: scripts/check-node-version.sh << parameters.node_version >> + + install_pnpm: + steps: + - run: + name: Install pnpm + command: sudo npm install -g "$(node -p "require('./package.json').packageManager.split('+')[0]")" + # Define a job to be invoked later in a workflow. # See: https://circleci.com/docs/2.0/configuration-reference/#jobs jobs: @@ -16,22 +32,24 @@ jobs: # Specify the execution environment. You can specify an image from Dockerhub or use one of our Convenience Images from CircleCI's Developer Hub. # See: https://circleci.com/docs/2.0/configuration-reference/#docker-machine-macos-windows-executor docker: - - image: cimg/node:24.18 + - image: cimg/node:26.12.0 resource_class: medium # Add steps to the job # See: https://circleci.com/docs/2.0/configuration-reference/#steps steps: - checkout + - check_node_version + - install_pnpm - restore_cache: # See the configuration reference documentation for more details on using restore_cache and save_cache steps # https://circleci.com/docs/2.0/configuration-reference/?section=reference#save_cache keys: - - deps-v1-{{ .Branch }}-{{checksum "pnpm-lock.yaml"}} + - deps-v1-{{ .Branch }}-{{checksum "pnpm-lock.yaml"}}-{{checksum ".tool-versions"}} - run: name: Install command: pnpm install --frozen-lockfile - save_cache: - key: deps-v1-{{ .Branch }}-{{checksum "pnpm-lock.yaml"}} + key: deps-v1-{{ .Branch }}-{{checksum "pnpm-lock.yaml"}}-{{checksum ".tool-versions"}} paths: - node_modules - persist_to_workspace: @@ -41,11 +59,13 @@ jobs: build: docker: - - image: cimg/node:24.18 + - image: cimg/node:26.12.0 resource_class: medium steps: - attach_workspace: at: ~/project + - check_node_version + - install_pnpm - run: name: Build packages command: pnpm build @@ -56,45 +76,53 @@ jobs: unit_test: docker: - - image: cimg/node:24.18 + - image: cimg/node:26.12.0 resource_class: medium parallelism: 1 steps: - attach_workspace: at: ~/project + - check_node_version + - install_pnpm - run: name: Run Tests command: pnpm test format: docker: - - image: cimg/node:24.18 + - image: cimg/node:26.12.0 resource_class: medium steps: - attach_workspace: at: ~/project + - check_node_version + - install_pnpm - run: name: Check formatting command: pnpm test:format type_check: docker: - - image: cimg/node:24.18 + - image: cimg/node:26.12.0 resource_class: medium steps: - attach_workspace: at: ~/project + - check_node_version + - install_pnpm - run: name: Type check command: pnpm test:types build_openfnx: docker: - - image: cimg/node:24.18 + - image: cimg/node:26.12.0 resource_class: medium steps: - attach_workspace: at: ~/project + - check_node_version + - install_pnpm - run: name: Build local tarballs command: pnpm pack:local dist --no-version @@ -114,6 +142,9 @@ jobs: steps: - attach_workspace: at: ~/project + - check_node_version: + node_version: << parameters.node_version >> + - install_pnpm - install_cli_global - run: name: Check install @@ -122,6 +153,29 @@ jobs: name: Run integration test suite command: pnpm test:integration + worker_image: + docker: + - image: cimg/node:26.12.0 + resource_class: medium + steps: + - checkout + - check_node_version + - setup_remote_docker + - run: + name: Build worker image + command: docker build -t ws-worker-ci . + - run: + name: Check Node in the image matches .tool-versions + command: docker run --rm ws-worker-ci /app/scripts/check-node-version.sh + - run: + name: Install and load motherduck as user node + command: | + docker run --rm --user node ws-worker-ci sh -c ' + mkdir -p /tmp/repo && cd /tmp/repo && + npm install --no-audit --no-fund --no-package-lock --ignore-scripts @openfn/language-motherduck@latest && + node -e "require(\"@openfn/language-motherduck\")" + ' + # This ensures that we can run a v1 CLI deploy in node 18, # which is important for GH sync legacy_test: @@ -165,9 +219,14 @@ workflows: - integration_test: matrix: parameters: - node_version: ['22.20', '24.18.1', '26.5.1'] + node_version: ['24.22.0', '26.12.0'] requires: - build_openfnx - legacy_test: requires: - build_openfnx + - worker_image: + filters: + branches: + ignore: + - main diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index e35a6be6c..d780253fe 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -3,7 +3,7 @@ { "name": "Default", // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile - "image": "mcr.microsoft.com/devcontainers/base:bullseye", + "image": "mcr.microsoft.com/devcontainers/base:bookworm", "features": { "ghcr.io/devcontainers/features/docker-outside-of-docker:1": { @@ -13,7 +13,7 @@ }, "ghcr.io/devcontainers/features/node:1": { "nodeGypDependencies": true, - "version": "18" + "version": "none" } }, @@ -25,8 +25,8 @@ // Use 'forwardPorts' to make a list of ports inside the container available locally. // "forwardPorts": [], - // Use 'postCreateCommand' to run commands after the container is created. - "postCreateCommand": "npm install -g pnpm" + // Node comes from .tool-versions, so the node feature only installs nvm. + "onCreateCommand": ".devcontainer/install-node.sh" // Configure tool-specific properties. // "customizations": {}, diff --git a/.devcontainer/install-node.sh b/.devcontainer/install-node.sh new file mode 100755 index 000000000..cdea067b4 --- /dev/null +++ b/.devcontainer/install-node.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash +set -euo pipefail + +node_version=$(awk '$1 == "nodejs" { print $2 }' .tool-versions) + +. "$NVM_DIR/nvm.sh" +nvm install "$node_version" +nvm alias default "$node_version" + +npm install -g "$(node -p "require('./package.json').packageManager.split('+')[0]")" diff --git a/.tool-versions b/.tool-versions index 1cdceb670..6f1d47511 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1 +1 @@ -nodejs 26.5.1 +nodejs 26.12.0 diff --git a/Dockerfile b/Dockerfile index 3bb4b610f..edc51835c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,6 @@ -FROM node:24-alpine AS base +ARG NODE_VERSION=26.12.0 + +FROM node:${NODE_VERSION}-bookworm-slim AS base ENV PNPM_HOME="/pnpm" ENV PATH="$PNPM_HOME:$PATH" COPY . /app @@ -18,7 +20,7 @@ RUN npm install -g "$(node -p "require('./package.json').packageManager.split('+ # TODO: remove simple build once prod optimized build is working --------------- FROM base AS ws-worker -RUN apk add --no-cache git +RUN apt-get update && apt-get install -y --no-install-recommends git ca-certificates && rm -rf /var/lib/apt/lists/* RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile RUN pnpm build WORKDIR /app/packages/ws-worker diff --git a/ava.config.js b/ava.config.js index df5cb8e81..87934e13e 100644 --- a/ava.config.js +++ b/ava.config.js @@ -1,6 +1,14 @@ +const path = require('node:path'); +const { pathToFileURL } = require('node:url'); const semver = require("semver") const loader_arg = semver.lte(process.version, 'v20.5.0') ? '--loader=@swc-node/register/esm' : '--import=@swc-node/register/esm-register' + +// Loaded before any test file, so that every module sees the patched fs +const memfs = pathToFileURL( + path.join(__dirname, 'packages/test-helpers/src/memfs.ts') +).href; + module.exports = { extensions: { ts: 'module', @@ -12,6 +20,7 @@ module.exports = { nodeArguments: [ loader_arg, + `--import=${memfs}`, '--no-warnings', // Disable experimental module warnings '--experimental-vm-modules', ], diff --git a/integration-tests/cli/.dockerignore b/integration-tests/cli/.dockerignore deleted file mode 100644 index c8c6ad9ba..000000000 --- a/integration-tests/cli/.dockerignore +++ /dev/null @@ -1,2 +0,0 @@ -node_modules -repo \ No newline at end of file diff --git a/integration-tests/cli/Dockerfile b/integration-tests/cli/Dockerfile deleted file mode 100644 index 4bcd59456..000000000 --- a/integration-tests/cli/Dockerfile +++ /dev/null @@ -1,15 +0,0 @@ -# This dockerfile allows us to run tests against the build in isolation -# (I'm not really sure we need it but it's been a useful dev tool) -FROM node:18.12.0 - -ENV NODE_ENV=production - -WORKDIR /app - -COPY . . - -RUN npm install -g ./dist/openfn-cli.tgz - -RUN npm install --production - -CMD ["npm", "test"] \ No newline at end of file diff --git a/integration-tests/cli/README.md b/integration-tests/cli/README.md index 28ec2ec25..fce4a2ec0 100644 --- a/integration-tests/cli/README.md +++ b/integration-tests/cli/README.md @@ -2,7 +2,7 @@ This package contains a set of integration tests against `openfn` CLI. -The test suite is designed to be run in CircleCI, but can run locally or in a local docker image. +The test suite is designed to be run in CircleCI, but can also run locally. ## Running Tests @@ -10,11 +10,6 @@ During development, you can run the unit tests against a local build (ie, `packa `pnpm test:dev` -You can run the test in isolation with a docker build. This can be useful while developing for CI, or if a test passes locally but fails in CI. - -`pnpm run build` -`pnpm run start` - ## Architecture The basic idea here is to test the built (but unreleased) CLI bundle in the global space. @@ -29,6 +24,6 @@ How it works: ## Creating tarballs -From this folder, run `build:pack` to create a bunch of tarballs from your built packages. Tarballs will be built to `dist`. +From the repo root, run `pnpm pack:local integration-tests/cli/dist --no-version` to create a bunch of tarballs from your built packages. Tarballs will be built to `dist`. -You can install the tarballs locally with `npm install -g dist openfn-cli.tgz.` +From this folder, install the CLI from the tarballs with `npm install -g ./dist/openfn-cli.tgz`. diff --git a/integration-tests/cli/package.json b/integration-tests/cli/package.json index 74bf51651..87435e058 100644 --- a/integration-tests/cli/package.json +++ b/integration-tests/cli/package.json @@ -8,9 +8,6 @@ "type": "module", "scripts": { "clean": "rimraf dist repo", - "build:pack": "pnpm clean && cd ../.. && pnpm pack:local integration-tests/cli/dist --no-version", - "build": "pnpm build:pack && docker build --tag cli-integration-tests .", - "start": "docker run cli-integration-tests", "test": "node prelude.js && npx ava -s --timeout 2m && pnpm clean", "test:dev": "node prelude.js && pnpm ava -s && pnpm clean" }, diff --git a/integration-tests/legacy-deploy/package.json b/integration-tests/legacy-deploy/package.json index 7eb35b66c..b9a6f59a8 100644 --- a/integration-tests/legacy-deploy/package.json +++ b/integration-tests/legacy-deploy/package.json @@ -8,7 +8,6 @@ "type": "module", "scripts": { "clean": "rimraf dist repo", - "start": "docker run cli-integration-tests", "test": "npx ava -s" }, "dependencies": { diff --git a/integration-tests/worker/Dockerfile b/integration-tests/worker/Dockerfile index c1b0072bc..fcd6d8e00 100644 --- a/integration-tests/worker/Dockerfile +++ b/integration-tests/worker/Dockerfile @@ -1,6 +1,7 @@ # This dockerfile allows us to run tests against the build in isolation # (I'm not really sure we need it but it's been a useful dev tool) -FROM node:18.12.0 +ARG NODE_VERSION=26.12.0 +FROM node:${NODE_VERSION}-bookworm-slim ENV NODE_ENV=production diff --git a/packages/cli/package.json b/packages/cli/package.json index 5c2b1987d..a52983505 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -38,9 +38,9 @@ "devDependencies": { "@openfn/language-collections": "^0.9.0", "@openfn/language-common": "3.2.3", + "@openfn/test-helpers": "workspace:*", "@types/json-diff": "^1.0.3", "@types/lodash-es": "~4.17.12", - "@types/mock-fs": "^4.13.4", "@types/node": "^18.19.130", "@types/rimraf": "^3.0.2", "@types/treeify": "^1.0.3", @@ -48,7 +48,6 @@ "@types/yargs": "^17.0.35", "ava": "5.3.1", "lodash-es": "^4.17.23", - "mock-fs": "^5.5.0", "tslib": "^2.8.1", "tsup": "^8.5.1", "typescript": "^5.9.3" diff --git a/packages/cli/test/collections/collections.test.ts b/packages/cli/test/collections/collections.test.ts index d761bdd6f..c95ed1668 100644 --- a/packages/cli/test/collections/collections.test.ts +++ b/packages/cli/test/collections/collections.test.ts @@ -1,5 +1,5 @@ import test from 'ava'; -import { mockFs, resetMockFs } from '../util'; +import { mockFs, restoreFs } from '@openfn/test-helpers'; import { get, set, remove } from '../../src/collections/handler'; @@ -10,6 +10,8 @@ import { createMockLogger } from '@openfn/logger'; import { collections } from '@openfn/language-collections'; import { readFile } from 'fs/promises'; +test.afterEach(restoreFs); + // Log as json to make testing easier const logger = createMockLogger('default', { level: 'debug', json: true }); @@ -49,8 +51,6 @@ test.beforeEach(() => { x: {}, y: {}, }); - - resetMockFs(); }); const createOptions = (opts = {}) => ({ diff --git a/packages/cli/test/commands.test.ts b/packages/cli/test/commands.test.ts index 823a6b9cc..06b15a4dc 100644 --- a/packages/cli/test/commands.test.ts +++ b/packages/cli/test/commands.test.ts @@ -1,6 +1,6 @@ import { createMockLogger } from '@openfn/logger'; import test from 'ava'; -import mock from 'mock-fs'; +import { loadPath, mockFs, restoreFs } from '@openfn/test-helpers'; import { execSync } from 'node:child_process'; import fs from 'node:fs/promises'; import os from 'node:os'; @@ -11,6 +11,8 @@ import { cmd } from '../src/cli'; import commandParser from '../src/commands'; import type { Opts } from '../src/options'; +test.afterEach(restoreFs); + // These CLI tests take quite a while to run, mostly I think // because of all the FS mocking // To make them more robust in CI, all the tests in this file need @@ -20,7 +22,6 @@ const TIMEOUT = 1000 * 30; const logger = createMockLogger('', { level: 'debug' }); test.afterEach(() => { - mock.restore(); logger._reset(); }); @@ -58,26 +59,18 @@ async function run(command: string, job: string, options: RunOptions = {}) { const state = JSON.stringify(options.state) || '{ "data": {}, "configuration": {} }'; - // Ensure that pnpm is not mocked out - // This is needed to ensure that pnpm dependencies can be dynamically loaded - // (for recast in particular) - const pnpm = path.resolve('../../node_modules/.pnpm'); - const pkgPath = path.resolve('./package.json'); - const recastPath = `${pnpm}/recast@0.21.5`; // Mock the file system in-memory if (!options.disableMock) { - mock({ + mockFs({ [expressionPath]: job, [statePath]: state, [outputPath]: '{}', - [recastPath]: mock.load(recastPath, {}), // enable us to load test modules through the mock - '/modules/': mock.load(path.resolve('test/__modules__/'), {}), - '/repo/': mock.load(path.resolve('test/__repo__/'), {}), - '/monorepo/': mock.load(path.resolve('test/__monorepo__/'), {}), - //'node_modules': mock.load(path.resolve('node_modules/'), {}), - [pkgPath]: mock.load(pkgPath), + '/modules/': loadPath(path.resolve('test/__modules__/')), + '/repo/': loadPath(path.resolve('test/__repo__/')), + '/monorepo/': loadPath(path.resolve('test/__monorepo__/')), + [pkgPath]: loadPath(pkgPath), ...(options.mockfs ?? {}), }); } @@ -732,7 +725,7 @@ test.serial('compile a workflow: openfn compile wf.json to file', async (t) => { test.serial('docs should print documentation with full names', async (t) => { t.timeout(TIMEOUT); - mock({ + mockFs({ '/repo/docs/@openfn/language-common@1.0.0.json': JSON.stringify({ name: 'test', functions: [ @@ -764,8 +757,8 @@ test.serial('docs should print documentation with full names', async (t) => { test.serial('docs adaptor should list operations', async (t) => { t.timeout(TIMEOUT); const pkgPath = path.resolve('./package.json'); - mock({ - [pkgPath]: mock.load(pkgPath), + mockFs({ + [pkgPath]: loadPath(pkgPath), '/repo/docs/@openfn/language-common@1.0.0.json': JSON.stringify({ name: 'test', version: '1.0.0', @@ -794,7 +787,7 @@ test.serial( 'docs adaptor + operation should print documentation with shorthand names', async (t) => { t.timeout(TIMEOUT); - mock({ + mockFs({ '/repo/docs/@openfn/language-common@1.0.0.json': JSON.stringify({ name: 'test', functions: [ diff --git a/packages/cli/test/compile/compile.test.ts b/packages/cli/test/compile/compile.test.ts index cab8e333b..2cb9fa226 100644 --- a/packages/cli/test/compile/compile.test.ts +++ b/packages/cli/test/compile/compile.test.ts @@ -1,5 +1,5 @@ import test from 'ava'; -import mock from 'mock-fs'; +import { loadPath, mockFs, restoreFs } from '@openfn/test-helpers'; import fs from 'node:fs/promises'; import path from 'node:path'; import { createMockLogger } from '@openfn/logger'; @@ -11,13 +11,12 @@ import compile, { compileProject, } from '../../src/compile/compile'; import { CompileOptions } from '../../src/compile/command'; -import { mockFs, resetMockFs } from '../util'; import type { ExecutionPlan, Job } from '@openfn/runtime'; -const mockLog = createMockLogger(); +test.afterEach(restoreFs); -test.after(resetMockFs); +const mockLog = createMockLogger(); const expressionPath = '/job.js'; @@ -100,8 +99,6 @@ steps: const code = await fs.readFile(outPaths[0], 'utf-8'); t.true(code.includes('export default [x()]')); - - mock.restore(); }); test.serial( @@ -131,8 +128,6 @@ steps: // Only step-a has exportable code — step-b should not be written t.is(outPaths.length, 1); t.true(outPaths[0].endsWith('step-a.mjs')); - - mock.restore(); } ); @@ -161,8 +156,6 @@ steps: t.is(outPaths.length, 1); t.true(outPaths[0].startsWith('/proj/build/')); - - mock.restore(); } ); @@ -198,8 +191,6 @@ steps: ); t.is(outPaths.length, 1); await t.throwsAsync(() => fs.access('/proj/dist/old-workflow/stale.mjs')); - - mock.restore(); } ); @@ -225,8 +216,6 @@ steps: t.is(outPaths.length, 1); t.true(outPaths[0].startsWith('/out/')); - - mock.restore(); }); test.serial('throw an AbortError if a job is uncompilable', async (t) => { @@ -314,7 +303,7 @@ test.serial('loadTransformOptions: do nothing', async (t) => { test.serial( "resolveSpecifierPath: return null if the module can't be resolved locally", async (t) => { - mock({ + mockFs({ '/repo': {}, }); const path = await resolveSpecifierPath('pkg', '/repo', mockLog); @@ -360,7 +349,7 @@ test.serial( test.serial( 'resolveSpecifierPath: return a path to the repo if the module is found', async (t) => { - mock({ + mockFs({ '/repo/package.json': JSON.stringify({ name: 'repo', dependencies: { @@ -376,7 +365,7 @@ test.serial( test.serial( 'resolveSpecifierPath: return null if a module is not in the repo', async (t) => { - mock({ + mockFs({ '/repo/package.json': JSON.stringify({ name: 'repo', dependencies: { @@ -392,8 +381,8 @@ test.serial( test.serial( 'loadTransformOptions: describes imports from an explicit path', async (t) => { - mock({ - '/modules/': mock.load(path.resolve('test/__modules__/'), {}), + mockFs({ + '/modules/': loadPath(path.resolve('test/__modules__/')), }); const opts = { // This should find the times-two module in test/__modules__ @@ -416,8 +405,8 @@ test.serial( test.serial( 'loadTransformOptions: describes imports from an explicit path and version specifier', async (t) => { - mock({ - '/modules/': mock.load(path.resolve('test/__modules__/'), {}), + mockFs({ + '/modules/': loadPath(path.resolve('test/__modules__/')), }); const opts = { adaptors: ['times-two@1.0.0=/modules/times-two'], @@ -439,8 +428,8 @@ test.serial( test.serial( 'loadTransformOptions: describes imports from a relative path in the repo', async (t) => { - mock({ - '/repo/': mock.load(path.resolve('test/__repo__/'), {}), + mockFs({ + '/repo/': loadPath(path.resolve('test/__repo__/')), }); const opts = { adaptors: ['times-two'], diff --git a/packages/cli/test/compile/handler.test.ts b/packages/cli/test/compile/handler.test.ts index 24ec95d2e..887fcce47 100644 --- a/packages/cli/test/compile/handler.test.ts +++ b/packages/cli/test/compile/handler.test.ts @@ -1,10 +1,12 @@ import test from 'ava'; +import { mockFs, restoreFs } from '@openfn/test-helpers'; import fs from 'node:fs/promises'; import { createMockLogger } from '@openfn/logger'; import compileHandler from '../../src/compile/handler'; import { CompileOptions } from '../../src/compile/command'; -import { mockFs, resetMockFs } from '../util'; + +test.afterEach(restoreFs); const logger = createMockLogger(); @@ -12,8 +14,6 @@ test.afterEach(() => { logger._reset(); }); -test.after(resetMockFs); - test.serial('compile an expression file to stdout', async (t) => { mockFs({ '/job.js': 'x();', diff --git a/packages/cli/test/deploy/deploy.test.ts b/packages/cli/test/deploy/deploy.test.ts index ca2356a5d..82231fcf1 100644 --- a/packages/cli/test/deploy/deploy.test.ts +++ b/packages/cli/test/deploy/deploy.test.ts @@ -1,7 +1,7 @@ // Test the actual functionality of docgen // ie, generate docs to a mock folder import test from 'ava'; -import mockfs from 'mock-fs'; +import { mockFs, restoreFs } from '@openfn/test-helpers'; import { Logger, createMockLogger } from '@openfn/logger'; import deployHandler, { DeployFn, @@ -12,14 +12,16 @@ import { yamlToJson } from '@openfn/project'; import { DeployError, type DeployConfig } from '@openfn/deploy'; import { DeployOptions } from '../../src/deploy/command'; +test.afterEach(restoreFs); + const logger = createMockLogger(); const { OPENFN_API_KEY, OPENFN_ENDPOINT, ...originalEnv } = process.env; test.beforeEach(() => { - mockfs.restore(); logger._reset(); - mockfs({ + restoreFs(); + mockFs({ ['./config.json']: `{"apiKey": "123"}`, ['./project.yaml']: `{"apiKey": "123"}`, }); @@ -96,7 +98,8 @@ test.serial( 'redirects to beta handler when openfn.yaml exists in cwd', async (t) => { t.plan(3); - mockfs({ + restoreFs(); + mockFs({ ['./config.json']: `{"apiKey": "123"}`, ['./project.yaml']: `{"apiKey": "123"}`, ['./openfn.yaml']: 'project:\n endpoint: https://from-yaml.org', @@ -112,7 +115,8 @@ test.serial( test.serial('does not redirect when PREFER_LEGACY_SYNC is set', async (t) => { t.plan(1); - mockfs({ + restoreFs(); + mockFs({ ['./config.json']: `{"apiKey": "123", "endpoint": "https://api.example.com"}`, ['./project.yaml']: `{"apiKey": "123"}`, ['./openfn.yaml']: 'project:\n endpoint: https://from-yaml.org', @@ -129,7 +133,8 @@ test.serial('does not redirect when PREFER_LEGACY_SYNC is set', async (t) => { test.serial('CLI endpoint preferred over openfn.yaml endpoint', async (t) => { t.plan(1); - mockfs({ + restoreFs(); + mockFs({ ['./config.json']: `{"apiKey": "123"}`, ['./project.yaml']: `{"apiKey": "123"}`, ['./openfn.yaml']: 'project:\n endpoint: https://from-yaml.org', @@ -148,7 +153,8 @@ test.serial('CLI endpoint preferred over openfn.yaml endpoint', async (t) => { test.serial( 'openfn.yaml endpoint preferred over config.json endpoint', async (t) => { - mockfs({ + restoreFs(); + mockFs({ ['./config.json']: `{"apiKey": "123", "endpoint": "https://from-config.org"}`, ['./project.yaml']: `{"apiKey": "123"}`, ['./openfn.yaml']: 'project:\n endpoint: https://from-yaml.org', @@ -164,7 +170,8 @@ test.serial( 'checkout file endpoint preferred over config.json endpoint', async (t) => { t.plan(1); - mockfs({ + restoreFs(); + mockFs({ ['./config.json']: `{"apiKey": "123", "endpoint": "https://from-config.org"}`, ['./project.yaml']: `{"apiKey": "123"}`, ['./openfn.yaml']: '', @@ -179,7 +186,8 @@ test.serial( test.serial('loads the checkout file for the current branch', async (t) => { t.plan(1); - mockfs({ + restoreFs(); + mockFs({ ['./config.json']: `{"apiKey": "123", "endpoint": "https://from-config.org"}`, ['./project.yaml']: `{"apiKey": "123"}`, ['./openfn.yaml']: '', @@ -200,7 +208,8 @@ test.serial('loads the checkout file for the current branch', async (t) => { test.serial('warns if no endpoint can be found when redirecting', async (t) => { t.plan(1); - mockfs({ + restoreFs(); + mockFs({ ['./config.json']: `{"apiKey": "123"}`, ['./project.yaml']: `{"apiKey": "123"}`, ['./openfn.yaml']: '', @@ -212,7 +221,8 @@ test.serial('warns if no endpoint can be found when redirecting', async (t) => { }); test.serial('CLI apiKey preferred over config.json apiKey', async (t) => { - mockfs({ + restoreFs(); + mockFs({ ['./config.json']: `{"apiKey": "from-config"}`, ['./project.yaml']: `{"apiKey": "from-config"}`, ['./openfn.yaml']: 'project:\n endpoint: https://from-yaml.org', @@ -290,12 +300,12 @@ workflows: configuration: super@openfn.org|http1 `; -test('maybeConvertV2spec: returns v1 yaml unchanged', async (t) => { +test.serial('maybeConvertV2spec: returns v1 yaml unchanged', async (t) => { const result = await maybeConvertV2spec(v1Yaml); t.is(result, v1Yaml); }); -test('maybeConvertV2spec: converts v2 to v1', async (t) => { +test.serial('maybeConvertV2spec: converts v2 to v1', async (t) => { const result = await maybeConvertV2spec(v2Yaml); const json = yamlToJson(result) as any; @@ -319,7 +329,7 @@ test('maybeConvertV2spec: converts v2 to v1', async (t) => { t.falsy(json.schema_version); }); -test('maybeConvertV2spec: converts with credentials', async (t) => { +test.serial('maybeConvertV2spec: converts with credentials', async (t) => { const result = await maybeConvertV2spec(v2Yaml); const json = yamlToJson(result) as any; @@ -333,26 +343,31 @@ test('maybeConvertV2spec: converts with credentials', async (t) => { ); }); -test('maybeConvertV2spec: converted edges use key references, not UUIDs', async (t) => { - const result = await maybeConvertV2spec(v2Yaml); - const json = yamlToJson(result) as any; +test.serial( + 'maybeConvertV2spec: converted edges use key references, not UUIDs', + async (t) => { + const result = await maybeConvertV2spec(v2Yaml); + const json = yamlToJson(result) as any; - const workflow = Object.values(json.workflows)[0] as any; - const edge = Object.values(workflow.edges)[0] as any; + const workflow = Object.values(json.workflows)[0] as any; + const edge = Object.values(workflow.edges)[0] as any; - // edge must use spec format (key references) so mergeSpecIntoState can resolve them - t.truthy(edge.source_trigger); - t.truthy(edge.target_job); - t.falsy(edge.source_trigger_id); - t.falsy(edge.target_job_id); + // edge must use spec format (key references) so mergeSpecIntoState can resolve them + t.truthy(edge.source_trigger); + t.truthy(edge.target_job); + t.falsy(edge.source_trigger_id); + t.falsy(edge.target_job_id); - // source_trigger must match a trigger key; target_job must match a job key - t.truthy(workflow.triggers[edge.source_trigger]); - t.truthy(workflow.jobs[edge.target_job]); -}); + // source_trigger must match a trigger key; target_job must match a job key + t.truthy(workflow.triggers[edge.source_trigger]); + t.truthy(workflow.jobs[edge.target_job]); + } +); -test('maybeConvertV2spec: converts legacy v2 (cli.version: 2) to v1', async (t) => { - const legacyV2Yaml = `id: my-project +test.serial( + 'maybeConvertV2spec: converts legacy v2 (cli.version: 2) to v1', + async (t) => { + const legacyV2Yaml = `id: my-project name: My Project cli: version: 2 @@ -365,9 +380,10 @@ workflows: type: webhook enabled: true `; - const result = await maybeConvertV2spec(legacyV2Yaml); - const json = yamlToJson(result) as any; + const result = await maybeConvertV2spec(legacyV2Yaml); + const json = yamlToJson(result) as any; - t.is(typeof json.workflows, 'object'); - t.false(Array.isArray(json.workflows)); -}); + t.is(typeof json.workflows, 'object'); + t.false(Array.isArray(json.workflows)); + } +); diff --git a/packages/cli/test/docgen/handler.test.ts b/packages/cli/test/docgen/handler.test.ts index 22f11d17c..e08fbb761 100644 --- a/packages/cli/test/docgen/handler.test.ts +++ b/packages/cli/test/docgen/handler.test.ts @@ -1,21 +1,23 @@ // Test the actual functionality of docgen // ie, generate docs to a mock folder import test from 'ava'; +import { mockFs, restoreFs } from '@openfn/test-helpers'; import { readFileSync, existsSync } from 'node:fs'; import fs from 'node:fs/promises'; -import mockfs from 'mock-fs'; import { createMockLogger } from '@openfn/logger'; import docsHandler, { DocGenFn, ensurePath } from '../../src/docgen/handler'; +test.afterEach(restoreFs); + const logger = createMockLogger(); const REPO_PATH = '/tmp/repo'; const DOCS_PATH = `${REPO_PATH}/docs`; test.beforeEach(() => { - mockfs.restore(); logger._reset(); - mockfs({ + restoreFs(); + mockFs({ [DOCS_PATH]: {}, }); }); @@ -79,7 +81,8 @@ test.serial('log the result path', async (t) => { }); test.serial("ensurePath if there's no repo", (t) => { - mockfs({ + restoreFs(); + mockFs({ ['/tmp']: {}, }); ensurePath('/tmp/repo/docs/x.json'); @@ -88,7 +91,8 @@ test.serial("ensurePath if there's no repo", (t) => { }); test.serial("ensurePath if there's no docs folder", (t) => { - mockfs({ + restoreFs(); + mockFs({ ['/tmp/repo']: {}, }); ensurePath('/tmp/repo/docs/x.json'); @@ -97,7 +101,8 @@ test.serial("ensurePath if there's no docs folder", (t) => { }); test.serial("ensurePath if there's a namespace", (t) => { - mockfs({ + restoreFs(); + mockFs({ ['/tmp']: {}, }); ensurePath('/tmp/repo/docs/@openfn/language-common.json'); @@ -114,7 +119,8 @@ test.serial.skip('do not generate docs if they already exist', async (t) => { return {}; }) as unknown as DocGenFn; - mockfs({ + restoreFs(); + mockFs({ [`${DOCS_PATH}/${specifier}.json`]: '{ "name": "test" }', }); @@ -209,7 +215,8 @@ test.serial.skip("remove the placeholder if there's an error", async (t) => { test.serial.skip('wait for docs if a placeholder is present', async (t) => { const path = `${DOCS_PATH}/${specifier}.json`; - mockfs({ + restoreFs(); + mockFs({ [path]: `{ "loading": true, "timestamp": ${Date.now()} }`, }); @@ -242,7 +249,8 @@ test.serial.skip('wait for docs if a placeholder is present', async (t) => { test.serial.skip("throw there's a timeout", async (t) => { const path = `${DOCS_PATH}/${specifier}.json`; - mockfs({ + restoreFs(); + mockFs({ [path]: `{ "loading": true, "timestamp": ${Date.now()} }`, }); @@ -262,7 +270,8 @@ test.serial.skip( async (t) => { const path = `${DOCS_PATH}/${specifier}.json`; - mockfs({ + restoreFs(); + mockFs({ [path]: `{ "loading": true, "timestamp": ${Date.now()} }`, }); @@ -286,7 +295,8 @@ test.serial.skip( test.serial.skip("reset the placeholder if it's old", async (t) => { const path = `${DOCS_PATH}/${specifier}.json`; - mockfs({ + restoreFs(); + mockFs({ [path]: `{ "loading": true, "test", true, "timestamp": ${ Date.now() - 1001 } }`, diff --git a/packages/cli/test/env.test.ts b/packages/cli/test/env.test.ts index 33523f612..d2f898900 100644 --- a/packages/cli/test/env.test.ts +++ b/packages/cli/test/env.test.ts @@ -1,18 +1,19 @@ import test from 'ava'; -import mock from 'mock-fs'; +import { mockFs, restoreFs } from '@openfn/test-helpers'; import { createMockLogger } from '@openfn/logger'; import load, { report } from '../src/env'; +test.afterEach(restoreFs); + const logger = createMockLogger(undefined, { level: 'debug' }); test.afterEach(() => { logger._reset(); - mock.restore(); }); -test('should load and return values from .env', (t) => { - mock({ +test.serial('should load and return values from .env', (t) => { + mockFs({ '/.env': ` FOO: BAR `, @@ -24,8 +25,8 @@ test('should load and return values from .env', (t) => { }); }); -test('should report values loaded from .env', (t) => { - mock({ +test.serial('should report values loaded from .env', (t) => { + mockFs({ '/.env': ` FOO: BAR `, @@ -45,15 +46,15 @@ test('should report values loaded from .env', (t) => { t.truthy(c); }); -test('should return null if no env is found', (t) => { - mock({}); +test.serial('should return null if no env is found', (t) => { + mockFs({}); const env = load('/.env'); t.is(env, null); }); -test('should log if no env is found', (t) => { - mock({}); +test.serial('should log if no env is found', (t) => { + mockFs({}); load('/.env'); @@ -63,16 +64,19 @@ test('should log if no env is found', (t) => { t.truthy(a); }); -test('should use $DOT_ENV_OVERRIDES found', (t) => { - mock({}); +test.serial('should use $DOT_ENV_OVERRIDES found', (t) => { + mockFs({}); // don't load .env // but set this magic env var instead process.env.$DOT_ENV_OVERRIDES = 'FOO,BAR'; + t.teardown(() => { + delete process.env.$DOT_ENV_OVERRIDES; + }); report(logger); - const a = logger._find('always', /imported 1 env vars/i); + const a = logger._find('always', /imported 2 env vars/i); t.truthy(a); const b = logger._find('debug', /set from .env:/i); diff --git a/packages/cli/test/execute/execute.test.ts b/packages/cli/test/execute/execute.test.ts index 12dac44c1..e480fbe27 100644 --- a/packages/cli/test/execute/execute.test.ts +++ b/packages/cli/test/execute/execute.test.ts @@ -3,10 +3,12 @@ // I don't want any io or adaptor tests here, really just looking for the actual execute flow import { createMockLogger } from '@openfn/logger'; import test from 'ava'; +import { mockFs, restoreFs } from '@openfn/test-helpers'; import fs from 'node:fs/promises'; import { ExecuteOptions } from '../../src/execute/command'; import handler from '../../src/execute/handler'; -import { mockFs, resetMockFs } from '../util'; + +test.afterEach(restoreFs); const logger = createMockLogger(undefined, { level: 'none' }); @@ -28,8 +30,6 @@ const defaultOptions = { const fn = `const fn = (fn) => (s) => fn(s); `; -test.after(resetMockFs); - test.serial('run a simple job', async (t) => { const job = `${fn}fn(() => ({ data: 42 }));`; diff --git a/packages/cli/test/execute/serialize-output.test.ts b/packages/cli/test/execute/serialize-output.test.ts index a698281d0..442f275a9 100644 --- a/packages/cli/test/execute/serialize-output.test.ts +++ b/packages/cli/test/execute/serialize-output.test.ts @@ -1,10 +1,12 @@ import test from 'ava'; -import mockfs from 'mock-fs'; +import { mockFs, restoreFs } from '@openfn/test-helpers'; import { readFile } from 'node:fs/promises'; import serializeOutput from '../../src/execute/serialize-output'; import { createMockLogger } from '@openfn/logger'; +test.afterEach(restoreFs); + const logger = createMockLogger(undefined, { level: 'debug' }); test.beforeEach(() => { @@ -13,7 +15,7 @@ test.beforeEach(() => { const toString = (obj: object) => JSON.stringify(obj, null, 2); -test('output true', async (t) => { +test.serial('output true', async (t) => { const result = await serializeOutput( { outputStdout: true, @@ -24,7 +26,7 @@ test('output true', async (t) => { t.is(result, 'true'); }); -test('output false', async (t) => { +test.serial('output false', async (t) => { const result = await serializeOutput( { outputStdout: true, @@ -35,7 +37,7 @@ test('output false', async (t) => { t.is(result, 'false'); }); -test('output a number', async (t) => { +test.serial('output a number', async (t) => { const result = await serializeOutput( { outputStdout: true, @@ -46,7 +48,7 @@ test('output a number', async (t) => { t.is(result, '42'); }); -test('output null', async (t) => { +test.serial('output null', async (t) => { const result = await serializeOutput( { outputStdout: true, @@ -57,7 +59,7 @@ test('output null', async (t) => { t.is(result, 'null'); }); -test('output undefined as an empty string', async (t) => { +test.serial('output undefined as an empty string', async (t) => { const result = await serializeOutput( { outputStdout: true, @@ -68,7 +70,7 @@ test('output undefined as an empty string', async (t) => { t.is(result, ''); }); -test('output a string', async (t) => { +test.serial('output a string', async (t) => { const result = await serializeOutput( { outputStdout: true, @@ -79,7 +81,7 @@ test('output a string', async (t) => { t.is(result, '"ok"'); }); -test('output an array', async (t) => { +test.serial('output an array', async (t) => { const result = await serializeOutput( { outputStdout: true, @@ -90,7 +92,7 @@ test('output an array', async (t) => { t.is(result, toString(['ok'])); }); -test('output an object', async (t) => { +test.serial('output an object', async (t) => { const result = await serializeOutput( { outputStdout: true, @@ -101,7 +103,7 @@ test('output an object', async (t) => { t.is(result, '{}'); }); -test('exclude configuration', async (t) => { +test.serial('exclude configuration', async (t) => { const result = await serializeOutput( { outputStdout: true, @@ -117,8 +119,8 @@ test('exclude configuration', async (t) => { t.is(result, toString({ data: {}, foo: 'bar', _secret: true })); }); -test('output to file', async (t) => { - mockfs({ +test.serial('output to file', async (t) => { + mockFs({ 'out.json': '', }); diff --git a/packages/cli/test/memfs.test.ts b/packages/cli/test/memfs.test.ts new file mode 100644 index 000000000..60c8be4ce --- /dev/null +++ b/packages/cli/test/memfs.test.ts @@ -0,0 +1,76 @@ +import test from 'ava'; +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { promisify } from 'node:util'; +import { createRequire } from 'node:module'; +import { mockFs, restoreFs } from '@openfn/test-helpers'; + +const require = createRequire(import.meta.url); + +// Resolved without require, so that require's own caches stay cold +const realModule = fs.realpathSync('node_modules/treeify/treeify.js'); + +test.afterEach(restoreFs); + +test.serial('require loads a package dependency while mocked', (t) => { + mockFs(); + const treeify = require('treeify'); + t.is(typeof treeify.asTree, 'function'); +}); + +test.serial('copyFileSync from node_modules writes into memfs', (t) => { + const dest = path.join(os.tmpdir(), `memfs-copy-${process.pid}.js`); + mockFs(); + try { + fs.copyFileSync(realModule, dest); + t.true(fs.existsSync(dest)); + restoreFs(); + t.false(fs.existsSync(dest)); + } finally { + restoreFs(); + fs.rmSync(dest, { force: true }); + } +}); + +test.serial('renameSync from node_modules into memfs throws EXDEV', (t) => { + mockFs(); + t.throws(() => fs.renameSync(realModule, '/moved.js'), { code: 'EXDEV' }); + restoreFs(); + t.true(fs.existsSync(realModule)); +}); + +test.serial('fstatSync works on stdout while mocked', (t) => { + mockFs(); + t.notThrows(() => fs.fstatSync(1)); +}); + +test.serial('fds from fs.promises.open can be read and closed', async (t) => { + mockFs({ '/a.txt': 'hello' }); + for (const [file, expected] of [ + ['/a.txt', 'hello'], + [realModule, fs.readFileSync(realModule, 'utf8').slice(0, 5)], + ]) { + const handle = await fs.promises.open(file); + const buffer = Buffer.alloc(5); + fs.readSync(handle.fd, buffer, 0, 5, 0); + t.is(buffer.toString(), expected); + await handle.close(); + } +}); + +test.serial('promisified exists resolves true for a mocked path', async (t) => { + mockFs({ '/a.txt': 'hello' }); + t.true(await promisify(fs.exists)('/a.txt')); +}); + +test.serial('a Buffer path into node_modules reads the real disk', (t) => { + mockFs(); + const content = fs.readFileSync(Buffer.from(realModule), 'utf8'); + t.true(content.length > 0); +}); + +test.serial('mockFs throws while a mock is already active', (t) => { + mockFs(); + t.throws(() => mockFs()); +}); diff --git a/packages/cli/test/projects/checkout.test.ts b/packages/cli/test/projects/checkout.test.ts index 9bd5eb277..bba033d7d 100644 --- a/packages/cli/test/projects/checkout.test.ts +++ b/packages/cli/test/projects/checkout.test.ts @@ -1,12 +1,14 @@ import test from 'ava'; +import { mockFs, restoreFs } from '@openfn/test-helpers'; import { createMockLogger } from '@openfn/logger'; import { handler as checkoutHandler } from '../../src/projects/checkout'; -import mock from 'mock-fs'; import fs from 'fs'; import { jsonToYaml, Workspace, yamlToJson } from '@openfn/project'; +test.afterEach(restoreFs); + test.beforeEach(() => { - mock({ + mockFs({ '/ws/workflows': {}, '/ws/openfn.yaml': jsonToYaml({ formats: { openfn: 'yaml', project: 'yaml', workflow: 'yaml' }, @@ -401,7 +403,8 @@ test.serial.skip('checkout: switch with alias and domain', async (t) => { }); test.serial('respect openfn.yaml settings', async (t) => { - mock({ + restoreFs(); + mockFs({ '/ws1/w': {}, '/ws1/openfn.yaml': jsonToYaml({ dirs: { workflows: 'w', projects: 'p' }, @@ -548,7 +551,8 @@ uuid: test.serial( 'checkout: removes old workflow directory when workflow is renamed on server', async (t) => { - mock({ + restoreFs(); + mockFs({ '/ws3/workflows/old-workflow': { 'old-workflow.yaml': jsonToYaml({ id: 'old-workflow', @@ -591,7 +595,8 @@ test.serial( test.serial( 'checkout: removes old step file when step is renamed on server', async (t) => { - mock({ + restoreFs(); + mockFs({ '/ws4/workflows/my-workflow': { 'my-workflow.yaml': jsonToYaml({ id: 'my-workflow', @@ -633,7 +638,8 @@ test.serial( ); test.serial('checkout: creates credentials.yaml', async (t) => { - mock({ + restoreFs(); + mockFs({ '/ws2/workflows': {}, '/ws2/openfn.yaml': '', '/ws2/.openfn/checkout.yaml': jsonToYaml({ id: 'main-project' }), @@ -684,7 +690,8 @@ test.serial('checkout: creates credentials.yaml', async (t) => { }); test.serial('checkout: do not create credentials.yaml', async (t) => { - mock({ + restoreFs(); + mockFs({ '/ws2/workflows': {}, '/ws2/openfn.yaml': '', '/ws2/.openfn/checkout.yaml': jsonToYaml({ id: 'main-project' }), @@ -733,7 +740,8 @@ test.serial('checkout: do not create credentials.yaml', async (t) => { test.serial( 'checkout: removes workflow directory when workflow is deleted on server', async (t) => { - mock({ + restoreFs(); + mockFs({ '/ws5/workflows/workflow-a': { 'workflow-a.yaml': jsonToYaml({ id: 'workflow-a', @@ -779,7 +787,8 @@ test.serial( test.serial( 'checkout: writes fetched collections into resources.yaml', async (t) => { - mock({ + restoreFs(); + mockFs({ '/ws6/workflows': {}, '/ws6/openfn.yaml': '', '/ws6/.openfn/checkout.yaml': jsonToYaml({ id: 'main-project' }), @@ -951,7 +960,8 @@ workflows: test.serial( 'Checkout unrelated bar from unrelated project foo without divergence warning', async (t) => { - mock({ + restoreFs(); + mockFs({ '/tmp/openfn.yaml': '', '/tmp/.projects/main@server.yaml': foo, '/tmp/.projects/staging@server.yaml': bar, @@ -999,7 +1009,8 @@ test.serial( test.serial( 'Checkout unrelated foo from unrelated project bar without divergence warning', async (t) => { - mock({ + restoreFs(); + mockFs({ '/tmp/openfn.yaml': '', '/tmp/.projects/main@server.yaml': foo, '/tmp/.projects/staging@server.yaml': bar, @@ -1047,7 +1058,8 @@ test.serial( test.serial( 'If the checked out project has diverged from the tracked version, show a divergence warning on checkout', async (t) => { - mock({ + restoreFs(); + mockFs({ '/tmp/openfn.yaml': '', '/tmp/.projects/main@server.yaml': foo, '/tmp/.projects/staging@server.yaml': bar, diff --git a/packages/cli/test/projects/clean.test.ts b/packages/cli/test/projects/clean.test.ts index a94926b39..e4636462d 100644 --- a/packages/cli/test/projects/clean.test.ts +++ b/packages/cli/test/projects/clean.test.ts @@ -1,10 +1,12 @@ import test from 'ava'; +import { mockFs, restoreFs } from '@openfn/test-helpers'; import { createMockLogger } from '@openfn/logger'; import { handler as cleanHandler } from '../../src/projects/clean'; -import mock from 'mock-fs'; import fs from 'fs'; import { jsonToYaml } from '@openfn/project'; +test.afterEach(restoreFs); + const logger = createMockLogger('', { level: 'debug' }); const projectStateFile = jsonToYaml({ @@ -38,7 +40,7 @@ const projectStateFile = jsonToYaml({ }); test.beforeEach(() => { - mock({ + mockFs({ '/ws/workflows/old-workflow': { 'old-job.js': 'fn(s => s)', }, @@ -51,7 +53,6 @@ test.beforeEach(() => { }); test.afterEach(() => { - mock.restore(); logger._reset(); }); @@ -94,7 +95,8 @@ test.serial('clean: aborts if user declines confirmation', async (t) => { test.serial( 'clean: throws if no active project found in workspace', async (t) => { - mock({ '/ws/workflows': {} }); + restoreFs(); + mockFs({ '/ws/workflows': {} }); await t.throwsAsync( () => @@ -111,7 +113,8 @@ test.serial( 'does not delete anything if it is not known which project is checked out', async (t) => { // two tracked projects, but no checkout metadata anywhere - mock({ + restoreFs(); + mockFs({ '/ws/workflows/old-workflow': { 'old-job.js': 'fn(s => s)' }, '/ws/openfn.yaml': '', '/ws/.projects/main@app.openfn.org.yaml': projectStateFile, @@ -133,7 +136,8 @@ test.serial( 'does not delete anything if there is no active project to restore', async (t) => { // a workspace with no projects and no checkout metadata at all - mock({ + restoreFs(); + mockFs({ '/ws/workflows/old-workflow': { 'old-job.js': 'fn(s => s)' }, '/ws/openfn.yaml': '', }); diff --git a/packages/cli/test/projects/credentials-helpers.test.ts b/packages/cli/test/projects/credentials-helpers.test.ts index 42feb0c02..60a4dd963 100644 --- a/packages/cli/test/projects/credentials-helpers.test.ts +++ b/packages/cli/test/projects/credentials-helpers.test.ts @@ -1,6 +1,6 @@ import test from 'ava'; import fs from 'node:fs'; -import mock from 'mock-fs'; +import { mockFs, restoreFs } from '@openfn/test-helpers'; import Project, { yamlToJson } from '@openfn/project'; @@ -18,11 +18,7 @@ import { default as parseCredentialsOption, } from '../../src/projects/credentials-helpers'; -test.afterEach(() => { - try { - mock.restore(); - } catch {} -}); +test.afterEach(restoreFs); const createProject = (credentials: any[], steps: any[]) => new Project({ @@ -38,7 +34,7 @@ const step = (id: string, configuration?: string) => ({ configuration, }); -test('byNone drops every credential', (t) => { +test.serial('byNone drops every credential', (t) => { const project = createProject( [{ name: 'a', owner: 'joe@openfn.org' }], [step('x', 'joe@openfn.org|a')] @@ -52,7 +48,7 @@ test('byNone drops every credential', (t) => { t.is(project.workflows[0].steps[0].configuration as any, null); }); -test('byAll keeps every credential and reference untouched', (t) => { +test.serial('byAll keeps every credential and reference untouched', (t) => { const project = createProject( [{ name: 'a', owner: 'joe@openfn.org' }], [step('x', 'joe@openfn.org|a')] @@ -64,7 +60,7 @@ test('byAll keeps every credential and reference untouched', (t) => { t.is(project.workflows[0].steps[0].configuration, 'joe@openfn.org|a'); }); -test('byPrune drops credentials not referenced by any step', (t) => { +test.serial('byPrune drops credentials not referenced by any step', (t) => { const project = createProject( [ { name: 'a', owner: 'joe@openfn.org' }, @@ -79,7 +75,7 @@ test('byPrune drops credentials not referenced by any step', (t) => { t.is(project.workflows[0].steps[0].configuration, 'joe@openfn.org|a'); }); -test('byMap drops credentials not named in the map', (t) => { +test.serial('byMap drops credentials not named in the map', (t) => { const project = createProject( [ { name: 'a', owner: 'joe@openfn.org' }, @@ -95,207 +91,255 @@ test('byMap drops credentials not named in the map', (t) => { t.is(project.workflows[0].steps[1].configuration as any, null); }); -test('byMap renames a credential and rewrites its step references', (t) => { - const project = createProject( - [{ name: 'c', owner: 'joe@openfn.org' }], - [step('x', 'joe@openfn.org|c')] - ); - - remapCredentials( - project, - byMap({ c: { name: 'c', owner: 'other@openfn.org' } }) - ); - - t.deepEqual(project.credentials, [ - { name: 'c', owner: 'other@openfn.org' }, - ]); - t.is(project.workflows[0].steps[0].configuration, 'other@openfn.org|c'); -}); - -test('byCredentialsFile drops credentials not listed in the file, keeps listed ones', (t) => { - const project = createProject( - [ - { name: 'a', owner: 'joe@openfn.org' }, - { name: 'b', owner: 'joe@openfn.org' }, - ], - [step('x', 'joe@openfn.org|a'), step('y', 'joe@openfn.org|b')] - ); - - remapCredentials( - project, - byCredentialsFile({ 'joe@openfn.org|a': undefined as any }) - ); - - t.deepEqual(project.credentials, [{ name: 'a', owner: 'joe@openfn.org' }]); - t.is(project.workflows[0].steps[0].configuration, 'joe@openfn.org|a'); - t.is(project.workflows[0].steps[1].configuration as any, null); -}); - -test('byCredentialsFile renames a credential per its alias entry', (t) => { - const project = createProject( - [{ name: 'a', owner: 'joe@openfn.org' }], - [step('x', 'joe@openfn.org|a')] - ); - - remapCredentials( - project, - byCredentialsFile({ - 'joe@openfn.org|a': { name: 'renamed', owner: 'other@openfn.org' }, - }) - ); - - t.deepEqual(project.credentials, [ - { name: 'renamed', owner: 'other@openfn.org' }, - ]); - t.is(project.workflows[0].steps[0].configuration, 'other@openfn.org|renamed'); -}); - -test('loadCredentialsMapFromFile: an entry with no alias just marks inclusion', (t) => { - mock({ - '/ws/credentials.yaml': `joe@openfn.org|a: +test.serial( + 'byMap renames a credential and rewrites its step references', + (t) => { + const project = createProject( + [{ name: 'c', owner: 'joe@openfn.org' }], + [step('x', 'joe@openfn.org|c')] + ); + + remapCredentials( + project, + byMap({ c: { name: 'c', owner: 'other@openfn.org' } }) + ); + + t.deepEqual(project.credentials, [ + { name: 'c', owner: 'other@openfn.org' }, + ]); + t.is(project.workflows[0].steps[0].configuration, 'other@openfn.org|c'); + } +); + +test.serial( + 'byCredentialsFile drops credentials not listed in the file, keeps listed ones', + (t) => { + const project = createProject( + [ + { name: 'a', owner: 'joe@openfn.org' }, + { name: 'b', owner: 'joe@openfn.org' }, + ], + [step('x', 'joe@openfn.org|a'), step('y', 'joe@openfn.org|b')] + ); + + remapCredentials( + project, + byCredentialsFile({ 'joe@openfn.org|a': undefined as any }) + ); + + t.deepEqual(project.credentials, [{ name: 'a', owner: 'joe@openfn.org' }]); + t.is(project.workflows[0].steps[0].configuration, 'joe@openfn.org|a'); + t.is(project.workflows[0].steps[1].configuration as any, null); + } +); + +test.serial( + 'byCredentialsFile renames a credential per its alias entry', + (t) => { + const project = createProject( + [{ name: 'a', owner: 'joe@openfn.org' }], + [step('x', 'joe@openfn.org|a')] + ); + + remapCredentials( + project, + byCredentialsFile({ + 'joe@openfn.org|a': { name: 'renamed', owner: 'other@openfn.org' }, + }) + ); + + t.deepEqual(project.credentials, [ + { name: 'renamed', owner: 'other@openfn.org' }, + ]); + t.is( + project.workflows[0].steps[0].configuration, + 'other@openfn.org|renamed' + ); + } +); + +test.serial( + 'loadCredentialsMapFromFile: an entry with no alias just marks inclusion', + (t) => { + mockFs({ + '/ws/credentials.yaml': `joe@openfn.org|a: user: someuser `, - }); - - const map = loadCredentialsMapFromFile('/ws/credentials.yaml'); - t.deepEqual(map, { 'joe@openfn.org|a': undefined as any }); -}); - -test('loadCredentialsMapFromFile: an entry with an alias carries it through', (t) => { - mock({ - '/ws/credentials.yaml': `joe@openfn.org|a: + }); + + const map = loadCredentialsMapFromFile('/ws/credentials.yaml'); + t.deepEqual(map, { 'joe@openfn.org|a': undefined as any }); + } +); + +test.serial( + 'loadCredentialsMapFromFile: an entry with an alias carries it through', + (t) => { + mockFs({ + '/ws/credentials.yaml': `joe@openfn.org|a: user: someuser alias: other@openfn.org|renamed `, - }); - - const map = loadCredentialsMapFromFile('/ws/credentials.yaml'); - t.deepEqual(map, { - 'joe@openfn.org|a': { name: 'renamed', owner: 'other@openfn.org' }, - }); -}); + }); -test('getCredentialsVisitor: a credentials file path syncs only what it lists', (t) => { - mock({ - '/ws/credentials.yaml': `joe@openfn.org|a: + const map = loadCredentialsMapFromFile('/ws/credentials.yaml'); + t.deepEqual(map, { + 'joe@openfn.org|a': { name: 'renamed', owner: 'other@openfn.org' }, + }); + } +); + +test.serial( + 'getCredentialsVisitor: a credentials file path syncs only what it lists', + (t) => { + mockFs({ + '/ws/credentials.yaml': `joe@openfn.org|a: user: someuser `, - }); - - const project = createProject( - [ - { name: 'a', owner: 'joe@openfn.org' }, - { name: 'b', owner: 'joe@openfn.org' }, - ], - [step('x', 'joe@openfn.org|a'), step('y', 'joe@openfn.org|b')] - ); - - remapCredentials( - project, - getCredentialsVisitor(project, 'credentials.yaml', '/ws') - ); - - t.deepEqual(project.credentials, [{ name: 'a', owner: 'joe@openfn.org' }]); - t.is(project.workflows[0].steps[1].configuration as any, null); -}); - -test('getCredentialsVisitor: throws when the --credentials map names a credential not in the project', (t) => { - const project = createProject( - [{ name: 'a', owner: 'joe@openfn.org' }], - [step('x', 'joe@openfn.org|a')] - ); - - const error = t.throws(() => - getCredentialsVisitor(project, { typo: { name: 'typo' } }) - ); - - t.regex(error!.message, /typo/); -}); + }); -test('getCredentialsVisitor: throws when a credentials.yaml file names a credential not in the project', (t) => { - mock({ - '/ws/credentials.yaml': `joe@openfn.org|not-a-real-credential: + const project = createProject( + [ + { name: 'a', owner: 'joe@openfn.org' }, + { name: 'b', owner: 'joe@openfn.org' }, + ], + [step('x', 'joe@openfn.org|a'), step('y', 'joe@openfn.org|b')] + ); + + remapCredentials( + project, + getCredentialsVisitor(project, 'credentials.yaml', '/ws') + ); + + t.deepEqual(project.credentials, [{ name: 'a', owner: 'joe@openfn.org' }]); + t.is(project.workflows[0].steps[1].configuration as any, null); + } +); + +test.serial( + 'getCredentialsVisitor: throws when the --credentials map names a credential not in the project', + (t) => { + const project = createProject( + [{ name: 'a', owner: 'joe@openfn.org' }], + [step('x', 'joe@openfn.org|a')] + ); + + const error = t.throws(() => + getCredentialsVisitor(project, { typo: { name: 'typo' } }) + ); + + t.regex(error!.message, /typo/); + } +); + +test.serial( + 'getCredentialsVisitor: throws when a credentials.yaml file names a credential not in the project', + (t) => { + mockFs({ + '/ws/credentials.yaml': `joe@openfn.org|not-a-real-credential: user: someuser `, - }); - - const project = createProject( - [{ name: 'a', owner: 'joe@openfn.org' }], - [step('x', 'joe@openfn.org|a')] - ); - - const error = t.throws(() => - getCredentialsVisitor(project, 'credentials.yaml', '/ws') - ); - - t.regex(error!.message, /joe@openfn\.org\|not-a-real-credential/); -}); - -test('getCredentialsVisitor: does not throw when every named credential exists', (t) => { - const project = createProject( - [{ name: 'a', owner: 'joe@openfn.org' }], - [step('x', 'joe@openfn.org|a')] - ); - - t.notThrows(() => - getCredentialsVisitor(project, { a: { name: 'renamed' } }) - ); -}); - -test('parseCredentialsOption: a credentials file path passes through unchanged', (t) => { - t.is(parseCredentialsOption('./credentials.yaml'), './credentials.yaml'); - t.is(parseCredentialsOption('creds.yml'), 'creds.yml'); - t.is(parseCredentialsOption('creds.json'), 'creds.json'); -}); - -test('parseCredentialsOption: none', (t) => { + }); + + const project = createProject( + [{ name: 'a', owner: 'joe@openfn.org' }], + [step('x', 'joe@openfn.org|a')] + ); + + const error = t.throws(() => + getCredentialsVisitor(project, 'credentials.yaml', '/ws') + ); + + t.regex(error!.message, /joe@openfn\.org\|not-a-real-credential/); + } +); + +test.serial( + 'getCredentialsVisitor: does not throw when every named credential exists', + (t) => { + const project = createProject( + [{ name: 'a', owner: 'joe@openfn.org' }], + [step('x', 'joe@openfn.org|a')] + ); + + t.notThrows(() => + getCredentialsVisitor(project, { a: { name: 'renamed' } }) + ); + } +); + +test.serial( + 'parseCredentialsOption: a credentials file path passes through unchanged', + (t) => { + t.is(parseCredentialsOption('./credentials.yaml'), './credentials.yaml'); + t.is(parseCredentialsOption('creds.yml'), 'creds.yml'); + t.is(parseCredentialsOption('creds.json'), 'creds.json'); + } +); + +test.serial('parseCredentialsOption: none', (t) => { t.is(parseCredentialsOption('none'), 'none'); }); -test('parseCredentialsOption: prune', (t) => { +test.serial('parseCredentialsOption: prune', (t) => { t.is(parseCredentialsOption('prune'), 'prune'); }); -test('parseCredentialsOption: all', (t) => { +test.serial('parseCredentialsOption: all', (t) => { t.is(parseCredentialsOption('all'), 'all'); }); -test('parseCredentialsOption: a single credential name with no alias', (t) => { - const result = parseCredentialsOption('a'); - t.deepEqual(result, { a: { name: 'a' } }); -}); - -test('parseCredentialsOption: a comma separated list of credential names', (t) => { - const result = parseCredentialsOption('a,b,c'); - t.deepEqual(result, { - a: { name: 'a' }, - b: { name: 'b' }, - c: { name: 'c' }, - }); -}); - -test('parseCredentialsOption: a credential mapped to a new name and owner', (t) => { - const result = parseCredentialsOption('c=joe@openfn.org|c'); - t.deepEqual(result, { - c: { name: 'c', owner: 'joe@openfn.org' }, - }); -}); - -test('parseCredentialsOption: a mix of plain names and aliased names', (t) => { - const result = parseCredentialsOption('a,b,c=joe@openfn.org|c'); - t.deepEqual(result, { - a: { name: 'a' }, - b: { name: 'b' }, - c: { name: 'c', owner: 'joe@openfn.org' }, - }); -}); - -test('parseCredentialsOption: an alias with a new name but no owner', (t) => { - const result = parseCredentialsOption('a=renamed'); - t.deepEqual(result, { - a: { name: 'renamed', owner: undefined }, - }); -}); +test.serial( + 'parseCredentialsOption: a single credential name with no alias', + (t) => { + const result = parseCredentialsOption('a'); + t.deepEqual(result, { a: { name: 'a' } }); + } +); + +test.serial( + 'parseCredentialsOption: a comma separated list of credential names', + (t) => { + const result = parseCredentialsOption('a,b,c'); + t.deepEqual(result, { + a: { name: 'a' }, + b: { name: 'b' }, + c: { name: 'c' }, + }); + } +); + +test.serial( + 'parseCredentialsOption: a credential mapped to a new name and owner', + (t) => { + const result = parseCredentialsOption('c=joe@openfn.org|c'); + t.deepEqual(result, { + c: { name: 'c', owner: 'joe@openfn.org' }, + }); + } +); + +test.serial( + 'parseCredentialsOption: a mix of plain names and aliased names', + (t) => { + const result = parseCredentialsOption('a,b,c=joe@openfn.org|c'); + t.deepEqual(result, { + a: { name: 'a' }, + b: { name: 'b' }, + c: { name: 'c', owner: 'joe@openfn.org' }, + }); + } +); + +test.serial( + 'parseCredentialsOption: an alias with a new name but no owner', + (t) => { + const result = parseCredentialsOption('a=renamed'); + t.deepEqual(result, { + a: { name: 'renamed', owner: undefined }, + }); + } +); const baseWorkflow = (steps: any[]) => ({ id: 'wf', @@ -304,7 +348,7 @@ const baseWorkflow = (steps: any[]) => ({ steps, }); -test('findCredentialIds: inline string references', (t) => { +test.serial('findCredentialIds: inline string references', (t) => { const project = new Project({ id: 'p', workflows: [ @@ -320,7 +364,7 @@ test('findCredentialIds: inline string references', (t) => { t.deepEqual(ids, ['owner|cred']); }); -test('findCredentialIds: ignores duplicate references', (t) => { +test.serial('findCredentialIds: ignores duplicate references', (t) => { const project = new Project({ id: 'p', workflows: [ @@ -332,8 +376,8 @@ test('findCredentialIds: ignores duplicate references', (t) => { t.deepEqual(findCredentialIds(project), ['same']); }); -test('createProjectCredentials: creates credential yaml file', (t) => { - mock({ '/ws': {} }); +test.serial('createProjectCredentials: creates credential yaml file', (t) => { + mockFs({ '/ws': {} }); const project = new Project( { @@ -352,32 +396,35 @@ test('createProjectCredentials: creates credential yaml file', (t) => { t.deepEqual(doc, { 'new-id': {} }); }); -test('createProjectCredentials: preserves existing credentials and adds missing ones', (t) => { - mock({ - '/ws': {}, - '/ws/credentials.yaml': `existing: +test.serial( + 'createProjectCredentials: preserves existing credentials and adds missing ones', + (t) => { + mockFs({ + '/ws': {}, + '/ws/credentials.yaml': `existing: password: secret `, - }); - - const project = new Project( - { - id: 'p', - workflows: [ - baseWorkflow([ - { id: 'j', configuration: 'existing' }, - { id: 'k', configuration: 'brand-new' }, - ]), - ], - } as any, - { credentials: 'credentials.yaml' } - ); - - createProjectCredentials('/ws', project); - - const doc = yamlToJson( - fs.readFileSync('/ws/credentials.yaml', 'utf8') - ) as any; - t.is(doc.existing.password, 'secret'); - t.deepEqual(doc['brand-new'], {}); -}); + }); + + const project = new Project( + { + id: 'p', + workflows: [ + baseWorkflow([ + { id: 'j', configuration: 'existing' }, + { id: 'k', configuration: 'brand-new' }, + ]), + ], + } as any, + { credentials: 'credentials.yaml' } + ); + + createProjectCredentials('/ws', project); + + const doc = yamlToJson( + fs.readFileSync('/ws/credentials.yaml', 'utf8') + ) as any; + t.is(doc.existing.password, 'secret'); + t.deepEqual(doc['brand-new'], {}); + } +); diff --git a/packages/cli/test/projects/deploy.test.ts b/packages/cli/test/projects/deploy.test.ts index 6abddc2a2..87e49e8d9 100644 --- a/packages/cli/test/projects/deploy.test.ts +++ b/packages/cli/test/projects/deploy.test.ts @@ -1,8 +1,6 @@ import { writeFile } from 'node:fs/promises'; import test from 'ava'; -import mock from 'mock-fs'; -import path from 'node:path'; -import { createRequire } from 'node:module'; +import { mockFs, restoreFs } from '@openfn/test-helpers'; import fs from 'node:fs'; import Project, { generateWorkflow, @@ -32,6 +30,8 @@ import { import { checkout } from '../../src/projects'; import { rimraf } from 'rimraf'; +test.afterEach(restoreFs); + let server: any; let strictServer: any; const logger = createMockLogger(undefined, { level: 'debug' }); @@ -44,28 +44,6 @@ const STRICT_ENDPOINT = `http://localhost:${strictPort}`; const projectYaml = myProject_yaml.replace('https://app.openfn.org', ENDPOINT); const two_workflows_yaml = twowfs.replace('https://app.openfn.org', ENDPOINT); -const require = createRequire(import.meta.url); - -const mockFs = (paths: Record) => { - // ensure this path is available to pnpm (needed by deps for some reason??) - // Note: loading all of pnpm takes ~7 seconds per test - // this workaround cuts out that delay entirely - const iconv = path.resolve( - '../../node_modules/.pnpm/iconv-lite@0.4.24/node_modules/iconv-lite/encodings' - ); - // undici v8 reads its llhttp WASM from disk on the first request, so keep - // that dir visible or fetches to the mock server fail with ENOENT - const undicihttp = path.join( - path.dirname(require.resolve('undici')), - 'lib/llhttp' - ); - mock({ - [iconv]: mock.load(iconv, {}), - [undicihttp]: mock.load(undicihttp, {}), - ...paths, - }); -}; - // Take a project yaml and expand it // This uses checkout to do the heavy lifting const setup = async (yaml: string = projectYaml) => { @@ -92,7 +70,6 @@ test.beforeEach(() => { server.reset(); server.addProject(myProject_v1); logger._reset(); - mock.restore(); }); test.serial( @@ -945,28 +922,31 @@ test.serial( } ); -test('printRichDiff: should report no changes for identical projects', (t) => { - const wf = generateWorkflow('@id a trigger-x'); +test.serial( + 'printRichDiff: should report no changes for identical projects', + (t) => { + const wf = generateWorkflow('@id a trigger-x'); - const local = new Project({ - name: 'local', - workflows: [wf], - }); + const local = new Project({ + name: 'local', + workflows: [wf], + }); - const remote = new Project({ - name: 'remote', - workflows: [wf], - }); + const remote = new Project({ + name: 'remote', + workflows: [wf], + }); - const diffs = printRichDiff(local, remote, [], logger); - t.is(diffs.length, 0); + const diffs = printRichDiff(local, remote, [], logger); + t.is(diffs.length, 0); - const { message, level } = logger._parse(logger._last); - t.is(level, 'info'); - t.is(message, 'No workflow changes detected'); -}); + const { message, level } = logger._parse(logger._last); + t.is(level, 'info'); + t.is(message, 'No workflow changes detected'); + } +); -test('printRichDiff: should report changed workflow', (t) => { +test.serial('printRichDiff: should report changed workflow', (t) => { const wfRemote = generateWorkflow('@id a trigger-x'); const wfLocal = generateWorkflow('@id a trigger-y'); @@ -987,7 +967,7 @@ test('printRichDiff: should report changed workflow', (t) => { t.truthy(logger._find('always', /: changed/i)); }); -test('printRichDiff: should report added workflow', (t) => { +test.serial('printRichDiff: should report added workflow', (t) => { const wf1 = generateWorkflow('@id a trigger-x'); const wf2 = generateWorkflow('@id b trigger-y'); @@ -1008,7 +988,7 @@ test('printRichDiff: should report added workflow', (t) => { t.truthy(logger._find('always', /: added/i)); }); -test('printRichDiff: should report removed workflow', (t) => { +test.serial('printRichDiff: should report removed workflow', (t) => { const wf1 = generateWorkflow('@id a trigger-x'); const wf2 = generateWorkflow('@id b trigger-y'); @@ -1029,45 +1009,48 @@ test('printRichDiff: should report removed workflow', (t) => { t.truthy(logger._find('always', /: deleted/i)); }); -test('printRichDiff: should report mix of added, changed, and removed workflows', (t) => { - const wf1 = generateWorkflow('@id a trigger-x'); - const wf2Remote = generateWorkflow('@id b trigger-y'); - const wf2Local = generateWorkflow('@id b trigger-different'); - const wf3 = generateWorkflow('@id c trigger-z'); - const wf4 = generateWorkflow('@id d trigger-w'); - - const local = new Project({ - name: 'local', - workflows: [wf1, wf2Local, wf4], // has a, b (changed), d (new) - }); +test.serial( + 'printRichDiff: should report mix of added, changed, and removed workflows', + (t) => { + const wf1 = generateWorkflow('@id a trigger-x'); + const wf2Remote = generateWorkflow('@id b trigger-y'); + const wf2Local = generateWorkflow('@id b trigger-different'); + const wf3 = generateWorkflow('@id c trigger-z'); + const wf4 = generateWorkflow('@id d trigger-w'); + + const local = new Project({ + name: 'local', + workflows: [wf1, wf2Local, wf4], // has a, b (changed), d (new) + }); - const remote = new Project({ - name: 'remote', - workflows: [wf1, wf2Remote, wf3], // has a, b, c - }); + const remote = new Project({ + name: 'remote', + workflows: [wf1, wf2Remote, wf3], // has a, b, c + }); - const diffs = printRichDiff(local, remote, [], logger); - t.is(diffs.length, 3); + const diffs = printRichDiff(local, remote, [], logger); + t.is(diffs.length, 3); - t.deepEqual( - diffs.find((d) => d.id === 'b'), - { id: 'b', type: 'changed' } - ); - t.deepEqual( - diffs.find((d) => d.id === 'c'), - { id: 'c', type: 'removed' } - ); - t.deepEqual( - diffs.find((d) => d.id === 'd'), - { id: 'd', type: 'added' } - ); + t.deepEqual( + diffs.find((d) => d.id === 'b'), + { id: 'b', type: 'changed' } + ); + t.deepEqual( + diffs.find((d) => d.id === 'c'), + { id: 'c', type: 'removed' } + ); + t.deepEqual( + diffs.find((d) => d.id === 'd'), + { id: 'd', type: 'added' } + ); - t.truthy(logger._find('always', /: added/i)); - t.truthy(logger._find('always', /: changed/i)); - t.truthy(logger._find('always', /: deleted/i)); -}); + t.truthy(logger._find('always', /: added/i)); + t.truthy(logger._find('always', /: changed/i)); + t.truthy(logger._find('always', /: deleted/i)); + } +); -test('hasRemoteDiverged: 1 workflow, no diverged', (t) => { +test.serial('hasRemoteDiverged: 1 workflow, no diverged', (t) => { const local = { workflows: [ { @@ -1092,7 +1075,7 @@ test('hasRemoteDiverged: 1 workflow, no diverged', (t) => { t.falsy(diverged); }); -test('hasRemoteDiverged: 1 workflow, 1 diverged', (t) => { +test.serial('hasRemoteDiverged: 1 workflow, 1 diverged', (t) => { const local = { workflows: [ { @@ -1117,21 +1100,24 @@ test('hasRemoteDiverged: 1 workflow, 1 diverged', (t) => { t.deepEqual(diverged, ['w']); }); -test('collectionsChanged: false when the same names are on both sides', (t) => { - const local = { - collections: [{ name: 'a' }, { name: 'b' }], - } as unknown as Project; - const remote = { - collections: [ - { uuid: 'uuid-a', name: 'a' }, - { uuid: 'uuid-b', name: 'b' }, - ], - } as unknown as Project; +test.serial( + 'collectionsChanged: false when the same names are on both sides', + (t) => { + const local = { + collections: [{ name: 'a' }, { name: 'b' }], + } as unknown as Project; + const remote = { + collections: [ + { uuid: 'uuid-a', name: 'a' }, + { uuid: 'uuid-b', name: 'b' }, + ], + } as unknown as Project; - t.false(collectionsChanged(local, remote)); -}); + t.false(collectionsChanged(local, remote)); + } +); -test('collectionsChanged: true when a name was added locally', (t) => { +test.serial('collectionsChanged: true when a name was added locally', (t) => { const local = { collections: [{ name: 'a' }, { name: 'b' }], } as unknown as Project; @@ -1142,7 +1128,7 @@ test('collectionsChanged: true when a name was added locally', (t) => { t.true(collectionsChanged(local, remote)); }); -test('collectionsChanged: true when a name was removed locally', (t) => { +test.serial('collectionsChanged: true when a name was removed locally', (t) => { const local = { collections: [{ name: 'a' }], } as unknown as Project; @@ -1156,63 +1142,78 @@ test('collectionsChanged: true when a name was removed locally', (t) => { t.true(collectionsChanged(local, remote)); }); -test('collectionsChanged: false when collections are not managed locally', (t) => { - // undefined means "leave alone", unlike an empty list - const local = {} as unknown as Project; - const remote = { - collections: [{ uuid: 'uuid-a', name: 'a' }], - } as unknown as Project; - - t.false(collectionsChanged(local, remote)); -}); - -test('collectionsChanged: true when every collection was removed locally', (t) => { - const local = { collections: [] } as unknown as Project; - const remote = { - collections: [{ uuid: 'uuid-a', name: 'a' }], - } as unknown as Project; - - t.true(collectionsChanged(local, remote)); -}); +test.serial( + 'collectionsChanged: false when collections are not managed locally', + (t) => { + // undefined means "leave alone", unlike an empty list + const local = {} as unknown as Project; + const remote = { + collections: [{ uuid: 'uuid-a', name: 'a' }], + } as unknown as Project; + + t.false(collectionsChanged(local, remote)); + } +); -test('deletedCollections: nothing to delete when collections are not managed locally', (t) => { - const merged = {} as unknown as Project; - const remote = { - collections: [{ uuid: 'uuid-a', name: 'a' }], - } as unknown as Project; +test.serial( + 'collectionsChanged: true when every collection was removed locally', + (t) => { + const local = { collections: [] } as unknown as Project; + const remote = { + collections: [{ uuid: 'uuid-a', name: 'a' }], + } as unknown as Project; + + t.true(collectionsChanged(local, remote)); + } +); - t.deepEqual(deletedCollections(merged, remote), []); -}); +test.serial( + 'deletedCollections: nothing to delete when collections are not managed locally', + (t) => { + const merged = {} as unknown as Project; + const remote = { + collections: [{ uuid: 'uuid-a', name: 'a' }], + } as unknown as Project; + + t.deepEqual(deletedCollections(merged, remote), []); + } +); -test('deletedCollections: flags a remote name missing from the merged project', (t) => { - const merged = { - collections: [{ name: 'keep-me' }], - } as unknown as Project; - const remote = { - collections: [ - { uuid: 'uuid-keep', name: 'keep-me' }, - { uuid: 'uuid-remove', name: 'remove-me' }, - ], - } as unknown as Project; +test.serial( + 'deletedCollections: flags a remote name missing from the merged project', + (t) => { + const merged = { + collections: [{ name: 'keep-me' }], + } as unknown as Project; + const remote = { + collections: [ + { uuid: 'uuid-keep', name: 'keep-me' }, + { uuid: 'uuid-remove', name: 'remove-me' }, + ], + } as unknown as Project; - t.deepEqual(deletedCollections(merged, remote), [ - { id: 'uuid-remove', name: 'remove-me', delete: true }, - ]); -}); + t.deepEqual(deletedCollections(merged, remote), [ + { id: 'uuid-remove', name: 'remove-me', delete: true }, + ]); + } +); -test('deletedCollections: nothing to delete when every remote name survives', (t) => { - const merged = { - collections: [{ name: 'a' }, { name: 'b' }], - } as unknown as Project; - const remote = { - collections: [ - { uuid: 'uuid-a', name: 'a' }, - { uuid: 'uuid-b', name: 'b' }, - ], - } as unknown as Project; +test.serial( + 'deletedCollections: nothing to delete when every remote name survives', + (t) => { + const merged = { + collections: [{ name: 'a' }, { name: 'b' }], + } as unknown as Project; + const remote = { + collections: [ + { uuid: 'uuid-a', name: 'a' }, + { uuid: 'uuid-b', name: 'b' }, + ], + } as unknown as Project; - t.deepEqual(deletedCollections(merged, remote), []); -}); + t.deepEqual(deletedCollections(merged, remote), []); + } +); const channel = { name: 'my-channel', @@ -1220,16 +1221,19 @@ const channel = { enabled: true, }; -test('channelsChanged: false when there is no local resources.yaml', (t) => { - const local = { channels: undefined } as unknown as Project; - const remote = { - channels: [{ ...channel, id: 'chan-1' }], - } as unknown as Project; - - t.false(channelsChanged(local, remote)); -}); +test.serial( + 'channelsChanged: false when there is no local resources.yaml', + (t) => { + const local = { channels: undefined } as unknown as Project; + const remote = { + channels: [{ ...channel, id: 'chan-1' }], + } as unknown as Project; + + t.false(channelsChanged(local, remote)); + } +); -test('channelsChanged: true when a channel is edited locally', (t) => { +test.serial('channelsChanged: true when a channel is edited locally', (t) => { const local = { channels: [{ ...channel, enabled: false }], } as unknown as Project; @@ -1240,19 +1244,26 @@ test('channelsChanged: true when a channel is edited locally', (t) => { t.true(channelsChanged(local, remote)); }); -test('channelsChanged: false when a credential name matches the remote uuid', (t) => { - const local = { - channels: [{ ...channel, destination_credential_id: 'me@openfn.org|cred' }], - } as unknown as Project; - const remote = { - credentials: [{ uuid: 'cred-uuid', name: 'cred', owner: 'me@openfn.org' }], - channels: [ - { ...channel, id: 'chan-1', destination_credential_id: 'cred-uuid' }, - ], - } as unknown as Project; +test.serial( + 'channelsChanged: false when a credential name matches the remote uuid', + (t) => { + const local = { + channels: [ + { ...channel, destination_credential_id: 'me@openfn.org|cred' }, + ], + } as unknown as Project; + const remote = { + credentials: [ + { uuid: 'cred-uuid', name: 'cred', owner: 'me@openfn.org' }, + ], + channels: [ + { ...channel, id: 'chan-1', destination_credential_id: 'cred-uuid' }, + ], + } as unknown as Project; - t.false(channelsChanged(local, remote)); -}); + t.false(channelsChanged(local, remote)); + } +); const diffChannel = (name: string, extra = {}) => ({ name, @@ -1261,7 +1272,7 @@ const diffChannel = (name: string, extra = {}) => ({ ...extra, }); -test('printRichDiff: should report a channel-only change', (t) => { +test.serial('printRichDiff: should report a channel-only change', (t) => { const local = new Project({ name: 'local', workflows: [], @@ -1276,7 +1287,7 @@ test('printRichDiff: should report a channel-only change', (t) => { t.truthy(logger._find('always', /one: added/)); }); -test('printRichDiff: should report removed channels', (t) => { +test.serial('printRichDiff: should report removed channels', (t) => { const local = new Project({ name: 'local', workflows: [], channels: [] }); const remote = new Project({ name: 'remote', @@ -1289,63 +1300,76 @@ test('printRichDiff: should report removed channels', (t) => { t.truthy(logger._find('always', /one: removed/)); }); -test('printRichDiff: should report added and removed collections', (t) => { - const local = new Project({ - name: 'local', - workflows: [], - collections: [{ name: 'keep-me' }, { name: 'new-one' }], - }); - const remote = new Project({ - name: 'remote', - workflows: [], - collections: [ - { uuid: 'a', name: 'keep-me' }, - { uuid: 'b', name: 'old-one' }, - ], - }); +test.serial( + 'printRichDiff: should report added and removed collections', + (t) => { + const local = new Project({ + name: 'local', + workflows: [], + collections: [{ name: 'keep-me' }, { name: 'new-one' }], + }); + const remote = new Project({ + name: 'remote', + workflows: [], + collections: [ + { uuid: 'a', name: 'keep-me' }, + { uuid: 'b', name: 'old-one' }, + ], + }); - printRichDiff(local, remote, [], logger); + printRichDiff(local, remote, [], logger); - t.truthy(logger._find('always', /following changes to the remote project/)); - t.truthy(logger._find('always', /Collections:/)); - t.truthy(logger._find('always', /new-one: added/)); - t.truthy(logger._find('always', /old-one: removed/)); -}); + t.truthy(logger._find('always', /following changes to the remote project/)); + t.truthy(logger._find('always', /Collections:/)); + t.truthy(logger._find('always', /new-one: added/)); + t.truthy(logger._find('always', /old-one: removed/)); + } +); -test('printRichDiff: should show every collection as removed when all are deleted', (t) => { - const local = new Project({ name: 'local', workflows: [], collections: [] }); - const remote = new Project({ - name: 'remote', - workflows: [], - collections: [ - { uuid: 'a', name: 'one' }, - { uuid: 'b', name: 'two' }, - ], - }); +test.serial( + 'printRichDiff: should show every collection as removed when all are deleted', + (t) => { + const local = new Project({ + name: 'local', + workflows: [], + collections: [], + }); + const remote = new Project({ + name: 'remote', + workflows: [], + collections: [ + { uuid: 'a', name: 'one' }, + { uuid: 'b', name: 'two' }, + ], + }); - printRichDiff(local, remote, [], logger); + printRichDiff(local, remote, [], logger); - t.truthy(logger._find('always', /one: removed/)); - t.truthy(logger._find('always', /two: removed/)); -}); + t.truthy(logger._find('always', /one: removed/)); + t.truthy(logger._find('always', /two: removed/)); + } +); -test('printRichDiff: should not report collections that are not managed locally', (t) => { - const local = new Project({ name: 'local', workflows: [] }); - const remote = new Project({ - name: 'remote', - workflows: [], - collections: [{ uuid: 'a', name: 'one' }], - }); +test.serial( + 'printRichDiff: should not report collections that are not managed locally', + (t) => { + const local = new Project({ name: 'local', workflows: [] }); + const remote = new Project({ + name: 'remote', + workflows: [], + collections: [{ uuid: 'a', name: 'one' }], + }); - // use a private logger: an absence check isn't safe on the shared one - const privateLogger = createMockLogger(undefined, { level: 'debug' }); - printRichDiff(local, remote, [], privateLogger); + // use a private logger: an absence check isn't safe on the shared one + const privateLogger = createMockLogger(undefined, { level: 'debug' }); + printRichDiff(local, remote, [], privateLogger); - t.falsy(privateLogger._find('always', /Collections:/)); - t.truthy(privateLogger._find('info', /No workflow changes detected/)); -}); + t.falsy(privateLogger._find('always', /Collections:/)); + t.truthy(privateLogger._find('info', /No workflow changes detected/)); + } +); -test('printRichDiff: should list changed channel fields', (t) => { +test.serial('printRichDiff: should list changed channel fields', (t) => { const local = new Project({ name: 'local', workflows: [], @@ -1444,7 +1468,6 @@ test.serial( // simulate a fresh git clone: the workflows are there, nothing is checked // out, and there's more than one tracked project - // (fs.rmSync is a no-op under mock-fs, hence rimraf) await rimraf('/ws/.openfn'); t.false(fs.existsSync('/ws/.openfn')); fs.writeFileSync('/ws/.projects/other@localhost.yaml', projectYaml); diff --git a/packages/cli/test/projects/ensure-checkout.test.ts b/packages/cli/test/projects/ensure-checkout.test.ts index e9ead3df6..5dab8f451 100644 --- a/packages/cli/test/projects/ensure-checkout.test.ts +++ b/packages/cli/test/projects/ensure-checkout.test.ts @@ -1,6 +1,6 @@ import test from 'ava'; +import { mockFs, restoreFs } from '@openfn/test-helpers'; import fs from 'node:fs'; -import mock from 'mock-fs'; import { createMockLogger } from '@openfn/logger'; import Project, { Workspace, yamlToJson } from '@openfn/project'; @@ -9,10 +9,11 @@ import ensureCheckout, { getDefaultProject, } from '../../src/projects/ensure-checkout'; +test.afterEach(restoreFs); + const logger = createMockLogger(undefined, { level: 'debug' }); test.afterEach(() => { - mock.restore(); logger._reset(); }); @@ -25,7 +26,7 @@ const projectFile = (id: string) => }).serialize('project', { format: 'yaml' }) as string; const setup = (ids = ['main', 'staging'], extra = {}) => - mock({ + mockFs({ '/ws/openfn.yaml': '', '/ws/workflows/wf/wf.yaml': 'id: wf', ...Object.fromEntries( @@ -258,29 +259,32 @@ test.serial('still prompts if interactive, even if not required', async (t) => { const projects = (aliases: string[]) => aliases.map((alias) => ({ alias })) as unknown as Project[]; -test('getDefaultProject: prefers the alias matching the branch', (t) => { +test.serial('getDefaultProject: prefers the alias matching the branch', (t) => { const result = getDefaultProject(projects(['main', 'staging']), 'staging'); t.is(result.alias, 'staging'); }); -test('getDefaultProject: defaults to main if there is no branch', (t) => { - t.is(getDefaultProject(projects(['a', 'main', 'b'])).alias, 'main'); -}); +test.serial( + 'getDefaultProject: defaults to main if there is no branch', + (t) => { + t.is(getDefaultProject(projects(['a', 'main', 'b'])).alias, 'main'); + } +); -test('getDefaultProject: falls back to the first project', (t) => { +test.serial('getDefaultProject: falls back to the first project', (t) => { t.is(getDefaultProject(projects(['a', 'b']), 'feature').alias, 'a'); }); const described = (props: object) => describeProject(props as Project); -test('describeProject: alias with id and uuid', (t) => { +test.serial('describeProject: alias with id and uuid', (t) => { t.is( described({ alias: 'staging', id: 'my-project', openfn: { uuid: 'abcd' } }), 'staging (my-project | abcd)' ); }); -test('describeProject: copes with a missing uuid or id', (t) => { +test.serial('describeProject: copes with a missing uuid or id', (t) => { t.is( described({ alias: 'staging', id: 'my-project' }), 'staging (my-project)' @@ -291,10 +295,13 @@ test('describeProject: copes with a missing uuid or id', (t) => { ); }); -test('describeProject: just the alias if there is no id or uuid', (t) => { - t.is(described({ alias: 'staging' }), 'staging'); -}); +test.serial( + 'describeProject: just the alias if there is no id or uuid', + (t) => { + t.is(described({ alias: 'staging' }), 'staging'); + } +); -test('describeProject: copes with a missing alias', (t) => { +test.serial('describeProject: copes with a missing alias', (t) => { t.is(described({ id: 'my-project' }), '(no alias) (my-project)'); }); diff --git a/packages/cli/test/projects/fetch.test.ts b/packages/cli/test/projects/fetch.test.ts index b65f7b2de..1227ccbfb 100644 --- a/packages/cli/test/projects/fetch.test.ts +++ b/packages/cli/test/projects/fetch.test.ts @@ -1,12 +1,14 @@ import { readFile, writeFile } from 'node:fs/promises'; import test from 'ava'; -import mock from 'mock-fs'; +import { mockFs, restoreFs } from '@openfn/test-helpers'; import { MockAgent, setGlobalDispatcher } from 'undici'; import { createMockLogger } from '@openfn/logger'; import { handler as fetchHandler } from '../../src/projects/fetch'; import { myProject_v1, myProject_yaml } from './fixtures'; +test.afterEach(restoreFs); + const logger = createMockLogger('', { level: 'debug' }); const ENDPOINT = 'https://app.openfn.org'; @@ -39,17 +41,13 @@ test.before(() => { }); test.beforeEach(() => { - mock({ + mockFs({ '/ws/.projects': {}, '/ws/openfn.yaml': '', }); logger._reset(); }); -test.afterEach(() => { - mock.restore(); -}); - test.serial('fetch by UUID to default new alias', async (t) => { t.throwsAsync(() => readFile(getYamlPath('main'), 'utf-8')); @@ -483,7 +481,8 @@ test.serial( .replace(' - a', ' - z'); // change the local history to be incompatible // Make it look like we've checked out the project - mock({ + restoreFs(); + mockFs({ '/ws/.projects': {}, '/ws/openfn.yaml': '', '/ws/.projects/project@app.openfn.org.yaml': modified, @@ -515,7 +514,8 @@ test.serial('force merge a diverged project', async (t) => { // Change project.yaml const modified = myProject_yaml.replace('fn()', 'fn(x)'); - mock({ + restoreFs(); + mockFs({ '/ws/.projects': {}, '/ws/openfn.yaml': '', '/ws/.projects/project@app.openfn.org.yaml': modified, @@ -544,7 +544,8 @@ test.serial('force merge a diverged project', async (t) => { test.serial( 'fetch by UUID to an output path when local copies share the UUID', async (t) => { - mock({ + restoreFs(); + mockFs({ '/ws/.projects/jam@app.openfn.org.yaml': yaml_v1, '/ws/.projects/jam-backup@app.openfn.org.yaml': yaml_v1, '/ws/openfn.yaml': '', @@ -570,7 +571,8 @@ test.serial( test.serial( 'error: fetch by UUID to an output path without an endpoint when local copies share the UUID', async (t) => { - mock({ + restoreFs(); + mockFs({ '/ws/.projects/jam@app.openfn.org.yaml': yaml_v1, '/ws/.projects/jam-backup@app.openfn.org.yaml': yaml_v1, '/ws/openfn.yaml': '', @@ -596,7 +598,8 @@ test.serial( test.serial( 'error: fetch by UUID without an output path when local copies share the UUID', async (t) => { - mock({ + restoreFs(); + mockFs({ '/ws/.projects/jam@app.openfn.org.yaml': yaml_v1, '/ws/.projects/jam-backup@app.openfn.org.yaml': yaml_v1, '/ws/openfn.yaml': '', @@ -620,7 +623,8 @@ test.serial( ); test.serial('fetch by alias when local copies share the UUID', async (t) => { - mock({ + restoreFs(); + mockFs({ '/ws/.projects/jam@app.openfn.org.yaml': yaml_v1, '/ws/.projects/jam-backup@app.openfn.org.yaml': yaml_v1, '/ws/openfn.yaml': '', diff --git a/packages/cli/test/projects/list.test.ts b/packages/cli/test/projects/list.test.ts index 2a23e4a90..d6d4da853 100644 --- a/packages/cli/test/projects/list.test.ts +++ b/packages/cli/test/projects/list.test.ts @@ -1,9 +1,11 @@ import test from 'ava'; +import { mockFs, restoreFs } from '@openfn/test-helpers'; import { handler as list } from '../../src/projects/list'; import { createMockLogger } from '@openfn/logger'; -import mock from 'mock-fs'; import { jsonToYaml } from '@openfn/project'; +test.afterEach(restoreFs); + // a workspace with several projects but no checkout metadata const noCheckoutProject = jsonToYaml({ id: 'my-project', workflows: [] }); @@ -136,11 +138,11 @@ files['/aliased/workflows/sw/simple-workflow.yaml'] = jsonToYaml({ steps: [], }); -mock(files); +test.beforeEach(() => mockFs(files)); const logger = createMockLogger('', { level: 'debug' }); -test('throw for invalid workspace directory', async (t) => { +test.serial('throw for invalid workspace directory', async (t) => { await t.throwsAsync( () => list({ command: 'projects', workspace: '/invalid' }, logger), { @@ -151,7 +153,7 @@ test('throw for invalid workspace directory', async (t) => { // t.is(message, 'Command was run in an invalid openfn workspace'); }); -test('throw if dir is not a workspace', async (t) => { +test.serial('throw if dir is not a workspace', async (t) => { await t.throwsAsync( () => list({ command: 'projects', workspace: '/no-ws' }, logger), { @@ -160,7 +162,7 @@ test('throw if dir is not a workspace', async (t) => { ); }); -test('valid workspace', async (t) => { +test.serial('valid workspace', async (t) => { await list({ command: 'projects', workspace: '/ws' }, logger); const { message } = logger._find('always', /available openfn projects/i); @@ -182,7 +184,7 @@ staging | my-project ); }); -test('shows workflow aliases on the active project', async (t) => { +test.serial('shows workflow aliases on the active project', async (t) => { const privateLogger = createMockLogger('', { level: 'debug' }); await list({ command: 'projects', workspace: '/aliased' }, privateLogger); diff --git a/packages/cli/test/projects/merge.test.ts b/packages/cli/test/projects/merge.test.ts index 94aed583a..f90dd3fe1 100644 --- a/packages/cli/test/projects/merge.test.ts +++ b/packages/cli/test/projects/merge.test.ts @@ -1,8 +1,11 @@ import Project, { jsonToYaml, Workspace } from '@openfn/project'; import test from 'ava'; -import mock from 'mock-fs'; +import { mockFs, restoreFs } from '@openfn/test-helpers'; import { handler as mergeHandler } from '../../src/projects/merge'; import { createMockLogger } from '@openfn/logger'; +import { handler as checkoutHandler } from '../../src/projects/checkout'; + +test.afterEach(restoreFs); const sandbox = { id: '', @@ -78,7 +81,7 @@ const main = { }; test.beforeEach(() => { - mock({ + mockFs({ '/ws/workflows': {}, '/ws/openfn.yaml': jsonToYaml({ dirs: { workflows: 'workflows' }, @@ -95,6 +98,17 @@ test.beforeEach(() => { const logger = createMockLogger('', { level: 'debug' }); +test.serial('checkout finds the project workflows on disk', async (t) => { + await checkoutHandler( + { command: 'project-checkout', project: 'my-sandbox', workspace: '/ws' }, + logger + ); + + const project = await Project.from('fs', { root: '/ws' }); + t.is(project.workflows.length, 1); + t.is(project.workflows[0].steps.length, 3); +}); + test.serial('merging into the same project', async (t) => { await mergeHandler( { @@ -204,7 +218,8 @@ test.serial( ); test.serial('Write to JSON using project config', async (t) => { - mock({ + restoreFs(); + mockFs({ '/ws/openfn.yaml': jsonToYaml({ dirs: { workflows: 'workflows' }, formats: { openfn: 'yaml', project: 'json', workflow: 'yaml' }, @@ -247,7 +262,8 @@ test.serial('Write to JSON using project config', async (t) => { }); test.serial('merge with custom base', async (t) => { - mock({ + restoreFs(); + mockFs({ '/ws/openfn.yaml': jsonToYaml({ dirs: { workflows: 'workflows' }, formats: { openfn: 'yaml', project: 'yaml', workflow: 'yaml' }, @@ -334,7 +350,8 @@ const multiMain = { }; const mockMultiWorkflowWorkspace = () => { - mock({ + restoreFs(); + mockFs({ '/ws/workflows': {}, '/ws/openfn.yaml': jsonToYaml({ dirs: { workflows: 'workflows' }, diff --git a/packages/cli/test/projects/util.test.ts b/packages/cli/test/projects/util.test.ts index d9679494f..6e97957e6 100644 --- a/packages/cli/test/projects/util.test.ts +++ b/packages/cli/test/projects/util.test.ts @@ -1,6 +1,6 @@ import test from 'ava'; +import { mockFs, restoreFs } from '@openfn/test-helpers'; import fs from 'node:fs'; -import mock from 'mock-fs'; import Project, { generateWorkflow, yamlToJson } from '@openfn/project'; import { findLocallyChangedWorkflows, @@ -8,72 +8,91 @@ import { writeCheckoutFile, } from '../../src/projects/util'; -test.afterEach(() => { - mock.restore(); -}); +test.afterEach(restoreFs); -test('tidyWorkflowDir: removes workflows that no longer exist', async (t) => { - const currentProject = new Project({ - name: 'current', - workflows: [ - generateWorkflow('@id A trigger-x'), - generateWorkflow('@id B trigger-y'), - ], - }); +test.serial( + 'tidyWorkflowDir: removes workflows that no longer exist', + async (t) => { + const currentProject = new Project({ + name: 'current', + workflows: [ + generateWorkflow('@id A trigger-x'), + generateWorkflow('@id B trigger-y'), + ], + }); - const incomingProject = new Project({ - name: 'incoming', - workflows: [ - generateWorkflow('@id A trigger-x'), - generateWorkflow('@id D trigger-w'), - ], - }); + const incomingProject = new Project({ + name: 'incoming', + workflows: [ + generateWorkflow('@id A trigger-x'), + generateWorkflow('@id D trigger-w'), + ], + }); - const toRemove = await tidyWorkflowDir(currentProject, incomingProject, true); + const toRemove = await tidyWorkflowDir( + currentProject, + incomingProject, + true + ); - t.deepEqual(toRemove, ['workflows/B/B.yaml']); -}); + t.deepEqual(toRemove, ['workflows/B/B.yaml']); + } +); -test('tidyWorkflowDir: do nothing when no workflows are removed', async (t) => { - const currentProject = new Project({ - name: 'current', - workflows: [generateWorkflow('@id A trigger-x')], - }); +test.serial( + 'tidyWorkflowDir: do nothing when no workflows are removed', + async (t) => { + const currentProject = new Project({ + name: 'current', + workflows: [generateWorkflow('@id A trigger-x')], + }); - const incomingProject = new Project({ - name: 'incoming', - workflows: [ - generateWorkflow('@id A trigger-x'), - generateWorkflow('@id B trigger-y'), - ], - }); + const incomingProject = new Project({ + name: 'incoming', + workflows: [ + generateWorkflow('@id A trigger-x'), + generateWorkflow('@id B trigger-y'), + ], + }); - const toRemove = await tidyWorkflowDir(currentProject, incomingProject, true); + const toRemove = await tidyWorkflowDir( + currentProject, + incomingProject, + true + ); - t.deepEqual(toRemove, []); -}); + t.deepEqual(toRemove, []); + } +); -test('tidyWorkflowDir: removes all workflows when incoming project is empty', async (t) => { - const currentProject = new Project({ - name: 'current', - workflows: [ - generateWorkflow('@id A trigger-x'), - generateWorkflow('@id B trigger-y'), - ], - }); +test.serial( + 'tidyWorkflowDir: removes all workflows when incoming project is empty', + async (t) => { + const currentProject = new Project({ + name: 'current', + workflows: [ + generateWorkflow('@id A trigger-x'), + generateWorkflow('@id B trigger-y'), + ], + }); - const incomingProject = new Project({ - name: 'incoming', - workflows: [], - }); + const incomingProject = new Project({ + name: 'incoming', + workflows: [], + }); - const toRemove = await tidyWorkflowDir(currentProject, incomingProject, true); + const toRemove = await tidyWorkflowDir( + currentProject, + incomingProject, + true + ); - // All workflows should be removed - t.deepEqual(toRemove, ['workflows/A/A.yaml', 'workflows/B/B.yaml']); -}); + // All workflows should be removed + t.deepEqual(toRemove, ['workflows/A/A.yaml', 'workflows/B/B.yaml']); + } +); -test('tidyWorkflowDir: both projects empty', async (t) => { +test.serial('tidyWorkflowDir: both projects empty', async (t) => { const currentProject = new Project({ name: 'current', workflows: [], @@ -89,7 +108,7 @@ test('tidyWorkflowDir: both projects empty', async (t) => { t.deepEqual(toRemove, []); }); -test('tidyWorkflowDir: identical projects', async (t) => { +test.serial('tidyWorkflowDir: identical projects', async (t) => { const currentProject = new Project({ name: 'current', workflows: [ @@ -111,29 +130,36 @@ test('tidyWorkflowDir: identical projects', async (t) => { t.deepEqual(toRemove, []); }); -test('tidyWorkflowDir: complete replacement with no overlap', async (t) => { - const currentProject = new Project({ - name: 'current', - workflows: [ - generateWorkflow('@id A trigger-x'), - generateWorkflow('@id B trigger-y'), - ], - }); +test.serial( + 'tidyWorkflowDir: complete replacement with no overlap', + async (t) => { + const currentProject = new Project({ + name: 'current', + workflows: [ + generateWorkflow('@id A trigger-x'), + generateWorkflow('@id B trigger-y'), + ], + }); - const incomingProject = new Project({ - name: 'incoming', - workflows: [ - generateWorkflow('@id X trigger-x'), - generateWorkflow('@id Y trigger-y'), - ], - }); + const incomingProject = new Project({ + name: 'incoming', + workflows: [ + generateWorkflow('@id X trigger-x'), + generateWorkflow('@id Y trigger-y'), + ], + }); - const toRemove = await tidyWorkflowDir(currentProject, incomingProject, true); + const toRemove = await tidyWorkflowDir( + currentProject, + incomingProject, + true + ); - t.deepEqual(toRemove, ['workflows/A/A.yaml', 'workflows/B/B.yaml']); -}); + t.deepEqual(toRemove, ['workflows/A/A.yaml', 'workflows/B/B.yaml']); + } +); -test('tidyWorkflowDir: handles undefined projects', async (t) => { +test.serial('tidyWorkflowDir: handles undefined projects', async (t) => { const project = new Project({ name: 'project', workflows: [generateWorkflow('@id A trigger-x')], @@ -152,23 +178,30 @@ test('tidyWorkflowDir: handles undefined projects', async (t) => { t.deepEqual(toRemove, []); }); -test('tidyWorkflowDir: removes expression files when workflow steps change', async (t) => { - const currentProject = new Project({ - name: 'current', - workflows: [generateWorkflow('@id A trigger-x(expression=fn)')], - }); +test.serial( + 'tidyWorkflowDir: removes expression files when workflow steps change', + async (t) => { + const currentProject = new Project({ + name: 'current', + workflows: [generateWorkflow('@id A trigger-x(expression=fn)')], + }); - const incomingProject = new Project({ - name: 'incoming', - workflows: [generateWorkflow('@id A trigger-z(expression=fn)')], - }); + const incomingProject = new Project({ + name: 'incoming', + workflows: [generateWorkflow('@id A trigger-z(expression=fn)')], + }); - const toRemove = await tidyWorkflowDir(currentProject, incomingProject, true); + const toRemove = await tidyWorkflowDir( + currentProject, + incomingProject, + true + ); - t.deepEqual(toRemove, ['workflows/A/x.js']); -}); + t.deepEqual(toRemove, ['workflows/A/x.js']); + } +); -test('findLocallyChangedWorkflows: no changed workflows', async (t) => { +test.serial('findLocallyChangedWorkflows: no changed workflows', async (t) => { const wf1 = generateWorkflow('@id a trigger-x'); const wf2 = generateWorkflow('@id b trigger-y'); @@ -194,90 +227,102 @@ test('findLocallyChangedWorkflows: no changed workflows', async (t) => { t.deepEqual(changed, []); }); -test('findLocallyChangedWorkflows: all workflows changed if there is no forked_from', async (t) => { - const wf1 = generateWorkflow('@id a trigger-x'); - const wf2 = generateWorkflow('@id b trigger-y'); - - const project = new Project({ - name: 'test', - workflows: [wf1, wf2], - }); +test.serial( + 'findLocallyChangedWorkflows: all workflows changed if there is no forked_from', + async (t) => { + const wf1 = generateWorkflow('@id a trigger-x'); + const wf2 = generateWorkflow('@id b trigger-y'); - // Create a mock workspace with NO forked_from - const workspace = { - activeProject: {}, - } as any; + const project = new Project({ + name: 'test', + workflows: [wf1, wf2], + }); - const changed = await findLocallyChangedWorkflows(workspace, project); - t.deepEqual(changed, ['a', 'b']); -}); + // Create a mock workspace with NO forked_from + const workspace = { + activeProject: {}, + } as any; -test('findLocallyChangedWorkflows: detect 1 locally changed workflow', async (t) => { - const wf1 = generateWorkflow('@id a trigger-x'); - const wf2 = generateWorkflow('@id b trigger-z'); + const changed = await findLocallyChangedWorkflows(workspace, project); + t.deepEqual(changed, ['a', 'b']); + } +); - const workspace = { - activeProject: { - forked_from: { - a: wf1.getVersionHash(), - b: wf2.getVersionHash(), +test.serial( + 'findLocallyChangedWorkflows: detect 1 locally changed workflow', + async (t) => { + const wf1 = generateWorkflow('@id a trigger-x'); + const wf2 = generateWorkflow('@id b trigger-z'); + + const workspace = { + activeProject: { + forked_from: { + a: wf1.getVersionHash(), + b: wf2.getVersionHash(), + }, }, - }, - } as any; - - const project = new Project({ - name: 'test', - workflows: [wf1, wf2], - }); + } as any; - project.workflows[0].name = 'changed'; + const project = new Project({ + name: 'test', + workflows: [wf1, wf2], + }); - const changed = await findLocallyChangedWorkflows(workspace, project); - t.deepEqual(changed, ['a']); -}); + project.workflows[0].name = 'changed'; -test('findLocallyChangedWorkflows: detect 1 locally added workflow', async (t) => { - const wf1 = generateWorkflow('@id a trigger-x'); - const wf2 = generateWorkflow('@id b trigger-y'); + const changed = await findLocallyChangedWorkflows(workspace, project); + t.deepEqual(changed, ['a']); + } +); - const workspace = { - activeProject: { - forked_from: { - a: wf1.getVersionHash(), +test.serial( + 'findLocallyChangedWorkflows: detect 1 locally added workflow', + async (t) => { + const wf1 = generateWorkflow('@id a trigger-x'); + const wf2 = generateWorkflow('@id b trigger-y'); + + const workspace = { + activeProject: { + forked_from: { + a: wf1.getVersionHash(), + }, }, - }, - } as any; - - const project = new Project({ - name: 'test', - workflows: [wf1, wf2], - }); + } as any; - const changed = await findLocallyChangedWorkflows(workspace, project); - t.deepEqual(changed, ['b']); -}); + const project = new Project({ + name: 'test', + workflows: [wf1, wf2], + }); -test('findLocallyChangedWorkflows: detect 1 locally removed workflow', async (t) => { - const wf1 = generateWorkflow('@id a trigger-x'); - const wf2 = generateWorkflow('@id b trigger-y'); + const changed = await findLocallyChangedWorkflows(workspace, project); + t.deepEqual(changed, ['b']); + } +); - const workspace = { - activeProject: { - forked_from: { - a: wf1.getVersionHash(), - b: wf2.getVersionHash(), +test.serial( + 'findLocallyChangedWorkflows: detect 1 locally removed workflow', + async (t) => { + const wf1 = generateWorkflow('@id a trigger-x'); + const wf2 = generateWorkflow('@id b trigger-y'); + + const workspace = { + activeProject: { + forked_from: { + a: wf1.getVersionHash(), + b: wf2.getVersionHash(), + }, }, - }, - } as any; + } as any; - const project = new Project({ - name: 'test', - workflows: [wf1], - }); + const project = new Project({ + name: 'test', + workflows: [wf1], + }); - const changed = await findLocallyChangedWorkflows(workspace, project); - t.deepEqual(changed, ['b']); -}); + const changed = await findLocallyChangedWorkflows(workspace, project); + t.deepEqual(changed, ['b']); + } +); const trackedProject = () => new Project({ @@ -292,7 +337,7 @@ const trackedProject = () => test.serial( 'writeCheckoutFile: writes the project meta and forked_from to the checkout file', async (t) => { - mock({ '/ws': {} }); + mockFs({ '/ws': {} }); const filePath = await writeCheckoutFile('/ws', trackedProject()); @@ -311,7 +356,7 @@ test.serial( test.serial( 'writeCheckoutFile: writes to the checkout file for a branch', async (t) => { - mock({ '/ws': {} }); + mockFs({ '/ws': {} }); const filePath = await writeCheckoutFile('/ws', trackedProject(), 'dev'); @@ -324,7 +369,7 @@ test.serial( test.serial( 'writeCheckoutFile: does not touch the workspace config or workflows', async (t) => { - mock({ + mockFs({ '/ws/openfn.yaml': 'credentials: creds.yaml', '/ws/workflows/wf/wf.yaml': 'id: wf', }); diff --git a/packages/cli/test/pull/handler.test.ts b/packages/cli/test/pull/handler.test.ts index e4761b0b9..909b7ab9d 100644 --- a/packages/cli/test/pull/handler.test.ts +++ b/packages/cli/test/pull/handler.test.ts @@ -1,5 +1,5 @@ import test from 'ava'; -import mockfs from 'mock-fs'; +import { mockFs, restoreFs } from '@openfn/test-helpers'; import fs from 'node:fs'; import { MockAgent, setGlobalDispatcher } from 'undici'; import { createMockLogger } from '@openfn/logger'; @@ -9,19 +9,15 @@ import { PullOptions } from '../../src/pull/command'; import { Workspace } from '@openfn/project'; import { myProject_v1 } from '../projects/fixtures'; +test.afterEach(restoreFs); + const ENDPOINT = 'https://fake.test.openfn.org'; const PROJECT_UUID = 'e16c5f09-f0cb-4ba7-a4c2-73fcb2f29d00'; test.beforeEach(() => { - mockfs.restore(); - process.env.OPENFN_API_KEY = 'fake-api-key'; }); -test.afterEach(() => { - mockfs.restore(); -}); - let mockAgent = new MockAgent(); mockAgent.disableNetConnect(); setGlobalDispatcher(mockAgent); @@ -53,7 +49,7 @@ test.serial( 'redirects to beta handler when openfn.yaml exists in cwd', async (t) => { const logger = createMockLogger('', { level: 'debug' }); - mockfs({ + mockFs({ ['/tmp/config.json']: `{"apiKey": "123", "endpoint": "${ENDPOINT}"}`, ['/tmp/openfn.yaml']: ` project: @@ -72,7 +68,7 @@ test.serial( 'openfn yaml endpoint preferred to config.json endpoint', async (t) => { const logger = createMockLogger('', { level: 'debug' }); - mockfs({ + mockFs({ ['/tmp/config.json']: `{"apiKey": "123", "endpoint": "DOES_NOT_EXIST"}`, ['/tmp/openfn.yaml']: ` project: @@ -90,7 +86,7 @@ test.serial( 'CLI endpoint preferred to openfn.yaml and config.json endpoints', async (t) => { const logger = createMockLogger('', { level: 'debug' }); - mockfs({ + mockFs({ ['/tmp/config.json']: `{"apiKey": "123", "endpoint": "DOES_NOT_EXIST"}`, ['/tmp/openfn.yaml']: ` project: @@ -112,7 +108,7 @@ project: test.serial('does not create credentials.yaml when redirecting', async (t) => { const logger = createMockLogger('', { level: 'debug' }); - mockfs({ + mockFs({ ['/tmp/config.json']: `{"apiKey": "123", "endpoint": "${ENDPOINT}"}`, ['/tmp/openfn.yaml']: ` project: @@ -128,7 +124,7 @@ test.serial( 'tracks the pulled project in the workspace when redirecting with legacy config.yaml structure', async (t) => { const logger = createMockLogger('', { level: 'debug' }); - mockfs({ + mockFs({ ['/tmp/openfn.yaml']: ` project: endpoint: ${ENDPOINT}`, @@ -150,7 +146,7 @@ test.serial( 'tracks the pulled project in the workspace when redirecting with new checkout.yaml file', async (t) => { const logger = createMockLogger('', { level: 'debug' }); - mockfs({ + mockFs({ ['/tmp/.openfn/checkout.yaml']: ` endpoint: ${ENDPOINT}`, ['/tmp/openfn.yaml']: '', diff --git a/packages/cli/test/util.ts b/packages/cli/test/util.ts index 84312058c..935bbd3c5 100644 --- a/packages/cli/test/util.ts +++ b/packages/cli/test/util.ts @@ -1,28 +1,8 @@ /* * test utils */ -import mock from 'mock-fs'; -import path from 'node:path'; - import type { ExecutionPlan, Job, StepEdge } from '@openfn/runtime'; -export const mockFs = (files: Record) => { - // We have to explicitly expose some modules paths so that dependencies can run in the tests - const pnpm = path.resolve('../../node_modules/.pnpm'); - const recastPath = `${pnpm}/recast@0.21.5`; - const sourceMapPath = `${pnpm}/source-map@0.7.6`; - mock({ - [recastPath]: mock.load(recastPath, {}), - [sourceMapPath]: mock.load(sourceMapPath, {}), - '/repo/': mock.load(path.resolve('test/__repo__/'), {}), - ...files, - }); -}; - -export const resetMockFs = () => { - mock.restore(); -}; - type CreateWorkflowOptions = { id?: string; }; diff --git a/packages/cli/test/util/file-exists.test.ts b/packages/cli/test/util/file-exists.test.ts index a6c8ce7b4..2c9385189 100644 --- a/packages/cli/test/util/file-exists.test.ts +++ b/packages/cli/test/util/file-exists.test.ts @@ -1,22 +1,20 @@ import test from 'ava'; -import mockfs from 'mock-fs'; +import { mockFs, restoreFs } from '@openfn/test-helpers'; import { fileExists } from '../../src/util/file-exists'; -test.afterEach(() => { - mockfs.restore(); -}); +test.afterEach(restoreFs); -test('returns true for an existing file', async (t) => { - mockfs({ './test.txt': 'content' }); +test.serial('returns true for an existing file', async (t) => { + mockFs({ './test.txt': 'content' }); t.true(await fileExists('./test.txt')); }); -test('returns false for a non-existent path', async (t) => { - mockfs({}); +test.serial('returns false for a non-existent path', async (t) => { + mockFs({}); t.false(await fileExists('./nonexistent.txt')); }); -test('returns false for a directory', async (t) => { - mockfs({ './mydir': {} }); +test.serial('returns false for a directory', async (t) => { + mockFs({ './mydir': {} }); t.false(await fileExists('./mydir')); }); diff --git a/packages/cli/test/util/load-plan.test.ts b/packages/cli/test/util/load-plan.test.ts index bc02812cc..a7fcb76bf 100644 --- a/packages/cli/test/util/load-plan.test.ts +++ b/packages/cli/test/util/load-plan.test.ts @@ -1,5 +1,5 @@ import test from 'ava'; -import mock from 'mock-fs'; +import { mockFs, restoreFs } from '@openfn/test-helpers'; import { createMockLogger } from '@openfn/logger'; import { omit } from 'lodash-es'; import type { Job } from '@openfn/runtime'; @@ -7,6 +7,8 @@ import type { Job } from '@openfn/runtime'; import loadPlan from '../../src/util/load-plan'; import { Opts } from '../../src/options'; +test.afterEach(restoreFs); + const logger = createMockLogger(undefined, { level: 'debug' }); const sampleXPlan = { @@ -27,7 +29,7 @@ const createPlan = (steps: Partial[] = []) => ({ }); test.beforeEach(() => { - mock({ + mockFs({ 'test/job.js': 'x', 'test/collections.js': 'collections.get()', 'test/wf-old.json': JSON.stringify({ @@ -42,7 +44,6 @@ test.beforeEach(() => { test.afterEach(() => { logger._reset(); - mock.restore(); }); // TODO: add some tests for handling yaml stuff @@ -205,7 +206,8 @@ test.serial('xplan: expand adaptors', async (t) => { }, ]); - mock({ + restoreFs(); + mockFs({ 'test/wf.json': JSON.stringify(plan), }); @@ -235,7 +237,8 @@ test.serial('flat xplan: expand adaptors', async (t) => { }, ], }; - mock({ + restoreFs(); + mockFs({ 'test/wf.json': JSON.stringify(plan), }); @@ -263,7 +266,8 @@ test.serial('xplan: do not expand adaptors', async (t) => { }, ]); - mock({ + restoreFs(); + mockFs({ 'test/wf.json': JSON.stringify(plan), }); @@ -293,7 +297,8 @@ test.serial('xplan: set timeout from CLI', async (t) => { // @ts-ignore plan.options.timeout = 1; - mock({ + restoreFs(); + mockFs({ 'test/wf.json': JSON.stringify(plan), }); @@ -318,7 +323,8 @@ test.serial('xplan: set start from CLI', async (t) => { // @ts-ignore plan.options.start = 'a'; - mock({ + restoreFs(); + mockFs({ 'test/wf.json': JSON.stringify(plan), }); @@ -342,7 +348,8 @@ test.serial('xplan: map to monorepo', async (t) => { }, ]); - mock({ + restoreFs(); + mockFs({ 'test/wf.json': JSON.stringify(plan), '/repo/packages/common': {}, }); @@ -388,7 +395,8 @@ test.serial('step: allow file paths for state', async (t) => { }, ]); - mock({ + restoreFs(); + mockFs({ 'test/state.json': JSON.stringify({ data: { x: 1, @@ -422,7 +430,8 @@ test.serial('xplan: support multiple adaptors', async (t) => { }, ]); - mock({ + restoreFs(); + mockFs({ 'test/wf.json': JSON.stringify(plan), }); @@ -454,7 +463,8 @@ test.serial('xplan: append collections', async (t) => { }, ]); - mock({ + restoreFs(); + mockFs({ 'test/wf.json': JSON.stringify(plan), }); @@ -496,7 +506,8 @@ test.serial( }, ]); - mock({ + restoreFs(); + mockFs({ 'test/wf.json': JSON.stringify(plan), }); @@ -520,7 +531,8 @@ test.serial( test.serial( 'xplan: load a workflow.yaml without top workflow key', async (t) => { - mock({ + restoreFs(); + mockFs({ 'test/wf.yaml': ` start: a steps: @@ -547,7 +559,8 @@ steps: test.serial( 'xplan: load a workflow.yaml without top workflow key and options', async (t) => { - mock({ + restoreFs(); + mockFs({ 'test/wf.yaml': ` name: wf steps: @@ -574,7 +587,8 @@ options: ); test.serial('xplan: load a workflow.yaml with top workflow key', async (t) => { - mock({ + restoreFs(); + mockFs({ 'test/wf.yaml': ` workflow: name: wf @@ -597,7 +611,8 @@ options: }); test.serial('xplan: load a workflow through a Workspace', async (t) => { - mock({ + restoreFs(); + mockFs({ '/tmp/workflows/wf.yaml': ` id: wf steps: @@ -629,7 +644,8 @@ dirs: }); test.serial('xplan: throw if a named workflow does not exist', async (t) => { - mock({ + restoreFs(); + mockFs({ '/tmp/workflows/wf.yaml': ` id: wf steps: @@ -655,7 +671,8 @@ dirs: test.serial( 'xplan: load a workflow through a project .yaml and apply the credentials map by default', async (t) => { - mock({ + restoreFs(); + mockFs({ '/tmp/workflows/wf.yaml': ` id: wf steps: diff --git a/packages/cli/test/util/map-adaptors-to-monorepo.test.ts b/packages/cli/test/util/map-adaptors-to-monorepo.test.ts index 83218a7a6..7c8f872e0 100644 --- a/packages/cli/test/util/map-adaptors-to-monorepo.test.ts +++ b/packages/cli/test/util/map-adaptors-to-monorepo.test.ts @@ -1,6 +1,6 @@ import path from 'node:path'; import test from 'ava'; -import mock from 'mock-fs'; +import { mockFs, restoreFs } from '@openfn/test-helpers'; import { createMockLogger } from '@openfn/logger'; import mapAdaptorsToMonorepo, { @@ -9,6 +9,8 @@ import mapAdaptorsToMonorepo, { } from '../../src/util/map-adaptors-to-monorepo'; import { ExecutionPlan } from '@openfn/runtime'; +test.afterEach(restoreFs); + // Paths are resolved to absolute in the option's ensure block, so the util // always receives absolute roots const REPO_PATH = path.resolve('a/b/c'); @@ -18,11 +20,10 @@ const logger = createMockLogger(); test.afterEach(() => { logger._reset(); - mock.restore(); }); test.serial('updatePath: common', (t) => { - mock({ + mockFs({ [`${REPO_PATH}/packages/common`]: {}, }); @@ -32,7 +33,7 @@ test.serial('updatePath: common', (t) => { }); test.serial('updatePath: @openfn/language-common', (t) => { - mock({ + mockFs({ [`${REPO_PATH}/packages/common`]: {}, }); @@ -42,7 +43,7 @@ test.serial('updatePath: @openfn/language-common', (t) => { }); test.serial('updatePath: common@1.2.3 (with warning)', (t) => { - mock({ + mockFs({ [`${REPO_PATH}/packages/common`]: {}, }); @@ -55,14 +56,14 @@ test.serial('updatePath: common@1.2.3 (with warning)', (t) => { t.regex(message as string, /ignoring version specifier/i); }); -test('updatePath: common=x/y/z', (t) => { +test.serial('updatePath: common=x/y/z', (t) => { const result = updatePath('common=x/y/z', [REPO_PATH], logger); t.is(result, `common=x/y/z`); }); test.serial('updatePath: prefer the root which has the adaptor', (t) => { - mock({ + mockFs({ [`${REPO_PATH_2}/packages/common`]: {}, }); @@ -73,7 +74,7 @@ test.serial('updatePath: prefer the root which has the adaptor', (t) => { }); test.serial('updatePath: earlier root wins when both have the adaptor', (t) => { - mock({ + mockFs({ [`${REPO_PATH}/packages/common`]: {}, [`${REPO_PATH_2}/packages/common`]: {}, }); @@ -84,7 +85,7 @@ test.serial('updatePath: earlier root wins when both have the adaptor', (t) => { }); test.serial('updatePath: throw if not found in the single root', (t) => { - mock({ + mockFs({ [`${REPO_PATH}/packages`]: {}, }); @@ -94,7 +95,7 @@ test.serial('updatePath: throw if not found in the single root', (t) => { }); test.serial('updatePath: throw if not found in any root', (t) => { - mock({ + mockFs({ [`${REPO_PATH}/packages`]: {}, [`${REPO_PATH_2}/packages`]: {}, }); @@ -106,7 +107,7 @@ test.serial('updatePath: throw if not found in any root', (t) => { // TODO can't test this in ava, have to use an integration test test.skip('validate monorepo: log and exit early if repo not found', async (t) => { - mock({ + mockFs({ a: {}, }); @@ -119,7 +120,7 @@ test.skip('validate monorepo: log and exit early if repo not found', async (t) = }); test.serial('validate monorepo: all OK', async (t) => { - mock({ + mockFs({ [`${REPO_PATH}/packages`]: {}, }); @@ -127,7 +128,7 @@ test.serial('validate monorepo: all OK', async (t) => { }); test.serial('validate monorepo: all OK with multiple paths', async (t) => { - mock({ + mockFs({ [`${REPO_PATH}/packages`]: {}, [`${REPO_PATH_2}/packages`]: {}, }); @@ -138,7 +139,7 @@ test.serial('validate monorepo: all OK with multiple paths', async (t) => { }); test.serial('mapAdaptorsToMonorepo: map adaptors', async (t) => { - mock({ + mockFs({ [`${REPO_PATH}/packages/common`]: {}, }); @@ -149,7 +150,7 @@ test.serial('mapAdaptorsToMonorepo: map adaptors', async (t) => { test.serial( 'mapAdaptorsToMonorepo: map adaptors across multiple roots', async (t) => { - mock({ + mockFs({ [`${REPO_PATH}/packages/http`]: {}, [`${REPO_PATH_2}/packages/common`]: {}, }); @@ -167,7 +168,7 @@ test.serial( ); test.serial('mapAdaptorsToMonorepo: map workflow', async (t) => { - mock({ + mockFs({ [`${REPO_PATH}/packages/common`]: {}, }); diff --git a/packages/cli/test/util/print-versions.test.ts b/packages/cli/test/util/print-versions.test.ts index 0ad35dd55..1a7a8dee3 100644 --- a/packages/cli/test/util/print-versions.test.ts +++ b/packages/cli/test/util/print-versions.test.ts @@ -1,12 +1,14 @@ import { createMockLogger, JSONLog } from '@openfn/logger'; import test from 'ava'; -import mock from 'mock-fs'; +import { loadPath, mockFs, restoreFs } from '@openfn/test-helpers'; import path from 'node:path'; import printVersions from '../../src/util/print-versions'; +test.afterEach(restoreFs); + const root = path.resolve('package.json'); -test('print versions for node and cli', async (t) => { +test.serial('print versions for node and cli', async (t) => { const logger = createMockLogger('', { level: 'info' }); await printVersions(logger); @@ -23,7 +25,7 @@ test('print versions for node and cli', async (t) => { t.notRegex(message, /runtime/); }); -test('print versions for node, cli and adaptor', async (t) => { +test.serial('print versions for node, cli and adaptor', async (t) => { const logger = createMockLogger('', { level: 'info' }); await printVersions(logger, { adaptors: ['http'] }); @@ -36,35 +38,41 @@ test('print versions for node, cli and adaptor', async (t) => { t.regex(message, /http .+ latest/); }); -test('print versions for node, cli, components and adaptor', async (t) => { - const logger = createMockLogger('', { level: 'info' }); - await printVersions(logger, { adaptors: ['http'] }, true); - - const last = logger._parse(logger._last); - const message = last.message as string; - - t.regex(message, /Versions:/); - t.regex(message, /cli/); - t.regex(message, /node/); - t.regex(message, /runtime/); - t.regex(message, /node/); - t.regex(message, /http .+ latest/); -}); - -test('print versions for node, cli and adaptor with version', async (t) => { - const logger = createMockLogger('', { level: 'info' }); - await printVersions(logger, { adaptors: ['http@1234'] }); - - const last = logger._parse(logger._last); - const message = last.message as string; - - t.regex(message, /Versions:/); - t.regex(message, /cli/); - t.regex(message, /node/); - t.regex(message, /http .+ 1234/); -}); - -test('print versions for node, cli and long-form adaptor', async (t) => { +test.serial( + 'print versions for node, cli, components and adaptor', + async (t) => { + const logger = createMockLogger('', { level: 'info' }); + await printVersions(logger, { adaptors: ['http'] }, true); + + const last = logger._parse(logger._last); + const message = last.message as string; + + t.regex(message, /Versions:/); + t.regex(message, /cli/); + t.regex(message, /node/); + t.regex(message, /runtime/); + t.regex(message, /node/); + t.regex(message, /http .+ latest/); + } +); + +test.serial( + 'print versions for node, cli and adaptor with version', + async (t) => { + const logger = createMockLogger('', { level: 'info' }); + await printVersions(logger, { adaptors: ['http@1234'] }); + + const last = logger._parse(logger._last); + const message = last.message as string; + + t.regex(message, /Versions:/); + t.regex(message, /cli/); + t.regex(message, /node/); + t.regex(message, /http .+ 1234/); + } +); + +test.serial('print versions for node, cli and long-form adaptor', async (t) => { const logger = createMockLogger('', { level: 'info' }); await printVersions(logger, { adaptors: ['@openfn/language-http'] }); @@ -74,20 +82,23 @@ test('print versions for node, cli and long-form adaptor', async (t) => { t.regex(message, /@openfn\/language-http .+ latest/); }); -test('print versions for node, cli and long-form adaptor with version', async (t) => { - const logger = createMockLogger('', { level: 'info' }); - await printVersions(logger, { adaptors: ['@openfn/language-http@1234'] }); +test.serial( + 'print versions for node, cli and long-form adaptor with version', + async (t) => { + const logger = createMockLogger('', { level: 'info' }); + await printVersions(logger, { adaptors: ['@openfn/language-http@1234'] }); - const last = logger._parse(logger._last); - const message = last.message as string; + const last = logger._parse(logger._last); + const message = last.message as string; - t.regex(message, /@openfn\/language-http .+ 1234/); -}); + t.regex(message, /@openfn\/language-http .+ 1234/); + } +); -test('print version of adaptor with monorepo', async (t) => { - mock({ +test.serial('print version of adaptor with monorepo', async (t) => { + mockFs({ '/repo/http/package.json': '{ "version": "1.0.0" }', - [root]: mock.load(root, {}), + [root]: loadPath(root), }); const logger = createMockLogger('', { level: 'info' }); @@ -102,10 +113,10 @@ test('print version of adaptor with monorepo', async (t) => { t.regex(message, /@openfn\/language-http(.+)monorepo/); }); -test('print version of adaptor with path', async (t) => { - mock({ +test.serial('print version of adaptor with path', async (t) => { + mockFs({ '/repo/http/package.json': '{ "version": "1.0.0" }', - [root]: mock.load(root, {}), + [root]: loadPath(root), }); const logger = createMockLogger('', { level: 'info' }); @@ -119,28 +130,31 @@ test('print version of adaptor with path', async (t) => { t.regex(message, /@openfn\/language-http(.+)1\.0\.0/); }); -test('print version of adaptor with path even if monorepo is set', async (t) => { - mock({ - '/repo/http/package.json': '{ "version": "1.0.0" }', - [root]: mock.load(root, {}), - }); - - const logger = createMockLogger('', { level: 'info' }); - await printVersions(logger, { - adaptors: ['@openfn/language-http=/repo/http'], - monorepoPath: ['.'], - }); - - const last = logger._parse(logger._last); - const message = last.message as string; - - t.regex(message, /@openfn\/language-http(.+)1\.0\.0/); -}); - -test('print version of adaptor with path and @', async (t) => { - mock({ +test.serial( + 'print version of adaptor with path even if monorepo is set', + async (t) => { + mockFs({ + '/repo/http/package.json': '{ "version": "1.0.0" }', + [root]: loadPath(root), + }); + + const logger = createMockLogger('', { level: 'info' }); + await printVersions(logger, { + adaptors: ['@openfn/language-http=/repo/http'], + monorepoPath: ['.'], + }); + + const last = logger._parse(logger._last); + const message = last.message as string; + + t.regex(message, /@openfn\/language-http(.+)1\.0\.0/); + } +); + +test.serial('print version of adaptor with path and @', async (t) => { + mockFs({ '/repo/node_modules/@openfn/http/package.json': '{ "version": "1.0.0" }', - [root]: mock.load(root, {}), + [root]: loadPath(root), }); const logger = createMockLogger('', { level: 'info' }); @@ -154,7 +168,7 @@ test('print version of adaptor with path and @', async (t) => { t.regex(message, /@openfn\/language-http(.+)1\.0\.0/); }); -test('json output', async (t) => { +test.serial('json output', async (t) => { const logger = createMockLogger('', { level: 'info', json: true }); await printVersions(logger, { adaptors: ['http'], logJson: true }); diff --git a/packages/cli/test/util/validate-adaptors.test.ts b/packages/cli/test/util/validate-adaptors.test.ts index 2e7a209c2..acd6c09bf 100644 --- a/packages/cli/test/util/validate-adaptors.test.ts +++ b/packages/cli/test/util/validate-adaptors.test.ts @@ -1,13 +1,11 @@ import { createMockLogger } from '@openfn/logger'; import test from 'ava'; -import mockfs from 'mock-fs'; import validateAdaptors from '../../src/util/validate-adaptors'; const logger = createMockLogger('', { level: 'default' }); test.afterEach(() => { logger._reset(); - mockfs.restore(); }); test.serial('should warn if expression passed with no adaptor', async (t) => { diff --git a/packages/deploy/package.json b/packages/deploy/package.json index 58b8230b0..36947f300 100644 --- a/packages/deploy/package.json +++ b/packages/deploy/package.json @@ -29,13 +29,12 @@ "license": "ISC", "devDependencies": { "@inquirer/testing": "^2.1.53", + "@openfn/test-helpers": "workspace:*", "@types/json-diff": "^1.0.3", "@types/jsonpath": "^0.2.4", - "@types/mock-fs": "^4.13.4", "@types/node": "^18.19.130", "ava": "5.3.1", "jsonpath": "^1.3.0", - "mock-fs": "^5.5.0", "tslib": "^2.8.1", "tsup": "^8.5.1", "typescript": "^5.9.3" diff --git a/packages/deploy/test/validator.test.ts b/packages/deploy/test/validator.test.ts index d963db1b0..2b0e915ac 100644 --- a/packages/deploy/test/validator.test.ts +++ b/packages/deploy/test/validator.test.ts @@ -1,16 +1,14 @@ -import mock from 'mock-fs'; import test from 'ava'; +import { mockFs, restoreFs } from '@openfn/test-helpers'; import { parseAndValidate } from '../src/validator'; +test.afterEach(restoreFs); + function findError(errors: any[], message: string) { return errors.find((e) => e.message === message); } -test.after(() => { - mock.restore(); -}); - -test('Workflows must be a map', async (t) => { +test.serial('Workflows must be a map', async (t) => { const doc = ` name: project-name workflows: @@ -26,7 +24,7 @@ test('Workflows must be a map', async (t) => { t.is(err.path, 'workflows'); }); -test('Workflows must have unique ids', async (t) => { +test.serial('Workflows must have unique ids', async (t) => { const doc = ` name: project-name workflows: @@ -45,7 +43,7 @@ test('Workflows must have unique ids', async (t) => { t.is(err.path, 'workflow-one'); }); -test('Jobs must have unique ids within a workflow', async (t) => { +test.serial('Jobs must have unique ids within a workflow', async (t) => { const doc = ` name: project-name workflows: @@ -64,7 +62,7 @@ test('Jobs must have unique ids within a workflow', async (t) => { t.truthy(err); }); -test('Job ids can duplicate across workflows', async (t) => { +test.serial('Job ids can duplicate across workflows', async (t) => { const doc = ` name: project-name workflows: @@ -83,7 +81,7 @@ test('Job ids can duplicate across workflows', async (t) => { t.is(results.errors.length, 0); }); -test('Workflow edges are parsed correctly', async (t) => { +test.serial('Workflow edges are parsed correctly', async (t) => { const doc = ` name: project-name workflows: @@ -115,7 +113,7 @@ test('Workflow edges are parsed correctly', async (t) => { ); }); -test('allow empty workflows', async (t) => { +test.serial('allow empty workflows', async (t) => { let doc = ` name: project-name `; @@ -130,7 +128,7 @@ test('allow empty workflows', async (t) => { }); }); -test('allows null channels by converting to empty map', async (t) => { +test.serial('allows null channels by converting to empty map', async (t) => { const doc = ` name: project-name channels: @@ -145,7 +143,7 @@ test('allows null channels by converting to empty map', async (t) => { t.deepEqual(result.doc.channels, {}); }); -test('parses channels with destination_credential', async (t) => { +test.serial('parses channels with destination_credential', async (t) => { const doc = ` name: project-name channels: @@ -170,15 +168,17 @@ test('parses channels with destination_credential', async (t) => { }); }); -test('adds the file content into the job body from the specified path', async (t) => { - // Step 1: Create a temporary file that the YAML will reference - const fileContent = 'fn(state => state.data);'; - mock({ - '/jobBody.js': fileContent, - }); - - // Step 2: YAML document that references the file - const doc = ` +test.serial( + 'adds the file content into the job body from the specified path', + async (t) => { + // Step 1: Create a temporary file that the YAML will reference + const fileContent = 'fn(state => state.data);'; + mockFs({ + '/jobBody.js': fileContent, + }); + + // Step 2: YAML document that references the file + const doc = ` name: project-name workflows: workflow-one: @@ -191,11 +191,12 @@ test('adds the file content into the job body from the specified path', async (t path: /jobBody.js `; - // Step 3: Run the parseAndValidate function - const results = await parseAndValidate(doc, 'spec.yaml'); + // Step 3: Run the parseAndValidate function + const results = await parseAndValidate(doc, 'spec.yaml'); - // Step 4: Assert that the content from the file was merged into the spec - const jobBody = results.doc.workflows['workflow-one'].jobs!['job-one'].body; + // Step 4: Assert that the content from the file was merged into the spec + const jobBody = results.doc.workflows['workflow-one'].jobs!['job-one'].body; - t.is(jobBody.content, fileContent); -}); + t.is(jobBody.content, fileContent); + } +); diff --git a/packages/project/package.json b/packages/project/package.json index f0ddc3158..de6035384 100644 --- a/packages/project/package.json +++ b/packages/project/package.json @@ -27,10 +27,9 @@ "types": "dist/index.d.ts", "keywords": [], "devDependencies": { + "@openfn/test-helpers": "workspace:*", "@types/lodash": "^4.17.24", - "@types/mock-fs": "~4.13.4", "ava": "5.3.1", - "mock-fs": "^5.5.0", "rimraf": "^6.1.3", "tslib": "^2.8.1", "tsup": "^8.5.1", diff --git a/packages/project/test/parse/from-fs.test.ts b/packages/project/test/parse/from-fs.test.ts index 22dc5d4a2..d69d000c1 100644 --- a/packages/project/test/parse/from-fs.test.ts +++ b/packages/project/test/parse/from-fs.test.ts @@ -1,12 +1,13 @@ import test from 'ava'; -import mock from 'mock-fs'; +import { mockFs, restoreFs } from '@openfn/test-helpers'; import { parseProject } from '../../src/parse/from-fs'; import { jsonToYaml, yamlToJson } from '../../src/util/yaml'; import { buildConfig } from '../../src/util/config'; +test.afterEach(restoreFs); + test.afterEach(() => { files = {}; - mock.restore(); }); let files: Record = {}; @@ -20,7 +21,8 @@ function mockFile(path: string, content: string | object) { // @ts-ignore files[path] = content; - mock(files); + restoreFs(); + mockFs(files); } test.serial('should include multiple workflows (legacy format)', async (t) => { diff --git a/packages/project/test/parse/from-path.test.ts b/packages/project/test/parse/from-path.test.ts index 088b14d75..8d6b7b175 100644 --- a/packages/project/test/parse/from-path.test.ts +++ b/packages/project/test/parse/from-path.test.ts @@ -1,16 +1,18 @@ import test from 'ava'; -import mock from 'mock-fs'; +import { mockFs, restoreFs } from '@openfn/test-helpers'; import { generateProject } from '../../src'; import fromPath, { extractAliasFromFilename } from '../../src/parse/from-path'; import * as v2 from '../fixtures/sample-v2-project'; +test.afterEach(restoreFs); + const proj = generateProject('my-project', ['a-b'], { openfnUuid: true, }); test.serial('should load a v1 state json', async (t) => { - mock({ + mockFs({ '/p1/main@openfn.org.json': JSON.stringify( proj.serialize('state', { format: 'json' }) ), @@ -26,7 +28,7 @@ test.serial('should load a v1 state json', async (t) => { }); test.serial('should load a v1 state yaml', async (t) => { - mock({ + mockFs({ // @ts-ignore '/p1/main@openfn.org.yaml': proj.serialize('state', { format: 'yaml' }), }); @@ -41,7 +43,7 @@ test.serial('should load a v1 state yaml', async (t) => { }); test.serial('should load a v2 project yaml', async (t) => { - mock({ + mockFs({ '/p1/main@openfn.org.yaml': v2.yaml, }); const project = await fromPath('/p1/main@openfn.org.yaml'); @@ -52,7 +54,7 @@ test.serial('should load a v2 project yaml', async (t) => { }); test.serial('should load a v2 project json', async (t) => { - mock({ + mockFs({ '/p1/main@openfn.org.json': JSON.stringify(v2.json), }); const project = await fromPath('/p1/main@openfn.org.json'); @@ -63,7 +65,7 @@ test.serial('should load a v2 project json', async (t) => { }); test.serial('should use workspace config', async (t) => { - mock({ + mockFs({ // @ts-ignore '/p1/main@openfn.org.yaml': proj.serialize('state', { format: 'yaml' }), }); @@ -95,7 +97,7 @@ test.serial('should use workspace config', async (t) => { }); test.serial('should apply a name override to a loaded project', async (t) => { - mock({ + mockFs({ '/p1/main@openfn.org.yaml': v2.yaml, }); const project = await fromPath('/p1/main@openfn.org.yaml', { @@ -105,27 +107,36 @@ test.serial('should apply a name override to a loaded project', async (t) => { t.is(project.name, 'My Duplicate'); }); -test('extractAliasFromFilename: should extract alias from alias@domain.yaml format', (t) => { - const alias = extractAliasFromFilename('main@app.openfn.org.yaml'); - t.is(alias, 'main'); -}); - -test('extractAliasFromFilename: should extract alias from alias@domain.json format', (t) => { - const alias = extractAliasFromFilename('staging@localhost.json'); - t.is(alias, 'staging'); -}); - -test('extractAliasFromFilename: should extract alias from simple filename', (t) => { - const alias = extractAliasFromFilename('production.yaml'); - t.is(alias, 'production'); -}); - -test('extractAliasFromFilename: should handle full paths', (t) => { +test.serial( + 'extractAliasFromFilename: should extract alias from alias@domain.yaml format', + (t) => { + const alias = extractAliasFromFilename('main@app.openfn.org.yaml'); + t.is(alias, 'main'); + } +); + +test.serial( + 'extractAliasFromFilename: should extract alias from alias@domain.json format', + (t) => { + const alias = extractAliasFromFilename('staging@localhost.json'); + t.is(alias, 'staging'); + } +); + +test.serial( + 'extractAliasFromFilename: should extract alias from simple filename', + (t) => { + const alias = extractAliasFromFilename('production.yaml'); + t.is(alias, 'production'); + } +); + +test.serial('extractAliasFromFilename: should handle full paths', (t) => { const alias = extractAliasFromFilename('/path/to/dev@app.openfn.org.yaml'); t.is(alias, 'dev'); }); -test('extractAliasFromFilename: should handle complex aliases', (t) => { +test.serial('extractAliasFromFilename: should handle complex aliases', (t) => { const alias = extractAliasFromFilename( 'my-project-staging@app.openfn.org.yaml' ); diff --git a/packages/project/test/util/config.test.ts b/packages/project/test/util/config.test.ts index 0cd128af3..ad2456531 100644 --- a/packages/project/test/util/config.test.ts +++ b/packages/project/test/util/config.test.ts @@ -1,4 +1,5 @@ import test from 'ava'; +import { mockFs, restoreFs } from '@openfn/test-helpers'; import { extractCheckout, extractConfig, @@ -7,14 +8,11 @@ import { findWorkspaceFile, loadWorkspaceFile, } from '../../src/util/config'; -import mock from 'mock-fs'; import Project from '../../src'; -test.afterEach(() => { - mock.restore(); -}); +test.afterEach(restoreFs); -test('load config as yaml', (t) => { +test.serial('load config as yaml', (t) => { const yaml = ` workspace: dirs: @@ -65,7 +63,7 @@ project: test.todo('load config as json'); // Note that the legacy format is the old 0.6 version of openfn.yaml -test('legacy: load config as yaml', (t) => { +test.serial('legacy: load config as yaml', (t) => { const yaml = ` name: joe-sandbox-testing workflowRoot: workflows @@ -100,7 +98,7 @@ project: }); }); -test('legacy: load config as json', (t) => { +test.serial('legacy: load config as json', (t) => { const json = JSON.stringify({ name: 'joe-sandbox-testing', workflowRoot: 'workflows', @@ -137,7 +135,7 @@ test('legacy: load config as json', (t) => { }); }); -test('legacy: load empty config', (t) => { +test.serial('legacy: load empty config', (t) => { const yaml = ``; const result = loadWorkspaceFile(yaml); @@ -145,23 +143,23 @@ test('legacy: load empty config', (t) => { t.deepEqual(result.project, {}); }); -test('find openfn.yaml', (t) => { - mock({ '/tmp/openfn.yaml': 'x: 1' }); +test.serial('find openfn.yaml', (t) => { + mockFs({ '/tmp/openfn.yaml': 'x: 1' }); const result = findWorkspaceFile('/tmp'); t.is(result.type, 'yaml'); t.is(result.content, 'x: 1'); }); -test('find openfn.json', (t) => { - mock({ '/tmp/openfn.json': '{ "x": 1 }' }); +test.serial('find openfn.json', (t) => { + mockFs({ '/tmp/openfn.json': '{ "x": 1 }' }); const result = findWorkspaceFile('/tmp'); t.is(result.type, 'json'); t.deepEqual(result.content, { x: 1 }); }); -test('generate openfn.yaml (workspace config only)', (t) => { +test.serial('generate openfn.yaml (workspace config only)', (t) => { const proj = new Project( { id: 'my-project', @@ -196,7 +194,7 @@ formats: ); }); -test('generate openfn.yaml does not include collections', (t) => { +test.serial('generate openfn.yaml does not include collections', (t) => { const proj = new Project({ id: 'my-project', collections: [{ uuid: 'remote-uuid', name: 'my-collection' }], @@ -205,7 +203,7 @@ test('generate openfn.yaml does not include collections', (t) => { t.false(result.content.includes('collections')); }); -test('generate checkout file with forked_from', (t) => { +test.serial('generate checkout file with forked_from', (t) => { const proj = new Project({ id: 'my-project', name: 'My Project', @@ -229,7 +227,7 @@ uuid: 1234 ); }); -test('getCheckoutPath: default and per-branch', (t) => { +test.serial('getCheckoutPath: default and per-branch', (t) => { t.is(getCheckoutPath(), '.openfn/checkout.yaml'); t.is(getCheckoutPath(false), '.openfn/checkout.yaml'); t.is(getCheckoutPath('dev'), '.openfn/branches/dev/checkout.yaml'); @@ -239,7 +237,7 @@ test('getCheckoutPath: default and per-branch', (t) => { ); }); -test("checkout file excludes forked_from if it's not set", (t) => { +test.serial("checkout file excludes forked_from if it's not set", (t) => { const proj = new Project( { id: 'my-project', @@ -269,7 +267,7 @@ uuid: 1234 test.todo('generate openfn.json'); -test('include project name', (t) => { +test.serial('include project name', (t) => { const proj = new Project({ id: 'my-project', name: 'My Project', @@ -280,20 +278,23 @@ test('include project name', (t) => { test.todo('include parent project name'); -test('hasCheckoutMeta: false if there is no workspace at all', (t) => { - mock({ '/ws': {} }); +test.serial('hasCheckoutMeta: false if there is no workspace at all', (t) => { + mockFs({ '/ws': {} }); t.false(hasCheckoutMeta('/ws')); }); -test('hasCheckoutMeta: false if there is a workspace but no checkout file', (t) => { - mock({ '/ws/openfn.yaml': 'credentials: creds.yaml' }); +test.serial( + 'hasCheckoutMeta: false if there is a workspace but no checkout file', + (t) => { + mockFs({ '/ws/openfn.yaml': 'credentials: creds.yaml' }); - t.false(hasCheckoutMeta('/ws')); -}); + t.false(hasCheckoutMeta('/ws')); + } +); -test('hasCheckoutMeta: true if there is a checkout file', (t) => { - mock({ +test.serial('hasCheckoutMeta: true if there is a checkout file', (t) => { + mockFs({ '/ws/openfn.yaml': 'credentials: creds.yaml', '/ws/.openfn/checkout.yaml': 'id: my-project', }); @@ -301,8 +302,8 @@ test('hasCheckoutMeta: true if there is a checkout file', (t) => { t.true(hasCheckoutMeta('/ws')); }); -test('hasCheckoutMeta: false if the checkout file is empty', (t) => { - mock({ +test.serial('hasCheckoutMeta: false if the checkout file is empty', (t) => { + mockFs({ '/ws/openfn.yaml': 'credentials: creds.yaml', '/ws/.openfn/checkout.yaml': '', }); @@ -310,23 +311,29 @@ test('hasCheckoutMeta: false if the checkout file is empty', (t) => { t.false(hasCheckoutMeta('/ws')); }); -test('hasCheckoutMeta: true if there is a legacy project block in openfn.yaml', (t) => { - mock({ - '/ws/openfn.yaml': - 'project:\n id: my-project\nworkspace:\n credentials: creds.yaml', - }); - - t.true(hasCheckoutMeta('/ws')); -}); - -test('hasCheckoutMeta: looks for the checkout file for the given branch', (t) => { - mock({ - '/ws/openfn.yaml': 'credentials: creds.yaml', - '/ws/.openfn/branches/dev/checkout.yaml': 'id: my-project', - }); - - t.true(hasCheckoutMeta('/ws', 'dev')); - // other branches, and no branch, don't have any - t.false(hasCheckoutMeta('/ws', 'main')); - t.false(hasCheckoutMeta('/ws')); -}); +test.serial( + 'hasCheckoutMeta: true if there is a legacy project block in openfn.yaml', + (t) => { + mockFs({ + '/ws/openfn.yaml': + 'project:\n id: my-project\nworkspace:\n credentials: creds.yaml', + }); + + t.true(hasCheckoutMeta('/ws')); + } +); + +test.serial( + 'hasCheckoutMeta: looks for the checkout file for the given branch', + (t) => { + mockFs({ + '/ws/openfn.yaml': 'credentials: creds.yaml', + '/ws/.openfn/branches/dev/checkout.yaml': 'id: my-project', + }); + + t.true(hasCheckoutMeta('/ws', 'dev')); + // other branches, and no branch, don't have any + t.false(hasCheckoutMeta('/ws', 'main')); + t.false(hasCheckoutMeta('/ws')); + } +); diff --git a/packages/project/test/workspace.test.ts b/packages/project/test/workspace.test.ts index b8ae20410..1713dce3c 100644 --- a/packages/project/test/workspace.test.ts +++ b/packages/project/test/workspace.test.ts @@ -1,6 +1,8 @@ -import mock from 'mock-fs'; -import { jsonToYaml, Workspace } from '../src'; +import Project, { jsonToYaml, Workspace } from '../src'; import test from 'ava'; +import { mockFs, restoreFs } from '@openfn/test-helpers'; + +test.afterEach(restoreFs); const gen = (uuid: any, _alias: string, id: string, _domain: string) => jsonToYaml({ @@ -14,240 +16,257 @@ const gen = (uuid: any, _alias: string, id: string, _domain: string) => }); // TODO need a test on the legacy and new yaml formats here -mock({ - '/ws/openfn.yaml': jsonToYaml({ - project: { - id: 'project-1', - uuid: '1234', - }, - workspace: { - formats: { - openfn: 'yaml', - project: 'yaml', - workflow: 'yaml', - custom: true, // Note that this will be excluded - }, - // deliberately exclude dirs - custom: true, - }, - }), - '/ws/.projects/staging@app.openfn.org.yaml': jsonToYaml({ - name: 'Project 1', - id: '', - workflows: [ - { - name: 'simple-workflow', // TODO clean up - id: 'wf-id', // TODO clean up - jobs: [ - { - name: 'Transform data to FHIR standard', - body: ' fn(state => state); // sdfl', - adaptor: '@openfn/language-http@latest', - id: 'job-a', - }, - ], - triggers: [ - { - type: 'webhook', - enabled: true, - id: 'trigger-id', - }, - ], - edges: [ - { - id: 'edge-id', - target_job_id: 'job-a', - enabled: true, - source_trigger_id: 'trigger-id', - condition_type: 'always', - }, - ], +test.beforeEach(() => { + mockFs({ + '/ws/openfn.yaml': jsonToYaml({ + project: { + id: 'project-1', + uuid: '1234', }, - { - name: 'another-workflow', - id: 'another-id', - jobs: [ - { - name: 'Transform data to FHIR standard', - body: ' fn(state => state); // sdfl', - adaptor: '@openfn/language-http@latest', - id: 'job-b', - }, - ], - triggers: [ - { - type: 'webhook', - enabled: true, - id: 'trigger-id', - }, - ], - edges: [ - { - id: 'edge-id', - target_job_id: 'job-b', - enabled: true, - source_trigger_id: 'trigger-id', - condition_type: 'always', - }, - ], + workspace: { + formats: { + openfn: 'yaml', + project: 'yaml', + workflow: 'yaml', + custom: true, // Note that this will be excluded + }, + // deliberately exclude dirs + custom: true, }, - ], - }), - - // json format - '/ws2/openfn.yaml': jsonToYaml({ - project: { - id: 'project-2', - uuid: '', - }, - workspace: { - formats: { - openfn: 'yaml', - project: 'json', - workflow: 'yaml', + }), + '/ws/workflows/wf-1/wf-1.yaml': jsonToYaml({ + id: 'wf-1', + steps: [{ id: 'a', expression: 'a.js' }], + }), + '/ws/workflows/wf-1/a.js': 'fn(s => s)', + '/ws/workflows/wf-2/wf-2.yaml': jsonToYaml({ + id: 'wf-2', + steps: [{ id: 'b', expression: 'b.js' }], + }), + '/ws/workflows/wf-2/b.js': 'fn(s => s)', + '/ws/.projects/staging@app.openfn.org.yaml': jsonToYaml({ + name: 'Project 1', + id: '', + workflows: [ + { + name: 'simple-workflow', // TODO clean up + id: 'wf-id', // TODO clean up + jobs: [ + { + name: 'Transform data to FHIR standard', + body: ' fn(state => state); // sdfl', + adaptor: '@openfn/language-http@latest', + id: 'job-a', + }, + ], + triggers: [ + { + type: 'webhook', + enabled: true, + id: 'trigger-id', + }, + ], + edges: [ + { + id: 'edge-id', + target_job_id: 'job-a', + enabled: true, + source_trigger_id: 'trigger-id', + condition_type: 'always', + }, + ], + }, + { + name: 'another-workflow', + id: 'another-id', + jobs: [ + { + name: 'Transform data to FHIR standard', + body: ' fn(state => state); // sdfl', + adaptor: '@openfn/language-http@latest', + id: 'job-b', + }, + ], + triggers: [ + { + type: 'webhook', + enabled: true, + id: 'trigger-id', + }, + ], + edges: [ + { + id: 'edge-id', + target_job_id: 'job-b', + enabled: true, + source_trigger_id: 'trigger-id', + condition_type: 'always', + }, + ], + }, + ], + }), + + // json format + '/ws2/openfn.yaml': jsonToYaml({ + project: { + id: 'project-2', + uuid: '', }, - }, - }), - '/ws2/.projects/staging@app.openfn.org.json': JSON.stringify({ - name: 'Project 2', - id: '', - workflows: [ - { - name: 'Simple Workflow', - id: '', - jobs: [ - { - name: 'X', - body: '.', - adaptor: '@openfn/language-http@latest', - id: 'a', - }, - ], - triggers: [ - { - type: 'webhook', - enabled: true, - id: 'trigger-id', - }, - ], - edges: [ - { - id: 'edge-id', - target_job_id: 'a', - enabled: true, - source_trigger_id: 'trigger-id', - condition_type: 'always', - }, - ], + workspace: { + formats: { + openfn: 'yaml', + project: 'json', + workflow: 'yaml', + }, }, - ], - }), - - // custom paths - '/ws3/openfn.yaml': jsonToYaml({ - project: { - id: 'project-3', - uuid: '', - }, - workspace: { - dirs: { - projects: 'p', + }), + '/ws2/.projects/staging@app.openfn.org.json': JSON.stringify({ + name: 'Project 2', + id: '', + workflows: [ + { + name: 'Simple Workflow', + id: '', + jobs: [ + { + name: 'X', + body: '.', + adaptor: '@openfn/language-http@latest', + id: 'a', + }, + ], + triggers: [ + { + type: 'webhook', + enabled: true, + id: 'trigger-id', + }, + ], + edges: [ + { + id: 'edge-id', + target_job_id: 'a', + enabled: true, + source_trigger_id: 'trigger-id', + condition_type: 'always', + }, + ], + }, + ], + }), + + // custom paths + '/ws3/openfn.yaml': jsonToYaml({ + project: { + id: 'project-3', + uuid: '', }, - }, - }), - '/ws3/p/project@app.openfn.org.yaml': jsonToYaml({ - name: 'Project 3', - id: '', - workflows: [ - { - name: 'Simple Workflow', - id: '', - jobs: [ - { - name: 'X', - body: '.', - adaptor: '@openfn/language-http@latest', - id: 'a', - }, - ], - triggers: [ - { - type: 'webhook', - enabled: true, - id: 'trigger-id', - }, - ], - edges: [ - { - id: 'edge-id', - target_job_id: 'a', - enabled: true, - source_trigger_id: 'trigger-id', - condition_type: 'always', - }, - ], + workspace: { + dirs: { + projects: 'p', + }, }, - ], - }), - - // aliasing - // Two local copies of the same project, with "main" checked out - '/ws5/openfn.yaml': jsonToYaml({ - project: { id: 'proj-1', uuid: '111', alias: 'main' }, - }), - '/ws5/.projects/main@openfn.org.yaml': gen( - 111, - 'main', - 'proj-1', - 'openfn.org' - ), - '/ws5/.projects/backup@openfn.org.yaml': gen( - 111, - 'backup', - 'proj-1', - 'openfn.org' - ), - // Two copies on different ports of the same host - '/ws6/openfn.yaml': '', - '/ws6/.openfn/checkout.yaml': jsonToYaml({ - id: 'proj-1', - uuid: '222', - alias: 'local', - endpoint: 'http://localhost:5000', - }), - '/ws6/.projects/local@localhost.yaml': jsonToYaml({ - id: 'proj-1', - version: 2, - openfn: { uuid: '111', endpoint: 'http://localhost:4000' }, - workflows: [], - }), - '/ws6/.projects/other@localhost.yaml': jsonToYaml({ - id: 'proj-1', - version: 2, - openfn: { uuid: '222', endpoint: 'http://localhost:5000' }, - workflows: [], - }), - '/ws4/openfn.yaml': '', - '/ws4/.projects/main@openfn.org.yaml': gen( - 111, - 'main', - 'proj-1', - 'openfn.org' - ), - // prettier-ignore - '/ws4/.projects/main@somewhere.com.yaml': gen(112, 'main', 'proj-1', 'somewhere.com'), - // prettier-ignore - '/ws4/.projects/staging@openfn.org.yaml': gen(113, 'staging', 'proj-1-staging', 'openfn.org'), + }), + '/ws3/p/project@app.openfn.org.yaml': jsonToYaml({ + name: 'Project 3', + id: '', + workflows: [ + { + name: 'Simple Workflow', + id: '', + jobs: [ + { + name: 'X', + body: '.', + adaptor: '@openfn/language-http@latest', + id: 'a', + }, + ], + triggers: [ + { + type: 'webhook', + enabled: true, + id: 'trigger-id', + }, + ], + edges: [ + { + id: 'edge-id', + target_job_id: 'a', + enabled: true, + source_trigger_id: 'trigger-id', + condition_type: 'always', + }, + ], + }, + ], + }), + + // aliasing + // Two local copies of the same project, with "main" checked out + '/ws5/openfn.yaml': jsonToYaml({ + project: { id: 'proj-1', uuid: '111', alias: 'main' }, + }), + '/ws5/.projects/main@openfn.org.yaml': gen( + 111, + 'main', + 'proj-1', + 'openfn.org' + ), + '/ws5/.projects/backup@openfn.org.yaml': gen( + 111, + 'backup', + 'proj-1', + 'openfn.org' + ), + // Two copies on different ports of the same host + '/ws6/openfn.yaml': '', + '/ws6/.openfn/checkout.yaml': jsonToYaml({ + id: 'proj-1', + uuid: '222', + alias: 'local', + endpoint: 'http://localhost:5000', + }), + '/ws6/.projects/local@localhost.yaml': jsonToYaml({ + id: 'proj-1', + version: 2, + openfn: { uuid: '111', endpoint: 'http://localhost:4000' }, + workflows: [], + }), + '/ws6/.projects/other@localhost.yaml': jsonToYaml({ + id: 'proj-1', + version: 2, + openfn: { uuid: '222', endpoint: 'http://localhost:5000' }, + workflows: [], + }), + '/ws4/openfn.yaml': '', + '/ws4/.projects/main@openfn.org.yaml': gen( + 111, + 'main', + 'proj-1', + 'openfn.org' + ), + // prettier-ignore + '/ws4/.projects/main@somewhere.com.yaml': gen(112, 'main', 'proj-1', 'somewhere.com'), + // prettier-ignore + '/ws4/.projects/staging@openfn.org.yaml': gen(113, 'staging', 'proj-1-staging', 'openfn.org'), + }); +}); + +test.serial('load the checked out project from the filesystem', async (t) => { + const project = await Project.from('fs', { root: '/ws' }); + t.is(project.workflows.length, 2); }); -test('workspace-path: valid workspace path', (t) => { +test.serial('workspace-path: valid workspace path', (t) => { const ws = new Workspace('/ws'); t.is(ws.valid, true); t.truthy(ws.config); }); -test('workspace-path: invalid workspace path', (t) => { +test.serial('workspace-path: invalid workspace path', (t) => { const ws = new Workspace('/invalid'); t.is(ws.valid, false); @@ -257,7 +276,7 @@ test('workspace-path: invalid workspace path', (t) => { t.truthy(ws.config.formats); }); -test('workspace-list: list projects in the workspace', (t) => { +test.serial('workspace-list: list projects in the workspace', (t) => { const ws = new Workspace('/ws'); const result = ws.list(); @@ -268,7 +287,7 @@ test('workspace-list: list projects in the workspace', (t) => { ); }); -test('workspace-get: get projects in the workspace', (t) => { +test.serial('workspace-get: get projects in the workspace', (t) => { const ws = new Workspace('/ws'); const found = ws.get('project-1'); t.truthy(found); @@ -279,7 +298,7 @@ test('workspace-get: get projects in the workspace', (t) => { ); }); -test('load JSON files', (t) => { +test.serial('load JSON files', (t) => { const ws = new Workspace('/ws2'); const proj = ws.get('project-2'); t.truthy(proj); @@ -287,7 +306,7 @@ test('load JSON files', (t) => { t.is(proj?.workflows[0].id, 'simple-workflow'); }); -test('load from custom path', (t) => { +test.serial('load from custom path', (t) => { const ws = new Workspace('/ws3'); const proj = ws.get('project-3'); t.truthy(proj); @@ -295,7 +314,7 @@ test('load from custom path', (t) => { t.is(proj?.workflows[0].id, 'simple-workflow'); }); -test('load config', (t) => { +test.serial('load config', (t) => { const ws = new Workspace('/ws'); t.deepEqual(ws.config, { credentials: 'credentials.yaml', @@ -312,7 +331,7 @@ test('load config', (t) => { }); }); -test('load project meta', (t) => { +test.serial('load project meta', (t) => { const ws = new Workspace('/ws'); t.deepEqual(ws.activeProject, { uuid: '1234', @@ -320,14 +339,14 @@ test('load project meta', (t) => { }); }); -test('load v2 projects with multiple matching ids', (t) => { +test.serial('load v2 projects with multiple matching ids', (t) => { const ws = new Workspace('/ws4'); // @ts-ignore t.is(ws.projects.length, 3); }); -test('get project by id', (t) => { +test.serial('get project by id', (t) => { const ws = new Workspace('/ws4'); const project = ws.get('proj-1-staging'); @@ -335,7 +354,7 @@ test('get project by id', (t) => { t.is(project?.id, 'proj-1-staging'); }); -test('get project by partial uuid', (t) => { +test.serial('get project by partial uuid', (t) => { const ws = new Workspace('/ws4'); const project = ws.get('3'); @@ -343,14 +362,14 @@ test('get project by partial uuid', (t) => { t.is(project?.uuid, '113'); }); -test('get project returns null when not found', (t) => { +test.serial('get project returns null when not found', (t) => { const ws = new Workspace('/ws4'); const project = ws.get('non-existent'); t.is(project, null); }); -test('get project throws on ambiguous match', (t) => { +test.serial('get project throws on ambiguous match', (t) => { const ws = new Workspace('/ws4'); const error = t.throws(() => ws.get('main')); @@ -358,7 +377,7 @@ test('get project throws on ambiguous match', (t) => { t.regex(error!.message, /Failed to resolve unique identifier/); }); -test('get a duplicated project by uuid throws', (t) => { +test.serial('get a duplicated project by uuid throws', (t) => { const ws = new Workspace('/ws5'); t.throws(() => ws.get('111'), { @@ -366,19 +385,22 @@ test('get a duplicated project by uuid throws', (t) => { }); }); -test('getTrackedProject returns the checked out copy by alias', (t) => { +test.serial('getTrackedProject returns the checked out copy by alias', (t) => { const ws = new Workspace('/ws5'); t.is(ws.getTrackedProject()?.alias, 'main'); }); -test('getTrackedProject matches the alias on the same port only', (t) => { - const ws = new Workspace('/ws6'); +test.serial( + 'getTrackedProject matches the alias on the same port only', + (t) => { + const ws = new Workspace('/ws6'); - t.is(ws.getTrackedProject()?.alias, 'other'); -}); + t.is(ws.getTrackedProject()?.alias, 'other'); + } +); -test('getProjectPath returns the file for each copy', (t) => { +test.serial('getProjectPath returns the file for each copy', (t) => { const ws = new Workspace('/ws5'); t.is( diff --git a/packages/runtime/package.json b/packages/runtime/package.json index 2cc920459..fc19887e0 100644 --- a/packages/runtime/package.json +++ b/packages/runtime/package.json @@ -21,7 +21,7 @@ "test:watch": "pnpm ava -w", "test:types": "pnpm tsc --project tsconfig.test.json", "test:memory": "pnpm ava --config memtest.ava.config.cjs", - "build": "tsup --config ../../tsup.config.js src/index.ts src/modules/esm-resolve-hook.js", + "build": "tsup --config ../../tsup.config.js --no-splitting src/index.ts src/modules/esm-resolve-hook.js", "build:watch": "pnpm build --watch", "pack": "pnpm pack --pack-destination ../../dist" }, @@ -32,11 +32,10 @@ "@openfn/compiler": "workspace:^", "@openfn/language-common": "3.2.3", "@openfn/lexicon": "workspace:^", - "@types/mock-fs": "^4.13.4", + "@openfn/test-helpers": "workspace:*", "@types/node": "^18.19.130", "@types/semver": "^7.7.1", "ava": "5.3.1", - "mock-fs": "^5.5.0", "recast": "^0.21.5", "ts-node": "^10.9.2", "tslib": "^2.8.1", @@ -48,7 +47,6 @@ "dist/index.d.ts", "dist/modules/esm-resolve-hook.js", "dist/modules/esm-resolve-hook.d.ts", - "dist/esm-resolve-hook.js", "README.md" ], "dependencies": { diff --git a/packages/runtime/src/modules/esm-resolve-hook.js b/packages/runtime/src/modules/esm-resolve-hook.js index cc6b71f17..41ab6b812 100644 --- a/packages/runtime/src/modules/esm-resolve-hook.js +++ b/packages/runtime/src/modules/esm-resolve-hook.js @@ -1,8 +1,24 @@ +const shouldRetry = (err, specifier) => + err.code === 'ERR_MODULE_NOT_FOUND' && !/\.[a-z]+$/.test(specifier); + +// For module.registerHooks() +export function resolveSync(specifier, context, nextResolve) { + try { + return nextResolve(specifier, context); + } catch (err) { + if (shouldRetry(err, specifier)) { + return nextResolve(specifier + '.js', context); + } + throw err; + } +} + +// For module.register(), which loads this file on a separate thread export async function resolve(specifier, context, nextResolve) { try { return await nextResolve(specifier, context); } catch (err) { - if (err.code === 'ERR_MODULE_NOT_FOUND' && !/\.[a-z]+$/.test(specifier)) { + if (shouldRetry(err, specifier)) { return nextResolve(specifier + '.js', context); } throw err; diff --git a/packages/runtime/src/modules/register-esm-hook.ts b/packages/runtime/src/modules/register-esm-hook.ts index eaf42e12a..289a44502 100644 --- a/packages/runtime/src/modules/register-esm-hook.ts +++ b/packages/runtime/src/modules/register-esm-hook.ts @@ -1,10 +1,28 @@ -import { register } from 'node:module'; +import module from 'node:module'; import path from 'node:path'; import url from 'node:url'; +import { resolveSync } from './esm-resolve-hook.js'; + +let registered = false; // Backcompat loader hook for ESM modules that import CJS subpaths without a .js extension // e.g. `import set from 'lodash/set'` instead of `import set from 'lodash/set.js'` +// Hooks chain on every registration, so this registers once per thread export const registerEsmHook = () => { + if (registered) { + return; + } + registered = true; + + // registerHooks() needs Node 22.15 or 23.5 + const { registerHooks } = module as typeof module & { + registerHooks?: (hooks: { resolve: typeof resolveSync }) => void; + }; + if (registerHooks) { + registerHooks({ resolve: resolveSync }); + return; + } + try { // Resolve relative to the package entry point so this works both from source // (where import.meta.url is src/modules/) and from the bundle (where it's dist/) @@ -15,7 +33,7 @@ export const registerEsmHook = () => { path.dirname(packageEntry), 'modules/esm-resolve-hook.js' ); - register(`file://${hookPath}`); + module.register(`file://${hookPath}`); } catch (e) { console.error(e); } diff --git a/packages/runtime/test/__esm-hook__/count-resolves.mjs b/packages/runtime/test/__esm-hook__/count-resolves.mjs new file mode 100644 index 000000000..1e42e00ba --- /dev/null +++ b/packages/runtime/test/__esm-hook__/count-resolves.mjs @@ -0,0 +1,8 @@ +import { writeSync } from 'node:fs'; + +// Writes each specifier it resolves to stderr. Registered beneath the ESM hook, +// it sees every attempt the hook makes. +export function resolve(specifier, context, nextResolve) { + writeSync(2, `${specifier}\n`); + return nextResolve(specifier, context); +} diff --git a/packages/runtime/test/__esm-hook__/node_modules/adaptor-with-subpath/index.js b/packages/runtime/test/__esm-hook__/node_modules/adaptor-with-subpath/index.js new file mode 100644 index 000000000..3426a81dc --- /dev/null +++ b/packages/runtime/test/__esm-hook__/node_modules/adaptor-with-subpath/index.js @@ -0,0 +1,3 @@ +import set from 'cjs-subpaths/set'; + +export const value = set; diff --git a/packages/runtime/test/__esm-hook__/node_modules/adaptor-with-subpath/package.json b/packages/runtime/test/__esm-hook__/node_modules/adaptor-with-subpath/package.json new file mode 100644 index 000000000..5f05eca12 --- /dev/null +++ b/packages/runtime/test/__esm-hook__/node_modules/adaptor-with-subpath/package.json @@ -0,0 +1,7 @@ +{ + "name": "adaptor-with-subpath", + "version": "1.0.0", + "type": "module", + "main": "index.js", + "private": true +} diff --git a/packages/runtime/test/__esm-hook__/node_modules/cjs-subpaths/package.json b/packages/runtime/test/__esm-hook__/node_modules/cjs-subpaths/package.json new file mode 100644 index 000000000..4b01f1e0c --- /dev/null +++ b/packages/runtime/test/__esm-hook__/node_modules/cjs-subpaths/package.json @@ -0,0 +1,5 @@ +{ + "name": "cjs-subpaths", + "version": "1.0.0", + "private": true +} diff --git a/packages/runtime/test/__esm-hook__/node_modules/cjs-subpaths/set.js b/packages/runtime/test/__esm-hook__/node_modules/cjs-subpaths/set.js new file mode 100644 index 000000000..571faddbd --- /dev/null +++ b/packages/runtime/test/__esm-hook__/node_modules/cjs-subpaths/set.js @@ -0,0 +1 @@ +module.exports = 'set'; diff --git a/packages/runtime/test/__esm-hook__/run-job.mjs b/packages/runtime/test/__esm-hook__/run-job.mjs new file mode 100644 index 000000000..5c0bb44e7 --- /dev/null +++ b/packages/runtime/test/__esm-hook__/run-job.mjs @@ -0,0 +1,51 @@ +// Runs in its own process so that every test starts with no hooks registered +import module from 'node:module'; +import path from 'node:path'; +import run, { registerEsmHook } from '../../dist/index.js'; + +const [action, arg] = process.argv.slice(2); +const here = path.dirname(new URL(import.meta.url).pathname); + +const actions = { + async job() { + const state = await run( + `import { value } from 'adaptor-with-subpath'; + export default [() => ({ value })];`, + {}, + { + disableEsmHook: arg === 'no-hook', + linker: { + modules: { + 'adaptor-with-subpath': { + path: path.join(here, 'node_modules/adaptor-with-subpath'), + }, + }, + }, + } + ); + return state.value; + }, + async import() { + registerEsmHook(); + const m = await import(arg); + return m.default; + }, + async 'import-after-registering-three-times'() { + const counter = new URL('./count-resolves.mjs', import.meta.url); + if (module.registerHooks) { + module.registerHooks(await import(counter)); + } else { + module.register(counter); + } + registerEsmHook(); + registerEsmHook(); + registerEsmHook(); + await import(arg); + }, +}; + +try { + console.log(JSON.stringify({ result: await actions[action]() })); +} catch (e) { + console.log(JSON.stringify({ error: e.code ?? e.name })); +} diff --git a/packages/runtime/test/modules/esm-hook.test.ts b/packages/runtime/test/modules/esm-hook.test.ts new file mode 100644 index 000000000..48d73783b --- /dev/null +++ b/packages/runtime/test/modules/esm-hook.test.ts @@ -0,0 +1,52 @@ +import test from 'ava'; +import path from 'node:path'; +import { execFile } from 'node:child_process'; +import { promisify } from 'node:util'; + +const script = path.resolve('test/__esm-hook__/run-job.mjs'); + +const exec = async (...args: string[]) => { + const { stdout, stderr } = await promisify(execFile)( + process.execPath, + ['--no-warnings', '--experimental-vm-modules', script, ...args], + { cwd: path.dirname(script) } + ); + return { ...JSON.parse(stdout), stderr }; +}; + +test('a job can use an adaptor that imports an extensionless CJS subpath', async (t) => { + const { result } = await exec('job'); + t.is(result, 'set'); +}); + +test('without the hook, the same job fails to import the adaptor', async (t) => { + const { error } = await exec('job', 'no-hook'); + t.is(error, 'ImportError'); +}); + +test('the hook resolves an extensionless CJS subpath', async (t) => { + const { result } = await exec('import', 'cjs-subpaths/set'); + t.is(result, 'set'); +}); + +test('the hook leaves a specifier with an extension alone', async (t) => { + const { result } = await exec('import', 'cjs-subpaths/set.js'); + t.is(result, 'set'); +}); + +test('the hook still fails on a subpath that does not exist', async (t) => { + const { error } = await exec('import', 'cjs-subpaths/missing'); + t.is(error, 'ERR_MODULE_NOT_FOUND'); +}); + +test('registering the hook again does not add it twice', async (t) => { + const { error, stderr } = await exec( + 'import-after-registering-three-times', + 'cjs-subpaths/missing' + ); + t.is(error, 'ERR_MODULE_NOT_FOUND'); + const attempts = stderr + .split('\n') + .filter((line: string) => line.startsWith('cjs-subpaths/missing')); + t.deepEqual(attempts, ['cjs-subpaths/missing', 'cjs-subpaths/missing.js']); +}); diff --git a/packages/runtime/test/repo/ensure-repo.test.ts b/packages/runtime/test/repo/ensure-repo.test.ts index 49e7247b6..958335b05 100644 --- a/packages/runtime/test/repo/ensure-repo.test.ts +++ b/packages/runtime/test/repo/ensure-repo.test.ts @@ -1,19 +1,20 @@ import test from 'ava'; -import mock from 'mock-fs'; +import { mockFs, restoreFs } from '@openfn/test-helpers'; import { createMockLogger } from '@openfn/logger'; import { ensureRepo, loadRepoPkg } from '../../src/modules/repo'; +test.afterEach(restoreFs); + const logger = createMockLogger('test', { level: 'debug' }); test.afterEach(() => { - mock.restore(); logger._reset(); }); const defaultPkg = JSON.stringify({ name: 'test' }); test.serial('return the existing package json', async (t) => { - mock({ + mockFs({ '/tmp/repo/package.json': defaultPkg, }); const result = await ensureRepo('/tmp/repo', logger); @@ -22,7 +23,7 @@ test.serial('return the existing package json', async (t) => { }); test.serial('do not modify the existing package.json', async (t) => { - mock({ + mockFs({ '/tmp/repo/package.json': defaultPkg, }); const result = await ensureRepo('/tmp/repo', logger); @@ -36,7 +37,7 @@ test.serial('do not modify the existing package.json', async (t) => { test.serial( 'create and return a package.json if the repo does not exist', async (t) => { - mock({}); + mockFs({}); const result = await ensureRepo('/tmp/repo', logger); t.truthy(result); t.is(result.name, 'openfn-repo'); @@ -53,7 +54,7 @@ test.serial( test.serial( 'create and return a package if the repo does not exist at a long path', async (t) => { - mock({}); + mockFs({}); const result = await ensureRepo('/a/b/c/d/repo', logger); t.truthy(result); t.is(result.name, 'openfn-repo'); @@ -64,7 +65,7 @@ test.serial( ); test.serial('log if a repo is created', async (t) => { - mock({}); + mockFs({}); await ensureRepo('/tmp/repo', logger); const { message } = logger._parse(logger._last); t.regex(message as string, /Creating new repo/); diff --git a/packages/runtime/test/repo/get-module-entry-point.test.ts b/packages/runtime/test/repo/get-module-entry-point.test.ts index cd941cbc2..ad9437186 100644 --- a/packages/runtime/test/repo/get-module-entry-point.test.ts +++ b/packages/runtime/test/repo/get-module-entry-point.test.ts @@ -1,20 +1,20 @@ import test from 'ava'; -import mock from 'mock-fs'; -import path from 'node:path'; +import { mockFs, restoreFs } from '@openfn/test-helpers'; import { createMockLogger } from '@openfn/logger'; import { getModuleEntryPoint } from '../../src/modules/repo'; +test.afterEach(restoreFs); + const logger = createMockLogger(); test.afterEach(() => { - mock.restore(); logger._reset(); }); -const repoPath = path.resolve('test/__repo__'); +const repoPath = '/repo'; const mockRepo = (pkg: object) => { - mock({ + mockFs({ // Mock a repo with one module installed [`${repoPath}/package.json`]: JSON.stringify({ name: 'test', diff --git a/packages/runtime/test/repo/get-module-path.test.ts b/packages/runtime/test/repo/get-module-path.test.ts index 924ab2024..abd4a7001 100644 --- a/packages/runtime/test/repo/get-module-path.test.ts +++ b/packages/runtime/test/repo/get-module-path.test.ts @@ -1,19 +1,20 @@ import test from 'ava'; -import mock from 'mock-fs'; +import { mockFs, restoreFs } from '@openfn/test-helpers'; import { createMockLogger } from '@openfn/logger'; import { getModulePath } from '../../src/modules/repo'; +test.afterEach(restoreFs); + const logger = createMockLogger(); test.afterEach(() => { - mock.restore(); logger._reset(); }); const repoPath = '/tmp/repo/'; const mockPkg = (dependencies: object) => { - mock({ + mockFs({ [`${repoPath}/package.json`]: JSON.stringify({ name: 'test', private: true, diff --git a/packages/runtime/test/repo/install.test.ts b/packages/runtime/test/repo/install.test.ts index a2f276f8c..694daec86 100644 --- a/packages/runtime/test/repo/install.test.ts +++ b/packages/runtime/test/repo/install.test.ts @@ -3,20 +3,21 @@ * Testing with actual installs is left as an exercise for integration testing */ import test from 'ava'; -import mock from 'mock-fs'; +import { mockFs, restoreFs } from '@openfn/test-helpers'; import { createMockLogger } from '@openfn/logger'; import { install, loadRepoPkg } from '../../src'; import exec from '../../src/util/exec'; +test.afterEach(restoreFs); + const mockLogger = createMockLogger(undefined, { level: 'debug' }); test.afterEach(() => { - mock.restore(); mockLogger._reset(); }); test.beforeEach(() => { - mock({ + mockFs({ '/tmp': {}, }); }); @@ -139,7 +140,8 @@ test.serial('install with the correct alias', async (t) => { test.serial('do nothing if the package is already installed', async (t) => { const exec = mockExec(); - mock({ + restoreFs(); + mockFs({ '/tmp/repo/package.json': JSON.stringify({ name: 'test', dependencies: { diff --git a/packages/test-helpers/package.json b/packages/test-helpers/package.json new file mode 100644 index 000000000..679b622ce --- /dev/null +++ b/packages/test-helpers/package.json @@ -0,0 +1,13 @@ +{ + "name": "@openfn/test-helpers", + "version": "0.0.0", + "private": true, + "description": "Shared helpers for kit's unit tests", + "type": "module", + "main": "src/memfs.ts", + "types": "src/memfs.ts", + "license": "ISC", + "dependencies": { + "memfs": "^4.80.0" + } +} diff --git a/packages/test-helpers/src/memfs.ts b/packages/test-helpers/src/memfs.ts new file mode 100644 index 000000000..ac655a323 --- /dev/null +++ b/packages/test-helpers/src/memfs.ts @@ -0,0 +1,181 @@ +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { promisify } from 'node:util'; +import { fileURLToPath } from 'node:url'; +import { syncBuiltinESMExports } from 'node:module'; +import { memfs, type NestedDirectoryJSON } from 'memfs'; + +/* + * Swaps node:fs for memfs while mocked. Every fs function is replaced once, + * when this module loads, with one that picks memfs or the real disk on each + * call. Libraries like glob and rimraf keep references to fs functions from + * load time, so the root ava config preloads this before any test file. + */ + +type FsObject = Record; + +const repoRoot = fileURLToPath(new URL('../../../', import.meta.url)); + +let mem: FsObject | undefined; +const memFds = new Set(); + +const toPath = (target: unknown) => { + if (typeof target === 'string') return target; + if (Buffer.isBuffer(target)) return target.toString(); + if (target instanceof URL) return fileURLToPath(target); +}; + +// node_modules folders in the repo stay on the real disk, so that Node's +// module loader (which reads through node:fs) can still load dependencies +// lazily. Mocked trees can still hold their own node_modules elsewhere. +const useReal = (target: unknown) => { + if (!mem) return true; + if (typeof target === 'number') return !memFds.has(target); + const p = toPath(target); + if (p === undefined) return false; + const abs = path.resolve(p); + return ( + abs.startsWith(repoRoot) && + abs.slice(repoRoot.length).split(path.sep).includes('node_modules') + ); +}; + +const exdev = (syscall: string, from: unknown, to: unknown) => + Object.assign( + new Error( + `EXDEV: cross-device link not permitted, ${syscall} '${from}' -> '${to}'` + ), + { code: 'EXDEV', syscall } + ); + +// Two-path calls go to the destination's side. A copyFile across sides +// copies the bytes; anything else across sides fails like a cross-device move. +const twoPath = new Set(['copyFile', 'cp', 'rename', 'link', 'symlink']); + +const crossSides = (name: string, [src, dest, mode]: any[]) => { + if (name !== 'copyFile') throw exdev(name, src, dest); + const excl = typeof mode === 'number' && mode & fs.constants.COPYFILE_EXCL; + fs.writeFileSync(dest, fs.readFileSync(src), { flag: excl ? 'wx' : 'w' }); +}; + +const dispatch = ( + target: FsObject, + key: string, + getMem: () => FsObject | undefined, + isPromise: boolean +) => { + const real = target[key]; + const name = key.replace(/Sync$/, ''); + const sync = key.endsWith('Sync'); + + const fn: any = (...args: any[]) => { + if (!mem) return real(...args); + if (twoPath.has(name)) { + const destReal = useReal(args[1]); + if (name !== 'symlink' && useReal(args[0]) !== destReal) { + if (sync) return crossSides(name, args); + const done = new Promise((resolve) => + resolve(crossSides(name, args)) + ); + if (isPromise) return done; + const cb = args.at(-1); + done.then(() => cb(null), cb); + return; + } + return destReal ? real(...args) : getMem()![key](...args); + } + if (useReal(args[0])) return real(...args); + + const memFn = getMem()![key]; + if (isPromise && key === 'open') { + return memFn(...args).then((handle: any) => { + memFds.add(handle.fd); + return handle; + }); + } + if (key === 'open' && typeof args.at(-1) === 'function') { + const cb = args.pop(); + return memFn(...args, (err: Error | null, fd: number) => { + if (!err) memFds.add(fd); + cb(err, fd); + }); + } + const result = memFn(...args); + if (key === 'openSync') memFds.add(result); + if (name === 'close') memFds.delete(args[0]); + return result; + }; + + // Keeps util.promisify working: customPromisifyArgs on read and write, + // and promisify.custom on exists (whose callback takes no error) + for (const sym of Object.getOwnPropertySymbols(real)) fn[sym] = real[sym]; + if (key === 'exists') { + fn[promisify.custom] = (p: unknown) => + new Promise((resolve) => fn(p, resolve)); + } + if (real.native) { + fn.native = (...args: any[]) => + useReal(args[0]) ? real.native(...args) : getMem()![key](...args); + } + target[key] = fn; +}; + +// Classes like fs.ReadStream and getter-only exports like fs.Utf8Stream +// are left alone +const functionKeys = (obj: FsObject) => + Object.keys(obj).filter( + (key) => + /^[a-z]/.test(key) && + typeof obj[key] === 'function' && + Object.getOwnPropertyDescriptor(obj, key)?.writable + ); + +const realFs = fs as unknown as FsObject; +const realPromises = fs.promises as unknown as FsObject; +for (const key of functionKeys(realFs)) { + dispatch(realFs, key, () => mem, false); +} +for (const key of functionKeys(realPromises)) { + dispatch(realPromises, key, () => mem?.promises, true); +} +syncBuiltinESMExports(); + +// Mocks the filesystem with only `tree`, plus the cwd and tmpdir. Relative +// paths in `tree` resolve against the cwd. +export const mockFs = (tree: NestedDirectoryJSON = {}) => { + if (mem) { + throw new Error('mockFs called while already mocked: call restoreFs first'); + } + const { fs: next } = memfs(tree, process.cwd()); + next.mkdirSync(process.cwd(), { recursive: true }); + next.mkdirSync(os.tmpdir(), { recursive: true }); + memFds.clear(); + mem = next as unknown as FsObject; +}; + +export const restoreFs = () => { + mem = undefined; +}; + +const readReal = (target: string): NestedDirectoryJSON | Buffer => { + if (!fs.statSync(target).isDirectory()) { + return fs.readFileSync(target); + } + const dir: NestedDirectoryJSON = {}; + for (const name of fs.readdirSync(target)) { + dir[name] = readReal(path.join(target, name)); + } + return dir; +}; + +// Reads a real file or directory into a tree for mockFs, even while mocked +export const loadPath = (target: string): NestedDirectoryJSON | Buffer => { + const current = mem; + mem = undefined; + try { + return readReal(target); + } finally { + mem = current; + } +}; diff --git a/packages/ws-worker/package.json b/packages/ws-worker/package.json index 9a07513dc..6d5fead96 100644 --- a/packages/ws-worker/package.json +++ b/packages/ws-worker/package.json @@ -29,7 +29,7 @@ "@openfn/lexicon": "workspace:^", "@openfn/logger": "workspace:*", "@openfn/runtime": "workspace:*", - "@sentry/node": "^10.44.0", + "@sentry/node": "^11.8.0", "fast-safe-stringify": "^2.1.1", "figures": "^5.0.0", "human-id": "^4.1.3", diff --git a/packages/ws-worker/test/api/execute.test.ts b/packages/ws-worker/test/api/execute.test.ts index dbce02066..7210a570a 100644 --- a/packages/ws-worker/test/api/execute.test.ts +++ b/packages/ws-worker/test/api/execute.test.ts @@ -72,6 +72,12 @@ test('send event should throw if an event errors', async (t) => { await t.throwsAsync(() => sendEvent(context, 'throw', 22), { message: '[throw] Error: err', }); + + const report = await waitForSentryReport( + testkit, + (r: any) => r.tags.lightning_event === 'throw' + ); + t.is(report.error.message, '[throw] Error: err'); }); test('jobLog should send a log event outside a run', async (t) => { @@ -300,12 +306,12 @@ test('loadDataclip report to sentry on fail', async (t) => { await loadDataclip(context, 'xyz'); } catch (e) {} - const reports = await waitForSentryReport(testkit); - const dataclip_report = reports.find((r: any) => - /fetch\:dataclip.+not_found/i.test(r.error.message) + const report = await waitForSentryReport( + testkit, + (r: any) => r.tags.lightning_event === GET_DATACLIP ); - t.truthy(dataclip_report); - t.is(dataclip_report.error.name, 'LightningSocketError'); + t.regex(report.error.message, /fetch\:dataclip.+not_found/i); + t.is(report.error.name, 'LightningSocketError'); }); test('loadCredential should fetch a credential', async (t) => { @@ -333,9 +339,11 @@ test('loadCredential report to sentry on fail', async (t) => { await loadCredential(context, 'abc'); } catch (e) {} - const reports = await waitForSentryReport(testkit); - t.is(reports.length, 1); - t.is(reports[0].error.name, 'LightningSocketError'); + const isCredentialReport = (r: any) => + r.tags.lightning_event === GET_CREDENTIAL; + const report = await waitForSentryReport(testkit, isCredentialReport); + t.is(testkit.reports().filter(isCredentialReport).length, 1); + t.is(report.error.name, 'LightningSocketError'); }); test('execute should pass the final result to onFinish', async (t) => { diff --git a/packages/ws-worker/test/util.ts b/packages/ws-worker/test/util.ts index 5fcf1e63a..7f933a572 100644 --- a/packages/ws-worker/test/util.ts +++ b/packages/ws-worker/test/util.ts @@ -13,23 +13,28 @@ export const initSentry = () => { Sentry.init({ dsn: 'https://296274784378f87245c369278a62b29a@o55451.ingest.us.sentry.io/4508936084848640', transport: sentryTransport, + // Dedupe drops an event identical to the previous one, so a test that + // repeats an earlier test's error would never see its report + integrations: (defaults) => defaults.filter((i) => i.name !== 'Dedupe'), }); return testkit; }; -// Telemetry will asynchronously submit the report in the background -// We have to wait for it here in this nasty loop :( -export const waitForSentryReport = async (testkit: any, max = 500) => { +// Sentry submits reports in the background, and a capture from an earlier +// test can land after the testkit is reset, so poll for the report you expect +export const waitForSentryReport = async ( + testkit: any, + match: (report: any) => boolean, + max = 500 +) => { for (let i = 0; i < max; i++) { - if (i >= max) { - throw new Error('SENTRY_REPORT_TIMEOUT'); - } - const reports = testkit.reports(); - if (reports.length) { - return reports; + const report = testkit.reports().find(match); + if (report) { + return report; } await sleep(1); } + throw new Error('SENTRY_REPORT_TIMEOUT'); }; export const wait = (fn: () => any, maxRuns = 100) => diff --git a/packages/ws-worker/test/util/send-event.test.ts b/packages/ws-worker/test/util/send-event.test.ts index 737e319ee..c9168d6bb 100644 --- a/packages/ws-worker/test/util/send-event.test.ts +++ b/packages/ws-worker/test/util/send-event.test.ts @@ -250,7 +250,7 @@ test.serial('should log if the event is rejected', async (t) => { }); test.serial('should report to sentry if the event is rejected', async (t) => { - const EVENT_NAME = 'test'; + const EVENT_NAME = 'rejected'; const channel = mockChannel({ [EVENT_NAME]: () => { throw new Error('err'); @@ -273,12 +273,15 @@ test.serial('should report to sentry if the event is rejected', async (t) => { t.true(e.reportedToSentry); } - const reports = await waitForSentryReport(testkit); - t.is(reports[0].error?.name, 'LightningSocketError'); + const report = await waitForSentryReport( + testkit, + (r: any) => r.tags.lightning_event === EVENT_NAME + ); + t.is(report.error?.name, 'LightningSocketError'); }); test.serial('should report to sentry if the event timesout', async (t) => { - const EVENT_NAME = 'test'; + const EVENT_NAME = 'timesout'; const channel = mockChannel({}); const context = { @@ -296,13 +299,16 @@ test.serial('should report to sentry if the event timesout', async (t) => { } catch (e: any) { t.true(e.reportedToSentry); } - const reports = await waitForSentryReport(testkit); - t.is(reports[0].error?.name, 'LightningTimeoutError'); + const report = await waitForSentryReport( + testkit, + (r: any) => r.tags.lightning_event === EVENT_NAME + ); + t.is(report.error?.name, 'LightningTimeoutError'); // Tags are indexed (unlike the run context below), so these are what make // it possible to ask sentry "is it only step:complete that times out?" - t.is(reports[0].tags.run_id, 'x'); - t.is(reports[0].tags.lightning_event, EVENT_NAME); + t.is(report.tags.run_id, 'x'); + t.is(report.tags.lightning_event, EVENT_NAME); }); test.serial( @@ -320,9 +326,12 @@ test.serial( sendEvent({ id: 'x', channel, logger, options: {} }, 'step:complete', {}) ); - const reports = await waitForSentryReport(testkit); - t.is(reports[0].extra?.channel_state, 'errored'); - t.is(reports[0].extra?.socket_state, 'connecting'); + const report = await waitForSentryReport( + testkit, + (r: any) => r.tags.lightning_event === 'step:complete' + ); + t.is(report.extra?.channel_state, 'errored'); + t.is(report.extra?.socket_state, 'connecting'); } ); @@ -337,14 +346,17 @@ test.serial('should report to sentry against the run scope', async (t) => { await t.throwsAsync(() => sendEvent(context, 'step:complete', {})); stop(); - const reports = await waitForSentryReport(testkit); - t.is(reports[0].error?.name, 'LightningSocketError'); - t.is(reports[0].tags.run_id, 'run-1'); + const report = await waitForSentryReport( + testkit, + (r: any) => r.tags.run_id === 'run-1' + ); + t.is(report.error?.name, 'LightningSocketError'); + t.is(report.tags.run_id, 'run-1'); // The run's breadcrumb trail must survive too - this is why the capture // re-enters the scope rather than passing it to captureException, which // merges tags but drops breadcrumbs - const trail = reports[0].originalReport?.breadcrumbs ?? []; + const trail = report.originalReport?.breadcrumbs ?? []; t.true(trail.some((b: any) => b.message === 'job-complete')); }); @@ -353,10 +365,7 @@ test.serial( async (t) => { // Without this, every timeout for every event collapses into one sentry // issue - this is the change that would have made the step:complete - // pattern visible without digging through raw events. Each event is - // checked against a fresh testkit so the two reports cannot be confused - // with each other or raced against waitForSentryReport's "at least one" - // polling. + // pattern visible without digging through raw events. const channelA = mockChannel({}); await t.throwsAsync(() => sendEvent( @@ -365,14 +374,15 @@ test.serial( {} ) ); - const [stepReport] = await waitForSentryReport(testkit); + const stepReport = await waitForSentryReport( + testkit, + (r: any) => r.tags.lightning_event === 'step:complete' + ); t.deepEqual(stepReport.originalReport.fingerprint, [ 'LightningTimeoutError', 'step:complete', ]); - testkit.reset(); - const channelB = mockChannel({}); await t.throwsAsync(() => sendEvent( @@ -381,7 +391,10 @@ test.serial( {} ) ); - const [runReport] = await waitForSentryReport(testkit); + const runReport = await waitForSentryReport( + testkit, + (r: any) => r.tags.lightning_event === 'run:complete' + ); t.deepEqual(runReport.originalReport.fingerprint, [ 'LightningTimeoutError', 'run:complete', @@ -392,7 +405,7 @@ test.serial( test.serial( 'should report caller-supplied sentryExtras alongside a failed event', async (t) => { - const EVENT_NAME = 'test'; + const EVENT_NAME = 'extras'; const channel = { ...mockChannel({}), state: 'joined' }; const context = { id: 'x', channel, logger, options: {} }; @@ -406,9 +419,21 @@ test.serial( ) ); - const reports = await waitForSentryReport(testkit); - t.is(reports[0].extra?.payloadSize_b, 1536); + const report = await waitForSentryReport( + testkit, + (r: any) => r.tags.lightning_event === EVENT_NAME + ); + t.is(report.extra?.payloadSize_b, 1536); // sentryExtras must not crowd out the fields send-event already reports - t.is(reports[0].extra?.channel_state, 'joined'); + t.is(report.extra?.channel_state, 'joined'); + } +); + +test.serial( + 'waitForSentryReport should throw if no report matches in time', + async (t) => { + await t.throwsAsync(() => waitForSentryReport(testkit, () => false, 5), { + message: 'SENTRY_REPORT_TIMEOUT', + }); } ); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4a741c902..fdcf6ff6f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -174,10 +174,10 @@ importers: version: 2.30.0(@types/node@25.6.0) '@slack/web-api': specifier: ^7.16.0 - version: 7.16.0 + version: 7.16.0(debug@4.4.3(supports-color@7.2.0))(supports-color@7.2.0) '@swc-node/register': specifier: ^1.11.1 - version: 1.11.1(@swc/core@1.15.18)(@swc/types@0.1.25)(typescript@5.9.3) + version: 1.11.1(@swc/core@1.15.18)(@swc/types@0.1.25)(supports-color@7.2.0)(typescript@5.9.3) '@types/gunzip-maybe': specifier: ^1.4.3 version: 1.4.3 @@ -219,7 +219,7 @@ importers: version: 18.19.130 ava: specifier: 5.3.1 - version: 5.3.1 + version: 5.3.1(supports-color@7.2.0) date-fns: specifier: ^2.30.0 version: 2.30.0 @@ -256,7 +256,7 @@ importers: version: 18.19.130 ava: specifier: 5.3.1 - version: 5.3.1 + version: 5.3.1(supports-color@7.2.0) date-fns: specifier: ^2.30.0 version: 2.30.0 @@ -281,7 +281,7 @@ importers: version: 18.19.130 ava: specifier: 5.3.1 - version: 5.3.1 + version: 5.3.1(supports-color@7.2.0) date-fns: specifier: ^2.30.0 version: 2.30.0 @@ -318,13 +318,13 @@ importers: version: 3.0.2 ava: specifier: 5.3.1 - version: 5.3.1 + version: 5.3.1(supports-color@7.2.0) date-fns: specifier: ^2.30.0 version: 2.30.0 koa: specifier: ^2.16.4 - version: 2.16.4 + version: 2.16.4(supports-color@7.2.0) rimraf: specifier: ^6.1.3 version: 6.1.3 @@ -343,7 +343,7 @@ importers: dependencies: ava: specifier: ^5.3.1 - version: 5.3.1 + version: 5.3.1(supports-color@7.2.0) packages/cli: dependencies: @@ -411,15 +411,15 @@ importers: '@openfn/language-common': specifier: 3.2.3 version: 3.2.3 + '@openfn/test-helpers': + specifier: workspace:* + version: link:../test-helpers '@types/json-diff': specifier: ^1.0.3 version: 1.0.3 '@types/lodash-es': specifier: ~4.17.12 version: 4.17.12 - '@types/mock-fs': - specifier: ^4.13.4 - version: 4.13.4 '@types/node': specifier: ^18.19.130 version: 18.19.130 @@ -437,19 +437,16 @@ importers: version: 17.0.35 ava: specifier: 5.3.1 - version: 5.3.1 + version: 5.3.1(supports-color@7.2.0) lodash-es: specifier: ^4.17.23 version: 4.17.23 - mock-fs: - specifier: ^5.5.0 - version: 5.5.0 tslib: specifier: ^2.8.1 version: 2.8.1 tsup: specifier: ^8.5.1 - version: 8.5.1(@swc/core@1.15.18)(jiti@1.21.7)(postcss@8.5.8)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2) + version: 8.5.1(@swc/core@1.15.18)(jiti@1.21.7)(postcss@8.5.8)(supports-color@7.2.0)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -483,13 +480,13 @@ importers: version: 17.0.35 ava: specifier: 5.3.1 - version: 5.3.1 + version: 5.3.1(supports-color@7.2.0) tslib: specifier: ^2.8.1 version: 2.8.1 tsup: specifier: ^8.5.1 - version: 8.5.1(@swc/core@1.15.18)(jiti@1.21.7)(postcss@8.5.8)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2) + version: 8.5.1(@swc/core@1.15.18)(jiti@1.21.7)(postcss@8.5.8)(supports-color@7.2.0)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -518,33 +515,30 @@ importers: '@inquirer/testing': specifier: ^2.1.53 version: 2.1.53(@types/node@18.19.130) + '@openfn/test-helpers': + specifier: workspace:* + version: link:../test-helpers '@types/json-diff': specifier: ^1.0.3 version: 1.0.3 '@types/jsonpath': specifier: ^0.2.4 version: 0.2.4 - '@types/mock-fs': - specifier: ^4.13.4 - version: 4.13.4 '@types/node': specifier: ^18.19.130 version: 18.19.130 ava: specifier: 5.3.1 - version: 5.3.1 + version: 5.3.1(supports-color@7.2.0) jsonpath: specifier: ^1.3.0 version: 1.3.0 - mock-fs: - specifier: ^5.5.0 - version: 5.5.0 tslib: specifier: ^2.8.1 version: 2.8.1 tsup: specifier: ^8.5.1 - version: 8.5.1(@swc/core@1.15.18)(jiti@1.21.7)(postcss@8.5.8)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2) + version: 8.5.1(@swc/core@1.15.18)(jiti@1.21.7)(postcss@8.5.8)(supports-color@7.2.0)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -553,7 +547,7 @@ importers: dependencies: '@typescript/vfs': specifier: ^1.6.4 - version: 1.6.4(typescript@5.9.3) + version: 1.6.4(supports-color@7.2.0)(typescript@5.9.3) cross-fetch: specifier: ^3.2.0 version: 3.2.0(encoding@0.1.13) @@ -578,7 +572,7 @@ importers: version: 3.0.2 ava: specifier: 5.3.1 - version: 5.3.1 + version: 5.3.1(supports-color@7.2.0) esbuild: specifier: ^0.18.20 version: 0.18.20 @@ -587,7 +581,7 @@ importers: version: 6.1.3 threads: specifier: 1.7.0 - version: 1.7.0 + version: 1.7.0(supports-color@7.2.0) tslib: specifier: ^2.8.1 version: 2.8.1 @@ -596,7 +590,7 @@ importers: version: 2.3.0 tsup: specifier: ^8.5.1 - version: 8.5.1(@swc/core@1.15.18)(jiti@1.21.7)(postcss@8.5.8)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2) + version: 8.5.1(@swc/core@1.15.18)(jiti@1.21.7)(postcss@8.5.8)(supports-color@7.2.0)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2) packages/engine-multi: dependencies: @@ -624,7 +618,7 @@ importers: version: 18.19.130 ava: specifier: 5.3.1 - version: 5.3.1 + version: 5.3.1(supports-color@7.2.0) tslib: specifier: ^2.8.1 version: 2.8.1 @@ -633,7 +627,7 @@ importers: version: 2.3.0 tsup: specifier: ^8.5.1 - version: 8.5.1(@swc/core@1.15.18)(jiti@1.21.7)(postcss@8.5.8)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2) + version: 8.5.1(@swc/core@1.15.18)(jiti@1.21.7)(postcss@8.5.8)(supports-color@7.2.0)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2) tsx: specifier: ^4.21.0 version: 4.21.0 @@ -655,10 +649,10 @@ importers: dependencies: '@koa/bodyparser': specifier: ^6.1.0 - version: 6.1.0(koa@2.16.4) + version: 6.1.0(koa@2.16.4(supports-color@7.2.0)) '@koa/router': specifier: ^15.4.0 - version: 15.4.0(koa@2.16.4) + version: 15.4.0(koa@2.16.4(supports-color@7.2.0))(supports-color@7.2.0) '@openfn/engine-multi': specifier: workspace:* version: link:../engine-multi @@ -691,7 +685,7 @@ importers: version: 4.15.9 koa: specifier: ^2.16.4 - version: 2.16.4 + version: 2.16.4(supports-color@7.2.0) koa-logger: specifier: ^4.0.0 version: 4.0.0 @@ -725,10 +719,10 @@ importers: version: 17.0.35 ava: specifier: 5.3.1 - version: 5.3.1 + version: 5.3.1(supports-color@7.2.0) koa-websocket: specifier: ^7.0.0 - version: 7.0.0 + version: 7.0.0(supports-color@7.2.0) query-string: specifier: ^8.2.0 version: 8.2.0 @@ -737,7 +731,7 @@ importers: version: 2.8.1 tsup: specifier: ^8.5.1 - version: 8.5.1(@swc/core@1.15.18)(jiti@1.21.7)(postcss@8.5.8)(tsx@4.21.0)(typescript@4.9.5)(yaml@2.8.2) + version: 8.5.1(@swc/core@1.15.18)(jiti@1.21.7)(postcss@8.5.8)(supports-color@7.2.0)(tsx@4.21.0)(typescript@4.9.5)(yaml@2.8.2) typescript: specifier: ^4.9.5 version: 4.9.5 @@ -765,13 +759,13 @@ importers: version: 18.19.130 ava: specifier: 5.3.1 - version: 5.3.1 + version: 5.3.1(supports-color@7.2.0) tslib: specifier: ^2.8.1 version: 2.8.1 tsup: specifier: ^8.5.1 - version: 8.5.1(@swc/core@1.15.18)(jiti@1.21.7)(postcss@8.5.8)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2) + version: 8.5.1(@swc/core@1.15.18)(jiti@1.21.7)(postcss@8.5.8)(supports-color@7.2.0)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -806,18 +800,15 @@ importers: specifier: ^2.8.2 version: 2.8.2 devDependencies: + '@openfn/test-helpers': + specifier: workspace:* + version: link:../test-helpers '@types/lodash': specifier: ^4.17.24 version: 4.17.24 - '@types/mock-fs': - specifier: ~4.13.4 - version: 4.13.4 ava: specifier: 5.3.1 - version: 5.3.1 - mock-fs: - specifier: ^5.5.0 - version: 5.5.0 + version: 5.3.1(supports-color@7.2.0) rimraf: specifier: ^6.1.3 version: 6.1.3 @@ -826,7 +817,7 @@ importers: version: 2.8.1 tsup: specifier: ^8.5.1 - version: 8.5.1(@swc/core@1.15.18)(jiti@1.21.7)(postcss@8.5.8)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2) + version: 8.5.1(@swc/core@1.15.18)(jiti@1.21.7)(postcss@8.5.8)(supports-color@7.2.0)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -858,9 +849,9 @@ importers: '@openfn/lexicon': specifier: workspace:^ version: link:../lexicon - '@types/mock-fs': - specifier: ^4.13.4 - version: 4.13.4 + '@openfn/test-helpers': + specifier: workspace:* + version: link:../test-helpers '@types/node': specifier: ^18.19.130 version: 18.19.130 @@ -869,10 +860,7 @@ importers: version: 7.7.1 ava: specifier: 5.3.1 - version: 5.3.1 - mock-fs: - specifier: ^5.5.0 - version: 5.5.0 + version: 5.3.1(supports-color@7.2.0) recast: specifier: ^0.21.5 version: 0.21.5 @@ -884,19 +872,25 @@ importers: version: 2.8.1 tsup: specifier: ^8.5.1 - version: 8.5.1(@swc/core@1.15.18)(jiti@1.21.7)(postcss@8.5.8)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2) + version: 8.5.1(@swc/core@1.15.18)(jiti@1.21.7)(postcss@8.5.8)(supports-color@7.2.0)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2) typescript: specifier: ^5.9.3 version: 5.9.3 + packages/test-helpers: + dependencies: + memfs: + specifier: ^4.80.0 + version: 4.80.0 + packages/ws-worker: dependencies: '@koa/bodyparser': specifier: ^6.1.0 - version: 6.1.0(koa@2.16.4) + version: 6.1.0(koa@2.16.4(supports-color@5.5.0)) '@koa/router': specifier: ^15.4.0 - version: 15.4.0(koa@2.16.4) + version: 15.4.0(koa@2.16.4(supports-color@5.5.0))(supports-color@5.5.0) '@openfn/engine-multi': specifier: workspace:* version: link:../engine-multi @@ -910,8 +904,8 @@ importers: specifier: workspace:* version: link:../runtime '@sentry/node': - specifier: ^10.44.0 - version: 10.44.0 + specifier: ^11.8.0 + version: 11.8.0(rollup@4.59.0) fast-safe-stringify: specifier: ^2.1.1 version: 2.1.1 @@ -926,7 +920,7 @@ importers: version: 4.15.9 koa: specifier: ^2.16.4 - version: 2.16.4 + version: 2.16.4(supports-color@5.5.0) koa-logger: specifier: ^3.2.1 version: 3.2.1 @@ -966,19 +960,19 @@ importers: version: 17.0.35 ava: specifier: 5.3.1 - version: 5.3.1 + version: 5.3.1(supports-color@5.5.0) nodemon: specifier: 3.1.14 version: 3.1.14 sentry-testkit: specifier: ^6.2.2 - version: 6.2.2 + version: 6.2.2(supports-color@5.5.0) tslib: specifier: ^2.8.1 version: 2.8.1 tsup: specifier: ^8.5.1 - version: 8.5.1(@swc/core@1.15.18)(jiti@1.21.7)(postcss@8.5.8)(tsx@4.21.0)(typescript@4.9.5)(yaml@2.8.2) + version: 8.5.1(@swc/core@1.15.18)(jiti@1.21.7)(postcss@8.5.8)(supports-color@5.5.0)(tsx@4.21.0)(typescript@4.9.5)(yaml@2.8.2) typescript: specifier: ^4.9.5 version: 4.9.5 @@ -1367,11 +1361,6 @@ packages: resolution: {integrity: sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==} engines: {node: '>=14'} - '@fastify/otel@0.17.1': - resolution: {integrity: sha512-K4wyxfUZx2ux5o+b6BtTqouYFVILohLZmSbA2tKUueJstNcBnoGPVhllCaOvbQ3ZrXdUxUC/fyrSWSCqHhdOPg==} - peerDependencies: - '@opentelemetry/api': ^1.9.0 - '@hapi/bourne@3.0.0': resolution: {integrity: sha512-Waj1cwPXJDucOib4a3bAISsKJVb15MKi9IvmTI/7ssVEm6sywXGjVJDhl6/umt1pK1ZS7PacXU3A1PmFKHEZ2w==} @@ -1486,6 +1475,126 @@ packages: peerDependencies: jsep: ^0.4.0||^1.0.0 + '@jsonjoy.com/base64@1.1.2': + resolution: {integrity: sha512-q6XAnWQDIMA3+FTiOYajoYqySkO+JSat0ytXGSuRdq9uXE7o92gzuQwQM14xaCRlBLGq3v5miDGC4vkVTn54xA==} + engines: {node: '>=10.0'} + peerDependencies: + tslib: '2' + + '@jsonjoy.com/base64@17.67.0': + resolution: {integrity: sha512-5SEsJGsm15aP8TQGkDfJvz9axgPwAEm98S5DxOuYe8e1EbfajcDmgeXXzccEjh+mLnjqEKrkBdjHWS5vFNwDdw==} + engines: {node: '>=10.0'} + peerDependencies: + tslib: '2' + + '@jsonjoy.com/buffers@1.2.1': + resolution: {integrity: sha512-12cdlDwX4RUM3QxmUbVJWqZ/mrK6dFQH4Zxq6+r1YXKXYBNgZXndx2qbCJwh3+WWkCSn67IjnlG3XYTvmvYtgA==} + engines: {node: '>=10.0'} + peerDependencies: + tslib: '2' + + '@jsonjoy.com/buffers@17.67.0': + resolution: {integrity: sha512-tfExRpYxBvi32vPs9ZHaTjSP4fHAfzSmcahOfNxtvGHcyJel+aibkPlGeBB+7AoC6hL7lXIE++8okecBxx7lcw==} + engines: {node: '>=10.0'} + peerDependencies: + tslib: '2' + + '@jsonjoy.com/codegen@1.0.0': + resolution: {integrity: sha512-E8Oy+08cmCf0EK/NMxpaJZmOxPqM+6iSe2S4nlSBrPZOORoDJILxtbSUEDKQyTamm/BVAhIGllOBNU79/dwf0g==} + engines: {node: '>=10.0'} + peerDependencies: + tslib: '2' + + '@jsonjoy.com/codegen@17.67.0': + resolution: {integrity: sha512-idnkUplROpdBOV0HMcwhsCUS5TRUi9poagdGs70A6S4ux9+/aPuKbh8+UYRTLYQHtXvAdNfQWXDqZEx5k4Dj2Q==} + engines: {node: '>=10.0'} + peerDependencies: + tslib: '2' + + '@jsonjoy.com/fs-core@4.80.0': + resolution: {integrity: sha512-qMKWshnyjbyhm+NbzqGE3w5y1mckFFEMj0aDPU2/JBNh1BzIjVy9esq9lBmRnNVmhMEkxwDzLZ8Gqh5G1P5Dag==} + engines: {node: '>=10.0'} + peerDependencies: + tslib: '2' + + '@jsonjoy.com/fs-fsa@4.80.0': + resolution: {integrity: sha512-ZBEKl7J6dbkRCfmrFV48Re211A/FHqniTUhsWCsaRteNSOqMgkX3qF1UreVgwB9oSPm81JLAwhIi6Lb/NH/PoA==} + engines: {node: '>=10.0'} + peerDependencies: + tslib: '2' + + '@jsonjoy.com/fs-node-builtins@4.80.0': + resolution: {integrity: sha512-OIOIhqaWiwUySFMny4epIAoviXsyMaQQ63PTc3zH+5HEOvarl5hpUmZIhp2m5+OfyKAh5ylxXnQIgew1OgJDVw==} + engines: {node: '>=10.0'} + peerDependencies: + tslib: '2' + + '@jsonjoy.com/fs-node-to-fsa@4.80.0': + resolution: {integrity: sha512-ljeaR5xwtX1EyRmB0wvXANUMQZ3mjKcm7z8s2N8D8SLQ04CcAGtUhHFRZQXFb9mhgFyEVMydQGQtSOFS7pcrFQ==} + engines: {node: '>=10.0'} + peerDependencies: + tslib: '2' + + '@jsonjoy.com/fs-node-utils@4.80.0': + resolution: {integrity: sha512-xwHeVVi3f+Khg/Fmb8i5qoYVmQ08QznE5TWhQokUmIZjz4KBPzU+zt/+bahBI6G1ghtqajt27lWuAciIxApuzA==} + engines: {node: '>=10.0'} + peerDependencies: + tslib: '2' + + '@jsonjoy.com/fs-node@4.80.0': + resolution: {integrity: sha512-yiBlUfkMMRFFGy8CD/h2zd5rPnrM6bjlF+nx2LRAxNV6hOSiuObQIUEE7m7y2VTMisjxCRtzINH94bFGySWL7g==} + engines: {node: '>=10.0'} + peerDependencies: + tslib: '2' + + '@jsonjoy.com/fs-print@4.80.0': + resolution: {integrity: sha512-1ocZLJw/m/xlq0moaIHO7HV9nHRVBUvdc2zMzhsJGkFOrqfwWvexfCv5cYCPPLRhP+9p3yunoi4Ep5YhRORC9Q==} + engines: {node: '>=10.0'} + peerDependencies: + tslib: '2' + + '@jsonjoy.com/fs-snapshot@4.80.0': + resolution: {integrity: sha512-3AV23Q1H228KTtJKTUE0nNq8FCtxSvtENOLrEat80wkAJCKHuqQEv8yjhpS8Bei2OEFJAnluGXp9Ri2SWBlROw==} + engines: {node: '>=10.0'} + peerDependencies: + tslib: '2' + + '@jsonjoy.com/json-pack@1.21.0': + resolution: {integrity: sha512-+AKG+R2cfZMShzrF2uQw34v3zbeDYUqnQ+jg7ORic3BGtfw9p/+N6RJbq/kkV8JmYZaINknaEQ2m0/f693ZPpg==} + engines: {node: '>=10.0'} + peerDependencies: + tslib: '2' + + '@jsonjoy.com/json-pack@17.67.0': + resolution: {integrity: sha512-t0ejURcGaZsn1ClbJ/3kFqSOjlryd92eQY465IYrezsXmPcfHPE/av4twRSxf6WE+TkZgLY+71vCZbiIiFKA/w==} + engines: {node: '>=10.0'} + peerDependencies: + tslib: '2' + + '@jsonjoy.com/json-pointer@1.0.2': + resolution: {integrity: sha512-Fsn6wM2zlDzY1U+v4Nc8bo3bVqgfNTGcn6dMgs6FjrEnt4ZCe60o6ByKRjOGlI2gow0aE/Q41QOigdTqkyK5fg==} + engines: {node: '>=10.0'} + peerDependencies: + tslib: '2' + + '@jsonjoy.com/json-pointer@17.67.0': + resolution: {integrity: sha512-+iqOFInH+QZGmSuaybBUNdh7yvNrXvqR+h3wjXm0N/3JK1EyyFAeGJvqnmQL61d1ARLlk/wJdFKSL+LHJ1eaUA==} + engines: {node: '>=10.0'} + peerDependencies: + tslib: '2' + + '@jsonjoy.com/util@1.9.0': + resolution: {integrity: sha512-pLuQo+VPRnN8hfPqUTLTHk126wuYdXVxE6aDmjSeV4NCAgyxWbiOIeNJVtID3h1Vzpoi9m4jXezf73I6LgabgQ==} + engines: {node: '>=10.0'} + peerDependencies: + tslib: '2' + + '@jsonjoy.com/util@17.67.0': + resolution: {integrity: sha512-6+8xBaz1rLSohlGh68D1pdw3AwDi9xydm8QNlAFkvnavCJYSze+pxoW2VKP8p308jtlMRLs5NTHfPlZLd4w7ew==} + engines: {node: '>=10.0'} + peerDependencies: + tslib: '2' + '@koa/bodyparser@6.1.0': resolution: {integrity: sha512-thVG/Utbz9+dB4Nl8EBJKoaOI1DzO74dJqeDFILbAVhUq6C+4rmmrKFwxiDE63ScywOs0CHypx1IUrSRMueFTw==} engines: {node: '>= 18'} @@ -1540,209 +1649,134 @@ packages: '@openfn/language-http@6.4.3': resolution: {integrity: sha512-8ihgIYId+ewMuNU9hbe5JWEWvaJInDrIEiy4EyO7tbzu5t/f1kO18JIzQWm6r7dcHiMfcG2QaXe6O3br1xOrDA==} - '@opentelemetry/api-logs@0.207.0': - resolution: {integrity: sha512-lAb0jQRVyleQQGiuuvCOTDVspc14nx6XJjP4FspJ1sNARo3Regq4ZZbrc3rN4b1TYSuUCvgH+UXUPug4SLOqEQ==} - engines: {node: '>=8.0.0'} - - '@opentelemetry/api-logs@0.212.0': - resolution: {integrity: sha512-TEEVrLbNROUkYY51sBJGk7lO/OLjuepch8+hmpM6ffMJQ2z/KVCjdHuCFX6fJj8OkJP2zckPjrJzQtXU3IAsFg==} - engines: {node: '>=8.0.0'} - - '@opentelemetry/api-logs@0.213.0': - resolution: {integrity: sha512-zRM5/Qj6G84Ej3F1yt33xBVY/3tnMxtL1fiDIxYbDWYaZ/eudVw3/PBiZ8G7JwUxXxjW8gU4g6LnOyfGKYHYgw==} - engines: {node: '>=8.0.0'} - - '@opentelemetry/api@1.9.0': - resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==} + '@opentelemetry/api@1.9.1': + resolution: {integrity: sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==} engines: {node: '>=8.0.0'} - '@opentelemetry/context-async-hooks@2.6.0': - resolution: {integrity: sha512-L8UyDwqpTcbkIK5cgwDRDYDoEhQoj8wp8BwsO19w3LB1Z41yEQm2VJyNfAi9DrLP/YTqXqWpKHyZfR9/tFYo1Q==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': '>=1.0.0 <1.10.0' - - '@opentelemetry/core@2.6.0': - resolution: {integrity: sha512-HLM1v2cbZ4TgYN6KEOj+Bbj8rAKriOdkF9Ed3tG25FoprSiQl7kYc+RRT6fUZGOvx0oMi5U67GoFdT+XUn8zEg==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': '>=1.0.0 <1.10.0' - - '@opentelemetry/instrumentation-amqplib@0.60.0': - resolution: {integrity: sha512-q/B2IvoVXRm1M00MvhnzpMN6rKYOszPXVsALi6u0ss4AYHe+TidZEtLW9N1ZhrobI1dSriHnBqqtAOZVAv07sg==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - - '@opentelemetry/instrumentation-connect@0.56.0': - resolution: {integrity: sha512-PKp+sSZ7AfzMvGgO3VCyo1inwNu+q7A1k9X88WK4PQ+S6Hp7eFk8pie+sWHDTaARovmqq5V2osav3lQej2B0nw==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - - '@opentelemetry/instrumentation-dataloader@0.30.0': - resolution: {integrity: sha512-MXHP2Q38cd2OhzEBKAIXUi9uBlPEYzF6BNJbyjUXBQ6kLaf93kRC41vNMIz0Nl5mnuwK7fDvKT+/lpx7BXRwdg==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - - '@opentelemetry/instrumentation-express@0.61.0': - resolution: {integrity: sha512-Xdmqo9RZuZlL29Flg8QdwrrX7eW1CZ7wFQPKHyXljNymgKhN1MCsYuqQ/7uxavhSKwAl7WxkTzKhnqpUApLMvQ==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - - '@opentelemetry/instrumentation-fs@0.32.0': - resolution: {integrity: sha512-koR6apx0g0wX6RRiPpjA4AFQUQUbXrK16kq4/SZjVp7u5cffJhNkY4TnITxcGA4acGSPYAfx3NHRIv4Khn1axQ==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - - '@opentelemetry/instrumentation-generic-pool@0.56.0': - resolution: {integrity: sha512-fg+Jffs6fqrf0uQS0hom7qBFKsbtpBiBl8+Vkc63Gx8xh6pVh+FhagmiO6oM0m3vyb683t1lP7yGYq22SiDnqg==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - - '@opentelemetry/instrumentation-graphql@0.61.0': - resolution: {integrity: sha512-pUiVASv6nh2XrerTvlbVHh7vKFzscpgwiQ/xvnZuAIzQ5lRjWVdRPUuXbvZJ/Yq79QsE81TZdJ7z9YsXiss1ew==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - - '@opentelemetry/instrumentation-hapi@0.59.0': - resolution: {integrity: sha512-33wa4mEr+9+ztwdgLor1SeBu4Opz4IsmpcLETXAd3VmBrOjez8uQtrsOhPCa5Vhbm5gzDlMYTgFRLQzf8/YHFA==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - - '@opentelemetry/instrumentation-http@0.213.0': - resolution: {integrity: sha512-B978Xsm5XEPGhm1P07grDoaOFLHapJPkOG9h016cJsyWWxmiLnPu2M/4Nrm7UCkHSiLnkXgC+zVGUAIahy8EEA==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - - '@opentelemetry/instrumentation-ioredis@0.61.0': - resolution: {integrity: sha512-hsHDadUtAFbws1YSDc1XW0svGFKiUbqv2td1Cby+UAiwvojm1NyBo/taifH0t8CuFZ0x/2SDm0iuTwrM5pnVOg==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - - '@opentelemetry/instrumentation-kafkajs@0.22.0': - resolution: {integrity: sha512-wJU4IBQMUikdJAcTChLFqK5lo+flo7pahqd8DSLv7uMxsdOdAHj6RzKYAm8pPfUS6ItKYutYyuicwKaFwQKsoA==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.3.0 + '@oxc-parser/binding-android-arm-eabi@0.152.0': + resolution: {integrity: sha512-393VtirINkae4XHBTkq/yQxhOwEr4V+r0et37P2fp+sYZSdYgZTRDPo5Um8vBh0QjMZ+RmaMy4PP0qwr4cTMwA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [android] - '@opentelemetry/instrumentation-knex@0.57.0': - resolution: {integrity: sha512-vMCSh8kolEm5rRsc+FZeTZymWmIJwc40hjIKnXH4O0Dv/gAkJJIRXCsPX5cPbe0c0j/34+PsENd0HqKruwhVYw==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.3.0 + '@oxc-parser/binding-android-arm64@0.152.0': + resolution: {integrity: sha512-iYFcExATJNZ0rZj0o02THbD1Nc9jAW0nVgIQPee6ejbSASpbZXlIMw8jxtpzaG14JQTWwGSZ6U5dEtRJXE6V3Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] - '@opentelemetry/instrumentation-koa@0.61.0': - resolution: {integrity: sha512-lvrfWe9ShK/D2X4brmx8ZqqeWPfRl8xekU0FCn7C1dHm5k6+rTOOi36+4fnaHAP8lig9Ux6XQ1D4RNIpPCt1WQ==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.9.0 + '@oxc-parser/binding-darwin-arm64@0.152.0': + resolution: {integrity: sha512-fnU+DfUzi5/rFy0LzmipYRz06PAUUkOS8kCJQ3Eg6oqlunC1XZXoffFqxmdL51RmcbYtUG3P8DVG1U5fckoa2A==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] - '@opentelemetry/instrumentation-lru-memoizer@0.57.0': - resolution: {integrity: sha512-cEqpUocSKJfwDtLYTTJehRLWzkZ2eoePCxfVIgGkGkb83fMB71O+y4MvRHJPbeV2bdoWdOVrl8uO0+EynWhTEA==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.3.0 + '@oxc-parser/binding-darwin-x64@0.152.0': + resolution: {integrity: sha512-Y3U2okryWTs7hDld6UhjJwLNf8/do+x4g4C+1U5SiGwrTLwbO83WJYJA8+l9iFSilWBgnfpd/+U76tfeNxUHzA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] - '@opentelemetry/instrumentation-mongodb@0.66.0': - resolution: {integrity: sha512-d7m9QnAY+4TCWI4q1QRkfrc6fo/92VwssaB1DzQfXNRvu51b78P+HJlWP7Qg6N6nkwdb9faMZNBCZJfftmszkw==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.3.0 + '@oxc-parser/binding-freebsd-x64@0.152.0': + resolution: {integrity: sha512-XkhkRrrPffEw+ZLv/d/5pEni0buxinm/E/N6fLbJCunD1DBbeCTtVP23ESV+4CWCWdSz397asks1ViGslpmWiQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] - '@opentelemetry/instrumentation-mongoose@0.59.0': - resolution: {integrity: sha512-6/jWU+c1NgznkVLDU/2y0bXV2nJo3o9FWZ9mZ9nN6T/JBNRoMnVXZl2FdBmgH+a5MwaWLs5kmRJTP5oUVGIkPw==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.3.0 + '@oxc-parser/binding-linux-arm-gnueabihf@0.152.0': + resolution: {integrity: sha512-MLzsRRbdfmdzo6JEhbC1yKU9ItfvM8tEf18HM32jCiNYZEz6J3bRBRl3cTkKR0vBdrk9ZVlcARXtwHccs+epDA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] - '@opentelemetry/instrumentation-mysql2@0.59.0': - resolution: {integrity: sha512-n9/xrVCRBfG9egVbffnlU1uhr+HX0vF4GgtAB/Bvm48wpFgRidqD8msBMiym1kRYzmpWvJqTxNT47u1MkgBEdw==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.3.0 + '@oxc-parser/binding-linux-arm-musleabihf@0.152.0': + resolution: {integrity: sha512-HHerTEJz7Iy10Ji6BGsk4gqG6PU0UzFuBu91jIEOX89FnrI5ECWSEr7BLpkYQhp7U0QFtMnBhNfQKI0hYZK78w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] - '@opentelemetry/instrumentation-mysql@0.59.0': - resolution: {integrity: sha512-r+V/Fh0sm7Ga8/zk/TI5H5FQRAjwr0RrpfPf8kNIehlsKf12XnvIaZi8ViZkpX0gyPEpLXqzqWD6QHlgObgzZw==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.3.0 + '@oxc-parser/binding-linux-arm64-gnu@0.152.0': + resolution: {integrity: sha512-9Bb8GyzZ2uSLzRDZ27usaKokr07mxiBe2dDF5b6bd2janmaJTT1gL51UfP7AU30yFiXrJvcRyFKREAgKwZYs0w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] - '@opentelemetry/instrumentation-pg@0.65.0': - resolution: {integrity: sha512-W0zpHEIEuyZ8zvb3njaX9AAbHgPYOsSWVOoWmv1sjVRSF6ZpBqtlxBWbU+6hhq1TFWBeWJOXZ8nZS/PUFpLJYQ==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.3.0 + '@oxc-parser/binding-linux-arm64-musl@0.152.0': + resolution: {integrity: sha512-om7iDMUroI74A/lfRRWY9/Ev3VWrVsrUZ4tyvyh8raBey/sdcfyAD7tYWc+iykAiotnmftyKTpUKFr1QnaBrAQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] - '@opentelemetry/instrumentation-redis@0.61.0': - resolution: {integrity: sha512-JnPexA034/0UJRsvH96B0erQoNOqKJZjE2ZRSw9hiTSC23LzE0nJE/u6D+xqOhgUhRnhhcPHq4MdYtmUdYTF+Q==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.3.0 + '@oxc-parser/binding-linux-ppc64-gnu@0.152.0': + resolution: {integrity: sha512-Lf1OL2lMTqwDMFC6jnRb30B+WYHKV0XT+bPHcFM6xHtNVwxpobiTzUIvKH6a0LWpvfwyru8Yu6zCGHqqFHH+gg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] - '@opentelemetry/instrumentation-tedious@0.32.0': - resolution: {integrity: sha512-BQS6gG8RJ1foEqfEZ+wxoqlwfCAzb1ZVG0ad8Gfe4x8T658HJCLGLd4E4NaoQd8EvPfLqOXgzGaE/2U4ytDSWA==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.3.0 + '@oxc-parser/binding-linux-riscv64-gnu@0.152.0': + resolution: {integrity: sha512-BYWSMQK+YJ9ZILStPcmmiSVq8KEG9Qdq/c4BHPC3rUEq/fEDCVjUIwdj5ZzjgZ9P9is9PKu0tcAXzq1ZXg4urQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + libc: [glibc] - '@opentelemetry/instrumentation-undici@0.23.0': - resolution: {integrity: sha512-LL0VySzKVR2cJSFVZaTYpZl1XTpBGnfzoQPe2W7McS2267ldsaEIqtQY6VXs2KCXN0poFjze5110PIpxHDaDGg==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.7.0 + '@oxc-parser/binding-linux-riscv64-musl@0.152.0': + resolution: {integrity: sha512-d2LqHNNNqdH185sRK5dPKWZu201JrNu9/CRFuVEuT+ht7n/QaVblEMWT9MALQPrDWYvhG10P+NSKJFquDWZ09w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + libc: [musl] - '@opentelemetry/instrumentation@0.207.0': - resolution: {integrity: sha512-y6eeli9+TLKnznrR8AZlQMSJT7wILpXH+6EYq5Vf/4Ao+huI7EedxQHwRgVUOMLFbe7VFDvHJrX9/f4lcwnJsA==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.3.0 + '@oxc-parser/binding-linux-s390x-gnu@0.152.0': + resolution: {integrity: sha512-8gBeHt5rA/8+pKhHl+zLggfa3L/V/L2at96p7ty6pBE7id6lFO19nFUc15u2PcjC4OYeYK465MKy1fhhbpFIGw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] - '@opentelemetry/instrumentation@0.212.0': - resolution: {integrity: sha512-IyXmpNnifNouMOe0I/gX7ENfv2ZCNdYTF0FpCsoBcpbIHzk81Ww9rQTYTnvghszCg7qGrIhNvWC8dhEifgX9Jg==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.3.0 + '@oxc-parser/binding-linux-x64-gnu@0.152.0': + resolution: {integrity: sha512-ltgrSF6L+yPTVsIdaAyqKq3zTqzj/1VlNbxWZuxAe8flR3sWdyNsKvdXukoO8/ucJbLickhmieRBge+o/e8Ugw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] - '@opentelemetry/instrumentation@0.213.0': - resolution: {integrity: sha512-3i9NdkET/KvQomeh7UaR/F4r9P25Rx6ooALlWXPIjypcEOUxksCmVu0zA70NBJWlrMW1rPr/LRidFAflLI+s/w==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.3.0 + '@oxc-parser/binding-linux-x64-musl@0.152.0': + resolution: {integrity: sha512-LcMp6BvrBnewSfdkLHoSqidhoAq4syKOTBV5TYHWd+2VnGkfD/nPRRVIzTBt/Lkbt7pfOFDGtkiTntFS0p6sQQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] - '@opentelemetry/redis-common@0.38.2': - resolution: {integrity: sha512-1BCcU93iwSRZvDAgwUxC/DV4T/406SkMfxGqu5ojc3AvNI+I9GhV7v0J1HljsczuuhcnFLYqD5VmwVXfCGHzxA==} - engines: {node: ^18.19.0 || >=20.6.0} + '@oxc-parser/binding-openharmony-arm64@0.152.0': + resolution: {integrity: sha512-jbhoMCslVxzRCRNQ8TSHxjYdSKRvk8O6ciLVDkahJvmyHDzBqba/bMQHM6ULKj/rgHNNbSzlIiakdx8SEfkJzA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] - '@opentelemetry/resources@2.6.0': - resolution: {integrity: sha512-D4y/+OGe3JSuYUCBxtH5T9DSAWNcvCb/nQWIga8HNtXTVPQn59j0nTBAgaAXxUVBDl40mG3Tc76b46wPlZaiJQ==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': '>=1.3.0 <1.10.0' + '@oxc-parser/binding-win32-arm64-msvc@0.152.0': + resolution: {integrity: sha512-JZYVlhv36mr/nertnRB7vchJECOBb1Lz6lltalILsB+o9M/is+H6BEDRLQKdqA4JKs6NjJszo46+iM5sfwj6jQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] - '@opentelemetry/sdk-trace-base@2.6.0': - resolution: {integrity: sha512-g/OZVkqlxllgFM7qMKqbPV9c1DUPhQ7d4n3pgZFcrnrNft9eJXZM2TNHTPYREJBrtNdRytYyvwjgL5geDKl3EQ==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': '>=1.3.0 <1.10.0' + '@oxc-parser/binding-win32-ia32-msvc@0.152.0': + resolution: {integrity: sha512-1X9oganvVsunRH0lH10UU05N+H9atpX6WsOybByUjwnfcd3h/0tYwvuv16XuHglrIUOnxV2TodbLVEwwafd64g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ia32] + os: [win32] - '@opentelemetry/semantic-conventions@1.40.0': - resolution: {integrity: sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw==} - engines: {node: '>=14'} + '@oxc-parser/binding-win32-x64-msvc@0.152.0': + resolution: {integrity: sha512-sw0M80/dn9rcpu+Rqqph/C+MjvDDce6MlXmFjsNQtPEymha0eJy3ml6+foSUTqFfpDIDyHZZ4eoJ43IoMe8bJw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] - '@opentelemetry/sql-common@0.41.2': - resolution: {integrity: sha512-4mhWm3Z8z+i508zQJ7r6Xi7y4mmoJpdvH0fZPFRkWrdp5fq7hhZ2HhYokEOLkfqSMgPR4Z9EyB3DBkbKGOqZiQ==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.1.0 + '@oxc-project/types@0.152.0': + resolution: {integrity: sha512-oM/5rLBm2tPkg0iBgkH/FOeR3PCDpY19GTgAZjMFM8h9WI9VW7cLgzp6nwtarYKmovavIQZ+Fe/RKX/8C8O/Rw==} '@oxc-resolver/binding-android-arm-eabi@11.19.1': resolution: {integrity: sha512-aUs47y+xyXHUKlbhqHUjBABjvycq6YSD7bpxSW7vplUmdzAlJ93yXY6ZR0c1o1x5A/QKbENCvs3+NlY8IpIVzg==} @@ -1852,11 +1886,6 @@ packages: cpu: [x64] os: [win32] - '@prisma/instrumentation@7.4.2': - resolution: {integrity: sha512-r9JfchJF1Ae6yAxcaLu/V1TGqBhAuSDe3mRNOssBfx1rMzfZ4fdNvrgUBwyb/TNTGXFxlH9AZix5P257x07nrg==} - peerDependencies: - '@opentelemetry/api': ^1.8 - '@rollup/rollup-android-arm-eabi@4.59.0': resolution: {integrity: sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==} cpu: [arm] @@ -1995,50 +2024,46 @@ packages: cpu: [x64] os: [win32] - '@sentry/core@10.44.0': - resolution: {integrity: sha512-aa7CiDaNFZvHpqd97LJhuskolfJ/4IH5xyuVVLnv7l6B0v9KTwskPUxb0tH1ej3FxuzfH+i8iTiTFuqpfHS3QA==} - engines: {node: '>=18'} - - '@sentry/node-core@10.44.0': - resolution: {integrity: sha512-jUGsadMrvZ08UMbqJBfjFFMk1k3VbyxfUypf0iDGGgyLmuHotYQPo/5aND+o2KxMDXR60LwcQrMoZFpanK6jXQ==} - engines: {node: '>=18'} + '@sentry/bundler-plugins@11.8.0': + resolution: {integrity: sha512-rQrbL2lYPyiiO6ngdpbbfPKyEzVVQsz1FG+aE8jUiUYs72gvmDmZmRyl9VDCGLrHZfIEwaQMT1t6WQH5nopu5A==} + engines: {node: '>=20.19.0 <22.0.0 || >=22.12.0 <23.0.0 || >=23.2.0'} peerDependencies: - '@opentelemetry/api': ^1.9.0 - '@opentelemetry/context-async-hooks': ^1.30.1 || ^2.1.0 - '@opentelemetry/core': ^1.30.1 || ^2.1.0 - '@opentelemetry/instrumentation': '>=0.57.1 <1' - '@opentelemetry/resources': ^1.30.1 || ^2.1.0 - '@opentelemetry/sdk-trace-base': ^1.30.1 || ^2.1.0 - '@opentelemetry/semantic-conventions': ^1.39.0 + '@babel/core': ^7.18.5 + rollup: '>=3.2.0' + webpack: '>=5.1.0' peerDependenciesMeta: - '@opentelemetry/api': - optional: true - '@opentelemetry/context-async-hooks': + '@babel/core': optional: true - '@opentelemetry/core': + rollup: optional: true - '@opentelemetry/instrumentation': - optional: true - '@opentelemetry/resources': - optional: true - '@opentelemetry/sdk-trace-base': - optional: true - '@opentelemetry/semantic-conventions': + webpack: optional: true - '@sentry/node@10.44.0': - resolution: {integrity: sha512-q+/WR9ZeF9Af8uyehOj2tQQOa7LH07mJfOuDus5X6G6cLuugdRUGUBB5Qhw+J/ULSxbzGADBZv6AYOyoGaNx7w==} - engines: {node: '>=18'} + '@sentry/conventions@0.27.0': + resolution: {integrity: sha512-Mrqd/o7n/z7qWSYfQQA7x4fFTA0IqOG6JNsbBpnC97khPwZk7n6DPtOO7YgaYplSl8R8QLRbBz9Xv2lVhToSAg==} + engines: {node: '>=14'} - '@sentry/opentelemetry@10.44.0': - resolution: {integrity: sha512-zP4vP8tBxjlmxQ4VcWOwZ0b3lPUxlYPg9FqJwANm9SRJN+7V5psm8TIaAtu9uqtIcJMRHdXkOM4cAggNiLk0KA==} - engines: {node: '>=18'} + '@sentry/core@11.8.0': + resolution: {integrity: sha512-4MVTthk6C/1oQFjbVIfygHyNkx2YGbwNLml0kJji0vjJXy6ZNls9zdIp0FTBTi4e3189IcSc5OXD4qcJdmEeKA==} + engines: {node: '>=20.19.0 <22.0.0 || >=22.12.0 <23.0.0 || >=23.2.0'} + + '@sentry/node@11.8.0': + resolution: {integrity: sha512-8vFPfCRYoUYmsno+OYdvtf5akIUUvd7FvcuIGcfrge8S/bSLFRKq58YtFmX25JFcrMNZ2oQD8r0jkgZfm/2iuA==} + engines: {node: '>=20.19.0 <22.0.0 || >=22.12.0 <23.0.0 || >=23.2.0'} + + '@sentry/opentelemetry@11.8.0': + resolution: {integrity: sha512-wKuG2SbHHiGDT/fgT4X5n2usO4AjVkYfAj41002mZ18JZU3mNLeCq+qw/nnDIYbcsfIMOUm/eMh3ShKCE+DpXQ==} + engines: {node: '>=20.19.0 <22.0.0 || >=22.12.0 <23.0.0 || >=23.2.0'} peerDependencies: '@opentelemetry/api': ^1.9.0 - '@opentelemetry/context-async-hooks': ^1.30.1 || ^2.1.0 - '@opentelemetry/core': ^1.30.1 || ^2.1.0 - '@opentelemetry/sdk-trace-base': ^1.30.1 || ^2.1.0 - '@opentelemetry/semantic-conventions': ^1.39.0 + + '@sentry/server-runtime-injection@11.8.0': + resolution: {integrity: sha512-4uB4E8+9ni5NcJ463aYK/+n1hTMw/vSt4s+AkQ84CKigRDwv2FNNnD1AWpipWNj/xEQ6eQbUitGl7wg7/Ri8Pg==} + engines: {node: '>=20.19.0 <22.0.0 || >=22.12.0 <23.0.0 || >=23.2.0'} + + '@sentry/server-utils@11.8.0': + resolution: {integrity: sha512-2MRFz1rXxnuDGafl6o/itwKHZeZn5hUZYeXNbQK012uO/a2pXhEc8Zh3FeO5L4tjGrIWHx419saAFnKwbVC2IQ==} + engines: {node: '>=20.19.0 <22.0.0 || >=22.12.0 <23.0.0 || >=23.2.0'} '@slack/logger@4.0.1': resolution: {integrity: sha512-6cmdPrV/RYfd2U0mDGiMK8S7OJqpCTm7enMLRR3edccsPX8j7zXTLnaEF4fhxxJJTAIOil6+qZrnUPTuaLvwrQ==} @@ -2232,18 +2257,12 @@ packages: '@types/lodash@4.17.24': resolution: {integrity: sha512-gIW7lQLZbue7lRSWEFql49QJJWThrTFFeIMJdp3eH4tKoxm1OvEPg02rm4wCCSHS0cL3/Fizimb35b7k8atwsQ==} - '@types/mock-fs@4.13.4': - resolution: {integrity: sha512-mXmM0o6lULPI8z3XNnQCpL0BGxPwx1Ul1wXYEPBGl4efShyxW2Rln0JOPEWGyZaYZMM6OVXM/15zUuFMY52ljg==} - '@types/mute-stream@0.0.1': resolution: {integrity: sha512-0yQLzYhCqGz7CQPE3iDmYjhb7KMBFOP+tBkyw+/Y2YyDI5wpS7itXXxneN1zSsUwWx3Ji6YiVYrhAnpQGS/vkw==} '@types/mute-stream@0.0.4': resolution: {integrity: sha512-CPM9nzrCPPJHQNA9keH9CVkVI+WR5kMa+7XEs5jcGQ0VoAGnLv242w8lIVgwAEfmE4oufJRaTc9PNLQl0ioAow==} - '@types/mysql@2.15.27': - resolution: {integrity: sha512-YfWiV16IY0OeBfBCk8+hXKmdTKrKlwKN1MNKAPBu5JYxLwBEZl7QzeEpGnlZb3VMGJrrGmB84gXiH+ofs/TezA==} - '@types/node-localstorage@1.3.3': resolution: {integrity: sha512-Wkn5g4eM5x10UNV9Xvl9K6y6m0zorocuJy4WjB5muUdyMZuPbZpSJG3hlhjGHe1HGxbOQO7RcB+jlHcNwkh+Jw==} @@ -2265,12 +2284,6 @@ packages: '@types/nodemon@1.19.3': resolution: {integrity: sha512-LcKdWgch8uHOF73yYpdE7YPVLT0HnFI60zyNBpJyfAiDDwPy3WAxReQeB84UseE8e8qdJsBqmFXWbjxv7jlXBg==} - '@types/pg-pool@2.0.7': - resolution: {integrity: sha512-U4CwmGVQcbEuqpyju8/ptOKg6gEC+Tqsvj2xS9o1g71bUh8twxnC6ZL5rZKCsGN0iyH0CwgUyc9VR5owNQF9Ng==} - - '@types/pg@8.15.6': - resolution: {integrity: sha512-NoaMtzhxOrubeL/7UZuNTrejB4MPAJ0RpxZqXQf2qXuVlTPuG6Y8p4u9dKRaue4yjmC7ZhzVO2/Yyyn25znrPQ==} - '@types/phoenix@1.6.7': resolution: {integrity: sha512-oN9ive//QSBkf19rfDv45M7eZPi0eEXylht2OLEXicu5b4KoQ1OzXIw+xDSGWxSxe1JmepRR/ZH283vsu518/Q==} @@ -2301,9 +2314,6 @@ packages: '@types/tar-stream@2.2.3': resolution: {integrity: sha512-if3mugZfjVkXOMZdFjIHySxY13r6GXPpyOlsDmLffvyI7tLz9wXE8BFjNivXsvUeyJ1KNlOpfLnag+ISmxgxPw==} - '@types/tedious@4.0.14': - resolution: {integrity: sha512-KHPsfX/FoVbUGbyYvk1q9MMQHLPeRZhRJZdO45Q4YjvFkv4hMNghCWTvy7rdKessBsmtz4euWCWAB6/tVpI1Iw==} - '@types/treeify@1.0.3': resolution: {integrity: sha512-hx0o7zWEUU4R2Amn+pjCBQQt23Khy/Dk56gQU5xi5jtPL1h83ACJCeFaB2M/+WO1AntvWrSoVnnCAfI1AQH4Cg==} @@ -2328,11 +2338,6 @@ packages: resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} engines: {node: '>= 0.6'} - acorn-import-attributes@1.9.5: - resolution: {integrity: sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==} - peerDependencies: - acorn: ^8 - acorn-walk@8.3.5: resolution: {integrity: sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==} engines: {node: '>=0.4.0'} @@ -2609,9 +2614,6 @@ packages: ci-parallel-vars@1.0.1: resolution: {integrity: sha512-uvzpYrpmidaoxvIQHM+rKSrigjOe9feHYbw4uOI2gdfe1C3xIlxO+kVXq83WQWNniTf8bAxVpy+cQeFQsMERKg==} - cjs-module-lexer@2.2.0: - resolution: {integrity: sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ==} - clean-stack@4.2.0: resolution: {integrity: sha512-LYv6XPxoyODi36Dp976riBtSY27VmFo+MKqEU9QCCWyTrdEPDog+RWA7xQWHi6Vbp61j5c4cdzzX1NidnwtUWg==} engines: {node: '>=12'} @@ -2833,6 +2835,10 @@ packages: resolution: {integrity: sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA==} engines: {node: '>=12'} + dotenv@17.4.2: + resolution: {integrity: sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==} + engines: {node: '>=12'} + dreamopt@0.8.0: resolution: {integrity: sha512-vyJTp8+mC+G+5dfgsY+r3ckxlz+QMX40VjPQsZc5gxVAxLmi64TBoVkP54A/pRAXMXsbu2GMMBrZPxNv23waMg==} engines: {node: '>=0.4.0'} @@ -3185,9 +3191,6 @@ packages: resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} engines: {node: '>= 6'} - forwarded-parse@2.1.2: - resolution: {integrity: sha512-alTFZZQDKMporBH77856pXgzhEzaUVmLCDk+egLgIgHst3Tpndzz8MnKe+GzRJRfvVdn69HhpW7cmXzvtLvJAw==} - forwarded@0.2.0: resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} engines: {node: '>= 0.6'} @@ -3235,6 +3238,12 @@ packages: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} + glob-to-regex.js@1.3.1: + resolution: {integrity: sha512-zhWhMRsgnOym+1txhUmYJGD6eUsbUdrZv55gCYKuJX8t2p3+M8My23PD09bEIsreGUs5pnSa/idmcfp2/IlqGg==} + engines: {node: '>=10.0'} + peerDependencies: + tslib: '2' + glob@13.0.6: resolution: {integrity: sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==} engines: {node: 18 || 20 || >=22} @@ -3310,6 +3319,10 @@ packages: humanize-number@0.0.2: resolution: {integrity: sha512-un3ZAcNQGI7RzaWGZzQDH47HETM4Wrj6z6E4TId8Yeq9w5ZKUVB1nrT2jwFheTUjEmqcgTjXDc959jum+ai1kQ==} + hyperdyperid@1.2.0: + resolution: {integrity: sha512-Y93lCzHYgGWdrJ66yIktxiaGULYc6oGiABxhcO5AufBeOyoIdZF7bIfLaOrbM0iGIOXQQgxxRrFEnb+Y6w1n4A==} + engines: {node: '>=10.18'} + iconv-lite@0.4.24: resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} engines: {node: '>=0.10.0'} @@ -3333,13 +3346,6 @@ packages: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} - import-in-the-middle@2.0.6: - resolution: {integrity: sha512-3vZV3jX0XRFW3EJDTwzWoZa+RH1b8eTTx6YOCjglrLyPuepwoBti1k3L2dKwdCUrnVEfc5CuRuGstaC/uQJJaw==} - - import-in-the-middle@3.0.0: - resolution: {integrity: sha512-OnGy+eYT7wVejH2XWgLRgbmzujhhVIATQH0ztIeRilwHBjTeG3pD+XnH3PKX0r9gJ0BuJmJ68q/oh9qgXnNDQg==} - engines: {node: '>=18'} - imurmurhash@0.1.4: resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} engines: {node: '>=0.8.19'} @@ -3601,6 +3607,9 @@ packages: resolution: {integrity: sha512-F2t4YIv9XQUBHt6AOJ0y7lSmP1+cY7Fm1DRh9GClTGzKST7UWLMx6ly9WZdLH/G/ppM5RL4MlQfRT71ri9t19A==} engines: {node: '>=12.20'} + memfs@4.80.0: + resolution: {integrity: sha512-N70W2isi44XdGn7uPo9NJ4zRukcdkwSFHlnYx42SWOH/+hzqNc8P7+tVoA2up4z7faHvNi1RSNCFjxoYQ71VPQ==} + merge-descriptors@1.0.3: resolution: {integrity: sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==} @@ -3652,13 +3661,6 @@ packages: mlly@1.8.1: resolution: {integrity: sha512-SnL6sNutTwRWWR/vcmCYHSADjiEesp5TGQQ0pXyLhW5IoeibRlF/CbSLailbB3CNqJUk9cVJ9dUDnbD7GrcHBQ==} - mock-fs@5.5.0: - resolution: {integrity: sha512-d/P1M/RacgM3dB0sJ8rjeRNXxtapkPCUnMGmIN0ixJ16F/E4GUZCvWcSGfWGz8eaXYvn1s9baUwNjI4LOPEjiA==} - engines: {node: '>=12.0.0'} - - module-details-from-path@1.0.4: - resolution: {integrity: sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w==} - mri@1.2.0: resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} engines: {node: '>=4'} @@ -3746,6 +3748,10 @@ packages: outdent@0.5.0: resolution: {integrity: sha512-/jHxFIzoMXdqPzTaCpFzAAWhpkSjZPF4Vsn6jAfNpmbH/ymsmd7Qc6VE9BGn0L6YMj6uwpQLxCECpus4ukKS9Q==} + oxc-parser@0.152.0: + resolution: {integrity: sha512-ZztsZgaCWmPeKYMdoTT5e0oV/oxD1NeLWNR3kAtr/G707cUPYaaozRlVDHLCGnszHJRszN9mJwmPNpqgwloC0A==} + engines: {node: ^20.19.0 || >=22.12.0} + oxc-resolver@11.19.1: resolution: {integrity: sha512-qE/CIg/spwrTBFt5aKmwe3ifeDdLfA2NESN30E42X/lII5ClF8V7Wt6WIJhcGZjp0/Q+nQ+9vgxGk//xZNX2hg==} @@ -3867,17 +3873,6 @@ packages: peek-stream@1.1.3: resolution: {integrity: sha512-FhJ+YbOSBb9/rIl2ZeE/QHEsWn7PqNYt8ARAY3kIgNGOk13g9FGyIY6JIl/xB/3TFRVoTv5as0l11weORrTekA==} - pg-int8@1.0.1: - resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==} - engines: {node: '>=4.0.0'} - - pg-protocol@1.13.0: - resolution: {integrity: sha512-zzdvXfS6v89r6v7OcFCHfHlyG/wvry1ALxZo4LqgUoy7W9xhBDMaqOuMiF3qEV45VqsN6rdlcehHrfDtlCPc8w==} - - pg-types@2.2.0: - resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==} - engines: {node: '>=4'} - phoenix@1.7.10: resolution: {integrity: sha512-akfr/QvLPFRB8sORyc8FQFY/YoGwjWhka/YRcu45sKlBOZHvA80EkLYBUsYlW63UicxgrXABZdrjDkv54LTE+g==} @@ -3936,22 +3931,6 @@ packages: resolution: {integrity: sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==} engines: {node: ^10 || ^12 || >=14} - postgres-array@2.0.0: - resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==} - engines: {node: '>=4'} - - postgres-bytea@1.0.1: - resolution: {integrity: sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==} - engines: {node: '>=0.10.0'} - - postgres-date@1.0.7: - resolution: {integrity: sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==} - engines: {node: '>=0.10.0'} - - postgres-interval@1.2.0: - resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==} - engines: {node: '>=0.10.0'} - prettier@2.8.8: resolution: {integrity: sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==} engines: {node: '>=10.13.0'} @@ -4034,10 +4013,6 @@ packages: resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} engines: {node: '>=0.10.0'} - require-in-the-middle@8.0.1: - resolution: {integrity: sha512-QT7FVMXfWOYFbeRBF6nu+I6tr2Tf3u0q8RIEjNob/heKY/nh7drD/k7eeMFmSQgnTtCzLDcCu/XEnpW2wk4xCQ==} - engines: {node: '>=9.3.0 || >=8.10.0 <9.0.0'} - resolve-cwd@3.0.0: resolution: {integrity: sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==} engines: {node: '>=8'} @@ -4099,6 +4074,11 @@ packages: sentry-testkit@6.2.2: resolution: {integrity: sha512-1YdDokVhf8pbCGF91TeaWP1ue+lHSB358nH+I3mGOWg79O1hssvv3H3OWMojp9RZ+qHXm0DgcZ05oIjnbYoMRw==} + sentry@0.45.0: + resolution: {integrity: sha512-kgmEbLlC22lL7uneeUm9Sjz65rp5cvwYOMTxXbZCmBum4sbVre/lLT3zTV3kwXHJ9/BM8ZS0/GhZe9Cl1t6WyA==} + engines: {node: '>=20.0'} + hasBin: true + serialize-error@7.0.1: resolution: {integrity: sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==} engines: {node: '>=10'} @@ -4246,6 +4226,10 @@ packages: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} engines: {node: '>=8'} + supports-color@8.1.1: + resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} + engines: {node: '>=10'} + tar-stream@3.1.8: resolution: {integrity: sha512-U6QpVRyCGHva435KoNWy9PRoi2IFYCgtEhq9nmrPPpbRacPs9IH4aJ3gbrFC8dPcXvdSZ4XXfXT5Fshbp2MtlQ==} @@ -4270,6 +4254,12 @@ packages: thenify@3.3.1: resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} + thingies@2.6.1: + resolution: {integrity: sha512-cV/CMGTK3M4MlnJ/0At6ismOw/A0EEniDNScajjz/Br3c1sqE72YD01rGpPTKwd27wAxI5Pr+6+0w8yofzFRYw==} + engines: {node: '>=10.18'} + peerDependencies: + tslib: ^2 + threads@1.7.0: resolution: {integrity: sha512-Mx5NBSHX3sQYR6iI9VYbgHKBLisyB+xROCBGjjWm1O9wb9vfLxdaGtmT/KCjUqMsSNW6nERzCW3T6H43LqjDZQ==} @@ -4309,6 +4299,12 @@ packages: tr46@0.0.3: resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + tree-dump@1.1.0: + resolution: {integrity: sha512-rMuvhU4MCDbcbnleZTFezWsaZXRFemSqAM+7jPnzUl1fo9w3YEKOxAeui0fz3OI4EU4hf23iyA7uQRVko+UaBA==} + engines: {node: '>=10.0'} + peerDependencies: + tslib: '2' + tree-kill@1.2.2: resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} hasBin: true @@ -4860,16 +4856,6 @@ snapshots: '@fastify/busboy@2.1.1': {} - '@fastify/otel@0.17.1(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.6.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation': 0.212.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.40.0 - minimatch: 10.2.4 - transitivePeerDependencies: - - supports-color - '@hapi/bourne@3.0.0': {} '@inquirer/checkbox@1.5.2': @@ -5041,38 +5027,184 @@ snapshots: dependencies: jsep: 1.4.0 - '@koa/bodyparser@6.1.0(koa@2.16.4)': + '@jsonjoy.com/base64@1.1.2(tslib@2.8.1)': dependencies: - '@types/co-body': 6.1.3 - co-body: 6.2.0 - koa: 2.16.4 - lodash.merge: 4.6.2 - type-is: 2.0.1 + tslib: 2.8.1 - '@koa/router@15.4.0(koa@2.16.4)': + '@jsonjoy.com/base64@17.67.0(tslib@2.8.1)': dependencies: - debug: 4.4.3(supports-color@5.5.0) - http-errors: 2.0.1 - koa: 2.16.4 - koa-compose: 4.1.0 - path-to-regexp: 8.4.1 - transitivePeerDependencies: - - supports-color + tslib: 2.8.1 - '@manypkg/find-root@1.1.0': + '@jsonjoy.com/buffers@1.2.1(tslib@2.8.1)': dependencies: - '@babel/runtime': 7.29.2 - '@types/node': 12.20.55 - find-up: 4.1.0 - fs-extra: 8.1.0 + tslib: 2.8.1 - '@manypkg/get-packages@1.1.3': + '@jsonjoy.com/buffers@17.67.0(tslib@2.8.1)': dependencies: - '@babel/runtime': 7.29.2 - '@changesets/types': 4.1.0 - '@manypkg/find-root': 1.1.0 - fs-extra: 8.1.0 - globby: 11.1.0 + tslib: 2.8.1 + + '@jsonjoy.com/codegen@1.0.0(tslib@2.8.1)': + dependencies: + tslib: 2.8.1 + + '@jsonjoy.com/codegen@17.67.0(tslib@2.8.1)': + dependencies: + tslib: 2.8.1 + + '@jsonjoy.com/fs-core@4.80.0(tslib@2.8.1)': + dependencies: + '@jsonjoy.com/fs-node-builtins': 4.80.0(tslib@2.8.1) + '@jsonjoy.com/fs-node-utils': 4.80.0(tslib@2.8.1) + thingies: 2.6.1(tslib@2.8.1) + tslib: 2.8.1 + + '@jsonjoy.com/fs-fsa@4.80.0(tslib@2.8.1)': + dependencies: + '@jsonjoy.com/fs-core': 4.80.0(tslib@2.8.1) + '@jsonjoy.com/fs-node-builtins': 4.80.0(tslib@2.8.1) + '@jsonjoy.com/fs-node-utils': 4.80.0(tslib@2.8.1) + thingies: 2.6.1(tslib@2.8.1) + tslib: 2.8.1 + + '@jsonjoy.com/fs-node-builtins@4.80.0(tslib@2.8.1)': + dependencies: + tslib: 2.8.1 + + '@jsonjoy.com/fs-node-to-fsa@4.80.0(tslib@2.8.1)': + dependencies: + '@jsonjoy.com/fs-fsa': 4.80.0(tslib@2.8.1) + '@jsonjoy.com/fs-node-builtins': 4.80.0(tslib@2.8.1) + '@jsonjoy.com/fs-node-utils': 4.80.0(tslib@2.8.1) + tslib: 2.8.1 + + '@jsonjoy.com/fs-node-utils@4.80.0(tslib@2.8.1)': + dependencies: + '@jsonjoy.com/fs-node-builtins': 4.80.0(tslib@2.8.1) + glob-to-regex.js: 1.3.1(tslib@2.8.1) + tslib: 2.8.1 + + '@jsonjoy.com/fs-node@4.80.0(tslib@2.8.1)': + dependencies: + '@jsonjoy.com/fs-core': 4.80.0(tslib@2.8.1) + '@jsonjoy.com/fs-node-builtins': 4.80.0(tslib@2.8.1) + '@jsonjoy.com/fs-node-utils': 4.80.0(tslib@2.8.1) + '@jsonjoy.com/fs-print': 4.80.0(tslib@2.8.1) + '@jsonjoy.com/fs-snapshot': 4.80.0(tslib@2.8.1) + glob-to-regex.js: 1.3.1(tslib@2.8.1) + thingies: 2.6.1(tslib@2.8.1) + tslib: 2.8.1 + + '@jsonjoy.com/fs-print@4.80.0(tslib@2.8.1)': + dependencies: + '@jsonjoy.com/fs-node-utils': 4.80.0(tslib@2.8.1) + tree-dump: 1.1.0(tslib@2.8.1) + tslib: 2.8.1 + + '@jsonjoy.com/fs-snapshot@4.80.0(tslib@2.8.1)': + dependencies: + '@jsonjoy.com/buffers': 17.67.0(tslib@2.8.1) + '@jsonjoy.com/fs-node-utils': 4.80.0(tslib@2.8.1) + '@jsonjoy.com/json-pack': 17.67.0(tslib@2.8.1) + '@jsonjoy.com/util': 17.67.0(tslib@2.8.1) + tslib: 2.8.1 + + '@jsonjoy.com/json-pack@1.21.0(tslib@2.8.1)': + dependencies: + '@jsonjoy.com/base64': 1.1.2(tslib@2.8.1) + '@jsonjoy.com/buffers': 1.2.1(tslib@2.8.1) + '@jsonjoy.com/codegen': 1.0.0(tslib@2.8.1) + '@jsonjoy.com/json-pointer': 1.0.2(tslib@2.8.1) + '@jsonjoy.com/util': 1.9.0(tslib@2.8.1) + hyperdyperid: 1.2.0 + thingies: 2.6.1(tslib@2.8.1) + tree-dump: 1.1.0(tslib@2.8.1) + tslib: 2.8.1 + + '@jsonjoy.com/json-pack@17.67.0(tslib@2.8.1)': + dependencies: + '@jsonjoy.com/base64': 17.67.0(tslib@2.8.1) + '@jsonjoy.com/buffers': 17.67.0(tslib@2.8.1) + '@jsonjoy.com/codegen': 17.67.0(tslib@2.8.1) + '@jsonjoy.com/json-pointer': 17.67.0(tslib@2.8.1) + '@jsonjoy.com/util': 17.67.0(tslib@2.8.1) + hyperdyperid: 1.2.0 + thingies: 2.6.1(tslib@2.8.1) + tree-dump: 1.1.0(tslib@2.8.1) + tslib: 2.8.1 + + '@jsonjoy.com/json-pointer@1.0.2(tslib@2.8.1)': + dependencies: + '@jsonjoy.com/codegen': 1.0.0(tslib@2.8.1) + '@jsonjoy.com/util': 1.9.0(tslib@2.8.1) + tslib: 2.8.1 + + '@jsonjoy.com/json-pointer@17.67.0(tslib@2.8.1)': + dependencies: + '@jsonjoy.com/util': 17.67.0(tslib@2.8.1) + tslib: 2.8.1 + + '@jsonjoy.com/util@1.9.0(tslib@2.8.1)': + dependencies: + '@jsonjoy.com/buffers': 1.2.1(tslib@2.8.1) + '@jsonjoy.com/codegen': 1.0.0(tslib@2.8.1) + tslib: 2.8.1 + + '@jsonjoy.com/util@17.67.0(tslib@2.8.1)': + dependencies: + '@jsonjoy.com/buffers': 17.67.0(tslib@2.8.1) + '@jsonjoy.com/codegen': 17.67.0(tslib@2.8.1) + tslib: 2.8.1 + + '@koa/bodyparser@6.1.0(koa@2.16.4(supports-color@5.5.0))': + dependencies: + '@types/co-body': 6.1.3 + co-body: 6.2.0 + koa: 2.16.4(supports-color@5.5.0) + lodash.merge: 4.6.2 + type-is: 2.0.1 + + '@koa/bodyparser@6.1.0(koa@2.16.4(supports-color@7.2.0))': + dependencies: + '@types/co-body': 6.1.3 + co-body: 6.2.0 + koa: 2.16.4(supports-color@7.2.0) + lodash.merge: 4.6.2 + type-is: 2.0.1 + + '@koa/router@15.4.0(koa@2.16.4(supports-color@5.5.0))(supports-color@5.5.0)': + dependencies: + debug: 4.4.3(supports-color@5.5.0) + http-errors: 2.0.1 + koa: 2.16.4(supports-color@5.5.0) + koa-compose: 4.1.0 + path-to-regexp: 8.4.1 + transitivePeerDependencies: + - supports-color + + '@koa/router@15.4.0(koa@2.16.4(supports-color@7.2.0))(supports-color@7.2.0)': + dependencies: + debug: 4.4.3(supports-color@7.2.0) + http-errors: 2.0.1 + koa: 2.16.4(supports-color@7.2.0) + koa-compose: 4.1.0 + path-to-regexp: 8.4.1 + transitivePeerDependencies: + - supports-color + + '@manypkg/find-root@1.1.0': + dependencies: + '@babel/runtime': 7.29.2 + '@types/node': 12.20.55 + find-up: 4.1.0 + fs-extra: 8.1.0 + + '@manypkg/get-packages@1.1.3': + dependencies: + '@babel/runtime': 7.29.2 + '@changesets/types': 4.1.0 + '@manypkg/find-root': 1.1.0 + fs-extra: 8.1.0 + globby: 11.1.0 read-yaml-file: 1.1.0 '@napi-rs/wasm-runtime@1.1.1': @@ -5156,268 +5288,66 @@ snapshots: cheerio: 1.0.0-rc.12 cheerio-tableparser: 1.0.1 - '@opentelemetry/api-logs@0.207.0': - dependencies: - '@opentelemetry/api': 1.9.0 - - '@opentelemetry/api-logs@0.212.0': - dependencies: - '@opentelemetry/api': 1.9.0 - - '@opentelemetry/api-logs@0.213.0': - dependencies: - '@opentelemetry/api': 1.9.0 - - '@opentelemetry/api@1.9.0': {} - - '@opentelemetry/context-async-hooks@2.6.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - - '@opentelemetry/core@2.6.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/semantic-conventions': 1.40.0 - - '@opentelemetry/instrumentation-amqplib@0.60.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.6.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation': 0.213.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.40.0 - transitivePeerDependencies: - - supports-color + '@opentelemetry/api@1.9.1': {} - '@opentelemetry/instrumentation-connect@0.56.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.6.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation': 0.213.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.40.0 - '@types/connect': 3.4.38 - transitivePeerDependencies: - - supports-color - - '@opentelemetry/instrumentation-dataloader@0.30.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/instrumentation': 0.213.0(@opentelemetry/api@1.9.0) - transitivePeerDependencies: - - supports-color - - '@opentelemetry/instrumentation-express@0.61.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.6.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation': 0.213.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.40.0 - transitivePeerDependencies: - - supports-color - - '@opentelemetry/instrumentation-fs@0.32.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.6.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation': 0.213.0(@opentelemetry/api@1.9.0) - transitivePeerDependencies: - - supports-color - - '@opentelemetry/instrumentation-generic-pool@0.56.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/instrumentation': 0.213.0(@opentelemetry/api@1.9.0) - transitivePeerDependencies: - - supports-color - - '@opentelemetry/instrumentation-graphql@0.61.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/instrumentation': 0.213.0(@opentelemetry/api@1.9.0) - transitivePeerDependencies: - - supports-color - - '@opentelemetry/instrumentation-hapi@0.59.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.6.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation': 0.213.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.40.0 - transitivePeerDependencies: - - supports-color - - '@opentelemetry/instrumentation-http@0.213.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.6.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation': 0.213.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.40.0 - forwarded-parse: 2.1.2 - transitivePeerDependencies: - - supports-color - - '@opentelemetry/instrumentation-ioredis@0.61.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/instrumentation': 0.213.0(@opentelemetry/api@1.9.0) - '@opentelemetry/redis-common': 0.38.2 - '@opentelemetry/semantic-conventions': 1.40.0 - transitivePeerDependencies: - - supports-color - - '@opentelemetry/instrumentation-kafkajs@0.22.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/instrumentation': 0.213.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.40.0 - transitivePeerDependencies: - - supports-color + '@oxc-parser/binding-android-arm-eabi@0.152.0': + optional: true - '@opentelemetry/instrumentation-knex@0.57.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/instrumentation': 0.213.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.40.0 - transitivePeerDependencies: - - supports-color + '@oxc-parser/binding-android-arm64@0.152.0': + optional: true - '@opentelemetry/instrumentation-koa@0.61.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.6.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation': 0.213.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.40.0 - transitivePeerDependencies: - - supports-color + '@oxc-parser/binding-darwin-arm64@0.152.0': + optional: true - '@opentelemetry/instrumentation-lru-memoizer@0.57.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/instrumentation': 0.213.0(@opentelemetry/api@1.9.0) - transitivePeerDependencies: - - supports-color + '@oxc-parser/binding-darwin-x64@0.152.0': + optional: true - '@opentelemetry/instrumentation-mongodb@0.66.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/instrumentation': 0.213.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.40.0 - transitivePeerDependencies: - - supports-color + '@oxc-parser/binding-freebsd-x64@0.152.0': + optional: true - '@opentelemetry/instrumentation-mongoose@0.59.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.6.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation': 0.213.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.40.0 - transitivePeerDependencies: - - supports-color + '@oxc-parser/binding-linux-arm-gnueabihf@0.152.0': + optional: true - '@opentelemetry/instrumentation-mysql2@0.59.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/instrumentation': 0.213.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.40.0 - '@opentelemetry/sql-common': 0.41.2(@opentelemetry/api@1.9.0) - transitivePeerDependencies: - - supports-color + '@oxc-parser/binding-linux-arm-musleabihf@0.152.0': + optional: true - '@opentelemetry/instrumentation-mysql@0.59.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/instrumentation': 0.213.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.40.0 - '@types/mysql': 2.15.27 - transitivePeerDependencies: - - supports-color + '@oxc-parser/binding-linux-arm64-gnu@0.152.0': + optional: true - '@opentelemetry/instrumentation-pg@0.65.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.6.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation': 0.213.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.40.0 - '@opentelemetry/sql-common': 0.41.2(@opentelemetry/api@1.9.0) - '@types/pg': 8.15.6 - '@types/pg-pool': 2.0.7 - transitivePeerDependencies: - - supports-color + '@oxc-parser/binding-linux-arm64-musl@0.152.0': + optional: true - '@opentelemetry/instrumentation-redis@0.61.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/instrumentation': 0.213.0(@opentelemetry/api@1.9.0) - '@opentelemetry/redis-common': 0.38.2 - '@opentelemetry/semantic-conventions': 1.40.0 - transitivePeerDependencies: - - supports-color + '@oxc-parser/binding-linux-ppc64-gnu@0.152.0': + optional: true - '@opentelemetry/instrumentation-tedious@0.32.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/instrumentation': 0.213.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.40.0 - '@types/tedious': 4.0.14 - transitivePeerDependencies: - - supports-color + '@oxc-parser/binding-linux-riscv64-gnu@0.152.0': + optional: true - '@opentelemetry/instrumentation-undici@0.23.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.6.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation': 0.213.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.40.0 - transitivePeerDependencies: - - supports-color + '@oxc-parser/binding-linux-riscv64-musl@0.152.0': + optional: true - '@opentelemetry/instrumentation@0.207.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/api-logs': 0.207.0 - import-in-the-middle: 2.0.6 - require-in-the-middle: 8.0.1 - transitivePeerDependencies: - - supports-color + '@oxc-parser/binding-linux-s390x-gnu@0.152.0': + optional: true - '@opentelemetry/instrumentation@0.212.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/api-logs': 0.212.0 - import-in-the-middle: 2.0.6 - require-in-the-middle: 8.0.1 - transitivePeerDependencies: - - supports-color + '@oxc-parser/binding-linux-x64-gnu@0.152.0': + optional: true - '@opentelemetry/instrumentation@0.213.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/api-logs': 0.213.0 - import-in-the-middle: 3.0.0 - require-in-the-middle: 8.0.1 - transitivePeerDependencies: - - supports-color + '@oxc-parser/binding-linux-x64-musl@0.152.0': + optional: true - '@opentelemetry/redis-common@0.38.2': {} + '@oxc-parser/binding-openharmony-arm64@0.152.0': + optional: true - '@opentelemetry/resources@2.6.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.6.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.40.0 + '@oxc-parser/binding-win32-arm64-msvc@0.152.0': + optional: true - '@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.6.0(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 2.6.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.40.0 + '@oxc-parser/binding-win32-ia32-msvc@0.152.0': + optional: true - '@opentelemetry/semantic-conventions@1.40.0': {} + '@oxc-parser/binding-win32-x64-msvc@0.152.0': + optional: true - '@opentelemetry/sql-common@0.41.2(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.6.0(@opentelemetry/api@1.9.0) + '@oxc-project/types@0.152.0': {} '@oxc-resolver/binding-android-arm-eabi@11.19.1': optional: true @@ -5481,13 +5411,6 @@ snapshots: '@oxc-resolver/binding-win32-x64-msvc@11.19.1': optional: true - '@prisma/instrumentation@7.4.2(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/instrumentation': 0.207.0(@opentelemetry/api@1.9.0) - transitivePeerDependencies: - - supports-color - '@rollup/rollup-android-arm-eabi@4.59.0': optional: true @@ -5563,70 +5486,54 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.59.0': optional: true - '@sentry/core@10.44.0': {} - - '@sentry/node-core@10.44.0(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@2.6.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.6.0(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.213.0(@opentelemetry/api@1.9.0))(@opentelemetry/resources@2.6.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.40.0)': + '@sentry/bundler-plugins@11.8.0(rollup@4.59.0)': dependencies: - '@sentry/core': 10.44.0 - '@sentry/opentelemetry': 10.44.0(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@2.6.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.6.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.40.0) - import-in-the-middle: 3.0.0 + '@sentry/core': 11.8.0 + dotenv: 17.4.2 + glob: 13.0.6 + magic-string: 0.30.21 + oxc-parser: 0.152.0 + sentry: 0.45.0 + supports-color: 8.1.1 optionalDependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/context-async-hooks': 2.6.0(@opentelemetry/api@1.9.0) - '@opentelemetry/core': 2.6.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation': 0.213.0(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 2.6.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-trace-base': 2.6.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.40.0 - - '@sentry/node@10.44.0': - dependencies: - '@fastify/otel': 0.17.1(@opentelemetry/api@1.9.0) - '@opentelemetry/api': 1.9.0 - '@opentelemetry/context-async-hooks': 2.6.0(@opentelemetry/api@1.9.0) - '@opentelemetry/core': 2.6.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation': 0.213.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-amqplib': 0.60.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-connect': 0.56.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-dataloader': 0.30.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-express': 0.61.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-fs': 0.32.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-generic-pool': 0.56.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-graphql': 0.61.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-hapi': 0.59.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-http': 0.213.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-ioredis': 0.61.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-kafkajs': 0.22.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-knex': 0.57.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-koa': 0.61.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-lru-memoizer': 0.57.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-mongodb': 0.66.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-mongoose': 0.59.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-mysql': 0.59.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-mysql2': 0.59.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-pg': 0.65.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-redis': 0.61.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-tedious': 0.32.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-undici': 0.23.0(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 2.6.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-trace-base': 2.6.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.40.0 - '@prisma/instrumentation': 7.4.2(@opentelemetry/api@1.9.0) - '@sentry/core': 10.44.0 - '@sentry/node-core': 10.44.0(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@2.6.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.6.0(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.213.0(@opentelemetry/api@1.9.0))(@opentelemetry/resources@2.6.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.40.0) - '@sentry/opentelemetry': 10.44.0(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@2.6.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.6.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.40.0) - import-in-the-middle: 3.0.0 + rollup: 4.59.0 + + '@sentry/conventions@0.27.0': {} + + '@sentry/core@11.8.0': + dependencies: + '@sentry/conventions': 0.27.0 + + '@sentry/node@11.8.0(rollup@4.59.0)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@sentry/bundler-plugins': 11.8.0(rollup@4.59.0) + '@sentry/conventions': 0.27.0 + '@sentry/core': 11.8.0 + '@sentry/opentelemetry': 11.8.0(@opentelemetry/api@1.9.1) + '@sentry/server-runtime-injection': 11.8.0 + '@sentry/server-utils': 11.8.0 transitivePeerDependencies: - - supports-color + - '@babel/core' + - rollup + - webpack - '@sentry/opentelemetry@10.44.0(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@2.6.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.6.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.40.0)': + '@sentry/opentelemetry@11.8.0(@opentelemetry/api@1.9.1)': dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/context-async-hooks': 2.6.0(@opentelemetry/api@1.9.0) - '@opentelemetry/core': 2.6.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-trace-base': 2.6.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.40.0 - '@sentry/core': 10.44.0 + '@opentelemetry/api': 1.9.1 + '@sentry/conventions': 0.27.0 + '@sentry/core': 11.8.0 + + '@sentry/server-runtime-injection@11.8.0': + dependencies: + '@sentry/core': 11.8.0 + '@sentry/server-utils': 11.8.0 + + '@sentry/server-utils@11.8.0': + dependencies: + '@opentelemetry/api': 1.9.1 + '@sentry/conventions': 0.27.0 + '@sentry/core': 11.8.0 '@slack/logger@4.0.1': dependencies: @@ -5634,13 +5541,13 @@ snapshots: '@slack/types@2.21.1': {} - '@slack/web-api@7.16.0': + '@slack/web-api@7.16.0(debug@4.4.3(supports-color@7.2.0))(supports-color@7.2.0)': dependencies: '@slack/logger': 4.0.1 '@slack/types': 2.21.1 '@types/node': 25.6.0 '@types/retry': 0.12.0 - axios: 1.16.1 + axios: 1.16.1(debug@4.4.3(supports-color@7.2.0))(supports-color@7.2.0) eventemitter3: 5.0.4 form-data: 4.0.5 is-electron: 2.2.2 @@ -5657,13 +5564,13 @@ snapshots: '@swc/core': 1.15.18 '@swc/types': 0.1.25 - '@swc-node/register@1.11.1(@swc/core@1.15.18)(@swc/types@0.1.25)(typescript@5.9.3)': + '@swc-node/register@1.11.1(@swc/core@1.15.18)(@swc/types@0.1.25)(supports-color@7.2.0)(typescript@5.9.3)': dependencies: '@swc-node/core': 1.14.1(@swc/core@1.15.18)(@swc/types@0.1.25) '@swc-node/sourcemap-support': 0.6.1 '@swc/core': 1.15.18 colorette: 2.0.20 - debug: 4.4.3(supports-color@5.5.0) + debug: 4.4.3(supports-color@7.2.0) oxc-resolver: 11.19.1 pirates: 4.0.7 tslib: 2.8.1 @@ -5837,10 +5744,6 @@ snapshots: '@types/lodash@4.17.24': {} - '@types/mock-fs@4.13.4': - dependencies: - '@types/node': 18.19.130 - '@types/mute-stream@0.0.1': dependencies: '@types/node': 18.19.130 @@ -5849,10 +5752,6 @@ snapshots: dependencies: '@types/node': 18.19.130 - '@types/mysql@2.15.27': - dependencies: - '@types/node': 18.19.130 - '@types/node-localstorage@1.3.3': dependencies: '@types/node': 18.19.130 @@ -5879,16 +5778,6 @@ snapshots: dependencies: '@types/node': 18.19.130 - '@types/pg-pool@2.0.7': - dependencies: - '@types/pg': 8.15.6 - - '@types/pg@8.15.6': - dependencies: - '@types/node': 18.19.130 - pg-protocol: 1.13.0 - pg-types: 2.2.0 - '@types/phoenix@1.6.7': {} '@types/qs@6.15.0': {} @@ -5919,10 +5808,6 @@ snapshots: dependencies: '@types/node': 25.5.0 - '@types/tedious@4.0.14': - dependencies: - '@types/node': 18.19.130 - '@types/treeify@1.0.3': {} '@types/wrap-ansi@3.0.0': {} @@ -5937,9 +5822,9 @@ snapshots: dependencies: '@types/yargs-parser': 21.0.3 - '@typescript/vfs@1.6.4(typescript@5.9.3)': + '@typescript/vfs@1.6.4(supports-color@7.2.0)(typescript@5.9.3)': dependencies: - debug: 4.4.3(supports-color@5.5.0) + debug: 4.4.3(supports-color@7.2.0) typescript: 5.9.3 transitivePeerDependencies: - supports-color @@ -5949,19 +5834,15 @@ snapshots: mime-types: 2.1.35 negotiator: 0.6.3 - acorn-import-attributes@1.9.5(acorn@8.16.0): - dependencies: - acorn: 8.16.0 - acorn-walk@8.3.5: dependencies: acorn: 8.16.0 acorn@8.16.0: {} - agent-base@6.0.2: + agent-base@6.0.2(supports-color@7.2.0): dependencies: - debug: 4.4.3(supports-color@5.5.0) + debug: 4.4.3(supports-color@7.2.0) transitivePeerDependencies: - supports-color @@ -6039,7 +5920,7 @@ snapshots: asynckit@0.4.0: {} - ava@5.3.1: + ava@5.3.1(supports-color@5.5.0): dependencies: acorn: 8.16.0 acorn-walk: 8.3.5 @@ -6087,11 +5968,59 @@ snapshots: transitivePeerDependencies: - supports-color - axios@1.16.1: + ava@5.3.1(supports-color@7.2.0): dependencies: - follow-redirects: 1.16.0 + acorn: 8.16.0 + acorn-walk: 8.3.5 + ansi-styles: 6.2.3 + arrgv: 1.0.2 + arrify: 3.0.0 + callsites: 4.2.0 + cbor: 8.1.0 + chalk: 5.6.2 + chokidar: 3.6.0 + chunkd: 2.0.1 + ci-info: 3.9.0 + ci-parallel-vars: 1.0.1 + clean-yaml-object: 0.1.0 + cli-truncate: 3.1.0 + code-excerpt: 4.0.0 + common-path-prefix: 3.0.0 + concordance: 5.0.4 + currently-unhandled: 0.4.1 + debug: 4.4.3(supports-color@7.2.0) + emittery: 1.2.1 + figures: 5.0.0 + globby: 13.2.2 + ignore-by-default: 2.1.0 + indent-string: 5.0.0 + is-error: 2.2.2 + is-plain-object: 5.0.0 + is-promise: 4.0.0 + matcher: 5.0.0 + mem: 9.0.2 + ms: 2.1.3 + p-event: 5.0.1 + p-map: 5.5.0 + picomatch: 2.3.1 + pkg-conf: 4.0.0 + plur: 5.1.0 + pretty-ms: 8.0.0 + resolve-cwd: 3.0.0 + stack-utils: 2.0.6 + strip-ansi: 7.2.0 + supertap: 3.0.1 + temp-dir: 3.0.0 + write-file-atomic: 5.0.1 + yargs: 17.7.2 + transitivePeerDependencies: + - supports-color + + axios@1.16.1(debug@4.4.3(supports-color@7.2.0))(supports-color@7.2.0): + dependencies: + follow-redirects: 1.16.0(debug@4.4.3(supports-color@7.2.0)) form-data: 4.0.5 - https-proxy-agent: 5.0.1 + https-proxy-agent: 5.0.1(supports-color@7.2.0) proxy-from-env: 2.1.0 transitivePeerDependencies: - debug @@ -6142,11 +6071,11 @@ snapshots: blueimp-md5@2.19.0: {} - body-parser@1.20.4: + body-parser@1.20.4(supports-color@5.5.0): dependencies: bytes: 3.1.2 content-type: 1.0.5 - debug: 2.6.9 + debug: 2.6.9(supports-color@5.5.0) depd: 2.0.0 destroy: 1.2.0 http-errors: 2.0.1 @@ -6267,8 +6196,6 @@ snapshots: ci-parallel-vars@1.0.1: {} - cjs-module-lexer@2.2.0: {} - clean-stack@4.2.0: dependencies: escape-string-regexp: 5.0.0 @@ -6404,9 +6331,11 @@ snapshots: dependencies: time-zone: 1.0.0 - debug@2.6.9: + debug@2.6.9(supports-color@5.5.0): dependencies: ms: 2.0.0 + optionalDependencies: + supports-color: 5.5.0 debug@4.4.3(supports-color@5.5.0): dependencies: @@ -6414,6 +6343,12 @@ snapshots: optionalDependencies: supports-color: 5.5.0 + debug@4.4.3(supports-color@7.2.0): + dependencies: + ms: 2.1.3 + optionalDependencies: + supports-color: 7.2.0 + decode-uri-component@0.4.1: {} deep-equal@1.0.1: {} @@ -6462,6 +6397,8 @@ snapshots: dotenv@17.3.1: {} + dotenv@17.4.2: {} + dreamopt@0.8.0: dependencies: wordwrap: 1.0.0 @@ -6706,21 +6643,21 @@ snapshots: transitivePeerDependencies: - bare-abort-controller - express@4.22.1: + express@4.22.1(supports-color@5.5.0): dependencies: accepts: 1.3.8 array-flatten: 1.1.1 - body-parser: 1.20.4 + body-parser: 1.20.4(supports-color@5.5.0) content-disposition: 0.5.4 content-type: 1.0.5 cookie: 0.7.2 cookie-signature: 1.0.7 - debug: 2.6.9 + debug: 2.6.9(supports-color@5.5.0) depd: 2.0.0 encodeurl: 2.0.0 escape-html: 1.0.3 etag: 1.8.1 - finalhandler: 1.3.2 + finalhandler: 1.3.2(supports-color@5.5.0) fresh: 0.5.2 http-errors: 2.0.1 merge-descriptors: 1.0.3 @@ -6732,8 +6669,8 @@ snapshots: qs: 6.14.2 range-parser: 1.2.1 safe-buffer: 5.2.1 - send: 0.19.2 - serve-static: 1.16.3 + send: 0.19.2(supports-color@5.5.0) + serve-static: 1.16.3(supports-color@5.5.0) setprototypeof: 1.2.0 statuses: 2.0.2 type-is: 1.6.18 @@ -6793,9 +6730,9 @@ snapshots: filter-obj@5.1.0: {} - finalhandler@1.3.2: + finalhandler@1.3.2(supports-color@5.5.0): dependencies: - debug: 2.6.9 + debug: 2.6.9(supports-color@5.5.0) encodeurl: 2.0.0 escape-html: 1.0.3 on-finished: 2.4.1 @@ -6821,7 +6758,9 @@ snapshots: mlly: 1.8.1 rollup: 4.59.0 - follow-redirects@1.16.0: {} + follow-redirects@1.16.0(debug@4.4.3(supports-color@7.2.0)): + optionalDependencies: + debug: 4.4.3(supports-color@7.2.0) form-data@4.0.5: dependencies: @@ -6831,8 +6770,6 @@ snapshots: hasown: 2.0.2 mime-types: 2.1.35 - forwarded-parse@2.1.2: {} - forwarded@0.2.0: {} fresh@0.5.2: {} @@ -6884,6 +6821,10 @@ snapshots: dependencies: is-glob: 4.0.3 + glob-to-regex.js@1.3.1(tslib@2.8.1): + dependencies: + tslib: 2.8.1 + glob@13.0.6: dependencies: minimatch: 10.2.4 @@ -6966,10 +6907,10 @@ snapshots: http-status-codes@2.3.0: {} - https-proxy-agent@5.0.1: + https-proxy-agent@5.0.1(supports-color@7.2.0): dependencies: - agent-base: 6.0.2 - debug: 4.4.3(supports-color@5.5.0) + agent-base: 6.0.2(supports-color@7.2.0) + debug: 4.4.3(supports-color@7.2.0) transitivePeerDependencies: - supports-color @@ -6977,6 +6918,8 @@ snapshots: humanize-number@0.0.2: {} + hyperdyperid@1.2.0: {} + iconv-lite@0.4.24: dependencies: safer-buffer: 2.1.2 @@ -6996,20 +6939,6 @@ snapshots: ignore@5.3.2: {} - import-in-the-middle@2.0.6: - dependencies: - acorn: 8.16.0 - acorn-import-attributes: 1.9.5(acorn@8.16.0) - cjs-module-lexer: 2.2.0 - module-details-from-path: 1.0.4 - - import-in-the-middle@3.0.0: - dependencies: - acorn: 8.16.0 - acorn-import-attributes: 1.9.5(acorn@8.16.0) - cjs-module-lexer: 2.2.0 - module-details-from-path: 1.0.4 - imurmurhash@0.1.4: {} indent-string@5.0.0: {} @@ -7155,10 +7084,10 @@ snapshots: on-finished: 2.4.1 passthrough-counter: 1.0.0 - koa-websocket@7.0.0: + koa-websocket@7.0.0(supports-color@7.2.0): dependencies: co: 4.6.0 - debug: 4.4.3(supports-color@5.5.0) + debug: 4.4.3(supports-color@7.2.0) koa-compose: 4.1.0 ws: 8.19.0 transitivePeerDependencies: @@ -7166,7 +7095,7 @@ snapshots: - supports-color - utf-8-validate - koa@2.16.4: + koa@2.16.4(supports-color@5.5.0): dependencies: accepts: 1.3.8 cache-content-type: 1.0.1 @@ -7194,6 +7123,34 @@ snapshots: transitivePeerDependencies: - supports-color + koa@2.16.4(supports-color@7.2.0): + dependencies: + accepts: 1.3.8 + cache-content-type: 1.0.1 + content-disposition: 0.5.4 + content-type: 1.0.5 + cookies: 0.9.1 + debug: 4.4.3(supports-color@7.2.0) + delegates: 1.0.0 + depd: 2.0.0 + destroy: 1.2.0 + encodeurl: 1.0.2 + escape-html: 1.0.3 + fresh: 0.5.2 + http-assert: 1.5.0 + http-errors: 1.8.1 + is-generator-function: 1.1.2 + koa-compose: 4.1.0 + koa-convert: 2.0.0 + on-finished: 2.4.1 + only: 0.0.2 + parseurl: 1.3.3 + statuses: 1.5.0 + type-is: 1.6.18 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + lilconfig@3.1.3: {} lines-and-columns@1.2.4: {} @@ -7253,6 +7210,23 @@ snapshots: map-age-cleaner: 0.1.3 mimic-fn: 4.0.0 + memfs@4.80.0: + dependencies: + '@jsonjoy.com/fs-core': 4.80.0(tslib@2.8.1) + '@jsonjoy.com/fs-fsa': 4.80.0(tslib@2.8.1) + '@jsonjoy.com/fs-node': 4.80.0(tslib@2.8.1) + '@jsonjoy.com/fs-node-builtins': 4.80.0(tslib@2.8.1) + '@jsonjoy.com/fs-node-to-fsa': 4.80.0(tslib@2.8.1) + '@jsonjoy.com/fs-node-utils': 4.80.0(tslib@2.8.1) + '@jsonjoy.com/fs-print': 4.80.0(tslib@2.8.1) + '@jsonjoy.com/fs-snapshot': 4.80.0(tslib@2.8.1) + '@jsonjoy.com/json-pack': 1.21.0(tslib@2.8.1) + '@jsonjoy.com/util': 1.9.0(tslib@2.8.1) + glob-to-regex.js: 1.3.1(tslib@2.8.1) + thingies: 2.6.1(tslib@2.8.1) + tree-dump: 1.1.0(tslib@2.8.1) + tslib: 2.8.1 + merge-descriptors@1.0.3: {} merge2@1.4.1: {} @@ -7293,10 +7267,6 @@ snapshots: pkg-types: 1.3.1 ufo: 1.6.3 - mock-fs@5.5.0: {} - - module-details-from-path@1.0.4: {} - mri@1.2.0: {} ms@2.0.0: {} @@ -7367,6 +7337,30 @@ snapshots: outdent@0.5.0: {} + oxc-parser@0.152.0: + dependencies: + '@oxc-project/types': 0.152.0 + optionalDependencies: + '@oxc-parser/binding-android-arm-eabi': 0.152.0 + '@oxc-parser/binding-android-arm64': 0.152.0 + '@oxc-parser/binding-darwin-arm64': 0.152.0 + '@oxc-parser/binding-darwin-x64': 0.152.0 + '@oxc-parser/binding-freebsd-x64': 0.152.0 + '@oxc-parser/binding-linux-arm-gnueabihf': 0.152.0 + '@oxc-parser/binding-linux-arm-musleabihf': 0.152.0 + '@oxc-parser/binding-linux-arm64-gnu': 0.152.0 + '@oxc-parser/binding-linux-arm64-musl': 0.152.0 + '@oxc-parser/binding-linux-ppc64-gnu': 0.152.0 + '@oxc-parser/binding-linux-riscv64-gnu': 0.152.0 + '@oxc-parser/binding-linux-riscv64-musl': 0.152.0 + '@oxc-parser/binding-linux-s390x-gnu': 0.152.0 + '@oxc-parser/binding-linux-x64-gnu': 0.152.0 + '@oxc-parser/binding-linux-x64-musl': 0.152.0 + '@oxc-parser/binding-openharmony-arm64': 0.152.0 + '@oxc-parser/binding-win32-arm64-msvc': 0.152.0 + '@oxc-parser/binding-win32-ia32-msvc': 0.152.0 + '@oxc-parser/binding-win32-x64-msvc': 0.152.0 + oxc-resolver@11.19.1: optionalDependencies: '@oxc-resolver/binding-android-arm-eabi': 11.19.1 @@ -7490,18 +7484,6 @@ snapshots: duplexify: 3.7.1 through2: 2.0.5 - pg-int8@1.0.1: {} - - pg-protocol@1.13.0: {} - - pg-types@2.2.0: - dependencies: - pg-int8: 1.0.1 - postgres-array: 2.0.0 - postgres-bytea: 1.0.1 - postgres-date: 1.0.7 - postgres-interval: 1.2.0 - phoenix@1.7.10: {} phoenix@1.8.5: {} @@ -7547,16 +7529,6 @@ snapshots: source-map-js: 1.2.1 optional: true - postgres-array@2.0.0: {} - - postgres-bytea@1.0.1: {} - - postgres-date@1.0.7: {} - - postgres-interval@1.2.0: - dependencies: - xtend: 4.0.2 - prettier@2.8.8: {} pretty-ms@8.0.0: @@ -7646,13 +7618,6 @@ snapshots: require-from-string@2.0.2: {} - require-in-the-middle@8.0.1: - dependencies: - debug: 4.4.3(supports-color@5.5.0) - module-details-from-path: 1.0.4 - transitivePeerDependencies: - - supports-color - resolve-cwd@3.0.0: dependencies: resolve-from: 5.0.0 @@ -7721,9 +7686,9 @@ snapshots: semver@7.7.4: {} - send@0.19.2: + send@0.19.2(supports-color@5.5.0): dependencies: - debug: 2.6.9 + debug: 2.6.9(supports-color@5.5.0) depd: 2.0.0 destroy: 1.2.0 encodeurl: 2.0.0 @@ -7739,23 +7704,25 @@ snapshots: transitivePeerDependencies: - supports-color - sentry-testkit@6.2.2: + sentry-testkit@6.2.2(supports-color@5.5.0): dependencies: - body-parser: 1.20.4 - express: 4.22.1 + body-parser: 1.20.4(supports-color@5.5.0) + express: 4.22.1(supports-color@5.5.0) transitivePeerDependencies: - supports-color + sentry@0.45.0: {} + serialize-error@7.0.1: dependencies: type-fest: 0.13.1 - serve-static@1.16.3: + serve-static@1.16.3(supports-color@5.5.0): dependencies: encodeurl: 2.0.0 escape-html: 1.0.3 parseurl: 1.3.3 - send: 0.19.2 + send: 0.19.2(supports-color@5.5.0) transitivePeerDependencies: - supports-color @@ -7911,6 +7878,10 @@ snapshots: dependencies: has-flag: 4.0.0 + supports-color@8.1.1: + dependencies: + has-flag: 4.0.0 + tar-stream@3.1.8: dependencies: b4a: 1.8.0 @@ -7947,10 +7918,14 @@ snapshots: dependencies: any-promise: 1.3.0 - threads@1.7.0: + thingies@2.6.1(tslib@2.8.1): + dependencies: + tslib: 2.8.1 + + threads@1.7.0(supports-color@7.2.0): dependencies: callsites: 3.1.0 - debug: 4.4.3(supports-color@5.5.0) + debug: 4.4.3(supports-color@7.2.0) is-observable: 2.1.0 observable-fns: 0.6.1 optionalDependencies: @@ -7991,6 +7966,10 @@ snapshots: tr46@0.0.3: {} + tree-dump@1.1.0(tslib@2.8.1): + dependencies: + tslib: 2.8.1 + tree-kill@1.2.2: {} treeify@1.1.0: {} @@ -8025,7 +8004,7 @@ snapshots: tsscmp@1.0.6: {} - tsup@8.5.1(@swc/core@1.15.18)(jiti@1.21.7)(postcss@8.5.8)(tsx@4.21.0)(typescript@4.9.5)(yaml@2.8.2): + tsup@8.5.1(@swc/core@1.15.18)(jiti@1.21.7)(postcss@8.5.8)(supports-color@5.5.0)(tsx@4.21.0)(typescript@4.9.5)(yaml@2.8.2): dependencies: bundle-require: 5.1.0(esbuild@0.27.4) cac: 6.7.14 @@ -8054,13 +8033,42 @@ snapshots: - tsx - yaml - tsup@8.5.1(@swc/core@1.15.18)(jiti@1.21.7)(postcss@8.5.8)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2): + tsup@8.5.1(@swc/core@1.15.18)(jiti@1.21.7)(postcss@8.5.8)(supports-color@7.2.0)(tsx@4.21.0)(typescript@4.9.5)(yaml@2.8.2): dependencies: bundle-require: 5.1.0(esbuild@0.27.4) cac: 6.7.14 chokidar: 4.0.3 consola: 3.4.2 - debug: 4.4.3(supports-color@5.5.0) + debug: 4.4.3(supports-color@7.2.0) + esbuild: 0.27.4 + fix-dts-default-cjs-exports: 1.0.1 + joycon: 3.1.1 + picocolors: 1.1.1 + postcss-load-config: 6.0.1(jiti@1.21.7)(postcss@8.5.8)(tsx@4.21.0)(yaml@2.8.2) + resolve-from: 5.0.0 + rollup: 4.59.0 + source-map: 0.7.6 + sucrase: 3.35.1 + tinyexec: 0.3.2 + tinyglobby: 0.2.15 + tree-kill: 1.2.2 + optionalDependencies: + '@swc/core': 1.15.18 + postcss: 8.5.8 + typescript: 4.9.5 + transitivePeerDependencies: + - jiti + - supports-color + - tsx + - yaml + + tsup@8.5.1(@swc/core@1.15.18)(jiti@1.21.7)(postcss@8.5.8)(supports-color@7.2.0)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2): + dependencies: + bundle-require: 5.1.0(esbuild@0.27.4) + cac: 6.7.14 + chokidar: 4.0.3 + consola: 3.4.2 + debug: 4.4.3(supports-color@7.2.0) esbuild: 0.27.4 fix-dts-default-cjs-exports: 1.0.1 joycon: 3.1.1 diff --git a/scripts/check-node-version.sh b/scripts/check-node-version.sh new file mode 100755 index 000000000..03bf2c8a3 --- /dev/null +++ b/scripts/check-node-version.sh @@ -0,0 +1,77 @@ +#!/usr/bin/env bash +# Fails unless the running node, every Dockerfile's `ARG NODE_VERSION=` and the +# cimg/node tags in .circleci/config.yml all match .tool-versions. +# +# Usage: scripts/check-node-version.sh [expected-node-version] +# The argument overrides the version `node -v` must report, for CI jobs that +# deliberately run on another Node (the integration test matrix). + +set -euo pipefail + +cd "$(dirname "$0")/.." + +# legacy_test deliberately runs the v1 CLI deploy on Node 18 +EXEMPT_JOBS="legacy_test" + +want=$(awk '$1 == "nodejs" { print $2 }' .tool-versions) +if [ -z "$want" ]; then + echo "No nodejs entry in .tool-versions" >&2 + exit 1 +fi +major=${want%%.*} +errors=() + +expected_node=${1:-$want} +actual_node=$(node -v) +actual_node=${actual_node#v} +if [ "$actual_node" != "$expected_node" ]; then + errors+=("node -v is $actual_node, expected $expected_node") +fi + +while IFS= read -r dockerfile; do + while IFS= read -r arg; do + if [ "$arg" != "$want" ]; then + errors+=("$dockerfile: ARG NODE_VERSION=$arg") + fi + done < <(sed -n 's/^ARG NODE_VERSION=\([^[:space:]]*\).*/\1/p' "$dockerfile") +done < <(find . -name node_modules -prune -o -name .git -prune -o -type f -name 'Dockerfile*' -print | sort) + +config=.circleci/config.yml + +while read -r job tag; do + case " $EXEMPT_JOBS " in *" $job "*) continue ;; esac + if [ "$tag" != "$want" ]; then + errors+=("$config: job $job uses cimg/node:$tag") + fi +done < <(awk ' + /^[^ #]/ { section = $1 } + section == "jobs:" && /^ [A-Za-z0-9_-]+:/ { job = $1; sub(/:$/, "", job) } + /image: *cimg\/node:/ { + tag = $0 + sub(/.*cimg\/node:/, "", tag) + sub(/[[:space:]].*/, "", tag) + gsub(/["\047]/, "", tag) + if (tag !~ /^<&2 + printf ' %s\n' "${errors[@]}" >&2 + exit 1 +fi + +echo "Node pins match .tool-versions ($want), node -v is $actual_node"