@barefootjs/mojolicious 0.17.0 → 0.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapter/expr/array-method.d.ts +13 -1
- package/dist/adapter/expr/array-method.d.ts.map +1 -1
- package/dist/adapter/expr/emitters.d.ts +9 -4
- package/dist/adapter/expr/emitters.d.ts.map +1 -1
- package/dist/adapter/index.js +100 -68
- package/dist/adapter/lib/ir-scope.d.ts +0 -7
- package/dist/adapter/lib/ir-scope.d.ts.map +1 -1
- package/dist/adapter/memo/seed.d.ts +10 -19
- package/dist/adapter/memo/seed.d.ts.map +1 -1
- package/dist/adapter/mojo-adapter.d.ts.map +1 -1
- package/dist/build.js +100 -68
- package/dist/conformance-pins.d.ts +10 -0
- package/dist/conformance-pins.d.ts.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +116 -68
- package/dist/test-render.d.ts.map +1 -1
- package/lib/BarefootJS/Backend/Mojo.pm +14 -5
- package/lib/Mojolicious/Plugin/BarefootJS/DevReload.pm +1 -1
- package/lib/Mojolicious/Plugin/BarefootJS.pm +55 -33
- package/package.json +3 -3
- package/src/__tests__/mojo-adapter.test.ts +180 -135
- package/src/__tests__/multi-component-registry.test.ts +215 -0
- package/src/__tests__/stock-route.test.ts +203 -0
- package/src/adapter/expr/array-method.ts +43 -1
- package/src/adapter/expr/emitters.ts +59 -15
- package/src/adapter/lib/ir-scope.ts +0 -13
- package/src/adapter/memo/seed.ts +20 -73
- package/src/adapter/mojo-adapter.ts +154 -34
- package/src/conformance-pins.ts +138 -0
- package/src/index.ts +1 -0
- package/src/test-render.ts +8 -0
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
// Stock-route smoke test for the Mojo scaffold contract (#2126).
|
|
2
|
+
//
|
|
3
|
+
// The scaffold's happy path is: `npm run dev` (which starts `bf build
|
|
4
|
+
// --watch` and morbo *concurrently*), then open `/`. That page renders a
|
|
5
|
+
// component whose EP template reads props/signals as bare scalars
|
|
6
|
+
// (`% my $count = ($initial // 0);`), and Mojo templates compile under
|
|
7
|
+
// `use strict` — so every one of those scalars must be declared by the
|
|
8
|
+
// time the template runs. Two production bugs hid in that path:
|
|
9
|
+
//
|
|
10
|
+
// 1. The route passes no props, so the stash seeding has to come from
|
|
11
|
+
// the manifest's `ssrDefaults` via the plugin's `before_render`
|
|
12
|
+
// hook — including props only referenced through `props.X` reads.
|
|
13
|
+
// 2. The plugin used to load the manifest once at register time, so
|
|
14
|
+
// booting before the first build wrote `manifest.json` disabled
|
|
15
|
+
// auto-init for the server's lifetime: every render of `/` died
|
|
16
|
+
// with `Global symbol "$initial" requires explicit package name`
|
|
17
|
+
// (HTTP 500) until a manual restart.
|
|
18
|
+
//
|
|
19
|
+
// This test boots a real Mojolicious app whose `app.pl` mirrors the
|
|
20
|
+
// scaffold's (plugin + dist/templates renderer path + a `/` route that
|
|
21
|
+
// passes NO props) against a template + manifest produced by the real
|
|
22
|
+
// compiler, and asserts the page renders — in both boot orders. It is
|
|
23
|
+
// the in-repo equivalent of "scaffold → build → curl / expects 200".
|
|
24
|
+
//
|
|
25
|
+
// Runs only when `perl` with Mojolicious is installed (same skip policy
|
|
26
|
+
// as the conformance render tests). The DevReload plugin is omitted:
|
|
27
|
+
// it's dev-only plumbing with its own coverage in t/dev_reload.t.
|
|
28
|
+
|
|
29
|
+
import { describe, test, expect, beforeAll } from 'bun:test'
|
|
30
|
+
import { mkdtempSync, mkdirSync, writeFileSync } from 'node:fs'
|
|
31
|
+
import { tmpdir } from 'node:os'
|
|
32
|
+
import path from 'node:path'
|
|
33
|
+
import { compileJSX } from '@barefootjs/jsx'
|
|
34
|
+
import { MojoAdapter } from '../adapter/mojo-adapter'
|
|
35
|
+
|
|
36
|
+
const MOJO_LIB_DIR = path.resolve(import.meta.dir, '../../lib')
|
|
37
|
+
const PERL_CORE_LIB_DIR = path.resolve(import.meta.dir, '../../../adapter-perl/lib')
|
|
38
|
+
|
|
39
|
+
function perlWithMojoAvailable(): boolean {
|
|
40
|
+
try {
|
|
41
|
+
const proc = Bun.spawnSync(['perl', '-MMojolicious', '-MTest::Mojo', '-e1'], {
|
|
42
|
+
env: process.env,
|
|
43
|
+
})
|
|
44
|
+
return proc.exitCode === 0
|
|
45
|
+
} catch {
|
|
46
|
+
return false
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const PERL_AVAILABLE = perlWithMojoAvailable()
|
|
51
|
+
|
|
52
|
+
// The starter-Counter shape: a prop read only through `props.X ?? …`
|
|
53
|
+
// (never passed by the stock route) feeding a signal, plus a memo.
|
|
54
|
+
// Exactly the pattern that dies under strict when seeding breaks.
|
|
55
|
+
// `props.label` is read directly in the body (no signal/memo in
|
|
56
|
+
// between) — that flattens to a bare `<%= $label %>` too, so it pins
|
|
57
|
+
// that ssrDefaults seeds every declared prop, not just the ones a
|
|
58
|
+
// signal initializer references.
|
|
59
|
+
const COUNTER_TSX = `'use client'
|
|
60
|
+
|
|
61
|
+
import { createSignal, createMemo } from '@barefootjs/client'
|
|
62
|
+
|
|
63
|
+
interface CounterProps {
|
|
64
|
+
initial?: number
|
|
65
|
+
label?: string
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function Counter(props: CounterProps) {
|
|
69
|
+
const [count, setCount] = createSignal(props.initial ?? 0)
|
|
70
|
+
const doubled = createMemo(() => count() * 2)
|
|
71
|
+
|
|
72
|
+
return (
|
|
73
|
+
<div className="counter">
|
|
74
|
+
<p className="counter-label">{props.label}</p>
|
|
75
|
+
<p className="counter-value">count: {count()}</p>
|
|
76
|
+
<p className="counter-doubled">doubled: {doubled()}</p>
|
|
77
|
+
<button onClick={() => setCount(n => n + 1)}>+1</button>
|
|
78
|
+
</div>
|
|
79
|
+
)
|
|
80
|
+
}
|
|
81
|
+
`
|
|
82
|
+
|
|
83
|
+
// Mirrors the scaffold's app.pl essentials. The `/` route deliberately
|
|
84
|
+
// passes NO props — the manifest seeding alone must carry it. The
|
|
85
|
+
// second route pins that a caller-supplied prop still wins.
|
|
86
|
+
const APP_PL = `#!/usr/bin/env perl
|
|
87
|
+
use Mojolicious::Lite -signatures;
|
|
88
|
+
|
|
89
|
+
plugin 'BarefootJS';
|
|
90
|
+
|
|
91
|
+
app->renderer->paths->[0] = app->home->child('dist/templates');
|
|
92
|
+
|
|
93
|
+
get '/' => sub ($c) {
|
|
94
|
+
$c->render(template => 'Counter', layout => 'default');
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
get '/with-prop' => sub ($c) {
|
|
98
|
+
$c->render(template => 'Counter', layout => 'default', initial => 5);
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
app->start;
|
|
102
|
+
|
|
103
|
+
__DATA__
|
|
104
|
+
|
|
105
|
+
@@ layouts/default.html.ep
|
|
106
|
+
<!DOCTYPE html>
|
|
107
|
+
<html><body>
|
|
108
|
+
<main><%== content %></main>
|
|
109
|
+
%== $c->bf->scripts
|
|
110
|
+
</body></html>
|
|
111
|
+
`
|
|
112
|
+
|
|
113
|
+
// Test::Mojo harness. SMOKE_SCENARIO picks the boot order:
|
|
114
|
+
// manifest-at-boot — the manifest exists before the app loads
|
|
115
|
+
// (server restarted after a completed build).
|
|
116
|
+
// manifest-after-boot — the app loads first, the manifest appears
|
|
117
|
+
// afterwards (the concurrent `bf build --watch`
|
|
118
|
+
// + morbo dev race on a fresh scaffold).
|
|
119
|
+
const SMOKE_PL = `use Mojo::Base -strict;
|
|
120
|
+
use Test::More;
|
|
121
|
+
use Test::Mojo;
|
|
122
|
+
use Mojo::File qw(curfile path);
|
|
123
|
+
|
|
124
|
+
my $home = curfile->dirname;
|
|
125
|
+
my $scenario = $ENV{SMOKE_SCENARIO} // 'manifest-at-boot';
|
|
126
|
+
my $staged = $home->child('manifest.staged.json');
|
|
127
|
+
my $manifest = $home->child('dist/templates/manifest.json');
|
|
128
|
+
|
|
129
|
+
unlink "$manifest";
|
|
130
|
+
$staged->copy_to("$manifest") if $scenario eq 'manifest-at-boot';
|
|
131
|
+
|
|
132
|
+
my $t = Test::Mojo->new($home->child('app.pl'));
|
|
133
|
+
|
|
134
|
+
$staged->copy_to("$manifest") if $scenario eq 'manifest-after-boot';
|
|
135
|
+
|
|
136
|
+
# Stock route: no props passed — manifest ssrDefaults must seed every
|
|
137
|
+
# bare template scalar ($initial included) or strict mode 500s.
|
|
138
|
+
$t->get_ok('/')->status_is(200)
|
|
139
|
+
->text_like('p.counter-value', qr/count:\\s*0/)
|
|
140
|
+
->text_like('p.counter-doubled', qr/doubled:\\s*0/)
|
|
141
|
+
->content_like(qr/Counter\\.client\\.js/, 'auto-init registered the client bundle');
|
|
142
|
+
|
|
143
|
+
# Caller-supplied prop wins over the manifest default.
|
|
144
|
+
$t->get_ok('/with-prop')->status_is(200)
|
|
145
|
+
->text_like('p.counter-value', qr/count:\\s*5/)
|
|
146
|
+
->text_like('p.counter-doubled', qr/doubled:\\s*10/);
|
|
147
|
+
|
|
148
|
+
done_testing;
|
|
149
|
+
`
|
|
150
|
+
|
|
151
|
+
describe.skipIf(!PERL_AVAILABLE)('Mojo scaffold stock route (#2126)', () => {
|
|
152
|
+
let appDir: string
|
|
153
|
+
|
|
154
|
+
beforeAll(() => {
|
|
155
|
+
appDir = mkdtempSync(path.join(tmpdir(), 'bf-mojo-stock-route-'))
|
|
156
|
+
mkdirSync(path.join(appDir, 'dist/templates'), { recursive: true })
|
|
157
|
+
|
|
158
|
+
const result = compileJSX(COUNTER_TSX, 'Counter.tsx', { adapter: new MojoAdapter() })
|
|
159
|
+
const template = result.files.find(f => f.type === 'markedTemplate')
|
|
160
|
+
const ssrDefaults = result.files.find(f => f.type === 'ssrDefaults')
|
|
161
|
+
if (!template || !ssrDefaults) throw new Error('compileJSX produced no template/ssrDefaults')
|
|
162
|
+
|
|
163
|
+
writeFileSync(path.join(appDir, 'dist/templates/Counter.html.ep'), template.content)
|
|
164
|
+
// Same entry shape `bf build` writes to dist/templates/manifest.json.
|
|
165
|
+
writeFileSync(
|
|
166
|
+
path.join(appDir, 'manifest.staged.json'),
|
|
167
|
+
JSON.stringify({
|
|
168
|
+
Counter: {
|
|
169
|
+
markedTemplate: 'templates/Counter.html.ep',
|
|
170
|
+
ssrDefaults: JSON.parse(ssrDefaults.content),
|
|
171
|
+
},
|
|
172
|
+
}),
|
|
173
|
+
)
|
|
174
|
+
writeFileSync(path.join(appDir, 'app.pl'), APP_PL)
|
|
175
|
+
writeFileSync(path.join(appDir, 'smoke.pl'), SMOKE_PL)
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
async function runScenario(scenario: string): Promise<void> {
|
|
179
|
+
const perl5lib = [MOJO_LIB_DIR, PERL_CORE_LIB_DIR, process.env.PERL5LIB]
|
|
180
|
+
.filter(Boolean)
|
|
181
|
+
.join(':')
|
|
182
|
+
const proc = Bun.spawn(['perl', 'smoke.pl'], {
|
|
183
|
+
cwd: appDir,
|
|
184
|
+
env: { ...process.env, PERL5LIB: perl5lib, SMOKE_SCENARIO: scenario },
|
|
185
|
+
stdout: 'pipe',
|
|
186
|
+
stderr: 'pipe',
|
|
187
|
+
})
|
|
188
|
+
const [stdout, stderr, exitCode] = await Promise.all([
|
|
189
|
+
new Response(proc.stdout).text(),
|
|
190
|
+
new Response(proc.stderr).text(),
|
|
191
|
+
proc.exited,
|
|
192
|
+
])
|
|
193
|
+
expect(exitCode, `TAP output:\n${stdout}\n${stderr}`).toBe(0)
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
test('renders / with the manifest present at boot', async () => {
|
|
197
|
+
await runScenario('manifest-at-boot')
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
test('renders / when the manifest appears after boot (concurrent dev-startup race)', async () => {
|
|
201
|
+
await runScenario('manifest-after-boot')
|
|
202
|
+
})
|
|
203
|
+
})
|
|
@@ -372,6 +372,27 @@ export function renderFlatMapEval(
|
|
|
372
372
|
return `bf->flat_map_eval(${recv}, '${escapePerlSingleQuote(json)}', '${param}', ${env})`
|
|
373
373
|
}
|
|
374
374
|
|
|
375
|
+
/**
|
|
376
|
+
* Emit a value-producing `.map(cb)` via the runtime evaluator (#2073): the
|
|
377
|
+
* projection `body` serializes to JSON and `bf->map_eval` projects each
|
|
378
|
+
* element, one result per element (no flatten — the JS `.map` contract).
|
|
379
|
+
* Composes through the array-method chain (`.map(cb).join(' ')`). Returns
|
|
380
|
+
* null when the projection is outside the evaluator surface, and the caller
|
|
381
|
+
* records BF101. The JSX-returning `.map` is an IRLoop upstream and never
|
|
382
|
+
* reaches this emit.
|
|
383
|
+
*/
|
|
384
|
+
export function renderMapEval(
|
|
385
|
+
recv: string,
|
|
386
|
+
body: ParsedExpr,
|
|
387
|
+
param: string,
|
|
388
|
+
emit: (e: ParsedExpr) => string,
|
|
389
|
+
): string | null {
|
|
390
|
+
const json = serializeParsedExpr(body)
|
|
391
|
+
if (json === null) return null
|
|
392
|
+
const env = emitEvalEnvArg(body, [param], emit)
|
|
393
|
+
return `bf->map_eval(${recv}, '${escapePerlSingleQuote(json)}', '${param}', ${env})`
|
|
394
|
+
}
|
|
395
|
+
|
|
375
396
|
/**
|
|
376
397
|
* Shared Mojo emit for `.sort(cmp)` / `.toSorted(cmp)` (#1448 Tier B).
|
|
377
398
|
* Used by both the filter-context emitter and the top-level emitter,
|
|
@@ -402,7 +423,28 @@ export function renderSortMethod(recv: string, c: SortComparator): string {
|
|
|
402
423
|
// `.flat(depth?)` → `bf->flat($recv, $depth)`. The `Infinity` form lowers
|
|
403
424
|
// to the `-1` sentinel (flatten fully); a finite depth flattens that many
|
|
404
425
|
// levels (`0` = shallow copy). See `sub flat` in BarefootJS.pm. (#1448)
|
|
405
|
-
export function renderFlatMethod(
|
|
426
|
+
export function renderFlatMethod(
|
|
427
|
+
recv: string,
|
|
428
|
+
depth: FlatDepth | { expr: ParsedExpr },
|
|
429
|
+
emit: (e: ParsedExpr) => string,
|
|
430
|
+
): string {
|
|
431
|
+
if (typeof depth === 'object') {
|
|
432
|
+
// Dynamic depth (#2094): routed to a SEPARATE runtime helper
|
|
433
|
+
// (`bf->flat_dynamic`), not `bf->flat`. The literal-depth path below
|
|
434
|
+
// already uses `-1` as a compile-time SENTINEL baked into the template
|
|
435
|
+
// source meaning "the source literally said `Infinity`". A genuinely
|
|
436
|
+
// dynamic depth that happens to evaluate to `-1` at render time means
|
|
437
|
+
// the OPPOSITE in real JS (`[1,[2]].flat(-1)` never recurses — same as
|
|
438
|
+
// `.flat(0)`). Since both paths would otherwise hand the same
|
|
439
|
+
// literal-looking argument to one shared helper, that helper couldn't
|
|
440
|
+
// tell which case it's in — so `flat_dynamic` performs its own
|
|
441
|
+
// `ToIntegerOrInfinity`-style coercion (truncate toward zero; negative
|
|
442
|
+
// -> 0; NaN/non-numeric -> 0; +Infinity or a huge finite value ->
|
|
443
|
+
// flatten fully) and then delegates to `flat`. See `sub flat_dynamic`
|
|
444
|
+
// in BarefootJS.pm and the Go reference (`FlatDynamicDepth`/
|
|
445
|
+
// `coerceFlatDepth` in bf.go).
|
|
446
|
+
return `bf->flat_dynamic(${recv}, ${emit(depth.expr)})`
|
|
447
|
+
}
|
|
406
448
|
const d = depth === 'infinity' ? -1 : depth
|
|
407
449
|
return `bf->flat(${recv}, ${d})`
|
|
408
450
|
}
|
|
@@ -38,6 +38,7 @@ import {
|
|
|
38
38
|
renderPredicateEval,
|
|
39
39
|
renderFlatMethod,
|
|
40
40
|
renderFlatMapEval,
|
|
41
|
+
renderMapEval,
|
|
41
42
|
} from './array-method.ts'
|
|
42
43
|
|
|
43
44
|
/**
|
|
@@ -81,6 +82,11 @@ export class MojoFilterEmitter implements ParsedExprEmitter {
|
|
|
81
82
|
// against it lowers to `eq`/`ne` (#1672). Defaults to "never" for callers
|
|
82
83
|
// that don't thread it through.
|
|
83
84
|
private readonly isStringName: (n: string) => boolean = () => false,
|
|
85
|
+
// Records a BF101 for nested callback shapes this emitter can only
|
|
86
|
+
// degrade — `find*` and the non-predicate methods (#2038). Optional so
|
|
87
|
+
// emitter construction stays possible without an adapter; a missing hook
|
|
88
|
+
// keeps the old silent-degrade emit.
|
|
89
|
+
private readonly onUnsupported?: (message: string, reason?: string) => void,
|
|
84
90
|
) {}
|
|
85
91
|
|
|
86
92
|
identifier(name: string): string {
|
|
@@ -172,20 +178,34 @@ export class MojoFilterEmitter implements ParsedExprEmitter {
|
|
|
172
178
|
): string {
|
|
173
179
|
// Filter context only meaningfully handles the predicate methods
|
|
174
180
|
// (filter / every / some land here through nested `.filter(...)` chains).
|
|
175
|
-
// Sort / reduce / flatMap
|
|
176
|
-
//
|
|
177
|
-
|
|
181
|
+
// Sort / reduce / flatMap have no scalar Perl form here — the old
|
|
182
|
+
// `default` arm degraded them to the truthy sentinel, silently rewriting
|
|
183
|
+
// the predicate, so surface BF101 instead (#2038).
|
|
184
|
+
if (!PREDICATE_METHODS.has(method)) {
|
|
185
|
+
this.onUnsupported?.(
|
|
186
|
+
`Filter predicate contains a nested '.${method}(...)' callback, which has no Perl scalar form`,
|
|
187
|
+
`Rewrite the predicate without a nested callback method, or add /* @client */ for client-only evaluation (no SSR).`,
|
|
188
|
+
)
|
|
189
|
+
return '1'
|
|
190
|
+
}
|
|
178
191
|
// The predicate body is also a filter context, but with this
|
|
179
192
|
// callback's own `param` (potentially shadowing the outer one),
|
|
180
193
|
// so we spin up a nested emitter with the inner param.
|
|
181
194
|
const param = arrow.params[0]
|
|
182
195
|
const predicate = arrow.body
|
|
183
196
|
const arrayExpr = emit(object)
|
|
184
|
-
const predBody = emitParsedExpr(predicate, new MojoFilterEmitter(param, this.localVarMap, this.isStringName))
|
|
197
|
+
const predBody = emitParsedExpr(predicate, new MojoFilterEmitter(param, this.localVarMap, this.isStringName, this.onUnsupported))
|
|
185
198
|
const grepBody = predBody.replace(new RegExp(`\\$${param}\\b`, 'g'), '$_')
|
|
186
199
|
if (method === 'filter') return `[grep { ${grepBody} } @{${arrayExpr}}]`
|
|
187
200
|
if (method === 'every') return `!(grep { !(${grepBody}) } @{${arrayExpr}})`
|
|
188
201
|
if (method === 'some') return `!!(grep { ${grepBody} } @{${arrayExpr}})`
|
|
202
|
+
// `find` / `findIndex` / `findLast` / `findLastIndex` return an element
|
|
203
|
+
// (or index), not a boolean — there is no inline grep form. Degrading to
|
|
204
|
+
// the receiver silently changes predicate semantics, so be loud (#2038).
|
|
205
|
+
this.onUnsupported?.(
|
|
206
|
+
`Filter predicate contains a nested '.${method}(...)' callback, which has no Perl scalar form`,
|
|
207
|
+
`Rewrite the predicate without a nested callback method, or add /* @client */ for client-only evaluation (no SSR).`,
|
|
208
|
+
)
|
|
189
209
|
return arrayExpr
|
|
190
210
|
}
|
|
191
211
|
|
|
@@ -212,8 +232,12 @@ export class MojoFilterEmitter implements ParsedExprEmitter {
|
|
|
212
232
|
return renderArrayMethod(method, object, args, emit)
|
|
213
233
|
}
|
|
214
234
|
|
|
215
|
-
flatMethod(
|
|
216
|
-
|
|
235
|
+
flatMethod(
|
|
236
|
+
object: ParsedExpr,
|
|
237
|
+
depth: FlatDepth | { expr: ParsedExpr },
|
|
238
|
+
emit: (e: ParsedExpr) => string,
|
|
239
|
+
): string {
|
|
240
|
+
return renderFlatMethod(emit(object), depth, emit)
|
|
217
241
|
}
|
|
218
242
|
|
|
219
243
|
conditional(_test: ParsedExpr, _consequent: ParsedExpr, _alternate: ParsedExpr): string {
|
|
@@ -448,6 +472,18 @@ export class MojoTopLevelEmitter implements ParsedExprEmitter {
|
|
|
448
472
|
return "''"
|
|
449
473
|
}
|
|
450
474
|
|
|
475
|
+
// Value-producing map (#2073): eval-only; BF101 when the projection is
|
|
476
|
+
// outside the surface. (The JSX-returning `.map` is an IRLoop upstream.)
|
|
477
|
+
if (method === 'map') {
|
|
478
|
+
const evalForm = renderMapEval(recv, body, params[0], emit)
|
|
479
|
+
if (evalForm !== null) return evalForm
|
|
480
|
+
this.ctx._recordExprBF101(
|
|
481
|
+
`.map(...) projection is not lowerable to a template map`,
|
|
482
|
+
`Pre-compute the projection in the route handler, or mark the position @client-only.`,
|
|
483
|
+
)
|
|
484
|
+
return "''"
|
|
485
|
+
}
|
|
486
|
+
|
|
451
487
|
// Predicate methods: filter / find / every / some / findIndex / findLast /
|
|
452
488
|
// findLastIndex. Eval-first (#2018 P2), then the inline `grep` / `bf->find`
|
|
453
489
|
// fallback for a predicate the evaluator can't model.
|
|
@@ -527,8 +563,12 @@ export class MojoTopLevelEmitter implements ParsedExprEmitter {
|
|
|
527
563
|
return renderArrayMethod(method, object, args, emit)
|
|
528
564
|
}
|
|
529
565
|
|
|
530
|
-
flatMethod(
|
|
531
|
-
|
|
566
|
+
flatMethod(
|
|
567
|
+
object: ParsedExpr,
|
|
568
|
+
depth: FlatDepth | { expr: ParsedExpr },
|
|
569
|
+
emit: (e: ParsedExpr) => string,
|
|
570
|
+
): string {
|
|
571
|
+
return renderFlatMethod(emit(object), depth, emit)
|
|
532
572
|
}
|
|
533
573
|
|
|
534
574
|
conditional(
|
|
@@ -596,12 +636,16 @@ export class MojoTopLevelEmitter implements ParsedExprEmitter {
|
|
|
596
636
|
return "''"
|
|
597
637
|
}
|
|
598
638
|
|
|
599
|
-
objectLiteral(
|
|
600
|
-
//
|
|
601
|
-
//
|
|
602
|
-
//
|
|
603
|
-
//
|
|
604
|
-
// `
|
|
605
|
-
|
|
639
|
+
objectLiteral(properties: ObjectLiteralProperty[], _raw: string, _emit: (e: ParsedExpr) => string): string {
|
|
640
|
+
// The shared `isSupported` gate only ever lets this dispatcher see an
|
|
641
|
+
// object literal as the EMPTY (`?? {}`) fallback operand of `??`
|
|
642
|
+
// (expression-parser.ts, `logical` case) — any other object literal is
|
|
643
|
+
// refused before reaching here. Emit Perl's real empty hashref literal,
|
|
644
|
+
// matching the `'{}'` convention `objectLiteralToPerlHashref` already
|
|
645
|
+
// uses for the zero-property case in the spread path. A populated
|
|
646
|
+
// literal is structurally unreachable given the gate, but still
|
|
647
|
+
// degrades safely to the pre-existing empty-string sentinel rather
|
|
648
|
+
// than silently dropping keys.
|
|
649
|
+
return properties.length === 0 ? '{}' : "''"
|
|
606
650
|
}
|
|
607
651
|
}
|
|
@@ -55,16 +55,3 @@ export function collectRootScopeNodes(node: IRNode): Set<IRNode> {
|
|
|
55
55
|
visit(node)
|
|
56
56
|
return out
|
|
57
57
|
}
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* True when every `$var` the lowered (Perl / Kolon) expression references is
|
|
61
|
-
* in the available set — i.e. the template already has that var in scope.
|
|
62
|
-
* Guards in-template memo seeding from referencing an out-of-scope binding
|
|
63
|
-
* (which would trip Perl strict mode). (#1297)
|
|
64
|
-
*/
|
|
65
|
-
export function referencedVarsAreAvailable(expr: string, available: ReadonlySet<string>): boolean {
|
|
66
|
-
for (const m of expr.matchAll(/\$([A-Za-z_]\w*)/g)) {
|
|
67
|
-
if (!available.has(m[1])) return false
|
|
68
|
-
}
|
|
69
|
-
return true
|
|
70
|
-
}
|
package/src/adapter/memo/seed.ts
CHANGED
|
@@ -13,13 +13,10 @@ import {
|
|
|
13
13
|
type ComponentIR,
|
|
14
14
|
type ContextConsumer,
|
|
15
15
|
collectContextConsumers,
|
|
16
|
-
|
|
17
|
-
isSupported,
|
|
18
|
-
parseExpression,
|
|
16
|
+
computeSsrSeedPlan,
|
|
19
17
|
} from '@barefootjs/jsx'
|
|
20
18
|
|
|
21
19
|
import type { MojoMemoContext } from '../emit-context.ts'
|
|
22
|
-
import { referencedVarsAreAvailable } from '../lib/ir-scope.ts'
|
|
23
20
|
|
|
24
21
|
/** Perl literal for a context-consumer's `createContext` default. */
|
|
25
22
|
export function contextDefaultPerl(c: ContextConsumer): string {
|
|
@@ -49,78 +46,28 @@ export function generateContextConsumerSeed(ir: ComponentIR): string {
|
|
|
49
46
|
}
|
|
50
47
|
|
|
51
48
|
/**
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
49
|
+
* Emit `% my $<name> = <perl>;` seed lines for every `derived` step of the
|
|
50
|
+
* backend-neutral SSR seed plan — the scope/availability/ordering analysis
|
|
51
|
+
* lives in `computeSsrSeedPlan` (packages/jsx/src/ssr-seed-plan.ts); this
|
|
52
|
+
* only lowers each step's expression to Perl and applies the two
|
|
53
|
+
* backend-specific emit guards: skip an empty lowering, and skip a lowering
|
|
54
|
+
* that references no `$var` at all (a constant init/body — e.g. a `derived`
|
|
55
|
+
* step with empty `frees` — keeps the existing static ssr-defaults seed
|
|
56
|
+
* instead). `env-reader` and `opaque` steps emit nothing (the runtime
|
|
57
|
+
* supplies the reader, or the adapter's ssr-defaults path already covers it).
|
|
58
|
+
* (#1297, #2075)
|
|
61
59
|
*/
|
|
62
60
|
export function generateDerivedMemoSeed(ctx: MojoMemoContext, ir: ComponentIR): string {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
// later one can reference an earlier one.
|
|
68
|
-
const available = new Set<string>(ir.metadata.propsParams.map(p => p.name))
|
|
61
|
+
// Package G attached this to metadata at compile time; the `??` fallback
|
|
62
|
+
// only covers hand-built metadata in older tests that predate the attached
|
|
63
|
+
// plan — same shared function, so there's no divergence from the compiler.
|
|
64
|
+
const plan = ir.metadata.ssrSeedPlan ?? computeSsrSeedPlan(ir.metadata)
|
|
69
65
|
const lines: string[] = []
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
// the passed prop — but ONLY when the init lowers cleanly AND references an
|
|
76
|
-
// in-scope var (i.e. it's genuinely derived). Object/array/constant inits
|
|
77
|
-
// (`createSignal({…})`, `createSignal([…])`, `createSignal('b')`) keep the
|
|
78
|
-
// existing ssr-defaults seeding, so the spread / loop fixtures are
|
|
79
|
-
// untouched.
|
|
80
|
-
for (const signal of signals) {
|
|
81
|
-
const perl = tryLowerToPerl(ctx, signal.initialValue, available)
|
|
82
|
-
if (perl !== null) lines.push(`% my $${signal.getter} = ${perl};`)
|
|
83
|
-
available.add(signal.getter)
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
for (const memo of memos) {
|
|
87
|
-
// Seed every memo whose body lowers cleanly — not just the ones whose
|
|
88
|
-
// static SSR default is null. A statically-foldable prop-derived memo
|
|
89
|
-
// (`createMemo(() => props.disabled ?? false)` → default `false`)
|
|
90
|
-
// still depends on the per-call prop: the static stash seed bakes in
|
|
91
|
-
// the absent-prop fold, so a caller passing `disabled => 1` would
|
|
92
|
-
// render the default branch (#1897, select's disabled item). The
|
|
93
|
-
// in-template recomputation reads the prop lexical the stash already
|
|
94
|
-
// seeded, so it's correct per call; block-bodied arrows /
|
|
95
|
-
// out-of-scope references fall back to the static ssr-defaults seed.
|
|
96
|
-
const body = extractArrowBodyExpression(memo.computation)
|
|
97
|
-
if (body !== null) {
|
|
98
|
-
const perl = tryLowerToPerl(ctx, body, available)
|
|
99
|
-
if (perl !== null) lines.push(`% my $${memo.name} = ${perl};`)
|
|
100
|
-
}
|
|
101
|
-
available.add(memo.name)
|
|
66
|
+
for (const step of plan.steps) {
|
|
67
|
+
if (step.kind !== 'derived') continue
|
|
68
|
+
const perl = ctx.convertExpressionToPerl(step.expr, step.parsed)
|
|
69
|
+
if (perl === '' || !/\$[A-Za-z_]\w*/.test(perl)) continue
|
|
70
|
+
lines.push(`% my $${step.name} = ${perl};`)
|
|
102
71
|
}
|
|
103
72
|
return lines.length > 0 ? lines.join('\n') + '\n' : ''
|
|
104
73
|
}
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* Lower a signal init / memo body to Perl for an in-template SSR seed, or
|
|
108
|
-
* `null` when it shouldn't be seeded this way. Returns null — without
|
|
109
|
-
* recording a BF101 — when the expression isn't a supported shape
|
|
110
|
-
* (`isSupported` pre-check, so object/array literals don't fail the build),
|
|
111
|
-
* when the lowering references no in-scope var (a constant — keep the
|
|
112
|
-
* existing ssr-defaults seeding), or when it references an out-of-scope
|
|
113
|
-
* binding. (#1297)
|
|
114
|
-
*/
|
|
115
|
-
export function tryLowerToPerl(
|
|
116
|
-
ctx: MojoMemoContext,
|
|
117
|
-
expr: string,
|
|
118
|
-
available: ReadonlySet<string>,
|
|
119
|
-
): string | null {
|
|
120
|
-
const trimmed = expr.trim()
|
|
121
|
-
if (!trimmed) return null
|
|
122
|
-
if (!isSupported(parseExpression(trimmed)).supported) return null
|
|
123
|
-
const perl = ctx.convertExpressionToPerl(trimmed)
|
|
124
|
-
if (perl === '' || !/\$[A-Za-z_]\w*/.test(perl)) return null
|
|
125
|
-
return referencedVarsAreAvailable(perl, available) ? perl : null
|
|
126
|
-
}
|