├── funding.yml ├── .prettierignore ├── .npmrc ├── test ├── node_modules │ ├── alpha │ │ ├── index.js │ │ ├── other.js │ │ └── package.json │ ├── charlie-delta │ │ ├── index.js │ │ ├── another.js │ │ └── package.json │ ├── remark-lint │ │ ├── index.js │ │ └── package.json │ └── @foxtrot │ │ └── golf-hotel │ │ ├── other.js │ │ ├── index.js │ │ └── package.json └── index.js ├── .gitignore ├── .editorconfig ├── index.js ├── tsconfig.json ├── license ├── .github └── workflows │ └── main.yml ├── package.json ├── readme.md └── lib └── index.js /funding.yml: -------------------------------------------------------------------------------- 1 | github: wooorm 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | coverage/ 2 | *.md 3 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | ignore-scripts=true 2 | package-lock=false 3 | -------------------------------------------------------------------------------- /test/node_modules/alpha/index.js: -------------------------------------------------------------------------------- 1 | export default 'bravo'; 2 | -------------------------------------------------------------------------------- /test/node_modules/alpha/other.js: -------------------------------------------------------------------------------- 1 | export default 'other'; 2 | -------------------------------------------------------------------------------- /test/node_modules/charlie-delta/index.js: -------------------------------------------------------------------------------- 1 | export default 'echo'; 2 | -------------------------------------------------------------------------------- /test/node_modules/remark-lint/index.js: -------------------------------------------------------------------------------- 1 | export default 'echo'; 2 | -------------------------------------------------------------------------------- /test/node_modules/@foxtrot/golf-hotel/other.js: -------------------------------------------------------------------------------- 1 | export default 'other' 2 | -------------------------------------------------------------------------------- /test/node_modules/charlie-delta/another.js: -------------------------------------------------------------------------------- 1 | export default 'another'; 2 | -------------------------------------------------------------------------------- /test/node_modules/@foxtrot/golf-hotel/index.js: -------------------------------------------------------------------------------- 1 | export default 'india'; 2 | -------------------------------------------------------------------------------- /test/node_modules/alpha/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "module", 3 | "main": "index.js" 4 | } 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /coverage/ 2 | /node_modules/ 3 | *.d.ts 4 | *.log 5 | *.map 6 | .DS_Store 7 | yarn.lock 8 | -------------------------------------------------------------------------------- /test/node_modules/remark-lint/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "module", 3 | "main": "index.js" 4 | } 5 | -------------------------------------------------------------------------------- /test/node_modules/charlie-delta/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "module", 3 | "main": "index.js" 4 | } 5 | -------------------------------------------------------------------------------- /test/node_modules/@foxtrot/golf-hotel/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "module", 3 | "main": "index.js" 4 | } 5 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @typedef {import('./lib/index.js').LoadOptions} LoadOptions 3 | * @typedef {import('./lib/index.js').ResolveOptions} ResolveOptions 4 | */ 5 | 6 | export {loadPlugin, resolvePlugin} from './lib/index.js' 7 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "checkJs": true, 4 | "customConditions": ["development"], 5 | "declaration": true, 6 | "declarationMap": true, 7 | "emitDeclarationOnly": true, 8 | "exactOptionalPropertyTypes": true, 9 | "lib": ["es2022"], 10 | "module": "node16", 11 | "strict": true, 12 | "target": "es2022" 13 | }, 14 | "exclude": ["coverage/", "node_modules/"], 15 | "include": ["**/*.js"] 16 | } 17 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) 2016 Titus Wormer 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | 'Software'), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | jobs: 2 | full: 3 | name: main 4 | runs-on: ubuntu-latest 5 | steps: 6 | - uses: actions/checkout@v4 7 | - uses: actions/setup-node@v4 8 | with: 9 | node-version: node 10 | - run: npm install 11 | - run: npm install --global f-ck@2 12 | - run: npm test 13 | - uses: codecov/codecov-action@v3 14 | no-nvm: 15 | name: '${{matrix.node}} on ${{matrix.os}} w/o nvm' 16 | runs-on: ${{matrix.os}} 17 | steps: 18 | - uses: actions/checkout@v4 19 | - uses: actions/setup-node@v4 20 | with: 21 | node-version: ${{matrix.node}} 22 | - run: npm install 23 | - run: npm install --global f-ck@2 24 | - run: npm run test-coverage 25 | strategy: 26 | matrix: 27 | node: 28 | - lts/gallium 29 | - node 30 | os: 31 | - ubuntu-latest 32 | - windows-latest 33 | nvm: 34 | name: '${{matrix.node}} on ${{matrix.os}} w/ nvm' 35 | runs-on: ${{matrix.os}} 36 | steps: 37 | - uses: actions/checkout@v4 38 | - uses: dcodeIO/setup-node-nvm@v5 39 | with: 40 | node-version: ${{matrix.node}} 41 | - run: npm install 42 | - run: npm install --global f-ck@2 43 | - run: npm run test-coverage 44 | strategy: 45 | matrix: 46 | node: 47 | - node 48 | os: 49 | - ubuntu-latest 50 | - windows-latest 51 | on: 52 | - pull_request 53 | - push 54 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "load-plugin", 3 | "version": "6.0.3", 4 | "description": "Load a submodule, plugin, or file", 5 | "license": "MIT", 6 | "keywords": [ 7 | "load", 8 | "package", 9 | "plugin", 10 | "submodule" 11 | ], 12 | "repository": "wooorm/load-plugin", 13 | "bugs": "https://github.com/wooorm/load-plugin/issues", 14 | "funding": { 15 | "type": "github", 16 | "url": "https://github.com/sponsors/wooorm" 17 | }, 18 | "author": "Titus Wormer (https://wooorm.com)", 19 | "contributors": [ 20 | "Titus Wormer (https://wooorm.com)" 21 | ], 22 | "sideEffects": false, 23 | "type": "module", 24 | "exports": "./index.js", 25 | "files": [ 26 | "lib/", 27 | "index.d.ts.map", 28 | "index.d.ts", 29 | "index.js" 30 | ], 31 | "dependencies": { 32 | "@npmcli/config": "^8.0.0", 33 | "import-meta-resolve": "^4.0.0" 34 | }, 35 | "devDependencies": { 36 | "@types/node": "^20.0.0", 37 | "c8": "^9.0.0", 38 | "github-slugger": "^2.0.0", 39 | "prettier": "^3.0.0", 40 | "remark-cli": "^12.0.0", 41 | "remark-lint": "^10.0.0", 42 | "remark-preset-wooorm": "^10.0.0", 43 | "type-coverage": "^2.0.0", 44 | "typescript": "^5.0.0", 45 | "xo": "^0.58.0" 46 | }, 47 | "scripts": { 48 | "build": "tsc --build --clean && tsc --build && type-coverage", 49 | "format": "remark . --frail --output --quiet && prettier . --log-level warn --write && xo --fix", 50 | "prepack": "npm run build && npm run format", 51 | "test": "npm run build && npm run format && npm run test-coverage", 52 | "test-api": "node --conditions development test/index.js", 53 | "test-coverage": "c8 --100 --reporter lcov npm run test-api" 54 | }, 55 | "prettier": { 56 | "bracketSpacing": false, 57 | "singleQuote": true, 58 | "semi": false, 59 | "tabWidth": 2, 60 | "trailingComma": "none", 61 | "useTabs": false 62 | }, 63 | "remarkConfig": { 64 | "plugins": [ 65 | "remark-preset-wooorm" 66 | ] 67 | }, 68 | "typeCoverage": { 69 | "atLeast": 100, 70 | "detail": true, 71 | "ignoreCatch": true, 72 | "strict": true 73 | }, 74 | "xo": { 75 | "prettier": true, 76 | "rules": { 77 | "complexity": "off", 78 | "logical-assignment-operators": "off" 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # load-plugin 2 | 3 | [![Build][badge-build-image]][badge-build-url] 4 | [![Coverage][badge-coverage-image]][badge-coverage-url] 5 | [![Downloads][badge-downloads-image]][badge-downloads-url] 6 | 7 | Load a submodule, plugin, or file. 8 | 9 | ## Contents 10 | 11 | * [What is this?](#what-is-this) 12 | * [When to use this?](#when-to-use-this) 13 | * [Install](#install) 14 | * [Use](#use) 15 | * [API](#api) 16 | * [`loadPlugin(name[, options])`](#loadpluginname-options) 17 | * [`resolvePlugin(name[, options])`](#resolvepluginname-options) 18 | * [`LoadOptions`](#loadoptions) 19 | * [`ResolveOptions`](#resolveoptions) 20 | * [Compatibility](#compatibility) 21 | * [Security](#security) 22 | * [Contribute](#contribute) 23 | * [License](#license) 24 | 25 | ## What is this? 26 | 27 | This package is useful when you want to load plugins. 28 | It resolves things like Node.js does, 29 | but supports a prefix (when given a prefix `remark` and the user provided value 30 | `gfm` it can find `remark-gfm`), 31 | can load from several places, 32 | and optionally global too. 33 | 34 | ## When to use this? 35 | 36 | This package is particularly useful when you want users to configure something 37 | with plugins. 38 | One example is `remark-cli` which can load remark plugins from configuration 39 | files. 40 | 41 | ## Install 42 | 43 | This package is [ESM only][github-gist-esm]. 44 | In Node.js (version 16+), 45 | install with [npm][npm-install]: 46 | 47 | ```sh 48 | npm install load-plugin 49 | ``` 50 | 51 | ## Use 52 | 53 | Say we’re in this project (with dependencies installed): 54 | 55 | ```js 56 | import {loadPlugin, resolvePlugin} from 'load-plugin' 57 | 58 | console.log(await resolvePlugin('lint', {prefix: 'remark'})) 59 | // => 'file:///Users/tilde/Projects/oss/load-plugin/node_modules/remark-lint/index.js' 60 | 61 | console.log( 62 | await resolvePlugin('validator-identifier', {prefix: '@babel/helper'}) 63 | ) 64 | // => 'file:///Users/tilde/Projects/oss/load-plugin/node_modules/@babel/helper-validator-identifier/lib/index.js' 65 | 66 | console.log(await resolvePlugin('./index.js', {prefix: 'remark'})) 67 | // => 'file:///Users/tilde/Projects/oss/load-plugin/index.js' 68 | 69 | console.log(await loadPlugin('lint', {prefix: 'remark'})) 70 | // => [Function: remarkLint] 71 | ``` 72 | 73 | ## API 74 | 75 | This package exports the identifiers 76 | [`loadPlugin`][api-load-plugin] and [`resolvePlugin`][api-resolve-plugin]. 77 | There is no default export. 78 | 79 | It exports the [TypeScript][] types 80 | [`LoadOptions`][api-load-options] and [`ResolveOptions`][api-resolve-options]. 81 | 82 | ### `loadPlugin(name[, options])` 83 | 84 | Import `name` from `from` (and optionally the global `node_modules` directory). 85 | 86 | Uses the Node.js [resolution algorithm][nodejs-resolution-algo] (through 87 | [`import-meta-resolve`][github-import-meta-resolve]) to resolve CJS and ESM 88 | packages and files. 89 | 90 | If a `prefix` is given and `name` is not a path, 91 | `$prefix-$name` is also searched (preferring these over non-prefixed 92 | modules). 93 | If `name` starts with a scope (`@scope/name`), 94 | the prefix is applied after it: `@scope/$prefix-name`. 95 | 96 | ###### Parameters 97 | 98 | * `name` (`string`) 99 | — specifier 100 | * `options` ([`LoadOptions`][api-load-options], optional) 101 | — configuration 102 | 103 | ###### Returns 104 | 105 | Promise to a whole module or specific export (`Promise`). 106 | 107 | ### `resolvePlugin(name[, options])` 108 | 109 | Resolve `name` from `from`. 110 | 111 | ###### Parameters 112 | 113 | * `name` (`string`) 114 | — specifier 115 | * `options` ([`ResolveOptions`][api-resolve-options], optional) 116 | — configuration 117 | 118 | ###### Returns 119 | 120 | Promise to a file URL (`Promise`). 121 | 122 | ### `LoadOptions` 123 | 124 | Configuration for `loadPlugin` (TypeScript type). 125 | 126 | This type extends `ResolveOptions` and adds: 127 | 128 | ###### Fields 129 | 130 | * `key` (`boolean` or `string`, default: `'default'`) 131 | — identifier to take from the exports; 132 | for example when given `'x'`, 133 | the value of `export const x = 1` will be returned; 134 | when given `'default'`, 135 | the value of `export default …` is used, 136 | and when `false` the whole module object is returned 137 | 138 | ### `ResolveOptions` 139 | 140 | Configuration for `resolvePlugin` (TypeScript type). 141 | 142 | ###### Fields 143 | 144 | * `from` (`Array | URL | string`, optional) 145 | — place or places to search from; 146 | defaults to the current working directory 147 | * `global` (`boolean`, default: whether global is detected) 148 | — whether to look for `name` in [global places][npm-node-modules]; 149 | if this is nullish, 150 | `load-plugin` will detect if it’s currently running in global mode: either 151 | because it’s in Electron or because a globally installed package is running 152 | it; 153 | note that Electron runs its own version of Node instead of your system Node, 154 | meaning global packages cannot be found, 155 | unless you’ve set-up a [`prefix`][npm-prefix] in your `.npmrc` or are using 156 | [nvm][github-nvm] to manage your system node 157 | * `prefix` (`string`, optional) 158 | — prefix to search for 159 | 160 | ## Compatibility 161 | 162 | This projects is compatible with maintained versions of Node.js. 163 | 164 | When we cut a new major release, 165 | we drop support for unmaintained versions of Node. 166 | This means we try to keep the current release line, 167 | `load-plugin@6`, 168 | compatible with Node.js 16. 169 | 170 | ## Security 171 | 172 | This package reads the file system and imports things into Node.js. 173 | 174 | ## Contribute 175 | 176 | Yes please! 177 | See [How to Contribute to Open Source][open-source-guide-contribute]. 178 | 179 | ## License 180 | 181 | [MIT][file-license] © [Titus Wormer][wooorm] 182 | 183 | 184 | 185 | [api-load-plugin]: #loadpluginname-options 186 | 187 | [api-load-options]: #loadoptions 188 | 189 | [api-resolve-plugin]: #resolvepluginname-options 190 | 191 | [api-resolve-options]: #resolveoptions 192 | 193 | [badge-build-image]: https://github.com/wooorm/load-plugin/workflows/main/badge.svg 194 | 195 | [badge-build-url]: https://github.com/wooorm/load-plugin/actions 196 | 197 | [badge-coverage-image]: https://img.shields.io/codecov/c/github/wooorm/load-plugin.svg 198 | 199 | [badge-coverage-url]: https://codecov.io/github/wooorm/load-plugin 200 | 201 | [badge-downloads-image]: https://img.shields.io/npm/dm/load-plugin.svg 202 | 203 | [badge-downloads-url]: https://www.npmjs.com/package/load-plugin 204 | 205 | [file-license]: license 206 | 207 | [github-gist-esm]: https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c 208 | 209 | [github-import-meta-resolve]: https://github.com/wooorm/import-meta-resolve 210 | 211 | [github-nvm]: https://github.com/nvm-sh/nvm 212 | 213 | [nodejs-resolution-algo]: https://nodejs.org/api/esm.html#esm_resolution_algorithm 214 | 215 | [npm-install]: https://docs.npmjs.com/cli/install 216 | 217 | [npm-node-modules]: https://docs.npmjs.com/cli/v10/configuring-npm/folders#node-modules 218 | 219 | [npm-prefix]: https://docs.npmjs.com/cli/v10/using-npm/config 220 | 221 | [open-source-guide-contribute]: https://opensource.guide/how-to-contribute/ 222 | 223 | [typescript]: https://www.typescriptlang.org 224 | 225 | [wooorm]: https://wooorm.com 226 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @typedef {LoadOptionsExtraFields & ResolveOptions} LoadOptions 3 | * Configuration for `loadPlugin`. 4 | * 5 | * @typedef LoadOptionsExtraFields 6 | * Extra configuration for `loadPlugin`. 7 | * @property {boolean | string | null | undefined} [key] 8 | * Identifier to take from the exports (default: `'default'`); 9 | * for example when given `'x'`, 10 | * the value of `export const x = 1` will be returned; 11 | * when given `'default'`, 12 | * the value of `export default …` is used, 13 | * and when `false` the whole module object is returned. 14 | * 15 | * @typedef ResolveOptions 16 | * Configuration for `resolvePlugin`. 17 | * @property {ReadonlyArray | string> | Readonly | string | null | undefined} [from] 18 | * Place or places to search from (optional); 19 | * defaults to the current working directory. 20 | * @property {boolean | null | undefined} [global=boolean] 21 | * Whether to look for `name` in global places (default: whether global is 22 | * detected); 23 | * if this is nullish, 24 | * `load-plugin` will detect if it’s currently running in global mode: either 25 | * because it’s in Electron or because a globally installed package is 26 | * running it; 27 | * note that Electron runs its own version of Node instead of your system 28 | * Node, 29 | * meaning global packages cannot be found, 30 | * unless you’ve set-up a `prefix` in your `.npmrc` or are using nvm to 31 | * manage your system node. 32 | * @property {string | null | undefined} [prefix] 33 | * Prefix to search for (optional). 34 | */ 35 | 36 | import path from 'node:path' 37 | import process from 'node:process' 38 | import {pathToFileURL} from 'node:url' 39 | // @ts-expect-error: untyped 40 | import NpmConfig from '@npmcli/config' 41 | // @ts-expect-error: untyped 42 | import definitions from '@npmcli/config/lib/definitions/definitions.js' 43 | import {resolve} from 'import-meta-resolve' 44 | 45 | const electron = process.versions.electron !== undefined 46 | const nvm = process.env.NVM_BIN 47 | const windows = process.platform === 'win32' 48 | /* c8 ignore next -- windows */ 49 | const argv = process.argv[1] || '' 50 | /* c8 ignore next -- windows */ 51 | const nodeModules = windows ? 'node_modules' : 'lib/node_modules' 52 | 53 | /** @type {Readonly} */ 54 | const defaultLoadOptions = {} 55 | /** @type {Readonly} */ 56 | const defaultResolveOptions = {} 57 | 58 | /** @type {Promise | string | undefined} */ 59 | let npmPrefix 60 | 61 | /** 62 | * Import `name` from `from` (and optionally the global `node_modules` directory). 63 | * 64 | * Uses the Node.js resolution algorithm (through `import-meta-resolve`) to 65 | * resolve CJS and ESM packages and files. 66 | * 67 | * If a `prefix` is given and `name` is not a path, 68 | * `$prefix-$name` is also searched (preferring these over non-prefixed 69 | * modules). 70 | * If `name` starts with a scope (`@scope/name`), 71 | * the prefix is applied after it: `@scope/$prefix-name`. 72 | * 73 | * @param {string} name 74 | * Specifier. 75 | * @param {Readonly | null | undefined} [options] 76 | * Configuration (optional). 77 | * @returns {Promise} 78 | * Promise to a whole module or specific export. 79 | */ 80 | export async function loadPlugin(name, options) { 81 | const settings = options || defaultLoadOptions 82 | const href = await resolvePlugin(name, settings) 83 | const module = /** @type {Record} */ (await import(href)) 84 | return typeof settings.key === 'string' 85 | ? module[settings.key] 86 | : settings.key === false 87 | ? module 88 | : module.default 89 | } 90 | 91 | /** 92 | * Resolve `name` from `from`. 93 | * 94 | * @param {string} name 95 | * Specifier. 96 | * @param {Readonly | null | undefined} [options] 97 | * Configuration (optional). 98 | * @returns {Promise} 99 | * Promise to a file URL. 100 | */ 101 | export async function resolvePlugin(name, options) { 102 | const settings = options || defaultResolveOptions 103 | const prefix = settings.prefix 104 | ? settings.prefix + (settings.prefix.at(-1) === '-' ? '' : '-') 105 | : undefined 106 | const fromNonEmpty = settings.from || pathToFileURL(process.cwd() + '/') 107 | const from = /** @type {Array | string>} */ ( 108 | Array.isArray(fromNonEmpty) ? fromNonEmpty : [fromNonEmpty] 109 | ) 110 | 111 | if (!npmPrefix) npmPrefix = inferNpmPrefix() 112 | if (typeof npmPrefix !== 'string') npmPrefix = await npmPrefix 113 | 114 | const globals = 115 | typeof settings.global === 'boolean' 116 | ? settings.global 117 | : electron || argv.startsWith(npmPrefix) 118 | 119 | /** @type {string | undefined} */ 120 | let plugin 121 | /** @type {Error | undefined} */ 122 | let lastError 123 | 124 | // Bare specifier. 125 | if (name.charAt(0) !== '.') { 126 | if (globals) { 127 | from.push(new URL(nodeModules, pathToFileURL(npmPrefix) + '/')) 128 | 129 | // If we’re in Electron, 130 | // we’re running in a modified Node that cannot really install global node 131 | // modules. 132 | // To find the actual modules, 133 | // the user has to set `prefix` somewhere in an `.npmrc` (which is picked up 134 | // by `@npmcli/config`). 135 | // Most people don’t do that, 136 | // and some use NVM instead to manage different versions of Node. 137 | // Luckily NVM leaks some environment variables that we can pick up on to try 138 | // and detect the actual modules. 139 | /* c8 ignore next 3 -- Electron. */ 140 | if (electron && nvm) { 141 | from.push(new URL(nodeModules, pathToFileURL(nvm))) 142 | } 143 | } 144 | 145 | let scope = '' 146 | 147 | // Unprefix module. 148 | if (prefix) { 149 | // Scope? 150 | if (name.charAt(0) === '@') { 151 | const slash = name.indexOf('/') 152 | 153 | // Let’s keep the algorithm simple. 154 | // No need to care if this is a “valid” scope (I think?). 155 | // But we do check for the slash. 156 | if (slash !== -1) { 157 | scope = name.slice(0, slash + 1) 158 | name = name.slice(slash + 1) 159 | } 160 | } 161 | 162 | if (name.slice(0, prefix.length) !== prefix) { 163 | plugin = scope + prefix + name 164 | } 165 | 166 | name = scope + name 167 | } 168 | } 169 | 170 | let index = -1 171 | 172 | while (++index < from.length) { 173 | const source = from[index] 174 | const href = typeof source === 'string' ? source : source.href 175 | 176 | if (plugin) { 177 | try { 178 | return resolve(plugin, href) 179 | } catch (error) { 180 | lastError = /** @type {Error} */ (error) 181 | } 182 | } 183 | 184 | try { 185 | return resolve(name, href) 186 | } catch (error) { 187 | lastError = /** @type {Error} */ (error) 188 | } 189 | } 190 | 191 | throw lastError 192 | } 193 | 194 | /** 195 | * Find npm prefix. 196 | * 197 | * @returns {Promise} 198 | */ 199 | async function inferNpmPrefix() { 200 | const config = new NpmConfig({ 201 | argv: [], 202 | definitions, 203 | npmPath: '' 204 | }) 205 | 206 | await config.load() 207 | 208 | /* c8 ignore next 6 -- typically defined */ 209 | return ( 210 | config.globalPrefix || 211 | (windows 212 | ? path.dirname(process.execPath) 213 | : path.resolve(process.execPath, '../..')) 214 | ) 215 | } 216 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | import assert from 'node:assert/strict' 2 | import process from 'node:process' 3 | import test from 'node:test' 4 | import {loadPlugin, resolvePlugin} from 'load-plugin' 5 | import {regex} from 'github-slugger/regex.js' 6 | // Get the real one, not the fake one from our `test/node_modules/`. 7 | import remarkLint from '../node_modules/remark-lint/index.js' 8 | 9 | test('core', async function (t) { 10 | await t.test('should expose the public api', async function () { 11 | assert.deepEqual(Object.keys(await import('load-plugin')).sort(), [ 12 | 'loadPlugin', 13 | 'resolvePlugin' 14 | ]) 15 | }) 16 | }) 17 | 18 | test('loadPlugin', async function (t) { 19 | await t.test('should fail w/o argument', async function () { 20 | try { 21 | // @ts-expect-error: check how the runtime handles a missing specifier. 22 | await loadPlugin() 23 | assert.fail() 24 | } catch (error) { 25 | assert.match(String(error), /Cannot read properties of undefined/) 26 | } 27 | }) 28 | 29 | await t.test( 30 | 'should look for `$cwd/node_modules/$prefix-$name`', 31 | async function () { 32 | assert.equal( 33 | await loadPlugin('delta', {from: import.meta.url, prefix: 'charlie'}), 34 | 'echo' 35 | ) 36 | } 37 | ) 38 | 39 | await t.test( 40 | 'should look for `$cwd/node_modules/$prefix-$name$rest` where $rest is a path', 41 | async function () { 42 | assert.equal( 43 | await loadPlugin('delta/another.js', { 44 | from: import.meta.url, 45 | prefix: 'charlie' 46 | }), 47 | 'another' 48 | ) 49 | } 50 | ) 51 | 52 | await t.test( 53 | 'should look for `$cwd/node_modules/$prefix-$name$rest` where $rest is a path', 54 | async function () { 55 | assert.equal( 56 | await loadPlugin('delta/another.js', { 57 | from: import.meta.url, 58 | prefix: 'charlie' 59 | }), 60 | 'another' 61 | ) 62 | } 63 | ) 64 | 65 | await t.test( 66 | 'should look for `$cwd/node_modules/$scope/$prefix-$name` if a scope is given', 67 | async function () { 68 | assert.equal( 69 | await loadPlugin('@foxtrot/hotel', { 70 | from: import.meta.url, 71 | prefix: 'golf' 72 | }), 73 | 'india' 74 | ) 75 | } 76 | ) 77 | 78 | await t.test( 79 | 'should look for `$cwd/node_modules/$scope/$prefix-$name$rest` if a scope is given and $rest is a path', 80 | async function () { 81 | assert.equal( 82 | await loadPlugin('@foxtrot/hotel/other.js', { 83 | from: import.meta.url, 84 | prefix: 'golf' 85 | }), 86 | 'other' 87 | ) 88 | } 89 | ) 90 | 91 | await t.test( 92 | 'should look for `$root/node_modules/$prefix-$name`', 93 | async function () { 94 | assert.equal(await loadPlugin('lint', {prefix: 'remark'}), remarkLint) 95 | } 96 | ) 97 | 98 | await t.test( 99 | 'should look for `$root/node_modules/$prefix-$name$rest` where $rest is a path', 100 | async function () { 101 | assert.deepEqual( 102 | await loadPlugin('slugger/regex.js', { 103 | key: 'regex', 104 | prefix: 'github' 105 | }), 106 | regex 107 | ) 108 | } 109 | ) 110 | 111 | await t.test('should throw if a path cannot be found', async function () { 112 | try { 113 | await loadPlugin('lint/index', {prefix: 'remark'}) 114 | assert.fail() 115 | } catch (error) { 116 | assert.match(String(error), /Cannot find/) 117 | } 118 | }) 119 | 120 | await t.test( 121 | 'should not duplicate `$root/node_modules/$prefix-$prefix-$name`', 122 | async function () { 123 | assert.equal( 124 | await loadPlugin('remark-lint', {prefix: 'remark'}), 125 | remarkLint 126 | ) 127 | } 128 | ) 129 | 130 | await t.test( 131 | 'should not duplicate `$cwd/node_modules/$scope/$prefix-$prefix-$name` if a scope is given', 132 | async function () { 133 | assert.equal( 134 | await loadPlugin('@foxtrot/golf-hotel', { 135 | from: import.meta.url, 136 | prefix: 'golf' 137 | }), 138 | 'india' 139 | ) 140 | } 141 | ) 142 | 143 | await t.test('should support a dash in `$prefix`', async function () { 144 | assert.equal(await loadPlugin('lint', {prefix: 'remark-'}), remarkLint) 145 | }) 146 | 147 | // Global: `$modules/$plugin` is untestable. 148 | 149 | await t.test('should look for `./index.js`', async function () { 150 | assert.equal( 151 | await loadPlugin('./index.js', {key: 'loadPlugin'}), 152 | loadPlugin 153 | ) 154 | }) 155 | 156 | await t.test( 157 | 'should throw if passing a path w/o extension', 158 | async function () { 159 | try { 160 | await loadPlugin('./index', {key: 'loadPlugin'}) 161 | assert.fail() 162 | } catch (error) { 163 | assert.match(String(error), /Cannot find module/) 164 | } 165 | } 166 | ) 167 | 168 | await t.test( 169 | 'should throw if passing a path to a directory', 170 | async function () { 171 | try { 172 | await loadPlugin('./', {key: 'loadPlugin'}) 173 | assert.fail() 174 | } catch (error) { 175 | assert.match(String(error), /Directory import/) 176 | } 177 | } 178 | ) 179 | 180 | await t.test('should support `key: false`', async function () { 181 | const main = /** @type {object} */ ( 182 | await loadPlugin('./index.js', {key: false}) 183 | ) 184 | 185 | assert.deepEqual(Object.keys(main), ['loadPlugin', 'resolvePlugin']) 186 | }) 187 | 188 | await t.test('should look for `$root/node_modules/$name`', async function () { 189 | assert.equal( 190 | typeof (await loadPlugin('micromark', {key: 'micromark'})), 191 | 'function' 192 | ) 193 | }) 194 | 195 | await t.test( 196 | 'should look for `$root/node_modules/$name$rest` where $rest is a path', 197 | async function () { 198 | assert.equal( 199 | typeof (await loadPlugin('micromark/stream', {key: 'stream'})), 200 | 'function' 201 | ) 202 | } 203 | ) 204 | 205 | await t.test('should look for `$cwd/node_modules/$name`', async function () { 206 | assert.equal(await loadPlugin('alpha', {from: import.meta.url}), 'bravo') 207 | }) 208 | 209 | await t.test( 210 | 'should look for `$cwd/node_modules/$name$rest` where $rest is a path', 211 | async function () { 212 | assert.equal( 213 | await loadPlugin('alpha/other.js', {from: import.meta.url}), 214 | 'other' 215 | ) 216 | } 217 | ) 218 | 219 | await t.test( 220 | 'should throw for `$cwd/node_modules/$name$rest` where $rest is a path without extension', 221 | async function () { 222 | try { 223 | await loadPlugin('alpha/other', {from: import.meta.url}) 224 | assert.fail() 225 | } catch (error) { 226 | assert.match(String(error), /Cannot find module/) 227 | } 228 | } 229 | ) 230 | 231 | await t.test('should support a list of `cwd`s (1)', async function () { 232 | assert.equal( 233 | await loadPlugin('lint', { 234 | from: [import.meta.url, new URL('../', import.meta.url)], 235 | prefix: 'remark' 236 | }), 237 | 'echo' 238 | ) 239 | }) 240 | 241 | await t.test('should support a list of `cwd`s (2)', async function () { 242 | assert.equal( 243 | await loadPlugin('lint', { 244 | from: [process.cwd(), new URL('../', import.meta.url)], 245 | prefix: 'remark' 246 | }), 247 | remarkLint 248 | ) 249 | }) 250 | 251 | // Global: `$modules/$plugin` is untestable 252 | 253 | await t.test('should throw if a path cannot be found', async function () { 254 | try { 255 | await loadPlugin('does not exist', {global: true, prefix: 'this'}) 256 | assert.fail() 257 | } catch (error) { 258 | assert.match(String(error), /Cannot find package 'does not exist'/) 259 | } 260 | }) 261 | 262 | await t.test('should throw for just a scope', async function () { 263 | try { 264 | await loadPlugin('@foxtrot', {from: import.meta.url, prefix: 'foo'}) 265 | assert.fail() 266 | } catch (error) { 267 | assert.match( 268 | String(error), 269 | /Invalid module "@foxtrot" is not a valid package name/ 270 | ) 271 | } 272 | }) 273 | 274 | await t.test( 275 | 'should support loading global packages (npm)', 276 | async function () { 277 | // If this fails, you need to make sure to install `f-ck` globally. 278 | // That’s done by CI; but when running tests locally needs to be done manually. 279 | const vowel = await loadPlugin('f-ck', {global: true, key: 'vowel'}) 280 | assert.equal(typeof vowel, 'function') 281 | } 282 | ) 283 | }) 284 | 285 | test('resolvePlugin', async function (t) { 286 | await t.test('should look for `$cwd/node_modules/$name`', async function () { 287 | assert.equal( 288 | await resolvePlugin('alpha', {from: import.meta.url}), 289 | new URL('node_modules/alpha/index.js', import.meta.url).href 290 | ) 291 | }) 292 | 293 | await t.test('should throw for just a scope', async function () { 294 | try { 295 | await resolvePlugin('does not exist') 296 | assert.fail() 297 | } catch (error) { 298 | assert.match(String(error), /Cannot find package/) 299 | } 300 | }) 301 | }) 302 | --------------------------------------------------------------------------------