├── .editorconfig ├── .github └── workflows │ ├── autofix.yml │ └── ci.yml ├── .gitignore ├── .prettierignore ├── .prettierrc ├── CHANGELOG.md ├── LICENSE ├── README.md ├── build.config.ts ├── eslint.config.mjs ├── package.json ├── pnpm-lock.yaml ├── renovate.json ├── src ├── index.ts ├── internal │ ├── builtins.ts │ ├── errors.ts │ ├── get-format.ts │ ├── package-json-reader.ts │ └── resolve.ts └── resolve.ts ├── test ├── fixture │ ├── cjs.mjs │ ├── eval-err.mjs │ ├── eval.mjs │ ├── exports.mjs │ ├── foo │ │ └── index.mjs │ ├── hello.link.mjs │ ├── hello.mjs │ ├── resolve-err.mjs │ ├── resolve.mjs │ ├── test.link.txt │ ├── test.txt │ └── utils.mjs └── resolve.test.ts └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | trim_trailing_whitespace = true 7 | charset = utf-8 8 | 9 | [*.js] 10 | indent_style = space 11 | indent_size = 2 12 | 13 | [{package.json,*.yml,*.cjson}] 14 | indent_style = space 15 | indent_size = 2 16 | -------------------------------------------------------------------------------- /.github/workflows/autofix.yml: -------------------------------------------------------------------------------- 1 | name: autofix.ci # needed to securely identify the workflow 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: ["main"] 7 | 8 | permissions: 9 | contents: read 10 | 11 | jobs: 12 | autofix: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v4 16 | - run: npm i -fg corepack && corepack enable 17 | - uses: actions/setup-node@v4 18 | with: 19 | node-version: 22 20 | cache: "pnpm" 21 | - run: pnpm install 22 | - run: pnpm lint:fix 23 | - uses: autofix-ci/action@551dded8c6cc8a1054039c8bc0b8b48c51dfc6ef 24 | with: 25 | commit-message: "chore: apply automated updates" 26 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | ci: 13 | runs-on: ${{ matrix.os }} 14 | strategy: 15 | matrix: 16 | os: [ubuntu-latest, macos-latest, windows-latest] 17 | steps: 18 | - uses: actions/checkout@v4 19 | - run: npm i -fg corepack && corepack enable 20 | - uses: actions/setup-node@v4 21 | with: 22 | node-version: 22 23 | cache: "pnpm" 24 | - run: pnpm install 25 | - if: matrix.os == 'ubuntu-latest' 26 | run: pnpm lint 27 | - if: matrix.os == 'ubuntu-latest' 28 | run: pnpm test:types 29 | - if: matrix.os == 'ubuntu-latest' 30 | run: pnpm build 31 | - run: pnpm vitest --coverage 32 | - uses: codecov/codecov-action@v5 33 | with: 34 | token: ${{ secrets.CODECOV_TOKEN }} 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | dist 4 | .vscode 5 | .DS_Store 6 | .eslintcache 7 | *.log* 8 | *.env* 9 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | pnpm-lock.yaml 2 | CHANGELOG.md 3 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | 4 | ## v1.0.5 5 | 6 | [compare changes](https://github.com/unjs/exsolve/compare/v1.0.4...v1.0.5) 7 | 8 | ### 🩹 Fixes 9 | 10 | - Resolve absolute symlinks to real paths ([#22](https://github.com/unjs/exsolve/pull/22)) 11 | 12 | ### 📖 Documentation 13 | 14 | - Capitalize titles ([#20](https://github.com/unjs/exsolve/pull/20)) 15 | 16 | ### 🏡 Chore 17 | 18 | - **release:** V1.0.4 ([dfff3e9](https://github.com/unjs/exsolve/commit/dfff3e9)) 19 | 20 | ### ❤️ Contributors 21 | 22 | - @beer ([@iiio2](https://github.com/iiio2)) 23 | - Kricsleo ([@kricsleo](https://github.com/kricsleo)) 24 | - Pooya Parsa ([@pi0](https://github.com/pi0)) 25 | 26 | ## v1.0.4 27 | 28 | [compare changes](https://github.com/unjs/exsolve/compare/v1.0.3...v1.0.4) 29 | 30 | ### 🩹 Fixes 31 | 32 | - Use bundled `nodeBuiltins` for internal implementation ([#18](https://github.com/unjs/exsolve/pull/18)) 33 | 34 | ### ❤️ Contributors 35 | 36 | - Pooya Parsa ([@pi0](http://github.com/pi0)) 37 | 38 | ## v1.0.3 39 | 40 | [compare changes](https://github.com/unjs/exsolve/compare/v1.0.2...v1.0.3) 41 | 42 | ### 🩹 Fixes 43 | 44 | - Keep a copy of node.js builtin modules ([#17](https://github.com/unjs/exsolve/pull/17)) 45 | 46 | ### ❤️ Contributors 47 | 48 | - Pooya Parsa ([@pi0](http://github.com/pi0)) 49 | 50 | ## v1.0.1 51 | 52 | [compare changes](https://github.com/unjs/exsolve/compare/v1.0.0...v1.0.1) 53 | 54 | ### 🩹 Fixes 55 | 56 | - **resolveModulePath:** Do not throw with try on non file:// result ([b61dea9](https://github.com/unjs/exsolve/commit/b61dea9)) 57 | 58 | ### ❤️ Contributors 59 | 60 | - Pooya Parsa ([@pi0](http://github.com/pi0)) 61 | 62 | ## v1.0.0 63 | 64 | [compare changes](https://github.com/unjs/exsolve/compare/v0.4.4...v1.0.0) 65 | 66 | ### 🏡 Chore 67 | 68 | - Simplify package.json ([49cfd75](https://github.com/unjs/exsolve/commit/49cfd75)) 69 | 70 | ### ❤️ Contributors 71 | 72 | - Pooya Parsa ([@pi0](http://github.com/pi0)) 73 | 74 | ## v0.4.4 75 | 76 | [compare changes](https://github.com/unjs/exsolve/compare/v0.4.3...v0.4.4) 77 | 78 | ### 🚀 Enhancements 79 | 80 | - **resolveModulePath:** Normalize windows paths ([#14](https://github.com/unjs/exsolve/pull/14)) 81 | 82 | ### ❤️ Contributors 83 | 84 | - Pooya Parsa ([@pi0](http://github.com/pi0)) 85 | 86 | ## v0.4.3 87 | 88 | [compare changes](https://github.com/unjs/exsolve/compare/v0.4.2...v0.4.3) 89 | 90 | ### 🩹 Fixes 91 | 92 | - Ensure no `//` in joined paths ([#13](https://github.com/unjs/exsolve/pull/13)) 93 | 94 | ### ❤️ Contributors 95 | 96 | - Daniel Roe ([@danielroe](http://github.com/danielroe)) 97 | 98 | ## v0.4.2 99 | 100 | [compare changes](https://github.com/unjs/exsolve/compare/v0.4.1...v0.4.2) 101 | 102 | ### 🩹 Fixes 103 | 104 | - Resolve modules using full url ([#12](https://github.com/unjs/exsolve/pull/12)) 105 | - Handle missing subpath as not found error ([80185bf](https://github.com/unjs/exsolve/commit/80185bf)) 106 | 107 | ### 💅 Refactors 108 | 109 | - Rework input normalization ([#11](https://github.com/unjs/exsolve/pull/11)) 110 | - Remove windows workaround ([8a12c0f](https://github.com/unjs/exsolve/commit/8a12c0f)) 111 | 112 | ### 🏡 Chore 113 | 114 | - Update pnpm ([0d4acd3](https://github.com/unjs/exsolve/commit/0d4acd3)) 115 | 116 | ### ✅ Tests 117 | 118 | - Add regression tests (#8, #9, #10) ([#8](https://github.com/unjs/exsolve/issues/8), [#9](https://github.com/unjs/exsolve/issues/9), [#10](https://github.com/unjs/exsolve/issues/10)) 119 | - Update windows test ([b4771c8](https://github.com/unjs/exsolve/commit/b4771c8)) 120 | 121 | ### ❤️ Contributors 122 | 123 | - Pooya Parsa ([@pi0](http://github.com/pi0)) 124 | - Daniel Roe ([@danielroe](http://github.com/danielroe)) 125 | 126 | ## v0.4.1 127 | 128 | [compare changes](https://github.com/unjs/exsolve/compare/v0.4.0...v0.4.1) 129 | 130 | ### 🩹 Fixes 131 | 132 | - Always apply custom suffix ([211f0fc](https://github.com/unjs/exsolve/commit/211f0fc)) 133 | 134 | ### 📖 Documentation 135 | 136 | - Tiny typo ([#6](https://github.com/unjs/exsolve/pull/6)) 137 | 138 | ### ❤️ Contributors 139 | 140 | - Pooya Parsa ([@pi0](http://github.com/pi0)) 141 | - @beer ([@iiio2](http://github.com/iiio2)) 142 | 143 | ## v0.4.0 144 | 145 | [compare changes](https://github.com/unjs/exsolve/compare/v0.3.2...v0.4.0) 146 | 147 | ### 🔥 Performance 148 | 149 | - Only test for protocol at beginning of id ([#4](https://github.com/unjs/exsolve/pull/4)) 150 | - **createResolver:** Normalize default `from` once ([71432c8](https://github.com/unjs/exsolve/commit/71432c8)) 151 | 152 | ### 🩹 Fixes 153 | 154 | - Normalise windows urls ([#5](https://github.com/unjs/exsolve/pull/5)) 155 | 156 | ### 💅 Refactors 157 | 158 | - ⚠️ Allow reorder `""` suffix ([69ab48c](https://github.com/unjs/exsolve/commit/69ab48c)) 159 | 160 | #### ⚠️ Breaking Changes 161 | 162 | - ⚠️ Allow reorder `""` suffix ([69ab48c](https://github.com/unjs/exsolve/commit/69ab48c)) 163 | 164 | ### ❤️ Contributors 165 | 166 | - Pooya Parsa ([@pi0](http://github.com/pi0)) 167 | - Daniel Roe ([@danielroe](http://github.com/danielroe)) 168 | 169 | ## v0.3.2 170 | 171 | [compare changes](https://github.com/unjs/exsolve/compare/v0.3.1...v0.3.2) 172 | 173 | ### 🩹 Fixes 174 | 175 | - Return `file://` url in absolute fast path ([9c99a04](https://github.com/unjs/exsolve/commit/9c99a04)) 176 | 177 | ### ❤️ Contributors 178 | 179 | - Pooya Parsa ([@pi0](http://github.com/pi0)) 180 | 181 | ## v0.3.1 182 | 183 | [compare changes](https://github.com/unjs/exsolve/compare/v0.3.0...v0.3.1) 184 | 185 | ### 🩹 Fixes 186 | 187 | - Handle `try` when cache hits ([ec75a93](https://github.com/unjs/exsolve/commit/ec75a93)) 188 | 189 | ### ❤️ Contributors 190 | 191 | - Pooya Parsa ([@pi0](http://github.com/pi0)) 192 | 193 | ## v0.3.0 194 | 195 | [compare changes](https://github.com/unjs/exsolve/compare/v0.2.0...v0.3.0) 196 | 197 | ### 🚀 Enhancements 198 | 199 | - `createResolver` ([064396b](https://github.com/unjs/exsolve/commit/064396b)) 200 | - Resolve cache ([d4ef4e9](https://github.com/unjs/exsolve/commit/d4ef4e9)) 201 | 202 | ### 🔥 Performance 203 | 204 | - ⚠️ Remove default extra fallbacks ([6b8cd74](https://github.com/unjs/exsolve/commit/6b8cd74)) 205 | 206 | ### 🏡 Chore 207 | 208 | - **release:** V0.2.0 ([bff9874](https://github.com/unjs/exsolve/commit/bff9874)) 209 | - Add pkg size badge ([a9a5b25](https://github.com/unjs/exsolve/commit/a9a5b25)) 210 | - Update docs ([28ea154](https://github.com/unjs/exsolve/commit/28ea154)) 211 | - Update docs ([4cb89e2](https://github.com/unjs/exsolve/commit/4cb89e2)) 212 | 213 | #### ⚠️ Breaking Changes 214 | 215 | - ⚠️ Remove default extra fallbacks ([6b8cd74](https://github.com/unjs/exsolve/commit/6b8cd74)) 216 | 217 | ### ❤️ Contributors 218 | 219 | - Pooya Parsa ([@pi0](http://github.com/pi0)) 220 | 221 | ## v0.2.0 222 | 223 | [compare changes](https://github.com/unjs/exsolve/compare/v0.1.4...v0.2.0) 224 | 225 | ### 🔥 Performance 226 | 227 | - ⚠️ Remove default extra fallbacks ([6b8cd74](https://github.com/unjs/exsolve/commit/6b8cd74)) 228 | 229 | #### ⚠️ Breaking Changes 230 | 231 | - ⚠️ Remove default extra fallbacks ([6b8cd74](https://github.com/unjs/exsolve/commit/6b8cd74)) 232 | 233 | ### ❤️ Contributors 234 | 235 | - Pooya Parsa ([@pi0](http://github.com/pi0)) 236 | 237 | ## v0.1.4 238 | 239 | [compare changes](https://github.com/unjs/exsolve/compare/v0.1.3...v0.1.4) 240 | 241 | ### 🩹 Fixes 242 | 243 | - Handle `try` option ([b1cec8a](https://github.com/unjs/exsolve/commit/b1cec8a)) 244 | 245 | ### 💅 Refactors 246 | 247 | - Less verbose error ([43e8f04](https://github.com/unjs/exsolve/commit/43e8f04)) 248 | 249 | ### ❤️ Contributors 250 | 251 | - Pooya Parsa ([@pi0](http://github.com/pi0)) 252 | 253 | ## v0.1.3 254 | 255 | [compare changes](https://github.com/unjs/exsolve/compare/v0.1.2...v0.1.3) 256 | 257 | ### 🔥 Performance 258 | 259 | - Skip fallback when both ext and suffix are empty ([32aa3fd](https://github.com/unjs/exsolve/commit/32aa3fd)) 260 | 261 | ### ❤️ Contributors 262 | 263 | - Pooya Parsa ([@pi0](http://github.com/pi0)) 264 | 265 | ## v0.1.2 266 | 267 | [compare changes](https://github.com/unjs/exsolve/compare/v0.1.1...v0.1.2) 268 | 269 | ### 🚀 Enhancements 270 | 271 | - `try` option ([f2275eb](https://github.com/unjs/exsolve/commit/f2275eb)) 272 | 273 | ### 🏡 Chore 274 | 275 | - Prettier ignore changelog ([408d81b](https://github.com/unjs/exsolve/commit/408d81b)) 276 | 277 | ### ❤️ Contributors 278 | 279 | - Pooya Parsa ([@pi0](http://github.com/pi0)) 280 | 281 | ## v0.1.1 282 | 283 | [compare changes](https://github.com/unjs/exsolve/compare/v0.1.0...v0.1.1) 284 | 285 | ### 🚀 Enhancements 286 | 287 | - Support custom suffixes ([dad6cb4](https://github.com/unjs/exsolve/commit/dad6cb4)) 288 | - Support ts extensions by default ([d5684d7](https://github.com/unjs/exsolve/commit/d5684d7)) 289 | 290 | ### 🔥 Performance 291 | 292 | - Avoid extension checks if id has extension ([5638cde](https://github.com/unjs/exsolve/commit/5638cde)) 293 | - Skip same suffixes ([01ce3ad](https://github.com/unjs/exsolve/commit/01ce3ad)) 294 | 295 | ### 💅 Refactors 296 | 297 | - Sync `get-format` with upstream ([11a8a79](https://github.com/unjs/exsolve/commit/11a8a79)) 298 | - Rename to internal ([5c7730b](https://github.com/unjs/exsolve/commit/5c7730b)) 299 | - Remove all external deps ([42ce8c1](https://github.com/unjs/exsolve/commit/42ce8c1)) 300 | - Rename to `exsolve` ([e5c9646](https://github.com/unjs/exsolve/commit/e5c9646)) 301 | 302 | ### 📖 Documentation 303 | 304 | - Add performance tips section ([fb7228f](https://github.com/unjs/exsolve/commit/fb7228f)) 305 | - Add perf note about `from` ([f57e220](https://github.com/unjs/exsolve/commit/f57e220)) 306 | 307 | ### 🏡 Chore 308 | 309 | - Remove unused type ([806b9be](https://github.com/unjs/exsolve/commit/806b9be)) 310 | - Update test ([58d3847](https://github.com/unjs/exsolve/commit/58d3847)) 311 | - Update docs ([1e72c22](https://github.com/unjs/exsolve/commit/1e72c22)) 312 | - Update docs ([949f80c](https://github.com/unjs/exsolve/commit/949f80c)) 313 | 314 | ### ✅ Tests 315 | 316 | - Fix for windows ([80e9f75](https://github.com/unjs/exsolve/commit/80e9f75)) 317 | 318 | ### 🤖 CI 319 | 320 | - Run on macos and windows too ([578efa9](https://github.com/unjs/exsolve/commit/578efa9)) 321 | - Lint on linux only ([9c42ad6](https://github.com/unjs/exsolve/commit/9c42ad6)) 322 | 323 | ### ❤️ Contributors 324 | 325 | - Pooya Parsa ([@pi0](http://github.com/pi0)) 326 | 327 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Pooya Parsa 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 | 23 | --- 24 | 25 | This is a derivative work based on: 26 | . 27 | 28 | --- 29 | 30 | This is a derivative work based on: 31 | . 32 | 33 | Which is licensed: 34 | 35 | """ 36 | (The MIT License) 37 | 38 | Copyright (c) 2021 Titus Wormer 39 | 40 | Permission is hereby granted, free of charge, to any person obtaining 41 | a copy of this software and associated documentation files (the 42 | 'Software'), to deal in the Software without restriction, including 43 | without limitation the rights to use, copy, modify, merge, publish, 44 | distribute, sublicense, and/or sell copies of the Software, and to 45 | permit persons to whom the Software is furnished to do so, subject to 46 | the following conditions: 47 | 48 | The above copyright notice and this permission notice shall be 49 | included in all copies or substantial portions of the Software. 50 | 51 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 52 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 53 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 54 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 55 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 56 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 57 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 58 | """ 59 | 60 | --- 61 | 62 | This is a derivative work based on: 63 | . 64 | 65 | Which is licensed: 66 | 67 | """ 68 | Copyright Node.js contributors. All rights reserved. 69 | 70 | Permission is hereby granted, free of charge, to any person obtaining a copy 71 | of this software and associated documentation files (the "Software"), to 72 | deal in the Software without restriction, including without limitation the 73 | rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 74 | sell copies of the Software, and to permit persons to whom the Software is 75 | furnished to do so, subject to the following conditions: 76 | 77 | The above copyright notice and this permission notice shall be included in 78 | all copies or substantial portions of the Software. 79 | 80 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 81 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 82 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 83 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 84 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 85 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 86 | IN THE SOFTWARE. 87 | """ 88 | 89 | This license applies to parts of Node.js originating from the 90 | https://github.com/joyent/node repository: 91 | 92 | """ 93 | Copyright Joyent, Inc. and other Node contributors. All rights reserved. 94 | Permission is hereby granted, free of charge, to any person obtaining a copy 95 | of this software and associated documentation files (the "Software"), to 96 | deal in the Software without restriction, including without limitation the 97 | rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 98 | sell copies of the Software, and to permit persons to whom the Software is 99 | furnished to do so, subject to the following conditions: 100 | 101 | The above copyright notice and this permission notice shall be included in 102 | all copies or substantial portions of the Software. 103 | 104 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 105 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 106 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 107 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 108 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 109 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 110 | IN THE SOFTWARE. 111 | """ 112 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # exsolve 2 | 3 | [![npm version](https://img.shields.io/npm/v/exsolve?color=yellow)](https://npmjs.com/package/exsolve) 4 | [![npm downloads](https://img.shields.io/npm/dm/exsolve?color=yellow)](https://npm.chart.dev/exsolve) 5 | [![pkg size](https://img.shields.io/npm/unpacked-size/exsolve?color=yellow)](https://packagephobia.com/result?p=exsolve) 6 | 7 | > Module resolution utilities for Node.js (based on previous work in [unjs/mlly](https://github.com/unjs/mlly), [wooorm/import-meta-resolve](https://github.com/wooorm/import-meta-resolve), and the upstream [Node.js](https://github.com/nodejs/node) implementation). 8 | 9 | This library exposes an API similar to [`import.meta.resolve`](https://nodejs.org/api/esm.html#importmetaresolvespecifier) based on Node.js's upstream implementation and [resolution algorithm](https://nodejs.org/api/esm.html#esm_resolution_algorithm). It supports all built-in functionalities—import maps, export maps, CJS, and ESM—with some additions: 10 | 11 | - Pure JS with no native dependencies (only Node.js is required). 12 | - Built-in resolve [cache](#resolve-cache). 13 | - Throws an error (or [try](#try)) if the resolved path does not exist in the filesystem. 14 | - Can override the default [conditions](#conditions). 15 | - Can resolve [from](#from) one or more parent URLs. 16 | - Can resolve with custom [suffixes](#suffixes). 17 | - Can resolve with custom [extensions](#extensions). 18 | 19 | ## Usage 20 | 21 | Install the package: 22 | 23 | ```sh 24 | # ✨ Auto-detect (npm, yarn, pnpm, bun, deno) 25 | npx nypm install exsolve 26 | ``` 27 | 28 | Import: 29 | 30 | ```ts 31 | // ESM import 32 | import { 33 | resolveModuleURL, 34 | resolveModulePath, 35 | createResolver, 36 | clearResolveCache, 37 | } from "exsolve"; 38 | 39 | // Or using dynamic import 40 | const { resolveModulePath } = await import("exsolve"); 41 | ``` 42 | 43 | ```ts 44 | resolveModuleURL(id, { 45 | /* options */ 46 | }); 47 | 48 | resolveModulePath(id, { 49 | /* options */ 50 | }); 51 | ``` 52 | 53 | Differences between `resolveModuleURL` and `resolveModulePath`: 54 | 55 | - `resolveModuleURL` returns a URL string like `file:///app/dep.mjs`. 56 | - `resolveModulePath` returns an absolute path like `/app/dep.mjs`. 57 | - If the resolved URL does not use the `file://` scheme (e.g., `data:` or `node:`), it will throw an error. 58 | 59 | ## Resolver with Options 60 | 61 | You can create a custom resolver instance with default [options](#resolve-options) using `createResolver`. 62 | 63 | **Example:** 64 | 65 | ```ts 66 | import { createResolver } from "exsolve"; 67 | 68 | const { resolveModuleURL, resolveModulePath } = createResolver({ 69 | suffixes: ["", "/index"], 70 | extensions: [".mjs", ".cjs", ".js", ".mts", ".cts", ".ts", ".json"], 71 | conditions: ["node", "import", "production"], 72 | }); 73 | ``` 74 | 75 | ## Resolve Cache 76 | 77 | To speed up resolution, resolved values (and errors) are globally cached with a unique key based on id and options. 78 | 79 | **Example:** Invalidate all (global) cache entries (to support file-system changes). 80 | 81 | ```ts 82 | import { clearResolveCache } from "exsolve"; 83 | 84 | clearResolveCache(); 85 | ``` 86 | 87 | **Example:** Custom resolver with custom cache object. 88 | 89 | ```ts 90 | import { createResolver } from "exsolve"; 91 | 92 | const { clearResolveCache, resolveModulePath } = createResolver({ 93 | cache: new Map(), 94 | }); 95 | ``` 96 | 97 | **Example:** Resolve without cache. 98 | 99 | ```ts 100 | import { resolveModulePath } from "exsolve"; 101 | 102 | resolveModulePath("id", { cache: false }); 103 | ``` 104 | 105 | ## Resolve Options 106 | 107 | ### `try` 108 | 109 | If set to `true` and the module cannot be resolved, the resolver returns `undefined` instead of throwing an error. 110 | 111 | **Example:** 112 | 113 | ```ts 114 | // undefined 115 | const resolved = resolveModuleURL("non-existing-package", { try: true }); 116 | ``` 117 | 118 | ### `from` 119 | 120 | A URL, path, or array of URLs/paths from which to resolve the module. 121 | 122 | If not provided, resolution starts from the current working directory. Setting this option is recommended. 123 | 124 | You can use `import.meta.url` for `from` to mimic the behavior of `import.meta.resolve()`. 125 | 126 | > [!TIP] 127 | > For better performance, ensure the value is a `file://` URL or at least ends with `/`. 128 | > 129 | > If it is set to an absolute path, the resolver must first check the filesystem to see if it is a file or directory. 130 | > If the input is a `file://` URL or ends with `/`, the resolver can skip this check. 131 | 132 | ### `conditions` 133 | 134 | Conditions to apply when resolving package exports (default: `["node", "import"]`). 135 | 136 | **Example:** 137 | 138 | ```ts 139 | // "/app/src/index.ts" 140 | const src = resolveModuleURL("pkg-name", { 141 | conditions: ["deno", "node", "import", "production"], 142 | }); 143 | ``` 144 | 145 | > [!NOTE] 146 | > Conditions are applied **without order**. The order is determined by the `exports` field in `package.json`. 147 | 148 | ### `extensions` 149 | 150 | Additional file extensions to check as fallbacks. 151 | 152 | **Example:** 153 | 154 | ```ts 155 | // "/app/src/index.ts" 156 | const src = resolveModulePath("./src/index", { 157 | extensions: [".mjs", ".cjs", ".js", ".mts", ".cts", ".ts", ".json"], 158 | }); 159 | ``` 160 | 161 | > [!TIP] 162 | > For better performance, use explicit extensions and avoid this option. 163 | 164 | ### `suffixes` 165 | 166 | Path suffixes to check. 167 | 168 | **Example:** 169 | 170 | ```ts 171 | // "/app/src/utils/index.ts" 172 | const src = resolveModulePath("./src/utils", { 173 | suffixes: ["", "/index"], 174 | extensions: [".mjs", ".cjs", ".js"], 175 | }); 176 | ``` 177 | 178 | > [!TIP] 179 | > For better performance, use explicit `/index` when needed and avoid this option. 180 | 181 | ### `cache` 182 | 183 | Resolve cache (enabled by default with a shared global object). 184 | 185 | Can be set to `false` to disable or a custom `Map` to bring your own cache object. 186 | 187 | See [cache](#resolve-cache) for more info. 188 | 189 | ## Other Performance Tips 190 | 191 | **Use explicit module extensions `.mjs` or `.cjs` instead of `.js`:** 192 | 193 | This allows the resolution fast path to skip reading the closest `package.json` for the [`type`](https://nodejs.org/api/packages.html#type). 194 | 195 | ## Development 196 | 197 |
198 | 199 | local development 200 | 201 | - Clone this repository 202 | - Install the latest LTS version of [Node.js](https://nodejs.org/en/) 203 | - Enable [Corepack](https://github.com/nodejs/corepack) using `corepack enable` 204 | - Install dependencies using `pnpm install` 205 | - Run interactive tests using `pnpm dev` 206 | 207 |
208 | 209 | ## License 210 | 211 | Published under the [MIT](https://github.com/unjs/exsolve/blob/main/LICENSE) license. 212 | 213 | Based on previous work in [unjs/mlly](https://github.com/unjs/mlly), [wooorm/import-meta-resolve](https://github.com/wooorm/import-meta-resolve) and [Node.js](https://github.com/nodejs/node) original implementation. 214 | -------------------------------------------------------------------------------- /build.config.ts: -------------------------------------------------------------------------------- 1 | import { defineBuildConfig } from "unbuild"; 2 | import { rm } from "node:fs/promises"; 3 | 4 | export default defineBuildConfig({ 5 | hooks: { 6 | async "build:done"() { 7 | await rm("dist/index.d.ts"); 8 | }, 9 | }, 10 | }); 11 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import unjs from "eslint-config-unjs"; 2 | 3 | export default unjs({ 4 | ignores: [ 5 | // ignore paths 6 | ], 7 | rules: { 8 | "unicorn/no-null": 0, 9 | }, 10 | markdown: { 11 | rules: { 12 | // markdown rule overrides 13 | }, 14 | }, 15 | }); 16 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "exsolve", 3 | "version": "1.0.5", 4 | "description": "Module resolution utilities based on Node.js upstream implementation.", 5 | "repository": "unjs/exsolve", 6 | "license": "MIT", 7 | "sideEffects": false, 8 | "type": "module", 9 | "exports": { 10 | "types": "./dist/index.d.mts", 11 | "default": "./dist/index.mjs" 12 | }, 13 | "types": "./dist/index.d.mts", 14 | "files": [ 15 | "dist" 16 | ], 17 | "scripts": { 18 | "build": "unbuild", 19 | "dev": "vitest dev", 20 | "lint": "eslint . && prettier -c .", 21 | "node-ts": "node --disable-warning=ExperimentalWarning --experimental-strip-types", 22 | "lint:fix": "automd && eslint . --fix && prettier -w .", 23 | "prepack": "pnpm build", 24 | "release": "pnpm test && changelogen --release && npm publish && git push --follow-tags", 25 | "test": "pnpm lint && pnpm test:types && vitest run --coverage", 26 | "test:types": "tsc --noEmit --skipLibCheck" 27 | }, 28 | "devDependencies": { 29 | "@types/node": "^22.14.1", 30 | "@vitest/coverage-v8": "^3.1.1", 31 | "automd": "^0.4.0", 32 | "changelogen": "^0.6.1", 33 | "eslint": "^9.24.0", 34 | "eslint-config-unjs": "^0.4.2", 35 | "jiti": "^2.4.2", 36 | "prettier": "^3.5.3", 37 | "typescript": "^5.8.3", 38 | "unbuild": "^3.5.0", 39 | "vitest": "^3.1.1" 40 | }, 41 | "packageManager": "pnpm@10.8.1" 42 | } 43 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["github>unjs/renovate-config"] 3 | } 4 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | resolveModuleURL, 3 | resolveModulePath, 4 | createResolver, 5 | clearResolveCache, 6 | } from "./resolve.ts"; 7 | 8 | export type { ResolveOptions, ResolverOptions } from "./resolve.ts"; 9 | -------------------------------------------------------------------------------- /src/internal/builtins.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Extracted from Node.js v22.14.0 3 | * For some reason, Bun decided to extend "node:modules" with bun specific modules which makes it unreliable source. 4 | */ 5 | // prettier-ignore 6 | export const nodeBuiltins = [ 7 | '_http_agent', '_http_client', '_http_common', 8 | '_http_incoming', '_http_outgoing', '_http_server', 9 | '_stream_duplex', '_stream_passthrough', '_stream_readable', 10 | '_stream_transform', '_stream_wrap', '_stream_writable', 11 | '_tls_common', '_tls_wrap', 'assert', 12 | 'assert/strict', 'async_hooks', 'buffer', 13 | 'child_process', 'cluster', 'console', 14 | 'constants', 'crypto', 'dgram', 15 | 'diagnostics_channel', 'dns', 'dns/promises', 16 | 'domain', 'events', 'fs', 17 | 'fs/promises', 'http', 'http2', 18 | 'https', 'inspector', 'inspector/promises', 19 | 'module', 'net', 'os', 20 | 'path', 'path/posix', 'path/win32', 21 | 'perf_hooks', 'process', 'punycode', 22 | 'querystring', 'readline', 'readline/promises', 23 | 'repl', 'stream', 'stream/consumers', 24 | 'stream/promises', 'stream/web', 'string_decoder', 25 | 'sys', 'timers', 'timers/promises', 26 | 'tls', 'trace_events', 'tty', 27 | 'url', 'util', 'util/types', 28 | 'v8', 'vm', 'wasi', 29 | 'worker_threads', 'zlib' 30 | ] 31 | -------------------------------------------------------------------------------- /src/internal/errors.ts: -------------------------------------------------------------------------------- 1 | // Source: https://github.com/nodejs/node/blob/main/lib/internal/errors.js 2 | // Changes: https://github.com/nodejs/node/commits/main/lib/internal/errors.js?since=2024-04-29 3 | 4 | import v8 from "node:v8"; 5 | import assert from "node:assert"; 6 | import { format, inspect } from "node:util"; 7 | 8 | export type ErrnoExceptionFields = { 9 | errnode?: number; 10 | code?: string; 11 | path?: string; 12 | syscall?: string; 13 | url?: string; 14 | }; 15 | 16 | export type ErrnoException = Error & ErrnoExceptionFields; 17 | export type MessageFunction = (...parameters: Array) => string; 18 | 19 | const own = {}.hasOwnProperty; 20 | 21 | const classRegExp = /^([A-Z][a-z\d]*)+$/; 22 | 23 | // Sorted by a rough estimate on most frequently used entries. 24 | const kTypes = new Set([ 25 | "string", 26 | "function", 27 | "number", 28 | "object", 29 | // Accept 'Function' and 'Object' as alternative to the lower cased version. 30 | "Function", 31 | "Object", 32 | "boolean", 33 | "bigint", 34 | "symbol", 35 | ]); 36 | 37 | const messages: Map = new Map(); 38 | 39 | const nodeInternalPrefix = "__node_internal_"; 40 | 41 | let userStackTraceLimit: number; 42 | 43 | /** 44 | * Create a list string in the form like 'A and B' or 'A, B, ..., and Z'. 45 | * We cannot use Intl.ListFormat because it's not available in 46 | * --without-intl builds. 47 | * 48 | * @param {Array} array 49 | * An array of strings. 50 | * @param {string} [type] 51 | * The list type to be inserted before the last element. 52 | * @returns {string} 53 | */ 54 | function formatList(array: string[], type = "and"): string { 55 | return array.length < 3 56 | ? array.join(` ${type} `) 57 | : `${array.slice(0, -1).join(", ")}, ${type} ${array.at(-1)}`; 58 | } 59 | 60 | /** 61 | * Utility function for registering the error codes. 62 | */ 63 | function createError< 64 | T extends MessageFunction | string, 65 | C extends ErrorConstructor, 66 | >( 67 | sym: string, 68 | value: T, 69 | constructor: C, 70 | ): T extends string 71 | ? C 72 | : { new (...args: Parameters>): InstanceType } { 73 | // Special case for SystemError that formats the error message differently 74 | // The SystemErrors only have SystemError as their base classes. 75 | messages.set(sym, value); 76 | 77 | return makeNodeErrorWithCode(constructor, sym) as any; 78 | } 79 | 80 | function makeNodeErrorWithCode( 81 | Base: ErrorConstructor, 82 | key: string, 83 | ): ErrorConstructor { 84 | // @ts-expect-error It’s a Node error. 85 | return function NodeError(...parameters: unknown[]) { 86 | const limit = Error.stackTraceLimit; 87 | if (isErrorStackTraceLimitWritable()) Error.stackTraceLimit = 0; 88 | const error = new Base(); 89 | // Reset the limit and setting the name property. 90 | if (isErrorStackTraceLimitWritable()) Error.stackTraceLimit = limit; 91 | const message = getMessage(key, parameters, error); 92 | Object.defineProperties(error, { 93 | // Note: no need to implement `kIsNodeError` symbol, would be hard, 94 | // probably. 95 | message: { 96 | value: message, 97 | enumerable: false, 98 | writable: true, 99 | configurable: true, 100 | }, 101 | toString: { 102 | /** @this {Error} */ 103 | value() { 104 | return `${this.name} [${key}]: ${this.message}`; 105 | }, 106 | enumerable: false, 107 | writable: true, 108 | configurable: true, 109 | }, 110 | }); 111 | 112 | captureLargerStackTrace(error); 113 | // @ts-expect-error It’s a Node error. 114 | error.code = key; 115 | return error; 116 | }; 117 | } 118 | 119 | function isErrorStackTraceLimitWritable(): boolean { 120 | // Do no touch Error.stackTraceLimit as V8 would attempt to install 121 | // it again during deserialization. 122 | try { 123 | if (v8.startupSnapshot.isBuildingSnapshot()) { 124 | return false; 125 | } 126 | } catch { 127 | // ignore 128 | } 129 | 130 | const desc = Object.getOwnPropertyDescriptor(Error, "stackTraceLimit"); 131 | if (desc === undefined) { 132 | return Object.isExtensible(Error); 133 | } 134 | 135 | return own.call(desc, "writable") && desc.writable !== undefined 136 | ? desc.writable 137 | : desc.set !== undefined; 138 | } 139 | 140 | /** 141 | * This function removes unnecessary frames from Node.js core errors. 142 | */ 143 | function hideStackFrames unknown>( 144 | wrappedFunction: T, 145 | ): T { 146 | // We rename the functions that will be hidden to cut off the stacktrace 147 | // at the outermost one 148 | const hidden = nodeInternalPrefix + wrappedFunction.name; 149 | Object.defineProperty(wrappedFunction, "name", { value: hidden }); 150 | return wrappedFunction; 151 | } 152 | 153 | const captureLargerStackTrace = hideStackFrames(function (error: unknown) { 154 | const stackTraceLimitIsWritable = isErrorStackTraceLimitWritable(); 155 | if (stackTraceLimitIsWritable) { 156 | userStackTraceLimit = Error.stackTraceLimit; 157 | Error.stackTraceLimit = Number.POSITIVE_INFINITY; 158 | } 159 | 160 | Error.captureStackTrace(error as Error); 161 | 162 | // Reset the limit 163 | if (stackTraceLimitIsWritable) Error.stackTraceLimit = userStackTraceLimit; 164 | 165 | return error; 166 | }); 167 | 168 | function getMessage(key: string, parameters: unknown[], self: Error): string { 169 | const message = messages.get(key); 170 | assert(message !== undefined, "expected `message` to be found"); 171 | 172 | if (typeof message === "function") { 173 | assert( 174 | message.length <= parameters.length, // Default options do not count. 175 | `Code: ${key}; The provided arguments length (${parameters.length}) does not ` + 176 | `match the required ones (${message.length}).`, 177 | ); 178 | return Reflect.apply(message, self, parameters); 179 | } 180 | 181 | const regex = /%[dfijoOs]/g; 182 | let expectedLength = 0; 183 | while (regex.exec(message) !== null) expectedLength++; 184 | assert( 185 | expectedLength === parameters.length, 186 | `Code: ${key}; The provided arguments length (${parameters.length}) does not ` + 187 | `match the required ones (${expectedLength}).`, 188 | ); 189 | if (parameters.length === 0) return message; 190 | 191 | parameters.unshift(message); 192 | 193 | return Reflect.apply(format, null, parameters); 194 | } 195 | 196 | /** 197 | * Determine the specific type of a value for type-mismatch errors. 198 | */ 199 | function determineSpecificType(value: unknown): string { 200 | if (value === null || value === undefined) { 201 | return String(value); 202 | } 203 | 204 | if (typeof value === "function" && value.name) { 205 | return `function ${value.name}`; 206 | } 207 | 208 | if (typeof value === "object") { 209 | if (value.constructor && value.constructor.name) { 210 | return `an instance of ${value.constructor.name}`; 211 | } 212 | 213 | return `${inspect(value, { depth: -1 })}`; 214 | } 215 | 216 | let inspected = inspect(value, { colors: false }); 217 | 218 | if (inspected.length > 28) { 219 | inspected = `${inspected.slice(0, 25)}...`; 220 | } 221 | 222 | return `type ${typeof value} (${inspected})`; 223 | } 224 | 225 | // ---------------------------------------------------------------------------- 226 | // Codes 227 | // ---------------------------------------------------------------------------- 228 | 229 | export const ERR_INVALID_ARG_TYPE = createError( 230 | "ERR_INVALID_ARG_TYPE", 231 | (name: string, expected: Array | string, actual: unknown) => { 232 | assert(typeof name === "string", "'name' must be a string"); 233 | if (!Array.isArray(expected)) { 234 | expected = [expected]; 235 | } 236 | 237 | let message = "The "; 238 | if (name.endsWith(" argument")) { 239 | // For cases like 'first argument' 240 | message += `${name} `; 241 | } else { 242 | const type = name.includes(".") ? "property" : "argument"; 243 | message += `"${name}" ${type} `; 244 | } 245 | 246 | message += "must be "; 247 | 248 | const types: string[] = []; 249 | const instances: string[] = []; 250 | const other: string[] = []; 251 | 252 | for (const value of expected) { 253 | assert( 254 | typeof value === "string", 255 | "All expected entries have to be of type string", 256 | ); 257 | 258 | if (kTypes.has(value)) { 259 | types.push(value.toLowerCase()); 260 | } else if (classRegExp.exec(value) === null) { 261 | assert( 262 | value !== "object", 263 | 'The value "object" should be written as "Object"', 264 | ); 265 | other.push(value); 266 | } else { 267 | instances.push(value); 268 | } 269 | } 270 | 271 | // Special handle `object` in case other instances are allowed to outline 272 | // the differences between each other. 273 | if (instances.length > 0) { 274 | const pos = types.indexOf("object"); 275 | if (pos !== -1) { 276 | types.slice(pos, 1); 277 | instances.push("Object"); 278 | } 279 | } 280 | 281 | if (types.length > 0) { 282 | message += `${types.length > 1 ? "one of type" : "of type"} ${formatList( 283 | types, 284 | "or", 285 | )}`; 286 | if (instances.length > 0 || other.length > 0) message += " or "; 287 | } 288 | 289 | if (instances.length > 0) { 290 | message += `an instance of ${formatList(instances, "or")}`; 291 | if (other.length > 0) message += " or "; 292 | } 293 | 294 | if (other.length > 0) { 295 | if (other.length > 1) { 296 | message += `one of ${formatList(other, "or")}`; 297 | } else { 298 | if (other[0]?.toLowerCase() !== other[0]) message += "an "; 299 | message += `${other[0]}`; 300 | } 301 | } 302 | 303 | message += `. Received ${determineSpecificType(actual)}`; 304 | 305 | return message; 306 | }, 307 | TypeError, 308 | ); 309 | 310 | export const ERR_INVALID_MODULE_SPECIFIER = createError( 311 | "ERR_INVALID_MODULE_SPECIFIER", 312 | /** 313 | * @param {string} request 314 | * @param {string} reason 315 | * @param {string} [base] 316 | */ 317 | (request: string, reason: string, base?: string) => { 318 | return `Invalid module "${request}" ${reason}${ 319 | base ? ` imported from ${base}` : "" 320 | }`; 321 | }, 322 | TypeError, 323 | ); 324 | 325 | export const ERR_INVALID_PACKAGE_CONFIG = createError( 326 | "ERR_INVALID_PACKAGE_CONFIG", 327 | (path: string, base?: string, message?: string) => { 328 | return `Invalid package config ${path}${ 329 | base ? ` while importing ${base}` : "" 330 | }${message ? `. ${message}` : ""}`; 331 | }, 332 | Error, 333 | ); 334 | 335 | export const ERR_INVALID_PACKAGE_TARGET = createError( 336 | "ERR_INVALID_PACKAGE_TARGET", 337 | ( 338 | packagePath: string, 339 | key: string, 340 | target: unknown, 341 | isImport: boolean = false, 342 | base?: string, 343 | ) => { 344 | const relatedError = 345 | typeof target === "string" && 346 | !isImport && 347 | target.length > 0 && 348 | !target.startsWith("./"); 349 | if (key === ".") { 350 | assert(isImport === false); 351 | return ( 352 | `Invalid "exports" main target ${JSON.stringify(target)} defined ` + 353 | `in the package config ${packagePath}package.json${ 354 | base ? ` imported from ${base}` : "" 355 | }${relatedError ? '; targets must start with "./"' : ""}` 356 | ); 357 | } 358 | 359 | return `Invalid "${ 360 | isImport ? "imports" : "exports" 361 | }" target ${JSON.stringify( 362 | target, 363 | )} defined for '${key}' in the package config ${packagePath}package.json${ 364 | base ? ` imported from ${base}` : "" 365 | }${relatedError ? '; targets must start with "./"' : ""}`; 366 | }, 367 | Error, 368 | ); 369 | 370 | export const ERR_MODULE_NOT_FOUND = createError( 371 | "ERR_MODULE_NOT_FOUND", 372 | (path: string, base: string, exactUrl: boolean = false) => { 373 | return `Cannot find ${ 374 | exactUrl ? "module" : "package" 375 | } '${path}' imported from ${base}`; 376 | }, 377 | Error, 378 | ); 379 | 380 | export const ERR_NETWORK_IMPORT_DISALLOWED = createError( 381 | "ERR_NETWORK_IMPORT_DISALLOWED", 382 | "import of '%s' by %s is not supported: %s", 383 | Error, 384 | ); 385 | 386 | export const ERR_PACKAGE_IMPORT_NOT_DEFINED = createError( 387 | "ERR_PACKAGE_IMPORT_NOT_DEFINED", 388 | (specifier: string, packagePath: string | undefined, base: string) => { 389 | return `Package import specifier "${specifier}" is not defined${ 390 | packagePath ? ` in package ${packagePath || ""}package.json` : "" 391 | } imported from ${base}`; 392 | }, 393 | TypeError, 394 | ); 395 | 396 | export const ERR_PACKAGE_PATH_NOT_EXPORTED = createError( 397 | "ERR_PACKAGE_PATH_NOT_EXPORTED", 398 | /** 399 | * @param {string} packagePath 400 | * @param {string} subpath 401 | * @param {string} [base] 402 | */ 403 | (packagePath: string, subpath: string, base?: string) => { 404 | if (subpath === ".") 405 | return `No "exports" main defined in ${packagePath}package.json${ 406 | base ? ` imported from ${base}` : "" 407 | }`; 408 | return `Package subpath '${subpath}' is not defined by "exports" in ${packagePath}package.json${ 409 | base ? ` imported from ${base}` : "" 410 | }`; 411 | }, 412 | Error, 413 | ); 414 | 415 | export const ERR_UNSUPPORTED_DIR_IMPORT = createError( 416 | "ERR_UNSUPPORTED_DIR_IMPORT", 417 | "Directory import '%s' is not supported " + 418 | "resolving ES modules imported from %s", 419 | Error, 420 | ); 421 | 422 | export const ERR_UNSUPPORTED_RESOLVE_REQUEST = createError( 423 | "ERR_UNSUPPORTED_RESOLVE_REQUEST", 424 | 'Failed to resolve module specifier "%s" from "%s": Invalid relative URL or base scheme is not hierarchical.', 425 | TypeError, 426 | ); 427 | 428 | export const ERR_UNKNOWN_FILE_EXTENSION = createError( 429 | "ERR_UNKNOWN_FILE_EXTENSION", 430 | (extension: string, path: string) => { 431 | return `Unknown file extension "${extension}" for ${path}`; 432 | }, 433 | TypeError, 434 | ); 435 | 436 | export const ERR_INVALID_ARG_VALUE = createError( 437 | "ERR_INVALID_ARG_VALUE", 438 | (name: string, value: unknown, reason: string = "is invalid") => { 439 | let inspected = inspect(value); 440 | 441 | if (inspected.length > 128) { 442 | inspected = `${inspected.slice(0, 128)}...`; 443 | } 444 | 445 | const type = name.includes(".") ? "property" : "argument"; 446 | 447 | return `The ${type} '${name}' ${reason}. Received ${inspected}`; 448 | }, 449 | TypeError, 450 | // Note: extra classes have been shaken out. 451 | // , RangeError 452 | ); 453 | -------------------------------------------------------------------------------- /src/internal/get-format.ts: -------------------------------------------------------------------------------- 1 | // Source: https://github.com/nodejs/node/blob/main/lib/internal/modules/esm/get_format.js 2 | // Changes: https://github.com/nodejs/node/commits/main/lib/internal/modules/esm/get_format.js?since=2025-02-24 3 | 4 | import { fileURLToPath } from "node:url"; 5 | import { getPackageScopeConfig } from "./package-json-reader.ts"; 6 | import { ERR_UNKNOWN_FILE_EXTENSION } from "./errors.ts"; 7 | 8 | const hasOwnProperty = {}.hasOwnProperty; 9 | 10 | const extensionFormatMap: Record & { __proto__: null } = 11 | { 12 | __proto__: null, 13 | ".json": "json", 14 | ".cjs": "commonjs", 15 | ".cts": "commonjs", 16 | ".js": "module", 17 | ".ts": "module", 18 | ".mts": "module", 19 | ".mjs": "module", 20 | }; 21 | 22 | type Protocol = "data:" | "file:" | "node:"; 23 | 24 | type ProtocolHandler = ( 25 | parsed: URL, 26 | context: { parentURL: string; source?: Buffer }, 27 | ignoreErrors: boolean, 28 | ) => string | null | void; 29 | 30 | const protocolHandlers: Record & { 31 | __proto__: null; 32 | } = { 33 | __proto__: null, 34 | "data:": getDataProtocolModuleFormat, 35 | "file:": getFileProtocolModuleFormat, 36 | "node:": () => "builtin", 37 | }; 38 | 39 | function mimeToFormat(mime: string | null): string | null { 40 | if ( 41 | mime && 42 | /\s*(text|application)\/javascript\s*(;\s*charset=utf-?8\s*)?/i.test(mime) 43 | ) 44 | return "module"; 45 | if (mime === "application/json") return "json"; 46 | return null; 47 | } 48 | 49 | function getDataProtocolModuleFormat(parsed: URL): string | null { 50 | const { 1: mime } = /^([^/]+\/[^;,]+)[^,]*?(;base64)?,/.exec( 51 | parsed.pathname, 52 | ) || [null, null, null]; 53 | return mimeToFormat(mime); 54 | } 55 | 56 | /** 57 | * Returns the file extension from a URL. 58 | * 59 | * Should give similar result to 60 | * `require('node:path').extname(require('node:url').fileURLToPath(url))` 61 | * when used with a `file:` URL. 62 | * 63 | */ 64 | function extname(url: URL): string { 65 | const pathname = url.pathname; 66 | let index = pathname.length; 67 | 68 | while (index--) { 69 | const code = pathname.codePointAt(index); 70 | 71 | if (code === 47 /* `/` */) { 72 | return ""; 73 | } 74 | 75 | if (code === 46 /* `.` */) { 76 | return pathname.codePointAt(index - 1) === 47 /* `/` */ 77 | ? "" 78 | : pathname.slice(index); 79 | } 80 | } 81 | 82 | return ""; 83 | } 84 | 85 | function getFileProtocolModuleFormat( 86 | url: URL, 87 | _context: unknown, 88 | ignoreErrors: boolean, 89 | ) { 90 | const ext = extname(url); 91 | 92 | if (ext === ".js") { 93 | const { type: packageType } = getPackageScopeConfig(url); 94 | 95 | if (packageType !== "none") { 96 | return packageType; 97 | } 98 | 99 | return "commonjs"; 100 | } 101 | 102 | if (ext === "") { 103 | const { type: packageType } = getPackageScopeConfig(url); 104 | 105 | // Legacy behavior 106 | if (packageType === "none" || packageType === "commonjs") { 107 | return "commonjs"; 108 | } 109 | 110 | // Note: we don’t implement WASM, so we don’t need 111 | // `getFormatOfExtensionlessFile` from `formats`. 112 | return "module"; 113 | } 114 | 115 | const format = extensionFormatMap[ext]; 116 | if (format) return format; 117 | 118 | // Explicit undefined return indicates load hook should rerun format check 119 | if (ignoreErrors) { 120 | return undefined; 121 | } 122 | 123 | const filepath = fileURLToPath(url); 124 | throw new ERR_UNKNOWN_FILE_EXTENSION(ext, filepath); 125 | } 126 | 127 | export function defaultGetFormatWithoutErrors( 128 | url: URL, 129 | context: { parentURL: string }, 130 | ): string | null { 131 | const protocol = url.protocol; 132 | 133 | if (!hasOwnProperty.call(protocolHandlers, protocol)) { 134 | return null; 135 | } 136 | 137 | return protocolHandlers[protocol as Protocol]!(url, context, true) || null; 138 | } 139 | -------------------------------------------------------------------------------- /src/internal/package-json-reader.ts: -------------------------------------------------------------------------------- 1 | // Source: https://github.com/nodejs/node/blob/main/lib/internal/modules/package_json_reader.js 2 | // Changes: https://github.com/nodejs/node/commits/main/lib/internal/modules/package_json_reader.js?since=2025-02-24 3 | // 4 | // Notes: 5 | // - Removed the native dependency. 6 | // - No need to cache, we do that in resolve already. 7 | 8 | import fs from "node:fs"; 9 | import path from "node:path"; 10 | import { fileURLToPath } from "node:url"; 11 | import { ERR_INVALID_PACKAGE_CONFIG } from "./errors.ts"; 12 | 13 | export type PackageConfig = { 14 | pjsonPath: string; 15 | exists: boolean; 16 | main?: string; 17 | name?: string; 18 | type: "commonjs" | "module" | "none"; 19 | exports?: Record; 20 | imports?: Record; 21 | }; 22 | 23 | import type { ErrnoException } from "./errors.ts"; 24 | 25 | const hasOwnProperty = {}.hasOwnProperty; 26 | 27 | const cache: Map = new Map(); 28 | 29 | export function read( 30 | jsonPath: string, 31 | { base, specifier }: { specifier: URL | string; base?: URL }, 32 | ): PackageConfig { 33 | const existing = cache.get(jsonPath); 34 | 35 | if (existing) { 36 | return existing; 37 | } 38 | 39 | let string: string | undefined; 40 | 41 | try { 42 | string = fs.readFileSync(path.toNamespacedPath(jsonPath), "utf8"); 43 | } catch (error) { 44 | const exception = error as ErrnoException; 45 | 46 | if (exception.code !== "ENOENT") { 47 | throw exception; 48 | } 49 | } 50 | 51 | const result: PackageConfig = { 52 | exists: false, 53 | pjsonPath: jsonPath, 54 | main: undefined, 55 | name: undefined, 56 | type: "none", // Ignore unknown types for forwards compatibility 57 | exports: undefined, 58 | imports: undefined, 59 | }; 60 | 61 | if (string !== undefined) { 62 | let parsed: Record; 63 | 64 | try { 65 | parsed = JSON.parse(string); 66 | } catch (error_: unknown) { 67 | const error = new ERR_INVALID_PACKAGE_CONFIG( 68 | jsonPath, 69 | (base ? `"${specifier}" from ` : "") + fileURLToPath(base || specifier), 70 | (error_ as ErrnoException).message, 71 | ); 72 | error.cause = error_; 73 | throw error; 74 | } 75 | 76 | result.exists = true; 77 | 78 | if ( 79 | hasOwnProperty.call(parsed, "name") && 80 | typeof parsed.name === "string" 81 | ) { 82 | result.name = parsed.name; 83 | } 84 | 85 | if ( 86 | hasOwnProperty.call(parsed, "main") && 87 | typeof parsed.main === "string" 88 | ) { 89 | result.main = parsed.main; 90 | } 91 | 92 | if (hasOwnProperty.call(parsed, "exports")) { 93 | result.exports = parsed.exports as any; 94 | } 95 | 96 | if (hasOwnProperty.call(parsed, "imports")) { 97 | result.imports = parsed.imports as any; 98 | } 99 | 100 | // Ignore unknown types for forwards compatibility 101 | if ( 102 | hasOwnProperty.call(parsed, "type") && 103 | (parsed.type === "commonjs" || parsed.type === "module") 104 | ) { 105 | result.type = parsed.type; 106 | } 107 | } 108 | 109 | cache.set(jsonPath, result); 110 | 111 | return result; 112 | } 113 | 114 | export function getPackageScopeConfig(resolved: URL | string): PackageConfig { 115 | // Note: in Node, this is now a native module. 116 | let packageJSONUrl = new URL("package.json", resolved); 117 | 118 | while (true) { 119 | const packageJSONPath = packageJSONUrl.pathname; 120 | if (packageJSONPath.endsWith("node_modules/package.json")) { 121 | break; 122 | } 123 | 124 | const packageConfig = read(fileURLToPath(packageJSONUrl), { 125 | specifier: resolved, 126 | }); 127 | 128 | if (packageConfig.exists) { 129 | return packageConfig; 130 | } 131 | 132 | const lastPackageJSONUrl = packageJSONUrl; 133 | packageJSONUrl = new URL("../package.json", packageJSONUrl); 134 | 135 | // Terminates at root where ../package.json equals ../../package.json 136 | // (can't just check "/package.json" for Windows support). 137 | if (packageJSONUrl.pathname === lastPackageJSONUrl.pathname) { 138 | break; 139 | } 140 | } 141 | 142 | const packageJSONPath = fileURLToPath(packageJSONUrl); 143 | // ^^ Note: in Node, this is now a native module. 144 | 145 | return { 146 | pjsonPath: packageJSONPath, 147 | exists: false, 148 | type: "none", 149 | }; 150 | } 151 | -------------------------------------------------------------------------------- /src/internal/resolve.ts: -------------------------------------------------------------------------------- 1 | // Source: https://github.com/nodejs/node/blob/main/lib/internal/modules/esm/resolve.js 2 | // Changes: https://github.com/nodejs/node/commits/main/lib/internal/modules/esm/resolve.js?since=2025-02-24 3 | // TODO: https://github.com/nodejs/node/commit/fb852798dcd3aceeeabbb07bc4b622157b7826e1#diff-b4c24f634e3741e5ad9e8c29864a48f2bd284a9d66d8fed3d077ccee0c44087b 4 | 5 | import type { Stats } from "node:fs"; 6 | import type { ErrnoException } from "./errors.ts"; 7 | import type { PackageConfig } from "./package-json-reader.ts"; 8 | 9 | import assert from "node:assert"; 10 | import process from "node:process"; 11 | import path from "node:path"; 12 | import { statSync, realpathSync } from "node:fs"; 13 | import { URL, fileURLToPath, pathToFileURL } from "node:url"; 14 | import { nodeBuiltins } from "./builtins"; 15 | 16 | import { defaultGetFormatWithoutErrors } from "./get-format.ts"; 17 | import { getPackageScopeConfig, read } from "./package-json-reader.ts"; 18 | 19 | import { 20 | ERR_INVALID_MODULE_SPECIFIER, 21 | ERR_INVALID_PACKAGE_CONFIG, 22 | ERR_INVALID_PACKAGE_TARGET, 23 | ERR_MODULE_NOT_FOUND, 24 | ERR_PACKAGE_IMPORT_NOT_DEFINED, 25 | ERR_PACKAGE_PATH_NOT_EXPORTED, 26 | ERR_UNSUPPORTED_DIR_IMPORT, 27 | ERR_UNSUPPORTED_RESOLVE_REQUEST, 28 | } from "./errors.ts"; 29 | 30 | const RegExpPrototypeSymbolReplace = RegExp.prototype[Symbol.replace]; 31 | 32 | const own = {}.hasOwnProperty; 33 | 34 | const invalidSegmentRegEx = 35 | /(^|\\|\/)((\.|%2e)(\.|%2e)?|(n|%6e|%4e)(o|%6f|%4f)(d|%64|%44)(e|%65|%45)(_|%5f)(m|%6d|%4d)(o|%6f|%4f)(d|%64|%44)(u|%75|%55)(l|%6c|%4c)(e|%65|%45)(s|%73|%53))?(\\|\/|$)/i; 36 | 37 | const deprecatedInvalidSegmentRegEx = 38 | /(^|\\|\/)((\.|%2e)(\.|%2e)?|(n|%6e|%4e)(o|%6f|%4f)(d|%64|%44)(e|%65|%45)(_|%5f)(m|%6d|%4d)(o|%6f|%4f)(d|%64|%44)(u|%75|%55)(l|%6c|%4c)(e|%65|%45)(s|%73|%53))(\\|\/|$)/i; 39 | 40 | const invalidPackageNameRegEx = /^\.|%|\\/; 41 | 42 | const patternRegEx = /\*/g; 43 | 44 | const encodedSeparatorRegEx = /%2f|%5c/i; 45 | 46 | const emittedPackageWarnings: Set = new Set(); 47 | 48 | const doubleSlashRegEx = /[/\\]{2}/; 49 | 50 | function emitInvalidSegmentDeprecation( 51 | target: string, 52 | request: string, 53 | match: string, 54 | packageJsonUrl: URL, 55 | internal: boolean, 56 | base: URL, 57 | isTarget: boolean, 58 | ) { 59 | // @ts-expect-error: apparently it does exist, TS. 60 | if (process.noDeprecation) { 61 | return; 62 | } 63 | 64 | const pjsonPath = fileURLToPath(packageJsonUrl); 65 | const double = doubleSlashRegEx.exec(isTarget ? target : request) !== null; 66 | process.emitWarning( 67 | `Use of deprecated ${ 68 | double ? "double slash" : "leading or trailing slash matching" 69 | } resolving "${target}" for module ` + 70 | `request "${request}" ${ 71 | request === match ? "" : `matched to "${match}" ` 72 | }in the "${ 73 | internal ? "imports" : "exports" 74 | }" field module resolution of the package at ${pjsonPath}${ 75 | base ? ` imported from ${fileURLToPath(base)}` : "" 76 | }.`, 77 | "DeprecationWarning", 78 | "DEP0166", 79 | ); 80 | } 81 | 82 | function emitLegacyIndexDeprecation( 83 | url: URL, 84 | packageJsonUrl: URL, 85 | base: URL, 86 | main?: string, 87 | ): void { 88 | // @ts-expect-error: apparently it does exist, TS. 89 | if (process.noDeprecation) { 90 | return; 91 | } 92 | 93 | const format = defaultGetFormatWithoutErrors(url, { parentURL: base.href }); 94 | if (format !== "module") return; 95 | const urlPath = fileURLToPath(url.href); 96 | const packagePath = fileURLToPath(new URL(".", packageJsonUrl)); 97 | const basePath = fileURLToPath(base); 98 | if (!main) { 99 | process.emitWarning( 100 | `No "main" or "exports" field defined in the package.json for ${packagePath} resolving the main entry point "${urlPath.slice( 101 | packagePath.length, 102 | )}", imported from ${basePath}.\nDefault "index" lookups for the main are deprecated for ES modules.`, 103 | "DeprecationWarning", 104 | "DEP0151", 105 | ); 106 | } else if (path.resolve(packagePath, main) !== urlPath) { 107 | process.emitWarning( 108 | `Package ${packagePath} has a "main" field set to "${main}", ` + 109 | `excluding the full filename and extension to the resolved file at "${urlPath.slice( 110 | packagePath.length, 111 | )}", imported from ${basePath}.\n Automatic extension resolution of the "main" field is ` + 112 | "deprecated for ES modules.", 113 | "DeprecationWarning", 114 | "DEP0151", 115 | ); 116 | } 117 | } 118 | 119 | function tryStatSync(path: string): Stats | undefined { 120 | // Note: from Node 15 onwards we can use `throwIfNoEntry: false` instead. 121 | try { 122 | return statSync(path); 123 | } catch { 124 | // Note: in Node code this returns `new Stats`, 125 | // but in Node 22 that’s marked as a deprecated internal API. 126 | // Which, well, we kinda are, but still to prevent that warning, 127 | // just yield `undefined`. 128 | } 129 | } 130 | 131 | /** 132 | * Legacy CommonJS main resolution: 133 | * 1. let M = pkg_url + (json main field) 134 | * 2. TRY(M, M.js, M.json, M.node) 135 | * 3. TRY(M/index.js, M/index.json, M/index.node) 136 | * 4. TRY(pkg_url/index.js, pkg_url/index.json, pkg_url/index.node) 137 | * 5. NOT_FOUND 138 | */ 139 | function fileExists(url: URL): boolean { 140 | const stats = statSync(url, { throwIfNoEntry: false }); 141 | const isFile = stats ? stats.isFile() : undefined; 142 | return isFile === null || isFile === undefined ? false : isFile; 143 | } 144 | 145 | function legacyMainResolve( 146 | packageJsonUrl: URL, 147 | packageConfig: PackageConfig, 148 | base: URL, 149 | ): URL { 150 | let guess: URL | undefined; 151 | if (packageConfig.main !== undefined) { 152 | guess = new URL(packageConfig.main, packageJsonUrl); 153 | // Note: fs check redundances will be handled by Descriptor cache here. 154 | if (fileExists(guess)) return guess; 155 | 156 | const tries = [ 157 | `./${packageConfig.main}.js`, 158 | `./${packageConfig.main}.json`, 159 | `./${packageConfig.main}.node`, 160 | `./${packageConfig.main}/index.js`, 161 | `./${packageConfig.main}/index.json`, 162 | `./${packageConfig.main}/index.node`, 163 | ]; 164 | let i = -1; 165 | 166 | while (++i < tries.length) { 167 | guess = new URL(tries[i]!, packageJsonUrl); 168 | if (fileExists(guess)) break; 169 | guess = undefined; 170 | } 171 | 172 | if (guess) { 173 | emitLegacyIndexDeprecation( 174 | guess, 175 | packageJsonUrl, 176 | base, 177 | packageConfig.main, 178 | ); 179 | return guess; 180 | } 181 | // Fallthrough. 182 | } 183 | 184 | const tries = ["./index.js", "./index.json", "./index.node"]; 185 | let i = -1; 186 | 187 | while (++i < tries.length) { 188 | guess = new URL(tries[i]!, packageJsonUrl); 189 | if (fileExists(guess)) break; 190 | guess = undefined; 191 | } 192 | 193 | if (guess) { 194 | emitLegacyIndexDeprecation(guess, packageJsonUrl, base, packageConfig.main); 195 | return guess; 196 | } 197 | 198 | // Not found. 199 | throw new ERR_MODULE_NOT_FOUND( 200 | fileURLToPath(new URL(".", packageJsonUrl)), 201 | fileURLToPath(base), 202 | ); 203 | } 204 | 205 | function finalizeResolution( 206 | resolved: URL, 207 | base: URL, 208 | preserveSymlinks?: boolean, 209 | ): URL { 210 | if (encodedSeparatorRegEx.exec(resolved.pathname) !== null) { 211 | throw new ERR_INVALID_MODULE_SPECIFIER( 212 | resolved.pathname, 213 | String.raw`must not include encoded "/" or "\" characters`, 214 | fileURLToPath(base), 215 | ); 216 | } 217 | 218 | let filePath: string; 219 | 220 | try { 221 | filePath = fileURLToPath(resolved); 222 | } catch (error) { 223 | Object.defineProperty(error, "input", { value: String(resolved) }); 224 | Object.defineProperty(error, "module", { value: String(base) }); 225 | throw error; 226 | } 227 | 228 | const stats = tryStatSync( 229 | filePath.endsWith("/") ? filePath.slice(-1) : filePath, 230 | ); 231 | 232 | if (stats && stats.isDirectory()) { 233 | // @ts-expect-error TODO: type issue 234 | const error = new ERR_UNSUPPORTED_DIR_IMPORT(filePath, fileURLToPath(base)); 235 | // @ts-expect-error Add this for `import.meta.resolve`. 236 | error.url = String(resolved); 237 | throw error; 238 | } 239 | 240 | if (!stats || !stats.isFile()) { 241 | const error = new ERR_MODULE_NOT_FOUND( 242 | filePath || resolved.pathname, 243 | base && fileURLToPath(base), 244 | true, 245 | ); 246 | // @ts-expect-error Add this for `import.meta.resolve`. 247 | error.url = String(resolved); 248 | throw error; 249 | } 250 | 251 | if (!preserveSymlinks) { 252 | const real = realpathSync(filePath); 253 | const { search, hash } = resolved; 254 | resolved = pathToFileURL(real + (filePath.endsWith(path.sep) ? "/" : "")); 255 | resolved.search = search; 256 | resolved.hash = hash; 257 | } 258 | 259 | return resolved; 260 | } 261 | 262 | function importNotDefined( 263 | specifier: string, 264 | packageJsonUrl: URL | undefined, 265 | base: URL, 266 | ): Error { 267 | return new ERR_PACKAGE_IMPORT_NOT_DEFINED( 268 | specifier, 269 | packageJsonUrl && fileURLToPath(new URL(".", packageJsonUrl)), 270 | fileURLToPath(base), 271 | ); 272 | } 273 | 274 | function exportsNotFound( 275 | subpath: string, 276 | packageJsonUrl: URL, 277 | base: URL, 278 | ): Error { 279 | return new ERR_PACKAGE_PATH_NOT_EXPORTED( 280 | fileURLToPath(new URL(".", packageJsonUrl)), 281 | subpath, 282 | base && fileURLToPath(base), 283 | ); 284 | } 285 | 286 | function throwInvalidSubpath( 287 | request: string, 288 | match: string, 289 | packageJsonUrl: URL, 290 | internal: boolean, 291 | base: URL, 292 | ) { 293 | const reason = `request is not a valid match in pattern "${match}" for the "${ 294 | internal ? "imports" : "exports" 295 | }" resolution of ${fileURLToPath(packageJsonUrl)}`; 296 | throw new ERR_INVALID_MODULE_SPECIFIER( 297 | request, 298 | reason, 299 | base && fileURLToPath(base), 300 | ); 301 | } 302 | 303 | function invalidPackageTarget( 304 | subpath: string, 305 | target: unknown, 306 | packageJsonUrl: URL, 307 | internal: boolean, 308 | base: URL, 309 | ): Error { 310 | target = 311 | typeof target === "object" && target !== null 312 | ? JSON.stringify(target, null, "") 313 | : `${target}`; 314 | 315 | return new ERR_INVALID_PACKAGE_TARGET( 316 | fileURLToPath(new URL(".", packageJsonUrl)), 317 | subpath, 318 | target, 319 | internal, 320 | base && fileURLToPath(base), 321 | ); 322 | } 323 | 324 | function resolvePackageTargetString( 325 | target: string, 326 | subpath: string, 327 | match: string, 328 | packageJsonUrl: URL, 329 | base: URL, 330 | pattern: boolean, 331 | internal: boolean, 332 | isPathMap: boolean, 333 | conditions: Set | undefined, 334 | ): URL { 335 | if (subpath !== "" && !pattern && target.at(-1) !== "/") 336 | throw invalidPackageTarget(match, target, packageJsonUrl, internal, base); 337 | 338 | if (!target.startsWith("./")) { 339 | if (internal && !target.startsWith("../") && !target.startsWith("/")) { 340 | let isURL = false; 341 | 342 | try { 343 | new URL(target); 344 | isURL = true; 345 | } catch { 346 | // Continue regardless of error. 347 | } 348 | 349 | if (!isURL) { 350 | const exportTarget = pattern 351 | ? RegExpPrototypeSymbolReplace.call( 352 | patternRegEx, 353 | target, 354 | () => subpath, 355 | ) 356 | : target + subpath; 357 | 358 | return packageResolve(exportTarget, packageJsonUrl, conditions); 359 | } 360 | } 361 | 362 | throw invalidPackageTarget(match, target, packageJsonUrl, internal, base); 363 | } 364 | 365 | if (invalidSegmentRegEx.exec(target.slice(2)) !== null) { 366 | if (deprecatedInvalidSegmentRegEx.exec(target.slice(2)) === null) { 367 | if (!isPathMap) { 368 | const request = pattern 369 | ? match.replace("*", () => subpath) 370 | : match + subpath; 371 | const resolvedTarget = pattern 372 | ? RegExpPrototypeSymbolReplace.call( 373 | patternRegEx, 374 | target, 375 | () => subpath, 376 | ) 377 | : target; 378 | emitInvalidSegmentDeprecation( 379 | resolvedTarget, 380 | request, 381 | match, 382 | packageJsonUrl, 383 | internal, 384 | base, 385 | true, 386 | ); 387 | } 388 | } else { 389 | throw invalidPackageTarget(match, target, packageJsonUrl, internal, base); 390 | } 391 | } 392 | 393 | const resolved = new URL(target, packageJsonUrl); 394 | const resolvedPath = resolved.pathname; 395 | const packagePath = new URL(".", packageJsonUrl).pathname; 396 | 397 | if (!resolvedPath.startsWith(packagePath)) 398 | throw invalidPackageTarget(match, target, packageJsonUrl, internal, base); 399 | 400 | if (subpath === "") return resolved; 401 | 402 | if (invalidSegmentRegEx.exec(subpath) !== null) { 403 | const request = pattern 404 | ? match.replace("*", () => subpath) 405 | : match + subpath; 406 | if (deprecatedInvalidSegmentRegEx.exec(subpath) === null) { 407 | if (!isPathMap) { 408 | const resolvedTarget = pattern 409 | ? RegExpPrototypeSymbolReplace.call( 410 | patternRegEx, 411 | target, 412 | () => subpath, 413 | ) 414 | : target; 415 | emitInvalidSegmentDeprecation( 416 | resolvedTarget, 417 | request, 418 | match, 419 | packageJsonUrl, 420 | internal, 421 | base, 422 | false, 423 | ); 424 | } 425 | } else { 426 | throwInvalidSubpath(request, match, packageJsonUrl, internal, base); 427 | } 428 | } 429 | 430 | if (pattern) { 431 | return new URL( 432 | RegExpPrototypeSymbolReplace.call( 433 | patternRegEx, 434 | resolved.href, 435 | () => subpath, 436 | ), 437 | ); 438 | } 439 | 440 | return new URL(subpath, resolved); 441 | } 442 | 443 | function isArrayIndex(key: string): boolean { 444 | const keyNumber = Number(key); 445 | if (`${keyNumber}` !== key) return false; 446 | return keyNumber >= 0 && keyNumber < 0xff_ff_ff_ff; 447 | } 448 | 449 | function resolvePackageTarget( 450 | packageJsonUrl: URL, 451 | target: unknown, 452 | subpath: string, 453 | packageSubpath: string, 454 | base: URL, 455 | pattern: boolean, 456 | internal: boolean, 457 | isPathMap: boolean, 458 | conditions: Set | undefined, 459 | ): URL | null { 460 | if (typeof target === "string") { 461 | return resolvePackageTargetString( 462 | target, 463 | subpath, 464 | packageSubpath, 465 | packageJsonUrl, 466 | base, 467 | pattern, 468 | internal, 469 | isPathMap, 470 | conditions, 471 | ); 472 | } 473 | 474 | if (Array.isArray(target)) { 475 | const targetList: Array = target; 476 | if (targetList.length === 0) return null; 477 | 478 | let lastException: ErrnoException | null | undefined; 479 | let i = -1; 480 | 481 | while (++i < targetList.length) { 482 | const targetItem = targetList[i]; 483 | let resolveResult: URL | null; 484 | try { 485 | resolveResult = resolvePackageTarget( 486 | packageJsonUrl, 487 | targetItem, 488 | subpath, 489 | packageSubpath, 490 | base, 491 | pattern, 492 | internal, 493 | isPathMap, 494 | conditions, 495 | ); 496 | } catch (error) { 497 | const exception = error as ErrnoException; 498 | lastException = exception; 499 | if (exception.code === "ERR_INVALID_PACKAGE_TARGET") continue; 500 | throw error; 501 | } 502 | 503 | if (resolveResult === undefined) continue; 504 | 505 | if (resolveResult === null) { 506 | lastException = null; 507 | continue; 508 | } 509 | 510 | return resolveResult; 511 | } 512 | 513 | if (lastException === undefined || lastException === null) { 514 | return null; 515 | } 516 | 517 | throw lastException; 518 | } 519 | 520 | if (typeof target === "object" && target !== null) { 521 | const keys = Object.getOwnPropertyNames(target); 522 | let i = -1; 523 | 524 | while (++i < keys.length) { 525 | const key = keys[i]!; 526 | if (isArrayIndex(key)) { 527 | throw new ERR_INVALID_PACKAGE_CONFIG( 528 | fileURLToPath(packageJsonUrl), 529 | fileURLToPath(base), 530 | '"exports" cannot contain numeric property keys.', 531 | ); 532 | } 533 | } 534 | 535 | i = -1; 536 | 537 | while (++i < keys.length) { 538 | const key = keys[i]!; 539 | if (key === "default" || (conditions && conditions.has(key))) { 540 | // @ts-expect-error: indexable. 541 | const conditionalTarget: unknown = target[key]; 542 | const resolveResult = resolvePackageTarget( 543 | packageJsonUrl, 544 | conditionalTarget, 545 | subpath, 546 | packageSubpath, 547 | base, 548 | pattern, 549 | internal, 550 | isPathMap, 551 | conditions, 552 | ); 553 | if (resolveResult === undefined) continue; 554 | return resolveResult; 555 | } 556 | } 557 | 558 | return null; 559 | } 560 | 561 | if (target === null) { 562 | return null; 563 | } 564 | 565 | throw invalidPackageTarget( 566 | packageSubpath, 567 | target, 568 | packageJsonUrl, 569 | internal, 570 | base, 571 | ); 572 | } 573 | 574 | function isConditionalExportsMainSugar( 575 | exports: unknown, 576 | packageJsonUrl: URL, 577 | base: URL, 578 | ): boolean { 579 | if (typeof exports === "string" || Array.isArray(exports)) return true; 580 | if (typeof exports !== "object" || exports === null) return false; 581 | 582 | const keys = Object.getOwnPropertyNames(exports); 583 | let isConditionalSugar = false; 584 | let i = 0; 585 | let keyIndex = -1; 586 | while (++keyIndex < keys.length) { 587 | const key = keys[keyIndex]!; 588 | const currentIsConditionalSugar = key === "" || key[0] !== "."; 589 | if (i++ === 0) { 590 | isConditionalSugar = currentIsConditionalSugar; 591 | } else if (isConditionalSugar !== currentIsConditionalSugar) { 592 | throw new ERR_INVALID_PACKAGE_CONFIG( 593 | fileURLToPath(packageJsonUrl), 594 | fileURLToPath(base), 595 | "\"exports\" cannot contain some keys starting with '.' and some not." + 596 | " The exports object must either be an object of package subpath keys" + 597 | " or an object of main entry condition name keys only.", 598 | ); 599 | } 600 | } 601 | 602 | return isConditionalSugar; 603 | } 604 | 605 | function emitTrailingSlashPatternDeprecation( 606 | match: string, 607 | pjsonUrl: URL, 608 | base: URL, 609 | ) { 610 | // @ts-expect-error: apparently it does exist, TS. 611 | if (process.noDeprecation) { 612 | return; 613 | } 614 | 615 | const pjsonPath = fileURLToPath(pjsonUrl); 616 | if (emittedPackageWarnings.has(pjsonPath + "|" + match)) return; 617 | emittedPackageWarnings.add(pjsonPath + "|" + match); 618 | process.emitWarning( 619 | `Use of deprecated trailing slash pattern mapping "${match}" in the ` + 620 | `"exports" field module resolution of the package at ${pjsonPath}${ 621 | base ? ` imported from ${fileURLToPath(base)}` : "" 622 | }. Mapping specifiers ending in "/" is no longer supported.`, 623 | "DeprecationWarning", 624 | "DEP0155", 625 | ); 626 | } 627 | 628 | function packageExportsResolve( 629 | packageJsonUrl: URL, 630 | packageSubpath: string, 631 | packageConfig: Record, 632 | base: URL, 633 | conditions: Set | undefined, 634 | ): URL { 635 | let exports = packageConfig.exports; 636 | 637 | if (isConditionalExportsMainSugar(exports, packageJsonUrl, base)) { 638 | exports = { ".": exports }; 639 | } 640 | 641 | if ( 642 | own.call(exports, packageSubpath) && 643 | !packageSubpath.includes("*") && 644 | !packageSubpath.endsWith("/") 645 | ) { 646 | // @ts-expect-error: indexable. 647 | const target = exports[packageSubpath]; 648 | const resolveResult = resolvePackageTarget( 649 | packageJsonUrl, 650 | target, 651 | "", 652 | packageSubpath, 653 | base, 654 | false, 655 | false, 656 | false, 657 | conditions, 658 | ); 659 | if (resolveResult === null || resolveResult === undefined) { 660 | throw exportsNotFound(packageSubpath, packageJsonUrl, base); 661 | } 662 | 663 | return resolveResult; 664 | } 665 | 666 | let bestMatch = ""; 667 | let bestMatchSubpath = ""; 668 | const keys = Object.getOwnPropertyNames(exports); 669 | let i = -1; 670 | 671 | while (++i < keys.length) { 672 | const key = keys[i]!; 673 | const patternIndex = key.indexOf("*"); 674 | 675 | if ( 676 | patternIndex !== -1 && 677 | packageSubpath.startsWith(key.slice(0, patternIndex)) 678 | ) { 679 | // When this reaches EOL, this can throw at the top of the whole function: 680 | // 681 | // if (StringPrototypeEndsWith(packageSubpath, '/')) 682 | // throwInvalidSubpath(packageSubpath) 683 | // 684 | // To match "imports" and the spec. 685 | if (packageSubpath.endsWith("/")) { 686 | emitTrailingSlashPatternDeprecation( 687 | packageSubpath, 688 | packageJsonUrl, 689 | base, 690 | ); 691 | } 692 | 693 | const patternTrailer = key.slice(patternIndex + 1); 694 | 695 | if ( 696 | packageSubpath.length >= key.length && 697 | packageSubpath.endsWith(patternTrailer) && 698 | patternKeyCompare(bestMatch, key) === 1 && 699 | key.lastIndexOf("*") === patternIndex 700 | ) { 701 | bestMatch = key; 702 | bestMatchSubpath = packageSubpath.slice( 703 | patternIndex, 704 | packageSubpath.length - patternTrailer.length, 705 | ); 706 | } 707 | } 708 | } 709 | 710 | if (bestMatch) { 711 | // @ts-expect-error: indexable. 712 | const target: unknown = exports[bestMatch]; 713 | const resolveResult = resolvePackageTarget( 714 | packageJsonUrl, 715 | target, 716 | bestMatchSubpath, 717 | bestMatch, 718 | base, 719 | true, 720 | false, 721 | packageSubpath.endsWith("/"), 722 | conditions, 723 | ); 724 | 725 | if (resolveResult === null || resolveResult === undefined) { 726 | throw exportsNotFound(packageSubpath, packageJsonUrl, base); 727 | } 728 | 729 | return resolveResult; 730 | } 731 | 732 | throw exportsNotFound(packageSubpath, packageJsonUrl, base); 733 | } 734 | 735 | function patternKeyCompare(a: string, b: string) { 736 | const aPatternIndex = a.indexOf("*"); 737 | const bPatternIndex = b.indexOf("*"); 738 | const baseLengthA = aPatternIndex === -1 ? a.length : aPatternIndex + 1; 739 | const baseLengthB = bPatternIndex === -1 ? b.length : bPatternIndex + 1; 740 | if (baseLengthA > baseLengthB) return -1; 741 | if (baseLengthB > baseLengthA) return 1; 742 | if (aPatternIndex === -1) return 1; 743 | if (bPatternIndex === -1) return -1; 744 | if (a.length > b.length) return -1; 745 | if (b.length > a.length) return 1; 746 | return 0; 747 | } 748 | 749 | function packageImportsResolve( 750 | name: string, 751 | base: URL, 752 | conditions?: Set, 753 | ): URL { 754 | if (name === "#" || name.startsWith("#/") || name.endsWith("/")) { 755 | const reason = "is not a valid internal imports specifier name"; 756 | throw new ERR_INVALID_MODULE_SPECIFIER(name, reason, fileURLToPath(base)); 757 | } 758 | 759 | let packageJsonUrl: URL | undefined; 760 | 761 | const packageConfig = getPackageScopeConfig(base); 762 | 763 | if (packageConfig.exists) { 764 | packageJsonUrl = pathToFileURL(packageConfig.pjsonPath); 765 | const imports = packageConfig.imports; 766 | if (imports) { 767 | if (own.call(imports, name) && !name.includes("*")) { 768 | const resolveResult = resolvePackageTarget( 769 | packageJsonUrl, 770 | imports[name], 771 | "", 772 | name, 773 | base, 774 | false, 775 | true, 776 | false, 777 | conditions, 778 | ); 779 | if (resolveResult !== null && resolveResult !== undefined) { 780 | return resolveResult; 781 | } 782 | } else { 783 | let bestMatch = ""; 784 | let bestMatchSubpath = ""; 785 | const keys = Object.getOwnPropertyNames(imports); 786 | let i = -1; 787 | 788 | while (++i < keys.length) { 789 | const key = keys[i]!; 790 | const patternIndex = key.indexOf("*"); 791 | 792 | if (patternIndex !== -1 && name.startsWith(key.slice(0, -1))) { 793 | const patternTrailer = key.slice(patternIndex + 1); 794 | if ( 795 | name.length >= key.length && 796 | name.endsWith(patternTrailer) && 797 | patternKeyCompare(bestMatch, key) === 1 && 798 | key.lastIndexOf("*") === patternIndex 799 | ) { 800 | bestMatch = key; 801 | bestMatchSubpath = name.slice( 802 | patternIndex, 803 | name.length - patternTrailer.length, 804 | ); 805 | } 806 | } 807 | } 808 | 809 | if (bestMatch) { 810 | const target = imports[bestMatch]; 811 | const resolveResult = resolvePackageTarget( 812 | packageJsonUrl, 813 | target, 814 | bestMatchSubpath, 815 | bestMatch, 816 | base, 817 | true, 818 | true, 819 | false, 820 | conditions, 821 | ); 822 | 823 | if (resolveResult !== null && resolveResult !== undefined) { 824 | return resolveResult; 825 | } 826 | } 827 | } 828 | } 829 | } 830 | 831 | throw importNotDefined(name, packageJsonUrl, base); 832 | } 833 | 834 | /** 835 | * @param {string} specifier 836 | * @param {URL} base 837 | */ 838 | function parsePackageName(specifier: string, base: URL) { 839 | let separatorIndex = specifier.indexOf("/"); 840 | let validPackageName = true; 841 | let isScoped = false; 842 | if (specifier[0] === "@") { 843 | isScoped = true; 844 | if (separatorIndex === -1 || specifier.length === 0) { 845 | validPackageName = false; 846 | } else { 847 | separatorIndex = specifier.indexOf("/", separatorIndex + 1); 848 | } 849 | } 850 | 851 | const packageName = 852 | separatorIndex === -1 ? specifier : specifier.slice(0, separatorIndex); 853 | 854 | // Package name cannot have leading . and cannot have percent-encoding or 855 | // \\ separators. 856 | if (invalidPackageNameRegEx.exec(packageName) !== null) { 857 | validPackageName = false; 858 | } 859 | 860 | if (!validPackageName) { 861 | throw new ERR_INVALID_MODULE_SPECIFIER( 862 | specifier, 863 | "is not a valid package name", 864 | fileURLToPath(base), 865 | ); 866 | } 867 | 868 | const packageSubpath = 869 | "." + (separatorIndex === -1 ? "" : specifier.slice(separatorIndex)); 870 | 871 | return { packageName, packageSubpath, isScoped }; 872 | } 873 | 874 | function packageResolve( 875 | specifier: string, 876 | base: URL, 877 | conditions: Set | undefined, 878 | ): URL { 879 | if (nodeBuiltins.includes(specifier)) { 880 | return new URL("node:" + specifier); 881 | } 882 | 883 | const { packageName, packageSubpath, isScoped } = parsePackageName( 884 | specifier, 885 | base, 886 | ); 887 | 888 | // ResolveSelf 889 | const packageConfig = getPackageScopeConfig(base); 890 | 891 | if ( 892 | packageConfig.exists && 893 | packageConfig.name === packageName && 894 | packageConfig.exports !== undefined && 895 | packageConfig.exports !== null 896 | ) { 897 | const packageJsonUrl = pathToFileURL(packageConfig.pjsonPath); 898 | return packageExportsResolve( 899 | packageJsonUrl, 900 | packageSubpath, 901 | packageConfig, 902 | base, 903 | conditions, 904 | ); 905 | } 906 | 907 | let packageJsonUrl = new URL( 908 | "./node_modules/" + packageName + "/package.json", 909 | base, 910 | ); 911 | let packageJsonPath = fileURLToPath(packageJsonUrl); 912 | let lastPath: string; 913 | do { 914 | const stat = tryStatSync(packageJsonPath.slice(0, -13)); 915 | if (!stat || !stat.isDirectory()) { 916 | lastPath = packageJsonPath; 917 | packageJsonUrl = new URL( 918 | (isScoped ? "../../../../node_modules/" : "../../../node_modules/") + 919 | packageName + 920 | "/package.json", 921 | packageJsonUrl, 922 | ); 923 | packageJsonPath = fileURLToPath(packageJsonUrl); 924 | continue; 925 | } 926 | 927 | // Package match. 928 | const packageConfig = read(packageJsonPath, { base, specifier }); 929 | if (packageConfig.exports !== undefined && packageConfig.exports !== null) { 930 | return packageExportsResolve( 931 | packageJsonUrl, 932 | packageSubpath, 933 | packageConfig, 934 | base, 935 | conditions, 936 | ); 937 | } 938 | 939 | if (packageSubpath === ".") { 940 | return legacyMainResolve(packageJsonUrl, packageConfig, base); 941 | } 942 | 943 | return new URL(packageSubpath, packageJsonUrl); 944 | // Cross-platform root check. 945 | } while (packageJsonPath.length !== lastPath.length); 946 | 947 | throw new ERR_MODULE_NOT_FOUND(packageName, fileURLToPath(base), false); 948 | } 949 | 950 | function isRelativeSpecifier(specifier: string): boolean { 951 | if (specifier[0] === ".") { 952 | if (specifier.length === 1 || specifier[1] === "/") return true; 953 | if ( 954 | specifier[1] === "." && 955 | (specifier.length === 2 || specifier[2] === "/") 956 | ) { 957 | return true; 958 | } 959 | } 960 | 961 | return false; 962 | } 963 | 964 | function shouldBeTreatedAsRelativeOrAbsolutePath(specifier: string): boolean { 965 | if (specifier === "") return false; 966 | if (specifier[0] === "/") return true; 967 | return isRelativeSpecifier(specifier); 968 | } 969 | 970 | /** 971 | * The “Resolver Algorithm Specification” as detailed in the Node docs (which is 972 | * sync and slightly lower-level than `resolve`). 973 | * 974 | * @param {string} specifier 975 | * `/example.js`, `./example.js`, `../example.js`, `some-package`, `fs`, etc. 976 | * @param {URL} base 977 | * Full URL (to a file) that `specifier` is resolved relative from. 978 | * @param {Set} [conditions] 979 | * Conditions. 980 | * @param {boolean} [preserveSymlinks] 981 | * Keep symlinks instead of resolving them. 982 | * @returns {URL} 983 | * A URL object to the found thing. 984 | */ 985 | export function moduleResolve( 986 | specifier: string, 987 | base: URL, 988 | conditions?: Set, 989 | preserveSymlinks?: boolean, 990 | ): URL { 991 | // Note: The Node code supports `base` as a string (in this internal API) too, 992 | // we don’t. 993 | const protocol = base.protocol; 994 | const isData = protocol === "data:"; 995 | // Order swapped from spec for minor perf gain. 996 | // Ok since relative URLs cannot parse as URLs. 997 | let resolved: URL | undefined; 998 | 999 | if (shouldBeTreatedAsRelativeOrAbsolutePath(specifier)) { 1000 | try { 1001 | resolved = new URL(specifier, base); 1002 | } catch (error_) { 1003 | // @ts-expect-error TODO: type issue 1004 | const error = new ERR_UNSUPPORTED_RESOLVE_REQUEST(specifier, base); 1005 | error.cause = error_; 1006 | throw error; 1007 | } 1008 | } else if (protocol === "file:" && specifier[0] === "#") { 1009 | resolved = packageImportsResolve(specifier, base, conditions); 1010 | } else { 1011 | try { 1012 | resolved = new URL(specifier); 1013 | } catch (error_) { 1014 | // Note: actual code uses `canBeRequiredWithoutScheme`. 1015 | if (isData && !nodeBuiltins.includes(specifier)) { 1016 | // @ts-expect-error TODO: type issue 1017 | const error = new ERR_UNSUPPORTED_RESOLVE_REQUEST(specifier, base); 1018 | error.cause = error_; 1019 | throw error; 1020 | } 1021 | 1022 | resolved = packageResolve(specifier, base, conditions); 1023 | } 1024 | } 1025 | 1026 | assert(resolved !== undefined, "expected to be defined"); 1027 | 1028 | if (resolved.protocol !== "file:") { 1029 | return resolved; 1030 | } 1031 | 1032 | return finalizeResolution(resolved, base, preserveSymlinks); 1033 | } 1034 | -------------------------------------------------------------------------------- /src/resolve.ts: -------------------------------------------------------------------------------- 1 | import { lstatSync, realpathSync, statSync } from "node:fs"; 2 | import { fileURLToPath, pathToFileURL } from "node:url"; 3 | import { isAbsolute } from "node:path"; 4 | import { moduleResolve } from "./internal/resolve.ts"; 5 | import { nodeBuiltins } from "./internal/builtins.ts"; 6 | 7 | const DEFAULT_CONDITIONS_SET = /* #__PURE__ */ new Set(["node", "import"]); 8 | 9 | const isWindows = /* #__PURE__ */ (() => process.platform === "win32")(); 10 | 11 | const NOT_FOUND_ERRORS = /* #__PURE__ */ new Set([ 12 | "ERR_MODULE_NOT_FOUND", 13 | "ERR_UNSUPPORTED_DIR_IMPORT", 14 | "MODULE_NOT_FOUND", 15 | "ERR_PACKAGE_PATH_NOT_EXPORTED", 16 | "ERR_PACKAGE_IMPORT_NOT_DEFINED", 17 | ]); 18 | 19 | const globalCache = /* #__PURE__ */ (() => 20 | // eslint-disable-next-line unicorn/no-unreadable-iife 21 | ((globalThis as any)["__EXSOLVE_CACHE__"] ||= new Map()))() as Map< 22 | string, 23 | unknown 24 | >; 25 | 26 | /** 27 | * Options to configure module resolution. 28 | */ 29 | export type ResolveOptions = { 30 | /** 31 | * A URL, path, or array of URLs/paths from which to resolve the module. 32 | * If not provided, resolution starts from the current working directory. 33 | * You can use `import.meta.url` to mimic the behavior of `import.meta.resolve()`. 34 | * For better performance, use a `file://` URL or path that ends with `/`. 35 | */ 36 | from?: string | URL | (string | URL)[]; 37 | 38 | /** 39 | * Resolve cache (enabled by default with a shared global object). 40 | * Can be set to `false` to disable or a custom `Map` to bring your own cache object. 41 | */ 42 | cache?: boolean | Map; 43 | 44 | /** 45 | * Additional file extensions to check. 46 | * For better performance, use explicit extensions and avoid this option. 47 | */ 48 | extensions?: string[]; 49 | 50 | /** 51 | * Conditions to apply when resolving package exports. 52 | * Defaults to `["node", "import"]`. 53 | * Conditions are applied without order. 54 | */ 55 | conditions?: string[]; 56 | 57 | /** 58 | * Path suffixes to check. 59 | * For better performance, use explicit paths and avoid this option. 60 | * Example: `["", "/index"]` 61 | */ 62 | suffixes?: string[]; 63 | 64 | /** 65 | * If set to `true` and the module cannot be resolved, 66 | * the resolver returns `undefined` instead of throwing an error. 67 | */ 68 | try?: boolean; 69 | }; 70 | 71 | export type ResolverOptions = Omit; 72 | 73 | type ResolveRes = Opts["try"] extends true 74 | ? string | undefined 75 | : string; 76 | 77 | /** 78 | * Synchronously resolves a module url based on the options provided. 79 | * 80 | * @param {string} input - The identifier or path of the module to resolve. 81 | * @param {ResolveOptions} [options] - Options to resolve the module. See {@link ResolveOptions}. 82 | * @returns {string} The resolved URL as a string. 83 | */ 84 | export function resolveModuleURL( 85 | input: string | URL, 86 | options?: O, 87 | ): ResolveRes { 88 | const parsedInput = _parseInput(input); 89 | 90 | if ("external" in parsedInput) { 91 | return parsedInput.external as ResolveRes; 92 | } 93 | 94 | const specifier = (parsedInput as { specifier: string }).specifier; 95 | let url = (parsedInput as { url: URL }).url; 96 | let absolutePath = (parsedInput as { absolutePath: string }).absolutePath; 97 | 98 | // Check for cache 99 | let cacheKey: string | undefined; 100 | let cacheObj: Map | undefined; 101 | if (options?.cache !== false) { 102 | cacheKey = _cacheKey(absolutePath || specifier, options); 103 | cacheObj = 104 | options?.cache && typeof options?.cache === "object" 105 | ? options.cache 106 | : globalCache; 107 | } 108 | 109 | if (cacheObj) { 110 | const cached = cacheObj.get(cacheKey!); 111 | if (typeof cached === "string") { 112 | return cached; 113 | } 114 | if (cached instanceof Error) { 115 | if (options?.try) { 116 | return undefined as any; 117 | } 118 | throw cached; 119 | } 120 | } 121 | 122 | // Absolute path to file (fast path) 123 | if (absolutePath) { 124 | try { 125 | const stat = lstatSync(absolutePath); 126 | 127 | if (stat.isSymbolicLink()) { 128 | absolutePath = realpathSync(absolutePath); 129 | url = pathToFileURL(absolutePath); 130 | } 131 | 132 | if (stat.isFile()) { 133 | if (cacheObj) { 134 | cacheObj.set(cacheKey!, url.href); 135 | } 136 | return url.href; 137 | } 138 | } catch (error: any) { 139 | if (error?.code !== "ENOENT") { 140 | if (cacheObj) { 141 | cacheObj.set(cacheKey!, error); 142 | } 143 | throw error; 144 | } 145 | } 146 | } 147 | 148 | // Condition set 149 | const conditionsSet = options?.conditions 150 | ? new Set(options.conditions) 151 | : DEFAULT_CONDITIONS_SET; 152 | 153 | // Search through bases 154 | const bases: URL[] = _normalizeBases(options?.from); 155 | const suffixes = options?.suffixes || [""]; 156 | const extensions = options?.extensions ? ["", ...options.extensions] : [""]; 157 | let resolved: URL | undefined; 158 | for (const base of bases) { 159 | for (const suffix of suffixes) { 160 | for (const extension of extensions) { 161 | resolved = _tryModuleResolve( 162 | _join(specifier || url.href, suffix) + extension, 163 | base, 164 | conditionsSet, 165 | ); 166 | if (resolved) { 167 | break; 168 | } 169 | } 170 | if (resolved) { 171 | break; 172 | } 173 | } 174 | if (resolved) { 175 | break; 176 | } 177 | } 178 | 179 | // Throw error if not found 180 | if (!resolved) { 181 | const error = new Error( 182 | `Cannot resolve module "${input}" (from: ${bases.map((u) => _fmtPath(u)).join(", ")})`, 183 | ); 184 | // @ts-ignore 185 | error.code = "ERR_MODULE_NOT_FOUND"; 186 | 187 | if (cacheObj) { 188 | cacheObj.set(cacheKey!, error); 189 | } 190 | 191 | if (options?.try) { 192 | return undefined as any; 193 | } 194 | 195 | throw error; 196 | } 197 | 198 | if (cacheObj) { 199 | cacheObj.set(cacheKey!, resolved.href); 200 | } 201 | 202 | return resolved.href; 203 | } 204 | 205 | /** 206 | * Synchronously resolves a module then converts it to a file path 207 | * 208 | * (throws error if reolved path is not file:// scheme) 209 | * 210 | * @param {string} id - The identifier or path of the module to resolve. 211 | * @param {ResolveOptions} [options] - Options to resolve the module. See {@link ResolveOptions}. 212 | * @returns {string} The resolved URL as a string. 213 | */ 214 | export function resolveModulePath( 215 | id: string | URL, 216 | options?: O, 217 | ): ResolveRes { 218 | const resolved = resolveModuleURL(id, options); 219 | if (!resolved) { 220 | return undefined as ResolveRes; 221 | } 222 | if (!resolved.startsWith("file://") && options?.try) { 223 | return undefined as ResolveRes; 224 | } 225 | const absolutePath = fileURLToPath(resolved); 226 | return isWindows ? _normalizeWinPath(absolutePath) : absolutePath; 227 | } 228 | 229 | export function createResolver(defaults?: ResolverOptions) { 230 | if (defaults?.from) { 231 | defaults = { 232 | ...defaults, 233 | from: _normalizeBases(defaults?.from), 234 | }; 235 | } 236 | return { 237 | resolveModuleURL: ( 238 | id: string | URL, 239 | opts: ResolveOptions, 240 | ): ResolveRes => resolveModuleURL(id, { ...defaults, ...opts }), 241 | resolveModulePath: ( 242 | id: string | URL, 243 | opts: ResolveOptions, 244 | ): ResolveRes => resolveModulePath(id, { ...defaults, ...opts }), 245 | clearResolveCache: () => { 246 | if (defaults?.cache !== false) { 247 | if (defaults?.cache && typeof defaults?.cache === "object") { 248 | defaults.cache.clear(); 249 | } else { 250 | globalCache.clear(); 251 | } 252 | } 253 | }, 254 | }; 255 | } 256 | 257 | export function clearResolveCache() { 258 | globalCache.clear(); 259 | } 260 | 261 | // --- Internal --- 262 | 263 | function _tryModuleResolve( 264 | specifier: string, 265 | base: URL, 266 | conditions: any, 267 | ): URL | undefined { 268 | try { 269 | return moduleResolve(specifier, base, conditions); 270 | } catch (error: any) { 271 | if (!NOT_FOUND_ERRORS.has(error?.code)) { 272 | throw error; 273 | } 274 | } 275 | } 276 | 277 | function _normalizeBases(inputs: unknown): URL[] { 278 | const urls = (Array.isArray(inputs) ? inputs : [inputs]).flatMap((input) => 279 | _normalizeBase(input), 280 | ); 281 | if (urls.length === 0) { 282 | return [pathToFileURL("./")]; 283 | } 284 | return urls; 285 | } 286 | 287 | function _normalizeBase(input: unknown): URL | URL[] { 288 | if (!input) { 289 | return []; 290 | } 291 | if (input instanceof URL) { 292 | return [input]; 293 | } 294 | if (typeof input !== "string") { 295 | return []; 296 | } 297 | if (/^(?:node|data|http|https|file):/.test(input)) { 298 | return new URL(input); 299 | } 300 | try { 301 | if (input.endsWith("/") || statSync(input).isDirectory()) { 302 | return pathToFileURL(input + "/"); 303 | } 304 | return pathToFileURL(input); 305 | } catch { 306 | return [pathToFileURL(input + "/"), pathToFileURL(input)]; 307 | } 308 | } 309 | 310 | function _fmtPath(input: URL | string) { 311 | try { 312 | return fileURLToPath(input); 313 | } catch { 314 | return input; 315 | } 316 | } 317 | 318 | function _cacheKey(id: string, opts?: ResolveOptions) { 319 | return JSON.stringify([ 320 | id, 321 | (opts?.conditions || ["node", "import"]).sort(), 322 | opts?.extensions, 323 | opts?.from, 324 | opts?.suffixes, 325 | ]); 326 | } 327 | 328 | function _join(a: string, b: string): string { 329 | if (!a || !b || b === "/") { 330 | return a; 331 | } 332 | return (a.endsWith("/") ? a : a + "/") + (b.startsWith("/") ? b.slice(1) : b); 333 | } 334 | 335 | function _normalizeWinPath(path: string): string { 336 | return path.replace(/\\/g, "/").replace(/^[a-z]:\//, (r) => r.toUpperCase()); 337 | } 338 | 339 | function _parseInput( 340 | input: string | URL, 341 | ): 342 | | { url: URL; absolutePath: string } 343 | | { external: string } 344 | | { specifier: string } { 345 | if (typeof input === "string") { 346 | if (input.startsWith("file:")) { 347 | const url = new URL(input); 348 | return { url, absolutePath: fileURLToPath(url) }; 349 | } 350 | 351 | if (isAbsolute(input)) { 352 | return { url: pathToFileURL(input), absolutePath: input }; 353 | } 354 | 355 | if (/^(?:node|data|http|https):/.test(input)) { 356 | return { external: input }; 357 | } 358 | 359 | if (nodeBuiltins.includes(input) && !input.includes(":")) { 360 | return { external: `node:${input}` }; 361 | } 362 | 363 | return { specifier: input }; 364 | } 365 | 366 | if (input instanceof URL) { 367 | if (input.protocol === "file:") { 368 | return { url: input, absolutePath: fileURLToPath(input) }; 369 | } 370 | return { external: input.href }; 371 | } 372 | 373 | throw new TypeError("id must be a `string` or `URL`"); 374 | } 375 | -------------------------------------------------------------------------------- /test/fixture/cjs.mjs: -------------------------------------------------------------------------------- 1 | import { createCommonJS } from "mlly"; 2 | 3 | const cjs = createCommonJS(import.meta.url); 4 | 5 | console.log(cjs); 6 | console.log(cjs.require.resolve("../../package.json")); 7 | -------------------------------------------------------------------------------- /test/fixture/eval-err.mjs: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | 3 | async function test() { 4 | throw new Error("Something went wrong in eval-err module!"); 5 | } 6 | 7 | await test(); 8 | -------------------------------------------------------------------------------- /test/fixture/eval.mjs: -------------------------------------------------------------------------------- 1 | import { evalModule, loadModule, fileURLToPath } from "mlly"; 2 | 3 | await evalModule('console.log("Eval works!")'); 4 | 5 | await evalModule( 6 | ` 7 | import { reverse } from './utils.mjs' 8 | console.log(reverse('!emosewa si sj')) 9 | `, 10 | { 11 | url: fileURLToPath(import.meta.url), 12 | }, 13 | ); 14 | 15 | await loadModule("./hello.mjs", { url: import.meta.url }); 16 | 17 | console.log( 18 | await loadModule("../../package.json", { url: import.meta.url }).then( 19 | (r) => r.default.name, 20 | ), 21 | ); 22 | 23 | await loadModule("./eval-err.mjs", { url: import.meta.url }).catch((error) => 24 | console.error(error), 25 | ); 26 | -------------------------------------------------------------------------------- /test/fixture/exports.mjs: -------------------------------------------------------------------------------- 1 | export * from "pathe"; 2 | 3 | export { resolve as _resolve } from "pathe"; 4 | 5 | export const foo = "bar"; 6 | -------------------------------------------------------------------------------- /test/fixture/foo/index.mjs: -------------------------------------------------------------------------------- 1 | console.log(import.meta.url); 2 | -------------------------------------------------------------------------------- /test/fixture/hello.link.mjs: -------------------------------------------------------------------------------- 1 | hello.mjs -------------------------------------------------------------------------------- /test/fixture/hello.mjs: -------------------------------------------------------------------------------- 1 | import pkg from "../../package.json"; 2 | 3 | console.log("Hello world from", pkg.name, import.meta.url); 4 | -------------------------------------------------------------------------------- /test/fixture/resolve-err.mjs: -------------------------------------------------------------------------------- 1 | import { resolve } from "mlly"; 2 | 3 | console.log(await resolve("./404.mjs")); 4 | -------------------------------------------------------------------------------- /test/fixture/resolve.mjs: -------------------------------------------------------------------------------- 1 | import { resolvePath, createResolve, resolveImports } from "mlly"; 2 | 3 | const resolve = createResolve({ url: import.meta.url }); 4 | console.log(await resolve("./cjs.mjs")); 5 | 6 | console.log(await resolvePath("./cjs.mjs", { url: import.meta.url })); 7 | console.log(await resolvePath("./foo", { url: import.meta.url })); 8 | 9 | console.log( 10 | await resolveImports("import foo from './eval.mjs'", { 11 | url: import.meta.url, 12 | }), 13 | ); 14 | -------------------------------------------------------------------------------- /test/fixture/test.link.txt: -------------------------------------------------------------------------------- 1 | test.txt -------------------------------------------------------------------------------- /test/fixture/test.txt: -------------------------------------------------------------------------------- 1 | Test file 2 | -------------------------------------------------------------------------------- /test/fixture/utils.mjs: -------------------------------------------------------------------------------- 1 | export function reverse(str) { 2 | return [...str].reverse().join(""); 3 | } 4 | -------------------------------------------------------------------------------- /test/resolve.test.ts: -------------------------------------------------------------------------------- 1 | import { existsSync } from "node:fs"; 2 | import { resolve as nodeResolve } from "node:path"; 3 | import { fileURLToPath, pathToFileURL } from "node:url"; 4 | import { describe, it, expect, vi } from "vitest"; 5 | import { resolveModuleURL, resolveModulePath } from "../src"; 6 | 7 | const isWindows = process.platform === "win32"; 8 | 9 | const tests = [ 10 | // Resolve to path 11 | { input: "vitest", action: "resolves" }, 12 | { input: "./fixture/cjs.mjs", action: "resolves" }, 13 | { input: "./fixture/foo", action: "resolves" }, 14 | { 15 | input: fileURLToPath(new URL("fixture/foo", import.meta.url)), 16 | action: "resolves", 17 | }, 18 | // Return same input as-is 19 | { input: "https://foo.com/a/b.js?a=1", action: "same" }, 20 | // Throw error 21 | // { input: 'script:alert("a")', action: "throws" }, // TODO: fixture from mlly 22 | { input: "/non/existent", action: "throws" }, 23 | ] as const; 24 | 25 | const extensions = [".mjs", ".cjs", ".js", ".mts", ".cts", ".ts", ".json"]; 26 | 27 | const suffixes = ["", "/index"]; 28 | 29 | describe("resolveModuleURL", () => { 30 | for (const test of tests) { 31 | it(`${test.input} should ${test.action}`, () => { 32 | switch (test.action) { 33 | case "resolves": { 34 | const resolved = resolveModuleURL(test.input, { 35 | from: import.meta.url, 36 | extensions, 37 | suffixes, 38 | }); 39 | expect(existsSync(fileURLToPath(resolved))).toBe(true); 40 | break; 41 | } 42 | case "same": { 43 | const resolved = resolveModuleURL(test.input, { 44 | from: import.meta.url, 45 | extensions, 46 | suffixes, 47 | }); 48 | expect(resolved).toBe(test.input); 49 | break; 50 | } 51 | case "throws": { 52 | expect(() => resolveModuleURL(test.input)).toThrow(); 53 | break; 54 | } 55 | } 56 | }); 57 | } 58 | 59 | it("follows symlinks", () => { 60 | const resolved = resolveModuleURL("./fixture/hello.link.mjs", { 61 | from: import.meta.url, 62 | }); 63 | expect(fileURLToPath(resolved)).match(/fixture[/\\]hello\.mjs$/); 64 | 65 | const resolved2 = resolveModuleURL("./fixture/test.link.txt", { 66 | from: import.meta.url, 67 | }); 68 | expect(fileURLToPath(resolved2)).match(/fixture[/\\]test.txt$/); 69 | 70 | const absolutePath = nodeResolve( 71 | process.cwd(), 72 | "./test/fixture/hello.link.mjs", 73 | ); 74 | const resolved3 = resolveModuleURL(absolutePath); 75 | expect(fileURLToPath(resolved3)).match(/fixture[/\\]hello\.mjs$/); 76 | }); 77 | 78 | it("resolves node built-ints", () => { 79 | expect(resolveModuleURL("node:fs")).toBe("node:fs"); 80 | expect(resolveModuleURL("fs")).toBe("node:fs"); 81 | expect(resolveModuleURL("node:foo")).toBe("node:foo"); 82 | }); 83 | 84 | it("handles missing subpath imports", () => { 85 | const resolved = resolveModuleURL("#build/auth.js", { 86 | from: import.meta.url, 87 | try: true, 88 | }); 89 | expect(resolved).toBeUndefined(); 90 | }); 91 | 92 | it("should resolve suffixes to real file", () => { 93 | const res = resolveModuleURL( 94 | fileURLToPath(new URL("fixture/foo", import.meta.url)), 95 | { 96 | from: import.meta.url, 97 | suffixes: ["/index"], 98 | extensions: [".mjs"], 99 | }, 100 | ); 101 | expect(res).toMatch(/\.mjs$/); 102 | }); 103 | 104 | it("resolve builtin modules", () => { 105 | vi.mock("node:module", () => { 106 | return { 107 | builtinModules: ["fs", "path", "url", "http", "https", "bun:sqlite"], 108 | }; 109 | }); 110 | 111 | expect(() => resolveModuleURL("unknown")).toThrowError(); 112 | expect(resolveModuleURL("node:fs")).toBe("node:fs"); 113 | expect(resolveModuleURL("fs")).toBe("node:fs"); 114 | 115 | expect(resolveModuleURL("bun:sqlite")).toBe("bun:sqlite"); 116 | }); 117 | }); 118 | 119 | describe("resolveModulePath", () => { 120 | for (const test of tests) { 121 | it(`${test.input} should ${test.action}`, () => { 122 | const action = test.input.startsWith("https://") ? "throws" : test.action; 123 | switch (action) { 124 | case "resolves": { 125 | const resolved = resolveModulePath(test.input, { 126 | from: import.meta.url, 127 | extensions, 128 | suffixes, 129 | }); 130 | expect(existsSync(resolved)).toBe(true); 131 | break; 132 | } 133 | case "same": { 134 | const resolved = resolveModulePath(test.input, { 135 | from: import.meta.url, 136 | extensions, 137 | suffixes, 138 | }); 139 | expect(resolved).toBe(test.input); 140 | break; 141 | } 142 | case "throws": { 143 | expect(() => resolveModulePath(test.input)).toThrow(); 144 | break; 145 | } 146 | } 147 | }); 148 | } 149 | 150 | it("throws error for built-ins", () => { 151 | expect(() => resolveModulePath("fs")).toThrow(); 152 | expect(() => resolveModulePath("node:fs")).toThrow(); 153 | }); 154 | 155 | it("not throws error for built-ins when try", () => { 156 | expect(() => resolveModulePath("fs", { try: true })).not.toThrow(); 157 | expect(() => resolveModulePath("node:fs", { try: true })).not.toThrow(); 158 | 159 | expect(resolveModulePath("fs", { try: true })).toBeUndefined(); 160 | expect(resolveModulePath("node:fs", { try: true })).toBeUndefined(); 161 | }); 162 | }); 163 | 164 | describe.runIf(isWindows)("windows", () => { 165 | it("normalizes drive letter and slashes", () => { 166 | for (const input of [ 167 | "./fixture/hello.mjs", 168 | new URL("fixture/hello.mjs", import.meta.url), 169 | new URL("fixture/hello.mjs", import.meta.url).href.toLowerCase(), 170 | ]) { 171 | const resolved = resolveModulePath(input, { 172 | from: import.meta.url, 173 | }); 174 | expect(resolved).to.not.include("\\"); 175 | expect(resolved).to.include("/"); 176 | const DRIVE_LETTER_RE = /^\w(?=:)/; 177 | const resolvedDriveLetter = resolved.match(DRIVE_LETTER_RE)![0]; 178 | expect(resolvedDriveLetter).toBe(resolvedDriveLetter.toUpperCase()); 179 | } 180 | }); 181 | }); 182 | 183 | describe("normalized parent urls", () => { 184 | const cannotResolveError = (id: string, urls: (string | URL)[]) => 185 | Object.assign( 186 | new Error( 187 | `Cannot resolve module "${id}" (from: ${urls.map((u) => fileURLToPath(u)).join(", ")})`, 188 | ), 189 | { code: "ERR_MODULE_NOT_FOUND" }, 190 | ); 191 | 192 | const commonCases = [ 193 | [[undefined, false, 123] as unknown, [pathToFileURL("./")]], 194 | [import.meta.url, [import.meta.url]], 195 | [new URL(import.meta.url), [import.meta.url]], 196 | [__filename, [pathToFileURL(__filename)]], 197 | [__dirname, [pathToFileURL(__dirname) + "/"]], 198 | ]; 199 | 200 | const posixCases = [ 201 | ["file:///project/index.js", ["file:///project/index.js"]], 202 | ["file:///project/", ["file:///project/"]], 203 | ["file:///project", ["file:///project"]], 204 | ["/non/existent", ["file:///non/existent/", "file:///non/existent"]], 205 | ]; 206 | 207 | const windowsCases = [ 208 | [ 209 | String.raw`C:\non\existent`, 210 | ["file:///C:/non/existent/", "file:///C:/non/existent"], 211 | ], 212 | ]; 213 | 214 | const testCases = [ 215 | ...commonCases, 216 | ...(process.platform === "win32" ? windowsCases : posixCases), 217 | ] as Array<[string | string[], string[]]>; 218 | 219 | for (const [input, expected] of testCases) { 220 | it(JSON.stringify(input), () => { 221 | expect(() => resolveModuleURL("xyz", { from: input })).toThrow( 222 | cannotResolveError("xyz", expected), 223 | ); 224 | }); 225 | } 226 | }); 227 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "module": "preserve", 5 | "moduleDetection": "force", 6 | "esModuleInterop": true, 7 | "allowSyntheticDefaultImports": true, 8 | "resolveJsonModule": true, 9 | "strict": true, 10 | "isolatedModules": true, 11 | "verbatimModuleSyntax": true, 12 | "noUncheckedIndexedAccess": true, 13 | "forceConsistentCasingInFileNames": true, 14 | "noImplicitOverride": true, 15 | "allowImportingTsExtensions": true, 16 | // "isolatedDeclarations": true, 17 | // "declaration": true, 18 | "noEmit": true 19 | } 20 | } 21 | --------------------------------------------------------------------------------