spark-html-bun 0.1.1 → 0.1.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.
Files changed (2) hide show
  1. package/package.json +2 -2
  2. package/src/index.js +23 -9
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "spark-html-bun",
3
- "version": "0.1.1",
4
- "description": "Dev server, build, and preview for spark-html apps built entirely on Bun. Scoped component HMR over a plain WebSocket, import-map dev serving (no bundling in dev), Bun.build for the app shell, and an explicit post-build pipeline (prerender, image, font, manifest, offline, sri). Zero dependencies.",
3
+ "version": "0.1.2",
4
+ "description": "Dev server, build, and preview for spark-html apps \u2014 built entirely on Bun. Scoped component HMR over a plain WebSocket, import-map dev serving (no bundling in dev), Bun.build for the app shell, and an explicit post-build pipeline (prerender, image, font, manifest, offline, sri). Zero dependencies.",
5
5
  "homepage": "https://wilkinnovo.github.io/spark",
6
6
  "type": "module",
7
7
  "main": "./src/index.js",
package/src/index.js CHANGED
@@ -60,6 +60,13 @@ function decodePath(pathname) {
60
60
  try { return decodeURIComponent(pathname); } catch { return null; }
61
61
  }
62
62
 
63
+ // One stat instead of existsSync + statSync — this runs per request candidate
64
+ // on the dev/preview hot path.
65
+ function isFile(p) {
66
+ const s = statSync(p, { throwIfNoEntry: false });
67
+ return s !== undefined && s.isFile();
68
+ }
69
+
63
70
  const MIME = {
64
71
  '.html': 'text/html', '.js': 'text/javascript', '.mjs': 'text/javascript',
65
72
  '.css': 'text/css', '.json': 'application/json', '.svg': 'image/svg+xml',
@@ -253,7 +260,7 @@ export async function dev(overrides = {}) {
253
260
  let file = null;
254
261
  for (const b of [projectRoot, pub]) {
255
262
  const c = safeJoin(b, rel);
256
- if (c && existsSync(c) && statSync(c).isFile()) { file = c; break; }
263
+ if (c && isFile(c)) { file = c; break; }
257
264
  }
258
265
 
259
266
  // SPA fallback: extensionless paths serve the app shell (the router
@@ -343,12 +350,17 @@ export async function build(overrides = {}) {
343
350
  const file = clean.startsWith('/') ? join(projectRoot, clean.slice(1)) : join(entryDir, clean);
344
351
  // Only bundle files that live in the PROJECT (src/…) — anything served
345
352
  // from publicDir ships verbatim and its URL already works.
346
- if (existsSync(file) && !file.startsWith(pub + '/')) found.push({ url, file });
353
+ if (existsSync(file) && !file.startsWith(pub + sep)) found.push({ url, file });
347
354
  }
348
355
 
349
356
  if (found.length) {
357
+ // Bun.build DEDUPES duplicate entrypoints (the same file listed twice —
358
+ // or reached via two URL spellings — yields ONE output), so bundle the
359
+ // UNIQUE files and map file → hashed name; never map outputs back by
360
+ // found-index, which would splice the wrong asset URL into the page.
361
+ const files = [...new Set(found.map((f) => f.file))];
350
362
  const result = await Bun.build({
351
- entrypoints: found.map((f) => f.file),
363
+ entrypoints: files,
352
364
  outdir: join(outDir, 'assets'),
353
365
  minify: true,
354
366
  splitting: true,
@@ -362,17 +374,19 @@ export async function build(overrides = {}) {
362
374
  throw new Error(`[spark] build failed:\n${msgs}`);
363
375
  }
364
376
  // Entry outputs come back in entrypoint order (verified for Bun's
365
- // splitting output, incl. same-basename entries) — map each to its URL.
377
+ // splitting output, incl. same-basename entries) — map each unique
378
+ // file to its hashed name, then rewrite every tag that referenced it.
366
379
  const entryOuts = result.outputs.filter((o) => o.kind === 'entry-point');
367
- found.forEach((f, i) => {
368
- const name = entryOuts[i] && basename(entryOuts[i].path);
369
- if (!name) return;
380
+ const outName = new Map(files.map((file, i) => [file, entryOuts[i] && basename(entryOuts[i].path)]));
381
+ for (const f of found) {
382
+ const name = outName.get(f.file);
383
+ if (!name) continue;
370
384
  const to = `${base}assets/${name}`;
371
385
  // Replace only the quoted attribute value, so a longer URL that ends
372
386
  // with this one (e.g. /lib/app.js vs /app.js) is never corrupted the
373
387
  // way a bare replaceAll(url) would corrupt it.
374
388
  for (const q of ['"', "'"]) html = html.split(`${q}${f.url}${q}`).join(`${q}${to}${q}`);
375
- });
389
+ }
376
390
  }
377
391
  await Bun.write(join(outDir, config.entry.split('/').pop()), html);
378
392
  }
@@ -411,7 +425,7 @@ export async function preview(overrides = {}) {
411
425
  rel !== '' && rel.endsWith('/') ? safeJoin(outDir, join(rel, 'index.html')) : null,
412
426
  ].filter(Boolean);
413
427
  for (const f of tryFiles) {
414
- if (existsSync(f) && statSync(f).isFile()) {
428
+ if (isFile(f)) {
415
429
  return new Response(Bun.file(f), { headers: { 'Content-Type': mime(f) } });
416
430
  }
417
431
  }