bimba-cli 0.7.0 → 0.7.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 +1 -1
  2. package/serve.js +140 -47
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bimba-cli",
3
- "version": "0.7.0",
3
+ "version": "0.7.2",
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,15 +523,10 @@ 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
- // Packages with an .imba entry point are served locally; others via esm.sh.
527
+ // The import map is intentionally simple it just maps bare specifiers
528
+ // to /node_modules/ URLs. All the smart resolution (conditional exports,
529
+ // CJS→ESM, entry points, extensions) happens on the server side.
442
530
  async function buildImportMap() {
443
531
  const imports = {
444
532
  'imba/runtime': 'https://esm.sh/imba/runtime',
@@ -448,45 +536,8 @@ async function buildImportMap() {
448
536
  const pkg = JSON.parse(await Bun.file('./package.json').text());
449
537
  for (const [name] of Object.entries(pkg.dependencies || {})) {
450
538
  if (name === 'imba') continue;
451
- try {
452
- const depPkg = JSON.parse(await Bun.file(`./node_modules/${name}/package.json`).text());
453
- const entry = resolveEntry(depPkg);
454
- if (entry && entry.endsWith('.imba')) {
455
- // Imba packages — always local
456
- imports[name] = `/node_modules/${name}/${entry}`;
457
- 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
- } else {
467
- // No resolvable entry — use esm.sh to auto-wrap
468
- imports[name] = `https://esm.sh/${name}`;
469
- imports[name + '/'] = `https://esm.sh/${name}/`;
470
- }
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
- } catch(_) {
487
- imports[name] = `https://esm.sh/${name}`;
488
- imports[name + '/'] = `https://esm.sh/${name}/`;
489
- }
539
+ imports[name] = `/node_modules/${name}/`;
540
+ imports[name + '/'] = `/node_modules/${name}/`;
490
541
  }
491
542
  } catch(_) { /* no package.json */ }
492
543
 
@@ -699,6 +750,46 @@ export function serve(entrypoint, flags) {
699
750
  }
700
751
  }
701
752
 
753
+ // node_modules: all smart resolution happens here.
754
+ // The import map just maps bare specifiers to /node_modules/pkg/ URLs.
755
+ // This block handles: entry point resolution, conditional exports
756
+ // (CJS→ESM), subpath resolution, extensionless paths.
757
+ if (pathname.startsWith('/node_modules/')) {
758
+ const filepath = '.' + pathname
759
+
760
+ // Resolve entry point for root package requests (/node_modules/pkg/ or /node_modules/pkg)
761
+ const parts = pathname.slice(1).split('/') // ['node_modules', 'pkg', ...]
762
+ const isScoped = parts[1]?.startsWith('@')
763
+ const pkgParts = isScoped ? 3 : 2 // node_modules/@scope/pkg or node_modules/pkg
764
+ const subParts = parts.slice(pkgParts)
765
+ const isRootRequest = subParts.length === 0 || (subParts.length === 1 && subParts[0] === '')
766
+
767
+ if (isRootRequest) {
768
+ // Bare import: resolve entry point from package.json
769
+ const pkgDir = './' + parts.slice(0, pkgParts).join('/')
770
+ const depPkg = await readPkgJson(pkgDir)
771
+ if (depPkg) {
772
+ const entry = resolveEntry(depPkg)
773
+ if (entry) {
774
+ const entryPath = path.join(pkgDir, entry)
775
+ const file = Bun.file(entryPath)
776
+ if (await file.exists()) return new Response(file, {
777
+ headers: { 'Content-Type': 'application/javascript' },
778
+ })
779
+ }
780
+ }
781
+ }
782
+
783
+ // Subpath: resolve via conditional exports (CJS→ESM)
784
+ const esmPath = await resolveNodeModuleESM(filepath)
785
+ if (esmPath) {
786
+ const file = Bun.file(esmPath)
787
+ if (await file.exists()) return new Response(file, {
788
+ headers: { 'Content-Type': 'application/javascript' },
789
+ })
790
+ }
791
+ }
792
+
702
793
  // Static files: check htmlDir first (for assets relative to HTML), then root
703
794
  const inHtmlDir = Bun.file(path.join(htmlDir, pathname))
704
795
  if (await inHtmlDir.exists()) return new Response(inHtmlDir)
@@ -708,6 +799,8 @@ export function serve(entrypoint, flags) {
708
799
  // Try .js / .mjs extension for extensionless paths (e.g. node_modules imports)
709
800
  const lastSegment = pathname.split('/').pop()
710
801
  if (!lastSegment.includes('.')) {
802
+ // For node_modules, ESM resolution above already handles extensionless
803
+ // paths via wildcard exports matching (tries subpath + '.js').
711
804
  for (const ext of ['.js', '.mjs']) {
712
805
  const withExt = Bun.file('.' + pathname + ext)
713
806
  if (await withExt.exists()) return new Response(withExt, {