Compile minified npm entries and untyped regex arguments

Refs #1777

npm packages are no longer refused up front for minified JavaScript. A
bundle whose interop helpers lost their names is still refused by the
bundle recognizers, and a package whose attempt fails is still refused on
its own.

A `var` read in a later declarator of the statement that stores it
(`var a = {...}, b = {...a}`) runs after the store. A read in a function
body is now located at the body's own statement instead of the body, so
JavaScript `var` bindings read after their store in a function keep their
type instead of gaining undefined.

`replace` and `replaceAll` with an untyped search value and a callback
call the regex callback replacement for a RegExp and the string-search
helper otherwise. That helper now accepts untyped callback parameters,
passes undefined to parameters after the subject, and converts number,
boolean and untyped results with ToString.

In JavaScript, `||` and `&&` over primitives of different types whose
result tsc types `any` evaluate in the checked domain.
This commit is contained in:
Chris Tate
2026-10-23 05:55:39 -05:00
parent 8d17fe4e57
commit 967f2ec85d
22 changed files with 340 additions and 69 deletions
+1 -1
View File
@@ -53,7 +53,7 @@ Type-only imports follow Node.js's type stripping. `import type` and `export typ
scriptc compiles a package automatically when it:
- ships its own type declarations, or is typed by its DefinitelyTyped `@types/` package, or is a dependency of another package that has no declarations at all, and
- ships readable JavaScript: unminified, or minified by a bundler whose helpers keep their names.
- ships JavaScript that isn't a bundle whose bundler-interop helpers, such as `__commonJS`, have lost their names. Minified JavaScript compiles like unminified JavaScript.
The declarations check your program; the package's JavaScript compiles with the types scriptc infers from it. A program that doesn't typecheck against the inferred types, for example because an `@types/` package describes another release, fails the build at that use.
+3 -3
View File
@@ -77,9 +77,9 @@ Options:
--npm-static <pkg[,pkg…]|auto>
compile the named npm packages' shipped JS statically as
program modules (repeatable; "auto" opts in every eligible
direct import: own .d.ts, unminified JS, no build-transform
markers; "pkg..." also names every package pkg depends
on). Packages that fail preflight are refused with a
direct import: own .d.ts, no build-transform markers;
"pkg..." also names every package pkg depends on).
Packages that fail preflight are refused with a
diagnostic. Eligible dependencies compile automatically.
--provenance-sources
EXPERIMENTAL: compile npm dependencies from their
@@ -2,6 +2,7 @@ import { keyedHelperName } from "../stable-names.js";
import * as ts from "../../ts7/adapter.js";
import {
BOOL,
DYN,
F64,
REGEX,
STRING,
@@ -14,7 +15,7 @@ import {
type IrType,
type SrcLoc,
} from "../../../ir/ir.js";
import { numLit, strLit, varRef } from "../../../ir/build.js";
import { dynUndefinedExpr, numLit, strLit, varRef } from "../../../ir/build.js";
import { InternalCompilerError } from "../../../errors.js";
import { locOf } from "../../program.js";
import type { Lowerer } from "../lowerer.js";
@@ -65,14 +66,14 @@ export function lowerStringReplacement(
const search = argument(args[0], false);
const replacement = argument(args[1], true);
if (search.type.kind === "dyn" || replacement.type.kind === "dyn") {
if (replacement.type.kind === "func") {
lowerer.noLowering(`string .${method} with checked search values and callbacks`, call);
}
// Evaluate all inputs once before choosing the search protocol. Regexes
// retain their native pattern; plain values use the string-search helper.
// A typed callback receives the regex match's arguments or the plain
// search's (match, offset, subject).
const values = [receiver, search, replacement];
const locals = values.map((value) => lowerer.declareHiddenLocal("%replaceArg", value.type));
const refs = locals.map((local) => varRef(local.id, local.type, loc));
const callback = replacement.type.kind === "func";
const checkedReplacement =
replacement.type.kind === "dyn"
? lowerer.coerceInto(args[1] ?? call, refs[2]!, STRING)
@@ -91,14 +92,27 @@ export function lowerStringReplacement(
? {
kind: "ternary",
cond: { kind: "libCall", fn: "dyn.nativeRegexIs", args: [refs[1]!], type: BOOL, loc },
then: {
kind: "regexIntrinsic",
method,
receiver: refs[0]!,
args: [lowerer.coerceInto(args[0] ?? call, refs[1]!, REGEX), checkedReplacement],
type: STRING,
loc,
},
then: callback
? {
kind: "libCall",
fn: "regex.replaceCallback",
args: [
refs[0]!,
lowerer.coerceInto(args[0] ?? call, refs[1]!, REGEX),
lowerer.coerceToExpected(refs[2]!, DYN),
{ kind: "boolLit", value: method === "replaceAll", type: BOOL, loc },
],
type: STRING,
loc,
}
: {
kind: "regexIntrinsic",
method,
receiver: refs[0]!,
args: [lowerer.coerceInto(args[0] ?? call, refs[1]!, REGEX), checkedReplacement],
type: STRING,
loc,
},
else_: plain,
type: STRING,
loc,
@@ -148,11 +162,17 @@ function lowerStringReplacementValues(
if (callback.kind !== "func") {
throw new InternalCompilerError(`string .${method} replacement of type ${callback.kind}`);
}
// A checked parameter receives the boxed argument (undefined after the
// subject), and a checked, number or boolean result converts with
// ToString.
const full = [STRING, F64, STRING];
const ret = callback.ret.kind;
if (
callback.ret.kind !== "string" ||
callback.params.length > full.length ||
!callback.params.every((type, index) => typeEquals(type, full[index]!))
(ret !== "string" && ret !== "dyn" && ret !== "f64" && ret !== "bool") ||
!callback.params.every(
(type, index) =>
type.kind === "dyn" || (index < full.length && typeEquals(type, full[index]!)),
)
) {
lowerer.noLowering(
`string .${method} callback signature`,
@@ -175,13 +195,21 @@ function lowerStringReplacementValues(
const chunksType = arrayOf(STRING);
const chunks = varRef("chunks.0", chunksType, loc);
const builder = stringBuilder(loc, chunks);
const value: IrExpr = {
const result: IrExpr = {
kind: "callValue",
callee: replace,
args: [needle, match, subject].slice(0, callback.params.length),
type: STRING,
args: callback.params.map((type, index) => {
const arg = [needle, match, subject][index];
if (arg === undefined) return dynUndefinedExpr(loc);
return type.kind === "dyn" ? lowerer.coerceToExpected(arg, DYN) : arg;
}),
type: callback.ret,
loc,
};
const value: IrExpr =
ret === "dyn"
? { kind: "libCall", fn: "dyn.toStringCoerce", args: [result], type: STRING, loc }
: lowerer.ensureString(result, call);
const stop: IrStmt = { kind: "break", loc };
const body: IrStmt[] = [
builder.declare("chunks.0", { kind: "arrayLit", elems: [], type: chunksType, loc }),
@@ -2841,6 +2841,24 @@ function lowerLogicalPair(lowerer: Lowerer, expr: ts.BinaryExpression, left: IrE
loc,
};
}
// A JavaScript default that tsc types `any` (`e.title || null` in an
// instance of a function with untyped parameters, where `e.title` is a
// string): the checked domain, as for union operands above.
if (
target === null &&
isJsSourceFile(ts.getSourceFileOfNode(expr)) &&
lowerer.dynConvertible(left.type) &&
lowerer.dynConvertible(right.type)
) {
return {
kind: "logical",
op: op === ts.SyntaxKind.AmpersandAmpersandToken ? "&&" : "||",
left: lowerer.coerceInto(expr.left, left, DYN),
right: lowerer.coerceInto(expr.right, right, DYN),
type: DYN,
loc,
};
}
lowerer.unsupported("SC1042", expr);
}
return {
@@ -194,6 +194,15 @@ function referencesBySymbol(
return out;
}
/** Where a binding is first stored unconditionally: the scope statement's
* index, and for a declarator of a `var` statement or a `for` initializer,
* the list and the declarator's end. */
interface Settle {
index: number;
list: ts.VariableDeclarationList | null;
end: number;
}
class ScopeAnalysis {
private readonly js: boolean;
/** Whether a function declaration's references all settle, by scope
@@ -214,8 +223,10 @@ class ScopeAnalysis {
}
/** The index of the scope statement that first stores `symbol`
* unconditionally, or Infinity. */
settleIndex(scope: Scope, symbol: ts.Symbol): number {
* unconditionally (Infinity when none does), and, when that statement
* declares it in a `var` statement or a `for` initializer, the end of the
* storing declarator: later declarators of the list run after it. */
settleIndex(scope: Scope, symbol: ts.Symbol): Settle {
const statements = statementsOf(scope);
const checker = this.lowerer.checker;
for (let i = 0; i < statements.length; i++) {
@@ -234,7 +245,7 @@ class ScopeAnalysis {
(d.initializer !== undefined || loop) &&
boundIdentifiersOf(d.name).some((n) => checker.getSymbolAtLocation(n) === symbol)
)
return i;
return { index: i, list: loop ? null : list, end: d.end };
}
} else if (
ts.isExpressionStatement(s) &&
@@ -243,20 +254,21 @@ class ScopeAnalysis {
ts.isIdentifier(s.expression.left) &&
checker.getSymbolAtLocation(s.expression.left) === symbol
) {
return i;
return { index: i, list: null, end: -1 };
}
}
return Infinity;
return { index: Infinity, list: null, end: -1 };
}
/** Whether `ref` (inside `scope`, or in another module) runs only after
* the statement at index `settle`. `read`: `ref` reads the binding
* itself, so tsc vouches for one in the scope's own code. */
settles(scope: Scope, settle: number, ref: ts.Identifier, read = true): boolean {
* the store `settle` describes. `read`: `ref` reads the binding itself,
* so tsc vouches for one in the scope's own code. */
settles(scope: Scope, settle: Settle, ref: ts.Identifier, read = true): boolean {
if (ts.getSourceFileOfNode(ref) !== this.sf) return true;
const statements = statementsOf(scope);
let deferred: ts.Node | null = null;
let top: ts.Node | null = null;
const container: ts.Node | undefined = ts.isSourceFile(scope) ? scope : scope.body;
for (let n: ts.Node = ref; n.parent !== undefined && n !== scope; n = n.parent) {
if (
ts.isFunctionLike(n) ||
@@ -265,18 +277,25 @@ class ScopeAnalysis {
ts.isClassStaticBlockDeclaration(n)
)
deferred = n;
if (n.parent === scope || (!ts.isSourceFile(scope) && n.parent === scope.body)) top = n;
// The scope's own statement, not the function body that holds it.
if (n.parent === container) top = n;
}
if (top === null) return false;
if (deferred === null && read && !this.js) return true;
if (deferred !== null && ts.isFunctionDeclaration(deferred) && deferred.name !== undefined)
return this.functionSettles(scope, settle, deferred);
const index = statements.indexOf(top as ts.Statement);
return index > settle || (index === settle && settlesWithin(top, ref));
return (
index > settle.index ||
(index === settle.index &&
(settlesWithin(top, ref) ||
// A later declarator of the storing list (`var a = {}, b = { ...a }`).
(settle.list !== null && isInside(ref, settle.list) && ref.pos >= settle.end)))
);
}
private functionSettles(scope: Scope, settle: number, fn: ts.FunctionDeclaration): boolean {
const key = `${fn.pos}:${settle}`;
private functionSettles(scope: Scope, settle: Settle, fn: ts.FunctionDeclaration): boolean {
const key = `${fn.pos}:${settle.index}:${settle.end}`;
const known = this.functionMemo.get(key);
if (known !== undefined) return known;
this.functionMemo.set(key, true);
+5 -21
View File
@@ -614,27 +614,13 @@ export function clearProgramBuiltinAliasCache(): void {
* but the bundler-emitted CJS export rewrite (npm-static-rewrite.ts)
* types the getter-table shapes and the offender attribution degrades a
* package whose surface still breaks its consumers — so those bundles are
* worth ATTEMPTING: eligible when the .d.ts and unminified criteria hold,
* gracefully per-package-degraded when the attempt fails. */
* worth ATTEMPTING: eligible when the .d.ts criteria hold, gracefully
* per-package-degraded when the attempt fails. Minified JS is attempted
* too: a whitespace-minified dist is ordinary JavaScript, and an
* identifier-minified bundle whose interop helpers lost their names is
* refused by the bundle recognizers, which name the reason. */
const TRANSFORM_MARKERS = ["__webpack_require__"];
/** Unminified-JS heuristic over an entry source: at least two lines and
* an average line length under 200 characters (minified dists are one
* enormous line; readable code averages well under 100). */
function looksUnminified(source: string): boolean {
const lines = source.split("\n");
if (lines.length < 2) return false;
return source.length / lines.length <= 200;
}
/** A whitespace/syntax-minified bundler dist whose interop helpers keep
* their names (`var __toESM=…,__toCommonJS=mod=>…`): the recognizers
* verify those shapes and refuse the package precisely when they cannot,
* so the minification heuristic need not refuse it up front. */
function namedBundlerHelpers(source: string): boolean {
return /\b__(?:toCommonJS|commonJS|toESM)\d*\s*=\s*(?:\(|[A-Za-z_$][\w$]*\s*=>)/.test(source);
}
function hasTransformMarkers(source: string): boolean {
return TRANSFORM_MARKERS.some((m) => source.includes(m));
}
@@ -676,8 +662,6 @@ export function npmStaticIneligibleReason(
if (jsEntry === null) return "no runtime JS entry resolves";
const source = trackedReadFile(jsEntry);
if (source === null) return `its runtime entry ${jsEntry} cannot be read`;
if (!looksUnminified(source) && !namedBundlerHelpers(source))
return "its shipped JS looks minified";
if (hasTransformMarkers(source)) {
return "its shipped JS carries build-transform markers (bundled/transpiled dist)";
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/7543-var-list-declarator-order.js"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/7545-string-regex-checked-arguments.js"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/fixtures/npm/cases/7546-minified-esm-rules/main.ts"
],
"diags": []
}
@@ -0,0 +1,66 @@
// A `var` read in a later declarator of the statement that stores it runs
// after the store; a read in the same or an earlier declarator sees undefined.
var base = { a: 1, b: "x" },
more = { ...base, c: 2 },
keys = Object.keys({ ...more }).join(","),
first = base.a + 1;
console.log(JSON.stringify(more), keys, first);
function describeBase() {
return base.b + more.c;
}
var rules = { space: /\s+/, word: /\w+/ },
merged = { ...rules, digit: /\d/ },
described = describeBase();
console.log(Object.keys(merged).join(","), merged.word.test("ab"), described);
var table = { one: 1, two: 2 },
lookup = function (key) {
return table[key];
},
arrow = (key) => ({ ...table })[key];
console.log(lookup("one"), arrow("two"));
var readLater = function () {
return later;
},
early = readLater(),
later = { k: 1 },
after = readLater().k;
console.log(early, after);
var self = self ? 1 : 2;
console.log(self);
for (var i = 0, j = i + 10; i < 2; i++) console.log(i, j);
function nested() {
var inner = [1, 2, 3],
copy = [...inner, 4],
total = copy.reduce((sum, n) => sum + n, 0);
return { copy, total };
}
console.log(JSON.stringify(nested()));
function earlyInFunction() {
show("before");
var point = { n: 1 },
next = { m: point.n + 1 };
show("after");
function show(when) {
console.log(when, typeof point, point === undefined ? "none" : point.n, next === undefined ? "none" : next.m);
}
var check = () => console.log("check", items === undefined);
check();
var items = [1];
check();
}
earlyInFunction();
function loopLists() {
for (var k = 0, sum = k; k < 3; k++) sum += k;
var doubled = sum * 2,
again = doubled + 1;
return [sum, doubled, again];
}
console.log(JSON.stringify(loopLists()));
@@ -0,0 +1,89 @@
// String search, match and replace with untyped pattern values: a RegExp
// is used as is, and any other value is converted as Node converts it.
const rules = JSON.parse('{"other":{}}');
rules.other.space = /\s+/g;
rules.other.word = /o+/;
rules.other.text = "l";
rules.other.dot = ".";
rules.other.none = undefined;
rules.other.nul = null;
rules.other.one = /(l)(o)/;
rules.other.many = /l+/g;
const t = " hello world";
console.log(
t.search(rules.other.word),
t.search(rules.other.text),
t.search(rules.other.none),
t.search(rules.other.nul),
t.search(rules.other.space),
"a.b".search(rules.other.dot),
);
console.log(rules.other.space.lastIndex);
let conversions = 0;
rules.other.obj = {
toString() {
conversions++;
return "w";
},
};
console.log(t.search(rules.other.obj), t.match(rules.other.obj)[0], conversions);
const all = t.match(rules.other.many);
console.log(JSON.stringify(all), rules.other.many.lastIndex);
const pair = t.match(rules.other.one);
console.log(pair[0], pair[1], pair[2], pair.index, pair.length, pair.input === t);
const literal = t.match(rules.other.dot);
console.log(literal[0], literal.index, literal.length);
console.log(JSON.stringify(t.match(rules.other.none)), t.match(rules.other.zz));
rules.other.zz = "zz";
console.log(t.match(rules.other.zz));
function first(text, re) {
const m = text.match(re);
return m ? m[0] : "none";
}
console.log(first("abc", rules.other.dot), first("", rules.other.dot));
console.log(t.replace(rules.other.space, (m) => "[" + m.length + "]"));
console.log(t.replace(rules.other.word, (m, offset) => m.toUpperCase() + offset));
console.log(t.replace(rules.other.text, (m, offset, subject) => "<" + m + offset + subject.length + ">"));
console.log(t.replaceAll(rules.other.text, (m) => m.toUpperCase()));
console.log(t.replaceAll(rules.other.space, () => "_"));
console.log(t.replace(rules.other.one, (m, p1, p2, offset) => p1 + "|" + p2 + "|" + offset));
console.log(t.replace(rules.other.text, (m, offset, subject, extra) => typeof extra));
console.log(t.replace(rules.other.text, (m) => m.length));
try {
t.replaceAll(rules.other.word, (m) => m);
} catch (e) {
console.log(e.constructor.name, e.message);
}
function tabs(text, width) {
let column = 0;
let out = "";
for (const ch of text) {
if (ch === "\t") {
const pad = 4 - (column % 4);
out += " ".repeat(pad);
column += pad;
} else {
out += ch;
column++;
}
}
return out + width;
}
class Lexer {
constructor(table) {
this.rules = table;
}
indent(line, width) {
return line.replace(this.rules.leading, (w) => tabs(w, width));
}
firstText(line) {
return line.search(this.rules.nonSpace);
}
}
const lexer = new Lexer({ leading: /^( {1,4}| {0,3}\t)/, nonSpace: /[^ ]/ });
console.log(JSON.stringify(lexer.indent("\tabc", 2)), lexer.firstText(" x"));
@@ -0,0 +1,22 @@
// A one-line minified ES module. Its module-level var statement spreads
// earlier declarators into later ones, its tokenizer reads regexes through
// an untyped rules table (search, match and replace with callbacks), and
// one function receives both typed and untyped link objects.
import { render } from "gtminimark";
const doc = [
"# Title \\*one\\*",
"## Sub",
'[a](/x "T\\*")',
"[b](/y)",
'[r]: /ref "Ref"',
"[n]: /none",
"[R]",
"[n]",
"[z]",
"\tcode *em* line",
" three spaces *x* and *y*",
"plain words here",
].join("\n");
console.log(render(doc));
console.log(render(doc, { pedantic: true }));
+1
View File
@@ -0,0 +1 @@
export declare function render(src: string, options?: { pedantic?: boolean }): string;
+1
View File
@@ -0,0 +1 @@
var m = { heading: /^(#{1,6}) +(.*)$/, link: /^\[([^\]]*)\]\(([^ )]*)(?: "([^"]*)")?\)/, ref: /^\[([^\]]*)\]$/, lead: /^( {1,4}| {0,3}\t)/, nonSpace: /[^ ]/, punct: /\\([\\*_\[\]])/g, word: /\w+/g, def: /^\[([^\]]*)\]: (\S+)(?: "([^"]*)")?$/ },B = { ...m, emph: /\*([^*]+)\*/g },P = { ...B, word: /[a-z]+/g },K = { normal: { other: m, inline: B }, pedantic: { other: m, inline: P, strict: !0 } };function X(a, e) { let t = 0, n = ""; for (let i of a) if (i === "\t") { let r = 4 - t % 4; n += " ".repeat(r), t += r } else n += i, t++; return n + e; }function Se(a, e, t, n) { let r = e.href, o = e.title || null; n.links++; return { type: "link", raw: t, href: r, title: o, text: a[1] }; }class T {constructor() { this.state = { links: 0 }; }heading(s) { let c = s.match(this.rules.other.heading); return c ? "h" + c[1].length + ":" + c[2].replace(this.rules.other.punct, "$1") : null; }indent(s, w) { return s.replace(this.rules.other.lead, W => X(W, w)); }first(s) { return s.search(this.rules.other.nonSpace); }emph(s) { return s.replace(this.rules.inline.emph, (W, x) => "<em>" + x + "</em>"); }words(s) { let c = s.match(this.rules.inline.word); return c ? c.length : 0; }link(s) { let c = this.rules.other.link.exec(s); if (!c) return null; let r = c[2], o = ""; o = c[3] ? c[3] : ""; return Se(c, { href: r && r.replace(this.rules.other.punct, "$1"), title: o && o.replace(this.rules.other.punct, "$1") }, c[0], this.state); }ref(s, l) { let c = this.rules.other.ref.exec(s); if (!c) return null; let o = l[c[1].toLowerCase()]; return o ? Se(c, o, c[0], this.state) : { type: "text", raw: c[0] }; }}class L {constructor(e) { this.options = e || {}; this.links = {}; this.tokenizer = new T(); this.tokenizer.rules = this.options.pedantic ? K.pedantic : K.normal; }define(s) { let c = s.match(this.tokenizer.rules.other.def); if (c) this.links[c[1].toLowerCase()] = { href: c[2], title: c[3] || "" }; return !!c; }line(s) {let t = this.tokenizer, h = t.heading(s);if (h) return h;let k = t.link(s);if (k) return JSON.stringify(k);let f = t.ref(s, this.links);if (f) return JSON.stringify(f);return t.first(s) + "|" + JSON.stringify(t.indent(s, t.first(s))) + "|" + t.emph(s) + "|" + t.words(s);}}export function render(src, options) {let l = new L(options), out = [];for (let s of src.split("\n")) if (!l.define(s)) out.push(l.line(s));return out.join("\n") + "\nlinks " + l.tokenizer.state.links;}
+8
View File
@@ -0,0 +1,8 @@
{
"name": "gtminimark",
"version": "1.0.0",
"description": "A one-line minified ES module: a module-level var statement whose later declarators spread earlier ones, regexes read through an untyped rules table, and a function called with typed and untyped link objects",
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts"
}
@@ -0,0 +1 @@
{ "plain": 4300, "san": 5900 }
@@ -0,0 +1 @@
{ "plain": 4900, "san": 4600 }
+16 -10
View File
@@ -50,8 +50,8 @@
* no-teachings default is pinned by K5/K7 over
* the teachings-free traps profile
* K13 npm-posture bare npm specifiers are static-or-refuse: an
* eligible package (own .d.ts, unminified JS,
* no transform markers) compiles statically
* eligible package (own .d.ts, no transform
* markers) compiles statically
* into the library graph, its own npm deps
* judged by the same bar; every ineligible one
* refuses SC4020 naming the failed bar — never
@@ -746,11 +746,11 @@ describe("K9: library-mode refusals", () => {
* synthesized per test like K9's. Emission never engages — SC4020 is a
* frontend verdict — so one lane suffices. */
let npmRefusalCounter = 0;
async function npmRefusal(
async function compileNpmFixture(
entryFile: string,
exports: unknown[],
profilePatch: Record<string, unknown> = {},
): Promise<{ code: string; message: string; hint?: string; note?: string; file: string }[]> {
): Promise<Awaited<ReturnType<typeof compileLibrary>>> {
const outDir = join(cacheDir, `npm-refusal-${npmRefusalCounter++}`);
mkdirSync(outDir, { recursive: true });
const profile = {
@@ -770,7 +770,15 @@ async function npmRefusal(
};
const profilePath = join(outDir, "profile.json");
writeFileSync(profilePath, JSON.stringify(profile));
const result = await compileLibrary({ profilePath, outDir });
return compileLibrary({ profilePath, outDir });
}
async function npmRefusal(
entryFile: string,
exports: unknown[],
profilePatch: Record<string, unknown> = {},
): Promise<{ code: string; message: string; hint?: string; note?: string; file: string }[]> {
const result = await compileNpmFixture(entryFile, exports, profilePatch);
expect(result.ok).toBe(false);
if (result.ok) throw new Error("unreachable");
return result.diagnostics.map((d) => ({
@@ -795,13 +803,11 @@ describe("K13: library-mode npm refusals (SC4020, static-or-refuse)", () => {
expect(diags[0]!.file.endsWith("lib-markers.ts")).toBe(true);
});
test("minified shipped JS refuses", async () => {
const diags = await npmRefusal("lib-minified.ts", [
test("minified shipped JS compiles", async () => {
const result = await compileNpmFixture("lib-minified.ts", [
{ export: "f", symbol: "kx_f", params: ["f64", "f64"], returns: "f64" },
]);
expect(diags.map((d) => d.code)).toEqual(["SC4020"]);
expect(diags[0]!.message).toContain("'slimmed'");
expect(diags[0]!.message).toContain("looks minified");
expect(result.ok ? [] : result.diagnostics.map((d) => d.message)).toEqual([]);
});
test("no own .d.ts refuses by name — never the generic import fence", async () => {
+9
View File
@@ -1490,6 +1490,15 @@ describe(`npm-static pilots${sanitize ? " (sanitized)" : ""}`, () => {
]);
}, 180_000);
// 7546: a one-line minified ES module: a var statement whose later
// declarators spread earlier ones, regexes read through an untyped rules
// table, and a function instantiated with typed and untyped arguments.
test("the minified module 7546-minified-esm-rules compiles statically and matches Node", async () => {
await expectNodeParity(join(fixturesRoot, "npm/cases/7546-minified-esm-rules/main.ts"), [
"gtminimark",
]);
}, 180_000);
// A whitespace/syntax-minified bundle whose helpers keep their names is
// judged by the recognizers, not refused up front as minified.
test("--npm-static=auto opts a minified bundle with named helpers in", () => {
@@ -1,5 +1,5 @@
// SC4020 fixture: slimmed ships a one-long-line dist — minified JS fails
// the eligibility bar.
// slimmed ships a one-long-line dist: minified JS compiles like any other
// eligible package.
import { pick } from "slimmed";
export function f(a: number, b: number): number {
+1 -1
View File
@@ -1,5 +1,5 @@
// Library-mode npm posture fixture (static-or-refuse): a bare specifier
// naming an ELIGIBLE package (own .d.ts, unminified shipped JS, no
// naming an ELIGIBLE package (own .d.ts, no
// build-transform markers) compiles statically as part of the library
// graph — no flag, no island — and the package's own top-level require
// ("mathdep") joins through the same bar. The node:path import alongside
+1 -1
View File
@@ -1,6 +1,6 @@
"use strict";
// A vendored fake package for the library-mode npm posture: eligible by
// every bar (own .d.ts, unminified CJS, no build-transform markers), with
// every bar (own .d.ts, no build-transform markers), with
// its OWN npm dep edge (mathdep) so the transitive fixpoint is exercised.
const steps = require("mathdep");