diff --git a/README.md b/README.md index 89f71ac..babaff6 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,7 @@ - [Brisa](#brisa-experimental) - [React](#react) - [Preact](#preact) + - [Kitajs/html](#kitajshtml) - [Add your framework example](#add-your-framework-example) - [Contributing](#contributing) - [License](#license) @@ -66,7 +67,6 @@ In this way: React example - ### How does it work? This plugin transforms the previous code into this code: @@ -148,11 +148,12 @@ The `prerenderConfig` named export needs this mandatory configuration to work: ## Configuration examples in different frameworks -| Framework | Render ahead of time | Inject ahead of time | Preserves the HTML structure | Demo | -| --------------------------------------------------------------------------- | -------------------- | -------------------- | ---------------------------- | ----------------------- | -|
Brisa
| ✅ | ✅ | ✅ | [🔗](/examples/brisa/) | -|
React
| ✅ | ❌ | ❌ | [🔗](/examples/react/) | -|
Preact
| ✅ | ✅ | ❌ | [🔗](/examples/preact/) | +| Framework | Render ahead of time | Inject ahead of time | Preserves the HTML structure | Demo | +| --------------------------------------------------------------------------- | -------------------- | -------------------- | ---------------------------- | ---------------------------- | +|
Brisa
| ✅ | ✅ | ✅ | [🔗](/examples/brisa/) | +|
React
| ✅ | ❌ | ❌ | [🔗](/examples/react/) | +|
Preact
| ✅ | ✅ | ❌ | [🔗](/examples/preact/) | +|
Kitajs/html
| ✅ | ✅ | ✅ | [🔗](/examples/kitajs-html/) | > [!TIP] > @@ -256,6 +257,31 @@ export const plugin = prerenderMacroPlugin({ > > **Additional `
` Nodes**: Using `dangerouslySetInnerHTML` attribute to inject HTML strings into JSX components results in the creation of an additional `
` 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. +### Kitajs/html + +Configuration example: + +```tsx +import { createElement } from "@kitajs/html"; +import prerenderMacroPlugin, { type PrerenderConfig } from "prerender-macro"; + +export const prerenderConfig = { + render: createElement, +} satisfies PrerenderConfig; + +export const plugin = prerenderMacroPlugin({ + prerenderConfigPath: import.meta.url, +}); +``` + +> [!NOTE] +> +> Kitajs/html elements can be seamlessly coerced with Bun's AST and everything can be done AOT without having to use a `postRender`. + +> [!NOTE] +> +> Kitajs/html does not add extra nodes in the HTML, so it is a prerender of the real component, without modifying its structure. + ### Add your framework example 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. diff --git a/bun.lockb b/bun.lockb index 3eee38f..8bac8e4 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/examples/kitajs-html/README.md b/examples/kitajs-html/README.md new file mode 100644 index 0000000..9793c76 --- /dev/null +++ b/examples/kitajs-html/README.md @@ -0,0 +1,17 @@ +# `prerender-macro` Kitajs/html Example + +This is an example with kitajs-html SSR without hotreloading and with a build process. + +To test it: + +- Clone the repo: `git clone git@github.com:aralroca/prerender-macro.git` +- Install dependencies: `cd prerender-macro && bun install` +- Run demo: `bun run demo:kitajs-html` +- Open http://localhost:1234 to see the result +- Look at `examples/kitajs-html/dist/index.js` to verify how the static parts have been converted to HTML in string. + +The static component is translated to html in string in build-time: + +```tsx +"Static Component \uD83E\uDD76 Random number = 0.41381527597071954"; +``` diff --git a/examples/kitajs-html/package.json b/examples/kitajs-html/package.json new file mode 100644 index 0000000..ca9ddb8 --- /dev/null +++ b/examples/kitajs-html/package.json @@ -0,0 +1,18 @@ +{ + "name": "kitajs-example", + "private": true, + "module": "build.tsx", + "type": "module", + "scripts": { + "start": "bun run src/build.ts && bun run dist/index.js" + }, + "devDependencies": { + "@types/bun": "latest" + }, + "peerDependencies": { + "typescript": "5.4.3" + }, + "dependencies": { + "prerender-macro": "workspace:*" + } +} diff --git a/examples/kitajs-html/src/build.ts b/examples/kitajs-html/src/build.ts new file mode 100644 index 0000000..636131a --- /dev/null +++ b/examples/kitajs-html/src/build.ts @@ -0,0 +1,16 @@ +import { join } from "node:path"; +import prerenderMacro from "prerender-macro"; + +const { success, logs } = await Bun.build({ + entrypoints: [join(import.meta.dir, "index.tsx")], + outdir: join(import.meta.dir, "..", "dist"), + target: "bun", + plugins: [ + prerenderMacro({ + prerenderConfigPath: join(import.meta.dir, "prerender.tsx"), + }), + ], +}); + +if (success) console.log("Build complete ✅"); +else console.error("Build failed ❌", logs); diff --git a/examples/kitajs-html/src/components/dynamic-component.tsx b/examples/kitajs-html/src/components/dynamic-component.tsx new file mode 100644 index 0000000..e892a15 --- /dev/null +++ b/examples/kitajs-html/src/components/dynamic-component.tsx @@ -0,0 +1,7 @@ +export default function DynamicComponent({ name }: { name: string }) { + return ( +
+ {name} Component 🔥 Random number = {Math.random()} +
+ ); +} diff --git a/examples/kitajs-html/src/components/static-component.tsx b/examples/kitajs-html/src/components/static-component.tsx new file mode 100644 index 0000000..a4db70e --- /dev/null +++ b/examples/kitajs-html/src/components/static-component.tsx @@ -0,0 +1,7 @@ +export default function StaticComponent({ name }: { name: string }) { + return ( +
+ {name} Component 🥶 Random number = {Math.random()} +
+ ); +} diff --git a/examples/kitajs-html/src/index.tsx b/examples/kitajs-html/src/index.tsx new file mode 100644 index 0000000..642e685 --- /dev/null +++ b/examples/kitajs-html/src/index.tsx @@ -0,0 +1,27 @@ +import DynamicComponent from "./components/dynamic-component"; +import StaticComponent from "./components/static-component" with { type: "prerender" }; + +Bun.serve({ + port: 1234, + fetch: async (request: Request) => { + const page = await ( + + + Prerender Macro | Brisa example + + + + + + Refresh + + + ); + + return new Response(page, { + headers: new Headers({ "Content-Type": "text/html" }), + }); + }, +}); + +console.log("Server running at http://localhost:1234"); diff --git a/examples/kitajs-html/src/prerender.tsx b/examples/kitajs-html/src/prerender.tsx new file mode 100644 index 0000000..d4938b6 --- /dev/null +++ b/examples/kitajs-html/src/prerender.tsx @@ -0,0 +1,10 @@ +import { createElement } from "@kitajs/html"; +import prerenderMacroPlugin, { type PrerenderConfig } from "prerender-macro"; + +export const prerenderConfig = { + render: createElement, +} satisfies PrerenderConfig; + +export const plugin = prerenderMacroPlugin({ + prerenderConfigPath: import.meta.url, +}); diff --git a/examples/kitajs-html/tsconfig.json b/examples/kitajs-html/tsconfig.json new file mode 100644 index 0000000..925a0bf --- /dev/null +++ b/examples/kitajs-html/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "baseUrl": ".", + "lib": ["dom", "dom.iterable", "esnext"], + "module": "esnext", + "target": "esnext", + "moduleResolution": "bundler", + "moduleDetection": "force", + "jsx": "react-jsx", + "jsxImportSource": "@kitajs/html", + "plugins": [{ "name": "@kitajs/ts-html-plugin" }] + }, + "include": ["src"] +} diff --git a/package.json b/package.json index f9c3417..e242dc1 100644 --- a/package.json +++ b/package.json @@ -6,23 +6,27 @@ "devDependencies": { "react": "18.2.0", "react-dom": "18.2.0", - "@types/bun": "1.0.10", - "@types/react": "18.2.69", + "@types/bun": "1.0.11", + "@types/react": "18.2.72", "@types/react-dom": "18.2.22", - "brisa": "0.0.37", - "preact": "10.20.1" + "brisa": "0.0.38", + "preact": "10.20.1", + "@kitajs/html": "4.0.0-next.3", + "@kitajs/ts-html-plugin": "4.0.0-next.3" }, "dependencies": { "typescript": "5.4.3" }, "scripts": { - "test": "bun run test:brisa && bun run test:react && bun run test:preact", + "test": "bun run test:brisa && bun run test:react && bun run test:preact && bun run test:kitajs-html", "test:brisa": "cd tests/brisa && bun test && cd ../..", "test:react": "cd tests/react && bun test && cd ../..", "test:preact": "cd tests/preact && bun test && cd ../..", + "test:kitajs-html": "cd tests/kitajs-html && bun test && cd ../..", "demo:react": "cd examples/react && bun start", "demo:brisa": "cd examples/brisa && bun start", - "demo:preact": "cd examples/preact && bun start" + "demo:preact": "cd examples/preact && bun start", + "demo:kitajs-html": "cd examples/kitajs-html && bun start" }, "workspaces": [ "package", diff --git a/tests/kitajs-html/components.tsx b/tests/kitajs-html/components.tsx new file mode 100644 index 0000000..c14f224 --- /dev/null +++ b/tests/kitajs-html/components.tsx @@ -0,0 +1,18 @@ +export default function Foo({ + name = "foo", + nested = {}, +}: { + name: string; + nested: { foo?: string }; +}) { + return ( +
+ Foo, {name} + {nested.foo}! +
+ ); +} + +export function Bar({ name = "bar" }: { name: string }) { + return
Bar, {name}!
; +} diff --git a/tests/kitajs-html/config.tsx b/tests/kitajs-html/config.tsx new file mode 100644 index 0000000..d4938b6 --- /dev/null +++ b/tests/kitajs-html/config.tsx @@ -0,0 +1,10 @@ +import { createElement } from "@kitajs/html"; +import prerenderMacroPlugin, { type PrerenderConfig } from "prerender-macro"; + +export const prerenderConfig = { + render: createElement, +} satisfies PrerenderConfig; + +export const plugin = prerenderMacroPlugin({ + prerenderConfigPath: import.meta.url, +}); diff --git a/tests/kitajs-html/package.json b/tests/kitajs-html/package.json new file mode 100644 index 0000000..7df73b6 --- /dev/null +++ b/tests/kitajs-html/package.json @@ -0,0 +1,10 @@ +{ + "name": "test-kitajs-html", + "private": true, + "scripts": { + "test": "bun test" + }, + "dependencies": { + "prerender-macro": "workspace:*" + } +} diff --git a/tests/kitajs-html/plugin.test.tsx b/tests/kitajs-html/plugin.test.tsx new file mode 100644 index 0000000..414ec9b --- /dev/null +++ b/tests/kitajs-html/plugin.test.tsx @@ -0,0 +1,189 @@ +import { join } from "node:path"; +import { describe, it, expect } from "bun:test"; +import { transpile, type TranspilerOptions } from "prerender-macro"; + +const format = (s: string) => s.replace(/\s*\n\s*/g, "").replaceAll("'", '"'); +const configPath = join(import.meta.dir, "config.tsx"); +const currentFile = import.meta.url.replace("file://", ""); +const bunTranspiler = new Bun.Transpiler({ loader: "tsx" }); + +function transpileAndRunMacros(config: TranspilerOptions) { + // Bun transpiler is needed here to run the macros + return format(bunTranspiler.transformSync(transpile(config))); +} + +describe("Kitajs/html", () => { + describe("plugin", () => { + it('should not transform if there is not an import attribute with type "prerender"', () => { + const code = ` + import Foo from "./components"; + import { Bar } from "./components"; + + export default function Test() { + return ( +
+ + +
+ ); + } + `; + const output = transpileAndRunMacros({ + code, + path: currentFile, + pluginConfig: { prerenderConfigPath: configPath }, + }); + const expected = format(bunTranspiler.transformSync(code)); + + expect(output).toBe(expected); + }); + it("should transform a static component", () => { + const code = ` + import Foo from "./components" with { type: "prerender" }; + import { Bar } from "./components"; + + export default function Test() { + return ( +
+ + +
+ ); + } + `; + const output = transpileAndRunMacros({ + code, + path: currentFile, + pluginConfig: { prerenderConfigPath: configPath }, + }); + const expected = format(` + import Foo from "./components"; + import {Bar} from "./components"; + + export default function Test() { + return jsxDEV("div", { + children: ["
Foo, foo!
", + jsxDEV(Bar, {}, undefined, false, undefined, this) + ]}, undefined, true, undefined, this); + } + `); + + expect(output).toBe(expected); + }); + + it("should transform a static component from named export", () => { + const code = ` + import { Bar } from "./components" with { type: "prerender" }; + import Foo from "./components"; + + export default function Test() { + return ( +
+ + +
+ ); + } + `; + const output = transpileAndRunMacros({ + code, + path: currentFile, + pluginConfig: { prerenderConfigPath: configPath }, + }); + const expected = format(` + import {Bar} from "./components"; + import Foo from "./components"; + + export default function Test() { + return jsxDEV("div", { + children: [jsxDEV(Foo, {}, undefined, false, undefined, this), + "
Bar, bar!
" + ]}, undefined, true, undefined, this) + ;} + `); + + expect(output).toBe(expected); + }); + + it("should transform a static component from named export and a fragment", () => { + const code = ` + import { Bar } from "./components" with { type: "prerender" }; + import Foo from "./components"; + + export default function Test() { + return ( + <> + + + + ); + } + `; + const output = transpileAndRunMacros({ + code, + path: currentFile, + pluginConfig: { prerenderConfigPath: configPath }, + }); + const expected = format(` + import {Bar} from "./components"; + import Foo from "./components"; + + export default function Test() { + return jsxDEV(Fragment, {children: [jsxDEV(Foo, {}, undefined, false, undefined, this), + "
Bar, bar!
" + ]}, undefined, true, undefined, this); + } + `); + + expect(output).toBe(expected); + }); + + it("should transform a static component when is not inside JSX", () => { + const code = ` + import { Bar } from "./components" with { type: "prerender" }; + + export default function Test() { + return ; + } + `; + const output = transpileAndRunMacros({ + code, + path: currentFile, + pluginConfig: { prerenderConfigPath: configPath }, + }); + const expected = format(` + import {Bar} from "./components"; + + export default function Test() { + return "
Bar, bar!
"; + } + `); + + expect(output).toBe(expected); + }); + + it("should transform a static component with props", () => { + const code = ` + import Foo from "./components" with { type: "prerender" }; + + export default function Test() { + return ; + } + `; + const output = transpileAndRunMacros({ + code, + path: currentFile, + pluginConfig: { prerenderConfigPath: configPath }, + }); + const expected = format(` + import Foo from "./components"; + + export default function Test() { + return "
Foo, Kitajs/html works!
"; + } + `); + + expect(output).toBe(expected); + }); + }); +}); diff --git a/tests/kitajs-html/prerender.test.tsx b/tests/kitajs-html/prerender.test.tsx new file mode 100644 index 0000000..bc806ff --- /dev/null +++ b/tests/kitajs-html/prerender.test.tsx @@ -0,0 +1,28 @@ +import { join } from "node:path"; +import { describe, expect, it } from "bun:test"; +import { prerender } from "prerender-macro/prerender"; + +describe("Kitajs/html", () => { + describe("prerender", () => { + it("should work with default module", async () => { + const result = await prerender({ + componentPath: join(import.meta.dir, "components.tsx"), + componentModuleName: "default", + componentProps: { name: "Kitajs/html" }, + prerenderConfigPath: join(import.meta.dir, "config.tsx"), + }); + + expect(result).toBe("
Foo, Kitajs/html!
"); + }); + it("should work with named module", async () => { + const result = await prerender({ + componentPath: join(import.meta.dir, "components.tsx"), + componentModuleName: "Bar", + componentProps: { name: "Kitajs/html" }, + prerenderConfigPath: join(import.meta.dir, "config.tsx"), + }); + + expect(result).toBe("
Bar, Kitajs/html!
"); + }); + }); +}); diff --git a/tests/kitajs-html/tsconfig.json b/tests/kitajs-html/tsconfig.json new file mode 100644 index 0000000..b713caf --- /dev/null +++ b/tests/kitajs-html/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "baseUrl": ".", + "lib": ["dom", "dom.iterable", "esnext"], + "module": "esnext", + "target": "esnext", + "moduleResolution": "bundler", + "moduleDetection": "force", + "jsx": "react-jsx", + "jsxImportSource": "@kitajs/html", + "plugins": [{ "name": "@kitajs/ts-html-plugin" }] + } +}