@barefootjs/mojolicious 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.
@@ -1,5 +1,5 @@
1
1
  package BarefootJS::Backend::Mojo;
2
- our $VERSION = "0.30.5";
2
+ our $VERSION = "0.30.6";
3
3
  use Mojo::Base -base, -signatures;
4
4
 
5
5
  use Mojo::ByteStream qw(b);
@@ -1,5 +1,5 @@
1
1
  package Mojolicious::Plugin::BarefootJS::DevReload;
2
- our $VERSION = "0.30.5";
2
+ our $VERSION = "0.30.6";
3
3
  use Mojo::Base 'Mojolicious::Plugin', -signatures;
4
4
 
5
5
  =head1 NAME
@@ -1,5 +1,5 @@
1
1
  package Mojolicious::Plugin::BarefootJS;
2
- our $VERSION = "0.30.5";
2
+ our $VERSION = "0.30.6";
3
3
  use Mojo::Base 'Mojolicious::Plugin', -signatures;
4
4
 
5
5
  use Mojo::File qw(path);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@barefootjs/mojolicious",
3
- "version": "0.30.6",
3
+ "version": "0.31.0",
4
4
  "description": "Mojolicious EP template adapter for BarefootJS - generates .html.ep files from IR",
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
- "./build": {
21
- "types": "./dist/build.d.ts",
22
- "import": "./dist/build.js"
20
+ "./vite": {
21
+ "types": "./dist/vite.d.ts",
22
+ "import": "./dist/vite.js"
23
23
  }
24
24
  },
25
25
  "files": [
@@ -29,7 +29,7 @@
29
29
  ],
30
30
  "scripts": {
31
31
  "build": "bun run build:js && bun run build:types",
32
- "build:js": "bun build ./src/index.ts ./src/adapter/index.ts ./src/build.ts --root ./src --outdir ./dist --format esm --external @barefootjs/jsx --external @barefootjs/shared --external typescript",
32
+ "build:js": "bun build ./src/index.ts ./src/adapter/index.ts --root ./src --outdir ./dist --format esm --external @barefootjs/jsx --external @barefootjs/shared --external typescript --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",
33
33
  "build:types": "tsgo --emitDeclarationOnly --outDir ./dist",
34
34
  "test": "bun test",
35
35
  "clean": "rm -rf dist",
@@ -52,14 +52,27 @@
52
52
  "directory": "packages/adapter-mojolicious"
53
53
  },
54
54
  "dependencies": {
55
- "@barefootjs/shared": "0.30.6"
55
+ "@barefootjs/shared": "0.31.0"
56
56
  },
57
57
  "peerDependencies": {
58
58
  "@barefootjs/jsx": ">=0.2.0",
59
- "typescript": "^5.0.0"
59
+ "typescript": "^5.0.0",
60
+ "@barefootjs/vite": ">=0.2.0",
61
+ "vite": "^6.0.0"
62
+ },
63
+ "peerDependenciesMeta": {
64
+ "@barefootjs/vite": {
65
+ "optional": true
66
+ },
67
+ "vite": {
68
+ "optional": true
69
+ }
60
70
  },
61
71
  "devDependencies": {
62
72
  "@barefootjs/adapter-tests": "0.1.0",
63
- "@barefootjs/jsx": "0.30.6"
73
+ "@barefootjs/jsx": "0.31.0",
74
+ "@barefootjs/vite": "0.31.0",
75
+ "@barefootjs/client": "0.31.0",
76
+ "vite": "^6.0.0"
64
77
  }
65
78
  }
@@ -2133,3 +2133,146 @@ export function Parent() {
2133
2133
  expect(template).not.toContain('$bf_prop_data')
2134
2134
  })
2135
2135
  })
