bimba-cli 0.7.12 → 0.7.14
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 +24 -5
- package/index.js +17 -1
- package/package.json +1 -1
- package/serve.js +60 -88
- package/typecheck.js +248 -0
package/README.md
CHANGED
|
@@ -37,9 +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
|
-
-
|
|
40
|
+
- Rewrites bare package imports in served JS modules to `__bimba_vendor__/*` URLs
|
|
41
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
|
|
42
|
+
- npm packages are bundled on demand by Bun (`target: "browser"`), so Bun owns `exports`, `browser`, CommonJS interop, and nested dependency resolution
|
|
43
43
|
- Injects an HMR client that swaps component prototypes without a full page reload
|
|
44
44
|
|
|
45
45
|
**HMR internals:**
|
|
@@ -58,7 +58,7 @@ Duplicate root elements (caused by `imba.mount()` running again on re-import) ar
|
|
|
58
58
|
|
|
59
59
|
For a deep dive into how Imba compiles tags, how the render cache works, and how bimba hooks into it — see [INTERNALS.md](INTERNALS.md).
|
|
60
60
|
|
|
61
|
-
**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
|
|
61
|
+
**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 remove existing import maps, since package imports are rewritten in served modules instead:
|
|
62
62
|
|
|
63
63
|
```html
|
|
64
64
|
<script type='module' src="./js/index.js" data-entrypoint></script>
|
|
@@ -72,9 +72,9 @@ For a deep dive into how Imba compiles tags, how the render cache works, and how
|
|
|
72
72
|
|
|
73
73
|
`--html <path>` — path to your HTML file (auto-detected from `./index.html`, `./public/index.html`, `./src/index.html` if omitted)
|
|
74
74
|
|
|
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
|
|
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 are resolved by trying `.imba`, `.js`, and `.mjs` extensions automatically.
|
|
76
76
|
|
|
77
|
-
**npm package resolution:** The dev server
|
|
77
|
+
**npm package resolution:** The dev server scans each served JS module and rewrites bare imports such as `imba/runtime`, `@scope/pkg`, and `pkg/subpath` to `__bimba_vendor__/*` URLs. Those vendor URLs are bundled 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
|
|
|
@@ -90,6 +90,21 @@ With watch:
|
|
|
90
90
|
bunx bimba src/index.imba --outdir public/js --watch --clearcache
|
|
91
91
|
```
|
|
92
92
|
|
|
93
|
+
### TypeScript diagnostics for Imba files
|
|
94
|
+
|
|
95
|
+
To check TypeScript diagnostics reported by the Imba language-service plugin:
|
|
96
|
+
```bash
|
|
97
|
+
bunx bimba --typecheck
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
By default this scans `src/` when it exists, otherwise the project root. You can also pass a specific file or folder:
|
|
101
|
+
```bash
|
|
102
|
+
bunx bimba src/index.imba --typecheck
|
|
103
|
+
bunx bimba src --typecheck
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
This mode requires `typescript` in the project and `typescript-imba-plugin` either in `node_modules` or in an installed Imba editor extension.
|
|
107
|
+
|
|
93
108
|
---
|
|
94
109
|
|
|
95
110
|
### All CLI flags
|
|
@@ -106,6 +121,10 @@ bunx bimba src/index.imba --outdir public/js --watch --clearcache
|
|
|
106
121
|
|
|
107
122
|
`--target <browser|node>` — platform flag passed to the Imba compiler (default: `browser`). The `node` value does not work under Bun.
|
|
108
123
|
|
|
124
|
+
`--typecheck` — check TypeScript diagnostics in `.imba` files using `tsserver` and `typescript-imba-plugin`.
|
|
125
|
+
|
|
126
|
+
`--tscheck` — alias for `--typecheck`.
|
|
127
|
+
|
|
109
128
|
`--serve` — start dev server with HMR instead of bundling.
|
|
110
129
|
|
|
111
130
|
`--port <number>` — port for the dev server (default: `5200`). Used with `--serve`.
|
package/index.js
CHANGED
|
@@ -7,6 +7,7 @@ import fs from 'fs'
|
|
|
7
7
|
import path from 'path';
|
|
8
8
|
import { rmSync } from "node:fs";
|
|
9
9
|
import { serve } from './serve.js';
|
|
10
|
+
import { checkImbaTypes } from './typecheck.js';
|
|
10
11
|
|
|
11
12
|
|
|
12
13
|
let flags = {}
|
|
@@ -28,6 +29,8 @@ try {
|
|
|
28
29
|
serve: { type: 'boolean' },
|
|
29
30
|
port: { type: 'string' },
|
|
30
31
|
html: { type: 'string' },
|
|
32
|
+
typecheck: { type: 'boolean' },
|
|
33
|
+
tscheck: { type: 'boolean' },
|
|
31
34
|
},
|
|
32
35
|
allowNegative: true,
|
|
33
36
|
strict: true,
|
|
@@ -74,6 +77,8 @@ if(flags.help) {
|
|
|
74
77
|
console.log(" "+theme.flags('--external <package>')+" Exclude package from bundle (repeatable, e.g. --external ws --external node-pty)");
|
|
75
78
|
console.log(" "+theme.flags('--watch')+" Watch for changes in the entrypoint folder");
|
|
76
79
|
console.log(" "+theme.flags('--clearcache')+" Clear cache on exit, works only when in watch mode");
|
|
80
|
+
console.log(" "+theme.flags('--typecheck')+" Check TypeScript diagnostics in .imba files");
|
|
81
|
+
console.log(" "+theme.flags('--tscheck')+" Alias for --typecheck");
|
|
77
82
|
console.log("");
|
|
78
83
|
console.log("Dev server (HMR):");
|
|
79
84
|
console.log(" "+theme.flags('--serve')+" Start dev server with Hot Module Replacement");
|
|
@@ -86,8 +91,19 @@ if(flags.help) {
|
|
|
86
91
|
|
|
87
92
|
let bundling = false;
|
|
88
93
|
|
|
94
|
+
// typecheck mode
|
|
95
|
+
if (flags.typecheck || flags.tscheck) {
|
|
96
|
+
try {
|
|
97
|
+
const success = await checkImbaTypes(entrypoint);
|
|
98
|
+
process.exit(success ? 0 : 1);
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
console.log(theme.failure(' Failure ') + ` ${error.message}`);
|
|
102
|
+
process.exit(1);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
89
105
|
// serve mode
|
|
90
|
-
if (flags.serve) {
|
|
106
|
+
else if (flags.serve) {
|
|
91
107
|
if (!entrypoint) {
|
|
92
108
|
console.log("");
|
|
93
109
|
console.log("You should provide entrypoint: "+theme.flags('bimba file.imba --serve'));
|
package/package.json
CHANGED
package/serve.js
CHANGED
|
@@ -255,6 +255,7 @@ const hmrClient = `
|
|
|
255
255
|
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
|
+
const _importScanner = new Bun.Transpiler({ loader: 'js' })
|
|
258
259
|
|
|
259
260
|
// Imba compiles tag render-cache slots as anonymous local Symbols at module top
|
|
260
261
|
// level: `var $4 = Symbol(), $11 = Symbol(), ...; let c$0 = Symbol();`. Each
|
|
@@ -299,6 +300,40 @@ function _normalizeResult(result, extras) {
|
|
|
299
300
|
}
|
|
300
301
|
}
|
|
301
302
|
|
|
303
|
+
function isBareSpecifier(specifier) {
|
|
304
|
+
if (!specifier) return false
|
|
305
|
+
if (specifier.startsWith('.') || specifier.startsWith('/')) return false
|
|
306
|
+
if (/^[a-zA-Z][a-zA-Z\d+.-]*:/.test(specifier)) return false
|
|
307
|
+
if (specifier.startsWith('#')) return false
|
|
308
|
+
return true
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function escapeRegExp(value) {
|
|
312
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function rewriteBareImports(js) {
|
|
316
|
+
let imports = []
|
|
317
|
+
try {
|
|
318
|
+
imports = _importScanner.scanImports(js)
|
|
319
|
+
} catch (_) {
|
|
320
|
+
return js
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
for (const specifier of new Set(imports.map(item => item.path).filter(isBareSpecifier))) {
|
|
324
|
+
const target = vendorUrl(specifier)
|
|
325
|
+
const escaped = escapeRegExp(specifier)
|
|
326
|
+
|
|
327
|
+
const staticPattern = new RegExp(`((?:import|export)\\s+(?:[^'"]*?\\s+from\\s*)?)(['"])${escaped}\\2`, 'g')
|
|
328
|
+
js = js.replace(staticPattern, (_match, prefix, quote) => `${prefix}${quote}${target}${quote}`)
|
|
329
|
+
|
|
330
|
+
const dynamicPattern = new RegExp(`(\\bimport\\s*\\(\\s*)(['"])${escaped}\\2`, 'g')
|
|
331
|
+
js = js.replace(dynamicPattern, (_match, prefix, quote) => `${prefix}${quote}${target}${quote}`)
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
return js
|
|
335
|
+
}
|
|
336
|
+
|
|
302
337
|
async function compileFile(filepath) {
|
|
303
338
|
const abs = path.resolve(filepath)
|
|
304
339
|
const file = Bun.file(filepath)
|
|
@@ -318,7 +353,7 @@ async function compileFile(filepath) {
|
|
|
318
353
|
const errors = result.errors || []
|
|
319
354
|
if (!errors.length && result.js) {
|
|
320
355
|
const { js, slotCount } = stabilizeSymbols(result.js, abs)
|
|
321
|
-
result.js = js
|
|
356
|
+
result.js = rewriteBareImports(js)
|
|
322
357
|
const prev = _prevSlots.get(abs)
|
|
323
358
|
result.slots = (prev === undefined || prev === slotCount) ? 'stable' : 'shifted'
|
|
324
359
|
_prevSlots.set(abs, slotCount)
|
|
@@ -345,11 +380,7 @@ function findHtml(flagHtml) {
|
|
|
345
380
|
const _vendorCache = new Map() // entrypoint → { mtime, code }
|
|
346
381
|
|
|
347
382
|
function vendorUrl(specifier) {
|
|
348
|
-
return '/__bimba_vendor__/' + specifier
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
function vendorPrefixUrl(specifier) {
|
|
352
|
-
return vendorUrl(specifier) + '/'
|
|
383
|
+
return '/__bimba_vendor__/' + encodeURIComponent(specifier)
|
|
353
384
|
}
|
|
354
385
|
|
|
355
386
|
function resolveFileCandidate(filepath) {
|
|
@@ -453,86 +484,22 @@ async function bundleVendor(entrypoint) {
|
|
|
453
484
|
}
|
|
454
485
|
}
|
|
455
486
|
|
|
456
|
-
async function
|
|
457
|
-
const
|
|
458
|
-
|
|
459
|
-
try {
|
|
460
|
-
const pkg = JSON.parse(await Bun.file('./package.json').text())
|
|
461
|
-
for (const field of ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies']) {
|
|
462
|
-
for (const name of Object.keys(pkg[field] || {})) {
|
|
463
|
-
imports[name] ||= vendorUrl(name)
|
|
464
|
-
imports[name + '/'] ||= vendorPrefixUrl(name)
|
|
465
|
-
}
|
|
466
|
-
}
|
|
467
|
-
} catch(_) { /* no package.json */ }
|
|
468
|
-
|
|
469
|
-
imports['imba'] ||= vendorUrl('imba')
|
|
470
|
-
imports['imba/'] ||= vendorPrefixUrl('imba')
|
|
471
|
-
imports['imba/runtime'] ||= vendorUrl('imba/runtime')
|
|
472
|
-
|
|
473
|
-
return { imports }
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
function extractUserImportMap(html) {
|
|
477
|
-
const merged = { imports: {}, scopes: {} }
|
|
478
|
-
|
|
479
|
-
html = html.replace(/<script\s+type=["']importmap["'][^>]*>([\s\S]*?)<\/script>/gi, (_match, json) => {
|
|
480
|
-
try {
|
|
481
|
-
const parsed = JSON.parse(json)
|
|
482
|
-
Object.assign(merged.imports, parsed.imports || {})
|
|
483
|
-
for (const [scope, specifiers] of Object.entries(parsed.scopes || {})) {
|
|
484
|
-
merged.scopes[scope] = { ...(merged.scopes[scope] || {}), ...specifiers }
|
|
485
|
-
}
|
|
486
|
-
} catch (_) {
|
|
487
|
-
// ignore invalid import maps and keep serving the page
|
|
488
|
-
}
|
|
489
|
-
return ''
|
|
490
|
-
})
|
|
491
|
-
|
|
492
|
-
if (!Object.keys(merged.scopes).length) delete merged.scopes
|
|
493
|
-
return { html, importMap: merged }
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
function mergeImportMaps(generated, user) {
|
|
497
|
-
const merged = {
|
|
498
|
-
imports: { ...(generated.imports || {}), ...(user.imports || {}) },
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
const scopeKeys = new Set([
|
|
502
|
-
...Object.keys(generated.scopes || {}),
|
|
503
|
-
...Object.keys(user.scopes || {}),
|
|
504
|
-
])
|
|
505
|
-
|
|
506
|
-
if (scopeKeys.size) {
|
|
507
|
-
merged.scopes = {}
|
|
508
|
-
for (const key of scopeKeys) {
|
|
509
|
-
merged.scopes[key] = {
|
|
510
|
-
...((generated.scopes || {})[key] || {}),
|
|
511
|
-
...((user.scopes || {})[key] || {}),
|
|
512
|
-
}
|
|
513
|
-
}
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
return merged
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
function renderImportMapTag(importMap) {
|
|
520
|
-
return `\t\t<script type="importmap">\n\t\t\t${JSON.stringify(importMap, null, '\t\t\t\t')}\n\t\t</script>`
|
|
487
|
+
async function serveJavaScriptFile(filepath) {
|
|
488
|
+
const js = rewriteBareImports(await Bun.file(filepath).text())
|
|
489
|
+
return new Response(js, { headers: { 'Content-Type': 'application/javascript' } })
|
|
521
490
|
}
|
|
522
491
|
|
|
523
492
|
// Rewrite production HTML for the dev server:
|
|
524
|
-
// strips existing importmap + data-entrypoint script, injects
|
|
493
|
+
// strips existing importmap + data-entrypoint script, then injects the Imba
|
|
525
494
|
// entrypoint module + HMR client before </head>.
|
|
526
|
-
function transformHtml(html, entrypoint
|
|
527
|
-
|
|
528
|
-
html = extracted.html
|
|
495
|
+
function transformHtml(html, entrypoint) {
|
|
496
|
+
html = html.replace(/<script\s+type=["']importmap["'][^>]*>[\s\S]*?<\/script>/gi, '')
|
|
529
497
|
html = html.replace(/<script([^>]*)\bdata-entrypoint\b([^>]*)><\/script>/gi, '')
|
|
530
498
|
|
|
531
499
|
const entryUrl = '/' + entrypoint.replace(/^\.\//, '').replaceAll('\\', '/')
|
|
532
|
-
const importMapTag = renderImportMapTag(mergeImportMaps(generatedImportMap, extracted.importMap))
|
|
533
500
|
|
|
534
501
|
html = html.replace('</head>',
|
|
535
|
-
|
|
502
|
+
`\t\t<script type='module' src='${entryUrl}'></script>\n${hmrClient}\n\t</head>`
|
|
536
503
|
)
|
|
537
504
|
return html
|
|
538
505
|
}
|
|
@@ -545,7 +512,6 @@ export function serve(entrypoint, flags) {
|
|
|
545
512
|
const htmlDir = path.dirname(htmlPath)
|
|
546
513
|
const srcDir = path.dirname(entrypoint)
|
|
547
514
|
const sockets = new Set()
|
|
548
|
-
let generatedImportMap = null
|
|
549
515
|
|
|
550
516
|
// ── Status line (prints current compile result, fades out on success) ──────
|
|
551
517
|
|
|
@@ -679,8 +645,7 @@ export function serve(entrypoint, flags) {
|
|
|
679
645
|
if (pathname === '/' || pathname.endsWith('.html')) {
|
|
680
646
|
const htmlFile = pathname === '/' ? htmlPath : '.' + pathname
|
|
681
647
|
let html = await Bun.file(htmlFile).text()
|
|
682
|
-
|
|
683
|
-
return new Response(transformHtml(html, entrypoint, generatedImportMap), {
|
|
648
|
+
return new Response(transformHtml(html, entrypoint), {
|
|
684
649
|
headers: { 'Content-Type': 'text/html' },
|
|
685
650
|
})
|
|
686
651
|
}
|
|
@@ -713,8 +678,13 @@ export function serve(entrypoint, flags) {
|
|
|
713
678
|
// Without this, `import './styles.css'` inside an ESM package fails because
|
|
714
679
|
// the browser expects a JS module response, not raw CSS.
|
|
715
680
|
if (pathname.endsWith('.css')) {
|
|
716
|
-
const
|
|
717
|
-
|
|
681
|
+
const cssPath = resolveFileCandidate(path.join(htmlDir, pathname)) || resolveFileCandidate('.' + pathname)
|
|
682
|
+
const cssFile = cssPath ? Bun.file(cssPath) : null
|
|
683
|
+
if (cssFile && await cssFile.exists()) {
|
|
684
|
+
if (req.headers.get('sec-fetch-dest') === 'style') {
|
|
685
|
+
return new Response(cssFile, { headers: { 'Content-Type': 'text/css' } })
|
|
686
|
+
}
|
|
687
|
+
|
|
718
688
|
const css = await cssFile.text()
|
|
719
689
|
const id = JSON.stringify(pathname)
|
|
720
690
|
const js = [
|
|
@@ -727,6 +697,11 @@ export function serve(entrypoint, flags) {
|
|
|
727
697
|
}
|
|
728
698
|
}
|
|
729
699
|
|
|
700
|
+
if (!pathname.startsWith('/node_modules/') && (pathname.endsWith('.js') || pathname.endsWith('.mjs'))) {
|
|
701
|
+
const jsFile = resolveFileCandidate(path.join(htmlDir, pathname)) || resolveFileCandidate('.' + pathname)
|
|
702
|
+
if (jsFile) return serveJavaScriptFile(jsFile)
|
|
703
|
+
}
|
|
704
|
+
|
|
730
705
|
// Direct node_modules URLs (from user import maps or explicit imports)
|
|
731
706
|
// are bundled through Bun too, so browser/cjs/exports handling stays
|
|
732
707
|
// in one place.
|
|
@@ -763,18 +738,15 @@ export function serve(entrypoint, flags) {
|
|
|
763
738
|
if (!out.errors?.length) return new Response(out.js, { headers: { 'Content-Type': 'application/javascript' } })
|
|
764
739
|
}
|
|
765
740
|
for (const ext of ['.js', '.mjs']) {
|
|
766
|
-
const withExt =
|
|
767
|
-
if (
|
|
768
|
-
headers: { 'Content-Type': 'application/javascript' },
|
|
769
|
-
})
|
|
741
|
+
const withExt = '.' + pathname + ext
|
|
742
|
+
if (existsSync(withExt)) return serveJavaScriptFile(withExt)
|
|
770
743
|
}
|
|
771
744
|
}
|
|
772
745
|
|
|
773
746
|
// SPA fallback for extension-less paths
|
|
774
747
|
if (!lastSegment.includes('.')) {
|
|
775
748
|
let html = await Bun.file(htmlPath).text()
|
|
776
|
-
|
|
777
|
-
return new Response(transformHtml(html, entrypoint, generatedImportMap), {
|
|
749
|
+
return new Response(transformHtml(html, entrypoint), {
|
|
778
750
|
headers: { 'Content-Type': 'text/html' },
|
|
779
751
|
})
|
|
780
752
|
}
|
package/typecheck.js
ADDED
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import { spawn } from 'child_process';
|
|
2
|
+
import { createRequire } from 'module';
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import os from 'os';
|
|
5
|
+
import path from 'path';
|
|
6
|
+
import { theme } from './utils.js';
|
|
7
|
+
|
|
8
|
+
const require = createRequire(import.meta.url);
|
|
9
|
+
|
|
10
|
+
const SKIP_DIRS = new Set([
|
|
11
|
+
'.bimba',
|
|
12
|
+
'.cache',
|
|
13
|
+
'.git',
|
|
14
|
+
'build',
|
|
15
|
+
'dist',
|
|
16
|
+
'node_modules',
|
|
17
|
+
'public',
|
|
18
|
+
]);
|
|
19
|
+
|
|
20
|
+
function canResolve(request, from) {
|
|
21
|
+
try {
|
|
22
|
+
return require.resolve(request, { paths: [from] });
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function findTypeScript(cwd) {
|
|
30
|
+
const tsserver = canResolve('typescript/lib/tsserver.js', cwd);
|
|
31
|
+
if (tsserver) return tsserver;
|
|
32
|
+
|
|
33
|
+
throw new Error('Could not find TypeScript. Install it in this project: bun add -d typescript');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function findPluginProbe(cwd) {
|
|
37
|
+
const localProbe = path.join(cwd, 'node_modules');
|
|
38
|
+
if (canResolve('typescript-imba-plugin', localProbe)) return localProbe;
|
|
39
|
+
|
|
40
|
+
const extensionRoots = [
|
|
41
|
+
path.join(os.homedir(), '.vscode', 'extensions'),
|
|
42
|
+
path.join(os.homedir(), '.cursor', 'extensions'),
|
|
43
|
+
path.join(os.homedir(), '.windsurf', 'extensions'),
|
|
44
|
+
path.join(os.homedir(), '.kiro', 'extensions'),
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
for (const root of extensionRoots) {
|
|
48
|
+
if (!fs.existsSync(root)) continue;
|
|
49
|
+
|
|
50
|
+
for (const entry of fs.readdirSync(root)) {
|
|
51
|
+
const probe = path.join(root, entry, 'node_modules');
|
|
52
|
+
if (canResolve('typescript-imba-plugin', probe)) return probe;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
throw new Error('Could not find typescript-imba-plugin. Install the Imba VSCode extension or add the plugin to node_modules.');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function getScanRoot(entrypoint, cwd) {
|
|
60
|
+
if (!entrypoint) {
|
|
61
|
+
const src = path.join(cwd, 'src');
|
|
62
|
+
return fs.existsSync(src) ? src : cwd;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const resolved = path.resolve(cwd, entrypoint);
|
|
66
|
+
if (!fs.existsSync(resolved)) {
|
|
67
|
+
throw new Error(`The specified typecheck path does not exist: ${entrypoint}`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const stat = fs.statSync(resolved);
|
|
71
|
+
return stat.isDirectory() ? resolved : path.dirname(resolved);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function collectImbaFiles(root) {
|
|
75
|
+
const files = [];
|
|
76
|
+
|
|
77
|
+
function walk(dir) {
|
|
78
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
79
|
+
if (entry.isDirectory()) {
|
|
80
|
+
if (!SKIP_DIRS.has(entry.name)) walk(path.join(dir, entry.name));
|
|
81
|
+
}
|
|
82
|
+
else if (entry.isFile() && entry.name.endsWith('.imba')) {
|
|
83
|
+
files.push(path.join(dir, entry.name));
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
walk(root);
|
|
89
|
+
files.sort();
|
|
90
|
+
return files;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function parseMessages(buffer, onMessage) {
|
|
94
|
+
while (true) {
|
|
95
|
+
const text = buffer.toString('utf8');
|
|
96
|
+
const headerEnd = text.indexOf('\r\n\r\n');
|
|
97
|
+
if (headerEnd < 0) return buffer;
|
|
98
|
+
|
|
99
|
+
const match = /Content-Length: (\d+)/i.exec(text.slice(0, headerEnd));
|
|
100
|
+
if (!match) return buffer;
|
|
101
|
+
|
|
102
|
+
const length = Number(match[1]);
|
|
103
|
+
const bodyStart = Buffer.byteLength(text.slice(0, headerEnd + 4));
|
|
104
|
+
if (buffer.length < bodyStart + length) return buffer;
|
|
105
|
+
|
|
106
|
+
const body = buffer.slice(bodyStart, bodyStart + length).toString('utf8');
|
|
107
|
+
buffer = buffer.slice(bodyStart + length);
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
onMessage(JSON.parse(body));
|
|
111
|
+
}
|
|
112
|
+
catch {}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function flattenMessage(text) {
|
|
117
|
+
if (typeof text == 'string') return text;
|
|
118
|
+
if (!text) return '';
|
|
119
|
+
if (text.messageText) {
|
|
120
|
+
const next = Array.isArray(text.next) ? text.next.map(flattenMessage) : [];
|
|
121
|
+
return [flattenMessage(text.messageText), ...next].filter(Boolean).join(' ');
|
|
122
|
+
}
|
|
123
|
+
return String(text);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function uniqueDiagnostics(diagnostics) {
|
|
127
|
+
const unique = Array.from(new Map(diagnostics.map(item => [item.key, item])).values());
|
|
128
|
+
unique.sort((a, b) => {
|
|
129
|
+
return a.file.localeCompare(b.file)
|
|
130
|
+
|| (a.start?.line || 0) - (b.start?.line || 0)
|
|
131
|
+
|| (a.start?.offset || 0) - (b.start?.offset || 0)
|
|
132
|
+
|| String(a.code).localeCompare(String(b.code));
|
|
133
|
+
});
|
|
134
|
+
return unique;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function printDiagnostics(cwd, diagnostics) {
|
|
138
|
+
for (const item of diagnostics) {
|
|
139
|
+
const rel = path.relative(cwd, item.file);
|
|
140
|
+
const line = item.start?.line || 0;
|
|
141
|
+
const offset = item.start?.offset || 0;
|
|
142
|
+
const code = item.code ? `TS${item.code}` : 'TS';
|
|
143
|
+
const category = item.category || 'error';
|
|
144
|
+
const text = flattenMessage(item.text);
|
|
145
|
+
|
|
146
|
+
console.log(`${theme.filedir(rel)}:${line}:${offset} ${theme.action(item.kind)} ${theme.failure(` ${code} `)} ${category}: ${text}`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function send(server, seq, command, args) {
|
|
151
|
+
server.stdin.write(JSON.stringify({ seq: seq.value++, type: 'request', command, arguments: args }) + '\n');
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export async function checkImbaTypes(entrypoint, options = {}) {
|
|
155
|
+
const cwd = options.cwd || process.cwd();
|
|
156
|
+
const timeout = Number(options.timeout || process.env.BIMBA_TYPECHECK_TIMEOUT || process.env.IMBA_TS_CHECK_TIMEOUT || 12000);
|
|
157
|
+
const scanRoot = getScanRoot(entrypoint, cwd);
|
|
158
|
+
const files = collectImbaFiles(scanRoot);
|
|
159
|
+
|
|
160
|
+
if (!files.length) {
|
|
161
|
+
console.log(theme.success('Success') + ` No Imba files found in ${theme.filedir(path.relative(cwd, scanRoot) || '.')}`);
|
|
162
|
+
return true;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const tsserver = findTypeScript(cwd);
|
|
166
|
+
const pluginProbe = findPluginProbe(cwd);
|
|
167
|
+
const runner = process.env.BIMBA_NODE || process.env.NODE || 'node';
|
|
168
|
+
|
|
169
|
+
console.log(theme.folder('──────────────────────────────────────────────────────────────────────'));
|
|
170
|
+
console.log(theme.start(`Start checking TypeScript diagnostics for ${theme.count(files.length)} Imba file${files.length > 1 ? 's' : ''}`));
|
|
171
|
+
|
|
172
|
+
return await new Promise((resolve) => {
|
|
173
|
+
let settled = false;
|
|
174
|
+
let buffer = Buffer.alloc(0);
|
|
175
|
+
const seq = { value: 1 };
|
|
176
|
+
const diagnostics = [];
|
|
177
|
+
|
|
178
|
+
const server = spawn(runner, [
|
|
179
|
+
tsserver,
|
|
180
|
+
'--globalPlugins',
|
|
181
|
+
'typescript-imba-plugin',
|
|
182
|
+
'--pluginProbeLocations',
|
|
183
|
+
pluginProbe,
|
|
184
|
+
], { cwd });
|
|
185
|
+
|
|
186
|
+
function finish(success) {
|
|
187
|
+
if (settled) return;
|
|
188
|
+
settled = true;
|
|
189
|
+
clearTimeout(timer);
|
|
190
|
+
server.kill();
|
|
191
|
+
resolve(success);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const timer = setTimeout(() => {
|
|
195
|
+
const unique = uniqueDiagnostics(diagnostics);
|
|
196
|
+
|
|
197
|
+
if (!unique.length) {
|
|
198
|
+
console.log(theme.success('Success') + ' No Imba TypeScript diagnostics');
|
|
199
|
+
finish(true);
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
printDiagnostics(cwd, unique);
|
|
204
|
+
console.log(theme.failure(' Failure ') + ` TypeScript found ${theme.count(unique.length)} diagnostic${unique.length > 1 ? 's' : ''}`);
|
|
205
|
+
finish(false);
|
|
206
|
+
}, timeout);
|
|
207
|
+
|
|
208
|
+
server.on('error', (error) => {
|
|
209
|
+
console.log(theme.failure(' Failure ') + ` Could not start ${runner}: ${error.message}`);
|
|
210
|
+
finish(false);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
server.stderr.on('data', chunk => process.stderr.write(chunk));
|
|
214
|
+
|
|
215
|
+
server.stdout.on('data', chunk => {
|
|
216
|
+
buffer = Buffer.concat([buffer, chunk]);
|
|
217
|
+
buffer = parseMessages(buffer, msg => {
|
|
218
|
+
if (msg.type != 'event' || !/Diag$/.test(msg.event)) return;
|
|
219
|
+
if (!msg.body?.diagnostics?.length) return;
|
|
220
|
+
|
|
221
|
+
for (const diagnostic of msg.body.diagnostics) {
|
|
222
|
+
const key = [
|
|
223
|
+
msg.event,
|
|
224
|
+
msg.body.file,
|
|
225
|
+
diagnostic.start?.line,
|
|
226
|
+
diagnostic.start?.offset,
|
|
227
|
+
diagnostic.code,
|
|
228
|
+
flattenMessage(diagnostic.text),
|
|
229
|
+
].join('\0');
|
|
230
|
+
|
|
231
|
+
diagnostics.push({
|
|
232
|
+
key,
|
|
233
|
+
kind: msg.event,
|
|
234
|
+
file: msg.body.file,
|
|
235
|
+
...diagnostic,
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
setTimeout(() => {
|
|
242
|
+
if (settled) return;
|
|
243
|
+
send(server, seq, 'configure', { preferences: {}, hostInfo: 'bimba-typecheck' });
|
|
244
|
+
for (const file of files) send(server, seq, 'open', { file, projectRootPath: cwd });
|
|
245
|
+
send(server, seq, 'geterr', { files, delay: 0 });
|
|
246
|
+
}, 100);
|
|
247
|
+
});
|
|
248
|
+
}
|