moduleShape

tcmts.cli.npm.moduleShape · #c64hpm25 · term · runs on the server

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

used by