-
-
Notifications
You must be signed in to change notification settings - Fork 1.4k
fix(build): support decorator metadata with TypeScript 7 #4505
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
carderne
merged 5 commits into
main
from
feature/tri-12948-support-emitdecoratormetadata-with-typescript-7
Aug 5, 2026
Merged
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
1f5afdc
fix(build): support decorator metadata with TypeScript 7
carderne b74d931
fix(build): parse TS7 configs without the native compiler API
carderne ace9ba6
wording change
carderne 9b50bd2
fix(build): continue after compiler load failures
carderne 5a71996
test(build): loosen compiler patch version assertions
carderne File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
133 changes: 133 additions & 0 deletions
133
packages/build/src/extensions/internal/loadTypescript.test.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,133 @@ | ||
| 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<string>(); | ||
|
|
||
| function createProject(packages: Record<string, string>) { | ||
| 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; | ||
| } | ||
|
|
||
| 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) { | ||
| 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).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).toMatch(/^6\./); | ||
| 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({}); | ||
| createBrokenCompiler(projectDir); | ||
|
|
||
| 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).toMatch(/^6\./); | ||
| expect(typeof compiler.transpileModule).toBe("function"); | ||
| }); | ||
|
|
||
| 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<T extends new (...args: any[]) => object>(target: T) {} | ||
|
|
||
| @injectable | ||
| class Service { | ||
| constructor(public dependency: Dependency) {} | ||
| } | ||
| `, | ||
| { | ||
| compilerOptions: { | ||
| experimentalDecorators: true, | ||
| emitDecoratorMetadata: true, | ||
| }, | ||
| } | ||
| ).outputText; | ||
|
|
||
| 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).toMatch(/^6\./); | ||
| expect(typeof compiler.transpileModule).toBe("function"); | ||
| }); | ||
| }); | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,85 @@ | ||
| 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")); | ||
| const loadErrors: Error[] = []; | ||
|
|
||
| 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) { | ||
| loadErrors.push( | ||
| new Error(`Failed to load "${packageName}" from ${projectDir}.`, { cause: error }) | ||
| ); | ||
| continue; | ||
| } | ||
|
|
||
| if (hasTranspileModule(compiler)) { | ||
| return compiler; | ||
| } | ||
| } | ||
|
|
||
| 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,", | ||
| "which TypeScript 7 does not expose.", | ||
| "", | ||
| "Install the 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") | ||
| ); | ||
|
carderne marked this conversation as resolved.
|
||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.