ttsc 0.26.2 → 0.28.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (170) hide show
  1. package/README.md +9 -0
  2. package/cmd/graphbench/main.go +26 -16
  3. package/cmd/graphdump/main.go +37 -12
  4. package/cmd/graphdump/run_declares_the_capability_its_output_carries_test.go +137 -0
  5. package/cmd/graphdump/run_reports_an_unknown_flag_without_exiting_the_process_test.go +56 -0
  6. package/cmd/graphdump/run_reports_an_unloadable_project_without_printing_a_dump_test.go +46 -0
  7. package/cmd/ttsc/api_transform.go +28 -3
  8. package/cmd/ttscgraph/auxiliary_identity_state_tracks_retargets_not_source_contents_test.go +85 -0
  9. package/cmd/ttscgraph/disk_realpath_other.go +16 -0
  10. package/cmd/ttscgraph/disk_realpath_windows.go +56 -0
  11. package/cmd/ttscgraph/dump.go +43 -11
  12. package/cmd/ttscgraph/run_dump_claims_artifacts_only_when_it_asked_test.go +135 -0
  13. package/cmd/ttscgraph/run_dump_declares_the_capability_its_output_carries_test.go +96 -0
  14. package/cmd/ttscgraph/serve.go +357 -60
  15. package/cmd/ttscgraph/serve_adopts_a_republished_artifact_set_test.go +348 -0
  16. package/cmd/ttscgraph/serve_projects_an_artifact_no_source_owns_test.go +90 -0
  17. package/cmd/ttscgraph/serve_shards.go +31 -9
  18. package/driver/config_discovery.go +121 -0
  19. package/driver/emit_plugin.go +77 -11
  20. package/driver/graph.go +64 -11
  21. package/driver/input_observation.go +273 -0
  22. package/driver/input_observation_test.go +149 -0
  23. package/driver/plugins.go +506 -15
  24. package/driver/program.go +54 -1
  25. package/driver/resolution_candidates.go +13 -5
  26. package/driver/rewrite.go +45 -2
  27. package/driver/transform_dependencies.go +283 -0
  28. package/driver/transform_dependencies_declares_every_file_without_a_contributor_test.go +28 -0
  29. package/driver/transform_dependencies_drops_a_self_reported_dependency_test.go +21 -0
  30. package/driver/transform_dependencies_keys_a_reported_path_like_the_envelope_test.go +46 -0
  31. package/driver/transform_dependencies_requires_every_contributor_to_declare_test.go +38 -0
  32. package/driver/transform_dependencies_stays_silent_for_an_undeclared_contributor_test.go +23 -0
  33. package/driver/transform_dependencies_withdraws_completeness_after_an_unusable_dependency_test.go +37 -0
  34. package/internal/graph/artifacts.go +230 -0
  35. package/internal/graph/artifacts_index_what_a_plugin_published_test.go +154 -0
  36. package/internal/graph/artifacts_survive_a_partial_rebuild_test.go +89 -0
  37. package/internal/graph/build.go +35 -0
  38. package/internal/graph/build_releases_the_scratch_it_no_longer_reads_test.go +118 -0
  39. package/internal/graph/doc_refs_are_the_use_the_checker_already_counts_test.go +93 -0
  40. package/internal/graph/doc_refs_resolve_documentation_links_test.go +335 -0
  41. package/internal/graph/doc_tags.go +416 -0
  42. package/internal/graph/doc_tags_are_claimed_before_they_are_read_test.go +131 -0
  43. package/internal/graph/doc_tags_capture_every_unrecognized_tag_test.go +165 -0
  44. package/internal/graph/doc_tags_read_only_what_the_parser_attached_test.go +97 -0
  45. package/internal/graph/doc_tags_stop_at_the_external_boundary_test.go +59 -0
  46. package/internal/graph/doc_tags_survive_line_endings_and_repeated_blocks_test.go +146 -0
  47. package/internal/graph/dump.go +60 -13
  48. package/internal/graph/dump_checkout_paths_are_stable_test.go +2 -2
  49. package/internal/graph/dump_paths.go +2 -2
  50. package/internal/graph/dump_paths_are_portable_and_injective_test.go +1 -1
  51. package/internal/graph/edge_kinds_names_every_declared_kind_test.go +49 -0
  52. package/internal/graph/edges.go +1 -0
  53. package/internal/graph/graph.go +79 -4
  54. package/internal/graph/provenance.go +29 -2
  55. package/lib/TtscCompiler.d.ts +12 -11
  56. package/lib/TtscCompiler.js +44 -29
  57. package/lib/TtscCompiler.js.map +1 -1
  58. package/lib/compiler/internal/captureProcessOutput.js +2 -2
  59. package/lib/compiler/internal/captureProcessOutput.js.map +1 -1
  60. package/lib/compiler/internal/compileProjectInMemory.js +4 -3
  61. package/lib/compiler/internal/compileProjectInMemory.js.map +1 -1
  62. package/lib/compiler/internal/project/createNativeProjectContextArgs.d.ts +9 -0
  63. package/lib/compiler/internal/project/createNativeProjectContextArgs.js +16 -4
  64. package/lib/compiler/internal/project/createNativeProjectContextArgs.js.map +1 -1
  65. package/lib/compiler/internal/resolveEmittedJavaScript.d.ts +2 -0
  66. package/lib/compiler/internal/resolveEmittedJavaScript.js +3 -0
  67. package/lib/compiler/internal/resolveEmittedJavaScript.js.map +1 -1
  68. package/lib/compiler/internal/runBuild.d.ts +1 -1
  69. package/lib/compiler/internal/runBuild.js +80 -34
  70. package/lib/compiler/internal/runBuild.js.map +1 -1
  71. package/lib/compiler/internal/runSingleFileEmit.js +2 -2
  72. package/lib/compiler/internal/runSingleFileEmit.js.map +1 -1
  73. package/lib/compiler/internal/sharedHostHelpers.d.ts +28 -0
  74. package/lib/compiler/internal/sharedHostHelpers.js +39 -0
  75. package/lib/compiler/internal/sharedHostHelpers.js.map +1 -1
  76. package/lib/compiler/internal/startResidentTransform.js +12 -4
  77. package/lib/compiler/internal/startResidentTransform.js.map +1 -1
  78. package/lib/compiler/internal/transformProjectInMemory.d.ts +3 -0
  79. package/lib/compiler/internal/transformProjectInMemory.js +239 -35
  80. package/lib/compiler/internal/transformProjectInMemory.js.map +1 -1
  81. package/lib/flags/schema.js +12 -5
  82. package/lib/flags/schema.js.map +1 -1
  83. package/lib/index.d.ts +1 -0
  84. package/lib/index.js +1 -0
  85. package/lib/index.js.map +1 -1
  86. package/lib/internal/createCanonicalTempDirectory.d.ts +17 -0
  87. package/lib/internal/createCanonicalTempDirectory.js +42 -0
  88. package/lib/internal/createCanonicalTempDirectory.js.map +1 -0
  89. package/lib/internal/projectInputPathIdentity.d.ts +3 -0
  90. package/lib/internal/projectInputPathIdentity.js +11 -5
  91. package/lib/internal/projectInputPathIdentity.js.map +1 -1
  92. package/lib/internal/resolveNodeBinary.d.ts +14 -0
  93. package/lib/internal/resolveNodeBinary.js +150 -0
  94. package/lib/internal/resolveNodeBinary.js.map +1 -0
  95. package/lib/internal/resolveSafeCacheCleanupTargets.d.ts +15 -0
  96. package/lib/internal/resolveSafeCacheCleanupTargets.js +61 -0
  97. package/lib/internal/resolveSafeCacheCleanupTargets.js.map +1 -0
  98. package/lib/launcher/internal/prepareExecution.d.ts +3 -0
  99. package/lib/launcher/internal/prepareExecution.js +42 -14
  100. package/lib/launcher/internal/prepareExecution.js.map +1 -1
  101. package/lib/launcher/internal/runTtsc.js +25 -13
  102. package/lib/launcher/internal/runTtsc.js.map +1 -1
  103. package/lib/launcher/internal/runTtscserver.js +18 -3
  104. package/lib/launcher/internal/runTtscserver.js.map +1 -1
  105. package/lib/launcher/internal/runTtsx.js +2 -0
  106. package/lib/launcher/internal/runTtsx.js.map +1 -1
  107. package/lib/launcher/internal/runtimeHooks.d.ts +47 -3
  108. package/lib/launcher/internal/runtimeHooks.js +746 -64
  109. package/lib/launcher/internal/runtimeHooks.js.map +1 -1
  110. package/lib/plugin/internal/buildSourcePlugin.d.ts +62 -8
  111. package/lib/plugin/internal/buildSourcePlugin.js +754 -72
  112. package/lib/plugin/internal/buildSourcePlugin.js.map +1 -1
  113. package/lib/plugin/internal/capabilityResolutionCache.d.ts +76 -0
  114. package/lib/plugin/internal/capabilityResolutionCache.js +224 -0
  115. package/lib/plugin/internal/capabilityResolutionCache.js.map +1 -0
  116. package/lib/plugin/internal/loadProjectPlugins.d.ts +17 -5
  117. package/lib/plugin/internal/loadProjectPlugins.js +1339 -77
  118. package/lib/plugin/internal/loadProjectPlugins.js.map +1 -1
  119. package/lib/plugin/resolveCapabilityPlugins.d.ts +55 -0
  120. package/lib/plugin/resolveCapabilityPlugins.js +118 -0
  121. package/lib/plugin/resolveCapabilityPlugins.js.map +1 -0
  122. package/lib/register.d.ts +1 -0
  123. package/lib/register.js +46 -0
  124. package/lib/register.js.map +1 -0
  125. package/lib/structures/ITtscCompilerContext.d.ts +6 -3
  126. package/lib/structures/ITtscCompilerTransformation.d.ts +38 -0
  127. package/lib/structures/ITtscPlugin.d.ts +33 -0
  128. package/package.json +16 -9
  129. package/shim/ast/parent.go +16 -3
  130. package/shim/checker/enums_gen.go +11 -0
  131. package/shim/checker/go.mod +9 -0
  132. package/shim/checker/go.sum +22 -0
  133. package/shim/checker/shim.go +78 -0
  134. package/shim/compiler/emithook.go +10 -0
  135. package/shim/compiler/incremental.go +45 -8
  136. package/src/TtscCompiler.ts +55 -33
  137. package/src/compiler/internal/captureProcessOutput.ts +3 -2
  138. package/src/compiler/internal/compileProjectInMemory.ts +4 -3
  139. package/src/compiler/internal/project/createNativeProjectContextArgs.ts +19 -4
  140. package/src/compiler/internal/resolveEmittedJavaScript.ts +6 -0
  141. package/src/compiler/internal/runBuild.ts +102 -39
  142. package/src/compiler/internal/runSingleFileEmit.ts +2 -2
  143. package/src/compiler/internal/sharedHostHelpers.ts +44 -0
  144. package/src/compiler/internal/startResidentTransform.ts +12 -3
  145. package/src/compiler/internal/transformProjectInMemory.ts +410 -39
  146. package/src/flags/schema.ts +12 -5
  147. package/src/index.ts +1 -0
  148. package/src/internal/createCanonicalTempDirectory.ts +53 -0
  149. package/src/internal/projectInputPathIdentity.ts +17 -4
  150. package/src/internal/resolveNodeBinary.ts +173 -0
  151. package/src/internal/resolveSafeCacheCleanupTargets.ts +85 -0
  152. package/src/launcher/internal/prepareExecution.ts +50 -14
  153. package/src/launcher/internal/runTtsc.ts +24 -13
  154. package/src/launcher/internal/runTtscserver.ts +18 -3
  155. package/src/launcher/internal/runTtsx.ts +2 -0
  156. package/src/launcher/internal/runtimeHooks.ts +934 -68
  157. package/src/plugin/internal/buildSourcePlugin.ts +984 -79
  158. package/src/plugin/internal/capabilityResolutionCache.ts +305 -0
  159. package/src/plugin/internal/loadProjectPlugins.ts +1718 -104
  160. package/src/plugin/resolveCapabilityPlugins.ts +171 -0
  161. package/src/register.ts +47 -0
  162. package/src/structures/ITtscCompilerContext.ts +6 -3
  163. package/src/structures/ITtscCompilerTransformation.ts +46 -0
  164. package/src/structures/ITtscPlugin.ts +36 -0
  165. package/tools/shim_audit/baseline.json +0 -2
  166. package/utility/host.go +18 -3
  167. package/lib/internal/assertSafeExplicitCacheDirectory.d.ts +0 -5
  168. package/lib/internal/assertSafeExplicitCacheDirectory.js +0 -26
  169. package/lib/internal/assertSafeExplicitCacheDirectory.js.map +0 -1
  170. package/src/internal/assertSafeExplicitCacheDirectory.ts +0 -29
