limina 0.0.3 → 0.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/cli.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import "./chunks/dep-lkQg1P9Q.js";
3
3
  import { c as getCheckerAdapter, d as normalizeAbsolutePath, f as normalizeSlashes, g as toRelativePath, h as toPosixPath, i as loadConfig, l as normalizeExtensions, m as toAbsolutePath, n as getActiveCheckerExtensions, p as normalizeWorkspacePath, r as getActiveCheckers, u as isPathInsideDirectory } from "./chunks/dep-DzYrmtQJ.js";
4
- import { A as shouldResolveThroughGraph$1, B as collectGraphProjectRoutes, C as formatArtifactDependencyPolicy, D as isRelativeSpecifier, E as isDtsProjectConfig, F as getPublishDependencySections, G as formatReferences, H as createExtensionPattern, I as isWorkspaceDependencySpecifier, J as isOrdinaryTypecheckConfigPath, K as getDtsCompanionConfigPath, L as collectCheckerEntryProjectRoutes, M as collectWorkspacePackages, N as findPackageForSpecifier, O as parseProject$1, P as getPackageRootSpecifier, Q as resolveReferencePath, R as collectGraphProjectRoute, S as findTargetProject, T as inferPackageProject$1, U as createExtraFileExtensions, V as collectSourceGraphProjectExtensions, W as createFormatHost, X as readJsonConfig, Y as parseProjectFileNamesForExtensions, Z as resolveProjectConfigPath, _ as normalizeGraphRules, a as runSourceCheck, b as findImporterForFile$1, c as GraphLogger, d as ProofLogger, f as clearCliScreen, g as getDeniedRefRule, h as getDeniedDepRuleForSpecifier, i as runCheckerTypecheck, j as collectImporters, k as resolveInternalImport$1, l as PackageLogger, m as getDeniedDepRuleForPackage, n as createLiminaFlowReporter, o as runInit, p as formatErrorMessage$1, q as getRawReferencePaths, r as runCheckerBuild, s as CliLogger, u as PathsLogger, v as collectImportsFromFile$1, w as getTypecheckConfigPath, x as findPackageForFile, y as createFileOwnerLookup$1, z as collectGraphProjectRouteFromRoot } from "./chunks/dep-UWxsul2A.js";
4
+ import { $ as resolveReferencePath, A as resolveInternalImport$1, B as collectGraphProjectRouteFromRoot, C as findTargetProject, D as isDtsProjectConfig, E as inferPackageProject$1, F as getPackageRootSpecifier, G as createFormatHost, H as collectSourceGraphProjectExtensions, I as getPublishDependencySections, J as getRawReferencePaths, K as formatReferences, L as isWorkspaceDependencySpecifier, M as collectImporters, N as collectWorkspacePackages, O as isRelativeSpecifier, P as findPackageForSpecifier, Q as resolveProjectConfigPath, R as collectCheckerEntryProjectRoutes, S as findPackageForFile, T as getTypecheckConfigPath, U as createExtensionPattern, V as collectGraphProjectRoutes, W as createExtraFileExtensions, X as parseProjectFileNamesForExtensions, Y as isOrdinaryTypecheckConfigPath, Z as readJsonConfig, _ as getDeniedRefRule, a as runSourceCheck, b as createFileOwnerLookup$1, c as GraphLogger, d as ProofLogger, f as ReleaseLogger, g as getDeniedDepRuleForSpecifier, h as getDeniedDepRuleForPackage, i as runCheckerTypecheck, j as shouldResolveThroughGraph$1, k as parseProject$1, l as PackageLogger, m as formatErrorMessage$1, n as createLiminaFlowReporter, o as runInit, p as clearCliScreen, q as getDtsCompanionConfigPath, r as runCheckerBuild, s as CliLogger, u as PathsLogger, v as normalizeGraphRules, w as formatArtifactDependencyPolicy, x as findImporterForFile$1, y as collectImportsFromFile$1, z as collectGraphProjectRoute } from "./chunks/dep-ZRIm_-Zk.js";
5
5
  import { builtinModules, createRequire } from "node:module";
6
6
  import { cac } from "cac";
7
7
  import { createElapsedTimer } from "logaria/helper";
@@ -423,1072 +423,713 @@ async function runGraphCheck(config, options = {}) {
423
423
  }
424
424
 
425
425
  //#endregion