2136
+
2137
+ describe('MojoAdapter - scriptAssets (Vite late-binding, PR1)', () => {
2138
+ const CLIENT_COMPONENT = `
2139
+ 'use client'
2140
+ import { createSignal } from '@barefootjs/client'
2141
+ export function Counter() {
2142
+ const [count, setCount] = createSignal(0)
2143
+ return <button onClick={() => setCount(count() + 1)}>{count()}</button>
2144
+ }
2145
+ `
2146
+
2147
+ test('emits one register_script per URL, in order, when scriptAssets is set', () => {
2148
+ const ir = compileToIR(CLIENT_COMPONENT)
2149
+ const { template } = new MojoAdapter().generate(ir, {
2150
+ scriptAssets: ['/assets/runtime-abc123.js', '/assets/counter-def456.js'],
2151
+ })
2152
+ const runtimeIdx = template.indexOf("bf->register_script('/assets/runtime-abc123.js')")
2153
+ const compIdx = template.indexOf("bf->register_script('/assets/counter-def456.js')")
2154
+ expect(runtimeIdx).toBeGreaterThanOrEqual(0)
2155
+ expect(compIdx).toBeGreaterThanOrEqual(0)
2156
+ expect(runtimeIdx).toBeLessThan(compIdx)
2157
+ expect(template).not.toContain('/static/components/barefoot.js')
2158
+ expect(template).not.toContain('Counter.client.js')
2159
+ })
2160
+
2161
+ test('emits a single registration for a single-element scriptAssets array', () => {
2162
+ const ir = compileToIR(CLIENT_COMPONENT)
2163
+ const { template } = new MojoAdapter().generate(ir, {
2164
+ scriptAssets: ['/assets/only-one.js'],
2165
+ })
2166
+ expect(template).toContain("bf->register_script('/assets/only-one.js')")
2167
+ expect(template.match(/register_script/g)?.length).toBe(1)
2168
+ })
2169
+
2170
+ test('an empty scriptAssets array emits no script registrations', () => {
2171
+ const ir = compileToIR(CLIENT_COMPONENT)
2172
+ const { template } = new MojoAdapter().generate(ir, { scriptAssets: [] })
2173
+ expect(template).not.toContain('register_script')
2174
+ })
2175
+
2176
+ test('skipScriptRegistration still wins when scriptAssets is also set', () => {
2177
+ const ir = compileToIR(CLIENT_COMPONENT)
2178
+ const { template } = new MojoAdapter().generate(ir, {
2179
+ skipScriptRegistration: true,
2180
+ scriptAssets: ['/assets/should-not-appear.js'],
2181
+ })
2182
+ expect(template).not.toContain('register_script')
2183
+ })
2184
+
2185
+ test('absent scriptAssets falls back to adapter-computed script paths', () => {
2186
+ const ir = compileToIR(CLIENT_COMPONENT)
2187
+ const computed = new MojoAdapter().generate(ir).template
2188
+ const explicitUndefined = new MojoAdapter().generate(ir, { scriptAssets: undefined }).template
2189
+ expect(computed).toContain("bf->register_script('/static/components/barefoot.js')")
2190
+ expect(computed).toContain("bf->register_script('/static/components/Counter.client.js')")
2191
+ expect(explicitUndefined).toBe(computed)
2192
+ })
2193
+ })
2194
+
2195
+ describe('MojoAdapter - preloadAssets', () => {
2196
+ const CLIENT_COMPONENT = `
2197
+ 'use client'
2198
+ import { createSignal } from '@barefootjs/client'
2199
+ export function Counter() {
2200
+ const [count, setCount] = createSignal(0)
2201
+ return <button onClick={() => setCount(count() + 1)}>{count()}</button>
2202
+ }
2203
+ `
2204
+
2205
+ test('non-empty preloadAssets + non-empty scriptAssets: preload registrations emitted, in order, before script registrations', () => {
2206
+ const ir = compileToIR(CLIENT_COMPONENT)
2207
+ const { template } = new MojoAdapter().generate(ir, {
2208
+ scriptAssets: ['/assets/runtime-abc123.js', '/assets/counter-def456.js'],
2209
+ preloadAssets: ['/assets/index-pre1.js', '/assets/shared-pre2.js'],
2210
+ })
2211
+ const pre1Idx = template.indexOf("% bf->register_preload('/assets/index-pre1.js');")
2212
+ const pre2Idx = template.indexOf("% bf->register_preload('/assets/shared-pre2.js');")
2213
+ const script1Idx = template.indexOf("% bf->register_script('/assets/runtime-abc123.js');")
2214
+ const script2Idx = template.indexOf("% bf->register_script('/assets/counter-def456.js');")
2215
+ expect(pre1Idx).toBeGreaterThanOrEqual(0)
2216
+ expect(pre2Idx).toBeGreaterThan(pre1Idx)
2217
+ expect(script1Idx).toBeGreaterThan(pre2Idx)
2218
+ expect(script2Idx).toBeGreaterThan(script1Idx)
2219
+ })
2220
+
2221
+ test('preloadAssets: [] emits no preload registration', () => {
2222
+ const ir = compileToIR(CLIENT_COMPONENT)
2223
+ const { template } = new MojoAdapter().generate(ir, {
2224
+ scriptAssets: ['/assets/runtime-abc123.js'],
2225
+ preloadAssets: [],
2226
+ })
2227
+ expect(template).not.toContain('register_preload')
2228
+ expect(template).toContain("bf->register_script('/assets/runtime-abc123.js')")
2229
+ })
2230
+
2231
+ test('preloadAssets: undefined emits no preload registration', () => {
2232
+ const ir = compileToIR(CLIENT_COMPONENT)
2233
+ const { template } = new MojoAdapter().generate(ir, {
2234
+ scriptAssets: ['/assets/runtime-abc123.js'],
2235
+ preloadAssets: undefined,
2236
+ })
2237
+ expect(template).not.toContain('register_preload')
2238
+ expect(template).toContain("bf->register_script('/assets/runtime-abc123.js')")
2239
+ })
2240
+
2241
+ test('preloadAssets non-empty but scriptAssets: [] emits no preload registration (preloads are only meaningful alongside a real script)', () => {
2242
+ const ir = compileToIR(CLIENT_COMPONENT)
2243
+ const { template } = new MojoAdapter().generate(ir, {
2244
+ scriptAssets: [],
2245
+ preloadAssets: ['/assets/index-pre1.js'],
2246
+ })
2247
+ expect(template).not.toContain('register_preload')
2248
+ expect(template).not.toContain('register_script')
2249
+ })
2250
+
2251
+ test('skipScriptRegistration: true suppresses both preloads and scripts', () => {
2252
+ const ir = compileToIR(CLIENT_COMPONENT)
2253
+ const { template } = new MojoAdapter().generate(ir, {
2254
+ skipScriptRegistration: true,
2255
+ scriptAssets: ['/assets/runtime-abc123.js'],
2256
+ preloadAssets: ['/assets/index-pre1.js'],
2257
+ })
2258
+ expect(template).not.toContain('register_preload')
2259
+ expect(template).not.toContain('register_script')
2260
+ })
2261
+
2262
+ // Regression guard: a previous attempt emitted a literal
2263
+ // `<link rel="modulepreload">` tag directly into the component template,
2264
+ // which injected a rendered DOM node before the component's root and
2265
+ // broke hydration across all eight integrations (blade, erb,
2266
+ // go-template, jinja, mojolicious, rust, twig, xslate). Preload hints
2267
+ // must ONLY ever be emitted as no-output register statements (here,
2268
+ // `% bf->register_preload('...');`) that the adapter's runtime later
2269
+ // renders itself — never as literal markup baked into the template.
2270
+ test('never emits a literal <link tag into the template', () => {
2271
+ const ir = compileToIR(CLIENT_COMPONENT)
2272
+ const { template } = new MojoAdapter().generate(ir, {
2273
+ scriptAssets: ['/assets/runtime-abc123.js'],
2274
+ preloadAssets: ['/assets/index-pre1.js'],
2275
+ })
2276
+ expect(template).not.toContain('<link')
2277
+ })
2278
+ })
@@ -12,9 +12,9 @@
12
12
  // This is the in-repo equivalent of the issue's repro: `bf add toast` →
