├── .editorconfig ├── .gitattributes ├── .github ├── logo-original.png ├── logo.webp └── workflows │ ├── release.yml │ └── test.yml ├── .gitignore ├── .nvmrc ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── package.json ├── pnpm-lock.yaml ├── src ├── cli.ts ├── rollup │ ├── configs │ │ ├── dts.ts │ │ └── pkg.ts │ ├── get-rollup-configs.ts │ ├── plugins │ │ ├── esbuild.ts │ │ ├── esm-inject-create-require.ts │ │ ├── externalize-node-builtins.ts │ │ ├── patch-binary.ts │ │ ├── resolve-tsconfig-paths.ts │ │ ├── resolve-typescript-mjs-cjs.ts │ │ └── strip-hashbang.ts │ └── types.ts ├── types.ts └── utils │ ├── clean-dist.ts │ ├── fs-exists.ts │ ├── get-source-path.ts │ ├── get-tsconfig.ts │ ├── local-typescript-loader.ts │ ├── log.ts │ ├── normalize-path.ts │ ├── parse-package-json │ ├── get-aliases.ts │ ├── get-export-entries.ts │ └── get-external-dependencies.ts │ ├── property-needs-quotes.ts │ └── read-package-json.ts ├── tests ├── fixtures.ts ├── index.ts ├── specs │ ├── builds │ │ ├── bin.ts │ │ ├── clean-dist.ts │ │ ├── dependencies.ts │ │ ├── env.ts │ │ ├── index.ts │ │ ├── minification.ts │ │ ├── output-commonjs.ts │ │ ├── output-dual.ts │ │ ├── output-module.ts │ │ ├── output-types.ts │ │ ├── package-exports.ts │ │ ├── package-imports.ts │ │ ├── sourcemap.ts │ │ ├── src-dist.ts │ │ ├── target.ts │ │ └── typescript.ts │ └── error-cases.ts ├── tsconfig.json └── utils.ts └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = tab 5 | end_of_line = lf 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | 10 | [*.yml] 11 | indent_style = space 12 | indent_size = 2 13 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | -------------------------------------------------------------------------------- /.github/logo-original.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/privatenumber/pkgroll/5a73adfa1f29ada16ec60f4a757a9ea98fe84fea/.github/logo-original.png -------------------------------------------------------------------------------- /.github/logo.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/privatenumber/pkgroll/5a73adfa1f29ada16ec60f4a757a9ea98fe84fea/.github/logo.webp -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: [master, develop] 6 | 7 | permissions: 8 | contents: write 9 | 10 | jobs: 11 | release: 12 | name: Release 13 | if: ( 14 | github.repository_owner == 'pvtnbr' && github.ref_name =='develop' 15 | ) || ( 16 | github.repository_owner == 'privatenumber' && github.ref_name =='master' 17 | ) 18 | runs-on: ubuntu-latest 19 | timeout-minutes: 10 20 | 21 | steps: 22 | - name: Checkout 23 | uses: actions/checkout@v4 24 | with: 25 | token: ${{ secrets.GH_TOKEN }} 26 | 27 | - name: Use Node.js 28 | uses: actions/setup-node@v4 29 | with: 30 | node-version-file: .nvmrc 31 | 32 | - name: Setup pnpm 33 | uses: pnpm/action-setup@v4 34 | with: 35 | run_install: true 36 | 37 | - name: Lint 38 | run: pnpm lint 39 | 40 | - name: Prerelease to GitHub 41 | if: github.repository_owner == 'pvtnbr' 42 | run: | 43 | git remote add public https://github.com/$(echo $GITHUB_REPOSITORY | sed "s/^pvtnbr/privatenumber/") 44 | git fetch public master 'refs/tags/*:refs/tags/*' 45 | git push --force --tags origin refs/remotes/public/master:refs/heads/master 46 | 47 | jq ' 48 | .publishConfig.registry = "https://npm.pkg.github.com" 49 | | .name = ("@" + env.GITHUB_REPOSITORY_OWNER + "/" + .name) 50 | | .repository = env.GITHUB_REPOSITORY 51 | | .release.branches = [ 52 | "master", 53 | { name: "develop", prerelease: "rc", channel: "latest" } 54 | ] 55 | ' package.json > _package.json 56 | mv _package.json package.json 57 | 58 | - name: Release 59 | env: 60 | GH_TOKEN: ${{ secrets.GH_TOKEN }} 61 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 62 | run: pnpm dlx semantic-release 63 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | on: 3 | push: 4 | branches: [master, develop] 5 | pull_request: 6 | branches: [master, develop, next] 7 | 8 | jobs: 9 | test: 10 | name: Test 11 | strategy: 12 | matrix: 13 | os: [ubuntu-latest, windows-latest] 14 | runs-on: ${{ matrix.os }} 15 | timeout-minutes: 10 16 | 17 | steps: 18 | - name: Checkout 19 | uses: actions/checkout@v4 20 | 21 | - name: Use Node.js 22 | uses: actions/setup-node@v4 23 | with: 24 | node-version-file: .nvmrc 25 | 26 | - name: Setup pnpm 27 | uses: pnpm/action-setup@v4 28 | with: 29 | run_install: true 30 | 31 | - name: Lint 32 | if: ${{ matrix.os == 'ubuntu-latest' }} 33 | run: pnpm lint 34 | 35 | - name: Type check 36 | if: ${{ matrix.os == 'ubuntu-latest' }} 37 | run: pnpm type-check 38 | 39 | - name: Test 40 | run: pnpm test 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # macOS 2 | .DS_Store 3 | 4 | # Logs 5 | logs 6 | *.log 7 | npm-debug.log* 8 | yarn-debug.log* 9 | yarn-error.log* 10 | lerna-debug.log* 11 | 12 | # Dependency directories 13 | /node_modules/ 14 | 15 | # Output of 'npm pack' 16 | *.tgz 17 | 18 | # dotenv environment variables file 19 | .env 20 | .env.test 21 | 22 | # Distribution 23 | dist 24 | 25 | # Eslint cache 26 | .eslintcache 27 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v20.11.1 2 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib", 3 | } 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Hiroki Osame 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 |

2 | 3 |

4 |

5 | pkgroll 6 |
7 | 8 |

9 | 10 | _pkgroll_ is a JavaScript package bundler powered by Rollup that automatically builds your package from entry-points defined in `package.json`. No config necessary! 11 | 12 | Write your code in TypeScript/ESM and run `pkgroll` to get ESM/CommonJS/.d.ts outputs! 13 | 14 | ### Features 15 | - ✅ `package.json#exports` to define entry-points 16 | - ✅ Dependency externalization 17 | - ✅ Minification 18 | - ✅ TypeScript support + `.d.ts` bundling 19 | - ✅ Watch mode 20 | - ✅ CLI outputs (auto hashbang insertion) 21 | 22 |
23 | 24 |

25 | 26 | 27 |

28 |

Already a sponsor? Join the discussion in the Development repo!

