@barefootjs/mojolicious 0.9.2 → 0.9.4
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.map +1 -1
- package/package.json +3 -3
- package/src/__tests__/mojo-adapter.test.ts +120 -33
- 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 +16 -38
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":"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"}
|
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
|
}
|
|
@@ -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;IACnC;;;;;OAKG;IACH,aAAa,CAAC,EAAE,MAAM,CAAA;CACvB;AAED,wBAAsB,mBAAmB,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,MAAM,CAAC,CAgNjF;
|
|
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.4",
|
|
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.4"
|
|
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.4"
|
|
64
64
|
}
|
|
65
65
|
}
|
|
@@ -17,39 +17,10 @@ runAdapterConformanceTests({
|
|
|
17
17
|
name: 'mojo',
|
|
18
18
|
factory: () => new MojoAdapter(),
|
|
19
19
|
render: renderMojoComponent,
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
'context-provider',
|
|
25
|
-
// `toggle-shared`: the parent maps a `ToggleItemProps[]` prop into
|
|
26
|
-
// sibling `ToggleItem` children inside a keyed `.map`. Each child's
|
|
27
|
-
// `on = props.defaultOn ?? false` signal is never seeded into the
|
|
28
|
-
// loop-child stash, so the rendered child template trips Perl strict
|
|
29
|
-
// mode (`Global symbol "$on" requires explicit package name`). The
|
|
30
|
-
// harness child renderer forwards only the explicitly-passed props;
|
|
31
|
-
// it does not run the `_derive_stash_from_defaults` seeding the
|
|
32
|
-
// production runtime applies. Plus the loop-child scope id is
|
|
33
|
-
// `toggle_item_<rand>` (snake-case template name) rather than the
|
|
34
|
-
// `ToggleItem_*` PascalCase the reference pins, and the `key=` →
|
|
35
|
-
// `data-key` attribute isn't emitted. Separate follow-up. (xslate
|
|
36
|
-
// skips this for the same reasons.)
|
|
37
|
-
'toggle-shared',
|
|
38
|
-
// `props-reactivity-comparison` (the `PropsReactivityComparison`
|
|
39
|
-
// export of `ReactiveProps.tsx`): componentName selection is now
|
|
40
|
-
// honoured (the test-render picks the requested export), but the
|
|
41
|
-
// child `PropsStyleChild` has a `displayValue = props.value * 10`
|
|
42
|
-
// memo. `extractSsrDefaults` cannot statically evaluate a
|
|
43
|
-
// prop-derived expression (it yields `null`), and the Perl SSR model
|
|
44
|
-
// seeds child memos from those *static* defaults — so `$displayValue`
|
|
45
|
-
// is never declared and the child render aborts under strict mode.
|
|
46
|
-
// Go matches only because it generates a child constructor that
|
|
47
|
-
// computes the memo from the passed prop at render time; the Perl
|
|
48
|
-
// adapters have no equivalent runtime memo-init in the static path
|
|
49
|
-
// (an opt-in `signal_init` sub would be required). (xslate skips this
|
|
50
|
-
// for the same reason.)
|
|
51
|
-
'props-reactivity-comparison',
|
|
52
|
-
],
|
|
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: [],
|
|
53
24
|
// Per-fixture build-time contracts for shapes the Mojo adapter
|
|
54
25
|
// intentionally refuses to lower. Owned by this adapter test file
|
|
55
26
|
// (not by the shared fixtures) so adding a new adapter doesn't
|
|
@@ -398,6 +369,122 @@ function C({ value = '' }: { value?: string }) {
|
|
|
398
369
|
})
|
|
399
370
|
})
|
|
400
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
|
+
|
|
401
488
|
describe('MojoAdapter - Template Generation', () => {
|
|
402
489
|
test('generates basic element with scope marker', () => {
|
|
403
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
|
@@ -314,7 +314,7 @@ print $output;
|
|
|
314
314
|
*/
|
|
315
315
|
function buildChildRenderers(
|
|
316
316
|
childTemplates: Map<string, { template: string; ir: ComponentIR }>,
|
|
317
|
-
|
|
317
|
+
_parentIR: ComponentIR,
|
|
318
318
|
tempDir: string,
|
|
319
319
|
): string {
|
|
320
320
|
if (childTemplates.size === 0) return ''
|
|
@@ -326,21 +326,12 @@ function buildChildRenderers(
|
|
|
326
326
|
const snakeName = toSnakeCase(componentName)
|
|
327
327
|
const childTemplatePath = resolve(tempDir, `${snakeName}.html.ep`)
|
|
328
328
|
|
|
329
|
-
// Compute child scope ID from parent IR
|
|
330
|
-
const childSlotIds = findChildSlotIds(parentIR, componentName)
|
|
331
|
-
|
|
332
329
|
lines.push(`{`)
|
|
333
330
|
lines.push(` open my $child_fh, '<:utf8', '${childTemplatePath}' or die "Cannot open child template: $!";`)
|
|
334
331
|
lines.push(` my $child_tmpl = do { local $/; <$child_fh> };`)
|
|
335
332
|
lines.push(` close $child_fh;`)
|
|
336
333
|
lines.push(` my $child_mt = Mojo::Template->new(vars => 1, auto_escape => 1);`)
|
|
337
334
|
|
|
338
|
-
// Track instance counter for multiple-instances support
|
|
339
|
-
lines.push(` my $instance_idx = 0;`)
|
|
340
|
-
const slotIdsPerl = childSlotIds.length > 0
|
|
341
|
-
? `my @slot_ids = (${childSlotIds.map(id => `'${id}'`).join(', ')}); my $sid = $slot_ids[$instance_idx] // $slot_ids[-1]; $instance_idx++;`
|
|
342
|
-
: `my $sid = '${snakeName}';`
|
|
343
|
-
|
|
344
335
|
lines.push(` $bf->register_child_renderer('${snakeName}', sub {`)
|
|
345
336
|
lines.push(` my ($child_props) = @_;`)
|
|
346
337
|
// (#1652) A child that destructures a rest-spread bag
|
|
@@ -359,14 +350,22 @@ function buildChildRenderers(
|
|
|
359
350
|
const rest = childIR.metadata.restPropsName
|
|
360
351
|
lines.push(` $child_props->{${rest}} = {} unless defined $child_props->{${rest}};`)
|
|
361
352
|
}
|
|
362
|
-
lines.push(` ${slotIdsPerl}`)
|
|
363
353
|
lines.push(` my $child_bf = BarefootJS->new($c, {});`)
|
|
364
|
-
//
|
|
365
|
-
//
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
//
|
|
369
|
-
|
|
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(` }`)
|
|
370
369
|
lines.push(` my $rendered = $child_mt->render($child_tmpl, { %$child_props, bf => $child_bf });`)
|
|
371
370
|
lines.push(` die $rendered->to_string if ref $rendered;`)
|
|
372
371
|
lines.push(` chomp $rendered;`)
|
|
@@ -379,27 +378,6 @@ function buildChildRenderers(
|
|
|
379
378
|
return lines.join('\n')
|
|
380
379
|
}
|
|
381
380
|
|
|
382
|
-
/**
|
|
383
|
-
* Find slot IDs assigned to a child component in the parent IR.
|
|
384
|
-
*/
|
|
385
|
-
function findChildSlotIds(parentIR: ComponentIR, childName: string): string[] {
|
|
386
|
-
const ids: string[] = []
|
|
387
|
-
function walk(node: import('@barefootjs/jsx').IRNode): void {
|
|
388
|
-
if (node.type === 'component' && node.name === childName && node.slotId) {
|
|
389
|
-
ids.push(node.slotId)
|
|
390
|
-
}
|
|
391
|
-
if ('children' in node && Array.isArray(node.children)) {
|
|
392
|
-
for (const child of node.children) walk(child)
|
|
393
|
-
}
|
|
394
|
-
if (node.type === 'conditional') {
|
|
395
|
-
walk(node.whenTrue)
|
|
396
|
-
walk(node.whenFalse)
|
|
397
|
-
}
|
|
398
|
-
}
|
|
399
|
-
walk(parentIR.root)
|
|
400
|
-
return ids
|
|
401
|
-
}
|
|
402
|
-
|
|
403
381
|
/**
|
|
404
382
|
* Convert PascalCase to snake_case for Mojo template naming.
|
|
405
383
|
*/
|