@barefootjs/mojolicious 0.9.1 → 0.9.3
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/index.d.ts +2 -2
- package/dist/adapter/index.d.ts.map +1 -1
- package/dist/adapter/index.js +119 -3
- package/dist/adapter/mojo-adapter.d.ts +38 -0
- package/dist/adapter/mojo-adapter.d.ts.map +1 -1
- package/dist/build.d.ts +2 -2
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +119 -3
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +119 -3
- package/dist/test-render.d.ts +7 -0
- package/dist/test-render.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/__tests__/mojo-adapter.test.ts +120 -14
- package/src/__tests__/mojo-streaming.test.ts +8 -3
- package/src/adapter/index.ts +2 -2
- package/src/adapter/mojo-adapter.ts +202 -4
- package/src/build.ts +2 -2
- package/src/index.ts +2 -2
- package/src/test-render.ts +29 -39
package/dist/adapter/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Mojolicious EP Template Adapter Exports
|
|
3
3
|
*/
|
|
4
|
-
export { MojoAdapter, mojoAdapter } from './mojo-adapter';
|
|
5
|
-
export type { MojoAdapterOptions } from './mojo-adapter';
|
|
4
|
+
export { MojoAdapter, mojoAdapter } from './mojo-adapter.ts';
|
|
5
|
+
export type { MojoAdapterOptions } from './mojo-adapter.ts';
|
|
6
6
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/adapter/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/adapter/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAC5D,YAAY,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAA"}
|
package/dist/adapter/index.js
CHANGED
|
@@ -11,7 +11,10 @@ import {
|
|
|
11
11
|
emitAttrValue,
|
|
12
12
|
augmentInheritedPropAccesses,
|
|
13
13
|
parseRecordIndexAccess,
|
|
14
|
-
evalStringArrayJoin
|
|
14
|
+
evalStringArrayJoin,
|
|
15
|
+
extractArrowBodyExpression,
|
|
16
|
+
collectContextConsumers,
|
|
17
|
+
extractSsrDefaults
|
|
15
18
|
} from "@barefootjs/jsx";
|
|
16
19
|
|
|
17
20
|
// src/adapter/boolean-result.ts
|
|
@@ -103,6 +106,36 @@ function parsePureStringLiteral(source) {
|
|
|
103
106
|
function perlHashKey(name) {
|
|
104
107
|
return /^[A-Za-z_][A-Za-z0-9_]*$/.test(name) ? name : `'${name.replace(/'/g, "\\'")}'`;
|
|
105
108
|
}
|
|
109
|
+
function collectRootScopeNodes(node) {
|
|
110
|
+
const out = new Set;
|
|
111
|
+
const visit = (n) => {
|
|
112
|
+
if (!n)
|
|
113
|
+
return;
|
|
114
|
+
if (n.type === "element") {
|
|
115
|
+
out.add(n);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
if (n.type === "if-statement") {
|
|
119
|
+
const s = n;
|
|
120
|
+
visit(s.consequent);
|
|
121
|
+
visit(s.alternate);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
if (n.type === "fragment") {
|
|
125
|
+
for (const c of n.children)
|
|
126
|
+
visit(c);
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
visit(node);
|
|
130
|
+
return out;
|
|
131
|
+
}
|
|
132
|
+
function referencedVarsAreAvailable(expr, available) {
|
|
133
|
+
for (const m of expr.matchAll(/\$([A-Za-z_]\w*)/g)) {
|
|
134
|
+
if (!available.has(m[1]))
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
return true;
|
|
138
|
+
}
|
|
106
139
|
|
|
107
140
|
class MojoAdapter extends BaseAdapter {
|
|
108
141
|
name = "mojolicious";
|
|
@@ -111,6 +144,7 @@ class MojoAdapter extends BaseAdapter {
|
|
|
111
144
|
importMapInjection = "html-snippet";
|
|
112
145
|
templatePrimitives = MOJO_PRIMITIVE_EMIT_MAP;
|
|
113
146
|
componentName = "";
|
|
147
|
+
rootScopeNodes = new Set;
|
|
114
148
|
options;
|
|
115
149
|
errors = [];
|
|
116
150
|
inLoop = false;
|
|
@@ -152,9 +186,12 @@ class MojoAdapter extends BaseAdapter {
|
|
|
152
186
|
if (!options?.siblingTemplatesRegistered) {
|
|
153
187
|
this.checkImportedLoopChildComponents(ir);
|
|
154
188
|
}
|
|
189
|
+
this.rootScopeNodes = collectRootScopeNodes(ir.root);
|
|
155
190
|
const templateBody = ir.root.type === "if-statement" ? this.renderIfStatement(ir.root) : this.renderNode(ir.root);
|
|
156
191
|
const scriptReg = options?.skipScriptRegistration ? "" : this.generateScriptRegistrations(ir, options?.scriptBaseName);
|
|
157
|
-
const
|
|
192
|
+
const ctxSeed = this.generateContextConsumerSeed(ir);
|
|
193
|
+
const memoSeed = this.generateDerivedMemoSeed(ir);
|
|
194
|
+
const template = `${scriptReg}${ctxSeed}${memoSeed}${templateBody}
|
|
158
195
|
`;
|
|
159
196
|
if (this.errors.length > 0) {
|
|
160
197
|
ir.errors.push(...this.errors);
|
|
@@ -240,7 +277,83 @@ class MojoAdapter extends BaseAdapter {
|
|
|
240
277
|
return this.renderIfStatement(node);
|
|
241
278
|
}
|
|
242
279
|
emitProvider(node, _ctx, _emit) {
|
|
243
|
-
|
|
280
|
+
const value = this.providerValuePerl(node.valueProp);
|
|
281
|
+
const children = this.renderChildren(node.children);
|
|
282
|
+
const name = node.contextName;
|
|
283
|
+
return `<% bf->provide_context('${name}', ${value}); %>` + children + `<% bf->revoke_context('${name}'); %>`;
|
|
284
|
+
}
|
|
285
|
+
providerValuePerl(valueProp) {
|
|
286
|
+
const v = valueProp.value;
|
|
287
|
+
if (v.kind === "literal") {
|
|
288
|
+
return typeof v.value === "string" ? `'${v.value.replace(/[\\']/g, (m) => `\\${m}`)}'` : String(v.value);
|
|
289
|
+
}
|
|
290
|
+
if (v.kind === "expression")
|
|
291
|
+
return this.convertExpressionToPerl(v.expr);
|
|
292
|
+
if (v.kind === "template")
|
|
293
|
+
return this.convertTemplateLiteralPartsToPerl(v.parts);
|
|
294
|
+
return "undef";
|
|
295
|
+
}
|
|
296
|
+
contextDefaultPerl(c) {
|
|
297
|
+
const d = c.defaultValue;
|
|
298
|
+
if (d === null || d === undefined)
|
|
299
|
+
return "undef";
|
|
300
|
+
if (typeof d === "string")
|
|
301
|
+
return `'${d.replace(/[\\']/g, (m) => `\\${m}`)}'`;
|
|
302
|
+
if (typeof d === "boolean")
|
|
303
|
+
return d ? "1" : "0";
|
|
304
|
+
return String(d);
|
|
305
|
+
}
|
|
306
|
+
generateContextConsumerSeed(ir) {
|
|
307
|
+
const consumers = collectContextConsumers(ir.metadata);
|
|
308
|
+
if (consumers.length === 0)
|
|
309
|
+
return "";
|
|
310
|
+
return consumers.map((c) => `% my $${c.localName} = bf->use_context('${c.contextName}', ${this.contextDefaultPerl(c)});`).join(`
|
|
311
|
+
`) + `
|
|
312
|
+
`;
|
|
313
|
+
}
|
|
314
|
+
generateDerivedMemoSeed(ir) {
|
|
315
|
+
const memos = ir.metadata.memos ?? [];
|
|
316
|
+
const signals = ir.metadata.signals ?? [];
|
|
317
|
+
if (memos.length === 0 && signals.length === 0)
|
|
318
|
+
return "";
|
|
319
|
+
const ssrDefaults = extractSsrDefaults(ir.metadata) ?? {};
|
|
320
|
+
const available = new Set(ir.metadata.propsParams.map((p) => p.name));
|
|
321
|
+
const lines = [];
|
|
322
|
+
for (const signal of signals) {
|
|
323
|
+
const perl = this.tryLowerToPerl(signal.initialValue, available);
|
|
324
|
+
if (perl !== null)
|
|
325
|
+
lines.push(`% my $${signal.getter} = ${perl};`);
|
|
326
|
+
available.add(signal.getter);
|
|
327
|
+
}
|
|
328
|
+
for (const memo of memos) {
|
|
329
|
+
const def = ssrDefaults[memo.name];
|
|
330
|
+
const isNull = !def || typeof def === "object" && "value" in def && def.value === null;
|
|
331
|
+
if (!isNull) {
|
|
332
|
+
available.add(memo.name);
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
const body = extractArrowBodyExpression(memo.computation);
|
|
336
|
+
if (body === null)
|
|
337
|
+
continue;
|
|
338
|
+
const perl = this.tryLowerToPerl(body, available);
|
|
339
|
+
if (perl !== null)
|
|
340
|
+
lines.push(`% my $${memo.name} = ${perl};`);
|
|
341
|
+
available.add(memo.name);
|
|
342
|
+
}
|
|
343
|
+
return lines.length > 0 ? lines.join(`
|
|
344
|
+
`) + `
|
|
345
|
+
` : "";
|
|
346
|
+
}
|
|
347
|
+
tryLowerToPerl(expr, available) {
|
|
348
|
+
const trimmed = expr.trim();
|
|
349
|
+
if (!trimmed)
|
|
350
|
+
return null;
|
|
351
|
+
if (!isSupported(parseExpression2(trimmed)).supported)
|
|
352
|
+
return null;
|
|
353
|
+
const perl = this.convertExpressionToPerl(trimmed);
|
|
354
|
+
if (perl === "" || !/\$[A-Za-z_]\w*/.test(perl))
|
|
355
|
+
return null;
|
|
356
|
+
return referencedVarsAreAvailable(perl, available) ? perl : null;
|
|
244
357
|
}
|
|
245
358
|
emitAsync(node, _ctx, _emit) {
|
|
246
359
|
return this.renderAsync(node);
|
|
@@ -253,6 +366,9 @@ class MojoAdapter extends BaseAdapter {
|
|
|
253
366
|
if (element.needsScope) {
|
|
254
367
|
hydrationAttrs += ` ${this.renderScopeMarker("")}`;
|
|
255
368
|
}
|
|
369
|
+
if (this.rootScopeNodes.has(element) && element.needsScope) {
|
|
370
|
+
hydrationAttrs += ` <%== bf->data_key_attr %>`;
|
|
371
|
+
}
|
|
256
372
|
if (element.slotId) {
|
|
257
373
|
hydrationAttrs += ` ${this.renderSlotMarker(element.slotId)}`;
|
|
258
374
|
}
|
|
@@ -39,6 +39,12 @@ export declare class MojoAdapter extends BaseAdapter implements IRNodeEmitter<Mo
|
|
|
39
39
|
*/
|
|
40
40
|
templatePrimitives: TemplatePrimitiveRegistry;
|
|
41
41
|
private componentName;
|
|
42
|
+
/** The component's root scope element(s) — each carries `data-key` for a
|
|
43
|
+
* keyed loop item (set by the child renderer from the JSX `key` prop). A
|
|
44
|
+
* plain element root is a single node; an `if-statement` (early-return) root
|
|
45
|
+
* contributes the top element of every branch, since any one of them can be
|
|
46
|
+
* the rendered root at runtime. */
|
|
47
|
+
private rootScopeNodes;
|
|
42
48
|
private options;
|
|
43
49
|
private errors;
|
|
44
50
|
private inLoop;
|
|
@@ -140,6 +146,38 @@ export declare class MojoAdapter extends BaseAdapter implements IRNodeEmitter<Mo
|
|
|
140
146
|
emitSlot(node: IRSlot): string;
|
|
141
147
|
emitIfStatement(node: IRIfStatement, _ctx: MojoRenderCtx, _emit: EmitIRNode<MojoRenderCtx>): string;
|
|
142
148
|
emitProvider(node: IRProvider, _ctx: MojoRenderCtx, _emit: EmitIRNode<MojoRenderCtx>): string;
|
|
149
|
+
/** Lower a `<Ctx.Provider value>` value prop to a Perl expression. */
|
|
150
|
+
private providerValuePerl;
|
|
151
|
+
/** Perl literal for a context-consumer's `createContext` default. */
|
|
152
|
+
private contextDefaultPerl;
|
|
153
|
+
/**
|
|
154
|
+
* Emit one `% my $<local> = bf->use_context(...)` seed line per context
|
|
155
|
+
* consumer so the template body's bare `$<local>` resolves to the active
|
|
156
|
+
* provider value (or the `createContext` default). (#1297)
|
|
157
|
+
*/
|
|
158
|
+
private generateContextConsumerSeed;
|
|
159
|
+
/**
|
|
160
|
+
* Seed memos whose SSR default is `null` (not statically evaluable) by
|
|
161
|
+
* computing them in-template from the already-seeded prop / signal vars.
|
|
162
|
+
* Targets the prop-derived memo shape (`createMemo(() => props.value * 10)`)
|
|
163
|
+
* that the static `extractSsrDefaults` evaluator can't fold — without this
|
|
164
|
+
* the memo's `$x` renders empty (the reason `props-reactivity-comparison`
|
|
165
|
+
* was skipped). Only emitted when the lowered expression references vars the
|
|
166
|
+
* template already has in scope (props params + signals + prior memos), so a
|
|
167
|
+
* memo over an out-of-scope binding stays on the null path rather than
|
|
168
|
+
* tripping Perl strict mode. (#1297)
|
|
169
|
+
*/
|
|
170
|
+
private generateDerivedMemoSeed;
|
|
171
|
+
/**
|
|
172
|
+
* Lower a signal init / memo body to Perl for an in-template SSR seed, or
|
|
173
|
+
* `null` when it shouldn't be seeded this way. Returns null — without
|
|
174
|
+
* recording a BF101 — when the expression isn't a supported shape
|
|
175
|
+
* (`isSupported` pre-check, so object/array literals don't fail the build),
|
|
176
|
+
* when the lowering references no in-scope var (a constant — keep the
|
|
177
|
+
* existing ssr-defaults seeding), or when it references an out-of-scope
|
|
178
|
+
* binding. (#1297)
|
|
179
|
+
*/
|
|
180
|
+
private tryLowerToPerl;
|
|
143
181
|
emitAsync(node: IRAsync, _ctx: MojoRenderCtx, _emit: EmitIRNode<MojoRenderCtx>): string;
|
|
144
182
|
renderElement(element: IRElement): string;
|
|
145
183
|
renderExpression(expr: IRExpression): string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mojo-adapter.d.ts","sourceRoot":"","sources":["../../src/adapter/mojo-adapter.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,OAAO,KAAK,EACV,WAAW,EAEX,MAAM,EACN,SAAS,EACT,MAAM,EACN,YAAY,EACZ,aAAa,EACb,MAAM,EACN,WAAW,EACX,UAAU,EACV,MAAM,EACN,aAAa,EACb,UAAU,EACV,OAAO,EAMP,yBAAyB,EAC1B,MAAM,iBAAiB,CAAA;AACxB,OAAO,EACL,WAAW,EACX,KAAK,aAAa,EAClB,KAAK,sBAAsB,EAM3B,KAAK,aAAa,EAClB,KAAK,UAAU,
|
|
1
|
+
{"version":3,"file":"mojo-adapter.d.ts","sourceRoot":"","sources":["../../src/adapter/mojo-adapter.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,OAAO,KAAK,EACV,WAAW,EAEX,MAAM,EACN,SAAS,EACT,MAAM,EACN,YAAY,EACZ,aAAa,EACb,MAAM,EACN,WAAW,EACX,UAAU,EACV,MAAM,EACN,aAAa,EACb,UAAU,EACV,OAAO,EAMP,yBAAyB,EAC1B,MAAM,iBAAiB,CAAA;AACxB,OAAO,EACL,WAAW,EACX,KAAK,aAAa,EAClB,KAAK,sBAAsB,EAM3B,KAAK,aAAa,EAClB,KAAK,UAAU,EAiBhB,MAAM,iBAAiB,CAAA;AAGxB;;;;;;GAMG;AACH,KAAK,aAAa,GAAG,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,CAAA;AAC1C,OAAO,KAAK,EAAE,UAAU,EAAiF,MAAM,iBAAiB,CAAA;AAqDhI,MAAM,WAAW,kBAAkB;IACjC,qEAAqE;IACrE,gBAAgB,CAAC,EAAE,MAAM,CAAA;IAEzB,8EAA8E;IAC9E,cAAc,CAAC,EAAE,MAAM,CAAA;CACxB;AAkFD,qBAAa,WAAY,SAAQ,WAAY,YAAW,aAAa,CAAC,aAAa,CAAC;IAClF,IAAI,SAAgB;IACpB,SAAS,SAAa;IACtB,qBAAqB,UAAO;IAG5B,kBAAkB,EAAG,cAAc,CAAS;IAE5C;;;;;;;;;;;OAWG;IACH,kBAAkB,EAAE,yBAAyB,CAA0B;IAEvE,OAAO,CAAC,aAAa,CAAa;IAClC;;;;wCAIoC;IACpC,OAAO,CAAC,cAAc,CAAyB;IAC/C,OAAO,CAAC,OAAO,CAA8B;IAC7C,OAAO,CAAC,MAAM,CAAsB;IACpC,OAAO,CAAC,MAAM,CAAiB;IAC/B;;;;;;OAMG;IACH,OAAO,CAAC,eAAe,CAAsB;IAC7C,OAAO,CAAC,WAAW,CAAyB;IAC5C;;;;;OAKG;IACH,OAAO,CAAC,gBAAgB,CAAyB;IACjD;;;;;;;;;OASG;IACH,OAAO,CAAC,kBAAkB,CAAiC;IAC3D;;;;;;OAMG;IACH,OAAO,CAAC,cAAc,CAAmC;IACzD;;;;;;;OAOG;IACH,OAAO,CAAC,cAAc,CAAiC;IACvD;;;;;;;;;;;;;;;OAeG;IACH,OAAO,CAAC,qBAAqB,CAAyB;IAEtD,YAAY,OAAO,GAAE,kBAAuB,EAM3C;IAED,QAAQ,CAAC,EAAE,EAAE,WAAW,EAAE,OAAO,CAAC,EAAE,sBAAsB,GAAG,aAAa,CAsHzE;IAED;;;;;;;;OAQG;IACH,OAAO,CAAC,yBAAyB;IAWjC;;;;;OAKG;IACH,wBAAwB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAOpD;IAMD,OAAO,CAAC,2BAA2B;IAenC,OAAO,CAAC,sBAAsB;IAa9B;;;;OAIG;IACH,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE/B;IAMD,WAAW,CAAC,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,aAAa,EAAE,KAAK,EAAE,UAAU,CAAC,aAAa,CAAC,GAAG,MAAM,CAE1F;IAED,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE7B;IAED,cAAc,CAAC,IAAI,EAAE,YAAY,GAAG,MAAM,CAEzC;IAED,eAAe,CAAC,IAAI,EAAE,aAAa,EAAE,IAAI,EAAE,aAAa,EAAE,KAAK,EAAE,UAAU,CAAC,aAAa,CAAC,GAAG,MAAM,CAElG;IAED,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,aAAa,EAAE,KAAK,EAAE,UAAU,CAAC,aAAa,CAAC,GAAG,MAAM,CAEpF;IAED,aAAa,CAAC,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,aAAa,EAAE,KAAK,EAAE,UAAU,CAAC,aAAa,CAAC,GAAG,MAAM,CAE9F;IAED,YAAY,CAAC,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,aAAa,EAAE,KAAK,EAAE,UAAU,CAAC,aAAa,CAAC,GAAG,MAAM,CAE5F;IAED,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE7B;IAED,eAAe,CAAC,IAAI,EAAE,aAAa,EAAE,IAAI,EAAE,aAAa,EAAE,KAAK,EAAE,UAAU,CAAC,aAAa,CAAC,GAAG,MAAM,CAElG;IAED,YAAY,CAAC,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,aAAa,EAAE,KAAK,EAAE,UAAU,CAAC,aAAa,CAAC,GAAG,MAAM,CAc5F;IAED,sEAAsE;IACtE,OAAO,CAAC,iBAAiB;IAczB,qEAAqE;IACrE,OAAO,CAAC,kBAAkB;IAQ1B;;;;OAIG;IACH,OAAO,CAAC,2BAA2B;IAanC;;;;;;;;;;OAUG;IACH,OAAO,CAAC,uBAAuB;IA0C/B;;;;;;;;OAQG;IACH,OAAO,CAAC,cAAc;IAStB,SAAS,CAAC,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,aAAa,EAAE,KAAK,EAAE,UAAU,CAAC,aAAa,CAAC,GAAG,MAAM,CAEtF;IAMD,aAAa,CAAC,OAAO,EAAE,SAAS,GAAG,MAAM,CA+BxC;IAMD,gBAAgB,CAAC,IAAI,EAAE,YAAY,GAAG,MAAM,CAe3C;IAMD,iBAAiB,CAAC,IAAI,EAAE,aAAa,GAAG,MAAM,CAsC7C;IAED,OAAO,CAAC,gBAAgB;IAOxB;;;OAGG;IACH,OAAO,CAAC,2BAA2B;IAcnC;;;;;OAKG;IACH,OAAO,CAAC,gCAAgC;IA8ExC,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAuI/B;IAMD;;;;;;;;OAQG;IACH,OAAO,CAAC,QAAQ,CAAC,oBAAoB,CA+BpC;IAED,eAAe,CAAC,IAAI,EAAE,WAAW,GAAG,MAAM,CA0CzC;IAED,OAAO,CAAC,sBAAsB,CAAI;IAElC,OAAO,CAAC,cAAc;IAYtB,OAAO,CAAC,iBAAiB;IA0BzB,OAAO,CAAC,cAAc;IAQtB,OAAO,CAAC,UAAU;IAIT,WAAW,CAAC,IAAI,EAAE,OAAO,GAAG,MAAM,CAgB1C;IAMD;;;OAGG;IACH,OAAO,CAAC,QAAQ,CAAC,kBAAkB,CAgKlC;IAED,OAAO,CAAC,gBAAgB;IA0BxB,iBAAiB,CAAC,eAAe,EAAE,MAAM,GAAG,MAAM,CAIjD;IAED,gBAAgB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAEvC;IAED,gBAAgB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAEvC;IAMD;;;;;OAKG;IACH,OAAO,CAAC,oBAAoB;IAqB5B;;;OAGG;IACH,OAAO,CAAC,wBAAwB;IAuBhC,OAAO,CAAC,kBAAkB;IAiC1B,OAAO,CAAC,wBAAwB;IAsBhC,OAAO,CAAC,mBAAmB;IAe3B,OAAO,CAAC,iCAAiC;IAmCzC;;;;;;;OAOG;IACH,OAAO,CAAC,gCAAgC;IAmBxC;;;;;;;;;;;;;;;;OAgBG;IACH,OAAO,CAAC,+BAA+B;IA+BvC;;;;;;;;;;;;;;;;;OAiBG;IACH,OAAO,CAAC,uBAAuB;IAyB/B;;;;;OAKG;IACH,OAAO,CAAC,0BAA0B;IA8BlC;;;;;;;;;;;;;OAaG;IACH,OAAO,CAAC,uBAAuB;IAe/B,OAAO,CAAC,uBAAuB;IAqC/B;;;;;OAKG;IACH,OAAO,CAAC,sBAAsB;IAI9B;;;;OAIG;IACH;4EACwE;IACxE,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAExC;IAED,gBAAgB,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAYvD;IAED,iFAAiF;IACjF,2BAA2B,CAAC,IAAI,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAEnE;CACF;AAswBD,eAAO,MAAM,WAAW,aAAoB,CAAA"}
|
package/dist/build.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { BuildOptions } from '@barefootjs/jsx';
|
|
2
|
-
import { MojoAdapter } from './adapter';
|
|
3
|
-
import type { MojoAdapterOptions } from './adapter';
|
|
2
|
+
import { MojoAdapter } from './adapter/index.ts';
|
|
3
|
+
import type { MojoAdapterOptions } from './adapter/index.ts';
|
|
4
4
|
export interface MojoBuildOptions extends BuildOptions {
|
|
5
5
|
/** Adapter-specific options passed to MojoAdapter */
|
|
6
6
|
adapterOptions?: MojoAdapterOptions;
|
package/dist/build.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"build.d.ts","sourceRoot":"","sources":["../src/build.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAA;AACnD,OAAO,EAAE,WAAW,EAAE,MAAM,
|
|
1
|
+
{"version":3,"file":"build.d.ts","sourceRoot":"","sources":["../src/build.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAA;AACnD,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAA;AAChD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAA;AAE5D,MAAM,WAAW,gBAAiB,SAAQ,YAAY;IACpD,qDAAqD;IACrD,cAAc,CAAC,EAAE,kBAAkB,CAAA;CACpC;AAED;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,OAAO,GAAE,gBAAqB;IAEvD,OAAO;IACP,KAAK;IACL,UAAU;IACV,MAAM;IACN,MAAM;IACN,WAAW;IACX,SAAS;IACT,iBAAiB;IACjB,aAAa;IACb,mBAAmB;IACnB,YAAY;IAKZ,SAAS;EAEZ"}
|
package/dist/build.js
CHANGED
|
@@ -11,7 +11,10 @@ import {
|
|
|
11
11
|
emitAttrValue,
|
|
12
12
|
augmentInheritedPropAccesses,
|
|
13
13
|
parseRecordIndexAccess,
|
|
14
|
-
evalStringArrayJoin
|
|
14
|
+
evalStringArrayJoin,
|
|
15
|
+
extractArrowBodyExpression,
|
|
16
|
+
collectContextConsumers,
|
|
17
|
+
extractSsrDefaults
|
|
15
18
|
} from "@barefootjs/jsx";
|
|
16
19
|
|
|
17
20
|
// src/adapter/boolean-result.ts
|
|
@@ -103,6 +106,36 @@ function parsePureStringLiteral(source) {
|
|
|
103
106
|
function perlHashKey(name) {
|
|
104
107
|
return /^[A-Za-z_][A-Za-z0-9_]*$/.test(name) ? name : `'${name.replace(/'/g, "\\'")}'`;
|
|
105
108
|
}
|
|
109
|
+
function collectRootScopeNodes(node) {
|
|
110
|
+
const out = new Set;
|
|
111
|
+
const visit = (n) => {
|
|
112
|
+
if (!n)
|
|
113
|
+
return;
|
|
114
|
+
if (n.type === "element") {
|
|
115
|
+
out.add(n);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
if (n.type === "if-statement") {
|
|
119
|
+
const s = n;
|
|
120
|
+
visit(s.consequent);
|
|
121
|
+
visit(s.alternate);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
if (n.type === "fragment") {
|
|
125
|
+
for (const c of n.children)
|
|
126
|
+
visit(c);
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
visit(node);
|
|
130
|
+
return out;
|
|
131
|
+
}
|
|
132
|
+
function referencedVarsAreAvailable(expr, available) {
|
|
133
|
+
for (const m of expr.matchAll(/\$([A-Za-z_]\w*)/g)) {
|
|
134
|
+
if (!available.has(m[1]))
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
return true;
|
|
138
|
+
}
|
|
106
139
|
|
|
107
140
|
class MojoAdapter extends BaseAdapter {
|
|
108
141
|
name = "mojolicious";
|
|
@@ -111,6 +144,7 @@ class MojoAdapter extends BaseAdapter {
|
|
|
111
144
|
importMapInjection = "html-snippet";
|
|
112
145
|
templatePrimitives = MOJO_PRIMITIVE_EMIT_MAP;
|
|
113
146
|
componentName = "";
|
|
147
|
+
rootScopeNodes = new Set;
|
|
114
148
|
options;
|
|
115
149
|
errors = [];
|
|
116
150
|
inLoop = false;
|
|
@@ -152,9 +186,12 @@ class MojoAdapter extends BaseAdapter {
|
|
|
152
186
|
if (!options?.siblingTemplatesRegistered) {
|
|
153
187
|
this.checkImportedLoopChildComponents(ir);
|
|
154
188
|
}
|
|
189
|
+
this.rootScopeNodes = collectRootScopeNodes(ir.root);
|
|
155
190
|
const templateBody = ir.root.type === "if-statement" ? this.renderIfStatement(ir.root) : this.renderNode(ir.root);
|
|
156
191
|
const scriptReg = options?.skipScriptRegistration ? "" : this.generateScriptRegistrations(ir, options?.scriptBaseName);
|
|
157
|
-
const
|
|
192
|
+
const ctxSeed = this.generateContextConsumerSeed(ir);
|
|
193
|
+
const memoSeed = this.generateDerivedMemoSeed(ir);
|
|
194
|
+
const template = `${scriptReg}${ctxSeed}${memoSeed}${templateBody}
|
|
158
195
|
`;
|
|
159
196
|
if (this.errors.length > 0) {
|
|
160
197
|
ir.errors.push(...this.errors);
|
|
@@ -240,7 +277,83 @@ class MojoAdapter extends BaseAdapter {
|
|
|
240
277
|
return this.renderIfStatement(node);
|
|
241
278
|
}
|
|
242
279
|
emitProvider(node, _ctx, _emit) {
|
|
243
|
-
|
|
280
|
+
const value = this.providerValuePerl(node.valueProp);
|
|
281
|
+
const children = this.renderChildren(node.children);
|
|
282
|
+
const name = node.contextName;
|
|
283
|
+
return `<% bf->provide_context('${name}', ${value}); %>` + children + `<% bf->revoke_context('${name}'); %>`;
|
|
284
|
+
}
|
|
285
|
+
providerValuePerl(valueProp) {
|
|
286
|
+
const v = valueProp.value;
|
|
287
|
+
if (v.kind === "literal") {
|
|
288
|
+
return typeof v.value === "string" ? `'${v.value.replace(/[\\']/g, (m) => `\\${m}`)}'` : String(v.value);
|
|
289
|
+
}
|
|
290
|
+
if (v.kind === "expression")
|
|
291
|
+
return this.convertExpressionToPerl(v.expr);
|
|
292
|
+
if (v.kind === "template")
|
|
293
|
+
return this.convertTemplateLiteralPartsToPerl(v.parts);
|
|
294
|
+
return "undef";
|
|
295
|
+
}
|
|
296
|
+
contextDefaultPerl(c) {
|
|
297
|
+
const d = c.defaultValue;
|
|
298
|
+
if (d === null || d === undefined)
|
|
299
|
+
return "undef";
|
|
300
|
+
if (typeof d === "string")
|
|
301
|
+
return `'${d.replace(/[\\']/g, (m) => `\\${m}`)}'`;
|
|
302
|
+
if (typeof d === "boolean")
|
|
303
|
+
return d ? "1" : "0";
|
|
304
|
+
return String(d);
|
|
305
|
+
}
|
|
306
|
+
generateContextConsumerSeed(ir) {
|
|
307
|
+
const consumers = collectContextConsumers(ir.metadata);
|
|
308
|
+
if (consumers.length === 0)
|
|
309
|
+
return "";
|
|
310
|
+
return consumers.map((c) => `% my $${c.localName} = bf->use_context('${c.contextName}', ${this.contextDefaultPerl(c)});`).join(`
|
|
311
|
+
`) + `
|
|
312
|
+
`;
|
|
313
|
+
}
|
|
314
|
+
generateDerivedMemoSeed(ir) {
|
|
315
|
+
const memos = ir.metadata.memos ?? [];
|
|
316
|
+
const signals = ir.metadata.signals ?? [];
|
|
317
|
+
if (memos.length === 0 && signals.length === 0)
|
|
318
|
+
return "";
|
|
319
|
+
const ssrDefaults = extractSsrDefaults(ir.metadata) ?? {};
|
|
320
|
+
const available = new Set(ir.metadata.propsParams.map((p) => p.name));
|
|
321
|
+
const lines = [];
|
|
322
|
+
for (const signal of signals) {
|
|
323
|
+
const perl = this.tryLowerToPerl(signal.initialValue, available);
|
|
324
|
+
if (perl !== null)
|
|
325
|
+
lines.push(`% my $${signal.getter} = ${perl};`);
|
|
326
|
+
available.add(signal.getter);
|
|
327
|
+
}
|
|
328
|
+
for (const memo of memos) {
|
|
329
|
+
const def = ssrDefaults[memo.name];
|
|
330
|
+
const isNull = !def || typeof def === "object" && "value" in def && def.value === null;
|
|
331
|
+
if (!isNull) {
|
|
332
|
+
available.add(memo.name);
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
const body = extractArrowBodyExpression(memo.computation);
|
|
336
|
+
if (body === null)
|
|
337
|
+
continue;
|
|
338
|
+
const perl = this.tryLowerToPerl(body, available);
|
|
339
|
+
if (perl !== null)
|
|
340
|
+
lines.push(`% my $${memo.name} = ${perl};`);
|
|
341
|
+
available.add(memo.name);
|
|
342
|
+
}
|
|
343
|
+
return lines.length > 0 ? lines.join(`
|
|
344
|
+
`) + `
|
|
345
|
+
` : "";
|
|
346
|
+
}
|
|
347
|
+
tryLowerToPerl(expr, available) {
|
|
348
|
+
const trimmed = expr.trim();
|
|
349
|
+
if (!trimmed)
|
|
350
|
+
return null;
|
|
351
|
+
if (!isSupported(parseExpression2(trimmed)).supported)
|
|
352
|
+
return null;
|
|
353
|
+
const perl = this.convertExpressionToPerl(trimmed);
|
|
354
|
+
if (perl === "" || !/\$[A-Za-z_]\w*/.test(perl))
|
|
355
|
+
return null;
|
|
356
|
+
return referencedVarsAreAvailable(perl, available) ? perl : null;
|
|
244
357
|
}
|
|
245
358
|
emitAsync(node, _ctx, _emit) {
|
|
246
359
|
return this.renderAsync(node);
|
|
@@ -253,6 +366,9 @@ class MojoAdapter extends BaseAdapter {
|
|
|
253
366
|
if (element.needsScope) {
|
|
254
367
|
hydrationAttrs += ` ${this.renderScopeMarker("")}`;
|
|
255
368
|
}
|
|
369
|
+
if (this.rootScopeNodes.has(element) && element.needsScope) {
|
|
370
|
+
hydrationAttrs += ` <%== bf->data_key_attr %>`;
|
|
371
|
+
}
|
|
256
372
|
if (element.slotId) {
|
|
257
373
|
hydrationAttrs += ` ${this.renderSlotMarker(element.slotId)}`;
|
|
258
374
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -3,6 +3,6 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Generates Mojolicious EP template files from BarefootJS IR.
|
|
5
5
|
*/
|
|
6
|
-
export { MojoAdapter, mojoAdapter } from './adapter';
|
|
7
|
-
export type { MojoAdapterOptions } from './adapter';
|
|
6
|
+
export { MojoAdapter, mojoAdapter } from './adapter/index.ts';
|
|
7
|
+
export type { MojoAdapterOptions } from './adapter/index.ts';
|
|
8
8
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAA;AAC7D,YAAY,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAA"}
|
package/dist/index.js
CHANGED
|
@@ -11,7 +11,10 @@ import {
|
|
|
11
11
|
emitAttrValue,
|
|
12
12
|
augmentInheritedPropAccesses,
|
|
13
13
|
parseRecordIndexAccess,
|
|
14
|
-
evalStringArrayJoin
|
|
14
|
+
evalStringArrayJoin,
|
|
15
|
+
extractArrowBodyExpression,
|
|
16
|
+
collectContextConsumers,
|
|
17
|
+
extractSsrDefaults
|
|
15
18
|
} from "@barefootjs/jsx";
|
|
16
19
|
|
|
17
20
|
// src/adapter/boolean-result.ts
|
|
@@ -103,6 +106,36 @@ function parsePureStringLiteral(source) {
|
|
|
103
106
|
function perlHashKey(name) {
|
|
104
107
|
return /^[A-Za-z_][A-Za-z0-9_]*$/.test(name) ? name : `'${name.replace(/'/g, "\\'")}'`;
|
|
105
108
|
}
|
|
109
|
+
function collectRootScopeNodes(node) {
|
|
110
|
+
const out = new Set;
|
|
111
|
+
const visit = (n) => {
|
|
112
|
+
if (!n)
|
|
113
|
+
return;
|
|
114
|
+
if (n.type === "element") {
|
|
115
|
+
out.add(n);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
if (n.type === "if-statement") {
|
|
119
|
+
const s = n;
|
|
120
|
+
visit(s.consequent);
|
|
121
|
+
visit(s.alternate);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
if (n.type === "fragment") {
|
|
125
|
+
for (const c of n.children)
|
|
126
|
+
visit(c);
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
visit(node);
|
|
130
|
+
return out;
|
|
131
|
+
}
|
|
132
|
+
function referencedVarsAreAvailable(expr, available) {
|
|
133
|
+
for (const m of expr.matchAll(/\$([A-Za-z_]\w*)/g)) {
|
|
134
|
+
if (!available.has(m[1]))
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
return true;
|
|
138
|
+
}
|
|
106
139
|
|
|
107
140
|
class MojoAdapter extends BaseAdapter {
|
|
108
141
|
name = "mojolicious";
|
|
@@ -111,6 +144,7 @@ class MojoAdapter extends BaseAdapter {
|
|
|
111
144
|
importMapInjection = "html-snippet";
|
|
112
145
|
templatePrimitives = MOJO_PRIMITIVE_EMIT_MAP;
|
|
113
146
|
componentName = "";
|
|
147
|
+
rootScopeNodes = new Set;
|
|
114
148
|
options;
|
|
115
149
|
errors = [];
|
|
116
150
|
inLoop = false;
|
|
@@ -152,9 +186,12 @@ class MojoAdapter extends BaseAdapter {
|
|
|
152
186
|
if (!options?.siblingTemplatesRegistered) {
|
|
153
187
|
this.checkImportedLoopChildComponents(ir);
|
|
154
188
|
}
|
|
189
|
+
this.rootScopeNodes = collectRootScopeNodes(ir.root);
|
|
155
190
|
const templateBody = ir.root.type === "if-statement" ? this.renderIfStatement(ir.root) : this.renderNode(ir.root);
|
|
156
191
|
const scriptReg = options?.skipScriptRegistration ? "" : this.generateScriptRegistrations(ir, options?.scriptBaseName);
|
|
157
|
-
const
|
|
192
|
+
const ctxSeed = this.generateContextConsumerSeed(ir);
|
|
193
|
+
const memoSeed = this.generateDerivedMemoSeed(ir);
|
|
194
|
+
const template = `${scriptReg}${ctxSeed}${memoSeed}${templateBody}
|
|
158
195
|
`;
|
|
159
196
|
if (this.errors.length > 0) {
|
|
160
197
|
ir.errors.push(...this.errors);
|
|
@@ -240,7 +277,83 @@ class MojoAdapter extends BaseAdapter {
|
|
|
240
277
|
return this.renderIfStatement(node);
|
|
241
278
|
}
|
|
242
279
|
emitProvider(node, _ctx, _emit) {
|
|
243
|
-
|
|
280
|
+
const value = this.providerValuePerl(node.valueProp);
|
|
281
|
+
const children = this.renderChildren(node.children);
|
|
282
|
+
const name = node.contextName;
|
|
283
|
+
return `<% bf->provide_context('${name}', ${value}); %>` + children + `<% bf->revoke_context('${name}'); %>`;
|
|
284
|
+
}
|
|
285
|
+
providerValuePerl(valueProp) {
|
|
286
|
+
const v = valueProp.value;
|
|
287
|
+
if (v.kind === "literal") {
|
|
288
|
+
return typeof v.value === "string" ? `'${v.value.replace(/[\\']/g, (m) => `\\${m}`)}'` : String(v.value);
|
|
289
|
+
}
|
|
290
|
+
if (v.kind === "expression")
|
|
291
|
+
return this.convertExpressionToPerl(v.expr);
|
|
292
|
+
if (v.kind === "template")
|
|
293
|
+
return this.convertTemplateLiteralPartsToPerl(v.parts);
|
|
294
|
+
return "undef";
|
|
295
|
+
}
|
|
296
|
+
contextDefaultPerl(c) {
|
|
297
|
+
const d = c.defaultValue;
|
|
298
|
+
if (d === null || d === undefined)
|
|
299
|
+
return "undef";
|
|
300
|
+
if (typeof d === "string")
|
|
301
|
+
return `'${d.replace(/[\\']/g, (m) => `\\${m}`)}'`;
|
|
302
|
+
if (typeof d === "boolean")
|
|
303
|
+
return d ? "1" : "0";
|
|
304
|
+
return String(d);
|
|
305
|
+
}
|
|
306
|
+
generateContextConsumerSeed(ir) {
|
|
307
|
+
const consumers = collectContextConsumers(ir.metadata);
|
|
308
|
+
if (consumers.length === 0)
|
|
309
|
+
return "";
|
|
310
|
+
return consumers.map((c) => `% my $${c.localName} = bf->use_context('${c.contextName}', ${this.contextDefaultPerl(c)});`).join(`
|
|
311
|
+
`) + `
|
|
312
|
+
`;
|
|
313
|
+
}
|
|
314
|
+
generateDerivedMemoSeed(ir) {
|
|
315
|
+
const memos = ir.metadata.memos ?? [];
|
|
316
|
+
const signals = ir.metadata.signals ?? [];
|
|
317
|
+
if (memos.length === 0 && signals.length === 0)
|
|
318
|
+
return "";
|
|
319
|
+
const ssrDefaults = extractSsrDefaults(ir.metadata) ?? {};
|
|
320
|
+
const available = new Set(ir.metadata.propsParams.map((p) => p.name));
|
|
321
|
+
const lines = [];
|
|
322
|
+
for (const signal of signals) {
|
|
323
|
+
const perl = this.tryLowerToPerl(signal.initialValue, available);
|
|
324
|
+
if (perl !== null)
|
|
325
|
+
lines.push(`% my $${signal.getter} = ${perl};`);
|
|
326
|
+
available.add(signal.getter);
|
|
327
|
+
}
|
|
328
|
+
for (const memo of memos) {
|
|
329
|
+
const def = ssrDefaults[memo.name];
|
|
330
|
+
const isNull = !def || typeof def === "object" && "value" in def && def.value === null;
|
|
331
|
+
if (!isNull) {
|
|
332
|
+
available.add(memo.name);
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
const body = extractArrowBodyExpression(memo.computation);
|
|
336
|
+
if (body === null)
|
|
337
|
+
continue;
|
|
338
|
+
const perl = this.tryLowerToPerl(body, available);
|
|
339
|
+
if (perl !== null)
|
|
340
|
+
lines.push(`% my $${memo.name} = ${perl};`);
|
|
341
|
+
available.add(memo.name);
|
|
342
|
+
}
|
|
343
|
+
return lines.length > 0 ? lines.join(`
|
|
344
|
+
`) + `
|
|
345
|
+
` : "";
|
|
346
|
+
}
|
|
347
|
+
tryLowerToPerl(expr, available) {
|
|
348
|
+
const trimmed = expr.trim();
|
|
349
|
+
if (!trimmed)
|
|
350
|
+
return null;
|
|
351
|
+
if (!isSupported(parseExpression2(trimmed)).supported)
|
|
352
|
+
return null;
|
|
353
|
+
const perl = this.convertExpressionToPerl(trimmed);
|
|
354
|
+
if (perl === "" || !/\$[A-Za-z_]\w*/.test(perl))
|
|
355
|
+
return null;
|
|
356
|
+
return referencedVarsAreAvailable(perl, available) ? perl : null;
|
|
244
357
|
}
|
|
245
358
|
emitAsync(node, _ctx, _emit) {
|
|
246
359
|
return this.renderAsync(node);
|
|
@@ -253,6 +366,9 @@ class MojoAdapter extends BaseAdapter {
|
|
|
253
366
|
if (element.needsScope) {
|
|
254
367
|
hydrationAttrs += ` ${this.renderScopeMarker("")}`;
|
|
255
368
|
}
|
|
369
|
+
if (this.rootScopeNodes.has(element) && element.needsScope) {
|
|
370
|
+
hydrationAttrs += ` <%== bf->data_key_attr %>`;
|
|
371
|
+
}
|
|
256
372
|
if (element.slotId) {
|
|
257
373
|
hydrationAttrs += ` ${this.renderSlotMarker(element.slotId)}`;
|
|
258
374
|
}
|
package/dist/test-render.d.ts
CHANGED
|
@@ -33,6 +33,13 @@ export interface RenderOptions {
|
|
|
33
33
|
props?: Record<string, unknown>;
|
|
34
34
|
/** Additional component files (filename → source) */
|
|
35
35
|
components?: Record<string, string>;
|
|
36
|
+
/**
|
|
37
|
+
* Explicit component to render when `source` declares multiple
|
|
38
|
+
* exports (e.g. `ReactiveProps.tsx` → `PropsReactivityComparison`).
|
|
39
|
+
* Mirrors the Hono reference's `componentName`; omitted for
|
|
40
|
+
* single-export fixtures, which fall back to the default/first export.
|
|
41
|
+
*/
|
|
42
|
+
componentName?: string;
|
|
36
43
|
}
|
|
37
44
|
export declare function renderMojoComponent(options: RenderOptions): Promise<string>;
|
|
38
45
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"test-render.d.ts","sourceRoot":"","sources":["../src/test-render.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAeH,qBAAa,qBAAsB,SAAQ,KAAK;IAC9C,YAAY,OAAO,EAAE,MAAM,EAG1B;CACF;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAKxE;AAkCD,MAAM,WAAW,aAAa;IAC5B,sBAAsB;IACtB,MAAM,EAAE,MAAM,CAAA;IACd,8BAA8B;IAC9B,OAAO,EAAE,OAAO,iBAAiB,EAAE,eAAe,CAAA;IAClD,iCAAiC;IACjC,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAC/B,qDAAqD;IACrD,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;
|
|
1
|
+
{"version":3,"file":"test-render.d.ts","sourceRoot":"","sources":["../src/test-render.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAeH,qBAAa,qBAAsB,SAAQ,KAAK;IAC9C,YAAY,OAAO,EAAE,MAAM,EAG1B;CACF;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAKxE;AAkCD,MAAM,WAAW,aAAa;IAC5B,sBAAsB;IACtB,MAAM,EAAE,MAAM,CAAA;IACd,8BAA8B;IAC9B,OAAO,EAAE,OAAO,iBAAiB,EAAE,eAAe,CAAA;IAClD,iCAAiC;IACjC,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAC/B,qDAAqD;IACrD,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IACnC;;;;;OAKG;IACH,aAAa,CAAC,EAAE,MAAM,CAAA;CACvB;AAED,wBAAsB,mBAAmB,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,MAAM,CAAC,CAgNjF;AAyQD;;;GAGG;AACH,wBAAgB,kBAAkB,CAChC,IAAI,EAAE,MAAM,EACZ,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC9B,OAAO,CAuBT"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@barefootjs/mojolicious",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.3",
|
|
4
4
|
"description": "Mojolicious EP template adapter for BarefootJS - generates .html.ep files from IR",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
"directory": "packages/adapter-mojolicious"
|
|
53
53
|
},
|
|
54
54
|
"dependencies": {
|
|
55
|
-
"@barefootjs/shared": "0.9.
|
|
55
|
+
"@barefootjs/shared": "0.9.3"
|
|
56
56
|
},
|
|
57
57
|
"peerDependencies": {
|
|
58
58
|
"@barefootjs/jsx": ">=0.2.0",
|
|
@@ -60,6 +60,6 @@
|
|
|
60
60
|
},
|
|
61
61
|
"devDependencies": {
|
|
62
62
|
"@barefootjs/adapter-tests": "0.1.0",
|
|
63
|
-
"@barefootjs/jsx": "0.9.
|
|
63
|
+
"@barefootjs/jsx": "0.9.3"
|
|
64
64
|
}
|
|
65
65
|
}
|
|
@@ -17,20 +17,10 @@ runAdapterConformanceTests({
|
|
|
17
17
|
name: 'mojo',
|
|
18
18
|
factory: () => new MojoAdapter(),
|
|
19
19
|
render: renderMojoComponent,
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
'context-provider',
|
|
25
|
-
// Multi-component shared fixtures with per-item loop state. The child
|
|
26
|
-
// scope-id plumbing is now fixed (children derive `<parentScope>_<sN>`
|
|
27
|
-
// from `$bf->_scope_id` in `test-render.ts`, which unblocked
|
|
28
|
-
// `reactive-props`); these two still diverge on additional per-item
|
|
29
|
-
// loop-state seeding the Mojo harness doesn't yet replicate. (xslate
|
|
30
|
-
// skips the same pair.)
|
|
31
|
-
'toggle-shared',
|
|
32
|
-
'props-reactivity-comparison',
|
|
33
|
-
],
|
|
20
|
+
// No JSX-render skips: every shared conformance fixture renders to Hono
|
|
21
|
+
// parity on real Mojolicious. Shapes the adapter intentionally refuses at
|
|
22
|
+
// build time are pinned in `expectedDiagnostics` below.
|
|
23
|
+
skipJsx: [],
|
|
34
24
|
// Per-fixture build-time contracts for shapes the Mojo adapter
|
|
35
25
|
// intentionally refuses to lower. Owned by this adapter test file
|
|
36
26
|
// (not by the shared fixtures) so adding a new adapter doesn't
|
|
@@ -379,6 +369,122 @@ function C({ value = '' }: { value?: string }) {
|
|
|
379
369
|
})
|
|
380
370
|
})
|
|
381
371
|
|
|
372
|
+
describe('MojoAdapter - SSR context propagation (#1297)', () => {
|
|
373
|
+
// `<Ctx.Provider value>` brackets its children with a provide/revoke pair
|
|
374
|
+
// on the shared package-level context stack; descendant `useContext`
|
|
375
|
+
// consumers read it during the same render (mirrors the client
|
|
376
|
+
// `provideContext` / `useContext`).
|
|
377
|
+
test('provider brackets children with provide_context / revoke_context', () => {
|
|
378
|
+
const { template } = compileAndGenerate(`
|
|
379
|
+
'use client'
|
|
380
|
+
import { createContext, useContext } from '@barefootjs/client'
|
|
381
|
+
const ThemeContext = createContext('light')
|
|
382
|
+
export function ThemeRoot() {
|
|
383
|
+
return <div><ThemeContext.Provider value="dark"><ThemeLabel /></ThemeContext.Provider></div>
|
|
384
|
+
}
|
|
385
|
+
function ThemeLabel() { const theme = useContext(ThemeContext); return <span>{theme}</span> }
|
|
386
|
+
`)
|
|
387
|
+
expect(template).toContain("bf->provide_context('ThemeContext', 'dark');")
|
|
388
|
+
expect(template).toContain("bf->revoke_context('ThemeContext');")
|
|
389
|
+
// The provide precedes the child render, the revoke follows it.
|
|
390
|
+
expect(template.indexOf('provide_context')).toBeLessThan(template.indexOf('render_child'))
|
|
391
|
+
expect(template.indexOf('render_child')).toBeLessThan(template.indexOf('revoke_context'))
|
|
392
|
+
})
|
|
393
|
+
|
|
394
|
+
test('consumer seeds its local from use_context with the createContext default', () => {
|
|
395
|
+
const { template } = compileAndGenerate(`
|
|
396
|
+
'use client'
|
|
397
|
+
import { createContext, useContext } from '@barefootjs/client'
|
|
398
|
+
const ThemeContext = createContext('light')
|
|
399
|
+
export function ThemeLabel() { const theme = useContext(ThemeContext); return <span>{theme}</span> }
|
|
400
|
+
`)
|
|
401
|
+
expect(template).toContain("% my $theme = bf->use_context('ThemeContext', 'light');")
|
|
402
|
+
})
|
|
403
|
+
})
|
|
404
|
+
|
|
405
|
+
describe('MojoAdapter - prop-derived memo SSR seeding (#1297)', () => {
|
|
406
|
+
// A memo whose body can't be statically folded (`props.value * 10`) gets a
|
|
407
|
+
// `null` SSR default; the adapter computes it in-template from the seeded
|
|
408
|
+
// prop var so the child renders the value instead of empty.
|
|
409
|
+
test('seeds a prop-derived memo from the prop var', () => {
|
|
410
|
+
const { template } = compileAndGenerate(`
|
|
411
|
+
'use client'
|
|
412
|
+
import { createMemo } from '@barefootjs/client'
|
|
413
|
+
export function Child(props: { value: number }) {
|
|
414
|
+
const displayValue = createMemo(() => props.value * 10)
|
|
415
|
+
return <span>{displayValue()}</span>
|
|
416
|
+
}
|
|
417
|
+
`)
|
|
418
|
+
expect(template).toContain('% my $displayValue = $value * 10;')
|
|
419
|
+
})
|
|
420
|
+
|
|
421
|
+
test('seeds a memo over a destructured prop', () => {
|
|
422
|
+
const { template } = compileAndGenerate(`
|
|
423
|
+
'use client'
|
|
424
|
+
import { createMemo } from '@barefootjs/client'
|
|
425
|
+
export function Child({ value }: { value: number }) {
|
|
426
|
+
const displayValue = createMemo(() => value * 10)
|
|
427
|
+
return <span>{displayValue()}</span>
|
|
428
|
+
}
|
|
429
|
+
`)
|
|
430
|
+
expect(template).toContain('% my $displayValue = $value * 10;')
|
|
431
|
+
})
|
|
432
|
+
})
|
|
433
|
+
|
|
434
|
+
describe('MojoAdapter - prop-derived signal SSR seeding + data-key (#1297, toggle-shared)', () => {
|
|
435
|
+
// A prop-derived signal (`createSignal(props.defaultOn ?? false)`) is seeded
|
|
436
|
+
// in-template from the passed prop, so a loop child honours its own
|
|
437
|
+
// per-item prop instead of the static default.
|
|
438
|
+
test('seeds a prop-derived signal from the prop var', () => {
|
|
439
|
+
const { template } = compileAndGenerate(`
|
|
440
|
+
'use client'
|
|
441
|
+
import { createSignal } from '@barefootjs/client'
|
|
442
|
+
export function Item(props: { defaultOn?: boolean }) {
|
|
443
|
+
const [on, setOn] = createSignal(props.defaultOn ?? false)
|
|
444
|
+
return <button>{on() ? 'ON' : 'OFF'}</button>
|
|
445
|
+
}
|
|
446
|
+
`)
|
|
447
|
+
expect(template).toContain('% my $on = ($defaultOn // 0);')
|
|
448
|
+
})
|
|
449
|
+
|
|
450
|
+
// An object/array-valued signal can't lower to Perl and must NOT be seeded
|
|
451
|
+
// in-template (it would record a BF101) — it keeps the existing ssr-defaults
|
|
452
|
+
// seeding.
|
|
453
|
+
test('does not in-template-seed an object-valued signal', () => {
|
|
454
|
+
const { template } = compileAndGenerate(`
|
|
455
|
+
'use client'
|
|
456
|
+
import { createSignal } from '@barefootjs/client'
|
|
457
|
+
export function Spread() {
|
|
458
|
+
const [attrs, setAttrs] = createSignal<Record<string, string>>({ id: 'a' })
|
|
459
|
+
return <div {...attrs()} />
|
|
460
|
+
}
|
|
461
|
+
`)
|
|
462
|
+
expect(template).not.toContain('my $attrs =')
|
|
463
|
+
})
|
|
464
|
+
|
|
465
|
+
// The component root carries data-key, emitted from the bf instance
|
|
466
|
+
// (render_child sets it from the JSX key); non-keyed renders add nothing.
|
|
467
|
+
test('emits data_key_attr on the component root', () => {
|
|
468
|
+
const { template } = compileAndGenerate(`
|
|
469
|
+
export function Item() { return <div className="x">hi</div> }
|
|
470
|
+
`)
|
|
471
|
+
expect(template).toContain('bf->data_key_attr')
|
|
472
|
+
})
|
|
473
|
+
|
|
474
|
+
// An early-return (if-statement) root has no single root element; data-key
|
|
475
|
+
// must land on each branch's top element so a keyed loop item still stamps it.
|
|
476
|
+
test('emits data_key_attr on each branch root of an if-statement root', () => {
|
|
477
|
+
const { template } = compileAndGenerate(`
|
|
478
|
+
export function Item({ on }: { on?: boolean }) {
|
|
479
|
+
if (on) return <div className="a">A</div>
|
|
480
|
+
return <div className="b">B</div>
|
|
481
|
+
}
|
|
482
|
+
`)
|
|
483
|
+
const count = (template.match(/bf->data_key_attr/g) ?? []).length
|
|
484
|
+
expect(count).toBe(2)
|
|
485
|
+
})
|
|
486
|
+
})
|
|
487
|
+
|
|
382
488
|
describe('MojoAdapter - Template Generation', () => {
|
|
383
489
|
test('generates basic element with scope marker', () => {
|
|
384
490
|
const result = compileAndGenerate(`
|
|
@@ -119,11 +119,14 @@ describe('MojoAdapter - Streaming SSR', () => {
|
|
|
119
119
|
expect(output).toContain('Please wait...')
|
|
120
120
|
})
|
|
121
121
|
|
|
122
|
-
test('renderNode dispatches provider type (
|
|
122
|
+
test('renderNode dispatches provider type (brackets children with provide/revoke)', () => {
|
|
123
|
+
// SSR context propagation (#1297): the provider is no longer transparent —
|
|
124
|
+
// it pushes its value before the children and pops it after so a
|
|
125
|
+
// descendant `useContext` consumer reads it during the same render.
|
|
123
126
|
const providerNode = {
|
|
124
127
|
type: 'provider' as const,
|
|
125
128
|
contextName: 'ThemeContext',
|
|
126
|
-
valueProp: { name: 'value', value: '
|
|
129
|
+
valueProp: { name: 'value', value: { kind: 'literal' as const, value: 'dark' } },
|
|
127
130
|
children: [
|
|
128
131
|
{ type: 'text' as const, value: 'child content', loc: { file: '', start: { line: 1, column: 0 }, end: { line: 1, column: 0 } } },
|
|
129
132
|
],
|
|
@@ -131,6 +134,8 @@ describe('MojoAdapter - Streaming SSR', () => {
|
|
|
131
134
|
}
|
|
132
135
|
|
|
133
136
|
const output = adapter.renderNode(providerNode)
|
|
134
|
-
expect(output).toBe(
|
|
137
|
+
expect(output).toBe(
|
|
138
|
+
"<% bf->provide_context('ThemeContext', 'dark'); %>child content<% bf->revoke_context('ThemeContext'); %>",
|
|
139
|
+
)
|
|
135
140
|
})
|
|
136
141
|
})
|
package/src/adapter/index.ts
CHANGED
|
@@ -2,5 +2,5 @@
|
|
|
2
2
|
* Mojolicious EP Template Adapter Exports
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
export { MojoAdapter, mojoAdapter } from './mojo-adapter'
|
|
6
|
-
export type { MojoAdapterOptions } from './mojo-adapter'
|
|
5
|
+
export { MojoAdapter, mojoAdapter } from './mojo-adapter.ts'
|
|
6
|
+
export type { MojoAdapterOptions } from './mojo-adapter.ts'
|
|
@@ -50,8 +50,12 @@ import {
|
|
|
50
50
|
augmentInheritedPropAccesses,
|
|
51
51
|
parseRecordIndexAccess,
|
|
52
52
|
evalStringArrayJoin,
|
|
53
|
+
extractArrowBodyExpression,
|
|
54
|
+
collectContextConsumers,
|
|
55
|
+
type ContextConsumer,
|
|
56
|
+
extractSsrDefaults,
|
|
53
57
|
} from '@barefootjs/jsx'
|
|
54
|
-
import { isAriaBooleanAttr, isBooleanResultExpr } from './boolean-result'
|
|
58
|
+
import { isAriaBooleanAttr, isBooleanResultExpr } from './boolean-result.ts'
|
|
55
59
|
|
|
56
60
|
/**
|
|
57
61
|
* Mojo adapter's IRNode render context. Mojo's lowering currently
|
|
@@ -161,6 +165,47 @@ function perlHashKey(name: string): string {
|
|
|
161
165
|
return /^[A-Za-z_][A-Za-z0-9_]*$/.test(name) ? name : `'${name.replace(/'/g, "\\'")}'`
|
|
162
166
|
}
|
|
163
167
|
|
|
168
|
+
/**
|
|
169
|
+
* Collect the component's root scope element node(s) — the elements that
|
|
170
|
+
* become the rendered root and so carry `data-key` for a keyed loop item. A
|
|
171
|
+
* plain element root is itself; an `if-statement` (early-return) root
|
|
172
|
+
* contributes the top element of each branch (`consequent` + the `alternate`
|
|
173
|
+
* chain), since exactly one branch renders at runtime. Non-element branch
|
|
174
|
+
* tops (fragments / nested shapes) are walked one level so an
|
|
175
|
+
* `if (…) return <A/>` still resolves to `<A>`. (#1297)
|
|
176
|
+
*/
|
|
177
|
+
function collectRootScopeNodes(node: IRNode): Set<IRNode> {
|
|
178
|
+
const out = new Set<IRNode>()
|
|
179
|
+
const visit = (n: IRNode | null): void => {
|
|
180
|
+
if (!n) return
|
|
181
|
+
if (n.type === 'element') { out.add(n); return }
|
|
182
|
+
if (n.type === 'if-statement') {
|
|
183
|
+
const s = n as IRIfStatement
|
|
184
|
+
visit(s.consequent)
|
|
185
|
+
visit(s.alternate)
|
|
186
|
+
return
|
|
187
|
+
}
|
|
188
|
+
if (n.type === 'fragment') {
|
|
189
|
+
for (const c of (n as IRFragment).children) visit(c)
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
visit(node)
|
|
193
|
+
return out
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* True when every `$var` the lowered (Perl / Kolon) expression references is
|
|
198
|
+
* in the available set — i.e. the template already has that var in scope.
|
|
199
|
+
* Guards in-template memo seeding from referencing an out-of-scope binding
|
|
200
|
+
* (which would trip Perl strict mode). (#1297)
|
|
201
|
+
*/
|
|
202
|
+
function referencedVarsAreAvailable(expr: string, available: ReadonlySet<string>): boolean {
|
|
203
|
+
for (const m of expr.matchAll(/\$([A-Za-z_]\w*)/g)) {
|
|
204
|
+
if (!available.has(m[1])) return false
|
|
205
|
+
}
|
|
206
|
+
return true
|
|
207
|
+
}
|
|
208
|
+
|
|
164
209
|
export class MojoAdapter extends BaseAdapter implements IRNodeEmitter<MojoRenderCtx> {
|
|
165
210
|
name = 'mojolicious'
|
|
166
211
|
extension = '.html.ep'
|
|
@@ -184,6 +229,12 @@ export class MojoAdapter extends BaseAdapter implements IRNodeEmitter<MojoRender
|
|
|
184
229
|
templatePrimitives: TemplatePrimitiveRegistry = MOJO_PRIMITIVE_EMIT_MAP
|
|
185
230
|
|
|
186
231
|
private componentName: string = ''
|
|
232
|
+
/** The component's root scope element(s) — each carries `data-key` for a
|
|
233
|
+
* keyed loop item (set by the child renderer from the JSX `key` prop). A
|
|
234
|
+
* plain element root is a single node; an `if-statement` (early-return) root
|
|
235
|
+
* contributes the top element of every branch, since any one of them can be
|
|
236
|
+
* the rendered root at runtime. */
|
|
237
|
+
private rootScopeNodes: Set<IRNode> = new Set()
|
|
187
238
|
private options: Required<MojoAdapterOptions>
|
|
188
239
|
private errors: CompilerError[] = []
|
|
189
240
|
private inLoop: boolean = false
|
|
@@ -329,6 +380,7 @@ export class MojoAdapter extends BaseAdapter implements IRNodeEmitter<MojoRender
|
|
|
329
380
|
this.checkImportedLoopChildComponents(ir)
|
|
330
381
|
}
|
|
331
382
|
|
|
383
|
+
this.rootScopeNodes = collectRootScopeNodes(ir.root)
|
|
332
384
|
const templateBody = ir.root.type === 'if-statement'
|
|
333
385
|
? this.renderIfStatement(ir.root as IRIfStatement)
|
|
334
386
|
: this.renderNode(ir.root)
|
|
@@ -338,7 +390,20 @@ export class MojoAdapter extends BaseAdapter implements IRNodeEmitter<MojoRender
|
|
|
338
390
|
? ''
|
|
339
391
|
: this.generateScriptRegistrations(ir, options?.scriptBaseName)
|
|
340
392
|
|
|
341
|
-
const
|
|
393
|
+
// SSR context consumers (`const x = useContext(Ctx)`): seed each local
|
|
394
|
+
// from the active provider value (or the `createContext` default) so the
|
|
395
|
+
// body's `$x` resolves. The provider side pushes the value via
|
|
396
|
+
// `emitProvider`; here the consumer reads it. (#1297)
|
|
397
|
+
const ctxSeed = this.generateContextConsumerSeed(ir)
|
|
398
|
+
|
|
399
|
+
// Prop/signal-derived memos that aren't statically evaluable (e.g.
|
|
400
|
+
// `createMemo(() => props.value * 10)`) have a `null` SSR default, so
|
|
401
|
+
// their `$x` would render empty. Compute them in-template from the
|
|
402
|
+
// already-seeded prop/signal vars — mirroring Go's generated child
|
|
403
|
+
// constructor that evaluates the memo from the passed prop. (#1297)
|
|
404
|
+
const memoSeed = this.generateDerivedMemoSeed(ir)
|
|
405
|
+
|
|
406
|
+
const template = `${scriptReg}${ctxSeed}${memoSeed}${templateBody}\n`
|
|
342
407
|
|
|
343
408
|
// Merge collected errors into IR errors
|
|
344
409
|
if (this.errors.length > 0) {
|
|
@@ -480,7 +545,132 @@ export class MojoAdapter extends BaseAdapter implements IRNodeEmitter<MojoRender
|
|
|
480
545
|
}
|
|
481
546
|
|
|
482
547
|
emitProvider(node: IRProvider, _ctx: MojoRenderCtx, _emit: EmitIRNode<MojoRenderCtx>): string {
|
|
483
|
-
|
|
548
|
+
// SSR context propagation (#1297): push the provider value onto the
|
|
549
|
+
// shared controller-stash context stack, render the children (descendant
|
|
550
|
+
// `useContext` consumers read it via `bf->use_context`), then pop. The
|
|
551
|
+
// push/pop bracket the children in the same render so the value scopes
|
|
552
|
+
// exactly to the subtree — mirroring the client `provideContext`.
|
|
553
|
+
const value = this.providerValuePerl(node.valueProp)
|
|
554
|
+
const children = this.renderChildren(node.children)
|
|
555
|
+
const name = node.contextName
|
|
556
|
+
return (
|
|
557
|
+
`<% bf->provide_context('${name}', ${value}); %>` +
|
|
558
|
+
children +
|
|
559
|
+
`<% bf->revoke_context('${name}'); %>`
|
|
560
|
+
)
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
/** Lower a `<Ctx.Provider value>` value prop to a Perl expression. */
|
|
564
|
+
private providerValuePerl(valueProp: IRProvider['valueProp']): string {
|
|
565
|
+
const v = valueProp.value
|
|
566
|
+
if (v.kind === 'literal') {
|
|
567
|
+
return typeof v.value === 'string'
|
|
568
|
+
? `'${v.value.replace(/[\\']/g, m => `\\${m}`)}'`
|
|
569
|
+
: String(v.value)
|
|
570
|
+
}
|
|
571
|
+
if (v.kind === 'expression') return this.convertExpressionToPerl(v.expr)
|
|
572
|
+
if (v.kind === 'template') return this.convertTemplateLiteralPartsToPerl(v.parts)
|
|
573
|
+
// Out-of-shape value (spread / jsx-children) — render as undef rather
|
|
574
|
+
// than emit invalid Perl; the consumer falls back to its default.
|
|
575
|
+
return 'undef'
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
/** Perl literal for a context-consumer's `createContext` default. */
|
|
579
|
+
private contextDefaultPerl(c: ContextConsumer): string {
|
|
580
|
+
const d = c.defaultValue
|
|
581
|
+
if (d === null || d === undefined) return 'undef'
|
|
582
|
+
if (typeof d === 'string') return `'${d.replace(/[\\']/g, m => `\\${m}`)}'`
|
|
583
|
+
if (typeof d === 'boolean') return d ? '1' : '0'
|
|
584
|
+
return String(d)
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
/**
|
|
588
|
+
* Emit one `% my $<local> = bf->use_context(...)` seed line per context
|
|
589
|
+
* consumer so the template body's bare `$<local>` resolves to the active
|
|
590
|
+
* provider value (or the `createContext` default). (#1297)
|
|
591
|
+
*/
|
|
592
|
+
private generateContextConsumerSeed(ir: ComponentIR): string {
|
|
593
|
+
const consumers = collectContextConsumers(ir.metadata)
|
|
594
|
+
if (consumers.length === 0) return ''
|
|
595
|
+
return (
|
|
596
|
+
consumers
|
|
597
|
+
.map(
|
|
598
|
+
c =>
|
|
599
|
+
`% my $${c.localName} = bf->use_context('${c.contextName}', ${this.contextDefaultPerl(c)});`,
|
|
600
|
+
)
|
|
601
|
+
.join('\n') + '\n'
|
|
602
|
+
)
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
/**
|
|
606
|
+
* Seed memos whose SSR default is `null` (not statically evaluable) by
|
|
607
|
+
* computing them in-template from the already-seeded prop / signal vars.
|
|
608
|
+
* Targets the prop-derived memo shape (`createMemo(() => props.value * 10)`)
|
|
609
|
+
* that the static `extractSsrDefaults` evaluator can't fold — without this
|
|
610
|
+
* the memo's `$x` renders empty (the reason `props-reactivity-comparison`
|
|
611
|
+
* was skipped). Only emitted when the lowered expression references vars the
|
|
612
|
+
* template already has in scope (props params + signals + prior memos), so a
|
|
613
|
+
* memo over an out-of-scope binding stays on the null path rather than
|
|
614
|
+
* tripping Perl strict mode. (#1297)
|
|
615
|
+
*/
|
|
616
|
+
private generateDerivedMemoSeed(ir: ComponentIR): string {
|
|
617
|
+
const memos = ir.metadata.memos ?? []
|
|
618
|
+
const signals = ir.metadata.signals ?? []
|
|
619
|
+
if (memos.length === 0 && signals.length === 0) return ''
|
|
620
|
+
const ssrDefaults = extractSsrDefaults(ir.metadata) ?? {}
|
|
621
|
+
// Props seed first; each signal/memo adds its own name as it lands so a
|
|
622
|
+
// later one can reference an earlier one.
|
|
623
|
+
const available = new Set<string>(ir.metadata.propsParams.map(p => p.name))
|
|
624
|
+
const lines: string[] = []
|
|
625
|
+
|
|
626
|
+
// Prop/signal-derived signals (`createSignal(props.defaultOn ?? false)`):
|
|
627
|
+
// a loop-child render receives no stash seed for the signal, so its `$on`
|
|
628
|
+
// would trip strict mode; and even when an entry render seeds it, the
|
|
629
|
+
// static default can't capture the per-call prop. Seed it in-template from
|
|
630
|
+
// the passed prop — but ONLY when the init lowers cleanly AND references an
|
|
631
|
+
// in-scope var (i.e. it's genuinely derived). Object/array/constant inits
|
|
632
|
+
// (`createSignal({…})`, `createSignal([…])`, `createSignal('b')`) keep the
|
|
633
|
+
// existing ssr-defaults seeding, so the spread / loop fixtures are
|
|
634
|
+
// untouched.
|
|
635
|
+
for (const signal of signals) {
|
|
636
|
+
const perl = this.tryLowerToPerl(signal.initialValue, available)
|
|
637
|
+
if (perl !== null) lines.push(`% my $${signal.getter} = ${perl};`)
|
|
638
|
+
available.add(signal.getter)
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
for (const memo of memos) {
|
|
642
|
+
const def = ssrDefaults[memo.name]
|
|
643
|
+
const isNull = !def || (typeof def === 'object' && 'value' in def && def.value === null)
|
|
644
|
+
if (!isNull) {
|
|
645
|
+
available.add(memo.name)
|
|
646
|
+
continue
|
|
647
|
+
}
|
|
648
|
+
const body = extractArrowBodyExpression(memo.computation)
|
|
649
|
+
// Block-bodied arrows / non-expression shapes stay on the null path.
|
|
650
|
+
if (body === null) continue
|
|
651
|
+
const perl = this.tryLowerToPerl(body, available)
|
|
652
|
+
if (perl !== null) lines.push(`% my $${memo.name} = ${perl};`)
|
|
653
|
+
available.add(memo.name)
|
|
654
|
+
}
|
|
655
|
+
return lines.length > 0 ? lines.join('\n') + '\n' : ''
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
/**
|
|
659
|
+
* Lower a signal init / memo body to Perl for an in-template SSR seed, or
|
|
660
|
+
* `null` when it shouldn't be seeded this way. Returns null — without
|
|
661
|
+
* recording a BF101 — when the expression isn't a supported shape
|
|
662
|
+
* (`isSupported` pre-check, so object/array literals don't fail the build),
|
|
663
|
+
* when the lowering references no in-scope var (a constant — keep the
|
|
664
|
+
* existing ssr-defaults seeding), or when it references an out-of-scope
|
|
665
|
+
* binding. (#1297)
|
|
666
|
+
*/
|
|
667
|
+
private tryLowerToPerl(expr: string, available: ReadonlySet<string>): string | null {
|
|
668
|
+
const trimmed = expr.trim()
|
|
669
|
+
if (!trimmed) return null
|
|
670
|
+
if (!isSupported(parseExpression(trimmed)).supported) return null
|
|
671
|
+
const perl = this.convertExpressionToPerl(trimmed)
|
|
672
|
+
if (perl === '' || !/\$[A-Za-z_]\w*/.test(perl)) return null
|
|
673
|
+
return referencedVarsAreAvailable(perl, available) ? perl : null
|
|
484
674
|
}
|
|
485
675
|
|
|
486
676
|
emitAsync(node: IRAsync, _ctx: MojoRenderCtx, _emit: EmitIRNode<MojoRenderCtx>): string {
|
|
@@ -500,6 +690,14 @@ export class MojoAdapter extends BaseAdapter implements IRNodeEmitter<MojoRender
|
|
|
500
690
|
if (element.needsScope) {
|
|
501
691
|
hydrationAttrs += ` ${this.renderScopeMarker('')}`
|
|
502
692
|
}
|
|
693
|
+
// A root scope element carries `data-key` for a keyed loop item — emitted
|
|
694
|
+
// from the bf instance (the child renderer sets it from the JSX `key`
|
|
695
|
+
// prop), so a non-keyed render adds nothing. Mirrors Hono stamping
|
|
696
|
+
// data-key on each loop item's scope root, including early-return
|
|
697
|
+
// (if-statement) roots where every branch's top element qualifies. (#1297)
|
|
698
|
+
if (this.rootScopeNodes.has(element) && element.needsScope) {
|
|
699
|
+
hydrationAttrs += ` <%== bf->data_key_attr %>`
|
|
700
|
+
}
|
|
503
701
|
if (element.slotId) {
|
|
504
702
|
hydrationAttrs += ` ${this.renderSlotMarker(element.slotId)}`
|
|
505
703
|
}
|
|
@@ -969,7 +1167,7 @@ export class MojoAdapter extends BaseAdapter implements IRNodeEmitter<MojoRender
|
|
|
969
1167
|
return `<%= content %>`
|
|
970
1168
|
}
|
|
971
1169
|
|
|
972
|
-
renderAsync(node: IRAsync): string {
|
|
1170
|
+
override renderAsync(node: IRAsync): string {
|
|
973
1171
|
const fallback = this.renderNode(node.fallback)
|
|
974
1172
|
const children = this.renderChildren(node.children)
|
|
975
1173
|
// Use the BarefootJS.pm streaming helpers for OOS streaming.
|
package/src/build.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// Mojolicious build config factory for barefoot.config.ts
|
|
2
2
|
|
|
3
3
|
import type { BuildOptions } from '@barefootjs/jsx'
|
|
4
|
-
import { MojoAdapter } from './adapter'
|
|
5
|
-
import type { MojoAdapterOptions } from './adapter'
|
|
4
|
+
import { MojoAdapter } from './adapter/index.ts'
|
|
5
|
+
import type { MojoAdapterOptions } from './adapter/index.ts'
|
|
6
6
|
|
|
7
7
|
export interface MojoBuildOptions extends BuildOptions {
|
|
8
8
|
/** Adapter-specific options passed to MojoAdapter */
|
package/src/index.ts
CHANGED
|
@@ -4,5 +4,5 @@
|
|
|
4
4
|
* Generates Mojolicious EP template files from BarefootJS IR.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
export { MojoAdapter, mojoAdapter } from './adapter'
|
|
8
|
-
export type { MojoAdapterOptions } from './adapter'
|
|
7
|
+
export { MojoAdapter, mojoAdapter } from './adapter/index.ts'
|
|
8
|
+
export type { MojoAdapterOptions } from './adapter/index.ts'
|
package/src/test-render.ts
CHANGED
|
@@ -89,10 +89,17 @@ export interface RenderOptions {
|
|
|
89
89
|
props?: Record<string, unknown>
|
|
90
90
|
/** Additional component files (filename → source) */
|
|
91
91
|
components?: Record<string, string>
|
|
92
|
+
/**
|
|
93
|
+
* Explicit component to render when `source` declares multiple
|
|
94
|
+
* exports (e.g. `ReactiveProps.tsx` → `PropsReactivityComparison`).
|
|
95
|
+
* Mirrors the Hono reference's `componentName`; omitted for
|
|
96
|
+
* single-export fixtures, which fall back to the default/first export.
|
|
97
|
+
*/
|
|
98
|
+
componentName?: string
|
|
92
99
|
}
|
|
93
100
|
|
|
94
101
|
export async function renderMojoComponent(options: RenderOptions): Promise<string> {
|
|
95
|
-
const { source, adapter, props, components } = options
|
|
102
|
+
const { source, adapter, props, components, componentName: requestedName } = options
|
|
96
103
|
|
|
97
104
|
// Compile child components first
|
|
98
105
|
const childTemplates: Map<string, { template: string; ir: ComponentIR }> = new Map()
|
|
@@ -152,7 +159,12 @@ export async function renderMojoComponent(options: RenderOptions): Promise<strin
|
|
|
152
159
|
const irFiles = result.files.filter(f => f.type === 'ir')
|
|
153
160
|
if (irFiles.length === 0) throw new Error('No IR output (set outputIR: true)')
|
|
154
161
|
const irs = irFiles.map(f => JSON.parse(f.content) as ComponentIR)
|
|
162
|
+
// Explicit `componentName` wins (multi-export sources pin the render
|
|
163
|
+
// target); otherwise default-export, first inline-exported, first IR.
|
|
164
|
+
// Mirrors the Hono reference so multi-component fixtures render the
|
|
165
|
+
// same export across adapters.
|
|
155
166
|
const ir =
|
|
167
|
+
(requestedName ? irs.find(i => i.metadata.componentName === requestedName) : undefined) ??
|
|
156
168
|
irs.find(i => i.metadata.hasDefaultExport) ??
|
|
157
169
|
irs.find(i => i.metadata.isExported) ??
|
|
158
170
|
irs[0]
|
|
@@ -302,7 +314,7 @@ print $output;
|
|
|
302
314
|
*/
|
|
303
315
|
function buildChildRenderers(
|
|
304
316
|
childTemplates: Map<string, { template: string; ir: ComponentIR }>,
|
|
305
|
-
|
|
317
|
+
_parentIR: ComponentIR,
|
|
306
318
|
tempDir: string,
|
|
307
319
|
): string {
|
|
308
320
|
if (childTemplates.size === 0) return ''
|
|
@@ -314,21 +326,12 @@ function buildChildRenderers(
|
|
|
314
326
|
const snakeName = toSnakeCase(componentName)
|
|
315
327
|
const childTemplatePath = resolve(tempDir, `${snakeName}.html.ep`)
|
|
316
328
|
|
|
317
|
-
// Compute child scope ID from parent IR
|
|
318
|
-
const childSlotIds = findChildSlotIds(parentIR, componentName)
|
|
319
|
-
|
|
320
329
|
lines.push(`{`)
|
|
321
330
|
lines.push(` open my $child_fh, '<:utf8', '${childTemplatePath}' or die "Cannot open child template: $!";`)
|
|
322
331
|
lines.push(` my $child_tmpl = do { local $/; <$child_fh> };`)
|
|
323
332
|
lines.push(` close $child_fh;`)
|
|
324
333
|
lines.push(` my $child_mt = Mojo::Template->new(vars => 1, auto_escape => 1);`)
|
|
325
334
|
|
|
326
|
-
// Track instance counter for multiple-instances support
|
|
327
|
-
lines.push(` my $instance_idx = 0;`)
|
|
328
|
-
const slotIdsPerl = childSlotIds.length > 0
|
|
329
|
-
? `my @slot_ids = (${childSlotIds.map(id => `'${id}'`).join(', ')}); my $sid = $slot_ids[$instance_idx] // $slot_ids[-1]; $instance_idx++;`
|
|
330
|
-
: `my $sid = '${snakeName}';`
|
|
331
|
-
|
|
332
335
|
lines.push(` $bf->register_child_renderer('${snakeName}', sub {`)
|
|
333
336
|
lines.push(` my ($child_props) = @_;`)
|
|
334
337
|
// (#1652) A child that destructures a rest-spread bag
|
|
@@ -347,14 +350,22 @@ function buildChildRenderers(
|
|
|
347
350
|
const rest = childIR.metadata.restPropsName
|
|
348
351
|
lines.push(` $child_props->{${rest}} = {} unless defined $child_props->{${rest}};`)
|
|
349
352
|
}
|
|
350
|
-
lines.push(` ${slotIdsPerl}`)
|
|
351
353
|
lines.push(` my $child_bf = BarefootJS->new($c, {});`)
|
|
352
|
-
//
|
|
353
|
-
//
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
//
|
|
357
|
-
|
|
354
|
+
// JSX `key` (reserved prop) → data-key on the child's scope root, for
|
|
355
|
+
// keyed-loop reconciliation parity with Hono.
|
|
356
|
+
lines.push(` my $data_key = delete $child_props->{key};`)
|
|
357
|
+
lines.push(` $child_bf->_data_key($data_key) if defined $data_key;`)
|
|
358
|
+
// Scope id: a slotted child (`_bf_slot` passed) derives from the PARENT's
|
|
359
|
+
// live scope id (`<Parent_test>_s5`); a loop child (no slot) gets a fresh
|
|
360
|
+
// `<ComponentName>_<rand>` id per iteration, matching Hono — the
|
|
361
|
+
// PascalCase component name is what `normalizeHTML` canonicalises to
|
|
362
|
+
// `<ComponentName>_*`.
|
|
363
|
+
lines.push(` my $slot = delete $child_props->{_bf_slot};`)
|
|
364
|
+
lines.push(` if (defined $slot) {`)
|
|
365
|
+
lines.push(` $child_bf->_scope_id($bf->_scope_id . "_$slot");`)
|
|
366
|
+
lines.push(` } else {`)
|
|
367
|
+
lines.push(` $child_bf->_scope_id('${componentName}_' . substr(rand() =~ s/^0\\.//r, 0, 6));`)
|
|
368
|
+
lines.push(` }`)
|
|
358
369
|
lines.push(` my $rendered = $child_mt->render($child_tmpl, { %$child_props, bf => $child_bf });`)
|
|
359
370
|
lines.push(` die $rendered->to_string if ref $rendered;`)
|
|
360
371
|
lines.push(` chomp $rendered;`)
|
|
@@ -367,27 +378,6 @@ function buildChildRenderers(
|
|
|
367
378
|
return lines.join('\n')
|
|
368
379
|
}
|
|
369
380
|
|
|
370
|
-
/**
|
|
371
|
-
* Find slot IDs assigned to a child component in the parent IR.
|
|
372
|
-
*/
|
|
373
|
-
function findChildSlotIds(parentIR: ComponentIR, childName: string): string[] {
|
|
374
|
-
const ids: string[] = []
|
|
375
|
-
function walk(node: import('@barefootjs/jsx').IRNode): void {
|
|
376
|
-
if (node.type === 'component' && node.name === childName && node.slotId) {
|
|
377
|
-
ids.push(node.slotId)
|
|
378
|
-
}
|
|
379
|
-
if ('children' in node && Array.isArray(node.children)) {
|
|
380
|
-
for (const child of node.children) walk(child)
|
|
381
|
-
}
|
|
382
|
-
if (node.type === 'conditional') {
|
|
383
|
-
walk(node.whenTrue)
|
|
384
|
-
walk(node.whenFalse)
|
|
385
|
-
}
|
|
386
|
-
}
|
|
387
|
-
walk(parentIR.root)
|
|
388
|
-
return ids
|
|
389
|
-
}
|
|
390
|
-
|
|
391
381
|
/**
|
|
392
382
|
* Convert PascalCase to snake_case for Mojo template naming.
|
|
393
383
|
*/
|