├── .all-contributorsrc ├── .changeset ├── README.md └── config.json ├── .gitattributes ├── .github ├── FUNDING.yml └── workflows │ ├── nanobundle-integration.yml │ └── release.yml ├── .gitignore ├── .yarn └── releases │ └── yarn-4.3.0.cjs ├── .yarnrc.yml ├── LICENSE ├── README.md ├── package.json ├── packages └── nanobundle │ ├── .gitattributes │ ├── .gitignore │ ├── CHANGELOG.md │ ├── README.md │ ├── build.mjs │ ├── package.json │ ├── src │ ├── __snapshots__ │ │ ├── config.test.ts.snap │ │ ├── context.test.ts.snap │ │ └── entry.test.ts.snap │ ├── bin.ts │ ├── cli.ts │ ├── commands │ │ ├── build │ │ │ ├── build.machine.ts │ │ │ ├── build.machine.typegen.ts │ │ │ └── index.ts │ │ └── clean │ │ │ └── index.ts │ ├── common.ts │ ├── context.test.ts │ ├── context.ts │ ├── entry.test.ts │ ├── entry.ts │ ├── entryGroup.test.ts │ ├── entryGroup.ts │ ├── errors.ts │ ├── formatUtils.ts │ ├── fsUtils.ts │ ├── importMaps.test.ts │ ├── importMaps.ts │ ├── manifest.ts │ ├── outputFile.ts │ ├── plugins │ │ ├── esbuildEmbedPlugin.ts │ │ ├── esbuildImportMapsPlugin.ts │ │ └── esbuildNanobundlePlugin.ts │ ├── reporter.ts │ ├── target.test.ts │ ├── target.ts │ └── tasks │ │ ├── buildBundleTask.ts │ │ ├── buildFileTask.ts │ │ ├── buildTypeTask.ts │ │ ├── chmodBinTask.ts │ │ ├── cleanupTask.ts │ │ ├── emitTask.ts │ │ └── reportEmitResultsTask.ts │ └── tsconfig.json ├── renovate.json └── yarn.lock /.all-contributorsrc: -------------------------------------------------------------------------------- 1 | { 2 | "projectName": "espub", 3 | "projectOwner": "cometkim", 4 | "repoType": "github", 5 | "repoHost": "https://github.com", 6 | "files": [ 7 | "README.md", 8 | "packages/nanobundle/README.md" 9 | ], 10 | "imageSize": 100, 11 | "commit": true, 12 | "commitConvention": "none", 13 | "contributors": [ 14 | { 15 | "login": "cometkim", 16 | "name": "Hyeseong Kim", 17 | "avatar_url": "https://avatars.githubusercontent.com/u/9696352?v=4", 18 | "profile": "https://blog.cometkim.kr/", 19 | "contributions": [ 20 | "code", 21 | "maintenance" 22 | ] 23 | }, 24 | { 25 | "login": "eolme", 26 | "name": "Anton Petrov", 27 | "avatar_url": "https://avatars.githubusercontent.com/u/11076888?v=4", 28 | "profile": "https://github.com/eolme", 29 | "contributions": [ 30 | "code" 31 | ] 32 | }, 33 | { 34 | "login": "easylogic", 35 | "name": "jinho park", 36 | "avatar_url": "https://avatars.githubusercontent.com/u/591983?v=4", 37 | "profile": "https://www.easylogic.studio/", 38 | "contributions": [ 39 | "test", 40 | "bug" 41 | ] 42 | }, 43 | { 44 | "login": "manuth", 45 | "name": "Manuel Thalmann", 46 | "avatar_url": "https://avatars.githubusercontent.com/u/7085564?v=4", 47 | "profile": "http://nuth.ch/", 48 | "contributions": [ 49 | "code" 50 | ] 51 | } 52 | ], 53 | "contributorsPerLine": 7, 54 | "linkToUsage": false 55 | } 56 | -------------------------------------------------------------------------------- /.changeset/README.md: -------------------------------------------------------------------------------- 1 | # Changesets 2 | 3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works 4 | with multi-package repos, or single-package repos to help you version and publish your code. You can 5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets) 6 | 7 | We have a quick list of common questions to get you started engaging with this project in 8 | [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) 9 | -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@1.6.1/schema.json", 3 | "changelog": "@changesets/cli/changelog", 4 | "commit": true, 5 | "linked": [], 6 | "access": "restricted", 7 | "baseBranch": "main", 8 | "updateInternalDependencies": "patch", 9 | "ignore": [] 10 | } 11 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | .yarn/** linguist-vendored 2 | .yarn/releases/* binary linguist-vendored 3 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: cometkim 2 | -------------------------------------------------------------------------------- /.github/workflows/nanobundle-integration.yml: -------------------------------------------------------------------------------- 1 | name: "[nanobundle] Integration" 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - main 7 | push: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | chore: 13 | name: Checking chores 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Checkout Repo 17 | uses: actions/checkout@v4 18 | 19 | - name: Setup Node.js 18.x 20 | uses: actions/setup-node@v4 21 | with: 22 | node-version: 18 23 | cache: yarn 24 | 25 | - name: Install Dependencies 26 | run: yarn install --immutable 27 | 28 | - name: Type Check 29 | run: yarn workspace nanobundle type-check 30 | 31 | test: 32 | name: Running unit tests 33 | runs-on: ubuntu-latest 34 | steps: 35 | - name: Checkout Repo 36 | uses: actions/checkout@v4 37 | 38 | - name: Setup Node.js 18.x 39 | uses: actions/setup-node@v4 40 | with: 41 | node-version: 18 42 | cache: yarn 43 | 44 | - name: Install Dependencies 45 | run: yarn install --immutable 46 | 47 | - name: Execute Tests 48 | run: yarn workspace nanobundle test run --coverage 49 | 50 | - uses: codecov/codecov-action@v4 51 | with: 52 | directory: packages/nanobundle 53 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: 7 | - main 8 | 9 | jobs: 10 | release: 11 | name: Release 12 | runs-on: ubuntu-latest 13 | permissions: 14 | contents: write 15 | pull-requests: write 16 | 17 | steps: 18 | - name: Checkout Repo 19 | uses: actions/checkout@v4 20 | with: 21 | # This makes Actions fetch all Git history so that Changesets can generate changelogs with the correct commits 22 | fetch-depth: 0 23 | 24 | - name: Setup Node.js 18.x 25 | uses: actions/setup-node@v4 26 | with: 27 | node-version: 18 28 | cache: yarn 29 | 30 | - name: Install Dependencies 31 | run: yarn install --immutable 32 | 33 | - name: Create Release Pull Request or Publish to npm 34 | uses: cometkim/yarn-changeset-action@v1 35 | with: 36 | autoPublish: true 37 | env: 38 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 39 | NPM_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }} 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.log 3 | *.tsbuildinfo 4 | 5 | node_modules/ 6 | coverage/ 7 | 8 | /bin.mjs 9 | /bin.mjs.LEGAL.txt 10 | /bin.min.mjs 11 | /bin.min.mjs.LEGAL.txt 12 | 13 | .yarn/* 14 | !.yarn/patches 15 | !.yarn/releases 16 | !.yarn/plugins 17 | !.yarn/sdks 18 | !.yarn/versions 19 | .pnp.* 20 | 21 | rome-logs/ 22 | -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nmMode: hardlinks-global 2 | 3 | nodeLinker: node-modules 4 | 5 | yarnPath: .yarn/releases/yarn-4.3.0.cjs 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Hyeseong Kim 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 | # espub 2 | 3 | [![LICENSE - MIT](https://img.shields.io/github/license/cometkim/espub)](#license) 4 | [![All Contributors](https://img.shields.io/badge/all_contributors-4-orange.svg?style=flat-square)](#contributors-) 5 | 6 | 7 | Monorepo of espub: the perfect build tool for libraries, powered by [esbuild] 8 | 9 | > [!NOTE] 10 | > espub is under development and will eventually completely replace nanobundle. You can find nanobundle docs [here](packages/nanobundle). 11 | 12 | [esbuild]: https://esbuild.github.io/ 13 | 14 | ## Contributors ✨ 15 | 16 | Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)): 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 |
Hyeseong Kim
Hyeseong Kim

💻 🚧
Anton Petrov
Anton Petrov

💻
jinho park
jinho park

⚠️ 🐛
Manuel Thalmann
Manuel Thalmann

💻
31 | 32 | 33 | 34 | 35 | 36 | 37 | ## LICENSE 38 | 39 | [MIT](LICENSE) 40 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "project", 3 | "private": true, 4 | "workspaces": [ 5 | "packages/*" 6 | ], 7 | "scripts": { 8 | "changeset": "changeset" 9 | }, 10 | "devDependencies": { 11 | "@changesets/cli": "^2.27.1", 12 | "all-contributors-cli": "^6.26.1", 13 | "typescript": "^5.3.3" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /packages/nanobundle/.gitattributes: -------------------------------------------------------------------------------- 1 | *.typegen.ts linguist-generated 2 | -------------------------------------------------------------------------------- /packages/nanobundle/.gitignore: -------------------------------------------------------------------------------- 1 | /bin.mjs 2 | /bin.mjs.LEGAL.txt 3 | -------------------------------------------------------------------------------- /packages/nanobundle/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # nanobundle 2 | 3 | ## 2.1.0 4 | 5 | ### Minor Changes 6 | 7 | - d32b8a3: Upgrade esbulid to ^0.21.4 8 | - 2b3f3ee: Update browserslist, `default` query will be affected 9 | 10 | ### Patch Changes 11 | 12 | - c5855c7: Update package info 13 | - 6f44020: Fix fsconfck loading behavior 14 | 15 | ## 2.0.0 16 | 17 | ### Major Changes 18 | 19 | - 7bf90bf: Drop Node'js < v18 and TypeScript < v5 as required by tsconfck v3. (#224) 20 | - dbe79a0: Set default target Node.js version to v18.0.0 21 | - 03ce072: Deprecate the `--platform` flag to specify default target platform 22 | 23 | ### Minor Changes 24 | 25 | - 24d37be: Better support for Deno target transform 26 | - fb11c28: Update esbuild to v0.19 27 | - 24d37be: Reduce bloats on output for deno 28 | 29 | ### Patch Changes 30 | 31 | - d2ed35b: Fix target option specified properly 32 | - f38bb52: Update browserslist 33 | 34 | ## 1.6.0 35 | 36 | ### Minor Changes 37 | 38 | - a3b954c: update esbuild 39 | - ff6a370: update esbuild 40 | - 4946b5c: Support TypeScript v5 41 | 42 | ### Patch Changes 43 | 44 | - 3d07b82: Fix standalone mode (embedding externals) 45 | 46 | ## 1.5.1 47 | 48 | ### Patch Changes 49 | 50 | - 680ff26: Add support for `modulde`-packages with `.cts` entrypoints and `commonjs` packages with `.mts` entrypoints 51 | 52 | ## 1.5.0 53 | 54 | ### Minor Changes 55 | 56 | - a546f4d: Support defined value `process.env.NANABUNDLE_PACKAGE_NAME` and `process.env.NANOBUNDLE_PACKAGE_VERSION` 57 | 58 | ## 1.4.0 59 | 60 | ### Minor Changes 61 | 62 | - b71eeef: Add --no-legal-comments to disable emitting legal text 63 | - 0ab2a4a: Support CSS bundle explicitly 64 | - 745a02c: Don't emit empty legal comments 65 | 66 | ## 1.3.6 67 | 68 | ### Patch Changes 69 | 70 | - 4559578: regression: emit sourcemap by default 71 | 72 | ## 1.3.5 73 | 74 | ### Patch Changes 75 | 76 | - bcafa5f: Fix to output files with .mjs/.cjs extension properly 77 | 78 | ## 1.3.4 79 | 80 | ### Patch Changes 81 | 82 | - 1c6dc99: regression: allow directory style importMaps 83 | 84 | ## 1.3.3 85 | 86 | ### Patch Changes 87 | 88 | - 0b85f38: Fix subpath pattern imports resolution 89 | 90 | ## 1.3.2 91 | 92 | ### Patch Changes 93 | 94 | - 95ee87d: Fix legal comments output path 95 | 96 | ## 1.3.1 97 | 98 | ### Patch Changes 99 | 100 | - 8f22759: Fix subpath pattern validation for importMaps 101 | 102 | ## 1.3.0 103 | 104 | ### Minor Changes 105 | 106 | - 28d0d02: upgrade esbuild to v0.17.x 107 | - 74182e0: Set --keep-names build flag by default 108 | - 684408c: Support custom condition for imports & exports entries 109 | - 7d8f1bf: support subpath pattern import maps 110 | - 7fb6b21: Set legal comments to be liked by default 111 | 112 | ## 1.2.2 113 | 114 | ### Patch Changes 115 | 116 | - e0f874d: Unhandle .cjsx and .mjsx which are unsupported extension by TypeScript 117 | 118 | See [TS6054](https://github.com/search?q=repo%3Amicrosoft%2FTypeScript%20ts6054&type=code) 119 | See also https://github.com/microsoft/TypeScript/issues/44442 120 | 121 | ## 1.2.1 122 | 123 | ### Patch Changes 124 | 125 | - 7f4d5c1: Fix output filenames for CSS bundles 126 | 127 | ## 1.2.0 128 | 129 | ### Minor Changes 130 | 131 | - ae45ed3: Add --no-bundle flag to disable bundle build 132 | - 27b11c3: Allow source file with specific conditiona key 133 | 134 | ## 1.1.0 135 | 136 | ### Minor Changes 137 | 138 | - 41414c0: Add --clean flag to build command 139 | - 8561c0b: Prioritize jsx extension over module format if enabled 140 | - 36c9d4b: upgrade esbuild 141 | - 41414c0: Add `nanobundle clean` command 142 | - a77652d: Allow JSX extension entries (resolves #76) 143 | 144 | ### Patch Changes 145 | 146 | - 6aca469: Fix to respect tsconfig's sourceMap option 147 | - 6aca469: Fix the `--no-sourcemap` option to work properly 148 | - 31ae8d3: Fix crash on project using Node16 and NodeNext in tsconfig.json 149 | 150 | For this, nanobundle temporarily uses forked version of tsconfck package. 151 | 152 | - 41414c0: fix directory cleanup don't remove cwd 153 | - 39ead88: chore: error message typo 154 | - 2dcbba7: update tsconfck 155 | 156 | ## 1.0.0 157 | 158 | ### Major Changes 159 | 160 | - c6fad2b: v1 features 161 | 162 | - support multiple entries 163 | - support nested conditional exports 164 | - source inference from rootDir and outDir 165 | - enable tree-shaking by default 166 | - pretty reporter 167 | 168 | ### Patch Changes 169 | 170 | - 8ad0d33: more debug logs 171 | - 7426fd6: fix build flags and help text 172 | - a7c3312: Fix internal error handling 173 | - d14d8d5: add padding to warn/error messages 174 | - a187ab5: fix implicit types entry not to be conflicted with others 175 | - 7af4bf4: polished error messages and validation 176 | - ad38773: Fix flag test on jsx options 177 | - a57385d: fix result to be reported properly 178 | - c2d75a8: fix boolean flag handling 179 | - c3284bc: prettify result report 180 | - 1f5c2d3: fix sourcemap output path 181 | - c910f0c: fix indentation on typescript diagnostics 182 | - 7762b4a: Support Node.js resolution for import maps 183 | - 6976909: fix dts generation 184 | - 67b74ef: Fix TypeScript build task 185 | - ea2782f: Support JSX sources 186 | - a414231: change pathLike string colors" 187 | - 781557a: fix things 188 | - 47f8eef: more compact logs 189 | - fde48ba: polished reporting and diagnostics 190 | - e6d2499: fix dts build option 191 | - 84c8214: Fix to make it failed on type errors 192 | - 2075f16: compact reporting while on verbose mode 193 | - 31124fe: fix to --no-dts properly skip buildTypeTask 194 | - d5ed671: Fix jsx transform to work with additional options 195 | - d4e84bc: update depdencies 196 | - 4918a88: Fix TypeScript declartion build and prettify reporting 197 | - 12d5305: normalize ts rootDir and outDir 198 | 199 | ## 1.0.0-rc.15 200 | 201 | ### Patch Changes 202 | 203 | - 47f8eef: more compact logs 204 | 205 | ## 1.0.0-rc.14 206 | 207 | ### Patch Changes 208 | 209 | - c910f0c: fix indentation on typescript diagnostics 210 | - 6976909: fix dts generation 211 | - fde48ba: polished reporting and diagnostics 212 | - 84c8214: Fix to make it failed on type errors 213 | - 2075f16: compact reporting while on verbose mode 214 | - 12d5305: normalize ts rootDir and outDir 215 | 216 | ## 1.0.0-rc.13 217 | 218 | ### Patch Changes 219 | 220 | - ad38773: Fix flag test on jsx options 221 | - d5ed671: Fix jsx transform to work with additional options 222 | 223 | ## 1.0.0-rc.12 224 | 225 | ### Patch Changes 226 | 227 | - ea2782f: Support JSX sources 228 | 229 | ## 1.0.0-rc.11 230 | 231 | ### Patch Changes 232 | 233 | - 7762b4a: Support Node.js resolution for import maps 234 | 235 | ## 1.0.0-rc.10 236 | 237 | ### Patch Changes 238 | 239 | - 7af4bf4: polished error messages and validation 240 | - c2d75a8: fix boolean flag handling 241 | - 31124fe: fix to --no-dts properly skip buildTypeTask 242 | 243 | ## 1.0.0-rc.9 244 | 245 | ### Patch Changes 246 | 247 | - a7c3312: Fix internal error handling 248 | 249 | ## 1.0.0-rc.8 250 | 251 | ### Patch Changes 252 | 253 | - a414231: change pathLike string colors" 254 | - d4e84bc: update depdencies 255 | - 4918a88: Fix TypeScript declartion build and prettify reporting 256 | 257 | ## 1.0.0-rc.7 258 | 259 | ### Patch Changes 260 | 261 | - 67b74ef: Fix TypeScript build task 262 | 263 | ## 1.0.0-rc.6 264 | 265 | ### Patch Changes 266 | 267 | - a57385d: fix result to be reported properly 268 | 269 | ## 1.0.0-rc.5 270 | 271 | ### Patch Changes 272 | 273 | - c3284bc: prettify result report 274 | 275 | ## 1.0.0-rc.4 276 | 277 | ### Patch Changes 278 | 279 | - 7426fd6: fix build flags and help text 280 | - 1f5c2d3: fix sourcemap output path 281 | - e6d2499: fix dts build option 282 | 283 | ## 1.0.0-rc.3 284 | 285 | ### Patch Changes 286 | 287 | - 781557a: fix things 288 | 289 | ## 1.0.0-rc.2 290 | 291 | ### Patch Changes 292 | 293 | - d14d8d5: add padding to warn/error messages 294 | - a187ab5: fix implicit types entry not to be conflicted with others 295 | 296 | ## 1.0.0-rc.1 297 | 298 | ### Patch Changes 299 | 300 | - 8ad0d33: more debug logs 301 | 302 | ## 1.0.0-rc.0 303 | 304 | ### Major Changes 305 | 306 | - c6fad2b: v1 features 307 | 308 | - support multiple entries 309 | - support nested conditional exports 310 | - source inference from rootDir and outDir 311 | - enable tree-shaking by default 312 | - pretty reporter 313 | 314 | ## 0.0.28 315 | 316 | ### Patch Changes 317 | 318 | - 6e07e74: Fix types generation 319 | 320 | ## 0.0.27 321 | 322 | ### Patch Changes 323 | 324 | - 4eed241: choose prefered target based on module type specified by "exports" entry 325 | - 127cf17: ignore android 4.4 target 326 | 327 | ## 0.0.26 328 | 329 | ### Patch Changes 330 | 331 | - ad3caf9: update browserslist 332 | - 761a11d: update esbuild 333 | - edf2458: update tsconfck 334 | - 4b284a0: fix build target resolutions 335 | 336 | - Support `ios_safari`, `android`, `and_chr` and `and_ff` query 337 | - Drop `deno` query 338 | 339 | ## 0.0.25 340 | 341 | ### Patch Changes 342 | 343 | - 7b4a7ba: Update dependencies 344 | 345 | ## 0.0.24 346 | 347 | ### Patch Changes 348 | 349 | - 31b11f7: Fixes packages with names similar to Node.js APIs are not properly embedded. 350 | 351 | ## 0.0.23 352 | 353 | ### Patch Changes 354 | 355 | - 3563009: Fixes packages with names similar to Node.js APIs are not properly embedded. 356 | 357 | ## 0.0.22 358 | 359 | ### Patch Changes 360 | 361 | - a19162b: update dependencies 362 | - 204cc76: Ignore noEmit option when `types` entry required 363 | 364 | ## 0.0.21 365 | 366 | ### Patch Changes 367 | 368 | - ccf3a06: upgrade dependencies 369 | 370 | ## 0.0.20 371 | 372 | ### Patch Changes 373 | 374 | - 522a039: Fix "Unexpected moduleResolution" on emitting dts 375 | 376 | ## 0.0.19 377 | 378 | ### Patch Changes 379 | 380 | - 27327c5: Don't emit dts files when "types" is not exist in package.json 381 | - 78bd184: feat: emit TypeScript declaration files" 382 | 383 | ## 0.0.18 384 | 385 | ### Patch Changes 386 | 387 | - 25e3996: fix standalone mode 388 | 389 | ## 0.0.17 390 | 391 | ### Patch Changes 392 | 393 | - fac2b8c: fix internal reference in embedding process 394 | -------------------------------------------------------------------------------- /packages/nanobundle/README.md: -------------------------------------------------------------------------------- 1 | # nanobundle 2 | [![Version on NPM](https://img.shields.io/npm/v/nanobundle)](https://www.npmjs.com/package/nanobundle) 3 | [![Downlaods on NPM](https://img.shields.io/npm/dm/nanobundle)](https://www.npmjs.com/package/nanobundle) 4 | [![Integration](https://github.com/cometkim/nanobundle/actions/workflows/integration.yml/badge.svg)](https://github.com/cometkim/nanobundle/actions/workflows/integration.yml) 5 | [![codecov](https://codecov.io/gh/cometkim/nanobundle/branch/main/graph/badge.svg?token=6Oj3oxqiyQ)](https://codecov.io/gh/cometkim/nanobundle) 6 | [![LICENSE - MIT](https://img.shields.io/github/license/cometkim/espub)](#license) 7 | [![All Contributors](https://img.shields.io/badge/all_contributors-4-orange.svg?style=flat-square)](#contributors-) 8 | 9 | 10 | Perfect build tool for libraries, powered by [esbuild] 11 | 12 | ## Features 13 | 14 | - Automatic entry points 15 | - Support for **ESM** and **CommonJS** 16 | - Support **TypeScript `NodeNext`** moduleResolution 17 | - Support **multple** & **complex** entries by Node.js's **[Conditional Exports](https://nodejs.org/api/packages.html#conditional-exports)** 18 | - Support **[Import Maps](https://wicg.github.io/import-maps/)** with Node.js's **[Subpath Imports](https://nodejs.org/api/packages.html#subpath-imports)** rule 19 | - Optimize esbuild options to **maximize concurrency** 20 | - Only configuration you need is **`package.json`** (and optionally **`tsconfig.json`**) 21 | 22 | See [feature comparison](#feature-comparison) for more detail. 23 | 24 | ## Usage 25 | 26 | **You don't need any config files or passing the entry paths. But only you need to have proper [`package.json`](https://nodejs.org/api/packages.html) (and `tsconfig.json`)** 27 | 28 | ```jsonc 29 | { 30 | "main": "./lib/index.js", 31 | "scripts": { 32 | "build": "nanobundle build" 33 | } 34 | } 35 | ``` 36 | 37 | That's it, then just run `yarn build` or `npm run build`. What a magic ✨ 38 | 39 | nanobundle is smart enough to automatically determine the location of the appropriate source files from the entries specified in your `package.json`. 40 | 41 | It searches based on the `--root-dir` and `--out-dir` on the CLI flags (defaults to `src` and `lib`) but respects `tsconfig.json` if present. 42 | 43 | ### `package.json` Recipes 44 | 45 | More interestingly, it supports all of Node.js' notoriously complex **[Conditional Exports](https://nodejs.org/api/packages.html#conditional-exports)** rules. 46 | 47 |
48 | The ESM-only approach 49 | 50 | ```jsonc 51 | { 52 | "type": "module", 53 | "main": "./lib/index.js", // => src/index.ts 54 | "module": "./lib/index.js", // => src/index.ts 55 | "exports": "./lib/index.js" // => src/index.ts 56 | } 57 | ``` 58 | 59 |
60 | 61 | 62 |
63 | Dual-package exports 64 | 65 | ```jsonc 66 | { 67 | "exports": { 68 | ".": { 69 | "types": "./lib/index.d.ts", // => src/index.ts 70 | "require": "./lib/index.js", // => src/index.ts 71 | "import": "./lib/index.mjs" // => src/index.mts or src/index.ts 72 | }, 73 | "./package.json": "./package.json" // => package.json 74 | } 75 | } 76 | ``` 77 | 78 |
79 | 80 | 81 |
82 | Mutliple platform support 83 | 84 | ```jsonc 85 | { 86 | "exports": { 87 | ".": { 88 | "node": { 89 | "require": "./lib/index.node.cjs", // => src/index.cts or src/index.ts 90 | "import": "./lib/index.node.mjs" // => src/index.mts or src/index.ts 91 | }, 92 | "deno": "./lib/index.deno.mjs", // => src/index.mts or src/index.ts 93 | "browser": "./lib/index.browser.mjs", // => src/index.mts or src/index.ts 94 | "default": "./lib/index.js" // => src/index.ts 95 | }, 96 | "./package.json": "./package.json" // => package.json 97 | } 98 | } 99 | ``` 100 | 101 |
102 | 103 | 104 |
105 | Server/Client submodules 106 | 107 | ```jsonc 108 | { 109 | "exports": { 110 | ".": "./lib/common.js", // => src/common.ts 111 | "./server": { 112 | "types": "./lib/server.d.ts", // => src/server.ts 113 | "require": "./lib/server.cjs", // => src/server.cts or src/server.ts 114 | "import": "./lib/server.mjs" // => src/server.mts or src/server.ts 115 | }, 116 | "./client": { 117 | "types": "./lib/client.d.ts", // => src/client.ts 118 | "require": "./lib/client.min.cjs", // => src/client.cts or src/client.ts, output will be minified:sparkles: 119 | "import": "./lib/client.min.mjs" // => src/client.mts or src/client.ts, output will be minified 120 | }, 121 | "./package.json": "./package.json" 122 | } 123 | } 124 | ``` 125 | 126 |
127 | 128 | 129 |
130 | Development-only code for debugging 131 | 132 | ```jsonc 133 | { 134 | "exports": { 135 | "development": "./dev.js", // => src/dev.ts 136 | "production": "./index.min.js" // => src/index.ts, output will be minified 137 | } 138 | } 139 | ``` 140 | 141 |
142 | 143 | 144 | ### CLI Options 145 | 146 |
147 | Full CLI options 148 | 149 | ``` 150 | Usage 151 | $ nanobundle [options] 152 | 153 | Available Commands 154 | build Build once and exit 155 | clean Remove outputs 156 | 157 | Options 158 | --version Display current version 159 | 160 | --cwd Use an alternative working directory 161 | 162 | --clean Clean outputs before build 163 | 164 | --tsconfig Specify the path to a custom tsconfig.json 165 | 166 | --import-maps Specify import map file path (default: package.json) 167 | 168 | --root-dir Specify the path to resolve source entry (default: ./src) 169 | This also can be configured by tsconfig.json 170 | 171 | --out-dir Specify the path to resolve source entry (default: ./lib) 172 | This also can be configured by tsconfig.json 173 | 174 | --platform Specify bundle target platform (default: "netural") 175 | One of "netural", "browser", "node" is allowed 176 | 177 | --standalone Embed external dependencies into the bundle (default: false) 178 | 179 | --external Specify external dependencies to exclude from the bundle 180 | 181 | --jsx Specify JSX mode. One of "transform", "preserve", "automatic" is allowed 182 | This also can be configufeature comparisonred by tsconfig.json 183 | 184 | --jsx-factory Specify JSX factory (default: "React.createElement") 185 | This also can be configured by tsconfig.json 186 | 187 | --jsx-fragment Specify JSX factory (default: "Fragment") 188 | This also can be configured by tsconfig.json 189 | 190 | --jsx-import-source Specify JSX import source (default: "react") 191 | This also can be configured by tsconfig.json 192 | 193 | --no-sourcemap Disable source map generation 194 | 195 | --no-legal-comments Disable legal comments generation 196 | 197 | --no-bundle Disable ESBuild bundle and other files build 198 | 199 | --no-dts Disable TypeScript .d.ts build 200 | 201 | --verbose Set to report build result more verbosely 202 | 203 | --help Display this message 204 | ``` 205 |
206 | 207 | ## Features 208 | 209 | Nanobundle believes the `package.json` today is expressive enough for most module use cases. 210 | 211 | So attempting to turn users' attention back to the [Node's package spec](https://nodejs.org/api/packages.html) like [ES Modules](https://nodejs.org/api/esm.html) and [Import Maps](https://wicg.github.io/import-maps/) which are already supported by Node.js, rather than adding another customizing options. 212 | 213 | ### Automatic entry points 214 | 215 | You don't need to pass or set entry points in any configuration file, only you have to do is provide correct `exports` in your `package.json`. 216 | 217 | nanobundle will automatically search for entry files in the `rootDir` and `outDir` you have. (defaults are `src` and `lib`, or respectively configurable by `tsconfig.json` or CLI arguments) 218 | 219 | ```jsonc 220 | { 221 | "main": "./lib/index.js", // => search src/index.cts, src/index.ts, etc 222 | "module": "./lib/index.mjs", // => search src/index.mts, src/index.ts, etc 223 | "exports": { 224 | "./feature": "./lib/feature.js" // => search src/feature.cts, src/feature.ts, etc 225 | } 226 | } 227 | ``` 228 | 229 | ### Build targets 230 | 231 | **nanobundle expects you to write a Web-compatible(netural) package.** 232 | 233 | If you use any Node.js APIs, you need to tell it explicitly via:. 234 | - Pass `--platform=node` flag 235 | - Set the entry point with `node` condition. 236 | 237 | Without `engines` field in `package.json`, the default Node.js version will be v14. 238 | 239 | ### Conditional Exports 240 | 241 | You can specify multiple/conditional entry points in your `package.json`. 242 | 243 | See [Node.js docs](https://nodejs.org/api/packages.html#packages_package_entry_points) for more detail. 244 | 245 | ```jsonc 246 | { 247 | "type": "module", 248 | "main": "./main.js", // Legacy entry 249 | "exports": { 250 | ".": "./main.js", 251 | "./feature": { 252 | "node": "./feature-node.js", // conditional entry 253 | "default": "./feature.js" 254 | } 255 | } 256 | } 257 | ``` 258 | 259 | You can use conditional exports for dealing with **[Dual Package Hazard](https://nodejs.org/api/packages.html#dual-package-hazard)**. 260 | 261 | E.g. for supporting both CommonJS and ESM package. 262 | 263 | ```jsonc 264 | { 265 | "exports": { 266 | "require": "./lib/index.cjs", 267 | "import": "./lib/index.mjs" 268 | } 269 | } 270 | ``` 271 | 272 | ### Import Maps 273 | 274 | nanobundle supports [Import Maps](https://wicg.github.io/import-maps/) 275 | 276 | You can specify import alias by your `package.json`, or by a separated json file with the `--import-map` option. 277 | 278 | ```jsonc 279 | { 280 | "imports": { 281 | "~/": "./", 282 | "@util/": "./src/utils/", 283 | } 284 | } 285 | ``` 286 | 287 | nanobundle also handles Node.js's [Subpath Imports](https://nodejs.org/api/packages.html#subpath-imports) rules. 288 | 289 | ```jsonc 290 | { 291 | "imports": { 292 | "#dep": { 293 | "node": "dep-node-native", 294 | "default": "./dep-polyfill.js" 295 | } 296 | } 297 | } 298 | ``` 299 | 300 | ### Embedding dependencies 301 | 302 | nanobundle by default does nothing about external like `dependencies` and `peerDependencies`. 303 | 304 | However, if the `--standalone` flag is set, it will try to embed all external dependencies into the bundle. 305 | 306 | Dependencies specified with `--external` and Node.js internal APIs are always excluded. 307 | 308 | ### TypeScript 309 | 310 | Given a `tsconfig.json` file in the cwd or `--tsconfig` option, nanobundle looks for options for TypeScript and JSX. 311 | 312 | nanobundle automatically generate TypeScript declaration as you specify `types` entries in the `package.json`, or you can disable it passing `--no-dts` flag. 313 | 314 | ### Minification 315 | 316 | Any entires with `.min.(c|m)?js` will generate minified output. 317 | 318 | ```jsonc 319 | { 320 | "exports": "./index.min.js" // will be minifies output 321 | } 322 | ``` 323 | 324 | ### Using `process.env.NODE_ENV` with condition 325 | 326 | Conditional entries with Node.js community condition `production` or `development` will be built with injected `process.env.NODE_ENV` as its value. 327 | 328 | ```jsonc 329 | { 330 | "exports": { 331 | ".": { 332 | "development": "./dev.js", // process.env.NODE_ENV === 'development' 333 | "production": "./prod.min.js" // process.env.NODE_ENV === 'production' 334 | } 335 | } 336 | } 337 | ``` 338 | 339 | ## Feature Comparison 340 | 341 | | Build tool | 0 Config | Respect `package.json` | TypeScript `.d.ts` generation | Concurrency | Multiple Entries | Conditional Exports | Import Maps | CSS Support | Plugins | Dev(watch) mode | 342 | | :------------------- | -------------------------------: | ---------------------: | ----------------------------: | ----------: | -----------------------: | -------------------: | ---------------------: | ---------------------: | ---------------: | ---------------: | 343 | | **nanobundle** | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ✖️
(planned) | ✖️
(planned) | 344 | | [microbundle] | ✔️ | ✔️ | ✔️ | ✔️ | ✖️ | 🟡
(only flat) | ✖️ | ✔️ | ✖️ | ✔️ | 345 | | [tsup] | 🟡
(mostly by custom file) | ✖️ | ✔️ | ✔️ | ✔️ | ✖️ | 🟡
(with plugin) | 🟡
(experimental) | ✔️ | ✔️ | 346 | | [estrella] | ✖️ | ✖️ | ✔️ | ✔️ | ✖️ | ✖️ | ✖️ | ✖️ | ✖️ | ✔️ | 347 | | [esbuild] | ✖️ | ✖️ | ✖️ | ✔️ | ✔️ | ✖️ | ✖️ | ✔️ | ✔️ | ✔️ | 348 | | [Rollup] | ✖️ | ✖️ | 🟡
(with plugin) | ✔️ | ✔️ | 🟡
(by code) | 🟡
(with plugin) | ✔️ | ✔️ | ✔️ | 349 | | [Vite (lib mode)] | ✖️ | ✖️ | 🟡
(with plugin) | ✔️ | ✔️ | 🟡
(by code) | 🟡
(with plugin) | ✔️ | ✔️ | ✔️ | 350 | | [Parcel (lib mode)] | ✔️ | ✔️ | ✔️ | ✔️ | ✖️ | ✖️ | ✖️ | ✔️ | ✖️ | ✔️ | 351 | 352 | 353 | ## Contributors ✨ 354 | 355 | Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)): 356 | 357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 | 365 | 366 | 367 | 368 | 369 |
Hyeseong Kim
Hyeseong Kim

