tscircuit/ti

ti tscircuit library

Version
1.0.198
License
unset
Stars
0

system-block-ui/src/rendering/local-ti-package-files.ts

import type { SubcircuitDefinition } from "../model";

const TI_PACKAGE_NAME = "@tsci/tscircuit.ti";
const TI_PACKAGE_ROOT = `node_modules/${TI_PACKAGE_NAME}`;

type RawSourceModules = Readonly<Record<string, string>>;

const discoverLocalTiSources = (): RawSourceModules => {
  try {
    // Vite turns the repository sources into strings in a lazy render chunk.
    // Bun tests do not implement import.meta.glob, so callers can inject the
    // same map when exercising this module outside Vite.
    return {
      ...(import.meta.glob("../../../lib/**/*.ts", {
        eager: true,
        import: "default",
        query: "?raw",
      }) as RawSourceModules),
      ...(import.meta.glob("../../../lib/**/*.tsx", {
        eager: true,
        import: "default",
        query: "?raw",
      }) as RawSourceModules),
      ...(import.meta.glob("../../../imports/**/*.ts", {
        eager: true,
        import: "default",
        query: "?raw",
      }) as RawSourceModules),
      ...(import.meta.glob("../../../imports/**/*.tsx", {
        eager: true,
        import: "default",
        query: "?raw",
      }) as RawSourceModules),
    };
  } catch {
    return {};
  }
};

const normalizePath = (value: string): string => {
  const result: string[] = [];
  for (const part of value.replace(/\\/g, "/").split("/")) {
    if (!part || part === ".") continue;
    if (part === "..") {
      result.pop();
      continue;
    }
    result.push(part);
  }
  return result.join("/");
};

const repositorySourcePath = (globPath: string): string => {
  const normalized = globPath.replace(/\\/g, "/");
  for (const marker of ["/lib/", "/imports/"]) {
    const markerIndex = normalized.lastIndexOf(marker);
    if (markerIndex >= 0) {
      return normalizePath(normalized.slice(markerIndex + 1));
    }
  }
  return normalizePath(normalized);
};

const sourceByRepositoryPath = (
  sourceModules: RawSourceModules,
): ReadonlyMap<string, string> =>
  new Map(
    Object.entries(sourceModules).map(([path, source]) => [
      repositorySourcePath(path),
      source,
    ]),
  );

const LOCAL_IMPORT_PATTERN =
  /(?:import|export)\s+(?:type\s+)?(?:[^"']*?\s+from\s+)?["'](\.{1,2}\/[^"']+)["']/g;

const getRelativeImports = (source: string): readonly string[] =>
  [...source.matchAll(LOCAL_IMPORT_PATTERN)].map((match) => match[1]);

const resolveLocalImport = (
  fromPath: string,
  specifier: string,
  sources: ReadonlyMap<string, string>,
): string | undefined => {
  const slash = fromPath.lastIndexOf("/");
  const directory = slash >= 0 ? fromPath.slice(0, slash) : "";
  const base = normalizePath(`${directory}/${specifier}`);
  const withoutJsExtension = base.replace(/\.(?:m?js|jsx)$/, "");
  const candidates = [
    base,
    `${base}.ts`,
    `${base}.tsx`,
    `${withoutJsExtension}.ts`,
    `${withoutJsExtension}.tsx`,
    `${base}/index.ts`,
    `${base}/index.tsx`,
  ];
  return candidates.find((candidate) => sources.has(candidate));
};

const collectDependencyClosure = (
  entryPaths: readonly string[],
  sources: ReadonlyMap<string, string>,
): ReadonlyMap<string, string> => {
  const collected = new Map<string, string>();
  const queue = entryPaths.map(normalizePath);

  while (queue.length > 0) {
    const sourcePath = queue.shift();
    if (!sourcePath || collected.has(sourcePath)) continue;
    const source = sources.get(sourcePath);
    if (source === undefined) {
      throw new Error(`Local TI source is unavailable: ${sourcePath}`);
    }
    collected.set(sourcePath, source);

    for (const specifier of getRelativeImports(source)) {
      const dependency = resolveLocalImport(sourcePath, specifier, sources);
      if (!dependency) {
        throw new Error(
          `Local TI dependency ${specifier} imported by ${sourcePath} is unavailable.`,
        );
      }
      if (!collected.has(dependency)) queue.push(dependency);
    }
  }

  return collected;
};

export type LocalTiSubcircuitDefinition = Pick<
  SubcircuitDefinition,
  "componentName" | "sourcePath"
>;

/**
 * Build a minimal virtual @tsci/tscircuit.ti package containing the selected
 * local subcircuits and their relative source dependencies.
 */
export const createLocalTiPackageEvaluationFsMap = (
  definitions: readonly LocalTiSubcircuitDefinition[],
  sourceModules: RawSourceModules = discoverLocalTiSources(),
): Readonly<Record<string, string>> => {
  const uniqueDefinitions = [
    ...new Map(
      definitions.map((definition) => [definition.componentName, definition]),
    ).values(),
  ].sort((left, right) =>
    left.componentName < right.componentName
      ? -1
      : left.componentName > right.componentName
        ? 1
        : 0,
  );
  if (uniqueDefinitions.length === 0) return {};

  const sources = sourceByRepositoryPath(sourceModules);
  const dependencyClosure = collectDependencyClosure(
    uniqueDefinitions.map(({ sourcePath }) => sourcePath),
    sources,
  );
  const packageIndex = uniqueDefinitions
    .map(
      ({ componentName, sourcePath }) =>
        `export { ${componentName} } from "./${normalizePath(sourcePath)}"`,
    )
    .join("\n");

  return {
    [`${TI_PACKAGE_ROOT}/package.json`]: JSON.stringify({
      name: TI_PACKAGE_NAME,
      type: "module",
      main: "index.ts",
      module: "index.ts",
    }),
    [`${TI_PACKAGE_ROOT}/index.ts`]: `${packageIndex}\n`,
    ...Object.fromEntries(
      [...dependencyClosure].map(([sourcePath, source]) => [
        `${TI_PACKAGE_ROOT}/${sourcePath}`,
        source,
      ]),
    ),
  };
};