@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
|
@@ -4,8 +4,10 @@
|
|
|
4
4
|
* Generates Mojolicious EP template files (.html.ep) from BarefootJS IR.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
import ts from 'typescript'
|
|
7
8
|
import type {
|
|
8
9
|
ComponentIR,
|
|
10
|
+
IRMetadata,
|
|
9
11
|
IRNode,
|
|
10
12
|
IRElement,
|
|
11
13
|
IRText,
|
|
@@ -117,6 +119,32 @@ export interface MojoAdapterOptions {
|
|
|
117
119
|
barefootJsPath?: string
|
|
118
120
|
}
|
|
119
121
|
|
|
122
|
+
/**
|
|
123
|
+
* Parse a const initializer's source text. Returns the unescaped string
|
|
124
|
+
* value when the whole initializer is a single string literal (or a
|
|
125
|
+
* no-substitution template literal), else `null`. Uses the TS parser so
|
|
126
|
+
* escapes/quotes resolve exactly as JS would, matching the value the Hono
|
|
127
|
+
* reference inlines at runtime.
|
|
128
|
+
*/
|
|
129
|
+
function parsePureStringLiteral(source: string): string | null {
|
|
130
|
+
const sf = ts.createSourceFile(
|
|
131
|
+
'__const.ts',
|
|
132
|
+
`const __x = (${source});`,
|
|
133
|
+
ts.ScriptTarget.Latest,
|
|
134
|
+
/*setParentNodes*/ false,
|
|
135
|
+
)
|
|
136
|
+
const stmt = sf.statements[0]
|
|
137
|
+
if (!stmt || !ts.isVariableStatement(stmt)) return null
|
|
138
|
+
const decl = stmt.declarationList.declarations[0]
|
|
139
|
+
let init = decl?.initializer
|
|
140
|
+
while (init && ts.isParenthesizedExpression(init)) init = init.expression
|
|
141
|
+
if (!init) return null
|
|
142
|
+
if (ts.isStringLiteral(init) || ts.isNoSubstitutionTemplateLiteral(init)) {
|
|
143
|
+
return init.text
|
|
144
|
+
}
|
|
145
|
+
return null
|
|
146
|
+
}
|
|
147
|
+
|
|
120
148
|
export class MojoAdapter extends BaseAdapter implements IRNodeEmitter<MojoRenderCtx> {
|
|
121
149
|
name = 'mojolicious'
|
|
122
150
|
extension = '.html.ep'
|
|
@@ -159,6 +187,26 @@ export class MojoAdapter extends BaseAdapter implements IRNodeEmitter<MojoRender
|
|
|
159
187
|
* true — selecting the string operator from the operand's type avoids that.
|
|
160
188
|
*/
|
|
161
189
|
private stringValueNames: Set<string> = new Set()
|
|
190
|
+
/**
|
|
191
|
+
* Module-scope pure string-literal constants (`const X = 'literal'` at
|
|
192
|
+
* file top-level), keyed by name → resolved literal value. Populated at
|
|
193
|
+
* `generate()` entry from `ir.metadata.localConstants`. When an identifier
|
|
194
|
+
* in an expression resolves to one of these, the adapter inlines the
|
|
195
|
+
* literal instead of emitting `$X` against a stash variable that is never
|
|
196
|
+
* bound (a module const isn't a prop, signal, or local — the value would
|
|
197
|
+
* render empty). Hono inlines it for free; this restores parity. Only
|
|
198
|
+
* module-scope pure string literals qualify (see `collectModuleStringConsts`).
|
|
199
|
+
*/
|
|
200
|
+
private moduleStringConsts: Map<string, string> = new Map()
|
|
201
|
+
/**
|
|
202
|
+
* Names currently bound by an enclosing loop body — the `my $<param>` and
|
|
203
|
+
* `my $<index>` bindings `renderLoop` introduces — ref-counted so nested
|
|
204
|
+
* loops compose. `resolveModuleStringConst` consults this so a loop
|
|
205
|
+
* variable whose name happens to match a module string const is NOT
|
|
206
|
+
* inlined as the const literal (mirrors the Go adapter's loop-param /
|
|
207
|
+
* loop-var shadowing guards). (#1749 review)
|
|
208
|
+
*/
|
|
209
|
+
private loopBoundNames: Map<string, number> = new Map()
|
|
162
210
|
|
|
163
211
|
constructor(options: MojoAdapterOptions = {}) {
|
|
164
212
|
super()
|
|
@@ -186,6 +234,8 @@ export class MojoAdapter extends BaseAdapter implements IRNodeEmitter<MojoRender
|
|
|
186
234
|
for (const p of ir.metadata.propsParams) {
|
|
187
235
|
if (isStringTypeInfo(p.type)) this.stringValueNames.add(p.name)
|
|
188
236
|
}
|
|
237
|
+
this.moduleStringConsts = this.collectModuleStringConsts(ir.metadata.localConstants)
|
|
238
|
+
this.loopBoundNames.clear()
|
|
189
239
|
this.errors = []
|
|
190
240
|
this.childrenCaptureCounter = 0
|
|
191
241
|
|
|
@@ -238,6 +288,41 @@ export class MojoAdapter extends BaseAdapter implements IRNodeEmitter<MojoRender
|
|
|
238
288
|
}
|
|
239
289
|
}
|
|
240
290
|
|
|
291
|
+
/**
|
|
292
|
+
* Build the module pure-string-const map from the IR's localConstants.
|
|
293
|
+
* A const qualifies only when it is module-scope (`isModule`) and its
|
|
294
|
+
* initializer parses to a single string literal (`ts.StringLiteral` or
|
|
295
|
+
* `ts.NoSubstitutionTemplateLiteral`). Template literals with `${}`,
|
|
296
|
+
* numeric/object initializers, `Record<T,string>` maps, memos, and
|
|
297
|
+
* signals are all excluded — only a pure compile-time string can be
|
|
298
|
+
* inlined byte-for-byte.
|
|
299
|
+
*/
|
|
300
|
+
private collectModuleStringConsts(constants: IRMetadata['localConstants']): Map<string, string> {
|
|
301
|
+
const map = new Map<string, string>()
|
|
302
|
+
for (const c of constants ?? []) {
|
|
303
|
+
if (!c.isModule) continue
|
|
304
|
+
if (c.value === undefined) continue
|
|
305
|
+
const literal = parsePureStringLiteral(c.value)
|
|
306
|
+
if (literal !== null) map.set(c.name, literal)
|
|
307
|
+
}
|
|
308
|
+
return map
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Resolve an identifier to its inlined Perl single-quoted string literal
|
|
313
|
+
* when it names a module pure-string const, else `null` (the caller then
|
|
314
|
+
* falls back to its normal `$name` stash lowering). Returns the Perl
|
|
315
|
+
* literal form `'<escaped>'` ready to drop into an expression.
|
|
316
|
+
*/
|
|
317
|
+
resolveModuleStringConst(name: string): string | null {
|
|
318
|
+
// A loop body introduces `my $<param>` / `my $<index>` bindings that
|
|
319
|
+
// shadow a module const of the same name — never inline inside one.
|
|
320
|
+
if (this.loopBoundNames.has(name)) return null
|
|
321
|
+
const value = this.moduleStringConsts.get(name)
|
|
322
|
+
if (value === undefined) return null
|
|
323
|
+
return `'${value.replace(/[\\']/g, m => `\\${m}`)}'`
|
|
324
|
+
}
|
|
325
|
+
|
|
241
326
|
// ===========================================================================
|
|
242
327
|
// Script Registration
|
|
243
328
|
// ===========================================================================
|
|
@@ -587,6 +672,16 @@ export class MojoAdapter extends BaseAdapter implements IRNodeEmitter<MojoRender
|
|
|
587
672
|
const indexVar = loop.iterationShape === 'keys'
|
|
588
673
|
? `$${param}`
|
|
589
674
|
: loop.index ? `$${loop.index}` : '$_i'
|
|
675
|
+
// Names this loop binds in body scope. Guard module-const inlining for
|
|
676
|
+
// the whole body (children + key + filter) so a same-named loop variable
|
|
677
|
+
// isn't replaced by the const literal (#1749 review). Ref-counted for
|
|
678
|
+
// nested loops; released after the body lines are assembled below.
|
|
679
|
+
const loopBound = loop.iterationShape === 'keys'
|
|
680
|
+
? [param]
|
|
681
|
+
: [param, loop.index ?? '_i']
|
|
682
|
+
for (const n of loopBound) {
|
|
683
|
+
this.loopBoundNames.set(n, (this.loopBoundNames.get(n) ?? 0) + 1)
|
|
684
|
+
}
|
|
590
685
|
const prevInLoop = this.inLoop
|
|
591
686
|
this.inLoop = true
|
|
592
687
|
const renderedChildren = this.renderChildren(loop.children)
|
|
@@ -644,6 +739,13 @@ export class MojoAdapter extends BaseAdapter implements IRNodeEmitter<MojoRender
|
|
|
644
739
|
lines.push(children)
|
|
645
740
|
}
|
|
646
741
|
|
|
742
|
+
// Body fully rendered — release the loop-bound names.
|
|
743
|
+
for (const n of loopBound) {
|
|
744
|
+
const c = (this.loopBoundNames.get(n) ?? 1) - 1
|
|
745
|
+
if (c <= 0) this.loopBoundNames.delete(n)
|
|
746
|
+
else this.loopBoundNames.set(n, c)
|
|
747
|
+
}
|
|
748
|
+
|
|
647
749
|
lines.push(`% }`)
|
|
648
750
|
lines.push(`<%== bf->comment("/loop:${loop.markerId}") %>`)
|
|
649
751
|
|
|
@@ -1307,7 +1409,7 @@ function renderArrayMethod(
|
|
|
1307
1409
|
// runtime's `bf->includes($recv, $elem)` inspects `ref($recv)`
|
|
1308
1410
|
// and dispatches: ARRAY ref scans the list with `eq`, scalar
|
|
1309
1411
|
// falls back to `index(..., ...) != -1`. Helper lives in
|
|
1310
|
-
// packages/adapter-
|
|
1412
|
+
// packages/adapter-perl/lib/BarefootJS.pm.
|
|
1311
1413
|
//
|
|
1312
1414
|
// The `bf->` (no `$`) form matches every other helper emit —
|
|
1313
1415
|
// in real Mojolicious `bf` is a controller helper; the
|
|
@@ -1817,6 +1919,11 @@ class MojoTopLevelEmitter implements ParsedExprEmitter {
|
|
|
1817
1919
|
constructor(private readonly adapter: MojoAdapter) {}
|
|
1818
1920
|
|
|
1819
1921
|
identifier(name: string): string {
|
|
1922
|
+
// Module pure-string const (e.g. `const baseClasses = '...'` used in a
|
|
1923
|
+
// className template literal): inline the literal value rather than emit
|
|
1924
|
+
// `$baseClasses` against a stash variable that is never bound.
|
|
1925
|
+
const inlined = this.adapter.resolveModuleStringConst(name)
|
|
1926
|
+
if (inlined !== null) return inlined
|
|
1820
1927
|
return `$${name}`
|
|
1821
1928
|
}
|
|
1822
1929
|
|
package/src/test-render.ts
CHANGED
|
@@ -11,7 +11,12 @@ import { mkdir, rm } from 'node:fs/promises'
|
|
|
11
11
|
import { resolve } from 'node:path'
|
|
12
12
|
|
|
13
13
|
const RENDER_TEMP_DIR = resolve(import.meta.dir, '../.render-temp')
|
|
14
|
+
// Mojo-specific lib (BarefootJS::Backend::Mojo + the plugin) lives in this
|
|
15
|
+
// package; the engine-agnostic core (BarefootJS.pm) moved to @barefootjs/perl.
|
|
16
|
+
// Both dirs must be on the render script's @INC so `use BarefootJS` and
|
|
17
|
+
// `use BarefootJS::Backend::Mojo` resolve.
|
|
14
18
|
const LIB_DIR = resolve(import.meta.dir, '../lib')
|
|
19
|
+
const PERL_CORE_LIB_DIR = resolve(import.meta.dir, '../../adapter-perl/lib')
|
|
15
20
|
|
|
16
21
|
export class PerlNotAvailableError extends Error {
|
|
17
22
|
constructor(message: string) {
|
|
@@ -216,7 +221,7 @@ use strict;
|
|
|
216
221
|
use warnings;
|
|
217
222
|
use utf8;
|
|
218
223
|
|
|
219
|
-
use lib '${LIB_DIR}';
|
|
224
|
+
use lib '${LIB_DIR}', '${PERL_CORE_LIB_DIR}';
|
|
220
225
|
use Mojolicious;
|
|
221
226
|
use Mojo::Template;
|
|
222
227
|
# Boolean values in spread bags arrive as Mojo::JSON::true /
|
|
@@ -428,6 +433,25 @@ function buildPerlProps(
|
|
|
428
433
|
entries.push(`${param.name} => undef`)
|
|
429
434
|
}
|
|
430
435
|
|
|
436
|
+
// A `{...props}` rest spread means props that aren't declared named
|
|
437
|
+
// params flow through the rest bag (`bf->spread_attrs($<restPropsName>)`),
|
|
438
|
+
// not their own top-level template var. Route them into the bag hashref so
|
|
439
|
+
// a fixture passing e.g. `placeholder` to `Input` (whose declared params
|
|
440
|
+
// are `className` / `type`) renders `placeholder="..."` via the spread
|
|
441
|
+
// rather than silently dropping it into an unused `my $placeholder`. (#1467
|
|
442
|
+
// Phase 2b — mirrors the Go harness fix in the sibling `test-render.ts`.)
|
|
443
|
+
const restPropsName = ir.metadata.restPropsName
|
|
444
|
+
const declaredParams = new Set(ir.metadata.propsParams.map(p => p.name))
|
|
445
|
+
const restBagEntries: Array<[string, unknown]> = []
|
|
446
|
+
if (restPropsName && props) {
|
|
447
|
+
for (const [key, value] of Object.entries(props)) {
|
|
448
|
+
if (key.startsWith('__')) continue
|
|
449
|
+
if (key === restPropsName || declaredParams.has(key)) continue
|
|
450
|
+
restBagEntries.push([key, value])
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
const routedKeys = new Set(restBagEntries.map(([k]) => k))
|
|
454
|
+
|
|
431
455
|
// (#1407 follow-up) Default the rest-binding identifier to an
|
|
432
456
|
// empty hashref so `bf->spread_attrs($extras)` in the generated
|
|
433
457
|
// Mojo template doesn't trip Perl's strict-mode "Global symbol
|
|
@@ -436,13 +460,16 @@ function buildPerlProps(
|
|
|
436
460
|
// the COMPILE path on Go, where the bag plumbing matters; the
|
|
437
461
|
// runtime is a no-op when the caller leaves the bag unset, which
|
|
438
462
|
// mirrors the empty-spread case on every adapter).
|
|
439
|
-
|
|
440
|
-
|
|
463
|
+
// When the fixture supplied undeclared props, seed the bag with those
|
|
464
|
+
// routed entries instead of an empty hashref.
|
|
465
|
+
if (restPropsName && !(props && restPropsName in props)) {
|
|
466
|
+
entries.push(`${restPropsName} => ${toPerlLiteral(Object.fromEntries(restBagEntries))}`)
|
|
441
467
|
}
|
|
442
468
|
|
|
443
469
|
// Add user props
|
|
444
470
|
if (props) {
|
|
445
471
|
for (const [key, value] of Object.entries(props)) {
|
|
472
|
+
if (routedKeys.has(key)) continue
|
|
446
473
|
if (typeof value === 'string') {
|
|
447
474
|
entries.push(`${key} => '${value.replace(/'/g, "\\'")}'`)
|
|
448
475
|
} else if (typeof value === 'number') {
|