├── .github └── workflows │ └── test.yml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── bun.lockb ├── examples ├── brisa │ ├── README.md │ ├── package.json │ ├── src │ │ ├── build.ts │ │ ├── components │ │ │ ├── dynamic-component.tsx │ │ │ └── static-component.tsx │ │ ├── index.tsx │ │ └── prerender.tsx │ └── tsconfig.json ├── kitajs-html │ ├── README.md │ ├── package.json │ ├── src │ │ ├── build.ts │ │ ├── components │ │ │ ├── dynamic-component.tsx │ │ │ └── static-component.tsx │ │ ├── index.tsx │ │ └── prerender.tsx │ └── tsconfig.json ├── preact │ ├── README.md │ ├── package.json │ ├── src │ │ ├── build.ts │ │ ├── components │ │ │ ├── dynamic-component.tsx │ │ │ └── static-component.tsx │ │ ├── index.tsx │ │ └── prerender.tsx │ └── tsconfig.json └── react │ ├── README.md │ ├── package.json │ ├── src │ ├── build.ts │ ├── components │ │ ├── dynamic-component.tsx │ │ └── static-component.tsx │ ├── index.tsx │ └── prerender.tsx │ └── tsconfig.json ├── package.json ├── package ├── index.ts ├── package.json ├── prerender.tsx └── tsconfig.json ├── tests ├── brisa │ ├── components.tsx │ ├── config.tsx │ ├── package.json │ ├── plugin.test.tsx │ ├── prerender.test.tsx │ └── tsconfig.json ├── kitajs-html │ ├── components.tsx │ ├── config.tsx │ ├── package.json │ ├── plugin.test.tsx │ ├── prerender.test.tsx │ └── tsconfig.json ├── preact │ ├── components.tsx │ ├── config.tsx │ ├── package.json │ ├── plugin.test.tsx │ ├── prerender.test.tsx │ └── tsconfig.json └── react │ ├── components.tsx │ ├── config.tsx │ ├── package.json │ ├── plugin.test.tsx │ ├── prerender.test.tsx │ └── tsconfig.json └── tsconfig.json /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v3 15 | - name: Bunjs 16 | uses: oven-sh/setup-bun@v1 17 | with: 18 | bun-version: 1.0.33 19 | - name: Install dependencies 20 | run: bun install 21 | #- run: bun run build 22 | - run: bun run test 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore 2 | 3 | # Logs 4 | 5 | logs 6 | _.log 7 | npm-debug.log_ 8 | yarn-debug.log* 9 | yarn-error.log* 10 | lerna-debug.log* 11 | .pnpm-debug.log* 12 | 13 | # Caches 14 | 15 | .cache 16 | 17 | # Diagnostic reports (https://nodejs.org/api/report.html) 18 | 19 | report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json 20 | 21 | # Runtime data 22 | 23 | pids 24 | _.pid 25 | _.seed 26 | *.pid.lock 27 | 28 | # Directory for instrumented libs generated by jscoverage/JSCover 29 | 30 | lib-cov 31 | 32 | # Coverage directory used by tools like istanbul 33 | 34 | coverage 35 | *.lcov 36 | 37 | # nyc test coverage 38 | 39 | .nyc_output 40 | 41 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 42 | 43 | .grunt 44 | 45 | # Bower dependency directory (https://bower.io/) 46 | 47 | bower_components 48 | 49 | # node-waf configuration 50 | 51 | .lock-wscript 52 | 53 | # Compiled binary addons (https://nodejs.org/api/addons.html) 54 | 55 | build/Release 56 | 57 | # Dependency directories 58 | 59 | node_modules/ 60 | jspm_packages/ 61 | 62 | # Snowpack dependency directory (https://snowpack.dev/) 63 | 64 | web_modules/ 65 | 66 | # TypeScript cache 67 | 68 | *.tsbuildinfo 69 | 70 | # Optional npm cache directory 71 | 72 | .npm 73 | 74 | # Optional eslint cache 75 | 76 | .eslintcache 77 | 78 | # Optional stylelint cache 79 | 80 | .stylelintcache 81 | 82 | # Microbundle cache 83 | 84 | .rpt2_cache/ 85 | .rts2_cache_cjs/ 86 | .rts2_cache_es/ 87 | .rts2_cache_umd/ 88 | 89 | # Optional REPL history 90 | 91 | .node_repl_history 92 | 93 | # Output of 'npm pack' 94 | 95 | *.tgz 96 | 97 | # Yarn Integrity file 98 | 99 | .yarn-integrity 100 | 101 | # dotenv environment variable files 102 | 103 | .env 104 | .env.development.local 105 | .env.test.local 106 | .env.production.local 107 | .env.local 108 | 109 | # parcel-bundler cache (https://parceljs.org/) 110 | 111 | .parcel-cache 112 | 113 | # Next.js build output 114 | 115 | .next 116 | out 117 | 118 | # Nuxt.js build / generate output 119 | 120 | .nuxt 121 | dist 122 | 123 | # Gatsby files 124 | 125 | # Comment in the public line in if your project uses Gatsby and not Next.js 126 | 127 | # https://nextjs.org/blog/next-9-1#public-directory-support 128 | 129 | # public 130 | 131 | # vuepress build output 132 | 133 | .vuepress/dist 134 | 135 | # vuepress v2.x temp and cache directory 136 | 137 | .temp 138 | 139 | # Docusaurus cache and generated files 140 | 141 | .docusaurus 142 | 143 | # Serverless directories 144 | 145 | .serverless/ 146 | 147 | # FuseBox cache 148 | 149 | .fusebox/ 150 | 151 | # DynamoDB Local files 152 | 153 | .dynamodb/ 154 | 155 | # TernJS port file 156 | 157 | .tern-port 158 | 159 | # Stores VSCode versions used for testing VSCode extensions 160 | 161 | .vscode-test 162 | 163 | # yarn v2 164 | 165 | .yarn/cache 166 | .yarn/unplugged 167 | .yarn/build-state.yml 168 | .yarn/install-state.gz 169 | .pnp.* 170 | 171 | # IntelliJ based IDEs 172 | .idea 173 | 174 | # Finder (MacOS) folder config 175 | .DS_Store 176 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | - Using welcoming and inclusive language 12 | - Being respectful of differing viewpoints and experiences 13 | - Gracefully accepting constructive criticism 14 | - Focusing on what is best for the community 15 | - Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | - The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | - Trolling, insulting/derogatory comments, and personal or political attacks 21 | - Public or private harassment 22 | - Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | - Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at contact@aralroca.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | When contributing to this repository, please first discuss the change you wish to make via issue, 4 | email, or any other method with the owners of this repository before making a change. 5 | 6 | Please note we have a code of conduct, please follow it in all your interactions with the project. 7 | 8 | ## Pull Request Process 9 | 10 | 1. Ensure you are doing the PR to the canary branch. 11 | 2. Write the failing tests about the issue / feature you are working on. 12 | 3. Update the README.md with details of changes to the interface. 13 | 4. You may merge the Pull Request in once you have the approval of at least one maintainer, or if you 14 | do not have permission to do that, you may request the maintainer to merge it for you. 15 | 16 | ## Code of Conduct 17 | 18 | ### Our Pledge 19 | 20 | In the interest of fostering an open and welcoming environment, we as 21 | contributors and maintainers pledge to making participation in our project and 22 | our community a harassment-free experience for everyone, regardless of age, body 23 | size, disability, ethnicity, gender identity and expression, level of experience, 24 | nationality, personal appearance, race, religion, or sexual identity and 25 | orientation. 26 | 27 | ### Our Standards 28 | 29 | Examples of behavior that contributes to creating a positive environment 30 | include: 31 | 32 | - Using welcoming and inclusive language 33 | - Being respectful of differing viewpoints and experiences 34 | - Gracefully accepting constructive criticism 35 | - Focusing on what is best for the community 36 | - Showing empathy towards other community members 37 | 38 | Examples of unacceptable behavior by participants include: 39 | 40 | - The use of sexualized language or imagery and unwelcome sexual attention or 41 | advances 42 | - Trolling, insulting/derogatory comments, and personal or political attacks 43 | - Public or private harassment 44 | - Publishing others' private information, such as a physical or electronic 45 | address, without explicit permission 46 | - Other conduct which could reasonably be considered inappropriate in a 47 | professional setting 48 | 49 | ### Our Responsibilities 50 | 51 | Project maintainers are responsible for clarifying the standards of acceptable 52 | behavior and are expected to take appropriate and fair corrective action in 53 | response to any instances of unacceptable behavior. 54 | 55 | Project maintainers have the right and responsibility to remove, edit, or 56 | reject comments, commits, code, wiki edits, issues, and other contributions 57 | that are not aligned to this Code of Conduct, or to ban temporarily or 58 | permanently any contributor for other behaviors that they deem inappropriate, 59 | threatening, offensive, or harmful. 60 | 61 | ### Scope 62 | 63 | This Code of Conduct applies both within project spaces and in public spaces 64 | when an individual is representing the project or its community. Examples of 65 | representing a project or community include using an official project e-mail 66 | address, posting via an official social media account, or acting as an appointed 67 | representative at an online or offline event. Representation of a project may be 68 | further defined and clarified by project maintainers. 69 | 70 | ### Enforcement 71 | 72 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 73 | reported by contacting the project team at contact@aralroca.com. All 74 | complaints will be reviewed and investigated and will result in a response that 75 | is deemed necessary and appropriate to the circumstances. The project team is 76 | obligated to maintain confidentiality with regard to the reporter of an incident. 77 | Further details of specific enforcement policies may be posted separately. 78 | 79 | Project maintainers who do not follow or enforce the Code of Conduct in good 80 | faith may face temporary or permanent repercussions as determined by other 81 | members of the project's leadership. 82 | 83 | ### Attribution 84 | 85 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 86 | available at [http://contributor-covenant.org/version/1/4][version] 87 | 88 | [homepage]: http://contributor-covenant.org 89 | [version]: http://contributor-covenant.org/version/1/4/ 90 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | MIT License 4 | 5 | Copyright (c) 2024 Aral Roca Gomez 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | SOFTWARE. 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | 4 | 5 | 6 |

