bimba-cli 0.7.9 → 0.7.10
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 +2 -2
- package/index.js +28 -16
- package/package.json +2 -2
- package/serve.js +316 -159
package/README.md
CHANGED
|
@@ -82,7 +82,7 @@ Static files are resolved relative to the HTML file's directory first, then from
|
|
|
82
82
|
|
|
83
83
|
To compile and bundle your source code from .imba to .js:
|
|
84
84
|
```bash
|
|
85
|
-
bunx bimba src/index.imba --outdir public/js
|
|
85
|
+
bunx bimba src/index.imba --outdir public/js
|
|
86
86
|
```
|
|
87
87
|
|
|
88
88
|
With watch:
|
|
@@ -100,7 +100,7 @@ bunx bimba src/index.imba --outdir public/js --watch --clearcache
|
|
|
100
100
|
|
|
101
101
|
`--clearcache` — delete the cache directory on exit (Ctrl+C). Works only in watch mode.
|
|
102
102
|
|
|
103
|
-
`--minify` —
|
|
103
|
+
`--no-minify` — disable minification. Bundle mode minifies by default.
|
|
104
104
|
|
|
105
105
|
`--sourcemap <inline|external|none>` — how to include source maps in the output (default: `none`).
|
|
106
106
|
|
package/index.js
CHANGED
|
@@ -14,7 +14,7 @@ let entrypoint = ''
|
|
|
14
14
|
|
|
15
15
|
try {
|
|
16
16
|
const { values, positionals } = parseArgs({
|
|
17
|
-
args: Bun.argv,
|
|
17
|
+
args: Bun.argv.slice(2),
|
|
18
18
|
options: {
|
|
19
19
|
watch: { type: 'boolean' },
|
|
20
20
|
outdir: { type: 'string' },
|
|
@@ -29,11 +29,12 @@ try {
|
|
|
29
29
|
port: { type: 'string' },
|
|
30
30
|
html: { type: 'string' },
|
|
31
31
|
},
|
|
32
|
+
allowNegative: true,
|
|
32
33
|
strict: true,
|
|
33
34
|
allowPositionals: true,
|
|
34
35
|
});
|
|
35
36
|
flags = values;
|
|
36
|
-
entrypoint =
|
|
37
|
+
entrypoint = positionals[0] || '';
|
|
37
38
|
}
|
|
38
39
|
catch (error) {
|
|
39
40
|
if (error instanceof Error)
|
|
@@ -43,16 +44,21 @@ catch (error) {
|
|
|
43
44
|
process.exit(0);
|
|
44
45
|
}
|
|
45
46
|
|
|
46
|
-
|
|
47
|
-
const bunfigPath = path.join(process.cwd(), 'bunfig.toml');
|
|
48
|
-
const preloadLine = 'preload = ["bimba-cli/plugin.js"]';
|
|
49
|
-
|
|
50
|
-
fs.
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
fs.appendFileSync(bunfigPath, (content.endsWith('\n') ? '' : '\n') + preloadLine + '\n');
|
|
47
|
+
function ensureBunfigPreload() {
|
|
48
|
+
const bunfigPath = path.join(process.cwd(), 'bunfig.toml');
|
|
49
|
+
const preloadLine = 'preload = ["bimba-cli/plugin.js"]';
|
|
50
|
+
|
|
51
|
+
if (!fs.existsSync(bunfigPath)) {
|
|
52
|
+
console.log(theme.action("note: ") + theme.filename("bunfig.toml was not found, so bimba left it unchanged."));
|
|
53
|
+
console.log(theme.action(" ") + `Add ${theme.flags(preloadLine)} manually if you want Bun to preload the plugin.`);
|
|
54
|
+
return;
|
|
55
55
|
}
|
|
56
|
+
|
|
57
|
+
const content = fs.readFileSync(bunfigPath, 'utf8');
|
|
58
|
+
if (content.includes(preloadLine)) return;
|
|
59
|
+
|
|
60
|
+
console.log(theme.action("note: ") + theme.filename("bunfig.toml already exists, so bimba did not edit it automatically."));
|
|
61
|
+
console.log(theme.action(" ") + `Add ${theme.flags(preloadLine)} manually if you want Bun to preload the plugin.`);
|
|
56
62
|
}
|
|
57
63
|
|
|
58
64
|
// help: more on bun building params here: https://bun.sh/docs/bundler
|
|
@@ -62,7 +68,7 @@ if(flags.help) {
|
|
|
62
68
|
console.log("For example like this: "+theme.filedir('bimba file.imba --outdir public'));
|
|
63
69
|
console.log("");
|
|
64
70
|
console.log(" "+theme.flags('--outdir <folder>')+" Compile imba files to the specified folder");
|
|
65
|
-
console.log(" "+theme.flags('--minify')+"
|
|
71
|
+
console.log(" "+theme.flags('--no-minify')+" Disable minification for compiled .js files");
|
|
66
72
|
console.log(" "+theme.flags('--sourcemap <inline|external|none>')+" How should sourcemap files be included in the .js");
|
|
67
73
|
console.log(" "+theme.flags('--target <browser|node>')+" Target platform for both Imba compiler and Bun bundler");
|
|
68
74
|
console.log(" "+theme.flags('--external <package>')+" Exclude package from bundle (repeatable, e.g. --external ws --external node-pty)");
|
|
@@ -88,6 +94,7 @@ if (flags.serve) {
|
|
|
88
94
|
console.log("");
|
|
89
95
|
process.exit(1);
|
|
90
96
|
}
|
|
97
|
+
ensureBunfigPreload();
|
|
91
98
|
serve(entrypoint, { port: parseInt(flags.port) || 5200, html: flags.html });
|
|
92
99
|
}
|
|
93
100
|
// no entrypoint or outdir
|
|
@@ -100,7 +107,9 @@ else if(!entrypoint || !flags.outdir) {
|
|
|
100
107
|
}
|
|
101
108
|
// build
|
|
102
109
|
else {
|
|
103
|
-
|
|
110
|
+
ensureBunfigPreload();
|
|
111
|
+
const success = await bundle();
|
|
112
|
+
if (!success && !flags.watch) process.exit(1);
|
|
104
113
|
watch(bundle);
|
|
105
114
|
}
|
|
106
115
|
|
|
@@ -126,7 +135,7 @@ async function bundle() {
|
|
|
126
135
|
|
|
127
136
|
if (!fs.existsSync(entrypoint)) {
|
|
128
137
|
console.log(theme.failure('Error.') + ` The specified entrypoint does not exist: ${theme.filedir(entrypoint)}`);
|
|
129
|
-
|
|
138
|
+
return false;
|
|
130
139
|
}
|
|
131
140
|
|
|
132
141
|
stats.failed = 0
|
|
@@ -150,7 +159,7 @@ async function bundle() {
|
|
|
150
159
|
outdir: flags.outdir,
|
|
151
160
|
target: buildTarget,
|
|
152
161
|
sourcemap: flags.sourcemap || 'none',
|
|
153
|
-
minify: true,
|
|
162
|
+
minify: flags.minify ?? true,
|
|
154
163
|
splitting: flags.splitting || false,
|
|
155
164
|
plugins: [imbaPlugin]
|
|
156
165
|
};
|
|
@@ -182,6 +191,8 @@ async function bundle() {
|
|
|
182
191
|
console.log(log);
|
|
183
192
|
}
|
|
184
193
|
}
|
|
194
|
+
|
|
195
|
+
return result.success && !stats.failed && !stats.errors;
|
|
185
196
|
}
|
|
186
197
|
catch(error) {
|
|
187
198
|
console.log(theme.folder("──────────────────────────────────────────────────────────────────────"));
|
|
@@ -189,8 +200,9 @@ async function bundle() {
|
|
|
189
200
|
console.log(error)
|
|
190
201
|
console.log(theme.folder("──────────────────────────────────────────────────────────────────────"));
|
|
191
202
|
console.log(theme.failure(" Failure ") + theme.filename(' Bun found an error in the compiled JS file'))
|
|
203
|
+
return false;
|
|
192
204
|
}
|
|
193
205
|
finally {
|
|
194
206
|
bundling = false;
|
|
195
207
|
};
|
|
196
|
-
}
|
|
208
|
+
}
|
package/package.json
CHANGED
package/serve.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { serve as bunServe } from 'bun'
|
|
1
|
+
import { serve as bunServe, Glob } from 'bun'
|
|
2
2
|
import * as compiler from 'imba/compiler'
|
|
3
|
-
import { watch, existsSync } from 'fs'
|
|
3
|
+
import { watch, existsSync, statSync } from 'fs'
|
|
4
4
|
import path from 'path'
|
|
5
5
|
import { theme } from './utils.js'
|
|
6
6
|
import { printerr } from './plugin.js'
|
|
@@ -256,57 +256,6 @@ const _compileCache = new Map() // filepath → { mtime, result }
|
|
|
256
256
|
const _prevJs = new Map() // filepath → compiled js — for change detection
|
|
257
257
|
const _prevSlots = new Map() // filepath → previous symbol slot count
|
|
258
258
|
|
|
259
|
-
// ─── Import dependency graph ──────────────────────────────────────────────────
|
|
260
|
-
//
|
|
261
|
-
// When a non-tag module (utility functions, constants, shared state) is edited,
|
|
262
|
-
// the existing class-prototype patching does nothing for the modules that
|
|
263
|
-
// imported it — they hold their own captured references. To make those
|
|
264
|
-
// updates flow into the UI, we track who imports whom and, on every change,
|
|
265
|
-
// re-broadcast updates for the transitive importer set. The client's existing
|
|
266
|
-
// HMR queue then re-imports each in turn; their top-level code reruns, picks
|
|
267
|
-
// up the new symbols, and any tag re-registrations patch instances in place.
|
|
268
|
-
//
|
|
269
|
-
// Keys are absolute, normalized paths (path.resolve). Edges are added during
|
|
270
|
-
// compilation by scanning the produced JS for relative .imba imports.
|
|
271
|
-
|
|
272
|
-
const _imports = new Map() // absFile → Set<absFile> (what it imports)
|
|
273
|
-
const _importers = new Map() // absFile → Set<absFile> (who imports it)
|
|
274
|
-
|
|
275
|
-
function extractImports(js, fromAbs) {
|
|
276
|
-
const dir = path.dirname(fromAbs)
|
|
277
|
-
const out = new Set()
|
|
278
|
-
const re = /(?:^|[\s;])(?:import|from)\s*['"]([^'"]+)['"]/g
|
|
279
|
-
let m
|
|
280
|
-
while ((m = re.exec(js))) {
|
|
281
|
-
const spec = m[1]
|
|
282
|
-
if (!spec.startsWith('.') && !spec.startsWith('/')) continue
|
|
283
|
-
if (!spec.endsWith('.imba')) continue
|
|
284
|
-
const resolved = spec.startsWith('/')
|
|
285
|
-
? path.resolve('.' + spec)
|
|
286
|
-
: path.resolve(dir, spec)
|
|
287
|
-
out.add(resolved)
|
|
288
|
-
}
|
|
289
|
-
return out
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
function updateImportGraph(fromAbs, newDeps) {
|
|
293
|
-
const old = _imports.get(fromAbs)
|
|
294
|
-
if (old) {
|
|
295
|
-
for (const d of old) {
|
|
296
|
-
if (newDeps.has(d)) continue
|
|
297
|
-
const set = _importers.get(d)
|
|
298
|
-
if (set) { set.delete(fromAbs); if (!set.size) _importers.delete(d) }
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
for (const d of newDeps) {
|
|
302
|
-
let set = _importers.get(d)
|
|
303
|
-
if (!set) { set = new Set(); _importers.set(d, set) }
|
|
304
|
-
set.add(fromAbs)
|
|
305
|
-
}
|
|
306
|
-
_imports.set(fromAbs, newDeps)
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
|
|
310
259
|
// Imba compiles tag render-cache slots as anonymous local Symbols at module top
|
|
311
260
|
// level: `var $4 = Symbol(), $11 = Symbol(), ...; let c$0 = Symbol();`. Each
|
|
312
261
|
// re-import of the file creates fresh Symbol objects, so old slot data on live
|
|
@@ -373,7 +322,6 @@ async function compileFile(filepath) {
|
|
|
373
322
|
const prev = _prevSlots.get(abs)
|
|
374
323
|
result.slots = (prev === undefined || prev === slotCount) ? 'stable' : 'shifted'
|
|
375
324
|
_prevSlots.set(abs, slotCount)
|
|
376
|
-
updateImportGraph(abs, extractImports(js, abs))
|
|
377
325
|
}
|
|
378
326
|
|
|
379
327
|
// Bake errors as an own property so caching/spreading preserves them.
|
|
@@ -408,69 +356,306 @@ async function readPkgJson(pkgDir) {
|
|
|
408
356
|
}
|
|
409
357
|
}
|
|
410
358
|
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
return `var module = { exports: {} }, exports = module.exports;\n${code}\nexport default module.exports;\nexport { module };`
|
|
359
|
+
function toPosix(filepath) {
|
|
360
|
+
return filepath.replaceAll('\\', '/')
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function toBrowserPath(filepath) {
|
|
364
|
+
return '/' + toPosix(path.relative(process.cwd(), path.resolve(filepath)))
|
|
418
365
|
}
|
|
419
366
|
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
if (typeof exp === 'string') return exp;
|
|
427
|
-
// exports: { ".": { "import": "...", "default": "..." } }
|
|
428
|
-
const dot = exp['.'];
|
|
429
|
-
if (dot) {
|
|
430
|
-
if (typeof dot === 'string') return dot;
|
|
431
|
-
if (dot.import) return dot.import;
|
|
432
|
-
if (dot.default) return dot.default;
|
|
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('/'),
|
|
433
373
|
}
|
|
434
|
-
// exports: { "import": "...", "default": "..." } (no "." wrapper)
|
|
435
|
-
if (exp.import) return exp.import;
|
|
436
|
-
if (exp.default) return exp.default;
|
|
437
374
|
}
|
|
438
|
-
|
|
439
|
-
|
|
375
|
+
|
|
376
|
+
const [name, ...rest] = specifier.split('/')
|
|
377
|
+
return { name, subpath: rest.join('/') }
|
|
440
378
|
}
|
|
441
379
|
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
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
|
|
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
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function resolveFileCandidate(filepath) {
|
|
455
|
+
const candidates = [
|
|
456
|
+
filepath,
|
|
457
|
+
filepath + '.js',
|
|
458
|
+
filepath + '.mjs',
|
|
459
|
+
filepath + '.cjs',
|
|
460
|
+
filepath + '.imba',
|
|
461
|
+
filepath + '.css',
|
|
462
|
+
path.join(filepath, 'index.js'),
|
|
463
|
+
path.join(filepath, 'index.mjs'),
|
|
464
|
+
path.join(filepath, 'index.cjs'),
|
|
465
|
+
path.join(filepath, 'index.imba'),
|
|
466
|
+
]
|
|
467
|
+
|
|
468
|
+
for (const candidate of candidates) {
|
|
469
|
+
if (!existsSync(candidate)) continue
|
|
470
|
+
try {
|
|
471
|
+
if (statSync(candidate).isFile()) return candidate
|
|
472
|
+
} catch (_) {
|
|
473
|
+
// ignore vanished files and continue resolving
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
return null
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
async function resolvePackageFile(specifier) {
|
|
481
|
+
const { name, subpath } = parsePackageSpecifier(specifier)
|
|
482
|
+
if (!name) return null
|
|
483
|
+
|
|
484
|
+
const pkgDir = path.join(process.cwd(), 'node_modules', name)
|
|
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
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
async function resolveNodeModulesRequest(pathname) {
|
|
496
|
+
const specifier = pathname.replace(/^\/node_modules\//, '')
|
|
497
|
+
if (!specifier) return null
|
|
498
|
+
|
|
499
|
+
const direct = resolveFileCandidate(path.join(process.cwd(), 'node_modules', specifier))
|
|
500
|
+
if (direct) return path.resolve(direct)
|
|
501
|
+
|
|
502
|
+
return resolvePackageFile(specifier)
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
function exportSpecifiers(name, exportsField) {
|
|
506
|
+
if (!exportsField || typeof exportsField === 'string' || Array.isArray(exportsField) || isConditionMap(exportsField)) {
|
|
507
|
+
return []
|
|
508
|
+
}
|
|
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
|
+
}
|
|
537
|
+
|
|
538
|
+
async function buildGeneratedImportMap() {
|
|
539
|
+
const imports = {}
|
|
540
|
+
const visited = new Set()
|
|
541
|
+
const queue = []
|
|
542
|
+
|
|
451
543
|
try {
|
|
452
|
-
const pkg = JSON.parse(await Bun.file('./package.json').text())
|
|
453
|
-
for (const
|
|
454
|
-
|
|
455
|
-
imports[name] = `/node_modules/${name}/`;
|
|
456
|
-
imports[name + '/'] = `/node_modules/${name}/`;
|
|
544
|
+
const pkg = JSON.parse(await Bun.file('./package.json').text())
|
|
545
|
+
for (const field of ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies']) {
|
|
546
|
+
queue.push(...Object.keys(pkg[field] || {}))
|
|
457
547
|
}
|
|
458
548
|
} catch(_) { /* no package.json */ }
|
|
459
549
|
|
|
460
|
-
|
|
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)
|
|
572
|
+
}
|
|
573
|
+
} else {
|
|
574
|
+
imports[name + '/'] = '/' + toPosix(path.join('node_modules', name)) + '/'
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
for (const field of ['dependencies', 'peerDependencies', 'optionalDependencies']) {
|
|
578
|
+
queue.push(...Object.keys(depPkg[field] || {}))
|
|
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'
|
|
584
|
+
|
|
585
|
+
return { imports }
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
function extractUserImportMap(html) {
|
|
589
|
+
const merged = { imports: {}, scopes: {} }
|
|
590
|
+
|
|
591
|
+
html = html.replace(/<script\s+type=["']importmap["'][^>]*>([\s\S]*?)<\/script>/gi, (_match, json) => {
|
|
592
|
+
try {
|
|
593
|
+
const parsed = JSON.parse(json)
|
|
594
|
+
Object.assign(merged.imports, parsed.imports || {})
|
|
595
|
+
for (const [scope, specifiers] of Object.entries(parsed.scopes || {})) {
|
|
596
|
+
merged.scopes[scope] = { ...(merged.scopes[scope] || {}), ...specifiers }
|
|
597
|
+
}
|
|
598
|
+
} catch (_) {
|
|
599
|
+
// ignore invalid import maps and keep serving the page
|
|
600
|
+
}
|
|
601
|
+
return ''
|
|
602
|
+
})
|
|
603
|
+
|
|
604
|
+
if (!Object.keys(merged.scopes).length) delete merged.scopes
|
|
605
|
+
return { html, importMap: merged }
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
function mergeImportMaps(generated, user) {
|
|
609
|
+
const merged = {
|
|
610
|
+
imports: { ...(generated.imports || {}), ...(user.imports || {}) },
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
const scopeKeys = new Set([
|
|
614
|
+
...Object.keys(generated.scopes || {}),
|
|
615
|
+
...Object.keys(user.scopes || {}),
|
|
616
|
+
])
|
|
617
|
+
|
|
618
|
+
if (scopeKeys.size) {
|
|
619
|
+
merged.scopes = {}
|
|
620
|
+
for (const key of scopeKeys) {
|
|
621
|
+
merged.scopes[key] = {
|
|
622
|
+
...((generated.scopes || {})[key] || {}),
|
|
623
|
+
...((user.scopes || {})[key] || {}),
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
return merged
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
function renderImportMapTag(importMap) {
|
|
632
|
+
return `\t\t<script type="importmap">\n\t\t\t${JSON.stringify(importMap, null, '\t\t\t\t')}\n\t\t</script>`
|
|
633
|
+
}
|
|
634
|
+
|
|
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 };`
|
|
461
642
|
}
|
|
462
643
|
|
|
463
644
|
// Rewrite production HTML for the dev server:
|
|
464
|
-
// strips existing importmap + data-entrypoint script, injects importmap +
|
|
645
|
+
// strips existing importmap + data-entrypoint script, injects merged importmap +
|
|
465
646
|
// entrypoint module + HMR client before </head>.
|
|
466
|
-
function transformHtml(html, entrypoint,
|
|
467
|
-
|
|
468
|
-
html = html
|
|
469
|
-
|
|
647
|
+
function transformHtml(html, entrypoint, generatedImportMap) {
|
|
648
|
+
const extracted = extractUserImportMap(html)
|
|
649
|
+
html = extracted.html
|
|
650
|
+
html = html.replace(/<script([^>]*)\bdata-entrypoint\b([^>]*)><\/script>/gi, '')
|
|
651
|
+
|
|
652
|
+
const entryUrl = '/' + entrypoint.replace(/^\.\//, '').replaceAll('\\', '/')
|
|
653
|
+
const importMapTag = renderImportMapTag(mergeImportMaps(generatedImportMap, extracted.importMap))
|
|
654
|
+
|
|
470
655
|
html = html.replace('</head>',
|
|
471
656
|
`${importMapTag}\n\t\t<script type='module' src='${entryUrl}'></script>\n${hmrClient}\n\t</head>`
|
|
472
|
-
)
|
|
473
|
-
return html
|
|
657
|
+
)
|
|
658
|
+
return html
|
|
474
659
|
}
|
|
475
660
|
|
|
476
661
|
// ─── Dev server ───────────────────────────────────────────────────────────────
|
|
@@ -481,7 +666,7 @@ export function serve(entrypoint, flags) {
|
|
|
481
666
|
const htmlDir = path.dirname(htmlPath)
|
|
482
667
|
const srcDir = path.dirname(entrypoint)
|
|
483
668
|
const sockets = new Set()
|
|
484
|
-
let
|
|
669
|
+
let generatedImportMap = null
|
|
485
670
|
|
|
486
671
|
// ── Status line (prints current compile result, fades out on success) ──────
|
|
487
672
|
|
|
@@ -601,15 +786,15 @@ export function serve(entrypoint, flags) {
|
|
|
601
786
|
if (server.upgrade(req)) return undefined
|
|
602
787
|
}
|
|
603
788
|
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
789
|
+
// HTML: index or any .html file
|
|
790
|
+
if (pathname === '/' || pathname.endsWith('.html')) {
|
|
791
|
+
const htmlFile = pathname === '/' ? htmlPath : '.' + pathname
|
|
792
|
+
let html = await Bun.file(htmlFile).text()
|
|
793
|
+
if (!generatedImportMap) generatedImportMap = await buildGeneratedImportMap()
|
|
794
|
+
return new Response(transformHtml(html, entrypoint, generatedImportMap), {
|
|
795
|
+
headers: { 'Content-Type': 'text/html' },
|
|
796
|
+
})
|
|
797
|
+
}
|
|
613
798
|
|
|
614
799
|
// Imba files: compile on demand and serve as JS
|
|
615
800
|
if (pathname.endsWith('.imba')) {
|
|
@@ -653,15 +838,14 @@ export function serve(entrypoint, flags) {
|
|
|
653
838
|
}
|
|
654
839
|
}
|
|
655
840
|
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
if (filePath.endsWith('.imba')) {
|
|
841
|
+
// node_modules: entry point resolution, .imba compilation, CJS→ESM wrapping.
|
|
842
|
+
// The import map points to concrete module URLs when possible, but we
|
|
843
|
+
// still resolve package-style /node_modules/pkg[/subpath] requests here
|
|
844
|
+
// so user-defined import maps and manual URLs keep working.
|
|
845
|
+
if (pathname.startsWith('/node_modules/')) {
|
|
846
|
+
// Serve a resolved file: compile .imba, wrap CJS as ESM, pass ESM through
|
|
847
|
+
const serveResolved = async (filePath) => {
|
|
848
|
+
if (filePath.endsWith('.imba')) {
|
|
665
849
|
const f = Bun.file(filePath)
|
|
666
850
|
if (!(await f.exists())) return null
|
|
667
851
|
const out = await compileFile(filePath)
|
|
@@ -671,41 +855,14 @@ export function serve(entrypoint, flags) {
|
|
|
671
855
|
const f = Bun.file(filePath)
|
|
672
856
|
if (!(await f.exists())) return null
|
|
673
857
|
const code = await f.text()
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
}
|
|
677
|
-
|
|
678
|
-
// Resolve entry point for root package requests (/node_modules/pkg/ or /node_modules/pkg)
|
|
679
|
-
const parts = pathname.slice(1).split('/') // ['node_modules', 'pkg', ...]
|
|
680
|
-
const isScoped = parts[1]?.startsWith('@')
|
|
681
|
-
const pkgParts = isScoped ? 3 : 2 // node_modules/@scope/pkg or node_modules/pkg
|
|
682
|
-
const subParts = parts.slice(pkgParts)
|
|
683
|
-
const isRootRequest = subParts.length === 0 || (subParts.length === 1 && subParts[0] === '')
|
|
684
|
-
|
|
685
|
-
if (isRootRequest) {
|
|
686
|
-
const pkgDir = './' + parts.slice(0, pkgParts).join('/')
|
|
687
|
-
const depPkg = await readPkgJson(pkgDir)
|
|
688
|
-
if (depPkg) {
|
|
689
|
-
const entry = resolveEntry(depPkg)
|
|
690
|
-
if (entry) {
|
|
691
|
-
const resp = await serveResolved(path.join(pkgDir, entry))
|
|
692
|
-
if (resp) return resp
|
|
693
|
-
}
|
|
858
|
+
const wrapped = wrapCJS(code)
|
|
859
|
+
return new Response(wrapped || code, { headers: { 'Content-Type': 'application/javascript' } })
|
|
694
860
|
}
|
|
695
|
-
}
|
|
696
|
-
|
|
697
|
-
// Subpath: try the exact path, then with extensions
|
|
698
|
-
const resp = await serveResolved(filepath)
|
|
699
|
-
if (resp) return resp
|
|
700
861
|
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
const resp = await serveResolved(filepath + ext)
|
|
705
|
-
if (resp) return resp
|
|
706
|
-
}
|
|
862
|
+
const resolved = await resolveNodeModulesRequest(pathname)
|
|
863
|
+
const resp = resolved ? await serveResolved(resolved) : null
|
|
864
|
+
if (resp) return resp
|
|
707
865
|
}
|
|
708
|
-
}
|
|
709
866
|
|
|
710
867
|
// Static files: check htmlDir first (for assets relative to HTML), then root
|
|
711
868
|
const inHtmlDir = Bun.file(path.join(htmlDir, pathname))
|
|
@@ -731,13 +888,13 @@ export function serve(entrypoint, flags) {
|
|
|
731
888
|
}
|
|
732
889
|
|
|
733
890
|
// SPA fallback for extension-less paths
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
891
|
+
if (!lastSegment.includes('.')) {
|
|
892
|
+
let html = await Bun.file(htmlPath).text()
|
|
893
|
+
if (!generatedImportMap) generatedImportMap = await buildGeneratedImportMap()
|
|
894
|
+
return new Response(transformHtml(html, entrypoint, generatedImportMap), {
|
|
895
|
+
headers: { 'Content-Type': 'text/html' },
|
|
896
|
+
})
|
|
897
|
+
}
|
|
741
898
|
|
|
742
899
|
return new Response('Not Found', { status: 404 })
|
|
743
900
|
},
|