Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion scripts/audits/14744-before-update-per-row-value-census.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,8 @@
import { readFileSync, readdirSync, statSync } from 'node:fs';
import { join, relative } from 'node:path';
import { fileURLToPath } from 'node:url';
import ts from 'typescript';
import { requireDefaultExport } from '../import-prerequisite.mjs';
const ts = await requireDefaultExport('typescript', () => import('typescript'), import.meta.url);
// ⛔ Never `ts.createSourceFile` directly. It does not throw on a source it
// cannot read — the errors are parked on `parseDiagnostics` and the recovered
// tree walks like any other, so a file this census could not parse would be
Expand Down
21 changes: 19 additions & 2 deletions scripts/check-comment-mask-corpus.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ import { tmpdir } from 'node:os';
import { dirname, extname, join, relative, resolve, sep } from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';
import { isEntrypoint } from './invoked-as.mjs';
import { requireDependency } from './import-prerequisite.mjs';

const HERE = dirname(fileURLToPath(import.meta.url));
const REPO_ROOT = resolve(HERE, '..');
Expand Down Expand Up @@ -301,9 +302,25 @@ export async function loadMasker(maskerPath) {
return module.scanSource;
}

/** The parser is loaded lazily so importing this module stays cheap. */
/**
* The parser is loaded lazily so importing this module stays cheap — and through
* the prerequisite thunk, so an uninstalled tree gets a NAMED prerequisite and
* exit 3 instead of a raw `ERR_MODULE_NOT_FOUND` stack and exit 1. A dynamic
* import defers the resolution failure past linking, but it does not change what
* the failure LOOKS like: the rejection reaches the top level unhandled and node
* prints the same node-internals stack with the same exit 1 a finding uses.
*
* ⛔ `requireDependency`, not `requireDefaultExport`: this module wants the
* NAMESPACE (`parser.parse`). `@typescript-eslint/parser` has no default export
* worth reading, and the default-export helper reads `.default` strictly.
*/
async function loadParser() {
const parser = await import('@typescript-eslint/parser');
const parser = await requireDependency(
'@typescript-eslint/parser',
() => import('@typescript-eslint/parser'),
import.meta.url,
{ measures: "`js-comment-mask.mjs` and an independent parser agree on every comment range in the tree" },
);
return (source, options) => parser.parse(source, options);
}

Expand Down
26 changes: 17 additions & 9 deletions scripts/check-exported-any-returns.mts
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,10 @@
// them to the rest of the program — which is why landing this file lowered the
// ledger entry by 54 errors it did not author. See the PR body.

import ts from 'typescript';
// `ts` is the RUNTIME namespace, loaded through the prerequisite thunk below; `TS`
// is the same namespace as TYPES ONLY. `import type` is erased before the module
// graph is linked, so it cannot bring back the `ERR_MODULE_NOT_FOUND` this closes.
import type TS from 'typescript';
import fs from 'node:fs';
import path from 'node:path';
import url from 'node:url';
Expand All @@ -127,6 +130,11 @@ import os from 'node:os';
import { distIsStale } from './check-regen-pending.mjs';
import { isEntrypoint } from './invoked-as.mjs';

import { requireDefaultExport } from './import-prerequisite.mjs';
const ts = await requireDefaultExport('typescript', () => import('typescript'), import.meta.url, {
measures: 'any exported callable of an SDK package resolves to `any`',
});

const HERE = path.dirname(url.fileURLToPath(import.meta.url));
const ROOT = path.resolve(HERE, '..');
const SELF_TEST = process.argv.includes('--self-test');
Expand Down Expand Up @@ -200,7 +208,7 @@ export type ScanResult = {
anyReturns: Map<string, string>;
};

