heximon 0.10.0 → 0.11.0
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/README.md +7 -2
- package/dist/bin.mjs +503 -42
- package/dist/index.d.mts +116 -16
- package/package.json +16 -16
package/README.md
CHANGED
|
@@ -8,12 +8,14 @@ after your last `pnpm build`.
|
|
|
8
8
|
|
|
9
9
|
## Why you want it
|
|
10
10
|
|
|
11
|
-
- **One binary for the whole surface.** `create`, `check`, `routes`, `graph`, `doctor`, `
|
|
12
|
-
`provision`, `migrations`, `task`, and `workflows` are one command each.
|
|
11
|
+
- **One binary for the whole surface.** `create`, `check`, `routes`, `graph`, `doctor`, `freshness`,
|
|
12
|
+
`deploy`, `provision`, `migrations`, `task`, and `workflows` are one command each.
|
|
13
13
|
- **`heximon check` catches a dependency-injection error before `tsgo` does.** It compiles your app
|
|
14
14
|
headlessly and reports every compile error with its position and a fix.
|
|
15
15
|
- **`heximon doctor` finds a package installed twice.** Class identity is the only dependency-injection
|
|
16
16
|
token, so two copies of one package bind two different sets of classes. `doctor` names both directories.
|
|
17
|
+
- **`heximon freshness` finds the packages that need a new `vp pack`.** It runs from any folder of a
|
|
18
|
+
workspace, lists every package that has a pack record, and names the file that made a package stale.
|
|
17
19
|
- **`heximon deploy` ships with the platform's own tool.** It builds your app, then runs `wrangler`,
|
|
18
20
|
`vercel`, `netlify`, or `deployctl` for you, with the right flags for your environment.
|
|
19
21
|
- **Agents and CI read one JSON document.** Every command takes `--json`: stdout gets one document with
|
|
@@ -58,6 +60,7 @@ const main = defineCommand({
|
|
|
58
60
|
routes: routesCommand,
|
|
59
61
|
graph: graphCommand,
|
|
60
62
|
doctor: doctorCommand,
|
|
63
|
+
freshness: freshnessCommand,
|
|
61
64
|
provision: provisionCommand,
|
|
62
65
|
migrations: migrationsCommand,
|
|
63
66
|
workflows: workflowsCommand,
|
|
@@ -76,6 +79,7 @@ const main = defineCommand({
|
|
|
76
79
|
| `heximon routes` | Prints one table for every path the app answers — routes, SSE streams, and WebSocket upgrades — read from the compiler, with no boot required. |
|
|
77
80
|
| `heximon graph` | Prints the resolved module dependency graph: modules, providers, and dependencies. |
|
|
78
81
|
| `heximon doctor` | Reports a package installed twice, an untraced deploy target, and a stale published module. |
|
|
82
|
+
| `heximon freshness` | Lists every workspace package that has a pack record (`dist/heximon.manifest.json` or `dist/heximon.inputs.json`), with its status and the input that made it stale. It finds the workspace root from any folder (`pnpm-workspace.yaml`, or a `workspaces` field in `package.json`), compiles nothing, and exits `1` when a package is stale or its check failed. A record file that exists but cannot be read is a failed check, not a missing record. `--stale` lists only those packages. |
|
|
79
83
|
| `heximon deploy` | Builds the app, then ships it with the target platform's own command-line tool. |
|
|
80
84
|
| `heximon provision` | Creates a resource your build needs an identifier for, before the build runs. |
|
|
81
85
|
| `heximon migrations` | Runs `generate`, `migrate`, `status`, or `push` against your database, with no hand-written `drizzle-kit` config. |
|
|
@@ -114,6 +118,7 @@ The document types are exported as types from the package root:
|
|
|
114
118
|
| `routes` | `RoutesJsonDocument` | `RoutesDiagnosticCode` |
|
|
115
119
|
| `graph` | `GraphJsonDocument` | `GraphDiagnosticCode` |
|
|
116
120
|
| `doctor` | `DoctorJsonDocument` | `DoctorDiagnosticCode` |
|
|
121
|
+
| `freshness` | `FreshnessJsonDocument` | `FreshnessDiagnosticCode` |
|
|
117
122
|
| `skills add`, `skills sync` | `SkillsAddJsonDocument`, `SkillsSyncJsonDocument` | `SkillsDiagnosticCode` |
|
|
118
123
|
| `task run` | `TaskRunJsonDocument` | `TaskDiagnosticCode` |
|
|
119
124
|
| `migrations generate`, `migrate`, `push`, `status` | `MigrationsGenerateJsonDocument`, `MigrationsMigrateJsonDocument`, `MigrationsPushJsonDocument`, `MigrationsStatusJsonDocument` | `MigrationsDiagnosticCode` |
|
package/dist/bin.mjs
CHANGED
|
@@ -4,15 +4,16 @@ import { createRequire, register } from "node:module";
|
|
|
4
4
|
import { cancel, confirm, intro, isCancel, log, outro, select, spinner } from "@clack/prompts";
|
|
5
5
|
import process from "node:process";
|
|
6
6
|
import { defineCommand } from "citty";
|
|
7
|
-
import { existsSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
7
|
+
import { existsSync, globSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
8
8
|
import { AstNodeType, CompileEnvironment, CompileMode, DeployPlatform, DiagnosticCode, DiagnosticRenderMode, DiagnosticRenderer, DiagnosticSeverity, ImportKind, ReexportIndex, RuntimeConfigFile, RuntimeSingletons, TerminalStyle, UpgradeBaselineFile } from "@heximon/compiler";
|
|
9
9
|
import { spawn } from "node:child_process";
|
|
10
10
|
import { basename, dirname, isAbsolute, join, relative, resolve } from "pathe";
|
|
11
11
|
import tab from "@bomb.sh/tab/citty";
|
|
12
|
-
import { ConfigLoader, LibraryEmitter, LibraryFactoryPreflight, LibraryFreshness, ProjectCompiler, RuntimeConfigLoader, SourceModuleResolution, SourceParser, WorkspaceTopologyReader } from "@heximon/compiler/project";
|
|
12
|
+
import { ConfigLoader, LibraryEmitter, LibraryFactoryPreflight, LibraryFreshness, LibraryFreshnessStatus, PnpmWorkspaceManifest, ProjectCompiler, RuntimeConfigLoader, SourceModuleResolution, SourceParser, WorkspaceTopologyReader } from "@heximon/compiler/project";
|
|
13
13
|
import { NodeCompileSeam } from "@heximon/node/compiler";
|
|
14
14
|
import { TransactionCensus, TransactionCensusPolicy } from "@heximon/cqrs/compiler";
|
|
15
15
|
import { BoundaryProvenanceAuditor, BoundaryProvenanceStatus, CredentialSelection, DatabaseConfigDetector, DeployDiagnosticSeverity, DeployLedger, DeployManifestStore, DeployRunner, DeployStrategyUnavailableError, DeployTargetResolver, DeployableServiceReader, DotenvCascade, DrizzleKitRunner, EnvFileReader, EnvFileUnreadableError, HostManagedDeployUnsupportedError, ImportScanner, MigrationInspector, MigrationTargetComposer, PackageCopyAuditor, PeerClosureAuditor, PlatformTracingAuditor, PlatformTracingStatus, SecretMask, WorkspaceConsumerLocator } from "@heximon/deploy";
|
|
16
|
+
import { matchesGlob } from "node:path";
|
|
16
17
|
import { RouteCensus, RouteScopeGap } from "@heximon/http/compiler";
|
|
17
18
|
import { pathToFileURL } from "node:url";
|
|
18
19
|
import { homedir } from "node:os";
|
|
@@ -1677,7 +1678,7 @@ var DoctorAudit = class DoctorAudit {
|
|
|
1677
1678
|
const hasConsumers = (result.workspaceConsumers?.consumers.length ?? 0) > 0;
|
|
1678
1679
|
parts.push(hasConsumers ? "Forked against a workspace consumer — equalize the two packages' installed peer contexts (a one-sided cover can flip the fork, not remove it) and reinstall." : "Forked within this package's own resolution (no workspace consumers found to compare against) — align the versions its own dependency tree resolves and reinstall.");
|
|
1679
1680
|
}
|
|
1680
|
-
if (result.selfReport === null) parts.push("This directory is not a packed Heximon library (no dist/heximon.manifest.json) — the peer-closure and copy census above still ran; run `vp pack` to enable the staleness check too.");
|
|
1681
|
+
if (result.selfReport === null) parts.push("This directory is not a packed Heximon library (no dist/heximon.manifest.json and no dist/heximon.inputs.json) — the peer-closure and copy census above still ran; run `vp pack` to enable the staleness check too.");
|
|
1681
1682
|
else if (result.selfReport.status === BoundaryProvenanceStatus.Stale || !forked) parts.push(DoctorAudit.selfProvenanceSummary(result.selfReport));
|
|
1682
1683
|
return parts.join(" ");
|
|
1683
1684
|
}
|
|
@@ -1707,7 +1708,7 @@ var DoctorAudit = class DoctorAudit {
|
|
|
1707
1708
|
*/
|
|
1708
1709
|
static selfProvenanceSummary(report) {
|
|
1709
1710
|
switch (report.status) {
|
|
1710
|
-
case BoundaryProvenanceStatus.Fresh: return "This package's dist is current with its src/.";
|
|
1711
|
+
case BoundaryProvenanceStatus.Fresh: return report.manifestSourceHash === void 0 ? "This package's dist is current with its build inputs." : "This package's dist is current with its src/.";
|
|
1711
1712
|
case BoundaryProvenanceStatus.NoProvenance: return "Its packed manifest predates the provenance stamp, so nothing was compared — repack (`vp pack`) to enable the staleness check.";
|
|
1712
1713
|
case BoundaryProvenanceStatus.NoSource: return "It ships no local src/, so its packed manifest was not compared with anything.";
|
|
1713
1714
|
case BoundaryProvenanceStatus.Stale: return "Your dist is STALE — repack (`vp pack`) before publishing or consuming this package.";
|
|
@@ -1890,61 +1891,113 @@ var DoctorJsonReport = class DoctorJsonReport {
|
|
|
1890
1891
|
/** One staleness entry. `self` marks the package that `--self` audits. */
|
|
1891
1892
|
static staleness(report, self) {
|
|
1892
1893
|
const freshness = report;
|
|
1893
|
-
const repack =
|
|
1894
|
-
const
|
|
1894
|
+
const repack = DoctorJsonReport.repackCommand(report.packageDirectory);
|
|
1895
|
+
const verdict = DoctorJsonReport.verdictOf(report.status, self ? "Repack (`vp pack`) before publishing or consuming it." : `Rebuild it (\`vp pack\` in ${report.packageDirectory}) before trusting this app's build.`, repack);
|
|
1896
|
+
return {
|
|
1897
|
+
name: report.specifier,
|
|
1898
|
+
status: verdict.status,
|
|
1899
|
+
reason: DoctorJsonReport.reasonOf(freshness),
|
|
1900
|
+
...verdict.hint === void 0 ? {} : { hint: verdict.hint },
|
|
1901
|
+
...verdict.fix === void 0 ? {} : { fix: verdict.fix },
|
|
1902
|
+
failing: verdict.failing,
|
|
1895
1903
|
self,
|
|
1896
1904
|
...report.packageVersion === void 0 ? {} : { version: report.packageVersion },
|
|
1897
1905
|
directory: report.packageDirectory,
|
|
1898
|
-
...
|
|
1899
|
-
...report.recomputedSourceHash === void 0 ? {} : { recomputedSourceHash: report.recomputedSourceHash },
|
|
1900
|
-
...report.missingSource === void 0 ? {} : { missingSource: report.missingSource },
|
|
1901
|
-
...freshness.currentSource === void 0 ? {} : { currentSource: freshness.currentSource },
|
|
1902
|
-
...report.manifestBundleInputHash === void 0 ? {} : { manifestBundleInputHash: report.manifestBundleInputHash },
|
|
1903
|
-
...report.recomputedBundleInputHash === void 0 ? {} : { recomputedBundleInputHash: report.recomputedBundleInputHash },
|
|
1904
|
-
...report.missingBundleInputs === void 0 ? {} : { missingBundleInputs: report.missingBundleInputs }
|
|
1906
|
+
...DoctorJsonReport.freshnessDetails(freshness)
|
|
1905
1907
|
};
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1908
|
+
}
|
|
1909
|
+
/**
|
|
1910
|
+
* The status, the hint, the fix and whether it fails the run, for a verdict: a stale package fails and is
|
|
1911
|
+
* repacked, a package with no provenance is repacked, and the others need nothing.
|
|
1912
|
+
*
|
|
1913
|
+
* @param status - The status of a {@link LibraryFreshness} verdict.
|
|
1914
|
+
* @param staleHint - The hint of a stale package.
|
|
1915
|
+
* @param repack - The command that repacks the package.
|
|
1916
|
+
* @returns The fields.
|
|
1917
|
+
*/
|
|
1918
|
+
static verdictOf(status, staleHint, repack) {
|
|
1919
|
+
switch (status) {
|
|
1920
|
+
case LibraryFreshnessStatus.Fresh: return {
|
|
1909
1921
|
status: DoctorPackageStatus.Fresh,
|
|
1910
|
-
|
|
1911
|
-
failing: false,
|
|
1912
|
-
...details
|
|
1922
|
+
failing: false
|
|
1913
1923
|
};
|
|
1914
|
-
case
|
|
1915
|
-
name: report.specifier,
|
|
1924
|
+
case LibraryFreshnessStatus.Stale: return {
|
|
1916
1925
|
status: DoctorPackageStatus.Stale,
|
|
1917
|
-
|
|
1918
|
-
hint: self ? "Repack (`vp pack`) before publishing or consuming it." : `Rebuild it (\`vp pack\` in ${report.packageDirectory}) before trusting this app's build.`,
|
|
1926
|
+
hint: staleHint,
|
|
1919
1927
|
fix: repack,
|
|
1920
|
-
failing: true
|
|
1921
|
-
...details
|
|
1928
|
+
failing: true
|
|
1922
1929
|
};
|
|
1923
|
-
case
|
|
1924
|
-
name: report.specifier,
|
|
1930
|
+
case LibraryFreshnessStatus.NoProvenance: return {
|
|
1925
1931
|
status: DoctorPackageStatus.NoProvenance,
|
|
1926
|
-
reason: "The packed manifest predates the provenance stamp, so nothing was compared.",
|
|
1927
1932
|
hint: "Repack (`vp pack`) to enable the staleness check.",
|
|
1928
1933
|
fix: repack,
|
|
1929
|
-
failing: false
|
|
1930
|
-
...details
|
|
1934
|
+
failing: false
|
|
1931
1935
|
};
|
|
1932
|
-
case
|
|
1933
|
-
name: report.specifier,
|
|
1936
|
+
case LibraryFreshnessStatus.NoSource: return {
|
|
1934
1937
|
status: DoctorPackageStatus.NoSource,
|
|
1935
|
-
|
|
1936
|
-
failing: false,
|
|
1937
|
-
...details
|
|
1938
|
+
failing: false
|
|
1938
1939
|
};
|
|
1939
1940
|
}
|
|
1940
1941
|
}
|
|
1942
|
+
/**
|
|
1943
|
+
* The fields of a verdict that the JSON documents print beside its status: the hashes, the missing and the
|
|
1944
|
+
* current source, and the stale build entries. A field the verdict does not have is left out.
|
|
1945
|
+
*
|
|
1946
|
+
* @param report - A {@link LibraryFreshness} verdict.
|
|
1947
|
+
* @returns The fields.
|
|
1948
|
+
*/
|
|
1949
|
+
static freshnessDetails(report) {
|
|
1950
|
+
return {
|
|
1951
|
+
...report.manifestSourceHash === void 0 ? {} : { manifestSourceHash: report.manifestSourceHash },
|
|
1952
|
+
...report.recomputedSourceHash === void 0 ? {} : { recomputedSourceHash: report.recomputedSourceHash },
|
|
1953
|
+
...report.missingSource === void 0 ? {} : { missingSource: report.missingSource },
|
|
1954
|
+
...report.currentSource === void 0 ? {} : { currentSource: report.currentSource },
|
|
1955
|
+
...report.manifestBundleInputHash === void 0 ? {} : { manifestBundleInputHash: report.manifestBundleInputHash },
|
|
1956
|
+
...report.recomputedBundleInputHash === void 0 ? {} : { recomputedBundleInputHash: report.recomputedBundleInputHash },
|
|
1957
|
+
...report.missingBundleInputs === void 0 ? {} : { missingBundleInputs: report.missingBundleInputs },
|
|
1958
|
+
...report.staleBuildEntries === void 0 ? {} : { staleBuildEntries: report.staleBuildEntries }
|
|
1959
|
+
};
|
|
1960
|
+
}
|
|
1961
|
+
/**
|
|
1962
|
+
* Why a package has the status of its verdict, as one sentence.
|
|
1963
|
+
*
|
|
1964
|
+
* @param report - A {@link LibraryFreshness} verdict.
|
|
1965
|
+
* @returns The reason.
|
|
1966
|
+
*/
|
|
1967
|
+
static reasonOf(report) {
|
|
1968
|
+
switch (report.status) {
|
|
1969
|
+
case LibraryFreshnessStatus.Fresh: return report.manifestSourceHash === void 0 ? "The pack inputs file matches the local build inputs." : `The packed manifest matches the local source (${report.manifestSourceHash}).`;
|
|
1970
|
+
case LibraryFreshnessStatus.Stale: return DoctorJsonReport.staleReason(report);
|
|
1971
|
+
case LibraryFreshnessStatus.NoProvenance: return "The packed manifest predates the provenance stamp, so nothing was compared.";
|
|
1972
|
+
case LibraryFreshnessStatus.NoSource: return "A published package with no local src/, so its provenance is display-only.";
|
|
1973
|
+
}
|
|
1974
|
+
}
|
|
1975
|
+
/**
|
|
1976
|
+
* The shell command that repacks a package.
|
|
1977
|
+
*
|
|
1978
|
+
* @param packageDirectory - The package root.
|
|
1979
|
+
* @returns `cd <directory> && vp pack`, with the directory quoted when it needs quotes.
|
|
1980
|
+
*/
|
|
1981
|
+
static repackCommand(packageDirectory) {
|
|
1982
|
+
return `cd ${DoctorJsonReport.shellWord(packageDirectory)} && vp pack`;
|
|
1983
|
+
}
|
|
1941
1984
|
/** Why a stale report is stale, in the words and the order of the text report. */
|
|
1942
1985
|
static staleReason(report) {
|
|
1943
|
-
|
|
1944
|
-
if (report.
|
|
1945
|
-
if (report.
|
|
1946
|
-
if (report.
|
|
1947
|
-
|
|
1986
|
+
const entries = LibraryFreshness.staleBuildEntriesClause(report);
|
|
1987
|
+
if (report.currentSource !== void 0) return `The source folders it was packed from (${(report.missingSource ?? []).join(", ")}) no longer exist, and its source is now in ${report.currentSource}${entries}.`;
|
|
1988
|
+
if (report.missingSource !== void 0 && report.missingSource.length > 0) return `The source it was packed from (${report.missingSource.join(", ")}) is no longer on disk${entries}.`;
|
|
1989
|
+
if (report.recomputedSourceHash !== void 0 && report.manifestSourceHash !== void 0 && report.recomputedSourceHash !== report.manifestSourceHash) return DoctorJsonReport.sourceChangedReason(report);
|
|
1990
|
+
if (report.staleBuildEntries !== void 0 && report.staleBuildEntries.length > 0) {
|
|
1991
|
+
const reason = LibraryFreshness.staleBuildEntriesReason(report.staleBuildEntries);
|
|
1992
|
+
return `${reason.charAt(0).toUpperCase()}${reason.slice(1)}.`;
|
|
1993
|
+
}
|
|
1994
|
+
if (report.missingBundleInputs !== void 0) return `Files its build read outside its source (${report.missingBundleInputs.join(", ")}) are no longer on disk.`;
|
|
1995
|
+
if (report.recomputedBundleInputHash !== void 0) return `A file its build read outside its source changed: the build inputs (${report.recomputedBundleInputHash}) no longer match the packed manifest (${String(report.manifestBundleInputHash)}).`;
|
|
1996
|
+
return DoctorJsonReport.sourceChangedReason(report);
|
|
1997
|
+
}
|
|
1998
|
+
/** The reason for a source fingerprint that no longer matches the manifest. */
|
|
1999
|
+
static sourceChangedReason(report) {
|
|
2000
|
+
return `Its src/ (${String(report.recomputedSourceHash)}) no longer matches the packed manifest (${String(report.manifestSourceHash)})${LibraryFreshness.staleBuildEntriesClause(report)}.`;
|
|
1948
2001
|
}
|
|
1949
2002
|
/** The registry diagnostic, when the probe failed. */
|
|
1950
2003
|
static registryDiagnostics(result) {
|
|
@@ -2032,8 +2085,10 @@ var DoctorReport = class DoctorReport {
|
|
|
2032
2085
|
const name = self ? `${report.specifier} (this package)` : report.specifier;
|
|
2033
2086
|
const version = report.packageVersion === void 0 ? "" : ` v${report.packageVersion}`;
|
|
2034
2087
|
switch (report.status) {
|
|
2035
|
-
case BoundaryProvenanceStatus.Fresh:
|
|
2036
|
-
|
|
2088
|
+
case BoundaryProvenanceStatus.Fresh:
|
|
2089
|
+
if (report.manifestSourceHash === void 0) return `✔ ${name}${version} — dist is current with its build inputs`;
|
|
2090
|
+
return self ? `✔ ${name}${version} — dist is current with src/ (${report.manifestSourceHash})` : `✔ ${name}${version} — up to date (${report.manifestSourceHash})`;
|
|
2091
|
+
case BoundaryProvenanceStatus.Stale: return self && report.missingSource === void 0 && !LibraryFreshness.isBundleStale(report) ? `✖ ${name}${version} — STALE: your src/ (${report.recomputedSourceHash}) no longer matches your own dist manifest (${report.manifestSourceHash})${LibraryFreshness.staleBuildEntriesClause(report)}. Repack (\`vp pack\`) before publishing or consuming it.` : LibraryFreshness.staleLine(self ? name : report.specifier, report);
|
|
2037
2092
|
case BoundaryProvenanceStatus.NoProvenance: return `● ${name}${version} — manifest predates the provenance stamp; repack to enable the check.`;
|
|
2038
2093
|
case BoundaryProvenanceStatus.NoSource: return `● ${name}${version} — published package (no local src/); provenance is display-only.`;
|
|
2039
2094
|
}
|
|
@@ -2192,6 +2247,411 @@ const doctorCommand = defineCommand({
|
|
|
2192
2247
|
}
|
|
2193
2248
|
});
|
|
2194
2249
|
//#endregion
|
|
2250
|
+
//#region src/freshness-json-document.ts
|
|
2251
|
+
/**
|
|
2252
|
+
* The verdict on one workspace package in a {@link FreshnessJsonDocument}. The first four are the staleness
|
|
2253
|
+
* values of `DoctorPackageStatus`.
|
|
2254
|
+
*/
|
|
2255
|
+
const FreshnessPackageStatus = {
|
|
2256
|
+
/** Every recorded input of the package matches the files on disk. */
|
|
2257
|
+
Fresh: "fresh",
|
|
2258
|
+
/** The source or another recorded input changed since the last pack, or is gone. */
|
|
2259
|
+
Stale: "stale",
|
|
2260
|
+
/** The packed manifest has no provenance stamp, so nothing was compared. */
|
|
2261
|
+
NoProvenance: "no-provenance",
|
|
2262
|
+
/** None of the recorded source is on disk, so nothing was compared. */
|
|
2263
|
+
NoSource: "no-source",
|
|
2264
|
+
/** The check of the package failed, for example because its `heximon.config.ts` threw. */
|
|
2265
|
+
Error: "error"
|
|
2266
|
+
};
|
|
2267
|
+
/** The stable codes of the `heximon freshness` diagnostics. */
|
|
2268
|
+
const FreshnessDiagnosticCode = {
|
|
2269
|
+
/** A workspace package changed since its last pack. Error. */
|
|
2270
|
+
StalePackage: "freshness.stale-package",
|
|
2271
|
+
/** The check of one package failed; the message has the error. Error. */
|
|
2272
|
+
CheckFailed: "freshness.check-failed",
|
|
2273
|
+
/** The `pnpm-workspace.yaml` or the root `package.json` cannot be read or parsed. Error. */
|
|
2274
|
+
WorkspaceUnreadable: "freshness.workspace-unreadable"
|
|
2275
|
+
};
|
|
2276
|
+
//#endregion
|
|
2277
|
+
//#region src/workspace-packages.ts
|
|
2278
|
+
/** Where the member list of a {@link Workspace} comes from. */
|
|
2279
|
+
const WorkspaceKind = {
|
|
2280
|
+
/** The `packages:` list of a `pnpm-workspace.yaml`. */
|
|
2281
|
+
Pnpm: "pnpm",
|
|
2282
|
+
/** The `workspaces` field of a root `package.json` (npm, Yarn, Bun). */
|
|
2283
|
+
PackageJson: "package-json",
|
|
2284
|
+
/** No workspace file: the nearest package is the only member. */
|
|
2285
|
+
SinglePackage: "single-package",
|
|
2286
|
+
/** No workspace file and no `package.json` from the start directory up. */
|
|
2287
|
+
None: "none"
|
|
2288
|
+
};
|
|
2289
|
+
/**
|
|
2290
|
+
* Finds the workspace that holds a directory and lists its packages. It walks up to the nearest directory with a
|
|
2291
|
+
* `pnpm-workspace.yaml`, or with a `package.json` that has a `workspaces` field (an array, or Yarn's
|
|
2292
|
+
* `{ packages }` form). With neither, the nearest package is a one-package workspace. The root package is a
|
|
2293
|
+
* member when it has a `package.json`. A `!` pattern removes the members it matches, and `node_modules` is never
|
|
2294
|
+
* searched. A folder that two paths reach (a symlink) is one member, under the path that sorts first.
|
|
2295
|
+
*/
|
|
2296
|
+
var WorkspacePackages = class WorkspacePackages {
|
|
2297
|
+
/** The package manifest file name. */
|
|
2298
|
+
static packageJsonFileName = "package.json";
|
|
2299
|
+
/** The folder name the member search never enters. */
|
|
2300
|
+
static installedDirectoryName = "node_modules";
|
|
2301
|
+
/**
|
|
2302
|
+
* Find the workspace of a directory.
|
|
2303
|
+
*
|
|
2304
|
+
* @param startDirectory - The directory to start the walk from.
|
|
2305
|
+
* @returns The workspace and its member packages.
|
|
2306
|
+
* @throws When a `pnpm-workspace.yaml` or a root `package.json` on the walk cannot be read or parsed.
|
|
2307
|
+
*/
|
|
2308
|
+
static locate(startDirectory) {
|
|
2309
|
+
const start = resolve(startDirectory);
|
|
2310
|
+
let nearestPackage;
|
|
2311
|
+
let current = start;
|
|
2312
|
+
while (true) {
|
|
2313
|
+
if (existsSync(join(current, PnpmWorkspaceManifest.fileName))) {
|
|
2314
|
+
const manifestPath = join(current, PnpmWorkspaceManifest.fileName);
|
|
2315
|
+
const patterns = PnpmWorkspaceManifest.parsePackagePatterns(readFileSync(manifestPath, "utf8"), manifestPath);
|
|
2316
|
+
return WorkspacePackages.workspaceOf(current, WorkspaceKind.Pnpm, patterns);
|
|
2317
|
+
}
|
|
2318
|
+
const packageJsonPath = join(current, WorkspacePackages.packageJsonFileName);
|
|
2319
|
+
if (existsSync(packageJsonPath)) {
|
|
2320
|
+
nearestPackage ??= current;
|
|
2321
|
+
const patterns = WorkspacePackages.workspacePatternsOf(packageJsonPath);
|
|
2322
|
+
if (patterns !== void 0) return WorkspacePackages.workspaceOf(current, WorkspaceKind.PackageJson, patterns);
|
|
2323
|
+
}
|
|
2324
|
+
const parent = dirname(current);
|
|
2325
|
+
if (parent === current) break;
|
|
2326
|
+
current = parent;
|
|
2327
|
+
}
|
|
2328
|
+
return nearestPackage === void 0 ? {
|
|
2329
|
+
root: start,
|
|
2330
|
+
kind: WorkspaceKind.None,
|
|
2331
|
+
members: []
|
|
2332
|
+
} : {
|
|
2333
|
+
root: nearestPackage,
|
|
2334
|
+
kind: WorkspaceKind.SinglePackage,
|
|
2335
|
+
members: [{
|
|
2336
|
+
directory: WorkspacePackages.realPathOf(nearestPackage),
|
|
2337
|
+
path: "."
|
|
2338
|
+
}]
|
|
2339
|
+
};
|
|
2340
|
+
}
|
|
2341
|
+
/**
|
|
2342
|
+
* The `workspaces` patterns of a `package.json`.
|
|
2343
|
+
*
|
|
2344
|
+
* @returns The patterns, or `undefined` when the file has no `workspaces` field in a known form.
|
|
2345
|
+
* @throws When the file is not valid JSON.
|
|
2346
|
+
*/
|
|
2347
|
+
static workspacePatternsOf(packageJsonPath) {
|
|
2348
|
+
let parsed;
|
|
2349
|
+
try {
|
|
2350
|
+
parsed = JSON.parse(readFileSync(packageJsonPath, "utf8"));
|
|
2351
|
+
} catch (cause) {
|
|
2352
|
+
const reason = cause instanceof Error ? cause.message : String(cause);
|
|
2353
|
+
throw new Error(`${packageJsonPath} could not be parsed as JSON: ${reason}`, { cause });
|
|
2354
|
+
}
|
|
2355
|
+
if (typeof parsed !== "object" || parsed === null || !("workspaces" in parsed)) return;
|
|
2356
|
+
const workspaces = parsed.workspaces;
|
|
2357
|
+
const patterns = typeof workspaces === "object" && workspaces !== null && "packages" in workspaces ? workspaces.packages : workspaces;
|
|
2358
|
+
return Array.isArray(patterns) ? patterns.filter((pattern) => typeof pattern === "string") : void 0;
|
|
2359
|
+
}
|
|
2360
|
+
/** The workspace at a root: the root package, then every directory the patterns name that has a package. */
|
|
2361
|
+
static workspaceOf(root, kind, patterns) {
|
|
2362
|
+
const includes = patterns.filter((pattern) => !pattern.trim().startsWith("!")).map((pattern) => WorkspacePackages.normalizedPattern(pattern)).filter((pattern) => pattern !== "");
|
|
2363
|
+
const excludes = patterns.filter((pattern) => pattern.trim().startsWith("!")).map((pattern) => WorkspacePackages.normalizedPattern(pattern.trim().slice(1))).filter((pattern) => pattern !== "");
|
|
2364
|
+
const members = (includes.length === 0 ? [] : globSync(includes, {
|
|
2365
|
+
cwd: root,
|
|
2366
|
+
exclude: (entry) => basename(String(entry)) === WorkspacePackages.installedDirectoryName
|
|
2367
|
+
})).map((match) => relative(root, resolve(root, match))).filter((path) => !WorkspacePackages.isExcluded(path, excludes)).sort();
|
|
2368
|
+
const workspaceMembers = [];
|
|
2369
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2370
|
+
for (const memberPath of ["", ...members]) {
|
|
2371
|
+
const directory = resolve(root, memberPath);
|
|
2372
|
+
if (!WorkspacePackages.isPackageDirectory(directory)) continue;
|
|
2373
|
+
const realDirectory = WorkspacePackages.realPathOf(directory);
|
|
2374
|
+
if (!seen.has(realDirectory)) {
|
|
2375
|
+
seen.add(realDirectory);
|
|
2376
|
+
workspaceMembers.push({
|
|
2377
|
+
directory: realDirectory,
|
|
2378
|
+
path: memberPath || "."
|
|
2379
|
+
});
|
|
2380
|
+
}
|
|
2381
|
+
}
|
|
2382
|
+
return {
|
|
2383
|
+
root,
|
|
2384
|
+
kind,
|
|
2385
|
+
members: workspaceMembers
|
|
2386
|
+
};
|
|
2387
|
+
}
|
|
2388
|
+
/** A pattern without a leading `./` and a trailing `/`, so it matches the paths the glob returns. */
|
|
2389
|
+
static normalizedPattern(pattern) {
|
|
2390
|
+
return pattern.trim().replace(/^(?:\.\/)+/, "").replace(/\/+$/, "");
|
|
2391
|
+
}
|
|
2392
|
+
/** Whether a `!` pattern removes a member path. A pattern that ends in a globstar also removes its folder. */
|
|
2393
|
+
static isExcluded(memberPath, excludes) {
|
|
2394
|
+
return excludes.some((pattern) => matchesGlob(memberPath, pattern) || matchesGlob(`${memberPath}/`, pattern));
|
|
2395
|
+
}
|
|
2396
|
+
/** Whether a path is a directory that holds a `package.json`. */
|
|
2397
|
+
static isPackageDirectory(directory) {
|
|
2398
|
+
try {
|
|
2399
|
+
return statSync(directory).isDirectory() && existsSync(join(directory, WorkspacePackages.packageJsonFileName));
|
|
2400
|
+
} catch {
|
|
2401
|
+
return false;
|
|
2402
|
+
}
|
|
2403
|
+
}
|
|
2404
|
+
/** The real path of a directory, or the path itself when it cannot be resolved. */
|
|
2405
|
+
static realPathOf(directory) {
|
|
2406
|
+
try {
|
|
2407
|
+
return realpathSync(directory);
|
|
2408
|
+
} catch {
|
|
2409
|
+
return directory;
|
|
2410
|
+
}
|
|
2411
|
+
}
|
|
2412
|
+
};
|
|
2413
|
+
//#endregion
|
|
2414
|
+
//#region src/workspace-freshness.ts
|
|
2415
|
+
/**
|
|
2416
|
+
* Judges every package of a workspace with {@link LibraryFreshness}: each package that has a Heximon manifest or
|
|
2417
|
+
* a `dist/heximon.inputs.json`. It compiles nothing and runs no other audit, so it only stats and hashes files.
|
|
2418
|
+
* A package whose check throws (a `heximon.config.ts` that fails to load) gets an `error` verdict.
|
|
2419
|
+
*/
|
|
2420
|
+
var WorkspaceFreshness = class WorkspaceFreshness {
|
|
2421
|
+
/** The fix hint of a package whose check failed. */
|
|
2422
|
+
static checkFailedHint = "Fix the error, for example in the package's heximon.config.ts, then run heximon freshness again.";
|
|
2423
|
+
/**
|
|
2424
|
+
* Find the workspace of a directory and judge each of its packages.
|
|
2425
|
+
*
|
|
2426
|
+
* @param startDirectory - Any directory inside the workspace.
|
|
2427
|
+
* @returns The verdicts.
|
|
2428
|
+
* @throws CommandFailure with {@link FreshnessDiagnosticCode.WorkspaceUnreadable} when the workspace file
|
|
2429
|
+
* cannot be read.
|
|
2430
|
+
*/
|
|
2431
|
+
static async run(startDirectory) {
|
|
2432
|
+
const workspace = WorkspaceFreshness.locate(startDirectory);
|
|
2433
|
+
const freshness = new LibraryFreshness();
|
|
2434
|
+
const packages = [];
|
|
2435
|
+
for (const member of workspace.members) {
|
|
2436
|
+
const identity = WorkspaceFreshness.identityOf(member);
|
|
2437
|
+
const recordProblem = LibraryFreshness.recordProblemOf(member.directory);
|
|
2438
|
+
if (recordProblem !== void 0) {
|
|
2439
|
+
packages.push({
|
|
2440
|
+
...identity,
|
|
2441
|
+
error: recordProblem,
|
|
2442
|
+
recordUnreadable: true
|
|
2443
|
+
});
|
|
2444
|
+
continue;
|
|
2445
|
+
}
|
|
2446
|
+
try {
|
|
2447
|
+
const report = await freshness.inspect(member.directory);
|
|
2448
|
+
if (report !== null) packages.push({
|
|
2449
|
+
...identity,
|
|
2450
|
+
report
|
|
2451
|
+
});
|
|
2452
|
+
} catch (error) {
|
|
2453
|
+
packages.push({
|
|
2454
|
+
...identity,
|
|
2455
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2456
|
+
});
|
|
2457
|
+
}
|
|
2458
|
+
}
|
|
2459
|
+
return {
|
|
2460
|
+
workspace,
|
|
2461
|
+
packages,
|
|
2462
|
+
skippedPackages: workspace.members.length - packages.length
|
|
2463
|
+
};
|
|
2464
|
+
}
|
|
2465
|
+
/** Whether a verdict fails the run: a stale package, or one whose check failed. */
|
|
2466
|
+
static isFailing(verdict) {
|
|
2467
|
+
return verdict.error !== void 0 || verdict.report?.status === LibraryFreshnessStatus.Stale;
|
|
2468
|
+
}
|
|
2469
|
+
/** Whether the run exits 1. */
|
|
2470
|
+
static isFailure(result) {
|
|
2471
|
+
return result.packages.some((verdict) => WorkspaceFreshness.isFailing(verdict));
|
|
2472
|
+
}
|
|
2473
|
+
/** The status of a verdict. */
|
|
2474
|
+
static statusOf(verdict) {
|
|
2475
|
+
return verdict.report === void 0 ? FreshnessPackageStatus.Error : verdict.report.status;
|
|
2476
|
+
}
|
|
2477
|
+
/**
|
|
2478
|
+
* The text line of one package: its name, version and path, its status, and for a stale package the
|
|
2479
|
+
* {@link LibraryFreshness.staleLine} that names the changed and missing inputs.
|
|
2480
|
+
*
|
|
2481
|
+
* @param verdict - The verdict.
|
|
2482
|
+
* @returns The line.
|
|
2483
|
+
*/
|
|
2484
|
+
static lineOf(verdict) {
|
|
2485
|
+
const version = verdict.version === void 0 ? "" : ` v${verdict.version}`;
|
|
2486
|
+
const label = `${verdict.name}${version} (${verdict.path})`;
|
|
2487
|
+
const report = verdict.report;
|
|
2488
|
+
if (report === void 0) return `✖ ${label} — ERROR: ${String(verdict.error)}`;
|
|
2489
|
+
switch (report.status) {
|
|
2490
|
+
case LibraryFreshnessStatus.Fresh: return `✔ ${label} — fresh`;
|
|
2491
|
+
case LibraryFreshnessStatus.Stale: return LibraryFreshness.staleLine(label, {
|
|
2492
|
+
...report,
|
|
2493
|
+
packageVersion: void 0
|
|
2494
|
+
}, { forApp: false });
|
|
2495
|
+
case LibraryFreshnessStatus.NoProvenance: return `● ${label} — no provenance: the manifest predates the provenance stamp. Repack to enable the check.`;
|
|
2496
|
+
case LibraryFreshnessStatus.NoSource: return `● ${label} — no source: none of its recorded source is on disk, so nothing was compared.`;
|
|
2497
|
+
}
|
|
2498
|
+
}
|
|
2499
|
+
/** The closing sentence of the run. */
|
|
2500
|
+
static summary(result) {
|
|
2501
|
+
const skipped = result.skippedPackages === 0 ? "" : ` ${String(result.skippedPackages)} package(s) without a record were skipped.`;
|
|
2502
|
+
if (result.packages.length === 0) return result.workspace.members.length === 0 ? `No package found from ${result.workspace.root}.` : `No workspace package has a freshness record.${skipped}`;
|
|
2503
|
+
const count = (status) => result.packages.filter((verdict) => WorkspaceFreshness.statusOf(verdict) === status).length;
|
|
2504
|
+
const parts = [`${String(count(FreshnessPackageStatus.Stale))} stale`, `${String(count(FreshnessPackageStatus.Fresh))} fresh`];
|
|
2505
|
+
const notCompared = count(FreshnessPackageStatus.NoProvenance) + count(FreshnessPackageStatus.NoSource);
|
|
2506
|
+
if (notCompared > 0) parts.push(`${String(notCompared)} not compared`);
|
|
2507
|
+
if (count(FreshnessPackageStatus.Error) > 0) parts.push(`${String(count(FreshnessPackageStatus.Error))} failed`);
|
|
2508
|
+
return `${String(result.packages.length)} package(s) with a record: ${parts.join(", ")}.${skipped}`;
|
|
2509
|
+
}
|
|
2510
|
+
/**
|
|
2511
|
+
* Build the `--json` document of one run.
|
|
2512
|
+
*
|
|
2513
|
+
* @param result - The verdicts.
|
|
2514
|
+
* @param staleOnly - List only the entries that fail the run.
|
|
2515
|
+
* @returns The document. Its `ok` is `false` exactly when {@link isFailure} is `true`.
|
|
2516
|
+
*/
|
|
2517
|
+
static toJson(result, staleOnly) {
|
|
2518
|
+
const packages = result.packages.map((verdict) => WorkspaceFreshness.entryOf(verdict));
|
|
2519
|
+
return {
|
|
2520
|
+
ok: !WorkspaceFreshness.isFailure(result),
|
|
2521
|
+
diagnostics: packages.flatMap((entry) => WorkspaceFreshness.diagnosticsOf(entry)),
|
|
2522
|
+
workspaceRoot: result.workspace.root,
|
|
2523
|
+
workspaceKind: result.workspace.kind,
|
|
2524
|
+
packages: staleOnly ? packages.filter((entry) => entry.failing) : packages,
|
|
2525
|
+
skippedPackages: result.skippedPackages,
|
|
2526
|
+
summary: WorkspaceFreshness.summary(result)
|
|
2527
|
+
};
|
|
2528
|
+
}
|
|
2529
|
+
/** The workspace of a directory, with a file that cannot be read as the command's failure. */
|
|
2530
|
+
static locate(startDirectory) {
|
|
2531
|
+
try {
|
|
2532
|
+
return WorkspacePackages.locate(startDirectory);
|
|
2533
|
+
} catch (error) {
|
|
2534
|
+
throw new CommandFailure([CliDiagnostics.error(FreshnessDiagnosticCode.WorkspaceUnreadable, error instanceof Error ? error.message : String(error), { hint: "Fix the workspace file, then run heximon freshness again." })]);
|
|
2535
|
+
}
|
|
2536
|
+
}
|
|
2537
|
+
/** One JSON package entry. */
|
|
2538
|
+
static entryOf(verdict) {
|
|
2539
|
+
const location = {
|
|
2540
|
+
...verdict.version === void 0 ? {} : { version: verdict.version },
|
|
2541
|
+
path: verdict.path,
|
|
2542
|
+
directory: verdict.directory
|
|
2543
|
+
};
|
|
2544
|
+
if (verdict.report === void 0) return {
|
|
2545
|
+
name: verdict.name,
|
|
2546
|
+
status: FreshnessPackageStatus.Error,
|
|
2547
|
+
reason: String(verdict.error),
|
|
2548
|
+
...verdict.recordUnreadable === true ? {
|
|
2549
|
+
hint: `Repack it (\`vp pack\` in ${verdict.directory}) to write the record again.`,
|
|
2550
|
+
fix: DoctorJsonReport.repackCommand(verdict.directory)
|
|
2551
|
+
} : { hint: WorkspaceFreshness.checkFailedHint },
|
|
2552
|
+
failing: true,
|
|
2553
|
+
...location
|
|
2554
|
+
};
|
|
2555
|
+
const fields = DoctorJsonReport.verdictOf(verdict.report.status, `Rebuild it (\`vp pack\` in ${verdict.directory}).`, DoctorJsonReport.repackCommand(verdict.directory));
|
|
2556
|
+
return {
|
|
2557
|
+
name: verdict.name,
|
|
2558
|
+
status: verdict.report.status,
|
|
2559
|
+
reason: DoctorJsonReport.reasonOf(verdict.report),
|
|
2560
|
+
...fields.hint === void 0 ? {} : { hint: fields.hint },
|
|
2561
|
+
...fields.fix === void 0 ? {} : { fix: fields.fix },
|
|
2562
|
+
failing: fields.failing,
|
|
2563
|
+
...location,
|
|
2564
|
+
...DoctorJsonReport.freshnessDetails(verdict.report)
|
|
2565
|
+
};
|
|
2566
|
+
}
|
|
2567
|
+
/** The error diagnostic of an entry that fails the run. */
|
|
2568
|
+
static diagnosticsOf(entry) {
|
|
2569
|
+
if (!entry.failing) return [];
|
|
2570
|
+
return [CliDiagnostics.error(entry.status === FreshnessPackageStatus.Error ? FreshnessDiagnosticCode.CheckFailed : FreshnessDiagnosticCode.StalePackage, `${entry.name} (${entry.path}): ${entry.reason}`, entry.hint === void 0 ? {} : { hint: entry.hint })];
|
|
2571
|
+
}
|
|
2572
|
+
/** The name, version and paths of a package, from its `package.json`. */
|
|
2573
|
+
static identityOf(member) {
|
|
2574
|
+
const directory = member.directory;
|
|
2575
|
+
let name;
|
|
2576
|
+
let version;
|
|
2577
|
+
try {
|
|
2578
|
+
const parsed = JSON.parse(readFileSync(join(directory, "package.json"), "utf8"));
|
|
2579
|
+
if (typeof parsed === "object" && parsed !== null) {
|
|
2580
|
+
const fields = parsed;
|
|
2581
|
+
name = typeof fields.name === "string" ? fields.name : void 0;
|
|
2582
|
+
version = typeof fields.version === "string" ? fields.version : void 0;
|
|
2583
|
+
}
|
|
2584
|
+
} catch {}
|
|
2585
|
+
return {
|
|
2586
|
+
name: name ?? basename(directory),
|
|
2587
|
+
...version === void 0 ? {} : { version },
|
|
2588
|
+
path: member.path,
|
|
2589
|
+
directory
|
|
2590
|
+
};
|
|
2591
|
+
}
|
|
2592
|
+
};
|
|
2593
|
+
//#endregion
|
|
2594
|
+
//#region src/freshness-command.ts
|
|
2595
|
+
/** The text renderer behind {@link freshnessCommand}: one clack line per package, then the summary. */
|
|
2596
|
+
var FreshnessReport = class {
|
|
2597
|
+
/**
|
|
2598
|
+
* Print the report of one run.
|
|
2599
|
+
*
|
|
2600
|
+
* @param result - The verdicts.
|
|
2601
|
+
* @param staleOnly - Print only the packages that fail the run.
|
|
2602
|
+
*/
|
|
2603
|
+
static render(result, staleOnly) {
|
|
2604
|
+
for (const verdict of result.packages) {
|
|
2605
|
+
const failing = WorkspaceFreshness.isFailing(verdict);
|
|
2606
|
+
if (staleOnly && !failing) continue;
|
|
2607
|
+
const line = WorkspaceFreshness.lineOf(verdict);
|
|
2608
|
+
if (failing) log.error(line);
|
|
2609
|
+
else if (verdict.report?.status === LibraryFreshnessStatus.Fresh) log.success(line);
|
|
2610
|
+
else log.info(line);
|
|
2611
|
+
}
|
|
2612
|
+
outro(WorkspaceFreshness.summary(result));
|
|
2613
|
+
}
|
|
2614
|
+
};
|
|
2615
|
+
/**
|
|
2616
|
+
* `heximon freshness` — lists every package of the workspace that has a freshness record, with its status. It
|
|
2617
|
+
* finds the workspace root from any directory inside it, judges each package with the rule of `heximon doctor`
|
|
2618
|
+
* ({@link LibraryFreshness}), and exits 1 when a package is stale or its check failed. It compiles nothing and
|
|
2619
|
+
* runs no other audit.
|
|
2620
|
+
*/
|
|
2621
|
+
const freshnessCommand = defineCommand({
|
|
2622
|
+
meta: {
|
|
2623
|
+
name: "freshness",
|
|
2624
|
+
description: "List every workspace package with a freshness record (a Heximon manifest or dist/heximon.inputs.json), its status, and the input that made it stale. Works from any directory of the workspace. Exits 1 when a package is stale."
|
|
2625
|
+
},
|
|
2626
|
+
args: {
|
|
2627
|
+
cwd: {
|
|
2628
|
+
type: "string",
|
|
2629
|
+
description: "A directory inside the workspace; the command walks up to the workspace root.",
|
|
2630
|
+
default: "."
|
|
2631
|
+
},
|
|
2632
|
+
json: CommandOutput.jsonFlag,
|
|
2633
|
+
stale: {
|
|
2634
|
+
type: "boolean",
|
|
2635
|
+
description: "List only the packages that fail the run: the stale ones, and the ones whose check failed.",
|
|
2636
|
+
default: false
|
|
2637
|
+
}
|
|
2638
|
+
},
|
|
2639
|
+
async run({ args }) {
|
|
2640
|
+
const commandOutput = CommandOutput.resolve("freshness", args);
|
|
2641
|
+
await commandOutput.run(async () => {
|
|
2642
|
+
const startDirectory = ProjectRoot.resolve(args.cwd);
|
|
2643
|
+
const result = await WorkspaceFreshness.run(startDirectory);
|
|
2644
|
+
const staleOnly = args.stale === true;
|
|
2645
|
+
if (WorkspaceFreshness.isFailure(result)) process.exitCode = 1;
|
|
2646
|
+
if (commandOutput.isJson()) commandOutput.emit(WorkspaceFreshness.toJson(result, staleOnly));
|
|
2647
|
+
else {
|
|
2648
|
+
intro(`heximon freshness — ${basename(result.workspace.root)}`);
|
|
2649
|
+
FreshnessReport.render(result, staleOnly);
|
|
2650
|
+
}
|
|
2651
|
+
});
|
|
2652
|
+
}
|
|
2653
|
+
});
|
|
2654
|
+
//#endregion
|
|
2195
2655
|
//#region src/graph-json-document.ts
|
|
2196
2656
|
/** The stable codes of the failures `heximon graph` raises itself. */
|
|
2197
2657
|
const GraphDiagnosticCode = {
|
|
@@ -5927,6 +6387,7 @@ const main = defineCommand({
|
|
|
5927
6387
|
routes: routesCommand,
|
|
5928
6388
|
graph: graphCommand,
|
|
5929
6389
|
doctor: doctorCommand,
|
|
6390
|
+
freshness: freshnessCommand,
|
|
5930
6391
|
provision: provisionCommand,
|
|
5931
6392
|
migrations: migrationsCommand,
|
|
5932
6393
|
workflows: workflowsCommand,
|
package/dist/index.d.mts
CHANGED
|
@@ -827,11 +827,45 @@ interface DoctorPackageCopy {
|
|
|
827
827
|
/** Who resolves to this copy (`the app`, `this package`, or a package name). */
|
|
828
828
|
readonly origins: readonly string[];
|
|
829
829
|
}
|
|
830
|
+
/** One build entry of a package whose recorded inputs no longer match the files on disk. */
|
|
831
|
+
interface DoctorStaleBuildEntry {
|
|
832
|
+
/** The key of the build entry: its output folder, its entry file-name patterns and its format. */
|
|
833
|
+
readonly entry: string;
|
|
834
|
+
/** The package-relative inputs whose digest changed. */
|
|
835
|
+
readonly changedInputs: readonly string[];
|
|
836
|
+
/** The package-relative inputs that are no longer on disk. */
|
|
837
|
+
readonly missingInputs: readonly string[];
|
|
838
|
+
}
|
|
839
|
+
/**
|
|
840
|
+
* The fields of a staleness verdict, as `heximon doctor --json` and `heximon freshness --json` both print them.
|
|
841
|
+
* Each field is present only when the verdict has it.
|
|
842
|
+
*/
|
|
843
|
+
interface DoctorFreshnessDetails {
|
|
844
|
+
/** The source fingerprint the packed manifest recorded. */
|
|
845
|
+
readonly manifestSourceHash?: string;
|
|
846
|
+
/** The source fingerprint computed from the local source now. */
|
|
847
|
+
readonly recomputedSourceHash?: string;
|
|
848
|
+
/** The recorded source folders and files that are no longer on disk. */
|
|
849
|
+
readonly missingSource?: readonly string[];
|
|
850
|
+
/** The current source directory, when the source moved since the pack. */
|
|
851
|
+
readonly currentSource?: string;
|
|
852
|
+
/** The hash of the files the bundle read outside the source, as the packed manifest recorded it. */
|
|
853
|
+
readonly manifestBundleInputHash?: string;
|
|
854
|
+
/** That hash recomputed from the files now, when the source matched and every file is on disk. */
|
|
855
|
+
readonly recomputedBundleInputHash?: string;
|
|
856
|
+
/** The recorded bundle inputs (package-relative) that are no longer on disk. */
|
|
857
|
+
readonly missingBundleInputs?: readonly string[];
|
|
858
|
+
/**
|
|
859
|
+
* The build entries of the package's `dist/heximon.inputs.json` whose inputs changed or are gone: each entry's
|
|
860
|
+
* key and its changed and missing inputs (package-relative).
|
|
861
|
+
*/
|
|
862
|
+
readonly staleBuildEntries?: readonly DoctorStaleBuildEntry[];
|
|
863
|
+
}
|
|
830
864
|
/**
|
|
831
865
|
* One audited package. A staleness verdict and a fork are separate entries, so a stale library that also forks
|
|
832
866
|
* appears twice: once with its staleness status and once with `fork`.
|
|
833
867
|
*/
|
|
834
|
-
interface DoctorPackageResult {
|
|
868
|
+
interface DoctorPackageResult extends DoctorFreshnessDetails {
|
|
835
869
|
/** The package name, or the bare specifier the app imports a consumed library under. */
|
|
836
870
|
readonly name: string;
|
|
837
871
|
/** The verdict. */
|
|
@@ -850,20 +884,6 @@ interface DoctorPackageResult {
|
|
|
850
884
|
readonly version?: string;
|
|
851
885
|
/** The real package directory, for a staleness entry. */
|
|
852
886
|
readonly directory?: string;
|
|
853
|
-
/** The source fingerprint the packed manifest recorded. */
|
|
854
|
-
readonly manifestSourceHash?: string;
|
|
855
|
-
/** The source fingerprint computed from the local source now. */
|
|
856
|
-
readonly recomputedSourceHash?: string;
|
|
857
|
-
/** The recorded source folders and files that are no longer on disk. */
|
|
858
|
-
readonly missingSource?: readonly string[];
|
|
859
|
-
/** The current source directory, when the source moved since the pack. */
|
|
860
|
-
readonly currentSource?: string;
|
|
861
|
-
/** The hash of the files the bundle read outside the source, as the packed manifest recorded it. */
|
|
862
|
-
readonly manifestBundleInputHash?: string;
|
|
863
|
-
/** That hash recomputed from the files now, when the source matched and every file is on disk. */
|
|
864
|
-
readonly recomputedBundleInputHash?: string;
|
|
865
|
-
/** The recorded bundle inputs (package-relative) that are no longer on disk. */
|
|
866
|
-
readonly missingBundleInputs?: readonly string[];
|
|
867
887
|
/** Every physical copy, for a `fork` entry. */
|
|
868
888
|
readonly copies?: readonly DoctorPackageCopy[];
|
|
869
889
|
}
|
|
@@ -935,6 +955,86 @@ interface DoctorJsonDocument extends CliJsonDocument {
|
|
|
935
955
|
readonly summary: string;
|
|
936
956
|
}
|
|
937
957
|
//#endregion
|
|
958
|
+
//#region src/workspace-packages.d.ts
|
|
959
|
+
/** Where the member list of a {@link Workspace} comes from. */
|
|
960
|
+
declare const WorkspaceKind: {
|
|
961
|
+
/** The `packages:` list of a `pnpm-workspace.yaml`. */
|
|
962
|
+
readonly Pnpm: "pnpm";
|
|
963
|
+
/** The `workspaces` field of a root `package.json` (npm, Yarn, Bun). */
|
|
964
|
+
readonly PackageJson: "package-json";
|
|
965
|
+
/** No workspace file: the nearest package is the only member. */
|
|
966
|
+
readonly SinglePackage: "single-package";
|
|
967
|
+
/** No workspace file and no `package.json` from the start directory up. */
|
|
968
|
+
readonly None: "none";
|
|
969
|
+
};
|
|
970
|
+
/** One of the {@link WorkspaceKind} values. */
|
|
971
|
+
type WorkspaceKind = (typeof WorkspaceKind)[keyof typeof WorkspaceKind];
|
|
972
|
+
//#endregion
|
|
973
|
+
//#region src/freshness-json-document.d.ts
|
|
974
|
+
/**
|
|
975
|
+
* The verdict on one workspace package in a {@link FreshnessJsonDocument}. The first four are the staleness
|
|
976
|
+
* values of `DoctorPackageStatus`.
|
|
977
|
+
*/
|
|
978
|
+
declare const FreshnessPackageStatus: {
|
|
979
|
+
/** Every recorded input of the package matches the files on disk. */
|
|
980
|
+
readonly Fresh: "fresh";
|
|
981
|
+
/** The source or another recorded input changed since the last pack, or is gone. */
|
|
982
|
+
readonly Stale: "stale";
|
|
983
|
+
/** The packed manifest has no provenance stamp, so nothing was compared. */
|
|
984
|
+
readonly NoProvenance: "no-provenance";
|
|
985
|
+
/** None of the recorded source is on disk, so nothing was compared. */
|
|
986
|
+
readonly NoSource: "no-source";
|
|
987
|
+
/** The check of the package failed, for example because its `heximon.config.ts` threw. */
|
|
988
|
+
readonly Error: "error";
|
|
989
|
+
};
|
|
990
|
+
/** One of the {@link FreshnessPackageStatus} values. */
|
|
991
|
+
type FreshnessPackageStatus = (typeof FreshnessPackageStatus)[keyof typeof FreshnessPackageStatus];
|
|
992
|
+
/** The stable codes of the `heximon freshness` diagnostics. */
|
|
993
|
+
declare const FreshnessDiagnosticCode: {
|
|
994
|
+
/** A workspace package changed since its last pack. Error. */
|
|
995
|
+
readonly StalePackage: "freshness.stale-package";
|
|
996
|
+
/** The check of one package failed; the message has the error. Error. */
|
|
997
|
+
readonly CheckFailed: "freshness.check-failed";
|
|
998
|
+
/** The `pnpm-workspace.yaml` or the root `package.json` cannot be read or parsed. Error. */
|
|
999
|
+
readonly WorkspaceUnreadable: "freshness.workspace-unreadable";
|
|
1000
|
+
};
|
|
1001
|
+
/** One of the {@link FreshnessDiagnosticCode} values. */
|
|
1002
|
+
type FreshnessDiagnosticCode = (typeof FreshnessDiagnosticCode)[keyof typeof FreshnessDiagnosticCode];
|
|
1003
|
+
/** One workspace package that has a record: a Heximon manifest, or a `dist/heximon.inputs.json`. */
|
|
1004
|
+
interface FreshnessPackageResult extends DoctorFreshnessDetails {
|
|
1005
|
+
/** The `name` of the package's `package.json`, else its folder name. */
|
|
1006
|
+
readonly name: string;
|
|
1007
|
+
/** The verdict. */
|
|
1008
|
+
readonly status: FreshnessPackageStatus;
|
|
1009
|
+
/** Why the package has this status. For `error`, the error message. */
|
|
1010
|
+
readonly reason: string;
|
|
1011
|
+
/** How to fix the package, as text. Absent when there is nothing to fix. */
|
|
1012
|
+
readonly hint?: string;
|
|
1013
|
+
/** The shell command that fixes the package. Absent when no single command fixes it. */
|
|
1014
|
+
readonly fix?: string;
|
|
1015
|
+
/** Whether this entry fails the run: `stale` and `error` do. */
|
|
1016
|
+
readonly failing: boolean;
|
|
1017
|
+
/** The `version` of the package's `package.json`, when it has one. */
|
|
1018
|
+
readonly version?: string;
|
|
1019
|
+
/** The package folder, relative to the workspace root (`.` for the root package). */
|
|
1020
|
+
readonly path: string;
|
|
1021
|
+
/** The absolute package folder. */
|
|
1022
|
+
readonly directory: string;
|
|
1023
|
+
}
|
|
1024
|
+
/** The `heximon freshness --json` document. `ok` is `false` exactly when the run exits 1. */
|
|
1025
|
+
interface FreshnessJsonDocument extends CliJsonDocument {
|
|
1026
|
+
/** The absolute workspace root. */
|
|
1027
|
+
readonly workspaceRoot: string;
|
|
1028
|
+
/** Where the member list came from. */
|
|
1029
|
+
readonly workspaceKind: WorkspaceKind;
|
|
1030
|
+
/** Each workspace package that has a record. With `--stale`, only the entries that fail the run. */
|
|
1031
|
+
readonly packages: readonly FreshnessPackageResult[];
|
|
1032
|
+
/** How many workspace packages have no record, so they were not checked. */
|
|
1033
|
+
readonly skippedPackages: number;
|
|
1034
|
+
/** The closing sentence of the text report. */
|
|
1035
|
+
readonly summary: string;
|
|
1036
|
+
}
|
|
1037
|
+
//#endregion
|
|
938
1038
|
//#region src/module-graph-report.d.ts
|
|
939
1039
|
/** A cross-package boundary node summarized for the graph report — its factory plus its `requires`/`exports` tokens. */
|
|
940
1040
|
interface BoundarySummary {
|
|
@@ -1395,4 +1495,4 @@ interface WorkflowsJsonDocument extends CliJsonDocument {
|
|
|
1395
1495
|
};
|
|
1396
1496
|
}
|
|
1397
1497
|
//#endregion
|
|
1398
|
-
export type { BoundarySummary, CheckDiagnosticCode, CheckJsonDocument, CheckUpgradeBaselineResult, CliDiagnostic, CliDiagnosticCode, CliFailureDocument, CliJsonDocument, CreateDiagnosticCode, CreateJsonDocument, DeployAction, DeployDiagnosticCode, DeployJsonCredential, DeployJsonDestroy, DeployJsonDocument, DeployJsonMigration, DeployJsonResources, DeployJsonShip, DeployJsonShipResult, DeployJsonTarget, DoctorCopyCensusResult, DoctorCopyCensusStatus, DoctorDiagnosticCode, DoctorJsonDocument, DoctorMode, DoctorPackageCopy, DoctorPackageResult, DoctorPackageStatus, DoctorPeerClosureResult, DoctorPeerPivot, DoctorPlatformTracingResult, DoctorRegistryResult, GraphDiagnosticCode, GraphJsonDocument, InstalledSkill, MigrationsDatabaseView, MigrationsDiagnosticCode, MigrationsDrizzleKitResult, MigrationsGenerateEntry, MigrationsGenerateJsonDocument, MigrationsMigrateJsonDocument, MigrationsPushJsonDocument, MigrationsStatusEntry, MigrationsStatusJsonDocument, MigrationsStepEntry, MigrationsStepStatus, PreparedTarget, ProcessRunResult, ProcessRunner, Prompter, ProvisionDiagnosticCode, ProvisionJsonDocument, ProvisionJsonResource, ProvisionJsonResult, RegistryAccessReport, RoutesDiagnosticCode, RoutesJsonDocument, ScaffoldRequest, ScaffoldResult, SkillJsonEntry, SkillSourceJsonEntry, SkillSourceRecording, SkillsAddJsonDocument, SkillsDiagnosticCode, SkillsSyncJsonDocument, StartersScaffoldRequest, TaskDiagnosticCode, TaskRunError, TaskRunJsonDocument, TemplateSelection, TemplateSource, WorkflowBaselineState, WorkflowInstanceInventory, WorkflowInstanceView, WorkflowStatusEntry, WorkflowStatusJsonEntry, WorkflowsDiagnosticCode, WorkflowsJsonDocument };
|
|
1498
|
+
export type { BoundarySummary, CheckDiagnosticCode, CheckJsonDocument, CheckUpgradeBaselineResult, CliDiagnostic, CliDiagnosticCode, CliFailureDocument, CliJsonDocument, CreateDiagnosticCode, CreateJsonDocument, DeployAction, DeployDiagnosticCode, DeployJsonCredential, DeployJsonDestroy, DeployJsonDocument, DeployJsonMigration, DeployJsonResources, DeployJsonShip, DeployJsonShipResult, DeployJsonTarget, DoctorCopyCensusResult, DoctorCopyCensusStatus, DoctorDiagnosticCode, DoctorFreshnessDetails, DoctorJsonDocument, DoctorMode, DoctorPackageCopy, DoctorPackageResult, DoctorPackageStatus, DoctorPeerClosureResult, DoctorPeerPivot, DoctorPlatformTracingResult, DoctorRegistryResult, DoctorStaleBuildEntry, FreshnessDiagnosticCode, FreshnessJsonDocument, FreshnessPackageResult, FreshnessPackageStatus, GraphDiagnosticCode, GraphJsonDocument, InstalledSkill, MigrationsDatabaseView, MigrationsDiagnosticCode, MigrationsDrizzleKitResult, MigrationsGenerateEntry, MigrationsGenerateJsonDocument, MigrationsMigrateJsonDocument, MigrationsPushJsonDocument, MigrationsStatusEntry, MigrationsStatusJsonDocument, MigrationsStepEntry, MigrationsStepStatus, PreparedTarget, ProcessRunResult, ProcessRunner, Prompter, ProvisionDiagnosticCode, ProvisionJsonDocument, ProvisionJsonResource, ProvisionJsonResult, RegistryAccessReport, RoutesDiagnosticCode, RoutesJsonDocument, ScaffoldRequest, ScaffoldResult, SkillJsonEntry, SkillSourceJsonEntry, SkillSourceRecording, SkillsAddJsonDocument, SkillsDiagnosticCode, SkillsSyncJsonDocument, StartersScaffoldRequest, TaskDiagnosticCode, TaskRunError, TaskRunJsonDocument, TemplateSelection, TemplateSource, WorkflowBaselineState, WorkflowInstanceInventory, WorkflowInstanceView, WorkflowStatusEntry, WorkflowStatusJsonEntry, WorkflowsDiagnosticCode, WorkflowsJsonDocument, WorkspaceKind };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "heximon",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"description": "The Heximon CLI: create, develop, build, migrate, and deploy Heximon apps.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cli",
|
|
@@ -38,26 +38,26 @@
|
|
|
38
38
|
"dependencies": {
|
|
39
39
|
"@bomb.sh/tab": "^0.0.22",
|
|
40
40
|
"@clack/prompts": "^1.8.1",
|
|
41
|
-
"@heximon/compiler": "0.
|
|
42
|
-
"@heximon/cqrs": "0.
|
|
43
|
-
"@heximon/deploy": "0.
|
|
44
|
-
"@heximon/http": "0.
|
|
45
|
-
"@heximon/node": "0.
|
|
46
|
-
"@heximon/runtime": "0.
|
|
47
|
-
"@heximon/testing": "0.
|
|
41
|
+
"@heximon/compiler": "0.11.0",
|
|
42
|
+
"@heximon/cqrs": "0.11.0",
|
|
43
|
+
"@heximon/deploy": "0.11.0",
|
|
44
|
+
"@heximon/http": "0.11.0",
|
|
45
|
+
"@heximon/node": "0.11.0",
|
|
46
|
+
"@heximon/runtime": "0.11.0",
|
|
47
|
+
"@heximon/testing": "0.11.0",
|
|
48
48
|
"citty": "^0.2.2",
|
|
49
49
|
"giget": "^3.3.1",
|
|
50
50
|
"pathe": "^2.0.3"
|
|
51
51
|
},
|
|
52
52
|
"devDependencies": {
|
|
53
|
-
"@heximon/cloudflare": "0.
|
|
54
|
-
"@heximon/durable": "0.
|
|
55
|
-
"@heximon/integration": "0.
|
|
56
|
-
"@heximon/library": "0.
|
|
57
|
-
"@heximon/nitro": "0.
|
|
58
|
-
"@heximon/queue": "0.
|
|
59
|
-
"@heximon/schedule": "0.
|
|
60
|
-
"@heximon/schema": "0.
|
|
53
|
+
"@heximon/cloudflare": "0.11.0",
|
|
54
|
+
"@heximon/durable": "0.11.0",
|
|
55
|
+
"@heximon/integration": "0.11.0",
|
|
56
|
+
"@heximon/library": "0.11.0",
|
|
57
|
+
"@heximon/nitro": "0.11.0",
|
|
58
|
+
"@heximon/queue": "0.11.0",
|
|
59
|
+
"@heximon/schedule": "0.11.0",
|
|
60
|
+
"@heximon/schema": "0.11.0",
|
|
61
61
|
"@heximon/test-support": "0.0.1",
|
|
62
62
|
"@types/node": "^24.19.0",
|
|
63
63
|
"@typescript/native-preview": "^7.0.0-dev.20260707.2",
|