├── test ├── fixtures │ ├── 02_workspaces │ │ ├── pnpm │ │ │ ├── pnpm-workspace.yaml │ │ │ ├── package.json │ │ │ ├── one │ │ │ │ └── package.json │ │ │ └── two │ │ │ │ └── package.json │ │ ├── lerna │ │ │ ├── lerna.json │ │ │ ├── package.json │ │ │ ├── one │ │ │ │ └── package.json │ │ │ └── two │ │ │ │ └── package.json │ │ ├── package.json │ │ └── npm-and-yarn │ │ │ ├── one │ │ │ └── package.json │ │ │ ├── two │ │ │ └── package.json │ │ │ └── package.json │ ├── 01_monorepo │ │ ├── package.json │ │ ├── one │ │ │ └── package.json │ │ └── two │ │ │ └── package.json │ ├── 00_simple │ │ └── package.json │ ├── 04_dual │ │ └── package.json │ └── package.json ├── tsconfig.json ├── _common.ts ├── workspaces.test.ts ├── monorepo.test.ts ├── options.test.ts ├── builtins.test.ts └── specifier.test.ts ├── .gitignore ├── .vscode ├── settings.json └── tasks.json ├── tsconfig.build.json ├── .github ├── FUNDING.yml └── workflows │ └── stale.yml ├── tsconfig.json ├── LICENSE ├── eslint.config.mjs ├── package.json ├── source └── index.ts └── README.md /test/fixtures/02_workspaces/pnpm/pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | - one 2 | - two 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | out/ 4 | zz* 5 | /.np-config.json 6 | /*.tgz 7 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib", 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/01_monorepo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "chalk": "*" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /test/fixtures/01_monorepo/one/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "moment": "*" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /test/fixtures/01_monorepo/two/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "react": "*" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /test/fixtures/02_workspaces/lerna/lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "packages": [ 3 | "one", "two " 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /test/fixtures/02_workspaces/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "rollup": "*" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /test/fixtures/02_workspaces/lerna/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "chalk": "*" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /test/fixtures/02_workspaces/pnpm/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "chalk": "*" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /test/fixtures/02_workspaces/lerna/one/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "moment": "*" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /test/fixtures/02_workspaces/lerna/two/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "react": "*" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /test/fixtures/02_workspaces/pnpm/one/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "moment": "*" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /test/fixtures/02_workspaces/pnpm/two/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "react": "*" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /test/fixtures/02_workspaces/npm-and-yarn/one/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "moment": "*" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /test/fixtures/02_workspaces/npm-and-yarn/two/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "react": "*" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /test/fixtures/00_simple/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "simple-dep": "*" 4 | }, 5 | "devDependencies": { 6 | "simple-dev-dep": "*" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /test/fixtures/02_workspaces/npm-and-yarn/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "chalk": "*" 4 | }, 5 | "workspaces": [ 6 | "./one", "./two" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /test/fixtures/04_dual/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "//": "Purposely marked both as a normal and a dev dependency", 3 | "dependencies": { 4 | "dual-dep": "*" 5 | }, 6 | "devDependencies": { 7 | "dual-dep": "*" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "include": [ 4 | "source" 5 | ], 6 | "compilerOptions": { 7 | "rootDir": "source", 8 | "outDir": "dist", 9 | "noEmit": false, 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "include": [ 4 | "./_common.ts", 5 | "./*.test.ts" 6 | ], 7 | "compilerOptions": { 8 | "allowImportingTsExtensions": true, 9 | "noEmit": true, 10 | "isolatedModules": true 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /test/fixtures/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "test-dep": "*" 4 | }, 5 | "devDependencies": { 6 | "test-dev-dep": "*" 7 | }, 8 | "peerDependencies": { 9 | "test-peer-dep": "*" 10 | }, 11 | "optionalDependencies": { 12 | "test-opt-dep": "*" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: Septh 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 13 | custom: [https://paypal.me/septh07] 14 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "npm: lint", 6 | "type": "npm", 7 | "script": "lint", 8 | "detail": "eslint src/index.ts", 9 | "problemMatcher": "$eslint-stylish" 10 | }, 11 | { 12 | "label": "tsc: watch - tsconfig.build.json", 13 | "group": { 14 | "kind": "build", 15 | "isDefault": true 16 | }, 17 | "type": "typescript", 18 | "tsconfig": "tsconfig.build.json", 19 | "option": "watch", 20 | "problemMatcher": [ 21 | "$tsc-watch" 22 | ], 23 | "presentation": { 24 | "echo": true, 25 | "reveal": "silent", 26 | }, 27 | "runOptions": { 28 | "runOn": "folderOpen" 29 | } 30 | } 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "source", 4 | "eslint.config.mjs" 5 | ], 6 | "compilerOptions": { 7 | 8 | // Node stuff 9 | "module": "Node16", 10 | "allowSyntheticDefaultImports": true, 11 | 12 | // Input 13 | "resolveJsonModule": true, 14 | "checkJs": true, 15 | 16 | // Output 17 | "target": "ESNext", 18 | "sourceMap": true, 19 | "removeComments": true, 20 | "noEmit": true, 21 | 22 | // Lib 23 | "lib": [ "ESNext" ], 24 | "skipLibCheck": true, 25 | "skipDefaultLibCheck": true, 26 | 27 | // Type checking 28 | "strict": true, 29 | "noImplicitReturns": true, 30 | "noUnusedLocals": false, 31 | "noUnusedParameters": false, 32 | "noPropertyAccessFromIndexSignature": true, 33 | "noUncheckedIndexedAccess": false, 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Stephan Schreiber 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 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | # This workflow warns and then closes issues and PRs that have had no activity for a specified amount of time. 2 | # 3 | # You can adjust the behavior by modifying this file. 4 | # For more information, see: 5 | # https://github.com/actions/stale 6 | name: Mark stale issues and pull requests 7 | 8 | on: 9 | schedule: 10 | - cron: '30 21 * * *' 11 | 12 | jobs: 13 | stale: 14 | 15 | runs-on: ubuntu-latest 16 | permissions: 17 | issues: write 18 | pull-requests: write 19 | 20 | steps: 21 | - uses: actions/stale@v9 22 | with: 23 | repo-token: ${{ secrets.GITHUB_TOKEN }} 24 | days-before-stale: 60 25 | days-before-close: 7 26 | stale-issue-message: > 27 | This issue has been automatically marked as stale because it has not had 28 | recent activity. It will be closed if no further activity occurs. Thank you 29 | for your contributions. 30 | exempt-issue-labels: pinned,security 31 | exempt-pr-labels: pinned,security 32 | stale-pr-message: > 33 | This PR has been automatically marked as stale because it has not had 34 | recent activity. It will be closed if no further activity occurs. Thank you 35 | for your contributions. 36 | remove-stale-when-updated: false 37 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import typescriptEslint from "@typescript-eslint/eslint-plugin" 2 | import globals from "globals" 3 | import tsParser from "@typescript-eslint/parser" 4 | import path from "node:path" 5 | import { fileURLToPath } from "node:url" 6 | import js from "@eslint/js" 7 | import { FlatCompat } from "@eslint/eslintrc" 8 | 9 | const __filename = fileURLToPath(import.meta.url) 10 | const __dirname = path.dirname(__filename) 11 | const compat = new FlatCompat({ 12 | baseDirectory: __dirname, 13 | recommendedConfig: js.configs.recommended, 14 | allConfig: js.configs.all 15 | }) 16 | 17 | export default [ 18 | { 19 | ignores: ["node_modules", "dist", "**/zz_*"], 20 | }, 21 | ...compat.extends("eslint:recommended", "plugin:@typescript-eslint/recommended"), 22 | { 23 | plugins: { 24 | "@typescript-eslint": typescriptEslint, 25 | }, 26 | languageOptions: { 27 | globals: { 28 | ...globals.node, 29 | }, 30 | 31 | parser: tsParser, 32 | }, 33 | rules: { 34 | "no-debugger": "off", 35 | "no-unused-vars": "off", 36 | "prefer-const": "warn", 37 | "no-inner-declarations": "off", 38 | "@typescript-eslint/no-unused-vars": "off", 39 | } 40 | } 41 | ] 42 | -------------------------------------------------------------------------------- /test/_common.ts: -------------------------------------------------------------------------------- 1 | import path from 'node:path' 2 | import { fileURLToPath } from 'node:url' 3 | import { 4 | VERSION, 5 | type Plugin, type RollupError, type PluginContextMeta, type NormalizedInputOptions 6 | } from 'rollup' 7 | import { nodeExternals, type ExternalsOptions } from '../source/index.ts' 8 | 9 | const __dirname = path.dirname(fileURLToPath(import.meta.url)) 10 | 11 | class MockPluginContext { 12 | private readonly plugin: Plugin 13 | readonly warnings: string[] 14 | readonly meta: PluginContextMeta 15 | 16 | constructor(plugin: Plugin) { 17 | this.plugin = plugin 18 | this.warnings = [] 19 | this.meta = { 20 | rollupVersion: VERSION, 21 | watchMode: false 22 | } 23 | } 24 | 25 | async buildStart() { 26 | let { buildStart: hook } = this.plugin 27 | if (typeof hook === 'object') 28 | hook = hook.handler 29 | if (typeof hook === 'function') 30 | return await hook.call(this as any, {} as NormalizedInputOptions) 31 | throw new Error('Oops') 32 | } 33 | 34 | async resolveId(specifier: string, importer?: string | undefined) { 35 | let { resolveId: hook } = this.plugin 36 | if (typeof hook === 'object') 37 | hook = hook.handler 38 | if (typeof hook === 'function') 39 | return await hook.call(this as any, specifier, importer, { attributes: {}, isEntry: typeof importer === 'string' ? false : true }) 40 | throw new Error('Oops') 41 | } 42 | 43 | error(err: string | RollupError): never { 44 | const message: string = typeof err === 'string' 45 | ? err 46 | : err.message 47 | throw new Error(message) 48 | } 49 | 50 | warn(message: string): void { 51 | this.warnings.push(message) 52 | } 53 | 54 | addWatchFile(_file: string) { 55 | // nop 56 | } 57 | } 58 | 59 | export async function initPlugin(options: ExternalsOptions = {}) { 60 | const plugin = await nodeExternals(options) 61 | const context = new MockPluginContext(plugin) 62 | await context.buildStart() 63 | return context 64 | } 65 | 66 | export function fixture(...parts: string[]) { 67 | return path.join(__dirname, 'fixtures', ...parts) 68 | } 69 | -------------------------------------------------------------------------------- /test/workspaces.test.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | import { initPlugin, fixture } from './_common.ts' 3 | 4 | test('npm/yarn workspaces usage', async t => { 5 | process.chdir(fixture('02_workspaces/npm-and-yarn/one')) 6 | const context = await initPlugin() 7 | 8 | // Should be external 9 | for (const dependency of [ 10 | 'moment', // 02_workspaces/npm-and-yarn/one/package.json 11 | 'chalk' // 02_workspaces/npm-and-yarn/package.json 12 | ]) { 13 | t.false(await context.resolveId(dependency, 'index.js')) 14 | } 15 | 16 | // Should be ignored 17 | for (const dependency of [ 18 | 'react', // 02_workspaces/npm-and-yarn/two/package.json 19 | 'rollup', // 02_workspaces/package.json 20 | 'test-dep' // ./package.json 21 | ]) { 22 | t.is(await context.resolveId(dependency, 'index.js'), null) 23 | } 24 | }) 25 | 26 | test('pnpm workspaces usage', async t => { 27 | process.chdir(fixture('02_workspaces/pnpm/one')) 28 | const context = await initPlugin() 29 | 30 | // Should be external 31 | for (const dependency of [ 32 | 'moment', // 02_workspaces/pnpm/one/package.json 33 | 'chalk' // 02_workspaces/pnpm/package.json 34 | ]) { 35 | t.false(await context.resolveId(dependency, 'index.js')) 36 | } 37 | 38 | // Should be ignored 39 | for (const dependency of [ 40 | 'react', // 02_workspaces/pnpm/two/package.json 41 | 'rollup', // 02_workspaces/package.json 42 | 'test-dep' // ./package.json 43 | ]) { 44 | t.is(await context.resolveId(dependency, 'index.js'), null) 45 | } 46 | }) 47 | 48 | test('lerna usage', async t => { 49 | process.chdir(fixture('02_workspaces/lerna/one')) 50 | const plugin = await initPlugin() 51 | 52 | // Should be external 53 | for (const dependency of [ 54 | 'moment', // 02_workspaces/lerna/one/package.json 55 | 'chalk' // 02_workspaces/lerna/package.json 56 | ]) { 57 | t.false(await plugin.resolveId(dependency, 'index.js')) 58 | } 59 | 60 | // Should be ignored 61 | for (const dependency of [ 62 | 'react', // 02_workspaces/lerna/two/package.json 63 | 'rollup', // 02_workspaces/package.json 64 | 'test-dep' // ./package.json 65 | ]) { 66 | t.is(await plugin.resolveId(dependency, 'index.js'), null) 67 | } 68 | }) 69 | -------------------------------------------------------------------------------- /test/monorepo.test.ts: -------------------------------------------------------------------------------- 1 | import { promisify } from 'node:util' 2 | import cp from 'node:child_process' 3 | import fs from 'node:fs/promises' 4 | import test from 'ava' 5 | import { initPlugin, fixture } from './_common.ts' 6 | 7 | // The two tests in this file need to be run in sequence so one does not interfere with the other 8 | 9 | test.serial('git monorepo usage', async t => { 10 | 11 | t.log('Creating temporary git repo...') 12 | process.chdir(fixture('01_monorepo')) 13 | const execFile = promisify(cp.execFile) 14 | await execFile('git', [ 'init' ] ).catch(() => {}) 15 | 16 | t.teardown(async () => { 17 | t.log('Removing temporary git repo...') 18 | process.chdir(fixture('01_monorepo')) 19 | await fs.rm('.git', { recursive: true, force: true }) 20 | }) 21 | 22 | // Should gather dependencies up to ./test/fixtures/01_monorepo 23 | process.chdir(fixture('01_monorepo/one')) 24 | const context = await initPlugin() 25 | 26 | // Should be external 27 | for (const dependency of [ 28 | 'moment', // dependency in ./test/fixtures/01_monorepo/one/package.json (picked) 29 | 'chalk' // dependency in ./test/fixtures/01_monorepo/package.json (picked) 30 | ]) { 31 | t.false(await context.resolveId(dependency, 'index.js')) 32 | } 33 | 34 | // Should be ignored 35 | for (const dependency of [ 36 | 'react', // dependency in ./test/fixtures/01_monorepo/two/package.json (not picked) 37 | 'test-dep' // dependency in ./test/fixtures/package.json (not picked) 38 | ]) { 39 | t.is(await context.resolveId(dependency, 'index.js'), null) 40 | } 41 | }) 42 | 43 | test.serial('non-git monorepo usage', async t => { 44 | 45 | // Should gather dependencies up to / ! 46 | process.chdir(fixture('01_monorepo/one')) 47 | const context = await initPlugin() 48 | 49 | // Should be external 50 | for (const dependency of [ 51 | 'moment', // dependency in ./test/fixtures/01_monorepo/one/package.json (picked) 52 | 'chalk', // dependency in ./test/fixtures/01_monorepo/package.json (picked) 53 | 'test-dep', // dependency in ./test/fixtures/package.json (picked) 54 | 'rollup', // peer dependency in ./package.json (picked !) 55 | ]) { 56 | t.false(await context.resolveId(dependency, 'index.js')) 57 | } 58 | 59 | // Should be ignored 60 | for (const dependency of [ 61 | 'react' // dependency in ./test/fixtures/01_monorepo/two/package.json (not picked) 62 | ]) { 63 | t.is(await context.resolveId(dependency, 'index.js'), null) 64 | } 65 | }) 66 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rollup-plugin-node-externals", 3 | "version": "8.1.2", 4 | "description": "Automatically declare NodeJS built-in modules and npm dependencies as 'external' in Rollup/Vite config", 5 | "author": "Stephan Schreiber ", 6 | "contributors": [ 7 | "Tomer Aberbach ", 8 | "Elad Ossadon " 9 | ], 10 | "keywords": [ 11 | "rollup", 12 | "vite", 13 | "plugin", 14 | "rollup-plugin", 15 | "vite-plugin", 16 | "external", 17 | "externals", 18 | "node", 19 | "builtin", 20 | "builtins", 21 | "dependencies", 22 | "devDependencies", 23 | "peerDependencies", 24 | "optionalDependencies", 25 | "modules", 26 | "monorepo" 27 | ], 28 | "homepage": "https://github.com/Septh/rollup-plugin-node-externals#readme", 29 | "repository": { 30 | "type": "git", 31 | "url": "https://github.com/Septh/rollup-plugin-node-externals" 32 | }, 33 | "funding": [ 34 | { 35 | "type": "patreon", 36 | "url": "https://patreon.com/Septh" 37 | }, 38 | { 39 | "type": "paypal", 40 | "url": "https://paypal.me/septh07" 41 | } 42 | ], 43 | "license": "MIT", 44 | "type": "module", 45 | "engines": { 46 | "node": ">= 21 || ^20.6.0 || ^18.19.0" 47 | }, 48 | "files": [ 49 | "dist", 50 | "!dist/**/*.map" 51 | ], 52 | "types": "./dist/index.d.ts", 53 | "exports": "./dist/index.js", 54 | "imports": { 55 | "#package.json": "./package.json" 56 | }, 57 | "scripts": { 58 | "build": "tsc -p tsconfig.build.json", 59 | "build:dts": "tsc -p tsconfig.build.json --declaration --emitDeclarationOnly --removeComments false", 60 | "watch": "tsc -p tsconfig.build.json -w", 61 | "test": "ava", 62 | "lint": "eslint source/index.ts", 63 | "clean": "tsc -b tsconfig.build.json --declaration --clean", 64 | "prepublishOnly": "npm run lint && npm run clean && npm run build && npm run build:dts" 65 | }, 66 | "devDependencies": { 67 | "@eslint/eslintrc": "^3.2.0", 68 | "@eslint/js": "^9.17.0", 69 | "@fast-check/ava": "^2.0.1", 70 | "@septh/ts-run": "^2.0.0", 71 | "@types/node": "^20.17.10", 72 | "@typescript-eslint/eslint-plugin": "^8.18.0", 73 | "@typescript-eslint/parser": "^8.18.0", 74 | "ava": "^6.2.0", 75 | "eslint": "^9.17.0", 76 | "fast-check": "^4.2.0", 77 | "rollup": "^4.28.1", 78 | "tslib": "^2.8.1", 79 | "typescript": "^5.7.2" 80 | }, 81 | "peerDependencies": { 82 | "rollup": "^4.0.0" 83 | }, 84 | "ava": { 85 | "workerThreads": false, 86 | "extensions": { 87 | "ts": "module" 88 | }, 89 | "nodeArguments": [ 90 | "--import=@septh/ts-run" 91 | ] 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /test/options.test.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | import { testProp, fc } from '@fast-check/ava' 3 | import type { Arbitrary } from 'fast-check' 4 | import { initPlugin, fixture } from './_common.ts' 5 | import { type ExternalsOptions } from '../source/index.ts' 6 | 7 | // Ensures tests use local package.json 8 | process.chdir(fixture()) 9 | 10 | // Returns an arbitrary for generating externals options objects 11 | const externalsOptionsArbitrary = (): Arbitrary => fc.record({ 12 | packagePath: fc.string(), 13 | builtins: fc.boolean(), 14 | builtinsPrefix: fc.oneof(fc.constant<'strip'>('strip'), fc.constant<'add'>('add'), fc.constant<'ignore'>('ignore')), 15 | deps: fc.boolean(), 16 | devDeps: fc.boolean(), 17 | peerDeps: fc.boolean(), 18 | optDeps: fc.boolean(), 19 | include: fc.oneof(fc.string(), fc.array(fc.string())), 20 | exclude: fc.oneof(fc.string(), fc.array(fc.string())), 21 | }, { requiredKeys: [] }) 22 | 23 | testProp( 24 | 'Does not throw on constructing plugin object for valid input', 25 | [externalsOptionsArbitrary()], 26 | async (t, options) => { 27 | try { 28 | await initPlugin(options) 29 | t.pass() 30 | } 31 | catch (err) { 32 | const { message } = err as Error 33 | message.startsWith('Cannot read') ? t.pass() : t.fail(message) 34 | } 35 | } 36 | ) 37 | 38 | test("Warns when given invalid include or exclude entry", async t => { 39 | const okay = 'some_dep' // string is ok 40 | const notOkay = 1 // number is not (unless 0, which is falsy) 41 | 42 | const context = await initPlugin({ 43 | include: [ okay, notOkay as any ], 44 | exclude: [ okay, notOkay as any ], 45 | }) 46 | 47 | t.is(context.warnings.length, 2) 48 | t.is(context.warnings[0], `Ignoring wrong entry type #1 in 'include' option: ${JSON.stringify(notOkay)}`) 49 | t.is(context.warnings[1], `Ignoring wrong entry type #1 in 'exclude' option: ${JSON.stringify(notOkay)}`) 50 | }) 51 | 52 | test("Obeys 'packagePath' option (single file name)", async t => { 53 | const context = await initPlugin({ 54 | packagePath: '00_simple/package.json' 55 | }) 56 | t.false(await context.resolveId('simple-dep', 'index.js')) 57 | }) 58 | 59 | test("Obeys 'packagePath' option (multiple file names)", async t => { 60 | const context = await initPlugin({ 61 | packagePath: [ 62 | '00_simple/package.json', 63 | '01_monorepo/package.json' 64 | ] 65 | }) 66 | 67 | // Should be external 68 | for (const dependency of [ 69 | 'simple-dep', // 00_simple/package.json 70 | 'chalk', // 01_monorepo/package.json 71 | ]) { 72 | t.false(await context.resolveId(dependency, 'index.js')) 73 | } 74 | }) 75 | -------------------------------------------------------------------------------- /test/builtins.test.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | import { initPlugin } from './_common.ts' 3 | 4 | test("Marks Node builtins external by default", async t => { 5 | const context = await initPlugin() 6 | for (const builtin of [ 'path', 'node:fs' ]) { 7 | t.like(await context.resolveId(builtin, 'index.js'), { 8 | external: true 9 | }) 10 | } 11 | }) 12 | 13 | test("Does NOT mark Node builtins external when builtins=false", async t => { 14 | const context = await initPlugin({ builtins: false }) 15 | for (const builtin of [ 'path', 'node:fs' ]) { 16 | t.like(await context.resolveId(builtin, 'index.js'), { 17 | external: false 18 | }) 19 | } 20 | }) 21 | 22 | test("Does NOT mark Node builtins external when implicitly excluded", async t => { 23 | const context = await initPlugin({ exclude: [ 'path', 'node:fs' ]}) 24 | for (const builtin of [ 'path', 'node:fs' ]) { 25 | t.like(await context.resolveId(builtin, 'index.js'), { 26 | external: false 27 | }) 28 | } 29 | }) 30 | 31 | test("Marks Node builtins external when builtins=false and implicitly included", async t => { 32 | const context = await initPlugin({ builtins: false, include: [ 'path', 'node:fs' ] }) 33 | for (const builtin of [ 'path', 'node:fs' ]) { 34 | t.like(await context.resolveId(builtin, 'index.js'), { 35 | external: true 36 | }) 37 | } 38 | }) 39 | 40 | test("Adds 'node:' prefix to builtins by default", async t => { 41 | const context = await initPlugin() 42 | for (const builtin of [ 'node:path', 'path' ]) { 43 | t.like(await context.resolveId(builtin, 'index.js'), { 44 | id: 'node:path' 45 | }) 46 | } 47 | }) 48 | 49 | test("Removes 'node:' prefix when using builtinsPrefix='strip'", async t => { 50 | const context = await initPlugin({ builtinsPrefix: 'strip' }) 51 | for (const builtin of [ 'node:path', 'path' ]) { 52 | t.like(await context.resolveId(builtin, 'index.js'), { 53 | id: 'path' 54 | }) 55 | } 56 | }) 57 | 58 | test("Does NOT remove 'node:test' prefix even with builtinsPrefix='strip'", async t => { 59 | const context = await initPlugin({ builtinsPrefix: 'strip' }) 60 | for (const builtin of [ 'node:test' ]) { 61 | t.like(await context.resolveId(builtin, 'index.js'), { 62 | id: builtin 63 | }) 64 | } 65 | }) 66 | 67 | test("Does not recognize 'test' as a Node builtin", async t => { 68 | const context = await initPlugin() 69 | t.is(await context.resolveId('node', 'index.js'), null) 70 | }) 71 | 72 | test("Ignores 'node:' prefix when using builtinsPrefix='ignore'", async t => { 73 | const context = await initPlugin({ builtinsPrefix: 'ignore' }) 74 | for (const builtin of [ 'node:path', 'path' ]) { 75 | t.like(await context.resolveId(builtin, 'index.js'), { 76 | id: builtin 77 | }) 78 | } 79 | }) 80 | -------------------------------------------------------------------------------- /test/specifier.test.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | import { initPlugin, fixture } from './_common.ts' 3 | 4 | const specifiers = { 5 | virtual: [ '\\0virtual' ], 6 | absolutePosix: [ '/root.js' ], 7 | absoluteWin32: [ '/root.js', '\\root.js', 'C:\\root.js' ], 8 | bare: [ 'foo', 'bar' ], 9 | relative: [ './sibling.js', '../parent.js' ], 10 | subpath: [ 'lodash', 'lodash/flatten' ], 11 | } 12 | 13 | // Ensures tests use local package.json 14 | process.chdir(fixture()) 15 | 16 | test("Always ignores bundle entry point", async t => { 17 | const context = await initPlugin() 18 | t.is(await context.resolveId('./path/to/entry.js'), null) 19 | }) 20 | 21 | test("Always ignores virtual modules from other plugins", async t => { 22 | const context = await initPlugin() 23 | t.is(await context.resolveId(specifiers.virtual[0], undefined), null, `Failed without importer`) 24 | t.is(await context.resolveId(specifiers.virtual[0], 'file.js'), null, `Failed with importer`) 25 | }) 26 | 27 | test("Always ignores absolute specifiers", async t => { 28 | const context = await initPlugin() 29 | for (const specifier of (process.platform === 'win32' ? specifiers.absoluteWin32 : specifiers.absolutePosix)) { 30 | t.is(await context.resolveId(specifier, undefined), null, `Failed on: ${specifier} without importer`) 31 | t.is(await context.resolveId(specifier, 'file.js'), null, `Failed on: ${specifier} with importer`) 32 | } 33 | }) 34 | 35 | test("Always ignores relative specifiers", async t => { 36 | const context = await initPlugin({ include: specifiers.relative }) 37 | for (const specifier of specifiers.relative) { 38 | t.is(await context.resolveId(specifier, undefined), null, `Failed on: ${specifier} without importer`) 39 | t.is(await context.resolveId(specifier, 'file.js'), null, `Failed on: ${specifier} with importer`) 40 | } 41 | }) 42 | 43 | test("Always ignores bare specifiers that are not dependencies", async t => { 44 | const context = await initPlugin({ deps: true, peerDeps: true, optDeps: true, devDeps: true }) 45 | t.is(await context.resolveId('not-a-dep', 'index.js'), null) 46 | }) 47 | 48 | test("Marks dependencies external by default", async t => { 49 | const context = await initPlugin() 50 | t.is(await context.resolveId('test-dep', 'index.js'), false) 51 | }) 52 | 53 | test("Does NOT mark dependencies external when deps=false", async t => { 54 | const context = await initPlugin({ deps: false }) 55 | t.is(await context.resolveId('test-dep', 'index.js'), null) 56 | }) 57 | 58 | test("Does NOT mark excluded dependencies external", async t => { 59 | const context = await initPlugin({ exclude: 'test-dep' }) 60 | t.is(await context.resolveId('test-dep', 'index.js'), null) 61 | }) 62 | 63 | test("Marks peerDependencies external by default", async t => { 64 | const context = await initPlugin() 65 | t.is(await context.resolveId('test-peer-dep', 'index.js'), false) 66 | }) 67 | 68 | test("Does NOT mark peerDependencies external when peerDeps=false", async t => { 69 | const context = await initPlugin({ peerDeps: false }) 70 | t.is(await context.resolveId('test-peer-dep', 'index.js'), null) 71 | }) 72 | 73 | test("Does NOT mark excluded peerDependencies external", async t => { 74 | const context = await initPlugin({ exclude: 'test-peer-dep' }) 75 | t.is(await context.resolveId('test-peer-dep', 'index.js'), null) 76 | }) 77 | 78 | test("Marks optionalDependencies external by default", async t => { 79 | const context = await initPlugin() 80 | t.is(await context.resolveId('test-opt-dep', 'index.js'), false) 81 | }) 82 | 83 | test("Does NOT mark optionalDependencies external when optDeps=false", async t => { 84 | const context = await initPlugin({ optDeps: false }) 85 | t.is(await context.resolveId('test-opt-dep', 'index.js'), null) 86 | }) 87 | 88 | test("Does NOT mark excluded optionalDependencies external", async t => { 89 | const context = await initPlugin({ exclude: 'test-opt-dep' }) 90 | t.is(await context.resolveId('test-opt-dep', 'index.js'), null) 91 | }) 92 | 93 | test("Does NOT mark devDependencies external by default", async t => { 94 | const context = await initPlugin() 95 | t.is(await context.resolveId('test-dev-dep', 'index.js'), null) 96 | }) 97 | 98 | test("Marks devDependencies external when devDeps=true", async t => { 99 | const context = await initPlugin({ devDeps: true }) 100 | t.is(await context.resolveId('test-dev-dep', 'index.js'), false) 101 | }) 102 | 103 | test("Marks included devDependencies external", async t => { 104 | const context = await initPlugin({ include: 'test-dev-dep' }) 105 | t.is(await context.resolveId('test-dev-dep', 'index.js'), false) 106 | }) 107 | 108 | test("Subpath imports do not prevent dependencies/peerDependencies/optionalDependencies from being marked external", async t => { 109 | const context = await initPlugin() 110 | t.is(await context.resolveId('test-dep/sub', 'index.js'), false) 111 | t.is(await context.resolveId('test-peer-dep/sub', 'index.js'), false) 112 | t.is(await context.resolveId('test-opt-dep/sub', 'index.js'), false) 113 | }) 114 | 115 | test("Marks both dependency and dependency/subpath as external (with regex)", async t => { 116 | const context = await initPlugin({ include: /^test-dev-dep/ }) 117 | t.is(await context.resolveId('test-dev-dep', 'index.js'), false) 118 | t.is(await context.resolveId('test-dev-dep/sub', 'index.js'), false) 119 | }) 120 | 121 | test("exclude has precedence over include (builtins)", async t => { 122 | const context = await initPlugin({ include: 'node:fs', exclude: 'node:fs' }) 123 | t.like(await context.resolveId('node:fs', 'index.js'), { external: false }) 124 | }) 125 | 126 | test("exclude has precedence over include (dependencies)", async t => { 127 | const context = await initPlugin({ include: 'test-dep', exclude: 'test-dep' }) 128 | t.is(await context.resolveId('test-dep', 'index.js'), null) 129 | }) 130 | 131 | test("exclude has precedence over include (with regexes)", async t => { 132 | const context = await initPlugin({ devDeps: true, exclude: /^test-dev-dep\/sub/ }) 133 | t.is(await context.resolveId('test-dev-dep', 'index.js'), false) 134 | t.is(await context.resolveId('test-dev-dep/sub', 'index.js'), null) 135 | }) 136 | 137 | test("Normal dependencies have precedence over devDependencies", async t => { 138 | const context = await initPlugin({ packagePath: '04_dual/package.json' }) 139 | t.is(await context.resolveId('dual-dep', 'index.js'), false) 140 | }) 141 | -------------------------------------------------------------------------------- /source/index.ts: -------------------------------------------------------------------------------- 1 | import path from 'node:path' 2 | import fs from 'node:fs/promises' 3 | import cp from 'node:child_process' 4 | import { createRequire, isBuiltin } from 'node:module' 5 | import type { Plugin } from 'rollup' 6 | 7 | type MaybeFalsy = (T) | undefined | null | false 8 | type MaybeArray = (T) | (T)[] 9 | 10 | interface ViteCompatiblePlugin extends Plugin { 11 | apply?: 'build' | 'serve' 12 | enforce?: 'pre' | 'post' 13 | } 14 | 15 | export interface ExternalsOptions { 16 | 17 | /** 18 | * Mark node built-in modules like `path`, `fs`... as external. 19 | * 20 | * Defaults to `true`. 21 | */ 22 | builtins?: boolean 23 | 24 | /** 25 | * node: prefix handing for importing Node builtins: 26 | * - `'add'` turns `'path'` to `'node:path'` 27 | * - `'strip'` turns `'node:path'` to `'path'` 28 | * - `'ignore'` leaves Node builtin names as-is 29 | * 30 | * Defaults to `add`. 31 | */ 32 | builtinsPrefix?: 'add' | 'strip' | 'ignore' 33 | 34 | /** 35 | * Path/to/your/package.json file (or array of paths). 36 | * 37 | * Defaults to all package.json files found in parent directories recursively. 38 | * Won't go outside of a git repository. 39 | */ 40 | packagePath?: string | string[] 41 | 42 | /** 43 | * Mark dependencies as external. 44 | * 45 | * Defaults to `true`. 46 | */ 47 | deps?: boolean 48 | 49 | /** 50 | * Mark devDependencies as external. 51 | * 52 | * Defaults to `false`. 53 | */ 54 | devDeps?: boolean 55 | 56 | /** 57 | * Mark peerDependencies as external. 58 | * 59 | * Defaults to `true`. 60 | */ 61 | peerDeps?: boolean 62 | 63 | /** 64 | * Mark optionalDependencies as external. 65 | * 66 | * Defaults to `true`. 67 | */ 68 | optDeps?: boolean 69 | 70 | /** 71 | * Force include these deps in the list of externals, regardless of other settings. 72 | * 73 | * Defaults to `[]` (force include nothing). 74 | */ 75 | include?: MaybeArray> 76 | 77 | /** 78 | * Force exclude these deps from the list of externals, regardless of other settings. 79 | * 80 | * Defaults to `[]` (force exclude nothing). 81 | */ 82 | exclude?: MaybeArray> 83 | } 84 | 85 | // Fields of interest in package.json 86 | interface PackageJson { 87 | name: string 88 | version: string 89 | workspaces?: string[] 90 | dependencies?: Record 91 | devDependencies?: Record 92 | peerDependencies?: Record 93 | optionalDependencies?: Record 94 | } 95 | 96 | // Get our own name and version 97 | const { name, version } = createRequire(import.meta.url)('#package.json') as PackageJson 98 | 99 | // Files that mark the root of a monorepo 100 | const workspaceRootFiles = [ 101 | 'pnpm-workspace.yaml', // pnpm 102 | 'lerna.json', // Lerna / Lerna Light 103 | 'rush.json', // Rush 104 | // Note: is there any interest in the following? 105 | // 'workspace.jsonc', // Bit 106 | // 'nx.json', // Nx 107 | ] 108 | 109 | // Our defaults. 110 | type Config = Required 111 | const defaults: Config = { 112 | builtins: true, 113 | builtinsPrefix: 'add', 114 | packagePath: [], 115 | deps: true, 116 | devDeps: false, 117 | peerDeps: true, 118 | optDeps: true, 119 | include: [], 120 | exclude: [] 121 | } 122 | 123 | // Helpers. 124 | const isString = (str: unknown): str is string => typeof str === 'string' && str.length > 0 125 | const fileExists = (name: string) => fs.stat(name).then(stat => stat.isFile()).catch(() => false) 126 | 127 | /** 128 | * A Rollup/Vite plugin that automatically declares NodeJS built-in modules, 129 | * and optionally npm dependencies, as 'external'. 130 | */ 131 | function nodeExternals(options: ExternalsOptions = {}): Plugin { 132 | 133 | const config: Config = { ...defaults, ...options } 134 | 135 | let include: RegExp[] = [], // Initialized to empty arrays 136 | exclude: RegExp[] = [] // as a workaround to issue #30 137 | 138 | const isIncluded = (id: string) => include.length > 0 && include.some(rx => rx.test(id)), 139 | isExcluded = (id: string) => exclude.length > 0 && exclude.some(rx => rx.test(id)) 140 | 141 | // Determine the root of the git repository, if any. 142 | // 143 | // Note: we can't await the promise here because this would require our factory function 144 | // to be async and that would break the old Vite compatibility trick 145 | // (see issue #37 and https://github.com/vitejs/vite/issues/20717). 146 | const gitTopLevel = new Promise(resolve => { 147 | cp.execFile('git', [ 'rev-parse', '--show-toplevel' ], (error, stdout) => { 148 | resolve(error ? '' : path.normalize(stdout.trim())) 149 | }) 150 | }) 151 | 152 | return { 153 | name: name.replace(/^rollup-plugin-/, ''), 154 | version, 155 | apply: 'build', 156 | enforce: 'pre', 157 | 158 | async buildStart() { 159 | 160 | // Map the include and exclude options to arrays of regexes. 161 | [ include, exclude ] = ([ 'include', 'exclude' ] as const).map(option => ([] as Array>) 162 | .concat(config[option]) 163 | .reduce((result, entry, index) => { 164 | if (entry instanceof RegExp) 165 | result.push(entry) 166 | else if (isString(entry)) 167 | result.push(new RegExp('^' + entry.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '$')) 168 | else if (entry) 169 | this.warn(`Ignoring wrong entry type #${index} in '${option}' option: ${JSON.stringify(entry)}`) 170 | return result 171 | }, [] as RegExp[]) 172 | ) 173 | 174 | // Populate the packagePath option if not given by getting all package.json files 175 | // from cwd up to the root of the git repo, the root of the monorepo, 176 | // or the root of the volume, whichever comes first. 177 | const packagePaths = ([] as string[]) 178 | .concat(config.packagePath) 179 | .filter(isString) 180 | .map(packagePath => path.resolve(packagePath)) 181 | if (packagePaths.length === 0) { 182 | for (let current = process.cwd(), previous = ''; previous !== current; previous = current, current = path.dirname(current)) { 183 | 184 | // Gather package.json files. 185 | const name = path.join(current, 'package.json') 186 | if (await fileExists(name)) 187 | packagePaths.push(name) 188 | 189 | // Break early if we are at the root of the git repo 190 | // or there is a known workspace root file. 191 | const breaks = await Promise.all([ 192 | gitTopLevel.then(topLevel => topLevel === current), 193 | ...workspaceRootFiles.map(name => fileExists(path.join(current, name))) 194 | ]) 195 | 196 | if (breaks.some(result => result)) 197 | break 198 | } 199 | } 200 | 201 | // Gather dependencies names. 202 | const externals: Record = {} 203 | for (const packagePath of packagePaths) { 204 | const manifest = await fs.readFile(packagePath) 205 | .then(buffer => JSON.parse(buffer.toString()) as PackageJson) 206 | .catch((err: NodeJS.ErrnoException | SyntaxError) => err) 207 | if (manifest instanceof Error) { 208 | const message = manifest instanceof SyntaxError 209 | ? `File ${JSON.stringify(packagePath)} does not look like a valid package.json.` 210 | : `Cannot read ${JSON.stringify(packagePath)}, error: ${manifest.code}.` 211 | return this.error({ message, stack: undefined }) 212 | } 213 | 214 | Object.assign(externals, 215 | config.deps ? manifest.dependencies : undefined, 216 | config.devDeps ? manifest.devDependencies : undefined, 217 | config.peerDeps ? manifest.peerDependencies : undefined, 218 | config.optDeps ? manifest.optionalDependencies : undefined 219 | ) 220 | 221 | // Watch this package.json. 222 | this.addWatchFile(packagePath) 223 | 224 | // Break early if this is an npm/yarn workspace root. 225 | if (Array.isArray(manifest.workspaces)) 226 | break 227 | } 228 | 229 | // Add all dependencies as an include RegEx. 230 | const names = Object.keys(externals) 231 | if (names.length > 0) 232 | include.push(new RegExp('^(?:' + names.join('|') + ')(?:/.+)?$')) 233 | }, 234 | 235 | resolveId(specifier, _, { isEntry }) { 236 | if ( 237 | isEntry // Ignore entry points 238 | || /^(?:\0|\.{1,2}\/)/.test(specifier) // Ignore virtual modules and relative imports 239 | || path.isAbsolute(specifier) // Ignore already resolved ids 240 | ) { 241 | return null 242 | } 243 | 244 | // Handle node builtins. 245 | if (isBuiltin(specifier)) { 246 | const stripped = specifier.replace(/^node:/, '') 247 | return { 248 | id: config.builtinsPrefix === 'ignore' 249 | ? specifier 250 | : config.builtinsPrefix === 'add' || !isBuiltin(stripped) 251 | ? 'node:' + stripped 252 | : stripped, 253 | external: (config.builtins || isIncluded(specifier)) && !isExcluded(specifier), 254 | moduleSideEffects: false 255 | } 256 | } 257 | 258 | // Handle npm dependencies. 259 | return isIncluded(specifier) && !isExcluded(specifier) 260 | ? false // external 261 | : null // normal handling 262 | } 263 | } as ViteCompatiblePlugin 264 | } 265 | 266 | export default nodeExternals 267 | export { nodeExternals } 268 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | ![NPM Version](https://img.shields.io/npm/v/rollup-plugin-node-externals?label=latest) 4 | ![Rollup 4.0](https://img.shields.io/badge/Rollup-%3E%3D4.0.0-orange) 5 | ![Vite 5.0](https://img.shields.io/badge/Vite-%3E%3D5.0.0-purple) 6 | ![NPM Downloads](https://img.shields.io/npm/dm/rollup-plugin-node-externals) 7 | ![NPM License](https://img.shields.io/npm/l/rollup-plugin-node-externals) 8 | 9 |

10 | 11 | # rollup-plugin-node-externals 12 | A Rollup/Vite plugin that automatically declares NodeJS built-in modules as `external`. Also handles dependencies, devDependencies, peerDependencies and optionalDependencies. 13 | 14 | Works in monorepos too! 15 | 16 | 17 | ## Why you need this 18 |
(click to read) 19 | 20 | By default, Rollup doesn't know a thing about NodeJS, so trying to bundle simple things like `import path from 'path'` in your code results in a `Unresolved dependencies` warning. 21 | 22 | The solution here is quite simple: you must tell Rollup that the `path` module is in fact _external_. This way, Rollup won't try to bundle it in and rather leave the `import` statement as is (or translate it to a `require()` call if bundling for CommonJS). 23 | 24 | However, this must be done for each and every NodeJS built-in you happen to use in your program: `path`, `os`, `fs`, `url`, etc., which can quickly become cumbersome when done manually. 25 | 26 | So the primary goal of this plugin is simply to automatically declare all NodeJS built-in modules as external. 27 | 28 | As an added bonus, this plugin will also declare your dependencies (as per your local or monorepo `package.json` file(s)) as external. 29 |
30 | 31 | ## Requirements 32 | - Rollup >= 4 or Vite >= 5 33 | - NodeJS >= 21 34 | 35 | 36 | ## Installation 37 | Use your favorite package manager. Mine is [npm](https://www.npmjs.com): 38 | 39 | ```sh 40 | npm install --save-dev rollup-plugin-node-externals 41 | ``` 42 | 43 | 44 | ## Usage 45 | 46 | ### Import 47 | The plugin is available both as the default export and as a named export: 48 | 49 | ```js 50 | import nodeExternals from 'rollup-plugin-node-externals' 51 | ``` 52 | 53 | and 54 | 55 | ```js 56 | import { nodeExternals } from 'rollup-plugin-node-externals' 57 | ``` 58 | 59 | will both work. 60 | 61 | 62 | ### Options 63 | You generally want to have your **runtime dependencies** (those that will be imported/required at runtime) listed under `dependencies` in `package.json`, and your **development dependencies** (those that should be bundled in by Rollup) listed under `devDependencies`. 64 | 65 | If you follow this simple rule, then the default settings are just what you need: 66 | 67 | ```js 68 | // rollup.config.js 69 | 70 | export default { 71 | ... 72 | plugins: [ 73 | nodeExternals(), 74 | ] 75 | } 76 | ``` 77 | 78 | This will bundle your `devDependencies` in while leaving your `dependencies`, `peerDependencies` and `optionalDependencies` external. 79 | 80 | Should the defaults not suit your case, here is the full list of options. 81 | 82 | ```typescript 83 | import nodeExternals from 'rollup-plugin-node-externals' 84 | 85 | export default { 86 | ... 87 | plugins: [ 88 | nodeExternals({ 89 | 90 | // Make node builtins external. Default: true. 91 | builtins?: boolean 92 | 93 | // node: prefix handing for importing Node builtins. Default: 'add'. 94 | builtinsPrefix?: 'add' | 'strip' | 'ignore' 95 | 96 | // The path(s) to your package.json. See below for default. 97 | packagePath?: string | string[] 98 | 99 | // Make pkg.dependencies external. Default: true. 100 | deps?: boolean 101 | 102 | // Make pkg.devDependencies external. Default: false. 103 | devDeps?: boolean 104 | 105 | // Make pkg.peerDependencies external. Default: true. 106 | peerDeps?: boolean 107 | 108 | // Make pkg.optionalDependencies external. Default: true. 109 | optDeps?: boolean 110 | 111 | // Modules to force include in externals. Default: []. 112 | include?: string | RegExp | (string | RegExp)[] 113 | 114 | // Modules to force exclude from externals. Default: []. 115 | exclude?: string | RegExp | (string | RegExp)[] 116 | }) 117 | ] 118 | } 119 | ``` 120 | 121 | #### builtins?: boolean = true 122 | Set the `builtins` option to `false` if you'd like to use some shims/polyfills for those. You'll most certainly need [an other plugin](https://github.com/ionic-team/rollup-plugin-node-polyfills) as well. 123 | 124 | #### builtinsPrefix?: 'add' | 'strip' | 'ignore' = 'add' 125 | How to handle the `node:` scheme when importing builtins (i.e., `import path from 'node:path'`). 126 | - If `add` (the default, recommended), the `node:` scheme is always added if missing. In effect, this dedupes your imports of Node builtins by homogenizing their names to their schemed version. 127 | - If `strip`, the scheme is always removed. In effect, this dedupes your imports of Node builtins by homogenizing their names to their scheme-less version. Schemed-only builtins like `node:test` are never stripped. 128 | - `ignore` will simply leave all builtins imports as written in your code. 129 | > _Note that scheme handling is always applied, regardless of the `builtins` options being enabled or not._ 130 | 131 | #### packagePath?: string | string[] = [] 132 | If you're working with monorepos, the `packagePath` option is made for you. It can take a path, or an array of paths, to your package.json file(s). If not specified, the default is to start with the current directory's package.json then go up scan for all `package.json` files in parent directories recursively until either the root git directory is reached, the root of the monorepo is reached, or no other `package.json` can be found. 133 | 134 | #### deps?: boolean = true
devDeps?: boolean = false
peerDeps?: boolean = true
optDeps?: boolean = true 135 | Set the `deps`, `devDeps`, `peerDeps` and `optDeps` options to `false` to prevent the corresponding dependencies from being externalized, therefore letting Rollup bundle them with your code. 136 | 137 | #### include?: string | RegExp | (string | RegExp)[] = [] 138 | Use the `include` option to force certain dependencies into the list of externals, regardless of other settings: 139 | 140 | ```js 141 | nodeExternals({ 142 | deps: false, // Deps will be bundled in 143 | include: 'fsevents' // Except for fsevents 144 | }) 145 | ``` 146 | 147 | #### exclude?: string | RegExp | (string | RegExp)[] = [] 148 | Conversely, use the `exclude` option to remove certain dependencies from the list of externals, regardless of other settings: 149 | 150 | ```js 151 | nodeExternals({ 152 | deps: true, // Keep deps external 153 | exclude: 'electron-reload' // Yet we want `electron-reload` bundled in 154 | }) 155 | ``` 156 | 157 | 158 | ## Notes 159 | 160 | ### 1/ This plugin is smart 161 | - Falsy values in `include` and `exclude` are silently ignored. This allows for conditional constructs like `exclude: process.env.NODE_ENV === 'production' && 'my-prod-only-dep'`. 162 | - Subpath imports are supported with regexes, meaning that `include: /^lodash/` will externalize `lodash` and also `lodash/map`, `lodash/merge`, etc. 163 | 164 | ### 2/ This plugin is not _that_ smart 165 | It uses an exact match against your imports _as written in your code_. No resolving of path aliases or substitutions is made: 166 | 167 | ```js 168 | // In your code, say '@/lib' is an alias for node_modules/lib/deep/path/to/some/file.js: 169 | import something from '@/lib' 170 | ``` 171 | 172 | If you don't want `node_modules/lib/deep/path/to/some/file.js` bundled in, then write: 173 | 174 | ```js 175 | // In rollup.config.js: 176 | nodeExternals({ 177 | include: '@/lib' 178 | }) 179 | ``` 180 | 181 | ### 3/ Order matters 182 | If you're also using [`@rollup/plugin-node-resolve`](https://github.com/rollup/plugins/tree/master/packages/node-resolve/#readme), make sure this plugin comes _before_ it in the `plugins` array: 183 | 184 | ```js 185 | import nodeExternals from 'rollup-plugin-node-externals' 186 | import nodeResolve from '@rollup/plugin-node-resolve' 187 | 188 | export default { 189 | ... 190 | plugins: [ 191 | nodeExternals(), 192 | nodeResolve(), 193 | ] 194 | } 195 | ``` 196 | 197 | Note that as of version 7.1, this plugin has a `enforce: 'pre'` property that will make Rollup and Vite call it very early in the module resolution process. Nevertheless, it is best to always make this plugin the first one in the `plugins` array. 198 | 199 | ### 4/ Rollup rules 200 | Rollup's own `external` configuration option always takes precedence over this plugin. This is intentional. 201 | 202 | ### 5/ Using with Vite 203 | While this plugin has always been compatible with Vite, it was previously necessary to use the following `vite.config.js` to make it work reliably in every situation: 204 | 205 | ```js 206 | import { defineConfig } from 'vite' 207 | import nodeExternals from 'rollup-plugin-node-externals' 208 | 209 | export default defineConfig({ 210 | ... 211 | plugins: [ 212 | { enforce: 'pre', ...nodeExternals() }, 213 | // other plugins follow 214 | ] 215 | }) 216 | ``` 217 | 218 | Since version 7.1, this is no longer necessary and you can use the normal syntax instead. You still want to keep this plugin early in the `plugins` array, though. 219 | 220 | ```js 221 | import { defineConfig } from 'vite' 222 | import nodeExternals from 'rollup-plugin-node-externals' 223 | 224 | export default defineConfig({ 225 | ... 226 | plugins: [ 227 | nodeExternals() 228 | // other plugins follow 229 | ] 230 | }) 231 | ``` 232 | 233 | > [!NOTE] 234 | > Make sure you use the _top-level plugins array_ in `vite.config.js` as shown above. **Using `build.rollupOptions.plugins` will probably not work**. See [#35](https://github.com/Septh/rollup-plugin-node-externals/issues/35) for details. 235 | 236 | 237 | ## Breaking changes 238 | 239 | ### Breaking changes in version 8 240 | - Removed support for Rollup 3. 241 | - Removed `order: pre` from `resolveId` hook (see [#33](https://github.com/Septh/rollup-plugin-node-externals/issues/33)). Might force users who relied on this, to make sure this plugin comes first in the plugins array. 242 | 243 | ### Breaking changes in previous versions 244 |
Previous versions -- click to expand 245 | 246 | ### Breaking changes in version 7 247 | - This package now only supports the [Maintenance, LTS and Current versions](https://github.com/nodejs/Release#release-schedule) of Node.js. 248 | - The previously undocumented `externals` named export has been removed. 249 | 250 | #### Breaking changes in version 6 251 | - This package is now esm-only and requires NodeJS v16+.
*If you need CommonJS or older NodeJS support, please stick to v5.* 252 | - This plugin now has a **peer-dependency** on Rollup `^3.0.0 || ^4.0.0`.
*If you need Rollup 2 support, please stick to v5.* 253 | 254 | #### Breaking changes in version 5 255 | - In previous versions, the `devDeps` option defaulted to `true`.
This was practical, but often wrong: devDependencies are meant just for that: being used when developing. Therefore, the `devDeps` option now defaults to `false`, meaning Rollup will include them in your bundle. 256 | - As anticipated since v4, the `builtinsPrefix` option now defaults to `'add'`. 257 | - The deprecated `prefixedBuiltins` option has been removed. Use `builtinsPrefix` instead. 258 | - `rollup-plugin-node-externals` no longer depends on the Find-Up package (while this is not a breaking change per se, it can be in some edge situations). 259 | - The plugin now has a _peer dependency_ on `rollup ^2.60.0 || ^3.0.0`. 260 | 261 | #### Breaking changes in version 4 262 | - In previous versions, the `deps` option defaulted to `false`.
This was practical, but often wrong: when bundling for distribution, you want your own dependencies to be installed by the package manager alongside your package, so they should not be bundled in the code. Therefore, the `deps` option now defaults to `true`. 263 | - Now requires Node 14 (up from Node 12 for previous versions). 264 | - Now has a _peer dependency_ on `rollup ^2.60.0`. 265 | 266 |
267 | 268 | 269 | ## License 270 | MIT 271 | --------------------------------------------------------------------------------