@ttsc/unplugin 0.27.0 → 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.
- package/README.md +1 -1
- package/lib/core/index.d.ts +1 -1
- package/lib/core/index.js +49 -8
- package/lib/core/index.js.map +1 -1
- package/lib/core/index.mjs +49 -8
- package/lib/core/index.mjs.map +1 -1
- package/lib/core/transform.d.ts +144 -1
- package/lib/core/transform.js +1188 -159
- package/lib/core/transform.js.map +1 -1
- package/lib/core/transform.mjs +1188 -159
- package/lib/core/transform.mjs.map +1 -1
- package/lib/core/viteServe.d.ts +9 -2
- package/lib/core/viteServe.js +2 -2
- package/lib/core/viteServe.js.map +1 -1
- package/lib/core/viteServe.mjs +2 -2
- package/lib/core/viteServe.mjs.map +1 -1
- package/package.json +3 -3
- package/src/core/index.ts +54 -10
- package/src/core/transform.ts +1579 -198
- package/src/core/viteServe.ts +11 -4
package/README.md
CHANGED
|
@@ -312,7 +312,7 @@ The transform host reports the program's reference graph (the transform envelope
|
|
|
312
312
|
|
|
313
313
|
Transform plugins may additionally report, per file, the source files they consulted (the envelope's `dependencies` field); the adapter registers those as watch files too, union semantics. A plugin that declares such a list [complete](https://ttsc.dev/docs/development/concepts/protocol#dependency-completeness) for a file narrows the registration instead: only its own list plus the tsconfig chain, so files the transform never consulted stop invalidating it. The JavaScript host also reports the descriptor's loaded CommonJS graph, every package manifest inspected by auto-discovery, and plugin-declared `hostInputs`. First-party plugins use that last field for implicit config discovery and evaluated config dependencies. These remain universal without turning arbitrary project assets into configuration. Files a plugin declares `volatile` (output depending on non-file inputs such as environment or time) bypass the adapter's transform cache and are marked uncacheable where the bundler exposes that control.
|
|
314
314
|
|
|
315
|
-
The transform cache snapshots every regular file reached by the non-following project walk plus graph-reported inputs outside that walk (`node_modules` declarations, monorepo sibling sources, files reached through symlinks or Windows junctions, and out-of-root `extends` ancestry). One whole-project compile already contains every module's output. Adapters with a guaranteed build boundary clear the previous generation there, then check only the supplied source on each module's first delivery from a complete generation in that build. An incomplete generation never takes that shortcut. Later deliveries of a graph-bearing generation validate that file's reference closure, globals, configs, resolution candidates, plugin dependencies, and exact host inputs. Generation-scoped directory notifications detect new, removed, or renamed project inputs without repeating a directory-stat pass per module; Windows isolates those notifications so deletion of a watched temporary tree cannot crash the host. The project is snapshotted before and after compilation, while every graph member carries the compiler filesystem's content and physical-identity proof; only a complete generation that still matches both may authorize narrow reuse. This also rejects an in-project or external A→B→A change whose restored post-compile bytes would otherwise hide the transient state used by the compiler. `dependenciesComplete` narrows file inputs by the same contract as watch registration. Envelopes without a graph,
|
|
315
|
+
The transform cache snapshots every regular file reached by the non-following project walk plus graph-reported inputs outside that walk (`node_modules` declarations, monorepo sibling sources, files reached through symlinks or Windows junctions, and out-of-root `extends` ancestry). One whole-project compile already contains every module's output. Adapters with a guaranteed build boundary clear the previous generation there, then check only the supplied source on each module's first delivery from a complete generation in that build. An incomplete generation never takes that shortcut. Later deliveries of a graph-bearing generation validate that file's reference closure, globals, configs, resolution candidates, plugin dependencies, and exact host inputs. Every input a generation has already proven carries the nanosecond metadata signature captured around the read that proved it, so a later delivery that finds the signature unchanged reuses that proof instead of re-reading the bytes; any signature change falls back to the full content comparison. A signature is recorded only for a read nothing raced, only for an input whose recorded state came from bytes that were read, since the metadata of a path nothing could read holds still while the bytes behind it appear, and only once the observed filesystem's own clock has provably left the tick that minted the input's modification stamp. A filesystem stamps writes once per clock tick, so a same-length rewrite landing inside the recorded stamp's tick would leave the signature unchanged; until some stamp the same filesystem minted later separates that tick, the content comparison keeps running, and the signature is re-earned the moment it can be. The reference instants come from the filesystem itself, never from the process clock: every stamp the adapter observes raises a floor kept per reporting device, and the adapter also stamps a probe in its own scratch directory (the way git separates racily-clean index entries with the index file's own timestamp) to cover a tree whose files were all written inside one tick. The probe counts only when the scratch volume is the inputs' volume, so on a split-volume layout the observed stamps carry the rule alone; an input they cannot separate is re-read rather than trusted. Both sides of every comparison are therefore stamps of equal granularity minted by one clock, so a filesystem clock running behind the host changes nothing. The floor is a maximum over stamps it merely observed, so what defeats it is a stamp that was set into the future rather than minted, such as a stamp-preserving extraction or copy from a machine whose clock ran ahead. A restored past stamp is harmless, since it never raises the floor. A clock that jumps backwards strands the floor above the present the same way, which is a different hazard from a clock running at a constant offset: an offset moves both stamps being compared and changes nothing, while a jump moves only the present. No stamp-based freshness proof survives either one. Sibling modules therefore share one proof of the closure and the globals rather than repeating it per module. Generation-scoped directory notifications detect new, removed, or renamed project inputs without repeating a directory-stat pass per module; Windows isolates those notifications so deletion of a watched temporary tree cannot crash the host. An absent resolution candidate is the one input a metadata signature cannot stand for, since a path that is not there has no metadata to compare. The generation therefore registers those names with a watcher of their own, along with the directory components of the spelling that lead to them, down to the project's own root, and a delivery reads the notification instead of re-probing each candidate. That makes the notification the sole positive evidence for this one input class, which is why the registration reaches past the candidate itself: a watcher opened on a path that traverses a link follows it, so retargeting the link would otherwise move the answer without disturbing what is watched, and a package link inside `node_modules` is exactly that. It stops at the project root, and a candidate whose spelling leaves the project subtree before reaching it is not claimed at all: above that line the components are the machine's own layout rather than the project's, which nobody retargets and which changes for reasons no generation should hear about. It listens for renames alone, since every event that can change a candidate's answer is one — the file appearing, a component being created, replaced or retargeted — while the directories carrying them have their attributes moved by anything written below. A candidate whose watch could not be opened, whose watcher has since failed, or that belongs to a set spanning more directories than the generation will watch, is probed exactly as before: a host that runs out of watch descriptors would fail the tracker outright, which costs every delivery a whole-project comparison rather than one probe. What none of that covers is a filesystem that accepts a watch and then reports nothing, which some network mounts do; that is the same assumption the project-membership proof beside it already rests on, though a wider one here: the project walk skips `node_modules`, so a candidate under it was never covered by that proof and kept a notification-independent probe until now. A project on such a mount should be treated as one where notifications do not work rather than one where they merely have not fired. The barrier that lets a synchronous edit reach those watchers before a delivery reads their verdict is the watcher's own acknowledgement rather than a fixed wait: an in-process watcher answers on the next turn of the loop its callbacks are queued on, and the Windows broker answers by an ordered round-trip, so a delivery waits for the crossing instead of guessing at it. The project is snapshotted before and after compilation, while every graph member carries the compiler filesystem's content and physical-identity proof; only a complete generation that still matches both may authorize narrow reuse. This also rejects an in-project or external A→B→A change whose restored post-compile bytes would otherwise hide the transient state used by the compiler. `dependenciesComplete` narrows file inputs by the same contract as watch registration, and a project reaches it without a type-driven plugin: ttsc's own transform lanes print syntactically, so the host declares every file complete when no plugin can contribute to it, and the first-party `@ttsc/banner` and `@ttsc/strip` declare their own contribution the same way. Such a delivery validates the universal inputs and the file itself instead of its reference closure. A narrowed file's diagnostics narrow with it: an edit to a file the declaration dropped no longer re-runs the compile, so a type error introduced there surfaces at the next compile the build runs for another reason rather than at the edit. Run the compiler's own check beside the bundler when you want it at the edit. Envelopes without a graph, and generations whose watchers could not be opened or have since failed, retain complete-snapshot validation: losing a notification is the absence of a membership proof rather than evidence of a change, so such a generation keeps validating against its own recorded state (the directory snapshot, the input hashes, and the universal descriptor inputs whose every rejection is evidence of a change rather than an inability to prove one) instead of being discarded. A reported membership event is evidence, and still replaces the generation. Only an unstable or incomplete generation is refused outright. Bun runtime setup defines one process-scoped loading session. A dev server started without a watcher takes the build-scoped lifecycle instead of persistent validation. `server.watch: null` leaves the session no channel through which a file change could ever reach it, so what per-delivery validation buys there is incoherence rather than freshness: modules delivered before an edit and after it would come from two different compilations of one program. Each module's first delivery in such a session is therefore settled against the generation the session started from, exactly as under a build, while a module the session already delivered keeps revalidating on its next request. Long-lived hosts without either boundary (Metro workers, the Turbopack loader, and a watching Vite development server, whose initial `buildStart` spans later HMR edits) apply the file-specific validation on every generation hit.
|
|
316
316
|
|
|
317
317
|
## Sponsors
|
|
318
318
|
|
package/lib/core/index.d.ts
CHANGED
|
@@ -10,7 +10,7 @@ import { beginTtscTransformBuild, collectExternalInputHashes, collectProjectInpu
|
|
|
10
10
|
export declare const sourceFilePattern: RegExp;
|
|
11
11
|
declare const unplugin: UnpluginInstance<TtscUnpluginOptions | undefined, false>;
|
|
12
12
|
export type { TtscUnpluginCompilerOptionsJson, TtscUnpluginOptions, } from "./options";
|
|
13
|
-
export type { TtscTransformFilesystemOperations, TtscTransformHooks, } from "./transform";
|
|
13
|
+
export type { TtscTransformFilesystemOperations, TtscTransformHooks, TtscWatchInputEvidence, } from "./transform";
|
|
14
14
|
export { beginTtscTransformBuild, collectExternalInputHashes, collectProjectInputHashes, createTtscTransformCache, isProjectWalkPath, resetTtscTransformCache, resolveOptions, transformTtsc, unplugin, };
|
|
15
15
|
export default unplugin;
|
|
16
16
|
/**
|
package/lib/core/index.js
CHANGED
|
@@ -30,8 +30,10 @@ const virtualModulePattern = /\0/;
|
|
|
30
30
|
* The factory resolves raw options once, creates a per-build transform cache,
|
|
31
31
|
* and captures Vite alias configuration via the `vite.configResolved` hook so
|
|
32
32
|
* that path aliases are forwarded to the generated tsconfig overlay. Real build
|
|
33
|
-
* lifecycles use a per-build cache; Vite
|
|
34
|
-
* validation because its one `buildStart` spans later HMR edits
|
|
33
|
+
* lifecycles use a per-build cache; a watching Vite development server keeps
|
|
34
|
+
* persistent validation because its one `buildStart` spans later HMR edits,
|
|
35
|
+
* while a dev server configured without a watcher takes the build-scoped path
|
|
36
|
+
* with them, having declared it will observe no edit at all.
|
|
35
37
|
*/
|
|
36
38
|
const unpluginFactory = (rawOptions = {}) => {
|
|
37
39
|
const options$1 = options.resolveOptions(rawOptions);
|
|
@@ -39,6 +41,7 @@ const unpluginFactory = (rawOptions = {}) => {
|
|
|
39
41
|
const missingInputs = viteServe.createViteServeMissingInputWatch();
|
|
40
42
|
let aliases;
|
|
41
43
|
let viteCommand;
|
|
44
|
+
let viteWatching = true;
|
|
42
45
|
return {
|
|
43
46
|
name,
|
|
44
47
|
enforce: "pre",
|
|
@@ -50,6 +53,16 @@ const unpluginFactory = (rawOptions = {}) => {
|
|
|
50
53
|
// serve-time poll, even though the closed server stays attached
|
|
51
54
|
// (see the dispose note in viteServe.ts).
|
|
52
55
|
viteCommand = config.command;
|
|
56
|
+
// `server.watch: null` disables Vite's watcher outright, which is how
|
|
57
|
+
// a one-shot consumer (a `vitest --run` suite above all) configures the
|
|
58
|
+
// dev server. Nothing can then deliver a change event, so every watch
|
|
59
|
+
// registration is dead weight, and not cheap dead weight: Vite's
|
|
60
|
+
// import analysis resolves each registered path like a real import of
|
|
61
|
+
// the transformed module, once per module, which is the dominant cost
|
|
62
|
+
// of a delivered module in a project with a real dependency graph
|
|
63
|
+
// (samchon/ttsc#1246).
|
|
64
|
+
viteWatching =
|
|
65
|
+
config.server?.watch !== null;
|
|
53
66
|
},
|
|
54
67
|
// Vite serve funnels every transform-context `addWatchFile()` into the
|
|
55
68
|
// module's added-import graph (`_addedImports`), which import-analysis
|
|
@@ -67,7 +80,22 @@ const unpluginFactory = (rawOptions = {}) => {
|
|
|
67
80
|
},
|
|
68
81
|
},
|
|
69
82
|
buildStart() {
|
|
70
|
-
|
|
83
|
+
// Persistent validation exists for a session that spans edits it can
|
|
84
|
+
// observe, and a dev server told to open no watcher is not one:
|
|
85
|
+
// `server.watch: null` leaves Vite with no change channel at all, so no
|
|
86
|
+
// edit can reach the session, nothing invalidates what one touched, and
|
|
87
|
+
// no client is hot-updated. Validating each delivery there does not buy
|
|
88
|
+
// freshness, it buys incoherence — modules delivered before an edit and
|
|
89
|
+
// after it would come from two different compilations of one program —
|
|
90
|
+
// while costing a full derived-input proof per delivered module. The
|
|
91
|
+
// build-scoped lifecycle settles each module's first delivery against the
|
|
92
|
+
// generation the session started from, exactly as a build does, and still
|
|
93
|
+
// revalidates a module this session already delivered. A one-shot suite
|
|
94
|
+
// configures precisely this server (`vitest --run` sets `server.watch =
|
|
95
|
+
// null`) and is the workload behind samchon/ttsc#970
|
|
96
|
+
// (samchon/ttsc#1260). The neighbouring watch-registration decision reads
|
|
97
|
+
// the same two properties for the same reason.
|
|
98
|
+
if (viteCommand === "serve" && viteWatching) {
|
|
71
99
|
transform.resetTtscTransformCache(transformCache);
|
|
72
100
|
}
|
|
73
101
|
else {
|
|
@@ -94,11 +122,24 @@ const unpluginFactory = (rawOptions = {}) => {
|
|
|
94
122
|
// (a superseding resolution candidate, a not-yet-generated
|
|
95
123
|
// dependency), so those are watched on the filesystem instead and
|
|
96
124
|
// invalidate this module when created.
|
|
97
|
-
addWatchFile: (watched) => {
|
|
98
|
-
if (viteCommand === "serve" &&
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
125
|
+
addWatchFile: (watched, evidence) => {
|
|
126
|
+
if (viteCommand === "serve" && missingInputs.serving()) {
|
|
127
|
+
// Trust the generation's recorded existence when it supplied one:
|
|
128
|
+
// every cache hit revalidates it, and probing each input again
|
|
129
|
+
// costs one `existsSync` per input per delivered module.
|
|
130
|
+
const missing = evidence?.missing ?? !fs.existsSync(watched);
|
|
131
|
+
if (missing) {
|
|
132
|
+
missingInputs.watch(watched, path.resolve(file), evidence?.identity);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
// A dev server configured without a watcher can never deliver a
|
|
137
|
+
// change event, so a registration here buys nothing, and it is not
|
|
138
|
+
// free: Vite's import analysis resolves every registered path like a
|
|
139
|
+
// real import of the transformed module, once per module. The
|
|
140
|
+
// adapter's own missing-input poll above stays active either way,
|
|
141
|
+
// because it never depended on Vite's watcher.
|
|
142
|
+
if (viteCommand === "serve" && !viteWatching) {
|
|
102
143
|
return;
|
|
103
144
|
}
|
|
104
145
|
this.addWatchFile(watched);
|
package/lib/core/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sources":["../../src/core/index.ts"],"sourcesContent":[null],"names":["options","resolveOptions","createTtscTransformCache","createViteServeMissingInputWatch","resetTtscTransformCache","beginTtscTransformBuild","stripQuery","transformTtsc","createUnplugin","isDeclarationFile"],"mappings":";;;;;;;;;;;AAoBA,MAAM,IAAI,GAAG,eAAe;AAC5B;;;;AAIG;AACI,MAAM,iBAAiB,GAAG;AACjC;AACA,MAAM,kBAAkB,GAAG,oCAAoC;AAC/D;;;AAGG;AACH,MAAM,oBAAoB,GAAG,IAAI;AAEjC
|
|
1
|
+
{"version":3,"file":"index.js","sources":["../../src/core/index.ts"],"sourcesContent":[null],"names":["options","resolveOptions","createTtscTransformCache","createViteServeMissingInputWatch","resetTtscTransformCache","beginTtscTransformBuild","stripQuery","transformTtsc","createUnplugin","isDeclarationFile"],"mappings":";;;;;;;;;;;AAoBA,MAAM,IAAI,GAAG,eAAe;AAC5B;;;;AAIG;AACI,MAAM,iBAAiB,GAAG;AACjC;AACA,MAAM,kBAAkB,GAAG,oCAAoC;AAC/D;;;AAGG;AACH,MAAM,oBAAoB,GAAG,IAAI;AAEjC;;;;;;;;;;;AAWG;AACH,MAAM,eAAe,GAGjB,CAAC,UAAU,GAAG,EAAE,KAAI;AACtB,IAAA,MAAMA,SAAO,GAAGC,sBAAc,CAAC,UAAU,CAAC;AAC1C,IAAA,MAAM,cAAc,GAAGC,kCAAwB,EAAE;AACjD,IAAA,MAAM,aAAa,GAAGC,0CAAgC,EAAE;AACxD,IAAA,IAAI,OAAgB;AACpB,IAAA,IAAI,WAA+B;IACnC,IAAI,YAAY,GAAG,IAAI;IAEvB,OAAO;QACL,IAAI;AACJ,QAAA,OAAO,EAAE,KAAK;AAEd,QAAA,IAAI,EAAE;AACJ,YAAA,cAAc,CAAC,MAAM,EAAA;AACnB,gBAAA,OAAO,GAAG,MAAM,CAAC,OAAO,CAAC,KAAK;;;;;AAK9B,gBAAA,WAAW,GAAG,MAAM,CAAC,OAAO;;;;;;;;;gBAS5B,YAAY;AACT,oBAAA,MAA2C,CAAC,MAAM,EAAE,KAAK,KAAK,IAAI;YACvE,CAAC;;;;;;;AAOD,YAAA,eAAe,CAAC,MAAM,EAAA;AACpB,gBAAA,aAAa,CAAC,MAAM,CAAC,MAAM,CAAC;YAC9B,CAAC;;;YAGD,QAAQ,GAAA;gBACN,aAAa,CAAC,OAAO,EAAE;YACzB,CAAC;AACF,SAAA;QAED,UAAU,GAAA;;;;;;;;;;;;;;;;AAgBR,YAAA,IAAI,WAAW,KAAK,OAAO,IAAI,YAAY,EAAE;gBAC3CC,iCAAuB,CAAC,cAAc,CAAC;YACzC;iBAAO;gBACLC,iCAAuB,CAAC,cAAc,CAAC;YACzC;QACF,CAAC;AAED,QAAA,gBAAgB,CAAC,EAAE,EAAA;AACjB,YAAA,MAAM,IAAI,GAAGC,oBAAU,CAAC,EAAE,CAAC;AAC3B,YAAA,OAAO,iBAAiB,CAAC,IAAI,CAAC;QAChC,CAAC;AAED,QAAA,MAAM,SAAS,CAAC,MAAM,EAAE,EAAE,EAAA;AACxB,YAAA,MAAM,IAAI,GAAGA,oBAAU,CAAC,EAAE,CAAC;AAC3B,YAAA,IAAI,CAAC,iBAAiB,CAAC,IAAI,CAAC,EAAE;AAC5B,gBAAA,OAAO,SAAS;YAClB;YACA,OAAOC,uBAAa,CAAC,IAAI,EAAE,MAAM,EAAEP,SAAO,EAAE,OAAO,EAAE,cAAc,EAAE;;;;;;;;;;;AAWnE,gBAAA,YAAY,EAAE,CAAC,OAAO,EAAE,QAAQ,KAAI;oBAClC,IAAI,WAAW,KAAK,OAAO,IAAI,aAAa,CAAC,OAAO,EAAE,EAAE;;;;AAItD,wBAAA,MAAM,OAAO,GAAG,QAAQ,EAAE,OAAO,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,OAAO,CAAC;wBAC5D,IAAI,OAAO,EAAE;AACX,4BAAA,aAAa,CAAC,KAAK,CACjB,OAAO,EACP,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,EAClB,QAAQ,EAAE,QAAQ,CACnB;4BACD;wBACF;oBACF;;;;;;;AAOA,oBAAA,IAAI,WAAW,KAAK,OAAO,IAAI,CAAC,YAAY,EAAE;wBAC5C;oBACF;AACA,oBAAA,IAAI,CAAC,YAAY,CAAC,OAAO,CAAC;gBAC5B,CAAC;;;;gBAID,YAAY,EAAE,MAAK;AACjB,oBAAA,MAAM,MAAM,GAAG,IAAI,CAAC,qBAAqB,IAAI;AAC7C,oBAAA,IACE,MAAM,EAAE,SAAS,KAAK,SAAS;AAC/B,wBAAA,MAAM,EAAE,SAAS,KAAK,QAAQ,EAC9B;wBACA,MAAM,CAAC,aAAa,EAAE,SAAS,GAAG,KAAK,CAAC;oBAC1C;gBACF,CAAC;AACF,aAAA,CAAC;QACJ,CAAC;KACF;AACH,CAAC;AAED,MAAM,QAAQ,GACZQ,yBAAc,CAAC,eAAe;AAyBhC;;;;;;AAMG;AACG,SAAU,iBAAiB,CAAC,EAAU,EAAA;AAC1C,IAAA,QACE,iBAAiB,CAAC,IAAI,CAAC,EAAE,CAAC;AAC1B,QAAA,CAAC,oBAAoB,CAAC,IAAI,CAAC,EAAE,CAAC;QAC9B,CAACC,2BAAiB,CAAC,EAAE,CAAC;AACtB,QAAA,CAAC,kBAAkB,CAAC,IAAI,CAAC,EAAE,CAAC;AAEhC;;;;;;;;;;;;;;;"}
|
package/lib/core/index.mjs
CHANGED
|
@@ -27,8 +27,10 @@ const virtualModulePattern = /\0/;
|
|
|
27
27
|
* The factory resolves raw options once, creates a per-build transform cache,
|
|
28
28
|
* and captures Vite alias configuration via the `vite.configResolved` hook so
|
|
29
29
|
* that path aliases are forwarded to the generated tsconfig overlay. Real build
|
|
30
|
-
* lifecycles use a per-build cache; Vite
|
|
31
|
-
* validation because its one `buildStart` spans later HMR edits
|
|
30
|
+
* lifecycles use a per-build cache; a watching Vite development server keeps
|
|
31
|
+
* persistent validation because its one `buildStart` spans later HMR edits,
|
|
32
|
+
* while a dev server configured without a watcher takes the build-scoped path
|
|
33
|
+
* with them, having declared it will observe no edit at all.
|
|
32
34
|
*/
|
|
33
35
|
const unpluginFactory = (rawOptions = {}) => {
|
|
34
36
|
const options = resolveOptions(rawOptions);
|
|
@@ -36,6 +38,7 @@ const unpluginFactory = (rawOptions = {}) => {
|
|
|
36
38
|
const missingInputs = createViteServeMissingInputWatch();
|
|
37
39
|
let aliases;
|
|
38
40
|
let viteCommand;
|
|
41
|
+
let viteWatching = true;
|
|
39
42
|
return {
|
|
40
43
|
name,
|
|
41
44
|
enforce: "pre",
|
|
@@ -47,6 +50,16 @@ const unpluginFactory = (rawOptions = {}) => {
|
|
|
47
50
|
// serve-time poll, even though the closed server stays attached
|
|
48
51
|
// (see the dispose note in viteServe.ts).
|
|
49
52
|
viteCommand = config.command;
|
|
53
|
+
// `server.watch: null` disables Vite's watcher outright, which is how
|
|
54
|
+
// a one-shot consumer (a `vitest --run` suite above all) configures the
|
|
55
|
+
// dev server. Nothing can then deliver a change event, so every watch
|
|
56
|
+
// registration is dead weight, and not cheap dead weight: Vite's
|
|
57
|
+
// import analysis resolves each registered path like a real import of
|
|
58
|
+
// the transformed module, once per module, which is the dominant cost
|
|
59
|
+
// of a delivered module in a project with a real dependency graph
|
|
60
|
+
// (samchon/ttsc#1246).
|
|
61
|
+
viteWatching =
|
|
62
|
+
config.server?.watch !== null;
|
|
50
63
|
},
|
|
51
64
|
// Vite serve funnels every transform-context `addWatchFile()` into the
|
|
52
65
|
// module's added-import graph (`_addedImports`), which import-analysis
|
|
@@ -64,7 +77,22 @@ const unpluginFactory = (rawOptions = {}) => {
|
|
|
64
77
|
},
|
|
65
78
|
},
|
|
66
79
|
buildStart() {
|
|
67
|
-
|
|
80
|
+
// Persistent validation exists for a session that spans edits it can
|
|
81
|
+
// observe, and a dev server told to open no watcher is not one:
|
|
82
|
+
// `server.watch: null` leaves Vite with no change channel at all, so no
|
|
83
|
+
// edit can reach the session, nothing invalidates what one touched, and
|
|
84
|
+
// no client is hot-updated. Validating each delivery there does not buy
|
|
85
|
+
// freshness, it buys incoherence — modules delivered before an edit and
|
|
86
|
+
// after it would come from two different compilations of one program —
|
|
87
|
+
// while costing a full derived-input proof per delivered module. The
|
|
88
|
+
// build-scoped lifecycle settles each module's first delivery against the
|
|
89
|
+
// generation the session started from, exactly as a build does, and still
|
|
90
|
+
// revalidates a module this session already delivered. A one-shot suite
|
|
91
|
+
// configures precisely this server (`vitest --run` sets `server.watch =
|
|
92
|
+
// null`) and is the workload behind samchon/ttsc#970
|
|
93
|
+
// (samchon/ttsc#1260). The neighbouring watch-registration decision reads
|
|
94
|
+
// the same two properties for the same reason.
|
|
95
|
+
if (viteCommand === "serve" && viteWatching) {
|
|
68
96
|
resetTtscTransformCache(transformCache);
|
|
69
97
|
}
|
|
70
98
|
else {
|
|
@@ -91,11 +119,24 @@ const unpluginFactory = (rawOptions = {}) => {
|
|
|
91
119
|
// (a superseding resolution candidate, a not-yet-generated
|
|
92
120
|
// dependency), so those are watched on the filesystem instead and
|
|
93
121
|
// invalidate this module when created.
|
|
94
|
-
addWatchFile: (watched) => {
|
|
95
|
-
if (viteCommand === "serve" &&
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
122
|
+
addWatchFile: (watched, evidence) => {
|
|
123
|
+
if (viteCommand === "serve" && missingInputs.serving()) {
|
|
124
|
+
// Trust the generation's recorded existence when it supplied one:
|
|
125
|
+
// every cache hit revalidates it, and probing each input again
|
|
126
|
+
// costs one `existsSync` per input per delivered module.
|
|
127
|
+
const missing = evidence?.missing ?? !fs.existsSync(watched);
|
|
128
|
+
if (missing) {
|
|
129
|
+
missingInputs.watch(watched, path.resolve(file), evidence?.identity);
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
// A dev server configured without a watcher can never deliver a
|
|
134
|
+
// change event, so a registration here buys nothing, and it is not
|
|
135
|
+
// free: Vite's import analysis resolves every registered path like a
|
|
136
|
+
// real import of the transformed module, once per module. The
|
|
137
|
+
// adapter's own missing-input poll above stays active either way,
|
|
138
|
+
// because it never depended on Vite's watcher.
|
|
139
|
+
if (viteCommand === "serve" && !viteWatching) {
|
|
99
140
|
return;
|
|
100
141
|
}
|
|
101
142
|
this.addWatchFile(watched);
|
package/lib/core/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","sources":["../../src/core/index.ts"],"sourcesContent":[null],"names":[],"mappings":";;;;;;;;AAoBA,MAAM,IAAI,GAAG,eAAe;AAC5B;;;;AAIG;AACI,MAAM,iBAAiB,GAAG;AACjC;AACA,MAAM,kBAAkB,GAAG,oCAAoC;AAC/D;;;AAGG;AACH,MAAM,oBAAoB,GAAG,IAAI;AAEjC
|
|
1
|
+
{"version":3,"file":"index.mjs","sources":["../../src/core/index.ts"],"sourcesContent":[null],"names":[],"mappings":"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"}
|
package/lib/core/transform.d.ts
CHANGED
|
@@ -32,6 +32,27 @@ interface TtscProjectDirectorySnapshot {
|
|
|
32
32
|
/** Generation-scoped directory watchers used to detect membership changes. */
|
|
33
33
|
interface TtscProjectMutationTracker {
|
|
34
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>;
|
|
35
56
|
failed: boolean;
|
|
36
57
|
membershipChanged: boolean;
|
|
37
58
|
settle?: Promise<void>;
|
|
@@ -73,15 +94,68 @@ export interface TtscCachedProjectTransform {
|
|
|
73
94
|
* compiler reported rather than a normalized replacement spelling.
|
|
74
95
|
*/
|
|
75
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>;
|
|
76
111
|
/**
|
|
77
112
|
* SHA-256 hash of each project-relative input path at the time of the
|
|
78
113
|
* transform.
|
|
79
114
|
*/
|
|
80
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>;
|
|
81
127
|
/** Metadata snapshot of every directory in the stable generation walk. */
|
|
82
128
|
projectDirectories?: TtscProjectDirectorySnapshot[];
|
|
83
129
|
/** Live notification state for universal host-input changes. */
|
|
84
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;
|
|
85
159
|
/** Live notification state for file/directory creation, deletion, and rename. */
|
|
86
160
|
projectMutationTracker?: TtscProjectMutationTracker;
|
|
87
161
|
/**
|
|
@@ -137,6 +211,23 @@ export interface TtscTransformFilesystemOperations {
|
|
|
137
211
|
statBigInt(location: string): fs.BigIntStats;
|
|
138
212
|
/** Override path parsing when the observed filesystem is not the host. */
|
|
139
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
|
+
};
|
|
140
231
|
}
|
|
141
232
|
/** Normalize one directory entry under the owning filesystem's case policy. */
|
|
142
233
|
export declare function normalizeHostInputName(name: string, caseSensitive: boolean): string;
|
|
@@ -159,6 +250,23 @@ export declare function beginTtscTransformBuild(cache: TtscTransformCache): void
|
|
|
159
250
|
* many edits, so that callback cannot authorize build-scoped shortcuts.
|
|
160
251
|
*/
|
|
161
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
|
+
}
|
|
162
270
|
/**
|
|
163
271
|
* Hooks the bundler adapter passes into {@link transformTtsc} so transform
|
|
164
272
|
* side-channels (plugin-reported dependencies and host resolution candidates)
|
|
@@ -178,7 +286,7 @@ export interface TtscTransformHooks {
|
|
|
178
286
|
* persistent-cache invalidation. See {@link selectWatchInputs} for the exact
|
|
179
287
|
* derivation.
|
|
180
288
|
*/
|
|
181
|
-
addWatchFile?: (file: string) => void;
|
|
289
|
+
addWatchFile?: (file: string, evidence?: TtscWatchInputEvidence) => void;
|
|
182
290
|
/**
|
|
183
291
|
* Invoked when the plugin declared the transformed file volatile (the
|
|
184
292
|
* envelope's `volatile` list): its output depends on non-file inputs that no
|
|
@@ -213,6 +321,41 @@ export interface TtscTransformHooks {
|
|
|
213
321
|
* per build, not per compilation.
|
|
214
322
|
*/
|
|
215
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
|
+
}
|
|
216
359
|
/**
|
|
217
360
|
* Strip a query string or hash fragment from a bundler module id.
|
|
218
361
|
*
|