@ttsc/unplugin 0.28.5 → 0.29.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 +15 -11
- package/lib/api.js +10 -0
- package/lib/api.js.map +1 -1
- package/lib/api.mjs +3 -2
- package/lib/api.mjs.map +1 -1
- package/lib/bun-register.d.cts +10 -7
- package/lib/bun-register.d.mts +10 -7
- package/lib/bun-register.d.ts +10 -7
- package/lib/bun-register.js +90 -24
- package/lib/bun-register.js.map +1 -1
- package/lib/bun-register.mjs +90 -24
- package/lib/bun-register.mjs.map +1 -1
- package/lib/bun.d.cts +10 -8
- package/lib/bun.d.mts +10 -8
- package/lib/bun.d.ts +10 -8
- package/lib/bun.js +19 -17
- package/lib/bun.js.map +1 -1
- package/lib/bun.mjs +20 -18
- package/lib/bun.mjs.map +1 -1
- package/lib/core/index.d.cts +10 -8
- package/lib/core/index.d.mts +10 -8
- package/lib/core/index.d.ts +10 -8
- package/lib/core/index.js +88 -16
- package/lib/core/index.js.map +1 -1
- package/lib/core/index.mjs +81 -18
- package/lib/core/index.mjs.map +1 -1
- package/lib/core/projectDiscovery.d.cts +55 -0
- package/lib/core/projectDiscovery.d.mts +55 -0
- package/lib/core/projectDiscovery.d.ts +55 -0
- package/lib/core/projectDiscovery.js +187 -0
- package/lib/core/projectDiscovery.js.map +1 -0
- package/lib/core/projectDiscovery.mjs +182 -0
- package/lib/core/projectDiscovery.mjs.map +1 -0
- package/lib/core/sourceExtensions.d.cts +10 -0
- package/lib/core/sourceExtensions.d.mts +10 -0
- package/lib/core/sourceExtensions.d.ts +10 -0
- package/lib/core/sourceExtensions.js +38 -0
- package/lib/core/sourceExtensions.js.map +1 -0
- package/lib/core/sourceExtensions.mjs +32 -0
- package/lib/core/sourceExtensions.mjs.map +1 -0
- package/lib/core/transform.d.cts +96 -16
- package/lib/core/transform.d.mts +96 -16
- package/lib/core/transform.d.ts +96 -16
- package/lib/core/transform.js +1295 -309
- package/lib/core/transform.js.map +1 -1
- package/lib/core/transform.mjs +1290 -310
- package/lib/core/transform.mjs.map +1 -1
- package/lib/core/tsconfigPaths.d.cts +55 -3
- package/lib/core/tsconfigPaths.d.mts +55 -3
- package/lib/core/tsconfigPaths.d.ts +55 -3
- package/lib/core/tsconfigPaths.js +260 -46
- package/lib/core/tsconfigPaths.js.map +1 -1
- package/lib/core/tsconfigPaths.mjs +255 -47
- package/lib/core/tsconfigPaths.mjs.map +1 -1
- package/lib/core/viteServe.d.cts +18 -21
- package/lib/core/viteServe.d.mts +18 -21
- package/lib/core/viteServe.d.ts +18 -21
- package/lib/core/viteServe.js +88 -47
- package/lib/core/viteServe.js.map +1 -1
- package/lib/core/viteServe.mjs +89 -49
- package/lib/core/viteServe.mjs.map +1 -1
- package/lib/next.d.cts +7 -0
- package/lib/next.d.mts +7 -0
- package/lib/next.d.ts +7 -0
- package/lib/next.js +186 -54
- package/lib/next.js.map +1 -1
- package/lib/next.mjs +186 -55
- package/lib/next.mjs.map +1 -1
- package/lib/turbopack.d.cts +6 -4
- package/lib/turbopack.d.mts +6 -4
- package/lib/turbopack.d.ts +6 -4
- package/lib/turbopack.js +11 -10
- package/lib/turbopack.js.map +1 -1
- package/lib/turbopack.mjs +11 -10
- package/lib/turbopack.mjs.map +1 -1
- package/package.json +7 -4
- package/src/bun-register.ts +112 -25
- package/src/bun.ts +60 -50
- package/src/core/index.ts +114 -19
- package/src/core/projectDiscovery.ts +256 -0
- package/src/core/sourceExtensions.ts +47 -0
- package/src/core/transform.ts +1671 -361
- package/src/core/tsconfigPaths.ts +406 -46
- package/src/core/viteServe.ts +125 -74
- package/src/next.ts +230 -54
- package/src/turbopack.ts +11 -10
package/src/core/viteServe.ts
CHANGED
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import type { ITtscCompilerTransformation } from "ttsc";
|
|
3
4
|
|
|
4
|
-
import { pathIdentityKey } from "./transform";
|
|
5
|
+
import { pathIdentityKey, validateGraphInputObservation } from "./transform";
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
|
-
* How often each registered
|
|
8
|
+
* How often each registered Vite-unsafe input is predicate-polled, in
|
|
8
9
|
* milliseconds.
|
|
9
10
|
*
|
|
10
11
|
* Polling is the only watch primitive that covers the whole class: the dev
|
|
11
12
|
* server's chokidar watcher ignores every `node_modules` directory, which is
|
|
12
13
|
* exactly where superseding resolution candidates usually live, and `fs.watch`
|
|
13
14
|
* cannot observe a path whose parent directories do not exist yet. One `stat`
|
|
14
|
-
* every half second per
|
|
15
|
-
* baseline.
|
|
15
|
+
* every half second per unavailable path is negligible against a dev server's
|
|
16
|
+
* baseline. Rich inputs replay only the predicates the compiler recorded.
|
|
16
17
|
*/
|
|
17
18
|
const MISSING_INPUT_POLL_INTERVAL = 500;
|
|
18
19
|
|
|
@@ -55,24 +56,23 @@ export interface ViteDevServerLike {
|
|
|
55
56
|
}
|
|
56
57
|
|
|
57
58
|
/**
|
|
58
|
-
* Filesystem watch for derived watch inputs that
|
|
59
|
-
* server is running.
|
|
59
|
+
* Filesystem watch for derived watch inputs that Vite cannot safely register as
|
|
60
|
+
* added imports while a development server is running.
|
|
60
61
|
*
|
|
61
62
|
* Vite serve treats every transform-context `addWatchFile()` registration as an
|
|
62
63
|
* added import: `TransformPluginContext.addWatchFile` stores the path in
|
|
63
64
|
* `_addedImports`, and `vite:import-analysis` resolves each entry like a real
|
|
64
|
-
* import of the transformed module. A missing path
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
* though the transform itself succeeded.
|
|
65
|
+
* import of the transformed module. A missing path or an existing directory
|
|
66
|
+
* that failed a compiler file predicate then fails that resolve and turns the
|
|
67
|
+
* importer's first request into a 500, even though the transform succeeded.
|
|
68
68
|
*
|
|
69
69
|
* This registry is the serve-only replacement for those registrations. Each
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
* the new resolution winner. The
|
|
74
|
-
*
|
|
75
|
-
*
|
|
70
|
+
* path is polled until its exact compiler predicate observation changes; legacy
|
|
71
|
+
* envelopes retain their exists-or-file availability check. Its importers are
|
|
72
|
+
* then invalidated in the server's module graphs and one full-reload is sent,
|
|
73
|
+
* so the next request retransforms against the new resolution winner. The
|
|
74
|
+
* project transform cache re-validates the compiler observation, so the
|
|
75
|
+
* retransform recompiles instead of replaying.
|
|
76
76
|
*/
|
|
77
77
|
export interface ViteServeMissingInputWatch {
|
|
78
78
|
/** Adopt the dev server whose module graphs creation events invalidate. */
|
|
@@ -86,32 +86,57 @@ export interface ViteServeMissingInputWatch {
|
|
|
86
86
|
* config's `command`, as the adapter does.
|
|
87
87
|
*/
|
|
88
88
|
serving(): boolean;
|
|
89
|
-
/**
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
* per-call identity context memoizes nothing (samchon/ttsc#1246).
|
|
96
|
-
*/
|
|
97
|
-
watch(input: string, importer: string, identity?: string): void;
|
|
89
|
+
/** Register one unsafe watch input and its exact recorded condition. */
|
|
90
|
+
watch(
|
|
91
|
+
input: string,
|
|
92
|
+
importer: string,
|
|
93
|
+
condition: ViteServeInputWatchCondition,
|
|
94
|
+
): void;
|
|
98
95
|
}
|
|
99
96
|
|
|
100
|
-
/**
|
|
97
|
+
/** A legacy availability condition or an exact compiler predicate proof. */
|
|
98
|
+
export type ViteServeInputWatchCondition =
|
|
99
|
+
| "exists"
|
|
100
|
+
| "file"
|
|
101
|
+
| ITtscCompilerTransformation.IInputObservation;
|
|
102
|
+
|
|
103
|
+
/** Poll bookkeeping for one registered unsafe path. */
|
|
101
104
|
interface IMissingInputEntry {
|
|
105
|
+
condition: ViteServeInputWatchCondition;
|
|
102
106
|
importers: Set<string>;
|
|
103
|
-
listener: (current: fs.Stats) => void;
|
|
104
107
|
spelling: string;
|
|
105
108
|
}
|
|
106
109
|
|
|
107
110
|
/** Create an empty missing-input watch for one plugin instance. */
|
|
108
111
|
export function createViteServeMissingInputWatch(): ViteServeMissingInputWatch {
|
|
109
112
|
const entries = new Map<string, IMissingInputEntry>();
|
|
113
|
+
let poller: NodeJS.Timeout | undefined;
|
|
110
114
|
let server: ViteDevServerLike | undefined;
|
|
111
115
|
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
|
|
116
|
+
const stopPollingIfEmpty = (): void => {
|
|
117
|
+
if (entries.size !== 0 || poller === undefined) {
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
clearInterval(poller);
|
|
121
|
+
poller = undefined;
|
|
122
|
+
};
|
|
123
|
+
const poll = (): void => {
|
|
124
|
+
const importers = new Set<string>();
|
|
125
|
+
for (const [identity, entry] of entries) {
|
|
126
|
+
if (!viteServeInputWatchConditionChanged(entry)) {
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
entries.delete(identity);
|
|
130
|
+
for (const importer of entry.importers) {
|
|
131
|
+
importers.add(importer);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
stopPollingIfEmpty();
|
|
135
|
+
if (server === undefined || importers.size === 0) {
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
invalidateImporters(server, importers);
|
|
139
|
+
sendFullReload(server);
|
|
115
140
|
};
|
|
116
141
|
|
|
117
142
|
return {
|
|
@@ -119,8 +144,10 @@ export function createViteServeMissingInputWatch(): ViteServeMissingInputWatch {
|
|
|
119
144
|
server = next;
|
|
120
145
|
},
|
|
121
146
|
dispose() {
|
|
122
|
-
|
|
123
|
-
|
|
147
|
+
entries.clear();
|
|
148
|
+
if (poller !== undefined) {
|
|
149
|
+
clearInterval(poller);
|
|
150
|
+
poller = undefined;
|
|
124
151
|
}
|
|
125
152
|
// The server reference deliberately survives: `vite.restartServer`
|
|
126
153
|
// configures the replacement server (attach) before it closes the old
|
|
@@ -132,60 +159,84 @@ export function createViteServeMissingInputWatch(): ViteServeMissingInputWatch {
|
|
|
132
159
|
serving() {
|
|
133
160
|
return server !== undefined;
|
|
134
161
|
},
|
|
135
|
-
watch(input, importer,
|
|
162
|
+
watch(input, importer, condition) {
|
|
136
163
|
const spelling = path.resolve(input);
|
|
137
|
-
const identity =
|
|
164
|
+
const identity = viteServeMissingInputWatchKey(spelling, condition);
|
|
138
165
|
const existing = entries.get(identity);
|
|
139
166
|
if (existing !== undefined) {
|
|
140
167
|
existing.importers.add(path.resolve(importer));
|
|
141
168
|
return;
|
|
142
169
|
}
|
|
143
|
-
|
|
170
|
+
entries.set(identity, {
|
|
171
|
+
condition,
|
|
144
172
|
importers: new Set([path.resolve(importer)]),
|
|
145
|
-
listener: (current) => {
|
|
146
|
-
// `fs.watchFile` reports a missing path as zeroed stats (and fires
|
|
147
|
-
// once with them right after registration); only a poll that
|
|
148
|
-
// observes a real file is a creation event.
|
|
149
|
-
if (current.mtimeMs === 0 && !fs.existsSync(entry.spelling)) {
|
|
150
|
-
return;
|
|
151
|
-
}
|
|
152
|
-
unwatch(identity, entry);
|
|
153
|
-
if (server === undefined) {
|
|
154
|
-
return;
|
|
155
|
-
}
|
|
156
|
-
invalidateImporters(server, entry.importers);
|
|
157
|
-
sendFullReload(server);
|
|
158
|
-
},
|
|
159
|
-
spelling,
|
|
160
|
-
};
|
|
161
|
-
entries.set(identity, entry);
|
|
162
|
-
const watcher = fs.watchFile(
|
|
163
173
|
spelling,
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
// only on a subsequent change, so a file created between the adapter's
|
|
171
|
-
// existence check and this registration would count as "unchanged" and
|
|
172
|
-
// never fire. One deferred recheck closes that window; routing through
|
|
173
|
-
// the listener keeps a single finalization path.
|
|
174
|
-
const recheck = setTimeout(() => {
|
|
175
|
-
if (entries.get(identity) !== entry) {
|
|
176
|
-
return;
|
|
177
|
-
}
|
|
178
|
-
try {
|
|
179
|
-
entry.listener(fs.statSync(entry.spelling));
|
|
180
|
-
} catch {
|
|
181
|
-
// Still missing (or deleted again): the ordinary poll stays armed.
|
|
182
|
-
}
|
|
183
|
-
}, MISSING_INPUT_POLL_INTERVAL);
|
|
184
|
-
recheck.unref?.();
|
|
174
|
+
});
|
|
175
|
+
if (poller === undefined) {
|
|
176
|
+
poller = setInterval(poll, MISSING_INPUT_POLL_INTERVAL);
|
|
177
|
+
// A poller must never keep the dev-server process alive on its own.
|
|
178
|
+
poller.unref?.();
|
|
179
|
+
}
|
|
185
180
|
},
|
|
186
181
|
};
|
|
187
182
|
}
|
|
188
183
|
|
|
184
|
+
/** Key a private poll by predicate and exact lexical spelling. */
|
|
185
|
+
export function viteServeMissingInputWatchKey(
|
|
186
|
+
input: string,
|
|
187
|
+
condition: ViteServeInputWatchCondition,
|
|
188
|
+
): string {
|
|
189
|
+
// Missing aliases can share a physical parent now and later retarget or
|
|
190
|
+
// diverge. Neither predicate may let one lexical spelling answer for another.
|
|
191
|
+
const predicate =
|
|
192
|
+
typeof condition === "string"
|
|
193
|
+
? condition
|
|
194
|
+
: `predicates:${JSON.stringify([
|
|
195
|
+
condition.accessibleEntries === undefined
|
|
196
|
+
? null
|
|
197
|
+
: [
|
|
198
|
+
condition.accessibleEntries.directories,
|
|
199
|
+
condition.accessibleEntries.files,
|
|
200
|
+
],
|
|
201
|
+
condition.directoryExists ?? null,
|
|
202
|
+
condition.fileExists ?? null,
|
|
203
|
+
condition.readFile === undefined
|
|
204
|
+
? null
|
|
205
|
+
: condition.readFile.ok
|
|
206
|
+
? [true, condition.readFile.hash]
|
|
207
|
+
: [false],
|
|
208
|
+
condition.realpath === undefined
|
|
209
|
+
? null
|
|
210
|
+
: condition.realpath.ok
|
|
211
|
+
? [true, condition.realpath.path]
|
|
212
|
+
: [false],
|
|
213
|
+
condition.stat ?? null,
|
|
214
|
+
])}`;
|
|
215
|
+
return `${predicate}:${path.resolve(input)}`;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/** Whether one registered condition no longer describes its lexical path. */
|
|
219
|
+
function viteServeInputWatchConditionChanged(
|
|
220
|
+
entry: IMissingInputEntry,
|
|
221
|
+
): boolean {
|
|
222
|
+
if (typeof entry.condition !== "string") {
|
|
223
|
+
return (
|
|
224
|
+
validateGraphInputObservation(entry.spelling, entry.condition).length !==
|
|
225
|
+
0
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
try {
|
|
229
|
+
if (!fs.existsSync(entry.spelling)) {
|
|
230
|
+
return false;
|
|
231
|
+
}
|
|
232
|
+
return (
|
|
233
|
+
entry.condition === "exists" || !fs.statSync(entry.spelling).isDirectory()
|
|
234
|
+
);
|
|
235
|
+
} catch {
|
|
236
|
+
return false;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
189
240
|
/**
|
|
190
241
|
* Invalidate every module-graph node of the registered importers so the next
|
|
191
242
|
* request retransforms them. Importers keep their original absolute spelling so
|
package/src/next.ts
CHANGED
|
@@ -1,4 +1,12 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
|
|
1
5
|
import type { TtscUnpluginOptions } from "./core/options";
|
|
6
|
+
import {
|
|
7
|
+
TYPESCRIPT_TRANSFORM_EXTENSIONS,
|
|
8
|
+
TYPESCRIPT_TURBOPACK_RULE_GLOBS,
|
|
9
|
+
} from "./core/sourceExtensions";
|
|
2
10
|
import webpack from "./webpack";
|
|
3
11
|
|
|
4
12
|
/** The standalone loader entry Turbopack accepts through `turbopack.rules`. */
|
|
@@ -6,11 +14,11 @@ const TURBOPACK_LOADER = "@ttsc/unplugin/turbopack";
|
|
|
6
14
|
/**
|
|
7
15
|
* The globs the loader is wired for.
|
|
8
16
|
*
|
|
9
|
-
* The same
|
|
10
|
-
* harmless, since `isTransformTarget` declines everything else that reaches
|
|
11
|
-
* loader, but it would also route files through a worker for nothing.
|
|
17
|
+
* The same exact set the manual wiring in the README uses. A wider glob would
|
|
18
|
+
* be harmless, since `isTransformTarget` declines everything else that reaches
|
|
19
|
+
* the loader, but it would also route files through a worker for nothing.
|
|
12
20
|
*/
|
|
13
|
-
const TURBOPACK_RULE_GLOBS =
|
|
21
|
+
const TURBOPACK_RULE_GLOBS = TYPESCRIPT_TURBOPACK_RULE_GLOBS;
|
|
14
22
|
|
|
15
23
|
/**
|
|
16
24
|
* Minimal structural type for a Next.js configuration object.
|
|
@@ -147,35 +155,33 @@ function withTtscTurbopackRules(
|
|
|
147
155
|
options?: TtscUnpluginOptions,
|
|
148
156
|
): TurbopackLikeConfig {
|
|
149
157
|
const rules: Record<string, unknown> = { ...(existing?.rules ?? {}) };
|
|
158
|
+
// Package ownership is stable only for this configuration snapshot. A later
|
|
159
|
+
// call may observe an atomic install or a directory-link retarget at the same
|
|
160
|
+
// lexical loader path, so no verdict can escape this invocation.
|
|
161
|
+
const resolvedLoaderResults = new Map<string, boolean>();
|
|
150
162
|
for (const glob of TURBOPACK_RULE_GLOBS) {
|
|
151
163
|
const rule = rules[glob];
|
|
152
|
-
|
|
153
|
-
if (loaders.some(referencesTtscLoader)) {
|
|
164
|
+
if (hasUnconditionalTtscLoader(rule, resolvedLoaderResults)) {
|
|
154
165
|
continue;
|
|
155
166
|
}
|
|
156
167
|
// The caller may have written the same file set under a different glob.
|
|
157
168
|
// Adding ours beside theirs makes every matching module run the loader
|
|
158
169
|
// twice, and the second pass receives the first pass's output, so the
|
|
159
170
|
// guard has to cover the spellings a caller plausibly uses rather than
|
|
160
|
-
// only the
|
|
161
|
-
if (coveredByAnotherRule(rules, glob)) {
|
|
171
|
+
// only the four this wrapper writes (samchon/ttsc#1314).
|
|
172
|
+
if (coveredByAnotherRule(rules, glob, resolvedLoaderResults)) {
|
|
162
173
|
continue;
|
|
163
174
|
}
|
|
164
175
|
const entry = { loader: TURBOPACK_LOADER, options: options ?? {} };
|
|
165
|
-
//
|
|
166
|
-
//
|
|
167
|
-
//
|
|
168
|
-
//
|
|
176
|
+
// Loader shorthand runs right to left, so ttsc is appended there to see
|
|
177
|
+
// the original source. Rule collections run matching items in order, so
|
|
178
|
+
// ttsc becomes an explicit first rule there for the same reason. That is
|
|
179
|
+
// the same position `enforce: "pre"` gives it on the webpack half.
|
|
169
180
|
//
|
|
170
181
|
// Measured rather than inferred from webpack's `loader-runner`: two
|
|
171
182
|
// loaders on one rule, each marking the source, came back marked in the
|
|
172
183
|
// order that only the last-runs-first chain produces (samchon/ttsc#1319).
|
|
173
|
-
rules[glob] =
|
|
174
|
-
rule === undefined || loaders.length === 0
|
|
175
|
-
? { loaders: [entry] }
|
|
176
|
-
: Array.isArray(rule)
|
|
177
|
-
? { loaders: [...loaders, entry] }
|
|
178
|
-
: { ...(rule as object), loaders: [...loaders, entry] };
|
|
184
|
+
rules[glob] = addUnconditionalTtscLoader(rule, entry);
|
|
179
185
|
}
|
|
180
186
|
return { ...(existing ?? {}), rules };
|
|
181
187
|
}
|
|
@@ -196,13 +202,14 @@ function withTtscTurbopackRules(
|
|
|
196
202
|
function coveredByAnotherRule(
|
|
197
203
|
rules: Record<string, unknown>,
|
|
198
204
|
glob: string,
|
|
205
|
+
resolvedLoaderResults: Map<string, boolean>,
|
|
199
206
|
): boolean {
|
|
200
207
|
const extension = glob.slice(glob.lastIndexOf(".") + 1);
|
|
201
208
|
return Object.entries(rules).some(([candidate, rule]) => {
|
|
202
209
|
if (candidate === glob) {
|
|
203
210
|
return false;
|
|
204
211
|
}
|
|
205
|
-
if (!
|
|
212
|
+
if (!hasUnconditionalTtscLoader(rule, resolvedLoaderResults)) {
|
|
206
213
|
return false;
|
|
207
214
|
}
|
|
208
215
|
return matchesExtension(candidate, extension);
|
|
@@ -231,7 +238,7 @@ function coveredByAnotherRule(
|
|
|
231
238
|
* spellings, and that document explains why it is a set rather than a rule.
|
|
232
239
|
*/
|
|
233
240
|
function matchesExtension(glob: string, extension: string): boolean {
|
|
234
|
-
return PROJECT_WIDE_GLOBS.get(glob
|
|
241
|
+
return PROJECT_WIDE_GLOBS.get(glob)?.includes(extension) === true;
|
|
235
242
|
}
|
|
236
243
|
|
|
237
244
|
/**
|
|
@@ -256,54 +263,223 @@ function matchesExtension(glob: string, extension: string): boolean {
|
|
|
256
263
|
* if any — is a second registration rather than a module that no loader ever
|
|
257
264
|
* sees.
|
|
258
265
|
*
|
|
259
|
-
* `experimental/test-unplugin`
|
|
260
|
-
*
|
|
261
|
-
* `.tsx
|
|
262
|
-
*
|
|
263
|
-
*
|
|
264
|
-
|
|
266
|
+
* `experimental/test-unplugin` reads this exported table from the installed
|
|
267
|
+
* package, drives every entry through one `next build --turbopack`, and asserts
|
|
268
|
+
* root, nested, and deep `.ts`, `.tsx`, `.mts`, and `.cts` sources match
|
|
269
|
+
* exactly the extension family recorded here. An entry therefore cannot be
|
|
270
|
+
* recognised without that same build measuring it.
|
|
271
|
+
*/
|
|
272
|
+
const TYPESCRIPT_EXTENSION_NAMES = TYPESCRIPT_TRANSFORM_EXTENSIONS.map(
|
|
273
|
+
(extension) => extension.slice(1),
|
|
274
|
+
);
|
|
275
|
+
const TYPESCRIPT_EXTENSION_GROUPS = extensionCombinations(
|
|
276
|
+
TYPESCRIPT_EXTENSION_NAMES,
|
|
277
|
+
);
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Measured project-wide Turbopack globs and the source extensions each covers.
|
|
281
|
+
*
|
|
282
|
+
* Exported so the packed-package E2E can measure the exact production allowlist
|
|
283
|
+
* instead of maintaining a second list that can drift from it.
|
|
265
284
|
*/
|
|
266
|
-
const
|
|
267
|
-
[
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
285
|
+
export const TURBOPACK_PROJECT_WIDE_GLOB_COVERAGE: ReadonlyArray<
|
|
286
|
+
readonly [string, readonly string[]]
|
|
287
|
+
> = Object.freeze([
|
|
288
|
+
...TYPESCRIPT_EXTENSION_NAMES.flatMap((extension) => {
|
|
289
|
+
const extensions = Object.freeze([extension]);
|
|
290
|
+
return [
|
|
291
|
+
Object.freeze([`*.${extension}`, extensions] as const),
|
|
292
|
+
Object.freeze([`**/*.${extension}`, extensions] as const),
|
|
293
|
+
Object.freeze([`{**/,}*.${extension}`, extensions] as const),
|
|
294
|
+
];
|
|
295
|
+
}),
|
|
296
|
+
...TYPESCRIPT_EXTENSION_GROUPS.flatMap(projectWideBraceGlobEntries),
|
|
277
297
|
]);
|
|
298
|
+
const PROJECT_WIDE_GLOBS: ReadonlyMap<string, readonly string[]> = new Map(
|
|
299
|
+
TURBOPACK_PROJECT_WIDE_GLOB_COVERAGE,
|
|
300
|
+
);
|
|
301
|
+
|
|
302
|
+
/** Every source-extension subset of size two or more, in shared-table order. */
|
|
303
|
+
function extensionCombinations(
|
|
304
|
+
extensions: readonly string[],
|
|
305
|
+
): ReadonlyArray<readonly string[]> {
|
|
306
|
+
const output: Array<readonly string[]> = [];
|
|
307
|
+
const visit = (start: number, selected: string[]): void => {
|
|
308
|
+
for (let index = start; index < extensions.length; index += 1) {
|
|
309
|
+
const next = [...selected, extensions[index]!];
|
|
310
|
+
if (next.length >= 2) output.push(Object.freeze(next));
|
|
311
|
+
visit(index + 1, next);
|
|
312
|
+
}
|
|
313
|
+
};
|
|
314
|
+
visit(0, []);
|
|
315
|
+
return output;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/** Measured project-wide brace spellings for one extension family. */
|
|
319
|
+
function projectWideBraceGlobEntries(
|
|
320
|
+
extensions: readonly string[],
|
|
321
|
+
): ReadonlyArray<readonly [string, readonly string[]]> {
|
|
322
|
+
const suffixes = extensions.join(",");
|
|
323
|
+
const alternatives = extensions
|
|
324
|
+
.map((extension) => `*.${extension}`)
|
|
325
|
+
.join(",");
|
|
326
|
+
return [
|
|
327
|
+
Object.freeze([`*.{${suffixes}}`, extensions] as const),
|
|
328
|
+
Object.freeze([`{${alternatives}}`, extensions] as const),
|
|
329
|
+
Object.freeze([`**/*.{${suffixes}}`, extensions] as const),
|
|
330
|
+
Object.freeze([`**/{${alternatives}}`, extensions] as const),
|
|
331
|
+
Object.freeze([`**/**/*.{${suffixes}}`, extensions] as const),
|
|
332
|
+
];
|
|
333
|
+
}
|
|
278
334
|
|
|
279
335
|
/**
|
|
280
|
-
*
|
|
336
|
+
* Add one unconditional loader without flattening Next's rule collection.
|
|
281
337
|
*
|
|
282
|
-
*
|
|
283
|
-
*
|
|
284
|
-
*
|
|
338
|
+
* An array is either loader shorthand or a collection whose entries may be
|
|
339
|
+
* complete conditional rule objects, so it must remain an array. A collection
|
|
340
|
+
* executes every matching rule in order; its unconditional ttsc rule therefore
|
|
341
|
+
* comes first so it sees the original source before any caller rule. A
|
|
342
|
+
* conditioned object becomes that same two-item collection because putting ttsc
|
|
343
|
+
* inside the condition would leave the rest of the glob uncovered. An
|
|
344
|
+
* unconditioned object can keep its own shape while gaining the loader,
|
|
345
|
+
* including when it had no loaders.
|
|
285
346
|
*/
|
|
286
|
-
function
|
|
347
|
+
function addUnconditionalTtscLoader(
|
|
348
|
+
rule: unknown,
|
|
349
|
+
entry: { loader: string; options: TtscUnpluginOptions },
|
|
350
|
+
): unknown {
|
|
287
351
|
if (Array.isArray(rule)) {
|
|
288
|
-
return rule
|
|
352
|
+
return rule.every(isTurbopackLoaderItem)
|
|
353
|
+
? [...rule, entry]
|
|
354
|
+
: [{ loaders: [entry] }, ...rule];
|
|
289
355
|
}
|
|
290
356
|
if (typeof rule === "object" && rule !== null) {
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
return loaders;
|
|
357
|
+
if ((rule as { condition?: unknown }).condition !== undefined) {
|
|
358
|
+
return [{ loaders: [entry] }, rule];
|
|
294
359
|
}
|
|
360
|
+
return {
|
|
361
|
+
...rule,
|
|
362
|
+
loaders: [...selectTurbopackConfigLoaders(rule), entry],
|
|
363
|
+
};
|
|
295
364
|
}
|
|
296
|
-
return [];
|
|
365
|
+
return { loaders: [entry] };
|
|
297
366
|
}
|
|
298
367
|
|
|
299
|
-
/**
|
|
300
|
-
function
|
|
301
|
-
if (typeof
|
|
302
|
-
return
|
|
368
|
+
/** Read the direct loader list from one Turbopack rule configuration item. */
|
|
369
|
+
function selectTurbopackConfigLoaders(rule: unknown): unknown[] {
|
|
370
|
+
if (typeof rule !== "object" || rule === null || Array.isArray(rule)) {
|
|
371
|
+
return [];
|
|
372
|
+
}
|
|
373
|
+
const loaders = (rule as { loaders?: unknown }).loaders;
|
|
374
|
+
return Array.isArray(loaders) ? loaders : [];
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/** Whether a rule collection provides this loader without a condition. */
|
|
378
|
+
function hasUnconditionalTtscLoader(
|
|
379
|
+
rule: unknown,
|
|
380
|
+
resolvedLoaderResults: Map<string, boolean>,
|
|
381
|
+
): boolean {
|
|
382
|
+
if (Array.isArray(rule)) {
|
|
383
|
+
return rule.some((item) => {
|
|
384
|
+
if (isTurbopackLoaderItem(item)) {
|
|
385
|
+
return referencesTtscLoader(item, resolvedLoaderResults);
|
|
386
|
+
}
|
|
387
|
+
return (
|
|
388
|
+
typeof item === "object" &&
|
|
389
|
+
item !== null &&
|
|
390
|
+
(item as { condition?: unknown }).condition === undefined &&
|
|
391
|
+
selectTurbopackConfigLoaders(item).some((loader) =>
|
|
392
|
+
referencesTtscLoader(loader, resolvedLoaderResults),
|
|
393
|
+
)
|
|
394
|
+
);
|
|
395
|
+
});
|
|
303
396
|
}
|
|
304
397
|
return (
|
|
305
|
-
typeof
|
|
306
|
-
|
|
307
|
-
(
|
|
398
|
+
typeof rule === "object" &&
|
|
399
|
+
rule !== null &&
|
|
400
|
+
(rule as { condition?: unknown }).condition === undefined &&
|
|
401
|
+
selectTurbopackConfigLoaders(rule).some((loader) =>
|
|
402
|
+
referencesTtscLoader(loader, resolvedLoaderResults),
|
|
403
|
+
)
|
|
308
404
|
);
|
|
309
405
|
}
|
|
406
|
+
|
|
407
|
+
/** Whether one collection item is a direct Turbopack loader. */
|
|
408
|
+
function isTurbopackLoaderItem(item: unknown): boolean {
|
|
409
|
+
return (
|
|
410
|
+
typeof item === "string" ||
|
|
411
|
+
(typeof item === "object" &&
|
|
412
|
+
item !== null &&
|
|
413
|
+
typeof (item as { loader?: unknown }).loader === "string")
|
|
414
|
+
);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/** Whether one Turbopack loader entry is already this package's loader. */
|
|
418
|
+
function referencesTtscLoader(
|
|
419
|
+
loader: unknown,
|
|
420
|
+
resolvedLoaderResults: Map<string, boolean>,
|
|
421
|
+
): boolean {
|
|
422
|
+
const identity =
|
|
423
|
+
typeof loader === "string"
|
|
424
|
+
? loader
|
|
425
|
+
: typeof loader === "object" && loader !== null
|
|
426
|
+
? (loader as { loader?: unknown }).loader
|
|
427
|
+
: undefined;
|
|
428
|
+
return (
|
|
429
|
+
typeof identity === "string" &&
|
|
430
|
+
(identity === TURBOPACK_LOADER ||
|
|
431
|
+
isResolvedTtscLoader(identity, resolvedLoaderResults))
|
|
432
|
+
);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/** Whether an absolute path is the Turbopack entry of an @ttsc/unplugin copy. */
|
|
436
|
+
function isResolvedTtscLoader(
|
|
437
|
+
identity: string,
|
|
438
|
+
resolvedLoaderResults: Map<string, boolean>,
|
|
439
|
+
): boolean {
|
|
440
|
+
const cached = resolvedLoaderResults.get(identity);
|
|
441
|
+
if (cached !== undefined) {
|
|
442
|
+
return cached;
|
|
443
|
+
}
|
|
444
|
+
let file: string;
|
|
445
|
+
try {
|
|
446
|
+
file = /^file:/i.test(identity) ? fileURLToPath(identity) : identity;
|
|
447
|
+
} catch {
|
|
448
|
+
resolvedLoaderResults.set(identity, false);
|
|
449
|
+
return false;
|
|
450
|
+
}
|
|
451
|
+
if (!path.isAbsolute(file)) {
|
|
452
|
+
resolvedLoaderResults.set(identity, false);
|
|
453
|
+
return false;
|
|
454
|
+
}
|
|
455
|
+
let resolvedFile: string;
|
|
456
|
+
try {
|
|
457
|
+
resolvedFile = fs.realpathSync.native(file);
|
|
458
|
+
if (!fs.statSync(resolvedFile).isFile()) {
|
|
459
|
+
resolvedLoaderResults.set(identity, false);
|
|
460
|
+
return false;
|
|
461
|
+
}
|
|
462
|
+
} catch {
|
|
463
|
+
resolvedLoaderResults.set(identity, false);
|
|
464
|
+
return false;
|
|
465
|
+
}
|
|
466
|
+
const packageRoot = path.dirname(path.dirname(resolvedFile));
|
|
467
|
+
const relative = path
|
|
468
|
+
.relative(packageRoot, resolvedFile)
|
|
469
|
+
.replaceAll(path.sep, "/");
|
|
470
|
+
if (relative !== "lib/turbopack.js" && relative !== "lib/turbopack.mjs") {
|
|
471
|
+
resolvedLoaderResults.set(identity, false);
|
|
472
|
+
return false;
|
|
473
|
+
}
|
|
474
|
+
let matches = false;
|
|
475
|
+
try {
|
|
476
|
+
const manifest = JSON.parse(
|
|
477
|
+
fs.readFileSync(path.join(packageRoot, "package.json"), "utf8"),
|
|
478
|
+
) as { name?: unknown };
|
|
479
|
+
matches = manifest.name === "@ttsc/unplugin";
|
|
480
|
+
} catch {
|
|
481
|
+
// An unreadable or malformed owner cannot prove this package's identity.
|
|
482
|
+
}
|
|
483
|
+
resolvedLoaderResults.set(identity, matches);
|
|
484
|
+
return matches;
|
|
485
|
+
}
|