// src/actions/package/platformPublint.ts
import fs from "node:fs";
import { builtinModules } from "node:module";
import path from "node:path";
import chalk from "chalk";
import { globSync } from "glob";
import ts from "typescript";
var platformSegments = /* @__PURE__ */ new Set(["browser", "neutral", "node"]);
var nodeConditionNames = /* @__PURE__ */ new Set(["node", "node-addons"]);
var browserConditionNames = /* @__PURE__ */ new Set(["browser", "web", "webworker", "worker"]);
var neutralConditionNames = /* @__PURE__ */ new Set(["neutral"]);
var builtinModuleNames = new Set(
  builtinModules.map((mod) => mod.replace(/^node:/, "")).filter((mod) => !mod.startsWith("_"))
);
var codeExtensions = [".mjs", ".cjs", ".js"];
var declarationExtensions = [".d.mts", ".d.cts", ".d.ts"];
function emptyResult() {
  return {
    errors: 0,
    modified: false,
    warnings: 0
  };
}
function toPosixPath(value) {
  return value.split(path.sep).join("/");
}
function relativePath(pkgDir, filePath) {
  return toPosixPath(path.relative(pkgDir, filePath));
}
function isSubpathExportKey(key) {
  return key === "." || key.startsWith("./");
}
function formatExportPath(pathParts) {
  return pathParts.map((part, index) => {
    if (index === 0) return part;
    if (part.startsWith("[")) return part;
    return /^[a-zA-Z_$][\w$]*$/.test(part) ? `.${part}` : `[${JSON.stringify(part)}]`;
  }).join("");
}
function conditionPlatform(key) {
  if (nodeConditionNames.has(key)) return "node";
  if (browserConditionNames.has(key)) return "browser";
  if (neutralConditionNames.has(key)) return "neutral";
  return void 0;
}
function getConditionPlatform(conditionPath) {
  for (const condition of conditionPath.toReversed()) {
    const platform = conditionPlatform(condition);
    if (platform) return platform;
  }
  return void 0;
}
function getPathPlatform(target) {
  const segments = target.replace(/^\.\//, "").split(/[\\/]+/);
  const distIndex = segments.indexOf("dist");
  const platform = distIndex === -1 ? void 0 : segments[distIndex + 1];
  return platformSegments.has(platform) ? platform : void 0;
}
function collectExportTargets(value, pathParts = ["exports"], conditionPath = []) {
  if (typeof value === "string") {
    if (!value.startsWith("./")) return [];
    return [{
      conditionPath,
      conditionPlatform: getConditionPlatform(conditionPath),
      exportPath: formatExportPath(pathParts),
      pathPlatform: getPathPlatform(value),
      target: value
    }];
  }
  if (Array.isArray(value)) {
    return value.flatMap((entry, index) => collectExportTargets(entry, [...pathParts, `[${index}]`], conditionPath));
  }
  if (typeof value !== "object" || value === null) return [];
  return Object.entries(value).flatMap(([key, nested]) => {
    const nextConditionPath = isSubpathExportKey(key) ? conditionPath : [...conditionPath, key];
    return collectExportTargets(nested, [...pathParts, key], nextConditionPath);
  });
}
function isDeclarationFile(filePath) {
  return declarationExtensions.some((ext) => filePath.endsWith(ext));
}
function isCodeFile(filePath) {
  return codeExtensions.some((ext) => filePath.endsWith(ext));
}
function hasCodeOrDeclarationExtension(filePath) {
  return isCodeFile(filePath) || isDeclarationFile(filePath);
}
function pathCandidates(filePath, extensions) {
  const candidates = [filePath];
  const parsed = path.parse(filePath);
  const withoutExtension = parsed.ext ? filePath.slice(0, -parsed.ext.length) : filePath;
  for (const ext of extensions) {
    candidates.push(`${withoutExtension}${ext}`, `${filePath}${ext}`, path.join(filePath, `index${ext}`));
  }
  return [...new Set(candidates)];
}
function declarationCandidates(filePath) {
  const extensionCandidates = filePath.endsWith(".mjs") ? [filePath.replace(/\.mjs$/, ".d.mts"), filePath.replace(/\.mjs$/, ".d.ts")] : filePath.endsWith(".cjs") ? [filePath.replace(/\.cjs$/, ".d.cts"), filePath.replace(/\.cjs$/, ".d.ts")] : filePath.endsWith(".js") ? [filePath.replace(/\.js$/, ".d.ts")] : [];
  return [.../* @__PURE__ */ new Set([filePath, ...extensionCandidates, ...pathCandidates(filePath, declarationExtensions)])];
}
function resolveExistingFile(filePath, declaration) {
  const candidates = declaration ? declarationCandidates(filePath) : pathCandidates(filePath, codeExtensions);
  return candidates.find((candidate) => fs.existsSync(candidate) && fs.statSync(candidate).isFile());
}
function resolveExportTargetFiles(pkgDir, target) {
  if (!hasCodeOrDeclarationExtension(target) && !target.includes("*")) return [];
  const relativeTarget = target.replace(/^\.\//, "");
  if (relativeTarget.includes("*")) {
    return globSync(relativeTarget, {
      cwd: pkgDir,
      nodir: true,
      absolute: true
    }).filter(hasCodeOrDeclarationExtension);
  }
  const absoluteTarget = path.resolve(pkgDir, relativeTarget);
  const exact = fs.existsSync(absoluteTarget) && fs.statSync(absoluteTarget).isFile() ? absoluteTarget : resolveExistingFile(absoluteTarget, isDeclarationFile(relativeTarget));
  return exact ? [exact] : [];
}
function isNodeBuiltin(specifier) {
  const normalized = specifier.replace(/^node:/, "");
  return specifier === "node" || specifier.startsWith("node:") || builtinModuleNames.has(normalized) || builtinModuleNames.has(normalized.split("/", 1)[0] ?? "");
}
function parseSourceFile(filePath) {
  const sourceCode = fs.readFileSync(filePath, "utf8");
  const scriptKind = isCodeFile(filePath) ? ts.ScriptKind.JS : ts.ScriptKind.TS;
  return ts.createSourceFile(path.basename(filePath), sourceCode, ts.ScriptTarget.Latest, true, scriptKind);
}
function stringLiteralText(node) {
  return node && ts.isStringLiteral(node) ? node.text : void 0;
}
function collectModuleSpecifiers(sourceFile) {
  const specifiers = [];
  function addSpecifier(node) {
    const text = stringLiteralText(node);
    if (text) specifiers.push(text);
  }
  function visit(node) {
    if (ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) {
      addSpecifier(node.moduleSpecifier);
    } else if (ts.isCallExpression(node) && node.expression.kind === ts.SyntaxKind.ImportKeyword) {
      addSpecifier(node.arguments[0]);
    } else if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === "require" && node.arguments.length > 0) {
      addSpecifier(node.arguments[0]);
    } else if (ts.isImportTypeNode(node)) {
      if (ts.isLiteralTypeNode(node.argument)) addSpecifier(node.argument.literal);
    } else if (ts.isImportEqualsDeclaration(node) && ts.isExternalModuleReference(node.moduleReference)) {
      addSpecifier(node.moduleReference.expression);
    }
    ts.forEachChild(node, visit);
  }
  visit(sourceFile);
  for (const ref of sourceFile.typeReferenceDirectives) {
    specifiers.push(ref.fileName);
  }
  return specifiers;
}
function resolveLocalSpecifier(filePath, specifier, declaration) {
  if (!specifier.startsWith(".")) return void 0;
  return resolveExistingFile(path.resolve(path.dirname(filePath), specifier), declaration);
}
function nodeAmbientGlobalFromName(name) {
  if (name === "NodeJS") return "NodeJS";
  if (name === "Buffer") return "Buffer";
  if (name === "process") return "process";
  return void 0;
}
function getQualifiedRootName(name) {
  return ts.isIdentifier(name) ? name.text : getQualifiedRootName(name.left);
}
function getPropertyAccessRootName(expression) {
  let current = expression;
  while (ts.isPropertyAccessExpression(current)) {
    current = current.expression;
  }
  return ts.isIdentifier(current) ? current.text : "";
}
function getNodeAmbientGlobal(sourceFile) {
  let ambient;
  function visit(node) {
    if (ambient) return;
    if (ts.isTypeReferenceNode(node)) {
      ambient = nodeAmbientGlobalFromName(getQualifiedRootName(node.typeName));
    } else if (ts.isExpressionWithTypeArguments(node) && ts.isIdentifier(node.expression)) {
      ambient = nodeAmbientGlobalFromName(node.expression.text);
    } else if (ts.isExpressionWithTypeArguments(node) && ts.isPropertyAccessExpression(node.expression)) {
      ambient = nodeAmbientGlobalFromName(getPropertyAccessRootName(node.expression));
    } else if (ts.isTypeQueryNode(node)) {
      ambient = nodeAmbientGlobalFromName(getQualifiedRootName(node.exprName));
    }
    ts.forEachChild(node, visit);
  }
  visit(sourceFile);
  return ambient;
}
function scanFileGraph(rootFile, platform, target) {
  const declaration = isDeclarationFile(rootFile);
  const queue = [rootFile];
  const seen = /* @__PURE__ */ new Set();
  const diagnostics = [];
  while (queue.length > 0) {
    const file = queue.shift();
    if (!file || seen.has(file)) continue;
    seen.add(file);
    const sourceFile = parseSourceFile(file);
    for (const specifier of collectModuleSpecifiers(sourceFile)) {
      if (isNodeBuiltin(specifier)) {
        diagnostics.push({
          detail: specifier,
          exportPath: target.exportPath,
          file,
          kind: "node-builtin-import",
          platform,
          target: target.target
        });
      }
      const resolved = resolveLocalSpecifier(file, specifier, declaration);
      if (resolved && !seen.has(resolved)) queue.push(resolved);
    }
    if (declaration) {
      const ambient = getNodeAmbientGlobal(sourceFile);
      if (ambient) {
        diagnostics.push({
          detail: ambient,
          exportPath: target.exportPath,
          file,
          kind: "node-ambient-type",
          platform,
          target: target.target
        });
      }
    }
  }
  return diagnostics;
}
function shouldScanForNodeUsage(target) {
  if (target.pathPlatform === "browser" || target.pathPlatform === "neutral") return target.pathPlatform;
  if (target.conditionPlatform === "browser" || target.conditionPlatform === "neutral") return target.conditionPlatform;
  return void 0;
}
function collectConditionTargetDiagnostics(targets) {
  const diagnostics = [];
  for (const target of targets) {
    const { conditionPlatform: condition, pathPlatform: output } = target;
    if (!condition || !output) continue;
    if (condition === "browser" && output === "node" || condition === "node" && output === "browser" || condition === "neutral" && output !== "neutral") {
      diagnostics.push({
        detail: `${condition} condition points to ${output} output`,
        exportPath: target.exportPath,
        kind: "condition-target-mismatch",
        platform: condition,
        target: target.target
      });
    }
  }
  return diagnostics;
}
function isRuntimeTarget(target) {
  return codeExtensions.some((ext) => target.target.endsWith(ext));
}
function isPortableTarget(target) {
  return target.conditionPlatform === "browser" || target.conditionPlatform === "neutral" || target.pathPlatform === "browser" || target.pathPlatform === "neutral" || !target.conditionPlatform && !target.pathPlatform;
}
function isNodeTarget(target) {
  return target.conditionPlatform === "node" || target.pathPlatform === "node";
}
function isNodeOnlyPackage(pkg, targets) {
  const runtimeTargets = targets.filter(isRuntimeTarget);
  const platformTargets = runtimeTargets.length > 0 ? runtimeTargets : targets;
  if (platformTargets.length === 0) return pkg.bin !== void 0;
  return platformTargets.some(isNodeTarget) && !platformTargets.some(isPortableTarget);
}
function readRootTsconfigTypes(pkgDir) {
  const configPath = path.join(pkgDir, "tsconfig.json");
  if (!fs.existsSync(configPath)) return [];
  const parsed = ts.parseConfigFileTextToJson(configPath, fs.readFileSync(configPath, "utf8"));
  const config = parsed.config;
  const types = config?.compilerOptions?.types;
  return Array.isArray(types) ? types.filter((value) => typeof value === "string") : [];
}
function collectTsconfigDiagnostics(pkgDir, pkg, targets) {
  if (targets.length === 0) return [];
  if (isNodeOnlyPackage(pkg, targets)) return [];
  const types = readRootTsconfigTypes(pkgDir);
  return types.includes("node") ? [{
    detail: 'compilerOptions.types includes "node"',
    file: path.join(pkgDir, "tsconfig.json"),
    kind: "tsconfig-node-types"
  }] : [];
}
function diagnosticKey(pkgDir, diagnostic) {
  return [
    diagnostic.kind,
    diagnostic.platform ?? "",
    diagnostic.target ?? "",
    diagnostic.exportPath ?? "",
    diagnostic.file ? relativePath(pkgDir, diagnostic.file) : "",
    diagnostic.detail ?? ""
  ].join("\0");
}
function dedupeDiagnostics(pkgDir, diagnostics) {
  const seen = /* @__PURE__ */ new Set();
  const result = [];
  for (const diagnostic of diagnostics) {
    const key = diagnosticKey(pkgDir, diagnostic);
    if (seen.has(key)) continue;
    seen.add(key);
    result.push(diagnostic);
  }
  return result;
}
function renderDiagnostic(pkgDir, diagnostic) {
  switch (diagnostic.kind) {
    case "condition-target-mismatch": {
      return `Publint [platform]: ${diagnostic.detail} at ${diagnostic.exportPath} (${diagnostic.target})`;
    }
    case "node-builtin-import": {
      const file = relativePath(pkgDir, diagnostic.file ?? "");
      return `Publint [platform]: ${diagnostic.platform} export ${diagnostic.target} imports Node builtin "${diagnostic.detail}" in ${file} (${diagnostic.exportPath})`;
    }
    case "node-ambient-type": {
      const file = relativePath(pkgDir, diagnostic.file ?? "");
      return `Publint [platform]: ${diagnostic.platform} declaration ${diagnostic.target} exposes Node ambient "${diagnostic.detail}" in ${file} (${diagnostic.exportPath})`;
    }
    case "tsconfig-node-types": {
      return 'Publint [platform]: non-node package tsconfig.json should not include compilerOptions.types "node"';
    }
  }
}
function checkPlatform(pkgDir, pkg) {
  const result = emptyResult();
  const targets = collectExportTargets(pkg.exports);
  const diagnostics = [...collectConditionTargetDiagnostics(targets)];
  for (const target of targets) {
    const platform = shouldScanForNodeUsage(target);
    if (!platform) continue;
    for (const file of resolveExportTargetFiles(pkgDir, target.target)) {
      diagnostics.push(...scanFileGraph(file, platform, target));
    }
  }
  diagnostics.push(...collectTsconfigDiagnostics(pkgDir, pkg, targets));
  for (const diagnostic of dedupeDiagnostics(pkgDir, diagnostics)) {
    console.error(chalk.red(renderDiagnostic(pkgDir, diagnostic)));
    result.errors++;
  }
  return result;
}
export {
  checkPlatform
};
//# sourceMappingURL=platformPublint.mjs.map
