bimba-cli 0.7.10 → 0.7.12
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 +117 -234
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,6 +1,6 @@
|
|
|
1
|
-
import { serve as bunServe
|
|
1
|
+
import { serve as bunServe } from 'bun'
|
|
2
2
|
import * as compiler from 'imba/compiler'
|
|
3
|
-
import { watch, existsSync, statSync } from 'fs'
|
|
3
|
+
import { mkdirSync, watch, existsSync, statSync, writeFileSync } from 'fs'
|
|
4
4
|
import path from 'path'
|
|
5
5
|
import { theme } from './utils.js'
|
|
6
6
|
import { printerr } from './plugin.js'
|
|
@@ -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('/') }
|
|
347
|
+
function vendorUrl(specifier) {
|
|
348
|
+
return '/__bimba_vendor__/' + specifier
|
|
378
349
|
}
|
|
379
350
|
|
|
380
|
-
function
|
|
381
|
-
return
|
|
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
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
function resolveExportTarget(exportsField, subpath = '') {
|
|
410
|
-
if (!exportsField) return null
|
|
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,97 @@ 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
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
.map(([key, value]) => ({ key, value }))
|
|
388
|
+
function vendorEntrypoint(entrypoint) {
|
|
389
|
+
if (path.isAbsolute(entrypoint)) return entrypoint
|
|
390
|
+
|
|
391
|
+
const dir = path.join(process.cwd(), 'node_modules', '.cache', 'bimba', 'vendor-entry')
|
|
392
|
+
mkdirSync(dir, { recursive: true })
|
|
393
|
+
|
|
394
|
+
const name = encodeURIComponent(entrypoint).replace(/%/g, '_')
|
|
395
|
+
const file = path.join(dir, name + '.js')
|
|
396
|
+
const specifier = JSON.stringify(entrypoint)
|
|
397
|
+
const code = [
|
|
398
|
+
`export * from ${specifier};`,
|
|
399
|
+
`import * as mod from ${specifier};`,
|
|
400
|
+
`export default (mod.default ?? mod);`,
|
|
401
|
+
'',
|
|
402
|
+
].join('\n')
|
|
403
|
+
writeFileSync(file, code)
|
|
404
|
+
return file
|
|
513
405
|
}
|
|
514
406
|
|
|
515
|
-
async function
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
407
|
+
async function bundleVendor(entrypoint) {
|
|
408
|
+
try {
|
|
409
|
+
const stat = path.isAbsolute(entrypoint) && existsSync(entrypoint) ? statSync(entrypoint) : null
|
|
410
|
+
const mtime = stat?.mtimeMs || 0
|
|
411
|
+
const cached = _vendorCache.get(entrypoint)
|
|
412
|
+
if (cached && cached.mtime === mtime) return cached
|
|
413
|
+
|
|
414
|
+
const buildEntrypoint = vendorEntrypoint(entrypoint)
|
|
415
|
+
const result = await Bun.build({
|
|
416
|
+
entrypoints: [buildEntrypoint],
|
|
417
|
+
target: 'browser',
|
|
418
|
+
format: 'esm',
|
|
419
|
+
write: false,
|
|
420
|
+
minify: false,
|
|
421
|
+
sourcemap: 'none',
|
|
422
|
+
packages: 'bundle',
|
|
423
|
+
})
|
|
424
|
+
|
|
425
|
+
if (!result.success) {
|
|
426
|
+
return { mtime, errors: result.logs.map(log => String(log)) }
|
|
427
|
+
}
|
|
529
428
|
|
|
530
|
-
const
|
|
531
|
-
|
|
532
|
-
|
|
429
|
+
const output = result.outputs.find(output => output.path.endsWith('.js')) || result.outputs[0]
|
|
430
|
+
if (!output) return { mtime, errors: ['Bun.build did not return a JavaScript output'] }
|
|
431
|
+
|
|
432
|
+
let code = await output.text()
|
|
433
|
+
const css = (await Promise.all(
|
|
434
|
+
result.outputs
|
|
435
|
+
.filter(output => output.path.endsWith('.css'))
|
|
436
|
+
.map(output => output.text())
|
|
437
|
+
)).join('\n')
|
|
438
|
+
if (css) {
|
|
439
|
+
const id = JSON.stringify('vendor:' + entrypoint)
|
|
440
|
+
code = [
|
|
441
|
+
`const __bimba_vendor_css_id = ${id};`,
|
|
442
|
+
`let __bimba_vendor_css = document.querySelector('style[data-bimba-css=' + JSON.stringify(__bimba_vendor_css_id) + ']');`,
|
|
443
|
+
`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); }`,
|
|
444
|
+
`__bimba_vendor_css.textContent = ${JSON.stringify(css)};`,
|
|
445
|
+
code,
|
|
446
|
+
].join('\n')
|
|
447
|
+
}
|
|
448
|
+
const bundled = { mtime, code }
|
|
449
|
+
_vendorCache.set(entrypoint, bundled)
|
|
450
|
+
return bundled
|
|
451
|
+
} catch (error) {
|
|
452
|
+
return { mtime: 0, errors: [error?.message || String(error)] }
|
|
533
453
|
}
|
|
534
|
-
|
|
535
|
-
return mappings
|
|
536
454
|
}
|
|
537
455
|
|
|
538
456
|
async function buildGeneratedImportMap() {
|
|
539
457
|
const imports = {}
|
|
540
|
-
const visited = new Set()
|
|
541
|
-
const queue = []
|
|
542
458
|
|
|
543
459
|
try {
|
|
544
460
|
const pkg = JSON.parse(await Bun.file('./package.json').text())
|
|
545
461
|
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)
|
|
462
|
+
for (const name of Object.keys(pkg[field] || {})) {
|
|
463
|
+
imports[name] ||= vendorUrl(name)
|
|
464
|
+
imports[name + '/'] ||= vendorPrefixUrl(name)
|
|
572
465
|
}
|
|
573
|
-
} else {
|
|
574
|
-
imports[name + '/'] = '/' + toPosix(path.join('node_modules', name)) + '/'
|
|
575
466
|
}
|
|
467
|
+
} catch(_) { /* no package.json */ }
|
|
576
468
|
|
|
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'
|
|
469
|
+
imports['imba'] ||= vendorUrl('imba')
|
|
470
|
+
imports['imba/'] ||= vendorPrefixUrl('imba')
|
|
471
|
+
imports['imba/runtime'] ||= vendorUrl('imba/runtime')
|
|
584
472
|
|
|
585
473
|
return { imports }
|
|
586
474
|
}
|
|
@@ -632,15 +520,6 @@ function renderImportMapTag(importMap) {
|
|
|
632
520
|
return `\t\t<script type="importmap">\n\t\t\t${JSON.stringify(importMap, null, '\t\t\t\t')}\n\t\t</script>`
|
|
633
521
|
}
|
|
634
522
|
|
|
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
523
|
// Rewrite production HTML for the dev server:
|
|
645
524
|
// strips existing importmap + data-entrypoint script, injects merged importmap +
|
|
646
525
|
// entrypoint module + HMR client before </head>.
|
|
@@ -786,16 +665,26 @@ export function serve(entrypoint, flags) {
|
|
|
786
665
|
if (server.upgrade(req)) return undefined
|
|
787
666
|
}
|
|
788
667
|
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
return new Response(transformHtml(html, entrypoint, generatedImportMap), {
|
|
795
|
-
headers: { 'Content-Type': 'text/html' },
|
|
796
|
-
})
|
|
668
|
+
if (pathname.startsWith('/__bimba_vendor__/')) {
|
|
669
|
+
const specifier = vendorSpecifierFromPath(pathname)
|
|
670
|
+
const bundled = specifier ? await bundleVendor(specifier) : null
|
|
671
|
+
if (bundled?.code) {
|
|
672
|
+
return new Response(bundled.code, { headers: { 'Content-Type': 'application/javascript' } })
|
|
797
673
|
}
|
|
798
674
|
|
|
675
|
+
return new Response((bundled?.errors || [`Could not bundle vendor module: ${specifier}`]).join('\n'), { status: 500 })
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
// HTML: index or any .html file
|
|
679
|
+
if (pathname === '/' || pathname.endsWith('.html')) {
|
|
680
|
+
const htmlFile = pathname === '/' ? htmlPath : '.' + pathname
|
|
681
|
+
let html = await Bun.file(htmlFile).text()
|
|
682
|
+
if (!generatedImportMap) generatedImportMap = await buildGeneratedImportMap()
|
|
683
|
+
return new Response(transformHtml(html, entrypoint, generatedImportMap), {
|
|
684
|
+
headers: { 'Content-Type': 'text/html' },
|
|
685
|
+
})
|
|
686
|
+
}
|
|
687
|
+
|
|
799
688
|
// Imba files: compile on demand and serve as JS
|
|
800
689
|
if (pathname.endsWith('.imba')) {
|
|
801
690
|
const filepath = '.' + pathname
|
|
@@ -838,31 +727,25 @@ export function serve(entrypoint, flags) {
|
|
|
838
727
|
}
|
|
839
728
|
}
|
|
840
729
|
|
|
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
|
-
}
|
|
730
|
+
// Direct node_modules URLs (from user import maps or explicit imports)
|
|
731
|
+
// are bundled through Bun too, so browser/cjs/exports handling stays
|
|
732
|
+
// in one place.
|
|
733
|
+
if (pathname.startsWith('/node_modules/')) {
|
|
734
|
+
const resolved = resolveFileCandidate('.' + pathname)
|
|
735
|
+
if (resolved?.endsWith('.imba')) {
|
|
736
|
+
const out = await compileFile(resolved)
|
|
737
|
+
if (out.errors?.length) return new Response(out.errors.map(e => e.message).join('\n'), { status: 500 })
|
|
738
|
+
return new Response(out.js, { headers: { 'Content-Type': 'application/javascript' } })
|
|
739
|
+
}
|
|
861
740
|
|
|
862
|
-
|
|
863
|
-
const
|
|
864
|
-
if (
|
|
741
|
+
if (resolved) {
|
|
742
|
+
const bundled = await bundleVendor(path.resolve(resolved))
|
|
743
|
+
if (bundled?.code) {
|
|
744
|
+
return new Response(bundled.code, { headers: { 'Content-Type': 'application/javascript' } })
|
|
745
|
+
}
|
|
746
|
+
return new Response((bundled?.errors || [`Could not bundle ${pathname}`]).join('\n'), { status: 500 })
|
|
865
747
|
}
|
|
748
|
+
}
|
|
866
749
|
|
|
867
750
|
// Static files: check htmlDir first (for assets relative to HTML), then root
|
|
868
751
|
const inHtmlDir = Bun.file(path.join(htmlDir, pathname))
|