426
- //#region src/package-release-consistency.ts
427
- var PackageReleaseConsistencyError = class extends Error {
428
- name = "PackageReleaseConsistencyError";
426
+ //#region src/commands/package.ts
427
+ const DEFAULT_PACKAGE_CHECKS = [
428
+ "publint",
429
+ "attw",
430
+ "boundary"
431
+ ];
432
+ const PACKAGE_CHECK_TOOLS = new Set(DEFAULT_PACKAGE_CHECKS);
433
+ const ATTW_PROFILE_IGNORED_RESOLUTIONS = {
434
+ strict: [],
435
+ node16: [],
436
+ "esm-only": ["node16-cjs"]
429
437
  };
430
- const semver = createRequire(import.meta.url)("semver");
438
+ const nodeBuiltinSpecifiers = new Set(builtinModules.flatMap((specifier) => specifier.startsWith("node:") ? [specifier, specifier.slice(5)] : [specifier, `node:${specifier}`]));
431
439
  function isRecord$1(value) {
432
440
  return typeof value === "object" && value !== null && !Array.isArray(value);
433
441
  }
434
- function isLinkDependencySpecifier(specifier) {
435
- return specifier.startsWith("link:");
442
+ function isPackageCheckTool(value) {
443
+ return PACKAGE_CHECK_TOOLS.has(value);
436
444
  }
437
- function createReleaseConsistencyState() {
445
+ function isRelativeOrAbsoluteSpecifier(specifier) {
446
+ return specifier.startsWith(".") || specifier.startsWith("/") || specifier.startsWith("file:") || specifier.startsWith("http:") || specifier.startsWith("https:") || specifier.startsWith("data:");
447
+ }
448
+ function toArrayBuffer(buffer) {
449
+ return buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
450
+ }
451
+ function collectSelfSpecifierMatchers(packageName, exportsField) {
452
+ const exact = new Set([packageName]);
453
+ const prefixes = [];
454
+ if (!isRecord$1(exportsField)) return {
455
+ exact,
456
+ prefixes
457
+ };
458
+ for (const exportKey of Object.keys(exportsField)) {
459
+ if (exportKey === ".") {
460
+ exact.add(packageName);
461
+ continue;
462
+ }
463
+ if (!exportKey.startsWith("./")) continue;
464
+ const normalizedSubpath = exportKey.slice(2);
465
+ if (normalizedSubpath.length === 0) continue;
466
+ const wildcardIndex = normalizedSubpath.indexOf("*");
467
+ if (wildcardIndex !== -1) {
468
+ prefixes.push(`${packageName}/${normalizedSubpath.slice(0, wildcardIndex)}`);
469
+ continue;
470
+ }
471
+ exact.add(`${packageName}/${normalizedSubpath}`);
472
+ }
438
473
  return {
439
- directWorkspaceDependencies: [],
440
- edges: /* @__PURE__ */ new Map(),
441
- missingWorkspaceDependencies: [],
442
- packedManifestProblems: [],
443
- privateWorkspaceDependencies: [],
444
- registryMetadataCache: /* @__PURE__ */ new Map(),
445
- registryProblems: [],
446
- sourceLinkDependencies: [],
447
- unpublishedPackageNames: /* @__PURE__ */ new Set(),
448
- visitedPackages: /* @__PURE__ */ new Set()
474
+ exact,
475
+ prefixes
449
476
  };
450
477
  }
451
- function collectPublishDependencyEntries(manifest) {
452
- const entries = [];
453
- for (const { dependencies, name } of getPublishDependencySections(manifest)) for (const [dependencyName, specifier] of Object.entries(dependencies)) entries.push({
454
- dependencyName,
455
- sectionName: name,
456
- specifier
457
- });
458
- return entries;
478
+ function isAllowedSelfSpecifier(specifier, matchers) {
479
+ return matchers.exact.has(specifier) || matchers.prefixes.some((prefix) => specifier.startsWith(prefix));
459
480
  }
460
- function addEdge(edges, importerName, dependencyName) {
461
- const dependencies = edges.get(importerName) ?? /* @__PURE__ */ new Set();
462
- dependencies.add(dependencyName);
463
- edges.set(importerName, dependencies);
481
+ function normalizePublishedModulePath(relativeFilePath) {
482
+ return relativeFilePath.replaceAll("\\", "/");
464
483
  }
465
- function formatDependencyLocation(problem) {
466
- const dependency = problem.dependencyName ? ` -> ${problem.dependencyName}` : "";
467
- const section = problem.sectionName ? ` [${problem.sectionName}]` : "";
468
- const specifier = problem.specifier ? ` (${problem.specifier})` : "";
469
- return `${problem.importerName}${dependency}${section}${specifier}`;
484
+ function classifyRuntimeEnvironment(target, relativeFilePath) {
485
+ if (typeof target.environment === "function") return target.environment(relativeFilePath);
486
+ if (target.environment) return target.environment;
487
+ const normalizedPath = normalizePublishedModulePath(relativeFilePath);
488
+ return normalizedPath.startsWith("node/") || normalizedPath.startsWith("plugin/") ? "node" : "browser";
470
489
  }
471
- function formatProblemLines(title, problems) {
472
- if (problems.length === 0) return [];
473
- return [
474
- "",
475
- title,
476
- ...problems.map((problem) => ` - ${formatDependencyLocation(problem)}: ${problem.message}`)
477
- ];
490
+ async function collectPublishedModuleFiles(directoryPath) {
491
+ const entries = await readdir(directoryPath, { withFileTypes: true });
492
+ const files = [];
493
+ for (const entry of entries) {
494
+ const absolutePath = path.join(directoryPath, entry.name);
495
+ if (entry.isDirectory()) {
496
+ files.push(...await collectPublishedModuleFiles(absolutePath));
497
+ continue;
498
+ }
499
+ if (/\.[cm]?js$/u.test(entry.name)) files.push(absolutePath);
500
+ }
501
+ return files;
478
502
  }
479
- function getPackedDependencySpecifier(manifest, dependencyName) {
480
- for (const { dependencies } of getPublishDependencySections(manifest)) {
481
- const specifier = dependencies[dependencyName];
482
- if (specifier) return specifier;
503
+ function validatePublishedSpecifier(options) {
504
+ const { allowedExternalPackages, environment, packageName, selfSpecifiers, specifier } = options;
505
+ if (isRelativeOrAbsoluteSpecifier(specifier)) return null;
506
+ if (nodeBuiltinSpecifiers.has(specifier)) {
507
+ if (environment === "node") return null;
508
+ return `browser/runtime output must not import Node builtin "${specifier}"`;
509
+ }
510
+ const packageRoot = getPackageRootSpecifier(specifier);
511
+ if (packageRoot === packageName) {
512
+ if (isAllowedSelfSpecifier(specifier, selfSpecifiers)) return null;
513
+ return `self import "${specifier}" is not exposed by output package.json exports`;
483
514
  }
515
+ if (allowedExternalPackages.has(packageRoot)) return null;
516
+ return `"${specifier}" resolves to package "${packageRoot}" which is not listed in dependencies, peerDependencies, optionalDependencies, or self exports`;
484
517
  }
485
- function getNpmPackageMetadataUrl(packageName) {
486
- return `https://registry.npmjs.org/${encodeURIComponent(packageName)}`;
518
+ function formatAttwProblem(problem) {
519
+ const resolutionKind = "resolutionKind" in problem ? ` [resolution: ${problem.resolutionKind}]` : "";
520
+ const entrypoint = "entrypoint" in problem ? ` [entrypoint: ${problem.entrypoint}]` : "";
521
+ switch (problem.kind) {
522
+ case "NoResolution": return `No resolution${resolutionKind}${entrypoint}`;
523
+ case "UntypedResolution": return `Untyped resolution${resolutionKind}${entrypoint}`;
524
+ case "FalseESM": return `False ESM: ${problem.typesFileName} -> ${problem.implementationFileName}`;
525
+ case "FalseCJS": return `False CJS: ${problem.typesFileName} -> ${problem.implementationFileName}`;
526
+ case "CJSResolvesToESM": return `CJS resolves to ESM${resolutionKind}${entrypoint}`;
527
+ case "FallbackCondition": return `Fallback condition used${resolutionKind}${entrypoint}`;
528
+ case "NamedExports": return problem.isMissingAllNamed ? `Named exports missing: all named exports [types: ${problem.typesFileName}] [implementation: ${problem.implementationFileName}]` : `Named exports missing: ${problem.missing.join(", ") || "(none)"} [types: ${problem.typesFileName}] [implementation: ${problem.implementationFileName}]`;
529
+ case "FalseExportDefault": return `False export default [types: ${problem.typesFileName}] [implementation: ${problem.implementationFileName}]`;
530
+ case "MissingExportEquals": return `Missing export equals [types: ${problem.typesFileName}] [implementation: ${problem.implementationFileName}]`;
531
+ case "InternalResolutionError": return `Internal resolution error in ${problem.fileName} [option: ${problem.resolutionOption}] [module: ${problem.moduleSpecifier}]`;
532
+ case "UnexpectedModuleSyntax": return `Unexpected module syntax in ${problem.fileName}`;
533
+ case "CJSOnlyExportsDefault": return `CJS only exports default in ${problem.fileName}`;
534
+ default: return `Unknown ATTW problem: ${JSON.stringify(problem)}`;
535
+ }
487
536
  }
488
- async function fetchRegistryPackageMetadata(packageName, state) {
489
- if (state.registryMetadataCache.has(packageName)) return state.registryMetadataCache.get(packageName) ?? null;
490
- const response = await fetch(getNpmPackageMetadataUrl(packageName), { headers: { accept: "application/vnd.npm.install-v1+json, application/json" } });
491
- if (!response.ok) {
492
- state.registryMetadataCache.set(packageName, null);
493
- return null;
537
+ function normalizeEntryChecks(entry) {
538
+ const checks = entry.checks ?? DEFAULT_PACKAGE_CHECKS;
539
+ const normalizedChecks = [];
540
+ for (const check of checks) {
541
+ if (!isPackageCheckTool(check)) throw new Error(`Invalid package check "${check}". Expected one of: publint, attw, boundary.`);
542
+ if (!normalizedChecks.includes(check)) normalizedChecks.push(check);
494
543
  }
495
- const metadata = await response.json();
496
- const registryMetadata = isRecord$1(metadata) ? metadata : null;
497
- state.registryMetadataCache.set(packageName, registryMetadata);
498
- return registryMetadata;
544
+ return normalizedChecks;
499
545
  }
500
- function findRegistryVersionMetadata(metadata, version) {
501
- if (!isRecord$1(metadata.versions)) return null;
502
- const versionMetadata = metadata.versions[version];
503
- return isRecord$1(versionMetadata) ? versionMetadata : null;
546
+ function selectEntryChecks(entry, requestedTool) {
547
+ const configuredChecks = normalizeEntryChecks(entry);
548
+ if (!requestedTool || requestedTool === "all") return configuredChecks;
549
+ return configuredChecks.includes(requestedTool) ? [requestedTool] : [];
504
550
  }
505
- function execGitCommand(config, args) {
506
- return new Promise((resolve, reject) => {
507
- execFile("git", [
508
- "-C",
509
- config.rootDir,
510
- ...args
511
- ], {
512
- encoding: "utf8",
513
- maxBuffer: 10 * 1024 * 1024
514
- }, (error, stdout) => {
515
- if (error) {
516
- reject(error);
517
- return;
518
- }
519
- resolve(stdout);
520
- });
521
- });
551
+ function findNearestPackageJsonPath(cwd, rootDir) {
552
+ const resolvedRootDir = path.resolve(rootDir);
553
+ let currentDir = path.resolve(cwd);
554
+ while (true) {
555
+ const relativeToRoot = path.relative(resolvedRootDir, currentDir);
556
+ if (!(relativeToRoot === "" || relativeToRoot !== ".." && !relativeToRoot.startsWith(`..${path.sep}`) && !path.isAbsolute(relativeToRoot))) return;
557
+ const packageJsonPath = path.join(currentDir, "package.json");
558
+ if (existsSync(packageJsonPath)) return packageJsonPath;
559
+ if (currentDir === resolvedRootDir) return;
560
+ const parentDir = path.dirname(currentDir);
561
+ if (parentDir === currentDir) return;
562
+ currentDir = parentDir;
563
+ }
522
564
  }
523
- async function hasWorkspacePackageChangesSinceGitHead(options) {
524
- const relativeDirectory = toRelativePath(options.config.rootDir, options.workspacePackage.directory);
565
+ function readCwdPackageName(cwd, rootDir) {
566
+ const packageJsonPath = findNearestPackageJsonPath(cwd, rootDir);
567
+ if (!packageJsonPath) return;
525
568
  try {
526
- await execGitCommand(options.config, [
527
- "diff",
528
- "--quiet",
529
- options.gitHead,
530
- "--",
531
- relativeDirectory
532
- ]);
569
+ const manifest = JSON.parse(readFileSync(packageJsonPath, "utf8"));
570
+ return typeof manifest.name === "string" && manifest.name.trim() ? manifest.name.trim() : void 0;
533
571
  } catch (error) {
534
- if (isRecord$1(error) && error.code === 1) return true;
535
- throw error;
572
+ throw new Error(`Unable to read package name from ${packageJsonPath}: ${formatErrorMessage$1(error)}`);
536
573
  }
537
- return (await execGitCommand(options.config, [
538
- "ls-files",
539
- "--others",
540
- "--exclude-standard",
541
- "--",
542
- relativeDirectory
543
- ])).trim().length > 0;
544
574
  }
545
- async function verifyWorkspacePackagePublished(options) {
546
- const { state, workspacePackage } = options;
547
- const version = workspacePackage.manifest.version;
548
- if (!version || !semver.valid(version)) {
549
- state.unpublishedPackageNames.add(workspacePackage.name);
550
- state.registryProblems.push({
551
- importerName: workspacePackage.name,
552
- message: ["workspace package must declare a valid semver version", "before another publishable package can depend on it"].join(" "),
553
- packageName: workspacePackage.name
554
- });
555
- return;
575
+ function formatConfiguredPackageEntryNames(entries) {
576
+ const names = entries.map((entry) => entry.name).filter(Boolean);
577
+ return names.length > 0 ? names.join(", ") : "(none)";
578
+ }
579
+ function getConfiguredPackageEntries(config) {
580
+ return config.package?.entries ?? [];
581
+ }
582
+ function normalizePackageNameFilters(packageNames) {
583
+ const normalizedNames = [];
584
+ for (const packageName of packageNames ?? []) {
585
+ const normalizedName = packageName.trim();
586
+ if (normalizedName && !normalizedNames.includes(normalizedName)) normalizedNames.push(normalizedName);
556
587
  }
557
- let metadata;
558
- try {
559
- metadata = await fetchRegistryPackageMetadata(workspacePackage.name, state);
560
- } catch (error) {
561
- state.unpublishedPackageNames.add(workspacePackage.name);
562
- state.registryProblems.push({
563
- importerName: workspacePackage.name,
564
- message: [`unable to read npm registry metadata for ${workspacePackage.name}@${version}:`, formatErrorMessage$1(error)].join(" "),
565
- packageName: workspacePackage.name
588
+ return normalizedNames;
589
+ }
590
+ function resolvePackageEntryOutDir(options) {
591
+ const outDir = options.entry.outDir;
592
+ if (typeof outDir !== "string" || outDir.trim().length === 0) throw new Error(`Invalid package entry at package.entries[${options.entryIndex}].outDir. Expected a non-empty string.`);
593
+ return path.resolve(options.config.rootDir, outDir);
594
+ }
595
+ function getPackageEntryLabel(options) {
596
+ const name = options.entry.name;
597
+ if (typeof name !== "string" || name.trim().length === 0) throw new Error(`Invalid package entry at package.entries[${options.entryIndex}].name. Expected a non-empty string.`);
598
+ return name.trim();
599
+ }
600
+ function createEntryPlan(options) {
601
+ const outDir = resolvePackageEntryOutDir({
602
+ config: options.config,
603
+ entry: options.entry,
604
+ entryIndex: options.entryIndex
605
+ });
606
+ return {
607
+ checks: selectEntryChecks(options.entry, options.requestedTool),
608
+ entryIndex: options.entryIndex,
609
+ label: getPackageEntryLabel({
610
+ entry: options.entry,
611
+ entryIndex: options.entryIndex
612
+ }),
613
+ outDir,
614
+ rawEntry: options.entry
615
+ };
616
+ }
617
+ function createPackageEntrySelectionPlan(options) {
618
+ const entries = getConfiguredPackageEntries(options.config);
619
+ if (entries.length === 0) throw new Error("No package entries are configured.");
620
+ let selectedEntries;
621
+ let selectionReason;
622
+ const packageNames = normalizePackageNameFilters(options.packageNames);
623
+ if (packageNames.length > 0) {
624
+ selectedEntries = packageNames.map((packageName) => {
625
+ const entry = entries.find((candidate) => candidate.name === packageName);
626
+ if (!entry) throw new Error([`No package entry named "${packageName}" is configured.`, `Configured package entries: ${formatConfiguredPackageEntryNames(entries)}.`].join(" "));
627
+ return entry;
566
628
  });
567
- return;
629
+ selectionReason = `--package matched configured package entry name(s): ${packageNames.join(", ")}.`;
630
+ } else {
631
+ const cwdPackageName = readCwdPackageName(options.cwd, options.config.rootDir);
632
+ if (cwdPackageName) {
633
+ selectedEntries = entries.filter((entry) => entry.name === cwdPackageName);
634
+ if (selectedEntries.length > 0) selectionReason = `nearest package.json name "${cwdPackageName}" matched configured package entry name.`;
635
+ else if (options.strictCwd) throw new Error([`Nearest package.json name "${cwdPackageName}" does not match a configured package entry.`, `Configured package entries: ${formatConfiguredPackageEntryNames(entries)}.`].join(" "));
636
+ else {
637
+ selectedEntries = entries;
638
+ selectionReason = `nearest package.json name "${cwdPackageName}" did not match configured package entries; running all configured entries.`;
639
+ }
640
+ } else if (options.strictCwd) throw new Error(["No package name was found from cwd up to the workspace root.", "Run from a configured package directory or pass --package <name>."].join(" "));
641
+ else {
642
+ selectedEntries = entries;
643
+ selectionReason = "No package name was found from cwd up to the workspace root; running all configured entries.";
644
+ }
568
645
  }
569
- if (!metadata) {
570
- state.unpublishedPackageNames.add(workspacePackage.name);
571
- state.registryProblems.push({
572
- importerName: workspacePackage.name,
573
- message: `${workspacePackage.name}@${version} is not published to the npm registry`,
574
- packageName: workspacePackage.name
575
- });
576
- return;
577
- }
578
- const versionMetadata = findRegistryVersionMetadata(metadata, version);
579
- if (!versionMetadata) {
580
- state.unpublishedPackageNames.add(workspacePackage.name);
581
- state.registryProblems.push({
582
- importerName: workspacePackage.name,
583
- message: `${workspacePackage.name}@${version} is not published to the npm registry`,
584
- packageName: workspacePackage.name
585
- });
586
- return;
587
- }
588
- if (typeof versionMetadata.gitHead !== "string" || versionMetadata.gitHead.trim().length === 0) {
589
- state.unpublishedPackageNames.add(workspacePackage.name);
590
- state.registryProblems.push({
591
- importerName: workspacePackage.name,
592
- message: [`${workspacePackage.name}@${version} registry metadata has no gitHead,`, "so limina cannot prove the published source baseline"].join(" "),
593
- packageName: workspacePackage.name
594
- });
595
- return;
596
- }
597
- let hasChanges;
598
- try {
599
- hasChanges = await hasWorkspacePackageChangesSinceGitHead({
646
+ return {
647
+ selectionReason,
648
+ entries: selectedEntries.map((entry) => createEntryPlan({
600
649
  config: options.config,
601
- gitHead: versionMetadata.gitHead,
602
- workspacePackage
603
- });
604
- } catch (error) {
605
- state.unpublishedPackageNames.add(workspacePackage.name);
606
- state.registryProblems.push({
607
- importerName: workspacePackage.name,
608
- message: [
609
- `unable to compare ${workspacePackage.name}@${version}`,
610
- `against published gitHead ${versionMetadata.gitHead}:`,
611
- formatErrorMessage$1(error)
612
- ].join(" "),
613
- packageName: workspacePackage.name
614
- });
615
- return;
616
- }
617
- if (hasChanges) {
618
- state.unpublishedPackageNames.add(workspacePackage.name);
619
- state.registryProblems.push({
620
- importerName: workspacePackage.name,
621
- message: [`${workspacePackage.name}@${version} has workspace changes`, `after the published npm registry gitHead ${versionMetadata.gitHead}`].join(" "),
622
- packageName: workspacePackage.name
623
- });
624
- }
650
+ entry,
651
+ entryIndex: entries.indexOf(entry),
652
+ requestedTool: options.tool
653
+ }))
654
+ };
625
655
  }
626
- async function visitWorkspacePackageDependencies(options) {
627
- const { config, importerName, isRoot, manifest, state, workspacePackagesByName } = options;
628
- for (const entry of collectPublishDependencyEntries(manifest)) {
629
- if (isLinkDependencySpecifier(entry.specifier)) {
630
- state.sourceLinkDependencies.push({
631
- dependencyName: entry.dependencyName,
632
- importerName,
633
- message: "publishable dependency sections must not use link:",
634
- sectionName: entry.sectionName,
635
- specifier: entry.specifier
636
- });
637
- continue;
638
- }
639
- if (!isWorkspaceDependencySpecifier(entry.specifier)) continue;
640
- const targetPackage = workspacePackagesByName.get(entry.dependencyName);
641
- if (!targetPackage) {
642
- state.missingWorkspaceDependencies.push({
643
- dependencyName: entry.dependencyName,
644
- importerName,
645
- message: "workspace: publish dependency does not match a named workspace package",
646
- sectionName: entry.sectionName,
647
- specifier: entry.specifier
648
- });
656
+ function logPackageCheckPlan(options) {
657
+ PackageLogger.info([
658
+ "Package check plan:",
659
+ ` config: ${toRelativePath(options.config.rootDir, options.config.configPath)}`,
660
+ ` cwd: ${toRelativePath(options.config.rootDir, options.cwd)}`,
661
+ ` selection: ${options.plan.selectionReason}`,
662
+ " entries:",
663
+ ...options.plan.entries.map((entry) => [
664
+ ` - ${entry.label}`,
665
+ ` outDir: ${toRelativePath(options.config.rootDir, entry.outDir)}`,
666
+ ` checks: ${entry.checks.length > 0 ? entry.checks.join(", ") : "(none)"}`
667
+ ].join("\n"))
668
+ ].join("\n"));
669
+ }
670
+ async function packOutputTarball(outDir) {
671
+ const destination = await mkdtemp(path.join(tmpdir(), "__LIMINA_PACKAGE__"));
672
+ return {
673
+ cleanup: async () => {
674
+ await rm(destination, {
675
+ force: true,
676
+ recursive: true
677
+ }).catch(() => null);
678
+ },
679
+ tarball: await readFile(await pack(outDir, {
680
+ destination,
681
+ ignoreScripts: true,
682
+ packageManager: "pnpm"
683
+ }))
684
+ };
685
+ }
686
+ async function readDistPackageJson(options) {
687
+ if (!existsSync(options.packageJsonPath)) throw new Error(`outDir package.json not found${options.label ? ` for ${options.label}` : ""} at ${options.config ? toRelativePath(options.config.rootDir, options.packageJsonPath) : options.packageJsonPath}. Run the package build first.`);
688
+ return JSON.parse(await readFile(options.packageJsonPath, "utf8"));
689
+ }
690
+ async function runPublintCheck(options) {
691
+ const task = options.flow?.start(`publint: ${options.label}`, { depth: options.flowDepth ?? 0 });
692
+ PackageLogger.info(`publint started: ${options.label}`);
693
+ const publintElapsed = createElapsedTimer();
694
+ const { messages, pkg } = await publint({
695
+ pack: { tarball: toArrayBuffer(options.tarball) },
696
+ strict: options.strict
697
+ });
698
+ if (messages.length === 0) {
699
+ if (!options.flow?.interactive) PackageLogger.success(`publint passed: ${options.label}`, publintElapsed());
700
+ task?.pass();
701
+ return true;
702
+ }
703
+ for (const message of messages) {
704
+ const rendered = formatMessage(message, pkg) ?? message.code;
705
+ if (message.type === "error") {
706
+ PackageLogger.error(`[${options.label}] [publint] ${rendered}`);
649
707
  continue;
650
708
  }
651
- addEdge(state.edges, importerName, targetPackage.name);
652
- if (isRoot) state.directWorkspaceDependencies.push({
653
- dependencyName: entry.dependencyName,
654
- sectionName: entry.sectionName,
655
- targetPackage
656
- });
657
- if (targetPackage.manifest.private === true) {
658
- state.privateWorkspaceDependencies.push({
659
- dependencyName: entry.dependencyName,
660
- importerName,
661
- message: "publishable packages cannot depend on a private workspace package",
662
- packageName: targetPackage.name,
663
- sectionName: entry.sectionName,
664
- specifier: entry.specifier
665
- });
709
+ if (message.type === "warning") {
710
+ PackageLogger.warn(`[${options.label}] [publint] ${rendered}`);
666
711
  continue;
667
712
  }
668
- if (!state.visitedPackages.has(targetPackage.name)) {
669
- state.visitedPackages.add(targetPackage.name);
670
- await verifyWorkspacePackagePublished({
671
- config,
672
- state,
673
- workspacePackage: targetPackage
674
- });
675
- await visitWorkspacePackageDependencies({
676
- config,
677
- importerName: targetPackage.name,
678
- isRoot: false,
679
- manifest: targetPackage.manifest,
680
- state,
681
- workspacePackagesByName
682
- });
683
- }
713
+ PackageLogger.info(`[${options.label}] [publint] ${rendered}`);
684
714
  }
715
+ PackageLogger.error(`publint found ${messages.length} issue(s): ${options.label}`, publintElapsed());
716
+ task?.fail(`publint found ${messages.length} issue(s): ${options.label}`);
717
+ return false;
685
718
  }
686
- async function readPackedPackageJson(tarball) {
687
- const unpacked = await unpack(tarball);
688
- const packageJsonFile = unpacked.files.find((file) => file.name === `${unpacked.rootDir}/package.json`);
689
- if (!packageJsonFile) throw new Error("packed tarball does not contain package.json");
690
- return JSON.parse(Buffer.from(packageJsonFile.data).toString("utf8"));
691
- }
692
- function validatePackedManifest(options) {
693
- const { manifest, rootPackageName, state } = options;
694
- for (const entry of collectPublishDependencyEntries(manifest)) if (isWorkspaceDependencySpecifier(entry.specifier) || isLinkDependencySpecifier(entry.specifier)) state.packedManifestProblems.push({
695
- dependencyName: entry.dependencyName,
696
- importerName: rootPackageName,
697
- message: "packed package manifest must not expose workspace: or link: dependency specifiers",
698
- sectionName: entry.sectionName,
699
- specifier: entry.specifier
719
+ async function runAttwCheck(options) {
720
+ const task = options.flow?.start(`attw: ${options.label}`, { depth: options.flowDepth ?? 0 });
721
+ PackageLogger.info(`attw started: ${options.label} (profile: ${options.profile})`);
722
+ const attwElapsed = createElapsedTimer();
723
+ const result = await checkPackage(createPackageFromTarballData(options.tarball));
724
+ if (!result.types) {
725
+ PackageLogger.error(`[${options.label}] [attw] package has no types`);
726
+ PackageLogger.error(`attw failed: ${options.label}`, attwElapsed());
727
+ task?.fail(`attw failed: ${options.label}`);
728
+ return false;
729
+ }
730
+ const ignoredResolutions = ATTW_PROFILE_IGNORED_RESOLUTIONS[options.profile];
731
+ const problems = result.problems.filter((problem) => {
732
+ if ("resolutionKind" in problem) return !ignoredResolutions.includes(problem.resolutionKind);
733
+ return true;
700
734
  });
701
- for (const dependency of state.directWorkspaceDependencies) {
702
- const packedSpecifier = getPackedDependencySpecifier(manifest, dependency.dependencyName);
703
- if (!packedSpecifier) {
704
- state.packedManifestProblems.push({
705
- dependencyName: dependency.dependencyName,
706
- importerName: rootPackageName,
707
- message: "packed package manifest must keep every source workspace publish dependency",
708
- sectionName: dependency.sectionName
709
- });
710
- continue;
711
- }
712
- if (isWorkspaceDependencySpecifier(packedSpecifier) || isLinkDependencySpecifier(packedSpecifier)) continue;
713
- const targetVersion = dependency.targetPackage.manifest.version;
714
- if (!targetVersion || !semver.satisfies(targetVersion, packedSpecifier, { includePrerelease: true })) state.packedManifestProblems.push({
715
- dependencyName: dependency.dependencyName,
716
- importerName: rootPackageName,
717
- message: `packed dependency range must include ${dependency.targetPackage.name}@${targetVersion ?? "(missing version)"}`,
718
- sectionName: dependency.sectionName,
719
- specifier: packedSpecifier
720
- });
735
+ if (problems.length === 0) {
736
+ if (!options.flow?.interactive) PackageLogger.success(`attw passed: ${options.label}`, attwElapsed());
737
+ task?.pass();
738
+ return true;
721
739
  }
740
+ for (const problem of problems) PackageLogger.error(`[${options.label}] [attw] ${formatAttwProblem(problem)}`);
741
+ PackageLogger.error(`attw found ${problems.length} problem(s): ${options.label}`, attwElapsed());
742
+ task?.fail(`attw found ${problems.length} problem(s): ${options.label}`);
743
+ return false;
722
744
  }
723
- function createPublishOrder(rootPackageName, state) {
724
- const publishOrder = [];
725
- const seen = /* @__PURE__ */ new Set();
726
- function visit(packageName) {
727
- if (seen.has(packageName)) return;
728
- seen.add(packageName);
729
- for (const dependencyName of state.edges.get(packageName) ?? []) visit(dependencyName);
730
- if (packageName === rootPackageName || state.unpublishedPackageNames.has(packageName)) publishOrder.push(packageName);
745
+ async function runBoundaryCheck(target, label, options = {}) {
746
+ const task = options.flow?.start(`package boundary: ${label}`, { depth: options.flowDepth ?? 0 });
747
+ PackageLogger.info(`package boundary started: ${label}`);
748
+ const boundaryElapsed = createElapsedTimer();
749
+ const violations = await auditPublishedPackageBoundaries(target);
750
+ if (violations.length === 0) {
751
+ if (!options.flow?.interactive) PackageLogger.success(`package boundary passed: ${label}`, boundaryElapsed());
752
+ task?.pass();
753
+ return true;
731
754
  }
732
- visit(rootPackageName);
733
- return publishOrder;
734
- }
735
- function createReleaseConsistencyError(options) {
736
- const { config, label, outDir, rootPackageName, state } = options;
737
- if (state.sourceLinkDependencies.length + state.privateWorkspaceDependencies.length + state.missingWorkspaceDependencies.length + state.registryProblems.length + state.packedManifestProblems.length === 0) return null;
738
- const publishOrder = createPublishOrder(rootPackageName, state);
739
- const lines = [
740
- `package release dependency consistency failed for ${label}:`,
741
- ` output: ${toRelativePath(config.rootDir, outDir)}`,
742
- ...formatProblemLines("Source manifest contains local link: publish dependencies:", state.sourceLinkDependencies),
743
- ...formatProblemLines("Source manifest depends on private workspace packages:", state.privateWorkspaceDependencies),
744
- ...formatProblemLines("Source manifest has invalid workspace: publish dependencies:", state.missingWorkspaceDependencies),
745
- ...formatProblemLines("Workspace packages must be published before this package:", state.registryProblems),
746
- ...formatProblemLines("Packed package manifest is inconsistent with workspace publish dependencies:", state.packedManifestProblems)
747
- ];
748
- if (publishOrder.length > 1) lines.push("", `Suggested publish order: ${publishOrder.join(" -> ")}`);
749
- return new PackageReleaseConsistencyError(lines.join("\n"));
755
+ for (const violation of violations) PackageLogger.error(`[${label}] [boundary] ${violation.filePath} (${violation.environment}) imports "${violation.specifier}": ${violation.message}`);
756
+ PackageLogger.error(`package boundary found ${violations.length} issue(s): ${label}`, boundaryElapsed());
757
+ task?.fail(`package boundary found ${violations.length} issue(s): ${label}`);
758
+ return false;
750
759
  }
751
- async function assertPackageReleaseConsistency(options) {
752
- const workspacePackages = await collectWorkspacePackages(options.config);
753
- const sourcePackage = workspacePackages.find((workspacePackage) => workspacePackage.name === options.outputManifest.name);
754
- const state = createReleaseConsistencyState();
755
- if (sourcePackage) {
756
- state.visitedPackages.add(sourcePackage.name);
757
- await visitWorkspacePackageDependencies({
760
+ async function runPackageCheckEntry(options) {
761
+ const entry = {
762
+ ...options.rawEntry,
763
+ outDir: options.outDir
764
+ };
765
+ const label = options.label;
766
+ const outputPackageJsonPath = path.join(entry.outDir, "package.json");
767
+ const task = options.flow?.start(`package entry: ${label}`, { depth: options.flowDepth ?? 0 });
768
+ let packedDist;
769
+ try {
770
+ await readDistPackageJson({
758
771
  config: options.config,
759
- importerName: sourcePackage.name,
760
- isRoot: true,
761
- manifest: sourcePackage.manifest,
762
- state,
763
- workspacePackagesByName: new Map(workspacePackages.map((workspacePackage) => [workspacePackage.name, workspacePackage]))
772
+ label,
773
+ packageJsonPath: outputPackageJsonPath
764
774
  });
775
+ if (options.checks.includes("publint") || options.checks.includes("attw")) {
776
+ const packTask = options.flow?.start(`package tarball: ${label}`, { depth: (options.flowDepth ?? 0) + 1 });
777
+ PackageLogger.info(`package tarball packing started: ${label}`);
778
+ const packElapsed = createElapsedTimer();
779
+ try {
780
+ packedDist = await packOutputTarball(entry.outDir);
781
+ } catch (error) {
782
+ PackageLogger.error(`package tarball failed: ${label}: ${formatErrorMessage$1(error)}`, packElapsed());
783
+ packTask?.fail(`package tarball failed: ${label}`, { error });
784
+ throw error;
785
+ }
786
+ if (!options.flow?.interactive) PackageLogger.success(`package tarball packed: ${label}`, packElapsed());
787
+ packTask?.pass();
788
+ }
789
+ let passed = true;
790
+ if (options.checks.includes("publint")) passed = await runPublintCheck({
791
+ flow: options.flow,
792
+ flowDepth: (options.flowDepth ?? 0) + 1,
793
+ label,
794
+ strict: entry.publint?.strict ?? true,
795
+ tarball: packedDist.tarball
796
+ }) && passed;
797
+ if (options.checks.includes("attw")) passed = await runAttwCheck({
798
+ flow: options.flow,
799
+ flowDepth: (options.flowDepth ?? 0) + 1,
800
+ label,
801
+ profile: options.attwProfile ?? entry.attw?.profile ?? "esm-only",
802
+ tarball: packedDist.tarball
803
+ }) && passed;
804
+ if (options.checks.includes("boundary")) passed = await runBoundaryCheck({
805
+ ...entry.boundary,
806
+ outDir: entry.outDir
807
+ }, label, {
808
+ flow: options.flow,
809
+ flowDepth: (options.flowDepth ?? 0) + 1
810
+ }) && passed;
811
+ if (passed) {
812
+ if (!options.flow?.interactive) PackageLogger.success(`package checks passed: ${label}`);
813
+ task?.pass();
814
+ } else {
815
+ PackageLogger.error(`package checks failed: ${label}`);
816
+ task?.fail(`package checks failed: ${label}`);
817
+ }
818
+ return passed;
819
+ } catch (error) {
820
+ PackageLogger.error(`package checks failed: ${label}: ${formatErrorMessage$1(error)}`);
821
+ task?.fail(`package checks failed: ${label}`, { error });
822
+ throw error;
823
+ } finally {
824
+ if (packedDist) await packedDist.cleanup();
765
825
  }
766
- validatePackedManifest({
767
- manifest: await readPackedPackageJson(options.packedTarball),
768
- rootPackageName: options.outputManifest.name,
769
- state
770
- });
771
- const error = createReleaseConsistencyError({
772
- config: options.config,
773
- label: options.label,
774
- outDir: options.outDir,
775
- rootPackageName: options.outputManifest.name,
776
- state
826
+ }
827
+ async function auditPublishedPackageBoundaries(target) {
828
+ const manifest = await readDistPackageJson({ packageJsonPath: path.join(target.outDir, "package.json") });
829
+ const allowedExternalPackages = new Set([
830
+ ...Object.keys(manifest.dependencies ?? {}),
831
+ ...Object.keys(manifest.peerDependencies ?? {}),
832
+ ...Object.keys(manifest.optionalDependencies ?? {}),
833
+ ...target.ignoredExternalPackages ?? []
834
+ ]);
835
+ const selfSpecifiers = collectSelfSpecifierMatchers(manifest.name, manifest.exports);
836
+ const publishedFiles = await collectPublishedModuleFiles(target.outDir);
837
+ const violations = [];
838
+ await init;
839
+ for (const filePath of publishedFiles) {
840
+ const relativeFilePath = path.relative(target.outDir, filePath);
841
+ const environment = classifyRuntimeEnvironment(target, relativeFilePath);
842
+ const [importSpecifiers] = parse(await readFile(filePath, "utf8"));
843
+ for (const importSpecifier of importSpecifiers) {
844
+ if (!importSpecifier.n) continue;
845
+ const message = validatePublishedSpecifier({
846
+ allowedExternalPackages,
847
+ environment,
848
+ packageName: manifest.name,
849
+ selfSpecifiers,
850
+ specifier: importSpecifier.n
851
+ });
852
+ if (!message) continue;
853
+ violations.push({
854
+ environment,
855
+ filePath: relativeFilePath,
856
+ message,
857
+ specifier: importSpecifier.n
858
+ });
859
+ }
860
+ }
861
+ return violations.toSorted((left, right) => {
862
+ if (left.filePath === right.filePath) return left.specifier.localeCompare(right.specifier);
863
+ return left.filePath.localeCompare(right.filePath);
777
864
  });
778
- if (error) throw error;
865
+ }
866
+ async function runPackageCheck(options) {
867
+ if (options.clearScreen ?? true) clearCliScreen();
868
+ const elapsed = createElapsedTimer();
869
+ const cwd = path.resolve(options.cwd ?? process.cwd());
870
+ const task = options.flow?.start("package check", { depth: options.flowDepth ?? 0 });
871
+ try {
872
+ PackageLogger.info("package check started");
873
+ const plan = createPackageEntrySelectionPlan({
874
+ config: options.config,
875
+ cwd,
876
+ packageNames: options.packageNames,
877
+ strictCwd: false,
878
+ tool: options.tool
879
+ });
880
+ logPackageCheckPlan({
881
+ config: options.config,
882
+ cwd,
883
+ plan
884
+ });
885
+ const runnableEntries = plan.entries.filter((entry) => entry.checks.length > 0);
886
+ if (runnableEntries.length === 0) throw new Error(options.tool && options.tool !== "all" ? `No package entries have "${options.tool}" enabled.` : "No package checks are enabled.");
887
+ let passed = true;
888
+ for (const entry of runnableEntries) passed = await runPackageCheckEntry({
889
+ attwProfile: options.attwProfile,
890
+ checks: entry.checks,
891
+ config: options.config,
892
+ flow: options.flow,
893
+ flowDepth: (options.flowDepth ?? 0) + 1,
894
+ label: entry.label,
895
+ outDir: entry.outDir,
896
+ rawEntry: entry.rawEntry
897
+ }) && passed;
898
+ if (passed) {
899
+ if (!options.flow?.interactive) PackageLogger.success("package check finished", elapsed());
900
+ task?.pass();
901
+ } else {
902
+ PackageLogger.error("package check finished with failures", elapsed());
903
+ task?.fail("package check finished with failures");
904
+ }
905
+ return passed;
906
+ } catch (error) {
907
+ PackageLogger.error(`package check failed: ${formatErrorMessage$1(error)}`, elapsed());
908
+ task?.fail("package check failed", { error });
909
+ throw error;
910
+ }
779
911
  }
780
912
 
781
913
  //#endregion
782
- //#region src/commands/package.ts
783
- const DEFAULT_PACKAGE_CHECKS = [
784
- "publint",
785
- "attw",
786
- "boundary"
914
+ //#region src/package-exports.ts
915
+ const defaultSourceExtensions = [
916
+ ".ts",
917
+ ".tsx",
918
+ ".mts",
919
+ ".cts",
920
+ ".d.ts",
921
+ ".d.mts",
922
+ ".d.cts"
787
923
  ];
788
- const PACKAGE_CHECK_TOOLS = new Set(DEFAULT_PACKAGE_CHECKS);
789
- const ATTW_PROFILE_IGNORED_RESOLUTIONS = {
790
- strict: [],
791
- node16: [],
792
- "esm-only": ["node16-cjs"]
793
- };
794
- const nodeBuiltinSpecifiers = new Set(builtinModules.flatMap((specifier) => specifier.startsWith("node:") ? [specifier, specifier.slice(5)] : [specifier, `node:${specifier}`]));
795
- const REQUIRED_PUBLIC_PACKAGE_FILES = ["README.md", "LICENSE.md"];
796
- function isRecord(value) {
797
- return typeof value === "object" && value !== null && !Array.isArray(value);
798
- }
799
- function isPackageCheckTool(value) {
800
- return PACKAGE_CHECK_TOOLS.has(value);
801
- }
802
- function isRelativeOrAbsoluteSpecifier(specifier) {
803
- return specifier.startsWith(".") || specifier.startsWith("/") || specifier.startsWith("file:") || specifier.startsWith("http:") || specifier.startsWith("https:") || specifier.startsWith("data:");
924
+ const defaultConditionPriority = [
925
+ "source",
926
+ "development",
927
+ "types",
928
+ "import",
929
+ "module",
930
+ "default",
931
+ "require"
932
+ ];
933
+ const defaultArtifactDirectories = [
934
+ "dist",
935
+ "build",
936
+ "lib",
937
+ "esm",
938
+ "cjs",
939
+ "out"
940
+ ];
941
+ function configuredArtifactDirectories(config) {
942
+ return config.paths?.artifactDirectories ?? defaultArtifactDirectories;
804
943
  }
805
- function toArrayBuffer(buffer) {
806
- return buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
944
+ function configuredSourceExtensions(config) {
945
+ return config.paths?.sourceExtensions ?? defaultSourceExtensions;
807
946
  }
808
- function collectSelfSpecifierMatchers(packageName, exportsField) {
809
- const exact = new Set([packageName]);
810
- const prefixes = [];
811
- if (!isRecord(exportsField)) return {
812
- exact,
813
- prefixes
814
- };
815
- for (const exportKey of Object.keys(exportsField)) {
816
- if (exportKey === ".") {
817
- exact.add(packageName);
818
- continue;
819
- }
820
- if (!exportKey.startsWith("./")) continue;
821
- const normalizedSubpath = exportKey.slice(2);
822
- if (normalizedSubpath.length === 0) continue;
823
- const wildcardIndex = normalizedSubpath.indexOf("*");
824
- if (wildcardIndex !== -1) {
825
- prefixes.push(`${packageName}/${normalizedSubpath.slice(0, wildcardIndex)}`);
826
- continue;
827
- }
828
- exact.add(`${packageName}/${normalizedSubpath}`);
947
+ function collectTargetCandidates(config, value) {
948
+ if (typeof value === "string") return [value];
949
+ if (!value || typeof value !== "object" || Array.isArray(value)) return [];
950
+ const record = value;
951
+ const candidates = [];
952
+ const visitedKeys = /* @__PURE__ */ new Set();
953
+ for (const key of config.paths?.conditionPriority ?? defaultConditionPriority) {
954
+ if (!(key in record)) continue;
955
+ visitedKeys.add(key);
956
+ candidates.push(...collectTargetCandidates(config, record[key]));
829
957
  }
830
- return {
831
- exact,
832
- prefixes
833
- };
834
- }
835
- function isAllowedSelfSpecifier(specifier, matchers) {
836
- return matchers.exact.has(specifier) || matchers.prefixes.some((prefix) => specifier.startsWith(prefix));
958
+ for (const key of Object.keys(record).sort()) {
959
+ if (visitedKeys.has(key)) continue;
960
+ candidates.push(...collectTargetCandidates(config, record[key]));
961
+ }
962
+ return candidates;
837
963
  }
838
- function normalizePublishedModulePath(relativeFilePath) {
839
- return relativeFilePath.replaceAll("\\", "/");
964
+ function normalizePackageTarget(target) {
965
+ if (!target.startsWith("./")) return null;
966
+ return target.slice(2);
840
967
  }
841
- function classifyRuntimeEnvironment(target, relativeFilePath) {
842
- if (typeof target.environment === "function") return target.environment(relativeFilePath);
843
- if (target.environment) return target.environment;
844
- const normalizedPath = normalizePublishedModulePath(relativeFilePath);
845
- return normalizedPath.startsWith("node/") || normalizedPath.startsWith("plugin/") ? "node" : "browser";
968
+ function packageExportKeyToAlias(packageName, exportKey) {
969
+ if (exportKey === ".") return packageName;
970
+ if (!exportKey.startsWith("./")) return "";
971
+ return `${packageName}/${exportKey.slice(2)}`;
846
972
  }
847
- async function collectPublishedModuleFiles(directoryPath) {
848
- const entries = await readdir(directoryPath, { withFileTypes: true });
849
- const files = [];
850
- for (const entry of entries) {
851
- const absolutePath = path.join(directoryPath, entry.name);
852
- if (entry.isDirectory()) {
853
- files.push(...await collectPublishedModuleFiles(absolutePath));
854
- continue;
855
- }
856
- if (/\.[cm]?js$/u.test(entry.name)) files.push(absolutePath);
857
- }
858
- return files;
973
+ function removeKnownExtension(filePath) {
974
+ for (const extension of [
975
+ ".d.mts",
976
+ ".d.cts",
977
+ ".d.ts",
978
+ ".mts",
979
+ ".cts",
980
+ ".mjs",
981
+ ".cjs",
982
+ ".js",
983
+ ".tsx",
984
+ ".ts"
985
+ ]) if (filePath.endsWith(extension)) return filePath.slice(0, -extension.length);
986
+ return filePath;
859
987
  }
860
- function validatePublishedSpecifier(options) {
861
- const { allowedExternalPackages, environment, packageName, selfSpecifiers, specifier } = options;
862
- if (isRelativeOrAbsoluteSpecifier(specifier)) return null;
863
- if (nodeBuiltinSpecifiers.has(specifier)) {
864
- if (environment === "node") return null;
865
- return `browser/runtime output must not import Node builtin "${specifier}"`;
866
- }
867
- const packageRoot = getPackageRootSpecifier(specifier);
868
- if (packageRoot === packageName) {
869
- if (isAllowedSelfSpecifier(specifier, selfSpecifiers)) return null;
870
- return `self import "${specifier}" is not exposed by output package.json exports`;
871
- }
872
- if (allowedExternalPackages.has(packageRoot)) return null;
873
- return `"${specifier}" resolves to package "${packageRoot}" which is not listed in dependencies, peerDependencies, optionalDependencies, or self exports`;
988
+ function hasConfiguredSourceExtension(config, target) {
989
+ return configuredSourceExtensions(config).some((extension) => target.endsWith(extension));
874
990
  }
875
- function formatAttwProblem(problem) {
876
- const resolutionKind = "resolutionKind" in problem ? ` [resolution: ${problem.resolutionKind}]` : "";
877
- const entrypoint = "entrypoint" in problem ? ` [entrypoint: ${problem.entrypoint}]` : "";
878
- switch (problem.kind) {
879
- case "NoResolution": return `No resolution${resolutionKind}${entrypoint}`;
880
- case "UntypedResolution": return `Untyped resolution${resolutionKind}${entrypoint}`;
881
- case "FalseESM": return `False ESM: ${problem.typesFileName} -> ${problem.implementationFileName}`;
882
- case "FalseCJS": return `False CJS: ${problem.typesFileName} -> ${problem.implementationFileName}`;
883
- case "CJSResolvesToESM": return `CJS resolves to ESM${resolutionKind}${entrypoint}`;
884
- case "FallbackCondition": return `Fallback condition used${resolutionKind}${entrypoint}`;
885
- case "NamedExports": return problem.isMissingAllNamed ? `Named exports missing: all named exports [types: ${problem.typesFileName}] [implementation: ${problem.implementationFileName}]` : `Named exports missing: ${problem.missing.join(", ") || "(none)"} [types: ${problem.typesFileName}] [implementation: ${problem.implementationFileName}]`;
886
- case "FalseExportDefault": return `False export default [types: ${problem.typesFileName}] [implementation: ${problem.implementationFileName}]`;
887
- case "MissingExportEquals": return `Missing export equals [types: ${problem.typesFileName}] [implementation: ${problem.implementationFileName}]`;
888
- case "InternalResolutionError": return `Internal resolution error in ${problem.fileName} [option: ${problem.resolutionOption}] [module: ${problem.moduleSpecifier}]`;
889
- case "UnexpectedModuleSyntax": return `Unexpected module syntax in ${problem.fileName}`;
890
- case "CJSOnlyExportsDefault": return `CJS only exports default in ${problem.fileName}`;
891
- default: return `Unknown ATTW problem: ${JSON.stringify(problem)}`;
892
- }
991
+ function isInsideArtifactDirectory(config, target) {
992
+ const normalizedTarget = normalizeSlashes(target);
993
+ return configuredArtifactDirectories(config).some((directoryName) => {
994
+ const normalizedDirectoryName = normalizeSlashes(directoryName).replace(/\/+$/u, "");
995
+ return normalizedTarget === normalizedDirectoryName || normalizedTarget.startsWith(`${normalizedDirectoryName}/`);
996
+ });
893
997
  }
894
- function normalizeTargetChecks(target) {
895
- const checks = target.checks ?? DEFAULT_PACKAGE_CHECKS;
896
- const normalizedChecks = [];
897
- for (const check of checks) {
898
- if (!isPackageCheckTool(check)) throw new Error(`Invalid package check "${check}". Expected one of: publint, attw, boundary.`);
899
- if (!normalizedChecks.includes(check)) normalizedChecks.push(check);
998
+ function isLikelySourceTarget(config, target) {
999
+ return hasConfiguredSourceExtension(config, target) && !isInsideArtifactDirectory(config, target);
1000
+ }
1001
+ function stripArtifactPrefix(config, target) {
1002
+ const normalizedTarget = normalizeSlashes(target);
1003
+ for (const directoryName of configuredArtifactDirectories(config)) {
1004
+ const normalizedDirectoryName = normalizeSlashes(directoryName).replace(/\/+$/u, "");
1005
+ if (normalizedTarget.startsWith(`${normalizedDirectoryName}/`)) return normalizedTarget.slice(normalizedDirectoryName.length + 1);
900
1006
  }
901
- return normalizedChecks;
1007
+ return normalizedTarget;
902
1008
  }
903
- function selectTargetChecks(target, requestedTool) {
904
- const configuredChecks = normalizeTargetChecks(target);
905
- if (!requestedTool || requestedTool === "all") return configuredChecks;
906
- return configuredChecks.includes(requestedTool) ? [requestedTool] : [];
1009
+ function sourceFileCandidates(config, target) {
1010
+ const normalizedTarget = normalizeSlashes(target);
1011
+ const withoutKnownExtension = removeKnownExtension(normalizedTarget);
1012
+ const withoutArtifactPrefix = stripArtifactPrefix(config, normalizedTarget);
1013
+ const sourceBase = removeKnownExtension(withoutArtifactPrefix);
1014
+ const bases = withoutArtifactPrefix === normalizedTarget ? [
1015
+ withoutKnownExtension,
1016
+ sourceBase,
1017
+ `src/${sourceBase}`,
1018
+ `${sourceBase}/index`,
1019
+ `src/${sourceBase}/index`
1020
+ ] : [
1021
+ sourceBase,
1022
+ `src/${sourceBase}`,
1023
+ `${sourceBase}/index`,
1024
+ `src/${sourceBase}/index`
1025
+ ];
1026
+ const candidates = [];
1027
+ for (const base of bases) for (const extension of configuredSourceExtensions(config)) candidates.push(`${base}${extension}`);
1028
+ return [...new Set(candidates)];
907
1029
  }
908
- function findNearestPackageJsonPath(cwd, rootDir) {
909
- const resolvedRootDir = path.resolve(rootDir);
910
- let currentDir = path.resolve(cwd);
911
- while (true) {
912
- const relativeToRoot = path.relative(resolvedRootDir, currentDir);
913
- if (!(relativeToRoot === "" || relativeToRoot !== ".." && !relativeToRoot.startsWith(`..${path.sep}`) && !path.isAbsolute(relativeToRoot))) return;
914
- const packageJsonPath = path.join(currentDir, "package.json");
915
- if (existsSync(packageJsonPath)) return packageJsonPath;
916
- if (currentDir === resolvedRootDir) return;
917
- const parentDir = path.dirname(currentDir);
918
- if (parentDir === currentDir) return;
919
- currentDir = parentDir;
920
- }
1030
+ function wildcardBaseDirectory(pattern) {
1031
+ const wildcardIndex = pattern.indexOf("*");
1032
+ const prefix = wildcardIndex === -1 ? pattern : pattern.slice(0, wildcardIndex);
1033
+ const lastSlashIndex = prefix.lastIndexOf("/");
1034
+ return lastSlashIndex === -1 ? "." : prefix.slice(0, lastSlashIndex);
921
1035
  }
922
- function readCwdPackageName(cwd, rootDir) {
923
- const packageJsonPath = findNearestPackageJsonPath(cwd, rootDir);
924
- if (!packageJsonPath) return;
925
- try {
926
- const manifest = JSON.parse(readFileSync(packageJsonPath, "utf8"));
927
- return typeof manifest.name === "string" && manifest.name.trim() ? manifest.name.trim() : void 0;
928
- } catch (error) {
929
- throw new Error(`Unable to read package name from ${packageJsonPath}: ${formatErrorMessage$1(error)}`);
1036
+ function sourceWildcardPatternCandidates(config, target) {
1037
+ const strippedPattern = stripArtifactPrefix(config, target);
1038
+ const sourcePattern = removeKnownExtension(strippedPattern);
1039
+ const preferSrcPrefix = strippedPattern !== normalizeSlashes(target);
1040
+ const candidates = [];
1041
+ for (const extension of configuredSourceExtensions(config)) if (preferSrcPrefix) {
1042
+ candidates.push(`src/${sourcePattern}${extension}`);
1043
+ candidates.push(`${sourcePattern}${extension}`);
1044
+ } else {
1045
+ candidates.push(`${sourcePattern}${extension}`);
1046
+ candidates.push(`src/${sourcePattern}${extension}`);
930
1047
  }
1048
+ return [...new Set(candidates)];
931
1049
  }
932
- function formatConfiguredTargetNames(targets) {
933
- const names = targets.map((target) => target.name).filter((name) => Boolean(name));
934
- return names.length > 0 ? names.join(", ") : "(none)";
935
- }
936
- function resolveTargetOutDir(options) {
937
- const outDir = options.target.outDir;
938
- if (typeof outDir !== "string" || outDir.trim().length === 0) throw new Error(`Invalid package check target at packageChecks.targets[${options.targetIndex}].outDir. Expected a non-empty string.`);
939
- return path.resolve(options.config.rootDir, outDir);
1050
+ function resolveWildcardTarget(config, packageDirectory, target) {
1051
+ const sourcePatterns = sourceWildcardPatternCandidates(config, target);
1052
+ const candidatePatterns = isLikelySourceTarget(config, target) ? [target, ...sourcePatterns] : sourcePatterns;
1053
+ for (const candidatePattern of candidatePatterns) {
1054
+ const baseDirectory = wildcardBaseDirectory(candidatePattern);
1055
+ if (existsSync(path.join(packageDirectory, baseDirectory))) return toPosixPath(path.join(toRelativePath(config.rootDir, packageDirectory), candidatePattern));
1056
+ }
1057
+ return null;
940
1058
  }
941
- function getTargetLabel(config, target, outDir) {
942
- return target.name ?? toRelativePath(config.rootDir, outDir);
1059
+ function resolveExactTarget(config, packageDirectory, target) {
1060
+ const absoluteTarget = path.join(packageDirectory, target);
1061
+ if (existsSync(absoluteTarget) && isLikelySourceTarget(config, target)) return normalizeWorkspacePath(config.rootDir, absoluteTarget);
1062
+ for (const candidate of sourceFileCandidates(config, target)) {
1063
+ const absoluteCandidate = path.join(packageDirectory, candidate);
1064
+ if (existsSync(absoluteCandidate)) return normalizeWorkspacePath(config.rootDir, absoluteCandidate);
1065
+ }
1066
+ return null;
943
1067
  }
944
- function createTargetPlan(options) {
945
- const outDir = resolveTargetOutDir({
946
- config: options.config,
947
- target: options.target,
948
- targetIndex: options.targetIndex
949
- });
950
- return {
951
- checks: selectTargetChecks(options.target, options.requestedTool),
952
- label: getTargetLabel(options.config, options.target, outDir),
953
- outDir,
954
- rawTarget: options.target
955
- };
1068
+ function resolvePackageTarget(config, packageDirectory, rawTarget) {
1069
+ const target = normalizePackageTarget(rawTarget);
1070
+ if (!target) return null;
1071
+ if (target.includes("*")) return resolveWildcardTarget(config, packageDirectory, target);
1072
+ return resolveExactTarget(config, packageDirectory, target);
956
1073
  }
957
- function createPackageCheckPlan(options) {
958
- const targets = options.config.packageChecks?.targets ?? [];
959
- if (targets.length === 0) throw new Error("No package check targets are configured.");
960
- let selectedTargets;
961
- let selectionReason;
962
- if (options.targetName) {
963
- selectedTargets = targets.filter((target) => target.name === options.targetName);
964
- if (selectedTargets.length === 0) throw new Error([`No package check target named "${options.targetName}" is configured.`, `Configured target names: ${formatConfiguredTargetNames(targets)}.`].join(" "));
965
- selectionReason = `--package "${options.targetName}" matched configured target name.`;
966
- } else {
967
- const cwdPackageName = readCwdPackageName(options.cwd, options.config.rootDir);
968
- if (cwdPackageName) {
969
- selectedTargets = targets.filter((target) => target.name === cwdPackageName);
970
- if (selectedTargets.length > 0) selectionReason = `nearest package.json name "${cwdPackageName}" matched configured target name.`;
971
- else {
972
- selectedTargets = targets;
973
- selectionReason = `nearest package.json name "${cwdPackageName}" did not match configured target names; running all configured targets.`;
1074
+ function collectExportEntries(config, workspacePackage) {
1075
+ const exportsField = workspacePackage.manifest.exports;
1076
+ if (!exportsField) return [];
1077
+ const exportEntries = typeof exportsField === "object" && exportsField !== null && !Array.isArray(exportsField) && Object.keys(exportsField).some((key) => key.startsWith(".")) ? Object.entries(exportsField) : [[".", exportsField]];
1078
+ const entries = [];
1079
+ for (const [exportKey, exportValue] of exportEntries.sort(([left], [right]) => left.localeCompare(right))) {
1080
+ const alias = packageExportKeyToAlias(workspacePackage.name, exportKey);
1081
+ if (!alias) continue;
1082
+ for (const candidate of collectTargetCandidates(config, exportValue)) {
1083
+ const resolvedTarget = resolvePackageTarget(config, workspacePackage.directory, candidate);
1084
+ if (resolvedTarget) {
1085
+ entries.push([alias, resolvedTarget]);
1086
+ break;
974
1087
  }
975
- } else {
976
- selectedTargets = targets;
977
- selectionReason = "No package name was found from cwd up to the workspace root; running all configured targets.";
978
1088
  }
979
1089
  }
980
- return {
981
- selectionReason,
982
- targets: selectedTargets.map((target) => createTargetPlan({
983
- config: options.config,
984
- requestedTool: options.tool,
985
- target,
986
- targetIndex: targets.indexOf(target)
987
- }))
988
- };
1090
+ return entries;
989
1091
  }
990
- function logPackageCheckPlan(options) {
991
- PackageLogger.info([
992
- "Package check plan:",
993
- ` config: ${toRelativePath(options.config.rootDir, options.config.configPath)}`,
994
- ` cwd: ${toRelativePath(options.config.rootDir, options.cwd)}`,
995
- ` selection: ${options.plan.selectionReason}`,
996
- " targets:",
997
- ...options.plan.targets.map((target) => [
998
- ` - ${target.label}`,
999
- ` outDir: ${toRelativePath(options.config.rootDir, target.outDir)}`,
1000
- ` checks: ${target.checks.length > 0 ? target.checks.join(", ") : "(none)"}`
1001
- ].join("\n"))
1002
- ].join("\n"));
1092
+ function aliasMatchesSpecifier(alias, specifier) {
1093
+ if (alias === specifier) return true;
1094
+ const wildcardIndex = alias.indexOf("*");
1095
+ if (wildcardIndex === -1) return false;
1096
+ const prefix = alias.slice(0, wildcardIndex);
1097
+ const suffix = alias.slice(wildcardIndex + 1);
1098
+ return specifier.startsWith(prefix) && specifier.endsWith(suffix);
1003
1099
  }
1004
- async function packOutputTarball(outDir) {
1005
- const destination = await mkdtemp(path.join(tmpdir(), "__LIMINA_PACKAGE__"));
1006
- return {
1007
- cleanup: async () => {
1008
- await rm(destination, {
1009
- force: true,
1010
- recursive: true
1011
- }).catch(() => null);
1012
- },
1013
- tarball: await readFile(await pack(outDir, {
1014
- destination,
1015
- ignoreScripts: true,
1016
- packageManager: "pnpm"
1017
- }))
1018
- };
1100
+
1101
+ //#endregion
1102
+ //#region src/commands/paths.ts
1103
+ function generatedFileName(config) {
1104
+ return config.paths?.generatedFileName ?? "tsconfig.dts.paths.generated.json";
1019
1105
  }
1020
- async function readDistPackageJson(options) {
1021
- if (!existsSync(options.packageJsonPath)) throw new Error(`outDir package.json not found${options.label ? ` for ${options.label}` : ""} at ${options.config ? toRelativePath(options.config.rootDir, options.packageJsonPath) : options.packageJsonPath}. Run the package build first.`);
1022
- return JSON.parse(await readFile(options.packageJsonPath, "utf8"));
1106
+ function generatedFileMarker(config) {
1107
+ return config.paths?.generatedFileMarker ?? "GENERATED FILE - DO NOT EDIT BY HAND.";
1023
1108
  }
1024
- async function assertPublicPackageMetadata(options) {
1025
- const manifest = await readDistPackageJson({
1026
- config: options.config,
1027
- label: options.label,
1028
- packageJsonPath: options.packageJsonPath
1109
+ function parseProject(config, configPath) {
1110
+ const diagnostics = [];
1111
+ const parsed = ts.getParsedCommandLineOfConfigFile(configPath, {}, {
1112
+ ...ts.sys,
1113
+ onUnRecoverableConfigFileDiagnostic: (diagnostic) => {
1114
+ diagnostics.push(diagnostic);
1115
+ }
1029
1116
  });
1030
- if (manifest.private === true) return manifest;
1031
- const missingFiles = REQUIRED_PUBLIC_PACKAGE_FILES.filter((fileName) => !existsSync(path.join(options.outDir, fileName)));
1032
- if (missingFiles.length === 0) return manifest;
1033
- throw new Error(`publishable package output for ${options.label} at ${toRelativePath(options.config.rootDir, options.outDir)} is missing required file(s): ${missingFiles.join(", ")}. Add them to the built output or set "private": true in the output package.json.`);
1034
- }
1035
- async function runPublintCheck(options) {
1036
- const task = options.flow?.start(`publint: ${options.label}`, { depth: options.flowDepth ?? 0 });
1037
- PackageLogger.info(`publint started: ${options.label}`);
1038
- const publintElapsed = createElapsedTimer();
1039
- const { messages, pkg } = await publint({
1040
- pack: { tarball: toArrayBuffer(options.tarball) },
1041
- strict: options.strict
1042
- });
1043
- if (messages.length === 0) {
1044
- if (!options.flow?.interactive) PackageLogger.success(`publint passed: ${options.label}`, publintElapsed());
1045
- task?.pass();
1046
- return true;
1047
- }
1048
- for (const message of messages) {
1049
- const rendered = formatMessage(message, pkg) ?? message.code;
1050
- if (message.type === "error") {
1051
- PackageLogger.error(`[${options.label}] [publint] ${rendered}`);
1052
- continue;
1053
- }
1054
- if (message.type === "warning") {
1055
- PackageLogger.warn(`[${options.label}] [publint] ${rendered}`);
1056
- continue;
1057
- }
1058
- PackageLogger.info(`[${options.label}] [publint] ${rendered}`);
1059
- }
1060
- PackageLogger.error(`publint found ${messages.length} issue(s): ${options.label}`, publintElapsed());
1061
- task?.fail(`publint found ${messages.length} issue(s): ${options.label}`);
1062
- return false;
1063
- }
1064
- async function runAttwCheck(options) {
1065
- const task = options.flow?.start(`attw: ${options.label}`, { depth: options.flowDepth ?? 0 });
1066
- PackageLogger.info(`attw started: ${options.label} (profile: ${options.profile})`);
1067
- const attwElapsed = createElapsedTimer();
1068
- const result = await checkPackage(createPackageFromTarballData(options.tarball));
1069
- if (!result.types) {
1070
- PackageLogger.error(`[${options.label}] [attw] package has no types`);
1071
- PackageLogger.error(`attw failed: ${options.label}`, attwElapsed());
1072
- task?.fail(`attw failed: ${options.label}`);
1073
- return false;
1074
- }
1075
- const ignoredResolutions = ATTW_PROFILE_IGNORED_RESOLUTIONS[options.profile];
1076
- const problems = result.problems.filter((problem) => {
1077
- if ("resolutionKind" in problem) return !ignoredResolutions.includes(problem.resolutionKind);
1078
- return true;
1079
- });
1080
- if (problems.length === 0) {
1081
- if (!options.flow?.interactive) PackageLogger.success(`attw passed: ${options.label}`, attwElapsed());
1082
- task?.pass();
1083
- return true;
1084
- }
1085
- for (const problem of problems) PackageLogger.error(`[${options.label}] [attw] ${formatAttwProblem(problem)}`);
1086
- PackageLogger.error(`attw found ${problems.length} problem(s): ${options.label}`, attwElapsed());
1087
- task?.fail(`attw found ${problems.length} problem(s): ${options.label}`);
1088
- return false;
1089
- }
1090
- async function runBoundaryCheck(target, label, options = {}) {
1091
- const task = options.flow?.start(`package boundary: ${label}`, { depth: options.flowDepth ?? 0 });
1092
- PackageLogger.info(`package boundary started: ${label}`);
1093
- const boundaryElapsed = createElapsedTimer();
1094
- const violations = await auditPublishedPackageBoundaries(target);
1095
- if (violations.length === 0) {
1096
- if (!options.flow?.interactive) PackageLogger.success(`package boundary passed: ${label}`, boundaryElapsed());
1097
- task?.pass();
1098
- return true;
1099
- }
1100
- for (const violation of violations) PackageLogger.error(`[${label}] [boundary] ${violation.filePath} (${violation.environment}) imports "${violation.specifier}": ${violation.message}`);
1101
- PackageLogger.error(`package boundary found ${violations.length} issue(s): ${label}`, boundaryElapsed());
1102
- task?.fail(`package boundary found ${violations.length} issue(s): ${label}`);
1103
- return false;
1104
- }
1105
- async function runPackageCheckTarget(options) {
1106
- const target = {
1107
- ...options.rawTarget,
1108
- outDir: options.outDir
1109
- };
1110
- const label = options.label;
1111
- const outputPackageJsonPath = path.join(target.outDir, "package.json");
1112
- const task = options.flow?.start(`package target: ${label}`, { depth: options.flowDepth ?? 0 });
1113
- let packedDist;
1114
- try {
1115
- const outputManifest = await assertPublicPackageMetadata({
1116
- config: options.config,
1117
- label,
1118
- outDir: target.outDir,
1119
- packageJsonPath: outputPackageJsonPath
1120
- });
1121
- if (outputManifest.private !== true || options.checks.includes("publint") || options.checks.includes("attw")) {
1122
- const packTask = options.flow?.start(`package tarball: ${label}`, { depth: (options.flowDepth ?? 0) + 1 });
1123
- PackageLogger.info(`package tarball packing started: ${label}`);
1124
- const packElapsed = createElapsedTimer();
1125
- try {
1126
- packedDist = await packOutputTarball(target.outDir);
1127
- } catch (error) {
1128
- PackageLogger.error(`package tarball failed: ${label}: ${formatErrorMessage$1(error)}`, packElapsed());
1129
- packTask?.fail(`package tarball failed: ${label}`, { error });
1130
- throw error;
1131
- }
1132
- if (!options.flow?.interactive) PackageLogger.success(`package tarball packed: ${label}`, packElapsed());
1133
- packTask?.pass();
1134
- }
1135
- if (outputManifest.private !== true) try {
1136
- await assertPackageReleaseConsistency({
1137
- config: options.config,
1138
- label,
1139
- outDir: target.outDir,
1140
- outputManifest,
1141
- packedTarball: packedDist.tarball
1142
- });
1143
- } catch (error) {
1144
- if (!(error instanceof PackageReleaseConsistencyError)) throw error;
1145
- PackageLogger.error(formatErrorMessage$1(error));
1146
- task?.fail(`package checks failed: ${label}`);
1147
- return false;
1148
- }
1149
- let passed = true;
1150
- if (options.checks.includes("publint")) passed = await runPublintCheck({
1151
- flow: options.flow,
1152
- flowDepth: (options.flowDepth ?? 0) + 1,
1153
- label,
1154
- strict: target.publint?.strict ?? true,
1155
- tarball: packedDist.tarball
1156
- }) && passed;
1157
- if (options.checks.includes("attw")) passed = await runAttwCheck({
1158
- flow: options.flow,
1159
- flowDepth: (options.flowDepth ?? 0) + 1,
1160
- label,
1161
- profile: options.attwProfile ?? target.attw?.profile ?? "esm-only",
1162
- tarball: packedDist.tarball
1163
- }) && passed;
1164
- if (options.checks.includes("boundary")) passed = await runBoundaryCheck({
1165
- ...target.boundary,
1166
- outDir: target.outDir
1167
- }, label, {
1168
- flow: options.flow,
1169
- flowDepth: (options.flowDepth ?? 0) + 1
1170
- }) && passed;
1171
- if (passed) {
1172
- if (!options.flow?.interactive) PackageLogger.success(`package checks passed: ${label}`);
1173
- task?.pass();
1174
- } else {
1175
- PackageLogger.error(`package checks failed: ${label}`);
1176
- task?.fail(`package checks failed: ${label}`);
1177
- }
1178
- return passed;
1179
- } catch (error) {
1180
- PackageLogger.error(`package checks failed: ${label}: ${formatErrorMessage$1(error)}`);
1181
- task?.fail(`package checks failed: ${label}`, { error });
1182
- throw error;
1183
- } finally {
1184
- if (packedDist) await packedDist.cleanup();
1185
- }
1186
- }
1187
- async function auditPublishedPackageBoundaries(target) {
1188
- const manifest = await readDistPackageJson({ packageJsonPath: path.join(target.outDir, "package.json") });
1189
- const allowedExternalPackages = new Set([
1190
- ...Object.keys(manifest.dependencies ?? {}),
1191
- ...Object.keys(manifest.peerDependencies ?? {}),
1192
- ...Object.keys(manifest.optionalDependencies ?? {}),
1193
- ...target.ignoredExternalPackages ?? []
1194
- ]);
1195
- const selfSpecifiers = collectSelfSpecifierMatchers(manifest.name, manifest.exports);
1196
- const publishedFiles = await collectPublishedModuleFiles(target.outDir);
1197
- const violations = [];
1198
- await init;
1199
- for (const filePath of publishedFiles) {
1200
- const relativeFilePath = path.relative(target.outDir, filePath);
1201
- const environment = classifyRuntimeEnvironment(target, relativeFilePath);
1202
- const [importSpecifiers] = parse(await readFile(filePath, "utf8"));
1203
- for (const importSpecifier of importSpecifiers) {
1204
- if (!importSpecifier.n) continue;
1205
- const message = validatePublishedSpecifier({
1206
- allowedExternalPackages,
1207
- environment,
1208
- packageName: manifest.name,
1209
- selfSpecifiers,
1210
- specifier: importSpecifier.n
1211
- });
1212
- if (!message) continue;
1213
- violations.push({
1214
- environment,
1215
- filePath: relativeFilePath,
1216
- message,
1217
- specifier: importSpecifier.n
1218
- });
1219
- }
1220
- }
1221
- return violations.toSorted((left, right) => {
1222
- if (left.filePath === right.filePath) return left.specifier.localeCompare(right.specifier);
1223
- return left.filePath.localeCompare(right.filePath);
1224
- });
1225
- }
1226
- async function runPackageCheck(options) {
1227
- if (options.clearScreen ?? true) clearCliScreen();
1228
- const elapsed = createElapsedTimer();
1229
- const cwd = path.resolve(options.cwd ?? process.cwd());
1230
- const task = options.flow?.start("package check", { depth: options.flowDepth ?? 0 });
1231
- try {
1232
- PackageLogger.info("package check started");
1233
- const plan = createPackageCheckPlan({
1234
- config: options.config,
1235
- cwd,
1236
- targetName: options.targetName,
1237
- tool: options.tool
1238
- });
1239
- logPackageCheckPlan({
1240
- config: options.config,
1241
- cwd,
1242
- plan
1243
- });
1244
- const runnableTargets = plan.targets.filter((target) => target.checks.length > 0);
1245
- if (runnableTargets.length === 0) throw new Error(options.tool && options.tool !== "all" ? `No package check targets have "${options.tool}" enabled.` : "No package checks are enabled.");
1246
- let passed = true;
1247
- for (const target of runnableTargets) passed = await runPackageCheckTarget({
1248
- attwProfile: options.attwProfile,
1249
- checks: target.checks,
1250
- config: options.config,
1251
- flow: options.flow,
1252
- flowDepth: (options.flowDepth ?? 0) + 1,
1253
- label: target.label,
1254
- outDir: target.outDir,
1255
- rawTarget: target.rawTarget
1256
- }) && passed;
1257
- if (passed) {
1258
- if (!options.flow?.interactive) PackageLogger.success("package check finished", elapsed());
1259
- task?.pass();
1260
- } else {
1261
- PackageLogger.error("package check finished with failures", elapsed());
1262
- task?.fail("package check finished with failures");
1263
- }
1264
- return passed;
1265
- } catch (error) {
1266
- PackageLogger.error(`package check failed: ${formatErrorMessage$1(error)}`, elapsed());
1267
- task?.fail("package check failed", { error });
1268
- throw error;
1269
- }
1270
- }
1271
-
1272
- //#endregion
1273
- //#region src/package-exports.ts
1274
- const defaultSourceExtensions = [
1275
- ".ts",
1276
- ".tsx",
1277
- ".mts",
1278
- ".cts",
1279
- ".d.ts",
1280
- ".d.mts",
1281
- ".d.cts"
1282
- ];
1283
- const defaultConditionPriority = [
1284
- "source",
1285
- "development",
1286
- "types",
1287
- "import",
1288
- "module",
1289
- "default",
1290
- "require"
1291
- ];
1292
- const defaultArtifactDirectories = [
1293
- "dist",
1294
- "build",
1295
- "lib",
1296
- "esm",
1297
- "cjs",
1298
- "out"
1299
- ];
1300
- function configuredArtifactDirectories(config) {
1301
- return config.paths?.artifactDirectories ?? defaultArtifactDirectories;
1302
- }
1303
- function configuredSourceExtensions(config) {
1304
- return config.paths?.sourceExtensions ?? defaultSourceExtensions;
1305
- }
1306
- function collectTargetCandidates(config, value) {
1307
- if (typeof value === "string") return [value];
1308
- if (!value || typeof value !== "object" || Array.isArray(value)) return [];
1309
- const record = value;
1310
- const candidates = [];
1311
- const visitedKeys = /* @__PURE__ */ new Set();
1312
- for (const key of config.paths?.conditionPriority ?? defaultConditionPriority) {
1313
- if (!(key in record)) continue;
1314
- visitedKeys.add(key);
1315
- candidates.push(...collectTargetCandidates(config, record[key]));
1316
- }
1317
- for (const key of Object.keys(record).sort()) {
1318
- if (visitedKeys.has(key)) continue;
1319
- candidates.push(...collectTargetCandidates(config, record[key]));
1320
- }
1321
- return candidates;
1322
- }
1323
- function normalizePackageTarget(target) {
1324
- if (!target.startsWith("./")) return null;
1325
- return target.slice(2);
1326
- }
1327
- function packageExportKeyToAlias(packageName, exportKey) {
1328
- if (exportKey === ".") return packageName;
1329
- if (!exportKey.startsWith("./")) return "";
1330
- return `${packageName}/${exportKey.slice(2)}`;
1331
- }
1332
- function removeKnownExtension(filePath) {
1333
- for (const extension of [
1334
- ".d.mts",
1335
- ".d.cts",
1336
- ".d.ts",
1337
- ".mts",
1338
- ".cts",
1339
- ".mjs",
1340
- ".cjs",
1341
- ".js",
1342
- ".tsx",
1343
- ".ts"
1344
- ]) if (filePath.endsWith(extension)) return filePath.slice(0, -extension.length);
1345
- return filePath;
1346
- }
1347
- function hasConfiguredSourceExtension(config, target) {
1348
- return configuredSourceExtensions(config).some((extension) => target.endsWith(extension));
1349
- }
1350
- function isInsideArtifactDirectory(config, target) {
1351
- const normalizedTarget = normalizeSlashes(target);
1352
- return configuredArtifactDirectories(config).some((directoryName) => {
1353
- const normalizedDirectoryName = normalizeSlashes(directoryName).replace(/\/+$/u, "");
1354
- return normalizedTarget === normalizedDirectoryName || normalizedTarget.startsWith(`${normalizedDirectoryName}/`);
1355
- });
1356
- }
1357
- function isLikelySourceTarget(config, target) {
1358
- return hasConfiguredSourceExtension(config, target) && !isInsideArtifactDirectory(config, target);
1359
- }
1360
- function stripArtifactPrefix(config, target) {
1361
- const normalizedTarget = normalizeSlashes(target);
1362
- for (const directoryName of configuredArtifactDirectories(config)) {
1363
- const normalizedDirectoryName = normalizeSlashes(directoryName).replace(/\/+$/u, "");
1364
- if (normalizedTarget.startsWith(`${normalizedDirectoryName}/`)) return normalizedTarget.slice(normalizedDirectoryName.length + 1);
1365
- }
1366
- return normalizedTarget;
1367
- }
1368
- function sourceFileCandidates(config, target) {
1369
- const normalizedTarget = normalizeSlashes(target);
1370
- const withoutKnownExtension = removeKnownExtension(normalizedTarget);
1371
- const withoutArtifactPrefix = stripArtifactPrefix(config, normalizedTarget);
1372
- const sourceBase = removeKnownExtension(withoutArtifactPrefix);
1373
- const bases = withoutArtifactPrefix === normalizedTarget ? [
1374
- withoutKnownExtension,
1375
- sourceBase,
1376
- `src/${sourceBase}`,
1377
- `${sourceBase}/index`,
1378
- `src/${sourceBase}/index`
1379
- ] : [
1380
- sourceBase,
1381
- `src/${sourceBase}`,
1382
- `${sourceBase}/index`,
1383
- `src/${sourceBase}/index`
1384
- ];
1385
- const candidates = [];
1386
- for (const base of bases) for (const extension of configuredSourceExtensions(config)) candidates.push(`${base}${extension}`);
1387
- return [...new Set(candidates)];
1388
- }
1389
- function wildcardBaseDirectory(pattern) {
1390
- const wildcardIndex = pattern.indexOf("*");
1391
- const prefix = wildcardIndex === -1 ? pattern : pattern.slice(0, wildcardIndex);
1392
- const lastSlashIndex = prefix.lastIndexOf("/");
1393
- return lastSlashIndex === -1 ? "." : prefix.slice(0, lastSlashIndex);
1394
- }
1395
- function sourceWildcardPatternCandidates(config, target) {
1396
- const strippedPattern = stripArtifactPrefix(config, target);
1397
- const sourcePattern = removeKnownExtension(strippedPattern);
1398
- const preferSrcPrefix = strippedPattern !== normalizeSlashes(target);
1399
- const candidates = [];
1400
- for (const extension of configuredSourceExtensions(config)) if (preferSrcPrefix) {
1401
- candidates.push(`src/${sourcePattern}${extension}`);
1402
- candidates.push(`${sourcePattern}${extension}`);
1403
- } else {
1404
- candidates.push(`${sourcePattern}${extension}`);
1405
- candidates.push(`src/${sourcePattern}${extension}`);
1406
- }
1407
- return [...new Set(candidates)];
1408
- }
1409
- function resolveWildcardTarget(config, packageDirectory, target) {
1410
- const sourcePatterns = sourceWildcardPatternCandidates(config, target);
1411
- const candidatePatterns = isLikelySourceTarget(config, target) ? [target, ...sourcePatterns] : sourcePatterns;
1412
- for (const candidatePattern of candidatePatterns) {
1413
- const baseDirectory = wildcardBaseDirectory(candidatePattern);
1414
- if (existsSync(path.join(packageDirectory, baseDirectory))) return toPosixPath(path.join(toRelativePath(config.rootDir, packageDirectory), candidatePattern));
1415
- }
1416
- return null;
1417
- }
1418
- function resolveExactTarget(config, packageDirectory, target) {
1419
- const absoluteTarget = path.join(packageDirectory, target);
1420
- if (existsSync(absoluteTarget) && isLikelySourceTarget(config, target)) return normalizeWorkspacePath(config.rootDir, absoluteTarget);
1421
- for (const candidate of sourceFileCandidates(config, target)) {
1422
- const absoluteCandidate = path.join(packageDirectory, candidate);
1423
- if (existsSync(absoluteCandidate)) return normalizeWorkspacePath(config.rootDir, absoluteCandidate);
1424
- }
1425
- return null;
1426
- }
1427
- function resolvePackageTarget(config, packageDirectory, rawTarget) {
1428
- const target = normalizePackageTarget(rawTarget);
1429
- if (!target) return null;
1430
- if (target.includes("*")) return resolveWildcardTarget(config, packageDirectory, target);
1431
- return resolveExactTarget(config, packageDirectory, target);
1432
- }
1433
- function collectExportEntries(config, workspacePackage) {
1434
- const exportsField = workspacePackage.manifest.exports;
1435
- if (!exportsField) return [];
1436
- const exportEntries = typeof exportsField === "object" && exportsField !== null && !Array.isArray(exportsField) && Object.keys(exportsField).some((key) => key.startsWith(".")) ? Object.entries(exportsField) : [[".", exportsField]];
1437
- const entries = [];
1438
- for (const [exportKey, exportValue] of exportEntries.sort(([left], [right]) => left.localeCompare(right))) {
1439
- const alias = packageExportKeyToAlias(workspacePackage.name, exportKey);
1440
- if (!alias) continue;
1441
- for (const candidate of collectTargetCandidates(config, exportValue)) {
1442
- const resolvedTarget = resolvePackageTarget(config, workspacePackage.directory, candidate);
1443
- if (resolvedTarget) {
1444
- entries.push([alias, resolvedTarget]);
1445
- break;
1446
- }
1447
- }
1448
- }
1449
- return entries;
1450
- }
1451
- function aliasMatchesSpecifier(alias, specifier) {
1452
- if (alias === specifier) return true;
1453
- const wildcardIndex = alias.indexOf("*");
1454
- if (wildcardIndex === -1) return false;
1455
- const prefix = alias.slice(0, wildcardIndex);
1456
- const suffix = alias.slice(wildcardIndex + 1);
1457
- return specifier.startsWith(prefix) && specifier.endsWith(suffix);
1458
- }
1459
-
1460
- //#endregion
1461
- //#region src/commands/paths.ts
1462
- function generatedFileName(config) {
1463
- return config.paths?.generatedFileName ?? "tsconfig.dts.paths.generated.json";
1464
- }
1465
- function generatedFileMarker(config) {
1466
- return config.paths?.generatedFileMarker ?? "GENERATED FILE - DO NOT EDIT BY HAND.";
1467
- }
1468
- function parseProject(config, configPath) {
1469
- const diagnostics = [];
1470
- const parsed = ts.getParsedCommandLineOfConfigFile(configPath, {}, {
1471
- ...ts.sys,
1472
- onUnRecoverableConfigFileDiagnostic: (diagnostic) => {
1473
- diagnostics.push(diagnostic);
1474
- }
1475
- });
1476
- if (!parsed) throw new Error(ts.formatDiagnosticsWithColorAndContext(diagnostics, {
1477
- getCanonicalFileName: (fileName) => fileName,
1478
- getCurrentDirectory: () => config.rootDir,
1479
- getNewLine: () => "\n"
1480
- }));
1481
- if (parsed.errors.length > 0) throw new Error(ts.formatDiagnosticsWithColorAndContext(parsed.errors, {
1482
- getCanonicalFileName: (fileName) => fileName,
1483
- getCurrentDirectory: () => config.rootDir,
1484
- getNewLine: () => "\n"
1485
- }));
1486
- return {
1487
- configPath: normalizeAbsolutePath(configPath),
1488
- fileNames: parsed.fileNames.filter((fileName) => /\.(?:[cm]?tsx?|d\.[cm]?ts)$/u.test(fileName)).map(normalizeAbsolutePath),
1489
- options: parsed.options,
1490
- references: new Set(getRawReferencePaths(config, configPath))
1491
- };
1117
+ if (!parsed) throw new Error(ts.formatDiagnosticsWithColorAndContext(diagnostics, {
1118
+ getCanonicalFileName: (fileName) => fileName,
1119
+ getCurrentDirectory: () => config.rootDir,
1120
+ getNewLine: () => "\n"
1121
+ }));
1122
+ if (parsed.errors.length > 0) throw new Error(ts.formatDiagnosticsWithColorAndContext(parsed.errors, {
1123
+ getCanonicalFileName: (fileName) => fileName,
1124
+ getCurrentDirectory: () => config.rootDir,
1125
+ getNewLine: () => "\n"
1126
+ }));
1127
+ return {
1128
+ configPath: normalizeAbsolutePath(configPath),
1129
+ fileNames: parsed.fileNames.filter((fileName) => /\.(?:[cm]?tsx?|d\.[cm]?ts)$/u.test(fileName)).map(normalizeAbsolutePath),
1130
+ options: parsed.options,
1131
+ references: new Set(getRawReferencePaths(config, configPath))
1132
+ };
1492
1133
  }
1493
1134
  function getSourceFileKind(filePath) {
1494
1135
  if (filePath.endsWith(".tsx")) return ts.ScriptKind.TSX;
@@ -1836,564 +1477,1103 @@ async function collectTsconfigPaths(config, pattern) {
1836
1477
  ]
1837
1478
  })).map(normalizeAbsolutePath).sort();
1838
1479
  }
1839
- async function collectDtsConfigPaths(config) {
1840
- return collectTsconfigPaths(config, dtsConfigPattern);
1480
+ async function collectDtsConfigPaths(config) {
1481
+ return collectTsconfigPaths(config, dtsConfigPattern);
1482
+ }
1483
+ async function collectBuildGraphConfigPaths(config) {
1484
+ return collectTsconfigPaths(config, buildGraphConfigPattern);
1485
+ }
1486
+ async function collectDefaultTsconfigPaths(config) {
1487
+ return collectTsconfigPaths(config, tsconfigJsonPattern);
1488
+ }
1489
+ async function collectOrdinaryTypecheckConfigPaths(config) {
1490
+ return (await collectTsconfigPaths(config, tsconfigFilePattern)).filter(isOrdinaryTypecheckConfigPath);
1491
+ }
1492
+ function isPlainRecord(value) {
1493
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
1494
+ }
1495
+ function formatUnknownValue(value) {
1496
+ if (value === void 0) return "undefined";
1497
+ return JSON.stringify(value);
1498
+ }
1499
+ function hasGlobSyntax(pattern) {
1500
+ return /[*?[\]{}()!+@]/u.test(pattern);
1501
+ }
1502
+ function isDirectoryShorthand(pattern) {
1503
+ return !hasGlobSyntax(pattern) && !pattern.includes("/") && !path.extname(pattern);
1504
+ }
1505
+ function normalizeSourceExcludePattern(pattern) {
1506
+ const normalized = pattern.replaceAll("\\", "/").replace(/\/+$/u, "");
1507
+ if (!normalized) return [];
1508
+ if (isDirectoryShorthand(normalized)) return [`${normalized}/**`, `**/${normalized}/**`];
1509
+ if (hasGlobSyntax(normalized)) return [normalized];
1510
+ if (normalized.includes("/")) return [normalized, `${normalized}/**`];
1511
+ return [normalized, `**/${normalized}`];
1512
+ }
1513
+ function sourceIncludePatterns(config) {
1514
+ if (config.config?.source?.include) return config.config.source.include;
1515
+ return getActiveCheckerExtensions(config).map((extension) => `**/*${extension}`);
1516
+ }
1517
+ function sourceExcludePatterns(config) {
1518
+ return (config.config?.source?.exclude ?? defaultSourceExclude).flatMap(normalizeSourceExcludePattern);
1519
+ }
1520
+ async function collectExpectedSourceFiles(config) {
1521
+ const proofFilePattern = config.config?.source?.include !== void 0 ? null : createExtensionPattern(getActiveCheckerExtensions(config));
1522
+ const files = await glob(sourceIncludePatterns(config), {
1523
+ cwd: config.rootDir,
1524
+ absolute: true,
1525
+ ignore: sourceExcludePatterns(config),
1526
+ onlyFiles: true
1527
+ });
1528
+ return new Set(files.map(normalizeAbsolutePath).filter((filePath) => proofFilePattern?.test(filePath) ?? true).sort());
1529
+ }
1530
+ function collectCheckerCoverageTargets(config) {
1531
+ const problems = [];
1532
+ const targets = [];
1533
+ for (const checker of getActiveCheckers(config)) {
1534
+ const configPath = resolveProjectConfigPath(config.rootDir, checker.entry);
1535
+ if (!existsSync(configPath)) {
1536
+ problems.push([
1537
+ "Checker proof entry references a missing tsconfig:",
1538
+ ` checker: ${checker.name}`,
1539
+ ` config: ${toRelativePath(config.rootDir, configPath)}`
1540
+ ].join("\n"));
1541
+ continue;
1542
+ }
1543
+ const routeCollection = collectGraphProjectRouteFromRoot({
1544
+ rootConfigPath: configPath,
1545
+ rootDir: config.rootDir
1546
+ });
1547
+ problems.push(...routeCollection.problems);
1548
+ targets.push({
1549
+ checker,
1550
+ configPath,
1551
+ coverageConfigPaths: routeCollection.projectPaths,
1552
+ label: `${checker.name}:entry`
1553
+ });
1554
+ }
1555
+ return {
1556
+ problems,
1557
+ targets
1558
+ };
1559
+ }
1560
+ function collectConfiguredAllowlistEntries(config) {
1561
+ const entries = [];
1562
+ const problems = [];
1563
+ const rawEntries = config.proof?.allowlist;
1564
+ if (rawEntries === void 0) return {
1565
+ entries,
1566
+ problems
1567
+ };
1568
+ if (!Array.isArray(rawEntries)) {
1569
+ problems.push([
1570
+ "Invalid proof allowlist config:",
1571
+ " field: proof.allowlist",
1572
+ ` value: ${formatUnknownValue(rawEntries)}`,
1573
+ " reason: proof.allowlist must be an array."
1574
+ ].join("\n"));
1575
+ return {
1576
+ entries,
1577
+ problems
1578
+ };
1579
+ }
1580
+ rawEntries.forEach((entry, index) => {
1581
+ const field = `proof.allowlist[${index}]`;
1582
+ if (!isPlainRecord(entry)) {
1583
+ problems.push([
1584
+ "Invalid proof allowlist config:",
1585
+ ` field: ${field}`,
1586
+ ` value: ${formatUnknownValue(entry)}`,
1587
+ " reason: allowlist entries must be objects with non-empty file and reason fields."
1588
+ ].join("\n"));
1589
+ return;
1590
+ }
1591
+ const fileValue = entry.file;
1592
+ const reasonValue = entry.reason;
1593
+ if (typeof fileValue !== "string" || fileValue.trim().length === 0) {
1594
+ problems.push([
1595
+ "Invalid proof allowlist config:",
1596
+ ` field: ${field}.file`,
1597
+ ` value: ${formatUnknownValue(fileValue)}`,
1598
+ " reason: allowlist file must be a non-empty string."
1599
+ ].join("\n"));
1600
+ return;
1601
+ }
1602
+ if (typeof reasonValue !== "string" || reasonValue.trim().length === 0) {
1603
+ problems.push([
1604
+ "Invalid proof allowlist config:",
1605
+ ` field: ${field}.reason`,
1606
+ ` value: ${formatUnknownValue(reasonValue)}`,
1607
+ " reason: allowlist reason must be a non-empty string."
1608
+ ].join("\n"));
1609
+ return;
1610
+ }
1611
+ entries.push({
1612
+ filePath: normalizeAbsolutePath(path.join(config.rootDir, fileValue)),
1613
+ reason: reasonValue.trim()
1614
+ });
1615
+ });
1616
+ return {
1617
+ entries,
1618
+ problems
1619
+ };
1620
+ }
1621
+ function addCoverage(coverageByFile, filePath, source) {
1622
+ const sources = coverageByFile.get(filePath) ?? [];
1623
+ sources.push(source);
1624
+ coverageByFile.set(filePath, sources);
1841
1625
  }
1842
- async function collectBuildGraphConfigPaths(config) {
1843
- return collectTsconfigPaths(config, buildGraphConfigPattern);
1626
+ function collectCoverage(options) {
1627
+ const coverageByFile = /* @__PURE__ */ new Map();
1628
+ for (const route of options.graphRoutes) for (const graphProjectPath of route.projectPaths) for (const filePath of parseProjectFileNamesForExtensions(options.config, graphProjectPath, getFirstClassCoverageExtensions(route.extensions))) {
1629
+ if (!options.sourceFiles.has(filePath)) continue;
1630
+ addCoverage(coverageByFile, filePath, {
1631
+ label: toRelativePath(options.config.rootDir, graphProjectPath),
1632
+ type: "graph"
1633
+ });
1634
+ }
1635
+ for (const checkerTarget of options.checkerTargets) for (const configPath of checkerTarget.coverageConfigPaths) for (const filePath of parseProjectFileNamesForExtensions(options.config, configPath, getCheckerCoverageExtensions(checkerTarget.checker))) {
1636
+ if (!options.sourceFiles.has(filePath)) continue;
1637
+ addCoverage(coverageByFile, filePath, {
1638
+ label: `${toRelativePath(options.config.rootDir, configPath)} via ${checkerTarget.label}`,
1639
+ type: "checker"
1640
+ });
1641
+ }
1642
+ if (options.includeAllowlist !== false) for (const entry of options.allowlistEntries) {
1643
+ if (!options.sourceFiles.has(entry.filePath)) continue;
1644
+ addCoverage(coverageByFile, entry.filePath, {
1645
+ label: entry.reason,
1646
+ type: "allowlist"
1647
+ });
1648
+ }
1649
+ return coverageByFile;
1844
1650
  }
1845
- async function collectDefaultTsconfigPaths(config) {
1846
- return collectTsconfigPaths(config, tsconfigJsonPattern);
1651
+ function collectProjectExtensionsByPath(routes) {
1652
+ const projectExtensionsByPath = /* @__PURE__ */ new Map();
1653
+ for (const route of routes) for (const projectPath of route.projectPaths) {
1654
+ const extensions = new Set([...projectExtensionsByPath.get(projectPath) ?? [], ...route.extensions]);
1655
+ projectExtensionsByPath.set(projectPath, [...extensions].sort());
1656
+ }
1657
+ return projectExtensionsByPath;
1847
1658
  }
1848
- async function collectOrdinaryTypecheckConfigPaths(config) {
1849
- return (await collectTsconfigPaths(config, tsconfigFilePattern)).filter(isOrdinaryTypecheckConfigPath);
1659
+ function parseConfig(config, configPath, extensions = []) {
1660
+ const diagnostics = [];
1661
+ const configObject = readJsonConfig(config, configPath);
1662
+ const parsed = ts.parseJsonConfigFileContent(configObject, ts.sys, path.dirname(configPath), {}, configPath, void 0, createExtraFileExtensions(extensions));
1663
+ if (diagnostics.length > 0) throw new Error(ts.formatDiagnosticsWithColorAndContext(diagnostics, createFormatHost(config.rootDir)));
1664
+ if (parsed.errors.length > 0) throw new Error(ts.formatDiagnosticsWithColorAndContext(parsed.errors, createFormatHost(config.rootDir)));
1665
+ return {
1666
+ fileNames: parsed.fileNames.map(normalizeAbsolutePath).sort(),
1667
+ options: parsed.options
1668
+ };
1850
1669
  }
1851
- function isPlainRecord(value) {
1852
- return Boolean(value) && typeof value === "object" && !Array.isArray(value);
1670
+ function formatJsonValue(value) {
1671
+ return JSON.stringify(value ?? null);
1853
1672
  }
1854
- function formatUnknownValue(value) {
1855
- if (value === void 0) return "undefined";
1856
- return JSON.stringify(value);
1673
+ function normalizeCompilerOptionValue(value) {
1674
+ if (Array.isArray(value)) return value;
1675
+ if (value && typeof value === "object") return Object.fromEntries(Object.entries(value).sort(([left], [right]) => left.localeCompare(right)));
1676
+ return value;
1857
1677
  }
1858
- function hasGlobSyntax(pattern) {
1859
- return /[*?[\]{}()!+@]/u.test(pattern);
1678
+ function addDtsConfigSemanticProblems(options) {
1679
+ const dtsFileNames = new Set(options.dtsConfig.fileNames);
1680
+ const localFileNames = new Set(options.localConfig.fileNames);
1681
+ const onlyInDts = options.dtsConfig.fileNames.filter((fileName) => !localFileNames.has(fileName));
1682
+ const onlyInLocal = options.localConfig.fileNames.filter((fileName) => !dtsFileNames.has(fileName));
1683
+ if (onlyInDts.length > 0 || onlyInLocal.length > 0) options.problems.push([
1684
+ "DTS config file set does not match its strict local tsconfig:",
1685
+ ` config: ${toRelativePath(options.config.rootDir, options.dtsConfigPath)}`,
1686
+ ` local: ${toRelativePath(options.config.rootDir, options.localConfigPath)}`,
1687
+ ...onlyInDts.length > 0 ? [
1688
+ " only in dts config:",
1689
+ ...onlyInDts.slice(0, 10).map((fileName) => ` - ${toRelativePath(options.config.rootDir, fileName)}`),
1690
+ onlyInDts.length > 10 ? ` ... ${onlyInDts.length - 10} more` : ""
1691
+ ] : [],
1692
+ ...onlyInLocal.length > 0 ? [
1693
+ " only in local config:",
1694
+ ...onlyInLocal.slice(0, 10).map((fileName) => ` - ${toRelativePath(options.config.rootDir, fileName)}`),
1695
+ onlyInLocal.length > 10 ? ` ... ${onlyInLocal.length - 10} more` : ""
1696
+ ] : []
1697
+ ].filter(Boolean).join("\n"));
1698
+ const optionNames = new Set([...Object.keys(options.localConfig.options), ...Object.keys(options.dtsConfig.options)]);
1699
+ for (const optionName of [...optionNames].sort()) {
1700
+ if (ignoredSemanticCompilerOptions.has(optionName)) continue;
1701
+ const localValue = normalizeCompilerOptionValue(options.localConfig.options[optionName]);
1702
+ const dtsValue = normalizeCompilerOptionValue(options.dtsConfig.options[optionName]);
1703
+ if (formatJsonValue(localValue) === formatJsonValue(dtsValue)) continue;
1704
+ options.problems.push([
1705
+ "DTS config overrides a typecheck compiler option from its strict local tsconfig:",
1706
+ ` config: ${toRelativePath(options.config.rootDir, options.dtsConfigPath)}`,
1707
+ ` local: ${toRelativePath(options.config.rootDir, options.localConfigPath)}`,
1708
+ ` option: compilerOptions.${optionName}`,
1709
+ ` local: ${formatJsonValue(localValue)}`,
1710
+ ` dts: ${formatJsonValue(dtsValue)}`
1711
+ ].join("\n"));
1712
+ }
1860
1713
  }
1861
- function isDirectoryShorthand(pattern) {
1862
- return !hasGlobSyntax(pattern) && !pattern.includes("/") && !path.extname(pattern);
1714
+ function addDtsConfigProblems(options) {
1715
+ for (const configPath of options.dtsConfigPaths) {
1716
+ if (!options.graphProjectPaths.has(configPath)) options.problems.push(["DTS config is not reachable from any checker entry:", ` config: ${toRelativePath(options.config.rootDir, configPath)}`].join("\n"));
1717
+ const localConfigPath = getDtsCompanionConfigPath(configPath);
1718
+ if (!existsSync(localConfigPath)) {
1719
+ options.problems.push([
1720
+ "DTS config is missing its strict local tsconfig:",
1721
+ ` config: ${toRelativePath(options.config.rootDir, configPath)}`,
1722
+ ` expected: ${toRelativePath(options.config.rootDir, localConfigPath)}`
1723
+ ].join("\n"));
1724
+ continue;
1725
+ }
1726
+ const extensions = options.projectExtensionsByPath.get(configPath) ?? [];
1727
+ const dtsConfig = parseConfig(options.config, configPath, extensions);
1728
+ const localConfig = parseConfig(options.config, localConfigPath, extensions);
1729
+ if (dtsConfig.options.composite !== true) options.problems.push([
1730
+ "DTS config is not valid for tsc -b:",
1731
+ ` config: ${toRelativePath(options.config.rootDir, configPath)}`,
1732
+ " reason: final compilerOptions.composite must be true."
1733
+ ].join("\n"));
1734
+ if (dtsConfig.options.noEmit === true) options.problems.push([
1735
+ "DTS config is not valid for tsc -b:",
1736
+ ` config: ${toRelativePath(options.config.rootDir, configPath)}`,
1737
+ " reason: final compilerOptions.noEmit must not be true."
1738
+ ].join("\n"));
1739
+ if (dtsConfig.options.declaration !== true) options.problems.push([
1740
+ "DTS config is not valid for declaration emit:",
1741
+ ` config: ${toRelativePath(options.config.rootDir, configPath)}`,
1742
+ " reason: final compilerOptions.declaration must be true."
1743
+ ].join("\n"));
1744
+ if (dtsConfig.options.emitDeclarationOnly !== true) options.problems.push([
1745
+ "DTS config is not valid for declaration emit:",
1746
+ ` config: ${toRelativePath(options.config.rootDir, configPath)}`,
1747
+ " reason: final compilerOptions.emitDeclarationOnly must be true."
1748
+ ].join("\n"));
1749
+ addDtsConfigSemanticProblems({
1750
+ config: options.config,
1751
+ dtsConfig,
1752
+ dtsConfigPath: configPath,
1753
+ localConfig,
1754
+ localConfigPath,
1755
+ problems: options.problems
1756
+ });
1757
+ }
1863
1758
  }
1864
- function normalizeSourceExcludePattern(pattern) {
1865
- const normalized = pattern.replaceAll("\\", "/").replace(/\/+$/u, "");
1866
- if (!normalized) return [];
1867
- if (isDirectoryShorthand(normalized)) return [`${normalized}/**`, `**/${normalized}/**`];
1868
- if (hasGlobSyntax(normalized)) return [normalized];
1869
- if (normalized.includes("/")) return [normalized, `${normalized}/**`];
1870
- return [normalized, `**/${normalized}`];
1759
+ function isEmptyArray(value) {
1760
+ return Array.isArray(value) && value.length === 0;
1871
1761
  }
1872
- function sourceIncludePatterns(config) {
1873
- if (config.config?.source?.include) return config.config.source.include;
1874
- return getActiveCheckerExtensions(config).map((extension) => `**/*${extension}`);
1762
+ function formatConfigRole(role) {
1763
+ return role === "build graph" ? "Build graph config" : "Default tsconfig.json";
1875
1764
  }
1876
- function sourceExcludePatterns(config) {
1877
- return (config.config?.source?.exclude ?? defaultSourceExclude).flatMap(normalizeSourceExcludePattern);
1765
+ function addPureAggregatorProblems(options) {
1766
+ const roleLabel = formatConfigRole(options.role);
1767
+ const allowedKeys = new Set([
1768
+ "$schema",
1769
+ "files",
1770
+ "references"
1771
+ ]);
1772
+ const extraKeys = Object.keys(options.configObject).filter((key) => !allowedKeys.has(key));
1773
+ if (!Object.hasOwn(options.configObject, "files")) options.problems.push([
1774
+ `${roleLabel} is not a pure aggregator:`,
1775
+ ` config: ${toRelativePath(options.config.rootDir, options.configPath)}`,
1776
+ " field: files",
1777
+ " reason: configs with project references must declare files: []."
1778
+ ].join("\n"));
1779
+ else if (!isEmptyArray(options.configObject.files)) options.problems.push([
1780
+ `${roleLabel} is not a pure aggregator:`,
1781
+ ` config: ${toRelativePath(options.config.rootDir, options.configPath)}`,
1782
+ " field: files",
1783
+ ` value: ${formatUnknownValue(options.configObject.files)}`,
1784
+ " reason: configs with project references must declare files: []."
1785
+ ].join("\n"));
1786
+ if (extraKeys.length > 0) options.problems.push([
1787
+ `${roleLabel} is not a pure aggregator:`,
1788
+ ` config: ${toRelativePath(options.config.rootDir, options.configPath)}`,
1789
+ ` fields: ${extraKeys.sort().join(", ")}`,
1790
+ " reason: pure aggregators may only declare $schema, files, and references; move source inputs and compiler options into leaf configs."
1791
+ ].join("\n"));
1878
1792
  }
1879
- async function collectExpectedSourceFiles(config) {
1880
- const proofFilePattern = config.config?.source?.include !== void 0 ? null : createExtensionPattern(getActiveCheckerExtensions(config));
1881
- const files = await glob(sourceIncludePatterns(config), {
1882
- cwd: config.rootDir,
1883
- absolute: true,
1884
- ignore: sourceExcludePatterns(config),
1885
- onlyFiles: true
1886
- });
1887
- return new Set(files.map(normalizeAbsolutePath).filter((filePath) => proofFilePattern?.test(filePath) ?? true).sort());
1793
+ function addBuildGraphConfigProblems(options) {
1794
+ for (const configPath of options.buildGraphConfigPaths) {
1795
+ const configObject = readJsonConfig(options.config, configPath);
1796
+ addPureAggregatorProblems({
1797
+ config: options.config,
1798
+ configObject,
1799
+ configPath,
1800
+ problems: options.problems,
1801
+ role: "build graph"
1802
+ });
1803
+ }
1888
1804
  }
1889
- function collectCheckerCoverageTargets(config) {
1890
- const problems = [];
1891
- const targets = [];
1892
- for (const checker of getActiveCheckers(config)) {
1893
- const configPath = resolveProjectConfigPath(config.rootDir, checker.entry);
1894
- if (!existsSync(configPath)) {
1895
- problems.push([
1896
- "Checker proof entry references a missing tsconfig:",
1897
- ` checker: ${checker.name}`,
1898
- ` config: ${toRelativePath(config.rootDir, configPath)}`
1805
+ function addDefaultTsconfigShapeProblems(options) {
1806
+ for (const configPath of options.tsconfigPaths) {
1807
+ const configObject = readJsonConfig(options.config, configPath);
1808
+ if (!Object.hasOwn(configObject, "references")) continue;
1809
+ addPureAggregatorProblems({
1810
+ config: options.config,
1811
+ configObject,
1812
+ configPath,
1813
+ problems: options.problems,
1814
+ role: "tsconfig.json"
1815
+ });
1816
+ if (!Array.isArray(configObject.references)) continue;
1817
+ configObject.references.forEach((reference, index) => {
1818
+ if (!isPlainRecord(reference) || typeof reference.path !== "string") return;
1819
+ const referencePath = resolveReferencePath(configPath, reference.path);
1820
+ if (isOrdinaryTypecheckConfigPath(referencePath)) return;
1821
+ options.problems.push([
1822
+ "Default tsconfig.json references a non-typecheck config:",
1823
+ ` config: ${toRelativePath(options.config.rootDir, configPath)}`,
1824
+ ` field: references[${index}].path`,
1825
+ ` reference: ${reference.path}`,
1826
+ ` resolved: ${toRelativePath(options.config.rootDir, referencePath)}`,
1827
+ " reason: tsconfig.json is the default IDE/typecheck entry and must not reference declaration build graph configs."
1899
1828
  ].join("\n"));
1900
- continue;
1901
- }
1902
- const routeCollection = collectGraphProjectRouteFromRoot({
1903
- rootConfigPath: configPath,
1904
- rootDir: config.rootDir
1905
- });
1906
- problems.push(...routeCollection.problems);
1907
- targets.push({
1908
- checker,
1909
- configPath,
1910
- coverageConfigPaths: routeCollection.projectPaths,
1911
- label: `${checker.name}:entry`
1912
1829
  });
1913
1830
  }
1914
- return {
1915
- problems,
1916
- targets
1917
- };
1918
1831
  }
1919
- function collectConfiguredAllowlistEntries(config) {
1920
- const entries = [];
1921
- const problems = [];
1922
- const rawEntries = config.proof?.allowlist;
1923
- if (rawEntries === void 0) return {
1924
- entries,
1925
- problems
1926
- };
1927
- if (!Array.isArray(rawEntries)) {
1928
- problems.push([
1929
- "Invalid proof allowlist config:",
1930
- " field: proof.allowlist",
1931
- ` value: ${formatUnknownValue(rawEntries)}`,
1932
- " reason: proof.allowlist must be an array."
1933
- ].join("\n"));
1934
- return {
1935
- entries,
1936
- problems
1937
- };
1832
+ function addDefaultTsconfigEnvironmentProblems(options) {
1833
+ const configsByDirectory = /* @__PURE__ */ new Map();
1834
+ for (const configPath of options.ordinaryConfigPaths) {
1835
+ const directory = path.dirname(configPath);
1836
+ const configs = configsByDirectory.get(directory) ?? [];
1837
+ configs.push(configPath);
1838
+ configsByDirectory.set(directory, configs);
1938
1839
  }
1939
- rawEntries.forEach((entry, index) => {
1940
- const field = `proof.allowlist[${index}]`;
1941
- if (!isPlainRecord(entry)) {
1942
- problems.push([
1943
- "Invalid proof allowlist config:",
1944
- ` field: ${field}`,
1945
- ` value: ${formatUnknownValue(entry)}`,
1946
- " reason: allowlist entries must be objects with non-empty file and reason fields."
1840
+ for (const [directory, configPaths] of configsByDirectory.entries()) {
1841
+ const scopedConfigPaths = configPaths.filter((configPath) => path.basename(configPath) !== "tsconfig.json");
1842
+ if (scopedConfigPaths.length === 0) continue;
1843
+ const defaultConfigPath = normalizeAbsolutePath(path.join(directory, "tsconfig.json"));
1844
+ if (!existsSync(defaultConfigPath)) {
1845
+ options.problems.push([
1846
+ "Directory with typecheck environments is missing default tsconfig.json:",
1847
+ ` directory: ${toRelativePath(options.config.rootDir, directory)}`,
1848
+ " reason: tsconfig.json is the default IDE/typecheck entry for its directory."
1947
1849
  ].join("\n"));
1948
- return;
1850
+ continue;
1949
1851
  }
1950
- const fileValue = entry.file;
1951
- const reasonValue = entry.reason;
1952
- if (typeof fileValue !== "string" || fileValue.trim().length === 0) {
1953
- problems.push([
1954
- "Invalid proof allowlist config:",
1955
- ` field: ${field}.file`,
1956
- ` value: ${formatUnknownValue(fileValue)}`,
1957
- " reason: allowlist file must be a non-empty string."
1852
+ if (scopedConfigPaths.length === 1) {
1853
+ options.problems.push([
1854
+ "Single typecheck environment should use default tsconfig.json:",
1855
+ ` config: ${toRelativePath(options.config.rootDir, scopedConfigPaths[0])}`,
1856
+ ` default: ${toRelativePath(options.config.rootDir, defaultConfigPath)}`,
1857
+ " reason: directories with only one type environment should make tsconfig.json the leaf entry."
1958
1858
  ].join("\n"));
1959
- return;
1859
+ continue;
1960
1860
  }
1961
- if (typeof reasonValue !== "string" || reasonValue.trim().length === 0) {
1962
- problems.push([
1963
- "Invalid proof allowlist config:",
1964
- ` field: ${field}.reason`,
1965
- ` value: ${formatUnknownValue(reasonValue)}`,
1966
- " reason: allowlist reason must be a non-empty string."
1861
+ const defaultConfigObject = readJsonConfig(options.config, defaultConfigPath);
1862
+ if (!Object.hasOwn(defaultConfigObject, "references")) options.problems.push([
1863
+ "Directory with multiple typecheck environments must use tsconfig.json as an aggregator:",
1864
+ ` config: ${toRelativePath(options.config.rootDir, defaultConfigPath)}`,
1865
+ " reason: multiple type environments require a default IDE/typecheck aggregator."
1866
+ ].join("\n"));
1867
+ }
1868
+ }
1869
+ function collectConfigFileOwners(config, graphRoutes, sourceFiles) {
1870
+ const ownersByFile = /* @__PURE__ */ new Map();
1871
+ for (const route of graphRoutes) for (const configPath of route.projectPaths) {
1872
+ if (!existsSync(configPath)) continue;
1873
+ for (const filePath of parseProjectFileNamesForExtensions(config, configPath, route.extensions)) {
1874
+ if (!sourceFiles.has(filePath)) continue;
1875
+ const owners = ownersByFile.get(filePath) ?? [];
1876
+ owners.push(configPath);
1877
+ ownersByFile.set(filePath, owners);
1878
+ }
1879
+ }
1880
+ return ownersByFile;
1881
+ }
1882
+ function addDuplicateGraphCoverageProblems(options) {
1883
+ for (const [filePath, owners] of [...options.ownersByFile.entries()].sort(([left], [right]) => toRelativePath(options.config.rootDir, left).localeCompare(toRelativePath(options.config.rootDir, right)))) {
1884
+ const uniqueOwners = [...new Set(owners)];
1885
+ if (uniqueOwners.length <= 1) continue;
1886
+ options.problems.push([
1887
+ "Duplicate checker graph coverage:",
1888
+ ` file: ${toRelativePath(options.config.rootDir, filePath)}`,
1889
+ " covered by:",
1890
+ ...uniqueOwners.sort((left, right) => toRelativePath(options.config.rootDir, left).localeCompare(toRelativePath(options.config.rootDir, right))).map((configPath) => ` - ${toRelativePath(options.config.rootDir, configPath)}`),
1891
+ " reason: a checker graph file must have a single declaration owner; move the file to one dts leaf or narrow include/exclude patterns."
1892
+ ].join("\n"));
1893
+ }
1894
+ }
1895
+ function addAllowlistProblems(options) {
1896
+ for (const entry of options.allowlistEntries) {
1897
+ if (!existsSync(entry.filePath)) {
1898
+ options.problems.push(["Typecheck proof allowlist references a missing file:", ` file: ${toRelativePath(options.config.rootDir, entry.filePath)}`].join("\n"));
1899
+ continue;
1900
+ }
1901
+ if (!options.sourceFiles.has(entry.filePath)) {
1902
+ options.problems.push([
1903
+ "Typecheck proof allowlist file is outside the configured source boundary:",
1904
+ ` file: ${toRelativePath(options.config.rootDir, entry.filePath)}`,
1905
+ " reason: allowlist entries should only describe source files that proof would otherwise require coverage for."
1967
1906
  ].join("\n"));
1968
- return;
1907
+ continue;
1969
1908
  }
1970
- entries.push({
1971
- filePath: normalizeAbsolutePath(path.join(config.rootDir, fileValue)),
1972
- reason: reasonValue.trim()
1973
- });
1909
+ if (options.baseCoverageByFile.has(entry.filePath)) options.problems.push(["Typecheck proof allowlist file is already covered without the allowlist:", ` file: ${toRelativePath(options.config.rootDir, entry.filePath)}`].join("\n"));
1910
+ }
1911
+ }
1912
+ function addUncoveredSourceProblems(options) {
1913
+ const uncoveredFiles = [...options.sourceFiles].filter((filePath) => !options.coverageByFile.has(filePath));
1914
+ if (uncoveredFiles.length === 0) return;
1915
+ options.problems.push([
1916
+ "Source files are not covered by typecheck proof:",
1917
+ ...uncoveredFiles.slice(0, 20).map((filePath) => ` - ${toRelativePath(options.config.rootDir, filePath)}`),
1918
+ uncoveredFiles.length > 20 ? ` ... ${uncoveredFiles.length - 20} more` : "",
1919
+ " reason: every file in config.source must be covered by a checker entry or an explicit allowlist entry."
1920
+ ].filter(Boolean).join("\n"));
1921
+ }
1922
+ async function runProofCheckInternal(config, options = {}) {
1923
+ const problems = [];
1924
+ const graphRouteCollection = collectGraphProjectRoutes(config);
1925
+ const entryRouteCollection = collectCheckerEntryProjectRoutes(config);
1926
+ const entryProjectPaths = [...new Set(entryRouteCollection.routes.flatMap((route) => route.projectPaths))].sort();
1927
+ const entryProjectPathSet = new Set(entryProjectPaths);
1928
+ const entryProjectExtensionsByPath = collectProjectExtensionsByPath(entryRouteCollection.routes);
1929
+ const dtsConfigPaths = await collectDtsConfigPaths(config);
1930
+ const buildGraphConfigPaths = await collectBuildGraphConfigPaths(config);
1931
+ const defaultTsconfigPaths = await collectDefaultTsconfigPaths(config);
1932
+ const ordinaryTypecheckConfigPaths = await collectOrdinaryTypecheckConfigPaths(config);
1933
+ problems.push(...graphRouteCollection.problems);
1934
+ problems.push(...entryRouteCollection.problems);
1935
+ addDtsConfigProblems({
1936
+ config,
1937
+ dtsConfigPaths,
1938
+ graphProjectPaths: entryProjectPathSet,
1939
+ problems,
1940
+ projectExtensionsByPath: entryProjectExtensionsByPath
1941
+ });
1942
+ addBuildGraphConfigProblems({
1943
+ buildGraphConfigPaths,
1944
+ config,
1945
+ problems
1946
+ });
1947
+ addDefaultTsconfigShapeProblems({
1948
+ config,
1949
+ problems,
1950
+ tsconfigPaths: defaultTsconfigPaths
1951
+ });
1952
+ addDefaultTsconfigEnvironmentProblems({
1953
+ config,
1954
+ ordinaryConfigPaths: ordinaryTypecheckConfigPaths,
1955
+ problems
1956
+ });
1957
+ if (problems.length > 0) {
1958
+ ProofLogger.error(problems.join("\n\n"));
1959
+ return false;
1960
+ }
1961
+ const checkerTargetCollection = collectCheckerCoverageTargets(config);
1962
+ const checkerTargets = checkerTargetCollection.targets;
1963
+ problems.push(...checkerTargetCollection.problems);
1964
+ if (problems.length > 0) {
1965
+ ProofLogger.error(problems.join("\n\n"));
1966
+ return false;
1967
+ }
1968
+ const sourceFiles = await collectExpectedSourceFiles(config);
1969
+ const allowlistCollection = collectConfiguredAllowlistEntries(config);
1970
+ const allowlistEntries = allowlistCollection.entries;
1971
+ problems.push(...allowlistCollection.problems);
1972
+ const baseCoverageByFile = collectCoverage({
1973
+ allowlistEntries,
1974
+ checkerTargets,
1975
+ config,
1976
+ graphRoutes: graphRouteCollection.routes,
1977
+ includeAllowlist: false,
1978
+ sourceFiles
1979
+ });
1980
+ const coverageByFile = collectCoverage({
1981
+ allowlistEntries,
1982
+ checkerTargets,
1983
+ config,
1984
+ graphRoutes: graphRouteCollection.routes,
1985
+ sourceFiles
1986
+ });
1987
+ addDuplicateGraphCoverageProblems({
1988
+ config,
1989
+ ownersByFile: collectConfigFileOwners(config, graphRouteCollection.routes, sourceFiles),
1990
+ problems
1991
+ });
1992
+ addAllowlistProblems({
1993
+ allowlistEntries,
1994
+ baseCoverageByFile,
1995
+ config,
1996
+ problems,
1997
+ sourceFiles
1998
+ });
1999
+ addUncoveredSourceProblems({
2000
+ config,
2001
+ coverageByFile,
2002
+ problems,
2003
+ sourceFiles
1974
2004
  });
2005
+ if (problems.length > 0) {
2006
+ ProofLogger.error(problems.join("\n\n"));
2007
+ return false;
2008
+ }
2009
+ const graphFileCount = [...coverageByFile.values()].filter((sources) => sources.some((source) => source.type === "graph")).length;
2010
+ const checkerFileCount = [...coverageByFile.values()].filter((sources) => sources.some((source) => source.type === "checker")).length;
2011
+ if (options.logSuccess ?? true) ProofLogger.success([
2012
+ `Checked ${entryProjectPaths.length} checker entry projects and ${dtsConfigPaths.length} dts configs.`,
2013
+ `Graph-capable checker entries cover ${graphFileCount} files; checker entries cover ${checkerFileCount} files.`,
2014
+ `Configured source boundary covers ${sourceFiles.size} files.`
2015
+ ].join("\n"));
2016
+ if ((options.logSuccess ?? true) && (config.proof?.allowlist ?? []).length > 0) ProofLogger.info(`Explicit typecheck proof allowlist: ${(config.proof?.allowlist ?? []).map((entry) => entry.file).join(", ")}`);
2017
+ return true;
2018
+ }
2019
+ async function runProofCheck(config, options = {}) {
2020
+ if (options.clearScreen ?? true) clearCliScreen();
2021
+ const elapsed = createElapsedTimer();
2022
+ const task = options.flow?.start("proof check", { depth: options.flowDepth ?? 0 });
2023
+ ProofLogger.info("proof check started");
2024
+ try {
2025
+ const logSuccess = !options.flow?.interactive;
2026
+ const passed = await runProofCheckInternal(config, { logSuccess });
2027
+ if (passed) {
2028
+ if (logSuccess) ProofLogger.success("proof check finished", elapsed());
2029
+ task?.pass();
2030
+ } else {
2031
+ ProofLogger.error("proof check finished with failures", elapsed());
2032
+ task?.fail("proof check finished with failures");
2033
+ }
2034
+ return passed;
2035
+ } catch (error) {
2036
+ ProofLogger.error(`proof check failed: ${formatErrorMessage$1(error)}`, elapsed());
2037
+ task?.fail("proof check failed", { error });
2038
+ throw error;
2039
+ }
2040
+ }
2041
+
2042
+ //#endregion
2043
+ //#region src/package-release-consistency.ts
2044
+ var PackageReleaseConsistencyError = class extends Error {
2045
+ name = "PackageReleaseConsistencyError";
2046
+ };
2047
+ const semver = createRequire(import.meta.url)("semver");
2048
+ const REQUIRED_RELEASE_FILES = ["README.md", "LICENSE.md"];
2049
+ const SOURCE_MAPPING_URL_PATTERN = /(?:\/\/\s*#\s*sourceMappingURL\s*=|\/\*\s*#\s*sourceMappingURL\s*=)/u;
2050
+ function isRecord(value) {
2051
+ return typeof value === "object" && value !== null && !Array.isArray(value);
2052
+ }
2053
+ function isLinkDependencySpecifier(specifier) {
2054
+ return specifier.startsWith("link:");
2055
+ }
2056
+ function createReleaseConsistencyState() {
1975
2057
  return {
1976
- entries,
1977
- problems
2058
+ directWorkspaceDependencies: [],
2059
+ edges: /* @__PURE__ */ new Map(),
2060
+ missingWorkspaceDependencies: [],
2061
+ packedManifestProblems: [],
2062
+ privateWorkspaceDependencies: [],
2063
+ releaseHygieneProblems: [],
2064
+ registryMetadataCache: /* @__PURE__ */ new Map(),
2065
+ registryProblems: [],
2066
+ sourceLinkDependencies: [],
2067
+ unpublishedPackageNames: /* @__PURE__ */ new Set(),
2068
+ visitedPackages: /* @__PURE__ */ new Set()
1978
2069
  };
1979
2070
  }
1980
- function addCoverage(coverageByFile, filePath, source) {
1981
- const sources = coverageByFile.get(filePath) ?? [];
1982
- sources.push(source);
1983
- coverageByFile.set(filePath, sources);
2071
+ function collectPublishDependencyEntries(manifest) {
2072
+ const entries = [];
2073
+ for (const { dependencies, name } of getPublishDependencySections(manifest)) for (const [dependencyName, specifier] of Object.entries(dependencies)) entries.push({
2074
+ dependencyName,
2075
+ sectionName: name,
2076
+ specifier
2077
+ });
2078
+ return entries;
1984
2079
  }
1985
- function collectCoverage(options) {
1986
- const coverageByFile = /* @__PURE__ */ new Map();
1987
- for (const route of options.graphRoutes) for (const graphProjectPath of route.projectPaths) for (const filePath of parseProjectFileNamesForExtensions(options.config, graphProjectPath, getFirstClassCoverageExtensions(route.extensions))) {
1988
- if (!options.sourceFiles.has(filePath)) continue;
1989
- addCoverage(coverageByFile, filePath, {
1990
- label: toRelativePath(options.config.rootDir, graphProjectPath),
1991
- type: "graph"
1992
- });
1993
- }
1994
- for (const checkerTarget of options.checkerTargets) for (const configPath of checkerTarget.coverageConfigPaths) for (const filePath of parseProjectFileNamesForExtensions(options.config, configPath, getCheckerCoverageExtensions(checkerTarget.checker))) {
1995
- if (!options.sourceFiles.has(filePath)) continue;
1996
- addCoverage(coverageByFile, filePath, {
1997
- label: `${toRelativePath(options.config.rootDir, configPath)} via ${checkerTarget.label}`,
1998
- type: "checker"
1999
- });
2000
- }
2001
- if (options.includeAllowlist !== false) for (const entry of options.allowlistEntries) {
2002
- if (!options.sourceFiles.has(entry.filePath)) continue;
2003
- addCoverage(coverageByFile, entry.filePath, {
2004
- label: entry.reason,
2005
- type: "allowlist"
2006
- });
2007
- }
2008
- return coverageByFile;
2080
+ function addEdge(edges, importerName, dependencyName) {
2081
+ const dependencies = edges.get(importerName) ?? /* @__PURE__ */ new Set();
2082
+ dependencies.add(dependencyName);
2083
+ edges.set(importerName, dependencies);
2009
2084
  }
2010
- function collectProjectExtensionsByPath(routes) {
2011
- const projectExtensionsByPath = /* @__PURE__ */ new Map();
2012
- for (const route of routes) for (const projectPath of route.projectPaths) {
2013
- const extensions = new Set([...projectExtensionsByPath.get(projectPath) ?? [], ...route.extensions]);
2014
- projectExtensionsByPath.set(projectPath, [...extensions].sort());
2085
+ function formatDependencyLocation(problem) {
2086
+ const dependency = problem.dependencyName ? ` -> ${problem.dependencyName}` : "";
2087
+ const section = problem.sectionName ? ` [${problem.sectionName}]` : "";
2088
+ const specifier = problem.specifier ? ` (${problem.specifier})` : "";
2089
+ return `${problem.importerName}${dependency}${section}${specifier}`;
2090
+ }
2091
+ function formatProblemLines(title, problems) {
2092
+ if (problems.length === 0) return [];
2093
+ return [
2094
+ "",
2095
+ title,
2096
+ ...problems.map((problem) => ` - ${formatDependencyLocation(problem)}: ${problem.message}`)
2097
+ ];
2098
+ }
2099
+ function getPackedDependencySpecifier(manifest, dependencyName) {
2100
+ for (const { dependencies } of getPublishDependencySections(manifest)) {
2101
+ const specifier = dependencies[dependencyName];
2102
+ if (specifier) return specifier;
2015
2103
  }
2016
- return projectExtensionsByPath;
2017
2104
  }
2018
- function parseConfig(config, configPath, extensions = []) {
2019
- const diagnostics = [];
2020
- const configObject = readJsonConfig(config, configPath);
2021
- const parsed = ts.parseJsonConfigFileContent(configObject, ts.sys, path.dirname(configPath), {}, configPath, void 0, createExtraFileExtensions(extensions));
2022
- if (diagnostics.length > 0) throw new Error(ts.formatDiagnosticsWithColorAndContext(diagnostics, createFormatHost(config.rootDir)));
2023
- if (parsed.errors.length > 0) throw new Error(ts.formatDiagnosticsWithColorAndContext(parsed.errors, createFormatHost(config.rootDir)));
2024
- return {
2025
- fileNames: parsed.fileNames.map(normalizeAbsolutePath).sort(),
2026
- options: parsed.options
2027
- };
2105
+ function getNpmPackageMetadataUrl(packageName) {
2106
+ return `https://registry.npmjs.org/${encodeURIComponent(packageName)}`;
2028
2107
  }
2029
- function formatJsonValue(value) {
2030
- return JSON.stringify(value ?? null);
2108
+ async function fetchRegistryPackageMetadata(packageName, state) {
2109
+ if (state.registryMetadataCache.has(packageName)) return state.registryMetadataCache.get(packageName) ?? null;
2110
+ const response = await fetch(getNpmPackageMetadataUrl(packageName), { headers: { accept: "application/json" } });
2111
+ if (!response.ok) {
2112
+ state.registryMetadataCache.set(packageName, null);
2113
+ return null;
2114
+ }
2115
+ const metadata = await response.json();
2116
+ const registryMetadata = isRecord(metadata) ? metadata : null;
2117
+ state.registryMetadataCache.set(packageName, registryMetadata);
2118
+ return registryMetadata;
2031
2119
  }
2032
- function normalizeCompilerOptionValue(value) {
2033
- if (Array.isArray(value)) return value;
2034
- if (value && typeof value === "object") return Object.fromEntries(Object.entries(value).sort(([left], [right]) => left.localeCompare(right)));
2035
- return value;
2120
+ function findRegistryVersionMetadata(metadata, version) {
2121
+ if (!isRecord(metadata.versions)) return null;
2122
+ const versionMetadata = metadata.versions[version];
2123
+ return isRecord(versionMetadata) ? versionMetadata : null;
2036
2124
  }
2037
- function addDtsConfigSemanticProblems(options) {
2038
- const dtsFileNames = new Set(options.dtsConfig.fileNames);
2039
- const localFileNames = new Set(options.localConfig.fileNames);
2040
- const onlyInDts = options.dtsConfig.fileNames.filter((fileName) => !localFileNames.has(fileName));
2041
- const onlyInLocal = options.localConfig.fileNames.filter((fileName) => !dtsFileNames.has(fileName));
2042
- if (onlyInDts.length > 0 || onlyInLocal.length > 0) options.problems.push([
2043
- "DTS config file set does not match its strict local tsconfig:",
2044
- ` config: ${toRelativePath(options.config.rootDir, options.dtsConfigPath)}`,
2045
- ` local: ${toRelativePath(options.config.rootDir, options.localConfigPath)}`,
2046
- ...onlyInDts.length > 0 ? [
2047
- " only in dts config:",
2048
- ...onlyInDts.slice(0, 10).map((fileName) => ` - ${toRelativePath(options.config.rootDir, fileName)}`),
2049
- onlyInDts.length > 10 ? ` ... ${onlyInDts.length - 10} more` : ""
2050
- ] : [],
2051
- ...onlyInLocal.length > 0 ? [
2052
- " only in local config:",
2053
- ...onlyInLocal.slice(0, 10).map((fileName) => ` - ${toRelativePath(options.config.rootDir, fileName)}`),
2054
- onlyInLocal.length > 10 ? ` ... ${onlyInLocal.length - 10} more` : ""
2055
- ] : []
2056
- ].filter(Boolean).join("\n"));
2057
- const optionNames = new Set([...Object.keys(options.localConfig.options), ...Object.keys(options.dtsConfig.options)]);
2058
- for (const optionName of [...optionNames].sort()) {
2059
- if (ignoredSemanticCompilerOptions.has(optionName)) continue;
2060
- const localValue = normalizeCompilerOptionValue(options.localConfig.options[optionName]);
2061
- const dtsValue = normalizeCompilerOptionValue(options.dtsConfig.options[optionName]);
2062
- if (formatJsonValue(localValue) === formatJsonValue(dtsValue)) continue;
2063
- options.problems.push([
2064
- "DTS config overrides a typecheck compiler option from its strict local tsconfig:",
2065
- ` config: ${toRelativePath(options.config.rootDir, options.dtsConfigPath)}`,
2066
- ` local: ${toRelativePath(options.config.rootDir, options.localConfigPath)}`,
2067
- ` option: compilerOptions.${optionName}`,
2068
- ` local: ${formatJsonValue(localValue)}`,
2069
- ` dts: ${formatJsonValue(dtsValue)}`
2070
- ].join("\n"));
2125
+ function execGitCommand(config, args) {
2126
+ return new Promise((resolve, reject) => {
2127
+ execFile("git", [
2128
+ "-C",
2129
+ config.rootDir,
2130
+ ...args
2131
+ ], {
2132
+ encoding: "utf8",
2133
+ maxBuffer: 10 * 1024 * 1024
2134
+ }, (error, stdout) => {
2135
+ if (error) {
2136
+ reject(error);
2137
+ return;
2138
+ }
2139
+ resolve(stdout);
2140
+ });
2141
+ });
2142
+ }
2143
+ async function hasWorkspacePackageChangesSinceGitHead(options) {
2144
+ const relativeDirectory = toRelativePath(options.config.rootDir, options.workspacePackage.directory);
2145
+ try {
2146
+ await execGitCommand(options.config, [
2147
+ "diff",
2148
+ "--quiet",
2149
+ options.gitHead,
2150
+ "--",
2151
+ relativeDirectory
2152
+ ]);
2153
+ } catch (error) {
2154
+ if (isRecord(error) && error.code === 1) return true;
2155
+ throw error;
2071
2156
  }
2157
+ return (await execGitCommand(options.config, [
2158
+ "ls-files",
2159
+ "--others",
2160
+ "--exclude-standard",
2161
+ "--",
2162
+ relativeDirectory
2163
+ ])).trim().length > 0;
2072
2164
  }
2073
- function addDtsConfigProblems(options) {
2074
- for (const configPath of options.dtsConfigPaths) {
2075
- if (!options.graphProjectPaths.has(configPath)) options.problems.push(["DTS config is not reachable from any checker entry:", ` config: ${toRelativePath(options.config.rootDir, configPath)}`].join("\n"));
2076
- const localConfigPath = getDtsCompanionConfigPath(configPath);
2077
- if (!existsSync(localConfigPath)) {
2078
- options.problems.push([
2079
- "DTS config is missing its strict local tsconfig:",
2080
- ` config: ${toRelativePath(options.config.rootDir, configPath)}`,
2081
- ` expected: ${toRelativePath(options.config.rootDir, localConfigPath)}`
2082
- ].join("\n"));
2083
- continue;
2084
- }
2085
- const extensions = options.projectExtensionsByPath.get(configPath) ?? [];
2086
- const dtsConfig = parseConfig(options.config, configPath, extensions);
2087
- const localConfig = parseConfig(options.config, localConfigPath, extensions);
2088
- if (dtsConfig.options.composite !== true) options.problems.push([
2089
- "DTS config is not valid for tsc -b:",
2090
- ` config: ${toRelativePath(options.config.rootDir, configPath)}`,
2091
- " reason: final compilerOptions.composite must be true."
2092
- ].join("\n"));
2093
- if (dtsConfig.options.noEmit === true) options.problems.push([
2094
- "DTS config is not valid for tsc -b:",
2095
- ` config: ${toRelativePath(options.config.rootDir, configPath)}`,
2096
- " reason: final compilerOptions.noEmit must not be true."
2097
- ].join("\n"));
2098
- if (dtsConfig.options.declaration !== true) options.problems.push([
2099
- "DTS config is not valid for declaration emit:",
2100
- ` config: ${toRelativePath(options.config.rootDir, configPath)}`,
2101
- " reason: final compilerOptions.declaration must be true."
2102
- ].join("\n"));
2103
- if (dtsConfig.options.emitDeclarationOnly !== true) options.problems.push([
2104
- "DTS config is not valid for declaration emit:",
2105
- ` config: ${toRelativePath(options.config.rootDir, configPath)}`,
2106
- " reason: final compilerOptions.emitDeclarationOnly must be true."
2107
- ].join("\n"));
2108
- addDtsConfigSemanticProblems({
2109
- config: options.config,
2110
- dtsConfig,
2111
- dtsConfigPath: configPath,
2112
- localConfig,
2113
- localConfigPath,
2114
- problems: options.problems
2165
+ async function verifyWorkspacePackagePublished(options) {
2166
+ const { state, workspacePackage } = options;
2167
+ const version = workspacePackage.manifest.version;
2168
+ if (!version || !semver.valid(version)) {
2169
+ state.unpublishedPackageNames.add(workspacePackage.name);
2170
+ state.registryProblems.push({
2171
+ importerName: workspacePackage.name,
2172
+ message: ["workspace package must declare a valid semver version", "before another publishable package can depend on it"].join(" "),
2173
+ packageName: workspacePackage.name
2174
+ });
2175
+ return;
2176
+ }
2177
+ let metadata;
2178
+ try {
2179
+ metadata = await fetchRegistryPackageMetadata(workspacePackage.name, state);
2180
+ } catch (error) {
2181
+ state.unpublishedPackageNames.add(workspacePackage.name);
2182
+ state.registryProblems.push({
2183
+ importerName: workspacePackage.name,
2184
+ message: [`unable to read npm registry metadata for ${workspacePackage.name}@${version}:`, formatErrorMessage$1(error)].join(" "),
2185
+ packageName: workspacePackage.name
2115
2186
  });
2187
+ return;
2116
2188
  }
2117
- }
2118
- function isEmptyArray(value) {
2119
- return Array.isArray(value) && value.length === 0;
2120
- }
2121
- function formatConfigRole(role) {
2122
- return role === "build graph" ? "Build graph config" : "Default tsconfig.json";
2123
- }
2124
- function addPureAggregatorProblems(options) {
2125
- const roleLabel = formatConfigRole(options.role);
2126
- const allowedKeys = new Set([
2127
- "$schema",
2128
- "files",
2129
- "references"
2130
- ]);
2131
- const extraKeys = Object.keys(options.configObject).filter((key) => !allowedKeys.has(key));
2132
- if (!Object.hasOwn(options.configObject, "files")) options.problems.push([
2133
- `${roleLabel} is not a pure aggregator:`,
2134
- ` config: ${toRelativePath(options.config.rootDir, options.configPath)}`,
2135
- " field: files",
2136
- " reason: configs with project references must declare files: []."
2137
- ].join("\n"));
2138
- else if (!isEmptyArray(options.configObject.files)) options.problems.push([
2139
- `${roleLabel} is not a pure aggregator:`,
2140
- ` config: ${toRelativePath(options.config.rootDir, options.configPath)}`,
2141
- " field: files",
2142
- ` value: ${formatUnknownValue(options.configObject.files)}`,
2143
- " reason: configs with project references must declare files: []."
2144
- ].join("\n"));
2145
- if (extraKeys.length > 0) options.problems.push([
2146
- `${roleLabel} is not a pure aggregator:`,
2147
- ` config: ${toRelativePath(options.config.rootDir, options.configPath)}`,
2148
- ` fields: ${extraKeys.sort().join(", ")}`,
2149
- " reason: pure aggregators may only declare $schema, files, and references; move source inputs and compiler options into leaf configs."
2150
- ].join("\n"));
2151
- }
2152
- function addBuildGraphConfigProblems(options) {
2153
- for (const configPath of options.buildGraphConfigPaths) {
2154
- const configObject = readJsonConfig(options.config, configPath);
2155
- addPureAggregatorProblems({
2156
- config: options.config,
2157
- configObject,
2158
- configPath,
2159
- problems: options.problems,
2160
- role: "build graph"
2189
+ if (!metadata) {
2190
+ state.unpublishedPackageNames.add(workspacePackage.name);
2191
+ state.registryProblems.push({
2192
+ importerName: workspacePackage.name,
2193
+ message: `${workspacePackage.name}@${version} is not published to the npm registry`,
2194
+ packageName: workspacePackage.name
2161
2195
  });
2196
+ return;
2162
2197
  }
2163
- }
2164
- function addDefaultTsconfigShapeProblems(options) {
2165
- for (const configPath of options.tsconfigPaths) {
2166
- const configObject = readJsonConfig(options.config, configPath);
2167
- if (!Object.hasOwn(configObject, "references")) continue;
2168
- addPureAggregatorProblems({
2198
+ const versionMetadata = findRegistryVersionMetadata(metadata, version);
2199
+ if (!versionMetadata) {
2200
+ state.unpublishedPackageNames.add(workspacePackage.name);
2201
+ state.registryProblems.push({
2202
+ importerName: workspacePackage.name,
2203
+ message: `${workspacePackage.name}@${version} is not published to the npm registry`,
2204
+ packageName: workspacePackage.name
2205
+ });
2206
+ return;
2207
+ }
2208
+ if (typeof versionMetadata.gitHead !== "string" || versionMetadata.gitHead.trim().length === 0) {
2209
+ state.unpublishedPackageNames.add(workspacePackage.name);
2210
+ state.registryProblems.push({
2211
+ importerName: workspacePackage.name,
2212
+ message: [`${workspacePackage.name}@${version} registry metadata has no gitHead,`, "so limina cannot prove the published source baseline"].join(" "),
2213
+ packageName: workspacePackage.name
2214
+ });
2215
+ return;
2216
+ }
2217
+ let hasChanges;
2218
+ try {
2219
+ hasChanges = await hasWorkspacePackageChangesSinceGitHead({
2169
2220
  config: options.config,
2170
- configObject,
2171
- configPath,
2172
- problems: options.problems,
2173
- role: "tsconfig.json"
2221
+ gitHead: versionMetadata.gitHead,
2222
+ workspacePackage
2174
2223
  });
2175
- if (!Array.isArray(configObject.references)) continue;
2176
- configObject.references.forEach((reference, index) => {
2177
- if (!isPlainRecord(reference) || typeof reference.path !== "string") return;
2178
- const referencePath = resolveReferencePath(configPath, reference.path);
2179
- if (isOrdinaryTypecheckConfigPath(referencePath)) return;
2180
- options.problems.push([
2181
- "Default tsconfig.json references a non-typecheck config:",
2182
- ` config: ${toRelativePath(options.config.rootDir, configPath)}`,
2183
- ` field: references[${index}].path`,
2184
- ` reference: ${reference.path}`,
2185
- ` resolved: ${toRelativePath(options.config.rootDir, referencePath)}`,
2186
- " reason: tsconfig.json is the default IDE/typecheck entry and must not reference declaration build graph configs."
2187
- ].join("\n"));
2224
+ } catch (error) {
2225
+ state.unpublishedPackageNames.add(workspacePackage.name);
2226
+ state.registryProblems.push({
2227
+ importerName: workspacePackage.name,
2228
+ message: [
2229
+ `unable to compare ${workspacePackage.name}@${version}`,
2230
+ `against published gitHead ${versionMetadata.gitHead}:`,
2231
+ formatErrorMessage$1(error)
2232
+ ].join(" "),
2233
+ packageName: workspacePackage.name
2188
2234
  });
2235
+ return;
2189
2236
  }
2190
- }
2191
- function addDefaultTsconfigEnvironmentProblems(options) {
2192
- const configsByDirectory = /* @__PURE__ */ new Map();
2193
- for (const configPath of options.ordinaryConfigPaths) {
2194
- const directory = path.dirname(configPath);
2195
- const configs = configsByDirectory.get(directory) ?? [];
2196
- configs.push(configPath);
2197
- configsByDirectory.set(directory, configs);
2237
+ if (hasChanges) {
2238
+ state.unpublishedPackageNames.add(workspacePackage.name);
2239
+ state.registryProblems.push({
2240
+ importerName: workspacePackage.name,
2241
+ message: [`${workspacePackage.name}@${version} has workspace changes`, `after the published npm registry gitHead ${versionMetadata.gitHead}`].join(" "),
2242
+ packageName: workspacePackage.name
2243
+ });
2198
2244
  }
2199
- for (const [directory, configPaths] of configsByDirectory.entries()) {
2200
- const scopedConfigPaths = configPaths.filter((configPath) => path.basename(configPath) !== "tsconfig.json");
2201
- if (scopedConfigPaths.length === 0) continue;
2202
- const defaultConfigPath = normalizeAbsolutePath(path.join(directory, "tsconfig.json"));
2203
- if (!existsSync(defaultConfigPath)) {
2204
- options.problems.push([
2205
- "Directory with typecheck environments is missing default tsconfig.json:",
2206
- ` directory: ${toRelativePath(options.config.rootDir, directory)}`,
2207
- " reason: tsconfig.json is the default IDE/typecheck entry for its directory."
2208
- ].join("\n"));
2245
+ }
2246
+ async function visitWorkspacePackageDependencies(options) {
2247
+ const { config, importerName, isRoot, manifest, state, workspacePackagesByName } = options;
2248
+ for (const entry of collectPublishDependencyEntries(manifest)) {
2249
+ if (isLinkDependencySpecifier(entry.specifier)) {
2250
+ state.sourceLinkDependencies.push({
2251
+ dependencyName: entry.dependencyName,
2252
+ importerName,
2253
+ message: "publishable dependency sections must not use link:",
2254
+ sectionName: entry.sectionName,
2255
+ specifier: entry.specifier
2256
+ });
2209
2257
  continue;
2210
2258
  }
2211
- if (scopedConfigPaths.length === 1) {
2212
- options.problems.push([
2213
- "Single typecheck environment should use default tsconfig.json:",
2214
- ` config: ${toRelativePath(options.config.rootDir, scopedConfigPaths[0])}`,
2215
- ` default: ${toRelativePath(options.config.rootDir, defaultConfigPath)}`,
2216
- " reason: directories with only one type environment should make tsconfig.json the leaf entry."
2217
- ].join("\n"));
2259
+ if (!isWorkspaceDependencySpecifier(entry.specifier)) continue;
2260
+ const targetPackage = workspacePackagesByName.get(entry.dependencyName);
2261
+ if (!targetPackage) {
2262
+ state.missingWorkspaceDependencies.push({
2263
+ dependencyName: entry.dependencyName,
2264
+ importerName,
2265
+ message: "workspace: publish dependency does not match a named workspace package",
2266
+ sectionName: entry.sectionName,
2267
+ specifier: entry.specifier
2268
+ });
2218
2269
  continue;
2219
2270
  }
2220
- const defaultConfigObject = readJsonConfig(options.config, defaultConfigPath);
2221
- if (!Object.hasOwn(defaultConfigObject, "references")) options.problems.push([
2222
- "Directory with multiple typecheck environments must use tsconfig.json as an aggregator:",
2223
- ` config: ${toRelativePath(options.config.rootDir, defaultConfigPath)}`,
2224
- " reason: multiple type environments require a default IDE/typecheck aggregator."
2225
- ].join("\n"));
2226
- }
2227
- }
2228
- function collectConfigFileOwners(config, graphRoutes, sourceFiles) {
2229
- const ownersByFile = /* @__PURE__ */ new Map();
2230
- for (const route of graphRoutes) for (const configPath of route.projectPaths) {
2231
- if (!existsSync(configPath)) continue;
2232
- for (const filePath of parseProjectFileNamesForExtensions(config, configPath, route.extensions)) {
2233
- if (!sourceFiles.has(filePath)) continue;
2234
- const owners = ownersByFile.get(filePath) ?? [];
2235
- owners.push(configPath);
2236
- ownersByFile.set(filePath, owners);
2271
+ addEdge(state.edges, importerName, targetPackage.name);
2272
+ if (isRoot) state.directWorkspaceDependencies.push({
2273
+ dependencyName: entry.dependencyName,
2274
+ sectionName: entry.sectionName,
2275
+ targetPackage
2276
+ });
2277
+ if (targetPackage.manifest.private === true) {
2278
+ state.privateWorkspaceDependencies.push({
2279
+ dependencyName: entry.dependencyName,
2280
+ importerName,
2281
+ message: "publishable packages cannot depend on a private workspace package",
2282
+ packageName: targetPackage.name,
2283
+ sectionName: entry.sectionName,
2284
+ specifier: entry.specifier
2285
+ });
2286
+ continue;
2287
+ }
2288
+ if (!state.visitedPackages.has(targetPackage.name)) {
2289
+ state.visitedPackages.add(targetPackage.name);
2290
+ await verifyWorkspacePackagePublished({
2291
+ config,
2292
+ state,
2293
+ workspacePackage: targetPackage
2294
+ });
2295
+ await visitWorkspacePackageDependencies({
2296
+ config,
2297
+ importerName: targetPackage.name,
2298
+ isRoot: false,
2299
+ manifest: targetPackage.manifest,
2300
+ state,
2301
+ workspacePackagesByName
2302
+ });
2237
2303
  }
2238
2304
  }
2239
- return ownersByFile;
2240
2305
  }
2241
- function addDuplicateGraphCoverageProblems(options) {
2242
- for (const [filePath, owners] of [...options.ownersByFile.entries()].sort(([left], [right]) => toRelativePath(options.config.rootDir, left).localeCompare(toRelativePath(options.config.rootDir, right)))) {
2243
- const uniqueOwners = [...new Set(owners)];
2244
- if (uniqueOwners.length <= 1) continue;
2245
- options.problems.push([
2246
- "Duplicate checker graph coverage:",
2247
- ` file: ${toRelativePath(options.config.rootDir, filePath)}`,
2248
- " covered by:",
2249
- ...uniqueOwners.sort((left, right) => toRelativePath(options.config.rootDir, left).localeCompare(toRelativePath(options.config.rootDir, right))).map((configPath) => ` - ${toRelativePath(options.config.rootDir, configPath)}`),
2250
- " reason: a checker graph file must have a single declaration owner; move the file to one dts leaf or narrow include/exclude patterns."
2251
- ].join("\n"));
2306
+ async function unpackPackedPackage(tarball) {
2307
+ return await unpack(tarball);
2308
+ }
2309
+ function getPackedContentFiles(packedPackage) {
2310
+ const rootPrefix = `${packedPackage.rootDir}/`;
2311
+ const files = [];
2312
+ for (const file of packedPackage.files) {
2313
+ if (!file.name.startsWith(rootPrefix)) continue;
2314
+ files.push({
2315
+ data: file.data,
2316
+ relativePath: file.name.slice(rootPrefix.length).replaceAll("\\", "/")
2317
+ });
2252
2318
  }
2319
+ return files;
2253
2320
  }
2254
- function addAllowlistProblems(options) {
2255
- for (const entry of options.allowlistEntries) {
2256
- if (!existsSync(entry.filePath)) {
2257
- options.problems.push(["Typecheck proof allowlist references a missing file:", ` file: ${toRelativePath(options.config.rootDir, entry.filePath)}`].join("\n"));
2258
- continue;
2259
- }
2260
- if (!options.sourceFiles.has(entry.filePath)) {
2261
- options.problems.push([
2262
- "Typecheck proof allowlist file is outside the configured source boundary:",
2263
- ` file: ${toRelativePath(options.config.rootDir, entry.filePath)}`,
2264
- " reason: allowlist entries should only describe source files that proof would otherwise require coverage for."
2265
- ].join("\n"));
2266
- continue;
2267
- }
2268
- if (options.baseCoverageByFile.has(entry.filePath)) options.problems.push(["Typecheck proof allowlist file is already covered without the allowlist:", ` file: ${toRelativePath(options.config.rootDir, entry.filePath)}`].join("\n"));
2321
+ function readPackedPackageJson(options) {
2322
+ const packageJsonFile = options.contentFiles.find((file) => file.relativePath === "package.json");
2323
+ if (!packageJsonFile) {
2324
+ options.state.releaseHygieneProblems.push({
2325
+ importerName: options.rootPackageName,
2326
+ message: "tarball does not contain package.json"
2327
+ });
2328
+ return null;
2329
+ }
2330
+ try {
2331
+ return JSON.parse(Buffer.from(packageJsonFile.data).toString("utf8"));
2332
+ } catch (error) {
2333
+ options.state.releaseHygieneProblems.push({
2334
+ importerName: options.rootPackageName,
2335
+ message: `tarball package.json is not valid JSON: ${formatErrorMessage$1(error)}`
2336
+ });
2337
+ return null;
2269
2338
  }
2270
2339
  }
2271
- function addUncoveredSourceProblems(options) {
2272
- const uncoveredFiles = [...options.sourceFiles].filter((filePath) => !options.coverageByFile.has(filePath));
2273
- if (uncoveredFiles.length === 0) return;
2274
- options.problems.push([
2275
- "Source files are not covered by typecheck proof:",
2276
- ...uncoveredFiles.slice(0, 20).map((filePath) => ` - ${toRelativePath(options.config.rootDir, filePath)}`),
2277
- uncoveredFiles.length > 20 ? ` ... ${uncoveredFiles.length - 20} more` : "",
2278
- " reason: every file in config.source must be covered by a checker entry or an explicit allowlist entry."
2279
- ].filter(Boolean).join("\n"));
2340
+ function isJavaScriptPackageFile(relativePath) {
2341
+ return /\.(?:cjs|mjs|js)$/u.test(relativePath);
2280
2342
  }
2281
- async function runProofCheckInternal(config, options = {}) {
2282
- const problems = [];
2283
- const graphRouteCollection = collectGraphProjectRoutes(config);
2284
- const entryRouteCollection = collectCheckerEntryProjectRoutes(config);
2285
- const entryProjectPaths = [...new Set(entryRouteCollection.routes.flatMap((route) => route.projectPaths))].sort();
2286
- const entryProjectPathSet = new Set(entryProjectPaths);
2287
- const entryProjectExtensionsByPath = collectProjectExtensionsByPath(entryRouteCollection.routes);
2288
- const dtsConfigPaths = await collectDtsConfigPaths(config);
2289
- const buildGraphConfigPaths = await collectBuildGraphConfigPaths(config);
2290
- const defaultTsconfigPaths = await collectDefaultTsconfigPaths(config);
2291
- const ordinaryTypecheckConfigPaths = await collectOrdinaryTypecheckConfigPaths(config);
2292
- problems.push(...graphRouteCollection.problems);
2293
- problems.push(...entryRouteCollection.problems);
2294
- addDtsConfigProblems({
2295
- config,
2296
- dtsConfigPaths,
2297
- graphProjectPaths: entryProjectPathSet,
2298
- problems,
2299
- projectExtensionsByPath: entryProjectExtensionsByPath
2300
- });
2301
- addBuildGraphConfigProblems({
2302
- buildGraphConfigPaths,
2303
- config,
2304
- problems
2305
- });
2306
- addDefaultTsconfigShapeProblems({
2307
- config,
2308
- problems,
2309
- tsconfigPaths: defaultTsconfigPaths
2343
+ function validateReleaseTarballHygiene(options) {
2344
+ const filePaths = new Set(options.contentFiles.map((file) => file.relativePath));
2345
+ const missingReleaseFiles = REQUIRED_RELEASE_FILES.filter((fileName) => !filePaths.has(fileName));
2346
+ if (missingReleaseFiles.length > 0) options.state.releaseHygieneProblems.push({
2347
+ importerName: options.rootPackageName,
2348
+ message: `tarball is missing required file(s): ${missingReleaseFiles.join(", ")}`
2310
2349
  });
2311
- addDefaultTsconfigEnvironmentProblems({
2312
- config,
2313
- ordinaryConfigPaths: ordinaryTypecheckConfigPaths,
2314
- problems
2350
+ for (const file of options.contentFiles) {
2351
+ if (/\.map$/u.test(file.relativePath)) {
2352
+ options.state.releaseHygieneProblems.push({
2353
+ importerName: options.rootPackageName,
2354
+ message: `tarball contains source map file: ${file.relativePath}`
2355
+ });
2356
+ continue;
2357
+ }
2358
+ if (!isJavaScriptPackageFile(file.relativePath)) continue;
2359
+ const source = Buffer.from(file.data).toString("utf8");
2360
+ if (SOURCE_MAPPING_URL_PATTERN.test(source)) options.state.releaseHygieneProblems.push({
2361
+ importerName: options.rootPackageName,
2362
+ message: `tarball JavaScript file contains sourceMappingURL directive: ${file.relativePath}`
2363
+ });
2364
+ }
2365
+ }
2366
+ function validatePackedManifest(options) {
2367
+ const { manifest, rootPackageName, state } = options;
2368
+ for (const entry of collectPublishDependencyEntries(manifest)) if (isWorkspaceDependencySpecifier(entry.specifier) || isLinkDependencySpecifier(entry.specifier)) state.packedManifestProblems.push({
2369
+ dependencyName: entry.dependencyName,
2370
+ importerName: rootPackageName,
2371
+ message: "packed package manifest must not expose workspace: or link: dependency specifiers",
2372
+ sectionName: entry.sectionName,
2373
+ specifier: entry.specifier
2315
2374
  });
2316
- if (problems.length > 0) {
2317
- ProofLogger.error(problems.join("\n\n"));
2318
- return false;
2375
+ for (const dependency of state.directWorkspaceDependencies) {
2376
+ const packedSpecifier = getPackedDependencySpecifier(manifest, dependency.dependencyName);
2377
+ if (!packedSpecifier) {
2378
+ state.packedManifestProblems.push({
2379
+ dependencyName: dependency.dependencyName,
2380
+ importerName: rootPackageName,
2381
+ message: "packed package manifest must keep every source workspace publish dependency",
2382
+ sectionName: dependency.sectionName
2383
+ });
2384
+ continue;
2385
+ }
2386
+ if (isWorkspaceDependencySpecifier(packedSpecifier) || isLinkDependencySpecifier(packedSpecifier)) continue;
2387
+ const targetVersion = dependency.targetPackage.manifest.version;
2388
+ if (!targetVersion || !semver.satisfies(targetVersion, packedSpecifier, { includePrerelease: true })) state.packedManifestProblems.push({
2389
+ dependencyName: dependency.dependencyName,
2390
+ importerName: rootPackageName,
2391
+ message: `packed dependency range must include ${dependency.targetPackage.name}@${targetVersion ?? "(missing version)"}`,
2392
+ sectionName: dependency.sectionName,
2393
+ specifier: packedSpecifier
2394
+ });
2319
2395
  }
2320
- const checkerTargetCollection = collectCheckerCoverageTargets(config);
2321
- const checkerTargets = checkerTargetCollection.targets;
2322
- problems.push(...checkerTargetCollection.problems);
2323
- if (problems.length > 0) {
2324
- ProofLogger.error(problems.join("\n\n"));
2325
- return false;
2396
+ }
2397
+ function createPublishOrder(rootPackageName, state) {
2398
+ const publishOrder = [];
2399
+ const seen = /* @__PURE__ */ new Set();
2400
+ function visit(packageName) {
2401
+ if (seen.has(packageName)) return;
2402
+ seen.add(packageName);
2403
+ for (const dependencyName of state.edges.get(packageName) ?? []) visit(dependencyName);
2404
+ if (packageName === rootPackageName || state.unpublishedPackageNames.has(packageName)) publishOrder.push(packageName);
2326
2405
  }
2327
- const sourceFiles = await collectExpectedSourceFiles(config);
2328
- const allowlistCollection = collectConfiguredAllowlistEntries(config);
2329
- const allowlistEntries = allowlistCollection.entries;
2330
- problems.push(...allowlistCollection.problems);
2331
- const baseCoverageByFile = collectCoverage({
2332
- allowlistEntries,
2333
- checkerTargets,
2334
- config,
2335
- graphRoutes: graphRouteCollection.routes,
2336
- includeAllowlist: false,
2337
- sourceFiles
2338
- });
2339
- const coverageByFile = collectCoverage({
2340
- allowlistEntries,
2341
- checkerTargets,
2342
- config,
2343
- graphRoutes: graphRouteCollection.routes,
2344
- sourceFiles
2406
+ visit(rootPackageName);
2407
+ return publishOrder;
2408
+ }
2409
+ function createReleaseConsistencyError(options) {
2410
+ const { config, label, outDir, rootPackageName, state } = options;
2411
+ if (state.sourceLinkDependencies.length + state.privateWorkspaceDependencies.length + state.missingWorkspaceDependencies.length + state.registryProblems.length + state.releaseHygieneProblems.length + state.packedManifestProblems.length === 0) return null;
2412
+ const publishOrder = createPublishOrder(rootPackageName, state);
2413
+ const lines = [
2414
+ `package release check failed for ${label}:`,
2415
+ ` output: ${toRelativePath(config.rootDir, outDir)}`,
2416
+ ...formatProblemLines("Release tarball is not publishable:", state.releaseHygieneProblems),
2417
+ ...formatProblemLines("Source manifest contains local link: publish dependencies:", state.sourceLinkDependencies),
2418
+ ...formatProblemLines("Source manifest depends on private workspace packages:", state.privateWorkspaceDependencies),
2419
+ ...formatProblemLines("Source manifest has invalid workspace: publish dependencies:", state.missingWorkspaceDependencies),
2420
+ ...formatProblemLines("Workspace packages must be published before this package:", state.registryProblems),
2421
+ ...formatProblemLines("Packed package manifest is inconsistent with workspace publish dependencies:", state.packedManifestProblems)
2422
+ ];
2423
+ if (publishOrder.length > 1) lines.push("", `Suggested publish order: ${publishOrder.join(" -> ")}`);
2424
+ return new PackageReleaseConsistencyError(lines.join("\n"));
2425
+ }
2426
+ async function assertPackageReleaseConsistency(options) {
2427
+ const workspacePackages = await collectWorkspacePackages(options.config);
2428
+ const sourcePackage = workspacePackages.find((workspacePackage) => workspacePackage.name === options.outputManifest.name);
2429
+ const state = createReleaseConsistencyState();
2430
+ if (sourcePackage) {
2431
+ state.visitedPackages.add(sourcePackage.name);
2432
+ await visitWorkspacePackageDependencies({
2433
+ config: options.config,
2434
+ importerName: sourcePackage.name,
2435
+ isRoot: true,
2436
+ manifest: sourcePackage.manifest,
2437
+ state,
2438
+ workspacePackagesByName: new Map(workspacePackages.map((workspacePackage) => [workspacePackage.name, workspacePackage]))
2439
+ });
2440
+ }
2441
+ const contentFiles = getPackedContentFiles(await unpackPackedPackage(options.packedTarball));
2442
+ validateReleaseTarballHygiene({
2443
+ contentFiles,
2444
+ rootPackageName: options.outputManifest.name,
2445
+ state
2345
2446
  });
2346
- addDuplicateGraphCoverageProblems({
2347
- config,
2348
- ownersByFile: collectConfigFileOwners(config, graphRouteCollection.routes, sourceFiles),
2349
- problems
2447
+ const packedManifest = readPackedPackageJson({
2448
+ contentFiles,
2449
+ rootPackageName: options.outputManifest.name,
2450
+ state
2350
2451
  });
2351
- addAllowlistProblems({
2352
- allowlistEntries,
2353
- baseCoverageByFile,
2354
- config,
2355
- problems,
2356
- sourceFiles
2452
+ if (packedManifest) validatePackedManifest({
2453
+ manifest: packedManifest,
2454
+ rootPackageName: options.outputManifest.name,
2455
+ state
2357
2456
  });
2358
- addUncoveredSourceProblems({
2359
- config,
2360
- coverageByFile,
2361
- problems,
2362
- sourceFiles
2457
+ const error = createReleaseConsistencyError({
2458
+ config: options.config,
2459
+ label: options.label,
2460
+ outDir: options.outDir,
2461
+ rootPackageName: options.outputManifest.name,
2462
+ state
2363
2463
  });
2364
- if (problems.length > 0) {
2365
- ProofLogger.error(problems.join("\n\n"));
2366
- return false;
2367
- }
2368
- const graphFileCount = [...coverageByFile.values()].filter((sources) => sources.some((source) => source.type === "graph")).length;
2369
- const checkerFileCount = [...coverageByFile.values()].filter((sources) => sources.some((source) => source.type === "checker")).length;
2370
- if (options.logSuccess ?? true) ProofLogger.success([
2371
- `Checked ${entryProjectPaths.length} checker entry projects and ${dtsConfigPaths.length} dts configs.`,
2372
- `Graph-capable checker entries cover ${graphFileCount} files; checker entries cover ${checkerFileCount} files.`,
2373
- `Configured source boundary covers ${sourceFiles.size} files.`
2464
+ if (error) throw error;
2465
+ }
2466
+
2467
+ //#endregion
2468
+ //#region src/commands/release.ts
2469
+ function logReleaseCheckPlan(options) {
2470
+ ReleaseLogger.info([
2471
+ "Release check plan:",
2472
+ ` config: ${toRelativePath(options.config.rootDir, options.config.configPath)}`,
2473
+ ` cwd: ${toRelativePath(options.config.rootDir, options.cwd)}`,
2474
+ ` selection: ${options.plan.selectionReason}`,
2475
+ " entries:",
2476
+ ...options.plan.entries.map((entry) => [` - ${entry.label}`, ` outDir: ${toRelativePath(options.config.rootDir, entry.outDir)}`].join("\n"))
2374
2477
  ].join("\n"));
2375
- if ((options.logSuccess ?? true) && (config.proof?.allowlist ?? []).length > 0) ProofLogger.info(`Explicit typecheck proof allowlist: ${(config.proof?.allowlist ?? []).map((entry) => entry.file).join(", ")}`);
2376
- return true;
2377
2478
  }
2378
- async function runProofCheck(config, options = {}) {
2479
+ async function packReleaseTarball(options) {
2480
+ const packTask = options.flow?.start(`release tarball: ${options.label}`, { depth: options.flowDepth ?? 0 });
2481
+ ReleaseLogger.info(`release tarball packing started: ${options.label}`);
2482
+ const packElapsed = createElapsedTimer();
2483
+ try {
2484
+ const packedDist = await packOutputTarball(options.outDir);
2485
+ if (!options.flow?.interactive) ReleaseLogger.success(`release tarball packed: ${options.label}`, packElapsed());
2486
+ packTask?.pass();
2487
+ return packedDist;
2488
+ } catch (error) {
2489
+ ReleaseLogger.error(`release tarball failed: ${options.label}: ${formatErrorMessage$1(error)}`, packElapsed());
2490
+ packTask?.fail(`release tarball failed: ${options.label}`, { error });
2491
+ throw error;
2492
+ }
2493
+ }
2494
+ async function runReleaseCheckEntry(options) {
2495
+ const task = options.flow?.start(`release entry: ${options.label}`, { depth: options.flowDepth ?? 0 });
2496
+ let packedDist;
2497
+ try {
2498
+ const outputPackageJsonPath = path.join(options.outDir, "package.json");
2499
+ const outputManifest = await readDistPackageJson({
2500
+ config: options.config,
2501
+ label: options.label,
2502
+ packageJsonPath: outputPackageJsonPath
2503
+ });
2504
+ if (outputManifest.private === true) throw new PackageReleaseConsistencyError([
2505
+ `package release check failed for ${options.label}:`,
2506
+ ` output: ${toRelativePath(options.config.rootDir, options.outDir)}`,
2507
+ "",
2508
+ "Release tarball is not publishable:",
2509
+ ` - ${outputManifest.name}: selected release package has "private": true; npm publish would reject it`
2510
+ ].join("\n"));
2511
+ packedDist = await packReleaseTarball({
2512
+ flow: options.flow,
2513
+ flowDepth: (options.flowDepth ?? 0) + 1,
2514
+ label: options.label,
2515
+ outDir: options.outDir
2516
+ });
2517
+ await assertPackageReleaseConsistency({
2518
+ config: options.config,
2519
+ label: options.label,
2520
+ outDir: options.outDir,
2521
+ outputManifest,
2522
+ packedTarball: packedDist.tarball
2523
+ });
2524
+ if (!options.flow?.interactive) ReleaseLogger.success(`release checks passed: ${options.label}`);
2525
+ task?.pass();
2526
+ return true;
2527
+ } catch (error) {
2528
+ if (error instanceof PackageReleaseConsistencyError) {
2529
+ ReleaseLogger.error(formatErrorMessage$1(error));
2530
+ task?.fail(`release checks failed: ${options.label}`);
2531
+ return false;
2532
+ }
2533
+ ReleaseLogger.error(`release checks failed: ${options.label}: ${formatErrorMessage$1(error)}`);
2534
+ task?.fail(`release checks failed: ${options.label}`, { error });
2535
+ throw error;
2536
+ } finally {
2537
+ if (packedDist) await packedDist.cleanup();
2538
+ }
2539
+ }
2540
+ async function runReleaseCheck(options) {
2379
2541
  if (options.clearScreen ?? true) clearCliScreen();
2380
2542
  const elapsed = createElapsedTimer();
2381
- const task = options.flow?.start("proof check", { depth: options.flowDepth ?? 0 });
2382
- ProofLogger.info("proof check started");
2543
+ const cwd = path.resolve(options.cwd ?? process.cwd());
2544
+ const task = options.flow?.start("release check", { depth: options.flowDepth ?? 0 });
2383
2545
  try {
2384
- const logSuccess = !options.flow?.interactive;
2385
- const passed = await runProofCheckInternal(config, { logSuccess });
2546
+ ReleaseLogger.info("release check started");
2547
+ const plan = createPackageEntrySelectionPlan({
2548
+ config: options.config,
2549
+ cwd,
2550
+ packageNames: options.packageNames,
2551
+ strictCwd: true
2552
+ });
2553
+ logReleaseCheckPlan({
2554
+ config: options.config,
2555
+ cwd,
2556
+ plan
2557
+ });
2558
+ let passed = true;
2559
+ for (const entry of plan.entries) passed = await runReleaseCheckEntry({
2560
+ config: options.config,
2561
+ flow: options.flow,
2562
+ flowDepth: (options.flowDepth ?? 0) + 1,
2563
+ label: entry.label,
2564
+ outDir: entry.outDir
2565
+ }) && passed;
2386
2566
  if (passed) {
2387
- if (logSuccess) ProofLogger.success("proof check finished", elapsed());
2567
+ if (!options.flow?.interactive) ReleaseLogger.success("release check finished", elapsed());
2388
2568
  task?.pass();
2389
2569
  } else {
2390
- ProofLogger.error("proof check finished with failures", elapsed());
2391
- task?.fail("proof check finished with failures");
2570
+ ReleaseLogger.error("release check finished with failures", elapsed());
2571
+ task?.fail("release check finished with failures");
2392
2572
  }
2393
2573
  return passed;
2394
2574
  } catch (error) {
2395
- ProofLogger.error(`proof check failed: ${formatErrorMessage$1(error)}`, elapsed());
2396
- task?.fail("proof check failed", { error });
2575
+ ReleaseLogger.error(`release check failed: ${formatErrorMessage$1(error)}`, elapsed());
2576
+ task?.fail("release check failed", { error });
2397
2577
  throw error;
2398
2578
  }
2399
2579
  }
@@ -2406,6 +2586,7 @@ const builtInTaskNames = new Set([
2406
2586
  "graph:check",
2407
2587
  "package:check",
2408
2588
  "proof:check",
2589
+ "release:check",
2409
2590
  "source:check"
2410
2591
  ]);
2411
2592
  const defaultCheckPipeline = [
@@ -2461,7 +2642,16 @@ async function runBuiltinTask(config, taskName, options = {}) {
2461
2642
  config,
2462
2643
  cwd: options.cwd,
2463
2644
  flow: options.flow,
2464
- flowDepth: 1
2645
+ flowDepth: 1,
2646
+ packageNames: options.packageNames
2647
+ });
2648
+ case "release:check": return runReleaseCheck({
2649
+ clearScreen: false,
2650
+ config,
2651
+ cwd: options.cwd,
2652
+ flow: options.flow,
2653
+ flowDepth: 1,
2654
+ packageNames: options.packageNames
2465
2655
  });
2466
2656
  case "checker:typecheck": return (await runCheckerTypecheck({
2467
2657
  clearScreen: false,
@@ -2591,6 +2781,11 @@ function parsePackageAttwProfile(profile) {
2591
2781
  if (profile === "strict" || profile === "node16" || profile === "esm-only") return profile;
2592
2782
  throw new Error(`Invalid package check --attw-profile "${profile}". Expected one of: strict, node16, esm-only.`);
2593
2783
  }
2784
+ function parsePackageNames(packageName) {
2785
+ if (!packageName) return;
2786
+ const packageNames = (Array.isArray(packageName) ? packageName : [packageName]).map((name) => name.trim()).filter(Boolean);
2787
+ return packageNames.length > 0 ? packageNames : void 0;
2788
+ }
2594
2789
  function createCliFlow() {
2595
2790
  clearCliScreen();
2596
2791
  return createLiminaFlowReporter();
@@ -2611,13 +2806,14 @@ async function main() {
2611
2806
  });
2612
2807
  flow.outro("limina init finished");
2613
2808
  });
2614
- cli.command("check [pipeline]", "Run the default check or a configured pipeline").action(async (pipeline, flags) => {
2809
+ cli.command("check [pipeline]", "Run the default check or a configured pipeline").option("-p, --package <name>", "Run package-aware pipeline tasks for one package entry").action(async (pipeline, flags) => {
2615
2810
  const flow = createCliFlow();
2616
2811
  flow.intro("limina check");
2617
2812
  const config = await load(flags, "check");
2618
2813
  const passed = pipeline ? await runPipeline(config, pipeline, {
2619
2814
  cwd: process.cwd(),
2620
- flow
2815
+ flow,
2816
+ packageNames: parsePackageNames(flags.package)
2621
2817
  }) : await runDefaultCheck(config, {
2622
2818
  cwd: process.cwd(),
2623
2819
  flow
@@ -2694,7 +2890,7 @@ async function main() {
2694
2890
  if (!result.passed) process.exitCode = 1;
2695
2891
  flow.outro(result.passed ? "limina checker passed" : "limina checker failed");
2696
2892
  });
2697
- cli.command("package <action>", "Check configured published package outputs").option("-p, --package <name>", "Run a single package check target").option("--tool <tool>", "Run one package check tool").option("--attw-profile <profile>", "Override the configured ATTW profile").action(async (action, flags) => {
2893
+ cli.command("package <action>", "Check configured published package outputs").option("-p, --package <name>", "Run one package check entry").option("--tool <tool>", "Run one package check tool").option("--attw-profile <profile>", "Override the configured ATTW profile").action(async (action, flags) => {
2698
2894
  if (action !== "check") throw new Error(`Unknown package action "${action}". Expected check.`);
2699
2895
  const flow = createCliFlow();
2700
2896
  flow.intro("limina package check");
@@ -2705,12 +2901,26 @@ async function main() {
2705
2901
  config,
2706
2902
  cwd: process.cwd(),
2707
2903
  flow,
2708
- targetName: flags.package,
2904
+ packageNames: parsePackageNames(flags.package),
2709
2905
  tool: parsePackageTool(flags.tool)
2710
2906
  });
2711
2907
  if (!passed) process.exitCode = 1;
2712
2908
  flow.outro(passed ? "limina package passed" : "limina package failed");
2713
2909
  });
2910
+ cli.command("release <action>", "Check package release readiness").option("-p, --package <name>", "Run one release check package entry").action(async (action, flags) => {
2911
+ if (action !== "check") throw new Error(`Unknown release action "${action}". Expected check.`);
2912
+ const flow = createCliFlow();
2913
+ flow.intro("limina release check");
2914
+ const passed = await runReleaseCheck({
2915
+ clearScreen: false,
2916
+ config: await load(flags, "release"),
2917
+ cwd: process.cwd(),
2918
+ flow,
2919
+ packageNames: parsePackageNames(flags.package)
2920
+ });
2921
+ if (!passed) process.exitCode = 1;
2922
+ flow.outro(passed ? "limina release passed" : "limina release failed");
2923
+ });
2714
2924
  cli.parse(process.argv, { run: false });
2715
2925
  try {
2716
2926
  await cli.runMatchedCommand();