@animus-ui/extract 0.1.8 → 0.1.11
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 +1 -1
- 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 +16 -0
- package/dist/index.cjs +73 -2070
- package/dist/index.mjs +2 -1996
- package/dist/{analyze-project-args.d.ts → pipeline/analyze-project-args.d.ts} +3 -1
- package/dist/pipeline/analyze-project-args.d.ts.map +1 -0
- package/dist/pipeline/assemble-stylesheet.d.ts.map +1 -0
- package/dist/pipeline/asset-placeholders.d.ts.map +1 -0
- package/dist/pipeline/content-hash.d.ts.map +1 -0
- package/dist/pipeline/core-options.d.ts +162 -0
- package/dist/pipeline/core-options.d.ts.map +1 -0
- package/dist/pipeline/correlate-external-tokens.d.ts.map +1 -0
- package/dist/pipeline/discover-files.d.ts +12 -0
- package/dist/pipeline/discover-files.d.ts.map +1 -0
- package/dist/{discover-packages.d.ts → pipeline/discover-packages.d.ts} +13 -8
- package/dist/pipeline/discover-packages.d.ts.map +1 -0
- package/dist/pipeline/dynamic-prop-config.d.ts.map +1 -0
- package/dist/{engine-adapter.d.ts → pipeline/engine-adapter.d.ts} +3 -1
- package/dist/pipeline/engine-adapter.d.ts.map +1 -0
- package/dist/pipeline/engine-retirement.d.ts.map +1 -0
- package/dist/pipeline/error-diagnostics.d.ts +32 -0
- package/dist/pipeline/error-diagnostics.d.ts.map +1 -0
- package/dist/pipeline/external-keyframes.d.ts.map +1 -0
- package/dist/{index.d.ts → pipeline/index.d.ts} +12 -1
- package/dist/pipeline/index.d.ts.map +1 -0
- package/dist/pipeline/manifest-diagnostics.d.ts.map +1 -0
- package/dist/{mdx-preprocessor.d.ts → pipeline/mdx-preprocessor.d.ts} +10 -0
- package/dist/pipeline/mdx-preprocessor.d.ts.map +1 -0
- package/dist/pipeline/path-aliases.d.ts.map +1 -0
- package/dist/pipeline/post-process-css.d.ts.map +1 -0
- package/dist/pipeline/prefix.d.ts.map +1 -0
- package/dist/pipeline/replacement-plans.d.ts.map +1 -0
- package/dist/pipeline/resolve-asset.d.ts.map +1 -0
- package/dist/pipeline/run-analysis.d.ts.map +1 -0
- package/dist/pipeline/source-identity.d.ts.map +1 -0
- package/dist/pipeline/source-ingestion.d.ts +173 -0
- package/dist/pipeline/source-ingestion.d.ts.map +1 -0
- package/dist/pipeline/static-css.d.ts.map +1 -0
- package/dist/pipeline/structural-self-check.d.ts +32 -0
- package/dist/pipeline/structural-self-check.d.ts.map +1 -0
- package/dist/pipeline/svelte-source-adapter.d.ts +88 -0
- package/dist/pipeline/svelte-source-adapter.d.ts.map +1 -0
- package/dist/{system-config.d.ts → pipeline/system-config.d.ts} +8 -0
- package/dist/pipeline/system-config.d.ts.map +1 -0
- package/dist/{system-props-module.d.ts → pipeline/system-props-module.d.ts} +4 -3
- package/dist/pipeline/system-props-module.d.ts.map +1 -0
- package/dist/pipeline/timing-waterfall.d.ts.map +1 -0
- package/dist/pipeline/tsconfig-paths.d.ts.map +1 -0
- package/dist/pipeline/unit-fallback.d.ts.map +1 -0
- package/dist/pipeline/utils.d.ts.map +1 -0
- package/dist/pipeline/watch-keys.d.ts.map +1 -0
- package/dist/pipeline-lkbOoyAr.mjs +3228 -0
- package/dist/pipeline-ojNeABi0.cjs +3685 -0
- package/dist/session/extraction-session.d.ts +446 -0
- package/dist/session/extraction-session.d.ts.map +1 -0
- package/dist/session/index.d.ts +24 -0
- package/dist/session/index.d.ts.map +1 -0
- package/dist/session/published-set.d.ts +32 -0
- package/dist/session/published-set.d.ts.map +1 -0
- package/dist/session/resolve-packages.d.ts +8 -0
- package/dist/session/resolve-packages.d.ts.map +1 -0
- package/dist/session/session-paths.d.ts +131 -0
- package/dist/session/session-paths.d.ts.map +1 -0
- package/dist/session/singleton.d.ts +124 -0
- package/dist/session/singleton.d.ts.map +1 -0
- package/dist/session/timing.d.ts +7 -0
- package/dist/session/timing.d.ts.map +1 -0
- package/dist/session/turbopack-orchestrator.d.ts +43 -0
- package/dist/session/turbopack-orchestrator.d.ts.map +1 -0
- package/dist/session.cjs +2146 -0
- package/dist/session.mjs +2110 -0
- package/package.json +18 -6
- package/dist/analyze-project-args.d.ts.map +0 -1
- package/dist/assemble-stylesheet.d.ts.map +0 -1
- package/dist/asset-placeholders.d.ts.map +0 -1
- package/dist/content-hash.d.ts.map +0 -1
- package/dist/correlate-external-tokens.d.ts.map +0 -1
- package/dist/discover-files.d.ts +0 -10
- package/dist/discover-files.d.ts.map +0 -1
- package/dist/discover-packages.d.ts.map +0 -1
- package/dist/dynamic-prop-config.d.ts.map +0 -1
- package/dist/engine-adapter.d.ts.map +0 -1
- package/dist/engine-retirement.d.ts.map +0 -1
- package/dist/external-keyframes.d.ts.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/manifest-diagnostics.d.ts.map +0 -1
- package/dist/mdx-preprocessor.d.ts.map +0 -1
- package/dist/path-aliases.d.ts.map +0 -1
- package/dist/post-process-css.d.ts.map +0 -1
- package/dist/prefix.d.ts.map +0 -1
- package/dist/replacement-plans.d.ts.map +0 -1
- package/dist/resolve-asset.d.ts.map +0 -1
- package/dist/run-analysis.d.ts.map +0 -1
- package/dist/source-identity.d.ts.map +0 -1
- package/dist/static-css.d.ts.map +0 -1
- package/dist/system-config.d.ts.map +0 -1
- package/dist/system-props-module.d.ts.map +0 -1
- package/dist/timing-waterfall.d.ts.map +0 -1
- package/dist/tsconfig-paths.d.ts.map +0 -1
- package/dist/unit-fallback.d.ts.map +0 -1
- package/dist/utils.d.ts.map +0 -1
- package/dist/watch-keys.d.ts.map +0 -1
- /package/dist/{assemble-stylesheet.d.ts → pipeline/assemble-stylesheet.d.ts} +0 -0
- /package/dist/{asset-placeholders.d.ts → pipeline/asset-placeholders.d.ts} +0 -0
- /package/dist/{content-hash.d.ts → pipeline/content-hash.d.ts} +0 -0
- /package/dist/{correlate-external-tokens.d.ts → pipeline/correlate-external-tokens.d.ts} +0 -0
- /package/dist/{dynamic-prop-config.d.ts → pipeline/dynamic-prop-config.d.ts} +0 -0
- /package/dist/{engine-retirement.d.ts → pipeline/engine-retirement.d.ts} +0 -0
- /package/dist/{external-keyframes.d.ts → pipeline/external-keyframes.d.ts} +0 -0
- /package/dist/{manifest-diagnostics.d.ts → pipeline/manifest-diagnostics.d.ts} +0 -0
- /package/dist/{path-aliases.d.ts → pipeline/path-aliases.d.ts} +0 -0
- /package/dist/{post-process-css.d.ts → pipeline/post-process-css.d.ts} +0 -0
- /package/dist/{prefix.d.ts → pipeline/prefix.d.ts} +0 -0
- /package/dist/{replacement-plans.d.ts → pipeline/replacement-plans.d.ts} +0 -0
- /package/dist/{resolve-asset.d.ts → pipeline/resolve-asset.d.ts} +0 -0
- /package/dist/{run-analysis.d.ts → pipeline/run-analysis.d.ts} +0 -0
- /package/dist/{source-identity.d.ts → pipeline/source-identity.d.ts} +0 -0
- /package/dist/{static-css.d.ts → pipeline/static-css.d.ts} +0 -0
- /package/dist/{timing-waterfall.d.ts → pipeline/timing-waterfall.d.ts} +0 -0
- /package/dist/{tsconfig-paths.d.ts → pipeline/tsconfig-paths.d.ts} +0 -0
- /package/dist/{unit-fallback.d.ts → pipeline/unit-fallback.d.ts} +0 -0
- /package/dist/{utils.d.ts → pipeline/utils.d.ts} +0 -0
- /package/dist/{watch-keys.d.ts → pipeline/watch-keys.d.ts} +0 -0
package/dist/session.cjs
ADDED
|
@@ -0,0 +1,2146 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
const require_pipeline = require("./pipeline-ojNeABi0.cjs");
|
|
3
|
+
let crypto = require("crypto");
|
|
4
|
+
let fs = require("fs");
|
|
5
|
+
let path = require("path");
|
|
6
|
+
//#region session/session-paths.ts
|
|
7
|
+
/**
|
|
8
|
+
* Session-scoped artifact names, path derivations, and the analysis-status
|
|
9
|
+
* shape (openspec: next-turbopack-served-transform-coherence, design D1–D3)
|
|
10
|
+
* — shared by the session writer (extraction-session), the webpack
|
|
11
|
+
* plugin/loader, and the Turbopack loader.
|
|
12
|
+
*
|
|
13
|
+
* Deliberately free of singleton/session imports: the Turbopack loader
|
|
14
|
+
* executes in isolated worker processes and its module graph must stay
|
|
15
|
+
* worker-local (guardrail G1) — this module is pure path/shape vocabulary.
|
|
16
|
+
*/
|
|
17
|
+
/** Session-dir-relative name of the replacement-epoch disk witness
|
|
18
|
+
* (openspec: next-webpack-served-transform-coherence, design D2;
|
|
19
|
+
* session-scoped by next-turbopack-served-transform-coherence D2). */
|
|
20
|
+
const REPLACEMENT_EPOCH_ARTIFACT = "replacements-epoch";
|
|
21
|
+
/** Virtual module id of the emitted system-props module — session
|
|
22
|
+
* vocabulary shared by the orchestrator and the Turbopack config
|
|
23
|
+
* assembly (hoisted here so the session home stays framework-free). */
|
|
24
|
+
const TURBOPACK_SYSTEM_PROPS_ID = "virtual:animus/system-props";
|
|
25
|
+
/** Session-dir-relative name of the analysis-commit transaction artifact
|
|
26
|
+
* (design D1). */
|
|
27
|
+
const ANALYSIS_COMMIT_ARTIFACT = "analysis-commit";
|
|
28
|
+
/** Session-dir-relative name of the analysis-status artifact (design D3). */
|
|
29
|
+
const ANALYSIS_STATUS_ARTIFACT = "analysis-status.json";
|
|
30
|
+
/** Session-dir-relative name of the manifest payload artifact. */
|
|
31
|
+
const MANIFEST_ARTIFACT = "manifest.json";
|
|
32
|
+
/** Session-dir-relative name of the analysis-inputs payload artifact
|
|
33
|
+
* (Turbopack loader-worker hydration corpus). */
|
|
34
|
+
const ANALYSIS_INPUTS_ARTIFACT = "analysis-inputs.json";
|
|
35
|
+
/** Session-dir-relative name of the emitted stylesheet artifact. */
|
|
36
|
+
const STYLES_ARTIFACT = "styles.css";
|
|
37
|
+
/** Session-dir-relative name of the emitted system-props module. */
|
|
38
|
+
const SYSTEM_PROPS_ARTIFACT = "system-props.js";
|
|
39
|
+
/** Session-dir-relative directory holding copied asset() files. */
|
|
40
|
+
const SESSION_ASSETS_DIR = "assets";
|
|
41
|
+
/** The standalone CLI's published-set commit record, written into the flat
|
|
42
|
+
* `.animus/` tree (openspec: standalone-extraction-cli D3). Shared here so
|
|
43
|
+
* the CLI writer and the session's start hygiene agree on ONE name — the
|
|
44
|
+
* hygiene confinement gate keys on this record, and a renamed literal on
|
|
45
|
+
* either side would silently re-arm deletion of the CLI's published output. */
|
|
46
|
+
const CLI_COMMIT_ARTIFACT = "commit.json";
|
|
47
|
+
/** The standalone CLI's single-writer advisory lock in the flat `.animus/`
|
|
48
|
+
* tree. A live holder means a CLI invocation owns that tree right now. */
|
|
49
|
+
const CLI_LOCK_ARTIFACT = "lock.json";
|
|
50
|
+
/** Root of every session-scoped artifact tree for a project. */
|
|
51
|
+
function sessionsRootDir(rootDir) {
|
|
52
|
+
return (0, path.join)(rootDir, ".animus", "sessions");
|
|
53
|
+
}
|
|
54
|
+
/** One session's artifact directory (design D2: session-scoped trees,
|
|
55
|
+
* no global pointer). */
|
|
56
|
+
function sessionArtifactDir(rootDir, sessionId) {
|
|
57
|
+
return (0, path.join)(sessionsRootDir(rootDir), sessionId);
|
|
58
|
+
}
|
|
59
|
+
/** Absolute path of a session's replacement-epoch artifact — the single
|
|
60
|
+
* derivation shared by the session writer, the webpack plugin's
|
|
61
|
+
* watch-ignore entry, and both loaders' file dependencies. Takes the
|
|
62
|
+
* SESSION directory (not the project root) since the artifact was
|
|
63
|
+
* relocated into the session tree. */
|
|
64
|
+
function replacementEpochPath(sessionDir) {
|
|
65
|
+
return (0, path.join)(sessionDir, REPLACEMENT_EPOCH_ARTIFACT);
|
|
66
|
+
}
|
|
67
|
+
/** Absolute path of a session's analysis-commit artifact. */
|
|
68
|
+
function analysisCommitPath(sessionDir) {
|
|
69
|
+
return (0, path.join)(sessionDir, ANALYSIS_COMMIT_ARTIFACT);
|
|
70
|
+
}
|
|
71
|
+
/** Absolute path of a session's analysis-status artifact. */
|
|
72
|
+
function analysisStatusPath(sessionDir) {
|
|
73
|
+
return (0, path.join)(sessionDir, ANALYSIS_STATUS_ARTIFACT);
|
|
74
|
+
}
|
|
75
|
+
/** Absolute path of a session's manifest artifact. */
|
|
76
|
+
function manifestPath(sessionDir) {
|
|
77
|
+
return (0, path.join)(sessionDir, MANIFEST_ARTIFACT);
|
|
78
|
+
}
|
|
79
|
+
/** Absolute path of a session's analysis-inputs artifact. */
|
|
80
|
+
function analysisInputsPath(sessionDir) {
|
|
81
|
+
return (0, path.join)(sessionDir, ANALYSIS_INPUTS_ARTIFACT);
|
|
82
|
+
}
|
|
83
|
+
/** Absolute path of a session's stylesheet artifact. */
|
|
84
|
+
function stylesPath(sessionDir) {
|
|
85
|
+
return (0, path.join)(sessionDir, STYLES_ARTIFACT);
|
|
86
|
+
}
|
|
87
|
+
/** Absolute path of a session's system-props module artifact. */
|
|
88
|
+
function systemPropsPath(sessionDir) {
|
|
89
|
+
return (0, path.join)(sessionDir, SYSTEM_PROPS_ARTIFACT);
|
|
90
|
+
}
|
|
91
|
+
/** Splice the session envelope into a JSON-object payload as a leading
|
|
92
|
+
* `__animusSession` field, byte-preserving the payload's own content
|
|
93
|
+
* (parse/re-stringify could reformat engine-emitted JSON). */
|
|
94
|
+
function envelopeJsonArtifact(payload, envelopeJson) {
|
|
95
|
+
const trimmed = payload.trimStart();
|
|
96
|
+
if (!trimmed.startsWith("{")) return payload;
|
|
97
|
+
if (trimmed.slice(1).trimStart().startsWith("}")) return `{"__animusSession":${envelopeJson}}`;
|
|
98
|
+
return `{"__animusSession":${envelopeJson},${trimmed.slice(1)}`;
|
|
99
|
+
}
|
|
100
|
+
/** Trailing-comment side-band for the CSS artifact (a leading comment
|
|
101
|
+
* would displace the @layer declaration consumers pin at offset 0). */
|
|
102
|
+
function envelopeCssArtifact(payload, envelopeJson) {
|
|
103
|
+
return `${payload}\n/* __animusSession ${envelopeJson} */\n`;
|
|
104
|
+
}
|
|
105
|
+
/** Matches the CSS artifact's envelope side-band. */
|
|
106
|
+
const ENVELOPE_CSS_COMMENT_RE = /\/\* __animusSession (\{.*\}) \*\//;
|
|
107
|
+
/** Envelope of a JSON artifact's bytes, or undefined when the artifact
|
|
108
|
+
* carries none. THROWS on unparseable bytes — callers decide whether a
|
|
109
|
+
* torn artifact fails closed or degrades. */
|
|
110
|
+
function readJsonEnvelope(bytes) {
|
|
111
|
+
return JSON.parse(bytes).__animusSession;
|
|
112
|
+
}
|
|
113
|
+
/** Envelope of the CSS artifact's side-band comment, or undefined when the
|
|
114
|
+
* side-band is absent. THROWS on an unparseable envelope body. */
|
|
115
|
+
function readCssEnvelope(bytes) {
|
|
116
|
+
const match = bytes.match(ENVELOPE_CSS_COMMENT_RE);
|
|
117
|
+
return match ? JSON.parse(match[1]) : void 0;
|
|
118
|
+
}
|
|
119
|
+
//#endregion
|
|
120
|
+
//#region session/published-set.ts
|
|
121
|
+
/**
|
|
122
|
+
* The CLI published-set contract, shared logic (openspec:
|
|
123
|
+
* standalone-extraction-cli D3): ONE implementation of "does this flat
|
|
124
|
+
* artifact set verify against its commit record" and ONE collector for the
|
|
125
|
+
* session's copied asset() files — consumed by the CLI writer
|
|
126
|
+
* (staging/verification) and the session's start hygiene (confinement
|
|
127
|
+
* gate). Two copies of either had already drifted once: the hygiene copy
|
|
128
|
+
* verified payloads as UTF-8 strings while the writer hashed raw bytes, so
|
|
129
|
+
* any asset-carrying set (fonts are not UTF-8) could never verify and a
|
|
130
|
+
* legitimately published set was classified as debris.
|
|
131
|
+
*/
|
|
132
|
+
/**
|
|
133
|
+
* Recompute every payload hash recorded in the directory's commit record
|
|
134
|
+
* and compare — raw bytes on both sides, matching the writer's hashing
|
|
135
|
+
* domain. Returns failure lines; empty means the set verifies. An absent
|
|
136
|
+
* or unparsable record is itself a failure (the record is written LAST, so
|
|
137
|
+
* a torn or aborted publish cannot verify).
|
|
138
|
+
*/
|
|
139
|
+
function verifyCommitRecord(dir) {
|
|
140
|
+
let record;
|
|
141
|
+
try {
|
|
142
|
+
record = JSON.parse((0, fs.readFileSync)((0, path.join)(dir, CLI_COMMIT_ARTIFACT), "utf-8"));
|
|
143
|
+
} catch (error) {
|
|
144
|
+
return [`commit record unreadable: ${String(error)}`];
|
|
145
|
+
}
|
|
146
|
+
if (record.schema !== 1 || typeof record.payloads !== "object" || record.payloads === null) return ["commit record is not a schema-1 payload record"];
|
|
147
|
+
const failures = [];
|
|
148
|
+
for (const [name, entry] of Object.entries(record.payloads)) try {
|
|
149
|
+
if (require_pipeline.contentHash((0, fs.readFileSync)((0, path.join)(dir, name))) !== entry?.hash) failures.push(`${name}: bytes do not match the commit record`);
|
|
150
|
+
} catch (error) {
|
|
151
|
+
failures.push(`${name}: unreadable (${String(error)})`);
|
|
152
|
+
}
|
|
153
|
+
return failures;
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Snapshot the session's copied asset() files (sorted by name) — read
|
|
157
|
+
* BEFORE the session tree is disposed. The emitted stylesheet references
|
|
158
|
+
* them as `./assets/<name>`, so every driver must publish them beside its
|
|
159
|
+
* stylesheet or the urls dangle.
|
|
160
|
+
*/
|
|
161
|
+
function collectSessionAssets(sessionDir) {
|
|
162
|
+
if (!sessionDir) return [];
|
|
163
|
+
const dir = (0, path.join)(sessionDir, SESSION_ASSETS_DIR);
|
|
164
|
+
if (!(0, fs.existsSync)(dir)) return [];
|
|
165
|
+
return (0, fs.readdirSync)(dir).sort().map((name) => ({
|
|
166
|
+
name,
|
|
167
|
+
bytes: (0, fs.readFileSync)((0, path.join)(dir, name))
|
|
168
|
+
}));
|
|
169
|
+
}
|
|
170
|
+
//#endregion
|
|
171
|
+
//#region session/resolve-packages.ts
|
|
172
|
+
/**
|
|
173
|
+
* Resolve external DS package specifiers to rootDir-relative entry paths.
|
|
174
|
+
* Node-generic (no webpack APIs): Pass 1 walks the consumer's workspaces for
|
|
175
|
+
* matching package names; Pass 2 falls back to `require.resolve`. Specifiers
|
|
176
|
+
* that resolve nowhere are simply omitted (spec: silent skip).
|
|
177
|
+
*/
|
|
178
|
+
function resolvePackagesByName(rootDir, names) {
|
|
179
|
+
if (names.length === 0) return {};
|
|
180
|
+
const nameSet = new Set(names);
|
|
181
|
+
const resolved = /* @__PURE__ */ new Set();
|
|
182
|
+
const packageMap = {};
|
|
183
|
+
try {
|
|
184
|
+
const rootPkg = JSON.parse((0, fs.readFileSync)((0, path.join)(rootDir, "package.json"), "utf-8"));
|
|
185
|
+
const workspaces = Array.isArray(rootPkg.workspaces) ? rootPkg.workspaces : rootPkg.workspaces?.packages ?? [];
|
|
186
|
+
for (const ws of workspaces) {
|
|
187
|
+
const wsDir = (0, path.resolve)(rootDir, ws);
|
|
188
|
+
if (!(0, fs.existsSync)(wsDir)) continue;
|
|
189
|
+
try {
|
|
190
|
+
const pkg = JSON.parse((0, fs.readFileSync)((0, path.join)(wsDir, "package.json"), "utf-8"));
|
|
191
|
+
const name = pkg.name || "";
|
|
192
|
+
if (nameSet.has(name)) {
|
|
193
|
+
const entryPath = (0, path.resolve)(wsDir, pkg.main || pkg.module || "index.ts");
|
|
194
|
+
if ((0, fs.existsSync)(entryPath)) {
|
|
195
|
+
packageMap[name] = (0, path.relative)(rootDir, entryPath);
|
|
196
|
+
resolved.add(name);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
} catch {}
|
|
200
|
+
}
|
|
201
|
+
} catch {}
|
|
202
|
+
for (const name of nameSet) {
|
|
203
|
+
if (resolved.has(name)) continue;
|
|
204
|
+
try {
|
|
205
|
+
packageMap[name] = (0, path.relative)(rootDir, require.resolve(name, { paths: [rootDir] }));
|
|
206
|
+
} catch {}
|
|
207
|
+
}
|
|
208
|
+
return packageMap;
|
|
209
|
+
}
|
|
210
|
+
//#endregion
|
|
211
|
+
//#region session/singleton.ts
|
|
212
|
+
/**
|
|
213
|
+
* Module-scope singleton for sharing analysis state between the webpack plugin and loader.
|
|
214
|
+
*
|
|
215
|
+
* Uses globalThis to survive the ESM/CJS module boundary: the plugin is loaded via ESM
|
|
216
|
+
* (next.config.ts import) while the loader is loaded by webpack via require(). Without
|
|
217
|
+
* globalThis, each module system gets its own singleton instance.
|
|
218
|
+
*/
|
|
219
|
+
const MANIFEST_KEY = "__animus_manifest_json__";
|
|
220
|
+
const PROMISE_KEY = "__animus_analysis_promise__";
|
|
221
|
+
const SHARED_CSS_KEY = "__animus_shared_css__";
|
|
222
|
+
const SHARED_SYSTEM_PROPS_KEY = "__animus_shared_system_props__";
|
|
223
|
+
const SHARED_EXTERNAL_DIRS_KEY = "__animus_external_pkg_dirs__";
|
|
224
|
+
const SHARED_EXTERNAL_ENTRIES_KEY = "__animus_external_source_entries__";
|
|
225
|
+
function getManifestJson() {
|
|
226
|
+
return globalThis[MANIFEST_KEY];
|
|
227
|
+
}
|
|
228
|
+
function setManifestJson(json) {
|
|
229
|
+
globalThis[MANIFEST_KEY] = json;
|
|
230
|
+
}
|
|
231
|
+
function getAnalysisPromise() {
|
|
232
|
+
return globalThis[PROMISE_KEY];
|
|
233
|
+
}
|
|
234
|
+
function setAnalysisPromise(promise) {
|
|
235
|
+
globalThis[PROMISE_KEY] = promise;
|
|
236
|
+
}
|
|
237
|
+
function resetAnalysisPromise() {
|
|
238
|
+
globalThis[PROMISE_KEY] = null;
|
|
239
|
+
}
|
|
240
|
+
function getSharedCss() {
|
|
241
|
+
return globalThis[SHARED_CSS_KEY] || "";
|
|
242
|
+
}
|
|
243
|
+
function setSharedCss(css) {
|
|
244
|
+
globalThis[SHARED_CSS_KEY] = css;
|
|
245
|
+
}
|
|
246
|
+
function getSharedSystemProps() {
|
|
247
|
+
return globalThis[SHARED_SYSTEM_PROPS_KEY] || "";
|
|
248
|
+
}
|
|
249
|
+
function setSharedSystemProps(content) {
|
|
250
|
+
globalThis[SHARED_SYSTEM_PROPS_KEY] = content;
|
|
251
|
+
}
|
|
252
|
+
function getSharedExternalDirs() {
|
|
253
|
+
return globalThis[SHARED_EXTERNAL_DIRS_KEY] || [];
|
|
254
|
+
}
|
|
255
|
+
function setSharedExternalDirs(dirs) {
|
|
256
|
+
globalThis[SHARED_EXTERNAL_DIRS_KEY] = dirs;
|
|
257
|
+
}
|
|
258
|
+
function getSharedExternalEntries() {
|
|
259
|
+
return globalThis[SHARED_EXTERNAL_ENTRIES_KEY] || /* @__PURE__ */ new Map();
|
|
260
|
+
}
|
|
261
|
+
function setSharedExternalEntries(entries) {
|
|
262
|
+
globalThis[SHARED_EXTERNAL_ENTRIES_KEY] = entries;
|
|
263
|
+
}
|
|
264
|
+
/** One typed globalThis slot — the single accessor shape every
|
|
265
|
+
* singleton-published value shares (null when unset). */
|
|
266
|
+
function globalSlot(key) {
|
|
267
|
+
const store = globalThis;
|
|
268
|
+
return {
|
|
269
|
+
get: () => store[key] ?? null,
|
|
270
|
+
set: (value) => {
|
|
271
|
+
store[key] = value;
|
|
272
|
+
}
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
const ANALYZED_HASHES_KEY = "__animus_analyzed_hashes__";
|
|
276
|
+
const REPLACEMENT_EPOCH_KEY = "__animus_replacement_epoch__";
|
|
277
|
+
const WATCH_TRANSACTION_KEY = "__animus_watch_transaction__";
|
|
278
|
+
const PROCESS_SESSION_ID_KEY = "__animus_process_session_id__";
|
|
279
|
+
const SESSION_ARTIFACT_DIR_KEY = "__animus_session_artifact_dir__";
|
|
280
|
+
const OWNING_WATCH_SESSION_KEY = "__animus_owning_watch_session__";
|
|
281
|
+
const EXCLUSIVE_SESSION_OWNER_KEY = "__animus_exclusive_session_owner__";
|
|
282
|
+
const analyzedHashesSlot = globalSlot(ANALYZED_HASHES_KEY);
|
|
283
|
+
const replacementEpochSlot = globalSlot(REPLACEMENT_EPOCH_KEY);
|
|
284
|
+
const watchTransactionSlot = globalSlot(WATCH_TRANSACTION_KEY);
|
|
285
|
+
const sessionArtifactDirSlot = globalSlot(SESSION_ARTIFACT_DIR_KEY);
|
|
286
|
+
const owningWatchSessionSlot = globalSlot(OWNING_WATCH_SESSION_KEY);
|
|
287
|
+
/**
|
|
288
|
+
* The session that completed the full pipeline and holds system state — the
|
|
289
|
+
* only instance that can run watch analysis. Each MultiCompiler child holds
|
|
290
|
+
* its own session AND its own watcher with its own modified set; a
|
|
291
|
+
* non-owning session forwards its batch here instead of dropping it (a file
|
|
292
|
+
* only that compiler watches would otherwise never be analyzed).
|
|
293
|
+
*/
|
|
294
|
+
function getOwningWatchSession() {
|
|
295
|
+
return owningWatchSessionSlot.get();
|
|
296
|
+
}
|
|
297
|
+
function setOwningWatchSession(session) {
|
|
298
|
+
owningWatchSessionSlot.set(session);
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* Per-file analyzed content hashes of the last published analysis (relPath →
|
|
302
|
+
* contentHash of the exact bytes analyzed) — the loader's witness for the
|
|
303
|
+
* unconditional catching-up guard (openspec:
|
|
304
|
+
* next-webpack-served-transform-coherence, design D4). Includes files
|
|
305
|
+
* analyzed with ZERO animus entries: analyzed identity is membership in the
|
|
306
|
+
* analysis input set, not manifest entry count.
|
|
307
|
+
*/
|
|
308
|
+
function getAnalyzedHashes() {
|
|
309
|
+
return analyzedHashesSlot.get();
|
|
310
|
+
}
|
|
311
|
+
function setAnalyzedHashes(hashes) {
|
|
312
|
+
analyzedHashesSlot.set(hashes);
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* Session identity is per Next INVOCATION — one process, however many
|
|
316
|
+
* compiler instances it holds (openspec:
|
|
317
|
+
* next-turbopack-served-transform-coherence, design D2). The first
|
|
318
|
+
* ExtractionSession constructed in a process claims a fresh randomUUID;
|
|
319
|
+
* every later instance (client/server/RSC compilers each construct one)
|
|
320
|
+
* adopts it, so all compilers alias, watch-ignore, and publish ONE
|
|
321
|
+
* session-scoped artifact tree. Separate invocations (`next dev` +
|
|
322
|
+
* `next build` co-writing) are separate processes and therefore separate
|
|
323
|
+
* sessions by construction.
|
|
324
|
+
*/
|
|
325
|
+
function claimProcessSessionId() {
|
|
326
|
+
const store = globalThis;
|
|
327
|
+
const existing = store[PROCESS_SESSION_ID_KEY];
|
|
328
|
+
if (typeof existing === "string" && existing.length > 0) return existing;
|
|
329
|
+
const fresh = (0, crypto.randomUUID)();
|
|
330
|
+
store[PROCESS_SESSION_ID_KEY] = fresh;
|
|
331
|
+
return fresh;
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* Exclusive-ownership claim over the process-global session state — the
|
|
335
|
+
* hard form of the invariant `claimProcessSessionId` documents softly. The
|
|
336
|
+
* shared slots above (manifest, css, artifact dir) plus the one session id
|
|
337
|
+
* mean two CONCURRENT drive loops in one process (a webpack MultiCompiler
|
|
338
|
+
* array config, parallel rollup array builds, two programmatic CLI runs)
|
|
339
|
+
* would share one session directory, overwrite each other's manifests, and
|
|
340
|
+
* delete each other's live trees on dispose. Drivers that run the loop
|
|
341
|
+
* exclusively (the unplugin host) claim for the build's lifetime and
|
|
342
|
+
* release on dispose; SEQUENTIAL claim/release cycles are legal. The Next
|
|
343
|
+
* multi-compiler adoption path stays the non-exclusive default. Throws
|
|
344
|
+
* naming both hosts and the remediation on overlap. The key lives in
|
|
345
|
+
* `SINGLETON_GLOBAL_KEYS`, so per-test global resets clear a leaked claim.
|
|
346
|
+
*/
|
|
347
|
+
function claimExclusiveSessionOwner(label) {
|
|
348
|
+
const store = globalThis;
|
|
349
|
+
const active = store[EXCLUSIVE_SESSION_OWNER_KEY];
|
|
350
|
+
if (typeof active === "string") 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.`);
|
|
351
|
+
store[EXCLUSIVE_SESSION_OWNER_KEY] = label;
|
|
352
|
+
return () => {
|
|
353
|
+
if (store[EXCLUSIVE_SESSION_OWNER_KEY] === label) delete store[EXCLUSIVE_SESSION_OWNER_KEY];
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
/**
|
|
357
|
+
* Absolute session artifact directory of the OWNING session's last
|
|
358
|
+
* publication — the webpack loader's source for the session-scoped epoch
|
|
359
|
+
* dependency path (the loader shares the process with the pipeline; the
|
|
360
|
+
* Turbopack loader instead receives the directory via its options).
|
|
361
|
+
*/
|
|
362
|
+
function getSessionArtifactDir() {
|
|
363
|
+
return sessionArtifactDirSlot.get();
|
|
364
|
+
}
|
|
365
|
+
function setSessionArtifactDir(dir) {
|
|
366
|
+
sessionArtifactDirSlot.set(dir);
|
|
367
|
+
}
|
|
368
|
+
/**
|
|
369
|
+
* Canonical replacement epoch of the last published analysis
|
|
370
|
+
* (`hashReplacementPlans(snapshotFilePlans(manifest), systemPropsContent)`
|
|
371
|
+
* — openspec: next-webpack-served-transform-coherence, design D5; the
|
|
372
|
+
* served system-props module rides as the served-dependency witness so
|
|
373
|
+
* offline system-props changes move the epoch). Published by the
|
|
374
|
+
* owning session AFTER the manifest so a reader that observes the epoch
|
|
375
|
+
* always observes at least that generation's manifest. (Session
|
|
376
|
+
* attribution lives on the DISK epoch artifact — no in-process mirror.)
|
|
377
|
+
*/
|
|
378
|
+
function getReplacementEpoch() {
|
|
379
|
+
return replacementEpochSlot.get();
|
|
380
|
+
}
|
|
381
|
+
function setReplacementEpoch(epoch) {
|
|
382
|
+
replacementEpochSlot.set(epoch);
|
|
383
|
+
}
|
|
384
|
+
/**
|
|
385
|
+
* The one in-flight watch-analysis transaction (design D3): the first
|
|
386
|
+
* compiler entering a watch batch runs analysis + publication; every other
|
|
387
|
+
* compiler (client/server/RSC — each holds its own session instance) joins
|
|
388
|
+
* this promise instead of proceeding against a pre-transaction generation.
|
|
389
|
+
*/
|
|
390
|
+
function getWatchTransaction() {
|
|
391
|
+
return watchTransactionSlot.get();
|
|
392
|
+
}
|
|
393
|
+
function setWatchTransaction(transaction) {
|
|
394
|
+
watchTransactionSlot.set(transaction);
|
|
395
|
+
}
|
|
396
|
+
const ENGINE_KEY = "__animus_engine__";
|
|
397
|
+
/** Engine selection travels through the singleton so non-owning compiler
|
|
398
|
+
* instances and the webpack loader honor the same choice as the owner. */
|
|
399
|
+
function setSharedEngine(engine) {
|
|
400
|
+
globalThis[ENGINE_KEY] = engine;
|
|
401
|
+
}
|
|
402
|
+
function getSharedEngine() {
|
|
403
|
+
return globalThis[ENGINE_KEY] || "v2";
|
|
404
|
+
}
|
|
405
|
+
/** Single engine choke-point for every native extraction call. Return type
|
|
406
|
+
* mirrors the untyped `require` the call sites previously used — the NAPI
|
|
407
|
+
* module's own .d.ts is the authoritative surface. */
|
|
408
|
+
function requireEngine() {
|
|
409
|
+
return require("@animus-ui/extract");
|
|
410
|
+
}
|
|
411
|
+
const V2_ENGINE_KEY = "__animus_v2_engine__";
|
|
412
|
+
/**
|
|
413
|
+
* Engine-agnostic API over both engines (extract-v2-spine row 13). The
|
|
414
|
+
* v2 leg adapts the v1 function surface onto a stateful ExtractEngine
|
|
415
|
+
* (hoisted to `createV2EngineApi` in @animus-ui/extract/pipeline — the
|
|
416
|
+
* single authoritative copy shared with vite-plugin). The engine INSTANCE,
|
|
417
|
+
* its analyze-time sources, and the one-shot drift flag all live on
|
|
418
|
+
* globalThis for the same reason the manifest does — the ESM plugin and the
|
|
419
|
+
* CJS webpack loader must see one engine (and one drift warning) across the
|
|
420
|
+
* double-load. (The next-plugin is already process-singleton by design:
|
|
421
|
+
* manifest, css, and system props share the same globalThis keys.)
|
|
422
|
+
* loadSystemModule is exported by both bindings from one engine-neutral
|
|
423
|
+
* Rust crate, so the default path no longer loads the v1 binary.
|
|
424
|
+
*/
|
|
425
|
+
const V2_SENT_SOURCES_KEY = "__animus_v2_sent_sources__";
|
|
426
|
+
const V2_DRIFT_WARNED_KEY = "__animus_v2_drift_warned__";
|
|
427
|
+
const v2EngineApi = require_pipeline.createV2EngineApi({
|
|
428
|
+
label: "animus",
|
|
429
|
+
isV2: () => getSharedEngine() === "v2",
|
|
430
|
+
loadNativeEngine: requireEngine,
|
|
431
|
+
passThroughUnknownPaths: true,
|
|
432
|
+
store: {
|
|
433
|
+
getEngine: () => globalThis[V2_ENGINE_KEY],
|
|
434
|
+
setEngine: (engine) => {
|
|
435
|
+
globalThis[V2_ENGINE_KEY] = engine;
|
|
436
|
+
},
|
|
437
|
+
getSentSources: () => globalThis[V2_SENT_SOURCES_KEY] ?? null,
|
|
438
|
+
setSentSources: (sources) => {
|
|
439
|
+
globalThis[V2_SENT_SOURCES_KEY] = sources;
|
|
440
|
+
},
|
|
441
|
+
getDriftWarned: () => Boolean(globalThis[V2_DRIFT_WARNED_KEY]),
|
|
442
|
+
setDriftWarned: (value) => {
|
|
443
|
+
globalThis[V2_DRIFT_WARNED_KEY] = value;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
});
|
|
447
|
+
const ENGINE_API_OVERRIDE_KEY = "__animus_engine_api_override__";
|
|
448
|
+
function engineApi() {
|
|
449
|
+
const override = globalThis[ENGINE_API_OVERRIDE_KEY];
|
|
450
|
+
return override ? override() : v2EngineApi();
|
|
451
|
+
}
|
|
452
|
+
//#endregion
|
|
453
|
+
//#region session/timing.ts
|
|
454
|
+
/**
|
|
455
|
+
* Verbose build-timing waterfall: JS phases, analysis sub-phases, and the
|
|
456
|
+
* shared Rust phase table. `ANIMUS_TIMING_JSON=1` additionally dumps a
|
|
457
|
+
* machine-readable merged record.
|
|
458
|
+
*/
|
|
459
|
+
function logBuildTimings(bt, rustTiming, log, verbose) {
|
|
460
|
+
if (!verbose) return;
|
|
461
|
+
const pad = (label, width) => label + " ".repeat(Math.max(0, width - label.length));
|
|
462
|
+
for (const [key, label] of [
|
|
463
|
+
["systemLoad", "system-load"],
|
|
464
|
+
["fileDiscovery", "file-discovery"],
|
|
465
|
+
["fileRead", "file-read+hash"],
|
|
466
|
+
["packageResolve", "pkg-resolve"],
|
|
467
|
+
["analysis", "analysis"]
|
|
468
|
+
]) {
|
|
469
|
+
const ms = bt[key] ?? 0;
|
|
470
|
+
if (ms === 0 && !Object.hasOwn(bt, key)) continue;
|
|
471
|
+
log(` ${pad(label, 17)}${String(ms).padStart(5)}ms`);
|
|
472
|
+
if (key === "analysis") for (const [sk, sl] of [
|
|
473
|
+
["jsonSerialize", "json-serialize"],
|
|
474
|
+
["rustExtract", "rust-extract"],
|
|
475
|
+
["jsonParse", "json-parse"]
|
|
476
|
+
]) {
|
|
477
|
+
const sms = bt[sk] ?? 0;
|
|
478
|
+
log(` ${pad(sl, 15)}${String(sms).padStart(5)}ms`);
|
|
479
|
+
if (sk === "rustExtract" && rustTiming) for (const line of require_pipeline.formatRustTimingWaterfall(rustTiming, {
|
|
480
|
+
indent: " ",
|
|
481
|
+
labelWidth: 13
|
|
482
|
+
})) log(line);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
log(` total ${String(bt.total).padStart(5)}ms`);
|
|
486
|
+
if (process.env.ANIMUS_TIMING_JSON === "1") {
|
|
487
|
+
const merged = {};
|
|
488
|
+
for (const [k, v] of Object.entries(bt)) merged[`buildStart.${k}`] = v;
|
|
489
|
+
if (rustTiming) {
|
|
490
|
+
for (const [k, v] of Object.entries(rustTiming)) if (typeof v === "number") merged[`rust.${k}`] = v;
|
|
491
|
+
}
|
|
492
|
+
console.error(`[animus:timing] ${JSON.stringify(merged)}`);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
//#endregion
|
|
496
|
+
//#region session/extraction-session.ts
|
|
497
|
+
/**
|
|
498
|
+
* Module id the Rust emitter injects for the extracted stylesheet — also the
|
|
499
|
+
* exact resolve.alias key with-animus registers for it, which the adapter's
|
|
500
|
+
* alias harvesting must skip.
|
|
501
|
+
*/
|
|
502
|
+
const ANIMUS_CSS_MODULE_ID = ".animus/styles.css";
|
|
503
|
+
/** Retention window for sibling session directories (design D2). */
|
|
504
|
+
const SESSION_DIR_MAX_AGE_MS = 1440 * 60 * 1e3;
|
|
505
|
+
/** Watchdog added to the debounce ceiling for status deadlines (D3). */
|
|
506
|
+
const STATUS_WATCHDOG_MS = 2e3;
|
|
507
|
+
/** Flat legacy `.animus/` artifacts removed at session start — unreachable
|
|
508
|
+
* by session-scoped loaders. The standalone CLI still publishes the three
|
|
509
|
+
* payload names flat; its verified/live sets are fenced off inside
|
|
510
|
+
* `runSessionStartHygiene`. */
|
|
511
|
+
const LEGACY_FLAT_ARTIFACTS = [
|
|
512
|
+
MANIFEST_ARTIFACT,
|
|
513
|
+
ANALYSIS_INPUTS_ARTIFACT,
|
|
514
|
+
STYLES_ARTIFACT,
|
|
515
|
+
SYSTEM_PROPS_ARTIFACT,
|
|
516
|
+
REPLACEMENT_EPOCH_ARTIFACT,
|
|
517
|
+
ANALYSIS_COMMIT_ARTIFACT,
|
|
518
|
+
ANALYSIS_STATUS_ARTIFACT
|
|
519
|
+
];
|
|
520
|
+
/**
|
|
521
|
+
* Bundler-agnostic extraction pipeline: system loading, source discovery
|
|
522
|
+
* and ingestion, external-package collection, analysis, stylesheet and
|
|
523
|
+
* system-props emission, and watch-cycle diffing.
|
|
524
|
+
*
|
|
525
|
+
* The webpack adapter (plugin.ts) owns only bundler wiring: hook
|
|
526
|
+
* registration, alias harvesting from the compiler config, in-memory asset
|
|
527
|
+
* replacement, and translating watch events into `handleWatchUpdate`
|
|
528
|
+
* change sets. Keeping this class free of webpack types is deliberate — a
|
|
529
|
+
* future Turbopack integration must drive the same session from outside
|
|
530
|
+
* the bundler (Turbopack has no compiler-hook surface), reusing everything
|
|
531
|
+
* here unchanged.
|
|
532
|
+
*
|
|
533
|
+
* Outputs are published through the package singleton (shared CSS,
|
|
534
|
+
* manifest, system props) and written to `.animus/` on disk.
|
|
535
|
+
*/
|
|
536
|
+
var ExtractionSession = class {
|
|
537
|
+
/** Set by the adapter before any pipeline run. */
|
|
538
|
+
rootDir = null;
|
|
539
|
+
/** Serialized path aliases, harvested by the adapter from bundler config. */
|
|
540
|
+
pathAliasesJson = null;
|
|
541
|
+
assetCopyCache = /* @__PURE__ */ new Map();
|
|
542
|
+
/** Physical asset files registered with the host watcher. */
|
|
543
|
+
assetDependencyPaths = /* @__PURE__ */ new Set();
|
|
544
|
+
assetDependencyKeys = /* @__PURE__ */ new Set();
|
|
545
|
+
/** Emitter identity override for the system-props module id. Webpack mode
|
|
546
|
+
* (null) injects the absolute `.animus/system-props.js` path, resolved by
|
|
547
|
+
* NormalModuleReplacement; Turbopack rejects absolute-path imports, so
|
|
548
|
+
* its driver sets the virtual id that `resolveAlias` maps to disk. */
|
|
549
|
+
systemPropsModuleId = null;
|
|
550
|
+
/** Whether the analysis-inputs hydration corpus is serialized + written.
|
|
551
|
+
* False (webpack mode): the loader shares the pipeline process and reads
|
|
552
|
+
* the manifest from memory — the corpus is never persisted. True is set
|
|
553
|
+
* by the Turbopack orchestration driver, whose isolated loader workers
|
|
554
|
+
* replay it (spec: next-turbopack-integration, "Manifest disk artifact" /
|
|
555
|
+
* "Webpack mode skips the hydration corpus"). */
|
|
556
|
+
persistAnalysisInputs = false;
|
|
557
|
+
/** Absolute directory prefixes for external DS packages (loader allowlisting). */
|
|
558
|
+
externalPackageDirs = [];
|
|
559
|
+
/** Absolute package dir → owning specifier (cross-source correlation). */
|
|
560
|
+
externalDirOwners = {};
|
|
561
|
+
/** rootDir-relative external file → owning specifier (correlation join). */
|
|
562
|
+
externalFileOwners = {};
|
|
563
|
+
/** External package specifier → absolute source entry path. */
|
|
564
|
+
externalSourceEntries = /* @__PURE__ */ new Map();
|
|
565
|
+
/** SourceId derivation authority for the current generation (openspec:
|
|
566
|
+
* external-source-watch-ingestion, design D2) — canonical roots
|
|
567
|
+
* realpath'd once per full pipeline; alias→SourceId associations
|
|
568
|
+
* recorded while files exist so deletion resolves through the cache. */
|
|
569
|
+
sourceIdentity = null;
|
|
570
|
+
/** Canonical admitted external root → declared specifiers (set-valued
|
|
571
|
+
* ownership, design D2; duplicate specifiers resolving to one canonical
|
|
572
|
+
* root share it). Engine watch wiring and tests read this. */
|
|
573
|
+
externalRootOwners = /* @__PURE__ */ new Map();
|
|
574
|
+
/** Canonical external root → the extension set its collection walk used
|
|
575
|
+
* (widened for dist-only roots); rewalks and classification share it. */
|
|
576
|
+
externalRootExtensions = /* @__PURE__ */ new Map();
|
|
577
|
+
/** Canonical admitted external watch roots — the engine-side watch
|
|
578
|
+
* surface (webpack contextDependencies / Turbopack watchers). */
|
|
579
|
+
externalWatchRoots = [];
|
|
580
|
+
/** Canonical external root → (raw sourceKey → {raw content hash, recorded
|
|
581
|
+
* abs spelling}): the previous-generation inventory a dirty-root rewalk
|
|
582
|
+
* diffs against (design D3). Raw hashes — MDX preprocessing never skews
|
|
583
|
+
* the diff; the recorded abs path is a registered deletion alias, so a
|
|
584
|
+
* reconstructed deletion always resolves through cached identity. */
|
|
585
|
+
externalInventory = /* @__PURE__ */ new Map();
|
|
586
|
+
/** Sticky diagnostics (design D5/D7): stable key → message, re-surfaced
|
|
587
|
+
* on every full pipeline until the underlying condition clears. */
|
|
588
|
+
stickyDiagnostics = /* @__PURE__ */ new Map();
|
|
589
|
+
/** Volume-membership predicate for the cross-volume gate (design D5).
|
|
590
|
+
* Injected seam: the shared predicate only discriminates on Windows
|
|
591
|
+
* (win32 semantics are unit-tested via path.win32), so runtime tests
|
|
592
|
+
* drive the gate through this. */
|
|
593
|
+
sharesProjectVolume = require_pipeline.sharesVolumeRoot;
|
|
594
|
+
/** Orchestrator seams (openspec: external-source-watch-ingestion, design
|
|
595
|
+
* D4). `onExternalRootResolved` fires per volume-admitted external root
|
|
596
|
+
* DURING collection — after resolution, BEFORE that root's sources are
|
|
597
|
+
* walked — so a watching host can open its handle with no blind gap
|
|
598
|
+
* between scan and watch. `onExternalRootsCommitted` fires once after a
|
|
599
|
+
* successful full-pipeline publication with the complete admitted
|
|
600
|
+
* canonical root set (the host's promote/replay/close-old phase; a
|
|
601
|
+
* failed pipeline never fires it, so the host rolls back). */
|
|
602
|
+
onExternalRootResolved = null;
|
|
603
|
+
onExternalRootsCommitted = null;
|
|
604
|
+
options;
|
|
605
|
+
staticCssJson;
|
|
606
|
+
system = null;
|
|
607
|
+
/** Discovery-time keyframes diagnostics awaiting the shared surfacing pass. */
|
|
608
|
+
externalKeyframesDiagnostics = [];
|
|
609
|
+
/** Full package-resolution map from the last full pipeline — replayed by
|
|
610
|
+
* incremental passes (sourceEntries alone omits dist-resolved packages). */
|
|
611
|
+
lastPackageMap = {};
|
|
612
|
+
fileCache = /* @__PURE__ */ new Map();
|
|
613
|
+
analysisEntryCache = /* @__PURE__ */ new Map();
|
|
614
|
+
sourceOwnership = {};
|
|
615
|
+
systemDependencyKeys = /* @__PURE__ */ new Set();
|
|
616
|
+
/** Loader-reported dependency paths for bundler watch registration. */
|
|
617
|
+
systemDependencyPaths = [];
|
|
618
|
+
systemDependencyHashes = /* @__PURE__ */ new Map();
|
|
619
|
+
lastSystemPropsHash = null;
|
|
620
|
+
/** Per-payload write guards: payloadHash gates rewrites (byte-identical
|
|
621
|
+
* payloads leave disk untouched); diskHash is the hash of the CURRENT
|
|
622
|
+
* disk bytes (envelope included) recorded in the analysis-commit. null =
|
|
623
|
+
* not yet seeded (first publication reads the artifact's envelope so a
|
|
624
|
+
* same-session restart never rewrites byte-identical artifacts). */
|
|
625
|
+
artifactRecords = {
|
|
626
|
+
manifest: null,
|
|
627
|
+
inputs: null,
|
|
628
|
+
styles: null
|
|
629
|
+
};
|
|
630
|
+
/** Last written analysis-commit; null = seeded from disk on first use. */
|
|
631
|
+
lastCommit = null;
|
|
632
|
+
/** Session identity — claimed once per PROCESS (one Next invocation),
|
|
633
|
+
* adopted by every subsequent session instance so all compilers share
|
|
634
|
+
* one artifact tree (design D2: `next dev`/`next build` co-writing are
|
|
635
|
+
* separate processes and therefore separate sessions). */
|
|
636
|
+
sessionId = claimProcessSessionId();
|
|
637
|
+
/** Epoch value of the last published analysis; null = not yet known
|
|
638
|
+
* (seeded from the session dir's artifact on first publication so a
|
|
639
|
+
* same-session restart with unchanged plans never rewrites bytes). */
|
|
640
|
+
lastEpochValue = null;
|
|
641
|
+
/** Watcher debounce ceiling feeding status deadlines (design D3). */
|
|
642
|
+
debounceCeilingMs = 75;
|
|
643
|
+
/** Test seam: observes every session-artifact write (name, content)
|
|
644
|
+
* post-rename — write ORDER is part of the transaction contract. */
|
|
645
|
+
onArtifactWrite = null;
|
|
646
|
+
/** Status attempt bookkeeping (design D3). */
|
|
647
|
+
statusAttemptId = 0;
|
|
648
|
+
statusAttemptOpen = false;
|
|
649
|
+
/** Monotonic first-emission witness (openspec: standalone-extraction-cli,
|
|
650
|
+
* watch readiness): flips true once the first complete publication
|
|
651
|
+
* succeeds and never regresses — every later status write (idle AND
|
|
652
|
+
* failed) carries it, so readiness stays observable across attempts. */
|
|
653
|
+
firstEmissionComplete = false;
|
|
654
|
+
/** Debounce-window observations pending analysis (sourceKey → hash). */
|
|
655
|
+
debouncePending = /* @__PURE__ */ new Map();
|
|
656
|
+
/** Session-start hygiene (pruning + legacy cleanup) runs once. */
|
|
657
|
+
sessionStartHygieneDone = false;
|
|
658
|
+
lcssTargets = null;
|
|
659
|
+
constructor(options) {
|
|
660
|
+
this.options = options;
|
|
661
|
+
this.staticCssJson = require_pipeline.serializeStaticCss(options.staticCss);
|
|
662
|
+
}
|
|
663
|
+
/** This session's artifact directory. Requires `rootDir` to be set. */
|
|
664
|
+
get sessionDir() {
|
|
665
|
+
return sessionArtifactDir(this.rootDir, this.sessionId);
|
|
666
|
+
}
|
|
667
|
+
get verbose() {
|
|
668
|
+
return this.options.verbose === true || process.env.ANIMUS_DEBUG === "1" || process.env.ANIMUS_DEBUG === "true";
|
|
669
|
+
}
|
|
670
|
+
log(msg) {
|
|
671
|
+
if (this.verbose) console.error(`[animus] ${msg}`);
|
|
672
|
+
}
|
|
673
|
+
warn(msg) {
|
|
674
|
+
console.warn(`[animus] ${msg}`);
|
|
675
|
+
}
|
|
676
|
+
now() {
|
|
677
|
+
return this.verbose ? performance.now() : 0;
|
|
678
|
+
}
|
|
679
|
+
elapsed(t) {
|
|
680
|
+
return this.verbose ? Math.round(performance.now() - t) : 0;
|
|
681
|
+
}
|
|
682
|
+
/** Lazily-computed scan config (options are constructor-fixed, so the
|
|
683
|
+
* derivation is stable for the session's lifetime). */
|
|
684
|
+
/** Diagnostic prefix label — drivers set their own so a host's errors
|
|
685
|
+
* never masquerade as another driver's (inc 05 review S1). The default
|
|
686
|
+
* keeps the Next plugin's historical prefix. */
|
|
687
|
+
driverLabel = "animus-next";
|
|
688
|
+
/** Per-specifier discovery outcomes from the last full collection —
|
|
689
|
+
* driver-consumed reporting surface (the CLI's summary). */
|
|
690
|
+
lastExternalOutcomes = [];
|
|
691
|
+
/** Component count of the last published analysis, or null before the
|
|
692
|
+
* first — the CLI's publish path reads this instead of re-parsing the
|
|
693
|
+
* manifest JSON every cycle. */
|
|
694
|
+
lastComponentCount = null;
|
|
695
|
+
/** Driver-owned STRUCTURAL exclusions (a CLI outDir inside the root),
|
|
696
|
+
* joined to the never-replaceable set — never the user `exclude` list,
|
|
697
|
+
* whose presence would flip the replace semantics and silently drop the
|
|
698
|
+
* replaceable defaults. Set before the first pipeline run (the scan
|
|
699
|
+
* config is memoized). */
|
|
700
|
+
structuralExclude = [];
|
|
701
|
+
scanConfigMemo = null;
|
|
702
|
+
/** Per-pattern exclusion hit counts from this session's matcher —
|
|
703
|
+
* driver-consumed reporting surface (dead-pattern visibility). */
|
|
704
|
+
getExcludeStats() {
|
|
705
|
+
return this.resolveScanConfig().excludeMatcher.stats();
|
|
706
|
+
}
|
|
707
|
+
/** Resolve the scan configuration from options — the single source of the
|
|
708
|
+
* exclude/extension policy shared by the full and incremental pipelines. */
|
|
709
|
+
resolveScanConfig() {
|
|
710
|
+
if (this.scanConfigMemo === null) {
|
|
711
|
+
const extensionsSet = new Set(this.options.extensions ?? require_pipeline.DEFAULT_EXTENSIONS);
|
|
712
|
+
this.scanConfigMemo = {
|
|
713
|
+
excludeMatcher: require_pipeline.createExcludeMatcher(this.options.exclude, this.structuralExclude),
|
|
714
|
+
extensionsSet
|
|
715
|
+
};
|
|
716
|
+
}
|
|
717
|
+
return this.scanConfigMemo;
|
|
718
|
+
}
|
|
719
|
+
/**
|
|
720
|
+
* Watch entry point: one single-flight analysis transaction per event
|
|
721
|
+
* batch (openspec: next-webpack-served-transform-coherence, design D3).
|
|
722
|
+
* The first entering session runs the transaction; every concurrent
|
|
723
|
+
* entry — same session re-entered, or another compiler's session
|
|
724
|
+
* (client/server/RSC each hold their own instance) — joins the in-flight
|
|
725
|
+
* promise, so no compiler proceeds against a generation older than the
|
|
726
|
+
* one the transaction publishes. A rejected transaction rejects every
|
|
727
|
+
* joiner; the gate always clears.
|
|
728
|
+
*/
|
|
729
|
+
async handleWatchUpdate(changes) {
|
|
730
|
+
const inflight = getWatchTransaction();
|
|
731
|
+
if (inflight) {
|
|
732
|
+
await inflight;
|
|
733
|
+
return;
|
|
734
|
+
}
|
|
735
|
+
if (!this.system) {
|
|
736
|
+
const owner = getOwningWatchSession();
|
|
737
|
+
const hasBatch = (changes.modifiedFiles?.size ?? 0) > 0 || (changes.removedFiles?.size ?? 0) > 0;
|
|
738
|
+
if (owner && owner !== this && hasBatch) await owner.ingestForwardedBatch(changes);
|
|
739
|
+
return;
|
|
740
|
+
}
|
|
741
|
+
const transaction = this.processWatchUpdate(changes);
|
|
742
|
+
setWatchTransaction(transaction);
|
|
743
|
+
try {
|
|
744
|
+
await transaction;
|
|
745
|
+
} finally {
|
|
746
|
+
setWatchTransaction(null);
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
/**
|
|
750
|
+
* Owner-side entry for a batch observed by a NON-OWNING compiler's
|
|
751
|
+
* watcher. Serializes behind any in-flight transaction, then runs the
|
|
752
|
+
* forwarded batch as its own transaction — never drops it (a forwarded
|
|
753
|
+
* batch has no other delivery; content-hash diffing makes an
|
|
754
|
+
* already-covered batch a cheap no-op).
|
|
755
|
+
*/
|
|
756
|
+
async ingestForwardedBatch(changes) {
|
|
757
|
+
if (!this.system) return;
|
|
758
|
+
for (;;) {
|
|
759
|
+
const inflight = getWatchTransaction();
|
|
760
|
+
if (!inflight) break;
|
|
761
|
+
await inflight;
|
|
762
|
+
}
|
|
763
|
+
const transaction = this.processWatchUpdate(changes);
|
|
764
|
+
setWatchTransaction(transaction);
|
|
765
|
+
try {
|
|
766
|
+
await transaction;
|
|
767
|
+
} finally {
|
|
768
|
+
setWatchTransaction(null);
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
/**
|
|
772
|
+
* Incremental watch pass: geological reset when the system file changed,
|
|
773
|
+
* otherwise content-hash diffing restricted to the watcher's change sets
|
|
774
|
+
* (falling back to a full discovery walk when no sets are provided).
|
|
775
|
+
*/
|
|
776
|
+
async processWatchUpdate(changes) {
|
|
777
|
+
const rootDir = this.rootDir;
|
|
778
|
+
try {
|
|
779
|
+
changes = this.reconcileExternalRoots(changes);
|
|
780
|
+
} catch (err) {
|
|
781
|
+
this.warn(`external root reconciliation failed: ${String(err)}`);
|
|
782
|
+
}
|
|
783
|
+
const changed = [...changes.modifiedFiles ?? [], ...changes.removedFiles ?? []];
|
|
784
|
+
let assetChanged = false;
|
|
785
|
+
let systemHit;
|
|
786
|
+
try {
|
|
787
|
+
if (!changes.modifiedFiles && !changes.removedFiles) for (const [dep, hash] of this.systemDependencyHashes) {
|
|
788
|
+
let current = "";
|
|
789
|
+
try {
|
|
790
|
+
current = require_pipeline.contentHash((0, fs.readFileSync)(dep, "utf-8"));
|
|
791
|
+
} catch {}
|
|
792
|
+
if (current !== hash) {
|
|
793
|
+
systemHit = dep;
|
|
794
|
+
break;
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
else systemHit = changed.find((path$2) => require_pipeline.toWatchKeys(path$2).some((key) => this.systemDependencyKeys.has(key)));
|
|
798
|
+
} catch (err) {
|
|
799
|
+
this.warn(`HMR geological-reset check failed: ${String(err)}`);
|
|
800
|
+
}
|
|
801
|
+
if (systemHit) {
|
|
802
|
+
this.log(`geological reset: system dependency changed (${(0, path.relative)(rootDir, systemHit)})`);
|
|
803
|
+
this.resetForHmr();
|
|
804
|
+
try {
|
|
805
|
+
const promise = this.runFullPipeline(this.pendingFromBatch(changes));
|
|
806
|
+
setAnalysisPromise(promise);
|
|
807
|
+
await promise;
|
|
808
|
+
} catch (err) {
|
|
809
|
+
try {
|
|
810
|
+
this.beginStatusAttempt();
|
|
811
|
+
this.writeAnalysisStatus("failed", this.pendingFromBatch(changes), String(err));
|
|
812
|
+
} catch (statusErr) {
|
|
813
|
+
this.warn(`failed-status write failed: ${String(statusErr)}`);
|
|
814
|
+
}
|
|
815
|
+
throw err;
|
|
816
|
+
}
|
|
817
|
+
return;
|
|
818
|
+
}
|
|
819
|
+
if (changed.some((path$3) => require_pipeline.toWatchKeys(path$3).some((key) => this.assetDependencyKeys.has(key)))) {
|
|
820
|
+
this.assetCopyCache.clear();
|
|
821
|
+
assetChanged = true;
|
|
822
|
+
}
|
|
823
|
+
const { excludeMatcher, extensionsSet } = this.resolveScanConfig();
|
|
824
|
+
const priorCacheEntries = /* @__PURE__ */ new Map();
|
|
825
|
+
const priorExternalFileOwners = { ...this.externalFileOwners };
|
|
826
|
+
const recordPrior = (key) => {
|
|
827
|
+
if (!priorCacheEntries.has(key)) priorCacheEntries.set(key, this.fileCache.get(key) ?? null);
|
|
828
|
+
};
|
|
829
|
+
const inventoryUpdates = [];
|
|
830
|
+
const ownerRemovals = [];
|
|
831
|
+
let removedAny = false;
|
|
832
|
+
if (changes.removedFiles) for (const removedPath of changes.removedFiles) {
|
|
833
|
+
let key;
|
|
834
|
+
let owningRoot = null;
|
|
835
|
+
if (require_pipeline.isPathWithinRoot(rootDir, removedPath)) key = (0, path.relative)(rootDir, removedPath);
|
|
836
|
+
else {
|
|
837
|
+
const resolved = this.sourceIdentity?.resolveDeletedSourceId(removedPath);
|
|
838
|
+
if (!resolved) continue;
|
|
839
|
+
key = resolved.sourceKey;
|
|
840
|
+
owningRoot = resolved.owningRoot;
|
|
841
|
+
}
|
|
842
|
+
recordPrior(key);
|
|
843
|
+
if (this.fileCache.delete(key)) {
|
|
844
|
+
removedAny = true;
|
|
845
|
+
if (owningRoot) {
|
|
846
|
+
inventoryUpdates.push({
|
|
847
|
+
root: owningRoot,
|
|
848
|
+
key,
|
|
849
|
+
hash: null,
|
|
850
|
+
abs: removedPath
|
|
851
|
+
});
|
|
852
|
+
ownerRemovals.push(key, key + ".tsx");
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
let targets;
|
|
857
|
+
if (changes.modifiedFiles) {
|
|
858
|
+
targets = [];
|
|
859
|
+
for (const modifiedPath of changes.modifiedFiles) {
|
|
860
|
+
const classified = this.classifyWatchPath(modifiedPath, {
|
|
861
|
+
excludeMatcher,
|
|
862
|
+
extensionsSet
|
|
863
|
+
});
|
|
864
|
+
if (classified) targets.push({
|
|
865
|
+
abs: modifiedPath,
|
|
866
|
+
...classified
|
|
867
|
+
});
|
|
868
|
+
}
|
|
869
|
+
} else targets = require_pipeline.discoverFiles(rootDir, rootDir, excludeMatcher, extensionsSet).map((abs) => ({
|
|
870
|
+
abs,
|
|
871
|
+
key: (0, path.relative)(rootDir, abs),
|
|
872
|
+
owningRoot: null
|
|
873
|
+
}));
|
|
874
|
+
const changedPaths = [];
|
|
875
|
+
for (const target of targets) {
|
|
876
|
+
const relPath = target.key;
|
|
877
|
+
let source;
|
|
878
|
+
try {
|
|
879
|
+
source = (0, fs.readFileSync)(target.abs, "utf-8");
|
|
880
|
+
} catch {
|
|
881
|
+
continue;
|
|
882
|
+
}
|
|
883
|
+
const rawHash = require_pipeline.contentHash(source);
|
|
884
|
+
const cached = this.fileCache.get(relPath);
|
|
885
|
+
const hash = rawHash;
|
|
886
|
+
if (!cached || cached.hash !== hash) {
|
|
887
|
+
changedPaths.push(relPath);
|
|
888
|
+
recordPrior(relPath);
|
|
889
|
+
this.fileCache.set(relPath, {
|
|
890
|
+
hash,
|
|
891
|
+
source
|
|
892
|
+
});
|
|
893
|
+
if (target.owningRoot) {
|
|
894
|
+
const owner = this.externalRootOwners.get(target.owningRoot)?.values().next().value;
|
|
895
|
+
if (owner !== void 0) this.externalFileOwners[relPath] ??= owner;
|
|
896
|
+
inventoryUpdates.push({
|
|
897
|
+
root: target.owningRoot,
|
|
898
|
+
key: target.key,
|
|
899
|
+
hash: rawHash,
|
|
900
|
+
abs: target.abs
|
|
901
|
+
});
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
if (changedPaths.length > 0 || removedAny || assetChanged) {
|
|
906
|
+
const pending = changedPaths.map((rel) => [rel, this.fileCache.get(rel).hash]);
|
|
907
|
+
let ingested;
|
|
908
|
+
try {
|
|
909
|
+
this.beginStatusAttempt();
|
|
910
|
+
ingested = await this.ingestAccepted();
|
|
911
|
+
this.externalFileOwners = this.projectExternalFileOwners(ingested, this.externalFileOwners);
|
|
912
|
+
resetAnalysisPromise();
|
|
913
|
+
const promise = this.runIncrementalPipeline(ingested.analysisEntries, pending);
|
|
914
|
+
setAnalysisPromise(promise);
|
|
915
|
+
await promise;
|
|
916
|
+
} catch (err) {
|
|
917
|
+
for (const [key, prior] of priorCacheEntries) if (prior === null) this.fileCache.delete(key);
|
|
918
|
+
else this.fileCache.set(key, prior);
|
|
919
|
+
this.externalFileOwners = priorExternalFileOwners;
|
|
920
|
+
if (this.statusAttemptOpen) {
|
|
921
|
+
this.debouncePending.clear();
|
|
922
|
+
this.writeAnalysisStatus("failed", pending, String(err));
|
|
923
|
+
}
|
|
924
|
+
throw err;
|
|
925
|
+
}
|
|
926
|
+
this.publishSourceIngestion(ingested);
|
|
927
|
+
for (const update of inventoryUpdates) {
|
|
928
|
+
let inventory = this.externalInventory.get(update.root);
|
|
929
|
+
if (!inventory) {
|
|
930
|
+
inventory = /* @__PURE__ */ new Map();
|
|
931
|
+
this.externalInventory.set(update.root, inventory);
|
|
932
|
+
}
|
|
933
|
+
if (update.hash === null) inventory.delete(update.key);
|
|
934
|
+
else inventory.set(update.key, {
|
|
935
|
+
hash: update.hash,
|
|
936
|
+
abs: update.abs
|
|
937
|
+
});
|
|
938
|
+
}
|
|
939
|
+
for (const key of ownerRemovals) delete this.externalFileOwners[key];
|
|
940
|
+
} else if (this.statusAttemptOpen) {
|
|
941
|
+
this.debouncePending.clear();
|
|
942
|
+
this.writeAnalysisStatus("idle", []);
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
/** The shared ingestion policy point (vite-plugin parity BY CODE): the
|
|
946
|
+
* capability guard, facts memo, and warn-dedupe lifecycle live in the
|
|
947
|
+
* pipeline; this host holds only its prefix, strict flag, and warn sink. */
|
|
948
|
+
sourceIngestor = require_pipeline.createSourceIngestor({
|
|
949
|
+
engineApi: () => engineApi(),
|
|
950
|
+
prefix: "[animus-next]",
|
|
951
|
+
strict: () => !!this.options.strict,
|
|
952
|
+
warn: (message) => this.warn(message)
|
|
953
|
+
});
|
|
954
|
+
/** Prepare one raw-source corpus through the shared adaptation boundary. */
|
|
955
|
+
async ingestRawSources(entries) {
|
|
956
|
+
return this.sourceIngestor.ingest(entries);
|
|
957
|
+
}
|
|
958
|
+
/** Ingest and apply the shared per-file quarantine: one invalid original
|
|
959
|
+
* (an `.mdx` with the optional peer absent, an unsupported `.svelte`
|
|
960
|
+
* shape) warns and drops; the rest of the corpus still analyzes. Strict
|
|
961
|
+
* mode throws from the shared policy — callers route that into their own
|
|
962
|
+
* status/rollback handling. */
|
|
963
|
+
async ingestAccepted(entries) {
|
|
964
|
+
const ingested = await this.ingestRawSources(entries ?? this.buildRawEntriesFromCache());
|
|
965
|
+
return require_pipeline.withoutInvalidOriginals(ingested, this.surfaceSourceDiagnostics(ingested.diagnostics));
|
|
966
|
+
}
|
|
967
|
+
/** Surface parser diagnostics under the shared strict/warn policy. */
|
|
968
|
+
surfaceSourceDiagnostics(diagnostics) {
|
|
969
|
+
return this.sourceIngestor.surfaceDiagnostics(diagnostics);
|
|
970
|
+
}
|
|
971
|
+
/**
|
|
972
|
+
* Correlate diagnostics emitted against generated children back to the
|
|
973
|
+
* external package owning their raw original.
|
|
974
|
+
*/
|
|
975
|
+
projectExternalFileOwners(result, rawOwners) {
|
|
976
|
+
const projected = {};
|
|
977
|
+
for (const owner of Object.values(result.ownership)) {
|
|
978
|
+
const packageOwner = rawOwners[owner.originalPath];
|
|
979
|
+
if (!packageOwner) continue;
|
|
980
|
+
projected[owner.originalPath] = packageOwner;
|
|
981
|
+
for (const analysisPath of owner.analysisPaths) projected[analysisPath] = packageOwner;
|
|
982
|
+
}
|
|
983
|
+
return projected;
|
|
984
|
+
}
|
|
985
|
+
/** Publish the raw cache and complete parser projection atomically. */
|
|
986
|
+
publishSourceIngestion(result) {
|
|
987
|
+
this.sourceIngestor.markPublished(result);
|
|
988
|
+
this.fileCache = new Map(result.originalEntries.map((entry) => [entry.path, {
|
|
989
|
+
hash: entry.hash,
|
|
990
|
+
source: entry.source
|
|
991
|
+
}]));
|
|
992
|
+
this.analysisEntryCache = new Map(result.analysisEntries.map((entry) => [entry.path, {
|
|
993
|
+
hash: entry.hash,
|
|
994
|
+
source: entry.source
|
|
995
|
+
}]));
|
|
996
|
+
this.sourceOwnership = result.ownership;
|
|
997
|
+
}
|
|
998
|
+
async runFullPipeline(pending = []) {
|
|
999
|
+
const pipelineStart = this.now();
|
|
1000
|
+
const bt = {};
|
|
1001
|
+
this.runSessionStartHygiene();
|
|
1002
|
+
setSessionArtifactDir(this.sessionDir);
|
|
1003
|
+
const rootDir = this.rootDir;
|
|
1004
|
+
const priorSourceState = this.snapshotSourceState();
|
|
1005
|
+
const resolvedSystemPath = (0, path.resolve)(rootDir, this.options.system);
|
|
1006
|
+
let t = this.now();
|
|
1007
|
+
this.system = require_pipeline.loadSystemConfig(engineApi, {
|
|
1008
|
+
systemPath: resolvedSystemPath,
|
|
1009
|
+
rootDir,
|
|
1010
|
+
prefix: this.options.prefix
|
|
1011
|
+
});
|
|
1012
|
+
this.assetCopyCache.clear();
|
|
1013
|
+
this.assetDependencyPaths.clear();
|
|
1014
|
+
this.assetDependencyKeys.clear();
|
|
1015
|
+
{
|
|
1016
|
+
const deps = this.system.dependencies ?? [];
|
|
1017
|
+
const keys = /* @__PURE__ */ new Set();
|
|
1018
|
+
for (const key of require_pipeline.toWatchKeys(resolvedSystemPath)) keys.add(key);
|
|
1019
|
+
for (const dep of deps) for (const key of require_pipeline.toWatchKeys(dep)) keys.add(key);
|
|
1020
|
+
this.systemDependencyKeys = keys;
|
|
1021
|
+
this.systemDependencyPaths = deps;
|
|
1022
|
+
const hashes = /* @__PURE__ */ new Map();
|
|
1023
|
+
for (const dep of deps.length > 0 ? deps : [resolvedSystemPath]) try {
|
|
1024
|
+
hashes.set(dep, require_pipeline.contentHash((0, fs.readFileSync)(dep, "utf-8")));
|
|
1025
|
+
} catch {
|
|
1026
|
+
hashes.set(dep, "");
|
|
1027
|
+
}
|
|
1028
|
+
this.systemDependencyHashes = hashes;
|
|
1029
|
+
}
|
|
1030
|
+
bt.systemLoad = this.elapsed(t);
|
|
1031
|
+
t = this.now();
|
|
1032
|
+
const { excludeMatcher, extensionsSet } = this.resolveScanConfig();
|
|
1033
|
+
const files = require_pipeline.discoverFiles(rootDir, rootDir, excludeMatcher, extensionsSet);
|
|
1034
|
+
bt.fileDiscovery = this.elapsed(t);
|
|
1035
|
+
t = this.now();
|
|
1036
|
+
const rawEntries = [];
|
|
1037
|
+
for (const filePath of files) {
|
|
1038
|
+
const source = (0, fs.readFileSync)(filePath, "utf-8");
|
|
1039
|
+
const relPath = (0, path.relative)(rootDir, filePath);
|
|
1040
|
+
const hash = require_pipeline.contentHash(source);
|
|
1041
|
+
rawEntries.push({
|
|
1042
|
+
path: relPath,
|
|
1043
|
+
source,
|
|
1044
|
+
hash
|
|
1045
|
+
});
|
|
1046
|
+
}
|
|
1047
|
+
bt.fileRead = this.elapsed(t);
|
|
1048
|
+
t = this.now();
|
|
1049
|
+
const packageNames = require_pipeline.extractSystemFilePackages(resolvedSystemPath);
|
|
1050
|
+
const preResolved = resolvePackagesByName(rootDir, packageNames);
|
|
1051
|
+
const rawExternalFiles = /* @__PURE__ */ new Map();
|
|
1052
|
+
const collected = await require_pipeline.collectExternalPackageSources({
|
|
1053
|
+
specifiers: packageNames,
|
|
1054
|
+
resolveSpecifier: (name) => preResolved[name] ? (0, path.resolve)(rootDir, preResolved[name]) : null,
|
|
1055
|
+
rootDir,
|
|
1056
|
+
extensionsSet,
|
|
1057
|
+
hasEntry: (relPath) => rawEntries.some((e) => e.path === relPath),
|
|
1058
|
+
onSourceRead: (source, _relPath, absPath) => {
|
|
1059
|
+
rawExternalFiles.set(absPath, require_pipeline.contentHash(source));
|
|
1060
|
+
},
|
|
1061
|
+
onUnreadable: (relPath, err) => this.warn(`skipped unreadable package file ${relPath}: ${String(err)}`),
|
|
1062
|
+
onPackageResolved: (_specifier, packageDir) => {
|
|
1063
|
+
if (!this.onExternalRootResolved) return;
|
|
1064
|
+
if (!this.sharesProjectVolume(rootDir, packageDir)) return;
|
|
1065
|
+
let canonical = packageDir;
|
|
1066
|
+
try {
|
|
1067
|
+
canonical = (0, fs.realpathSync)(packageDir);
|
|
1068
|
+
} catch {}
|
|
1069
|
+
this.onExternalRootResolved(canonical);
|
|
1070
|
+
}
|
|
1071
|
+
});
|
|
1072
|
+
this.lastExternalOutcomes = collected.outcomes;
|
|
1073
|
+
for (const record of collected.outcomes) if (record.outcome === "empty") this.warn(`include '${record.specifier}' resolved but discovered no component sources`);
|
|
1074
|
+
const unresolvableMessage = require_pipeline.unresolvableIncludesMessage(collected.outcomes);
|
|
1075
|
+
if (unresolvableMessage !== null) {
|
|
1076
|
+
if (this.options.strict) throw new Error(unresolvableMessage);
|
|
1077
|
+
this.warn(unresolvableMessage);
|
|
1078
|
+
}
|
|
1079
|
+
const staleDistMessage = require_pipeline.staleDistIncludesMessage(collected.outcomes);
|
|
1080
|
+
if (staleDistMessage !== null) {
|
|
1081
|
+
if (this.options.strict) throw new Error(staleDistMessage);
|
|
1082
|
+
this.warn(staleDistMessage);
|
|
1083
|
+
}
|
|
1084
|
+
const rejectedSpecifiers = /* @__PURE__ */ new Set();
|
|
1085
|
+
const crossVolumeDetails = [];
|
|
1086
|
+
for (const [dir, specifiers] of Object.entries(collected.dirOwnerSets)) {
|
|
1087
|
+
if (this.sharesProjectVolume(rootDir, dir)) continue;
|
|
1088
|
+
for (const specifier of specifiers) rejectedSpecifiers.add(specifier);
|
|
1089
|
+
crossVolumeDetails.push(`${specifiers.join(", ")} → ${dir}`);
|
|
1090
|
+
}
|
|
1091
|
+
this.stickyDiagnostics.delete("cross-volume");
|
|
1092
|
+
if (rejectedSpecifiers.size > 0) {
|
|
1093
|
+
const message = `ANIMUS_EXTERNAL_CROSS_VOLUME_UNSUPPORTED: external package source root(s) on a different volume than the project root (${rootDir}): ${crossVolumeDetails.join("; ")} — cross-volume workspace sources are unsupported; the package(s) are excluded from extraction and watching`;
|
|
1094
|
+
if (this.options.strict) throw new Error(`[${this.driverLabel}] ${message}`);
|
|
1095
|
+
this.stickyDiagnostics.set("cross-volume", message);
|
|
1096
|
+
}
|
|
1097
|
+
for (const message of this.stickyDiagnostics.values()) this.warn(message);
|
|
1098
|
+
const admitted = require_pipeline.excludeCollectedPackages(collected, rejectedSpecifiers, rootDir);
|
|
1099
|
+
try {
|
|
1100
|
+
const identity = require_pipeline.createSourceIdentity(rootDir);
|
|
1101
|
+
this.externalRootOwners = /* @__PURE__ */ new Map();
|
|
1102
|
+
this.externalRootExtensions = /* @__PURE__ */ new Map();
|
|
1103
|
+
for (const [dir, specifiers] of Object.entries(admitted.dirOwnerSets)) {
|
|
1104
|
+
const canonical = identity.registerExternalRoot(dir);
|
|
1105
|
+
const owners = this.externalRootOwners.get(canonical) ?? /* @__PURE__ */ new Set();
|
|
1106
|
+
for (const specifier of specifiers) owners.add(specifier);
|
|
1107
|
+
this.externalRootOwners.set(canonical, owners);
|
|
1108
|
+
const exts = admitted.dirExtensions[dir];
|
|
1109
|
+
if (exts) this.externalRootExtensions.set(canonical, new Set(exts));
|
|
1110
|
+
}
|
|
1111
|
+
this.sourceIdentity = identity;
|
|
1112
|
+
this.externalWatchRoots = identity.externalRoots();
|
|
1113
|
+
this.externalInventory = /* @__PURE__ */ new Map();
|
|
1114
|
+
for (const [absPath, rawHash] of rawExternalFiles) {
|
|
1115
|
+
const resolved = identity.resolveSourceId(absPath);
|
|
1116
|
+
if (!resolved?.owningRoot) continue;
|
|
1117
|
+
let inventory = this.externalInventory.get(resolved.owningRoot);
|
|
1118
|
+
if (!inventory) {
|
|
1119
|
+
inventory = /* @__PURE__ */ new Map();
|
|
1120
|
+
this.externalInventory.set(resolved.owningRoot, inventory);
|
|
1121
|
+
}
|
|
1122
|
+
inventory.set(resolved.sourceKey, {
|
|
1123
|
+
hash: rawHash,
|
|
1124
|
+
abs: absPath
|
|
1125
|
+
});
|
|
1126
|
+
}
|
|
1127
|
+
const packageMap = admitted.packageMap;
|
|
1128
|
+
this.lastPackageMap = packageMap;
|
|
1129
|
+
this.externalDirOwners = require_pipeline.firstOwners(admitted.dirOwnerSets);
|
|
1130
|
+
this.externalFileOwners = admitted.fileOwners;
|
|
1131
|
+
this.externalSourceEntries = admitted.sourceEntries;
|
|
1132
|
+
for (const entry of admitted.entries) {
|
|
1133
|
+
const hash = require_pipeline.contentHash(entry.source);
|
|
1134
|
+
rawEntries.push({
|
|
1135
|
+
path: entry.path,
|
|
1136
|
+
source: entry.source,
|
|
1137
|
+
hash
|
|
1138
|
+
});
|
|
1139
|
+
}
|
|
1140
|
+
this.externalPackageDirs = admitted.packageDirs;
|
|
1141
|
+
if (this.system && admitted.keyframesScanEntries.size > 0) {
|
|
1142
|
+
const api = engineApi();
|
|
1143
|
+
const merge = require_pipeline.mergeExternalKeyframes((entry, root) => api.scanKeyframesExports(entry, root), this.system.keyframesJson, admitted.keyframesScanEntries.values(), this.rootDir);
|
|
1144
|
+
this.system.keyframesJson = merge.keyframesJson;
|
|
1145
|
+
this.externalKeyframesDiagnostics = merge.diagnostics;
|
|
1146
|
+
} else this.externalKeyframesDiagnostics = [];
|
|
1147
|
+
bt.packageResolve = this.elapsed(t);
|
|
1148
|
+
this.beginStatusAttempt();
|
|
1149
|
+
let accepted;
|
|
1150
|
+
try {
|
|
1151
|
+
accepted = await this.ingestAccepted(rawEntries);
|
|
1152
|
+
} catch (err) {
|
|
1153
|
+
this.debouncePending.clear();
|
|
1154
|
+
this.writeAnalysisStatus("failed", pending, String(err));
|
|
1155
|
+
throw err;
|
|
1156
|
+
}
|
|
1157
|
+
this.externalFileOwners = this.projectExternalFileOwners(accepted, this.externalFileOwners);
|
|
1158
|
+
bt.fileCount = accepted.analysisEntries.length;
|
|
1159
|
+
require_pipeline.clearEngineCache(engineApi);
|
|
1160
|
+
await this.analyzeAndEmit(accepted.analysisEntries, packageMap, false, bt, pipelineStart, pending);
|
|
1161
|
+
this.publishSourceIngestion(accepted);
|
|
1162
|
+
setSharedExternalDirs(admitted.packageDirs);
|
|
1163
|
+
setSharedExternalEntries(admitted.sourceEntries);
|
|
1164
|
+
this.onExternalRootsCommitted?.(this.externalWatchRoots);
|
|
1165
|
+
setOwningWatchSession(this);
|
|
1166
|
+
} catch (err) {
|
|
1167
|
+
this.restoreSourceState(priorSourceState);
|
|
1168
|
+
throw err;
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
/** The per-generation source state runFullPipeline mutates, captured as
|
|
1172
|
+
* ONE object so the restore below cannot omit a member. The three
|
|
1173
|
+
* deep-copied fields are the ones later steps mutate in place rather
|
|
1174
|
+
* than reassign; everything else is rebound wholesale on success. */
|
|
1175
|
+
snapshotSourceState() {
|
|
1176
|
+
return {
|
|
1177
|
+
sourceIdentity: this.sourceIdentity,
|
|
1178
|
+
externalRootOwners: new Map([...this.externalRootOwners].map(([root, owners]) => [root, new Set(owners)])),
|
|
1179
|
+
externalRootExtensions: new Map(this.externalRootExtensions),
|
|
1180
|
+
externalWatchRoots: [...this.externalWatchRoots],
|
|
1181
|
+
externalInventory: new Map([...this.externalInventory].map(([root, inventory]) => [root, new Map(inventory)])),
|
|
1182
|
+
lastPackageMap: this.lastPackageMap,
|
|
1183
|
+
externalDirOwners: this.externalDirOwners,
|
|
1184
|
+
externalFileOwners: this.externalFileOwners,
|
|
1185
|
+
externalSourceEntries: this.externalSourceEntries,
|
|
1186
|
+
externalPackageDirs: this.externalPackageDirs,
|
|
1187
|
+
externalKeyframesDiagnostics: this.externalKeyframesDiagnostics
|
|
1188
|
+
};
|
|
1189
|
+
}
|
|
1190
|
+
restoreSourceState(snapshot) {
|
|
1191
|
+
Object.assign(this, snapshot);
|
|
1192
|
+
}
|
|
1193
|
+
/**
|
|
1194
|
+
* Reset analysis state for HMR geological reset. Payload write guards go
|
|
1195
|
+
* back to null so the next publication reseeds them from the disk
|
|
1196
|
+
* envelopes — a byte-identical post-reset artifact is still not
|
|
1197
|
+
* rewritten.
|
|
1198
|
+
*/
|
|
1199
|
+
resetForHmr() {
|
|
1200
|
+
resetAnalysisPromise();
|
|
1201
|
+
this.artifactRecords = {
|
|
1202
|
+
manifest: null,
|
|
1203
|
+
inputs: null,
|
|
1204
|
+
styles: null
|
|
1205
|
+
};
|
|
1206
|
+
this.lastSystemPropsHash = null;
|
|
1207
|
+
}
|
|
1208
|
+
/**
|
|
1209
|
+
* The (sourceKey, observedSourceHash) pairs of a watch batch — cheap
|
|
1210
|
+
* evidence for the status file's pending set on the geological-reset
|
|
1211
|
+
* path, where the batch's component edits ride along with the system
|
|
1212
|
+
* edit (design D3: loaders wait only on observed inputs).
|
|
1213
|
+
*/
|
|
1214
|
+
pendingFromBatch(changes) {
|
|
1215
|
+
const { excludeMatcher, extensionsSet } = this.resolveScanConfig();
|
|
1216
|
+
const pending = [];
|
|
1217
|
+
for (const path$4 of changes.modifiedFiles ?? []) {
|
|
1218
|
+
const classified = this.classifyWatchPath(path$4, {
|
|
1219
|
+
excludeMatcher,
|
|
1220
|
+
extensionsSet
|
|
1221
|
+
});
|
|
1222
|
+
if (!classified) continue;
|
|
1223
|
+
try {
|
|
1224
|
+
pending.push([classified.key, require_pipeline.contentHash((0, fs.readFileSync)(path$4, "utf-8"))]);
|
|
1225
|
+
} catch {}
|
|
1226
|
+
}
|
|
1227
|
+
return pending;
|
|
1228
|
+
}
|
|
1229
|
+
/**
|
|
1230
|
+
* Root-dirty inventory reconciliation (design D3): partition the
|
|
1231
|
+
* watcher's change sets into explicit FILE events and external ROOT hits
|
|
1232
|
+
* (a reported path that IS an admitted root, or a directory inside one),
|
|
1233
|
+
* rewalk each dirty root through the shared discovery policy, diff the
|
|
1234
|
+
* SourceId+raw-hash inventory against the previous generation, and merge
|
|
1235
|
+
* the reconstructed created/edited/deleted deltas with the explicit
|
|
1236
|
+
* events (identity-level dedup happens downstream at the cache gate).
|
|
1237
|
+
* Deletion falls out of the inventory diff — webpack may report only the
|
|
1238
|
+
* directory (probe-proven).
|
|
1239
|
+
*/
|
|
1240
|
+
reconcileExternalRoots(changes) {
|
|
1241
|
+
const identity = this.sourceIdentity;
|
|
1242
|
+
if (!identity) return changes;
|
|
1243
|
+
if (!changes.modifiedFiles && !changes.removedFiles) return changes;
|
|
1244
|
+
if (this.externalWatchRoots.length === 0) return changes;
|
|
1245
|
+
const { extensionsSet } = this.resolveScanConfig();
|
|
1246
|
+
const dirtyRoots = /* @__PURE__ */ new Set();
|
|
1247
|
+
const modified = /* @__PURE__ */ new Set();
|
|
1248
|
+
const removed = /* @__PURE__ */ new Set();
|
|
1249
|
+
const rootHitFor = (path$5) => {
|
|
1250
|
+
const containing = identity.containingExternalRoot(path$5);
|
|
1251
|
+
if (!containing) return null;
|
|
1252
|
+
try {
|
|
1253
|
+
return (0, fs.statSync)(path$5).isDirectory() ? containing : null;
|
|
1254
|
+
} catch {
|
|
1255
|
+
return identity.resolveDeletedSourceId(path$5) ? null : containing;
|
|
1256
|
+
}
|
|
1257
|
+
};
|
|
1258
|
+
for (const path$6 of changes.modifiedFiles ?? []) {
|
|
1259
|
+
const root = rootHitFor(path$6);
|
|
1260
|
+
if (root) dirtyRoots.add(root);
|
|
1261
|
+
else modified.add(path$6);
|
|
1262
|
+
}
|
|
1263
|
+
for (const path$7 of changes.removedFiles ?? []) {
|
|
1264
|
+
const root = rootHitFor(path$7);
|
|
1265
|
+
if (root) dirtyRoots.add(root);
|
|
1266
|
+
else removed.add(path$7);
|
|
1267
|
+
}
|
|
1268
|
+
for (const root of dirtyRoots) {
|
|
1269
|
+
const previous = this.externalInventory.get(root) ?? /* @__PURE__ */ new Map();
|
|
1270
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1271
|
+
const walked = require_pipeline.walkPackageSources(root, this.externalRootExtensions.get(root) ?? extensionsSet);
|
|
1272
|
+
if (walked.length === 0 && previous.size > 0) this.warn(`external root rewalk found no files under ${root} while its inventory holds ${previous.size} — reconciling as full deletion`);
|
|
1273
|
+
for (const abs of walked) {
|
|
1274
|
+
const resolved = identity.resolveSourceId(abs);
|
|
1275
|
+
if (!resolved || resolved.owningRoot !== root) continue;
|
|
1276
|
+
seen.add(resolved.sourceKey);
|
|
1277
|
+
let rawHash;
|
|
1278
|
+
try {
|
|
1279
|
+
rawHash = require_pipeline.contentHash((0, fs.readFileSync)(abs, "utf-8"));
|
|
1280
|
+
} catch {
|
|
1281
|
+
continue;
|
|
1282
|
+
}
|
|
1283
|
+
if (previous.get(resolved.sourceKey)?.hash !== rawHash) modified.add(abs);
|
|
1284
|
+
}
|
|
1285
|
+
for (const [key, entry] of previous) if (!seen.has(key)) removed.add(entry.abs);
|
|
1286
|
+
}
|
|
1287
|
+
return {
|
|
1288
|
+
modifiedFiles: modified,
|
|
1289
|
+
removedFiles: removed
|
|
1290
|
+
};
|
|
1291
|
+
}
|
|
1292
|
+
/**
|
|
1293
|
+
* Route one watcher-reported path (design D1/D2): a project-root member
|
|
1294
|
+
* (lexical containment — existing local semantics preserved; consumer
|
|
1295
|
+
* exclude patterns apply) or an admitted external member (identity
|
|
1296
|
+
* resolution with symlink-escape rejection; package-relative excludes
|
|
1297
|
+
* mirror the collection walk's filters — guardrail G1, one policy).
|
|
1298
|
+
* Returns null for dropped paths.
|
|
1299
|
+
*/
|
|
1300
|
+
classifyWatchPath(absPath, scan) {
|
|
1301
|
+
const ext = (0, path.extname)(absPath);
|
|
1302
|
+
const rootDir = this.rootDir;
|
|
1303
|
+
if (require_pipeline.isPathWithinRoot(rootDir, absPath)) {
|
|
1304
|
+
if (!scan.extensionsSet.has(ext)) return null;
|
|
1305
|
+
const rel = (0, path.relative)(rootDir, absPath);
|
|
1306
|
+
if (scan.excludeMatcher.matches(absPath, rel)) return null;
|
|
1307
|
+
return {
|
|
1308
|
+
key: rel,
|
|
1309
|
+
owningRoot: null
|
|
1310
|
+
};
|
|
1311
|
+
}
|
|
1312
|
+
const resolved = this.sourceIdentity?.resolveSourceId(absPath);
|
|
1313
|
+
if (!resolved) return null;
|
|
1314
|
+
if (resolved.owningRoot === null) {
|
|
1315
|
+
if (!scan.extensionsSet.has(ext)) return null;
|
|
1316
|
+
if (scan.excludeMatcher.matches(resolved.sourceKey, resolved.sourceKey)) return null;
|
|
1317
|
+
return {
|
|
1318
|
+
key: resolved.sourceKey,
|
|
1319
|
+
owningRoot: null
|
|
1320
|
+
};
|
|
1321
|
+
}
|
|
1322
|
+
if (!(this.externalRootExtensions.get(resolved.owningRoot) ?? scan.extensionsSet).has(ext)) return null;
|
|
1323
|
+
if (require_pipeline.isExcludedPackageRelativePath(resolved.pathInRoot)) return null;
|
|
1324
|
+
return {
|
|
1325
|
+
key: resolved.sourceKey,
|
|
1326
|
+
owningRoot: resolved.owningRoot
|
|
1327
|
+
};
|
|
1328
|
+
}
|
|
1329
|
+
/** Build full raw originals for one shared adaptation attempt. */
|
|
1330
|
+
buildRawEntriesFromCache() {
|
|
1331
|
+
const entries = [];
|
|
1332
|
+
for (const [path$8, { hash, source }] of this.fileCache) entries.push({
|
|
1333
|
+
path: path$8,
|
|
1334
|
+
source,
|
|
1335
|
+
hash
|
|
1336
|
+
});
|
|
1337
|
+
return entries;
|
|
1338
|
+
}
|
|
1339
|
+
/**
|
|
1340
|
+
* Run incremental pipeline with cache-aware file entries.
|
|
1341
|
+
* Reuses system config from the last full pipeline run.
|
|
1342
|
+
*/
|
|
1343
|
+
async runIncrementalPipeline(fileEntries, pending = []) {
|
|
1344
|
+
const bt = {};
|
|
1345
|
+
const pipelineStart = this.now();
|
|
1346
|
+
await this.analyzeAndEmit(fileEntries, this.lastPackageMap, true, bt, pipelineStart, pending);
|
|
1347
|
+
}
|
|
1348
|
+
/**
|
|
1349
|
+
* Shared analysis + emit core for both pipelines — the single call site
|
|
1350
|
+
* that routes every manifest through the shared `runProjectAnalysis`,
|
|
1351
|
+
* reachable from both runFullPipeline (production) and
|
|
1352
|
+
* runIncrementalPipeline (HMR).
|
|
1353
|
+
*
|
|
1354
|
+
* Owns diagnostic surfacing, CSS assembly + styles.css write guard,
|
|
1355
|
+
* system-props module emit, and the timing log. The engine's own
|
|
1356
|
+
* `dev_mode` (reconciliation pruning) is derived separately via
|
|
1357
|
+
* `engineDevMode` — an explicit `mode` option overrides the pipeline
|
|
1358
|
+
* path there. For everything else the `devMode` flag is the ONLY
|
|
1359
|
+
* behavioral fork:
|
|
1360
|
+
*
|
|
1361
|
+
* - `false` (production): computes bt.analysis + logs the extraction
|
|
1362
|
+
* report, and writes system-props.js UNCONDITIONALLY (no
|
|
1363
|
+
* lastSystemPropsHash guard).
|
|
1364
|
+
* - `true` (HMR): skips the report log, and guards the system-props.js
|
|
1365
|
+
* write by lastSystemPropsHash.
|
|
1366
|
+
*
|
|
1367
|
+
* The disk artifacts land under the session directory in transaction
|
|
1368
|
+
* order (design D1): manifest → analysis-inputs (Turbopack orchestration
|
|
1369
|
+
* only) → styles.css → system-props → analysis-commit →
|
|
1370
|
+
* replacements-epoch (last, only when moved). The analysis-status file walks starting → analyzing →
|
|
1371
|
+
* committing → idle around the attempt, landing in `failed` (with the
|
|
1372
|
+
* diagnostic) on any throw (design D3).
|
|
1373
|
+
*/
|
|
1374
|
+
async analyzeAndEmit(fileEntries, packageMap, devMode, bt, pipelineStart, pending = []) {
|
|
1375
|
+
this.beginStatusAttempt();
|
|
1376
|
+
this.debouncePending.clear();
|
|
1377
|
+
this.writeAnalysisStatus("starting", pending);
|
|
1378
|
+
try {
|
|
1379
|
+
await this.analyzeAndEmitAttempt(fileEntries, packageMap, devMode, bt, pipelineStart, pending);
|
|
1380
|
+
this.firstEmissionComplete = true;
|
|
1381
|
+
this.writeAnalysisStatus("idle", []);
|
|
1382
|
+
} catch (err) {
|
|
1383
|
+
this.writeAnalysisStatus("failed", pending, String(err));
|
|
1384
|
+
throw err;
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1387
|
+
/**
|
|
1388
|
+
* The engine's `dev_mode` flag (retain all components vs reconciliation
|
|
1389
|
+
* pruning) is an EMISSION decision, so an explicit `mode` option wins over
|
|
1390
|
+
* the pipeline path (core-options: `mode` "decides emitted bytes …
|
|
1391
|
+
* engine devMode"). Without an explicit mode the historical per-pipeline
|
|
1392
|
+
* default applies: full = production pruning, incremental = dev retention.
|
|
1393
|
+
* A pinned-production watch must not flip to unpruned CSS on its first
|
|
1394
|
+
* incremental republication, and a pinned-development full build must
|
|
1395
|
+
* retain all components.
|
|
1396
|
+
*/
|
|
1397
|
+
engineDevMode(pipelineDefault) {
|
|
1398
|
+
if (this.options.mode !== void 0) return this.options.mode === "development";
|
|
1399
|
+
return pipelineDefault;
|
|
1400
|
+
}
|
|
1401
|
+
async analyzeAndEmitAttempt(fileEntries, packageMap, devMode, bt, pipelineStart, pending) {
|
|
1402
|
+
const system = this.system;
|
|
1403
|
+
const analysisOptions = {
|
|
1404
|
+
fileEntries,
|
|
1405
|
+
packageMap,
|
|
1406
|
+
system,
|
|
1407
|
+
emitter: {
|
|
1408
|
+
runtimeImport: "@animus-ui/system/runtime",
|
|
1409
|
+
cssModuleId: ANIMUS_CSS_MODULE_ID,
|
|
1410
|
+
systemPropsModuleId: this.systemPropsModuleId ?? systemPropsPath(this.sessionDir)
|
|
1411
|
+
},
|
|
1412
|
+
pathAliasesJson: this.pathAliasesJson,
|
|
1413
|
+
staticCssJson: this.staticCssJson,
|
|
1414
|
+
externalDirs: this.externalPackageDirs.map((dir) => (0, path.relative)(this.rootDir, dir)),
|
|
1415
|
+
devMode: this.engineDevMode(devMode)
|
|
1416
|
+
};
|
|
1417
|
+
this.writeAnalysisStatus("analyzing", pending);
|
|
1418
|
+
const result = require_pipeline.runProjectAnalysis(engineApi, {
|
|
1419
|
+
...analysisOptions,
|
|
1420
|
+
warn: (message) => this.warn(message),
|
|
1421
|
+
strict: this.options.strict,
|
|
1422
|
+
extraDiagnostics: this.externalKeyframesDiagnostics
|
|
1423
|
+
});
|
|
1424
|
+
require_pipeline.assertNoErrorDiagnostics(result.manifest?.diagnostics);
|
|
1425
|
+
require_pipeline.enforceExternalTokenContracts({
|
|
1426
|
+
diagnostics: result.manifest?.diagnostics,
|
|
1427
|
+
fileOwners: this.externalFileOwners,
|
|
1428
|
+
dirOwners: this.externalDirOwners,
|
|
1429
|
+
sourceThemeManifestsJson: system.sourceThemeManifestsJson,
|
|
1430
|
+
strict: this.options.strict,
|
|
1431
|
+
prefix: `[${this.driverLabel}]`,
|
|
1432
|
+
warn: (message) => this.warn(message)
|
|
1433
|
+
});
|
|
1434
|
+
bt.jsonSerialize = result.timings.serializeMs;
|
|
1435
|
+
bt.rustExtract = result.timings.extractMs;
|
|
1436
|
+
bt.jsonParse = result.timings.parseMs;
|
|
1437
|
+
const manifest = result.manifest;
|
|
1438
|
+
this.lastComponentCount = Object.keys(manifest?.components ?? {}).length;
|
|
1439
|
+
if (!devMode) {
|
|
1440
|
+
bt.analysis = (bt.jsonSerialize ?? 0) + (bt.rustExtract ?? 0) + (bt.jsonParse ?? 0);
|
|
1441
|
+
if (manifest?.report) this.log(`Extracted ${manifest.report.components_extracted ?? "?"}/${manifest.report.components_total ?? "?"} components (${bt.analysis}ms)`);
|
|
1442
|
+
}
|
|
1443
|
+
const globalCss = this.substituteAssetReferences(result.globalCss, devMode);
|
|
1444
|
+
const { declaration, variables, body } = require_pipeline.assembleStylesheet({
|
|
1445
|
+
layers: this.options.layers,
|
|
1446
|
+
variableCss: system.variableCss,
|
|
1447
|
+
globalCss,
|
|
1448
|
+
componentCss: result.componentCss,
|
|
1449
|
+
split: true
|
|
1450
|
+
});
|
|
1451
|
+
if (this.lcssTargets === null) this.lcssTargets = require_pipeline.resolveLightningTargets(this.options.targets, this.rootDir);
|
|
1452
|
+
const fullCss = [
|
|
1453
|
+
declaration,
|
|
1454
|
+
variables,
|
|
1455
|
+
require_pipeline.postProcessCss(body, {
|
|
1456
|
+
minify: this.options.minify ?? require_pipeline.resolveMode(this.options.mode, () => process.env.NODE_ENV === "production" ? "production" : "development").mode === "production",
|
|
1457
|
+
targets: this.lcssTargets,
|
|
1458
|
+
warnFn: (msg) => this.warn(msg)
|
|
1459
|
+
})
|
|
1460
|
+
].filter(Boolean).join("\n");
|
|
1461
|
+
setSharedCss(fullCss);
|
|
1462
|
+
const systemPropsContent = require_pipeline.buildSystemPropsModule({
|
|
1463
|
+
systemPropMapJson: JSON.stringify(manifest?.system_prop_map ?? {}),
|
|
1464
|
+
groupRegistryJson: system.groupRegistryJson,
|
|
1465
|
+
dynamicProps: manifest?.dynamic_props ?? {}
|
|
1466
|
+
});
|
|
1467
|
+
setSharedSystemProps(systemPropsContent);
|
|
1468
|
+
setManifestJson(result.manifestJson);
|
|
1469
|
+
setAnalyzedHashes(new Map(fileEntries.map((entry) => [entry.path, entry.hash])));
|
|
1470
|
+
this.writeAnalysisStatus("committing", pending);
|
|
1471
|
+
const epoch = require_pipeline.hashReplacementPlans(require_pipeline.snapshotFilePlans(manifest), systemPropsContent);
|
|
1472
|
+
if (this.lastCommit === null) this.lastCommit = this.seedCommitFromDisk();
|
|
1473
|
+
const generation = (this.lastCommit?.generation ?? 0) + 1;
|
|
1474
|
+
this.publishPayloadArtifact("manifest", MANIFEST_ARTIFACT, result.manifestJson, epoch, generation, envelopeJsonArtifact);
|
|
1475
|
+
if (this.persistAnalysisInputs) this.publishPayloadArtifact("inputs", ANALYSIS_INPUTS_ARTIFACT, JSON.stringify({
|
|
1476
|
+
analyzedHashes: Object.fromEntries(fileEntries.map((entry) => [entry.path, entry.hash])),
|
|
1477
|
+
...result.inputs
|
|
1478
|
+
}), epoch, generation, envelopeJsonArtifact);
|
|
1479
|
+
this.publishPayloadArtifact("styles", STYLES_ARTIFACT, fullCss, epoch, generation, envelopeCssArtifact);
|
|
1480
|
+
if (devMode) {
|
|
1481
|
+
const systemPropsHash = require_pipeline.contentHash(systemPropsContent);
|
|
1482
|
+
if (systemPropsHash !== this.lastSystemPropsHash) {
|
|
1483
|
+
this.writeSessionArtifact(SYSTEM_PROPS_ARTIFACT, systemPropsContent);
|
|
1484
|
+
this.lastSystemPropsHash = systemPropsHash;
|
|
1485
|
+
}
|
|
1486
|
+
} else this.writeSessionArtifact(SYSTEM_PROPS_ARTIFACT, systemPropsContent);
|
|
1487
|
+
this.publishAnalysisCommit(epoch, generation);
|
|
1488
|
+
this.publishReplacementEpoch(epoch);
|
|
1489
|
+
bt.total = this.elapsed(pipelineStart);
|
|
1490
|
+
logBuildTimings(bt, manifest?.timing, (msg) => this.log(msg), this.verbose);
|
|
1491
|
+
}
|
|
1492
|
+
/** Open a status attempt (attemptId increments once per burst — a
|
|
1493
|
+
* debouncing pre-write and the analysis that follows share one id). */
|
|
1494
|
+
beginStatusAttempt() {
|
|
1495
|
+
if (!this.statusAttemptOpen) {
|
|
1496
|
+
this.statusAttemptId += 1;
|
|
1497
|
+
this.statusAttemptOpen = true;
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1500
|
+
/** Write the session's analysis-status artifact (design D3). Terminal
|
|
1501
|
+
* states (idle/failed) close the attempt. */
|
|
1502
|
+
writeAnalysisStatus(state, pending, diagnostic) {
|
|
1503
|
+
const status = {
|
|
1504
|
+
schema: 2,
|
|
1505
|
+
sessionId: this.sessionId,
|
|
1506
|
+
attemptId: this.statusAttemptId,
|
|
1507
|
+
state,
|
|
1508
|
+
pending,
|
|
1509
|
+
deadlineAt: Date.now() + this.debounceCeilingMs + STATUS_WATCHDOG_MS,
|
|
1510
|
+
...diagnostic !== void 0 ? { diagnostic } : {},
|
|
1511
|
+
ready: this.firstEmissionComplete
|
|
1512
|
+
};
|
|
1513
|
+
this.writeSessionArtifact(ANALYSIS_STATUS_ARTIFACT, JSON.stringify(status));
|
|
1514
|
+
if (state === "idle" || state === "failed") this.statusAttemptOpen = false;
|
|
1515
|
+
}
|
|
1516
|
+
/**
|
|
1517
|
+
* Orchestrator seam (Turbopack watcher): record watch events observed
|
|
1518
|
+
* during the debounce window so a loader running ahead of the analysis
|
|
1519
|
+
* has positive evidence to wait on (design D3 'debouncing'). Paths are
|
|
1520
|
+
* filtered by the scan config and hashed at observation time.
|
|
1521
|
+
*/
|
|
1522
|
+
noteDebouncedWatchEvents(absPaths) {
|
|
1523
|
+
if (!this.rootDir) return;
|
|
1524
|
+
const additions = this.pendingFromBatch({ modifiedFiles: new Set(absPaths) });
|
|
1525
|
+
if (additions.length === 0) return;
|
|
1526
|
+
this.beginStatusAttempt();
|
|
1527
|
+
for (const [key, hash] of additions) this.debouncePending.set(key, hash);
|
|
1528
|
+
if (this.debounceStatusWriteScheduled) return;
|
|
1529
|
+
this.debounceStatusWriteScheduled = true;
|
|
1530
|
+
queueMicrotask(() => {
|
|
1531
|
+
this.debounceStatusWriteScheduled = false;
|
|
1532
|
+
if (this.debouncePending.size === 0) return;
|
|
1533
|
+
try {
|
|
1534
|
+
this.writeAnalysisStatus("debouncing", [...this.debouncePending.entries()]);
|
|
1535
|
+
} catch (err) {
|
|
1536
|
+
this.warn(`debounce status write failed: ${String(err)}`);
|
|
1537
|
+
}
|
|
1538
|
+
});
|
|
1539
|
+
}
|
|
1540
|
+
/** One pending microtask flushes a burst of debounce observations. */
|
|
1541
|
+
debounceStatusWriteScheduled = false;
|
|
1542
|
+
/**
|
|
1543
|
+
* Write one payload artifact into the session directory, enveloped with
|
|
1544
|
+
* `{sessionId, generation, replacementEpoch, payloadHash}` and rewritten
|
|
1545
|
+
* only when the PAYLOAD bytes changed (byte-identical re-analyses leave
|
|
1546
|
+
* disk untouched — spec: "Unchanged manifest is not rewritten"). The
|
|
1547
|
+
* recorded diskHash (hash of the enveloped bytes) feeds the
|
|
1548
|
+
* analysis-commit.
|
|
1549
|
+
*/
|
|
1550
|
+
publishPayloadArtifact(key, name, payload, epoch, generation, wrap) {
|
|
1551
|
+
const payloadHash = require_pipeline.contentHash(payload);
|
|
1552
|
+
if (this.artifactRecords[key] === null) this.artifactRecords[key] = this.seedPayloadRecord(key, name);
|
|
1553
|
+
const record = this.artifactRecords[key];
|
|
1554
|
+
if (record !== null && record.payloadHash === payloadHash) return;
|
|
1555
|
+
const envelope = {
|
|
1556
|
+
sessionId: this.sessionId,
|
|
1557
|
+
generation,
|
|
1558
|
+
replacementEpoch: epoch,
|
|
1559
|
+
payloadHash
|
|
1560
|
+
};
|
|
1561
|
+
const bytes = wrap(payload, JSON.stringify(envelope));
|
|
1562
|
+
this.writeSessionArtifact(name, bytes);
|
|
1563
|
+
this.artifactRecords[key] = {
|
|
1564
|
+
payloadHash,
|
|
1565
|
+
diskHash: require_pipeline.contentHash(bytes)
|
|
1566
|
+
};
|
|
1567
|
+
}
|
|
1568
|
+
/** Reconstruct a payload write guard from the on-disk artifact's envelope
|
|
1569
|
+
* (same-session restart: a byte-identical payload must not rewrite). */
|
|
1570
|
+
seedPayloadRecord(key, name) {
|
|
1571
|
+
let bytes;
|
|
1572
|
+
try {
|
|
1573
|
+
bytes = (0, fs.readFileSync)((0, path.join)(this.sessionDir, name), "utf-8");
|
|
1574
|
+
} catch {
|
|
1575
|
+
return null;
|
|
1576
|
+
}
|
|
1577
|
+
let envelope;
|
|
1578
|
+
try {
|
|
1579
|
+
envelope = key === "styles" ? readCssEnvelope(bytes) : readJsonEnvelope(bytes);
|
|
1580
|
+
} catch {
|
|
1581
|
+
return null;
|
|
1582
|
+
}
|
|
1583
|
+
if (!envelope || typeof envelope.payloadHash !== "string") return null;
|
|
1584
|
+
return {
|
|
1585
|
+
payloadHash: envelope.payloadHash,
|
|
1586
|
+
diskHash: require_pipeline.contentHash(bytes)
|
|
1587
|
+
};
|
|
1588
|
+
}
|
|
1589
|
+
/** Last analysis-commit persisted in this session's directory, or null. */
|
|
1590
|
+
seedCommitFromDisk() {
|
|
1591
|
+
try {
|
|
1592
|
+
const parsed = JSON.parse((0, fs.readFileSync)(analysisCommitPath(this.sessionDir), "utf-8"));
|
|
1593
|
+
return parsed.schema === 1 && parsed.sessionId === this.sessionId && typeof parsed.generation === "number" ? parsed : null;
|
|
1594
|
+
} catch {
|
|
1595
|
+
return null;
|
|
1596
|
+
}
|
|
1597
|
+
}
|
|
1598
|
+
/**
|
|
1599
|
+
* Publish the analysis-commit — the transaction identity (design D1).
|
|
1600
|
+
* Written AFTER every payload and BEFORE the epoch; skipped entirely when
|
|
1601
|
+
* the payload set (disk hashes) and epoch are unchanged, so a no-op
|
|
1602
|
+
* re-analysis neither re-keys loader-worker hydration nor burns a
|
|
1603
|
+
* generation.
|
|
1604
|
+
*/
|
|
1605
|
+
publishAnalysisCommit(epoch, generation) {
|
|
1606
|
+
const manifestHash = this.artifactRecords.manifest?.diskHash ?? "";
|
|
1607
|
+
const inputsHash = this.persistAnalysisInputs ? this.artifactRecords.inputs?.diskHash ?? "" : void 0;
|
|
1608
|
+
const stylesHash = this.artifactRecords.styles?.diskHash ?? "";
|
|
1609
|
+
const prev = this.lastCommit;
|
|
1610
|
+
if (prev !== null && prev.manifestHash === manifestHash && prev.inputsHash === inputsHash && prev.stylesHash === stylesHash && prev.replacementEpoch === epoch) return;
|
|
1611
|
+
const commit = {
|
|
1612
|
+
schema: 1,
|
|
1613
|
+
sessionId: this.sessionId,
|
|
1614
|
+
generation,
|
|
1615
|
+
replacementEpoch: epoch,
|
|
1616
|
+
manifestHash,
|
|
1617
|
+
...inputsHash !== void 0 ? { inputsHash } : {},
|
|
1618
|
+
stylesHash
|
|
1619
|
+
};
|
|
1620
|
+
this.writeSessionArtifact(ANALYSIS_COMMIT_ARTIFACT, JSON.stringify(commit));
|
|
1621
|
+
this.lastCommit = commit;
|
|
1622
|
+
}
|
|
1623
|
+
/**
|
|
1624
|
+
* Publish the canonical replacement epoch: maintain the session-scoped
|
|
1625
|
+
* disk witness `{schema, sessionId, epoch}`, rewritten ONLY when the
|
|
1626
|
+
* epoch VALUE changes so style-only analyses and same-session restarts
|
|
1627
|
+
* leave bytes and mtime untouched, then expose the value through the
|
|
1628
|
+
* singleton for the plugin's needBuild fan-out and the loader's catch-up
|
|
1629
|
+
* re-check. Every VALUE move also reconciles sibling sessions' epoch
|
|
1630
|
+
* artifacts (below) — the webpack cold-cache validity witness.
|
|
1631
|
+
*/
|
|
1632
|
+
publishReplacementEpoch(epoch) {
|
|
1633
|
+
const onDisk = this.diskEpochValue();
|
|
1634
|
+
if (this.lastEpochValue === null) this.lastEpochValue = onDisk;
|
|
1635
|
+
if (epoch !== onDisk) this.writeSessionArtifact(REPLACEMENT_EPOCH_ARTIFACT, JSON.stringify({
|
|
1636
|
+
schema: 1,
|
|
1637
|
+
sessionId: this.sessionId,
|
|
1638
|
+
epoch
|
|
1639
|
+
}));
|
|
1640
|
+
if (epoch !== this.lastEpochValue) {
|
|
1641
|
+
this.lastEpochValue = epoch;
|
|
1642
|
+
this.reconcileSiblingEpochs(epoch);
|
|
1643
|
+
}
|
|
1644
|
+
setReplacementEpoch(epoch);
|
|
1645
|
+
}
|
|
1646
|
+
/**
|
|
1647
|
+
* Sibling epoch reconciliation — the cross-session half of the webpack
|
|
1648
|
+
* persistent-cache witness (spec: dev-served-transform-coherence,
|
|
1649
|
+
* "Offline change invalidates restored modules"). Restored-module
|
|
1650
|
+
* snapshots reference the epoch artifact of the session that BUILT them;
|
|
1651
|
+
* with session-scoped trees that file would otherwise sit untouched
|
|
1652
|
+
* forever, keeping stale snapshots valid. Whenever this session's epoch
|
|
1653
|
+
* value moves, delete every sibling epoch artifact that disagrees (their
|
|
1654
|
+
* snapshots must invalidate — restore-on-demand then rebuilds from the
|
|
1655
|
+
* current generation) and leave agreeing siblings byte-untouched (warm
|
|
1656
|
+
* restores stay valid). Deleting a stale artifact is pruning, not a
|
|
1657
|
+
* foreign-session write; races with concurrent sessions are tolerated
|
|
1658
|
+
* (S14).
|
|
1659
|
+
*/
|
|
1660
|
+
/** Sibling ids already reconciled away (artifact deleted or absent) —
|
|
1661
|
+
* nothing left to invalidate for them on later moves. */
|
|
1662
|
+
reconciledSiblingIds = /* @__PURE__ */ new Set();
|
|
1663
|
+
/** sessions-root listing memo, keyed by the root dir's mtime — a new or
|
|
1664
|
+
* pruned sibling DIRECTORY moves it; agreeing siblings stay listed. */
|
|
1665
|
+
siblingListing = null;
|
|
1666
|
+
reconcileSiblingEpochs(epoch) {
|
|
1667
|
+
const rootPath = sessionsRootDir(this.rootDir);
|
|
1668
|
+
let entries;
|
|
1669
|
+
try {
|
|
1670
|
+
const mtimeMs = (0, fs.statSync)(rootPath).mtimeMs;
|
|
1671
|
+
if (this.siblingListing?.mtimeMs === mtimeMs) entries = this.siblingListing.entries;
|
|
1672
|
+
else {
|
|
1673
|
+
entries = (0, fs.readdirSync)(rootPath);
|
|
1674
|
+
this.siblingListing = {
|
|
1675
|
+
mtimeMs,
|
|
1676
|
+
entries
|
|
1677
|
+
};
|
|
1678
|
+
}
|
|
1679
|
+
} catch {
|
|
1680
|
+
return;
|
|
1681
|
+
}
|
|
1682
|
+
for (const entry of entries) {
|
|
1683
|
+
if (entry === this.sessionId) continue;
|
|
1684
|
+
if (this.reconciledSiblingIds.has(entry)) continue;
|
|
1685
|
+
const siblingEpochPath = (0, path.join)(rootPath, entry, REPLACEMENT_EPOCH_ARTIFACT);
|
|
1686
|
+
try {
|
|
1687
|
+
if (JSON.parse((0, fs.readFileSync)(siblingEpochPath, "utf-8")).epoch === epoch) continue;
|
|
1688
|
+
} catch (err) {
|
|
1689
|
+
if (err?.code === "ENOENT") {
|
|
1690
|
+
this.reconciledSiblingIds.add(entry);
|
|
1691
|
+
continue;
|
|
1692
|
+
}
|
|
1693
|
+
}
|
|
1694
|
+
try {
|
|
1695
|
+
(0, fs.unlinkSync)(siblingEpochPath);
|
|
1696
|
+
} catch {}
|
|
1697
|
+
this.reconciledSiblingIds.add(entry);
|
|
1698
|
+
}
|
|
1699
|
+
}
|
|
1700
|
+
/** Epoch value held by this session's on-disk artifact, or null when
|
|
1701
|
+
* absent, unreadable, or not the expected schema. */
|
|
1702
|
+
diskEpochValue() {
|
|
1703
|
+
try {
|
|
1704
|
+
const parsed = JSON.parse((0, fs.readFileSync)(replacementEpochPath(this.sessionDir), "utf-8"));
|
|
1705
|
+
return parsed.schema === 1 && typeof parsed.epoch === "string" ? parsed.epoch : null;
|
|
1706
|
+
} catch {
|
|
1707
|
+
return null;
|
|
1708
|
+
}
|
|
1709
|
+
}
|
|
1710
|
+
/** True when the flat `.animus/` advisory lock names a live pid — a CLI
|
|
1711
|
+
* invocation (build mid-publish, or a whole watch run) owns the flat
|
|
1712
|
+
* tree right now, and any instantaneous inconsistency is its in-flight
|
|
1713
|
+
* write, not debris. */
|
|
1714
|
+
cliWriterHoldsLock(animusDir) {
|
|
1715
|
+
let holder;
|
|
1716
|
+
try {
|
|
1717
|
+
holder = JSON.parse((0, fs.readFileSync)((0, path.join)(animusDir, CLI_LOCK_ARTIFACT), "utf-8"));
|
|
1718
|
+
} catch {
|
|
1719
|
+
return false;
|
|
1720
|
+
}
|
|
1721
|
+
if (typeof holder.pid !== "number") return false;
|
|
1722
|
+
try {
|
|
1723
|
+
process.kill(holder.pid, 0);
|
|
1724
|
+
return true;
|
|
1725
|
+
} catch {
|
|
1726
|
+
return false;
|
|
1727
|
+
}
|
|
1728
|
+
}
|
|
1729
|
+
/** Session-start hygiene (design D2): delete legacy flat artifacts
|
|
1730
|
+
* (unreachable by session-scoped loaders; the standalone CLI is the one
|
|
1731
|
+
* remaining writer of flat payload names, and its live output is
|
|
1732
|
+
* fenced off below) and prune sibling session directories older than
|
|
1733
|
+
* the retention window — never the own dir, tolerating races with
|
|
1734
|
+
* concurrently-pruning sessions (S14). */
|
|
1735
|
+
runSessionStartHygiene() {
|
|
1736
|
+
if (this.sessionStartHygieneDone) return;
|
|
1737
|
+
this.sessionStartHygieneDone = true;
|
|
1738
|
+
const animusDir = (0, path.join)(this.rootDir, ".animus");
|
|
1739
|
+
const isCliPublishedSet = this.cliWriterHoldsLock(animusDir) || verifyCommitRecord(animusDir).length === 0;
|
|
1740
|
+
const cliPublishedNames = [
|
|
1741
|
+
MANIFEST_ARTIFACT,
|
|
1742
|
+
STYLES_ARTIFACT,
|
|
1743
|
+
SYSTEM_PROPS_ARTIFACT
|
|
1744
|
+
];
|
|
1745
|
+
for (const name of LEGACY_FLAT_ARTIFACTS) {
|
|
1746
|
+
if (isCliPublishedSet && cliPublishedNames.includes(name)) continue;
|
|
1747
|
+
try {
|
|
1748
|
+
(0, fs.unlinkSync)((0, path.join)(animusDir, name));
|
|
1749
|
+
} catch {}
|
|
1750
|
+
}
|
|
1751
|
+
if (!isCliPublishedSet) try {
|
|
1752
|
+
(0, fs.unlinkSync)((0, path.join)(animusDir, CLI_COMMIT_ARTIFACT));
|
|
1753
|
+
} catch {}
|
|
1754
|
+
let entries;
|
|
1755
|
+
try {
|
|
1756
|
+
entries = (0, fs.readdirSync)(sessionsRootDir(this.rootDir));
|
|
1757
|
+
} catch {
|
|
1758
|
+
return;
|
|
1759
|
+
}
|
|
1760
|
+
const cutoff = Date.now() - SESSION_DIR_MAX_AGE_MS;
|
|
1761
|
+
for (const entry of entries) {
|
|
1762
|
+
if (entry === this.sessionId) continue;
|
|
1763
|
+
const dir = (0, path.join)(sessionsRootDir(this.rootDir), entry);
|
|
1764
|
+
try {
|
|
1765
|
+
if ((0, fs.statSync)(dir).mtimeMs < cutoff) (0, fs.rmSync)(dir, {
|
|
1766
|
+
recursive: true,
|
|
1767
|
+
force: true
|
|
1768
|
+
});
|
|
1769
|
+
} catch {}
|
|
1770
|
+
}
|
|
1771
|
+
}
|
|
1772
|
+
/** Ensure the session directory exists and write one artifact into it.
|
|
1773
|
+
* Write-then-rename so cross-process readers (Turbopack loader workers)
|
|
1774
|
+
* can never observe a torn half-written file. The tmp name carries the
|
|
1775
|
+
* pid — Next dev evaluates the config in more than one process, and two
|
|
1776
|
+
* sessions writing the same artifact must not race on one tmp path. */
|
|
1777
|
+
writeSessionArtifact(name, content) {
|
|
1778
|
+
const dir = this.sessionDir;
|
|
1779
|
+
(0, fs.mkdirSync)(dir, { recursive: true });
|
|
1780
|
+
const tmpPath = (0, path.join)(dir, `.${name}.${process.pid}.tmp`);
|
|
1781
|
+
(0, fs.writeFileSync)(tmpPath, content);
|
|
1782
|
+
(0, fs.renameSync)(tmpPath, (0, path.join)(dir, name));
|
|
1783
|
+
this.onArtifactWrite?.(name, content);
|
|
1784
|
+
}
|
|
1785
|
+
/**
|
|
1786
|
+
* asset() placeholder substitution (global-styles-system): resolve each
|
|
1787
|
+
* referenced specifier through Node resolution, copy the bytes into the
|
|
1788
|
+
* session directory's `assets/` under a content-hashed name, and
|
|
1789
|
+
* substitute a RELATIVE url — relative to the session-scoped styles.css,
|
|
1790
|
+
* which sits beside `assets/`, so the emitted `./assets/<file>` form is
|
|
1791
|
+
* unchanged. The stylesheet is processed by Next's own CSS pipeline
|
|
1792
|
+
* (webpack and Turbopack alike), which applies its native asset handling
|
|
1793
|
+
* — publicPath and output hashing — to relative url() references.
|
|
1794
|
+
* Unsubstitutable specifiers warn and emit literally in non-strict mode,
|
|
1795
|
+
* fail the build under `strict: true`.
|
|
1796
|
+
*/
|
|
1797
|
+
substituteAssetReferences(globalCss, devMode) {
|
|
1798
|
+
const specifiers = require_pipeline.findAssetSpecifiers(globalCss);
|
|
1799
|
+
const assetsDir = (0, path.join)(this.sessionDir, SESSION_ASSETS_DIR);
|
|
1800
|
+
const expected = /* @__PURE__ */ new Set();
|
|
1801
|
+
this.assetDependencyPaths.clear();
|
|
1802
|
+
this.assetDependencyKeys.clear();
|
|
1803
|
+
const urlBySpecifier = /* @__PURE__ */ new Map();
|
|
1804
|
+
for (const specifier of specifiers) {
|
|
1805
|
+
const cached = this.assetCopyCache.get(specifier);
|
|
1806
|
+
if (cached && (0, fs.existsSync)((0, path.join)(assetsDir, cached.fileName))) try {
|
|
1807
|
+
const current = (0, fs.statSync)(cached.sourcePath);
|
|
1808
|
+
if (current.mtimeMs === cached.mtimeMs && current.size === cached.size) {
|
|
1809
|
+
this.trackAssetDependency(cached.sourcePath);
|
|
1810
|
+
expected.add(cached.fileName);
|
|
1811
|
+
urlBySpecifier.set(specifier, cached.url);
|
|
1812
|
+
continue;
|
|
1813
|
+
}
|
|
1814
|
+
} catch {}
|
|
1815
|
+
const resolvedPath = this.resolveAssetSpecifier(specifier);
|
|
1816
|
+
if (!resolvedPath) {
|
|
1817
|
+
const message = `unresolvable asset() specifier: ${specifier}`;
|
|
1818
|
+
if (this.options.strict) throw new Error(`[${this.driverLabel}] ${message}`);
|
|
1819
|
+
this.warn(message);
|
|
1820
|
+
urlBySpecifier.set(specifier, specifier);
|
|
1821
|
+
continue;
|
|
1822
|
+
}
|
|
1823
|
+
const bytes = (0, fs.readFileSync)(resolvedPath);
|
|
1824
|
+
const sourceStat = (0, fs.statSync)(resolvedPath);
|
|
1825
|
+
this.trackAssetDependency(resolvedPath);
|
|
1826
|
+
const ext = (0, path.extname)(resolvedPath);
|
|
1827
|
+
const fileName = `${(0, path.basename)(resolvedPath, ext)}.${require_pipeline.contentHash(bytes).slice(0, 8)}${ext}`;
|
|
1828
|
+
expected.add(fileName);
|
|
1829
|
+
if (!(0, fs.existsSync)(assetsDir)) (0, fs.mkdirSync)(assetsDir, { recursive: true });
|
|
1830
|
+
const assetPath = (0, path.join)(assetsDir, fileName);
|
|
1831
|
+
if (!(0, fs.existsSync)(assetPath)) (0, fs.writeFileSync)(assetPath, bytes);
|
|
1832
|
+
const url = `./${SESSION_ASSETS_DIR}/${fileName}`;
|
|
1833
|
+
urlBySpecifier.set(specifier, url);
|
|
1834
|
+
this.assetCopyCache.set(specifier, {
|
|
1835
|
+
sourcePath: resolvedPath,
|
|
1836
|
+
mtimeMs: sourceStat.mtimeMs,
|
|
1837
|
+
size: sourceStat.size,
|
|
1838
|
+
fileName,
|
|
1839
|
+
url
|
|
1840
|
+
});
|
|
1841
|
+
}
|
|
1842
|
+
if (!devMode) pruneStaleAssets(assetsDir, expected);
|
|
1843
|
+
return require_pipeline.substituteAssetPlaceholders(globalCss, urlBySpecifier);
|
|
1844
|
+
}
|
|
1845
|
+
trackAssetDependency(path$9) {
|
|
1846
|
+
this.assetDependencyPaths.add(path$9);
|
|
1847
|
+
for (const key of require_pipeline.toWatchKeys(path$9)) this.assetDependencyKeys.add(key);
|
|
1848
|
+
}
|
|
1849
|
+
/**
|
|
1850
|
+
* Resolve an asset specifier to an absolute file via the shared pipeline
|
|
1851
|
+
* resolver (host aliases → Node resolution → package root), with one
|
|
1852
|
+
* session-local last resort: an already-discovered source entry's package
|
|
1853
|
+
* root (dist-less workspace kits the shared resolver cannot see).
|
|
1854
|
+
*/
|
|
1855
|
+
resolveAssetSpecifier(specifier) {
|
|
1856
|
+
const resolved = require_pipeline.resolveAssetFile(specifier, this.rootDir, this.pathAliasesJson);
|
|
1857
|
+
if (resolved) return resolved;
|
|
1858
|
+
const segments = specifier.split("/");
|
|
1859
|
+
const packageName = specifier.startsWith("@") ? segments.slice(0, 2).join("/") : segments[0];
|
|
1860
|
+
const subpath = specifier.slice(packageName.length + 1);
|
|
1861
|
+
if (!subpath) return null;
|
|
1862
|
+
const sourceEntry = this.externalSourceEntries.get(packageName) ?? [...this.externalSourceEntries].find(([declared]) => declared === packageName || declared.startsWith(`${packageName}/`))?.[1];
|
|
1863
|
+
if (sourceEntry) {
|
|
1864
|
+
const candidate = (0, path.join)(require_pipeline.findPackageRoot(sourceEntry), subpath);
|
|
1865
|
+
if ((0, fs.existsSync)(candidate)) return candidate;
|
|
1866
|
+
}
|
|
1867
|
+
return null;
|
|
1868
|
+
}
|
|
1869
|
+
};
|
|
1870
|
+
/**
|
|
1871
|
+
* Sync `.animus/assets/` to the current build's content-hashed file set:
|
|
1872
|
+
* anything else in the directory is a superseded revision (the copies are
|
|
1873
|
+
* content-addressed and never overwritten) or the leftover of an asset()
|
|
1874
|
+
* reference that no longer exists. Failures are tolerated per entry — Next
|
|
1875
|
+
* dev evaluates the config in more than one process, and a concurrent
|
|
1876
|
+
* session may have removed (or be about to rewrite) the same file; every
|
|
1877
|
+
* pass rewrites whatever of its own set is missing, so races self-heal.
|
|
1878
|
+
*/
|
|
1879
|
+
function pruneStaleAssets(assetsDir, expected) {
|
|
1880
|
+
if (!(0, fs.existsSync)(assetsDir)) return;
|
|
1881
|
+
let entries;
|
|
1882
|
+
try {
|
|
1883
|
+
entries = (0, fs.readdirSync)(assetsDir);
|
|
1884
|
+
} catch {
|
|
1885
|
+
return;
|
|
1886
|
+
}
|
|
1887
|
+
for (const entry of entries) {
|
|
1888
|
+
if (expected.has(entry)) continue;
|
|
1889
|
+
try {
|
|
1890
|
+
(0, fs.unlinkSync)((0, path.join)(assetsDir, entry));
|
|
1891
|
+
} catch {}
|
|
1892
|
+
}
|
|
1893
|
+
}
|
|
1894
|
+
//#endregion
|
|
1895
|
+
//#region session/turbopack-orchestrator.ts
|
|
1896
|
+
/**
|
|
1897
|
+
* Out-of-band pipeline orchestration for Turbopack (spec:
|
|
1898
|
+
* next-turbopack-integration). Turbopack exposes no compiler hooks, so the
|
|
1899
|
+
* pipeline runs during next.config resolution and, in dev, from a
|
|
1900
|
+
* filesystem watcher — the bundler only ever sees the `.animus/` disk
|
|
1901
|
+
* artifacts.
|
|
1902
|
+
*/
|
|
1903
|
+
/** Run the full pipeline with the Turbopack emitter identity: the virtual
|
|
1904
|
+
* system-props id (Turbopack rejects the webpack path's absolute-path
|
|
1905
|
+
* imports). Turbopack orchestration persists the analysis-inputs
|
|
1906
|
+
* hydration corpus — its isolated loader workers replay it (spec:
|
|
1907
|
+
* next-turbopack-integration, "Manifest disk artifact"; webpack mode
|
|
1908
|
+
* skips the corpus). */
|
|
1909
|
+
async function runSessionPipeline(session) {
|
|
1910
|
+
session.systemPropsModuleId = TURBOPACK_SYSTEM_PROPS_ID;
|
|
1911
|
+
session.persistAnalysisInputs = true;
|
|
1912
|
+
await session.runFullPipeline();
|
|
1913
|
+
}
|
|
1914
|
+
const activeWatcherRoots = /* @__PURE__ */ new Set();
|
|
1915
|
+
const IGNORED_SEGMENTS = new Set([
|
|
1916
|
+
".animus",
|
|
1917
|
+
".next",
|
|
1918
|
+
"node_modules"
|
|
1919
|
+
]);
|
|
1920
|
+
/**
|
|
1921
|
+
* Start the dev watcher: fs.watch per eligible top-level directory (plus a
|
|
1922
|
+
* non-recursive watch on the root itself), debounced into
|
|
1923
|
+
* existence-partitioned modified/removed sets feeding
|
|
1924
|
+
* `session.handleWatchUpdate` (serialized — updates never overlap).
|
|
1925
|
+
* Generated and vendored trees (`.animus`, `.next`, node_modules) are never
|
|
1926
|
+
* registered with the OS — recursive registration of those trees exhausts
|
|
1927
|
+
* inotify/kqueue descriptors (EMFILE/ENOSPC) on large projects.
|
|
1928
|
+
* Idempotent per project root; unref'd so it never holds the process open.
|
|
1929
|
+
* Asynchronous FSWatcher errors degrade to no-watch with a warning instead
|
|
1930
|
+
* of crashing the dev server. Returns a close handle, or null when this
|
|
1931
|
+
* root is already watched or the platform lacks recursive fs.watch (Linux
|
|
1932
|
+
* before Node 20 — degrades to no-watch with a warning).
|
|
1933
|
+
*/
|
|
1934
|
+
function startTurbopackWatcher(session, rootDir, debounceMs = 75, watchFn = fs.watch) {
|
|
1935
|
+
if (activeWatcherRoots.has(rootDir)) return null;
|
|
1936
|
+
activeWatcherRoots.add(rootDir);
|
|
1937
|
+
if (typeof session.noteDebouncedWatchEvents === "function") session.debounceCeilingMs = debounceMs;
|
|
1938
|
+
const pendingPaths = /* @__PURE__ */ new Set();
|
|
1939
|
+
const watchers = /* @__PURE__ */ new Map();
|
|
1940
|
+
let timer = null;
|
|
1941
|
+
let updateChain = Promise.resolve();
|
|
1942
|
+
let closed = false;
|
|
1943
|
+
let died = false;
|
|
1944
|
+
const externalWatchers = /* @__PURE__ */ new Map();
|
|
1945
|
+
let pendingOpened = /* @__PURE__ */ new Map();
|
|
1946
|
+
let capturedDuringSnapshot = [];
|
|
1947
|
+
const externalDiagnosticKey = (root) => `external-watch:${root}`;
|
|
1948
|
+
const failureReason = (err) => {
|
|
1949
|
+
const code = err?.code;
|
|
1950
|
+
if (code === "EMFILE" || code === "ENFILE" || code === "ENOSPC" || code === "EPERM") return `capacity(${code})`;
|
|
1951
|
+
if (/inotify|too many/i.test(String(err))) return "capacity";
|
|
1952
|
+
return code ?? "error";
|
|
1953
|
+
};
|
|
1954
|
+
const degradeExternalRoot = (root, err) => {
|
|
1955
|
+
externalWatchers.get(root)?.close();
|
|
1956
|
+
externalWatchers.delete(root);
|
|
1957
|
+
pendingOpened.get(root)?.close();
|
|
1958
|
+
pendingOpened.delete(root);
|
|
1959
|
+
const message = `ANIMUS_EXTERNAL_WATCH_UNAVAILABLE root=${(0, path.relative)(rootDir, root)} reason=${failureReason(err)} effect=changes in this workspace source may require restart`;
|
|
1960
|
+
session.stickyDiagnostics?.set(externalDiagnosticKey(root), message);
|
|
1961
|
+
console.warn(`[animus-extract] ${message}`);
|
|
1962
|
+
};
|
|
1963
|
+
const flush = () => {
|
|
1964
|
+
timer = null;
|
|
1965
|
+
const modifiedFiles = /* @__PURE__ */ new Set();
|
|
1966
|
+
const removedFiles = /* @__PURE__ */ new Set();
|
|
1967
|
+
for (const path$1 of pendingPaths) ((0, fs.existsSync)(path$1) ? modifiedFiles : removedFiles).add(path$1);
|
|
1968
|
+
pendingPaths.clear();
|
|
1969
|
+
updateChain = updateChain.then(() => session.handleWatchUpdate({
|
|
1970
|
+
modifiedFiles,
|
|
1971
|
+
removedFiles
|
|
1972
|
+
}).catch((err) => {
|
|
1973
|
+
console.warn(`[animus-extract] Turbopack watch update failed: ${String(err)}`);
|
|
1974
|
+
}).then(() => {
|
|
1975
|
+
if (pendingOpened.size > 0 || capturedDuringSnapshot.length > 0) rollbackPendingExternal();
|
|
1976
|
+
}));
|
|
1977
|
+
};
|
|
1978
|
+
const rollbackPendingExternal = () => {
|
|
1979
|
+
for (const watcher of pendingOpened.values()) watcher.close();
|
|
1980
|
+
pendingOpened = /* @__PURE__ */ new Map();
|
|
1981
|
+
capturedDuringSnapshot = [];
|
|
1982
|
+
};
|
|
1983
|
+
const closeAll = () => {
|
|
1984
|
+
if (closed) return;
|
|
1985
|
+
closed = true;
|
|
1986
|
+
if (timer) clearTimeout(timer);
|
|
1987
|
+
timer = null;
|
|
1988
|
+
for (const watcher of watchers.values()) watcher.close();
|
|
1989
|
+
watchers.clear();
|
|
1990
|
+
for (const watcher of externalWatchers.values()) watcher.close();
|
|
1991
|
+
externalWatchers.clear();
|
|
1992
|
+
rollbackPendingExternal();
|
|
1993
|
+
session.onExternalRootResolved = null;
|
|
1994
|
+
session.onExternalRootsCommitted = null;
|
|
1995
|
+
activeWatcherRoots.delete(rootDir);
|
|
1996
|
+
};
|
|
1997
|
+
const onWatcherError = (err) => {
|
|
1998
|
+
died = true;
|
|
1999
|
+
closeAll();
|
|
2000
|
+
console.warn(`[animus-extract] Turbopack dev watcher failed (${String(err)}); source edits require a dev-server restart`);
|
|
2001
|
+
handle.onDied?.();
|
|
2002
|
+
};
|
|
2003
|
+
const enqueuePath = (abs) => {
|
|
2004
|
+
pendingPaths.add(abs);
|
|
2005
|
+
try {
|
|
2006
|
+
session.noteDebouncedWatchEvents?.([abs]);
|
|
2007
|
+
} catch (err) {
|
|
2008
|
+
console.warn(`[animus-extract] Turbopack debounce status update failed: ${String(err)}`);
|
|
2009
|
+
}
|
|
2010
|
+
if (!timer) {
|
|
2011
|
+
timer = setTimeout(flush, debounceMs);
|
|
2012
|
+
timer.unref?.();
|
|
2013
|
+
}
|
|
2014
|
+
};
|
|
2015
|
+
const onEvent = (baseDir, filename) => {
|
|
2016
|
+
if (closed || !filename) return;
|
|
2017
|
+
const rel = filename.toString();
|
|
2018
|
+
if (rel.split(/[\\/]/).some((segment) => IGNORED_SEGMENTS.has(segment))) return;
|
|
2019
|
+
const abs = (0, path.join)(baseDir, rel);
|
|
2020
|
+
if (baseDir === rootDir && !watchers.has(abs)) try {
|
|
2021
|
+
if ((0, fs.statSync)(abs).isDirectory()) addWatcher(abs, true);
|
|
2022
|
+
} catch {}
|
|
2023
|
+
enqueuePath(abs);
|
|
2024
|
+
};
|
|
2025
|
+
/** External-root event path: fenced by watcher identity ({watcherRootId,
|
|
2026
|
+
* generation} — a handle that is neither current nor pending belongs to
|
|
2027
|
+
* a retired generation and its events are rejected), `filename == null`
|
|
2028
|
+
* marks the ROOT dirty for rediscovery, and events observed while the
|
|
2029
|
+
* root's generation is still snapshotting are buffered for replay. */
|
|
2030
|
+
const onExternalEvent = (root, self, filename) => {
|
|
2031
|
+
if (closed) return;
|
|
2032
|
+
const watcher = self();
|
|
2033
|
+
const active = watcher !== void 0 && externalWatchers.get(root) === watcher;
|
|
2034
|
+
const isPending = watcher !== void 0 && pendingOpened.get(root) === watcher;
|
|
2035
|
+
if (!active && !isPending) return;
|
|
2036
|
+
let abs;
|
|
2037
|
+
if (filename == null) abs = root;
|
|
2038
|
+
else {
|
|
2039
|
+
const rel = filename.toString();
|
|
2040
|
+
if (rel.split(/[\\/]/).some((segment) => IGNORED_SEGMENTS.has(segment))) return;
|
|
2041
|
+
abs = rel === "" ? root : (0, path.join)(root, rel);
|
|
2042
|
+
}
|
|
2043
|
+
if (isPending && !active) {
|
|
2044
|
+
capturedDuringSnapshot.push(abs);
|
|
2045
|
+
return;
|
|
2046
|
+
}
|
|
2047
|
+
enqueuePath(abs);
|
|
2048
|
+
};
|
|
2049
|
+
/** Open one external-root watcher into `into` (the committed set at
|
|
2050
|
+
* startup, the pending set during reset reconciliation). Failures
|
|
2051
|
+
* degrade ONLY this root (design D7); node_modules-resident roots are
|
|
2052
|
+
* documented unwatchable and never registered (guardrail G2). */
|
|
2053
|
+
const openExternalWatcher = (root, into) => {
|
|
2054
|
+
if (root.split(/[\\/]/).includes("node_modules")) return;
|
|
2055
|
+
try {
|
|
2056
|
+
let watcher;
|
|
2057
|
+
watcher = watchFn(root, { recursive: true }, (_event, filename) => onExternalEvent(root, () => watcher, filename));
|
|
2058
|
+
watcher.on("error", (err) => degradeExternalRoot(root, err));
|
|
2059
|
+
watcher.unref?.();
|
|
2060
|
+
into.set(root, watcher);
|
|
2061
|
+
session.stickyDiagnostics?.delete(externalDiagnosticKey(root));
|
|
2062
|
+
} catch (err) {
|
|
2063
|
+
degradeExternalRoot(root, err);
|
|
2064
|
+
}
|
|
2065
|
+
};
|
|
2066
|
+
const addWatcher = (dir, recursive) => {
|
|
2067
|
+
const watcher = watchFn(dir, { recursive }, (_event, filename) => onEvent(dir, filename));
|
|
2068
|
+
watcher.on("error", onWatcherError);
|
|
2069
|
+
watcher.unref?.();
|
|
2070
|
+
watchers.set(dir, watcher);
|
|
2071
|
+
};
|
|
2072
|
+
try {
|
|
2073
|
+
addWatcher(rootDir, false);
|
|
2074
|
+
for (const entry of (0, fs.readdirSync)(rootDir, { withFileTypes: true })) {
|
|
2075
|
+
if (!entry.isDirectory() || IGNORED_SEGMENTS.has(entry.name)) continue;
|
|
2076
|
+
addWatcher((0, path.join)(rootDir, entry.name), true);
|
|
2077
|
+
}
|
|
2078
|
+
} catch (err) {
|
|
2079
|
+
closeAll();
|
|
2080
|
+
console.warn(`[animus-extract] Turbopack dev watcher unavailable (${String(err)}); source edits require a dev-server restart`);
|
|
2081
|
+
return null;
|
|
2082
|
+
}
|
|
2083
|
+
for (const root of session.externalWatchRoots ?? []) openExternalWatcher(root, externalWatchers);
|
|
2084
|
+
session.onExternalRootResolved = (root) => {
|
|
2085
|
+
if (closed) return;
|
|
2086
|
+
if (externalWatchers.has(root) || pendingOpened.has(root)) return;
|
|
2087
|
+
openExternalWatcher(root, pendingOpened);
|
|
2088
|
+
};
|
|
2089
|
+
session.onExternalRootsCommitted = (roots) => {
|
|
2090
|
+
if (closed) return;
|
|
2091
|
+
const admitted = new Set(roots);
|
|
2092
|
+
for (const [root, watcher] of pendingOpened) externalWatchers.set(root, watcher);
|
|
2093
|
+
pendingOpened = /* @__PURE__ */ new Map();
|
|
2094
|
+
for (const [root, watcher] of [...externalWatchers]) if (!admitted.has(root)) {
|
|
2095
|
+
watcher.close();
|
|
2096
|
+
externalWatchers.delete(root);
|
|
2097
|
+
}
|
|
2098
|
+
for (const abs of capturedDuringSnapshot.splice(0)) enqueuePath(abs);
|
|
2099
|
+
};
|
|
2100
|
+
const handle = {
|
|
2101
|
+
close: closeAll,
|
|
2102
|
+
get died() {
|
|
2103
|
+
return died;
|
|
2104
|
+
},
|
|
2105
|
+
onDied: null,
|
|
2106
|
+
settle: () => updateChain
|
|
2107
|
+
};
|
|
2108
|
+
return handle;
|
|
2109
|
+
}
|
|
2110
|
+
//#endregion
|
|
2111
|
+
exports.ANIMUS_CSS_MODULE_ID = ANIMUS_CSS_MODULE_ID;
|
|
2112
|
+
exports.CLI_COMMIT_ARTIFACT = CLI_COMMIT_ARTIFACT;
|
|
2113
|
+
exports.CLI_LOCK_ARTIFACT = CLI_LOCK_ARTIFACT;
|
|
2114
|
+
exports.ExtractionSession = ExtractionSession;
|
|
2115
|
+
exports.MANIFEST_ARTIFACT = MANIFEST_ARTIFACT;
|
|
2116
|
+
exports.SESSION_ASSETS_DIR = SESSION_ASSETS_DIR;
|
|
2117
|
+
exports.STYLES_ARTIFACT = STYLES_ARTIFACT;
|
|
2118
|
+
exports.SYSTEM_PROPS_ARTIFACT = SYSTEM_PROPS_ARTIFACT;
|
|
2119
|
+
exports.TURBOPACK_SYSTEM_PROPS_ID = TURBOPACK_SYSTEM_PROPS_ID;
|
|
2120
|
+
exports.analysisCommitPath = analysisCommitPath;
|
|
2121
|
+
exports.analysisInputsPath = analysisInputsPath;
|
|
2122
|
+
exports.analysisStatusPath = analysisStatusPath;
|
|
2123
|
+
exports.claimExclusiveSessionOwner = claimExclusiveSessionOwner;
|
|
2124
|
+
exports.collectSessionAssets = collectSessionAssets;
|
|
2125
|
+
exports.engineApi = engineApi;
|
|
2126
|
+
exports.getAnalysisPromise = getAnalysisPromise;
|
|
2127
|
+
exports.getAnalyzedHashes = getAnalyzedHashes;
|
|
2128
|
+
exports.getManifestJson = getManifestJson;
|
|
2129
|
+
exports.getReplacementEpoch = getReplacementEpoch;
|
|
2130
|
+
exports.getSessionArtifactDir = getSessionArtifactDir;
|
|
2131
|
+
exports.getSharedCss = getSharedCss;
|
|
2132
|
+
exports.getSharedExternalDirs = getSharedExternalDirs;
|
|
2133
|
+
exports.getSharedExternalEntries = getSharedExternalEntries;
|
|
2134
|
+
exports.getSharedSystemProps = getSharedSystemProps;
|
|
2135
|
+
exports.manifestPath = manifestPath;
|
|
2136
|
+
exports.pruneStaleAssets = pruneStaleAssets;
|
|
2137
|
+
exports.readJsonEnvelope = readJsonEnvelope;
|
|
2138
|
+
exports.replacementEpochPath = replacementEpochPath;
|
|
2139
|
+
exports.runSessionPipeline = runSessionPipeline;
|
|
2140
|
+
exports.sessionArtifactDir = sessionArtifactDir;
|
|
2141
|
+
exports.setAnalysisPromise = setAnalysisPromise;
|
|
2142
|
+
exports.setSharedEngine = setSharedEngine;
|
|
2143
|
+
exports.startTurbopackWatcher = startTurbopackWatcher;
|
|
2144
|
+
exports.stylesPath = stylesPath;
|
|
2145
|
+
exports.systemPropsPath = systemPropsPath;
|
|
2146
|
+
exports.verifyCommitRecord = verifyCommitRecord;
|