@ttsc/unplugin 0.20.0 → 0.21.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 +3 -1
- package/lib/api.js +3 -0
- package/lib/api.js.map +1 -1
- package/lib/api.mjs +2 -2
- package/lib/bun.d.ts +41 -15
- package/lib/bun.js +88 -32
- package/lib/bun.js.map +1 -1
- package/lib/bun.mjs +88 -32
- package/lib/bun.mjs.map +1 -1
- package/lib/core/index.d.ts +10 -2
- package/lib/core/index.js +51 -6
- package/lib/core/index.js.map +1 -1
- package/lib/core/index.mjs +50 -8
- package/lib/core/index.mjs.map +1 -1
- package/lib/core/transform.d.ts +40 -16
- package/lib/core/transform.js +154 -80
- package/lib/core/transform.js.map +1 -1
- package/lib/core/transform.mjs +153 -81
- package/lib/core/transform.mjs.map +1 -1
- package/lib/core/viteServe.d.ts +74 -0
- package/lib/core/viteServe.js +180 -0
- package/lib/core/viteServe.js.map +1 -0
- package/lib/core/viteServe.mjs +178 -0
- package/lib/core/viteServe.mjs.map +1 -0
- package/lib/turbopack.js +3 -3
- package/lib/turbopack.mjs +3 -3
- package/package.json +3 -3
- package/src/bun.ts +132 -52
- package/src/core/index.ts +54 -7
- package/src/core/transform.ts +181 -96
- package/src/core/viteServe.ts +271 -0
- package/src/turbopack.ts +3 -3
package/src/bun.ts
CHANGED
|
@@ -1,10 +1,22 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
|
-
import type { UnpluginContextMeta } from "unplugin";
|
|
3
2
|
|
|
4
|
-
import {
|
|
5
|
-
|
|
3
|
+
import {
|
|
4
|
+
beginTtscTransformBuild,
|
|
5
|
+
createTtscTransformCache,
|
|
6
|
+
isTransformTarget,
|
|
7
|
+
resolveOptions,
|
|
8
|
+
transformTtsc,
|
|
9
|
+
} from "./core/index";
|
|
6
10
|
import type { TtscUnpluginOptions } from "./core/options";
|
|
7
11
|
|
|
12
|
+
/**
|
|
13
|
+
* Bun normally reports filesystem source paths, while plugin-created virtual
|
|
14
|
+
* ids may contain a NUL sentinel. A virtual id must stay with the plugin that
|
|
15
|
+
* created it: claiming it here and trying to read it from disk prevents that
|
|
16
|
+
* plugin's later loader from running.
|
|
17
|
+
*/
|
|
18
|
+
const bunSourceFilePattern = /^[^\x00]*\.[cm]?tsx?$/;
|
|
19
|
+
|
|
8
20
|
/**
|
|
9
21
|
* Minimal subset of the Bun plugin API consumed by this adapter.
|
|
10
22
|
*
|
|
@@ -38,7 +50,7 @@ export type TtscBunOptions =
|
|
|
38
50
|
| (() => TtscUnpluginOptions | undefined);
|
|
39
51
|
|
|
40
52
|
/**
|
|
41
|
-
* Transform
|
|
53
|
+
* Transform hooks handed to the shared transform under Bun.
|
|
42
54
|
*
|
|
43
55
|
* The shared transform calls `addWatchFile` once per plugin-reported dependency
|
|
44
56
|
* so type-only inputs can enter a bundler's watch graph. Bun's bundler and
|
|
@@ -49,7 +61,7 @@ export type TtscBunOptions =
|
|
|
49
61
|
* `this.addWatchFile` `undefined`, so any plugin reporting dependencies threw
|
|
50
62
|
* `TypeError: this.addWatchFile is not a function`.
|
|
51
63
|
*/
|
|
52
|
-
const
|
|
64
|
+
const bunTransformHooks = {
|
|
53
65
|
addWatchFile(): void {},
|
|
54
66
|
};
|
|
55
67
|
|
|
@@ -63,91 +75,159 @@ function resolveBunOptions(
|
|
|
63
75
|
/**
|
|
64
76
|
* Minimal subset of the Bun `BuildConfig` plugin build object.
|
|
65
77
|
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
78
|
+
* `onLoad` drives the source transform. Bun's bundler also exposes `onStart`,
|
|
79
|
+
* which is used when available to forward the shared plugin's build lifecycle
|
|
80
|
+
* and clear its per-build cache. The runtime plugin API omits that hook, so
|
|
81
|
+
* plugin setup itself starts its one process-scoped module-loading session.
|
|
68
82
|
*/
|
|
69
83
|
export interface BunLikeBuild {
|
|
84
|
+
/**
|
|
85
|
+
* Build configuration exposed unchanged by Bun's bundler plugin builder.
|
|
86
|
+
*
|
|
87
|
+
* Runtime plugin builders do not supply `files`. Bun's bundler accepts an
|
|
88
|
+
* in-memory file map whose values deliberately remain `unknown` here because
|
|
89
|
+
* this adapter only needs to preserve ownership, not consume their contents.
|
|
90
|
+
*/
|
|
91
|
+
config?: {
|
|
92
|
+
files?: Readonly<Record<string, unknown>>;
|
|
93
|
+
};
|
|
94
|
+
/**
|
|
95
|
+
* Register a callback for the start of a bundler build.
|
|
96
|
+
*
|
|
97
|
+
* Optional because `Bun.plugin()` runtime builders do not expose this hook.
|
|
98
|
+
*/
|
|
99
|
+
onStart?(callback: () => void | Promise<void>): void;
|
|
70
100
|
/**
|
|
71
101
|
* Register a loader callback for files matching `filter`.
|
|
72
102
|
*
|
|
73
|
-
* The callback receives the
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
103
|
+
* The callback receives the file path and must return the transformed file
|
|
104
|
+
* contents plus the `loader` Bun should apply next. Configured in-memory
|
|
105
|
+
* files retain relative key spellings; ordinary disk files are normally
|
|
106
|
+
* absolute. The `loader` matters most for the runtime path (`Bun.plugin`),
|
|
107
|
+
* where Bun must be told the returned contents are still TypeScript so it
|
|
108
|
+
* keeps transpiling them before execution.
|
|
78
109
|
*/
|
|
79
110
|
onLoad(
|
|
80
111
|
options: { filter: RegExp },
|
|
81
112
|
loader: (args: {
|
|
82
113
|
path: string;
|
|
83
|
-
}) => Promise<{ contents: string; loader: BunLoader }>,
|
|
114
|
+
}) => Promise<{ contents: string; loader: BunLoader } | undefined>,
|
|
84
115
|
): void;
|
|
85
116
|
}
|
|
86
117
|
|
|
87
118
|
/**
|
|
88
119
|
* Create a ttsc plugin for Bun's bundler AND runtime.
|
|
89
120
|
*
|
|
90
|
-
* Bun does not implement the unplugin protocol, so this adapter
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
121
|
+
* Bun does not implement the unplugin protocol, so this adapter wires the
|
|
122
|
+
* shared ttsc transform core to Bun's `onLoad` hook directly. It reads each
|
|
123
|
+
* included file from disk and forwards the content to the transform. Under
|
|
124
|
+
* `Bun.build`, excluded files and no-op transforms return `undefined` so the
|
|
125
|
+
* next loader retains ownership. Entries supplied through `BuildConfig.files`
|
|
126
|
+
* also stay with Bun's in-memory loader: they are not filesystem project inputs
|
|
127
|
+
* and reading the same path from disk would either fail or silently replace the
|
|
128
|
+
* configured contents. The runtime `Bun.plugin()` API rejects an undefined
|
|
129
|
+
* `onLoad` result, so that path explicitly returns the original source and
|
|
130
|
+
* loader instead.
|
|
95
131
|
*
|
|
96
132
|
* The same object works for `Bun.build({ plugins: [ttsc()] })` (bundler) and
|
|
97
133
|
* for `Bun.plugin(ttsc())` / a `bunfig.toml` preload (runtime) — see
|
|
98
134
|
* `bun-register`. Every result carries an explicit `loader` so Bun keeps
|
|
99
|
-
* transpiling the emitted TypeScript at runtime; `
|
|
100
|
-
* matches TypeScript, so the loader is always `ts`/`tsx`.
|
|
135
|
+
* transpiling the emitted TypeScript at runtime; `bunSourceFilePattern` only
|
|
136
|
+
* matches TypeScript, so the loader is always `ts`/`tsx`. A runtime plugin
|
|
137
|
+
* instance is one immutable load session, like Bun's own module cache; restart
|
|
138
|
+
* the process after changing compiler inputs.
|
|
101
139
|
*/
|
|
102
140
|
export default function bun(options?: TtscBunOptions): BunLikePlugin {
|
|
103
141
|
return {
|
|
104
142
|
name: "ttsc-unplugin",
|
|
105
143
|
setup(build) {
|
|
106
|
-
//
|
|
107
|
-
//
|
|
108
|
-
//
|
|
109
|
-
//
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
144
|
+
// Resolve options lazily on first load. Runtime registration may call
|
|
145
|
+
// register(options) immediately after the import-time default
|
|
146
|
+
// registration; the provider form must observe that last synchronous
|
|
147
|
+
// update without installing a second shadowing loader.
|
|
148
|
+
let resolved: ReturnType<typeof resolveOptions> | undefined;
|
|
149
|
+
const getOptions = () =>
|
|
150
|
+
(resolved ??= resolveOptions(resolveBunOptions(options)));
|
|
151
|
+
const cache = createTtscTransformCache();
|
|
152
|
+
const runtime = build.onStart === undefined;
|
|
153
|
+
const ownsInMemoryFile = createBunInMemoryFileMatcher(build);
|
|
154
|
+
// Bun.plugin() has no onStart callback, but one setup invocation belongs
|
|
155
|
+
// to exactly one runtime process and module-loading session. Mark that
|
|
156
|
+
// session up front so first delivery of every emitted project module is
|
|
157
|
+
// constant-time instead of re-reading the whole project. Bun.build()
|
|
158
|
+
// immediately starts the same initial scope again through onStart and
|
|
159
|
+
// repeats it for subsequent builds.
|
|
160
|
+
beginTtscTransformBuild(cache);
|
|
161
|
+
build.onStart?.(() => beginTtscTransformBuild(cache));
|
|
162
|
+
build.onLoad({ filter: bunSourceFilePattern }, async (args) => {
|
|
163
|
+
if (!runtime && ownsInMemoryFile(args.path)) {
|
|
164
|
+
return undefined;
|
|
165
|
+
}
|
|
166
|
+
if (!isTransformTarget(args.path)) {
|
|
167
|
+
if (!runtime) return undefined;
|
|
168
|
+
return {
|
|
169
|
+
contents: await fs.readFile(args.path, "utf8"),
|
|
170
|
+
loader: bunLoaderFor(args.path),
|
|
171
|
+
};
|
|
172
|
+
}
|
|
118
173
|
const loader = bunLoaderFor(args.path);
|
|
119
174
|
const source = await fs.readFile(args.path, "utf8");
|
|
120
|
-
const result =
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
if (typeof result === "string") {
|
|
130
|
-
return { contents: result, loader };
|
|
131
|
-
}
|
|
132
|
-
if (
|
|
133
|
-
typeof result === "object" &&
|
|
134
|
-
result !== null &&
|
|
135
|
-
"code" in result &&
|
|
136
|
-
typeof result.code === "string"
|
|
137
|
-
) {
|
|
175
|
+
const result = await transformTtsc(
|
|
176
|
+
args.path,
|
|
177
|
+
source,
|
|
178
|
+
getOptions(),
|
|
179
|
+
undefined,
|
|
180
|
+
cache,
|
|
181
|
+
bunTransformHooks,
|
|
182
|
+
);
|
|
183
|
+
if (result !== undefined) {
|
|
138
184
|
return { contents: result.code, loader };
|
|
139
185
|
}
|
|
140
|
-
return { contents: source, loader };
|
|
186
|
+
return runtime ? { contents: source, loader } : undefined;
|
|
141
187
|
});
|
|
142
188
|
},
|
|
143
189
|
};
|
|
144
190
|
}
|
|
145
191
|
|
|
146
192
|
/**
|
|
147
|
-
* Pick the Bun loader for a matched file. `
|
|
193
|
+
* Pick the Bun loader for a matched file. `bunSourceFilePattern` is
|
|
148
194
|
* `/\.[cm]?tsx?$/`, so a trailing `x` (`.tsx`/`.ctsx`/`.mtsx`) is JSX-flavored
|
|
149
195
|
* TypeScript and everything else (`.ts`/`.cts`/`.mts`) is plain TypeScript.
|
|
150
196
|
*/
|
|
151
197
|
function bunLoaderFor(filePath: string): BunLoader {
|
|
152
198
|
return /x$/i.test(filePath) ? "tsx" : "ts";
|
|
153
199
|
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Create a stable ownership matcher for Bun's `BuildConfig.files` map.
|
|
203
|
+
*
|
|
204
|
+
* Bun preserves relative `files` keys in the corresponding `onLoad` path.
|
|
205
|
+
* Preserve relative versus absolute spelling and dot segments exactly. Windows
|
|
206
|
+
* normalizes separators and drive-letter case, but not component case. No path
|
|
207
|
+
* is resolved against cwd, so `process.chdir()` cannot change ownership.
|
|
208
|
+
*/
|
|
209
|
+
function createBunInMemoryFileMatcher(
|
|
210
|
+
build: BunLikeBuild,
|
|
211
|
+
): (file: string) => boolean {
|
|
212
|
+
const files = build.config?.files;
|
|
213
|
+
if (files === undefined) return () => false;
|
|
214
|
+
const identities = new Set(Object.keys(files).map(bunPathIdentityKey));
|
|
215
|
+
return (file) => identities.has(bunPathIdentityKey(file));
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Normalize the path forms Bun equates for its in-memory file map.
|
|
220
|
+
*
|
|
221
|
+
* Bun normalizes Windows separators and drive-letter case, but preserves path
|
|
222
|
+
* component case, relative versus absolute spelling, and dot segments. A
|
|
223
|
+
* filesystem identity key is broader and would suppress real disk transforms.
|
|
224
|
+
*/
|
|
225
|
+
function bunPathIdentityKey(file: string): string {
|
|
226
|
+
if (process.platform !== "win32") return file;
|
|
227
|
+
return file
|
|
228
|
+
.replace(/\\/g, "/")
|
|
229
|
+
.replace(
|
|
230
|
+
/^([a-z]):/i,
|
|
231
|
+
(_match, drive: string) => `${drive.toLowerCase()}:`,
|
|
232
|
+
);
|
|
233
|
+
}
|
package/src/core/index.ts
CHANGED
|
@@ -1,17 +1,22 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
1
3
|
import type { UnpluginFactory, UnpluginInstance } from "unplugin";
|
|
2
4
|
import { createUnplugin } from "unplugin";
|
|
3
5
|
|
|
4
6
|
import type { TtscUnpluginOptions } from "./options";
|
|
5
7
|
import { resolveOptions } from "./options";
|
|
6
8
|
import {
|
|
9
|
+
beginTtscTransformBuild,
|
|
7
10
|
collectExternalInputHashes,
|
|
8
11
|
collectProjectInputHashes,
|
|
9
12
|
createTtscTransformCache,
|
|
10
13
|
isDeclarationFile,
|
|
11
14
|
isProjectWalkPath,
|
|
15
|
+
resetTtscTransformCache,
|
|
12
16
|
stripQuery,
|
|
13
17
|
transformTtsc,
|
|
14
18
|
} from "./transform";
|
|
19
|
+
import { createViteServeMissingInputWatch } from "./viteServe";
|
|
15
20
|
|
|
16
21
|
const name = "ttsc-unplugin";
|
|
17
22
|
/**
|
|
@@ -34,9 +39,9 @@ const virtualModulePattern = /\0/;
|
|
|
34
39
|
*
|
|
35
40
|
* The factory resolves raw options once, creates a per-build transform cache,
|
|
36
41
|
* and captures Vite alias configuration via the `vite.configResolved` hook so
|
|
37
|
-
* that path aliases are forwarded to the generated tsconfig overlay.
|
|
38
|
-
*
|
|
39
|
-
*
|
|
42
|
+
* that path aliases are forwarded to the generated tsconfig overlay. Real build
|
|
43
|
+
* lifecycles use a per-build cache; Vite's development server keeps persistent
|
|
44
|
+
* validation because its one `buildStart` spans later HMR edits.
|
|
40
45
|
*/
|
|
41
46
|
const unpluginFactory: UnpluginFactory<
|
|
42
47
|
TtscUnpluginOptions | undefined,
|
|
@@ -44,7 +49,9 @@ const unpluginFactory: UnpluginFactory<
|
|
|
44
49
|
> = (rawOptions = {}) => {
|
|
45
50
|
const options = resolveOptions(rawOptions);
|
|
46
51
|
const transformCache = createTtscTransformCache();
|
|
52
|
+
const missingInputs = createViteServeMissingInputWatch();
|
|
47
53
|
let aliases: unknown;
|
|
54
|
+
let viteCommand: string | undefined;
|
|
48
55
|
|
|
49
56
|
return {
|
|
50
57
|
name,
|
|
@@ -53,11 +60,34 @@ const unpluginFactory: UnpluginFactory<
|
|
|
53
60
|
vite: {
|
|
54
61
|
configResolved(config) {
|
|
55
62
|
aliases = config.resolve.alias;
|
|
63
|
+
// Re-read per config resolution: a plugin instance reused across a
|
|
64
|
+
// serve and a later build must stop routing missing inputs to the
|
|
65
|
+
// serve-time poll, even though the closed server stays attached
|
|
66
|
+
// (see the dispose note in viteServe.ts).
|
|
67
|
+
viteCommand = config.command;
|
|
68
|
+
},
|
|
69
|
+
// Vite serve funnels every transform-context `addWatchFile()` into the
|
|
70
|
+
// module's added-import graph (`_addedImports`), which import-analysis
|
|
71
|
+
// resolves like real imports. Capture the dev server so the transform
|
|
72
|
+
// hook can route watch inputs that do not exist yet — superseding
|
|
73
|
+
// resolution candidates above all — around that graph and still
|
|
74
|
+
// invalidate their importers when the path is created.
|
|
75
|
+
configureServer(server) {
|
|
76
|
+
missingInputs.attach(server);
|
|
77
|
+
},
|
|
78
|
+
// Vite calls buildEnd when the dev server (or build) closes; drop every
|
|
79
|
+
// poller so a stopped server leaks no watch state.
|
|
80
|
+
buildEnd() {
|
|
81
|
+
missingInputs.dispose();
|
|
56
82
|
},
|
|
57
83
|
},
|
|
58
84
|
|
|
59
85
|
buildStart() {
|
|
60
|
-
|
|
86
|
+
if (viteCommand === "serve") {
|
|
87
|
+
resetTtscTransformCache(transformCache);
|
|
88
|
+
} else {
|
|
89
|
+
beginTtscTransformBuild(transformCache);
|
|
90
|
+
}
|
|
61
91
|
},
|
|
62
92
|
|
|
63
93
|
transformInclude(id) {
|
|
@@ -75,8 +105,23 @@ const unpluginFactory: UnpluginFactory<
|
|
|
75
105
|
// unioned with the host-owned reference graph) so type-only inputs
|
|
76
106
|
// invalidate this module in watch mode and persistent caches;
|
|
77
107
|
// bundlers erase type-only imports from their own module graph and
|
|
78
|
-
// would otherwise serve stale generated code.
|
|
79
|
-
|
|
108
|
+
// would otherwise serve stale generated code. Under Vite serve a
|
|
109
|
+
// missing input must not enter `addWatchFile`: import-analysis
|
|
110
|
+
// resolves added imports and 500s on a path that is absent by design
|
|
111
|
+
// (a superseding resolution candidate, a not-yet-generated
|
|
112
|
+
// dependency), so those are watched on the filesystem instead and
|
|
113
|
+
// invalidate this module when created.
|
|
114
|
+
addWatchFile: (watched) => {
|
|
115
|
+
if (
|
|
116
|
+
viteCommand === "serve" &&
|
|
117
|
+
missingInputs.serving() &&
|
|
118
|
+
!fs.existsSync(watched)
|
|
119
|
+
) {
|
|
120
|
+
missingInputs.watch(watched, path.resolve(file));
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
this.addWatchFile(watched);
|
|
124
|
+
},
|
|
80
125
|
// A module the plugin declared volatile depends on non-file inputs,
|
|
81
126
|
// which no file-dependency snapshot can represent; mark it
|
|
82
127
|
// uncacheable where the bundler exposes that control.
|
|
@@ -103,10 +148,12 @@ export type {
|
|
|
103
148
|
} from "./options";
|
|
104
149
|
export type { TtscTransformHooks } from "./transform";
|
|
105
150
|
export {
|
|
151
|
+
beginTtscTransformBuild,
|
|
106
152
|
collectExternalInputHashes,
|
|
107
153
|
collectProjectInputHashes,
|
|
108
154
|
createTtscTransformCache,
|
|
109
155
|
isProjectWalkPath,
|
|
156
|
+
resetTtscTransformCache,
|
|
110
157
|
resolveOptions,
|
|
111
158
|
transformTtsc,
|
|
112
159
|
unplugin,
|
|
@@ -121,7 +168,7 @@ export default unplugin;
|
|
|
121
168
|
* Excluded ids: virtual modules (NUL prefix), `.d.ts` declaration files, and
|
|
122
169
|
* anything inside `node_modules`.
|
|
123
170
|
*/
|
|
124
|
-
function isTransformTarget(id: string): boolean {
|
|
171
|
+
export function isTransformTarget(id: string): boolean {
|
|
125
172
|
return (
|
|
126
173
|
sourceFilePattern.test(id) &&
|
|
127
174
|
!virtualModulePattern.test(id) &&
|