@ttsc/unplugin 0.18.3 → 0.19.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 +4 -2
- package/lib/bun-register.d.ts +9 -3
- package/lib/bun-register.js +32 -4
- package/lib/bun-register.js.map +1 -1
- package/lib/bun-register.mjs +32 -4
- package/lib/bun-register.mjs.map +1 -1
- package/lib/bun.d.ts +13 -1
- package/lib/bun.js +28 -2
- package/lib/bun.js.map +1 -1
- package/lib/bun.mjs +28 -2
- package/lib/bun.mjs.map +1 -1
- package/lib/core/transform.js +73 -5
- package/lib/core/transform.js.map +1 -1
- package/lib/core/transform.mjs +73 -5
- package/lib/core/transform.mjs.map +1 -1
- package/lib/core/tsconfigPaths.js +52 -0
- package/lib/core/tsconfigPaths.js.map +1 -1
- package/lib/core/tsconfigPaths.mjs +52 -0
- package/lib/core/tsconfigPaths.mjs.map +1 -1
- package/lib/turbopack.d.ts +8 -0
- package/lib/turbopack.js +7 -1
- package/lib/turbopack.js.map +1 -1
- package/lib/turbopack.mjs +7 -1
- package/lib/turbopack.mjs.map +1 -1
- package/package.json +5 -4
- package/src/bun-register.ts +33 -4
- package/src/bun.ts +55 -3
- package/src/core/transform.ts +91 -5
- package/src/core/tsconfigPaths.ts +61 -0
- package/src/turbopack.ts +15 -0
package/src/core/transform.ts
CHANGED
|
@@ -147,10 +147,14 @@ export async function transformTtsc(
|
|
|
147
147
|
|
|
148
148
|
let transformed = cache?.get(key);
|
|
149
149
|
if (transformed !== undefined) {
|
|
150
|
-
|
|
150
|
+
// A rejected in-flight generation must not stay cached: evict it (only if
|
|
151
|
+
// it is still the current entry) so a later call re-runs the transform.
|
|
152
|
+
const cached = await awaitOrEvict(cache, key, transformed);
|
|
151
153
|
if (matchesCachedSource(cached, file, source)) {
|
|
152
154
|
reportSuccessDiagnostics(cached.result);
|
|
153
|
-
|
|
155
|
+
// A resolved `"exception"` / `"failure"` envelope makes this throw; that
|
|
156
|
+
// is a failed generation too, so evict before surfacing it.
|
|
157
|
+
const code = selectOrEvict(cache, key, transformed, {
|
|
154
158
|
file,
|
|
155
159
|
projectRoot: cached.projectRoot,
|
|
156
160
|
result: cached.result,
|
|
@@ -177,13 +181,80 @@ export async function transformTtsc(
|
|
|
177
181
|
});
|
|
178
182
|
cache?.set(key, transformed);
|
|
179
183
|
}
|
|
180
|
-
const
|
|
184
|
+
const generation = transformed;
|
|
185
|
+
const { projectRoot, result } = await awaitOrEvict(cache, key, generation);
|
|
181
186
|
reportSuccessDiagnostics(result);
|
|
182
|
-
const code =
|
|
187
|
+
const code = selectOrEvict(cache, key, generation, {
|
|
188
|
+
file,
|
|
189
|
+
projectRoot,
|
|
190
|
+
result,
|
|
191
|
+
});
|
|
183
192
|
notifyFileDependencies(hooks, { file, projectRoot, result });
|
|
184
193
|
return createTransformResult(source, code);
|
|
185
194
|
}
|
|
186
195
|
|
|
196
|
+
/**
|
|
197
|
+
* Await a cached generation, evicting it on rejection.
|
|
198
|
+
*
|
|
199
|
+
* The cache stores the in-flight transform Promise before it settles so
|
|
200
|
+
* concurrent callers share one compilation. A rejected generation must not
|
|
201
|
+
* remain the authoritative cached result, or a transient toolchain/host failure
|
|
202
|
+
* becomes permanent for a long-lived worker. Eviction is identity-guarded so a
|
|
203
|
+
* newer generation another caller installed under the same key survives.
|
|
204
|
+
*/
|
|
205
|
+
async function awaitOrEvict(
|
|
206
|
+
cache: TtscTransformCache | undefined,
|
|
207
|
+
key: string,
|
|
208
|
+
generation: Promise<TtscCachedProjectTransform>,
|
|
209
|
+
): Promise<TtscCachedProjectTransform> {
|
|
210
|
+
try {
|
|
211
|
+
return await generation;
|
|
212
|
+
} catch (error) {
|
|
213
|
+
evictGeneration(cache, key, generation);
|
|
214
|
+
throw error;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Extract the transformed source, evicting the generation when the result is a
|
|
220
|
+
* host `"exception"` or compiler `"failure"` (which makes
|
|
221
|
+
* {@link selectTransformedSource} throw). Such a failed generation must not be
|
|
222
|
+
* replayed to later callers of an unchanged module.
|
|
223
|
+
*/
|
|
224
|
+
function selectOrEvict(
|
|
225
|
+
cache: TtscTransformCache | undefined,
|
|
226
|
+
key: string,
|
|
227
|
+
generation: Promise<TtscCachedProjectTransform>,
|
|
228
|
+
props: {
|
|
229
|
+
file: string;
|
|
230
|
+
projectRoot: string;
|
|
231
|
+
result: ITtscCompilerTransformation;
|
|
232
|
+
},
|
|
233
|
+
): string {
|
|
234
|
+
try {
|
|
235
|
+
return selectTransformedSource(props);
|
|
236
|
+
} catch (error) {
|
|
237
|
+
evictGeneration(cache, key, generation);
|
|
238
|
+
throw error;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Delete a failed generation from the cache only when it is still the entry
|
|
244
|
+
* stored under `key`. The identity check prevents an older failed generation's
|
|
245
|
+
* cleanup from removing a newer replacement created by another caller for the
|
|
246
|
+
* same key.
|
|
247
|
+
*/
|
|
248
|
+
function evictGeneration(
|
|
249
|
+
cache: TtscTransformCache | undefined,
|
|
250
|
+
key: string,
|
|
251
|
+
generation: Promise<TtscCachedProjectTransform>,
|
|
252
|
+
): void {
|
|
253
|
+
if (cache?.get(key) === generation) {
|
|
254
|
+
cache.delete(key);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
187
258
|
/**
|
|
188
259
|
* Forward the plugin-reported dependency list for `file` to the adapter's
|
|
189
260
|
* `addWatchFile` hook.
|
|
@@ -462,6 +533,13 @@ async function transformProject(props: {
|
|
|
462
533
|
try {
|
|
463
534
|
const result = new TtscCompiler({
|
|
464
535
|
cwd: projectRoot,
|
|
536
|
+
// The generated tsconfig (if any) lives in the system temp directory,
|
|
537
|
+
// so declare the real project as the plugin config anchor: utility
|
|
538
|
+
// plugin config discovery (banner.config.*, strip.config.*,
|
|
539
|
+
// lint.config.*) and relative configFile resolution walk the project,
|
|
540
|
+
// never the temp tree. In the passthrough case this equals the
|
|
541
|
+
// tsconfig's own directory, the default anchor.
|
|
542
|
+
pluginConfigDir: projectRoot,
|
|
465
543
|
plugins: props.plugins,
|
|
466
544
|
projectRoot,
|
|
467
545
|
tsconfig: configured.path,
|
|
@@ -568,6 +646,14 @@ function normalizeCompilerOptionsForGeneratedTsconfig(
|
|
|
568
646
|
return output;
|
|
569
647
|
}
|
|
570
648
|
|
|
649
|
+
/**
|
|
650
|
+
* Absolutize the relative path-typed keys of one plugin entry before it is
|
|
651
|
+
* written into the generated temp-dir tsconfig: `config`/`source`/`transform`
|
|
652
|
+
* are the descriptor-resolution keys, and `configFile` is the config-file
|
|
653
|
+
* override accepted by the shipped utility plugins (`@ttsc/banner`,
|
|
654
|
+
* `@ttsc/strip`, `@ttsc/lint`). Left relative, each would resolve against the
|
|
655
|
+
* temp directory instead of the project.
|
|
656
|
+
*/
|
|
571
657
|
function normalizePluginConfigForGeneratedTsconfig(
|
|
572
658
|
entry: unknown,
|
|
573
659
|
tsconfigDir: string,
|
|
@@ -576,7 +662,7 @@ function normalizePluginConfigForGeneratedTsconfig(
|
|
|
576
662
|
return entry;
|
|
577
663
|
}
|
|
578
664
|
const output: Record<string, unknown> = { ...entry };
|
|
579
|
-
for (const key of ["config", "source", "transform"]) {
|
|
665
|
+
for (const key of ["config", "configFile", "source", "transform"]) {
|
|
580
666
|
const value = output[key];
|
|
581
667
|
if (typeof value === "string" && isRelativeSpecifier(value)) {
|
|
582
668
|
output[key] = path.resolve(tsconfigDir, value);
|
|
@@ -148,6 +148,15 @@ function resolveExtendsConfig(
|
|
|
148
148
|
);
|
|
149
149
|
}
|
|
150
150
|
const resolver = createRequire(tsconfig);
|
|
151
|
+
// A bare package root selects its preset through `package.json#tsconfig`,
|
|
152
|
+
// matching TypeScript's config resolution and the core project reader. Such
|
|
153
|
+
// presets often ship no JS/JSON entrypoint, so Node's entrypoint resolver and
|
|
154
|
+
// the `<specifier>.json` fallback both miss them, silently dropping the
|
|
155
|
+
// preset's inherited `paths`.
|
|
156
|
+
const viaManifest = resolvePackageManifestTsconfig(resolver, specifier);
|
|
157
|
+
if (viaManifest !== null) {
|
|
158
|
+
return viaManifest;
|
|
159
|
+
}
|
|
151
160
|
try {
|
|
152
161
|
return resolveRealPath(resolver.resolve(specifier));
|
|
153
162
|
} catch {
|
|
@@ -159,6 +168,58 @@ function resolveExtendsConfig(
|
|
|
159
168
|
}
|
|
160
169
|
}
|
|
161
170
|
|
|
171
|
+
/**
|
|
172
|
+
* When `specifier` names a bare package root, resolve the config file its
|
|
173
|
+
* `package.json#tsconfig` field selects (anchored at the package directory).
|
|
174
|
+
* Best-effort: returns `null` for a subpath, an unresolvable/unparsable
|
|
175
|
+
* manifest, a missing `tsconfig` field, or a field target that does not exist —
|
|
176
|
+
* the compiler owns the real diagnostic, and this reader must not invent
|
|
177
|
+
* aliases.
|
|
178
|
+
*/
|
|
179
|
+
function resolvePackageManifestTsconfig(
|
|
180
|
+
resolver: NodeRequire,
|
|
181
|
+
specifier: string,
|
|
182
|
+
): string | null {
|
|
183
|
+
if (!isBarePackageRoot(specifier)) {
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
let manifestPath: string;
|
|
187
|
+
try {
|
|
188
|
+
manifestPath = resolver.resolve(`${specifier}/package.json`);
|
|
189
|
+
} catch {
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
let field: unknown;
|
|
193
|
+
try {
|
|
194
|
+
const text = fs.readFileSync(manifestPath, "utf8");
|
|
195
|
+
field = (
|
|
196
|
+
JSON.parse(text.charCodeAt(0) === 0xfeff ? text.slice(1) : text) as {
|
|
197
|
+
tsconfig?: unknown;
|
|
198
|
+
}
|
|
199
|
+
).tsconfig;
|
|
200
|
+
} catch {
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
if (typeof field !== "string" || field.length === 0) {
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
206
|
+
return resolveExistingExtendsPath(
|
|
207
|
+
path.resolve(path.dirname(manifestPath), field),
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Return true when `specifier` is a bare package root (no subpath): a plain
|
|
213
|
+
* package name (`preset`) or a scoped name (`@scope/preset`). Subpaths such as
|
|
214
|
+
* `@scope/preset/base.json` resolve directly and keep their current meaning.
|
|
215
|
+
*/
|
|
216
|
+
function isBarePackageRoot(specifier: string): boolean {
|
|
217
|
+
if (specifier.startsWith("@")) {
|
|
218
|
+
return specifier.split("/").length === 2;
|
|
219
|
+
}
|
|
220
|
+
return !specifier.includes("/");
|
|
221
|
+
}
|
|
222
|
+
|
|
162
223
|
/**
|
|
163
224
|
* Try an on-disk `extends` location as-is, with `.json` appended, and as a
|
|
164
225
|
* directory containing `tsconfig.json`. Returns the first existing match.
|
package/src/turbopack.ts
CHANGED
|
@@ -21,6 +21,14 @@ export interface TtscTurbopackLoaderContext {
|
|
|
21
21
|
resourcePath: string;
|
|
22
22
|
/** The rule's `options` object, when one was configured. */
|
|
23
23
|
getOptions?(): TtscUnpluginOptions | undefined;
|
|
24
|
+
/**
|
|
25
|
+
* Register an additional file the transformed module depends on. Part of the
|
|
26
|
+
* webpack loader context contract Turbopack implements; a registered file
|
|
27
|
+
* enters Turbopack's `fileDependencies` set so editing it re-runs this loader
|
|
28
|
+
* for the owning module. Optional so a minimal stub context (or a Turbopack
|
|
29
|
+
* build that predates the method) still loads.
|
|
30
|
+
*/
|
|
31
|
+
addDependency?(file: string): void;
|
|
24
32
|
}
|
|
25
33
|
|
|
26
34
|
/** Matches any path segment that is a `node_modules` directory (cross-platform). */
|
|
@@ -71,12 +79,19 @@ export default function turbopack(
|
|
|
71
79
|
callback(undefined, source);
|
|
72
80
|
return;
|
|
73
81
|
}
|
|
82
|
+
// Forward plugin-reported dependencies into Turbopack's `fileDependencies`
|
|
83
|
+
// set so editing a type-only input a transform consulted re-runs this loader.
|
|
84
|
+
// `addDependency` is bound so the webpack loader context stays `this` inside
|
|
85
|
+
// it; the hook fires on cache hits too, which is required because the shared
|
|
86
|
+
// transform cache lives for the worker lifetime across requests.
|
|
87
|
+
const addDependency = this.addDependency?.bind(this);
|
|
74
88
|
transformTtsc(
|
|
75
89
|
file,
|
|
76
90
|
source,
|
|
77
91
|
resolveOptions(this.getOptions?.() ?? {}),
|
|
78
92
|
undefined,
|
|
79
93
|
transformCache,
|
|
94
|
+
addDependency === undefined ? undefined : { addWatchFile: addDependency },
|
|
80
95
|
).then(
|
|
81
96
|
(result) => callback(undefined, result?.code ?? source),
|
|
82
97
|
(error) => callback(error),
|