importClassifier

tcmts.core.analyze.importClassifier · #kaabkyya · term · runs on the server

uses node:path

import {
  externalModule, isServerHostFile, SERVER_API, SERVER_HOST, TCM_REACT_GONE, TCM_SERVER_GONE,
} from "#codebase/tcmts/core/analyze";
import type { AnalyzeHost } from "#codebase/tcmts/core/analyze";
import type { Resolved } from "#codebase/tcmts/core/canonicalize";
import { TcmError } from "#codebase/tcmts/core/extract";
import { hashFromExportId } from "#codebase/tcmts/core/hash";
import { REACT_MODULE, SERVER_HOST_MODULE } from "#codebase/tcmts/core/types";
import path_2 from "node:path";
import { isImportClause as ts_isImportClause, isImportSpecifier as ts_isImportSpecifier } from "typescript/ast";
import type {
  ImportClause as ts_ImportClause, ImportDeclaration as ts_ImportDeclaration, ImportSpecifier as ts_ImportSpecifier,
  NamespaceImport as ts_NamespaceImport, Node as ts_Node, StringLiteral as ts_StringLiteral,
} from "typescript/ast";

export function importClassifier(file: string, host: AnalyzeHost) {
  const cache = new Map<string, Resolved>();
  const ids = new Map<ts_Node, number>();
  return (
    d: ts_ImportSpecifier | ts_ImportClause | ts_NamespaceImport,
    meaning: "type" | "value",
  ): Resolved => {
    if (!ids.has(d)) ids.set(d, ids.size);
    const key = `${ids.get(d)} ${meaning}`;
    const hit = cache.get(key);
    if (hit) return hit;
    const decl = ts_isImportSpecifier(d)
      ? d.parent.parent.parent
      : ts_isImportClause(d)
        ? d.parent
        : d.parent.parent;
    const spec = ((decl as ts_ImportDeclaration).moduleSpecifier as ts_StringLiteral).text;
    const exportName = ts_isImportSpecifier(d)
      ? (d.propertyName ?? d.name).text
      : ts_isImportClause(d)
        ? "default"
        : "*";
    let r: Resolved;
    if (spec === SERVER_API) {
      throw new TcmError(TCM_SERVER_GONE, d);
    } else if (spec === SERVER_HOST || isServerHostFile(spec, file, host)) {
      r = { kind: "ext", ext: { module: SERVER_HOST_MODULE, subpath: "", export: exportName } };
    } else if (spec === "#tcm/react") {
      throw new TcmError(TCM_REACT_GONE, d);
    } else if (
      /^\.\.?\//.test(spec) &&
      path_2.resolve(path_2.dirname(file), spec) === path_2.join(host.cacheDir, "tcm-cache.js")
    ) {
      // React's cache and cacheSignal, as a cache module imports them (tcm's: see `loader.render`): React's, as written
      r = { kind: "ext", ext: { module: REACT_MODULE, subpath: "", export: exportName } };
    } else if (spec === "#codebase" || spec.startsWith("#codebase/")) {
      if (exportName === "*" || exportName === "default") {
        throw new TcmError(
          'import codebase definitions by name: `import { sort } from "#codebase/lib"`',
          d,
        );
      }
      const ns = spec.slice("#codebase".length).replace(/^\//, "").replaceAll("/", ".");
      try {
        r = {
          kind: "hash",
          hash: host.resolveName(ns ? `${ns}.${exportName}` : exportName, meaning),
        };
      } catch (e) {
        throw new TcmError((e as Error).message, d);
      }
    } else if (spec.startsWith(".") || spec.startsWith("/")) {
      const abs = path_2.resolve(path_2.dirname(file), spec);
      const hash = hashFromExportId(exportName);
      if (path_2.dirname(abs) !== host.cacheDir || !hash) {
        throw new TcmError(
          `cannot import local file '${spec}': add it to the codebase and import it from '#codebase/...'`,
          d,
        );
      }
      r = { kind: "hash", hash };
    } else {
      r = { kind: "ext", ext: externalModule(spec, file, d, exportName, host) };
    }
    cache.set(key, r);
    return r;
  };
}

uses

used by