@reactra/babel-plugin 0.1.0-alpha.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/LICENSE +21 -0
- package/README.md +17 -0
- package/dist/ast/index.d.ts +2 -0
- package/dist/ast/index.d.ts.map +1 -0
- package/dist/ast/index.js +3 -0
- package/dist/ast/index.js.map +1 -0
- package/dist/ast/nodes.d.ts +437 -0
- package/dist/ast/nodes.d.ts.map +1 -0
- package/dist/ast/nodes.js +35 -0
- package/dist/ast/nodes.js.map +1 -0
- package/dist/behaviours/index.d.ts +18 -0
- package/dist/behaviours/index.d.ts.map +1 -0
- package/dist/behaviours/index.js +36 -0
- package/dist/behaviours/index.js.map +1 -0
- package/dist/behaviours/plugin.d.ts +22 -0
- package/dist/behaviours/plugin.d.ts.map +1 -0
- package/dist/behaviours/plugin.js +70 -0
- package/dist/behaviours/plugin.js.map +1 -0
- package/dist/behaviours/replayable.d.ts +10 -0
- package/dist/behaviours/replayable.d.ts.map +1 -0
- package/dist/behaviours/replayable.js +86 -0
- package/dist/behaviours/replayable.js.map +1 -0
- package/dist/behaviours/types.d.ts +77 -0
- package/dist/behaviours/types.d.ts.map +1 -0
- package/dist/behaviours/types.js +10 -0
- package/dist/behaviours/types.js.map +1 -0
- package/dist/behaviours/undoable.d.ts +10 -0
- package/dist/behaviours/undoable.d.ts.map +1 -0
- package/dist/behaviours/undoable.js +62 -0
- package/dist/behaviours/undoable.js.map +1 -0
- package/dist/compile.d.ts +69 -0
- package/dist/compile.d.ts.map +1 -0
- package/dist/compile.js +75 -0
- package/dist/compile.js.map +1 -0
- package/dist/conventions/index.d.ts +110 -0
- package/dist/conventions/index.d.ts.map +1 -0
- package/dist/conventions/index.js +193 -0
- package/dist/conventions/index.js.map +1 -0
- package/dist/index.d.ts +48 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +77 -0
- package/dist/index.js.map +1 -0
- package/dist/passes/index.d.ts +5 -0
- package/dist/passes/index.d.ts.map +1 -0
- package/dist/passes/index.js +6 -0
- package/dist/passes/index.js.map +1 -0
- package/dist/passes/pass-1-parse.d.ts +3 -0
- package/dist/passes/pass-1-parse.d.ts.map +1 -0
- package/dist/passes/pass-1-parse.js +21 -0
- package/dist/passes/pass-1-parse.js.map +1 -0
- package/dist/passes/pass-2-extract.d.ts +4 -0
- package/dist/passes/pass-2-extract.d.ts.map +1 -0
- package/dist/passes/pass-2-extract.js +762 -0
- package/dist/passes/pass-2-extract.js.map +1 -0
- package/dist/passes/pass-3-readset.d.ts +11 -0
- package/dist/passes/pass-3-readset.d.ts.map +1 -0
- package/dist/passes/pass-3-readset.js +338 -0
- package/dist/passes/pass-3-readset.js.map +1 -0
- package/dist/passes/pass-9-codegen.d.ts +27 -0
- package/dist/passes/pass-9-codegen.d.ts.map +1 -0
- package/dist/passes/pass-9-codegen.js +2755 -0
- package/dist/passes/pass-9-codegen.js.map +1 -0
- package/dist/preprocess/helpers.d.ts +71 -0
- package/dist/preprocess/helpers.d.ts.map +1 -0
- package/dist/preprocess/helpers.js +342 -0
- package/dist/preprocess/helpers.js.map +1 -0
- package/dist/preprocess/index.d.ts +6 -0
- package/dist/preprocess/index.d.ts.map +1 -0
- package/dist/preprocess/index.js +11 -0
- package/dist/preprocess/index.js.map +1 -0
- package/dist/preprocess/keywords.d.ts +28 -0
- package/dist/preprocess/keywords.d.ts.map +1 -0
- package/dist/preprocess/keywords.js +99 -0
- package/dist/preprocess/keywords.js.map +1 -0
- package/dist/preprocess/lexer.d.ts +8 -0
- package/dist/preprocess/lexer.d.ts.map +1 -0
- package/dist/preprocess/lexer.js +143 -0
- package/dist/preprocess/lexer.js.map +1 -0
- package/dist/preprocess/preprocess.d.ts +3 -0
- package/dist/preprocess/preprocess.d.ts.map +1 -0
- package/dist/preprocess/preprocess.js +568 -0
- package/dist/preprocess/preprocess.js.map +1 -0
- package/dist/preprocess/rewriters.d.ts +35 -0
- package/dist/preprocess/rewriters.d.ts.map +1 -0
- package/dist/preprocess/rewriters.js +1391 -0
- package/dist/preprocess/rewriters.js.map +1 -0
- package/dist/preprocess/source-map.d.ts +70 -0
- package/dist/preprocess/source-map.d.ts.map +1 -0
- package/dist/preprocess/source-map.js +253 -0
- package/dist/preprocess/source-map.js.map +1 -0
- package/dist/preprocess/types.d.ts +57 -0
- package/dist/preprocess/types.d.ts.map +1 -0
- package/dist/preprocess/types.js +7 -0
- package/dist/preprocess/types.js.map +1 -0
- package/dist/sidecar.d.ts +137 -0
- package/dist/sidecar.d.ts.map +1 -0
- package/dist/sidecar.js +172 -0
- package/dist/sidecar.js.map +1 -0
- package/package.json +42 -0
|
@@ -0,0 +1,1391 @@
|
|
|
1
|
+
// Per-keyword rewriters — Compiler §4 Pass 1.1 / DSL v2.
|
|
2
|
+
//
|
|
3
|
+
// Each rewriter consumes a starting token index and emits the spec-exact
|
|
4
|
+
// `__reactra_*__()` marker call. Covers the full 24-keyword surface:
|
|
5
|
+
// component keywords (state/derived/action/resource/effect/mount/ref/view/
|
|
6
|
+
// await/uses/inject/errorBoundary/suspense), store keywords (input/preserved),
|
|
7
|
+
// page keywords (param/query/meta/prefetch/transition), and service keywords
|
|
8
|
+
// (provide/implements).
|
|
9
|
+
import { skipTrivia } from "./lexer.js";
|
|
10
|
+
import { guardTrailingLineComment, readBlockEnd, readBracketEnd, readCommandFetcherEnd, readParenEnd, readOptionalTypeAnnotation, readToStatementEnd, readToStatementEndStrict, slice, } from "./helpers.js";
|
|
11
|
+
// ----------------------------------------------------------------------------
|
|
12
|
+
// Shared helper: build the `__reactra_state__(name, value)` text fragment.
|
|
13
|
+
//
|
|
14
|
+
// Both `rewriteState` and `rewritePreserved` emit a state marker; factoring
|
|
15
|
+
// here prevents the two call-sites from drifting independently (F7).
|
|
16
|
+
// ----------------------------------------------------------------------------
|
|
17
|
+
const emitStateMarker = (name, type, initText) => {
|
|
18
|
+
const value = type ? `(${initText}) as ${type}` : initText;
|
|
19
|
+
return `__reactra_state__("${name}", ${value})`;
|
|
20
|
+
};
|
|
21
|
+
// ----------------------------------------------------------------------------
|
|
22
|
+
// state x = init -> __reactra_state__("x", init)
|
|
23
|
+
// state x: T = init -> __reactra_state__("x", (init) as T)
|
|
24
|
+
// state x: T -> __reactra_state__("x", undefined as T | undefined)
|
|
25
|
+
//
|
|
26
|
+
// The no-init form (annotation present, no `=`) is valid per the locked v2
|
|
27
|
+
// grammar (plan/reactra-dsl-v2-surface.md:138). No annotation and no init
|
|
28
|
+
// is still invalid — return null and let the caller error.
|
|
29
|
+
// ----------------------------------------------------------------------------
|
|
30
|
+
export const rewriteState = (tokens, i) => {
|
|
31
|
+
const stateTok = tokens[i];
|
|
32
|
+
const nameIdx = skipTrivia(tokens, i + 1);
|
|
33
|
+
const nameTok = tokens[nameIdx];
|
|
34
|
+
if (!nameTok || nameTok.kind !== "ident")
|
|
35
|
+
return null;
|
|
36
|
+
const { type, next } = readOptionalTypeAnnotation(tokens, nameIdx + 1);
|
|
37
|
+
const eqIdx = skipTrivia(tokens, next);
|
|
38
|
+
if (tokens[eqIdx]?.text !== "=") {
|
|
39
|
+
// No initializer — only legal when a type annotation is present.
|
|
40
|
+
if (!type)
|
|
41
|
+
return null;
|
|
42
|
+
// readOptionalTypeAnnotation may include a trailing `;` in the type text
|
|
43
|
+
// when the annotation is on a single line and `;` precedes the newline.
|
|
44
|
+
const cleanType = type.replace(/;$/, "").trim();
|
|
45
|
+
// Find the last non-trivia token before `next` (the newline) so the span
|
|
46
|
+
// covers exactly the declaration including any `;` but NOT the trailing `\n`.
|
|
47
|
+
// This preserves the newline as a statement separator in the output.
|
|
48
|
+
let endIdx = next - 1;
|
|
49
|
+
while (endIdx > nameIdx &&
|
|
50
|
+
(tokens[endIdx]?.kind === "whitespace" ||
|
|
51
|
+
tokens[endIdx]?.kind === "comment-line" ||
|
|
52
|
+
tokens[endIdx]?.kind === "comment-block")) {
|
|
53
|
+
endIdx--;
|
|
54
|
+
}
|
|
55
|
+
return {
|
|
56
|
+
rewrite: {
|
|
57
|
+
span: { start: stateTok.start, end: tokens[endIdx].end },
|
|
58
|
+
text: `__reactra_state__("${nameTok.text}", undefined as ${cleanType} | undefined)`,
|
|
59
|
+
},
|
|
60
|
+
consumedUpTo: endIdx,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
const initStart = skipTrivia(tokens, eqIdx + 1);
|
|
64
|
+
const endIdx = readToStatementEndStrict(tokens, initStart, {
|
|
65
|
+
keyword: "state",
|
|
66
|
+
name: nameTok.text,
|
|
67
|
+
startTok: stateTok,
|
|
68
|
+
});
|
|
69
|
+
const initText = slice(tokens, initStart, endIdx).trim();
|
|
70
|
+
return {
|
|
71
|
+
rewrite: {
|
|
72
|
+
span: { start: stateTok.start, end: tokens[endIdx].end },
|
|
73
|
+
text: emitStateMarker(nameTok.text, type ?? null, initText),
|
|
74
|
+
},
|
|
75
|
+
consumedUpTo: endIdx,
|
|
76
|
+
};
|
|
77
|
+
};
|
|
78
|
+
// ----------------------------------------------------------------------------
|
|
79
|
+
// derived d = expr -> __reactra_derived__("d", () => (expr))
|
|
80
|
+
// derived d = { …; return v } (block) -> __reactra_derived__("d", () => (() => { …; return v })())
|
|
81
|
+
// derived d = { a: 1 } (object) -> __reactra_derived__("d", () => ({ a: 1 }))
|
|
82
|
+
//
|
|
83
|
+
// DSL v3.0 §2.2 (Compiler §4 Pass 1.1): a `{`-prefixed initialiser is a BLOCK when
|
|
84
|
+
// its content contains a top-level `return` keyword or a top-level `;`; otherwise
|
|
85
|
+
// it is an object literal. The block form lowers to an IIFE inside the thunk so
|
|
86
|
+
// hover type = value type (not function), matching F3 of the shadow-fidelity spec.
|
|
87
|
+
// A block body must end with a `return` to produce a value.
|
|
88
|
+
//
|
|
89
|
+
// Top-level detection: scan tokens between the outer `{` and its matching `}` at
|
|
90
|
+
// brace-depth 0. We consider a token top-level when parenthesis/bracket/brace depth
|
|
91
|
+
// is zero relative to the content (the outer braces are excluded). A `return` at
|
|
92
|
+
// depth 0 → block. A `;` at depth 0 → block. Anything else (object properties,
|
|
93
|
+
// arrow functions, template literals etc.) → object literal.
|
|
94
|
+
// ----------------------------------------------------------------------------
|
|
95
|
+
export const rewriteDerived = (tokens, i) => {
|
|
96
|
+
const startTok = tokens[i];
|
|
97
|
+
const nameIdx = skipTrivia(tokens, i + 1);
|
|
98
|
+
const nameTok = tokens[nameIdx];
|
|
99
|
+
if (!nameTok || nameTok.kind !== "ident")
|
|
100
|
+
return null;
|
|
101
|
+
const eqIdx = skipTrivia(tokens, nameIdx + 1);
|
|
102
|
+
if (tokens[eqIdx]?.text !== "=")
|
|
103
|
+
return null;
|
|
104
|
+
const exprStart = skipTrivia(tokens, eqIdx + 1);
|
|
105
|
+
// If the first token after `=` is `{`, decide block vs object literal.
|
|
106
|
+
if (tokens[exprStart]?.text === "{") {
|
|
107
|
+
const braceCloseIdx = readBlockEnd(tokens, exprStart);
|
|
108
|
+
const inner = tokens.slice(exprStart + 1, braceCloseIdx); // tokens between { and }
|
|
109
|
+
const isBlock = detectBlockDerived(inner);
|
|
110
|
+
const innerText = slice(tokens, exprStart + 1, braceCloseIdx - 1).trim();
|
|
111
|
+
const endIdx = braceCloseIdx;
|
|
112
|
+
let exprText;
|
|
113
|
+
if (isBlock) {
|
|
114
|
+
// Block form → IIFE: `() => (() => { …; return v })()`
|
|
115
|
+
exprText = `__reactra_derived__("${nameTok.text}", () => (() => { ${innerText} })())`;
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
// Object literal form → wrap in parentheses so `() => ({…})` parses correctly
|
|
119
|
+
exprText = `__reactra_derived__("${nameTok.text}", () => ({ ${innerText} }))`;
|
|
120
|
+
}
|
|
121
|
+
return {
|
|
122
|
+
rewrite: {
|
|
123
|
+
span: { start: startTok.start, end: tokens[endIdx].end },
|
|
124
|
+
text: exprText,
|
|
125
|
+
},
|
|
126
|
+
consumedUpTo: endIdx,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
// Non-`{` initialiser: plain expression
|
|
130
|
+
const endIdx = readToStatementEndStrict(tokens, exprStart, {
|
|
131
|
+
keyword: "derived",
|
|
132
|
+
name: nameTok.text,
|
|
133
|
+
startTok,
|
|
134
|
+
});
|
|
135
|
+
const exprText = slice(tokens, exprStart, endIdx).trim();
|
|
136
|
+
return {
|
|
137
|
+
rewrite: {
|
|
138
|
+
span: { start: startTok.start, end: tokens[endIdx].end },
|
|
139
|
+
text: `__reactra_derived__("${nameTok.text}", () => (${exprText}))`,
|
|
140
|
+
},
|
|
141
|
+
consumedUpTo: endIdx,
|
|
142
|
+
};
|
|
143
|
+
};
|
|
144
|
+
/**
|
|
145
|
+
* Scan a flat token list (the content between `{` and `}`) and return `true` when
|
|
146
|
+
* the content is a **statement block** (contains a top-level `return` keyword or a
|
|
147
|
+
* top-level `;`), or `false` for an object literal.
|
|
148
|
+
*
|
|
149
|
+
* "Top-level" means brace/bracket/paren depth = 0 within the inner token list.
|
|
150
|
+
* Template literals are opaque (lexed as a single token) so their content is not
|
|
151
|
+
* inspected. Arrow functions (`() => { … }`) contribute nested braces which are
|
|
152
|
+
* depth > 0 — their `return` / `;` does NOT trigger the block detection.
|
|
153
|
+
*/
|
|
154
|
+
const detectBlockDerived = (inner) => {
|
|
155
|
+
let depth = 0;
|
|
156
|
+
for (const tok of inner) {
|
|
157
|
+
if (tok.kind === "punct") {
|
|
158
|
+
if (tok.text === "(" || tok.text === "[" || tok.text === "{") {
|
|
159
|
+
depth++;
|
|
160
|
+
}
|
|
161
|
+
else if (tok.text === ")" || tok.text === "]" || tok.text === "}") {
|
|
162
|
+
depth--;
|
|
163
|
+
}
|
|
164
|
+
else if (tok.text === ";" && depth === 0) {
|
|
165
|
+
return true; // top-level semicolon → statement block
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
if (tok.kind === "ident" && tok.text === "return" && depth === 0) {
|
|
169
|
+
return true; // top-level return → statement block
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return false;
|
|
173
|
+
};
|
|
174
|
+
// ----------------------------------------------------------------------------
|
|
175
|
+
// ref name = init -> __reactra_ref__("name", init)
|
|
176
|
+
// ----------------------------------------------------------------------------
|
|
177
|
+
export const rewriteRef = (tokens, i) => {
|
|
178
|
+
const startTok = tokens[i];
|
|
179
|
+
const nameIdx = skipTrivia(tokens, i + 1);
|
|
180
|
+
const nameTok = tokens[nameIdx];
|
|
181
|
+
if (!nameTok || nameTok.kind !== "ident")
|
|
182
|
+
return null;
|
|
183
|
+
const eqIdx = skipTrivia(tokens, nameIdx + 1);
|
|
184
|
+
// ref may also appear without an initialiser: `ref formEl` (initial value null)
|
|
185
|
+
if (tokens[eqIdx]?.text !== "=") {
|
|
186
|
+
// bare ref declaration
|
|
187
|
+
return {
|
|
188
|
+
rewrite: {
|
|
189
|
+
span: { start: startTok.start, end: nameTok.end },
|
|
190
|
+
text: `__reactra_ref__("${nameTok.text}", null)`,
|
|
191
|
+
},
|
|
192
|
+
consumedUpTo: nameIdx,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
const initStart = skipTrivia(tokens, eqIdx + 1);
|
|
196
|
+
const endIdx = readToStatementEnd(tokens, initStart);
|
|
197
|
+
const initText = slice(tokens, initStart, endIdx).trim();
|
|
198
|
+
return {
|
|
199
|
+
rewrite: {
|
|
200
|
+
span: { start: startTok.start, end: tokens[endIdx].end },
|
|
201
|
+
text: `__reactra_ref__("${nameTok.text}", ${initText})`,
|
|
202
|
+
},
|
|
203
|
+
consumedUpTo: endIdx,
|
|
204
|
+
};
|
|
205
|
+
};
|
|
206
|
+
// ----------------------------------------------------------------------------
|
|
207
|
+
// action f(args) { body } -> __reactra_action__("f", (args) => { body })
|
|
208
|
+
// action async f(args) { body } -> __reactra_action_async__("f", async (args) => { body })
|
|
209
|
+
// ----------------------------------------------------------------------------
|
|
210
|
+
export const rewriteAction = (tokens, i) => {
|
|
211
|
+
const startTok = tokens[i];
|
|
212
|
+
let cursor = skipTrivia(tokens, i + 1);
|
|
213
|
+
let isAsync = false;
|
|
214
|
+
if (tokens[cursor]?.text === "async") {
|
|
215
|
+
isAsync = true;
|
|
216
|
+
cursor = skipTrivia(tokens, cursor + 1);
|
|
217
|
+
}
|
|
218
|
+
const nameTok = tokens[cursor];
|
|
219
|
+
if (!nameTok || nameTok.kind !== "ident")
|
|
220
|
+
return null;
|
|
221
|
+
const parenOpenIdx = skipTrivia(tokens, cursor + 1);
|
|
222
|
+
if (tokens[parenOpenIdx]?.text !== "(")
|
|
223
|
+
return null;
|
|
224
|
+
const parenCloseIdx = readParenEnd(tokens, parenOpenIdx);
|
|
225
|
+
const args = slice(tokens, parenOpenIdx + 1, parenCloseIdx - 1).trim();
|
|
226
|
+
const braceOpenIdx = skipTrivia(tokens, parenCloseIdx + 1);
|
|
227
|
+
if (tokens[braceOpenIdx]?.text !== "{")
|
|
228
|
+
return null;
|
|
229
|
+
const braceCloseIdx = readBlockEnd(tokens, braceOpenIdx);
|
|
230
|
+
const rawBody = slice(tokens, braceOpenIdx + 1, braceCloseIdx - 1);
|
|
231
|
+
const body = rawBody.trim();
|
|
232
|
+
const marker = isAsync ? "__reactra_action_async__" : "__reactra_action__";
|
|
233
|
+
const arrowHead = isAsync ? `async (${args}) => { ` : `(${args}) => { `;
|
|
234
|
+
const textHead = `${marker}("${nameTok.text}", ${arrowHead}`;
|
|
235
|
+
// Guard a body whose last line ends in a `//` comment so the appended `})`
|
|
236
|
+
// isn't commented out (the preprocessor is line-oriented — see helpers).
|
|
237
|
+
const text = `${textHead}${guardTrailingLineComment(body)} })`;
|
|
238
|
+
// Sub-map the body so its interior lines map back to their own source line
|
|
239
|
+
// (#10-followup §8). The body is copied verbatim; only the ends are trimmed,
|
|
240
|
+
// so a single run from the first non-whitespace char suffices. (The trim joins
|
|
241
|
+
// the body's FIRST line onto the marker/header line — that one line maps to the
|
|
242
|
+
// header; lines 2..n map exactly.) The first body token's `.start` is the source
|
|
243
|
+
// anchor; `rawBody`'s leading whitespace is how much the trim consumed.
|
|
244
|
+
const bodyTokStart = tokens[braceOpenIdx + 1]?.start ?? tokens[braceOpenIdx].end;
|
|
245
|
+
const leadingWs = rawBody.length - rawBody.trimStart().length;
|
|
246
|
+
const srcMap = body.length > 0
|
|
247
|
+
? [{ textStart: textHead.length, srcStart: bodyTokStart + leadingWs, length: body.length }]
|
|
248
|
+
: undefined;
|
|
249
|
+
return {
|
|
250
|
+
rewrite: {
|
|
251
|
+
span: { start: startTok.start, end: tokens[braceCloseIdx].end },
|
|
252
|
+
text,
|
|
253
|
+
srcMap,
|
|
254
|
+
},
|
|
255
|
+
consumedUpTo: braceCloseIdx,
|
|
256
|
+
};
|
|
257
|
+
};
|
|
258
|
+
// ----------------------------------------------------------------------------
|
|
259
|
+
// command f(args) { body } -> __reactra_command__("f", async (args) => { body })
|
|
260
|
+
// `command` is the single async-WRITE primitive (CQRS twin of `resource`);
|
|
261
|
+
// the block form is implicitly async (it awaits a server write) and lowers to
|
|
262
|
+
// React 19 `useActionState` in Pass 9. The arrow form `command f() => expr`
|
|
263
|
+
// (+ optimistic/invalidate/rollback clauses) is P3 Slice 3 — not yet matched.
|
|
264
|
+
// ----------------------------------------------------------------------------
|
|
265
|
+
export const rewriteCommand = (tokens, i) => {
|
|
266
|
+
const startTok = tokens[i];
|
|
267
|
+
const cursor = skipTrivia(tokens, i + 1);
|
|
268
|
+
const nameTok = tokens[cursor];
|
|
269
|
+
if (!nameTok || nameTok.kind !== "ident")
|
|
270
|
+
return null;
|
|
271
|
+
const parenOpenIdx = skipTrivia(tokens, cursor + 1);
|
|
272
|
+
if (tokens[parenOpenIdx]?.text !== "(")
|
|
273
|
+
return null;
|
|
274
|
+
const parenCloseIdx = readParenEnd(tokens, parenOpenIdx);
|
|
275
|
+
const args = slice(tokens, parenOpenIdx + 1, parenCloseIdx - 1).trim();
|
|
276
|
+
const afterParen = skipTrivia(tokens, parenCloseIdx + 1);
|
|
277
|
+
// Block form: command f(args) { body } -> __reactra_command__ (Slice 1).
|
|
278
|
+
if (tokens[afterParen]?.text === "{") {
|
|
279
|
+
const braceOpenIdx = afterParen;
|
|
280
|
+
const braceCloseIdx = readBlockEnd(tokens, braceOpenIdx);
|
|
281
|
+
const rawBody = slice(tokens, braceOpenIdx + 1, braceCloseIdx - 1);
|
|
282
|
+
const body = rawBody.trim();
|
|
283
|
+
const textHead = `__reactra_command__("${nameTok.text}", async (${args}) => { `;
|
|
284
|
+
const text = `${textHead}${guardTrailingLineComment(body)} })`;
|
|
285
|
+
const bodyTokStart = tokens[braceOpenIdx + 1]?.start ?? tokens[braceOpenIdx].end;
|
|
286
|
+
const leadingWs = rawBody.length - rawBody.trimStart().length;
|
|
287
|
+
const srcMap = body.length > 0
|
|
288
|
+
? [{ textStart: textHead.length, srcStart: bodyTokStart + leadingWs, length: body.length }]
|
|
289
|
+
: undefined;
|
|
290
|
+
return {
|
|
291
|
+
rewrite: { span: { start: startTok.start, end: tokens[braceCloseIdx].end }, text, srcMap },
|
|
292
|
+
consumedUpTo: braceCloseIdx,
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
// Arrow form: command f(args) => fetcher -> __reactra_command_arrow__.
|
|
296
|
+
// Trailing clauses (optimistic {} / invalidate [] / rollback(e){}) are scanned
|
|
297
|
+
// after the fetcher, in any order, and collected into a 3rd marker options arg
|
|
298
|
+
// `{ optimistic: () => {…}, invalidate: [a, b], rollback: (e) => {…} }`.
|
|
299
|
+
if (tokens[afterParen]?.text === "=") {
|
|
300
|
+
const gtIdx = skipTrivia(tokens, afterParen + 1);
|
|
301
|
+
if (tokens[gtIdx]?.text !== ">")
|
|
302
|
+
return null;
|
|
303
|
+
const fnStart = skipTrivia(tokens, gtIdx + 1);
|
|
304
|
+
// Stops at the statement end OR a trailing clause keyword (so a clause is not
|
|
305
|
+
// swallowed into the fetcher).
|
|
306
|
+
const fnEnd = readCommandFetcherEnd(tokens, fnStart);
|
|
307
|
+
const fnText = slice(tokens, fnStart, fnEnd).trim();
|
|
308
|
+
let lastConsumed = fnEnd;
|
|
309
|
+
const props = [];
|
|
310
|
+
// Scan trailing clauses until a non-clause token (or statement end) is hit.
|
|
311
|
+
for (;;) {
|
|
312
|
+
const kw = skipTrivia(tokens, lastConsumed + 1);
|
|
313
|
+
const kwText = tokens[kw]?.text;
|
|
314
|
+
if (kwText === "optimistic") {
|
|
315
|
+
const obrace = skipTrivia(tokens, kw + 1);
|
|
316
|
+
if (tokens[obrace]?.text !== "{")
|
|
317
|
+
break;
|
|
318
|
+
const cbrace = readBlockEnd(tokens, obrace);
|
|
319
|
+
const body = slice(tokens, obrace + 1, cbrace - 1).trim();
|
|
320
|
+
props.push(`optimistic: () => { ${guardTrailingLineComment(body)} }`);
|
|
321
|
+
lastConsumed = cbrace;
|
|
322
|
+
}
|
|
323
|
+
else if (kwText === "invalidate") {
|
|
324
|
+
const obrack = skipTrivia(tokens, kw + 1);
|
|
325
|
+
if (tokens[obrack]?.text !== "[")
|
|
326
|
+
break;
|
|
327
|
+
const cbrack = readBracketEnd(tokens, obrack);
|
|
328
|
+
// The names are bare resource identifiers; carried verbatim as an array
|
|
329
|
+
// expression (parsed, never executed) — Pass 2 reads the identifier names.
|
|
330
|
+
const names = slice(tokens, obrack, cbrack);
|
|
331
|
+
props.push(`invalidate: ${names}`);
|
|
332
|
+
lastConsumed = cbrack;
|
|
333
|
+
}
|
|
334
|
+
else if (kwText === "rollback") {
|
|
335
|
+
const oparen = skipTrivia(tokens, kw + 1);
|
|
336
|
+
if (tokens[oparen]?.text !== "(")
|
|
337
|
+
break;
|
|
338
|
+
const cparen = readParenEnd(tokens, oparen);
|
|
339
|
+
const params = slice(tokens, oparen + 1, cparen - 1).trim();
|
|
340
|
+
const obrace = skipTrivia(tokens, cparen + 1);
|
|
341
|
+
if (tokens[obrace]?.text !== "{")
|
|
342
|
+
break;
|
|
343
|
+
const cbrace = readBlockEnd(tokens, obrace);
|
|
344
|
+
const body = slice(tokens, obrace + 1, cbrace - 1).trim();
|
|
345
|
+
props.push(`rollback: (${params}) => { ${guardTrailingLineComment(body)} }`);
|
|
346
|
+
lastConsumed = cbrace;
|
|
347
|
+
}
|
|
348
|
+
else {
|
|
349
|
+
break;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
const optsArg = props.length > 0 ? `, { ${props.join(", ")} }` : "";
|
|
353
|
+
const text = `__reactra_command_arrow__("${nameTok.text}", async (${args}) => (${fnText})${optsArg})`;
|
|
354
|
+
return {
|
|
355
|
+
rewrite: { span: { start: startTok.start, end: tokens[lastConsumed].end }, text },
|
|
356
|
+
consumedUpTo: lastConsumed,
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
return null;
|
|
360
|
+
};
|
|
361
|
+
// ----------------------------------------------------------------------------
|
|
362
|
+
// resource r(deps) => fn(deps)
|
|
363
|
+
// -> __reactra_resource__("r", () => (deps), (deps) => (fn(deps)))
|
|
364
|
+
// resource r(deps) signal => fn(deps, signal) (improvements #3)
|
|
365
|
+
// -> __reactra_resource__("r", () => (deps), (deps, { signal }) => (fn(deps, signal)))
|
|
366
|
+
// resource r(deps) cache => fn(deps) (compiler stage 3)
|
|
367
|
+
// -> __reactra_resource__("r", () => (deps), (deps) => (fn(deps)), { cache: true })
|
|
368
|
+
// resource r(deps) signal cache swr retry => fn(deps, signal)
|
|
369
|
+
// -> __reactra_resource__("r", () => (deps), (deps, { signal }) => (fn(deps, signal)),
|
|
370
|
+
// { cache: { staleWhileRevalidate: true, ttlMs: 60_000 }, retry: 3 })
|
|
371
|
+
//
|
|
372
|
+
// Modifiers sit between the deps `)` and `=>` in ANY order; order is
|
|
373
|
+
// preserved in source but normalized at emission. All bare (Stage 3 v0 —
|
|
374
|
+
// parameterized `cache(ttl: 60s)` / `retry(5)` is a future grammar
|
|
375
|
+
// extension). The 24-keyword cap is untouched — these are contextual
|
|
376
|
+
// tokens like `signal`, only special in this slot.
|
|
377
|
+
//
|
|
378
|
+
// Modifier defaults (Resource v1 §2):
|
|
379
|
+
// `cache` → `cache: true`
|
|
380
|
+
// `swr` → `cache: { staleWhileRevalidate: true, ttlMs: 60_000 }`
|
|
381
|
+
// (swr supersedes a bare `cache` if both are written)
|
|
382
|
+
// `retry` → `retry: 3`
|
|
383
|
+
//
|
|
384
|
+
// Architect Q2 Path B is preserved: omitting all opt-ins keeps the emitted
|
|
385
|
+
// useResource call cache-OFF and retry-OFF.
|
|
386
|
+
// ----------------------------------------------------------------------------
|
|
387
|
+
/** A bare identifier / dotted member chain — a constant reference (`MY_TTL`, `cfg.ttl`). */
|
|
388
|
+
const IDENT_OR_MEMBER = /^[A-Za-z_$][\w$]*(\.[A-Za-z_$][\w$]*)*$/;
|
|
389
|
+
/**
|
|
390
|
+
* Parse a `swr(…)` / `cache(ttl: …)` argument into a millisecond numeric string.
|
|
391
|
+
* Accepts a bare duration (`30s`, `500ms`, `5m`, `60000`) or a named `ttl: <dur>`;
|
|
392
|
+
* a unit is `ms` | `s` | `m` (underscores allowed in the number). A bare identifier
|
|
393
|
+
* / member chain (a constant in scope) passes through. Empty (`swr()`) → undefined,
|
|
394
|
+
* so the caller uses the default. Anything else — a typo'd unit (`5h`), a decimal
|
|
395
|
+
* (`1.5s`), whitespace (`30 s`), a non-positive ttl, junk — THROWS a clear compile
|
|
396
|
+
* error instead of emitting broken TSX (no-DX-surprises; Component DSL §2.4).
|
|
397
|
+
*/
|
|
398
|
+
const parseTtlArg = (inner) => {
|
|
399
|
+
const raw = inner.trim();
|
|
400
|
+
const v = raw.includes(":") ? raw.slice(raw.indexOf(":") + 1).trim() : raw;
|
|
401
|
+
if (v === "")
|
|
402
|
+
return undefined;
|
|
403
|
+
const m = /^(\d[\d_]*)(ms|s|m)?$/.exec(v);
|
|
404
|
+
if (m) {
|
|
405
|
+
const ms = Number(m[1].replace(/_/g, "")) * (m[2] === "s" ? 1000 : m[2] === "m" ? 60_000 : 1);
|
|
406
|
+
if (!(ms > 0)) {
|
|
407
|
+
throw new Error(`[reactra:compile] resource cache ttl must be > 0 (got "${raw}"). Component DSL §2.4.`);
|
|
408
|
+
}
|
|
409
|
+
return String(ms);
|
|
410
|
+
}
|
|
411
|
+
if (IDENT_OR_MEMBER.test(v))
|
|
412
|
+
return v;
|
|
413
|
+
throw new Error(`[reactra:compile] invalid resource cache ttl "${raw}" — expected a duration ` +
|
|
414
|
+
`(e.g. 30s / 500ms / 5m / 60000) or a constant. Component DSL §2.4.`);
|
|
415
|
+
};
|
|
416
|
+
/**
|
|
417
|
+
* Validate a `retry(…)` argument: a positive integer count, a constant identifier,
|
|
418
|
+
* or empty (→ undefined, use the default 3). Anything else (a comma list, a call,
|
|
419
|
+
* junk) THROWS rather than emit `retry: 1, 2` (two object props → broken TSX).
|
|
420
|
+
*/
|
|
421
|
+
const parseRetryArg = (inner) => {
|
|
422
|
+
const raw = inner.trim();
|
|
423
|
+
if (raw === "")
|
|
424
|
+
return undefined;
|
|
425
|
+
if (/^\d[\d_]*$/.test(raw))
|
|
426
|
+
return raw.replace(/_/g, "");
|
|
427
|
+
if (IDENT_OR_MEMBER.test(raw))
|
|
428
|
+
return raw;
|
|
429
|
+
throw new Error(`[reactra:compile] invalid retry argument "${raw}" — expected a count (e.g. 5) ` +
|
|
430
|
+
`or a constant. Component DSL §2.4.`);
|
|
431
|
+
};
|
|
432
|
+
export const rewriteResource = (tokens, i) => {
|
|
433
|
+
const startTok = tokens[i];
|
|
434
|
+
const nameIdx = skipTrivia(tokens, i + 1);
|
|
435
|
+
const nameTok = tokens[nameIdx];
|
|
436
|
+
if (!nameTok || nameTok.kind !== "ident")
|
|
437
|
+
return null;
|
|
438
|
+
const parenOpenIdx = skipTrivia(tokens, nameIdx + 1);
|
|
439
|
+
if (tokens[parenOpenIdx]?.text !== "(")
|
|
440
|
+
return null;
|
|
441
|
+
const parenCloseIdx = readParenEnd(tokens, parenOpenIdx);
|
|
442
|
+
const deps = slice(tokens, parenOpenIdx + 1, parenCloseIdx - 1).trim();
|
|
443
|
+
// Modifier chain: any number of `signal` / `cache` / `swr` / `retry` in any
|
|
444
|
+
// order, then `=>`. Each is single-use; a duplicate is permitted but harmless
|
|
445
|
+
// (idempotent flag flip). `swr` / `cache` / `retry` may take a parenthesized
|
|
446
|
+
// argument (`swr(30s)`, `cache(ttl: 60s)`, `retry(5)`); a bare modifier keeps
|
|
447
|
+
// its default. Any other ident bails (return null → caller falls back to the
|
|
448
|
+
// bare identifier, which TypeScript will then flag).
|
|
449
|
+
let cur = skipTrivia(tokens, parenCloseIdx + 1);
|
|
450
|
+
let hasSignal = false;
|
|
451
|
+
let hasCache = false;
|
|
452
|
+
let hasSwr = false;
|
|
453
|
+
let hasRetry = false;
|
|
454
|
+
let cacheTtl;
|
|
455
|
+
let retryCount;
|
|
456
|
+
let selectArrow;
|
|
457
|
+
while (tokens[cur]?.kind === "ident") {
|
|
458
|
+
const text = tokens[cur].text;
|
|
459
|
+
if (text === "signal") {
|
|
460
|
+
hasSignal = true;
|
|
461
|
+
}
|
|
462
|
+
else if (text === "select") {
|
|
463
|
+
const aft = skipTrivia(tokens, cur + 1);
|
|
464
|
+
if (tokens[aft]?.text !== "(") {
|
|
465
|
+
throw new Error(`[reactra:compile] \`select\` requires a selector arrow, e.g. \`select(u => u.name)\`. Component DSL §2.4.`);
|
|
466
|
+
}
|
|
467
|
+
const close = readParenEnd(tokens, aft);
|
|
468
|
+
const inner = slice(tokens, aft + 1, close - 1).trim();
|
|
469
|
+
if (!inner.includes("=>")) {
|
|
470
|
+
throw new Error(`[reactra:compile] \`select(...)\` must be an arrow function (got "${inner}"). Component DSL §2.4.`);
|
|
471
|
+
}
|
|
472
|
+
selectArrow = inner;
|
|
473
|
+
cur = skipTrivia(tokens, close + 1);
|
|
474
|
+
continue;
|
|
475
|
+
}
|
|
476
|
+
else if (text === "cache" || text === "swr") {
|
|
477
|
+
if (text === "swr")
|
|
478
|
+
hasSwr = true;
|
|
479
|
+
else
|
|
480
|
+
hasCache = true;
|
|
481
|
+
const aft = skipTrivia(tokens, cur + 1);
|
|
482
|
+
if (tokens[aft]?.text === "(") {
|
|
483
|
+
const close = readParenEnd(tokens, aft);
|
|
484
|
+
cacheTtl = parseTtlArg(slice(tokens, aft + 1, close - 1));
|
|
485
|
+
cur = skipTrivia(tokens, close + 1);
|
|
486
|
+
continue;
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
else if (text === "retry") {
|
|
490
|
+
hasRetry = true;
|
|
491
|
+
const aft = skipTrivia(tokens, cur + 1);
|
|
492
|
+
if (tokens[aft]?.text === "(") {
|
|
493
|
+
const close = readParenEnd(tokens, aft);
|
|
494
|
+
retryCount = parseRetryArg(slice(tokens, aft + 1, close - 1));
|
|
495
|
+
cur = skipTrivia(tokens, close + 1);
|
|
496
|
+
continue;
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
else
|
|
500
|
+
break;
|
|
501
|
+
cur = skipTrivia(tokens, cur + 1);
|
|
502
|
+
}
|
|
503
|
+
if (tokens[cur]?.text !== "=")
|
|
504
|
+
return null;
|
|
505
|
+
cur = skipTrivia(tokens, cur + 1);
|
|
506
|
+
if (tokens[cur]?.text !== ">")
|
|
507
|
+
return null;
|
|
508
|
+
const fnStart = skipTrivia(tokens, cur + 1);
|
|
509
|
+
const endIdx = readToStatementEnd(tokens, fnStart);
|
|
510
|
+
const fnText = slice(tokens, fnStart, endIdx).trim();
|
|
511
|
+
// Fetcher params: the deps binding first (if any), then `{ signal }` if the
|
|
512
|
+
// `signal` token was present. Built as a list so empty deps don't leave a
|
|
513
|
+
// dangling leading comma (e.g. `resource cart() signal =>` → `({ signal })`).
|
|
514
|
+
//
|
|
515
|
+
// When `deps` is a member-expression (e.g. `customer.data?.id`), it cannot be
|
|
516
|
+
// used directly as a binding-pattern parameter in the fetcher arrow — TypeScript
|
|
517
|
+
// rejects `(customer.data?.id) => …`. In that case we synthesize `_dep` as the
|
|
518
|
+
// param name. The fetcher body already references the reactive value directly
|
|
519
|
+
// (e.g. `api.snap(customer.data?.id)`), so `_dep` is an unused param by design.
|
|
520
|
+
const isSimpleBinding = /^[A-Za-z_$][\w$]*$/.test(deps);
|
|
521
|
+
const fetcherDepParam = isSimpleBinding ? deps : "_dep";
|
|
522
|
+
const fetcherParams = [deps.length > 0 ? fetcherDepParam : "", hasSignal ? "{ signal }" : ""]
|
|
523
|
+
.filter((p) => p.length > 0)
|
|
524
|
+
.join(", ");
|
|
525
|
+
// Stage 3 — opt-ins compile to a 4th argument carrying a ResourceOptions
|
|
526
|
+
// partial. `swr` supersedes a bare `cache` (swr implies caching). Omitting
|
|
527
|
+
// ALL opt-ins emits 3 args (matching the Stage 1 / Path B shape), so
|
|
528
|
+
// backwards-compatibility with the existing AST is exact.
|
|
529
|
+
const optsParts = [];
|
|
530
|
+
if (hasSwr) {
|
|
531
|
+
optsParts.push(`cache: { staleWhileRevalidate: true, ttlMs: ${cacheTtl ?? "60_000"} }`);
|
|
532
|
+
}
|
|
533
|
+
else if (hasCache) {
|
|
534
|
+
// Bare `cache` → cache-by-deps, no TTL. `cache(ttl: D)` → hard-expiry: the
|
|
535
|
+
// entry is stale past `ttlMs` and the next read refetches (no stale-serve;
|
|
536
|
+
// that's `swr`). The runtime treats a non-SWR ttl'd stale entry as a miss.
|
|
537
|
+
optsParts.push(cacheTtl !== undefined ? `cache: { ttlMs: ${cacheTtl} }` : `cache: true`);
|
|
538
|
+
}
|
|
539
|
+
if (hasRetry)
|
|
540
|
+
optsParts.push(`retry: ${retryCount ?? "3"}`);
|
|
541
|
+
// `select(u => u.name)` → a `select` option (the selector arrow, verbatim). The
|
|
542
|
+
// handle's `.data` becomes the selector's return; the consumer re-renders only
|
|
543
|
+
// when the selected slice changes (Resource v1 §2). Wrapped in parens so an
|
|
544
|
+
// expression-body arrow stays a single expression in the object literal.
|
|
545
|
+
if (selectArrow !== undefined)
|
|
546
|
+
optsParts.push(`select: (${selectArrow})`);
|
|
547
|
+
const optsArg = optsParts.length > 0 ? `, { ${optsParts.join(", ")} }` : ``;
|
|
548
|
+
return {
|
|
549
|
+
rewrite: {
|
|
550
|
+
span: { start: startTok.start, end: tokens[endIdx].end },
|
|
551
|
+
text: `__reactra_resource__("${nameTok.text}", () => (${deps}), (${fetcherParams}) => (${fnText})${optsArg})`,
|
|
552
|
+
},
|
|
553
|
+
consumedUpTo: endIdx,
|
|
554
|
+
};
|
|
555
|
+
};
|
|
556
|
+
// ----------------------------------------------------------------------------
|
|
557
|
+
// mount { body } -> __reactra_mount__(() => { body })
|
|
558
|
+
// cleanup { body } -> __reactra_cleanup__(() => { body })
|
|
559
|
+
// effect { body } -> __reactra_effect__(() => { body })
|
|
560
|
+
// effect on(x) { body } -> __reactra_effect_on__(() => x, () => { body })
|
|
561
|
+
// ----------------------------------------------------------------------------
|
|
562
|
+
export const rewriteBlock = (markerName, tokens, i) => {
|
|
563
|
+
const startTok = tokens[i];
|
|
564
|
+
const braceIdx = skipTrivia(tokens, i + 1);
|
|
565
|
+
if (tokens[braceIdx]?.text !== "{")
|
|
566
|
+
return null;
|
|
567
|
+
const closeIdx = readBlockEnd(tokens, braceIdx);
|
|
568
|
+
const body = slice(tokens, braceIdx + 1, closeIdx - 1).trim();
|
|
569
|
+
return {
|
|
570
|
+
rewrite: {
|
|
571
|
+
span: { start: startTok.start, end: tokens[closeIdx].end },
|
|
572
|
+
text: `__reactra_${markerName}__(() => { ${guardTrailingLineComment(body)} })`,
|
|
573
|
+
},
|
|
574
|
+
consumedUpTo: closeIdx,
|
|
575
|
+
};
|
|
576
|
+
};
|
|
577
|
+
/**
|
|
578
|
+
* `effect on(x) { body }` → `__reactra_effect_on__(() => x, () => { body })`
|
|
579
|
+
*
|
|
580
|
+
* DSL v3.0: bare `effect {}` is retired (teardown is a `return` inside `mount`
|
|
581
|
+
* or `effect on`). When the token after `effect` is not `on`, this rewriter
|
|
582
|
+
* returns `null` — the source is not a DSL construct and falls through as plain JS.
|
|
583
|
+
*/
|
|
584
|
+
export const rewriteEffect = (tokens, i) => {
|
|
585
|
+
const startTok = tokens[i];
|
|
586
|
+
const nextIdx = skipTrivia(tokens, i + 1);
|
|
587
|
+
// `effect on(x) { body }`
|
|
588
|
+
if (tokens[nextIdx]?.text === "on") {
|
|
589
|
+
const parenIdx = skipTrivia(tokens, nextIdx + 1);
|
|
590
|
+
if (tokens[parenIdx]?.text !== "(")
|
|
591
|
+
return null;
|
|
592
|
+
const parenCloseIdx = readParenEnd(tokens, parenIdx);
|
|
593
|
+
const watchExpr = slice(tokens, parenIdx + 1, parenCloseIdx - 1).trim();
|
|
594
|
+
const braceIdx = skipTrivia(tokens, parenCloseIdx + 1);
|
|
595
|
+
if (tokens[braceIdx]?.text !== "{")
|
|
596
|
+
return null;
|
|
597
|
+
const closeIdx = readBlockEnd(tokens, braceIdx);
|
|
598
|
+
const body = slice(tokens, braceIdx + 1, closeIdx - 1).trim();
|
|
599
|
+
return {
|
|
600
|
+
rewrite: {
|
|
601
|
+
span: { start: startTok.start, end: tokens[closeIdx].end },
|
|
602
|
+
text: `__reactra_effect_on__(() => (${watchExpr}), () => { ${guardTrailingLineComment(body)} })`,
|
|
603
|
+
},
|
|
604
|
+
consumedUpTo: closeIdx,
|
|
605
|
+
};
|
|
606
|
+
}
|
|
607
|
+
// v3.0: bare `effect {}` is not a DSL form. The caller (preprocess.ts dispatch)
|
|
608
|
+
// throws R027 before reaching here when `on` is absent — this null is a fallback
|
|
609
|
+
// for any non-component-body context where `effect` appears without `on`.
|
|
610
|
+
return null;
|
|
611
|
+
};
|
|
612
|
+
// `view { JSX }` is rewritten by the scope tracker (not as a single keyword
|
|
613
|
+
// rewriter) so that DSL keywords inside the view body — notably
|
|
614
|
+
// `await(r) { } pending { } error(e) { }` — get the chance to rewrite first.
|
|
615
|
+
// See preprocess.ts: open-rewrite happens when view-body is pushed; close-
|
|
616
|
+
// rewrite happens when view-body is popped. The two rewrites combined yield:
|
|
617
|
+
// view { JSX } -> __reactra_view__(() => (<>JSX</>))
|
|
618
|
+
// ----------------------------------------------------------------------------
|
|
619
|
+
// await(r) { A } pending { B } error(e) { C }
|
|
620
|
+
// -> {__reactra_await__(() => r, () => (<>A</>), () => (<>B</>), (e) => (<>C</>))}
|
|
621
|
+
//
|
|
622
|
+
// `pending` and `error` are optional. If absent, their slot is `null`.
|
|
623
|
+
//
|
|
624
|
+
// The output is wrapped in `{}` because `await` is legal only in view-body
|
|
625
|
+
// scope (per KEYWORDS_BY_SCOPE), where the surrounding text is a JSX
|
|
626
|
+
// fragment. A bare call expression in JSX text position would be parsed as
|
|
627
|
+
// raw text by Babel — wrapping in `{}` makes it a valid JSXExpressionContainer
|
|
628
|
+
// so Pass 9 codegen can find and rewrite the call.
|
|
629
|
+
// ----------------------------------------------------------------------------
|
|
630
|
+
export const rewriteAwait = (tokens, i) => {
|
|
631
|
+
const startTok = tokens[i];
|
|
632
|
+
// Trimmed verbatim slice + the source offset its first kept char came from
|
|
633
|
+
// (the trim consumes leading whitespace — same anchoring as rewriteAction's
|
|
634
|
+
// srcMap). Powers the per-block VerbatimRuns below.
|
|
635
|
+
const trimmedSlice = (openIdx, closeIdx) => {
|
|
636
|
+
const raw = slice(tokens, openIdx + 1, closeIdx - 1);
|
|
637
|
+
const anchor = tokens[openIdx + 1]?.start ?? tokens[openIdx].end;
|
|
638
|
+
return { text: raw.trim(), srcStart: anchor + (raw.length - raw.trimStart().length) };
|
|
639
|
+
};
|
|
640
|
+
// resource expression in parens
|
|
641
|
+
const parenIdx = skipTrivia(tokens, i + 1);
|
|
642
|
+
if (tokens[parenIdx]?.text !== "(")
|
|
643
|
+
return null;
|
|
644
|
+
const parenCloseIdx = readParenEnd(tokens, parenIdx);
|
|
645
|
+
const resource = trimmedSlice(parenIdx, parenCloseIdx);
|
|
646
|
+
// success body
|
|
647
|
+
const bodyOpenIdx = skipTrivia(tokens, parenCloseIdx + 1);
|
|
648
|
+
if (tokens[bodyOpenIdx]?.text !== "{")
|
|
649
|
+
return null;
|
|
650
|
+
const bodyCloseIdx = readBlockEnd(tokens, bodyOpenIdx);
|
|
651
|
+
const success = trimmedSlice(bodyOpenIdx, bodyCloseIdx);
|
|
652
|
+
let pending = null;
|
|
653
|
+
let errorParam = null;
|
|
654
|
+
let errorBlock = null;
|
|
655
|
+
let endIdx = bodyCloseIdx;
|
|
656
|
+
// optional `pending { body }` immediately after
|
|
657
|
+
const afterBodyIdx = skipTrivia(tokens, bodyCloseIdx + 1);
|
|
658
|
+
let cursor = afterBodyIdx;
|
|
659
|
+
if (tokens[cursor]?.text === "pending") {
|
|
660
|
+
const pBraceIdx = skipTrivia(tokens, cursor + 1);
|
|
661
|
+
if (tokens[pBraceIdx]?.text === "{") {
|
|
662
|
+
const pCloseIdx = readBlockEnd(tokens, pBraceIdx);
|
|
663
|
+
pending = trimmedSlice(pBraceIdx, pCloseIdx);
|
|
664
|
+
endIdx = pCloseIdx;
|
|
665
|
+
cursor = skipTrivia(tokens, pCloseIdx + 1);
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
// optional `error(e) { body }`
|
|
669
|
+
if (tokens[cursor]?.text === "error") {
|
|
670
|
+
const eParenIdx = skipTrivia(tokens, cursor + 1);
|
|
671
|
+
if (tokens[eParenIdx]?.text === "(") {
|
|
672
|
+
const eParenCloseIdx = readParenEnd(tokens, eParenIdx);
|
|
673
|
+
errorParam = slice(tokens, eParenIdx + 1, eParenCloseIdx - 1).trim();
|
|
674
|
+
const eBraceIdx = skipTrivia(tokens, eParenCloseIdx + 1);
|
|
675
|
+
if (tokens[eBraceIdx]?.text === "{") {
|
|
676
|
+
const eCloseIdx = readBlockEnd(tokens, eBraceIdx);
|
|
677
|
+
errorBlock = trimmedSlice(eBraceIdx, eCloseIdx);
|
|
678
|
+
endIdx = eCloseIdx;
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
// Build the replacement text incrementally so each verbatim block records a
|
|
683
|
+
// VerbatimRun (textStart in the replacement, srcStart in the DSL) — these are
|
|
684
|
+
// what give editor tooling exact positions INSIDE await blocks (the success/
|
|
685
|
+
// pending/error JSX is user code carried verbatim; everything else here is
|
|
686
|
+
// synthesized).
|
|
687
|
+
const srcMap = [];
|
|
688
|
+
let text = "";
|
|
689
|
+
const append = (s) => {
|
|
690
|
+
text += s;
|
|
691
|
+
};
|
|
692
|
+
const appendVerbatim = (piece) => {
|
|
693
|
+
if (piece.text.length > 0) {
|
|
694
|
+
srcMap.push({ textStart: text.length, srcStart: piece.srcStart, length: piece.text.length });
|
|
695
|
+
}
|
|
696
|
+
text += piece.text;
|
|
697
|
+
};
|
|
698
|
+
append("{__reactra_await__(() => (");
|
|
699
|
+
appendVerbatim(resource);
|
|
700
|
+
append("), () => (<>");
|
|
701
|
+
appendVerbatim(success);
|
|
702
|
+
append("</>), ");
|
|
703
|
+
if (pending !== null) {
|
|
704
|
+
append("() => (<>");
|
|
705
|
+
appendVerbatim(pending);
|
|
706
|
+
append("</>)");
|
|
707
|
+
}
|
|
708
|
+
else {
|
|
709
|
+
append("null");
|
|
710
|
+
}
|
|
711
|
+
append(", ");
|
|
712
|
+
if (errorBlock !== null) {
|
|
713
|
+
append(`(${errorParam ?? "e"}) => (<>`);
|
|
714
|
+
appendVerbatim(errorBlock);
|
|
715
|
+
append("</>)");
|
|
716
|
+
}
|
|
717
|
+
else {
|
|
718
|
+
append("null");
|
|
719
|
+
}
|
|
720
|
+
append(")}");
|
|
721
|
+
return {
|
|
722
|
+
rewrite: {
|
|
723
|
+
span: { start: startTok.start, end: tokens[endIdx].end },
|
|
724
|
+
text,
|
|
725
|
+
srcMap: srcMap.length > 0 ? srcMap : undefined,
|
|
726
|
+
},
|
|
727
|
+
consumedUpTo: endIdx,
|
|
728
|
+
};
|
|
729
|
+
};
|
|
730
|
+
// ----------------------------------------------------------------------------
|
|
731
|
+
// uses A, B, C -> __reactra_uses__("A", "B", "C")
|
|
732
|
+
// ----------------------------------------------------------------------------
|
|
733
|
+
export const rewriteUses = (tokens, i) => {
|
|
734
|
+
const startTok = tokens[i];
|
|
735
|
+
const names = [];
|
|
736
|
+
let cursor = skipTrivia(tokens, i + 1);
|
|
737
|
+
while (cursor < tokens.length) {
|
|
738
|
+
const t = tokens[cursor];
|
|
739
|
+
if (!t || t.kind !== "ident")
|
|
740
|
+
break;
|
|
741
|
+
names.push(t.text);
|
|
742
|
+
const next = skipTrivia(tokens, cursor + 1);
|
|
743
|
+
if (tokens[next]?.text === ",") {
|
|
744
|
+
cursor = skipTrivia(tokens, next + 1);
|
|
745
|
+
continue;
|
|
746
|
+
}
|
|
747
|
+
cursor = next - 1;
|
|
748
|
+
break;
|
|
749
|
+
}
|
|
750
|
+
if (names.length === 0)
|
|
751
|
+
return null;
|
|
752
|
+
// endIdx is the last consumed ident; back up to find its end pos
|
|
753
|
+
let lastIdent = i;
|
|
754
|
+
let count = 0;
|
|
755
|
+
for (let j = i + 1; j < tokens.length && count < names.length; j++) {
|
|
756
|
+
if (tokens[j].kind === "ident") {
|
|
757
|
+
lastIdent = j;
|
|
758
|
+
count++;
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
return {
|
|
762
|
+
rewrite: {
|
|
763
|
+
span: { start: startTok.start, end: tokens[lastIdent].end },
|
|
764
|
+
text: `__reactra_uses__(${names.map((n) => `"${n}"`).join(", ")})`,
|
|
765
|
+
},
|
|
766
|
+
consumedUpTo: lastIdent,
|
|
767
|
+
};
|
|
768
|
+
};
|
|
769
|
+
// ----------------------------------------------------------------------------
|
|
770
|
+
// `inject` dispatcher (Requirements v14 / backlog 1a — `use` retired). The
|
|
771
|
+
// token after `inject` selects the form:
|
|
772
|
+
// inject store storeX -> __reactra_inject_store__("storeX")
|
|
773
|
+
// inject store storeX { a, b } -> __reactra_inject_store__("storeX", { fields: ["a","b"] })
|
|
774
|
+
// inject store storeX({ k: v }) -> __reactra_inject_store_args__("storeX", { k: v })
|
|
775
|
+
// inject store storeX({ k: v }) { a, b } -> __reactra_inject_store_args__("storeX", { k: v }, { fields: ["a","b"] })
|
|
776
|
+
// inject store storeX(key)({ k: v }) -> __reactra_inject_store_keyed__("storeX", key, { k: v })
|
|
777
|
+
// inject store storeX: T -> __reactra_inject_store__("storeX", { typeOverride: "T" })
|
|
778
|
+
// inject store storeX({ k: v }): T { a } -> __reactra_inject_store_args__("storeX", { k: v }, { typeOverride: "T", fields: ["a"] })
|
|
779
|
+
// inject service svcX -> __reactra_inject__("svcX", { kind: "service" })
|
|
780
|
+
// inject X from config("K") -> __reactra_inject__("X", { source: "config" })
|
|
781
|
+
// inject X: T from inject -> __reactra_inject__("X", { source: "inject" })
|
|
782
|
+
// inject X (no qualifier) -> __reactra_inject__("X") (Pass 3a raises SVC015)
|
|
783
|
+
//
|
|
784
|
+
// Object-literal inputs (`{ k: v }`) are emitted verbatim — the value is a
|
|
785
|
+
// route `param`/`query` in scope; whether it's a param or query is resolved by
|
|
786
|
+
// codegen (the lexer-level preprocessor can't know). Field-selection braces
|
|
787
|
+
// drive the destructure (Store §2.5); omitted → namespace binding.
|
|
788
|
+
// ----------------------------------------------------------------------------
|
|
789
|
+
export const rewriteInject = (tokens, i) => {
|
|
790
|
+
const startTok = tokens[i];
|
|
791
|
+
const k1 = skipTrivia(tokens, i + 1);
|
|
792
|
+
const k1Tok = tokens[k1];
|
|
793
|
+
if (!k1Tok || k1Tok.kind !== "ident")
|
|
794
|
+
return null;
|
|
795
|
+
// `inject store ...`
|
|
796
|
+
if (k1Tok.text === "store")
|
|
797
|
+
return rewriteInjectStore(tokens, i, k1);
|
|
798
|
+
// `inject service X [as Y]`
|
|
799
|
+
if (k1Tok.text === "service") {
|
|
800
|
+
const nameIdx = skipTrivia(tokens, k1 + 1);
|
|
801
|
+
const nameTok = tokens[nameIdx];
|
|
802
|
+
if (!nameTok || nameTok.kind !== "ident")
|
|
803
|
+
return null;
|
|
804
|
+
// optional namespace alias `as Y` (Service v2.3 §4.2 — parity with inject store)
|
|
805
|
+
let alias = null;
|
|
806
|
+
const afterName = skipTrivia(tokens, nameIdx + 1);
|
|
807
|
+
if (tokens[afterName]?.text === "as") {
|
|
808
|
+
const aliasTok = tokens[skipTrivia(tokens, afterName + 1)];
|
|
809
|
+
if (aliasTok && aliasTok.kind === "ident")
|
|
810
|
+
alias = aliasTok.text;
|
|
811
|
+
}
|
|
812
|
+
const endIdx = readToStatementEnd(tokens, nameIdx);
|
|
813
|
+
const opts = alias ? `{ kind: "service", alias: "${alias}" }` : `{ kind: "service" }`;
|
|
814
|
+
return {
|
|
815
|
+
rewrite: {
|
|
816
|
+
span: { start: startTok.start, end: tokens[endIdx].end },
|
|
817
|
+
text: `__reactra_inject__("${nameTok.text}", ${opts})`,
|
|
818
|
+
},
|
|
819
|
+
consumedUpTo: endIdx,
|
|
820
|
+
};
|
|
821
|
+
}
|
|
822
|
+
// `inject X [: T] [from <source>]` — name is k1. A `from config(...)` /
|
|
823
|
+
// `from inject` clause makes the injection explicit (no kind qualifier
|
|
824
|
+
// required); a bare `inject X` carries no second arg and Pass 3a raises
|
|
825
|
+
// SVC015.
|
|
826
|
+
const nameTok = k1Tok;
|
|
827
|
+
const endIdx = readToStatementEnd(tokens, k1);
|
|
828
|
+
let source = null;
|
|
829
|
+
for (let j = k1 + 1; j <= endIdx; j++) {
|
|
830
|
+
if (tokens[j]?.text === "from") {
|
|
831
|
+
const srcTok = tokens[skipTrivia(tokens, j + 1)];
|
|
832
|
+
if (srcTok?.text === "config")
|
|
833
|
+
source = "config";
|
|
834
|
+
else if (srcTok?.text === "inject")
|
|
835
|
+
source = "inject";
|
|
836
|
+
break;
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
const args = source ? `"${nameTok.text}", { source: "${source}" }` : `"${nameTok.text}"`;
|
|
840
|
+
return {
|
|
841
|
+
rewrite: {
|
|
842
|
+
span: { start: startTok.start, end: tokens[endIdx].end },
|
|
843
|
+
text: `__reactra_inject__(${args})`,
|
|
844
|
+
},
|
|
845
|
+
consumedUpTo: endIdx,
|
|
846
|
+
};
|
|
847
|
+
};
|
|
848
|
+
/**
|
|
849
|
+
* Parse the store form of `inject` starting at `inject` (index `startIdx`),
|
|
850
|
+
* with `store` at `storeKwIdx`. Handles optional object-literal inputs
|
|
851
|
+
* `({...})`, keyed `(key)({...})`, and field-selection braces `{ a, b }`.
|
|
852
|
+
*/
|
|
853
|
+
const rewriteInjectStore = (tokens, startIdx, storeKwIdx) => {
|
|
854
|
+
const startTok = tokens[startIdx];
|
|
855
|
+
const nameIdx = skipTrivia(tokens, storeKwIdx + 1);
|
|
856
|
+
const nameTok = tokens[nameIdx];
|
|
857
|
+
if (!nameTok || nameTok.kind !== "ident")
|
|
858
|
+
return null;
|
|
859
|
+
const storeName = nameTok.text;
|
|
860
|
+
let cursor = skipTrivia(tokens, nameIdx + 1);
|
|
861
|
+
let inputsText = null;
|
|
862
|
+
let keyText = null;
|
|
863
|
+
let endIdx = nameIdx;
|
|
864
|
+
// optional `(...)` inputs, or keyed `(key)(...)`
|
|
865
|
+
if (tokens[cursor]?.text === "(") {
|
|
866
|
+
const close1 = readParenEnd(tokens, cursor);
|
|
867
|
+
const group1 = slice(tokens, cursor + 1, close1 - 1).trim();
|
|
868
|
+
endIdx = close1;
|
|
869
|
+
const after1 = skipTrivia(tokens, close1 + 1);
|
|
870
|
+
if (tokens[after1]?.text === "(") {
|
|
871
|
+
const close2 = readParenEnd(tokens, after1);
|
|
872
|
+
keyText = group1;
|
|
873
|
+
inputsText = slice(tokens, after1 + 1, close2 - 1).trim();
|
|
874
|
+
endIdx = close2;
|
|
875
|
+
cursor = skipTrivia(tokens, close2 + 1);
|
|
876
|
+
}
|
|
877
|
+
else {
|
|
878
|
+
inputsText = group1;
|
|
879
|
+
cursor = after1;
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
// optional `: Type` override (Store §2.5 / Compiler §4 v14) — sits between the
|
|
883
|
+
// inputs `)` and the field `{`. Read the type reference up to the top-level
|
|
884
|
+
// field-opening `{` or statement end, tracking <>/()/[] depth so generics,
|
|
885
|
+
// tuples, and parenthesised types stay intact (single-`>` angle tracking matches
|
|
886
|
+
// `readOptionalTypeAnnotation`). Recorded as `typeOverride`; the structural-subset
|
|
887
|
+
// check (S015) is left to TypeScript via the emitted `useReactraStore<Type>`
|
|
888
|
+
// generic — not done here. An inline object-literal type at this position is
|
|
889
|
+
// unsupported in Phase 1 (use a named type, per the architect review).
|
|
890
|
+
let typeOverride = null;
|
|
891
|
+
if (tokens[cursor]?.text === ":") {
|
|
892
|
+
const typeStart = skipTrivia(tokens, cursor + 1);
|
|
893
|
+
let paren = 0;
|
|
894
|
+
let bracket = 0;
|
|
895
|
+
let angle = 0;
|
|
896
|
+
let last = typeStart - 1;
|
|
897
|
+
for (let j = typeStart; j < tokens.length; j++) {
|
|
898
|
+
const t = tokens[j];
|
|
899
|
+
if (t.kind === "punct") {
|
|
900
|
+
if (t.text === "(")
|
|
901
|
+
paren++;
|
|
902
|
+
else if (t.text === ")") {
|
|
903
|
+
if (paren === 0)
|
|
904
|
+
break;
|
|
905
|
+
paren--;
|
|
906
|
+
}
|
|
907
|
+
else if (t.text === "[")
|
|
908
|
+
bracket++;
|
|
909
|
+
else if (t.text === "]")
|
|
910
|
+
bracket--;
|
|
911
|
+
else if (t.text === "<")
|
|
912
|
+
angle++;
|
|
913
|
+
else if (t.text === ">")
|
|
914
|
+
angle = Math.max(0, angle - 1);
|
|
915
|
+
else if (paren === 0 &&
|
|
916
|
+
bracket === 0 &&
|
|
917
|
+
angle === 0 &&
|
|
918
|
+
(t.text === "{" || t.text === "}" || t.text === ";")) {
|
|
919
|
+
break; // field-selection `{`, enclosing-scope `}`, or explicit `;`
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
// `inject store X: IFoo as Y` — the alias `as` ends the type annotation
|
|
923
|
+
// (TS types never contain a bare `as`). Store v4.9 §2.5.
|
|
924
|
+
if (paren === 0 && bracket === 0 && angle === 0 && t.kind === "ident" && t.text === "as") {
|
|
925
|
+
break;
|
|
926
|
+
}
|
|
927
|
+
if (paren === 0 &&
|
|
928
|
+
bracket === 0 &&
|
|
929
|
+
angle === 0 &&
|
|
930
|
+
t.kind === "whitespace" &&
|
|
931
|
+
t.text.includes("\n")) {
|
|
932
|
+
break;
|
|
933
|
+
}
|
|
934
|
+
if (t.kind !== "whitespace" && t.kind !== "comment-line" && t.kind !== "comment-block") {
|
|
935
|
+
last = j;
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
if (last >= typeStart) {
|
|
939
|
+
typeOverride = slice(tokens, typeStart, last).trim();
|
|
940
|
+
endIdx = last;
|
|
941
|
+
cursor = skipTrivia(tokens, last + 1);
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
// optional namespace alias `as Y` — renames the whole-store binding
|
|
945
|
+
// (Store v4.9 §2.5). Mirrors ES `import * as Y`.
|
|
946
|
+
let alias = null;
|
|
947
|
+
if (tokens[cursor]?.text === "as") {
|
|
948
|
+
const aliasIdx = skipTrivia(tokens, cursor + 1);
|
|
949
|
+
const aliasTok = tokens[aliasIdx];
|
|
950
|
+
if (aliasTok && aliasTok.kind === "ident") {
|
|
951
|
+
alias = aliasTok.text;
|
|
952
|
+
endIdx = aliasIdx;
|
|
953
|
+
cursor = skipTrivia(tokens, aliasIdx + 1);
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
let fields = null;
|
|
957
|
+
if (tokens[cursor]?.text === "{") {
|
|
958
|
+
const closeBrace = readBlockEnd(tokens, cursor);
|
|
959
|
+
fields = slice(tokens, cursor + 1, closeBrace - 1)
|
|
960
|
+
.split(",")
|
|
961
|
+
.map((s) => s.trim())
|
|
962
|
+
.filter(Boolean)
|
|
963
|
+
.map((entry) => {
|
|
964
|
+
const asParts = entry.split(/\s+as\s+/);
|
|
965
|
+
if (asParts.length === 2)
|
|
966
|
+
return { source: asParts[0].trim(), local: asParts[1].trim(), as: true };
|
|
967
|
+
const colon = entry.indexOf(":");
|
|
968
|
+
if (colon >= 0)
|
|
969
|
+
return { source: entry.slice(0, colon).trim(), local: entry.slice(colon + 1).trim() };
|
|
970
|
+
return { source: entry };
|
|
971
|
+
});
|
|
972
|
+
endIdx = closeBrace;
|
|
973
|
+
}
|
|
974
|
+
// Trailing options object — unified `{ typeOverride?, fields? }` (Compiler §4
|
|
975
|
+
// v14). `typeOverride` first to match the spec's marker rows; emitted only when
|
|
976
|
+
// non-empty so the no-options / fields-only output stays byte-identical.
|
|
977
|
+
const optionParts = [];
|
|
978
|
+
if (typeOverride)
|
|
979
|
+
optionParts.push(`typeOverride: ${JSON.stringify(typeOverride)}`);
|
|
980
|
+
if (fields && fields.length > 0) {
|
|
981
|
+
// Bare fields stay plain strings (byte-identical to pre-rename output); a
|
|
982
|
+
// rename is a `{ source, local }` object (Store v4.7 §2.5 / Compiler §4).
|
|
983
|
+
const lit = fields
|
|
984
|
+
.map((f) => f.local != null
|
|
985
|
+
? `{ source: "${f.source}", local: "${f.local}"${f.as ? ", as: true" : ""} }`
|
|
986
|
+
: `"${f.source}"`)
|
|
987
|
+
.join(", ");
|
|
988
|
+
optionParts.push(`fields: [${lit}]`);
|
|
989
|
+
}
|
|
990
|
+
if (alias)
|
|
991
|
+
optionParts.push(`alias: ${JSON.stringify(alias)}`);
|
|
992
|
+
const optionsArg = optionParts.length > 0 ? `{ ${optionParts.join(", ")} }` : null;
|
|
993
|
+
const inputsObj = inputsText !== null ? (inputsText.length > 0 ? inputsText : "{}") : null;
|
|
994
|
+
let text;
|
|
995
|
+
if (keyText !== null) {
|
|
996
|
+
const parts = [`"${storeName}"`, keyText, inputsObj ?? "{}"];
|
|
997
|
+
if (optionsArg)
|
|
998
|
+
parts.push(optionsArg);
|
|
999
|
+
text = `__reactra_inject_store_keyed__(${parts.join(", ")})`;
|
|
1000
|
+
}
|
|
1001
|
+
else if (inputsObj !== null) {
|
|
1002
|
+
const parts = [`"${storeName}"`, inputsObj];
|
|
1003
|
+
if (optionsArg)
|
|
1004
|
+
parts.push(optionsArg);
|
|
1005
|
+
text = `__reactra_inject_store_args__(${parts.join(", ")})`;
|
|
1006
|
+
}
|
|
1007
|
+
else {
|
|
1008
|
+
const parts = [`"${storeName}"`];
|
|
1009
|
+
if (optionsArg)
|
|
1010
|
+
parts.push(optionsArg);
|
|
1011
|
+
text = `__reactra_inject_store__(${parts.join(", ")})`;
|
|
1012
|
+
}
|
|
1013
|
+
return {
|
|
1014
|
+
rewrite: { span: { start: startTok.start, end: tokens[endIdx].end }, text },
|
|
1015
|
+
consumedUpTo: endIdx,
|
|
1016
|
+
};
|
|
1017
|
+
};
|
|
1018
|
+
// ----------------------------------------------------------------------------
|
|
1019
|
+
// provide X with Y -> __reactra_provide__("X", Y)
|
|
1020
|
+
// ----------------------------------------------------------------------------
|
|
1021
|
+
export const rewriteProvide = (tokens, i) => {
|
|
1022
|
+
const startTok = tokens[i];
|
|
1023
|
+
const nameIdx = skipTrivia(tokens, i + 1);
|
|
1024
|
+
const nameTok = tokens[nameIdx];
|
|
1025
|
+
if (!nameTok || nameTok.kind !== "ident")
|
|
1026
|
+
return null;
|
|
1027
|
+
const withIdx = skipTrivia(tokens, nameIdx + 1);
|
|
1028
|
+
if (tokens[withIdx]?.text !== "with")
|
|
1029
|
+
return null;
|
|
1030
|
+
const valueStart = skipTrivia(tokens, withIdx + 1);
|
|
1031
|
+
const endIdx = readToStatementEnd(tokens, valueStart);
|
|
1032
|
+
const valueText = slice(tokens, valueStart, endIdx).trim();
|
|
1033
|
+
return {
|
|
1034
|
+
rewrite: {
|
|
1035
|
+
span: { start: startTok.start, end: tokens[endIdx].end },
|
|
1036
|
+
text: `__reactra_provide__("${nameTok.text}", ${valueText})`,
|
|
1037
|
+
},
|
|
1038
|
+
consumedUpTo: endIdx,
|
|
1039
|
+
};
|
|
1040
|
+
};
|
|
1041
|
+
// ----------------------------------------------------------------------------
|
|
1042
|
+
// param X: T -> __reactra_param__("X")
|
|
1043
|
+
// query X: T = default -> __reactra_query__("X", default)
|
|
1044
|
+
// query X: T -> __reactra_query__("X")
|
|
1045
|
+
// ----------------------------------------------------------------------------
|
|
1046
|
+
export const rewriteParam = (tokens, i) => {
|
|
1047
|
+
const startTok = tokens[i];
|
|
1048
|
+
const nameIdx = skipTrivia(tokens, i + 1);
|
|
1049
|
+
const nameTok = tokens[nameIdx];
|
|
1050
|
+
if (!nameTok || nameTok.kind !== "ident")
|
|
1051
|
+
return null;
|
|
1052
|
+
// The `: T` annotation is intentionally NOT threaded: route params are always
|
|
1053
|
+
// `string` at runtime (the matched-route invariant — `__route.params` is
|
|
1054
|
+
// `Record<string, string>`, and Pass 9 emits no coercion), so projecting a
|
|
1055
|
+
// non-string declared type in the shadow would be a runtime-contradicting lie.
|
|
1056
|
+
// The shadow types every param `string` (honest); `: number` etc. surfaces as a
|
|
1057
|
+
// type error on number ops, nudging the author to coerce. R026 still applies.
|
|
1058
|
+
const endIdx = readToStatementEndStrict(tokens, nameIdx, {
|
|
1059
|
+
keyword: "param",
|
|
1060
|
+
name: nameTok.text,
|
|
1061
|
+
startTok,
|
|
1062
|
+
});
|
|
1063
|
+
return {
|
|
1064
|
+
rewrite: {
|
|
1065
|
+
span: { start: startTok.start, end: tokens[endIdx].end },
|
|
1066
|
+
text: `__reactra_param__("${nameTok.text}")`,
|
|
1067
|
+
},
|
|
1068
|
+
consumedUpTo: endIdx,
|
|
1069
|
+
};
|
|
1070
|
+
};
|
|
1071
|
+
export const rewriteQuery = (tokens, i) => {
|
|
1072
|
+
const startTok = tokens[i];
|
|
1073
|
+
const nameIdx = skipTrivia(tokens, i + 1);
|
|
1074
|
+
const nameTok = tokens[nameIdx];
|
|
1075
|
+
if (!nameTok || nameTok.kind !== "ident")
|
|
1076
|
+
return null;
|
|
1077
|
+
// Optional `?` for optional fields
|
|
1078
|
+
let cursor = nameIdx + 1;
|
|
1079
|
+
if (tokens[skipTrivia(tokens, cursor)]?.text === "?") {
|
|
1080
|
+
cursor = skipTrivia(tokens, cursor) + 1;
|
|
1081
|
+
}
|
|
1082
|
+
// Optional `: T` annotation — captured (not discarded) so the compiler can
|
|
1083
|
+
// coerce the raw URL string to the declared type at read time (#5c-typed
|
|
1084
|
+
// runtime coercion). The type text is threaded as a string-literal arg.
|
|
1085
|
+
const { type, next } = readOptionalTypeAnnotation(tokens, cursor);
|
|
1086
|
+
cursor = skipTrivia(tokens, next);
|
|
1087
|
+
// Strip any trailing `;` that readOptionalTypeAnnotation may include when the
|
|
1088
|
+
// annotation is on a single line and `;` precedes the newline (same issue as
|
|
1089
|
+
// rewriteState — the `;` is captured in the type text).
|
|
1090
|
+
let typeText = type !== undefined && type.trim().length > 0 ? type.replace(/;$/, "").trim() : "string";
|
|
1091
|
+
// Custom parser: `query foo: T from parseFoo` (Router §3.1). The type reader
|
|
1092
|
+
// doesn't stop at `from`, so it captures `T from parseFoo` — split the trailing
|
|
1093
|
+
// ` from <ident>` back off (a TS type never contains a bare `from` keyword).
|
|
1094
|
+
let fromParser;
|
|
1095
|
+
const fromMatch = /^([\s\S]*?)\s+from\s+([A-Za-z_$][\w$]*)$/.exec(typeText);
|
|
1096
|
+
if (fromMatch) {
|
|
1097
|
+
typeText = fromMatch[1].trim() || "string";
|
|
1098
|
+
fromParser = fromMatch[2];
|
|
1099
|
+
}
|
|
1100
|
+
let defaultText;
|
|
1101
|
+
if (tokens[cursor]?.text === "=") {
|
|
1102
|
+
const valueStart = skipTrivia(tokens, cursor + 1);
|
|
1103
|
+
const valueEnd = readToStatementEndStrict(tokens, valueStart, {
|
|
1104
|
+
keyword: "query",
|
|
1105
|
+
name: nameTok.text,
|
|
1106
|
+
startTok,
|
|
1107
|
+
});
|
|
1108
|
+
defaultText = slice(tokens, valueStart, valueEnd).trim();
|
|
1109
|
+
cursor = valueEnd;
|
|
1110
|
+
}
|
|
1111
|
+
else {
|
|
1112
|
+
cursor = readToStatementEndStrict(tokens, nameIdx, {
|
|
1113
|
+
keyword: "query",
|
|
1114
|
+
name: nameTok.text,
|
|
1115
|
+
startTok,
|
|
1116
|
+
});
|
|
1117
|
+
}
|
|
1118
|
+
// Emit `__reactra_query__("name", "<type>"[, default[, parser]])`. The type is
|
|
1119
|
+
// JSON-stringified so an enum like `"a" | "b"` survives as a safe string
|
|
1120
|
+
// literal. Pass-2 reads arg[1]=type, arg[2]=default, arg[3]=custom parser
|
|
1121
|
+
// (a bare identifier — a function in scope). When a parser is present without a
|
|
1122
|
+
// default, arg[2] is the `undefined` placeholder so the parser stays at arg[3].
|
|
1123
|
+
const argParts = [`"${nameTok.text}"`, JSON.stringify(typeText)];
|
|
1124
|
+
if (fromParser) {
|
|
1125
|
+
argParts.push(defaultText ?? "undefined", fromParser);
|
|
1126
|
+
}
|
|
1127
|
+
else if (defaultText !== undefined) {
|
|
1128
|
+
argParts.push(defaultText);
|
|
1129
|
+
}
|
|
1130
|
+
const args = argParts.join(", ");
|
|
1131
|
+
return {
|
|
1132
|
+
rewrite: {
|
|
1133
|
+
span: { start: startTok.start, end: tokens[cursor].end },
|
|
1134
|
+
text: `__reactra_query__(${args})`,
|
|
1135
|
+
},
|
|
1136
|
+
consumedUpTo: cursor,
|
|
1137
|
+
};
|
|
1138
|
+
};
|
|
1139
|
+
// ----------------------------------------------------------------------------
|
|
1140
|
+
// meta { K: V; ... } -> __reactra_meta__({ K: V, ... })
|
|
1141
|
+
// ----------------------------------------------------------------------------
|
|
1142
|
+
export const rewriteMeta = (tokens, i) => {
|
|
1143
|
+
const startTok = tokens[i];
|
|
1144
|
+
const braceIdx = skipTrivia(tokens, i + 1);
|
|
1145
|
+
if (tokens[braceIdx]?.text !== "{")
|
|
1146
|
+
return null;
|
|
1147
|
+
const closeIdx = readBlockEnd(tokens, braceIdx);
|
|
1148
|
+
// Convert `K: V;` separators to commas. Simple heuristic: replace `;` at top
|
|
1149
|
+
// level inside the object literal with `,`. We do this on the raw text.
|
|
1150
|
+
const inner = slice(tokens, braceIdx + 1, closeIdx - 1);
|
|
1151
|
+
const normalised = inner.replace(/;\s*$/gm, ",").replace(/;/g, ",").trim();
|
|
1152
|
+
return {
|
|
1153
|
+
rewrite: {
|
|
1154
|
+
span: { start: startTok.start, end: tokens[closeIdx].end },
|
|
1155
|
+
text: `__reactra_meta__({ ${normalised} })`,
|
|
1156
|
+
},
|
|
1157
|
+
consumedUpTo: closeIdx,
|
|
1158
|
+
};
|
|
1159
|
+
};
|
|
1160
|
+
// ----------------------------------------------------------------------------
|
|
1161
|
+
// prefetch on TRIGGER -> __reactra_prefetch__("TRIGGER")
|
|
1162
|
+
// prefetch none -> __reactra_prefetch__("none")
|
|
1163
|
+
// ----------------------------------------------------------------------------
|
|
1164
|
+
export const rewritePrefetch = (tokens, i) => {
|
|
1165
|
+
const startTok = tokens[i];
|
|
1166
|
+
let cursor = skipTrivia(tokens, i + 1);
|
|
1167
|
+
if (tokens[cursor]?.text === "on") {
|
|
1168
|
+
cursor = skipTrivia(tokens, cursor + 1);
|
|
1169
|
+
}
|
|
1170
|
+
const trigger = tokens[cursor];
|
|
1171
|
+
if (!trigger || trigger.kind !== "ident")
|
|
1172
|
+
return null;
|
|
1173
|
+
return {
|
|
1174
|
+
rewrite: {
|
|
1175
|
+
span: { start: startTok.start, end: trigger.end },
|
|
1176
|
+
text: `__reactra_prefetch__("${trigger.text}")`,
|
|
1177
|
+
},
|
|
1178
|
+
consumedUpTo: cursor,
|
|
1179
|
+
};
|
|
1180
|
+
};
|
|
1181
|
+
// ----------------------------------------------------------------------------
|
|
1182
|
+
// input X: T -> __reactra_input__("X")
|
|
1183
|
+
// input X?: T -> __reactra_input__("X", { optional: true })
|
|
1184
|
+
// input X: T = default -> __reactra_input__("X", { default })
|
|
1185
|
+
//
|
|
1186
|
+
// Store-body only. The TS annotation `: T` passes through to the sidecar
|
|
1187
|
+
// metadata in Compiler §6; the marker carries only the runtime-needed bits.
|
|
1188
|
+
// ----------------------------------------------------------------------------
|
|
1189
|
+
export const rewriteInput = (tokens, i) => {
|
|
1190
|
+
const startTok = tokens[i];
|
|
1191
|
+
const nameIdx = skipTrivia(tokens, i + 1);
|
|
1192
|
+
const nameTok = tokens[nameIdx];
|
|
1193
|
+
if (!nameTok || nameTok.kind !== "ident")
|
|
1194
|
+
return null;
|
|
1195
|
+
// Detect optional marker `?`
|
|
1196
|
+
let cursor = nameIdx + 1;
|
|
1197
|
+
let isOptional = false;
|
|
1198
|
+
const afterName = skipTrivia(tokens, cursor);
|
|
1199
|
+
if (tokens[afterName]?.text === "?") {
|
|
1200
|
+
isOptional = true;
|
|
1201
|
+
cursor = afterName + 1;
|
|
1202
|
+
}
|
|
1203
|
+
// Optional type annotation — captured (not discarded) and threaded into the
|
|
1204
|
+
// marker's options object as `tsType` for the shadow emitter (Run 2 / D3).
|
|
1205
|
+
// Pass-9 ignores it (store inputs lower structurally), so emission is unchanged.
|
|
1206
|
+
const { type, next } = readOptionalTypeAnnotation(tokens, cursor);
|
|
1207
|
+
const tsType = type !== undefined && type.trim().length > 0 ? type.replace(/;$/, "").trim() : undefined;
|
|
1208
|
+
cursor = skipTrivia(tokens, next);
|
|
1209
|
+
// Optional default
|
|
1210
|
+
let defaultText;
|
|
1211
|
+
let endIdx = nameIdx;
|
|
1212
|
+
if (tokens[cursor]?.text === "=") {
|
|
1213
|
+
const valueStart = skipTrivia(tokens, cursor + 1);
|
|
1214
|
+
endIdx = readToStatementEndStrict(tokens, valueStart, {
|
|
1215
|
+
keyword: "input",
|
|
1216
|
+
name: nameTok.text,
|
|
1217
|
+
startTok,
|
|
1218
|
+
});
|
|
1219
|
+
defaultText = slice(tokens, valueStart, endIdx).trim();
|
|
1220
|
+
}
|
|
1221
|
+
else {
|
|
1222
|
+
endIdx = readToStatementEndStrict(tokens, nameIdx, {
|
|
1223
|
+
keyword: "input",
|
|
1224
|
+
name: nameTok.text,
|
|
1225
|
+
startTok,
|
|
1226
|
+
});
|
|
1227
|
+
}
|
|
1228
|
+
// Assemble the options object, threading `tsType` last when present.
|
|
1229
|
+
const opts = [];
|
|
1230
|
+
if (isOptional)
|
|
1231
|
+
opts.push("optional: true");
|
|
1232
|
+
else if (defaultText !== undefined)
|
|
1233
|
+
opts.push(`default: ${defaultText}`);
|
|
1234
|
+
if (tsType)
|
|
1235
|
+
opts.push(`tsType: ${JSON.stringify(tsType)}`);
|
|
1236
|
+
const args = opts.length > 0 ? `"${nameTok.text}", { ${opts.join(", ")} }` : `"${nameTok.text}"`;
|
|
1237
|
+
return {
|
|
1238
|
+
rewrite: {
|
|
1239
|
+
span: { start: startTok.start, end: tokens[endIdx].end },
|
|
1240
|
+
text: `__reactra_input__(${args})`,
|
|
1241
|
+
},
|
|
1242
|
+
consumedUpTo: endIdx,
|
|
1243
|
+
};
|
|
1244
|
+
};
|
|
1245
|
+
// ----------------------------------------------------------------------------
|
|
1246
|
+
// preserved state X = init -> __reactra_state__("X", init); __reactra_preserved__("X")
|
|
1247
|
+
//
|
|
1248
|
+
// DSL v3.0 §6 (Store spec §6): `preserved` is now an inline modifier on a single
|
|
1249
|
+
// `state` declaration. One field per line; multi-declarator comma lists are DROPPED
|
|
1250
|
+
// (S012 is structurally impossible — no comma-list to parse). Both the state binding
|
|
1251
|
+
// AND the preserved marker are emitted in the same rewrite so Pass 2 sees them as
|
|
1252
|
+
// a paired sequence. The initialiser follows `=`; an optional `: T` annotation is
|
|
1253
|
+
// consumed (moved into the state cast per the state rewriter convention).
|
|
1254
|
+
//
|
|
1255
|
+
// Store-body only. The literal word `state` follows `preserved` as a modifier.
|
|
1256
|
+
// ----------------------------------------------------------------------------
|
|
1257
|
+
export const rewritePreserved = (tokens, i) => {
|
|
1258
|
+
const startTok = tokens[i];
|
|
1259
|
+
// expect literal `state` after `preserved`
|
|
1260
|
+
const stateIdx = skipTrivia(tokens, i + 1);
|
|
1261
|
+
if (tokens[stateIdx]?.text !== "state")
|
|
1262
|
+
return null;
|
|
1263
|
+
// Read the single field name
|
|
1264
|
+
const nameIdx = skipTrivia(tokens, stateIdx + 1);
|
|
1265
|
+
const nameTok = tokens[nameIdx];
|
|
1266
|
+
if (!nameTok || nameTok.kind !== "ident")
|
|
1267
|
+
return null;
|
|
1268
|
+
// Optional `: T` type annotation
|
|
1269
|
+
const { type, next } = readOptionalTypeAnnotation(tokens, nameIdx + 1);
|
|
1270
|
+
// Required `= init` initialiser
|
|
1271
|
+
const eqIdx = skipTrivia(tokens, next);
|
|
1272
|
+
if (tokens[eqIdx]?.text !== "=")
|
|
1273
|
+
return null;
|
|
1274
|
+
const initStart = skipTrivia(tokens, eqIdx + 1);
|
|
1275
|
+
const endIdx = readToStatementEnd(tokens, initStart);
|
|
1276
|
+
const initText = slice(tokens, initStart, endIdx).trim();
|
|
1277
|
+
const name = nameTok.text;
|
|
1278
|
+
// Emit BOTH the state binding and the preserved marker as a single rewrite.
|
|
1279
|
+
// Pass 2 reads them in sequence: the state marker registers the field, then the
|
|
1280
|
+
// preserved marker tags it. This keeps the emission byte-identical to the prior
|
|
1281
|
+
// separate `state X` + `preserved state X` pair.
|
|
1282
|
+
return {
|
|
1283
|
+
rewrite: {
|
|
1284
|
+
span: { start: startTok.start, end: tokens[endIdx].end },
|
|
1285
|
+
text: `${emitStateMarker(name, type ?? null, initText)}; __reactra_preserved__("${name}")`,
|
|
1286
|
+
},
|
|
1287
|
+
consumedUpTo: endIdx,
|
|
1288
|
+
};
|
|
1289
|
+
};
|
|
1290
|
+
// ----------------------------------------------------------------------------
|
|
1291
|
+
// errorBoundary(e) { JSX } -> __reactra_error_boundary__((e) => (<>JSX</>))
|
|
1292
|
+
//
|
|
1293
|
+
// Component-body only. Provides a React ErrorBoundary fallback scoped to
|
|
1294
|
+
// this component's children. See Component DSL v2 §7.2.
|
|
1295
|
+
// ----------------------------------------------------------------------------
|
|
1296
|
+
export const rewriteErrorBoundary = (tokens, i) => {
|
|
1297
|
+
const startTok = tokens[i];
|
|
1298
|
+
const parenIdx = skipTrivia(tokens, i + 1);
|
|
1299
|
+
if (tokens[parenIdx]?.text !== "(")
|
|
1300
|
+
return null;
|
|
1301
|
+
const parenCloseIdx = readParenEnd(tokens, parenIdx);
|
|
1302
|
+
const errorParam = slice(tokens, parenIdx + 1, parenCloseIdx - 1).trim();
|
|
1303
|
+
const braceIdx = skipTrivia(tokens, parenCloseIdx + 1);
|
|
1304
|
+
if (tokens[braceIdx]?.text !== "{")
|
|
1305
|
+
return null;
|
|
1306
|
+
const closeIdx = readBlockEnd(tokens, braceIdx);
|
|
1307
|
+
const body = slice(tokens, braceIdx + 1, closeIdx - 1).trim();
|
|
1308
|
+
return {
|
|
1309
|
+
rewrite: {
|
|
1310
|
+
span: { start: startTok.start, end: tokens[closeIdx].end },
|
|
1311
|
+
text: `__reactra_error_boundary__((${errorParam || "e"}) => (<>${body}</>))`,
|
|
1312
|
+
},
|
|
1313
|
+
consumedUpTo: closeIdx,
|
|
1314
|
+
};
|
|
1315
|
+
};
|
|
1316
|
+
// ----------------------------------------------------------------------------
|
|
1317
|
+
// suspense { JSX } -> __reactra_suspense__(() => (<>JSX</>))
|
|
1318
|
+
//
|
|
1319
|
+
// Component-body only. Provides a route/component-scoped Suspense fallback.
|
|
1320
|
+
// See Component DSL v2 §7.3.
|
|
1321
|
+
// ----------------------------------------------------------------------------
|
|
1322
|
+
export const rewriteSuspense = (tokens, i) => {
|
|
1323
|
+
const startTok = tokens[i];
|
|
1324
|
+
const braceIdx = skipTrivia(tokens, i + 1);
|
|
1325
|
+
if (tokens[braceIdx]?.text !== "{")
|
|
1326
|
+
return null;
|
|
1327
|
+
const closeIdx = readBlockEnd(tokens, braceIdx);
|
|
1328
|
+
const body = slice(tokens, braceIdx + 1, closeIdx - 1).trim();
|
|
1329
|
+
return {
|
|
1330
|
+
rewrite: {
|
|
1331
|
+
span: { start: startTok.start, end: tokens[closeIdx].end },
|
|
1332
|
+
text: `__reactra_suspense__(() => (<>${body}</>))`,
|
|
1333
|
+
},
|
|
1334
|
+
consumedUpTo: closeIdx,
|
|
1335
|
+
};
|
|
1336
|
+
};
|
|
1337
|
+
// ----------------------------------------------------------------------------
|
|
1338
|
+
// transition NAME -> __reactra_transition__("NAME")
|
|
1339
|
+
// transition NAME for Yms -> __reactra_transition__("NAME", Y)
|
|
1340
|
+
//
|
|
1341
|
+
// NAME may be a hyphenated identifier like `slide-left`, `slide-right`,
|
|
1342
|
+
// `slide-up`, `slide-down`, etc. The lexer tokenises `slide-left` as
|
|
1343
|
+
// `slide` + `-` + `left`, so we greedy-consume `(- ident)*` after the
|
|
1344
|
+
// first ident to assemble the full transition name.
|
|
1345
|
+
// ----------------------------------------------------------------------------
|
|
1346
|
+
export const rewriteTransition = (tokens, i) => {
|
|
1347
|
+
const startTok = tokens[i];
|
|
1348
|
+
const nameIdx = skipTrivia(tokens, i + 1);
|
|
1349
|
+
const nameTok = tokens[nameIdx];
|
|
1350
|
+
if (!nameTok || nameTok.kind !== "ident")
|
|
1351
|
+
return null;
|
|
1352
|
+
// Assemble hyphenated name: `slide` + `-` + `left` -> "slide-left"
|
|
1353
|
+
let nameText = nameTok.text;
|
|
1354
|
+
let endIdx = nameIdx;
|
|
1355
|
+
let lookahead = nameIdx + 1;
|
|
1356
|
+
while (lookahead < tokens.length) {
|
|
1357
|
+
const dashCandidate = tokens[lookahead];
|
|
1358
|
+
if (!dashCandidate)
|
|
1359
|
+
break;
|
|
1360
|
+
if (dashCandidate.kind === "punct" && dashCandidate.text === "-") {
|
|
1361
|
+
const after = tokens[lookahead + 1];
|
|
1362
|
+
if (after && after.kind === "ident") {
|
|
1363
|
+
nameText += `-${after.text}`;
|
|
1364
|
+
endIdx = lookahead + 1;
|
|
1365
|
+
lookahead = endIdx + 1;
|
|
1366
|
+
continue;
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
break;
|
|
1370
|
+
}
|
|
1371
|
+
let duration;
|
|
1372
|
+
const forIdx = skipTrivia(tokens, endIdx + 1);
|
|
1373
|
+
if (tokens[forIdx]?.text === "for") {
|
|
1374
|
+
const numIdx = skipTrivia(tokens, forIdx + 1);
|
|
1375
|
+
const numTok = tokens[numIdx];
|
|
1376
|
+
if (numTok && numTok.kind === "number") {
|
|
1377
|
+
// Number token may be "300ms" — strip trailing units
|
|
1378
|
+
duration = numTok.text.replace(/[^\d]/g, "");
|
|
1379
|
+
endIdx = numIdx;
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
const args = duration ? `"${nameText}", ${duration}` : `"${nameText}"`;
|
|
1383
|
+
return {
|
|
1384
|
+
rewrite: {
|
|
1385
|
+
span: { start: startTok.start, end: tokens[endIdx].end },
|
|
1386
|
+
text: `__reactra_transition__(${args})`,
|
|
1387
|
+
},
|
|
1388
|
+
consumedUpTo: endIdx,
|
|
1389
|
+
};
|
|
1390
|
+
};
|
|
1391
|
+
//# sourceMappingURL=rewriters.js.map
|