fusee-framework 1.7.0 → 1.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +206 -139
- package/app/benchmark/benchmark.js +2 -2
- package/app/benchmark/vite.config.js +23 -0
- package/app/main.js +24 -3
- package/app/main.ts +7 -1
- package/app/pages/_layout.js +27 -0
- package/app/pages/async-work.js +65 -0
- package/app/pages/index.js +25 -5
- package/app/pages/index.ts +9 -0
- package/app/pages/router-example.js +24 -1
- package/app/pages/scroll-demo.ts +102 -0
- package/app/providers/ChildComponent.js +8 -2
- package/app/providers/ChildComponent.ts +47 -47
- package/app/providers/GrandchildComponent.js +19 -0
- package/app/providers/ParentComponent.js +5 -1
- package/app/routes/RouterExample.js +23 -1
- package/app/routes/TestResource.js +47 -0
- package/app/services.js +15 -0
- package/auto-imports.d.ts +12 -0
- package/framework/auto-import-preset.js +15 -9
- package/framework/core/async-context.js +27 -0
- package/framework/core/compiler/ast.js +167 -0
- package/framework/core/compiler/errors-list.js +47 -0
- package/framework/core/compiler/errors.js +107 -0
- package/framework/core/compiler/generator.js +363 -0
- package/framework/core/compiler/lexer.js +347 -0
- package/framework/core/compiler/main-compiler.js +115 -0
- package/framework/core/compiler/parser.js +414 -0
- package/framework/core/compiler/plugins/actions-plugin.js +263 -0
- package/framework/core/compiler/plugins/compiler-plugin.d.ts +5 -0
- package/framework/core/compiler/plugins/compiler-plugin.js +45 -0
- package/framework/core/compiler/plugins/file-router-plugin.js +301 -0
- package/framework/core/compiler/plugins/router-plugin.js +196 -0
- package/framework/core/compiler/transformer.js +398 -0
- package/framework/core/component.js +61 -23
- package/framework/core/di.js +187 -0
- package/framework/core/directives.js +40 -96
- package/framework/core/evaluator.js +1 -1
- package/framework/core/h.js +430 -0
- package/framework/core/hydration.js +83 -0
- package/framework/core/reconcile.js +151 -0
- package/framework/core/signal.js +269 -0
- package/framework/core/store.js +322 -81
- package/framework/index.js +5 -3
- package/framework/router/file-router.js +1 -1
- package/framework/router/router.js +113 -21
- package/framework/server/actions.js +173 -0
- package/framework/server/actions.server.js +88 -0
- package/framework/server/index.js +18 -0
- package/framework/types/actions.d.ts +50 -0
- package/framework/types/actions.server.d.ts +36 -0
- package/framework/types/compiler/ast.d.ts +166 -0
- package/framework/types/compiler/errors-codes.d.ts +23 -0
- package/framework/types/compiler/errors.d.ts +43 -0
- package/framework/types/compiler/generator.d.ts +8 -0
- package/framework/types/compiler/h.d.ts +46 -0
- package/framework/types/compiler/lexer.d.ts +27 -0
- package/framework/types/compiler/main-compiler.d.ts +60 -0
- package/framework/types/compiler/parser.d.ts +4 -0
- package/framework/types/compiler/plugins/actions.d.ts +30 -0
- package/framework/types/compiler/plugins/compiler-plugin.d.ts +5 -0
- package/framework/types/compiler/plugins/file-router-plugins.d.ts +32 -0
- package/framework/types/compiler/plugins/router-plugin.d.ts +24 -0
- package/framework/types/compiler/transformer.d.ts +28 -0
- package/framework/types/component.d.ts +89 -88
- package/framework/types/di.d.ts +43 -0
- package/framework/types/hydration.d.ts +30 -0
- package/framework/types/index.d.ts +38 -22
- package/framework/types/reconcile.d.ts +7 -0
- package/framework/types/router.d.ts +11 -1
- package/framework/types/server.d.ts +11 -0
- package/framework/types/signal.d.ts +142 -79
- package/framework/types/store.d.ts +66 -8
- package/ghid_go_fusee_integrat_go1_26.txt +276 -0
- package/ghid_rust_fusee_rust_1_95_0.txt +1118 -0
- package/package.json +44 -36
- package/vite.config.js +65 -47
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
import { TokenType } from "./lexer.js";
|
|
2
|
+
import { ErrorCode } from "./errors-list.js";
|
|
3
|
+
import { throwError } from "./errors.js";
|
|
4
|
+
import {
|
|
5
|
+
NodeType,
|
|
6
|
+
createRoot,
|
|
7
|
+
createElement,
|
|
8
|
+
createComponent,
|
|
9
|
+
createText,
|
|
10
|
+
createInterpolation,
|
|
11
|
+
createExpression,
|
|
12
|
+
createAttribute,
|
|
13
|
+
createBinding,
|
|
14
|
+
createEvent,
|
|
15
|
+
createDirective,
|
|
16
|
+
createSlotOutlet,
|
|
17
|
+
createSlotContent,
|
|
18
|
+
createForArg,
|
|
19
|
+
createLoc,
|
|
20
|
+
createPos,
|
|
21
|
+
} from "./ast.js";
|
|
22
|
+
|
|
23
|
+
const VOID_ELEMENTS = new Set([
|
|
24
|
+
"area",
|
|
25
|
+
"base",
|
|
26
|
+
"br",
|
|
27
|
+
"col",
|
|
28
|
+
"embed",
|
|
29
|
+
"hr",
|
|
30
|
+
"img",
|
|
31
|
+
"input",
|
|
32
|
+
"link",
|
|
33
|
+
"meta",
|
|
34
|
+
"param",
|
|
35
|
+
"source",
|
|
36
|
+
"track",
|
|
37
|
+
"wbr",
|
|
38
|
+
]);
|
|
39
|
+
const FOR_RE =
|
|
40
|
+
/^(?:\(\s*([^,)]+?)\s*(?:,\s*([^)]+?)\s*)?\))\s+in\s+(.+)$|^([^\s,]+)\s+in\s+(.+)$/;
|
|
41
|
+
|
|
42
|
+
export function parse(tokens, source, components = new Set()) {
|
|
43
|
+
const parser = new Parser(tokens, source, components);
|
|
44
|
+
return parser.parse();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
class Parser {
|
|
48
|
+
constructor(tokens, source, components) {
|
|
49
|
+
this.tokens = tokens;
|
|
50
|
+
this.source = source;
|
|
51
|
+
this.components = components;
|
|
52
|
+
this.pos = 0;
|
|
53
|
+
this.stack = [];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
parse() {
|
|
57
|
+
const start = this._startLoc();
|
|
58
|
+
const children = this._parseChildren(null);
|
|
59
|
+
return createRoot(
|
|
60
|
+
children,
|
|
61
|
+
createLoc(start, this._startLoc(), this.source),
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
_parseChildren(parentTag) {
|
|
66
|
+
const children = [];
|
|
67
|
+
|
|
68
|
+
while (!this._eof()) {
|
|
69
|
+
const tok = this._peek();
|
|
70
|
+
|
|
71
|
+
if (tok.type === TokenType.EOF) break;
|
|
72
|
+
if (tok.type === TokenType.TAG_OPEN_CLOSE) break;
|
|
73
|
+
|
|
74
|
+
if (tok.type === TokenType.COMMENT) {
|
|
75
|
+
this._advance();
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (tok.type === TokenType.TEXT) {
|
|
80
|
+
children.push(this._parseText());
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (tok.type === TokenType.MUSTACHE_OPEN) {
|
|
85
|
+
children.push(this._parseMustache());
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (tok.type === TokenType.TAG_OPEN) {
|
|
90
|
+
const node = this._parseElement();
|
|
91
|
+
if (node) children.push(node);
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
this._advance();
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return children;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
_parseText() {
|
|
102
|
+
const tok = this._consume(TokenType.TEXT);
|
|
103
|
+
return createText(tok.value, tok.loc);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
_parseMustache() {
|
|
107
|
+
const open = this._consume(TokenType.MUSTACHE_OPEN);
|
|
108
|
+
const expr = this._consume(TokenType.MUSTACHE_EXPR);
|
|
109
|
+
const close = this._consume(TokenType.MUSTACHE_CLOSE);
|
|
110
|
+
|
|
111
|
+
if (!expr.value.trim()) {
|
|
112
|
+
throwError(
|
|
113
|
+
ErrorCode.EMPTY_EXPRESSION,
|
|
114
|
+
expr.loc,
|
|
115
|
+
this.source,
|
|
116
|
+
"{{ }}",
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const loc = createLoc(open.loc.start, close.loc.end);
|
|
121
|
+
return createInterpolation(
|
|
122
|
+
createExpression(expr.value, false, expr.loc),
|
|
123
|
+
loc,
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
_parseElement() {
|
|
128
|
+
const openTok = this._consume(TokenType.TAG_OPEN);
|
|
129
|
+
const tag = openTok.value;
|
|
130
|
+
const startLoc = openTok.loc.start;
|
|
131
|
+
const props = this._parseAttrs(tag);
|
|
132
|
+
|
|
133
|
+
let selfClosing = false;
|
|
134
|
+
if (this._peek()?.type === TokenType.TAG_SELF_CLOSE) {
|
|
135
|
+
this._advance();
|
|
136
|
+
selfClosing = true;
|
|
137
|
+
} else {
|
|
138
|
+
this._consume(TokenType.TAG_CLOSE);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (tag === "slot") {
|
|
142
|
+
return this._buildSlotOutlet(props, startLoc, selfClosing);
|
|
143
|
+
}
|
|
144
|
+
const slotAttr = props.find(
|
|
145
|
+
(p) => p.type === NodeType.ATTRIBUTE && p.name === "slot",
|
|
146
|
+
);
|
|
147
|
+
if (tag === "template" && slotAttr) {
|
|
148
|
+
return this._buildSlotContent(
|
|
149
|
+
slotAttr,
|
|
150
|
+
props,
|
|
151
|
+
startLoc,
|
|
152
|
+
selfClosing,
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (selfClosing || VOID_ELEMENTS.has(tag.toLowerCase())) {
|
|
157
|
+
return this._buildNode(tag, props, [], true, startLoc);
|
|
158
|
+
}
|
|
159
|
+
this.stack.push(tag);
|
|
160
|
+
const children = this._parseChildren(tag);
|
|
161
|
+
this.stack.pop();
|
|
162
|
+
|
|
163
|
+
if (this._peek()?.type === TokenType.TAG_OPEN_CLOSE) {
|
|
164
|
+
const closeTok = this._advance();
|
|
165
|
+
if (closeTok.value.toLowerCase() !== tag.toLowerCase()) {
|
|
166
|
+
throwError(
|
|
167
|
+
ErrorCode.UNEXPECTED_CLOSING_TAG,
|
|
168
|
+
closeTok.loc,
|
|
169
|
+
this.source,
|
|
170
|
+
closeTok.value,
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
if (this._peek()?.type === TokenType.TAG_CLOSE) this._advance();
|
|
174
|
+
} else {
|
|
175
|
+
throwError(
|
|
176
|
+
ErrorCode.MISSING_CLOSING_TAG,
|
|
177
|
+
createLoc(startLoc, startLoc),
|
|
178
|
+
this.source,
|
|
179
|
+
tag,
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const endLoc = this._startLoc();
|
|
184
|
+
return this._buildNode(tag, props, children, false, startLoc, endLoc);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
_buildNode(tag, props, children, selfClosing, startLoc, endLoc = null) {
|
|
188
|
+
const loc = createLoc(startLoc, endLoc ?? startLoc);
|
|
189
|
+
|
|
190
|
+
if (this.components.has(tag) || /^[A-Z]/.test(tag)) {
|
|
191
|
+
const slots = this._extractSlots(children);
|
|
192
|
+
return createComponent(tag, props, slots, loc);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return createElement(tag, props, children, selfClosing, loc);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
_buildSlotOutlet(props, startLoc, selfClosing) {
|
|
199
|
+
const nameAttr = props.find(
|
|
200
|
+
(p) => p.type === NodeType.ATTRIBUTE && p.name === "name",
|
|
201
|
+
);
|
|
202
|
+
const slotName = nameAttr?.value ?? "default";
|
|
203
|
+
let fallback = [];
|
|
204
|
+
if (!selfClosing) {
|
|
205
|
+
this.stack.push("slot");
|
|
206
|
+
fallback = this._parseChildren("slot");
|
|
207
|
+
this.stack.pop();
|
|
208
|
+
if (this._peek()?.type === TokenType.TAG_OPEN_CLOSE) {
|
|
209
|
+
this._advance();
|
|
210
|
+
if (this._peek()?.type === TokenType.TAG_CLOSE) this._advance();
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return createSlotOutlet(
|
|
215
|
+
slotName,
|
|
216
|
+
fallback,
|
|
217
|
+
createLoc(startLoc, this._startLoc()),
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
_buildSlotContent(slotAttr, props, startLoc, selfClosing) {
|
|
222
|
+
const slotName = slotAttr.value ?? "default";
|
|
223
|
+
let children = [];
|
|
224
|
+
|
|
225
|
+
if (!selfClosing) {
|
|
226
|
+
this.stack.push("template");
|
|
227
|
+
children = this._parseChildren("template");
|
|
228
|
+
this.stack.pop();
|
|
229
|
+
if (this._peek()?.type === TokenType.TAG_OPEN_CLOSE) {
|
|
230
|
+
this._advance();
|
|
231
|
+
if (this._peek()?.type === TokenType.TAG_CLOSE) this._advance();
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return createSlotContent(
|
|
236
|
+
slotName,
|
|
237
|
+
children,
|
|
238
|
+
createLoc(startLoc, this._startLoc()),
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
_extractSlots(children) {
|
|
243
|
+
const slots = { default: [] };
|
|
244
|
+
|
|
245
|
+
for (const child of children) {
|
|
246
|
+
if (child.type === NodeType.SLOT_CONTENT) {
|
|
247
|
+
slots[child.slotName] = child.children;
|
|
248
|
+
} else {
|
|
249
|
+
slots.default.push(child);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return slots;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
_parseAttrs(tag) {
|
|
257
|
+
const props = [];
|
|
258
|
+
const seen = new Set();
|
|
259
|
+
|
|
260
|
+
while (!this._eof()) {
|
|
261
|
+
const tok = this._peek();
|
|
262
|
+
if (
|
|
263
|
+
tok.type === TokenType.TAG_CLOSE ||
|
|
264
|
+
tok.type === TokenType.TAG_SELF_CLOSE ||
|
|
265
|
+
tok.type === TokenType.EOF
|
|
266
|
+
)
|
|
267
|
+
break;
|
|
268
|
+
|
|
269
|
+
if (tok.type !== TokenType.ATTR_NAME) {
|
|
270
|
+
this._advance();
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const nameTok = this._advance();
|
|
275
|
+
const rawName = nameTok.value;
|
|
276
|
+
if (seen.has(rawName)) {
|
|
277
|
+
throwError(
|
|
278
|
+
ErrorCode.DUPLICATE_KEY,
|
|
279
|
+
nameTok.loc,
|
|
280
|
+
this.source,
|
|
281
|
+
rawName,
|
|
282
|
+
tag,
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
seen.add(rawName);
|
|
286
|
+
|
|
287
|
+
let value = null;
|
|
288
|
+
if (this._peek()?.type === TokenType.ATTR_EQUALS) {
|
|
289
|
+
this._advance();
|
|
290
|
+
const valTok = this._consume(TokenType.ATTR_VALUE);
|
|
291
|
+
value = valTok.value;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const loc = nameTok.loc;
|
|
295
|
+
|
|
296
|
+
if (rawName.startsWith("@")) {
|
|
297
|
+
props.push(this._parseEventAttr(rawName, value, loc));
|
|
298
|
+
} else if (rawName.startsWith(":")) {
|
|
299
|
+
props.push(this._parseBindingAttr(rawName, value, loc));
|
|
300
|
+
} else if (rawName.startsWith("f-")) {
|
|
301
|
+
props.push(this._parseDirectiveAttr(rawName, value, loc, tag));
|
|
302
|
+
} else {
|
|
303
|
+
props.push(createAttribute(rawName, value, loc));
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
return props;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
_parseEventAttr(rawName, value, loc) {
|
|
311
|
+
const rest = rawName.slice(1);
|
|
312
|
+
const parts = rest.split(".");
|
|
313
|
+
const eventName = parts[0];
|
|
314
|
+
const modifiers = parts.slice(1);
|
|
315
|
+
|
|
316
|
+
if (!value || !value.trim()) {
|
|
317
|
+
throwError(ErrorCode.EMPTY_EXPRESSION, loc, this.source, rawName);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
return createEvent(
|
|
321
|
+
eventName,
|
|
322
|
+
createExpression(value.trim(), false, loc),
|
|
323
|
+
modifiers,
|
|
324
|
+
loc,
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
_parseBindingAttr(rawName, value, loc) {
|
|
329
|
+
const name = rawName.slice(1);
|
|
330
|
+
const isProp = name.startsWith("prop-");
|
|
331
|
+
|
|
332
|
+
if (!value || !value.trim()) {
|
|
333
|
+
throwError(ErrorCode.EMPTY_EXPRESSION, loc, this.source, rawName);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
return createBinding(
|
|
337
|
+
isProp ? name.slice(5) : name,
|
|
338
|
+
createExpression(value.trim(), false, loc),
|
|
339
|
+
isProp,
|
|
340
|
+
loc,
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
_parseDirectiveAttr(rawName, value, loc, tag) {
|
|
345
|
+
const name = rawName.slice(2);
|
|
346
|
+
const needsExpr = !["else", "once"].includes(name);
|
|
347
|
+
|
|
348
|
+
if (needsExpr && (!value || !value.trim())) {
|
|
349
|
+
throwError(ErrorCode.EMPTY_EXPRESSION, loc, this.source, rawName);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
let expression = null;
|
|
353
|
+
let arg = null;
|
|
354
|
+
|
|
355
|
+
if (value) {
|
|
356
|
+
if (name === "for") {
|
|
357
|
+
arg = this._parseForExpression(value.trim(), loc);
|
|
358
|
+
} else {
|
|
359
|
+
expression = createExpression(value.trim(), false, loc);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
return createDirective(name, expression, arg, loc);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
_parseForExpression(raw, loc) {
|
|
367
|
+
const m = FOR_RE.exec(raw);
|
|
368
|
+
if (!m) {
|
|
369
|
+
throwError(ErrorCode.INVALID_FOR_SYNTAX, loc, this.source, raw);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
const item = (m[1] ?? m[4]).trim();
|
|
373
|
+
const index = m[2] ? m[2].trim() : null;
|
|
374
|
+
const source = (m[3] ?? m[5]).trim();
|
|
375
|
+
|
|
376
|
+
return createForArg(item, source, index, loc);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
_eof() {
|
|
380
|
+
return (
|
|
381
|
+
this.pos >= this.tokens.length ||
|
|
382
|
+
this._peek()?.type === TokenType.EOF
|
|
383
|
+
);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
_peek() {
|
|
387
|
+
return this.tokens[this.pos];
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
_advance() {
|
|
391
|
+
const tok = this.tokens[this.pos];
|
|
392
|
+
if (tok?.type !== TokenType.EOF) this.pos++;
|
|
393
|
+
return tok;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
_consume(expectedType) {
|
|
397
|
+
const tok = this._peek();
|
|
398
|
+
if (!tok || tok.type !== expectedType) {
|
|
399
|
+
throwError(
|
|
400
|
+
ErrorCode.UNEXPECTED_TOKEN,
|
|
401
|
+
tok?.loc ?? createLoc(this._startLoc(), this._startLoc()),
|
|
402
|
+
this.source,
|
|
403
|
+
tok?.type ?? "EOF",
|
|
404
|
+
expectedType,
|
|
405
|
+
);
|
|
406
|
+
}
|
|
407
|
+
return this._advance();
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
_startLoc() {
|
|
411
|
+
const tok = this._peek();
|
|
412
|
+
return tok?.loc?.start ?? createPos(0, 0, 0);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
|
|
4
|
+
const ACTION_BASE_URL = '/__fusee/actions'
|
|
5
|
+
const VIRTUAL_CLIENT_ID = 'virtual:fusee-actions'
|
|
6
|
+
const VIRTUAL_SERVER_ID = 'virtual:fusee-action-routes'
|
|
7
|
+
|
|
8
|
+
export function actionsPlugin(options = {}) {
|
|
9
|
+
const {
|
|
10
|
+
serverActionsFile = 'src/actions.server.js',
|
|
11
|
+
clientActionsFile = 'src/actions.js',
|
|
12
|
+
baseUrl = ACTION_BASE_URL,
|
|
13
|
+
typesOutput = 'src/types/actions.d.ts',
|
|
14
|
+
} = options
|
|
15
|
+
|
|
16
|
+
let resolvedServerFile = null
|
|
17
|
+
let resolvedClientFile = null
|
|
18
|
+
|
|
19
|
+
return {
|
|
20
|
+
name: 'vite-plugin-fusee-actions',
|
|
21
|
+
|
|
22
|
+
configResolved(config) {
|
|
23
|
+
resolvedServerFile = path.resolve(config.root, serverActionsFile)
|
|
24
|
+
resolvedClientFile = path.resolve(config.root, clientActionsFile)
|
|
25
|
+
},
|
|
26
|
+
|
|
27
|
+
resolveId(id) {
|
|
28
|
+
if (id === VIRTUAL_CLIENT_ID)
|
|
29
|
+
return '\0' + VIRTUAL_CLIENT_ID
|
|
30
|
+
if (id === VIRTUAL_SERVER_ID)
|
|
31
|
+
return '\0' + VIRTUAL_SERVER_ID
|
|
32
|
+
},
|
|
33
|
+
|
|
34
|
+
async load(id) {
|
|
35
|
+
if (id === '\0' + VIRTUAL_CLIENT_ID) {
|
|
36
|
+
const serverActions = _extractServerActions(resolvedServerFile)
|
|
37
|
+
const clientActions = resolvedClientFile ? _extractClientActions(resolvedClientFile) : []
|
|
38
|
+
|
|
39
|
+
const { warnings, errors } = validateActions(serverActions, clientActions)
|
|
40
|
+
for (const w of warnings)
|
|
41
|
+
this.warn(w)
|
|
42
|
+
for (const e of errors)
|
|
43
|
+
this.error(e)
|
|
44
|
+
|
|
45
|
+
return generateClientStubs(serverActions, { baseUrl })
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (id === '\0' + VIRTUAL_SERVER_ID) {
|
|
49
|
+
const serverActions = _extractServerActions(resolvedServerFile)
|
|
50
|
+
return generateServerRoutes(serverActions, { serverActionsFile, baseUrl })
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
|
|
54
|
+
async buildEnd() {
|
|
55
|
+
if (typesOutput && resolvedServerFile) {
|
|
56
|
+
const serverActions = _extractServerActions(resolvedServerFile)
|
|
57
|
+
const types = generateActionTypes(serverActions)
|
|
58
|
+
const absOutput = path.resolve(process.cwd(), typesOutput)
|
|
59
|
+
fs.mkdirSync(path.dirname(absOutput), { recursive: true })
|
|
60
|
+
fs.writeFileSync(absOutput, types, 'utf8')
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
|
|
64
|
+
handleHotUpdate({ file, server }) {
|
|
65
|
+
if (file === resolvedServerFile || file === resolvedClientFile) {
|
|
66
|
+
const mod = server.moduleGraph.getModuleById('\0' + VIRTUAL_CLIENT_ID)
|
|
67
|
+
if (mod)
|
|
68
|
+
server.moduleGraph.invalidateModule(mod)
|
|
69
|
+
server.ws.send({ type: 'full-reload' })
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function _extractServerActions(filePath) {
|
|
76
|
+
if (!filePath || !fs.existsSync(filePath))
|
|
77
|
+
return []
|
|
78
|
+
|
|
79
|
+
const source = fs.readFileSync(filePath, 'utf8')
|
|
80
|
+
const actions = []
|
|
81
|
+
|
|
82
|
+
const RE_EXPORT = /export\s+(?:const|let|var)\s+(\w+)\s*=\s*defineAction\s*\(/g
|
|
83
|
+
const RE_NAMED = /defineAction\s*\(\s*(?:async\s+)?function\s+(\w+)/g
|
|
84
|
+
const RE_OPTS = /defineAction\s*\([^,)]+,\s*\{[^}]*name\s*:\s*['"](\w+)['"]/g
|
|
85
|
+
|
|
86
|
+
const seen = new Set()
|
|
87
|
+
|
|
88
|
+
let m
|
|
89
|
+
while ((m = RE_EXPORT.exec(source)) !== null) {
|
|
90
|
+
const name = m[1]
|
|
91
|
+
if (!seen.has(name)) { seen.add(name); actions.push({ name, exportName: name }) }
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
while ((m = RE_NAMED.exec(source)) !== null) {
|
|
95
|
+
const name = m[1]
|
|
96
|
+
if (!seen.has(name)) { seen.add(name); actions.push({ name, exportName: name }) }
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
while ((m = RE_OPTS.exec(source)) !== null) {
|
|
100
|
+
const name = m[1]
|
|
101
|
+
if (!seen.has(name)) { seen.add(name); actions.push({ name, exportName: name }) }
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return actions
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function _extractClientActions(filePath) {
|
|
108
|
+
if (!filePath || !fs.existsSync(filePath))
|
|
109
|
+
return []
|
|
110
|
+
|
|
111
|
+
const source = fs.readFileSync(filePath, 'utf8')
|
|
112
|
+
const names = []
|
|
113
|
+
const RE = /defineAction\s*\(\s*['"](\w+)['"]/g
|
|
114
|
+
let m
|
|
115
|
+
|
|
116
|
+
while ((m = RE.exec(source)) !== null) {
|
|
117
|
+
names.push(m[1])
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return names
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function validateActions(serverActions, clientActions) {
|
|
124
|
+
const warnings = []
|
|
125
|
+
const errors = []
|
|
126
|
+
|
|
127
|
+
const serverNames = new Set(serverActions.map(a => a.name))
|
|
128
|
+
const clientNames = new Set(clientActions)
|
|
129
|
+
|
|
130
|
+
for (const name of clientNames) {
|
|
131
|
+
if (!serverNames.has(name)) {
|
|
132
|
+
errors.push(`[fusée] Action "${name}" is defined on the client but has no corresponding defineAction() in actions.server.js`)
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
for (const name of serverNames) {
|
|
137
|
+
if (!clientNames.has(name) && clientActions.length > 0) {
|
|
138
|
+
warnings.push(`[fusée] Action "${name}" is defined on the server but is not used in actions.js — it will still be accessible via HTTP POST /${name}`
|
|
139
|
+
)
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return { warnings, errors }
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function generateClientStubs(actions, options = {}) {
|
|
147
|
+
const { baseUrl = ACTION_BASE_URL } = options
|
|
148
|
+
|
|
149
|
+
const lines = [
|
|
150
|
+
'// [fusée] Auto-generated action stubs — do not edit',
|
|
151
|
+
'// Generated by compiler/plugins/actions-plugin.js',
|
|
152
|
+
'',
|
|
153
|
+
`import { createActionProxy, useAction } from '../core/actions.js'`,
|
|
154
|
+
'',
|
|
155
|
+
]
|
|
156
|
+
|
|
157
|
+
for (const action of actions) {
|
|
158
|
+
lines.push(`export const ${action.exportName} = createActionProxy(` + `${JSON.stringify(action.name)}, { baseUrl: ${JSON.stringify(baseUrl)} })`)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
lines.push('')
|
|
162
|
+
lines.push('export { useAction }')
|
|
163
|
+
lines.push('')
|
|
164
|
+
|
|
165
|
+
return lines.join('\n')
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export function generateServerRoutes(actions, options = {}) {
|
|
169
|
+
const {
|
|
170
|
+
serverActionsFile = 'src/actions.server.js',
|
|
171
|
+
baseUrl = ACTION_BASE_URL,
|
|
172
|
+
} = options
|
|
173
|
+
|
|
174
|
+
const relPath = './' + serverActionsFile.replace(/^src\//, '')
|
|
175
|
+
|
|
176
|
+
const lines = [
|
|
177
|
+
'// [fusée] Auto-generated action routes — do not edit',
|
|
178
|
+
'// Generated by compiler/plugins/actions-plugin.js',
|
|
179
|
+
'',
|
|
180
|
+
`import { handleActionRequest, getRegisteredActions } from ${JSON.stringify(relPath)}`,
|
|
181
|
+
'',
|
|
182
|
+
'// Import side-effect: registers all actions in the registry',
|
|
183
|
+
`import ${JSON.stringify(relPath)}`,
|
|
184
|
+
'',
|
|
185
|
+
'/** Map of action name → request handler */',
|
|
186
|
+
'export const actionHandlers = {',
|
|
187
|
+
]
|
|
188
|
+
|
|
189
|
+
for (const action of actions) {
|
|
190
|
+
lines.push(` ${JSON.stringify(action.name)}: (req, res) => handleActionRequest(req, res),`)
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
lines.push('}')
|
|
194
|
+
lines.push('')
|
|
195
|
+
lines.push('/**')
|
|
196
|
+
lines.push(' * Mount all action routes on an Express app.')
|
|
197
|
+
lines.push(' * @param {import("express").Application} app')
|
|
198
|
+
lines.push(' */')
|
|
199
|
+
lines.push('export function mountActionRoutes(app) {')
|
|
200
|
+
lines.push(` const base = ${JSON.stringify(baseUrl)}`)
|
|
201
|
+
lines.push(' for (const [name, handler] of Object.entries(actionHandlers)) {')
|
|
202
|
+
lines.push(' app.post(`${base}/${name}`, handler)')
|
|
203
|
+
lines.push(' }')
|
|
204
|
+
lines.push('}')
|
|
205
|
+
lines.push('')
|
|
206
|
+
lines.push('/**')
|
|
207
|
+
lines.push(' * Fetch API compatible dispatcher (Bun / Deno / CF Workers).')
|
|
208
|
+
lines.push(' * @param {Request} request')
|
|
209
|
+
lines.push(' */')
|
|
210
|
+
lines.push('export async function dispatchAction(request) {')
|
|
211
|
+
lines.push(` const base = ${JSON.stringify(baseUrl)}`)
|
|
212
|
+
lines.push(' const url = new URL(request.url)')
|
|
213
|
+
lines.push(' const name = url.pathname.slice(base.length + 1)')
|
|
214
|
+
lines.push(' const handler = actionHandlers[name]')
|
|
215
|
+
lines.push(' if (!handler) return new Response(JSON.stringify({ error: `Action "${name}" not found` }), { status: 404 })')
|
|
216
|
+
lines.push(' return handler(request, null)')
|
|
217
|
+
lines.push('}')
|
|
218
|
+
lines.push('')
|
|
219
|
+
|
|
220
|
+
return lines.join('\n')
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export function generateActionTypes(actions) {
|
|
224
|
+
const lines = [
|
|
225
|
+
'// [fusée] Auto-generated action types — do not edit',
|
|
226
|
+
'// Generated by compiler/plugins/actions-plugin.js',
|
|
227
|
+
'',
|
|
228
|
+
"import type { useAction } from '../core/actions.js'",
|
|
229
|
+
'',
|
|
230
|
+
'/** All registered server action names */',
|
|
231
|
+
`export type ActionName = ${actions.map(a => JSON.stringify(a.name)).join(' | ') || 'never'}`,
|
|
232
|
+
'',
|
|
233
|
+
'/** Client proxy function for a server action */',
|
|
234
|
+
'export type ActionProxy<TArgs extends unknown[] = unknown[], TReturn = unknown> =',
|
|
235
|
+
' ((...args: TArgs) => Promise<TReturn>) & {',
|
|
236
|
+
' _isAction: true',
|
|
237
|
+
' _actionName: string',
|
|
238
|
+
' }',
|
|
239
|
+
'',
|
|
240
|
+
'/** useAction() result shape */',
|
|
241
|
+
'export type UseActionResult<TReturn = unknown> = {',
|
|
242
|
+
' execute: (...args: unknown[]) => Promise<TReturn | undefined>',
|
|
243
|
+
' pending: () => boolean',
|
|
244
|
+
' data: () => TReturn | undefined',
|
|
245
|
+
' error: () => Error | undefined',
|
|
246
|
+
' reset: () => void',
|
|
247
|
+
'}',
|
|
248
|
+
'',
|
|
249
|
+
'/** Auto-generated action stubs */',
|
|
250
|
+
'export declare const actionStubs: Record<ActionName, ActionProxy>',
|
|
251
|
+
'',
|
|
252
|
+
]
|
|
253
|
+
|
|
254
|
+
for (const action of actions) {
|
|
255
|
+
lines.push(`export declare const ${action.exportName}: ActionProxy`)
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
lines.push('')
|
|
259
|
+
lines.push("export { useAction } from '../core/actions.js'")
|
|
260
|
+
lines.push('')
|
|
261
|
+
|
|
262
|
+
return lines.join('\n')
|
|
263
|
+
}
|