function makeProgram(files: string[], extra: ts.CompilerOptions = {}): ts.Program {
function makeProgram(files: string[], extra: TS.CompilerOptions = {}): TS.Program {
return ts.createProgram(files, {
module: ts.ModuleKind.NodeNext,
moduleResolution: ts.ModuleResolutionKind.NodeNext,
Expand All @@ -224,7 +232,7 @@ function makeProgram(files: string[], extra: ts.CompilerOptions = {}): ts.Progra
* refuses. A named class that IS part of the surface (`RealtimeAPI`,
* `QueryBuilder`) is reached anyway, as its own module export.
*/
export function scan(program: ts.Program, entryFile: string): ScanResult {
export function scan(program: TS.Program, entryFile: string): ScanResult {
const checker = program.getTypeChecker();
const result: ScanResult = { callables: 0, generics: 0, anyReturns: new Map() };

Expand All @@ -236,19 +244,19 @@ export function scan(program: ts.Program, entryFile: string): ScanResult {
);
}

const isAny = (t: ts.Type | undefined): boolean => Boolean(t && t.flags & ts.TypeFlags.Any);
const unalias = (s: ts.Symbol): ts.Symbol =>
const isAny = (t: TS.Type | undefined): boolean => Boolean(t && t.flags & ts.TypeFlags.Any);
const unalias = (s: TS.Symbol): TS.Symbol =>
s.getFlags() & ts.SymbolFlags.Alias ? checker.getAliasedSymbol(s) : s;

/** A `{ … }` type literal — the namespacing shape, not a named data type. */
const isAnonymousObject = (t: ts.Type): boolean => {
const isAnonymousObject = (t: TS.Type): boolean => {
if (!(t.flags & ts.TypeFlags.Object)) return false;
if (!((t as ts.ObjectType).objectFlags & ts.ObjectFlags.Anonymous)) return false;
if (!((t as TS.ObjectType).objectFlags & ts.ObjectFlags.Anonymous)) return false;
const decl = t.getSymbol()?.declarations?.[0];
return Boolean(decl && ts.isTypeLiteralNode(decl));
};

const record = (key: string, sig: ts.Signature): void => {
const record = (key: string, sig: TS.Signature): void => {
result.callables++;
if ((sig.getTypeParameters() ?? []).length > 0) result.generics++;
const ret = checker.getReturnTypeOfSignature(sig);
Expand All @@ -272,7 +280,7 @@ export function scan(program: ts.Program, entryFile: string): ScanResult {
// walk happened to reach first, so the second path's sites are invisible to the
// ratchet and the ledger keys silently depend on walk order. A per-branch set
// still terminates (a cycle must revisit an ancestor) and reports every path.
const walk = (type: ts.Type, path: string, depth: number, ancestors: ReadonlySet<ts.Type>): void => {
const walk = (type: TS.Type, path: string, depth: number, ancestors: ReadonlySet<TS.Type>): void => {
if (depth > 8 || ancestors.has(type)) return;
const branch = new Set(ancestors).add(type);
for (const prop of checker.getPropertiesOfType(type)) {
Expand Down
3 changes: 2 additions & 1 deletion scripts/check-sdui-lockstep.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,8 @@ import { fileURLToPath } from 'node:url';

import { isEntrypoint } from './invoked-as.mjs';
import { parseSourceFile } from './ts-parse.mjs';
import ts from 'typescript';
import { requireDefaultExport } from './import-prerequisite.mjs';
const ts = await requireDefaultExport('typescript', () => import('typescript'), import.meta.url);

const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');

Expand Down
62 changes: 62 additions & 0 deletions scripts/import-prerequisite.d.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
// Types for the two dependency loaders `import-prerequisite.mjs` publishes to
// the gates that import it.
//
// The module itself stays `.mjs` for the reason its three sibling mirrors
// state: `pre-commit` and the gates invoke these scripts with bare `node`, and
// every root script here is authored that way. What needs the declaration is
// the other direction -- a TypeScript-authored gate (`.mts`) importing a loader
// from inside the ROOT tsc program, where an untyped `.mjs` import is TS7016.
// `check-exported-any-returns.mts` is the first such consumer; without this
// file its conversion would have added one error to the `@objectstack/spec-
// monorepo` entry of `check:type-check-debt`, a shrink-only ratchet.
//
// PARTIAL on purpose, the `check-regen-pending.d.mts` shape: the module also
// exports the exit-code constants, the classifier and its self-test, and
// omitting them cannot fail green -- a consumer importing an undeclared name
// gets TS2305, which is loud and immediate. Keep this file in step with the
// module by hand; `check:declaration-mirrors` checks name, kind and required
// arity, never types.
//
// ⛔ The return type is `any` BY MEASUREMENT, not by omission: what these
// return is whatever the loaded package exports, and the 27 `.mjs` call sites
// already bind it untyped. A consumer that wants the real namespace types
// declares them itself -- `check-exported-any-returns.mts` keeps an erased
// `import type TS from 'typescript'` beside the runtime binding for exactly
// that, and a `typescript`-shaped return here would be a lie at every other
// call site (`yaml`, `semver`, `eslint`, `github-slugger`).

/**
* Load a dependency, or print a named `PREREQUISITE NOT MET` diagnosis and exit
* `EXIT_PREREQUISITE_NOT_MET` (3). Resolves to the module NAMESPACE.
*
* @param specifier The bare specifier, e.g. `'@typescript-eslint/parser'`.
* @param load `() => import('@typescript-eslint/parser')`, written in the CALLER
* so the failing resolution is the caller's own.
* @param importerUrl The caller's `import.meta.url`.
* @param options `measures` is what the gate would have judged, in the gate's
* own words -- the one half of the refusal text that is never shared.
*/
export function requireDependency(
specifier: string,
load: () => Promise<unknown>,
importerUrl: string,
options?: { measures?: string },
): Promise<any>;

/**
* `requireDependency` for the DEFAULT export -- the shape `import ts from
* 'typescript'` had. Reads `.default` strictly rather than falling back to the
* namespace.
*
* @param specifier The bare specifier, e.g. `'typescript'`.
* @param load `() => import('typescript')`, written in the CALLER.
* @param importerUrl The caller's `import.meta.url`.
* @param options `measures` is what the gate would have judged, in the gate's
* own words.
*/
export function requireDefaultExport(
specifier: string,
load: () => Promise<unknown>,
importerUrl: string,
options?: { measures?: string },
): Promise<any>;
3 changes: 2 additions & 1 deletion scripts/isystem-census.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,8 @@ import { execFileSync } from 'node:child_process';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';

import ts from 'typescript';
import { requireDefaultExport } from './import-prerequisite.mjs';
const ts = await requireDefaultExport('typescript', () => import('typescript'), import.meta.url);

import { isEntrypoint } from './invoked-as.mjs';
import { parseSourceFile } from './ts-parse.mjs';
Expand Down
3 changes: 2 additions & 1 deletion scripts/measure-durability-swallow-family.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -256,7 +256,8 @@
import { readFileSync, readdirSync, statSync } from 'node:fs';
import { join, relative, sep } from 'node:path';
import { fileURLToPath } from 'node:url';
import ts from 'typescript';
import { requireDefaultExport } from './import-prerequisite.mjs';
const ts = await requireDefaultExport('typescript', () => import('typescript'), import.meta.url);
import { parseSourceFile } from './ts-parse.mjs';

const ROOT = fileURLToPath(new URL('..', import.meta.url));
Expand Down
3 changes: 2 additions & 1 deletion scripts/tenant-audit-census.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,8 @@ import { execFileSync } from 'node:child_process';
import { join } from 'node:path';
import { fileURLToPath } from 'node:url';

import ts from 'typescript';
import { requireDefaultExport } from './import-prerequisite.mjs';
const ts = await requireDefaultExport('typescript', () => import('typescript'), import.meta.url);

import { isEntrypoint } from './invoked-as.mjs';
import { parseSourceFile } from './ts-parse.mjs';
Expand Down
Loading