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.
This commit is contained in:
Chris Tate
2026-09-30 01:36:30 -05:00
parent c7b11a9242
commit aef97ce9de
2 changed files with 56 additions and 0 deletions
@@ -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) {
@@ -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 <stdio.h>\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 });
}
});