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.
- package/package.json +1 -1
- package/serve.js +114 -32
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,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.
|
|
455
|
-
//
|
|
540
|
+
if (entry && !entry.startsWith('http')) {
|
|
541
|
+
// Local package — serve 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, {
|