29 | 30 | ## Install 31 | ```sh 32 | npm install --save-dev pkgroll 33 | ``` 34 | 35 | ## Quick setup 36 | 1. Setup your project with source files in `src` and output in `dist` (configurable). 37 | 38 | 2. Define package entry-files in `package.json`. 39 | 40 | [These configurations](https://nodejs.org/api/packages.html#package-entry-points) are for Node.js to determine how to import the package. 41 | 42 | Pkgroll leverages the same configuration to determine how to build the package. 43 | 44 | ```json5 45 | { 46 | "name": "my-package", 47 | 48 | // Set "module" or "commonjs" (https://nodejs.org/api/packages.html#type) 49 | // "type": "module", 50 | 51 | // Define the output files 52 | "main": "./dist/index.cjs", 53 | "module": "./dist/index.mjs", 54 | "types": "./dist/index.d.cts", 55 | 56 | // Define output files for Node.js export maps (https://nodejs.org/api/packages.html#exports) 57 | "exports": { 58 | "require": { 59 | "types": "./dist/index.d.cts", 60 | "default": "./dist/index.cjs", 61 | }, 62 | "import": { 63 | "types": "./dist/index.d.mts", 64 | "default": "./dist/index.mjs", 65 | }, 66 | }, 67 | 68 | // bin files will be compiled to be executable with the Node.js hashbang 69 | "bin": "./dist/cli.js", 70 | 71 | // (Optional) Add a build script referencing `pkgroll` 72 | "scripts": { 73 | "build": "pkgroll", 74 | }, 75 | 76 | // ... 77 | } 78 | ``` 79 | 80 | Paths that start with `./dist/` are automatically mapped to files in the `./src/` directory. 81 | 82 | 3. Package roll! 83 | ```sh 84 | npm run build # or npx pkgroll 85 | ``` 86 | 87 | ## Usage 88 | 89 | ### Entry-points 90 | _Pkgroll_ parses package entry-points from `package.json` by reading properties `main`, `module`, `types`, and `exports`. 91 | 92 | The paths in `./dist` are mapped to paths in `./src` (configurable with `--src` and `--dist` flags) to determine bundle entry-points. 93 | 94 | ### Output formats 95 | _Pkgroll_ detects the format for each entry-point based on the file extension or the `package.json` property it's placed in, using the [same lookup logic as Node.js](https://nodejs.org/api/packages.html#determining-module-system). 96 | 97 | | `package.json` property | Output format | 98 | | - | - | 99 | | `main` | Auto-detect | 100 | | `module` | ESM
Note: This [unofficial property](https://stackoverflow.com/a/42817320/911407) is not supported by Node.js and is mainly used by bundlers. | 101 | | `types` | TypeScript declaration | 102 | | `exports` | Auto-detect | 103 | | `exports.require` | CommonJS | 104 | | `exports.import` | Auto-detect | 105 | | `exports.types` | TypeScript declaration | 106 | | `bin` | Auto-detect
Also patched to be executable with the Node.js hashbang. | 107 | 108 | _Auto-detect_ infers the type by extension or `package.json#type`: 109 | 110 | | Extension | Output format | 111 | | - | - | 112 | | `.cjs` | [CommonJS](https://nodejs.org/api/packages.html#:~:text=Files%20ending%20with%20.cjs%20are%20always%20loaded%20as%20CommonJS%20regardless%20of%20the%20nearest%20parent%20package.json) | 113 | | `.mjs` | [ECMAScript Modules](https://nodejs.org/api/modules.html#the-mjs-extension) | 114 | | `.js` | Determined by `package.json#type`, defaulting to CommonJS | 115 | 116 | 117 | ### Dependency bundling & externalization 118 | 119 | Packages to externalize are detected by reading dependency types in `package.json`. Only dependencies listed in `devDependencies` are bundled in. 120 | 121 | When generating type declarations (`.d.ts` files), this also bundles and tree-shakes type dependencies declared in `devDependencies` as well. 122 | 123 | ```json5 124 | // package.json 125 | { 126 | // ... 127 | 128 | "peerDependencies": { 129 | // Externalized 130 | }, 131 | "dependencies": { 132 | // Externalized 133 | }, 134 | "optionalDependencies": { 135 | // Externalized 136 | }, 137 | "devDependencies": { 138 | // Bundled 139 | }, 140 | } 141 | ``` 142 | 143 | ### Aliases 144 | 145 | #### Import map 146 | 147 | You can configure aliases using the [import map](https://nodejs.org/api/packages.html#imports) in `package.json#imports`. 148 | 149 | In Node.js, import mappings must start with `#` to indicate an internal [subpath import](https://nodejs.org/api/packages.html#subpath-imports). However, _Pkgroll_ allows defining aliases **without** the `#` prefix. 150 | 151 | > [!NOTE] 152 | > While Node.js supports conditional imports (e.g., different paths for Node.js vs. browsers), _Pkgroll_ does not. 153 | 154 | Example: 155 | 156 | ```json5 157 | { 158 | "imports": { 159 | // Alias '~utils' points to './src/utils.js' 160 | "~utils": "./src/utils.js", 161 | 162 | // Native Node.js subpath import (must use '#', can't reference './src') 163 | "#internal-package": "./vendors/package/index.js", 164 | }, 165 | } 166 | ``` 167 | 168 | #### Tsconfig paths 169 | 170 | You can also define aliases in `tsconfig.json` using `compilerOptions.paths`: 171 | 172 | ```json5 173 | { 174 | "compilerOptions": { 175 | "paths": { 176 | "@foo/*": [ 177 | "./src/foo/*", 178 | ], 179 | "~bar": [ 180 | "./src/bar/index.ts", 181 | ], 182 | }, 183 | }, 184 | } 185 | ``` 186 | 187 | > [!TIP] 188 | > The community is shifting towards using import maps (`imports`) as the source of truth for aliases because of their wider support across tools like Node.js, TypeScript, Vite, Webpack, and esbuild. 189 | 190 | ### Target 191 | 192 | _Pkgroll_ uses [esbuild](https://esbuild.github.io/) to handle TypeScript and JavaScript transformation and minification. 193 | 194 | The target specifies the environments the output should support. Depending on how new the target is, it can generate less code using newer syntax. Read more about it in the [esbuild docs](https://esbuild.github.io/api/#target). 195 | 196 | 197 | By default, the target is set to the version of Node.js used. It can be overwritten with the `--target` flag: 198 | 199 | ```sh 200 | pkgroll --target=es2020 --target=node14.18.0 201 | ``` 202 | 203 | It will also automatically detect and include the `target` specified in `tsconfig.json#compilerOptions`. 204 | 205 | 206 | #### Strip `node:` protocol 207 | Node.js builtin modules can be prefixed with the [`node:` protocol](https://nodejs.org/api/esm.html#node-imports) for explicitness: 208 | 209 | ```js 210 | import fs from 'node:fs/promises' 211 | ``` 212 | 213 | This is a new feature and may not work in older versions of Node.js. While you can opt out of using it, your dependencies may still be using it (example package using `node:`: [path-exists](https://github.com/sindresorhus/path-exists/blob/7c95f5c1f5f811c7f4dac78ab5b9e258491f03af/index.js#L1)). 214 | 215 | Pass in a Node.js target that that doesn't support it to strip the `node:` protocol from imports: 216 | 217 | ```sh 218 | pkgroll --target=node12.19 219 | ``` 220 | 221 | ### Custom `tsconfig.json` path 222 | 223 | By default, _Pkgroll_ looks for `tsconfig.json` configuration file in the current working directory. You can pass in a custom `tsconfig.json` path with the `--tsconfig` flag: 224 | 225 | ```sh 226 | pkgroll --tsconfig=tsconfig.build.json 227 | ``` 228 | 229 | ### Export condition 230 | 231 | Similarly to the target, the export condition specifies which fields to read from when evaluating [export](https://nodejs.org/api/packages.html#exports) and [import](https://nodejs.org/api/packages.html#imports) maps. 232 | 233 | For example, to simulate import resolutions in Node.js, pass in `node` as the export condition: 234 | ```sh 235 | pkgroll --export-condition=node 236 | ``` 237 | 238 | 239 | ### ESM ⇄ CJS interoperability 240 | 241 | Node.js ESM offers [interoperability with CommonJS](https://nodejs.org/api/esm.html#interoperability-with-commonjs) via [static analysis](https://github.com/nodejs/cjs-module-lexer). However, not all bundlers compile ESM to CJS syntax in a way that is statically analyzable. 242 | 243 | Because _pkgroll_ uses Rollup, it's able to produce CJS modules that are minimal and interoperable with Node.js ESM. 244 | 245 | This means you can technically output in CommonJS to get ESM and CommonJS support. 246 | 247 | #### `require()` in ESM 248 | Sometimes it's useful to use `require()` or `require.resolve()` in ESM. ESM code that uses `require()` can be seamlessly compiled to CommonJS, but when compiling to ESM, Node.js will error because `require` doesn't exist in the module scope. 249 | 250 | When compiling to ESM, _Pkgroll_ detects `require()` usages and shims it with [`createRequire(import.meta.url)`](https://nodejs.org/api/module.html#modulecreaterequirefilename). 251 | 252 | ### Environment variables 253 | Pass in compile-time environment variables with the `--env` flag. 254 | 255 | This will replace all instances of `process.env.NODE_ENV` with `'production'` and remove unused code: 256 | ```sh 257 | pkgroll --env.NODE_ENV=production 258 | ``` 259 | 260 | ### Minification 261 | Pass in the `--minify` flag to minify assets. 262 | ```sh 263 | pkgroll --minify 264 | ``` 265 | 266 | ### Watch mode 267 | Run the bundler in watch mode during development: 268 | ```sh 269 | pkgroll --watch 270 | ``` 271 | 272 | ### Clean dist 273 | Clean dist directory before bundling: 274 | ```sh 275 | pkgroll --clean-dist 276 | ``` 277 | 278 | ### Source maps 279 | Pass in the `--sourcemap` flag to emit a source map file: 280 | 281 | ```sh 282 | pkgroll --sourcemap 283 | ``` 284 | 285 | Or to inline them in the distribution files: 286 | ```sh 287 | pkgroll --sourcemap=inline 288 | ``` 289 | 290 | ## Dev vs Prod config 291 | 292 | In some cases, it makes sense to use different `package.json` field values for the published environment. You can achieve this by using the [`publishConfig`](https://pnpm.io/package_json#publishconfig) field (extended by pnpm). This allows you to override specific fields during publication with a clean separation of concerns. 293 | 294 | The following fields can be overridden using `publishConfig`: 295 | - `bin` 296 | - `main` 297 | - `exports` 298 | - `types` 299 | - `module` 300 | 301 | ## FAQ 302 | 303 | ### Why bundle with Rollup? 304 | [Rollup](https://rollupjs.org/) has the best tree-shaking performance, outputs simpler code, and produces seamless CommonJS and ESM formats (minimal interop code). Notably, CJS outputs generated by Rollup supports named exports so it can be parsed by Node.js ESM. TypeScript & minification transformations are handled by [esbuild](https://esbuild.github.io/) for speed. 305 | 306 | ### Why bundle Node.js packages? 307 | 308 | - **ESM and CommonJS outputs** 309 | 310 | As the Node.js ecosystem migrates to [ESM](https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c), there will be both ESM and CommonJS users. A bundler helps accommodate both distribution types. 311 | 312 | - **Dependency bundling** yields smaller and faster installation. 313 | 314 | Tree-shaking only pulls in used code from dependencies, preventing unused code and unnecessary files (eg. `README.md`, `package.json`, etc.) from getting downloaded. 315 | 316 | Removing dependencies also eliminates dependency tree traversal, which is [one of the biggest bottlenecks](https://dev.to/atian25/in-depth-of-tnpm-rapid-mode-how-could-we-fast-10s-than-pnpm-3bpp#:~:text=The%20first%20optimization%20comes%20from%20%27dependencies%20graph%27%3A). 317 | 318 | - **Inadvertent breaking changes** 319 | 320 | Dependencies can introduce breaking changes due to a discrepancy in environment support criteria, by accident, or in rare circumstances, [maliciously](https://snyk.io/blog/peacenotwar-malicious-npm-node-ipc-package-vulnerability/). 321 | 322 | Compiling dependencies will make sure new syntax & features are downgraded to support the same environments. And also prevent any unexpected changes from sneaking in during installation. 323 | 324 | 325 | - **Type dependencies** must be declared in the `dependencies` object in `package.json`, instead of `devDependencies`, to be resolved by the consumer. 326 | 327 | This may seem counterintuitive because types are a development enhancement. By bundling them in with your package, you remove the need for an external type dependency. Additionally, bundling only keeps the types that are actually used which helps minimize unnecessary bloat. 328 | 329 | - **Minification** strips dead-code, comments, white-space, and shortens variable names. 330 | 331 | ### How does it compare to tsup? 332 | 333 | They are similar bundlers, but I think the main differences are: 334 | 335 | - _pkgroll_ is zero-config. It reads the entry-points declared in your `package.json` to determine how to bundle the package. _tsup_ requires manual configuration. 336 | 337 | - _pkgroll_ is a thin abstraction over Rollup (just smartly configures it). Similarly, _tsup_ is a thin abstraction over esbuild. _pkgroll_ also uses esbuild for transformations & minification as a Rollup plugin, but the bundling & tree-shaking is done by Rollup (which is [known to output best/cleanest code](#why-bundle-with-rollup)). However, when _tsup_ emits type declaration, it also uses Rollup which negates the performance benefits from using esbuild. 338 | 339 | - IIRC because _tsup_ uses esbuild, the ESM to CJS compilation wasn't great compared to Rollups. As a package maintainer who wants to support both ESM and CJS exports, this was one of the biggest limitations of using _tsup_ (which compelled me to develop _pkgroll_). 340 | 341 | ## Sponsors 342 | 343 |

344 | 345 | 346 | 347 |

348 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pkgroll", 3 | "version": "0.0.0-semantic-release", 4 | "description": "Zero-config rollup bundler", 5 | "keywords": [ 6 | "zero config", 7 | "rollup", 8 | "package.json", 9 | "exports", 10 | "esm", 11 | "cjs", 12 | "commonjs", 13 | "typescript", 14 | "declaration" 15 | ], 16 | "license": "MIT", 17 | "repository": "privatenumber/pkgroll", 18 | "funding": "https://github.com/privatenumber/pkgroll?sponsor=1", 19 | "author": { 20 | "name": "Hiroki Osame", 21 | "email": "hiroki.osame@gmail.com" 22 | }, 23 | "files": [ 24 | "dist" 25 | ], 26 | "type": "module", 27 | "bin": "./dist/cli.mjs", 28 | "packageManager": "pnpm@9.4.0", 29 | "scripts": { 30 | "build": "tsx src/cli.ts --minify --target node18", 31 | "test": "pnpm build && tsx tests/index.ts", 32 | "lint": "lintroll --cache --node --ignore-pattern tests/fixture-package .", 33 | "type-check": "tsc", 34 | "prepack": "pnpm build && clean-pkg-json" 35 | }, 36 | "engines": { 37 | "node": ">=18" 38 | }, 39 | "dependencies": { 40 | "@rollup/plugin-alias": "^5.1.1", 41 | "@rollup/plugin-commonjs": "^28.0.2", 42 | "@rollup/plugin-dynamic-import-vars": "^2.1.5", 43 | "@rollup/plugin-inject": "^5.0.5", 44 | "@rollup/plugin-json": "^6.1.0", 45 | "@rollup/plugin-node-resolve": "^16.0.0", 46 | "@rollup/pluginutils": "^5.1.4", 47 | "esbuild": "^0.25.1", 48 | "magic-string": "^0.30.17", 49 | "rollup": "^4.34.6", 50 | "rollup-pluginutils": "^2.8.2" 51 | }, 52 | "peerDependencies": { 53 | "typescript": "^4.1 || ^5.0" 54 | }, 55 | "peerDependenciesMeta": { 56 | "typescript": { 57 | "optional": true 58 | } 59 | }, 60 | "devDependencies": { 61 | "@types/node": "^22.13.1", 62 | "@types/react": "^18.3.18", 63 | "clean-pkg-json": "^1.2.0", 64 | "cleye": "^1.3.3", 65 | "estree-walker": "^3.0.3", 66 | "execa": "9.3.0", 67 | "fs-fixture": "^2.7.0", 68 | "get-node": "^15.0.1", 69 | "get-tsconfig": "^4.10.0", 70 | "kolorist": "^1.8.0", 71 | "lintroll": "^1.15.0", 72 | "manten": "^1.3.0", 73 | "outdent": "^0.8.0", 74 | "react": "^18.3.1", 75 | "rollup-plugin-dts": "6.1.1", 76 | "tsx": "^4.19.2", 77 | "type-fest": "^4.33.0", 78 | "typescript": "^5.7.3" 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/cli.ts: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs'; 2 | import { cli } from 'cleye'; 3 | import { rollup, watch } from 'rollup'; 4 | import { version } from '../package.json'; 5 | import { readPackageJson } from './utils/read-package-json.js'; 6 | import { parseCliInputFlag, getExportEntries } from './utils/parse-package-json/get-export-entries.js'; 7 | import { getAliases } from './utils/parse-package-json/get-aliases.js'; 8 | import { normalizePath } from './utils/normalize-path.js'; 9 | import { getSourcePath } from './utils/get-source-path.js'; 10 | import { getRollupConfigs } from './rollup/get-rollup-configs.js'; 11 | import { getTsconfig } from './utils/get-tsconfig'; 12 | import { log } from './utils/log.js'; 13 | import { cleanDist } from './utils/clean-dist.js'; 14 | 15 | const { stringify } = JSON; 16 | 17 | const argv = cli({ 18 | name: 'pkgroll', 19 | 20 | version, 21 | 22 | flags: { 23 | input: { 24 | type: [parseCliInputFlag], 25 | alias: 'i', 26 | description: 'Dist paths for source files to bundle (Only use if you cannot use package.json entries)', 27 | }, 28 | src: { 29 | type: String, 30 | description: 'Source directory', 31 | default: './src', 32 | }, 33 | dist: { 34 | type: String, 35 | description: 'Distribution directory', 36 | default: './dist', 37 | }, 38 | minify: { 39 | type: Boolean, 40 | description: 'Minify output', 41 | alias: 'm', 42 | default: false, 43 | }, 44 | target: { 45 | type: [String], 46 | default: [`node${process.versions.node}`], 47 | description: 'Environments to support. `target` in tsconfig.json is automatically added. Defaults to the current Node.js version.', 48 | alias: 't', 49 | }, 50 | tsconfig: { 51 | type: String, 52 | description: 'Custom tsconfig.json file path', 53 | alias: 'p', 54 | }, 55 | watch: { 56 | type: Boolean, 57 | description: 'Watch mode', 58 | alias: 'w', 59 | default: false, 60 | }, 61 | env: { 62 | type: [ 63 | (flagValue: string) => { 64 | const [key, value] = flagValue.split('='); 65 | return { 66 | key, 67 | value, 68 | }; 69 | }, 70 | ], 71 | description: 'Compile-time environment variables (eg. --env.NODE_ENV=production)', 72 | }, 73 | 74 | // TODO: rename to conditions and -C flag like Node.js 75 | exportCondition: { 76 | type: [String], 77 | description: 'Export conditions for resolving dependency export and import maps (eg. --export-condition=node)', 78 | }, 79 | sourcemap: { 80 | type: (flagValue: string) => { 81 | if (flagValue === '') { 82 | return true; 83 | } 84 | if (flagValue === 'inline') { 85 | return flagValue; 86 | } 87 | 88 | throw new Error(`Invalid sourcemap option ${stringify(flagValue)}`); 89 | }, 90 | description: 'Sourcemap generation. Provide `inline` option for inline sourcemap (eg. --sourcemap, --sourcemap=inline)', 91 | }, 92 | cleanDist: { 93 | type: Boolean, 94 | description: 'Clean dist before bundling', 95 | default: false, 96 | }, 97 | }, 98 | 99 | help: { 100 | description: 'Minimalistic package bundler', 101 | render: (nodes, renderers) => { 102 | renderers.flagOperator = flagData => ( 103 | (flagData.name === 'env') 104 | ? '.key=' 105 | : ' ' 106 | ); 107 | 108 | return renderers.render(nodes); 109 | }, 110 | }, 111 | }); 112 | 113 | const cwd = process.cwd(); 114 | 115 | /** 116 | * The sourcepath may be a symlink. 117 | * In the tests, the temp directory is a symlink: 118 | * /var/folders/hl/ -> /private/var/folders/hl/ 119 | */ 120 | const sourcePath = normalizePath(argv.flags.src, true); 121 | const distPath = normalizePath(argv.flags.dist, true); 122 | 123 | const tsconfig = getTsconfig(argv.flags.tsconfig); 124 | const tsconfigTarget = tsconfig?.config.compilerOptions?.target; 125 | if (tsconfigTarget) { 126 | argv.flags.target.push(tsconfigTarget); 127 | } 128 | 129 | (async () => { 130 | const packageJson = await readPackageJson(cwd); 131 | 132 | let exportEntries = getExportEntries(packageJson); 133 | 134 | const cliInputs = argv.flags.input; 135 | if (cliInputs.length > 0) { 136 | const packageType = packageJson.type ?? 'commonjs'; 137 | exportEntries.push(...cliInputs.map((input) => { 138 | if (!input.type) { 139 | input.type = packageType; 140 | } 141 | return input; 142 | })); 143 | } 144 | 145 | exportEntries = exportEntries.filter((entry) => { 146 | const validPath = entry.outputPath.startsWith(distPath); 147 | 148 | if (!validPath) { 149 | console.warn(`Ignoring entry outside of ${distPath} directory: package.json#${entry.from}=${stringify(entry.outputPath)}`); 150 | } 151 | 152 | return validPath; 153 | }); 154 | 155 | if (exportEntries.length === 0) { 156 | throw new Error('No export entries found in package.json'); 157 | } 158 | 159 | const sourcePaths = await Promise.all(exportEntries.map(async exportEntry => ({ 160 | ...(await getSourcePath(exportEntry, sourcePath, distPath)), 161 | exportEntry, 162 | }))); 163 | 164 | const rollupConfigs = await getRollupConfigs( 165 | 166 | /** 167 | * Resolve symlink in source path. 168 | * 169 | * Tests since symlinks because tmpdir is a symlink: 170 | * /var/ -> /private/var/ 171 | */ 172 | normalizePath(fs.realpathSync.native(sourcePath), true), 173 | distPath, 174 | sourcePaths, 175 | argv.flags, 176 | getAliases(packageJson, cwd), 177 | packageJson, 178 | tsconfig, 179 | ); 180 | 181 | if (argv.flags.cleanDist) { 182 | /** 183 | * Typically, something like this would be implemented as a plugin, so it only 184 | * deletes what it needs to but pkgroll runs multiple builds (e.g. d.ts, mjs, etc) 185 | * so as a plugin, it won't be aware of the files emitted by other builds 186 | */ 187 | await cleanDist(distPath); 188 | } 189 | 190 | if (argv.flags.watch) { 191 | log('Watch initialized'); 192 | 193 | rollupConfigs.map(async (rollupConfig) => { 194 | const watcher = watch(rollupConfig); 195 | 196 | watcher.on('event', async (event) => { 197 | if (event.code === 'BUNDLE_START') { 198 | log('Building', ...(Array.isArray(event.input) ? event.input : [event.input])); 199 | } 200 | 201 | if (event.code === 'BUNDLE_END') { 202 | await Promise.all(rollupConfig.output.map( 203 | outputOption => event.result.write(outputOption), 204 | )); 205 | 206 | log('Built', ...(Array.isArray(event.input) ? event.input : [event.input])); 207 | } 208 | 209 | if (event.code === 'ERROR') { 210 | log('Error:', event.error.message); 211 | } 212 | }); 213 | }); 214 | } else { 215 | await Promise.all( 216 | rollupConfigs.map(async (rollupConfig) => { 217 | const bundle = await rollup(rollupConfig); 218 | 219 | return Promise.all(rollupConfig.output.map( 220 | outputOption => bundle.write(outputOption), 221 | )); 222 | }), 223 | ); 224 | } 225 | })().catch((error) => { 226 | console.error(error); 227 | process.exit(1); 228 | }); 229 | -------------------------------------------------------------------------------- /src/rollup/configs/dts.ts: -------------------------------------------------------------------------------- 1 | import type { RollupOptions, Plugin } from 'rollup'; 2 | import type { TsConfigResult } from 'get-tsconfig'; 3 | import { externalizeNodeBuiltins } from '../plugins/externalize-node-builtins.js'; 4 | import { resolveTypescriptMjsCts } from '../plugins/resolve-typescript-mjs-cjs.js'; 5 | import { resolveTsconfigPaths } from '../plugins/resolve-tsconfig-paths.js'; 6 | import type { Options, Output } from '../types.js'; 7 | 8 | export const getDtsConfig = async ( 9 | options: Options, 10 | tsconfig: TsConfigResult | null, 11 | ) => { 12 | const [dts, ts] = await Promise.all([ 13 | import('rollup-plugin-dts'), 14 | import('../../utils/local-typescript-loader.js'), 15 | ]); 16 | return { 17 | input: [] as string[], 18 | preserveEntrySignatures: 'strict' as const, 19 | plugins: [ 20 | externalizeNodeBuiltins(options), 21 | ...( 22 | tsconfig 23 | ? [resolveTsconfigPaths(tsconfig)] 24 | : [] 25 | ), 26 | resolveTypescriptMjsCts(), 27 | dts.default({ 28 | respectExternal: true, 29 | 30 | /** 31 | * https://github.com/privatenumber/pkgroll/pull/54 32 | * 33 | * I think this is necessary because TypeScript's composite requires 34 | * that all files are passed in via `include`. However, it seems that 35 | * rollup-plugin-dts doesn't read or relay the `include` option in tsconfig. 36 | * 37 | * For now, simply disabling composite does the trick since it doesn't seem 38 | * necessary for dts bundling. 39 | * 40 | * One concern here is that this overwrites the compilerOptions. According to 41 | * the rollup-plugin-dts docs, it reads from baseUrl and paths. 42 | */ 43 | compilerOptions: { 44 | composite: false, 45 | preserveSymlinks: false, 46 | module: ts.default.ModuleKind.Preserve, 47 | moduleResolution: ts.default.ModuleResolutionKind.Bundler, 48 | }, 49 | tsconfig: tsconfig?.path, 50 | }) as Plugin, 51 | ], 52 | output: [] as unknown as Output, 53 | external: [] as (string | RegExp)[], 54 | } satisfies RollupOptions; 55 | }; 56 | -------------------------------------------------------------------------------- /src/rollup/configs/pkg.ts: -------------------------------------------------------------------------------- 1 | import type { RollupOptions } from 'rollup'; 2 | import type { TransformOptions } from 'esbuild'; 3 | import nodeResolve from '@rollup/plugin-node-resolve'; 4 | import commonjs from '@rollup/plugin-commonjs'; 5 | import json from '@rollup/plugin-json'; 6 | import alias from '@rollup/plugin-alias'; 7 | import dynamicImportVars from '@rollup/plugin-dynamic-import-vars'; 8 | import type { TsConfigResult } from 'get-tsconfig'; 9 | import type { AliasMap } from '../../types.js'; 10 | import { esbuildTransform, esbuildMinify } from '../plugins/esbuild.js'; 11 | import { externalizeNodeBuiltins } from '../plugins/externalize-node-builtins.js'; 12 | import { patchBinary } from '../plugins/patch-binary.js'; 13 | import { resolveTypescriptMjsCts } from '../plugins/resolve-typescript-mjs-cjs.js'; 14 | import { resolveTsconfigPaths } from '../plugins/resolve-tsconfig-paths.js'; 15 | import { stripHashbang } from '../plugins/strip-hashbang.js'; 16 | import { esmInjectCreateRequire } from '../plugins/esm-inject-create-require.js'; 17 | import type { Options, EnvObject, Output } from '../types.js'; 18 | 19 | export const getPkgConfig = ( 20 | options: Options, 21 | aliases: AliasMap, 22 | env: EnvObject, 23 | executablePaths: string[], 24 | tsconfig: TsConfigResult | null, 25 | ) => { 26 | const esbuildConfig: TransformOptions = { 27 | target: options.target, 28 | tsconfigRaw: tsconfig?.config, 29 | define: env, 30 | }; 31 | 32 | return { 33 | input: [] as string[], 34 | preserveEntrySignatures: 'strict' as const, 35 | plugins: [ 36 | externalizeNodeBuiltins(options), 37 | ...( 38 | tsconfig 39 | ? [resolveTsconfigPaths(tsconfig)] 40 | : [] 41 | ), 42 | resolveTypescriptMjsCts(), 43 | alias({ 44 | entries: aliases, 45 | }), 46 | nodeResolve({ 47 | extensions: ['.mjs', '.js', '.ts', '.jsx', '.tsx', '.json'], 48 | exportConditions: options.exportCondition, 49 | }), 50 | stripHashbang(), 51 | json(), 52 | esbuildTransform(esbuildConfig), 53 | commonjs({ 54 | ignoreDynamicRequires: true, 55 | extensions: ['.js', '.ts', '.jsx', '.tsx'], 56 | transformMixedEsModules: true, 57 | }), 58 | dynamicImportVars({ 59 | warnOnError: true, 60 | }), 61 | esmInjectCreateRequire(), 62 | ...( 63 | options.minify 64 | ? [esbuildMinify(esbuildConfig)] 65 | : [] 66 | ), 67 | patchBinary(executablePaths), 68 | ], 69 | output: [] as unknown as Output, 70 | external: [] as (string | RegExp)[], 71 | } satisfies RollupOptions; 72 | }; 73 | -------------------------------------------------------------------------------- /src/rollup/get-rollup-configs.ts: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs'; 2 | import path from 'node:path'; 3 | import type { OutputOptions } from 'rollup'; 4 | import type { PackageJson } from 'type-fest'; 5 | import type { TsConfigResult } from 'get-tsconfig'; 6 | import type { ExportEntry, AliasMap } from '../types.js'; 7 | import { getExternalDependencies } from '../utils/parse-package-json/get-external-dependencies.js'; 8 | import type { EnvObject, Options } from './types.js'; 9 | import { getPkgConfig } from './configs/pkg.js'; 10 | import { getDtsConfig } from './configs/dts.js'; 11 | 12 | const stripQuery = (url: string) => url.split('?')[0]; 13 | 14 | type RollupConfigs = { 15 | dts?: Awaited>; 16 | pkg?: ReturnType; 17 | }; 18 | 19 | export const getRollupConfigs = async ( 20 | sourceDirectoryPath: string, 21 | distributionDirectoryPath: string, 22 | inputs: { 23 | input: string; 24 | srcExtension: string; 25 | distExtension: string; 26 | exportEntry: ExportEntry; 27 | }[], 28 | flags: Options, 29 | aliases: AliasMap, 30 | packageJson: PackageJson, 31 | tsconfig: TsConfigResult | null, 32 | ) => { 33 | const executablePaths = inputs 34 | .filter(({ exportEntry }) => exportEntry.isExecutable) 35 | .map(({ exportEntry }) => exportEntry.outputPath); 36 | 37 | const configs: RollupConfigs = Object.create(null); 38 | 39 | const env: EnvObject = Object.fromEntries( 40 | flags.env.map(({ key, value }) => [`process.env.${key}`, JSON.stringify(value)]), 41 | ); 42 | 43 | const externalDependencies = getExternalDependencies(packageJson, aliases); 44 | const externalTypeDependencies = getExternalDependencies(packageJson, aliases, true); 45 | 46 | for (const { 47 | input, srcExtension, distExtension, exportEntry, 48 | } of inputs) { 49 | if (exportEntry.type === 'types') { 50 | let config = configs.dts; 51 | 52 | if (!config) { 53 | config = await getDtsConfig(flags, tsconfig); 54 | config.external = externalTypeDependencies; 55 | configs.dts = config; 56 | } 57 | 58 | if (!config.input.includes(input)) { 59 | config.input.push(input); 60 | } 61 | 62 | config.output.push({ 63 | dir: distributionDirectoryPath, 64 | 65 | /** 66 | * Preserve source path in dist path 67 | * realpath used for few reasons: 68 | * - dts plugin resolves paths to be absolute anyway, but doesn't resolve symlinks 69 | * - input may be an absolute symlink path 70 | * - test tmpdir is a symlink: /var/ -> /private/var/ 71 | */ 72 | entryFileNames: chunk => ( 73 | fs.realpathSync.native(stripQuery(chunk.facadeModuleId!)) 74 | .slice(sourceDirectoryPath.length, -srcExtension.length) 75 | + distExtension 76 | ), 77 | 78 | exports: 'auto', 79 | format: 'esm', 80 | }); 81 | 82 | continue; 83 | } 84 | 85 | let config = configs.pkg; 86 | if (!config) { 87 | config = getPkgConfig( 88 | flags, 89 | aliases, 90 | env, 91 | executablePaths, 92 | tsconfig, 93 | ); 94 | config.external = externalDependencies; 95 | configs.pkg = config; 96 | } 97 | 98 | if (!config.input.includes(input)) { 99 | config.input.push(input); 100 | } 101 | 102 | const outputs = config.output; 103 | const extension = path.extname(exportEntry.outputPath); 104 | const key = `${exportEntry.type}-${extension}`; 105 | if (!outputs[key]) { 106 | const outputOptions: OutputOptions = { 107 | dir: distributionDirectoryPath, 108 | exports: 'auto', 109 | format: exportEntry.type, 110 | chunkFileNames: `[name]-[hash]${extension}`, 111 | sourcemap: flags.sourcemap, 112 | 113 | /** 114 | * Preserve source path in dist path 115 | * realpath used for few reasons: 116 | * - dts plugin resolves paths to be absolute anyway, but doesn't resolve symlinks 117 | * - input may be an absolute symlink path 118 | * - test tmpdir is a symlink: /var/ -> /private/var/ 119 | */ 120 | entryFileNames: (chunk) => { 121 | const realPath = fs.realpathSync.native(stripQuery(chunk.facadeModuleId!)); 122 | const relativePath = realPath.slice(sourceDirectoryPath.length); 123 | const filePath = path.posix.join(path.dirname(relativePath), chunk.name); 124 | return filePath + distExtension; 125 | }, 126 | }; 127 | 128 | outputs.push(outputOptions); 129 | outputs[key] = outputOptions; 130 | } 131 | } 132 | 133 | return Object.values(configs); 134 | }; 135 | -------------------------------------------------------------------------------- /src/rollup/plugins/esbuild.ts: -------------------------------------------------------------------------------- 1 | import type { Plugin, InternalModuleFormat } from 'rollup'; 2 | import { createFilter } from '@rollup/pluginutils'; 3 | import { transform, type TransformOptions, type Format } from 'esbuild'; 4 | 5 | export const esbuildTransform = ( 6 | options?: TransformOptions, 7 | ): Plugin => { 8 | const filter = createFilter( 9 | /\.([cm]?[jt]s|[jt]sx)$/, 10 | ); 11 | 12 | return { 13 | name: 'esbuild-transform', 14 | transform: async (code, id) => { 15 | if (!filter(id)) { 16 | return null; 17 | } 18 | 19 | const result = await transform(code, { 20 | ...options, 21 | 22 | supported: { 23 | /** 24 | * esbuild is used for TS, syntax lowering, & define, but 25 | * we'll ignore import.meta as it injects a polyfill that 26 | * may break if the output is ESM 27 | * 28 | * https://esbuild.github.io/try/#dAAwLjI1LjAAe3RhcmdldDogWydlczIwMTcnXX0AY29uc29sZS5sb2coaW1wb3J0Lm1ldGEudXJsKQ 29 | */ 30 | 'import-meta': true, 31 | }, 32 | 33 | loader: 'default', 34 | 35 | // https://github.com/evanw/esbuild/issues/1932#issuecomment-1013380565 36 | sourcefile: id.replace(/\.[cm]ts/, '.ts'), 37 | }); 38 | 39 | return { 40 | code: result.code, 41 | map: result.map || null, 42 | }; 43 | }, 44 | }; 45 | }; 46 | 47 | const getEsbuildFormat = ( 48 | rollupFormat: InternalModuleFormat, 49 | ): Format | undefined => { 50 | if (rollupFormat === 'es') { 51 | return 'esm'; 52 | } 53 | 54 | if (rollupFormat === 'cjs' || rollupFormat === 'iife') { 55 | return rollupFormat; 56 | } 57 | }; 58 | 59 | export const esbuildMinify = ( 60 | options?: TransformOptions, 61 | ): Plugin => ({ 62 | name: 'esbuild-minify', 63 | renderChunk: async (code, _, rollupOptions) => { 64 | const result = await transform(code, { 65 | /** 66 | * `target` is used to prevent new minification syntax 67 | * from being used. 68 | * 69 | * https://github.com/evanw/esbuild/releases/tag/v0.14.25#:~:text=Minification%20now%20takes%20advantage%20of%20the%20%3F.%20operator 70 | */ 71 | ...options, 72 | 73 | // https://github.com/egoist/rollup-plugin-esbuild/issues/317 74 | format: getEsbuildFormat(rollupOptions.format), 75 | 76 | minify: true, 77 | 78 | keepNames: true, 79 | }); 80 | 81 | return { 82 | code: result.code, 83 | map: result.map || null, 84 | }; 85 | }, 86 | }); 87 | -------------------------------------------------------------------------------- /src/rollup/plugins/esm-inject-create-require.ts: -------------------------------------------------------------------------------- 1 | import MagicString from 'magic-string'; 2 | import { attachScopes, type AttachedScope } from 'rollup-pluginutils'; 3 | import { walk } from 'estree-walker'; 4 | import type { Plugin } from 'rollup'; 5 | 6 | export const esmInjectCreateRequire = (): Plugin => { 7 | const createRequire = 'import{createRequire as _pkgrollCR}from"node:module";const require=_pkgrollCR(import.meta.url);'; 8 | 9 | return { 10 | name: 'esmInjectCreateRequire', 11 | renderChunk(code, _chunk, options) { 12 | if ( 13 | options.format !== 'es' 14 | || !/\brequire\b/.test(code) 15 | ) { 16 | return null; 17 | } 18 | 19 | const ast = this.parse(code); 20 | let currentScope = attachScopes(ast, 'scope'); 21 | let injectionNeeded = false; 22 | 23 | walk(ast, { 24 | enter(node, parent) { 25 | // Not all nodes have scopes 26 | if (node.scope) { 27 | currentScope = node.scope as AttachedScope; 28 | } 29 | 30 | if (node.type !== 'Identifier' || node.name !== 'require') { 31 | return; 32 | } 33 | 34 | if ( 35 | parent?.type === 'Property' 36 | && parent.key === node 37 | && !parent.compute 38 | ) { 39 | return; 40 | } 41 | 42 | // If the current scope (or its parents) does not contain 'require' 43 | if (!currentScope.contains('require')) { 44 | injectionNeeded = true; 45 | 46 | // No need to continue if one instance is found 47 | this.skip(); 48 | } 49 | }, 50 | leave: (node) => { 51 | if (node.scope) { 52 | currentScope = currentScope.parent!; 53 | } 54 | }, 55 | }); 56 | 57 | if (!injectionNeeded) { 58 | return null; 59 | } 60 | 61 | const magicString = new MagicString(code); 62 | magicString.prepend(createRequire); 63 | return { 64 | code: magicString.toString(), 65 | map: magicString.generateMap({ hires: true }), 66 | }; 67 | }, 68 | }; 69 | }; 70 | -------------------------------------------------------------------------------- /src/rollup/plugins/externalize-node-builtins.ts: -------------------------------------------------------------------------------- 1 | import { builtinModules } from 'node:module'; 2 | import type { Plugin } from 'rollup'; 3 | 4 | type Semver = [number, number, number]; 5 | 6 | const compareSemver = ( 7 | semverA: Semver, 8 | semverB: Semver, 9 | ) => ( 10 | semverA[0] - semverB[0] 11 | || semverA[1] - semverB[1] 12 | || semverA[2] - semverB[2] 13 | ); 14 | 15 | /** 16 | * Implemented as a plugin instead of the external API 17 | * to support altering the import specifier to remove `node:` 18 | * 19 | * Alternatively, we can create a mapping via output.paths 20 | * but this seems cleaner 21 | */ 22 | export const externalizeNodeBuiltins = ({ target }: { 23 | target: string[]; 24 | }): Plugin => { 25 | /** 26 | * Only remove protocol if a Node.js version that doesn't 27 | * support it is specified. 28 | */ 29 | const stripNodeProtocol = target.some((platform) => { 30 | platform = platform.trim(); 31 | 32 | // Ignore non Node platforms 33 | if (!platform.startsWith('node')) { 34 | return; 35 | } 36 | 37 | const parsedVersion = platform.slice(4).split('.').map(Number); 38 | const semver: Semver = [ 39 | parsedVersion[0], 40 | parsedVersion[1] ?? 0, 41 | parsedVersion[2] ?? 0, 42 | ]; 43 | 44 | return !( 45 | 46 | // 12.20.0 <= x < 13.0.0 47 | ( 48 | compareSemver(semver, [12, 20, 0]) >= 0 49 | && compareSemver(semver, [13, 0, 0]) < 0 50 | ) 51 | 52 | // 14.13.1 <= x 53 | || compareSemver(semver, [14, 13, 1]) >= 0 54 | ); 55 | }); 56 | 57 | return { 58 | name: 'externalize-node-builtins', 59 | resolveId: (id) => { 60 | const hasNodeProtocol = id.startsWith('node:'); 61 | if (stripNodeProtocol && hasNodeProtocol) { 62 | id = id.slice(5); 63 | } 64 | 65 | if (builtinModules.includes(id) || hasNodeProtocol) { 66 | return { 67 | id, 68 | external: true, 69 | }; 70 | } 71 | }, 72 | }; 73 | }; 74 | -------------------------------------------------------------------------------- /src/rollup/plugins/patch-binary.ts: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs'; 2 | import path from 'node:path'; 3 | import type { 4 | Plugin, RenderedChunk, OutputChunk, SourceMapInput, 5 | } from 'rollup'; 6 | import MagicString from 'magic-string'; 7 | 8 | export const patchBinary = ( 9 | executablePaths: string[], 10 | ): Plugin => ({ 11 | name: 'patch-binary', 12 | 13 | renderChunk: (code, chunk, outputOptions) => { 14 | if (!chunk.isEntry || !chunk.facadeModuleId) { 15 | return; 16 | } 17 | 18 | const entryFileNames = outputOptions.entryFileNames as (chunk: RenderedChunk) => string; 19 | const outputPath = `./${path.posix.join(outputOptions.dir!, entryFileNames(chunk))}`; 20 | 21 | if (executablePaths.includes(outputPath)) { 22 | const transformed = new MagicString(code); 23 | transformed.prepend('#!/usr/bin/env node\n'); 24 | 25 | return { 26 | code: transformed.toString(), 27 | map: ( 28 | outputOptions.sourcemap 29 | ? transformed.generateMap({ hires: true }) as SourceMapInput 30 | : undefined 31 | ), 32 | }; 33 | } 34 | }, 35 | 36 | writeBundle: async (outputOptions, bundle) => { 37 | const entryFileNames = outputOptions.entryFileNames as (chunk: OutputChunk) => string; 38 | 39 | const chmodFiles = Object.values(bundle).map(async (chunk) => { 40 | const outputChunk = chunk as OutputChunk; 41 | 42 | if (outputChunk.isEntry && outputChunk.facadeModuleId) { 43 | const outputPath = `./${path.posix.join(outputOptions.dir!, entryFileNames(outputChunk))}`; 44 | await fs.promises.chmod(outputPath, 0o755); 45 | } 46 | }); 47 | 48 | await Promise.all(chmodFiles); 49 | }, 50 | }); 51 | -------------------------------------------------------------------------------- /src/rollup/plugins/resolve-tsconfig-paths.ts: -------------------------------------------------------------------------------- 1 | import { createPathsMatcher, type TsConfigResult } from 'get-tsconfig'; 2 | import type { Plugin } from 'rollup'; 3 | 4 | const name = 'resolve-tsconfig-paths'; 5 | 6 | const isRelative = (filePath: string) => filePath[0] === '.'; 7 | const isAbsolute = (filePath: string) => filePath[0] === '/' || /^[\s\S]:/.test(filePath); 8 | 9 | export const resolveTsconfigPaths = ( 10 | tsconfig: TsConfigResult, 11 | ): Plugin => { 12 | const pathsMatcher = createPathsMatcher(tsconfig); 13 | if (!pathsMatcher) { 14 | return { 15 | name, 16 | }; 17 | } 18 | 19 | return { 20 | name, 21 | async resolveId(id, importer, options) { 22 | if ( 23 | !importer 24 | || isRelative(id) 25 | || isAbsolute(id) 26 | || id.startsWith('\0') 27 | ) { 28 | return null; 29 | } 30 | 31 | const possiblePaths = pathsMatcher(id); 32 | for (const tryPath of possiblePaths) { 33 | const resolved = await this.resolve( 34 | tryPath, 35 | importer, 36 | { 37 | skipSelf: true, 38 | ...options, 39 | }, 40 | ); 41 | if (resolved) { 42 | return resolved; 43 | } 44 | } 45 | 46 | return null; 47 | }, 48 | }; 49 | }; 50 | -------------------------------------------------------------------------------- /src/rollup/plugins/resolve-typescript-mjs-cjs.ts: -------------------------------------------------------------------------------- 1 | import type { Plugin } from 'rollup'; 2 | 3 | export const resolveTypescriptMjsCts = (): Plugin => { 4 | const isJs = /\.(?:[mc]?js|jsx)$/; 5 | 6 | return { 7 | name: 'resolve-typescript-mjs-cjs', 8 | resolveId(id, importer, options) { 9 | if ( 10 | isJs.test(id) 11 | && importer 12 | ) { 13 | return this.resolve( 14 | id.replace(/js(x?)$/, 'ts$1'), 15 | importer, 16 | options, 17 | ); 18 | } 19 | 20 | return null; 21 | }, 22 | }; 23 | }; 24 | -------------------------------------------------------------------------------- /src/rollup/plugins/strip-hashbang.ts: -------------------------------------------------------------------------------- 1 | import type { Plugin } from 'rollup'; 2 | import MagicString from 'magic-string'; 3 | 4 | const hashbangPattern = /^#!.*/; 5 | export const stripHashbang = (): Plugin => ({ 6 | name: 'strip-hashbang', 7 | 8 | transform: (code) => { 9 | if (!hashbangPattern.test(code)) { 10 | return null; 11 | } 12 | 13 | const transformed = new MagicString(code); 14 | transformed.replace(hashbangPattern, ''); 15 | 16 | return { 17 | code: transformed.toString(), 18 | map: transformed.generateMap({ hires: true }), 19 | }; 20 | }, 21 | }); 22 | -------------------------------------------------------------------------------- /src/rollup/types.ts: -------------------------------------------------------------------------------- 1 | import type { OutputOptions } from 'rollup'; 2 | 3 | export type Options = { 4 | minify: boolean; 5 | target: string[]; 6 | exportCondition: string[]; 7 | env: { 8 | key: string; 9 | value: string; 10 | }[]; 11 | sourcemap?: true | 'inline'; 12 | }; 13 | 14 | export type EnvObject = { 15 | [key: string]: string; 16 | }; 17 | 18 | export type Output = OutputOptions[] & Record; 19 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | export type PackageType = 'module' | 'commonjs'; 2 | 3 | export type ExportEntry = { 4 | outputPath: string; 5 | type: PackageType | 'types' | undefined; 6 | platform?: 'node'; 7 | isExecutable?: boolean; 8 | from: string; 9 | }; 10 | 11 | export type AliasMap = { [alias: string]: string }; 12 | -------------------------------------------------------------------------------- /src/utils/clean-dist.ts: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs'; 2 | import { fsExists } from './fs-exists.js'; 3 | 4 | export const cleanDist = async (directoryPath: string) => { 5 | const exists = await fsExists(directoryPath); 6 | if (!exists) { 7 | return; 8 | } 9 | 10 | await fs.promises.rm(directoryPath, { 11 | recursive: true, 12 | force: true, 13 | }); 14 | }; 15 | -------------------------------------------------------------------------------- /src/utils/fs-exists.ts: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs'; 2 | 3 | export const fsExists = ( 4 | path: string, 5 | ) => fs.promises.access(path).then( 6 | () => true, 7 | () => false, 8 | ); 9 | -------------------------------------------------------------------------------- /src/utils/get-source-path.ts: -------------------------------------------------------------------------------- 1 | import type { ExportEntry } from '../types.js'; 2 | import { fsExists } from './fs-exists.js'; 3 | 4 | const { stringify } = JSON; 5 | 6 | const tryExtensions = async ( 7 | pathWithoutExtension: string, 8 | extensions: readonly string[], 9 | ) => { 10 | for (const extension of extensions) { 11 | const pathWithExtension = pathWithoutExtension + extension; 12 | if (await fsExists(pathWithExtension)) { 13 | return { 14 | extension, 15 | path: pathWithExtension, 16 | }; 17 | } 18 | } 19 | }; 20 | 21 | const extensionMap = { 22 | '.d.ts': ['.d.ts', '.d.mts', '.d.cts', '.ts', '.tsx', '.mts', '.cts'], 23 | '.d.mts': ['.d.mts', '.d.ts', '.d.cts', '.ts', '.tsx', '.mts', '.cts'], 24 | '.d.cts': ['.d.cts', '.d.ts', '.d.mts', '.ts', '.tsx', '.mts', '.cts'], 25 | '.js': ['.js', '.ts', '.tsx', '.mts', '.cts'], 26 | '.mjs': ['.mjs', '.js', '.cjs', '.mts', '.cts', '.ts', '.tsx'], 27 | '.cjs': ['.cjs', '.js', '.mjs', '.mts', '.cts', '.ts', '.tsx'], 28 | } as const; 29 | 30 | const distExtensions = Object.keys(extensionMap) as (keyof typeof extensionMap)[]; 31 | 32 | export const getSourcePath = async ( 33 | { outputPath }: ExportEntry, 34 | source: string, 35 | dist: string, 36 | ) => { 37 | const sourcePathUnresolved = source + outputPath.slice(dist.length); 38 | 39 | const distExtension = distExtensions.find(extension => outputPath.endsWith(extension)); 40 | if (distExtension) { 41 | const sourcePathWithoutExtension = sourcePathUnresolved.slice(0, -distExtension.length); 42 | const sourcePath = await tryExtensions( 43 | sourcePathWithoutExtension, 44 | extensionMap[distExtension], 45 | ); 46 | 47 | if (sourcePath) { 48 | return { 49 | input: sourcePath.path, 50 | srcExtension: sourcePath.extension, 51 | distExtension, 52 | }; 53 | } 54 | throw new Error(`Could not find matching source file for export path: ${stringify(outputPath)}; Expected: ${sourcePathWithoutExtension}[${extensionMap[distExtension].join('|')}]`); 55 | } 56 | 57 | throw new Error(`Package.json output path contains invalid extension: ${stringify(outputPath)}; Expected: ${distExtensions.join(', ')}`); 58 | }; 59 | -------------------------------------------------------------------------------- /src/utils/get-tsconfig.ts: -------------------------------------------------------------------------------- 1 | import path from 'node:path'; 2 | import { getTsconfig as _getTsconfig, parseTsconfig } from 'get-tsconfig'; 3 | 4 | export const getTsconfig = ( 5 | tscFile?: string, 6 | ) => { 7 | if (!tscFile) { 8 | return _getTsconfig(); 9 | } 10 | 11 | const resolvedTscFile = path.resolve(tscFile); 12 | const config = parseTsconfig(resolvedTscFile); 13 | return { 14 | path: resolvedTscFile, 15 | config, 16 | }; 17 | }; 18 | -------------------------------------------------------------------------------- /src/utils/local-typescript-loader.ts: -------------------------------------------------------------------------------- 1 | const getLocalTypescriptPath = () => { 2 | const cwd = process.cwd(); 3 | try { 4 | return require.resolve('typescript', { 5 | paths: [cwd], 6 | }); 7 | } catch { 8 | throw new Error(`Could not find \`typescript\` in ${cwd}`); 9 | } 10 | }; 11 | 12 | // eslint-disable-next-line import-x/no-dynamic-require, @typescript-eslint/no-require-imports 13 | export default require(getLocalTypescriptPath()); 14 | -------------------------------------------------------------------------------- /src/utils/log.ts: -------------------------------------------------------------------------------- 1 | import { gray } from 'kolorist'; 2 | 3 | const currentTime = () => (new Date()).toLocaleTimeString(); 4 | 5 | export const log = (...messages: unknown[]) => console.log( 6 | `[${gray(currentTime())}]`, 7 | ...messages, 8 | ); 9 | -------------------------------------------------------------------------------- /src/utils/normalize-path.ts: -------------------------------------------------------------------------------- 1 | import path from 'node:path'; 2 | 3 | const hasPathPrefixPattern = /^[/.]/; 4 | 5 | export const normalizePath = ( 6 | filePath: string, 7 | isDirectory?: boolean, 8 | ) => { 9 | if ( 10 | !path.isAbsolute(filePath) // Windows paths starts with C:\\ 11 | && !hasPathPrefixPattern.test(filePath) 12 | ) { 13 | filePath = `./${filePath}`; 14 | } 15 | 16 | if (isDirectory && !filePath.endsWith('/')) { 17 | filePath += '/'; 18 | } 19 | 20 | return filePath; 21 | }; 22 | -------------------------------------------------------------------------------- /src/utils/parse-package-json/get-aliases.ts: -------------------------------------------------------------------------------- 1 | import path from 'node:path'; 2 | import type { PackageJson } from 'type-fest'; 3 | import type { AliasMap } from '../../types.js'; 4 | 5 | export const getAliases = ( 6 | { imports }: PackageJson, 7 | baseDirectory: string, 8 | ): AliasMap => { 9 | const aliases: AliasMap = {}; 10 | 11 | if (imports) { 12 | for (const alias in imports) { 13 | if (alias.startsWith('#')) { 14 | continue; 15 | } 16 | 17 | const subpath = imports[alias as keyof PackageJson.Imports]; 18 | if (typeof subpath !== 'string') { 19 | continue; 20 | } 21 | 22 | aliases[alias] = path.join(baseDirectory, subpath); 23 | } 24 | } 25 | 26 | return aliases; 27 | }; 28 | -------------------------------------------------------------------------------- /src/utils/parse-package-json/get-export-entries.ts: -------------------------------------------------------------------------------- 1 | import type { PackageJson } from 'type-fest'; 2 | import type { PackageType, ExportEntry } from '../../types.js'; 3 | import { normalizePath } from '../normalize-path.js'; 4 | import { propertyNeedsQuotes } from '../property-needs-quotes.js'; 5 | 6 | const getFileType = ( 7 | filePath: string, 8 | ): PackageType | 'types' | undefined => { 9 | if (filePath.endsWith('.mjs')) { 10 | return 'module'; 11 | } 12 | if (filePath.endsWith('.cjs')) { 13 | return 'commonjs'; 14 | } 15 | if ( 16 | filePath.endsWith('.d.ts') 17 | || filePath.endsWith('.d.cts') 18 | || filePath.endsWith('.d.mts') 19 | ) { 20 | return 'types'; 21 | } 22 | }; 23 | 24 | const isPath = (filePath: string) => filePath.startsWith('.'); 25 | 26 | const parseExportsMap = ( 27 | exportMap: PackageJson['exports'], 28 | packageType: PackageType, 29 | packagePath = 'exports', 30 | ): ExportEntry[] => { 31 | if (exportMap) { 32 | if (typeof exportMap === 'string') { 33 | if (isPath(exportMap)) { 34 | return [{ 35 | outputPath: exportMap, 36 | type: getFileType(exportMap) || packageType, 37 | from: packagePath, 38 | }]; 39 | } 40 | 41 | return []; 42 | } 43 | 44 | if (Array.isArray(exportMap)) { 45 | return exportMap.flatMap( 46 | (exportPath, index) => { 47 | const from = `${packagePath}[${index}]`; 48 | 49 | return ( 50 | typeof exportPath === 'string' 51 | ? ( 52 | isPath(exportPath) 53 | ? { 54 | outputPath: exportPath, 55 | type: getFileType(exportPath) || packageType, 56 | from, 57 | } 58 | : [] 59 | ) 60 | : parseExportsMap(exportPath, packageType, from) 61 | ); 62 | }, 63 | ); 64 | } 65 | 66 | if (typeof exportMap === 'object') { 67 | return Object.entries(exportMap).flatMap(([key, value]) => { 68 | if (typeof value === 'string') { 69 | const newProperty = propertyNeedsQuotes(key) ? `["${key}"]` : `.${key}`; 70 | const baseEntry = { 71 | outputPath: value, 72 | from: `${packagePath}${newProperty}`, 73 | }; 74 | 75 | if (key === 'require') { 76 | return { 77 | ...baseEntry, 78 | type: 'commonjs', 79 | }; 80 | } 81 | 82 | if (key === 'import') { 83 | return { 84 | ...baseEntry, 85 | type: getFileType(value) || packageType, 86 | }; 87 | } 88 | 89 | if (key === 'types') { 90 | return { 91 | ...baseEntry, 92 | type: 'types', 93 | }; 94 | } 95 | 96 | if (key === 'node') { 97 | return { 98 | ...baseEntry, 99 | type: getFileType(value) || packageType, 100 | platform: 'node', 101 | }; 102 | } 103 | 104 | if (key === 'default') { 105 | return { 106 | ...baseEntry, 107 | type: getFileType(value) || packageType, 108 | }; 109 | } 110 | } 111 | 112 | const newProperty = propertyNeedsQuotes(key) ? `["${key}"]` : `.${key}`; 113 | return parseExportsMap(value, packageType, `${packagePath}${newProperty}`); 114 | }); 115 | } 116 | } 117 | 118 | return []; 119 | }; 120 | 121 | const addExportPath = ( 122 | exportPathsMap: Record, 123 | exportEntry: ExportEntry, 124 | ) => { 125 | exportEntry.outputPath = normalizePath(exportEntry.outputPath); 126 | 127 | const { outputPath: exportPath, type, platform } = exportEntry; 128 | 129 | const existingExportPath = exportPathsMap[exportPath]; 130 | if (existingExportPath) { 131 | if (existingExportPath.type !== type) { 132 | throw new Error(`Conflicting export types "${existingExportPath.type}" & "${type}" found for ${exportPath}`); 133 | } 134 | 135 | if (existingExportPath.platform !== platform) { 136 | throw new Error(`Conflicting export platforms "${existingExportPath.platform}" & "${platform}" found for ${exportPath}`); 137 | } 138 | 139 | Object.assign(existingExportPath, exportEntry); 140 | } else { 141 | exportPathsMap[exportPath] = exportEntry; 142 | } 143 | }; 144 | 145 | export const getExportEntries = ( 146 | _packageJson: Readonly, 147 | ) => { 148 | const packageJson = { ..._packageJson }; 149 | 150 | // Prefer publishConfig when defined 151 | // https://pnpm.io/package_json#publishconfig 152 | const { publishConfig } = packageJson; 153 | if (publishConfig) { 154 | const fields = [ 155 | 'bin', 156 | 'main', 157 | 'exports', 158 | 'types', 159 | 'module', 160 | ]; 161 | 162 | for (const field of fields) { 163 | if (publishConfig[field]) { 164 | packageJson[field] = publishConfig[field]; 165 | } 166 | } 167 | } 168 | 169 | const exportEntriesMap: Record = {}; 170 | const packageType = packageJson.type ?? 'commonjs'; 171 | 172 | const mainPath = packageJson.main; 173 | if (mainPath) { 174 | addExportPath(exportEntriesMap, { 175 | outputPath: mainPath, 176 | type: getFileType(mainPath) ?? packageType, 177 | from: 'main', 178 | }); 179 | } 180 | 181 | // Defacto module entry-point for bundlers (not Node.js) 182 | // https://github.com/dherman/defense-of-dot-js/blob/master/proposal.md 183 | if (packageJson.module) { 184 | addExportPath(exportEntriesMap, { 185 | outputPath: packageJson.module, 186 | type: 'module', 187 | from: 'module', 188 | }); 189 | } 190 | 191 | // Entry point for TypeScript 192 | if (packageJson.types) { 193 | addExportPath(exportEntriesMap, { 194 | outputPath: packageJson.types, 195 | type: 'types', 196 | from: 'types', 197 | }); 198 | } 199 | 200 | const { bin } = packageJson; 201 | if (bin) { 202 | if (typeof bin === 'string') { 203 | addExportPath(exportEntriesMap, { 204 | outputPath: bin, 205 | type: getFileType(bin) ?? packageType, 206 | isExecutable: true, 207 | from: 'bin', 208 | }); 209 | } else { 210 | for (const [binName, binPath] of Object.entries(bin)) { 211 | const newProperty = propertyNeedsQuotes(binName) ? `["${binName}"]` : `.${binName}`; 212 | addExportPath(exportEntriesMap, { 213 | outputPath: binPath!, 214 | type: getFileType(binPath!) ?? packageType, 215 | isExecutable: true, 216 | from: `bin${newProperty}`, 217 | }); 218 | } 219 | } 220 | } 221 | 222 | if (packageJson.exports) { 223 | const exportMap = parseExportsMap(packageJson.exports, packageType); 224 | for (const exportEntry of exportMap) { 225 | addExportPath(exportEntriesMap, exportEntry); 226 | } 227 | } 228 | 229 | return Object.values(exportEntriesMap); 230 | }; 231 | 232 | export const parseCliInputFlag = (distPath: string): ExportEntry => { 233 | let isExecutable = false; 234 | 235 | if (distPath.includes('=')) { 236 | const [type, filePath] = distPath.split('='); 237 | distPath = filePath; 238 | isExecutable = type === 'bin' || type === 'binary'; 239 | } 240 | return { 241 | outputPath: normalizePath(distPath), 242 | type: getFileType(distPath), 243 | isExecutable, 244 | from: 'cli', 245 | }; 246 | }; 247 | -------------------------------------------------------------------------------- /src/utils/parse-package-json/get-external-dependencies.ts: -------------------------------------------------------------------------------- 1 | import type { PackageJson } from 'type-fest'; 2 | 3 | const externalProperties = [ 4 | 'peerDependencies', 5 | 'dependencies', 6 | 'optionalDependencies', 7 | ] as const; 8 | 9 | const typesPrefix = '@types/'; 10 | 11 | export const getExternalDependencies = ( 12 | packageJson: PackageJson, 13 | aliases: Record, 14 | forTypes = false, 15 | ) => { 16 | const externalDependencies = []; 17 | const { devDependencies } = packageJson; 18 | 19 | for (const property of externalProperties) { 20 | const externalDependenciesObject = packageJson[property]; 21 | 22 | if (!externalDependenciesObject) { 23 | continue; 24 | } 25 | 26 | const packageNames = Object.keys(externalDependenciesObject); 27 | 28 | for (const packageName of packageNames) { 29 | if (packageName in aliases) { 30 | continue; 31 | } 32 | 33 | /** 34 | * "@types/name" is imported in source as "name" 35 | * e.g. '@types/react' is imported as 'react' 36 | * 37 | * This was motivated by @types/estree, which doesn't 38 | * actually have a runtime package. It's a type-only package. 39 | */ 40 | if (packageName.startsWith(typesPrefix)) { 41 | if (forTypes) { 42 | let originalPackageName = packageName.slice(typesPrefix.length); 43 | 44 | if (originalPackageName.includes('__')) { 45 | originalPackageName = `@${originalPackageName.replace('__', '/')}`; 46 | } 47 | 48 | externalDependencies.push(originalPackageName); 49 | } 50 | } else { 51 | if (devDependencies && forTypes) { 52 | const typePackageName = typesPrefix + packageName.replace('@', '').replace('/', '__'); 53 | if ( 54 | devDependencies[typePackageName] 55 | && !(typePackageName in externalDependenciesObject) 56 | ) { 57 | console.warn(`Recommendation: "${typePackageName}" is externalized because "${packageName}" is in "${property}". Place "${typePackageName}" in "${property}" as well so users don't have missing types.`); 58 | } 59 | } 60 | 61 | externalDependencies.push(packageName); 62 | } 63 | } 64 | } 65 | 66 | return externalDependencies.flatMap(dependency => [ 67 | dependency, 68 | new RegExp(`^${dependency}/`), 69 | ]); 70 | }; 71 | -------------------------------------------------------------------------------- /src/utils/property-needs-quotes.ts: -------------------------------------------------------------------------------- 1 | // Check if the property is a valid JavaScript identifier 2 | const isValidIdentifier = /^[$_\p{ID_Start}][$\u200C\u200D\p{ID_Continue}]*$/u; 3 | 4 | // Check if the property is a reserved word 5 | const reservedWords = new Set([ 6 | 'do', 7 | 'if', 8 | 'in', 9 | 'for', 10 | 'int', 11 | 'new', 12 | 'try', 13 | 'var', 14 | 'byte', 15 | 'case', 16 | 'char', 17 | 'else', 18 | 'enum', 19 | 'goto', 20 | 'long', 21 | 'null', 22 | 'this', 23 | 'true', 24 | 'void', 25 | 'with', 26 | 'break', 27 | 'catch', 28 | 'class', 29 | 'const', 30 | 'false', 31 | 'final', 32 | 'float', 33 | 'short', 34 | 'super', 35 | 'throw', 36 | 'while', 37 | 'delete', 38 | 'double', 39 | 'export', 40 | 'import', 41 | 'native', 42 | 'public', 43 | 'return', 44 | 'static', 45 | 'switch', 46 | 'throws', 47 | 'typeof', 48 | 'boolean', 49 | 'default', 50 | 'extends', 51 | 'finally', 52 | 'package', 53 | 'private', 54 | 'abstract', 55 | 'continue', 56 | 'debugger', 57 | 'function', 58 | 'volatile', 59 | 'interface', 60 | 'protected', 61 | 'transient', 62 | 'implements', 63 | 'instanceof', 64 | 'synchronized', 65 | ]); 66 | 67 | export const propertyNeedsQuotes = ( 68 | property: string, 69 | ) => !isValidIdentifier.test(property) || reservedWords.has(property); 70 | -------------------------------------------------------------------------------- /src/utils/read-package-json.ts: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs'; 2 | import path from 'node:path'; 3 | import type { PackageJson } from 'type-fest'; 4 | import { fsExists } from './fs-exists.js'; 5 | 6 | export const readPackageJson = async (directoryPath: string): Promise => { 7 | const packageJsonPath = path.join(directoryPath, 'package.json'); 8 | 9 | const exists = await fsExists(packageJsonPath); 10 | 11 | if (!exists) { 12 | throw new Error(`package.json not found at: ${packageJsonPath}`); 13 | } 14 | 15 | const packageJsonString = await fs.promises.readFile(packageJsonPath, 'utf8'); 16 | 17 | try { 18 | return JSON.parse(packageJsonString); 19 | } catch (error) { 20 | throw new Error(`Cannot parse package.json: ${(error as Error).message}`); 21 | } 22 | }; 23 | -------------------------------------------------------------------------------- /tests/fixtures.ts: -------------------------------------------------------------------------------- 1 | import path from 'node:path'; 2 | import outdent from 'outdent'; 3 | import type { FileTree } from 'fs-fixture'; 4 | import type { PackageJson, TsConfigJson } from 'type-fest'; 5 | 6 | export const createPackageJson = (packageJson: PackageJson) => JSON.stringify(packageJson); 7 | export const createTsconfigJson = (tsconfigJson: TsConfigJson) => JSON.stringify(tsconfigJson); 8 | 9 | export const installTypeScript: FileTree = { 10 | 'node_modules/typescript': ({ symlink }) => symlink(path.resolve('node_modules/typescript'), 'dir'), 11 | }; 12 | 13 | export const installReact: FileTree = { 14 | 'node_modules/react': ({ symlink }) => symlink(path.resolve('node_modules/react'), 'dir'), 15 | 'node_modules/@types/react': ({ symlink }) => symlink(path.resolve('node_modules/@types/react'), 'dir'), 16 | }; 17 | 18 | type Options = { 19 | installTypeScript?: boolean; 20 | installReact?: boolean; 21 | }; 22 | 23 | export const fixtureFiles = { 24 | nested: { 25 | 'index.ts': outdent` 26 | console.log('nested entry point'); 27 | 28 | export function sayHello(name: string) { 29 | return name; 30 | } 31 | `, 32 | 'utils.ts': outdent` 33 | export { writeFileSync } from 'fs'; 34 | export { readFileSync } from 'node:fs'; 35 | 36 | export function sayGoodbye(name: string) { 37 | console.log('Goodbye', name); 38 | } 39 | `, 40 | }, 41 | 42 | 'cjs.cjs': outdent` 43 | #! /usr/bin/env node 44 | 45 | console.log('side effect'); 46 | 47 | module.exports = function sayHello(name) { 48 | console.log('Hello', name); 49 | }; 50 | `, 51 | 52 | 'component.tsx': outdent` 53 | export const Component = () => (
Hello World
); 54 | `, 55 | 56 | 'conditional-require.js': outdent` 57 | if ( 58 | process.env.NODE_ENV === 'production' 59 | || process.env['NODE_ENV'] === 'production' 60 | || process.env['PROD'] === 'true' 61 | ) { 62 | console.log('production'); 63 | require('./cjs.cjs'); 64 | } else { 65 | console.log('development'); 66 | } 67 | 68 | console.log(1); 69 | `, 70 | 71 | 'cts.cts': outdent` 72 | export function sayHello(name: string) { 73 | console.log('Hello', name); 74 | } 75 | `, 76 | 77 | 'dts.d.ts': outdent` 78 | import type { SomeType } from './types'; 79 | 80 | declare const value: SomeType; 81 | 82 | export default value; 83 | `, 84 | 85 | 'index.js': outdent` 86 | #! /usr/bin/env node 87 | 88 | import value from './value.js'; 89 | import { Component } from './component.tsx'; 90 | import { sayHello } from './utils.js'; 91 | import { sayHello as sayHelloMjs } from './mjs.mjs'; 92 | import { sayHello as sayHelloMts } from './mts.mts'; 93 | import { sayHello as sayHelloCjs } from './cjs.cjs'; 94 | import { sayHello as sayHelloCts } from './cts.cts'; 95 | 96 | console.log( 97 | Component, 98 | sayHello, 99 | sayHelloMjs, 100 | sayHelloMts, 101 | sayHelloCjs, 102 | sayHelloCts, 103 | ); 104 | 105 | export default value * 2; 106 | `, 107 | 108 | 'mjs.mjs': outdent` 109 | export function sayHello(name) { 110 | console.log('Hello', name); 111 | }; 112 | `, 113 | 114 | 'mts.mts': outdent` 115 | export { sayGoodbye } from './mts2.mjs'; 116 | export { foo } from './target.js'; 117 | export { sayHello as sayHello2 } from './mjs.mjs'; 118 | 119 | export function sayHello(name: string) { 120 | console.log('Hello', name); 121 | } 122 | `, 123 | 124 | 'mts2.mts': outdent` 125 | export function sayGoodbye(name: string) { 126 | console.log('Goodbye', name); 127 | } 128 | `, 129 | 130 | 'require.ts': outdent` 131 | console.log(require('fs')); 132 | 133 | export const a = 1; 134 | `, 135 | 136 | 'target.ts': outdent` 137 | function preservesName() { return 2 ** 3; } 138 | export const functionName = preservesName.name; 139 | 140 | /** 141 | * Expect minification to apply ?. optional chaining. 142 | * https://github.com/evanw/esbuild/releases/tag/v0.14.25#:~:text=Minification%20now%20takes%20advantage%20of%20the%20%3F.%20operator 143 | */ 144 | export let foo = (x: any) => { 145 | if (x !== null && x !== undefined) x.y() 146 | return x === null || x === undefined ? undefined : x.z 147 | } 148 | `, 149 | 150 | 'types.ts': outdent` 151 | export type SomeType = string | number; 152 | `, 153 | 154 | 'utils.ts': outdent` 155 | import type { SomeType } from './types'; 156 | export { writeFileSync } from 'fs'; 157 | export { readFileSync } from 'node:fs'; 158 | 159 | export function sayHello(name: SomeType) { 160 | return \`Hello \${name}!\`; 161 | } 162 | `, 163 | 164 | 'value.js': outdent` 165 | #! /usr/bin/env node 166 | export default 1234; 167 | `, 168 | }; 169 | 170 | export const packageFixture = (options: Options = {}): FileTree => ({ 171 | src: fixtureFiles, 172 | ...(options.installTypeScript ? installTypeScript : {}), 173 | ...(options.installReact ? installReact : {}), 174 | }); 175 | 176 | export const fixtureDependencyExportsMap = (entryFile: string): FileTree => ({ 177 | 'package.json': createPackageJson({ 178 | main: entryFile, 179 | }), 180 | 181 | src: { 182 | 'dependency-exports-require.js': outdent` 183 | console.log(require('dependency-exports-dual')); 184 | `, 185 | 186 | 'dependency-exports-import.js': outdent` 187 | import esm from 'dependency-exports-dual'; 188 | 189 | console.log(esm); 190 | `, 191 | }, 192 | 193 | 'node_modules/dependency-exports-dual': { 194 | 'file.js': outdent` 195 | module.exports = 'cjs'; 196 | `, 197 | 'file.mjs': outdent` 198 | export default 'esm'; 199 | `, 200 | 'package.json': createPackageJson({ 201 | name: 'dependency-exports-dual', 202 | exports: { 203 | require: './file.js', 204 | import: './file.mjs', 205 | }, 206 | }), 207 | }, 208 | }); 209 | 210 | export const fixtureDependencyImportsMap: FileTree = { 211 | 'package.json': createPackageJson({ 212 | main: './dist/dependency-imports-map.js', 213 | }), 214 | 215 | 'src/dependency-imports-map.js': outdent` 216 | import value from 'dependency-imports-map'; 217 | console.log(value); 218 | `, 219 | 220 | 'node_modules/dependency-imports-map': { 221 | 'default.js': outdent` 222 | module.exports = 'default'; 223 | `, 224 | 'index.js': outdent` 225 | console.log(require('#conditional')); 226 | `, 227 | 'node.js': outdent` 228 | module.exports = 'node'; 229 | `, 230 | 'package.json': createPackageJson({ 231 | name: 'dependency-exports-dual', 232 | imports: { 233 | '#conditional': { 234 | node: './node.js', 235 | default: './default.js', 236 | }, 237 | }, 238 | }), 239 | }, 240 | }; 241 | 242 | export const fixtureDynamicImports: FileTree = { 243 | 'package.json': createPackageJson({ 244 | main: './dist/dynamic-imports.js', 245 | }), 246 | src: { 247 | 'dynamic-imports.js': outdent` 248 | const files = [ 249 | 'aaa', 250 | 'bbb', 251 | 'ccc', 252 | ]; 253 | const randomFile = files[Math.floor(Math.random() * files.length)]; 254 | import(\`./files/\${randomFile}.js\`) 255 | `, 256 | 257 | files: { 258 | 'aaa.js': 'console.log(111)', 259 | 'bbb.js': 'console.log(222)', 260 | 'ccc.js': 'console.log(333)', 261 | }, 262 | }, 263 | }; 264 | 265 | // https://github.com/privatenumber/pkgroll/issues/104 266 | export const fixtureDynamicImportUnresolvable: FileTree = { 267 | 'package.json': createPackageJson({ 268 | main: './dist/dynamic-imports.js', 269 | }), 270 | src: { 271 | 'dynamic-imports.js': outdent` 272 | function importModule(path) { 273 | // who knows what will be imported here? 274 | return import(path); 275 | } 276 | 277 | importModule('./too-dynamic.js'); 278 | `, 279 | 'too-dynamic.js': 'console.log(123)', 280 | }, 281 | }; 282 | -------------------------------------------------------------------------------- /tests/index.ts: -------------------------------------------------------------------------------- 1 | import { describe } from 'manten'; 2 | import getNode from 'get-node'; 3 | 4 | const nodeVersions = [ 5 | '20', 6 | ...( 7 | process.env.CI 8 | ? [ 9 | '18', 10 | ] 11 | : [] 12 | ), 13 | ]; 14 | 15 | (async () => { 16 | for (const nodeVersion of nodeVersions) { 17 | const node = await getNode(nodeVersion); 18 | await describe(`Node ${node.version}`, ({ runTestSuite }) => { 19 | runTestSuite(import('./specs/error-cases.js'), node.path); 20 | runTestSuite(import('./specs/builds/index.js'), node.path); 21 | }); 22 | } 23 | })(); 24 | -------------------------------------------------------------------------------- /tests/specs/builds/bin.ts: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs/promises'; 2 | import { testSuite, expect } from 'manten'; 3 | import { createFixture } from 'fs-fixture'; 4 | import { pkgroll } from '../../utils.js'; 5 | import { packageFixture, createPackageJson } from '../../fixtures.js'; 6 | 7 | export default testSuite(({ describe }, nodePath: string) => { 8 | describe('bin', ({ test }) => { 9 | test('supports single path', async () => { 10 | await using fixture = await createFixture({ 11 | // Using a subpath tests that the paths are joined correctly on Windows 12 | 'src/subpath/bin.ts': 'console.log("Hello, world!");', 13 | 'package.json': createPackageJson({ 14 | bin: './dist/subpath/bin.mjs', 15 | }), 16 | }); 17 | 18 | const pkgrollProcess = await pkgroll([], { 19 | cwd: fixture.path, 20 | nodePath, 21 | }); 22 | 23 | expect(pkgrollProcess.exitCode).toBe(0); 24 | expect(pkgrollProcess.stderr).toBe(''); 25 | 26 | await test('is executable', async () => { 27 | const content = await fixture.readFile('dist/subpath/bin.mjs', 'utf8'); 28 | expect(content).toMatch('#!/usr/bin/env node'); 29 | 30 | // File modes don't exist on Windows 31 | if (process.platform !== 'win32') { 32 | const stats = await fs.stat(fixture.getPath('dist/subpath/bin.mjs')); 33 | const unixFilePermissions = `0${(stats.mode & 0o777).toString(8)}`; // eslint-disable-line no-bitwise 34 | 35 | expect(unixFilePermissions).toBe('0755'); 36 | } 37 | }); 38 | }); 39 | 40 | test('supports object', async () => { 41 | await using fixture = await createFixture({ 42 | ...packageFixture(), 43 | 'package.json': createPackageJson({ 44 | bin: { 45 | a: './dist/index.mjs', 46 | b: './dist/index.js', 47 | }, 48 | }), 49 | }); 50 | 51 | const pkgrollProcess = await pkgroll([], { 52 | cwd: fixture.path, 53 | nodePath, 54 | }); 55 | 56 | expect(pkgrollProcess.exitCode).toBe(0); 57 | expect(pkgrollProcess.stderr).toBe(''); 58 | 59 | expect(await fixture.exists('dist/index.mjs')).toBe(true); 60 | expect(await fixture.exists('dist/index.js')).toBe(true); 61 | }); 62 | 63 | test('hashbang gets inserted at the top (despite other injections e.g. createRequire)', async () => { 64 | await using fixture = await createFixture({ 65 | 'src/dynamic-require.ts': 'require((() => \'fs\')());', 66 | 'package.json': createPackageJson({ 67 | bin: './dist/dynamic-require.mjs', 68 | }), 69 | }); 70 | 71 | const pkgrollProcess = await pkgroll([], { 72 | cwd: fixture.path, 73 | nodePath, 74 | }); 75 | 76 | expect(pkgrollProcess.exitCode).toBe(0); 77 | expect(pkgrollProcess.stderr).toBe(''); 78 | 79 | const content = await fixture.readFile('dist/dynamic-require.mjs', 'utf8'); 80 | expect(content.startsWith('#!/usr/bin/env node')).toBeTruthy(); 81 | }); 82 | 83 | test('publishConfig', async () => { 84 | await using fixture = await createFixture({ 85 | ...packageFixture(), 86 | 'package.json': createPackageJson({ 87 | bin: './dist/invalid.mjs', 88 | publishConfig: { 89 | bin: './dist/index.mjs', 90 | }, 91 | }), 92 | }); 93 | 94 | const pkgrollProcess = await pkgroll([], { 95 | cwd: fixture.path, 96 | nodePath, 97 | }); 98 | 99 | expect(pkgrollProcess.exitCode).toBe(0); 100 | expect(pkgrollProcess.stderr).toBe(''); 101 | 102 | expect(await fixture.exists('dist/index.mjs')).toBe(true); 103 | }); 104 | }); 105 | }); 106 | -------------------------------------------------------------------------------- /tests/specs/builds/clean-dist.ts: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs/promises'; 2 | import { testSuite, expect } from 'manten'; 3 | import { createFixture } from 'fs-fixture'; 4 | import { pkgroll } from '../../utils.js'; 5 | import { packageFixture, createPackageJson } from '../../fixtures.js'; 6 | 7 | export default testSuite(({ describe }, nodePath: string) => { 8 | describe('clean dist', ({ test }) => { 9 | test('no flag', async () => { 10 | await using fixture = await createFixture({ 11 | ...packageFixture({ installTypeScript: true }), 12 | 'package.json': createPackageJson({ 13 | main: './dist/nested/index.js', 14 | module: './dist/nested/index.mjs', 15 | types: './dist/nested/index.d.ts', 16 | }), 17 | }); 18 | 19 | await pkgroll( 20 | [], 21 | { 22 | cwd: fixture.path, 23 | nodePath, 24 | }, 25 | ); 26 | 27 | await fs.mkdir(fixture.getPath('src', 'nested2')); 28 | await fixture.writeFile('./src/nested2/index.ts', 'export function sayHello2(name: string) { return name; }'); 29 | 30 | await fixture.writeJson('package.json', { 31 | main: './dist/nested2/index.js', 32 | module: './dist/nested2/index.mjs', 33 | types: './dist/nested2/index.d.ts', 34 | }); 35 | 36 | const pkgrollProcess = await pkgroll( 37 | [], 38 | { 39 | cwd: fixture.path, 40 | nodePath, 41 | }, 42 | ); 43 | 44 | expect(pkgrollProcess.exitCode).toBe(0); 45 | expect(pkgrollProcess.stderr).toBe(''); 46 | 47 | expect(await fixture.exists('dist/nested/index.js')).toBe(true); 48 | expect(await fixture.exists('dist/nested/index.mjs')).toBe(true); 49 | expect(await fixture.exists('dist/nested/index.d.ts')).toBe(true); 50 | expect(await fixture.exists('dist/nested2/index.js')).toBe(true); 51 | expect(await fixture.exists('dist/nested2/index.mjs')).toBe(true); 52 | expect(await fixture.exists('dist/nested2/index.d.ts')).toBe(true); 53 | }); 54 | 55 | test('with flag', async () => { 56 | await using fixture = await createFixture({ 57 | ...packageFixture({ installTypeScript: true }), 58 | 'package.json': createPackageJson({ 59 | main: './dist/nested/index.js', 60 | module: './dist/nested/index.mjs', 61 | types: './dist/nested/index.d.ts', 62 | }), 63 | }); 64 | 65 | await pkgroll( 66 | [], 67 | { 68 | cwd: fixture.path, 69 | nodePath, 70 | }, 71 | ); 72 | 73 | await fs.mkdir(fixture.getPath('src', 'nested2')); 74 | await fixture.writeFile('./src/nested2/index.ts', 'export function sayHello2(name: string) { return name; }'); 75 | 76 | await fixture.writeJson('package.json', { 77 | main: './dist/nested2/index.js', 78 | module: './dist/nested2/index.mjs', 79 | types: './dist/nested2/index.d.ts', 80 | }); 81 | 82 | const pkgrollProcess = await pkgroll( 83 | ['--clean-dist'], 84 | { 85 | cwd: fixture.path, 86 | nodePath, 87 | }, 88 | ); 89 | 90 | expect(pkgrollProcess.exitCode).toBe(0); 91 | expect(pkgrollProcess.stderr).toBe(''); 92 | 93 | expect(await fixture.exists('dist/nested/index.js')).toBe(false); 94 | expect(await fixture.exists('dist/nested/index.mjs')).toBe(false); 95 | expect(await fixture.exists('dist/nested/index.d.ts')).toBe(false); 96 | expect(await fixture.exists('dist/nested2/index.js')).toBe(true); 97 | expect(await fixture.exists('dist/nested2/index.mjs')).toBe(true); 98 | expect(await fixture.exists('dist/nested2/index.d.ts')).toBe(true); 99 | }); 100 | }); 101 | }); 102 | -------------------------------------------------------------------------------- /tests/specs/builds/dependencies.ts: -------------------------------------------------------------------------------- 1 | import { testSuite, expect } from 'manten'; 2 | import { createFixture } from 'fs-fixture'; 3 | import { pkgroll } from '../../utils.js'; 4 | import { 5 | installTypeScript, 6 | createPackageJson, 7 | fixtureDependencyExportsMap, 8 | fixtureDependencyImportsMap, 9 | } from '../../fixtures.js'; 10 | 11 | export default testSuite(({ describe }, nodePath: string) => { 12 | describe('dependencies', ({ test }) => { 13 | test('externalize dependencies', async () => { 14 | await using fixture = await createFixture({ 15 | 'src/dependency-external.js': ` 16 | /** 17 | * Should be imported with a package.json 18 | * with "@org/name" in the "dependency" field 19 | */ 20 | import someValue from '@org/name/path'; 21 | 22 | console.log(someValue); 23 | `, 24 | 25 | 'package.json': createPackageJson({ 26 | main: './dist/dependency-external.js', 27 | dependencies: { 28 | '@org/name': '*', 29 | }, 30 | }), 31 | }); 32 | 33 | const pkgrollProcess = await pkgroll([], { 34 | cwd: fixture.path, 35 | nodePath, 36 | }); 37 | 38 | expect(pkgrollProcess.exitCode).toBe(0); 39 | expect(pkgrollProcess.stderr).toBe(''); 40 | 41 | const content = await fixture.readFile('dist/dependency-external.js', 'utf8'); 42 | expect(content).toMatch('require(\'@org/name/path\')'); 43 | }); 44 | 45 | test('externalize types', async () => { 46 | await using fixture = await createFixture({ 47 | 'package.json': createPackageJson({ 48 | types: 'dist/index.d.ts', 49 | dependencies: { 50 | '@types/pkg': '*', 51 | '@types/square-icons__react': '*', 52 | }, 53 | }), 54 | 'node_modules/@types': { 55 | 'pkg/index.d.ts': 'export type typeA = {}', 56 | 'square-icons__react/index.d.ts': 'export type typeB = {}', 57 | }, 58 | 'src/index.d.ts': ` 59 | import type { typeA } from 'pkg'; 60 | import type { typeB } from '@square-icons/react'; 61 | export const a: typeA; 62 | export const b: typeB; 63 | `, 64 | 65 | ...installTypeScript, 66 | }); 67 | 68 | const pkgrollProcess = await pkgroll([], { 69 | cwd: fixture.path, 70 | nodePath, 71 | }); 72 | expect(pkgrollProcess.exitCode).toBe(0); 73 | expect(pkgrollProcess.stderr).toBe(''); 74 | 75 | const content = await fixture.readFile('dist/index.d.ts', 'utf8'); 76 | expect(content).toMatch('from \'pkg\''); 77 | expect(content).toMatch('from \'@square-icons/react\''); 78 | }); 79 | 80 | test('bundle in types if only in devDependency', async () => { 81 | await using fixture = await createFixture({ 82 | 'package.json': createPackageJson({ 83 | types: 'dist/index.d.ts', 84 | devDependencies: { 85 | '@types/react': '*', 86 | }, 87 | }), 88 | 'node_modules/@types/react': { 89 | 'index.d.ts': 'declare const A: { b: number }; export { A }', 90 | }, 91 | 'src/index.d.ts': 'export { A } from "react"', 92 | ...installTypeScript, 93 | }); 94 | 95 | const pkgrollProcess = await pkgroll([], { 96 | cwd: fixture.path, 97 | nodePath, 98 | }); 99 | expect(pkgrollProcess.exitCode).toBe(0); 100 | expect(pkgrollProcess.stderr).toBe(''); 101 | 102 | const content = await fixture.readFile('dist/index.d.ts', 'utf8'); 103 | expect(content).toBe('declare const A: { b: number };\n\nexport { A };\n'); 104 | }); 105 | 106 | test('externalize dependency & type despite devDependency type', async () => { 107 | await using fixture = await createFixture({ 108 | 'package.json': createPackageJson({ 109 | main: 'dist/index.js', 110 | types: 'dist/index.d.ts', 111 | dependencies: { 112 | react: '*', 113 | }, 114 | devDependencies: { 115 | '@types/react': '*', 116 | }, 117 | }), 118 | node_modules: { 119 | '@types/react': { 120 | 'index.d.ts': 'declare const A: { b: number }; export { A }', 121 | }, 122 | react: { 123 | 'index.js': 'export const A = {}', 124 | }, 125 | }, 126 | 'src/index.ts': 'export { A } from "react"', 127 | ...installTypeScript, 128 | }); 129 | 130 | const pkgrollProcess = await pkgroll([], { 131 | cwd: fixture.path, 132 | nodePath, 133 | }); 134 | expect(pkgrollProcess.exitCode).toBe(0); 135 | expect(pkgrollProcess.stderr).toBe( 136 | 'Recommendation: "@types/react" is externalized because "react" is in "dependencies". Place "@types/react" in "dependencies" as well so users don\'t have missing types.', 137 | ); 138 | 139 | const contentJs = await fixture.readFile('dist/index.js', 'utf8'); 140 | expect(contentJs).toMatch('require(\'react\')'); 141 | 142 | // Types externalized despite @types/react being a devDependency 143 | const contentTypes = await fixture.readFile('dist/index.d.ts', 'utf8'); 144 | expect(contentTypes).toBe('export { A } from \'react\';\n'); 145 | }); 146 | 147 | test('dual package - require', async () => { 148 | await using fixture = await createFixture( 149 | fixtureDependencyExportsMap('./dist/dependency-exports-require.js'), 150 | ); 151 | 152 | const pkgrollProcess = await pkgroll([], { 153 | cwd: fixture.path, 154 | nodePath, 155 | }); 156 | 157 | expect(pkgrollProcess.exitCode).toBe(0); 158 | expect(pkgrollProcess.stderr).toBe(''); 159 | 160 | const content = await fixture.readFile('dist/dependency-exports-require.js', 'utf8'); 161 | expect(content).toMatch('cjs'); 162 | }); 163 | 164 | test('dual package - import', async () => { 165 | await using fixture = await createFixture( 166 | fixtureDependencyExportsMap('./dist/dependency-exports-import.js'), 167 | ); 168 | 169 | const pkgrollProcess = await pkgroll([], { 170 | cwd: fixture.path, 171 | nodePath, 172 | }); 173 | 174 | expect(pkgrollProcess.exitCode).toBe(0); 175 | expect(pkgrollProcess.stderr).toBe(''); 176 | 177 | const content = await fixture.readFile('dist/dependency-exports-import.js', 'utf8'); 178 | expect(content).toMatch('esm'); 179 | }); 180 | 181 | test('imports map - default', async () => { 182 | await using fixture = await createFixture(fixtureDependencyImportsMap); 183 | 184 | const pkgrollProcess = await pkgroll([], { 185 | cwd: fixture.path, 186 | nodePath, 187 | }); 188 | 189 | expect(pkgrollProcess.exitCode).toBe(0); 190 | expect(pkgrollProcess.stderr).toBe(''); 191 | 192 | const content = await fixture.readFile('dist/dependency-imports-map.js', 'utf8'); 193 | expect(content).toMatch('default'); 194 | }); 195 | 196 | test('imports map - node', async () => { 197 | await using fixture = await createFixture(fixtureDependencyImportsMap); 198 | 199 | const pkgrollProcess = await pkgroll(['--export-condition=node'], { 200 | cwd: fixture.path, 201 | nodePath, 202 | }); 203 | 204 | expect(pkgrollProcess.exitCode).toBe(0); 205 | expect(pkgrollProcess.stderr).toBe(''); 206 | 207 | const content = await fixture.readFile('dist/dependency-imports-map.js', 'utf8'); 208 | expect(content).toMatch('node'); 209 | }); 210 | }); 211 | }); 212 | -------------------------------------------------------------------------------- /tests/specs/builds/env.ts: -------------------------------------------------------------------------------- 1 | import { testSuite, expect } from 'manten'; 2 | import { createFixture } from 'fs-fixture'; 3 | import { pkgroll } from '../../utils.js'; 4 | import { packageFixture, createPackageJson } from '../../fixtures.js'; 5 | 6 | export default testSuite(({ describe }, nodePath: string) => { 7 | describe('env', ({ test }) => { 8 | test('dead code elimination via env', async () => { 9 | await using fixture = await createFixture({ 10 | ...packageFixture(), 11 | 'package.json': createPackageJson({ 12 | main: './dist/conditional-require.js', 13 | }), 14 | }); 15 | 16 | const pkgrollProcess = await pkgroll(['--env.NODE_ENV=development', '--env.PROD=false'], { 17 | cwd: fixture.path, 18 | nodePath, 19 | }); 20 | 21 | expect(pkgrollProcess.exitCode).toBe(0); 22 | expect(pkgrollProcess.stderr).toBe(''); 23 | 24 | const content = await fixture.readFile('dist/conditional-require.js', 'utf8'); 25 | expect(content).toMatch('development'); 26 | expect(content).not.toMatch('production'); 27 | }); 28 | 29 | test('dead code elimination via env in node_modules', async () => { 30 | await using fixture = await createFixture({ 31 | 'package.json': createPackageJson({ 32 | main: './dist/index.mjs', 33 | }), 34 | 'src/index.mjs': 'import "dep"', 35 | 'node_modules/dep': { 36 | 'package.json': createPackageJson({ 37 | main: 'index.js', 38 | }), 39 | 'index.js': ` 40 | if ( 41 | process.env.NODE_ENV === 'production' 42 | || process.env['NODE_ENV'] === 'production' 43 | || process.env['PROD'] === 'true' 44 | ) { 45 | console.log('production'); 46 | } else { 47 | console.log('development'); 48 | } 49 | `, 50 | }, 51 | }); 52 | 53 | const pkgrollProcess = await pkgroll(['--env.NODE_ENV=development', '--env.PROD=false'], { 54 | cwd: fixture.path, 55 | nodePath, 56 | }); 57 | 58 | expect(pkgrollProcess.exitCode).toBe(0); 59 | expect(pkgrollProcess.stderr).toBe(''); 60 | 61 | const content = await fixture.readFile('dist/index.mjs', 'utf8'); 62 | expect(content).toMatch('development'); 63 | expect(content).not.toMatch('production'); 64 | }); 65 | }); 66 | }); 67 | -------------------------------------------------------------------------------- /tests/specs/builds/index.ts: -------------------------------------------------------------------------------- 1 | import { testSuite } from 'manten'; 2 | 3 | export default testSuite(({ describe }, nodePath: string) => { 4 | describe('builds', async ({ runTestSuite }) => { 5 | runTestSuite(import('./output-commonjs.js'), nodePath); 6 | runTestSuite(import('./output-module.js'), nodePath); 7 | runTestSuite(import('./output-dual.js'), nodePath); 8 | runTestSuite(import('./output-types.js'), nodePath); 9 | runTestSuite(import('./env.js'), nodePath); 10 | runTestSuite(import('./target.js'), nodePath); 11 | runTestSuite(import('./minification.js'), nodePath); 12 | runTestSuite(import('./package-exports.js'), nodePath); 13 | runTestSuite(import('./package-imports.js'), nodePath); 14 | runTestSuite(import('./bin.js'), nodePath); 15 | runTestSuite(import('./dependencies.js'), nodePath); 16 | runTestSuite(import('./src-dist.js'), nodePath); 17 | runTestSuite(import('./sourcemap.js'), nodePath); 18 | runTestSuite(import('./typescript.js'), nodePath); 19 | runTestSuite(import('./clean-dist.js'), nodePath); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /tests/specs/builds/minification.ts: -------------------------------------------------------------------------------- 1 | import { pathToFileURL } from 'node:url'; 2 | import { testSuite, expect } from 'manten'; 3 | import { createFixture } from 'fs-fixture'; 4 | import { pkgroll } from '../../utils.js'; 5 | import { packageFixture, createPackageJson } from '../../fixtures.js'; 6 | 7 | export default testSuite(({ describe }, nodePath: string) => { 8 | describe('minification', ({ test }) => { 9 | test('minification', async () => { 10 | await using fixture = await createFixture({ 11 | ...packageFixture(), 12 | 'package.json': createPackageJson({ 13 | main: './dist/target.js', 14 | }), 15 | }); 16 | 17 | const pkgrollProcess = await pkgroll(['--minify', '--target', 'esnext'], { 18 | cwd: fixture.path, 19 | nodePath, 20 | }); 21 | 22 | expect(pkgrollProcess.exitCode).toBe(0); 23 | expect(pkgrollProcess.stderr).toBe(''); 24 | 25 | const content = await fixture.readFile('dist/target.js', 'utf8'); 26 | 27 | // Optional chaining function call 28 | expect(content).toMatch(/\w\?\.\w\(\)/); 29 | 30 | // Name should be minified 31 | expect(content).not.toMatch('exports.foo=foo'); 32 | 33 | // Minification should preserve name 34 | const { functionName } = await import(pathToFileURL(fixture.getPath('dist/target.js')).toString()); 35 | expect(functionName).toBe('preservesName'); 36 | }); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /tests/specs/builds/output-commonjs.ts: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs/promises'; 2 | import { testSuite, expect } from 'manten'; 3 | import { createFixture } from 'fs-fixture'; 4 | import { pkgroll } from '../../utils.js'; 5 | import { 6 | packageFixture, createPackageJson, createTsconfigJson, fixtureDynamicImports, 7 | fixtureDynamicImportUnresolvable, 8 | } from '../../fixtures.js'; 9 | 10 | export default testSuite(({ describe }, nodePath: string) => { 11 | describe('output: commonjs', ({ test }) => { 12 | test('{ type: commonjs, field: main, srcExt: js, distExt: js }', async () => { 13 | await using fixture = await createFixture({ 14 | ...packageFixture(), 15 | 'package.json': createPackageJson({ 16 | main: './dist/index.js', 17 | }), 18 | }); 19 | 20 | const pkgrollProcess = await pkgroll([], { 21 | cwd: fixture.path, 22 | nodePath, 23 | }); 24 | 25 | expect(pkgrollProcess.exitCode).toBe(0); 26 | expect(pkgrollProcess.stderr).toBe(''); 27 | 28 | const content = await fixture.readFile('dist/index.js', 'utf8'); 29 | expect(content).toMatch('module.exports ='); 30 | }); 31 | 32 | test('{ type: commonjs, field: main, srcExt: mts, distExt: js }', async () => { 33 | await using fixture = await createFixture({ 34 | ...packageFixture(), 35 | 'package.json': createPackageJson({ 36 | main: './dist/mts.js', 37 | }), 38 | }); 39 | 40 | const pkgrollProcess = await pkgroll([], { 41 | cwd: fixture.path, 42 | nodePath, 43 | }); 44 | 45 | expect(pkgrollProcess.exitCode).toBe(0); 46 | expect(pkgrollProcess.stderr).toBe(''); 47 | 48 | const content = await fixture.readFile('dist/mts.js', 'utf8'); 49 | expect(content).toMatch('exports.sayHello ='); 50 | }); 51 | 52 | test('{ type: commonjs, field: main, srcExt: cts, distExt: js }', async () => { 53 | await using fixture = await createFixture({ 54 | ...packageFixture(), 55 | 'package.json': createPackageJson({ 56 | main: './dist/cts.js', 57 | }), 58 | }); 59 | 60 | const pkgrollProcess = await pkgroll([], { 61 | cwd: fixture.path, 62 | nodePath, 63 | }); 64 | 65 | expect(pkgrollProcess.exitCode).toBe(0); 66 | expect(pkgrollProcess.stderr).toBe(''); 67 | 68 | const content = await fixture.readFile('dist/cts.js', 'utf8'); 69 | expect(content).toMatch('exports.sayHello ='); 70 | }); 71 | 72 | test('{ type: commonjs, field: main, srcExt: cts, distExt: cjs }', async () => { 73 | await using fixture = await createFixture({ 74 | ...packageFixture(), 75 | 'package.json': createPackageJson({ 76 | main: './dist/cts.cjs', 77 | }), 78 | }); 79 | 80 | const pkgrollProcess = await pkgroll([], { 81 | cwd: fixture.path, 82 | nodePath, 83 | }); 84 | 85 | expect(pkgrollProcess.exitCode).toBe(0); 86 | expect(pkgrollProcess.stderr).toBe(''); 87 | 88 | const content = await fixture.readFile('dist/cts.cjs', 'utf8'); 89 | expect(content).toMatch('exports.sayHello ='); 90 | }); 91 | 92 | test('{ type: module, field: main, srcExt: js, distExt: cjs }', async () => { 93 | await using fixture = await createFixture({ 94 | ...packageFixture(), 95 | 'package.json': createPackageJson({ 96 | type: 'module', 97 | main: './dist/index.cjs', 98 | }), 99 | }); 100 | 101 | const pkgrollProcess = await pkgroll([], { 102 | cwd: fixture.path, 103 | nodePath, 104 | }); 105 | 106 | expect(pkgrollProcess.exitCode).toBe(0); 107 | expect(pkgrollProcess.stderr).toBe(''); 108 | 109 | const content = await fixture.readFile('dist/index.cjs', 'utf8'); 110 | expect(content).toMatch('module.exports ='); 111 | }); 112 | 113 | test('{ type: commonjs, field: main, srcExt: mjs, distExt: cjs }', async () => { 114 | await using fixture = await createFixture({ 115 | ...packageFixture(), 116 | 'package.json': createPackageJson({ 117 | main: './dist/mjs.cjs', 118 | }), 119 | }); 120 | 121 | const pkgrollProcess = await pkgroll([], { 122 | cwd: fixture.path, 123 | nodePath, 124 | }); 125 | 126 | expect(pkgrollProcess.exitCode).toBe(0); 127 | expect(pkgrollProcess.stderr).toBe(''); 128 | 129 | const content = await fixture.readFile('dist/mjs.cjs', 'utf8'); 130 | expect(content).toMatch('exports.sayHello ='); 131 | }); 132 | 133 | test('{ type: commonjs, field: component, srcExt: mjs, distExt: cjs }', async () => { 134 | await using fixture = await createFixture({ 135 | ...packageFixture({ installReact: true }), 136 | 'package.json': createPackageJson({ 137 | main: './dist/component.cjs', 138 | peerDependencies: { 139 | react: '*', 140 | }, 141 | }), 142 | 'tsconfig.json': createTsconfigJson({ 143 | compilerOptions: { 144 | jsx: 'react-jsx', 145 | }, 146 | }), 147 | }); 148 | 149 | const pkgrollProcess = await pkgroll([], { 150 | cwd: fixture.path, 151 | nodePath, 152 | }); 153 | 154 | expect(pkgrollProcess.exitCode).toBe(0); 155 | expect(pkgrollProcess.stderr).toBe(''); 156 | 157 | const content = await fixture.readFile('dist/component.cjs', 'utf8'); 158 | expect(content).toMatch('require(\'react/jsx-runtime\')'); 159 | expect(content).toMatch('const Component = () => /* @__PURE__ */ jsxRuntime.jsx("div", { children: "Hello World" })'); 160 | expect(content).toMatch('exports.Component = Component'); 161 | }); 162 | 163 | test('nested directory', async () => { 164 | await using fixture = await createFixture({ 165 | ...packageFixture(), 166 | 'package.json': createPackageJson({ 167 | main: './dist/nested/index.js', 168 | }), 169 | }); 170 | 171 | const pkgrollProcess = await pkgroll([], { 172 | cwd: fixture.path, 173 | nodePath, 174 | }); 175 | 176 | expect(pkgrollProcess.exitCode).toBe(0); 177 | expect(pkgrollProcess.stderr).toBe(''); 178 | 179 | const content = await fixture.readFile('dist/nested/index.js', 'utf8'); 180 | expect(content).toMatch('nested entry point'); 181 | }); 182 | 183 | test('dynamic imports', async () => { 184 | await using fixture = await createFixture({ 185 | ...fixtureDynamicImports, 186 | 'package.json': createPackageJson({ 187 | exports: './dist/dynamic-imports.cjs', 188 | }), 189 | }); 190 | 191 | const pkgrollProcess = await pkgroll([], { 192 | cwd: fixture.path, 193 | nodePath, 194 | }); 195 | 196 | expect(pkgrollProcess.exitCode).toBe(0); 197 | expect(pkgrollProcess.stderr).toBe(''); 198 | 199 | const content = await fixture.readFile('dist/dynamic-imports.cjs', 'utf8'); 200 | expect(content).toMatch('require('); 201 | 202 | const files = await fs.readdir(fixture.getPath('dist')); 203 | files.sort(); 204 | expect(files[0]).toMatch(/^aaa-/); 205 | expect(files[1]).toMatch(/^bbb-/); 206 | expect(files[2]).toMatch(/^ccc-/); 207 | }); 208 | 209 | // https://github.com/privatenumber/pkgroll/issues/104 210 | test('unresolvable dynamic import should not fail', async () => { 211 | await using fixture = await createFixture({ 212 | ...fixtureDynamicImportUnresolvable, 213 | 'package.json': createPackageJson({ 214 | exports: './dist/dynamic-imports.cjs', 215 | }), 216 | }); 217 | 218 | const pkgrollProcess = await pkgroll([], { 219 | cwd: fixture.path, 220 | nodePath, 221 | }); 222 | 223 | expect(pkgrollProcess.exitCode).toBe(0); 224 | expect(pkgrollProcess.stderr).toContain('[plugin rollup-plugin-dynamic-import-variables]'); 225 | 226 | const content = await fixture.readFile('dist/dynamic-imports.cjs', 'utf8'); 227 | expect(content).toMatch('import('); 228 | }); 229 | }); 230 | }); 231 | -------------------------------------------------------------------------------- /tests/specs/builds/output-dual.ts: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs/promises'; 2 | import { testSuite, expect } from 'manten'; 3 | import { createFixture } from 'fs-fixture'; 4 | import { pkgroll } from '../../utils.js'; 5 | import { packageFixture, createPackageJson } from '../../fixtures.js'; 6 | 7 | export default testSuite(({ describe }, nodePath: string) => { 8 | describe('output: commonjs & module', ({ test }) => { 9 | test('dual', async () => { 10 | await using fixture = await createFixture({ 11 | ...packageFixture(), 12 | 'package.json': createPackageJson({ 13 | exports: { 14 | './a': './dist/mjs.cjs', 15 | './b': './dist/value.mjs', 16 | }, 17 | }), 18 | }); 19 | 20 | const pkgrollProcess = await pkgroll([], { 21 | cwd: fixture.path, 22 | nodePath, 23 | }); 24 | expect(pkgrollProcess.exitCode).toBe(0); 25 | expect(pkgrollProcess.stderr).toBe(''); 26 | 27 | const files = await fs.readdir(fixture.getPath('dist')); 28 | files.sort(); 29 | expect(files).toStrictEqual([ 30 | 'mjs.cjs', 31 | 'mjs.mjs', 32 | 'value.cjs', 33 | 'value.mjs', 34 | ]); 35 | }); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /tests/specs/builds/output-module.ts: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs/promises'; 2 | import { pathToFileURL } from 'node:url'; 3 | import { testSuite, expect } from 'manten'; 4 | import { createFixture } from 'fs-fixture'; 5 | import { pkgroll } from '../../utils.js'; 6 | import { 7 | packageFixture, createPackageJson, createTsconfigJson, fixtureDynamicImports, 8 | fixtureDynamicImportUnresolvable, 9 | } from '../../fixtures.js'; 10 | 11 | export default testSuite(({ describe }, nodePath: string) => { 12 | describe('output: module', ({ test }) => { 13 | test('{ type: module, field: main, srcExt: js, distExt: js }', async () => { 14 | await using fixture = await createFixture({ 15 | ...packageFixture(), 16 | 'package.json': createPackageJson({ 17 | type: 'module', 18 | main: './dist/index.js', 19 | }), 20 | }); 21 | 22 | const pkgrollProcess = await pkgroll([], { 23 | cwd: fixture.path, 24 | nodePath, 25 | }); 26 | 27 | expect(pkgrollProcess.exitCode).toBe(0); 28 | expect(pkgrollProcess.stderr).toBe(''); 29 | 30 | const content = await fixture.readFile('dist/index.js', 'utf8'); 31 | expect(content).toMatch('export { index as default }'); 32 | }); 33 | 34 | test('{ type: commonjs, field: main, srcExt: js, distExt: mjs }', async () => { 35 | await using fixture = await createFixture({ 36 | ...packageFixture(), 37 | 'package.json': createPackageJson({ 38 | main: './dist/index.mjs', 39 | }), 40 | }); 41 | 42 | const pkgrollProcess = await pkgroll([], { 43 | cwd: fixture.path, 44 | nodePath, 45 | }); 46 | 47 | expect(pkgrollProcess.exitCode).toBe(0); 48 | expect(pkgrollProcess.stderr).toBe(''); 49 | 50 | const content = await fixture.readFile('dist/index.mjs', 'utf8'); 51 | expect(content).toMatch('export { index as default }'); 52 | }); 53 | 54 | test('{ type: commonjs, field: module, srcExt: js, distExt: js }', async () => { 55 | await using fixture = await createFixture({ 56 | ...packageFixture(), 57 | 'package.json': createPackageJson({ 58 | module: './dist/index.js', 59 | }), 60 | }); 61 | 62 | const pkgrollProcess = await pkgroll([], { 63 | cwd: fixture.path, 64 | nodePath, 65 | }); 66 | 67 | expect(pkgrollProcess.exitCode).toBe(0); 68 | expect(pkgrollProcess.stderr).toBe(''); 69 | 70 | const content = await fixture.readFile('dist/index.js', 'utf8'); 71 | expect(content).toMatch('export { index as default }'); 72 | }); 73 | 74 | test('{ type: commonjs, field: main, srcExt: cjs, distExt: mjs }', async () => { 75 | await using fixture = await createFixture({ 76 | ...packageFixture(), 77 | 'package.json': createPackageJson({ 78 | main: './dist/cjs.mjs', 79 | }), 80 | }); 81 | 82 | const pkgrollProcess = await pkgroll([], { 83 | cwd: fixture.path, 84 | nodePath, 85 | }); 86 | 87 | expect(pkgrollProcess.exitCode).toBe(0); 88 | expect(pkgrollProcess.stderr).toBe(''); 89 | 90 | const content = await fixture.readFile('dist/cjs.mjs', 'utf8'); 91 | expect(content).toMatch('export { cjs as default }'); 92 | }); 93 | 94 | test('{ type: commonjs, field: component, srcExt: tsx, distExt: mjs }', async () => { 95 | await using fixture = await createFixture({ 96 | ...packageFixture({ installReact: true }), 97 | 'package.json': createPackageJson({ 98 | main: './dist/component.mjs', 99 | peerDependencies: { 100 | react: '*', 101 | }, 102 | }), 103 | 'tsconfig.json': createTsconfigJson({ 104 | compilerOptions: { 105 | jsx: 'react-jsx', 106 | }, 107 | }), 108 | }); 109 | 110 | const pkgrollProcess = await pkgroll([], { 111 | cwd: fixture.path, 112 | nodePath, 113 | }); 114 | 115 | expect(pkgrollProcess.exitCode).toBe(0); 116 | expect(pkgrollProcess.stderr).toBe(''); 117 | 118 | const content = await fixture.readFile('dist/component.mjs', 'utf8'); 119 | expect(content).toMatch('import { jsx } from \'react/jsx-runtime\''); 120 | expect(content).toMatch('const Component = () => /* @__PURE__ */ jsx("div", { children: "Hello World" })'); 121 | expect(content).toMatch('export { Component }'); 122 | }); 123 | 124 | test('{ type: commonjs, field: main, srcExt: mts, distExt: mjs }', async () => { 125 | await using fixture = await createFixture({ 126 | ...packageFixture(), 127 | 'package.json': createPackageJson({ 128 | main: './dist/mts.mjs', 129 | }), 130 | }); 131 | 132 | const pkgrollProcess = await pkgroll([], { 133 | cwd: fixture.path, 134 | nodePath, 135 | }); 136 | 137 | expect(pkgrollProcess.exitCode).toBe(0); 138 | expect(pkgrollProcess.stderr).toBe(''); 139 | 140 | const content = await fixture.readFile('dist/mts.mjs', 'utf8'); 141 | expect(content).toMatch('export { foo, sayGoodbye, sayHello, sayHello$1 as sayHello2 }'); 142 | }); 143 | 144 | test('{ type: commonjs, field: main, srcExt: cts, distExt: mjs }', async () => { 145 | await using fixture = await createFixture({ 146 | ...packageFixture(), 147 | 'package.json': createPackageJson({ 148 | main: './dist/cts.mjs', 149 | }), 150 | }); 151 | 152 | const pkgrollProcess = await pkgroll([], { 153 | cwd: fixture.path, 154 | nodePath, 155 | }); 156 | 157 | expect(pkgrollProcess.exitCode).toBe(0); 158 | expect(pkgrollProcess.stderr).toBe(''); 159 | 160 | const content = await fixture.readFile('dist/cts.mjs', 'utf8'); 161 | expect(content).toMatch('export { sayHello }'); 162 | }); 163 | 164 | test('{ type: module, field: main, srcExt: cts, distExt: js }', async () => { 165 | await using fixture = await createFixture({ 166 | ...packageFixture(), 167 | 'package.json': createPackageJson({ 168 | type: 'module', 169 | main: './dist/cts.js', 170 | }), 171 | }); 172 | 173 | const pkgrollProcess = await pkgroll([], { 174 | cwd: fixture.path, 175 | nodePath, 176 | }); 177 | 178 | expect(pkgrollProcess.exitCode).toBe(0); 179 | expect(pkgrollProcess.stderr).toBe(''); 180 | 181 | const content = await fixture.readFile('dist/cts.js', 'utf8'); 182 | expect(content).toMatch('export { sayHello }'); 183 | }); 184 | 185 | test('require() gets converted to import in esm', async () => { 186 | await using fixture = await createFixture({ 187 | ...packageFixture(), 188 | 'package.json': createPackageJson({ 189 | main: './dist/require.js', 190 | module: './dist/require.mjs', 191 | }), 192 | }); 193 | 194 | const pkgrollProcess = await pkgroll(['--minify'], { 195 | cwd: fixture.path, 196 | nodePath, 197 | }); 198 | 199 | expect(pkgrollProcess.exitCode).toBe(0); 200 | expect(pkgrollProcess.stderr).toBe(''); 201 | 202 | const js = await fixture.readFile('dist/require.js', 'utf8'); 203 | expect(js).not.toMatch('createRequire'); 204 | 205 | const mjs = await fixture.readFile('dist/require.mjs', 'utf8'); 206 | expect(mjs).not.toMatch('require('); 207 | expect(mjs).toMatch(/import . from"fs"/); 208 | }); 209 | 210 | test('conditional require() no side-effects', async () => { 211 | await using fixture = await createFixture({ 212 | ...packageFixture(), 213 | 'package.json': createPackageJson({ 214 | main: './dist/conditional-require.mjs', 215 | }), 216 | }); 217 | 218 | const pkgrollProcess = await pkgroll([], { 219 | cwd: fixture.path, 220 | nodePath, 221 | }); 222 | 223 | expect(pkgrollProcess.exitCode).toBe(0); 224 | expect(pkgrollProcess.stderr).toBe(''); 225 | 226 | const content = await fixture.readFile('dist/conditional-require.mjs', 'utf8'); 227 | expect(content).toMatch('\tconsole.log("side effect");'); 228 | }); 229 | 230 | test('require() & createRequire gets completely removed on conditional', async () => { 231 | await using fixture = await createFixture({ 232 | ...packageFixture(), 233 | 'package.json': createPackageJson({ 234 | main: './dist/conditional-require.mjs', 235 | }), 236 | }); 237 | 238 | const pkgrollProcess = await pkgroll(['--env.NODE_ENV=development', '--minify'], { 239 | cwd: fixture.path, 240 | nodePath, 241 | }); 242 | 243 | expect(pkgrollProcess.exitCode).toBe(0); 244 | expect(pkgrollProcess.stderr).toBe(''); 245 | 246 | const content = await fixture.readFile('dist/conditional-require.mjs', 'utf8'); 247 | expect(content).not.toMatch('\tconsole.log(\'side effect\');'); 248 | expect(content).not.toMatch('require('); 249 | expect(content).toMatch('"development"'); 250 | }); 251 | 252 | describe('injects createRequire', ({ test }) => { 253 | test('dynamic require should get a createRequire', async () => { 254 | await using fixture = await createFixture({ 255 | 'src/dynamic-require.ts': 'require((() => \'fs\')());', 256 | 'package.json': createPackageJson({ 257 | main: './dist/dynamic-require.mjs', 258 | }), 259 | }); 260 | 261 | const pkgrollProcess = await pkgroll([], { 262 | cwd: fixture.path, 263 | nodePath, 264 | }); 265 | 266 | expect(pkgrollProcess.exitCode).toBe(0); 267 | expect(pkgrollProcess.stderr).toBe(''); 268 | 269 | const content = await fixture.readFile('dist/dynamic-require.mjs', 'utf8'); 270 | expect(content).toMatch('createRequire'); 271 | expect(content).toMatch('(import.meta.url)'); 272 | 273 | // Shouldn't throw 274 | await import(pathToFileURL(fixture.getPath('dist/dynamic-require.mjs')).toString()); 275 | }); 276 | 277 | test('defined require should not get a createRequire', async () => { 278 | await using fixture = await createFixture({ 279 | 'src/dynamic-require.ts': 'const require=console.log; require((() => \'fs\')());', 280 | 'package.json': createPackageJson({ 281 | main: './dist/dynamic-require.mjs', 282 | }), 283 | }); 284 | 285 | const pkgrollProcess = await pkgroll([], { 286 | cwd: fixture.path, 287 | nodePath, 288 | }); 289 | 290 | expect(pkgrollProcess.exitCode).toBe(0); 291 | expect(pkgrollProcess.stderr).toBe(''); 292 | 293 | const content = await fixture.readFile('dist/dynamic-require.mjs', 'utf8'); 294 | expect(content).not.toMatch('createRequire'); 295 | expect(content).not.toMatch('(import.meta.url)'); 296 | expect(content).toMatch('"fs"'); 297 | }); 298 | 299 | test('object property should not get a createRequire', async () => { 300 | await using fixture = await createFixture({ 301 | 'src/dynamic-require.ts': 'console.log({ require: 1 });', 302 | 'package.json': createPackageJson({ 303 | main: './dist/dynamic-require.mjs', 304 | }), 305 | }); 306 | 307 | const pkgrollProcess = await pkgroll([], { 308 | cwd: fixture.path, 309 | nodePath, 310 | }); 311 | 312 | expect(pkgrollProcess.exitCode).toBe(0); 313 | expect(pkgrollProcess.stderr).toBe(''); 314 | 315 | const content = await fixture.readFile('dist/dynamic-require.mjs', 'utf8'); 316 | expect(content).not.toMatch('createRequire'); 317 | expect(content).not.toMatch('(import.meta.url)'); 318 | }); 319 | }); 320 | 321 | test('dynamic imports', async () => { 322 | await using fixture = await createFixture({ 323 | ...fixtureDynamicImports, 324 | 'package.json': createPackageJson({ 325 | exports: './dist/dynamic-imports.mjs', 326 | }), 327 | }); 328 | 329 | const pkgrollProcess = await pkgroll([], { 330 | cwd: fixture.path, 331 | nodePath, 332 | }); 333 | 334 | expect(pkgrollProcess.exitCode).toBe(0); 335 | expect(pkgrollProcess.stderr).toBe(''); 336 | 337 | const content = await fixture.readFile('dist/dynamic-imports.mjs', 'utf8'); 338 | expect(content).toMatch('import('); 339 | 340 | const files = await fs.readdir(fixture.getPath('dist')); 341 | files.sort(); 342 | expect(files[0]).toMatch(/^aaa-/); 343 | expect(files[1]).toMatch(/^bbb-/); 344 | expect(files[2]).toMatch(/^ccc-/); 345 | }); 346 | 347 | // https://github.com/privatenumber/pkgroll/issues/104 348 | test('unresolvable dynamic import should not fail', async () => { 349 | await using fixture = await createFixture({ 350 | ...fixtureDynamicImportUnresolvable, 351 | 'package.json': createPackageJson({ 352 | exports: './dist/dynamic-imports.mjs', 353 | }), 354 | }); 355 | 356 | const pkgrollProcess = await pkgroll([], { 357 | cwd: fixture.path, 358 | nodePath, 359 | }); 360 | 361 | expect(pkgrollProcess.exitCode).toBe(0); 362 | expect(pkgrollProcess.stderr).toContain('[plugin rollup-plugin-dynamic-import-variables]'); 363 | 364 | const content = await fixture.readFile('dist/dynamic-imports.mjs', 'utf8'); 365 | expect(content).toMatch('import('); 366 | }); 367 | 368 | // https://github.com/privatenumber/pkgroll/issues/115 369 | test('import.meta.url should be preserved', async () => { 370 | await using fixture = await createFixture({ 371 | 'src/index.js': 'console.log(import.meta.url)', 372 | 'package.json': createPackageJson({ 373 | exports: './dist/index.mjs', 374 | }), 375 | }); 376 | 377 | const pkgrollProcess = await pkgroll(['--target=es2017'], { 378 | cwd: fixture.path, 379 | nodePath, 380 | }); 381 | expect(pkgrollProcess.exitCode).toBe(0); 382 | 383 | const content = await fixture.readFile('dist/index.mjs', 'utf8'); 384 | expect(content).toMatch('import.meta.url'); 385 | }); 386 | }); 387 | }); 388 | -------------------------------------------------------------------------------- /tests/specs/builds/output-types.ts: -------------------------------------------------------------------------------- 1 | import path from 'node:path'; 2 | import { testSuite, expect } from 'manten'; 3 | import { createFixture } from 'fs-fixture'; 4 | import { pkgroll } from '../../utils.js'; 5 | import { 6 | packageFixture, 7 | installTypeScript, 8 | createPackageJson, 9 | createTsconfigJson, 10 | } from '../../fixtures.js'; 11 | 12 | export default testSuite(({ describe }, nodePath: string) => { 13 | describe('types', ({ test }) => { 14 | test('emits', async () => { 15 | await using fixture = await createFixture({ 16 | ...packageFixture({ installTypeScript: true }), 17 | 'package.json': createPackageJson({ 18 | types: './dist/utils.d.ts', 19 | }), 20 | 'tsconfig.json': createTsconfigJson({ 21 | compilerOptions: { 22 | // Check that it handles different module types 23 | module: 'NodeNext', 24 | typeRoots: [ 25 | path.resolve('node_modules/@types'), 26 | ], 27 | }, 28 | }), 29 | }); 30 | 31 | const pkgrollProcess = await pkgroll([], { 32 | cwd: fixture.path, 33 | nodePath, 34 | }); 35 | 36 | expect(pkgrollProcess.exitCode).toBe(0); 37 | expect(pkgrollProcess.stderr).toBe(''); 38 | 39 | const content = await fixture.readFile('dist/utils.d.ts', 'utf8'); 40 | expect(content).toMatch('declare function'); 41 | }); 42 | 43 | test('{ srcExt: mts, distExt: d.ts }', async () => { 44 | await using fixture = await createFixture({ 45 | ...packageFixture({ installTypeScript: true }), 46 | 'package.json': createPackageJson({ 47 | types: './dist/mts.d.ts', 48 | }), 49 | }); 50 | 51 | const pkgrollProcess = await pkgroll([], { 52 | cwd: fixture.path, 53 | nodePath, 54 | }); 55 | 56 | expect(pkgrollProcess.exitCode).toBe(0); 57 | expect(pkgrollProcess.stderr).toBe(''); 58 | 59 | const content = await fixture.readFile('dist/mts.d.ts', 'utf8'); 60 | expect(content).toMatch('declare function'); 61 | }); 62 | 63 | test('{ srcExt: tsx, distExt: d.ts }', async () => { 64 | await using fixture = await createFixture({ 65 | ...packageFixture({ 66 | installTypeScript: true, 67 | installReact: true, 68 | }), 69 | 'package.json': createPackageJson({ 70 | types: './dist/component.d.ts', 71 | peerDependencies: { 72 | react: '*', 73 | }, 74 | }), 75 | 'tsconfig.json': createTsconfigJson({ 76 | compilerOptions: { 77 | jsx: 'react-jsx', 78 | }, 79 | }), 80 | }); 81 | 82 | const pkgrollProcess = await pkgroll([], { 83 | cwd: fixture.path, 84 | nodePath, 85 | }); 86 | 87 | expect(pkgrollProcess.exitCode).toBe(0); 88 | expect(pkgrollProcess.stderr).toBe(''); 89 | 90 | const content = await fixture.readFile('dist/component.d.ts', 'utf8'); 91 | expect(content).toMatch('import * as react_jsx_runtime from \'react/jsx-runtime\''); 92 | expect(content).toMatch('declare const Component: () => react_jsx_runtime.JSX.Element'); 93 | expect(content).toMatch('export { Component }'); 94 | }); 95 | 96 | test('{ srcExt: tsx, distExt: d.mts }', async () => { 97 | await using fixture = await createFixture({ 98 | ...packageFixture({ 99 | installTypeScript: true, 100 | installReact: true, 101 | }), 102 | 'package.json': createPackageJson({ 103 | types: './dist/component.d.mts', 104 | peerDependencies: { 105 | react: '*', 106 | }, 107 | }), 108 | 'tsconfig.json': createTsconfigJson({ 109 | compilerOptions: { 110 | jsx: 'react-jsx', 111 | }, 112 | }), 113 | }); 114 | 115 | const pkgrollProcess = await pkgroll([], { 116 | cwd: fixture.path, 117 | nodePath, 118 | }); 119 | 120 | expect(pkgrollProcess.exitCode).toBe(0); 121 | expect(pkgrollProcess.stderr).toBe(''); 122 | 123 | const content = await fixture.readFile('dist/component.d.mts', 'utf8'); 124 | expect(content).toMatch('import * as react_jsx_runtime from \'react/jsx-runtime\''); 125 | expect(content).toMatch('declare const Component: () => react_jsx_runtime.JSX.Element'); 126 | expect(content).toMatch('export { Component }'); 127 | }); 128 | 129 | test('{ srcExt: tsx, distExt: d.cts }', async () => { 130 | await using fixture = await createFixture({ 131 | ...packageFixture({ 132 | installTypeScript: true, 133 | installReact: true, 134 | }), 135 | 'package.json': createPackageJson({ 136 | types: './dist/component.d.cts', 137 | peerDependencies: { 138 | react: '*', 139 | }, 140 | }), 141 | 'tsconfig.json': createTsconfigJson({ 142 | compilerOptions: { 143 | jsx: 'react-jsx', 144 | }, 145 | }), 146 | }); 147 | 148 | const pkgrollProcess = await pkgroll([], { 149 | cwd: fixture.path, 150 | nodePath, 151 | }); 152 | 153 | expect(pkgrollProcess.exitCode).toBe(0); 154 | expect(pkgrollProcess.stderr).toBe(''); 155 | 156 | const content = await fixture.readFile('dist/component.d.cts', 'utf8'); 157 | expect(content).toMatch('import * as react_jsx_runtime from \'react/jsx-runtime\''); 158 | expect(content).toMatch('declare const Component: () => react_jsx_runtime.JSX.Element'); 159 | expect(content).toMatch('export { Component }'); 160 | }); 161 | 162 | test('{ srcExt: .mts, distExt: d.cts }', async () => { 163 | await using fixture = await createFixture({ 164 | ...packageFixture({ installTypeScript: true }), 165 | 'package.json': createPackageJson({ 166 | types: './dist/mts.d.cts', 167 | }), 168 | }); 169 | 170 | const pkgrollProcess = await pkgroll([], { 171 | cwd: fixture.path, 172 | nodePath, 173 | }); 174 | 175 | expect(pkgrollProcess.exitCode).toBe(0); 176 | expect(pkgrollProcess.stderr).toBe(''); 177 | 178 | const content = await fixture.readFile('dist/mts.d.cts', 'utf8'); 179 | expect(content).toMatch('declare function'); 180 | }); 181 | 182 | test('{ srcExt: .mts, distExt: d.mts }', async () => { 183 | await using fixture = await createFixture({ 184 | ...packageFixture({ installTypeScript: true }), 185 | 'package.json': createPackageJson({ 186 | types: './dist/mts.d.mts', 187 | }), 188 | }); 189 | 190 | const pkgrollProcess = await pkgroll([], { 191 | cwd: fixture.path, 192 | nodePath, 193 | }); 194 | 195 | expect(pkgrollProcess.exitCode).toBe(0); 196 | expect(pkgrollProcess.stderr).toBe(''); 197 | 198 | const content = await fixture.readFile('dist/mts.d.mts', 'utf8'); 199 | expect(content).toMatch('declare function'); 200 | }); 201 | 202 | test('emits multiple', async () => { 203 | await using fixture = await createFixture({ 204 | ...packageFixture({ installTypeScript: true }), 205 | 'package.json': createPackageJson({ 206 | exports: { 207 | './utils.js': { 208 | types: './dist/utils.d.ts', 209 | }, 210 | './nested': { 211 | types: './dist/nested/index.d.ts', 212 | }, 213 | }, 214 | }), 215 | 'tsconfig.json': createTsconfigJson({ 216 | compilerOptions: { 217 | typeRoots: [ 218 | path.resolve('node_modules/@types'), 219 | ], 220 | }, 221 | }), 222 | }); 223 | 224 | const pkgrollProcess = await pkgroll([], { 225 | cwd: fixture.path, 226 | nodePath, 227 | }); 228 | 229 | expect(pkgrollProcess.exitCode).toBe(0); 230 | expect(pkgrollProcess.stderr).toBe(''); 231 | 232 | const utilsDts = await fixture.readFile('dist/utils.d.ts', 'utf8'); 233 | expect(utilsDts).toMatch('declare function'); 234 | 235 | const nestedDts = await fixture.readFile('dist/nested/index.d.ts', 'utf8'); 236 | expect(nestedDts).toMatch('declare function sayHello'); 237 | }); 238 | 239 | test('emits multiple - same name', async () => { 240 | await using fixture = await createFixture({ 241 | ...packageFixture({ installTypeScript: true }), 242 | 'package.json': createPackageJson({ 243 | exports: { 244 | './a': { 245 | types: './dist/utils.d.ts', 246 | }, 247 | './b': { 248 | types: './dist/nested/utils.d.ts', 249 | }, 250 | }, 251 | }), 252 | 'tsconfig.json': createTsconfigJson({ 253 | compilerOptions: { 254 | typeRoots: [ 255 | path.resolve('node_modules/@types'), 256 | ], 257 | }, 258 | }), 259 | }); 260 | 261 | const pkgrollProcess = await pkgroll([], { 262 | cwd: fixture.path, 263 | nodePath, 264 | }); 265 | 266 | expect(pkgrollProcess.exitCode).toBe(0); 267 | expect(pkgrollProcess.stderr).toBe(''); 268 | 269 | const utilsDts = await fixture.readFile('dist/utils.d.ts', 'utf8'); 270 | expect(utilsDts).toMatch('declare function sayHello'); 271 | 272 | const nestedDts = await fixture.readFile('dist/nested/utils.d.ts', 'utf8'); 273 | expect(nestedDts).toMatch('declare function sayGoodbye'); 274 | }); 275 | 276 | test('emits multiple - different extension', async () => { 277 | await using fixture = await createFixture({ 278 | ...packageFixture({ installTypeScript: true }), 279 | 'package.json': createPackageJson({ 280 | exports: { 281 | require: { 282 | types: './dist/utils.d.cts', 283 | default: './dist/utils.cjs', 284 | }, 285 | import: { 286 | types: './dist/utils.d.mts', 287 | default: './dist/utils.mjs', 288 | }, 289 | }, 290 | }), 291 | 'tsconfig.json': createTsconfigJson({ 292 | compilerOptions: { 293 | typeRoots: [ 294 | path.resolve('node_modules/@types'), 295 | ], 296 | }, 297 | }), 298 | }); 299 | 300 | const pkgrollProcess = await pkgroll([], { 301 | cwd: fixture.path, 302 | nodePath, 303 | }); 304 | 305 | expect(pkgrollProcess.exitCode).toBe(0); 306 | expect(pkgrollProcess.stderr).toBe(''); 307 | 308 | const utilsDMts = await fixture.readFile('dist/utils.d.mts', 'utf8'); 309 | expect(utilsDMts).toMatch('declare function sayHello'); 310 | 311 | const utilsDCts = await fixture.readFile('dist/utils.d.cts', 'utf8'); 312 | expect(utilsDCts).toMatch('declare function sayHello'); 313 | }); 314 | 315 | test('bundles .d.ts', async () => { 316 | await using fixture = await createFixture({ 317 | ...packageFixture({ installTypeScript: true }), 318 | 'package.json': createPackageJson({ 319 | types: './dist/dts.d.ts', 320 | }), 321 | }); 322 | 323 | const pkgrollProcess = await pkgroll([], { 324 | cwd: fixture.path, 325 | nodePath, 326 | }); 327 | 328 | expect(pkgrollProcess.exitCode).toBe(0); 329 | expect(pkgrollProcess.stderr).toBe(''); 330 | 331 | const content = await fixture.readFile('dist/dts.d.ts', 'utf8'); 332 | expect(content).toMatch('declare const'); 333 | }); 334 | 335 | test('composite monorepos', async () => { 336 | await using fixture = await createFixture({ 337 | ...installTypeScript, 338 | packages: { 339 | one: { 340 | 'package.json': createPackageJson({ 341 | name: '@org/one', 342 | exports: { 343 | types: './dist/index.d.mts', 344 | }, 345 | }), 346 | 'tsconfig.json': createTsconfigJson({ 347 | compilerOptions: { 348 | composite: true, 349 | }, 350 | include: [ 351 | 'src/index.mts', 352 | 'src/name.mts', 353 | ], 354 | }), 355 | src: { 356 | 'index.mts': 'export { Name } from "./name.mjs";', 357 | 'name.mts': 'export type Name = string;', 358 | }, 359 | }, 360 | two: { 361 | 'package.json': createPackageJson({ 362 | main: './dist/index.mjs', 363 | dependencies: { 364 | '@org/one': 'workspace:*', 365 | }, 366 | }), 367 | 'tsconfig.json': createTsconfigJson({ 368 | compilerOptions: { 369 | composite: true, 370 | }, 371 | include: ['src/index.mts'], 372 | references: [{ path: '../one' }], 373 | }), 374 | 'src/index.mts': ` 375 | import { Name } from '@org/one'; 376 | export function sayHello(name: Name) { 377 | console.log('Hello', name); 378 | } 379 | `, 380 | }, 381 | }, 382 | 'tsconfig.json': createTsconfigJson({ 383 | references: [ 384 | { path: './packages/one' }, 385 | { path: './packages/two' }, 386 | ], 387 | }), 388 | 'package.json': createPackageJson({ 389 | workspaces: ['packages/*'], 390 | }), 391 | }); 392 | 393 | const pkgrollOne = await pkgroll([], { 394 | cwd: `${fixture.path}/packages/one`, 395 | nodePath, 396 | }); 397 | expect(pkgrollOne.exitCode).toBe(0); 398 | expect(pkgrollOne.stderr).toBe(''); 399 | 400 | const contentOne = await fixture.readFile('packages/one/dist/index.d.mts', 'utf8'); 401 | expect(contentOne).toMatch('export type { Name };'); 402 | 403 | const pkgrollTwo = await pkgroll([], { 404 | cwd: `${fixture.path}/packages/two`, 405 | nodePath, 406 | }); 407 | expect(pkgrollTwo.exitCode).toBe(0); 408 | expect(pkgrollTwo.stderr).toBe(''); 409 | 410 | const contentTwo = await fixture.readFile('packages/two/dist/index.mjs', 'utf8'); 411 | expect(contentTwo).toMatch('export { sayHello };'); 412 | }); 413 | 414 | test('symlinks', async () => { 415 | await using fixture = await createFixture({ 416 | ...installTypeScript, 417 | 'package.json': createPackageJson({ 418 | types: './dist/index.d.ts', 419 | peerDependencies: { 420 | 'dep-a': '*', 421 | }, 422 | }), 423 | 'src/index.ts': ` 424 | import { fn } from 'dep-a'; 425 | export default fn({ value: 1 }); 426 | `, 427 | node_modules: { 428 | 'dep-a/index.d.ts': ({ symlink }) => symlink('../../store/dep-a/index.d.ts'), 429 | }, 430 | store: { 431 | 'dep-a': { 432 | 'node_modules/dep-b/index.d.ts': ` 433 | type data = { 434 | value: number; 435 | }; 436 | export declare function fn(a: data): data; 437 | `, 438 | 'index.d.ts': 'export * from \'dep-b\';', 439 | }, 440 | }, 441 | }); 442 | 443 | const pkgrollOne = await pkgroll([], { 444 | cwd: fixture.path, 445 | nodePath, 446 | }); 447 | expect(pkgrollOne.stderr).toBe(''); 448 | 449 | const types = await fixture.readFile('dist/index.d.ts', 'utf8'); 450 | expect(types).toMatch('\'dep-a\''); 451 | expect(types).toMatch('.data'); 452 | }); 453 | 454 | test('custom tsconfig.json path', async () => { 455 | await using fixture = await createFixture({ 456 | ...packageFixture({ 457 | installTypeScript: true, 458 | installReact: true, 459 | }), 460 | 'package.json': createPackageJson({ 461 | types: './dist/component.d.ts', 462 | peerDependencies: { 463 | react: '*', 464 | }, 465 | }), 466 | 'tsconfig.custom.json': createTsconfigJson({ 467 | compilerOptions: { 468 | jsx: 'react-jsx', 469 | }, 470 | }), 471 | }); 472 | 473 | const pkgrollProcess = await pkgroll(['-p', 'tsconfig.custom.json'], { 474 | cwd: fixture.path, 475 | nodePath, 476 | }); 477 | 478 | expect(pkgrollProcess.exitCode).toBe(0); 479 | expect(pkgrollProcess.stderr).toBe(''); 480 | 481 | const content = await fixture.readFile('dist/component.d.ts', 'utf8'); 482 | expect(content).toMatch('declare const Component: () => react_jsx_runtime.JSX.Element'); 483 | expect(content).toMatch('export { Component }'); 484 | }); 485 | }); 486 | }); 487 | -------------------------------------------------------------------------------- /tests/specs/builds/package-exports.ts: -------------------------------------------------------------------------------- 1 | import { testSuite, expect } from 'manten'; 2 | import { createFixture } from 'fs-fixture'; 3 | import { pkgroll } from '../../utils.js'; 4 | import { packageFixture, createPackageJson } from '../../fixtures.js'; 5 | 6 | export default testSuite(({ describe }, nodePath: string) => { 7 | describe('package exports', ({ test }) => { 8 | test('string', async () => { 9 | await using fixture = await createFixture({ 10 | ...packageFixture(), 11 | 'package.json': createPackageJson({ 12 | exports: './dist/index.js', 13 | }), 14 | }); 15 | 16 | const pkgrollProcess = await pkgroll([], { 17 | cwd: fixture.path, 18 | nodePath, 19 | }); 20 | 21 | expect(pkgrollProcess.exitCode).toBe(0); 22 | expect(pkgrollProcess.stderr).toBe(''); 23 | 24 | const content = await fixture.readFile('dist/index.js', 'utf8'); 25 | expect(content).toMatch('module.exports ='); 26 | }); 27 | 28 | test('type module - string', async () => { 29 | await using fixture = await createFixture({ 30 | ...packageFixture(), 31 | 'package.json': createPackageJson({ 32 | type: 'module', 33 | exports: './dist/index.js', 34 | }), 35 | }); 36 | 37 | const pkgrollProcess = await pkgroll([], { 38 | cwd: fixture.path, 39 | nodePath, 40 | }); 41 | 42 | expect(pkgrollProcess.exitCode).toBe(0); 43 | expect(pkgrollProcess.stderr).toBe(''); 44 | 45 | const content = await fixture.readFile('dist/index.js', 'utf8'); 46 | expect(content).toMatch('export {'); 47 | }); 48 | 49 | test('type module - object - string', async () => { 50 | await using fixture = await createFixture({ 51 | ...packageFixture(), 52 | 'package.json': createPackageJson({ 53 | type: 'module', 54 | exports: { 55 | './something': './dist/index.js', 56 | }, 57 | }), 58 | }); 59 | 60 | const pkgrollProcess = await pkgroll([], { 61 | cwd: fixture.path, 62 | nodePath, 63 | }); 64 | 65 | expect(pkgrollProcess.exitCode).toBe(0); 66 | expect(pkgrollProcess.stderr).toBe(''); 67 | 68 | const content = await fixture.readFile('dist/index.js', 'utf8'); 69 | expect(content).toMatch('export {'); 70 | }); 71 | 72 | /** 73 | * This test generates an extra index.cjs, because the rollup 74 | * config generator finds that they can be build in the same config. 75 | * 76 | * This actually seems more performant because only one build is procuced 77 | * instead of two just to remove one file. If this is problematic, 78 | * we can consider deleting or intercepting file emission. 79 | */ 80 | test('conditions', async () => { 81 | await using fixture = await createFixture({ 82 | ...packageFixture(), 83 | 'package.json': createPackageJson({ 84 | exports: { 85 | node: { 86 | import: './dist/utils.mjs', 87 | require: './dist/utils.cjs', 88 | }, 89 | default: './dist/index.js', 90 | }, 91 | }), 92 | }); 93 | 94 | const pkgrollProcess = await pkgroll([], { 95 | cwd: fixture.path, 96 | nodePath, 97 | }); 98 | 99 | expect(pkgrollProcess.exitCode).toBe(0); 100 | expect(pkgrollProcess.stderr).toBe(''); 101 | 102 | const indexMjs = await fixture.readFile('dist/index.js', 'utf8'); 103 | expect(indexMjs).toMatch('module.exports ='); 104 | 105 | const utilsMjs = await fixture.readFile('dist/utils.mjs', 'utf8'); 106 | expect(utilsMjs).toMatch('export {'); 107 | 108 | const utilsCjs = await fixture.readFile('dist/utils.cjs', 'utf8'); 109 | expect(utilsCjs).toMatch('exports.sayHello ='); 110 | }); 111 | 112 | test('conditions - types', async () => { 113 | await using fixture = await createFixture({ 114 | ...packageFixture({ 115 | installTypeScript: true, 116 | }), 117 | 'package.json': createPackageJson({ 118 | type: 'module', 119 | exports: { 120 | types: { 121 | default: './dist/mts.d.ts', 122 | }, 123 | import: './dist/mts.js', 124 | }, 125 | }), 126 | }); 127 | 128 | const pkgrollProcess = await pkgroll([], { 129 | cwd: fixture.path, 130 | nodePath, 131 | }); 132 | 133 | expect(pkgrollProcess.exitCode).toBe(0); 134 | expect(pkgrollProcess.stderr).toBe(''); 135 | 136 | const indexMjs = await fixture.readFile('dist/mts.js', 'utf8'); 137 | expect(indexMjs).toMatch('function sayGoodbye(name) {'); 138 | 139 | const indexDts = await fixture.readFile('dist/mts.d.ts', 'utf8'); 140 | expect(indexDts).toMatch('declare function sayGoodbye(name: string): void;'); 141 | }); 142 | 143 | test('conditions - import should allow cjs', async () => { 144 | await using fixture = await createFixture({ 145 | ...packageFixture(), 146 | 'package.json': createPackageJson({ 147 | exports: { 148 | node: { 149 | import: './dist/utils.js', 150 | }, 151 | default: './dist/index.js', 152 | }, 153 | }), 154 | }); 155 | 156 | const pkgrollProcess = await pkgroll([], { 157 | cwd: fixture.path, 158 | nodePath, 159 | }); 160 | 161 | expect(pkgrollProcess.exitCode).toBe(0); 162 | expect(pkgrollProcess.stderr).toBe(''); 163 | 164 | const indexMjs = await fixture.readFile('dist/index.js', 'utf8'); 165 | expect(indexMjs).toMatch('module.exports ='); 166 | 167 | const utilsMjs = await fixture.readFile('dist/utils.js', 'utf8'); 168 | expect(utilsMjs).toMatch('exports.sayHello ='); 169 | }); 170 | 171 | test('get basename with dot', async () => { 172 | await using fixture = await createFixture({ 173 | ...packageFixture({ 174 | installTypeScript: true, 175 | }), 176 | src: { 177 | 'index.node.ts': 'export default () => "foo";', 178 | nested: { 179 | 'index.node.ts': 'export default () => "foo";', 180 | }, 181 | }, 182 | 'package.json': createPackageJson({ 183 | exports: { 184 | './': { 185 | default: './dist/index.node.js', 186 | types: './dist/index.node.d.ts', 187 | }, 188 | './nested': { 189 | default: './dist/nested/index.node.js', 190 | types: './dist/nested/index.node.d.ts', 191 | }, 192 | }, 193 | }), 194 | }); 195 | 196 | const pkgrollProcess = await pkgroll([], { 197 | cwd: fixture.path, 198 | nodePath, 199 | }); 200 | 201 | expect(pkgrollProcess.exitCode).toBe(0); 202 | expect(pkgrollProcess.stderr).toBe(''); 203 | 204 | const content = await fixture.readFile('dist/index.node.js', 'utf8'); 205 | expect(content).toMatch('module.exports ='); 206 | await fixture.exists('dist/index.node.d.ts'); 207 | await fixture.exists('dist/nested/index.node.js'); 208 | await fixture.exists('dist/nested/index.node.d.ts'); 209 | }); 210 | 211 | test('publishConfig', async () => { 212 | await using fixture = await createFixture({ 213 | ...packageFixture(), 214 | 'package.json': createPackageJson({ 215 | exports: './dist/invalid.js', 216 | publishConfig: { 217 | exports: './dist/index.js', 218 | }, 219 | }), 220 | }); 221 | 222 | const pkgrollProcess = await pkgroll([], { 223 | cwd: fixture.path, 224 | nodePath, 225 | }); 226 | 227 | expect(pkgrollProcess.exitCode).toBe(0); 228 | expect(pkgrollProcess.stderr).toBe(''); 229 | 230 | const content = await fixture.readFile('dist/index.js', 'utf8'); 231 | expect(content).toMatch('module.exports ='); 232 | }); 233 | }); 234 | }); 235 | -------------------------------------------------------------------------------- /tests/specs/builds/package-imports.ts: -------------------------------------------------------------------------------- 1 | import { testSuite, expect } from 'manten'; 2 | import { createFixture } from 'fs-fixture'; 3 | import outdent from 'outdent'; 4 | import { pkgroll } from '../../utils.js'; 5 | import { packageFixture, createPackageJson } from '../../fixtures.js'; 6 | 7 | export default testSuite(({ describe }, nodePath: string) => { 8 | describe('package imports', ({ test }) => { 9 | test('imports', async () => { 10 | await using fixture = await createFixture({ 11 | ...packageFixture(), 12 | 'package.json': createPackageJson({ 13 | main: './dist/entry.js', 14 | imports: { 15 | // @ts-expect-error Invalid subpath import 16 | '~': './src/nested', 17 | }, 18 | }), 19 | 'src/entry.ts': outdent` 20 | import { sayGoodbye } from '~/utils.js'; 21 | console.log(sayGoodbye); 22 | `, 23 | }); 24 | 25 | const pkgrollProcess = await pkgroll([], { 26 | cwd: fixture.path, 27 | nodePath, 28 | }); 29 | 30 | expect(pkgrollProcess.exitCode).toBe(0); 31 | expect(pkgrollProcess.stderr).toBe(''); 32 | 33 | const content = await fixture.readFile('dist/entry.js', 'utf8'); 34 | expect(content).toMatch('sayGoodbye'); 35 | }); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /tests/specs/builds/sourcemap.ts: -------------------------------------------------------------------------------- 1 | import { testSuite, expect } from 'manten'; 2 | import { createFixture } from 'fs-fixture'; 3 | import { pkgroll } from '../../utils.js'; 4 | import { packageFixture, createPackageJson } from '../../fixtures.js'; 5 | 6 | export default testSuite(({ describe }, nodePath: string) => { 7 | describe('generate sourcemap', ({ test }) => { 8 | test('separate files', async () => { 9 | await using fixture = await createFixture({ 10 | ...packageFixture(), 11 | 'package.json': createPackageJson({ 12 | main: './dist/index.js', 13 | module: './dist/index.mjs', 14 | }), 15 | }); 16 | 17 | const pkgrollProcess = await pkgroll( 18 | ['--sourcemap'], 19 | { 20 | cwd: fixture.path, 21 | nodePath, 22 | }, 23 | ); 24 | 25 | expect(pkgrollProcess.exitCode).toBe(0); 26 | expect(pkgrollProcess.stderr).toBe(''); 27 | 28 | expect(await fixture.exists('dist/index.js.map')).toBe(true); 29 | expect(await fixture.exists('dist/index.mjs.map')).toBe(true); 30 | }); 31 | 32 | test('inline sourcemap', async () => { 33 | await using fixture = await createFixture({ 34 | ...packageFixture(), 35 | 'package.json': createPackageJson({ 36 | type: 'module', 37 | main: './dist/index.js', 38 | }), 39 | }); 40 | 41 | const pkgrollProcess = await pkgroll( 42 | ['--sourcemap=inline'], 43 | { 44 | cwd: fixture.path, 45 | nodePath, 46 | }, 47 | ); 48 | 49 | expect(pkgrollProcess.exitCode).toBe(0); 50 | expect(pkgrollProcess.stderr).toBe(''); 51 | 52 | expect( 53 | await fixture.readFile('dist/index.js', 'utf8'), 54 | ).toMatch(/\/\/# sourceMappingURL=data:application\/json;charset=utf-8;base64,\w+/); 55 | expect(await fixture.exists('dist/index.js.map')).toBe(false); 56 | }); 57 | }); 58 | }); 59 | -------------------------------------------------------------------------------- /tests/specs/builds/src-dist.ts: -------------------------------------------------------------------------------- 1 | import { testSuite, expect } from 'manten'; 2 | import { createFixture } from 'fs-fixture'; 3 | import { pkgroll } from '../../utils.js'; 4 | import { 5 | packageFixture, fixtureFiles, installTypeScript, createPackageJson, 6 | } from '../../fixtures.js'; 7 | 8 | export default testSuite(({ describe }, nodePath: string) => { 9 | describe('change src', ({ test }) => { 10 | test('nested directory - relative path', async () => { 11 | const srcPath = 'custom-src/nested/src/'; 12 | await using fixture = await createFixture({ 13 | 'package.json': createPackageJson({ 14 | main: './dist/nested/index.js', 15 | module: './dist/nested/index.mjs', 16 | types: './dist/nested/index.d.ts', 17 | }), 18 | [srcPath]: fixtureFiles, 19 | ...installTypeScript, 20 | }); 21 | 22 | const pkgrollProcess = await pkgroll( 23 | ['--src', srcPath], 24 | { 25 | cwd: fixture.path, 26 | nodePath, 27 | }, 28 | ); 29 | expect(pkgrollProcess.exitCode).toBe(0); 30 | expect(pkgrollProcess.stderr).toBe(''); 31 | 32 | expect(await fixture.exists('dist/nested/index.js')).toBe(true); 33 | expect(await fixture.exists('dist/nested/index.mjs')).toBe(true); 34 | expect(await fixture.exists('dist/nested/index.d.ts')).toBe(true); 35 | }); 36 | 37 | test('nested directory - absolute path', async () => { 38 | const srcPath = 'custom-src/nested/src/'; 39 | await using fixture = await createFixture({ 40 | 'package.json': createPackageJson({ 41 | main: './dist/nested/index.js', 42 | module: './dist/nested/index.mjs', 43 | types: './dist/nested/index.d.ts', 44 | }), 45 | [srcPath]: fixtureFiles, 46 | ...installTypeScript, 47 | }); 48 | 49 | const pkgrollProcess = await pkgroll( 50 | ['--src', fixture.getPath(srcPath)], 51 | { 52 | cwd: fixture.path, 53 | nodePath, 54 | }, 55 | ); 56 | 57 | expect(pkgrollProcess.exitCode).toBe(0); 58 | expect(pkgrollProcess.stderr).toBe(''); 59 | 60 | expect(await fixture.exists('dist/nested/index.js')).toBe(true); 61 | expect(await fixture.exists('dist/nested/index.mjs')).toBe(true); 62 | expect(await fixture.exists('dist/nested/index.d.ts')).toBe(true); 63 | }); 64 | }); 65 | 66 | describe('change dist', ({ test }) => { 67 | test('nested directory', async () => { 68 | await using fixture = await createFixture({ 69 | ...packageFixture({ installTypeScript: true }), 70 | 'package.json': createPackageJson({ 71 | main: './nested/index.js', 72 | module: './nested/index.mjs', 73 | types: './nested/index.d.ts', 74 | }), 75 | }); 76 | 77 | const pkgrollProcess = await pkgroll(['--dist', '.'], { 78 | cwd: fixture.path, 79 | nodePath, 80 | }); 81 | expect(pkgrollProcess.exitCode).toBe(0); 82 | expect(pkgrollProcess.stderr).toBe(''); 83 | 84 | expect(await fixture.exists('nested/index.js')).toBe(true); 85 | expect(await fixture.exists('nested/index.mjs')).toBe(true); 86 | expect(await fixture.exists('nested/index.d.ts')).toBe(true); 87 | }); 88 | }); 89 | }); 90 | -------------------------------------------------------------------------------- /tests/specs/builds/target.ts: -------------------------------------------------------------------------------- 1 | import path from 'node:path'; 2 | import { testSuite, expect } from 'manten'; 3 | import { createFixture } from 'fs-fixture'; 4 | import { pkgroll } from '../../utils.js'; 5 | import { packageFixture, createPackageJson, createTsconfigJson } from '../../fixtures.js'; 6 | 7 | export default testSuite(({ describe }, nodePath: string) => { 8 | describe('target', ({ describe, test }) => { 9 | test('transformation', async () => { 10 | await using fixture = await createFixture({ 11 | ...packageFixture(), 12 | 'package.json': createPackageJson({ 13 | main: './dist/target.js', 14 | }), 15 | }); 16 | 17 | const pkgrollProcess = await pkgroll(['--target', 'es2015'], { 18 | cwd: fixture.path, 19 | nodePath, 20 | }); 21 | 22 | expect(pkgrollProcess.exitCode).toBe(0); 23 | expect(pkgrollProcess.stderr).toBe(''); 24 | 25 | const content = await fixture.readFile('dist/target.js', 'utf8'); 26 | expect(content).toMatch('Math.pow'); 27 | }); 28 | 29 | describe('node protocol', () => { 30 | test('strips node protocol', async () => { 31 | await using fixture = await createFixture({ 32 | ...packageFixture({ installTypeScript: true }), 33 | 'package.json': createPackageJson({ 34 | main: './dist/utils.js', 35 | module: './dist/utils.mjs', 36 | types: './dist/utils.d.ts', 37 | }), 38 | 'tsconfig.json': createTsconfigJson({ 39 | compilerOptions: { 40 | jsx: 'react', 41 | typeRoots: [ 42 | path.resolve('node_modules/@types'), 43 | ], 44 | }, 45 | }), 46 | }); 47 | 48 | const pkgrollProcess = await pkgroll(['--target', 'node12.19'], { 49 | cwd: fixture.path, 50 | nodePath, 51 | }); 52 | 53 | expect(pkgrollProcess.exitCode).toBe(0); 54 | expect(pkgrollProcess.stderr).toBe(''); 55 | 56 | expect(await fixture.readFile('dist/utils.js', 'utf8')).not.toMatch('node:'); 57 | expect(await fixture.readFile('dist/utils.mjs', 'utf8')).not.toMatch('node:'); 58 | 59 | const content = await fixture.readFile('dist/utils.d.ts', 'utf8'); 60 | expect(content).toMatch('declare function'); 61 | expect(content).not.toMatch('node:'); 62 | }); 63 | 64 | test('keeps node protocol', async () => { 65 | await using fixture = await createFixture({ 66 | ...packageFixture({ installTypeScript: true }), 67 | 'package.json': createPackageJson({ 68 | main: './dist/utils.js', 69 | module: './dist/utils.mjs', 70 | types: './dist/utils.d.ts', 71 | }), 72 | 'tsconfig.json': createTsconfigJson({ 73 | compilerOptions: { 74 | jsx: 'react', 75 | typeRoots: [ 76 | path.resolve('node_modules/@types'), 77 | ], 78 | }, 79 | }), 80 | }); 81 | 82 | const pkgrollProcess = await pkgroll(['--target', 'node14.18'], { 83 | cwd: fixture.path, 84 | nodePath, 85 | }); 86 | 87 | expect(pkgrollProcess.exitCode).toBe(0); 88 | expect(pkgrollProcess.stderr).toBe(''); 89 | 90 | expect(await fixture.readFile('dist/utils.js', 'utf8')).toMatch('\'node:fs\''); 91 | expect(await fixture.readFile('dist/utils.mjs', 'utf8')).toMatch('\'node:fs\''); 92 | 93 | const content = await fixture.readFile('dist/utils.d.ts', 'utf8'); 94 | expect(content).toMatch('\'fs\''); 95 | expect(content).toMatch('\'node:fs\''); 96 | }); 97 | }); 98 | }); 99 | }); 100 | -------------------------------------------------------------------------------- /tests/specs/builds/typescript.ts: -------------------------------------------------------------------------------- 1 | import { testSuite, expect } from 'manten'; 2 | import { createFixture } from 'fs-fixture'; 3 | import { outdent } from 'outdent'; 4 | import { pkgroll } from '../../utils.js'; 5 | import { createPackageJson, createTsconfigJson, installTypeScript } from '../../fixtures.js'; 6 | 7 | export default testSuite(({ describe }, nodePath: string) => { 8 | describe('TypeScript', ({ test }) => { 9 | test('resolves .jsx -> .tsx', async () => { 10 | await using fixture = await createFixture({ 11 | src: { 12 | 'index.ts': 'import "./file.jsx"', 13 | 'file.tsx': 'console.log(1)', 14 | }, 15 | 'package.json': createPackageJson({ 16 | main: './dist/index.js', 17 | type: 'module', 18 | }), 19 | }); 20 | 21 | const pkgrollProcess = await pkgroll(['--env.NODE_ENV=development'], { 22 | cwd: fixture.path, 23 | nodePath, 24 | }); 25 | 26 | expect(pkgrollProcess.exitCode).toBe(0); 27 | expect(pkgrollProcess.stderr).toBe(''); 28 | 29 | const content = await fixture.readFile('dist/index.js', 'utf8'); 30 | expect(content).toBe('console.log(1);\n'); 31 | }); 32 | 33 | test('resolves .jsx from .js', async () => { 34 | await using fixture = await createFixture({ 35 | src: { 36 | 'index.js': 'import "./file.jsx"', 37 | 'file.jsx': 'console.log(1)', 38 | }, 39 | 'package.json': createPackageJson({ 40 | main: './dist/index.js', 41 | type: 'module', 42 | }), 43 | }); 44 | 45 | const pkgrollProcess = await pkgroll(['--env.NODE_ENV=development'], { 46 | cwd: fixture.path, 47 | nodePath, 48 | }); 49 | 50 | expect(pkgrollProcess.exitCode).toBe(0); 51 | expect(pkgrollProcess.stderr).toBe(''); 52 | 53 | const content = await fixture.readFile('dist/index.js', 'utf8'); 54 | expect(content).toBe('console.log(1);\n'); 55 | }); 56 | 57 | test('resolves baseUrl', async () => { 58 | await using fixture = await createFixture({ 59 | src: { 60 | 'index.ts': outdent` 61 | import { qux } from 'dir/exportee.js'; 62 | import { quux } from 'dir/deep/exportee.js'; 63 | console.log(qux, quux); 64 | `, 65 | 'importee.ts': 'export const foo = \'foo\'', 66 | dir: { 67 | 'importee.ts': 'export const bar = \'bar\'', 68 | 'exportee.ts': outdent` 69 | import { foo } from 'importee.js'; 70 | import { baz } from 'dir/deep/importee.js'; 71 | export const qux = foo + baz;`, 72 | deep: { 73 | 'importee.ts': 'export const baz = \'baz\'', 74 | 'exportee.ts': outdent` 75 | import { foo } from 'importee.js'; 76 | import { bar } from 'dir/importee.js'; 77 | import { baz } from 'dir/deep/importee.js'; 78 | export const quux = foo + bar + baz;`, 79 | }, 80 | }, 81 | }, 82 | 'package.json': createPackageJson({ 83 | exports: './dist/index.mjs', 84 | }), 85 | 'tsconfig.json': createTsconfigJson({ 86 | compilerOptions: { 87 | baseUrl: './src', 88 | }, 89 | }), 90 | }); 91 | 92 | const pkgrollProcess = await pkgroll(['--minify'], { 93 | cwd: fixture.path, 94 | nodePath, 95 | }); 96 | 97 | expect(pkgrollProcess.exitCode).toBe(0); 98 | expect(pkgrollProcess.stderr).toBe(''); 99 | 100 | const content = await fixture.readFile('dist/index.mjs', 'utf8'); 101 | expect(content).toMatch('"foo"'); 102 | expect(content).toMatch('"bar"'); 103 | expect(content).toMatch('"baz"'); 104 | }); 105 | 106 | test('resolves paths', async () => { 107 | await using fixture = await createFixture({ 108 | ...installTypeScript, 109 | src: { 110 | 'index.ts': outdent` 111 | import * as foo from '@foo/index.js'; 112 | import { bar } from '~bar'; 113 | import { baz } from '#baz'; 114 | export { foo, bar, baz };`, 115 | foo: { 116 | 'index.ts': 'export { a } from \'@foo/a.js\';', 117 | 'a.ts': 'export const a = \'a\';', 118 | }, 119 | 'bar/index.ts': 'export const bar = \'bar\';', 120 | 'baz.ts': 'export const baz = \'baz\';', 121 | }, 122 | 'package.json': createPackageJson({ 123 | exports: { 124 | types: './dist/index.d.mts', 125 | default: './dist/index.mjs', 126 | }, 127 | }), 128 | 'tsconfig.json': createTsconfigJson({ 129 | compilerOptions: { 130 | paths: { 131 | '@foo/*': ['./src/foo/*'], 132 | '~bar': ['./src/bar/index.ts'], 133 | '#baz': ['./src/baz.ts'], 134 | }, 135 | }, 136 | }), 137 | }); 138 | 139 | const pkgrollProcess = await pkgroll(['--minify'], { 140 | cwd: fixture.path, 141 | nodePath, 142 | }); 143 | 144 | expect(pkgrollProcess.exitCode).toBe(0); 145 | expect(pkgrollProcess.stderr).toBe(''); 146 | 147 | const content = await fixture.readFile('dist/index.mjs', 'utf8'); 148 | expect(content).toMatch('"a"'); 149 | expect(content).toMatch('"bar"'); 150 | expect(content).toMatch('"baz"'); 151 | }); 152 | }); 153 | 154 | describe('custom tsconfig.json path', ({ test }) => { 155 | test('respects compile target', async () => { 156 | await using fixture = await createFixture({ 157 | src: { 158 | 'index.ts': 'export default () => "foo";', 159 | }, 160 | 'package.json': createPackageJson({ 161 | main: './dist/index.js', 162 | }), 163 | 'tsconfig.json': createTsconfigJson({ 164 | compilerOptions: { 165 | target: 'ES6', 166 | }, 167 | }), 168 | 'tsconfig.build.json': createTsconfigJson({ 169 | compilerOptions: { 170 | target: 'ES5', 171 | }, 172 | }), 173 | }); 174 | 175 | const pkgrollProcess = await pkgroll([ 176 | '--env.NODE_ENV=test', 177 | '--tsconfig=tsconfig.build.json', 178 | ], { 179 | cwd: fixture.path, 180 | nodePath, 181 | }); 182 | 183 | expect(pkgrollProcess.exitCode).toBe(0); 184 | expect(pkgrollProcess.stderr).toBe(''); 185 | 186 | const content = await fixture.readFile('dist/index.js', 'utf8'); 187 | expect(content.includes('function')).toBe(true); 188 | }); 189 | 190 | test('error on invalid tsconfig.json path', async () => { 191 | await using fixture = await createFixture({ 192 | src: { 193 | 'index.ts': 'export default () => "foo";', 194 | }, 195 | 'package.json': createPackageJson({ 196 | main: './dist/index.js', 197 | }), 198 | 'tsconfig.json': createTsconfigJson({ 199 | compilerOptions: { 200 | target: 'ES6', 201 | }, 202 | }), 203 | 'tsconfig.build.json': createTsconfigJson({ 204 | compilerOptions: { 205 | target: 'ES5', 206 | }, 207 | }), 208 | }); 209 | 210 | const pkgrollProcess = await pkgroll([ 211 | '--env.NODE_ENV=test', 212 | '--tsconfig=tsconfig.invalid.json', 213 | ], { 214 | cwd: fixture.path, 215 | nodePath, 216 | reject: false, 217 | }); 218 | 219 | expect(pkgrollProcess.exitCode).toBe(1); 220 | // expect(pkgrollProcess.stderr).toMatch('Cannot resolve tsconfig at path:'); 221 | }); 222 | }); 223 | }); 224 | -------------------------------------------------------------------------------- /tests/specs/error-cases.ts: -------------------------------------------------------------------------------- 1 | import { testSuite, expect } from 'manten'; 2 | import { createFixture } from 'fs-fixture'; 3 | import { pkgroll } from '../utils.js'; 4 | import { packageFixture, createPackageJson } from '../fixtures.js'; 5 | 6 | export default testSuite(({ describe }, nodePath: string) => { 7 | describe('Error handling', ({ test }) => { 8 | test('no package.json', async () => { 9 | await using fixture = await createFixture(packageFixture()); 10 | 11 | const pkgrollProcess = await pkgroll( 12 | [], 13 | { 14 | cwd: fixture.path, 15 | nodePath, 16 | reject: false, 17 | }, 18 | ); 19 | 20 | expect(pkgrollProcess.exitCode).toBe(1); 21 | expect(pkgrollProcess.stderr).toMatch('package.json not found'); 22 | }); 23 | 24 | test('invalid package.json', async () => { 25 | await using fixture = await createFixture({ 26 | ...packageFixture(), 27 | 'package.json': '{ name: pkg }', 28 | }); 29 | 30 | const pkgrollProcess = await pkgroll( 31 | [], 32 | { 33 | cwd: fixture.path, 34 | nodePath, 35 | reject: false, 36 | }, 37 | ); 38 | 39 | expect(pkgrollProcess.exitCode).toBe(1); 40 | expect(pkgrollProcess.stderr).toMatch('Cannot parse package.json'); 41 | }); 42 | 43 | test('no entry in package.json', async () => { 44 | await using fixture = await createFixture({ 45 | ...packageFixture(), 46 | 'package.json': createPackageJson({ 47 | name: 'pkg', 48 | }), 49 | }); 50 | 51 | const pkgrollProcess = await pkgroll( 52 | [], 53 | { 54 | cwd: fixture.path, 55 | nodePath, 56 | reject: false, 57 | }, 58 | ); 59 | 60 | expect(pkgrollProcess.exitCode).toBe(1); 61 | expect(pkgrollProcess.stderr).toMatch('No export entries found in package.json'); 62 | }); 63 | 64 | test('conflicting entry in package.json', async () => { 65 | await using fixture = await createFixture({ 66 | ...packageFixture(), 67 | 'package.json': createPackageJson({ 68 | name: 'pkg', 69 | main: 'dist/index.js', 70 | module: 'dist/index.js', 71 | }), 72 | }); 73 | 74 | const pkgrollProcess = await pkgroll( 75 | [], 76 | { 77 | cwd: fixture.path, 78 | nodePath, 79 | reject: false, 80 | }, 81 | ); 82 | 83 | expect(pkgrollProcess.exitCode).toBe(1); 84 | expect(pkgrollProcess.stderr).toMatch('Error: Conflicting export types "commonjs" & "module" found for ./dist/index.js'); 85 | }); 86 | 87 | test('ignore and warn on path entry outside of dist directory', async () => { 88 | await using fixture = await createFixture({ 89 | ...packageFixture(), 90 | 'package.json': createPackageJson({ 91 | name: 'pkg', 92 | main: '/dist/main.js', 93 | }), 94 | }); 95 | 96 | const pkgrollProcess = await pkgroll( 97 | [], 98 | { 99 | cwd: fixture.path, 100 | nodePath, 101 | reject: false, 102 | }, 103 | ); 104 | 105 | expect(pkgrollProcess.exitCode).toBe(1); 106 | expect(pkgrollProcess.stderr).toMatch('Ignoring entry outside of ./dist/ directory: package.json#main="/dist/main.js"'); 107 | expect(pkgrollProcess.stderr).toMatch('No export entries found in package.json'); 108 | }); 109 | 110 | test('cannot find matching source file', async () => { 111 | await using fixture = await createFixture({ 112 | ...packageFixture(), 113 | 'package.json': createPackageJson({ 114 | name: 'pkg', 115 | main: 'dist/missing.js', 116 | module: 'dist/missing.mjs', 117 | }), 118 | }); 119 | 120 | const pkgrollProcess = await pkgroll( 121 | [], 122 | { 123 | cwd: fixture.path, 124 | nodePath, 125 | reject: false, 126 | }, 127 | ); 128 | expect(pkgrollProcess.exitCode).toBe(1); 129 | expect(pkgrollProcess.stderr).toMatch('Could not find matching source file for export path'); 130 | expect(pkgrollProcess.stderr).toMatch('Expected: ./src/missing[.js|.ts|.tsx|.mts|.cts]'); 131 | }); 132 | 133 | test('unexpected extension', async () => { 134 | await using fixture = await createFixture({ 135 | ...packageFixture(), 136 | 'package.json': createPackageJson({ 137 | name: 'pkg', 138 | main: 'dist/index.foo', 139 | }), 140 | }); 141 | 142 | const pkgrollProcess = await pkgroll( 143 | [], 144 | { 145 | cwd: fixture.path, 146 | nodePath, 147 | reject: false, 148 | }, 149 | ); 150 | expect(pkgrollProcess.exitCode).toBe(1); 151 | expect(pkgrollProcess.stderr).toMatch('Error: Package.json output path contains invalid extension'); 152 | expect(pkgrollProcess.stderr).toMatch('Expected: .d.ts, .d.mts, .d.cts, .js, .mjs, .cjs'); 153 | }); 154 | }); 155 | }); 156 | -------------------------------------------------------------------------------- /tests/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "..", 3 | "include": [ 4 | ".", 5 | ], 6 | } 7 | -------------------------------------------------------------------------------- /tests/utils.ts: -------------------------------------------------------------------------------- 1 | import path from 'node:path'; 2 | import { execa, type Options } from 'execa'; 3 | 4 | const pkgrollBinPath = path.resolve('./dist/cli.mjs'); 5 | 6 | export const pkgroll = async ( 7 | cliArguments: string[], 8 | options: Options, 9 | ) => await execa( 10 | pkgrollBinPath, 11 | cliArguments, 12 | { 13 | ...options, 14 | env: { 15 | NODE_PATH: '', 16 | }, 17 | }, 18 | ); 19 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | // Node 18 4 | "target": "ES2022", 5 | "module": "Preserve", 6 | "resolveJsonModule": true, 7 | "strict": true, 8 | "noEmit": true, 9 | "esModuleInterop": true, 10 | "verbatimModuleSyntax": true, 11 | "skipLibCheck": true, 12 | }, 13 | } 14 | --------------------------------------------------------------------------------