@reactra/language-tools 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 +109 -0
- package/dist/cli/reactra-tsc.d.ts +3 -0
- package/dist/cli/reactra-tsc.d.ts.map +1 -0
- package/dist/cli/reactra-tsc.js +25 -0
- package/dist/cli/reactra-tsc.js.map +1 -0
- package/dist/diagnostic-plugin.d.ts +12 -0
- package/dist/diagnostic-plugin.d.ts.map +1 -0
- package/dist/diagnostic-plugin.js +112 -0
- package/dist/diagnostic-plugin.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -0
- package/dist/language-plugin.d.ts +44 -0
- package/dist/language-plugin.d.ts.map +1 -0
- package/dist/language-plugin.js +323 -0
- package/dist/language-plugin.js.map +1 -0
- package/dist/lsp/server.cjs +68546 -0
- package/dist/shadow/emitter.d.ts +15 -0
- package/dist/shadow/emitter.d.ts.map +1 -0
- package/dist/shadow/emitter.js +871 -0
- package/dist/shadow/emitter.js.map +1 -0
- package/dist/shadow/index.d.ts +3 -0
- package/dist/shadow/index.d.ts.map +1 -0
- package/dist/shadow/index.js +4 -0
- package/dist/shadow/index.js.map +1 -0
- package/dist/shadow/mapper.d.ts +55 -0
- package/dist/shadow/mapper.d.ts.map +1 -0
- package/dist/shadow/mapper.js +111 -0
- package/dist/shadow/mapper.js.map +1 -0
- package/dist/ts-plugin/impl.cjs +50132 -0
- package/dist/ts-plugin/index.cjs +3 -0
- package/dist/ts-plugin/package.json +8 -0
- package/package.json +58 -0
|
@@ -0,0 +1,871 @@
|
|
|
1
|
+
// Shadow emitter — reactra2tsx lowering for the Reactra DSL v2.
|
|
2
|
+
// Per plan/phase-4-language-tools-spec.md §3 (reactra2tsx emitter contract).
|
|
3
|
+
//
|
|
4
|
+
// Produces a TypeScript string that tsc can type-check, with every DSL binding
|
|
5
|
+
// reachable under its real type. Consumes FileGraph from compile() — same object
|
|
6
|
+
// Pass 9 receives (anti-drift decision §2).
|
|
7
|
+
// framework-review §B1: the compiler's behaviour registry + the shared emission
|
|
8
|
+
// conventions (setter naming, await arg order). The shadow imports these via the
|
|
9
|
+
// package surface (no cross-package relative paths; babel-plugin owns both).
|
|
10
|
+
// `classifyStateSetters`/`setterNameFor` are the GATED reconciliation that
|
|
11
|
+
// replaced the shadow's old looser `resolveSetterName` (§B1 C1).
|
|
12
|
+
import { NATIVE_BEHAVIOURS } from "@reactra/babel-plugin";
|
|
13
|
+
import { AWAIT_ARG_ORDER, classifyStateSetters, setterNameFor } from "@reactra/babel-plugin/conventions";
|
|
14
|
+
import { preprocessedToDslOffset, ppSpanToDslRuns } from "./mapper.js";
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
// Surface-type naming convention (§3.2 / store consumer resolution)
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
/** PascalCase: `fooBar` → `FooBar`. */
|
|
19
|
+
const toPascalCase = (name) => name.charAt(0).toUpperCase() + name.slice(1);
|
|
20
|
+
/** The exported surface type alias for a given store/service name. */
|
|
21
|
+
const surfaceTypeName = (name) => `${toPascalCase(name)}Surface`;
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
// Import-path extraction from userImports entries
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
/**
|
|
26
|
+
* True when the import statement imports `name` (value or type import).
|
|
27
|
+
* Handles: `{ name }`, `{ name as alias }`, `{ type name }`, `import type { name }`.
|
|
28
|
+
*/
|
|
29
|
+
const importsName = (imp, name) => {
|
|
30
|
+
const match = imp.match(/\{([^}]+)\}/);
|
|
31
|
+
if (!match)
|
|
32
|
+
return false;
|
|
33
|
+
return match[1].split(",").some((part) => {
|
|
34
|
+
const stripped = part.trim().replace(/^type\s+/, "");
|
|
35
|
+
const [imported] = stripped.split(/\s+as\s+/);
|
|
36
|
+
return imported?.trim() === name;
|
|
37
|
+
});
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* Extract the module specifier from an import statement.
|
|
41
|
+
* `import type { X } from "./stores"` → `"./stores"`.
|
|
42
|
+
*/
|
|
43
|
+
const extractImportPath = (imp) => {
|
|
44
|
+
const m = imp.match(/from\s+["']([^"']+)["']/);
|
|
45
|
+
return m ? (m[1] ?? null) : null;
|
|
46
|
+
};
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
// Store-surface resolution
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
/**
|
|
51
|
+
* Resolve the TypeScript type to use as the generic for useReactraStore<T>.
|
|
52
|
+
* Three cases (per §3, open question 5):
|
|
53
|
+
* 1. Same-file store container → <Name>Surface (emitted by emitStore in this file)
|
|
54
|
+
* 2. Imported store (import type { name } from "path") → <Name>Surface
|
|
55
|
+
* (caller must emit import type { <Name>Surface } from "path")
|
|
56
|
+
* 3. No binding → "unknown"
|
|
57
|
+
*/
|
|
58
|
+
const resolveStoreSurface = (storeName, graph) => {
|
|
59
|
+
// Case 1: same-file store container.
|
|
60
|
+
const isLocal = graph.containers.some((c) => (c.kind === "route-store" || c.kind === "session-store" || c.kind === "export-store") &&
|
|
61
|
+
c.name === storeName);
|
|
62
|
+
if (isLocal)
|
|
63
|
+
return { surface: surfaceTypeName(storeName), importPath: null };
|
|
64
|
+
// Case 2: imported (value or type import).
|
|
65
|
+
for (const imp of graph.userImports) {
|
|
66
|
+
if (importsName(imp, storeName)) {
|
|
67
|
+
const path = extractImportPath(imp);
|
|
68
|
+
return { surface: surfaceTypeName(storeName), importPath: path };
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
// Case 3: fallback.
|
|
72
|
+
return { surface: "unknown", importPath: null };
|
|
73
|
+
};
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
// Service-surface resolution (symmetric partner to resolveStoreSurface)
|
|
76
|
+
// ---------------------------------------------------------------------------
|
|
77
|
+
/**
|
|
78
|
+
* Resolve the TypeScript type for useService<T>.
|
|
79
|
+
* Same three-case logic as resolveStoreSurface:
|
|
80
|
+
* 1. Same-file service container → <Name>Surface
|
|
81
|
+
* 2. Imported service → <Name>Surface + importPath
|
|
82
|
+
* 3. No binding → "unknown"
|
|
83
|
+
*/
|
|
84
|
+
const resolveServiceSurface = (serviceName, graph) => {
|
|
85
|
+
// Case 1: same-file service container.
|
|
86
|
+
if (graph.containers.some((c) => c.kind === "service" && c.name === serviceName)) {
|
|
87
|
+
return { surface: surfaceTypeName(serviceName), importPath: null };
|
|
88
|
+
}
|
|
89
|
+
// Case 2: imported (value or type import).
|
|
90
|
+
for (const imp of graph.userImports) {
|
|
91
|
+
if (importsName(imp, serviceName)) {
|
|
92
|
+
const path = extractImportPath(imp);
|
|
93
|
+
return { surface: surfaceTypeName(serviceName), importPath: path };
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
// Case 3: fallback.
|
|
97
|
+
return { surface: "unknown", importPath: null };
|
|
98
|
+
};
|
|
99
|
+
// ---------------------------------------------------------------------------
|
|
100
|
+
// Pre-pass: collect Surface imports needed for the preamble
|
|
101
|
+
// ---------------------------------------------------------------------------
|
|
102
|
+
/**
|
|
103
|
+
* Scan all storeUses and service injects across all containers and collect any
|
|
104
|
+
* Surface type imports that are not same-file.
|
|
105
|
+
* Returns entries: `import type { XSurface } from "path"`.
|
|
106
|
+
*/
|
|
107
|
+
const collectSurfaceImports = (graph) => {
|
|
108
|
+
const seen = new Set();
|
|
109
|
+
const result = [];
|
|
110
|
+
const record = (surface, importPath) => {
|
|
111
|
+
if (!importPath || surface === "unknown")
|
|
112
|
+
return;
|
|
113
|
+
const key = `${importPath}::${surface}`;
|
|
114
|
+
if (seen.has(key))
|
|
115
|
+
return;
|
|
116
|
+
seen.add(key);
|
|
117
|
+
result.push(`import type { ${surface} } from "${importPath}"`);
|
|
118
|
+
};
|
|
119
|
+
for (const c of graph.containers) {
|
|
120
|
+
for (const su of c.storeUses) {
|
|
121
|
+
if (su.typeOverride)
|
|
122
|
+
continue;
|
|
123
|
+
const { surface, importPath } = resolveStoreSurface(su.storeName, graph);
|
|
124
|
+
record(surface, importPath);
|
|
125
|
+
}
|
|
126
|
+
for (const inj of c.injects) {
|
|
127
|
+
if (inj.serviceKind !== "service")
|
|
128
|
+
continue;
|
|
129
|
+
const { surface, importPath } = resolveServiceSurface(inj.name, graph);
|
|
130
|
+
record(surface, importPath);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return result;
|
|
134
|
+
};
|
|
135
|
+
// ---------------------------------------------------------------------------
|
|
136
|
+
// Main entry point
|
|
137
|
+
// ---------------------------------------------------------------------------
|
|
138
|
+
/**
|
|
139
|
+
* Emit a TypeScript shadow file for the given FileGraph.
|
|
140
|
+
*
|
|
141
|
+
* @param graph - result of compile(source).graph
|
|
142
|
+
* @param source - the original DSL source string (for slice offsets)
|
|
143
|
+
* @param rewrites - compile(source).rewrites — used to compose preprocessed→DSL offsets.
|
|
144
|
+
* Pass an empty array when source-map fidelity is not required.
|
|
145
|
+
* @returns shadow text + position mappings
|
|
146
|
+
*/
|
|
147
|
+
export const emitShadow = (graph, source, rewrites = []) => {
|
|
148
|
+
const lines = [];
|
|
149
|
+
const mappings = [];
|
|
150
|
+
// Track current output offset for mapping generation.
|
|
151
|
+
let offset = 0;
|
|
152
|
+
// emit: pushes a line, advances offset by (text.length + 1) for the \n join.
|
|
153
|
+
// Returns the generated offset at which this line STARTS.
|
|
154
|
+
const emit = (text) => {
|
|
155
|
+
const startOffset = offset;
|
|
156
|
+
lines.push(text);
|
|
157
|
+
offset += text.length + 1; // +1 for the \n join
|
|
158
|
+
return startOffset;
|
|
159
|
+
};
|
|
160
|
+
/**
|
|
161
|
+
* Record source mappings from a verbatim-sliced span in graph.preprocessed
|
|
162
|
+
* back to the DSL source. `genStart` is the byte offset in the shadow text
|
|
163
|
+
* where the span begins; `ppStart`/`ppEnd` are the preprocessed offsets
|
|
164
|
+
* (from Babel AST .start/.end).
|
|
165
|
+
*
|
|
166
|
+
* Emits one mapping per exact preprocessed↔DSL run (ppSpanToDslRuns):
|
|
167
|
+
* Volar interpolates positions linearly inside a mapping, so a
|
|
168
|
+
* completion-grade mapping must cover text that is IDENTICAL in source and
|
|
169
|
+
* shadow. The emitter writes `preprocessed.slice(ppStart, ppEnd)` verbatim
|
|
170
|
+
* at genStart, so generated≡preprocessed by construction; the runs restrict
|
|
171
|
+
* that to where preprocessed≡DSL too. When no exact run exists (fully
|
|
172
|
+
* synthesized text), falls back to a single anchor mapping at the span
|
|
173
|
+
* start — hover/diagnostic-grade, like the pre-fix behavior.
|
|
174
|
+
*/
|
|
175
|
+
const record = (genStart, ppStart, ppEnd) => {
|
|
176
|
+
if (ppStart == null || ppEnd == null || ppEnd <= ppStart)
|
|
177
|
+
return;
|
|
178
|
+
let exact = false;
|
|
179
|
+
for (const run of ppSpanToDslRuns(ppStart, ppEnd, rewrites, source)) {
|
|
180
|
+
// Guard the suffix-alignment heuristic: only verbatim runs ship.
|
|
181
|
+
const dslText = source.slice(run.dslOffset, run.dslOffset + run.length);
|
|
182
|
+
const ppText = graph.preprocessed.slice(run.ppOffset, run.ppOffset + run.length);
|
|
183
|
+
if (dslText !== ppText)
|
|
184
|
+
continue;
|
|
185
|
+
mappings.push({
|
|
186
|
+
generatedOffset: genStart + (run.ppOffset - ppStart),
|
|
187
|
+
sourceOffset: run.dslOffset,
|
|
188
|
+
length: run.length,
|
|
189
|
+
});
|
|
190
|
+
exact = true;
|
|
191
|
+
}
|
|
192
|
+
if (!exact) {
|
|
193
|
+
const dslOffset = preprocessedToDslOffset(ppStart, rewrites);
|
|
194
|
+
if (dslOffset === null)
|
|
195
|
+
return;
|
|
196
|
+
mappings.push({ generatedOffset: genStart, sourceOffset: dslOffset, length: ppEnd - ppStart });
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
const hasPageComponent = graph.containers.some((c) => c.kind === "component" && (c.params.length > 0 || c.queries.length > 0));
|
|
200
|
+
// Preamble — per spec §3 "Preamble rules".
|
|
201
|
+
for (const imp of graph.userImports)
|
|
202
|
+
emit(imp);
|
|
203
|
+
for (const t of graph.userTypes)
|
|
204
|
+
emit(t);
|
|
205
|
+
emit(`import type { Dispatch, SetStateAction } from "react"`);
|
|
206
|
+
// Real package boundaries (Runtime spec §symbol map): useReactraStore lives in
|
|
207
|
+
// @reactra/store, useService in @reactra/service, ResourceHandle in
|
|
208
|
+
// @reactra/resource. The shadow must import what actually resolves in a
|
|
209
|
+
// consumer app — a phantom import makes every binding silently `any`
|
|
210
|
+
// (hover "any", dead completions) while stub-backed tests stay green.
|
|
211
|
+
emit(`import { useReactraStore } from "@reactra/store"`);
|
|
212
|
+
emit(`import { useService } from "@reactra/service"`);
|
|
213
|
+
if (graph.containers.some((c) => c.resources.length > 0)) {
|
|
214
|
+
emit(`import { type ResourceHandle } from "@reactra/resource"`);
|
|
215
|
+
}
|
|
216
|
+
if (hasPageComponent) {
|
|
217
|
+
emit(`import { useRoute, coerceQuery } from "@reactra/router"`);
|
|
218
|
+
}
|
|
219
|
+
// Emit any Surface type imports needed for cross-file inject store consumers.
|
|
220
|
+
for (const surfaceImp of collectSurfaceImports(graph))
|
|
221
|
+
emit(surfaceImp);
|
|
222
|
+
for (const container of graph.containers) {
|
|
223
|
+
if (container.kind === "component") {
|
|
224
|
+
emitComponent(container, graph, emit, record);
|
|
225
|
+
}
|
|
226
|
+
else if (container.kind === "route-store" ||
|
|
227
|
+
container.kind === "session-store" ||
|
|
228
|
+
container.kind === "export-store") {
|
|
229
|
+
emitStore(container, graph, emit, record);
|
|
230
|
+
}
|
|
231
|
+
else if (container.kind === "service") {
|
|
232
|
+
emitService(container, graph, emit);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
// Mirror the compiler's auto-default-export rule (Pass 9 Day-21 #18): when
|
|
236
|
+
// exactly one component is exported, the compiled output emits `export default X`.
|
|
237
|
+
// The route manifest types `import("./pages/…")` as `Promise<{ default: ComponentType }>`,
|
|
238
|
+
// so the shadow must match or the generated-file check fires a TS2322.
|
|
239
|
+
const exportedComponents = graph.containers.filter((c) => c.kind === "component" && c.exported);
|
|
240
|
+
if (exportedComponents.length === 1) {
|
|
241
|
+
emit(`\nexport default ${exportedComponents[0].name}`);
|
|
242
|
+
}
|
|
243
|
+
return { text: lines.join("\n"), mappings };
|
|
244
|
+
};
|
|
245
|
+
// ---------------------------------------------------------------------------
|
|
246
|
+
// Component lowering (§3.1)
|
|
247
|
+
// ---------------------------------------------------------------------------
|
|
248
|
+
const emitComponent = (c, graph, emit, record) => {
|
|
249
|
+
const isPage = c.params.length > 0 || c.queries.length > 0;
|
|
250
|
+
const decl = c.exported ? "export const" : "const";
|
|
251
|
+
if (isPage) {
|
|
252
|
+
emit(`${decl} ${c.name} = () => {`);
|
|
253
|
+
emit(` const __route = useRoute()`);
|
|
254
|
+
for (const p of c.params)
|
|
255
|
+
emitParam(p, graph, emit);
|
|
256
|
+
for (const q of c.queries)
|
|
257
|
+
emitQuery(q, graph, emit);
|
|
258
|
+
}
|
|
259
|
+
else {
|
|
260
|
+
emit(`${decl} ${c.name} = (${c.propsParam ?? ""}) => {`);
|
|
261
|
+
}
|
|
262
|
+
// §B1 C1: classify setters ONCE per component (gated: only renames for a
|
|
263
|
+
// non-trivial `action set<Cap(X)>` targeting an existing `state X`).
|
|
264
|
+
const { renamedStates } = classifyStateSetters(c);
|
|
265
|
+
for (const su of c.storeUses)
|
|
266
|
+
emitStoreUse(su, graph, emit);
|
|
267
|
+
for (const inj of c.injects)
|
|
268
|
+
emitInject(inj, graph, emit);
|
|
269
|
+
for (const s of c.states)
|
|
270
|
+
emitState(s, graph, emit, record, renamedStates);
|
|
271
|
+
// `uses` bindings declared before derived/action/view so they're in scope for all of them.
|
|
272
|
+
if (c.uses)
|
|
273
|
+
emitUses(c.uses, c.name, emit);
|
|
274
|
+
for (const d of c.deriveds)
|
|
275
|
+
emitDerived(d, graph, emit, record);
|
|
276
|
+
for (const a of c.actions)
|
|
277
|
+
emitAction(a, graph, emit, record);
|
|
278
|
+
for (const r of c.resources)
|
|
279
|
+
emitResource(r, graph, emit, record);
|
|
280
|
+
for (const cmd of c.commands)
|
|
281
|
+
emitCommand(cmd, graph, emit, record);
|
|
282
|
+
for (const m of c.mounts)
|
|
283
|
+
emitFlatBody(m.body, graph, emit);
|
|
284
|
+
for (const e of c.effects)
|
|
285
|
+
emitFlatBody(e.body, graph, emit);
|
|
286
|
+
if (c.meta)
|
|
287
|
+
emitMeta(c.meta, graph, emit);
|
|
288
|
+
if (c.errorBoundary)
|
|
289
|
+
emitErrorBoundary(c.errorBoundary, graph, emit);
|
|
290
|
+
// Suspense: emit body JSX as a void expression so it type-checks independently.
|
|
291
|
+
if (c.suspense) {
|
|
292
|
+
const sb = c.suspense.body.body;
|
|
293
|
+
if (sb.start != null && sb.end != null) {
|
|
294
|
+
const bodyText = graph.preprocessed.slice(sb.start, sb.end);
|
|
295
|
+
emit(` void (<>{(${bodyText})}</>)`);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
if (c.view) {
|
|
299
|
+
emitView(c.view, graph, emit, record);
|
|
300
|
+
}
|
|
301
|
+
else {
|
|
302
|
+
emit(` return null as never`);
|
|
303
|
+
}
|
|
304
|
+
emit(`}`);
|
|
305
|
+
};
|
|
306
|
+
// ---------------------------------------------------------------------------
|
|
307
|
+
// Store lowering (§3.2)
|
|
308
|
+
// ---------------------------------------------------------------------------
|
|
309
|
+
const emitStore = (c, graph, emit, record) => {
|
|
310
|
+
// Anti-drift: the compiler FORCE-exports every store regardless of the source
|
|
311
|
+
// `export` keyword (pass-9-codegen.ts — "Force-exported regardless of c.exported").
|
|
312
|
+
// The shadow must match, or a non-`export`ed store reads as module-private here while
|
|
313
|
+
// the build ships it exported (the D4 divergence the F-PARITY gate now guards).
|
|
314
|
+
const decl = "export";
|
|
315
|
+
const inputsTypeName = `${toPascalCase(c.name)}Inputs`;
|
|
316
|
+
const surfType = surfaceTypeName(c.name);
|
|
317
|
+
// Inputs type — each InputNode contributes a field typed from its declared
|
|
318
|
+
// annotation (Run 2 / D3 — `tsType` is now threaded through the preprocessor and
|
|
319
|
+
// AST). per §3.2: "input X: T -> const X = __inputs.X follows as the first body
|
|
320
|
+
// statement". Falls back to `unknown` only when the source omits the annotation.
|
|
321
|
+
if (c.inputs.length > 0) {
|
|
322
|
+
const fields = c.inputs
|
|
323
|
+
.map((inp) => {
|
|
324
|
+
const opt = inp.requirement === "optional" ? "?" : "";
|
|
325
|
+
return `${inp.name}${opt}: ${inp.tsType ?? "unknown"}`;
|
|
326
|
+
})
|
|
327
|
+
.join("; ");
|
|
328
|
+
emit(`${decl ? decl + " " : ""}type ${inputsTypeName} = { ${fields} }`);
|
|
329
|
+
emit(`${decl ? decl + " " : ""}function ${c.name}(__inputs: ${inputsTypeName}) {`);
|
|
330
|
+
// Bind each input as a const in the function body.
|
|
331
|
+
for (const inp of c.inputs)
|
|
332
|
+
emitInput(inp, emit);
|
|
333
|
+
}
|
|
334
|
+
else {
|
|
335
|
+
emit(`${decl ? decl + " " : ""}function ${c.name}() {`);
|
|
336
|
+
}
|
|
337
|
+
// §B1 C1: stores never rename setters — `classifyStateSetters` returns an
|
|
338
|
+
// empty `renamedStates` for non-component containers, so a store `state X`
|
|
339
|
+
// always emits `setX` (matches Pass 9, which renames in components only).
|
|
340
|
+
const { renamedStates } = classifyStateSetters(c);
|
|
341
|
+
for (const s of c.states)
|
|
342
|
+
emitState(s, graph, emit, record, renamedStates);
|
|
343
|
+
for (const d of c.deriveds)
|
|
344
|
+
emitDerived(d, graph, emit, record);
|
|
345
|
+
for (const a of c.actions)
|
|
346
|
+
emitAction(a, graph, emit, record);
|
|
347
|
+
for (const r of c.resources)
|
|
348
|
+
emitResource(r, graph, emit, record);
|
|
349
|
+
for (const e of c.effects)
|
|
350
|
+
emitFlatBody(e.body, graph, emit);
|
|
351
|
+
// Return all state/derived/action/resource names.
|
|
352
|
+
const returnNames = [
|
|
353
|
+
...c.states.map((s) => s.name),
|
|
354
|
+
...c.deriveds.map((d) => d.name),
|
|
355
|
+
...c.actions.map((a) => a.name),
|
|
356
|
+
...c.resources.map((r) => r.name),
|
|
357
|
+
];
|
|
358
|
+
emit(` return { ${returnNames.join(", ")} }`);
|
|
359
|
+
emit(`}`);
|
|
360
|
+
// Surface type alias — ReturnType<typeof storeFn> is the consumable surface.
|
|
361
|
+
emit(`${decl ? decl + " " : ""}type ${surfType} = ReturnType<typeof ${c.name}>`);
|
|
362
|
+
};
|
|
363
|
+
// ---------------------------------------------------------------------------
|
|
364
|
+
// Service lowering (§3.3)
|
|
365
|
+
// ---------------------------------------------------------------------------
|
|
366
|
+
const emitService = (c, graph, emit) => {
|
|
367
|
+
// Anti-drift: the compiler force-exports every service regardless of the source
|
|
368
|
+
// `export` keyword (pass-9-codegen.ts). Match it (see the store note above — D4).
|
|
369
|
+
const decl = "export";
|
|
370
|
+
const surfType = surfaceTypeName(c.name);
|
|
371
|
+
const noop = () => { };
|
|
372
|
+
emit(`${decl ? decl + " " : ""}function ${c.name}() {`);
|
|
373
|
+
for (const a of c.actions)
|
|
374
|
+
emitAction(a, graph, emit, noop);
|
|
375
|
+
const returnNames = c.actions.map((a) => a.name);
|
|
376
|
+
emit(` return { ${returnNames.join(", ")} }`);
|
|
377
|
+
emit(`}`);
|
|
378
|
+
emit(`${decl ? decl + " " : ""}type ${surfType} = ReturnType<typeof ${c.name}>`);
|
|
379
|
+
};
|
|
380
|
+
// ---------------------------------------------------------------------------
|
|
381
|
+
// Per-construct lowering helpers
|
|
382
|
+
// ---------------------------------------------------------------------------
|
|
383
|
+
const emitParam = (p, _graph, emit) => {
|
|
384
|
+
// Route params are always `string` at runtime (`__route.params` is
|
|
385
|
+
// `Record<string, string>`; Pass 9 emits no coercion). The shadow projects
|
|
386
|
+
// `string` unconditionally — typing a `param count: number` as `number` would be
|
|
387
|
+
// a runtime-contradicting lie (the value is the URL string `"42"`). A non-string
|
|
388
|
+
// annotation therefore surfaces honestly as a type error on number/array ops.
|
|
389
|
+
emit(` const ${p.name} = __route.params.${p.name}`);
|
|
390
|
+
};
|
|
391
|
+
const emitQuery = (q, _graph, emit) => {
|
|
392
|
+
const tsType = q.tsType ?? "string";
|
|
393
|
+
// coerceQuery returns unknown at runtime; cast to the declared type (D2: array/union
|
|
394
|
+
// types would otherwise land as unknown). A query with no default can be absent from
|
|
395
|
+
// the URL, so it widens to `| undefined` (matching the router); a defaulted query never is.
|
|
396
|
+
const cast = q.defaultText !== undefined ? tsType : `${tsType} | undefined`;
|
|
397
|
+
emit(` const ${q.name} = coerceQuery(__route.query.${q.name}, ${JSON.stringify(tsType)}) as ${cast}`);
|
|
398
|
+
};
|
|
399
|
+
const emitInput = (inp, emit) => {
|
|
400
|
+
emit(` const ${inp.name} = __inputs.${inp.name}`);
|
|
401
|
+
};
|
|
402
|
+
const emitStoreUse = (su, graph, emit) => {
|
|
403
|
+
// typeOverride takes precedence over resolved surface (per spec §3.1 inject-store table).
|
|
404
|
+
const surface = su.typeOverride ?? resolveStoreSurface(su.storeName, graph).surface;
|
|
405
|
+
if (su.fields && su.fields.length > 0) {
|
|
406
|
+
const destructure = su.fields
|
|
407
|
+
.map((f) => (typeof f === "string" ? f : `${f.source}: ${f.local}`))
|
|
408
|
+
.join(", ");
|
|
409
|
+
emit(` const { ${destructure} } = useReactraStore<${surface}>("${su.storeName}")`);
|
|
410
|
+
}
|
|
411
|
+
else {
|
|
412
|
+
emit(` const ${su.alias ?? su.storeName} = useReactraStore<${surface}>("${su.storeName}")`);
|
|
413
|
+
}
|
|
414
|
+
};
|
|
415
|
+
const emitInject = (inj, graph, emit) => {
|
|
416
|
+
if (inj.serviceKind !== "service")
|
|
417
|
+
return;
|
|
418
|
+
const binding = inj.alias ?? inj.name;
|
|
419
|
+
const { surface } = resolveServiceSurface(inj.name, graph);
|
|
420
|
+
emit(` const ${binding} = useService<${surface}>("${inj.name}")`);
|
|
421
|
+
};
|
|
422
|
+
const emitState = (s, graph, emit, record, renamedStates) => {
|
|
423
|
+
const init = s.initializer;
|
|
424
|
+
const ppStart = init.start;
|
|
425
|
+
const ppEnd = init.end;
|
|
426
|
+
const initText = s.initializerText;
|
|
427
|
+
// Emit the let binding; record a mapping from the initializer span.
|
|
428
|
+
const lineText = ` let ${s.name} = ${initText}`;
|
|
429
|
+
// The initializer text appears after " let <name> = " in the emitted line.
|
|
430
|
+
const prefixLen = ` let ${s.name} = `.length;
|
|
431
|
+
const lineStart = emit(lineText);
|
|
432
|
+
if (ppStart != null && ppEnd != null) {
|
|
433
|
+
record(lineStart + prefixLen, ppStart, ppEnd);
|
|
434
|
+
}
|
|
435
|
+
// §B1 C1: shared GATED setter naming — `renamedStates` is computed once per
|
|
436
|
+
// container by the caller via `classifyStateSetters` (the SAME derivation
|
|
437
|
+
// Pass 9 uses). The old shadow-local `resolveSetterName` matched on a bare
|
|
438
|
+
// action name across all containers and could rename when Pass 9 wouldn't.
|
|
439
|
+
const setterName = setterNameFor(s.name, renamedStates);
|
|
440
|
+
emit(` const ${setterName}: Dispatch<SetStateAction<typeof ${s.name}>> = null as never`);
|
|
441
|
+
};
|
|
442
|
+
const emitDerived = (d, graph, emit, record) => {
|
|
443
|
+
const thunk = d.thunk;
|
|
444
|
+
const body = thunk.body;
|
|
445
|
+
const preprocessed = graph.preprocessed;
|
|
446
|
+
const start = body.start;
|
|
447
|
+
const end = body.end;
|
|
448
|
+
if (start == null || end == null) {
|
|
449
|
+
emit(` // [reactra2tsx] unhandled: derived ${d.name} — missing offsets`);
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
const bodyText = preprocessed.slice(start, end);
|
|
453
|
+
const inner = stripOuterParens(bodyText.trim());
|
|
454
|
+
// Both bare and IIFE forms emit as-is (IIFE already is the IIFE; bare is the expression).
|
|
455
|
+
const prefixLen = ` const ${d.name} = `.length;
|
|
456
|
+
const lineStart = emit(` const ${d.name} = ${inner}`);
|
|
457
|
+
// Map the derived body back to the preprocessed span (trimmed inner may differ — map raw body).
|
|
458
|
+
record(lineStart + prefixLen, start, end);
|
|
459
|
+
};
|
|
460
|
+
/**
|
|
461
|
+
* True when `node` is a pure entity name — an Identifier or a dotted member chain
|
|
462
|
+
* with no call or computed access anywhere. Only such a name is valid inside `typeof`.
|
|
463
|
+
*/
|
|
464
|
+
const isEntityName = (node) => {
|
|
465
|
+
if (node.type === "Identifier")
|
|
466
|
+
return true;
|
|
467
|
+
if (node.type === "MemberExpression" && node.computed !== true && node.object != null) {
|
|
468
|
+
return isEntityName(node.object);
|
|
469
|
+
}
|
|
470
|
+
return false;
|
|
471
|
+
};
|
|
472
|
+
const emitResource = (r, graph, emit, record) => {
|
|
473
|
+
// The raw fetched type: `Awaited<ReturnType<typeof <callee>>>` for an entity
|
|
474
|
+
// fetcher, else `unknown`. `rawCallee` carries the mappable callee text/range.
|
|
475
|
+
let rawType = "unknown";
|
|
476
|
+
let calleeRange = null;
|
|
477
|
+
const body = r.fetcher.body;
|
|
478
|
+
if (body.type === "CallExpression") {
|
|
479
|
+
const callee = body.callee;
|
|
480
|
+
if (isEntityName(callee) && callee.start != null && callee.end != null) {
|
|
481
|
+
const calleeText = graph.preprocessed.slice(callee.start, callee.end);
|
|
482
|
+
rawType = `Awaited<ReturnType<typeof ${calleeText}>>`;
|
|
483
|
+
calleeRange = { start: callee.start, end: callee.end };
|
|
484
|
+
}
|
|
485
|
+
else {
|
|
486
|
+
emit(` // resource type unknown — complex fetcher`);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
else {
|
|
490
|
+
emit(` // resource type unknown — complex fetcher`);
|
|
491
|
+
}
|
|
492
|
+
if (r.select === undefined) {
|
|
493
|
+
emitResourceDecl(r.name, rawType, "", calleeRange, emit, record);
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
// `select(u => u.name)` projects `.data` to the selector's return (the type
|
|
497
|
+
// parameter `S`). A `typeof` type query can't read an inline arrow expression,
|
|
498
|
+
// so we bind the SAME selector source Pass 9 emits to a named helper (no drift —
|
|
499
|
+
// B1) and `ReturnType<typeof helper>` it. The helper's param is the raw fetched
|
|
500
|
+
// type, so an UNannotated selector (`u => u.name`) still infers `S` correctly;
|
|
501
|
+
// for a complex fetcher the raw type is `any` so the selector body doesn't error.
|
|
502
|
+
const selParam = rawType === "unknown" ? "any" : rawType;
|
|
503
|
+
emit(` const __sel_${r.name} = (__d: ${selParam}) => (${r.select})(__d)`);
|
|
504
|
+
emitResourceDecl(r.name, rawType, `, ReturnType<typeof __sel_${r.name}>`, calleeRange, emit, record);
|
|
505
|
+
};
|
|
506
|
+
/** Emit the `const <name>: ResourceHandle<<raw><selectArg>> = null as never` line + map the callee. */
|
|
507
|
+
const emitResourceDecl = (name, rawType, selectArg, calleeRange, emit, record) => {
|
|
508
|
+
const prefix = ` const ${name}: ResourceHandle<`;
|
|
509
|
+
const lineStart = emit(`${prefix}${rawType}${selectArg}> = null as never`);
|
|
510
|
+
// The callee sits inside `Awaited<ReturnType<typeof <callee>>>` — offset past that wrapper.
|
|
511
|
+
if (calleeRange)
|
|
512
|
+
record(lineStart + prefix.length + "Awaited<ReturnType<typeof ".length, calleeRange.start, calleeRange.end);
|
|
513
|
+
};
|
|
514
|
+
const emitAction = (a, graph, emit, record) => {
|
|
515
|
+
const preprocessed = graph.preprocessed;
|
|
516
|
+
const arrow = a.arrow;
|
|
517
|
+
const paramsText = arrow.params
|
|
518
|
+
.map((p) => {
|
|
519
|
+
if (p.start == null || p.end == null)
|
|
520
|
+
return "";
|
|
521
|
+
return preprocessed.slice(p.start, p.end);
|
|
522
|
+
})
|
|
523
|
+
.join(", ");
|
|
524
|
+
const bodyText = arrow.body.start != null && arrow.body.end != null
|
|
525
|
+
? preprocessed.slice(arrow.body.start, arrow.body.end)
|
|
526
|
+
: "{ }";
|
|
527
|
+
const asyncPrefix = a.isAsync ? "async " : "";
|
|
528
|
+
// No return annotation — TS infers from the sliced body, matching Pass-9 (which annotates nothing).
|
|
529
|
+
// Component `action async` is transition-wrapped by Pass-9 so its runtime return is void; the shadow
|
|
530
|
+
// models the raw body — a deliberate simplification.
|
|
531
|
+
const prefix = ` const ${a.name} = ${asyncPrefix}(${paramsText}) => `;
|
|
532
|
+
const lineStart = emit(`${prefix}${bodyText}`);
|
|
533
|
+
// Map the action body (the part the user wrote) back to its preprocessed origin.
|
|
534
|
+
if (arrow.body.start != null && arrow.body.end != null) {
|
|
535
|
+
record(lineStart + prefix.length, arrow.body.start, arrow.body.end);
|
|
536
|
+
}
|
|
537
|
+
};
|
|
538
|
+
/**
|
|
539
|
+
* Emit the shadow for a `command` (the write primitive). The handle surface must
|
|
540
|
+
* type-check in the view:
|
|
541
|
+
* - block form → `f.pending` / `f.result` / `f.error` (mirrors useActionState).
|
|
542
|
+
* The body is emitted as a typed reducer so `f.result` infers its return type
|
|
543
|
+
* AND the body itself type-checks (states/imports in scope).
|
|
544
|
+
* - arrow form → `f.pending` only (mirrors useTransition). The fetcher is
|
|
545
|
+
* type-checked via a `void` expression.
|
|
546
|
+
* The optimistic / rollback clause bodies are emitted as `void` arrows so their
|
|
547
|
+
* state writes / error param type-check too. The optimistic-tracked states need
|
|
548
|
+
* no special shadow: they're ordinary `let` bindings (the view reads them as the
|
|
549
|
+
* mirror's element type, which equals the base type).
|
|
550
|
+
*/
|
|
551
|
+
const emitCommand = (cmd, graph, emit, record) => {
|
|
552
|
+
const pp = graph.preprocessed;
|
|
553
|
+
const arrow = cmd.arrow;
|
|
554
|
+
const paramsText = arrow.params
|
|
555
|
+
.map((p) => (p.start != null && p.end != null ? pp.slice(p.start, p.end) : ""))
|
|
556
|
+
.join(", ");
|
|
557
|
+
if (cmd.form === "block") {
|
|
558
|
+
const bodyText = arrow.body.start != null && arrow.body.end != null ? pp.slice(arrow.body.start, arrow.body.end) : "{ }";
|
|
559
|
+
const prefix = ` const __cmd_${cmd.name} = async (${paramsText}) => `;
|
|
560
|
+
const lineStart = emit(`${prefix}${bodyText}`);
|
|
561
|
+
if (arrow.body.start != null && arrow.body.end != null) {
|
|
562
|
+
record(lineStart + prefix.length, arrow.body.start, arrow.body.end);
|
|
563
|
+
}
|
|
564
|
+
emit(` const ${cmd.name}: ((arg?: unknown) => void) & ` +
|
|
565
|
+
`{ pending: boolean; result: Awaited<ReturnType<typeof __cmd_${cmd.name}>> | undefined; error: string | undefined } = null as never`);
|
|
566
|
+
}
|
|
567
|
+
else {
|
|
568
|
+
// Arrow form — type-check the fetcher; expose only `.pending`.
|
|
569
|
+
const fetcherText = arrow.body.start != null && arrow.body.end != null ? pp.slice(arrow.body.start, arrow.body.end) : "undefined";
|
|
570
|
+
const prefix = ` void (async (${paramsText}) => (`;
|
|
571
|
+
const lineStart = emit(`${prefix}${fetcherText}))`);
|
|
572
|
+
if (arrow.body.start != null && arrow.body.end != null) {
|
|
573
|
+
record(lineStart + prefix.length, arrow.body.start, arrow.body.end);
|
|
574
|
+
}
|
|
575
|
+
emit(` const ${cmd.name}: ((arg?: unknown) => void) & { pending: boolean } = null as never`);
|
|
576
|
+
}
|
|
577
|
+
// Type-check the optional clause bodies (state writes / error param).
|
|
578
|
+
if (cmd.optimistic?.start != null && cmd.optimistic.end != null) {
|
|
579
|
+
const prefix = ` void (`;
|
|
580
|
+
const lineStart = emit(`${prefix}${pp.slice(cmd.optimistic.start, cmd.optimistic.end)})`);
|
|
581
|
+
record(lineStart + prefix.length, cmd.optimistic.start, cmd.optimistic.end);
|
|
582
|
+
}
|
|
583
|
+
// The rollback error param is `unknown` at runtime (it comes from a `catch`), so the
|
|
584
|
+
// shadow forces `: unknown` rather than slicing the bare `(e)` verbatim — otherwise
|
|
585
|
+
// a `rollback(e)` would be a false TS7006 (implicit any) under strict. Mirrors
|
|
586
|
+
// emitErrorBoundary, which annotates its error param the same way.
|
|
587
|
+
const rb = cmd.rollback;
|
|
588
|
+
if (rb?.body?.start != null && rb.body.end != null) {
|
|
589
|
+
const p0 = rb.params[0];
|
|
590
|
+
const paramDecl = p0?.name ? `${p0.name}: unknown` : "";
|
|
591
|
+
const prefix = ` void ((${paramDecl}) => `;
|
|
592
|
+
const lineStart = emit(`${prefix}${pp.slice(rb.body.start, rb.body.end)})`);
|
|
593
|
+
record(lineStart + prefix.length, rb.body.start, rb.body.end);
|
|
594
|
+
}
|
|
595
|
+
};
|
|
596
|
+
/**
|
|
597
|
+
* Emit the statements of an arrow body as flat top-level statements.
|
|
598
|
+
* Used for mount/effect — teardown `return () => ...` is dropped per §3.1.
|
|
599
|
+
*/
|
|
600
|
+
const emitFlatBody = (arrow, graph, emit) => {
|
|
601
|
+
const preprocessed = graph.preprocessed;
|
|
602
|
+
const arrowBody = arrow.body;
|
|
603
|
+
if (arrowBody.type !== "BlockStatement" || !Array.isArray(arrowBody.body))
|
|
604
|
+
return;
|
|
605
|
+
for (const stmt of arrowBody.body) {
|
|
606
|
+
// Drop `return () => ...` teardown statement.
|
|
607
|
+
if (stmt.type === "ReturnStatement" &&
|
|
608
|
+
stmt.argument != null &&
|
|
609
|
+
stmt.argument.type === "ArrowFunctionExpression") {
|
|
610
|
+
continue;
|
|
611
|
+
}
|
|
612
|
+
if (stmt.start != null && stmt.end != null) {
|
|
613
|
+
emit(` ${preprocessed.slice(stmt.start, stmt.end)}`);
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
};
|
|
617
|
+
/**
|
|
618
|
+
* meta { title: expr } → document.title = expr (per §3.1).
|
|
619
|
+
* document.title is string — a non-string title expr raises TS2322.
|
|
620
|
+
*/
|
|
621
|
+
const emitMeta = (meta, graph, emit) => {
|
|
622
|
+
const preprocessed = graph.preprocessed;
|
|
623
|
+
const obj = meta.object;
|
|
624
|
+
if (obj.type !== "ObjectExpression" || !obj.properties)
|
|
625
|
+
return;
|
|
626
|
+
for (const prop of obj.properties) {
|
|
627
|
+
if (prop.type === "ObjectProperty" && prop.key.type === "Identifier" && prop.key.name === "title") {
|
|
628
|
+
const v = prop.value;
|
|
629
|
+
if (v.start != null && v.end != null) {
|
|
630
|
+
const titleExpr = preprocessed.slice(v.start, v.end);
|
|
631
|
+
emit(` document.title = (${titleExpr}) as string`);
|
|
632
|
+
}
|
|
633
|
+
break;
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
};
|
|
637
|
+
/**
|
|
638
|
+
* errorBoundary(e) { JSX } → IIFE call that type-checks the JSX body (per §3.1).
|
|
639
|
+
* The error param is typed unknown (React ErrorBoundary contract).
|
|
640
|
+
* A prop type mismatch raises TS2345.
|
|
641
|
+
*/
|
|
642
|
+
const emitErrorBoundary = (eb, graph, emit) => {
|
|
643
|
+
const preprocessed = graph.preprocessed;
|
|
644
|
+
const body = eb.body;
|
|
645
|
+
const bodyStart = body.body.start;
|
|
646
|
+
const bodyEnd = body.body.end;
|
|
647
|
+
if (bodyStart == null || bodyEnd == null)
|
|
648
|
+
return;
|
|
649
|
+
const bodyText = preprocessed.slice(bodyStart, bodyEnd);
|
|
650
|
+
emit(` ;((${eb.errorParam}: unknown) => ${bodyText})(undefined)`);
|
|
651
|
+
};
|
|
652
|
+
/** Collect all __reactra_await__ CallExpression nodes via a recursive walk. */
|
|
653
|
+
const collectAwaitCalls = (node) => {
|
|
654
|
+
if (!node || typeof node !== "object")
|
|
655
|
+
return [];
|
|
656
|
+
const results = [];
|
|
657
|
+
if (node.type === "CallExpression" &&
|
|
658
|
+
node.callee?.type === "Identifier" &&
|
|
659
|
+
node.callee?.name === "__reactra_await__") {
|
|
660
|
+
results.push(node);
|
|
661
|
+
// Don't recurse inside — arguments are the arrows we'll slice separately.
|
|
662
|
+
return results;
|
|
663
|
+
}
|
|
664
|
+
// Recurse into known child fields that may contain JSX nodes.
|
|
665
|
+
const childFields = [
|
|
666
|
+
"body", "expression", "children", "openingElement", "closingElement",
|
|
667
|
+
"attributes", "argument", "left", "right", "test", "consequent", "alternate",
|
|
668
|
+
"callee", "arguments", "elements", "properties", "value",
|
|
669
|
+
];
|
|
670
|
+
for (const field of childFields) {
|
|
671
|
+
const child = node[field];
|
|
672
|
+
if (Array.isArray(child)) {
|
|
673
|
+
for (const item of child) {
|
|
674
|
+
if (item && typeof item === "object") {
|
|
675
|
+
results.push(...collectAwaitCalls(item));
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
else if (child && typeof child === "object") {
|
|
680
|
+
results.push(...collectAwaitCalls(child));
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
return results;
|
|
684
|
+
};
|
|
685
|
+
/**
|
|
686
|
+
* Emit the component view as a return statement.
|
|
687
|
+
* Each `__reactra_await__(rArrow, successArrow, pendingArrow, errorArrow)`
|
|
688
|
+
* call embedded in the JSX is replaced with:
|
|
689
|
+
* {(r).status === "success" ? (successBody) : (r).status === "pending" ? (pendingBody) : (errorBody)}
|
|
690
|
+
* so tsc sees the resource field access under the correct union type.
|
|
691
|
+
* The error param is typed `as unknown` to match the runtime contract.
|
|
692
|
+
*/
|
|
693
|
+
const emitView = (view, graph, emit, record) => {
|
|
694
|
+
const preprocessed = graph.preprocessed;
|
|
695
|
+
const viewBodyExpr = view.body.body;
|
|
696
|
+
if (viewBodyExpr.start == null || viewBodyExpr.end == null) {
|
|
697
|
+
emit(` return null as never`);
|
|
698
|
+
return;
|
|
699
|
+
}
|
|
700
|
+
const awaitCalls = collectAwaitCalls(viewBodyExpr);
|
|
701
|
+
if (awaitCalls.length === 0) {
|
|
702
|
+
// No await blocks — emit the view body verbatim and record the span.
|
|
703
|
+
const bodyText = preprocessed.slice(viewBodyExpr.start, viewBodyExpr.end);
|
|
704
|
+
const prefix = ` return `;
|
|
705
|
+
const lineStart = emit(`${prefix}${bodyText}`);
|
|
706
|
+
record(lineStart + prefix.length, viewBodyExpr.start, viewBodyExpr.end);
|
|
707
|
+
return;
|
|
708
|
+
}
|
|
709
|
+
// Build text with await calls replaced by ternaries.
|
|
710
|
+
// Each call occupies [callStart, callEnd] in `preprocessed`. We need to
|
|
711
|
+
// also replace the surrounding JSXExpressionContainer `{...}` if present,
|
|
712
|
+
// since the ternary already includes `{}` delimiters.
|
|
713
|
+
// Strategy: walk left-to-right, replacing each call span with the ternary.
|
|
714
|
+
// We target the CallExpression span directly (not the container) since we
|
|
715
|
+
// need the `{}` in the output.
|
|
716
|
+
let result = "";
|
|
717
|
+
let cursor = viewBodyExpr.start;
|
|
718
|
+
// Every span of `result` that is a verbatim preprocessed slice, recorded at
|
|
719
|
+
// its REAL position in `result` — the substitution changes lengths, so a
|
|
720
|
+
// single whole-body mapping would drift for everything after the first
|
|
721
|
+
// ternary. Each piece becomes its own record() call (→ exact runs), which is
|
|
722
|
+
// what makes hover/completion work INSIDE await blocks.
|
|
723
|
+
const pieces = [];
|
|
724
|
+
const appendSlice = (ppStart, ppEnd) => {
|
|
725
|
+
if (ppEnd > ppStart)
|
|
726
|
+
pieces.push({ resultPos: result.length, ppStart, ppEnd });
|
|
727
|
+
result += preprocessed.slice(ppStart, ppEnd);
|
|
728
|
+
};
|
|
729
|
+
for (const call of awaitCalls) {
|
|
730
|
+
if (call.start == null || call.end == null)
|
|
731
|
+
continue;
|
|
732
|
+
const callStart = call.start;
|
|
733
|
+
const callEnd = call.end;
|
|
734
|
+
if (callStart < cursor)
|
|
735
|
+
continue;
|
|
736
|
+
// Verbatim text before this call.
|
|
737
|
+
appendSlice(cursor, callStart);
|
|
738
|
+
// Extract argument arrows.
|
|
739
|
+
const args = call.arguments ?? [];
|
|
740
|
+
// §B1: index via the shared AWAIT_ARG_ORDER (same constants Pass 9 uses).
|
|
741
|
+
const resourceArrow = args[AWAIT_ARG_ORDER.resource];
|
|
742
|
+
const successArrow = args[AWAIT_ARG_ORDER.success];
|
|
743
|
+
const pendingArrow = args[AWAIT_ARG_ORDER.pending];
|
|
744
|
+
const errorArrow = args[AWAIT_ARG_ORDER.error];
|
|
745
|
+
const rBody = resourceArrow?.body;
|
|
746
|
+
const rRaw = rBody?.start != null && rBody?.end != null
|
|
747
|
+
? preprocessed.slice(rBody.start, rBody.end)
|
|
748
|
+
: null;
|
|
749
|
+
const rText = rRaw !== null ? rRaw.trim() : "undefined";
|
|
750
|
+
// The trim consumed leading whitespace — anchor the mapped span at the
|
|
751
|
+
// first kept char so the slice equals rText exactly.
|
|
752
|
+
const rPpStart = rRaw !== null ? rBody.start + (rRaw.length - rRaw.trimStart().length) : null;
|
|
753
|
+
const appendR = () => {
|
|
754
|
+
if (rPpStart !== null)
|
|
755
|
+
appendSlice(rPpStart, rPpStart + rText.length);
|
|
756
|
+
else
|
|
757
|
+
result += rText;
|
|
758
|
+
};
|
|
759
|
+
const successBody = successArrow?.body;
|
|
760
|
+
const hasPending = pendingArrow != null && pendingArrow.type === "ArrowFunctionExpression";
|
|
761
|
+
const hasError = errorArrow != null && errorArrow.type === "ArrowFunctionExpression";
|
|
762
|
+
const pendingBody = hasPending ? pendingArrow.body : null;
|
|
763
|
+
// Error param from the error arrow's first param (e.g. `e`).
|
|
764
|
+
const errorParam = (() => {
|
|
765
|
+
if (!hasError)
|
|
766
|
+
return "e";
|
|
767
|
+
const params = errorArrow.params;
|
|
768
|
+
const p = params?.[0];
|
|
769
|
+
return p?.type === "Identifier" ? p.name : "e";
|
|
770
|
+
})();
|
|
771
|
+
const errorBody = hasError ? errorArrow.body : null;
|
|
772
|
+
const appendBody = (node, fallback) => {
|
|
773
|
+
if (node?.start != null && node?.end != null) {
|
|
774
|
+
appendSlice(node.start, node.end);
|
|
775
|
+
}
|
|
776
|
+
else {
|
|
777
|
+
result += fallback;
|
|
778
|
+
}
|
|
779
|
+
};
|
|
780
|
+
// Ternary on the REAL ResourceHandle discriminators (it has no `status`
|
|
781
|
+
// field): r.isPending ? pending : r.error ? error : success.
|
|
782
|
+
// The error branch wraps the body in an IIFE with `(param: unknown)` so tsc
|
|
783
|
+
// checks any member access against `unknown` (e.g. `e.message` raises TS18046).
|
|
784
|
+
// Passing `undefined` as the IIFE argument is fine — we only need type-checking,
|
|
785
|
+
// not runtime execution. Pattern mirrors emitErrorBoundary (per §3.1).
|
|
786
|
+
// Built via appendSlice so each user-written block lands in `pieces`.
|
|
787
|
+
result += "(";
|
|
788
|
+
appendR();
|
|
789
|
+
result += ").isPending ? ";
|
|
790
|
+
if (hasPending) {
|
|
791
|
+
appendBody(pendingBody, "<></>");
|
|
792
|
+
}
|
|
793
|
+
else {
|
|
794
|
+
result += "null";
|
|
795
|
+
}
|
|
796
|
+
if (hasError) {
|
|
797
|
+
result += " : (";
|
|
798
|
+
appendR();
|
|
799
|
+
result += `).error ? ((${errorParam}: unknown) => `;
|
|
800
|
+
appendBody(errorBody, "null");
|
|
801
|
+
result += ")(undefined)";
|
|
802
|
+
}
|
|
803
|
+
result += " : ";
|
|
804
|
+
appendBody(successBody, "null");
|
|
805
|
+
cursor = callEnd;
|
|
806
|
+
}
|
|
807
|
+
// Remaining verbatim text after last await call.
|
|
808
|
+
appendSlice(cursor, viewBodyExpr.end);
|
|
809
|
+
// One record per verbatim piece — the substitution changes lengths, so each
|
|
810
|
+
// piece needs its own generated offset for exact (completion-grade) runs.
|
|
811
|
+
const viewPrefix = ` return `;
|
|
812
|
+
const lineStart = emit(`${viewPrefix}${result}`);
|
|
813
|
+
for (const p of pieces) {
|
|
814
|
+
record(lineStart + viewPrefix.length + p.resultPos, p.ppStart, p.ppEnd);
|
|
815
|
+
}
|
|
816
|
+
};
|
|
817
|
+
// ---------------------------------------------------------------------------
|
|
818
|
+
// Behaviour uses lowering (§3.1)
|
|
819
|
+
// ---------------------------------------------------------------------------
|
|
820
|
+
/**
|
|
821
|
+
* Emit a comment for each `uses <behaviour>` declaration.
|
|
822
|
+
* No mixin type contracts are published by @reactra/behaviours in Phase 1,
|
|
823
|
+
* so the shadow degrades gracefully — the comment is a signal to LSP consumers
|
|
824
|
+
* that the keyword was seen.
|
|
825
|
+
*/
|
|
826
|
+
const emitUses = (uses, _componentName, emit) => {
|
|
827
|
+
// D1: a `uses <behaviour>` injects bare names into the body (e.g. `undo`/`redo` from
|
|
828
|
+
// `uses undoable`). Declare each so references type-check instead of TS2304 — using the
|
|
829
|
+
// compiler behaviour registry's exposedBindings/exposedTypes (the single source of truth,
|
|
830
|
+
// shared with codegen). `null as never` is the same value-less binding the kept `setX`
|
|
831
|
+
// escape hatch uses; the type comes from exposedTypes (falls back to `unknown`).
|
|
832
|
+
for (const name of uses.names) {
|
|
833
|
+
const behaviour = NATIVE_BEHAVIOURS.get(name);
|
|
834
|
+
if (!behaviour || behaviour.exposedBindings.length === 0) {
|
|
835
|
+
emit(` // uses ${name} — no injected bindings`);
|
|
836
|
+
continue;
|
|
837
|
+
}
|
|
838
|
+
for (const binding of behaviour.exposedBindings) {
|
|
839
|
+
const t = behaviour.exposedTypes?.[binding] ?? "unknown";
|
|
840
|
+
// `null as unknown as (T)` — a value-less binding that keeps `typeof binding === T`.
|
|
841
|
+
// (`null as never` collapses `typeof` to `never` for primitive/union types like boolean.)
|
|
842
|
+
emit(` const ${binding} = null as unknown as (${t})`);
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
};
|
|
846
|
+
// ---------------------------------------------------------------------------
|
|
847
|
+
// Small utilities
|
|
848
|
+
// ---------------------------------------------------------------------------
|
|
849
|
+
// §B1 C1: `resolveSetterName` deleted. The shadow now shares the GATED setter
|
|
850
|
+
// derivation from `@reactra/babel-plugin/conventions` (`classifyStateSetters` +
|
|
851
|
+
// `setterNameFor`) — the old bare-name-match impl lived here and had drifted
|
|
852
|
+
// from Pass 9 (it renamed on any action named `setX`, ignoring triviality and
|
|
853
|
+
// state-membership; Pass 9 only renames for a NON-TRIVIAL action targeting an
|
|
854
|
+
// existing state). They agreed on the examples by luck.
|
|
855
|
+
/** Strip outer parentheses if the string starts with `(` and ends with `)`. */
|
|
856
|
+
const stripOuterParens = (s) => {
|
|
857
|
+
if (!s.startsWith("(") || !s.endsWith(")"))
|
|
858
|
+
return s;
|
|
859
|
+
let depth = 0;
|
|
860
|
+
for (let i = 0; i < s.length; i++) {
|
|
861
|
+
if (s[i] === "(")
|
|
862
|
+
depth++;
|
|
863
|
+
else if (s[i] === ")") {
|
|
864
|
+
depth--;
|
|
865
|
+
if (depth === 0 && i < s.length - 1)
|
|
866
|
+
return s;
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
return s.slice(1, -1);
|
|
870
|
+
};
|
|
871
|
+
//# sourceMappingURL=emitter.js.map
|