rip-lang 3.14.4 → 3.15.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/README.md +9 -5
- package/bin/rip +5 -0
- package/docs/AGENTS.md +1 -1
- package/docs/RIP-LANG.md +17 -5
- package/docs/RIP-SCHEMA.md +4 -4
- package/docs/demo/README.md +43 -0
- package/docs/demo/components/_layout.rip +28 -0
- package/docs/demo/components/about.rip +36 -0
- package/docs/demo/components/card.rip +10 -0
- package/docs/demo/components/counter.rip +33 -0
- package/docs/demo/components/index.rip +30 -0
- package/docs/demo/components/todos.rip +48 -0
- package/docs/demo/css/styles.css +472 -0
- package/docs/dist/rip.js +3331 -4741
- package/docs/dist/rip.min.js +270 -683
- package/docs/dist/rip.min.js.br +0 -0
- package/docs/example/index.json +6 -6
- package/docs/extensions/duckdb/index.html +7 -5
- package/docs/extensions/duckdb/manifest.json +1 -1
- package/docs/extensions/duckdb/v1.5.2/linux_amd64/ripdb.duckdb_extension.gz +0 -0
- package/docs/extensions/duckdb/v1.5.2/osx_arm64/ripdb.duckdb_extension.gz +0 -0
- package/package.json +11 -3
- package/src/AGENTS.md +105 -9
- package/src/{ui.rip → app.rip} +24 -2
- package/src/browser.js +154 -37
- package/src/compiler.js +108 -32
- package/src/grammar/grammar.rip +1 -1
- package/src/grammar/solar.rip +0 -1
- package/src/lexer.js +25 -3
- package/src/parser.js +4 -4
- package/src/typecheck.js +3 -2
- package/src/types-emit.js +1021 -0
- package/src/types.js +11 -1035
- package/src/schema.js +0 -3389
|
@@ -0,0 +1,1021 @@
|
|
|
1
|
+
import { SCHEMA_INTRINSIC_DECLS, emitSchemaTypes } from "@rip-lang/schema/dts-emit";
|
|
2
|
+
import { setTypesEmitter } from "./compiler.js";
|
|
3
|
+
|
|
4
|
+
// Type System — .d.ts emission for Rip (CLI / typecheck only).
|
|
5
|
+
//
|
|
6
|
+
// This module is a CLI/editor-only sidecar:
|
|
7
|
+
//
|
|
8
|
+
// emitTypes(tokens, sexpr, source) — generates .d.ts from annotated
|
|
9
|
+
// tokens and the parsed s-expression tree.
|
|
10
|
+
//
|
|
11
|
+
// INTRINSIC_TYPE_DECLS / INTRINSIC_FN_DECL / ARIA_TYPE_DECLS /
|
|
12
|
+
// SIGNAL_*, COMPUTED_*, EFFECT_* — declaration tables consumed by
|
|
13
|
+
// emitTypes() and by typecheck.js when building the virtual TS
|
|
14
|
+
// file. Browser code never references these.
|
|
15
|
+
//
|
|
16
|
+
// The browser bundle must NOT import this module — see
|
|
17
|
+
// scripts/check-bundle-graph.js. Token-level type stripping
|
|
18
|
+
// (installTypeSupport) and runtime enum codegen (emitEnum) live
|
|
19
|
+
// in types.js, which the browser does need.
|
|
20
|
+
|
|
21
|
+
// ============================================================================
|
|
22
|
+
// Shared type declaration constants — single source of truth
|
|
23
|
+
// ============================================================================
|
|
24
|
+
// Used by emitTypes() for .d.ts emission and by compileForCheck() in
|
|
25
|
+
// typecheck.js for virtual .ts injection. Keeping them here eliminates
|
|
26
|
+
// divergence between what gets written to disk and what TS analyzes.
|
|
27
|
+
|
|
28
|
+
export const INTRINSIC_TYPE_DECLS = [
|
|
29
|
+
'type __RipElementMap = HTMLElementTagNameMap & Omit<SVGElementTagNameMap, keyof HTMLElementTagNameMap>;',
|
|
30
|
+
'type __RipTag = keyof __RipElementMap;',
|
|
31
|
+
"type __RipBrowserElement = Omit<HTMLElement, 'querySelector' | 'querySelectorAll' | 'closest' | 'setAttribute' | 'hidden'> & { hidden: boolean | 'until-found'; setAttribute(qualifiedName: string, value: any): void; querySelector(selectors: string): __RipBrowserElement | null; querySelectorAll(selectors: string): NodeListOf<__RipBrowserElement>; closest(selectors: string): __RipBrowserElement | null; };",
|
|
32
|
+
"type __RipDomEl<K extends __RipTag> = Omit<__RipElementMap[K], 'querySelector' | 'querySelectorAll' | 'closest' | 'setAttribute' | 'hidden'> & __RipBrowserElement;",
|
|
33
|
+
"type __RipAttrKeys<T> = { [K in keyof T]-?: K extends 'style' | 'classList' | 'className' | 'nodeValue' | 'textContent' | 'innerHTML' | 'innerText' | 'outerHTML' | 'outerText' | 'scrollLeft' | 'scrollTop' ? never : K extends `on${string}` | `aria${string}Element` | `aria${string}Elements` ? never : T[K] extends (...args: any[]) => any ? never : (<V>() => V extends Pick<T, K> ? 1 : 2) extends (<V>() => V extends { -readonly [P in K]: T[P] } ? 1 : 2) ? K : never }[keyof T] & string;",
|
|
34
|
+
'type __RipEvents = { [K in keyof HTMLElementEventMap as `@${K}`]?: ((event: HTMLElementEventMap[K]) => void) | null };',
|
|
35
|
+
'type __RipClassValue = string | boolean | null | undefined | Record<string, boolean> | __RipClassValue[];',
|
|
36
|
+
'type __RipProps<K extends __RipTag> = { [P in __RipAttrKeys<__RipElementMap[K]>]?: __RipElementMap[K][P] } & __RipEvents & { ref?: string; class?: __RipClassValue | __RipClassValue[]; style?: string; [k: `data-${string}`]: any; [k: `aria-${string}`]: any };',
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
export const INTRINSIC_FN_DECL = 'declare function __ripEl<K extends __RipTag>(tag: K, props?: __RipProps<K>): void;';
|
|
40
|
+
|
|
41
|
+
export const ARIA_TYPE_DECLS = [
|
|
42
|
+
'type __RipAriaNavHandlers = { next?: () => void; prev?: () => void; first?: () => void; last?: () => void; select?: () => void; dismiss?: () => void; tab?: () => void; char?: () => void; };',
|
|
43
|
+
"declare const ARIA: {",
|
|
44
|
+
" bindPopover(open: boolean, popover: () => Element | null | undefined, setOpen: (isOpen: boolean) => void, source?: (() => Element | null | undefined) | null): void;",
|
|
45
|
+
" bindDialog(open: boolean, dialog: () => Element | null | undefined, setOpen: (isOpen: boolean) => void, dismissable?: boolean): void;",
|
|
46
|
+
" popupDismiss(open: boolean, popup: () => Element | null | undefined, close: () => void, els?: Array<() => Element | null | undefined>, repos?: (() => void) | null): void;",
|
|
47
|
+
" popupGuard(delay?: number): any;",
|
|
48
|
+
" listNav(event: KeyboardEvent, handlers: __RipAriaNavHandlers): void;",
|
|
49
|
+
" rovingNav(event: KeyboardEvent, handlers: __RipAriaNavHandlers, orientation?: 'vertical' | 'horizontal' | 'both'): void;",
|
|
50
|
+
" positionBelow(trigger: Element | null | undefined, popup: Element | null | undefined, gap?: number, setVisible?: boolean): void;",
|
|
51
|
+
" position(trigger: Element | null | undefined, floating: Element | null | undefined, opts?: any): void;",
|
|
52
|
+
" trapFocus(panel: Element | null | undefined): void;",
|
|
53
|
+
" wireAria(panel: Element, id: string): void;",
|
|
54
|
+
" lockScroll(instance: any): void;",
|
|
55
|
+
" unlockScroll(instance: any): void;",
|
|
56
|
+
" hasAnchor: boolean;",
|
|
57
|
+
" [key: string]: any;",
|
|
58
|
+
"};",
|
|
59
|
+
];
|
|
60
|
+
|
|
61
|
+
export const SIGNAL_INTERFACE = 'interface Signal<T> { value: T; read(): T; lock(): Signal<T>; free(): Signal<T>; kill(): T; }';
|
|
62
|
+
export const SIGNAL_FN = 'declare function __state<T>(value: T | Signal<T>): Signal<T>;';
|
|
63
|
+
export const COMPUTED_INTERFACE = 'interface Computed<T> { readonly value: T; read(): T; lock(): Computed<T>; free(): Computed<T>; kill(): T; }';
|
|
64
|
+
export const COMPUTED_FN = 'declare function __computed<T>(fn: () => T): Computed<T>;';
|
|
65
|
+
export const EFFECT_FN = 'declare function __effect(fn: () => void | (() => void)): () => void;';
|
|
66
|
+
|
|
67
|
+
// ============================================================================
|
|
68
|
+
// emitTypes — generate .d.ts from annotated tokens + s-expression tree
|
|
69
|
+
// ============================================================================
|
|
70
|
+
|
|
71
|
+
export function emitTypes(tokens, sexpr = null, source = '') {
|
|
72
|
+
let lines = [];
|
|
73
|
+
let indentLevel = 0;
|
|
74
|
+
let indentStr = ' ';
|
|
75
|
+
let indent = () => indentStr.repeat(indentLevel);
|
|
76
|
+
let inClass = false;
|
|
77
|
+
let classFields = new Set(); // Track emitted field names to avoid duplicates
|
|
78
|
+
let usesSignal = false;
|
|
79
|
+
let usesComputed = false;
|
|
80
|
+
let usesRipIntrinsicProps = false;
|
|
81
|
+
const sourceLines = typeof source === 'string' ? source.split('\n') : [];
|
|
82
|
+
|
|
83
|
+
// Pre-scan: detect reactive operators regardless of type annotations.
|
|
84
|
+
// This ensures the DTS preamble declares __state/__computed/__effect
|
|
85
|
+
// so TypeScript can infer types for untyped reactive variables.
|
|
86
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
87
|
+
const tag = tokens[i][0];
|
|
88
|
+
if (tag === 'REACTIVE_ASSIGN') usesSignal = true;
|
|
89
|
+
else if (tag === 'COMPUTED_ASSIGN') usesComputed = true;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Format { prop; prop } into multi-line block. Only applies when the
|
|
93
|
+
// body is a single { ... } object — not a union like { ... } | { ... }.
|
|
94
|
+
let emitBlock = (prefix, body, suffix) => {
|
|
95
|
+
if (body.startsWith('{ ') && body.endsWith(' }')) {
|
|
96
|
+
let depth = 0, firstTopClose = -1;
|
|
97
|
+
for (let c = 0; c < body.length; c++) {
|
|
98
|
+
if (body[c] === '{') depth++;
|
|
99
|
+
else if (body[c] === '}') { depth--; if (depth === 0) { firstTopClose = c; break; } }
|
|
100
|
+
}
|
|
101
|
+
if (firstTopClose === body.length - 1) {
|
|
102
|
+
// Depth-aware split on '; ' at top level only
|
|
103
|
+
let inner = body.slice(2, -2);
|
|
104
|
+
let props = [], start = 0, d = 0;
|
|
105
|
+
for (let c = 0; c < inner.length; c++) {
|
|
106
|
+
if (inner[c] === '{') d++;
|
|
107
|
+
else if (inner[c] === '}') d--;
|
|
108
|
+
else if (d === 0 && inner[c] === ';' && inner[c + 1] === ' ') {
|
|
109
|
+
props.push(inner.slice(start, c));
|
|
110
|
+
start = c + 2;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
if (start < inner.length) props.push(inner.slice(start));
|
|
114
|
+
props = props.filter(p => p.trim());
|
|
115
|
+
if (props.length > 0) {
|
|
116
|
+
lines.push(`${indent()}${prefix}{`);
|
|
117
|
+
indentLevel++;
|
|
118
|
+
for (let prop of props) lines.push(`${indent()}${prop};`);
|
|
119
|
+
indentLevel--;
|
|
120
|
+
lines.push(`${indent()}}${suffix}`);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
lines.push(`${indent()}${prefix}${body}${suffix}`);
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
// Skip past a default value expression (= ...) returning the new j
|
|
129
|
+
let skipDefault = (tokens, j) => {
|
|
130
|
+
j++; // skip =
|
|
131
|
+
let dd = 0;
|
|
132
|
+
while (j < tokens.length) {
|
|
133
|
+
let dt = tokens[j];
|
|
134
|
+
if (dt[0] === '(' || dt[0] === '[' || dt[0] === '{') dd++;
|
|
135
|
+
if (dt[0] === ')' || dt[0] === ']' || dt[0] === '}') {
|
|
136
|
+
if (dd === 0) break; // closing bracket of enclosing structure
|
|
137
|
+
dd--;
|
|
138
|
+
}
|
|
139
|
+
if (dd === 0 && dt[1] === ',') break;
|
|
140
|
+
j++;
|
|
141
|
+
}
|
|
142
|
+
return j;
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
// Collect a destructured object { a, b: c, ...rest } recursively.
|
|
146
|
+
// tokens[startJ] must be '{'. Returns { patternStr, typeStr, endJ, hasAnyType }.
|
|
147
|
+
let collectDestructuredObj = (tokens, startJ) => {
|
|
148
|
+
let props = [];
|
|
149
|
+
let hasAnyType = false;
|
|
150
|
+
let j = startJ + 1; // skip opening {
|
|
151
|
+
let d = 1;
|
|
152
|
+
while (j < tokens.length && d > 0) {
|
|
153
|
+
if (tokens[j][0] === '{') d++;
|
|
154
|
+
if (tokens[j][0] === '}') d--;
|
|
155
|
+
if (d <= 0) { j++; break; }
|
|
156
|
+
|
|
157
|
+
// Rest: ...name
|
|
158
|
+
if (tokens[j][0] === '...' || tokens[j][0] === 'SPREAD') {
|
|
159
|
+
j++;
|
|
160
|
+
if (tokens[j]?.[0] === 'IDENTIFIER') {
|
|
161
|
+
props.push({ kind: 'rest', propName: tokens[j][1] });
|
|
162
|
+
j++;
|
|
163
|
+
}
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// PROPERTY : ... (rename, nested object, or nested array)
|
|
168
|
+
if (tokens[j][0] === 'PROPERTY' && tokens[j + 1]?.[0] === ':') {
|
|
169
|
+
let propName = tokens[j][1];
|
|
170
|
+
j += 2; // skip PROPERTY and :
|
|
171
|
+
// Nested object
|
|
172
|
+
if (tokens[j]?.[0] === '{') {
|
|
173
|
+
let inner = collectDestructuredObj(tokens, j);
|
|
174
|
+
if (inner.hasAnyType) hasAnyType = true;
|
|
175
|
+
props.push({ kind: 'nested-obj', propName, inner });
|
|
176
|
+
j = inner.endJ;
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
// Nested array
|
|
180
|
+
if (tokens[j]?.[0] === '[') {
|
|
181
|
+
let inner = collectDestructuredArr(tokens, j);
|
|
182
|
+
if (inner.hasAnyType) hasAnyType = true;
|
|
183
|
+
props.push({ kind: 'nested-arr', propName, inner });
|
|
184
|
+
j = inner.endJ;
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
// Simple rename: PROPERTY : IDENTIFIER
|
|
188
|
+
if (tokens[j]?.[0] === 'IDENTIFIER') {
|
|
189
|
+
let localName = tokens[j][1];
|
|
190
|
+
let type = tokens[j].data?.type;
|
|
191
|
+
if (type) hasAnyType = true;
|
|
192
|
+
let hasDefault = tokens[j + 1]?.[0] === '=';
|
|
193
|
+
props.push({ kind: 'rename', propName, localName, type: type ? expandSuffixes(type) : null, hasDefault });
|
|
194
|
+
j++;
|
|
195
|
+
if (hasDefault) j = skipDefault(tokens, j);
|
|
196
|
+
}
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Simple: IDENTIFIER (possibly with default)
|
|
201
|
+
if (tokens[j][0] === 'IDENTIFIER') {
|
|
202
|
+
let name = tokens[j][1];
|
|
203
|
+
let type = tokens[j].data?.type;
|
|
204
|
+
if (type) hasAnyType = true;
|
|
205
|
+
let hasDefault = tokens[j + 1]?.[0] === '=';
|
|
206
|
+
props.push({ kind: 'simple', propName: name, type: type ? expandSuffixes(type) : null, hasDefault });
|
|
207
|
+
j++;
|
|
208
|
+
if (hasDefault) j = skipDefault(tokens, j);
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Skip commas and other tokens
|
|
213
|
+
j++;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Build pattern and type strings
|
|
217
|
+
let patternParts = [];
|
|
218
|
+
let typeParts = [];
|
|
219
|
+
for (let p of props) {
|
|
220
|
+
if (p.kind === 'rest') {
|
|
221
|
+
patternParts.push(`...${p.propName}`);
|
|
222
|
+
typeParts.push(`[key: string]: unknown`);
|
|
223
|
+
} else if (p.kind === 'nested-obj' || p.kind === 'nested-arr') {
|
|
224
|
+
patternParts.push(`${p.propName}: ${p.inner.patternStr}`);
|
|
225
|
+
typeParts.push(`${p.propName}: ${p.inner.typeStr}`);
|
|
226
|
+
} else if (p.kind === 'rename') {
|
|
227
|
+
patternParts.push(`${p.propName}: ${p.localName}`);
|
|
228
|
+
typeParts.push(`${p.propName}${p.hasDefault ? '?' : ''}: ${p.type || 'any'}`);
|
|
229
|
+
} else {
|
|
230
|
+
patternParts.push(p.propName);
|
|
231
|
+
typeParts.push(`${p.propName}${p.hasDefault ? '?' : ''}: ${p.type || 'any'}`);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
return {
|
|
235
|
+
patternStr: `{${patternParts.join(', ')}}`,
|
|
236
|
+
typeStr: `{${typeParts.join(', ')}}`,
|
|
237
|
+
endJ: j,
|
|
238
|
+
hasAnyType,
|
|
239
|
+
};
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
// Collect a destructured array [ a, b ] from tokens.
|
|
243
|
+
// tokens[startJ] must be '['. Returns { patternStr, typeStr, endJ, hasAnyType }.
|
|
244
|
+
let collectDestructuredArr = (tokens, startJ) => {
|
|
245
|
+
let names = [];
|
|
246
|
+
let elemTypes = [];
|
|
247
|
+
let hasAnyType = false;
|
|
248
|
+
let j = startJ + 1; // skip opening [
|
|
249
|
+
let d = 1;
|
|
250
|
+
while (j < tokens.length && d > 0) {
|
|
251
|
+
if (tokens[j][0] === '[') d++;
|
|
252
|
+
if (tokens[j][0] === ']') d--;
|
|
253
|
+
if (d > 0 && tokens[j][0] === 'IDENTIFIER') {
|
|
254
|
+
let name = tokens[j][1];
|
|
255
|
+
let type = tokens[j].data?.type;
|
|
256
|
+
names.push(name);
|
|
257
|
+
elemTypes.push(type ? expandSuffixes(type) : null);
|
|
258
|
+
if (type) hasAnyType = true;
|
|
259
|
+
}
|
|
260
|
+
j++;
|
|
261
|
+
}
|
|
262
|
+
return {
|
|
263
|
+
patternStr: `[${names.join(', ')}]`,
|
|
264
|
+
typeStr: `[${elemTypes.map(t => t || 'any').join(', ')}]`,
|
|
265
|
+
endJ: j,
|
|
266
|
+
hasAnyType,
|
|
267
|
+
};
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
// Collect function parameters (handles simple, destructured, rest, defaults)
|
|
271
|
+
let collectParams = (tokens, startIdx) => {
|
|
272
|
+
let params = [];
|
|
273
|
+
let fields = []; // Track @param:: type for class field emission
|
|
274
|
+
let j = startIdx;
|
|
275
|
+
let openTag = tokens[j]?.[0];
|
|
276
|
+
if (openTag !== 'CALL_START' && openTag !== 'PARAM_START') return { params, fields, endIdx: j };
|
|
277
|
+
let closeTag = openTag === 'CALL_START' ? 'CALL_END' : 'PARAM_END';
|
|
278
|
+
j++;
|
|
279
|
+
let depth = 0;
|
|
280
|
+
|
|
281
|
+
while (j < tokens.length && !(tokens[j][0] === closeTag && depth === 0)) {
|
|
282
|
+
let tok = tokens[j];
|
|
283
|
+
|
|
284
|
+
// Skip commas at depth 0
|
|
285
|
+
if (tok[1] === ',' && depth === 0) { j++; continue; }
|
|
286
|
+
|
|
287
|
+
// Track nesting
|
|
288
|
+
if (tok[0] === '{' || tok[0] === '[' || tok[0] === 'CALL_START' ||
|
|
289
|
+
tok[0] === 'PARAM_START' || tok[0] === 'INDEX_START') depth++;
|
|
290
|
+
if (tok[0] === '}' || tok[0] === ']' || tok[0] === 'CALL_END' ||
|
|
291
|
+
tok[0] === 'PARAM_END' || tok[0] === 'INDEX_END') { depth--; j++; continue; }
|
|
292
|
+
|
|
293
|
+
// @ prefix (constructor shorthand: @name)
|
|
294
|
+
if (tok[0] === '@') {
|
|
295
|
+
j++;
|
|
296
|
+
if (tokens[j]?.[0] === 'PROPERTY' || tokens[j]?.[0] === 'IDENTIFIER') {
|
|
297
|
+
let name = tokens[j][1];
|
|
298
|
+
let type = tokens[j].data?.type;
|
|
299
|
+
params.push(type ? `${name}: ${expandSuffixes(type)}` : name);
|
|
300
|
+
if (type) fields.push({ name, type: expandSuffixes(type) });
|
|
301
|
+
j++;
|
|
302
|
+
}
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Rest parameter: ...name
|
|
307
|
+
if (tok[0] === 'SPREAD' || tok[1] === '...') {
|
|
308
|
+
j++;
|
|
309
|
+
if (tokens[j]?.[0] === 'IDENTIFIER') {
|
|
310
|
+
let name = tokens[j][1];
|
|
311
|
+
let type = tokens[j].data?.type;
|
|
312
|
+
params.push(type ? `...${name}: ${expandSuffixes(type)}` : `...${name}: any[]`);
|
|
313
|
+
j++;
|
|
314
|
+
}
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Destructured object parameter: { a, b }
|
|
319
|
+
if (tok[0] === '{') {
|
|
320
|
+
depth--; // undo the depth++ from nesting tracker above
|
|
321
|
+
let result = collectDestructuredObj(tokens, j);
|
|
322
|
+
j = result.endJ;
|
|
323
|
+
if (result.hasAnyType) {
|
|
324
|
+
params.push(`${result.patternStr}: ${result.typeStr}`);
|
|
325
|
+
} else {
|
|
326
|
+
params.push(result.patternStr);
|
|
327
|
+
}
|
|
328
|
+
continue;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Destructured array parameter: [ a, b ]
|
|
332
|
+
if (tok[0] === '[') {
|
|
333
|
+
depth--; // undo the depth++ from nesting tracker above
|
|
334
|
+
let result = collectDestructuredArr(tokens, j);
|
|
335
|
+
j = result.endJ;
|
|
336
|
+
if (result.hasAnyType) {
|
|
337
|
+
params.push(`${result.patternStr}: ${result.typeStr}`);
|
|
338
|
+
} else {
|
|
339
|
+
params.push(result.patternStr);
|
|
340
|
+
}
|
|
341
|
+
continue;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Simple identifier parameter
|
|
345
|
+
if (tok[0] === 'IDENTIFIER') {
|
|
346
|
+
let paramName = tok[1];
|
|
347
|
+
let paramType = tok.data?.type;
|
|
348
|
+
|
|
349
|
+
// Check for default value (skip = and the default expression)
|
|
350
|
+
let hasDefault = false;
|
|
351
|
+
if (tokens[j + 1]?.[0] === '=') {
|
|
352
|
+
hasDefault = true;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
let isOptional = hasDefault || tok.data?.predicate;
|
|
356
|
+
if (paramType) {
|
|
357
|
+
params.push(`${paramName}${isOptional ? '?' : ''}: ${expandSuffixes(paramType)}`);
|
|
358
|
+
} else {
|
|
359
|
+
params.push(paramName);
|
|
360
|
+
}
|
|
361
|
+
j++;
|
|
362
|
+
|
|
363
|
+
// Skip past default value expression
|
|
364
|
+
if (hasDefault) {
|
|
365
|
+
j++; // skip =
|
|
366
|
+
let dd = 0;
|
|
367
|
+
while (j < tokens.length) {
|
|
368
|
+
let dt = tokens[j];
|
|
369
|
+
if (dt[0] === '(' || dt[0] === '[' || dt[0] === '{') dd++;
|
|
370
|
+
if (dt[0] === ')' || dt[0] === ']' || dt[0] === '}') dd--;
|
|
371
|
+
if (dd === 0 && (dt[1] === ',' || dt[0] === 'CALL_END')) break;
|
|
372
|
+
j++;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
continue;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
j++;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
return { params, fields, endIdx: j };
|
|
382
|
+
};
|
|
383
|
+
|
|
384
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
385
|
+
let t = tokens[i];
|
|
386
|
+
let tag = t[0];
|
|
387
|
+
|
|
388
|
+
// Track export flag
|
|
389
|
+
let exported = false;
|
|
390
|
+
if (tag === 'EXPORT') {
|
|
391
|
+
exported = true;
|
|
392
|
+
i++;
|
|
393
|
+
if (i >= tokens.length) break;
|
|
394
|
+
t = tokens[i];
|
|
395
|
+
tag = t[0];
|
|
396
|
+
|
|
397
|
+
// Export default
|
|
398
|
+
if (tag === 'DEFAULT') {
|
|
399
|
+
i++;
|
|
400
|
+
if (i >= tokens.length) break;
|
|
401
|
+
t = tokens[i];
|
|
402
|
+
tag = t[0];
|
|
403
|
+
|
|
404
|
+
// export default IDENTIFIER (re-export)
|
|
405
|
+
if (tag === 'IDENTIFIER') {
|
|
406
|
+
lines.push(`${indent()}export default ${t[1]};`);
|
|
407
|
+
}
|
|
408
|
+
// export default { ... } or other expressions — skip for now
|
|
409
|
+
continue;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// Import statements — pass through for type references
|
|
414
|
+
if (tag === 'IMPORT') {
|
|
415
|
+
let importTokens = [];
|
|
416
|
+
let j = i + 1;
|
|
417
|
+
while (j < tokens.length && tokens[j][0] !== 'TERMINATOR') {
|
|
418
|
+
importTokens.push(tokens[j]);
|
|
419
|
+
j++;
|
|
420
|
+
}
|
|
421
|
+
// Reconstruct: join with spaces, then clean up spacing
|
|
422
|
+
let raw = 'import ' + importTokens.map(tk => tk[1]).join(' ');
|
|
423
|
+
raw = raw.replace(/\s+/g, ' ')
|
|
424
|
+
.replace(/\s*,\s*/g, ', ')
|
|
425
|
+
.replace(/\{\s*/g, '{ ').replace(/\s*\}/g, ' }')
|
|
426
|
+
.trim();
|
|
427
|
+
lines.push(`${indent()}${raw};`);
|
|
428
|
+
i = j;
|
|
429
|
+
continue;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// TYPE_DECL marker — emit type alias or interface
|
|
433
|
+
if (tag === 'TYPE_DECL') {
|
|
434
|
+
let data = t.data;
|
|
435
|
+
if (!data) continue;
|
|
436
|
+
let exp = (exported || data.exported) ? 'export ' : '';
|
|
437
|
+
let params = data.typeParams || '';
|
|
438
|
+
|
|
439
|
+
if (data.kind === 'overload') {
|
|
440
|
+
// Emit function overload signature from saved tokens
|
|
441
|
+
let ot = data.overloadTokens;
|
|
442
|
+
let nameToken = ot[1]; // DEF is [0], name is [1]
|
|
443
|
+
let { params: paramList } = collectParams(ot, 2);
|
|
444
|
+
let returnType = nameToken.data?.returnType;
|
|
445
|
+
let ret = returnType ? `: ${expandSuffixes(returnType)}` : '';
|
|
446
|
+
let declare = inClass ? '' : (exp ? '' : 'declare ');
|
|
447
|
+
let typeParams = data.typeParams || '';
|
|
448
|
+
if (inClass) {
|
|
449
|
+
lines.push(`${indent()}${data.name}${typeParams}(${paramList.join(', ')})${ret};`);
|
|
450
|
+
} else {
|
|
451
|
+
lines.push(`${indent()}${exp}${declare}function ${data.name}${typeParams}(${paramList.join(', ')})${ret};`);
|
|
452
|
+
}
|
|
453
|
+
} else if (data.kind === 'interface') {
|
|
454
|
+
let ext = data.extends ? ` extends ${data.extends}` : '';
|
|
455
|
+
emitBlock(`${exp}interface ${data.name}${params}${ext} `, data.typeText || '{}', '');
|
|
456
|
+
} else {
|
|
457
|
+
let typeText = expandSuffixes(data.typeText || '');
|
|
458
|
+
emitBlock(`${exp}type ${data.name}${params} = `, typeText, ';');
|
|
459
|
+
}
|
|
460
|
+
continue;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// ENUM — emit enum declaration for .d.ts
|
|
464
|
+
if (tag === 'ENUM') {
|
|
465
|
+
let exp = exported ? 'export ' : '';
|
|
466
|
+
let nameToken = tokens[i + 1];
|
|
467
|
+
if (!nameToken) continue;
|
|
468
|
+
let enumName = nameToken[1];
|
|
469
|
+
|
|
470
|
+
// Find INDENT ... OUTDENT for enum body
|
|
471
|
+
let j = i + 2;
|
|
472
|
+
if (tokens[j]?.[0] === 'INDENT') {
|
|
473
|
+
lines.push(`${indent()}${exp}enum ${enumName} {`);
|
|
474
|
+
indentLevel++;
|
|
475
|
+
j++;
|
|
476
|
+
let members = [];
|
|
477
|
+
|
|
478
|
+
while (j < tokens.length && tokens[j][0] !== 'OUTDENT') {
|
|
479
|
+
if (tokens[j][0] === 'TERMINATOR') { j++; continue; }
|
|
480
|
+
if (tokens[j][0] === 'IDENTIFIER') {
|
|
481
|
+
let memberName = tokens[j][1];
|
|
482
|
+
j++;
|
|
483
|
+
if (tokens[j]?.[1] === '=') {
|
|
484
|
+
j++;
|
|
485
|
+
let val = tokens[j]?.[1];
|
|
486
|
+
members.push(`${memberName} = ${val}`);
|
|
487
|
+
j++;
|
|
488
|
+
} else {
|
|
489
|
+
members.push(memberName);
|
|
490
|
+
}
|
|
491
|
+
} else {
|
|
492
|
+
j++;
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
for (let m = 0; m < members.length; m++) {
|
|
497
|
+
let comma = m < members.length - 1 ? ',' : '';
|
|
498
|
+
lines.push(`${indent()}${members[m]}${comma}`);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
indentLevel--;
|
|
502
|
+
lines.push(`${indent()}}`);
|
|
503
|
+
}
|
|
504
|
+
// Don't advance i — the parser still needs to see ENUM tokens
|
|
505
|
+
continue;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// CLASS — emit class declaration
|
|
509
|
+
if (tag === 'CLASS') {
|
|
510
|
+
let exp = exported ? 'export ' : '';
|
|
511
|
+
let classNameToken = tokens[i + 1];
|
|
512
|
+
if (!classNameToken) continue;
|
|
513
|
+
let className = classNameToken[1];
|
|
514
|
+
|
|
515
|
+
// Check for extends
|
|
516
|
+
let ext = '';
|
|
517
|
+
let j = i + 2;
|
|
518
|
+
if (tokens[j]?.[0] === 'EXTENDS') {
|
|
519
|
+
ext = ` extends ${tokens[j + 1]?.[1] || ''}`;
|
|
520
|
+
j += 2;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// Only emit if there are typed members
|
|
524
|
+
if (tokens[j]?.[0] === 'INDENT') {
|
|
525
|
+
let hasTypedMembers = false;
|
|
526
|
+
let k = j + 1;
|
|
527
|
+
while (k < tokens.length && tokens[k][0] !== 'OUTDENT') {
|
|
528
|
+
if (tokens[k].data?.type || tokens[k].data?.returnType) {
|
|
529
|
+
hasTypedMembers = true;
|
|
530
|
+
break;
|
|
531
|
+
}
|
|
532
|
+
k++;
|
|
533
|
+
}
|
|
534
|
+
if (hasTypedMembers) {
|
|
535
|
+
lines.push(`${indent()}${exp}declare class ${className}${ext} {`);
|
|
536
|
+
inClass = true;
|
|
537
|
+
classFields.clear();
|
|
538
|
+
indentLevel++;
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
continue;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// DEF — emit function or method declaration
|
|
545
|
+
if (tag === 'DEF') {
|
|
546
|
+
let nameToken = tokens[i + 1];
|
|
547
|
+
if (!nameToken) continue;
|
|
548
|
+
let fnName = nameToken[1];
|
|
549
|
+
let returnType = nameToken.data?.returnType;
|
|
550
|
+
if (!returnType && nameToken.data?.await === true) returnType = 'void';
|
|
551
|
+
let typeParams = nameToken.data?.typeParams || '';
|
|
552
|
+
|
|
553
|
+
let { params, endIdx } = collectParams(tokens, i + 2);
|
|
554
|
+
|
|
555
|
+
// Only emit if there are type annotations
|
|
556
|
+
if (returnType || params.some(p => p.includes(':'))) {
|
|
557
|
+
let exp = exported ? 'export ' : '';
|
|
558
|
+
let declare = inClass ? '' : (exported ? '' : 'declare ');
|
|
559
|
+
let ret = returnType ? `: ${expandSuffixes(returnType)}` : '';
|
|
560
|
+
let paramStr = params.join(', ');
|
|
561
|
+
if (inClass) {
|
|
562
|
+
lines.push(`${indent()}${fnName}${typeParams}(${paramStr})${ret};`);
|
|
563
|
+
} else {
|
|
564
|
+
lines.push(`${indent()}${exp}${declare}function ${fnName}${typeParams}(${paramStr})${ret};`);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
i = endIdx; // advance past params to avoid leaking destructured typed identifiers
|
|
568
|
+
continue;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
// Class method block: { PROPERTY ... PROPERTY ... }
|
|
572
|
+
// Contains one or more methods separated by TERMINATOR
|
|
573
|
+
if (tag === '{' && inClass) {
|
|
574
|
+
let j = i + 1;
|
|
575
|
+
let braceDepth = 1;
|
|
576
|
+
|
|
577
|
+
while (j < tokens.length && braceDepth > 0) {
|
|
578
|
+
let tok = tokens[j];
|
|
579
|
+
|
|
580
|
+
if (tok[0] === '{') { braceDepth++; j++; continue; }
|
|
581
|
+
if (tok[0] === '}') { braceDepth--; j++; continue; }
|
|
582
|
+
if (tok[0] === 'TERMINATOR') { j++; continue; }
|
|
583
|
+
|
|
584
|
+
// Found a method: PROPERTY "name" : PARAM_START ... PARAM_END -> body
|
|
585
|
+
if (tok[0] === 'PROPERTY' && braceDepth === 1) {
|
|
586
|
+
let methodName = tok[1];
|
|
587
|
+
let returnType = tok.data?.returnType;
|
|
588
|
+
j++;
|
|
589
|
+
|
|
590
|
+
// Skip : separator
|
|
591
|
+
if (tokens[j]?.[1] === ':') j++;
|
|
592
|
+
|
|
593
|
+
let params = [];
|
|
594
|
+
let fields = [];
|
|
595
|
+
if (tokens[j]?.[0] === 'PARAM_START') {
|
|
596
|
+
let result = collectParams(tokens, j);
|
|
597
|
+
params = result.params;
|
|
598
|
+
fields = result.fields;
|
|
599
|
+
j = result.endIdx + 1;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
// Skip -> and method body (INDENT ... OUTDENT)
|
|
603
|
+
if (tokens[j]?.[0] === '->' || tokens[j]?.[0] === '=>') j++;
|
|
604
|
+
if (tokens[j]?.[0] === 'INDENT') {
|
|
605
|
+
let d = 1;
|
|
606
|
+
j++;
|
|
607
|
+
while (j < tokens.length && d > 0) {
|
|
608
|
+
if (tokens[j][0] === 'INDENT') d++;
|
|
609
|
+
if (tokens[j][0] === 'OUTDENT') d--;
|
|
610
|
+
j++;
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
if (returnType || params.some(p => p.includes(':'))) {
|
|
615
|
+
let ret = returnType ? `: ${expandSuffixes(returnType)}` : '';
|
|
616
|
+
let paramStr = params.join(', ');
|
|
617
|
+
// Emit field declarations for constructor @param:: type shorthand
|
|
618
|
+
if (methodName === 'constructor' && fields.length) {
|
|
619
|
+
for (let f of fields) {
|
|
620
|
+
if (!classFields.has(f.name)) {
|
|
621
|
+
lines.push(`${indent()}${f.name}: ${f.type};`);
|
|
622
|
+
classFields.add(f.name);
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
lines.push(`${indent()}${methodName}(${paramStr})${ret};`);
|
|
627
|
+
}
|
|
628
|
+
continue;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
j++;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
i = j - 1;
|
|
635
|
+
continue;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
// Track INDENT/OUTDENT for class body
|
|
639
|
+
if (tag === 'INDENT') {
|
|
640
|
+
continue;
|
|
641
|
+
}
|
|
642
|
+
if (tag === 'OUTDENT') {
|
|
643
|
+
if (inClass) {
|
|
644
|
+
indentLevel--;
|
|
645
|
+
lines.push(`${indent()}}`);
|
|
646
|
+
inClass = false;
|
|
647
|
+
}
|
|
648
|
+
continue;
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
// Arrow function assignment: name = (params) -> body
|
|
652
|
+
if (tag === 'IDENTIFIER' && !inClass && tokens[i + 1]?.[0] === '=' &&
|
|
653
|
+
(tokens[i + 2]?.[0] === 'PARAM_START' || tokens[i + 2]?.[0] === '(')) {
|
|
654
|
+
let fnName = t[1];
|
|
655
|
+
let j = i + 2;
|
|
656
|
+
|
|
657
|
+
let { params } = collectParams(tokens, j);
|
|
658
|
+
|
|
659
|
+
// Find the -> or => token to get return type
|
|
660
|
+
let k = j;
|
|
661
|
+
let depth = 0;
|
|
662
|
+
while (k < tokens.length) {
|
|
663
|
+
if (tokens[k][0] === 'PARAM_START' || tokens[k][0] === '(') depth++;
|
|
664
|
+
if (tokens[k][0] === 'PARAM_END' || tokens[k][0] === ')') depth--;
|
|
665
|
+
if (depth === 0 && (tokens[k][0] === '->' || tokens[k][0] === '=>')) break;
|
|
666
|
+
k++;
|
|
667
|
+
}
|
|
668
|
+
let returnType = tokens[k]?.data?.returnType;
|
|
669
|
+
|
|
670
|
+
if (returnType || params.some(p => p.includes(':'))) {
|
|
671
|
+
let exp = exported ? 'export ' : '';
|
|
672
|
+
let declare = exported ? '' : 'declare ';
|
|
673
|
+
let ret = returnType ? `: ${expandSuffixes(returnType)}` : '';
|
|
674
|
+
let paramStr = params.join(', ');
|
|
675
|
+
lines.push(`${indent()}${exp}${declare}function ${fnName}(${paramStr})${ret};`);
|
|
676
|
+
continue;
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
// Variable assignments with type annotations
|
|
681
|
+
if (tag === 'IDENTIFIER' && t.data?.type) {
|
|
682
|
+
let varName = t[1];
|
|
683
|
+
let type = expandSuffixes(t.data.type);
|
|
684
|
+
let next = tokens[i + 1];
|
|
685
|
+
|
|
686
|
+
if (next) {
|
|
687
|
+
let exp = exported ? 'export ' : '';
|
|
688
|
+
let declare = exported ? '' : 'declare ';
|
|
689
|
+
|
|
690
|
+
if (next[0] === 'READONLY_ASSIGN') {
|
|
691
|
+
lines.push(`${indent()}${exp}${declare}const ${varName}: ${type};`);
|
|
692
|
+
} else if (next[0] === 'REACTIVE_ASSIGN') {
|
|
693
|
+
usesSignal = true;
|
|
694
|
+
lines.push(`${indent()}${exp}${declare}const ${varName}: Signal<${type}>;`);
|
|
695
|
+
} else if (next[0] === 'COMPUTED_ASSIGN') {
|
|
696
|
+
usesComputed = true;
|
|
697
|
+
lines.push(`${indent()}${exp}${declare}const ${varName}: Computed<${type}>;`);
|
|
698
|
+
} else if (next[0] === 'EFFECT') {
|
|
699
|
+
lines.push(`${indent()}${exp}${declare}const ${varName}: () => void;`);
|
|
700
|
+
} else if (next[0] === '=') {
|
|
701
|
+
// Check if RHS is an arrow function with return type
|
|
702
|
+
let arrowIdx = i + 2;
|
|
703
|
+
// Skip past PARAM_START ... PARAM_END if present
|
|
704
|
+
if (tokens[arrowIdx]?.[0] === 'PARAM_START') {
|
|
705
|
+
let d = 1, k = arrowIdx + 1;
|
|
706
|
+
while (k < tokens.length && d > 0) {
|
|
707
|
+
if (tokens[k][0] === 'PARAM_START') d++;
|
|
708
|
+
if (tokens[k][0] === 'PARAM_END') d--;
|
|
709
|
+
k++;
|
|
710
|
+
}
|
|
711
|
+
arrowIdx = k;
|
|
712
|
+
}
|
|
713
|
+
let arrowToken = tokens[arrowIdx];
|
|
714
|
+
if (arrowToken && (arrowToken[0] === '->' || arrowToken[0] === '=>') &&
|
|
715
|
+
arrowToken.data?.returnType) {
|
|
716
|
+
// Typed arrow function assignment
|
|
717
|
+
let returnType = expandSuffixes(arrowToken.data.returnType);
|
|
718
|
+
let { params } = collectParams(tokens, i + 2);
|
|
719
|
+
let paramStr = params.join(', ');
|
|
720
|
+
lines.push(`${indent()}${exp}${declare}function ${varName}(${paramStr}): ${returnType};`);
|
|
721
|
+
} else if (inClass) {
|
|
722
|
+
lines.push(`${indent()}${varName}: ${type};`);
|
|
723
|
+
classFields.add(varName);
|
|
724
|
+
} else {
|
|
725
|
+
lines.push(`${indent()}${exp}let ${varName}: ${type};`);
|
|
726
|
+
}
|
|
727
|
+
} else if (inClass) {
|
|
728
|
+
// Class property without assignment
|
|
729
|
+
lines.push(`${indent()}${varName}: ${type};`);
|
|
730
|
+
classFields.add(varName);
|
|
731
|
+
}
|
|
732
|
+
} else if (inClass) {
|
|
733
|
+
lines.push(`${indent()}${varName}: ${type};`);
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
// Walk s-expression tree for component declarations
|
|
739
|
+
let componentVars = new Set();
|
|
740
|
+
let hasSchemaDecls = false;
|
|
741
|
+
if (sexpr) {
|
|
742
|
+
usesRipIntrinsicProps = emitComponentTypes(sexpr, lines, indent, indentLevel, componentVars, sourceLines) || usesRipIntrinsicProps;
|
|
743
|
+
|
|
744
|
+
// Remove lines for variables that belong to components (emitted as class members)
|
|
745
|
+
if (componentVars.size > 0) {
|
|
746
|
+
for (let k = lines.length - 1; k >= 0; k--) {
|
|
747
|
+
let match = lines[k].match(/(?:declare |export )*(?:const|let) (\w+)/);
|
|
748
|
+
if (match && componentVars.has(match[1])) lines.splice(k, 1);
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
// Schema declarations — strip any prior auto-emitted `declare let Foo`
|
|
753
|
+
// for the same bindings (they are re-emitted as typed Schema<T>).
|
|
754
|
+
let schemaLines = [];
|
|
755
|
+
hasSchemaDecls = emitSchemaTypes(sexpr, schemaLines);
|
|
756
|
+
if (hasSchemaDecls) {
|
|
757
|
+
let bindings = new Set();
|
|
758
|
+
for (let line of schemaLines) {
|
|
759
|
+
let m = line.match(/(?:declare |export )*const (\w+)/);
|
|
760
|
+
if (m) bindings.add(m[1]);
|
|
761
|
+
}
|
|
762
|
+
for (let k = lines.length - 1; k >= 0; k--) {
|
|
763
|
+
let m = lines[k].match(/(?:declare |export )*(?:const|let) (\w+)/);
|
|
764
|
+
if (m && bindings.has(m[1])) lines.splice(k, 1);
|
|
765
|
+
}
|
|
766
|
+
lines.push(...schemaLines);
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
if (lines.length === 0) return null;
|
|
771
|
+
|
|
772
|
+
// Prepend reactive type definitions if used
|
|
773
|
+
let preamble = [];
|
|
774
|
+
if (usesRipIntrinsicProps) {
|
|
775
|
+
preamble.push(...INTRINSIC_TYPE_DECLS);
|
|
776
|
+
}
|
|
777
|
+
if (/\bARIA\./.test(source)) {
|
|
778
|
+
preamble.push(...ARIA_TYPE_DECLS);
|
|
779
|
+
}
|
|
780
|
+
if (usesSignal) {
|
|
781
|
+
preamble.push(SIGNAL_INTERFACE);
|
|
782
|
+
preamble.push(SIGNAL_FN);
|
|
783
|
+
}
|
|
784
|
+
if (usesComputed) {
|
|
785
|
+
preamble.push(COMPUTED_INTERFACE);
|
|
786
|
+
preamble.push(COMPUTED_FN);
|
|
787
|
+
}
|
|
788
|
+
if (usesSignal || usesComputed) {
|
|
789
|
+
preamble.push(EFFECT_FN);
|
|
790
|
+
}
|
|
791
|
+
if (hasSchemaDecls) {
|
|
792
|
+
preamble.push(...SCHEMA_INTRINSIC_DECLS);
|
|
793
|
+
}
|
|
794
|
+
if (preamble.length > 0) {
|
|
795
|
+
preamble.push('');
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
return preamble.concat(lines).join('\n') + '\n';
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
// ============================================================================
|
|
802
|
+
// Suffix expansion — Rip type suffixes to TypeScript
|
|
803
|
+
// ============================================================================
|
|
804
|
+
|
|
805
|
+
function expandSuffixes(typeStr) {
|
|
806
|
+
if (!typeStr) return typeStr;
|
|
807
|
+
|
|
808
|
+
// Convert :: to : (annotation sigil to type separator)
|
|
809
|
+
typeStr = typeStr.replace(/::/g, ':');
|
|
810
|
+
|
|
811
|
+
// T?? → T | null | undefined
|
|
812
|
+
typeStr = typeStr.replace(/(\w+(?:<[^>]+>)?)\?\?/g, '$1 | null | undefined');
|
|
813
|
+
|
|
814
|
+
// T? → T | undefined (but not ?. or ?: which are different)
|
|
815
|
+
typeStr = typeStr.replace(/(\w+(?:<[^>]+>)?)\?(?![.:])/g, '$1 | undefined');
|
|
816
|
+
|
|
817
|
+
// T! → NonNullable<T>
|
|
818
|
+
typeStr = typeStr.replace(/(\w+(?:<[^>]+>)?)\!/g, 'NonNullable<$1>');
|
|
819
|
+
|
|
820
|
+
return typeStr;
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
// ============================================================================
|
|
824
|
+
// Component type emission — walk s-expression for component declarations
|
|
825
|
+
// ============================================================================
|
|
826
|
+
|
|
827
|
+
function emitComponentTypes(sexpr, lines, indent, indentLevel, componentVars, sourceLines) {
|
|
828
|
+
if (!Array.isArray(sexpr)) return false;
|
|
829
|
+
let head = sexpr[0]?.valueOf?.() ?? sexpr[0];
|
|
830
|
+
let usesIntrinsicProps = false;
|
|
831
|
+
|
|
832
|
+
const refMembers = new Map();
|
|
833
|
+
const isIntrinsicTag = (name) => typeof name === 'string' && /^[a-z]/.test(name);
|
|
834
|
+
const collectRefMembers = (node) => {
|
|
835
|
+
if (!Array.isArray(node)) return;
|
|
836
|
+
let nodeHead = node[0]?.valueOf?.() ?? node[0];
|
|
837
|
+
if (isIntrinsicTag(nodeHead)) {
|
|
838
|
+
for (let i = 1; i < node.length; i++) {
|
|
839
|
+
let child = node[i];
|
|
840
|
+
if (!Array.isArray(child)) continue;
|
|
841
|
+
let childHead = child[0]?.valueOf?.() ?? child[0];
|
|
842
|
+
if (childHead !== 'object') continue;
|
|
843
|
+
for (let j = 1; j < child.length; j++) {
|
|
844
|
+
let entry = child[j];
|
|
845
|
+
if (!Array.isArray(entry)) continue;
|
|
846
|
+
let key = entry[0]?.valueOf?.() ?? entry[0];
|
|
847
|
+
if (key !== 'ref') continue;
|
|
848
|
+
let refName = entry[1]?.valueOf?.() ?? entry[1];
|
|
849
|
+
if (typeof refName === 'string') refName = refName.replace(/^["']|["']$/g, '');
|
|
850
|
+
if (typeof refName === 'string' && !refMembers.has(refName)) {
|
|
851
|
+
refMembers.set(refName, `__RipDomEl<'${nodeHead}'> | null`);
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
for (let i = 1; i < node.length; i++) {
|
|
857
|
+
if (Array.isArray(node[i])) collectRefMembers(node[i]);
|
|
858
|
+
}
|
|
859
|
+
};
|
|
860
|
+
|
|
861
|
+
// export Name = component ... → ["export", ["=", "Name", ["component", ...members]]]
|
|
862
|
+
// Name = component ... → ["=", "Name", ["component", ...members]]
|
|
863
|
+
let exported = false;
|
|
864
|
+
let name = null;
|
|
865
|
+
let compNode = null;
|
|
866
|
+
let typeParams = '';
|
|
867
|
+
|
|
868
|
+
if (head === 'export' && Array.isArray(sexpr[1])) {
|
|
869
|
+
exported = true;
|
|
870
|
+
let inner = sexpr[1];
|
|
871
|
+
let innerHead = inner[0]?.valueOf?.() ?? inner[0];
|
|
872
|
+
if (innerHead === '=' && Array.isArray(inner[2]) &&
|
|
873
|
+
(inner[2][0]?.valueOf?.() ?? inner[2][0]) === 'component') {
|
|
874
|
+
typeParams = inner[1]?.typeParams || '';
|
|
875
|
+
name = inner[1]?.valueOf?.() ?? inner[1];
|
|
876
|
+
compNode = inner[2];
|
|
877
|
+
}
|
|
878
|
+
} else if (head === '=' && Array.isArray(sexpr[2]) &&
|
|
879
|
+
(sexpr[2][0]?.valueOf?.() ?? sexpr[2][0]) === 'component') {
|
|
880
|
+
typeParams = sexpr[1]?.typeParams || '';
|
|
881
|
+
name = sexpr[1]?.valueOf?.() ?? sexpr[1];
|
|
882
|
+
compNode = sexpr[2];
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
if (name && compNode) {
|
|
886
|
+
let exp = exported ? 'export ' : '';
|
|
887
|
+
let inheritsTag = compNode[1]?.valueOf?.() ?? null;
|
|
888
|
+
let inheritedPropsType = inheritsTag ? `__RipProps<'${inheritsTag}'>` : null;
|
|
889
|
+
if (inheritedPropsType) usesIntrinsicProps = true;
|
|
890
|
+
|
|
891
|
+
// Component structure: ["component", parent, ["block", ...members]]
|
|
892
|
+
let body = compNode[2];
|
|
893
|
+
let members = (Array.isArray(body) && (body[0]?.valueOf?.() ?? body[0]) === 'block')
|
|
894
|
+
? body.slice(1) : (body ? [body] : []);
|
|
895
|
+
|
|
896
|
+
let publicProps = [];
|
|
897
|
+
let bodyMembers = [];
|
|
898
|
+
let hasRequired = false;
|
|
899
|
+
|
|
900
|
+
// Infer type from literal initializer when no explicit annotation
|
|
901
|
+
let inferLiteralType = (v) => {
|
|
902
|
+
let s = v?.valueOf?.() ?? v;
|
|
903
|
+
if (typeof s !== 'string') return null;
|
|
904
|
+
if (s === 'true' || s === 'false') return 'boolean';
|
|
905
|
+
if (/^-?\d+(\.\d+)?$/.test(s)) return 'number';
|
|
906
|
+
if (s.startsWith('"') || s.startsWith("'")) return 'string';
|
|
907
|
+
return null;
|
|
908
|
+
};
|
|
909
|
+
|
|
910
|
+
for (let member of members) {
|
|
911
|
+
if (!Array.isArray(member)) continue;
|
|
912
|
+
let mHead = member[0]?.valueOf?.() ?? member[0];
|
|
913
|
+
|
|
914
|
+
let target, propName, isProp, type, hasDefault;
|
|
915
|
+
|
|
916
|
+
if (mHead === 'state' || mHead === 'readonly' || mHead === 'computed') {
|
|
917
|
+
target = member[1];
|
|
918
|
+
isProp = Array.isArray(target) && (target[0]?.valueOf?.() ?? target[0]) === '.' && (target[1]?.valueOf?.() ?? target[1]) === 'this';
|
|
919
|
+
propName = isProp ? (target[2]?.valueOf?.() ?? target[2]) : (target?.valueOf?.() ?? target);
|
|
920
|
+
type = isProp ? target[2]?.type : target?.type;
|
|
921
|
+
hasDefault = true;
|
|
922
|
+
if (!isProp) {
|
|
923
|
+
componentVars.add(propName);
|
|
924
|
+
let wrapper = (mHead === 'computed') ? 'Computed' : 'Signal';
|
|
925
|
+
let typeStr = type ? expandSuffixes(type) : (inferLiteralType(member[2]) || 'any');
|
|
926
|
+
bodyMembers.push(` ${propName}: ${wrapper}<${typeStr}>;`);
|
|
927
|
+
continue;
|
|
928
|
+
}
|
|
929
|
+
} else if (mHead === '.') {
|
|
930
|
+
isProp = (member[1]?.valueOf?.() ?? member[1]) === 'this';
|
|
931
|
+
propName = isProp ? (member[2]?.valueOf?.() ?? member[2]) : null;
|
|
932
|
+
type = isProp ? member[2]?.type : null;
|
|
933
|
+
hasDefault = false;
|
|
934
|
+
if (!isProp && propName) componentVars.add(propName);
|
|
935
|
+
} else if (mHead === 'object') {
|
|
936
|
+
// Method definitions: (object (: methodName (-> (params...) (block ...))))
|
|
937
|
+
for (let i = 1; i < member.length; i++) {
|
|
938
|
+
let entry = member[i];
|
|
939
|
+
if (!Array.isArray(entry) || entry.length < 3) continue;
|
|
940
|
+
let methName = entry[1]?.valueOf?.() ?? entry[1];
|
|
941
|
+
let funcDef = entry[2];
|
|
942
|
+
if (!Array.isArray(funcDef)) continue;
|
|
943
|
+
let fHead = funcDef[0]?.valueOf?.() ?? funcDef[0];
|
|
944
|
+
if (fHead !== '->' && fHead !== '=>') continue;
|
|
945
|
+
let params = funcDef[1];
|
|
946
|
+
if (!Array.isArray(params)) continue;
|
|
947
|
+
let hasTypedParams = params.some(p => p?.type);
|
|
948
|
+
if (!hasTypedParams) continue;
|
|
949
|
+
let paramStrs = [];
|
|
950
|
+
for (let p of params) {
|
|
951
|
+
let pName = p?.valueOf?.() ?? p;
|
|
952
|
+
let pType = p?.type ? expandSuffixes(p.type) : 'any';
|
|
953
|
+
paramStrs.push(`${pName}: ${pType}`);
|
|
954
|
+
}
|
|
955
|
+
bodyMembers.push(` ${methName}(${paramStrs.join(', ')}): void;`);
|
|
956
|
+
}
|
|
957
|
+
continue;
|
|
958
|
+
} else if (mHead === 'render') {
|
|
959
|
+
usesIntrinsicProps = true;
|
|
960
|
+
collectRefMembers(member[1]);
|
|
961
|
+
continue;
|
|
962
|
+
} else {
|
|
963
|
+
continue;
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
if (!isProp || !propName) continue;
|
|
967
|
+
|
|
968
|
+
let typeStr = type ? expandSuffixes(type) : 'any';
|
|
969
|
+
let opt = hasDefault ? '?' : '';
|
|
970
|
+
if (!hasDefault) hasRequired = true;
|
|
971
|
+
publicProps.push(` ${propName}${opt}: ${typeStr};`);
|
|
972
|
+
if (mHead === 'state') {
|
|
973
|
+
publicProps.push(` __bind_${propName}__?: Signal<${typeStr}>;`);
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
lines.push(`${exp}declare class ${name}${typeParams} {`);
|
|
978
|
+
if (publicProps.length > 0 || inheritedPropsType) {
|
|
979
|
+
let propsOpt = hasRequired ? '' : '?';
|
|
980
|
+
if (publicProps.length > 0) {
|
|
981
|
+
lines.push(` constructor(props${propsOpt}: {`);
|
|
982
|
+
for (let p of publicProps) lines.push(p);
|
|
983
|
+
lines.push(inheritedPropsType ? ` } & ${inheritedPropsType});` : ' });');
|
|
984
|
+
} else {
|
|
985
|
+
lines.push(` constructor(props${propsOpt}: ${inheritedPropsType});`);
|
|
986
|
+
}
|
|
987
|
+
} else {
|
|
988
|
+
lines.push(` constructor(props?: {});`);
|
|
989
|
+
}
|
|
990
|
+
for (let [refName, refType] of refMembers) {
|
|
991
|
+
bodyMembers.push(` ${refName}: ${refType};`);
|
|
992
|
+
}
|
|
993
|
+
for (let m of bodyMembers) lines.push(m);
|
|
994
|
+
lines.push(`}`);
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
// Recurse into child nodes
|
|
998
|
+
if (head === 'program' || head === 'block') {
|
|
999
|
+
for (let i = 1; i < sexpr.length; i++) {
|
|
1000
|
+
if (Array.isArray(sexpr[i])) {
|
|
1001
|
+
usesIntrinsicProps = emitComponentTypes(sexpr[i], lines, indent, indentLevel, componentVars, sourceLines) || usesIntrinsicProps;
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
// Also check inside export wrappers
|
|
1006
|
+
if (head === 'export' && Array.isArray(sexpr[1]) && !compNode) {
|
|
1007
|
+
usesIntrinsicProps = emitComponentTypes(sexpr[1], lines, indent, indentLevel, componentVars, sourceLines) || usesIntrinsicProps;
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
return usesIntrinsicProps;
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
// ============================================================================
|
|
1014
|
+
// Registration — install emitTypes into the compiler at module load.
|
|
1015
|
+
// ============================================================================
|
|
1016
|
+
// The compiler exposes setTypesEmitter() as a no-op-friendly hook. When
|
|
1017
|
+
// nothing imports types-emit.js (browser bundle), the emitter stays null
|
|
1018
|
+
// and compile()s .d.ts output is silently skipped. CLI entry points and
|
|
1019
|
+
// typecheck.js import this module specifically to install the emitter.
|
|
1020
|
+
|
|
1021
|
+
setTypesEmitter(emitTypes);
|