import { findAppRoot } from "./appResolver.js";
import { registerEntityIds } from "../encryption/entityIds.js";
import { createLogger } from "../logger/index.js";
import {
  ensureMikroOrmV7GeneratedCacheCompatibility,
  recoverMikroOrmV7GeneratedCacheFromImportError
} from "./generatedCacheRecovery.js";
import { CLIENT_ONLY_STUB_NAMESPACE, createClientOnlyStubPlugin } from "./clientOnlyModules.js";
import path from "node:path";
import fs from "node:fs";
import crypto from "node:crypto";
import { createRequire } from "node:module";
import { pathToFileURL } from "node:url";
let activeBootstrapLoads = 0;
let esbuildRuntime = null;
let esbuildStopPromise = null;
const logger = createLogger("shared").child({ component: "bootstrap" });
async function getEsbuildRuntime() {
  if (esbuildStopPromise) await esbuildStopPromise;
  if (esbuildRuntime) return esbuildRuntime;
  const loadedRuntime = await import("esbuild");
  esbuildRuntime ??= loadedRuntime;
  return esbuildRuntime;
}
async function withEsbuildLifecycle(load) {
  activeBootstrapLoads += 1;
  try {
    return await load();
  } finally {
    activeBootstrapLoads -= 1;
    if (activeBootstrapLoads === 0 && esbuildRuntime) {
      const runtimeToStop = esbuildRuntime;
      esbuildRuntime = null;
      const stopPromise = runtimeToStop.stop().catch((err) => {
        logger.warn("Failed to stop the bootstrap compiler service", { err });
      });
      esbuildStopPromise = stopPromise;
      try {
        await stopPromise;
      } finally {
        if (esbuildStopPromise === stopPromise) esbuildStopPromise = null;
      }
    }
  }
}
class GeneratedFileNotFoundError extends Error {
  constructor(filePath) {
    super(`Generated file not found: ${filePath}`);
    this.name = "GeneratedFileNotFoundError";
    this.filePath = filePath;
  }
}
function createCliBundlePlugins(appRoot) {
  const aliasPlugin = {
    name: "alias-resolver",
    setup(build) {
      build.onResolve({ filter: /^@\// }, (args) => {
        const rest = args.path.slice("@/".length);
        const bases = rest.startsWith(".mercato/") ? [path.join(appRoot, rest)] : [path.join(appRoot, "src", rest), path.join(appRoot, rest)];
        for (const base of bases) {
          if (fs.existsSync(base) && fs.statSync(base).isFile()) {
            return { path: base };
          }
          for (const suffix of [".ts", ".tsx", "/index.ts", "/index.tsx"]) {
            if (fs.existsSync(base + suffix)) {
              return { path: base + suffix };
            }
          }
        }
        return { path: path.join(appRoot, rest) };
      });
    }
  };
  const externalNonJsonPlugin = {
    name: "external-non-json",
    setup(build) {
      build.onResolve({ filter: /^[^./]/ }, (args) => {
        if (/^[a-zA-Z]:/.test(args.path)) {
          return null;
        }
        if (args.path.endsWith(".json")) {
          return null;
        }
        return { path: args.path, external: true };
      });
    }
  };
  return [createClientOnlyStubPlugin(), aliasPlugin, externalNonJsonPlugin];
}
const DYNAMIC_LOADER_CACHE_VERSION = 4;
function cacheMetadataPath(jsPath) {
  return `${jsPath}.cache.json`;
}
function contentHash(content) {
  return crypto.createHash("sha256").update(content).digest("hex");
}
function parseJsonConfig(content) {
  let normalized = "";
  let inString = false;
  let escaped = false;
  for (let index = 0; index < content.length; index += 1) {
    const character = content[index];
    const nextCharacter = content[index + 1];
    if (inString) {
      normalized += character;
      if (escaped) {
        escaped = false;
      } else if (character === "\\") {
        escaped = true;
      } else if (character === '"') {
        inString = false;
      }
      continue;
    }
    if (character === '"') {
      inString = true;
      normalized += character;
      continue;
    }
    if (character === "/" && nextCharacter === "/") {
      while (index < content.length && content[index] !== "\n") index += 1;
      normalized += "\n";
      continue;
    }
    if (character === "/" && nextCharacter === "*") {
      index += 2;
      while (index < content.length && !(content[index] === "*" && content[index + 1] === "/")) {
        index += 1;
      }
      index += 1;
      continue;
    }
    if (character === ",") {
      let lookahead = index + 1;
      while (lookahead < content.length && /\s/.test(content[lookahead])) lookahead += 1;
      if (content[lookahead] === "}" || content[lookahead] === "]") continue;
    }
    normalized += character;
  }
  return JSON.parse(normalized);
}
function resolveExistingConfigPath(candidate) {
  for (const configPath of [candidate, `${candidate}.json`, path.join(candidate, "tsconfig.json")]) {
    if (fs.existsSync(configPath) && fs.statSync(configPath).isFile()) return configPath;
  }
  return null;
}
function resolvePackageConfig(configPath, reference) {
  try {
    const resolved = createRequire(pathToFileURL(configPath)).resolve(reference);
    return path.extname(resolved) === ".json" ? resolved : null;
  } catch {
    return null;
  }
}
function resolveExtendedConfig(configPath, reference) {
  if (path.isAbsolute(reference) || reference.startsWith(".")) {
    const resolved = resolveExistingConfigPath(path.resolve(path.dirname(configPath), reference));
    if (resolved) return resolved;
  } else {
    for (const packageReference of [reference, `${reference}/tsconfig.json`]) {
      const resolved = resolvePackageConfig(configPath, packageReference);
      if (resolved) return resolved;
    }
  }
  throw new Error(`[internal] TypeScript config extends target not found: ${reference}`);
}
function collectTsconfigPaths(entryPath, visited = /* @__PURE__ */ new Set()) {
  const configPath = path.resolve(entryPath);
  if (visited.has(configPath)) return [];
  visited.add(configPath);
  const parsed = parseJsonConfig(fs.readFileSync(configPath, "utf8"));
  if (typeof parsed !== "object" || parsed === null || !("extends" in parsed)) return [configPath];
  const extendsValue = parsed.extends;
  const references = typeof extendsValue === "string" ? [extendsValue] : Array.isArray(extendsValue) && extendsValue.every((value) => typeof value === "string") ? extendsValue : [];
  return [
    ...references.flatMap((reference) => collectTsconfigPaths(
      resolveExtendedConfig(configPath, reference),
      visited
    )),
    configPath
  ];
}
function hashFilesRelativeTo(appRoot, filePaths) {
  return Object.fromEntries(filePaths.map((filePath) => [
    path.relative(appRoot, filePath).split(path.sep).join("/"),
    contentHash(fs.readFileSync(filePath))
  ]));
}
function cacheInputHash(tsPath, appRoot, tsconfigPaths) {
  const hash = crypto.createHash("sha256");
  hash.update(JSON.stringify({
    version: DYNAMIC_LOADER_CACHE_VERSION,
    sourceHash: contentHash(fs.readFileSync(tsPath)),
    tsconfigHashes: hashFilesRelativeTo(appRoot, tsconfigPaths)
  }));
  return hash.digest("hex");
}
function dependenciesAreValid(appRoot, dependencies) {
  return Object.entries(dependencies).every(([relativePath, expectedHash]) => {
    const dependencyPath = path.resolve(appRoot, relativePath);
    return fs.existsSync(dependencyPath) && contentHash(fs.readFileSync(dependencyPath)) === expectedHash;
  });
}
function collectDependencyHashes(appRoot, inputs) {
  return Object.fromEntries(
    Object.keys(inputs).filter((inputPath) => !inputPath.startsWith(`${CLIENT_ONLY_STUB_NAMESPACE}:`)).map((inputPath) => {
      const absolutePath = path.isAbsolute(inputPath) ? inputPath : path.resolve(appRoot, inputPath);
      const relativePath = path.relative(appRoot, absolutePath).split(path.sep).join("/");
      return [relativePath, contentHash(fs.readFileSync(absolutePath))];
    }).sort(([left], [right]) => left.localeCompare(right))
  );
}
function readCacheMetadata(metadataPath) {
  try {
    const parsed = JSON.parse(fs.readFileSync(metadataPath, "utf8"));
    if (typeof parsed === "object" && parsed !== null && "version" in parsed && parsed.version === DYNAMIC_LOADER_CACHE_VERSION && "inputHash" in parsed && typeof parsed.inputHash === "string" && "outputHash" in parsed && typeof parsed.outputHash === "string" && "dependencies" in parsed && typeof parsed.dependencies === "object" && parsed.dependencies !== null && Object.values(parsed.dependencies).every((hash) => typeof hash === "string")) {
      return {
        version: parsed.version,
        inputHash: parsed.inputHash,
        outputHash: parsed.outputHash,
        dependencies: parsed.dependencies
      };
    }
  } catch {
    return null;
  }
  return null;
}
function cacheIsValid(appRoot, jsPath, metadataPath, expectedInputHash) {
  if (!fs.existsSync(jsPath)) return false;
  const metadata = readCacheMetadata(metadataPath);
  if (!metadata || metadata.inputHash !== expectedInputHash) return false;
  return contentHash(fs.readFileSync(jsPath)) === metadata.outputHash && dependenciesAreValid(appRoot, metadata.dependencies);
}
async function compileAppSourceFile(tsPath, options) {
  return withEsbuildLifecycle(() => compileAppSourceFileWithActiveEsbuild(tsPath, options));
}
async function compileAppSourceFileWithActiveEsbuild(tsPath, options) {
  const { appRoot, outFile } = options;
  const format = options.format ?? "esm";
  const appTsconfig = path.join(appRoot, "tsconfig.json");
  const metadataPath = cacheMetadataPath(outFile);
  const tsExists = fs.existsSync(tsPath);
  const tsconfigExists = fs.existsSync(appTsconfig);
  if (!tsExists) {
    throw new GeneratedFileNotFoundError(tsPath);
  }
  if (!tsconfigExists) {
    throw new Error(`App TypeScript config not found: ${appTsconfig}`);
  }
  const tsconfigPaths = collectTsconfigPaths(appTsconfig);
  const expectedInputHash = cacheInputHash(tsPath, appRoot, tsconfigPaths);
  if (cacheIsValid(appRoot, outFile, metadataPath, expectedInputHash)) {
    return outFile;
  }
  fs.mkdirSync(path.dirname(outFile), { recursive: true });
  const esbuild = await getEsbuildRuntime();
  const result = await esbuild.build({
    entryPoints: [tsPath],
    outfile: outFile,
    absWorkingDir: appRoot,
    bundle: true,
    metafile: true,
    format,
    platform: "node",
    target: "node18",
    tsconfig: appTsconfig,
    plugins: createCliBundlePlugins(appRoot),
    // Allow JSON imports
    loader: { ".json": "json" }
  });
  const metadata = {
    version: DYNAMIC_LOADER_CACHE_VERSION,
    inputHash: expectedInputHash,
    outputHash: contentHash(fs.readFileSync(outFile)),
    dependencies: {
      ...collectDependencyHashes(appRoot, result.metafile.inputs),
      ...hashFilesRelativeTo(appRoot, tsconfigPaths)
    }
  };
  fs.writeFileSync(metadataPath, JSON.stringify(metadata));
  return outFile;
}
async function compileAndImport(tsPath, options = {}) {
  const allowRecovery = options.allowRecovery ?? true;
  const jsPath = options.outFile ?? tsPath.replace(/\.ts$/, ".mjs");
  const appRoot = options.appRoot ?? path.dirname(path.dirname(path.dirname(tsPath)));
  await compileAppSourceFile(tsPath, { appRoot, outFile: jsPath });
  try {
    const outputHash = contentHash(fs.readFileSync(jsPath));
    const fileUrl = `${pathToFileURL(jsPath).href}?cache=${outputHash}`;
    return await import(fileUrl);
  } catch (error) {
    if (!allowRecovery) {
      throw error;
    }
    const recovered = recoverMikroOrmV7GeneratedCacheFromImportError(appRoot, error);
    if (!recovered.applied) {
      throw error;
    }
    return compileAndImport(tsPath, { ...options, allowRecovery: false });
  }
}
async function loadOptionalGeneratedModule(tsPath, fallback) {
  try {
    return await compileAndImport(tsPath);
  } catch (error) {
    if (error instanceof GeneratedFileNotFoundError) {
      logger.debug("Optional generated registry not present, using empty fallback", {
        file: path.basename(tsPath)
      });
      return fallback;
    }
    logger.error("Failed to load generated registry, continuing without its entries", {
      file: path.basename(tsPath),
      filePath: tsPath,
      err: error
    });
    return fallback;
  }
}
function resolveAppRootOrThrow(appRoot) {
  const resolved = appRoot ? {
    generatedDir: path.join(appRoot, ".mercato", "generated"),
    appDir: appRoot,
    mercatoDir: path.join(appRoot, ".mercato")
  } : findAppRoot();
  if (!resolved) {
    throw new Error(
      'Could not find app root with .mercato/generated directory. Make sure you run this command from within a Next.js app directory, or run "yarn mercato generate" first to create the generated files.'
    );
  }
  return resolved;
}
async function loadAppDiRegistrar(appDir) {
  const tsPath = path.join(appDir, "src", "di.ts");
  if (!fs.existsSync(tsPath)) {
    logger.debug("App-level DI module not present, skipping its registrations", { filePath: tsPath });
    return null;
  }
  try {
    const appDiModule = await compileAndImport(tsPath, {
      appRoot: appDir,
      outFile: path.join(appDir, ".mercato", "generated", "app-di.compiled.mjs")
    });
    const register = appDiModule.register;
    if (typeof register !== "function") {
      logger.error("App-level DI module exports no register(); its registrations are skipped", {
        filePath: tsPath
      });
      return null;
    }
    return register;
  } catch (error) {
    logger.error("Failed to load the app-level DI module; its registrations are skipped", {
      filePath: tsPath,
      err: error
    });
    return null;
  }
}
async function loadBootstrapDataWithActiveEsbuild(appRoot) {
  const resolved = resolveAppRootOrThrow(appRoot);
  const { generatedDir } = resolved;
  ensureMikroOrmV7GeneratedCacheCompatibility(resolved.appDir);
  const entityIdsModule = await compileAndImport(path.join(generatedDir, "entities.ids.generated.ts"));
  registerEntityIds(entityIdsModule.E);
  const [
    modulesModule,
    entitiesModule,
    diModule,
    searchModule,
    commandLoadersModule,
    commandInterceptorsModule,
    workflowsModule
  ] = await Promise.all([
    compileAndImport(path.join(generatedDir, "modules.cli.generated.ts")),
    compileAndImport(path.join(generatedDir, "entities.generated.ts")),
    compileAndImport(path.join(generatedDir, "di.generated.ts")),
    loadOptionalGeneratedModule(path.join(generatedDir, "search.generated.ts"), { searchModuleConfigs: [] }),
    loadOptionalGeneratedModule(path.join(generatedDir, "command-loaders.generated.ts"), { commandLoaderEntries: [] }),
    loadOptionalGeneratedModule(path.join(generatedDir, "command-interceptors.generated.ts"), {
      commandInterceptorEntries: []
    }),
    loadOptionalGeneratedModule(path.join(generatedDir, "workflows.generated.ts"), { allCodeWorkflows: [] })
  ]);
  return {
    modules: modulesModule.modules,
    entities: entitiesModule.entities,
    diRegistrars: diModule.diRegistrars,
    entityIds: entityIdsModule.E,
    // Search configs are needed by workers for indexing
    searchModuleConfigs: searchModule.searchModuleConfigs ?? [],
    commandLoaderEntries: commandLoadersModule.commandLoaderEntries ?? [],
    // Command interceptors must apply in worker/CLI processes too — the
    // interceptor registry is per-process, so relying on the Next.js runtime's
    // registration silently no-ops every interceptor for queued/CLI commands
    // (#4327).
    commandInterceptorEntries: commandInterceptorsModule.commandInterceptorEntries ?? [],
    // Code workflow definitions are needed by workers to resume code-defined instances
    codeWorkflows: workflowsModule.allCodeWorkflows ?? [],
    // Empty UI-related data - not needed for CLI
    dashboardWidgetEntries: [],
    injectionWidgetEntries: [],
    injectionTables: [],
    interceptorEntries: [],
    componentOverrideEntries: []
  };
}
async function loadBootstrapData(appRoot) {
  return withEsbuildLifecycle(() => loadBootstrapDataWithActiveEsbuild(appRoot));
}
async function bootstrapFromAppRoot(appRoot) {
  const { createBootstrap, waitForAsyncRegistration } = await import("./factory.js");
  const resolved = resolveAppRootOrThrow(appRoot);
  const { data, appDiRegistrar } = await withEsbuildLifecycle(async () => ({
    data: await loadBootstrapData(resolved.appDir),
    appDiRegistrar: await loadAppDiRegistrar(resolved.appDir)
  }));
  const bootstrap = createBootstrap(data, appDiRegistrar ? { appDiRegistrar } : {});
  bootstrap();
  await waitForAsyncRegistration();
  return data;
}
export {
  bootstrapFromAppRoot,
  compileAppSourceFile,
  createCliBundlePlugins,
  loadBootstrapData
};
//# sourceMappingURL=dynamicLoader.js.map