13
13
  // render a <Toast>-using component on mojo → expect 200 with toast markup.
14
14
  // It boots a real Mojolicious app with the production plugin against
15
- // compiler-produced templates plus a manifest in the shape `bf build` now
15
+ // compiler-produced templates plus a manifest in the shape `@barefootjs/vite`
16
16
  // emits (per-component rows under `components` — pinned on the emitter side
17
- // by packages/cli/src/__tests__/build-manifest-components.test.ts).
17
+ // by packages/vite/src/__tests__/component-manifest.test.ts).
18
18
  //
19
19
  // Runs only when `perl` with Mojolicious is installed (same skip policy as
20
20
  // stock-route.test.ts, which this file mirrors).
@@ -176,7 +176,7 @@ describe.skipIf(!PERL_AVAILABLE)('Mojo multi-component registry modules (#2132)'
176
176
  if (!probeTemplate) throw new Error('probe compile produced no template')
177
177
  writeFileSync(path.join(appDir, 'dist/templates/ToastProbe.html.ep'), probeTemplate.content)
178
178
 
179
- // Same entry shape `bf build` writes to dist/templates/manifest.json
179
+ // Same entry shape `@barefootjs/vite` writes to dist/templates/manifest.json
180
180
  // (see build-manifest-components.test.ts for the emitter pin).
181
181
  writeFileSync(
182
182
  path.join(appDir, 'dist/templates/manifest.json'),
@@ -4,8 +4,8 @@
4
4
  // cross-adapter contract defined in `create-barefootjs` and the
5
5
  // Mojo-specific wiring:
6
6
  //
7
- // - `barefoot.config.ts` targets `@barefootjs/mojolicious/build` and
8
- // uses `clientJsBasePath: '/static/components/'`.
7
+ // - `vite.config.ts` targets `@barefootjs/mojolicious/vite` and
8
+ // uses `base: '/static/components/'`.
9
9
  // - `app.pl` forwards `/static/*` URLs to the on-disk static paths
10
10
  // (Mojolicious's built-in dispatcher does not honour URL prefixes,
11
11
  // so the explicit routes are load-bearing — without them every
@@ -116,7 +116,7 @@ describe.skipIf(!INTEGRATION)(
116
116
 
117
117
  describe('app.pl serves static assets', () => {
118
118
  // Mojolicious's built-in static dispatcher does not honour URL
119
- // prefixes. The scaffold's `barefoot.config.ts` and layout `<link>`s
119
+ // prefixes. The scaffold's `vite.config.ts` and layout `<link>`s
120
120
  // all reference `/static/*` URLs, so `app.pl` needs explicit
121
121
  // forwarding routes — without them every stylesheet and client bundle
122
122
  // 404s in the browser even though the SSR HTML rendered correctly.
@@ -176,10 +176,10 @@ describe.skipIf(!INTEGRATION)(
176
176
  expect(app).not.toContain("app->home->child('dist/templates/manifest.json')")
177
177
  })
178
178
 
179
- test('barefoot.config.ts targets the mojolicious adapter', () => {
180
- const cfg = readFileSync(path.join(projectDir, 'barefoot.config.ts'), 'utf-8')
181
- expect(cfg).toContain("from '@barefootjs/mojolicious/build'")
182
- expect(cfg).toContain("clientJsBasePath: '/static/components/'")
179
+ test('vite.config.ts targets the mojolicious adapter', () => {
180
+ const cfg = readFileSync(path.join(projectDir, 'vite.config.ts'), 'utf-8')
181
+ expect(cfg).toContain("from '@barefootjs/mojolicious/vite'")
182
+ expect(cfg).toContain("base: '/static/components/'")
183
183
  })
184
184
 
185
185
  test('layout stylesheets point at /static/*.css', () => {
@@ -1,7 +1,7 @@
1
1
  // Stock-route smoke test for the Mojo scaffold contract (#2126).
2
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
3
+ // The scaffold's happy path is: `npm run dev` (which starts `vite dev`
4
+ // and morbo *concurrently*), then open `/`. That page renders a
5
5
  // component whose EP template reads props/signals as bare scalars
6
6
  // (`% my $count = ($initial // 0);`), and Mojo templates compile under
7
7
  // `use strict` — so every one of those scalars must be declared by the
@@ -114,8 +114,8 @@ __DATA__
114
114
  // manifest-at-boot — the manifest exists before the app loads
115
115
  // (server restarted after a completed build).
116
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).
117
+ // afterwards (the concurrent `vite dev` +
118
+ // morbo dev race on a fresh scaffold).
119
119
  const SMOKE_PL = `use Mojo::Base -strict;
120
120
  use Test::More;
121
121
  use Test::Mojo;
@@ -161,7 +161,8 @@ describe.skipIf(!PERL_AVAILABLE)('Mojo scaffold stock route (#2126)', () => {
161
161
  if (!template || !ssrDefaults) throw new Error('compileJSX produced no template/ssrDefaults')
162
162
 
163
163
  writeFileSync(path.join(appDir, 'dist/templates/Counter.html.ep'), template.content)
164
- // Same entry shape `bf build` writes to dist/templates/manifest.json.
164
+ // Same entry shape `@barefootjs/vite` writes to
165
+ // dist/templates/manifest.json.
165
166
  writeFileSync(
166
167
  path.join(appDir, 'manifest.staged.json'),
167
168
  JSON.stringify({
@@ -0,0 +1,126 @@
1
+ /**
2
+ * Coverage of `@barefootjs/mojolicious/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) — `MojoAdapter.generate()` never
16
+ * 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/mojolicious/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 .html.ep template with scriptAssets baked in, same as core alone would do', async () => {
39
+ const outDir = await mkdtemp(join(tmpdir(), 'barefoot-mojolicious-vite-dist-'))
40
+ const templatesDir = await mkdtemp(join(tmpdir(), 'barefoot-mojolicious-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.html.ep'), '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-mojolicious-vite-dist-assets-'))
65
+ const templatesDir = await mkdtemp(join(tmpdir(), 'barefoot-mojolicious-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-mojolicious-vite-dist-assets-missing-'))
103
+ const templatesDir = await mkdtemp(join(tmpdir(), 'barefoot-mojolicious-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
+ })
@@ -77,19 +77,37 @@ const PREDICATE_METHODS: ReadonlySet<string> = new Set([
77
77
  ])
78
78
 
79
79
  export class MojoFilterEmitter implements ParsedExprEmitter {
80
+ // Plain field declarations + assignment, NOT TS constructor-parameter-
81
+ // property shorthand: Vite's `bundleConfigFile` externalizes any bare
82
+ // (non-relative) import when loading `vite.config.ts` (see
83
+ // `@barefootjs/mojolicious/vite`'s docstring), so this file can be loaded
84
+ // directly by Node's OWN native TypeScript type-stripping (enabled by
85
+ // default since Node 22.18/23.6) rather than esbuild — and Node's
86
+ // strip-only mode does not support parameter properties (`SyntaxError
87
+ // [ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX]`), only plain type annotations.
88
+ private readonly param: string
89
+ private readonly localVarMap: Map<string, string>
90
+ // Reports whether a getter/prop name is string-typed, so `===`/`!==`
91
+ // against it lowers to `eq`/`ne` (#1672). Defaults to "never" for callers
92
+ // that don't thread it through.
93
+ private readonly isStringName: (n: string) => boolean
94
+ // Records a BF101 for nested callback shapes this emitter can only
95
+ // degrade — `find*` and the non-predicate methods (#2038). Optional so
96
+ // emitter construction stays possible without an adapter; a missing hook
97
+ // keeps the old silent-degrade emit.
98
+ private readonly onUnsupported?: (message: string, reason?: string) => void
99
+
80
100
  constructor(
81
- private readonly param: string,
82
- private readonly localVarMap: Map<string, string>,
83
- // Reports whether a getter/prop name is string-typed, so `===`/`!==`
84
- // against it lowers to `eq`/`ne` (#1672). Defaults to "never" for callers
85
- // that don't thread it through.
86
- private readonly isStringName: (n: string) => boolean = () => false,
87
- // Records a BF101 for nested callback shapes this emitter can only
88
- // degrade — `find*` and the non-predicate methods (#2038). Optional so
89
- // emitter construction stays possible without an adapter; a missing hook
90
- // keeps the old silent-degrade emit.
91
- private readonly onUnsupported?: (message: string, reason?: string) => void,
92
- ) {}
101
+ param: string,
102
+ localVarMap: Map<string, string>,
103
+ isStringName: (n: string) => boolean = () => false,
104
+ onUnsupported?: (message: string, reason?: string) => void,
105
+ ) {
106
+ this.param = param
107
+ this.localVarMap = localVarMap
108
+ this.isStringName = isStringName
109
+ this.onUnsupported = onUnsupported
110
+ }
93
111
 
94
112
  identifier(name: string): string {
95
113
  if (name === this.param) return `$${this.param}`
@@ -296,7 +314,13 @@ export class MojoFilterEmitter implements ParsedExprEmitter {
296
314
  * shapes the AST can't classify still emit something coherent.
297
315
  */
298
316
  export class MojoTopLevelEmitter implements ParsedExprEmitter {
299
- constructor(private readonly ctx: MojoEmitContext) {}
317
+ // Plain field + assignment, not a parameter property — see
318
+ // `MojoFilterEmitter`'s constructor comment above for why.
319
+ private readonly ctx: MojoEmitContext
320
+
321
+ constructor(ctx: MojoEmitContext) {
322
+ this.ctx = ctx
323
+ }
300
324
 
301
325
  identifier(name: string): string {
302
326
  // `undefined` / `null` nested inside a larger expression tree
@@ -144,9 +144,6 @@ export class MojoAdapter extends BaseAdapter implements IRNodeEmitter<MojoRender
144
144
  name = 'mojolicious'
145
145
  extension = '.html.ep'
146
146
  templatesPerComponent = true
147
- // Template-string target with no component layer: `bf build` emits a static
148
- // `barefoot-importmap.html` to `%= include` into the page <head> (#1644).
149
- importMapInjection = 'html-snippet' as const
150
147
 
151
148
  /**
152
149
  * Identifier-path callees the Mojo runtime can render in template
@@ -323,7 +320,7 @@ export class MojoAdapter extends BaseAdapter implements IRNodeEmitter<MojoRender
323
320
  // Generate script registration
324
321
  const scriptReg = options?.skipScriptRegistration
325
322
  ? ''
326
- : this.generateScriptRegistrations(ir, options?.scriptBaseName)
323
+ : this.generateScriptRegistrations(ir, options?.scriptBaseName, options?.scriptAssets, options?.preloadAssets)
327
324
 
328
325
  // SSR context consumers (`const x = useContext(Ctx)`): seed each local
329
326
  // from the active provider value (or the `createContext` default) so the
@@ -467,7 +464,32 @@ export class MojoAdapter extends BaseAdapter implements IRNodeEmitter<MojoRender
467
464
  // Script Registration
468
465
  // ===========================================================================
469
466
 
470
- private generateScriptRegistrations(ir: ComponentIR, scriptBaseName?: string): string {
467
+ private generateScriptRegistrations(
468
+ ir: ComponentIR,
469
+ scriptBaseName?: string,
470
+ scriptAssets?: string[],
471
+ preloadAssets?: string[],
472
+ ): string {
473
+ // `scriptAssets`, when present (including `[]`), fully supersedes the
474
+ // adapter-computed `barefootJsPath` / `clientJsBasePath` pair — see
475
+ // `AdapterGenerateOptions.scriptAssets`. The caller (e.g. the Vite
476
+ // plugin) has already decided the exact ordered URL list, including
477
+ // whether any script is needed at all.
478
+ if (scriptAssets) {
479
+ if (scriptAssets.length === 0) return ''
480
+ // `preloadAssets` is only meaningful alongside a non-empty
481
+ // `scriptAssets` (see `AdapterGenerateOptions.preloadAssets`), and
482
+ // every preload registration is emitted BEFORE every script
483
+ // registration — a hint that arrives after the script it describes
484
+ // is useless. `% ...;` is a no-output EP statement line, same as
485
+ // `register_script` below — `register_preload`'s return value is
486
+ // never printed.
487
+ const lines = (preloadAssets ?? []).map((url) => `% bf->register_preload('${url}');`)
488
+ lines.push(...scriptAssets.map((url) => `% bf->register_script('${url}');`))
489
+ lines.push('')
490
+ return lines.join('\n')
491
+ }
492
+
471
493
  const hasInteractivity = hasClientInteractivity(ir)
472
494
  if (!hasInteractivity) return ''
473
495