@ttsc/unplugin 0.28.1 → 0.28.2

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.
@@ -0,0 +1,414 @@
1
+ import fs from "node:fs";
2
+ import type { ITtscCompilerTransformation } from "ttsc";
3
+ import { type FilesystemPathIdentityContext, type FilesystemPathIdentityOperations } from "ttsc/path-identity";
4
+ import type { TransformResult } from "unplugin";
5
+ import type { ResolvedTtscUnpluginOptions } from "./options.cjs";
6
+ /**
7
+ * The normalised transform result type that this module produces.
8
+ *
9
+ * Excludes the shorthand `string`, `null`, and `undefined` variants of
10
+ * unplugin's `TransformResult` so callers always receive an object or
11
+ * `undefined`.
12
+ */
13
+ export type TtscTransformResult = Exclude<TransformResult, string | null | undefined>;
14
+ /**
15
+ * Normalised alias entry used when building the `paths` overlay for the
16
+ * generated tsconfig. Derived from either a Vite array alias or a webpack/
17
+ * Rspack object alias.
18
+ */
19
+ export interface TtscTransformAlias {
20
+ /** The alias key (module specifier prefix). */
21
+ find: string;
22
+ /** Absolute or cwd-relative path that the alias points to. */
23
+ replacement: string;
24
+ }
25
+ /** One directory's cheap project-membership identity at generation time. */
26
+ interface TtscProjectDirectorySnapshot {
27
+ /** Absolute directory spelling used by the project walk. */
28
+ path: string;
29
+ /** Metadata signature that changes when its immediate membership changes. */
30
+ signature: string;
31
+ }
32
+ /** Generation-scoped directory watchers used to detect membership changes. */
33
+ interface TtscProjectMutationTracker {
34
+ close: () => void;
35
+ /**
36
+ * Absolute spellings whose creation, change or removal this tracker would
37
+ * report, when it watches exact names rather than whole directories.
38
+ *
39
+ * A validation that finds an input here needs no filesystem call of its own:
40
+ * the tracker is the evidence, and every path that leaves this set falls back
41
+ * to being proven by hand. Empty for a tracker that watches directories as a
42
+ * whole, which cannot answer for one name.
43
+ */
44
+ covered?: ReadonlySet<string>;
45
+ /**
46
+ * Wait until every event this tracker's watcher has already dispatched has
47
+ * been applied to it.
48
+ *
49
+ * An in-process watcher drains on the next macrotask turn, because its
50
+ * callbacks are already queued on this loop. A watcher living in the Windows
51
+ * broker drains by round-trip instead: the child replies after its own turn,
52
+ * and IPC preserves order, so the reply cannot overtake an event the child
53
+ * had already sent (samchon/ttsc#1272).
54
+ */
55
+ drain?: () => Promise<void>;
56
+ failed: boolean;
57
+ membershipChanged: boolean;
58
+ settle?: Promise<void>;
59
+ }
60
+ /**
61
+ * A single entry in the project transform cache.
62
+ *
63
+ * Stores the full compiler result together with SHA-256 hashes of every project
64
+ * input file. In a cache with an explicit build lifecycle, the first delivery
65
+ * of each compiled module compares its supplied source with the generation
66
+ * snapshot in constant time. Later graph-bearing deliveries validate only the
67
+ * requested file's derived inputs plus exact host descriptor/config inputs;
68
+ * graph-free envelopes retain complete-snapshot validation.
69
+ */
70
+ export interface TtscCachedProjectTransform {
71
+ /**
72
+ * SHA-256 hash of every input the compiler reported outside the project walk
73
+ * (keyed by filesystem identity), captured at the time of the transform.
74
+ *
75
+ * The project walk cannot see files outside the project root or under ignored
76
+ * directories (`node_modules` declarations, monorepo sibling sources,
77
+ * out-of-root tsconfig `extends` ancestry), yet the host-owned reference
78
+ * graph proves they are transform inputs. Long-lived hosts that never clear
79
+ * the cache between builds (Metro workers and the Turbopack loader) would
80
+ * otherwise replay a project transform computed against a stale out-of-walk
81
+ * input for the whole process lifetime; per-build hosts clear the cache on
82
+ * `buildStart` and never replay across edits.
83
+ */
84
+ externalInputHashes?: Record<string, string>;
85
+ /**
86
+ * Compiler-time physical identities for graph-owned entries in
87
+ * {@link externalInputHashes}. Dependency-only paths have no generation
88
+ * realpath protocol and therefore omit this evidence.
89
+ */
90
+ externalInputRealpaths?: Record<string, string | null>;
91
+ /**
92
+ * Original absolute spellings of {@link externalInputHashes} inputs. These
93
+ * stay separate from their identity keys so validation reads the paths the
94
+ * compiler reported rather than a normalized replacement spelling.
95
+ */
96
+ externalInputPaths?: string[];
97
+ /**
98
+ * Metadata signature of each out-of-walk input, captured around the read that
99
+ * proved its {@link externalInputHashes} entry and recorded only once the
100
+ * observed filesystem's clock provably left the stamp's tick
101
+ * ({@link stampSeparable}). An input whose signature still holds carries the
102
+ * recorded content, so revalidation may skip the read.
103
+ *
104
+ * Keyed by lexical spelling rather than by physical identity, for the reason
105
+ * {@link TtscHostInputValidation} states: a symlink or junction spelling and
106
+ * its selected target deliberately share one identity but have different
107
+ * metadata, so an identity key would let the two overwrite each other's
108
+ * signature and force both to be re-read on every delivery.
109
+ */
110
+ externalInputSignatures?: Record<string, string>;
111
+ /**
112
+ * SHA-256 hash of each project-relative input path at the time of the
113
+ * transform.
114
+ */
115
+ inputHashes: Record<string, string>;
116
+ /**
117
+ * Metadata signature of each {@link inputHashes} entry whose hash was proven
118
+ * against an unracing read of the file on disk, in a tick the observed
119
+ * filesystem's clock had provably left ({@link stampSeparable}).
120
+ *
121
+ * The generation's own current file is absent at capture: its recorded hash
122
+ * comes from the bundler's in-memory source, so the walk that produced it
123
+ * compared nothing. A later delivery of a sibling does compare that file's
124
+ * disk bytes against the recorded hash, and may record a signature then.
125
+ */
126
+ inputSignatures?: Record<string, string>;
127
+ /** Metadata snapshot of every directory in the stable generation walk. */
128
+ projectDirectories?: TtscProjectDirectorySnapshot[];
129
+ /** Live notification state for universal host-input changes. */
130
+ hostInputMutationTracker?: TtscProjectMutationTracker;
131
+ /**
132
+ * Live notification state for the generation's absent resolution candidates
133
+ * and the directories that carry them.
134
+ *
135
+ * Separate from the universal-input tracker because it listens for a
136
+ * different thing. Every event that can make an absent candidate present is a
137
+ * rename — the file appearing, a component of the path being created,
138
+ * replaced, or retargeted — so a change event on one of these names is never
139
+ * evidence this tracker exists to collect. What it is, on a backend that
140
+ * reports a write below a directory as a change to that directory's own entry
141
+ * (Windows does), is a dev server's steady traffic: listening for every event
142
+ * would replace the generation each time a bundler wrote inside
143
+ * `node_modules`. The filter therefore drops noise without dropping proof.
144
+ * The one appearance it cannot see is a Windows junction retargeted in place
145
+ * through `FSCTL_SET_REPARSE_POINT`, which no mainstream tool does; every
146
+ * package manager replaces the entry instead, which is a rename.
147
+ */
148
+ candidateMutationTracker?: TtscProjectMutationTracker;
149
+ /**
150
+ * Universal descriptor/config inputs proven once at generation time, then by
151
+ * metadata.
152
+ *
153
+ * Recorded state of the generation, like the input hashes and the directory
154
+ * snapshot beside it, rather than state derived from the envelope: an entry
155
+ * carries the manifest that proved it, so nothing can present one
156
+ * generation's recorded inputs under another envelope's proof.
157
+ */
158
+ hostInputValidation?: TtscHostInputValidation;
159
+ /** Live notification state for file/directory creation, deletion, and rename. */
160
+ projectMutationTracker?: TtscProjectMutationTracker;
161
+ /**
162
+ * Whether the generation-time project walk observed every directory and file
163
+ * it attempted to snapshot. An incomplete walk may never authorize narrow
164
+ * validation; a later complete walk must be allowed to replace it.
165
+ */
166
+ projectSnapshotComplete?: boolean;
167
+ /** Absolute path to the directory that owns the tsconfig. */
168
+ projectRoot: string;
169
+ /** Raw compiler output returned by {@link TtscCompiler.transform}. */
170
+ result: ITtscCompilerTransformation;
171
+ /**
172
+ * Files already delivered from this generation, keyed by filesystem identity.
173
+ * Build-scoped caches use this to skip persistent validation only for a
174
+ * module's first delivery inside the current build.
175
+ */
176
+ servedFiles?: Set<string>;
177
+ /**
178
+ * Absolute path of the generated temp-dir tsconfig this compile ran against,
179
+ * when an alias/compiler-options overlay required one. The compiler reports
180
+ * it in the envelope's `graph.configs` chain, but it is disposed right after
181
+ * the compile, so registering it as a watch input would invalidate every
182
+ * bundler cache snapshot on the next build; watch derivation must skip
183
+ * exactly this path.
184
+ */
185
+ temporaryTsconfig?: string;
186
+ }
187
+ /**
188
+ * Keyed by a stable JSON string that encodes the tsconfig path, compiler
189
+ * options overlay, plugin list, and alias paths. The value is a `Promise` so
190
+ * concurrent transforms for the same project share a single in-flight
191
+ * compilation rather than spawning multiple `TtscCompiler` instances.
192
+ */
193
+ export type TtscTransformCache = Map<string, Promise<TtscCachedProjectTransform>>;
194
+ /** Cache-owned synchronous filesystem reads used by transform validation. */
195
+ export interface TtscTransformFilesystemOperations {
196
+ /** Override the case policy when the observed filesystem is not the host. */
197
+ caseSensitive?: FilesystemPathIdentityOperations["caseSensitive"];
198
+ /** Test whether a validation or resolution candidate currently exists. */
199
+ exists(location: string): boolean;
200
+ /** Read link metadata without following a symbolic link. */
201
+ lstat(location: string): fs.BigIntStats;
202
+ /** Read bytes used by project, graph, and host-input fingerprints. */
203
+ readFile(location: string): Buffer;
204
+ /** Enumerate one project or missing-input proof directory. */
205
+ readdir(location: string): fs.Dirent[];
206
+ /** Resolve one lexical path to its current physical target. */
207
+ realpath(location: string): string;
208
+ /** Read ordinary metadata for file-kind and missing-path checks. */
209
+ stat(location: string): fs.Stats;
210
+ /** Read nanosecond metadata for stable file and directory signatures. */
211
+ statBigInt(location: string): fs.BigIntStats;
212
+ /** Override path parsing when the observed filesystem is not the host. */
213
+ platform?: NodeJS.Platform;
214
+ /**
215
+ * Open one directory's change notification, or throw when the observed
216
+ * filesystem cannot provide one.
217
+ *
218
+ * Left undefined, generations watch the host filesystem: `fs.watch` on POSIX
219
+ * and an isolated broker process on Windows. An embedder observing another
220
+ * filesystem supplies its own; a generation whose watch cannot be opened
221
+ * keeps validating from recorded state instead of losing its cache.
222
+ *
223
+ * Supplying one replaces the Windows broker as well, so an embedder that
224
+ * wraps Node's own `fs.watch` there gives up the isolation that contains the
225
+ * native abort Node's Windows fs-event backend can raise when a watched
226
+ * temporary tree is deleted.
227
+ */
228
+ watch?(directory: string, listener: (eventType: string, filename: string | null) => void, onError: () => void): {
229
+ close: () => void;
230
+ };
231
+ }
232
+ /** Normalize one directory entry under the owning filesystem's case policy. */
233
+ export declare function normalizeHostInputName(name: string, caseSensitive: boolean): string;
234
+ /** Create an empty persistent transform cache with isolated filesystem reads. */
235
+ export declare function createTtscTransformCache(operations?: Partial<TtscTransformFilesystemOperations>): TtscTransformCache;
236
+ /**
237
+ * Start a host build, clearing its prior generation and enabling constant-time
238
+ * first delivery for modules compiled during this build.
239
+ *
240
+ * Hosts without a guaranteed build-start callback use persistent validation
241
+ * unless they have another immutable lifecycle. Bun runtime setup, for example,
242
+ * defines one process-scoped module-loading session.
243
+ */
244
+ export declare function beginTtscTransformBuild(cache: TtscTransformCache): void;
245
+ /**
246
+ * Clear a cache and return it to persistent validation mode.
247
+ *
248
+ * This is distinct from {@link beginTtscTransformBuild}: hosts such as Vite's
249
+ * development server may invoke `buildStart` only once for a process that spans
250
+ * many edits, so that callback cannot authorize build-scoped shortcuts.
251
+ */
252
+ export declare function resetTtscTransformCache(cache: TtscTransformCache): void;
253
+ /**
254
+ * What the generation already knows about one derived watch input, handed to
255
+ * the adapter so it does not rederive it per input per delivery.
256
+ *
257
+ * Both facts are generation state: the identity is the memoized
258
+ * {@link pathIdentityKey} of the input, and `missing` is the existence the
259
+ * generation recorded and every cache hit revalidates. An adapter that computes
260
+ * them itself pays a `realpath`, a case-sensitivity directory listing, and an
261
+ * `existsSync` for every input of every delivered module, which is O(modules x
262
+ * inputs) for one build (samchon/ttsc#1246).
263
+ */
264
+ export interface TtscWatchInputEvidence {
265
+ /** Memoized filesystem identity of the input. */
266
+ identity: string;
267
+ /** Whether the generation recorded this input as absent. */
268
+ missing: boolean;
269
+ }
270
+ /**
271
+ * Hooks the bundler adapter passes into {@link transformTtsc} so transform
272
+ * side-channels (plugin-reported dependencies and host resolution candidates)
273
+ * reach the bundler without leaking extra fields on the returned
274
+ * `TransformResult`.
275
+ */
276
+ export interface TtscTransformHooks {
277
+ /**
278
+ * Invoked once per absolute watch-input path derived for the transformed file
279
+ * `F`: the plugin-reported `dependencies[F]` list unioned with the host-owned
280
+ * reference graph's contribution — the reachability closure of `graph.edges`
281
+ * from `F`, the `graph.globals` files, the `graph.configs` chain, and missing
282
+ * higher-priority `graph.candidates` — or, for a file the envelope declared
283
+ * `dependenciesComplete`, only `dependencies[F]`, `graph.candidates`, and the
284
+ * universal `graph.configs` chain. Adapters forward this to the bundler's
285
+ * `addWatchFile` so type-only inputs participate in watch-mode and
286
+ * persistent-cache invalidation. See {@link selectWatchInputs} for the exact
287
+ * derivation.
288
+ */
289
+ addWatchFile?: (file: string, evidence?: TtscWatchInputEvidence) => void;
290
+ /**
291
+ * Invoked when the plugin declared the transformed file volatile (the
292
+ * envelope's `volatile` list): its output depends on non-file inputs that no
293
+ * file-dependency snapshot can represent. Adapters should mark the module
294
+ * uncacheable where the bundler exposes that control (e.g. a webpack loader
295
+ * context's `cacheable(false)`).
296
+ */
297
+ markVolatile?: () => void;
298
+ }
299
+ /**
300
+ * Apply the ttsc plugin transform to a single source file.
301
+ *
302
+ * The function is intentionally project-scoped: it compiles the entire tsconfig
303
+ * project in one shot and extracts the result for `id`. Subsequent calls for
304
+ * sibling files in the same project reuse the cached result as long as none of
305
+ * the project's input files have changed (verified by comparing SHA-256
306
+ * hashes).
307
+ *
308
+ * Returns `undefined` when no transform is needed (declaration files, virtual
309
+ * modules, disabled plugins, or source unchanged after transform).
310
+ *
311
+ * @param id - Bundler module id (may carry a query string or virtual prefix).
312
+ * @param source - Current file content supplied by the bundler.
313
+ * @param options - Resolved plugin options.
314
+ * @param aliases - Raw bundler alias configuration (Vite array or webpack
315
+ * object).
316
+ * @param cache - Optional project cache. Callers with a real `buildStart`
317
+ * boundary declare it through {@link beginTtscTransformBuild}; other hosts
318
+ * retain persistent validation.
319
+ * @param hooks - Optional adapter callbacks; see {@link TtscTransformHooks}.
320
+ * Dependency notifications fire on cache hits too; watch registrations are
321
+ * per build, not per compilation.
322
+ */
323
+ export declare function transformTtsc(id: string, source: string, options: ResolvedTtscUnpluginOptions, aliases?: unknown, cache?: TtscTransformCache, hooks?: TtscTransformHooks): Promise<TtscTransformResult | undefined>;
324
+ interface TtscHostInputValidation {
325
+ /** Lexical input spellings that existed when the generation was captured. */
326
+ readonly entries: Map<string, {
327
+ path: string;
328
+ /**
329
+ * Whether the recorded state of this input came from reading its bytes.
330
+ * An input that existed but could not be read records a missing state, so
331
+ * no signature may stand in for it: its metadata holds still while the
332
+ * bytes behind it appear.
333
+ */
334
+ readable: boolean;
335
+ realpath: string | null;
336
+ /**
337
+ * The signature that may stand in for this entry's content comparison, or
338
+ * `undefined` when none may. A blocker keeps one regardless: it proves a
339
+ * kind and an identity rather than content.
340
+ */
341
+ signature: string | undefined;
342
+ strict?: true;
343
+ }>;
344
+ /**
345
+ * Lexical spellings the manifest accounts for, omitted from the per-module
346
+ * dependency loop below.
347
+ *
348
+ * Spellings, not identities: a symlink and its target share one identity but
349
+ * are two inputs, and skipping the alias because the manifest proved the
350
+ * target would leave the alias's own retarget unvalidated.
351
+ */
352
+ readonly covered: Set<string>;
353
+ /**
354
+ * Missing paths grouped by the nearest directory whose listing proves them
355
+ * absent.
356
+ */
357
+ readonly missing: Map<string, Set<string>>;
358
+ }
359
+ /**
360
+ * Strip a query string or hash fragment from a bundler module id.
361
+ *
362
+ * Vite appends query parameters (e.g. `?raw`, `?url`, `?inline`) to
363
+ * differentiate import variants of the same file. We must strip them before
364
+ * using the id as a file-system path.
365
+ */
366
+ export declare function stripQuery(id: string): string;
367
+ /**
368
+ * Returns `true` for TypeScript declaration files (`.d.ts`, `.d.mts`,
369
+ * `.d.cts`).
370
+ */
371
+ export declare function isDeclarationFile(id: string): boolean;
372
+ /**
373
+ * Build the unplugin transform result, or `undefined` when the transform
374
+ * produced no changes.
375
+ *
376
+ * Returning `undefined` instead of `{ code: source }` lets the bundler skip the
377
+ * unnecessary module update and preserves the original source map.
378
+ */
379
+ export declare function createTransformResult(source: string, code: string): TtscTransformResult | undefined;
380
+ /**
381
+ * Hash every input file under `projectRoot` (the same walk universe
382
+ * {@link matchesCachedSource} validates against), keyed by project-relative
383
+ * slash path. Exported so hosts without a per-build boundary (`@ttsc/metro`)
384
+ * can fold the identical input universe into their own cache fingerprints.
385
+ */
386
+ export declare function collectProjectInputHashes(projectRoot: string, identities?: FilesystemPathIdentityContext, filesystem?: TtscTransformFilesystemOperations): Record<string, string>;
387
+ /**
388
+ * Report whether an absolute `file` belongs to the project walk universe of
389
+ * `root`: it lies under `root`, every component exists without traversing a
390
+ * symbolic link, the leaf is a regular file, and no segment of the relative
391
+ * path is ignored. The predicate mirrors {@link walkProjectInputs} exactly, so
392
+ * "walk-visible" here means "hashed by {@link collectProjectInputHashes}".
393
+ * Missing paths and files reached through symlinks or Windows junctions are
394
+ * out-of-walk inputs that only the reference graph can prove relevant.
395
+ */
396
+ export declare function isProjectWalkPath(root: string, file: string, _identities?: FilesystemPathIdentityContext, filesystem?: TtscTransformFilesystemOperations): boolean;
397
+ /**
398
+ * Hash a list of absolute out-of-walk input paths: content SHA-256 for a
399
+ * readable file, a stable directory-kind digest for a directory candidate, and
400
+ * a stable `missing` marker otherwise. Keys use filesystem identity so
401
+ * case-only spellings share one snapshot entry, while reads retain the original
402
+ * path supplied by the compiler. The marker is state, not an error — a recorded
403
+ * input disappearing (or reappearing) must change the comparison exactly like a
404
+ * content edit. Exported so `@ttsc/metro` can re-hash its recorded snapshot
405
+ * with identical semantics at cache-key time.
406
+ */
407
+ export declare function collectExternalInputHashes(paths: readonly string[], filesystem?: TtscTransformFilesystemOperations): Record<string, string>;
408
+ /**
409
+ * Build a comparison key for a path without changing the spelling handed to a
410
+ * filesystem or bundler. Windows is case-insensitive; macOS is probed per
411
+ * existing filesystem location so case-sensitive volumes keep distinct paths.
412
+ */
413
+ export declare function pathIdentityKey(file: string, identities?: FilesystemPathIdentityContext): string;
414
+ export {};