Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
- 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
Uh oh!
There was an error while loading. Please reload this page.
Changes from all commits
File filter
Filter by extension
Conversations
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Uh oh!
There was an error while loading. Please reload this page.
Diff view
Diff view
There are no files selected for viewing
| 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).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({}); | ||
| 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).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({ | ||
| 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).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"); | ||
| }); | ||
| }); |
| 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") | ||
| ); | ||
| } |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.