` node for each injection, which may affect the structure of your rendered output. Unlike [Brisa](#brisa-experimental), where this issue is avoided, the extra `
` nodes can lead to unexpected layout changes or styling issues.
259 |
260 | ### Kitajs/html
261 |
262 | Configuration example:
263 |
264 | ```tsx
265 | import { createElement } from "@kitajs/html";
266 | import prerenderMacroPlugin, { type PrerenderConfig } from "prerender-macro";
267 |
268 | export const prerenderConfig = {
269 | render: createElement,
270 | } satisfies PrerenderConfig;
271 |
272 | export const plugin = prerenderMacroPlugin({
273 | prerenderConfigPath: import.meta.url,
274 | });
275 | ```
276 |
277 | > [!NOTE]
278 | >
279 | > Kitajs/html elements can be seamlessly coerced with Bun's AST and everything can be done AOT without having to use a `postRender`.
280 |
281 | > [!NOTE]
282 | >
283 | > Kitajs/html does not add extra nodes in the HTML, so it is a prerender of the real component, without modifying its structure.
284 |
285 | ### Add your framework example
286 |
287 | This project is open-source and totally open for you to contribute by adding the JSX framework you use, I'm sure it can help a lot of people.
288 |
289 | To add your framework you have to:
290 |
291 | - Fork & clone
292 | - Create a folder inside [`tests`](/tests/) with your framework that is a copy of some other framework. The same for [`examples`](/examples/).
293 | - Make the changes and adapt the example and tests to your framework
294 | - Update the package.json scripts to add your framework
295 | - Update the [`README.md`](/README.md) adding the documentation of your framework.
296 | - Open a PR with the changes.
297 |
298 | ## Contributing
299 |
300 | See [Contributing Guide](CONTRIBUTING.md) and please follow our [Code of Conduct](CODE_OF_CONDUCT.md).
301 |
302 | ## License
303 |
304 | [MIT](LICENSE)
305 |
--------------------------------------------------------------------------------
/bun.lockb:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aralroca/prerender-macro/853f58c7b5a174a4bcd2ecd05f6c7d074ccc856f/bun.lockb
--------------------------------------------------------------------------------
/examples/brisa/README.md:
--------------------------------------------------------------------------------
1 | # `prerender-macro` Brisa Example
2 |
3 | This is an example with Brisa SSR without hotreloading and with a build process.
4 |
5 | To test it:
6 |
7 | - Clone the repo: `git clone git@github.com:aralroca/prerender-macro.git`
8 | - Install dependencies: `cd prerender-macro && bun install`
9 | - Run demo: `bun run demo:brisa`
10 | - Open http://localhost:1234 to see the result
11 | - Look at `examples/brisa/dist/index.js` to verify how the static parts have been converted to HTML in string.
12 |
13 | The static component is translated to html in string in build-time:
14 |
15 | ```tsx
16 | "Static Component \uD83E\uDD76 Random number = 0.41381527597071954";
17 | ```
18 |
--------------------------------------------------------------------------------
/examples/brisa/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "brisa-example",
3 | "private": true,
4 | "module": "build.tsx",
5 | "type": "module",
6 | "scripts": {
7 | "start": "bun run src/build.ts && bun run dist/index.js"
8 | },
9 | "devDependencies": {
10 | "@types/bun": "latest"
11 | },
12 | "peerDependencies": {
13 | "typescript": "5.4.3"
14 | },
15 | "dependencies": {
16 | "prerender-macro": "workspace:*",
17 | "brisa": "latest"
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/examples/brisa/src/build.ts:
--------------------------------------------------------------------------------
1 | import { join } from "node:path";
2 | import prerenderMacro from "prerender-macro";
3 |
4 | const { success, logs } = await Bun.build({
5 | entrypoints: [join(import.meta.dir, "index.tsx")],
6 | outdir: join(import.meta.dir, "..", "dist"),
7 | target: "bun",
8 | plugins: [
9 | prerenderMacro({
10 | prerenderConfigPath: join(import.meta.dir, "prerender.tsx"),
11 | }),
12 | ],
13 | });
14 |
15 | if (success) console.log("Build complete ✅");
16 | else console.error("Build failed ❌", logs);
17 |
--------------------------------------------------------------------------------
/examples/brisa/src/components/dynamic-component.tsx:
--------------------------------------------------------------------------------
1 | export default function DynamicComponent({ name }: { name: string }) {
2 | return (
3 |
4 | {name} Component 🔥 Random number = {Math.random()}
5 |
6 | );
7 | }
8 |
--------------------------------------------------------------------------------
/examples/brisa/src/components/static-component.tsx:
--------------------------------------------------------------------------------
1 | export default function StaticComponent({ name }: { name: string }) {
2 | return (
3 |
4 | {name} Component 🥶 Random number = {Math.random()}
5 |
6 | );
7 | }
8 |
--------------------------------------------------------------------------------
/examples/brisa/src/index.tsx:
--------------------------------------------------------------------------------
1 | import DynamicComponent from "./components/dynamic-component";
2 | import StaticComponent from "./components/static-component" with { type: "prerender" };
3 | import { renderToReadableStream } from "brisa/server";
4 |
5 | Bun.serve({
6 | port: 1234,
7 | fetch: async (request: Request) => {
8 | return new Response(
9 | renderToReadableStream(
10 |
11 |
12 |
Prerender Macro | Brisa example
13 |
14 |
15 |
16 |
17 |
18 |
Refresh
19 |
20 | ,
21 | { request },
22 | ),
23 | { headers: new Headers({ "Content-Type": "text/html" }) },
24 | );
25 | },
26 | });
27 |
28 | console.log("Server running at http://localhost:1234");
29 |
--------------------------------------------------------------------------------
/examples/brisa/src/prerender.tsx:
--------------------------------------------------------------------------------
1 | import { type PrerenderConfig } from "prerender-macro";
2 | import { dangerHTML } from "brisa";
3 | import { renderToString } from "brisa/server";
4 |
5 | export const prerenderConfig = {
6 | render: async (Component, props) =>
7 | dangerHTML(await renderToString(
)),
8 | } satisfies PrerenderConfig;
9 |
--------------------------------------------------------------------------------
/examples/brisa/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": ".",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "module": "esnext",
6 | "target": "esnext",
7 | "moduleResolution": "bundler",
8 | "moduleDetection": "force",
9 | "allowImportingTsExtensions": true,
10 | "noEmit": true,
11 | "composite": true,
12 | "strict": true,
13 | "downlevelIteration": true,
14 | "skipLibCheck": true,
15 | "jsx": "react-jsx",
16 | "jsxImportSource": "brisa",
17 | "allowSyntheticDefaultImports": true,
18 | "forceConsistentCasingInFileNames": true,
19 | "allowJs": true,
20 | "verbatimModuleSyntax": true,
21 | "noFallthroughCasesInSwitch": true,
22 | "types": ["brisa"],
23 | "paths": {
24 | "@/*": ["*"]
25 | }
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/examples/kitajs-html/README.md:
--------------------------------------------------------------------------------
1 | # `prerender-macro` Kitajs/html Example
2 |
3 | This is an example with kitajs-html SSR without hotreloading and with a build process.
4 |
5 | To test it:
6 |
7 | - Clone the repo: `git clone git@github.com:aralroca/prerender-macro.git`
8 | - Install dependencies: `cd prerender-macro && bun install`
9 | - Run demo: `bun run demo:kitajs-html`
10 | - Open http://localhost:1234 to see the result
11 | - Look at `examples/kitajs-html/dist/index.js` to verify how the static parts have been converted to HTML in string.
12 |
13 | The static component is translated to html in string in build-time:
14 |
15 | ```tsx
16 | "Static Component \uD83E\uDD76 Random number = 0.41381527597071954";
17 | ```
18 |
--------------------------------------------------------------------------------
/examples/kitajs-html/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "kitajs-example",
3 | "private": true,
4 | "module": "build.tsx",
5 | "type": "module",
6 | "scripts": {
7 | "start": "bun run src/build.ts && bun run dist/index.js"
8 | },
9 | "devDependencies": {
10 | "@types/bun": "latest"
11 | },
12 | "peerDependencies": {
13 | "typescript": "5.4.3"
14 | },
15 | "dependencies": {
16 | "prerender-macro": "workspace:*"
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/examples/kitajs-html/src/build.ts:
--------------------------------------------------------------------------------
1 | import { join } from "node:path";
2 | import prerenderMacro from "prerender-macro";
3 |
4 | const { success, logs } = await Bun.build({
5 | entrypoints: [join(import.meta.dir, "index.tsx")],
6 | outdir: join(import.meta.dir, "..", "dist"),
7 | target: "bun",
8 | plugins: [
9 | prerenderMacro({
10 | prerenderConfigPath: join(import.meta.dir, "prerender.tsx"),
11 | }),
12 | ],
13 | });
14 |
15 | if (success) console.log("Build complete ✅");
16 | else console.error("Build failed ❌", logs);
17 |
--------------------------------------------------------------------------------
/examples/kitajs-html/src/components/dynamic-component.tsx:
--------------------------------------------------------------------------------
1 | export default function DynamicComponent({ name }: { name: string }) {
2 | return (
3 |
4 | {name} Component 🔥 Random number = {Math.random()}
5 |
6 | );
7 | }
8 |
--------------------------------------------------------------------------------
/examples/kitajs-html/src/components/static-component.tsx:
--------------------------------------------------------------------------------
1 | export default function StaticComponent({ name }: { name: string }) {
2 | return (
3 |
4 | {name} Component 🥶 Random number = {Math.random()}
5 |
6 | );
7 | }
8 |
--------------------------------------------------------------------------------
/examples/kitajs-html/src/index.tsx:
--------------------------------------------------------------------------------
1 | import DynamicComponent from "./components/dynamic-component";
2 | import StaticComponent from "./components/static-component" with { type: "prerender" };
3 |
4 | Bun.serve({
5 | port: 1234,
6 | fetch: async (request: Request) => {
7 | const page = await (
8 |
9 |
10 |
Prerender Macro | Brisa example
11 |
12 |
13 |
14 |
15 |
16 |
Refresh
17 |
18 |
19 | );
20 |
21 | return new Response(page, {
22 | headers: new Headers({ "Content-Type": "text/html" }),
23 | });
24 | },
25 | });
26 |
27 | console.log("Server running at http://localhost:1234");
28 |
--------------------------------------------------------------------------------
/examples/kitajs-html/src/prerender.tsx:
--------------------------------------------------------------------------------
1 | import { createElement } from "@kitajs/html";
2 | import prerenderMacroPlugin, { type PrerenderConfig } from "prerender-macro";
3 |
4 | export const prerenderConfig = {
5 | render: createElement,
6 | } satisfies PrerenderConfig;
7 |
8 | export const plugin = prerenderMacroPlugin({
9 | prerenderConfigPath: import.meta.url,
10 | });
11 |
--------------------------------------------------------------------------------
/examples/kitajs-html/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": ".",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "module": "esnext",
6 | "target": "esnext",
7 | "moduleResolution": "bundler",
8 | "moduleDetection": "force",
9 | "jsx": "react-jsx",
10 | "jsxImportSource": "@kitajs/html",
11 | "plugins": [{ "name": "@kitajs/ts-html-plugin" }]
12 | },
13 | "include": ["src"]
14 | }
15 |
--------------------------------------------------------------------------------
/examples/preact/README.md:
--------------------------------------------------------------------------------
1 | # `prerender-macro` Preact Example
2 |
3 | This is an example with Preact SSR without hotreloading and with a build process.
4 |
5 | To test it:
6 |
7 | - Clone the repo: `git clone git@github.com:aralroca/prerender-macro.git`
8 | - Install dependencies: `cd prerender-macro && bun install`
9 | - Run demo: `bun run demo:preact`
10 | - Open http://localhost:1234 to see the result
11 | - Look at `examples/preact/dist/index.js` to verify how the static parts have been converted to HTML in string.
12 |
13 | The static component is translated to html in string in build-time:
14 |
15 | ```tsx
16 | "
Static Component \uD83E\uDD76 Random number = 0.41381527597071954
";
17 | ```
18 |
--------------------------------------------------------------------------------
/examples/preact/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "preact-example",
3 | "private": true,
4 | "module": "build.tsx",
5 | "type": "module",
6 | "scripts": {
7 | "start": "bun run src/build.ts && bun run dist/index.js"
8 | },
9 | "devDependencies": {
10 | "@types/bun": "latest"
11 | },
12 | "peerDependencies": {
13 | "typescript": "5.4.3"
14 | },
15 | "dependencies": {
16 | "prerender-macro": "workspace:*",
17 | "preact-render-to-string": "6.4.1"
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/examples/preact/src/build.ts:
--------------------------------------------------------------------------------
1 | import { join } from "node:path";
2 | import prerenderMacro from "prerender-macro";
3 |
4 | const { success, logs } = await Bun.build({
5 | entrypoints: [join(import.meta.dir, "index.tsx")],
6 | outdir: join(import.meta.dir, "..", "dist"),
7 | target: "bun",
8 | plugins: [
9 | prerenderMacro({
10 | prerenderConfigPath: join(import.meta.dir, "prerender.tsx"),
11 | }),
12 | ],
13 | });
14 |
15 | if (success) console.log("Build complete ✅");
16 | else console.error("Build failed ❌", logs);
17 |
--------------------------------------------------------------------------------
/examples/preact/src/components/dynamic-component.tsx:
--------------------------------------------------------------------------------
1 | export default function DynamicComponent({ name }: { name: string }) {
2 | return (
3 |
4 | {name} Component 🔥 Random number = {Math.random()}
5 |
6 | );
7 | }
8 |
--------------------------------------------------------------------------------
/examples/preact/src/components/static-component.tsx:
--------------------------------------------------------------------------------
1 | export default function StaticComponent({ name }: { name: string }) {
2 | return (
3 |
4 | {name} Component 🥶 Random number = {Math.random()}
5 |
6 | );
7 | }
8 |
--------------------------------------------------------------------------------
/examples/preact/src/index.tsx:
--------------------------------------------------------------------------------
1 | import DynamicComponent from "./components/dynamic-component";
2 | import StaticComponent from "./components/static-component" with { type: "prerender" };
3 | import { render } from "preact-render-to-string";
4 |
5 | Bun.serve({
6 | port: 1234,
7 | fetch: async () => {
8 | return new Response(
9 | render(
10 |
11 |
12 |
Prerender Macro | Preact example
13 |
14 |
15 |
16 |
17 |
18 |
Refresh
19 |
20 | ,
21 | ),
22 | { headers: new Headers({ "Content-Type": "text/html" }) },
23 | );
24 | },
25 | });
26 |
27 | console.log("Server running at http://localhost:1234");
28 |
--------------------------------------------------------------------------------
/examples/preact/src/prerender.tsx:
--------------------------------------------------------------------------------
1 | import { render } from "preact-render-to-string";
2 | import type { PrerenderConfig } from "prerender-macro";
3 |
4 | export const prerenderConfig = {
5 | render: async (Component, props) => {
6 | return (
7 |
) }}
9 | />
10 | );
11 | },
12 | } satisfies PrerenderConfig;
13 |
--------------------------------------------------------------------------------
/examples/preact/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "jsxImportSource": "preact",
4 | "jsx": "react-jsx",
5 | "baseUrl": ".",
6 | "lib": ["dom", "dom.iterable", "esnext"],
7 | "module": "esnext",
8 | "target": "esnext",
9 | "moduleResolution": "bundler",
10 | "moduleDetection": "force",
11 | "skipLibCheck": true,
12 | "paths": {
13 | "react": ["node_modules/preact/compat"],
14 | "react-dom": ["./node_modules/preact/compat/"]
15 | },
16 | "typeRoots": ["src/types", "./node_modules/@types"]
17 | },
18 | "resolve": {
19 | "extensions": [".js", ".jsx", ".ts", ".tsx", ".json", ".svg", ".css"]
20 | },
21 | "include": ["src"]
22 | }
23 |
--------------------------------------------------------------------------------
/examples/react/README.md:
--------------------------------------------------------------------------------
1 | # `prerender-macro` React Example
2 |
3 | This is an example with React SSR without hotreloading and with a build process.
4 |
5 | To test it:
6 |
7 | - Clone the repo: `git clone git@github.com:aralroca/prerender-macro.git`
8 | - Install dependencies: `cd prerender-macro && bun install`
9 | - Run demo: `bun run demo:react`
10 | - Open http://localhost:1234 to see the result
11 | - Look at `examples/react/dist/index.js` to verify how the static parts have been converted to HTML in string.
12 |
13 | The static component is translated to html in string in build-time:
14 |
15 | ```tsx
16 | "
Static Component \uD83E\uDD76 Random number = 0.41381527597071954
";
17 | ```
18 |
--------------------------------------------------------------------------------
/examples/react/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-example",
3 | "private": true,
4 | "module": "build.tsx",
5 | "type": "module",
6 | "scripts": {
7 | "start": "bun run src/build.ts && bun run dist/index.js"
8 | },
9 | "devDependencies": {
10 | "@types/bun": "latest"
11 | },
12 | "peerDependencies": {
13 | "typescript": "5.4.3"
14 | },
15 | "dependencies": {
16 | "@types/react": "18.2.69",
17 | "@types/react-dom": "18.2.22",
18 | "prerender-macro": "workspace:*",
19 | "react": "18.2.0",
20 | "react-dom": "18.2.0"
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/examples/react/src/build.ts:
--------------------------------------------------------------------------------
1 | import { join } from "node:path";
2 | import prerenderMacro from "prerender-macro";
3 |
4 | const { success, logs } = await Bun.build({
5 | entrypoints: [join(import.meta.dir, "index.tsx")],
6 | outdir: join(import.meta.dir, "..", "dist"),
7 | target: "bun",
8 | plugins: [
9 | prerenderMacro({
10 | prerenderConfigPath: join(import.meta.dir, "prerender.tsx"),
11 | }),
12 | ],
13 | });
14 |
15 | if (success) console.log("Build complete ✅");
16 | else console.error("Build failed ❌", logs);
17 |
--------------------------------------------------------------------------------
/examples/react/src/components/dynamic-component.tsx:
--------------------------------------------------------------------------------
1 | export default function DynamicComponent({ name }: { name: string }) {
2 | return (
3 |
4 | {name} Component 🔥 Random number = {Math.random()}
5 |
6 | );
7 | }
8 |
--------------------------------------------------------------------------------
/examples/react/src/components/static-component.tsx:
--------------------------------------------------------------------------------
1 | export default function StaticComponent({ name }: { name: string }) {
2 | return (
3 |
4 | {name} Component 🥶 Random number = {Math.random()}
5 |
6 | );
7 | }
8 |
--------------------------------------------------------------------------------
/examples/react/src/index.tsx:
--------------------------------------------------------------------------------
1 | import DynamicComponent from "./components/dynamic-component";
2 | import StaticComponent from "./components/static-component" with { type: "prerender" };
3 | import { renderToReadableStream } from "react-dom/server";
4 |
5 | Bun.serve({
6 | port: 1234,
7 | fetch: async () => {
8 | return new Response(
9 | await renderToReadableStream(
10 |
11 |
12 |
Prerender Macro | React example
13 |
14 |
15 |
16 |
17 |
18 |
Refresh
19 |
20 | ,
21 | ),
22 | { headers: new Headers({ "Content-Type": "text/html" }) },
23 | );
24 | },
25 | });
26 |
27 | console.log("Server running at http://localhost:1234");
28 |
--------------------------------------------------------------------------------
/examples/react/src/prerender.tsx:
--------------------------------------------------------------------------------
1 | import { type PrerenderConfig } from "prerender-macro";
2 | import { renderToString } from "react-dom/server";
3 |
4 | export const prerenderConfig = {
5 | render: async (Component, props) => {
6 | return renderToString(
);
7 | },
8 | postRender: (htmlString) => {
9 | return
;
10 | },
11 | } satisfies PrerenderConfig;
12 |
--------------------------------------------------------------------------------
/examples/react/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | // Enable latest features
4 | "lib": ["ESNext"],
5 | "target": "ESNext",
6 | "module": "ESNext",
7 | "moduleDetection": "force",
8 | "jsx": "react-jsx",
9 | "allowJs": true,
10 |
11 | // Bundler mode
12 | "moduleResolution": "bundler",
13 | "allowImportingTsExtensions": true,
14 | "verbatimModuleSyntax": true,
15 | "noEmit": true,
16 |
17 | // Best practices
18 | "strict": true,
19 | "skipLibCheck": true,
20 | "noFallthroughCasesInSwitch": true,
21 |
22 | // Some stricter flags (disabled by default)
23 | "noUnusedLocals": false,
24 | "noUnusedParameters": false,
25 | "noPropertyAccessFromIndexSignature": false
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "prerender-macro",
3 | "version": "0.1.1",
4 | "module": "package/index.ts",
5 | "type": "module",
6 | "devDependencies": {
7 | "react": "18.2.0",
8 | "react-dom": "18.2.0",
9 | "@types/bun": "1.0.11",
10 | "@types/react": "18.2.72",
11 | "@types/react-dom": "18.2.22",
12 | "brisa": "0.0.38",
13 | "preact": "10.20.1",
14 | "@kitajs/html": "4.0.0-next.3",
15 | "@kitajs/ts-html-plugin": "4.0.0-next.3"
16 | },
17 | "dependencies": {
18 | "typescript": "5.4.3"
19 | },
20 | "scripts": {
21 | "test": "bun run test:brisa && bun run test:react && bun run test:preact && bun run test:kitajs-html",
22 | "test:brisa": "cd tests/brisa && bun test && cd ../..",
23 | "test:react": "cd tests/react && bun test && cd ../..",
24 | "test:preact": "cd tests/preact && bun test && cd ../..",
25 | "test:kitajs-html": "cd tests/kitajs-html && bun test && cd ../..",
26 | "demo:react": "cd examples/react && bun start",
27 | "demo:brisa": "cd examples/brisa && bun start",
28 | "demo:preact": "cd examples/preact && bun start",
29 | "demo:kitajs-html": "cd examples/kitajs-html && bun start"
30 | },
31 | "workspaces": [
32 | "package",
33 | "tests/*",
34 | "examples/*"
35 | ],
36 | "files": [
37 | "package"
38 | ],
39 | "exports": {
40 | ".": {
41 | "import": "./package/index.ts",
42 | "require": "./package/index.ts"
43 | },
44 | "./prerender": {
45 | "import": "./package/prerender.tsx",
46 | "require": "./package/prerender.tsx"
47 | }
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/package/index.ts:
--------------------------------------------------------------------------------
1 | import type { BunPlugin } from "bun";
2 | import { dirname } from "node:path";
3 | import ts from "typescript";
4 |
5 | export type PluginConfig = {
6 | prerenderConfigPath: string;
7 | };
8 |
9 | export type PrerenderConfig = {
10 | render: (
11 | Component: any,
12 | props: any,
13 | ) => JSX.Element | string | Promise
;
14 | postRender?: (htmlString: string) => JSX.Element;
15 | };
16 |
17 | export type TranspilerOptions = {
18 | code: string;
19 | path: string;
20 | pluginConfig: PluginConfig;
21 | prerenderConfig?: PrerenderConfig;
22 | };
23 |
24 | export default function plugin(pluginConfig: PluginConfig) {
25 | if (!pluginConfig?.prerenderConfigPath) {
26 | throw new Error("prerender-macro: prerenderConfigPath config is required");
27 | }
28 | return {
29 | name: "prerender-plugin",
30 | async setup(build) {
31 | build.onLoad({ filter: /\.(tsx|jsx)$/ }, async ({ path, loader }) => {
32 | const code = await Bun.file(path).text();
33 | const prerenderConfig = (await import(pluginConfig.prerenderConfigPath))
34 | ?.prerenderConfig;
35 |
36 | try {
37 | const contents = transpile({
38 | code,
39 | path,
40 | pluginConfig,
41 | prerenderConfig,
42 | });
43 |
44 | return { contents, loader };
45 | } catch (e) {
46 | console.error(e);
47 | return { contents: code, loader };
48 | }
49 | });
50 | },
51 | } satisfies BunPlugin;
52 | }
53 |
54 | export function transpile({
55 | code,
56 | path,
57 | pluginConfig,
58 | prerenderConfig,
59 | }: TranspilerOptions) {
60 | const sourceFile = createSourceFile(code);
61 | const importsWithPrerender = getImportsWithPrerender(sourceFile, path);
62 |
63 | if (!importsWithPrerender.length) return code;
64 |
65 | let modifiedAst = addExtraImports(sourceFile, pluginConfig, prerenderConfig);
66 |
67 | modifiedAst = replaceJSXToMacroCall(
68 | modifiedAst,
69 | importsWithPrerender,
70 | pluginConfig,
71 | prerenderConfig,
72 | ) as ts.SourceFile;
73 |
74 | return ts
75 | .createPrinter()
76 | .printNode(ts.EmitHint.Unspecified, modifiedAst, sourceFile);
77 | }
78 |
79 | function createSourceFile(code: string) {
80 | const result = ts.transpileModule(code, {
81 | compilerOptions: {
82 | module: ts.ModuleKind.ESNext,
83 | target: ts.ScriptTarget.ESNext,
84 | jsx: ts.JsxEmit.Preserve,
85 | },
86 | });
87 |
88 | return ts.createSourceFile(
89 | "file.tsx",
90 | result.outputText,
91 | ts.ScriptTarget.ESNext,
92 | true,
93 | ts.ScriptKind.TSX,
94 | );
95 | }
96 |
97 | function getImportsWithPrerender(sourceFile: ts.SourceFile, path: string) {
98 | const dir = dirname(path);
99 |
100 | return sourceFile.statements.filter(isPrerenderImport).flatMap((node) => {
101 | const namedExports = node.importClause?.namedBindings as ts.NamedImports;
102 |
103 | if (namedExports) {
104 | return namedExports.elements.map((element) => ({
105 | identifier: element.name.getText(),
106 | path: Bun.resolveSync(
107 | (node.moduleSpecifier as any).text ?? node.moduleSpecifier.getText(),
108 | dir,
109 | ),
110 | moduleName: element.propertyName?.getText() ?? element.name.getText(),
111 | }));
112 | }
113 |
114 | return {
115 | identifier: node.importClause?.getText(),
116 | path: Bun.resolveSync(
117 | (node.moduleSpecifier as any).text ?? node.moduleSpecifier.getText(),
118 | dir,
119 | ),
120 | moduleName: "default",
121 | };
122 | });
123 | }
124 |
125 | function isPrerenderImport(node: ts.Node): node is ts.ImportDeclaration {
126 | return (
127 | ts.isImportDeclaration(node) &&
128 | Boolean(
129 | node.attributes?.elements?.some(
130 | (element: any) =>
131 | element.name.getText() === "type" &&
132 | element.value.text === "prerender",
133 | ),
134 | )
135 | );
136 | }
137 |
138 | function addExtraImports(
139 | ast: ts.SourceFile,
140 | pluginConfig: PluginConfig,
141 | prerenderConfig?: PrerenderConfig,
142 | ) {
143 | const allImports = [...ast.statements];
144 |
145 | allImports.unshift(
146 | ts.factory.createImportDeclaration(
147 | undefined,
148 | ts.factory.createImportClause(
149 | false,
150 | undefined,
151 | ts.factory.createNamedImports([
152 | ts.factory.createImportSpecifier(
153 | false,
154 | ts.factory.createIdentifier("prerender"),
155 | ts.factory.createIdentifier("__prerender__macro"),
156 | ),
157 | ]),
158 | ),
159 | ts.factory.createStringLiteral("prerender-macro/prerender"),
160 | ts.factory.createImportAttributes(
161 | ts.factory.createNodeArray([
162 | ts.factory.createImportAttribute(
163 | ts.factory.createStringLiteral("type"),
164 | ts.factory.createStringLiteral("macro"),
165 | ),
166 | ]),
167 | ),
168 | ),
169 | );
170 |
171 | if (prerenderConfig?.postRender) {
172 | allImports.unshift(
173 | ts.factory.createImportDeclaration(
174 | undefined,
175 | ts.factory.createImportClause(
176 | false,
177 | undefined,
178 | ts.factory.createNamedImports([
179 | ts.factory.createImportSpecifier(
180 | false,
181 | undefined,
182 | ts.factory.createIdentifier("prerenderConfig"),
183 | ),
184 | ]),
185 | ),
186 | ts.factory.createStringLiteral(pluginConfig.prerenderConfigPath),
187 | ),
188 | );
189 | }
190 |
191 | return ts.factory.updateSourceFile(ast, allImports);
192 | }
193 |
194 | /**
195 | *
196 | * Replace
197 | *
198 | * to
199 | * __prerender__macro({
200 | * componentPath: "path/to/component.tsx",
201 | * componentModuleName: "StaticComponent",
202 | * componentProps: {},
203 | * prerenderConfigPath: "path/to/config.tsx"
204 | * });
205 | *
206 | */
207 | function replaceJSXToMacroCall(
208 | node: ts.Node,
209 | imports: any[],
210 | pluginConfig: PluginConfig,
211 | prerenderConfig?: PrerenderConfig,
212 | context?: ts.TransformationContext,
213 | ): ts.Node {
214 | if (ts.isJsxSelfClosingElement(node) || ts.isJsxOpeningElement(node)) {
215 | const module = imports.find((i) => i.identifier === node.tagName.getText());
216 |
217 | if (module) {
218 | const props: ts.ObjectLiteralElementLike[] = [];
219 |
220 | for (const attr of node.attributes.properties) {
221 | if (ts.isJsxAttribute(attr)) {
222 | const propName = attr.name.getText();
223 | let propValue = attr.initializer as ts.Expression;
224 |
225 | if (ts.isJsxExpression(propValue)) {
226 | propValue = propValue.expression as ts.Expression;
227 | }
228 |
229 | props.push(
230 | ts.factory.createPropertyAssignment(
231 | ts.factory.createIdentifier(propName),
232 | propValue,
233 | ),
234 | );
235 | }
236 | }
237 |
238 | let macroCall = ts.factory.createCallExpression(
239 | ts.factory.createIdentifier("__prerender__macro"),
240 | undefined,
241 | [
242 | ts.factory.createObjectLiteralExpression([
243 | ts.factory.createPropertyAssignment(
244 | ts.factory.createIdentifier("componentPath"),
245 | ts.factory.createStringLiteral(module.path),
246 | ),
247 | ts.factory.createPropertyAssignment(
248 | ts.factory.createIdentifier("componentModuleName"),
249 | ts.factory.createStringLiteral(module.moduleName),
250 | ),
251 | ts.factory.createPropertyAssignment(
252 | ts.factory.createIdentifier("componentProps"),
253 | ts.factory.createObjectLiteralExpression(props),
254 | ),
255 | ts.factory.createPropertyAssignment(
256 | ts.factory.createIdentifier("prerenderConfigPath"),
257 | ts.factory.createStringLiteral(pluginConfig.prerenderConfigPath),
258 | ),
259 | ]),
260 | ],
261 | );
262 |
263 | // Wrap with postRender function
264 | if (prerenderConfig?.postRender) {
265 | macroCall = ts.factory.createCallExpression(
266 | ts.factory.createPropertyAccessExpression(
267 | ts.factory.createIdentifier("prerenderConfig"),
268 | ts.factory.createIdentifier("postRender"),
269 | ),
270 | undefined,
271 | [macroCall],
272 | );
273 | }
274 |
275 | if (
276 | node.parent &&
277 | (ts.isJsxElement(node.parent) || ts.isJsxFragment(node.parent))
278 | ) {
279 | return ts.factory.createJsxExpression(undefined, macroCall);
280 | }
281 |
282 | return macroCall;
283 | }
284 | }
285 |
286 | return ts.visitEachChild(
287 | node,
288 | (child) =>
289 | replaceJSXToMacroCall(
290 | child,
291 | imports,
292 | pluginConfig,
293 | prerenderConfig,
294 | context,
295 | ),
296 | context,
297 | );
298 | }
299 |
300 | declare global {
301 | var jsxRuntime: string | undefined;
302 | }
303 |
--------------------------------------------------------------------------------
/package/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "prerender-macro",
3 | "private": true,
4 | "module": "index.ts",
5 | "type": "module"
6 | }
7 |
--------------------------------------------------------------------------------
/package/prerender.tsx:
--------------------------------------------------------------------------------
1 | type PrerenderParams = {
2 | componentPath: string;
3 | componentModuleName?: string;
4 | componentProps?: Record;
5 | prerenderConfigPath: string;
6 | };
7 |
8 | export async function prerender({
9 | componentPath,
10 | componentModuleName = "default",
11 | componentProps = {},
12 | prerenderConfigPath,
13 | }: PrerenderParams) {
14 | try {
15 | const Component = (await import(componentPath))[componentModuleName];
16 | const config = (await import(prerenderConfigPath)).prerenderConfig;
17 | return await config.render(Component, componentProps);
18 | } catch (e) {
19 | console.error(e);
20 | }
21 | }
22 |
23 | export const __prerender__macro = prerender;
24 |
--------------------------------------------------------------------------------
/package/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": "/package",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "module": "esnext",
6 | "target": "esnext",
7 | "moduleResolution": "bundler",
8 | "moduleDetection": "force",
9 | "allowImportingTsExtensions": true,
10 | "noEmit": true,
11 | "composite": true,
12 | "strict": true,
13 | "downlevelIteration": true,
14 | "skipLibCheck": true,
15 | "jsx": "react-jsx",
16 | "jsxImportSource": "brisa",
17 | "allowSyntheticDefaultImports": true,
18 | "forceConsistentCasingInFileNames": true,
19 | "allowJs": true,
20 | "verbatimModuleSyntax": true,
21 | "noFallthroughCasesInSwitch": true,
22 | "paths": {
23 | "@/*": ["*"]
24 | }
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/tests/brisa/components.tsx:
--------------------------------------------------------------------------------
1 | export default function Foo({
2 | name = "foo",
3 | nested = {},
4 | }: {
5 | name: string;
6 | nested: { foo?: string };
7 | }) {
8 | return (
9 |
10 | Foo, {name}
11 | {nested.foo}!
12 |
13 | );
14 | }
15 |
16 | export function Bar({ name = "bar" }: { name: string }) {
17 | return Bar, {name}!
;
18 | }
19 |
--------------------------------------------------------------------------------
/tests/brisa/config.tsx:
--------------------------------------------------------------------------------
1 | import { dangerHTML } from "brisa";
2 | import { renderToString } from "brisa/server";
3 | import type { PrerenderConfig } from "prerender-macro";
4 |
5 | export const prerenderConfig = {
6 | render: async (Component, props) =>
7 | dangerHTML(await renderToString()),
8 | } satisfies PrerenderConfig;
9 |
--------------------------------------------------------------------------------
/tests/brisa/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "test-brisa",
3 | "private": true,
4 | "scripts": {
5 | "test": "bun test"
6 | },
7 | "dependencies": {
8 | "prerender-macro": "workspace:*"
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/tests/brisa/plugin.test.tsx:
--------------------------------------------------------------------------------
1 | import { join } from "node:path";
2 | import { describe, it, expect } from "bun:test";
3 | import { transpile, type TranspilerOptions } from "prerender-macro";
4 |
5 | const format = (s: string) => s.replace(/\s*\n\s*/g, "").replaceAll("'", '"');
6 | const configPath = join(import.meta.dir, "config.tsx");
7 | const currentFile = import.meta.url.replace("file://", "");
8 | const bunTranspiler = new Bun.Transpiler({ loader: "tsx" });
9 |
10 | function transpileAndRunMacros(config: TranspilerOptions) {
11 | // Bun transpiler is needed here to run the macros
12 | return format(bunTranspiler.transformSync(transpile(config)));
13 | }
14 |
15 | describe("Brisa", () => {
16 | describe("plugin", () => {
17 | it('should not transform if there is not an import attribute with type "prerender"', () => {
18 | const code = `
19 | import Foo from "./components";
20 | import { Bar } from "./components";
21 |
22 | export default function Test() {
23 | return (
24 |
25 |
26 |
27 |
28 | );
29 | }
30 | `;
31 | const output = transpileAndRunMacros({
32 | code,
33 | path: currentFile,
34 | pluginConfig: { prerenderConfigPath: configPath },
35 | });
36 | const expected = format(bunTranspiler.transformSync(code));
37 |
38 | expect(output).toBe(expected);
39 | });
40 | it("should transform a static component", () => {
41 | const code = `
42 | import Foo from "./components" with { type: "prerender" };
43 | import { Bar } from "./components";
44 |
45 | export default function Test() {
46 | return (
47 |
48 |
49 |
50 |
51 | );
52 | }
53 | `;
54 | const output = transpileAndRunMacros({
55 | code,
56 | path: currentFile,
57 | pluginConfig: { prerenderConfigPath: configPath },
58 | });
59 | const expected = format(`
60 | import Foo from "./components";
61 | import {Bar} from "./components";
62 |
63 | export default function Test() {
64 | return jsxDEV("div", {
65 | children: [{type: "HTML",props: {html: "Foo, foo!
"}},
66 | jsxDEV(Bar, {}, undefined, false, undefined, this)
67 | ]}, undefined, true, undefined, this);
68 | }
69 | `);
70 |
71 | expect(output).toBe(expected);
72 | });
73 |
74 | it("should transform a static component from named export", () => {
75 | const code = `
76 | import { Bar } from "./components" with { type: "prerender" };
77 | import Foo from "./components";
78 |
79 | export default function Test() {
80 | return (
81 |
82 |
83 |
84 |
85 | );
86 | }
87 | `;
88 | const output = transpileAndRunMacros({
89 | code,
90 | path: currentFile,
91 | pluginConfig: { prerenderConfigPath: configPath },
92 | });
93 | const expected = format(`
94 | import {Bar} from "./components";
95 | import Foo from "./components";
96 |
97 | export default function Test() {
98 | return jsxDEV("div", {
99 | children: [jsxDEV(Foo, {}, undefined, false, undefined, this),
100 | {type: "HTML",props: {html: "Bar, bar!
"}}
101 | ]}, undefined, true, undefined, this)
102 | ;}
103 | `);
104 |
105 | expect(output).toBe(expected);
106 | });
107 |
108 | it("should transform a static component from named export and a fragment", () => {
109 | const code = `
110 | import { Bar } from "./components" with { type: "prerender" };
111 | import Foo from "./components";
112 |
113 | export default function Test() {
114 | return (
115 | <>
116 |
117 |
118 | >
119 | );
120 | }
121 | `;
122 | const output = transpileAndRunMacros({
123 | code,
124 | path: currentFile,
125 | pluginConfig: { prerenderConfigPath: configPath },
126 | });
127 | const expected = format(`
128 | import {Bar} from "./components";
129 | import Foo from "./components";
130 |
131 | export default function Test() {
132 | return jsxDEV(Fragment, {
133 | children: [jsxDEV(Foo, {}, undefined, false, undefined, this),
134 | {type: "HTML",props: {html: "Bar, bar!
"}}
135 | ]}, undefined, true, undefined, this)
136 | ;}
137 | `);
138 |
139 | expect(output).toBe(expected);
140 | });
141 |
142 | it("should transform a static component when is not inside JSX", () => {
143 | const code = `
144 | import { Bar } from "./components" with { type: "prerender" };
145 |
146 | export default function Test() {
147 | return ;
148 | }
149 | `;
150 | const output = transpileAndRunMacros({
151 | code,
152 | path: currentFile,
153 | pluginConfig: { prerenderConfigPath: configPath },
154 | });
155 | const expected = format(`
156 | import {Bar} from "./components";
157 |
158 | export default function Test() {
159 | return {type: "HTML",props: {html: "Bar, bar!
"}};
160 | }
161 | `);
162 |
163 | expect(output).toBe(expected);
164 | });
165 |
166 | it("should transform a static component with props", () => {
167 | const code = `
168 | import Foo from "./components" with { type: "prerender" };
169 |
170 | export default function Test() {
171 | return ;
172 | }
173 | `;
174 | const output = transpileAndRunMacros({
175 | code,
176 | path: currentFile,
177 | pluginConfig: { prerenderConfigPath: configPath },
178 | });
179 | const expected = format(`
180 | import Foo from "./components";
181 |
182 | export default function Test() {
183 | return {type: "HTML",props: {html: "Foo, Brisa works!
"}};
184 | }
185 | `);
186 |
187 | expect(output).toBe(expected);
188 | });
189 | });
190 | });
191 |
--------------------------------------------------------------------------------
/tests/brisa/prerender.test.tsx:
--------------------------------------------------------------------------------
1 | import { join } from "node:path";
2 | import { describe, expect, it } from "bun:test";
3 | import { prerender } from "prerender-macro/prerender";
4 |
5 | describe("Brisa", () => {
6 | describe("prerender", () => {
7 | it("should work with default module", async () => {
8 | const result = await prerender({
9 | componentPath: join(import.meta.dir, "components.tsx"),
10 | componentModuleName: "default",
11 | componentProps: { name: "Brisa" },
12 | prerenderConfigPath: join(import.meta.dir, "config.tsx"),
13 | });
14 |
15 | expect(result).toStrictEqual({
16 | // HTML is special element in Brisa, different than html (lowercase)
17 | type: "HTML",
18 | props: {
19 | html: "Foo, Brisa!
",
20 | },
21 | });
22 | });
23 | it("should work with named module", async () => {
24 | const result = await prerender({
25 | componentPath: join(import.meta.dir, "components.tsx"),
26 | componentModuleName: "Bar",
27 | componentProps: { name: "Brisa" },
28 | prerenderConfigPath: join(import.meta.dir, "config.tsx"),
29 | });
30 |
31 | expect(result).toStrictEqual({
32 | // HTML is special element in Brisa, different than html (lowercase)
33 | type: "HTML",
34 | props: {
35 | html: "Bar, Brisa!
",
36 | },
37 | });
38 | });
39 | });
40 | });
41 |
--------------------------------------------------------------------------------
/tests/brisa/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": ".",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "module": "esnext",
6 | "target": "esnext",
7 | "moduleResolution": "bundler",
8 | "moduleDetection": "force",
9 | "allowImportingTsExtensions": true,
10 | "noEmit": true,
11 | "composite": true,
12 | "strict": true,
13 | "downlevelIteration": true,
14 | "skipLibCheck": true,
15 | "jsx": "react-jsx",
16 | "jsxImportSource": "brisa",
17 | "allowSyntheticDefaultImports": true,
18 | "forceConsistentCasingInFileNames": true,
19 | "allowJs": true,
20 | "verbatimModuleSyntax": true,
21 | "noFallthroughCasesInSwitch": true,
22 | "types": ["brisa"],
23 | "paths": {
24 | "@/*": ["*"]
25 | }
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/tests/kitajs-html/components.tsx:
--------------------------------------------------------------------------------
1 | export default function Foo({
2 | name = "foo",
3 | nested = {},
4 | }: {
5 | name: string;
6 | nested: { foo?: string };
7 | }) {
8 | return (
9 |
10 | Foo, {name}
11 | {nested.foo}!
12 |
13 | );
14 | }
15 |
16 | export function Bar({ name = "bar" }: { name: string }) {
17 | return Bar, {name}!
;
18 | }
19 |
--------------------------------------------------------------------------------
/tests/kitajs-html/config.tsx:
--------------------------------------------------------------------------------
1 | import { createElement } from "@kitajs/html";
2 | import prerenderMacroPlugin, { type PrerenderConfig } from "prerender-macro";
3 |
4 | export const prerenderConfig = {
5 | render: createElement,
6 | } satisfies PrerenderConfig;
7 |
8 | export const plugin = prerenderMacroPlugin({
9 | prerenderConfigPath: import.meta.url,
10 | });
11 |
--------------------------------------------------------------------------------
/tests/kitajs-html/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "test-kitajs-html",
3 | "private": true,
4 | "scripts": {
5 | "test": "bun test"
6 | },
7 | "dependencies": {
8 | "prerender-macro": "workspace:*"
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/tests/kitajs-html/plugin.test.tsx:
--------------------------------------------------------------------------------
1 | import { join } from "node:path";
2 | import { describe, it, expect } from "bun:test";
3 | import { transpile, type TranspilerOptions } from "prerender-macro";
4 |
5 | const format = (s: string) => s.replace(/\s*\n\s*/g, "").replaceAll("'", '"');
6 | const configPath = join(import.meta.dir, "config.tsx");
7 | const currentFile = import.meta.url.replace("file://", "");
8 | const bunTranspiler = new Bun.Transpiler({ loader: "tsx" });
9 |
10 | function transpileAndRunMacros(config: TranspilerOptions) {
11 | // Bun transpiler is needed here to run the macros
12 | return format(bunTranspiler.transformSync(transpile(config)));
13 | }
14 |
15 | describe("Kitajs/html", () => {
16 | describe("plugin", () => {
17 | it('should not transform if there is not an import attribute with type "prerender"', () => {
18 | const code = `
19 | import Foo from "./components";
20 | import { Bar } from "./components";
21 |
22 | export default function Test() {
23 | return (
24 |
25 |
26 |
27 |
28 | );
29 | }
30 | `;
31 | const output = transpileAndRunMacros({
32 | code,
33 | path: currentFile,
34 | pluginConfig: { prerenderConfigPath: configPath },
35 | });
36 | const expected = format(bunTranspiler.transformSync(code));
37 |
38 | expect(output).toBe(expected);
39 | });
40 | it("should transform a static component", () => {
41 | const code = `
42 | import Foo from "./components" with { type: "prerender" };
43 | import { Bar } from "./components";
44 |
45 | export default function Test() {
46 | return (
47 |
48 |
49 |
50 |
51 | );
52 | }
53 | `;
54 | const output = transpileAndRunMacros({
55 | code,
56 | path: currentFile,
57 | pluginConfig: { prerenderConfigPath: configPath },
58 | });
59 | const expected = format(`
60 | import Foo from "./components";
61 | import {Bar} from "./components";
62 |
63 | export default function Test() {
64 | return jsxDEV("div", {
65 | children: ["Foo, foo!
",
66 | jsxDEV(Bar, {}, undefined, false, undefined, this)
67 | ]}, undefined, true, undefined, this);
68 | }
69 | `);
70 |
71 | expect(output).toBe(expected);
72 | });
73 |
74 | it("should transform a static component from named export", () => {
75 | const code = `
76 | import { Bar } from "./components" with { type: "prerender" };
77 | import Foo from "./components";
78 |
79 | export default function Test() {
80 | return (
81 |
82 |
83 |
84 |
85 | );
86 | }
87 | `;
88 | const output = transpileAndRunMacros({
89 | code,
90 | path: currentFile,
91 | pluginConfig: { prerenderConfigPath: configPath },
92 | });
93 | const expected = format(`
94 | import {Bar} from "./components";
95 | import Foo from "./components";
96 |
97 | export default function Test() {
98 | return jsxDEV("div", {
99 | children: [jsxDEV(Foo, {}, undefined, false, undefined, this),
100 | "Bar, bar!
"
101 | ]}, undefined, true, undefined, this)
102 | ;}
103 | `);
104 |
105 | expect(output).toBe(expected);
106 | });
107 |
108 | it("should transform a static component from named export and a fragment", () => {
109 | const code = `
110 | import { Bar } from "./components" with { type: "prerender" };
111 | import Foo from "./components";
112 |
113 | export default function Test() {
114 | return (
115 | <>
116 |
117 |
118 | >
119 | );
120 | }
121 | `;
122 | const output = transpileAndRunMacros({
123 | code,
124 | path: currentFile,
125 | pluginConfig: { prerenderConfigPath: configPath },
126 | });
127 | const expected = format(`
128 | import {Bar} from "./components";
129 | import Foo from "./components";
130 |
131 | export default function Test() {
132 | return jsxDEV(Fragment, {children: [jsxDEV(Foo, {}, undefined, false, undefined, this),
133 | "Bar, bar!
"
134 | ]}, undefined, true, undefined, this);
135 | }
136 | `);
137 |
138 | expect(output).toBe(expected);
139 | });
140 |
141 | it("should transform a static component when is not inside JSX", () => {
142 | const code = `
143 | import { Bar } from "./components" with { type: "prerender" };
144 |
145 | export default function Test() {
146 | return ;
147 | }
148 | `;
149 | const output = transpileAndRunMacros({
150 | code,
151 | path: currentFile,
152 | pluginConfig: { prerenderConfigPath: configPath },
153 | });
154 | const expected = format(`
155 | import {Bar} from "./components";
156 |
157 | export default function Test() {
158 | return "Bar, bar!
";
159 | }
160 | `);
161 |
162 | expect(output).toBe(expected);
163 | });
164 |
165 | it("should transform a static component with props", () => {
166 | const code = `
167 | import Foo from "./components" with { type: "prerender" };
168 |
169 | export default function Test() {
170 | return ;
171 | }
172 | `;
173 | const output = transpileAndRunMacros({
174 | code,
175 | path: currentFile,
176 | pluginConfig: { prerenderConfigPath: configPath },
177 | });
178 | const expected = format(`
179 | import Foo from "./components";
180 |
181 | export default function Test() {
182 | return "Foo, Kitajs/html works!
";
183 | }
184 | `);
185 |
186 | expect(output).toBe(expected);
187 | });
188 | });
189 | });
190 |
--------------------------------------------------------------------------------
/tests/kitajs-html/prerender.test.tsx:
--------------------------------------------------------------------------------
1 | import { join } from "node:path";
2 | import { describe, expect, it } from "bun:test";
3 | import { prerender } from "prerender-macro/prerender";
4 |
5 | describe("Kitajs/html", () => {
6 | describe("prerender", () => {
7 | it("should work with default module", async () => {
8 | const result = await prerender({
9 | componentPath: join(import.meta.dir, "components.tsx"),
10 | componentModuleName: "default",
11 | componentProps: { name: "Kitajs/html" },
12 | prerenderConfigPath: join(import.meta.dir, "config.tsx"),
13 | });
14 |
15 | expect(result).toBe("Foo, Kitajs/html!
");
16 | });
17 | it("should work with named module", async () => {
18 | const result = await prerender({
19 | componentPath: join(import.meta.dir, "components.tsx"),
20 | componentModuleName: "Bar",
21 | componentProps: { name: "Kitajs/html" },
22 | prerenderConfigPath: join(import.meta.dir, "config.tsx"),
23 | });
24 |
25 | expect(result).toBe("Bar, Kitajs/html!
");
26 | });
27 | });
28 | });
29 |
--------------------------------------------------------------------------------
/tests/kitajs-html/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": ".",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "module": "esnext",
6 | "target": "esnext",
7 | "moduleResolution": "bundler",
8 | "moduleDetection": "force",
9 | "jsx": "react-jsx",
10 | "jsxImportSource": "@kitajs/html",
11 | "plugins": [{ "name": "@kitajs/ts-html-plugin" }]
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/tests/preact/components.tsx:
--------------------------------------------------------------------------------
1 | export default function Foo({
2 | name = "foo",
3 | nested = {},
4 | }: {
5 | name: string;
6 | nested: { foo?: string };
7 | }) {
8 | return (
9 |
10 | Foo, {name}
11 | {nested.foo}!
12 |
13 | );
14 | }
15 |
16 | export function Bar({ name = "bar" }: { name: string }) {
17 | return Bar, {name}!
;
18 | }
19 |
--------------------------------------------------------------------------------
/tests/preact/config.tsx:
--------------------------------------------------------------------------------
1 | import { render } from "preact-render-to-string";
2 | import type { PrerenderConfig } from "prerender-macro";
3 |
4 | export const prerenderConfig = {
5 | render: async (Component, props) => {
6 | return (
7 | ) }}
9 | />
10 | );
11 | },
12 | } satisfies PrerenderConfig;
13 |
--------------------------------------------------------------------------------
/tests/preact/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "test-preact",
3 | "private": true,
4 | "scripts": {
5 | "test": "bun test"
6 | },
7 | "dependencies": {
8 | "preact-render-to-string": "6.4.1",
9 | "prerender-macro": "workspace:*"
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/tests/preact/plugin.test.tsx:
--------------------------------------------------------------------------------
1 | import { join } from "node:path";
2 | import { describe, it, expect } from "bun:test";
3 | import { TranspilerOptions, transpile } from "prerender-macro";
4 |
5 | const format = (s: string) => s.replace(/\s*\n\s*/g, "").replaceAll("'", '"');
6 | const configPath = join(import.meta.dir, "config.tsx");
7 | const currentFile = import.meta.url.replace("file://", "");
8 | const bunTranspiler = new Bun.Transpiler({ loader: "tsx" });
9 |
10 | function transpileAndRunMacros(config: TranspilerOptions) {
11 | // Bun transpiler is needed here to run the macros
12 | return format(bunTranspiler.transformSync(transpile(config)));
13 | }
14 |
15 | describe("Preact", () => {
16 | describe("plugin", () => {
17 | it('should not transform if there is not an import attribute with type "prerender"', () => {
18 | const code = `
19 | import Foo from "./components";
20 | import { Bar } from "./components";
21 |
22 | export default function Test() {
23 | return (
24 |
25 |
26 |
27 |
28 | );
29 | }
30 | `;
31 | const output = transpileAndRunMacros({
32 | code,
33 | path: currentFile,
34 | pluginConfig: { prerenderConfigPath: configPath },
35 | });
36 | const expected = format(bunTranspiler.transformSync(code));
37 |
38 | expect(output).toBe(expected);
39 | });
40 | it("should transform a static component", () => {
41 | const code = `
42 | import Foo from "./components" with { type: "prerender" };
43 | import { Bar } from "./components";
44 |
45 | export default function Test() {
46 | return (
47 |
48 |
49 |
50 |
51 | );
52 | }
53 | `;
54 | const output = transpileAndRunMacros({
55 | code,
56 | path: currentFile,
57 | pluginConfig: { prerenderConfigPath: configPath },
58 | });
59 | const expected = format(`
60 | import Foo from "./components";
61 | import {Bar} from "./components";
62 |
63 | export default function Test() {
64 | return jsxDEV("div", {
65 | children: [{type: "div",props: {
66 | dangerouslySetInnerHTML: {__html: "Foo, foo!
"
67 | }},
68 | key: undefined,ref: undefined,__k: null,__: null,__b: 0,__e: null,__d: undefined,__c: null,constructor: undefined,__v: -3,__i: -1,__u: 0,__source: undefined,__self: undefined},jsxDEV(Bar, {}, undefined, false, undefined, this)]}, undefined, true, undefined, this);
69 | }
70 | `);
71 |
72 | expect(output).toBe(expected);
73 | });
74 |
75 | it("should transform a static component from named export", () => {
76 | const code = `
77 | import { Bar } from "./components" with { type: "prerender" };
78 | import Foo from "./components";
79 |
80 | export default function Test() {
81 | return (
82 |
83 |
84 |
85 |
86 | );
87 | }
88 | `;
89 | const output = transpileAndRunMacros({
90 | code,
91 | path: currentFile,
92 | pluginConfig: { prerenderConfigPath: configPath },
93 | });
94 | const expected = format(`
95 | import {Bar} from "./components";
96 | import Foo from "./components";
97 |
98 | export default function Test() {
99 | return jsxDEV("div", {
100 | children: [jsxDEV(Foo, {}, undefined, false, undefined, this),{
101 | type: "div",props: {dangerouslySetInnerHTML: {
102 | __html: "Bar, bar!
"
103 | }
104 | },key: undefined,ref: undefined,__k: null,__: null,__b: 0,__e: null,__d: undefined,__c: null,constructor: undefined,__v: -6,__i: -1,__u: 0,__source: undefined,__self: undefined}]}, undefined, true, undefined, this);
105 | }
106 | `);
107 |
108 | expect(output).toBe(expected);
109 | });
110 |
111 | it("should transform a static component from named export and a fragment", () => {
112 | const code = `
113 | import { Bar } from "./components" with { type: "prerender" };
114 | import Foo from "./components";
115 |
116 | export default function Test() {
117 | return (
118 | <>
119 |
120 |
121 | >
122 | );
123 | }
124 | `;
125 | const output = transpileAndRunMacros({
126 | code,
127 | path: currentFile,
128 | pluginConfig: { prerenderConfigPath: configPath },
129 | });
130 | const expected = format(`
131 | import {Bar} from "./components";
132 | import Foo from "./components";
133 |
134 | export default function Test() {
135 | return jsxDEV(Fragment, {
136 | children: [jsxDEV(Foo, {}, undefined, false, undefined, this),{
137 | type: "div",props: {dangerouslySetInnerHTML: {
138 | __html: "Bar, bar!
"
139 | }
140 | },key: undefined,ref: undefined,__k: null,__: null,__b: 0,__e: null,__d: undefined,__c: null,constructor: undefined,__v: -9,__i: -1,__u: 0,__source: undefined,__self: undefined}]}, undefined, true, undefined, this);
141 | }
142 | `);
143 |
144 | expect(output).toBe(expected);
145 | });
146 |
147 | it("should transform a static component when is not inside JSX", () => {
148 | const code = `
149 | import { Bar } from "./components" with { type: "prerender" };
150 |
151 | export default function Test() {
152 | return ;
153 | }
154 | `;
155 | const output = transpileAndRunMacros({
156 | code,
157 | path: currentFile,
158 | pluginConfig: { prerenderConfigPath: configPath },
159 | });
160 | const expected = format(`
161 | import {Bar} from "./components";
162 |
163 | export default function Test() {
164 | return {type: "div",props: {dangerouslySetInnerHTML: {__html: "Bar, bar!
"}},key: undefined,ref: undefined,__k: null,__: null,__b: 0,__e: null,__d: undefined,__c: null,constructor: undefined,__v: -12,__i: -1,__u: 0,__source: undefined,__self: undefined};
165 | }
166 | `);
167 |
168 | expect(output).toBe(expected);
169 | });
170 |
171 | it("should transform a static component with props", () => {
172 | const code = `
173 | import Foo from "./components" with { type: "prerender" };
174 |
175 | export default function Test() {
176 | return ;
177 | }
178 | `;
179 | const output = transpileAndRunMacros({
180 | code,
181 | path: currentFile,
182 | pluginConfig: { prerenderConfigPath: configPath },
183 | });
184 | const expected = format(`
185 | import Foo from "./components";
186 |
187 | export default function Test() {
188 | return {type: "div",props: {dangerouslySetInnerHTML: {__html: "Foo, Preact works!
"}},key: undefined,ref: undefined,__k: null,__: null,__b: 0,__e: null,__d: undefined,__c: null,constructor: undefined,__v: -15,__i: -1,__u: 0,__source: undefined,__self: undefined};
189 | }
190 | `);
191 |
192 | expect(output).toBe(expected);
193 | });
194 | });
195 | });
196 |
--------------------------------------------------------------------------------
/tests/preact/prerender.test.tsx:
--------------------------------------------------------------------------------
1 | import { describe, expect, it } from "bun:test";
2 | import { join } from "node:path";
3 | import { prerender } from "prerender-macro/prerender";
4 |
5 | describe("Preact", () => {
6 | describe("prerender", () => {
7 | it("should work with default export", async () => {
8 | const result = await prerender({
9 | componentPath: join(import.meta.dir, "components.tsx"),
10 | componentModuleName: "default",
11 | componentProps: { name: "Preact" },
12 | prerenderConfigPath: join(import.meta.dir, "config.tsx"),
13 | });
14 |
15 | expect(result.type).toBe("div");
16 | expect(result.props).toStrictEqual({
17 | dangerouslySetInnerHTML: {
18 | __html: "Foo, Preact!
",
19 | },
20 | });
21 | });
22 | it("should work with named export", async () => {
23 | const result = await prerender({
24 | componentPath: join(import.meta.dir, "components.tsx"),
25 | componentModuleName: "Bar",
26 | componentProps: { name: "Preact" },
27 | prerenderConfigPath: join(import.meta.dir, "config.tsx"),
28 | });
29 |
30 | expect(result.type).toBe("div");
31 | expect(result.props).toStrictEqual({
32 | dangerouslySetInnerHTML: {
33 | __html: "Bar, Preact!
",
34 | },
35 | });
36 | });
37 | });
38 | });
39 |
--------------------------------------------------------------------------------
/tests/preact/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "jsxImportSource": "preact",
4 | "jsx": "react-jsx",
5 | "baseUrl": ".",
6 | "lib": ["dom", "dom.iterable", "esnext"],
7 | "module": "esnext",
8 | "target": "esnext",
9 | "moduleResolution": "bundler",
10 | "moduleDetection": "force",
11 | "skipLibCheck": true,
12 | "paths": {
13 | "react": ["node_modules/preact/compat"],
14 | "react-dom": ["./node_modules/preact/compat/"]
15 | },
16 | "typeRoots": ["src/types", "./node_modules/@types"]
17 | },
18 | "resolve": {
19 | "extensions": [".js", ".jsx", ".ts", ".tsx", ".json", ".svg", ".css"]
20 | },
21 | "include": ["."]
22 | }
23 |
--------------------------------------------------------------------------------
/tests/react/components.tsx:
--------------------------------------------------------------------------------
1 | export default function Foo({
2 | name = "foo",
3 | nested = {},
4 | }: {
5 | name: string;
6 | nested: { foo?: string };
7 | }) {
8 | return (
9 |
10 | Foo, {name}
11 | {nested.foo}!
12 |
13 | );
14 | }
15 |
16 | export function Bar({ name = "bar" }: { name: string }) {
17 | return Bar, {name}!
;
18 | }
19 |
--------------------------------------------------------------------------------
/tests/react/config.tsx:
--------------------------------------------------------------------------------
1 | import { renderToString } from "react-dom/server";
2 | import type { PrerenderConfig } from "prerender-macro";
3 |
4 | export const prerenderConfig = {
5 | render: async (Component: any, props: any) => {
6 | return renderToString();
7 | },
8 | postRender: (htmlString: string) => (
9 |
10 | ),
11 | } satisfies PrerenderConfig;
12 |
--------------------------------------------------------------------------------
/tests/react/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "test-react",
3 | "private": true,
4 | "scripts": {
5 | "test": "bun test"
6 | },
7 | "dependencies": {
8 | "prerender-macro": "workspace:*"
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/tests/react/plugin.test.tsx:
--------------------------------------------------------------------------------
1 | import { join } from "node:path";
2 | import { describe, it, expect } from "bun:test";
3 | import { transpile, type TranspilerOptions } from "prerender-macro";
4 | import { prerenderConfig } from "./config";
5 |
6 | const format = (s: string) => s.replace(/\s*\n\s*/g, "").replaceAll("'", '"');
7 | const configPath = join(import.meta.dir, "config.tsx");
8 | const currentFile = import.meta.url.replace("file://", "");
9 | const importConfig = `import {prerenderConfig} from "${configPath}";`;
10 | const bunTranspiler = new Bun.Transpiler({ loader: "tsx" });
11 |
12 | function transpileAndRunMacros(config: TranspilerOptions) {
13 | // Bun transpiler is needed here to run the macros
14 | return format(bunTranspiler.transformSync(transpile(config)));
15 | }
16 |
17 | describe("React", () => {
18 | describe("plugin", () => {
19 | it('should not transform if there is not an import attribute with type "prerender"', () => {
20 | const code = `
21 | import Foo from "./components";
22 | import { Bar } from "./components";
23 |
24 | export default function Test() {
25 | return (
26 |
27 |
28 |
29 |
30 | );
31 | }
32 | `;
33 | const output = transpileAndRunMacros({
34 | code,
35 | path: currentFile,
36 | pluginConfig: { prerenderConfigPath: configPath },
37 | prerenderConfig,
38 | });
39 | const expected = format(bunTranspiler.transformSync(code));
40 |
41 | expect(output).toBe(expected);
42 | });
43 | it("should transform a static component", () => {
44 | const code = `
45 | import Foo from "./components" with { type: "prerender" };
46 | import { Bar } from "./components";
47 |
48 | export default function Test() {
49 | return (
50 |
51 |
52 |
53 |
54 | );
55 | }
56 | `;
57 | const output = transpileAndRunMacros({
58 | code,
59 | path: currentFile,
60 | pluginConfig: { prerenderConfigPath: configPath },
61 | prerenderConfig,
62 | });
63 | const expected = format(`
64 | ${importConfig}
65 | import Foo from "./components";
66 | import {Bar} from "./components";
67 |
68 | export default function Test() {
69 | return jsxDEV("div", {children: [
70 | prerenderConfig.postRender(
71 | "Foo, foo!
"
72 | ),
73 | jsxDEV(Bar, {}, undefined, false, undefined, this)]}, undefined, true, undefined, this);
74 | }
75 | `);
76 |
77 | expect(output).toBe(expected);
78 | });
79 |
80 | it("should transform a static component from named export", () => {
81 | const code = `
82 | import { Bar } from "./components" with { type: "prerender" };
83 | import Foo from "./components";
84 |
85 | export default function Test() {
86 | return (
87 |
88 |
89 |
90 |
91 | );
92 | }
93 | `;
94 | const output = transpileAndRunMacros({
95 | code,
96 | path: currentFile,
97 | pluginConfig: { prerenderConfigPath: configPath },
98 | prerenderConfig,
99 | });
100 | const expected = format(`
101 | ${importConfig}
102 | import {Bar} from "./components";
103 | import Foo from "./components";
104 |
105 | export default function Test() {
106 | return jsxDEV("div", {children: [
107 | jsxDEV(Foo, {}, undefined, false, undefined, this),
108 | prerenderConfig.postRender(
109 | "Bar, bar!
"
110 | )
111 | ]}, undefined, true, undefined, this);
112 | }
113 | `);
114 |
115 | expect(output).toBe(expected);
116 | });
117 |
118 | it("should transform a static component from named export and a fragment", () => {
119 | const code = `
120 | import { Bar } from "./components" with { type: "prerender" };
121 | import Foo from "./components";
122 |
123 | export default function Test() {
124 | return (
125 | <>
126 |
127 |
128 | >
129 | );
130 | }
131 | `;
132 | const output = transpileAndRunMacros({
133 | code,
134 | path: currentFile,
135 | pluginConfig: { prerenderConfigPath: configPath },
136 | prerenderConfig,
137 | });
138 | const expected = format(`
139 | ${importConfig}
140 | import {Bar} from "./components";
141 | import Foo from "./components";
142 |
143 | export default function Test() {
144 | return jsxDEV(Fragment, {children: [
145 | jsxDEV(Foo, {}, undefined, false, undefined, this),
146 | prerenderConfig.postRender(
147 | "Bar, bar!
"
148 | )
149 | ]}, undefined, true, undefined, this);
150 | }
151 | `);
152 |
153 | expect(output).toBe(expected);
154 | });
155 |
156 | it("should transform a static component when is not inside JSX", () => {
157 | const code = `
158 | import { Bar } from "./components" with { type: "prerender" };
159 |
160 | export default function Test() {
161 | return ;
162 | }
163 | `;
164 | const output = transpileAndRunMacros({
165 | code,
166 | path: currentFile,
167 | pluginConfig: { prerenderConfigPath: configPath },
168 | prerenderConfig,
169 | });
170 | const expected = format(`
171 | ${importConfig}
172 | import {Bar} from "./components";
173 |
174 | export default function Test() {
175 | return prerenderConfig.postRender(
176 | "Bar, bar!
"
177 | );
178 | }
179 | `);
180 |
181 | expect(output).toBe(expected);
182 | });
183 |
184 | it("should transform a static component with props", () => {
185 | const code = `
186 | import Foo from "./components" with { type: "prerender" };
187 |
188 | export default function Test() {
189 | return ;
190 | }
191 | `;
192 | const output = transpileAndRunMacros({
193 | code,
194 | path: currentFile,
195 | pluginConfig: { prerenderConfigPath: configPath },
196 | prerenderConfig,
197 | });
198 | const expected = format(`
199 | ${importConfig}
200 | import Foo from "./components";
201 |
202 | export default function Test() {
203 | return prerenderConfig.postRender(
204 | "Foo, React works!
"
205 | );
206 | }
207 | `);
208 |
209 | expect(output).toBe(expected);
210 | });
211 | });
212 | });
213 |
--------------------------------------------------------------------------------
/tests/react/prerender.test.tsx:
--------------------------------------------------------------------------------
1 | import { describe, expect, it } from "bun:test";
2 | import { join } from "node:path";
3 | import { prerender } from "prerender-macro/prerender";
4 |
5 | describe("React", () => {
6 | describe("prerender", () => {
7 | it("should work with default export", async () => {
8 | const result = await prerender({
9 | componentPath: join(import.meta.dir, "components.tsx"),
10 | componentModuleName: "default",
11 | componentProps: { name: "React" },
12 | prerenderConfigPath: join(import.meta.dir, "config.tsx"),
13 | });
14 |
15 | expect(result).toBe("Foo, React!
");
16 | });
17 | it("should work with named export", async () => {
18 | const result = await prerender({
19 | componentPath: join(import.meta.dir, "components.tsx"),
20 | componentModuleName: "Bar",
21 | componentProps: { name: "React" },
22 | prerenderConfigPath: join(import.meta.dir, "config.tsx"),
23 | });
24 |
25 | expect(result).toBe("Bar, React!
");
26 | });
27 | });
28 | });
29 |
--------------------------------------------------------------------------------
/tests/react/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": ".",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "module": "esnext",
6 | "target": "esnext",
7 | "moduleResolution": "bundler",
8 | "moduleDetection": "force",
9 | "allowImportingTsExtensions": true,
10 | "noEmit": true,
11 | "composite": true,
12 | "strict": true,
13 | "downlevelIteration": true,
14 | "skipLibCheck": true,
15 | "jsx": "react-jsx",
16 | "jsxImportSource": "react",
17 | "allowSyntheticDefaultImports": true,
18 | "forceConsistentCasingInFileNames": true,
19 | "allowJs": true,
20 | "verbatimModuleSyntax": true,
21 | "noFallthroughCasesInSwitch": true,
22 | "paths": {
23 | "@/*": ["*"]
24 | }
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": "/package",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "module": "esnext",
6 | "target": "esnext",
7 | "moduleResolution": "bundler",
8 | "moduleDetection": "force",
9 | "allowImportingTsExtensions": true,
10 | "noEmit": true,
11 | "composite": true,
12 | "strict": true,
13 | "downlevelIteration": true,
14 | "skipLibCheck": true,
15 | "jsx": "react-jsx",
16 | "jsxImportSource": "brisa",
17 | "allowSyntheticDefaultImports": true,
18 | "forceConsistentCasingInFileNames": true,
19 | "allowJs": true,
20 | "verbatimModuleSyntax": true,
21 | "noFallthroughCasesInSwitch": true,
22 | "paths": {
23 | "@/*": ["*"]
24 | }
25 | }
26 | }
27 |
--------------------------------------------------------------------------------