@barefootjs/erb 0.31.8 → 0.31.10
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/vite.js +63 -2
- package/lib/barefoot_js.rb +13 -1
- package/package.json +5 -5
- package/src/__tests__/erb-adapter.test.ts +9 -5
- package/src/test-render.ts +62 -4
package/dist/vite.js
CHANGED
|
@@ -3025,6 +3025,34 @@ var SVG_ROOT_TAGS = new Set([
|
|
|
3025
3025
|
"animateTransform",
|
|
3026
3026
|
"animateMotion"
|
|
3027
3027
|
]);
|
|
3028
|
+
var MATHML_ROOT_TAGS = new Set([
|
|
3029
|
+
"math",
|
|
3030
|
+
"mrow",
|
|
3031
|
+
"mfrac",
|
|
3032
|
+
"msup",
|
|
3033
|
+
"msub",
|
|
3034
|
+
"msubsup",
|
|
3035
|
+
"mn",
|
|
3036
|
+
"mi",
|
|
3037
|
+
"mo",
|
|
3038
|
+
"mtext",
|
|
3039
|
+
"munder",
|
|
3040
|
+
"mover",
|
|
3041
|
+
"munderover",
|
|
3042
|
+
"mtable",
|
|
3043
|
+
"mtr",
|
|
3044
|
+
"mtd",
|
|
3045
|
+
"msqrt",
|
|
3046
|
+
"mroot",
|
|
3047
|
+
"mstyle",
|
|
3048
|
+
"merror",
|
|
3049
|
+
"mpadded",
|
|
3050
|
+
"mphantom",
|
|
3051
|
+
"menclose",
|
|
3052
|
+
"semantics",
|
|
3053
|
+
"annotation",
|
|
3054
|
+
"annotation-xml"
|
|
3055
|
+
]);
|
|
3028
3056
|
|
|
3029
3057
|
// ../jsx/src/ir-to-client-js/collect-elements.ts
|
|
3030
3058
|
var EMPTY_RENDER_EXPRS = new Set(["null", "undefined", "false", "''", '""', "``"]);
|
|
@@ -3888,6 +3916,38 @@ function classify(name, origin, expr, parsed, available) {
|
|
|
3888
3916
|
}
|
|
3889
3917
|
return { kind: "derived", name, origin, expr, parsed, frees: [...frees] };
|
|
3890
3918
|
}
|
|
3919
|
+
function localConstExprsByName(metadata) {
|
|
3920
|
+
const out = new Map;
|
|
3921
|
+
for (const c of metadata.localConstants ?? []) {
|
|
3922
|
+
if (c.isModule || c.declarationKind !== "const" || c.value === undefined)
|
|
3923
|
+
continue;
|
|
3924
|
+
out.set(c.name, c.parsed ?? parseExpression(c.value.trim()));
|
|
3925
|
+
}
|
|
3926
|
+
return out;
|
|
3927
|
+
}
|
|
3928
|
+
function resolveThroughLocalConsts(parsed, localConsts) {
|
|
3929
|
+
let current = parsed;
|
|
3930
|
+
const maxIter = localConsts.size + 1;
|
|
3931
|
+
for (let i = 0;i < maxIter; i++) {
|
|
3932
|
+
const frees = freeIdentifiers(current);
|
|
3933
|
+
if (frees === null)
|
|
3934
|
+
break;
|
|
3935
|
+
let changed = false;
|
|
3936
|
+
for (const name of frees) {
|
|
3937
|
+
const value = localConsts.get(name);
|
|
3938
|
+
if (!value)
|
|
3939
|
+
continue;
|
|
3940
|
+
const inlined = inlineBinding(current, name, value);
|
|
3941
|
+
if (inlined === null)
|
|
3942
|
+
continue;
|
|
3943
|
+
current = inlined;
|
|
3944
|
+
changed = true;
|
|
3945
|
+
}
|
|
3946
|
+
if (!changed)
|
|
3947
|
+
break;
|
|
3948
|
+
}
|
|
3949
|
+
return current;
|
|
3950
|
+
}
|
|
3891
3951
|
function computeSsrSeedPlan(metadata) {
|
|
3892
3952
|
const baseScope = metadata.propsParams.map((p) => p.name);
|
|
3893
3953
|
if (metadata.propsObjectName)
|
|
@@ -3896,6 +3956,7 @@ function computeSsrSeedPlan(metadata) {
|
|
|
3896
3956
|
baseScope.push(name);
|
|
3897
3957
|
}
|
|
3898
3958
|
const available = new Set(baseScope);
|
|
3959
|
+
const localConsts = localConstExprsByName(metadata);
|
|
3899
3960
|
const steps = [];
|
|
3900
3961
|
for (const signal of metadata.signals) {
|
|
3901
3962
|
if (signal.envReader) {
|
|
@@ -3907,13 +3968,13 @@ function computeSsrSeedPlan(metadata) {
|
|
|
3907
3968
|
}
|
|
3908
3969
|
}
|
|
3909
3970
|
const expr = signal.initialValue.trim();
|
|
3910
|
-
steps.push(expr === "" ? { kind: "opaque", name: signal.getter, origin: "signal" } : classify(signal.getter, "signal", expr, parseExpression(expr), available));
|
|
3971
|
+
steps.push(expr === "" ? { kind: "opaque", name: signal.getter, origin: "signal" } : classify(signal.getter, "signal", expr, resolveThroughLocalConsts(parseExpression(expr), localConsts), available));
|
|
3911
3972
|
available.add(signal.getter);
|
|
3912
3973
|
}
|
|
3913
3974
|
for (const memo of metadata.memos) {
|
|
3914
3975
|
const body = extractArrowBodyExpression(memo.computation);
|
|
3915
3976
|
const expr = body?.trim() ?? "";
|
|
3916
|
-
steps.push(expr === "" ? { kind: "opaque", name: memo.name, origin: "memo" } : classify(memo.name, "memo", expr, memo.parsed ?? parseExpression(expr), available));
|
|
3977
|
+
steps.push(expr === "" ? { kind: "opaque", name: memo.name, origin: "memo" } : classify(memo.name, "memo", expr, resolveThroughLocalConsts(memo.parsed ?? parseExpression(expr), localConsts), available));
|
|
3917
3978
|
available.add(memo.name);
|
|
3918
3979
|
}
|
|
3919
3980
|
return { baseScope, steps };
|
package/lib/barefoot_js.rb
CHANGED
|
@@ -125,11 +125,23 @@ module BarefootJS
|
|
|
125
125
|
props = _props
|
|
126
126
|
return '' unless props && !props.empty?
|
|
127
127
|
|
|
128
|
+
# Exclude the internal `scope_id` key from the client hydration
|
|
129
|
+
# payload: it is a server-side render detail (already carried by
|
|
130
|
+
# `_scope_id` for bf-s/bf-h/bf-m emission) with zero client runtime
|
|
131
|
+
# consumers -- the only bf-p parser (packages/client/src/runtime/
|
|
132
|
+
# hydrate.ts's parseProps -> runInit) never reads it back out.
|
|
133
|
+
# Filtered here, the single marshal boundary for bf-p, so it's
|
|
134
|
+
# excluded no matter how a `scope_id` key ends up in the props Hash
|
|
135
|
+
# (found via the bf-p semantic-comparison audit against the Hono
|
|
136
|
+
# reference adapter, which never emits it).
|
|
137
|
+
client_props = props.reject { |k, _| k.to_s == 'scope_id' }
|
|
138
|
+
return '' if client_props.empty?
|
|
139
|
+
|
|
128
140
|
# The JSON must be attribute-escaped: a raw `'` inside a string value
|
|
129
141
|
# (e.g. a blog paragraph) terminates the single-quoted attribute and
|
|
130
142
|
# truncates the hydration payload. The browser entity-decodes the
|
|
131
143
|
# attribute value, so the client's JSON.parse sees the original text.
|
|
132
|
-
json = html_escape(backend.encode_json(
|
|
144
|
+
json = html_escape(backend.encode_json(client_props))
|
|
133
145
|
%( bf-p='#{json}')
|
|
134
146
|
end
|
|
135
147
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@barefootjs/erb",
|
|
3
|
-
"version": "0.31.
|
|
3
|
+
"version": "0.31.10",
|
|
4
4
|
"description": "ERB (Embedded Ruby) adapter for BarefootJS — compiles IR to .erb templates and ships the Ruby rendering backend; runs under any Rack app (Sinatra, Rails)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"directory": "packages/adapter-erb"
|
|
55
55
|
},
|
|
56
56
|
"dependencies": {
|
|
57
|
-
"@barefootjs/shared": "0.31.
|
|
57
|
+
"@barefootjs/shared": "0.31.10"
|
|
58
58
|
},
|
|
59
59
|
"peerDependencies": {
|
|
60
60
|
"@barefootjs/jsx": ">=0.2.0",
|
|
@@ -71,9 +71,9 @@
|
|
|
71
71
|
},
|
|
72
72
|
"devDependencies": {
|
|
73
73
|
"@barefootjs/adapter-tests": "0.1.0",
|
|
74
|
-
"@barefootjs/jsx": "0.31.
|
|
75
|
-
"@barefootjs/vite": "0.31.
|
|
76
|
-
"@barefootjs/client": "0.31.
|
|
74
|
+
"@barefootjs/jsx": "0.31.10",
|
|
75
|
+
"@barefootjs/vite": "0.31.10",
|
|
76
|
+
"@barefootjs/client": "0.31.10",
|
|
77
77
|
"typescript": "^5.0.0",
|
|
78
78
|
"vite": "^6.0.0"
|
|
79
79
|
}
|
|
@@ -479,11 +479,15 @@ export function TodoList(props: { initialTodos?: Todo[] }) {
|
|
|
479
479
|
// as a thrown "ruby render failed" error, so a `NoMethodError` string
|
|
480
480
|
// anywhere in a caught error would fail this test outright rather than
|
|
481
481
|
// reaching these assertions. Post-fix: only the not-done todo (id 2)
|
|
482
|
-
// survives the 'active' filter.
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
482
|
+
// survives the 'active' filter. The bf-p hydration payload legitimately
|
|
483
|
+
// carries the full unfiltered `initialTodos` (both quote styles,
|
|
484
|
+
// mirroring `normalizeHTML`), so the assertions target the rendered
|
|
485
|
+
// list only.
|
|
486
|
+
const rendered = html.replace(/\s*bf-p=(?:"[^"]*"|'[^']*')/g, '')
|
|
487
|
+
expect(rendered).not.toContain('Eat breakfast')
|
|
488
|
+
expect(rendered).toContain('Write tests')
|
|
489
|
+
expect(rendered).toContain('data-key="2"')
|
|
490
|
+
expect(rendered).not.toContain('data-key="1"')
|
|
487
491
|
})
|
|
488
492
|
})
|
|
489
493
|
|
package/src/test-render.ts
CHANGED
|
@@ -264,8 +264,13 @@ export async function renderErbComponent(options: RenderOptions): Promise<string
|
|
|
264
264
|
// value domain is JSON-shaped symbol-keyed Hashes throughout, so
|
|
265
265
|
// `JSON.parse(..., symbolize_names: true)` on the Ruby side is the
|
|
266
266
|
// whole marshalling story; no hand-built Ruby literals.
|
|
267
|
-
const { obj: rootProps, needsSearchParams } = buildRubyProps(props, ir)
|
|
267
|
+
const { obj: rootProps, needsSearchParams, userProps } = buildRubyProps(props, ir)
|
|
268
268
|
await Bun.write(resolve(tempDir, 'props.json'), JSON.stringify(rootProps))
|
|
269
|
+
// Separate from `props.json`: the caller-facing subset only (no
|
|
270
|
+
// scope_id / signal / memo bookkeeping) — what `bf._props(...)` below
|
|
271
|
+
// feeds `props_attr`'s bf-p payload, matching what a production
|
|
272
|
+
// Sinatra route handler passes, not this harness's internal vars hash.
|
|
273
|
+
await Bun.write(resolve(tempDir, 'user_props.json'), JSON.stringify(userProps))
|
|
269
274
|
|
|
270
275
|
// Honour `__instanceId` from props for the root scope id so
|
|
271
276
|
// shared-component fixtures (which pin `<ComponentName>_test`) match
|
|
@@ -310,6 +315,16 @@ bf = BarefootJS::Context.new(backend)
|
|
|
310
315
|
bf._scope_id(${rootScopeId})
|
|
311
316
|
|
|
312
317
|
props = JSON.parse(File.read(File.join(__dir__, 'props.json')), symbolize_names: true)
|
|
318
|
+
# Mirrors production's \`props_attr\` contract (see barefoot_js.rb and
|
|
319
|
+
# props_attr_test.rb): the caller is expected to seed \`_props\` before
|
|
320
|
+
# rendering the root component, so this harness must call it explicitly.
|
|
321
|
+
#
|
|
322
|
+
# \`user_props.json\` — NOT \`props\` above — is what \`_props\` gets: \`props\`
|
|
323
|
+
# is this harness's internal vars hash (scope_id, signal/memo seed
|
|
324
|
+
# values), which a real Sinatra route handler never has and never passes.
|
|
325
|
+
# Production's bf-p carries neither scope_id nor signal state, only the
|
|
326
|
+
# caller-facing props.
|
|
327
|
+
bf._props(JSON.parse(File.read(File.join(__dir__, 'user_props.json')), symbolize_names: true))
|
|
313
328
|
${needsSearchParams ? "# (#1922) Request-scoped searchParams() env signal: bind the reserved\n# `search_params` vars key to an empty-query reader. Only when the\n# component imports `searchParams`.\nprops[:search_params] = bf.search_params('')\n" : ''}
|
|
314
329
|
${childRenderers}
|
|
315
330
|
html = backend.render_named(${rubyStringLiteral(toSnakeCase(componentName))}, bf, props)
|
|
@@ -491,12 +506,20 @@ function toSnakeCase(name: string): string {
|
|
|
491
506
|
|
|
492
507
|
/**
|
|
493
508
|
* Build the root props object (later JSON-serialised) + whether the
|
|
494
|
-
* component imports `searchParams
|
|
509
|
+
* component imports `searchParams`, PLUS `userProps` — the exact raw
|
|
510
|
+
* fixture props (no defaults, no null-fill, no signal/memo seeding),
|
|
511
|
+
* mirroring production's route-handler call `bf._props(props)` verbatim.
|
|
512
|
+
* `obj` stays the full internal vars hash the compiled template renders
|
|
513
|
+
* against (local-var-keyed, defaulted, signal/memo-seeded); `userProps`
|
|
514
|
+
* is for `bf._props(...)` (bf-p hydration payload) only — these must NOT
|
|
515
|
+
* be the same object, and `userProps` must NOT be derived from `obj` or
|
|
516
|
+
* from the defaulted stash: production's own bf-p carries only what the
|
|
517
|
+
* caller actually passed in, unmodified.
|
|
495
518
|
*/
|
|
496
519
|
function buildRubyProps(
|
|
497
520
|
props: Record<string, unknown> | undefined,
|
|
498
521
|
ir: ComponentIR,
|
|
499
|
-
): { obj: Record<string, unknown>; needsSearchParams: boolean } {
|
|
522
|
+
): { obj: Record<string, unknown>; needsSearchParams: boolean; userProps: Record<string, unknown> } {
|
|
500
523
|
const obj: Record<string, unknown> = {}
|
|
501
524
|
|
|
502
525
|
const explicitScope = typeof props?.__instanceId === 'string' ? props.__instanceId : 'test'
|
|
@@ -509,6 +532,10 @@ function buildRubyProps(
|
|
|
509
532
|
// (`propName`, e.g. `n` for `{ n: count }`) is honoured, not just the
|
|
510
533
|
// local template var name (#2524 SSR half). `props` is keyed by the
|
|
511
534
|
// caller-facing name, exactly what `propName` resolves against.
|
|
535
|
+
//
|
|
536
|
+
// This defaulted/derived stash feeds ONLY `obj` (the template-rendering
|
|
537
|
+
// vars hash) below — never `userProps` (the bf-p payload), which is
|
|
538
|
+
// built separately, straight off the raw `props` argument.
|
|
512
539
|
const rootSsrDefaults = extractSsrDefaults(ir.metadata) ?? {}
|
|
513
540
|
const derivedProps = deriveStashFromDefaults(rootSsrDefaults, props ?? {})
|
|
514
541
|
for (const param of ir.metadata.propsParams) {
|
|
@@ -558,11 +585,36 @@ function buildRubyProps(
|
|
|
558
585
|
}
|
|
559
586
|
}
|
|
560
587
|
|
|
588
|
+
// `bf._props(...)` payload (bf-p hydration): mirrors production's
|
|
589
|
+
// route-handler call `bf._props(props)` verbatim — the caller's raw
|
|
590
|
+
// props dict, unmodified. No default-filling, no signal/memo seeding,
|
|
591
|
+
// no rest-bag nesting: a real Sinatra/Rack handler never rearranges
|
|
592
|
+
// the dict it received before handing it to `_props`. Excludes
|
|
593
|
+
// internal harness-only keys (`__instanceId` etc.), which are never a
|
|
594
|
+
// real caller-facing prop.
|
|
595
|
+
const userProps: Record<string, unknown> = {}
|
|
596
|
+
if (props) {
|
|
597
|
+
for (const [key, value] of Object.entries(props)) {
|
|
598
|
+
if (key.startsWith('__')) continue
|
|
599
|
+
userProps[key] = value
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
|
|
561
603
|
// Signal values evaluated from props (after user props).
|
|
562
604
|
for (const signal of ir.metadata.signals) {
|
|
563
605
|
// Env signals (#2057 / #1922) are bound below via `search_params('')`,
|
|
564
606
|
// not from a static initial value.
|
|
565
607
|
if (signal.envReader) continue
|
|
608
|
+
// #2669: a self-derivation collision (the signal's own initializer
|
|
609
|
+
// derives from a same-named prop, e.g. `createSignal(props.label ??
|
|
610
|
+
// 'Default')`) already got the correct RAW-prop seed above via
|
|
611
|
+
// `derivedProps` / `obj[param.name]` — `extractSsrDefaults` marks that
|
|
612
|
+
// with a `propName`-carrying entry (see its docstring's invariant).
|
|
613
|
+
// Re-seeding here with this harness's OWN recompute
|
|
614
|
+
// (`evaluateSignalInit`) would clobber the correctly-derived caller
|
|
615
|
+
// value with the harness's evaluated-from-static-props number, papering
|
|
616
|
+
// over the exact production bug this fixture exists to catch.
|
|
617
|
+
if (rootSsrDefaults[signal.getter]?.propName !== undefined) continue
|
|
566
618
|
const value = evaluateSignalInit(signal.initialValue.trim(), props)
|
|
567
619
|
if (value !== null) {
|
|
568
620
|
obj[signal.getter] = value
|
|
@@ -572,11 +624,17 @@ function buildRubyProps(
|
|
|
572
624
|
// Memo values seeded from the statically-evaluated ssrDefaults, same
|
|
573
625
|
// as the production plugin's before_render hook.
|
|
574
626
|
for (const memo of ir.metadata.memos) {
|
|
627
|
+
// #2669: same self-derivation skip as the signal loop above — a
|
|
628
|
+
// `propName`-carrying memo entry already stands as the correctly
|
|
629
|
+
// prop-derived seed; don't clobber it with the memo's OWN evaluated
|
|
630
|
+
// value (which for a non-idempotent derivation is already the WRONG,
|
|
631
|
+
// double-applied number).
|
|
632
|
+
if (rootSsrDefaults[memo.name]?.propName !== undefined) continue
|
|
575
633
|
obj[memo.name] = rootSsrDefaults[memo.name]?.value ?? 0
|
|
576
634
|
}
|
|
577
635
|
|
|
578
636
|
const needsSearchParams = importsSearchParams(ir.metadata)
|
|
579
637
|
|
|
580
|
-
return { obj, needsSearchParams }
|
|
638
|
+
return { obj, needsSearchParams, userProps }
|
|
581
639
|
}
|
|
582
640
|
|