@barefootjs/mojolicious 0.30.6 → 0.31.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/dist/adapter/expr/emitters.d.ts +1 -1
- package/dist/adapter/expr/emitters.d.ts.map +1 -1
- package/dist/adapter/index.js +11 -3
- package/dist/adapter/mojo-adapter.d.ts +0 -1
- package/dist/adapter/mojo-adapter.d.ts.map +1 -1
- package/dist/index.js +11 -3
- package/dist/vite.d.ts +38 -0
- package/dist/vite.d.ts.map +1 -0
- package/dist/vite.js +6663 -0
- package/lib/BarefootJS/Backend/Mojo.pm +1 -1
- package/lib/Mojolicious/Plugin/BarefootJS/DevReload.pm +1 -1
- package/lib/Mojolicious/Plugin/BarefootJS.pm +1 -1
- package/package.json +21 -8
- package/src/__tests__/mojo-adapter.test.ts +143 -0
- package/src/__tests__/multi-component-registry.test.ts +3 -3
- package/src/__tests__/scaffold.test.ts +7 -7
- package/src/__tests__/stock-route.test.ts +6 -5
- package/src/__tests__/vite.test.ts +126 -0
- package/src/adapter/expr/emitters.ts +37 -13
- package/src/adapter/mojo-adapter.ts +27 -5
- package/src/vite.ts +216 -0
- package/dist/build.d.ts +0 -28
- package/dist/build.d.ts.map +0 -1
- package/dist/build.js +0 -1933
- package/src/build.ts +0 -37
package/dist/build.js
DELETED
|
@@ -1,1933 +0,0 @@
|
|
|
1
|
-
// src/adapter/mojo-adapter.ts
|
|
2
|
-
import {
|
|
3
|
-
BaseAdapter,
|
|
4
|
-
isBooleanAttr,
|
|
5
|
-
parseExpression as parseExpression2,
|
|
6
|
-
stringifyParsedExpr as stringifyParsedExpr2,
|
|
7
|
-
parseStyleObjectEntries,
|
|
8
|
-
isSupported,
|
|
9
|
-
exprToString,
|
|
10
|
-
parseProviderObjectLiteral,
|
|
11
|
-
emitParsedExpr as emitParsedExpr2,
|
|
12
|
-
emitIRNode,
|
|
13
|
-
emitAttrValue,
|
|
14
|
-
augmentInheritedPropAccesses,
|
|
15
|
-
collectModuleStringConsts,
|
|
16
|
-
lookupStaticRecordLiteral,
|
|
17
|
-
searchParamsLocalNames,
|
|
18
|
-
prepareLoweringMatchers,
|
|
19
|
-
queryHrefArgs,
|
|
20
|
-
isValidHelperId,
|
|
21
|
-
sortComparatorFromArrow as sortComparatorFromArrow2,
|
|
22
|
-
isLowerableLoopDestructure,
|
|
23
|
-
isDangerousInnerHtmlAttr,
|
|
24
|
-
resolveDangerousInnerHtml,
|
|
25
|
-
dangerousInnerHtmlMetacharViolation,
|
|
26
|
-
dangerousInnerHtmlDiagnostic,
|
|
27
|
-
resolveStaticLoopSource,
|
|
28
|
-
derivesScopeFromSlot
|
|
29
|
-
} from "@barefootjs/jsx";
|
|
30
|
-
|
|
31
|
-
// src/adapter/boolean-result.ts
|
|
32
|
-
import { parseExpression } from "@barefootjs/jsx";
|
|
33
|
-
var COMPARISON_OPS = new Set([
|
|
34
|
-
"<",
|
|
35
|
-
">",
|
|
36
|
-
"<=",
|
|
37
|
-
">=",
|
|
38
|
-
"==",
|
|
39
|
-
"===",
|
|
40
|
-
"!=",
|
|
41
|
-
"!=="
|
|
42
|
-
]);
|
|
43
|
-
function isBooleanResultParsed(node) {
|
|
44
|
-
switch (node.kind) {
|
|
45
|
-
case "literal":
|
|
46
|
-
return node.literalType === "boolean";
|
|
47
|
-
case "binary":
|
|
48
|
-
return COMPARISON_OPS.has(node.op);
|
|
49
|
-
case "unary":
|
|
50
|
-
return node.op === "!";
|
|
51
|
-
case "logical":
|
|
52
|
-
return isBooleanResultParsed(node.left) && isBooleanResultParsed(node.right);
|
|
53
|
-
case "conditional":
|
|
54
|
-
return isBooleanResultParsed(node.consequent) && isBooleanResultParsed(node.alternate);
|
|
55
|
-
default:
|
|
56
|
-
return false;
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
function isBooleanResultExpr(expr) {
|
|
60
|
-
const parsed = parseExpression(expr.trim());
|
|
61
|
-
if (!parsed)
|
|
62
|
-
return false;
|
|
63
|
-
return isBooleanResultParsed(parsed);
|
|
64
|
-
}
|
|
65
|
-
var ARIA_BOOLEAN_ATTRS = new Set([
|
|
66
|
-
"aria-atomic",
|
|
67
|
-
"aria-busy",
|
|
68
|
-
"aria-disabled",
|
|
69
|
-
"aria-hidden",
|
|
70
|
-
"aria-modal",
|
|
71
|
-
"aria-multiline",
|
|
72
|
-
"aria-multiselectable",
|
|
73
|
-
"aria-readonly",
|
|
74
|
-
"aria-required",
|
|
75
|
-
"aria-selected",
|
|
76
|
-
"aria-expanded",
|
|
77
|
-
"aria-checked",
|
|
78
|
-
"aria-pressed"
|
|
79
|
-
]);
|
|
80
|
-
function isAriaBooleanAttr(name) {
|
|
81
|
-
return ARIA_BOOLEAN_ATTRS.has(name);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// src/adapter/mojo-adapter.ts
|
|
85
|
-
import { BF_SLOT, BF_COND, BF_REGION, escapeHtml } from "@barefootjs/shared";
|
|
86
|
-
|
|
87
|
-
// src/adapter/lib/constants.ts
|
|
88
|
-
var MOJO_TEMPLATE_PRIMITIVES = {
|
|
89
|
-
"JSON.stringify": { arity: 1, emit: (args) => `bf->json(${args[0]})` },
|
|
90
|
-
String: { arity: 1, emit: (args) => `bf->string(${args[0]})` },
|
|
91
|
-
Number: { arity: 1, emit: (args) => `bf->number(${args[0]})` },
|
|
92
|
-
"Math.floor": { arity: 1, emit: (args) => `bf->floor(${args[0]})` },
|
|
93
|
-
"Math.ceil": { arity: 1, emit: (args) => `bf->ceil(${args[0]})` },
|
|
94
|
-
"Math.round": { arity: 1, emit: (args) => `bf->round(${args[0]})` },
|
|
95
|
-
"Math.min": { arity: 2, emit: (args) => `bf->min(${args[0]}, ${args[1]})` },
|
|
96
|
-
"Math.max": { arity: 2, emit: (args) => `bf->max(${args[0]}, ${args[1]})` },
|
|
97
|
-
"Math.abs": { arity: 1, emit: (args) => `bf->abs(${args[0]})` },
|
|
98
|
-
isValidElement: { arity: 1, emit: (args) => `bf->is_element(${args[0]})` }
|
|
99
|
-
};
|
|
100
|
-
var MOJO_PRIMITIVE_EMIT_MAP = Object.fromEntries(Object.entries(MOJO_TEMPLATE_PRIMITIVES).map(([k, v]) => [k, v.emit]));
|
|
101
|
-
|
|
102
|
-
// src/adapter/lib/perl-naming.ts
|
|
103
|
-
function perlHashKey(name) {
|
|
104
|
-
return /^[A-Za-z_][A-Za-z0-9_]*$/.test(name) ? name : `'${name.replace(/'/g, "\\'")}'`;
|
|
105
|
-
}
|
|
106
|
-
function perlIdentifierFromMarkerId(markerId) {
|
|
107
|
-
return markerId.replace(/[^a-zA-Z0-9]/g, (ch) => ch === "_" ? "__" : `_x${ch.charCodeAt(0).toString(16)}`);
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
// src/adapter/lib/ir-scope.ts
|
|
111
|
-
function resolveJsxChildrenProp(props) {
|
|
112
|
-
const prop = props.find((p) => p.name === "children");
|
|
113
|
-
if (!prop)
|
|
114
|
-
return [];
|
|
115
|
-
if (prop.value.kind !== "jsx-children")
|
|
116
|
-
return [];
|
|
117
|
-
return prop.value.children;
|
|
118
|
-
}
|
|
119
|
-
function collectRootScopeNodes(node) {
|
|
120
|
-
const out = new Set;
|
|
121
|
-
const visit = (n) => {
|
|
122
|
-
if (!n)
|
|
123
|
-
return;
|
|
124
|
-
if (n.type === "element") {
|
|
125
|
-
out.add(n);
|
|
126
|
-
return;
|
|
127
|
-
}
|
|
128
|
-
if (n.type === "if-statement") {
|
|
129
|
-
const s = n;
|
|
130
|
-
visit(s.consequent);
|
|
131
|
-
visit(s.alternate);
|
|
132
|
-
return;
|
|
133
|
-
}
|
|
134
|
-
if (n.type === "fragment") {
|
|
135
|
-
for (const c of n.children)
|
|
136
|
-
visit(c);
|
|
137
|
-
}
|
|
138
|
-
};
|
|
139
|
-
visit(node);
|
|
140
|
-
return out;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
// src/adapter/expr/array-method.ts
|
|
144
|
-
import {
|
|
145
|
-
serializeParsedExpr,
|
|
146
|
-
freeVarsInBody
|
|
147
|
-
} from "@barefootjs/jsx";
|
|
148
|
-
function renderArrayMethod(method, object, args, emit) {
|
|
149
|
-
switch (method) {
|
|
150
|
-
case "join": {
|
|
151
|
-
const obj = emit(object);
|
|
152
|
-
const sep = args.length >= 1 ? emit(args[0]) : `','`;
|
|
153
|
-
return `bf->join(${obj}, ${sep})`;
|
|
154
|
-
}
|
|
155
|
-
case "includes": {
|
|
156
|
-
const obj = emit(object);
|
|
157
|
-
const needle = emit(args[0]);
|
|
158
|
-
return `bf->includes(${obj}, ${needle})`;
|
|
159
|
-
}
|
|
160
|
-
case "indexOf":
|
|
161
|
-
case "lastIndexOf": {
|
|
162
|
-
const fn = method === "indexOf" ? "index_of" : "last_index_of";
|
|
163
|
-
const obj = emit(object);
|
|
164
|
-
const needle = emit(args[0]);
|
|
165
|
-
return `bf->${fn}(${obj}, ${needle})`;
|
|
166
|
-
}
|
|
167
|
-
case "at": {
|
|
168
|
-
const obj = emit(object);
|
|
169
|
-
const idx = args.length >= 1 ? emit(args[0]) : "0";
|
|
170
|
-
return `bf->at(${obj}, ${idx})`;
|
|
171
|
-
}
|
|
172
|
-
case "concat": {
|
|
173
|
-
if (args.length === 0) {
|
|
174
|
-
return emit(object);
|
|
175
|
-
}
|
|
176
|
-
const a = emit(object);
|
|
177
|
-
const b = emit(args[0]);
|
|
178
|
-
return `bf->concat(${a}, ${b})`;
|
|
179
|
-
}
|
|
180
|
-
case "slice": {
|
|
181
|
-
const recv = emit(object);
|
|
182
|
-
const start = args.length >= 1 ? emit(args[0]) : "0";
|
|
183
|
-
const end = args.length >= 2 ? emit(args[1]) : "undef";
|
|
184
|
-
return `bf->slice(${recv}, ${start}, ${end})`;
|
|
185
|
-
}
|
|
186
|
-
case "reverse":
|
|
187
|
-
case "toReversed": {
|
|
188
|
-
const recv = emit(object);
|
|
189
|
-
return `bf->reverse(${recv})`;
|
|
190
|
-
}
|
|
191
|
-
case "toLowerCase": {
|
|
192
|
-
const recv = emit(object);
|
|
193
|
-
return `lc(${recv})`;
|
|
194
|
-
}
|
|
195
|
-
case "toUpperCase": {
|
|
196
|
-
const recv = emit(object);
|
|
197
|
-
return `uc(${recv})`;
|
|
198
|
-
}
|
|
199
|
-
case "trim": {
|
|
200
|
-
const recv = emit(object);
|
|
201
|
-
return `bf->trim(${recv})`;
|
|
202
|
-
}
|
|
203
|
-
case "trimStart":
|
|
204
|
-
case "trimEnd": {
|
|
205
|
-
const fn = method === "trimStart" ? "trim_start" : "trim_end";
|
|
206
|
-
const recv = emit(object);
|
|
207
|
-
return `bf->${fn}(${recv})`;
|
|
208
|
-
}
|
|
209
|
-
case "toFixed": {
|
|
210
|
-
const recv = emit(object);
|
|
211
|
-
const digits = args.length >= 1 ? emit(args[0]) : "0";
|
|
212
|
-
return `bf->to_fixed(${recv}, ${digits})`;
|
|
213
|
-
}
|
|
214
|
-
case "split": {
|
|
215
|
-
const recv = emit(object);
|
|
216
|
-
if (args.length === 0) {
|
|
217
|
-
return `bf->split(${recv})`;
|
|
218
|
-
}
|
|
219
|
-
const sep = emit(args[0]);
|
|
220
|
-
if (args.length === 1) {
|
|
221
|
-
return `bf->split(${recv}, ${sep})`;
|
|
222
|
-
}
|
|
223
|
-
const limit = emit(args[1]);
|
|
224
|
-
return `bf->split(${recv}, ${sep}, ${limit})`;
|
|
225
|
-
}
|
|
226
|
-
case "startsWith":
|
|
227
|
-
case "endsWith": {
|
|
228
|
-
const fn = method === "startsWith" ? "starts_with" : "ends_with";
|
|
229
|
-
const recv = emit(object);
|
|
230
|
-
const arg = emit(args[0]);
|
|
231
|
-
if (args.length >= 2) {
|
|
232
|
-
return `bf->${fn}(${recv}, ${arg}, ${emit(args[1])})`;
|
|
233
|
-
}
|
|
234
|
-
return `bf->${fn}(${recv}, ${arg})`;
|
|
235
|
-
}
|
|
236
|
-
case "replace": {
|
|
237
|
-
const recv = emit(object);
|
|
238
|
-
const oldS = emit(args[0]);
|
|
239
|
-
const newS = emit(args[1]);
|
|
240
|
-
return `bf->replace(${recv}, ${oldS}, ${newS})`;
|
|
241
|
-
}
|
|
242
|
-
case "replaceAll": {
|
|
243
|
-
const recv = emit(object);
|
|
244
|
-
const oldS = emit(args[0]);
|
|
245
|
-
const newS = emit(args[1]);
|
|
246
|
-
return `bf->replace_all(${recv}, ${oldS}, ${newS})`;
|
|
247
|
-
}
|
|
248
|
-
case "repeat": {
|
|
249
|
-
const recv = emit(object);
|
|
250
|
-
const count = args.length === 0 ? "0" : emit(args[0]);
|
|
251
|
-
return `bf->repeat(${recv}, ${count})`;
|
|
252
|
-
}
|
|
253
|
-
case "padStart":
|
|
254
|
-
case "padEnd": {
|
|
255
|
-
const fn = method === "padStart" ? "pad_start" : "pad_end";
|
|
256
|
-
const recv = emit(object);
|
|
257
|
-
if (args.length === 0) {
|
|
258
|
-
return `bf->${fn}(${recv}, 0)`;
|
|
259
|
-
}
|
|
260
|
-
const target = emit(args[0]);
|
|
261
|
-
if (args.length === 1) {
|
|
262
|
-
return `bf->${fn}(${recv}, ${target})`;
|
|
263
|
-
}
|
|
264
|
-
const pad = emit(args[1]);
|
|
265
|
-
return `bf->${fn}(${recv}, ${target}, ${pad})`;
|
|
266
|
-
}
|
|
267
|
-
default: {
|
|
268
|
-
const _exhaustive = method;
|
|
269
|
-
throw new Error(`renderArrayMethod: unhandled ArrayMethod '${_exhaustive}'`);
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
function escapePerlSingleQuote(s) {
|
|
274
|
-
return s.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
|
|
275
|
-
}
|
|
276
|
-
function emitEvalEnvArg(body, params, emit) {
|
|
277
|
-
const free = freeVarsInBody(body, new Set(params));
|
|
278
|
-
if (free.length === 0)
|
|
279
|
-
return "{}";
|
|
280
|
-
const pairs = free.map((n) => `'${escapePerlSingleQuote(n)}' => ${emit({ kind: "identifier", name: n })}`);
|
|
281
|
-
return `{ ${pairs.join(", ")} }`;
|
|
282
|
-
}
|
|
283
|
-
function renderSortEval(recv, body, params, emit) {
|
|
284
|
-
if (params.length < 2)
|
|
285
|
-
return null;
|
|
286
|
-
const [paramA, paramB] = params;
|
|
287
|
-
const json = serializeParsedExpr(body);
|
|
288
|
-
if (json === null)
|
|
289
|
-
return null;
|
|
290
|
-
const env = emitEvalEnvArg(body, [paramA, paramB], emit);
|
|
291
|
-
return `bf->sort_eval(${recv}, '${escapePerlSingleQuote(json)}', '${paramA}', '${paramB}', ${env})`;
|
|
292
|
-
}
|
|
293
|
-
function renderReduceEval(recv, body, params, init, direction, emit) {
|
|
294
|
-
if (params.length < 2)
|
|
295
|
-
return null;
|
|
296
|
-
const [paramAcc, paramItem] = params;
|
|
297
|
-
const json = serializeParsedExpr(body);
|
|
298
|
-
if (json === null)
|
|
299
|
-
return null;
|
|
300
|
-
let initPerl;
|
|
301
|
-
if (init.kind === "literal" && init.literalType === "string") {
|
|
302
|
-
initPerl = `'${escapePerlSingleQuote(String(init.value))}'`;
|
|
303
|
-
} else if (init.kind === "literal" && init.literalType === "number") {
|
|
304
|
-
initPerl = String(init.value);
|
|
305
|
-
} else {
|
|
306
|
-
return null;
|
|
307
|
-
}
|
|
308
|
-
const env = emitEvalEnvArg(body, [paramAcc, paramItem], emit);
|
|
309
|
-
return `bf->reduce_eval(${recv}, '${escapePerlSingleQuote(json)}', '${paramAcc}', '${paramItem}', ${initPerl}, '${direction}', ${env})`;
|
|
310
|
-
}
|
|
311
|
-
function renderPredicateEval(funcName, recv, predicate, param, emit, forward) {
|
|
312
|
-
const json = serializeParsedExpr(predicate);
|
|
313
|
-
if (json === null)
|
|
314
|
-
return null;
|
|
315
|
-
const env = emitEvalEnvArg(predicate, [param], emit);
|
|
316
|
-
const fwd = forward === undefined ? "" : `, ${forward ? 1 : 0}`;
|
|
317
|
-
return `bf->${funcName}(${recv}, '${escapePerlSingleQuote(json)}', '${param}'${fwd}, ${env})`;
|
|
318
|
-
}
|
|
319
|
-
function renderFlatMapEval(recv, body, param, emit) {
|
|
320
|
-
const json = serializeParsedExpr(body);
|
|
321
|
-
if (json === null)
|
|
322
|
-
return null;
|
|
323
|
-
const env = emitEvalEnvArg(body, [param], emit);
|
|
324
|
-
return `bf->flat_map_eval(${recv}, '${escapePerlSingleQuote(json)}', '${param}', ${env})`;
|
|
325
|
-
}
|
|
326
|
-
function renderMapEval(recv, body, param, emit) {
|
|
327
|
-
const json = serializeParsedExpr(body);
|
|
328
|
-
if (json === null)
|
|
329
|
-
return null;
|
|
330
|
-
const env = emitEvalEnvArg(body, [param], emit);
|
|
331
|
-
return `bf->map_eval(${recv}, '${escapePerlSingleQuote(json)}', '${param}', ${env})`;
|
|
332
|
-
}
|
|
333
|
-
function renderSortMethod(recv, c) {
|
|
334
|
-
const keyHashes = c.keys.map((k) => {
|
|
335
|
-
const keyEntry = k.key.kind === "self" ? `key_kind => 'self'` : `key_kind => 'field', key => '${k.key.field}'`;
|
|
336
|
-
return `{ ${keyEntry}, compare_type => '${k.type}', direction => '${k.direction}' }`;
|
|
337
|
-
});
|
|
338
|
-
return `bf->sort(${recv}, { keys => [${keyHashes.join(", ")}] })`;
|
|
339
|
-
}
|
|
340
|
-
function renderFlatMethod(recv, depth, emit) {
|
|
341
|
-
if (typeof depth === "object") {
|
|
342
|
-
return `bf->flat_dynamic(${recv}, ${emit(depth.expr)})`;
|
|
343
|
-
}
|
|
344
|
-
const d = depth === "infinity" ? -1 : depth;
|
|
345
|
-
return `bf->flat(${recv}, ${d})`;
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
// src/adapter/lib/static-value.ts
|
|
349
|
-
function escapePerlSingleQuote2(s) {
|
|
350
|
-
return s.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
|
|
351
|
-
}
|
|
352
|
-
function staticValueToPerl(value) {
|
|
353
|
-
if (value === null || value === undefined)
|
|
354
|
-
return "undef";
|
|
355
|
-
if (typeof value === "boolean")
|
|
356
|
-
return null;
|
|
357
|
-
if (typeof value === "number")
|
|
358
|
-
return String(value);
|
|
359
|
-
if (typeof value === "string")
|
|
360
|
-
return `'${escapePerlSingleQuote2(value)}'`;
|
|
361
|
-
if (Array.isArray(value)) {
|
|
362
|
-
const items = [];
|
|
363
|
-
for (const el of value) {
|
|
364
|
-
const serialized = staticValueToPerl(el);
|
|
365
|
-
if (serialized === null)
|
|
366
|
-
return null;
|
|
367
|
-
items.push(serialized);
|
|
368
|
-
}
|
|
369
|
-
return `[${items.join(", ")}]`;
|
|
370
|
-
}
|
|
371
|
-
if (typeof value === "object") {
|
|
372
|
-
const entries = [];
|
|
373
|
-
for (const [key, val] of Object.entries(value)) {
|
|
374
|
-
const serialized = staticValueToPerl(val);
|
|
375
|
-
if (serialized === null)
|
|
376
|
-
return null;
|
|
377
|
-
entries.push(`${perlHashKey(key)} => ${serialized}`);
|
|
378
|
-
}
|
|
379
|
-
return `{ ${entries.join(", ")} }`;
|
|
380
|
-
}
|
|
381
|
-
return null;
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
// src/adapter/expr/emitters.ts
|
|
385
|
-
import {
|
|
386
|
-
groupBinaryOperand,
|
|
387
|
-
isStringTypedOperand,
|
|
388
|
-
isStringConcatBinary,
|
|
389
|
-
emitParsedExpr,
|
|
390
|
-
identifierPath,
|
|
391
|
-
matchSearchParamsMethodCall,
|
|
392
|
-
sortComparatorFromArrow,
|
|
393
|
-
asCallbackMethodCall
|
|
394
|
-
} from "@barefootjs/jsx";
|
|
395
|
-
|
|
396
|
-
// src/adapter/expr/operand.ts
|
|
397
|
-
function emitIndexAccessPerl(object, index, emit, isStringName) {
|
|
398
|
-
return `bf->get(${emit(object)}, ${emit(index)})`;
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
// src/adapter/expr/emitters.ts
|
|
402
|
-
var PREDICATE_METHODS = new Set([
|
|
403
|
-
"filter",
|
|
404
|
-
"find",
|
|
405
|
-
"findIndex",
|
|
406
|
-
"findLast",
|
|
407
|
-
"findLastIndex",
|
|
408
|
-
"every",
|
|
409
|
-
"some"
|
|
410
|
-
]);
|
|
411
|
-
|
|
412
|
-
class MojoFilterEmitter {
|
|
413
|
-
param;
|
|
414
|
-
localVarMap;
|
|
415
|
-
isStringName;
|
|
416
|
-
onUnsupported;
|
|
417
|
-
constructor(param, localVarMap, isStringName = () => false, onUnsupported) {
|
|
418
|
-
this.param = param;
|
|
419
|
-
this.localVarMap = localVarMap;
|
|
420
|
-
this.isStringName = isStringName;
|
|
421
|
-
this.onUnsupported = onUnsupported;
|
|
422
|
-
}
|
|
423
|
-
identifier(name) {
|
|
424
|
-
if (name === this.param)
|
|
425
|
-
return `$${this.param}`;
|
|
426
|
-
const signal = this.localVarMap.get(name);
|
|
427
|
-
if (signal)
|
|
428
|
-
return `$${signal}`;
|
|
429
|
-
return `$${name}`;
|
|
430
|
-
}
|
|
431
|
-
literal(value, literalType) {
|
|
432
|
-
if (literalType === "string")
|
|
433
|
-
return `'${value}'`;
|
|
434
|
-
if (literalType === "boolean")
|
|
435
|
-
return value ? "1" : "0";
|
|
436
|
-
if (literalType === "null")
|
|
437
|
-
return "undef";
|
|
438
|
-
return String(value);
|
|
439
|
-
}
|
|
440
|
-
member(object, property, _computed, _optional, emit) {
|
|
441
|
-
if (property === "length" && (asCallbackMethodCall(object) !== null || object.kind === "array-literal")) {
|
|
442
|
-
return `scalar(@{${emit(object)}})`;
|
|
443
|
-
}
|
|
444
|
-
return `${emit(object)}->{${property}}`;
|
|
445
|
-
}
|
|
446
|
-
indexAccess(object, index, emit) {
|
|
447
|
-
return emitIndexAccessPerl(object, index, emit, this.isStringName);
|
|
448
|
-
}
|
|
449
|
-
call(callee, args, emit) {
|
|
450
|
-
if (callee.kind === "identifier" && args.length === 0) {
|
|
451
|
-
return `$${callee.name}`;
|
|
452
|
-
}
|
|
453
|
-
return emit(callee);
|
|
454
|
-
}
|
|
455
|
-
unary(op, argument, emit) {
|
|
456
|
-
const arg = emit(argument);
|
|
457
|
-
if (op === "!") {
|
|
458
|
-
const needsParens = argument.kind === "binary" || argument.kind === "logical";
|
|
459
|
-
return needsParens ? `!(${arg})` : `!${arg}`;
|
|
460
|
-
}
|
|
461
|
-
if (op === "-")
|
|
462
|
-
return `-${arg}`;
|
|
463
|
-
return arg;
|
|
464
|
-
}
|
|
465
|
-
binary(op, left, right, emit) {
|
|
466
|
-
const l = groupBinaryOperand(left, emit(left));
|
|
467
|
-
const r = groupBinaryOperand(right, emit(right));
|
|
468
|
-
const isStr = (e) => isStringTypedOperand(e, this.isStringName);
|
|
469
|
-
const stringCmp = isStr(left) || isStr(right);
|
|
470
|
-
if ((op === "===" || op === "==") && stringCmp) {
|
|
471
|
-
return `${l} eq ${r}`;
|
|
472
|
-
}
|
|
473
|
-
if ((op === "!==" || op === "!=") && stringCmp) {
|
|
474
|
-
return `${l} ne ${r}`;
|
|
475
|
-
}
|
|
476
|
-
if (isStringConcatBinary(op, left, right, this.isStringName)) {
|
|
477
|
-
return `${l} . ${r}`;
|
|
478
|
-
}
|
|
479
|
-
const opMap = {
|
|
480
|
-
"===": "==",
|
|
481
|
-
"!==": "!=",
|
|
482
|
-
">": ">",
|
|
483
|
-
"<": "<",
|
|
484
|
-
">=": ">=",
|
|
485
|
-
"<=": "<=",
|
|
486
|
-
"+": "+",
|
|
487
|
-
"-": "-",
|
|
488
|
-
"*": "*",
|
|
489
|
-
"/": "/"
|
|
490
|
-
};
|
|
491
|
-
return `${l} ${opMap[op] ?? op} ${r}`;
|
|
492
|
-
}
|
|
493
|
-
logical(op, left, right, emit) {
|
|
494
|
-
const l = emit(left);
|
|
495
|
-
const r = emit(right);
|
|
496
|
-
if (op === "&&")
|
|
497
|
-
return `(${l} && ${r})`;
|
|
498
|
-
if (op === "||")
|
|
499
|
-
return `(${l} || ${r})`;
|
|
500
|
-
return `(${l} // ${r})`;
|
|
501
|
-
}
|
|
502
|
-
callbackMethod(method, object, arrow, _restArgs, emit) {
|
|
503
|
-
if (!PREDICATE_METHODS.has(method)) {
|
|
504
|
-
this.onUnsupported?.(`Filter predicate contains a nested '.${method}(...)' callback, which has no Perl scalar form`, `Rewrite the predicate without a nested callback method, or add /* @client */ for client-only evaluation (no SSR).`);
|
|
505
|
-
return "1";
|
|
506
|
-
}
|
|
507
|
-
const param = arrow.params[0];
|
|
508
|
-
const predicate = arrow.body;
|
|
509
|
-
const arrayExpr = emit(object);
|
|
510
|
-
const predBody = emitParsedExpr(predicate, new MojoFilterEmitter(param, this.localVarMap, this.isStringName, this.onUnsupported));
|
|
511
|
-
const grepBody = predBody.replace(new RegExp(`\\$${param}\\b`, "g"), "$_");
|
|
512
|
-
if (method === "filter")
|
|
513
|
-
return `[grep { ${grepBody} } @{${arrayExpr}}]`;
|
|
514
|
-
if (method === "every")
|
|
515
|
-
return `!(grep { !(${grepBody}) } @{${arrayExpr}})`;
|
|
516
|
-
if (method === "some")
|
|
517
|
-
return `!!(grep { ${grepBody} } @{${arrayExpr}})`;
|
|
518
|
-
this.onUnsupported?.(`Filter predicate contains a nested '.${method}(...)' callback, which has no Perl scalar form`, `Rewrite the predicate without a nested callback method, or add /* @client */ for client-only evaluation (no SSR).`);
|
|
519
|
-
return arrayExpr;
|
|
520
|
-
}
|
|
521
|
-
arrayLiteral(elements, emit) {
|
|
522
|
-
return `[${elements.map(emit).join(", ")}]`;
|
|
523
|
-
}
|
|
524
|
-
arrayMethod(method, object, args, emit) {
|
|
525
|
-
return renderArrayMethod(method, object, args, emit);
|
|
526
|
-
}
|
|
527
|
-
flatMethod(object, depth, emit) {
|
|
528
|
-
return renderFlatMethod(emit(object), depth, emit);
|
|
529
|
-
}
|
|
530
|
-
conditional(_test, _consequent, _alternate) {
|
|
531
|
-
return "1";
|
|
532
|
-
}
|
|
533
|
-
templateLiteral(_parts) {
|
|
534
|
-
return "1";
|
|
535
|
-
}
|
|
536
|
-
arrow(_params, _body, _emit) {
|
|
537
|
-
return "1";
|
|
538
|
-
}
|
|
539
|
-
regex(_raw) {
|
|
540
|
-
return "1";
|
|
541
|
-
}
|
|
542
|
-
unsupported(_raw, _reason) {
|
|
543
|
-
return "1";
|
|
544
|
-
}
|
|
545
|
-
objectLiteral(_properties, _raw, _emit) {
|
|
546
|
-
return "1";
|
|
547
|
-
}
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
class MojoTopLevelEmitter {
|
|
551
|
-
ctx;
|
|
552
|
-
constructor(ctx) {
|
|
553
|
-
this.ctx = ctx;
|
|
554
|
-
}
|
|
555
|
-
identifier(name) {
|
|
556
|
-
if (name === "undefined" || name === "null")
|
|
557
|
-
return "undef";
|
|
558
|
-
const inlined = this.ctx.resolveModuleStringConst(name);
|
|
559
|
-
if (inlined !== null)
|
|
560
|
-
return inlined;
|
|
561
|
-
const literalConst = this.ctx.resolveLiteralConst(name);
|
|
562
|
-
if (literalConst !== null)
|
|
563
|
-
return literalConst;
|
|
564
|
-
return `$${name}`;
|
|
565
|
-
}
|
|
566
|
-
literal(value, literalType) {
|
|
567
|
-
if (literalType === "string")
|
|
568
|
-
return `'${value}'`;
|
|
569
|
-
if (literalType === "boolean")
|
|
570
|
-
return value ? "1" : "0";
|
|
571
|
-
if (literalType === "null")
|
|
572
|
-
return "undef";
|
|
573
|
-
return String(value);
|
|
574
|
-
}
|
|
575
|
-
member(object, property, _computed, _optional, emit) {
|
|
576
|
-
if (object.kind === "identifier" && object.name === "props") {
|
|
577
|
-
return `$${property}`;
|
|
578
|
-
}
|
|
579
|
-
if (object.kind === "identifier") {
|
|
580
|
-
const staticValue = this.ctx.resolveStaticRecordLiteral(object.name, property);
|
|
581
|
-
if (staticValue !== null)
|
|
582
|
-
return staticValue;
|
|
583
|
-
}
|
|
584
|
-
const obj = emit(object);
|
|
585
|
-
if (property === "length") {
|
|
586
|
-
const isStr = (e) => isStringTypedOperand(e, (n) => this.ctx._isStringValueName(n));
|
|
587
|
-
const isStringReceiver = isStr(object) || object.kind === "identifier" && this.ctx._isStringValueName(object.name);
|
|
588
|
-
if (isStringReceiver)
|
|
589
|
-
return `bf->length(${obj})`;
|
|
590
|
-
return `scalar(@{${obj}})`;
|
|
591
|
-
}
|
|
592
|
-
return `${obj}->{${property}}`;
|
|
593
|
-
}
|
|
594
|
-
indexAccess(object, index, emit) {
|
|
595
|
-
return emitIndexAccessPerl(object, index, emit, (n) => this.ctx._isStringValueName(n));
|
|
596
|
-
}
|
|
597
|
-
call(callee, args, emit) {
|
|
598
|
-
if (callee.kind === "identifier" && args.length === 0) {
|
|
599
|
-
return `$${callee.name}`;
|
|
600
|
-
}
|
|
601
|
-
if (this.ctx._searchParamsLocals.size > 0) {
|
|
602
|
-
const sp = matchSearchParamsMethodCall(callee, args, this.ctx._searchParamsLocals);
|
|
603
|
-
if (sp) {
|
|
604
|
-
return `$searchParams->${sp.method}(${sp.args.map(emit).join(", ")})`;
|
|
605
|
-
}
|
|
606
|
-
}
|
|
607
|
-
const path = identifierPath(callee);
|
|
608
|
-
const spec = path ? MOJO_TEMPLATE_PRIMITIVES[path] : undefined;
|
|
609
|
-
if (path && spec) {
|
|
610
|
-
if (args.length === spec.arity) {
|
|
611
|
-
return spec.emit(args.map(emit));
|
|
612
|
-
}
|
|
613
|
-
this.ctx._recordExprBF101(`templatePrimitive '${path}' expects ${spec.arity} arg(s), got ${args.length}`, `Call '${path}' with exactly ${spec.arity} argument(s).`);
|
|
614
|
-
return "''";
|
|
615
|
-
}
|
|
616
|
-
return emit(callee);
|
|
617
|
-
}
|
|
618
|
-
unary(op, argument, emit) {
|
|
619
|
-
const arg = emit(argument);
|
|
620
|
-
if (op === "!")
|
|
621
|
-
return `!${arg}`;
|
|
622
|
-
if (op === "-")
|
|
623
|
-
return `-${arg}`;
|
|
624
|
-
return arg;
|
|
625
|
-
}
|
|
626
|
-
binary(op, left, right, emit) {
|
|
627
|
-
const l = groupBinaryOperand(left, emit(left));
|
|
628
|
-
const r = groupBinaryOperand(right, emit(right));
|
|
629
|
-
const isStr = (e) => isStringTypedOperand(e, (n) => this.ctx._isStringValueName(n));
|
|
630
|
-
const stringCmp = isStr(left) || isStr(right);
|
|
631
|
-
if ((op === "===" || op === "==") && stringCmp) {
|
|
632
|
-
return `${l} eq ${r}`;
|
|
633
|
-
}
|
|
634
|
-
if ((op === "!==" || op === "!=") && stringCmp) {
|
|
635
|
-
return `${l} ne ${r}`;
|
|
636
|
-
}
|
|
637
|
-
if (isStringConcatBinary(op, left, right, (n) => this.ctx._isStringValueName(n))) {
|
|
638
|
-
return `${l} . ${r}`;
|
|
639
|
-
}
|
|
640
|
-
const opMap = {
|
|
641
|
-
"===": "==",
|
|
642
|
-
"!==": "!=",
|
|
643
|
-
">": ">",
|
|
644
|
-
"<": "<",
|
|
645
|
-
">=": ">=",
|
|
646
|
-
"<=": "<=",
|
|
647
|
-
"+": "+",
|
|
648
|
-
"-": "-",
|
|
649
|
-
"*": "*"
|
|
650
|
-
};
|
|
651
|
-
return `${l} ${opMap[op] ?? op} ${r}`;
|
|
652
|
-
}
|
|
653
|
-
logical(op, left, right, emit) {
|
|
654
|
-
const l = emit(left);
|
|
655
|
-
const r = emit(right);
|
|
656
|
-
if (op === "&&")
|
|
657
|
-
return `(${l} && ${r})`;
|
|
658
|
-
if (op === "||")
|
|
659
|
-
return `(${l} || ${r})`;
|
|
660
|
-
return `(${l} // ${r})`;
|
|
661
|
-
}
|
|
662
|
-
callbackMethod(method, object, arrow, restArgs, emit) {
|
|
663
|
-
const recv = emit(object);
|
|
664
|
-
const body = arrow.body;
|
|
665
|
-
const params = arrow.params;
|
|
666
|
-
if (method === "sort" || method === "toSorted") {
|
|
667
|
-
const evalForm = renderSortEval(recv, body, params, emit);
|
|
668
|
-
if (evalForm !== null)
|
|
669
|
-
return evalForm;
|
|
670
|
-
const c = sortComparatorFromArrow(arrow);
|
|
671
|
-
if (c !== null)
|
|
672
|
-
return renderSortMethod(recv, c);
|
|
673
|
-
this.ctx._recordExprBF101(`.${method}(...) comparator is not lowerable to a template sort`, `Pre-sort the array in the route handler, or mark the loop @client-only.`);
|
|
674
|
-
return "''";
|
|
675
|
-
}
|
|
676
|
-
if (method === "reduce" || method === "reduceRight") {
|
|
677
|
-
const direction = method === "reduceRight" ? "right" : "left";
|
|
678
|
-
const init = restArgs[0];
|
|
679
|
-
const evalForm = init !== undefined ? renderReduceEval(recv, body, params, init, direction, emit) : null;
|
|
680
|
-
if (evalForm !== null)
|
|
681
|
-
return evalForm;
|
|
682
|
-
this.ctx._recordExprBF101(`.${method}(...) is not lowerable to a template fold`, `Pre-compute the fold in the route handler, or mark the loop @client-only.`);
|
|
683
|
-
return "''";
|
|
684
|
-
}
|
|
685
|
-
if (method === "flatMap") {
|
|
686
|
-
const evalForm = renderFlatMapEval(recv, body, params[0], emit);
|
|
687
|
-
if (evalForm !== null)
|
|
688
|
-
return evalForm;
|
|
689
|
-
this.ctx._recordExprBF101(`.flatMap(...) projection is not lowerable to a template flat-map`, `Pre-compute the projection in the route handler, or mark the loop @client-only.`);
|
|
690
|
-
return "''";
|
|
691
|
-
}
|
|
692
|
-
if (method === "map") {
|
|
693
|
-
const evalForm = renderMapEval(recv, body, params[0], emit);
|
|
694
|
-
if (evalForm !== null)
|
|
695
|
-
return evalForm;
|
|
696
|
-
this.ctx._recordExprBF101(`.map(...) projection is not lowerable to a template map`, `Pre-compute the projection in the route handler, or mark the position @client-only.`);
|
|
697
|
-
return "''";
|
|
698
|
-
}
|
|
699
|
-
const cb = {
|
|
700
|
-
method,
|
|
701
|
-
object,
|
|
702
|
-
param: params[0],
|
|
703
|
-
predicate: body
|
|
704
|
-
};
|
|
705
|
-
return this.renderPredicate(cb, recv, emit);
|
|
706
|
-
}
|
|
707
|
-
renderPredicate(cb, arrayExpr, emit) {
|
|
708
|
-
const { method, param, predicate } = cb;
|
|
709
|
-
const evalFn = {
|
|
710
|
-
filter: ["filter_eval"],
|
|
711
|
-
every: ["every_eval"],
|
|
712
|
-
some: ["some_eval"],
|
|
713
|
-
find: ["find_eval", true],
|
|
714
|
-
findLast: ["find_eval", false],
|
|
715
|
-
findIndex: ["find_index_eval", true],
|
|
716
|
-
findLastIndex: ["find_index_eval", false]
|
|
717
|
-
};
|
|
718
|
-
const isIdentity = method === "filter" && predicate.kind === "identifier" && predicate.name === param;
|
|
719
|
-
const spec = evalFn[method];
|
|
720
|
-
if (spec && !isIdentity) {
|
|
721
|
-
const evalForm = renderPredicateEval(spec[0], arrayExpr, predicate, param, emit, spec[1]);
|
|
722
|
-
if (evalForm !== null)
|
|
723
|
-
return evalForm;
|
|
724
|
-
}
|
|
725
|
-
const predBody = this.ctx._renderPerlFilterExprPublic(predicate, param);
|
|
726
|
-
const grepBody = predBody.replace(new RegExp(`\\$${param}\\b`, "g"), "$_");
|
|
727
|
-
if (method === "filter")
|
|
728
|
-
return `[grep { ${grepBody} } @{${arrayExpr}}]`;
|
|
729
|
-
if (method === "every")
|
|
730
|
-
return `!(grep { !(${grepBody}) } @{${arrayExpr}})`;
|
|
731
|
-
if (method === "some")
|
|
732
|
-
return `!!(grep { ${grepBody} } @{${arrayExpr}})`;
|
|
733
|
-
const findHelper = {
|
|
734
|
-
find: "find",
|
|
735
|
-
findIndex: "find_index",
|
|
736
|
-
findLast: "find_last",
|
|
737
|
-
findLastIndex: "find_last_index"
|
|
738
|
-
};
|
|
739
|
-
if (findHelper[method]) {
|
|
740
|
-
return `bf->${findHelper[method]}(${arrayExpr}, sub { my $${param} = $_[0]; ${predBody} })`;
|
|
741
|
-
}
|
|
742
|
-
return arrayExpr;
|
|
743
|
-
}
|
|
744
|
-
arrayLiteral(elements, emit) {
|
|
745
|
-
return `[${elements.map(emit).join(", ")}]`;
|
|
746
|
-
}
|
|
747
|
-
arrayMethod(method, object, args, emit) {
|
|
748
|
-
return renderArrayMethod(method, object, args, emit);
|
|
749
|
-
}
|
|
750
|
-
flatMethod(object, depth, emit) {
|
|
751
|
-
return renderFlatMethod(emit(object), depth, emit);
|
|
752
|
-
}
|
|
753
|
-
conditional(test, consequent, alternate, emit) {
|
|
754
|
-
return `(${emit(test)} ? ${emit(consequent)} : ${emit(alternate)})`;
|
|
755
|
-
}
|
|
756
|
-
templateLiteral(parts, emit) {
|
|
757
|
-
const terms = [];
|
|
758
|
-
for (const part of parts) {
|
|
759
|
-
if (part.type === "string") {
|
|
760
|
-
if (part.value !== "") {
|
|
761
|
-
terms.push(`"${part.value.replace(/[\\"$@]/g, (m) => `\\${m}`)}"`);
|
|
762
|
-
}
|
|
763
|
-
} else {
|
|
764
|
-
const rendered = emit(part.expr);
|
|
765
|
-
const needsParens = part.expr.kind === "binary" || part.expr.kind === "logical" || part.expr.kind === "conditional";
|
|
766
|
-
terms.push(needsParens ? `(${rendered})` : rendered);
|
|
767
|
-
}
|
|
768
|
-
}
|
|
769
|
-
if (terms.length === 0)
|
|
770
|
-
return '""';
|
|
771
|
-
return terms.join(" . ");
|
|
772
|
-
}
|
|
773
|
-
arrow(_params, _body, _emit) {
|
|
774
|
-
return "''";
|
|
775
|
-
}
|
|
776
|
-
regex(_raw) {
|
|
777
|
-
return "''";
|
|
778
|
-
}
|
|
779
|
-
unsupported(_raw, _reason) {
|
|
780
|
-
return "''";
|
|
781
|
-
}
|
|
782
|
-
objectLiteral(properties, _raw, _emit) {
|
|
783
|
-
return properties.length === 0 ? "{}" : "''";
|
|
784
|
-
}
|
|
785
|
-
}
|
|
786
|
-
|
|
787
|
-
// src/adapter/analysis/component-tree.ts
|
|
788
|
-
function hasClientInteractivity(ir) {
|
|
789
|
-
return ir.metadata.signals.length > 0 || ir.metadata.effects.length > 0 || ir.metadata.onMounts.length > 0 || (ir.metadata.clientAnalysis?.needsInit ?? false);
|
|
790
|
-
}
|
|
791
|
-
function collectImportedLoopChildComponentErrors(ir, componentName) {
|
|
792
|
-
const errors = [];
|
|
793
|
-
const relativeImports = new Set;
|
|
794
|
-
for (const imp of ir.metadata.templateImports ?? ir.metadata.imports ?? []) {
|
|
795
|
-
if (!imp.source.startsWith("./") && !imp.source.startsWith("../"))
|
|
796
|
-
continue;
|
|
797
|
-
if (imp.isTypeOnly)
|
|
798
|
-
continue;
|
|
799
|
-
for (const spec of imp.specifiers) {
|
|
800
|
-
relativeImports.add(spec.alias ?? spec.name);
|
|
801
|
-
}
|
|
802
|
-
}
|
|
803
|
-
if (relativeImports.size === 0)
|
|
804
|
-
return errors;
|
|
805
|
-
const loc = { file: componentName + ".tsx", start: { line: 1, column: 0 }, end: { line: 1, column: 0 } };
|
|
806
|
-
const visit = (node, inLoop) => {
|
|
807
|
-
switch (node.type) {
|
|
808
|
-
case "component": {
|
|
809
|
-
const comp = node;
|
|
810
|
-
if (inLoop && relativeImports.has(comp.name)) {
|
|
811
|
-
errors.push({
|
|
812
|
-
code: "BF103",
|
|
813
|
-
severity: "error",
|
|
814
|
-
message: `Component <${comp.name}> is imported from a sibling module and used inside a loop. The Mojo adapter emits a cross-template call; the child template must be registered alongside the parent at render time.`,
|
|
815
|
-
loc: comp.loc ?? loc,
|
|
816
|
-
suggestion: {
|
|
817
|
-
message: `Options:
|
|
818
|
-
` + ` 1. Compile '${comp.name}' (its source file) with the same adapter and register the resulting Mojo template alongside the parent at render time.
|
|
819
|
-
` + ` 2. Inline <${comp.name}> directly inside the loop body so no cross-file template lookup is needed.
|
|
820
|
-
` + ` 3. Mark the loop position as @client-only so the template is materialised on the client instead of at SSR time.`
|
|
821
|
-
}
|
|
822
|
-
});
|
|
823
|
-
}
|
|
824
|
-
for (const child of comp.children)
|
|
825
|
-
visit(child, inLoop);
|
|
826
|
-
break;
|
|
827
|
-
}
|
|
828
|
-
case "element":
|
|
829
|
-
for (const child of node.children)
|
|
830
|
-
visit(child, inLoop);
|
|
831
|
-
break;
|
|
832
|
-
case "fragment":
|
|
833
|
-
for (const child of node.children)
|
|
834
|
-
visit(child, inLoop);
|
|
835
|
-
break;
|
|
836
|
-
case "conditional": {
|
|
837
|
-
const cond = node;
|
|
838
|
-
visit(cond.whenTrue, inLoop);
|
|
839
|
-
if (cond.whenFalse)
|
|
840
|
-
visit(cond.whenFalse, inLoop);
|
|
841
|
-
break;
|
|
842
|
-
}
|
|
843
|
-
case "loop":
|
|
844
|
-
for (const child of node.children)
|
|
845
|
-
visit(child, true);
|
|
846
|
-
break;
|
|
847
|
-
case "if-statement": {
|
|
848
|
-
const stmt = node;
|
|
849
|
-
visit(stmt.consequent, inLoop);
|
|
850
|
-
if (stmt.alternate)
|
|
851
|
-
visit(stmt.alternate, inLoop);
|
|
852
|
-
break;
|
|
853
|
-
}
|
|
854
|
-
case "provider":
|
|
855
|
-
for (const child of node.children)
|
|
856
|
-
visit(child, inLoop);
|
|
857
|
-
break;
|
|
858
|
-
case "async": {
|
|
859
|
-
const a = node;
|
|
860
|
-
visit(a.fallback, inLoop);
|
|
861
|
-
for (const child of a.children)
|
|
862
|
-
visit(child, inLoop);
|
|
863
|
-
break;
|
|
864
|
-
}
|
|
865
|
-
}
|
|
866
|
-
};
|
|
867
|
-
visit(ir.root, false);
|
|
868
|
-
return errors;
|
|
869
|
-
}
|
|
870
|
-
|
|
871
|
-
// src/adapter/spread/spread-codegen.ts
|
|
872
|
-
import ts from "typescript";
|
|
873
|
-
import { parseRecordIndexAccess, stringifyParsedExpr } from "@barefootjs/jsx";
|
|
874
|
-
function conditionalSpreadToPerl(ctx, expr) {
|
|
875
|
-
if (!expr || expr.kind !== "conditional")
|
|
876
|
-
return null;
|
|
877
|
-
const whenTrue = expr.consequent;
|
|
878
|
-
const whenFalse = expr.alternate;
|
|
879
|
-
if (whenTrue.kind !== "object-literal" || whenFalse.kind !== "object-literal") {
|
|
880
|
-
return null;
|
|
881
|
-
}
|
|
882
|
-
const condPerl = ctx.convertExpressionToPerl("", expr.test);
|
|
883
|
-
const truePerl = objectLiteralToPerlHashref(ctx, whenTrue);
|
|
884
|
-
const falsePerl = objectLiteralToPerlHashref(ctx, whenFalse);
|
|
885
|
-
if (truePerl === null || falsePerl === null)
|
|
886
|
-
return null;
|
|
887
|
-
return `${condPerl} ? ${truePerl} : ${falsePerl}`;
|
|
888
|
-
}
|
|
889
|
-
function objectLiteralExprToPerlHashref(ctx, expr) {
|
|
890
|
-
if (!expr || expr.kind !== "object-literal")
|
|
891
|
-
return null;
|
|
892
|
-
return objectLiteralToPerlHashref(ctx, expr);
|
|
893
|
-
}
|
|
894
|
-
function objectLiteralToPerlHashref(ctx, obj) {
|
|
895
|
-
const entries = [];
|
|
896
|
-
for (const prop of obj.properties) {
|
|
897
|
-
if (prop.shorthand)
|
|
898
|
-
return null;
|
|
899
|
-
if (prop.keyKind === "numeric")
|
|
900
|
-
return null;
|
|
901
|
-
const key = prop.key;
|
|
902
|
-
const val = prop.value;
|
|
903
|
-
const indexed = recordIndexAccessToPerl(ctx, val);
|
|
904
|
-
if (indexed === null && val.kind === "index-access" && !isLiteralIndex(val.index)) {
|
|
905
|
-
ctx.errors.push({
|
|
906
|
-
code: "BF101",
|
|
907
|
-
severity: "error",
|
|
908
|
-
message: `Spread object value '${stringifyParsedExpr(val)}' indexes a record map whose values aren't scalar literals — it can't lower to an inline Perl hashref.`,
|
|
909
|
-
loc: { file: ctx.componentName + ".tsx", start: { line: 1, column: 0 }, end: { line: 1, column: 0 } },
|
|
910
|
-
suggestion: {
|
|
911
|
-
message: "Index a record whose values are number/string literals, or move the spread into a `'use client'` component so hydration computes it."
|
|
912
|
-
}
|
|
913
|
-
});
|
|
914
|
-
return null;
|
|
915
|
-
}
|
|
916
|
-
const valPerl = indexed !== null ? indexed : ctx.convertExpressionToPerl("", val);
|
|
917
|
-
entries.push(`'${key.replace(/'/g, "\\'")}' => ${valPerl}`);
|
|
918
|
-
}
|
|
919
|
-
return entries.length === 0 ? "{}" : `{ ${entries.join(", ")} }`;
|
|
920
|
-
}
|
|
921
|
-
function isLiteralIndex(index) {
|
|
922
|
-
return index.kind === "literal" && (index.literalType === "number" || index.literalType === "string");
|
|
923
|
-
}
|
|
924
|
-
function recordIndexAccessToPerl(ctx, val) {
|
|
925
|
-
if (val.kind !== "index-access" || val.object.kind !== "identifier" || val.index.kind !== "identifier") {
|
|
926
|
-
return null;
|
|
927
|
-
}
|
|
928
|
-
const tsVal = ts.factory.createElementAccessExpression(ts.factory.createIdentifier(val.object.name), ts.factory.createIdentifier(val.index.name));
|
|
929
|
-
const parsed = parseRecordIndexAccess(tsVal, ctx.localConstants, ctx.propsParams);
|
|
930
|
-
if (!parsed)
|
|
931
|
-
return null;
|
|
932
|
-
const entries = parsed.entries.map((e) => {
|
|
933
|
-
const mapVal = e.value.kind === "number" ? e.value.text : `'${e.value.text.replace(/'/g, "\\'")}'`;
|
|
934
|
-
return `'${e.key.replace(/'/g, "\\'")}' => ${mapVal}`;
|
|
935
|
-
});
|
|
936
|
-
return `{ ${entries.join(", ")} }->{$${parsed.indexPropName}}`;
|
|
937
|
-
}
|
|
938
|
-
|
|
939
|
-
// src/adapter/memo/seed.ts
|
|
940
|
-
import {
|
|
941
|
-
collectContextConsumers,
|
|
942
|
-
computeSsrSeedPlan
|
|
943
|
-
} from "@barefootjs/jsx";
|
|
944
|
-
function contextDefaultPerl(c) {
|
|
945
|
-
const d = c.defaultValue;
|
|
946
|
-
if (d === null || d === undefined)
|
|
947
|
-
return "undef";
|
|
948
|
-
if (typeof d === "string")
|
|
949
|
-
return `'${d.replace(/[\\']/g, (m) => `\\${m}`)}'`;
|
|
950
|
-
if (typeof d === "boolean")
|
|
951
|
-
return d ? "1" : "0";
|
|
952
|
-
return String(d);
|
|
953
|
-
}
|
|
954
|
-
function generateContextConsumerSeed(ir) {
|
|
955
|
-
const consumers = collectContextConsumers(ir.metadata);
|
|
956
|
-
if (consumers.length === 0)
|
|
957
|
-
return "";
|
|
958
|
-
return consumers.map((c) => `% my $${c.localName} = bf->use_context('${c.contextName}', ${contextDefaultPerl(c)});`).join(`
|
|
959
|
-
`) + `
|
|
960
|
-
`;
|
|
961
|
-
}
|
|
962
|
-
function generateDerivedMemoSeed(ctx, ir) {
|
|
963
|
-
const plan = ir.metadata.ssrSeedPlan ?? computeSsrSeedPlan(ir.metadata);
|
|
964
|
-
const lines = [];
|
|
965
|
-
for (const step of plan.steps) {
|
|
966
|
-
if (step.kind !== "derived")
|
|
967
|
-
continue;
|
|
968
|
-
const perl = ctx.convertExpressionToPerl(step.expr, step.parsed);
|
|
969
|
-
if (perl === "" || !/\$[A-Za-z_]\w*/.test(perl))
|
|
970
|
-
continue;
|
|
971
|
-
lines.push(`% my $${step.name} = ${perl};`);
|
|
972
|
-
}
|
|
973
|
-
return lines.length > 0 ? lines.join(`
|
|
974
|
-
`) + `
|
|
975
|
-
` : "";
|
|
976
|
-
}
|
|
977
|
-
|
|
978
|
-
// src/adapter/props/prop-classes.ts
|
|
979
|
-
import { collectLoopBoundNames } from "@barefootjs/jsx";
|
|
980
|
-
|
|
981
|
-
// src/adapter/value/parsed-literal.ts
|
|
982
|
-
function isStringTypeInfo(type) {
|
|
983
|
-
return type?.kind === "primitive" && type.primitive === "string";
|
|
984
|
-
}
|
|
985
|
-
function isBareStringLiteral(initialValue) {
|
|
986
|
-
if (!initialValue)
|
|
987
|
-
return false;
|
|
988
|
-
const v = initialValue.trim();
|
|
989
|
-
return v.startsWith("'") && v.endsWith("'") || v.startsWith('"') && v.endsWith('"');
|
|
990
|
-
}
|
|
991
|
-
|
|
992
|
-
// src/adapter/props/prop-classes.ts
|
|
993
|
-
function collectProviderDataNames(ir) {
|
|
994
|
-
return new Set([
|
|
995
|
-
...ir.metadata.propsParams.map((p) => p.name),
|
|
996
|
-
...(ir.metadata.signals ?? []).map((s) => s.getter),
|
|
997
|
-
...(ir.metadata.memos ?? []).map((m) => m.name)
|
|
998
|
-
]);
|
|
999
|
-
}
|
|
1000
|
-
function collectBooleanTypedProps(ir) {
|
|
1001
|
-
return new Set(ir.metadata.propsParams.filter((prop) => prop.type?.primitive === "boolean" || prop.type?.raw === "boolean").map((prop) => prop.name));
|
|
1002
|
-
}
|
|
1003
|
-
function collectNullableOptionalProps(ir) {
|
|
1004
|
-
return new Set(ir.metadata.propsParams.filter((p) => p.defaultValue === undefined && !p.isRest && (p.type?.kind !== "primitive" || p.optional)).map((p) => p.name));
|
|
1005
|
-
}
|
|
1006
|
-
function collectStringValueNames(ir) {
|
|
1007
|
-
const names = new Set;
|
|
1008
|
-
for (const s of ir.metadata.signals) {
|
|
1009
|
-
if (isStringTypeInfo(s.type) || isBareStringLiteral(s.initialValue)) {
|
|
1010
|
-
names.add(s.getter);
|
|
1011
|
-
}
|
|
1012
|
-
}
|
|
1013
|
-
for (const p of ir.metadata.propsParams) {
|
|
1014
|
-
if (isStringTypeInfo(p.type))
|
|
1015
|
-
names.add(p.name);
|
|
1016
|
-
}
|
|
1017
|
-
for (const c of ir.metadata.localConstants) {
|
|
1018
|
-
if (isStringTypeInfo(c.type ?? undefined) || isBareStringLiteral(c.value))
|
|
1019
|
-
names.add(c.name);
|
|
1020
|
-
}
|
|
1021
|
-
for (const bound of collectLoopBoundNames(ir))
|
|
1022
|
-
names.delete(bound);
|
|
1023
|
-
return names;
|
|
1024
|
-
}
|
|
1025
|
-
|
|
1026
|
-
// src/adapter/mojo-adapter.ts
|
|
1027
|
-
function perlSegmentAccessor(base, segments) {
|
|
1028
|
-
let expr = base;
|
|
1029
|
-
for (const seg of segments) {
|
|
1030
|
-
expr += seg.kind === "field" ? `->{${seg.isIdent ? seg.key : perlHashKey(seg.key)}}` : `->[${seg.index}]`;
|
|
1031
|
-
}
|
|
1032
|
-
return expr;
|
|
1033
|
-
}
|
|
1034
|
-
function perlStringLiteral(s) {
|
|
1035
|
-
return `'${s.replace(/\\/g, "\\\\").replace(/'/g, "\\'")}'`;
|
|
1036
|
-
}
|
|
1037
|
-
|
|
1038
|
-
class MojoAdapter extends BaseAdapter {
|
|
1039
|
-
name = "mojolicious";
|
|
1040
|
-
extension = ".html.ep";
|
|
1041
|
-
templatesPerComponent = true;
|
|
1042
|
-
importMapInjection = "html-snippet";
|
|
1043
|
-
templatePrimitives = MOJO_PRIMITIVE_EMIT_MAP;
|
|
1044
|
-
componentName = "";
|
|
1045
|
-
rootScopeNodes = new Set;
|
|
1046
|
-
options;
|
|
1047
|
-
errors = [];
|
|
1048
|
-
currentLoopKeyDepth = 0;
|
|
1049
|
-
propsObjectName = null;
|
|
1050
|
-
propsParams = [];
|
|
1051
|
-
booleanTypedProps = new Set;
|
|
1052
|
-
providerDataNames = new Set;
|
|
1053
|
-
stringValueNames = new Set;
|
|
1054
|
-
_searchParamsLocals = new Set;
|
|
1055
|
-
_loweringMatchers = [];
|
|
1056
|
-
moduleStringConsts = new Map;
|
|
1057
|
-
localConstants = [];
|
|
1058
|
-
loopBoundNames = new Map;
|
|
1059
|
-
nullableOptionalProps = new Set;
|
|
1060
|
-
constructor(options = {}) {
|
|
1061
|
-
super();
|
|
1062
|
-
this.options = {
|
|
1063
|
-
clientJsBasePath: options.clientJsBasePath ?? "/static/components/",
|
|
1064
|
-
barefootJsPath: options.barefootJsPath ?? "/static/components/barefoot.js"
|
|
1065
|
-
};
|
|
1066
|
-
}
|
|
1067
|
-
generate(ir, options) {
|
|
1068
|
-
this.componentName = ir.metadata.componentName;
|
|
1069
|
-
this.propsObjectName = ir.metadata.propsObjectName ?? null;
|
|
1070
|
-
augmentInheritedPropAccesses(ir);
|
|
1071
|
-
this.propsParams = ir.metadata.propsParams.map((p) => ({ name: p.name }));
|
|
1072
|
-
this.providerDataNames = collectProviderDataNames(ir);
|
|
1073
|
-
this.booleanTypedProps = collectBooleanTypedProps(ir);
|
|
1074
|
-
this.nullableOptionalProps = collectNullableOptionalProps(ir);
|
|
1075
|
-
this.stringValueNames = collectStringValueNames(ir);
|
|
1076
|
-
this.moduleStringConsts = collectModuleStringConsts(ir.metadata.localConstants);
|
|
1077
|
-
this._searchParamsLocals = searchParamsLocalNames(ir.metadata);
|
|
1078
|
-
this._loweringMatchers = prepareLoweringMatchers(ir.metadata);
|
|
1079
|
-
this.localConstants = ir.metadata.localConstants ?? [];
|
|
1080
|
-
this.loopBoundNames.clear();
|
|
1081
|
-
this.errors = [];
|
|
1082
|
-
this.childrenCaptureCounter = 0;
|
|
1083
|
-
if (!options?.siblingTemplatesRegistered) {
|
|
1084
|
-
this.errors.push(...collectImportedLoopChildComponentErrors(ir, this.componentName));
|
|
1085
|
-
}
|
|
1086
|
-
this.rootScopeNodes = collectRootScopeNodes(ir.root);
|
|
1087
|
-
const templateBody = ir.root.type === "if-statement" ? this.renderIfStatement(ir.root) : this.renderNode(ir.root);
|
|
1088
|
-
const scriptReg = options?.skipScriptRegistration ? "" : this.generateScriptRegistrations(ir, options?.scriptBaseName);
|
|
1089
|
-
const ctxSeed = generateContextConsumerSeed(ir);
|
|
1090
|
-
const memoSeed = generateDerivedMemoSeed(this.memoCtx, ir);
|
|
1091
|
-
const template = `${scriptReg}${ctxSeed}${memoSeed}${templateBody}
|
|
1092
|
-
`;
|
|
1093
|
-
if (this.errors.length > 0) {
|
|
1094
|
-
ir.errors.push(...this.errors);
|
|
1095
|
-
}
|
|
1096
|
-
const sections = {
|
|
1097
|
-
imports: "",
|
|
1098
|
-
types: "",
|
|
1099
|
-
component: template,
|
|
1100
|
-
defaultExport: ""
|
|
1101
|
-
};
|
|
1102
|
-
return {
|
|
1103
|
-
template,
|
|
1104
|
-
sections,
|
|
1105
|
-
extension: this.extension
|
|
1106
|
-
};
|
|
1107
|
-
}
|
|
1108
|
-
isBooleanTypedPropRef(expr) {
|
|
1109
|
-
let bare = expr.trim();
|
|
1110
|
-
if (this.propsObjectName && bare.startsWith(`${this.propsObjectName}.`)) {
|
|
1111
|
-
bare = bare.slice(this.propsObjectName.length + 1);
|
|
1112
|
-
}
|
|
1113
|
-
if (!/^[A-Za-z_$][\w$]*$/.test(bare))
|
|
1114
|
-
return false;
|
|
1115
|
-
if (this.loopBoundNames.has(bare))
|
|
1116
|
-
return false;
|
|
1117
|
-
return this.booleanTypedProps.has(bare);
|
|
1118
|
-
}
|
|
1119
|
-
parseUndefinedAlternateTernary(expr) {
|
|
1120
|
-
const parsed = parseExpression2(expr.trim());
|
|
1121
|
-
if (parsed?.kind !== "conditional")
|
|
1122
|
-
return null;
|
|
1123
|
-
const alt = parsed.alternate;
|
|
1124
|
-
const isUndef = alt.kind === "identifier" && (alt.name === "undefined" || alt.name === "null") || alt.kind === "literal" && (alt.value === null || alt.value === undefined);
|
|
1125
|
-
if (!isUndef)
|
|
1126
|
-
return null;
|
|
1127
|
-
return {
|
|
1128
|
-
condition: exprToString(parsed.test),
|
|
1129
|
-
consequent: exprToString(parsed.consequent)
|
|
1130
|
-
};
|
|
1131
|
-
}
|
|
1132
|
-
resolveLiteralConst(name) {
|
|
1133
|
-
if (this.loopBoundNames?.has?.(name))
|
|
1134
|
-
return null;
|
|
1135
|
-
const c = (this.localConstants ?? []).find((lc) => lc.name === name);
|
|
1136
|
-
if (c?.value === undefined)
|
|
1137
|
-
return null;
|
|
1138
|
-
const v = c.value.trim();
|
|
1139
|
-
if (/^-?\d+(\.\d+)?$/.test(v))
|
|
1140
|
-
return v;
|
|
1141
|
-
const strLit = /^'([^'\\]*)'$/.exec(v) ?? /^"([^"\\]*)"$/.exec(v);
|
|
1142
|
-
if (strLit)
|
|
1143
|
-
return `'${strLit[1].replace(/[\\']/g, (m) => `\\${m}`)}'`;
|
|
1144
|
-
return null;
|
|
1145
|
-
}
|
|
1146
|
-
resolveStaticRecordLiteral(objectName, key) {
|
|
1147
|
-
if (this.loopBoundNames?.has?.(objectName))
|
|
1148
|
-
return null;
|
|
1149
|
-
const hit = lookupStaticRecordLiteral(objectName, key, this.localConstants);
|
|
1150
|
-
if (!hit)
|
|
1151
|
-
return null;
|
|
1152
|
-
return hit.kind === "number" ? hit.text : `'${hit.text.replace(/[\\']/g, (m) => `\\${m}`)}'`;
|
|
1153
|
-
}
|
|
1154
|
-
resolveModuleStringConst(name) {
|
|
1155
|
-
if (this.loopBoundNames.has(name))
|
|
1156
|
-
return null;
|
|
1157
|
-
const value = this.moduleStringConsts.get(name);
|
|
1158
|
-
if (value === undefined)
|
|
1159
|
-
return null;
|
|
1160
|
-
return `'${value.replace(/[\\']/g, (m) => `\\${m}`)}'`;
|
|
1161
|
-
}
|
|
1162
|
-
generateScriptRegistrations(ir, scriptBaseName) {
|
|
1163
|
-
const hasInteractivity = hasClientInteractivity(ir);
|
|
1164
|
-
if (!hasInteractivity)
|
|
1165
|
-
return "";
|
|
1166
|
-
const name = scriptBaseName ?? ir.metadata.componentName;
|
|
1167
|
-
const runtimePath = this.options.barefootJsPath;
|
|
1168
|
-
const clientJsPath = `${this.options.clientJsBasePath}${name}.client.js`;
|
|
1169
|
-
const lines = [];
|
|
1170
|
-
lines.push(`% bf->register_script('${runtimePath}');`);
|
|
1171
|
-
lines.push(`% bf->register_script('${clientJsPath}');`);
|
|
1172
|
-
lines.push("");
|
|
1173
|
-
return lines.join(`
|
|
1174
|
-
`);
|
|
1175
|
-
}
|
|
1176
|
-
renderNode(node) {
|
|
1177
|
-
return emitIRNode(node, this, {});
|
|
1178
|
-
}
|
|
1179
|
-
emitElement(node, _ctx, _emit) {
|
|
1180
|
-
return this.renderElement(node);
|
|
1181
|
-
}
|
|
1182
|
-
emitText(node) {
|
|
1183
|
-
return escapeHtml(node.value);
|
|
1184
|
-
}
|
|
1185
|
-
emitExpression(node) {
|
|
1186
|
-
return this.renderExpression(node);
|
|
1187
|
-
}
|
|
1188
|
-
emitConditional(node, _ctx, _emit) {
|
|
1189
|
-
return this.renderConditional(node);
|
|
1190
|
-
}
|
|
1191
|
-
emitLoop(node, _ctx, _emit) {
|
|
1192
|
-
return this.renderLoop(node);
|
|
1193
|
-
}
|
|
1194
|
-
emitComponent(node, _ctx, _emit) {
|
|
1195
|
-
return this.renderComponent(node);
|
|
1196
|
-
}
|
|
1197
|
-
emitFragment(node, _ctx, _emit) {
|
|
1198
|
-
return this.renderFragment(node);
|
|
1199
|
-
}
|
|
1200
|
-
emitSlot(node) {
|
|
1201
|
-
return this.renderSlot(node);
|
|
1202
|
-
}
|
|
1203
|
-
emitIfStatement(node, _ctx, _emit) {
|
|
1204
|
-
return this.renderIfStatement(node);
|
|
1205
|
-
}
|
|
1206
|
-
emitProvider(node, _ctx, _emit) {
|
|
1207
|
-
const value = this.providerValuePerl(node.valueProp);
|
|
1208
|
-
const children = this.renderChildren(node.children);
|
|
1209
|
-
const name = node.contextName;
|
|
1210
|
-
return `<% bf->provide_context('${name}', ${value}); %>` + children + `<% bf->revoke_context('${name}'); %>`;
|
|
1211
|
-
}
|
|
1212
|
-
providerValuePerl(valueProp) {
|
|
1213
|
-
const v = valueProp.value;
|
|
1214
|
-
if (v.kind === "literal") {
|
|
1215
|
-
return typeof v.value === "string" ? `'${v.value.replace(/[\\']/g, (m) => `\\${m}`)}'` : String(v.value);
|
|
1216
|
-
}
|
|
1217
|
-
if (v.kind === "expression") {
|
|
1218
|
-
const hashref = this.providerObjectLiteralPerl(v.expr);
|
|
1219
|
-
if (hashref !== null)
|
|
1220
|
-
return hashref;
|
|
1221
|
-
return this.convertExpressionToPerl(v.expr);
|
|
1222
|
-
}
|
|
1223
|
-
if (v.kind === "template")
|
|
1224
|
-
return this.convertTemplateLiteralPartsToPerl(v.parts);
|
|
1225
|
-
return "undef";
|
|
1226
|
-
}
|
|
1227
|
-
providerObjectLiteralPerl(expr) {
|
|
1228
|
-
const members = parseProviderObjectLiteral(expr.trim());
|
|
1229
|
-
if (members === null)
|
|
1230
|
-
return null;
|
|
1231
|
-
const entries = members.map((m) => {
|
|
1232
|
-
const key = `'${m.name.replace(/[\\']/g, (c) => `\\${c}`)}'`;
|
|
1233
|
-
if (m.kind === "function" || /^on[A-Z]/.test(m.name))
|
|
1234
|
-
return `${key} => undef`;
|
|
1235
|
-
const src = m.kind === "getter" ? m.body : m.expr;
|
|
1236
|
-
if (this.isClientOnlyContextIdentifier(src))
|
|
1237
|
-
return `${key} => undef`;
|
|
1238
|
-
return `${key} => ${this.convertExpressionToPerl(src)}`;
|
|
1239
|
-
});
|
|
1240
|
-
return `{ ${entries.join(", ")} }`;
|
|
1241
|
-
}
|
|
1242
|
-
isClientOnlyContextIdentifier(src) {
|
|
1243
|
-
const t = src.trim();
|
|
1244
|
-
if (!/^[A-Za-z_$][\w$]*$/.test(t))
|
|
1245
|
-
return false;
|
|
1246
|
-
return !this.providerDataNames.has(t) && !this.moduleStringConsts.has(t);
|
|
1247
|
-
}
|
|
1248
|
-
emitAsync(node, _ctx, _emit) {
|
|
1249
|
-
return this.renderAsync(node);
|
|
1250
|
-
}
|
|
1251
|
-
renderElement(element) {
|
|
1252
|
-
const tag = element.tag;
|
|
1253
|
-
const attrs = this.renderAttributes(element);
|
|
1254
|
-
const dangerousHtml = this.renderDangerousInnerHtml(element);
|
|
1255
|
-
const children = dangerousHtml !== null ? dangerousHtml : this.renderChildren(element.children);
|
|
1256
|
-
let hydrationAttrs = "";
|
|
1257
|
-
if (element.needsScope) {
|
|
1258
|
-
hydrationAttrs += ` ${this.renderScopeMarker("")}`;
|
|
1259
|
-
}
|
|
1260
|
-
if (this.rootScopeNodes.has(element) && element.needsScope) {
|
|
1261
|
-
hydrationAttrs += ` <%== bf->data_key_attr %>`;
|
|
1262
|
-
}
|
|
1263
|
-
if (element.slotId) {
|
|
1264
|
-
hydrationAttrs += ` ${this.renderSlotMarker(element.slotId)}`;
|
|
1265
|
-
}
|
|
1266
|
-
if (element.regionId) {
|
|
1267
|
-
hydrationAttrs += ` ${BF_REGION}="${element.regionId}"`;
|
|
1268
|
-
}
|
|
1269
|
-
const voidElements = [
|
|
1270
|
-
"area",
|
|
1271
|
-
"base",
|
|
1272
|
-
"br",
|
|
1273
|
-
"col",
|
|
1274
|
-
"embed",
|
|
1275
|
-
"hr",
|
|
1276
|
-
"img",
|
|
1277
|
-
"input",
|
|
1278
|
-
"link",
|
|
1279
|
-
"meta",
|
|
1280
|
-
"param",
|
|
1281
|
-
"source",
|
|
1282
|
-
"track",
|
|
1283
|
-
"wbr"
|
|
1284
|
-
];
|
|
1285
|
-
if (voidElements.includes(tag.toLowerCase())) {
|
|
1286
|
-
return `<${tag}${attrs}${hydrationAttrs}>`;
|
|
1287
|
-
}
|
|
1288
|
-
return `<${tag}${attrs}${hydrationAttrs}>${children}</${tag}>`;
|
|
1289
|
-
}
|
|
1290
|
-
renderDangerousInnerHtml(element) {
|
|
1291
|
-
const resolution = resolveDangerousInnerHtml(element);
|
|
1292
|
-
if (!resolution)
|
|
1293
|
-
return null;
|
|
1294
|
-
if (resolution.kind === "unlowerable") {
|
|
1295
|
-
this.errors.push(dangerousInnerHtmlDiagnostic(resolution.expr, resolution.loc));
|
|
1296
|
-
return "";
|
|
1297
|
-
}
|
|
1298
|
-
if (resolution.kind === "dynamic") {
|
|
1299
|
-
return `<%== ${this.convertExpressionToPerl(resolution.valueExpr, resolution.valueParsed)} %>`;
|
|
1300
|
-
}
|
|
1301
|
-
const violation = dangerousInnerHtmlMetacharViolation(resolution.html, this.name);
|
|
1302
|
-
if (violation) {
|
|
1303
|
-
const attr = element.attrs.find(isDangerousInnerHtmlAttr);
|
|
1304
|
-
this.errors.push(dangerousInnerHtmlDiagnostic(`{ __html: ${JSON.stringify(resolution.html)} }`, attr.loc, violation));
|
|
1305
|
-
return "";
|
|
1306
|
-
}
|
|
1307
|
-
return resolution.html;
|
|
1308
|
-
}
|
|
1309
|
-
renderExpression(expr) {
|
|
1310
|
-
if (expr.clientOnly) {
|
|
1311
|
-
if (expr.markerless)
|
|
1312
|
-
return "";
|
|
1313
|
-
if (expr.slotId) {
|
|
1314
|
-
return `<%== bf->text_start("${expr.slotId}") %><%== bf->text_end %>`;
|
|
1315
|
-
}
|
|
1316
|
-
return "";
|
|
1317
|
-
}
|
|
1318
|
-
const perlExpr = this.convertExpressionToPerl(expr.expr, expr.parsed);
|
|
1319
|
-
if (expr.slotId) {
|
|
1320
|
-
return `<%== bf->text_start("${expr.slotId}") %><%= ${perlExpr} %><%== bf->text_end %>`;
|
|
1321
|
-
}
|
|
1322
|
-
return `<%= ${perlExpr} %>`;
|
|
1323
|
-
}
|
|
1324
|
-
renderConditional(cond) {
|
|
1325
|
-
if (cond.clientOnly && cond.slotId) {
|
|
1326
|
-
return `<%== bf->comment("cond-start:${cond.slotId}") %><%== bf->comment("cond-end:${cond.slotId}") %>`;
|
|
1327
|
-
}
|
|
1328
|
-
const condition = this.convertExpressionToPerl(cond.condition);
|
|
1329
|
-
const whenTrue = this.renderNode(cond.whenTrue);
|
|
1330
|
-
const whenFalse = this.renderNodeOrNull(cond.whenFalse);
|
|
1331
|
-
const isFragmentBranch = cond.whenTrue.type === "fragment" || cond.whenFalse.type === "fragment";
|
|
1332
|
-
const useCommentMarkers = cond.slotId && isFragmentBranch;
|
|
1333
|
-
let markedTrue = whenTrue;
|
|
1334
|
-
let markedFalse = whenFalse;
|
|
1335
|
-
if (cond.slotId && !useCommentMarkers) {
|
|
1336
|
-
markedTrue = this.addCondMarkerToFirstElement(whenTrue, cond.slotId);
|
|
1337
|
-
markedFalse = whenFalse ? this.addCondMarkerToFirstElement(whenFalse, cond.slotId) : whenFalse;
|
|
1338
|
-
}
|
|
1339
|
-
let result;
|
|
1340
|
-
if (useCommentMarkers) {
|
|
1341
|
-
const inner = whenFalse ? `
|
|
1342
|
-
% if (${condition}) {
|
|
1343
|
-
${whenTrue}
|
|
1344
|
-
% } else {
|
|
1345
|
-
${whenFalse}
|
|
1346
|
-
% }
|
|
1347
|
-
` : `
|
|
1348
|
-
% if (${condition}) {
|
|
1349
|
-
${whenTrue}
|
|
1350
|
-
% }
|
|
1351
|
-
`;
|
|
1352
|
-
result = `<%== bf->comment("cond-start:${cond.slotId}") %>${inner}<%== bf->comment("cond-end:${cond.slotId}") %>`;
|
|
1353
|
-
} else if (markedFalse) {
|
|
1354
|
-
result = `
|
|
1355
|
-
% if (${condition}) {
|
|
1356
|
-
${markedTrue}
|
|
1357
|
-
% } else {
|
|
1358
|
-
${markedFalse}
|
|
1359
|
-
% }
|
|
1360
|
-
`;
|
|
1361
|
-
} else if (cond.slotId) {
|
|
1362
|
-
result = `<%== bf->comment("cond-start:${cond.slotId}") %>
|
|
1363
|
-
% if (${condition}) {
|
|
1364
|
-
${whenTrue}
|
|
1365
|
-
% }
|
|
1366
|
-
<%== bf->comment("cond-end:${cond.slotId}") %>`;
|
|
1367
|
-
} else {
|
|
1368
|
-
result = `
|
|
1369
|
-
% if (${condition}) {
|
|
1370
|
-
${whenTrue}
|
|
1371
|
-
% }
|
|
1372
|
-
`;
|
|
1373
|
-
}
|
|
1374
|
-
return result;
|
|
1375
|
-
}
|
|
1376
|
-
renderNodeOrNull(node) {
|
|
1377
|
-
if (node.type === "expression" && (node.expr === "null" || node.expr === "undefined")) {
|
|
1378
|
-
return null;
|
|
1379
|
-
}
|
|
1380
|
-
return this.renderNode(node);
|
|
1381
|
-
}
|
|
1382
|
-
addCondMarkerToFirstElement(content, condId) {
|
|
1383
|
-
const match = content.match(/^(<\w+)([\s>])/);
|
|
1384
|
-
if (match) {
|
|
1385
|
-
return content.replace(/^(<\w+)([\s>])/, `$1 ${BF_COND}="${condId}"$2`);
|
|
1386
|
-
}
|
|
1387
|
-
return `<%== bf->comment("cond-start:${condId}") %>${content}<%== bf->comment("cond-end:${condId}") %>`;
|
|
1388
|
-
}
|
|
1389
|
-
renderLoop(loop) {
|
|
1390
|
-
if (loop.clientOnly) {
|
|
1391
|
-
return `<%== bf->comment("loop:${loop.markerId}") %><%== bf->comment("/loop:${loop.markerId}") %>`;
|
|
1392
|
-
}
|
|
1393
|
-
const destructure = !!(loop.paramBindings && loop.paramBindings.length > 0);
|
|
1394
|
-
const supportableDestructure = destructure && isLowerableLoopDestructure(loop);
|
|
1395
|
-
if (destructure && !supportableDestructure) {
|
|
1396
|
-
this.errors.push({
|
|
1397
|
-
code: "BF104",
|
|
1398
|
-
severity: "error",
|
|
1399
|
-
message: `Loop callback uses a destructure pattern (\`${loop.param}\`) that the Mojo adapter cannot lower — see the diagnostic detail for the specific shape.`,
|
|
1400
|
-
loc: loop.loc ?? { file: this.componentName + ".tsx", start: { line: 1, column: 0 }, end: { line: 1, column: 0 } },
|
|
1401
|
-
suggestion: {
|
|
1402
|
-
message: `Options:
|
|
1403
|
-
` + ` 1. If this is an object-rest binding (\`{ ...rest }\`), only reading \`rest.field\` or spreading \`{...rest}\` onto an intrinsic element lowers — other uses (passing \`rest\` to a function, rendering it as text) need the client runtime.
|
|
1404
|
-
` + ` 2. If this is chained \`.filter().map(({ ... }) => ...)\`, hoist the destructure into a variable inside the callback body instead.
|
|
1405
|
-
` + ` 3. Mark the loop position as @client-only so the destructure runs in JS on the client.
|
|
1406
|
-
` + ` 4. Move the loop into a primitive that the adapter registers explicitly.`
|
|
1407
|
-
}
|
|
1408
|
-
});
|
|
1409
|
-
}
|
|
1410
|
-
const staticItems = resolveStaticLoopSource(loop.arrayParsed, this.localConstants, {
|
|
1411
|
-
isNameShadowed: (name) => this.loopBoundNames.has(name)
|
|
1412
|
-
});
|
|
1413
|
-
const staticArray = staticItems !== null ? staticValueToPerl(staticItems) : null;
|
|
1414
|
-
const arrayName = loop.array.trim();
|
|
1415
|
-
if (staticArray === null && /^[A-Za-z_$][\w$]*$/.test(arrayName)) {
|
|
1416
|
-
const arrayConst = (this.localConstants ?? []).find((c) => c.name === arrayName);
|
|
1417
|
-
if (arrayConst && !arrayConst.isModule && this.resolveLiteralConst(arrayName) === null) {
|
|
1418
|
-
this.errors.push({
|
|
1419
|
-
code: "BF101",
|
|
1420
|
-
severity: "error",
|
|
1421
|
-
message: `Loop array \`${arrayName}\` is a local computed value (\`${arrayConst.value}\`) that the Mojo adapter cannot bind as a template variable — only numeric/string-literal locals inline at their use site.`,
|
|
1422
|
-
loc: loop.loc ?? { file: this.componentName + ".tsx", start: { line: 1, column: 0 }, end: { line: 1, column: 0 } },
|
|
1423
|
-
suggestion: {
|
|
1424
|
-
message: "Pre-compute the array server-side and pass it as a prop, or mark the loop position as @client-only so it runs in JS on the client."
|
|
1425
|
-
}
|
|
1426
|
-
});
|
|
1427
|
-
}
|
|
1428
|
-
}
|
|
1429
|
-
const rawArray = staticArray ?? this.convertExpressionToPerl(loop.array);
|
|
1430
|
-
let sortedHoist = null;
|
|
1431
|
-
let array = rawArray;
|
|
1432
|
-
if (loop.sortComparator) {
|
|
1433
|
-
sortedHoist = `bf_iter_${perlIdentifierFromMarkerId(loop.markerId)}`;
|
|
1434
|
-
array = `$${sortedHoist}`;
|
|
1435
|
-
}
|
|
1436
|
-
const param = loop.param;
|
|
1437
|
-
const indexVar = loop.iterationShape === "keys" ? `$${param}` : loop.index ? `$${loop.index}` : "$_i";
|
|
1438
|
-
const loopBound = loop.objectIteration === "entries" ? [param, loop.index ?? "_k"] : loop.objectIteration === "keys" || loop.objectIteration === "values" || loop.iterationShape === "keys" ? [param] : supportableDestructure ? ["__bf_item", ...(loop.paramBindings ?? []).map((b) => b.name), loop.index ?? "_i"] : [param, loop.index ?? "_i"];
|
|
1439
|
-
const preambleDecls = loop.preamble?.declarations ?? [];
|
|
1440
|
-
for (const d of preambleDecls)
|
|
1441
|
-
loopBound.push(d.name);
|
|
1442
|
-
for (const n of loopBound) {
|
|
1443
|
-
this.loopBoundNames.set(n, (this.loopBoundNames.get(n) ?? 0) + 1);
|
|
1444
|
-
}
|
|
1445
|
-
const prevLoopKeyDepth = this.currentLoopKeyDepth;
|
|
1446
|
-
this.currentLoopKeyDepth = loop.depth;
|
|
1447
|
-
const renderedChildren = this.renderChildren(loop.children);
|
|
1448
|
-
this.currentLoopKeyDepth = prevLoopKeyDepth;
|
|
1449
|
-
const children = loop.bodyIsItemConditional && loop.key ? `<%== bf->comment("loop-i:" . ${this.convertExpressionToPerl(loop.key)}) %>
|
|
1450
|
-
${renderedChildren}` : renderedChildren;
|
|
1451
|
-
const lines = [];
|
|
1452
|
-
lines.push(`<%== bf->comment("loop:${loop.markerId}") %>`);
|
|
1453
|
-
if (sortedHoist && loop.sortComparator) {
|
|
1454
|
-
for (const n of loopBound) {
|
|
1455
|
-
const c = (this.loopBoundNames.get(n) ?? 1) - 1;
|
|
1456
|
-
if (c <= 0)
|
|
1457
|
-
this.loopBoundNames.delete(n);
|
|
1458
|
-
else
|
|
1459
|
-
this.loopBoundNames.set(n, c);
|
|
1460
|
-
}
|
|
1461
|
-
const sortEmit = (e) => this.convertExpressionToPerl("", e);
|
|
1462
|
-
const sortArrow = loop.sortComparator.arrow;
|
|
1463
|
-
let sorted = null;
|
|
1464
|
-
if (sortArrow.kind === "arrow") {
|
|
1465
|
-
sorted = renderSortEval(rawArray, sortArrow.body, sortArrow.params, sortEmit);
|
|
1466
|
-
}
|
|
1467
|
-
if (sorted === null) {
|
|
1468
|
-
const structured = sortComparatorFromArrow2(sortArrow);
|
|
1469
|
-
if (structured !== null)
|
|
1470
|
-
sorted = renderSortMethod(rawArray, structured);
|
|
1471
|
-
}
|
|
1472
|
-
if (sorted === null) {
|
|
1473
|
-
this._recordExprBF101(`.sort(...) loop comparator is not lowerable to a template sort`, `Pre-sort the array in the route handler, or mark the loop @client-only.`);
|
|
1474
|
-
sorted = rawArray;
|
|
1475
|
-
}
|
|
1476
|
-
for (const n of loopBound) {
|
|
1477
|
-
this.loopBoundNames.set(n, (this.loopBoundNames.get(n) ?? 0) + 1);
|
|
1478
|
-
}
|
|
1479
|
-
lines.push(`% my $${sortedHoist} = ${sorted};`);
|
|
1480
|
-
}
|
|
1481
|
-
if (loop.objectIteration) {
|
|
1482
|
-
const keyVar = loop.objectIteration === "values" ? "$__bf_k" : `$${loop.index ?? param}`;
|
|
1483
|
-
lines.push(`% for my ${keyVar} (sort keys %{${array}}) {`);
|
|
1484
|
-
if (loop.objectIteration === "entries" || loop.objectIteration === "values") {
|
|
1485
|
-
lines.push(`% my $${param} = ${array}->{${keyVar}};`);
|
|
1486
|
-
}
|
|
1487
|
-
} else {
|
|
1488
|
-
lines.push(`% for my ${indexVar} (0..$#{${array}}) {`);
|
|
1489
|
-
if (loop.iterationShape !== "keys") {
|
|
1490
|
-
if (supportableDestructure) {
|
|
1491
|
-
lines.push(`% my $__bf_item = ${array}->[${indexVar}];`);
|
|
1492
|
-
for (const b of loop.paramBindings ?? []) {
|
|
1493
|
-
const parent = perlSegmentAccessor("$__bf_item", b.segments ?? []);
|
|
1494
|
-
if (b.rest?.kind === "object") {
|
|
1495
|
-
const exclude = b.rest.exclude.map((k) => perlStringLiteral(k.key)).join(", ");
|
|
1496
|
-
lines.push(`% my $${b.name} = bf->omit(${parent}, [${exclude}]);`);
|
|
1497
|
-
} else if (b.rest?.kind === "array") {
|
|
1498
|
-
lines.push(`% my $${b.name} = bf->slice(${parent}, ${b.rest.from}, undef);`);
|
|
1499
|
-
} else {
|
|
1500
|
-
lines.push(`% my $${b.name} = ${perlSegmentAccessor("$__bf_item", b.segments ?? [])};`);
|
|
1501
|
-
}
|
|
1502
|
-
}
|
|
1503
|
-
} else {
|
|
1504
|
-
lines.push(`% my $${param} = ${array}->[${indexVar}];`);
|
|
1505
|
-
}
|
|
1506
|
-
}
|
|
1507
|
-
}
|
|
1508
|
-
const preambleLines = preambleDecls.map((d) => `% my $${d.name} = ${this.convertExpressionToPerl(d.raw, d.valueParsed)};`);
|
|
1509
|
-
if (loop.filterPredicate) {
|
|
1510
|
-
let filterCond;
|
|
1511
|
-
if (loop.filterPredicate.predicate) {
|
|
1512
|
-
filterCond = this.renderPerlFilterExpr(loop.filterPredicate.predicate, loop.filterPredicate.param);
|
|
1513
|
-
} else {
|
|
1514
|
-
filterCond = "1";
|
|
1515
|
-
}
|
|
1516
|
-
if (loop.filterPredicate.param !== param) {
|
|
1517
|
-
filterCond = filterCond.replace(new RegExp(`\\$${loop.filterPredicate.param}\\b`, "g"), `$${param}`);
|
|
1518
|
-
}
|
|
1519
|
-
lines.push(`% if (${filterCond}) {`);
|
|
1520
|
-
lines.push(...preambleLines);
|
|
1521
|
-
lines.push(children);
|
|
1522
|
-
lines.push(`% }`);
|
|
1523
|
-
} else {
|
|
1524
|
-
lines.push(...preambleLines);
|
|
1525
|
-
lines.push(children);
|
|
1526
|
-
}
|
|
1527
|
-
for (const n of loopBound) {
|
|
1528
|
-
const c = (this.loopBoundNames.get(n) ?? 1) - 1;
|
|
1529
|
-
if (c <= 0)
|
|
1530
|
-
this.loopBoundNames.delete(n);
|
|
1531
|
-
else
|
|
1532
|
-
this.loopBoundNames.set(n, c);
|
|
1533
|
-
}
|
|
1534
|
-
lines.push(`% }`);
|
|
1535
|
-
lines.push(`<%== bf->comment("/loop:${loop.markerId}") %>`);
|
|
1536
|
-
return lines.join(`
|
|
1537
|
-
`);
|
|
1538
|
-
}
|
|
1539
|
-
componentPropEmitter = {
|
|
1540
|
-
emitLiteral: (value, name) => `${perlHashKey(name)} => '${value.value}'`,
|
|
1541
|
-
emitExpression: (value, name) => {
|
|
1542
|
-
if (value.parts) {
|
|
1543
|
-
return `${perlHashKey(name)} => ${this.convertTemplateLiteralPartsToPerl(value.parts)}`;
|
|
1544
|
-
}
|
|
1545
|
-
if (value.parsed) {
|
|
1546
|
-
const hashref = objectLiteralExprToPerlHashref(this.spreadCtx, value.parsed);
|
|
1547
|
-
if (hashref !== null)
|
|
1548
|
-
return `${perlHashKey(name)} => ${hashref}`;
|
|
1549
|
-
}
|
|
1550
|
-
return `${perlHashKey(name)} => ${this.convertExpressionToPerl(value.expr)}`;
|
|
1551
|
-
},
|
|
1552
|
-
emitSpread: (value) => {
|
|
1553
|
-
const perlExpr = this.convertExpressionToPerl(value.expr);
|
|
1554
|
-
return perlExpr.startsWith("%") ? perlExpr : `%{${perlExpr}}`;
|
|
1555
|
-
},
|
|
1556
|
-
emitTemplate: (value, name) => `${perlHashKey(name)} => ${this.convertTemplateLiteralPartsToPerl(value.parts)}`,
|
|
1557
|
-
emitBooleanAttr: (_value, name) => `${perlHashKey(name)} => 1`,
|
|
1558
|
-
emitBooleanShorthand: (_value, name) => `${perlHashKey(name)} => 1`,
|
|
1559
|
-
emitJsxChildren: () => ""
|
|
1560
|
-
};
|
|
1561
|
-
renderComponent(comp) {
|
|
1562
|
-
const propParts = [];
|
|
1563
|
-
const namedSlotCaptures = [];
|
|
1564
|
-
for (const p of comp.props) {
|
|
1565
|
-
if ((p.name.match(/^on[A-Z]/) || p.name === "ref") && p.value.kind === "expression")
|
|
1566
|
-
continue;
|
|
1567
|
-
if (p.value.kind === "jsx-children" && p.name !== "children") {
|
|
1568
|
-
const slotBody = this.renderChildren(p.value.children);
|
|
1569
|
-
const varName = `$bf_prop_${this.childrenCaptureCounter++}`;
|
|
1570
|
-
namedSlotCaptures.push(`<% my ${varName} = begin %>${slotBody}<% end %>`);
|
|
1571
|
-
propParts.push(`${perlHashKey(p.name)} => ${varName}`);
|
|
1572
|
-
continue;
|
|
1573
|
-
}
|
|
1574
|
-
const lowered = emitAttrValue(p.value, this.componentPropEmitter, p.name);
|
|
1575
|
-
if (lowered)
|
|
1576
|
-
propParts.push(lowered);
|
|
1577
|
-
}
|
|
1578
|
-
if (derivesScopeFromSlot(comp)) {
|
|
1579
|
-
propParts.push(`_bf_slot => '${comp.slotId}'`);
|
|
1580
|
-
}
|
|
1581
|
-
const propsStr = propParts.length > 0 ? ", " + propParts.join(", ") : "";
|
|
1582
|
-
const tplName = this.toTemplateName(comp.name);
|
|
1583
|
-
const effectiveChildren = comp.children.length > 0 ? comp.children : resolveJsxChildrenProp(comp.props);
|
|
1584
|
-
if (effectiveChildren.length > 0) {
|
|
1585
|
-
const childrenBody = this.renderChildren(effectiveChildren);
|
|
1586
|
-
const varName = `$bf_children_${comp.slotId ?? "c" + this.childrenCaptureCounter++}`;
|
|
1587
|
-
return `${namedSlotCaptures.join("")}<% my ${varName} = begin %>${childrenBody}<% end %><%== bf->render_child('${tplName}'${propsStr}, children => ${varName}) %>`;
|
|
1588
|
-
}
|
|
1589
|
-
return `${namedSlotCaptures.join("")}<%== bf->render_child('${tplName}'${propsStr}) %>`;
|
|
1590
|
-
}
|
|
1591
|
-
childrenCaptureCounter = 0;
|
|
1592
|
-
presenceVarCounter = 0;
|
|
1593
|
-
toTemplateName(componentName) {
|
|
1594
|
-
return componentName.replace(/([A-Z])/g, "_$1").toLowerCase().replace(/^_/, "");
|
|
1595
|
-
}
|
|
1596
|
-
renderIfStatement(ifStmt) {
|
|
1597
|
-
const condition = this.convertExpressionToPerl(ifStmt.condition);
|
|
1598
|
-
const consequent = ifStmt.consequent.type === "if-statement" ? this.renderIfStatement(ifStmt.consequent) : this.renderNode(ifStmt.consequent);
|
|
1599
|
-
let result = `% if (${condition}) {
|
|
1600
|
-
${consequent}
|
|
1601
|
-
`;
|
|
1602
|
-
if (ifStmt.alternate) {
|
|
1603
|
-
if (ifStmt.alternate.type === "if-statement") {
|
|
1604
|
-
const altResult = this.renderIfStatement(ifStmt.alternate);
|
|
1605
|
-
result += altResult.replace(/^% if/, "% } elsif");
|
|
1606
|
-
} else {
|
|
1607
|
-
const alternate = this.renderNode(ifStmt.alternate);
|
|
1608
|
-
result += `% } else {
|
|
1609
|
-
${alternate}
|
|
1610
|
-
`;
|
|
1611
|
-
}
|
|
1612
|
-
}
|
|
1613
|
-
result += `% }`;
|
|
1614
|
-
return result;
|
|
1615
|
-
}
|
|
1616
|
-
renderFragment(fragment) {
|
|
1617
|
-
const children = this.renderChildren(fragment.children);
|
|
1618
|
-
if (fragment.needsScopeComment) {
|
|
1619
|
-
return `<%== bf->scope_comment %>${children}<%== bf->scope_comment_end %>`;
|
|
1620
|
-
}
|
|
1621
|
-
return children;
|
|
1622
|
-
}
|
|
1623
|
-
renderSlot(_slot) {
|
|
1624
|
-
return `<%= content %>`;
|
|
1625
|
-
}
|
|
1626
|
-
renderAsync(node) {
|
|
1627
|
-
const fallback = this.renderNode(node.fallback);
|
|
1628
|
-
const children = this.renderChildren(node.children);
|
|
1629
|
-
const fallbackVar = `$bf_async_fallback_${node.id}`;
|
|
1630
|
-
return `<% my ${fallbackVar} = begin %>${fallback}<% end %><%== bf->async_boundary('${node.id}', ${fallbackVar}) %>
|
|
1631
|
-
${children}`;
|
|
1632
|
-
}
|
|
1633
|
-
elementAttrEmitter = {
|
|
1634
|
-
emitLiteral: (value, name) => `${name}="${escapeHtml(value.value)}"`,
|
|
1635
|
-
emitExpression: (value, name) => {
|
|
1636
|
-
if (name === "style") {
|
|
1637
|
-
const css = this.tryLowerStyleObject(value.expr);
|
|
1638
|
-
if (css !== null)
|
|
1639
|
-
return `style="${css}"`;
|
|
1640
|
-
}
|
|
1641
|
-
if (this.refuseUnsupportedAttrExpression(value.expr, name)) {
|
|
1642
|
-
return "";
|
|
1643
|
-
}
|
|
1644
|
-
const bareId = value.expr.trim();
|
|
1645
|
-
const normalizedBareId = this.propsObjectName && bareId.startsWith(`${this.propsObjectName}.`) ? bareId.slice(this.propsObjectName.length + 1) : bareId;
|
|
1646
|
-
if (!isBooleanAttr(name) && !value.presenceOrUndefined && /^[A-Za-z_$][\w$]*$/.test(normalizedBareId) && this.nullableOptionalProps.has(normalizedBareId) && !this.loopBoundNames.has(normalizedBareId)) {
|
|
1647
|
-
const perl2 = this.convertExpressionToPerl(value.expr);
|
|
1648
|
-
const body = isBooleanResultExpr(value.expr) || isAriaBooleanAttr(name) || this.isBooleanTypedPropRef(value.expr) ? `${name}="<%= bf->bool_str(${perl2}) %>"` : `${name}="<%= ${perl2} %>"`;
|
|
1649
|
-
return `<% if (defined ${perl2}) { %>${body}<% } %>`;
|
|
1650
|
-
}
|
|
1651
|
-
if (isBooleanAttr(name)) {
|
|
1652
|
-
return `<%= ${this.convertExpressionToPerl(value.expr)} ? '${name}' : '' %>`;
|
|
1653
|
-
}
|
|
1654
|
-
if (value.presenceOrUndefined) {
|
|
1655
|
-
const perl2 = this.convertExpressionToPerl(value.expr);
|
|
1656
|
-
const tmp = `$bf_pu${this.presenceVarCounter++}`;
|
|
1657
|
-
const body = isBooleanResultExpr(value.expr) || isAriaBooleanAttr(name) || this.isBooleanTypedPropRef(value.expr) ? `${name}="<%= bf->bool_str(${tmp}) %>"` : `${name}="<%= ${tmp} %>"`;
|
|
1658
|
-
return `<% my ${tmp} = ${perl2}; if (${tmp}) { %>${body}<% } %>`;
|
|
1659
|
-
}
|
|
1660
|
-
{
|
|
1661
|
-
const m = this.parseUndefinedAlternateTernary(value.expr);
|
|
1662
|
-
if (m) {
|
|
1663
|
-
const cond = this.convertExpressionToPerl(m.condition);
|
|
1664
|
-
const val = this.convertExpressionToPerl(m.consequent);
|
|
1665
|
-
return `<% if (${cond}) { %>${name}="<%= ${val} %>"<% } %>`;
|
|
1666
|
-
}
|
|
1667
|
-
}
|
|
1668
|
-
const perl = this.convertExpressionToPerl(value.expr);
|
|
1669
|
-
if (isBooleanResultExpr(value.expr) || isAriaBooleanAttr(name) || this.isBooleanTypedPropRef(value.expr)) {
|
|
1670
|
-
return `${name}="<%= bf->bool_str(${perl}) %>"`;
|
|
1671
|
-
}
|
|
1672
|
-
return `${name}="<%= ${perl} %>"`;
|
|
1673
|
-
},
|
|
1674
|
-
emitBooleanAttr: (_value, name) => name,
|
|
1675
|
-
emitTemplate: (value, name) => `${name}="<%= ${this.convertTemplateLiteralPartsToPerl(value.parts)} %>"`,
|
|
1676
|
-
emitSpread: (value) => {
|
|
1677
|
-
if (this.refuseUnsupportedAttrExpression(value.expr, "...")) {
|
|
1678
|
-
return "";
|
|
1679
|
-
}
|
|
1680
|
-
const trimmed = value.expr.trim();
|
|
1681
|
-
if (this.propsObjectName && this.propsObjectName === trimmed) {
|
|
1682
|
-
const entries = this.propsParams.map((p) => `${JSON.stringify(p.name)} => $${p.name}`);
|
|
1683
|
-
return `<%== bf->spread_attrs({${entries.join(", ")}}) %>`;
|
|
1684
|
-
}
|
|
1685
|
-
const ternaryHashref = conditionalSpreadToPerl(this.spreadCtx, value.parsed);
|
|
1686
|
-
if (ternaryHashref !== null) {
|
|
1687
|
-
return `<%== bf->spread_attrs(${ternaryHashref}) %>`;
|
|
1688
|
-
}
|
|
1689
|
-
if (/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(trimmed) && !this.loopBoundNames.has(trimmed)) {
|
|
1690
|
-
const localConst = this.localConstants.find((c) => c.name === trimmed && !c.isModule);
|
|
1691
|
-
if (localConst?.value !== undefined) {
|
|
1692
|
-
const initTrimmed = localConst.value.trim();
|
|
1693
|
-
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(initTrimmed)) {
|
|
1694
|
-
const resolved = conditionalSpreadToPerl(this.spreadCtx, parseExpression2(initTrimmed));
|
|
1695
|
-
if (resolved !== null) {
|
|
1696
|
-
return `<%== bf->spread_attrs(${resolved}) %>`;
|
|
1697
|
-
}
|
|
1698
|
-
}
|
|
1699
|
-
}
|
|
1700
|
-
}
|
|
1701
|
-
const perlExpr = this.convertExpressionToPerl(value.expr);
|
|
1702
|
-
return `<%== bf->spread_attrs(${perlExpr}) %>`;
|
|
1703
|
-
},
|
|
1704
|
-
emitBooleanShorthand: () => "",
|
|
1705
|
-
emitJsxChildren: () => ""
|
|
1706
|
-
};
|
|
1707
|
-
tryLowerStyleObject(expr) {
|
|
1708
|
-
const entries = parseStyleObjectEntries(expr);
|
|
1709
|
-
if (!entries)
|
|
1710
|
-
return null;
|
|
1711
|
-
for (const e of entries) {
|
|
1712
|
-
if (e.kind === "expr" && !isSupported(parseExpression2(e.expr)).supported)
|
|
1713
|
-
return null;
|
|
1714
|
-
}
|
|
1715
|
-
const args = entries.flatMap((e) => [
|
|
1716
|
-
`'${e.cssKey.replace(/[\\']/g, (m) => `\\${m}`)}'`,
|
|
1717
|
-
e.kind === "literal" ? `'${e.value.replace(/[\\']/g, (m) => `\\${m}`)}'` : this.convertExpressionToPerl(e.expr)
|
|
1718
|
-
]);
|
|
1719
|
-
return `<%== bf->style_object(${args.join(", ")}) %>`;
|
|
1720
|
-
}
|
|
1721
|
-
renderAttributes(element) {
|
|
1722
|
-
const parts = [];
|
|
1723
|
-
for (const attr of element.attrs) {
|
|
1724
|
-
if (attr.clientOnly)
|
|
1725
|
-
continue;
|
|
1726
|
-
if (isDangerousInnerHtmlAttr(attr))
|
|
1727
|
-
continue;
|
|
1728
|
-
let attrName;
|
|
1729
|
-
if (attr.name === "className")
|
|
1730
|
-
attrName = "class";
|
|
1731
|
-
else if (attr.name === "key") {
|
|
1732
|
-
const depth = this.currentLoopKeyDepth;
|
|
1733
|
-
attrName = depth > 0 ? `data-key-${depth}` : "data-key";
|
|
1734
|
-
} else
|
|
1735
|
-
attrName = attr.name;
|
|
1736
|
-
const lowered = emitAttrValue(attr.value, this.elementAttrEmitter, attrName);
|
|
1737
|
-
if (lowered)
|
|
1738
|
-
parts.push(lowered);
|
|
1739
|
-
}
|
|
1740
|
-
return parts.length > 0 ? " " + parts.join(" ") : "";
|
|
1741
|
-
}
|
|
1742
|
-
renderScopeMarker(_instanceIdExpr) {
|
|
1743
|
-
return `bf-s="<%= bf->scope_attr %>" <%== bf->hydration_attrs %> <%== bf->props_attr %>`;
|
|
1744
|
-
}
|
|
1745
|
-
renderSlotMarker(slotId) {
|
|
1746
|
-
return `${BF_SLOT}="${slotId}"`;
|
|
1747
|
-
}
|
|
1748
|
-
renderCondMarker(condId) {
|
|
1749
|
-
return `${BF_COND}="${condId}"`;
|
|
1750
|
-
}
|
|
1751
|
-
renderPerlFilterExpr(expr, param, localVarMap = new Map) {
|
|
1752
|
-
return emitParsedExpr2(expr, new MojoFilterEmitter(param, localVarMap, (n) => this._isStringValueName(n), (message, reason) => this._recordExprBF101(message, reason)));
|
|
1753
|
-
}
|
|
1754
|
-
convertTemplateLiteralPartsToPerl(literalParts) {
|
|
1755
|
-
const parts = [];
|
|
1756
|
-
for (const part of literalParts) {
|
|
1757
|
-
if (part.type === "string") {
|
|
1758
|
-
parts.push(this.substituteJsInterpolationsToPerl(part.value));
|
|
1759
|
-
} else if (part.type === "ternary") {
|
|
1760
|
-
const cond = this.convertExpressionToPerl(part.condition);
|
|
1761
|
-
parts.push(`(${cond} ? '${part.whenTrue}' : '${part.whenFalse}')`);
|
|
1762
|
-
} else if (part.type === "lookup") {
|
|
1763
|
-
const keyExpr = this.convertExpressionToPerl(part.key);
|
|
1764
|
-
const entries = Object.entries(part.cases).map(([k, v]) => `'${k}' => '${v}'`).join(", ");
|
|
1765
|
-
parts.push(`({ ${entries} }->{${keyExpr}} // '')`);
|
|
1766
|
-
}
|
|
1767
|
-
}
|
|
1768
|
-
return parts.length === 1 ? parts[0] : parts.join(" . ");
|
|
1769
|
-
}
|
|
1770
|
-
substituteJsInterpolationsToPerl(s) {
|
|
1771
|
-
const segments = [];
|
|
1772
|
-
const re = /\$\{([^}]+)\}/g;
|
|
1773
|
-
let lastIndex = 0;
|
|
1774
|
-
let m;
|
|
1775
|
-
while ((m = re.exec(s)) !== null) {
|
|
1776
|
-
if (m.index > lastIndex) {
|
|
1777
|
-
segments.push(`'${s.slice(lastIndex, m.index)}'`);
|
|
1778
|
-
}
|
|
1779
|
-
segments.push(this.convertExpressionToPerl(m[1].trim()));
|
|
1780
|
-
lastIndex = re.lastIndex;
|
|
1781
|
-
}
|
|
1782
|
-
if (lastIndex < s.length) {
|
|
1783
|
-
segments.push(`'${s.slice(lastIndex)}'`);
|
|
1784
|
-
}
|
|
1785
|
-
if (segments.length === 0)
|
|
1786
|
-
return `''`;
|
|
1787
|
-
return segments.length === 1 ? segments[0] : `(${segments.join(" . ")})`;
|
|
1788
|
-
}
|
|
1789
|
-
refuseUnsupportedAttrExpression(expr, attrName) {
|
|
1790
|
-
let probe = expr.trim();
|
|
1791
|
-
while (probe.startsWith("("))
|
|
1792
|
-
probe = probe.slice(1).trimStart();
|
|
1793
|
-
const startsAsObjectLiteral = probe.startsWith("{");
|
|
1794
|
-
const hasTaggedTemplate = /[A-Za-z_$][\w$]*\s*`/.test(probe);
|
|
1795
|
-
if (!startsAsObjectLiteral && !hasTaggedTemplate)
|
|
1796
|
-
return false;
|
|
1797
|
-
const parsed = parseExpression2(expr.trim());
|
|
1798
|
-
const support = isSupported(parsed);
|
|
1799
|
-
if (parsed.kind !== "unsupported" && support.supported)
|
|
1800
|
-
return false;
|
|
1801
|
-
const reason = support.reason ?? (parsed.kind === "unsupported" ? parsed.reason : undefined);
|
|
1802
|
-
const reasonLine = reason ? `
|
|
1803
|
-
${reason}` : "";
|
|
1804
|
-
this.errors.push({
|
|
1805
|
-
code: "BF101",
|
|
1806
|
-
severity: "error",
|
|
1807
|
-
message: `Expression not supported on attribute '${attrName}': ${expr.trim()}${reasonLine}`,
|
|
1808
|
-
loc: { file: this.componentName + ".tsx", start: { line: 1, column: 0 }, end: { line: 1, column: 0 } },
|
|
1809
|
-
suggestion: {
|
|
1810
|
-
message: "The Mojo adapter cannot lower JS object literals or tagged-template-literal expressions into Embedded Perl. Move the expression into a `'use client'` component (so hydration computes it), or expand it into discrete attributes whose values are values the adapter can lower."
|
|
1811
|
-
}
|
|
1812
|
-
});
|
|
1813
|
-
return true;
|
|
1814
|
-
}
|
|
1815
|
-
get emitCtx() {
|
|
1816
|
-
return {
|
|
1817
|
-
_searchParamsLocals: this._searchParamsLocals,
|
|
1818
|
-
resolveModuleStringConst: (name) => this.resolveModuleStringConst(name),
|
|
1819
|
-
resolveLiteralConst: (name) => this.resolveLiteralConst(name),
|
|
1820
|
-
resolveStaticRecordLiteral: (o, k) => this.resolveStaticRecordLiteral(o, k),
|
|
1821
|
-
_isStringValueName: (name) => this._isStringValueName(name),
|
|
1822
|
-
_recordExprBF101: (message, reason) => this._recordExprBF101(message, reason),
|
|
1823
|
-
_renderPerlFilterExprPublic: (e, p) => this._renderPerlFilterExprPublic(e, p)
|
|
1824
|
-
};
|
|
1825
|
-
}
|
|
1826
|
-
get spreadCtx() {
|
|
1827
|
-
return {
|
|
1828
|
-
componentName: this.componentName,
|
|
1829
|
-
errors: this.errors,
|
|
1830
|
-
localConstants: this.localConstants,
|
|
1831
|
-
propsParams: this.propsParams,
|
|
1832
|
-
convertExpressionToPerl: (e, preParsed) => this.convertExpressionToPerl(e, preParsed)
|
|
1833
|
-
};
|
|
1834
|
-
}
|
|
1835
|
-
get memoCtx() {
|
|
1836
|
-
return { convertExpressionToPerl: (e, preParsed) => this.convertExpressionToPerl(e, preParsed) };
|
|
1837
|
-
}
|
|
1838
|
-
convertExpressionToPerl(expr, preParsed) {
|
|
1839
|
-
let parsed;
|
|
1840
|
-
if (preParsed) {
|
|
1841
|
-
parsed = preParsed;
|
|
1842
|
-
} else {
|
|
1843
|
-
const trimmed = expr.trim();
|
|
1844
|
-
if (trimmed === "")
|
|
1845
|
-
return "''";
|
|
1846
|
-
parsed = parseExpression2(trimmed);
|
|
1847
|
-
}
|
|
1848
|
-
if (parsed.kind === "call") {
|
|
1849
|
-
for (const matcher of this._loweringMatchers) {
|
|
1850
|
-
const node = matcher(parsed.callee, parsed.args);
|
|
1851
|
-
if (node?.kind === "guard-list" && node.helper === "query") {
|
|
1852
|
-
const argsGo = queryHrefArgs(node, (n) => this.renderParsedExprToPerl(n));
|
|
1853
|
-
return `bf->query(${argsGo.join(", ")})`;
|
|
1854
|
-
}
|
|
1855
|
-
if (node?.kind === "helper-call" && isValidHelperId(node.helper)) {
|
|
1856
|
-
const argsX = node.args.map((a) => this.renderParsedExprToPerl(a));
|
|
1857
|
-
return `bf->${node.helper}(${argsX.join(", ")})`;
|
|
1858
|
-
}
|
|
1859
|
-
}
|
|
1860
|
-
}
|
|
1861
|
-
const support = isSupported(parsed);
|
|
1862
|
-
if (!support.supported) {
|
|
1863
|
-
this.errors.push({
|
|
1864
|
-
code: "BF101",
|
|
1865
|
-
severity: "error",
|
|
1866
|
-
message: `Expression not supported: ${preParsed ? stringifyParsedExpr2(parsed) : expr.trim()}`,
|
|
1867
|
-
loc: { file: this.componentName + ".tsx", start: { line: 1, column: 0 }, end: { line: 1, column: 0 } },
|
|
1868
|
-
suggestion: {
|
|
1869
|
-
message: support.reason ? `${support.reason}
|
|
1870
|
-
|
|
1871
|
-
Options:
|
|
1872
|
-
1. Use /* @client */ for client-side evaluation
|
|
1873
|
-
2. Pre-compute the value in Perl` : `Options:
|
|
1874
|
-
1. Use /* @client */ for client-side evaluation
|
|
1875
|
-
2. Pre-compute the value in Perl`
|
|
1876
|
-
}
|
|
1877
|
-
});
|
|
1878
|
-
return "''";
|
|
1879
|
-
}
|
|
1880
|
-
return this.renderParsedExprToPerl(parsed);
|
|
1881
|
-
}
|
|
1882
|
-
renderParsedExprToPerl(expr) {
|
|
1883
|
-
return emitParsedExpr2(expr, new MojoTopLevelEmitter(this.emitCtx));
|
|
1884
|
-
}
|
|
1885
|
-
_isStringValueName(name) {
|
|
1886
|
-
return this.stringValueNames.has(name);
|
|
1887
|
-
}
|
|
1888
|
-
_recordExprBF101(message, reason) {
|
|
1889
|
-
this.errors.push({
|
|
1890
|
-
code: "BF101",
|
|
1891
|
-
severity: "error",
|
|
1892
|
-
message,
|
|
1893
|
-
loc: { file: this.componentName + ".tsx", start: { line: 1, column: 0 }, end: { line: 1, column: 0 } },
|
|
1894
|
-
suggestion: {
|
|
1895
|
-
message: reason ? `${reason}
|
|
1896
|
-
|
|
1897
|
-
Options:
|
|
1898
|
-
1. Use /* @client */ for client-side evaluation
|
|
1899
|
-
2. Pre-compute the value in Perl` : `Options:
|
|
1900
|
-
1. Use /* @client */ for client-side evaluation
|
|
1901
|
-
2. Pre-compute the value in Perl`
|
|
1902
|
-
}
|
|
1903
|
-
});
|
|
1904
|
-
}
|
|
1905
|
-
_renderPerlFilterExprPublic(expr, param) {
|
|
1906
|
-
return this.renderPerlFilterExpr(expr, param);
|
|
1907
|
-
}
|
|
1908
|
-
}
|
|
1909
|
-
var mojoAdapter = new MojoAdapter;
|
|
1910
|
-
// src/build.ts
|
|
1911
|
-
function createConfig(options = {}) {
|
|
1912
|
-
return {
|
|
1913
|
-
adapter: new MojoAdapter(options.adapterOptions),
|
|
1914
|
-
paths: options.paths,
|
|
1915
|
-
components: options.components,
|
|
1916
|
-
outDir: options.outDir,
|
|
1917
|
-
minify: options.minify,
|
|
1918
|
-
contentHash: options.contentHash,
|
|
1919
|
-
externals: options.externals,
|
|
1920
|
-
externalsBasePath: options.externalsBasePath,
|
|
1921
|
-
bundleEntries: options.bundleEntries,
|
|
1922
|
-
localImportPrefixes: options.localImportPrefixes,
|
|
1923
|
-
outputLayout: options.outputLayout ?? {
|
|
1924
|
-
templates: "templates",
|
|
1925
|
-
clientJs: "client",
|
|
1926
|
-
runtime: "client"
|
|
1927
|
-
},
|
|
1928
|
-
postBuild: options.postBuild
|
|
1929
|
-
};
|
|
1930
|
-
}
|
|
1931
|
-
export {
|
|
1932
|
-
createConfig
|
|
1933
|
-
};
|