diff --git a/test/_common.ts b/test/_common.ts index 053cac3..dba133f 100644 --- a/test/_common.ts +++ b/test/_common.ts @@ -1,52 +1,63 @@ import path from 'node:path' import { fileURLToPath } from 'node:url' -import type { Plugin, RollupError, ObjectHook } from 'rollup' +import type { Plugin, RollupError, ObjectHook, PluginContextMeta, PluginHooks, PluginContext, NormalizedInputOptions } from 'rollup' import { nodeExternals, type ExternalsOptions } from '../source/index.ts' const __dirname = path.dirname(fileURLToPath(import.meta.url)) -const warnings: string[] = [] -const fakePluginContext = { - meta: { - watchMode: false - }, +class MockPluginContext { + private readonly externals: Plugin + readonly warnings: string[] + readonly meta: PluginContextMeta + + constructor(externals: Plugin) { + this.externals = externals + this.warnings = [] + this.meta = { + rollupVersion: '4.9.6', + watchMode: false + } + } + + async buildStart() { + let { buildStart } = this.externals + if (typeof buildStart === 'object') + buildStart = buildStart.handler + if (typeof buildStart === 'function') + return await buildStart.call(this as any, {} as NormalizedInputOptions) + throw new Error('Ooops') + } + + async resolveId(specifier: string, importer: string | undefined) { + let { resolveId } = this.externals + if (typeof resolveId === 'object') + resolveId = resolveId.handler + if (typeof resolveId === 'function') + return await resolveId.call(this as any, specifier, importer, { attributes: {}, isEntry: typeof importer === 'string' ? false : true }) + throw new Error('Ooops') + } error(err: string | RollupError): never { const message: string = typeof err === 'string' ? err : err.message throw new Error(message) - }, + } warn(message: string): void { - warnings.push(message) - }, + this.warnings.push(message) + } addWatchFile(_file: string) { // nop } } -// node-externals only implements these hooks -type ImplementedHooks = - | 'buildStart' - | 'resolveId' - -export async function callHook(plugin: Plugin, hookName: ImplementedHooks, ...args: any[]) { - const hook = plugin[hookName] as ObjectHook<(this: typeof fakePluginContext, ...args: any) => any> - if (typeof hook === 'function') - return hook.apply(fakePluginContext, args) - if (typeof hook === 'object' && typeof hook.handler === 'function') - return hook.handler.apply(fakePluginContext, args) - throw new Error('Ooops') -} - -export async function initPlugin(options: ExternalsOptions = {}): Promise<{ plugin: Plugin, warnings: string[] }> { - warnings.splice(0, Infinity) - - const plugin = nodeExternals(options) - await callHook(plugin, 'buildStart') - return { plugin, warnings } +export async function initPlugin(options: ExternalsOptions = {}) { + const plugin = await nodeExternals(options) + const context = new MockPluginContext(plugin) + await context.buildStart() + return context } export function fixture(...parts: string[]) { diff --git a/test/builtins.test.ts b/test/builtins.test.ts index b3ca4b8..6bc9af7 100644 --- a/test/builtins.test.ts +++ b/test/builtins.test.ts @@ -1,78 +1,78 @@ import test from 'ava' -import { initPlugin, callHook } from './_common.ts' +import { initPlugin } from './_common.ts' test("Marks Node builtins external by default", async t => { - const { plugin } = await initPlugin() + const context = await initPlugin() for (const builtin of [ 'path', 'node:fs' ]) { - t.like(await callHook(plugin, 'resolveId', builtin, 'index.js'), { + t.like(await context.resolveId(builtin, 'index.js'), { external: true }) } }) test("Does NOT mark Node builtins external when builtins=false", async t => { - const { plugin } = await initPlugin({ builtins: false }) + const context = await initPlugin({ builtins: false }) for (const builtin of [ 'path', 'node:fs' ]) { - t.like(await callHook(plugin, 'resolveId', builtin, 'index.js'), { + t.like(await context.resolveId(builtin, 'index.js'), { external: false }) } }) test("Does NOT mark Node builtins external when implicitely excluded", async t => { - const { plugin } = await initPlugin({ exclude: [ 'path', 'node:fs' ]}) + const context = await initPlugin({ exclude: [ 'path', 'node:fs' ]}) for (const builtin of [ 'path', 'node:fs' ]) { - t.like(await callHook(plugin, 'resolveId', builtin, 'index.js'), { + t.like(await context.resolveId(builtin, 'index.js'), { external: false }) } }) test("Marks Node builtins external when builtins=false and implicitly included", async t => { - const { plugin } = await initPlugin({ builtins: false, include: [ 'path', 'node:fs' ] }) + const context = await initPlugin({ builtins: false, include: [ 'path', 'node:fs' ] }) for (const builtin of [ 'path', 'node:fs' ]) { - t.like(await callHook(plugin, 'resolveId', builtin, 'index.js'), { + t.like(await context.resolveId(builtin, 'index.js'), { external: true }) } }) test("Adds 'node:' prefix to builtins by default", async t => { - const { plugin } = await initPlugin() + const context = await initPlugin() for (const builtin of [ 'node:path', 'path' ]) { - t.like(await callHook(plugin, 'resolveId', builtin, 'index.js'), { + t.like(await context.resolveId(builtin, 'index.js'), { id: 'node:path' }) } }) test("Removes 'node:' prefix when using builtinsPrefix='strip'", async t => { - const { plugin } = await initPlugin({ builtinsPrefix: 'strip' }) + const context = await initPlugin({ builtinsPrefix: 'strip' }) for (const builtin of [ 'node:path', 'path' ]) { - t.like(await callHook(plugin, 'resolveId', builtin, 'index.js'), { + t.like(await context.resolveId(builtin, 'index.js'), { id: 'path' }) } }) test("Does NOT remove 'node:test' prefix even with builtinsPrefix='add'", async t => { - const { plugin } = await initPlugin({ builtinsPrefix: 'strip' }) + const context = await initPlugin({ builtinsPrefix: 'strip' }) for (const builtin of [ 'node:test' ]) { - t.like(await callHook(plugin, 'resolveId', builtin, 'index.js'), { + t.like(await context.resolveId(builtin, 'index.js'), { id: builtin }) } }) test("Does not recognize 'test' as a Node builtin", async t => { - const { plugin } = await initPlugin() - t.is(await callHook(plugin, 'resolveId', 'node', 'index.js'), null) + const context = await initPlugin() + t.is(await context.resolveId('node', 'index.js'), null) }) test("Ignores 'node:' prefix when using builtinsPrefix='ignore'", async t => { - const { plugin } = await initPlugin({ builtinsPrefix: 'ignore' }) + const context = await initPlugin({ builtinsPrefix: 'ignore' }) for (const builtin of [ 'node:path', 'path' ]) { - t.like(await callHook(plugin, 'resolveId', builtin, 'index.js'), { + t.like(await context.resolveId(builtin, 'index.js'), { id: builtin }) } diff --git a/test/monorepo.test.ts b/test/monorepo.test.ts index f4e2572..f43f110 100644 --- a/test/monorepo.test.ts +++ b/test/monorepo.test.ts @@ -1,6 +1,6 @@ import fs from 'node:fs/promises' import test from 'ava' -import { initPlugin, callHook, fixture } from './_common.ts' +import { initPlugin, fixture } from './_common.ts' // These two tests need to be run in sequence test.serial('git monorepo usage', async t => { @@ -8,14 +8,14 @@ test.serial('git monorepo usage', async t => { process.chdir(fixture('01_monorepo/one')) // Should gather dependencies up to ./test/fixtures/01_monorepo - const { plugin } = await initPlugin() + const context = await initPlugin() // Should be external for (const dependency of [ 'moment', // dependency in ./test/fixtures/01_monorepo/one/package.json (picked) 'chalk' // dependency in ./test/fixtures/01_monorepo/package.json (picked) ]) { - t.false(await callHook(plugin, 'resolveId', dependency, 'index.js')) + t.false(await context.resolveId(dependency, 'index.js')) } // Should be ignored @@ -23,7 +23,7 @@ test.serial('git monorepo usage', async t => { 'react', // dependency in ./test/fixtures/01_monorepo/two/package.json (not picked) 'test-dep' // dependency in ./test/fixtures/package.json (not picked) ]) { - t.is(await callHook(plugin, 'resolveId', dependency, 'index.js'), null) + t.is(await context.resolveId(dependency, 'index.js'), null) } }) @@ -32,7 +32,7 @@ test.serial('non-git monorepo usage', async t => { process.chdir(fixture('01_monorepo/one')) // Should gather dependencies up to . ! - const { plugin } = await initPlugin() + const context = await initPlugin() // Should be external for (const dependency of [ @@ -41,13 +41,13 @@ test.serial('non-git monorepo usage', async t => { 'test-dep', // dependency in ./test/fixtures/package.json (picked) 'rollup', // peer dependency in ./package.json (picked !) ]) { - t.false(await callHook(plugin, 'resolveId', dependency, 'index.js')) + t.false(await context.resolveId(dependency, 'index.js')) } // Should be ignored for (const dependency of [ 'react' // dependency in ./test/fixtures/01_monorepo/two/package.json (not picked) ]) { - t.is(await callHook(plugin, 'resolveId', dependency, 'index.js'), null) + t.is(await context.resolveId(dependency, 'index.js'), null) } }) diff --git a/test/options.test.ts b/test/options.test.ts index baff46b..ebceb44 100644 --- a/test/options.test.ts +++ b/test/options.test.ts @@ -1,7 +1,7 @@ import test from 'ava' import { testProp, fc } from '@fast-check/ava' import type { Arbitrary } from 'fast-check' -import { initPlugin, callHook, fixture } from './_common.ts' +import { initPlugin, fixture } from './_common.ts' import { type ExternalsOptions } from '../source/index.ts' // Ensures tests use local package.json @@ -35,28 +35,29 @@ testProp( } ) -// Must be serial because it uses the 'warnings' global in _common.ts. -test.serial("Warns when given invalid include or exclude entry", async t => { +test("Warns when given invalid include or exclude entry", async t => { const okay = 'some_dep' // string is ok const notOkay = 1 // number is not (unless 0, which is falsy) - const { warnings } = await initPlugin({ - include: [ okay, notOkay as any ] + const context = await initPlugin({ + include: [ okay, notOkay as any ], + exclude: [ okay, notOkay as any ], }) - t.is(warnings.length, 1) - t.is(warnings[0], `Ignoring wrong entry type #1 in 'include' option: ${JSON.stringify(notOkay)}`) + t.is(context.warnings.length, 2) + t.is(context.warnings[0], `Ignoring wrong entry type #1 in 'include' option: ${JSON.stringify(notOkay)}`) + t.is(context.warnings[1], `Ignoring wrong entry type #1 in 'exclude' option: ${JSON.stringify(notOkay)}`) }) test("Obeys 'packagePath' option (single file name)", async t => { - const { plugin } = await initPlugin({ + const context = await initPlugin({ packagePath: '00_simple/package.json' }) - t.false(await callHook(plugin, 'resolveId', 'simple-dep', 'index.js')) + t.false(await context.resolveId('simple-dep', 'index.js')) }) test("Obeys 'packagePath' option (multiple file names)", async t => { - const { plugin } = await initPlugin({ + const context = await initPlugin({ packagePath: [ '00_simple/package.json', '01_monorepo/package.json' @@ -68,6 +69,6 @@ test("Obeys 'packagePath' option (multiple file names)", async t => { 'simple-dep', // 00_simple/package.json 'chalk', // 01_monorepo/package.json ]) { - t.false(await callHook(plugin, 'resolveId', dependency, 'index.js')) + t.false(await context.resolveId(dependency, 'index.js')) } }) diff --git a/test/specifier.test.ts b/test/specifier.test.ts index 440c8f4..ea7a449 100644 --- a/test/specifier.test.ts +++ b/test/specifier.test.ts @@ -1,5 +1,5 @@ import test from 'ava' -import { initPlugin, callHook, fixture } from './_common.ts' +import { initPlugin, fixture } from './_common.ts' const specifiers = { virtual: [ '\\0virtual' ], @@ -14,113 +14,113 @@ const specifiers = { process.chdir(fixture()) test("Always ignores bundle entry point", async t => { - const { plugin } = await initPlugin() - t.is(await callHook(plugin, 'resolveId', './path/to/entry.js', undefined), null) + const context = await initPlugin() + t.is(await context.resolveId('./path/to/entry.js', undefined), null) }) test("Always ignores virtual modules from other plugins", async t => { - const { plugin } = await initPlugin() - t.is(await callHook(plugin, 'resolveId', '\\0virtual', undefined), null, `Failed without importer`) - t.is(await callHook(plugin, 'resolveId', '\\0virtual', 'file.js'), null, `Failed with importer`) + const context = await initPlugin() + t.is(await context.resolveId('\\0virtual', undefined), null, `Failed without importer`) + t.is(await context.resolveId('\\0virtual', 'file.js'), null, `Failed with importer`) }) test("Always ignores absolute specifiers", async t => { - const { plugin } = await initPlugin() + const context = await initPlugin() for (const specifier of specifiers[process.platform === 'win32' ? 'absoluteWin32' : 'absolutePosix']) { - t.is(await callHook(plugin, 'resolveId', specifier, undefined), null, `Failed on: ${specifier} without importer`) - t.is(await callHook(plugin, 'resolveId', specifier, 'file.js'), null, `Failed on: ${specifier} with importer`) + t.is(await context.resolveId(specifier, undefined), null, `Failed on: ${specifier} without importer`) + t.is(await context.resolveId(specifier, 'file.js'), null, `Failed on: ${specifier} with importer`) } }) test("Always ignores relative specifiers", async t => { - const { plugin } = await initPlugin({ include: specifiers.relative }) + const context = await initPlugin({ include: specifiers.relative }) for (const specifier of specifiers.relative) { - t.is(await callHook(plugin, 'resolveId', specifier, undefined), null, `Failed on: ${specifier} without importer`) - t.is(await callHook(plugin, 'resolveId', specifier, 'file.js'), null, `Failed on: ${specifier} with importer`) + t.is(await context.resolveId(specifier, undefined), null, `Failed on: ${specifier} without importer`) + t.is(await context.resolveId(specifier, 'file.js'), null, `Failed on: ${specifier} with importer`) } }) test("Always ignores bare specifiers that are not dependencies", async t => { - const { plugin } = await initPlugin({ deps: true, peerDeps: true, optDeps: true, devDeps: true }) - t.is(await callHook(plugin, 'resolveId', 'not-a-dep', 'index.js'), null) + const context = await initPlugin({ deps: true, peerDeps: true, optDeps: true, devDeps: true }) + t.is(await context.resolveId('not-a-dep', 'index.js'), null) }) test("Marks dependencies external by default", async t => { - const { plugin } = await initPlugin() - t.false(await callHook(plugin, 'resolveId', 'test-dep', 'index.js')) + const context = await initPlugin() + t.false(await context.resolveId('test-dep', 'index.js')) }) test("Does NOT mark dependencies external when deps=false", async t => { - const { plugin } = await initPlugin({ deps: false }) - t.is(await callHook(plugin, 'resolveId', 'test-dep', 'index.js'), null) + const context = await initPlugin({ deps: false }) + t.is(await context.resolveId('test-dep', 'index.js'), null) }) test("Does NOT mark excluded dependencies external", async t => { - const { plugin } = await initPlugin({ exclude: 'test-dep' }) - t.is(await callHook(plugin, 'resolveId', 'test-dep', 'index.js'), null) + const context = await initPlugin({ exclude: 'test-dep' }) + t.is(await context.resolveId('test-dep', 'index.js'), null) }) test("Marks peerDependencies external by default", async t => { - const { plugin } = await initPlugin() - t.is(await callHook(plugin, 'resolveId', 'test-dev-dep', 'index.js'), null) + const context = await initPlugin() + t.is(await context.resolveId('test-dev-dep', 'index.js'), null) }) test("Does NOT mark peerDependencies external when peerDeps=false", async t => { - const { plugin } = await initPlugin({ peerDeps: false }) - t.is(await callHook(plugin, 'resolveId', 'test-dev-dep', 'index.js'), null) + const context = await initPlugin({ peerDeps: false }) + t.is(await context.resolveId('test-dev-dep', 'index.js'), null) }) test("Does NOT mark excluded peerDependencies external", async t => { - const { plugin } = await initPlugin({ exclude: 'test-peer-dep' }) - t.is(await callHook(plugin, 'resolveId', 'test-dev-dep', 'index.js'), null) + const context = await initPlugin({ exclude: 'test-peer-dep' }) + t.is(await context.resolveId('test-dev-dep', 'index.js'), null) }) test("Marks optionalDependencies external by default", async t => { - const { plugin } = await initPlugin() - t.false(await callHook(plugin, 'resolveId', 'test-opt-dep', 'index.js')) + const context = await initPlugin() + t.false(await context.resolveId('test-opt-dep', 'index.js')) }) test("Does NOT mark optionalDependencies external when optDeps=false", async t => { - const { plugin } = await initPlugin({ optDeps: false }) - t.is(await callHook(plugin, 'resolveId', 'test-dev-dep', 'index.js'), null) + const context = await initPlugin({ optDeps: false }) + t.is(await context.resolveId('test-dev-dep', 'index.js'), null) }) test("Does NOT mark excluded optionalDependencies external", async t => { - const { plugin } = await initPlugin({ exclude: 'test-opt-dep' }) - t.is(await callHook(plugin, 'resolveId', 'test-dev-dep', 'index.js'), null) + const context = await initPlugin({ exclude: 'test-opt-dep' }) + t.is(await context.resolveId('test-dev-dep', 'index.js'), null) }) test("Does NOT mark devDependencies external by default", async t => { - const { plugin } = await initPlugin() - t.is(await callHook(plugin, 'resolveId', 'test-dev-dep', 'index.js'), null) + const context = await initPlugin() + t.is(await context.resolveId('test-dev-dep', 'index.js'), null) }) test("Marks devDependencies external when devDeps=true", async t => { - const { plugin } = await initPlugin({ devDeps: true }) - t.false(await callHook(plugin, 'resolveId', 'test-dev-dep', 'index.js')) + const context = await initPlugin({ devDeps: true }) + t.false(await context.resolveId('test-dev-dep', 'index.js')) }) test("Marks included devDependencies external", async t => { - const { plugin } = await initPlugin({ include: 'test-dev-dep' }) - t.false(await callHook(plugin, 'resolveId', 'test-dev-dep', 'index.js')) + const context = await initPlugin({ include: 'test-dev-dep' }) + t.false(await context.resolveId('test-dev-dep', 'index.js')) }) test("Marks dependencies/peerDependencies/optionalDependencies subpath imports external", async t => { - const { plugin } = await initPlugin() - t.is(await callHook(plugin, 'resolveId', 'test-dep/sub', 'index.js'), false) - t.is(await callHook(plugin, 'resolveId', 'test-peer-dep/sub', 'index.js'), false) - t.is(await callHook(plugin, 'resolveId', 'test-opt-dep/sub', 'index.js'), false) + const context = await initPlugin() + t.is(await context.resolveId('test-dep/sub', 'index.js'), false) + t.is(await context.resolveId('test-peer-dep/sub', 'index.js'), false) + t.is(await context.resolveId('test-opt-dep/sub', 'index.js'), false) }) test("Marks subpath imports external (with regexes)", async t => { - const { plugin } = await initPlugin({ include: /^test-dev-dep/ }) - t.is(await callHook(plugin, 'resolveId', 'test-dev-dep', 'index.js'), false) - t.is(await callHook(plugin, 'resolveId', 'test-dev-dep/sub', 'index.js'), false) + const context = await initPlugin({ include: /^test-dev-dep/ }) + t.is(await context.resolveId('test-dev-dep', 'index.js'), false) + t.is(await context.resolveId('test-dev-dep/sub', 'index.js'), false) }) test("External dependencies have precedence over devDependencies", async t => { - const { plugin } = await initPlugin({ + const context = await initPlugin({ packagePath: '04_dual/package.json' }) - t.false(await callHook(plugin, 'resolveId', 'dual-dep', 'index.js')) + t.false(await context.resolveId('dual-dep', 'index.js')) }) diff --git a/test/workspaces.test.ts b/test/workspaces.test.ts index 1f269f5..c5ac13d 100644 --- a/test/workspaces.test.ts +++ b/test/workspaces.test.ts @@ -1,16 +1,16 @@ import test from 'ava' -import { initPlugin, callHook, fixture } from './_common.ts' +import { initPlugin, fixture } from './_common.ts' test('npm/yarn workspaces usage', async t => { process.chdir(fixture('02_workspaces/npm-and-yarn/one')) - const { plugin } = await initPlugin() + const context = await initPlugin() // Should be external for (const dependency of [ 'moment', // 02_workspaces/npm-and-yarn/one/package.json 'chalk' // 02_workspaces/npm-and-yarn/package.json ]) { - t.false(await callHook(plugin, 'resolveId', dependency, 'index.js')) + t.false(await context.resolveId(dependency, 'index.js')) } // Should be ignored @@ -19,20 +19,20 @@ test('npm/yarn workspaces usage', async t => { 'rollup', // 02_workspaces/package.json 'test-dep' // ./package.json ]) { - t.is(await callHook(plugin, 'resolveId', dependency, 'index.js'), null) + t.is(await context.resolveId(dependency, 'index.js'), null) } }) test('pnpm workspaces usage', async t => { process.chdir(fixture('02_workspaces/pnpm/one')) - const { plugin } = await initPlugin() + const context = await initPlugin() // Should be external for (const dependency of [ 'moment', // 02_workspaces/pnpm/one/package.json 'chalk' // 02_workspaces/pnpm/package.json ]) { - t.false(await callHook(plugin, 'resolveId', dependency, 'index.js')) + t.false(await context.resolveId(dependency, 'index.js')) } // Should be ignored @@ -41,20 +41,20 @@ test('pnpm workspaces usage', async t => { 'rollup', // 02_workspaces/package.json 'test-dep' // ./package.json ]) { - t.is(await callHook(plugin, 'resolveId', dependency, 'index.js'), null) + t.is(await context.resolveId(dependency, 'index.js'), null) } }) test('lerna usage', async t => { process.chdir(fixture('02_workspaces/lerna/one')) - const { plugin } = await initPlugin() + const plugin = await initPlugin() // Should be external for (const dependency of [ 'moment', // 02_workspaces/lerna/one/package.json 'chalk' // 02_workspaces/lerna/package.json ]) { - t.false(await callHook(plugin, 'resolveId', dependency, 'index.js')) + t.false(await plugin.resolveId(dependency, 'index.js')) } // Should be ignored @@ -63,6 +63,6 @@ test('lerna usage', async t => { 'rollup', // 02_workspaces/package.json 'test-dep' // ./package.json ]) { - t.is(await callHook(plugin, 'resolveId', dependency, 'index.js'), null) + t.is(await plugin.resolveId(dependency, 'index.js'), null) } })