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.
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rip-lang",
3
- "version": "3.9.3",
3
+ "version": "3.10.0",
4
4
  "description": "A modern language that compiles to JavaScript",
5
5
  "type": "module",
6
6
  "main": "src/compiler.js",
package/src/browser.js CHANGED
@@ -32,6 +32,7 @@ async function processRipScripts() {
32
32
 
33
33
  for (const script of scripts) {
34
34
  if (script.hasAttribute('data-rip-processed')) continue;
35
+ if (script.hasAttribute('data-name')) continue;
35
36
 
36
37
  try {
37
38
  let ripCode;
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(` this.${name} = props.${name} ?? ${val};`);
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(` this.${name} = __state(props.${name} ?? ${val});`);
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
- // Event listener: DOM signal (two-way)
1106
- const event = key === 'checked' ? 'change' : 'input';
1107
- const accessor = key === 'checked' ? 'e.target.checked'
1108
- : (inputType === 'number' || inputType === 'range') ? 'e.target.valueAsNumber'
1109
- : 'e.target.value';
1110
- this._createLines.push(`${elVar}.addEventListener('${event}', (e) => { ${valueCode} = ${accessor}; });`);
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 Generator — zero dependencies
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 JavaScript
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
- export { SourceMapGenerator, vlqEncode };
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 };
@@ -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
+ }