Prerender Macro

7 |

8 | 9 |

Bun plugin to prerender JSX components using a kind of macro.

10 | 11 |
12 | 13 | [![npm version](https://badge.fury.io/js/prerender-macro.svg)](https://badge.fury.io/js/prerender-macro) 14 | ![npm](https://img.shields.io/npm/dw/prerender-macro) 15 | [![PRs Welcome][badge-prwelcome]][prwelcome] 16 | 17 | 18 | 19 | follow on Twitter 21 | 22 |
23 | 24 | [badge-prwelcome]: https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square 25 | [prwelcome]: http://makeapullrequest.com 26 | 27 |

Work in every JSX Framework.

28 | 29 | - [At a glance](#at-a-glance) 30 | - [How does it work?](#how-does-it-work) 31 | - [Quick Start](#quick-start) 32 | - [Install](#install) 33 | - [Use it in `Bun.build`](#use-it-in-bunbuild) 34 | - [Configuration](#configuration) 35 | - [Configuration examples in different frameworks](#configuration-examples-in-different-frameworks) 36 | - [Brisa](#brisa-experimental) 37 | - [React](#react) 38 | - [Preact](#preact) 39 | - [Kitajs/html](#kitajshtml) 40 | - [Add your framework example](#add-your-framework-example) 41 | - [Contributing](#contributing) 42 | - [License](#license) 43 | 44 | ## At a glance 45 | 46 | `prerender-macro` plugin allows [Partial Prerendering](https://aralroca.com/blog/partial-prerendering) (PPR) to make hybrid pages between dynamic and static components, avoiding the rendering in runtime of the static ones, this rendering is done in build-time thanks to Bun's macros. 47 | 48 | ```tsx 49 | import StaticComponent from "@/static-component" with { type: "prerender" }; 50 | import DynamicComponent from "@/dynamic-component"; 51 | 52 | // ... 53 | return ( 54 | <> 55 | 56 | 57 | 58 | ); 59 | ``` 60 | 61 | In this way: 62 | 63 | - The bundle is smaller because instead of carrying all the JS it only carries the prerendered HTML. 64 | - The runtime speed of rendering is faster, it only has to render the dynamic components. 65 | 66 |
67 | React example 68 |
69 | 70 | ### How does it work? 71 | 72 | This plugin transforms the previous code into this code: 73 | 74 | ```tsx 75 | import { prerender } from "prerender-macro/prerender" with { "type": "macro" }; 76 | import DynamicComponent from "@/dynamic-component"; 77 | 78 | // ... 79 | return ( 80 | <> 81 | {prerender({ 82 | componentPath: "@/static-component", 83 | componentModuleName: "default", 84 | componentProps: { foo: "bar" }, 85 | })} 86 | 87 | 88 | ); 89 | ``` 90 | 91 | And pass it back through the [Bun transpiler](https://bun.sh/docs/api/transpiler) to run the macro. [Bun macro](https://bun.sh/docs/bundler/macros#:~:text=Bun%20Macros%20are%20import%20statements,number%20of%20browsers%20and%20runtimes) together with the prerender helper takes care of converting the component to html `string` in build-time. This way it will only be necessary in runtime to make the rendering of those dynamic. 92 | 93 | > [!IMPORTANT] 94 | > 95 | > Macros can accept **component properties**, but only in limited cases. The value must be **statically known**. For more info take a look at the [Bun Macros Arguments](https://bun.sh/docs/bundler/macros#arguments) documentation. 96 | 97 | ## Quick start 98 | 99 | ### Install 100 | 101 | ```sh 102 | bun install prerender-macro 103 | ``` 104 | 105 | ### Use it in `Bun.build` 106 | 107 | To use it you have to set the `prerenderConfigPath` (**mandatory**), which is the path where you have the configuration export, if it is in the same file you can use `import.meta.url`. 108 | 109 | ```tsx 110 | import prerenderMacroPlugin from "prerender-macro"; 111 | 112 | // The configuration should be adapted to the framework that you are using: 113 | export const prerenderConfig = { 114 | render: (Component, props) => /* mandatory */, 115 | postRender: () => /* optional */ 116 | }; 117 | 118 | Bun.build({ 119 | plugins: [prerenderMacroPlugin({ prerenderConfigPath: import.meta.url })], 120 | entrypoints, 121 | outdir, 122 | root, 123 | }); 124 | ``` 125 | 126 | ## Configuration 127 | 128 | The `prerender-macro` plugin needs this mandatory configuration to work: 129 | 130 | | Parameter | Description | Mandatory | 131 | | --------------------- | --------------------------------------------------------------- | --------- | 132 | | `prerenderConfigPath` | String path of the file with the `prerenderConfig` named export | `true` | 133 | 134 | The configuration can be in another file, but it must have the named export `prerenderConfig`. 135 | 136 | It is necessary to do it this way because this configuration will be executed when doing the prerender inside a Bun macro, and at this point we cannot pass it from the plugin because it would need to be serialized, so it is better that you directly access it. 137 | 138 | The `prerenderConfig` named export needs this mandatory configuration to work: 139 | 140 | | Parameter | Description | Mandatory | Can be async | 141 | | ------------ | ------------------------------------------------------------------------------------------------------------------- | --------- | ------------ | 142 | | `render` | Function to render the component on your framework ([AOT](https://en.wikipedia.org/wiki/Ahead-of-time_compilation)) | `true` | `yes` | 143 | | `postRender` | Function to make a post rendering in runtime ([JIT](https://en.wikipedia.org/wiki/Just-in-time_compilation)) | `false` | `no` | 144 | 145 | > [!NOTE] 146 | > 147 | > It is not necessary to indicate the `jsx-runtime`, it will work with the one you have and it can connect with **any JSX framework**. 148 | 149 | ## Configuration examples in different frameworks 150 | 151 | | Framework | Render ahead of time | Inject ahead of time | Preserves the HTML structure | Demo | 152 | | --------------------------------------------------------------------------- | -------------------- | -------------------- | ---------------------------- | ---------------------------- | 153 | |
Brisa
| ✅ | ✅ | ✅ | [🔗](/examples/brisa/) | 154 | |
React
| ✅ | ❌ | ❌ | [🔗](/examples/react/) | 155 | |
Preact
| ✅ | ✅ | ❌ | [🔗](/examples/preact/) | 156 | |
Kitajs/html
| ✅ | ✅ | ✅ | [🔗](/examples/kitajs-html/) | 157 | 158 | > [!TIP] 159 | > 160 | > 👉 [Add your framework](#add-your-framework-example) 161 | 162 | ### Brisa _(experimental)_ 163 | 164 | Configuration example: 165 | 166 | ```tsx 167 | import prerenderMacroPlugin, { type PrerenderConfig } from "prerender-macro"; 168 | import { dangerHTML } from "brisa"; 169 | import { renderToString } from "brisa/server"; 170 | 171 | export const prerenderConfig = { 172 | render: async (Component, props) => 173 | dangerHTML(await renderToString()), 174 | } satisfies PrerenderConfig; 175 | 176 | export const plugin = prerenderMacroPlugin({ 177 | prerenderConfigPath: import.meta.url, 178 | }); 179 | ``` 180 | 181 | > [!NOTE] 182 | > 183 | > Brisa elements can be seamlessly coerced with Bun's AST and everything can be done AOT without having to use a `postRender`. 184 | 185 | > [!NOTE] 186 | > 187 | > Brisa does not add extra nodes in the HTML, so it is a prerender of the real component, without modifying its structure. 188 | 189 | > [!WARNING] 190 | > 191 | > Brisa is an _experimental_ framework that we are building. 192 | 193 | Brisa is not yet public but it will be in the next months. If you want to be updated, subscribe to my [blog newsletter](https://aralroca.com/blog). 194 | 195 | ### React 196 | 197 | For React components, since React does not have a built-in function for injecting HTML strings directly into JSX, you need to use `dangerouslySetInnerHTML`. This allows you to bypass React's default behavior and inject raw HTML into the DOM. 198 | 199 | Configuration example: 200 | 201 | ```tsx 202 | import prerenderMacroPlugin, { type PrerenderConfig } from "prerender-macro"; 203 | import { renderToString } from "react-dom/server"; 204 | 205 | export const prerenderConfig = { 206 | render: async (Component, props) => { 207 | return renderToString(); 208 | }, 209 | postRender: (htmlString) => ( 210 |
211 | ), 212 | } satisfies PrerenderConfig; 213 | 214 | export const plugin = prerenderMacroPlugin({ 215 | prerenderConfigPath: import.meta.url, 216 | }); 217 | ``` 218 | 219 | > [!IMPORTANT] 220 | > 221 | > React elements have the `$$typeof` symbol and therefore cannot coerce to Bun's AST. This is why it is necessary to do the `postRender` in [JIT](https://en.wikipedia.org/wiki/Just-in-time_compilation). 222 | 223 | > [!CAUTION] 224 | > 225 | > **Additional `
` Nodes**: Using `dangerouslySetInnerHTML` 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. 226 | 227 | ### Preact 228 | 229 | For Preact components, since Preact does not have a built-in function for injecting HTML strings directly into JSX, you need to use `dangerouslySetInnerHTML`. This allows you to bypass Preact's default behavior and inject raw HTML into the DOM. 230 | 231 | Configuration example: 232 | 233 | ```tsx 234 | import prerenderMacroPlugin, { type PrerenderConfig } from "prerender-macro"; 235 | import { render } from "preact-render-to-string"; 236 | 237 | export const prerenderConfig = { 238 | render: async (Component, props) => { 239 | return ( 240 |
) }} 242 | /> 243 | ); 244 | }, 245 | } satisfies PrerenderConfig; 246 | 247 | export const plugin = prerenderMacroPlugin({ 248 | prerenderConfigPath: import.meta.url, 249 | }); 250 | ``` 251 | 252 | > [!NOTE] 253 | > 254 | > Preact elements can be seamlessly coerced with Bun's AST and everything can be done AOT without having to use a `postRender`. 255 | 256 | > [!CAUTION] 257 | > 258 | > **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. 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 | --------------------------------------------------------------------------------