bimba-cli 0.7.0 → 0.7.1

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 +1 -1
  2. package/serve.js +114 -32
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bimba-cli",
3
- "version": "0.7.0",
3
+ "version": "0.7.1",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/HeapVoid/bimba.git"
package/serve.js CHANGED
@@ -408,6 +408,99 @@ function findHtml(flagHtml) {
408
408
  return candidates.find(p => existsSync(p)) || './index.html';
409
409
  }
410
410
 
411
+ // ─── Node modules resolution ─────────────────────────────────────────────────
412
+ //
413
+ // Packages with conditional exports (e.g. highlight.js) map subpaths like
414
+ // "./lib/languages/*" to different files for `require` vs `import`. The browser
415
+ // import map uses a simple trailing-slash prefix mapping, so
416
+ // `highlight.js/lib/languages/javascript` → `/node_modules/highlight.js/lib/languages/javascript`.
417
+ // But that's the CJS file — the browser needs the ESM one under `es/`.
418
+ //
419
+ // We solve this at the HTTP level: when serving a JS file from node_modules,
420
+ // check if the file is CJS and the package has an ESM alternative via its
421
+ // `exports` field. If so, rewrite to the ESM path.
422
+
423
+ const _pkgJsonCache = new Map() // pkg root dir → parsed package.json
424
+
425
+ async function readPkgJson(pkgDir) {
426
+ const cached = _pkgJsonCache.get(pkgDir)
427
+ if (cached) return cached
428
+ try {
429
+ const json = JSON.parse(await Bun.file(path.join(pkgDir, 'package.json')).text())
430
+ _pkgJsonCache.set(pkgDir, json)
431
+ return json
432
+ } catch(_) {
433
+ return null
434
+ }
435
+ }
436
+
437
+ // Given a request path inside node_modules (e.g. "node_modules/highlight.js/lib/languages/javascript"),
438
+ // try to resolve via conditional exports to find the ESM version.
439
+ // Returns the rewritten filesystem path, or null if no rewrite needed.
440
+ async function resolveNodeModuleESM(filepath) {
441
+ // Only rewrite JS files (or extensionless → .js)
442
+ const parts = filepath.split('/')
443
+ const nmIdx = parts.indexOf('node_modules')
444
+ if (nmIdx === -1) return null
445
+
446
+ const pkgName = parts[nmIdx + 1].startsWith('@')
447
+ ? parts[nmIdx + 1] + '/' + parts[nmIdx + 2]
448
+ : parts[nmIdx + 1]
449
+ const pkgDir = parts.slice(0, nmIdx + 1 + (pkgName.includes('/') ? 2 : 1)).join('/')
450
+ const depPkg = await readPkgJson(pkgDir)
451
+ if (!depPkg?.exports || typeof depPkg.exports === 'string') return null
452
+
453
+ // Build the subpath relative to pkg root: "./lib/languages/javascript"
454
+ const pkgParts = pkgName.includes('/') ? 2 : 1
455
+ const subParts = parts.slice(nmIdx + 1 + pkgParts)
456
+ const subpath = './' + subParts.join('/')
457
+
458
+ const exp = depPkg.exports
459
+
460
+ // Try exact match first: exports["./lib/core"]
461
+ if (exp[subpath]) {
462
+ const target = exp[subpath]
463
+ const esm = typeof target === 'string' ? target : (target?.import || target?.default)
464
+ if (esm) {
465
+ const resolved = path.join(pkgDir, esm)
466
+ if (existsSync(resolved)) return resolved
467
+ }
468
+ }
469
+
470
+ // Try with .js extension: exports["./lib/core"] for request "./lib/core"
471
+ const subpathJs = subpath + '.js'
472
+ if (exp[subpathJs]) {
473
+ const target = exp[subpathJs]
474
+ const esm = typeof target === 'string' ? target : (target?.import || target?.default)
475
+ if (esm) {
476
+ const resolved = path.join(pkgDir, esm)
477
+ if (existsSync(resolved)) return resolved
478
+ }
479
+ }
480
+
481
+ // Try wildcard match: exports["./lib/languages/*"]
482
+ for (const [pattern, target] of Object.entries(exp)) {
483
+ if (!pattern.includes('*')) continue
484
+ const prefix = pattern.slice(0, pattern.indexOf('*'))
485
+ const suffix = pattern.slice(pattern.indexOf('*') + 1)
486
+
487
+ // Check both with and without .js extension
488
+ for (const sp of [subpath, subpathJs]) {
489
+ if (!sp.startsWith(prefix)) continue
490
+ if (suffix && !sp.endsWith(suffix)) continue
491
+ const stem = sp.slice(prefix.length, suffix ? -suffix.length || undefined : undefined)
492
+
493
+ const esm = typeof target === 'string' ? target : (target?.import || target?.default)
494
+ if (!esm || !esm.includes('*')) continue
495
+
496
+ const resolved = path.join(pkgDir, esm.replace('*', stem))
497
+ if (existsSync(resolved)) return resolved
498
+ }
499
+ }
500
+
501
+ return null
502
+ }
503
+
411
504
  // Resolve the ESM entry point for an npm package.
