├── .github └── workflows │ ├── lint.yml │ ├── main.yml │ └── npm-publish.yml ├── .gitignore ├── .npmignore ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── biome.json ├── build.ts ├── bun.lockb ├── bunfig.toml ├── examples └── tailwind │ ├── build.ts │ ├── bun.lockb │ ├── package.json │ ├── serve.ts │ ├── src │ ├── index.html │ ├── main.ts │ └── styles.css │ └── tailwind.config.ts ├── package.json ├── src ├── index.ts └── utils.ts ├── test ├── css.test.ts ├── css │ ├── import.css │ ├── index.html │ └── main.css ├── custom-extension.test.ts ├── exclude-extensions.test.ts ├── exclude-selector.test.ts ├── expected │ ├── css │ │ └── index.html │ ├── custom-extension │ │ ├── images │ │ │ └── shubham-dhage-unsplash.jpg │ │ ├── index.html │ │ ├── js │ │ │ └── build-custom.js │ │ └── main.js │ ├── exclude-extensions │ │ ├── images │ │ │ └── shubham-dhage-unsplash.jpg │ │ ├── index.html │ │ ├── js │ │ │ └── build-custom.cljs │ │ └── main.js │ ├── exclude-selector │ │ ├── images │ │ │ └── shubham-dhage-unsplash.jpg │ │ ├── index.html │ │ ├── js │ │ │ ├── build-custom.cljs │ │ │ └── secondary.js │ │ └── main.js │ ├── html │ │ ├── images │ │ │ ├── favicon.ico │ │ │ └── shubham-dhage-unsplash.jpg │ │ ├── index.html │ │ ├── js │ │ │ ├── build-custom.cljs │ │ │ └── secondary.js │ │ ├── main.css │ │ ├── main.js │ │ └── tailwind.css │ ├── inline-css │ │ ├── images │ │ │ ├── favicon.ico │ │ │ └── shubham-dhage-unsplash.jpg │ │ ├── index.html │ │ ├── js │ │ │ ├── build-custom.cljs │ │ │ └── secondary.js │ │ └── main.js │ ├── inline-js │ │ ├── images │ │ │ ├── favicon.ico │ │ │ └── shubham-dhage-unsplash.jpg │ │ ├── index.html │ │ ├── js │ │ │ └── build-custom.cljs │ │ ├── main.css │ │ └── tailwind.css │ ├── inline-minify │ │ ├── images │ │ │ ├── favicon.ico │ │ │ └── shubham-dhage-unsplash.jpg │ │ ├── index.html │ │ └── js │ │ │ └── build-custom.cljs │ ├── inline │ │ ├── images │ │ │ ├── favicon.ico │ │ │ └── shubham-dhage-unsplash.jpg │ │ ├── index.html │ │ └── js │ │ │ └── build-custom.cljs │ ├── minify-custom-options │ │ ├── images │ │ │ ├── favicon.ico │ │ │ └── shubham-dhage-unsplash.jpg │ │ ├── index.html │ │ ├── js │ │ │ ├── build-custom.cljs │ │ │ └── secondary.js │ │ ├── main.css │ │ ├── main.js │ │ └── tailwind.css │ ├── minify-skip-html │ │ ├── images │ │ │ ├── favicon.ico │ │ │ └── shubham-dhage-unsplash.jpg │ │ ├── index.html │ │ ├── js │ │ │ ├── build-custom.cljs │ │ │ └── secondary.js │ │ ├── main.css │ │ ├── main.js │ │ └── tailwind.css │ ├── minify │ │ ├── images │ │ │ ├── favicon.ico │ │ │ └── shubham-dhage-unsplash.jpg │ │ ├── index.html │ │ ├── js │ │ │ ├── build-custom.cljs │ │ │ └── secondary.js │ │ ├── main.css │ │ ├── main.js │ │ └── tailwind.css │ ├── naming │ │ ├── assets │ │ │ ├── build-custom.cljs │ │ │ ├── favicon.ico │ │ │ └── shubham-dhage-unsplash.jpg │ │ ├── chunks │ │ │ ├── js │ │ │ │ └── secondary-mv47jd7p.js │ │ │ └── main-vfbc17q6.js │ │ ├── css │ │ │ ├── main-1234.css │ │ │ └── tailwind-1234.css │ │ └── main.html │ ├── preprocessor │ │ ├── hello.txt │ │ ├── images │ │ │ ├── favicon.ico │ │ │ └── shubham-dhage-unsplash.jpg │ │ ├── index.html │ │ ├── js │ │ │ ├── build-custom.cljs │ │ │ ├── secondary.js │ │ │ └── third.js │ │ ├── main.css │ │ ├── main.js │ │ └── tailwind.css │ ├── resolution-inner │ │ ├── index.js │ │ └── inner │ │ │ └── index.html │ └── resolution │ │ ├── index.html │ │ └── index.js ├── html.test.ts ├── inline-css.test.ts ├── inline-js.test.ts ├── inline-minify.test.ts ├── inline.test.ts ├── keep-path-strings.test.ts ├── minify-custom-options.test.ts ├── minify-skip-html.test.ts ├── minify.test.ts ├── naming.test.ts ├── preprocessor.test.ts ├── resolution-inner.test.ts ├── resolution.test.ts ├── resolution │ ├── index.html │ ├── index.ts │ ├── inner │ │ └── index.html │ └── moduleA.ts ├── splitting.test.ts ├── splitting │ ├── index.html │ ├── keep-path-strings.html │ ├── style.scss │ ├── x.ts │ └── y.ts ├── starting │ ├── images │ │ ├── favicon.ico │ │ └── shubham-dhage-unsplash.jpg │ ├── index.html │ ├── js │ │ ├── build-custom.cljs │ │ ├── index.ts │ │ └── secondary.tsx │ ├── main.css │ ├── main.ts │ └── tailwind.css └── utils.ts └── tsconfig.json /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Code quality 2 | 3 | on: 4 | push: 5 | pull_request: 6 | 7 | jobs: 8 | quality: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout 12 | uses: actions/checkout@v4 13 | - name: Setup Biome 14 | uses: biomejs/setup-biome@v2 15 | with: 16 | version: latest 17 | - name: Run Biome 18 | run: biome ci . 19 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | # This workflow will run tests using node and then publish a package to GitHub Packages when a release is created 2 | # For more information see: https://docs.github.com/en/actions/publishing-packages/publishing-nodejs-packages 3 | 4 | name: Test 5 | 6 | on: 7 | push: 8 | branches: [ main ] 9 | pull_request: 10 | branches: [ main ] 11 | 12 | env: 13 | FORCE_COLOR: 1 14 | 15 | jobs: 16 | test: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: actions/checkout@v3 20 | - uses: oven-sh/setup-bun@v1 21 | with: 22 | bun-version: latest 23 | - run: bun install 24 | - run: bun run build 25 | - run: bun test 26 | -------------------------------------------------------------------------------- /.github/workflows/npm-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflow will run tests using node and then publish a package to GitHub Packages when a release is created 2 | # For more information see: https://docs.github.com/en/actions/publishing-packages/publishing-nodejs-packages 3 | 4 | name: Node.js Package 5 | 6 | on: 7 | release: 8 | types: [created] 9 | 10 | env: 11 | FORCE_COLOR: 1 12 | 13 | jobs: 14 | build: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v3 18 | - uses: oven-sh/setup-bun@v1 19 | with: 20 | bun-version: latest 21 | - run: bun install 22 | - run: bun test 23 | 24 | publish-npm: 25 | needs: build 26 | runs-on: ubuntu-latest 27 | steps: 28 | - uses: actions/checkout@v3 29 | - uses: actions/setup-node@v3 30 | with: 31 | node-version: '22.x' 32 | registry-url: 'https://registry.npmjs.org' 33 | - uses: oven-sh/setup-bun@v1 34 | with: 35 | bun-version: latest 36 | - run: bun install 37 | - run: bun run build 38 | - run: npm publish 39 | env: 40 | NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} 41 | -------------------------------------------------------------------------------- /.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 | # Diagnostic reports (https://nodejs.org/api/report.html) 14 | 15 | report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json 16 | 17 | # Runtime data 18 | 19 | pids 20 | _.pid 21 | _.seed 22 | \*.pid.lock 23 | 24 | # Directory for instrumented libs generated by jscoverage/JSCover 25 | 26 | lib-cov 27 | 28 | # Coverage directory used by tools like istanbul 29 | 30 | coverage 31 | \*.lcov 32 | 33 | # nyc test coverage 34 | 35 | .nyc_output 36 | 37 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 38 | 39 | .grunt 40 | 41 | # Bower dependency directory (https://bower.io/) 42 | 43 | bower_components 44 | 45 | # node-waf configuration 46 | 47 | .lock-wscript 48 | 49 | # Compiled binary addons (https://nodejs.org/api/addons.html) 50 | 51 | build/Release 52 | 53 | # Dependency directories 54 | 55 | node_modules/ 56 | jspm_packages/ 57 | 58 | # Snowpack dependency directory (https://snowpack.dev/) 59 | 60 | web_modules/ 61 | 62 | # TypeScript cache 63 | 64 | \*.tsbuildinfo 65 | 66 | # Optional npm cache directory 67 | 68 | .npm 69 | 70 | # Optional eslint cache 71 | 72 | .eslintcache 73 | 74 | # Optional stylelint cache 75 | 76 | .stylelintcache 77 | 78 | # Microbundle cache 79 | 80 | .rpt2_cache/ 81 | .rts2_cache_cjs/ 82 | .rts2_cache_es/ 83 | .rts2_cache_umd/ 84 | 85 | # Optional REPL history 86 | 87 | .node_repl_history 88 | 89 | # Output of 'npm pack' 90 | 91 | \*.tgz 92 | 93 | # Yarn Integrity file 94 | 95 | .yarn-integrity 96 | 97 | # dotenv environment variable files 98 | 99 | .env 100 | .env.development.local 101 | .env.test.local 102 | .env.production.local 103 | .env.local 104 | 105 | # parcel-bundler cache (https://parceljs.org/) 106 | 107 | .cache 108 | .parcel-cache 109 | 110 | # Next.js build output 111 | 112 | .next 113 | out 114 | 115 | # Nuxt.js build / generate output 116 | 117 | .nuxt 118 | dist 119 | 120 | # Gatsby files 121 | 122 | .cache/ 123 | 124 | # Comment in the public line in if your project uses Gatsby and not Next.js 125 | 126 | # https://nextjs.org/blog/next-9-1#public-directory-support 127 | 128 | # public 129 | 130 | # vuepress build output 131 | 132 | .vuepress/dist 133 | 134 | # vuepress v2.x temp and cache directory 135 | 136 | .temp 137 | .cache 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 | **/dist/** 174 | **/generation/** 175 | **/.DS_Store 176 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src/* 2 | **/.DS_Store 3 | test/* 4 | node_modules/* 5 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to bun-plugin-html 2 | 3 | Thank you for your interest in contributing to `bun-plugin-html`! We welcome all contributions, whether they're bug fixes, new features, documentation improvements, or general feedback. 4 | 5 | To get started contributing to `bun-plugin-html`, please follow the guidelines below. 6 | 7 | ## Getting started 8 | 9 | ### Prerequisites 10 | Ensure that you have the following installed 11 | * [Git](https://git-scm.com/) 12 | * [Bun](https://bun.sh/) (version 1.1.34 or later) 13 | 14 | ### Setup 15 | 1. Fork the repository 16 | 2. Clone your fork to your machine 17 | 3. Install dependencies with `bun install` 18 | 4. Install the [`Biome`](https://biomejs.dev/guides/integrate-in-editor/) LSP for your editor 19 | 20 | ### Developing 21 | You're now ready to start developing! Just make your desired changes to the repository, and run the code by writing a new or running an existing test, to try out your changes! 22 | 23 | > [!CAUTION] 24 | > Some tests may fail automatically for you, if you do not have the latest version of bun installed before hand, or if the tests have not been updated to work with the latest version of Bun! 25 | > If this happens upgrade bun with `bun upgrade`, and if the issue persists, attempt overwriting the `test/expected` directory with the `test/generation` directory. 26 | 27 | Ensure that if you're adding another feature to always add a new test. 28 | 29 | After you are finished with all of your changes, and you wish to make a pull request, first ensure all tests are passing when running `bun run test`, then run `bun run lint` to see if there are any linting issues. You can automatically fix some of the issues with `bun run lint:fix`. 30 | 31 | That's it! Just create a pull request with your changes, and we'll try to review it quickly! 32 | 33 | ## Reporting Issues 34 | If you encounter a bug or have a suggestion, please [create an issue](https://github.com/BjornTheProgrammer/bun-plugin-html/issues/new). Provide as much detail as possible, including: 35 | 36 | * Steps to reproduce the issue. 37 | * Expected behavior vs. actual behavior. 38 | * Any relevant error messages or logs. 39 | 40 | When suggesting enhancements, describe the use case and how it would benefit users 41 | 42 | ## License 43 | By contributing to `bun-plugin-html`, you agree that your contributions will be licensed under the same MIT License as the project unless explicitly stated otherwise. 44 | 45 | Thank you for helping improve the Bun Plugin for HTML! 🎉 46 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Bjorn Beishline 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Bun Plugin for HTML 2 | 3 | The `bun-plugin-html` is a plugin for the Bun build tool that enables `.html` file entrypoints. This document instructions on how to install, use, and configure the plugin. 4 | 5 | > [!IMPORTANT] 6 | > With bun v1.2 the html loader will be [stabilized](https://github.com/oven-sh/bun/issues/4688). This plugin will still be updated if any issues occur, 7 | > but it is recommended that you use the built-in loader. 8 | 9 | ## Installation 10 | 11 | You can install `bun-plugin-html` using the following command: 12 | 13 | ```bash 14 | bun add -d bun-plugin-html 15 | ``` 16 | 17 | Ensure Bun is upgraded to `v1.1.34`, as a bug fix was introduced in this version of Bun. 18 | 19 | ## Usage 20 | 21 | To use this plugin, import it into your code and add it to the list of plugins when building your project with Bun. Here's an example: 22 | 23 | ```typescript 24 | import html from 'bun-plugin-html'; 25 | 26 | await Bun.build({ 27 | entrypoints: ['./src/index.html', './src/other.html'], 28 | outdir: './dist', // Specify the output directory 29 | plugins: [ 30 | html() 31 | ], 32 | }); 33 | ``` 34 | 35 | This code snippet builds HTML files from the specified entrypoints and places them in the specified output directory, along with their associated scripts and links. 36 | 37 | ### Input 38 | 39 | Here is an example of an HTML file (`index.html`) that serves as an input: 40 | 41 | ```html 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | Hello World! 50 | 51 | 52 |

Hello World

53 |

This should be changed by JS

54 | 55 | 56 | 57 | ``` 58 | 59 | Along with a file structure like the one below, the plugin generates the output as described: 60 | 61 | ``` 62 | . 63 | └── src/ 64 | ├── index.html 65 | ├── main.css 66 | ├── style.scss 67 | ├── main.ts 68 | ├── js/ 69 | │ └── secondary.ts 70 | └── images/ 71 | └── favicon.ico 72 | ``` 73 | 74 | ### Output 75 | 76 | The plugin generates the output in the specified output directory. If certain files are missing, the console will indicate the issue while generating the rest of the files. The generated output would look like this: 77 | 78 | ``` 79 | . 80 | └── src/ 81 | └── ... 82 | └── dist/ 83 | ├── index.html 84 | ├── main.css 85 | ├── style.scss 86 | ├── main.js 87 | ├── js/ 88 | │ └── secondary.js 89 | └── images/ 90 | └── favicon.ico 91 | ``` 92 | 93 | Here's the transformed HTML file in the output directory (`dist/index.html`): 94 | 95 | ```html 96 | 97 | 98 | 99 | 100 | 101 | 102 | Hello World! 103 | 104 | 105 |

Hello World

106 |

This should be changed by JS

107 | 108 | 109 | 110 | ``` 111 | 112 | Note that `sass` and `scss` files are transpiled by default. 113 | 114 | ## Configuration Options 115 | 116 | You can customize the behavior of the `bun-plugin-html` by providing options. Here's the available configuration: 117 | 118 | ```typescript 119 | type BunPluginHTMLOptions = { 120 | inline?: boolean | { 121 | css?: boolean; 122 | js?: boolean; 123 | }; 124 | naming?: { 125 | css?: string; 126 | }; 127 | minifyOptions?: HTMLTerserOptions; 128 | includeExtensions?: string[]; 129 | excludeExtensions?: string[]; 130 | excludeSelectors?: string[]; 131 | preprocessor?: (processor: Processor) => void | Promise; 132 | keepOriginalPaths?: boolean | string[]; 133 | }; 134 | ``` 135 | 136 | ### Inline Option 137 | 138 | By setting the `inline` option to `true`, you can choose to inline CSS and/or JS files within your HTML. Here's an example: 139 | 140 | ```html 141 | 142 | 143 | 144 | 145 | 151 | 152 | Hello World! 153 | 154 | 155 |

Hello World

156 |

This should be changed by JS

