@barefootjs/erb 0.30.6 → 0.31.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/erb-adapter.d.ts +0 -1
- package/dist/adapter/erb-adapter.d.ts.map +1 -1
- package/dist/adapter/expr/emitters.d.ts +1 -1
- package/dist/adapter/expr/emitters.d.ts.map +1 -1
- package/dist/adapter/index.js +11 -3
- package/dist/index.js +11 -3
- package/dist/vite.d.ts +38 -0
- package/dist/vite.d.ts.map +1 -0
- package/dist/vite.js +6634 -0
- package/lib/barefoot_js.rb +28 -3
- package/package.json +22 -9
- package/src/__tests__/erb-adapter.test.ts +144 -0
- package/src/__tests__/vite.test.ts +125 -0
- package/src/adapter/erb-adapter.ts +27 -6
- package/src/adapter/expr/emitters.ts +60 -32
- package/src/vite.ts +216 -0
- package/dist/build.d.ts +0 -28
- package/dist/build.d.ts.map +0 -1
- package/dist/build.js +0 -189276
- package/src/build.ts +0 -37
package/lib/barefoot_js.rb
CHANGED
|
@@ -68,6 +68,8 @@ module BarefootJS
|
|
|
68
68
|
bf_accessor :backend
|
|
69
69
|
bf_accessor :_scripts, default: -> { [] }
|
|
70
70
|
bf_accessor :_script_seen, default: -> { {} }
|
|
71
|
+
bf_accessor :_preloads, default: -> { [] }
|
|
72
|
+
bf_accessor :_preload_seen, default: -> { {} }
|
|
71
73
|
bf_accessor :_scope_id
|
|
72
74
|
bf_accessor :_is_child, default: -> { false }
|
|
73
75
|
bf_accessor :_bf_parent
|
|
@@ -217,8 +219,28 @@ module BarefootJS
|
|
|
217
219
|
_scripts.push(path)
|
|
218
220
|
end
|
|
219
221
|
|
|
222
|
+
# Register a `<link rel="modulepreload">` hint (mirrors `register_script`
|
|
223
|
+
# exactly: same dedup-by-path set, same insertion-order array, same
|
|
224
|
+
# lifetime/reset semantics, same no-output-string return so the compiled
|
|
225
|
+
# ERB template's `<% bf.register_preload(...) %>` produces no bytes
|
|
226
|
+
# where it sits). The `<link>` itself is only ever emitted by `scripts`
|
|
227
|
+
# below, never here -- a preload registration must never inject a node
|
|
228
|
+
# into a component's own template output (see the previous attempt's
|
|
229
|
+
# regression this guards against).
|
|
230
|
+
def register_preload(path)
|
|
231
|
+
return if _preload_seen.key?(path)
|
|
232
|
+
|
|
233
|
+
_preload_seen[path] = true
|
|
234
|
+
_preloads.push(path)
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
# Emits preload hints BEFORE script tags -- a hint that arrives after
|
|
238
|
+
# the script it describes is useless. Same escaping (none; paths are
|
|
239
|
+
# caller-trusted resolved URLs) as the script tags below.
|
|
220
240
|
def scripts
|
|
221
|
-
|
|
241
|
+
preload_tags = _preloads.map { |path| %(<link rel="modulepreload" crossorigin href="#{path}">) }
|
|
242
|
+
script_tags = _scripts.map { |path| %(<script type="module" src="#{path}"></script>) }
|
|
243
|
+
(preload_tags + script_tags).join("\n")
|
|
222
244
|
end
|
|
223
245
|
|
|
224
246
|
# -----------------------------------------------------------------
|
|
@@ -255,8 +277,9 @@ module BarefootJS
|
|
|
255
277
|
# Bulk registration from build manifest
|
|
256
278
|
# -----------------------------------------------------------------
|
|
257
279
|
#
|
|
258
|
-
# `
|
|
259
|
-
#
|
|
280
|
+
# `vite build` (via `@barefootjs/erb/vite`'s `barefoot()` plugin) emits
|
|
281
|
+
# dist/templates/manifest.json describing every component the page
|
|
282
|
+
# might invoke. This walks that manifest and
|
|
260
283
|
# registers one child renderer per UI registry entry (`ui/<name>/index`
|
|
261
284
|
# -> slot key `<name>`), seeding each child's template vars from the
|
|
262
285
|
# manifest's statically-derived `ssrDefaults` (prop destructure
|
|
@@ -300,6 +323,8 @@ module BarefootJS
|
|
|
300
323
|
child_bf._child_renderers(parent._child_renderers)
|
|
301
324
|
child_bf._scripts(parent._scripts)
|
|
302
325
|
child_bf._script_seen(parent._script_seen)
|
|
326
|
+
child_bf._preloads(parent._preloads)
|
|
327
|
+
child_bf._preload_seen(parent._preload_seen)
|
|
303
328
|
|
|
304
329
|
extra =
|
|
305
330
|
if sig_init
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@barefootjs/erb",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.31.0",
|
|
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",
|
|
@@ -17,9 +17,9 @@
|
|
|
17
17
|
"./test-render": {
|
|
18
18
|
"bun": "./src/test-render.ts"
|
|
19
19
|
},
|
|
20
|
-
"./
|
|
21
|
-
"types": "./dist/
|
|
22
|
-
"import": "./dist/
|
|
20
|
+
"./vite": {
|
|
21
|
+
"types": "./dist/vite.d.ts",
|
|
22
|
+
"import": "./dist/vite.js"
|
|
23
23
|
}
|
|
24
24
|
},
|
|
25
25
|
"files": [
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
],
|
|
31
31
|
"scripts": {
|
|
32
32
|
"build": "bun run build:js && bun run build:types",
|
|
33
|
-
"build:js": "bun build ./src/index.ts ./src/adapter/index.ts
|
|
33
|
+
"build:js": "bun build ./src/index.ts ./src/adapter/index.ts --root ./src --outdir ./dist --format esm --external @barefootjs/jsx --external @barefootjs/shared --external @barefootjs/vite --external vite && bun build ./src/vite.ts --outfile ./dist/vite.js --format esm --target node --external @barefootjs/vite --external vite --external typescript",
|
|
34
34
|
"build:types": "tsgo --emitDeclarationOnly --outDir ./dist",
|
|
35
35
|
"test": "bun test",
|
|
36
36
|
"clean": "rm -rf dist",
|
|
@@ -54,14 +54,27 @@
|
|
|
54
54
|
"directory": "packages/adapter-erb"
|
|
55
55
|
},
|
|
56
56
|
"dependencies": {
|
|
57
|
-
"@barefootjs/shared": "0.
|
|
57
|
+
"@barefootjs/shared": "0.31.0"
|
|
58
58
|
},
|
|
59
59
|
"peerDependencies": {
|
|
60
|
-
"@barefootjs/jsx": ">=0.2.0"
|
|
60
|
+
"@barefootjs/jsx": ">=0.2.0",
|
|
61
|
+
"@barefootjs/vite": ">=0.2.0",
|
|
62
|
+
"vite": "^6.0.0"
|
|
63
|
+
},
|
|
64
|
+
"peerDependenciesMeta": {
|
|
65
|
+
"@barefootjs/vite": {
|
|
66
|
+
"optional": true
|
|
67
|
+
},
|
|
68
|
+
"vite": {
|
|
69
|
+
"optional": true
|
|
70
|
+
}
|
|
61
71
|
},
|
|
62
72
|
"devDependencies": {
|
|
63
73
|
"@barefootjs/adapter-tests": "0.1.0",
|
|
64
|
-
"@barefootjs/jsx": "0.
|
|
65
|
-
"
|
|
74
|
+
"@barefootjs/jsx": "0.31.0",
|
|
75
|
+
"@barefootjs/vite": "0.31.0",
|
|
76
|
+
"@barefootjs/client": "0.31.0",
|
|
77
|
+
"typescript": "^5.0.0",
|
|
78
|
+
"vite": "^6.0.0"
|
|
66
79
|
}
|
|
67
80
|
}
|
|
@@ -507,3 +507,147 @@ export function Parent() {
|
|
|
507
507
|
expect(template).not.toContain('bf_prop_data')
|
|
508
508
|
})
|
|
509
509
|
})
|
|
510
|
+
|
|
511
|
+
describe('ErbAdapter - scriptAssets (Vite late-binding, PR1)', () => {
|
|
512
|
+
const CLIENT_COMPONENT = `
|
|
513
|
+
'use client'
|
|
514
|
+
import { createSignal } from '@barefootjs/client'
|
|
515
|
+
export function Counter() {
|
|
516
|
+
const [count, setCount] = createSignal(0)
|
|
517
|
+
return <button onClick={() => setCount(count() + 1)}>{count()}</button>
|
|
518
|
+
}
|
|
519
|
+
`
|
|
520
|
+
|
|
521
|
+
test('emits one register_script per URL, in order, when scriptAssets is set', () => {
|
|
522
|
+
const ir = compileToIR(CLIENT_COMPONENT)
|
|
523
|
+
const { template } = new ErbAdapter().generate(ir, {
|
|
524
|
+
scriptAssets: ['/assets/runtime-abc123.js', '/assets/counter-def456.js'],
|
|
525
|
+
})
|
|
526
|
+
const runtimeIdx = template.indexOf("bf.register_script('/assets/runtime-abc123.js')")
|
|
527
|
+
const compIdx = template.indexOf("bf.register_script('/assets/counter-def456.js')")
|
|
528
|
+
expect(runtimeIdx).toBeGreaterThanOrEqual(0)
|
|
529
|
+
expect(compIdx).toBeGreaterThanOrEqual(0)
|
|
530
|
+
expect(runtimeIdx).toBeLessThan(compIdx)
|
|
531
|
+
// Legacy computed paths must not leak in.
|
|
532
|
+
expect(template).not.toContain('/static/components/barefoot.js')
|
|
533
|
+
expect(template).not.toContain('Counter.client.js')
|
|
534
|
+
})
|
|
535
|
+
|
|
536
|
+
test('emits a single registration for a single-element scriptAssets array', () => {
|
|
537
|
+
const ir = compileToIR(CLIENT_COMPONENT)
|
|
538
|
+
const { template } = new ErbAdapter().generate(ir, {
|
|
539
|
+
scriptAssets: ['/assets/only-one.js'],
|
|
540
|
+
})
|
|
541
|
+
expect(template).toContain("bf.register_script('/assets/only-one.js')")
|
|
542
|
+
expect(template.match(/register_script/g)?.length).toBe(1)
|
|
543
|
+
})
|
|
544
|
+
|
|
545
|
+
test('an empty scriptAssets array emits no script registrations', () => {
|
|
546
|
+
const ir = compileToIR(CLIENT_COMPONENT)
|
|
547
|
+
const { template } = new ErbAdapter().generate(ir, { scriptAssets: [] })
|
|
548
|
+
expect(template).not.toContain('register_script')
|
|
549
|
+
})
|
|
550
|
+
|
|
551
|
+
test('skipScriptRegistration still wins when scriptAssets is also set', () => {
|
|
552
|
+
const ir = compileToIR(CLIENT_COMPONENT)
|
|
553
|
+
const { template } = new ErbAdapter().generate(ir, {
|
|
554
|
+
skipScriptRegistration: true,
|
|
555
|
+
scriptAssets: ['/assets/should-not-appear.js'],
|
|
556
|
+
})
|
|
557
|
+
expect(template).not.toContain('register_script')
|
|
558
|
+
})
|
|
559
|
+
|
|
560
|
+
test('absent scriptAssets falls back to adapter-computed script paths', () => {
|
|
561
|
+
const ir = compileToIR(CLIENT_COMPONENT)
|
|
562
|
+
const computed = new ErbAdapter().generate(ir).template
|
|
563
|
+
const explicitUndefined = new ErbAdapter().generate(ir, { scriptAssets: undefined }).template
|
|
564
|
+
expect(computed).toContain("bf.register_script('/static/components/barefoot.js')")
|
|
565
|
+
expect(computed).toContain("bf.register_script('/static/components/Counter.client.js')")
|
|
566
|
+
expect(explicitUndefined).toBe(computed)
|
|
567
|
+
})
|
|
568
|
+
})
|
|
569
|
+
|
|
570
|
+
describe('ErbAdapter - preloadAssets', () => {
|
|
571
|
+
const CLIENT_COMPONENT = `
|
|
572
|
+
'use client'
|
|
573
|
+
import { createSignal } from '@barefootjs/client'
|
|
574
|
+
export function Counter() {
|
|
575
|
+
const [count, setCount] = createSignal(0)
|
|
576
|
+
return <button onClick={() => setCount(count() + 1)}>{count()}</button>
|
|
577
|
+
}
|
|
578
|
+
`
|
|
579
|
+
|
|
580
|
+
test('non-empty preloadAssets + non-empty scriptAssets: preload registrations emitted, in order, before script registrations', () => {
|
|
581
|
+
const ir = compileToIR(CLIENT_COMPONENT)
|
|
582
|
+
const { template } = new ErbAdapter().generate(ir, {
|
|
583
|
+
scriptAssets: ['/assets/runtime-abc123.js', '/assets/counter-def456.js'],
|
|
584
|
+
preloadAssets: ['/assets/index-pre1.js', '/assets/shared-pre2.js'],
|
|
585
|
+
})
|
|
586
|
+
const pre1Idx = template.indexOf("<%- bf.register_preload('/assets/index-pre1.js') -%>")
|
|
587
|
+
const pre2Idx = template.indexOf("<%- bf.register_preload('/assets/shared-pre2.js') -%>")
|
|
588
|
+
const script1Idx = template.indexOf("<%- bf.register_script('/assets/runtime-abc123.js') -%>")
|
|
589
|
+
const script2Idx = template.indexOf("<%- bf.register_script('/assets/counter-def456.js') -%>")
|
|
590
|
+
expect(pre1Idx).toBeGreaterThanOrEqual(0)
|
|
591
|
+
expect(pre2Idx).toBeGreaterThan(pre1Idx)
|
|
592
|
+
expect(script1Idx).toBeGreaterThan(pre2Idx)
|
|
593
|
+
expect(script2Idx).toBeGreaterThan(script1Idx)
|
|
594
|
+
})
|
|
595
|
+
|
|
596
|
+
test('preloadAssets: [] emits no preload registration', () => {
|
|
597
|
+
const ir = compileToIR(CLIENT_COMPONENT)
|
|
598
|
+
const { template } = new ErbAdapter().generate(ir, {
|
|
599
|
+
scriptAssets: ['/assets/runtime-abc123.js'],
|
|
600
|
+
preloadAssets: [],
|
|
601
|
+
})
|
|
602
|
+
expect(template).not.toContain('register_preload')
|
|
603
|
+
expect(template).toContain("<%- bf.register_script('/assets/runtime-abc123.js') -%>")
|
|
604
|
+
})
|
|
605
|
+
|
|
606
|
+
test('preloadAssets: undefined emits no preload registration', () => {
|
|
607
|
+
const ir = compileToIR(CLIENT_COMPONENT)
|
|
608
|
+
const { template } = new ErbAdapter().generate(ir, {
|
|
609
|
+
scriptAssets: ['/assets/runtime-abc123.js'],
|
|
610
|
+
preloadAssets: undefined,
|
|
611
|
+
})
|
|
612
|
+
expect(template).not.toContain('register_preload')
|
|
613
|
+
expect(template).toContain("<%- bf.register_script('/assets/runtime-abc123.js') -%>")
|
|
614
|
+
})
|
|
615
|
+
|
|
616
|
+
test('preloadAssets non-empty but scriptAssets: [] emits no preload registration (preloads are only meaningful alongside a real script)', () => {
|
|
617
|
+
const ir = compileToIR(CLIENT_COMPONENT)
|
|
618
|
+
const { template } = new ErbAdapter().generate(ir, {
|
|
619
|
+
scriptAssets: [],
|
|
620
|
+
preloadAssets: ['/assets/index-pre1.js'],
|
|
621
|
+
})
|
|
622
|
+
expect(template).not.toContain('register_preload')
|
|
623
|
+
expect(template).not.toContain('register_script')
|
|
624
|
+
})
|
|
625
|
+
|
|
626
|
+
test('skipScriptRegistration: true suppresses both preloads and scripts', () => {
|
|
627
|
+
const ir = compileToIR(CLIENT_COMPONENT)
|
|
628
|
+
const { template } = new ErbAdapter().generate(ir, {
|
|
629
|
+
skipScriptRegistration: true,
|
|
630
|
+
scriptAssets: ['/assets/runtime-abc123.js'],
|
|
631
|
+
preloadAssets: ['/assets/index-pre1.js'],
|
|
632
|
+
})
|
|
633
|
+
expect(template).not.toContain('register_preload')
|
|
634
|
+
expect(template).not.toContain('register_script')
|
|
635
|
+
})
|
|
636
|
+
|
|
637
|
+
// Regression guard: a previous attempt emitted a literal
|
|
638
|
+
// `<link rel="modulepreload">` tag directly into the component template,
|
|
639
|
+
// which injected a rendered DOM node before the component's root and
|
|
640
|
+
// broke hydration across all eight integrations (blade, erb,
|
|
641
|
+
// go-template, jinja, mojolicious, rust, twig, xslate). Preload hints
|
|
642
|
+
// must ONLY ever be emitted as no-output register statements (here,
|
|
643
|
+
// `<%- bf.register_preload(...) -%>`) that the adapter's runtime later
|
|
644
|
+
// renders itself — never as literal markup baked into the template.
|
|
645
|
+
test('never emits a literal <link tag into the template', () => {
|
|
646
|
+
const ir = compileToIR(CLIENT_COMPONENT)
|
|
647
|
+
const { template } = new ErbAdapter().generate(ir, {
|
|
648
|
+
scriptAssets: ['/assets/runtime-abc123.js'],
|
|
649
|
+
preloadAssets: ['/assets/index-pre1.js'],
|
|
650
|
+
})
|
|
651
|
+
expect(template).not.toContain('<link')
|
|
652
|
+
})
|
|
653
|
+
})
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Coverage of `@barefootjs/erb/vite`'s `barefoot()`:
|
|
3
|
+
*
|
|
4
|
+
* - one real `vite build()` end to end (mirrors `@barefootjs/go-template/
|
|
5
|
+
* vite`, `@barefootjs/hono/vite`, `@barefootjs/blade/vite`, and
|
|
6
|
+
* `@barefootjs/jinja/vite`'s own `vite.test.ts` rigor — a plugin that
|
|
7
|
+
* only passes mocked unit tests hasn't been shown to work) against a
|
|
8
|
+
* checked-in fixture (not a system tmpdir) so `@barefootjs/client`
|
|
9
|
+
* resolves through the monorepo's real node_modules symlinks;
|
|
10
|
+
* - the `assets` → generated `bf-assets.json` behavior, exercised the same
|
|
11
|
+
* way (a real build, since it needs the real manifest Vite writes).
|
|
12
|
+
*
|
|
13
|
+
* Unlike Go, there is no `afterEmit`-driven type-combination step to test
|
|
14
|
+
* here (see `vite.ts`'s module docstring) — `ErbAdapter.generate()` never
|
|
15
|
+
* produces a `types` section at all, so `barefoot()` always returns a
|
|
16
|
+
* single-element array unless `assets` is set.
|
|
17
|
+
*/
|
|
18
|
+
import { describe, test, expect } from 'bun:test'
|
|
19
|
+
import { build } from 'vite'
|
|
20
|
+
import { mkdtemp, rm, readFile } from 'node:fs/promises'
|
|
21
|
+
import { tmpdir } from 'node:os'
|
|
22
|
+
import { join, resolve } from 'node:path'
|
|
23
|
+
import { barefoot, barefoot as defaultBarefoot } from '../vite.ts'
|
|
24
|
+
|
|
25
|
+
const FIXTURE_ROOT = resolve(import.meta.dirname, '../../e2e-fixture')
|
|
26
|
+
|
|
27
|
+
describe('@barefootjs/erb/vite: real vite build', () => {
|
|
28
|
+
test('exports the same function as both named `barefoot` and default', () => {
|
|
29
|
+
expect(defaultBarefoot).toBe(barefoot)
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
test('returns a single-element plugin array when `assets` is omitted', () => {
|
|
33
|
+
const plugins = barefoot({ components: ['src/components'], templates: 'views' })
|
|
34
|
+
expect(plugins).toHaveLength(1)
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
test('writes a self-contained .erb template with scriptAssets baked in, same as core alone would do', async () => {
|
|
38
|
+
const outDir = await mkdtemp(join(tmpdir(), 'barefoot-erb-vite-dist-'))
|
|
39
|
+
const templatesDir = await mkdtemp(join(tmpdir(), 'barefoot-erb-vite-views-'))
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
await build({
|
|
43
|
+
configFile: false,
|
|
44
|
+
root: FIXTURE_ROOT,
|
|
45
|
+
base: '/static/build/',
|
|
46
|
+
logLevel: 'warn',
|
|
47
|
+
build: { outDir, emptyOutDir: true },
|
|
48
|
+
plugins: barefoot({
|
|
49
|
+
components: ['src/components'],
|
|
50
|
+
templates: templatesDir,
|
|
51
|
+
}),
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
const template = await readFile(join(templatesDir, 'Counter.erb'), 'utf8')
|
|
55
|
+
expect(template).toContain("bf.register_script(")
|
|
56
|
+
} finally {
|
|
57
|
+
await rm(outDir, { recursive: true, force: true })
|
|
58
|
+
await rm(templatesDir, { recursive: true, force: true })
|
|
59
|
+
}
|
|
60
|
+
}, 60_000)
|
|
61
|
+
|
|
62
|
+
test('`assets` resolves a non-component entry\'s manifest-hashed URL into a generated JSON asset map', async () => {
|
|
63
|
+
const outDir = await mkdtemp(join(tmpdir(), 'barefoot-erb-vite-dist-assets-'))
|
|
64
|
+
const templatesDir = await mkdtemp(join(tmpdir(), 'barefoot-erb-vite-views-assets-'))
|
|
65
|
+
const assetsPath = join(FIXTURE_ROOT, 'dist/bf-assets.json')
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
await build({
|
|
69
|
+
configFile: false,
|
|
70
|
+
root: FIXTURE_ROOT,
|
|
71
|
+
base: '/static/build/',
|
|
72
|
+
logLevel: 'warn',
|
|
73
|
+
build: {
|
|
74
|
+
outDir,
|
|
75
|
+
emptyOutDir: true,
|
|
76
|
+
// Registering the non-component entry is the CALLER's job (stock
|
|
77
|
+
// Vite config) — `assets` below only resolves the URL Vite
|
|
78
|
+
// already bundled it to, it doesn't request the bundling.
|
|
79
|
+
rollupOptions: { input: { bootstrap: resolve(FIXTURE_ROOT, 'client/bootstrap.ts') } },
|
|
80
|
+
},
|
|
81
|
+
plugins: barefoot({
|
|
82
|
+
components: ['src/components'],
|
|
83
|
+
templates: templatesDir,
|
|
84
|
+
assets: { Bootstrap: 'client/bootstrap.ts' },
|
|
85
|
+
}),
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
const manifest = JSON.parse(await readFile(join(outDir, '.vite/manifest.json'), 'utf8'))
|
|
89
|
+
const expectedUrl = `/static/build/${manifest['client/bootstrap.ts'].file}`
|
|
90
|
+
|
|
91
|
+
const content = JSON.parse(await readFile(assetsPath, 'utf8'))
|
|
92
|
+
expect(content).toEqual({ Bootstrap: expectedUrl })
|
|
93
|
+
} finally {
|
|
94
|
+
await rm(outDir, { recursive: true, force: true })
|
|
95
|
+
await rm(templatesDir, { recursive: true, force: true })
|
|
96
|
+
await rm(join(FIXTURE_ROOT, 'dist'), { recursive: true, force: true })
|
|
97
|
+
}
|
|
98
|
+
}, 60_000)
|
|
99
|
+
|
|
100
|
+
test('`assets` throws an actionable error when the entry was never registered as a Rollup input', async () => {
|
|
101
|
+
const outDir = await mkdtemp(join(tmpdir(), 'barefoot-erb-vite-dist-assets-missing-'))
|
|
102
|
+
const templatesDir = await mkdtemp(join(tmpdir(), 'barefoot-erb-vite-views-assets-missing-'))
|
|
103
|
+
|
|
104
|
+
try {
|
|
105
|
+
await expect(
|
|
106
|
+
build({
|
|
107
|
+
configFile: false,
|
|
108
|
+
root: FIXTURE_ROOT,
|
|
109
|
+
base: '/static/build/',
|
|
110
|
+
logLevel: 'silent',
|
|
111
|
+
build: { outDir, emptyOutDir: true },
|
|
112
|
+
plugins: barefoot({
|
|
113
|
+
components: ['src/components'],
|
|
114
|
+
templates: templatesDir,
|
|
115
|
+
assets: { Bootstrap: 'client/bootstrap.ts' },
|
|
116
|
+
}),
|
|
117
|
+
}),
|
|
118
|
+
).rejects.toThrow(/was not found in the build manifest/)
|
|
119
|
+
} finally {
|
|
120
|
+
await rm(outDir, { recursive: true, force: true })
|
|
121
|
+
await rm(templatesDir, { recursive: true, force: true })
|
|
122
|
+
await rm(join(FIXTURE_ROOT, 'dist'), { recursive: true, force: true })
|
|
123
|
+
}
|
|
124
|
+
}, 60_000)
|
|
125
|
+
})
|
|
@@ -160,10 +160,6 @@ export class ErbAdapter extends BaseAdapter implements IRNodeEmitter<ErbRenderCt
|
|
|
160
160
|
name = 'erb'
|
|
161
161
|
extension = '.erb'
|
|
162
162
|
templatesPerComponent = true
|
|
163
|
-
// Template-string target with no component layer: `bf build` emits a
|
|
164
|
-
// static `barefoot-importmap.html` to include in the page <head>, same as
|
|
165
|
-
// the Mojo/Go adapters.
|
|
166
|
-
importMapInjection = 'html-snippet' as const
|
|
167
163
|
|
|
168
164
|
/**
|
|
169
165
|
* Identifier-path callees the ERB runtime can render in template scope.
|
|
@@ -328,7 +324,7 @@ export class ErbAdapter extends BaseAdapter implements IRNodeEmitter<ErbRenderCt
|
|
|
328
324
|
// Generate script registration
|
|
329
325
|
const scriptReg = options?.skipScriptRegistration
|
|
330
326
|
? ''
|
|
331
|
-
: this.generateScriptRegistrations(ir, options?.scriptBaseName)
|
|
327
|
+
: this.generateScriptRegistrations(ir, options?.scriptBaseName, options?.scriptAssets, options?.preloadAssets)
|
|
332
328
|
|
|
333
329
|
// SSR context consumers (`const x = useContext(Ctx)`): seed each local
|
|
334
330
|
// from the active provider value (or the `createContext` default) so
|
|
@@ -480,7 +476,32 @@ export class ErbAdapter extends BaseAdapter implements IRNodeEmitter<ErbRenderCt
|
|
|
480
476
|
// Script Registration
|
|
481
477
|
// ===========================================================================
|
|
482
478
|
|
|
483
|
-
private generateScriptRegistrations(
|
|
479
|
+
private generateScriptRegistrations(
|
|
480
|
+
ir: ComponentIR,
|
|
481
|
+
scriptBaseName?: string,
|
|
482
|
+
scriptAssets?: string[],
|
|
483
|
+
preloadAssets?: string[],
|
|
484
|
+
): string {
|
|
485
|
+
// `scriptAssets`, when present (including `[]`), fully supersedes the
|
|
486
|
+
// adapter-computed `barefootJsPath` / `clientJsBasePath` pair — see
|
|
487
|
+
// `AdapterGenerateOptions.scriptAssets`. The caller (e.g. the Vite
|
|
488
|
+
// plugin) has already decided the exact ordered URL list, including
|
|
489
|
+
// whether any script is needed at all.
|
|
490
|
+
if (scriptAssets) {
|
|
491
|
+
if (scriptAssets.length === 0) return ''
|
|
492
|
+
// `preloadAssets` is only meaningful alongside a non-empty
|
|
493
|
+
// `scriptAssets` (see `AdapterGenerateOptions.preloadAssets`), and
|
|
494
|
+
// every preload registration is emitted BEFORE every script
|
|
495
|
+
// registration — a hint that arrives after the script it describes
|
|
496
|
+
// is useless. Same `<%- ... -%>` no-output ERB tag as
|
|
497
|
+
// `register_script`: the `<link rel="modulepreload">` itself is only
|
|
498
|
+
// ever rendered by `Context#scripts` (barefoot_js.rb), never here.
|
|
499
|
+
const lines = (preloadAssets ?? []).map((url) => `<%- bf.register_preload('${url}') -%>`)
|
|
500
|
+
lines.push(...scriptAssets.map((url) => `<%- bf.register_script('${url}') -%>`))
|
|
501
|
+
lines.push('')
|
|
502
|
+
return lines.join('\n')
|
|
503
|
+
}
|
|
504
|
+
|
|
484
505
|
const hasInteractivity = hasClientInteractivity(ir)
|
|
485
506
|
if (!hasInteractivity) return ''
|
|
486
507
|
|
|
@@ -88,38 +88,60 @@ function unusedIsStringName(_n: string): boolean {
|
|
|
88
88
|
}
|
|
89
89
|
|
|
90
90
|
export class ErbFilterEmitter implements ParsedExprEmitter {
|
|
91
|
+
// Plain field declarations + assignment, NOT TS constructor-parameter-
|
|
92
|
+
// property shorthand: Vite's `bundleConfigFile` externalizes any bare
|
|
93
|
+
// (non-relative) import when loading `vite.config.ts` (see
|
|
94
|
+
// `@barefootjs/erb/vite`'s docstring), so this file can be loaded
|
|
95
|
+
// directly by Node's OWN native TypeScript type-stripping (enabled by
|
|
96
|
+
// default since Node 22.18/23.6) rather than esbuild — and Node's
|
|
97
|
+
// strip-only mode does not support parameter properties (`SyntaxError
|
|
98
|
+
// [ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX]`), only plain type annotations.
|
|
99
|
+
private readonly param: string
|
|
100
|
+
private readonly localVarMap: Map<string, string>
|
|
101
|
+
// Whether `name` currently names a bare Ruby local bound by an
|
|
102
|
+
// ENCLOSING loop/block (distinct from `this.param`, which is this
|
|
103
|
+
// predicate's own — possibly nested — loop param). See
|
|
104
|
+
// `ErbEmitContext.isLoopBoundName`'s docstring for why ERB needs this
|
|
105
|
+
// and EP does not.
|
|
106
|
+
private readonly isLoopBoundOuter: (n: string) => boolean
|
|
107
|
+
// Reports whether a getter/prop name is string-typed, for the Hash-vs-
|
|
108
|
+
// Array index-access split (#operand.ts). Defaults to "never" for
|
|
109
|
+
// callers that don't thread it through.
|
|
110
|
+
private readonly isStringName: (n: string) => boolean
|
|
111
|
+
// Records a BF101 for nested callback shapes this emitter can only
|
|
112
|
+
// degrade — `find*` and the non-predicate methods. Optional so emitter
|
|
113
|
+
// construction stays possible without an adapter; a missing hook keeps
|
|
114
|
+
// the old silent-degrade emit.
|
|
115
|
+
private readonly onUnsupported?: (message: string, reason?: string) => void
|
|
116
|
+
// The Ruby local to EMIT for a reference matching `this.param` — as
|
|
117
|
+
// opposed to `this.param` itself, which is only the name to MATCH.
|
|
118
|
+
// These two are the SAME value everywhere in this file except the
|
|
119
|
+
// `filter().map()` loop-gating `<if>` (erb-adapter.ts's `renderLoop`,
|
|
120
|
+
// #2245): `todos.filter(t => t.done).map(todo => ...)` parses the
|
|
121
|
+
// predicate against the filter callback's OWN param (`t`), but the
|
|
122
|
+
// Ruby local actually bound by the loop is the MAP callback's param
|
|
123
|
+
// (`todo`) — Ruby has no per-callback block scope there (unlike the
|
|
124
|
+
// real nested `.select { |t| ... }` block `callbackMethod` below
|
|
125
|
+
// builds, where match and render are naturally the same param).
|
|
126
|
+
// Defaults to `rubyLocal(param)`, i.e. every other construction site is
|
|
127
|
+
// unaffected.
|
|
128
|
+
private readonly renderParamAs: string
|
|
129
|
+
|
|
91
130
|
constructor(
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
// construction stays possible without an adapter; a missing hook keeps
|
|
107
|
-
// the old silent-degrade emit.
|
|
108
|
-
private readonly onUnsupported?: (message: string, reason?: string) => void,
|
|
109
|
-
// The Ruby local to EMIT for a reference matching `this.param` — as
|
|
110
|
-
// opposed to `this.param` itself, which is only the name to MATCH.
|
|
111
|
-
// These two are the SAME value everywhere in this file except the
|
|
112
|
-
// `filter().map()` loop-gating `<if>` (erb-adapter.ts's `renderLoop`,
|
|
113
|
-
// #2245): `todos.filter(t => t.done).map(todo => ...)` parses the
|
|
114
|
-
// predicate against the filter callback's OWN param (`t`), but the
|
|
115
|
-
// Ruby local actually bound by the loop is the MAP callback's param
|
|
116
|
-
// (`todo`) — Ruby has no per-callback block scope there (unlike the
|
|
117
|
-
// real nested `.select { |t| ... }` block `callbackMethod` below
|
|
118
|
-
// builds, where match and render are naturally the same param).
|
|
119
|
-
// Defaults to `rubyLocal(this.param)`, i.e. every other construction
|
|
120
|
-
// site is unaffected.
|
|
121
|
-
private readonly renderParamAs: string = rubyLocal(param),
|
|
122
|
-
) {}
|
|
131
|
+
param: string,
|
|
132
|
+
localVarMap: Map<string, string>,
|
|
133
|
+
isLoopBoundOuter: (n: string) => boolean = () => false,
|
|
134
|
+
isStringName: (n: string) => boolean = unusedIsStringName,
|
|
135
|
+
onUnsupported?: (message: string, reason?: string) => void,
|
|
136
|
+
renderParamAs: string = rubyLocal(param),
|
|
137
|
+
) {
|
|
138
|
+
this.param = param
|
|
139
|
+
this.localVarMap = localVarMap
|
|
140
|
+
this.isLoopBoundOuter = isLoopBoundOuter
|
|
141
|
+
this.isStringName = isStringName
|
|
142
|
+
this.onUnsupported = onUnsupported
|
|
143
|
+
this.renderParamAs = renderParamAs
|
|
144
|
+
}
|
|
123
145
|
|
|
124
146
|
identifier(name: string): string {
|
|
125
147
|
if (name === this.param) return this.renderParamAs
|
|
@@ -315,7 +337,13 @@ export class ErbFilterEmitter implements ParsedExprEmitter {
|
|
|
315
337
|
* - the `unsupported` fallback returns the safe empty-string Ruby literal.
|
|
316
338
|
*/
|
|
317
339
|
export class ErbTopLevelEmitter implements ParsedExprEmitter {
|
|
318
|
-
|
|
340
|
+
// Plain field + assignment, not a parameter property — see
|
|
341
|
+
// `ErbFilterEmitter`'s constructor comment above for why.
|
|
342
|
+
private readonly ctx: ErbEmitContext
|
|
343
|
+
|
|
344
|
+
constructor(ctx: ErbEmitContext) {
|
|
345
|
+
this.ctx = ctx
|
|
346
|
+
}
|
|
319
347
|
|
|
320
348
|
identifier(name: string): string {
|
|
321
349
|
// `undefined` / `null` nested inside a larger expression tree (e.g.
|