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.
- package/package.json +1 -1
- package/serve.js +140 -47
package/package.json
CHANGED
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
|
-
//
|
|
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
|
-
|
|
452
|
-
|
|
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, {
|