mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-27 06:43:19 +08:00
Keep the .d.ts-only types of a package compiled from its JavaScript (#1729)
This commit is contained in:
@@ -0,0 +1,59 @@
|
||||
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { afterAll, expect, test } from "vitest";
|
||||
import { nodeFrontendServices } from "./services-node.js";
|
||||
import { npmStaticTypeExportPlan } from "./npm-static-type-exports-host.js";
|
||||
|
||||
const dirs: string[] = [];
|
||||
afterAll(() => {
|
||||
for (const dir of dirs) rmSync(dir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
/** A program directory with one package whose .d.ts is `declarations`;
|
||||
* returns the program's entry path. */
|
||||
function fixture(declarations: string): string {
|
||||
const dir = mkdtempSync(join(tmpdir(), "scriptc-type-exports-"));
|
||||
dirs.push(dir);
|
||||
const lib = join(dir, "node_modules", "pkg", "lib");
|
||||
mkdirSync(lib, { recursive: true });
|
||||
writeFileSync(
|
||||
join(dir, "node_modules", "pkg", "package.json"),
|
||||
JSON.stringify({ name: "pkg", main: "./lib/index.js", types: "./lib/index.d.ts" }),
|
||||
);
|
||||
writeFileSync(join(lib, "index.d.ts"), declarations);
|
||||
writeFileSync(join(lib, "index.js"), "exports.grow = function grow(into) {};\n");
|
||||
writeFileSync(join(dir, "main.ts"), 'import { grow } from "pkg";\n');
|
||||
return join(dir, "main.ts");
|
||||
}
|
||||
|
||||
/** The type names the runtime entry's typedefs re-export. */
|
||||
function reexported(entry: string): string[] {
|
||||
const plan = npmStaticTypeExportPlan(nodeFrontendServices(), entry, ["pkg"]);
|
||||
const block = [...plan.typedefs.values()].join("\n");
|
||||
return [...block.matchAll(/@typedef \{[^}]*\} (\w+)/g)].map((m) => m[1]!);
|
||||
}
|
||||
|
||||
test("only data shapes are re-exported", () => {
|
||||
const entry = fixture(`export interface Point { x: number; y: number }
|
||||
export interface Handle { close(): void }
|
||||
export type Label = { text: string; at: Point };
|
||||
export type Callback = (point: Point) => void;
|
||||
export interface Holder { handle: Handle }
|
||||
export declare function grow(into: Point[]): void;
|
||||
`);
|
||||
expect(reexported(entry)).toEqual(["Point", "Label"]);
|
||||
});
|
||||
|
||||
test("mutually recursive types are data shapes only when every type they reach is", () => {
|
||||
// Branch is checked first and reaches Tree, which refers back to Branch:
|
||||
// Tree is a data shape only if Branch is, and Branch holds a Handle.
|
||||
const entry = fixture(`export interface Branch { child: Tree; handle: Handle }
|
||||
export interface Tree { name: string; parent: Branch | null }
|
||||
export interface Handle { close(): void }
|
||||
export interface Node { value: number; next: List }
|
||||
export interface List { head: Node | null }
|
||||
export declare function grow(into: Tree[]): void;
|
||||
`);
|
||||
expect(reexported(entry)).toEqual(["Node", "List"]);
|
||||
});
|
||||
@@ -0,0 +1,238 @@
|
||||
/** Filesystem side of npm-static-type-exports.ts: finds each opted-in
|
||||
* package's declaration entry and runtime entry, copies the declaration
|
||||
* files the entry reaches, and builds the runtime entry's typedefs. */
|
||||
import { dirname, relative, resolve as resolvePath } from "node:path";
|
||||
import { trackedFileExists, trackedReadFile } from "./input-tracker.js";
|
||||
import { resolveBareModule } from "./resolve.js";
|
||||
import { isJsSourceFileName } from "./tsc-codes.js";
|
||||
import { NPM_STATIC_TYPES_SUFFIX } from "./npm-static.js";
|
||||
import type { FrontendServices } from "./services.js";
|
||||
import * as ts from "./ts7/syntax.js";
|
||||
import {
|
||||
npmStaticDeclarationCopy,
|
||||
npmStaticDeclarationExports,
|
||||
npmStaticDeclarationSpecifiers,
|
||||
npmStaticTypedefBlock,
|
||||
type NpmStaticDeclarationExports,
|
||||
type NpmStaticTypeDeclaration,
|
||||
} from "./npm-static-type-exports.js";
|
||||
|
||||
export interface NpmStaticTypeExportPlan {
|
||||
/** Copies of the declaration files, by copy path. */
|
||||
copies: Map<string, string>;
|
||||
/** The typedef block to append, by runtime entry (forward slashes). */
|
||||
typedefs: Map<string, string>;
|
||||
}
|
||||
|
||||
/** The copy path of a declaration file. */
|
||||
function copyPathOf(declarationFile: string): string {
|
||||
return declarationFile.replace(/\.d\.ts$/, NPM_STATIC_TYPES_SUFFIX);
|
||||
}
|
||||
|
||||
/** The declaration file a relative specifier names from `fromFile`, or null. */
|
||||
function declarationTargetOf(fromFile: string, specifier: string): string | null {
|
||||
const base = resolvePath(dirname(fromFile), specifier);
|
||||
const candidates = [
|
||||
`${base}.d.ts`,
|
||||
`${base.replace(/\.(c|m)?js$/, "")}.d.ts`,
|
||||
resolvePath(base, "index.d.ts"),
|
||||
];
|
||||
for (const candidate of candidates) if (trackedFileExists(candidate)) return candidate;
|
||||
return null;
|
||||
}
|
||||
|
||||
/** A specifier for `target` from `fromFile`'s directory, without `.d.ts`. */
|
||||
function specifierFor(fromFile: string, target: string): string {
|
||||
const rel = relative(dirname(fromFile), target)
|
||||
.split("\\")
|
||||
.join("/")
|
||||
.replace(/\.d\.ts$/, "");
|
||||
return rel.startsWith(".") ? rel : `./${rel}`;
|
||||
}
|
||||
|
||||
/** The names the runtime JavaScript declares as types itself: classes,
|
||||
* JSDoc typedefs and callbacks, and `@enum` constants. */
|
||||
function runtimeTypeNames(services: FrontendServices, file: string, text: string): Set<string> {
|
||||
const names = new Set<string>();
|
||||
for (const m of text.matchAll(/@(?:typedef|callback)\s+(?:\{[^}]*\}\s*)?([A-Za-z_$][\w$]*)/g))
|
||||
names.add(m[1]!);
|
||||
// `/** @enum {number} */ const Kind = { ... }`: the constant names a type too.
|
||||
for (const m of text.matchAll(
|
||||
/@enum\b[^\n]*\n(?:\s*\*[^\n]*\n)*?\s*(?:\*\/\s*)?(?:export\s+)?(?:const|let|var)\s+([A-Za-z_$][\w$]*)/g,
|
||||
))
|
||||
names.add(m[1]!);
|
||||
const sf = services.parse(file, text, "js");
|
||||
for (const stmt of sf.statements.nodes)
|
||||
if (ts.isClassDeclaration(stmt) && stmt.name !== undefined) names.add(stmt.name.text);
|
||||
return names;
|
||||
}
|
||||
|
||||
export function npmStaticTypeExportPlan(
|
||||
services: FrontendServices,
|
||||
entryPath: string,
|
||||
packages: readonly string[],
|
||||
): NpmStaticTypeExportPlan {
|
||||
const copies = new Map<string, string>();
|
||||
const typedefs = new Map<string, string>();
|
||||
const parsed = new Map<string, ts.SourceFile | null>();
|
||||
const parse = (file: string): ts.SourceFile | null => {
|
||||
let sf = parsed.get(file);
|
||||
if (sf === undefined) {
|
||||
const text = trackedReadFile(file);
|
||||
sf = text === null ? null : services.parse(file, text, "ts");
|
||||
parsed.set(file, sf);
|
||||
}
|
||||
return sf;
|
||||
};
|
||||
// Copies every declaration file reachable from `file` through relative
|
||||
// specifiers, each specifier respelled to its target's copy.
|
||||
const copy = (file: string): void => {
|
||||
const copyPath = copyPathOf(file);
|
||||
if (copies.has(copyPath)) return;
|
||||
const sf = parse(file);
|
||||
if (sf === null) return;
|
||||
copies.set(copyPath, "");
|
||||
const targets = new Map<string, string>();
|
||||
for (const specifier of npmStaticDeclarationSpecifiers(sf)) {
|
||||
const target = declarationTargetOf(file, specifier);
|
||||
if (target !== null) targets.set(specifier, target);
|
||||
}
|
||||
copies.set(
|
||||
copyPath,
|
||||
npmStaticDeclarationCopy(sf, (specifier) => {
|
||||
const target = targets.get(specifier);
|
||||
return target === undefined ? null : specifierFor(copyPath, copyPathOf(target));
|
||||
}),
|
||||
);
|
||||
for (const target of targets.values()) copy(target);
|
||||
};
|
||||
const exportsOf = new Map<string, NpmStaticDeclarationExports | null>();
|
||||
const declarationExports = (file: string): NpmStaticDeclarationExports | null => {
|
||||
let ex = exportsOf.get(file);
|
||||
if (ex === undefined) {
|
||||
const sf = parse(file);
|
||||
ex = sf === null ? null : npmStaticDeclarationExports(sf);
|
||||
exportsOf.set(file, ex);
|
||||
}
|
||||
return ex;
|
||||
};
|
||||
// The exported type-only declarations of `file`, through its re-exports,
|
||||
// each with the file that declares it.
|
||||
type Found = { decl: NpmStaticTypeDeclaration; file: string };
|
||||
const typesOf = (
|
||||
file: string,
|
||||
seen: Set<string>,
|
||||
): { types: Map<string, Found>; values: Set<string> } => {
|
||||
const result = { types: new Map<string, Found>(), values: new Set<string>() };
|
||||
if (seen.has(file)) return result;
|
||||
seen.add(file);
|
||||
const own = declarationExports(file);
|
||||
if (own === null) return result;
|
||||
for (const t of own.types) result.types.set(t.name, { decl: t, file });
|
||||
for (const v of own.values) result.values.add(v);
|
||||
for (const n of own.named) {
|
||||
const target = n.specifier === null ? null : declarationTargetOf(file, n.specifier);
|
||||
if (target === null) continue;
|
||||
const inner = typesOf(target, new Set(seen));
|
||||
const t = inner.types.get(n.local);
|
||||
if (t !== undefined)
|
||||
result.types.set(n.exported, { ...t, decl: { ...t.decl, name: n.exported } });
|
||||
else if (inner.values.has(n.local)) result.values.add(n.exported);
|
||||
}
|
||||
for (const specifier of own.stars) {
|
||||
const target = declarationTargetOf(file, specifier);
|
||||
if (target === null) continue;
|
||||
const inner = typesOf(target, new Set(seen));
|
||||
for (const [name, t] of inner.types) if (!result.types.has(name)) result.types.set(name, t);
|
||||
for (const v of inner.values) result.values.add(v);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
// Whether `name`, as `file` sees it, is a data shape: an interface, type
|
||||
// alias or enum whose syntax describes no behavior (npm-static-type-
|
||||
// exports.ts) and whose referenced types are data shapes too. A name the
|
||||
// package's declaration files do not declare is a global (`Array`,
|
||||
// `Record`, `Date`); a class, a value or an import from another package
|
||||
// is not a data shape.
|
||||
//
|
||||
// `referencesOf` answers one name: null when it is not a data shape by
|
||||
// itself, otherwise the names it refers to.
|
||||
type Ref = { file: string; name: string };
|
||||
const referencesOf = (file: string, name: string): Ref[] | null => {
|
||||
const ex = declarationExports(file);
|
||||
if (ex === null) return null;
|
||||
const local = ex.locals.get(name);
|
||||
if (local !== undefined)
|
||||
return local.behavior ? null : local.references.map((r) => ({ file, name: r }));
|
||||
if (ex.valueNames.has(name)) return null;
|
||||
const from = ex.imports.get(name);
|
||||
if (from !== undefined) {
|
||||
const target = declarationTargetOf(file, from.specifier);
|
||||
return target === null ? null : [{ file: target, name: from.local }];
|
||||
}
|
||||
const reexport = typesOf(file, new Set()).types.get(name);
|
||||
if (reexport !== undefined) return [{ file: reexport.file, name: reexport.decl.declared }];
|
||||
return [];
|
||||
};
|
||||
// A name is a data shape when every name reachable from it is one by
|
||||
// itself. `isData` walks everything reachable before it records any name
|
||||
// as a data shape, so a type in a reference cycle is not recorded while
|
||||
// another type in the cycle is still unchecked.
|
||||
const dataMemo = new Map<string, boolean>();
|
||||
const isData = (file: string, name: string): boolean => {
|
||||
const keyOf = (ref: Ref): string => `${ref.file}\0${ref.name}`;
|
||||
// Each reached name, by key, with the key of the name it was reached from.
|
||||
const reachedFrom = new Map<string, string | null>([[keyOf({ file, name }), null]]);
|
||||
const queue: Ref[] = [{ file, name }];
|
||||
for (let i = 0; i < queue.length; i++) {
|
||||
const ref = queue[i]!;
|
||||
const key = keyOf(ref);
|
||||
const memo = dataMemo.get(key);
|
||||
if (memo === true) continue;
|
||||
const next = memo === false ? null : referencesOf(ref.file, ref.name);
|
||||
if (next === null) {
|
||||
// Every name on the path to this one reaches a non-data type.
|
||||
for (let k: string | null = key; k !== null; k = reachedFrom.get(k) ?? null)
|
||||
dataMemo.set(k, false);
|
||||
return false;
|
||||
}
|
||||
for (const target of next) {
|
||||
const targetKey = keyOf(target);
|
||||
if (reachedFrom.has(targetKey)) continue;
|
||||
reachedFrom.set(targetKey, key);
|
||||
queue.push(target);
|
||||
}
|
||||
}
|
||||
for (const key of reachedFrom.keys()) dataMemo.set(key, true);
|
||||
return true;
|
||||
};
|
||||
for (const pkg of packages) {
|
||||
const declared = resolveBareModule(entryPath, pkg, "types-only");
|
||||
if (declared === null || declared.packageName !== pkg || !declared.typesFile.endsWith(".d.ts"))
|
||||
continue;
|
||||
const runtime = resolveBareModule(entryPath, pkg, "js-only");
|
||||
if (runtime === null || !isJsSourceFileName(runtime.typesFile)) continue;
|
||||
const runtimeText = trackedReadFile(runtime.typesFile);
|
||||
if (runtimeText === null) continue;
|
||||
const { types, values } = typesOf(declared.typesFile, new Set());
|
||||
const declaredByRuntime = runtimeTypeNames(services, runtime.typesFile, runtimeText);
|
||||
// Only data shapes: a type with methods describes the package's
|
||||
// JavaScript objects, which stay as the JavaScript types them.
|
||||
const exported = [...types.values()]
|
||||
.filter(
|
||||
({ decl, file }) =>
|
||||
!values.has(decl.name) &&
|
||||
!declaredByRuntime.has(decl.name) &&
|
||||
isData(file, decl.declared),
|
||||
)
|
||||
.map(({ decl }) => decl);
|
||||
if (exported.length === 0) continue;
|
||||
copy(declared.typesFile);
|
||||
const specifier = specifierFor(runtime.typesFile, copyPathOf(declared.typesFile));
|
||||
typedefs.set(
|
||||
runtime.typesFile.split("\\").join("/"),
|
||||
npmStaticTypedefBlock(exported, specifier),
|
||||
);
|
||||
}
|
||||
return { copies, typedefs };
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
import { expect, test } from "vitest";
|
||||
import { nodeFrontendServices } from "./services-node.js";
|
||||
import {
|
||||
npmStaticDeclarationCopy,
|
||||
npmStaticDeclarationExports,
|
||||
npmStaticTypedefBlock,
|
||||
} from "./npm-static-type-exports.js";
|
||||
|
||||
function parse(text: string) {
|
||||
return nodeFrontendServices().parse("/pkg/lib/index.d.ts", text, "ts");
|
||||
}
|
||||
|
||||
test("type-only exports are told from values and re-exports", () => {
|
||||
const exports = npmStaticDeclarationExports(
|
||||
parse(`import type { Page } from "./paging";
|
||||
export * from "./paging";
|
||||
export { Entry as Item, Kind } from "./entries";
|
||||
export interface Options<T extends object = {}, U = string> { value: T; label: U }
|
||||
export type Mode = "a" | "b";
|
||||
export declare const enum Level { Low = 1 }
|
||||
export declare class Client { url: string }
|
||||
export declare function make(options: Options): Client;
|
||||
export declare const version: string;
|
||||
interface Hidden { x: number }
|
||||
export { Hidden as Visible };
|
||||
declare function helper(): void;
|
||||
export { helper };`),
|
||||
);
|
||||
expect(exports.types.map((t) => [t.name, t.params, t.templates])).toEqual([
|
||||
["Options", ["T", "U"], ["{object} [T={}]", "[U=string]"]],
|
||||
["Mode", [], []],
|
||||
["Level", [], []],
|
||||
["Visible", [], []],
|
||||
]);
|
||||
expect(exports.values).toEqual(["Client", "make", "version", "helper"]);
|
||||
expect(exports.named).toEqual([
|
||||
{ exported: "Item", local: "Entry", specifier: "./entries" },
|
||||
{ exported: "Kind", local: "Kind", specifier: "./entries" },
|
||||
]);
|
||||
expect(exports.stars).toEqual(["./paging"]);
|
||||
});
|
||||
|
||||
test("a local export of an imported name resolves like a re-export", () => {
|
||||
const exports = npmStaticDeclarationExports(
|
||||
parse(`import type { Kind } from "./kinds";
|
||||
import { type Shape as Form, helper } from "./kinds";
|
||||
export { Kind, Form as Outline, helper };`),
|
||||
);
|
||||
expect(exports.types).toEqual([]);
|
||||
expect(exports.named).toEqual([
|
||||
{ exported: "Kind", local: "Kind", specifier: "./kinds" },
|
||||
{ exported: "Outline", local: "Shape", specifier: "./kinds" },
|
||||
{ exported: "helper", local: "helper", specifier: "./kinds" },
|
||||
]);
|
||||
});
|
||||
|
||||
test("each declaration records whether it describes behavior and what it references", () => {
|
||||
const { locals } = npmStaticDeclarationExports(
|
||||
parse(`interface Row<T> { key: string; value: T; next?: Row<T>; tags: Tag[] }
|
||||
type Tag = "a" | "b";
|
||||
interface Counter { add(value: number): void }
|
||||
type Callback = (row: Row<string>) => void;
|
||||
type Keys = keyof Row<string>;
|
||||
interface Named extends Base { label: Label }`),
|
||||
);
|
||||
const shape = (name: string) => {
|
||||
const d = locals.get(name)!;
|
||||
return [d.behavior, [...d.references].sort()];
|
||||
};
|
||||
expect(shape("Row")).toEqual([false, ["Row", "Tag"]]);
|
||||
expect(shape("Tag")).toEqual([false, []]);
|
||||
expect(shape("Counter")).toEqual([true, []]);
|
||||
expect(shape("Callback")[0]).toBe(true);
|
||||
expect(shape("Keys")[0]).toBe(true);
|
||||
expect(shape("Named")).toEqual([false, ["Base", "Label"]]);
|
||||
});
|
||||
|
||||
test("a copy respells the relative specifiers it is told", () => {
|
||||
const sf = parse(`import type { Page } from "./paging";
|
||||
export * from './entries';
|
||||
export type Box = import("./paging").Page<number>;
|
||||
export { readFile } from "fs";
|
||||
`);
|
||||
const copy = npmStaticDeclarationCopy(sf, (s) =>
|
||||
s === "./paging" ? "./paging.scriptc-types" : null,
|
||||
);
|
||||
expect(copy).toBe(`import type { Page } from "./paging.scriptc-types";
|
||||
export * from './entries';
|
||||
export type Box = import("./paging.scriptc-types").Page<number>;
|
||||
export { readFile } from "fs";
|
||||
`);
|
||||
});
|
||||
|
||||
test("the typedef block re-exports each type with its parameters", () => {
|
||||
expect(
|
||||
npmStaticTypedefBlock(
|
||||
[
|
||||
{
|
||||
name: "Mode",
|
||||
declared: "Mode",
|
||||
params: [],
|
||||
templates: [],
|
||||
behavior: false,
|
||||
references: [],
|
||||
},
|
||||
{
|
||||
name: "Options",
|
||||
declared: "Options",
|
||||
params: ["T", "U"],
|
||||
templates: ["{object} [T={}]", "[U=string]"],
|
||||
behavior: false,
|
||||
references: [],
|
||||
},
|
||||
],
|
||||
"./index.scriptc-types",
|
||||
),
|
||||
).toBe(`/**
|
||||
* @typedef {import("./index.scriptc-types").Mode} Mode
|
||||
*/
|
||||
/**
|
||||
* @template {object} [T={}]
|
||||
* @template [U=string]
|
||||
* @typedef {import("./index.scriptc-types").Options<T, U>} Options
|
||||
*/`);
|
||||
});
|
||||
@@ -0,0 +1,287 @@
|
||||
/* The type-only exports of an opted-in package's declaration files.
|
||||
*
|
||||
* An opted-in package compiles from its JavaScript, and the checker types
|
||||
* the package from it: the package's shipped .d.ts is hidden from
|
||||
* resolution (npmStaticFsShadow). Values keep their JavaScript types, but a
|
||||
* type that exists only in the .d.ts (an interface, a type alias, an enum
|
||||
* used as a type) has no declaration in the JavaScript, so a program's
|
||||
* `import type { Options } from "pkg"` resolved to `any`. TypeScript and
|
||||
* Node see the .d.ts types.
|
||||
*
|
||||
* The package's declaration files stay available as copies under another
|
||||
* name (`<file>.scriptc-types.d.ts`, relative specifiers respelled to the
|
||||
* copies), and the runtime entry gains a JSDoc typedef per type-only export
|
||||
* that names the declaration in the copy:
|
||||
*
|
||||
* / ** @template T @typedef {import("./main.scriptc-types").Box<T>} Box * /
|
||||
*
|
||||
* Classes and values keep the JavaScript's own declarations. */
|
||||
import * as ts from "./ts7/syntax.js";
|
||||
import { tokenStartOfNode } from "./ts7/node-functions.js";
|
||||
|
||||
/** A type-only declaration: its exported name, its name where it is
|
||||
* declared, its type parameters' names and their JSDoc @template tag
|
||||
* bodies, and what npmStaticDataShape needs to know. */
|
||||
export interface NpmStaticTypeDeclaration {
|
||||
name: string;
|
||||
declared: string;
|
||||
params: readonly string[];
|
||||
templates: readonly string[];
|
||||
/** The declaration's own syntax describes behavior or a computed type:
|
||||
* a method, call or construct signature, a function or constructor
|
||||
* type, an accessor, `typeof`, `import("...")`, a conditional, mapped,
|
||||
* indexed-access or `keyof` type, or a qualified type name. */
|
||||
behavior: boolean;
|
||||
/** The type names it refers to, its own type parameters excepted. */
|
||||
references: readonly string[];
|
||||
}
|
||||
|
||||
/** What a declaration file declares and re-exports, by exported name. */
|
||||
export interface NpmStaticDeclarationExports {
|
||||
/** Exported interfaces, type aliases and enums, with their parameters. */
|
||||
types: readonly NpmStaticTypeDeclaration[];
|
||||
/** Exported classes, functions, variables and namespaces: names the
|
||||
* JavaScript declares itself, or that are not type-only. */
|
||||
values: readonly string[];
|
||||
/** `export { local as exported } from "specifier"`, including a local
|
||||
* `export { a }` of an imported name. */
|
||||
named: readonly { exported: string; local: string; specifier: string | null }[];
|
||||
/** `export * from "specifier"`. */
|
||||
stars: readonly string[];
|
||||
/** Every interface, type alias and enum the file declares, by name. */
|
||||
locals: ReadonlyMap<string, NpmStaticTypeDeclaration>;
|
||||
/** The file's named imports: local name to imported name and specifier. */
|
||||
imports: ReadonlyMap<string, { local: string; specifier: string }>;
|
||||
/** Every class, function, namespace and variable the file declares. */
|
||||
valueNames: ReadonlySet<string>;
|
||||
}
|
||||
|
||||
function isExported(node: ts.Node): boolean {
|
||||
const modifiers = (node as { modifiers?: { nodes: readonly ts.Node[] } }).modifiers?.nodes ?? [];
|
||||
return modifiers.some((m) => m.kind === ts.SyntaxKind.ExportKeyword);
|
||||
}
|
||||
|
||||
function templatesOf(sf: ts.SourceFile, params: readonly ts.TypeParameterDeclaration[]): string[] {
|
||||
const text = (n: ts.Node): string => sf.text.slice(tokenStartOfNode(n), n.end);
|
||||
return params.map((p) => {
|
||||
const name = p.name.text;
|
||||
const constraint = p.constraint === undefined ? "" : `{${text(p.constraint)}} `;
|
||||
return p.defaultType === undefined
|
||||
? `${constraint}${name}`
|
||||
: `${constraint}[${name}=${text(p.defaultType)}]`;
|
||||
});
|
||||
}
|
||||
|
||||
/** Syntax kinds of a type that describes behavior or is computed. */
|
||||
const BEHAVIOR_KINDS: ReadonlySet<ts.SyntaxKind> = new Set([
|
||||
ts.SyntaxKind.MethodSignature,
|
||||
ts.SyntaxKind.CallSignature,
|
||||
ts.SyntaxKind.ConstructSignature,
|
||||
ts.SyntaxKind.FunctionType,
|
||||
ts.SyntaxKind.ConstructorType,
|
||||
ts.SyntaxKind.GetAccessor,
|
||||
ts.SyntaxKind.SetAccessor,
|
||||
ts.SyntaxKind.TypeQuery,
|
||||
ts.SyntaxKind.ImportType,
|
||||
ts.SyntaxKind.ConditionalType,
|
||||
ts.SyntaxKind.MappedType,
|
||||
ts.SyntaxKind.InferType,
|
||||
ts.SyntaxKind.IndexedAccessType,
|
||||
ts.SyntaxKind.ThisType,
|
||||
ts.SyntaxKind.TypePredicate,
|
||||
ts.SyntaxKind.TemplateLiteralType,
|
||||
]);
|
||||
|
||||
/** The behavior flag and type references of an interface or type alias. */
|
||||
function shapeOf(
|
||||
decl: ts.InterfaceDeclaration | ts.TypeAliasDeclaration,
|
||||
params: readonly string[],
|
||||
): { behavior: boolean; references: string[] } {
|
||||
let behavior = false;
|
||||
const references = new Set<string>();
|
||||
const refer = (name: ts.Node): void => {
|
||||
if (ts.isIdentifier(name)) {
|
||||
if (!params.includes(name.text)) references.add(name.text);
|
||||
} else behavior = true; // a qualified name: a namespace member
|
||||
};
|
||||
ts.walkPreorder(decl, (n) => {
|
||||
if (behavior) return "stop";
|
||||
if (BEHAVIOR_KINDS.has(n.kind)) behavior = true;
|
||||
else if (ts.isTypeOperatorNode(n) && n.operator === ts.SyntaxKind.KeyOfKeyword) behavior = true;
|
||||
else if (ts.isTypeReferenceNode(n)) refer(n.typeName);
|
||||
else if (ts.isExpressionWithTypeArguments(n)) refer(n.expression);
|
||||
return undefined;
|
||||
});
|
||||
return { behavior, references: [...references] };
|
||||
}
|
||||
|
||||
/** The exports of one declaration file. */
|
||||
export function npmStaticDeclarationExports(sf: ts.SourceFile): NpmStaticDeclarationExports {
|
||||
const types: NpmStaticTypeDeclaration[] = [];
|
||||
const values: string[] = [];
|
||||
const named: { exported: string; local: string; specifier: string | null }[] = [];
|
||||
const stars: string[] = [];
|
||||
const localTypes = new Map<string, NpmStaticTypeDeclaration>();
|
||||
const localValues = new Set<string>();
|
||||
// `import { A as B } from "./x"`: local name B is A of "./x".
|
||||
const imported = new Map<string, { local: string; specifier: string }>();
|
||||
for (const stmt of sf.statements.nodes) {
|
||||
const exported = isExported(stmt);
|
||||
if (ts.isImportDeclaration(stmt)) {
|
||||
const bindings = stmt.importClause?.namedBindings;
|
||||
if (ts.isStringLiteral(stmt.moduleSpecifier) && bindings && ts.isNamedImports(bindings)) {
|
||||
for (const e of bindings.elements.nodes) {
|
||||
imported.set(e.name.text, {
|
||||
local: (e.propertyName ?? e.name).text,
|
||||
specifier: stmt.moduleSpecifier.text,
|
||||
});
|
||||
}
|
||||
}
|
||||
} else if (ts.isInterfaceDeclaration(stmt) || ts.isTypeAliasDeclaration(stmt)) {
|
||||
const params = stmt.typeParameters?.nodes ?? [];
|
||||
const paramNames = params.map((p) => p.name.text);
|
||||
const shape = shapeOf(stmt, paramNames);
|
||||
const merged = localTypes.get(stmt.name.text); // interface merging
|
||||
const decl: NpmStaticTypeDeclaration = {
|
||||
name: stmt.name.text,
|
||||
declared: stmt.name.text,
|
||||
params: paramNames,
|
||||
templates: templatesOf(sf, params),
|
||||
behavior: shape.behavior || merged?.behavior === true,
|
||||
references: [...shape.references, ...(merged?.references ?? [])],
|
||||
};
|
||||
localTypes.set(decl.name, decl);
|
||||
if (exported && merged === undefined) types.push(decl);
|
||||
} else if (ts.isEnumDeclaration(stmt)) {
|
||||
const decl: NpmStaticTypeDeclaration = {
|
||||
name: stmt.name.text,
|
||||
declared: stmt.name.text,
|
||||
params: [],
|
||||
templates: [],
|
||||
behavior: false,
|
||||
references: [],
|
||||
};
|
||||
localTypes.set(decl.name, decl);
|
||||
if (exported) types.push(decl);
|
||||
} else if (
|
||||
ts.isClassDeclaration(stmt) ||
|
||||
ts.isFunctionDeclaration(stmt) ||
|
||||
ts.isModuleDeclaration(stmt)
|
||||
) {
|
||||
const name = stmt.name !== undefined && ts.isIdentifier(stmt.name) ? stmt.name.text : null;
|
||||
if (name !== null) {
|
||||
localValues.add(name);
|
||||
if (exported) values.push(name);
|
||||
}
|
||||
} else if (ts.isVariableStatement(stmt)) {
|
||||
for (const d of stmt.declarationList.declarations.nodes) {
|
||||
if (!ts.isIdentifier(d.name)) continue;
|
||||
localValues.add(d.name.text);
|
||||
if (exported) values.push(d.name.text);
|
||||
}
|
||||
} else if (ts.isExportDeclaration(stmt)) {
|
||||
const specifier =
|
||||
stmt.moduleSpecifier !== undefined && ts.isStringLiteral(stmt.moduleSpecifier)
|
||||
? stmt.moduleSpecifier.text
|
||||
: null;
|
||||
const clause = stmt.exportClause;
|
||||
if (clause === undefined) {
|
||||
if (specifier !== null) stars.push(specifier);
|
||||
} else if (ts.isNamedExports(clause)) {
|
||||
for (const e of clause.elements.nodes) {
|
||||
const exportedName = e.name.text;
|
||||
const local = (e.propertyName ?? e.name).text;
|
||||
named.push({ exported: exportedName, local, specifier });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Local `export { a as b }`: of the file's own declarations, or of a
|
||||
// name it imports (`import type { A } from "./a"; export { A }`), which
|
||||
// resolves like `export { A } from "./a"`.
|
||||
const resolvedNamed: typeof named = [];
|
||||
for (const n of named) {
|
||||
if (n.specifier !== null) {
|
||||
resolvedNamed.push(n);
|
||||
continue;
|
||||
}
|
||||
const local = localTypes.get(n.local);
|
||||
const from = imported.get(n.local);
|
||||
if (local !== undefined) types.push({ ...local, name: n.exported });
|
||||
else if (localValues.has(n.local)) values.push(n.exported);
|
||||
else if (from !== undefined) resolvedNamed.push({ exported: n.exported, ...from });
|
||||
}
|
||||
// A merged interface's exported entry takes the merged flags.
|
||||
const exportedTypes: NpmStaticTypeDeclaration[] = [];
|
||||
for (const t of types) {
|
||||
const local = localTypes.get(t.declared);
|
||||
if (local === undefined) exportedTypes.push(t);
|
||||
else exportedTypes.push({ ...local, name: t.name });
|
||||
}
|
||||
return {
|
||||
types: exportedTypes,
|
||||
values,
|
||||
named: resolvedNamed,
|
||||
stars,
|
||||
locals: localTypes,
|
||||
imports: imported,
|
||||
valueNames: localValues,
|
||||
};
|
||||
}
|
||||
|
||||
/** The relative module specifiers of a declaration file's imports, exports
|
||||
* and `import("...")` types, with their spans. */
|
||||
function relativeSpecifiers(sf: ts.SourceFile): { start: number; end: number; text: string }[] {
|
||||
const found: { start: number; end: number; text: string }[] = [];
|
||||
const add = (literal: ts.Node | undefined): void => {
|
||||
if (literal === undefined || !ts.isStringLiteral(literal)) return;
|
||||
if (!literal.text.startsWith("./") && !literal.text.startsWith("../")) return;
|
||||
found.push({ start: tokenStartOfNode(literal), end: literal.end, text: literal.text });
|
||||
};
|
||||
ts.walkPreorder(sf, (n) => {
|
||||
if (ts.isImportDeclaration(n) || ts.isExportDeclaration(n)) add(n.moduleSpecifier);
|
||||
else if (ts.isImportTypeNode(n) && ts.isLiteralTypeNode(n.argument)) add(n.argument.literal);
|
||||
return undefined;
|
||||
});
|
||||
return found;
|
||||
}
|
||||
|
||||
/** The relative specifiers a declaration file names. */
|
||||
export function npmStaticDeclarationSpecifiers(sf: ts.SourceFile): string[] {
|
||||
return relativeSpecifiers(sf).map((s) => s.text);
|
||||
}
|
||||
|
||||
/** The declaration file's text with each relative specifier `rename`
|
||||
* answers respelled (quoted with the original's quote). */
|
||||
export function npmStaticDeclarationCopy(
|
||||
sf: ts.SourceFile,
|
||||
rename: (specifier: string) => string | null,
|
||||
): string {
|
||||
const spans = relativeSpecifiers(sf).sort((a, b) => a.start - b.start);
|
||||
let text = "";
|
||||
let at = 0;
|
||||
for (const span of spans) {
|
||||
const renamed = rename(span.text);
|
||||
if (renamed === null) continue;
|
||||
const quote = sf.text[span.start] ?? '"';
|
||||
text += sf.text.slice(at, span.start) + quote + renamed + quote;
|
||||
at = span.end;
|
||||
}
|
||||
return text + sf.text.slice(at);
|
||||
}
|
||||
|
||||
/** The JSDoc typedefs that re-export `types` from the copy at `specifier`. */
|
||||
export function npmStaticTypedefBlock(
|
||||
types: readonly NpmStaticTypeDeclaration[],
|
||||
specifier: string,
|
||||
): string {
|
||||
const lines: string[] = [];
|
||||
for (const t of types) {
|
||||
const args = t.params.length === 0 ? "" : `<${t.params.join(", ")}>`;
|
||||
lines.push("/**");
|
||||
for (const tag of t.templates) lines.push(` * @template ${tag}`);
|
||||
lines.push(` * @typedef {import(${JSON.stringify(specifier)}).${t.name}${args}} ${t.name}`);
|
||||
lines.push(" */");
|
||||
}
|
||||
return lines.join("\n");
|
||||
}
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
} from "./workspace-registry.js";
|
||||
import {
|
||||
clearVirtualSources,
|
||||
isVirtualSource,
|
||||
setVirtualSource,
|
||||
trackedExists,
|
||||
trackedReadFile,
|
||||
@@ -53,10 +54,43 @@ export function npmStaticLexerVisibleNames(path: string): readonly string[] {
|
||||
return lexerNames.get(path.split("\\").join("/")) ?? [];
|
||||
}
|
||||
|
||||
/** The suffix that replaces `.d.ts` in the name of a copy of an opted-in
|
||||
* package's declaration file (npm-static-type-exports.ts). Defined here so
|
||||
* this module does not depend on the syntax modules. */
|
||||
export const NPM_STATIC_TYPES_SUFFIX = ".scriptc-types.d.ts";
|
||||
|
||||
/** Per runtime entry (forward slashes), the JSDoc typedefs that re-export
|
||||
* its package's type-only declarations (npm-static-type-exports.ts). */
|
||||
let typeExportTypedefs = new Map<string, string>();
|
||||
|
||||
/** Registers the copies of the opted-in packages' declaration files and the
|
||||
* typedefs their runtime entries gain (after setNpmStaticPackages, which
|
||||
* clears virtual sources). */
|
||||
export function setNpmStaticTypeExports(
|
||||
copies: ReadonlyMap<string, string>,
|
||||
typedefs: ReadonlyMap<string, string>,
|
||||
): void {
|
||||
for (const [path, text] of copies) setVirtualSource(path, text);
|
||||
typeExportTypedefs = new Map(typedefs);
|
||||
rewriteCache.clear();
|
||||
}
|
||||
|
||||
/** `text` (or the file's own text when null) with the typedefs of a
|
||||
* package runtime entry appended: the entry re-exports the type-only
|
||||
* declarations of the package's .d.ts, after every respelling, so
|
||||
* positions stay. Null when `path` is no runtime entry and `text` is null. */
|
||||
function withTypeExportTypedefs(path: string, text: string | null): string | null {
|
||||
const typedefs = typeExportTypedefs.get(path.split("\\").join("/"));
|
||||
if (typedefs === undefined) return text;
|
||||
const source = text ?? trackedReadFile(path);
|
||||
return source === null ? text : `${source}\n${typedefs}\n`;
|
||||
}
|
||||
|
||||
export function setNpmStaticPackages<T extends Iterable<string>>(packages: T): void {
|
||||
// npm packages served as builtins (graceful-fs as fs) never compile
|
||||
// their JavaScript, even when named.
|
||||
activePackages = new Set([...packages].filter((name) => npmBuiltinAliasOf(name) === null));
|
||||
typeExportTypedefs = new Map();
|
||||
declarationOverloads = new Map();
|
||||
declarationProperties = new Map();
|
||||
offenders.clear();
|
||||
@@ -524,12 +558,17 @@ export function npmStaticFsShadow(services: FrontendServices): NpmStaticFsShadow
|
||||
} catch {
|
||||
rewritten = null; // unreadable/unparseable: fall through untouched
|
||||
}
|
||||
rewritten = withTypeExportTypedefs(path, rewritten);
|
||||
rewriteCache.set(path, rewritten);
|
||||
return rewritten ?? undefined;
|
||||
}
|
||||
return undefined;
|
||||
// Other runtime entries (`.mjs`) keep their text and gain only the
|
||||
// typedefs.
|
||||
return withTypeExportTypedefs(path, null) ?? undefined;
|
||||
},
|
||||
hideFile: (path) => {
|
||||
// The copies of the package's declaration files the typedefs name.
|
||||
if (path.endsWith(NPM_STATIC_TYPES_SUFFIX) && isVirtualSource(path)) return false;
|
||||
const target = shadowTargetOf(path);
|
||||
if (!target) return false;
|
||||
if (isBundledShimForwarder(path)) return false;
|
||||
|
||||
@@ -76,8 +76,10 @@ import {
|
||||
reportNpmStaticOffender,
|
||||
setNpmStaticDeclarationOverloads,
|
||||
setNpmStaticPackages,
|
||||
setNpmStaticTypeExports,
|
||||
} from "./npm-static.js";
|
||||
import { isPrunedNpmReexport, planNpmStaticReexports } from "./npm-static-prune.js";
|
||||
import { npmStaticTypeExportPlan } from "./npm-static-type-exports-host.js";
|
||||
import { isNpmStaticSubclassArgument } from "./npm-static-subtyping.js";
|
||||
import {
|
||||
npmStaticDeclarationReexports,
|
||||
@@ -1032,6 +1034,10 @@ export function loadProgram(
|
||||
}
|
||||
setNpmStaticPackages(npmStaticPackages);
|
||||
setNpmStaticDeclarationOverloads(declarationOverloads, declarationProperties);
|
||||
// Types only the packages' .d.ts declares keep their declarations
|
||||
// (npm-static-type-exports.ts).
|
||||
const typeExports = npmStaticTypeExportPlan(services, entryPath, npmStaticPackages);
|
||||
setNpmStaticTypeExports(typeExports.copies, typeExports.typedefs);
|
||||
// Workspace-package registrations reset per load (same discipline as the
|
||||
// npm-static set), then the opted-in names are probed UP FRONT: a
|
||||
// workspace-linked opt-in resolves to files whose realpaths carry no
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7001-package-declared-types/main.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7004-package-declared-type-reexports/main.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
// A package that compiles from its JavaScript, whose interfaces, type
|
||||
// aliases and enums exist only in its .d.ts. A program that imports its
|
||||
// values compiles the JavaScript, and the type-only names it imports keep
|
||||
// the declarations TypeScript reads from the .d.ts.
|
||||
import { Severity, collect, firstOf, label, type Issue, type Page, type Report } from "issue-list";
|
||||
|
||||
function report(text: string): Report {
|
||||
const issues: Issue[] = [];
|
||||
const count = collect(text, issues);
|
||||
return { count, issues };
|
||||
}
|
||||
|
||||
const result = report("a?b!c?");
|
||||
console.log(result.count, result.issues.length);
|
||||
const first = result.issues[0]!;
|
||||
console.log(label(first.severity), first.offset, first.severity === Severity.Warning);
|
||||
const page: Page<Issue> = { items: result.issues.slice(1), next: null };
|
||||
console.log(page.items.map((issue) => `${label(issue.severity)}@${issue.offset}`).join(","), page.next);
|
||||
console.log(firstOf(page)?.offset);
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
import type { Page } from "./paging";
|
||||
export * from "./paging";
|
||||
export declare enum Severity {
|
||||
Warning = 1,
|
||||
Error = 2
|
||||
}
|
||||
export interface Issue {
|
||||
severity: Severity;
|
||||
offset: number;
|
||||
}
|
||||
export type Report = {
|
||||
count: number;
|
||||
issues: Issue[];
|
||||
};
|
||||
export declare function collect(text: string, issues: Issue[]): number;
|
||||
export declare function label(severity: Severity): string;
|
||||
export declare function firstOf<T>(page: Page<T>): T | undefined;
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.Severity = void 0;
|
||||
exports.collect = collect;
|
||||
exports.label = label;
|
||||
exports.firstOf = firstOf;
|
||||
var Severity;
|
||||
(function (Severity) {
|
||||
Severity[Severity["Warning"] = 1] = "Warning";
|
||||
Severity[Severity["Error"] = 2] = "Error";
|
||||
})(Severity || (exports.Severity = Severity = {}));
|
||||
function collect(text, issues) {
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
if (text[i] === "?") issues.push({ severity: Severity.Warning, offset: i });
|
||||
else if (text[i] === "!") issues.push({ severity: Severity.Error, offset: i });
|
||||
}
|
||||
return issues.length;
|
||||
}
|
||||
function label(severity) {
|
||||
return severity === Severity.Warning ? "warning" : "error";
|
||||
}
|
||||
function firstOf(page) {
|
||||
return page.items[0];
|
||||
}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
export interface Page<T> {
|
||||
items: T[];
|
||||
next: string | null;
|
||||
}
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
+1
@@ -0,0 +1 @@
|
||||
{ "name": "issue-list", "version": "1.0.0", "main": "./lib/index.js", "types": "./lib/index.d.ts" }
|
||||
@@ -0,0 +1,19 @@
|
||||
// Two packages that compile from their JavaScript, with types only their
|
||||
// .d.ts declares: one re-exports types its entry imports
|
||||
// (`import type { Kind } from "./kinds"; export { Kind }`), the other's
|
||||
// runtime entry is an ES module (`.mjs`). Declarations using those types
|
||||
// keep TypeScript's types: the empty arrays the packages fill hold the
|
||||
// declared rows.
|
||||
import { addShapes, type Form, type Kind } from "shape-kinds";
|
||||
import { readRows, type Row } from "row-module";
|
||||
|
||||
const forms: Form[] = [];
|
||||
addShapes("c2 s3", forms);
|
||||
const kinds: Kind[] = forms.map((form) => form.kind);
|
||||
const first = forms[0]!;
|
||||
console.log(kinds.join(","), first.size + forms[1]!.size, first.kind === "circle");
|
||||
|
||||
const rows: Row[] = [];
|
||||
const count = readRows("a=1;b=22", rows);
|
||||
const last = rows[count - 1]!;
|
||||
console.log(count, last.key, last.value.length);
|
||||
Generated
Vendored
+5
@@ -0,0 +1,5 @@
|
||||
export interface Row {
|
||||
key: string;
|
||||
value: string;
|
||||
}
|
||||
export declare function readRows(text: string, into: Row[]): number;
|
||||
Generated
Vendored
+7
@@ -0,0 +1,7 @@
|
||||
export function readRows(text, into) {
|
||||
for (const part of text.split(";")) {
|
||||
const at = part.indexOf("=");
|
||||
into.push({ key: part.slice(0, at), value: part.slice(at + 1) });
|
||||
}
|
||||
return into.length;
|
||||
}
|
||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
||||
{ "name": "row-module", "version": "1.0.0", "main": "./lib/index.mjs", "types": "./lib/index.d.ts" }
|
||||
Generated
Vendored
+4
@@ -0,0 +1,4 @@
|
||||
import type { Kind } from "./kinds";
|
||||
import { type Shape as Form } from "./kinds";
|
||||
export { Kind, Form };
|
||||
export declare function addShapes(text: string, into: Form[]): void;
|
||||
Generated
Vendored
+8
@@ -0,0 +1,8 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.addShapes = addShapes;
|
||||
function addShapes(text, into) {
|
||||
for (const part of text.split(" ")) {
|
||||
into.push({ kind: part.charAt(0) === "c" ? "circle" : "square", size: Number(part.slice(1)) });
|
||||
}
|
||||
}
|
||||
Generated
Vendored
+5
@@ -0,0 +1,5 @@
|
||||
export type Kind = "circle" | "square";
|
||||
export interface Shape {
|
||||
kind: Kind;
|
||||
size: number;
|
||||
}
|
||||
Generated
Vendored
+2
@@ -0,0 +1,2 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
||||
{ "name": "shape-kinds", "version": "1.0.0", "main": "./lib/index.js", "types": "./lib/index.d.ts" }
|
||||
@@ -2,7 +2,7 @@ scriptc coverage tests/corpus/native-npm-imports/main.ts
|
||||
|
||||
statements analyzed 14
|
||||
compile statically 14 (100%)
|
||||
in dynamic tier 3 (operate on values typed only at runtime; --explain-dynamic lists them)
|
||||
in dynamic tier 1 (operate on values typed only at runtime; --explain-dynamic lists them)
|
||||
|
||||
npm package compilation:
|
||||
mathkit static
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 2683, "san": 1176 }
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 1015, "san": 2516 }
|
||||
@@ -506,6 +506,19 @@ test("JavaScript builtin aliases keep their deferred value fence", async () => {
|
||||
);
|
||||
});
|
||||
|
||||
test("types only a package's .d.ts declares stay static through re-exports and .mjs entries", () => {
|
||||
// shape-kinds re-exports types its .d.ts entry imports; row-module's
|
||||
// runtime entry is an .mjs file. Without their declared types the
|
||||
// program's uses of Form, Kind and Row run in the dynamic tier (12
|
||||
// statements); with them only the packages' own JavaScript and one
|
||||
// arithmetic on a JavaScript return value do (4).
|
||||
const { coverage } = analyze(
|
||||
join(repoRoot, "tests/corpus/7004-package-declared-type-reexports/main.ts"),
|
||||
);
|
||||
expect(coverage.diagnostics).toEqual([]);
|
||||
expect(coverage.stats.statementsDynamic).toBe(4);
|
||||
});
|
||||
|
||||
test("any-typed checked-dynamic operators compile natively", () => {
|
||||
const file = join(repoRoot, "tests/corpus/2856-dynamic-any-local-operators.ts");
|
||||
const staticCoverage = analyze(file).coverage;
|
||||
|
||||
Reference in New Issue
Block a user