bimba-cli 0.7.10 → 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.
- package/README.md +1 -1
- package/package.json +1 -1
- package/serve.js +98 -235
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
|
|
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
|
|
package/package.json
CHANGED
package/serve.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { serve as bunServe
|
|
1
|
+
import { serve as bunServe } from 'bun'
|
|
2
2
|
import * as compiler from 'imba/compiler'
|
|
3
3
|
import { watch, existsSync, statSync } from 'fs'
|
|
4
4
|
import path from 'path'
|
|
@@ -340,115 +340,16 @@ function findHtml(flagHtml) {
|
|
|
340
340
|
return candidates.find(p => existsSync(p)) || './index.html';
|
|
341
341
|
}
|
|
342
342
|
|
|
343
|
-
// ───
|
|
343
|
+
// ─── Vendor modules ──────────────────────────────────────────────────────────
|
|
344
344
|
|
|
345
|
-
const
|
|
345
|
+
const _vendorCache = new Map() // entrypoint → { mtime, code }
|
|
346
346
|
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
if (cached) return cached
|
|
350
|
-
try {
|
|
351
|
-
const json = JSON.parse(await Bun.file(path.join(pkgDir, 'package.json')).text())
|
|
352
|
-
_pkgJsonCache.set(pkgDir, json)
|
|
353
|
-
return json
|
|
354
|
-
} catch(_) {
|
|
355
|
-
return null
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
function toPosix(filepath) {
|
|
360
|
-
return filepath.replaceAll('\\', '/')
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
function toBrowserPath(filepath) {
|
|
364
|
-
return '/' + toPosix(path.relative(process.cwd(), path.resolve(filepath)))
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
function parsePackageSpecifier(specifier) {
|
|
368
|
-
if (specifier.startsWith('@')) {
|
|
369
|
-
const parts = specifier.split('/')
|
|
370
|
-
return {
|
|
371
|
-
name: parts.slice(0, 2).join('/'),
|
|
372
|
-
subpath: parts.slice(2).join('/'),
|
|
373
|
-
}
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
const [name, ...rest] = specifier.split('/')
|
|
377
|
-
return { name, subpath: rest.join('/') }
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
function isConditionMap(value) {
|
|
381
|
-
return !!value && typeof value === 'object' && !Array.isArray(value) && Object.keys(value).every(key => !key.startsWith('.'))
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
function pickExportTarget(value) {
|
|
385
|
-
if (!value) return null
|
|
386
|
-
if (typeof value === 'string') return value
|
|
387
|
-
if (Array.isArray(value)) {
|
|
388
|
-
for (const item of value) {
|
|
389
|
-
const resolved = pickExportTarget(item)
|
|
390
|
-
if (resolved) return resolved
|
|
391
|
-
}
|
|
392
|
-
return null
|
|
393
|
-
}
|
|
394
|
-
if (!isConditionMap(value)) return null
|
|
395
|
-
|
|
396
|
-
for (const key of ['browser', 'import', 'default', 'module']) {
|
|
397
|
-
const resolved = pickExportTarget(value[key])
|
|
398
|
-
if (resolved) return resolved
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
for (const nested of Object.values(value)) {
|
|
402
|
-
const resolved = pickExportTarget(nested)
|
|
403
|
-
if (resolved) return resolved
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
return null
|
|
347
|
+
function vendorUrl(specifier) {
|
|
348
|
+
return '/__bimba_vendor__/' + specifier
|
|
407
349
|
}
|
|
408
350
|
|
|
409
|
-
function
|
|
410
|
-
|
|
411
|
-
const exportKey = subpath ? `./${subpath}` : '.'
|
|
412
|
-
|
|
413
|
-
if (typeof exportsField === 'string' || Array.isArray(exportsField) || isConditionMap(exportsField)) {
|
|
414
|
-
return exportKey === '.' ? pickExportTarget(exportsField) : null
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
const exact = exportsField[exportKey]
|
|
418
|
-
if (exact) return pickExportTarget(exact)
|
|
419
|
-
|
|
420
|
-
let bestMatch = null
|
|
421
|
-
for (const [key, value] of Object.entries(exportsField)) {
|
|
422
|
-
if (!key.startsWith('./') || !key.includes('*')) continue
|
|
423
|
-
|
|
424
|
-
const [prefix, suffix] = key.split('*')
|
|
425
|
-
if (!exportKey.startsWith(prefix) || !exportKey.endsWith(suffix)) continue
|
|
426
|
-
|
|
427
|
-
const matched = exportKey.slice(prefix.length, exportKey.length - suffix.length)
|
|
428
|
-
const target = pickExportTarget(value)
|
|
429
|
-
if (!target) continue
|
|
430
|
-
|
|
431
|
-
const score = prefix.length + suffix.length
|
|
432
|
-
if (!bestMatch || score > bestMatch.score) {
|
|
433
|
-
bestMatch = { matched, target, score }
|
|
434
|
-
}
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
return bestMatch ? bestMatch.target.replaceAll('*', bestMatch.matched) : null
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
function resolvePackageTarget(depPkg, subpath = '') {
|
|
441
|
-
if (subpath) {
|
|
442
|
-
const exported = resolveExportTarget(depPkg.exports, subpath)
|
|
443
|
-
if (exported) return exported
|
|
444
|
-
if (depPkg.exports) return null
|
|
445
|
-
return './' + subpath
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
const exported = resolveExportTarget(depPkg.exports)
|
|
449
|
-
if (exported) return exported
|
|
450
|
-
if (typeof depPkg.browser === 'string') return depPkg.browser
|
|
451
|
-
return depPkg.module || depPkg.main || null
|
|
351
|
+
function vendorPrefixUrl(specifier) {
|
|
352
|
+
return vendorUrl(specifier) + '/'
|
|
452
353
|
}
|
|
453
354
|
|
|
454
355
|
function resolveFileCandidate(filepath) {
|
|
@@ -477,110 +378,77 @@ function resolveFileCandidate(filepath) {
|
|
|
477
378
|
return null
|
|
478
379
|
}
|
|
479
380
|
|
|
480
|
-
|
|
481
|
-
const
|
|
482
|
-
if (!
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
const depPkg = await readPkgJson(pkgDir)
|
|
486
|
-
if (!depPkg) return null
|
|
487
|
-
|
|
488
|
-
const target = resolvePackageTarget(depPkg, subpath)
|
|
489
|
-
if (!target) return null
|
|
490
|
-
|
|
491
|
-
const candidate = resolveFileCandidate(path.resolve(pkgDir, target))
|
|
492
|
-
return candidate ? path.resolve(candidate) : null
|
|
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
|
|
493
386
|
}
|
|
494
387
|
|
|
495
|
-
async function
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
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
|
+
}
|
|
504
408
|
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
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')
|
|
427
|
+
}
|
|
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)] }
|
|
508
433
|
}
|
|
509
|
-
|
|
510
|
-
return Object.entries(exportsField)
|
|
511
|
-
.filter(([key]) => key.startsWith('./') && key !== '.')
|
|
512
|
-
.map(([key, value]) => ({ key, value }))
|
|
513
|
-
}
|
|
514
|
-
|
|
515
|
-
async function expandPatternExport(name, pkgDir, key, value) {
|
|
516
|
-
const target = pickExportTarget(value)
|
|
517
|
-
if (!target || !key.includes('*') || !target.includes('*')) return {}
|
|
518
|
-
|
|
519
|
-
const specPattern = key.slice(2)
|
|
520
|
-
const [specPrefix, specSuffix] = specPattern.split('*')
|
|
521
|
-
const normalizedTarget = target.replace(/^\.\//, '')
|
|
522
|
-
const [targetPrefix, targetSuffix] = normalizedTarget.split('*')
|
|
523
|
-
const glob = new Glob(normalizedTarget)
|
|
524
|
-
const mappings = {}
|
|
525
|
-
|
|
526
|
-
for await (const file of glob.scan(pkgDir)) {
|
|
527
|
-
const rel = toPosix(file)
|
|
528
|
-
if (!rel.startsWith(targetPrefix) || !rel.endsWith(targetSuffix)) continue
|
|
529
|
-
|
|
530
|
-
const matched = rel.slice(targetPrefix.length, rel.length - targetSuffix.length)
|
|
531
|
-
const specifier = `${name}/${specPrefix}${matched}${specSuffix}`.replace(/\/+/g, '/')
|
|
532
|
-
mappings[specifier] = '/' + toPosix(path.join('node_modules', name, rel))
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
return mappings
|
|
536
434
|
}
|
|
537
435
|
|
|
538
436
|
async function buildGeneratedImportMap() {
|
|
539
437
|
const imports = {}
|
|
540
|
-
const visited = new Set()
|
|
541
|
-
const queue = []
|
|
542
438
|
|
|
543
439
|
try {
|
|
544
440
|
const pkg = JSON.parse(await Bun.file('./package.json').text())
|
|
545
441
|
for (const field of ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies']) {
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
while (queue.length) {
|
|
551
|
-
const name = queue.shift()
|
|
552
|
-
if (!name || visited.has(name)) continue
|
|
553
|
-
visited.add(name)
|
|
554
|
-
|
|
555
|
-
const pkgDir = path.join(process.cwd(), 'node_modules', name)
|
|
556
|
-
const depPkg = await readPkgJson(pkgDir)
|
|
557
|
-
if (!depPkg) continue
|
|
558
|
-
|
|
559
|
-
const entryFile = await resolvePackageFile(name)
|
|
560
|
-
if (entryFile) imports[name] = toBrowserPath(entryFile)
|
|
561
|
-
|
|
562
|
-
if (depPkg.exports) {
|
|
563
|
-
for (const { key, value } of exportSpecifiers(name, depPkg.exports)) {
|
|
564
|
-
if (key.includes('*')) {
|
|
565
|
-
Object.assign(imports, await expandPatternExport(name, pkgDir, key, value))
|
|
566
|
-
continue
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
const specifier = `${name}/${key.slice(2)}`
|
|
570
|
-
const file = await resolvePackageFile(specifier)
|
|
571
|
-
if (file) imports[specifier] = toBrowserPath(file)
|
|
442
|
+
for (const name of Object.keys(pkg[field] || {})) {
|
|
443
|
+
imports[name] ||= vendorUrl(name)
|
|
444
|
+
imports[name + '/'] ||= vendorPrefixUrl(name)
|
|
572
445
|
}
|
|
573
|
-
} else {
|
|
574
|
-
imports[name + '/'] = '/' + toPosix(path.join('node_modules', name)) + '/'
|
|
575
446
|
}
|
|
447
|
+
} catch(_) { /* no package.json */ }
|
|
576
448
|
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
}
|
|
581
|
-
|
|
582
|
-
if (!imports['imba']) imports['imba'] = 'https://esm.sh/imba'
|
|
583
|
-
if (!imports['imba/runtime']) imports['imba/runtime'] = 'https://esm.sh/imba/runtime'
|
|
449
|
+
imports['imba'] ||= vendorUrl('imba')
|
|
450
|
+
imports['imba/'] ||= vendorPrefixUrl('imba')
|
|
451
|
+
imports['imba/runtime'] ||= vendorUrl('imba/runtime')
|
|
584
452
|
|
|
585
453
|
return { imports }
|
|
586
454
|
}
|
|
@@ -632,15 +500,6 @@ function renderImportMapTag(importMap) {
|
|
|
632
500
|
return `\t\t<script type="importmap">\n\t\t\t${JSON.stringify(importMap, null, '\t\t\t\t')}\n\t\t</script>`
|
|
633
501
|
}
|
|
634
502
|
|
|
635
|
-
// Wrap a CommonJS file as an ESM module so the browser can import it.
|
|
636
|
-
// Detects CJS by checking for `module.exports` or top-level `exports.` usage.
|
|
637
|
-
function wrapCJS(code) {
|
|
638
|
-
if (!code.includes('module.exports') && !code.includes('exports.')) return null
|
|
639
|
-
// Already has ESM syntax — don't wrap (dual-format files)
|
|
640
|
-
if (/^\s*(export\s|import\s)/m.test(code)) return null
|
|
641
|
-
return `var module = { exports: {} }, exports = module.exports;\n${code}\nexport default module.exports;\nexport { module };`
|
|
642
|
-
}
|
|
643
|
-
|
|
644
503
|
// Rewrite production HTML for the dev server:
|
|
645
504
|
// strips existing importmap + data-entrypoint script, injects merged importmap +
|
|
646
505
|
// entrypoint module + HMR client before </head>.
|
|
@@ -786,16 +645,26 @@ export function serve(entrypoint, flags) {
|
|
|
786
645
|
if (server.upgrade(req)) return undefined
|
|
787
646
|
}
|
|
788
647
|
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
return new Response(transformHtml(html, entrypoint, generatedImportMap), {
|
|
795
|
-
headers: { 'Content-Type': 'text/html' },
|
|
796
|
-
})
|
|
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' } })
|
|
797
653
|
}
|
|
798
654
|
|
|
655
|
+
return new Response((bundled?.errors || [`Could not bundle vendor module: ${specifier}`]).join('\n'), { status: 500 })
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
// HTML: index or any .html file
|
|
659
|
+
if (pathname === '/' || pathname.endsWith('.html')) {
|
|
660
|
+
const htmlFile = pathname === '/' ? htmlPath : '.' + pathname
|
|
661
|
+
let html = await Bun.file(htmlFile).text()
|
|
662
|
+
if (!generatedImportMap) generatedImportMap = await buildGeneratedImportMap()
|
|
663
|
+
return new Response(transformHtml(html, entrypoint, generatedImportMap), {
|
|
664
|
+
headers: { 'Content-Type': 'text/html' },
|
|
665
|
+
})
|
|
666
|
+
}
|
|
667
|
+
|
|
799
668
|
// Imba files: compile on demand and serve as JS
|
|
800
669
|
if (pathname.endsWith('.imba')) {
|
|
801
670
|
const filepath = '.' + pathname
|
|
@@ -838,31 +707,25 @@ export function serve(entrypoint, flags) {
|
|
|
838
707
|
}
|
|
839
708
|
}
|
|
840
709
|
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
const
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
const out = await compileFile(filePath)
|
|
852
|
-
if (out.errors?.length) return new Response(out.errors.map(e => e.message).join('\n'), { status: 500 })
|
|
853
|
-
return new Response(out.js, { headers: { 'Content-Type': 'application/javascript' } })
|
|
854
|
-
}
|
|
855
|
-
const f = Bun.file(filePath)
|
|
856
|
-
if (!(await f.exists())) return null
|
|
857
|
-
const code = await f.text()
|
|
858
|
-
const wrapped = wrapCJS(code)
|
|
859
|
-
return new Response(wrapped || code, { headers: { 'Content-Type': 'application/javascript' } })
|
|
860
|
-
}
|
|
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.
|
|
713
|
+
if (pathname.startsWith('/node_modules/')) {
|
|
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' } })
|
|
719
|
+
}
|
|
861
720
|
|
|
862
|
-
|
|
863
|
-
const
|
|
864
|
-
if (
|
|
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' } })
|
|
725
|
+
}
|
|
726
|
+
return new Response((bundled?.errors || [`Could not bundle ${pathname}`]).join('\n'), { status: 500 })
|
|
865
727
|
}
|
|
728
|
+
}
|
|
866
729
|
|
|
867
730
|
// Static files: check htmlDir first (for assets relative to HTML), then root
|
|
868
731
|
const inHtmlDir = Bun.file(path.join(htmlDir, pathname))
|