Ctrl K
ComunidadGuías…

desktop/test-loader-hooks.mjs

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
import { fileURLToPath, pathToFileURL } from "node:url";
import fs from "node:fs";
import path from "node:path";
import ts from "typescript";

const srcRoot = path.resolve(
  path.dirname(fileURLToPath(import.meta.url)),
  "src",
);

const repoRoot = path.resolve(
  path.dirname(fileURLToPath(import.meta.url)),
  "..",
);

// `nextResolve` requires specifiers to be URLs or relative paths. Passing an
// absolute filesystem path happens to work on POSIX (node coerces it), but on
// Windows an absolute path like `C:\...` parses as a URL with protocol `c:`
// and every test run dies with ERR_UNSUPPORTED_ESM_URL_SCHEME. Hand absolute
// paths to node as proper file:// URLs on all platforms.
function toFileSpecifier(candidatePath) {
  return path.isAbsolute(candidatePath)
    ? pathToFileURL(candidatePath).href
    : candidatePath;
}

function resolveSourcePath(basePath) {
  // Existence decides, not path.extname — a dotted basename like
  // `ProfileAvatarEditor.utils` (→ .utils.ts on disk) looks like an
  // extension but still needs resolving.
  if (fs.existsSync(basePath) && fs.statSync(basePath).isFile()) {
    return basePath;
  }

  for (const extension of [".ts", ".tsx", ".js", ".jsx", ".mjs"]) {
    const candidate = `${basePath}${extension}`;
    if (fs.existsSync(candidate)) {
      return candidate;
    }
  }

  for (const extension of [".ts", ".tsx", ".js", ".jsx", ".mjs"]) {
    const candidate = path.join(basePath, `index${extension}`);
    if (fs.existsSync(candidate)) {
      return candidate;
    }
  }

  return null;
}

// emoji-mart ships a bundled CJS main that node's cjs-module-lexer cannot
// extract named exports from (`import { init } from "emoji-mart"` throws
// under node ESM even though the bundler handles it). Tests never exercise
// the picker, so serve inert stubs for the emoji-mart entrypoints.
const stubModules = new Map([
  [
    "emoji-mart",
    "export const init = (...args) => globalThis.__BUZZ_TEST_EMOJI_MART_INIT__?.(...args);\n" +
      "export const SearchIndex = { search: async () => [] };\n" +
      "export default {};\n",
  ],
  ["@emoji-mart/react", "export default function Picker() { return null; }\n"],
]);

const STUB_URL_PREFIX = "buzz-test-stub:";

// Vite resolves asset imports (`./logo.png`, `./logo.png?inline`) to a URL or
// base64 string at bundle time; node's ESM resolver has no such loader and
// throws on the query suffix. Serve an inert string so components that embed
// assets stay unit-testable.
const ASSET_SPECIFIER = /\.(?:png|jpe?g|gif|svg|webp|avif|ico)(?:\?[^/]*)?$/;
const ASSET_URL_PREFIX = "buzz-test-asset:";