412
505
  // Priority: exports["."].import → exports["."].default → module → main
413
506
  function resolveEntry(depPkg) {
@@ -430,13 +523,6 @@ function resolveEntry(depPkg) {
430
523
  return depPkg.module || depPkg.browser || depPkg.main;
431
524
  }
432
525
 
433
- // Check if a package has subpath exports (exports with keys other than ".")
434
- function hasSubpathExports(depPkg) {
435
- const exp = depPkg.exports;
436
- if (!exp || typeof exp === 'string') return false;
437
- return Object.keys(exp).some(k => k !== '.' && k.startsWith('./'));
438
- }
439
-
440
526
  // Build an ES import map from package.json dependencies.
441
527
  // Packages with an .imba entry point are served locally; others via esm.sh.
442
528
  async function buildImportMap() {
@@ -451,38 +537,19 @@ async function buildImportMap() {
451
537
  try {
452
538
  const depPkg = JSON.parse(await Bun.file(`./node_modules/${name}/package.json`).text());
453
539
  const entry = resolveEntry(depPkg);
454
- if (entry && entry.endsWith('.imba')) {
455
- // Imba packagesalways local
540
+ if (entry && !entry.startsWith('http')) {
541
+ // Local packageserve from node_modules
456
542
  imports[name] = `/node_modules/${name}/${entry}`;
543
+ // Always add trailing-slash mapping for deep/subpath imports
544
+ // (e.g. highlight.js/lib/languages/javascript). The browser
545
+ // doesn't enforce Node's `exports` restrictions, it just needs
546
+ // a URL prefix to resolve bare specifiers like "pkg/sub/path".
457
547
  imports[name + '/'] = `/node_modules/${name}/`;
458
- } else if (entry && !entry.startsWith('http')) {
459
- // Local ESM/CJS package — serve from node_modules
460
- imports[name] = `/node_modules/${name}/${entry}`;
461
- // Trailing-slash mapping for deep imports (unless package
462
- // uses subpath exports — those need explicit mappings)
463
- if (!hasSubpathExports(depPkg)) {
464
- imports[name + '/'] = `/node_modules/${name}/`;
465
- }
466
548
  } else {
467
549
  // No resolvable entry — use esm.sh to auto-wrap
468
550
  imports[name] = `https://esm.sh/${name}`;
469
551
  imports[name + '/'] = `https://esm.sh/${name}/`;
470
552
  }
471
-
472
- // Add subpath export mappings (e.g. "pkg/styles.css" → local path)
473
- if (hasSubpathExports(depPkg)) {
474
- const exp = depPkg.exports;
475
- for (const [subpath, target] of Object.entries(exp)) {
476
- if (subpath === '.') continue;
477
- if (!subpath.startsWith('./')) continue;
478
- const importKey = name + '/' + subpath.slice(2);
479
- const resolved = typeof target === 'string' ? target
480
- : (target?.import || target?.default);
481
- if (resolved) {
482
- imports[importKey] = `/node_modules/${name}/${resolved}`;
483
- }
484
- }
485
- }
486
553
  } catch(_) {
487
554
  imports[name] = `https://esm.sh/${name}`;
488
555
  imports[name + '/'] = `https://esm.sh/${name}/`;
@@ -699,6 +766,19 @@ export function serve(entrypoint, flags) {
699
766
  }
700
767
  }
701
768
 
769
+ // node_modules: resolve conditional exports (CJS → ESM) before serving.
770
+ // e.g. highlight.js/lib/languages/javascript → es/languages/javascript.js
771
+ if (pathname.startsWith('/node_modules/')) {
772
+ const filepath = '.' + pathname
773
+ const esmPath = await resolveNodeModuleESM(filepath)
774
+ if (esmPath) {
775
+ const file = Bun.file(esmPath)
776
+ if (await file.exists()) return new Response(file, {
777
+ headers: { 'Content-Type': 'application/javascript' },
778
+ })
779
+ }
780
+ }
781
+
702
782
  // Static files: check htmlDir first (for assets relative to HTML), then root
703
783
  const inHtmlDir = Bun.file(path.join(htmlDir, pathname))
704
784
  if (await inHtmlDir.exists()) return new Response(inHtmlDir)
@@ -708,6 +788,8 @@ export function serve(entrypoint, flags) {
708
788
  // Try .js / .mjs extension for extensionless paths (e.g. node_modules imports)
709
789
  const lastSegment = pathname.split('/').pop()
710
790
  if (!lastSegment.includes('.')) {
791
+ // For node_modules, ESM resolution above already handles extensionless
792
+ // paths via wildcard exports matching (tries subpath + '.js').
711
793
  for (const ext of ['.js', '.mjs']) {
712
794
  const withExt = Bun.file('.' + pathname + ext)
713
795
  if (await withExt.exists()) return new Response(withExt, {