From aef97ce9de99f7cb38e9c869bf4e8c6922d45463 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Wed, 30 Sep 2026 01:36:30 -0500 Subject: [PATCH] Respect explicit static library package selections - Compile opted-in runtime packages when external type declarations are installed. - Preserve static dependency checks across package subpaths and scoped imports. --- packages/compiler/src/frontend/pipeline.ts | 3 ++ tests/harness/library-static-types.test.ts | 53 ++++++++++++++++++++++ 2 files changed, 56 insertions(+) create mode 100644 tests/harness/library-static-types.test.ts diff --git a/packages/compiler/src/frontend/pipeline.ts b/packages/compiler/src/frontend/pipeline.ts index cb90bd5b..e7db7d24 100644 --- a/packages/compiler/src/frontend/pipeline.ts +++ b/packages/compiler/src/frontend/pipeline.ts @@ -112,6 +112,9 @@ function detectAutoPackages( // never npm candidates. Auto keeps its original path (the // @types/node answer skips them below), byte-for-byte. if (mode === "lib" && canonicalBuiltinModule(spec) !== null) continue; + // Explicit attempts name the runtime package. Types-first resolution + // may instead identify its @types twin, which has no runtime to admit. + if (judged?.has(packageNameOfSpecifier(spec))) continue; const npm = resolveNpmImport(sf.fileName, spec); if (npm !== null && isNodeTypesPath(npm.typesFile)) continue; if (npm === null) { diff --git a/tests/harness/library-static-types.test.ts b/tests/harness/library-static-types.test.ts new file mode 100644 index 00000000..b9174b28 --- /dev/null +++ b/tests/harness/library-static-types.test.ts @@ -0,0 +1,53 @@ +import { execFileSync } from "node:child_process"; +import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import { join } from "node:path"; +import { expect, test } from "vitest"; +import { compileLibrary } from "@scriptc/compiler"; + +test.each(["sample", "@scope/sample"])("explicit library attempts use %s runtime code with installed declaration twins", async (name) => { + const dir = await mkdtemp("/tmp/scriptc-library-static-types-"); + try { + const pkg = join(dir, "node_modules", name); + const types = join(dir, "node_modules/@types", name.startsWith("@") ? "scope__sample" : name); + await mkdir(pkg, { recursive: true }); + await mkdir(types, { recursive: true }); + await writeFile(join(dir, "package.json"), '{"type":"module"}'); + await writeFile(join(pkg, "package.json"), JSON.stringify({ name, version: "1.0.0", type: "module", exports: { ".": "./index.js", "./value": "./index.js" } })); + await writeFile(join(pkg, "index.js"), "export function calculate(value) { return value * 3 + 1; }\n"); + await writeFile(join(types, "package.json"), JSON.stringify({ name: `@types/${name.startsWith("@") ? "scope__sample" : name}`, version: "1.0.0", types: "index.d.ts" })); + const declaration = "export function calculate(value: number): number;\n"; + await writeFile(join(types, "index.d.ts"), declaration); + await writeFile(join(types, "value.d.ts"), declaration); + await writeFile(join(dir, "entry.ts"), `import { calculate } from '${name}/value';\nexport function result(): number { return calculate(7); }\n`); + const profile = { + profile_format: 1, name: "static-types", entry: "entry.ts", emission: "llvm", npm_static: [name], + abi: { prefix: "st_", init_symbol: "st_init", sink_register_symbol: "st_sink", collect_symbol: "st_collect" }, + exports: [{ export: "result", symbol: "st_result", params: [], returns: "f64" }], + }; + const profilePath = join(dir, "profile.json"); + await writeFile(profilePath, JSON.stringify(profile)); + const result = await compileLibrary({ profilePath, outDir: join(dir, "out") }); + if (!result.ok) throw new Error(JSON.stringify(result.diagnostics)); + if (process.env["SCRIPTC_PORTABLE_ONLY"] !== "1") { + const source = join(dir, "host.c"), binary = join(dir, "host"); + await writeFile(source, '#include \nextern void st_init(void); extern double st_result(void);\nint main(void) { st_init(); printf("%.0f\\n", st_result()); }\n'); + execFileSync("clang", [source, result.archivePath, "-lm", "-o", binary]); + const actual = execFileSync(binary, { encoding: "utf8" }); + const reference = execFileSync(process.execPath, ["--input-type=module", "-e", `import { result } from ${JSON.stringify(join(dir, "entry.ts"))}; console.log(result());`], { encoding: "utf8" }); + expect(actual).toBe(reference); + expect(actual).toBe("22\n"); + } + // Opting in to one runtime package does not admit its untyped dependency. + const dependency = join(dir, "node_modules/unchecked-dependency"); + await mkdir(dependency, { recursive: true }); + await writeFile(join(dependency, "package.json"), '{"name":"unchecked-dependency","version":"1.0.0","type":"module","main":"index.js"}'); + await writeFile(join(dependency, "index.js"), 'export function adjust(value) { return value; }'); + await writeFile(join(pkg, "index.js"), "import { adjust } from 'unchecked-dependency'; export function calculate(value) { return adjust(value) * 3 + 1; }\n"); + const refused = await compileLibrary({ profilePath, outDir: join(dir, "refused") }); + expect(refused.ok).toBe(false); + if (!refused.ok) expect(refused.diagnostics.some((d) => d.code === "SC4020" && d.message.includes("unchecked-dependency"))).toBe(true); + expect(await readFile(join(types, "index.d.ts"), "utf8")).toBe(declaration); + } finally { + await rm(dir, { recursive: true, force: true }); + } +});