moduleShape
uses tcmts.core.ts.parse, which uses tcmts.core.ts.compiler, which uses process
A module's shape (ModuleShape), from its code.
import { blockNames, bound, emptyUnit, hoisted, literalText, merge, unitOf } from "#codebase/tcmts/cli/npm"; import type { ModuleShape } from "#codebase/tcmts/cli/npm"; import { forEachChild, parse } from "#codebase/tcmts/core/ts"; import { isBindingElement, isCallExpression, isClassDeclaration, isExportAssignment, isExportDeclaration, isFunctionDeclaration, isIdentifier, isImportDeclaration, isNamedExports, isNamedImports, isNamespaceExport, isNamespaceImport, isPropertyAccessExpression, isVariableStatement, SyntaxKind, } from "typescript/ast"; import type { Node, SourceFile } from "typescript/ast"; export function moduleShape(file: string, text: string): ModuleShape { const sf: SourceFile = parse(file.replace(/\.(c|m)?js$/, ".$1js"), text); const shape: ModuleShape = { units: {}, top: emptyUnit(), exports: {}, stars: [], cjs: false }; // the module's own names, and the bindings it imports const top = new Set<string>(); const imported = new Map<string, { spec: string; name: string }>(); blockNames(sf.statements, top); hoisted(sf, top); // the top-level bindings something changes: assigned again, or (what they hold) assigned into, deleted from, or // changed by a method that changes it in place const changed = new Set<string>(); const MUTATORS = new Set([ "push", "pop", "shift", "unshift", "splice", "sort", "reverse", "fill", "copyWithin", "set", "add", "delete", "clear", ]); const base = (n: Node): string | undefined => { while (isPropertyAccessExpression(n) || n.kind === SyntaxKind.ElementAccessExpression) n = (n as unknown as { expression: Node }).expression; return isIdentifier(n) ? n.text : undefined; }; const watch = (n: Node): void => { const k = n.kind; if (k === SyntaxKind.BinaryExpression) { const b = n as unknown as { left: Node; operatorToken: Node }; const op = b.operatorToken.kind; if (op >= SyntaxKind.FirstAssignment && op <= SyntaxKind.LastAssignment) { const name = base(b.left); if (name) changed.add(name); } } else if (k === SyntaxKind.PrefixUnaryExpression || k === SyntaxKind.PostfixUnaryExpression) { const u = n as unknown as { operand: Node; operator: SyntaxKind }; if (u.operator === SyntaxKind.PlusPlusToken || u.operator === SyntaxKind.MinusMinusToken) { const name = base(u.operand); if (name) changed.add(name); } } else if (k === SyntaxKind.DeleteExpression) { const name = base((n as unknown as { expression: Node }).expression); if (name) changed.add(name); } else if ( isCallExpression(n) && isPropertyAccessExpression(n.expression) && MUTATORS.has(n.expression.name.text) ) { const name = base(n.expression.expression); if (name) changed.add(name); } forEachChild(n, watch); }; watch(sf); const runs: Node[] = []; for (const s of sf.statements) { if (isImportDeclaration(s)) { const spec = literalText(s.moduleSpecifier)!; const clause = s.importClause; if (!clause) shape.top.imports.push({ spec, name: "" }); if (clause?.name) imported.set(clause.name.text, { spec, name: "default" }); const bindings = clause?.namedBindings; if (bindings && isNamespaceImport(bindings)) imported.set(bindings.name.text, { spec, name: "*" }); if (bindings && isNamedImports(bindings)) for (const e of bindings.elements) imported.set(e.name.text, { spec, name: (e.propertyName ?? e.name).text }); } } for (const s of sf.statements) { const exported = !!(s as unknown as { modifiers?: readonly Node[] }).modifiers?.some( (m) => m.kind === SyntaxKind.ExportKeyword, ); const isDefault = !!(s as unknown as { modifiers?: readonly Node[] }).modifiers?.some( (m) => m.kind === SyntaxKind.DefaultKeyword, ); if (isImportDeclaration(s)) continue; if (isExportDeclaration(s)) { const spec = literalText(s.moduleSpecifier); const clause = s.exportClause; if (!clause && spec) shape.stars.push(spec); else if (clause && isNamespaceExport(clause) && spec) shape.exports[clause.name.text] = { from: { spec, name: "*" } }; else if (clause && isNamedExports(clause)) for (const e of clause.elements) { const local = (e.propertyName ?? e.name).text; shape.exports[e.name.text] = spec ? { from: { spec, name: local } } : imported.has(local) ? { from: imported.get(local)! } : { local }; } continue; } if (isExportAssignment(s)) { shape.units.default = unitOf(s.expression, top, imported); shape.exports.default = { local: "default" }; continue; } if (isVariableStatement(s)) { for (const d of s.declarationList.declarations) { const u = unitOf(d, top, imported); // state: something changes it (assigns it again, or changes what it holds) u.state = bound(d.name).some((b) => changed.has(b)); for (const b of bound(d.name)) { shape.units[b] = u; if (exported) shape.exports[b] = { local: b }; } } continue; } if ((isFunctionDeclaration(s) || isClassDeclaration(s)) && (s.name || isDefault)) { const name = s.name?.text ?? "default"; shape.units[name] = unitOf(s, top, imported); if (exported) shape.exports[isDefault ? "default" : name] = { local: name }; continue; } runs.push(s); } // what running it does: its top-level statements (and the initializers of its constants, which run with it) for (const r of runs) merge(shape.top, unitOf(r, top, imported)); // CommonJS: read whole, as one; its top-level `const x = require("m")` (or `{ a } = require("m")`) are imports const required = new Map<string, { spec: string; name: string }>(); for (const s of sf.statements) if (isVariableStatement(s)) for (const d of s.declarationList.declarations) { const init = d.initializer; if ( !init || !isCallExpression(init) || !isIdentifier(init.expression) || init.expression.text !== "require" ) continue; const spec = literalText(init.arguments[0]); if (spec === undefined) continue; if (isIdentifier(d.name)) required.set(d.name.text, { spec, name: "*" }); else for (const e of (d.name as unknown as { elements: readonly Node[] }).elements) { const n = isBindingElement(e) ? e.name : undefined; if (!n || !isIdentifier(n)) continue; const p = (e as unknown as { propertyName?: Node }).propertyName; required.set(n.text, { spec, name: p && isIdentifier(p) ? p.text : n.text }); } } const whole = unitOf(sf, new Set([...top].filter((n) => !required.has(n))), required); if ( whole.globals.some((g) => g === "module" || g === "exports") || (whole.imports.length && whole.globals.includes("require")) ) { shape.cjs = true; // (its own `exports` and `module.exports` are how it exports, not state) whole.state = [...top].some((n) => changed.has(n)); shape.top = whole; } return shape; }
uses
- tcmts.cli.npm.ModuleShape
- tcmts.cli.npm.blockNames
- tcmts.cli.npm.bound
- tcmts.cli.npm.emptyUnit
- tcmts.cli.npm.hoisted
- tcmts.cli.npm.literalText
- tcmts.cli.npm.merge
- tcmts.cli.npm.unitOf
- tcmts.core.ts.forEachChild
- tcmts.core.ts.parse