@barefootjs/mojolicious 0.6.1 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapter/index.js +56 -0
- package/dist/adapter/mojo-adapter.d.ts +37 -0
- package/dist/adapter/mojo-adapter.d.ts.map +1 -1
- package/dist/build.js +56 -0
- package/dist/index.js +56 -0
- package/dist/test-render.d.ts.map +1 -1
- package/lib/BarefootJS/Backend/Mojo.pm +91 -0
- package/lib/Mojolicious/Plugin/BarefootJS/DevReload.pm +1 -0
- package/lib/Mojolicious/Plugin/BarefootJS.pm +56 -0
- package/package.json +7 -6
- package/src/__tests__/mojo-adapter.test.ts +47 -10
- package/src/adapter/mojo-adapter.ts +108 -1
- package/src/test-render.ts +30 -3
- package/lib/BarefootJS.pm +0 -1060
package/dist/adapter/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
// src/adapter/mojo-adapter.ts
|
|
2
|
+
import ts from "typescript";
|
|
2
3
|
import {
|
|
3
4
|
BaseAdapter,
|
|
4
5
|
isBooleanAttr,
|
|
@@ -80,6 +81,22 @@ function resolveJsxChildrenProp(props) {
|
|
|
80
81
|
return [];
|
|
81
82
|
return prop.value.children;
|
|
82
83
|
}
|
|
84
|
+
function parsePureStringLiteral(source) {
|
|
85
|
+
const sf = ts.createSourceFile("__const.ts", `const __x = (${source});`, ts.ScriptTarget.Latest, false);
|
|
86
|
+
const stmt = sf.statements[0];
|
|
87
|
+
if (!stmt || !ts.isVariableStatement(stmt))
|
|
88
|
+
return null;
|
|
89
|
+
const decl = stmt.declarationList.declarations[0];
|
|
90
|
+
let init = decl?.initializer;
|
|
91
|
+
while (init && ts.isParenthesizedExpression(init))
|
|
92
|
+
init = init.expression;
|
|
93
|
+
if (!init)
|
|
94
|
+
return null;
|
|
95
|
+
if (ts.isStringLiteral(init) || ts.isNoSubstitutionTemplateLiteral(init)) {
|
|
96
|
+
return init.text;
|
|
97
|
+
}
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
83
100
|
|
|
84
101
|
class MojoAdapter extends BaseAdapter {
|
|
85
102
|
name = "mojolicious";
|
|
@@ -94,6 +111,8 @@ class MojoAdapter extends BaseAdapter {
|
|
|
94
111
|
propsObjectName = null;
|
|
95
112
|
propsParams = [];
|
|
96
113
|
stringValueNames = new Set;
|
|
114
|
+
moduleStringConsts = new Map;
|
|
115
|
+
loopBoundNames = new Map;
|
|
97
116
|
constructor(options = {}) {
|
|
98
117
|
super();
|
|
99
118
|
this.options = {
|
|
@@ -115,6 +134,8 @@ class MojoAdapter extends BaseAdapter {
|
|
|
115
134
|
if (isStringTypeInfo(p.type))
|
|
116
135
|
this.stringValueNames.add(p.name);
|
|
117
136
|
}
|
|
137
|
+
this.moduleStringConsts = this.collectModuleStringConsts(ir.metadata.localConstants);
|
|
138
|
+
this.loopBoundNames.clear();
|
|
118
139
|
this.errors = [];
|
|
119
140
|
this.childrenCaptureCounter = 0;
|
|
120
141
|
if (!options?.siblingTemplatesRegistered) {
|
|
@@ -139,6 +160,27 @@ class MojoAdapter extends BaseAdapter {
|
|
|
139
160
|
extension: this.extension
|
|
140
161
|
};
|
|
141
162
|
}
|
|
163
|
+
collectModuleStringConsts(constants) {
|
|
164
|
+
const map = new Map;
|
|
165
|
+
for (const c of constants ?? []) {
|
|
166
|
+
if (!c.isModule)
|
|
167
|
+
continue;
|
|
168
|
+
if (c.value === undefined)
|
|
169
|
+
continue;
|
|
170
|
+
const literal = parsePureStringLiteral(c.value);
|
|
171
|
+
if (literal !== null)
|
|
172
|
+
map.set(c.name, literal);
|
|
173
|
+
}
|
|
174
|
+
return map;
|
|
175
|
+
}
|
|
176
|
+
resolveModuleStringConst(name) {
|
|
177
|
+
if (this.loopBoundNames.has(name))
|
|
178
|
+
return null;
|
|
179
|
+
const value = this.moduleStringConsts.get(name);
|
|
180
|
+
if (value === undefined)
|
|
181
|
+
return null;
|
|
182
|
+
return `'${value.replace(/[\\']/g, (m) => `\\${m}`)}'`;
|
|
183
|
+
}
|
|
142
184
|
generateScriptRegistrations(ir, scriptBaseName) {
|
|
143
185
|
const hasInteractivity = this.hasClientInteractivity(ir);
|
|
144
186
|
if (!hasInteractivity)
|
|
@@ -405,6 +447,10 @@ ${whenTrue}
|
|
|
405
447
|
}
|
|
406
448
|
const param = loop.param;
|
|
407
449
|
const indexVar = loop.iterationShape === "keys" ? `$${param}` : loop.index ? `$${loop.index}` : "$_i";
|
|
450
|
+
const loopBound = loop.iterationShape === "keys" ? [param] : [param, loop.index ?? "_i"];
|
|
451
|
+
for (const n of loopBound) {
|
|
452
|
+
this.loopBoundNames.set(n, (this.loopBoundNames.get(n) ?? 0) + 1);
|
|
453
|
+
}
|
|
408
454
|
const prevInLoop = this.inLoop;
|
|
409
455
|
this.inLoop = true;
|
|
410
456
|
const renderedChildren = this.renderChildren(loop.children);
|
|
@@ -438,6 +484,13 @@ ${renderedChildren}` : renderedChildren;
|
|
|
438
484
|
} else {
|
|
439
485
|
lines.push(children);
|
|
440
486
|
}
|
|
487
|
+
for (const n of loopBound) {
|
|
488
|
+
const c = (this.loopBoundNames.get(n) ?? 1) - 1;
|
|
489
|
+
if (c <= 0)
|
|
490
|
+
this.loopBoundNames.delete(n);
|
|
491
|
+
else
|
|
492
|
+
this.loopBoundNames.set(n, c);
|
|
493
|
+
}
|
|
441
494
|
lines.push(`% }`);
|
|
442
495
|
lines.push(`<%== bf->comment("/loop:${loop.markerId}") %>`);
|
|
443
496
|
return lines.join(`
|
|
@@ -1056,6 +1109,9 @@ class MojoTopLevelEmitter {
|
|
|
1056
1109
|
this.adapter = adapter;
|
|
1057
1110
|
}
|
|
1058
1111
|
identifier(name) {
|
|
1112
|
+
const inlined = this.adapter.resolveModuleStringConst(name);
|
|
1113
|
+
if (inlined !== null)
|
|
1114
|
+
return inlined;
|
|
1059
1115
|
return `$${name}`;
|
|
1060
1116
|
}
|
|
1061
1117
|
literal(value, literalType) {
|
|
@@ -58,8 +58,45 @@ export declare class MojoAdapter extends BaseAdapter implements IRNodeEmitter<Mo
|
|
|
58
58
|
* true — selecting the string operator from the operand's type avoids that.
|
|
59
59
|
*/
|
|
60
60
|
private stringValueNames;
|
|
61
|
+
/**
|
|
62
|
+
* Module-scope pure string-literal constants (`const X = 'literal'` at
|
|
63
|
+
* file top-level), keyed by name → resolved literal value. Populated at
|
|
64
|
+
* `generate()` entry from `ir.metadata.localConstants`. When an identifier
|
|
65
|
+
* in an expression resolves to one of these, the adapter inlines the
|
|
66
|
+
* literal instead of emitting `$X` against a stash variable that is never
|
|
67
|
+
* bound (a module const isn't a prop, signal, or local — the value would
|
|
68
|
+
* render empty). Hono inlines it for free; this restores parity. Only
|
|
69
|
+
* module-scope pure string literals qualify (see `collectModuleStringConsts`).
|
|
70
|
+
*/
|
|
71
|
+
private moduleStringConsts;
|
|
72
|
+
/**
|
|
73
|
+
* Names currently bound by an enclosing loop body — the `my $<param>` and
|
|
74
|
+
* `my $<index>` bindings `renderLoop` introduces — ref-counted so nested
|
|
75
|
+
* loops compose. `resolveModuleStringConst` consults this so a loop
|
|
76
|
+
* variable whose name happens to match a module string const is NOT
|
|
77
|
+
* inlined as the const literal (mirrors the Go adapter's loop-param /
|
|
78
|
+
* loop-var shadowing guards). (#1749 review)
|
|
79
|
+
*/
|
|
80
|
+
private loopBoundNames;
|
|
61
81
|
constructor(options?: MojoAdapterOptions);
|
|
62
82
|
generate(ir: ComponentIR, options?: AdapterGenerateOptions): AdapterOutput;
|
|
83
|
+
/**
|
|
84
|
+
* Build the module pure-string-const map from the IR's localConstants.
|
|
85
|
+
* A const qualifies only when it is module-scope (`isModule`) and its
|
|
86
|
+
* initializer parses to a single string literal (`ts.StringLiteral` or
|
|
87
|
+
* `ts.NoSubstitutionTemplateLiteral`). Template literals with `${}`,
|
|
88
|
+
* numeric/object initializers, `Record<T,string>` maps, memos, and
|
|
89
|
+
* signals are all excluded — only a pure compile-time string can be
|
|
90
|
+
* inlined byte-for-byte.
|
|
91
|
+
*/
|
|
92
|
+
private collectModuleStringConsts;
|
|
93
|
+
/**
|
|
94
|
+
* Resolve an identifier to its inlined Perl single-quoted string literal
|
|
95
|
+
* when it names a module pure-string const, else `null` (the caller then
|
|
96
|
+
* falls back to its normal `$name` stash lowering). Returns the Perl
|
|
97
|
+
* literal form `'<escaped>'` ready to drop into an expression.
|
|
98
|
+
*/
|
|
99
|
+
resolveModuleStringConst(name: string): string | null;
|
|
63
100
|
private generateScriptRegistrations;
|
|
64
101
|
private hasClientInteractivity;
|
|
65
102
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mojo-adapter.d.ts","sourceRoot":"","sources":["../../src/adapter/mojo-adapter.ts"],"names":[],"mappings":"AAAA;;;;GAIG;
|
|
1
|
+
{"version":3,"file":"mojo-adapter.d.ts","sourceRoot":"","sources":["../../src/adapter/mojo-adapter.ts"],"names":[],"mappings":"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"}
|
package/dist/build.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
// src/adapter/mojo-adapter.ts
|
|
2
|
+
import ts from "typescript";
|
|
2
3
|
import {
|
|
3
4
|
BaseAdapter,
|
|
4
5
|
isBooleanAttr,
|
|
@@ -80,6 +81,22 @@ function resolveJsxChildrenProp(props) {
|
|
|
80
81
|
return [];
|
|
81
82
|
return prop.value.children;
|
|
82
83
|
}
|
|
84
|
+
function parsePureStringLiteral(source) {
|
|
85
|
+
const sf = ts.createSourceFile("__const.ts", `const __x = (${source});`, ts.ScriptTarget.Latest, false);
|
|
86
|
+
const stmt = sf.statements[0];
|
|
87
|
+
if (!stmt || !ts.isVariableStatement(stmt))
|
|
88
|
+
return null;
|
|
89
|
+
const decl = stmt.declarationList.declarations[0];
|
|
90
|
+
let init = decl?.initializer;
|
|
91
|
+
while (init && ts.isParenthesizedExpression(init))
|
|
92
|
+
init = init.expression;
|
|
93
|
+
if (!init)
|
|
94
|
+
return null;
|
|
95
|
+
if (ts.isStringLiteral(init) || ts.isNoSubstitutionTemplateLiteral(init)) {
|
|
96
|
+
return init.text;
|
|
97
|
+
}
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
83
100
|
|
|
84
101
|
class MojoAdapter extends BaseAdapter {
|
|
85
102
|
name = "mojolicious";
|
|
@@ -94,6 +111,8 @@ class MojoAdapter extends BaseAdapter {
|
|
|
94
111
|
propsObjectName = null;
|
|
95
112
|
propsParams = [];
|
|
96
113
|
stringValueNames = new Set;
|
|
114
|
+
moduleStringConsts = new Map;
|
|
115
|
+
loopBoundNames = new Map;
|
|
97
116
|
constructor(options = {}) {
|
|
98
117
|
super();
|
|
99
118
|
this.options = {
|
|
@@ -115,6 +134,8 @@ class MojoAdapter extends BaseAdapter {
|
|
|
115
134
|
if (isStringTypeInfo(p.type))
|
|
116
135
|
this.stringValueNames.add(p.name);
|
|
117
136
|
}
|
|
137
|
+
this.moduleStringConsts = this.collectModuleStringConsts(ir.metadata.localConstants);
|
|
138
|
+
this.loopBoundNames.clear();
|
|
118
139
|
this.errors = [];
|
|
119
140
|
this.childrenCaptureCounter = 0;
|
|
120
141
|
if (!options?.siblingTemplatesRegistered) {
|
|
@@ -139,6 +160,27 @@ class MojoAdapter extends BaseAdapter {
|
|
|
139
160
|
extension: this.extension
|
|
140
161
|
};
|
|
141
162
|
}
|
|
163
|
+
collectModuleStringConsts(constants) {
|
|
164
|
+
const map = new Map;
|
|
165
|
+
for (const c of constants ?? []) {
|
|
166
|
+
if (!c.isModule)
|
|
167
|
+
continue;
|
|
168
|
+
if (c.value === undefined)
|
|
169
|
+
continue;
|
|
170
|
+
const literal = parsePureStringLiteral(c.value);
|
|
171
|
+
if (literal !== null)
|
|
172
|
+
map.set(c.name, literal);
|
|
173
|
+
}
|
|
174
|
+
return map;
|
|
175
|
+
}
|
|
176
|
+
resolveModuleStringConst(name) {
|
|
177
|
+
if (this.loopBoundNames.has(name))
|
|
178
|
+
return null;
|
|
179
|
+
const value = this.moduleStringConsts.get(name);
|
|
180
|
+
if (value === undefined)
|
|
181
|
+
return null;
|
|
182
|
+
return `'${value.replace(/[\\']/g, (m) => `\\${m}`)}'`;
|
|
183
|
+
}
|
|
142
184
|
generateScriptRegistrations(ir, scriptBaseName) {
|
|
143
185
|
const hasInteractivity = this.hasClientInteractivity(ir);
|
|
144
186
|
if (!hasInteractivity)
|
|
@@ -405,6 +447,10 @@ ${whenTrue}
|
|
|
405
447
|
}
|
|
406
448
|
const param = loop.param;
|
|
407
449
|
const indexVar = loop.iterationShape === "keys" ? `$${param}` : loop.index ? `$${loop.index}` : "$_i";
|
|
450
|
+
const loopBound = loop.iterationShape === "keys" ? [param] : [param, loop.index ?? "_i"];
|
|
451
|
+
for (const n of loopBound) {
|
|
452
|
+
this.loopBoundNames.set(n, (this.loopBoundNames.get(n) ?? 0) + 1);
|
|
453
|
+
}
|
|
408
454
|
const prevInLoop = this.inLoop;
|
|
409
455
|
this.inLoop = true;
|
|
410
456
|
const renderedChildren = this.renderChildren(loop.children);
|
|
@@ -438,6 +484,13 @@ ${renderedChildren}` : renderedChildren;
|
|
|
438
484
|
} else {
|
|
439
485
|
lines.push(children);
|
|
440
486
|
}
|
|
487
|
+
for (const n of loopBound) {
|
|
488
|
+
const c = (this.loopBoundNames.get(n) ?? 1) - 1;
|
|
489
|
+
if (c <= 0)
|
|
490
|
+
this.loopBoundNames.delete(n);
|
|
491
|
+
else
|
|
492
|
+
this.loopBoundNames.set(n, c);
|
|
493
|
+
}
|
|
441
494
|
lines.push(`% }`);
|
|
442
495
|
lines.push(`<%== bf->comment("/loop:${loop.markerId}") %>`);
|
|
443
496
|
return lines.join(`
|
|
@@ -1056,6 +1109,9 @@ class MojoTopLevelEmitter {
|
|
|
1056
1109
|
this.adapter = adapter;
|
|
1057
1110
|
}
|
|
1058
1111
|
identifier(name) {
|
|
1112
|
+
const inlined = this.adapter.resolveModuleStringConst(name);
|
|
1113
|
+
if (inlined !== null)
|
|
1114
|
+
return inlined;
|
|
1059
1115
|
return `$${name}`;
|
|
1060
1116
|
}
|
|
1061
1117
|
literal(value, literalType) {
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
// src/adapter/mojo-adapter.ts
|
|
2
|
+
import ts from "typescript";
|
|
2
3
|
import {
|
|
3
4
|
BaseAdapter,
|
|
4
5
|
isBooleanAttr,
|
|
@@ -80,6 +81,22 @@ function resolveJsxChildrenProp(props) {
|
|
|
80
81
|
return [];
|
|
81
82
|
return prop.value.children;
|
|
82
83
|
}
|
|
84
|
+
function parsePureStringLiteral(source) {
|
|
85
|
+
const sf = ts.createSourceFile("__const.ts", `const __x = (${source});`, ts.ScriptTarget.Latest, false);
|
|
86
|
+
const stmt = sf.statements[0];
|
|
87
|
+
if (!stmt || !ts.isVariableStatement(stmt))
|
|
88
|
+
return null;
|
|
89
|
+
const decl = stmt.declarationList.declarations[0];
|
|
90
|
+
let init = decl?.initializer;
|
|
91
|
+
while (init && ts.isParenthesizedExpression(init))
|
|
92
|
+
init = init.expression;
|
|
93
|
+
if (!init)
|
|
94
|
+
return null;
|
|
95
|
+
if (ts.isStringLiteral(init) || ts.isNoSubstitutionTemplateLiteral(init)) {
|
|
96
|
+
return init.text;
|
|
97
|
+
}
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
83
100
|
|
|
84
101
|
class MojoAdapter extends BaseAdapter {
|
|
85
102
|
name = "mojolicious";
|
|
@@ -94,6 +111,8 @@ class MojoAdapter extends BaseAdapter {
|
|
|
94
111
|
propsObjectName = null;
|
|
95
112
|
propsParams = [];
|
|
96
113
|
stringValueNames = new Set;
|
|
114
|
+
moduleStringConsts = new Map;
|
|
115
|
+
loopBoundNames = new Map;
|
|
97
116
|
constructor(options = {}) {
|
|
98
117
|
super();
|
|
99
118
|
this.options = {
|
|
@@ -115,6 +134,8 @@ class MojoAdapter extends BaseAdapter {
|
|
|
115
134
|
if (isStringTypeInfo(p.type))
|
|
116
135
|
this.stringValueNames.add(p.name);
|
|
117
136
|
}
|
|
137
|
+
this.moduleStringConsts = this.collectModuleStringConsts(ir.metadata.localConstants);
|
|
138
|
+
this.loopBoundNames.clear();
|
|
118
139
|
this.errors = [];
|
|
119
140
|
this.childrenCaptureCounter = 0;
|
|
120
141
|
if (!options?.siblingTemplatesRegistered) {
|
|
@@ -139,6 +160,27 @@ class MojoAdapter extends BaseAdapter {
|
|
|
139
160
|
extension: this.extension
|
|
140
161
|
};
|
|
141
162
|
}
|
|
163
|
+
collectModuleStringConsts(constants) {
|
|
164
|
+
const map = new Map;
|
|
165
|
+
for (const c of constants ?? []) {
|
|
166
|
+
if (!c.isModule)
|
|
167
|
+
continue;
|
|
168
|
+
if (c.value === undefined)
|
|
169
|
+
continue;
|
|
170
|
+
const literal = parsePureStringLiteral(c.value);
|
|
171
|
+
if (literal !== null)
|
|
172
|
+
map.set(c.name, literal);
|
|
173
|
+
}
|
|
174
|
+
return map;
|
|
175
|
+
}
|
|
176
|
+
resolveModuleStringConst(name) {
|
|
177
|
+
if (this.loopBoundNames.has(name))
|
|
178
|
+
return null;
|
|
179
|
+
const value = this.moduleStringConsts.get(name);
|
|
180
|
+
if (value === undefined)
|
|
181
|
+
return null;
|
|
182
|
+
return `'${value.replace(/[\\']/g, (m) => `\\${m}`)}'`;
|
|
183
|
+
}
|
|
142
184
|
generateScriptRegistrations(ir, scriptBaseName) {
|
|
143
185
|
const hasInteractivity = this.hasClientInteractivity(ir);
|
|
144
186
|
if (!hasInteractivity)
|
|
@@ -405,6 +447,10 @@ ${whenTrue}
|
|
|
405
447
|
}
|
|
406
448
|
const param = loop.param;
|
|
407
449
|
const indexVar = loop.iterationShape === "keys" ? `$${param}` : loop.index ? `$${loop.index}` : "$_i";
|
|
450
|
+
const loopBound = loop.iterationShape === "keys" ? [param] : [param, loop.index ?? "_i"];
|
|
451
|
+
for (const n of loopBound) {
|
|
452
|
+
this.loopBoundNames.set(n, (this.loopBoundNames.get(n) ?? 0) + 1);
|
|
453
|
+
}
|
|
408
454
|
const prevInLoop = this.inLoop;
|
|
409
455
|
this.inLoop = true;
|
|
410
456
|
const renderedChildren = this.renderChildren(loop.children);
|
|
@@ -438,6 +484,13 @@ ${renderedChildren}` : renderedChildren;
|
|
|
438
484
|
} else {
|
|
439
485
|
lines.push(children);
|
|
440
486
|
}
|
|
487
|
+
for (const n of loopBound) {
|
|
488
|
+
const c = (this.loopBoundNames.get(n) ?? 1) - 1;
|
|
489
|
+
if (c <= 0)
|
|
490
|
+
this.loopBoundNames.delete(n);
|
|
491
|
+
else
|
|
492
|
+
this.loopBoundNames.set(n, c);
|
|
493
|
+
}
|
|
441
494
|
lines.push(`% }`);
|
|
442
495
|
lines.push(`<%== bf->comment("/loop:${loop.markerId}") %>`);
|
|
443
496
|
return lines.join(`
|
|
@@ -1056,6 +1109,9 @@ class MojoTopLevelEmitter {
|
|
|
1056
1109
|
this.adapter = adapter;
|
|
1057
1110
|
}
|
|
1058
1111
|
identifier(name) {
|
|
1112
|
+
const inlined = this.adapter.resolveModuleStringConst(name);
|
|
1113
|
+
if (inlined !== null)
|
|
1114
|
+
return inlined;
|
|
1059
1115
|
return `$${name}`;
|
|
1060
1116
|
}
|
|
1061
1117
|
literal(value, literalType) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"test-render.d.ts","sourceRoot":"","sources":["../src/test-render.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;
|
|
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;CACpC;AAED,wBAAsB,mBAAmB,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,MAAM,CAAC,CA2MjF;AAsOD;;;GAGG;AACH,wBAAgB,kBAAkB,CAChC,IAAI,EAAE,MAAM,EACZ,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC9B,OAAO,CAuBT"}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
package BarefootJS::Backend::Mojo;
|
|
2
|
+
our $VERSION = "0.01";
|
|
3
|
+
use Mojo::Base -base, -signatures;
|
|
4
|
+
|
|
5
|
+
use Mojo::ByteStream qw(b);
|
|
6
|
+
use Mojo::JSON qw(to_json);
|
|
7
|
+
use Scalar::Util qw(weaken);
|
|
8
|
+
|
|
9
|
+
# ---------------------------------------------------------------------------
|
|
10
|
+
# Reference rendering backend (Mojolicious / Mojo::Template).
|
|
11
|
+
# ---------------------------------------------------------------------------
|
|
12
|
+
#
|
|
13
|
+
# BarefootJS.pm holds all the template-engine-agnostic logic (the JS-compat
|
|
14
|
+
# value helpers, array/string methods, hydration markers). Everything that is
|
|
15
|
+
# specific to *how a template is rendered* — JSON marshalling, raw-string
|
|
16
|
+
# marking, JSX-children materialisation, and named-template rendering — lives
|
|
17
|
+
# behind this backend object so the same runtime can drive a different Perl
|
|
18
|
+
# template engine (Text::Xslate, Template Toolkit, …) without rewriting the
|
|
19
|
+
# helper surface.
|
|
20
|
+
#
|
|
21
|
+
# A backend MUST implement:
|
|
22
|
+
# - encode_json($data) -> string
|
|
23
|
+
# - mark_raw($str) -> value the engine emits without escaping
|
|
24
|
+
# - materialize($value) -> string (resolve a captured-children ref)
|
|
25
|
+
# - render_named($name, $bf, \%vars) -> string
|
|
26
|
+
#
|
|
27
|
+
# This Mojo implementation is the reference. To target another engine, write a
|
|
28
|
+
# sibling backend (BarefootJS::Backend::Xslate, …) implementing the same four
|
|
29
|
+
# methods and pass it via `BarefootJS->new($c, { backend => $b })`.
|
|
30
|
+
|
|
31
|
+
# The Mojolicious controller. Optional: the value-marshalling helpers
|
|
32
|
+
# (`encode_json` / `mark_raw` / `materialize`) work without it; only
|
|
33
|
+
# `render_named` reaches into the controller's renderer + stash.
|
|
34
|
+
has 'c';
|
|
35
|
+
|
|
36
|
+
# Pluggable JSON encoder (#engine-abstraction). Defaults to
|
|
37
|
+
# `Mojo::JSON::to_json`, which returns a *character* string (not bytes)
|
|
38
|
+
# suitable for embedding in HTML output via `<%==` / Mojo::ByteStream.
|
|
39
|
+
#
|
|
40
|
+
# Override with any `sub ($data) { ... }` to swap in a faster XS encoder —
|
|
41
|
+
# e.g. `json_encoder => sub { Cpanel::JSON::XS->new->canonical->encode($_[0]) }`.
|
|
42
|
+
# The pure-Perl JSON::PP fallback Mojo::JSON uses can be a hot spot for large
|
|
43
|
+
# props payloads; the seam lets a host pick its own implementation without
|
|
44
|
+
# touching the runtime.
|
|
45
|
+
has 'json_encoder' => sub { \&to_json };
|
|
46
|
+
|
|
47
|
+
# Hold the controller weakly for the same reason BarefootJS does: the
|
|
48
|
+
# controller owns the bf instance (which owns this backend) via its stash,
|
|
49
|
+
# so a strong back-reference would close a per-request cycle the refcount GC
|
|
50
|
+
# can't reclaim. `render_named` only touches `$self->c` mid-render, while the
|
|
51
|
+
# controller is still alive on the request stack.
|
|
52
|
+
sub new ($class, %args) {
|
|
53
|
+
my $self = $class->SUPER::new(%args);
|
|
54
|
+
weaken($self->{c}) if $self->{c};
|
|
55
|
+
return $self;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
sub encode_json ($self, $data) {
|
|
59
|
+
return $self->json_encoder->($data);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
# Mark a string as already-safe so the template engine emits it verbatim
|
|
63
|
+
# (no re-escaping). In Mojo this is a Mojo::ByteStream, which the calling
|
|
64
|
+
# template's `<%==` raw-emit passes through unescaped.
|
|
65
|
+
sub mark_raw ($self, $str) {
|
|
66
|
+
return b($str);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
# JSX children / async fallbacks arrive via Mojo's `begin %>...<% end`
|
|
70
|
+
# capture, which produces a CODE ref returning a Mojo::ByteStream. Resolve
|
|
71
|
+
# it to a string before embedding. Plain (already-rendered) strings pass
|
|
72
|
+
# through unchanged.
|
|
73
|
+
sub materialize ($self, $value) {
|
|
74
|
+
return ref($value) eq 'CODE' ? $value->() : $value;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
# Render a named template with `$child_bf` bound as the active runtime
|
|
78
|
+
# instance for that render. The Mojo `bf` helper resolves the current
|
|
79
|
+
# instance off `$c->stash->{'bf.instance'}`; swap it for the duration of
|
|
80
|
+
# the nested render and restore it afterwards so sibling renders are
|
|
81
|
+
# unaffected.
|
|
82
|
+
sub render_named ($self, $template_name, $child_bf, $vars) {
|
|
83
|
+
my $c = $self->c;
|
|
84
|
+
my $prev = $c->stash->{'bf.instance'};
|
|
85
|
+
$c->stash->{'bf.instance'} = $child_bf;
|
|
86
|
+
my $html = $c->render_to_string(template => $template_name, %$vars);
|
|
87
|
+
$c->stash->{'bf.instance'} = $prev;
|
|
88
|
+
return $html;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
1;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
package Mojolicious::Plugin::BarefootJS;
|
|
2
|
+
our $VERSION = "0.01";
|
|
2
3
|
use Mojo::Base 'Mojolicious::Plugin', -signatures;
|
|
3
4
|
|
|
4
5
|
use Mojo::File qw(path);
|
|
@@ -102,3 +103,58 @@ sub _load_manifest ($app, $config) {
|
|
|
102
103
|
}
|
|
103
104
|
|
|
104
105
|
1;
|
|
106
|
+
__END__
|
|
107
|
+
|
|
108
|
+
=encoding utf8
|
|
109
|
+
|
|
110
|
+
=head1 NAME
|
|
111
|
+
|
|
112
|
+
Mojolicious::Plugin::BarefootJS - Mojolicious integration for BarefootJS
|
|
113
|
+
|
|
114
|
+
=head1 SYNOPSIS
|
|
115
|
+
|
|
116
|
+
# Mojolicious application
|
|
117
|
+
$self->plugin('BarefootJS');
|
|
118
|
+
|
|
119
|
+
# In a controller / template, the `bf` helper exposes a per-request
|
|
120
|
+
# BarefootJS runtime backed by BarefootJS::Backend::Mojo.
|
|
121
|
+
|
|
122
|
+
=head1 DESCRIPTION
|
|
123
|
+
|
|
124
|
+
Wires the L<BarefootJS> server runtime into L<Mojolicious>. It registers a
|
|
125
|
+
C<bf> controller helper that lazily instantiates one BarefootJS object per
|
|
126
|
+
request (rendering via L<BarefootJS::Backend::Mojo>), and supports rendering
|
|
127
|
+
compiled marked templates as Mojolicious templates.
|
|
128
|
+
|
|
129
|
+
For non-Mojolicious / PSGI hosts, see L<BarefootJS::Backend::Xslate>, which
|
|
130
|
+
drives the same runtime with Text::Xslate and no web framework.
|
|
131
|
+
|
|
132
|
+
=head1 METHODS
|
|
133
|
+
|
|
134
|
+
L<Mojolicious::Plugin::BarefootJS> inherits all methods from
|
|
135
|
+
L<Mojolicious::Plugin> and implements the following new one.
|
|
136
|
+
|
|
137
|
+
=head2 register
|
|
138
|
+
|
|
139
|
+
$plugin->register(Mojolicious->new, \%conf);
|
|
140
|
+
|
|
141
|
+
Registers the plugin (the C<bf> helper and supporting hooks) in a Mojolicious
|
|
142
|
+
application.
|
|
143
|
+
|
|
144
|
+
=head1 SEE ALSO
|
|
145
|
+
|
|
146
|
+
L<BarefootJS>, L<BarefootJS::Backend::Mojo>, L<BarefootJS::Backend::Xslate>,
|
|
147
|
+
L<Mojolicious>, L<https://github.com/piconic-ai/barefootjs>
|
|
148
|
+
|
|
149
|
+
=head1 AUTHOR
|
|
150
|
+
|
|
151
|
+
kobaken E<lt>kentafly88@gmail.comE<gt>
|
|
152
|
+
|
|
153
|
+
=head1 LICENSE
|
|
154
|
+
|
|
155
|
+
Copyright (c) 2025-present BarefootJS Contributors.
|
|
156
|
+
|
|
157
|
+
This library is free software; you can redistribute it and/or modify it under
|
|
158
|
+
the MIT License. See the F<LICENSE> file in the distribution for the full text.
|
|
159
|
+
|
|
160
|
+
=cut
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@barefootjs/mojolicious",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
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",
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
],
|
|
30
30
|
"scripts": {
|
|
31
31
|
"build": "bun run build:js && bun run build:types",
|
|
32
|
-
"build:js": "bun build ./src/index.ts ./src/adapter/index.ts ./src/build.ts --root ./src --outdir ./dist --format esm --external @barefootjs/jsx --external @barefootjs/shared",
|
|
32
|
+
"build:js": "bun build ./src/index.ts ./src/adapter/index.ts ./src/build.ts --root ./src --outdir ./dist --format esm --external @barefootjs/jsx --external @barefootjs/shared --external typescript",
|
|
33
33
|
"build:types": "tsgo --emitDeclarationOnly --outDir ./dist",
|
|
34
34
|
"test": "bun test",
|
|
35
35
|
"clean": "rm -rf dist",
|
|
@@ -52,14 +52,15 @@
|
|
|
52
52
|
"directory": "packages/adapter-mojolicious"
|
|
53
53
|
},
|
|
54
54
|
"dependencies": {
|
|
55
|
-
"@barefootjs/
|
|
55
|
+
"@barefootjs/perl": "0.7.0",
|
|
56
|
+
"@barefootjs/shared": "0.7.0"
|
|
56
57
|
},
|
|
57
58
|
"peerDependencies": {
|
|
58
|
-
"@barefootjs/jsx": ">=0.2.0"
|
|
59
|
+
"@barefootjs/jsx": ">=0.2.0",
|
|
60
|
+
"typescript": "^5.0.0"
|
|
59
61
|
},
|
|
60
62
|
"devDependencies": {
|
|
61
63
|
"@barefootjs/adapter-tests": "0.1.0",
|
|
62
|
-
"@barefootjs/jsx": "0.
|
|
63
|
-
"typescript": "^5.0.0"
|
|
64
|
+
"@barefootjs/jsx": "0.7.0"
|
|
64
65
|
}
|
|
65
66
|
}
|
|
@@ -69,16 +69,17 @@ runAdapterConformanceTests({
|
|
|
69
69
|
'toggle-shared',
|
|
70
70
|
'reactive-props',
|
|
71
71
|
'props-reactivity-comparison',
|
|
72
|
-
// #1467 Phase
|
|
73
|
-
//
|
|
74
|
-
//
|
|
75
|
-
//
|
|
76
|
-
//
|
|
77
|
-
//
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
'
|
|
72
|
+
// #1467 Phase 2b: basic interactive `site/ui` primitives. Cross-adapter
|
|
73
|
+
// parity for the `site/ui` corpus is Phase 3, so these participate only
|
|
74
|
+
// in Hono SSR conformance + the fixture-hydrate runtime layer for now.
|
|
75
|
+
// (`label` and `kbd` are static helpers; `toggle` / `switch` /
|
|
76
|
+
// `checkbox` carry uncontrolled state; `input` / `textarea` are
|
|
77
|
+
// pass-through native controls.)
|
|
78
|
+
'toggle',
|
|
79
|
+
'switch',
|
|
80
|
+
'checkbox',
|
|
81
|
+
'textarea',
|
|
82
|
+
'kbd',
|
|
82
83
|
],
|
|
83
84
|
// Per-fixture build-time contracts for shapes the Mojo adapter
|
|
84
85
|
// intentionally refuses to lower. Owned by this adapter test file
|
|
@@ -256,6 +257,42 @@ export function Hello() {
|
|
|
256
257
|
expect(adapter.extension).toBe('.html.ep')
|
|
257
258
|
})
|
|
258
259
|
|
|
260
|
+
test('module pure-string const referenced in className inlines the literal (#1467 Phase 2b)', () => {
|
|
261
|
+
// A module-scope `const X = 'literal'` used inside a className template
|
|
262
|
+
// literal must inline its value, NOT emit `$X` against a stash variable
|
|
263
|
+
// that is never bound (the value would render empty). Hono inlines it at
|
|
264
|
+
// runtime; this restores byte-parity.
|
|
265
|
+
const result = compileAndGenerate(`
|
|
266
|
+
"use client"
|
|
267
|
+
const labelClasses = 'flex items-center group-data-[disabled=true]:opacity-50'
|
|
268
|
+
export function Label({ className = '' }: { className?: string }) {
|
|
269
|
+
return <label className={\`\${labelClasses} \${className}\`} />
|
|
270
|
+
}
|
|
271
|
+
`)
|
|
272
|
+
// Inlined as a Perl single-quoted literal, escaped tokens intact.
|
|
273
|
+
expect(result.template).toContain(
|
|
274
|
+
"'flex items-center group-data-[disabled=true]:opacity-50'",
|
|
275
|
+
)
|
|
276
|
+
// No stash-variable reference to the const.
|
|
277
|
+
expect(result.template).not.toContain('$labelClasses')
|
|
278
|
+
})
|
|
279
|
+
|
|
280
|
+
test('module pure-string const is NOT inlined when shadowed by a loop variable (#1749 review)', () => {
|
|
281
|
+
// A loop param whose name matches a module const must keep its loop
|
|
282
|
+
// binding (`$label`) inside the body — the const literal must not leak
|
|
283
|
+
// in. `renderLoop` guards module-const inlining for the loop body.
|
|
284
|
+
const result = compileAndGenerate(`
|
|
285
|
+
"use client"
|
|
286
|
+
const label = 'MODULE_CONST'
|
|
287
|
+
export function List({ items }: { items: string[] }) {
|
|
288
|
+
return <ul>{items.map(label => <li>{label}</li>)}</ul>
|
|
289
|
+
}
|
|
290
|
+
`)
|
|
291
|
+
// Inside the loop the param wins — emit the loop variable, not the const.
|
|
292
|
+
expect(result.template).toContain('$label')
|
|
293
|
+
expect(result.template).not.toContain('MODULE_CONST')
|
|
294
|
+
})
|
|
295
|
+
|
|
259
296
|
test('generates conditional with Perl if/else', () => {
|
|
260
297
|
const result = compileAndGenerate(`
|
|
261
298
|
"use client"
|