export function resolve(specifier, context, nextResolve) {
  if (ASSET_SPECIFIER.test(specifier)) {
    return {
      shortCircuit: true,
      url: `${ASSET_URL_PREFIX}${specifier}`,
    };
  }
  if (stubModules.has(specifier)) {
    return {
      shortCircuit: true,
      url: `${STUB_URL_PREFIX}${specifier}`,
    };
  }
  if (specifier === "@features-manifest") {
    const resolved = path.join(repoRoot, "preview-features.json");
    return nextResolve(toFileSpecifier(resolved), context);
  }
  if (specifier === "@protected-features") {
    const variant =
      process.env.VITE_BUZZ_BESTIE === "1" ? "internal.ts" : "public.ts";
    const resolved = path.join(srcRoot, "protectedFeatures", variant);
    return nextResolve(toFileSpecifier(resolved), context);
  }
  if (specifier === "@protected-feature-components") {
    const variant =
      process.env.VITE_BUZZ_BESTIE === "1" ? "internalUi.ts" : "publicUi.ts";
    const resolved = path.join(srcRoot, "protectedFeatures", variant);
    return nextResolve(toFileSpecifier(resolved), context);
  }
  if (specifier === "@model-capabilities-manifest") {
    const resolved = path.join(repoRoot, "scripts", "model-capabilities.json");
    return nextResolve(toFileSpecifier(resolved), context);
  }
  if (specifier.startsWith("@/")) {
    const stripped = specifier.slice(2);
    // Preserve explicit extensions (.mjs, .js, .json, .ts, etc.). The bundler
    // tolerates extensionless `@/` imports for source files; node's ESM
    // resolver does not, so resolve against the extensions the app uses.
    // Otherwise paths like `@/.../foo.mjs` would be coerced into `foo.mjs.ts`
    // and fail to resolve.
    const resolved = resolveSourcePath(`${srcRoot}/${stripped}`);
    return nextResolve(
      toFileSpecifier(resolved ?? `${srcRoot}/${stripped}`),
      context,
    );
  }
  // Resolve extensionless relative TS imports (e.g. `./parseImeta`) — the app's
  // bundler adds the extension, but node's ESM resolver does not. Without this,
  // any .ts that relative-imports a sibling .ts can't be imported from a test,
  // which previously forced stale inlined copies of the source under test.
  // Dotted basenames (`./ProfileAvatarEditor.utils`) look like extensions to
  // path.extname, so resolveSourcePath existence-checks instead.
  if (
    (specifier.startsWith("./") || specifier.startsWith("../")) &&
    context.parentURL?.startsWith("file:")
  ) {
    const parentPath = fileURLToPath(context.parentURL);
    const resolved = resolveSourcePath(
      path.resolve(path.dirname(parentPath), specifier),
    );
    if (resolved) {
      return nextResolve(toFileSpecifier(resolved), context);
    }
    return nextResolve(specifier, context);
  }
  return nextResolve(specifier, context);
}

export async function load(url, context, nextLoad) {
  if (url.startsWith(ASSET_URL_PREFIX)) {
    return {
      format: "module",
      shortCircuit: true,
      source: 'export default "test-asset";\n',
    };
  }

  if (url.startsWith(STUB_URL_PREFIX)) {
    return {
      format: "module",
      shortCircuit: true,
      source: stubModules.get(url.slice(STUB_URL_PREFIX.length)) ?? "",
    };
  }

  // The app bundler loads .json imports without attributes (e.g. the bare
  // `@emoji-mart/data` entrypoint); node's ESM resolver requires
  // `with { type: "json" }` on every hop. Serve json here so transitive
  // imports from source under test don't need bundler-only semantics.
  if (url.endsWith(".json")) {
    return {
      format: "json",
      shortCircuit: true,
      source: fs.readFileSync(fileURLToPath(url), "utf8"),
    };
  }

  // Vite handles side-effect CSS imports (e.g. `import "./card-texture.css"`
  // in shared/ui) at bundle time; node's ESM loader has no CSS support. Serve
  // them as empty modules so components with style imports stay unit-testable.
  if (url.endsWith(".css")) {
    return {
      format: "module",
      shortCircuit: true,
      source: "",
    };
  }

  if (url.endsWith(".tsx")) {
    const source = fs.readFileSync(fileURLToPath(url), "utf8");
    const transpiled = ts.transpileModule(source, {
      compilerOptions: {
        jsx: ts.JsxEmit.ReactJSX,
        module: ts.ModuleKind.ESNext,
        target: ts.ScriptTarget.ES2020,
      },
      fileName: fileURLToPath(url),
    });

    return {
      format: "module",
      shortCircuit: true,
      source: transpiled.outputText,
    };
  }

  return nextLoad(url, context);
}