@rohal12/twee-ts 1.15.0 → 1.15.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,340 +1,407 @@
1
- import {
2
- TweeTsError,
3
- compileIncremental,
4
- mediaTypeFromFilename
5
- } from "../chunk-W3SNMQO4.js";
6
-
7
- // src/plugins/vite.ts
8
- import { resolve } from "path";
9
- import { stripVTControlCharacters } from "util";
10
- import { build, version as viteVersion } from "vite";
11
-
12
- // src/plugins/paths.ts
1
+ import { i as mediaTypeFromFilename, r as compileIncremental, t as TweeTsError } from "../compiler-Bfi1w-to.js";
2
+ import { resolve } from "node:path";
3
+ import { stripVTControlCharacters } from "node:util";
4
+ import { build, version } from "vite";
5
+ //#region src/plugins/paths.ts
6
+ /**
7
+ * Path helpers for the Vite plugin. Vite reports module ids with forward
8
+ * slashes on every platform, while node:path gives backslashes on Windows, so
9
+ * the plugin compares paths in forward-slash form only.
10
+ */
11
+ /** The path with Windows separators turned into forward slashes. */
13
12
  function toPosix(path) {
14
- return path.replace(/\\/g, "/");
13
+ return path.replace(/\\/g, "/");
15
14
  }
15
+ /** Whether `file` is one of `dirs` or inside one of them. All paths in forward-slash form. */
16
16
  function isInside(file, dirs) {
17
- return dirs.some((dir) => file === dir || file.startsWith(dir.endsWith("/") ? dir : `${dir}/`));
17
+ return dirs.some((dir) => file === dir || file.startsWith(dir.endsWith("/") ? dir : `${dir}/`));
18
18
  }
19
+ /**
20
+ * Whether `file` is a temporary copy of a Vite config file. Vite writes one next
21
+ * to the config file each time it loads it (when no node_modules folder is
22
+ * above the config, and always under Deno), then deletes it.
23
+ */
19
24
  function isViteConfigTemp(file) {
20
- return /\.timestamp-\d+-[0-9a-f]*\.mjs$/.test(file);
25
+ return /\.timestamp-\d+-[0-9a-f]*\.mjs$/.test(file);
21
26
  }
22
-
23
- // src/plugins/vite.ts
24
- var ENTRY_SCRIPT_NAME = "twee-ts-entry.js";
25
- var ENTRY_STYLE_NAME = "twee-ts-entry.css";
26
- var INNER_BUILD_FLAG = "__tweeTsEntryBuild";
27
- var EMPTY_INPUT = "virtual:twee-ts-empty-input";
28
- var RESOLVED_EMPTY_INPUT = "\0" + EMPTY_INPUT;
29
- var viteMajor = Number.parseInt(viteVersion, 10);
27
+ //#endregion
28
+ //#region src/plugins/vite.ts
29
+ /**
30
+ * Ready-made Vite plugin for twee-ts.
31
+ *
32
+ * Compiles Twee sources into the story HTML. With `entry`, Vite also bundles a
33
+ * script, and the CSS it imports, into the story as its Story JavaScript and
34
+ * Story Stylesheet.
35
+ *
36
+ * In dev the compiled HTML is served at the base URL with Vite's client added.
37
+ * Every change to a source, the head file, a module or a file the entry imports
38
+ * recompiles it and reloads the page, and errors appear in Vite's overlay.
39
+ */
40
+ /** Passage names the bundled entry takes inside the story. */
41
+ const ENTRY_SCRIPT_NAME = "twee-ts-entry.js";
42
+ const ENTRY_STYLE_NAME = "twee-ts-entry.css";
43
+ /**
44
+ * Set on the inline config of the build this plugin starts itself to bundle the
45
+ * entry in dev. The plugin instance that build loads from the user's config file
46
+ * sees it and stands aside.
47
+ */
48
+ const INNER_BUILD_FLAG = "__tweeTsEntryBuild";
49
+ /**
50
+ * A build input that stands in when there is no entry, so Vite does not look
51
+ * for an index.html (whose page would replace the story). Its empty chunk is
52
+ * dropped from the output.
53
+ */
54
+ const EMPTY_INPUT = "virtual:twee-ts-empty-input";
55
+ const RESOLVED_EMPTY_INPUT = "\0" + EMPTY_INPUT;
56
+ const viteMajor = Number.parseInt(version, 10);
30
57
  function formatDiagnostic(d) {
31
- const where = d.file ? `${d.file}${d.line ? `:${d.line}` : ""}: ` : "";
32
- const message = d.file && d.line ? d.message.replace(/^line \d+: /, "") : d.message;
33
- return `${where}${message}`;
58
+ return `${d.file ? `${d.file}${d.line ? `:${d.line}` : ""}: ` : ""}${d.file && d.line ? d.message.replace(/^line \d+: /, "") : d.message}`;
34
59
  }
