UNPKG

@inlang/paraglide-js

Version:

[![NPM Downloads](https://img.shields.io/npm/dw/%40inlang%2Fparaglide-js?logo=npm&logoColor=red&label=npm%20downloads)](https://www.npmjs.com/package/@inlang/paraglide-js) [![GitHub Issues](https://img.shields.io/github/issues-closed/opral/paraglide-js?lo

181 lines (180 loc) 7.71 kB
import { beforeEach, expect, test, vi } from "vitest"; import { createProject as typescriptProject, ts } from "@ts-morph/bootstrap"; import { insertBundleNested, loadProjectInMemory, newProject, } from "@inlang/sdk"; import { createRequire } from "node:module"; import path from "node:path"; import { compileProject } from "./compile-project.js"; // TypeScript 7 (the Go-based compiler, npm `latest` since 7.0.2) no longer // ships the in-process compiler API. Its main entry point only exports // `version` and `versionMajorMinor` — no `createProgram`, no `ScriptTarget`, // no `ModuleKind`. Emitting declarations must go through the `tsc` CLI // instead. https://github.com/opral/paraglide-js/issues/711 // // The repo's own `typescript` dev dependency stays on 5.x for the existing // tests; this file swaps in the aliased TypeScript 7 package to simulate a // user who upgraded. // // Note: vitest wraps factory mocks in a proxy that throws on missing exports. // The real TypeScript 7 module returns `undefined` for them instead, so the // classic compiler API surface is stubbed out explicitly to mirror that. vi.mock("typescript", async () => { const actual = await vi.importActual("typescript-go"); return { ...actual, createProgram: undefined, createCompilerHost: undefined, createSourceFile: undefined, ScriptTarget: undefined, ScriptKind: undefined, ModuleKind: undefined, ModuleResolutionKind: undefined, }; }); // In production, the `typescript` module and the `tsc` CLI resolve to the // same package. In this test the module is mocked with the TypeScript 7 // alias, so the CLI resolution must point at that alias too — otherwise it // would find the repo's TypeScript 5 dev dependency. Mutable so the error // path can be tested with a broken resolution. const tscResolution = vi.hoisted(() => ({ tscJsPath: "" })); vi.mock("./resolve-tsc-js-path.js", () => ({ resolveTscJsPath: () => tscResolution.tscJsPath, })); const typescriptGoTscJsPath = path.join(path.dirname(createRequire(import.meta.url).resolve("typescript-go/package.json")), "lib", "tsc.js"); beforeEach(() => { tscResolution.tscJsPath = typescriptGoTscJsPath; }); async function newProjectWithMessages() { const project = await loadProjectInMemory({ blob: await newProject({ settings: { locales: ["en"], baseLocale: "en", }, }), }); // a message id that is not a valid JS identifier -> quoted export alias const quotedAliasBundle = { id: "greeting.hello", declarations: [], messages: [ { id: "greeting.hello_en", bundleId: "greeting.hello", locale: "en", selectors: [], variants: [ { id: "greeting.hello_en_variant", messageId: "greeting.hello", matches: [], pattern: [{ type: "text", value: "Hello" }], }, ], }, ], }; // a message with an input so the declarations carry a typed parameter const parameterizedBundle = { id: "balance", declarations: [{ type: "input-variable", name: "amount" }], messages: [ { id: "balance_en", bundleId: "balance", locale: "en", selectors: [], variants: [ { id: "balance_en_variant", messageId: "balance", matches: [], pattern: [ { type: "text", value: "You have " }, { type: "expression", arg: { type: "variable-reference", name: "amount" }, }, { type: "text", value: " coins." }, ], }, ], }, ], }; await insertBundleNested(project.db, quotedAliasBundle); await insertBundleNested(project.db, parameterizedBundle); return project; } test("emitTsDeclarations works with TypeScript 7 (tsgo)", async () => { const project = await newProjectWithMessages(); for (const outputStructure of [ "message-modules", "locale-modules", ]) { const output = await compileProject({ project, compilerOptions: { emitTsDeclarations: true, outputStructure, // path-traversing keys (incl. Windows separators) must be // silently skipped like on the compiler-API path — neither // escaping the temp dir nor failing the completeness check additionalFiles: { "../escape.js": "export const escape = 1;", "foo\\..\\..\\escape.js": "export const escape = 1;", }, }, }); expect(output["../escape.d.ts"]).toBeUndefined(); expect(output["foo\\..\\..\\escape.d.ts"]).toBeUndefined(); expect(output).toHaveProperty("runtime.d.ts"); expect(output).toHaveProperty("messages.d.ts"); expect(output).toHaveProperty("registry.d.ts"); expect(output["runtime.d.ts"]).toContain("getLocale"); // TypeScript 5's declaration emitter drops the quotes around export // aliases that are not valid identifiers (worked around by a custom // transformer on the compiler-API path); tsgo emits them correctly. const declarationFile = outputStructure === "message-modules" ? "messages/greeting_hello.d.ts" : "messages/_index.d.ts"; expect(output[declarationFile]).toContain(`as "greeting.hello"`); // the emitted declarations must typecheck for a consumer // (@ts-morph/bootstrap bundles its own compiler and is unaffected // by the TypeScript 7 mock above) const tsProject = await typescriptProject({ useInMemoryFileSystem: true, compilerOptions: { module: ts.ModuleKind.Node16, moduleResolution: ts.ModuleResolutionKind.Node16, strict: true, }, }); for (const [fileName, code] of Object.entries(output)) { if (fileName.endsWith(".d.ts")) { tsProject.createSourceFile(fileName, code); } } tsProject.createSourceFile("test.ts", ` import { "greeting.hello" as greetingHello, balance } from "./messages.js"; greetingHello() satisfies string; balance({ amount: 5 }) satisfies string; `); const program = tsProject.createProgram(); const diagnostics = ts.getPreEmitDiagnostics(program); for (const diagnostic of diagnostics) { console.error(diagnostic.messageText, diagnostic.file?.fileName); } expect(diagnostics.length).toEqual(0); } }); test("emitTsDeclarations throws a descriptive error when the tsc CLI produces no declarations", async () => { tscResolution.tscJsPath = path.join(path.dirname(typescriptGoTscJsPath), "does-not-exist.js"); const project = await newProjectWithMessages(); await expect(compileProject({ project, compilerOptions: { emitTsDeclarations: true, }, })).rejects.toThrow(/did not produce a declaration file for every compiled/); }); //# sourceMappingURL=emit-ts-declarations.test.js.map