@animus-ui/extract 0.1.11 → 0.1.12
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 +47 -0
- package/crates/extract-v2/animus-extract-v2.darwin-arm64.node +0 -0
- package/crates/extract-v2/animus-extract-v2.linux-arm64-gnu.node +0 -0
- package/crates/extract-v2/animus-extract-v2.linux-x64-gnu.node +0 -0
- package/crates/extract-v2/index.d.ts +17 -98
- package/dist/index.cjs +11 -6
- package/dist/index.mjs +2 -2
- package/dist/pipeline/analyze-project-args.d.ts +0 -12
- package/dist/pipeline/analyze-project-args.d.ts.map +1 -1
- package/dist/pipeline/assemble-stylesheet.d.ts +0 -35
- package/dist/pipeline/assemble-stylesheet.d.ts.map +1 -1
- package/dist/pipeline/asset-placeholders.d.ts +0 -25
- package/dist/pipeline/asset-placeholders.d.ts.map +1 -1
- package/dist/pipeline/content-hash.d.ts +0 -7
- package/dist/pipeline/content-hash.d.ts.map +1 -1
- package/dist/pipeline/core-options.d.ts +27 -93
- package/dist/pipeline/core-options.d.ts.map +1 -1
- package/dist/pipeline/correlate-external-tokens.d.ts +10 -18
- package/dist/pipeline/correlate-external-tokens.d.ts.map +1 -1
- package/dist/pipeline/discover-files.d.ts +1 -9
- package/dist/pipeline/discover-files.d.ts.map +1 -1
- package/dist/pipeline/discover-packages.d.ts +18 -154
- package/dist/pipeline/discover-packages.d.ts.map +1 -1
- package/dist/pipeline/dynamic-prop-config.d.ts +4 -19
- package/dist/pipeline/dynamic-prop-config.d.ts.map +1 -1
- package/dist/pipeline/engine-adapter.d.ts +16 -52
- package/dist/pipeline/engine-adapter.d.ts.map +1 -1
- package/dist/pipeline/engine-retirement.d.ts +0 -19
- package/dist/pipeline/engine-retirement.d.ts.map +1 -1
- package/dist/pipeline/error-diagnostics.d.ts +0 -24
- package/dist/pipeline/error-diagnostics.d.ts.map +1 -1
- package/dist/pipeline/index.d.ts +9 -8
- package/dist/pipeline/index.d.ts.map +1 -1
- package/dist/pipeline/internal-wire.d.ts +2 -0
- package/dist/pipeline/internal-wire.d.ts.map +1 -0
- package/dist/pipeline/manifest-diagnostics.d.ts +32 -37
- package/dist/pipeline/manifest-diagnostics.d.ts.map +1 -1
- package/dist/pipeline/manifest-schema.d.ts +117 -0
- package/dist/pipeline/manifest-schema.d.ts.map +1 -0
- package/dist/pipeline/mdx-preprocessor.d.ts +9 -33
- package/dist/pipeline/mdx-preprocessor.d.ts.map +1 -1
- package/dist/pipeline/path-aliases.d.ts +0 -19
- package/dist/pipeline/path-aliases.d.ts.map +1 -1
- package/dist/pipeline/post-process-css.d.ts +0 -8
- package/dist/pipeline/post-process-css.d.ts.map +1 -1
- package/dist/pipeline/prefix.d.ts +3 -10
- package/dist/pipeline/prefix.d.ts.map +1 -1
- package/dist/pipeline/replacement-plans.d.ts +10 -34
- package/dist/pipeline/replacement-plans.d.ts.map +1 -1
- package/dist/pipeline/resolve-asset.d.ts +4 -7
- package/dist/pipeline/resolve-asset.d.ts.map +1 -1
- package/dist/pipeline/run-analysis.d.ts +15 -27
- package/dist/pipeline/run-analysis.d.ts.map +1 -1
- package/dist/pipeline/source-corpus.d.ts +25 -0
- package/dist/pipeline/source-corpus.d.ts.map +1 -0
- package/dist/pipeline/source-identity.d.ts +23 -53
- package/dist/pipeline/source-identity.d.ts.map +1 -1
- package/dist/pipeline/source-ingestion.d.ts +48 -53
- package/dist/pipeline/source-ingestion.d.ts.map +1 -1
- package/dist/pipeline/static-css.d.ts +4 -13
- package/dist/pipeline/static-css.d.ts.map +1 -1
- package/dist/pipeline/structural-self-check.d.ts +0 -20
- package/dist/pipeline/structural-self-check.d.ts.map +1 -1
- package/dist/pipeline/svelte-source-adapter.d.ts +5 -12
- package/dist/pipeline/svelte-source-adapter.d.ts.map +1 -1
- package/dist/pipeline/system-config.d.ts +15 -32
- package/dist/pipeline/system-config.d.ts.map +1 -1
- package/dist/pipeline/system-props-module.d.ts +0 -15
- package/dist/pipeline/system-props-module.d.ts.map +1 -1
- package/dist/pipeline/timing-waterfall.d.ts +0 -11
- package/dist/pipeline/timing-waterfall.d.ts.map +1 -1
- package/dist/pipeline/tsconfig-paths.d.ts +2 -3
- package/dist/pipeline/tsconfig-paths.d.ts.map +1 -1
- package/dist/pipeline/unit-fallback.d.ts +0 -5
- package/dist/pipeline/unit-fallback.d.ts.map +1 -1
- package/dist/pipeline/utils.d.ts +1 -1
- package/dist/pipeline/utils.d.ts.map +1 -1
- package/dist/pipeline/watch-keys.d.ts +0 -10
- package/dist/pipeline/watch-keys.d.ts.map +1 -1
- package/dist/{pipeline-lkbOoyAr.mjs → pipeline-DeEENy5C.mjs} +1940 -2263
- package/dist/{pipeline-ojNeABi0.cjs → pipeline-x1yBZ2UP.cjs} +1970 -2269
- package/dist/session/extraction-session.d.ts +110 -316
- package/dist/session/extraction-session.d.ts.map +1 -1
- package/dist/session/index.d.ts +6 -20
- package/dist/session/index.d.ts.map +1 -1
- package/dist/session/published-set.d.ts +45 -25
- package/dist/session/published-set.d.ts.map +1 -1
- package/dist/session/resolve-packages.d.ts +6 -7
- package/dist/session/resolve-packages.d.ts.map +1 -1
- package/dist/session/session-paths.d.ts +38 -72
- package/dist/session/session-paths.d.ts.map +1 -1
- package/dist/session/singleton.d.ts +32 -71
- package/dist/session/singleton.d.ts.map +1 -1
- package/dist/session/timing.d.ts +0 -5
- package/dist/session/timing.d.ts.map +1 -1
- package/dist/session/turbopack-orchestrator.d.ts +17 -33
- package/dist/session/turbopack-orchestrator.d.ts.map +1 -1
- package/dist/session.cjs +530 -677
- package/dist/session.mjs +517 -672
- package/index-v2.js +0 -9
- package/package.json +9 -8
- package/dist/pipeline/external-keyframes.d.ts +0 -23
- package/dist/pipeline/external-keyframes.d.ts.map +0 -1
package/dist/session.mjs
CHANGED
|
@@ -1,35 +1,24 @@
|
|
|
1
|
-
import { A as
|
|
1
|
+
import { A as enforceExternalTokenContracts, B as firstOwners, D as postProcessCss, E as vocabularyWitnessDiagnostics, F as createSourceCorpus, G as walkPackageSources, I as collectExternalPackageSources, J as sharesVolumeRoot, K as createSourceIdentity, L as excludeCollectedPackages, N as findAssetSpecifiers, O as resolveLightningTargets, P as substituteAssetPlaceholders, R as extractSystemFilePackages, U as staleDistIncludesMessage, V as isExcludedPackageRelativePath, W as unresolvableIncludesMessage, X as discoverFiles, _ as clearEngineCache, a as assertNoErrorDiagnostics, at as resolveMode, c as buildSystemPropsModule, ct as createV2EngineApi, d as loadSystemConfig, ft as projectExternalFileOwners, i as snapshotFilePlans, it as createExcludeMatcher, j as resolveAssetFile, mt as DEFAULT_EXTENSIONS, p as serializeStaticCss, q as isPathWithinRoot, r as hashReplacementPlans, s as formatRustTimingWaterfall, t as toWatchKeys, v as runProjectAnalysis, vt as contentHash, w as unreadableSourceDiagnostic, wt as __require, xt as assembleStylesheet, z as findPackageRoot } from "./pipeline-DeEENy5C.mjs";
|
|
2
2
|
import { randomUUID } from "crypto";
|
|
3
3
|
import { existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, statSync, unlinkSync, watch, writeFileSync } from "fs";
|
|
4
4
|
import { basename, extname, join, relative, resolve } from "path";
|
|
5
5
|
//#region session/session-paths.ts
|
|
6
6
|
/**
|
|
7
|
-
* Session
|
|
8
|
-
*
|
|
9
|
-
* — shared by the session writer (extraction-session), the webpack
|
|
10
|
-
* plugin/loader, and the Turbopack loader.
|
|
11
|
-
*
|
|
12
|
-
* Deliberately free of singleton/session imports: the Turbopack loader
|
|
13
|
-
* executes in isolated worker processes and its module graph must stay
|
|
14
|
-
* worker-local (guardrail G1) — this module is pure path/shape vocabulary.
|
|
7
|
+
* Session artifact names, path derivations, and artifact shapes. Imports no
|
|
8
|
+
* session or singleton module: the Turbopack loader graph stays worker-local.
|
|
15
9
|
*/
|
|
16
|
-
/** Session-dir-relative name of the replacement-epoch disk witness
|
|
17
|
-
* (openspec: next-webpack-served-transform-coherence, design D2;
|
|
18
|
-
* session-scoped by next-turbopack-served-transform-coherence D2). */
|
|
10
|
+
/** Session-dir-relative name of the replacement-epoch disk witness. */
|
|
19
11
|
const REPLACEMENT_EPOCH_ARTIFACT = "replacements-epoch";
|
|
20
|
-
/** Virtual module id of the emitted system-props module
|
|
21
|
-
* vocabulary shared by the orchestrator and the Turbopack config
|
|
22
|
-
* assembly (hoisted here so the session home stays framework-free). */
|
|
12
|
+
/** Virtual module id of the emitted system-props module. */
|
|
23
13
|
const TURBOPACK_SYSTEM_PROPS_ID = "virtual:animus/system-props";
|
|
24
|
-
/** Session-dir-relative name of the analysis-commit transaction artifact
|
|
25
|
-
* (design D1). */
|
|
14
|
+
/** Session-dir-relative name of the analysis-commit transaction artifact. */
|
|
26
15
|
const ANALYSIS_COMMIT_ARTIFACT = "analysis-commit";
|
|
27
|
-
/** Session-dir-relative name of the analysis-status artifact
|
|
16
|
+
/** Session-dir-relative name of the analysis-status artifact. */
|
|
28
17
|
const ANALYSIS_STATUS_ARTIFACT = "analysis-status.json";
|
|
29
18
|
/** Session-dir-relative name of the manifest payload artifact. */
|
|
30
19
|
const MANIFEST_ARTIFACT = "manifest.json";
|
|
31
|
-
/** Session-dir-relative name of the analysis-inputs payload artifact
|
|
32
|
-
*
|
|
20
|
+
/** Session-dir-relative name of the analysis-inputs payload artifact — the
|
|
21
|
+
* Turbopack loader-worker hydration corpus. */
|
|
33
22
|
const ANALYSIS_INPUTS_ARTIFACT = "analysis-inputs.json";
|
|
34
23
|
/** Session-dir-relative name of the emitted stylesheet artifact. */
|
|
35
24
|
const STYLES_ARTIFACT = "styles.css";
|
|
@@ -37,126 +26,226 @@ const STYLES_ARTIFACT = "styles.css";
|
|
|
37
26
|
const SYSTEM_PROPS_ARTIFACT = "system-props.js";
|
|
38
27
|
/** Session-dir-relative directory holding copied asset() files. */
|
|
39
28
|
const SESSION_ASSETS_DIR = "assets";
|
|
40
|
-
/** The standalone CLI's published-set commit record
|
|
41
|
-
*
|
|
42
|
-
* the CLI writer and the session's start hygiene agree on ONE name — the
|
|
43
|
-
* hygiene confinement gate keys on this record, and a renamed literal on
|
|
44
|
-
* either side would silently re-arm deletion of the CLI's published output. */
|
|
29
|
+
/** The standalone CLI's published-set commit record in the flat `.animus/`
|
|
30
|
+
* tree; the session's start hygiene keys on this name before deleting. */
|
|
45
31
|
const CLI_COMMIT_ARTIFACT = "commit.json";
|
|
46
|
-
/**
|
|
47
|
-
*
|
|
32
|
+
/** Advisory claim record (pid plus heartbeat) of whichever directory holds
|
|
33
|
+
* it; sessions write it into their own tree too, despite the `CLI_` name. */
|
|
48
34
|
const CLI_LOCK_ARTIFACT = "lock.json";
|
|
49
|
-
/**
|
|
35
|
+
/** Project-relative artifact directory: the CLI's default output tree and
|
|
36
|
+
* the parent of every session tree. One spelling, or the tree splits. */
|
|
37
|
+
const ANIMUS_ARTIFACT_DIR = ".animus";
|
|
38
|
+
/** Module id the Rust emitter injects, and the alias key both bundlers
|
|
39
|
+
* register. A literal on purpose: a rename breaks built consumer packages.
|
|
40
|
+
* The Next plugin's webpack alias harvest skips this exact key, so a change
|
|
41
|
+
* here is a change in the emitter, both bundler arms and that harvest. */
|
|
42
|
+
const ANIMUS_CSS_MODULE_ID = ".animus/styles.css";
|
|
50
43
|
function sessionsRootDir(rootDir) {
|
|
51
|
-
return join(rootDir,
|
|
44
|
+
return join(rootDir, ANIMUS_ARTIFACT_DIR, "sessions");
|
|
52
45
|
}
|
|
53
|
-
/** One session's artifact directory (design D2: session-scoped trees,
|
|
54
|
-
* no global pointer). */
|
|
55
46
|
function sessionArtifactDir(rootDir, sessionId) {
|
|
56
47
|
return join(sessionsRootDir(rootDir), sessionId);
|
|
57
48
|
}
|
|
58
|
-
/** Absolute path of a session's replacement-epoch artifact — the single
|
|
59
|
-
* derivation shared by the session writer, the webpack plugin's
|
|
60
|
-
* watch-ignore entry, and both loaders' file dependencies. Takes the
|
|
61
|
-
* SESSION directory (not the project root) since the artifact was
|
|
62
|
-
* relocated into the session tree. */
|
|
63
49
|
function replacementEpochPath(sessionDir) {
|
|
64
50
|
return join(sessionDir, REPLACEMENT_EPOCH_ARTIFACT);
|
|
65
51
|
}
|
|
66
|
-
/** Absolute path of a session's analysis-commit artifact. */
|
|
67
52
|
function analysisCommitPath(sessionDir) {
|
|
68
53
|
return join(sessionDir, ANALYSIS_COMMIT_ARTIFACT);
|
|
69
54
|
}
|
|
70
|
-
/** Absolute path of a session's analysis-status artifact. */
|
|
71
55
|
function analysisStatusPath(sessionDir) {
|
|
72
56
|
return join(sessionDir, ANALYSIS_STATUS_ARTIFACT);
|
|
73
57
|
}
|
|
74
|
-
/** Absolute path of a session's manifest artifact. */
|
|
75
58
|
function manifestPath(sessionDir) {
|
|
76
59
|
return join(sessionDir, MANIFEST_ARTIFACT);
|
|
77
60
|
}
|
|
78
|
-
/** Absolute path of a session's analysis-inputs artifact. */
|
|
79
61
|
function analysisInputsPath(sessionDir) {
|
|
80
62
|
return join(sessionDir, ANALYSIS_INPUTS_ARTIFACT);
|
|
81
63
|
}
|
|
82
|
-
/** Absolute path of a session's stylesheet artifact. */
|
|
83
64
|
function stylesPath(sessionDir) {
|
|
84
65
|
return join(sessionDir, STYLES_ARTIFACT);
|
|
85
66
|
}
|
|
86
|
-
/** Absolute path of a session's system-props module artifact. */
|
|
87
67
|
function systemPropsPath(sessionDir) {
|
|
88
68
|
return join(sessionDir, SYSTEM_PROPS_ARTIFACT);
|
|
89
69
|
}
|
|
90
|
-
/**
|
|
91
|
-
*
|
|
92
|
-
* (parse/re-stringify could reformat engine-emitted JSON). */
|
|
70
|
+
/** Splices the envelope in as a leading `__animusSession` field without
|
|
71
|
+
* re-serializing: parse/re-stringify would reformat engine-emitted JSON. */
|
|
93
72
|
function envelopeJsonArtifact(payload, envelopeJson) {
|
|
94
73
|
const trimmed = payload.trimStart();
|
|
95
74
|
if (!trimmed.startsWith("{")) return payload;
|
|
96
75
|
if (trimmed.slice(1).trimStart().startsWith("}")) return `{"__animusSession":${envelopeJson}}`;
|
|
97
76
|
return `{"__animusSession":${envelopeJson},${trimmed.slice(1)}`;
|
|
98
77
|
}
|
|
99
|
-
/** Trailing-comment side-band
|
|
100
|
-
*
|
|
78
|
+
/** Trailing-comment side-band: a leading comment would displace the `@layer`
|
|
79
|
+
* declaration consumers pin at offset 0. */
|
|
101
80
|
function envelopeCssArtifact(payload, envelopeJson) {
|
|
102
81
|
return `${payload}\n/* __animusSession ${envelopeJson} */\n`;
|
|
103
82
|
}
|
|
104
|
-
/** Matches the CSS artifact's envelope side-band. */
|
|
105
83
|
const ENVELOPE_CSS_COMMENT_RE = /\/\* __animusSession (\{.*\}) \*\//;
|
|
106
|
-
/** Envelope of a JSON artifact's bytes, or undefined when
|
|
107
|
-
*
|
|
108
|
-
* torn artifact fails closed or degrades. */
|
|
84
|
+
/** Envelope of a JSON artifact's bytes, or undefined when it carries none.
|
|
85
|
+
* Throws on unparseable bytes; callers decide torn-artifact policy. */
|
|
109
86
|
function readJsonEnvelope(bytes) {
|
|
110
87
|
return JSON.parse(bytes).__animusSession;
|
|
111
88
|
}
|
|
112
|
-
/** Envelope of the CSS
|
|
113
|
-
*
|
|
89
|
+
/** Envelope of the CSS side-band comment, or undefined when it is absent.
|
|
90
|
+
* Throws on an unparseable envelope body. */
|
|
114
91
|
function readCssEnvelope(bytes) {
|
|
115
92
|
const match = bytes.match(ENVELOPE_CSS_COMMENT_RE);
|
|
116
93
|
return match ? JSON.parse(match[1]) : void 0;
|
|
117
94
|
}
|
|
118
95
|
//#endregion
|
|
119
96
|
//#region session/published-set.ts
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
97
|
+
function isPublishedSetRecord(value) {
|
|
98
|
+
return Object.prototype.toString.call(value) === "[object Object]";
|
|
99
|
+
}
|
|
100
|
+
function isPublishedSetString(value) {
|
|
101
|
+
return Object.prototype.toString.call(value) === "[object String]";
|
|
102
|
+
}
|
|
103
|
+
function isPublishedSetNumber(value) {
|
|
104
|
+
return Object.prototype.toString.call(value) === "[object Number]";
|
|
105
|
+
}
|
|
106
|
+
function decodeCommitRecord(bytes) {
|
|
107
|
+
let candidate;
|
|
108
|
+
try {
|
|
109
|
+
candidate = JSON.parse(bytes);
|
|
110
|
+
} catch {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
if (!isPublishedSetRecord(candidate) || candidate.schema !== 1) return null;
|
|
114
|
+
const declared = candidate.payloads;
|
|
115
|
+
if (!isPublishedSetRecord(declared)) return null;
|
|
116
|
+
const payloads = {};
|
|
117
|
+
for (const [name, entry] of Object.entries(declared)) {
|
|
118
|
+
if (!isPublishedSetRecord(entry) || !isPublishedSetString(entry.hash)) return null;
|
|
119
|
+
payloads[name] = { hash: entry.hash };
|
|
120
|
+
}
|
|
121
|
+
return {
|
|
122
|
+
schema: 1,
|
|
123
|
+
payloads
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
/** Recompute every recorded payload hash over raw bytes — the writer's own
|
|
127
|
+
* hashing domain — and return the mismatches; empty means the set verifies. */
|
|
138
128
|
function verifyCommitRecord(dir) {
|
|
139
|
-
let
|
|
129
|
+
let bytes;
|
|
140
130
|
try {
|
|
141
|
-
|
|
131
|
+
bytes = readFileSync(join(dir, CLI_COMMIT_ARTIFACT), "utf-8");
|
|
142
132
|
} catch (error) {
|
|
143
133
|
return [`commit record unreadable: ${String(error)}`];
|
|
144
134
|
}
|
|
145
|
-
|
|
135
|
+
const record = decodeCommitRecord(bytes);
|
|
136
|
+
if (record === null) return ["commit record is not a schema-1 payload record"];
|
|
146
137
|
const failures = [];
|
|
147
138
|
for (const [name, entry] of Object.entries(record.payloads)) try {
|
|
148
|
-
if (contentHash(readFileSync(join(dir, name))) !== entry
|
|
139
|
+
if (contentHash(readFileSync(join(dir, name))) !== entry.hash) failures.push(`${name}: bytes do not match the commit record`);
|
|
149
140
|
} catch (error) {
|
|
150
141
|
failures.push(`${name}: unreadable (${String(error)})`);
|
|
151
142
|
}
|
|
152
143
|
return failures;
|
|
153
144
|
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
*
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
*/
|
|
145
|
+
const CLI_LOCK_HEARTBEAT_INTERVAL_MS = 6e4;
|
|
146
|
+
/** A claim whose heartbeat is older than this has no live holder. Five missed
|
|
147
|
+
* refreshes: synchronous extraction can block the holder's refresh timer. */
|
|
148
|
+
const CLI_LOCK_STALE_AFTER_MS = 5 * CLI_LOCK_HEARTBEAT_INTERVAL_MS;
|
|
149
|
+
/** Absence of the file — the only read failure that means "no holder".
|
|
150
|
+
* ENOTDIR joins ENOENT: the path cannot exist under a non-directory. */
|
|
151
|
+
function isMissingFile(error) {
|
|
152
|
+
return error instanceof Object && "code" in error && (error.code === "ENOENT" || error.code === "ENOTDIR");
|
|
153
|
+
}
|
|
154
|
+
function readCliLockRecord(dir) {
|
|
155
|
+
let bytes;
|
|
156
|
+
try {
|
|
157
|
+
bytes = readFileSync(join(dir, CLI_LOCK_ARTIFACT), "utf-8");
|
|
158
|
+
} catch (error) {
|
|
159
|
+
if (isMissingFile(error)) return { kind: "none" };
|
|
160
|
+
throw error;
|
|
161
|
+
}
|
|
162
|
+
let candidate;
|
|
163
|
+
try {
|
|
164
|
+
candidate = JSON.parse(bytes);
|
|
165
|
+
} catch {
|
|
166
|
+
return { kind: "indeterminate" };
|
|
167
|
+
}
|
|
168
|
+
if (!isPublishedSetRecord(candidate) || !isPublishedSetNumber(candidate.pid)) return { kind: "indeterminate" };
|
|
169
|
+
const startedAt = candidate.startedAt;
|
|
170
|
+
const heartbeatAt = candidate.heartbeatAt;
|
|
171
|
+
const record = { pid: candidate.pid };
|
|
172
|
+
if (isPublishedSetString(startedAt)) record.startedAt = startedAt;
|
|
173
|
+
if (isPublishedSetString(heartbeatAt)) record.heartbeatAt = heartbeatAt;
|
|
174
|
+
return {
|
|
175
|
+
kind: "held",
|
|
176
|
+
record
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
function isPermissionDenied(error) {
|
|
180
|
+
return error instanceof Object && "code" in error && error.code === "EPERM";
|
|
181
|
+
}
|
|
182
|
+
function isLockHolderAlive(pid) {
|
|
183
|
+
try {
|
|
184
|
+
process.kill(pid, 0);
|
|
185
|
+
return true;
|
|
186
|
+
} catch (error) {
|
|
187
|
+
return isPermissionDenied(error);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
function checkLockLiveness(record) {
|
|
191
|
+
if (!isLockHolderAlive(record.pid)) return {
|
|
192
|
+
live: false,
|
|
193
|
+
reason: `holder pid ${record.pid} is not running`
|
|
194
|
+
};
|
|
195
|
+
const beatAt = record.heartbeatAt === void 0 ? NaN : Date.parse(record.heartbeatAt);
|
|
196
|
+
if (Number.isNaN(beatAt)) return { live: true };
|
|
197
|
+
const ageMs = Date.now() - beatAt;
|
|
198
|
+
if (ageMs <= 3e5) return { live: true };
|
|
199
|
+
return {
|
|
200
|
+
live: false,
|
|
201
|
+
reason: `holder pid ${record.pid} is running but its lock heartbeat (${record.heartbeatAt}) is ${Math.round(ageMs / 1e3)}s old, past the ${CLI_LOCK_STALE_AFTER_MS / 1e3}s staleness window — the pid names a different process now`
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
function lockRecordBytes(startedAt) {
|
|
205
|
+
return JSON.stringify({
|
|
206
|
+
pid: process.pid,
|
|
207
|
+
startedAt,
|
|
208
|
+
heartbeatAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
const HEARTBEAT_TEMP_PREFIX = ".lock-heartbeat-";
|
|
212
|
+
function holdDirectoryClaim(dir, options = {}) {
|
|
213
|
+
const startedAt = options.startedAt ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
214
|
+
const path = join(dir, CLI_LOCK_ARTIFACT);
|
|
215
|
+
const tempPath = join(dir, `${HEARTBEAT_TEMP_PREFIX}${process.pid}`);
|
|
216
|
+
const rename = options.write === "rename";
|
|
217
|
+
let refresh = null;
|
|
218
|
+
let stopped = false;
|
|
219
|
+
const stop = () => {
|
|
220
|
+
stopped = true;
|
|
221
|
+
if (refresh !== null) clearInterval(refresh);
|
|
222
|
+
refresh = null;
|
|
223
|
+
};
|
|
224
|
+
const stamp = () => {
|
|
225
|
+
try {
|
|
226
|
+
if (rename) {
|
|
227
|
+
writeFileSync(tempPath, lockRecordBytes(startedAt));
|
|
228
|
+
renameSync(tempPath, path);
|
|
229
|
+
} else writeFileSync(path, lockRecordBytes(startedAt));
|
|
230
|
+
} catch (error) {
|
|
231
|
+
stop();
|
|
232
|
+
if (isMissingFile(error)) return;
|
|
233
|
+
console.error(`[animus] Could not write the directory claim ${path}: ${String(error)}`);
|
|
234
|
+
}
|
|
235
|
+
};
|
|
236
|
+
stamp();
|
|
237
|
+
if (!stopped) {
|
|
238
|
+
refresh = setInterval(stamp, CLI_LOCK_HEARTBEAT_INTERVAL_MS);
|
|
239
|
+
refresh.unref?.();
|
|
240
|
+
}
|
|
241
|
+
return () => {
|
|
242
|
+
stop();
|
|
243
|
+
if (rename) rmSync(tempPath, { force: true });
|
|
244
|
+
rmSync(path, { force: true });
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
/** Snapshot the session's copied asset() files, sorted, BEFORE the session
|
|
248
|
+
* tree is disposed. Drivers publish them beside the emitted stylesheet. */
|
|
160
249
|
function collectSessionAssets(sessionDir) {
|
|
161
250
|
if (!sessionDir) return [];
|
|
162
251
|
const dir = join(sessionDir, SESSION_ASSETS_DIR);
|
|
@@ -168,12 +257,6 @@ function collectSessionAssets(sessionDir) {
|
|
|
168
257
|
}
|
|
169
258
|
//#endregion
|
|
170
259
|
//#region session/resolve-packages.ts
|
|
171
|
-
/**
|
|
172
|
-
* Resolve external DS package specifiers to rootDir-relative entry paths.
|
|
173
|
-
* Node-generic (no webpack APIs): Pass 1 walks the consumer's workspaces for
|
|
174
|
-
* matching package names; Pass 2 falls back to `require.resolve`. Specifiers
|
|
175
|
-
* that resolve nowhere are simply omitted (spec: silent skip).
|
|
176
|
-
*/
|
|
177
260
|
function resolvePackagesByName(rootDir, names) {
|
|
178
261
|
if (names.length === 0) return {};
|
|
179
262
|
const nameSet = new Set(names);
|
|
@@ -189,7 +272,8 @@ function resolvePackagesByName(rootDir, names) {
|
|
|
189
272
|
const pkg = JSON.parse(readFileSync(join(wsDir, "package.json"), "utf-8"));
|
|
190
273
|
const name = pkg.name || "";
|
|
191
274
|
if (nameSet.has(name)) {
|
|
192
|
-
const
|
|
275
|
+
const main = pkg.main || pkg.module || "index.ts";
|
|
276
|
+
const entryPath = resolve(wsDir, main);
|
|
193
277
|
if (existsSync(entryPath)) {
|
|
194
278
|
packageMap[name] = relative(rootDir, entryPath);
|
|
195
279
|
resolved.add(name);
|
|
@@ -201,7 +285,8 @@ function resolvePackagesByName(rootDir, names) {
|
|
|
201
285
|
for (const name of nameSet) {
|
|
202
286
|
if (resolved.has(name)) continue;
|
|
203
287
|
try {
|
|
204
|
-
|
|
288
|
+
const entryPath = __require.resolve(name, { paths: [rootDir] });
|
|
289
|
+
packageMap[name] = relative(rootDir, entryPath);
|
|
205
290
|
} catch {}
|
|
206
291
|
}
|
|
207
292
|
return packageMap;
|
|
@@ -209,86 +294,96 @@ function resolvePackagesByName(rootDir, names) {
|
|
|
209
294
|
//#endregion
|
|
210
295
|
//#region session/singleton.ts
|
|
211
296
|
/**
|
|
212
|
-
*
|
|
213
|
-
*
|
|
214
|
-
* Uses globalThis to survive the ESM/CJS module boundary: the plugin is loaded via ESM
|
|
215
|
-
* (next.config.ts import) while the loader is loaded by webpack via require(). Without
|
|
216
|
-
* globalThis, each module system gets its own singleton instance.
|
|
297
|
+
* globalThis-backed so the ESM plugin copy and the CJS webpack-loader copy
|
|
298
|
+
* share one store; module scope alone gives each copy its own instance.
|
|
217
299
|
*/
|
|
218
300
|
const MANIFEST_KEY = "__animus_manifest_json__";
|
|
219
|
-
const
|
|
301
|
+
const ANALYSIS_STARTED_KEY = "__animus_analysis_started__";
|
|
220
302
|
const SHARED_CSS_KEY = "__animus_shared_css__";
|
|
221
303
|
const SHARED_SYSTEM_PROPS_KEY = "__animus_shared_system_props__";
|
|
222
304
|
const SHARED_EXTERNAL_DIRS_KEY = "__animus_external_pkg_dirs__";
|
|
223
305
|
const SHARED_EXTERNAL_ENTRIES_KEY = "__animus_external_source_entries__";
|
|
306
|
+
const ANALYZED_HASHES_KEY = "__animus_analyzed_hashes__";
|
|
307
|
+
const REPLACEMENT_EPOCH_KEY = "__animus_replacement_epoch__";
|
|
308
|
+
const WATCH_TRANSACTION_KEY = "__animus_watch_transaction__";
|
|
309
|
+
const PROCESS_SESSION_ID_KEY = "__animus_process_session_id__";
|
|
310
|
+
const SESSION_ARTIFACT_DIR_KEY = "__animus_session_artifact_dir__";
|
|
311
|
+
const OWNING_WATCH_SESSION_KEY = "__animus_owning_watch_session__";
|
|
312
|
+
const EXCLUSIVE_SESSION_OWNER_KEY = "__animus_exclusive_session_owner__";
|
|
313
|
+
const ENGINE_KEY = "__animus_engine__";
|
|
314
|
+
const V2_ENGINE_KEY = "__animus_v2_engine__";
|
|
315
|
+
const V2_SENT_SOURCES_KEY = "__animus_v2_sent_sources__";
|
|
316
|
+
const V2_DRIFT_WARNED_KEY = "__animus_v2_drift_warned__";
|
|
317
|
+
const ENGINE_API_OVERRIDE_KEY = "__animus_engine_api_override__";
|
|
318
|
+
const singletonStore = globalThis;
|
|
319
|
+
const isSingletonString = (value) => {
|
|
320
|
+
if (Object(value) === value) return false;
|
|
321
|
+
try {
|
|
322
|
+
String.prototype.valueOf.call(value);
|
|
323
|
+
return true;
|
|
324
|
+
} catch {
|
|
325
|
+
return false;
|
|
326
|
+
}
|
|
327
|
+
};
|
|
224
328
|
function getManifestJson() {
|
|
225
|
-
return
|
|
329
|
+
return singletonStore[MANIFEST_KEY];
|
|
226
330
|
}
|
|
227
331
|
function setManifestJson(json) {
|
|
228
|
-
|
|
332
|
+
singletonStore[MANIFEST_KEY] = json;
|
|
229
333
|
}
|
|
230
|
-
|
|
231
|
-
|
|
334
|
+
/**
|
|
335
|
+
* A latch over the most recently STARTED analysis, never cleared on settle:
|
|
336
|
+
* non-null means one was scheduled. In-flight lives in getWatchTransaction.
|
|
337
|
+
*/
|
|
338
|
+
function getAnalysisStartedPromise() {
|
|
339
|
+
return singletonStore[ANALYSIS_STARTED_KEY];
|
|
232
340
|
}
|
|
233
|
-
function
|
|
234
|
-
|
|
341
|
+
function setAnalysisStartedPromise(promise) {
|
|
342
|
+
singletonStore[ANALYSIS_STARTED_KEY] = promise;
|
|
235
343
|
}
|
|
236
|
-
function
|
|
237
|
-
|
|
344
|
+
function resetAnalysisStartedPromise() {
|
|
345
|
+
singletonStore[ANALYSIS_STARTED_KEY] = null;
|
|
238
346
|
}
|
|
239
347
|
function getSharedCss() {
|
|
240
|
-
return
|
|
348
|
+
return singletonStore[SHARED_CSS_KEY] || "";
|
|
241
349
|
}
|
|
242
350
|
function setSharedCss(css) {
|
|
243
|
-
|
|
351
|
+
singletonStore[SHARED_CSS_KEY] = css;
|
|
244
352
|
}
|
|
245
353
|
function getSharedSystemProps() {
|
|
246
|
-
return
|
|
354
|
+
return singletonStore[SHARED_SYSTEM_PROPS_KEY] || "";
|
|
247
355
|
}
|
|
248
356
|
function setSharedSystemProps(content) {
|
|
249
|
-
|
|
357
|
+
singletonStore[SHARED_SYSTEM_PROPS_KEY] = content;
|
|
250
358
|
}
|
|
251
359
|
function getSharedExternalDirs() {
|
|
252
|
-
return
|
|
360
|
+
return singletonStore[SHARED_EXTERNAL_DIRS_KEY] || [];
|
|
253
361
|
}
|
|
254
362
|
function setSharedExternalDirs(dirs) {
|
|
255
|
-
|
|
363
|
+
singletonStore[SHARED_EXTERNAL_DIRS_KEY] = dirs;
|
|
256
364
|
}
|
|
257
365
|
function getSharedExternalEntries() {
|
|
258
|
-
return
|
|
366
|
+
return singletonStore[SHARED_EXTERNAL_ENTRIES_KEY] || /* @__PURE__ */ new Map();
|
|
259
367
|
}
|
|
260
368
|
function setSharedExternalEntries(entries) {
|
|
261
|
-
|
|
369
|
+
singletonStore[SHARED_EXTERNAL_ENTRIES_KEY] = entries;
|
|
262
370
|
}
|
|
263
|
-
/** One typed globalThis slot — the single accessor shape every
|
|
264
|
-
* singleton-published value shares (null when unset). */
|
|
265
371
|
function globalSlot(key) {
|
|
266
|
-
const store = globalThis;
|
|
267
372
|
return {
|
|
268
|
-
get: () =>
|
|
373
|
+
get: () => singletonStore[key] ?? null,
|
|
269
374
|
set: (value) => {
|
|
270
|
-
|
|
375
|
+
singletonStore[key] = value;
|
|
271
376
|
}
|
|
272
377
|
};
|
|
273
378
|
}
|
|
274
|
-
const ANALYZED_HASHES_KEY = "__animus_analyzed_hashes__";
|
|
275
|
-
const REPLACEMENT_EPOCH_KEY = "__animus_replacement_epoch__";
|
|
276
|
-
const WATCH_TRANSACTION_KEY = "__animus_watch_transaction__";
|
|
277
|
-
const PROCESS_SESSION_ID_KEY = "__animus_process_session_id__";
|
|
278
|
-
const SESSION_ARTIFACT_DIR_KEY = "__animus_session_artifact_dir__";
|
|
279
|
-
const OWNING_WATCH_SESSION_KEY = "__animus_owning_watch_session__";
|
|
280
|
-
const EXCLUSIVE_SESSION_OWNER_KEY = "__animus_exclusive_session_owner__";
|
|
281
379
|
const analyzedHashesSlot = globalSlot(ANALYZED_HASHES_KEY);
|
|
282
380
|
const replacementEpochSlot = globalSlot(REPLACEMENT_EPOCH_KEY);
|
|
283
381
|
const watchTransactionSlot = globalSlot(WATCH_TRANSACTION_KEY);
|
|
284
382
|
const sessionArtifactDirSlot = globalSlot(SESSION_ARTIFACT_DIR_KEY);
|
|
285
383
|
const owningWatchSessionSlot = globalSlot(OWNING_WATCH_SESSION_KEY);
|
|
286
384
|
/**
|
|
287
|
-
* The session that
|
|
288
|
-
*
|
|
289
|
-
* its own session AND its own watcher with its own modified set; a
|
|
290
|
-
* non-owning session forwards its batch here instead of dropping it (a file
|
|
291
|
-
* only that compiler watches would otherwise never be analyzed).
|
|
385
|
+
* The only session that can run watch analysis. A non-owning session forwards
|
|
386
|
+
* its batch here; dropping it loses files only that watcher sees.
|
|
292
387
|
*/
|
|
293
388
|
function getOwningWatchSession() {
|
|
294
389
|
return owningWatchSessionSlot.get();
|
|
@@ -297,12 +392,8 @@ function setOwningWatchSession(session) {
|
|
|
297
392
|
owningWatchSessionSlot.set(session);
|
|
298
393
|
}
|
|
299
394
|
/**
|
|
300
|
-
* Per-file
|
|
301
|
-
*
|
|
302
|
-
* unconditional catching-up guard (openspec:
|
|
303
|
-
* next-webpack-served-transform-coherence, design D4). Includes files
|
|
304
|
-
* analyzed with ZERO animus entries: analyzed identity is membership in the
|
|
305
|
-
* analysis input set, not manifest entry count.
|
|
395
|
+
* Per-file content hashes of the last published analysis (relPath → hash of
|
|
396
|
+
* the exact bytes analyzed), including files that produced zero entries.
|
|
306
397
|
*/
|
|
307
398
|
function getAnalyzedHashes() {
|
|
308
399
|
return analyzedHashesSlot.get();
|
|
@@ -311,52 +402,31 @@ function setAnalyzedHashes(hashes) {
|
|
|
311
402
|
analyzedHashesSlot.set(hashes);
|
|
312
403
|
}
|
|
313
404
|
/**
|
|
314
|
-
* Session identity is per
|
|
315
|
-
*
|
|
316
|
-
* next-turbopack-served-transform-coherence, design D2). The first
|
|
317
|
-
* ExtractionSession constructed in a process claims a fresh randomUUID;
|
|
318
|
-
* every later instance (client/server/RSC compilers each construct one)
|
|
319
|
-
* adopts it, so all compilers alias, watch-ignore, and publish ONE
|
|
320
|
-
* session-scoped artifact tree. Separate invocations (`next dev` +
|
|
321
|
-
* `next build` co-writing) are separate processes and therefore separate
|
|
322
|
-
* sessions by construction.
|
|
405
|
+
* Session identity is per process: the first caller claims a fresh id and
|
|
406
|
+
* every later one adopts it, so all compilers share one artifact tree.
|
|
323
407
|
*/
|
|
324
408
|
function claimProcessSessionId() {
|
|
325
|
-
const
|
|
326
|
-
|
|
327
|
-
if (typeof existing === "string" && existing.length > 0) return existing;
|
|
409
|
+
const existing = singletonStore[PROCESS_SESSION_ID_KEY];
|
|
410
|
+
if (isSingletonString(existing) && existing.length > 0) return existing;
|
|
328
411
|
const fresh = randomUUID();
|
|
329
|
-
|
|
412
|
+
singletonStore[PROCESS_SESSION_ID_KEY] = fresh;
|
|
330
413
|
return fresh;
|
|
331
414
|
}
|
|
332
415
|
/**
|
|
333
|
-
*
|
|
334
|
-
*
|
|
335
|
-
* shared slots above (manifest, css, artifact dir) plus the one session id
|
|
336
|
-
* mean two CONCURRENT drive loops in one process (a webpack MultiCompiler
|
|
337
|
-
* array config, parallel rollup array builds, two programmatic CLI runs)
|
|
338
|
-
* would share one session directory, overwrite each other's manifests, and
|
|
339
|
-
* delete each other's live trees on dispose. Drivers that run the loop
|
|
340
|
-
* exclusively (the unplugin host) claim for the build's lifetime and
|
|
341
|
-
* release on dispose; SEQUENTIAL claim/release cycles are legal. The Next
|
|
342
|
-
* multi-compiler adoption path stays the non-exclusive default. Throws
|
|
343
|
-
* naming both hosts and the remediation on overlap. The key lives in
|
|
344
|
-
* `SINGLETON_GLOBAL_KEYS`, so per-test global resets clear a leaked claim.
|
|
416
|
+
* Two CONCURRENT drive loops in one process would share one session tree and
|
|
417
|
+
* clobber each other; sequential claim/release cycles are legal.
|
|
345
418
|
*/
|
|
346
419
|
function claimExclusiveSessionOwner(label) {
|
|
347
|
-
const
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
store[EXCLUSIVE_SESSION_OWNER_KEY] = label;
|
|
420
|
+
const active = singletonStore[EXCLUSIVE_SESSION_OWNER_KEY];
|
|
421
|
+
if (isSingletonString(active)) throw new Error(`[animus] a second Animus host ("${label}") started while "${active}" is still active in this process. The extraction session is process-global (one session tree, one manifest), so concurrent hosts would clobber each other's analysis. Run one Animus-enabled config per process, or make the builds sequential.`);
|
|
422
|
+
singletonStore[EXCLUSIVE_SESSION_OWNER_KEY] = label;
|
|
351
423
|
return () => {
|
|
352
|
-
if (
|
|
424
|
+
if (singletonStore[EXCLUSIVE_SESSION_OWNER_KEY] === label) delete singletonStore[EXCLUSIVE_SESSION_OWNER_KEY];
|
|
353
425
|
};
|
|
354
426
|
}
|
|
355
427
|
/**
|
|
356
|
-
* Absolute
|
|
357
|
-
*
|
|
358
|
-
* dependency path (the loader shares the process with the pipeline; the
|
|
359
|
-
* Turbopack loader instead receives the directory via its options).
|
|
428
|
+
* Absolute artifact directory of the OWNING session's last publication. The
|
|
429
|
+
* Turbopack loader runs out of process and receives it via options instead.
|
|
360
430
|
*/
|
|
361
431
|
function getSessionArtifactDir() {
|
|
362
432
|
return sessionArtifactDirSlot.get();
|
|
@@ -365,14 +435,8 @@ function setSessionArtifactDir(dir) {
|
|
|
365
435
|
sessionArtifactDirSlot.set(dir);
|
|
366
436
|
}
|
|
367
437
|
/**
|
|
368
|
-
*
|
|
369
|
-
*
|
|
370
|
-
* — openspec: next-webpack-served-transform-coherence, design D5; the
|
|
371
|
-
* served system-props module rides as the served-dependency witness so
|
|
372
|
-
* offline system-props changes move the epoch). Published by the
|
|
373
|
-
* owning session AFTER the manifest so a reader that observes the epoch
|
|
374
|
-
* always observes at least that generation's manifest. (Session
|
|
375
|
-
* attribution lives on the DISK epoch artifact — no in-process mirror.)
|
|
438
|
+
* Replacement epoch of the last published analysis, written AFTER the
|
|
439
|
+
* manifest so an epoch reader always sees at least that generation's manifest.
|
|
376
440
|
*/
|
|
377
441
|
function getReplacementEpoch() {
|
|
378
442
|
return replacementEpochSlot.get();
|
|
@@ -381,10 +445,8 @@ function setReplacementEpoch(epoch) {
|
|
|
381
445
|
replacementEpochSlot.set(epoch);
|
|
382
446
|
}
|
|
383
447
|
/**
|
|
384
|
-
* The one in-flight watch-analysis transaction
|
|
385
|
-
*
|
|
386
|
-
* compiler (client/server/RSC — each holds its own session instance) joins
|
|
387
|
-
* this promise instead of proceeding against a pre-transaction generation.
|
|
448
|
+
* The one in-flight watch-analysis transaction: a compiler that did not start
|
|
449
|
+
* it joins this promise rather than read a pre-transaction generation.
|
|
388
450
|
*/
|
|
389
451
|
function getWatchTransaction() {
|
|
390
452
|
return watchTransactionSlot.get();
|
|
@@ -392,69 +454,49 @@ function getWatchTransaction() {
|
|
|
392
454
|
function setWatchTransaction(transaction) {
|
|
393
455
|
watchTransactionSlot.set(transaction);
|
|
394
456
|
}
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
* instances and the webpack loader honor the same choice as the owner. */
|
|
457
|
+
/** Engine selection travels through the singleton so non-owning compilers
|
|
458
|
+
* and the webpack loader honor the owner's choice. */
|
|
398
459
|
function setSharedEngine(engine) {
|
|
399
|
-
|
|
460
|
+
singletonStore[ENGINE_KEY] = engine;
|
|
400
461
|
}
|
|
401
462
|
function getSharedEngine() {
|
|
402
|
-
return
|
|
463
|
+
return singletonStore[ENGINE_KEY] || "v2";
|
|
403
464
|
}
|
|
404
|
-
/** Single
|
|
405
|
-
*
|
|
406
|
-
* module's own .d.ts is the authoritative surface. */
|
|
465
|
+
/** Single choke-point for every native extraction call; the NAPI module's
|
|
466
|
+
* own `.d.ts` is the authoritative surface for what it returns. */
|
|
407
467
|
function requireEngine() {
|
|
408
468
|
return __require("@animus-ui/extract");
|
|
409
469
|
}
|
|
410
|
-
const V2_ENGINE_KEY = "__animus_v2_engine__";
|
|
411
470
|
/**
|
|
412
|
-
*
|
|
413
|
-
*
|
|
414
|
-
* (hoisted to `createV2EngineApi` in @animus-ui/extract/pipeline — the
|
|
415
|
-
* single authoritative copy shared with vite-plugin). The engine INSTANCE,
|
|
416
|
-
* its analyze-time sources, and the one-shot drift flag all live on
|
|
417
|
-
* globalThis for the same reason the manifest does — the ESM plugin and the
|
|
418
|
-
* CJS webpack loader must see one engine (and one drift warning) across the
|
|
419
|
-
* double-load. (The next-plugin is already process-singleton by design:
|
|
420
|
-
* manifest, css, and system props share the same globalThis keys.)
|
|
421
|
-
* loadSystemModule is exported by both bindings from one engine-neutral
|
|
422
|
-
* Rust crate, so the default path no longer loads the v1 binary.
|
|
471
|
+
* The one engine API for this process: the instance, its analyze-time
|
|
472
|
+
* sources, and the drift flag live on globalThis so both copies share them.
|
|
423
473
|
*/
|
|
424
|
-
const V2_SENT_SOURCES_KEY = "__animus_v2_sent_sources__";
|
|
425
|
-
const V2_DRIFT_WARNED_KEY = "__animus_v2_drift_warned__";
|
|
426
474
|
const v2EngineApi = createV2EngineApi({
|
|
427
475
|
label: "animus",
|
|
428
476
|
isV2: () => getSharedEngine() === "v2",
|
|
429
477
|
loadNativeEngine: requireEngine,
|
|
430
478
|
passThroughUnknownPaths: true,
|
|
431
479
|
store: {
|
|
432
|
-
getEngine: () =>
|
|
480
|
+
getEngine: () => singletonStore[V2_ENGINE_KEY],
|
|
433
481
|
setEngine: (engine) => {
|
|
434
|
-
|
|
482
|
+
singletonStore[V2_ENGINE_KEY] = engine;
|
|
435
483
|
},
|
|
436
|
-
getSentSources: () =>
|
|
484
|
+
getSentSources: () => singletonStore[V2_SENT_SOURCES_KEY] ?? null,
|
|
437
485
|
setSentSources: (sources) => {
|
|
438
|
-
|
|
486
|
+
singletonStore[V2_SENT_SOURCES_KEY] = sources;
|
|
439
487
|
},
|
|
440
|
-
getDriftWarned: () => Boolean(
|
|
488
|
+
getDriftWarned: () => Boolean(singletonStore[V2_DRIFT_WARNED_KEY]),
|
|
441
489
|
setDriftWarned: (value) => {
|
|
442
|
-
|
|
490
|
+
singletonStore[V2_DRIFT_WARNED_KEY] = value;
|
|
443
491
|
}
|
|
444
492
|
}
|
|
445
493
|
});
|
|
446
|
-
const ENGINE_API_OVERRIDE_KEY = "__animus_engine_api_override__";
|
|
447
494
|
function engineApi() {
|
|
448
|
-
const override =
|
|
495
|
+
const override = singletonStore[ENGINE_API_OVERRIDE_KEY];
|
|
449
496
|
return override ? override() : v2EngineApi();
|
|
450
497
|
}
|
|
451
498
|
//#endregion
|
|
452
499
|
//#region session/timing.ts
|
|
453
|
-
/**
|
|
454
|
-
* Verbose build-timing waterfall: JS phases, analysis sub-phases, and the
|
|
455
|
-
* shared Rust phase table. `ANIMUS_TIMING_JSON=1` additionally dumps a
|
|
456
|
-
* machine-readable merged record.
|
|
457
|
-
*/
|
|
458
500
|
function logBuildTimings(bt, rustTiming, log, verbose) {
|
|
459
501
|
if (!verbose) return;
|
|
460
502
|
const pad = (label, width) => label + " ".repeat(Math.max(0, width - label.length));
|
|
@@ -485,28 +527,14 @@ function logBuildTimings(bt, rustTiming, log, verbose) {
|
|
|
485
527
|
if (process.env.ANIMUS_TIMING_JSON === "1") {
|
|
486
528
|
const merged = {};
|
|
487
529
|
for (const [k, v] of Object.entries(bt)) merged[`buildStart.${k}`] = v;
|
|
488
|
-
if (rustTiming) {
|
|
489
|
-
for (const [k, v] of Object.entries(rustTiming)) if (typeof v === "number") merged[`rust.${k}`] = v;
|
|
490
|
-
}
|
|
530
|
+
if (rustTiming) for (const [k, v] of Object.entries(rustTiming)) merged[`rust.${k}`] = v;
|
|
491
531
|
console.error(`[animus:timing] ${JSON.stringify(merged)}`);
|
|
492
532
|
}
|
|
493
533
|
}
|
|
494
534
|
//#endregion
|
|
495
535
|
//#region session/extraction-session.ts
|
|
496
|
-
|
|
497
|
-
* Module id the Rust emitter injects for the extracted stylesheet — also the
|
|
498
|
-
* exact resolve.alias key with-animus registers for it, which the adapter's
|
|
499
|
-
* alias harvesting must skip.
|
|
500
|
-
*/
|
|
501
|
-
const ANIMUS_CSS_MODULE_ID = ".animus/styles.css";
|
|
502
|
-
/** Retention window for sibling session directories (design D2). */
|
|
503
|
-
const SESSION_DIR_MAX_AGE_MS = 1440 * 60 * 1e3;
|
|
504
|
-
/** Watchdog added to the debounce ceiling for status deadlines (D3). */
|
|
536
|
+
const SESSION_DIR_MAX_AGE_MS = 864e5;
|
|
505
537
|
const STATUS_WATCHDOG_MS = 2e3;
|
|
506
|
-
/** Flat legacy `.animus/` artifacts removed at session start — unreachable
|
|
507
|
-
* by session-scoped loaders. The standalone CLI still publishes the three
|
|
508
|
-
* payload names flat; its verified/live sets are fenced off inside
|
|
509
|
-
* `runSessionStartHygiene`. */
|
|
510
538
|
const LEGACY_FLAT_ARTIFACTS = [
|
|
511
539
|
MANIFEST_ARTIFACT,
|
|
512
540
|
ANALYSIS_INPUTS_ARTIFACT,
|
|
@@ -516,22 +544,16 @@ const LEGACY_FLAT_ARTIFACTS = [
|
|
|
516
544
|
ANALYSIS_COMMIT_ARTIFACT,
|
|
517
545
|
ANALYSIS_STATUS_ARTIFACT
|
|
518
546
|
];
|
|
519
|
-
/**
|
|
520
|
-
*
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
*
|
|
529
|
-
* the bundler (Turbopack has no compiler-hook surface), reusing everything
|
|
530
|
-
* here unchanged.
|
|
531
|
-
*
|
|
532
|
-
* Outputs are published through the package singleton (shared CSS,
|
|
533
|
-
* manifest, system props) and written to `.animus/` on disk.
|
|
534
|
-
*/
|
|
547
|
+
/** Brands values read back off DISK. `Object(value) !== value` rejects
|
|
548
|
+
* boxed primitives; the tag is unspoofable by `Symbol.toStringTag`. */
|
|
549
|
+
function isDiskString(value) {
|
|
550
|
+
return Object(value) !== value && Object.prototype.toString.call(value) === "[object String]";
|
|
551
|
+
}
|
|
552
|
+
function isDiskNumber(value) {
|
|
553
|
+
return Object(value) !== value && Object.prototype.toString.call(value) === "[object Number]";
|
|
554
|
+
}
|
|
555
|
+
/** Bundler-agnostic extraction pipeline: system load, source ingestion,
|
|
556
|
+
* analysis, emission, and watch-cycle diffing. Carries no bundler types. */
|
|
535
557
|
var ExtractionSession = class {
|
|
536
558
|
/** Set by the adapter before any pipeline run. */
|
|
537
559
|
rootDir = null;
|
|
@@ -541,19 +563,13 @@ var ExtractionSession = class {
|
|
|
541
563
|
/** Physical asset files registered with the host watcher. */
|
|
542
564
|
assetDependencyPaths = /* @__PURE__ */ new Set();
|
|
543
565
|
assetDependencyKeys = /* @__PURE__ */ new Set();
|
|
544
|
-
/**
|
|
545
|
-
*
|
|
546
|
-
* NormalModuleReplacement; Turbopack rejects absolute-path imports, so
|
|
547
|
-
* its driver sets the virtual id that `resolveAlias` maps to disk. */
|
|
566
|
+
/** System-props module id override. Turbopack rejects absolute-path
|
|
567
|
+
* imports, so its driver sets a virtual id; null injects the abs path. */
|
|
548
568
|
systemPropsModuleId = null;
|
|
549
|
-
/** Whether the analysis-inputs hydration corpus is
|
|
550
|
-
*
|
|
551
|
-
* the manifest from memory — the corpus is never persisted. True is set
|
|
552
|
-
* by the Turbopack orchestration driver, whose isolated loader workers
|
|
553
|
-
* replay it (spec: next-turbopack-integration, "Manifest disk artifact" /
|
|
554
|
-
* "Webpack mode skips the hydration corpus"). */
|
|
569
|
+
/** Whether the analysis-inputs hydration corpus is written to disk. Only
|
|
570
|
+
* isolated loader workers replay it; an in-process loader reads memory. */
|
|
555
571
|
persistAnalysisInputs = false;
|
|
556
|
-
/** Absolute directory prefixes for external
|
|
572
|
+
/** Absolute directory prefixes for external packages (loader allowlist). */
|
|
557
573
|
externalPackageDirs = [];
|
|
558
574
|
/** Absolute package dir → owning specifier (cross-source correlation). */
|
|
559
575
|
externalDirOwners = {};
|
|
@@ -561,14 +577,11 @@ var ExtractionSession = class {
|
|
|
561
577
|
externalFileOwners = {};
|
|
562
578
|
/** External package specifier → absolute source entry path. */
|
|
563
579
|
externalSourceEntries = /* @__PURE__ */ new Map();
|
|
564
|
-
/** SourceId
|
|
565
|
-
*
|
|
566
|
-
* realpath'd once per full pipeline; alias→SourceId associations
|
|
567
|
-
* recorded while files exist so deletion resolves through the cache. */
|
|
580
|
+
/** SourceId authority for this generation — roots realpath'd once per
|
|
581
|
+
* pipeline; aliases recorded while files exist so deletions resolve. */
|
|
568
582
|
sourceIdentity = null;
|
|
569
|
-
/** Canonical admitted external root → declared specifiers
|
|
570
|
-
*
|
|
571
|
-
* root share it). Engine watch wiring and tests read this. */
|
|
583
|
+
/** Canonical admitted external root → declared specifiers; duplicate
|
|
584
|
+
* specifiers resolving to one canonical root share an entry. */
|
|
572
585
|
externalRootOwners = /* @__PURE__ */ new Map();
|
|
573
586
|
/** Canonical external root → the extension set its collection walk used
|
|
574
587
|
* (widened for dist-only roots); rewalks and classification share it. */
|
|
@@ -576,90 +589,81 @@ var ExtractionSession = class {
|
|
|
576
589
|
/** Canonical admitted external watch roots — the engine-side watch
|
|
577
590
|
* surface (webpack contextDependencies / Turbopack watchers). */
|
|
578
591
|
externalWatchRoots = [];
|
|
579
|
-
/** Canonical external root →
|
|
580
|
-
*
|
|
581
|
-
* diffs against (design D3). Raw hashes — MDX preprocessing never skews
|
|
582
|
-
* the diff; the recorded abs path is a registered deletion alias, so a
|
|
583
|
-
* reconstructed deletion always resolves through cached identity. */
|
|
592
|
+
/** Canonical external root → sourceKey → {raw content hash, recorded abs}:
|
|
593
|
+
* the inventory a dirty-root rewalk diffs against, hashed before MDX. */
|
|
584
594
|
externalInventory = /* @__PURE__ */ new Map();
|
|
585
|
-
/**
|
|
586
|
-
*
|
|
595
|
+
/** Stable key → message, re-surfaced on every full pipeline until the
|
|
596
|
+
* underlying condition clears. */
|
|
587
597
|
stickyDiagnostics = /* @__PURE__ */ new Map();
|
|
588
|
-
/** Volume-membership predicate for the cross-volume gate
|
|
589
|
-
*
|
|
590
|
-
* (win32 semantics are unit-tested via path.win32), so runtime tests
|
|
591
|
-
* drive the gate through this. */
|
|
598
|
+
/** Volume-membership predicate for the cross-volume gate. Injected seam:
|
|
599
|
+
* the real predicate only discriminates on Windows, so tests replace it. */
|
|
592
600
|
sharesProjectVolume = sharesVolumeRoot;
|
|
593
|
-
/**
|
|
594
|
-
*
|
|
595
|
-
* DURING collection — after resolution, BEFORE that root's sources are
|
|
596
|
-
* walked — so a watching host can open its handle with no blind gap
|
|
597
|
-
* between scan and watch. `onExternalRootsCommitted` fires once after a
|
|
598
|
-
* successful full-pipeline publication with the complete admitted
|
|
599
|
-
* canonical root set (the host's promote/replay/close-old phase; a
|
|
600
|
-
* failed pipeline never fires it, so the host rolls back). */
|
|
601
|
+
/** `onExternalRootResolved` fires per admitted root DURING collection,
|
|
602
|
+
* before its walk; `onExternalRootsCommitted` only after publication. */
|
|
601
603
|
onExternalRootResolved = null;
|
|
602
604
|
onExternalRootsCommitted = null;
|
|
605
|
+
/** Fires exactly once per settled `handleWatchUpdate` — the failure cause,
|
|
606
|
+
* or null when clean. Observation never alters delivery. */
|
|
607
|
+
onCycleSettled = null;
|
|
603
608
|
options;
|
|
604
609
|
staticCssJson;
|
|
605
610
|
system = null;
|
|
606
|
-
/**
|
|
607
|
-
|
|
611
|
+
/** Vocabulary witness diagnostics from the sealed system's registration
|
|
612
|
+
* record, awaiting the shared surfacing pass. */
|
|
613
|
+
systemVocabularyDiagnostics = [];
|
|
614
|
+
/** Configured external files that could not be read. Incremental passes
|
|
615
|
+
* re-collect nothing, so they replay these instead of passing strict. */
|
|
616
|
+
ingestionFailureDiagnostics = [];
|
|
608
617
|
/** Full package-resolution map from the last full pipeline — replayed by
|
|
609
618
|
* incremental passes (sourceEntries alone omits dist-resolved packages). */
|
|
610
619
|
lastPackageMap = {};
|
|
611
620
|
fileCache = /* @__PURE__ */ new Map();
|
|
612
|
-
|
|
613
|
-
|
|
621
|
+
corpus = createSourceCorpus({
|
|
622
|
+
engineApi: () => engineApi(),
|
|
623
|
+
prefix: "[animus-next]",
|
|
624
|
+
strict: () => !!this.options.strict,
|
|
625
|
+
warn: (message) => this.warn(message)
|
|
626
|
+
});
|
|
614
627
|
systemDependencyKeys = /* @__PURE__ */ new Set();
|
|
615
628
|
/** Loader-reported dependency paths for bundler watch registration. */
|
|
616
629
|
systemDependencyPaths = [];
|
|
617
630
|
systemDependencyHashes = /* @__PURE__ */ new Map();
|
|
618
631
|
lastSystemPropsHash = null;
|
|
619
|
-
/** Per-payload write guards: payloadHash gates rewrites (byte-identical
|
|
620
|
-
* payloads leave disk untouched); diskHash is the hash of the CURRENT
|
|
621
|
-
* disk bytes (envelope included) recorded in the analysis-commit. null =
|
|
622
|
-
* not yet seeded (first publication reads the artifact's envelope so a
|
|
623
|
-
* same-session restart never rewrites byte-identical artifacts). */
|
|
624
632
|
artifactRecords = {
|
|
625
633
|
manifest: null,
|
|
626
634
|
inputs: null,
|
|
627
635
|
styles: null
|
|
628
636
|
};
|
|
629
|
-
/** Last written analysis-commit; null
|
|
637
|
+
/** Last written analysis-commit; null seeds from disk on first use. Its
|
|
638
|
+
* generation counter is correct only because one session owns the dir. */
|
|
630
639
|
lastCommit = null;
|
|
631
|
-
/**
|
|
632
|
-
*
|
|
633
|
-
|
|
634
|
-
|
|
640
|
+
/** Release handle for the in-process owner slot AND the directory's
|
|
641
|
+
* on-disk owner record, taken and released as one; null when unheld. */
|
|
642
|
+
releasePublicationClaim = null;
|
|
643
|
+
/** Session identity, claimed once per PROCESS and adopted by every later
|
|
644
|
+
* session instance so all compilers share one artifact tree. */
|
|
635
645
|
sessionId = claimProcessSessionId();
|
|
636
|
-
/** Epoch
|
|
637
|
-
*
|
|
638
|
-
* same-session restart with unchanged plans never rewrites bytes). */
|
|
646
|
+
/** Epoch of the last published analysis; null seeds from disk so a
|
|
647
|
+
* same-session restart with unchanged plans rewrites no bytes. */
|
|
639
648
|
lastEpochValue = null;
|
|
640
|
-
/** Watcher debounce ceiling feeding status deadlines
|
|
649
|
+
/** Watcher debounce ceiling feeding status deadlines. */
|
|
641
650
|
debounceCeilingMs = 75;
|
|
642
651
|
/** Test seam: observes every session-artifact write (name, content)
|
|
643
652
|
* post-rename — write ORDER is part of the transaction contract. */
|
|
644
653
|
onArtifactWrite = null;
|
|
645
|
-
/** Status attempt bookkeeping (design D3). */
|
|
646
654
|
statusAttemptId = 0;
|
|
647
655
|
statusAttemptOpen = false;
|
|
648
|
-
/** Monotonic
|
|
649
|
-
*
|
|
650
|
-
* succeeds and never regresses — every later status write (idle AND
|
|
651
|
-
* failed) carries it, so readiness stays observable across attempts. */
|
|
656
|
+
/** Monotonic readiness witness: flips true on the first complete
|
|
657
|
+
* publication and never regresses; every later status write carries it. */
|
|
652
658
|
firstEmissionComplete = false;
|
|
653
659
|
/** Debounce-window observations pending analysis (sourceKey → hash). */
|
|
654
660
|
debouncePending = /* @__PURE__ */ new Map();
|
|
655
|
-
/** Session-start hygiene (pruning + legacy cleanup) runs once. */
|
|
656
661
|
sessionStartHygieneDone = false;
|
|
657
662
|
lcssTargets = null;
|
|
658
663
|
constructor(options) {
|
|
659
664
|
this.options = options;
|
|
660
665
|
this.staticCssJson = serializeStaticCss(options.staticCss);
|
|
661
666
|
}
|
|
662
|
-
/** This session's artifact directory. Requires `rootDir` to be set. */
|
|
663
667
|
get sessionDir() {
|
|
664
668
|
return sessionArtifactDir(this.rootDir, this.sessionId);
|
|
665
669
|
}
|
|
@@ -678,24 +682,20 @@ var ExtractionSession = class {
|
|
|
678
682
|
elapsed(t) {
|
|
679
683
|
return this.verbose ? Math.round(performance.now() - t) : 0;
|
|
680
684
|
}
|
|
681
|
-
/**
|
|
682
|
-
*
|
|
683
|
-
/** Diagnostic prefix label — drivers set their own so a host's errors
|
|
684
|
-
* never masquerade as another driver's (inc 05 review S1). The default
|
|
685
|
-
* keeps the Next plugin's historical prefix. */
|
|
685
|
+
/** Diagnostic prefix label — drivers set their own so one host's errors
|
|
686
|
+
* never masquerade as another driver's. */
|
|
686
687
|
driverLabel = "animus-next";
|
|
687
688
|
/** Per-specifier discovery outcomes from the last full collection —
|
|
688
689
|
* driver-consumed reporting surface (the CLI's summary). */
|
|
689
690
|
lastExternalOutcomes = [];
|
|
690
|
-
/** Component count of the last published analysis,
|
|
691
|
-
*
|
|
692
|
-
* manifest JSON every cycle. */
|
|
691
|
+
/** Component count of the last published analysis, null before the first —
|
|
692
|
+
* read by drivers instead of re-parsing the manifest JSON every cycle. */
|
|
693
693
|
lastComponentCount = null;
|
|
694
|
-
/**
|
|
695
|
-
*
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
*
|
|
694
|
+
/** When superseded asset copies leave `assets/`: `'full-pipeline'` suits a
|
|
695
|
+
* driver serving the dir in place, `'every-cycle'` one republishing it. */
|
|
696
|
+
staleAssetPruning = "full-pipeline";
|
|
697
|
+
/** Driver-owned STRUCTURAL exclusions, joined to the never-replaceable set
|
|
698
|
+
* — never the user list, whose presence flips the replace semantics. */
|
|
699
699
|
structuralExclude = [];
|
|
700
700
|
scanConfigMemo = null;
|
|
701
701
|
/** Per-pattern exclusion hit counts from this session's matcher —
|
|
@@ -715,17 +715,18 @@ var ExtractionSession = class {
|
|
|
715
715
|
}
|
|
716
716
|
return this.scanConfigMemo;
|
|
717
717
|
}
|
|
718
|
-
/**
|
|
719
|
-
*
|
|
720
|
-
* batch (openspec: next-webpack-served-transform-coherence, design D3).
|
|
721
|
-
* The first entering session runs the transaction; every concurrent
|
|
722
|
-
* entry — same session re-entered, or another compiler's session
|
|
723
|
-
* (client/server/RSC each hold their own instance) — joins the in-flight
|
|
724
|
-
* promise, so no compiler proceeds against a generation older than the
|
|
725
|
-
* one the transaction publishes. A rejected transaction rejects every
|
|
726
|
-
* joiner; the gate always clears.
|
|
727
|
-
*/
|
|
718
|
+
/** One single-flight analysis transaction per event batch: concurrent
|
|
719
|
+
* entries join the in-flight promise; each settled entry reports outcome. */
|
|
728
720
|
async handleWatchUpdate(changes) {
|
|
721
|
+
try {
|
|
722
|
+
await this.routeWatchUpdate(changes);
|
|
723
|
+
} catch (error) {
|
|
724
|
+
this.onCycleSettled?.(error);
|
|
725
|
+
throw error;
|
|
726
|
+
}
|
|
727
|
+
this.onCycleSettled?.(null);
|
|
728
|
+
}
|
|
729
|
+
async routeWatchUpdate(changes) {
|
|
729
730
|
const inflight = getWatchTransaction();
|
|
730
731
|
if (inflight) {
|
|
731
732
|
await inflight;
|
|
@@ -745,13 +746,8 @@ var ExtractionSession = class {
|
|
|
745
746
|
setWatchTransaction(null);
|
|
746
747
|
}
|
|
747
748
|
}
|
|
748
|
-
/**
|
|
749
|
-
*
|
|
750
|
-
* watcher. Serializes behind any in-flight transaction, then runs the
|
|
751
|
-
* forwarded batch as its own transaction — never drops it (a forwarded
|
|
752
|
-
* batch has no other delivery; content-hash diffing makes an
|
|
753
|
-
* already-covered batch a cheap no-op).
|
|
754
|
-
*/
|
|
749
|
+
/** Owner-side entry for a batch a non-owning compiler observed. Serializes
|
|
750
|
+
* behind any in-flight transaction — it has no other delivery. */
|
|
755
751
|
async ingestForwardedBatch(changes) {
|
|
756
752
|
if (!this.system) return;
|
|
757
753
|
for (;;) {
|
|
@@ -767,11 +763,6 @@ var ExtractionSession = class {
|
|
|
767
763
|
setWatchTransaction(null);
|
|
768
764
|
}
|
|
769
765
|
}
|
|
770
|
-
/**
|
|
771
|
-
* Incremental watch pass: geological reset when the system file changed,
|
|
772
|
-
* otherwise content-hash diffing restricted to the watcher's change sets
|
|
773
|
-
* (falling back to a full discovery walk when no sets are provided).
|
|
774
|
-
*/
|
|
775
766
|
async processWatchUpdate(changes) {
|
|
776
767
|
const rootDir = this.rootDir;
|
|
777
768
|
try {
|
|
@@ -795,14 +786,14 @@ var ExtractionSession = class {
|
|
|
795
786
|
}
|
|
796
787
|
else systemHit = changed.find((path) => toWatchKeys(path).some((key) => this.systemDependencyKeys.has(key)));
|
|
797
788
|
} catch (err) {
|
|
798
|
-
this.warn(`HMR
|
|
789
|
+
this.warn(`HMR system-reload check failed: ${String(err)}`);
|
|
799
790
|
}
|
|
800
791
|
if (systemHit) {
|
|
801
|
-
this.log(`
|
|
792
|
+
this.log(`system reload: system dependency changed (${relative(rootDir, systemHit)})`);
|
|
802
793
|
this.resetForHmr();
|
|
803
794
|
try {
|
|
804
795
|
const promise = this.runFullPipeline(this.pendingFromBatch(changes));
|
|
805
|
-
|
|
796
|
+
setAnalysisStartedPromise(promise);
|
|
806
797
|
await promise;
|
|
807
798
|
} catch (err) {
|
|
808
799
|
try {
|
|
@@ -906,11 +897,14 @@ var ExtractionSession = class {
|
|
|
906
897
|
let ingested;
|
|
907
898
|
try {
|
|
908
899
|
this.beginStatusAttempt();
|
|
909
|
-
ingested = await this.
|
|
910
|
-
|
|
911
|
-
|
|
900
|
+
ingested = await this.corpus.prepare({
|
|
901
|
+
fileCache: this.fileCache,
|
|
902
|
+
externalFileOwners: this.externalFileOwners
|
|
903
|
+
});
|
|
904
|
+
this.externalFileOwners = projectExternalFileOwners(ingested, this.externalFileOwners);
|
|
905
|
+
resetAnalysisStartedPromise();
|
|
912
906
|
const promise = this.runIncrementalPipeline(ingested.analysisEntries, pending);
|
|
913
|
-
|
|
907
|
+
setAnalysisStartedPromise(promise);
|
|
914
908
|
await promise;
|
|
915
909
|
} catch (err) {
|
|
916
910
|
for (const [key, prior] of priorCacheEntries) if (prior === null) this.fileCache.delete(key);
|
|
@@ -941,60 +935,39 @@ var ExtractionSession = class {
|
|
|
941
935
|
this.writeAnalysisStatus("idle", []);
|
|
942
936
|
}
|
|
943
937
|
}
|
|
944
|
-
/** The shared ingestion policy point (vite-plugin parity BY CODE): the
|
|
945
|
-
* capability guard, facts memo, and warn-dedupe lifecycle live in the
|
|
946
|
-
* pipeline; this host holds only its prefix, strict flag, and warn sink. */
|
|
947
|
-
sourceIngestor = createSourceIngestor({
|
|
948
|
-
engineApi: () => engineApi(),
|
|
949
|
-
prefix: "[animus-next]",
|
|
950
|
-
strict: () => !!this.options.strict,
|
|
951
|
-
warn: (message) => this.warn(message)
|
|
952
|
-
});
|
|
953
|
-
/** Prepare one raw-source corpus through the shared adaptation boundary. */
|
|
954
|
-
async ingestRawSources(entries) {
|
|
955
|
-
return this.sourceIngestor.ingest(entries);
|
|
956
|
-
}
|
|
957
|
-
/** Ingest and apply the shared per-file quarantine: one invalid original
|
|
958
|
-
* (an `.mdx` with the optional peer absent, an unsupported `.svelte`
|
|
959
|
-
* shape) warns and drops; the rest of the corpus still analyzes. Strict
|
|
960
|
-
* mode throws from the shared policy — callers route that into their own
|
|
961
|
-
* status/rollback handling. */
|
|
962
|
-
async ingestAccepted(entries) {
|
|
963
|
-
const ingested = await this.ingestRawSources(entries ?? this.buildRawEntriesFromCache());
|
|
964
|
-
return withoutInvalidOriginals(ingested, this.surfaceSourceDiagnostics(ingested.diagnostics));
|
|
965
|
-
}
|
|
966
|
-
/** Surface parser diagnostics under the shared strict/warn policy. */
|
|
967
|
-
surfaceSourceDiagnostics(diagnostics) {
|
|
968
|
-
return this.sourceIngestor.surfaceDiagnostics(diagnostics);
|
|
969
|
-
}
|
|
970
|
-
/**
|
|
971
|
-
* Correlate diagnostics emitted against generated children back to the
|
|
972
|
-
* external package owning their raw original.
|
|
973
|
-
*/
|
|
974
|
-
projectExternalFileOwners(result, rawOwners) {
|
|
975
|
-
const projected = {};
|
|
976
|
-
for (const owner of Object.values(result.ownership)) {
|
|
977
|
-
const packageOwner = rawOwners[owner.originalPath];
|
|
978
|
-
if (!packageOwner) continue;
|
|
979
|
-
projected[owner.originalPath] = packageOwner;
|
|
980
|
-
for (const analysisPath of owner.analysisPaths) projected[analysisPath] = packageOwner;
|
|
981
|
-
}
|
|
982
|
-
return projected;
|
|
983
|
-
}
|
|
984
|
-
/** Publish the raw cache and complete parser projection atomically. */
|
|
985
938
|
publishSourceIngestion(result) {
|
|
986
|
-
this.sourceIngestor.markPublished(result);
|
|
987
939
|
this.fileCache = new Map(result.originalEntries.map((entry) => [entry.path, {
|
|
988
940
|
hash: entry.hash,
|
|
989
941
|
source: entry.source
|
|
990
942
|
}]));
|
|
991
|
-
this.
|
|
992
|
-
hash: entry.hash,
|
|
993
|
-
source: entry.source
|
|
994
|
-
}]));
|
|
995
|
-
this.sourceOwnership = result.ownership;
|
|
943
|
+
this.corpus.publish(result);
|
|
996
944
|
}
|
|
945
|
+
/** Full analysis + publication, registered in the one in-flight
|
|
946
|
+
* transaction slot; a nested reload leaves the enclosing registration. */
|
|
997
947
|
async runFullPipeline(pending = []) {
|
|
948
|
+
if (this.releasePublicationClaim === null) {
|
|
949
|
+
const releaseOwner = claimExclusiveSessionOwner(`${this.driverLabel}:${this.sessionDir}`);
|
|
950
|
+
mkdirSync(this.sessionDir, { recursive: true });
|
|
951
|
+
const releaseClaimRecord = holdDirectoryClaim(this.sessionDir);
|
|
952
|
+
this.releasePublicationClaim = () => {
|
|
953
|
+
releaseOwner();
|
|
954
|
+
releaseClaimRecord();
|
|
955
|
+
};
|
|
956
|
+
}
|
|
957
|
+
const priorSystem = this.system;
|
|
958
|
+
const nested = getWatchTransaction() !== null;
|
|
959
|
+
const transaction = this.runPipelineTransaction(pending);
|
|
960
|
+
if (!nested) setWatchTransaction(transaction);
|
|
961
|
+
try {
|
|
962
|
+
await transaction;
|
|
963
|
+
} catch (error) {
|
|
964
|
+
this.system = priorSystem;
|
|
965
|
+
throw error;
|
|
966
|
+
} finally {
|
|
967
|
+
if (!nested) setWatchTransaction(null);
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
async runPipelineTransaction(pending) {
|
|
998
971
|
const pipelineStart = this.now();
|
|
999
972
|
const bt = {};
|
|
1000
973
|
this.runSessionStartHygiene();
|
|
@@ -1008,6 +981,7 @@ var ExtractionSession = class {
|
|
|
1008
981
|
rootDir,
|
|
1009
982
|
prefix: this.options.prefix
|
|
1010
983
|
});
|
|
984
|
+
this.systemVocabularyDiagnostics = vocabularyWitnessDiagnostics(this.system.vocabularyWitnessesJson);
|
|
1011
985
|
this.assetCopyCache.clear();
|
|
1012
986
|
this.assetDependencyPaths.clear();
|
|
1013
987
|
this.assetDependencyKeys.clear();
|
|
@@ -1048,6 +1022,7 @@ var ExtractionSession = class {
|
|
|
1048
1022
|
const packageNames = extractSystemFilePackages(resolvedSystemPath);
|
|
1049
1023
|
const preResolved = resolvePackagesByName(rootDir, packageNames);
|
|
1050
1024
|
const rawExternalFiles = /* @__PURE__ */ new Map();
|
|
1025
|
+
const ingestionFailures = [];
|
|
1051
1026
|
const collected = await collectExternalPackageSources({
|
|
1052
1027
|
specifiers: packageNames,
|
|
1053
1028
|
resolveSpecifier: (name) => preResolved[name] ? resolve(rootDir, preResolved[name]) : null,
|
|
@@ -1057,7 +1032,7 @@ var ExtractionSession = class {
|
|
|
1057
1032
|
onSourceRead: (source, _relPath, absPath) => {
|
|
1058
1033
|
rawExternalFiles.set(absPath, contentHash(source));
|
|
1059
1034
|
},
|
|
1060
|
-
onUnreadable: (relPath, err) =>
|
|
1035
|
+
onUnreadable: (relPath, err) => ingestionFailures.push(unreadableSourceDiagnostic(relPath, err)),
|
|
1061
1036
|
onPackageResolved: (_specifier, packageDir) => {
|
|
1062
1037
|
if (!this.onExternalRootResolved) return;
|
|
1063
1038
|
if (!this.sharesProjectVolume(rootDir, packageDir)) return;
|
|
@@ -1069,6 +1044,7 @@ var ExtractionSession = class {
|
|
|
1069
1044
|
}
|
|
1070
1045
|
});
|
|
1071
1046
|
this.lastExternalOutcomes = collected.outcomes;
|
|
1047
|
+
this.ingestionFailureDiagnostics = ingestionFailures;
|
|
1072
1048
|
for (const record of collected.outcomes) if (record.outcome === "empty") this.warn(`include '${record.specifier}' resolved but discovered no component sources`);
|
|
1073
1049
|
const unresolvableMessage = unresolvableIncludesMessage(collected.outcomes);
|
|
1074
1050
|
if (unresolvableMessage !== null) {
|
|
@@ -1137,23 +1113,17 @@ var ExtractionSession = class {
|
|
|
1137
1113
|
});
|
|
1138
1114
|
}
|
|
1139
1115
|
this.externalPackageDirs = admitted.packageDirs;
|
|
1140
|
-
if (this.system && admitted.keyframesScanEntries.size > 0) {
|
|
1141
|
-
const api = engineApi();
|
|
1142
|
-
const merge = mergeExternalKeyframes((entry, root) => api.scanKeyframesExports(entry, root), this.system.keyframesJson, admitted.keyframesScanEntries.values(), this.rootDir);
|
|
1143
|
-
this.system.keyframesJson = merge.keyframesJson;
|
|
1144
|
-
this.externalKeyframesDiagnostics = merge.diagnostics;
|
|
1145
|
-
} else this.externalKeyframesDiagnostics = [];
|
|
1146
1116
|
bt.packageResolve = this.elapsed(t);
|
|
1147
1117
|
this.beginStatusAttempt();
|
|
1148
1118
|
let accepted;
|
|
1149
1119
|
try {
|
|
1150
|
-
accepted = await this.
|
|
1120
|
+
accepted = await this.corpus.prepare(rawEntries);
|
|
1151
1121
|
} catch (err) {
|
|
1152
1122
|
this.debouncePending.clear();
|
|
1153
1123
|
this.writeAnalysisStatus("failed", pending, String(err));
|
|
1154
1124
|
throw err;
|
|
1155
1125
|
}
|
|
1156
|
-
this.externalFileOwners =
|
|
1126
|
+
this.externalFileOwners = projectExternalFileOwners(accepted, this.externalFileOwners);
|
|
1157
1127
|
bt.fileCount = accepted.analysisEntries.length;
|
|
1158
1128
|
clearEngineCache(engineApi);
|
|
1159
1129
|
await this.analyzeAndEmit(accepted.analysisEntries, packageMap, false, bt, pipelineStart, pending);
|
|
@@ -1167,10 +1137,8 @@ var ExtractionSession = class {
|
|
|
1167
1137
|
throw err;
|
|
1168
1138
|
}
|
|
1169
1139
|
}
|
|
1170
|
-
/**
|
|
1171
|
-
*
|
|
1172
|
-
* deep-copied fields are the ones later steps mutate in place rather
|
|
1173
|
-
* than reassign; everything else is rebound wholesale on success. */
|
|
1140
|
+
/** Per-generation source state captured as ONE object so the restore
|
|
1141
|
+
* cannot omit a member; deep-copied fields are mutated in place. */
|
|
1174
1142
|
snapshotSourceState() {
|
|
1175
1143
|
return {
|
|
1176
1144
|
sourceIdentity: this.sourceIdentity,
|
|
@@ -1183,20 +1151,23 @@ var ExtractionSession = class {
|
|
|
1183
1151
|
externalFileOwners: this.externalFileOwners,
|
|
1184
1152
|
externalSourceEntries: this.externalSourceEntries,
|
|
1185
1153
|
externalPackageDirs: this.externalPackageDirs,
|
|
1186
|
-
|
|
1154
|
+
systemVocabularyDiagnostics: this.systemVocabularyDiagnostics,
|
|
1155
|
+
ingestionFailureDiagnostics: this.ingestionFailureDiagnostics
|
|
1187
1156
|
};
|
|
1188
1157
|
}
|
|
1189
1158
|
restoreSourceState(snapshot) {
|
|
1190
1159
|
Object.assign(this, snapshot);
|
|
1191
1160
|
}
|
|
1192
|
-
/**
|
|
1193
|
-
*
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1161
|
+
/** Release this session's publication claim so a successor may publish.
|
|
1162
|
+
* Idempotent, and scoped to this session's own claim. */
|
|
1163
|
+
close() {
|
|
1164
|
+
this.releasePublicationClaim?.();
|
|
1165
|
+
this.releasePublicationClaim = null;
|
|
1166
|
+
}
|
|
1167
|
+
/** Reset analysis state for a system reload: write guards go back to null
|
|
1168
|
+
* and reseed from the disk envelopes on the next publication. */
|
|
1198
1169
|
resetForHmr() {
|
|
1199
|
-
|
|
1170
|
+
resetAnalysisStartedPromise();
|
|
1200
1171
|
this.artifactRecords = {
|
|
1201
1172
|
manifest: null,
|
|
1202
1173
|
inputs: null,
|
|
@@ -1204,12 +1175,8 @@ var ExtractionSession = class {
|
|
|
1204
1175
|
};
|
|
1205
1176
|
this.lastSystemPropsHash = null;
|
|
1206
1177
|
}
|
|
1207
|
-
/**
|
|
1208
|
-
*
|
|
1209
|
-
* evidence for the status file's pending set on the geological-reset
|
|
1210
|
-
* path, where the batch's component edits ride along with the system
|
|
1211
|
-
* edit (design D3: loaders wait only on observed inputs).
|
|
1212
|
-
*/
|
|
1178
|
+
/** The (sourceKey, observed hash) pairs of a watch batch — the status
|
|
1179
|
+
* file's pending set; loaders wait only on observed inputs. */
|
|
1213
1180
|
pendingFromBatch(changes) {
|
|
1214
1181
|
const { excludeMatcher, extensionsSet } = this.resolveScanConfig();
|
|
1215
1182
|
const pending = [];
|
|
@@ -1225,17 +1192,8 @@ var ExtractionSession = class {
|
|
|
1225
1192
|
}
|
|
1226
1193
|
return pending;
|
|
1227
1194
|
}
|
|
1228
|
-
/**
|
|
1229
|
-
*
|
|
1230
|
-
* watcher's change sets into explicit FILE events and external ROOT hits
|
|
1231
|
-
* (a reported path that IS an admitted root, or a directory inside one),
|
|
1232
|
-
* rewalk each dirty root through the shared discovery policy, diff the
|
|
1233
|
-
* SourceId+raw-hash inventory against the previous generation, and merge
|
|
1234
|
-
* the reconstructed created/edited/deleted deltas with the explicit
|
|
1235
|
-
* events (identity-level dedup happens downstream at the cache gate).
|
|
1236
|
-
* Deletion falls out of the inventory diff — webpack may report only the
|
|
1237
|
-
* directory (probe-proven).
|
|
1238
|
-
*/
|
|
1195
|
+
/** Rewalk dirty external roots and diff the inventory into created,
|
|
1196
|
+
* edited and deleted deltas: a watcher may report only the directory. */
|
|
1239
1197
|
reconcileExternalRoots(changes) {
|
|
1240
1198
|
const identity = this.sourceIdentity;
|
|
1241
1199
|
if (!identity) return changes;
|
|
@@ -1288,14 +1246,8 @@ var ExtractionSession = class {
|
|
|
1288
1246
|
removedFiles: removed
|
|
1289
1247
|
};
|
|
1290
1248
|
}
|
|
1291
|
-
/**
|
|
1292
|
-
*
|
|
1293
|
-
* (lexical containment — existing local semantics preserved; consumer
|
|
1294
|
-
* exclude patterns apply) or an admitted external member (identity
|
|
1295
|
-
* resolution with symlink-escape rejection; package-relative excludes
|
|
1296
|
-
* mirror the collection walk's filters — guardrail G1, one policy).
|
|
1297
|
-
* Returns null for dropped paths.
|
|
1298
|
-
*/
|
|
1249
|
+
/** Route one watcher path: a project-root member (consumer excludes apply)
|
|
1250
|
+
* or an admitted external member (symlink escapes rejected). */
|
|
1299
1251
|
classifyWatchPath(absPath, scan) {
|
|
1300
1252
|
const ext = extname(absPath);
|
|
1301
1253
|
const rootDir = this.rootDir;
|
|
@@ -1325,51 +1277,13 @@ var ExtractionSession = class {
|
|
|
1325
1277
|
owningRoot: resolved.owningRoot
|
|
1326
1278
|
};
|
|
1327
1279
|
}
|
|
1328
|
-
/** Build full raw originals for one shared adaptation attempt. */
|
|
1329
|
-
buildRawEntriesFromCache() {
|
|
1330
|
-
const entries = [];
|
|
1331
|
-
for (const [path, { hash, source }] of this.fileCache) entries.push({
|
|
1332
|
-
path,
|
|
1333
|
-
source,
|
|
1334
|
-
hash
|
|
1335
|
-
});
|
|
1336
|
-
return entries;
|
|
1337
|
-
}
|
|
1338
|
-
/**
|
|
1339
|
-
* Run incremental pipeline with cache-aware file entries.
|
|
1340
|
-
* Reuses system config from the last full pipeline run.
|
|
1341
|
-
*/
|
|
1342
1280
|
async runIncrementalPipeline(fileEntries, pending = []) {
|
|
1343
1281
|
const bt = {};
|
|
1344
1282
|
const pipelineStart = this.now();
|
|
1345
1283
|
await this.analyzeAndEmit(fileEntries, this.lastPackageMap, true, bt, pipelineStart, pending);
|
|
1346
1284
|
}
|
|
1347
|
-
/**
|
|
1348
|
-
*
|
|
1349
|
-
* that routes every manifest through the shared `runProjectAnalysis`,
|
|
1350
|
-
* reachable from both runFullPipeline (production) and
|
|
1351
|
-
* runIncrementalPipeline (HMR).
|
|
1352
|
-
*
|
|
1353
|
-
* Owns diagnostic surfacing, CSS assembly + styles.css write guard,
|
|
1354
|
-
* system-props module emit, and the timing log. The engine's own
|
|
1355
|
-
* `dev_mode` (reconciliation pruning) is derived separately via
|
|
1356
|
-
* `engineDevMode` — an explicit `mode` option overrides the pipeline
|
|
1357
|
-
* path there. For everything else the `devMode` flag is the ONLY
|
|
1358
|
-
* behavioral fork:
|
|
1359
|
-
*
|
|
1360
|
-
* - `false` (production): computes bt.analysis + logs the extraction
|
|
1361
|
-
* report, and writes system-props.js UNCONDITIONALLY (no
|
|
1362
|
-
* lastSystemPropsHash guard).
|
|
1363
|
-
* - `true` (HMR): skips the report log, and guards the system-props.js
|
|
1364
|
-
* write by lastSystemPropsHash.
|
|
1365
|
-
*
|
|
1366
|
-
* The disk artifacts land under the session directory in transaction
|
|
1367
|
-
* order (design D1): manifest → analysis-inputs (Turbopack orchestration
|
|
1368
|
-
* only) → styles.css → system-props → analysis-commit →
|
|
1369
|
-
* replacements-epoch (last, only when moved). The analysis-status file walks starting → analyzing →
|
|
1370
|
-
* committing → idle around the attempt, landing in `failed` (with the
|
|
1371
|
-
* diagnostic) on any throw (design D3).
|
|
1372
|
-
*/
|
|
1285
|
+
/** Shared analysis + emit core for both pipelines. `devMode` forks only
|
|
1286
|
+
* the report log and the system-props write guard. */
|
|
1373
1287
|
async analyzeAndEmit(fileEntries, packageMap, devMode, bt, pipelineStart, pending = []) {
|
|
1374
1288
|
this.beginStatusAttempt();
|
|
1375
1289
|
this.debouncePending.clear();
|
|
@@ -1383,16 +1297,8 @@ var ExtractionSession = class {
|
|
|
1383
1297
|
throw err;
|
|
1384
1298
|
}
|
|
1385
1299
|
}
|
|
1386
|
-
/**
|
|
1387
|
-
*
|
|
1388
|
-
* pruning) is an EMISSION decision, so an explicit `mode` option wins over
|
|
1389
|
-
* the pipeline path (core-options: `mode` "decides emitted bytes …
|
|
1390
|
-
* engine devMode"). Without an explicit mode the historical per-pipeline
|
|
1391
|
-
* default applies: full = production pruning, incremental = dev retention.
|
|
1392
|
-
* A pinned-production watch must not flip to unpruned CSS on its first
|
|
1393
|
-
* incremental republication, and a pinned-development full build must
|
|
1394
|
-
* retain all components.
|
|
1395
|
-
*/
|
|
1300
|
+
/** An explicit `mode` wins over the pipeline path: a pinned-production
|
|
1301
|
+
* watch must not emit unpruned CSS on its first incremental republish. */
|
|
1396
1302
|
engineDevMode(pipelineDefault) {
|
|
1397
1303
|
if (this.options.mode !== void 0) return this.options.mode === "development";
|
|
1398
1304
|
return pipelineDefault;
|
|
@@ -1418,7 +1324,7 @@ var ExtractionSession = class {
|
|
|
1418
1324
|
...analysisOptions,
|
|
1419
1325
|
warn: (message) => this.warn(message),
|
|
1420
1326
|
strict: this.options.strict,
|
|
1421
|
-
extraDiagnostics: this.
|
|
1327
|
+
extraDiagnostics: [...this.systemVocabularyDiagnostics, ...this.ingestionFailureDiagnostics]
|
|
1422
1328
|
});
|
|
1423
1329
|
assertNoErrorDiagnostics(result.manifest?.diagnostics);
|
|
1424
1330
|
enforceExternalTokenContracts({
|
|
@@ -1439,7 +1345,7 @@ var ExtractionSession = class {
|
|
|
1439
1345
|
bt.analysis = (bt.jsonSerialize ?? 0) + (bt.rustExtract ?? 0) + (bt.jsonParse ?? 0);
|
|
1440
1346
|
if (manifest?.report) this.log(`Extracted ${manifest.report.components_extracted ?? "?"}/${manifest.report.components_total ?? "?"} components (${bt.analysis}ms)`);
|
|
1441
1347
|
}
|
|
1442
|
-
const globalCss = this.substituteAssetReferences(result.globalCss, devMode);
|
|
1348
|
+
const globalCss = this.substituteAssetReferences(result.globalCss, !devMode || this.staleAssetPruning === "every-cycle");
|
|
1443
1349
|
const { declaration, variables, body } = assembleStylesheet({
|
|
1444
1350
|
layers: this.options.layers,
|
|
1445
1351
|
variableCss: system.variableCss,
|
|
@@ -1496,8 +1402,8 @@ var ExtractionSession = class {
|
|
|
1496
1402
|
this.statusAttemptOpen = true;
|
|
1497
1403
|
}
|
|
1498
1404
|
}
|
|
1499
|
-
/** Write the session's analysis-status artifact
|
|
1500
|
-
*
|
|
1405
|
+
/** Write the session's analysis-status artifact. Terminal states
|
|
1406
|
+
* (idle/failed) close the attempt. */
|
|
1501
1407
|
writeAnalysisStatus(state, pending, diagnostic) {
|
|
1502
1408
|
const status = {
|
|
1503
1409
|
schema: 2,
|
|
@@ -1505,19 +1411,15 @@ var ExtractionSession = class {
|
|
|
1505
1411
|
attemptId: this.statusAttemptId,
|
|
1506
1412
|
state,
|
|
1507
1413
|
pending,
|
|
1508
|
-
deadlineAt: Date.now() + this.debounceCeilingMs + STATUS_WATCHDOG_MS
|
|
1509
|
-
...diagnostic !== void 0 ? { diagnostic } : {},
|
|
1510
|
-
ready: this.firstEmissionComplete
|
|
1414
|
+
deadlineAt: Date.now() + this.debounceCeilingMs + STATUS_WATCHDOG_MS
|
|
1511
1415
|
};
|
|
1416
|
+
if (diagnostic !== void 0) status.diagnostic = diagnostic;
|
|
1417
|
+
status.ready = this.firstEmissionComplete;
|
|
1512
1418
|
this.writeSessionArtifact(ANALYSIS_STATUS_ARTIFACT, JSON.stringify(status));
|
|
1513
1419
|
if (state === "idle" || state === "failed") this.statusAttemptOpen = false;
|
|
1514
1420
|
}
|
|
1515
|
-
/**
|
|
1516
|
-
*
|
|
1517
|
-
* during the debounce window so a loader running ahead of the analysis
|
|
1518
|
-
* has positive evidence to wait on (design D3 'debouncing'). Paths are
|
|
1519
|
-
* filtered by the scan config and hashed at observation time.
|
|
1520
|
-
*/
|
|
1421
|
+
/** Record watch events observed during the debounce window so a loader
|
|
1422
|
+
* running ahead of the analysis has evidence to wait on. */
|
|
1521
1423
|
noteDebouncedWatchEvents(absPaths) {
|
|
1522
1424
|
if (!this.rootDir) return;
|
|
1523
1425
|
const additions = this.pendingFromBatch({ modifiedFiles: new Set(absPaths) });
|
|
@@ -1538,14 +1440,8 @@ var ExtractionSession = class {
|
|
|
1538
1440
|
}
|
|
1539
1441
|
/** One pending microtask flushes a burst of debounce observations. */
|
|
1540
1442
|
debounceStatusWriteScheduled = false;
|
|
1541
|
-
/**
|
|
1542
|
-
*
|
|
1543
|
-
* `{sessionId, generation, replacementEpoch, payloadHash}` and rewritten
|
|
1544
|
-
* only when the PAYLOAD bytes changed (byte-identical re-analyses leave
|
|
1545
|
-
* disk untouched — spec: "Unchanged manifest is not rewritten"). The
|
|
1546
|
-
* recorded diskHash (hash of the enveloped bytes) feeds the
|
|
1547
|
-
* analysis-commit.
|
|
1548
|
-
*/
|
|
1443
|
+
/** Write one enveloped payload artifact, rewritten only when the PAYLOAD
|
|
1444
|
+
* bytes changed; the recorded diskHash feeds the analysis-commit. */
|
|
1549
1445
|
publishPayloadArtifact(key, name, payload, epoch, generation, wrap) {
|
|
1550
1446
|
const payloadHash = contentHash(payload);
|
|
1551
1447
|
if (this.artifactRecords[key] === null) this.artifactRecords[key] = this.seedPayloadRecord(key, name);
|
|
@@ -1579,7 +1475,7 @@ var ExtractionSession = class {
|
|
|
1579
1475
|
} catch {
|
|
1580
1476
|
return null;
|
|
1581
1477
|
}
|
|
1582
|
-
if (!envelope ||
|
|
1478
|
+
if (!envelope || !isDiskString(envelope.payloadHash)) return null;
|
|
1583
1479
|
return {
|
|
1584
1480
|
payloadHash: envelope.payloadHash,
|
|
1585
1481
|
diskHash: contentHash(bytes)
|
|
@@ -1589,18 +1485,13 @@ var ExtractionSession = class {
|
|
|
1589
1485
|
seedCommitFromDisk() {
|
|
1590
1486
|
try {
|
|
1591
1487
|
const parsed = JSON.parse(readFileSync(analysisCommitPath(this.sessionDir), "utf-8"));
|
|
1592
|
-
return parsed.schema === 1 && parsed.sessionId === this.sessionId &&
|
|
1488
|
+
return parsed.schema === 1 && parsed.sessionId === this.sessionId && isDiskNumber(parsed.generation) ? parsed : null;
|
|
1593
1489
|
} catch {
|
|
1594
1490
|
return null;
|
|
1595
1491
|
}
|
|
1596
1492
|
}
|
|
1597
|
-
/**
|
|
1598
|
-
*
|
|
1599
|
-
* Written AFTER every payload and BEFORE the epoch; skipped entirely when
|
|
1600
|
-
* the payload set (disk hashes) and epoch are unchanged, so a no-op
|
|
1601
|
-
* re-analysis neither re-keys loader-worker hydration nor burns a
|
|
1602
|
-
* generation.
|
|
1603
|
-
*/
|
|
1493
|
+
/** Publish the analysis-commit: written after every payload and before the
|
|
1494
|
+
* epoch, skipped when the payload hashes and epoch are unchanged. */
|
|
1604
1495
|
publishAnalysisCommit(epoch, generation) {
|
|
1605
1496
|
const manifestHash = this.artifactRecords.manifest?.diskHash ?? "";
|
|
1606
1497
|
const inputsHash = this.persistAnalysisInputs ? this.artifactRecords.inputs?.diskHash ?? "" : void 0;
|
|
@@ -1613,21 +1504,14 @@ var ExtractionSession = class {
|
|
|
1613
1504
|
generation,
|
|
1614
1505
|
replacementEpoch: epoch,
|
|
1615
1506
|
manifestHash,
|
|
1616
|
-
...inputsHash !== void 0 ? { inputsHash } : {},
|
|
1617
1507
|
stylesHash
|
|
1618
1508
|
};
|
|
1509
|
+
if (inputsHash !== void 0) commit.inputsHash = inputsHash;
|
|
1619
1510
|
this.writeSessionArtifact(ANALYSIS_COMMIT_ARTIFACT, JSON.stringify(commit));
|
|
1620
1511
|
this.lastCommit = commit;
|
|
1621
1512
|
}
|
|
1622
|
-
/**
|
|
1623
|
-
*
|
|
1624
|
-
* disk witness `{schema, sessionId, epoch}`, rewritten ONLY when the
|
|
1625
|
-
* epoch VALUE changes so style-only analyses and same-session restarts
|
|
1626
|
-
* leave bytes and mtime untouched, then expose the value through the
|
|
1627
|
-
* singleton for the plugin's needBuild fan-out and the loader's catch-up
|
|
1628
|
-
* re-check. Every VALUE move also reconciles sibling sessions' epoch
|
|
1629
|
-
* artifacts (below) — the webpack cold-cache validity witness.
|
|
1630
|
-
*/
|
|
1513
|
+
/** Maintain the session's epoch witness, rewritten ONLY when the value
|
|
1514
|
+
* changes, then expose it through the singleton. */
|
|
1631
1515
|
publishReplacementEpoch(epoch) {
|
|
1632
1516
|
const onDisk = this.diskEpochValue();
|
|
1633
1517
|
if (this.lastEpochValue === null) this.lastEpochValue = onDisk;
|
|
@@ -1642,26 +1526,13 @@ var ExtractionSession = class {
|
|
|
1642
1526
|
}
|
|
1643
1527
|
setReplacementEpoch(epoch);
|
|
1644
1528
|
}
|
|
1645
|
-
/**
|
|
1646
|
-
* Sibling epoch reconciliation — the cross-session half of the webpack
|
|
1647
|
-
* persistent-cache witness (spec: dev-served-transform-coherence,
|
|
1648
|
-
* "Offline change invalidates restored modules"). Restored-module
|
|
1649
|
-
* snapshots reference the epoch artifact of the session that BUILT them;
|
|
1650
|
-
* with session-scoped trees that file would otherwise sit untouched
|
|
1651
|
-
* forever, keeping stale snapshots valid. Whenever this session's epoch
|
|
1652
|
-
* value moves, delete every sibling epoch artifact that disagrees (their
|
|
1653
|
-
* snapshots must invalidate — restore-on-demand then rebuilds from the
|
|
1654
|
-
* current generation) and leave agreeing siblings byte-untouched (warm
|
|
1655
|
-
* restores stay valid). Deleting a stale artifact is pruning, not a
|
|
1656
|
-
* foreign-session write; races with concurrent sessions are tolerated
|
|
1657
|
-
* (S14).
|
|
1658
|
-
*/
|
|
1659
|
-
/** Sibling ids already reconciled away (artifact deleted or absent) —
|
|
1660
|
-
* nothing left to invalidate for them on later moves. */
|
|
1661
|
-
reconciledSiblingIds = /* @__PURE__ */ new Set();
|
|
1662
1529
|
/** sessions-root listing memo, keyed by the root dir's mtime — a new or
|
|
1663
1530
|
* pruned sibling DIRECTORY moves it; agreeing siblings stay listed. */
|
|
1664
1531
|
siblingListing = null;
|
|
1532
|
+
/** Restored-module snapshots point at the epoch artifact of the session
|
|
1533
|
+
* that built them, so a moved epoch deletes every DISAGREEING sibling
|
|
1534
|
+
* artifact to invalidate them. Agreeing siblings stay byte-untouched so
|
|
1535
|
+
* warm restores keep working; a sibling with a live owner is exempt. */
|
|
1665
1536
|
reconcileSiblingEpochs(epoch) {
|
|
1666
1537
|
const rootPath = sessionsRootDir(this.rootDir);
|
|
1667
1538
|
let entries;
|
|
@@ -1680,61 +1551,61 @@ var ExtractionSession = class {
|
|
|
1680
1551
|
}
|
|
1681
1552
|
for (const entry of entries) {
|
|
1682
1553
|
if (entry === this.sessionId) continue;
|
|
1683
|
-
|
|
1684
|
-
const siblingEpochPath = join(
|
|
1554
|
+
const siblingDir = join(rootPath, entry);
|
|
1555
|
+
const siblingEpochPath = join(siblingDir, REPLACEMENT_EPOCH_ARTIFACT);
|
|
1685
1556
|
try {
|
|
1686
1557
|
if (JSON.parse(readFileSync(siblingEpochPath, "utf-8")).epoch === epoch) continue;
|
|
1687
1558
|
} catch (err) {
|
|
1688
|
-
if (err?.code === "ENOENT")
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1559
|
+
if (err?.code === "ENOENT") continue;
|
|
1560
|
+
}
|
|
1561
|
+
const ownerPid = this.liveSiblingOwnerPid(siblingDir);
|
|
1562
|
+
if (ownerPid !== null) {
|
|
1563
|
+
this.warn(`sibling session ${entry} still has a live owner (pid ${ownerPid}) and its replacement epoch disagrees with this session's (${epoch}) — leaving its epoch artifact in place`);
|
|
1564
|
+
continue;
|
|
1692
1565
|
}
|
|
1693
1566
|
try {
|
|
1694
1567
|
unlinkSync(siblingEpochPath);
|
|
1695
1568
|
} catch {}
|
|
1696
|
-
this.reconciledSiblingIds.add(entry);
|
|
1697
1569
|
}
|
|
1698
1570
|
}
|
|
1571
|
+
/** The pid of `siblingDir`'s live owner, or null when nothing proves one.
|
|
1572
|
+
* Our own pid is never one — publication is process-exclusive. A missing,
|
|
1573
|
+
* undecodable or unreadable claim all mean no owner: the opposite default
|
|
1574
|
+
* from the CLI lock check on purpose, so do not unify the two. */
|
|
1575
|
+
liveSiblingOwnerPid(siblingDir) {
|
|
1576
|
+
let claim;
|
|
1577
|
+
try {
|
|
1578
|
+
claim = readCliLockRecord(siblingDir);
|
|
1579
|
+
} catch {
|
|
1580
|
+
return null;
|
|
1581
|
+
}
|
|
1582
|
+
if (claim.kind !== "held") return null;
|
|
1583
|
+
if (claim.record.pid === process.pid) return null;
|
|
1584
|
+
return checkLockLiveness(claim.record).live ? claim.record.pid : null;
|
|
1585
|
+
}
|
|
1699
1586
|
/** Epoch value held by this session's on-disk artifact, or null when
|
|
1700
1587
|
* absent, unreadable, or not the expected schema. */
|
|
1701
1588
|
diskEpochValue() {
|
|
1702
1589
|
try {
|
|
1703
1590
|
const parsed = JSON.parse(readFileSync(replacementEpochPath(this.sessionDir), "utf-8"));
|
|
1704
|
-
return parsed.schema === 1 &&
|
|
1591
|
+
return parsed.schema === 1 && isDiskString(parsed.epoch) ? parsed.epoch : null;
|
|
1705
1592
|
} catch {
|
|
1706
1593
|
return null;
|
|
1707
1594
|
}
|
|
1708
1595
|
}
|
|
1709
|
-
/** True when the flat `.animus/` advisory lock
|
|
1710
|
-
*
|
|
1711
|
-
* tree right now, and any instantaneous inconsistency is its in-flight
|
|
1712
|
-
* write, not debris. */
|
|
1596
|
+
/** True when the flat `.animus/` advisory lock claims the tree. A lock that
|
|
1597
|
+
* exists but cannot be decoded claims it too — unknown is never absent. */
|
|
1713
1598
|
cliWriterHoldsLock(animusDir) {
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
} catch {
|
|
1718
|
-
return false;
|
|
1719
|
-
}
|
|
1720
|
-
if (typeof holder.pid !== "number") return false;
|
|
1721
|
-
try {
|
|
1722
|
-
process.kill(holder.pid, 0);
|
|
1723
|
-
return true;
|
|
1724
|
-
} catch {
|
|
1725
|
-
return false;
|
|
1726
|
-
}
|
|
1599
|
+
const lock = readCliLockRecord(animusDir);
|
|
1600
|
+
if (lock.kind === "none") return false;
|
|
1601
|
+
return lock.kind === "indeterminate" || checkLockLiveness(lock.record).live;
|
|
1727
1602
|
}
|
|
1728
|
-
/**
|
|
1729
|
-
*
|
|
1730
|
-
* remaining writer of flat payload names, and its live output is
|
|
1731
|
-
* fenced off below) and prune sibling session directories older than
|
|
1732
|
-
* the retention window — never the own dir, tolerating races with
|
|
1733
|
-
* concurrently-pruning sessions (S14). */
|
|
1603
|
+
/** Delete legacy flat artifacts and prune sibling session directories past
|
|
1604
|
+
* the retention window — never this session's own dir. */
|
|
1734
1605
|
runSessionStartHygiene() {
|
|
1735
1606
|
if (this.sessionStartHygieneDone) return;
|
|
1736
1607
|
this.sessionStartHygieneDone = true;
|
|
1737
|
-
const animusDir = join(this.rootDir,
|
|
1608
|
+
const animusDir = join(this.rootDir, ANIMUS_ARTIFACT_DIR);
|
|
1738
1609
|
const isCliPublishedSet = this.cliWriterHoldsLock(animusDir) || verifyCommitRecord(animusDir).length === 0;
|
|
1739
1610
|
const cliPublishedNames = [
|
|
1740
1611
|
MANIFEST_ARTIFACT,
|
|
@@ -1761,18 +1632,17 @@ var ExtractionSession = class {
|
|
|
1761
1632
|
if (entry === this.sessionId) continue;
|
|
1762
1633
|
const dir = join(sessionsRootDir(this.rootDir), entry);
|
|
1763
1634
|
try {
|
|
1764
|
-
if (statSync(dir).mtimeMs
|
|
1635
|
+
if (statSync(dir).mtimeMs >= cutoff) continue;
|
|
1636
|
+
if (this.liveSiblingOwnerPid(dir) !== null) continue;
|
|
1637
|
+
rmSync(dir, {
|
|
1765
1638
|
recursive: true,
|
|
1766
1639
|
force: true
|
|
1767
1640
|
});
|
|
1768
1641
|
} catch {}
|
|
1769
1642
|
}
|
|
1770
1643
|
}
|
|
1771
|
-
/**
|
|
1772
|
-
*
|
|
1773
|
-
* can never observe a torn half-written file. The tmp name carries the
|
|
1774
|
-
* pid — Next dev evaluates the config in more than one process, and two
|
|
1775
|
-
* sessions writing the same artifact must not race on one tmp path. */
|
|
1644
|
+
/** Write one artifact write-then-rename, so a cross-process reader never
|
|
1645
|
+
* sees a torn file; the tmp name carries the pid to keep writers apart. */
|
|
1776
1646
|
writeSessionArtifact(name, content) {
|
|
1777
1647
|
const dir = this.sessionDir;
|
|
1778
1648
|
mkdirSync(dir, { recursive: true });
|
|
@@ -1781,19 +1651,9 @@ var ExtractionSession = class {
|
|
|
1781
1651
|
renameSync(tmpPath, join(dir, name));
|
|
1782
1652
|
this.onArtifactWrite?.(name, content);
|
|
1783
1653
|
}
|
|
1784
|
-
/**
|
|
1785
|
-
*
|
|
1786
|
-
|
|
1787
|
-
* session directory's `assets/` under a content-hashed name, and
|
|
1788
|
-
* substitute a RELATIVE url — relative to the session-scoped styles.css,
|
|
1789
|
-
* which sits beside `assets/`, so the emitted `./assets/<file>` form is
|
|
1790
|
-
* unchanged. The stylesheet is processed by Next's own CSS pipeline
|
|
1791
|
-
* (webpack and Turbopack alike), which applies its native asset handling
|
|
1792
|
-
* — publicPath and output hashing — to relative url() references.
|
|
1793
|
-
* Unsubstitutable specifiers warn and emit literally in non-strict mode,
|
|
1794
|
-
* fail the build under `strict: true`.
|
|
1795
|
-
*/
|
|
1796
|
-
substituteAssetReferences(globalCss, devMode) {
|
|
1654
|
+
/** Copy each asset() specifier's bytes into `assets/` under a
|
|
1655
|
+
* content-hashed name and substitute a url relative to styles.css. */
|
|
1656
|
+
substituteAssetReferences(globalCss, pruneSuperseded) {
|
|
1797
1657
|
const specifiers = findAssetSpecifiers(globalCss);
|
|
1798
1658
|
const assetsDir = join(this.sessionDir, SESSION_ASSETS_DIR);
|
|
1799
1659
|
const expected = /* @__PURE__ */ new Set();
|
|
@@ -1838,19 +1698,15 @@ var ExtractionSession = class {
|
|
|
1838
1698
|
url
|
|
1839
1699
|
});
|
|
1840
1700
|
}
|
|
1841
|
-
if (
|
|
1701
|
+
if (pruneSuperseded) pruneStaleAssets(assetsDir, expected);
|
|
1842
1702
|
return substituteAssetPlaceholders(globalCss, urlBySpecifier);
|
|
1843
1703
|
}
|
|
1844
1704
|
trackAssetDependency(path) {
|
|
1845
1705
|
this.assetDependencyPaths.add(path);
|
|
1846
1706
|
for (const key of toWatchKeys(path)) this.assetDependencyKeys.add(key);
|
|
1847
1707
|
}
|
|
1848
|
-
/**
|
|
1849
|
-
*
|
|
1850
|
-
* resolver (host aliases → Node resolution → package root), with one
|
|
1851
|
-
* session-local last resort: an already-discovered source entry's package
|
|
1852
|
-
* root (dist-less workspace kits the shared resolver cannot see).
|
|
1853
|
-
*/
|
|
1708
|
+
/** Resolve an asset specifier through the shared resolver, with one
|
|
1709
|
+
* last resort: a discovered source entry's package root. */
|
|
1854
1710
|
resolveAssetSpecifier(specifier) {
|
|
1855
1711
|
const resolved = resolveAssetFile(specifier, this.rootDir, this.pathAliasesJson);
|
|
1856
1712
|
if (resolved) return resolved;
|
|
@@ -1866,15 +1722,8 @@ var ExtractionSession = class {
|
|
|
1866
1722
|
return null;
|
|
1867
1723
|
}
|
|
1868
1724
|
};
|
|
1869
|
-
/**
|
|
1870
|
-
*
|
|
1871
|
-
* anything else in the directory is a superseded revision (the copies are
|
|
1872
|
-
* content-addressed and never overwritten) or the leftover of an asset()
|
|
1873
|
-
* reference that no longer exists. Failures are tolerated per entry — Next
|
|
1874
|
-
* dev evaluates the config in more than one process, and a concurrent
|
|
1875
|
-
* session may have removed (or be about to rewrite) the same file; every
|
|
1876
|
-
* pass rewrites whatever of its own set is missing, so races self-heal.
|
|
1877
|
-
*/
|
|
1725
|
+
/** Sync `assets/` to the current build's file set; per-entry failures are
|
|
1726
|
+
* tolerated — a concurrent session may have removed the same file. */
|
|
1878
1727
|
function pruneStaleAssets(assetsDir, expected) {
|
|
1879
1728
|
if (!existsSync(assetsDir)) return;
|
|
1880
1729
|
let entries;
|
|
@@ -1892,61 +1741,54 @@ function pruneStaleAssets(assetsDir, expected) {
|
|
|
1892
1741
|
}
|
|
1893
1742
|
//#endregion
|
|
1894
1743
|
//#region session/turbopack-orchestrator.ts
|
|
1895
|
-
/**
|
|
1896
|
-
* Out-of-band pipeline orchestration for Turbopack (spec:
|
|
1897
|
-
* next-turbopack-integration). Turbopack exposes no compiler hooks, so the
|
|
1898
|
-
* pipeline runs during next.config resolution and, in dev, from a
|
|
1899
|
-
* filesystem watcher — the bundler only ever sees the `.animus/` disk
|
|
1900
|
-
* artifacts.
|
|
1901
|
-
*/
|
|
1902
|
-
/** Run the full pipeline with the Turbopack emitter identity: the virtual
|
|
1903
|
-
* system-props id (Turbopack rejects the webpack path's absolute-path
|
|
1904
|
-
* imports). Turbopack orchestration persists the analysis-inputs
|
|
1905
|
-
* hydration corpus — its isolated loader workers replay it (spec:
|
|
1906
|
-
* next-turbopack-integration, "Manifest disk artifact"; webpack mode
|
|
1907
|
-
* skips the corpus). */
|
|
1908
1744
|
async function runSessionPipeline(session) {
|
|
1909
1745
|
session.systemPropsModuleId = TURBOPACK_SYSTEM_PROPS_ID;
|
|
1910
1746
|
session.persistAnalysisInputs = true;
|
|
1911
1747
|
await session.runFullPipeline();
|
|
1912
1748
|
}
|
|
1913
1749
|
const activeWatcherRoots = /* @__PURE__ */ new Set();
|
|
1914
|
-
const IGNORED_SEGMENTS = new Set([
|
|
1915
|
-
|
|
1750
|
+
const IGNORED_SEGMENTS = /* @__PURE__ */ new Set([
|
|
1751
|
+
ANIMUS_ARTIFACT_DIR,
|
|
1916
1752
|
".next",
|
|
1917
1753
|
"node_modules"
|
|
1918
1754
|
]);
|
|
1919
|
-
/**
|
|
1920
|
-
*
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1755
|
+
/** Registration failures meaning the OS is out of watch capacity: descriptor
|
|
1756
|
+
* and inotify limits surface under all of these. */
|
|
1757
|
+
const CAPACITY_CODES = /* @__PURE__ */ new Set([
|
|
1758
|
+
"EMFILE",
|
|
1759
|
+
"ENFILE",
|
|
1760
|
+
"ENOSPC",
|
|
1761
|
+
"EPERM"
|
|
1762
|
+
]);
|
|
1763
|
+
function errnoCode(error) {
|
|
1764
|
+
if (!(error instanceof Object) || !("code" in error)) return null;
|
|
1765
|
+
const { code } = error;
|
|
1766
|
+
return isIntrinsicString(code) ? code : null;
|
|
1767
|
+
}
|
|
1768
|
+
/** A primitive string, decided by the intrinsic tag. `Object(value) !== value`
|
|
1769
|
+
* rejects boxed strings and a forged `Symbol.toStringTag`. */
|
|
1770
|
+
function isIntrinsicString(value) {
|
|
1771
|
+
return Object(value) !== value && Object.prototype.toString.call(value) === "[object String]";
|
|
1772
|
+
}
|
|
1773
|
+
function startTurbopackWatcher(session, rootDir, options = {}) {
|
|
1774
|
+
const { debounceMs = 75, watchFn = watch, holdEvents = false } = options;
|
|
1775
|
+
if (activeWatcherRoots.has(rootDir)) return { kind: "already-watched" };
|
|
1935
1776
|
activeWatcherRoots.add(rootDir);
|
|
1936
|
-
|
|
1777
|
+
session.debounceCeilingMs = debounceMs;
|
|
1937
1778
|
const pendingPaths = /* @__PURE__ */ new Set();
|
|
1938
1779
|
const watchers = /* @__PURE__ */ new Map();
|
|
1939
1780
|
let timer = null;
|
|
1940
1781
|
let updateChain = Promise.resolve();
|
|
1941
1782
|
let closed = false;
|
|
1942
1783
|
let died = false;
|
|
1784
|
+
let holdingEvents = holdEvents;
|
|
1943
1785
|
const externalWatchers = /* @__PURE__ */ new Map();
|
|
1944
1786
|
let pendingOpened = /* @__PURE__ */ new Map();
|
|
1945
1787
|
let capturedDuringSnapshot = [];
|
|
1946
1788
|
const externalDiagnosticKey = (root) => `external-watch:${root}`;
|
|
1947
1789
|
const failureReason = (err) => {
|
|
1948
|
-
const code = err
|
|
1949
|
-
if (code
|
|
1790
|
+
const code = errnoCode(err);
|
|
1791
|
+
if (code !== null && CAPACITY_CODES.has(code)) return `capacity(${code})`;
|
|
1950
1792
|
if (/inotify|too many/i.test(String(err))) return "capacity";
|
|
1951
1793
|
return code ?? "error";
|
|
1952
1794
|
};
|
|
@@ -1965,14 +1807,17 @@ function startTurbopackWatcher(session, rootDir, debounceMs = 75, watchFn = watc
|
|
|
1965
1807
|
const removedFiles = /* @__PURE__ */ new Set();
|
|
1966
1808
|
for (const path of pendingPaths) (existsSync(path) ? modifiedFiles : removedFiles).add(path);
|
|
1967
1809
|
pendingPaths.clear();
|
|
1968
|
-
updateChain = updateChain.then(() =>
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1810
|
+
updateChain = updateChain.then(() => {
|
|
1811
|
+
if (closed) return;
|
|
1812
|
+
return session.handleWatchUpdate({
|
|
1813
|
+
modifiedFiles,
|
|
1814
|
+
removedFiles
|
|
1815
|
+
}).catch((err) => {
|
|
1816
|
+
console.warn(`[animus-extract] watch update failed: ${String(err)}`);
|
|
1817
|
+
}).then(() => {
|
|
1818
|
+
if (pendingOpened.size > 0 || capturedDuringSnapshot.length > 0) rollbackPendingExternal();
|
|
1819
|
+
});
|
|
1820
|
+
});
|
|
1976
1821
|
};
|
|
1977
1822
|
const rollbackPendingExternal = () => {
|
|
1978
1823
|
for (const watcher of pendingOpened.values()) watcher.close();
|
|
@@ -2001,6 +1846,7 @@ function startTurbopackWatcher(session, rootDir, debounceMs = 75, watchFn = watc
|
|
|
2001
1846
|
};
|
|
2002
1847
|
const enqueuePath = (abs) => {
|
|
2003
1848
|
pendingPaths.add(abs);
|
|
1849
|
+
if (holdingEvents) return;
|
|
2004
1850
|
try {
|
|
2005
1851
|
session.noteDebouncedWatchEvents?.([abs]);
|
|
2006
1852
|
} catch (err) {
|
|
@@ -2021,11 +1867,6 @@ function startTurbopackWatcher(session, rootDir, debounceMs = 75, watchFn = watc
|
|
|
2021
1867
|
} catch {}
|
|
2022
1868
|
enqueuePath(abs);
|
|
2023
1869
|
};
|
|
2024
|
-
/** External-root event path: fenced by watcher identity ({watcherRootId,
|
|
2025
|
-
* generation} — a handle that is neither current nor pending belongs to
|
|
2026
|
-
* a retired generation and its events are rejected), `filename == null`
|
|
2027
|
-
* marks the ROOT dirty for rediscovery, and events observed while the
|
|
2028
|
-
* root's generation is still snapshotting are buffered for replay. */
|
|
2029
1870
|
const onExternalEvent = (root, self, filename) => {
|
|
2030
1871
|
if (closed) return;
|
|
2031
1872
|
const watcher = self();
|
|
@@ -2045,10 +1886,6 @@ function startTurbopackWatcher(session, rootDir, debounceMs = 75, watchFn = watc
|
|
|
2045
1886
|
}
|
|
2046
1887
|
enqueuePath(abs);
|
|
2047
1888
|
};
|
|
2048
|
-
/** Open one external-root watcher into `into` (the committed set at
|
|
2049
|
-
* startup, the pending set during reset reconciliation). Failures
|
|
2050
|
-
* degrade ONLY this root (design D7); node_modules-resident roots are
|
|
2051
|
-
* documented unwatchable and never registered (guardrail G2). */
|
|
2052
1889
|
const openExternalWatcher = (root, into) => {
|
|
2053
1890
|
if (root.split(/[\\/]/).includes("node_modules")) return;
|
|
2054
1891
|
try {
|
|
@@ -2077,7 +1914,7 @@ function startTurbopackWatcher(session, rootDir, debounceMs = 75, watchFn = watc
|
|
|
2077
1914
|
} catch (err) {
|
|
2078
1915
|
closeAll();
|
|
2079
1916
|
console.warn(`[animus-extract] Turbopack dev watcher unavailable (${String(err)}); source edits require a dev-server restart`);
|
|
2080
|
-
return
|
|
1917
|
+
return { kind: "unavailable" };
|
|
2081
1918
|
}
|
|
2082
1919
|
for (const root of session.externalWatchRoots ?? []) openExternalWatcher(root, externalWatchers);
|
|
2083
1920
|
session.onExternalRootResolved = (root) => {
|
|
@@ -2102,9 +1939,17 @@ function startTurbopackWatcher(session, rootDir, debounceMs = 75, watchFn = watc
|
|
|
2102
1939
|
return died;
|
|
2103
1940
|
},
|
|
2104
1941
|
onDied: null,
|
|
1942
|
+
deliverHeldEvents: () => {
|
|
1943
|
+
holdingEvents = false;
|
|
1944
|
+
if (pendingPaths.size > 0) flush();
|
|
1945
|
+
return updateChain;
|
|
1946
|
+
},
|
|
2105
1947
|
settle: () => updateChain
|
|
2106
1948
|
};
|
|
2107
|
-
return
|
|
1949
|
+
return {
|
|
1950
|
+
kind: "started",
|
|
1951
|
+
handle
|
|
1952
|
+
};
|
|
2108
1953
|
}
|
|
2109
1954
|
//#endregion
|
|
2110
|
-
export { ANIMUS_CSS_MODULE_ID, CLI_COMMIT_ARTIFACT, CLI_LOCK_ARTIFACT, ExtractionSession, MANIFEST_ARTIFACT, SESSION_ASSETS_DIR, STYLES_ARTIFACT, SYSTEM_PROPS_ARTIFACT, TURBOPACK_SYSTEM_PROPS_ID, analysisCommitPath, analysisInputsPath, analysisStatusPath,
|
|
1955
|
+
export { ANIMUS_ARTIFACT_DIR, ANIMUS_CSS_MODULE_ID, CLI_COMMIT_ARTIFACT, CLI_LOCK_ARTIFACT, CLI_LOCK_HEARTBEAT_INTERVAL_MS, CLI_LOCK_STALE_AFTER_MS, ExtractionSession, MANIFEST_ARTIFACT, SESSION_ASSETS_DIR, STYLES_ARTIFACT, SYSTEM_PROPS_ARTIFACT, TURBOPACK_SYSTEM_PROPS_ID, analysisCommitPath, analysisInputsPath, analysisStatusPath, checkLockLiveness, collectSessionAssets, decodeCommitRecord, engineApi, getAnalysisStartedPromise, getAnalyzedHashes, getManifestJson, getReplacementEpoch, getSessionArtifactDir, getSharedCss, getSharedExternalDirs, getSharedExternalEntries, getSharedSystemProps, holdDirectoryClaim, isLockHolderAlive, lockRecordBytes, manifestPath, pruneStaleAssets, readCliLockRecord, readJsonEnvelope, replacementEpochPath, runSessionPipeline, sessionArtifactDir, setAnalysisStartedPromise, setSharedEngine, startTurbopackWatcher, stylesPath, systemPropsPath, verifyCommitRecord };
|