60
+ /** Returns the warnings; throws a LocatedError when the compile reported errors. */
35
61
  function splitDiagnostics(result) {
36
- const errors = result.diagnostics.filter((d) => d.level === "error");
37
- if (errors.length === 0) return result.diagnostics.filter((d) => d.level === "warning");
38
- const error = new Error(errors.map(formatDiagnostic).join("\n"));
39
- const [first] = errors;
40
- if (first?.file) {
41
- error.id = first.file;
42
- error.loc = { file: first.file, line: first.line ?? 1, column: 1 };
43
- }
44
- throw error;
62
+ const errors = result.diagnostics.filter((d) => d.level === "error");
63
+ if (errors.length === 0) return result.diagnostics.filter((d) => d.level === "warning");
64
+ const error = new Error(errors.map(formatDiagnostic).join("\n"));
65
+ const [first] = errors;
66
+ if (first?.file) {
67
+ error.id = first.file;
68
+ error.loc = {
69
+ file: first.file,
70
+ line: first.line ?? 1,
71
+ column: 1
72
+ };
73
+ }
74
+ throw error;
45
75
  }
76
+ /** An error thrown by the compiler or bundler, with a TweeTsError's error diagnostics folded into its message. */
46
77
  function fatalError(e) {
47
- if (e instanceof TweeTsError) {
48
- const errors = e.diagnostics.filter((d) => d.level === "error").map(formatDiagnostic);
49
- return new Error([...errors, e.message].join("\n"));
50
- }
51
- return e instanceof Error ? e : new Error(String(e));
78
+ if (e instanceof TweeTsError) {
79
+ const errors = e.diagnostics.filter((d) => d.level === "error").map(formatDiagnostic);
80
+ return new Error([...errors, e.message].join("\n"));
81
+ }
82
+ return e instanceof Error ? e : new Error(String(e));
52
83
  }
53
84
  function buildCompileOptions(options, entry) {
54
- const inline = [];
55
- if (entry) {
56
- inline.push({ filename: ENTRY_SCRIPT_NAME, content: entry.script });
57
- if (entry.style !== "") inline.push({ filename: ENTRY_STYLE_NAME, content: entry.style });
58
- }
59
- return {
60
- ...options.compileOptions,
61
- sources: [...options.sources, ...inline],
62
- formatId: options.format ?? options.compileOptions?.formatId
63
- };
85
+ const inline = [];
86
+ if (entry) {
87
+ inline.push({
88
+ filename: ENTRY_SCRIPT_NAME,
89
+ content: entry.script
90
+ });
91
+ if (entry.style !== "") inline.push({
92
+ filename: ENTRY_STYLE_NAME,
93
+ content: entry.style
94
+ });
95
+ }
96
+ return {
97
+ ...options.compileOptions,
98
+ sources: [...options.sources, ...inline],
99
+ formatId: options.format ?? options.compileOptions?.formatId
100
+ };
64
101
  }
102
+ /** Whether the user's config names build inputs of its own. */
65
103
  function hasUserInput(userConfig) {
66
- const build2 = userConfig.build;
67
- return build2?.["rolldownOptions"]?.input !== void 0 || build2?.["rollupOptions"]?.input !== void 0;
104
+ const build = userConfig.build;
105
+ return build?.["rolldownOptions"]?.input !== void 0 || build?.["rollupOptions"]?.input !== void 0;
68
106
  }