157 | 162 | 166 | 167 | ``` 168 | 169 | ### MinifyOptions Option 170 | 171 | Use `minifyOptions` to configure [`html-minifier-terser`](https://github.com/terser/html-minifier-terser?tab=readme-ov-file#options-quick-reference). 172 | 173 | The `minifyCSS` and `minifyJS` fields enable further configuration of 174 | [`clean-css`](https://github.com/clean-css/clean-css?tab=readme-ov-file#constructor-options) and 175 | [`terser`](https://github.com/terser/terser?tab=readme-ov-file#minify-options), respectively. 176 | Additionally minifyHTML is an added option, which can be disabled to skip html minification. 177 | 178 | The following default options are exported as `defaultMinifyOptions`: 179 | ```ts 180 | { 181 | collapseWhitespace: true, 182 | collapseInlineTagWhitespace: true, 183 | caseSensitive: true, 184 | minifyCSS: {}, 185 | minifyJS: true, 186 | minifyHTML: true, 187 | removeComments: true, 188 | removeRedundantAttributes: true, 189 | } 190 | ``` 191 | 192 | `minifyCSS` and `minifyJS` can both be set to either `true`, `false`, a configuration object, or a callback function. 193 | `minifyHTML` can only be set to either `true`, `false`. The different 194 | values of `minifyCSS` and `minifyJS` behave as follows: 195 | 196 | #### MinifyCSS 197 | 198 | | Value | Result | 199 | |--------------------------------------------|-----------------------------| 200 | | `false` | CSS minification is skipped | 201 | | `true` or `undefined` | CSS minification is performed with default options using `clean-css` | 202 | | `{ opts } ` | CSS minification is performed with the provided options using `clean-css` | 203 | | `((text: string, type: string) => string)` | Your function is called for every CSS element encountered and should return minified content. | 204 | 205 | 206 | #### MinifyJS 207 | 208 | | Value | Result | 209 | |----------------------------------------------|-----------------------------| 210 | | `false` | JS minification is skipped | 211 | | `true` or `undefined` | JS minification is performed by `Bun.build` and inlined scripts will also be processed with default options using `terser` | 212 | | `{ opts } ` | JS minification is performed with the provided options using `terser`. No minification is performed by `Bun.build` | 213 | | `((text: string, inline: boolean) => string)`| Your function is called for every JS element encoutered and should return minified content. | 214 | 215 | #### MinifyHTML 216 | 217 | | Value | Result | 218 | |----------------------------------------------|-----------------------------| 219 | | `false` | HTML minification is skipped | 220 | | `true` or `undefined` | HTML minification is performed by `terser`| 221 | 222 | An important consideration when using the `minifyHTML` option, is that it will skip minification done by the terser completely. 223 | This includes any originally inlined scripts and css. They will still be minified with `clean-css` and `Bun.build` when imported. 224 | 225 | ### IncludeExtensions Option 226 | 227 | The `includeExtensions` option takes an array of strings. Any files whose extensions match any of those strings will be passed to 228 | `Bun.build` (in addition to `['.js', '.jsx', '.ts', '.tsx']`). 229 | 230 | **Note: you must ensure an appropriate plugin is included for each file extension.** 231 | 232 | ### ExcludeExtensions Option 233 | 234 | The `excludeExtensions` option takes an array of strings. Any files whose extensions match any of those strings will be ignored by 235 | the plugin. 236 | 237 | The extension name follows the same format as the [path.extname](https://nodejs.org/api/path.html#pathextnamepath) return. 238 | 239 | ### ExcludeSelectors Option 240 | 241 | The `excludeSelectors` option takes an array of strings. Any HTML elements matched by a selector will be ignored by the plugin. 242 | 243 | ### Naming Option 244 | 245 | The `naming` option takes in an optional template to name css files with. By default css files follow the `chunk` naming [rules](https://bun.sh/docs/bundler#naming). This overrides that default behavior, following the same syntax. 246 | 247 | The example below shows spliting the js, assets, and css into different directories. 248 | ```ts 249 | await Bun.build({ 250 | entrypoints: ['index.html'], 251 | outdir: 'dist', 252 | naming: { 253 | chunk: 'js/[dir]/[name]-[hash].[ext]', 254 | asset: 'assets/[name].[ext]', 255 | entry: 'main.html' 256 | }, 257 | plugins: [html({ 258 | naming: { 259 | css: 'css/[name].[ext]' 260 | } 261 | })], 262 | }) 263 | ``` 264 | 265 | ### Preprocessor Option 266 | 267 | The `preprocessor` option takes in a funciton which will be provided a `Processor` class, in which you can modify the files provided to it, before they are processed by `bun-plugin-html`. 268 | 269 | The example below shows processing the css files with tailwind. By default `sass` is transpiled. 270 | ```ts 271 | await Bun.build({ 272 | entrypoints: ['src/index.html'], 273 | outdir: 'dist', 274 | minify: true, 275 | plugins: [html({ 276 | async preprocessor(processor) { 277 | const files = processor.getFiles(); 278 | 279 | for (const file of files) { 280 | if (file.extension == '.css') { 281 | const contents = await $`bun run tailwindcss -i ${file.path} --content 'src/**/*.{html,js,ts}'`.quiet().text(); 282 | processor.writeFile(file.path, contents); 283 | } 284 | } 285 | 286 | // Add hello.txt to the out dir. 287 | // The path provided to writeFile must be an absolute path. 288 | processor.writeFile(path.resolve('src/hello.txt'), 'Hello World!') 289 | }, 290 | })] 291 | }) 292 | ``` 293 | 294 | ### Keep Original Paths Option 295 | 296 | Determines whether file paths in the source code are replaced by new paths. 297 | | Value | Result | 298 | |-------------------------------------------|-----------------------------| 299 | | `true` | Path replacement is completely skipped. | 300 | | `string[]` | Only the specified file paths are excluded from replacement. | 301 | | `false` or `undefined` | All paths are replaced within the source code. | 302 | 303 | ### Suppress Errors 304 | Determines whether errors are supressed. Default is false. 305 | 306 | ## License 307 | 308 | This plugin is licensed under MIT. 309 | -------------------------------------------------------------------------------- /biome.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json", 3 | "vcs": { 4 | "enabled": false, 5 | "clientKind": "git", 6 | "useIgnoreFile": false 7 | }, 8 | "files": { 9 | "ignoreUnknown": false, 10 | "ignore": ["test/expected", "test/generation", "dist"] 11 | }, 12 | "formatter": { 13 | "enabled": true, 14 | "indentStyle": "tab" 15 | }, 16 | "organizeImports": { 17 | "enabled": true 18 | }, 19 | "linter": { 20 | "enabled": true, 21 | "rules": { 22 | "recommended": true 23 | } 24 | }, 25 | "javascript": { 26 | "formatter": { 27 | "quoteStyle": "single" 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /build.ts: -------------------------------------------------------------------------------- 1 | import dts from 'bun-plugin-dts'; 2 | 3 | await Bun.build({ 4 | entrypoints: ['./src/index.ts'], 5 | outdir: './dist', 6 | minify: true, 7 | plugins: [dts()], 8 | target: 'node', 9 | packages: 'external', 10 | }); 11 | -------------------------------------------------------------------------------- /bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BjornTheProgrammer/bun-plugin-html/ea72d4d079744ad3a4383aa39e9e2c62b1ea3d1a/bun.lockb -------------------------------------------------------------------------------- /bunfig.toml: -------------------------------------------------------------------------------- 1 | [test] 2 | root = "./test" 3 | -------------------------------------------------------------------------------- /examples/tailwind/build.ts: -------------------------------------------------------------------------------- 1 | import autoprefixer from 'autoprefixer'; 2 | import html, { defaultMinifyOptions } from 'bun-plugin-html'; 3 | import postcss from 'postcss'; 4 | import tailwindcss from 'tailwindcss'; 5 | import twconfig from './tailwind.config.ts'; 6 | 7 | await Bun.build({ 8 | entrypoints: ['src/index.html'], 9 | outdir: 'dist', 10 | naming: '[dir]/[name].[ext]', 11 | minify: true, 12 | plugins: [ 13 | html({ 14 | inline: true, 15 | async preprocessor(processor) { 16 | const files = processor.getFiles(); 17 | 18 | for (const file of files) { 19 | if (file.extension === '.css') { 20 | const contents = await postcss([ 21 | tailwindcss(twconfig), 22 | autoprefixer(), 23 | ]).process(await file.content, { from: undefined }); 24 | processor.writeFile(file.path, contents.css); 25 | } 26 | } 27 | }, 28 | }), 29 | ], 30 | }); 31 | -------------------------------------------------------------------------------- /examples/tailwind/bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BjornTheProgrammer/bun-plugin-html/ea72d4d079744ad3a4383aa39e9e2c62b1ea3d1a/examples/tailwind/bun.lockb -------------------------------------------------------------------------------- /examples/tailwind/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bun-plugin-html-tailwind", 3 | "module": "index.ts", 4 | "devDependencies": { 5 | "@types/bun": "latest", 6 | "autoprefixer": "^10.4.20", 7 | "bun-html-live-reload": "^0.1.4", 8 | "postcss": "^8.4.47", 9 | "tailwindcss": "^3.4.14" 10 | }, 11 | "peerDependencies": { 12 | "typescript": "^5.0.0" 13 | }, 14 | "scripts": { 15 | "dev": "bun run --hot serve.ts", 16 | "build": "bun run build.ts" 17 | }, 18 | "type": "module", 19 | "dependencies": { 20 | "bun-plugin-html": "^2.1.4" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /examples/tailwind/serve.ts: -------------------------------------------------------------------------------- 1 | import path from 'node:path'; 2 | import { $ } from 'bun'; 3 | import { withHtmlLiveReload } from 'bun-html-live-reload'; 4 | 5 | const outdir = './dist'; 6 | 7 | await $`bun run build`; 8 | 9 | export default withHtmlLiveReload( 10 | { 11 | fetch(req) { 12 | const pathname = new URL(req.url).pathname; 13 | const filePath = 14 | pathname === '/' ? `${outdir}/index.html` : outdir + pathname; 15 | const file = Bun.file(path.resolve(filePath)); 16 | return new Response(file); 17 | }, 18 | error(error) { 19 | console.error(error); 20 | return new Response(null, { status: 404 }); 21 | }, 22 | port: 3000, 23 | }, 24 | { 25 | watchPath: path.resolve(import.meta.dir, 'src'), 26 | async onChange() { 27 | await $`bun run build`; 28 | }, 29 | }, 30 | ); 31 | -------------------------------------------------------------------------------- /examples/tailwind/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Tailwind bun-plugin-html Example 7 | 8 | 9 | 10 |

Built w/ Bun + Tailwindcss

11 |
12 | 13 | 14 | 15 | + 16 | 17 | 18 | 19 |
20 | Learn more about tailwind here! 21 |

Make a change in this html file, then save, see it update upon running `bun run dev`

22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /examples/tailwind/src/main.ts: -------------------------------------------------------------------------------- 1 | console.log( 2 | 'This has gotten linked into the HTML automatically from `bun-plugin-html`!', 3 | ); 4 | -------------------------------------------------------------------------------- /examples/tailwind/src/styles.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | a { 6 | @apply hover:text-blue-500 active:text-blue-900; 7 | } 8 | -------------------------------------------------------------------------------- /examples/tailwind/tailwind.config.ts: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | export default { 3 | content: ['./src/**/*.{html,js,ts}'], 4 | theme: { 5 | extend: {}, 6 | }, 7 | plugins: [], 8 | }; 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bun-plugin-html", 3 | "version": "2.2.8", 4 | "author": "Bjorn Beishline", 5 | "repository": { 6 | "type": "git", 7 | "url": "git+https://github.com/BjornTheProgrammer/bun-plugin-html.git" 8 | }, 9 | "engines": { 10 | "bun": ">=1.1.34" 11 | }, 12 | "main": "dist/index.js", 13 | "module": "index.ts", 14 | "dependencies": { 15 | "@types/clean-css": "^4.2.11", 16 | "@types/html-minifier-terser": "^7.0.2", 17 | "clean-css": "^5.3.3", 18 | "html-minifier-terser": "^7.2.0", 19 | "sass": "^1.81.0" 20 | }, 21 | "devDependencies": { 22 | "@biomejs/biome": "1.9.4", 23 | "@types/diff": "^6.0.0", 24 | "@types/react": "^18.2.47", 25 | "@types/react-dom": "^18.2.18", 26 | "bun-plugin-dts": "^0.2.0", 27 | "bun-types": "latest", 28 | "chalk": "^5.3.0", 29 | "diff": "^7.0.0", 30 | "react": "^18.2.0", 31 | "react-dom": "^18.2.0", 32 | "squint-cljs": "^0.5.86", 33 | "tailwindcss": "^3.4.14", 34 | "typescript": "^5.3.2" 35 | }, 36 | "bugs": "https://github.com/BjornTheProgrammer/bun-plugin-html/issues", 37 | "description": "A plugin for bun build which allows html entrypoints.", 38 | "files": ["dist"], 39 | "keywords": ["bun", "plugin", "build", "bun.build"], 40 | "license": "MIT", 41 | "scripts": { 42 | "start": "bun run src/index.ts", 43 | "build": "bun run build.ts", 44 | "lint": "biome check", 45 | "lint:fix": "biome check --write" 46 | }, 47 | "type": "module", 48 | "types": "dist/index.d.ts" 49 | } 50 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | import fs from 'node:fs/promises'; 5 | import os from 'node:os'; 6 | import path, { resolve } from 'node:path'; 7 | import { 8 | type BuildArtifact, 9 | type BuildConfig, 10 | type BunFile, 11 | type BunPlugin, 12 | type PluginBuilder, 13 | file, 14 | } from 'bun'; 15 | import CleanCSS, { type OptionsOutput as CleanCssOptions } from 'clean-css'; 16 | import { 17 | type Options as HTMLTerserOptions, 18 | minify, 19 | } from 'html-minifier-terser'; 20 | import * as sass from 'sass'; 21 | import { type MinifyOptions, minify as terser } from 'terser'; 22 | import { 23 | type FileDetails, 24 | Processor, 25 | attributeToSelector, 26 | changeFileExtension, 27 | contentToString, 28 | findLastCommonPath, 29 | getColumnNumber, 30 | getLines, 31 | isURL, 32 | removeCommonPath, 33 | returnLineNumberOfOccurance, 34 | } from './utils'; 35 | 36 | export type File = { 37 | file: BunFile; 38 | details: FileDetails; 39 | }; 40 | 41 | export type BunPluginHTMLOptions = { 42 | /** 43 | * Whether to inline all files or not. Additionally, you can choose whether to inline just 44 | * css and js files. 45 | */ 46 | inline?: 47 | | boolean 48 | | { 49 | css?: boolean; 50 | js?: boolean; 51 | }; 52 | /** 53 | * `bun-plugin-html` already respects the default naming rules of Bun.build, but if you wish to override 54 | * that behavior for the naming of css files, then you can do so here. 55 | */ 56 | naming?: { 57 | css?: string; 58 | }; 59 | /** 60 | * Choose how the content is minified, if `Bun.build({ minify: true })` is set. 61 | */ 62 | minifyOptions?: HtmlMinifyOptions; 63 | /** 64 | * Choose what extensions to include in building of javascript files with `Bun.build`. 65 | * 66 | * Defaults are `.js`, `.jsx`, `.ts`, and `.tsx` files. 67 | */ 68 | includeExtensions?: string[]; 69 | /** 70 | * Choose which extensions to exclude from Bun.build processing. 71 | */ 72 | excludeExtensions?: string[]; 73 | /** 74 | * Choose which selectors to exclude. Only one is excluded by default, that being `a` 75 | */ 76 | excludeSelectors?: string[]; 77 | /** 78 | * Processes the files before they are processed by `bun-plugin-html`. Useful for things like tailwindcss. 79 | */ 80 | preprocessor?: (processor: Processor) => void | Promise; 81 | /** 82 | * Determines whether file paths in the source code are replaced by new paths. 83 | * - If `true`, path replacement is completely skipped. 84 | * - If an array of strings is provided, only the specified file paths are excluded from replacement. 85 | * - If omitted or `false`, all paths are replaced by default. 86 | */ 87 | keepOriginalPaths?: boolean | string[]; 88 | /** 89 | * Whether or not to suppress errors from being logged when building. Useful for when you know what 90 | * you are doing works, but are still getting errors. `true` means that the errors won't be logged. 91 | */ 92 | suppressErrors?: boolean; 93 | }; 94 | 95 | const attributesToSearch = [ 96 | 'src', 97 | 'href', 98 | 'data', 99 | 'action', 100 | 'data-src', 101 | 'lowsrc', 102 | ] as const; 103 | const extensionsToBuild: readonly string[] = [ 104 | '.js', 105 | '.jsx', 106 | '.ts', 107 | '.tsx', 108 | ] as const; 109 | const selectorsToExclude: readonly string[] = ['a'] as const; 110 | 111 | export type HtmlMinifyOptions = HTMLTerserOptions & { 112 | minifyHTML?: boolean; 113 | }; 114 | export const defaultMinifyOptions: HtmlMinifyOptions = { 115 | collapseWhitespace: true, 116 | collapseInlineTagWhitespace: true, 117 | caseSensitive: true, 118 | minifyCSS: {}, 119 | minifyJS: true, 120 | minifyHTML: true, 121 | removeComments: true, 122 | removeRedundantAttributes: true, 123 | } as const; 124 | 125 | async function getAllFiles( 126 | options: BunPluginHTMLOptions | undefined, 127 | filePath: string, 128 | excluded: readonly string[], 129 | ) { 130 | const extension = path.parse(filePath).ext; 131 | if (extension !== '.htm' && extension !== '.html') return []; 132 | 133 | const files: File[] = []; 134 | const rewriter = new HTMLRewriter(); 135 | 136 | const htmlResolvedPath = path.resolve(filePath); 137 | const originalFile = Bun.file(htmlResolvedPath); 138 | let fileText = await originalFile.text(); 139 | 140 | const hash = Bun.hash(fileText, 1).toString(16).slice(0, 8); 141 | 142 | files.push({ 143 | file: originalFile, 144 | details: { 145 | kind: 'entry-point', 146 | hash, 147 | originalPath: htmlResolvedPath, 148 | htmlImporter: htmlResolvedPath, 149 | }, 150 | }); 151 | 152 | let excludedSelector = ''; 153 | 154 | for (const exclude of excluded) { 155 | excludedSelector += `:not(${exclude})`; 156 | } 157 | 158 | rewriter.on(excludedSelector, { 159 | async element(el) { 160 | let attributeName: string | undefined; 161 | let attributeValue: string | null | undefined; 162 | 163 | for (const attribute of attributesToSearch) { 164 | if (el.hasAttribute(attribute)) { 165 | attributeName = attribute; 166 | attributeValue = el.getAttribute(attribute); 167 | break; 168 | } 169 | } 170 | 171 | if (!attributeName || !attributeValue || isURL(attributeValue)) return; 172 | const resolvedPath = path.resolve(path.dirname(filePath), attributeValue); 173 | const extension = path.parse(resolvedPath).ext; 174 | if (options?.excludeExtensions?.includes(extension)) return; 175 | const file = Bun.file(resolvedPath); 176 | 177 | if (!(await file.exists())) { 178 | fileText = fileText.replace(/\t/g, ' '); 179 | const search = `${attributeName}="${attributeValue}"`; 180 | const line = returnLineNumberOfOccurance(fileText, search); 181 | const columnNumber = 182 | getColumnNumber( 183 | fileText, 184 | fileText.indexOf(search) + search.length / 2, 185 | ) + 186 | `${line}`.length + 187 | 1; 188 | if (options?.suppressErrors !== true) { 189 | console.log(getLines(fileText, 4, line + 1)); 190 | console.log('^'.padStart(columnNumber)); 191 | console.error( 192 | `bun-plugin-html - HTMLParseError: Specified <${el.tagName}> ${attributeName} '${attributeValue}' does not exist!`, 193 | ); 194 | console.log(` at ${filePath}:${line}:${columnNumber}`); 195 | } 196 | return; 197 | } 198 | 199 | files.push({ 200 | file, 201 | details: { 202 | kind: 'chunk', 203 | attribute: { 204 | name: attributeName, 205 | value: attributeValue, 206 | }, 207 | hash, 208 | originalPath: resolvedPath, 209 | htmlImporter: htmlResolvedPath, 210 | }, 211 | }); 212 | }, 213 | }); 214 | 215 | rewriter.transform(fileText); 216 | 217 | return files; 218 | } 219 | 220 | function getExtensionFiles( 221 | files: Map, 222 | extensions: readonly string[], 223 | ) { 224 | const result: File[] = []; 225 | for (const [file, details] of files.entries()) { 226 | if (!file.name) continue; 227 | const extension = path.parse(file.name).ext; 228 | if (!extensions.includes(extension)) continue; 229 | 230 | result.push({ file, details }); 231 | } 232 | 233 | return result; 234 | } 235 | 236 | function getCSSMinifier( 237 | config: BuildConfig, 238 | options: HtmlMinifyOptions, 239 | ): (text: string) => string { 240 | if (config.minify && options.minifyCSS !== false) { 241 | if (typeof options.minifyCSS === 'function') { 242 | return options.minifyCSS as (text: string) => string; 243 | } 244 | const cssOptions = 245 | typeof options.minifyCSS === 'object' 246 | ? (options.minifyCSS as CleanCssOptions) 247 | : {}; 248 | const minifier = new CleanCSS(cssOptions); 249 | 250 | return (text: string) => { 251 | const output = minifier.minify(text); 252 | output.warnings.forEach(console.warn); 253 | if (output.errors.length > 0) { 254 | output.errors.forEach(console.error); 255 | return text; 256 | } 257 | return output.styles; 258 | }; 259 | } 260 | return (text: string) => text; 261 | } 262 | 263 | function getJSMinifier( 264 | config: BuildConfig, 265 | options: HtmlMinifyOptions, 266 | ): (text: string) => Promise { 267 | const noop = async (text: string) => text; 268 | if (config.minify) { 269 | return async (text: string) => { 270 | if (typeof options.minifyJS === 'function') { 271 | return options.minifyJS(text, false); 272 | } 273 | if (typeof options.minifyJS === 'object') { 274 | const result = await terser(text, options.minifyJS as MinifyOptions); 275 | return result.code ? result.code : text; 276 | } 277 | return text; 278 | }; 279 | } 280 | return noop; 281 | } 282 | 283 | async function forJsFiles( 284 | options: BunPluginHTMLOptions | undefined, 285 | build: PluginBuilder, 286 | files: Map, 287 | buildExtensions: readonly string[], 288 | htmlOptions: HtmlMinifyOptions, 289 | ) { 290 | const jsFiles = getExtensionFiles(files, buildExtensions); 291 | for (const item of jsFiles) files.delete(item.file); 292 | 293 | if (build.config.experimentalCss) { 294 | const cssFiles = await forStyleFiles(options, build, htmlOptions, files); 295 | if (cssFiles) { 296 | for (const file of cssFiles) { 297 | jsFiles.push(file); 298 | } 299 | } 300 | } 301 | 302 | if (!jsFiles) return; 303 | 304 | const naming: BuildConfig['naming'] = {}; 305 | if (typeof build.config.naming === 'string') { 306 | naming.entry = build.config.naming; 307 | naming.chunk = build.config.naming; 308 | naming.asset = build.config.naming; 309 | } else if (typeof build.config.naming === 'object') { 310 | naming.entry = build.config.naming.chunk; 311 | naming.chunk = build.config.naming.chunk; 312 | naming.asset = build.config.naming.asset; 313 | } else { 314 | naming.entry = './[name]-[hash].[ext]'; 315 | } 316 | 317 | const entrypoints = jsFiles.map((item) => item.file.name as string); 318 | if (entrypoints.length === 0) return; 319 | const commonPath = findLastCommonPath(entrypoints); 320 | 321 | const requiresTempDir = jsFiles.some( 322 | (file) => file.details.content !== undefined, 323 | ); 324 | const tempDirPath = await fs.mkdtemp(path.join(os.tmpdir(), 'bun-build-')); 325 | 326 | if (requiresTempDir) { 327 | // Write files with `content` to the temporary directory 328 | await Promise.all( 329 | jsFiles.map(async (item, index) => { 330 | if (!item.file.name) return; 331 | const filePath = removeCommonPath(item.file.name, commonPath); 332 | const tempFilePath = path.resolve(tempDirPath, filePath); 333 | await Bun.write(tempFilePath, item.details.content ?? item.file); 334 | entrypoints[index] = tempFilePath; 335 | }), 336 | ); 337 | } 338 | 339 | const toReplacePaths: { 340 | from: string; 341 | path: string; 342 | resolved: string; 343 | }[] = []; 344 | 345 | const customResolver = (resolverOptions: { 346 | pathToResolveFrom: string; 347 | }): BunPlugin => { 348 | return { 349 | name: 'Custom Resolver', 350 | setup(build) { 351 | build.onResolve({ filter: /[\s\S]*/ }, async (args) => { 352 | try { 353 | if (isURL(args.path)) return; 354 | let resolved: string; 355 | let external = false; 356 | const tempPath = path.resolve(tempDirPath, args.path); 357 | const originalPath = path.resolve( 358 | args.path, 359 | resolverOptions.pathToResolveFrom, 360 | ); 361 | 362 | // Check if the path is a module 363 | const isModule = 364 | !args.path.startsWith('./') && 365 | !args.path.startsWith('../') && 366 | !args.path.startsWith('/'); 367 | 368 | if (await Bun.file(tempPath).exists()) { 369 | resolved = Bun.resolveSync(args.path, tempDirPath); 370 | } else if (isModule || (await Bun.file(originalPath).exists())) { 371 | resolved = Bun.resolveSync( 372 | args.path, 373 | resolverOptions.pathToResolveFrom, 374 | ); 375 | } else { 376 | resolved = path.resolve(args.importer, '../', args.path); 377 | 378 | const exists = await fs.exists(resolved); 379 | 380 | if (!isModule) { 381 | if (exists) { 382 | if ((await fs.lstat(resolved)).isDirectory()) { 383 | if (requiresTempDir) 384 | resolved = Bun.resolveSync(args.path, originalPath); 385 | else resolved = Bun.resolveSync('.', resolved); 386 | } else { 387 | resolved = Bun.resolveSync( 388 | args.path, 389 | path.parse(args.importer).dir, 390 | ); 391 | } 392 | } else { 393 | resolved = Bun.resolveSync('.', resolved); 394 | } 395 | } 396 | 397 | if (build.config.splitting) { 398 | external = true; 399 | toReplacePaths.push({ 400 | from: args.importer, 401 | resolved, 402 | path: args.path, 403 | }); 404 | } 405 | } 406 | 407 | return { 408 | ...args, 409 | path: resolved, 410 | external, 411 | }; 412 | } catch (error) { 413 | if (options?.suppressErrors !== true) { 414 | console.error('Error during module resolution:'); 415 | console.error('Potential reasons:'); 416 | console.error('- Missing file in specified paths'); 417 | console.error('- Invalid file type (non-JS file)'); 418 | console.error( 419 | 'If unresolved, please report to `bun-plugin-html`.', 420 | ); 421 | console.error(error); 422 | } 423 | 424 | // Return an empty path to prevent build failure 425 | return { 426 | ...args, 427 | path: '', 428 | }; 429 | } 430 | }); 431 | }, 432 | }; 433 | }; 434 | 435 | const entrypointToOutput: Map = new Map(); 436 | for (const [index, entrypoint] of entrypoints.entries()) { 437 | const result = await Bun.build({ 438 | ...build.config, 439 | entrypoints: [entrypoint], 440 | naming, 441 | outdir: undefined, 442 | plugins: [ 443 | customResolver({ 444 | pathToResolveFrom: commonPath, 445 | }), 446 | ...build.config.plugins.filter( 447 | (plugin) => plugin.name !== 'bun-plugin-html', 448 | ), 449 | ], 450 | root: build.config.root || commonPath, 451 | }); 452 | 453 | if (!result.success && options?.suppressErrors !== true) { 454 | console.error(result.logs); 455 | } 456 | 457 | for (const output of result.outputs) { 458 | const outputText = await output.text(); 459 | let filePath = path.resolve(`${commonPath}/${output.path}`); 460 | if (filePath.includes(tempDirPath)) { 461 | filePath = filePath.replace(`/private${tempDirPath}`, commonPath); 462 | filePath = filePath.replace(tempDirPath, commonPath); 463 | } 464 | 465 | if (output.kind === 'entry-point' || output.loader === 'css') { 466 | if (!jsFiles[index].file.name) continue; 467 | entrypointToOutput.set(jsFiles[index].file.name, filePath); 468 | files.set(Bun.file(filePath), { 469 | content: outputText, 470 | attribute: jsFiles[index].details.attribute, 471 | kind: jsFiles[index].details.kind, 472 | hash: output.hash || Bun.hash(outputText, 1).toString(16).slice(0, 8), 473 | originalPath: jsFiles[index].details.originalPath, 474 | htmlImporter: jsFiles[index].details.htmlImporter, 475 | }); 476 | } else { 477 | files.set(Bun.file(filePath), { 478 | content: outputText, 479 | kind: output.kind, 480 | hash: output.hash || Bun.hash(outputText, 1).toString(16).slice(0, 8), 481 | originalPath: false, 482 | htmlImporter: jsFiles[index].details.htmlImporter, 483 | }); 484 | } 485 | } 486 | } 487 | 488 | for (const [file, details] of files) { 489 | const toReplace = toReplacePaths.find( 490 | (item) => entrypointToOutput.get(item.from) === file.name, 491 | ); 492 | if (!toReplace) continue; 493 | const output = entrypointToOutput.get(toReplace.resolved); 494 | const fromOutput = entrypointToOutput.get(toReplace.from); 495 | if (!output || !fromOutput) continue; 496 | const newPath = path.relative(path.parse(fromOutput).dir, output); 497 | details.content = (await contentToString(details.content)) 498 | .replaceAll(`from "${toReplace.path}"`, `from "./${newPath}"`) 499 | .replaceAll(`from"${toReplace.path}"`, `from"./${newPath}"`) 500 | .replaceAll(`require("${toReplace.path}")`, `require("./${newPath}")`); 501 | } 502 | } 503 | 504 | async function forStyleFiles( 505 | options: BunPluginHTMLOptions | undefined, 506 | build: PluginBuilder, 507 | htmlOptions: HtmlMinifyOptions, 508 | files: Map, 509 | ) { 510 | const cssMinifier = getCSSMinifier(build.config, htmlOptions); 511 | const cssFiles = getExtensionFiles(files, ['.css', '.scss', '.sass']); 512 | 513 | if (!cssFiles) return; 514 | 515 | for (const item of cssFiles) { 516 | const file = item.file; 517 | let content = 518 | (await contentToString(item.details.content)) || (await file.text()); 519 | const { originalPath } = item.details; 520 | if (/\.s[ac]ss$/i.test(originalPath || '')) { 521 | content = sass.compileString(content, { style: 'compressed' }).css; 522 | } else { 523 | content = cssMinifier(content); 524 | } 525 | 526 | if (!build.config.experimentalCss) 527 | files.set(file, { 528 | content, 529 | attribute: item.details.attribute, 530 | kind: item.details.kind, 531 | hash: Bun.hash(content, 1).toString(16).slice(0, 8), 532 | originalPath: originalPath, 533 | htmlImporter: item.details.htmlImporter, 534 | }); 535 | else files.delete(file); 536 | } 537 | 538 | if (build.config.experimentalCss) return cssFiles; 539 | } 540 | 541 | interface NamedAs { 542 | [name: string]: { 543 | [dir: string]: { 544 | as: string; 545 | fd: BunFile; 546 | }; 547 | }; 548 | } 549 | 550 | function mapIntoKeys(files: Map) { 551 | const keys = []; 552 | for (const key of files.keys()) { 553 | keys.push(key.name as string); 554 | } 555 | 556 | return keys; 557 | } 558 | 559 | async function processHtmlFiles( 560 | options: BunPluginHTMLOptions | undefined, 561 | build: PluginBuilder, 562 | files: Map, 563 | buildExtensions: readonly string[], 564 | ) { 565 | const htmlFiles = getExtensionFiles(files, ['.html', '.htm']); 566 | const toChangeAttributes: (( 567 | rewriter: HTMLRewriter, 568 | fileLocation: string, 569 | ) => void)[] = []; 570 | 571 | if (!htmlFiles) return toChangeAttributes; 572 | 573 | for (const htmlFile of htmlFiles) { 574 | for (const [file, details] of files) { 575 | const attribute = details.attribute; 576 | if (attribute) { 577 | const selector = attributeToSelector(attribute); 578 | 579 | if (!file.name) continue; 580 | const extension = path.parse(file.name).ext; 581 | 582 | if (/\.(c|s[ac])ss$/i.test(extension)) { 583 | if ( 584 | options && 585 | (options.inline === true || 586 | (typeof options.inline === 'object' && 587 | options.inline?.css === true)) 588 | ) { 589 | files.delete(file); 590 | toChangeAttributes.push((rewriter: HTMLRewriter) => { 591 | rewriter.on(selector, { 592 | async element(el) { 593 | let content = 594 | (await contentToString(details.content)) || 595 | (await file.text()); 596 | if (/\.s[ac]ss$/i.test(extension)) { 597 | content = sass.compileString(content).css; 598 | } 599 | el.replace(``, { 600 | html: true, 601 | }); 602 | }, 603 | }); 604 | }); 605 | } 606 | } else if (buildExtensions.includes(extension)) { 607 | if ( 608 | options && 609 | (options.inline === true || 610 | (typeof options.inline === 'object' && 611 | options.inline?.js === true)) 612 | ) { 613 | files.delete(file); 614 | 615 | toChangeAttributes.push((rewriter: HTMLRewriter) => { 616 | rewriter.on(selector, { 617 | async element(el) { 618 | const contentToStringThing = await contentToString( 619 | details.content, 620 | ); 621 | let content: string; 622 | if (details.content === undefined) 623 | content = await file.text(); 624 | else content = await contentToString(details.content); 625 | content = content.replaceAll(/(<)(\/script>)/g, '\\x3C$2'); 626 | 627 | el.removeAttribute('src'); 628 | el.setInnerContent(content, { 629 | html: true, 630 | }); 631 | }, 632 | }); 633 | }); 634 | } 635 | } else { 636 | files.set(file, { 637 | ...details, 638 | hash: Bun.hash(await file.arrayBuffer(), 1) 639 | .toString(16) 640 | .slice(0, 8), 641 | kind: 'asset', 642 | }); 643 | } 644 | } 645 | } 646 | } 647 | 648 | return toChangeAttributes; 649 | } 650 | 651 | function keepNamedAs( 652 | parsedOriginPath: path.ParsedPath, 653 | parsedNewPath: path.ParsedPath, 654 | resolved: string, 655 | namedAs: NamedAs, 656 | ) { 657 | const { root, dir, base } = parsedOriginPath; 658 | const names = namedAs[base] || {}; 659 | const nameDir = path.join(root, dir); 660 | if (!names[nameDir]) { 661 | const as = path.join( 662 | parsedNewPath.root, 663 | parsedNewPath.dir, 664 | parsedNewPath.base, 665 | ); 666 | if (as !== path.join(nameDir, base)) { 667 | names[nameDir] = { as, fd: Bun.file(resolved) }; 668 | namedAs[base] = names; 669 | } 670 | } 671 | return names[nameDir]; 672 | } 673 | 674 | async function renameFile( 675 | options: BunPluginHTMLOptions | undefined, 676 | build: PluginBuilder, 677 | file: BunFile, 678 | hash: string, 679 | kind: BuildArtifact['kind'], 680 | sharedPath: string, 681 | namedAs: NamedAs, 682 | ) { 683 | let buildNamingType: 'chunk' | 'entry' | 'asset' = 'asset'; 684 | if (kind === 'entry-point') buildNamingType = 'entry'; 685 | if (kind === 'chunk') buildNamingType = 'chunk'; 686 | if (kind === 'sourcemap' || kind === 'bytecode') return file; 687 | 688 | if (!file.name) return file; 689 | const extension = path.parse(file.name).ext; 690 | 691 | let naming: string | undefined; 692 | if (/\.(c|s[ac])ss$/i.test(extension) && options && options.naming?.css) { 693 | naming = options.naming.css; 694 | } else if (typeof build.config.naming === 'string') { 695 | naming = build.config.naming; 696 | } else if (typeof build.config.naming === 'object') { 697 | naming = build.config.naming[buildNamingType]; 698 | } 699 | 700 | if (!naming) return file; 701 | 702 | let filePath = path.normalize(file.name); 703 | filePath = filePath.replace(`${sharedPath}`, '.'); 704 | const parsedPath = path.parse(filePath); 705 | 706 | const dir = parsedPath.dir; 707 | let ext = parsedPath.ext.replace('.', ''); 708 | const name = parsedPath.name; 709 | 710 | if (/s[ac]ss$/i.test(ext)) { 711 | ext = 'css'; 712 | } 713 | 714 | const newPath = naming 715 | .replaceAll('[dir]', dir) 716 | .replaceAll('[hash]', `${hash}`) 717 | .replaceAll('[ext]', ext) 718 | .replaceAll('[name]', name); 719 | 720 | const resolved = path.resolve(sharedPath, newPath); 721 | 722 | const newPathParsed = path.parse(newPath); 723 | const named = keepNamedAs(parsedPath, newPathParsed, resolved, namedAs); 724 | return named?.fd || Bun.file(resolved); 725 | } 726 | 727 | const html = (options?: BunPluginHTMLOptions): BunPlugin => { 728 | const _keepOriginalPaths = options?.keepOriginalPaths; 729 | const _namedAs: NamedAs = {}; 730 | const _pathSaved: { [path: string]: boolean } = {}; 731 | 732 | const save = async ( 733 | name: string, 734 | body: Blob | NodeJS.TypedArray | ArrayBufferLike | string | Bun.BlobPart[], 735 | options?: Parameters[2], 736 | outdir?: string, 737 | ) => { 738 | if (_pathSaved[name]) return; // avoid duplicated-saving a file 739 | _pathSaved[name] = true; 740 | if (_keepOriginalPaths === true || typeof body !== 'string') { 741 | return await Bun.write(name, body, options); 742 | } 743 | // replace mapping items string inside body 744 | let content = body; 745 | // host relative dir 746 | const hostDir = path.relative(outdir || '.', path.parse(name).dir); 747 | 748 | const originNames = Object.keys(_namedAs); 749 | for (let j = 0; j < originNames.length; j++) { 750 | const originName = originNames[j]; 751 | const originNameMatcher = originName.replace(/\./g, '\\.'); 752 | const clue = new RegExp( 753 | `(['"])([^'"\\n]*\/)?${originNameMatcher}([?#][^'"]*)?\\1|\\(([^\\)\\n'"]+\/)?${originNameMatcher}([?#][^'"\\)]*)?\\)`, 754 | 'g', 755 | ); 756 | const pathStrings = content.match(clue); 757 | if (!pathStrings) continue; 758 | const asNewNames = _namedAs[originName]; 759 | for (let i = 0; i < pathStrings.length; i++) { 760 | const pathStrCtx: string = pathStrings[i]; 761 | let [prefix, pathString, suffix]: string[] = [ 762 | pathStrCtx.substring(0, 1), 763 | pathStrCtx.substring(1, pathStrCtx.length - 1), 764 | pathStrCtx.substring(pathStrCtx.length - 1), 765 | ]; 766 | if (isURL(pathString)) continue; 767 | const pathExtraTail = pathString.match(/[?#]/); 768 | if (pathExtraTail?.index) { 769 | // with extra query or hash 770 | suffix = pathString.substring(pathExtraTail.index) + suffix; 771 | pathString = pathString.substring(0, pathExtraTail.index); 772 | } 773 | if ( 774 | Array.isArray(_keepOriginalPaths) && 775 | _keepOriginalPaths.length > 0 && 776 | _keepOriginalPaths.some( 777 | (s) => pathString.length >= s.length && pathString.endsWith(s), 778 | ) 779 | ) 780 | continue; 781 | 782 | for (const originDir in asNewNames) { 783 | const originPath = path.join(originDir, originName); 784 | let { as: newPath } = asNewNames[originDir]; 785 | if (pathString.startsWith('/')) { 786 | newPath = `/${newPath}`; 787 | } else if ( 788 | pathString.replace(/^\.\//, '') !== originPath.replace(/^\.\//, '') 789 | ) { 790 | const pathStrDir = path.parse(path.join(hostDir, pathString)).dir; 791 | if (pathStrDir !== originDir) continue; // same dir 792 | newPath = path.relative(hostDir, newPath); 793 | } 794 | content = content.replace(pathStrCtx, `${prefix}${newPath}${suffix}`); 795 | } 796 | } 797 | } 798 | return await Bun.write(name, content, options); 799 | }; 800 | 801 | return { 802 | name: 'bun-plugin-html', 803 | async setup(build) { 804 | build.onLoad({ filter: /\.(html|htm)$/ }, async (args) => { 805 | throw new Error( 806 | 'bun-plugin-html does not support output information at this time.', 807 | ); 808 | }); 809 | 810 | const htmlOptions = options?.minifyOptions ?? defaultMinifyOptions; 811 | 812 | const excluded = options?.excludeSelectors 813 | ? options.excludeSelectors.concat(selectorsToExclude) 814 | : selectorsToExclude; 815 | const buildExtensions = options?.includeExtensions 816 | ? options.includeExtensions.concat(extensionsToBuild) 817 | : extensionsToBuild; 818 | 819 | const filesPromises = await Promise.all( 820 | build.config.entrypoints.map((entrypoint) => 821 | getAllFiles(options, entrypoint, excluded), 822 | ), 823 | ); 824 | let files: Map = new Map( 825 | filesPromises.flat().map((item) => [item.file, item.details]), 826 | ); 827 | if (!files.size) return; 828 | 829 | if (options?.preprocessor) { 830 | const processor = new Processor(files); 831 | await options.preprocessor(processor); 832 | files = processor.export(); 833 | } 834 | 835 | await forJsFiles(options, build, files, buildExtensions, htmlOptions); 836 | if (!build.config.experimentalCss) 837 | await forStyleFiles(options, build, htmlOptions, files); 838 | 839 | const attributesToChange = await processHtmlFiles( 840 | options, 841 | build, 842 | files, 843 | buildExtensions, 844 | ); 845 | 846 | const keys = mapIntoKeys(files); 847 | const commonPath = findLastCommonPath(keys); 848 | 849 | const newFiles: [BunFile, FileDetails][] = []; 850 | 851 | for (const [file, details] of files.entries()) { 852 | if (!file.name) continue; 853 | const extension = path.parse(file.name).ext; 854 | const content = details.content ?? file; 855 | 856 | if (buildExtensions.includes(extension)) { 857 | let filePath = removeCommonPath(file.name, commonPath); 858 | const parsedNewPath = path.parse(filePath); 859 | if (build.config.outdir) 860 | filePath = path.resolve(build.config.outdir, filePath); 861 | const named = details.originalPath 862 | ? keepNamedAs( 863 | path.parse(removeCommonPath(details.originalPath, commonPath)), 864 | parsedNewPath, 865 | filePath, 866 | _namedAs, 867 | ) 868 | : undefined; 869 | newFiles.push([ 870 | named?.fd || Bun.file(filePath), 871 | { 872 | content, 873 | attribute: details.attribute, 874 | kind: details.kind, 875 | hash: details.hash, 876 | originalPath: details.originalPath, 877 | htmlImporter: details.htmlImporter, 878 | }, 879 | ]); 880 | continue; 881 | } 882 | 883 | const newFile = await renameFile( 884 | options, 885 | build, 886 | file, 887 | details.hash, 888 | details.kind, 889 | commonPath, 890 | _namedAs, 891 | ); 892 | if (!newFile.name) continue; 893 | let filePath = removeCommonPath(newFile.name, commonPath); 894 | if (build.config.outdir) 895 | filePath = path.resolve(build.config.outdir, filePath); 896 | newFiles.push([ 897 | Bun.file(filePath), 898 | { 899 | content, 900 | attribute: details.attribute, 901 | kind: details.kind, 902 | hash: details.hash, 903 | originalPath: details.originalPath, 904 | htmlImporter: details.htmlImporter, 905 | }, 906 | ]); 907 | } 908 | 909 | const commonPathOutput = findLastCommonPath( 910 | newFiles.map(([name]) => name.name as string), 911 | ); 912 | 913 | for (const [file, details] of newFiles.filter( 914 | ([file, details]) => details.kind !== 'entry-point', 915 | )) { 916 | const { name } = file; 917 | if (!name || !details.content) continue; 918 | if (name.indexOf(details.hash) > -1 && (await fs.exists(name))) 919 | continue; 920 | await save( 921 | name, 922 | details.content, 923 | { 924 | createPath: true, 925 | }, 926 | build.config.outdir, 927 | ); 928 | 929 | if (!details.attribute) continue; 930 | const attribute = details.attribute; 931 | const selector = attributeToSelector(attribute); 932 | const extension = path.parse(name).ext; 933 | 934 | attributesToChange.push((rewriter, fileLocation) => { 935 | rewriter.on(selector, { 936 | element(el) { 937 | if (el.getAttribute(attribute.name) === null || !file.name) 938 | return; 939 | 940 | let filePath = path.relative( 941 | path.dirname(fileLocation), 942 | file.name, 943 | ); 944 | 945 | if (buildExtensions.includes(extension)) 946 | filePath = changeFileExtension(filePath, '.js'); 947 | 948 | el.setAttribute(attribute.name, filePath); 949 | }, 950 | }); 951 | }); 952 | } 953 | 954 | for (const [file, details] of newFiles.filter( 955 | ([file, details]) => details.kind === 'entry-point', 956 | )) { 957 | let fileContents = await contentToString(details.content); 958 | const rewriter = new HTMLRewriter(); 959 | for (const item of attributesToChange) 960 | item(rewriter, file.name as string); 961 | fileContents = rewriter.transform(fileContents); 962 | fileContents = 963 | build.config.minify && htmlOptions.minifyHTML 964 | ? await minify(fileContents, htmlOptions) 965 | : fileContents; 966 | 967 | const { name } = file; 968 | if (!name) continue; 969 | if (name.indexOf(details.hash) > -1 && (await fs.exists(name))) 970 | continue; 971 | await save( 972 | name, 973 | fileContents, 974 | { 975 | createPath: true, 976 | }, 977 | build.config.outdir, 978 | ); 979 | } 980 | }, 981 | }; 982 | }; 983 | 984 | export default html; 985 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import path from 'node:path'; 2 | import type { BuildArtifact, BunFile } from 'bun'; 3 | 4 | export type FileDetails = { 5 | attribute?: { 6 | name: string; 7 | value: string; 8 | }; 9 | content?: 10 | | Blob 11 | | NodeJS.TypedArray 12 | | ArrayBufferLike 13 | | string 14 | | Bun.BlobPart[]; 15 | kind: BuildArtifact['kind']; 16 | hash: string; 17 | originalPath: string | false; 18 | htmlImporter: string; 19 | }; 20 | 21 | const urlTester = /^(#|http[s]:\/\/|\/)/i; 22 | export function isURL(link: string) { 23 | if (urlTester.test(link)) { 24 | return true; 25 | } 26 | try { 27 | const url = new URL(link); 28 | return true; 29 | } catch (error) { 30 | return false; 31 | } 32 | } 33 | 34 | export function returnLineNumberOfOccurance(file: string, text: string) { 35 | for (const [i, line] of file.split('\n').entries()) { 36 | if (line.includes(text)) return i + 1; 37 | } 38 | 39 | return 0; 40 | } 41 | 42 | export function getColumnNumber(file: string, index: number) { 43 | const lastLine = file.slice(0, index).split('\n'); 44 | return lastLine[lastLine.length - 1].length; 45 | } 46 | 47 | export function getLines(file: string, amount: number, end: number) { 48 | const maxdigits = end.toString().length; 49 | const start = end - amount < 0 ? 0 : end - amount; 50 | return file 51 | .split('\n') 52 | .slice(start, end) 53 | .map((l, i) => { 54 | const lineNumber = `${i + start + 1}`.padStart(maxdigits); 55 | return `${lineNumber}:${l}`; 56 | }) 57 | .join('\n'); 58 | } 59 | 60 | export function changeFileExtension(filePath: string, newExtension: string) { 61 | return path.format({ ...path.parse(filePath), base: '', ext: newExtension }); 62 | } 63 | 64 | export function findLastCommonPath(paths: string[]) { 65 | if (paths.length === 0) return ''; 66 | 67 | const allPathsIdentical = paths.every((p) => p === paths[0]); 68 | if (allPathsIdentical) return path.dirname(paths[0]); 69 | 70 | // Normalize paths and split them 71 | const splitPaths = paths.map((p) => path.normalize(p).split(path.sep)); 72 | const commonPath = []; 73 | 74 | for (let i = 0; i < splitPaths[0].length; i++) { 75 | const currentPart = splitPaths[0][i]; 76 | if (splitPaths.every((p) => p[i] === currentPart)) { 77 | commonPath.push(currentPart); 78 | } else { 79 | break; 80 | } 81 | } 82 | 83 | return commonPath.join(path.sep); 84 | } 85 | 86 | export function removeCommonPath(filePath: string, commonPath: string) { 87 | return path.relative(commonPath, filePath); 88 | } 89 | 90 | export function attributeToSelector( 91 | attribute: Exclude, 92 | ) { 93 | return `*[${attribute.name}="${attribute.value}"]`; 94 | } 95 | 96 | export function contentToString(content: FileDetails['content']) { 97 | if (content === undefined) return ''; 98 | if (typeof content === 'string') return content; 99 | if (content instanceof Blob) return content.text(); 100 | if (ArrayBuffer.isView(content)) return new TextDecoder().decode(content); 101 | if (Array.isArray(content)) return new Blob(content as BlobPart[]).text(); 102 | if (content instanceof SharedArrayBuffer) 103 | return new TextDecoder().decode(new Uint8Array(content)); 104 | return new TextDecoder().decode(content); 105 | } 106 | 107 | export class Processor { 108 | #files: Map = new Map(); 109 | 110 | constructor(inputs: Map) { 111 | for (const [file, details] of inputs) { 112 | if (!file.name) continue; 113 | this.#files.set(file.name, details); 114 | } 115 | } 116 | 117 | /** 118 | * Get all the files, that can then be changed using writeFile. 119 | */ 120 | getFiles() { 121 | const fileList: { 122 | path: string; 123 | content: Promise | string; 124 | extension: string; 125 | }[] = []; 126 | 127 | for (const [filepath, details] of this.#files) { 128 | const extension = path.parse(filepath).ext; 129 | fileList.push({ 130 | path: filepath, 131 | content: contentToString(details.content) || Bun.file(filepath).text(), 132 | extension, 133 | }); 134 | } 135 | 136 | return fileList; 137 | } 138 | 139 | /** 140 | * Writes the specified file at filepath with the specified content. 141 | * 142 | * If the path is the same as what the original file path was, then this will overwrite the content in that file. 143 | * 144 | * If it isn't this creates a new file which will be outputted to the outdir. New files are treated as chunks. 145 | */ 146 | writeFile(filepath: string, content: string) { 147 | if (!path.isAbsolute(filepath)) throw new Error('Path MUST be absolute'); 148 | if (this.#files.has(filepath)) { 149 | const fileDetails = this.#files.get(filepath) as FileDetails; 150 | this.#files.set(filepath, { 151 | ...fileDetails, 152 | content, 153 | }); 154 | } else { 155 | this.#files.set(filepath, { 156 | kind: 'chunk', 157 | hash: Bun.hash(content, 1).toString(16).slice(0, 8), 158 | content, 159 | originalPath: filepath, 160 | htmlImporter: '', 161 | }); 162 | } 163 | } 164 | 165 | /** 166 | * This should only be called by the internals 167 | */ 168 | export() { 169 | const files: Map = new Map(); 170 | 171 | for (const [filepath, details] of this.#files) { 172 | files.set(Bun.file(filepath), details); 173 | } 174 | 175 | return files; 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /test/css.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from 'bun:test'; 2 | import fs from 'node:fs'; 3 | import { sleep, sleepSync } from 'bun'; 4 | import html from '../src/index'; 5 | import { emptyDir, testIfFileExists } from './utils'; 6 | 7 | describe('Testing Generation of HTML', async () => { 8 | const generationDirectory = './test/generation/css'; 9 | const expectedDirectory = './test/expected/css'; 10 | 11 | if (fs.existsSync(generationDirectory)) emptyDir(generationDirectory); 12 | 13 | const response = await Bun.build({ 14 | entrypoints: ['./test/css/index.html'], 15 | outdir: generationDirectory, 16 | experimentalCss: true, 17 | plugins: [ 18 | html({ 19 | inline: true, 20 | }), 21 | ], 22 | naming: '[dir]/[name].[ext]', 23 | }); 24 | 25 | testIfFileExists(generationDirectory, expectedDirectory, 'index.html'); 26 | }); 27 | -------------------------------------------------------------------------------- /test/css/import.css: -------------------------------------------------------------------------------- 1 | html { 2 | background-color: black; 3 | color: white; 4 | } 5 | -------------------------------------------------------------------------------- /test/css/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Test 7 | 8 | 9 | 10 |

11 | 12 | 13 | -------------------------------------------------------------------------------- /test/css/main.css: -------------------------------------------------------------------------------- 1 | @import url("./import.css"); 2 | 3 | * { 4 | border-color: red; 5 | } 6 | -------------------------------------------------------------------------------- /test/custom-extension.test.ts: -------------------------------------------------------------------------------- 1 | import { describe } from 'bun:test'; 2 | import fs from 'node:fs'; 3 | import type { PluginBuilder } from 'bun'; 4 | import html from '../src/index'; 5 | import { emptyDir, testFileDoesntExist, testIfFileExists } from './utils'; 6 | 7 | import 'squint-cljs'; // somehow I needed to pre-require this; 8 | const squintLoader = { 9 | name: 'squint loader', 10 | async setup(build: PluginBuilder) { 11 | // @ts-ignore 12 | const { compileString } = await import('squint-cljs'); 13 | const { readFileSync } = await import('node:fs'); 14 | // when a .cljs file is imported... 15 | build.onLoad({ filter: /\.cljs$/ }, ({ path }) => { 16 | // read and compile it with squint 17 | const file = readFileSync(path, 'utf8'); 18 | const contents = compileString(file); 19 | // and return the compiled source code as "js" 20 | return { 21 | contents, 22 | loader: 'js', 23 | }; 24 | }); 25 | }, 26 | }; 27 | 28 | describe('Testing Generation of Custom Extension', async () => { 29 | const generationDirectory = './test/generation/custom-extension'; 30 | const expectedDirectory = './test/expected/custom-extension'; 31 | 32 | if (fs.existsSync(generationDirectory)) emptyDir(generationDirectory); 33 | 34 | await Bun.build({ 35 | entrypoints: ['./test/starting/index.html'], 36 | outdir: generationDirectory, 37 | plugins: [ 38 | squintLoader, 39 | html({ 40 | includeExtensions: ['.cljs'], 41 | excludeExtensions: ['.css', '.ico', '.tsx'], 42 | }), 43 | ], 44 | naming: '[dir]/[name].[ext]', 45 | }); 46 | 47 | testIfFileExists(generationDirectory, expectedDirectory, 'index.html'); 48 | testIfFileExists(generationDirectory, expectedDirectory, 'main.js'); 49 | testIfFileExists( 50 | generationDirectory, 51 | expectedDirectory, 52 | 'js/build-custom.js', 53 | ); 54 | 55 | testFileDoesntExist(generationDirectory, 'js/build-custom.cljs'); 56 | }); 57 | -------------------------------------------------------------------------------- /test/exclude-extensions.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from 'bun:test'; 2 | import fs from 'node:fs'; 3 | import { sleep, sleepSync } from 'bun'; 4 | import html from '../src/index'; 5 | import { emptyDir, testFileDoesntExist, testIfFileExists } from './utils'; 6 | 7 | describe('Testing Generation of Exclude Extension', async () => { 8 | const generationDirectory = './test/generation/exclude-extensions'; 9 | const expectedDirectory = './test/expected/exclude-extensions'; 10 | 11 | if (fs.existsSync(generationDirectory)) emptyDir(generationDirectory); 12 | 13 | await Bun.build({ 14 | entrypoints: ['./test/starting/index.html'], 15 | outdir: generationDirectory, 16 | plugins: [html({ excludeExtensions: ['.css', '.ico', '.tsx'] })], 17 | naming: '[dir]/[name].[ext]', 18 | }); 19 | 20 | testIfFileExists(generationDirectory, expectedDirectory, 'index.html'); 21 | testIfFileExists(generationDirectory, expectedDirectory, 'main.js'); 22 | 23 | testFileDoesntExist(generationDirectory, 'main.css'); 24 | testFileDoesntExist(generationDirectory, 'images/favicon.ico'); 25 | testFileDoesntExist(generationDirectory, 'js/secondary.js'); 26 | }); 27 | -------------------------------------------------------------------------------- /test/exclude-selector.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from 'bun:test'; 2 | import fs from 'node:fs'; 3 | import { sleep, sleepSync } from 'bun'; 4 | import html from '../src/index'; 5 | import { emptyDir, testFileDoesntExist, testIfFileExists } from './utils'; 6 | 7 | describe('Testing Generation of Exclude Selector', async () => { 8 | const generationDirectory = './test/generation/exclude-selector'; 9 | const expectedDirectory = './test/expected/exclude-selector'; 10 | 11 | if (fs.existsSync(generationDirectory)) emptyDir(generationDirectory); 12 | 13 | await Bun.build({ 14 | entrypoints: ['./test/starting/index.html'], 15 | outdir: generationDirectory, 16 | plugins: [html({ excludeSelectors: ['link'] })], 17 | naming: '[dir]/[name].[ext]', 18 | }); 19 | 20 | testIfFileExists(generationDirectory, expectedDirectory, 'index.html'); 21 | testIfFileExists(generationDirectory, expectedDirectory, 'main.js'); 22 | 23 | testFileDoesntExist(generationDirectory, 'main.css'); 24 | testFileDoesntExist(generationDirectory, 'images/favicon.ico'); 25 | }); 26 | -------------------------------------------------------------------------------- /test/expected/css/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Test 7 | 18 | 19 | 20 |

21 | 22 | 23 | -------------------------------------------------------------------------------- /test/expected/custom-extension/images/shubham-dhage-unsplash.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BjornTheProgrammer/bun-plugin-html/ea72d4d079744ad3a4383aa39e9e2c62b1ea3d1a/test/expected/custom-extension/images/shubham-dhage-unsplash.jpg -------------------------------------------------------------------------------- /test/expected/custom-extension/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Hello World! 9 | 10 | 11 |

Hello World

12 | 13 |
14 |

This should be changed by JS

15 |

This should be changed by CLJS

16 |
17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /test/expected/custom-extension/js/build-custom.js: -------------------------------------------------------------------------------- 1 | // /Users/bjorn/Documents/GitHub/bun-plugin-html/node_modules/squint-cljs/src/squint/core.js 2 | function seqable_QMARK_(x) { 3 | return x === null || x === undefined || !!x[Symbol.iterator]; 4 | } 5 | function iterable(x) { 6 | if (x === null || x === undefined) { 7 | return []; 8 | } 9 | if (seqable_QMARK_(x)) { 10 | return x; 11 | } 12 | if (x instanceof Object) 13 | return Object.entries(x); 14 | throw new TypeError(`${x} is not iterable`); 15 | } 16 | var IIterable = Symbol("Iterable"); 17 | var tolr = false; 18 | class LazyIterable { 19 | constructor(gen) { 20 | this.gen = gen; 21 | this.usages = 0; 22 | } 23 | [Symbol.iterator]() { 24 | this.usages++; 25 | if (this.usages >= 2 && tolr) { 26 | try { 27 | throw new Error; 28 | } catch (e) { 29 | console.warn("Re-use of lazy value", e.stack); 30 | } 31 | } 32 | return this.gen(); 33 | } 34 | } 35 | LazyIterable.prototype[IIterable] = true; 36 | function lazy(f) { 37 | return new LazyIterable(f); 38 | } 39 | var IApply__apply = Symbol("IApply__apply"); 40 | function concat1(colls) { 41 | return lazy(function* () { 42 | for (const coll of colls) { 43 | yield* iterable(coll); 44 | } 45 | }); 46 | } 47 | function concat(...colls) { 48 | return concat1(colls); 49 | } 50 | concat[IApply__apply] = (colls) => { 51 | return concat1(colls); 52 | }; 53 | var _metaSym = Symbol("meta"); 54 | // test/starting/js/build-custom.cljs 55 | console.log("in build-custom.cljs"); 56 | document.getElementById("cljs-target").innerHTML = "Changed!"; 57 | -------------------------------------------------------------------------------- /test/expected/custom-extension/main.js: -------------------------------------------------------------------------------- 1 | // /Users/bjorn/Documents/GitHub/bun-plugin-html/test/starting/js/index.ts 2 | function fromJs() { 3 | console.log("from js/index.ts"); 4 | } 5 | 6 | // test/starting/main.ts 7 | console.log("Running JS for browser"); 8 | fromJs(); 9 | document.querySelector("#js-target").innerHTML = "Changed!"; 10 | -------------------------------------------------------------------------------- /test/expected/exclude-extensions/images/shubham-dhage-unsplash.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BjornTheProgrammer/bun-plugin-html/ea72d4d079744ad3a4383aa39e9e2c62b1ea3d1a/test/expected/exclude-extensions/images/shubham-dhage-unsplash.jpg -------------------------------------------------------------------------------- /test/expected/exclude-extensions/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Hello World! 9 | 10 | 11 |

Hello World

12 | 13 |
14 |

This should be changed by JS

15 |

This should be changed by CLJS

16 |
17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /test/expected/exclude-extensions/js/build-custom.cljs: -------------------------------------------------------------------------------- 1 | (js/console.log "in build-custom.cljs") 2 | (set! (.-innerHTML (js/document.getElementById "cljs-target")) "Changed!") -------------------------------------------------------------------------------- /test/expected/exclude-extensions/main.js: -------------------------------------------------------------------------------- 1 | // /Users/bjorn/Documents/GitHub/bun-plugin-html/test/starting/js/index.ts 2 | function fromJs() { 3 | console.log("from js/index.ts"); 4 | } 5 | 6 | // test/starting/main.ts 7 | console.log("Running JS for browser"); 8 | fromJs(); 9 | document.querySelector("#js-target").innerHTML = "Changed!"; 10 | -------------------------------------------------------------------------------- /test/expected/exclude-selector/images/shubham-dhage-unsplash.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BjornTheProgrammer/bun-plugin-html/ea72d4d079744ad3a4383aa39e9e2c62b1ea3d1a/test/expected/exclude-selector/images/shubham-dhage-unsplash.jpg -------------------------------------------------------------------------------- /test/expected/exclude-selector/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Hello World! 9 | 10 | 11 |

Hello World

12 | 13 |
14 |

This should be changed by JS

15 |

This should be changed by CLJS

16 |
17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /test/expected/exclude-selector/js/build-custom.cljs: -------------------------------------------------------------------------------- 1 | (js/console.log "in build-custom.cljs") 2 | (set! (.-innerHTML (js/document.getElementById "cljs-target")) "Changed!") -------------------------------------------------------------------------------- /test/expected/exclude-selector/main.js: -------------------------------------------------------------------------------- 1 | // /Users/bjorn/Documents/GitHub/bun-plugin-html/test/starting/js/index.ts 2 | function fromJs() { 3 | console.log("from js/index.ts"); 4 | } 5 | 6 | // test/starting/main.ts 7 | console.log("Running JS for browser"); 8 | fromJs(); 9 | document.querySelector("#js-target").innerHTML = "Changed!"; 10 | -------------------------------------------------------------------------------- /test/expected/html/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BjornTheProgrammer/bun-plugin-html/ea72d4d079744ad3a4383aa39e9e2c62b1ea3d1a/test/expected/html/images/favicon.ico -------------------------------------------------------------------------------- /test/expected/html/images/shubham-dhage-unsplash.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BjornTheProgrammer/bun-plugin-html/ea72d4d079744ad3a4383aa39e9e2c62b1ea3d1a/test/expected/html/images/shubham-dhage-unsplash.jpg -------------------------------------------------------------------------------- /test/expected/html/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Hello World! 9 | 10 | 11 |

Hello World

12 | 13 |
14 |

This should be changed by JS

15 |

This should be changed by CLJS

16 |
17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /test/expected/html/js/build-custom.cljs: -------------------------------------------------------------------------------- 1 | (js/console.log "in build-custom.cljs") 2 | (set! (.-innerHTML (js/document.getElementById "cljs-target")) "Changed!") -------------------------------------------------------------------------------- /test/expected/html/main.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: #000; 3 | color: #fff; 4 | } 5 | -------------------------------------------------------------------------------- /test/expected/html/main.js: -------------------------------------------------------------------------------- 1 | // /Users/bjorn/Documents/GitHub/bun-plugin-html/test/starting/js/index.ts 2 | function fromJs() { 3 | console.log("from js/index.ts"); 4 | } 5 | 6 | // test/starting/main.ts 7 | console.log("Running JS for browser"); 8 | fromJs(); 9 | document.querySelector("#js-target").innerHTML = "Changed!"; 10 | -------------------------------------------------------------------------------- /test/expected/html/tailwind.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /test/expected/inline-css/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BjornTheProgrammer/bun-plugin-html/ea72d4d079744ad3a4383aa39e9e2c62b1ea3d1a/test/expected/inline-css/images/favicon.ico -------------------------------------------------------------------------------- /test/expected/inline-css/images/shubham-dhage-unsplash.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BjornTheProgrammer/bun-plugin-html/ea72d4d079744ad3a4383aa39e9e2c62b1ea3d1a/test/expected/inline-css/images/shubham-dhage-unsplash.jpg -------------------------------------------------------------------------------- /test/expected/inline-css/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 10 | 14 | 15 | Hello World! 16 | 17 | 18 |

Hello World

19 | 20 |
21 |

This should be changed by JS

22 |

This should be changed by CLJS

23 |
24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /test/expected/inline-css/js/build-custom.cljs: -------------------------------------------------------------------------------- 1 | (js/console.log "in build-custom.cljs") 2 | (set! (.-innerHTML (js/document.getElementById "cljs-target")) "Changed!") -------------------------------------------------------------------------------- /test/expected/inline-css/main.js: -------------------------------------------------------------------------------- 1 | // /Users/bjorn/Documents/GitHub/bun-plugin-html/test/starting/js/index.ts 2 | function fromJs() { 3 | console.log("from js/index.ts"); 4 | } 5 | 6 | // test/starting/main.ts 7 | console.log("Running JS for browser"); 8 | fromJs(); 9 | document.querySelector("#js-target").innerHTML = "Changed!"; 10 | -------------------------------------------------------------------------------- /test/expected/inline-js/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BjornTheProgrammer/bun-plugin-html/ea72d4d079744ad3a4383aa39e9e2c62b1ea3d1a/test/expected/inline-js/images/favicon.ico -------------------------------------------------------------------------------- /test/expected/inline-js/images/shubham-dhage-unsplash.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BjornTheProgrammer/bun-plugin-html/ea72d4d079744ad3a4383aa39e9e2c62b1ea3d1a/test/expected/inline-js/images/shubham-dhage-unsplash.jpg -------------------------------------------------------------------------------- /test/expected/inline-js/js/build-custom.cljs: -------------------------------------------------------------------------------- 1 | (js/console.log "in build-custom.cljs") 2 | (set! (.-innerHTML (js/document.getElementById "cljs-target")) "Changed!") -------------------------------------------------------------------------------- /test/expected/inline-js/main.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: #000; 3 | color: #fff; 4 | } 5 | -------------------------------------------------------------------------------- /test/expected/inline-js/tailwind.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /test/expected/inline-minify/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BjornTheProgrammer/bun-plugin-html/ea72d4d079744ad3a4383aa39e9e2c62b1ea3d1a/test/expected/inline-minify/images/favicon.ico -------------------------------------------------------------------------------- /test/expected/inline-minify/images/shubham-dhage-unsplash.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BjornTheProgrammer/bun-plugin-html/ea72d4d079744ad3a4383aa39e9e2c62b1ea3d1a/test/expected/inline-minify/images/shubham-dhage-unsplash.jpg -------------------------------------------------------------------------------- /test/expected/inline-minify/js/build-custom.cljs: -------------------------------------------------------------------------------- 1 | (js/console.log "in build-custom.cljs") 2 | (set! (.-innerHTML (js/document.getElementById "cljs-target")) "Changed!") -------------------------------------------------------------------------------- /test/expected/inline/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BjornTheProgrammer/bun-plugin-html/ea72d4d079744ad3a4383aa39e9e2c62b1ea3d1a/test/expected/inline/images/favicon.ico -------------------------------------------------------------------------------- /test/expected/inline/images/shubham-dhage-unsplash.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BjornTheProgrammer/bun-plugin-html/ea72d4d079744ad3a4383aa39e9e2c62b1ea3d1a/test/expected/inline/images/shubham-dhage-unsplash.jpg -------------------------------------------------------------------------------- /test/expected/inline/js/build-custom.cljs: -------------------------------------------------------------------------------- 1 | (js/console.log "in build-custom.cljs") 2 | (set! (.-innerHTML (js/document.getElementById "cljs-target")) "Changed!") -------------------------------------------------------------------------------- /test/expected/minify-custom-options/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BjornTheProgrammer/bun-plugin-html/ea72d4d079744ad3a4383aa39e9e2c62b1ea3d1a/test/expected/minify-custom-options/images/favicon.ico -------------------------------------------------------------------------------- /test/expected/minify-custom-options/images/shubham-dhage-unsplash.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BjornTheProgrammer/bun-plugin-html/ea72d4d079744ad3a4383aa39e9e2c62b1ea3d1a/test/expected/minify-custom-options/images/shubham-dhage-unsplash.jpg -------------------------------------------------------------------------------- /test/expected/minify-custom-options/index.html: -------------------------------------------------------------------------------- 1 | Hello World!

Hello World

This should be changed by JS

This should be changed by CLJS

-------------------------------------------------------------------------------- /test/expected/minify-custom-options/js/build-custom.cljs: -------------------------------------------------------------------------------- 1 | (js/console.log "in build-custom.cljs") 2 | (set! (.-innerHTML (js/document.getElementById "cljs-target")) "Changed!") -------------------------------------------------------------------------------- /test/expected/minify-custom-options/main.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: #000; 3 | color: #fff 4 | } -------------------------------------------------------------------------------- /test/expected/minify-custom-options/main.js: -------------------------------------------------------------------------------- 1 | function o(){console.log("from js/index.ts")}console.log("Running JS for browser");o();document.querySelector("#js-target").innerHTML="Changed!"; 2 | -------------------------------------------------------------------------------- /test/expected/minify-custom-options/tailwind.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; -------------------------------------------------------------------------------- /test/expected/minify-skip-html/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BjornTheProgrammer/bun-plugin-html/ea72d4d079744ad3a4383aa39e9e2c62b1ea3d1a/test/expected/minify-skip-html/images/favicon.ico -------------------------------------------------------------------------------- /test/expected/minify-skip-html/images/shubham-dhage-unsplash.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BjornTheProgrammer/bun-plugin-html/ea72d4d079744ad3a4383aa39e9e2c62b1ea3d1a/test/expected/minify-skip-html/images/shubham-dhage-unsplash.jpg -------------------------------------------------------------------------------- /test/expected/minify-skip-html/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Hello World! 9 | 10 | 11 |

Hello World

12 | 13 |
14 |

This should be changed by JS

15 |

This should be changed by CLJS

16 |
17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /test/expected/minify-skip-html/js/build-custom.cljs: -------------------------------------------------------------------------------- 1 | (js/console.log "in build-custom.cljs") 2 | (set! (.-innerHTML (js/document.getElementById "cljs-target")) "Changed!") -------------------------------------------------------------------------------- /test/expected/minify-skip-html/main.css: -------------------------------------------------------------------------------- 1 | body{background-color:#000;color:#fff} -------------------------------------------------------------------------------- /test/expected/minify-skip-html/main.js: -------------------------------------------------------------------------------- 1 | function o(){console.log("from js/index.ts")}console.log("Running JS for browser");o();document.querySelector("#js-target").innerHTML="Changed!"; 2 | -------------------------------------------------------------------------------- /test/expected/minify-skip-html/tailwind.css: -------------------------------------------------------------------------------- 1 | @tailwind base;@tailwind components;@tailwind utilities; -------------------------------------------------------------------------------- /test/expected/minify/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BjornTheProgrammer/bun-plugin-html/ea72d4d079744ad3a4383aa39e9e2c62b1ea3d1a/test/expected/minify/images/favicon.ico -------------------------------------------------------------------------------- /test/expected/minify/images/shubham-dhage-unsplash.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BjornTheProgrammer/bun-plugin-html/ea72d4d079744ad3a4383aa39e9e2c62b1ea3d1a/test/expected/minify/images/shubham-dhage-unsplash.jpg -------------------------------------------------------------------------------- /test/expected/minify/index.html: -------------------------------------------------------------------------------- 1 | Hello World!

Hello World

This should be changed by JS

This should be changed by CLJS

-------------------------------------------------------------------------------- /test/expected/minify/js/build-custom.cljs: -------------------------------------------------------------------------------- 1 | (js/console.log "in build-custom.cljs") 2 | (set! (.-innerHTML (js/document.getElementById "cljs-target")) "Changed!") -------------------------------------------------------------------------------- /test/expected/minify/main.css: -------------------------------------------------------------------------------- 1 | body{background-color:#000;color:#fff} -------------------------------------------------------------------------------- /test/expected/minify/main.js: -------------------------------------------------------------------------------- 1 | function o(){console.log("from js/index.ts")}console.log("Running JS for browser");o();document.querySelector("#js-target").innerHTML="Changed!"; 2 | -------------------------------------------------------------------------------- /test/expected/minify/tailwind.css: -------------------------------------------------------------------------------- 1 | @tailwind base;@tailwind components;@tailwind utilities; -------------------------------------------------------------------------------- /test/expected/naming/assets/build-custom.cljs: -------------------------------------------------------------------------------- 1 | (js/console.log "in build-custom.cljs") 2 | (set! (.-innerHTML (js/document.getElementById "cljs-target")) "Changed!") -------------------------------------------------------------------------------- /test/expected/naming/assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BjornTheProgrammer/bun-plugin-html/ea72d4d079744ad3a4383aa39e9e2c62b1ea3d1a/test/expected/naming/assets/favicon.ico -------------------------------------------------------------------------------- /test/expected/naming/assets/shubham-dhage-unsplash.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BjornTheProgrammer/bun-plugin-html/ea72d4d079744ad3a4383aa39e9e2c62b1ea3d1a/test/expected/naming/assets/shubham-dhage-unsplash.jpg -------------------------------------------------------------------------------- /test/expected/naming/chunks/main-vfbc17q6.js: -------------------------------------------------------------------------------- 1 | function o(){console.log("from js/index.ts")}console.log("Running JS for browser");o();document.querySelector("#js-target").innerHTML="Changed!"; 2 | -------------------------------------------------------------------------------- /test/expected/naming/css/main-1234.css: -------------------------------------------------------------------------------- 1 | body{background-color:#000;color:#fff} -------------------------------------------------------------------------------- /test/expected/naming/css/tailwind-1234.css: -------------------------------------------------------------------------------- 1 | @tailwind base;@tailwind components;@tailwind utilities; -------------------------------------------------------------------------------- /test/expected/naming/main.html: -------------------------------------------------------------------------------- 1 | Hello World!

Hello World

This should be changed by JS

This should be changed by CLJS

-------------------------------------------------------------------------------- /test/expected/preprocessor/hello.txt: -------------------------------------------------------------------------------- 1 | Hello World! -------------------------------------------------------------------------------- /test/expected/preprocessor/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BjornTheProgrammer/bun-plugin-html/ea72d4d079744ad3a4383aa39e9e2c62b1ea3d1a/test/expected/preprocessor/images/favicon.ico -------------------------------------------------------------------------------- /test/expected/preprocessor/images/shubham-dhage-unsplash.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BjornTheProgrammer/bun-plugin-html/ea72d4d079744ad3a4383aa39e9e2c62b1ea3d1a/test/expected/preprocessor/images/shubham-dhage-unsplash.jpg -------------------------------------------------------------------------------- /test/expected/preprocessor/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Hello World! 9 | 10 | 11 |

Hello World

12 | 13 |
14 |

This should be changed by JS

15 |

This should be changed by CLJS

16 |
17 | 18 | 19 | 20 |

From preprocessor

21 | -------------------------------------------------------------------------------- /test/expected/preprocessor/js/build-custom.cljs: -------------------------------------------------------------------------------- 1 | (js/console.log "in build-custom.cljs") 2 | (set! (.-innerHTML (js/document.getElementById "cljs-target")) "Changed!") -------------------------------------------------------------------------------- /test/expected/preprocessor/js/third.js: -------------------------------------------------------------------------------- 1 | // ../../../../../private/var/folders/xp/2h79jwp12nq0qc6hrr1lhlb00000gn/T/bun-build-iaWWHG/js/third.js 2 | var third_default = { hello: "world" }; 3 | export { 4 | third_default as default 5 | }; 6 | -------------------------------------------------------------------------------- /test/expected/preprocessor/main.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: #000; 3 | color: #fff; 4 | } 5 | -------------------------------------------------------------------------------- /test/expected/preprocessor/main.js: -------------------------------------------------------------------------------- 1 | // /private/var/folders/xp/2h79jwp12nq0qc6hrr1lhlb00000gn/T/bun-build-iaWWHG/js/third.js 2 | var third_default = { hello: "world" }; 3 | 4 | // /Users/bjorn/Documents/GitHub/bun-plugin-html/test/starting/js/index.ts 5 | function fromJs() { 6 | console.log("from js/index.ts"); 7 | } 8 | 9 | // ../../../../../private/var/folders/xp/2h79jwp12nq0qc6hrr1lhlb00000gn/T/bun-build-iaWWHG/main.ts 10 | console.log(third_default); 11 | console.log("Running JS for browser"); 12 | fromJs(); 13 | document.querySelector("#js-target").innerHTML = "Changed!"; 14 | -------------------------------------------------------------------------------- /test/expected/preprocessor/tailwind.css: -------------------------------------------------------------------------------- 1 | *, ::before, ::after { 2 | --tw-border-spacing-x: 0; 3 | --tw-border-spacing-y: 0; 4 | --tw-translate-x: 0; 5 | --tw-translate-y: 0; 6 | --tw-rotate: 0; 7 | --tw-skew-x: 0; 8 | --tw-skew-y: 0; 9 | --tw-scale-x: 1; 10 | --tw-scale-y: 1; 11 | --tw-pan-x: ; 12 | --tw-pan-y: ; 13 | --tw-pinch-zoom: ; 14 | --tw-scroll-snap-strictness: proximity; 15 | --tw-gradient-from-position: ; 16 | --tw-gradient-via-position: ; 17 | --tw-gradient-to-position: ; 18 | --tw-ordinal: ; 19 | --tw-slashed-zero: ; 20 | --tw-numeric-figure: ; 21 | --tw-numeric-spacing: ; 22 | --tw-numeric-fraction: ; 23 | --tw-ring-inset: ; 24 | --tw-ring-offset-width: 0px; 25 | --tw-ring-offset-color: #fff; 26 | --tw-ring-color: rgb(59 130 246 / 0.5); 27 | --tw-ring-offset-shadow: 0 0 #0000; 28 | --tw-ring-shadow: 0 0 #0000; 29 | --tw-shadow: 0 0 #0000; 30 | --tw-shadow-colored: 0 0 #0000; 31 | --tw-blur: ; 32 | --tw-brightness: ; 33 | --tw-contrast: ; 34 | --tw-grayscale: ; 35 | --tw-hue-rotate: ; 36 | --tw-invert: ; 37 | --tw-saturate: ; 38 | --tw-sepia: ; 39 | --tw-drop-shadow: ; 40 | --tw-backdrop-blur: ; 41 | --tw-backdrop-brightness: ; 42 | --tw-backdrop-contrast: ; 43 | --tw-backdrop-grayscale: ; 44 | --tw-backdrop-hue-rotate: ; 45 | --tw-backdrop-invert: ; 46 | --tw-backdrop-opacity: ; 47 | --tw-backdrop-saturate: ; 48 | --tw-backdrop-sepia: ; 49 | --tw-contain-size: ; 50 | --tw-contain-layout: ; 51 | --tw-contain-paint: ; 52 | --tw-contain-style: ; 53 | } 54 | 55 | ::backdrop { 56 | --tw-border-spacing-x: 0; 57 | --tw-border-spacing-y: 0; 58 | --tw-translate-x: 0; 59 | --tw-translate-y: 0; 60 | --tw-rotate: 0; 61 | --tw-skew-x: 0; 62 | --tw-skew-y: 0; 63 | --tw-scale-x: 1; 64 | --tw-scale-y: 1; 65 | --tw-pan-x: ; 66 | --tw-pan-y: ; 67 | --tw-pinch-zoom: ; 68 | --tw-scroll-snap-strictness: proximity; 69 | --tw-gradient-from-position: ; 70 | --tw-gradient-via-position: ; 71 | --tw-gradient-to-position: ; 72 | --tw-ordinal: ; 73 | --tw-slashed-zero: ; 74 | --tw-numeric-figure: ; 75 | --tw-numeric-spacing: ; 76 | --tw-numeric-fraction: ; 77 | --tw-ring-inset: ; 78 | --tw-ring-offset-width: 0px; 79 | --tw-ring-offset-color: #fff; 80 | --tw-ring-color: rgb(59 130 246 / 0.5); 81 | --tw-ring-offset-shadow: 0 0 #0000; 82 | --tw-ring-shadow: 0 0 #0000; 83 | --tw-shadow: 0 0 #0000; 84 | --tw-shadow-colored: 0 0 #0000; 85 | --tw-blur: ; 86 | --tw-brightness: ; 87 | --tw-contrast: ; 88 | --tw-grayscale: ; 89 | --tw-hue-rotate: ; 90 | --tw-invert: ; 91 | --tw-saturate: ; 92 | --tw-sepia: ; 93 | --tw-drop-shadow: ; 94 | --tw-backdrop-blur: ; 95 | --tw-backdrop-brightness: ; 96 | --tw-backdrop-contrast: ; 97 | --tw-backdrop-grayscale: ; 98 | --tw-backdrop-hue-rotate: ; 99 | --tw-backdrop-invert: ; 100 | --tw-backdrop-opacity: ; 101 | --tw-backdrop-saturate: ; 102 | --tw-backdrop-sepia: ; 103 | --tw-contain-size: ; 104 | --tw-contain-layout: ; 105 | --tw-contain-paint: ; 106 | --tw-contain-style: ; 107 | } 108 | 109 | /* 110 | ! tailwindcss v3.4.15 | MIT License | https://tailwindcss.com 111 | */ 112 | 113 | /* 114 | 1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4) 115 | 2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116) 116 | */ 117 | 118 | *, 119 | ::before, 120 | ::after { 121 | box-sizing: border-box; 122 | /* 1 */ 123 | border-width: 0; 124 | /* 2 */ 125 | border-style: solid; 126 | /* 2 */ 127 | border-color: #e5e7eb; 128 | /* 2 */ 129 | } 130 | 131 | ::before, 132 | ::after { 133 | --tw-content: ''; 134 | } 135 | 136 | /* 137 | 1. Use a consistent sensible line-height in all browsers. 138 | 2. Prevent adjustments of font size after orientation changes in iOS. 139 | 3. Use a more readable tab size. 140 | 4. Use the user's configured `sans` font-family by default. 141 | 5. Use the user's configured `sans` font-feature-settings by default. 142 | 6. Use the user's configured `sans` font-variation-settings by default. 143 | 7. Disable tap highlights on iOS 144 | */ 145 | 146 | html, 147 | :host { 148 | line-height: 1.5; 149 | /* 1 */ 150 | -webkit-text-size-adjust: 100%; 151 | /* 2 */ 152 | -moz-tab-size: 4; 153 | /* 3 */ 154 | -o-tab-size: 4; 155 | tab-size: 4; 156 | /* 3 */ 157 | font-family: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; 158 | /* 4 */ 159 | font-feature-settings: normal; 160 | /* 5 */ 161 | font-variation-settings: normal; 162 | /* 6 */ 163 | -webkit-tap-highlight-color: transparent; 164 | /* 7 */ 165 | } 166 | 167 | /* 168 | 1. Remove the margin in all browsers. 169 | 2. Inherit line-height from `html` so users can set them as a class directly on the `html` element. 170 | */ 171 | 172 | body { 173 | margin: 0; 174 | /* 1 */ 175 | line-height: inherit; 176 | /* 2 */ 177 | } 178 | 179 | /* 180 | 1. Add the correct height in Firefox. 181 | 2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655) 182 | 3. Ensure horizontal rules are visible by default. 183 | */ 184 | 185 | hr { 186 | height: 0; 187 | /* 1 */ 188 | color: inherit; 189 | /* 2 */ 190 | border-top-width: 1px; 191 | /* 3 */ 192 | } 193 | 194 | /* 195 | Add the correct text decoration in Chrome, Edge, and Safari. 196 | */ 197 | 198 | abbr:where([title]) { 199 | -webkit-text-decoration: underline dotted; 200 | text-decoration: underline dotted; 201 | } 202 | 203 | /* 204 | Remove the default font size and weight for headings. 205 | */ 206 | 207 | h1, 208 | h2, 209 | h3, 210 | h4, 211 | h5, 212 | h6 { 213 | font-size: inherit; 214 | font-weight: inherit; 215 | } 216 | 217 | /* 218 | Reset links to optimize for opt-in styling instead of opt-out. 219 | */ 220 | 221 | a { 222 | color: inherit; 223 | text-decoration: inherit; 224 | } 225 | 226 | /* 227 | Add the correct font weight in Edge and Safari. 228 | */ 229 | 230 | b, 231 | strong { 232 | font-weight: bolder; 233 | } 234 | 235 | /* 236 | 1. Use the user's configured `mono` font-family by default. 237 | 2. Use the user's configured `mono` font-feature-settings by default. 238 | 3. Use the user's configured `mono` font-variation-settings by default. 239 | 4. Correct the odd `em` font sizing in all browsers. 240 | */ 241 | 242 | code, 243 | kbd, 244 | samp, 245 | pre { 246 | font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; 247 | /* 1 */ 248 | font-feature-settings: normal; 249 | /* 2 */ 250 | font-variation-settings: normal; 251 | /* 3 */ 252 | font-size: 1em; 253 | /* 4 */ 254 | } 255 | 256 | /* 257 | Add the correct font size in all browsers. 258 | */ 259 | 260 | small { 261 | font-size: 80%; 262 | } 263 | 264 | /* 265 | Prevent `sub` and `sup` elements from affecting the line height in all browsers. 266 | */ 267 | 268 | sub, 269 | sup { 270 | font-size: 75%; 271 | line-height: 0; 272 | position: relative; 273 | vertical-align: baseline; 274 | } 275 | 276 | sub { 277 | bottom: -0.25em; 278 | } 279 | 280 | sup { 281 | top: -0.5em; 282 | } 283 | 284 | /* 285 | 1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297) 286 | 2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016) 287 | 3. Remove gaps between table borders by default. 288 | */ 289 | 290 | table { 291 | text-indent: 0; 292 | /* 1 */ 293 | border-color: inherit; 294 | /* 2 */ 295 | border-collapse: collapse; 296 | /* 3 */ 297 | } 298 | 299 | /* 300 | 1. Change the font styles in all browsers. 301 | 2. Remove the margin in Firefox and Safari. 302 | 3. Remove default padding in all browsers. 303 | */ 304 | 305 | button, 306 | input, 307 | optgroup, 308 | select, 309 | textarea { 310 | font-family: inherit; 311 | /* 1 */ 312 | font-feature-settings: inherit; 313 | /* 1 */ 314 | font-variation-settings: inherit; 315 | /* 1 */ 316 | font-size: 100%; 317 | /* 1 */ 318 | font-weight: inherit; 319 | /* 1 */ 320 | line-height: inherit; 321 | /* 1 */ 322 | letter-spacing: inherit; 323 | /* 1 */ 324 | color: inherit; 325 | /* 1 */ 326 | margin: 0; 327 | /* 2 */ 328 | padding: 0; 329 | /* 3 */ 330 | } 331 | 332 | /* 333 | Remove the inheritance of text transform in Edge and Firefox. 334 | */ 335 | 336 | button, 337 | select { 338 | text-transform: none; 339 | } 340 | 341 | /* 342 | 1. Correct the inability to style clickable types in iOS and Safari. 343 | 2. Remove default button styles. 344 | */ 345 | 346 | button, 347 | input:where([type='button']), 348 | input:where([type='reset']), 349 | input:where([type='submit']) { 350 | -webkit-appearance: button; 351 | /* 1 */ 352 | background-color: transparent; 353 | /* 2 */ 354 | background-image: none; 355 | /* 2 */ 356 | } 357 | 358 | /* 359 | Use the modern Firefox focus style for all focusable elements. 360 | */ 361 | 362 | :-moz-focusring { 363 | outline: auto; 364 | } 365 | 366 | /* 367 | Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737) 368 | */ 369 | 370 | :-moz-ui-invalid { 371 | box-shadow: none; 372 | } 373 | 374 | /* 375 | Add the correct vertical alignment in Chrome and Firefox. 376 | */ 377 | 378 | progress { 379 | vertical-align: baseline; 380 | } 381 | 382 | /* 383 | Correct the cursor style of increment and decrement buttons in Safari. 384 | */ 385 | 386 | ::-webkit-inner-spin-button, 387 | ::-webkit-outer-spin-button { 388 | height: auto; 389 | } 390 | 391 | /* 392 | 1. Correct the odd appearance in Chrome and Safari. 393 | 2. Correct the outline style in Safari. 394 | */ 395 | 396 | [type='search'] { 397 | -webkit-appearance: textfield; 398 | /* 1 */ 399 | outline-offset: -2px; 400 | /* 2 */ 401 | } 402 | 403 | /* 404 | Remove the inner padding in Chrome and Safari on macOS. 405 | */ 406 | 407 | ::-webkit-search-decoration { 408 | -webkit-appearance: none; 409 | } 410 | 411 | /* 412 | 1. Correct the inability to style clickable types in iOS and Safari. 413 | 2. Change font properties to `inherit` in Safari. 414 | */ 415 | 416 | ::-webkit-file-upload-button { 417 | -webkit-appearance: button; 418 | /* 1 */ 419 | font: inherit; 420 | /* 2 */ 421 | } 422 | 423 | /* 424 | Add the correct display in Chrome and Safari. 425 | */ 426 | 427 | summary { 428 | display: list-item; 429 | } 430 | 431 | /* 432 | Removes the default spacing and border for appropriate elements. 433 | */ 434 | 435 | blockquote, 436 | dl, 437 | dd, 438 | h1, 439 | h2, 440 | h3, 441 | h4, 442 | h5, 443 | h6, 444 | hr, 445 | figure, 446 | p, 447 | pre { 448 | margin: 0; 449 | } 450 | 451 | fieldset { 452 | margin: 0; 453 | padding: 0; 454 | } 455 | 456 | legend { 457 | padding: 0; 458 | } 459 | 460 | ol, 461 | ul, 462 | menu { 463 | list-style: none; 464 | margin: 0; 465 | padding: 0; 466 | } 467 | 468 | /* 469 | Reset default styling for dialogs. 470 | */ 471 | 472 | dialog { 473 | padding: 0; 474 | } 475 | 476 | /* 477 | Prevent resizing textareas horizontally by default. 478 | */ 479 | 480 | textarea { 481 | resize: vertical; 482 | } 483 | 484 | /* 485 | 1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300) 486 | 2. Set the default placeholder color to the user's configured gray 400 color. 487 | */ 488 | 489 | input::-moz-placeholder, textarea::-moz-placeholder { 490 | opacity: 1; 491 | /* 1 */ 492 | color: #9ca3af; 493 | /* 2 */ 494 | } 495 | 496 | input::placeholder, 497 | textarea::placeholder { 498 | opacity: 1; 499 | /* 1 */ 500 | color: #9ca3af; 501 | /* 2 */ 502 | } 503 | 504 | /* 505 | Set the default cursor for buttons. 506 | */ 507 | 508 | button, 509 | [role="button"] { 510 | cursor: pointer; 511 | } 512 | 513 | /* 514 | Make sure disabled buttons don't get the pointer cursor. 515 | */ 516 | 517 | :disabled { 518 | cursor: default; 519 | } 520 | 521 | /* 522 | 1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14) 523 | 2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210) 524 | This can trigger a poorly considered lint error in some tools but is included by design. 525 | */ 526 | 527 | img, 528 | svg, 529 | video, 530 | canvas, 531 | audio, 532 | iframe, 533 | embed, 534 | object { 535 | display: block; 536 | /* 1 */ 537 | vertical-align: middle; 538 | /* 2 */ 539 | } 540 | 541 | /* 542 | Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14) 543 | */ 544 | 545 | img, 546 | video { 547 | max-width: 100%; 548 | height: auto; 549 | } 550 | 551 | /* Make elements with the HTML hidden attribute stay hidden by default */ 552 | 553 | [hidden]:where(:not([hidden="until-found"])) { 554 | display: none; 555 | } 556 | 557 | .bg-white { 558 | --tw-bg-opacity: 1; 559 | background-color: rgb(255 255 255 / var(--tw-bg-opacity, 1)); 560 | } 561 | 562 | .text-black { 563 | --tw-text-opacity: 1; 564 | color: rgb(0 0 0 / var(--tw-text-opacity, 1)); 565 | } 566 | -------------------------------------------------------------------------------- /test/expected/resolution-inner/index.js: -------------------------------------------------------------------------------- 1 | // /Users/bjorn/Documents/GitHub/bun-plugin-html/test/resolution/moduleA.ts 2 | var getARandomNumber = () => Math.floor(Math.random() * 1000); 3 | 4 | // test/resolution/index.ts 5 | console.log("random number:", getARandomNumber()); 6 | -------------------------------------------------------------------------------- /test/expected/resolution-inner/inner/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Document 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /test/expected/resolution/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Document 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /test/expected/resolution/index.js: -------------------------------------------------------------------------------- 1 | // /Users/bjorn/Documents/GitHub/bun-plugin-html/test/resolution/moduleA.ts 2 | var getARandomNumber = () => Math.floor(Math.random() * 1000); 3 | 4 | // test/resolution/index.ts 5 | console.log("random number:", getARandomNumber()); 6 | -------------------------------------------------------------------------------- /test/html.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from 'bun:test'; 2 | import fs from 'node:fs'; 3 | import { sleep, sleepSync } from 'bun'; 4 | import html from '../src/index'; 5 | import { emptyDir, testIfFileExists } from './utils'; 6 | 7 | describe('Testing Generation of HTML', async () => { 8 | const generationDirectory = './test/generation/html'; 9 | const expectedDirectory = './test/expected/html'; 10 | 11 | if (fs.existsSync(generationDirectory)) emptyDir(generationDirectory); 12 | 13 | await Bun.build({ 14 | entrypoints: ['./test/starting/index.html'], 15 | outdir: generationDirectory, 16 | plugins: [html()], 17 | naming: '[dir]/[name].[ext]', 18 | }); 19 | 20 | testIfFileExists(generationDirectory, expectedDirectory, 'index.html'); 21 | testIfFileExists( 22 | generationDirectory, 23 | expectedDirectory, 24 | 'images/favicon.ico', 25 | ); 26 | testIfFileExists(generationDirectory, expectedDirectory, 'main.css'); 27 | testIfFileExists(generationDirectory, expectedDirectory, 'js/secondary.js'); 28 | testIfFileExists(generationDirectory, expectedDirectory, 'main.js'); 29 | }); 30 | -------------------------------------------------------------------------------- /test/inline-css.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from 'bun:test'; 2 | import fs from 'node:fs'; 3 | import { sleep, sleepSync } from 'bun'; 4 | import html from '../src/index'; 5 | import { emptyDir, testIfFileExists } from './utils'; 6 | 7 | describe('Testing Generation of Inline CSS', async () => { 8 | const generationDirectory = './test/generation/inline-css'; 9 | const expectedDirectory = './test/expected/inline-css'; 10 | 11 | if (fs.existsSync(generationDirectory)) emptyDir(generationDirectory); 12 | 13 | await Bun.build({ 14 | entrypoints: ['./test/starting/index.html'], 15 | outdir: generationDirectory, 16 | plugins: [html({ inline: { css: true } })], 17 | naming: '[dir]/[name].[ext]', 18 | }); 19 | 20 | testIfFileExists(generationDirectory, expectedDirectory, 'index.html'); 21 | testIfFileExists( 22 | generationDirectory, 23 | expectedDirectory, 24 | 'images/favicon.ico', 25 | ); 26 | testIfFileExists(generationDirectory, expectedDirectory, 'js/secondary.js'); 27 | testIfFileExists(generationDirectory, expectedDirectory, 'main.js'); 28 | }); 29 | -------------------------------------------------------------------------------- /test/inline-js.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from 'bun:test'; 2 | import fs from 'node:fs'; 3 | import { sleep, sleepSync } from 'bun'; 4 | import html from '../src/index'; 5 | import { emptyDir, testIfFileExists } from './utils'; 6 | 7 | describe('Testing Generation of Inline JS', async () => { 8 | const generationDirectory = './test/generation/inline-js'; 9 | const expectedDirectory = './test/expected/inline-js'; 10 | 11 | if (fs.existsSync(generationDirectory)) emptyDir(generationDirectory); 12 | 13 | await Bun.build({ 14 | entrypoints: ['./test/starting/index.html'], 15 | outdir: generationDirectory, 16 | plugins: [html({ inline: { js: true } })], 17 | naming: '[dir]/[name].[ext]', 18 | }); 19 | 20 | testIfFileExists(generationDirectory, expectedDirectory, 'index.html'); 21 | testIfFileExists( 22 | generationDirectory, 23 | expectedDirectory, 24 | 'images/favicon.ico', 25 | ); 26 | testIfFileExists(generationDirectory, expectedDirectory, 'main.css'); 27 | }); 28 | -------------------------------------------------------------------------------- /test/inline-minify.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from 'bun:test'; 2 | import fs from 'node:fs'; 3 | import { sleep, sleepSync } from 'bun'; 4 | import html from '../src/index'; 5 | import { emptyDir, testIfFileExists } from './utils'; 6 | 7 | describe('Testing Generation of Inlined Minified HTML', async () => { 8 | const generationDirectory = './test/generation/inline-minify'; 9 | const expectedDirectory = './test/expected/inline-minify'; 10 | 11 | if (fs.existsSync(generationDirectory)) emptyDir(generationDirectory); 12 | 13 | await Bun.build({ 14 | entrypoints: ['./test/starting/index.html'], 15 | minify: true, 16 | outdir: generationDirectory, 17 | plugins: [html({ inline: true })], 18 | naming: '[dir]/[name].[ext]', 19 | }); 20 | 21 | testIfFileExists(generationDirectory, expectedDirectory, 'index.html'); 22 | testIfFileExists( 23 | generationDirectory, 24 | expectedDirectory, 25 | 'images/favicon.ico', 26 | ); 27 | }); 28 | -------------------------------------------------------------------------------- /test/inline.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from 'bun:test'; 2 | import fs from 'node:fs'; 3 | import { sleep, sleepSync } from 'bun'; 4 | import html from '../src/index'; 5 | import { emptyDir, testIfFileExists } from './utils'; 6 | 7 | describe('Testing Generation of Inlined HTML', async () => { 8 | const generationDirectory = './test/generation/inline'; 9 | const expectedDirectory = './test/expected/inline'; 10 | 11 | if (fs.existsSync(generationDirectory)) emptyDir(generationDirectory); 12 | 13 | await Bun.build({ 14 | entrypoints: ['./test/starting/index.html'], 15 | outdir: generationDirectory, 16 | plugins: [html({ inline: true })], 17 | naming: '[dir]/[name].[ext]', 18 | }); 19 | 20 | testIfFileExists(generationDirectory, expectedDirectory, 'index.html'); 21 | testIfFileExists( 22 | generationDirectory, 23 | expectedDirectory, 24 | 'images/favicon.ico', 25 | ); 26 | }); 27 | -------------------------------------------------------------------------------- /test/keep-path-strings.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from 'bun:test'; 2 | import fs from 'node:fs'; 3 | import html from '../src/index'; 4 | import { emptyDir } from './utils'; 5 | 6 | describe('Testing keepOriginalPaths', async () => { 7 | const generationDirectory = './test/generation/keep-path-strings'; 8 | if (fs.existsSync(generationDirectory)) emptyDir(generationDirectory); 9 | 10 | test('Checking keepOriginalPaths is true', async () => { 11 | await Bun.build({ 12 | entrypoints: ['./test/splitting/keep-path-strings.html'], 13 | outdir: generationDirectory, 14 | plugins: [ 15 | html({ 16 | keepOriginalPaths: true, 17 | }), 18 | ], 19 | root: '.', 20 | naming: { 21 | entry: 'keep.html', 22 | chunk: '[name]-[hash].[ext]', 23 | }, 24 | }); 25 | const entryHtml = `${generationDirectory}/keep.html`; 26 | expect(fs.existsSync(entryHtml)); 27 | const content = await Bun.file(entryHtml).text(); 28 | expect(content.indexOf('x.ts') > -1).toBeTrue(); 29 | expect(content.indexOf('y.ts') > -1).toBeTrue(); 30 | }); 31 | 32 | test('Checking keepOriginalPaths is string[]', async () => { 33 | await Bun.build({ 34 | entrypoints: ['./test/splitting/keep-path-strings.html'], 35 | outdir: generationDirectory, 36 | plugins: [ 37 | html({ 38 | keepOriginalPaths: ['y.ts'], 39 | }), 40 | ], 41 | root: '.', 42 | naming: { 43 | entry: 'keep-y.html', 44 | chunk: '[name]-[hash].[ext]', 45 | }, 46 | }); 47 | const entryHtml = `${generationDirectory}/keep-y.html`; 48 | expect(fs.existsSync(entryHtml)); 49 | const content = await Bun.file(entryHtml).text(); 50 | expect(content.indexOf('x.ts') > -1).toBeFalse(); 51 | expect(content.indexOf('y.ts') > -1).toBeTrue(); 52 | }); 53 | 54 | test('Checking no keepOriginalPaths', async () => { 55 | await Bun.build({ 56 | entrypoints: ['./test/splitting/keep-path-strings.html'], 57 | outdir: generationDirectory, 58 | plugins: [html()], 59 | root: '.', 60 | naming: { 61 | entry: 'keep-none.html', 62 | chunk: '[name]-[hash].[ext]', 63 | }, 64 | }); 65 | const entryHtml = `${generationDirectory}/keep-none.html`; 66 | expect(fs.existsSync(entryHtml)); 67 | const content = await Bun.file(entryHtml).text(); 68 | expect(content.indexOf('x.ts') > -1).toBeFalse(); 69 | expect(content.indexOf('y.ts') > -1).toBeFalse(); 70 | }); 71 | }); 72 | -------------------------------------------------------------------------------- /test/minify-custom-options.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from 'bun:test'; 2 | import fs from 'node:fs'; 3 | import { sleep, sleepSync } from 'bun'; 4 | import html, { defaultMinifyOptions } from '../src/index'; 5 | import { emptyDir, testIfFileExists } from './utils'; 6 | 7 | describe('Testing Generation of Minified HTML with Custom', async () => { 8 | const generationDirectory = './test/generation/minify-custom-options'; 9 | const expectedDirectory = './test/expected/minify-custom-options'; 10 | 11 | if (fs.existsSync(generationDirectory)) emptyDir(generationDirectory); 12 | 13 | await Bun.build({ 14 | entrypoints: ['./test/starting/index.html'], 15 | outdir: generationDirectory, 16 | minify: true, 17 | plugins: [ 18 | html({ 19 | minifyOptions: { 20 | ...defaultMinifyOptions, 21 | removeStyleLinkTypeAttributes: true, 22 | minifyCSS: { format: 'beautify' }, 23 | minifyJS: { format: { beautify: true } }, 24 | }, 25 | }), 26 | ], 27 | naming: '[dir]/[name].[ext]', 28 | }); 29 | 30 | testIfFileExists(generationDirectory, expectedDirectory, 'index.html'); 31 | testIfFileExists(generationDirectory, expectedDirectory, 'main.css'); 32 | }); 33 | -------------------------------------------------------------------------------- /test/minify-skip-html.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from 'bun:test'; 2 | import fs from 'node:fs'; 3 | import { sleep, sleepSync } from 'bun'; 4 | import html from '../src/index'; 5 | import { emptyDir, testIfFileExists } from './utils'; 6 | 7 | describe('Testing Generation of Minified HTML', async () => { 8 | const generationDirectory = './test/generation/minify-skip-html'; 9 | const expectedDirectory = './test/expected/minify-skip-html'; 10 | 11 | if (fs.existsSync(generationDirectory)) emptyDir(generationDirectory); 12 | 13 | await Bun.build({ 14 | entrypoints: ['./test/starting/index.html'], 15 | outdir: generationDirectory, 16 | minify: true, 17 | plugins: [ 18 | html({ 19 | minifyOptions: { 20 | minifyHTML: false, 21 | }, 22 | }), 23 | ], 24 | naming: '[dir]/[name].[ext]', 25 | }); 26 | 27 | testIfFileExists(generationDirectory, expectedDirectory, 'index.html'); 28 | testIfFileExists(generationDirectory, expectedDirectory, 'main.css'); 29 | testIfFileExists(generationDirectory, expectedDirectory, 'main.js'); 30 | testIfFileExists(generationDirectory, expectedDirectory, 'tailwind.css'); 31 | testIfFileExists( 32 | generationDirectory, 33 | expectedDirectory, 34 | 'images/favicon.ico', 35 | ); 36 | }); 37 | -------------------------------------------------------------------------------- /test/minify.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from 'bun:test'; 2 | import fs from 'node:fs'; 3 | import { sleep, sleepSync } from 'bun'; 4 | import html from '../src/index'; 5 | import { emptyDir, testIfFileExists } from './utils'; 6 | 7 | describe('Testing Generation of Minified HTML', async () => { 8 | const generationDirectory = './test/generation/minify'; 9 | const expectedDirectory = './test/expected/minify'; 10 | 11 | if (fs.existsSync(generationDirectory)) emptyDir(generationDirectory); 12 | 13 | await Bun.build({ 14 | entrypoints: ['./test/starting/index.html'], 15 | outdir: generationDirectory, 16 | minify: true, 17 | plugins: [html()], 18 | naming: '[dir]/[name].[ext]', 19 | }); 20 | 21 | testIfFileExists(generationDirectory, expectedDirectory, 'index.html'); 22 | testIfFileExists( 23 | generationDirectory, 24 | expectedDirectory, 25 | 'images/favicon.ico', 26 | ); 27 | }); 28 | -------------------------------------------------------------------------------- /test/naming.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from 'bun:test'; 2 | import fs, { readdirSync } from 'node:fs'; 3 | import path from 'node:path'; 4 | import { Glob } from 'bun'; 5 | import html from '../src/index'; 6 | import { emptyDir, testIfFileExists } from './utils'; 7 | 8 | describe('Testing Using Custom Name', async () => { 9 | const generationDirectory = './test/generation/naming'; 10 | const expectedDirectory = './test/expected/naming'; 11 | 12 | if (fs.existsSync(generationDirectory)) emptyDir(generationDirectory); 13 | 14 | await Bun.build({ 15 | entrypoints: ['./test/starting/index.html'], 16 | outdir: generationDirectory, 17 | naming: { 18 | chunk: 'chunks/[dir]/[name]-[hash].[ext]', 19 | asset: 'assets/[name].[ext]', 20 | entry: 'main.html', 21 | }, 22 | plugins: [ 23 | html({ 24 | naming: { 25 | css: 'css/[name]-1234.[ext]', 26 | }, 27 | }), 28 | ], 29 | minify: true, 30 | }); 31 | 32 | test('main.html file exists', async () => { 33 | const filepath = path.resolve(generationDirectory, 'main.html'); 34 | expect(await Bun.file(filepath).exists()).toEqual(true); 35 | }); 36 | 37 | // testIfFileExists(generationDirectory, expectedDirectory, 'main.html'); 38 | testIfFileExists(generationDirectory, expectedDirectory, 'css/main-1234.css'); 39 | // testIfFileExists(generationDirectory, expectedDirectory, 'chunks/main-pgegyjtv.js'); 40 | // testIfFileExists(generationDirectory, expectedDirectory, 'chunks/js/secondary-yn1gbx15.js'); 41 | testIfFileExists( 42 | generationDirectory, 43 | expectedDirectory, 44 | 'assets/build-custom.cljs', 45 | ); 46 | testIfFileExists( 47 | generationDirectory, 48 | expectedDirectory, 49 | 'assets/favicon.ico', 50 | ); 51 | testIfFileExists( 52 | generationDirectory, 53 | expectedDirectory, 54 | 'assets/shubham-dhage-unsplash.jpg', 55 | ); 56 | }); 57 | -------------------------------------------------------------------------------- /test/preprocessor.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from 'bun:test'; 2 | import fs from 'node:fs'; 3 | import path from 'node:path'; 4 | import { $, sleep, sleepSync } from 'bun'; 5 | import html from '../src/index'; 6 | import { emptyDir, testIfFileExists } from './utils'; 7 | 8 | describe('Testing Tailwind Preprocessor', async () => { 9 | const generationDirectory = './test/generation/preprocessor'; 10 | const expectedDirectory = './test/expected/preprocessor'; 11 | 12 | if (fs.existsSync(generationDirectory)) emptyDir(generationDirectory); 13 | 14 | const startingDirectory = './test/starting/'; 15 | 16 | await Bun.build({ 17 | entrypoints: [path.resolve(startingDirectory, 'index.html')], 18 | outdir: generationDirectory, 19 | plugins: [ 20 | html({ 21 | async preprocessor(processor) { 22 | const files = processor.getFiles(); 23 | 24 | processor.writeFile( 25 | path.resolve(startingDirectory, './js/third.js'), 26 | `export default { hello: 'world' }`, 27 | ); 28 | 29 | for (const file of files) { 30 | if (file.extension === '.css') { 31 | const contents = 32 | await $`bun run tailwindcss -i ${file.path} --content '${startingDirectory}**/*.{html,js,ts}'` 33 | .quiet() 34 | .text(); 35 | processor.writeFile(file.path, contents); 36 | } 37 | 38 | if (file.extension === '.ts') { 39 | processor.writeFile( 40 | file.path, 41 | `import hello from "./js/third.js"\nconsole.log(hello);\n${await file.content}`, 42 | ); 43 | } 44 | 45 | if (file.extension === '.html') { 46 | const rewriter = new HTMLRewriter(); 47 | rewriter.on('body', { 48 | element(el) { 49 | el.append('

From preprocessor

', { 50 | html: true, 51 | }); 52 | }, 53 | }); 54 | 55 | const output = rewriter.transform(await file.content); 56 | processor.writeFile(file.path, output); 57 | } 58 | } 59 | 60 | processor.writeFile( 61 | path.resolve(startingDirectory, 'hello.txt'), 62 | 'Hello World!', 63 | ); 64 | }, 65 | }), 66 | ], 67 | naming: '[dir]/[name].[ext]', 68 | }); 69 | 70 | testIfFileExists(generationDirectory, expectedDirectory, 'index.html'); 71 | testIfFileExists( 72 | generationDirectory, 73 | expectedDirectory, 74 | 'images/favicon.ico', 75 | ); 76 | testIfFileExists(generationDirectory, expectedDirectory, 'main.css'); 77 | testIfFileExists(generationDirectory, expectedDirectory, 'tailwind.css'); 78 | testIfFileExists(generationDirectory, expectedDirectory, 'js/secondary.js'); 79 | testIfFileExists(generationDirectory, expectedDirectory, 'js/third.js'); 80 | testIfFileExists(generationDirectory, expectedDirectory, 'main.js'); 81 | }); 82 | -------------------------------------------------------------------------------- /test/resolution-inner.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from 'bun:test'; 2 | import fs from 'node:fs'; 3 | import { sleep, sleepSync } from 'bun'; 4 | import html from '../src/index'; 5 | import { emptyDir, testIfFileExists } from './utils'; 6 | 7 | describe('Testing Generation of HTML', async () => { 8 | const generationDirectory = './test/generation/resolution-inner'; 9 | const expectedDirectory = './test/expected/resolution-inner'; 10 | 11 | if (fs.existsSync(generationDirectory)) emptyDir(generationDirectory); 12 | 13 | const response = await Bun.build({ 14 | entrypoints: ['./test/resolution/inner/index.html'], 15 | outdir: generationDirectory, 16 | plugins: [html()], 17 | naming: '[dir]/[name].[ext]', 18 | }); 19 | 20 | // testIfFileExists(generationDirectory, expectedDirectory, 'index.html'); 21 | // testIfFileExists(generationDirectory, expectedDirectory, 'index.js'); 22 | }); 23 | -------------------------------------------------------------------------------- /test/resolution.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from 'bun:test'; 2 | import fs from 'node:fs'; 3 | import { sleep, sleepSync } from 'bun'; 4 | import html from '../src/index'; 5 | import { emptyDir, testIfFileExists } from './utils'; 6 | 7 | describe('Testing Generation of HTML', async () => { 8 | const generationDirectory = './test/generation/resolution'; 9 | const expectedDirectory = './test/expected/resolution'; 10 | 11 | if (fs.existsSync(generationDirectory)) emptyDir(generationDirectory); 12 | 13 | const response = await Bun.build({ 14 | entrypoints: ['./test/resolution/index.html'], 15 | outdir: generationDirectory, 16 | plugins: [html()], 17 | naming: '[dir]/[name].[ext]', 18 | }); 19 | 20 | testIfFileExists(generationDirectory, expectedDirectory, 'index.html'); 21 | testIfFileExists(generationDirectory, expectedDirectory, 'index.js'); 22 | }); 23 | -------------------------------------------------------------------------------- /test/resolution/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Document 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /test/resolution/index.ts: -------------------------------------------------------------------------------- 1 | import { getARandomNumber } from './moduleA'; 2 | 3 | console.log('random number:', getARandomNumber()); 4 | -------------------------------------------------------------------------------- /test/resolution/inner/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Document 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /test/resolution/moduleA.ts: -------------------------------------------------------------------------------- 1 | export const getARandomNumber = (): number => Math.floor(Math.random() * 1000); 2 | -------------------------------------------------------------------------------- /test/splitting.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from 'bun:test'; 2 | import fs from 'node:fs'; 3 | import html from '../src/index'; 4 | import { emptyDir } from './utils'; 5 | 6 | describe('Testing Splitting', async () => { 7 | const generationDirectory = './test/generation/splitting'; 8 | 9 | if (fs.existsSync(generationDirectory)) emptyDir(generationDirectory); 10 | 11 | await Bun.build({ 12 | entrypoints: ['./test/splitting/index.html'], 13 | outdir: generationDirectory, 14 | plugins: [ 15 | html({ 16 | naming: { 17 | css: '[name]-[hash].[ext]', 18 | }, 19 | }), 20 | ], 21 | root: '.', 22 | naming: { 23 | entry: '[dir]/[name].[ext]', 24 | chunk: '[name]-[hash].[ext]', 25 | }, 26 | splitting: true, 27 | }); 28 | 29 | const entryHtml = `${generationDirectory}/index.html`; 30 | expect(fs.existsSync(entryHtml)); 31 | 32 | const content = await Bun.file(entryHtml).text(); 33 | const src: string[] = content.match(/[^"\/]+\.js/g) || []; 34 | test('Checking ts module compiled', async () => { 35 | expect(src?.length).toBe(2); 36 | expect(src[0].startsWith('x-')).toBeTrue(); 37 | expect(src[1].startsWith('y-')).toBeTrue(); 38 | expect(fs.existsSync(`${generationDirectory}/${src[0]}`)); 39 | expect(fs.existsSync(`${generationDirectory}/${src[1]}`)); 40 | }); 41 | test('Checking module splitting', async () => { 42 | const c = await Bun.file(`${generationDirectory}/${src[1]}`).text(); 43 | expect(c && c.indexOf(src[0]) > -1).toBeTrue(); 44 | }); 45 | 46 | test('Checking import url', async () => { 47 | const xc = await Bun.file(`${generationDirectory}/${src[0]}`).text(); 48 | expect( 49 | xc?.includes('https://cdn.jsdelivr.net/npm/luxon@3.5.0/+esm'), 50 | ).toBeTrue(); 51 | }); 52 | 53 | test('Checking .scss compiled', async () => { 54 | const m = content.match(/style-[^.]+.css/); 55 | expect(m?.length).toBe(1); 56 | expect(fs.existsSync(`${generationDirectory}/${m?.[0]}`)); 57 | }); 58 | }); 59 | -------------------------------------------------------------------------------- /test/splitting/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Hello A,B,C 4 | 5 | 6 | 7 | 8 | 9 | 10 | Chrome 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /test/splitting/keep-path-strings.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 10 |
Hello
11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /test/splitting/style.scss: -------------------------------------------------------------------------------- 1 | $font-stack: Helvetica, sans-serif; 2 | $primary-color: #333; 3 | 4 | body { 5 | font: 100% $font-stack; 6 | color: $primary-color; 7 | } 8 | -------------------------------------------------------------------------------- /test/splitting/x.ts: -------------------------------------------------------------------------------- 1 | import { DateTime } from 'https://cdn.jsdelivr.net/npm/luxon@3.5.0/+esm'; 2 | export function Hello(name: string) { 3 | console.log( 4 | `hello, ${name || 'world'}! The date in New York is ${DateTime.now().setZone('America/New_York').toLocaleString()}!`, 5 | ); 6 | } 7 | -------------------------------------------------------------------------------- /test/splitting/y.ts: -------------------------------------------------------------------------------- 1 | import { Hello } from './x.ts'; 2 | 3 | console.log(Hello('y')); 4 | -------------------------------------------------------------------------------- /test/starting/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BjornTheProgrammer/bun-plugin-html/ea72d4d079744ad3a4383aa39e9e2c62b1ea3d1a/test/starting/images/favicon.ico -------------------------------------------------------------------------------- /test/starting/images/shubham-dhage-unsplash.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BjornTheProgrammer/bun-plugin-html/ea72d4d079744ad3a4383aa39e9e2c62b1ea3d1a/test/starting/images/shubham-dhage-unsplash.jpg -------------------------------------------------------------------------------- /test/starting/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Hello World! 9 | 10 | 11 |

Hello World

12 | 13 |
14 |

This should be changed by JS

15 |

This should be changed by CLJS

16 |
17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /test/starting/js/build-custom.cljs: -------------------------------------------------------------------------------- 1 | (js/console.log "in build-custom.cljs") 2 | (set! (.-innerHTML (js/document.getElementById "cljs-target")) "Changed!") -------------------------------------------------------------------------------- /test/starting/js/index.ts: -------------------------------------------------------------------------------- 1 | export function fromJs() { 2 | console.log('from js/index.ts'); 3 | } 4 | -------------------------------------------------------------------------------- /test/starting/js/secondary.tsx: -------------------------------------------------------------------------------- 1 | import type * as React from 'react'; 2 | import { render } from 'react-dom'; 3 | 4 | console.log('in secondary.tsx'); 5 | 6 | window.addEventListener('DOMContentLoaded', () => { 7 | const root = document.createElement('div'); 8 | document.body.appendChild(root); 9 | render(, root); 10 | }); 11 | 12 | function App(): React.ReactNode { 13 | return ( 14 |
15 |

Hello from React!

16 |
17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /test/starting/main.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: #000; 3 | color: #fff; 4 | } 5 | -------------------------------------------------------------------------------- /test/starting/main.ts: -------------------------------------------------------------------------------- 1 | import { fromJs } from './js'; 2 | 3 | console.log('Running JS for browser'); 4 | 5 | fromJs(); 6 | 7 | document.querySelector('#js-target').innerHTML = 'Changed!'; 8 | -------------------------------------------------------------------------------- /test/starting/tailwind.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /test/utils.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from 'bun:test'; 2 | import fs from 'node:fs'; 3 | import path from 'node:path'; 4 | import chalk from 'chalk'; 5 | import * as Diff from 'diff'; 6 | import { getLines, returnLineNumberOfOccurance } from '../src/utils'; 7 | 8 | export function emptyDir(dirPath: string) { 9 | const dirContents = fs.readdirSync(dirPath); // List dir content 10 | 11 | for (const fileOrDirPath of dirContents) { 12 | try { 13 | // Get Full path 14 | const fullPath = path.join(dirPath, fileOrDirPath); 15 | const stat = fs.statSync(fullPath); 16 | if (stat.isDirectory()) { 17 | // It's a sub directory 18 | if (fs.readdirSync(fullPath).length) emptyDir(fullPath); 19 | // If the dir is not empty then remove it's contents too(recursively) 20 | fs.rmdirSync(fullPath); 21 | } else fs.unlinkSync(fullPath); // It's a file 22 | } catch (ex) { 23 | console.error((ex as Error).message); 24 | } 25 | } 26 | } 27 | 28 | export function stripCommnets(source: string) { 29 | return source.replace(/\/\*[\s\S]*?\*\/|([^\\:]|^)\/\/.*$/gm, '$1'); 30 | } 31 | 32 | export function testIfExplicitFilePathExists( 33 | generatedFileLocation: string, 34 | expectedFileLocation: string, 35 | file: string, 36 | ) { 37 | test(`Checking for ${file}`, async () => { 38 | try { 39 | const expected = Bun.file(generatedFileLocation); 40 | const generated = Bun.file(expectedFileLocation); 41 | 42 | if (!(await expected.exists())) { 43 | throw new Error( 44 | `Could not find file '${file}' in '${expectedFileLocation}'`, 45 | ); 46 | } 47 | if (!(await generated.exists())) { 48 | throw new Error( 49 | `Could not find file '${file}' in '${generatedFileLocation}'`, 50 | ); 51 | } 52 | 53 | const expectedText = stripCommnets(await expected.text()); 54 | const generatedText = stripCommnets(await generated.text()); 55 | 56 | if (expectedText !== generatedText) { 57 | const diff = Diff.diffChars(expectedText, generatedText); 58 | 59 | let lineNumber = 0; 60 | let firstOccurance = false; 61 | const result = diff 62 | .map((part) => { 63 | if (part.added || part.removed) firstOccurance = true; 64 | if (firstOccurance === false) 65 | lineNumber += part.value.split('\n').length; 66 | if (part.added) return chalk.green(part.value); 67 | if (part.removed) return chalk.red(part.value); 68 | return part.value; 69 | }) 70 | .join(''); 71 | 72 | const lines = getLines(result, 5, lineNumber + 3); 73 | console.log(lines); 74 | 75 | throw new Error( 76 | `File '${file}' in '${generatedFileLocation}' does not match '${expectedFileLocation}'`, 77 | ); 78 | } 79 | 80 | expect().pass('Files exist and match'); 81 | } catch (error) { 82 | expect().fail((error as Error).message); 83 | } 84 | }); 85 | } 86 | 87 | export function testIfFileExists( 88 | generationDirectory: string, 89 | expectedDirectory: string, 90 | file: string, 91 | ) { 92 | const generatedFileLocation = path.resolve(generationDirectory, file); 93 | const expectedFileLocation = path.resolve(expectedDirectory, file); 94 | 95 | return testIfExplicitFilePathExists( 96 | generatedFileLocation, 97 | expectedFileLocation, 98 | file, 99 | ); 100 | } 101 | 102 | export function testFileDoesntExist(generationDirectory: string, file: string) { 103 | const generatedFileLocation = path.resolve(generationDirectory, file); 104 | 105 | test(`Checking that ${file} does NOT exist`, async () => { 106 | try { 107 | expect(await Bun.file(generatedFileLocation).exists()).toBeFalse(); 108 | } catch (error) { 109 | console.error(error); 110 | expect().fail( 111 | `Found '${file}' in '${generationDirectory}' when it shouldn't exist`, 112 | ); 113 | } 114 | }); 115 | } 116 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["ESNext"], 4 | "module": "esnext", 5 | "target": "esnext", 6 | "moduleResolution": "bundler", 7 | "moduleDetection": "force", 8 | "allowImportingTsExtensions": true, 9 | "noEmit": true, 10 | "composite": true, 11 | "strict": true, 12 | "downlevelIteration": true, 13 | "skipLibCheck": true, 14 | "jsx": "react-jsx", 15 | "allowSyntheticDefaultImports": true, 16 | "forceConsistentCasingInFileNames": true, 17 | "allowJs": true, 18 | "types": [ 19 | "bun-types" // add Bun global 20 | ] 21 | } 22 | } 23 | --------------------------------------------------------------------------------