rip-lang 3.9.2 → 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 +2 -6
- package/src/browser.js +1 -0
- package/src/compiler.js +3 -1
- package/src/components.js +130 -37
- package/src/sourcemaps.js +72 -4
- package/src/typecheck.js +367 -0
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
|
+
}
|