@@ -0,0 +1,305 @@
1
+ import crypto from "node:crypto";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+
5
+ import { resolveSourceBuildCachePaths } from "./buildSourcePlugin";
6
+ import {
7
+ hashHostInputPaths,
8
+ realpathHostInputPaths,
9
+ } from "./loadProjectPlugins";
10
+
11
+ /**
12
+ * The answer `resolveCapabilityPlugins` produced for one project, and the exact
13
+ * state that makes it still true.
14
+ *
15
+ * The answer is small and fully serializable — a binary path, a capability map,
16
+ * a manifest string — which is why it is cached here and not one layer down.
17
+ * `loadProjectPlugins` returns live plugin descriptors the compiler drives, and
18
+ * writing those to disk would be caching a different, much larger thing.
19
+ */
20
+ export interface ITtscCapabilityResolutionEntry {
21
+ /** Cache format plus the ttsc build that wrote it. */
22
+ version: string;
23
+ /** Paths whose state decides whether this answer is still the answer. */
24
+ hostInputs: string[];
25
+ /** Content hash per host input, `null` for one that does not exist. */
26
+ hostInputHashes: Record<string, string | null>;
27
+ /** Physical identity per host input, so a retargeted link is a change. */
28
+ hostInputRealpaths: Record<string, string | null>;
29
+ /**
30
+ * Fingerprint per plugin Go source directory.
31
+ *
32
+ * The binary path is content-keyed on that source, so a source edit produces
33
+ * a different path — and the cached entry would keep naming the old one,
34
+ * which still exists because the build cache retains it. Nothing in the host
35
+ * inputs moves when a plugin's own Go changes, so this is what notices.
36
+ */
37
+ sources: Record<string, string>;
38
+ /** The `--plugins-json` payload, verbatim. */
39
+ manifest: string;
40
+ /** The `--project-context-json` payload, or `null` when none was wanted. */
41
+ projectContext: string | null;
42
+ /** One entry per configured native plugin, in configured order. */
43
+ plugins: ITtscCapabilityResolutionPlugin[];
44
+ }
45
+
46
+ /** One plugin as the cache records it. */
47
+ export interface ITtscCapabilityResolutionPlugin {
48
+ binary: string;
49
+ capabilities: Record<string, boolean>;
50
+ source: string;
51
+ }
52
+
53
+ /**
54
+ * Cache format tag.
55
+ *
56
+ * Moves when the entry shape or the validation rule changes, so an older entry
57
+ * is discarded rather than read under new rules.
58
+ */
59
+ const FORMAT = "ttsc-capability-resolution-v1";
60
+
61
+ /**
62
+ * The character that joins fields a path could otherwise forge.
63
+ *
64
+ * A path cannot contain it, which is the whole reason it is the separator: with
65
+ * a space, a file named `a 1 2` states the same string as a one-byte file named
66
+ * `a`, and an edit to either would read as no edit at all. Built rather than
67
+ * written literally, because a source file carrying a raw NUL is one Git
68
+ * classifies as binary — which silently exempts it from this repository's
69
+ * end-of-line contract and leaves it with no textual diff for a reviewer.
70
+ */
71
+ const SEPARATOR = String.fromCharCode(0);
72
+
73
+ /**
74
+ * Read the recorded answer for a project, or `null` when there is none that is
75
+ * still true.
76
+ *
77
+ * Fail-closed by construction: every path that cannot prove the entry — a
78
+ * missing file, a parse failure, a format bump, a moved input, a rebuilt plugin
79
+ * source, a binary that is gone — returns `null`, and `null` means "walk the
80
+ * project properly". A cache that guessed here would answer "no plugin declares
81
+ * this capability" for a project that had just configured one, which is a wrong
82
+ * answer indistinguishable from the correct answer for the common case.
83
+ */
84
+ export function readCapabilityResolution(options: {
85
+ cwd: string;
86
+ tsconfig: string;
87
+ version: string;
88
+ env?: NodeJS.ProcessEnv;
89
+ }): ITtscCapabilityResolutionEntry | null {
90
+ const file = resolutionFile(options);
91
+ if (file === null) return null;
92
+ let entry: ITtscCapabilityResolutionEntry;
93
+ try {
94
+ entry = JSON.parse(
95
+ fs.readFileSync(file, "utf8"),
96
+ ) as ITtscCapabilityResolutionEntry;
97
+ } catch {
98
+ return null;
99
+ }
100
+ if (!isEntry(entry) || entry.version !== formatVersion(options.version))
101
+ return null;
102
+ // An entry recording nothing proves nothing. Discovery always reads at least
103
+ // the project's own manifest, so an empty input set is a malformed entry
104
+ // rather than a project with no inputs.
105
+ if (entry.hostInputs.length === 0) return null;
106
+ if (
107
+ !sameMap(entry.hostInputHashes, hashHostInputPaths(entry.hostInputs)) ||
108
+ !sameMap(entry.hostInputRealpaths, realpathHostInputPaths(entry.hostInputs))
109
+ )
110
+ return null;
111
+ for (const [source, fingerprint] of Object.entries(entry.sources))
112
+ if (fingerprintDirectory(source) !== fingerprint) return null;
113
+ for (const plugin of entry.plugins)
114
+ if (!fs.existsSync(plugin.binary)) return null;
115
+ return entry;
116
+ }
117
+
118
+ /**
119
+ * Record the answer and the state it was true for.
120
+ *
121
+ * A write failure is not reported. The cache is an optimization over a walk
122
+ * that still works, and a read-only or full disk is a reason to be slower, not
123
+ * a reason for `resolveCapabilityPlugins` to start throwing at a caller whose
124
+ * contract is that it never does.
125
+ */
126
+ export function writeCapabilityResolution(
127
+ options: {
128
+ cwd: string;
129
+ tsconfig: string;
130
+ version: string;
131
+ env?: NodeJS.ProcessEnv;
132
+ },
133
+ answer: {
134
+ hostInputs: readonly string[];
135
+ manifest: string;
136
+ projectContext: string | null;
137
+ plugins: readonly ITtscCapabilityResolutionPlugin[];
138
+ },
139
+ ): void {
140
+ const file = resolutionFile(options);
141
+ if (file === null) return;
142
+ const hostInputs = [
143
+ ...new Set(
144
+ answer.hostInputs
145
+ .filter((input) => input !== "")
146
+ .map((input) => path.resolve(input)),
147
+ ),
148
+ ].sort();
149
+ if (hostInputs.length === 0) return;
150
+ const entry: ITtscCapabilityResolutionEntry = {
151
+ hostInputHashes: hashHostInputPaths(hostInputs),
152
+ hostInputRealpaths: realpathHostInputPaths(hostInputs),
153
+ hostInputs,
154
+ manifest: answer.manifest,
155
+ plugins: [...answer.plugins],
156
+ projectContext: answer.projectContext,
157
+ sources: Object.fromEntries(
158
+ [...new Set(answer.plugins.map((plugin) => plugin.source))]
159
+ .filter((source) => source !== "")
160
+ .map((source) => [source, fingerprintDirectory(source)] as const),
161
+ ),
162
+ version: formatVersion(options.version),
163
+ };
164
+ try {
165
+ fs.mkdirSync(path.dirname(file), { recursive: true });
166
+ // Written beside the target and renamed, so a reader never sees half an
167
+ // entry: a truncated JSON parses as a failure and falls back, but a
168
+ // partially written one could parse and be believed.
169
+ const staging = `${file}.${String(process.pid)}.tmp`;
170
+ fs.writeFileSync(staging, JSON.stringify(entry), "utf8");
171
+ fs.renameSync(staging, file);
172
+ } catch {
173
+ return;
174
+ }
175
+ }
176
+
177
+ function formatVersion(version: string): string {
178
+ return `${FORMAT}:${version}`;
179
+ }
180
+
181
+ /**
182
+ * Where the entry for this project lives, or `null` when no cache root can be
183
+ * resolved.
184
+ *
185
+ * Keyed on the project rather than on the capability: the walk it replaces
186
+ * discovers every configured plugin, so one entry answers for all of them and a
187
+ * second consumer asking about a different capability costs nothing.
188
+ */
189
+ function resolutionFile(options: {
190
+ cwd: string;
191
+ tsconfig: string;
192
+ env?: NodeJS.ProcessEnv;
193
+ }): string | null {
194
+ let root: string;
195
+ try {
196
+ root = resolveSourceBuildCachePaths(
197
+ path.resolve(options.cwd),
198
+ undefined,
199
+ options.env ?? process.env,
200
+ ).root;
201
+ } catch {
202
+ return null;
203
+ }
204
+ const key = crypto
205
+ .createHash("sha256")
206
+ .update(path.resolve(options.cwd))
207
+ .update(SEPARATOR)
208
+ .update(options.tsconfig)
209
+ .digest("hex");
210
+ return path.join(root, "capabilities", `${key}.json`);
211
+ }
212
+
213
+ /** Whether the parsed value has every field the validation reads. */
214
+ function isEntry(value: unknown): value is ITtscCapabilityResolutionEntry {
215
+ if (typeof value !== "object" || value === null) return false;
216
+ const entry = value as Partial<ITtscCapabilityResolutionEntry>;
217
+ return (
218
+ typeof entry.version === "string" &&
219
+ Array.isArray(entry.hostInputs) &&
220
+ entry.hostInputs.every((input) => typeof input === "string") &&
221
+ isRecord(entry.hostInputHashes) &&
222
+ isRecord(entry.hostInputRealpaths) &&
223
+ isRecord(entry.sources) &&
224
+ typeof entry.manifest === "string" &&
225
+ (entry.projectContext === null ||
226
+ typeof entry.projectContext === "string") &&
227
+ Array.isArray(entry.plugins) &&
228
+ entry.plugins.every(
229
+ (plugin) =>
230
+ typeof plugin === "object" &&
231
+ plugin !== null &&
232
+ typeof plugin.binary === "string" &&
233
+ typeof plugin.source === "string" &&
234
+ isRecord(plugin.capabilities),
235
+ )
236
+ );
237
+ }
238
+
239
+ function isRecord(value: unknown): value is Record<string, unknown> {
240
+ return typeof value === "object" && value !== null && !Array.isArray(value);
241
+ }
242
+
243
+ /**
244
+ * Whether two snapshots describe the same state.
245
+ *
246
+ * Compared both ways: an input that has appeared is as much a change as one
247
+ * that has moved, and a recorded key the fresh snapshot lacks means the two
248
+ * were taken over different input sets.
249
+ */
250
+ function sameMap(
251
+ recorded: Record<string, string | null>,
252
+ current: Record<string, string | null>,
253
+ ): boolean {
254
+ const keys = Object.keys(recorded);
255
+ if (keys.length !== Object.keys(current).length) return false;
256
+ return keys.every(
257
+ (key) =>
258
+ Object.prototype.hasOwnProperty.call(current, key) &&
259
+ recorded[key] === current[key],
260
+ );
261
+ }
262
+
263
+ /**
264
+ * A directory's shape and the state of every file in it.
265
+ *
266
+ * Size and modification time rather than content: a plugin's Go source is
267
+ * hundreds of files, this runs on the fast path, and the question it answers is
268
+ * only whether the build cache would now key on something else. Bounded in
269
+ * depth, and blind to `node_modules` and dot directories, for the same reason
270
+ * the build that reads this source is.
271
+ */
272
+ function fingerprintDirectory(directory: string, depth = 0): string {
273
+ const hash = crypto.createHash("sha256");
274
+ for (const line of directoryState(directory, depth).sort())
275
+ hash.update(line).update("\n");
276
+ return hash.digest("hex");
277
+ }
278
+
279
+ function directoryState(directory: string, depth: number): string[] {
280
+ if (depth > 16) return [];
281
+ let entries: fs.Dirent[];
282
+ try {
283
+ entries = fs.readdirSync(directory, { withFileTypes: true });
284
+ } catch {
285
+ return [`${directory}${SEPARATOR}absent`];
286
+ }
287
+ const states: string[] = [];
288
+ for (const entry of entries) {
289
+ const child = path.join(directory, entry.name);
290
+ if (entry.isDirectory()) {
291
+ if (entry.name === "node_modules" || entry.name.startsWith(".")) continue;
292
+ states.push(...directoryState(child, depth + 1));
293
+ continue;
294
+ }
295
+ try {
296
+ const stat = fs.statSync(child);
297
+ states.push(
298
+ [child, String(stat.size), String(stat.mtimeMs)].join(SEPARATOR),
299
+ );
300
+ } catch {
301
+ states.push(`${child}${SEPARATOR}absent`);
302
+ }
303
+ }
304
+ return states;
305
+ }