mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-27 06:43:19 +08:00
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:
@@ -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.
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)";
|
||||
}
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7543-var-list-declarator-order.js"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7545-string-regex-checked-arguments.js"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -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
@@ -0,0 +1 @@
|
||||
export declare function render(src: string, options?: { pedantic?: boolean }): string;
|
||||
+1
@@ -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
@@ -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 }
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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,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
@@ -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");
|
||||
|
||||
|
||||
Reference in New Issue
Block a user