💻 🚧
Anton Petrov
Anton Petrov

💻
jinho park
jinho park

⚠️ 🐛
Manuel Thalmann
Manuel Thalmann

💻
370 | 371 | 372 | 373 | 374 | 375 | This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome! 376 | 377 | ## License 378 | 379 | MIT 380 | 381 | [microbundle]: https://github.com/developit/microbundle 382 | [tsup]: https://tsup.egoist.dev/ 383 | [estrella]: https://github.com/rsms/estrella 384 | [esbuild]: https://esbuild.github.io/ 385 | [Rollup]: https://rollupjs.org/guide/ 386 | [Vite (lib mode)]: https://vitejs.dev/guide/build.html#library-mode 387 | [Parcel (lib mode)]: https://parceljs.org/getting-started/library/ 388 | -------------------------------------------------------------------------------- /packages/nanobundle/build.mjs: -------------------------------------------------------------------------------- 1 | import esbuild from 'esbuild'; 2 | import manifest from './package.json' assert { type: 'json' }; 3 | 4 | esbuild.build({ 5 | entryPoints: ['src/bin.ts'], 6 | outfile: 'bin.min.mjs', 7 | bundle: true, 8 | write: true, 9 | treeShaking: true, 10 | sourcemap: false, 11 | minify: true, 12 | format: 'esm', 13 | platform: 'node', 14 | target: ['node16'], 15 | external: [ 16 | ...Object.keys(manifest.dependencies), 17 | ...Object.keys(manifest.peerDependencies), 18 | ], 19 | }); -------------------------------------------------------------------------------- /packages/nanobundle/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nanobundle", 3 | "version": "2.1.0", 4 | "type": "module", 5 | "license": "MIT", 6 | "bin": "./bin.min.mjs", 7 | "repository": { 8 | "type": "git", 9 | "url": "git+https://github.com/cometkim/espub.git", 10 | "directory": "packages/nanobundle" 11 | }, 12 | "bugs": { 13 | "url": "https://github.com/cometkim/espub" 14 | }, 15 | "author": { 16 | "name": "Hyeseong Kim", 17 | "email": "hey@hyeseong.kim" 18 | }, 19 | "publishConfig": { 20 | "access": "public" 21 | }, 22 | "scripts": { 23 | "prepack": "yarn build", 24 | "build": "node build.mjs", 25 | "build:self": "node bin.min.mjs build --platform=node", 26 | "type-check": "tsc --noEmit --skipLibCheck", 27 | "test": "vitest" 28 | }, 29 | "files": [ 30 | "bin.mjs" 31 | ], 32 | "engines": { 33 | "node": ">=18.0.0", 34 | "deno": ">=1.9.0" 35 | }, 36 | "resolutions": { 37 | "vite": "^5.0.0" 38 | }, 39 | "peerDependencies": { 40 | "typescript": "^5.0.0" 41 | }, 42 | "peerDependenciesMeta": { 43 | "typescript": { 44 | "optional": true 45 | } 46 | }, 47 | "dependencies": { 48 | "@cometjs/core": "^2.1.0", 49 | "browserslist": "^4.22.2", 50 | "esbuild": "^0.21.4", 51 | "kleur": "^4.1.5", 52 | "meow": "^12.0.0", 53 | "pretty-bytes": "^6.0.0", 54 | "semver": "^7.3.8", 55 | "string-dedent": "^3.0.1", 56 | "tsconfck": "^3.0.0", 57 | "xstate": "^4.35.0" 58 | }, 59 | "devDependencies": { 60 | "@types/node": "^18.0.0", 61 | "@types/semver": "^7.3.13", 62 | "@vitest/coverage-v8": "^1.0.0", 63 | "@xstate/cli": "^0.5.0", 64 | "pkg-types": "^1.0.1", 65 | "typescript": "^5.0.0", 66 | "vitest": "^1.6.0" 67 | }, 68 | "packageManager": "yarn@4.3.0" 69 | } 70 | -------------------------------------------------------------------------------- /packages/nanobundle/src/__snapshots__/config.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1 2 | 3 | exports[`parseConfig > flags > rootDir=outDir 1`] = `"Directory rootDir(.) and outDir(.) are conflict! Please specify different directory for one of them."`; 4 | -------------------------------------------------------------------------------- /packages/nanobundle/src/__snapshots__/context.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`parseConfig > flags > rootDir=outDir is not allowed without TypeScript 1`] = ` 4 | [NanobundleConfigError: "rootDir" (.) and "outDir" (.) are conflict! 5 | 6 | Please specify different directory for one of them.] 7 | `; 8 | -------------------------------------------------------------------------------- /packages/nanobundle/src/__snapshots__/entry.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`getEntriesFromContext > bin entry only accepts js 1`] = ` 4 | [Error: Only JavaScript files are allowed for bin entry. 5 | ] 6 | `; 7 | 8 | exports[`getEntriesFromContext > throw if "main" and "module" is on conflict 1`] = ` 9 | [Error: Hint: Did you forgot to set "type" to 'module' for ESM-first approach? 10 | 11 | ] 12 | `; 13 | 14 | exports[`getEntriesFromContext - in TypeScript project > types entry does not accept nesting 1`] = ` 15 | [Error: "types" entry must be .d.ts file and cannot be nested! 16 | ] 17 | `; 18 | 19 | exports[`getEntriesFromContext - in TypeScript project > types entry must has .d.ts extension 1`] = ` 20 | [Error: Only .d.ts or .d.cts or .d.mts allowed for "types" entry. 21 | ] 22 | `; 23 | 24 | exports[`getEntriesFromContext - in TypeScript project > types entry must occur first in conditional exports 1`] = ` 25 | [Error: "types" entry must occur first in conditional exports for correct type resolution. 26 | ] 27 | `; 28 | -------------------------------------------------------------------------------- /packages/nanobundle/src/bin.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import { 4 | parse as parseTsConfig, 5 | TSConfckParseError, 6 | type TSConfckParseResult, 7 | } from 'tsconfck'; 8 | import dedent from 'string-dedent'; 9 | 10 | import { cli } from './cli'; 11 | import { ConsoleReporter } from './reporter'; 12 | import { loadTargets } from './target'; 13 | import { loadManifest } from './manifest'; 14 | import { NanobundleConfigError, parseConfig } from './context'; 15 | import { getEntriesFromContext } from './entry'; 16 | import * as formatUtils from './formatUtils'; 17 | import { NanobundleError } from './errors'; 18 | 19 | import { buildCommand } from './commands/build'; 20 | import { cleanCommand } from './commands/clean'; 21 | 22 | const { flags, input } = cli; 23 | const [command] = input; 24 | 25 | const reporter = new ConsoleReporter(console); 26 | reporter.level = process.env.DEBUG === 'true' ? 'debug' : 'default'; 27 | 28 | if (!command) { 29 | cli.showHelp(0); 30 | } 31 | 32 | const supportedCommands = ['build', 'clean']; 33 | 34 | try { 35 | if (supportedCommands.includes(command)) { 36 | const manifest = await loadManifest({ basePath: flags.cwd }); 37 | reporter.debug('loaded manifest %o', manifest); 38 | 39 | let tsconfigResult: TSConfckParseResult | undefined; 40 | try { 41 | tsconfigResult = await parseTsConfig(flags.tsconfig); 42 | } catch (err) { 43 | if (err instanceof TSConfckParseError) { 44 | throw err; 45 | } 46 | } 47 | const tsconfigPath = tsconfigResult?.tsconfigFile; 48 | if (tsconfigPath) { 49 | reporter.debug(`loaded tsconfig from ${tsconfigPath}`); 50 | } 51 | const tsconfig = tsconfigResult?.tsconfig; 52 | if (tsconfig) { 53 | reporter.debug('loaded tsconfig %o', tsconfig); 54 | } 55 | 56 | const targets = loadTargets({ 57 | manifest, 58 | basePath: flags.cwd, 59 | }); 60 | reporter.debug(`loaded targets ${targets.join(', ')}`); 61 | 62 | const context = parseConfig({ 63 | flags, 64 | targets, 65 | manifest, 66 | tsconfig, 67 | tsconfigPath, 68 | reporter, 69 | }); 70 | reporter.debug(`loaded context %o`, context); 71 | 72 | const entries = getEntriesFromContext({ 73 | context, 74 | reporter, 75 | }); 76 | 77 | if ( 78 | entries.some(entry => entry.module === 'dts') && 79 | tsconfigPath == null 80 | ) { 81 | throw new NanobundleConfigError(dedent` 82 | You have set ${formatUtils.key('types')} entry. But no ${formatUtils.path('tsconfig.json')} found. 83 | 84 | Please create ${formatUtils.path('tsconfig.json')} file in the current directory, or pass its path to ${formatUtils.command('--tsconfig')} argument. 85 | 86 | `); 87 | } 88 | 89 | reporter.debug(`parsed entries %o`, entries); 90 | 91 | if (command === 'build') { 92 | await buildCommand({ 93 | context, 94 | entries, 95 | cleanFirst: flags.clean, 96 | }); 97 | } 98 | 99 | if (command === 'clean') { 100 | await cleanCommand({ 101 | context, 102 | entries, 103 | }); 104 | } 105 | 106 | } else { 107 | throw new NanobundleError(dedent` 108 | Command "${command}" is not available. 109 | 110 | Run ${formatUtils.command('nanobundle --help')} for usage. 111 | `); 112 | } 113 | } catch (error) { 114 | if (error instanceof NanobundleError) { 115 | reporter.error(error.message); 116 | } else if (error instanceof TSConfckParseError) { 117 | reporter.error(error.message); 118 | } else { 119 | reporter.captureException(error); 120 | } 121 | process.exit(1); 122 | } 123 | -------------------------------------------------------------------------------- /packages/nanobundle/src/cli.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import meow from 'meow'; 4 | 5 | export const cli = meow(` 6 | Usage 7 | $ nanobundle [options] 8 | 9 | Available Commands 10 | build Build once and exit 11 | clean Remove outputs 12 | 13 | Options 14 | --version Display current version 15 | 16 | --cwd Use an alternative working directory 17 | 18 | --clean Clean outputs before build 19 | 20 | --tsconfig Specify the path to a custom tsconfig.json 21 | 22 | --import-maps Specify import map file path (default: package.json) 23 | 24 | --root-dir Specify the path to resolve source entry (default: ./src) 25 | This also can be configured by tsconfig.json 26 | 27 | --out-dir Specify the path to resolve source entry (default: ./lib) 28 | This also can be configured by tsconfig.json 29 | 30 | --platform DEPRECATED. Specify bundle target platform (default: "netural") 31 | One of "netural", "browser", "node" is allowed 32 | 33 | --standalone Embed external dependencies into the bundle (default: false) 34 | 35 | --external Specify external dependencies to exclude from the bundle 36 | 37 | --jsx Specify JSX mode. One of "transform", "preserve", "automatic" is allowed 38 | This also can be configured by tsconfig.json 39 | 40 | --jsx-factory Specify JSX factory (default: "React.createElement") 41 | This also can be configured by tsconfig.json 42 | 43 | --jsx-fragment Specify JSX factory (default: "Fragment") 44 | This also can be configured by tsconfig.json 45 | 46 | --jsx-import-source Specify JSX import source (default: "react") 47 | This also can be configured by tsconfig.json 48 | 49 | --no-sourcemap Disable source map generation 50 | 51 | --no-legal-comments Disable legal comments generation 52 | 53 | --no-bundle Disable ESBuild bundle and other files build 54 | 55 | --no-dts Disable TypeScript .d.ts build 56 | 57 | --verbose Set to report build result more verbosely 58 | 59 | --help Display this message 60 | `, { 61 | importMeta: import.meta, 62 | flags: { 63 | cwd: { 64 | type: 'string', 65 | default: process.cwd(), 66 | }, 67 | clean: { 68 | type: 'boolean', 69 | default: false, 70 | }, 71 | rootDir: { 72 | type: 'string', 73 | }, 74 | outDir: { 75 | type: 'string', 76 | }, 77 | tsconfig: { 78 | type: 'string', 79 | default: 'tsconfig.json', 80 | }, 81 | importMaps: { 82 | type: 'string', 83 | default: 'package.json', 84 | }, 85 | external: { 86 | type: 'string', 87 | isMultiple: true, 88 | default: [], 89 | }, 90 | platform: { 91 | type: 'string', 92 | }, 93 | standalone: { 94 | type: 'boolean', 95 | default: false, 96 | }, 97 | sourcemap: { 98 | type: 'boolean', 99 | default: true, 100 | }, 101 | legalComments: { 102 | type: 'boolean', 103 | default: true, 104 | }, 105 | bundle: { 106 | type: 'boolean', 107 | default: true, 108 | }, 109 | dts: { 110 | type: 'boolean', 111 | default: true, 112 | }, 113 | jsx: { 114 | type: 'string', 115 | }, 116 | jsxFactory: { 117 | type: 'string', 118 | }, 119 | jsxFragment: { 120 | type: 'string', 121 | }, 122 | jsxImportSource: { 123 | type: 'string', 124 | }, 125 | verbose: { 126 | type: 'boolean', 127 | default: false, 128 | }, 129 | }, 130 | }); 131 | 132 | export type Flags = typeof cli.flags; 133 | -------------------------------------------------------------------------------- /packages/nanobundle/src/commands/build/build.machine.ts: -------------------------------------------------------------------------------- 1 | import { performance } from 'node:perf_hooks'; 2 | import dedent from 'string-dedent'; 3 | import { assign, createMachine } from 'xstate'; 4 | 5 | import { type Context } from '../../context'; 6 | import { type Entry } from '../../entry'; 7 | import { 8 | filterBundleEntry, 9 | filterFileEntry, 10 | filterTypeEntry, 11 | } from '../../entryGroup'; 12 | import { type OutputFile } from '../../outputFile'; 13 | 14 | import { buildBundleTask, type BuildBundleTaskError } from '../../tasks/buildBundleTask'; 15 | import { buildFileTask, type BuildFileTaskError } from '../../tasks/buildFileTask'; 16 | import { buildTypeTask, type BuildTypeTaskError } from '../../tasks/buildTypeTask'; 17 | import { chmodBinTask } from '../../tasks/chmodBinTask'; 18 | import { cleanupTask, type CleanupTaskError } from '../../tasks/cleanupTask'; 19 | import { emitTask, type EmitTaskError } from '../../tasks/emitTask'; 20 | import { reportEmitResultsTask } from '../../tasks/reportEmitResultsTask'; 21 | 22 | export const buildMachine = 23 | /** @xstate-layout N4IgpgJg5mDOIC5QCMCuBLANhAsgQwGMALdAOzADpkB7agF1joCc8AHAYgCEBVASQBkAIgG0ADAF1EoVtVjo66aqSkgAHogCMGgEwUAnKO0BmHaIAcAdj0BWa9o0AaEAE9EAFlF6K2s2Y163ADZtN0tQ6wBfCKc0LFxCEnIqWgZmNnYAYX4AUQBBADkxSSQQGTkFJRV1BABaMyMKCyMQi2Mjcw89MydXBFtdIyNAs20LDSttAyMomIxsfGIySljsbNJmdDh2CCVKRjw6Zbn4xaSViDWNuCKVMvlFZRLqszcvIwNJjVE3C0CLa26Lk02m+FDc2lsYxGzUGbhmIHOC0SRzilyYm1g212FH2hyoxyRS3xqPW6OuGmK0lk90qT0QzQ0FGa4KM1kCon8xkBvVMbjBEOsQXBelZrPhiISRPOaIxxOwnFQpAgmDAMrgcogWKSZAAbtQANYo+aSs7HNWwDUKpUq80ahC66gEA4PIo3Ep3CqPUDVQLsijmax6L7-GwWDk9TRGPk+Iz1Nw-Nm-IwWcUEk1Gi6k2XnK3K1VZ9XndhgJhMahMCisTAHABm5YAthrCaaSVcLTnFXnbed7aQ9U7Pa6JLdqZ6qohfaJ-WZA8HA9Yw44gQhjIyXsmIdotxYgsNU3Fmxnu8cAGJYfNtjVaygOw1N9Ma49xM82gvt469-vOpRDymlUcPOOCCBLGFAgW4XwQcm1iiCYEYIKEuijCC9Rhu8OgptECJpqcR5vhqL4XmS75xMWpblpW1Z0HWTCNhKuGPvh5yEU+2Cfo636kL+I7lIBdLAaB4GQRo0GwUuvQLl49T+DubgwV00xYfRyKMZe5wACrOKwRHZsc14ULeGaHqpxEapp2msRA7EDi6EhulSvG0t6E6gbJVgiSKQxbvB0n6P4QaCno7IWCF+7Ggx0pMcc5k6YWeklmWFZVrWDb3hFZpRXEMWWdZnHce6AFOWoLlmI08bue8gzBNo8F6JMYJWGYojBG4JihGFJwqWA9byOa+mGWlXU9XQ5q5YOdnDgVjlesVCBRqV1gaCMgStWM2i+v88EaEEugeJMTRLa0ViYbMB4Pt1vVvmRiWUSltGDUSF0jW+Y22eI9n-tNQGBoEjTDO0Zjsv4JjcpGC7+jGIQeKEAQdcZBAqngpCoBwOzan2BpGQ+CNgEjKOvT+E1-h6fHOQggPWBD0lRt8Iq2FtMGUytegBBoAKsqMgRw9jiPIxwCUUcl1GpcpRI43jrAE1xRM8TSM3PIElMvID22tF864M5YFAzs1UbDGMQZwkpOEqcQ9bUBAnBkH1aM3hjd6i0kZsW1bpCjQ6NmE+9k0OXLQHBFOogcjGASBOMIRba0v3jKKojWBzIFmNzDHO5b1tXQLSVUTRdEm2LRDm2nbsvR7eUy1Nfv8QH2v2GHGhh+trSR00-pLaEgRBdtQxJ8bZ0MUwYAyEwI3DQASnAqCYAw-X21j-eD+WI-yOPsCTwwUv5b7Y78TUowUCJcbhK1TWWNYW0gZTrIQf8vi+mz2jJypA9D0vdAr2vmK2wZs8PUkz+L9kMeE8p6wA3nZCkstt5k1eKVZotcTDs02sueurImSCnGACQGYdIi93CqbXmKMzxMEYDPPUDs85OwIawIhjAwHe2JoVeWiAahRkpgfWwfh2ZxzPsuRWug1YBF8FuduRtTp4LFlQmhdBrqC2ziLChlBxZ8ykXQj6JMirVBYfGMEfwuhB08OyPQFh4JhkDiFbu0NOh6CiFhUgFs4AqEdmASBpNZosJFPvWMHC2b1G4fBGoOhSps27iFNC7QNCPylCkRgLBWAuI0cwoM2swyvA5D8Lond4KtV0KIHcis7As3Wn4LmuDOpSgym2eJTC5omH0PGDkaSqohB4b0GoHgwI7kWt3eooxYalOMpFNSxxcyvkqRXKBs1YxTggnVQU605mhyyb9Jqu5YIij+OCB+-SHyDNMh2a0sUSLYCqUBcEpUZkQnBIrYUQQtqhAoJJaGLw6paHjpElsqxMryk7KM0yq8CAEDgPAcZrjqgGCnL8SYZhwW+mhgzCw-p-gwMDC84J7y8JDLiCMw5FAax4CwKgAeJz+Lgr+lCmFK1vjwUuWBUU0K9Y+DbuikyulnznnNMSsmyYvABA8F8LuXk5LwXeHyCYlh-JBX6IpMRZSPmZkxdgFiXyICctmuCBFvKGkCpai09wQUHl-GubktygNmW7NZYq9l+F-mAtgMCreoLEAzgaItDu9cQL-DEvBRm-oPU-BGCakpMqBkVL2aeK1l48UEqJSChJ5N44PPrkGX0ok4K8MMGBYwicFxySWls4NOzQ0WogNlN8qrqhTP0O5FNnlghGBMdo+paF3IjEMGaotcUspaRxecct7gfBVuTSBWtxgfJBG8GzcIgNO4+Hba2MNXaLLWtQACoFfb427T8L8WCp9DCg3jaK15Vgw4zp7gW9K87i2lsjfizAhLnGxuqTOTd4x2Sxk9T4WqdhvBjB+LBX0-183YT7kNS6YyHVxtYUycwddvj5LZLqhAIk2RMhMCmlaOgRInWA+IpItt10zi8EtHWnS2Qg0jhmvNd8RL+C0MypRKMCMrWg4mTwetoRbVZNHV46ErkBBCPRguLt07gc+pXLlrUwKA3sL4do27kwUd0H8E1dhkwQWZf-YegDl7AIYOuwYDRc2A2+JYd4nhEP3w1SzGjTVJiCmw04igDHqHoGIXQddATBhMjDsmF43wdxYfgn8SmxrRTBGdV0GxEQgA */ 24 | createMachine({ 25 | tsTypes: {} as import("./build.machine.typegen").Typegen0, 26 | schema: { 27 | events: {} as ( 28 | | { 29 | type: "BUILD"; 30 | } 31 | | { 32 | type: "CLEAN"; 33 | } 34 | ), 35 | context: {} as { 36 | root: Context; 37 | buildStartedAt: number; 38 | entries: Entry[]; 39 | bundleOutputs: OutputFile[]; 40 | fileOutputs: OutputFile[]; 41 | typeOutputs: OutputFile[]; 42 | errors: { 43 | buildBundle?: BuildBundleTaskError; 44 | buildFile?: BuildFileTaskError; 45 | buildType?: BuildTypeTaskError; 46 | emit?: EmitTaskError; 47 | cleanup?: CleanupTaskError; 48 | }; 49 | }, 50 | }, 51 | predictableActionArguments: true, 52 | id: "buildMachine", 53 | initial: "bootstrap", 54 | states: { 55 | bootstrap: { 56 | on: { 57 | BUILD: { 58 | target: "buildEntries", 59 | actions: "reportBuildStart", 60 | }, 61 | 62 | CLEAN: "cleanupFirst" 63 | }, 64 | }, 65 | 66 | buildEntries: { 67 | states: { 68 | buildBundleEntries: { 69 | initial: "build", 70 | states: { 71 | build: { 72 | invoke: { 73 | src: "buildBundleTask", 74 | onDone: [ 75 | { 76 | target: "success", 77 | actions: "assignBundleOutputs", 78 | }, 79 | ], 80 | onError: [ 81 | { 82 | target: "failure", 83 | actions: "assignBuildBundleError", 84 | }, 85 | ], 86 | }, 87 | }, 88 | success: { 89 | type: "final", 90 | }, 91 | failure: { 92 | type: "final", 93 | }, 94 | }, 95 | }, 96 | 97 | buildFileEntries: { 98 | initial: "build", 99 | states: { 100 | build: { 101 | invoke: { 102 | src: "buildFileTask", 103 | onDone: [ 104 | { 105 | target: "success", 106 | actions: "assignFileOutputs", 107 | }, 108 | ], 109 | onError: [ 110 | { 111 | target: "failure", 112 | actions: "assignBuildFileError", 113 | }, 114 | ], 115 | }, 116 | }, 117 | success: { 118 | type: "final", 119 | }, 120 | failure: { 121 | type: "final", 122 | }, 123 | }, 124 | }, 125 | 126 | buildTypeEntries: { 127 | initial: "build", 128 | states: { 129 | build: { 130 | invoke: { 131 | src: "buildTypeTask", 132 | onDone: [ 133 | { 134 | target: "success", 135 | actions: "assignTypeOutputs", 136 | }, 137 | ], 138 | onError: [ 139 | { 140 | target: "failure", 141 | actions: "assignBuildTypeError", 142 | }, 143 | ], 144 | }, 145 | }, 146 | success: { 147 | type: "final", 148 | }, 149 | failure: { 150 | type: "final", 151 | }, 152 | }, 153 | } 154 | }, 155 | type: "parallel", 156 | onDone: [ 157 | { 158 | target: "cleanup", 159 | cond: "hasBuildErrors", 160 | actions: "reportBuildErrors", 161 | }, 162 | { 163 | target: "emitEntries", 164 | }, 165 | ], 166 | }, 167 | 168 | emitEntries: { 169 | invoke: { 170 | src: "emitTask", 171 | onDone: [ 172 | { 173 | target: "reportEmitResults", 174 | }, 175 | ], 176 | onError: [ 177 | { 178 | target: "cleanup", 179 | actions: "assignEmitError", 180 | }, 181 | ], 182 | }, 183 | }, 184 | 185 | done: { 186 | entry: "reportBuildEnd", 187 | type: "final", 188 | }, 189 | 190 | cleanup: { 191 | invoke: { 192 | src: "cleanupTask", 193 | onDone: [ 194 | { 195 | target: "done", 196 | }, 197 | ], 198 | onError: [ 199 | { 200 | target: "done", 201 | }, 202 | ], 203 | }, 204 | }, 205 | 206 | chmodBinEntries: { 207 | invoke: { 208 | src: "chmodBinTask", 209 | onDone: [ 210 | { 211 | target: "done", 212 | }, 213 | ], 214 | onError: [ 215 | { 216 | target: "done", 217 | }, 218 | ], 219 | }, 220 | }, 221 | 222 | reportEmitResults: { 223 | invoke: { 224 | src: "reportEmitResults", 225 | onDone: [ 226 | { 227 | target: "chmodBinEntries", 228 | cond: "hasBinEntries", 229 | }, 230 | { 231 | target: "done", 232 | }, 233 | ], 234 | }, 235 | }, 236 | 237 | cleanupFirst: { 238 | invoke: { 239 | src: "cleanupTask", 240 | onDone: "bootstrap", 241 | onError: "bootstrap" 242 | }, 243 | 244 | entry: "reportCleanupStart", 245 | exit: "reportCleanupEnd" 246 | } 247 | }, 248 | }, { 249 | guards: { 250 | hasBuildErrors: ctx => Object.values(ctx.errors).some(Boolean), 251 | hasBinEntries: ctx => ctx.entries 252 | .some(entry => entry.key.startsWith('bin')), 253 | }, 254 | actions: { 255 | reportBuildStart: assign({ 256 | buildStartedAt: _ctx => performance.now(), 257 | }), 258 | reportBuildEnd: ctx => { 259 | const hasBuildErrors = Object.values(ctx.errors).some(Boolean); 260 | if (hasBuildErrors) { 261 | return; 262 | } 263 | const endedAt = performance.now(); 264 | const elapsedTime = (endedAt - ctx.buildStartedAt).toFixed(1); 265 | ctx.root.reporter.info(`⚡ Done in ${elapsedTime}ms.`); 266 | }, 267 | reportCleanupStart: ctx => { 268 | ctx.root.reporter.info(dedent` 269 | Cleanup outputs first 270 | 271 | `); 272 | }, 273 | reportCleanupEnd: ctx => { 274 | console.log() 275 | }, 276 | reportBuildErrors: ctx => { 277 | if (ctx.errors.buildBundle) { 278 | ctx.root.reporter.error(ctx.errors.buildBundle.message); 279 | for (const cause of ctx.errors.buildBundle.esbuildErrors) { 280 | ctx.root.reporter.error(cause.text); 281 | } 282 | } 283 | if (ctx.errors.buildFile) { 284 | for (const cause of ctx.errors.buildFile.reasons) { 285 | ctx.root.reporter.captureException(cause); 286 | } 287 | } 288 | if (ctx.errors.buildType) { 289 | ctx.root.reporter.captureException(ctx.errors.buildType); 290 | } 291 | }, 292 | assignBundleOutputs: assign({ 293 | bundleOutputs: (ctx, event) => [ 294 | ...ctx.bundleOutputs, 295 | ...(event.data as { outputFiles: OutputFile[] }).outputFiles, 296 | ], 297 | }), 298 | assignFileOutputs: assign({ 299 | bundleOutputs: (ctx, event) => [ 300 | ...ctx.fileOutputs, 301 | ...(event.data as { outputFiles: OutputFile[] }).outputFiles, 302 | ], 303 | }), 304 | assignTypeOutputs: assign({ 305 | typeOutputs: (ctx, event) => [ 306 | ...ctx.typeOutputs, 307 | ...(event.data as { outputFiles: OutputFile[] }).outputFiles, 308 | ], 309 | }), 310 | assignBuildBundleError: assign({ 311 | errors: (ctx, event) => ({ 312 | ...ctx.errors, 313 | buildBundle: event.data as BuildBundleTaskError, 314 | }), 315 | }), 316 | assignBuildFileError: assign({ 317 | errors: (ctx, event) => ({ 318 | ...ctx.errors, 319 | buildFile: event.data as BuildFileTaskError, 320 | }), 321 | }), 322 | assignBuildTypeError: assign((ctx, event) => { 323 | return { 324 | errors: { 325 | ...ctx.errors, 326 | buildType: event.data as BuildTypeTaskError, 327 | }, 328 | }; 329 | }), 330 | assignEmitError: assign({ 331 | errors: (ctx, event) => ({ 332 | ...ctx.errors, 333 | emit: event.data as EmitTaskError, 334 | }) 335 | }), 336 | }, 337 | services: { 338 | buildBundleTask: ctx => buildBundleTask({ 339 | context: ctx.root, 340 | bundleEntries: ctx.entries.filter(filterBundleEntry), 341 | }), 342 | buildFileTask: ctx => buildFileTask({ 343 | context: ctx.root, 344 | fileEntries: ctx.entries.filter(filterFileEntry), 345 | }), 346 | buildTypeTask: ctx => buildTypeTask({ 347 | context: ctx.root, 348 | typeEntries: ctx.entries.filter(filterTypeEntry), 349 | }), 350 | emitTask: ctx => emitTask({ 351 | context: ctx.root, 352 | outputFiles: [ 353 | ...ctx.bundleOutputs, 354 | ...ctx.fileOutputs, 355 | ...ctx.typeOutputs, 356 | ], 357 | }), 358 | reportEmitResults: ctx => reportEmitResultsTask({ 359 | context: ctx.root, 360 | bundleOutputs: ctx.bundleOutputs, 361 | fileOutputs: ctx.fileOutputs, 362 | typeOutputs: ctx.typeOutputs, 363 | }), 364 | cleanupTask: ctx => cleanupTask({ 365 | context: ctx.root, 366 | outputFiles: ctx.entries.map(entry => ({ 367 | sourcePath: entry.sourceFile[0], 368 | path: entry.outputFile, 369 | })), 370 | }), 371 | chmodBinTask: ctx => chmodBinTask({ 372 | context: ctx.root, 373 | binEntries: ctx.entries 374 | .filter(filterBundleEntry) 375 | .filter(entry => entry.key.startsWith('bin')), 376 | }), 377 | }, 378 | }); 379 | -------------------------------------------------------------------------------- /packages/nanobundle/src/commands/build/build.machine.typegen.ts: -------------------------------------------------------------------------------- 1 | // This file was automatically generated. Edits will be overwritten 2 | 3 | export interface Typegen0 { 4 | "@@xstate/typegen": true; 5 | internalEvents: { 6 | "done.invoke.buildMachine.buildEntries.buildBundleEntries.build:invocation[0]": { 7 | type: "done.invoke.buildMachine.buildEntries.buildBundleEntries.build:invocation[0]"; 8 | data: unknown; 9 | __tip: "See the XState TS docs to learn how to strongly type this."; 10 | }; 11 | "done.invoke.buildMachine.buildEntries.buildFileEntries.build:invocation[0]": { 12 | type: "done.invoke.buildMachine.buildEntries.buildFileEntries.build:invocation[0]"; 13 | data: unknown; 14 | __tip: "See the XState TS docs to learn how to strongly type this."; 15 | }; 16 | "done.invoke.buildMachine.buildEntries.buildTypeEntries.build:invocation[0]": { 17 | type: "done.invoke.buildMachine.buildEntries.buildTypeEntries.build:invocation[0]"; 18 | data: unknown; 19 | __tip: "See the XState TS docs to learn how to strongly type this."; 20 | }; 21 | "done.invoke.buildMachine.chmodBinEntries:invocation[0]": { 22 | type: "done.invoke.buildMachine.chmodBinEntries:invocation[0]"; 23 | data: unknown; 24 | __tip: "See the XState TS docs to learn how to strongly type this."; 25 | }; 26 | "done.invoke.buildMachine.cleanup:invocation[0]": { 27 | type: "done.invoke.buildMachine.cleanup:invocation[0]"; 28 | data: unknown; 29 | __tip: "See the XState TS docs to learn how to strongly type this."; 30 | }; 31 | "done.invoke.buildMachine.cleanupFirst:invocation[0]": { 32 | type: "done.invoke.buildMachine.cleanupFirst:invocation[0]"; 33 | data: unknown; 34 | __tip: "See the XState TS docs to learn how to strongly type this."; 35 | }; 36 | "done.invoke.buildMachine.emitEntries:invocation[0]": { 37 | type: "done.invoke.buildMachine.emitEntries:invocation[0]"; 38 | data: unknown; 39 | __tip: "See the XState TS docs to learn how to strongly type this."; 40 | }; 41 | "done.invoke.buildMachine.reportEmitResults:invocation[0]": { 42 | type: "done.invoke.buildMachine.reportEmitResults:invocation[0]"; 43 | data: unknown; 44 | __tip: "See the XState TS docs to learn how to strongly type this."; 45 | }; 46 | "error.platform.buildMachine.buildEntries.buildBundleEntries.build:invocation[0]": { 47 | type: "error.platform.buildMachine.buildEntries.buildBundleEntries.build:invocation[0]"; 48 | data: unknown; 49 | }; 50 | "error.platform.buildMachine.buildEntries.buildFileEntries.build:invocation[0]": { 51 | type: "error.platform.buildMachine.buildEntries.buildFileEntries.build:invocation[0]"; 52 | data: unknown; 53 | }; 54 | "error.platform.buildMachine.buildEntries.buildTypeEntries.build:invocation[0]": { 55 | type: "error.platform.buildMachine.buildEntries.buildTypeEntries.build:invocation[0]"; 56 | data: unknown; 57 | }; 58 | "error.platform.buildMachine.chmodBinEntries:invocation[0]": { 59 | type: "error.platform.buildMachine.chmodBinEntries:invocation[0]"; 60 | data: unknown; 61 | }; 62 | "error.platform.buildMachine.cleanup:invocation[0]": { 63 | type: "error.platform.buildMachine.cleanup:invocation[0]"; 64 | data: unknown; 65 | }; 66 | "error.platform.buildMachine.cleanupFirst:invocation[0]": { 67 | type: "error.platform.buildMachine.cleanupFirst:invocation[0]"; 68 | data: unknown; 69 | }; 70 | "error.platform.buildMachine.emitEntries:invocation[0]": { 71 | type: "error.platform.buildMachine.emitEntries:invocation[0]"; 72 | data: unknown; 73 | }; 74 | "xstate.init": { type: "xstate.init" }; 75 | "xstate.stop": { type: "xstate.stop" }; 76 | }; 77 | invokeSrcNameMap: { 78 | buildBundleTask: "done.invoke.buildMachine.buildEntries.buildBundleEntries.build:invocation[0]"; 79 | buildFileTask: "done.invoke.buildMachine.buildEntries.buildFileEntries.build:invocation[0]"; 80 | buildTypeTask: "done.invoke.buildMachine.buildEntries.buildTypeEntries.build:invocation[0]"; 81 | chmodBinTask: "done.invoke.buildMachine.chmodBinEntries:invocation[0]"; 82 | cleanupTask: 83 | | "done.invoke.buildMachine.cleanup:invocation[0]" 84 | | "done.invoke.buildMachine.cleanupFirst:invocation[0]"; 85 | emitTask: "done.invoke.buildMachine.emitEntries:invocation[0]"; 86 | reportEmitResults: "done.invoke.buildMachine.reportEmitResults:invocation[0]"; 87 | }; 88 | missingImplementations: { 89 | actions: never; 90 | delays: never; 91 | guards: never; 92 | services: never; 93 | }; 94 | eventsCausingActions: { 95 | assignBuildBundleError: "error.platform.buildMachine.buildEntries.buildBundleEntries.build:invocation[0]"; 96 | assignBuildFileError: "error.platform.buildMachine.buildEntries.buildFileEntries.build:invocation[0]"; 97 | assignBuildTypeError: "error.platform.buildMachine.buildEntries.buildTypeEntries.build:invocation[0]"; 98 | assignBundleOutputs: "done.invoke.buildMachine.buildEntries.buildBundleEntries.build:invocation[0]"; 99 | assignEmitError: "error.platform.buildMachine.emitEntries:invocation[0]"; 100 | assignFileOutputs: "done.invoke.buildMachine.buildEntries.buildFileEntries.build:invocation[0]"; 101 | assignTypeOutputs: "done.invoke.buildMachine.buildEntries.buildTypeEntries.build:invocation[0]"; 102 | reportBuildEnd: 103 | | "done.invoke.buildMachine.chmodBinEntries:invocation[0]" 104 | | "done.invoke.buildMachine.cleanup:invocation[0]" 105 | | "done.invoke.buildMachine.reportEmitResults:invocation[0]" 106 | | "error.platform.buildMachine.chmodBinEntries:invocation[0]" 107 | | "error.platform.buildMachine.cleanup:invocation[0]"; 108 | reportBuildErrors: "done.state.buildMachine.buildEntries"; 109 | reportBuildStart: "BUILD"; 110 | reportCleanupEnd: 111 | | "done.invoke.buildMachine.cleanupFirst:invocation[0]" 112 | | "error.platform.buildMachine.cleanupFirst:invocation[0]" 113 | | "xstate.stop"; 114 | reportCleanupStart: "CLEAN"; 115 | }; 116 | eventsCausingDelays: {}; 117 | eventsCausingGuards: { 118 | hasBinEntries: "done.invoke.buildMachine.reportEmitResults:invocation[0]"; 119 | hasBuildErrors: "done.state.buildMachine.buildEntries"; 120 | }; 121 | eventsCausingServices: { 122 | buildBundleTask: "BUILD"; 123 | buildFileTask: "BUILD"; 124 | buildTypeTask: "BUILD"; 125 | chmodBinTask: "done.invoke.buildMachine.reportEmitResults:invocation[0]"; 126 | cleanupTask: 127 | | "CLEAN" 128 | | "done.state.buildMachine.buildEntries" 129 | | "error.platform.buildMachine.emitEntries:invocation[0]"; 130 | emitTask: "done.state.buildMachine.buildEntries"; 131 | reportEmitResults: "done.invoke.buildMachine.emitEntries:invocation[0]"; 132 | }; 133 | matchesStates: 134 | | "bootstrap" 135 | | "buildEntries" 136 | | "buildEntries.buildBundleEntries" 137 | | "buildEntries.buildBundleEntries.build" 138 | | "buildEntries.buildBundleEntries.failure" 139 | | "buildEntries.buildBundleEntries.success" 140 | | "buildEntries.buildFileEntries" 141 | | "buildEntries.buildFileEntries.build" 142 | | "buildEntries.buildFileEntries.failure" 143 | | "buildEntries.buildFileEntries.success" 144 | | "buildEntries.buildTypeEntries" 145 | | "buildEntries.buildTypeEntries.build" 146 | | "buildEntries.buildTypeEntries.failure" 147 | | "buildEntries.buildTypeEntries.success" 148 | | "chmodBinEntries" 149 | | "cleanup" 150 | | "cleanupFirst" 151 | | "done" 152 | | "emitEntries" 153 | | "reportEmitResults" 154 | | { 155 | buildEntries?: 156 | | "buildBundleEntries" 157 | | "buildFileEntries" 158 | | "buildTypeEntries" 159 | | { 160 | buildBundleEntries?: "build" | "failure" | "success"; 161 | buildFileEntries?: "build" | "failure" | "success"; 162 | buildTypeEntries?: "build" | "failure" | "success"; 163 | }; 164 | }; 165 | tags: never; 166 | } 167 | -------------------------------------------------------------------------------- /packages/nanobundle/src/commands/build/index.ts: -------------------------------------------------------------------------------- 1 | import dedent from 'string-dedent'; 2 | import { performance } from 'node:perf_hooks'; 3 | import { interpret } from 'xstate'; 4 | 5 | import { type Context } from '../../context'; 6 | import * as formatUtils from '../../formatUtils'; 7 | import { type Entry } from '../../entry'; 8 | import { NanobundleError } from '../../errors'; 9 | 10 | import { buildMachine } from './build.machine'; 11 | 12 | type BuildCommandOptions = { 13 | context: Context, 14 | entries: Entry[], 15 | cleanFirst?: boolean, 16 | }; 17 | 18 | export async function buildCommand({ 19 | context, 20 | entries, 21 | cleanFirst = false, 22 | }: BuildCommandOptions): Promise { 23 | context.reporter.info(dedent` 24 | Build ${formatUtils.highlight(context.manifest.name || 'unnamed')} package 25 | 26 | `); 27 | 28 | const service = interpret( 29 | buildMachine 30 | .withContext({ 31 | root: context, 32 | entries, 33 | bundleOutputs: [], 34 | fileOutputs: [], 35 | typeOutputs: [], 36 | errors: {}, 37 | buildStartedAt: performance.now(), 38 | }), 39 | ); 40 | service.start(); 41 | 42 | if (cleanFirst) { 43 | service.send('CLEAN'); 44 | service.onTransition(state => { 45 | if (state.can('BUILD')) { 46 | service.send('BUILD'); 47 | } 48 | }); 49 | } else { 50 | service.send('BUILD'); 51 | } 52 | 53 | return new Promise((resolve, reject) => { 54 | service.onDone(() => { 55 | const state = service.getSnapshot(); 56 | const hasBuildErrors = Object.values(state.context.errors).some(Boolean); 57 | if (hasBuildErrors) { 58 | reject(new NanobundleError()); 59 | } else { 60 | resolve(); 61 | } 62 | }); 63 | }); 64 | } 65 | -------------------------------------------------------------------------------- /packages/nanobundle/src/commands/clean/index.ts: -------------------------------------------------------------------------------- 1 | import dedent from 'string-dedent'; 2 | 3 | import { type Context } from '../../context'; 4 | import * as formatUtils from '../../formatUtils'; 5 | import { type Entry } from '../../entry'; 6 | import { cleanupTask } from '../../tasks/cleanupTask'; 7 | 8 | type CleanCommandOptions = { 9 | context: Context, 10 | entries: Entry[], 11 | }; 12 | 13 | export async function cleanCommand({ 14 | context, 15 | entries, 16 | }: CleanCommandOptions): Promise { 17 | context.reporter.info(dedent` 18 | Clean ${formatUtils.highlight(context.manifest.name || 'unnamed')} package 19 | 20 | `); 21 | 22 | const outputFiles = entries.map(entry => ({ 23 | sourcePath: entry.sourceFile[0], 24 | path: entry.outputFile, 25 | })); 26 | await cleanupTask({ context, outputFiles }); 27 | } -------------------------------------------------------------------------------- /packages/nanobundle/src/common.ts: -------------------------------------------------------------------------------- 1 | export type PathResolver = (...paths: string[]) => string; 2 | export type RelativePathResolver = (path: string, startsWithDot?: boolean) => string; 3 | -------------------------------------------------------------------------------- /packages/nanobundle/src/context.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, test, expect, vi } from 'vitest'; 2 | import { type TSConfig } from 'pkg-types'; 3 | 4 | import { type Context } from './context'; 5 | import { type Flags } from './cli'; 6 | import { type Manifest } from './manifest'; 7 | import { type Reporter } from './reporter'; 8 | import { parseConfig } from './context'; 9 | import { loadTargets } from './target'; 10 | 11 | class ViReporter implements Reporter { 12 | debug = vi.fn(); 13 | info = vi.fn(); 14 | warn = vi.fn(); 15 | error = vi.fn(); 16 | captureException = vi.fn(); 17 | createChildReporter() { 18 | return new ViReporter(); 19 | } 20 | } 21 | 22 | describe('parseConfig', () => { 23 | const reporter = new ViReporter(); 24 | const defaultFlags: Flags = { 25 | cwd: '/project', 26 | clean: false, 27 | verbose: false, 28 | platform: undefined, 29 | rootDir: undefined, 30 | outDir: undefined, 31 | tsconfig: 'tsconfig.json', 32 | importMaps: 'package.json', 33 | jsx: undefined, 34 | jsxFactory: undefined, 35 | jsxFragment: undefined, 36 | jsxImportSource: undefined, 37 | external: [], 38 | standalone: false, 39 | bundle: true, 40 | dts: true, 41 | sourcemap: true, 42 | legalComments: true, 43 | }; 44 | const defaultManifest: Manifest = { 45 | name: 'package', 46 | }; 47 | const defaultTsConfig: TSConfig = { 48 | compilerOptions: { 49 | target: 'ESNext', // ESNext, 50 | declaration: true, 51 | }, 52 | }; 53 | const defaultTargets = loadTargets({ 54 | manifest: defaultManifest, 55 | }); 56 | 57 | test('validate manifest', () => { 58 | const result = parseConfig({ 59 | flags: defaultFlags, 60 | manifest: defaultManifest, 61 | targets: defaultTargets, 62 | reporter, 63 | }); 64 | 65 | expect(result).toEqual({ 66 | cwd: '/project', 67 | verbose: false, 68 | module: 'commonjs', 69 | platform: 'neutral', 70 | sourcemap: true, 71 | legalComments: true, 72 | bundle: true, 73 | declaration: false, 74 | standalone: false, 75 | jsx: undefined, 76 | jsxDev: false, 77 | jsxFactory: 'React.createElement', 78 | jsxFragment: 'Fragment', 79 | jsxImportSource: 'react', 80 | rootDir: 'src', 81 | outDir: 'lib', 82 | tsconfigPath: undefined, 83 | importMapsPath: '/project/package.json', 84 | externalDependencies: [], 85 | forceExternalDependencies: [], 86 | manifest: defaultManifest, 87 | targets: defaultTargets, 88 | reporter, 89 | resolvePath: expect.any(Function), 90 | resolveRelativePath: expect.any(Function), 91 | }); 92 | }); 93 | 94 | describe('flags', () => { 95 | test('--rootDir', () => { 96 | const result = parseConfig({ 97 | flags: { 98 | ...defaultFlags, 99 | rootDir: '.', 100 | }, 101 | manifest: defaultManifest, 102 | targets: defaultTargets, 103 | reporter, 104 | }); 105 | 106 | expect(result).toEqual({ 107 | cwd: '/project', 108 | verbose: false, 109 | module: 'commonjs', 110 | platform: 'neutral', 111 | sourcemap: true, 112 | legalComments: true, 113 | bundle: true, 114 | declaration: false, 115 | standalone: false, 116 | rootDir: '.', 117 | outDir: 'lib', 118 | jsx: undefined, 119 | jsxDev: false, 120 | jsxFactory: 'React.createElement', 121 | jsxFragment: 'Fragment', 122 | jsxImportSource: 'react', 123 | tsconfigPath: undefined, 124 | importMapsPath: '/project/package.json', 125 | externalDependencies: [], 126 | forceExternalDependencies: [], 127 | manifest: defaultManifest, 128 | targets: defaultTargets, 129 | reporter, 130 | resolvePath: expect.any(Function), 131 | resolveRelativePath: expect.any(Function), 132 | }); 133 | }); 134 | 135 | test('--outDir', () => { 136 | const result = parseConfig({ 137 | flags: { 138 | ...defaultFlags, 139 | outDir: '.', 140 | }, 141 | manifest: defaultManifest, 142 | targets: defaultTargets, 143 | reporter, 144 | }); 145 | 146 | expect(result).toEqual({ 147 | cwd: '/project', 148 | verbose: false, 149 | module: 'commonjs', 150 | platform: 'neutral', 151 | sourcemap: true, 152 | legalComments: true, 153 | bundle: true, 154 | declaration: false, 155 | standalone: false, 156 | rootDir: 'src', 157 | outDir: '.', 158 | tsconfigPath: undefined, 159 | jsx: undefined, 160 | jsxDev: false, 161 | jsxFactory: 'React.createElement', 162 | jsxFragment: 'Fragment', 163 | jsxImportSource: 'react', 164 | importMapsPath: '/project/package.json', 165 | externalDependencies: [], 166 | forceExternalDependencies: [], 167 | manifest: defaultManifest, 168 | targets: defaultTargets, 169 | reporter, 170 | resolvePath: expect.any(Function), 171 | resolveRelativePath: expect.any(Function), 172 | }); 173 | }); 174 | 175 | test('rootDir=outDir is not allowed without TypeScript', () => { 176 | expect(() => parseConfig({ 177 | flags: { 178 | ...defaultFlags, 179 | rootDir: '.', 180 | outDir: '.', 181 | }, 182 | manifest: defaultManifest, 183 | targets: defaultTargets, 184 | reporter, 185 | })).toThrowErrorMatchingSnapshot(); 186 | }); 187 | }); 188 | 189 | describe('node platform', () => { 190 | test('platform should be node when engines.node exist', () => { 191 | const manifest = { 192 | ...defaultManifest, 193 | engines: { 194 | node: '>=16.0.0', 195 | }, 196 | }; 197 | 198 | const targets = loadTargets({ 199 | manifest, 200 | }); 201 | 202 | const result = parseConfig({ 203 | flags: defaultFlags, 204 | targets, 205 | manifest, 206 | reporter, 207 | }); 208 | 209 | expect(result).toEqual({ 210 | cwd: '/project', 211 | verbose: false, 212 | module: 'commonjs', 213 | platform: 'node', 214 | sourcemap: true, 215 | legalComments: true, 216 | bundle: true, 217 | declaration: false, 218 | standalone: false, 219 | rootDir: 'src', 220 | outDir: 'lib', 221 | tsconfigPath: undefined, 222 | importMapsPath: '/project/package.json', 223 | jsx: undefined, 224 | jsxDev: false, 225 | jsxFactory: 'React.createElement', 226 | jsxFragment: 'Fragment', 227 | jsxImportSource: 'react', 228 | externalDependencies: [], 229 | forceExternalDependencies: [], 230 | manifest, 231 | targets: [ 232 | 'chrome96', 233 | 'firefox115', 234 | 'edge124', 235 | 'ios15.6', 236 | 'safari17.4', 237 | 'node16.0.0', 238 | 'deno1.9', 239 | ], 240 | reporter, 241 | resolvePath: expect.any(Function), 242 | resolveRelativePath: expect.any(Function), 243 | }); 244 | }); 245 | }); 246 | 247 | describe('externalDependencies', () => { 248 | test('externalDependencies has manifest dependencies', () => { 249 | const result = parseConfig({ 250 | flags: defaultFlags, 251 | manifest: { 252 | ...defaultManifest, 253 | dependencies: { 254 | 'dependency-a': '^1.0.0', 255 | 'dependency-b': '^1.0.0', 256 | 'dependency-c': '^1.0.0', 257 | }, 258 | peerDependencies: { 259 | 'peer-dependency-a': '^1.0.0', 260 | 'peer-dependency-b': '^1.0.0', 261 | }, 262 | }, 263 | targets: defaultTargets, 264 | reporter, 265 | }); 266 | 267 | expect(result).toEqual({ 268 | cwd: '/project', 269 | verbose: false, 270 | module: 'commonjs', 271 | platform: 'neutral', 272 | sourcemap: true, 273 | legalComments: true, 274 | bundle: true, 275 | declaration: false, 276 | standalone: false, 277 | rootDir: 'src', 278 | outDir: 'lib', 279 | tsconfigPath: undefined, 280 | jsx: undefined, 281 | jsxDev: false, 282 | jsxFactory: 'React.createElement', 283 | jsxFragment: 'Fragment', 284 | jsxImportSource: 'react', 285 | importMapsPath: '/project/package.json', 286 | externalDependencies: [ 287 | 'dependency-a', 288 | 'dependency-b', 289 | 'dependency-c', 290 | 'peer-dependency-a', 291 | 'peer-dependency-b', 292 | ], 293 | forceExternalDependencies: [], 294 | manifest: { 295 | ...defaultManifest, 296 | dependencies: { 297 | 'dependency-a': '^1.0.0', 298 | 'dependency-b': '^1.0.0', 299 | 'dependency-c': '^1.0.0', 300 | }, 301 | peerDependencies: { 302 | 'peer-dependency-a': '^1.0.0', 303 | 'peer-dependency-b': '^1.0.0', 304 | }, 305 | }, 306 | targets: defaultTargets, 307 | reporter, 308 | resolvePath: expect.any(Function), 309 | resolveRelativePath: expect.any(Function), 310 | }); 311 | }); 312 | 313 | test('forceExternalDependencies always include --external flag list', () => { 314 | const result = parseConfig({ 315 | flags: { 316 | ...defaultFlags, 317 | external: [ 318 | 'external-a', 319 | 'external-b', 320 | 'external-c', 321 | ], 322 | }, 323 | manifest: defaultManifest, 324 | targets: defaultTargets, 325 | reporter, 326 | }); 327 | 328 | expect(result).toEqual({ 329 | cwd: '/project', 330 | verbose: false, 331 | module: 'commonjs', 332 | platform: 'neutral', 333 | sourcemap: true, 334 | legalComments: true, 335 | bundle: true, 336 | declaration: false, 337 | standalone: false, 338 | rootDir: 'src', 339 | outDir: 'lib', 340 | tsconfigPath: undefined, 341 | jsx: undefined, 342 | jsxDev: false, 343 | jsxFactory: 'React.createElement', 344 | jsxFragment: 'Fragment', 345 | jsxImportSource: 'react', 346 | importMapsPath: '/project/package.json', 347 | externalDependencies: [ 348 | 'external-a', 349 | 'external-b', 350 | 'external-c', 351 | ], 352 | forceExternalDependencies: [ 353 | 'external-a', 354 | 'external-b', 355 | 'external-c', 356 | ], 357 | manifest: defaultManifest, 358 | targets: defaultTargets, 359 | reporter, 360 | resolvePath: expect.any(Function), 361 | resolveRelativePath: expect.any(Function), 362 | }); 363 | }); 364 | }); 365 | 366 | describe('tsCompilerOptions', () => { 367 | test('declaration=true', () => { 368 | const result = parseConfig({ 369 | flags: defaultFlags, 370 | manifest: defaultManifest, 371 | targets: defaultTargets, 372 | reporter, 373 | tsconfigPath: 'tsconfig.json', 374 | tsconfig: defaultTsConfig, 375 | }); 376 | 377 | expect(result).toEqual({ 378 | cwd: '/project', 379 | verbose: false, 380 | module: 'commonjs', 381 | platform: 'neutral', 382 | sourcemap: true, 383 | legalComments: true, 384 | bundle: true, 385 | declaration: true, 386 | standalone: false, 387 | rootDir: 'src', 388 | outDir: 'lib', 389 | tsconfigPath: 'tsconfig.json', 390 | importMapsPath: '/project/package.json', 391 | jsx: undefined, 392 | jsxDev: false, 393 | jsxFactory: 'React.createElement', 394 | jsxFragment: 'Fragment', 395 | jsxImportSource: 'react', 396 | externalDependencies: [], 397 | forceExternalDependencies: [], 398 | manifest: defaultManifest, 399 | targets: defaultTargets, 400 | reporter, 401 | resolvePath: expect.any(Function), 402 | resolveRelativePath: expect.any(Function), 403 | }); 404 | }); 405 | 406 | test('declaration=false', () => { 407 | const result = parseConfig({ 408 | flags: defaultFlags, 409 | manifest: defaultManifest, 410 | targets: defaultTargets, 411 | reporter, 412 | tsconfigPath: 'tsconfig.json', 413 | tsconfig: { 414 | compilerOptions: { 415 | ...defaultTsConfig.compilerOptions, 416 | declaration: false, 417 | standalone: false, 418 | }, 419 | }, 420 | }); 421 | 422 | expect(result).toEqual({ 423 | cwd: '/project', 424 | verbose: false, 425 | module: 'commonjs', 426 | platform: 'neutral', 427 | sourcemap: true, 428 | legalComments: true, 429 | bundle: true, 430 | declaration: false, 431 | standalone: false, 432 | rootDir: 'src', 433 | outDir: 'lib', 434 | tsconfigPath: 'tsconfig.json', 435 | importMapsPath: '/project/package.json', 436 | jsx: undefined, 437 | jsxDev: false, 438 | jsxFactory: 'React.createElement', 439 | jsxFragment: 'Fragment', 440 | jsxImportSource: 'react', 441 | externalDependencies: [], 442 | forceExternalDependencies: [], 443 | manifest: defaultManifest, 444 | targets: defaultTargets, 445 | reporter, 446 | resolvePath: expect.any(Function), 447 | resolveRelativePath: expect.any(Function), 448 | }); 449 | }); 450 | 451 | test('declaration=true, --no-dts', () => { 452 | const result = parseConfig({ 453 | flags: { 454 | ...defaultFlags, 455 | dts: false, 456 | }, 457 | manifest: defaultManifest, 458 | targets: defaultTargets, 459 | reporter, 460 | tsconfigPath: 'tsconfig.json', 461 | tsconfig: defaultTsConfig, 462 | }); 463 | 464 | expect(result).toEqual({ 465 | cwd: '/project', 466 | verbose: false, 467 | module: 'commonjs', 468 | platform: 'neutral', 469 | sourcemap: true, 470 | legalComments: true, 471 | bundle: true, 472 | declaration: false, 473 | standalone: false, 474 | rootDir: 'src', 475 | outDir: 'lib', 476 | tsconfigPath: 'tsconfig.json', 477 | importMapsPath: '/project/package.json', 478 | jsx: undefined, 479 | jsxDev: false, 480 | jsxFactory: 'React.createElement', 481 | jsxFragment: 'Fragment', 482 | jsxImportSource: 'react', 483 | externalDependencies: [], 484 | forceExternalDependencies: [], 485 | manifest: defaultManifest, 486 | targets: defaultTargets, 487 | reporter, 488 | resolvePath: expect.any(Function), 489 | resolveRelativePath: expect.any(Function), 490 | }); 491 | }); 492 | 493 | test('respect rootDir and outDir in compilerOptions', () => { 494 | const result = parseConfig({ 495 | flags: defaultFlags, 496 | manifest: defaultManifest, 497 | targets: defaultTargets, 498 | reporter, 499 | tsconfigPath: 'tsconfig.json', 500 | tsconfig: { 501 | ...defaultTsConfig, 502 | compilerOptions: { 503 | ...defaultTsConfig.compilerOptions, 504 | rootDir: 'tsconfig-src', 505 | outDir: 'tsconfig-lib', 506 | }, 507 | }, 508 | }); 509 | 510 | expect(result).toEqual({ 511 | cwd: '/project', 512 | verbose: false, 513 | module: 'commonjs', 514 | platform: 'neutral', 515 | sourcemap: true, 516 | legalComments: true, 517 | bundle: true, 518 | declaration: true, 519 | standalone: false, 520 | rootDir: 'tsconfig-src', 521 | outDir: 'tsconfig-lib', 522 | tsconfigPath: 'tsconfig.json', 523 | importMapsPath: '/project/package.json', 524 | jsx: undefined, 525 | jsxDev: false, 526 | jsxFactory: 'React.createElement', 527 | jsxFragment: 'Fragment', 528 | jsxImportSource: 'react', 529 | externalDependencies: [], 530 | forceExternalDependencies: [], 531 | manifest: defaultManifest, 532 | targets: defaultTargets, 533 | reporter, 534 | resolvePath: expect.any(Function), 535 | resolveRelativePath: expect.any(Function), 536 | }); 537 | }); 538 | 539 | test('respect jsx in compilerOptions', () => { 540 | const result = parseConfig({ 541 | flags: defaultFlags, 542 | manifest: defaultManifest, 543 | targets: defaultTargets, 544 | reporter, 545 | tsconfigPath: 'tsconfig.json', 546 | tsconfig: { 547 | ...defaultTsConfig, 548 | compilerOptions: { 549 | ...defaultTsConfig.compilerOptions, 550 | jsx: 'react-jsxdev', 551 | }, 552 | }, 553 | }); 554 | 555 | expect(result).toEqual({ 556 | cwd: '/project', 557 | verbose: false, 558 | module: 'commonjs', 559 | platform: 'neutral', 560 | sourcemap: true, 561 | legalComments: true, 562 | bundle: true, 563 | declaration: true, 564 | standalone: false, 565 | rootDir: 'src', 566 | outDir: 'lib', 567 | tsconfigPath: 'tsconfig.json', 568 | importMapsPath: '/project/package.json', 569 | jsx: 'automatic', 570 | jsxDev: true, 571 | jsxFactory: 'React.createElement', 572 | jsxFragment: 'Fragment', 573 | jsxImportSource: 'react', 574 | externalDependencies: [], 575 | forceExternalDependencies: [], 576 | manifest: defaultManifest, 577 | targets: defaultTargets, 578 | reporter, 579 | resolvePath: expect.any(Function), 580 | resolveRelativePath: expect.any(Function), 581 | }); 582 | }); 583 | 584 | test('respect jsxSource in compilerOptions', () => { 585 | const result = parseConfig({ 586 | flags: defaultFlags, 587 | manifest: defaultManifest, 588 | targets: defaultTargets, 589 | reporter, 590 | tsconfigPath: 'tsconfig.json', 591 | tsconfig: { 592 | ...defaultTsConfig, 593 | compilerOptions: { 594 | ...defaultTsConfig.compilerOptions, 595 | jsx: 'react', 596 | jsxFactory: 'h', 597 | jsxFragmentFactory: 'Fragment', 598 | jsxImportSource: 'preact', 599 | }, 600 | }, 601 | }); 602 | 603 | expect(result).toEqual({ 604 | cwd: '/project', 605 | verbose: false, 606 | module: 'commonjs', 607 | platform: 'neutral', 608 | sourcemap: true, 609 | legalComments: true, 610 | bundle: true, 611 | declaration: true, 612 | standalone: false, 613 | rootDir: 'src', 614 | outDir: 'lib', 615 | tsconfigPath: 'tsconfig.json', 616 | importMapsPath: '/project/package.json', 617 | jsx: 'transform', 618 | jsxDev: false, 619 | jsxFactory: 'h', 620 | jsxFragment: 'Fragment', 621 | jsxImportSource: 'preact', 622 | externalDependencies: [], 623 | forceExternalDependencies: [], 624 | manifest: defaultManifest, 625 | targets: defaultTargets, 626 | reporter, 627 | resolvePath: expect.any(Function), 628 | resolveRelativePath: expect.any(Function), 629 | }); 630 | }); 631 | 632 | test('respect sourceMap in compilerOptions', () => { 633 | const result = parseConfig({ 634 | flags: defaultFlags, 635 | manifest: defaultManifest, 636 | targets: defaultTargets, 637 | reporter, 638 | tsconfigPath: 'tsconfig.json', 639 | tsconfig: { 640 | ...defaultTsConfig, 641 | compilerOptions: { 642 | ...defaultTsConfig.compilerOptions, 643 | sourceMap: false, 644 | }, 645 | }, 646 | }); 647 | 648 | expect(result).toEqual({ 649 | cwd: '/project', 650 | verbose: false, 651 | module: 'commonjs', 652 | platform: 'neutral', 653 | sourcemap: false, 654 | legalComments: true, 655 | bundle: true, 656 | declaration: true, 657 | standalone: false, 658 | rootDir: 'src', 659 | outDir: 'lib', 660 | tsconfigPath: 'tsconfig.json', 661 | importMapsPath: '/project/package.json', 662 | jsx: undefined, 663 | jsxDev: false, 664 | jsxFactory: 'React.createElement', 665 | jsxFragment: 'Fragment', 666 | jsxImportSource: 'react', 667 | externalDependencies: [], 668 | forceExternalDependencies: [], 669 | manifest: defaultManifest, 670 | targets: defaultTargets, 671 | reporter, 672 | resolvePath: expect.any(Function), 673 | resolveRelativePath: expect.any(Function), 674 | }); 675 | }); 676 | 677 | test('flags precedense over tsconfig', () => { 678 | const result = parseConfig({ 679 | flags: { 680 | ...defaultFlags, 681 | sourcemap: false, 682 | jsx: 'preserve', 683 | rootDir: 'flags-src', 684 | outDir: 'flags-lib', 685 | }, 686 | manifest: defaultManifest, 687 | targets: defaultTargets, 688 | reporter, 689 | tsconfigPath: 'tsconfig.json', 690 | tsconfig: { 691 | ...defaultTsConfig, 692 | compilerOptions: { 693 | ...defaultTsConfig.compilerOptions, 694 | sourceMap: true, 695 | jsx: 'react-jsxdev', 696 | rootDir: 'tsconfig-src', 697 | outDir: 'tsconfig-lib', 698 | }, 699 | }, 700 | }); 701 | 702 | expect(result).toEqual({ 703 | cwd: '/project', 704 | verbose: false, 705 | module: 'commonjs', 706 | platform: 'neutral', 707 | sourcemap: false, 708 | legalComments: true, 709 | bundle: true, 710 | declaration: true, 711 | standalone: false, 712 | rootDir: 'flags-src', 713 | outDir: 'flags-lib', 714 | tsconfigPath: 'tsconfig.json', 715 | importMapsPath: '/project/package.json', 716 | jsx: 'preserve', 717 | jsxDev: false, 718 | jsxFactory: 'React.createElement', 719 | jsxFragment: 'Fragment', 720 | jsxImportSource: 'react', 721 | externalDependencies: [], 722 | forceExternalDependencies: [], 723 | manifest: defaultManifest, 724 | targets: defaultTargets, 725 | reporter, 726 | resolvePath: expect.any(Function), 727 | resolveRelativePath: expect.any(Function), 728 | }); 729 | }); 730 | 731 | test('rootDir=outDir is allowed with TypeScript', () => { 732 | expect(() => parseConfig({ 733 | flags: { 734 | ...defaultFlags, 735 | rootDir: '.', 736 | outDir: '.', 737 | }, 738 | manifest: defaultManifest, 739 | targets: defaultTargets, 740 | reporter, 741 | tsconfig: defaultTsConfig, 742 | })).not.toThrowError(); 743 | }); 744 | }); 745 | }); 746 | -------------------------------------------------------------------------------- /packages/nanobundle/src/context.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'node:path'; 2 | import dedent from 'string-dedent'; 3 | import { type TSConfig } from 'pkg-types'; 4 | 5 | import { type Flags } from './cli'; 6 | import { type Manifest } from './manifest'; 7 | import { type Entry } from './entry'; 8 | import { type Reporter } from './reporter'; 9 | import { type PathResolver, type RelativePathResolver } from './common'; 10 | import * as formatUtils from './formatUtils'; 11 | import { NanobundleError } from './errors'; 12 | 13 | export class NanobundleConfigError extends NanobundleError { 14 | name = 'NanobundleConfigError'; 15 | } 16 | 17 | export type Context = { 18 | cwd: string, 19 | verbose: boolean, 20 | module: Entry['module'], 21 | platform: Entry['platform'], 22 | sourcemap: boolean, 23 | legalComments: boolean, 24 | bundle: boolean, 25 | declaration: boolean, 26 | jsx: 'transform' | 'preserve' | 'automatic' | undefined, 27 | jsxDev: boolean, 28 | jsxFactory: string, 29 | jsxFragment: string, 30 | jsxImportSource: string, 31 | standalone: boolean, 32 | rootDir: string, 33 | outDir: string, 34 | tsconfigPath?: string, 35 | importMapsPath: string, 36 | externalDependencies: string[], 37 | forceExternalDependencies: string[], 38 | manifest: Manifest, 39 | targets: string[], 40 | reporter: Reporter, 41 | resolvePath: PathResolver, 42 | resolveRelativePath: RelativePathResolver, 43 | }; 44 | 45 | export type Config = { 46 | flags: Flags, 47 | manifest: Manifest, 48 | targets: string[], 49 | reporter: Reporter, 50 | tsconfig?: TSConfig, 51 | tsconfigPath?: string, 52 | }; 53 | 54 | export function parseConfig({ 55 | flags, 56 | manifest, 57 | targets, 58 | reporter, 59 | tsconfig, 60 | tsconfigPath: resolvedTsConfigPath, 61 | }: Config): Context { 62 | const cwd = path.resolve(flags.cwd); 63 | const resolvePath: PathResolver = (...paths: string[]) => path.resolve(cwd, ...paths); 64 | const resolveRelativePath: RelativePathResolver = (targetPath: string, startsWithDot = false) => { 65 | const relativePath = path.relative(cwd, targetPath); 66 | if (startsWithDot) return `./${relativePath}`; 67 | return relativePath; 68 | } 69 | const bundle = flags.bundle; 70 | const verbose = flags.verbose; 71 | const legalComments = flags.legalComments; 72 | const standalone = flags.standalone; 73 | const tsconfigPath = resolvedTsConfigPath; 74 | const importMapsPath = path.resolve(cwd, flags.importMaps); 75 | const forceExternalDependencies = flags.external; 76 | const externalDependencies = [ 77 | ...(manifest.dependencies ? Object.keys(manifest.dependencies) : []), 78 | ...(manifest.peerDependencies ? Object.keys(manifest.peerDependencies) : []), 79 | ...forceExternalDependencies, 80 | ]; 81 | 82 | const rootDir: string = ( 83 | flags.rootDir || 84 | tsconfig?.compilerOptions?.rootDir || 85 | 'src' 86 | ); 87 | const outDir: string = ( 88 | flags.outDir || 89 | tsconfig?.compilerOptions?.outDir || 90 | 'lib' 91 | ); 92 | 93 | const module = ( 94 | manifest.type === 'module' 95 | ? 'esmodule' 96 | : 'commonjs' 97 | ); 98 | 99 | let sourcemap: Context['sourcemap'] = true; 100 | if (tsconfig?.compilerOptions?.sourceMap != null) { 101 | sourcemap = tsconfig.compilerOptions.sourceMap; 102 | } 103 | if (flags.sourcemap === false) { 104 | sourcemap = false; 105 | } 106 | 107 | if (flags.platform) { 108 | reporter.warn(`${formatUtils.literal('--platform')} flag is deprecated, and will be removed next major update.`); 109 | } 110 | 111 | let platform: Entry['platform'] = 'neutral'; 112 | if (['node', 'deno', 'browser'].includes(flags.platform || '')) { 113 | platform = flags.platform as Entry['platform']; 114 | } else if (manifest.engines?.node) { 115 | platform = 'node'; 116 | } else if (manifest.engines?.deno) { 117 | platform = 'deno'; 118 | } 119 | 120 | let declaration = false; 121 | if (flags.dts && tsconfig) { 122 | declaration = tsconfig.compilerOptions?.declaration !== false; 123 | } 124 | 125 | if (!declaration && rootDir === outDir) { 126 | throw new NanobundleConfigError(dedent` 127 | ${formatUtils.key('rootDir')} (${formatUtils.path(rootDir)}) and ${formatUtils.key('outDir')} (${formatUtils.path(outDir)}) are conflict! 128 | 129 | Please specify different directory for one of them. 130 | `,) 131 | } 132 | 133 | let jsx: Context['jsx'] = undefined; 134 | if (tsconfig?.compilerOptions?.jsx === 'preserve') { 135 | jsx = 'preserve'; 136 | } 137 | if (['react', 'react-native'].includes(tsconfig?.compilerOptions?.jsx)) { 138 | jsx = 'transform'; 139 | } 140 | if (['react-jsx', 'react-jsxdev'].includes(tsconfig?.compilerOptions?.jsx)) { 141 | jsx = 'automatic'; 142 | } 143 | if (flags.jsx === 'preserve') { 144 | jsx = 'preserve'; 145 | } 146 | if (flags.jsx === 'transform') { 147 | jsx = 'transform'; 148 | } 149 | if (flags.jsx === 'automatic') { 150 | jsx = 'automatic'; 151 | } 152 | 153 | let jsxDev = false; 154 | if (!flags.jsx && tsconfig?.compilerOptions?.jsx === 'react-jsxdev') { 155 | jsxDev = true; 156 | } 157 | 158 | const jsxFactory: Context['jsxFactory'] = ( 159 | flags.jsxFactory || 160 | tsconfig?.compilerOptions?.jsxFactory || 161 | 'React.createElement' 162 | ); 163 | 164 | const jsxFragment: Context['jsxFactory'] = ( 165 | flags.jsxFragment || 166 | tsconfig?.compilerOptions?.jsxFragmentFactory || 167 | 'Fragment' 168 | ); 169 | 170 | const jsxImportSource: Context['jsxImportSource'] = ( 171 | flags.jsxImportSource || 172 | tsconfig?.compilerOptions?.jsxImportSource || 173 | 'react' 174 | ); 175 | 176 | return { 177 | cwd, 178 | verbose, 179 | module, 180 | platform, 181 | sourcemap, 182 | legalComments, 183 | bundle, 184 | declaration, 185 | jsx, 186 | jsxDev, 187 | jsxFactory, 188 | jsxFragment, 189 | jsxImportSource, 190 | standalone, 191 | rootDir, 192 | outDir, 193 | tsconfigPath, 194 | importMapsPath, 195 | externalDependencies, 196 | forceExternalDependencies, 197 | manifest, 198 | targets, 199 | reporter, 200 | resolvePath, 201 | resolveRelativePath, 202 | }; 203 | }; 204 | -------------------------------------------------------------------------------- /packages/nanobundle/src/entry.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'node:path'; 2 | import dedent from 'string-dedent'; 3 | import kleur from 'kleur'; 4 | 5 | import { type ConditionalExports } from './manifest'; 6 | import { type Context } from './context'; 7 | import { type Reporter } from './reporter'; 8 | import * as formatUtils from './formatUtils'; 9 | import { NanobundleError } from './errors'; 10 | 11 | export type Entry = { 12 | key: string; 13 | entryPath: string; 14 | minify: boolean; 15 | mode: undefined | 'development' | 'production'; 16 | sourcemap: boolean; 17 | platform: 'neutral' | 'browser' | 'deno' | 'node'; 18 | module: 'commonjs' | 'esmodule' | 'css' | 'dts' | 'file'; 19 | sourceFile: string[]; 20 | outputFile: string; 21 | customConditions: string[], 22 | }; 23 | 24 | type EntryTarget = { 25 | key: string, 26 | parentKey?: string, 27 | entryPath: string, 28 | platform: Entry['platform'], 29 | sourcemap: Entry['sourcemap'], 30 | mode: Entry['mode'], 31 | module: Entry['module'], 32 | preferredModule?: 'esmodule' | 'commonjs', 33 | customConditions: string[], 34 | }; 35 | 36 | interface GetEntriesFromContext { 37 | (props: { 38 | context: Context; 39 | reporter: Reporter; 40 | }): Entry[]; 41 | } 42 | export const getEntriesFromContext: GetEntriesFromContext = ({ 43 | context, 44 | reporter, 45 | }) => { 46 | const defaultMinify: Entry['minify'] = false; 47 | const defaultMode: Entry['mode'] = undefined; 48 | const { 49 | cwd, 50 | rootDir, 51 | outDir, 52 | sourcemap, 53 | manifest, 54 | tsconfigPath, 55 | jsx, 56 | platform: defaultPlatform, 57 | module: defaultModule, 58 | } = context; 59 | 60 | const defaultPreferredModule = ({ 61 | commonjs: 'commonjs', 62 | esmodule: 'esmodule', 63 | css: undefined, 64 | dts: undefined, 65 | file: undefined, 66 | } as const)[defaultModule]; 67 | 68 | const resolvedRootDir = context.resolvePath(rootDir); 69 | const resolvedOutDir = context.resolvePath(outDir); 70 | 71 | const useJsx = jsx != null; 72 | const useTsSource = tsconfigPath != null; 73 | const useJsSource = !(useTsSource && resolvedRootDir === resolvedOutDir); 74 | 75 | const preserveJsx = context.jsx === 'preserve'; 76 | 77 | const entryMap = new Map(); 78 | 79 | function addEntry(target: EntryTarget) { 80 | const { 81 | key, 82 | parentKey, 83 | sourcemap, 84 | entryPath, 85 | platform, 86 | module, 87 | mode, 88 | preferredModule, 89 | customConditions, 90 | } = target; 91 | 92 | if (!entryPath.startsWith('./')) { 93 | throw new NanobundleEntryError( 94 | Message.INVALID_PATH_KEY(key), 95 | ); 96 | } 97 | 98 | if (entryPath.includes('*')) { 99 | throw new NanobundleEntryError( 100 | Message.SUBPATH_PATTERN(entryPath), 101 | ); 102 | } 103 | 104 | if (module === 'dts' && !/\.d\.(c|m)?ts$/.test(entryPath)) { 105 | throw new NanobundleEntryError( 106 | Message.INVALID_DTS_FORMAT(), 107 | ); 108 | } 109 | 110 | const entry = entryMap.get(entryPath); 111 | if (entry) { 112 | // exports should be prioritized 113 | if (entry.key.startsWith("exports") && !key.startsWith("exports")) { 114 | if (entry.platform !== platform || entry.module !== module) { 115 | reporter.warn( 116 | Message.PRECEDENSE_ENTRY(entry, target), 117 | ); 118 | } 119 | return; 120 | } 121 | if (entry.platform !== platform || entry.module !== module) { 122 | let hint = ''; 123 | if ( 124 | (entry.key === 'main' && key === 'module') || 125 | (entry.key === 'module' && key === 'main') 126 | ) { 127 | hint = dedent` 128 | Did you forgot to set ${formatUtils.key('type')} to ${formatUtils.literal('module')} for ESM-first approach? 129 | `; 130 | } 131 | if ( 132 | entry.module === module && 133 | entry.platform !== platform 134 | ) { 135 | hint = dedent` 136 | Did you forget to specify the Node.js version in the ${formatUtils.key('engines')} field? 137 | Or you may not need to specify ${formatUtils.key('require')} or ${formatUtils.key('node')} entries. 138 | `; 139 | } 140 | throw new NanobundleEntryError( 141 | Message.CONFLICT_ENTRY(entry, target, hint), 142 | ); 143 | } 144 | return; 145 | } 146 | 147 | const sourceFileCandidates = new Set(); 148 | 149 | const resolvedOutputFile = context.resolvePath(entryPath); 150 | 151 | let resolvedSourceFile = resolvedOutputFile.replace( 152 | resolvedOutDir, 153 | resolvedRootDir, 154 | ); 155 | 156 | const minifyPattern = /\.min(?\.(m|c)?jsx?)$/; 157 | const minifyMatch = resolvedSourceFile.match(minifyPattern); 158 | const minify = defaultMinify || Boolean(minifyMatch); 159 | const ext = minifyMatch?.groups?.ext; 160 | if (ext) { 161 | resolvedSourceFile = resolvedSourceFile.replace(minifyPattern, ext); 162 | } 163 | 164 | if (!/jsx?$/.test(resolvedSourceFile)) { 165 | switch (module) { 166 | case 'commonjs': { 167 | useTsSource && sourceFileCandidates.add(`${resolvedSourceFile}.cts`); 168 | useJsSource && sourceFileCandidates.add(`${resolvedSourceFile}.cjs`); 169 | useJsx && useTsSource && sourceFileCandidates.add(`${resolvedSourceFile}.tsx`); 170 | useTsSource && sourceFileCandidates.add(`${resolvedSourceFile}.ts`); 171 | useJsx && useJsSource && sourceFileCandidates.add(`${resolvedSourceFile}.jsx`); 172 | useJsSource && sourceFileCandidates.add(`${resolvedSourceFile}.js`); 173 | break; 174 | } 175 | case 'esmodule': { 176 | useTsSource && sourceFileCandidates.add(`${resolvedSourceFile}.mts`); 177 | useJsSource && sourceFileCandidates.add(`${resolvedSourceFile}.mjs`); 178 | useJsx && useTsSource && sourceFileCandidates.add(`${resolvedSourceFile}.tsx`); 179 | useTsSource && sourceFileCandidates.add(`${resolvedSourceFile}.ts`); 180 | useJsx && useJsSource && sourceFileCandidates.add(`${resolvedSourceFile}.jsx`); 181 | useJsSource && sourceFileCandidates.add(`${resolvedSourceFile}.js`); 182 | break; 183 | } 184 | } 185 | } 186 | 187 | switch (module) { 188 | case 'commonjs': { 189 | useTsSource && sourceFileCandidates.add(resolvedSourceFile.replace(/\.c?jsx?$/, '.cts')); 190 | useJsSource && sourceFileCandidates.add(resolvedSourceFile.replace(/\.c?jsx?$/, '.cjs')); 191 | useJsx && useTsSource && sourceFileCandidates.add(resolvedSourceFile.replace(/\.c?jsx?$/, '.tsx')); 192 | useTsSource && sourceFileCandidates.add(resolvedSourceFile.replace(/\.c?jsx?$/, '.ts')); 193 | useJsx && useJsSource && sourceFileCandidates.add(resolvedSourceFile.replace(/\.c?jsx?$/, '.jsx')); 194 | useJsSource && sourceFileCandidates.add(resolvedSourceFile.replace(/\.c?jsx?$/, '.js')); 195 | if (parentKey) { 196 | let resolvedSourceFileWithoutCondition = resolvedSourceFile.replace('.' + parentKey, ''); 197 | useTsSource && sourceFileCandidates.add(resolvedSourceFileWithoutCondition.replace(/\.c?jsx?$/, '.cts')); 198 | useJsSource && sourceFileCandidates.add(resolvedSourceFileWithoutCondition.replace(/\.c?jsx?$/, '.cjs')); 199 | useJsx && useTsSource && sourceFileCandidates.add(resolvedSourceFileWithoutCondition.replace(/\.c?jsx?$/, '.tsx')); 200 | useTsSource && sourceFileCandidates.add(resolvedSourceFileWithoutCondition.replace(/\.c?jsx?$/, '.ts')); 201 | useJsx && useJsSource && sourceFileCandidates.add(resolvedSourceFileWithoutCondition.replace(/\.c?jsx?$/, '.jsx')); 202 | useJsSource && sourceFileCandidates.add(resolvedSourceFileWithoutCondition.replace(/\.c?jsx?$/, '.js')); 203 | } 204 | useJsSource && sourceFileCandidates.add(resolvedSourceFile); 205 | break; 206 | } 207 | case 'esmodule': { 208 | useTsSource && sourceFileCandidates.add(resolvedSourceFile.replace(/\.m?jsx?$/, '.mts')); 209 | useJsSource && sourceFileCandidates.add(resolvedSourceFile.replace(/\.m?jsx?$/, '.mjs')); 210 | useJsx && useTsSource && sourceFileCandidates.add(resolvedSourceFile.replace(/\.m?jsx?$/, '.tsx')); 211 | useTsSource && sourceFileCandidates.add(resolvedSourceFile.replace(/\.m?jsx?$/, '.ts')); 212 | useJsx && useJsSource && sourceFileCandidates.add(resolvedSourceFile.replace(/\.m?jsx?$/, '.jsx')); 213 | useJsSource && sourceFileCandidates.add(resolvedSourceFile.replace(/\.m?jsx?$/, '.js')); 214 | if (parentKey) { 215 | let resolvedSourceFileWithoutCondition = resolvedSourceFile.replace('.' + parentKey, ''); 216 | useTsSource && sourceFileCandidates.add(resolvedSourceFileWithoutCondition.replace(/\.m?jsx?$/, '.mts')); 217 | useJsSource && sourceFileCandidates.add(resolvedSourceFileWithoutCondition.replace(/\.m?jsx?$/, '.mjs')); 218 | useJsx && useTsSource && sourceFileCandidates.add(resolvedSourceFileWithoutCondition.replace(/\.m?jsx?$/, '.tsx')); 219 | useTsSource && sourceFileCandidates.add(resolvedSourceFileWithoutCondition.replace(/\.m?jsx?$/, '.ts')); 220 | useJsx && useJsSource && sourceFileCandidates.add(resolvedSourceFileWithoutCondition.replace(/\.m?jsx?$/, '.jsx')); 221 | useJsSource && sourceFileCandidates.add(resolvedSourceFileWithoutCondition.replace(/\.m?jsx?$/, '.js')); 222 | } 223 | useJsSource && sourceFileCandidates.add(resolvedSourceFile); 224 | break; 225 | } 226 | case 'css': { 227 | sourceFileCandidates.add(resolvedSourceFile); 228 | break; 229 | } 230 | case 'dts': { 231 | const explicitMatcher = /\.d\.(c|m)ts$/; 232 | let implicitMatcher; 233 | let implicitReplacement = '.ts'; 234 | if (!useTsSource) break; 235 | if (preferredModule === 'commonjs') { 236 | implicitMatcher = /\.d\.(c?)ts$/; 237 | implicitReplacement = '.cts'; 238 | } else if (preferredModule === 'esmodule') { 239 | implicitMatcher = /\.d\.(m?)ts$/; 240 | implicitReplacement = '.mts'; 241 | } 242 | if (implicitMatcher?.test(resolvedSourceFile)) { 243 | sourceFileCandidates.add(resolvedSourceFile.replace(implicitMatcher, implicitReplacement)); 244 | } else if (explicitMatcher.test(resolvedSourceFile)) { 245 | sourceFileCandidates.add(resolvedSourceFile.replace(explicitMatcher, '.$1ts')); 246 | } 247 | useJsx && sourceFileCandidates.add(resolvedSourceFile.replace(/\.d\.(m|c)?ts$/, '.tsx')); 248 | sourceFileCandidates.add(resolvedSourceFile.replace(/\.d\.(m|c)?ts$/, '.ts')); 249 | break; 250 | } 251 | case 'file': { 252 | if (path.relative(cwd, path.dirname(resolvedOutputFile))) { 253 | sourceFileCandidates.add(resolvedSourceFile); 254 | } else { 255 | sourceFileCandidates.add(resolvedOutputFile); 256 | } 257 | break; 258 | } 259 | } 260 | 261 | const sourceFile = [...sourceFileCandidates]; 262 | if (useJsx) { 263 | sourceFile.sort((a, b) => { 264 | if (a.endsWith('x') && b.endsWith('x')) { 265 | return 0; 266 | } else if (a.endsWith('x')) { 267 | return -1; 268 | } else { 269 | return 1; 270 | } 271 | }); 272 | } 273 | 274 | entryMap.set(entryPath, { 275 | key, 276 | entryPath, 277 | mode, 278 | minify, 279 | sourcemap, 280 | platform, 281 | module, 282 | sourceFile, 283 | outputFile: resolvedOutputFile, 284 | customConditions, 285 | }); 286 | } 287 | 288 | function addMainEntry({ 289 | key, 290 | entryPath, 291 | }: { 292 | key: string; 293 | entryPath: string; 294 | }) { 295 | const ext = path.extname(entryPath); 296 | switch (ext) { 297 | case '.cjs': { 298 | addEntry({ 299 | key, 300 | sourcemap, 301 | platform: defaultPlatform, 302 | mode: defaultMode, 303 | module: 'commonjs', 304 | preferredModule: 'commonjs', 305 | entryPath, 306 | customConditions: [], 307 | }); 308 | break; 309 | } 310 | case '.mjs': { 311 | addEntry({ 312 | key, 313 | sourcemap, 314 | platform: defaultPlatform, 315 | mode: defaultMode, 316 | module: 'esmodule', 317 | preferredModule: 'esmodule', 318 | entryPath, 319 | customConditions: [], 320 | }); 321 | break; 322 | } 323 | case '.node': { 324 | addEntry({ 325 | key, 326 | sourcemap, 327 | platform: 'node', 328 | mode: defaultMode, 329 | module: 'file', 330 | entryPath, 331 | customConditions: [], 332 | }); 333 | break; 334 | } 335 | case '.json': { 336 | addEntry({ 337 | key, 338 | sourcemap, 339 | platform: defaultPlatform, 340 | mode: defaultMode, 341 | module: 'file', 342 | entryPath, 343 | customConditions: [], 344 | }); 345 | break; 346 | } 347 | case '.jsx': { 348 | if (!preserveJsx) { 349 | reporter.warn(Message.NO_NEED_JSX(entryPath)); 350 | } 351 | } 352 | default: { 353 | addEntry({ 354 | key, 355 | sourcemap, 356 | platform: defaultPlatform, 357 | mode: defaultMode, 358 | module: defaultModule, 359 | entryPath, 360 | customConditions: [], 361 | }); 362 | break; 363 | } 364 | } 365 | } 366 | 367 | function addModuleEntry({ 368 | key, 369 | entryPath, 370 | }: { 371 | key: string; 372 | entryPath: string; 373 | }) { 374 | if (/\.m?jsx?$/.test(entryPath)) { 375 | addEntry({ 376 | key, 377 | sourcemap, 378 | platform: defaultPlatform, 379 | mode: defaultMode, 380 | module: 'esmodule', 381 | preferredModule: 'esmodule', 382 | entryPath, 383 | customConditions: [], 384 | }); 385 | } else { 386 | throw new NanobundleEntryError(Message.INVALID_MODULE_EXTENSION()); 387 | } 388 | } 389 | 390 | function addTypesEntry({ 391 | key, 392 | entryPath, 393 | }: { 394 | key: string; 395 | entryPath: string; 396 | }) { 397 | if (/\.d\.(m|c)?ts$/.test(entryPath)) { 398 | addEntry({ 399 | key, 400 | sourcemap, 401 | platform: defaultPlatform, 402 | mode: defaultMode, 403 | module: 'dts', 404 | preferredModule: defaultPreferredModule, 405 | entryPath, 406 | customConditions: [], 407 | }); 408 | } else { 409 | throw new NanobundleEntryError(Message.INVALID_TYPES_EXTENSION()); 410 | } 411 | } 412 | 413 | function addBinEntry({ 414 | key, 415 | entryPath, 416 | }: { 417 | key: string; 418 | entryPath: string; 419 | }) { 420 | const ext = path.extname(entryPath); 421 | switch (ext) { 422 | case '.js': { 423 | addEntry({ 424 | key, 425 | sourcemap: false, 426 | platform: 'node', 427 | mode: defaultMode, 428 | module: defaultModule, 429 | preferredModule: defaultPreferredModule, 430 | entryPath, 431 | customConditions: [], 432 | }); 433 | break; 434 | } 435 | case '.cjs': { 436 | addEntry({ 437 | key, 438 | sourcemap: false, 439 | platform: 'node', 440 | mode: defaultMode, 441 | module: 'commonjs', 442 | preferredModule: defaultPreferredModule, 443 | entryPath, 444 | customConditions: [], 445 | }); 446 | break; 447 | } 448 | case '.mjs': { 449 | addEntry({ 450 | key, 451 | sourcemap: false, 452 | platform: 'node', 453 | mode: defaultMode, 454 | module: 'esmodule', 455 | preferredModule: defaultPreferredModule, 456 | entryPath, 457 | customConditions: [], 458 | }); 459 | break; 460 | } 461 | default: { 462 | throw new NanobundleEntryError(Message.INVALID_BIN_EXTENSION()); 463 | } 464 | } 465 | } 466 | 467 | function addConditionalEntry({ 468 | key, 469 | parentKey, 470 | platform, 471 | mode, 472 | module, 473 | preferredModule, 474 | entryPath, 475 | customConditions, 476 | }: { 477 | key: string, 478 | parentKey: string, 479 | platform: Entry['platform'], 480 | mode: Entry['mode'], 481 | module: Entry['module'], 482 | preferredModule?: 'commonjs' | 'esmodule', 483 | entryPath: ConditionalExports, 484 | customConditions: string[], 485 | }) { 486 | if (typeof entryPath === 'string') { 487 | if (parentKey === 'types') { 488 | addEntry({ 489 | key, 490 | parentKey, 491 | sourcemap, 492 | platform, 493 | mode, 494 | module: 'dts', 495 | preferredModule, 496 | entryPath, 497 | customConditions, 498 | }); 499 | return; 500 | } 501 | 502 | const ext = path.extname(entryPath); 503 | switch (ext) { 504 | case '.cjs': { 505 | addEntry({ 506 | key, 507 | parentKey, 508 | sourcemap, 509 | platform, 510 | mode, 511 | module: 'commonjs', 512 | preferredModule: 'commonjs', 513 | entryPath, 514 | customConditions, 515 | }); 516 | break; 517 | } 518 | case '.mjs': { 519 | addEntry({ 520 | key, 521 | parentKey, 522 | sourcemap, 523 | platform, 524 | mode, 525 | module: 'esmodule', 526 | preferredModule: 'esmodule', 527 | entryPath, 528 | customConditions, 529 | }); 530 | break; 531 | } 532 | case '.node': { 533 | addEntry({ 534 | key, 535 | parentKey, 536 | sourcemap, 537 | platform: 'node', 538 | mode, 539 | module: 'file', 540 | preferredModule, 541 | entryPath, 542 | customConditions, 543 | }); 544 | break; 545 | } 546 | case '.json': { 547 | addEntry({ 548 | key, 549 | parentKey, 550 | sourcemap, 551 | platform, 552 | mode, 553 | module: 'file', 554 | preferredModule, 555 | entryPath, 556 | customConditions, 557 | }); 558 | break; 559 | } 560 | case '.css': { 561 | addEntry({ 562 | key, 563 | parentKey, 564 | sourcemap, 565 | platform, 566 | mode, 567 | module: 'css', 568 | preferredModule, 569 | entryPath, 570 | customConditions, 571 | }); 572 | break; 573 | } 574 | case '.jsx': { 575 | if (!preserveJsx) { 576 | reporter.warn(Message.NO_NEED_JSX(entryPath)); 577 | } 578 | } 579 | default: { 580 | addEntry({ 581 | key, 582 | parentKey, 583 | sourcemap, 584 | platform, 585 | mode, 586 | module, 587 | preferredModule, 588 | entryPath, 589 | customConditions, 590 | }); 591 | break; 592 | } 593 | } 594 | } else if (typeof entryPath === 'object') { 595 | if (parentKey === 'types') { 596 | throw new NanobundleEntryError(Message.INVALID_DTS_FORMAT()); 597 | } 598 | 599 | let entries = Object.entries(entryPath); 600 | 601 | if (typeof entryPath.types !== 'undefined') { 602 | const typesEntryIndex = entries.findIndex(entry => entry[0] === 'types'); 603 | if (typesEntryIndex !== 0) { 604 | throw new NanobundleEntryError(Message.INVALID_DTS_ORDER()); 605 | } 606 | } else { 607 | const firstLeaf = entries.find(([entryKey, entry]) => { 608 | return typeof entry === 'string' && !entryKey.startsWith('.'); 609 | }); 610 | const isLeaf = firstLeaf !== undefined; 611 | 612 | // has leaf default entry 613 | if (useTsSource && isLeaf) { 614 | if (typeof entryPath.default === 'string') { 615 | const dtsExport: [string, ConditionalExports] = [ 616 | 'types$implicit', 617 | inferDtsEntry(entryPath.default), 618 | ]; 619 | entries = [dtsExport, ...entries]; 620 | } else if (typeof entryPath.require === 'string' && typeof entryPath.import === 'string') { 621 | throw new NanobundleEntryError( 622 | Message.UNDETEMINED_DTS_SOURCE(key, entryPath.require, entryPath.import), 623 | ); 624 | } else if (typeof entryPath.require === 'string') { 625 | const dtsExport: [string, ConditionalExports] = [ 626 | 'types$implicit', 627 | inferDtsEntry(entryPath.require), 628 | ]; 629 | entries = [dtsExport, ...entries]; 630 | } else if (typeof entryPath.import === 'string') { 631 | const dtsExport: [string, ConditionalExports] = [ 632 | 'types$implicit', 633 | inferDtsEntry(entryPath.import), 634 | ]; 635 | entries = [dtsExport, ...entries]; 636 | } else if (preferredModule) { 637 | const dtsExport: [string, ConditionalExports] = [ 638 | 'types$implicit', 639 | inferDtsEntry(firstLeaf[1] as string), 640 | ]; 641 | entries = [dtsExport, ...entries]; 642 | } else { 643 | reporter.warn(Message.TYPES_MAY_NOT_BE_RESOLVED(key)); 644 | } 645 | } 646 | } 647 | 648 | for (const [currentKey, output] of entries) { 649 | // See https://nodejs.org/api/packages.html#packages_community_conditions_definitions 650 | switch (currentKey) { 651 | case 'import': { 652 | addConditionalEntry({ 653 | key: `${key}.${currentKey}`, 654 | parentKey: currentKey, 655 | platform, 656 | mode, 657 | module: 'esmodule', 658 | preferredModule: 'esmodule', 659 | entryPath: output, 660 | customConditions, 661 | }); 662 | break; 663 | } 664 | case 'require': { 665 | addConditionalEntry({ 666 | key: `${key}.${currentKey}`, 667 | parentKey: currentKey, 668 | platform, 669 | mode, 670 | module: 'commonjs', 671 | preferredModule: 'commonjs', 672 | entryPath: output, 673 | customConditions, 674 | }); 675 | break; 676 | } 677 | case 'types': { 678 | addConditionalEntry({ 679 | key: `${key}.${currentKey}`, 680 | parentKey: currentKey, 681 | platform, 682 | mode, 683 | module: 'dts', 684 | preferredModule: undefined, 685 | entryPath: output, 686 | customConditions, 687 | }); 688 | break; 689 | } 690 | case 'types$implicit': { 691 | addConditionalEntry({ 692 | key: `${key}.types`, 693 | parentKey: currentKey, 694 | platform, 695 | mode, 696 | module: 'dts', 697 | preferredModule, 698 | entryPath: output, 699 | customConditions, 700 | }); 701 | break; 702 | } 703 | case 'node': { 704 | addConditionalEntry({ 705 | key: `${key}.${currentKey}`, 706 | parentKey: currentKey, 707 | platform: 'node', 708 | mode, 709 | module, 710 | preferredModule, 711 | entryPath: output, 712 | customConditions, 713 | }); 714 | break; 715 | } 716 | case 'deno': { 717 | addConditionalEntry({ 718 | key: `${key}.${currentKey}`, 719 | parentKey: currentKey, 720 | platform: 'deno', 721 | mode, 722 | module, 723 | preferredModule, 724 | entryPath: output, 725 | customConditions, 726 | }); 727 | break; 728 | } 729 | case 'browser': { 730 | addConditionalEntry({ 731 | key: `${key}.${currentKey}`, 732 | parentKey: currentKey, 733 | platform: 'browser', 734 | mode, 735 | module, 736 | preferredModule, 737 | entryPath: output, 738 | customConditions, 739 | }); 740 | break; 741 | } 742 | case 'development': { 743 | addConditionalEntry({ 744 | key: `${key}.${currentKey}`, 745 | parentKey: currentKey, 746 | platform, 747 | mode: 'development', 748 | module, 749 | preferredModule, 750 | entryPath: output, 751 | customConditions, 752 | }); 753 | break; 754 | } 755 | case 'production': { 756 | addConditionalEntry({ 757 | key: `${key}.${currentKey}`, 758 | parentKey: currentKey, 759 | platform, 760 | mode: 'production', 761 | module, 762 | preferredModule, 763 | entryPath: output, 764 | customConditions, 765 | }); 766 | break; 767 | } 768 | case 'default': { 769 | addConditionalEntry({ 770 | key: `${key}.${currentKey}`, 771 | parentKey: currentKey, 772 | platform, 773 | mode, 774 | module, 775 | preferredModule, 776 | entryPath: output, 777 | customConditions, 778 | }); 779 | break; 780 | } 781 | case '.': { 782 | addConditionalEntry({ 783 | key: `${key}[\"${currentKey}\"]`, 784 | parentKey: currentKey, 785 | platform, 786 | mode, 787 | module, 788 | preferredModule, 789 | entryPath: output, 790 | customConditions, 791 | }); 792 | break; 793 | } 794 | default: { 795 | if (currentKey.startsWith('./')) { 796 | addConditionalEntry({ 797 | key: `${key}[\"${currentKey}\"]`, 798 | parentKey: currentKey, 799 | platform, 800 | mode, 801 | module, 802 | preferredModule, 803 | entryPath: output, 804 | customConditions, 805 | }); 806 | } else { 807 | reporter.warn(Message.CUSTOM_CONDITION(currentKey)); 808 | addConditionalEntry({ 809 | key: `${key}.${currentKey}`, 810 | parentKey: currentKey, 811 | platform, 812 | mode, 813 | module, 814 | preferredModule, 815 | entryPath: output, 816 | customConditions: [...new Set([...customConditions, currentKey])], 817 | }); 818 | } 819 | break; 820 | } 821 | } 822 | } 823 | } 824 | } 825 | 826 | if (manifest.exports) { 827 | addConditionalEntry({ 828 | key: 'exports', 829 | parentKey: 'exports', 830 | platform: defaultPlatform, 831 | mode: defaultMode, 832 | module: defaultModule, 833 | preferredModule: defaultPreferredModule, 834 | entryPath: manifest.exports, 835 | customConditions: [], 836 | }); 837 | } else if (manifest.main || manifest.module) { 838 | reporter.warn(Message.RECOMMEND_EXPORTS()); 839 | } 840 | 841 | if (typeof manifest.main === 'string') { 842 | addMainEntry({ 843 | key: 'main', 844 | entryPath: manifest.main, 845 | }); 846 | } 847 | 848 | if (typeof manifest.module === 'string') { 849 | addModuleEntry({ 850 | key: 'module', 851 | entryPath: manifest.module, 852 | }); 853 | reporter.warn(Message.MODULE_NOT_RECOMMENDED()); 854 | } 855 | 856 | if (typeof manifest.types === 'string') { 857 | addTypesEntry({ 858 | key: 'types', 859 | entryPath: manifest.types, 860 | }); 861 | } 862 | 863 | if (typeof manifest.bin === 'string') { 864 | addBinEntry({ 865 | key: 'bin', 866 | entryPath: manifest.bin, 867 | }); 868 | } 869 | 870 | if (typeof manifest.bin === 'object') { 871 | for (const [commandName, entryPath] of Object.entries(manifest.bin)) { 872 | addBinEntry({ 873 | key: `bin["${commandName}"]`, 874 | entryPath, 875 | }); 876 | } 877 | } 878 | 879 | const entries = [...entryMap.values()]; 880 | return entries; 881 | }; 882 | 883 | function inferDtsEntry(entryPath: string): string { 884 | return entryPath.replace(/(\.min)?\.(m|c)?jsx?$/, '.d.$2ts'); 885 | } 886 | 887 | export class NanobundleEntryError extends NanobundleError { 888 | } 889 | 890 | export const Message = { 891 | INVALID_MAIN_EXTENSION: () => dedent` 892 | Only ${formatUtils.path('.js')}, ${formatUtils.path('.cjs')}, ${formatUtils.path('.mjs')}, ${formatUtils.path('.json')}, or ${formatUtils.path('.node')} allowed for ${formatUtils.key('main')} entry. 893 | 894 | `, 895 | 896 | INVALID_MODULE_EXTENSION: () => dedent` 897 | Only ${formatUtils.path('.js')} or ${formatUtils.path('.mjs')} allowed for ${formatUtils.key('module')} entry. 898 | 899 | `, 900 | 901 | INVALID_TYPES_EXTENSION: () => dedent` 902 | Only ${formatUtils.path('.d.ts')} or ${formatUtils.path('.d.cts')} or ${formatUtils.path('.d.mts')} allowed for ${formatUtils.key('types')} entry. 903 | 904 | `, 905 | 906 | INVALID_BIN_EXTENSION: () => dedent` 907 | Only JavaScript files are allowed for ${formatUtils.path('bin')} entry. 908 | 909 | `, 910 | 911 | INVALID_PATH_KEY: (path: string) => dedent` 912 | Invalid entry path ${formatUtils.path(path)}, entry path should starts with ${formatUtils.literal('./')}. 913 | 914 | `, 915 | 916 | INVALID_DTS_FORMAT: () => dedent` 917 | ${formatUtils.key('types')} entry must be .d.ts file and cannot be nested! 918 | 919 | `, 920 | 921 | INVALID_DTS_ORDER: () => dedent` 922 | ${formatUtils.key('types')} entry must occur first in conditional exports for correct type resolution. 923 | 924 | `, 925 | 926 | UNDETEMINED_DTS_SOURCE: (key: string, requirePath: string, importPath: string) => dedent` 927 | ${formatUtils.key('types')} entry doesn't set properly for ${formatUtils.key(key)}: 928 | 929 | "require": "${requirePath}", 930 | "import": "${importPath}" 931 | 932 | Solution 1. Explicitly set ${formatUtils.key('types')} entry 933 | 934 | For example like this 935 | 936 | + "types": "${requirePath.replace(/\.(m|c)?js$/, '.d.ts')}", 937 | "require": "${requirePath}", 938 | "import": "${importPath}" 939 | 940 | Or like this 941 | 942 | "require": { 943 | + "types": "${requirePath.replace(/\.(m|c)?js$/, '.d.$1ts')}", 944 | "default": "${requirePath}" 945 | }, 946 | "import": { 947 | + "types": "${importPath.replace(/\.(m|c)?js$/, '.d.$1ts')}", 948 | "default": "${importPath}" 949 | } 950 | 951 | Solution 2. Add ${formatUtils.key('default')} entry 952 | 953 | "require": "${requirePath}", 954 | "import": "${importPath}", 955 | + "default": "/path/to/entry.js" 956 | 957 | `, 958 | 959 | SUBPATH_PATTERN: (path: string) => dedent` 960 | Subpath pattern (${formatUtils.path(path)}) is not supported yet. 961 | 962 | `, 963 | 964 | CONFLICT_ENTRY: (a: EntryTarget, b: EntryTarget, hint: string) => formatUtils.format( 965 | dedent` 966 | Conflict found for ${formatUtils.path(a.entryPath)} 967 | 968 | %s 969 | %s 970 | 971 | vs 972 | 973 | %s ${kleur.bold('(conflited)')} 974 | %s 975 | 976 | `, 977 | formatUtils.key(a.key), 978 | formatUtils.object({ module: a.module, platform: a.platform }), 979 | formatUtils.key(b.key), 980 | formatUtils.object({ module: b.module, platform: b.platform }), 981 | ) + hint ? `Hint: ${hint}\n\n` : '', 982 | 983 | PRECEDENSE_ENTRY: (a: EntryTarget, b: EntryTarget) => formatUtils.format( 984 | dedent` 985 | Entry ${formatUtils.key(b.key)} will be ignored since 986 | 987 | %s 988 | %s 989 | 990 | precedense over 991 | 992 | %s ${kleur.bold('(ignored)')} 993 | %s 994 | 995 | `, 996 | formatUtils.key(a.key), 997 | formatUtils.object({ module: a.module, platform: a.platform }), 998 | formatUtils.key(b.key), 999 | formatUtils.object({ module: b.module, platform: b.platform }), 1000 | ), 1001 | 1002 | RECOMMEND_EXPORTS: () => dedent` 1003 | Using ${formatUtils.key('exports')} field is highly recommended. 1004 | 1005 | See ${formatUtils.hyperlink('https://nodejs.org/api/packages.html')} for more detail. 1006 | 1007 | `, 1008 | 1009 | MODULE_NOT_RECOMMENDED: () => dedent` 1010 | ${formatUtils.key('module')} field is not standard and may works in only legacy bundlers. Consider using ${formatUtils.key('exports')} instead. 1011 | See ${formatUtils.hyperlink('https://nodejs.org/api/packages.html')} for more detail. 1012 | 1013 | `, 1014 | 1015 | TYPES_MAY_NOT_BE_RESOLVED: (key: string) => dedent` 1016 | ${formatUtils.key(key)} entry might not be resolved correctly in ${formatUtils.key('moduleResolution')}: ${formatUtils.literal('Node16')}. 1017 | 1018 | Consider to specify ${formatUtils.key('types')} entry for it. 1019 | 1020 | `, 1021 | 1022 | NO_NEED_JSX: (path: string) => dedent` 1023 | ${formatUtils.path(path)} doesn't have to be \`.jsx\` unless you are using ${formatUtils.key('preserve')} mode. 1024 | `, 1025 | 1026 | CUSTOM_CONDITION: (condition: string) => dedent` 1027 | Custom condition ${formatUtils.key(condition)} may has no effects. 1028 | `, 1029 | 1030 | } as const; 1031 | -------------------------------------------------------------------------------- /packages/nanobundle/src/entryGroup.test.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from 'vitest'; 2 | 3 | import type { BundleEntry, BundleEntryGroup } from './entryGroup'; 4 | import { groupBundleEntries, hashBundleOptions } from './entryGroup'; 5 | 6 | test('groupEntries should split different option set', () => { 7 | const entries: BundleEntry[] = [ 8 | { 9 | key: 'exports["."].node.require', 10 | module: "commonjs", 11 | mode: undefined, 12 | minify: false, 13 | sourcemap: true, 14 | platform: "node", 15 | entryPath: "./lib/index.cjs", 16 | sourceFile: ["/project/src/index.cjs", "/project/src/index.js"], 17 | outputFile: "/project/lib/index.cjs", 18 | customConditions: [], 19 | }, 20 | { 21 | key: 'exports["."].node.import.development', 22 | module: "esmodule", 23 | mode: "development", 24 | minify: false, 25 | sourcemap: true, 26 | platform: "node", 27 | entryPath: "./lib/index.mjs", 28 | sourceFile: ["/project/src/index.mjs", "/project/src/index.js"], 29 | outputFile: "/project/lib/index.mjs", 30 | customConditions: [], 31 | }, 32 | { 33 | key: 'exports["."].node.import.production', 34 | module: "esmodule", 35 | mode: "production", 36 | minify: true, 37 | sourcemap: true, 38 | platform: "node", 39 | entryPath: "./lib/index.min.mjs", 40 | sourceFile: ["/project/src/index.mjs", "/project/src/index.js"], 41 | outputFile: "/project/lib/index.min.mjs", 42 | customConditions: [], 43 | }, 44 | { 45 | key: 'exports["."].browser.development', 46 | module: "commonjs", 47 | mode: "development", 48 | minify: false, 49 | sourcemap: true, 50 | platform: "browser", 51 | entryPath: "./lib/browser.js", 52 | sourceFile: ["/project/src/browser.cjs", "/project/src/browser.js"], 53 | outputFile: "/project/lib/browser.js", 54 | customConditions: [], 55 | }, 56 | { 57 | key: 'exports["."].browser.production', 58 | module: "commonjs", 59 | mode: "production", 60 | minify: true, 61 | sourcemap: true, 62 | platform: "browser", 63 | entryPath: "./lib/browser.min.js", 64 | sourceFile: ["/project/src/browser.cjs", "/project/src/browser.js"], 65 | outputFile: "/project/lib/browser.min.js", 66 | customConditions: [], 67 | }, 68 | ]; 69 | 70 | expect(groupBundleEntries(entries)).toEqual({ 71 | [hashBundleOptions({ mode: undefined, module: 'commonjs', platform: 'node', sourcemap: true, minify: false, customConditions: [] })]: [ 72 | { 73 | key: 'exports["."].node.require', 74 | module: "commonjs", 75 | mode: undefined, 76 | minify: false, 77 | sourcemap: true, 78 | platform: "node", 79 | entryPath: "./lib/index.cjs", 80 | sourceFile: ["/project/src/index.cjs", "/project/src/index.js"], 81 | outputFile: "/project/lib/index.cjs", 82 | customConditions: [], 83 | }, 84 | ], 85 | [hashBundleOptions({ mode: 'development', module: 'esmodule', platform: 'node', sourcemap: true, minify: false, customConditions: [] })]: [ 86 | { 87 | key: 'exports["."].node.import.development', 88 | module: "esmodule", 89 | mode: "development", 90 | minify: false, 91 | sourcemap: true, 92 | platform: "node", 93 | entryPath: "./lib/index.mjs", 94 | sourceFile: ["/project/src/index.mjs", "/project/src/index.js"], 95 | outputFile: "/project/lib/index.mjs", 96 | customConditions: [], 97 | }, 98 | ], 99 | [hashBundleOptions({ mode: 'production', module: 'esmodule', platform: 'node', sourcemap: true, minify: true, customConditions: [] })]: [ 100 | { 101 | key: 'exports["."].node.import.production', 102 | module: "esmodule", 103 | mode: "production", 104 | minify: true, 105 | sourcemap: true, 106 | platform: "node", 107 | entryPath: "./lib/index.min.mjs", 108 | sourceFile: ["/project/src/index.mjs", "/project/src/index.js"], 109 | outputFile: "/project/lib/index.min.mjs", 110 | customConditions: [], 111 | }, 112 | ], 113 | [hashBundleOptions({ mode: 'development', module: 'commonjs', platform: 'browser', sourcemap: true, minify: false, customConditions: [] })]: [ 114 | { 115 | key: 'exports["."].browser.development', 116 | module: "commonjs", 117 | mode: "development", 118 | minify: false, 119 | sourcemap: true, 120 | platform: "browser", 121 | entryPath: "./lib/browser.js", 122 | sourceFile: ["/project/src/browser.cjs", "/project/src/browser.js"], 123 | outputFile: "/project/lib/browser.js", 124 | customConditions: [], 125 | }, 126 | ], 127 | [hashBundleOptions({ mode: 'production', module: 'commonjs', platform: 'browser', sourcemap: true, minify: true, customConditions: [] })]: [ 128 | { 129 | key: 'exports["."].browser.production', 130 | module: "commonjs", 131 | mode: "production", 132 | minify: true, 133 | sourcemap: true, 134 | platform: "browser", 135 | entryPath: "./lib/browser.min.js", 136 | sourceFile: ["/project/src/browser.cjs", "/project/src/browser.js"], 137 | outputFile: "/project/lib/browser.min.js", 138 | customConditions: [], 139 | }, 140 | ], 141 | }) 142 | }); 143 | 144 | test('groupEntries should merge same option set', () => { 145 | const entries: BundleEntry[] = [ 146 | { 147 | key: 'exports["."].require', 148 | module: "commonjs", 149 | mode: undefined, 150 | minify: false, 151 | sourcemap: true, 152 | platform: "neutral", 153 | entryPath: "./lib/index.cjs", 154 | sourceFile: ["/project/src/index.cjs", "/project/src/index.js"], 155 | outputFile: "/project/lib/index.cjs", 156 | customConditions: [], 157 | }, 158 | { 159 | key: 'exports["."].default.require', 160 | module: "commonjs", 161 | mode: undefined, 162 | minify: false, 163 | sourcemap: true, 164 | platform: "neutral", 165 | entryPath: "./lib/index.cjs", 166 | sourceFile: ["/project/src/index.cjs", "/project/src/index.js"], 167 | outputFile: "/project/lib/index.cjs", 168 | customConditions: [], 169 | }, 170 | { 171 | key: 'exports["."].default.default.require', 172 | module: "commonjs", 173 | mode: undefined, 174 | minify: false, 175 | sourcemap: true, 176 | platform: "neutral", 177 | entryPath: "./lib/index.cjs", 178 | sourceFile: ["/project/src/index.cjs", "/project/src/index.js"], 179 | outputFile: "/project/lib/index.cjs", 180 | customConditions: [], 181 | }, 182 | ]; 183 | 184 | expect(groupBundleEntries(entries)).toEqual({ 185 | [hashBundleOptions({ mode: undefined, module: 'commonjs', platform: 'neutral', sourcemap: true, minify: false, customConditions: [] })]: [ 186 | { 187 | key: 'exports["."].require', 188 | module: "commonjs", 189 | mode: undefined, 190 | minify: false, 191 | sourcemap: true, 192 | platform: "neutral", 193 | entryPath: "./lib/index.cjs", 194 | sourceFile: ["/project/src/index.cjs", "/project/src/index.js"], 195 | outputFile: "/project/lib/index.cjs", 196 | customConditions: [], 197 | }, 198 | { 199 | key: 'exports["."].default.require', 200 | module: "commonjs", 201 | mode: undefined, 202 | minify: false, 203 | sourcemap: true, 204 | platform: "neutral", 205 | entryPath: "./lib/index.cjs", 206 | sourceFile: ["/project/src/index.cjs", "/project/src/index.js"], 207 | outputFile: "/project/lib/index.cjs", 208 | customConditions: [], 209 | }, 210 | { 211 | key: 'exports["."].default.default.require', 212 | module: "commonjs", 213 | mode: undefined, 214 | minify: false, 215 | sourcemap: true, 216 | platform: "neutral", 217 | entryPath: "./lib/index.cjs", 218 | sourceFile: ["/project/src/index.cjs", "/project/src/index.js"], 219 | outputFile: "/project/lib/index.cjs", 220 | customConditions: [], 221 | }, 222 | ], 223 | }); 224 | }); -------------------------------------------------------------------------------- /packages/nanobundle/src/entryGroup.ts: -------------------------------------------------------------------------------- 1 | import { type OverrideProps } from '@cometjs/core'; 2 | 3 | import { type Entry } from './entry'; 4 | 5 | export type BundleEntry = OverrideProps; 8 | export function filterBundleEntry(entry: Entry): entry is BundleEntry { 9 | return entry.module === 'esmodule' || entry.module === 'commonjs' || entry.module === 'css'; 10 | } 11 | 12 | export type TypeEntry = OverrideProps; 15 | export function filterTypeEntry(entry: Entry): entry is TypeEntry { 16 | return entry.module === 'dts'; 17 | } 18 | 19 | export type FileEntry = OverrideProps; 22 | export function filterFileEntry(entry: Entry): entry is FileEntry { 23 | return entry.module === 'file'; 24 | } 25 | 26 | export type BundleOptions = { 27 | mode: BundleEntry['mode'], 28 | module: BundleEntry['module'], 29 | minify: BundleEntry['minify'], 30 | platform: BundleEntry['platform'], 31 | sourcemap: BundleEntry['sourcemap'], 32 | customConditions: BundleEntry['customConditions'], 33 | }; 34 | 35 | export type BundleEntryGroup = Record< 36 | ReturnType, 37 | BundleEntry[] 38 | >; 39 | 40 | export function hashBundleOptions(options: BundleOptions): string { 41 | const normalized: BundleOptions = { 42 | mode: options.mode, 43 | module: options.module, 44 | minify: options.minify, 45 | platform: options.platform, 46 | sourcemap: options.sourcemap, 47 | customConditions: [...options.customConditions].sort(), 48 | }; 49 | return JSON.stringify(normalized); 50 | } 51 | 52 | export function optionsFromHash(hash: string): BundleOptions { 53 | return JSON.parse(hash); 54 | } 55 | 56 | export function extractBundleOptions(entry: BundleEntry): BundleOptions { 57 | const options: BundleOptions = { 58 | mode: entry.mode, 59 | module: entry.module, 60 | minify: entry.minify, 61 | platform: entry.platform, 62 | sourcemap: entry.sourcemap, 63 | customConditions: entry.customConditions, 64 | }; 65 | return options; 66 | } 67 | 68 | export function groupBundleEntries(entries: BundleEntry[]): BundleEntryGroup { 69 | const group: BundleEntryGroup = {}; 70 | 71 | for (const entry of entries) { 72 | const options = extractBundleOptions(entry); 73 | const optionsHash = hashBundleOptions(options); 74 | if (group[optionsHash]) { 75 | group[optionsHash].push(entry); 76 | } else { 77 | group[optionsHash] = [entry]; 78 | } 79 | } 80 | 81 | return group; 82 | } 83 | -------------------------------------------------------------------------------- /packages/nanobundle/src/errors.ts: -------------------------------------------------------------------------------- 1 | import dedent from 'string-dedent'; 2 | 3 | import * as formatUtils from './formatUtils'; 4 | 5 | export class NanobundleError extends Error { 6 | } 7 | 8 | export class NanobundleConfigError extends NanobundleError { 9 | 10 | } 11 | 12 | -------------------------------------------------------------------------------- /packages/nanobundle/src/formatUtils.ts: -------------------------------------------------------------------------------- 1 | import { formatWithOptions } from 'node:util'; 2 | import kleur from 'kleur'; 3 | 4 | import { Entry } from './entry'; 5 | 6 | let { FORCE_COLOR, NODE_DISABLE_COLORS, NO_COLOR, TERM } = process.env; 7 | let isTTY = process.stdout.isTTY; 8 | 9 | export const colorEnabled = !NODE_DISABLE_COLORS && NO_COLOR == null && TERM !== 'dumb' && ( 10 | FORCE_COLOR != null && FORCE_COLOR !== '0' || isTTY 11 | ); 12 | 13 | export function indent(msg: string, level: number): string { 14 | const tab = ' '; 15 | const padding = tab.repeat(level); 16 | return msg 17 | .split('\n') 18 | .map(msg => `${padding}${msg}`) 19 | .join('\n'); 20 | } 21 | 22 | export function format(msg: string, ...args: any[]): string { 23 | return formatWithOptions({ colors: colorEnabled }, msg, ...args); 24 | } 25 | 26 | export function module(module: Entry['module']): string { 27 | return { 28 | esmodule: 'ESM', 29 | commonjs: 'CommonJS', 30 | file: 'File', 31 | dts: 'TypeScript declaration', 32 | css: 'CSS', 33 | }[module]; 34 | } 35 | 36 | export function platform(platform: 'web' | 'node'): string { 37 | return { 38 | web: 'Web', 39 | node: 'Node.js', 40 | }[platform]; 41 | } 42 | 43 | export function hyperlink(hyperlink: string): string { 44 | return kleur.underline().cyan(hyperlink); 45 | } 46 | 47 | export function path(path: string): string { 48 | return kleur.underline().cyan(path); 49 | } 50 | 51 | export function literal(literal: unknown): string { 52 | if (literal === null || literal === undefined) { 53 | return kleur.bold().green(`${literal}`); 54 | } 55 | if (typeof literal === 'string') { 56 | return kleur.green(`'${literal}'`); 57 | } 58 | if (typeof literal !== 'object') { 59 | return kleur.green(`${literal}`); 60 | } 61 | return object(literal); 62 | } 63 | 64 | export function key(text: string): string { 65 | return kleur.bold().blue(`"${text}"`); 66 | } 67 | 68 | export function object(object: object): string { 69 | const formatted = formatWithOptions({ colors: colorEnabled }, '%o', object); 70 | return kleur.white(formatted); 71 | } 72 | 73 | export function command(command: string): string { 74 | return kleur.bold().blue(`\`${command}\``); 75 | } 76 | 77 | export function highlight(text: string): string { 78 | return kleur.bold().cyan(text); 79 | } 80 | -------------------------------------------------------------------------------- /packages/nanobundle/src/fsUtils.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'node:fs'; 2 | 3 | export function exists(path: string): Promise { 4 | return fs.promises.access(path, fs.constants.F_OK) 5 | .then(() => true) 6 | .catch(() => false); 7 | } 8 | 9 | export async function chooseExist(paths: string[]): Promise { 10 | let result: string | null = null; 11 | for (const candidate of paths) { 12 | if (await exists(candidate)) { 13 | result = candidate; 14 | break; 15 | } 16 | } 17 | return result; 18 | } 19 | 20 | export function isFileSystemReference(path: string): boolean { 21 | const fileSystemReferencePattern = /^(\.{0,2}\/).*/; 22 | return fileSystemReferencePattern.test(path); 23 | } 24 | -------------------------------------------------------------------------------- /packages/nanobundle/src/importMaps.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, test, expect, vi } from 'vitest'; 2 | 3 | 4 | import { type Context } from './context'; 5 | import { NanobundleConfigError } from './errors'; 6 | import { type Manifest } from './manifest'; 7 | import { type Reporter } from './reporter'; 8 | import { 9 | replaceSubpathPattern, 10 | validateImportMaps, 11 | normalizeImportMaps, 12 | type NodeImportMaps, 13 | type ValidNodeImportMaps, 14 | } from './importMaps'; 15 | 16 | class ViReporter implements Reporter { 17 | debug = vi.fn(); 18 | info = vi.fn(); 19 | warn = vi.fn(); 20 | error = vi.fn(); 21 | captureException = vi.fn(); 22 | createChildReporter() { 23 | return new ViReporter(); 24 | } 25 | } 26 | 27 | describe('validateImportMaps', () => { 28 | const reporter = new ViReporter(); 29 | const defaultManifest: Manifest = { 30 | name: 'package' 31 | }; 32 | const defaultTargets: string[] = [ 33 | 'chrome', 34 | 'firefox', 35 | 'safari', 36 | ]; 37 | const defaultContext: Context = { 38 | cwd: '/project', 39 | verbose: false, 40 | module: 'commonjs', 41 | platform: 'neutral', 42 | sourcemap: true, 43 | legalComments: true, 44 | bundle: true, 45 | declaration: false, 46 | standalone: false, 47 | jsx: undefined, 48 | jsxDev: false, 49 | jsxFactory: 'React.createElement', 50 | jsxFragment: 'Fragment', 51 | jsxImportSource: 'react', 52 | rootDir: 'src', 53 | outDir: 'lib', 54 | tsconfigPath: undefined, 55 | importMapsPath: '/project/package.json', 56 | externalDependencies: [], 57 | forceExternalDependencies: [], 58 | manifest: defaultManifest, 59 | targets: defaultTargets, 60 | reporter, 61 | resolvePath: expect.any(Function), 62 | resolveRelativePath: expect.any(Function), 63 | }; 64 | 65 | test('subpath import pattern only allowed for Node.js-style imports', async () => { 66 | await expect(validateImportMaps({ 67 | context: defaultContext, 68 | importMaps: { 69 | imports: { 70 | 'src/*.js': 'dest/*.js', 71 | }, 72 | }, 73 | })).rejects.toThrowError(NanobundleConfigError); 74 | 75 | await expect(validateImportMaps({ 76 | context: defaultContext, 77 | importMaps: { 78 | imports: { 79 | '#src/*.js': '#dest/*.js', 80 | }, 81 | }, 82 | })).resolves.not.toThrow(); 83 | }); 84 | }); 85 | 86 | describe('normalizeImportMaps', () => { 87 | function validate(importMaps: NodeImportMaps): ValidNodeImportMaps { 88 | return importMaps as unknown as ValidNodeImportMaps; 89 | } 90 | 91 | test('flat importMaps (as is)', () => { 92 | const nodeImportMaps = validate({ 93 | imports: { 94 | './features.js': './src/features.js', 95 | }, 96 | }); 97 | expect( 98 | normalizeImportMaps(nodeImportMaps, { 99 | mode: undefined, 100 | module: 'esmodule', 101 | platform: 'neutral', 102 | minify: false, 103 | sourcemap: false, 104 | customConditions: [], 105 | }), 106 | ).toEqual({ 107 | imports: { 108 | './features.js': './src/features.js', 109 | }, 110 | }); 111 | }); 112 | 113 | test('conditional importMaps', () => { 114 | const nodeImportMaps = validate({ 115 | imports: { 116 | '#dep': { 117 | 'node': 'dep-node-native', 118 | 'default': './dep-polyfill.js', 119 | }, 120 | }, 121 | }); 122 | 123 | expect( 124 | normalizeImportMaps(nodeImportMaps, { 125 | mode: undefined, 126 | module: 'esmodule', 127 | platform: 'node', 128 | minify: false, 129 | sourcemap: false, 130 | customConditions: [], 131 | }), 132 | ).toEqual({ 133 | imports: { 134 | '#dep': 'dep-node-native', 135 | }, 136 | }); 137 | 138 | expect( 139 | normalizeImportMaps(nodeImportMaps, { 140 | mode: undefined, 141 | module: 'esmodule', 142 | platform: 'browser', 143 | minify: false, 144 | sourcemap: false, 145 | customConditions: [], 146 | }), 147 | ).toEqual({ 148 | imports: { 149 | '#dep': './dep-polyfill.js', 150 | }, 151 | }); 152 | 153 | expect( 154 | normalizeImportMaps(nodeImportMaps, { 155 | mode: undefined, 156 | module: 'esmodule', 157 | platform: 'neutral', 158 | minify: false, 159 | sourcemap: false, 160 | customConditions: [], 161 | }), 162 | ).toEqual({ 163 | imports: { 164 | '#dep': './dep-polyfill.js', 165 | }, 166 | }); 167 | }); 168 | 169 | test('custom condition importMaps', () => { 170 | const nodeImportMaps = validate({ 171 | imports: { 172 | '#shared/*.js': './src/shared/*.js', 173 | '#globals/*.js': { 174 | custom: './src/globals/*.custom.js', 175 | default: './src/globals/*.js', 176 | }, 177 | }, 178 | }); 179 | 180 | expect( 181 | normalizeImportMaps(nodeImportMaps, { 182 | mode: undefined, 183 | module: 'commonjs', 184 | platform: 'neutral', 185 | minify: false, 186 | sourcemap: false, 187 | customConditions: ['custom'], 188 | }), 189 | ).toEqual({ 190 | imports: { 191 | '#shared/*.js': './src/shared/*.js', 192 | '#globals/*.js': './src/globals/*.custom.js', 193 | }, 194 | }); 195 | 196 | expect( 197 | normalizeImportMaps(nodeImportMaps, { 198 | mode: undefined, 199 | module: 'commonjs', 200 | platform: 'neutral', 201 | minify: false, 202 | sourcemap: false, 203 | customConditions: [], 204 | }), 205 | ).toEqual({ 206 | imports: { 207 | '#shared/*.js': './src/shared/*.js', 208 | '#globals/*.js': './src/globals/*.js', 209 | }, 210 | }); 211 | }); 212 | 213 | test('nested conditional importMaps', () => { 214 | const nodeImportMaps = validate({ 215 | imports: { 216 | '#dep': { 217 | 'node': { 218 | 'require': './dep.cjs', 219 | 'import': './dep.mjs', 220 | }, 221 | 'default': './dep.js', 222 | }, 223 | }, 224 | }); 225 | expect( 226 | normalizeImportMaps(nodeImportMaps, { 227 | mode: undefined, 228 | module: 'commonjs', 229 | platform: 'node', 230 | minify: false, 231 | sourcemap: false, 232 | customConditions: [], 233 | }), 234 | ).toEqual({ 235 | imports: { 236 | '#dep': './dep.cjs', 237 | }, 238 | }); 239 | expect( 240 | normalizeImportMaps(nodeImportMaps, { 241 | mode: undefined, 242 | module: 'esmodule', 243 | platform: 'node', 244 | minify: false, 245 | sourcemap: false, 246 | customConditions: [], 247 | }), 248 | ).toEqual({ 249 | imports: { 250 | '#dep': './dep.mjs', 251 | }, 252 | }); 253 | expect( 254 | normalizeImportMaps(nodeImportMaps, { 255 | mode: undefined, 256 | module: 'commonjs', 257 | platform: 'neutral', 258 | minify: false, 259 | sourcemap: false, 260 | customConditions: [], 261 | }), 262 | ).toEqual({ 263 | imports: { 264 | '#dep': './dep.js', 265 | }, 266 | }); 267 | expect( 268 | normalizeImportMaps(nodeImportMaps, { 269 | mode: undefined, 270 | module: 'esmodule', 271 | platform: 'browser', 272 | minify: false, 273 | sourcemap: false, 274 | customConditions: [], 275 | }), 276 | ).toEqual({ 277 | imports: { 278 | '#dep': './dep.js', 279 | }, 280 | }); 281 | }); 282 | 283 | describe('common usecases', () => { 284 | test('ReScript dual package', () => { 285 | const nodeImportMaps = validate({ 286 | imports: { 287 | '@rescript/std/lib/es6/': { 288 | 'require': '@rescript/std/lib/js/', 289 | 'default': '@rescript/std/lib/es6/', 290 | }, 291 | }, 292 | }); 293 | expect( 294 | normalizeImportMaps(nodeImportMaps, { 295 | mode: undefined, 296 | module: 'commonjs', 297 | platform: 'neutral', 298 | minify: false, 299 | sourcemap: false, 300 | customConditions: [], 301 | }), 302 | ).toEqual({ 303 | imports: { 304 | '@rescript/std/lib/es6/': '@rescript/std/lib/js/', 305 | }, 306 | }); 307 | expect( 308 | normalizeImportMaps(nodeImportMaps, { 309 | mode: undefined, 310 | module: 'esmodule', 311 | platform: 'neutral', 312 | minify: false, 313 | sourcemap: false, 314 | customConditions: [], 315 | }), 316 | ).toEqual({ 317 | imports: { 318 | '@rescript/std/lib/es6/': '@rescript/std/lib/es6/', 319 | }, 320 | }); 321 | }); 322 | }); 323 | }); 324 | 325 | describe('replaceSubpathPattern', () => { 326 | test('replace', () => { 327 | expect( 328 | replaceSubpathPattern( 329 | { 330 | imports: { 331 | '#test/': './src/test/', 332 | } 333 | }, 334 | '#test/module.js', 335 | ), 336 | ).toEqual('./src/test/module.js'); 337 | 338 | expect( 339 | replaceSubpathPattern( 340 | { 341 | imports: { 342 | '#test/*': './src/test/test.css', 343 | }, 344 | }, 345 | '#test/module.js', 346 | ), 347 | ).toEqual('./src/test/test.css'); 348 | 349 | expect( 350 | replaceSubpathPattern( 351 | { 352 | imports: { 353 | '#test/*.js': './src/test/*.js', 354 | }, 355 | }, 356 | '#test/module.js', 357 | ), 358 | ).toEqual('./src/test/module.js'); 359 | 360 | expect( 361 | replaceSubpathPattern( 362 | { 363 | imports: { 364 | '#test/*.js': './src/test/*.default.js', 365 | }, 366 | }, 367 | '#test/module.js', 368 | ), 369 | ).toEqual('./src/test/module.default.js'); 370 | }); 371 | 372 | test('does not replace', () => { 373 | expect( 374 | replaceSubpathPattern( 375 | { 376 | imports: { 377 | '#test1/': './src/test/', 378 | }, 379 | }, 380 | '#test2/module.js', 381 | ), 382 | ).toEqual('#test2/module.js'); 383 | 384 | expect( 385 | replaceSubpathPattern( 386 | { 387 | imports: { 388 | '#test/*.js': './src/test/*.js', 389 | }, 390 | }, 391 | '#test/module.css', 392 | ), 393 | ).toEqual('#test/module.css'); 394 | }); 395 | 396 | test('priority', () => { 397 | expect( 398 | replaceSubpathPattern( 399 | { 400 | imports: { 401 | '#test/': './src/test1/', 402 | '#test/module.js': './src/test2/module.js', 403 | }, 404 | }, 405 | '#test/module.js', 406 | ), 407 | ).toEqual('./src/test2/module.js'); 408 | }); 409 | }); 410 | -------------------------------------------------------------------------------- /packages/nanobundle/src/importMaps.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'node:path'; 2 | import * as fs from 'node:fs/promises'; 3 | 4 | import { type Context } from './context'; 5 | import * as formatUtils from './formatUtils'; 6 | import * as fsUtils from './fsUtils'; 7 | import { type ConditionalImports } from './manifest'; 8 | import { NanobundleConfigError } from './errors'; 9 | 10 | import { type BundleOptions } from './entryGroup'; 11 | 12 | const importSubpathPattern = /^(?.+\/)(?(?[^\/\.]+?)(?\..+)?)$/; 13 | 14 | export type NodeImportMaps = { 15 | imports: Exclude, 16 | }; 17 | export type ValidNodeImportMaps = NodeImportMaps & { __BRAND__: 'ValidNodeImportMaps' }; 18 | export type ImportMaps = { 19 | imports: Record, 20 | }; 21 | 22 | export async function loadImportMaps(context: Context): Promise { 23 | const { imports = {} } = await fs.readFile(context.importMapsPath, 'utf-8') 24 | .then(JSON.parse) as Partial; 25 | return { imports }; 26 | } 27 | 28 | type ValidateNodeImportMapsOptions = { 29 | context: Context, 30 | importMaps: NodeImportMaps, 31 | rootKey?: string, 32 | } 33 | export async function validateImportMaps({ 34 | context, 35 | importMaps, 36 | rootKey, 37 | }: ValidateNodeImportMapsOptions): Promise { 38 | for (const [key, importPath] of Object.entries(importMaps.imports)) { 39 | if (typeof importPath === 'object') { 40 | await validateImportMaps({ 41 | context, 42 | importMaps: { 43 | imports: importPath, 44 | }, 45 | rootKey: rootKey || key, 46 | }); 47 | } else { 48 | if (!(rootKey || key).startsWith('#')) { 49 | if (key.includes('*') || importPath.includes('*')) { 50 | throw new NanobundleConfigError( 51 | 'Subpath pattern (*) imports is supported only for Node.js-style imports like #pattern/*.js', 52 | ); 53 | } 54 | } 55 | 56 | if (!fsUtils.isFileSystemReference(importPath)) { 57 | // Loosen validation if path doesn't seems to be a file system reference 58 | // Instead, expecting it can be resolved as a Node.js module later 59 | continue; 60 | } 61 | 62 | const resolvedPath = path.resolve( 63 | path.dirname(context.importMapsPath), 64 | importPath.includes('*') 65 | ? path.dirname(importPath) 66 | : importPath, 67 | ); 68 | const exist = await fsUtils.exists(resolvedPath); 69 | if (!exist) { 70 | throw new NanobundleConfigError(`${formatUtils.path(resolvedPath)} doesn't exist`); 71 | } 72 | } 73 | } 74 | return importMaps as ValidNodeImportMaps; 75 | } 76 | 77 | export function normalizeImportMaps( 78 | importMaps: ValidNodeImportMaps, 79 | bundleOptions: BundleOptions, 80 | ): ImportMaps { 81 | function normalize( 82 | rootKey: string, 83 | imports: ConditionalImports, 84 | mode: BundleOptions['mode'], 85 | module: BundleOptions['module'], 86 | platform: BundleOptions['platform'], 87 | customConditions: BundleOptions['customConditions'], 88 | ): string { 89 | if (typeof imports === 'string') { 90 | return imports; 91 | 92 | } else { 93 | for (const [key, value] of Object.entries(imports)) { 94 | if (key === 'node' && platform === 'node') { 95 | if (typeof value === 'string') { 96 | return value; 97 | } else { 98 | return normalize( 99 | rootKey, 100 | value, 101 | mode, 102 | module, 103 | 'node', 104 | customConditions, 105 | ); 106 | } 107 | } 108 | if (key === 'browser' && platform === 'browser') { 109 | if (typeof value === 'string') { 110 | return value; 111 | } else { 112 | return normalize( 113 | rootKey, 114 | imports, 115 | mode, 116 | module, 117 | 'browser', 118 | customConditions, 119 | ); 120 | } 121 | } 122 | if (key === 'require' && module === 'commonjs') { 123 | if (typeof value === 'string') { 124 | return value; 125 | } else { 126 | return normalize( 127 | rootKey, 128 | imports, 129 | mode, 130 | 'commonjs', 131 | platform, 132 | customConditions, 133 | ); 134 | } 135 | } 136 | if (key === 'import' && module === 'esmodule') { 137 | if (typeof value === 'string') { 138 | return value; 139 | } else { 140 | return normalize( 141 | rootKey, 142 | imports, 143 | mode, 144 | 'esmodule', 145 | platform, 146 | customConditions, 147 | ); 148 | } 149 | } 150 | if (key === 'development' && mode === 'development') { 151 | if (typeof value === 'string') { 152 | return value; 153 | } else { 154 | return normalize( 155 | rootKey, 156 | imports, 157 | 'development', 158 | module, 159 | platform, 160 | customConditions, 161 | ); 162 | } 163 | } 164 | if (key === 'production' && mode === 'production') { 165 | if (typeof value === 'string') { 166 | return value; 167 | } else { 168 | return normalize( 169 | rootKey, 170 | imports, 171 | 'production', 172 | module, 173 | platform, 174 | customConditions, 175 | ); 176 | } 177 | } 178 | if (customConditions.includes(key)) { 179 | if (typeof value === 'string') { 180 | return value; 181 | } else { 182 | return normalize( 183 | rootKey, 184 | imports, 185 | mode, 186 | module, 187 | platform, 188 | customConditions, 189 | ); 190 | } 191 | } 192 | if (key === 'default') { 193 | if (typeof value === 'string') { 194 | return value; 195 | } else { 196 | return normalize( 197 | rootKey, 198 | imports, 199 | mode, 200 | module, 201 | platform, 202 | customConditions, 203 | ); 204 | } 205 | } 206 | continue; 207 | } 208 | return rootKey; 209 | } 210 | } 211 | 212 | const { mode, module, platform, customConditions } = bundleOptions; 213 | const result: ImportMaps = { 214 | imports: {}, 215 | }; 216 | for (const [key, imports] of Object.entries(importMaps.imports)) { 217 | result.imports[key] = normalize( 218 | key, 219 | imports, 220 | mode, 221 | module, 222 | platform, 223 | customConditions, 224 | ); 225 | } 226 | 227 | return result; 228 | } 229 | 230 | export function replaceSubpathPattern(importMaps: ImportMaps, modulePath: string): string { 231 | if (importMaps.imports[modulePath]) { 232 | return importMaps.imports[modulePath]; 233 | } 234 | 235 | const importsEntries = Object.entries(importMaps.imports) 236 | .sort(([from, _to]) => { 237 | if (from.includes('*')) { 238 | return -1; 239 | } 240 | return 0; 241 | }); 242 | 243 | const matchCache: Record = {}; 244 | 245 | for (const [fromPrefix, toPrefix] of importsEntries) { 246 | if (modulePath.startsWith(fromPrefix)) { 247 | return modulePath.replace(fromPrefix, toPrefix); 248 | } 249 | 250 | const fromPrefixMatch = matchCache[fromPrefix] || fromPrefix.match(importSubpathPattern); 251 | const toPrefixMatch = matchCache[toPrefix] || toPrefix.match(importSubpathPattern); 252 | const modulePathMatch = matchCache[modulePath] || modulePath.match(importSubpathPattern); 253 | 254 | if (fromPrefixMatch?.groups?.['dirname'] === modulePathMatch?.groups?.['dirname']) { 255 | if (fromPrefixMatch?.groups?.['filename'] === '*') { 256 | return (toPrefixMatch?.groups?.['dirname'] || '') + 257 | (toPrefixMatch?.groups?.['base'] === '*' 258 | ? modulePathMatch?.groups?.['filename'] + (toPrefixMatch?.groups?.['ext'] || '') 259 | : (toPrefixMatch?.groups?.['filename'] || '') 260 | ); 261 | } 262 | if (fromPrefixMatch?.groups?.['base'] === '*') { 263 | if (fromPrefixMatch?.groups?.['ext'] === modulePathMatch?.groups?.['ext']) { 264 | return (toPrefixMatch?.groups?.['dirname'] || '') + 265 | (modulePathMatch?.groups?.['base'] || '') + (toPrefixMatch?.groups?.['ext'] || ''); 266 | } 267 | } 268 | } 269 | } 270 | 271 | return modulePath; 272 | } 273 | -------------------------------------------------------------------------------- /packages/nanobundle/src/manifest.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'node:path'; 2 | import * as fs from 'node:fs/promises'; 3 | 4 | export type Manifest = { 5 | name?: string, 6 | 7 | version?: string, 8 | 9 | type?: 'commonjs' | 'module', 10 | 11 | /** 12 | * Source file for the `main`, `module`, and `exports` entry 13 | */ 14 | source?: string, 15 | 16 | // Non-standard entry style for legacy bundlers 17 | module?: string, 18 | 19 | // Main entry 20 | main?: string, 21 | 22 | // Binary entries 23 | bin?: string | { 24 | [name: string]: string, 25 | }, 26 | 27 | // TypeScript declaration for "main" entry 28 | types?: string, 29 | 30 | // Export maps 31 | exports?: ConditionalExports, 32 | 33 | // Subpath imports 34 | imports?: ConditionalImports, 35 | 36 | dependencies?: { 37 | [name: string]: string, 38 | }, 39 | 40 | peerDependencies?: { 41 | [name: string]: string, 42 | }, 43 | 44 | browserslist?: string | string[], 45 | 46 | engines?: { 47 | node?: string, 48 | deno?: string, 49 | }, 50 | }; 51 | 52 | // See https://nodejs.org/api/packages.html#packages_nested_conditions 53 | // What a mess :/ 54 | export type ConditionalExports = ( 55 | | string 56 | | { 57 | [module: string]: ConditionalExports, 58 | } 59 | | { 60 | 'import'?: ConditionalExports, 61 | 'require'?: ConditionalExports, 62 | 'node'?: ConditionalExports, 63 | 'node-addons'?: ConditionalExports, 64 | 'default'?: ConditionalExports, 65 | 66 | // community conditions definitions 67 | // See https://nodejs.org/api/packages.html#packages_community_conditions_definitions 68 | // See also https://devblogs.microsoft.com/typescript/announcing-typescript-4-7/#package-json-exports-imports-and-self-referencing 69 | 'types'?: ConditionalExports, 70 | 'deno'?: ConditionalExports, 71 | 'browser'?: ConditionalExports, 72 | 'development'?: ConditionalExports, 73 | 'production'?: ConditionalExports, 74 | } 75 | ); 76 | 77 | export type ConditionalImports = ( 78 | | string 79 | | { 80 | [module: string]: ConditionalImports, 81 | } 82 | | { 83 | 'import'?: ConditionalImports, 84 | 'require'?: ConditionalImports, 85 | 'node'?: ConditionalImports, 86 | 'default'?: ConditionalImports, 87 | 'browser'?: ConditionalImports, 88 | 'development'?: ConditionalImports, 89 | 'production'?: ConditionalImports, 90 | } 91 | ); 92 | 93 | type ManifestWithOverride = Manifest & { 94 | publishConfig?: Manifest, 95 | }; 96 | 97 | interface LoadManifest { 98 | (props: { 99 | basePath: string, 100 | }): Promise; 101 | } 102 | export const loadManifest: LoadManifest = async ({ 103 | basePath, 104 | }) => { 105 | const configPath = path.resolve(basePath, 'package.json'); 106 | 107 | const { publishConfig, ...config } = await fs.readFile(configPath, 'utf-8') 108 | .then(JSON.parse) as ManifestWithOverride; 109 | 110 | return { 111 | ...config, 112 | ...publishConfig, 113 | }; 114 | } 115 | -------------------------------------------------------------------------------- /packages/nanobundle/src/outputFile.ts: -------------------------------------------------------------------------------- 1 | export type OutputFile = { 2 | sourcePath?: string, 3 | 4 | path: string, 5 | 6 | content: Uint8Array, 7 | }; -------------------------------------------------------------------------------- /packages/nanobundle/src/plugins/esbuildEmbedPlugin.ts: -------------------------------------------------------------------------------- 1 | import { type Plugin } from 'esbuild'; 2 | 3 | import { type Context } from '../context'; 4 | import * as fsUtils from '../fsUtils'; 5 | 6 | type PluginOptions = { 7 | context: Context, 8 | }; 9 | 10 | /** 11 | * @deprecated since esbuild resolution cannot be chained 12 | */ 13 | export function makePlugin({ 14 | context: { 15 | reporter, 16 | standalone, 17 | externalDependencies, 18 | forceExternalDependencies, 19 | }, 20 | }: PluginOptions): Plugin { 21 | const ownedModule = (packageName: string, modulePath: string) => { 22 | return packageName === modulePath || modulePath.startsWith(packageName + '/'); 23 | }; 24 | 25 | const isNodeApi = (modulePath: string) => { 26 | if (externalDependencies.some(dep => modulePath.startsWith(dep))) { 27 | return false; 28 | } 29 | return modulePath.startsWith('node:') || nodeApis.some(api => ownedModule(api, modulePath)); 30 | }; 31 | 32 | const shouldEmbed = (modulePath: string) => { 33 | if (forceExternalDependencies.some(dep => ownedModule(dep, modulePath))) { 34 | return false; 35 | } 36 | return standalone || !externalDependencies.some(dep => ownedModule(dep, modulePath)); 37 | }; 38 | 39 | return { 40 | name: 'nanobundle/embed', 41 | setup(build) { 42 | let dependOnNode = false; 43 | 44 | build.onResolve({ filter: /.*/ }, async args => { 45 | if (fsUtils.isFileSystemReference(args.path)) { 46 | return; 47 | } 48 | 49 | let resolvedAsNodeApi = isNodeApi(args.path); 50 | if (resolvedAsNodeApi) { 51 | dependOnNode = true; 52 | } 53 | 54 | let external = resolvedAsNodeApi || !shouldEmbed(args.path); 55 | let path = external ? args.path : undefined; 56 | 57 | return { path, external }; 58 | }); 59 | 60 | build.onEnd(() => { 61 | if (standalone && dependOnNode) { 62 | reporter.warn('Not completely standalone bundle, while the code depends on some Node.js APIs.'); 63 | } 64 | }); 65 | }, 66 | }; 67 | } 68 | 69 | const nodeApis = [ 70 | 'assert', 71 | 'async_hooks', 72 | 'buffer', 73 | 'child_process', 74 | 'cluster', 75 | 'console', 76 | 'crypto', 77 | 'diagnostics_channel', 78 | 'dns', 79 | 'events', 80 | 'fs', 81 | 'http', 82 | 'http2', 83 | 'https', 84 | 'inspector', 85 | 'module', 86 | 'net', 87 | 'os', 88 | 'path', 89 | 'perf_hooks', 90 | 'process', 91 | 'readline', 92 | 'stream', 93 | 'string_decoder', 94 | 'timers', 95 | 'tls', 96 | 'trace_events', 97 | 'tty', 98 | 'dgram', 99 | 'url', 100 | 'util', 101 | 'v8', 102 | 'vm', 103 | 'wasi', 104 | 'worker_threads', 105 | 'zlib', 106 | 107 | // legacy 108 | 'querystring', 109 | 110 | // deprecated 111 | '_linklist', 112 | '_stream_wrap', 113 | 'constants', 114 | 'domain', 115 | 'punycode', 116 | 'sys', 117 | ]; 118 | -------------------------------------------------------------------------------- /packages/nanobundle/src/plugins/esbuildImportMapsPlugin.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'node:path'; 2 | import { type Plugin } from 'esbuild'; 3 | 4 | import { type Context } from '../context'; 5 | import * as fsUtils from '../fsUtils'; 6 | import { type ImportMaps, replaceSubpathPattern } from '../importMaps'; 7 | 8 | type PluginOptions = { 9 | context: Context, 10 | importMaps: ImportMaps, 11 | }; 12 | 13 | /** 14 | * @deprecated since esbuild resolution cannot be chained 15 | */ 16 | export function makePlugin({ 17 | context, 18 | importMaps, 19 | }: PluginOptions): Plugin { 20 | const isExternalPath = (path: string) => !fsUtils.isFileSystemReference(path); 21 | const resolveModulePathFromImportMaps = async (modulePath: string) => { 22 | const resolved = path.resolve(path.dirname(context.importMapsPath), modulePath); 23 | const exist = await fsUtils.chooseExist([ 24 | resolved.replace(/\.(c|m)?js$/, '.tsx'), 25 | resolved.replace(/\.(c|m)?js$/, '.ts'), 26 | resolved, 27 | ]); 28 | return exist || resolved; 29 | }; 30 | return { 31 | name: '@nanobundle/import-maps', 32 | setup(build) { 33 | build.onResolve({ filter: /.*/ }, async args => { 34 | if (isExternalPath(args.path)) { 35 | const modulePath = replaceSubpathPattern(importMaps, args.path); 36 | const external = isExternalPath(modulePath); 37 | if (external) { 38 | return { 39 | path: modulePath, 40 | external, 41 | } 42 | } else { 43 | const resolvedModulePath = await resolveModulePathFromImportMaps(modulePath); 44 | return { 45 | path: resolvedModulePath, 46 | }; 47 | } 48 | } 49 | }); 50 | }, 51 | }; 52 | }; 53 | -------------------------------------------------------------------------------- /packages/nanobundle/src/plugins/esbuildNanobundlePlugin.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'node:path'; 2 | import { type Plugin } from 'esbuild'; 3 | 4 | import { type Context } from '../context'; 5 | import * as fsUtils from '../fsUtils'; 6 | import { type ImportMaps, replaceSubpathPattern } from '../importMaps'; 7 | 8 | type PluginOptions = { 9 | context: Context, 10 | importMaps: ImportMaps, 11 | }; 12 | 13 | export function makePlugin({ 14 | context, 15 | context: { 16 | reporter, 17 | standalone, 18 | externalDependencies, 19 | forceExternalDependencies, 20 | }, 21 | importMaps, 22 | }: PluginOptions): Plugin { 23 | const ownedModule = (packageName: string, modulePath: string) => { 24 | return packageName === modulePath || modulePath.startsWith(packageName + '/'); 25 | }; 26 | 27 | const isNodeApi = (modulePath: string) => { 28 | if (externalDependencies.some(dep => modulePath.startsWith(dep))) { 29 | return false; 30 | } 31 | return modulePath.startsWith('node:') || nodeApis.some(api => ownedModule(api, modulePath)); 32 | }; 33 | 34 | const shouldEmbed = (modulePath: string) => { 35 | if (forceExternalDependencies.some(dep => ownedModule(dep, modulePath))) { 36 | return false; 37 | } 38 | return standalone || !externalDependencies.some(dep => ownedModule(dep, modulePath)); 39 | }; 40 | 41 | const resolveModulePathFromImportMaps = async (modulePath: string) => { 42 | const resolved = path.resolve(path.dirname(context.importMapsPath), modulePath); 43 | const exist = await fsUtils.chooseExist([ 44 | resolved.replace(/\.(c|m)?js$/, '.tsx'), 45 | resolved.replace(/\.(c|m)?js$/, '.ts'), 46 | resolved, 47 | ]); 48 | return exist || resolved; 49 | }; 50 | 51 | return { 52 | name: 'nanobundle', 53 | setup(build) { 54 | let dependOnNode = false; 55 | 56 | build.onResolve({ filter: /.*/ }, async args => { 57 | if (fsUtils.isFileSystemReference(args.path)) { 58 | return; 59 | } 60 | 61 | const modulePath = replaceSubpathPattern(importMaps, args.path); 62 | const external = !fsUtils.isFileSystemReference(modulePath); 63 | 64 | let resolvedAsNodeApi = isNodeApi(modulePath); 65 | if (resolvedAsNodeApi) { 66 | dependOnNode = true; 67 | } 68 | 69 | if (!resolvedAsNodeApi && shouldEmbed(modulePath)) { 70 | return {}; 71 | } 72 | 73 | return { 74 | external, 75 | path: external 76 | ? modulePath 77 | : await resolveModulePathFromImportMaps(modulePath), 78 | }; 79 | }); 80 | 81 | build.onEnd(() => { 82 | if (standalone && dependOnNode) { 83 | reporter.warn('Not completely standalone bundle, while the code depends on some Node.js APIs.'); 84 | } 85 | }); 86 | }, 87 | }; 88 | } 89 | 90 | const nodeApis = [ 91 | 'assert', 92 | 'async_hooks', 93 | 'buffer', 94 | 'child_process', 95 | 'cluster', 96 | 'console', 97 | 'crypto', 98 | 'diagnostics_channel', 99 | 'dns', 100 | 'events', 101 | 'fs', 102 | 'http', 103 | 'http2', 104 | 'https', 105 | 'inspector', 106 | 'module', 107 | 'net', 108 | 'os', 109 | 'path', 110 | 'perf_hooks', 111 | 'process', 112 | 'readline', 113 | 'stream', 114 | 'string_decoder', 115 | 'timers', 116 | 'tls', 117 | 'trace_events', 118 | 'tty', 119 | 'dgram', 120 | 'url', 121 | 'util', 122 | 'v8', 123 | 'vm', 124 | 'wasi', 125 | 'worker_threads', 126 | 'zlib', 127 | 128 | // legacy 129 | 'querystring', 130 | 131 | // deprecated 132 | '_linklist', 133 | '_stream_wrap', 134 | 'constants', 135 | 'domain', 136 | 'punycode', 137 | 'sys', 138 | ]; 139 | -------------------------------------------------------------------------------- /packages/nanobundle/src/reporter.ts: -------------------------------------------------------------------------------- 1 | import { formatWithOptions } from 'node:util'; 2 | import kleur from 'kleur'; 3 | 4 | import * as formatUtils from './formatUtils'; 5 | import { NanobundleError } from './errors'; 6 | 7 | export interface Reporter { 8 | debug(msg: string, ...args: any[]): void; 9 | info(msg: string, ...args: any[]): void; 10 | warn(msg: string, ...args: any[]): void; 11 | error(msg: string, ...args: any[]): void; 12 | captureException(exn: unknown): void; 13 | createChildReporter(): Reporter; 14 | } 15 | 16 | export class ConsoleReporter implements Reporter { 17 | #level: number; 18 | #console: Console; 19 | 20 | color = formatUtils.colorEnabled; 21 | level: 'default' | 'debug' = 'debug'; 22 | 23 | constructor(console: Console, level = 0) { 24 | this.#level = level; 25 | this.#console = console; 26 | } 27 | 28 | #indent(msg: string): string { 29 | return formatUtils.indent(msg, this.#level); 30 | } 31 | 32 | debug(msg: string, ...args: any[]): void { 33 | if (this.level !== 'debug') { 34 | return; 35 | } 36 | 37 | const formatted = formatWithOptions( 38 | { colors: this.color }, 39 | msg, 40 | ...args, 41 | ); 42 | const indented = this.#indent(formatted); 43 | this.#console.debug( 44 | kleur.gray(`[debug] ${indented}`), 45 | ); 46 | } 47 | 48 | info(msg: string, ...args: any[]): void { 49 | const formatted = formatWithOptions( 50 | { colors: this.color }, 51 | msg, 52 | ...args, 53 | ); 54 | const indented = this.#indent(formatted); 55 | this.#console.info( 56 | kleur.white(`[info] ${indented}`), 57 | ); 58 | } 59 | 60 | warn(msg: string, ...args: any[]): void { 61 | const formatted = formatWithOptions( 62 | { colors: this.color }, 63 | msg, 64 | ...args, 65 | ); 66 | const indented = this.#indent(formatted); 67 | this.#console.warn( 68 | kleur.yellow(`[warn] ${indented}`), 69 | ); 70 | } 71 | 72 | error(msg: string, ...args: any[]): void { 73 | const formatted = formatWithOptions( 74 | { colors: this.color }, 75 | msg, 76 | ...args, 77 | ); 78 | const indented = this.#indent(formatted); 79 | this.#console.error( 80 | kleur.red(`[error] ${indented}`), 81 | ); 82 | } 83 | 84 | captureException(exn: unknown): void { 85 | let formatted; 86 | if (exn instanceof NanobundleError && exn.message) { 87 | formatted = exn.message; 88 | } else if (exn instanceof Error) { 89 | formatted = formatWithOptions( 90 | { colors: this.color }, 91 | exn.stack, 92 | ); 93 | } else { 94 | formatted = formatWithOptions( 95 | { colors: this.color }, 96 | '%s', 97 | exn, 98 | ); 99 | } 100 | const indented = this.#indent(formatted); 101 | this.#console.error( 102 | kleur.bold().red(`${indented}`), 103 | ); 104 | } 105 | 106 | createChildReporter(): ConsoleReporter { 107 | const child = new ConsoleReporter(this.#console, this.#level + 1); 108 | child.color = this.color; 109 | return child; 110 | } 111 | }; 112 | -------------------------------------------------------------------------------- /packages/nanobundle/src/target.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect } from 'vitest'; 2 | 3 | import { loadTargets } from './target'; 4 | 5 | describe('loadTargets', test => { 6 | const query = (query: string) => { 7 | return loadTargets({ basePath: __dirname, query }); 8 | }; 9 | 10 | test('defaults', () => { 11 | const t1 = query('defaults'); 12 | 13 | expect(t1).toEqual( 14 | expect.arrayContaining([ 15 | expect.stringContaining('chrome'), 16 | expect.stringContaining('firefox'), 17 | expect.stringContaining('edge'), 18 | expect.stringContaining('ios'), 19 | expect.stringContaining('safari'), 20 | expect.stringContaining('node'), 21 | expect.stringContaining('deno'), 22 | ]), 23 | ); 24 | }); 25 | 26 | test('ignore unsupported query', () => { 27 | const t1 = query('last 1 ie versions, last 1 opera versions'); 28 | expect(t1).toEqual([ 29 | expect.stringContaining('node'), 30 | expect.stringContaining('deno'), 31 | ]); 32 | }); 33 | 34 | test('ios safari', () => { 35 | const t1 = query('last 1 ios_saf versions'); 36 | expect(t1).toEqual([ 37 | expect.stringMatching(/^ios\d+/), 38 | expect.stringContaining('node'), 39 | expect.stringContaining('deno'), 40 | ]); 41 | }); 42 | 43 | test('android queries', () => { 44 | const t1 = query('android > 5'); 45 | expect(t1).toEqual([ 46 | expect.stringContaining('chrome'), 47 | expect.stringContaining('node'), 48 | expect.stringContaining('deno'), 49 | ]); 50 | 51 | const t2 = query('android <= 4.4'); 52 | expect(t2).toEqual([ 53 | expect.stringContaining('node'), 54 | expect.stringContaining('deno'), 55 | ]); 56 | 57 | const t3 = query('last 1 and_chr versions'); 58 | expect(t3).toEqual([ 59 | expect.stringContaining('chrome'), 60 | expect.stringContaining('node'), 61 | expect.stringContaining('deno'), 62 | ]); 63 | 64 | const t4 = query('last 1 and_ff versions'); 65 | expect(t4).toEqual([ 66 | expect.stringContaining('firefox'), 67 | expect.stringContaining('node'), 68 | expect.stringContaining('deno'), 69 | ]); 70 | }); 71 | }); 72 | -------------------------------------------------------------------------------- /packages/nanobundle/src/target.ts: -------------------------------------------------------------------------------- 1 | import browserslist from 'browserslist'; 2 | import semver from 'semver'; 3 | 4 | import { type Manifest } from './manifest'; 5 | 6 | type SupportedBrowser = keyof typeof browsersToTargets; 7 | type EsbuildTarget = typeof browsersToTargets[SupportedBrowser]; 8 | 9 | const browsersToTargets = { 10 | 'chrome': 'chrome', 11 | 'firefox': 'firefox', 12 | 'safari': 'safari', 13 | 'edge': 'edge', 14 | 'ios_saf': 'ios', 15 | 'android': 'chrome', 16 | 'and_chr': 'chrome', 17 | 'and_ff': 'firefox', 18 | } as const; 19 | 20 | const supportedBrowsers = new Set(Object.keys(browsersToTargets)); 21 | 22 | function isSupportedBrowser(browser: string): browser is SupportedBrowser { 23 | return supportedBrowsers.has(browser); 24 | } 25 | 26 | type LoadTargetOptions = { 27 | basePath?: string, 28 | query?: string, 29 | manifest?: Manifest, 30 | }; 31 | 32 | export function loadTargets(options?: LoadTargetOptions): string[] { 33 | const queries = browserslist(options?.query, { 34 | path: options?.basePath, 35 | }); 36 | 37 | const targetVersions = new Map(); 38 | 39 | for (const query of queries) { 40 | const [browser, versionString] = query.split(' '); 41 | 42 | if (!isSupportedBrowser(browser)) { 43 | continue; 44 | } 45 | 46 | let target = browsersToTargets[browser]; 47 | let minVersion = +versionString.split('-')[0]; 48 | 49 | if (browser === 'android') { 50 | // according to https://developer.android.com/guide/webapps/migrating 51 | if (minVersion > 4.4) { 52 | target = 'chrome'; 53 | // "defaults" minimum chrome version 54 | // at 2022.02.26. 55 | minVersion = 96; 56 | } else { 57 | continue; 58 | } 59 | } 60 | 61 | const targetVersion = targetVersions.get(target); 62 | if (!targetVersion || targetVersion > minVersion) { 63 | targetVersions.set(target, minVersion); 64 | } 65 | } 66 | 67 | let targets = Array.from(targetVersions.entries()) 68 | .map(targetVersion => targetVersion.join('')); 69 | 70 | if (options?.manifest?.engines?.node) { 71 | const version = semver.minVersion(options.manifest.engines.node); 72 | if (version) { 73 | targets.push('node' + version.format()); 74 | } 75 | } else { 76 | // latest officially supported version 77 | targets.push('node18'); 78 | } 79 | 80 | if (options?.manifest?.engines?.deno) { 81 | const version = semver.minVersion(options.manifest.engines.deno); 82 | if (version) { 83 | targets.push('deno' + version.format()); 84 | } 85 | } else { 86 | // minimum version supports ClassPrivateBrandCheck 87 | // See https://github.com/evanw/esbuild/issues/2940#issuecomment-1437818002 88 | targets.push('deno1.9'); 89 | } 90 | 91 | return targets; 92 | } 93 | -------------------------------------------------------------------------------- /packages/nanobundle/src/tasks/buildBundleTask.ts: -------------------------------------------------------------------------------- 1 | import * as esbuild from 'esbuild'; 2 | import dedent from 'string-dedent'; 3 | 4 | import { type Context } from '../context'; 5 | import { NanobundleError } from '../errors'; 6 | import * as fsUtils from '../fsUtils'; 7 | import * as formatUtils from '../formatUtils'; 8 | import { 9 | groupBundleEntries, 10 | optionsFromHash, 11 | type BundleEntry, 12 | type BundleOptions, 13 | } from '../entryGroup'; 14 | import { 15 | loadImportMaps, 16 | normalizeImportMaps, 17 | validateImportMaps, 18 | type ValidNodeImportMaps, 19 | } from '../importMaps'; 20 | import { type OutputFile } from '../outputFile'; 21 | import { makePlugin as makeDefaultPlugin } from '../plugins/esbuildNanobundlePlugin'; 22 | 23 | export class BuildBundleTaskError extends NanobundleError { 24 | esbuildErrors: esbuild.Message[]; 25 | constructor(message: string, errors: esbuild.Message[]) { 26 | super(message); 27 | this.esbuildErrors = errors; 28 | } 29 | } 30 | 31 | type BuildBundleTaskOptions = { 32 | context: Context, 33 | bundleEntries: BundleEntry[], 34 | }; 35 | 36 | type BuildBundleTaskResult = { 37 | outputFiles: OutputFile[], 38 | }; 39 | 40 | export async function buildBundleTask({ 41 | context, 42 | bundleEntries, 43 | }: BuildBundleTaskOptions): Promise { 44 | if (!context.bundle) { 45 | context.reporter.debug('buildBundleTask skipped since bundle=false'); 46 | return { outputFiles: [] }; 47 | } 48 | 49 | if (bundleEntries.length > 0) { 50 | context.reporter.debug(`start buildBundleTask for ${bundleEntries.length} entries`); 51 | } else { 52 | context.reporter.debug('there are no js entries, skipped buildBundleTask'); 53 | return { outputFiles: [] }; 54 | } 55 | 56 | const importMaps = await loadImportMaps(context); 57 | const validImportMaps = await validateImportMaps({ context, importMaps }); 58 | const bundleGroup = groupBundleEntries(bundleEntries); 59 | const subtasks: Array> = []; 60 | for (const [optionsHash, entries] of Object.entries(bundleGroup)) { 61 | const options = optionsFromHash(optionsHash); 62 | context.reporter.debug('bundle options %o', options); 63 | 64 | subtasks.push( 65 | buildBundleGroup({ 66 | context, 67 | options, 68 | bundleEntries: entries, 69 | validImportMaps, 70 | plugins: [], 71 | }), 72 | ); 73 | } 74 | const results = await Promise.all(subtasks); 75 | 76 | const errors = results.flatMap(result => result.errors); 77 | if (errors.length > 0) { 78 | throw new BuildBundleTaskError('Some errors occur while running esbuild', errors); 79 | } 80 | 81 | const warnings = results.flatMap(result => result.warnings); 82 | if (warnings.length > 0) { 83 | for (const warning of warnings) { 84 | context.reporter.warn(warning.text); 85 | } 86 | } 87 | 88 | const outputFiles = results 89 | .flatMap(result => result.outputFiles) 90 | .map(outputFile => ({ 91 | path: outputFile.path, 92 | content: outputFile.contents, 93 | })); 94 | 95 | return { outputFiles }; 96 | } 97 | 98 | type BuildBundleGroupOptions = { 99 | context: Context, 100 | plugins: esbuild.Plugin[], 101 | validImportMaps: ValidNodeImportMaps, 102 | bundleEntries: BundleEntry[], 103 | options: BundleOptions, 104 | }; 105 | 106 | type BuildBundleGroupResult = { 107 | errors: esbuild.Message[], 108 | warnings: esbuild.Message[], 109 | outputFiles: esbuild.OutputFile[], 110 | }; 111 | 112 | async function buildBundleGroup({ 113 | context, 114 | plugins, 115 | validImportMaps, 116 | bundleEntries, 117 | options, 118 | }: BuildBundleGroupOptions): Promise { 119 | const entryPoints: Array<{ in: string, out: string }> = []; 120 | for (const entry of bundleEntries) { 121 | const sourceFile = await fsUtils.chooseExist(entry.sourceFile); 122 | if (!sourceFile) { 123 | // FIXME 124 | throw new BuildBundleTaskError(dedent` 125 | Source file does not exist. 126 | 127 | Expected one of 128 | - ${entry.sourceFile.join('\n - ')} 129 | 130 | But no matched files found. 131 | 132 | Please check your ${formatUtils.key('rootDir')} or ${formatUtils.key('outDir')} and try again. 133 | You can configure it in your ${formatUtils.path('tsconfig.json')}, or in CLI by ${formatUtils.command('--root-dir')} and ${formatUtils.command('--out-dir')} argument. 134 | 135 | `, []); 136 | } 137 | entryPoints.push({ 138 | in: sourceFile, 139 | out: entry.outputFile, 140 | }); 141 | } 142 | 143 | context.reporter.debug('esbuild entryPoints: %o', entryPoints); 144 | 145 | let esbuildOptions: esbuild.BuildOptions = { 146 | bundle: true, 147 | sourcemap: options.sourcemap, 148 | legalComments: context.legalComments ? 'linked' : 'none', 149 | minify: options.minify, 150 | define: { 151 | 'process.env.NANOBUNDLE_PACKAGE_NAME': JSON.stringify(context.manifest.name || 'unknown'), 152 | 'process.env.NANOBUNDLE_PACKAGE_VERSION': JSON.stringify(context.manifest.version || '0.0.0'), 153 | }, 154 | }; 155 | 156 | if (options.module === 'commonjs' || options.module === 'esmodule') { 157 | esbuildOptions = { 158 | ...esbuildOptions, 159 | tsconfig: context.tsconfigPath, 160 | jsx: context.jsx, 161 | jsxDev: context.jsxDev, 162 | jsxFactory: context.jsxFactory, 163 | jsxFragment: context.jsxFragment, 164 | jsxImportSource: context.jsxImportSource, 165 | treeShaking: true, 166 | keepNames: true, 167 | format: options.module === 'commonjs' ? 'cjs' : 'esm', 168 | conditions: options.customConditions, 169 | }; 170 | 171 | if (options.platform === 'deno') { 172 | esbuildOptions.platform = 'neutral'; 173 | esbuildOptions.target = context.targets 174 | .filter(target => target.startsWith('deno')); 175 | } else { 176 | esbuildOptions.platform = options.platform; 177 | if (options.platform === 'node') { 178 | esbuildOptions.target = context.targets 179 | .filter(target => target.startsWith('node')); 180 | } else { 181 | esbuildOptions.target = context.targets 182 | .filter(target => !(target.startsWith('node') || target.startsWith('deno'))); 183 | } 184 | } 185 | 186 | if (options.mode) { 187 | esbuildOptions.define = { 188 | ...esbuildOptions.define, 189 | 'process.env.NODE_ENV': JSON.stringify(options.mode), 190 | 'process.env.NANOBUNDLE_MODE': JSON.stringify(options.mode), 191 | }; 192 | } 193 | 194 | const importMaps = normalizeImportMaps(validImportMaps, options); 195 | 196 | const defaultPlugin = makeDefaultPlugin({ context, importMaps }); 197 | esbuildOptions.plugins = [defaultPlugin, ...plugins]; 198 | } 199 | 200 | context.reporter.debug('esbuild build options %o', esbuildOptions); 201 | 202 | const results = await Promise.all( 203 | entryPoints.map(entry => esbuild.build({ 204 | ...esbuildOptions, 205 | entryPoints: [entry.in], 206 | outfile: entry.out, 207 | write: false, 208 | })) 209 | ); 210 | 211 | const outputFiles = results.flatMap(result => 212 | result.outputFiles 213 | .filter(outputFile => { 214 | if (outputFile.path.endsWith('.LEGAL.txt') && outputFile.contents.length === 0) { 215 | return false; 216 | } 217 | return true; 218 | }) 219 | .map(outputFile => ({ 220 | ...outputFile, 221 | path: outputFile.path, 222 | })), 223 | ); 224 | 225 | const errors = results.flatMap(result => result.errors); 226 | const warnings = results.flatMap(result => result.warnings); 227 | 228 | return { 229 | errors, 230 | warnings, 231 | outputFiles, 232 | }; 233 | } 234 | -------------------------------------------------------------------------------- /packages/nanobundle/src/tasks/buildFileTask.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'node:fs/promises'; 2 | import dedent from 'string-dedent'; 3 | 4 | import * as formatUtils from '../formatUtils'; 5 | import { type Context } from '../context'; 6 | import { type FileEntry } from '../entryGroup'; 7 | import { type OutputFile } from '../outputFile'; 8 | import { NanobundleError } from '../errors'; 9 | 10 | export class BuildFileTaskError extends NanobundleError { 11 | reasons: any[]; 12 | constructor(reasons: any[]) { 13 | super(); 14 | this.reasons = reasons; 15 | } 16 | } 17 | 18 | type BuildFileTaskOptions = { 19 | context: Context, 20 | fileEntries: FileEntry[], 21 | }; 22 | 23 | type BuildFileTaskResult = { 24 | outputFiles: OutputFile[], 25 | }; 26 | 27 | export async function buildFileTask({ 28 | context, 29 | fileEntries, 30 | }: BuildFileTaskOptions): Promise { 31 | if (!context.bundle) { 32 | context.reporter.debug('buildFileTask skipped since bundle=false'); 33 | return { outputFiles: [] }; 34 | } 35 | 36 | if (fileEntries.length > 0) { 37 | context.reporter.debug(`start buildFileTask for ${fileEntries.length} entries`); 38 | } else { 39 | context.reporter.debug('there are no file entries, skipped buildFileTask'); 40 | return { outputFiles: [] }; 41 | } 42 | 43 | const subtasks: Array> = []; 44 | for (const entry of fileEntries) { 45 | const sourceFile = entry.sourceFile[0]; 46 | const outputFile = entry.outputFile; 47 | if (sourceFile === outputFile) { 48 | context.reporter.debug(dedent` 49 | noop for ${formatUtils.key(entry.key)} because of source path and output path are the same. 50 | entry path: ${formatUtils.path(entry.entryPath)} 51 | `); 52 | continue; 53 | } 54 | subtasks.push(buildFile({ sourceFile, outputFile })); 55 | } 56 | 57 | const results = await Promise.allSettled(subtasks); 58 | const rejects = results.filter((result): result is PromiseRejectedResult => result.status === 'rejected'); 59 | if (rejects.length) { 60 | throw new BuildFileTaskError(rejects.map(reject => reject.reason)); 61 | } 62 | const resolves = results as PromiseFulfilledResult[]; 63 | const outputFiles = resolves.map(result => result.value.outputFile); 64 | 65 | return { outputFiles }; 66 | } 67 | 68 | type BuildFileOptions = { 69 | sourceFile: string, 70 | outputFile: string, 71 | }; 72 | 73 | type BuildFileResult = { 74 | outputFile: OutputFile, 75 | } 76 | 77 | async function buildFile({ 78 | sourceFile, 79 | outputFile, 80 | }: BuildFileOptions): Promise { 81 | const content = await fs.readFile(sourceFile); 82 | return { 83 | outputFile: { 84 | sourcePath: sourceFile, 85 | path: outputFile, 86 | content, 87 | }, 88 | }; 89 | } -------------------------------------------------------------------------------- /packages/nanobundle/src/tasks/buildTypeTask.ts: -------------------------------------------------------------------------------- 1 | import dedent from 'string-dedent'; 2 | import { parseNative } from 'tsconfck'; 3 | import { 4 | type CompilerOptions, 5 | type CompilerHost, 6 | type Diagnostic, 7 | } from 'typescript'; 8 | 9 | import * as formatUtils from '../formatUtils'; 10 | import { NanobundleError } from '../errors'; 11 | import { type Context } from '../context'; 12 | import { type TypeEntry } from '../entryGroup'; 13 | import { type OutputFile } from '../outputFile'; 14 | 15 | export class BuildTypeTaskError extends NanobundleError { 16 | } 17 | 18 | export class BuildTypeTaskTsError extends NanobundleError { 19 | constructor(ts: typeof import('typescript'), host: CompilerHost, diagnostics: readonly Diagnostic[]) { 20 | const message = dedent` 21 | [error] TypeScript compilation failed 22 | 23 | ${formatUtils.indent( 24 | formatUtils.colorEnabled 25 | ? ts.formatDiagnosticsWithColorAndContext(diagnostics, host) 26 | : ts.formatDiagnostics(diagnostics, host), 27 | 1, 28 | )} 29 | `; 30 | super(message); 31 | } 32 | } 33 | 34 | type BuildTypeTaskOptions = { 35 | context: Context, 36 | typeEntries: TypeEntry[], 37 | } 38 | 39 | type BuildTypeTaskResult = { 40 | outputFiles: OutputFile[], 41 | } 42 | 43 | export async function buildTypeTask({ 44 | context, 45 | typeEntries, 46 | }: BuildTypeTaskOptions): Promise { 47 | if (!context.declaration) { 48 | context.reporter.debug('buildTypeTask skipped since declaration=false'); 49 | return { outputFiles: [] }; 50 | } 51 | 52 | if (!context.tsconfigPath) { 53 | context.reporter.debug(`buildTypeTask skipped since no tsconfig.json provided`); 54 | return { outputFiles: [] }; 55 | } 56 | 57 | if (typeEntries.length > 0) { 58 | context.reporter.debug(`start buildTypeTask for ${typeEntries.length} entries`); 59 | } else { 60 | context.reporter.debug('there are no dts entries, skipped buildTypeTask'); 61 | return { outputFiles: [] }; 62 | } 63 | 64 | let ts: typeof import('typescript'); 65 | try { 66 | ts = await import('typescript').then(mod => mod.default); 67 | } catch (error: unknown) { 68 | throw new BuildTypeTaskError(dedent` 69 | Couldn't load TypeScript API 70 | 71 | Try ${formatUtils.command('npm i -D typescript')} or ${formatUtils.command('yarn add -D typescript')} and build again. 72 | 73 | `); 74 | } 75 | 76 | context.reporter.debug('loaded TypeScript compiler API version %s', ts.version); 77 | 78 | const { result } = await parseNative(context.tsconfigPath); 79 | const compilerOptions: CompilerOptions = { 80 | ...result.options, 81 | rootDir: context.rootDir, 82 | outDir: context.outDir, 83 | allowJs: true, 84 | composite: false, 85 | incremental: false, 86 | skipLibCheck: true, 87 | declaration: true, 88 | emitDeclarationOnly: true, 89 | }; 90 | 91 | if (compilerOptions.noEmit) { 92 | context.reporter.warn(dedent` 93 | Ignored ${formatUtils.key('noEmit')} specified in your tsconfig.json 94 | 95 | You can disable emitting declaration via ${formatUtils.command('--no-dts')} flag. 96 | 97 | `); 98 | } 99 | compilerOptions.noEmit = false; 100 | 101 | if (!( 102 | compilerOptions.moduleResolution === ts.ModuleResolutionKind.Node16 || 103 | compilerOptions.moduleResolution === ts.ModuleResolutionKind.NodeNext 104 | )) { 105 | context.reporter.warn(dedent` 106 | nanobundle recommends to use ${formatUtils.literal('Node16')} or ${formatUtils.literal('NodeNext')} for ${formatUtils.key('compilerOptions.moduleResolution')} 107 | 108 | See ${formatUtils.hyperlink('https://www.typescriptlang.org/docs/handbook/esm-node.html')} for more detail. 109 | 110 | `); 111 | } 112 | 113 | context.reporter.debug('loaded compilerOptions %o', compilerOptions); 114 | 115 | const outputMap = new Map(); 116 | const host = ts.createCompilerHost(compilerOptions); 117 | host.writeFile = (filename, content) => { 118 | context.reporter.debug(`ts program emitted file to ${formatUtils.path(filename)}`); 119 | outputMap.set(filename, Buffer.from(content, 'utf-8')); 120 | }; 121 | 122 | const otherDiagnostics: Diagnostic[] = []; 123 | 124 | for (const entry of typeEntries) { 125 | const program = ts.createProgram(entry.sourceFile, compilerOptions, host); 126 | context.reporter.debug(`created ts program from %o`, entry.sourceFile); 127 | 128 | const result = program.emit(); 129 | const allDiagnostics = dedupeDiagnostics( 130 | ts.getPreEmitDiagnostics(program).concat(result.diagnostics), 131 | ); 132 | 133 | const errorDiagnostics: Diagnostic[] = []; 134 | 135 | for (const diagnostic of allDiagnostics) { 136 | if (diagnosticIgnores.includes(diagnostic.code)) { 137 | continue; 138 | } 139 | switch (diagnostic.category) { 140 | case ts.DiagnosticCategory.Error: { 141 | errorDiagnostics.push(diagnostic); 142 | break; 143 | } 144 | default: { 145 | otherDiagnostics.push(diagnostic); 146 | break; 147 | } 148 | } 149 | } 150 | 151 | if (errorDiagnostics.length > 0) { 152 | throw new BuildTypeTaskTsError( 153 | ts, 154 | host, 155 | errorDiagnostics, 156 | ); 157 | } 158 | } 159 | 160 | if (otherDiagnostics.length > 0) { 161 | context.reporter.warn( 162 | formatUtils.colorEnabled 163 | ? ts.formatDiagnosticsWithColorAndContext(otherDiagnostics, host) 164 | : ts.formatDiagnostics(otherDiagnostics, host), 165 | ); 166 | } 167 | 168 | const outputFiles = [...outputMap.entries()] 169 | .map(([path, content]) => ({ 170 | path, 171 | content, 172 | })); 173 | 174 | return { outputFiles }; 175 | } 176 | 177 | function dedupeDiagnostics(diagnostics: readonly Diagnostic[]): readonly Diagnostic[] { 178 | const unique: Diagnostic[] = []; 179 | 180 | const rootCodes = new Set(); 181 | const files = new Set(); 182 | 183 | for (const diagnostic of diagnostics) { 184 | if (diagnostic.file) { 185 | if (!files.has(diagnostic.file.fileName)) { 186 | files.add(diagnostic.file.fileName); 187 | unique.push(diagnostic); 188 | } 189 | } else { 190 | if (!rootCodes.has(diagnostic.code)) { 191 | rootCodes.add(diagnostic.code); 192 | unique.push(diagnostic); 193 | } 194 | } 195 | } 196 | return unique; 197 | } 198 | 199 | const diagnosticIgnores: number[] = [ 200 | 6053, 201 | ]; 202 | -------------------------------------------------------------------------------- /packages/nanobundle/src/tasks/chmodBinTask.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'node:fs/promises'; 2 | 3 | import { type Context } from '../context'; 4 | import { type BundleEntry } from '../entryGroup'; 5 | 6 | type ChmodBinTaskOptions = { 7 | context: Context, 8 | binEntries: BundleEntry[], 9 | }; 10 | 11 | type ChmodBinTaskResult = { 12 | }; 13 | 14 | export async function chmodBinTask({ 15 | context, 16 | binEntries, 17 | }: ChmodBinTaskOptions): Promise { 18 | const subtasks: Array> = []; 19 | for (const entry of binEntries) { 20 | subtasks.push( 21 | fs.chmod(entry.outputFile, '+x'), 22 | ); 23 | } 24 | await Promise.all(subtasks); 25 | 26 | return {}; 27 | } 28 | -------------------------------------------------------------------------------- /packages/nanobundle/src/tasks/cleanupTask.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'node:fs/promises'; 2 | 3 | import * as formatUtils from '../formatUtils'; 4 | import { NanobundleError } from '../errors'; 5 | import { type Context } from '../context'; 6 | import { type OutputFile } from '../outputFile'; 7 | 8 | export class CleanupTaskError extends NanobundleError { 9 | reasons: any[]; 10 | constructor(reasons: any[]) { 11 | super(); 12 | this.reasons = reasons; 13 | } 14 | } 15 | 16 | type CleanupTaskOptions = { 17 | context: Context, 18 | outputFiles: Array>, 19 | }; 20 | 21 | type CleanupTaskResult = { 22 | }; 23 | 24 | export async function cleanupTask({ 25 | context, 26 | outputFiles, 27 | }: CleanupTaskOptions): Promise { 28 | const resolvedOutDir = context.resolvePath(context.outDir); 29 | const relativeOutDir = context.resolveRelativePath(resolvedOutDir); 30 | 31 | if (relativeOutDir !== '' && !relativeOutDir.startsWith('..')) { 32 | context.reporter.info(`🗑️ ${formatUtils.path('./' + relativeOutDir)}`); 33 | await fs.rm(resolvedOutDir, { recursive: true, force: true }); 34 | return {}; 35 | } 36 | 37 | const subtasks: Array> = []; 38 | for (const file of outputFiles) { 39 | if (file.path === file.sourcePath) { 40 | context.reporter.debug(`src=dest for ${file.path}, skipping`); 41 | continue; 42 | } 43 | context.reporter.info(`🗑️ ${formatUtils.path('./' + context.resolveRelativePath(file.path))}`); 44 | subtasks.push(fs.rm(file.path, { force: true })); 45 | } 46 | 47 | const results = await Promise.allSettled(subtasks); 48 | const rejects = results.filter((result): result is PromiseRejectedResult => result.status === 'rejected'); 49 | if (rejects.length) { 50 | throw new CleanupTaskError(rejects.map(reject => reject.reason)); 51 | } 52 | 53 | return {}; 54 | } 55 | -------------------------------------------------------------------------------- /packages/nanobundle/src/tasks/emitTask.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'node:fs/promises'; 2 | import * as path from 'node:path'; 3 | 4 | import { NanobundleError } from '../errors'; 5 | import { type Context } from '../context'; 6 | import { type OutputFile } from '../outputFile'; 7 | 8 | export class EmitTaskError extends NanobundleError { 9 | reasons: any[]; 10 | constructor(reasons: any[]) { 11 | super(); 12 | this.reasons = reasons; 13 | } 14 | } 15 | 16 | type EmitTaskOptions = { 17 | context: Context, 18 | outputFiles: OutputFile[], 19 | }; 20 | 21 | type EmitTaskResult = { 22 | outputFiles: OutputFile[], 23 | }; 24 | 25 | export async function emitTask({ 26 | outputFiles, 27 | }: EmitTaskOptions): Promise { 28 | const subtasks: Array> = []; 29 | 30 | for (const outputFile of outputFiles) { 31 | subtasks.push( 32 | fs.mkdir(path.dirname(outputFile.path), { recursive: true }) 33 | .then(() => fs.writeFile(outputFile.path, outputFile.content)), 34 | ); 35 | } 36 | 37 | const results = await Promise.allSettled(subtasks); 38 | const rejects = results.filter((result): result is PromiseRejectedResult => result.status === 'rejected'); 39 | if (rejects.length) { 40 | throw new EmitTaskError(rejects.map(reject => reject.reason)); 41 | } 42 | 43 | return { outputFiles }; 44 | } 45 | -------------------------------------------------------------------------------- /packages/nanobundle/src/tasks/reportEmitResultsTask.ts: -------------------------------------------------------------------------------- 1 | import * as zlib from 'node:zlib'; 2 | import { promisify } from 'node:util'; 3 | import dedent from 'string-dedent'; 4 | import prettyBytes from 'pretty-bytes'; 5 | 6 | import * as formatUtils from '../formatUtils'; 7 | import { type Context } from '../context'; 8 | import { type OutputFile } from '../outputFile'; 9 | 10 | const gzip = promisify(zlib.gzip); 11 | const brotli = promisify(zlib.brotliCompress); 12 | 13 | type ReportEmitResultsTaskOptions = { 14 | context: Context, 15 | bundleOutputs: OutputFile[], 16 | fileOutputs: OutputFile[], 17 | typeOutputs: OutputFile[], 18 | }; 19 | 20 | export async function reportEmitResultsTask({ 21 | context, 22 | bundleOutputs, 23 | fileOutputs, 24 | typeOutputs, 25 | }: ReportEmitResultsTaskOptions): Promise { 26 | const bundles = bundleOutputs 27 | .filter(bundle => !bundle.path.endsWith('.map')) 28 | .filter(bundle => !bundle.path.endsWith('.LEGAL.txt')) 29 | const lastBundle = bundles.at(-1); 30 | const plural = bundles.length !== 1; 31 | 32 | context.reporter.info(dedent` 33 | ${plural ? 'Bundles' : 'A bundle'} generated 34 | 35 | `); 36 | 37 | for (const bundle of bundles) { 38 | const [gzipped, brotlied] = await Promise.all([ 39 | gzip(bundle.content), 40 | brotli(bundle.content), 41 | ]); 42 | context.reporter.info(dedent` 43 | 📦 ${formatUtils.path(context.resolveRelativePath(bundle.path, true))}${context.verbose ? '\n' + formatUtils.indent(dedent` 44 | Size : ${prettyBytes(bundle.content.byteLength)} 45 | Size (gz) : ${prettyBytes(gzipped.byteLength)} 46 | Size (br) : ${prettyBytes(brotlied.byteLength)} 47 | 48 | `, 1) : (bundle === lastBundle ? '\n' : '')} 49 | `); 50 | } 51 | 52 | if (typeOutputs.length > 0) { 53 | context.reporter.info(dedent` 54 | Also ${typeOutputs.length} declaration ${plural ? 'files are' : 'file is'} generated 55 | 56 | ${context.verbose 57 | ? ` 📦 ${typeOutputs.map(output => formatUtils.path(context.resolveRelativePath(output.path, true))).join('\n 📦 ')}\n` 58 | : '' 59 | } 60 | `); 61 | } 62 | 63 | if (fileOutputs.length > 0) { 64 | for (const file of fileOutputs) { 65 | context.reporter.info(dedent` 66 | Copied ${formatUtils.path(context.resolveRelativePath(file.sourcePath!, true))} to ${formatUtils.path(context.resolveRelativePath(file.path, true))} 67 | `); 68 | } 69 | console.log(); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /packages/nanobundle/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "module": "ESNext", 5 | "moduleResolution": "Node", 6 | "strict": true, 7 | "composite": true, 8 | "incremental": true, 9 | "allowSyntheticDefaultImports": true, 10 | "rootDir": "src", 11 | "outDir": "." 12 | }, 13 | "include": [ 14 | "src" 15 | ], 16 | "exclude": [ 17 | ] 18 | } -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:base" 5 | ] 6 | } 7 | --------------------------------------------------------------------------------