@barefootjs/rust 0.30.6 → 0.31.1
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/README.md +11 -7
- package/dist/adapter/expr/emitters.d.ts +1 -1
- package/dist/adapter/expr/emitters.d.ts.map +1 -1
- package/dist/adapter/index.js +18 -3
- package/dist/adapter/minijinja-adapter.d.ts +0 -1
- package/dist/adapter/minijinja-adapter.d.ts.map +1 -1
- package/dist/index.js +18 -3
- package/dist/vite.d.ts +38 -0
- package/dist/vite.d.ts.map +1 -0
- package/dist/vite.js +6738 -0
- package/package.json +22 -9
- package/runtime/src/runtime.rs +54 -6
- package/src/__tests__/minijinja-adapter-unit.test.ts +146 -0
- package/src/__tests__/vite.test.ts +126 -0
- package/src/adapter/expr/emitters.ts +33 -9
- package/src/adapter/minijinja-adapter.ts +30 -5
- package/src/vite.ts +217 -0
- package/dist/build.d.ts +0 -29
- package/dist/build.d.ts.map +0 -1
- package/dist/build.js +0 -189253
- package/src/build.ts +0 -38
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@barefootjs/rust",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.31.1",
|
|
4
4
|
"description": "minijinja (Rust) adapter for BarefootJS — compiles IR to .j2 templates and ships a Rust rendering runtime (packages/adapter-rust/runtime/); runs under any Rust web framework (axum, etc.)",
|
|
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-rust"
|
|
55
55
|
},
|
|
56
56
|
"dependencies": {
|
|
57
|
-
"@barefootjs/shared": "0.
|
|
57
|
+
"@barefootjs/shared": "0.31.1"
|
|
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.1",
|
|
75
|
+
"@barefootjs/vite": "0.31.1",
|
|
76
|
+
"@barefootjs/client": "0.31.1",
|
|
77
|
+
"typescript": "^5.0.0",
|
|
78
|
+
"vite": "^6.0.0"
|
|
66
79
|
}
|
|
67
80
|
}
|
package/runtime/src/runtime.rs
CHANGED
|
@@ -811,6 +811,14 @@ pub struct ChildRendererSpec {
|
|
|
811
811
|
pub struct RenderSession {
|
|
812
812
|
pub scripts: Mutex<Vec<String>>,
|
|
813
813
|
pub script_seen: Mutex<HashSet<String>>,
|
|
814
|
+
/// `<link rel="modulepreload">` hints, kept as their own list/dedup-set
|
|
815
|
+
/// pair mirroring `scripts`/`script_seen` exactly -- see
|
|
816
|
+
/// `register_preload`'s docstring. A preload URL and a script URL are
|
|
817
|
+
/// never the same concern (preloads are the entry's TRANSITIVE deps,
|
|
818
|
+
/// never the entry itself), so there is no cross-dedup between the two
|
|
819
|
+
/// sets.
|
|
820
|
+
pub preloads: Mutex<Vec<String>>,
|
|
821
|
+
pub preload_seen: Mutex<HashSet<String>>,
|
|
814
822
|
pub child_renderers: Mutex<HashMap<String, ChildRendererSpec>>,
|
|
815
823
|
pub context_stacks: Mutex<HashMap<String, Vec<JsValue>>>,
|
|
816
824
|
rng_counter: Mutex<u64>,
|
|
@@ -821,6 +829,8 @@ impl RenderSession {
|
|
|
821
829
|
Arc::new(RenderSession {
|
|
822
830
|
scripts: Mutex::new(Vec::new()),
|
|
823
831
|
script_seen: Mutex::new(HashSet::new()),
|
|
832
|
+
preloads: Mutex::new(Vec::new()),
|
|
833
|
+
preload_seen: Mutex::new(HashSet::new()),
|
|
824
834
|
child_renderers: Mutex::new(HashMap::new()),
|
|
825
835
|
context_stacks: Mutex::new(HashMap::new()),
|
|
826
836
|
rng_counter: Mutex::new(0),
|
|
@@ -976,20 +986,54 @@ impl BfInstance {
|
|
|
976
986
|
self.session.scripts.lock().unwrap().push(path.to_string());
|
|
977
987
|
}
|
|
978
988
|
|
|
989
|
+
/// Register a `<link rel="modulepreload">` hint (a component's resolved
|
|
990
|
+
/// TRANSITIVE-chunk preload URL, per `AdapterGenerateOptions.
|
|
991
|
+
/// preloadAssets`) into the SAME session every `BfInstance` clone in
|
|
992
|
+
/// this render tree shares (`Arc<RenderSession>` -- see the struct
|
|
993
|
+
/// docstring), exactly mirroring `register_script` above. Because every
|
|
994
|
+
/// clone (root, and every child minted by `render_child`) holds the
|
|
995
|
+
/// SAME `Arc`, a preload registered three levels deep is visible to the
|
|
996
|
+
/// root's `scripts()` call with no separate propagation step needed --
|
|
997
|
+
/// unlike the PHP/Python ports, which mutate per-instance state and
|
|
998
|
+
/// must explicitly re-seed each child's collector from the parent's.
|
|
999
|
+
fn register_preload(&self, path: &str) {
|
|
1000
|
+
let mut seen = self.session.preload_seen.lock().unwrap();
|
|
1001
|
+
if seen.contains(path) {
|
|
1002
|
+
return;
|
|
1003
|
+
}
|
|
1004
|
+
seen.insert(path.to_string());
|
|
1005
|
+
self.session.preloads.lock().unwrap().push(path.to_string());
|
|
1006
|
+
}
|
|
1007
|
+
|
|
979
1008
|
/// `pub` (beyond the `"scripts"` `call_method` dispatch below) so a
|
|
980
|
-
/// production host can read back the accumulated `<script>`
|
|
981
|
-
/// rendering, to splice into its own page layout (mirrors
|
|
982
|
-
/// integrations' `bf.scripts()` call in their layout helper, e.g.
|
|
1009
|
+
/// production host can read back the accumulated `<link>`/`<script>`
|
|
1010
|
+
/// tags AFTER rendering, to splice into its own page layout (mirrors
|
|
1011
|
+
/// Python integrations' `bf.scripts()` call in their layout helper, e.g.
|
|
983
1012
|
/// `integrations/flask/app.py`'s `layout(..., scripts=bf.scripts())`).
|
|
1013
|
+
///
|
|
1014
|
+
/// Preload hints are emitted FIRST, ahead of every `<script
|
|
1015
|
+
/// type="module">` tag -- a hint that arrives after the script it
|
|
1016
|
+
/// describes is useless. Preloads carry no execution-order constraint
|
|
1017
|
+
/// the way scripts do (they are just hints, not code that runs), so
|
|
1018
|
+
/// registration order is emitted as-is.
|
|
984
1019
|
pub fn scripts(&self) -> String {
|
|
985
|
-
self
|
|
1020
|
+
let preload_tags = self
|
|
1021
|
+
.session
|
|
1022
|
+
.preloads
|
|
1023
|
+
.lock()
|
|
1024
|
+
.unwrap()
|
|
1025
|
+
.iter()
|
|
1026
|
+
.map(|p| format!("<link rel=\"modulepreload\" crossorigin href=\"{p}\">"))
|
|
1027
|
+
.collect::<Vec<_>>();
|
|
1028
|
+
let script_tags = self
|
|
1029
|
+
.session
|
|
986
1030
|
.scripts
|
|
987
1031
|
.lock()
|
|
988
1032
|
.unwrap()
|
|
989
1033
|
.iter()
|
|
990
1034
|
.map(|p| format!("<script type=\"module\" src=\"{p}\"></script>"))
|
|
991
|
-
.collect::<Vec<_>>()
|
|
992
|
-
|
|
1035
|
+
.collect::<Vec<_>>();
|
|
1036
|
+
preload_tags.into_iter().chain(script_tags).collect::<Vec<_>>().join("\n")
|
|
993
1037
|
}
|
|
994
1038
|
|
|
995
1039
|
/// Renderer contract (#1897): invoked from a template as
|
|
@@ -1305,6 +1349,10 @@ impl Object for BfInstance {
|
|
|
1305
1349
|
self.register_script(a(0).as_str().unwrap_or(""));
|
|
1306
1350
|
Ok(MjValue::from(()))
|
|
1307
1351
|
}
|
|
1352
|
+
"register_preload" => {
|
|
1353
|
+
self.register_preload(a(0).as_str().unwrap_or(""));
|
|
1354
|
+
Ok(MjValue::from(()))
|
|
1355
|
+
}
|
|
1308
1356
|
"scripts" => Ok(safe(self.scripts())),
|
|
1309
1357
|
|
|
1310
1358
|
// -- Streaming SSR ----------------------------------------------
|
|
@@ -555,3 +555,149 @@ export function Wrapper({ children }: { children?: any }) {
|
|
|
555
555
|
)
|
|
556
556
|
})
|
|
557
557
|
})
|
|
558
|
+
|
|
559
|
+
describe('MinijinjaAdapter - scriptAssets (Vite late-binding, PR1)', () => {
|
|
560
|
+
const CLIENT_COMPONENT = `
|
|
561
|
+
'use client'
|
|
562
|
+
import { createSignal } from '@barefootjs/client'
|
|
563
|
+
export function Counter() {
|
|
564
|
+
const [count, setCount] = createSignal(0)
|
|
565
|
+
return <button onClick={() => setCount(count() + 1)}>{count()}</button>
|
|
566
|
+
}
|
|
567
|
+
`
|
|
568
|
+
|
|
569
|
+
test('emits one register_script per URL, in order, when scriptAssets is set', () => {
|
|
570
|
+
const ir = compileToIR(CLIENT_COMPONENT)
|
|
571
|
+
const { template } = new MinijinjaAdapter().generate(ir, {
|
|
572
|
+
scriptAssets: ['/assets/runtime-abc123.js', '/assets/counter-def456.js'],
|
|
573
|
+
})
|
|
574
|
+
const runtimeIdx = template.indexOf("bf.register_script('/assets/runtime-abc123.js')")
|
|
575
|
+
const compIdx = template.indexOf("bf.register_script('/assets/counter-def456.js')")
|
|
576
|
+
expect(runtimeIdx).toBeGreaterThanOrEqual(0)
|
|
577
|
+
expect(compIdx).toBeGreaterThanOrEqual(0)
|
|
578
|
+
expect(runtimeIdx).toBeLessThan(compIdx)
|
|
579
|
+
expect(template).toContain('_bf_reg0')
|
|
580
|
+
expect(template).toContain('_bf_reg1')
|
|
581
|
+
expect(template).not.toContain('/static/components/barefoot.js')
|
|
582
|
+
expect(template).not.toContain('Counter.client.js')
|
|
583
|
+
})
|
|
584
|
+
|
|
585
|
+
test('emits a single registration for a single-element scriptAssets array', () => {
|
|
586
|
+
const ir = compileToIR(CLIENT_COMPONENT)
|
|
587
|
+
const { template } = new MinijinjaAdapter().generate(ir, {
|
|
588
|
+
scriptAssets: ['/assets/only-one.js'],
|
|
589
|
+
})
|
|
590
|
+
expect(template).toContain("bf.register_script('/assets/only-one.js')")
|
|
591
|
+
expect(template.match(/register_script/g)?.length).toBe(1)
|
|
592
|
+
})
|
|
593
|
+
|
|
594
|
+
test('an empty scriptAssets array emits no script registrations', () => {
|
|
595
|
+
const ir = compileToIR(CLIENT_COMPONENT)
|
|
596
|
+
const { template } = new MinijinjaAdapter().generate(ir, { scriptAssets: [] })
|
|
597
|
+
expect(template).not.toContain('register_script')
|
|
598
|
+
})
|
|
599
|
+
|
|
600
|
+
test('skipScriptRegistration still wins when scriptAssets is also set', () => {
|
|
601
|
+
const ir = compileToIR(CLIENT_COMPONENT)
|
|
602
|
+
const { template } = new MinijinjaAdapter().generate(ir, {
|
|
603
|
+
skipScriptRegistration: true,
|
|
604
|
+
scriptAssets: ['/assets/should-not-appear.js'],
|
|
605
|
+
})
|
|
606
|
+
expect(template).not.toContain('register_script')
|
|
607
|
+
})
|
|
608
|
+
|
|
609
|
+
test('absent scriptAssets falls back to adapter-computed script paths', () => {
|
|
610
|
+
const ir = compileToIR(CLIENT_COMPONENT)
|
|
611
|
+
const computed = new MinijinjaAdapter().generate(ir).template
|
|
612
|
+
const explicitUndefined = new MinijinjaAdapter().generate(ir, { scriptAssets: undefined }).template
|
|
613
|
+
expect(computed).toContain("bf.register_script('/static/components/barefoot.js')")
|
|
614
|
+
expect(computed).toContain("bf.register_script('/static/components/Counter.client.js')")
|
|
615
|
+
expect(explicitUndefined).toBe(computed)
|
|
616
|
+
})
|
|
617
|
+
})
|
|
618
|
+
|
|
619
|
+
describe('MinijinjaAdapter - preloadAssets', () => {
|
|
620
|
+
const CLIENT_COMPONENT = `
|
|
621
|
+
'use client'
|
|
622
|
+
import { createSignal } from '@barefootjs/client'
|
|
623
|
+
export function Counter() {
|
|
624
|
+
const [count, setCount] = createSignal(0)
|
|
625
|
+
return <button onClick={() => setCount(count() + 1)}>{count()}</button>
|
|
626
|
+
}
|
|
627
|
+
`
|
|
628
|
+
|
|
629
|
+
test('non-empty preloadAssets + non-empty scriptAssets: preload registrations emitted, in order, before script registrations', () => {
|
|
630
|
+
const ir = compileToIR(CLIENT_COMPONENT)
|
|
631
|
+
const { template } = new MinijinjaAdapter().generate(ir, {
|
|
632
|
+
scriptAssets: ['/assets/runtime-abc123.js', '/assets/counter-def456.js'],
|
|
633
|
+
preloadAssets: ['/assets/index-pre1.js', '/assets/shared-pre2.js'],
|
|
634
|
+
})
|
|
635
|
+
const pre1Idx = template.indexOf("{% set _bf_pre0 = bf.register_preload('/assets/index-pre1.js') %}")
|
|
636
|
+
const pre2Idx = template.indexOf("{% set _bf_pre1 = bf.register_preload('/assets/shared-pre2.js') %}")
|
|
637
|
+
const script1Idx = template.indexOf("{% set _bf_reg0 = bf.register_script('/assets/runtime-abc123.js') %}")
|
|
638
|
+
const script2Idx = template.indexOf("{% set _bf_reg1 = bf.register_script('/assets/counter-def456.js') %}")
|
|
639
|
+
expect(pre1Idx).toBeGreaterThanOrEqual(0)
|
|
640
|
+
expect(pre2Idx).toBeGreaterThan(pre1Idx)
|
|
641
|
+
expect(script1Idx).toBeGreaterThan(pre2Idx)
|
|
642
|
+
expect(script2Idx).toBeGreaterThan(script1Idx)
|
|
643
|
+
})
|
|
644
|
+
|
|
645
|
+
test('preloadAssets: [] emits no preload registration', () => {
|
|
646
|
+
const ir = compileToIR(CLIENT_COMPONENT)
|
|
647
|
+
const { template } = new MinijinjaAdapter().generate(ir, {
|
|
648
|
+
scriptAssets: ['/assets/runtime-abc123.js'],
|
|
649
|
+
preloadAssets: [],
|
|
650
|
+
})
|
|
651
|
+
expect(template).not.toContain('register_preload')
|
|
652
|
+
expect(template).toContain("bf.register_script('/assets/runtime-abc123.js')")
|
|
653
|
+
})
|
|
654
|
+
|
|
655
|
+
test('preloadAssets: undefined emits no preload registration', () => {
|
|
656
|
+
const ir = compileToIR(CLIENT_COMPONENT)
|
|
657
|
+
const { template } = new MinijinjaAdapter().generate(ir, {
|
|
658
|
+
scriptAssets: ['/assets/runtime-abc123.js'],
|
|
659
|
+
preloadAssets: undefined,
|
|
660
|
+
})
|
|
661
|
+
expect(template).not.toContain('register_preload')
|
|
662
|
+
expect(template).toContain("bf.register_script('/assets/runtime-abc123.js')")
|
|
663
|
+
})
|
|
664
|
+
|
|
665
|
+
test('preloadAssets non-empty but scriptAssets: [] emits no preload registration (preloads are only meaningful alongside a real script)', () => {
|
|
666
|
+
const ir = compileToIR(CLIENT_COMPONENT)
|
|
667
|
+
const { template } = new MinijinjaAdapter().generate(ir, {
|
|
668
|
+
scriptAssets: [],
|
|
669
|
+
preloadAssets: ['/assets/index-pre1.js'],
|
|
670
|
+
})
|
|
671
|
+
expect(template).not.toContain('register_preload')
|
|
672
|
+
expect(template).not.toContain('register_script')
|
|
673
|
+
})
|
|
674
|
+
|
|
675
|
+
test('skipScriptRegistration: true suppresses both preloads and scripts', () => {
|
|
676
|
+
const ir = compileToIR(CLIENT_COMPONENT)
|
|
677
|
+
const { template } = new MinijinjaAdapter().generate(ir, {
|
|
678
|
+
skipScriptRegistration: true,
|
|
679
|
+
scriptAssets: ['/assets/runtime-abc123.js'],
|
|
680
|
+
preloadAssets: ['/assets/index-pre1.js'],
|
|
681
|
+
})
|
|
682
|
+
expect(template).not.toContain('register_preload')
|
|
683
|
+
expect(template).not.toContain('register_script')
|
|
684
|
+
})
|
|
685
|
+
|
|
686
|
+
// Regression guard: a previous attempt emitted a literal
|
|
687
|
+
// `<link rel="modulepreload">` tag directly into the component template,
|
|
688
|
+
// which injected a rendered DOM node before the component's root and
|
|
689
|
+
// broke hydration across all eight integrations (blade, erb,
|
|
690
|
+
// go-template, jinja, mojolicious, rust, twig, xslate). Preload hints
|
|
691
|
+
// must ONLY ever be emitted as no-output register statements (here,
|
|
692
|
+
// `{% set _bf_preN = bf.register_preload(...) %}`) that the adapter's
|
|
693
|
+
// runtime later renders itself — never as literal markup baked into the
|
|
694
|
+
// template.
|
|
695
|
+
test('never emits a literal <link tag into the template', () => {
|
|
696
|
+
const ir = compileToIR(CLIENT_COMPONENT)
|
|
697
|
+
const { template } = new MinijinjaAdapter().generate(ir, {
|
|
698
|
+
scriptAssets: ['/assets/runtime-abc123.js'],
|
|
699
|
+
preloadAssets: ['/assets/index-pre1.js'],
|
|
700
|
+
})
|
|
701
|
+
expect(template).not.toContain('<link')
|
|
702
|
+
})
|
|
703
|
+
})
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Coverage of `@barefootjs/rust/vite`'s `barefoot()`:
|
|
3
|
+
*
|
|
4
|
+
* - one real `vite build()` end to end (mirrors `@barefootjs/go-template/
|
|
5
|
+
* vite`, `@barefootjs/hono/vite`, `@barefootjs/blade/vite`'s,
|
|
6
|
+
* `@barefootjs/jinja/vite`'s, and `@barefootjs/erb/vite`'s own
|
|
7
|
+
* `vite.test.ts` rigor — a plugin that only passes mocked unit tests
|
|
8
|
+
* hasn't been shown to work) against a checked-in fixture (not a system
|
|
9
|
+
* tmpdir) so `@barefootjs/client` resolves through the monorepo's real
|
|
10
|
+
* node_modules symlinks;
|
|
11
|
+
* - the `assets` → generated `bf-assets.json` behavior, exercised the same
|
|
12
|
+
* way (a real build, since it needs the real manifest Vite writes).
|
|
13
|
+
*
|
|
14
|
+
* Unlike Go, there is no `afterEmit`-driven type-combination step to test
|
|
15
|
+
* here (see `vite.ts`'s module docstring) — `MinijinjaAdapter.generate()`
|
|
16
|
+
* never produces a `types` section at all, so `barefoot()` always returns a
|
|
17
|
+
* single-element array unless `assets` is set.
|
|
18
|
+
*/
|
|
19
|
+
import { describe, test, expect } from 'bun:test'
|
|
20
|
+
import { build } from 'vite'
|
|
21
|
+
import { mkdtemp, rm, readFile } from 'node:fs/promises'
|
|
22
|
+
import { tmpdir } from 'node:os'
|
|
23
|
+
import { join, resolve } from 'node:path'
|
|
24
|
+
import { barefoot, barefoot as defaultBarefoot } from '../vite.ts'
|
|
25
|
+
|
|
26
|
+
const FIXTURE_ROOT = resolve(import.meta.dirname, '../../e2e-fixture')
|
|
27
|
+
|
|
28
|
+
describe('@barefootjs/rust/vite: real vite build', () => {
|
|
29
|
+
test('exports the same function as both named `barefoot` and default', () => {
|
|
30
|
+
expect(defaultBarefoot).toBe(barefoot)
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
test('returns a single-element plugin array when `assets` is omitted', () => {
|
|
34
|
+
const plugins = barefoot({ components: ['src/components'], templates: 'views' })
|
|
35
|
+
expect(plugins).toHaveLength(1)
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
test('writes a self-contained .j2 template with scriptAssets baked in, same as core alone would do', async () => {
|
|
39
|
+
const outDir = await mkdtemp(join(tmpdir(), 'barefoot-rust-vite-dist-'))
|
|
40
|
+
const templatesDir = await mkdtemp(join(tmpdir(), 'barefoot-rust-vite-views-'))
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
await build({
|
|
44
|
+
configFile: false,
|
|
45
|
+
root: FIXTURE_ROOT,
|
|
46
|
+
base: '/static/build/',
|
|
47
|
+
logLevel: 'warn',
|
|
48
|
+
build: { outDir, emptyOutDir: true },
|
|
49
|
+
plugins: barefoot({
|
|
50
|
+
components: ['src/components'],
|
|
51
|
+
templates: templatesDir,
|
|
52
|
+
}),
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
const template = await readFile(join(templatesDir, 'Counter.j2'), 'utf8')
|
|
56
|
+
expect(template).toContain("bf.register_script(")
|
|
57
|
+
} finally {
|
|
58
|
+
await rm(outDir, { recursive: true, force: true })
|
|
59
|
+
await rm(templatesDir, { recursive: true, force: true })
|
|
60
|
+
}
|
|
61
|
+
}, 60_000)
|
|
62
|
+
|
|
63
|
+
test('`assets` resolves a non-component entry\'s manifest-hashed URL into a generated JSON asset map', async () => {
|
|
64
|
+
const outDir = await mkdtemp(join(tmpdir(), 'barefoot-rust-vite-dist-assets-'))
|
|
65
|
+
const templatesDir = await mkdtemp(join(tmpdir(), 'barefoot-rust-vite-views-assets-'))
|
|
66
|
+
const assetsPath = join(FIXTURE_ROOT, 'dist/bf-assets.json')
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
await build({
|
|
70
|
+
configFile: false,
|
|
71
|
+
root: FIXTURE_ROOT,
|
|
72
|
+
base: '/static/build/',
|
|
73
|
+
logLevel: 'warn',
|
|
74
|
+
build: {
|
|
75
|
+
outDir,
|
|
76
|
+
emptyOutDir: true,
|
|
77
|
+
// Registering the non-component entry is the CALLER's job (stock
|
|
78
|
+
// Vite config) — `assets` below only resolves the URL Vite
|
|
79
|
+
// already bundled it to, it doesn't request the bundling.
|
|
80
|
+
rollupOptions: { input: { bootstrap: resolve(FIXTURE_ROOT, 'client/bootstrap.ts') } },
|
|
81
|
+
},
|
|
82
|
+
plugins: barefoot({
|
|
83
|
+
components: ['src/components'],
|
|
84
|
+
templates: templatesDir,
|
|
85
|
+
assets: { Bootstrap: 'client/bootstrap.ts' },
|
|
86
|
+
}),
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
const manifest = JSON.parse(await readFile(join(outDir, '.vite/manifest.json'), 'utf8'))
|
|
90
|
+
const expectedUrl = `/static/build/${manifest['client/bootstrap.ts'].file}`
|
|
91
|
+
|
|
92
|
+
const content = JSON.parse(await readFile(assetsPath, 'utf8'))
|
|
93
|
+
expect(content).toEqual({ Bootstrap: expectedUrl })
|
|
94
|
+
} finally {
|
|
95
|
+
await rm(outDir, { recursive: true, force: true })
|
|
96
|
+
await rm(templatesDir, { recursive: true, force: true })
|
|
97
|
+
await rm(join(FIXTURE_ROOT, 'dist'), { recursive: true, force: true })
|
|
98
|
+
}
|
|
99
|
+
}, 60_000)
|
|
100
|
+
|
|
101
|
+
test('`assets` throws an actionable error when the entry was never registered as a Rollup input', async () => {
|
|
102
|
+
const outDir = await mkdtemp(join(tmpdir(), 'barefoot-rust-vite-dist-assets-missing-'))
|
|
103
|
+
const templatesDir = await mkdtemp(join(tmpdir(), 'barefoot-rust-vite-views-assets-missing-'))
|
|
104
|
+
|
|
105
|
+
try {
|
|
106
|
+
await expect(
|
|
107
|
+
build({
|
|
108
|
+
configFile: false,
|
|
109
|
+
root: FIXTURE_ROOT,
|
|
110
|
+
base: '/static/build/',
|
|
111
|
+
logLevel: 'silent',
|
|
112
|
+
build: { outDir, emptyOutDir: true },
|
|
113
|
+
plugins: barefoot({
|
|
114
|
+
components: ['src/components'],
|
|
115
|
+
templates: templatesDir,
|
|
116
|
+
assets: { Bootstrap: 'client/bootstrap.ts' },
|
|
117
|
+
}),
|
|
118
|
+
}),
|
|
119
|
+
).rejects.toThrow(/was not found in the build manifest/)
|
|
120
|
+
} finally {
|
|
121
|
+
await rm(outDir, { recursive: true, force: true })
|
|
122
|
+
await rm(templatesDir, { recursive: true, force: true })
|
|
123
|
+
await rm(join(FIXTURE_ROOT, 'dist'), { recursive: true, force: true })
|
|
124
|
+
}
|
|
125
|
+
}, 60_000)
|
|
126
|
+
})
|
|
@@ -117,15 +117,33 @@ export function truthyTest(node: ParsedExpr, rendered: string): string {
|
|
|
117
117
|
* callback's receiver.
|
|
118
118
|
*/
|
|
119
119
|
export class JinjaFilterEmitter implements ParsedExprEmitter {
|
|
120
|
+
// Plain field declarations + assignment, NOT TS constructor-parameter-
|
|
121
|
+
// property shorthand: Vite's `bundleConfigFile` externalizes any bare
|
|
122
|
+
// (non-relative) import when loading `vite.config.ts` (see
|
|
123
|
+
// `@barefootjs/rust/vite`'s docstring), so this file can be loaded
|
|
124
|
+
// directly by Node's OWN native TypeScript type-stripping (enabled by
|
|
125
|
+
// default since Node 22.18/23.6) rather than esbuild — and Node's
|
|
126
|
+
// strip-only mode does not support parameter properties (`SyntaxError
|
|
127
|
+
// [ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX]`), only plain type annotations.
|
|
128
|
+
private readonly param: string
|
|
129
|
+
private readonly localVarMap: Map<string, string>
|
|
130
|
+
private readonly isStringName: (n: string) => boolean
|
|
131
|
+
// Records a BF101 for predicate shapes this emitter can only degrade
|
|
132
|
+
// (#2038). Optional so emitter construction stays possible without an
|
|
133
|
+
// adapter; a missing hook keeps the old silent-degrade emit.
|
|
134
|
+
private readonly onUnsupported?: (message: string, reason?: string) => void
|
|
135
|
+
|
|
120
136
|
constructor(
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
137
|
+
param: string,
|
|
138
|
+
localVarMap: Map<string, string>,
|
|
139
|
+
isStringName: (n: string) => boolean = () => false,
|
|
140
|
+
onUnsupported?: (message: string, reason?: string) => void,
|
|
141
|
+
) {
|
|
142
|
+
this.param = param
|
|
143
|
+
this.localVarMap = localVarMap
|
|
144
|
+
this.isStringName = isStringName
|
|
145
|
+
this.onUnsupported = onUnsupported
|
|
146
|
+
}
|
|
129
147
|
|
|
130
148
|
identifier(name: string): string {
|
|
131
149
|
if (name === this.param) return minijinjaIdent(this.param)
|
|
@@ -288,7 +306,13 @@ export class JinjaFilterEmitter implements ParsedExprEmitter {
|
|
|
288
306
|
* - no lambda fallback exists (see the file header, divergence 2).
|
|
289
307
|
*/
|
|
290
308
|
export class JinjaTopLevelEmitter implements ParsedExprEmitter {
|
|
291
|
-
|
|
309
|
+
// Plain field + assignment, not a parameter property — see
|
|
310
|
+
// `JinjaFilterEmitter`'s constructor comment above for why.
|
|
311
|
+
private readonly ctx: JinjaEmitContext
|
|
312
|
+
|
|
313
|
+
constructor(ctx: JinjaEmitContext) {
|
|
314
|
+
this.ctx = ctx
|
|
315
|
+
}
|
|
292
316
|
|
|
293
317
|
identifier(name: string): string {
|
|
294
318
|
// `undefined` / `null` nested inside a larger expression tree — Jinja
|
|
@@ -241,9 +241,6 @@ export class MinijinjaAdapter extends BaseAdapter implements IRNodeEmitter<Jinja
|
|
|
241
241
|
name = 'minijinja'
|
|
242
242
|
extension = '.j2'
|
|
243
243
|
templatesPerComponent = true
|
|
244
|
-
// Template-string target with no component layer: `bf build` emits a static
|
|
245
|
-
// import-map HTML snippet to include into the page <head>.
|
|
246
|
-
importMapInjection = 'html-snippet' as const
|
|
247
244
|
|
|
248
245
|
/**
|
|
249
246
|
* Identifier-path callees the Jinja runtime can render in template scope.
|
|
@@ -408,7 +405,7 @@ export class MinijinjaAdapter extends BaseAdapter implements IRNodeEmitter<Jinja
|
|
|
408
405
|
// Generate script registration
|
|
409
406
|
const scriptReg = options?.skipScriptRegistration
|
|
410
407
|
? ''
|
|
411
|
-
: this.generateScriptRegistrations(ir, options?.scriptBaseName)
|
|
408
|
+
: this.generateScriptRegistrations(ir, options?.scriptBaseName, options?.scriptAssets, options?.preloadAssets)
|
|
412
409
|
|
|
413
410
|
// SSR context consumers (`const x = useContext(Ctx)`): seed each local
|
|
414
411
|
// from the active provider value (or the `createContext` default). The
|
|
@@ -450,7 +447,35 @@ export class MinijinjaAdapter extends BaseAdapter implements IRNodeEmitter<Jinja
|
|
|
450
447
|
// Script Registration
|
|
451
448
|
// ===========================================================================
|
|
452
449
|
|
|
453
|
-
private generateScriptRegistrations(ir: ComponentIR, scriptBaseName?: string): string {
|
|
450
|
+
private generateScriptRegistrations(ir: ComponentIR, scriptBaseName?: string, scriptAssets?: string[], preloadAssets?: string[]): string {
|
|
451
|
+
// `scriptAssets`, when present (including `[]`), fully supersedes the
|
|
452
|
+
// adapter-computed `barefootJsPath` / `clientJsBasePath` pair — see
|
|
453
|
+
// `AdapterGenerateOptions.scriptAssets`. The caller (e.g. the Vite
|
|
454
|
+
// plugin) has already decided the exact ordered URL list, including
|
|
455
|
+
// whether any script is needed at all.
|
|
456
|
+
if (scriptAssets) {
|
|
457
|
+
if (scriptAssets.length === 0) return ''
|
|
458
|
+
const lines: string[] = []
|
|
459
|
+
// `preloadAssets` is only meaningful alongside a non-empty
|
|
460
|
+
// `scriptAssets` (see `AdapterGenerateOptions.preloadAssets`) — this
|
|
461
|
+
// branch is only reached when that already holds. Emitted BEFORE the
|
|
462
|
+
// script registrations: every preload hint must precede the script
|
|
463
|
+
// tags it describes, or the hint is useless (see `register_preload`'s
|
|
464
|
+
// docstring in the Rust runtime — `bf.register_preload`/`bf.scripts()`
|
|
465
|
+
// preserve that ordering downstream, so codegen order is what fixes
|
|
466
|
+
// it here).
|
|
467
|
+
if (preloadAssets && preloadAssets.length > 0) {
|
|
468
|
+
preloadAssets.forEach((url, i) => {
|
|
469
|
+
lines.push(`{% set _bf_pre${i} = bf.register_preload('${url}') %}`)
|
|
470
|
+
})
|
|
471
|
+
}
|
|
472
|
+
scriptAssets.forEach((url, i) => {
|
|
473
|
+
lines.push(`{% set _bf_reg${i} = bf.register_script('${url}') %}`)
|
|
474
|
+
})
|
|
475
|
+
lines.push('')
|
|
476
|
+
return lines.join('\n')
|
|
477
|
+
}
|
|
478
|
+
|
|
454
479
|
const hasInteractivity = hasClientInteractivity(ir)
|
|
455
480
|
if (!hasInteractivity) return ''
|
|
456
481
|
|