107
+ /** Build settings naming one input, under the option name this Vite version reads. */
69
108
  function inputOnly(input) {
70
- return viteMajor >= 8 ? { rolldownOptions: { input } } : { rollupOptions: { input } };
109
+ return viteMajor >= 8 ? { rolldownOptions: { input } } : { rollupOptions: { input } };
71
110
  }
111
+ /**
112
+ * Build settings that turn the entry into one IIFE script and one stylesheet.
113
+ * Fonts and images the entry uses are inlined as data URLs, so the story stays
114
+ * one file; only an import marked `?no-inline` is still emitted separately.
115
+ */
72
116
  function entryBuildOptions(entryPath) {
73
- return {
74
- cssCodeSplit: false,
75
- assetsInlineLimit: () => true,
76
- rolldownOptions: {
77
- input: entryPath,
78
- output: { format: "iife", entryFileNames: ENTRY_SCRIPT_NAME, assetFileNames: "[name][extname]" }
79
- }
80
- };
117
+ return {
118
+ cssCodeSplit: false,
119
+ assetsInlineLimit: () => true,
120
+ rolldownOptions: {
121
+ input: entryPath,
122
+ output: {
123
+ format: "iife",
124
+ entryFileNames: ENTRY_SCRIPT_NAME,
125
+ assetFileNames: "[name][extname]"
126
+ }
127
+ }
128
+ };
81
129
  }
