bimba-cli 0.7.9 → 0.7.11

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 (4) hide show
  1. package/README.md +3 -3
  2. package/index.js +28 -16
  3. package/package.json +2 -2
  4. package/serve.js +188 -168
package/README.md CHANGED
@@ -74,7 +74,7 @@ For a deep dive into how Imba compiles tags, how the render cache works, and how
74
74
 
75
75
  Static files are resolved relative to the HTML file's directory first, then from the project root (for `node_modules`, `src`, etc.). Extensionless imports (common in `node_modules`) are resolved by trying `.js` and `.mjs` extensions automatically.
76
76
 
77
- **npm package resolution:** The import map is built by reading each dependency's `package.json`. The resolution order is: `exports["."].import` `exports["."].default` `module` → `browser` `main`. Packages with subpath exports (e.g. `"./styles.css"`) get individual import map entries. CJS-only packages (no ESM entry) are proxied through esm.sh.
77
+ **npm package resolution:** The dev server maps package imports to `__bimba_vendor__/*` URLs and bundles those modules on demand with Bun (`target: "browser"`). Imba source files still compile separately for HMR, while Bun owns dependency resolution, `exports`, `browser` fields, nested `node_modules`, and CommonJS interop.
78
78
 
79
79
  ---
80
80
 
@@ -82,7 +82,7 @@ Static files are resolved relative to the HTML file's directory first, then from
82
82
 
83
83
  To compile and bundle your source code from .imba to .js:
84
84
  ```bash
85
- bunx bimba src/index.imba --outdir public/js --minify
85
+ bunx bimba src/index.imba --outdir public/js
86
86
  ```
87
87
 
88
88
  With watch:
@@ -100,7 +100,7 @@ bunx bimba src/index.imba --outdir public/js --watch --clearcache
100
100
 
101
101
  `--clearcache` — delete the cache directory on exit (Ctrl+C). Works only in watch mode.
102
102
 
103
- `--minify` — minify the output JS. Enabled by default in bundle mode.
103
+ `--no-minify` — disable minification. Bundle mode minifies by default.
104
104
 
105
105
  `--sourcemap <inline|external|none>` — how to include source maps in the output (default: `none`).
106
106
 
package/index.js CHANGED
@@ -14,7 +14,7 @@ let entrypoint = ''
14
14
 
