From 1f5afdccb53ebd9b87a55cf09d8a442bf9a2c4f3 Mon Sep 17 00:00:00 2001 From: Chris Arderne Date: Wed, 5 Aug 2026 09:12:38 +0100 Subject: [PATCH 1/5] fix(build): support decorator metadata with TypeScript 7 --- .changeset/typescript-seven-builds.md | 2 +- .../extensions/emitDecoratorMetadata.mdx | 31 ++++- packages/build/package.json | 9 +- .../internal/loadTypescript.test.ts | 117 ++++++++++++++++++ .../src/extensions/internal/loadTypescript.ts | 70 +++++++++++ packages/build/src/extensions/typescript.ts | 6 +- pnpm-lock.yaml | 16 +++ 7 files changed, 242 insertions(+), 9 deletions(-) create mode 100644 packages/build/src/extensions/internal/loadTypescript.test.ts create mode 100644 packages/build/src/extensions/internal/loadTypescript.ts diff --git a/.changeset/typescript-seven-builds.md b/.changeset/typescript-seven-builds.md index 3f9d30ffcf1..960d4a58b83 100644 --- a/.changeset/typescript-seven-builds.md +++ b/.changeset/typescript-seven-builds.md @@ -10,4 +10,4 @@ "@trigger.dev/sdk": patch --- -Refresh package builds for TypeScript 7 compatibility while preserving existing runtime entry points. TypeScript remains an optional peer for the decorator metadata build extension, so installing the Trigger.dev CLI does not install an additional compiler. +Refresh package builds for TypeScript 7 compatibility while preserving existing runtime entry points. Projects using `emitDecoratorMetadata()` with TypeScript 7 can install Microsoft's `@typescript/typescript6` compatibility package alongside it; the package remains optional, so installing the Trigger.dev CLI does not install an additional compiler. diff --git a/docs/config/extensions/emitDecoratorMetadata.mdx b/docs/config/extensions/emitDecoratorMetadata.mdx index 24317fae5df..947e2315cec 100644 --- a/docs/config/extensions/emitDecoratorMetadata.mdx +++ b/docs/config/extensions/emitDecoratorMetadata.mdx @@ -22,8 +22,31 @@ export default defineConfig({ This is usually required if you are using certain ORMs, like TypeORM, that require this option to be enabled. It's not enabled by default because there is a performance cost to enabling it. - emitDecoratorMetadata works by hooking into the esbuild bundle process and using the TypeScript - compiler API to compile files where we detect the use of decorators. This means you must have - `emitDecoratorMetadata` enabled in your `tsconfig.json` file, as well as `typescript` installed in - your `devDependencies`. + `emitDecoratorMetadata` hooks into the esbuild bundle process and uses the TypeScript compiler API + to compile files containing decorators. Enable `emitDecoratorMetadata` in your `tsconfig.json` and + install `typescript` in your `devDependencies`. + +## Using with TypeScript 7 + +TypeScript 7 does not expose the JavaScript compiler API required by this extension. Install Microsoft's TypeScript 6 compatibility package alongside TypeScript 7: + + + +```bash npm +npm install --save-dev @typescript/typescript6@latest +``` + +```bash pnpm +pnpm add --save-dev @typescript/typescript6@latest +``` + +```bash bun +bun add --dev @typescript/typescript6@latest +``` + + + +Your project continues using TypeScript 7 for its normal type checking and compiler commands. The extension loads the compatibility package only when it needs to emit decorator metadata. + +Restart the Trigger.dev dev server after installing the package. diff --git a/packages/build/package.json b/packages/build/package.json index 5affd1a20c7..9f506b244c0 100644 --- a/packages/build/package.json +++ b/packages/build/package.json @@ -89,16 +89,23 @@ "devDependencies": { "@arethetypeswrong/cli": "^0.18.5", "@types/resolve": "^1.20.6", + "@typescript/typescript6": "6.0.2", "esbuild": "^0.23.0", "rimraf": "6.0.1", "tshy": "^4.1.3", "tsx": "4.17.0", - "typescript": "6.0.3" + "typescript": "6.0.3", + "typescript5": "npm:typescript@5.9.3", + "typescript7": "npm:typescript@7.0.2" }, "peerDependencies": { + "@typescript/typescript6": "^6.0.0", "typescript": ">=5.0.0" }, "peerDependenciesMeta": { + "@typescript/typescript6": { + "optional": true + }, "typescript": { "optional": true } diff --git a/packages/build/src/extensions/internal/loadTypescript.test.ts b/packages/build/src/extensions/internal/loadTypescript.test.ts new file mode 100644 index 00000000000..408d4ab7242 --- /dev/null +++ b/packages/build/src/extensions/internal/loadTypescript.test.ts @@ -0,0 +1,117 @@ +import { createRequire } from "node:module"; +import { mkdtempSync, mkdirSync, rmSync, symlinkSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { dirname, join } from "node:path"; +import { afterEach, describe, expect, it } from "vitest"; +import { loadTypescript } from "./loadTypescript.js"; + +const packageRequire = createRequire(join(process.cwd(), "package.json")); +const projectDirs = new Set(); + +function createProject(packages: Record) { + const projectDir = mkdtempSync(join(tmpdir(), "trigger-typescript-")); + projectDirs.add(projectDir); + const nodeModulesDir = join(projectDir, "node_modules"); + + mkdirSync(nodeModulesDir); + writeFileSync(join(projectDir, "package.json"), JSON.stringify({ private: true })); + + for (const [installedName, sourceName] of Object.entries(packages)) { + const target = dirname(packageRequire.resolve(`${sourceName}/package.json`)); + const destination = join(nodeModulesDir, installedName); + + mkdirSync(dirname(destination), { recursive: true }); + symlinkSync(target, destination, "junction"); + } + + return projectDir; +} + +describe("loadTypescript", () => { + afterEach(() => { + for (const projectDir of projectDirs) { + rmSync(projectDir, { recursive: true, force: true }); + } + + projectDirs.clear(); + }); + + it("loads the consumer's TypeScript 5 compiler", () => { + const compiler = loadTypescript(createProject({ typescript: "typescript5" })); + + expect(compiler.version).toBe("5.9.3"); + expect(typeof compiler.transpileModule).toBe("function"); + }); + + it("loads the consumer's TypeScript 6 compiler", () => { + const compiler = loadTypescript(createProject({ typescript: "typescript" })); + + expect(compiler.version).toBe("6.0.3"); + expect(typeof compiler.transpileModule).toBe("function"); + }); + + it("returns an actionable error for TypeScript 7 without the compatibility package", () => { + const projectDir = createProject({ typescript: "typescript7" }); + const requireFromProject = createRequire(join(projectDir, "package.json")); + + expect(typeof requireFromProject("typescript").transpileModule).toBe("undefined"); + expect(() => loadTypescript(projectDir, ["typescript"])).toThrowError( + expect.objectContaining({ + message: expect.stringContaining("npm install --save-dev @typescript/typescript6"), + }) + ); + }); + + it("surfaces errors from an installed compiler package", () => { + const projectDir = createProject({}); + const packageDir = join(projectDir, "node_modules", "typescript"); + + mkdirSync(packageDir); + writeFileSync( + join(packageDir, "package.json"), + JSON.stringify({ name: "typescript", main: "index.cjs" }) + ); + writeFileSync(join(packageDir, "index.cjs"), 'throw new Error("broken compiler");'); + + expect(() => loadTypescript(projectDir)).toThrowError( + `Failed to load "typescript" from ${projectDir}.` + ); + }); + + it("falls back to the TypeScript 6 compatibility package for TypeScript 7", () => { + const compiler = loadTypescript( + createProject({ + typescript: "typescript7", + "@typescript/typescript6": "@typescript/typescript6", + }) + ); + + const output = compiler.transpileModule( + ` + class Dependency {} + function injectable object>(target: T) {} + + @injectable + class Service { + constructor(public dependency: Dependency) {} + } + `, + { + compilerOptions: { + experimentalDecorators: true, + emitDecoratorMetadata: true, + }, + } + ).outputText; + + expect(compiler.version).toBe("6.0.3"); + expect(output).toContain('__metadata("design:paramtypes", [Dependency])'); + }); + + it("supports aliasing TypeScript to the compatibility package", () => { + const compiler = loadTypescript(createProject({ typescript: "@typescript/typescript6" })); + + expect(compiler.version).toBe("6.0.3"); + expect(typeof compiler.transpileModule).toBe("function"); + }); +}); diff --git a/packages/build/src/extensions/internal/loadTypescript.ts b/packages/build/src/extensions/internal/loadTypescript.ts new file mode 100644 index 00000000000..74178594d6c --- /dev/null +++ b/packages/build/src/extensions/internal/loadTypescript.ts @@ -0,0 +1,70 @@ +import { createRequire } from "node:module"; +import { join } from "node:path"; + +export type TypeScriptCompiler = typeof import("typescript"); + +const compilerPackages = ["typescript", "@typescript/typescript6"] as const; + +function hasTranspileModule(value: unknown): value is TypeScriptCompiler { + return ( + typeof value === "object" && + value !== null && + "transpileModule" in value && + typeof value.transpileModule === "function" + ); +} + +function isUnavailablePackage(error: unknown) { + return ( + error instanceof Error && + "code" in error && + (error.code === "MODULE_NOT_FOUND" || error.code === "ERR_PACKAGE_PATH_NOT_EXPORTED") + ); +} + +export function loadTypescript( + projectDir: string, + packageNames: readonly string[] = compilerPackages +): TypeScriptCompiler { + const requireFromProject = createRequire(join(projectDir, "package.json")); + + for (const packageName of packageNames) { + let resolvedPackage: string; + + try { + resolvedPackage = requireFromProject.resolve(packageName); + } catch (error) { + if (isUnavailablePackage(error)) { + continue; + } + + throw error; + } + + let compiler: unknown; + + try { + compiler = requireFromProject(resolvedPackage); + } catch (error) { + throw new Error(`Failed to load "${packageName}" from ${projectDir}.`, { cause: error }); + } + + if (hasTranspileModule(compiler)) { + return compiler; + } + } + + throw new Error( + [ + "The emitDecoratorMetadata() build extension requires the TypeScript JavaScript compiler API,", + "which TypeScript 7 does not expose.", + "", + "Install Microsoft's TypeScript 6 compatibility package alongside TypeScript 7:", + "", + " npm install --save-dev @typescript/typescript6", + "", + "Restart the Trigger.dev dev server after installing the package.", + "See https://trigger.dev/docs/config/extensions/emitDecoratorMetadata#using-with-typescript-7", + ].join("\n") + ); +} diff --git a/packages/build/src/extensions/typescript.ts b/packages/build/src/extensions/typescript.ts index a9e955edecd..a4cf9c11643 100644 --- a/packages/build/src/extensions/typescript.ts +++ b/packages/build/src/extensions/typescript.ts @@ -1,8 +1,6 @@ import { BuildExtension } from "@trigger.dev/core/v3/build"; import { readFile } from "node:fs/promises"; -import typescriptPkg from "typescript"; - -const { transpileModule, ModuleKind } = typescriptPkg; +import { loadTypescript } from "./internal/loadTypescript.js"; const decoratorMatcher = new RegExp(/((?=14.17'} hasBin: true + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + typescript@6.0.3: resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==} engines: {node: '>=14.17'} @@ -31587,6 +31601,8 @@ snapshots: typescript@5.6.1-rc: {} + typescript@5.9.3: {} + typescript@6.0.3: {} typescript@7.0.2: From b74d93165077555e0636e58adfe7eb4e92a8e97c Mon Sep 17 00:00:00 2001 From: Chris Arderne Date: Wed, 5 Aug 2026 09:23:36 +0100 Subject: [PATCH 2/5] fix(build): parse TS7 configs without the native compiler API --- packages/build/src/extensions/typescript.ts | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/packages/build/src/extensions/typescript.ts b/packages/build/src/extensions/typescript.ts index a4cf9c11643..1021d4b94a3 100644 --- a/packages/build/src/extensions/typescript.ts +++ b/packages/build/src/extensions/typescript.ts @@ -1,5 +1,6 @@ import { BuildExtension } from "@trigger.dev/core/v3/build"; import { readFile } from "node:fs/promises"; +import { dirname } from "node:path"; import { loadTypescript } from "./internal/loadTypescript.js"; const decoratorMatcher = new RegExp(/((?(); build.onLoad({ filter: /\.ts$/ }, async (args) => { context.logger.debug("emitDecoratorMetadata onLoad", { args }); - const { tsconfigFile, tsconfig } = await parseNative(args.path, { + const { tsconfigFile, tsconfig } = await parse(args.path, { ignoreNodeModules: true, cache, }); + const { options: compilerOptions } = convertCompilerOptionsFromJson( + tsconfig.compilerOptions ?? {}, + tsconfigFile ? dirname(tsconfigFile) : context.workingDir + ); - context.logger.debug("emitDecoratorMetadata parsed native tsconfig", { + context.logger.debug("emitDecoratorMetadata parsed tsconfig", { tsconfig, tsconfigFile, args, }); - if (tsconfig.compilerOptions?.emitDecoratorMetadata !== true) { + if (compilerOptions.emitDecoratorMetadata !== true) { context.logger.debug("emitDecoratorMetadata skipping", { args, tsconfig, @@ -55,7 +62,7 @@ export function emitDecoratorMetadata(): BuildExtension { const program = transpileModule(ts, { fileName: args.path, compilerOptions: { - ...tsconfig.compilerOptions, + ...compilerOptions, module: ModuleKind.ES2022, }, }); From ace9ba6ac16626d67cb819f105789ed10ba576be Mon Sep 17 00:00:00 2001 From: Chris Arderne Date: Wed, 5 Aug 2026 09:30:59 +0100 Subject: [PATCH 3/5] wording change --- .changeset/typescript-seven-builds.md | 2 +- docs/config/extensions/emitDecoratorMetadata.mdx | 2 +- packages/build/src/extensions/internal/loadTypescript.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.changeset/typescript-seven-builds.md b/.changeset/typescript-seven-builds.md index 960d4a58b83..68406409833 100644 --- a/.changeset/typescript-seven-builds.md +++ b/.changeset/typescript-seven-builds.md @@ -10,4 +10,4 @@ "@trigger.dev/sdk": patch --- -Refresh package builds for TypeScript 7 compatibility while preserving existing runtime entry points. Projects using `emitDecoratorMetadata()` with TypeScript 7 can install Microsoft's `@typescript/typescript6` compatibility package alongside it; the package remains optional, so installing the Trigger.dev CLI does not install an additional compiler. +Refresh package builds for TypeScript 7 compatibility while preserving existing runtime entry points. Projects using `emitDecoratorMetadata()` with TypeScript 7 can install the `@typescript/typescript6` compatibility package alongside it; the package remains optional, so installing the Trigger.dev CLI does not install an additional compiler. diff --git a/docs/config/extensions/emitDecoratorMetadata.mdx b/docs/config/extensions/emitDecoratorMetadata.mdx index 947e2315cec..ef0aa07d9eb 100644 --- a/docs/config/extensions/emitDecoratorMetadata.mdx +++ b/docs/config/extensions/emitDecoratorMetadata.mdx @@ -29,7 +29,7 @@ This is usually required if you are using certain ORMs, like TypeORM, that requi ## Using with TypeScript 7 -TypeScript 7 does not expose the JavaScript compiler API required by this extension. Install Microsoft's TypeScript 6 compatibility package alongside TypeScript 7: +TypeScript 7 does not expose the JavaScript compiler API required by this extension. Install the TypeScript 6 compatibility package alongside TypeScript 7: diff --git a/packages/build/src/extensions/internal/loadTypescript.ts b/packages/build/src/extensions/internal/loadTypescript.ts index 74178594d6c..79c1a45e8b9 100644 --- a/packages/build/src/extensions/internal/loadTypescript.ts +++ b/packages/build/src/extensions/internal/loadTypescript.ts @@ -59,7 +59,7 @@ export function loadTypescript( "The emitDecoratorMetadata() build extension requires the TypeScript JavaScript compiler API,", "which TypeScript 7 does not expose.", "", - "Install Microsoft's TypeScript 6 compatibility package alongside TypeScript 7:", + "Install the TypeScript 6 compatibility package alongside TypeScript 7:", "", " npm install --save-dev @typescript/typescript6", "", From 9b50bd2449bab061dbaf4aeb0740291c6b23b607 Mon Sep 17 00:00:00 2001 From: Chris Arderne Date: Wed, 5 Aug 2026 10:12:37 +0100 Subject: [PATCH 4/5] fix(build): continue after compiler load failures --- .../internal/loadTypescript.test.ts | 34 ++++++++++++++----- .../src/extensions/internal/loadTypescript.ts | 17 +++++++++- 2 files changed, 41 insertions(+), 10 deletions(-) diff --git a/packages/build/src/extensions/internal/loadTypescript.test.ts b/packages/build/src/extensions/internal/loadTypescript.test.ts index 408d4ab7242..f326c1fe062 100644 --- a/packages/build/src/extensions/internal/loadTypescript.test.ts +++ b/packages/build/src/extensions/internal/loadTypescript.test.ts @@ -27,6 +27,17 @@ function createProject(packages: Record) { return projectDir; } +function createBrokenCompiler(projectDir: string, packageName = "typescript") { + const packageDir = join(projectDir, "node_modules", packageName); + + mkdirSync(packageDir, { recursive: true }); + writeFileSync( + join(packageDir, "package.json"), + JSON.stringify({ name: packageName, main: "index.cjs" }) + ); + writeFileSync(join(packageDir, "index.cjs"), 'throw new Error("broken compiler");'); +} + describe("loadTypescript", () => { afterEach(() => { for (const projectDir of projectDirs) { @@ -64,20 +75,25 @@ describe("loadTypescript", () => { it("surfaces errors from an installed compiler package", () => { const projectDir = createProject({}); - const packageDir = join(projectDir, "node_modules", "typescript"); - - mkdirSync(packageDir); - writeFileSync( - join(packageDir, "package.json"), - JSON.stringify({ name: "typescript", main: "index.cjs" }) - ); - writeFileSync(join(packageDir, "index.cjs"), 'throw new Error("broken compiler");'); + createBrokenCompiler(projectDir); - expect(() => loadTypescript(projectDir)).toThrowError( + expect(() => loadTypescript(projectDir, ["typescript"])).toThrowError( `Failed to load "typescript" from ${projectDir}.` ); }); + it("falls back when an earlier compiler package fails to load", () => { + const projectDir = createProject({ + "@typescript/typescript6": "@typescript/typescript6", + }); + createBrokenCompiler(projectDir); + + const compiler = loadTypescript(projectDir); + + expect(compiler.version).toBe("6.0.3"); + expect(typeof compiler.transpileModule).toBe("function"); + }); + it("falls back to the TypeScript 6 compatibility package for TypeScript 7", () => { const compiler = loadTypescript( createProject({ diff --git a/packages/build/src/extensions/internal/loadTypescript.ts b/packages/build/src/extensions/internal/loadTypescript.ts index 79c1a45e8b9..2b187618aca 100644 --- a/packages/build/src/extensions/internal/loadTypescript.ts +++ b/packages/build/src/extensions/internal/loadTypescript.ts @@ -27,6 +27,7 @@ export function loadTypescript( packageNames: readonly string[] = compilerPackages ): TypeScriptCompiler { const requireFromProject = createRequire(join(projectDir, "package.json")); + const loadErrors: Error[] = []; for (const packageName of packageNames) { let resolvedPackage: string; @@ -46,7 +47,10 @@ export function loadTypescript( try { compiler = requireFromProject(resolvedPackage); } catch (error) { - throw new Error(`Failed to load "${packageName}" from ${projectDir}.`, { cause: error }); + loadErrors.push( + new Error(`Failed to load "${packageName}" from ${projectDir}.`, { cause: error }) + ); + continue; } if (hasTranspileModule(compiler)) { @@ -54,6 +58,17 @@ export function loadTypescript( } } + if (loadErrors.length === 1) { + throw loadErrors[0]; + } + + if (loadErrors.length > 1) { + throw new AggregateError( + loadErrors, + `Failed to load a compatible TypeScript compiler from ${projectDir}.` + ); + } + throw new Error( [ "The emitDecoratorMetadata() build extension requires the TypeScript JavaScript compiler API,", From 5a71996c8c7870df1e61871b98b54d725c75cf18 Mon Sep 17 00:00:00 2001 From: Chris Arderne Date: Wed, 5 Aug 2026 16:02:07 +0100 Subject: [PATCH 5/5] test(build): loosen compiler patch version assertions --- .../src/extensions/internal/loadTypescript.test.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/build/src/extensions/internal/loadTypescript.test.ts b/packages/build/src/extensions/internal/loadTypescript.test.ts index f326c1fe062..7846681b4b4 100644 --- a/packages/build/src/extensions/internal/loadTypescript.test.ts +++ b/packages/build/src/extensions/internal/loadTypescript.test.ts @@ -50,14 +50,14 @@ describe("loadTypescript", () => { it("loads the consumer's TypeScript 5 compiler", () => { const compiler = loadTypescript(createProject({ typescript: "typescript5" })); - expect(compiler.version).toBe("5.9.3"); + expect(compiler.version).toMatch(/^5\./); expect(typeof compiler.transpileModule).toBe("function"); }); it("loads the consumer's TypeScript 6 compiler", () => { const compiler = loadTypescript(createProject({ typescript: "typescript" })); - expect(compiler.version).toBe("6.0.3"); + expect(compiler.version).toMatch(/^6\./); expect(typeof compiler.transpileModule).toBe("function"); }); @@ -90,7 +90,7 @@ describe("loadTypescript", () => { const compiler = loadTypescript(projectDir); - expect(compiler.version).toBe("6.0.3"); + expect(compiler.version).toMatch(/^6\./); expect(typeof compiler.transpileModule).toBe("function"); }); @@ -120,14 +120,14 @@ describe("loadTypescript", () => { } ).outputText; - expect(compiler.version).toBe("6.0.3"); + expect(compiler.version).toMatch(/^6\./); expect(output).toContain('__metadata("design:paramtypes", [Dependency])'); }); it("supports aliasing TypeScript to the compatibility package", () => { const compiler = loadTypescript(createProject({ typescript: "@typescript/typescript6" })); - expect(compiler.version).toBe("6.0.3"); + expect(compiler.version).toMatch(/^6\./); expect(typeof compiler.transpileModule).toBe("function"); }); });