diff --git a/.changeset/violet-bikes-study.md b/.changeset/violet-bikes-study.md new file mode 100644 index 00000000..7b93d215 --- /dev/null +++ b/.changeset/violet-bikes-study.md @@ -0,0 +1,5 @@ +--- +"@inngest/test": minor +--- + +Refactor `@inngest/test` to have a much simpler public API diff --git a/packages/test/.gitignore b/packages/test/.gitignore index de4d1f00..36f7a103 100644 --- a/packages/test/.gitignore +++ b/packages/test/.gitignore @@ -1,2 +1,3 @@ dist node_modules +inngest-test.tgz diff --git a/packages/test/CHANGELOG.md b/packages/test/CHANGELOG.md new file mode 100644 index 00000000..e69de29b diff --git a/packages/test/README.md b/packages/test/README.md index df3ae75c..5ef1185f 100644 --- a/packages/test/README.md +++ b/packages/test/README.md @@ -1,5 +1,315 @@ # @inngest/test -- [ ] TODO Make Inngest a peer dep once package changes are shipped -- [ ] TODO Re-add release scripts -- [ ] TODO Copy `CHANGELOG.md` +This package helps you test your Inngest functions with Jest-compatible mocking, +allowing you to mock function state, step tooling, and inputs. Jest +compatibility means we aim for compatibility with all major testing frameworks, +runtimes, and libraries: + +- `jest` +- `vitest` +- `bun:test` (Bun) +- `@std/expect` (Deno) +- `chai`/`expect` + +## Table of contents + +- [Installation](#installation) +- [Running tests](#running-tests) + - [Running an entire function](#running-tests) + - [Running an individual step](#running-an-individual-step) +- [Assertions](#assertions) + - [Function/step output](#assertions) + - [Function input](#assertions) + - [Step state](#assertions) +- [Mocking](#mocking) + - [Events](#event-data) + - [Steps](#step-data) + - [Modules and imports](#modules-and-imports) + - [Custom](#custom) +- [TODO](#known-issues--todo) + +## Installation + +This package requires `inngest@>=3.22.12`. + +``` +npm install -D @inngest/test +``` + +## Running tests + +Use whichever supported testing framework; `@inngest/test` is unopinionated +about how your tests are run. We'll demonstrate here using `jest`. + +We import `InngestTestEngine` and our target function, `helloWorld`, and create +a new `InngestTestEngine` instance. + +```ts +import { InngestTestEngine } from "@inngest/test"; +import { helloWorld } from "./helloWorld"; + +describe("helloWorld function", () => { + const t = new InngestTestEngine({ + function: helloWorld, + }); +}); +``` + +Within that, we'll add a new test using the primary API, +`t.execute()`: + +```ts +test("returns a greeting", async () => { + const { result } = await t.execute(); + expect(result).toEqual("Hello World!"); +}); +``` + +This will run the entire function (steps and all) to completion, then return the +response from the function, where we assert that it was the string `"Hello +World!"`. + +An error + +### Running an individual step + +`t.executeStep()` can be used to run the function until a particular step has +been executed. This is useful to test a single step within a function or to see +that a non-runnable step such as `step.waitForEvent()` has been registered with +the correct options. + +```ts +test("runs the price calculations", async () => { + const { result } = await t.executeStep("calculate-price"); + expect(result).toEqual(123); +}); +``` + +Assertions can also be made on steps in any part of a run, regardless of if +that's the checkpoint we've waited for. See [Assertions -> State](#assertions). + +## Assertions + +Inngest adds like Jest-compatible mocks by default that can help you assert +function and step input and output. You can assert: + +- Function input +- Function output +- Step output +- Step tool usage + +All of these values are returned from both `t.execute()` and `t.executeStep()`; +we'll only show one for simplicity here. + +The `result` is returned, which is the output of the run or step: + +```ts +const { result } = await t.execute(); +expect(result).toEqual("Hello World!"); +``` + +`ctx` is the input used for the function run. This can be used to assert outputs +that are based on input data such as `event` or `runId`: + +```ts +const { ctx, result } = await t.execute(); +expect(result).toEqual(`Run ID was: "${ctx.runId}"`); +``` + +> [!NOTE] +> The tests also run middleware, so you can test that middleware inputs are also +> being used correctly. + +The step tooling at `ctx.step` are Jest-compatible spy functions, so you can use +them to assert that they've been called and used correctly: + +```ts +const { ctx } = await t.execute(); +expect(ctx.step.run).toHaveBeenCalledWith("my-step", expect.any(Function)); +``` + +`state` is also returned, which is a view into the outputs of all of the steps +in the run. This allows you to test each individual step output for any given +input: + +```ts +const { state } = await t.execute(); +expect(state["my-step"]).resolves.toEqual("some successful output"); +expect(state["dangerous-step"]).rejects.toThrowError("something failed"); +``` + +## Mocking + +Some mocking is done automatically by `@inngest/test`, but can be overwritten if +needed. + +All mocks (detailed below) can be specified either when creating an `InngestTestEngine` instance +or for each individual execution: + +```ts +// Set the events for every execution +const t = new InngestTestEngine({ + function: helloWorld, + // mocks here +}); + +// Or for just one, which will overwrite any current event mocks +t.execute({ + // mocks here +}); + +t.executeStep("my-step", { + // mocks here +}) +``` + +You can also clone an existing `InngestTestEngine` instance to encourage re-use +of complex mocks: + +```ts +// Make a direct clone, which includes any mocks +const otherT = t.clone(); + +// Provide some more mocks in addition to any existing ones +const anotherT = t.clone({ + // mocks here +}); +``` + +For simplicity, the following examples will show usage of +`t.execute()`, but the mocks can be placed in any of these locations. + +### Event data + +The incoming event data can be mocked. They are always specified as an array of +events to allow also mocking batches. + +```ts +t.execute({ + events: [{ name: "demo/event.sent", data: { message: "Hi!" } }], +}); +``` + +If no event mocks are given at all (or `events: undefined` is explicitly set), +an `inngest/function.invoked` event will be mocked for you. + +### Step data + +Mocking step data can help you model different paths and situations within your +function. To do so, any step can be mocked by providing the `steps` option. + +Here we mock two steps, one that will run successfully and another that will +model a failure and throw an error. + +```ts +t.execute({ + steps: [ + { + id: "successful-step", + handler() { + return "We did it!"; + }, + }, + { + id: "dangerous-step", + handler() { + throw new Error("Oh no!"); + }, + }, + ], +}); +``` + +These handlers will run lazily when they are found during a function's +execution. This means you can write complex mocks that respond to other +information: + +```ts +let message = ""; + +t.execute({ + steps: [ + { + id: "build-greeting", + handler() { + message = "Hello, "; + return message; + }, + }, + { + id: "build-name", + handler() { + return message + " World!"; + }, + }, + ], +}); +``` + +> [!NOTE] +> We'll later add `ctx` and `state` to the input of `handler`, meaning you'll +> get much easier access to existing state and function input in order to +> provide more accurate mocks. + +### Modules and imports + +Any mocking of modules or imports outside of Inngest which your functions may +rely on should be done outside of Inngest with the testing framework you're +using. For convenience, here are some links to the major supported frameworks +and their guidance for mocking imports: + +- [`jest`](https://jestjs.io/docs/mock-functions#mocking-modules) +- [`vitest`](https://vitest.dev/guide/mocking#modules) +- [`bun:test` (Bun)](https://bun.sh/docs/test/mocks#module-mocks-with-mock-module) +- [`@std/testing` (Deno)](https://jsr.io/@std/testing/doc/mock/~) + +### Custom + +While the package performs some basic mocks of the input object to a function in +order to spy on `ctx.step.*`, you can provide your own mocks for the function +input to do whatever you want with. + +When instantiating a new `InngestTestEngine` or starting an execution, provide a +`transformCtx` function that will add these mocks every time the function is +run: + +```ts +const t = new InngestTestEngine({ + function: helloWorld, + transformCtx: (ctx) => { + return { + ...ctx, + event: someCustomThing, + }; + }, +}); +``` + +If you wish to still add the automatic spies to `ctx.step.*`, you can import and +use the automatic transforms as part of your own: + +```ts +import { InngestTestEngine, mockCtx } from "@inngest/test"; + +const t = new InngestTestEngine({ + function: helloWorld, + transformCtx: (ctx) => { + return { + ...mockCtx(ctx), + event: someCustomThing, + }; + }, +}); +``` + +## Known issues / TODO + +- There are currently no retries modelled; any step or function that fails once + will fail permanently +- `onFailure` handlers are not run automatically +- Mocked step outputs do not model the JSON (de)serialization process yet, so + some typing may be off (e.g. `Date`) +- You cannot specify any `reqArgs` yet, which could affect some middleware usage + that relies on a particular serve handler being used +- Calling `inngest.send()` within a function is not yet automatically mocked, likely + resulting in an error diff --git a/packages/test/package.json b/packages/test/package.json index f413859e..3beb8f25 100644 --- a/packages/test/package.json +++ b/packages/test/package.json @@ -9,10 +9,14 @@ }, "scripts": { "test": "jest", + "build": "pnpm run build:clean && pnpm run build:tsc && pnpm run build:copy", "build:clean": "rm -rf ./dist", "build:tsc": "tsc --project tsconfig.build.json", - "build:copy": "cp package.json LICENSE.md README.md dist", - "pack": "pnpm run build && yarn pack --verbose --frozen-lockfile --filename inngest-test.tgz --cwd dist" + "build:copy": "cp package.json LICENSE.md README.md CHANGELOG.md dist", + "pack": "pnpm run build && yarn pack --verbose --frozen-lockfile --filename inngest-test.tgz --cwd dist", + "postversion": "pnpm run build && pnpm run build:copy", + "release": "DIST_DIR=dist node ../../scripts/release/publish.js && pnpm dlx jsr publish --allow-slow-types --allow-dirty", + "release:version": "node ../../scripts/release/jsrVersion.js" }, "exports": { ".": { @@ -34,10 +38,14 @@ }, "author": "Jack Williams ", "license": "Apache-2.0", - "devDependencies": { - "@jest/globals": "^29.5.0" - }, "dependencies": { + "tinyspy": "^3.0.2", "ulid": "^2.3.0" + }, + "peerDependencies": { + "inngest": "^3.22.12" + }, + "devDependencies": { + "prettier": "^3.1.0" } } diff --git a/packages/test/src/InngestTestEngine.ts b/packages/test/src/InngestTestEngine.ts index 251356e6..c5b642ca 100644 --- a/packages/test/src/InngestTestEngine.ts +++ b/packages/test/src/InngestTestEngine.ts @@ -1,4 +1,3 @@ -import { jest } from "@jest/globals"; import { ExecutionVersion, type MemoizedOp, @@ -8,10 +7,11 @@ import type { InngestFunction } from "inngest/components/InngestFunction"; import { serializeError } from "inngest/helpers/errors"; import { createDeferredPromise } from "inngest/helpers/promises"; import { ServerTiming } from "inngest/helpers/ServerTiming"; -import { Context, EventPayload } from "inngest/types"; +import { Context, EventPayload, StepOpCode } from "inngest/types"; import { ulid } from "ulid"; import { InngestTestRun } from "./InngestTestRun.js"; -import { createMockEvent, mockCtx } from "./util.js"; +import type { Mock } from "./spy.js"; +import { createMockEvent, mockCtx, type DeepPartial } from "./util.js"; /** * A test engine for running Inngest functions in a test environment, providing @@ -100,7 +100,7 @@ export namespace InngestTestEngine { */ export interface MockContext extends Omit { step: { - [K in keyof Context.Any["step"]]: jest.Mock; + [K in keyof Context.Any["step"]]: Mock; }; } @@ -110,14 +110,32 @@ export namespace InngestTestEngine { */ export type InlineOptions = Omit; + /** + * Options that can be passed to an initial execution that then waits for a + * particular checkpoint to occur. + */ + export type ExecuteOptions< + T extends InngestTestRun.CheckpointKey = InngestTestRun.CheckpointKey, + > = InlineOptions & { + /** + * An optional subset of the checkpoint to match against. Any checkpoint of + * this type will be matched. + * + * When providing a `subset`, use `expect` tooling such as + * `expect.stringContaining` to match partial values. + */ + subset?: DeepPartial>; + }; + + export type ExecuteStepOptions = InlineOptions & { + subset?: DeepPartial>; + }; + /** * A mocked state object that allows you to assert step usage, input, and * output. */ - export type MockState = Record< - string, - jest.Mock<(...args: unknown[]) => Promise> - >; + export type MockState = Record>; /** * The output of an individual function execution. @@ -185,35 +203,156 @@ export class InngestTestEngine { * * Is a shortcut for and uses `run.waitFor()`. */ - public async executeAndWaitFor( + public async execute( /** * Options and state to start the run with. */ - inlineOpts: InngestTestEngine.InlineOptions, + inlineOpts?: InngestTestEngine.ExecuteOptions + ): Promise { + const { run } = await this.individualExecution(inlineOpts); + + return run + .waitFor("function-resolved") + .then((output) => { + return { + ctx: output.ctx, + state: output.state, + result: output.result.data, + }; + }) + .catch((rejectedOutput) => { + if ( + typeof rejectedOutput === "object" && + rejectedOutput !== null && + "ctx" in rejectedOutput && + "state" in rejectedOutput + ) { + return { + ctx: rejectedOutput.ctx, + state: rejectedOutput.state, + error: rejectedOutput.error, + }; + } + + throw rejectedOutput; + }); + } + + /** + * Start a run from the given state and keep executing the function until the + * given step has run. + */ + public async executeStep( + /** + * The ID of the step to execute. + */ + stepId: string, + /** + * Options and state to start the run with. + */ + inlineOpts?: InngestTestEngine.ExecuteOptions + ): Promise { + const { run, result: resultaaa } = await this.individualExecution({ + ...inlineOpts, + // always overwrite this so it's easier to capture non-runnable steps in + // the same flow. + disableImmediateExecution: true, + }); + + const foundSteps = await run.waitFor("steps-found", { + steps: [{ id: stepId }], + }); + + const hashedStepId = _internals.hashId(stepId); + + const step = foundSteps.result.steps.find( + (step) => step.id === hashedStepId + ); + + // never found the step? Unexpected. + if (!step) { + throw new Error( + `Step "${stepId}" not found, but execution was still paused. This is a bug.` + ); + } + + // if this is not a runnable step, return it now + // runnable steps should return void + // + // some of these ops are nonsensical for the checkpoint we're waiting for, + // but we consider them anyway to ensure that this type requires attention + // if we add more opcodes + const baseRet: InngestTestRun.RunStepOutput = { + ctx: foundSteps.ctx, + state: foundSteps.state, + step, + }; + + const opHandlers: Record< + StepOpCode, + () => InngestTestRun.RunStepOutput | void + > = { + // runnable, so do nothing now + [StepOpCode.StepPlanned]: () => {}, + + [StepOpCode.InvokeFunction]: () => baseRet, + [StepOpCode.Sleep]: () => baseRet, + [StepOpCode.StepError]: () => ({ ...baseRet, error: step.error }), + [StepOpCode.StepNotFound]: () => baseRet, + [StepOpCode.StepRun]: () => ({ ...baseRet, result: step.data }), + [StepOpCode.WaitForEvent]: () => baseRet, + [StepOpCode.Step]: () => ({ ...baseRet, result: step.data }), + }; + + const result = opHandlers[step.op](); + if (result) { + return result; + } + + // otherwise, run the step and return the output + const runOutput = await run.waitFor("step-ran", { + step: { id: stepId }, + }); + + return { + ctx: runOutput.ctx, + state: runOutput.state, + result: runOutput.result.step.data, + error: runOutput.result.step.error, + step: runOutput.result.step, + }; + } + + /** + * Start a run from the given state and keep executing the function until a + * specific checkpoint is reached. + * + * Is a shortcut for and uses `run.waitFor()`. + * + * @TODO This is a duplicate of `execute` and will probably be removed; it's a + * very minor convenience method that deals too much with the internals. + */ + protected async executeAndWaitFor( /** * The checkpoint to wait for. */ checkpoint: T, /** - * An optional subset of the checkpoint to match against. Any checkpoint of - * this type will be matched. - * - * When providing a `subset`, use `expect` tooling such as - * `expect.stringContaining` to match partial values. + * Options and state to start the run with. */ - subset?: Partial> + inlineOpts?: InngestTestEngine.ExecuteOptions ): Promise> { - const { run } = await this.execute(inlineOpts); + const { run } = await this.individualExecution(inlineOpts); - return run.waitFor(checkpoint, subset); + return run.waitFor(checkpoint, inlineOpts?.subset); } /** * Execute the function with the given inline options. */ - public async execute( + protected async individualExecution( inlineOpts?: InngestTestEngine.InlineOptions ): Promise { const options = { @@ -314,45 +453,29 @@ export class InngestTestEngine { const { ctx, ops, ...result } = await execution.start(); - const mockState: InngestTestEngine.MockState = Object.keys(ops).reduce( - (acc, stepId) => { + const mockState: InngestTestEngine.MockState = await Object.keys( + ops + ).reduce( + async (acc, stepId) => { const op = ops[stepId]; - if (op?.seen === false || !op?.rawArgs) { + if ( + op?.seen === false || + !op?.rawArgs || + !op?.fulfilled || + !op?.promise + ) { return acc; } - const mock = jest.fn(async (...args: unknown[]) => { - if ("error" in op) { - throw op.error; - } - - return op.data; - }); - - // execute it to show it was hit - mock(op.rawArgs); - return { - ...acc, - [stepId]: mock, + ...(await acc), + [stepId]: op.promise, }; }, - {} as InngestTestEngine.MockState + Promise.resolve({}) as Promise ); - // now proxy the mock state to always retrn some empty mock that hasn't been - // called for missing keys - const mockStateProxy = new Proxy(mockState, { - get(target, prop) { - if (prop in target) { - return target[prop as keyof typeof target]; - } - - return jest.fn(); - }, - }); - const run = new InngestTestRun({ testEngine: this.clone(options), }); @@ -360,7 +483,7 @@ export class InngestTestEngine { return { result, ctx: ctx as InngestTestEngine.MockContext, - state: mockStateProxy, + state: mockState, run, }; } diff --git a/packages/test/src/InngestTestRun.ts b/packages/test/src/InngestTestRun.ts index 2e640465..f7314982 100644 --- a/packages/test/src/InngestTestRun.ts +++ b/packages/test/src/InngestTestRun.ts @@ -1,10 +1,12 @@ -import { expect } from "@jest/globals"; +import { OutgoingOp } from "inngest"; import type { ExecutionResult, ExecutionResults, } from "inngest/components/execution/InngestExecution"; +import { _internals } from "inngest/components/execution/v1"; import { createDeferredPromise } from "inngest/helpers/promises"; import type { InngestTestEngine } from "./InngestTestEngine.js"; +import { isDeeplyEqual, type DeepPartial } from "./util"; /** * A test run that allows you to wait for specific checkpoints in a run that @@ -35,6 +37,16 @@ export namespace InngestTestRun { Extract, "ctx" | "ops" >; + + export interface RunOutput + extends Pick { + result?: Checkpoint<"function-resolved">["data"]; + error?: Checkpoint<"function-rejected">["error"]; + } + + export interface RunStepOutput extends RunOutput { + step: OutgoingOp; + } } /** @@ -68,39 +80,69 @@ export class InngestTestRun { * When providing a `subset`, use `expect` tooling such as * `expect.stringContaining` to match partial values. */ - subset?: Partial> + subset?: DeepPartial> ): Promise> { let finished = false; - const runningState: InngestTestEngine.InlineOptions = {}; + const runningState: InngestTestEngine.InlineOptions = { + events: this.options.testEngine["options"].events, + steps: this.options.testEngine["options"].steps, + }; - const { promise, resolve } = + const { promise, resolve, reject } = createDeferredPromise>(); const finish = (output: InngestTestEngine.ExecutionOutput) => { finished = true; + + if (output.result.type !== checkpoint) { + return reject(output); + } + resolve(output as InngestTestEngine.ExecutionOutput); }; + /** + * Make sure we sanitize any given ID to prehash it for the user. This is + * abstracted from the user entirely so they shouldn't be expected to be + * providing hashes. + */ + const sanitizedSubset: typeof subset = subset && { + ...subset, + + // "step" for "step-ran" + ...("step" in subset && + typeof subset.step === "object" && + subset.step !== null && + "id" in subset.step && + typeof subset.step.id === "string" && { + step: { ...subset.step, id: _internals.hashId(subset.step.id) }, + }), + + // "steps" for "steps-found" + ...("steps" in subset && + Array.isArray(subset.steps) && { + steps: subset.steps.map((step) => ({ + ...step, + id: _internals.hashId(step.id), + })), + }), + }; + const processChain = async (targetStepId?: string) => { if (finished) { return; } - const exec = await this.options.testEngine.execute({ + const exec = await this.options.testEngine["individualExecution"]({ ...runningState, targetStepId, }); - if (exec.result.type === checkpoint) { - try { - if (subset) { - expect(exec.result).toMatchObject(subset); - } - - return finish(exec); - } catch (err) { - // noop - } + if ( + exec.result.type === checkpoint && + (!sanitizedSubset || isDeeplyEqual(sanitizedSubset, exec.result)) + ) { + return finish(exec); } const resultHandlers: Record void> = { diff --git a/packages/test/src/spy.ts b/packages/test/src/spy.ts new file mode 100644 index 00000000..dd4dc0a2 --- /dev/null +++ b/packages/test/src/spy.ts @@ -0,0 +1,625 @@ +import type { SpyInternalImpl } from "tinyspy"; +import * as tinyspy from "tinyspy"; + +interface MockResultReturn { + type: "return"; + /** + * The value that was returned from the function. If function returned a Promise, then this will be a resolved value. + */ + value: T; +} +interface MockResultIncomplete { + type: "incomplete"; + value: undefined; +} +interface MockResultThrow { + type: "throw"; + /** + * An error that was thrown during function execution. + */ + value: any; +} + +interface MockSettledResultFulfilled { + type: "fulfilled"; + value: T; +} + +interface MockSettledResultRejected { + type: "rejected"; + value: any; +} + +export type MockResult = + | MockResultReturn + | MockResultThrow + | MockResultIncomplete; +export type MockSettledResult = + | MockSettledResultFulfilled + | MockSettledResultRejected; + +export interface MockContext { + /** + * This is an array containing all arguments for each call. One item of the array is the arguments of that call. + * + * @example + * const fn = vi.fn() + * + * fn('arg1', 'arg2') + * fn('arg3') + * + * fn.mock.calls === [ + * ['arg1', 'arg2'], // first call + * ['arg3'], // second call + * ] + */ + calls: Parameters[]; + /** + * This is an array containing all instances that were instantiated when mock was called with a `new` keyword. Note that this is an actual context (`this`) of the function, not a return value. + */ + instances: ReturnType[]; + /** + * An array of `this` values that were used during each call to the mock function. + */ + contexts: ThisParameterType[]; + /** + * The order of mock's execution. This returns an array of numbers which are shared between all defined mocks. + * + * @example + * const fn1 = vi.fn() + * const fn2 = vi.fn() + * + * fn1() + * fn2() + * fn1() + * + * fn1.mock.invocationCallOrder === [1, 3] + * fn2.mock.invocationCallOrder === [2] + */ + invocationCallOrder: number[]; + /** + * This is an array containing all values that were `returned` from the function. + * + * The `value` property contains the returned value or thrown error. If the function returned a `Promise`, then `result` will always be `'return'` even if the promise was rejected. + * + * @example + * const fn = vi.fn() + * .mockReturnValueOnce('result') + * .mockImplementationOnce(() => { throw new Error('thrown error') }) + * + * const result = fn() + * + * try { + * fn() + * } + * catch {} + * + * fn.mock.results === [ + * { + * type: 'return', + * value: 'result', + * }, + * { + * type: 'throw', + * value: Error, + * }, + * ] + */ + results: MockResult>[]; + /** + * An array containing all values that were `resolved` or `rejected` from the function. + * + * This array will be empty if the function was never resolved or rejected. + * + * @example + * const fn = vi.fn().mockResolvedValueOnce('result') + * + * const result = fn() + * + * fn.mock.settledResults === [] + * fn.mock.results === [ + * { + * type: 'return', + * value: Promise<'result'>, + * }, + * ] + * + * await result + * + * fn.mock.settledResults === [ + * { + * type: 'fulfilled', + * value: 'result', + * }, + * ] + */ + settledResults: MockSettledResult>>[]; + /** + * This contains the arguments of the last call. If spy wasn't called, will return `undefined`. + */ + lastCall: Parameters | undefined; +} + +type Procedure = (...args: any[]) => any; +// pick a single function type from function overloads, unions, etc... +type NormalizedPrecedure = ( + ...args: Parameters +) => ReturnType; + +type Methods = keyof { + [K in keyof T as T[K] extends Procedure ? K : never]: T[K]; +}; +type Properties = { + [K in keyof T]: T[K] extends Procedure ? never : K; +}[keyof T] & + (string | symbol); +type Classes = { + [K in keyof T]: T[K] extends new (...args: any[]) => any ? K : never; +}[keyof T] & + (string | symbol); + +/* +cf. https://typescript-eslint.io/rules/method-signature-style/ + +Typescript assignability is different between + { foo: (f: T) => U } (this is "method-signature-style") +and + { foo(f: T): U } + +Jest uses the latter for `MockInstance.mockImplementation` etc... and it allows assignment such as: + const boolFn: Jest.Mock<() => boolean> = jest.fn<() => true>(() => true) +*/ +/* eslint-disable ts/method-signature-style */ +export interface MockInstance { + /** + * Use it to return the name given to mock with method `.mockName(name)`. + */ + getMockName(): string; + /** + * Sets internal mock name. Useful to see the name of the mock if an assertion fails. + */ + mockName(n: string): this; + /** + * Current context of the mock. It stores information about all invocation calls, instances, and results. + */ + mock: MockContext; + /** + * Clears all information about every call. After calling it, all properties on `.mock` will return an empty state. This method does not reset implementations. + * + * It is useful if you need to clean up mock between different assertions. + */ + mockClear(): this; + /** + * Does what `mockClear` does and makes inner implementation an empty function (returning `undefined` when invoked). This also resets all "once" implementations. + * + * This is useful when you want to completely reset a mock to the default state. + */ + mockReset(): this; + /** + * Does what `mockReset` does and restores inner implementation to the original function. + * + * Note that restoring mock from `vi.fn()` will set implementation to an empty function that returns `undefined`. Restoring a `vi.fn(impl)` will restore implementation to `impl`. + */ + mockRestore(): void; + /** + * Returns current mock implementation if there is one. + * + * If mock was created with `vi.fn`, it will consider passed down method as a mock implementation. + * + * If mock was created with `vi.spyOn`, it will return `undefined` unless a custom implementation was provided. + */ + getMockImplementation(): NormalizedPrecedure | undefined; + /** + * Accepts a function that will be used as an implementation of the mock. + * @example + * const increment = vi.fn().mockImplementation(count => count + 1); + * expect(increment(3)).toBe(4); + */ + mockImplementation(fn: NormalizedPrecedure): this; + /** + * Accepts a function that will be used as a mock implementation during the next call. Can be chained so that multiple function calls produce different results. + * @example + * const fn = vi.fn(count => count).mockImplementationOnce(count => count + 1); + * expect(fn(3)).toBe(4); + * expect(fn(3)).toBe(3); + */ + mockImplementationOnce(fn: NormalizedPrecedure): this; + /** + * Overrides the original mock implementation temporarily while the callback is being executed. + * @example + * const myMockFn = vi.fn(() => 'original') + * + * myMockFn.withImplementation(() => 'temp', () => { + * myMockFn() // 'temp' + * }) + * + * myMockFn() // 'original' + */ + withImplementation( + fn: NormalizedPrecedure, + cb: () => T2 + ): T2 extends Promise ? Promise : this; + + /** + * Use this if you need to return `this` context from the method without invoking actual implementation. + */ + mockReturnThis(): this; + /** + * Accepts a value that will be returned whenever the mock function is called. + */ + mockReturnValue(obj: ReturnType): this; + /** + * Accepts a value that will be returned during the next function call. If chained, every consecutive call will return the specified value. + * + * When there are no more `mockReturnValueOnce` values to use, mock will fallback to the previously defined implementation if there is one. + * @example + * const myMockFn = vi + * .fn() + * .mockReturnValue('default') + * .mockReturnValueOnce('first call') + * .mockReturnValueOnce('second call') + * + * // 'first call', 'second call', 'default' + * console.log(myMockFn(), myMockFn(), myMockFn()) + */ + mockReturnValueOnce(obj: ReturnType): this; + /** + * Accepts a value that will be resolved when async function is called. + * @example + * const asyncMock = vi.fn().mockResolvedValue(42) + * asyncMock() // Promise<42> + */ + mockResolvedValue(obj: Awaited>): this; + /** + * Accepts a value that will be resolved during the next function call. If chained, every consecutive call will resolve specified value. + * @example + * const myMockFn = vi + * .fn() + * .mockResolvedValue('default') + * .mockResolvedValueOnce('first call') + * .mockResolvedValueOnce('second call') + * + * // Promise<'first call'>, Promise<'second call'>, Promise<'default'> + * console.log(myMockFn(), myMockFn(), myMockFn()) + */ + mockResolvedValueOnce(obj: Awaited>): this; + /** + * Accepts an error that will be rejected when async function is called. + * @example + * const asyncMock = vi.fn().mockRejectedValue(new Error('Async error')) + * await asyncMock() // throws 'Async error' + */ + mockRejectedValue(obj: any): this; + /** + * Accepts a value that will be rejected during the next function call. If chained, every consecutive call will reject specified value. + * @example + * const asyncMock = vi + * .fn() + * .mockResolvedValueOnce('first call') + * .mockRejectedValueOnce(new Error('Async error')) + * + * await asyncMock() // first call + * await asyncMock() // throws "Async error" + */ + mockRejectedValueOnce(obj: any): this; +} +/* eslint-enable ts/method-signature-style */ + +export interface Mock extends MockInstance { + new (...args: Parameters): ReturnType; + (...args: Parameters): ReturnType; +} + +type PartialMaybePromise = T extends Promise> + ? Promise>> + : Partial; + +export interface PartialMock + extends MockInstance< + (...args: Parameters) => PartialMaybePromise> + > { + new (...args: Parameters): ReturnType; + (...args: Parameters): ReturnType; +} + +export type MaybeMockedConstructor = T extends new ( + ...args: Array +) => infer R + ? Mock<(...args: ConstructorParameters) => R> + : T; +export type MockedFunction = Mock & { + [K in keyof T]: T[K]; +}; +export type PartiallyMockedFunction = PartialMock & { + [K in keyof T]: T[K]; +}; +export type MockedFunctionDeep = Mock & + MockedObjectDeep; +export type PartiallyMockedFunctionDeep = PartialMock & + MockedObjectDeep; +export type MockedObject = MaybeMockedConstructor & { + [K in Methods]: T[K] extends Procedure ? MockedFunction : T[K]; +} & { [K in Properties]: T[K] }; +export type MockedObjectDeep = MaybeMockedConstructor & { + [K in Methods]: T[K] extends Procedure ? MockedFunctionDeep : T[K]; +} & { [K in Properties]: MaybeMockedDeep }; + +export type MaybeMockedDeep = T extends Procedure + ? MockedFunctionDeep + : T extends object + ? MockedObjectDeep + : T; + +export type MaybePartiallyMockedDeep = T extends Procedure + ? PartiallyMockedFunctionDeep + : T extends object + ? MockedObjectDeep + : T; + +export type MaybeMocked = T extends Procedure + ? MockedFunction + : T extends object + ? MockedObject + : T; + +export type MaybePartiallyMocked = T extends Procedure + ? PartiallyMockedFunction + : T extends object + ? MockedObject + : T; + +interface Constructable { + new (...args: any[]): any; +} + +export type MockedClass = MockInstance< + (...args: ConstructorParameters) => InstanceType +> & { + prototype: T extends { prototype: any } ? Mocked : never; +} & T; + +export type Mocked = { + [P in keyof T]: T[P] extends Procedure + ? MockInstance + : T[P] extends Constructable + ? MockedClass + : T[P]; +} & T; + +export const mocks: Set = new Set(); + +export function isMockFunction(fn: any): fn is MockInstance { + return ( + typeof fn === "function" && "_isMockFunction" in fn && fn._isMockFunction + ); +} + +export function spyOn>>( + obj: T, + methodName: S, + accessType: "get" +): MockInstance<() => T[S]>; +export function spyOn>>( + obj: T, + methodName: G, + accessType: "set" +): MockInstance<(arg: T[G]) => void>; +export function spyOn> | Methods>>( + obj: T, + methodName: M +): Required[M] extends { new (...args: infer A): infer R } + ? MockInstance<(this: R, ...args: A) => R> + : T[M] extends Procedure + ? MockInstance + : never; +export function spyOn( + obj: T, + method: K, + accessType?: "get" | "set" +): MockInstance { + const dictionary = { + get: "getter", + set: "setter", + } as const; + const objMethod = accessType ? { [dictionary[accessType]]: method } : method; + + const stub = tinyspy.internalSpyOn(obj, objMethod as any); + + return enhanceSpy(stub) as MockInstance; +} + +let callOrder = 0; + +function enhanceSpy( + spy: SpyInternalImpl, ReturnType> +): MockInstance { + type TArgs = Parameters; + type TReturns = ReturnType; + + const stub = spy as unknown as MockInstance; + + let implementation: T | undefined; + + let instances: any[] = []; + let contexts: any[] = []; + let invocations: number[] = []; + + const state = tinyspy.getInternalState(spy); + + const mockContext: MockContext = { + get calls() { + return state.calls; + }, + get contexts() { + return contexts; + }, + get instances() { + return instances; + }, + get invocationCallOrder() { + return invocations; + }, + get results() { + return state.results.map(([callType, value]) => { + const type = + callType === "error" ? ("throw" as const) : ("return" as const); + return { type, value }; + }); + }, + get settledResults() { + return state.resolves.map(([callType, value]) => { + const type = + callType === "error" ? ("rejected" as const) : ("fulfilled" as const); + return { type, value }; + }); + }, + get lastCall() { + return state.calls[state.calls.length - 1]; + }, + }; + + let onceImplementations: ((...args: TArgs) => TReturns)[] = []; + let implementationChangedTemporarily = false; + + function mockCall(this: unknown, ...args: any) { + instances.push(this); + contexts.push(this); + invocations.push(++callOrder); + const impl = implementationChangedTemporarily + ? implementation! + : onceImplementations.shift() || + implementation || + state.getOriginal() || + (() => {}); + return impl.apply(this, args); + } + + let name: string = (stub as any).name; + + stub.getMockName = () => name || "vi.fn()"; + stub.mockName = (n) => { + name = n; + return stub; + }; + + stub.mockClear = () => { + state.reset(); + instances = []; + contexts = []; + invocations = []; + return stub; + }; + + stub.mockReset = () => { + stub.mockClear(); + implementation = (() => undefined) as T; + onceImplementations = []; + return stub; + }; + + stub.mockRestore = () => { + stub.mockReset(); + state.restore(); + implementation = undefined; + return stub; + }; + + stub.getMockImplementation = () => implementation; + stub.mockImplementation = (fn: T) => { + implementation = fn; + state.willCall(mockCall); + return stub; + }; + + stub.mockImplementationOnce = (fn: T) => { + onceImplementations.push(fn); + return stub; + }; + + function withImplementation(fn: T, cb: () => void): MockInstance; + function withImplementation( + fn: T, + cb: () => Promise + ): Promise>; + function withImplementation( + fn: T, + cb: () => void | Promise + ): MockInstance | Promise> { + const originalImplementation = implementation; + + implementation = fn; + state.willCall(mockCall); + implementationChangedTemporarily = true; + + const reset = () => { + implementation = originalImplementation; + implementationChangedTemporarily = false; + }; + + const result = cb(); + + if (result instanceof Promise) { + return result.then(() => { + reset(); + return stub; + }); + } + + reset(); + + return stub; + } + + stub.withImplementation = withImplementation; + + stub.mockReturnThis = () => + stub.mockImplementation(function (this: TReturns) { + return this; + } as any); + + stub.mockReturnValue = (val: TReturns) => + stub.mockImplementation((() => val) as any); + stub.mockReturnValueOnce = (val: TReturns) => + stub.mockImplementationOnce((() => val) as any); + + stub.mockResolvedValue = (val: Awaited) => + stub.mockImplementation((() => Promise.resolve(val as TReturns)) as any); + + stub.mockResolvedValueOnce = (val: Awaited) => + stub.mockImplementationOnce((() => + Promise.resolve(val as TReturns)) as any); + + stub.mockRejectedValue = (val: unknown) => + stub.mockImplementation((() => Promise.reject(val)) as any); + + stub.mockRejectedValueOnce = (val: unknown) => + stub.mockImplementationOnce((() => Promise.reject(val)) as any); + + Object.defineProperty(stub, "mock", { + get: () => mockContext, + }); + + state.willCall(mockCall); + + mocks.add(stub); + + return stub as any; +} + +export function fn( + implementation?: T +): Mock { + const enhancedSpy = enhanceSpy( + tinyspy.internalSpyOn( + { + spy: implementation || (function () {} as T), + }, + "spy" + ) + ); + if (implementation) { + enhancedSpy.mockImplementation(implementation); + } + + return enhancedSpy as any; +} diff --git a/packages/test/src/util.ts b/packages/test/src/util.ts index 541d21ee..973acb20 100644 --- a/packages/test/src/util.ts +++ b/packages/test/src/util.ts @@ -1,7 +1,7 @@ -import { jest } from "@jest/globals"; import { internalEvents } from "inngest"; import type { Context, EventPayload } from "inngest/types"; import { ulid } from "ulid"; +import { fn as mockFn } from "./spy.js"; /** * The default context transformation function that mocks all step tools. Use @@ -12,7 +12,7 @@ export const mockCtx = (ctx: Readonly): Context.Any => { const step = Object.keys(ctx.step).reduce( (acc, key) => { const tool = ctx.step[key as keyof typeof ctx.step]; - const mock = jest.fn(tool); + const mock = mockFn(tool); return { ...acc, @@ -40,3 +40,44 @@ export const createMockEvent = () => { ts: Date.now(), } satisfies EventPayload; }; + +/** + * A deep partial, where every key of every object is optional. + */ +export type DeepPartial = { + [K in keyof T]?: T[K] extends object ? DeepPartial : T[K]; +}; + +/** + * Ensures that all keys in the subset are present in the actual object and that + * the values match. + */ +export const isDeeplyEqual = ( + subset: DeepPartial, + actual: T +): boolean => { + return Object.keys(subset).every((key) => { + const subsetValue = subset[key as keyof T]; + const actualValue = actual[key as keyof T]; + + // an array? find all of the values + if (Array.isArray(subsetValue) && Array.isArray(actualValue)) { + return subsetValue.every((subValue, i) => { + return isDeeplyEqual(subValue, actualValue[i]); + }); + } + + // a non-array object? + if ( + typeof subsetValue === "object" && + subsetValue !== null && + typeof actualValue === "object" && + actualValue !== null + ) { + return isDeeplyEqual(subsetValue as T, actualValue); + } + + // anything else + return subsetValue === actualValue; + }); +}; diff --git a/packages/test/tsconfig.json b/packages/test/tsconfig.json index b93f8983..52ecc4d6 100644 --- a/packages/test/tsconfig.json +++ b/packages/test/tsconfig.json @@ -9,7 +9,10 @@ "esModuleInterop": true, "forceConsistentCasingInFileNames": true, "strict": true, - "skipLibCheck": true + "skipLibCheck": true, + "noUncheckedIndexedAccess": true, + "strictNullChecks": true, + "moduleResolution": "node" }, "include": ["./src/**/*"] } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b8605068..a069647c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -41,10 +41,10 @@ importers: version: 6.11.0(@eslint/eslintrc@2.1.3)(eslint@8.53.0)(typescript@5.5.2) jest: specifier: ^29.3.1 - version: 29.5.0(ts-node@10.9.1(typescript@5.5.2)) + version: 29.5.0(@types/node@20.14.8)(ts-node@10.9.1(@types/node@20.14.8)(typescript@5.5.2)) ts-jest: specifier: ^29.1.0 - version: 29.1.0(@babel/core@7.23.6)(@jest/types@29.5.0)(babel-jest@29.5.0(@babel/core@7.23.6))(jest@29.5.0(ts-node@10.9.1(typescript@5.5.2)))(typescript@5.5.2) + version: 29.1.0(@babel/core@7.23.6)(@jest/types@29.5.0)(babel-jest@29.5.0(@babel/core@7.23.6))(jest@29.5.0(@types/node@20.14.8)(ts-node@10.9.1(@types/node@20.14.8)(typescript@5.5.2)))(typescript@5.5.2) packages/eslint-plugin-internal: dependencies: @@ -271,13 +271,13 @@ importers: version: 1.5.1(node-fetch@2.7.0) inngest: specifier: ^3.21.0 - version: 3.21.0(@sveltejs/kit@1.27.3)(@vercel/node@2.15.9)(aws-lambda@1.0.7)(express@4.19.2)(fastify@4.21.0)(h3@1.8.1)(hono@4.2.7)(koa@2.14.2)(next@13.5.4(@babel/core@7.23.6))(typescript@5.5.2) + version: 3.21.0(@sveltejs/kit@1.27.3(svelte@4.2.5)(vite@4.5.3(@types/node@20.14.8)))(@vercel/node@2.15.9)(aws-lambda@1.0.7)(express@4.19.2)(fastify@4.21.0)(h3@1.8.1)(hono@4.2.7)(koa@2.14.2)(next@13.5.4(@babel/core@7.23.6)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(typescript@5.5.2) jest: specifier: ^29.3.1 - version: 29.5.0(ts-node@10.9.1(typescript@5.5.2)) + version: 29.5.0(@types/node@20.14.8)(ts-node@10.9.1(@types/node@20.14.8)(typescript@5.5.2)) ts-jest: specifier: ^29.1.0 - version: 29.1.0(@babel/core@7.23.6)(@jest/types@29.5.0)(babel-jest@29.5.0(@babel/core@7.23.6))(jest@29.5.0(ts-node@10.9.1(typescript@5.5.2)))(typescript@5.5.2) + version: 29.1.0(@babel/core@7.23.6)(@jest/types@29.5.0)(babel-jest@29.5.0(@babel/core@7.23.6))(jest@29.5.0(@types/node@20.14.8)(ts-node@10.9.1(@types/node@20.14.8)(typescript@5.5.2)))(typescript@5.5.2) typescript: specifier: ~5.5.2 version: 5.5.2 @@ -295,20 +295,26 @@ importers: version: 8.14.0 inngest: specifier: ^3.19.11 - version: 3.19.20(@sveltejs/kit@1.27.3)(@vercel/node@2.15.9)(aws-lambda@1.0.7)(express@4.19.2)(fastify@4.21.0)(h3@1.8.1)(hono@4.2.7)(koa@2.14.2)(next@13.5.4)(typescript@5.4.2) + version: 3.19.20(@sveltejs/kit@1.27.3(svelte@4.2.5)(vite@4.5.3(@types/node@20.14.8)))(@vercel/node@2.15.9)(aws-lambda@1.0.7)(express@4.19.2)(fastify@4.21.0)(h3@1.8.1)(hono@4.2.7)(koa@2.14.2)(next@13.5.4(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(typescript@5.4.2) typescript: specifier: ~5.4.0 version: 5.4.2 packages/test: dependencies: + inngest: + specifier: ^3.22.12 + version: 3.22.13(@sveltejs/kit@1.27.3(svelte@4.2.5)(vite@4.5.3(@types/node@20.14.8)))(@vercel/node@2.15.9)(aws-lambda@1.0.7)(express@4.19.2)(fastify@4.21.0)(h3@1.8.1)(hono@4.2.7)(koa@2.14.2)(next@13.5.4(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(typescript@5.6.2) + tinyspy: + specifier: ^3.0.2 + version: 3.0.2 ulid: specifier: ^2.3.0 version: 2.3.0 devDependencies: - '@jest/globals': - specifier: ^29.5.0 - version: 29.5.0 + prettier: + specifier: ^3.1.0 + version: 3.1.0 packages: @@ -3020,6 +3026,42 @@ packages: typescript: optional: true + inngest@3.22.13: + resolution: {integrity: sha512-IBF0wgw3hfYyl59RX6xilKYQFbI+8kOLi78jIoi1zRheVoUAW4MMZEPOFpQVn3at1XtZehwSffctqZ4TjijtCQ==} + engines: {node: '>=14'} + peerDependencies: + '@sveltejs/kit': '>=1.27.3' + '@vercel/node': '>=2.15.9' + aws-lambda: '>=1.0.7' + express: '>=4.19.2' + fastify: '>=4.21.0' + h3: '>=1.8.1' + hono: '>=4.2.7' + koa: '>=2.14.2' + next: '>=12.0.0' + typescript: '>=4.7.2' + peerDependenciesMeta: + '@sveltejs/kit': + optional: true + '@vercel/node': + optional: true + aws-lambda: + optional: true + express: + optional: true + fastify: + optional: true + h3: + optional: true + hono: + optional: true + koa: + optional: true + next: + optional: true + typescript: + optional: true + inquirer@9.2.10: resolution: {integrity: sha512-tVVNFIXU8qNHoULiazz612GFl+yqNfjMTbLuViNJE/d860Qxrd3NMrse8dm40VUQLOQeULvaQF8lpAhvysjeyA==} engines: {node: '>=14.18.0'} @@ -4516,6 +4558,10 @@ packages: resolution: {integrity: sha512-iNgFugVuQgBKrqeO/mpiTTgmBsTP0WL6yeuLfLs/Ctf0pI/ixGqIRm8sDCwMcXGe9WWvt2sGXI5mNqZbValmJg==} engines: {node: '>=12'} + tinyspy@3.0.2: + resolution: {integrity: sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==} + engines: {node: '>=14.0.0'} + titleize@3.0.0: resolution: {integrity: sha512-KxVu8EYHDPBdUYdKZdKtU2aj2XfEx9AfjXxE/Aj0vT06w2icA09Vus1rh6eSu1y01akYg6BjIK/hxyLJINoMLQ==} engines: {node: '>=12'} @@ -5645,7 +5691,7 @@ snapshots: jest-util: 29.5.0 slash: 3.0.0 - '@jest/core@29.5.0(ts-node@10.9.1(@types/node@20.14.8)(typescript@5.6.2))': + '@jest/core@29.5.0(ts-node@10.9.1(@types/node@20.14.8)(typescript@5.5.2))': dependencies: '@jest/console': 29.5.0 '@jest/reporters': 29.5.0 @@ -5659,7 +5705,7 @@ snapshots: exit: 0.1.2 graceful-fs: 4.2.11 jest-changed-files: 29.5.0 - jest-config: 29.5.0(@types/node@20.14.8)(ts-node@10.9.1(@types/node@20.14.8)(typescript@5.6.2)) + jest-config: 29.5.0(@types/node@20.14.8)(ts-node@10.9.1(@types/node@20.14.8)(typescript@5.5.2)) jest-haste-map: 29.5.0 jest-message-util: 29.5.0 jest-regex-util: 29.4.3 @@ -5679,7 +5725,7 @@ snapshots: - supports-color - ts-node - '@jest/core@29.5.0(ts-node@10.9.1(typescript@5.5.2))': + '@jest/core@29.5.0(ts-node@10.9.1(@types/node@20.14.8)(typescript@5.6.2))': dependencies: '@jest/console': 29.5.0 '@jest/reporters': 29.5.0 @@ -5693,7 +5739,7 @@ snapshots: exit: 0.1.2 graceful-fs: 4.2.11 jest-changed-files: 29.5.0 - jest-config: 29.5.0(@types/node@20.14.8)(ts-node@10.9.1(typescript@5.5.2)) + jest-config: 29.5.0(@types/node@20.14.8)(ts-node@10.9.1(@types/node@20.14.8)(typescript@5.6.2)) jest-haste-map: 29.5.0 jest-message-util: 29.5.0 jest-regex-util: 29.4.3 @@ -6025,25 +6071,6 @@ snapshots: dependencies: '@sinonjs/commons': 2.0.0 - '@sveltejs/kit@1.27.3': - dependencies: - '@sveltejs/vite-plugin-svelte': 2.4.6 - '@types/cookie': 0.5.4 - cookie: 0.5.0 - devalue: 4.3.2 - esm-env: 1.0.0 - kleur: 4.1.5 - magic-string: 0.30.5 - mrmime: 1.0.1 - sade: 1.8.1 - set-cookie-parser: 2.6.0 - sirv: 2.0.3 - tiny-glob: 0.2.9 - undici: 5.26.5 - transitivePeerDependencies: - - supports-color - optional: true - '@sveltejs/kit@1.27.3(svelte@4.2.5)(vite@4.5.3(@types/node@20.14.8))': dependencies: '@sveltejs/vite-plugin-svelte': 2.4.6(svelte@4.2.5)(vite@4.5.3(@types/node@20.14.8)) @@ -6073,27 +6100,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@sveltejs/vite-plugin-svelte-inspector@1.0.4(@sveltejs/vite-plugin-svelte@2.4.6)': - dependencies: - '@sveltejs/vite-plugin-svelte': 2.4.6 - debug: 4.3.4 - transitivePeerDependencies: - - supports-color - optional: true - - '@sveltejs/vite-plugin-svelte@2.4.6': - dependencies: - '@sveltejs/vite-plugin-svelte-inspector': 1.0.4(@sveltejs/vite-plugin-svelte@2.4.6) - debug: 4.3.4 - deepmerge: 4.3.1 - kleur: 4.1.5 - magic-string: 0.30.5 - svelte-hmr: 0.15.3(svelte@4.2.5) - vitefu: 0.2.5(vite@4.5.3(@types/node@20.14.8)) - transitivePeerDependencies: - - supports-color - optional: true - '@sveltejs/vite-plugin-svelte@2.4.6(svelte@4.2.5)(vite@4.5.3(@types/node@20.14.8))': dependencies: '@sveltejs/vite-plugin-svelte-inspector': 1.0.4(@sveltejs/vite-plugin-svelte@2.4.6(svelte@4.2.5)(vite@4.5.3(@types/node@20.14.8)))(svelte@4.2.5)(vite@4.5.3(@types/node@20.14.8)) @@ -8121,7 +8127,7 @@ snapshots: inherits@2.0.4: {} - inngest@3.19.20(@sveltejs/kit@1.27.3)(@vercel/node@2.15.9)(aws-lambda@1.0.7)(express@4.19.2)(fastify@4.21.0)(h3@1.8.1)(hono@4.2.7)(koa@2.14.2)(next@13.5.4)(typescript@5.4.2): + inngest@3.19.20(@sveltejs/kit@1.27.3(svelte@4.2.5)(vite@4.5.3(@types/node@20.14.8)))(@vercel/node@2.15.9)(aws-lambda@1.0.7)(express@4.19.2)(fastify@4.21.0)(h3@1.8.1)(hono@4.2.7)(koa@2.14.2)(next@13.5.4(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(typescript@5.4.2): dependencies: '@types/debug': 4.1.12 canonicalize: 1.0.8 @@ -8135,7 +8141,7 @@ snapshots: strip-ansi: 5.2.0 zod: 3.22.3 optionalDependencies: - '@sveltejs/kit': 1.27.3 + '@sveltejs/kit': 1.27.3(svelte@4.2.5)(vite@4.5.3(@types/node@20.14.8)) '@vercel/node': 2.15.9 aws-lambda: 1.0.7 express: 4.19.2 @@ -8149,7 +8155,7 @@ snapshots: - encoding - supports-color - inngest@3.21.0(@sveltejs/kit@1.27.3)(@vercel/node@2.15.9)(aws-lambda@1.0.7)(express@4.19.2)(fastify@4.21.0)(h3@1.8.1)(hono@4.2.7)(koa@2.14.2)(next@13.5.4(@babel/core@7.23.6))(typescript@5.5.2): + inngest@3.21.0(@sveltejs/kit@1.27.3(svelte@4.2.5)(vite@4.5.3(@types/node@20.14.8)))(@vercel/node@2.15.9)(aws-lambda@1.0.7)(express@4.19.2)(fastify@4.21.0)(h3@1.8.1)(hono@4.2.7)(koa@2.14.2)(next@13.5.4(@babel/core@7.23.6)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(typescript@5.5.2): dependencies: '@types/debug': 4.1.12 canonicalize: 1.0.8 @@ -8163,7 +8169,7 @@ snapshots: strip-ansi: 5.2.0 zod: 3.22.3 optionalDependencies: - '@sveltejs/kit': 1.27.3 + '@sveltejs/kit': 1.27.3(svelte@4.2.5)(vite@4.5.3(@types/node@20.14.8)) '@vercel/node': 2.15.9 aws-lambda: 1.0.7 express: 4.19.2 @@ -8177,6 +8183,34 @@ snapshots: - encoding - supports-color + inngest@3.22.13(@sveltejs/kit@1.27.3(svelte@4.2.5)(vite@4.5.3(@types/node@20.14.8)))(@vercel/node@2.15.9)(aws-lambda@1.0.7)(express@4.19.2)(fastify@4.21.0)(h3@1.8.1)(hono@4.2.7)(koa@2.14.2)(next@13.5.4(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(typescript@5.6.2): + dependencies: + '@types/debug': 4.1.12 + canonicalize: 1.0.8 + chalk: 4.1.2 + cross-fetch: 4.0.0 + debug: 4.3.4 + hash.js: 1.1.7 + json-stringify-safe: 5.0.1 + ms: 2.1.3 + serialize-error-cjs: 0.1.3 + strip-ansi: 5.2.0 + zod: 3.22.3 + optionalDependencies: + '@sveltejs/kit': 1.27.3(svelte@4.2.5)(vite@4.5.3(@types/node@20.14.8)) + '@vercel/node': 2.15.9 + aws-lambda: 1.0.7 + express: 4.19.2 + fastify: 4.21.0 + h3: 1.8.1 + hono: 4.2.7 + koa: 2.14.2 + next: 13.5.4(@babel/core@7.23.6)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + typescript: 5.6.2 + transitivePeerDependencies: + - encoding + - supports-color + inquirer@9.2.10: dependencies: '@ljharb/through': 2.3.9 @@ -8413,16 +8447,16 @@ snapshots: transitivePeerDependencies: - supports-color - jest-cli@29.5.0(@types/node@20.14.8)(ts-node@10.9.1(@types/node@20.14.8)(typescript@5.6.2)): + jest-cli@29.5.0(@types/node@20.14.8)(ts-node@10.9.1(@types/node@20.14.8)(typescript@5.5.2)): dependencies: - '@jest/core': 29.5.0(ts-node@10.9.1(@types/node@20.14.8)(typescript@5.6.2)) + '@jest/core': 29.5.0(ts-node@10.9.1(@types/node@20.14.8)(typescript@5.5.2)) '@jest/test-result': 29.5.0 '@jest/types': 29.5.0 chalk: 4.1.2 exit: 0.1.2 graceful-fs: 4.2.11 import-local: 3.1.0 - jest-config: 29.5.0(@types/node@20.14.8)(ts-node@10.9.1(@types/node@20.14.8)(typescript@5.6.2)) + jest-config: 29.5.0(@types/node@20.14.8)(ts-node@10.9.1(@types/node@20.14.8)(typescript@5.5.2)) jest-util: 29.5.0 jest-validate: 29.5.0 prompts: 2.4.2 @@ -8432,16 +8466,16 @@ snapshots: - supports-color - ts-node - jest-cli@29.5.0(ts-node@10.9.1(typescript@5.5.2)): + jest-cli@29.5.0(@types/node@20.14.8)(ts-node@10.9.1(@types/node@20.14.8)(typescript@5.6.2)): dependencies: - '@jest/core': 29.5.0(ts-node@10.9.1(typescript@5.5.2)) + '@jest/core': 29.5.0(ts-node@10.9.1(@types/node@20.14.8)(typescript@5.6.2)) '@jest/test-result': 29.5.0 '@jest/types': 29.5.0 chalk: 4.1.2 exit: 0.1.2 graceful-fs: 4.2.11 import-local: 3.1.0 - jest-config: 29.5.0(@types/node@20.14.8)(ts-node@10.9.1(typescript@5.5.2)) + jest-config: 29.5.0(@types/node@20.14.8)(ts-node@10.9.1(@types/node@20.14.8)(typescript@5.6.2)) jest-util: 29.5.0 jest-validate: 29.5.0 prompts: 2.4.2 @@ -8451,7 +8485,7 @@ snapshots: - supports-color - ts-node - jest-config@29.5.0(@types/node@20.14.8)(ts-node@10.9.1(@types/node@20.14.8)(typescript@5.6.2)): + jest-config@29.5.0(@types/node@20.14.8)(ts-node@10.9.1(@types/node@20.14.8)(typescript@5.5.2)): dependencies: '@babel/core': 7.23.6 '@jest/test-sequencer': 29.5.0 @@ -8477,11 +8511,11 @@ snapshots: strip-json-comments: 3.1.1 optionalDependencies: '@types/node': 20.14.8 - ts-node: 10.9.1(@types/node@20.14.8)(typescript@5.6.2) + ts-node: 10.9.1(@types/node@20.14.8)(typescript@5.5.2) transitivePeerDependencies: - supports-color - jest-config@29.5.0(@types/node@20.14.8)(ts-node@10.9.1(typescript@5.5.2)): + jest-config@29.5.0(@types/node@20.14.8)(ts-node@10.9.1(@types/node@20.14.8)(typescript@5.6.2)): dependencies: '@babel/core': 7.23.6 '@jest/test-sequencer': 29.5.0 @@ -8507,7 +8541,7 @@ snapshots: strip-json-comments: 3.1.1 optionalDependencies: '@types/node': 20.14.8 - ts-node: 10.9.1(typescript@5.5.2) + ts-node: 10.9.1(@types/node@20.14.8)(typescript@5.6.2) transitivePeerDependencies: - supports-color @@ -8752,23 +8786,23 @@ snapshots: merge-stream: 2.0.0 supports-color: 8.1.1 - jest@29.5.0(@types/node@20.14.8)(ts-node@10.9.1(@types/node@20.14.8)(typescript@5.6.2)): + jest@29.5.0(@types/node@20.14.8)(ts-node@10.9.1(@types/node@20.14.8)(typescript@5.5.2)): dependencies: - '@jest/core': 29.5.0(ts-node@10.9.1(@types/node@20.14.8)(typescript@5.6.2)) + '@jest/core': 29.5.0(ts-node@10.9.1(@types/node@20.14.8)(typescript@5.5.2)) '@jest/types': 29.5.0 import-local: 3.1.0 - jest-cli: 29.5.0(@types/node@20.14.8)(ts-node@10.9.1(@types/node@20.14.8)(typescript@5.6.2)) + jest-cli: 29.5.0(@types/node@20.14.8)(ts-node@10.9.1(@types/node@20.14.8)(typescript@5.5.2)) transitivePeerDependencies: - '@types/node' - supports-color - ts-node - jest@29.5.0(ts-node@10.9.1(typescript@5.5.2)): + jest@29.5.0(@types/node@20.14.8)(ts-node@10.9.1(@types/node@20.14.8)(typescript@5.6.2)): dependencies: - '@jest/core': 29.5.0(ts-node@10.9.1(typescript@5.5.2)) + '@jest/core': 29.5.0(ts-node@10.9.1(@types/node@20.14.8)(typescript@5.6.2)) '@jest/types': 29.5.0 import-local: 3.1.0 - jest-cli: 29.5.0(ts-node@10.9.1(typescript@5.5.2)) + jest-cli: 29.5.0(@types/node@20.14.8)(ts-node@10.9.1(@types/node@20.14.8)(typescript@5.6.2)) transitivePeerDependencies: - '@types/node' - supports-color @@ -9894,6 +9928,8 @@ snapshots: tiny-lru@11.0.1: {} + tinyspy@3.0.2: {} + titleize@3.0.0: {} tmp@0.0.33: @@ -9938,34 +9974,34 @@ snapshots: dependencies: typescript: 5.5.2 - ts-jest@29.1.0(@babel/core@7.23.6)(@jest/types@29.5.0)(babel-jest@29.5.0(@babel/core@7.23.6))(jest@29.5.0(@types/node@20.14.8)(ts-node@10.9.1(@types/node@20.14.8)(typescript@5.6.2)))(typescript@5.6.2): + ts-jest@29.1.0(@babel/core@7.23.6)(@jest/types@29.5.0)(babel-jest@29.5.0(@babel/core@7.23.6))(jest@29.5.0(@types/node@20.14.8)(ts-node@10.9.1(@types/node@20.14.8)(typescript@5.5.2)))(typescript@5.5.2): dependencies: bs-logger: 0.2.6 fast-json-stable-stringify: 2.1.0 - jest: 29.5.0(@types/node@20.14.8)(ts-node@10.9.1(@types/node@20.14.8)(typescript@5.6.2)) + jest: 29.5.0(@types/node@20.14.8)(ts-node@10.9.1(@types/node@20.14.8)(typescript@5.5.2)) jest-util: 29.5.0 json5: 2.2.3 lodash.memoize: 4.1.2 make-error: 1.3.6 semver: 7.5.4 - typescript: 5.6.2 + typescript: 5.5.2 yargs-parser: 21.1.1 optionalDependencies: '@babel/core': 7.23.6 '@jest/types': 29.5.0 babel-jest: 29.5.0(@babel/core@7.23.6) - ts-jest@29.1.0(@babel/core@7.23.6)(@jest/types@29.5.0)(babel-jest@29.5.0(@babel/core@7.23.6))(jest@29.5.0(ts-node@10.9.1(typescript@5.5.2)))(typescript@5.5.2): + ts-jest@29.1.0(@babel/core@7.23.6)(@jest/types@29.5.0)(babel-jest@29.5.0(@babel/core@7.23.6))(jest@29.5.0(@types/node@20.14.8)(ts-node@10.9.1(@types/node@20.14.8)(typescript@5.6.2)))(typescript@5.6.2): dependencies: bs-logger: 0.2.6 fast-json-stable-stringify: 2.1.0 - jest: 29.5.0(ts-node@10.9.1(typescript@5.5.2)) + jest: 29.5.0(@types/node@20.14.8)(ts-node@10.9.1(@types/node@20.14.8)(typescript@5.6.2)) jest-util: 29.5.0 json5: 2.2.3 lodash.memoize: 4.1.2 make-error: 1.3.6 semver: 7.5.4 - typescript: 5.5.2 + typescript: 5.6.2 yargs-parser: 21.1.1 optionalDependencies: '@babel/core': 7.23.6 @@ -9995,7 +10031,7 @@ snapshots: v8-compile-cache-lib: 3.0.1 yn: 3.1.1 - ts-node@10.9.1(@types/node@20.14.8)(typescript@5.6.2): + ts-node@10.9.1(@types/node@20.14.8)(typescript@5.5.2): dependencies: '@cspotcode/source-map-support': 0.8.1 '@tsconfig/node10': 1.0.9 @@ -10009,25 +10045,26 @@ snapshots: create-require: 1.1.1 diff: 4.0.2 make-error: 1.3.6 - typescript: 5.6.2 + typescript: 5.5.2 v8-compile-cache-lib: 3.0.1 yn: 3.1.1 optional: true - ts-node@10.9.1(typescript@5.5.2): + ts-node@10.9.1(@types/node@20.14.8)(typescript@5.6.2): dependencies: '@cspotcode/source-map-support': 0.8.1 '@tsconfig/node10': 1.0.9 '@tsconfig/node12': 1.0.11 '@tsconfig/node14': 1.0.3 '@tsconfig/node16': 1.0.4 + '@types/node': 20.14.8 acorn: 8.11.2 acorn-walk: 8.2.0 arg: 4.1.3 create-require: 1.1.1 diff: 4.0.2 make-error: 1.3.6 - typescript: 5.5.2 + typescript: 5.6.2 v8-compile-cache-lib: 3.0.1 yn: 3.1.1 optional: true