vite-plugin-spiral 0.1.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/dist/index.js ADDED
@@ -0,0 +1,1111 @@
1
+ import {
2
+ familyToSlug
3
+ } from "./chunk-VHEBKNM2.js";
4
+
5
+ // src/index.ts
6
+ import { createHash as createHash2 } from "crypto";
7
+ import fsSync2 from "fs";
8
+ import { promises as fs2 } from "fs";
9
+ import os from "os";
10
+ import path2 from "path";
11
+ import colors from "picocolors";
12
+ import { glob as glob2 } from "tinyglobby";
13
+ import { loadEnv } from "vite";
14
+ import fullReload from "vite-plugin-full-reload";
15
+
16
+ // src/fonts/plugin.ts
17
+ import { createHash } from "crypto";
18
+ import { createRequire } from "module";
19
+ import fsSync from "fs";
20
+ import { promises as fs } from "fs";
21
+ import path from "path";
22
+ import { pathToFileURL } from "url";
23
+ import { glob } from "tinyglobby";
24
+
25
+ // src/fonts/types.ts
26
+ var FORMAT_MIME = {
27
+ woff2: "font/woff2",
28
+ woff: "font/woff",
29
+ ttf: "font/ttf",
30
+ otf: "font/otf",
31
+ eot: "application/vnd.ms-fontobject"
32
+ };
33
+
34
+ // src/fonts/plugin.ts
35
+ var remoteCssUrls = {
36
+ google: "https://fonts.googleapis.com/css2",
37
+ bunny: "https://fonts.bunny.net/css2"
38
+ };
39
+ var fontExtensions = {
40
+ ".woff2": "woff2",
41
+ ".woff": "woff",
42
+ ".ttf": "ttf",
43
+ ".otf": "otf",
44
+ ".eot": "eot"
45
+ };
46
+ var formatPreference = ["woff2", "woff", "ttf", "otf", "eot"];
47
+ var supportedGlob = "*.{woff2,woff,ttf,otf,eot}";
48
+ function resolveFontsPlugin(fonts, hotFile, buildDirectory) {
49
+ if (fonts.length === 0) {
50
+ return [];
51
+ }
52
+ validateFonts(fonts);
53
+ let config;
54
+ let cacheDir;
55
+ let hotManifestPath;
56
+ let families = [];
57
+ let refs = /* @__PURE__ */ new Map();
58
+ return [{
59
+ name: "vite-plugin-spiral:fonts",
60
+ enforce: "post",
61
+ configResolved(resolvedConfig) {
62
+ config = resolvedConfig;
63
+ cacheDir = path.resolve(config.root, "node_modules/.cache/vite-plugin-spiral/fonts");
64
+ hotManifestPath = path.resolve(config.root, path.dirname(hotFile), "fonts-manifest.dev.json");
65
+ },
66
+ async buildStart() {
67
+ if (config.command !== "build") {
68
+ return;
69
+ }
70
+ families = await resolveFontFamilies(fonts, config.root, cacheDir);
71
+ refs = emitFontAssets(families, (asset) => this.emitFile(asset));
72
+ },
73
+ generateBundle() {
74
+ if (config.command !== "build" || families.length === 0) {
75
+ return;
76
+ }
77
+ const fileMap = /* @__PURE__ */ new Map();
78
+ for (const [source, ref] of refs) {
79
+ fileMap.set(source, this.getFileName(ref));
80
+ }
81
+ const publicBase = normalizePublicBase(config.base, buildDirectory);
82
+ const css = generateFontCss(families, fileMap, (file) => `${publicBase}${file}`);
83
+ const cssRef = this.emitFile({
84
+ type: "asset",
85
+ name: "fonts.css",
86
+ source: css
87
+ });
88
+ const cssFile = this.getFileName(cssRef);
89
+ const manifest = buildFontManifest(families, fileMap, cssFile, (file) => file);
90
+ this.emitFile({
91
+ type: "asset",
92
+ fileName: "fonts-manifest.json",
93
+ source: `${JSON.stringify(manifest, null, 2)}
94
+ `
95
+ });
96
+ },
97
+ configureServer(server) {
98
+ const middleware = createFontMiddleware();
99
+ server.middlewares.use(middleware);
100
+ server.httpServer?.once("listening", async () => {
101
+ try {
102
+ families = await resolveFontFamilies(fonts, config.root, cacheDir);
103
+ const devUrl = fsSync.existsSync(path.resolve(config.root, hotFile)) ? fsSync.readFileSync(path.resolve(config.root, hotFile), "utf8").trim() : `http://localhost:${server.config.server.port ?? 5173}`;
104
+ const urlMap = /* @__PURE__ */ new Map();
105
+ for (const family of families) {
106
+ for (const variant of family.variants) {
107
+ for (const file of variant.files) {
108
+ const urlPath = `/@spiral-vite-fonts/${encodeURIComponent(createHash("sha1").update(file.source).digest("hex"))}.${file.format}`;
109
+ middleware.files.set(urlPath, file);
110
+ urlMap.set(file.source, `${devUrl}${urlPath}`);
111
+ }
112
+ }
113
+ }
114
+ const css = generateFontCss(families, urlMap, (file) => file);
115
+ const manifest = buildFontManifest(families, urlMap, void 0, (file) => file, css);
116
+ fsSync.mkdirSync(path.dirname(hotManifestPath), { recursive: true });
117
+ fsSync.writeFileSync(hotManifestPath, `${JSON.stringify(manifest, null, 2)}
118
+ `, "utf8");
119
+ } catch (error) {
120
+ server.config.logger.error(`[vite-plugin-spiral:fonts] ${error.message}`);
121
+ }
122
+ });
123
+ const cleanup = () => {
124
+ fsSync.rmSync(hotManifestPath, { force: true });
125
+ };
126
+ process.on("exit", cleanup);
127
+ server.httpServer?.once("close", () => {
128
+ cleanup();
129
+ process.removeListener("exit", cleanup);
130
+ });
131
+ }
132
+ }];
133
+ }
134
+ async function resolveFontFamilies(fonts, root, cacheDir) {
135
+ const families = [];
136
+ for (const font of fonts) {
137
+ let family;
138
+ if (font.provider === "local") {
139
+ family = await resolveLocalFont(font, root);
140
+ } else if (font.provider === "fontsource") {
141
+ family = resolveFontsourceFont(font, root);
142
+ } else {
143
+ family = await resolveRemoteFont(font, cacheDir, remoteCssUrls[font.provider]);
144
+ }
145
+ families.push(await withOptimizedFallbacks(family));
146
+ }
147
+ return families;
148
+ }
149
+ async function resolveLocalFont(font, root) {
150
+ if (!font._local) {
151
+ throw new Error(`vite-plugin-spiral: Local font "${font.family}" requires src or variants.`);
152
+ }
153
+ if ("variants" in font._local) {
154
+ return buildResolvedFamily(font, font._local.variants.map((variant) => {
155
+ const src = Array.isArray(variant.src) ? variant.src : [variant.src];
156
+ const files2 = src.map((file) => resolveFontFile(path.resolve(root, file)));
157
+ return {
158
+ weight: variant.weight ?? inferWeight(src[0]),
159
+ style: variant.style ?? inferStyle(src[0]),
160
+ files: sortFiles(files2)
161
+ };
162
+ }));
163
+ }
164
+ const files = await discoverLocalFiles(font.family, font._local.src, root);
165
+ if (files.some(isVariableFontFilename)) {
166
+ throw new Error(`vite-plugin-spiral: Local variable font "${font.family}" must use explicit variants instead of src shorthand.`);
167
+ }
168
+ return buildResolvedFamily(font, groupByVariant(files));
169
+ }
170
+ function resolveFontsourceFont(font, root) {
171
+ const packageName = font._fontsource?.package ?? `@fontsource/${familyToSlug(font.family)}`;
172
+ const require2 = createRequire(path.resolve(root, "package.json"));
173
+ let packageDirectory;
174
+ try {
175
+ packageDirectory = path.dirname(require2.resolve(`${packageName}/package.json`));
176
+ } catch {
177
+ throw new Error(`vite-plugin-spiral: Fontsource package "${packageName}" was not found. Install it with npm install ${packageName}.`);
178
+ }
179
+ const variants = [];
180
+ for (const weight of font.weights) {
181
+ for (const style of font.styles) {
182
+ for (const subset of font.subsets) {
183
+ const fileName = style === "italic" ? `${subset}-${weight}-italic.css` : `${subset}-${weight}.css`;
184
+ const cssPath = path.resolve(packageDirectory, fileName);
185
+ if (!fsSync.existsSync(cssPath)) {
186
+ throw new Error(`vite-plugin-spiral: Fontsource CSS file "${fileName}" was not found in "${packageName}".`);
187
+ }
188
+ const faces = parseFontFaceCss(fsSync.readFileSync(cssPath, "utf8"));
189
+ for (const face of faces) {
190
+ variants.push({
191
+ weight: face.weight,
192
+ style: face.style,
193
+ files: sortFiles(face.src.map((src) => ({
194
+ source: path.resolve(path.dirname(cssPath), src.url),
195
+ format: src.format,
196
+ unicodeRange: face.unicodeRange
197
+ })))
198
+ });
199
+ }
200
+ }
201
+ }
202
+ }
203
+ if (variants.length === 0) {
204
+ throw new Error(`vite-plugin-spiral: Fontsource package "${packageName}" resolved no variants for "${font.family}".`);
205
+ }
206
+ return buildResolvedFamily(font, variants);
207
+ }
208
+ async function resolveRemoteFont(font, cacheDir, cssBaseUrl) {
209
+ if (!cssBaseUrl) {
210
+ throw new Error(`vite-plugin-spiral: Unsupported remote font provider "${font.provider}".`);
211
+ }
212
+ await fs.mkdir(cacheDir, { recursive: true });
213
+ const css = await fetchTextAndCache(buildCss2Url(cssBaseUrl, font), cacheDir, {
214
+ "User-Agent": "Mozilla/5.0 AppleWebKit/537.36 Chrome/131 Safari/537.36"
215
+ });
216
+ const faces = parseFontFaceCss(css);
217
+ if (faces.length === 0) {
218
+ throw new Error(`vite-plugin-spiral: ${font.provider} returned no @font-face rules for "${font.family}".`);
219
+ }
220
+ const variants = [];
221
+ for (const face of faces) {
222
+ const files = [];
223
+ for (const src of face.src) {
224
+ files.push({
225
+ source: await fetchFileAndCache(src.url, cacheDir),
226
+ format: src.format,
227
+ unicodeRange: face.unicodeRange
228
+ });
229
+ }
230
+ variants.push({ weight: face.weight, style: face.style, files: sortFiles(files) });
231
+ }
232
+ return buildResolvedFamily(font, variants);
233
+ }
234
+ function buildCss2Url(baseUrl, font) {
235
+ const family = font.family.replace(/ /g, "+");
236
+ const hasItalic = font.styles.includes("italic");
237
+ const axes = hasItalic ? ["ital", "wght"] : ["wght"];
238
+ const tuples = /* @__PURE__ */ new Set();
239
+ for (const weight of font.weights) {
240
+ for (const style of font.styles) {
241
+ tuples.add(hasItalic ? `${style === "italic" ? "1" : "0"},${weight}` : `${weight}`);
242
+ }
243
+ }
244
+ return `${baseUrl}?family=${family}:${axes.join(",")}@${[...tuples].sort().join(";")}&display=${font.display}&subset=${font.subsets.join(",")}`;
245
+ }
246
+ async function discoverLocalFiles(family, src, root) {
247
+ const absolute = path.isAbsolute(src) ? src : path.resolve(root, src);
248
+ if (/[*?{]/.test(src)) {
249
+ const files = await glob(src, { cwd: root, absolute: true });
250
+ const supported = files.filter(isSupportedFontFile);
251
+ if (supported.length === 0) {
252
+ throw new Error(`vite-plugin-spiral: Local font "${family}" glob "${src}" matched no supported font files.`);
253
+ }
254
+ return supported.sort();
255
+ }
256
+ if (fsSync.existsSync(absolute) && fsSync.statSync(absolute).isDirectory()) {
257
+ const files = await glob(`**/${supportedGlob}`, { cwd: absolute, absolute: true });
258
+ if (files.length === 0) {
259
+ throw new Error(`vite-plugin-spiral: Local font "${family}" directory "${src}" contains no supported font files.`);
260
+ }
261
+ return files.sort();
262
+ }
263
+ if (fsSync.existsSync(absolute) && fsSync.statSync(absolute).isFile()) {
264
+ return [absolute];
265
+ }
266
+ throw new Error(`vite-plugin-spiral: Local font "${family}" source "${src}" does not exist.`);
267
+ }
268
+ function groupByVariant(files) {
269
+ const groups = /* @__PURE__ */ new Map();
270
+ for (const file of files) {
271
+ const weight = inferWeight(file);
272
+ const style = inferStyle(file);
273
+ const key = `${weight}:${style}`;
274
+ const group = groups.get(key) ?? { weight, style, files: [] };
275
+ group.files.push(resolveFontFile(file));
276
+ groups.set(key, group);
277
+ }
278
+ return [...groups.values()].map((variant) => ({ ...variant, files: sortFiles(variant.files) })).sort((a, b) => Number(a.weight) - Number(b.weight) || a.style.localeCompare(b.style));
279
+ }
280
+ function parseFontFaceCss(css) {
281
+ return [...css.matchAll(/@font-face\s*{([^}]+)}/g)].map((match) => {
282
+ const body = match[1];
283
+ const src = [...body.matchAll(/url\((?:'|")?([^'")]+)(?:'|")?\)\s*format\((?:'|")?([^'")]+)(?:'|")?\)/g)].map((srcMatch) => ({
284
+ url: srcMatch[1],
285
+ format: normalizeFormat(srcMatch[2])
286
+ }));
287
+ return {
288
+ family: property(body, "font-family")?.replace(/^['"]|['"]$/g, "") ?? "",
289
+ style: property(body, "font-style") ?? "normal",
290
+ weight: property(body, "font-weight") ?? 400,
291
+ src,
292
+ unicodeRange: property(body, "unicode-range"),
293
+ display: property(body, "font-display")
294
+ };
295
+ }).filter((face) => face.src.length > 0);
296
+ }
297
+ function generateFontCss(families, fileMap, urlResolver) {
298
+ const rules = [];
299
+ for (const family of families) {
300
+ for (const variant of family.variants) {
301
+ const src = variant.files.map((file) => `url("${urlResolver(fileMap.get(file.source) ?? file.source)}") format("${file.format}")`).join(", ");
302
+ const unicodeRange = variant.files.find((file) => file.unicodeRange)?.unicodeRange;
303
+ rules.push([
304
+ "@font-face {",
305
+ ` font-family: "${family.family}";`,
306
+ ` font-style: ${variant.style};`,
307
+ ` font-weight: ${variant.weight};`,
308
+ ` font-display: ${family.display};`,
309
+ ` src: ${src};`,
310
+ unicodeRange ? ` unicode-range: ${unicodeRange};` : void 0,
311
+ "}"
312
+ ].filter(Boolean).join("\n"));
313
+ }
314
+ if (family.fallbackCss) {
315
+ rules.push(family.fallbackCss.trim());
316
+ }
317
+ }
318
+ return `${rules.join("\n\n")}
319
+ `;
320
+ }
321
+ function buildFontManifest(families, fileMap, cssFile, urlResolver, inlineCss) {
322
+ const manifest = {
323
+ version: 1,
324
+ style: {
325
+ file: cssFile,
326
+ inline: inlineCss,
327
+ familyStyles: {},
328
+ variables: {}
329
+ },
330
+ preloads: [],
331
+ families: {}
332
+ };
333
+ for (const family of families) {
334
+ const familyStack = [`"${family.family}"`];
335
+ if (family.fallbackFamily) {
336
+ familyStack.push(`"${family.fallbackFamily}"`);
337
+ }
338
+ manifest.style.familyStyles[family.alias] = `font-family: var(${family.variable});`;
339
+ manifest.style.variables[family.variable] = [...familyStack, ...family.fallbacks].join(", ");
340
+ if (family.fallbackCss) {
341
+ manifest.style.fallbackCss = [manifest.style.fallbackCss, family.fallbackCss].filter(Boolean).join("\n");
342
+ }
343
+ manifest.families[family.alias] = {
344
+ family: family.family,
345
+ variable: family.variable,
346
+ fallbackFamily: family.fallbackFamily,
347
+ fallbacks: family.fallbacks,
348
+ variants: {}
349
+ };
350
+ for (const variant of family.variants) {
351
+ const key = `${variant.weight}:${variant.style}`;
352
+ manifest.families[family.alias].variants[key] = {
353
+ files: variant.files.map((file) => {
354
+ const resolved = fileMap.get(file.source);
355
+ return {
356
+ file: resolved && !resolved.startsWith("http") ? resolved : void 0,
357
+ url: resolved?.startsWith("http") ? resolved : void 0,
358
+ format: file.format,
359
+ unicodeRange: file.unicodeRange
360
+ };
361
+ })
362
+ };
363
+ for (const file of variant.files) {
364
+ if (!shouldPreload(family, variant, file)) {
365
+ continue;
366
+ }
367
+ const resolved = fileMap.get(file.source);
368
+ manifest.preloads.push({
369
+ alias: family.alias,
370
+ family: family.family,
371
+ weight: variant.weight,
372
+ style: variant.style,
373
+ file: resolved && !resolved.startsWith("http") ? resolved : void 0,
374
+ url: resolved?.startsWith("http") ? urlResolver(resolved) : void 0,
375
+ as: "font",
376
+ type: FORMAT_MIME[file.format],
377
+ crossorigin: "anonymous"
378
+ });
379
+ }
380
+ }
381
+ }
382
+ return manifest;
383
+ }
384
+ function emitFontAssets(families, emitFile) {
385
+ const refs = /* @__PURE__ */ new Map();
386
+ for (const family of families) {
387
+ for (const variant of family.variants) {
388
+ for (const file of variant.files) {
389
+ if (refs.has(file.source)) {
390
+ continue;
391
+ }
392
+ const name = `${familyToSlug(family.family)}-${variant.weight}-${variant.style}.${file.format}`;
393
+ refs.set(file.source, emitFile({ type: "asset", name, source: fsSync.readFileSync(file.source) }));
394
+ }
395
+ }
396
+ }
397
+ return refs;
398
+ }
399
+ function createFontMiddleware() {
400
+ const files = /* @__PURE__ */ new Map();
401
+ const middleware = ((req, res, next) => {
402
+ const pathname = req.url?.split("?")[0] ?? "";
403
+ const file = files.get(pathname);
404
+ if (!file) {
405
+ next();
406
+ return;
407
+ }
408
+ res.statusCode = 200;
409
+ res.setHeader("Content-Type", FORMAT_MIME[file.format]);
410
+ res.end(fsSync.readFileSync(file.source));
411
+ });
412
+ middleware.files = files;
413
+ return middleware;
414
+ }
415
+ function validateFonts(fonts) {
416
+ for (const font of fonts) {
417
+ if (font.family.trim() === "") {
418
+ throw new Error("vite-plugin-spiral: Font family must be a non-empty string.");
419
+ }
420
+ if (font.alias.trim() === "") {
421
+ throw new Error(`vite-plugin-spiral: Font "${font.family}" alias must be a non-empty string.`);
422
+ }
423
+ }
424
+ }
425
+ async function withOptimizedFallbacks(family) {
426
+ if (!family.optimizedFallbacks) {
427
+ return family;
428
+ }
429
+ const fontaine = await loadFontaine();
430
+ const fallbackFamily = fontaine.generateFallbackName(family.family);
431
+ const fallbackCss = [];
432
+ for (const variant of family.variants) {
433
+ const metrics = await readVariantMetrics(fontaine, variant);
434
+ if (!metrics) {
435
+ throw new Error(`vite-plugin-spiral: Could not read font metrics for "${family.family}" ${variant.weight} ${variant.style}.`);
436
+ }
437
+ const fallbackFonts = family.fallbacks.length > 0 ? family.fallbacks : fontaine.resolveCategoryFallbacks({
438
+ fontFamily: family.family,
439
+ fallbacks: {},
440
+ metrics
441
+ });
442
+ let generated = false;
443
+ for (const fallbackFont of [...fallbackFonts].reverse()) {
444
+ const fallbackMetrics = await fontaine.getMetricsForFamily(unquoteFontFamily(fallbackFont));
445
+ if (!fallbackMetrics) {
446
+ continue;
447
+ }
448
+ fallbackCss.push(fontaine.generateFontFace(metrics, {
449
+ name: fallbackFamily,
450
+ font: unquoteFontFamily(fallbackFont),
451
+ metrics: fallbackMetrics,
452
+ "font-weight": String(variant.weight),
453
+ "font-style": variant.style
454
+ }).trim());
455
+ generated = true;
456
+ }
457
+ if (!generated && fallbackFonts.length > 0) {
458
+ fallbackCss.push(fontaine.generateFontFace(metrics, {
459
+ name: fallbackFamily,
460
+ font: unquoteFontFamily(fallbackFonts[0]),
461
+ "font-weight": String(variant.weight),
462
+ "font-style": variant.style
463
+ }).trim());
464
+ }
465
+ }
466
+ return {
467
+ ...family,
468
+ fallbackFamily,
469
+ fallbackCss: `${fallbackCss.join("\n\n")}
470
+ `
471
+ };
472
+ }
473
+ async function loadFontaine() {
474
+ try {
475
+ return await import("fontaine");
476
+ } catch (error) {
477
+ const code = typeof error === "object" && error !== null && "code" in error ? error.code : void 0;
478
+ if (code === "ERR_MODULE_NOT_FOUND" || code === "MODULE_NOT_FOUND") {
479
+ throw new Error('vite-plugin-spiral: optimizedFallbacks requires the optional peer dependency "fontaine". Install it with npm install -D fontaine, or set optimizedFallbacks: false.');
480
+ }
481
+ throw error;
482
+ }
483
+ }
484
+ async function readVariantMetrics(fontaine, variant) {
485
+ for (const file of variant.files) {
486
+ if (!["woff2", "woff", "ttf"].includes(file.format)) {
487
+ continue;
488
+ }
489
+ try {
490
+ const metrics = await fontaine.readMetrics(pathToFileURL(file.source));
491
+ if (metrics) {
492
+ return metrics;
493
+ }
494
+ } catch {
495
+ continue;
496
+ }
497
+ }
498
+ return null;
499
+ }
500
+ function buildResolvedFamily(font, variants) {
501
+ return {
502
+ family: font.family,
503
+ alias: font.alias,
504
+ variable: font.variable,
505
+ display: font.display,
506
+ optimizedFallbacks: font.optimizedFallbacks,
507
+ fallbacks: font.fallbacks,
508
+ preload: font.preload,
509
+ provider: font.provider,
510
+ variants
511
+ };
512
+ }
513
+ function shouldPreload(family, variant, file) {
514
+ if (file.format !== "woff2") {
515
+ return false;
516
+ }
517
+ if (family.preload === true) {
518
+ return true;
519
+ }
520
+ if (family.preload === false) {
521
+ return false;
522
+ }
523
+ return family.preload.some((selector) => selector.weight === variant.weight && (selector.style ?? "normal") === variant.style);
524
+ }
525
+ function resolveFontFile(file) {
526
+ return {
527
+ source: file,
528
+ format: inferFormat(file)
529
+ };
530
+ }
531
+ function isVariableFontFilename(file) {
532
+ const stem = path.basename(file, path.extname(file)).toLowerCase();
533
+ return stem.includes("variable") || stem.includes("-vf") || stem.includes("_vf") || /\[[^\]]+\]/.test(stem);
534
+ }
535
+ function unquoteFontFamily(family) {
536
+ return family.trim().replace(/^['"]|['"]$/g, "");
537
+ }
538
+ function inferFormat(file) {
539
+ const format = fontExtensions[path.extname(file).toLowerCase()];
540
+ if (!format) {
541
+ throw new Error(`vite-plugin-spiral: Unsupported font file "${file}".`);
542
+ }
543
+ return format;
544
+ }
545
+ function isSupportedFontFile(file) {
546
+ return path.extname(file).toLowerCase() in fontExtensions;
547
+ }
548
+ function sortFiles(files) {
549
+ return [...files].sort((a, b) => formatPreference.indexOf(a.format) - formatPreference.indexOf(b.format));
550
+ }
551
+ function inferWeight(file) {
552
+ const stem = path.basename(file, path.extname(file)).toLowerCase();
553
+ const named = [
554
+ ["thin", 100],
555
+ ["extralight", 200],
556
+ ["light", 300],
557
+ ["regular", 400],
558
+ ["normal", 400],
559
+ ["medium", 500],
560
+ ["semibold", 600],
561
+ ["bold", 700],
562
+ ["extrabold", 800],
563
+ ["black", 900]
564
+ ];
565
+ const match = named.find(([name]) => stem.includes(name));
566
+ const numeric = stem.match(/(?:^|[^\d])([1-9]00)(?:[^\d]|$)/);
567
+ return match?.[1] ?? (numeric ? Number.parseInt(numeric[1], 10) : 400);
568
+ }
569
+ function inferStyle(file) {
570
+ const stem = path.basename(file, path.extname(file)).toLowerCase();
571
+ if (stem.includes("italic") || stem.endsWith("it")) {
572
+ return "italic";
573
+ }
574
+ if (stem.includes("oblique")) {
575
+ return "oblique";
576
+ }
577
+ return "normal";
578
+ }
579
+ function normalizeFormat(format) {
580
+ const normalized = format.toLowerCase().replace(/^font\//, "");
581
+ if (normalized in FORMAT_MIME) {
582
+ return normalized;
583
+ }
584
+ throw new Error(`vite-plugin-spiral: Unsupported font format "${format}".`);
585
+ }
586
+ function property(body, propertyName) {
587
+ const match = body.match(new RegExp(`${propertyName}\\s*:\\s*([^;]+)`, "i"));
588
+ return match?.[1]?.trim();
589
+ }
590
+ async function fetchTextAndCache(url, cacheDir, headers) {
591
+ const file = cachePath(url, cacheDir, ".css");
592
+ if (fsSync.existsSync(file)) {
593
+ return fsSync.readFileSync(file, "utf8");
594
+ }
595
+ const response = await fetch(url, { headers });
596
+ if (!response.ok) {
597
+ throw new Error(`vite-plugin-spiral: Failed to fetch font CSS from "${url}" (${response.status}).`);
598
+ }
599
+ const text = await response.text();
600
+ await fs.mkdir(path.dirname(file), { recursive: true });
601
+ await fs.writeFile(file, text, "utf8");
602
+ return text;
603
+ }
604
+ async function fetchFileAndCache(url, cacheDir) {
605
+ const extension = path.extname(new URL(url).pathname) || ".woff2";
606
+ const file = cachePath(url, cacheDir, extension);
607
+ if (fsSync.existsSync(file)) {
608
+ return file;
609
+ }
610
+ const response = await fetch(url);
611
+ if (!response.ok) {
612
+ throw new Error(`vite-plugin-spiral: Failed to fetch font file from "${url}" (${response.status}).`);
613
+ }
614
+ await fs.mkdir(path.dirname(file), { recursive: true });
615
+ await fs.writeFile(file, Buffer.from(await response.arrayBuffer()));
616
+ return file;
617
+ }
618
+ function cachePath(url, cacheDir, extension) {
619
+ return path.resolve(cacheDir, `${createHash("sha1").update(url).digest("hex")}${extension}`);
620
+ }
621
+ function trimSlashes(value) {
622
+ return value.replace(/^\/+|\/+$/g, "");
623
+ }
624
+ function normalizePublicBase(base, buildDirectory) {
625
+ const resolved = base === "" ? `/${trimSlashes(buildDirectory)}/` : base;
626
+ return resolved.endsWith("/") ? resolved : `${resolved}/`;
627
+ }
628
+
629
+ // src/index.ts
630
+ var defaultRefreshPaths = [
631
+ "app/views/**/*.php",
632
+ "app/config/**/*.php",
633
+ "app/locale/**/*.php",
634
+ "app/src/Endpoint/**/*.php"
635
+ ];
636
+ var imageExtensions = /* @__PURE__ */ new Set([".png", ".jpg", ".jpeg", ".gif", ".webp", ".avif", ".svg", ".ico"]);
637
+ var fontExtensions2 = /* @__PURE__ */ new Set([".woff", ".woff2", ".ttf", ".otf", ".eot"]);
638
+ var placeholderOrigin = "http://__spiral_vite_placeholder__.test";
639
+ var exitHandlersBound = false;
640
+ function spiral(config) {
641
+ const resolved = resolveOptions(config);
642
+ return [
643
+ resolveSpiralPlugin(resolved),
644
+ ...resolveAssetPlugins(resolved),
645
+ ...resolveFontsPlugin(resolved.fonts, resolved.hotFile, resolved.buildDirectory),
646
+ ...resolveRefreshPlugins(resolved.refresh)
647
+ ];
648
+ }
649
+ function resolveSpiralPlugin(resolved) {
650
+ let viteConfig;
651
+ let userConfig = {};
652
+ let devServerUrl;
653
+ return {
654
+ name: "vite-plugin-spiral",
655
+ config(config, env) {
656
+ userConfig = config;
657
+ const buildConfig = config.build;
658
+ const modeEnv = loadEnv(env.mode, config.envDir || process.cwd(), "");
659
+ const assetUrl = resolved.assetUrl ?? modeEnv.SPIRAL_VITE_ASSET_URL ?? modeEnv.ASSET_URL ?? "";
660
+ const appUrl = modeEnv.SPIRAL_APP_URL ?? modeEnv.APP_URL;
661
+ const isSsrBuild = env.isSsrBuild === true;
662
+ const output = resolved.output;
663
+ const bundlerInput = resolveBuildInput(buildConfig, isSsrBuild ? resolved.ssr : resolved.input);
664
+ const serverConfig = env.command === "serve" ? resolveDevelopmentEnvironmentServerConfig(resolved.detectTls) ?? resolveEnvironmentServerConfig(modeEnv) : void 0;
665
+ ensureCommandShouldRunInEnvironment(env.command, modeEnv);
666
+ return {
667
+ base: config.base ?? (env.command === "build" ? resolveBase(resolved.buildDirectory, assetUrl) : ""),
668
+ publicDir: config.publicDir ?? false,
669
+ build: {
670
+ manifest: config.build?.manifest ?? (isSsrBuild ? false : "manifest.json"),
671
+ ssrManifest: config.build?.ssrManifest ?? (isSsrBuild ? "ssr-manifest.json" : false),
672
+ outDir: config.build?.outDir ?? (isSsrBuild ? resolved.ssrOutputDirectory : posixJoin(resolved.publicDirectory, resolved.buildDirectory)),
673
+ assetsInlineLimit: config.build?.assetsInlineLimit ?? 0,
674
+ rolldownOptions: {
675
+ ...buildConfig?.rolldownOptions,
676
+ input: buildConfig?.rolldownOptions?.input ?? bundlerInput,
677
+ output: resolveBundlerOutput(buildConfig?.rolldownOptions?.output ?? buildConfig?.rollupOptions?.output, output)
678
+ }
679
+ },
680
+ server: {
681
+ ...config.server,
682
+ origin: resolved.server?.origin ?? config.server?.origin ?? placeholderOrigin,
683
+ cors: config.server?.cors ?? {
684
+ origin: resolved.server?.corsOrigin ?? resolveAllowedOrigins(modeEnv, appUrl)
685
+ },
686
+ ...modeEnv.SPIRAL_VITE_PORT || modeEnv.VITE_PORT ? { port: config.server?.port ?? Number.parseInt(modeEnv.SPIRAL_VITE_PORT ?? modeEnv.VITE_PORT, 10), strictPort: config.server?.strictPort ?? true } : {},
687
+ ...serverConfig ? {
688
+ host: config.server?.host ?? serverConfig.host,
689
+ hmr: config.server?.hmr === false ? false : {
690
+ ...serverConfig.hmr,
691
+ ...config.server?.hmr === true ? {} : config.server?.hmr
692
+ },
693
+ https: config.server?.https ?? serverConfig.https
694
+ } : {}
695
+ },
696
+ resolve: {
697
+ ...config.resolve,
698
+ alias: resolveAliases(config)
699
+ }
700
+ };
701
+ },
702
+ configResolved(config) {
703
+ viteConfig = config;
704
+ },
705
+ transform(code) {
706
+ if (viteConfig?.command !== "serve" || devServerUrl === void 0) {
707
+ return;
708
+ }
709
+ return resolved.transformOnServe(code.replaceAll(placeholderOrigin, devServerUrl), devServerUrl);
710
+ },
711
+ configureServer(server) {
712
+ const cleanHotFile = () => {
713
+ fsSync2.rmSync(resolveRootPath(server.config.root, resolved.hotFile), { force: true });
714
+ };
715
+ const writeHotFile = () => {
716
+ const address = server.httpServer?.address();
717
+ if (typeof address === "object" && address !== null) {
718
+ devServerUrl = resolved.server?.origin ? stripTrailingSlash(resolved.server.origin) : resolveDevServerUrl(address, server.config, userConfig);
719
+ } else {
720
+ devServerUrl = resolved.server?.origin ? stripTrailingSlash(resolved.server.origin) : stripTrailingSlash(server.resolvedUrls?.local[0] ?? "http://localhost:5173");
721
+ }
722
+ const hotFile = resolveRootPath(server.config.root, resolved.hotFile);
723
+ fsSync2.mkdirSync(path2.dirname(hotFile), { recursive: true });
724
+ fsSync2.writeFileSync(hotFile, `${devServerUrl}${server.config.base.replace(/\/$/, "")}`, "utf8");
725
+ logDevServer(server, resolved, devServerUrl);
726
+ };
727
+ if (server.httpServer) {
728
+ server.httpServer.once("listening", writeHotFile);
729
+ server.httpServer.once("close", cleanHotFile);
730
+ } else {
731
+ writeHotFile();
732
+ }
733
+ if (!exitHandlersBound) {
734
+ const clean = () => {
735
+ if (viteConfig) {
736
+ fsSync2.rmSync(resolveRootPath(viteConfig.root, resolved.hotFile), { force: true });
737
+ }
738
+ };
739
+ process.on("exit", clean);
740
+ process.on("SIGINT", () => process.exit());
741
+ process.on("SIGTERM", () => process.exit());
742
+ process.on("SIGHUP", () => process.exit());
743
+ exitHandlersBound = true;
744
+ }
745
+ return () => {
746
+ server.middlewares.use((req, res, next) => {
747
+ if (req.url !== "/index.html" && req.url !== "/") {
748
+ next();
749
+ return;
750
+ }
751
+ const env = loadEnv(server.config.mode, server.config.envDir || process.cwd(), "");
752
+ const appUrl = env.SPIRAL_APP_URL ?? env.APP_URL ?? "http://127.0.0.1:8080";
753
+ res.statusCode = 404;
754
+ res.setHeader("Content-Type", "text/html; charset=utf-8");
755
+ res.end(devServerIndexHtml(appUrl));
756
+ });
757
+ };
758
+ },
759
+ async closeBundle() {
760
+ if (viteConfig === void 0 || viteConfig.command !== "build") {
761
+ return;
762
+ }
763
+ await fs2.rm(resolveRootPath(viteConfig.root, resolved.hotFile), { force: true });
764
+ if (resolved.integrity !== false) {
765
+ await writeIntegrityManifest(viteConfig, resolved);
766
+ }
767
+ }
768
+ };
769
+ }
770
+ function resolveOptions(config) {
771
+ const options = typeof config === "string" || Array.isArray(config) ? { input: config } : Object.prototype.hasOwnProperty.call(config, "input") ? config : { input: config };
772
+ if (options.input === void 0 || Array.isArray(options.input) && options.input.length === 0) {
773
+ throw new Error("vite-plugin-spiral requires at least one input file.");
774
+ }
775
+ return {
776
+ input: options.input,
777
+ publicDirectory: trimSlashes2(options.publicDirectory ?? "public", "publicDirectory"),
778
+ buildDirectory: trimSlashes2(options.buildDirectory ?? "build", "buildDirectory"),
779
+ hotFile: normalizePath(options.hotFile ?? "runtime/vite.hot"),
780
+ refresh: resolveRefreshConfig(options.refresh),
781
+ ssr: options.ssr ?? options.input,
782
+ ssrOutputDirectory: trimSlashes2(options.ssrOutputDirectory ?? "runtime/vite-ssr", "ssrOutputDirectory"),
783
+ integrity: options.integrity === true ? "sha384" : options.integrity ?? false,
784
+ assetUrl: options.assetUrl,
785
+ detectTls: options.detectTls ?? options.valetTls ?? null,
786
+ assets: typeof options.assets === "string" ? [options.assets] : options.assets ?? [],
787
+ fonts: options.fonts ?? [],
788
+ transformOnServe: options.transformOnServe ?? ((code) => code),
789
+ server: options.server,
790
+ output: {
791
+ jsDirectory: trimSlashes2(options.output?.jsDirectory ?? "js", "output.jsDirectory"),
792
+ cssDirectory: trimSlashes2(options.output?.cssDirectory ?? "css", "output.cssDirectory"),
793
+ imageDirectory: trimSlashes2(options.output?.imageDirectory ?? "images", "output.imageDirectory"),
794
+ fontDirectory: trimSlashes2(options.output?.fontDirectory ?? "fonts", "output.fontDirectory"),
795
+ assetDirectory: trimSlashes2(options.output?.assetDirectory ?? "assets", "output.assetDirectory")
796
+ }
797
+ };
798
+ }
799
+ function resolveBuildInput(buildConfig, defaultInput) {
800
+ return buildConfig?.rolldownOptions?.input ?? buildConfig?.rollupOptions?.input ?? defaultInput;
801
+ }
802
+ function resolveBundlerOutput(userOutput, output) {
803
+ const spiralOutput = {
804
+ entryFileNames: `${output.jsDirectory}/[name]-[hash].js`,
805
+ chunkFileNames: `${output.jsDirectory}/chunks/[name]-[hash].js`,
806
+ assetFileNames: (assetInfo) => assetFileName(assetInfo.name, output)
807
+ };
808
+ if (Array.isArray(userOutput)) {
809
+ return userOutput;
810
+ }
811
+ if (typeof userOutput === "object" && userOutput !== null) {
812
+ return { ...spiralOutput, ...userOutput };
813
+ }
814
+ return spiralOutput;
815
+ }
816
+ function resolveAssetPlugins(options) {
817
+ if (options.assets.length === 0) {
818
+ return [];
819
+ }
820
+ let root = process.cwd();
821
+ return [{
822
+ name: "vite-plugin-spiral:assets",
823
+ apply: "build",
824
+ configResolved(config) {
825
+ root = config.root;
826
+ },
827
+ async buildStart() {
828
+ const files = await glob2(options.assets, {
829
+ cwd: root,
830
+ absolute: true
831
+ });
832
+ for (const file of files.sort()) {
833
+ const stats = await fs2.stat(file);
834
+ if (!stats.isFile()) {
835
+ continue;
836
+ }
837
+ this.emitFile({
838
+ type: "asset",
839
+ name: path2.basename(file),
840
+ originalFileName: normalizePath(path2.relative(root, file)),
841
+ source: await fs2.readFile(file)
842
+ });
843
+ }
844
+ }
845
+ }];
846
+ }
847
+ function resolveRefreshPlugins(refresh) {
848
+ return refresh.flatMap((config) => fullReload(config.paths, config.config));
849
+ }
850
+ function resolveRefreshConfig(refresh) {
851
+ if (refresh === false) {
852
+ return [];
853
+ }
854
+ if (refresh === void 0 || refresh === true) {
855
+ return [{ paths: defaultRefreshPaths }];
856
+ }
857
+ if (typeof refresh === "string") {
858
+ return [{ paths: [normalizePath(refresh)] }];
859
+ }
860
+ if (Array.isArray(refresh)) {
861
+ if (refresh.every((item) => typeof item === "string")) {
862
+ return [{ paths: refresh.map((item) => normalizePath(item)) }];
863
+ }
864
+ return refresh.map((item) => ({
865
+ ...item,
866
+ paths: item.paths.map(normalizePath)
867
+ }));
868
+ }
869
+ return [{ ...refresh, paths: refresh.paths.map(normalizePath) }];
870
+ }
871
+ function resolveBase(buildDirectory, assetUrl) {
872
+ const normalizedBuildDirectory = trimSlashes2(buildDirectory, "buildDirectory");
873
+ if (assetUrl === "") {
874
+ return `/${normalizedBuildDirectory}/`;
875
+ }
876
+ return `${stripTrailingSlash(assetUrl)}/${normalizedBuildDirectory}/`;
877
+ }
878
+ function resolveAliases(config) {
879
+ const defaults = {
880
+ "@": path2.resolve(config.root ?? process.cwd(), "app/resources/js"),
881
+ "@resources": path2.resolve(config.root ?? process.cwd(), "app/resources")
882
+ };
883
+ if (Array.isArray(config.resolve?.alias)) {
884
+ return [
885
+ ...config.resolve.alias,
886
+ ...Object.entries(defaults).map(([find, replacement]) => ({ find, replacement }))
887
+ ];
888
+ }
889
+ return {
890
+ ...defaults,
891
+ ...config.resolve?.alias
892
+ };
893
+ }
894
+ function resolveAllowedOrigins(env, appUrl) {
895
+ const configuredOrigins = (env.SPIRAL_VITE_ALLOWED_ORIGINS ?? "").split(",").map((origin) => origin.trim()).filter(Boolean);
896
+ return [
897
+ /^https?:\/\/localhost(:\d+)?$/,
898
+ /^https?:\/\/127\.0\.0\.1(:\d+)?$/,
899
+ /^https?:\/\/\[::1\](:\d+)?$/,
900
+ /^https?:\/\/.*\.test(:\d+)?$/,
901
+ ...appUrl ? [appUrl] : [],
902
+ ...configuredOrigins
903
+ ];
904
+ }
905
+ function resolveEnvironmentServerConfig(env) {
906
+ const key = env.SPIRAL_VITE_DEV_SERVER_KEY;
907
+ const cert = env.SPIRAL_VITE_DEV_SERVER_CERT;
908
+ if (!key && !cert) {
909
+ return;
910
+ }
911
+ if (!key || !cert || !fsSync2.existsSync(key) || !fsSync2.existsSync(cert)) {
912
+ throw new Error(`vite-plugin-spiral could not find certificate files from SPIRAL_VITE_DEV_SERVER_KEY [${key ?? ""}] and SPIRAL_VITE_DEV_SERVER_CERT [${cert ?? ""}].`);
913
+ }
914
+ const host = resolveHostFromEnv(env);
915
+ if (!host) {
916
+ throw new Error(`vite-plugin-spiral could not determine the dev server host from SPIRAL_APP_URL or APP_URL.`);
917
+ }
918
+ return {
919
+ host,
920
+ hmr: { host },
921
+ https: {
922
+ key: fsSync2.readFileSync(key),
923
+ cert: fsSync2.readFileSync(cert)
924
+ }
925
+ };
926
+ }
927
+ function resolveDevelopmentEnvironmentServerConfig(host) {
928
+ if (host === false) {
929
+ return;
930
+ }
931
+ const configPath = determineDevelopmentEnvironmentConfigPath();
932
+ if (configPath === void 0 && host === null) {
933
+ return;
934
+ }
935
+ if (configPath === void 0) {
936
+ throw new Error("vite-plugin-spiral could not find a Herd or Valet configuration directory.");
937
+ }
938
+ const resolvedHost = host === true || host === null ? `${path2.basename(process.cwd())}.${resolveDevelopmentEnvironmentTld(configPath)}` : host;
939
+ const key = path2.resolve(configPath, "Certificates", `${resolvedHost}.key`);
940
+ const cert = path2.resolve(configPath, "Certificates", `${resolvedHost}.crt`);
941
+ if (!fsSync2.existsSync(key) || !fsSync2.existsSync(cert)) {
942
+ if (host === null) {
943
+ return;
944
+ }
945
+ throw new Error(`vite-plugin-spiral could not find TLS certificate files for [${resolvedHost}] in [${configPath}/Certificates].`);
946
+ }
947
+ return {
948
+ host: resolvedHost,
949
+ hmr: { host: resolvedHost },
950
+ https: { key, cert }
951
+ };
952
+ }
953
+ function resolveHostFromEnv(env) {
954
+ const appUrl = env.SPIRAL_APP_URL ?? env.APP_URL;
955
+ try {
956
+ return appUrl ? new URL(appUrl).host : void 0;
957
+ } catch {
958
+ return void 0;
959
+ }
960
+ }
961
+ function ensureCommandShouldRunInEnvironment(command, env) {
962
+ if (command === "build" || env.SPIRAL_VITE_BYPASS_ENV_CHECK === "1") {
963
+ return;
964
+ }
965
+ if (env.CI !== void 0) {
966
+ throw new Error("vite-plugin-spiral refuses to run the Vite dev server in CI. Run npm run build instead or set SPIRAL_VITE_BYPASS_ENV_CHECK=1.");
967
+ }
968
+ if (env.APP_ENV === "production" || env.SPIRAL_ENV === "production") {
969
+ throw new Error("vite-plugin-spiral refuses to run the Vite dev server in production. Run npm run build instead or set SPIRAL_VITE_BYPASS_ENV_CHECK=1.");
970
+ }
971
+ }
972
+ function assetFileName(assetName, output) {
973
+ const extension = path2.extname(assetName ?? "").toLowerCase();
974
+ if (extension === ".css") {
975
+ return `${output.cssDirectory}/[name]-[hash][extname]`;
976
+ }
977
+ if (imageExtensions.has(extension)) {
978
+ return `${output.imageDirectory}/[name]-[hash][extname]`;
979
+ }
980
+ if (fontExtensions2.has(extension)) {
981
+ return `${output.fontDirectory}/[name]-[hash][extname]`;
982
+ }
983
+ return `${output.assetDirectory}/[name]-[hash][extname]`;
984
+ }
985
+ async function writeIntegrityManifest(config, options) {
986
+ if (options.integrity === false) {
987
+ return;
988
+ }
989
+ const outDir = path2.resolve(config.root, config.build.outDir);
990
+ const manifestPath = path2.join(outDir, "manifest.json");
991
+ const algorithm = options.integrity;
992
+ let manifest;
993
+ try {
994
+ manifest = JSON.parse(await fs2.readFile(manifestPath, "utf8"));
995
+ } catch {
996
+ return;
997
+ }
998
+ for (const chunk of Object.values(manifest)) {
999
+ if (typeof chunk.file === "string") {
1000
+ chunk.integrity = await integrityForFile(path2.join(outDir, chunk.file), algorithm);
1001
+ }
1002
+ if (Array.isArray(chunk.css) && chunk.css.length > 0) {
1003
+ chunk.cssIntegrity = {};
1004
+ for (const cssFile of chunk.css) {
1005
+ chunk.cssIntegrity[cssFile] = await integrityForFile(path2.join(outDir, cssFile), algorithm);
1006
+ }
1007
+ }
1008
+ }
1009
+ await fs2.writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}
1010
+ `, "utf8");
1011
+ }
1012
+ async function integrityForFile(file, algorithm) {
1013
+ const contents = await fs2.readFile(file);
1014
+ return `${algorithm}-${createHash2(algorithm).update(contents).digest("base64")}`;
1015
+ }
1016
+ function resolveDevServerUrl(address, config, rawConfig) {
1017
+ const hmr = typeof config.server.hmr === "object" ? config.server.hmr : void 0;
1018
+ const protocol = hmr?.protocol ? hmr.protocol === "wss" ? "https" : "http" : config.server.https ? "https" : "http";
1019
+ const host = hmr?.host ?? (typeof config.server.host === "string" && config.server.host !== "0.0.0.0" && config.server.host !== "::" ? config.server.host : void 0) ?? (rawConfig.server?.host === "0.0.0.0" ? "localhost" : void 0) ?? (isIpv6(address) ? `[${address.address}]` : address.address);
1020
+ const port = hmr?.clientPort ?? address.port;
1021
+ return `${protocol}://${host}:${port}`;
1022
+ }
1023
+ function isIpv6(address) {
1024
+ return address.family === "IPv6" || address.family === 6;
1025
+ }
1026
+ function logDevServer(server, options, devServerUrl) {
1027
+ const env = loadEnv(server.config.mode, server.config.envDir || process.cwd(), "");
1028
+ const appUrl = env.SPIRAL_APP_URL ?? env.APP_URL ?? "undefined";
1029
+ setTimeout(() => {
1030
+ server.config.logger.info(`
1031
+ ${colors.cyan(`${colors.bold("SPIRAL")} Vite`)} ${colors.dim("plugin")} ${colors.bold(`v${pluginVersion()}`)}`);
1032
+ server.config.logger.info(` ${colors.green("\u279C")} APP_URL: ${colors.cyan(appUrl)}`);
1033
+ server.config.logger.info(` ${colors.green("\u279C")} VITE_URL: ${colors.cyan(devServerUrl)}`);
1034
+ server.config.logger.info(` ${colors.green("\u279C")} HOT_FILE: ${colors.dim(options.hotFile)}`);
1035
+ if (server.config.server.https) {
1036
+ server.config.logger.info(` ${colors.green("\u279C")} TLS: ${colors.cyan("enabled")}`);
1037
+ }
1038
+ }, 100);
1039
+ }
1040
+ function pluginVersion() {
1041
+ try {
1042
+ const packageJson = JSON.parse(fsSync2.readFileSync(path2.resolve(import.meta.dirname, "../package.json"), "utf8"));
1043
+ return packageJson.version ?? "";
1044
+ } catch {
1045
+ return "";
1046
+ }
1047
+ }
1048
+ function determineDevelopmentEnvironmentConfigPath() {
1049
+ const paths = [
1050
+ path2.resolve(os.homedir(), "Library", "Application Support", "Herd", "config", "valet"),
1051
+ path2.resolve(os.homedir(), ".config", "herd", "config", "valet"),
1052
+ path2.resolve(os.homedir(), ".config", "valet"),
1053
+ path2.resolve(os.homedir(), ".valet")
1054
+ ];
1055
+ return paths.find((configPath) => fsSync2.existsSync(configPath));
1056
+ }
1057
+ function resolveDevelopmentEnvironmentTld(configPath) {
1058
+ const configFile = path2.resolve(configPath, "config.json");
1059
+ if (!fsSync2.existsSync(configFile)) {
1060
+ throw new Error(`vite-plugin-spiral could not find Herd or Valet config file [${configFile}].`);
1061
+ }
1062
+ const config = JSON.parse(fsSync2.readFileSync(configFile, "utf8"));
1063
+ if (!config.tld) {
1064
+ throw new Error(`vite-plugin-spiral could not determine the Herd or Valet TLD from [${configFile}].`);
1065
+ }
1066
+ return config.tld;
1067
+ }
1068
+ function devServerIndexHtml(appUrl) {
1069
+ return `<!doctype html>
1070
+ <html lang="en">
1071
+ <head>
1072
+ <meta charset="utf-8">
1073
+ <meta name="viewport" content="width=device-width, initial-scale=1">
1074
+ <title>Spiral Vite</title>
1075
+ <style>body{font-family:system-ui,-apple-system,Segoe UI,sans-serif;margin:0;min-height:100vh;display:grid;place-items:center;background:#0f172a;color:#e2e8f0}main{max-width:42rem;padding:2rem}a{color:#67e8f9}</style>
1076
+ </head>
1077
+ <body>
1078
+ <main>
1079
+ <h1>Spiral Vite dev server</h1>
1080
+ <p>This server provides Vite assets and hot module replacement for a Spiral application.</p>
1081
+ <p>Open the application at <a href="${escapeHtml(appUrl)}">${escapeHtml(appUrl)}</a>.</p>
1082
+ </main>
1083
+ </body>
1084
+ </html>`;
1085
+ }
1086
+ function resolveRootPath(root, file) {
1087
+ return path2.resolve(root, file);
1088
+ }
1089
+ function posixJoin(...segments) {
1090
+ return segments.map((segment) => trimSlashes2(segment, "path segment")).filter(Boolean).join("/");
1091
+ }
1092
+ function trimSlashes2(value, name) {
1093
+ const normalized = normalizePath(value).replace(/^\/+|\/+$/g, "");
1094
+ if (normalized === "") {
1095
+ throw new Error(`vite-plugin-spiral: ${name} must be a non-empty relative path.`);
1096
+ }
1097
+ return normalized;
1098
+ }
1099
+ function normalizePath(value) {
1100
+ return value.replace(/\\/g, "/");
1101
+ }
1102
+ function stripTrailingSlash(value) {
1103
+ return value.replace(/\/+$/g, "");
1104
+ }
1105
+ function escapeHtml(value) {
1106
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
1107
+ }
1108
+ export {
1109
+ spiral as default,
1110
+ spiral
1111
+ };