Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,8 @@ project uses [Semantic Versioning](https://semver.org/).

### Added

- `@bquery/unplugin` — a bundler plugin for Vite, Rollup, webpack, Rspack and esbuild ([bQuery/bQuery#224](https://github.com/bQuery/bQuery/issues/224), developed in [bQuery/bQuery#278](https://github.com/bQuery/bQuery/pull/278)). It compiles `*.view.html` imports with the view compiler (HMR in Vite), generates the file-route manifest as `virtual:bquery/routes`, extracts i18n messages into the catalogs in watch mode (`virtual:bquery/messages` hot-reloads them), and in Vite dev adds a CSP nonce and an SSR middleware for a `render(request, ctx)` server entry. Requires `@bquery/bquery` 1.17.2 or later.
- `@bquery/unplugin` — a bundler plugin for Vite, Rollup, webpack, Rspack and esbuild ([bQuery/bQuery#224](https://github.com/bQuery/bQuery/issues/224), developed in [bQuery/bQuery#278](https://github.com/bQuery/bQuery/pull/278)). It compiles `*.view.html` imports with the view compiler (HMR in Vite), generates the file-route manifest as `virtual:bquery/routes`, extracts i18n messages into the catalogs in watch mode (`virtual:bquery/messages` hot-reloads them), and in Vite dev adds a CSP nonce and an SSR middleware for a `render(request, ctx)` server entry. Requires `@bquery/bquery` 1.18.0 or later.

### Changed

- Requires `@bquery/bquery` 1.18.0 or later (peer and dev dependency `^1.18.0`, was `^1.17.2`). `bq-on` handlers with several `;`-separated statements (`$event.preventDefault(); add()`) now always compile: the view compiler puts them in a separate statements map that the emitted module passes to `registerCompiledExpressions(expressions, statements)`, instead of leaving them to the runtime evaluator with a warning as 1.17.2 did. bQuery 1.18 also keeps the `<!DOCTYPE html>` of full-page templates in its SSR renderers, so pages served by the SSR dev middleware no longer fall into quirks mode.
24 changes: 19 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ bun add -d @bquery/unplugin
pnpm add -D @bquery/unplugin
```

`@bquery/bquery` (`^1.17.2`) is a peer dependency. Install the bundler you use;
`@bquery/bquery` (`^1.18.0`) is a peer dependency. Install the bundler you use;
each one (`vite`, `rollup`, `webpack`, `@rspack/core`, `esbuild`) is an optional
peer dependency.

Expand Down Expand Up @@ -153,6 +153,21 @@ keeps its status and headers, except `content-encoding`, `content-length` and
hop-by-hop headers: its body is read decoded (as `fetch()` returns it), so
those no longer describe what the middleware sends. Every `Set-Cookie` is kept.

bQuery's SSR renderers drop every `<script>` from the template, including the
`<script type="module" src="/src/main.ts">` client entry of `index.html`. Add
the entry through the SSR context instead; it is injected once and gets the
CSP nonce:

```ts
import { createSSRContext, renderToResponse } from '@bquery/bquery/ssr';

export async function render(request: Request, ctx) {
const ssr = createSSRContext({ request, nonce: ctx.nonce });
ssr.head.add({ script: [{ src: '/src/main.ts', module: true }] });
return renderToResponse(ctx.template, { title: 'Home' }, { context: ssr });
}
```

## Options

```ts
Expand All @@ -174,10 +189,9 @@ with the render contract and the limitations.
## Compatibility

- Node.js 24+ or Bun 1.4+.
- `@bquery/bquery` 1.17.2 or later. `bq-on` handlers with several
`;`-separated statements (`$event.preventDefault(); add()`) are compiled only
by bQuery releases after 1.17.2, which also accept them at runtime. With
1.17.2 the plugin leaves them to the runtime evaluator and warns.
- `@bquery/bquery` 1.18.0 or later. `bq-on` handlers with several
`;`-separated statements (`$event.preventDefault(); add()`) are compiled
like single expressions.
- `csp` and `ssr` need the Vite dev server; other bundlers ignore them with
one warning per build. webpack and Rspack use the same code paths through
unplugin's adapters but are not covered by this package's test suite.
Expand Down
6 changes: 3 additions & 3 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@
"unplugin": "^3.4.0"
},
"peerDependencies": {
"@bquery/bquery": "^1.17.2",
"@bquery/bquery": "^1.18.0",
"@rspack/core": ">=1.0.0",
"esbuild": ">=0.18.0",
"rollup": ">=3.0.0",
Expand All @@ -122,7 +122,7 @@
}
},
"devDependencies": {
"@bquery/bquery": "^1.17.2",
"@bquery/bquery": "^1.18.0",
"@types/bun": "^1.4.3",
"@typescript-eslint/eslint-plugin": "^8.72.0",
"@typescript-eslint/parser": "^8.72.0",
Expand Down
50 changes: 40 additions & 10 deletions test/unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,17 +147,47 @@ describe('generators', () => {
expect(stats).toEqual({ total: 1, compiled: 1, skipped: [] });
});

it('handles bq-on statement lists with any supported @bquery/bquery', async () => {
// `a(); b()` in bq-on compiles with bQuery releases after 1.17.2 (which
// register it in a separate statements map); 1.17.2 skips it (runtime
// fallback + warning). Either way the module is valid.
it('compiles bq-on statement lists into the statements map', async () => {
const handler = '$event.preventDefault(); add()';
const { code, stats } = buildViewModule(`<form bq-on:submit="${handler}"></form>`, {}, false);
await transform(code, { format: 'esm', loader: 'js' });
const skipped = stats.skipped.some((entry) => entry.expression === handler);
const registration = code.slice(0, code.indexOf('const template'));
expect(registration.includes(JSON.stringify(handler))).toBe(!skipped);
expect(stats.total).toBe(1);
const { code, stats } = buildViewModule(
`<form bq-on:submit="${handler}"><p bq-text="count"></p></form>`,
{},
false
);
expect(stats).toEqual({ total: 2, compiled: 2, skipped: [] });

// Run the emitted module against a stub registry to see what it registers.
const calls: Array<Array<Record<string, (ctx: unknown) => unknown>>> = [];
const { code: cjs } = await transform(code, { format: 'cjs', loader: 'js' });
const run = new Function('require', 'module', 'exports', cjs);
const module = { exports: {} as Record<string, unknown> };
run(
() => ({
registerCompiledExpressions: (...args: (typeof calls)[number]) => calls.push(args),
}),
module,
module.exports
);
expect(calls).toHaveLength(1);
const [expressions, statements] = calls[0]!;
expect(Object.keys(expressions!)).toEqual(['count']);
expect(Object.keys(statements!)).toEqual([handler]);

// Both statements run in order; the last one's value is the handler's value.
const order: string[] = [];
const ctx = {
$event: { preventDefault: () => order.push('preventDefault') },
add: () => (order.push('add'), 'added'),
};
expect(statements![handler]!(ctx)).toBe('added');
expect(order).toEqual(['preventDefault', 'add']);
expect(module.exports.default).toContain(`bq-on:submit="${handler}"`);
});

it('leaves a statement list outside bq-on to the runtime evaluator', () => {
const { stats } = buildViewModule('<p bq-show="a(); b()"></p>', {}, false);
expect(stats.compiled).toBe(0);
expect(stats.skipped.map((entry) => entry.expression)).toEqual(['a(); b()']);
});

it('compiles only bare view imports or the view marker', () => {
Expand Down
2 changes: 2 additions & 0 deletions test/vite.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,8 @@ describe('vite dev server', () => {
const res = await get('/bquery');
expect(res.status).toBe(200);
const html = await res.text();
// A full-page template keeps its doctype (bQuery 1.18+), so no quirks mode.
expect(html).toMatch(/^<!doctype html>/i);
expect(html).toMatch(/<h1[^>]*>Rendered by bQuery<\/h1>/);
expect(html).toContain('/@vite/client');
});
Expand Down
Loading