rip-lang 3.9.3 → 3.10.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/bin/rip +10 -0
- package/docs/dist/rip-ui.min.js +81 -81
- package/docs/dist/rip-ui.min.js.br +0 -0
- package/docs/dist/rip.browser.min.js +39 -39
- package/docs/dist/rip.browser.min.js.br +0 -0
- package/package.json +1 -1
- package/src/browser.js +1 -0
- package/src/compiler.js +3 -1
- package/src/components.js +55 -14
- package/src/sourcemaps.js +72 -4
- package/src/typecheck.js +367 -0
|
Binary file
|
package/package.json
CHANGED
package/src/browser.js
CHANGED
package/src/compiler.js
CHANGED
|
@@ -3141,7 +3141,9 @@ export class Compiler {
|
|
|
3141
3141
|
this.options = { showTokens: false, showSExpr: false, ...options };
|
|
3142
3142
|
}
|
|
3143
3143
|
|
|
3144
|
-
compile(source) {
|
|
3144
|
+
compile(source, options) {
|
|
3145
|
+
if (options) this.options = { ...this.options, ...options };
|
|
3146
|
+
|
|
3145
3147
|
// Handle __DATA__ marker
|
|
3146
3148
|
let dataSection = null;
|
|
3147
3149
|
let lines = source.split('\n');
|
package/src/components.js
CHANGED
|
@@ -57,6 +57,14 @@ function getMemberName(target) {
|
|
|
57
57
|
return null;
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
+
/**
|
|
61
|
+
* Check if target uses @property syntax (public prop).
|
|
62
|
+
* [".", "this", name] = @prop (public), plain string = private.
|
|
63
|
+
*/
|
|
64
|
+
function isPublicProp(target) {
|
|
65
|
+
return Array.isArray(target) && target[0] === '.' && target[1] === 'this';
|
|
66
|
+
}
|
|
67
|
+
|
|
60
68
|
/**
|
|
61
69
|
* Detect fragment root and collect direct child variables for proper removal.
|
|
62
70
|
* After insertBefore, a DocumentFragment is empty — .remove() is a no-op.
|
|
@@ -527,6 +535,19 @@ export function installComponentSupport(CodeGenerator, Lexer) {
|
|
|
527
535
|
return ['=>', ...sexpr.slice(1).map(item => this.transformComponentMembers(item))];
|
|
528
536
|
}
|
|
529
537
|
|
|
538
|
+
// Object literals: transform values but leave bare string keys untouched
|
|
539
|
+
if (sexpr[0] === 'object') {
|
|
540
|
+
return ['object', ...sexpr.slice(1).map(pair => {
|
|
541
|
+
if (Array.isArray(pair) && pair.length >= 2) {
|
|
542
|
+
let key = pair[0];
|
|
543
|
+
let newKey = Array.isArray(key) ? this.transformComponentMembers(key) : key;
|
|
544
|
+
let newValue = this.transformComponentMembers(pair[1]);
|
|
545
|
+
return [newKey, newValue, pair[2]];
|
|
546
|
+
}
|
|
547
|
+
return this.transformComponentMembers(pair);
|
|
548
|
+
})];
|
|
549
|
+
}
|
|
550
|
+
|
|
530
551
|
return sexpr.map(item => this.transformComponentMembers(item));
|
|
531
552
|
};
|
|
532
553
|
|
|
@@ -566,7 +587,7 @@ export function installComponentSupport(CodeGenerator, Lexer) {
|
|
|
566
587
|
if (op === 'state') {
|
|
567
588
|
const varName = getMemberName(stmt[1]);
|
|
568
589
|
if (varName) {
|
|
569
|
-
stateVars.push({ name: varName, value: stmt[2] });
|
|
590
|
+
stateVars.push({ name: varName, value: stmt[2], isPublic: isPublicProp(stmt[1]) });
|
|
570
591
|
memberNames.add(varName);
|
|
571
592
|
reactiveMembers.add(varName);
|
|
572
593
|
}
|
|
@@ -580,7 +601,7 @@ export function installComponentSupport(CodeGenerator, Lexer) {
|
|
|
580
601
|
} else if (op === 'readonly') {
|
|
581
602
|
const varName = getMemberName(stmt[1]);
|
|
582
603
|
if (varName) {
|
|
583
|
-
readonlyVars.push({ name: varName, value: stmt[2] });
|
|
604
|
+
readonlyVars.push({ name: varName, value: stmt[2], isPublic: isPublicProp(stmt[1]) });
|
|
584
605
|
memberNames.add(varName);
|
|
585
606
|
}
|
|
586
607
|
} else if (op === '=') {
|
|
@@ -594,7 +615,7 @@ export function installComponentSupport(CodeGenerator, Lexer) {
|
|
|
594
615
|
methods.push({ name: varName, func: val });
|
|
595
616
|
memberNames.add(varName);
|
|
596
617
|
} else {
|
|
597
|
-
stateVars.push({ name: varName, value: val });
|
|
618
|
+
stateVars.push({ name: varName, value: val, isPublic: isPublicProp(stmt[1]) });
|
|
598
619
|
memberNames.add(varName);
|
|
599
620
|
reactiveMembers.add(varName);
|
|
600
621
|
}
|
|
@@ -634,15 +655,19 @@ export function installComponentSupport(CodeGenerator, Lexer) {
|
|
|
634
655
|
lines.push(' _init(props) {');
|
|
635
656
|
|
|
636
657
|
// Constants (readonly)
|
|
637
|
-
for (const { name, value } of readonlyVars) {
|
|
658
|
+
for (const { name, value, isPublic } of readonlyVars) {
|
|
638
659
|
const val = this.generateInComponent(value, 'value');
|
|
639
|
-
lines.push(
|
|
660
|
+
lines.push(isPublic
|
|
661
|
+
? ` this.${name} = props.${name} ?? ${val};`
|
|
662
|
+
: ` this.${name} = ${val};`);
|
|
640
663
|
}
|
|
641
664
|
|
|
642
665
|
// State variables (__state handles signal passthrough)
|
|
643
|
-
for (const { name, value } of stateVars) {
|
|
666
|
+
for (const { name, value, isPublic } of stateVars) {
|
|
644
667
|
const val = this.generateInComponent(value, 'value');
|
|
645
|
-
lines.push(
|
|
668
|
+
lines.push(isPublic
|
|
669
|
+
? ` this.${name} = __state(props.${name} ?? ${val});`
|
|
670
|
+
: ` this.${name} = __state(${val});`);
|
|
646
671
|
}
|
|
647
672
|
|
|
648
673
|
// Computed (derived)
|
|
@@ -1100,14 +1125,17 @@ export function installComponentSupport(CodeGenerator, Lexer) {
|
|
|
1100
1125
|
|
|
1101
1126
|
// Smart two-way binding for value/checked when bound to reactive state
|
|
1102
1127
|
if ((key === 'value' || key === 'checked') && this.hasReactiveDeps(value)) {
|
|
1103
|
-
// Reactive effect: signal → DOM property
|
|
1104
1128
|
this._setupLines.push(`__effect(() => { ${elVar}.${key} = ${valueCode}; });`);
|
|
1105
|
-
//
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
: '
|
|
1110
|
-
|
|
1129
|
+
// Only generate reverse binding when the value is a simple assignable
|
|
1130
|
+
// target (plain reactive member or @prop), not a complex expression
|
|
1131
|
+
// like selected.includes(opt) which can't be assigned to.
|
|
1132
|
+
if (this.isSimpleAssignable(value)) {
|
|
1133
|
+
const event = key === 'checked' ? 'change' : 'input';
|
|
1134
|
+
const accessor = key === 'checked' ? 'e.target.checked'
|
|
1135
|
+
: (inputType === 'number' || inputType === 'range') ? 'e.target.valueAsNumber'
|
|
1136
|
+
: 'e.target.value';
|
|
1137
|
+
this._createLines.push(`${elVar}.addEventListener('${event}', (e) => { ${valueCode} = ${accessor}; });`);
|
|
1138
|
+
}
|
|
1111
1139
|
continue;
|
|
1112
1140
|
}
|
|
1113
1141
|
|
|
@@ -1605,6 +1633,19 @@ export function installComponentSupport(CodeGenerator, Lexer) {
|
|
|
1605
1633
|
return false;
|
|
1606
1634
|
};
|
|
1607
1635
|
|
|
1636
|
+
// isSimpleAssignable — check if value is a plain reactive member (assignable target)
|
|
1637
|
+
// --------------------------------------------------------------------------
|
|
1638
|
+
|
|
1639
|
+
proto.isSimpleAssignable = function(sexpr) {
|
|
1640
|
+
if (typeof sexpr === 'string') {
|
|
1641
|
+
return !!(this.reactiveMembers && this.reactiveMembers.has(sexpr));
|
|
1642
|
+
}
|
|
1643
|
+
if (Array.isArray(sexpr) && sexpr[0] === '.' && sexpr[1] === 'this' && typeof sexpr[2] === 'string') {
|
|
1644
|
+
return !!(this.reactiveMembers && this.reactiveMembers.has(sexpr[2]));
|
|
1645
|
+
}
|
|
1646
|
+
return false;
|
|
1647
|
+
};
|
|
1648
|
+
|
|
1608
1649
|
// _rootsAtThis — check if a property-access chain is rooted at 'this'
|
|
1609
1650
|
// --------------------------------------------------------------------------
|
|
1610
1651
|
|
package/src/sourcemaps.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
// Source Map V3
|
|
1
|
+
// Source Map V3 — zero dependencies
|
|
2
2
|
//
|
|
3
3
|
// Implements the ECMA-426 Source Map specification (V3).
|
|
4
|
-
// Generates .map JSON files that map compiled
|
|
5
|
-
// back to original Rip source positions.
|
|
4
|
+
// Generates and parses .map JSON files that map compiled
|
|
5
|
+
// JavaScript back to original Rip source positions.
|
|
6
6
|
|
|
7
7
|
const B64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
|
|
8
8
|
|
|
@@ -118,4 +118,72 @@ class SourceMapGenerator {
|
|
|
118
118
|
}
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
-
|
|
121
|
+
// Decode a Base64-VLQ string back to signed integers
|
|
122
|
+
function vlqDecode(str) {
|
|
123
|
+
const values = [];
|
|
124
|
+
let i = 0;
|
|
125
|
+
while (i < str.length) {
|
|
126
|
+
let value = 0, shift = 0, digit;
|
|
127
|
+
do {
|
|
128
|
+
digit = B64.indexOf(str[i++]);
|
|
129
|
+
value |= (digit & 0x1F) << shift;
|
|
130
|
+
shift += 5;
|
|
131
|
+
} while (digit & 0x20);
|
|
132
|
+
values.push(value & 1 ? -(value >> 1) : value >> 1);
|
|
133
|
+
}
|
|
134
|
+
return values;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Parse a Source Map V3 JSON string into a generated-line → source-line Map
|
|
138
|
+
function parseSourceMap(mapJSON) {
|
|
139
|
+
const map = JSON.parse(mapJSON);
|
|
140
|
+
const genToSrc = new Map();
|
|
141
|
+
let srcLine = 0, srcCol = 0, genCol = 0;
|
|
142
|
+
|
|
143
|
+
const lines = map.mappings.split(';');
|
|
144
|
+
for (let genLine = 0; genLine < lines.length; genLine++) {
|
|
145
|
+
genCol = 0;
|
|
146
|
+
if (!lines[genLine]) continue;
|
|
147
|
+
for (const seg of lines[genLine].split(',')) {
|
|
148
|
+
const fields = vlqDecode(seg);
|
|
149
|
+
if (fields.length < 4) continue;
|
|
150
|
+
genCol += fields[0];
|
|
151
|
+
srcLine += fields[2];
|
|
152
|
+
srcCol += fields[3];
|
|
153
|
+
if (!genToSrc.has(genLine)) genToSrc.set(genLine, srcLine);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return genToSrc;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Build bidirectional line maps from compiler output.
|
|
160
|
+
// Tries reverseMap first (detailed mapping from the compiler), falls back
|
|
161
|
+
// to decoding the VLQ source map JSON. Keys in the returned maps are
|
|
162
|
+
// adjusted by headerLines so they correspond to tsContent line numbers.
|
|
163
|
+
function buildLineMap(reverseMap, mapJSON, headerLines) {
|
|
164
|
+
const srcToGen = new Map();
|
|
165
|
+
const genToSrc = new Map();
|
|
166
|
+
|
|
167
|
+
let hasEntries = false;
|
|
168
|
+
if (reverseMap) {
|
|
169
|
+
for (const [srcLine, { genLine }] of reverseMap) {
|
|
170
|
+
const adj = genLine + headerLines;
|
|
171
|
+
srcToGen.set(srcLine, adj);
|
|
172
|
+
genToSrc.set(adj, srcLine);
|
|
173
|
+
hasEntries = true;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (!hasEntries && mapJSON) {
|
|
178
|
+
const vlqMap = parseSourceMap(mapJSON);
|
|
179
|
+
for (const [genLine, srcLine] of vlqMap) {
|
|
180
|
+
const adj = genLine + headerLines;
|
|
181
|
+
srcToGen.set(srcLine, adj);
|
|
182
|
+
genToSrc.set(adj, srcLine);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return { srcToGen, genToSrc };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export { SourceMapGenerator, vlqEncode, vlqDecode, parseSourceMap, buildLineMap };
|
package/src/typecheck.js
ADDED
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
// Shared type-checking infrastructure for Rip
|
|
2
|
+
//
|
|
3
|
+
// Used by both the CLI type-checker (bin/rip check) and the
|
|
4
|
+
// VS Code language server (packages/vscode/src/lsp.js).
|
|
5
|
+
//
|
|
6
|
+
// compileForCheck() — the shared compilation pipeline that transforms
|
|
7
|
+
// .rip source into TypeScript content suitable for type-checking.
|
|
8
|
+
//
|
|
9
|
+
// runCheck() — the CLI batch type-checker that compiles all .rip files
|
|
10
|
+
// in a directory, creates a TypeScript language service, and reports
|
|
11
|
+
// type errors mapped back to Rip source positions.
|
|
12
|
+
|
|
13
|
+
import { Compiler } from './compiler.js';
|
|
14
|
+
import { readFileSync, existsSync, readdirSync } from 'fs';
|
|
15
|
+
import { resolve, relative, dirname } from 'path';
|
|
16
|
+
import { buildLineMap } from './sourcemaps.js';
|
|
17
|
+
|
|
18
|
+
// ── Shared helpers ─────────────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
// Detect type annotations (:: followed by space or =) ignoring comments
|
|
21
|
+
// and prototype syntax (Class::method).
|
|
22
|
+
export function hasTypeAnnotations(source) {
|
|
23
|
+
return source.split('\n').some(line => /::[ \t=]/.test(line.replace(/#.*$/, '')));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function countLines(str) {
|
|
27
|
+
let n = 0;
|
|
28
|
+
for (let i = 0; i < str.length; i++) if (str[i] === '\n') n++;
|
|
29
|
+
return n;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function toVirtual(p) { return p + '.ts'; }
|
|
33
|
+
export function fromVirtual(p) { return p.endsWith('.rip.ts') ? p.slice(0, -3) : p; }
|
|
34
|
+
|
|
35
|
+
// TS error codes to skip — Rip resolves modules differently and
|
|
36
|
+
// treats async return types transparently.
|
|
37
|
+
export const SKIP_CODES = new Set([
|
|
38
|
+
2307, // Cannot find module
|
|
39
|
+
2304, // Cannot find name
|
|
40
|
+
1064, // Return type of async function must be Promise
|
|
41
|
+
2582, // Cannot find name 'test' (test runner globals)
|
|
42
|
+
2593, // Cannot find name 'describe' (test runner globals)
|
|
43
|
+
]);
|
|
44
|
+
|
|
45
|
+
// Base TypeScript compiler settings for type-checking. Callers can
|
|
46
|
+
// pass overrides (e.g. { noImplicitAny: true } for the CLI).
|
|
47
|
+
export function createTypeCheckSettings(ts, overrides = {}) {
|
|
48
|
+
return {
|
|
49
|
+
target: ts.ScriptTarget.ESNext,
|
|
50
|
+
module: ts.ModuleKind.ESNext,
|
|
51
|
+
moduleResolution: ts.ModuleResolutionKind.Bundler,
|
|
52
|
+
allowJs: true,
|
|
53
|
+
strict: false,
|
|
54
|
+
strictNullChecks: true,
|
|
55
|
+
noEmit: true,
|
|
56
|
+
skipLibCheck: true,
|
|
57
|
+
...overrides,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ── Shared compilation pipeline ────────────────────────────────────
|
|
62
|
+
|
|
63
|
+
// Compile a .rip file for type-checking. Merges .d.ts declarations into
|
|
64
|
+
// the compiled JS, detects type annotations, and builds bidirectional
|
|
65
|
+
// source maps. Returns everything both the CLI and LSP need.
|
|
66
|
+
export function compileForCheck(filePath, source, compiler) {
|
|
67
|
+
const result = compiler.compile(source, { sourceMap: true, types: true });
|
|
68
|
+
let code = result.code || '';
|
|
69
|
+
let dts = result.dts ? result.dts.trimEnd() + '\n' : '';
|
|
70
|
+
|
|
71
|
+
// Strip .d.ts imports — compiled JS already has them
|
|
72
|
+
dts = dts.replace(/^import\s.*;\s*\n/gm, '');
|
|
73
|
+
|
|
74
|
+
// Extract well-formed function signatures and merge into JS.
|
|
75
|
+
// Leaving them as bare declarations causes TypeScript to treat
|
|
76
|
+
// them as overload signatures that conflict with the implementations.
|
|
77
|
+
const funcSigs = new Map();
|
|
78
|
+
dts = dts.replace(
|
|
79
|
+
/^(?:export|declare)\s+function\s+(\w+)\(([^)]*)\):\s*(.+);\s*$/gm,
|
|
80
|
+
(_m, name, params, ret) => { funcSigs.set(name, { params, ret }); return ''; },
|
|
81
|
+
);
|
|
82
|
+
dts = dts.replace(/^\s*\n/gm, '');
|
|
83
|
+
|
|
84
|
+
// Strip remaining malformed multi-line declarations
|
|
85
|
+
dts = dts.replace(/(?:export|declare)\s+function\s+\w+\([\s\S]*?\);\s*/g, '');
|
|
86
|
+
dts = dts.replace(/^\s*\n/gm, '');
|
|
87
|
+
|
|
88
|
+
for (const [name, { params, ret }] of funcSigs) {
|
|
89
|
+
const paramTypes = new Map();
|
|
90
|
+
if (params.trim()) {
|
|
91
|
+
for (const p of params.split(',')) {
|
|
92
|
+
const colon = p.indexOf(':');
|
|
93
|
+
if (colon !== -1) paramTypes.set(p.slice(0, colon).trim(), p.slice(colon + 1).trim());
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
const funcRe = new RegExp(
|
|
97
|
+
`((?:export\\s+)?(?:async\\s+)?function\\s+${name})\\(([^)]*)\\)(\\s*\\{)`,
|
|
98
|
+
);
|
|
99
|
+
code = code.replace(funcRe, (_match, prefix, codeParams, brace) => {
|
|
100
|
+
const typed = codeParams.split(',').map(p => {
|
|
101
|
+
const n = p.trim();
|
|
102
|
+
const t = paramTypes.get(n);
|
|
103
|
+
return t ? `${n}: ${t}` : n;
|
|
104
|
+
}).join(', ');
|
|
105
|
+
return `${prefix}(${typed}): ${ret}${brace}`;
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Remove bare `let x;` declarations when the DTS already declares
|
|
110
|
+
// `let x: Type;` — avoids "Cannot redeclare" conflicts. Handles
|
|
111
|
+
// both single (`let x;`) and comma-separated (`let x, y;`) forms.
|
|
112
|
+
const dtsVars = new Set();
|
|
113
|
+
for (const m of dts.matchAll(/^(?:let|var)\s+(\w+)\s*:/gm)) dtsVars.add(m[1]);
|
|
114
|
+
if (dtsVars.size) {
|
|
115
|
+
code = code.replace(/^(let|var)\s+([\w\s,]+);[ \t]*$/gm, (_m, kw, vars) => {
|
|
116
|
+
const kept = vars.split(',').map(v => v.trim()).filter(v => !dtsVars.has(v));
|
|
117
|
+
return kept.length ? `${kw} ${kept.join(', ')};` : '';
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Determine if this file should be type-checked
|
|
122
|
+
const hasOwnTypes = hasTypeAnnotations(source);
|
|
123
|
+
let importsTyped = false;
|
|
124
|
+
if (!hasOwnTypes) {
|
|
125
|
+
const ripImports = [...source.matchAll(/from\s+['"]([^'"]*\.rip)['"]/g)];
|
|
126
|
+
for (const m of ripImports) {
|
|
127
|
+
const imported = resolve(dirname(filePath), m[1]);
|
|
128
|
+
try {
|
|
129
|
+
const impSrc = readFileSync(imported, 'utf8');
|
|
130
|
+
if (hasTypeAnnotations(impSrc)) { importsTyped = true; break; }
|
|
131
|
+
} catch {}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
const hasTypes = hasOwnTypes || importsTyped;
|
|
135
|
+
if (!hasTypes) code = '// @ts-nocheck\n' + code;
|
|
136
|
+
|
|
137
|
+
// Ensure every file is treated as a module (not a global script)
|
|
138
|
+
if (!/\bexport\b/.test(code) && !/\bimport\b/.test(code)) code += '\nexport {};\n';
|
|
139
|
+
|
|
140
|
+
const tsContent = (hasTypes ? dts + '\n' : '') + code;
|
|
141
|
+
const headerLines = hasTypes ? countLines(dts + '\n') : 1;
|
|
142
|
+
|
|
143
|
+
// Build bidirectional line maps
|
|
144
|
+
const { srcToGen, genToSrc } = buildLineMap(result.reverseMap, result.map, headerLines);
|
|
145
|
+
|
|
146
|
+
// Map DTS variable declaration lines back to their source lines.
|
|
147
|
+
// TypeScript may report errors on the `let x: Type;` line in the
|
|
148
|
+
// DTS header, which has no entry in genToSrc. Fix by matching
|
|
149
|
+
// variable names to source lines with `x::`.
|
|
150
|
+
if (hasTypes && dts) {
|
|
151
|
+
const dtsLines = dts.split('\n');
|
|
152
|
+
const srcLines = source.split('\n');
|
|
153
|
+
for (let i = 0; i < dtsLines.length; i++) {
|
|
154
|
+
const m = dtsLines[i].match(/^(?:let|var)\s+(\w+)\s*:/);
|
|
155
|
+
if (!m) continue;
|
|
156
|
+
const varName = m[1];
|
|
157
|
+
for (let s = 0; s < srcLines.length; s++) {
|
|
158
|
+
if (new RegExp('\\b' + varName + '\\s*::').test(srcLines[s])) {
|
|
159
|
+
genToSrc.set(i, s);
|
|
160
|
+
break;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return { tsContent, headerLines, hasTypes, srcToGen, genToSrc, source };
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// ── Source mapping helpers ──────────────────────────────────────────
|
|
170
|
+
|
|
171
|
+
export function offsetToLine(text, offset) {
|
|
172
|
+
let line = 0;
|
|
173
|
+
for (let i = 0; i < offset && i < text.length; i++) {
|
|
174
|
+
if (text[i] === '\n') line++;
|
|
175
|
+
}
|
|
176
|
+
return line;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Map a TypeScript diagnostic offset back to a Rip source line number.
|
|
180
|
+
// Returns -1 if the offset falls in the DTS header.
|
|
181
|
+
export function mapToSource(entry, offset) {
|
|
182
|
+
const tsLine = offsetToLine(entry.tsContent, offset);
|
|
183
|
+
if (tsLine < entry.headerLines) return -1;
|
|
184
|
+
|
|
185
|
+
if (entry.genToSrc.has(tsLine)) return entry.genToSrc.get(tsLine);
|
|
186
|
+
for (let d = 1; d <= 3; d++) {
|
|
187
|
+
if (entry.genToSrc.has(tsLine - d)) return entry.genToSrc.get(tsLine - d);
|
|
188
|
+
if (entry.genToSrc.has(tsLine + d)) return entry.genToSrc.get(tsLine + d);
|
|
189
|
+
}
|
|
190
|
+
return tsLine - entry.headerLines;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// ── CLI batch type-checker ─────────────────────────────────────────
|
|
194
|
+
|
|
195
|
+
function findRipFiles(dir, files = []) {
|
|
196
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
197
|
+
if (entry.name.startsWith('.') || entry.name === 'node_modules') continue;
|
|
198
|
+
const full = resolve(dir, entry.name);
|
|
199
|
+
if (entry.isDirectory()) findRipFiles(full, files);
|
|
200
|
+
else if (entry.name.endsWith('.rip')) files.push(full);
|
|
201
|
+
}
|
|
202
|
+
return files;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const isColor = process.stdout.isTTY !== false;
|
|
206
|
+
const red = (s) => isColor ? `\x1b[31m${s}\x1b[0m` : s;
|
|
207
|
+
const yellow = (s) => isColor ? `\x1b[33m${s}\x1b[0m` : s;
|
|
208
|
+
const cyan = (s) => isColor ? `\x1b[36m${s}\x1b[0m` : s;
|
|
209
|
+
const dim = (s) => isColor ? `\x1b[2m${s}\x1b[0m` : s;
|
|
210
|
+
const bold = (s) => isColor ? `\x1b[1m${s}\x1b[0m` : s;
|
|
211
|
+
|
|
212
|
+
export async function runCheck(targetDir, opts = {}) {
|
|
213
|
+
const ts = await import('typescript').then(m => m.default || m);
|
|
214
|
+
const rootPath = resolve(targetDir);
|
|
215
|
+
|
|
216
|
+
if (!existsSync(rootPath)) {
|
|
217
|
+
console.error(red(`Error: directory not found: ${targetDir}`));
|
|
218
|
+
return 1;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const allFiles = findRipFiles(rootPath);
|
|
222
|
+
if (allFiles.length === 0) {
|
|
223
|
+
console.error(red(`No .rip files found in ${targetDir}`));
|
|
224
|
+
return 1;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Compile all files
|
|
228
|
+
const compiled = new Map();
|
|
229
|
+
const compiler = new Compiler();
|
|
230
|
+
let compileErrors = 0;
|
|
231
|
+
|
|
232
|
+
for (const fp of allFiles) {
|
|
233
|
+
try {
|
|
234
|
+
const source = readFileSync(fp, 'utf8');
|
|
235
|
+
compiled.set(fp, compileForCheck(fp, source, compiler));
|
|
236
|
+
} catch (e) {
|
|
237
|
+
compileErrors++;
|
|
238
|
+
const rel = relative(rootPath, fp);
|
|
239
|
+
console.error(`${red('error')} ${cyan(rel)}: compile error — ${e.message}`);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Also compile any .rip files imported from typed files that aren't in rootPath
|
|
244
|
+
for (const [fp, entry] of [...compiled.entries()]) {
|
|
245
|
+
if (!entry.hasTypes) continue;
|
|
246
|
+
const ripImports = [...entry.source.matchAll(/from\s+['"]([^'"]*\.rip)['"]/g)];
|
|
247
|
+
for (const m of ripImports) {
|
|
248
|
+
const imported = resolve(dirname(fp), m[1]);
|
|
249
|
+
if (!compiled.has(imported) && existsSync(imported)) {
|
|
250
|
+
try {
|
|
251
|
+
const impSrc = readFileSync(imported, 'utf8');
|
|
252
|
+
compiled.set(imported, compileForCheck(imported, impSrc, compiler));
|
|
253
|
+
} catch {}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Create TypeScript language service
|
|
259
|
+
const settings = createTypeCheckSettings(ts, { noImplicitAny: true });
|
|
260
|
+
|
|
261
|
+
const host = {
|
|
262
|
+
getScriptFileNames: () => [...compiled.keys()].map(toVirtual),
|
|
263
|
+
getScriptVersion: () => '1',
|
|
264
|
+
getScriptSnapshot(f) {
|
|
265
|
+
const c = compiled.get(fromVirtual(f));
|
|
266
|
+
if (c) return ts.ScriptSnapshot.fromString(c.tsContent);
|
|
267
|
+
try { return ts.ScriptSnapshot.fromString(readFileSync(f, 'utf8')); } catch { return undefined; }
|
|
268
|
+
},
|
|
269
|
+
getCompilationSettings: () => settings,
|
|
270
|
+
getDefaultLibFileName: (o) => ts.getDefaultLibFilePath(o),
|
|
271
|
+
getCurrentDirectory: () => rootPath,
|
|
272
|
+
fileExists(f) { return compiled.has(fromVirtual(f)) || ts.sys.fileExists(f); },
|
|
273
|
+
readFile(f) { return compiled.get(fromVirtual(f))?.tsContent || ts.sys.readFile(f); },
|
|
274
|
+
readDirectory: (...a) => ts.sys.readDirectory(...a),
|
|
275
|
+
getDirectories: (...a) => ts.sys.getDirectories(...a),
|
|
276
|
+
directoryExists: (...a) => ts.sys.directoryExists(...a),
|
|
277
|
+
|
|
278
|
+
resolveModuleNames(names, containingFile) {
|
|
279
|
+
return names.map((name) => {
|
|
280
|
+
if (name.endsWith('.rip')) {
|
|
281
|
+
const resolved = resolve(dirname(fromVirtual(containingFile)), name);
|
|
282
|
+
if (compiled.has(resolved)) {
|
|
283
|
+
return { resolvedFileName: toVirtual(resolved), extension: '.ts', isExternalLibraryImport: false };
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
const r = ts.resolveModuleName(name, containingFile, settings, {
|
|
287
|
+
fileExists: host.fileExists,
|
|
288
|
+
readFile: host.readFile,
|
|
289
|
+
directoryExists: host.directoryExists,
|
|
290
|
+
getCurrentDirectory: host.getCurrentDirectory,
|
|
291
|
+
getDirectories: host.getDirectories,
|
|
292
|
+
});
|
|
293
|
+
return r.resolvedModule;
|
|
294
|
+
});
|
|
295
|
+
},
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
const service = ts.createLanguageService(host, ts.createDocumentRegistry());
|
|
299
|
+
|
|
300
|
+
// Collect diagnostics
|
|
301
|
+
let totalErrors = 0;
|
|
302
|
+
let totalWarnings = 0;
|
|
303
|
+
const fileResults = [];
|
|
304
|
+
|
|
305
|
+
for (const [fp, entry] of compiled) {
|
|
306
|
+
if (!entry.hasTypes) continue;
|
|
307
|
+
|
|
308
|
+
const vf = toVirtual(fp);
|
|
309
|
+
let diags;
|
|
310
|
+
try {
|
|
311
|
+
const sem = service.getSemanticDiagnostics(vf);
|
|
312
|
+
const syn = service.getSyntacticDiagnostics(vf);
|
|
313
|
+
diags = [...syn, ...sem];
|
|
314
|
+
} catch {
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const errors = [];
|
|
319
|
+
for (const d of diags) {
|
|
320
|
+
if (d.start === undefined) continue;
|
|
321
|
+
if (SKIP_CODES.has(d.code)) continue;
|
|
322
|
+
|
|
323
|
+
const srcLine = mapToSource(entry, d.start);
|
|
324
|
+
if (srcLine < 0) continue;
|
|
325
|
+
|
|
326
|
+
const message = ts.flattenDiagnosticMessageText(d.messageText, '\n');
|
|
327
|
+
const severity = d.category === 1 ? 'error' : d.category === 0 ? 'warning' : 'info';
|
|
328
|
+
|
|
329
|
+
errors.push({ line: srcLine + 1, message, severity, code: d.code });
|
|
330
|
+
if (severity === 'error') totalErrors++;
|
|
331
|
+
else if (severity === 'warning') totalWarnings++;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
if (errors.length > 0) {
|
|
335
|
+
fileResults.push({ file: fp, errors });
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Print results
|
|
340
|
+
for (const { file, errors } of fileResults) {
|
|
341
|
+
const rel = relative(rootPath, file);
|
|
342
|
+
for (const e of errors) {
|
|
343
|
+
const loc = `${cyan(rel)}${dim(':')}${yellow(String(e.line))}`;
|
|
344
|
+
const sev = e.severity === 'error' ? red('error') : yellow('warning');
|
|
345
|
+
console.log(`${loc} ${sev} ${e.message} ${dim(`TS${e.code}`)}`);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// Summary
|
|
350
|
+
const typedFiles = [...compiled.values()].filter(e => e.hasTypes).length;
|
|
351
|
+
const totalFiles = compiled.size;
|
|
352
|
+
|
|
353
|
+
if (totalErrors === 0 && totalWarnings === 0) {
|
|
354
|
+
console.log(`\n${bold('✓')} ${typedFiles} typed file${typedFiles !== 1 ? 's' : ''} checked, no errors found`);
|
|
355
|
+
if (compileErrors > 0) {
|
|
356
|
+
console.log(dim(` (${compileErrors} file${compileErrors !== 1 ? 's' : ''} had compile errors)`));
|
|
357
|
+
}
|
|
358
|
+
return compileErrors > 0 ? 1 : 0;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const parts = [];
|
|
362
|
+
if (totalErrors > 0) parts.push(red(`${totalErrors} error${totalErrors !== 1 ? 's' : ''}`));
|
|
363
|
+
if (totalWarnings > 0) parts.push(yellow(`${totalWarnings} warning${totalWarnings !== 1 ? 's' : ''}`));
|
|
364
|
+
console.log(`\n${bold('✗')} ${parts.join(', ')} in ${fileResults.length} file${fileResults.length !== 1 ? 's' : ''} (${typedFiles} typed / ${totalFiles} total)`);
|
|
365
|
+
|
|
366
|
+
return totalErrors > 0 ? 1 : 0;
|
|
367
|
+
}
|