bimba-cli 0.6.1 → 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/README.md +9 -3
- package/package.json +1 -1
- package/serve.js +180 -13
package/README.md
CHANGED
|
@@ -37,7 +37,9 @@ bunx bimba src/index.imba --serve --port 5200 --html public/index.html
|
|
|
37
37
|
**How it works:**
|
|
38
38
|
- Serves your HTML file and compiles `.imba` files on demand (no bundling step)
|
|
39
39
|
- Watches `src/` for changes and pushes updates over WebSocket
|
|
40
|
-
- Injects an importmap built from your `package.json` dependencies
|
|
40
|
+
- Injects an importmap built from your `package.json` dependencies (supports `exports`, `module`, `browser`, and `main` fields)
|
|
41
|
+
- CSS files imported from JS (e.g. `import 'some-lib/styles.css'`) are automatically wrapped as JS modules that inject `<style>` tags
|
|
42
|
+
- npm packages with ESM entry points are served from `node_modules` locally — no esm.sh proxy needed
|
|
41
43
|
- Injects an HMR client that swaps component prototypes without a full page reload
|
|
42
44
|
|
|
43
45
|
**HMR internals:**
|
|
@@ -48,10 +50,12 @@ Since Imba custom elements can't be registered twice (`customElements.define` th
|
|
|
48
50
|
|
|
49
51
|
After patching, bimba clears each element's Imba render cache (anonymous `Symbol` keys pointing to DOM nodes) and sets `innerHTML = ''`, so the new render method starts from a clean slate. Then `imba.commit()` triggers a re-render of all mounted components.
|
|
50
52
|
|
|
51
|
-
CSS is handled automatically: Imba's runtime calls `imba_styles.register()` during module execution, which updates the `<style>` tag in place — no extra DOM work needed.
|
|
53
|
+
CSS is handled automatically: Imba's runtime calls `imba_styles.register()` during module execution, which updates the `<style>` tag in place — no extra DOM work needed. CSS files from npm packages (e.g. `import 'pkg/styles.css'`) are served as JS modules that inject and update `<style>` tags.
|
|
52
54
|
|
|
53
55
|
Duplicate root elements (caused by `imba.mount()` running again on re-import) are removed by a dedup pass over `document.body.children` before any other HMR logic runs.
|
|
54
56
|
|
|
57
|
+
**Smart HMR:** bimba detects whether a change affects the template structure (adding/removing elements) or just CSS/logic. CSS-only and logic-only changes patch prototypes in place without wiping innerHTML — preserving input focus, scroll position, and open popups. Template-structural changes do a full wipe-and-rerender to ensure correctness.
|
|
58
|
+
|
|
55
59
|
**HTML setup:** add a `data-entrypoint` attribute to the script tag that loads your bundle. The dev server will replace it with your `.imba` entrypoint and inject the importmap above it:
|
|
56
60
|
|
|
57
61
|
```html
|
|
@@ -66,7 +70,9 @@ Duplicate root elements (caused by `imba.mount()` running again on re-import) ar
|
|
|
66
70
|
|
|
67
71
|
`--html <path>` — path to your HTML file (auto-detected from `./index.html`, `./public/index.html`, `./src/index.html` if omitted)
|
|
68
72
|
|
|
69
|
-
Static files are resolved relative to the HTML file's directory first, then from the project root (for `node_modules`, `src`, etc.).
|
|
73
|
+
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.
|
|
74
|
+
|
|
75
|
+
**npm package resolution:** The import map is built by reading each dependency's `package.json`. The resolution order is: `exports["."].import` → `exports["."].default` → `module` → `browser` → `main`. Packages with subpath exports (e.g. `"./styles.css"`) get individual import map entries. CJS-only packages (no ESM entry) are proxied through esm.sh.
|
|
70
76
|
|
|
71
77
|
---
|
|
72
78
|
|
package/package.json
CHANGED
package/serve.js
CHANGED
|
@@ -28,7 +28,6 @@ const hmrClient = `
|
|
|
28
28
|
const _newClasses = new Map(); // tagName → latest class from HMR import
|
|
29
29
|
const _oldNs = new Map(); // tagName → previous _ns_ (saved before _patchClass wipes it)
|
|
30
30
|
let _collector = null; // when set, captures tag names defined during one HMR import
|
|
31
|
-
let _stableSlots = false; // when true, _patchClass skips Symbol-keyed props (CSS-only HMR)
|
|
32
31
|
|
|
33
32
|
customElements.define = function(name, cls, opts) {
|
|
34
33
|
if (_collector) _collector.push(name);
|
|
@@ -42,9 +41,8 @@ const hmrClient = `
|
|
|
42
41
|
if (target) {
|
|
43
42
|
// Save old _ns_ before _patchClass overwrites prototype descriptors
|
|
44
43
|
if (target.prototype._ns_) _oldNs.set(name, target.prototype._ns_);
|
|
45
|
-
//
|
|
46
|
-
|
|
47
|
-
if (!_stableSlots) _patchClass(target, cls);
|
|
44
|
+
// Always patch: even CSS-only changes may update methods.
|
|
45
|
+
_patchClass(target, cls);
|
|
48
46
|
}
|
|
49
47
|
}
|
|
50
48
|
};
|
|
@@ -109,12 +107,10 @@ const hmrClient = `
|
|
|
109
107
|
const collected = [];
|
|
110
108
|
const prev = _collector;
|
|
111
109
|
_collector = collected;
|
|
112
|
-
_stableSlots = (slots === 'stable');
|
|
113
110
|
try {
|
|
114
111
|
await import('/' + file + '?t=' + Date.now());
|
|
115
112
|
} finally {
|
|
116
113
|
_collector = prev;
|
|
117
|
-
_stableSlots = false;
|
|
118
114
|
}
|
|
119
115
|
|
|
120
116
|
// Sync _ns_ (CSS namespace) from the new classes. imba_defineTag sets
|
|
@@ -141,8 +137,10 @@ const hmrClient = `
|
|
|
141
137
|
}
|
|
142
138
|
|
|
143
139
|
// Destructive HMR: wipe inner DOM and re-render each collected tag.
|
|
144
|
-
// Skip when slots === 'stable' (CSS-only
|
|
145
|
-
//
|
|
140
|
+
// Skip when slots === 'stable' — template structure unchanged (CSS-only
|
|
141
|
+
// or logic-only edit that didn't add/remove elements), so wiping innerHTML
|
|
142
|
+
// would destroy DOM state (inputs, focus, popups) for nothing.
|
|
143
|
+
// _patchClass already ran above, so methods are up-to-date either way.
|
|
146
144
|
if (slots !== 'stable') {
|
|
147
145
|
for (const tag of collected) {
|
|
148
146
|
const els = document.querySelectorAll(tag);
|
|
@@ -410,6 +408,121 @@ function findHtml(flagHtml) {
|
|
|
410
408
|
return candidates.find(p => existsSync(p)) || './index.html';
|
|
411
409
|
}
|
|
412
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
|
+
|
|
504
|
+
// Resolve the ESM entry point for an npm package.
|
|
505
|
+
// Priority: exports["."].import → exports["."].default → module → main
|
|
506
|
+
function resolveEntry(depPkg) {
|
|
507
|
+
const exp = depPkg.exports;
|
|
508
|
+
if (exp) {
|
|
509
|
+
// exports: "./index.js" (string shorthand)
|
|
510
|
+
if (typeof exp === 'string') return exp;
|
|
511
|
+
// exports: { ".": { "import": "...", "default": "..." } }
|
|
512
|
+
const dot = exp['.'];
|
|
513
|
+
if (dot) {
|
|
514
|
+
if (typeof dot === 'string') return dot;
|
|
515
|
+
if (dot.import) return dot.import;
|
|
516
|
+
if (dot.default) return dot.default;
|
|
517
|
+
}
|
|
518
|
+
// exports: { "import": "...", "default": "..." } (no "." wrapper)
|
|
519
|
+
if (exp.import) return exp.import;
|
|
520
|
+
if (exp.default) return exp.default;
|
|
521
|
+
}
|
|
522
|
+
// Fallback to legacy fields
|
|
523
|
+
return depPkg.module || depPkg.browser || depPkg.main;
|
|
524
|
+
}
|
|
525
|
+
|
|
413
526
|
// Build an ES import map from package.json dependencies.
|
|
414
527
|
// Packages with an .imba entry point are served locally; others via esm.sh.
|
|
415
528
|
async function buildImportMap() {
|
|
@@ -423,12 +536,23 @@ async function buildImportMap() {
|
|
|
423
536
|
if (name === 'imba') continue;
|
|
424
537
|
try {
|
|
425
538
|
const depPkg = JSON.parse(await Bun.file(`./node_modules/${name}/package.json`).text());
|
|
426
|
-
const entry = depPkg
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
539
|
+
const entry = resolveEntry(depPkg);
|
|
540
|
+
if (entry && !entry.startsWith('http')) {
|
|
541
|
+
// Local package — serve from node_modules
|
|
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".
|
|
547
|
+
imports[name + '/'] = `/node_modules/${name}/`;
|
|
548
|
+
} else {
|
|
549
|
+
// No resolvable entry — use esm.sh to auto-wrap
|
|
550
|
+
imports[name] = `https://esm.sh/${name}`;
|
|
551
|
+
imports[name + '/'] = `https://esm.sh/${name}/`;
|
|
552
|
+
}
|
|
430
553
|
} catch(_) {
|
|
431
554
|
imports[name] = `https://esm.sh/${name}`;
|
|
555
|
+
imports[name + '/'] = `https://esm.sh/${name}/`;
|
|
432
556
|
}
|
|
433
557
|
}
|
|
434
558
|
} catch(_) { /* no package.json */ }
|
|
@@ -624,14 +748,57 @@ export function serve(entrypoint, flags) {
|
|
|
624
748
|
}
|
|
625
749
|
}
|
|
626
750
|
|
|
751
|
+
// CSS files imported from JS: wrap as a JS module that injects a <style> tag.
|
|
752
|
+
// Without this, `import './styles.css'` inside an ESM package fails because
|
|
753
|
+
// the browser expects a JS module response, not raw CSS.
|
|
754
|
+
if (pathname.endsWith('.css')) {
|
|
755
|
+
const cssFile = Bun.file('.' + pathname)
|
|
756
|
+
if (await cssFile.exists()) {
|
|
757
|
+
const css = await cssFile.text()
|
|
758
|
+
const id = JSON.stringify(pathname)
|
|
759
|
+
const js = [
|
|
760
|
+
`const id = ${id};`,
|
|
761
|
+
`let el = document.querySelector('style[data-bimba-css=' + JSON.stringify(id) + ']');`,
|
|
762
|
+
`if (!el) { el = document.createElement('style'); el.setAttribute('data-bimba-css', id); document.head.appendChild(el); }`,
|
|
763
|
+
`el.textContent = ${JSON.stringify(css)};`,
|
|
764
|
+
].join('\n')
|
|
765
|
+
return new Response(js, { headers: { 'Content-Type': 'application/javascript' } })
|
|
766
|
+
}
|
|
767
|
+
}
|
|
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
|
+
|
|
627
782
|
// Static files: check htmlDir first (for assets relative to HTML), then root
|
|
628
783
|
const inHtmlDir = Bun.file(path.join(htmlDir, pathname))
|
|
629
784
|
if (await inHtmlDir.exists()) return new Response(inHtmlDir)
|
|
630
785
|
const inRoot = Bun.file('.' + pathname)
|
|
631
786
|
if (await inRoot.exists()) return new Response(inRoot)
|
|
632
787
|
|
|
633
|
-
//
|
|
788
|
+
// Try .js / .mjs extension for extensionless paths (e.g. node_modules imports)
|
|
634
789
|
const lastSegment = pathname.split('/').pop()
|
|
790
|
+
if (!lastSegment.includes('.')) {
|
|
791
|
+
// For node_modules, ESM resolution above already handles extensionless
|
|
792
|
+
// paths via wildcard exports matching (tries subpath + '.js').
|
|
793
|
+
for (const ext of ['.js', '.mjs']) {
|
|
794
|
+
const withExt = Bun.file('.' + pathname + ext)
|
|
795
|
+
if (await withExt.exists()) return new Response(withExt, {
|
|
796
|
+
headers: { 'Content-Type': 'application/javascript' },
|
|
797
|
+
})
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
// SPA fallback for extension-less paths
|
|
635
802
|
if (!lastSegment.includes('.')) {
|
|
636
803
|
let html = await Bun.file(htmlPath).text()
|
|
637
804
|
if (!importMapTag) importMapTag = await buildImportMap()
|