15
15
  try {
16
16
  const { values, positionals } = parseArgs({
17
- args: Bun.argv,
17
+ args: Bun.argv.slice(2),
18
18
  options: {
19
19
  watch: { type: 'boolean' },
20
20
  outdir: { type: 'string' },
@@ -29,11 +29,12 @@ try {
29
29
  port: { type: 'string' },
30
30
  html: { type: 'string' },
31
31
  },
32
+ allowNegative: true,
32
33
  strict: true,
33
34
  allowPositionals: true,
34
35
  });
35
36
  flags = values;
36
- entrypoint = Bun.argv[2];
37
+ entrypoint = positionals[0] || '';
37
38
  }
38
39
  catch (error) {
39
40
  if (error instanceof Error)
@@ -43,16 +44,21 @@ catch (error) {
43
44
  process.exit(0);
44
45
  }
45
46
 
46
- // Ensure bunfig.toml exists and contains the required preload line
47
- const bunfigPath = path.join(process.cwd(), 'bunfig.toml');
48
- const preloadLine = 'preload = ["bimba-cli/plugin.js"]';
49
- if (!fs.existsSync(bunfigPath)) {
50
- fs.writeFileSync(bunfigPath, preloadLine + '\n');
51
- } else {
52
- const content = fs.readFileSync(bunfigPath, 'utf8');
53
- if (!content.includes(preloadLine)) {
54
- fs.appendFileSync(bunfigPath, (content.endsWith('\n') ? '' : '\n') + preloadLine + '\n');
47
+ function ensureBunfigPreload() {
48
+ const bunfigPath = path.join(process.cwd(), 'bunfig.toml');
49
+ const preloadLine = 'preload = ["bimba-cli/plugin.js"]';
50
+
51
+ if (!fs.existsSync(bunfigPath)) {
52
+ console.log(theme.action("note: ") + theme.filename("bunfig.toml was not found, so bimba left it unchanged."));
53
+ console.log(theme.action(" ") + `Add ${theme.flags(preloadLine)} manually if you want Bun to preload the plugin.`);
54
+ return;
55
55
  }
56
+
57
+ const content = fs.readFileSync(bunfigPath, 'utf8');
58
+ if (content.includes(preloadLine)) return;
59
+
60
+ console.log(theme.action("note: ") + theme.filename("bunfig.toml already exists, so bimba did not edit it automatically."));
61
+ console.log(theme.action(" ") + `Add ${theme.flags(preloadLine)} manually if you want Bun to preload the plugin.`);
56
62
  }
57
63
 
58
64
  // help: more on bun building params here: https://bun.sh/docs/bundler
@@ -62,7 +68,7 @@ if(flags.help) {
62
68
  console.log("For example like this: "+theme.filedir('bimba file.imba --outdir public'));
63
69
  console.log("");
64
70
  console.log(" "+theme.flags('--outdir <folder>')+" Compile imba files to the specified folder");
65
- console.log(" "+theme.flags('--minify')+" Minify compiled .js files");
71
+ console.log(" "+theme.flags('--no-minify')+" Disable minification for compiled .js files");
66
72
  console.log(" "+theme.flags('--sourcemap <inline|external|none>')+" How should sourcemap files be included in the .js");
67
73
  console.log(" "+theme.flags('--target <browser|node>')+" Target platform for both Imba compiler and Bun bundler");
68
74
  console.log(" "+theme.flags('--external <package>')+" Exclude package from bundle (repeatable, e.g. --external ws --external node-pty)");
@@ -88,6 +94,7 @@ if (flags.serve) {
88
94
  console.log("");
89
95
  process.exit(1);
90
96
  }
97
+ ensureBunfigPreload();
91
98
  serve(entrypoint, { port: parseInt(flags.port) || 5200, html: flags.html });
92
99
  }
93
100
  // no entrypoint or outdir
@@ -100,7 +107,9 @@ else if(!entrypoint || !flags.outdir) {
100
107
  }
101
108
  // build
102
109
  else {
103
- bundle();
110
+ ensureBunfigPreload();
111
+ const success = await bundle();
112
+ if (!success && !flags.watch) process.exit(1);
104
113
  watch(bundle);
105
114
  }
106
115
 
@@ -126,7 +135,7 @@ async function bundle() {
126
135
 
127
136
  if (!fs.existsSync(entrypoint)) {
128
137
  console.log(theme.failure('Error.') + ` The specified entrypoint does not exist: ${theme.filedir(entrypoint)}`);
129
- process.exit(0);
138
+ return false;
130
139
  }
131
140
 
132
141
  stats.failed = 0
@@ -150,7 +159,7 @@ async function bundle() {
150
159
  outdir: flags.outdir,
151
160
  target: buildTarget,
152
161
  sourcemap: flags.sourcemap || 'none',
153
- minify: true,
162
+ minify: flags.minify ?? true,
154
163
  splitting: flags.splitting || false,
155
164
  plugins: [imbaPlugin]
156
165
  };
@@ -182,6 +191,8 @@ async function bundle() {
182
191
  console.log(log);
183
192
  }
184
193
  }
194
+
195
+ return result.success && !stats.failed && !stats.errors;
185
196
  }
186
197
  catch(error) {
187
198
  console.log(theme.folder("──────────────────────────────────────────────────────────────────────"));
@@ -189,8 +200,9 @@ async function bundle() {
189
200
  console.log(error)
190
201
  console.log(theme.folder("──────────────────────────────────────────────────────────────────────"));
191
202
  console.log(theme.failure(" Failure ") + theme.filename(' Bun found an error in the compiled JS file'))
203
+ return false;
192
204
  }
193
205
  finally {
194
206
  bundling = false;
195
207
  };
196
- }
208
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bimba-cli",
3
- "version": "0.7.9",
3
+ "version": "0.7.11",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/HeapVoid/bimba.git"
@@ -19,4 +19,4 @@
19
19
  "devDependencies": {
20
20
  "imba": "latest"
21
21
  }
22
- }
22
+ }
package/serve.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { serve as bunServe } from 'bun'
2
2
  import * as compiler from 'imba/compiler'
3
- import { watch, existsSync } from 'fs'
3
+ import { watch, existsSync, statSync } from 'fs'
4
4
  import path from 'path'
5
5
  import { theme } from './utils.js'
6
6
  import { printerr } from './plugin.js'
@@ -256,57 +256,6 @@ const _compileCache = new Map() // filepath → { mtime, result }
256
256
  const _prevJs = new Map() // filepath → compiled js — for change detection
257
257
  const _prevSlots = new Map() // filepath → previous symbol slot count
258
258
 
259
- // ─── Import dependency graph ──────────────────────────────────────────────────
260
- //
261
- // When a non-tag module (utility functions, constants, shared state) is edited,
262
- // the existing class-prototype patching does nothing for the modules that
263
- // imported it — they hold their own captured references. To make those
264
- // updates flow into the UI, we track who imports whom and, on every change,
265
- // re-broadcast updates for the transitive importer set. The client's existing
266
- // HMR queue then re-imports each in turn; their top-level code reruns, picks
267
- // up the new symbols, and any tag re-registrations patch instances in place.
268
- //
269
- // Keys are absolute, normalized paths (path.resolve). Edges are added during
270
- // compilation by scanning the produced JS for relative .imba imports.
271
-
272
- const _imports = new Map() // absFile → Set<absFile> (what it imports)
273
- const _importers = new Map() // absFile → Set<absFile> (who imports it)
274
-
275
- function extractImports(js, fromAbs) {
276
- const dir = path.dirname(fromAbs)
277
- const out = new Set()
278
- const re = /(?:^|[\s;])(?:import|from)\s*['"]([^'"]+)['"]/g
279
- let m
280
- while ((m = re.exec(js))) {
281
- const spec = m[1]
282
- if (!spec.startsWith('.') && !spec.startsWith('/')) continue
283
- if (!spec.endsWith('.imba')) continue
284
- const resolved = spec.startsWith('/')
285
- ? path.resolve('.' + spec)
286
- : path.resolve(dir, spec)
287
- out.add(resolved)
288
- }
289
- return out
290
- }
291
-
292
- function updateImportGraph(fromAbs, newDeps) {
293
- const old = _imports.get(fromAbs)
294
- if (old) {
295
- for (const d of old) {
296
- if (newDeps.has(d)) continue
297
- const set = _importers.get(d)
298
- if (set) { set.delete(fromAbs); if (!set.size) _importers.delete(d) }
299
- }
300
- }
301
- for (const d of newDeps) {
302
- let set = _importers.get(d)
303
- if (!set) { set = new Set(); _importers.set(d, set) }
304
- set.add(fromAbs)
305
- }
306
- _imports.set(fromAbs, newDeps)
307
- }
308
-
309
-
310
259
  // Imba compiles tag render-cache slots as anonymous local Symbols at module top
311
260
  // level: `var $4 = Symbol(), $11 = Symbol(), ...; let c$0 = Symbol();`. Each
312
261
  // re-import of the file creates fresh Symbol objects, so old slot data on live
@@ -373,7 +322,6 @@ async function compileFile(filepath) {
373
322
  const prev = _prevSlots.get(abs)
374
323
  result.slots = (prev === undefined || prev === slotCount) ? 'stable' : 'shifted'
375
324
  _prevSlots.set(abs, slotCount)
376
- updateImportGraph(abs, extractImports(js, abs))
377
325
  }
378
326
 
379
327
  // Bake errors as an own property so caching/spreading preserves them.
@@ -392,85 +340,181 @@ function findHtml(flagHtml) {
392
340
  return candidates.find(p => existsSync(p)) || './index.html';
393
341
  }
394
342
 
395
- // ─── Node modules resolution ─────────────────────────────────────────────────
343
+ // ─── Vendor modules ──────────────────────────────────────────────────────────
396
344
 
397
- const _pkgJsonCache = new Map() // pkg root dir parsed package.json
345
+ const _vendorCache = new Map() // entrypoint { mtime, code }
398
346
 
399
- async function readPkgJson(pkgDir) {
400
- const cached = _pkgJsonCache.get(pkgDir)
401
- if (cached) return cached
402
- try {
403
- const json = JSON.parse(await Bun.file(path.join(pkgDir, 'package.json')).text())
404
- _pkgJsonCache.set(pkgDir, json)
405
- return json
406
- } catch(_) {
407
- return null
347
+ function vendorUrl(specifier) {
348
+ return '/__bimba_vendor__/' + specifier
349
+ }
350
+
351
+ function vendorPrefixUrl(specifier) {
352
+ return vendorUrl(specifier) + '/'
353
+ }
354
+
355
+ function resolveFileCandidate(filepath) {
356
+ const candidates = [
357
+ filepath,
358
+ filepath + '.js',
359
+ filepath + '.mjs',
360
+ filepath + '.cjs',
361
+ filepath + '.imba',
362
+ filepath + '.css',
363
+ path.join(filepath, 'index.js'),
364
+ path.join(filepath, 'index.mjs'),
365
+ path.join(filepath, 'index.cjs'),
366
+ path.join(filepath, 'index.imba'),
367
+ ]
368
+
369
+ for (const candidate of candidates) {
370
+ if (!existsSync(candidate)) continue
371
+ try {
372
+ if (statSync(candidate).isFile()) return candidate
373
+ } catch (_) {
374
+ // ignore vanished files and continue resolving
375
+ }
408
376
  }
377
+
378
+ return null
409
379
  }
410
380
 
411
- // Wrap a CommonJS file as an ESM module so the browser can import it.
412
- // Detects CJS by checking for `module.exports` or top-level `exports.` usage.
413
- function wrapCJS(code) {
414
- if (!code.includes('module.exports') && !code.includes('exports.')) return null
415
- // Already has ESM syntax — don't wrap (dual-format files)
416
- if (/^\s*(export\s|import\s)/m.test(code)) return null
417
- return `var module = { exports: {} }, exports = module.exports;\n${code}\nexport default module.exports;\nexport { module };`
381
+ function vendorSpecifierFromPath(pathname) {
382
+ const prefix = '/__bimba_vendor__/'
383
+ if (!pathname.startsWith(prefix)) return null
384
+ const specifier = decodeURIComponent(pathname.slice(prefix.length))
385
+ return specifier || null
418
386
  }
419
387
 
420
- // Resolve the ESM entry point for an npm package.
421
- // Priority: exports["."].import → exports["."].default → module → main
422
- function resolveEntry(depPkg) {
423
- const exp = depPkg.exports;
424
- if (exp) {
425
- // exports: "./index.js" (string shorthand)
426
- if (typeof exp === 'string') return exp;
427
- // exports: { ".": { "import": "...", "default": "..." } }
428
- const dot = exp['.'];
429
- if (dot) {
430
- if (typeof dot === 'string') return dot;
431
- if (dot.import) return dot.import;
432
- if (dot.default) return dot.default;
388
+ async function bundleVendor(entrypoint) {
389
+ try {
390
+ const stat = path.isAbsolute(entrypoint) && existsSync(entrypoint) ? statSync(entrypoint) : null
391
+ const mtime = stat?.mtimeMs || 0
392
+ const cached = _vendorCache.get(entrypoint)
393
+ if (cached && cached.mtime === mtime) return cached
394
+
395
+ const result = await Bun.build({
396
+ entrypoints: [entrypoint],
397
+ target: 'browser',
398
+ format: 'esm',
399
+ write: false,
400
+ minify: false,
401
+ sourcemap: 'none',
402
+ packages: 'bundle',
403
+ })
404
+
405
+ if (!result.success) {
406
+ return { mtime, errors: result.logs.map(log => String(log)) }
407
+ }
408
+
409
+ const output = result.outputs.find(output => output.path.endsWith('.js')) || result.outputs[0]
410
+ if (!output) return { mtime, errors: ['Bun.build did not return a JavaScript output'] }
411
+
412
+ let code = await output.text()
413
+ const css = (await Promise.all(
414
+ result.outputs
415
+ .filter(output => output.path.endsWith('.css'))
416
+ .map(output => output.text())
417
+ )).join('\n')
418
+ if (css) {
419
+ const id = JSON.stringify('vendor:' + entrypoint)
420
+ code = [
421
+ `const __bimba_vendor_css_id = ${id};`,
422
+ `let __bimba_vendor_css = document.querySelector('style[data-bimba-css=' + JSON.stringify(__bimba_vendor_css_id) + ']');`,
423
+ `if (!__bimba_vendor_css) { __bimba_vendor_css = document.createElement('style'); __bimba_vendor_css.setAttribute('data-bimba-css', __bimba_vendor_css_id); document.head.appendChild(__bimba_vendor_css); }`,
424
+ `__bimba_vendor_css.textContent = ${JSON.stringify(css)};`,
425
+ code,
426
+ ].join('\n')
433
427
  }
434
- // exports: { "import": "...", "default": "..." } (no "." wrapper)
435
- if (exp.import) return exp.import;
436
- if (exp.default) return exp.default;
428
+ const bundled = { mtime, code }
429
+ _vendorCache.set(entrypoint, bundled)
430
+ return bundled
431
+ } catch (error) {
432
+ return { mtime: 0, errors: [error?.message || String(error)] }
437
433
  }
438
- // Fallback to legacy fields
439
- return depPkg.module || depPkg.browser || depPkg.main;
440
434
  }
441
435
 
442
- // Build an ES import map from package.json dependencies.
443
- // The import map is intentionally simple — it just maps bare specifiers
444
- // to /node_modules/ URLs. All the smart resolution (conditional exports,
445
- // CJS→ESM, entry points, extensions) happens on the server side.
446
- async function buildImportMap() {
447
- const imports = {
448
- 'imba/runtime': 'https://esm.sh/imba/runtime',
449
- 'imba': 'https://esm.sh/imba',
450
- };
436
+ async function buildGeneratedImportMap() {
437
+ const imports = {}
438
+
451
439
  try {
452
- const pkg = JSON.parse(await Bun.file('./package.json').text());
453
- for (const [name] of Object.entries(pkg.dependencies || {})) {
454
- if (name === 'imba') continue;
455
- imports[name] = `/node_modules/${name}/`;
456
- imports[name + '/'] = `/node_modules/${name}/`;
440
+ const pkg = JSON.parse(await Bun.file('./package.json').text())
441
+ for (const field of ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies']) {
442
+ for (const name of Object.keys(pkg[field] || {})) {
443
+ imports[name] ||= vendorUrl(name)
444
+ imports[name + '/'] ||= vendorPrefixUrl(name)
445
+ }
457
446
  }
458
447
  } catch(_) { /* no package.json */ }
459
448
 
460
- return `\t\t<script type="importmap">\n\t\t\t${JSON.stringify({ imports }, null, '\t\t\t\t')}\n\t\t</script>`;
449
+ imports['imba'] ||= vendorUrl('imba')
450
+ imports['imba/'] ||= vendorPrefixUrl('imba')
451
+ imports['imba/runtime'] ||= vendorUrl('imba/runtime')
452
+
453
+ return { imports }
454
+ }
455
+
456
+ function extractUserImportMap(html) {
457
+ const merged = { imports: {}, scopes: {} }
458
+
459
+ html = html.replace(/<script\s+type=["']importmap["'][^>]*>([\s\S]*?)<\/script>/gi, (_match, json) => {
460
+ try {
461
+ const parsed = JSON.parse(json)
462
+ Object.assign(merged.imports, parsed.imports || {})
463
+ for (const [scope, specifiers] of Object.entries(parsed.scopes || {})) {
464
+ merged.scopes[scope] = { ...(merged.scopes[scope] || {}), ...specifiers }
465
+ }
466
+ } catch (_) {
467
+ // ignore invalid import maps and keep serving the page
468
+ }
469
+ return ''
470
+ })
471
+
472
+ if (!Object.keys(merged.scopes).length) delete merged.scopes
473
+ return { html, importMap: merged }
474
+ }
475
+
476
+ function mergeImportMaps(generated, user) {
477
+ const merged = {
478
+ imports: { ...(generated.imports || {}), ...(user.imports || {}) },
479
+ }
480
+
481
+ const scopeKeys = new Set([
482
+ ...Object.keys(generated.scopes || {}),
483
+ ...Object.keys(user.scopes || {}),
484
+ ])
485
+
486
+ if (scopeKeys.size) {
487
+ merged.scopes = {}
488
+ for (const key of scopeKeys) {
489
+ merged.scopes[key] = {
490
+ ...((generated.scopes || {})[key] || {}),
491
+ ...((user.scopes || {})[key] || {}),
492
+ }
493
+ }
494
+ }
495
+
496
+ return merged
497
+ }
498
+
499
+ function renderImportMapTag(importMap) {
500
+ return `\t\t<script type="importmap">\n\t\t\t${JSON.stringify(importMap, null, '\t\t\t\t')}\n\t\t</script>`
461
501
  }
462
502
 
463
503
  // Rewrite production HTML for the dev server:
464
- // strips existing importmap + data-entrypoint script, injects importmap +
504
+ // strips existing importmap + data-entrypoint script, injects merged importmap +
465
505
  // entrypoint module + HMR client before </head>.
466
- function transformHtml(html, entrypoint, importMapTag) {
467
- html = html.replace(/<script\s+type=["']importmap["'][^>]*>[\s\S]*?<\/script>/gi, '');
468
- html = html.replace(/<script([^>]*)\bdata-entrypoint\b([^>]*)><\/script>/gi, '');
469
- const entryUrl = '/' + entrypoint.replace(/^\.\//, '').replaceAll('\\', '/');
506
+ function transformHtml(html, entrypoint, generatedImportMap) {
507
+ const extracted = extractUserImportMap(html)
508
+ html = extracted.html
509
+ html = html.replace(/<script([^>]*)\bdata-entrypoint\b([^>]*)><\/script>/gi, '')
510
+
511
+ const entryUrl = '/' + entrypoint.replace(/^\.\//, '').replaceAll('\\', '/')
512
+ const importMapTag = renderImportMapTag(mergeImportMaps(generatedImportMap, extracted.importMap))
513
+
470
514
  html = html.replace('</head>',
471
515
  `${importMapTag}\n\t\t<script type='module' src='${entryUrl}'></script>\n${hmrClient}\n\t</head>`
472
- );
473
- return html;
516
+ )
517
+ return html
474
518
  }
475
519
 
476
520
  // ─── Dev server ───────────────────────────────────────────────────────────────
@@ -481,7 +525,7 @@ export function serve(entrypoint, flags) {
481
525
  const htmlDir = path.dirname(htmlPath)
482
526
  const srcDir = path.dirname(entrypoint)
483
527
  const sockets = new Set()
484
- let importMapTag = null
528
+ let generatedImportMap = null
485
529
 
486
530
  // ── Status line (prints current compile result, fades out on success) ──────
487
531
 
@@ -601,12 +645,22 @@ export function serve(entrypoint, flags) {
601
645
  if (server.upgrade(req)) return undefined
602
646
  }
603
647
 
648
+ if (pathname.startsWith('/__bimba_vendor__/')) {
649
+ const specifier = vendorSpecifierFromPath(pathname)
650
+ const bundled = specifier ? await bundleVendor(specifier) : null
651
+ if (bundled?.code) {
652
+ return new Response(bundled.code, { headers: { 'Content-Type': 'application/javascript' } })
653
+ }
654
+
655
+ return new Response((bundled?.errors || [`Could not bundle vendor module: ${specifier}`]).join('\n'), { status: 500 })
656
+ }
657
+
604
658
  // HTML: index or any .html file
605
659
  if (pathname === '/' || pathname.endsWith('.html')) {
606
660
  const htmlFile = pathname === '/' ? htmlPath : '.' + pathname
607
661
  let html = await Bun.file(htmlFile).text()
608
- if (!importMapTag) importMapTag = await buildImportMap()
609
- return new Response(transformHtml(html, entrypoint, importMapTag), {
662
+ if (!generatedImportMap) generatedImportMap = await buildGeneratedImportMap()
663
+ return new Response(transformHtml(html, entrypoint, generatedImportMap), {
610
664
  headers: { 'Content-Type': 'text/html' },
611
665
  })
612
666
  }
@@ -653,57 +707,23 @@ export function serve(entrypoint, flags) {
653
707
  }
654
708
  }
655
709
 
656
- // node_modules: entry point resolution, .imba compilation, CJS→ESM wrapping.
657
- // The import map just maps bare specifiers to /node_modules/pkg/ URLs.
658
- // All smart resolution happens here at request time.
710
+ // Direct node_modules URLs (from user import maps or explicit imports)
711
+ // are bundled through Bun too, so browser/cjs/exports handling stays
712
+ // in one place.
659
713
  if (pathname.startsWith('/node_modules/')) {
660
- const filepath = '.' + pathname
661
-
662
- // Serve a resolved file: compile .imba, wrap CJS as ESM, pass ESM through
663
- const serveResolved = async (filePath) => {
664
- if (filePath.endsWith('.imba')) {
665
- const f = Bun.file(filePath)
666
- if (!(await f.exists())) return null
667
- const out = await compileFile(filePath)
668
- if (out.errors?.length) return new Response(out.errors.map(e => e.message).join('\n'), { status: 500 })
669
- return new Response(out.js, { headers: { 'Content-Type': 'application/javascript' } })
670
- }
671
- const f = Bun.file(filePath)
672
- if (!(await f.exists())) return null
673
- const code = await f.text()
674
- const wrapped = wrapCJS(code)
675
- return new Response(wrapped || code, { headers: { 'Content-Type': 'application/javascript' } })
676
- }
677
-
678
- // Resolve entry point for root package requests (/node_modules/pkg/ or /node_modules/pkg)
679
- const parts = pathname.slice(1).split('/') // ['node_modules', 'pkg', ...]
680
- const isScoped = parts[1]?.startsWith('@')
681
- const pkgParts = isScoped ? 3 : 2 // node_modules/@scope/pkg or node_modules/pkg
682
- const subParts = parts.slice(pkgParts)
683
- const isRootRequest = subParts.length === 0 || (subParts.length === 1 && subParts[0] === '')
684
-
685
- if (isRootRequest) {
686
- const pkgDir = './' + parts.slice(0, pkgParts).join('/')
687
- const depPkg = await readPkgJson(pkgDir)
688
- if (depPkg) {
689
- const entry = resolveEntry(depPkg)
690
- if (entry) {
691
- const resp = await serveResolved(path.join(pkgDir, entry))
692
- if (resp) return resp
693
- }
694
- }
714
+ const resolved = resolveFileCandidate('.' + pathname)
715
+ if (resolved?.endsWith('.imba')) {
716
+ const out = await compileFile(resolved)
717
+ if (out.errors?.length) return new Response(out.errors.map(e => e.message).join('\n'), { status: 500 })
718
+ return new Response(out.js, { headers: { 'Content-Type': 'application/javascript' } })
695
719
  }
696
720
 
697
- // Subpath: try the exact path, then with extensions
698
- const resp = await serveResolved(filepath)
699
- if (resp) return resp
700
-
701
- // Extensionless: try .imba, .js, .mjs
702
- if (!filepath.includes('.', filepath.lastIndexOf('/') + 1)) {
703
- for (const ext of ['.imba', '.js', '.mjs']) {
704
- const resp = await serveResolved(filepath + ext)
705
- if (resp) return resp
721
+ if (resolved) {
722
+ const bundled = await bundleVendor(path.resolve(resolved))
723
+ if (bundled?.code) {
724
+ return new Response(bundled.code, { headers: { 'Content-Type': 'application/javascript' } })
706
725
  }
726
+ return new Response((bundled?.errors || [`Could not bundle ${pathname}`]).join('\n'), { status: 500 })
707
727
  }
708
728
  }
709
729
 
@@ -731,13 +751,13 @@ export function serve(entrypoint, flags) {
731
751
  }
732
752
 
733
753
  // SPA fallback for extension-less paths
734
- if (!lastSegment.includes('.')) {
735
- let html = await Bun.file(htmlPath).text()
736
- if (!importMapTag) importMapTag = await buildImportMap()
737
- return new Response(transformHtml(html, entrypoint, importMapTag), {
738
- headers: { 'Content-Type': 'text/html' },
739
- })
740
- }
754
+ if (!lastSegment.includes('.')) {
755
+ let html = await Bun.file(htmlPath).text()
756
+ if (!generatedImportMap) generatedImportMap = await buildGeneratedImportMap()
757
+ return new Response(transformHtml(html, entrypoint, generatedImportMap), {
758
+ headers: { 'Content-Type': 'text/html' },
759
+ })
760
+ }
741
761
 
742
762
  return new Response('Not Found', { status: 404 })
743
763
  },