diff --git a/.github/workflows/cucumber.yaml b/.github/workflows/cucumber.yaml deleted file mode 100644 index adc503af9..000000000 --- a/.github/workflows/cucumber.yaml +++ /dev/null @@ -1,55 +0,0 @@ -name: Cucumber E2E tests - -on: - workflow_dispatch: - push: - paths: - - "features/**" - # risky... but we trust our developers :finger_crossed: - # pull_request: - # paths: - # - "features/**" - -jobs: - make_salad: - name: Cucumber tests - strategy: - fail-fast: false - matrix: - os: [ubuntu-latest, windows-2025] - # We only test LTS for now - node-version: [24] - - runs-on: ${{ matrix.os }} - - steps: - - name: Mask secrets - run: | - echo "::add-mask::${{ secrets.APIFY_TEST_USER_API_TOKEN }}" - - - uses: actions/checkout@v6 - - - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v6 - with: - node-version: ${{ matrix.node-version }} - package-manager-cache: false - - - name: Enable corepack - run: | - corepack enable - corepack prepare yarn@stable --activate - - - name: Activate cache for yarn - uses: actions/setup-node@v6 - with: - cache: yarn - - - name: Install Dependencies - run: yarn - - - name: Run Cucumber tests - env: - APIFY_CLI_DISABLE_TELEMETRY: 1 - TEST_USER_TOKEN: ${{ secrets.APIFY_TEST_USER_API_TOKEN }} - run: yarn test:cucumber diff --git a/.github/workflows/e2e.yaml b/.github/workflows/e2e.yaml new file mode 100644 index 000000000..068ebf70a --- /dev/null +++ b/.github/workflows/e2e.yaml @@ -0,0 +1,87 @@ +name: E2E Tests + +on: + # Run after a pre-release is published to npm + workflow_run: + workflows: ["Create a pre-release"] + types: [completed] + branches: [master] + + # TODO: Remove after validating the workflow works + pull_request: + branches: [master] + + # Manual trigger with optional version override + workflow_dispatch: + inputs: + cli_version: + description: "npm version/tag to test (e.g. beta, latest, 1.5.0-beta.1). Leave empty for default (npx apify-cli@beta)." + required: false + + # Daily fallback at 06:00 UTC + schedule: + - cron: "0 6 * * *" + +concurrency: + group: e2e-${{ github.event_name }}-${{ github.ref }} + cancel-in-progress: false + +jobs: + e2e: + if: > + startsWith(github.repository, 'apify/') && + (github.event_name != 'workflow_run' || + github.event.workflow_run.conclusion == 'success') + + name: E2E Tests (${{ matrix.os }}) + timeout-minutes: 30 + + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest] + + runs-on: ${{ matrix.os }} + + steps: + - name: Mask secrets + run: | + echo "::add-mask::${{ secrets.APIFY_TEST_USER_API_TOKEN }}" + + - uses: actions/checkout@v6 + + - name: Use Node.js 24 + uses: actions/setup-node@v6 + with: + node-version: 24 + package-manager-cache: false + + - name: Enable corepack + run: | + corepack enable + corepack prepare yarn@stable --activate + + - name: Activate cache for yarn + uses: actions/setup-node@v6 + with: + cache: yarn + + - name: Install dependencies + run: yarn + + - name: Install specific CLI version + if: ${{ inputs.cli_version }} + env: + CLI_VERSION: ${{ inputs.cli_version }} + run: npm install -g "apify-cli@${CLI_VERSION}" + + - name: Verify installed CLI version + if: ${{ inputs.cli_version }} + run: apify --version + + - name: Run E2E tests + env: + APIFY_CLI_DISABLE_TELEMETRY: 1 + APIFY_CLI_E2E_MODE: ${{ inputs.cli_version && 'global' || '' }} + TEST_USER_TOKEN: ${{ secrets.APIFY_TEST_USER_API_TOKEN }} + run: yarn test:e2e diff --git a/cucumber.json b/cucumber.json deleted file mode 100644 index 1030108d3..000000000 --- a/cucumber.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "default": { - "import": ["features/**/*.ts"], - "parallel": 4 - } -} diff --git a/features/actor-run-input.feature.md b/features/actor-run-input.feature.md deleted file mode 100644 index 5b4129c0c..000000000 --- a/features/actor-run-input.feature.md +++ /dev/null @@ -1,124 +0,0 @@ -# Feature: Actor Run Input (`apify run` / `apify actor run`) - -- As an Actor developer -- I want to pass input to the locally developer Actor -- In order to test different scenario -- And set and verify my assertions on the Actor behavior - -## Background: - -- Given my `pwd` is a fully initialized Actor project directory -- And the `actor.json` is valid -- And the Actor implementation doesn't throw itself - -## Rule: The `run` command `--input` flag accepts -in-line JSON - -### Example: pass JSON string to `--input` flag - -- When I run: - ``` - $ apify run --input='{"foo":"bar"}' - ``` -- Then the local Run has an input JSON: - ``` - {"foo":"bar"} - ``` -- And the local Actor Run has started -- And the exit status code is `0` - -### Example: `--input` suggests `--input-file` flag on filename input - -- When I run: - ``` - $ apify run --input=./my-path/file.json - ``` -- Then I don't see any Node.js exception -- And the local Actor Run hasn't even started -- And I can read text on stderr: - ``` - use the "--input-file=" flag instead - ``` -- And the exit status code is not `0` - -### Example: `--input` has a short alias `-i` - -- When I run: - ``` - $ apify run -i='{"foo":"bar"}' - ``` -- And the local Actor Run has started -- Then the local Run has an input JSON: - ``` - {"foo":"bar"} - ``` - -## Rule: The `--input-file` accepts JSON from a file - -### Example: Existing, valid file input - -- Given the following input provided via file `my-file.json`: - ``` - {"foo":"bar"} - ``` -- When I run: - ``` - $ apify run --input-file=my-file.json - ``` -- Then the local Run has an input JSON: - ``` - {"foo":"bar"} - ``` -- And the local Actor Run has started -- And the exit status code is `0` - -### Example: The input file doesn't exist - -- When I run: - - ``` - $ apify run --input-file=the-file-that-doesnt-exist.json - ``` - -- Then I don't see any Node.js exception -- And the local Actor Run hasn't even started -- And I can read text on stderr: - ``` - Cannot read input file at - ``` -- And the exit status code is not `0` - -## Rule: The `run` command accepts JSON stdin input - -### Example: `--input-file` accepts JSON on stdin with a `-` shorthand - -- Given the following input provided via standard input: - ``` - {"foo":"bar"} - ``` -- When I run: - ``` - $ apify run --input-file=- - ``` -- Then the local Run has an input JSON: - ``` - {"foo":"bar"} - ``` -- And the local Actor Run has started -- And the exit status code is `0` - -### Example: `run` command accepts JSON on stdin an assumed alias for `--input-file=-` - -- Given the following input provided via standard input: - ``` - {"foo":"bar"} - ``` -- When I run: - ``` - $ apify run - ``` -- Then the local Run has an input JSON: - ``` - {"foo":"bar"} - ``` -- And the local Actor Run has started -- And the exit status code is `0` diff --git a/features/builds-namespace.feature.md b/features/builds-namespace.feature.md deleted file mode 100644 index d110eae0b..000000000 --- a/features/builds-namespace.feature.md +++ /dev/null @@ -1,121 +0,0 @@ -# Feature: Builds namespace - -- As an Actor developer or user -- I want to be able to manage the builds of my actors on Apify Console -- In order to trigger new builds from the CLI, list them, and get details about them - -## Background: - -- Given my `pwd` is a fully initialized Actor project directory -- And the `actor.json` is valid -- And I am a logged in Apify Console User - -## Rule: Creating builds works - -### Example: calling create with invalid actor ID fails - -- When I run: - ``` - $ apify builds create invalid-id - ``` -- Then I can read text on stdout: - ``` - Actor with name or ID "invalid-id" was not found - ``` - -### Example: calling create from an unpublished actor directory fails - -- When I run: - ``` - $ apify builds create - ``` -- Then I can read text on stdout: - ``` - Actor with name "{{ testActorName }}" was not found - ``` - -### Example: calling create from a published actor directory works - -- Given the local Actor is pushed to the Apify platform -- When I run: - ``` - $ apify builds create - ``` -- Then I can read text on stdout: - ``` - Build Started - ``` -- And I can read text on stdout: - ``` - {{ testActorName}} - ``` - -### Example: calling create from a published actor with `--json` prints valid JSON data - -- Given the local Actor is pushed to the Apify platform -- When I run: - - ``` - $ apify builds create --json - ``` - - - Then I can read valid JSON on stdout - -## Rule: Printing information about builds works - -### Example: calling info with invalid build ID fails - -- When I run: - ``` - $ apify builds info invalid-id - ``` -- Then I can read text on stdout: - ``` - Build with ID "invalid-id" was not found - ``` - -### Example: calling info with valid build ID works - -- Given the local Actor is pushed to the Apify platform -- When I run: - ``` - $ apify builds create - ``` -- And I capture the build ID -- And I run with captured data: - ``` - $ apify builds info {{ buildId }} - ``` -- Then I can read text on stdout: - ``` - {{ testActorName }} - ``` - -### Example: calling info with valid build ID and `--json` prints valid JSON data - -- Given the local Actor is pushed to the Apify platform -- When I run: - ``` - $ apify builds create - ``` -- And I capture the build ID -- And I run with captured data: - ``` - $ apify builds info {{ buildId }} --json - ``` -- Then I can read valid JSON on stdout - -## Rule: Listing builds works - - - -### Example: calling list with --json prints valid JSON data - -- Given the local Actor is pushed to the Apify platform -- When I run: - ``` - $ apify builds ls --json - ``` -- Then I can read valid JSON on stdout - - diff --git a/features/help-command.feature.md b/features/help-command.feature.md deleted file mode 100644 index 86e5f7258..000000000 --- a/features/help-command.feature.md +++ /dev/null @@ -1,74 +0,0 @@ -# Feature: Help command (`apify help`) - -- As a CLI user -- I want to get easy help messages - -## Rule: `apify help` prints the whole help message - -### Example: call `apify help` with no commands - -- When I run anywhere: - ``` - $ apify help - ``` -- Then I can read text on stdout: - ``` - https://apify.com/contact - ``` - -### Example: call `actor help` with no commands - -- When I run anywhere: - ``` - $ actor help - ``` -- Then I can read text on stdout: - ``` - https://apify.com/contact - ``` - -### Example: call `apify help help` prints the main message - -- When I run anywhere: - ``` - $ apify help help - ``` -- Then I can read text on stdout: - ``` - https://apify.com/contact - ``` - -## Rule: `apify help ` prints a command's help message - -### Example: `apify help -h` - -- When I run anywhere: - ``` - $ apify help -h - ``` -- Then I can read text on stdout: - ``` - Prints out help about a command, or all available commands. - ``` - -### Example: `apify help run` - -- When I run anywhere: - ``` - $ apify help run - ``` -- Then I can read text on stdout: - ``` - Executes Actor locally with simulated Apify environment variables. - ``` - -### Example: `apify run --help` returns the same message as `apify help run` - -- When I run anywhere: - ``` - $ apify run --help - ``` -- Then I can read text on stdout: - ``` - Executes Actor locally with simulated Apify environment variables. - ``` diff --git a/features/invalid-actor-json-output.feature.md b/features/invalid-actor-json-output.feature.md deleted file mode 100644 index 1c09d8429..000000000 --- a/features/invalid-actor-json-output.feature.md +++ /dev/null @@ -1,29 +0,0 @@ -# Feature: Print out path to invalid `actor.json` - -- As an Actor developer or user -- I want to know the path to the invalid `actor.json` -- In order to fix it -- If the file is not valid JSON - -## Background: - -- Given my `pwd` is a fully initialized Actor project directory -- And the `actor.json` is invalid - -## Rule: Print out path to invalid `actor.json` - -### Example: Invalid `actor.json` file - -- When I run: - ``` - $ apify run - ``` -- Then I can read text on stderr: - ``` - Failed to read local config at path: - ``` -- And I can read text on stderr: - ``` - Expected property name or '}' - ``` -- And the exit status code is `5` diff --git a/features/test-implementations/0.utils.ts b/features/test-implementations/0.utils.ts deleted file mode 100644 index e72cc316b..000000000 --- a/features/test-implementations/0.utils.ts +++ /dev/null @@ -1,12 +0,0 @@ -export interface StringMatcherTemplate { - testActorName?: string; - buildId?: string; -} - -export function replaceMatchersInString(str: string, matchers: StringMatcherTemplate): string { - for (const [key, replaceValue] of Object.entries(matchers) as [keyof StringMatcherTemplate, string][]) { - str = str.replace(new RegExp(`{{\\s*${key}\\s*}}`, 'g'), replaceValue); - } - - return str; -} diff --git a/features/test-implementations/0.world.ts b/features/test-implementations/0.world.ts deleted file mode 100644 index 07844ac39..000000000 --- a/features/test-implementations/0.world.ts +++ /dev/null @@ -1,243 +0,0 @@ -import { mkdir, readFile } from 'node:fs/promises'; -import { createRequire } from 'node:module'; -import { fileURLToPath } from 'node:url'; - -import type { IWorld } from '@cucumber/cucumber'; -import { Result } from '@sapphire/result'; -import type { ApifyClient } from 'apify-client'; -import { type ExecaError, execaNode, type Options, type Result as ExecaResult } from 'execa'; - -type DynamicOptions = { - -readonly [P in keyof Options]: Options[P]; -}; - -/** - * An interface representing the `this` context in cucumber. This will store things, from what command to run, to the output of the command, its standard output/error, and any other information that needs to be shared between steps. - */ -export interface TestWorld extends IWorld { - testResults?: { - exitCode: number; - stdout: string; - stderr: string; - runResults: Awaited> | null; - }; - testActor?: { - /** - * The path to use as the working directory for the test/command execution. Usually, it'll be in ./test/tmp/ - */ - pwd?: URL; - /** - * Whether the actor is ready to execute `When` steps. This is only used to ensure order in certain `Given` requirements - */ - initialized?: boolean; - /** - * Input that should be provided to the command via stdin - */ - stdinInput?: string; - /** - * The name of the actor, used for matchers - */ - name?: string; - }; - apifyClient?: ApifyClient; - authStatePath?: string; - capturedData?: Record; -} - -/** - * The root path of the CLI project, used as the cwd for process execution - */ -export const ProjectRoot = new URL('../../', import.meta.url); - -export const ApifyDevRunFile = new URL('./src/entrypoints/apify.ts', ProjectRoot); -export const ActorDevRunFile = new URL('./src/entrypoints/actor.ts', ProjectRoot); -export const TestTmpRoot = new URL('./test/tmp/', ProjectRoot); - -await mkdir(TestTmpRoot, { recursive: true }); - -const require = createRequire(import.meta.url); - -const tsxCli = require.resolve('tsx/cli'); - -if (!tsxCli) { - throw new RangeError('tsx/cli not found! Make sure you have all dependencies installed'); -} - -export async function executeCommand({ - rawCommand, - stdin, - cwd = TestTmpRoot, - env, -}: { - rawCommand: string; - stdin?: string; - cwd?: string | URL; - env?: Record; -}) { - // Step 0: ensure the command is executable -> strip out $, trim spaces - const commandToRun = rawCommand.split('\n').map((str) => str.replace(/^\$/, '').trim()); - - if (commandToRun.length > 1) { - // Step 0.1: ensure the command string is not multi-line - throw new RangeError(`Command cannot be multi-line, received:\n${commandToRun}`); - } - - // step 1: get the first element, and make sure it starts with `apify` - const [command] = commandToRun; - - if (!command.startsWith('apify') && !command.startsWith('actor')) { - // TODO: maybe try to parse these commands out and provide stdin that way, but for now, its better to get the writer to use the existing rules - if (command.startsWith('echo') || command.startsWith('jo')) { - throw new RangeError( - `When writing a test case, please use the "given the following input provided via standard input" rule for providing standard input to the command you're testing.\nReceived: ${command}`, - ); - } - - throw new RangeError(`Command must start with 'apify' or 'actor', received: ${command}`); - } - - const devRunFile = command.startsWith('apify') ? ApifyDevRunFile : ActorDevRunFile; - - const cleanCommand = command.replace(/^apify|actor/, '').trim(); - - const options: DynamicOptions = { - cwd, - env, - }; - - if (process.env.CUCUMBER_PRINT_EXEC) { - options.verbose = 'full'; - } - - if (stdin) { - options.input = stdin; - } - - function stripQuotes(str: string) { - return str.replace(/^['"]/, '').replace(/['"]$/, ''); - } - - // Who knows if this will be stable! - const commandArguments = cleanCommand.split(' ').flatMap((val) => { - const split = val.split('='); - - // --input, --run - if (split.length === 1) { - return stripQuotes(val); - } - - // --xxx=yyy[=zzz...] - const [key, ...value] = split; - - // technically this shouldn't happen, but sanity insanity - if (value.length === 0) { - return stripQuotes(key); - } - - // Join it back together, as we split it apart, hopefully keeping it 1:1 with what the user wrote - return [stripQuotes(key), stripQuotes(value.join('='))].join('='); - }); - - // Step 2: execute the command - return Result.fromAsync< - ExecaResult<{ cwd: typeof cwd; input: typeof stdin }>, - ExecaError<{ cwd: typeof cwd; input: typeof stdin }> - >(async () => { - const process = execaNode( - tsxCli, - [fileURLToPath(devRunFile), ...commandArguments], - options as { cwd: typeof cwd; input: typeof stdin }, - ); - - // This is needed as otherwise the process just hangs and never resolves when running certain cli commands that may try to read from stdin - if (!stdin) { - process.stdin?.end(); - } - - return await process; - }); -} - -export function assertWorldIsValid( - world: TestWorld, -): asserts world is TestWorld & { testActor: { pwd: URL; initialized: true; name: string } } { - if (!world.testActor || !world.testActor.initialized) { - throw new RangeError( - 'Test actor must be initialized before running any subsequent background requirements. You may have the order of your steps wrong. The "Given my `pwd` is a fully initialized Actor project directory" step needs to run before this step', - ); - } -} - -export function assertWorldHasRanCommand( - world: TestWorld, -): asserts world is TestWorld & { testResults: { exitCode: number; stdout: string; stderr: string } } { - if (!world.testResults) { - throw new RangeError('A command must be ran before this assertion can be checked'); - } -} - -export function assertWorldHasRunResult(world: TestWorld): asserts world is TestWorld & { - testResults: { - exitCode: number; - stdout: string; - stderr: string; - runResults: Awaited>; - }; -} { - if (!world.testResults || !world.testResults.runResults) { - const message = ['A command must be ran successfully before this assertion can be checked.']; - - if (world.testResults) { - message.push(`The command exited with code ${world.testResults.exitCode}`); - - if (world.testResults.stdout) { - message.push(`stdout: ${world.testResults.stdout}`); - } - - if (world.testResults.stderr) { - message.push(`stderr: ${world.testResults.stderr}`); - } - } else { - message.push('No command has been ran yet'); - } - - throw new RangeError(message.join('\n')); - } -} - -export function assertWorldIsLoggedIn(world: TestWorld): asserts world is TestWorld & { apifyClient: ApifyClient } { - if (!world.apifyClient) { - throw new RangeError('You must run the "Given a logged in Apify Console user" step before running this step'); - } -} - -export function assertWorldHasCapturedData(world: TestWorld): asserts world is TestWorld & { - capturedData: Record; -} { - if (!world.capturedData) { - throw new RangeError(`You must run the "I capture the ID" step before running this step`); - } -} - -export async function getActorRunResults(world: TestWorld & { testActor: { pwd: URL; initialized: true } }) { - const startedPath = new URL('./storage/key_value_stores/default/STARTED.json', world.testActor.pwd); - const inputPath = new URL('./storage/key_value_stores/default/RECEIVED_INPUT.json', world.testActor.pwd); - - const result = await readFile(startedPath, 'utf8').catch(() => null); - const receivedInput = await readFile(inputPath, 'utf8').catch(() => null); - - if (!result) { - throw new Error('Actor did not start correctly'); - } - - const parsed = JSON.parse(result); - - if (parsed !== 'works') { - throw new Error('Actor did not start correctly, expected "works" to be set in STARTED.json'); - } - - return { - started: parsed as 'works', - receivedInput: receivedInput ? JSON.parse(receivedInput) : null, - }; -} diff --git a/features/test-implementations/1.setup.ts b/features/test-implementations/1.setup.ts deleted file mode 100644 index 83b714845..000000000 --- a/features/test-implementations/1.setup.ts +++ /dev/null @@ -1,250 +0,0 @@ -import { randomBytes } from 'node:crypto'; -import { readFile, rm, writeFile } from 'node:fs/promises'; - -import { AfterAll, Given, setDefaultTimeout } from '@cucumber/cucumber'; -import { ApifyClient } from 'apify-client'; - -import { getApifyClientOptions } from '../../src/lib/utils.js'; -import { - assertWorldIsLoggedIn, - assertWorldIsValid, - executeCommand, - getActorRunResults, - TestTmpRoot, - type TestWorld, -} from './0.world'; - -setDefaultTimeout(20_000); - -const createdActors: URL[] = []; -const pushedActorIds: string[] = []; -let globalClient: ApifyClient; - -if (!process.env.DO_NOT_DELETE_CUCUMBER_TEST_ACTORS) { - AfterAll(async () => { - if (!createdActors.length) { - return; - } - - console.log(`\n Cleaning up Actors for worker ${process.env.CUCUMBER_WORKER_ID}...`); - - for (const path of createdActors) { - await rm(path, { recursive: true, force: true }); - } - }); - - AfterAll(async () => { - if (!pushedActorIds.length) { - return; - } - - console.log(`\n Cleaning up pushed Actors for worker ${process.env.CUCUMBER_WORKER_ID}...`); - - const me = await globalClient.user('me').get(); - - for (const id of pushedActorIds) { - try { - await globalClient.actor(`${me.username}/${id}`).delete(); - } catch (err) { - console.error(`Failed to delete Actor ${id}: ${(err as Error).message}`); - } - } - }); -} - -const actorJs = await readFile(new URL('./0.basic-actor.js', import.meta.url), 'utf8'); - -Given(/my `?pwd`? is a fully initialized actor project directory/i, { timeout: 120_000 }, async function () { - this.testActor ??= {}; - - if (this.testActor.initialized) { - throw new RangeError('A test actor cannot be initialized multiple times in one scenario'); - } - - const randomId = randomBytes(12).toString('hex'); - const actorName = `cucumber-${randomId}`; - - console.log(`\n Creating actor: ${actorName} for test...`); - - const result = await executeCommand({ - rawCommand: `apify create ${actorName} --template project_empty`, - }); - - if (result.isOk()) { - this.testActor.pwd = new URL(`./${actorName}/`, TestTmpRoot); - this.testActor.initialized = true; - this.testActor.name = actorName; - - createdActors.push(this.testActor.pwd); - - // Overwrite the main.js file in the actor with a specific one that should do specific test things - - const mainJsFile = new URL('./src/main.js', this.testActor.pwd); - await writeFile(mainJsFile, actorJs); - } else { - const error = result.unwrapErr(); - - throw new Error(`Failed to create actor: ${error.message}`); - } -}); - -Given(/the `actor.json` is valid/i, function () { - assertWorldIsValid(this); - - // Well we have no code right now to validate the actor.json file, so we'll just assume it's valid. 🤷 - // Maybe this is something we want to implement >:3 -}); - -Given(/the `actor.json` is invalid/i, async function () { - assertWorldIsValid(this); - - const actorJsonFile = new URL('./.actor/actor.json', this.testActor.pwd); - - // We'll just overwrite the file with some invalid JSON - await writeFile(actorJsonFile, `{ wow "name": "my-invalid-actor" }`); -}); - -Given(/the actor implementation doesn't throw itself/i, { timeout: 120_000 }, async function () { - assertWorldIsValid(this); - - const result = await executeCommand({ - rawCommand: 'apify run', - cwd: this.testActor.pwd, - }); - - if (!result.isOk()) { - const error = result.unwrapErr(); - - throw new Error(`Failed to run actor: ${error.message}`); - } - - // This throws if actor didn't work, which is plenty for this given step - getActorRunResults(this); - - // Clean up after ourselves - await rm(new URL('./storage/key_value_stores/default/STARTED.json', this.testActor.pwd), { force: true }); - await rm(new URL('./storage/key_value_stores/default/RECEIVED_INPUT.json', this.testActor.pwd), { force: true }); -}); - -Given(/the following input provided via (?:standard input|stdin)/i, function (jsonValue: string) { - assertWorldIsValid(this); - - if (typeof jsonValue !== 'string') { - throw new TypeError( - 'When using the `the following input provided via standard input` step, you must provide a text block containing a JSON object', - ); - } - - const parsed = JSON.parse(jsonValue); - - if (typeof parsed !== 'object' || !parsed) { - throw new TypeError( - 'When using the `the following input provided via standard input` step, you must provide a JSON object', - ); - } - - if (Array.isArray(parsed)) { - throw new TypeError( - 'When using the `the following input provided via standard input` step, you must provide a JSON object, not an array', - ); - } - - if (this.testActor.stdinInput) { - console.warn(`\n Warning: Overwriting existing stdin input: ${this.testActor.stdinInput}`); - } - - this.testActor.stdinInput = jsonValue; -}); - -Given( - /the following input provided via file `?(.+)`/i, - async function (filePath: string, jsonValue: string) { - assertWorldIsValid(this); - - if (typeof jsonValue !== 'string') { - throw new TypeError( - 'When using the `the following input provided via a file` step, you must provide a text block containing a JSON object', - ); - } - - const parsed = JSON.parse(jsonValue); - - if (typeof parsed !== 'object' || !parsed) { - throw new TypeError( - 'When using the `the following input provided via a file` step, you must provide a JSON object', - ); - } - - if (Array.isArray(parsed)) { - throw new TypeError( - 'When using the `the following input provided via a file` step, you must provide a JSON object, not an array', - ); - } - - const file = new URL(`./${filePath}`, this.testActor.pwd); - - await writeFile(file, jsonValue); - }, -); - -Given(/logged in apify console user/i, async function () { - if (!process.env.TEST_USER_TOKEN) { - throw new Error('No test user token provided'); - } - - // Try to make the client with the token - const client = new ApifyClient(getApifyClientOptions(process.env.TEST_USER_TOKEN)); - - try { - await client.user('me').get(); - } catch (err) { - throw new Error(`Failed to get user information: ${(err as Error).message}`); - } - - // Login with the CLI too - - const authStatePath = `cucumber-${randomBytes(12).toString('hex')}`; - - const result = await executeCommand({ - rawCommand: `apify login --token ${process.env.TEST_USER_TOKEN}`, - env: { - // Keep in sync with GLOBAL_CONFIGS_FOLDER in consts.ts - __APIFY_INTERNAL_TEST_AUTH_PATH__: authStatePath, - }, - }); - - // This will throw if there was an error - result.unwrap(); - - this.apifyClient = client; - this.authStatePath = authStatePath; - - // We need it for later cleanup - globalClient = client; -}); - -Given(/the local actor is pushed to the Apify platform/i, { timeout: 240_000 }, async function () { - assertWorldIsValid(this); - assertWorldIsLoggedIn(this); - - const extraEnv: Record = {}; - - if (this.authStatePath) { - extraEnv.__APIFY_INTERNAL_TEST_AUTH_PATH__ = this.authStatePath; - } - - const result = await executeCommand({ - rawCommand: 'apify push', - cwd: this.testActor.pwd, - env: extraEnv, - }); - - if (result.isOk()) { - pushedActorIds.push(this.testActor.name); - } else { - // This throws on errors - const err = result.unwrapErr(); - - throw new Error(`Failed to push actor: ${err.message}`); - } -}); diff --git a/features/test-implementations/2.execution.ts b/features/test-implementations/2.execution.ts deleted file mode 100644 index 65f7b4e52..000000000 --- a/features/test-implementations/2.execution.ts +++ /dev/null @@ -1,191 +0,0 @@ -import { When } from '@cucumber/cucumber'; - -import { replaceMatchersInString } from './0.utils'; -import { - assertWorldHasCapturedData, - assertWorldHasRanCommand, - assertWorldIsValid, - executeCommand, - getActorRunResults, - type TestWorld, -} from './0.world'; - -When(/i run anywhere:?$/i, async function (commandBlock: string) { - if (typeof commandBlock !== 'string') { - throw new TypeError('When using the `I run anywhere` step, you must provide a text block containing a command'); - } - - const extraEnv: Record = {}; - - if (this.authStatePath) { - extraEnv.__APIFY_INTERNAL_TEST_AUTH_PATH__ = this.authStatePath; - } - - const result = await executeCommand({ - rawCommand: commandBlock, - env: extraEnv, - }); - - if (result.isOk()) { - const value = result.unwrap(); - - if (this.testResults) { - console.error(`\n Warning: Overwriting existing test results: ${JSON.stringify(this.testResults)}`); - } - - this.testResults = { - exitCode: value.exitCode!, - stderr: value.stderr, - stdout: value.stdout, - runResults: null, - }; - } else { - const error = result.unwrapErr(); - - if (this.testResults) { - console.error(`\n Warning: Overwriting existing test results: ${JSON.stringify(this.testResults)}`); - } - - this.testResults = { - exitCode: error.exitCode!, - stderr: error.stderr, - stdout: error.stdout, - runResults: null, - }; - } -}); - -When(/i run:?$/i, async function (commandBlock: string) { - assertWorldIsValid(this); - - if (typeof commandBlock !== 'string') { - throw new TypeError('When using the `I run` step, you must provide a text block containing a command'); - } - - const extraEnv: Record = {}; - - if (this.authStatePath) { - extraEnv.__APIFY_INTERNAL_TEST_AUTH_PATH__ = this.authStatePath; - } - - const result = await executeCommand({ - rawCommand: commandBlock, - cwd: this.testActor.pwd, - stdin: this.testActor.stdinInput, - env: extraEnv, - }); - - const runResults = await getActorRunResults(this).catch(() => null); - - if (result.isOk()) { - const value = result.unwrap(); - - if (this.testResults) { - console.error(`\n Warning: Overwriting existing test results: ${JSON.stringify(this.testResults)}`); - } - - this.testResults = { - exitCode: value.exitCode!, - stderr: value.stderr, - stdout: value.stdout, - runResults, - }; - } else { - const error = result.unwrapErr(); - - if (this.testResults) { - console.error(`\n Warning: Overwriting existing test results: ${JSON.stringify(this.testResults)}`); - } - - this.testResults = { - exitCode: error.exitCode!, - stderr: error.stderr, - stdout: error.stdout, - runResults, - }; - } -}); - -When(/i capture the (build) id/i, function (match: string) { - assertWorldIsValid(this); - assertWorldHasRanCommand(this); - - this.capturedData ??= {}; - - switch (match.toLowerCase()) { - case 'build': { - const buildId = - this.testResults?.stderr.match(/Build Started \(ID: (\w+)\)/)?.[1] ?? - // Try stdout as well, as it might be there - this.testResults?.stdout.match(/Build Started \(ID: (\w+)\)/)?.[1]; - - if (!buildId) { - throw new Error('Failed to capture build ID'); - } - - this.capturedData.buildId = buildId; - - break; - } - - default: { - throw new TypeError(`Unhandled capture match type: ${match}`); - } - } -}); - -When(/i run with captured data/i, async function (commandBlock: string) { - assertWorldIsValid(this); - assertWorldHasCapturedData(this); - - if (typeof commandBlock !== 'string') { - throw new TypeError( - 'When using the `I run with captured data` step, you must provide a text block containing a command', - ); - } - - const extraEnv: Record = {}; - - if (this.authStatePath) { - extraEnv.__APIFY_INTERNAL_TEST_AUTH_PATH__ = this.authStatePath; - } - - const result = await executeCommand({ - rawCommand: replaceMatchersInString(commandBlock, { - buildId: this.capturedData.buildId, - }), - cwd: this.testActor.pwd, - stdin: this.testActor.stdinInput, - env: extraEnv, - }); - - const runResults = await getActorRunResults(this).catch(() => null); - - if (result.isOk()) { - const value = result.unwrap(); - - if (this.testResults) { - console.error(`\n Warning: Overwriting existing test results: ${JSON.stringify(this.testResults)}`); - } - - this.testResults = { - exitCode: value.exitCode!, - stderr: value.stderr, - stdout: value.stdout, - runResults, - }; - } else { - const error = result.unwrapErr(); - - if (this.testResults) { - console.error(`\n Warning: Overwriting existing test results: ${JSON.stringify(this.testResults)}`); - } - - this.testResults = { - exitCode: error.exitCode!, - stderr: error.stderr, - stdout: error.stdout, - runResults, - }; - } -}); diff --git a/features/test-implementations/3.results.ts b/features/test-implementations/3.results.ts deleted file mode 100644 index 8ae41d3cd..000000000 --- a/features/test-implementations/3.results.ts +++ /dev/null @@ -1,144 +0,0 @@ -import { deepStrictEqual, notStrictEqual, strictEqual } from 'node:assert'; - -import { Then } from '@cucumber/cucumber'; - -import { replaceMatchersInString } from './0.utils'; -import { assertWorldHasRanCommand, assertWorldHasRunResult, assertWorldIsValid, type TestWorld } from './0.world'; - -Then(/the local run has an input json/i, function (jsonBlock: string) { - assertWorldIsValid(this); - assertWorldHasRanCommand(this); - assertWorldHasRunResult(this); - - if (typeof jsonBlock !== 'string') { - throw new TypeError( - 'When using the `the local run has an input json` step, you must provide a text block containing the expected json object', - ); - } - - const parsed = JSON.parse(jsonBlock); - - if (typeof parsed !== 'object' || !parsed) { - throw new TypeError('When using the `the local run has an input json` step, you must provide a JSON object'); - } - - if (Array.isArray(parsed)) { - throw new TypeError( - 'When using the `the local run has an input json` step, you must provide a JSON object, not an array', - ); - } - - deepStrictEqual(this.testResults.runResults.receivedInput, parsed); -}); - -Then(/the local actor run has started/i, function () { - assertWorldIsValid(this); - assertWorldHasRanCommand(this); - assertWorldHasRunResult(this); - - // Impossible at this point, but we'll check anyway - strictEqual(this.testResults.runResults.started, 'works', 'Actor did not start correctly'); -}); - -Then(/the local actor run hasn't even started/i, function () { - assertWorldIsValid(this); - assertWorldHasRanCommand(this); - - strictEqual(this.testResults.runResults, null, 'Actor started when it should not have'); -}); - -Then(/the exit status code is `?(\d+)`?/i, function (expectedExitCode: number) { - assertWorldIsValid(this); - assertWorldHasRanCommand(this); - - if (typeof expectedExitCode !== 'number') { - throw new TypeError('When using the `the exit status code is` step, you must provide a number'); - } - - strictEqual(this.testResults.exitCode, expectedExitCode); -}); - -Then(/the exit status code is not `?(\d+)`?/i, function (expectedExitCode: number) { - assertWorldIsValid(this); - assertWorldHasRanCommand(this); - - if (typeof expectedExitCode !== 'number') { - throw new TypeError('When using the `the exit status code is not` step, you must provide a number'); - } - - notStrictEqual(this.testResults.exitCode, expectedExitCode); -}); - -Then(/i don't see any node\.js exception/i, function () { - assertWorldIsValid(this); - assertWorldHasRanCommand(this); - - // TODO: what does this actually mean? Not seeing any stack traces? Because in dev those will show, in prod they won't... -}); - -Then(/i can read text on stderr/i, function (expectedStdout: string) { - assertWorldHasRanCommand(this); - - if (typeof expectedStdout !== 'string') { - throw new TypeError( - 'When using the `i can read text on stderr` step, you must provide a string block with the expected text', - ); - } - - const lowercasedResult = this.testResults.stderr.toLowerCase(); - - let lowercasedExpected = expectedStdout; - - if (this.testActor) { - lowercasedExpected = replaceMatchersInString(lowercasedExpected, { - testActorName: this.testActor.name, - }); - } - - lowercasedExpected = lowercasedExpected.toLowerCase(); - - strictEqual( - lowercasedResult.includes(lowercasedExpected), - true, - `Expected to find "${lowercasedExpected}" in "${lowercasedResult}"`, - ); -}); - -Then(/i can read text on stdout/i, function (expectedStdout: string) { - assertWorldHasRanCommand(this); - - if (typeof expectedStdout !== 'string') { - throw new TypeError( - 'When using the `i can read text on stdout` step, you must provide a string block with the expected text', - ); - } - - const lowercasedResult = this.testResults.stdout.toLowerCase(); - - let lowercasedExpected = expectedStdout; - - if (this.testActor) { - lowercasedExpected = replaceMatchersInString(lowercasedExpected, { - testActorName: this.testActor.name, - }); - } - - lowercasedExpected = lowercasedExpected.toLowerCase(); - - strictEqual( - lowercasedResult.includes(lowercasedExpected), - true, - `Expected to find "${lowercasedExpected}" in "${lowercasedResult}"`, - ); -}); - -Then(/I can read valid JSON on stdout/i, function () { - assertWorldIsValid(this); - assertWorldHasRanCommand(this); - - try { - JSON.parse(this.testResults.stdout); - } catch { - throw new Error(`Expected valid JSON on stdout, but got: ${this.testResults.stdout}`); - } -}); diff --git a/features/test-implementations/README.md b/features/test-implementations/README.md deleted file mode 100644 index 5569fc2ca..000000000 --- a/features/test-implementations/README.md +++ /dev/null @@ -1,173 +0,0 @@ -# Cucumber Structure - -Welcome to the land of code definitions for the cucumber syntax we support for writing human-readable test cases for Apify CLI! - -Throughout this document, you will encounter several specific keywords: - -- world: an object that stores results and certain configurations and that gets passed around per test case - -This folder is (at the time of writing) split into several distinct parts: - -## World (`0.world.ts`) - -This file's sole purpose is to try to define the world object that gets passed around. This file can change as more test cases are added, and it is recommended to keep it up to date with the latest test cases. - -This file also includes methods that are shared across all layers of the test case (setup, execution, results). - -## Test Actor (`0.test-actor.js`) - -Contains the basic source code actors run in the tests. If this file requires more specific logic, newer versions can be created and referenced in the [Setup](#setup-1setupts) file. - -## Utilities (`0.utils.ts`) - -Contains random utilities that can be used across all stages of a test case. - -### Replacers - -Certain tests may want to reference values from the test actor (for instance checking that the actor name is mentioned in the output). To make this easier, we have a replacer system that can replace certain strings with values from the test actor. - -The syntax to use is `{{}}`, where `replacer` is the name of the replacer as listed below. You can add in whitespaces between the braces and the replacer name if you wish. - -The following replacers are implemented: - -- `testActorName`: the name of the test actor (same as `name` in `.actor/actor.json`) -- `buildId`: the ID of the build that was created, when the `I capture the build ID` step is ran - -## Setup (`1.setup.ts`) - -This file is intended to setup the world: gather any arguments, prepare an actor to test, setup the input, prepare the stdin input, etc. This is where you configure all `Given` steps. - -Currently, the following phrases are implemented: - -- ``given my `pwd` is a fully initialized actor project directory`` -- ``given the `actor.json` is valid`` -- ``given the `actor.json` is invalid`` -- `given the actor implementation doesn't throw itself` -- `given the following input provided via standard input` - - Example: - - ``` - Given the following input provided via standard input - """ - {"foo":"bar"} - """ - ``` - - - This step supports providing input to the CLI via stdin. This is useful for testing CLI commands that can optionally accept standard input. - -- `given the following input provided via file \`\`` - - Example: - - ``` - Given the following input provided via file `input.json` - """ - {"foo":"bar"} - """ - ``` - - - This step supports providing input to the CLI via a file. - -- `given a logged in apify console user` - - This step ensures the test is ran assuming a logged in user on the Apify console. -- `given the local actor is pushed to the apify platform` - - This step ensures the test is ran assuming the actor is pushed to the Apify platform. - -Certain phrases may require a specific order to be executed in. Errors will be thrown accordingly if the order is not followed. - -## Execution (`2.execution.ts`) - -This file is intended to execute everything that is passed in the `When` steps. This is where you run the actor, run the actor with certain flags, etc. You rely on the information stored in the world to decide what extra data to pass to the CLI. - -Currently, the following phrases are implemented: - -- `when I run:` followed by a code block consisting of a CLI command (optionally prefixed with `$`) - - Example: - - ``` - When I run: - `​`​` - $ apify actor run --input='{"foo":"bar"}' - `​`​` - ``` - - - This step supports running only CLI commands. It also expects only **one** command to be ran. Any more than one command will result in an error (this is done for simplicity sake) - - When referring to the CLI, you should mention the `apify` binary (as if you'd write this in a terminal). For testing sake, when we actually run the command, it will instead call the `tsx ./bin/dev.js` script, meaning changes that you do to the CLI will be reflected in the tests without needing a rebuild. - -- `when i capture the id` - - Example: - - ``` - When I capture the build ID - ``` - - - This step captures the ID of the specified type and stores it in the world. This is useful for checking the output of the CLI, as some variables may be needed to check the output of the actor run. - - Currently, the following types are implemented: - - `build`: captures the ID of the build that was created - -- `when i run with captured data` - - Identical to `when I run`, with the only difference being that you can use it in conjunction with `when i capture the id` to run the CLI with the captured data. - -## Results (`3.results.ts`) - -This file is intended to check the results of the execution. This is where you check the output of the CLI, the exit code, the output of the actor, etc. You rely on the information stored in the world to decide what to check. - -Currently, the following phrases are implemented: - -- `then the local run has an input JSON:` followed by a code block consisting of a JSON object - - Example: - - ``` - Then the local run has an input JSON: - `​`​` - {"foo":"bar"} - `​`​` - ``` - - - This step checks the input of the actor run. It expects the input to be a JSON object. If the input is not a JSON object, an error will be thrown. This will ensure the overridden input is correctly passed to the actor run. - -- `then the local actor run has started` - - This step checks if the actor run has actually started. -- `then the local actor run hasn't even started` - - This step checks if the actor run hasn't started. If the actor run has started, an error will be thrown. -- ``then the exit status code is ``​`` - - Example: - - ``` - Then the exit status code is `0` - ``` - - - This step checks the exit status code of the CLI. If the exit status code is not the same as the one provided, an error will be thrown. - -- ``then the exit status code is not ``​`` - - Example: - - ``` - Then the exit status code is not `0` - ``` - - - This step checks the exit status code of the CLI. If the exit status code is the same as the one provided, an error will be thrown. - -- `then I don't see any Node.js exception` - - This step checks if there are any Node.js exceptions in the output. If there are any, an error will be thrown. -- `then I can read text on stderr:` followed by a code block consisting of a string - - Example: - - ``` - Then I can read text on stderr: - `​`​` - use "--input-file=" flag instead - `​`​` - ``` - - - This step checks if the text provided is in the stderr output. If the text is not in the stderr output, an error will be thrown. - -- `then i can read text on stdout:` followed by a code block consisting of a string - - Example: - ``` - Then I can read text on stdout: - `​`​` - use "--input-file=" flag instead - `​`​` - ``` -- `then i can read valid json on stdout` - - This step checks if the stdout output is valid JSON. If the stdout output is not valid JSON, an error will be thrown. diff --git a/package.json b/package.json index 5c259df7a..37919dc49 100644 --- a/package.json +++ b/package.json @@ -8,12 +8,13 @@ "dev:apify": "tsx ./src/entrypoints/apify.ts", "dev:actor": "tsx ./src/entrypoints/actor.ts", "test:all": "yarn test:local && yarn test:api", - "test:local": "vitest run --testNamePattern '^((?!\\[api]).)*$' --exclude ./test/api", + "test:local": "vitest run --testNamePattern '^((?!\\[api]).)*$' --exclude ./test/api --exclude ./test/e2e", + "test:e2e": "vitest run ./test/e2e", + "test:e2e:local": "vitest run ./test/e2e --exclude '**/builds*'", "test:api": "vitest run --testNamePattern '\\[api\\]'", "test:python": "vitest run --testNamePattern '\\[python\\]'", - "test:cucumber": "cross-env NODE_OPTIONS=\"--import tsx\" cucumber-js", - "lint": "eslint src test scripts features --ext .ts,.cjs,.mjs", - "lint:fix": "eslint src test scripts features --fix --ext .ts,.cjs,.mjs", + "lint": "eslint src test scripts --ext .ts,.cjs,.mjs", + "lint:fix": "eslint src test scripts --fix --ext .ts,.cjs,.mjs", "format": "biome format . && prettier --check \"**/*.{md,yml,yaml}\"", "format:fix": "biome format --write . && prettier --write \"**/*.{md,yml,yaml}\"", "clean": "rimraf dist", @@ -119,7 +120,6 @@ "@apify/tsconfig": "^0.1.1", "@biomejs/biome": "^2.0.0", "@crawlee/types": "^3.11.1", - "@cucumber/cucumber": "^12.0.0", "@types/adm-zip": "^0.5.5", "@types/archiver": "^7.0.0", "@types/bun": "^1.2.5", diff --git a/test/api/commands/call.test.ts b/test/api/commands/call.test.ts index 8bfcaa315..20f6f4dbc 100644 --- a/test/api/commands/call.test.ts +++ b/test/api/commands/call.test.ts @@ -169,7 +169,7 @@ describe('[api] apify call', () => { expect(EXPECTED_INPUT_CONTENT_TYPE).toStrictEqual(input!.contentType); }); - // TODO: move this to cucumber, much easier to test + // TODO: move this to e2e tests, much easier to test // it.skip('should work with stdin input without --input or --input-file', async () => { // const expectedInput = { // hello: 'from cli', diff --git a/features/test-implementations/0.basic-actor.js b/test/e2e/__fixtures__/basic-actor.js similarity index 100% rename from features/test-implementations/0.basic-actor.js rename to test/e2e/__fixtures__/basic-actor.js diff --git a/test/e2e/__helpers__/run-cli.ts b/test/e2e/__helpers__/run-cli.ts new file mode 100644 index 000000000..8058149c9 --- /dev/null +++ b/test/e2e/__helpers__/run-cli.ts @@ -0,0 +1,92 @@ +import { fileURLToPath } from 'node:url'; + +import { execa } from 'execa'; +import isCI from 'is-ci'; + +const ProjectRoot = new URL('../../../', import.meta.url); +const DistApify = fileURLToPath(new URL('./dist/apify.js', ProjectRoot)); +const DistActor = fileURLToPath(new URL('./dist/actor.js', ProjectRoot)); + +const VALID_MODES = ['dist', 'npx', 'global'] as const; + +export interface RunCliOptions { + cwd?: string | URL; + stdin?: string; + env?: Record; + timeout?: number; +} + +export interface RunCliResult { + stdout: string; + stderr: string; + exitCode: number; +} + +/** + * Resolve the executable and arguments based on the current mode. + */ +function resolveExec(binary: 'apify' | 'actor', args: string[]): { file: string; args: string[] } { + const mode = process.env.APIFY_CLI_E2E_MODE?.toLowerCase() || (isCI ? 'npx' : 'dist'); + + switch (mode) { + case 'dist': { + const distFile = binary === 'actor' ? DistActor : DistApify; + return { file: 'node', args: [distFile, ...args] }; + } + case 'npx': { + if (binary === 'actor') { + return { file: 'npx', args: ['-p', 'apify-cli@beta', 'actor', ...args] }; + } + return { file: 'npx', args: ['apify-cli@beta', ...args] }; + } + case 'global': { + return { file: binary, args }; + } + default: { + throw new Error( + `Invalid APIFY_CLI_E2E_MODE value: "${process.env.APIFY_CLI_E2E_MODE}". Expected one of: ${VALID_MODES.join(', ')}`, + ); + } + } +} + +/** + * Run a CLI command and return { stdout, stderr, exitCode }. + * Does not throw on non-zero exit codes. May throw on timeout or if the binary is not found. + * + * Execution mode controlled by APIFY_CLI_E2E_MODE env var (auto-detects CI when unset): + * "dist" → node ./dist/apify.js (default locally, after `yarn build`) + * "npx" → npx apify-cli@beta (default in CI, tests the published package) + * "global" → apify (tests globally installed binary) + * + * @example + * const result = await runCli('apify', ['help']); + * expect(result.exitCode).toBe(0); + * expect(result.stdout).toContain('apify-cli'); + */ +export async function runCli( + binary: 'apify' | 'actor', + args: string[], + options: RunCliOptions = {}, +): Promise { + const exec = resolveExec(binary, args); + + const result = await execa(exec.file, exec.args, { + cwd: options.cwd, + reject: false, + timeout: options.timeout ?? 120_000, + stdin: options.stdin ? 'pipe' : 'ignore', + input: options.stdin, + env: { + APIFY_CLI_DISABLE_TELEMETRY: '1', + APIFY_CLI_SKIP_UPDATE_CHECK: '1', + ...options.env, + }, + }); + + return { + stdout: result.stdout ?? '', + stderr: result.stderr ?? '', + exitCode: result.exitCode ?? 1, + }; +} diff --git a/test/e2e/__helpers__/test-actor.ts b/test/e2e/__helpers__/test-actor.ts new file mode 100644 index 000000000..16ccb832d --- /dev/null +++ b/test/e2e/__helpers__/test-actor.ts @@ -0,0 +1,89 @@ +import { randomBytes } from 'node:crypto'; +import { mkdir, readFile, rm, writeFile } from 'node:fs/promises'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +import { runCli } from './run-cli.js'; + +const TestTmpRoot = fileURLToPath(new URL('../../tmp/', import.meta.url)); +const BasicActorPath = fileURLToPath(new URL('../__fixtures__/basic-actor.js', import.meta.url)); +const basicActorSource = await readFile(BasicActorPath, 'utf-8'); + +const KV_STORE_PATH = 'storage/key_value_stores/default'; + +export interface TestActor { + /** Absolute path to the actor project directory */ + dir: string; + /** Actor name (e.g. "e2e-a1b2c3d4") */ + name: string; +} + +/** + * Create a temporary actor project using `apify create`. + * Overwrites main.js with the basic test actor that writes STARTED.json + RECEIVED_INPUT.json. + */ +export async function createTestActor(prefix = 'e2e'): Promise { + const name = `${prefix}-${randomBytes(6).toString('hex')}`; + + await mkdir(TestTmpRoot, { recursive: true }); + + const result = await runCli('apify', ['create', name, '--template', 'project_empty'], { + cwd: TestTmpRoot, + }); + + if (result.exitCode !== 0) { + throw new Error(`Failed to create test actor "${name}":\n${result.stderr || result.stdout}`); + } + + const dir = path.join(TestTmpRoot, name); + + await writeFile(path.join(dir, 'src', 'main.js'), basicActorSource); + + return { dir, name }; +} + +/** + * Read what the test actor stored during its run. + */ +export async function getRunResults(actorDir: string): Promise<{ + started: boolean; + input: unknown | null; +}> { + const storePath = path.join(actorDir, KV_STORE_PATH); + + const [started, input] = await Promise.all([ + readFile(path.join(storePath, 'STARTED.json'), 'utf-8').catch(() => null), + readFile(path.join(storePath, 'RECEIVED_INPUT.json'), 'utf-8').catch(() => null), + ]); + + return { + started: started !== null && JSON.parse(started) === 'works', + input: input ? JSON.parse(input) : null, + }; +} + +/** + * Clean up run artifacts so the actor can be run again cleanly. + */ +export async function cleanRunResults(actorDir: string): Promise { + const storePath = path.join(actorDir, KV_STORE_PATH); + + await Promise.all([ + rm(path.join(storePath, 'STARTED.json'), { force: true }), + rm(path.join(storePath, 'RECEIVED_INPUT.json'), { force: true }), + ]); +} + +/** + * Delete the actor project directory. + */ +export async function removeTestActor(actor: TestActor): Promise { + await rm(actor.dir, { recursive: true, force: true }); +} + +/** + * Corrupt the actor.json to test error handling. + */ +export async function corruptActorJson(actorDir: string): Promise { + await writeFile(path.join(actorDir, '.actor', 'actor.json'), '{ wow "name": "my-invalid-actor" }'); +} diff --git a/test/e2e/actor-run-input.test.ts b/test/e2e/actor-run-input.test.ts new file mode 100644 index 000000000..e41418fa9 --- /dev/null +++ b/test/e2e/actor-run-input.test.ts @@ -0,0 +1,113 @@ +import { writeFile } from 'node:fs/promises'; +import path from 'node:path'; + +import { runCli } from './__helpers__/run-cli.js'; +import { + cleanRunResults, + createTestActor, + getRunResults, + removeTestActor, + type TestActor, +} from './__helpers__/test-actor.js'; + +describe('[e2e] actor run input', () => { + let actor: TestActor; + + beforeAll(async () => { + actor = await createTestActor(); + + // Verify the actor runs cleanly before testing input + const check = await runCli('apify', ['run'], { cwd: actor.dir }); + if (check.exitCode !== 0) { + throw new Error(`Test actor failed to run:\n${check.stderr}`); + } + + await cleanRunResults(actor.dir); + }); + + afterAll(async () => { + if (actor) await removeTestActor(actor); + }); + + afterEach(async () => { + await cleanRunResults(actor.dir); + }); + + describe('--input flag (inline JSON)', () => { + it('passes JSON string to the actor', async () => { + const result = await runCli('apify', ['run', '--input={"foo":"bar"}'], { cwd: actor.dir }); + + expect(result.exitCode, `stderr: ${result.stderr}`).toBe(0); + const run = await getRunResults(actor.dir); + expect(run.started).toBe(true); + expect(run.input).toEqual({ foo: 'bar' }); + }); + + it('suggests --input-file when given a file path', async () => { + const result = await runCli('apify', ['run', '--input=./my-path/file.json'], { cwd: actor.dir }); + + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain('Use the "--input-file=" flag instead'); + const run = await getRunResults(actor.dir); + expect(run.started).toBe(false); + }); + + it('accepts -i as short alias', async () => { + await runCli('apify', ['run', '-i={"foo":"bar"}'], { cwd: actor.dir }); + + const run = await getRunResults(actor.dir); + expect(run.started).toBe(true); + expect(run.input).toEqual({ foo: 'bar' }); + }); + }); + + describe('--input-file flag (file input)', () => { + it('reads JSON from an existing file', async () => { + await writeFile(path.join(actor.dir, 'my-file.json'), '{"foo":"bar"}'); + + const result = await runCli('apify', ['run', '--input-file=my-file.json'], { cwd: actor.dir }); + + expect(result.exitCode, `stderr: ${result.stderr}`).toBe(0); + const run = await getRunResults(actor.dir); + expect(run.started).toBe(true); + expect(run.input).toEqual({ foo: 'bar' }); + }); + + it('errors when the input file does not exist', async () => { + const result = await runCli('apify', ['run', '--input-file=the-file-that-doesnt-exist.json'], { + cwd: actor.dir, + }); + + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain('Cannot read input file at'); + const run = await getRunResults(actor.dir); + expect(run.started).toBe(false); + }); + }); + + describe('stdin input', () => { + it('accepts stdin via --input-file=-', async () => { + const result = await runCli('apify', ['run', '--input-file=-'], { + cwd: actor.dir, + stdin: '{"foo":"bar"}', + }); + + expect(result.exitCode, `stderr: ${result.stderr}`).toBe(0); + const run = await getRunResults(actor.dir); + expect(run.started).toBe(true); + expect(run.input).toEqual({ foo: 'bar' }); + }); + + it('accepts stdin implicitly (no flag)', async () => { + const result = await runCli('apify', ['run'], { + cwd: actor.dir, + stdin: '{"foo":"bar"}', + }); + + expect(result.exitCode, `stderr: ${result.stderr}`).toBe(0); + const run = await getRunResults(actor.dir); + expect(run.started).toBe(true); + expect(run.input).toEqual({ foo: 'bar' }); + }); + }); +}); diff --git a/test/e2e/builds.test.ts b/test/e2e/builds.test.ts new file mode 100644 index 000000000..08540f090 --- /dev/null +++ b/test/e2e/builds.test.ts @@ -0,0 +1,156 @@ +import { randomBytes } from 'node:crypto'; + +import { ApifyClient } from 'apify-client'; + +import { getApifyClientOptions } from '../../src/lib/utils.js'; +import { runCli } from './__helpers__/run-cli.js'; +import { createTestActor, removeTestActor, type TestActor } from './__helpers__/test-actor.js'; + +describe('[e2e] builds namespace', () => { + let actor: TestActor; + let authEnv: Record; + let client: ApifyClient; + + beforeAll(async () => { + const token = process.env.TEST_USER_TOKEN; + if (!token) throw new Error('TEST_USER_TOKEN env var is required for builds tests'); + + // Unique auth path so parallel runs don't collide + const authPath = `e2e-builds-${randomBytes(6).toString('hex')}`; + authEnv = { __APIFY_INTERNAL_TEST_AUTH_PATH__: authPath }; + + const loginResult = await runCli('apify', ['login', '--token', token], { env: authEnv }); + if (loginResult.exitCode !== 0) { + throw new Error(`Failed to login:\n${loginResult.stderr}`); + } + + client = new ApifyClient(getApifyClientOptions(token)); + + // Create and push actor + actor = await createTestActor('e2e-builds'); + + const pushResult = await runCli('apify', ['push'], { + cwd: actor.dir, + env: authEnv, + }); + + if (pushResult.exitCode !== 0) { + throw new Error(`Failed to push actor:\n${pushResult.stderr}`); + } + }); + + afterAll(async () => { + if (actor?.name && client) { + try { + const me = await client.user('me').get(); + await client.actor(`${me.username}/${actor.name}`).delete(); + } catch { + // Best-effort cleanup + } + } + + if (actor) await removeTestActor(actor); + }); + + describe('builds create', () => { + it('fails with invalid actor ID', async () => { + const result = await runCli('apify', ['builds', 'create', 'invalid-id'], { + cwd: actor.dir, + env: authEnv, + }); + + expect(result.stdout).toContain('Actor with name or ID "invalid-id" was not found'); + }); + + it('fails from an unpublished actor directory', async () => { + const unpushed = await createTestActor('e2e-unpushed'); + + const result = await runCli('apify', ['builds', 'create'], { + cwd: unpushed.dir, + env: authEnv, + }); + + expect(result.stdout).toContain(`Actor with name "${unpushed.name}" was not found`); + + await removeTestActor(unpushed); + }); + + it('succeeds from a published actor directory', async () => { + const result = await runCli('apify', ['builds', 'create'], { + cwd: actor.dir, + env: authEnv, + }); + + expect(result.stdout).toContain('Build Started'); + expect(result.stdout).toContain(actor.name); + }); + + it('prints valid JSON with --json flag', async () => { + const result = await runCli('apify', ['builds', 'create', '--json'], { + cwd: actor.dir, + env: authEnv, + }); + + expect(() => JSON.parse(result.stdout)).not.toThrow(); + }); + }); + + describe('builds info', () => { + let buildId: string; + + beforeAll(async () => { + const create = await runCli('apify', ['builds', 'create'], { + cwd: actor.dir, + env: authEnv, + }); + + const id = + create.stdout.match(/Build Started \(ID: (\w+)\)/)?.[1] ?? + create.stderr.match(/Build Started \(ID: (\w+)\)/)?.[1]; + + if (!id) { + throw new Error(`Failed to capture build ID.\nstdout: ${create.stdout}\nstderr: ${create.stderr}`); + } + + buildId = id; + }); + + it('fails with invalid build ID', async () => { + const result = await runCli('apify', ['builds', 'info', 'invalid-id'], { + cwd: actor.dir, + env: authEnv, + }); + + expect(result.stdout).toContain('Build with ID "invalid-id" was not found'); + }); + + it('shows info for a valid build', async () => { + const result = await runCli('apify', ['builds', 'info', buildId], { + cwd: actor.dir, + env: authEnv, + }); + + expect(result.stdout).toContain(actor.name); + }); + + it('prints valid JSON with --json flag', async () => { + const result = await runCli('apify', ['builds', 'info', buildId, '--json'], { + cwd: actor.dir, + env: authEnv, + }); + + expect(() => JSON.parse(result.stdout)).not.toThrow(); + }); + }); + + describe('builds ls', () => { + it('prints valid JSON with --json flag', async () => { + const result = await runCli('apify', ['builds', 'ls', '--json'], { + cwd: actor.dir, + env: authEnv, + }); + + expect(() => JSON.parse(result.stdout)).not.toThrow(); + }); + }); +}); diff --git a/test/e2e/help.test.ts b/test/e2e/help.test.ts new file mode 100644 index 000000000..6a517af25 --- /dev/null +++ b/test/e2e/help.test.ts @@ -0,0 +1,39 @@ +import { runCli } from './__helpers__/run-cli.js'; + +describe.concurrent('[e2e] help command', () => { + it('apify help prints the full help message', async () => { + const result = await runCli('apify', ['help']); + expect(result.exitCode, `stderr: ${result.stderr}`).toBe(0); + expect(result.stdout).toContain('https://apify.com/contact'); + }); + + it('actor help prints the full help message', async () => { + const result = await runCli('actor', ['help']); + expect(result.exitCode, `stderr: ${result.stderr}`).toBe(0); + expect(result.stdout).toContain('https://apify.com/contact'); + }); + + it('apify help help prints the full help message', async () => { + const result = await runCli('apify', ['help', 'help']); + expect(result.exitCode, `stderr: ${result.stderr}`).toBe(0); + expect(result.stdout).toContain('https://apify.com/contact'); + }); + + it('apify help -h prints help for the help command', async () => { + const result = await runCli('apify', ['help', '-h']); + expect(result.exitCode, `stderr: ${result.stderr}`).toBe(0); + expect(result.stdout).toContain('Prints out help about a command, or all available commands.'); + }); + + it('apify help run prints run command description', async () => { + const result = await runCli('apify', ['help', 'run']); + expect(result.exitCode, `stderr: ${result.stderr}`).toBe(0); + expect(result.stdout).toContain('Executes Actor locally with simulated Apify environment variables.'); + }); + + it('apify run --help prints the same as apify help run', async () => { + const result = await runCli('apify', ['run', '--help']); + expect(result.exitCode, `stderr: ${result.stderr}`).toBe(0); + expect(result.stdout).toContain('Executes Actor locally with simulated Apify environment variables.'); + }); +}); diff --git a/test/e2e/invalid-config.test.ts b/test/e2e/invalid-config.test.ts new file mode 100644 index 000000000..ff01fb619 --- /dev/null +++ b/test/e2e/invalid-config.test.ts @@ -0,0 +1,23 @@ +import { runCli } from './__helpers__/run-cli.js'; +import { corruptActorJson, createTestActor, removeTestActor, type TestActor } from './__helpers__/test-actor.js'; + +describe('[e2e] invalid actor.json', () => { + let actor: TestActor; + + beforeAll(async () => { + actor = await createTestActor(); + await corruptActorJson(actor.dir); + }); + + afterAll(async () => { + if (actor) await removeTestActor(actor); + }); + + it('prints the path to invalid actor.json and exits with code 5', async () => { + const result = await runCli('apify', ['run'], { cwd: actor.dir }); + + expect(result.exitCode, `stdout: ${result.stdout}\nstderr: ${result.stderr}`).toBe(5); + expect(result.stderr).toContain('Failed to read local config at path:'); + expect(result.stderr).toContain("Expected property name or '}'"); + }); +}); diff --git a/yarn.lock b/yarn.lock index 8c5652016..68a8f22fb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -169,24 +169,6 @@ __metadata: languageName: node linkType: hard -"@babel/code-frame@npm:^7.26.2": - version: 7.29.0 - resolution: "@babel/code-frame@npm:7.29.0" - dependencies: - "@babel/helper-validator-identifier": "npm:^7.28.5" - js-tokens: "npm:^4.0.0" - picocolors: "npm:^1.1.1" - checksum: 10c0/d34cc504e7765dfb576a663d97067afb614525806b5cad1a5cc1a7183b916fec8ff57fa233585e3926fd5a9e6b31aae6df91aa81ae9775fb7a28f658d3346f0d - languageName: node - linkType: hard - -"@babel/helper-validator-identifier@npm:^7.28.5": - version: 7.28.5 - resolution: "@babel/helper-validator-identifier@npm:7.28.5" - checksum: 10c0/42aaebed91f739a41f3d80b72752d1f95fd7c72394e8e4bd7cdd88817e0774d80a432451bcba17c2c642c257c483bf1d409dd4548883429ea9493a3bc4ab0847 - languageName: node - linkType: hard - "@babel/runtime@npm:^7.26.10": version: 7.29.2 resolution: "@babel/runtime@npm:7.29.2" @@ -376,198 +358,6 @@ __metadata: languageName: node linkType: hard -"@cucumber/ci-environment@npm:13.0.0": - version: 13.0.0 - resolution: "@cucumber/ci-environment@npm:13.0.0" - checksum: 10c0/6fba34f31f80451a1cc4772a154fd519e74ad3534dac25fbc21e7a3a47b45a7de2b5585ff8f43867320278e56140058b5190a66cd5fba83a4616733cc38ceaab - languageName: node - linkType: hard - -"@cucumber/cucumber-expressions@npm:19.0.0": - version: 19.0.0 - resolution: "@cucumber/cucumber-expressions@npm:19.0.0" - dependencies: - regexp-match-indices: "npm:1.0.2" - checksum: 10c0/afb4647c3f397433d778ff4e7ddf8e31439578cd0cc5375fcd43f94d7b39536763af6ec2952ccfc6fc53c873590a2f780e51ebe0b7b267bf94d35e5ff82d7dcd - languageName: node - linkType: hard - -"@cucumber/cucumber@npm:^12.0.0": - version: 12.7.0 - resolution: "@cucumber/cucumber@npm:12.7.0" - dependencies: - "@cucumber/ci-environment": "npm:13.0.0" - "@cucumber/cucumber-expressions": "npm:19.0.0" - "@cucumber/gherkin": "npm:38.0.0" - "@cucumber/gherkin-streams": "npm:6.0.0" - "@cucumber/gherkin-utils": "npm:11.0.0" - "@cucumber/html-formatter": "npm:23.0.0" - "@cucumber/junit-xml-formatter": "npm:0.9.0" - "@cucumber/message-streams": "npm:4.0.1" - "@cucumber/messages": "npm:32.0.1" - "@cucumber/pretty-formatter": "npm:1.0.1" - "@cucumber/tag-expressions": "npm:9.1.0" - assertion-error-formatter: "npm:^3.0.0" - capital-case: "npm:^1.0.4" - chalk: "npm:^4.1.2" - cli-table3: "npm:0.6.5" - commander: "npm:^14.0.0" - debug: "npm:^4.3.4" - error-stack-parser: "npm:^2.1.4" - figures: "npm:^3.2.0" - glob: "npm:^13.0.0" - has-ansi: "npm:^4.0.1" - indent-string: "npm:^4.0.0" - is-installed-globally: "npm:^0.4.0" - is-stream: "npm:^2.0.0" - knuth-shuffle-seeded: "npm:^1.0.6" - lodash.merge: "npm:^4.6.2" - lodash.mergewith: "npm:^4.6.2" - luxon: "npm:3.7.2" - mime: "npm:^3.0.0" - mkdirp: "npm:^3.0.0" - mz: "npm:^2.7.0" - progress: "npm:^2.0.3" - read-package-up: "npm:^12.0.0" - semver: "npm:7.7.4" - string-argv: "npm:0.3.1" - supports-color: "npm:^8.1.1" - type-fest: "npm:^4.41.0" - util-arity: "npm:^1.1.0" - yaml: "npm:^2.2.2" - yup: "npm:1.7.1" - bin: - cucumber-js: bin/cucumber.js - checksum: 10c0/e9de27b6260af4a2104561e7dbf0980428cedf40b05b4fb9d77b65698e2826fd7d941c56f04fb8053bb11f28c6ad03fd984cf183bfd73f7f13c33fe58cd96d5a - languageName: node - linkType: hard - -"@cucumber/gherkin-streams@npm:6.0.0": - version: 6.0.0 - resolution: "@cucumber/gherkin-streams@npm:6.0.0" - dependencies: - commander: "npm:14.0.0" - source-map-support: "npm:0.5.21" - peerDependencies: - "@cucumber/gherkin": ">=22.0.0" - "@cucumber/message-streams": ">=4.0.0" - "@cucumber/messages": ">=17.1.1" - bin: - gherkin-javascript: bin/gherkin - checksum: 10c0/2ad5bfa78a9709402a0282464b7412d2c6da61e8053fb47a80a3c4d3cb7659105d8469df0fb6d47369bdf5eeb2689d3fb6e3691f9fc37f405cc18c13b202a2cc - languageName: node - linkType: hard - -"@cucumber/gherkin-utils@npm:11.0.0": - version: 11.0.0 - resolution: "@cucumber/gherkin-utils@npm:11.0.0" - dependencies: - "@cucumber/gherkin": "npm:^38.0.0" - "@cucumber/messages": "npm:^32.0.0" - "@teppeis/multimaps": "npm:3.0.0" - commander: "npm:14.0.2" - source-map-support: "npm:^0.5.21" - bin: - gherkin-utils: bin/gherkin-utils - checksum: 10c0/8abf570afcf4934b4f9287c71d508935590e7d0d91997474c7c4c9a92b8d5995e9cb6ad49ce94970cb5b106ab0ccb2b722114cc693148503389e814d4640d8b9 - languageName: node - linkType: hard - -"@cucumber/gherkin@npm:38.0.0, @cucumber/gherkin@npm:^38.0.0": - version: 38.0.0 - resolution: "@cucumber/gherkin@npm:38.0.0" - dependencies: - "@cucumber/messages": "npm:>=31.0.0 <33" - checksum: 10c0/f3dd06b7d603c06bc7be09546d1f2a7b47dc8d433528237c0a2be9c2dbb4d95da9492716a0290848cd116801c98720b044b7a46bed5b8bdd467e027c6cac0ad3 - languageName: node - linkType: hard - -"@cucumber/html-formatter@npm:23.0.0": - version: 23.0.0 - resolution: "@cucumber/html-formatter@npm:23.0.0" - peerDependencies: - "@cucumber/messages": ">=18" - checksum: 10c0/695b9ce602f705fbc09efee81503b5da9e07521a071601f2a984b622097ae143a0ece2042192faae57419f24b71c2b5fb9a87d75b0436146ef694d7ebb24d252 - languageName: node - linkType: hard - -"@cucumber/junit-xml-formatter@npm:0.9.0": - version: 0.9.0 - resolution: "@cucumber/junit-xml-formatter@npm:0.9.0" - dependencies: - "@cucumber/query": "npm:^14.0.1" - "@teppeis/multimaps": "npm:^3.0.0" - luxon: "npm:^3.5.0" - xmlbuilder: "npm:^15.1.1" - peerDependencies: - "@cucumber/messages": "*" - checksum: 10c0/c99deb87d200749e5cae82a1331ddd15e254a9da83b4b2a5d41624cfd8fc76023fafc821f009c3d9b105423f39e98758250ae4aef287bdd0254734d5ec638837 - languageName: node - linkType: hard - -"@cucumber/message-streams@npm:4.0.1": - version: 4.0.1 - resolution: "@cucumber/message-streams@npm:4.0.1" - peerDependencies: - "@cucumber/messages": ">=17.1.1" - checksum: 10c0/521b68b4a8c00fbdc950f80c057537d1fed9ca532c3d2d3b03b72051a410ee1530e2a5a4f0207086068a0250d9056e23b6c6cd0322b6fd2ec1a520868b696efa - languageName: node - linkType: hard - -"@cucumber/messages@npm:32.0.1": - version: 32.0.1 - resolution: "@cucumber/messages@npm:32.0.1" - dependencies: - class-transformer: "npm:0.5.1" - reflect-metadata: "npm:0.2.2" - checksum: 10c0/ae9379318cf04c011fea7ab4ba1c5e9c08101beec27dde2a2bd1bb4c4dff9cda229dca3fd638900bc7bf707fa4a452ca01198532fe2a7f36d0973ea1bc5c9587 - languageName: node - linkType: hard - -"@cucumber/messages@npm:>=31.0.0 <33, @cucumber/messages@npm:^32.0.0": - version: 32.2.0 - resolution: "@cucumber/messages@npm:32.2.0" - dependencies: - class-transformer: "npm:0.5.1" - reflect-metadata: "npm:0.2.2" - checksum: 10c0/4d1b126aa814b1b31baa59235aa560b826ad914b8c775a5d6da4ef7e64ed3a87e24d5992e33fffc6e3f55ccd142fca79e88d11a25b36e4467a8154792e4967d1 - languageName: node - linkType: hard - -"@cucumber/pretty-formatter@npm:1.0.1": - version: 1.0.1 - resolution: "@cucumber/pretty-formatter@npm:1.0.1" - dependencies: - ansi-styles: "npm:^5.0.0" - cli-table3: "npm:^0.6.0" - figures: "npm:^3.2.0" - ts-dedent: "npm:^2.0.0" - peerDependencies: - "@cucumber/cucumber": ">=7.0.0" - "@cucumber/messages": "*" - checksum: 10c0/16ac0174bdad47718296c241d20e3ac6b305809675da7c64df8f15542a66be7603dad73a37bbb850241d51da8bbba61e461b861c7e41c0dbbd64684e93a8c265 - languageName: node - linkType: hard - -"@cucumber/query@npm:^14.0.1": - version: 14.7.0 - resolution: "@cucumber/query@npm:14.7.0" - dependencies: - "@teppeis/multimaps": "npm:3.0.0" - lodash.sortby: "npm:^4.7.0" - peerDependencies: - "@cucumber/messages": "*" - checksum: 10c0/b3884d7f29d3a89f4cf2fa8456dd57fe64ab7412fef6b9e8e0f6115c17349ff2610427daa81e693cb82a8fa453c4fdcb4245054af8d433da4c1959599a3bd8a0 - languageName: node - linkType: hard - -"@cucumber/tag-expressions@npm:9.1.0": - version: 9.1.0 - resolution: "@cucumber/tag-expressions@npm:9.1.0" - checksum: 10c0/53d23039d097dd7c8e0c22eb0565a82326515f9df52da7ec4a9f5729435de93b2dc22fbe0abecdf925cd75dc1aa2d683b3eeae75c461b0656740a8b41f6e2fd1 - languageName: node - linkType: hard - "@emnapi/core@npm:^1.7.1": version: 1.9.1 resolution: "@emnapi/core@npm:1.9.1" @@ -1570,13 +1360,6 @@ __metadata: languageName: node linkType: hard -"@teppeis/multimaps@npm:3.0.0, @teppeis/multimaps@npm:^3.0.0": - version: 3.0.0 - resolution: "@teppeis/multimaps@npm:3.0.0" - checksum: 10c0/7012b1faec5a6f76288db71ea71754e73714931e72da5717ca0285e6eae4e2c98df3044039f2d32008bca705f4c4fc866e5455eab7002eae68ed664aedc17604 - languageName: node - linkType: hard - "@tokenizer/inflate@npm:^0.2.6": version: 0.2.7 resolution: "@tokenizer/inflate@npm:0.2.7" @@ -1826,13 +1609,6 @@ __metadata: languageName: node linkType: hard -"@types/normalize-package-data@npm:^2.4.4": - version: 2.4.4 - resolution: "@types/normalize-package-data@npm:2.4.4" - checksum: 10c0/aef7bb9b015883d6f4119c423dd28c4bdc17b0e8a0ccf112c78b4fe0e91fbc4af7c6204b04bba0e199a57d2f3fbbd5b4a14bf8739bf9d2a39b2a0aad545e0f86 - languageName: node - linkType: hard - "@types/qs@npm:*": version: 6.15.0 resolution: "@types/qs@npm:6.15.0" @@ -2338,13 +2114,6 @@ __metadata: languageName: node linkType: hard -"ansi-regex@npm:^4.1.0": - version: 4.1.1 - resolution: "ansi-regex@npm:4.1.1" - checksum: 10c0/d36d34234d077e8770169d980fed7b2f3724bfa2a01da150ccd75ef9707c80e883d27cdf7a0eac2f145ac1d10a785a8a855cffd05b85f778629a0db62e7033da - languageName: node - linkType: hard - "ansi-regex@npm:^5.0.1": version: 5.0.1 resolution: "ansi-regex@npm:5.0.1" @@ -2368,13 +2137,6 @@ __metadata: languageName: node linkType: hard -"ansi-styles@npm:^5.0.0": - version: 5.2.0 - resolution: "ansi-styles@npm:5.2.0" - checksum: 10c0/9c4ca80eb3c2fb7b33841c210d2f20807f40865d27008d7c3f707b7f95cab7d67462a565e2388ac3285b71cb3d9bb2173de8da37c57692a362885ec34d6e27df - languageName: node - linkType: hard - "ansi-styles@npm:^6.1.0, ansi-styles@npm:^6.2.1, ansi-styles@npm:^6.2.3": version: 6.2.3 resolution: "ansi-styles@npm:6.2.3" @@ -2403,7 +2165,6 @@ __metadata: "@biomejs/biome": "npm:^2.0.0" "@crawlee/memory-storage": "npm:^3.12.0" "@crawlee/types": "npm:^3.11.1" - "@cucumber/cucumber": "npm:^12.0.0" "@inquirer/core": "npm:^11.0.0" "@inquirer/input": "npm:^5.0.0" "@inquirer/password": "npm:^5.0.0" @@ -2648,17 +2409,6 @@ __metadata: languageName: node linkType: hard -"assertion-error-formatter@npm:^3.0.0": - version: 3.0.0 - resolution: "assertion-error-formatter@npm:3.0.0" - dependencies: - diff: "npm:^4.0.1" - pad-right: "npm:^0.2.2" - repeat-string: "npm:^1.6.1" - checksum: 10c0/ea2741cfe9272b1efdcc9589c572a6880a32b9efcedc7171dbb0790d59aead8e1553b1edd4c78b65267a301efd2bc5aa75ef0f3cdc2394afb32ede4ea4cc6b64 - languageName: node - linkType: hard - "assertion-error@npm:^2.0.1": version: 2.0.1 resolution: "assertion-error@npm:2.0.1" @@ -2946,13 +2696,6 @@ __metadata: languageName: node linkType: hard -"buffer-from@npm:^1.0.0": - version: 1.1.2 - resolution: "buffer-from@npm:1.1.2" - checksum: 10c0/124fff9d66d691a86d3b062eff4663fe437a9d9ee4b47b1b9e97f5a5d14f6d5399345db80f796827be7c95e70a8e765dd404b7c3ff3b3324f98e9b0c8826cc34 - languageName: node - linkType: hard - "buffer@npm:^6.0.3": version: 6.0.3 resolution: "buffer@npm:6.0.3" @@ -3135,17 +2878,6 @@ __metadata: languageName: node linkType: hard -"capital-case@npm:^1.0.4": - version: 1.0.4 - resolution: "capital-case@npm:1.0.4" - dependencies: - no-case: "npm:^3.0.4" - tslib: "npm:^2.0.3" - upper-case-first: "npm:^2.0.2" - checksum: 10c0/6a034af73401f6e55d91ea35c190bbf8bda21714d4ea8bb8f1799311d123410a80f0875db4e3236dc3f97d74231ff4bf1c8783f2be13d7733c7d990c57387281 - languageName: node - linkType: hard - "chai@npm:^6.2.2": version: 6.2.2 resolution: "chai@npm:6.2.2" @@ -3222,13 +2954,6 @@ __metadata: languageName: node linkType: hard -"class-transformer@npm:0.5.1": - version: 0.5.1 - resolution: "class-transformer@npm:0.5.1" - checksum: 10c0/19809914e51c6db42c036166839906420bb60367df14e15f49c45c8c1231bf25ae661ebe94736ee29cc688b77101ef851a8acca299375cc52fc141b64acde18a - languageName: node - linkType: hard - "cli-cursor@npm:^5.0.0": version: 5.0.0 resolution: "cli-cursor@npm:5.0.0" @@ -3238,7 +2963,7 @@ __metadata: languageName: node linkType: hard -"cli-table3@npm:0.6.5, cli-table3@npm:^0.6.0, cli-table3@npm:^0.6.5": +"cli-table3@npm:^0.6.5": version: 0.6.5 resolution: "cli-table3@npm:0.6.5" dependencies: @@ -3327,21 +3052,7 @@ __metadata: languageName: node linkType: hard -"commander@npm:14.0.0": - version: 14.0.0 - resolution: "commander@npm:14.0.0" - checksum: 10c0/73c4babfa558077868d84522b11ef56834165d472b9e86a634cd4c3ae7fc72d59af6377d8878e06bd570fe8f3161eced3cbe383c38f7093272bb65bd242b595b - languageName: node - linkType: hard - -"commander@npm:14.0.2": - version: 14.0.2 - resolution: "commander@npm:14.0.2" - checksum: 10c0/245abd1349dbad5414cb6517b7b5c584895c02c4f7836ff5395f301192b8566f9796c82d7bd6c92d07eba8775fe4df86602fca5d86d8d10bcc2aded1e21c2aeb - languageName: node - linkType: hard - -"commander@npm:^14.0.0, commander@npm:^14.0.3": +"commander@npm:^14.0.3": version: 14.0.3 resolution: "commander@npm:14.0.3" checksum: 10c0/755652564bbf56ff2ff083313912b326450d3f8d8c85f4b71416539c9a05c3c67dbd206821ca72635bf6b160e2afdefcb458e86b317827d5cb333b69ce7f1a24 @@ -3733,13 +3444,6 @@ __metadata: languageName: node linkType: hard -"diff@npm:^4.0.1": - version: 4.0.4 - resolution: "diff@npm:4.0.4" - checksum: 10c0/855fb70b093d1d9643ddc12ea76dca90dc9d9cdd7f82c08ee8b9325c0dc5748faf3c82e2047ced5dcaa8b26e58f7903900be2628d0380a222c02d79d8de385df - languageName: node - linkType: hard - "diff@npm:^5.1.0": version: 5.2.2 resolution: "diff@npm:5.2.2" @@ -3941,15 +3645,6 @@ __metadata: languageName: node linkType: hard -"error-stack-parser@npm:^2.1.4": - version: 2.1.4 - resolution: "error-stack-parser@npm:2.1.4" - dependencies: - stackframe: "npm:^1.3.4" - checksum: 10c0/7679b780043c98b01fc546725484e0cfd3071bf5c906bbe358722972f04abf4fc3f0a77988017665bab367f6ef3fc2d0185f7528f45966b83e7c99c02d5509b9 - languageName: node - linkType: hard - "es-abstract@npm:^1.23.2, es-abstract@npm:^1.23.5, es-abstract@npm:^1.23.9, es-abstract@npm:^1.24.0": version: 1.24.1 resolution: "es-abstract@npm:1.24.1" @@ -4196,13 +3891,6 @@ __metadata: languageName: node linkType: hard -"escape-string-regexp@npm:^1.0.5": - version: 1.0.5 - resolution: "escape-string-regexp@npm:1.0.5" - checksum: 10c0/a968ad453dd0c2724e14a4f20e177aaf32bb384ab41b674a8454afe9a41c5e6fe8903323e0a1052f56289d04bd600f81278edf140b0fcc02f5cac98d0f5b5371 - languageName: node - linkType: hard - "escape-string-regexp@npm:^4.0.0": version: 4.0.0 resolution: "escape-string-regexp@npm:4.0.0" @@ -4687,15 +4375,6 @@ __metadata: languageName: node linkType: hard -"figures@npm:^3.2.0": - version: 3.2.0 - resolution: "figures@npm:3.2.0" - dependencies: - escape-string-regexp: "npm:^1.0.5" - checksum: 10c0/9c421646ede432829a50bc4e55c7a4eb4bcb7cc07b5bab2f471ef1ab9a344595bbebb6c5c21470093fbb730cd81bbca119624c40473a125293f656f49cb47629 - languageName: node - linkType: hard - "figures@npm:^6.1.0": version: 6.1.0 resolution: "figures@npm:6.1.0" @@ -4749,13 +4428,6 @@ __metadata: languageName: node linkType: hard -"find-up-simple@npm:^1.0.1": - version: 1.0.1 - resolution: "find-up-simple@npm:1.0.1" - checksum: 10c0/ad34de157b7db925d50ff78302fefb28e309f3bc947c93ffca0f9b0bccf9cf1a2dc57d805d5c94ec9fc60f4838f5dbdfd2a48ecd77c23015fa44c6dd5f60bc40 - languageName: node - linkType: hard - "find-up@npm:^5.0.0": version: 5.0.0 resolution: "find-up@npm:5.0.0" @@ -5127,15 +4799,6 @@ __metadata: languageName: node linkType: hard -"global-dirs@npm:^3.0.0": - version: 3.0.1 - resolution: "global-dirs@npm:3.0.1" - dependencies: - ini: "npm:2.0.0" - checksum: 10c0/ef65e2241a47ff978f7006a641302bc7f4c03dfb98783d42bf7224c136e3a06df046e70ee3a010cf30214114755e46c9eb5eb1513838812fbbe0d92b14c25080 - languageName: node - linkType: hard - "globals@npm:^14.0.0": version: 14.0.0 resolution: "globals@npm:14.0.0" @@ -5267,15 +4930,6 @@ __metadata: languageName: node linkType: hard -"has-ansi@npm:^4.0.1": - version: 4.0.1 - resolution: "has-ansi@npm:4.0.1" - dependencies: - ansi-regex: "npm:^4.1.0" - checksum: 10c0/fa711146258487d0f7c5bb2d97dbb16ab9a78a314238aaa017d7fbab4ebf555ac3465ebec5875eb378138289467e0612d8f5606bc2273149b4361e4befebbbcf - languageName: node - linkType: hard - "has-bigints@npm:^1.0.2": version: 1.1.0 resolution: "has-bigints@npm:1.1.0" @@ -5345,15 +4999,6 @@ __metadata: languageName: node linkType: hard -"hosted-git-info@npm:^9.0.0": - version: 9.0.2 - resolution: "hosted-git-info@npm:9.0.2" - dependencies: - lru-cache: "npm:^11.1.0" - checksum: 10c0/6c616339b61a103e3de4fef2776bc2b797767c3ed58fc2e3bb2e3b49294740c8c5ec3dde2d6440b09729e5a1d661dab6bacf54fdec46d1c466407a8df045d7a1 - languageName: node - linkType: hard - "hpagent@npm:^1.2.0": version: 1.2.0 resolution: "hpagent@npm:1.2.0" @@ -5506,13 +5151,6 @@ __metadata: languageName: node linkType: hard -"indent-string@npm:^4.0.0": - version: 4.0.0 - resolution: "indent-string@npm:4.0.0" - checksum: 10c0/1e1904ddb0cb3d6cce7cd09e27a90184908b7a5d5c21b92e232c93579d314f0b83c246ffb035493d0504b1e9147ba2c9b21df0030f48673fba0496ecd698161f - languageName: node - linkType: hard - "indent-string@npm:^5.0.0": version: 5.0.0 resolution: "indent-string@npm:5.0.0" @@ -5520,13 +5158,6 @@ __metadata: languageName: node linkType: hard -"index-to-position@npm:^1.1.0": - version: 1.2.0 - resolution: "index-to-position@npm:1.2.0" - checksum: 10c0/d7ac9fae9fad1d7fbeb7bd92e1553b26e8b10522c2d80af5c362828428a41360e21fc5915d7b8c8227eb0f0d37b12099846ac77381a04d6c0059eb81749e374d - languageName: node - linkType: hard - "inflight@npm:^1.0.4": version: 1.0.6 resolution: "inflight@npm:1.0.6" @@ -5544,13 +5175,6 @@ __metadata: languageName: node linkType: hard -"ini@npm:2.0.0": - version: 2.0.0 - resolution: "ini@npm:2.0.0" - checksum: 10c0/2e0c8f386369139029da87819438b20a1ff3fe58372d93fb1a86e9d9344125ace3a806b8ec4eb160a46e64cbc422fe68251869441676af49b7fc441af2389c25 - languageName: node - linkType: hard - "internal-slot@npm:^1.1.0": version: 1.1.0 resolution: "internal-slot@npm:1.1.0" @@ -5748,16 +5372,6 @@ __metadata: languageName: node linkType: hard -"is-installed-globally@npm:^0.4.0": - version: 0.4.0 - resolution: "is-installed-globally@npm:0.4.0" - dependencies: - global-dirs: "npm:^3.0.0" - is-path-inside: "npm:^3.0.2" - checksum: 10c0/f3e6220ee5824b845c9ed0d4b42c24272701f1f9926936e30c0e676254ca5b34d1b92c6205cae11b283776f9529212c0cdabb20ec280a6451677d6493ca9c22d - languageName: node - linkType: hard - "is-map@npm:^2.0.3": version: 2.0.3 resolution: "is-map@npm:2.0.3" @@ -5796,13 +5410,6 @@ __metadata: languageName: node linkType: hard -"is-path-inside@npm:^3.0.2": - version: 3.0.3 - resolution: "is-path-inside@npm:3.0.3" - checksum: 10c0/cf7d4ac35fb96bab6a1d2c3598fe5ebb29aafb52c0aaa482b5a3ed9d8ba3edc11631e3ec2637660c44b3ce0e61a08d54946e8af30dec0b60a7c27296c68ffd05 - languageName: node - linkType: hard - "is-plain-obj@npm:^4.1.0": version: 4.1.0 resolution: "is-plain-obj@npm:4.1.0" @@ -5845,7 +5452,7 @@ __metadata: languageName: node linkType: hard -"is-stream@npm:^2.0.0, is-stream@npm:^2.0.1": +"is-stream@npm:^2.0.1": version: 2.0.1 resolution: "is-stream@npm:2.0.1" checksum: 10c0/7c284241313fc6efc329b8d7f08e16c0efeb6baab1b4cd0ba579eb78e5af1aa5da11e68559896a2067cd6c526bd29241dda4eb1225e627d5aa1a89a76d4635a5 @@ -6011,13 +5618,6 @@ __metadata: languageName: node linkType: hard -"js-tokens@npm:^4.0.0": - version: 4.0.0 - resolution: "js-tokens@npm:4.0.0" - checksum: 10c0/e248708d377aa058eacf2037b07ded847790e6de892bbad3dac0abba2e759cb9f121b00099a65195616badcb6eca8d14d975cb3e89eb1cfda644756402c8aeed - languageName: node - linkType: hard - "js-yaml@npm:^3.10.0": version: 3.14.2 resolution: "js-yaml@npm:3.14.2" @@ -6139,15 +5739,6 @@ __metadata: languageName: node linkType: hard -"knuth-shuffle-seeded@npm:^1.0.6": - version: 1.0.6 - resolution: "knuth-shuffle-seeded@npm:1.0.6" - dependencies: - seed-random: "npm:~2.2.0" - checksum: 10c0/c8a3085fec79a3bac24bcccd0be7bf757f881e023d587ce858afc684c1a6c979ae6d9493e5efc6dc331527a8af0e4fe512d66e34ac39e79f45ae967ac8a60d6e - languageName: node - linkType: hard - "lazystream@npm:^1.0.0": version: 1.0.1 resolution: "lazystream@npm:1.0.1" @@ -6361,20 +5952,6 @@ __metadata: languageName: node linkType: hard -"lodash.mergewith@npm:^4.6.2": - version: 4.6.2 - resolution: "lodash.mergewith@npm:4.6.2" - checksum: 10c0/4adbed65ff96fd65b0b3861f6899f98304f90fd71e7f1eb36c1270e05d500ee7f5ec44c02ef979b5ddbf75c0a0b9b99c35f0ad58f4011934c4d4e99e5200b3b5 - languageName: node - linkType: hard - -"lodash.sortby@npm:^4.7.0": - version: 4.7.0 - resolution: "lodash.sortby@npm:4.7.0" - checksum: 10c0/fc48fb54ff7669f33bb32997cab9460757ee99fafaf72400b261c3e10fde21538e47d8cfcbe6a25a31bcb5b7b727c27d52626386fc2de24eb059a6d64a89cdf5 - languageName: node - linkType: hard - "lodash@npm:^4.17.15, lodash@npm:^4.17.21": version: 4.17.23 resolution: "lodash@npm:4.17.23" @@ -6395,15 +5972,6 @@ __metadata: languageName: node linkType: hard -"lower-case@npm:^2.0.2": - version: 2.0.2 - resolution: "lower-case@npm:2.0.2" - dependencies: - tslib: "npm:^2.0.3" - checksum: 10c0/3d925e090315cf7dc1caa358e0477e186ffa23947740e4314a7429b6e62d72742e0bbe7536a5ae56d19d7618ce998aba05caca53c2902bd5742fdca5fc57fd7b - languageName: node - linkType: hard - "lowercase-keys@npm:^2.0.0": version: 2.0.0 resolution: "lowercase-keys@npm:2.0.0" @@ -6439,13 +6007,6 @@ __metadata: languageName: node linkType: hard -"luxon@npm:3.7.2, luxon@npm:^3.5.0": - version: 3.7.2 - resolution: "luxon@npm:3.7.2" - checksum: 10c0/ed8f0f637826c08c343a29dd478b00628be93bba6f068417b1d8896b61cb61c6deacbe1df1e057dbd9298334044afa150f9aaabbeb3181418ac8520acfdc2ae2 - languageName: node - linkType: hard - "magic-string@npm:^0.30.17, magic-string@npm:^0.30.21": version: 0.30.21 resolution: "magic-string@npm:0.30.21" @@ -6580,15 +6141,6 @@ __metadata: languageName: node linkType: hard -"mime@npm:^3.0.0": - version: 3.0.0 - resolution: "mime@npm:3.0.0" - bin: - mime: cli.js - checksum: 10c0/402e792a8df1b2cc41cb77f0dcc46472b7944b7ec29cb5bbcd398624b6b97096728f1239766d3fdeb20551dd8d94738344c195a6ea10c4f906eb0356323b0531 - languageName: node - linkType: hard - "mimic-function@npm:^5.0.0": version: 5.0.1 resolution: "mimic-function@npm:5.0.1" @@ -6747,15 +6299,6 @@ __metadata: languageName: node linkType: hard -"mkdirp@npm:^3.0.0": - version: 3.0.1 - resolution: "mkdirp@npm:3.0.1" - bin: - mkdirp: dist/cjs/src/bin.js - checksum: 10c0/9f2b975e9246351f5e3a40dcfac99fcd0baa31fbfab615fe059fb11e51f10e4803c63de1f384c54d656e4db31d000e4767e9ef076a22e12a641357602e31d57d - languageName: node - linkType: hard - "mlly@npm:^1.7.4": version: 1.8.2 resolution: "mlly@npm:1.8.2" @@ -6844,16 +6387,6 @@ __metadata: languageName: node linkType: hard -"no-case@npm:^3.0.4": - version: 3.0.4 - resolution: "no-case@npm:3.0.4" - dependencies: - lower-case: "npm:^2.0.2" - tslib: "npm:^2.0.3" - checksum: 10c0/8ef545f0b3f8677c848f86ecbd42ca0ff3cd9dd71c158527b344c69ba14710d816d8489c746b6ca225e7b615108938a0bda0a54706f8c255933703ac1cf8e703 - languageName: node - linkType: hard - "node-gyp@npm:latest": version: 12.2.0 resolution: "node-gyp@npm:12.2.0" @@ -6892,17 +6425,6 @@ __metadata: languageName: node linkType: hard -"normalize-package-data@npm:^8.0.0": - version: 8.0.0 - resolution: "normalize-package-data@npm:8.0.0" - dependencies: - hosted-git-info: "npm:^9.0.0" - semver: "npm:^7.3.5" - validate-npm-package-license: "npm:^3.0.4" - checksum: 10c0/abd9d85912d6435979a5779d30e54b7725a6271e36186f284d00b33886a584d738ca7c2d2569e7f7e1be9cc72d90c1485d58562f546163b49edb87ea30804acf - languageName: node - linkType: hard - "normalize-path@npm:^3.0.0": version: 3.0.0 resolution: "normalize-path@npm:3.0.0" @@ -7212,15 +6734,6 @@ __metadata: languageName: node linkType: hard -"pad-right@npm:^0.2.2": - version: 0.2.2 - resolution: "pad-right@npm:0.2.2" - dependencies: - repeat-string: "npm:^1.5.2" - checksum: 10c0/fa8752633a616d1310550df4bf602d4f3bc58753033cfdd45a381fbf0c39c7ddeb577d37f9dd3746cbc85f9fd957ca9dce346f39e9f18e199aaa3d0dbb11595a - languageName: node - linkType: hard - "parent-module@npm:^1.0.0": version: 1.0.1 resolution: "parent-module@npm:1.0.1" @@ -7230,17 +6743,6 @@ __metadata: languageName: node linkType: hard -"parse-json@npm:^8.3.0": - version: 8.3.0 - resolution: "parse-json@npm:8.3.0" - dependencies: - "@babel/code-frame": "npm:^7.26.2" - index-to-position: "npm:^1.1.0" - type-fest: "npm:^4.39.1" - checksum: 10c0/0eb5a50f88b8428c8f7a9cf021636c16664f0c62190323652d39e7bdf62953e7c50f9957e55e17dc2d74fc05c89c11f5553f381dbc686735b537ea9b101c7153 - languageName: node - linkType: hard - "parse-ms@npm:^4.0.0": version: 4.0.0 resolution: "parse-ms@npm:4.0.0" @@ -7485,13 +6987,6 @@ __metadata: languageName: node linkType: hard -"progress@npm:^2.0.3": - version: 2.0.3 - resolution: "progress@npm:2.0.3" - checksum: 10c0/1697e07cb1068055dbe9fe858d242368ff5d2073639e652b75a7eb1f2a1a8d4afd404d719de23c7b48481a6aa0040686310e2dac2f53d776daa2176d3f96369c - languageName: node - linkType: hard - "proper-lockfile@npm:^4.1.2": version: 4.1.2 resolution: "proper-lockfile@npm:4.1.2" @@ -7503,13 +6998,6 @@ __metadata: languageName: node linkType: hard -"property-expr@npm:^2.0.5": - version: 2.0.6 - resolution: "property-expr@npm:2.0.6" - checksum: 10c0/69b7da15038a1146d6447c69c445306f66a33c425271235bb20507f1846dbf9577a8f9dfafe8acbfcb66f924b270157f155248308f026a68758f35fc72265b3c - languageName: node - linkType: hard - "proxy-addr@npm:^2.0.7": version: 2.0.7 resolution: "proxy-addr@npm:2.0.7" @@ -7609,30 +7097,6 @@ __metadata: languageName: node linkType: hard -"read-package-up@npm:^12.0.0": - version: 12.0.0 - resolution: "read-package-up@npm:12.0.0" - dependencies: - find-up-simple: "npm:^1.0.1" - read-pkg: "npm:^10.0.0" - type-fest: "npm:^5.2.0" - checksum: 10c0/aa0aa280e7adc00edef9c157b475262f64df3ba3bdd3c27f59f5b28225d3522c2e354bb823d5df08079bfbd863466a1512255c92a9776a93e3bdc49b9d90fc2d - languageName: node - linkType: hard - -"read-pkg@npm:^10.0.0": - version: 10.1.0 - resolution: "read-pkg@npm:10.1.0" - dependencies: - "@types/normalize-package-data": "npm:^2.4.4" - normalize-package-data: "npm:^8.0.0" - parse-json: "npm:^8.3.0" - type-fest: "npm:^5.4.4" - unicorn-magic: "npm:^0.4.0" - checksum: 10c0/6a284bd00945239f715b8a0e986ad0faf29e184f5f26890734991c2696e48b4fa330939f937faac85bc4a2f6be17f8fce288ecd795b337bb4e37a5b75c464569 - languageName: node - linkType: hard - "readable-stream@npm:^2.0.5": version: 2.3.8 resolution: "readable-stream@npm:2.3.8" @@ -7677,13 +7141,6 @@ __metadata: languageName: node linkType: hard -"reflect-metadata@npm:0.2.2": - version: 0.2.2 - resolution: "reflect-metadata@npm:0.2.2" - checksum: 10c0/1cd93a15ea291e420204955544637c264c216e7aac527470e393d54b4bb075f10a17e60d8168ec96600c7e0b9fcc0cb0bb6e91c3fbf5b0d8c9056f04e6ac1ec2 - languageName: node - linkType: hard - "reflect.getprototypeof@npm:^1.0.6, reflect.getprototypeof@npm:^1.0.9": version: 1.0.10 resolution: "reflect.getprototypeof@npm:1.0.10" @@ -7700,24 +7157,6 @@ __metadata: languageName: node linkType: hard -"regexp-match-indices@npm:1.0.2": - version: 1.0.2 - resolution: "regexp-match-indices@npm:1.0.2" - dependencies: - regexp-tree: "npm:^0.1.11" - checksum: 10c0/35e8af9844a4d08f54000766a7f364579c2082080b6f0f6edb9af9faab0452a83e68ef71304e3b2d12ff6d71a615d30b43de60cafeabbce3e1fa95c56383d147 - languageName: node - linkType: hard - -"regexp-tree@npm:^0.1.11": - version: 0.1.27 - resolution: "regexp-tree@npm:0.1.27" - bin: - regexp-tree: bin/regexp-tree - checksum: 10c0/f636f44b4a0d93d7d6926585ecd81f63e4ce2ac895bc417b2ead0874cd36b337dcc3d0fedc63f69bf5aaeaa4340f36ca7e750c9687cceaf8087374e5284e843c - languageName: node - linkType: hard - "regexp.prototype.flags@npm:^1.5.4": version: 1.5.4 resolution: "regexp.prototype.flags@npm:1.5.4" @@ -7732,13 +7171,6 @@ __metadata: languageName: node linkType: hard -"repeat-string@npm:^1.5.2, repeat-string@npm:^1.6.1": - version: 1.6.1 - resolution: "repeat-string@npm:1.6.1" - checksum: 10c0/87fa21bfdb2fbdedc44b9a5b118b7c1239bdd2c2c1e42742ef9119b7d412a5137a1d23f1a83dc6bb686f4f27429ac6f542e3d923090b44181bafa41e8ac0174d - languageName: node - linkType: hard - "require-from-string@npm:^2.0.2": version: 2.0.2 resolution: "require-from-string@npm:2.0.2" @@ -8125,13 +7557,6 @@ __metadata: languageName: node linkType: hard -"seed-random@npm:~2.2.0": - version: 2.2.0 - resolution: "seed-random@npm:2.2.0" - checksum: 10c0/5d48dedf312e8075faa899664ec5867e3b088a11d0e955d7c6bcfffcd7fc6de5dbb51c41c24ef48060ee6a9592cad607e5c44306b4fd4828667451afe16b09a5 - languageName: node - linkType: hard - "seedrandom@npm:^3.0.5": version: 3.0.5 resolution: "seedrandom@npm:3.0.5" @@ -8139,21 +7564,21 @@ __metadata: languageName: node linkType: hard -"semver@npm:7.7.4, semver@npm:^7.1.2, semver@npm:^7.3.5, semver@npm:^7.5.4, semver@npm:^7.7.3, semver@npm:~7.7.0": - version: 7.7.4 - resolution: "semver@npm:7.7.4" +"semver@npm:^6.3.0, semver@npm:^6.3.1": + version: 6.3.1 + resolution: "semver@npm:6.3.1" bin: semver: bin/semver.js - checksum: 10c0/5215ad0234e2845d4ea5bb9d836d42b03499546ddafb12075566899fc617f68794bb6f146076b6881d755de17d6c6cc73372555879ec7dce2c2feee947866ad2 + checksum: 10c0/e3d79b609071caa78bcb6ce2ad81c7966a46a7431d9d58b8800cfa9cb6a63699b3899a0e4bcce36167a284578212d9ae6942b6929ba4aa5015c079a67751d42d languageName: node linkType: hard -"semver@npm:^6.3.0, semver@npm:^6.3.1": - version: 6.3.1 - resolution: "semver@npm:6.3.1" +"semver@npm:^7.1.2, semver@npm:^7.3.5, semver@npm:^7.5.4, semver@npm:^7.7.3, semver@npm:~7.7.0": + version: 7.7.4 + resolution: "semver@npm:7.7.4" bin: semver: bin/semver.js - checksum: 10c0/e3d79b609071caa78bcb6ce2ad81c7966a46a7431d9d58b8800cfa9cb6a63699b3899a0e4bcce36167a284578212d9ae6942b6929ba4aa5015c079a67751d42d + checksum: 10c0/5215ad0234e2845d4ea5bb9d836d42b03499546ddafb12075566899fc617f68794bb6f146076b6881d755de17d6c6cc73372555879ec7dce2c2feee947866ad2 languageName: node linkType: hard @@ -8372,17 +7797,7 @@ __metadata: languageName: node linkType: hard -"source-map-support@npm:0.5.21, source-map-support@npm:^0.5.21": - version: 0.5.21 - resolution: "source-map-support@npm:0.5.21" - dependencies: - buffer-from: "npm:^1.0.0" - source-map: "npm:^0.6.0" - checksum: 10c0/9ee09942f415e0f721d6daad3917ec1516af746a8120bba7bb56278707a37f1eb8642bde456e98454b8a885023af81a16e646869975f06afc1a711fb90484e7d - languageName: node - linkType: hard - -"source-map@npm:^0.6.0, source-map@npm:^0.6.1, source-map@npm:~0.6.1": +"source-map@npm:^0.6.1, source-map@npm:~0.6.1": version: 0.6.1 resolution: "source-map@npm:0.6.1" checksum: 10c0/ab55398007c5e5532957cb0beee2368529618ac0ab372d789806f5718123cc4367d57de3904b4e6a4170eb5a0b0f41373066d02ca0735a0c4d75c7d328d3e011 @@ -8396,40 +7811,6 @@ __metadata: languageName: node linkType: hard -"spdx-correct@npm:^3.0.0": - version: 3.2.0 - resolution: "spdx-correct@npm:3.2.0" - dependencies: - spdx-expression-parse: "npm:^3.0.0" - spdx-license-ids: "npm:^3.0.0" - checksum: 10c0/49208f008618b9119208b0dadc9208a3a55053f4fd6a0ae8116861bd22696fc50f4142a35ebfdb389e05ccf2de8ad142573fefc9e26f670522d899f7b2fe7386 - languageName: node - linkType: hard - -"spdx-exceptions@npm:^2.1.0": - version: 2.5.0 - resolution: "spdx-exceptions@npm:2.5.0" - checksum: 10c0/37217b7762ee0ea0d8b7d0c29fd48b7e4dfb94096b109d6255b589c561f57da93bf4e328c0290046115961b9209a8051ad9f525e48d433082fc79f496a4ea940 - languageName: node - linkType: hard - -"spdx-expression-parse@npm:^3.0.0": - version: 3.0.1 - resolution: "spdx-expression-parse@npm:3.0.1" - dependencies: - spdx-exceptions: "npm:^2.1.0" - spdx-license-ids: "npm:^3.0.0" - checksum: 10c0/6f8a41c87759fa184a58713b86c6a8b028250f158159f1d03ed9d1b6ee4d9eefdc74181c8ddc581a341aa971c3e7b79e30b59c23b05d2436d5de1c30bdef7171 - languageName: node - linkType: hard - -"spdx-license-ids@npm:^3.0.0": - version: 3.0.23 - resolution: "spdx-license-ids@npm:3.0.23" - checksum: 10c0/8495620f6f2a237749cce922ea2d593a66f7885c301b1a0f5542183e7041182f27f616a8f13345cefdea0c9b3e0899328e0aa8cec100cf4f3fac4bb3bd975515 - languageName: node - linkType: hard - "split@npm:0.3": version: 0.3.3 resolution: "split@npm:0.3.3" @@ -8462,13 +7843,6 @@ __metadata: languageName: node linkType: hard -"stackframe@npm:^1.3.4": - version: 1.3.4 - resolution: "stackframe@npm:1.3.4" - checksum: 10c0/18410f7a1e0c5d211a4effa83bdbf24adbe8faa8c34db52e1cd3e89837518c592be60b60d8b7270ac53eeeb8b807cd11b399a41667f6c9abb41059c3ccc8a989 - languageName: node - linkType: hard - "statuses@npm:^2.0.1, statuses@npm:^2.0.2, statuses@npm:~2.0.2": version: 2.0.2 resolution: "statuses@npm:2.0.2" @@ -8529,13 +7903,6 @@ __metadata: languageName: node linkType: hard -"string-argv@npm:0.3.1": - version: 0.3.1 - resolution: "string-argv@npm:0.3.1" - checksum: 10c0/f59582070f0a4a2d362d8331031f313771ad2b939b223b0593d7765de2689c975e0069186cef65977a29af9deec248c7e480ea4015d153ead754aea5e4bcfe7c - languageName: node - linkType: hard - "string-argv@npm:^0.3.2": version: 0.3.2 resolution: "string-argv@npm:0.3.2" @@ -8717,15 +8084,6 @@ __metadata: languageName: node linkType: hard -"supports-color@npm:^8.1.1": - version: 8.1.1 - resolution: "supports-color@npm:8.1.1" - dependencies: - has-flag: "npm:^4.0.0" - checksum: 10c0/ea1d3c275dd604c974670f63943ed9bd83623edc102430c05adb8efc56ba492746b6e95386e7831b872ec3807fd89dd8eb43f735195f37b5ec343e4234cc7e89 - languageName: node - linkType: hard - "supports-preserve-symlinks-flag@npm:^1.0.0": version: 1.0.0 resolution: "supports-preserve-symlinks-flag@npm:1.0.0" @@ -8733,13 +8091,6 @@ __metadata: languageName: node linkType: hard -"tagged-tag@npm:^1.0.0": - version: 1.0.0 - resolution: "tagged-tag@npm:1.0.0" - checksum: 10c0/91d25c9ffb86a91f20522cefb2cbec9b64caa1febe27ad0df52f08993ff60888022d771e868e6416cf2e72dab68449d2139e8709ba009b74c6c7ecd4000048d1 - languageName: node - linkType: hard - "tar-stream@npm:^3.0.0": version: 3.1.8 resolution: "tar-stream@npm:3.1.8" @@ -8837,13 +8188,6 @@ __metadata: languageName: node linkType: hard -"tiny-case@npm:^1.0.3": - version: 1.0.3 - resolution: "tiny-case@npm:1.0.3" - checksum: 10c0/c0cbed35884a322265e2cd61ff435168d1ea523f88bf3864ce14a238ae9169e732649776964283a66e4eb882e655992081d4daf8c865042e2233425866111b35 - languageName: node - linkType: hard - "tiny-emitter@npm:^2.1.0": version: 2.1.0 resolution: "tiny-emitter@npm:2.1.0" @@ -8951,13 +8295,6 @@ __metadata: languageName: node linkType: hard -"toposort@npm:^2.0.2": - version: 2.0.2 - resolution: "toposort@npm:2.0.2" - checksum: 10c0/ab9ca91fce4b972ccae9e2f539d755bf799a0c7eb60da07fd985fce0f14c159ed1e92305ff55697693b5bc13e300f5417db90e2593b127d421c9f6c440950222 - languageName: node - linkType: hard - "tough-cookie@npm:^6.0.0": version: 6.0.1 resolution: "tough-cookie@npm:6.0.1" @@ -8992,13 +8329,6 @@ __metadata: languageName: node linkType: hard -"ts-dedent@npm:^2.0.0": - version: 2.2.0 - resolution: "ts-dedent@npm:2.2.0" - checksum: 10c0/175adea838468cc2ff7d5e97f970dcb798bbcb623f29c6088cb21aa2880d207c5784be81ab1741f56b9ac37840cbaba0c0d79f7f8b67ffe61c02634cafa5c303 - languageName: node - linkType: hard - "ts-interface-checker@npm:^0.1.9": version: 0.1.13 resolution: "ts-interface-checker@npm:0.1.13" @@ -9018,7 +8348,7 @@ __metadata: languageName: node linkType: hard -"tslib@npm:^2.0.1, tslib@npm:^2.0.3, tslib@npm:^2.4.0, tslib@npm:^2.5.0, tslib@npm:^2.6.2": +"tslib@npm:^2.0.1, tslib@npm:^2.4.0, tslib@npm:^2.5.0, tslib@npm:^2.6.2": version: 2.8.1 resolution: "tslib@npm:2.8.1" checksum: 10c0/9c4759110a19c53f992d9aae23aac5ced636e99887b51b9e61def52611732872ff7668757d4e4c61f19691e36f4da981cd9485e869b4a7408d689f6bf1f14e62 @@ -9099,29 +8429,20 @@ __metadata: languageName: node linkType: hard -"type-fest@npm:^2.11.2, type-fest@npm:^2.19.0": +"type-fest@npm:^2.11.2": version: 2.19.0 resolution: "type-fest@npm:2.19.0" checksum: 10c0/a5a7ecf2e654251613218c215c7493574594951c08e52ab9881c9df6a6da0aeca7528c213c622bc374b4e0cb5c443aa3ab758da4e3c959783ce884c3194e12cb languageName: node linkType: hard -"type-fest@npm:^4.0.0, type-fest@npm:^4.26.1, type-fest@npm:^4.39.1, type-fest@npm:^4.41.0": +"type-fest@npm:^4.0.0, type-fest@npm:^4.26.1": version: 4.41.0 resolution: "type-fest@npm:4.41.0" checksum: 10c0/f5ca697797ed5e88d33ac8f1fec21921839871f808dc59345c9cf67345bfb958ce41bd821165dbf3ae591cedec2bf6fe8882098dfdd8dc54320b859711a2c1e4 languageName: node linkType: hard -"type-fest@npm:^5.2.0, type-fest@npm:^5.4.4": - version: 5.5.0 - resolution: "type-fest@npm:5.5.0" - dependencies: - tagged-tag: "npm:^1.0.0" - checksum: 10c0/60bf79a8df45abf99490e3204eceb5cf7f915413f8a69fb578c75cab37ddcb7d29ee21f185f0e1617323ac0b2a441e001b8dc691e220d0b087e9c29ea205538c - languageName: node - linkType: hard - "type-is@npm:^2.0.1": version: 2.0.1 resolution: "type-is@npm:2.0.1" @@ -9284,13 +8605,6 @@ __metadata: languageName: node linkType: hard -"unicorn-magic@npm:^0.4.0": - version: 0.4.0 - resolution: "unicorn-magic@npm:0.4.0" - checksum: 10c0/cd6eff90967a5528dfa2016bdb5b38b0cd64c8558f9ba04fb5c2c23f3a232a67dfe2bfa4c45af3685d5f1a40dbac6a36d48e053f80f97ae4da1e0f6a55431685 - languageName: node - linkType: hard - "universalify@npm:^2.0.0": version: 2.0.1 resolution: "universalify@npm:2.0.1" @@ -9319,15 +8633,6 @@ __metadata: languageName: node linkType: hard -"upper-case-first@npm:^2.0.2": - version: 2.0.2 - resolution: "upper-case-first@npm:2.0.2" - dependencies: - tslib: "npm:^2.0.3" - checksum: 10c0/ccad6a0b143310ebfba2b5841f30bef71246297385f1329c022c902b2b5fc5aee009faf1ac9da5ab3ba7f615b88f5dc1cd80461b18a8f38cb1d4c3eb92538ea9 - languageName: node - linkType: hard - "uri-js@npm:^4.2.2": version: 4.4.1 resolution: "uri-js@npm:4.4.1" @@ -9337,13 +8642,6 @@ __metadata: languageName: node linkType: hard -"util-arity@npm:^1.1.0": - version: 1.1.0 - resolution: "util-arity@npm:1.1.0" - checksum: 10c0/eed63ffd055fcbbcfce9dd93c10cc94472e76d8d4e4d9c2ba9f08668af84aaa7efe3396123045660e6449f61adf83be8b455e20b231305dae9cfa3c599b69e0d - languageName: node - linkType: hard - "util-deprecate@npm:~1.0.1": version: 1.0.2 resolution: "util-deprecate@npm:1.0.2" @@ -9358,16 +8656,6 @@ __metadata: languageName: node linkType: hard -"validate-npm-package-license@npm:^3.0.4": - version: 3.0.4 - resolution: "validate-npm-package-license@npm:3.0.4" - dependencies: - spdx-correct: "npm:^3.0.0" - spdx-expression-parse: "npm:^3.0.0" - checksum: 10c0/7b91e455a8de9a0beaa9fe961e536b677da7f48c9a493edf4d4d4a87fd80a7a10267d438723364e432c2fcd00b5650b5378275cded362383ef570276e6312f4f - languageName: node - linkType: hard - "vary@npm:^1, vary@npm:^1.1.2": version: 1.1.2 resolution: "vary@npm:1.1.2" @@ -9702,13 +8990,6 @@ __metadata: languageName: node linkType: hard -"xmlbuilder@npm:^15.1.1": - version: 15.1.1 - resolution: "xmlbuilder@npm:15.1.1" - checksum: 10c0/665266a8916498ff8d82b3d46d3993913477a254b98149ff7cff060d9b7cc0db7cf5a3dae99aed92355254a808c0e2e3ec74ad1b04aa1061bdb8dfbea26c18b8 - languageName: node - linkType: hard - "yallist@npm:^4.0.0": version: 4.0.0 resolution: "yallist@npm:4.0.0" @@ -9723,7 +9004,7 @@ __metadata: languageName: node linkType: hard -"yaml@npm:^2.2.2, yaml@npm:^2.8.2": +"yaml@npm:^2.8.2": version: 2.8.2 resolution: "yaml@npm:2.8.2" bin: @@ -9746,18 +9027,6 @@ __metadata: languageName: node linkType: hard -"yup@npm:1.7.1": - version: 1.7.1 - resolution: "yup@npm:1.7.1" - dependencies: - property-expr: "npm:^2.0.5" - tiny-case: "npm:^1.0.3" - toposort: "npm:^2.0.2" - type-fest: "npm:^2.19.0" - checksum: 10c0/76b8c7fc2ba467a346935d027a25c067f9653bb0413cd60fbe0518e3d62637a56dbfca49979c4bab1a93d8e9a50843ca73d90bdc271e2f5bce1effa7734e5f28 - languageName: node - linkType: hard - "zip-stream@npm:^6.0.1": version: 6.0.1 resolution: "zip-stream@npm:6.0.1"