bimba-cli 0.6.1 → 0.7.0
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 +98 -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,35 @@ function findHtml(flagHtml) {
|
|
|
410
408
|
return candidates.find(p => existsSync(p)) || './index.html';
|
|
411
409
|
}
|
|
412
410
|
|
|
411
|
+
// Resolve the ESM entry point for an npm package.
|
|
412
|
+
// Priority: exports["."].import → exports["."].default → module → main
|
|
413
|
+
function resolveEntry(depPkg) {
|
|
414
|
+
const exp = depPkg.exports;
|
|
415
|
+
if (exp) {
|
|
416
|
+
// exports: "./index.js" (string shorthand)
|
|
417
|
+
if (typeof exp === 'string') return exp;
|
|
418
|
+
// exports: { ".": { "import": "...", "default": "..." } }
|
|
419
|
+
const dot = exp['.'];
|
|
420
|
+
if (dot) {
|
|
421
|
+
if (typeof dot === 'string') return dot;
|
|
422
|
+
if (dot.import) return dot.import;
|
|
423
|
+
if (dot.default) return dot.default;
|
|
424
|
+
}
|
|
425
|
+
// exports: { "import": "...", "default": "..." } (no "." wrapper)
|
|
426
|
+
if (exp.import) return exp.import;
|
|
427
|
+
if (exp.default) return exp.default;
|
|
428
|
+
}
|
|
429
|
+
// Fallback to legacy fields
|
|
430
|
+
return depPkg.module || depPkg.browser || depPkg.main;
|
|
431
|
+
}
|
|
432
|
+
|
|
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
|
+
|
|
413
440
|
// Build an ES import map from package.json dependencies.
|
|
414
441
|
// Packages with an .imba entry point are served locally; others via esm.sh.
|
|
415
442
|
async function buildImportMap() {
|
|
@@ -423,12 +450,42 @@ async function buildImportMap() {
|
|
|
423
450
|
if (name === 'imba') continue;
|
|
424
451
|
try {
|
|
425
452
|
const depPkg = JSON.parse(await Bun.file(`./node_modules/${name}/package.json`).text());
|
|
426
|
-
const entry = depPkg
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
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
|
+
}
|
|
430
486
|
} catch(_) {
|
|
431
487
|
imports[name] = `https://esm.sh/${name}`;
|
|
488
|
+
imports[name + '/'] = `https://esm.sh/${name}/`;
|
|
432
489
|
}
|
|
433
490
|
}
|
|
434
491
|
} catch(_) { /* no package.json */ }
|
|
@@ -624,14 +681,42 @@ export function serve(entrypoint, flags) {
|
|
|
624
681
|
}
|
|
625
682
|
}
|
|
626
683
|
|
|
684
|
+
// CSS files imported from JS: wrap as a JS module that injects a <style> tag.
|
|
685
|
+
// Without this, `import './styles.css'` inside an ESM package fails because
|
|
686
|
+
// the browser expects a JS module response, not raw CSS.
|
|
687
|
+
if (pathname.endsWith('.css')) {
|
|
688
|
+
const cssFile = Bun.file('.' + pathname)
|
|
689
|
+
if (await cssFile.exists()) {
|
|
690
|
+
const css = await cssFile.text()
|
|
691
|
+
const id = JSON.stringify(pathname)
|
|
692
|
+
const js = [
|
|
693
|
+
`const id = ${id};`,
|
|
694
|
+
`let el = document.querySelector('style[data-bimba-css=' + JSON.stringify(id) + ']');`,
|
|
695
|
+
`if (!el) { el = document.createElement('style'); el.setAttribute('data-bimba-css', id); document.head.appendChild(el); }`,
|
|
696
|
+
`el.textContent = ${JSON.stringify(css)};`,
|
|
697
|
+
].join('\n')
|
|
698
|
+
return new Response(js, { headers: { 'Content-Type': 'application/javascript' } })
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
|
|
627
702
|
// Static files: check htmlDir first (for assets relative to HTML), then root
|
|
628
703
|
const inHtmlDir = Bun.file(path.join(htmlDir, pathname))
|
|
629
704
|
if (await inHtmlDir.exists()) return new Response(inHtmlDir)
|
|
630
705
|
const inRoot = Bun.file('.' + pathname)
|
|
631
706
|
if (await inRoot.exists()) return new Response(inRoot)
|
|
632
707
|
|
|
633
|
-
//
|
|
708
|
+
// Try .js / .mjs extension for extensionless paths (e.g. node_modules imports)
|
|
634
709
|
const lastSegment = pathname.split('/').pop()
|
|
710
|
+
if (!lastSegment.includes('.')) {
|
|
711
|
+
for (const ext of ['.js', '.mjs']) {
|
|
712
|
+
const withExt = Bun.file('.' + pathname + ext)
|
|
713
|
+
if (await withExt.exists()) return new Response(withExt, {
|
|
714
|
+
headers: { 'Content-Type': 'application/javascript' },
|
|
715
|
+
})
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
// SPA fallback for extension-less paths
|
|
635
720
|
if (!lastSegment.includes('.')) {
|
|
636
721
|
let html = await Bun.file(htmlPath).text()
|
|
637
722
|
if (!importMapTag) importMapTag = await buildImportMap()
|