130
+ /** Removes a trailing source-map comment that points at a file (a data: URL map stays). */
82
131
  function dropFileSourceMapComment(code) {
83
- return code.replace(/\n?\/\/# sourceMappingURL=(?!data:)\S*\s*$/, "").replace(/\n?\/\*# sourceMappingURL=(?!data:)[^*]*\*\/\s*$/, "");
132
+ return code.replace(/\n?\/\/# sourceMappingURL=(?!data:)\S*\s*$/, "").replace(/\n?\/\*# sourceMappingURL=(?!data:)[^*]*\*\/\s*$/, "");
84
133
  }
134
+ /**
135
+ * Takes the entry's script and stylesheet out of the bundle, together with
136
+ * their map files, which the story cannot ship. Anything else the bundler
137
+ * emitted stays in the bundle, so a build writes it next to the HTML, and is
138
+ * also returned, so the dev server can serve it.
139
+ */
85
140
  function takeEntryFromBundle(bundle) {
86
- const entry = { script: "", style: "", files: /* @__PURE__ */ new Set(), assets: /* @__PURE__ */ new Map() };
87
- const styles = [];
88
- for (const [fileName, item] of Object.entries(bundle)) {
89
- if (item.type === "chunk") {
90
- if (item.isEntry) {
91
- entry.script = dropFileSourceMapComment(item.code);
92
- for (const id of item.moduleIds) if (!id.startsWith("\0")) entry.files.add(toPosix(id));
93
- }
94
- delete bundle[fileName];
95
- } else if (fileName.endsWith(".css")) {
96
- const css = typeof item.source === "string" ? item.source : new TextDecoder().decode(item.source);
97
- styles.push(dropFileSourceMapComment(css));
98
- delete bundle[fileName];
99
- } else if (fileName.endsWith(".map")) {
100
- delete bundle[fileName];
101
- } else {
102
- entry.assets.set(fileName, item.source);
103
- }
104
- }
105
- entry.style = styles.join("\n");
106
- return entry;
141
+ const entry = {
142
+ script: "",
143
+ style: "",
144
+ files: /* @__PURE__ */ new Set(),
145
+ assets: /* @__PURE__ */ new Map()
146
+ };
147
+ const styles = [];
148
+ for (const [fileName, item] of Object.entries(bundle)) if (item.type === "chunk") {
149
+ if (item.isEntry) {
150
+ entry.script = dropFileSourceMapComment(item.code);
151
+ for (const id of item.moduleIds) if (!id.startsWith("\0")) entry.files.add(toPosix(id));
152
+ }
153
+ delete bundle[fileName];
154
+ } else if (fileName.endsWith(".css")) {
155
+ const css = typeof item.source === "string" ? item.source : new TextDecoder().decode(item.source);
156
+ styles.push(dropFileSourceMapComment(css));
157
+ delete bundle[fileName];
158
+ } else if (fileName.endsWith(".map")) delete bundle[fileName];
159
+ else entry.assets.set(fileName, item.source);
160
+ entry.style = styles.join("\n");
161
+ return entry;
107
162
  }
163
+ /** Absolute forward-slash paths whose changes recompile the story: sources, head file, modules. */
108
164
  function watchedInputs(options) {
109
- const extra = options.compileOptions;
110
- return [...options.sources, ...extra?.headFile ? [extra.headFile] : [], ...extra?.modules ?? []].map(
111
- (p) => toPosix(resolve(p))
112
- );
165
+ const extra = options.compileOptions;
166
+ return [
167
+ ...options.sources,
168
+ ...extra?.headFile ? [extra.headFile] : [],
169
+ ...extra?.modules ?? []
170
+ ].map((p) => toPosix(resolve(p)));
113
171
  }
172
+ /** Adds Vite's client to the page so reloads and the error overlay reach it. */
114
173
  function injectViteClient(html, base) {
115
- const tag = `<script type="module" src="${base}@vite/client"></script>`;
116
- const head = /<head[^>]*>/i.exec(html);
117
- if (!head) return tag + html;
118
- const at = head.index + head[0].length;
119
- return html.slice(0, at) + tag + html.slice(at);
174
+ const tag = `<script type="module" src="${base}@vite/client"><\/script>`;
175
+ const head = /<head[^>]*>/i.exec(html);
176
+ if (!head) return tag + html;
177
+ const at = head.index + head[0].length;
178
+ return html.slice(0, at) + tag + html.slice(at);
120
179
  }
180
+ /** Served until the first successful compile, so the overlay has a page to appear on. */
121
181
  function waitingPage(base) {
122
- return `<!doctype html><html><head><meta charset="utf-8"><script type="module" src="${base}@vite/client"></script><title>twee-ts</title></head><body><p>The story has not compiled yet.</p></body></html>`;
182
+ return `<!doctype html><html><head><meta charset="utf-8"><script type="module" src="${base}@vite/client"><\/script><title>twee-ts</title></head><body><p>The story has not compiled yet.</p></body></html>`;
123
183
  }
124
184
  function toOverlayError(e) {
125
- const error = fatalError(e);
126
- return {
127
- // Bundler messages carry terminal colour codes; the overlay would show them raw.
128
- message: stripVTControlCharacters(error.message),
129
- stack: "",
130
- plugin: "twee-ts",
131
- ...error.id ? { id: error.id } : {},
132
- ...error.loc ? { loc: error.loc } : {}
133
- };
185
+ const error = fatalError(e);
186
+ return {
187
+ message: stripVTControlCharacters(error.message),
188
+ stack: "",
189
+ plugin: "twee-ts",
190
+ ...error.id ? { id: error.id } : {},
191
+ ...error.loc ? { loc: error.loc } : {}
192
+ };
134
193
  }
194
+ /** Bundles the entry for dev with the user's own Vite config, unminified, with an inline source map. */
195
+ /**
196
+ * The entry build's logger: its warnings go to the dev server's logger; its
197
+ * progress lines and its own "build failed" line are dropped, because the plugin
198
+ * reports a failed bundle itself.
199
+ */
135
200
  function entryBuildLogger(outer) {
136
- return {
137
- info: () => {
138
- },
139
- warn: (message, options) => outer.warn(message, options),
140
- warnOnce: (message, options) => outer.warnOnce(message, options),
141
- error: () => {
142
- },
143
- clearScreen: () => {
144
- },
145
- hasErrorLogged: () => false,
146
- get hasWarned() {
147
- return outer.hasWarned;
148
- }
149
- };
201
+ return {
202
+ info: () => {},
203
+ warn: (message, options) => outer.warn(message, options),
204
+ warnOnce: (message, options) => outer.warnOnce(message, options),
205
+ error: () => {},
206
+ clearScreen: () => {},
207
+ hasErrorLogged: () => false,
208
+ get hasWarned() {
209
+ return outer.hasWarned;
210
+ }
211
+ };
150
212
  }
151
- var oneOffEntryBuild = {
152
- name: "twee-ts:one-off-entry-build",
153
- enforce: "post",
154
- config(userConfig) {
155
- if (userConfig.build) userConfig.build.watch = null;
156
- }
213
+ /**
214
+ * Keeps the entry build a one-off even when the user's config sets build.watch:
215
+ * Vite's config merge skips a null, so the inline `watch: null` alone can't.
216
+ */
217
+ const oneOffEntryBuild = {
218
+ name: "twee-ts:one-off-entry-build",
219
+ enforce: "post",
220
+ config(userConfig) {
221
+ if (userConfig.build) userConfig.build.watch = null;
222
+ }
157
223
  };
224
+ /** Bundles the entry for dev with the user's own Vite config, unminified, with an inline source map. */
158
225
  async function bundleEntryForDev(config, entryPath) {
159
- const inline = {
160
- configFile: config.configFile ?? false,
161
- root: config.root,
162
- mode: config.mode,
163
- customLogger: entryBuildLogger(config.logger),
164
- publicDir: false,
165
- // With a config file the entry build reads it again, plugins and all. Without
166
- // one, the settings that change how code bundles come across from the server.
167
- ...config.configFile ? {} : { define: config.define, resolve: { alias: config.resolve.alias } },
168
- build: {
169
- ...entryBuildOptions(entryPath),
170
- write: false,
171
- minify: false,
172
- sourcemap: "inline",
173
- emptyOutDir: false,
174
- copyPublicDir: false,
175
- watch: null
176
- },
177
- plugins: [oneOffEntryBuild],
178
- [INNER_BUILD_FLAG]: true
179
- };
180
- const out = await build(inline);
181
- const bundle = {};
182
- for (const result of Array.isArray(out) ? out : [out]) {
183
- if (!("output" in result)) {
184
- await result.close();
185
- throw new Error("twee-ts: the entry build returned a watcher instead of a bundle.");
186
- }
187
- for (const item of result.output) bundle[item.fileName] = item;
188
- }
189
- return takeEntryFromBundle(bundle);
226
+ const inline = {
227
+ configFile: config.configFile ?? false,
228
+ root: config.root,
229
+ mode: config.mode,
230
+ customLogger: entryBuildLogger(config.logger),
231
+ publicDir: false,
232
+ ...config.configFile ? {} : {
233
+ define: config.define,
234
+ resolve: { alias: config.resolve.alias }
235
+ },
236
+ build: {
237
+ ...entryBuildOptions(entryPath),
238
+ write: false,
239
+ minify: false,
240
+ sourcemap: "inline",
241
+ emptyOutDir: false,
242
+ copyPublicDir: false,
243
+ watch: null
244
+ },
245
+ plugins: [oneOffEntryBuild],
246
+ [INNER_BUILD_FLAG]: true
247
+ };
248
+ const out = await build(inline);
249
+ const bundle = {};
250
+ for (const result of Array.isArray(out) ? out : [out]) {
251
+ if (!("output" in result)) {
252
+ await result.close();
253
+ throw new Error("twee-ts: the entry build returned a watcher instead of a bundle.");
254
+ }
255
+ for (const item of result.output) bundle[item.fileName] = item;
256
+ }
257
+ return takeEntryFromBundle(bundle);
190
258
  }
191
259
  function tweeTsPlugin(options) {
192
- const outputFilename = options.outputFilename ?? "index.html";
193
- const cache = /* @__PURE__ */ new Map();
194
- let innerBuild = false;
195
- let stopDev;
196
- if (options.entry && viteMajor < 8) {
197
- throw new Error(`twee-ts: the entry option needs Vite 8 or newer (found ${viteVersion}).`);
198
- }
199
- return {
200
- name: "twee-ts",
201
- config(userConfig, env) {
202
- innerBuild = userConfig[INNER_BUILD_FLAG] === true;
203
- if (innerBuild || env.command !== "build") return void 0;
204
- if (options.entry) return { build: entryBuildOptions(resolve(options.entry)) };
205
- if (hasUserInput(userConfig)) return void 0;
206
- return { build: inputOnly(EMPTY_INPUT) };
207
- },
208
- configResolved(config) {
209
- if (innerBuild || !options.entry) return;
210
- const sources = options.sources.map((p) => toPosix(resolve(p)));
211
- if (isInside(toPosix(resolve(options.entry)), sources)) {
212
- config.logger.warn(
213
- `[twee-ts] The entry ${options.entry} is inside the story sources (${options.sources.join(", ")}). twee-ts also loads the .js and .css files it finds in source folders, unbundled, as Story JavaScript and Story Stylesheet; keep the entry's folder out of \`sources\`.`
214
- );
215
- }
216
- },
217
- resolveId(id) {
218
- return id === EMPTY_INPUT ? RESOLVED_EMPTY_INPUT : void 0;
219
- },
220
- load(id) {
221
- return id === RESOLVED_EMPTY_INPUT ? "globalThis.tweeTsEmptyInput = true;" : void 0;
222
- },
223
- generateBundle: {
224
- order: "post",
225
- async handler(_outputOptions, bundle) {
226
- if (innerBuild) return;
227
- for (const [fileName, item] of Object.entries(bundle)) {
228
- if (item.type === "chunk" && item.facadeModuleId === RESOLVED_EMPTY_INPUT) delete bundle[fileName];
229
- }
230
- const entry = options.entry ? takeEntryFromBundle(bundle) : void 0;
231
- let result;
232
- try {
233
- result = await compileIncremental(buildCompileOptions(options, entry), cache);
234
- } catch (e) {
235
- return this.error(fatalError(e));
236
- }
237
- let warnings;
238
- try {
239
- warnings = splitDiagnostics(result);
240
- } catch (e) {
241
- return this.error(e);
242
- }
243
- for (const w of warnings) this.warn(formatDiagnostic(w));
244
- this.emitFile({ type: "asset", fileName: outputFilename, source: result.output });
245
- }
246
- },
247
- async configureServer(server) {
248
- if (innerBuild) return;
249
- const base = server.config.base;
250
- const servePaths = outputFilename === "index.html" ? [base, `${base}index.html`] : [`${base}${outputFilename}`];
251
- const inputs = watchedInputs(options);
252
- const entryPath = options.entry ? resolve(options.entry) : void 0;
253
- const root = toPosix(server.config.root);
254
- server.watcher.add(inputs);
255
- let html = "";
256
- let lastError;
257
- let entry;
258
- let entryStale = true;
259
- let queue = Promise.resolve();
260
- let pending = /* @__PURE__ */ new Set();
261
- let timer;
262
- let closed = false;
263
- stopDev = () => {
264
- closed = true;
265
- clearTimeout(timer);
266
- pending.clear();
267
- };
268
- const touchesEntry = (file) => entryPath !== void 0 && (entryStale ? isInside(file, [root]) : entry?.files.has(file) ?? false);
269
- const rebuild = async (changed, initial = false) => {
270
- if (closed) return;
271
- for (const key of [...cache.keys()]) if (changed.has(toPosix(resolve(key)))) cache.delete(key);
272
- try {
273
- if (entryPath && (entryStale || [...changed].some(touchesEntry))) {
274
- entryStale = true;
275
- entry = await bundleEntryForDev(server.config, entryPath);
276
- entryStale = false;
277
- }
278
- const result = await compileIncremental(buildCompileOptions(options, entry), cache);
279
- for (const w of splitDiagnostics(result)) server.config.logger.warn(`[twee-ts] ${formatDiagnostic(w)}`);
280
- html = injectViteClient(result.output, base);
281
- lastError = void 0;
282
- if (!initial) server.ws.send({ type: "full-reload" });
283
- } catch (e) {
284
- lastError = toOverlayError(e);
285
- server.config.logger.error(`[twee-ts] ${lastError.message}`);
286
- server.ws.send({ type: "error", err: lastError });
287
- }
288
- };
289
- const keepQueueAlive = (e) => {
290
- try {
291
- server.config.logger.error(`[twee-ts] ${fatalError(e).message}`);
292
- } catch {
293
- }
294
- };
295
- await rebuild(/* @__PURE__ */ new Set(), true);
296
- server.watcher.on("all", (event, file) => {
297
- if (event !== "add" && event !== "change" && event !== "unlink") return;
298
- const changed = toPosix(resolve(file));
299
- if (isViteConfigTemp(changed)) return;
300
- if (!isInside(changed, inputs) && !touchesEntry(changed)) return;
301
- pending.add(changed);
302
- clearTimeout(timer);
303
- timer = setTimeout(() => {
304
- const files = pending;
305
- pending = /* @__PURE__ */ new Set();
306
- queue = queue.then(() => rebuild(files)).catch(keepQueueAlive);
307
- }, 50);
308
- });
309
- server.ws.on("connection", () => {
310
- if (lastError) server.ws.send({ type: "error", err: lastError });
311
- });
312
- server.middlewares.use((req, res, next) => {
313
- const path = (req.url ?? "").split("?")[0] ?? "";
314
- if (servePaths.includes(path)) {
315
- res.statusCode = 200;
316
- res.setHeader("Content-Type", "text/html; charset=utf-8");
317
- res.end(html || waitingPage(base));
318
- return;
319
- }
320
- const asset = path.startsWith(base) ? entry?.assets.get(path.slice(base.length)) : void 0;
321
- if (asset !== void 0) {
322
- res.statusCode = 200;
323
- res.setHeader("Content-Type", mediaTypeFromFilename(path));
324
- res.end(asset);
325
- return;
326
- }
327
- next();
328
- });
329
- },
330
- // Vite runs this when the dev server closes (and after a build); no rebuild may
331
- // start after it, including one whose timer is still pending in middleware mode.
332
- closeBundle() {
333
- stopDev?.();
334
- }
335
- };
260
+ const outputFilename = options.outputFilename ?? "index.html";
261
+ const cache = /* @__PURE__ */ new Map();
262
+ let innerBuild = false;
263
+ let stopDev;
264
+ if (options.entry && viteMajor < 8) throw new Error(`twee-ts: the entry option needs Vite 8 or newer (found ${version}).`);
265
+ return {
266
+ name: "twee-ts",
267
+ config(userConfig, env) {
268
+ innerBuild = userConfig[INNER_BUILD_FLAG] === true;
269
+ if (innerBuild || env.command !== "build") return void 0;
270
+ if (options.entry) return { build: entryBuildOptions(resolve(options.entry)) };
271
+ if (hasUserInput(userConfig)) return void 0;
272
+ return { build: inputOnly(EMPTY_INPUT) };
273
+ },
274
+ configResolved(config) {
275
+ if (innerBuild || !options.entry) return;
276
+ const sources = options.sources.map((p) => toPosix(resolve(p)));
277
+ if (isInside(toPosix(resolve(options.entry)), sources)) config.logger.warn(`[twee-ts] The entry ${options.entry} is inside the story sources (${options.sources.join(", ")}). twee-ts also loads the .js and .css files it finds in source folders, unbundled, as Story JavaScript and Story Stylesheet; keep the entry's folder out of \`sources\`.`);
278
+ },
279
+ resolveId(id) {
280
+ return id === EMPTY_INPUT ? RESOLVED_EMPTY_INPUT : void 0;
281
+ },
282
+ load(id) {
283
+ return id === RESOLVED_EMPTY_INPUT ? "globalThis.tweeTsEmptyInput = true;" : void 0;
284
+ },
285
+ generateBundle: {
286
+ order: "post",
287
+ async handler(_outputOptions, bundle) {
288
+ if (innerBuild) return;
289
+ for (const [fileName, item] of Object.entries(bundle)) if (item.type === "chunk" && item.facadeModuleId === RESOLVED_EMPTY_INPUT) delete bundle[fileName];
290
+ const entry = options.entry ? takeEntryFromBundle(bundle) : void 0;
291
+ let result;
292
+ try {
293
+ result = await compileIncremental(buildCompileOptions(options, entry), cache);
294
+ } catch (e) {
295
+ return this.error(fatalError(e));
296
+ }
297
+ let warnings;
298
+ try {
299
+ warnings = splitDiagnostics(result);
300
+ } catch (e) {
301
+ return this.error(e);
302
+ }
303
+ for (const w of warnings) this.warn(formatDiagnostic(w));
304
+ this.emitFile({
305
+ type: "asset",
306
+ fileName: outputFilename,
307
+ source: result.output
308
+ });
309
+ }
310
+ },
311
+ async configureServer(server) {
312
+ if (innerBuild) return;
313
+ const base = server.config.base;
314
+ const servePaths = outputFilename === "index.html" ? [base, `${base}index.html`] : [`${base}${outputFilename}`];
315
+ const inputs = watchedInputs(options);
316
+ const entryPath = options.entry ? resolve(options.entry) : void 0;
317
+ const root = toPosix(server.config.root);
318
+ server.watcher.add(inputs);
319
+ let html = "";
320
+ let lastError;
321
+ let entry;
322
+ let entryStale = true;
323
+ let queue = Promise.resolve();
324
+ let pending = /* @__PURE__ */ new Set();
325
+ let timer;
326
+ let closed = false;
327
+ stopDev = () => {
328
+ closed = true;
329
+ clearTimeout(timer);
330
+ pending.clear();
331
+ };
332
+ const touchesEntry = (file) => entryPath !== void 0 && (entryStale ? isInside(file, [root]) : entry?.files.has(file) ?? false);
333
+ const rebuild = async (changed, initial = false) => {
334
+ if (closed) return;
335
+ for (const key of [...cache.keys()]) if (changed.has(toPosix(resolve(key)))) cache.delete(key);
336
+ try {
337
+ if (entryPath && (entryStale || [...changed].some(touchesEntry))) {
338
+ entryStale = true;
339
+ entry = await bundleEntryForDev(server.config, entryPath);
340
+ entryStale = false;
341
+ }
342
+ const result = await compileIncremental(buildCompileOptions(options, entry), cache);
343
+ for (const w of splitDiagnostics(result)) server.config.logger.warn(`[twee-ts] ${formatDiagnostic(w)}`);
344
+ html = injectViteClient(result.output, base);
345
+ lastError = void 0;
346
+ if (!initial) server.ws.send({ type: "full-reload" });
347
+ } catch (e) {
348
+ lastError = toOverlayError(e);
349
+ server.config.logger.error(`[twee-ts] ${lastError.message}`);
350
+ server.ws.send({
351
+ type: "error",
352
+ err: lastError
353
+ });
354
+ }
355
+ };
356
+ const keepQueueAlive = (e) => {
357
+ try {
358
+ server.config.logger.error(`[twee-ts] ${fatalError(e).message}`);
359
+ } catch {}
360
+ };
361
+ await rebuild(/* @__PURE__ */ new Set(), true);
362
+ server.watcher.on("all", (event, file) => {
363
+ if (event !== "add" && event !== "change" && event !== "unlink") return;
364
+ const changed = toPosix(resolve(file));
365
+ if (isViteConfigTemp(changed)) return;
366
+ if (!isInside(changed, inputs) && !touchesEntry(changed)) return;
367
+ pending.add(changed);
368
+ clearTimeout(timer);
369
+ timer = setTimeout(() => {
370
+ const files = pending;
371
+ pending = /* @__PURE__ */ new Set();
372
+ queue = queue.then(() => rebuild(files)).catch(keepQueueAlive);
373
+ }, 50);
374
+ });
375
+ server.ws.on("connection", () => {
376
+ if (lastError) server.ws.send({
377
+ type: "error",
378
+ err: lastError
379
+ });
380
+ });
381
+ server.middlewares.use((req, res, next) => {
382
+ const path = (req.url ?? "").split("?")[0] ?? "";
383
+ if (servePaths.includes(path)) {
384
+ res.statusCode = 200;
385
+ res.setHeader("Content-Type", "text/html; charset=utf-8");
386
+ res.end(html || waitingPage(base));
387
+ return;
388
+ }
389
+ const asset = path.startsWith(base) ? entry?.assets.get(path.slice(base.length)) : void 0;
390
+ if (asset !== void 0) {
391
+ res.statusCode = 200;
392
+ res.setHeader("Content-Type", mediaTypeFromFilename(path));
393
+ res.end(asset);
394
+ return;
395
+ }
396
+ next();
397
+ });
398
+ },
399
+ closeBundle() {
400
+ stopDev?.();
401
+ }
402
+ };
336
403
  }
337
- export {
338
- tweeTsPlugin
339
- };
404
+ //#endregion
405
+ export { tweeTsPlugin };
406
+
340
407
  //# sourceMappingURL=vite.js.map