diff --git a/packages/multi-entry/package.json b/packages/multi-entry/package.json index 1f40bb55d..641e74379 100755 --- a/packages/multi-entry/package.json +++ b/packages/multi-entry/package.json @@ -61,7 +61,7 @@ }, "dependencies": { "@rollup/plugin-virtual": "^3.0.0", - "matched": "^5.0.1" + "tinyglobby": "^0.2.14" }, "devDependencies": { "rollup": "^4.0.0-24" diff --git a/packages/multi-entry/rollup.config.mjs b/packages/multi-entry/rollup.config.mjs index 80558c224..824d64250 100755 --- a/packages/multi-entry/rollup.config.mjs +++ b/packages/multi-entry/rollup.config.mjs @@ -1,11 +1,13 @@ import { readFileSync } from 'fs'; +import typescript from '@rollup/plugin-typescript'; + import { createConfig } from '../../shared/rollup.config.mjs'; export default { ...createConfig({ pkg: JSON.parse(readFileSync(new URL('./package.json', import.meta.url), 'utf8')) }), - input: 'src/index.js', - plugins: [] + input: 'src/index.ts', + plugins: [typescript()] }; diff --git a/packages/multi-entry/src/index.js b/packages/multi-entry/src/index.js deleted file mode 100755 index c53f6e8fd..000000000 --- a/packages/multi-entry/src/index.js +++ /dev/null @@ -1,79 +0,0 @@ -/* eslint-disable no-param-reassign */ - -import virtual from '@rollup/plugin-virtual'; -import { promise as matched } from 'matched'; - -const DEFAULT_OUTPUT = 'multi-entry.js'; -const AS_IMPORT = 'import'; -const AS_EXPORT = 'export * from'; - -export default function multiEntry(conf = {}) { - const config = { - include: [], - exclude: [], - entryFileName: DEFAULT_OUTPUT, - exports: true, - ...conf - }; - - let prefix = config.exports === false ? AS_IMPORT : AS_EXPORT; - const exporter = (path) => `${prefix} ${JSON.stringify(path)}`; - - const configure = (input) => { - if (typeof input === 'string') { - config.include = [input]; - } else if (Array.isArray(input)) { - config.include = input; - } else { - const { include = [], exclude = [], entryFileName = DEFAULT_OUTPUT, exports } = input; - config.include = include; - config.exclude = exclude; - config.entryFileName = entryFileName; - if (exports === false) { - prefix = AS_IMPORT; - } - } - }; - - let virtualisedEntry; - - return { - name: 'multi-entry', - - options(options) { - if (options.input !== config.entryFileName) { - configure(options.input); - } - return { - ...options, - input: config.entryFileName - }; - }, - - outputOptions(options) { - return { - ...options, - entryFileNames: config.preserveModules ? options.entryFileNames : config.entryFileName - }; - }, - - buildStart(options) { - const patterns = config.include.concat(config.exclude.map((pattern) => `!${pattern}`)); - const entries = patterns.length - ? matched(patterns, { realpath: true }).then((paths) => - paths.sort().map(exporter).join('\n') - ) - : Promise.resolve(''); - - virtualisedEntry = virtual({ [options.input]: entries }); - }, - - resolveId(id, importer) { - return virtualisedEntry && virtualisedEntry.resolveId(id, importer); - }, - - load(id) { - return virtualisedEntry && virtualisedEntry.load(id); - } - }; -} diff --git a/packages/multi-entry/src/index.ts b/packages/multi-entry/src/index.ts new file mode 100755 index 000000000..82ab026c8 --- /dev/null +++ b/packages/multi-entry/src/index.ts @@ -0,0 +1,82 @@ +import virtual from '@rollup/plugin-virtual'; +import { glob } from 'tinyglobby'; + +import type { Plugin } from 'rollup'; + +import type { RollupMultiEntryOptions } from '../types'; + +import { extractDirectories } from './utils'; + +const DEFAULT_OUTPUT = 'multi-entry.js'; +const AS_IMPORT = 'import'; +const AS_EXPORT = 'export * from'; + +export default function multiEntry(config: RollupMultiEntryOptions = {}): Plugin { + let entryFileName = config.entryFileName ?? DEFAULT_OUTPUT; + let include: string[] = []; + let exclude: string[] = []; + let exports = config.exports ?? true; + + const exporter = (path: string) => `${exports ? AS_EXPORT : AS_IMPORT} ${JSON.stringify(path)}`; + + let virtualisedEntry: { + resolveId(id: string, importer?: string): string | null; + load(id: string): string | null; + }; + + return { + name: 'multi-entry', + + options(options) { + if (options.input !== entryFileName) { + if (typeof options.input === 'string') { + include = [options.input]; + } else if (Array.isArray(options.input)) { + include = options.input; + } else if (options.input) { + // Consider options.input as a configuration object for this plugin instead + // of an `{ [entryAlias: string]: string; }` map object + const input = options.input as RollupMultiEntryOptions; + entryFileName = input.entryFileName ?? DEFAULT_OUTPUT; + include = typeof input.include === 'string' ? [input.include] : input.include ?? []; + exclude = typeof input.exclude === 'string' ? [input.exclude] : input.exclude ?? []; + exports = input.exports ?? true; + } + } + + return { + ...options, + input: entryFileName + }; + }, + + outputOptions(options) { + return { + ...options, + entryFileNames: config.preserveModules ? options.entryFileNames : entryFileName + }; + }, + + async buildStart(options) { + const patterns = include.concat(exclude.map((pattern) => `!${pattern}`)); + const entries = patterns.length + ? glob(patterns, { absolute: true }) + .then((paths) => paths.sort()) + .then((paths) => paths.map(exporter).join('\n')) + : Promise.resolve(''); + virtualisedEntry = virtual({ [options.input as unknown as string]: await entries }) as any; + + if (this.meta.watchMode) { + for (const dir of extractDirectories(patterns)) this.addWatchFile(dir); + } + }, + + resolveId(id, importer) { + return virtualisedEntry && virtualisedEntry.resolveId(id, importer); + }, + + load(id) { + return virtualisedEntry && virtualisedEntry.load(id); + } + }; +} diff --git a/packages/multi-entry/src/utils.ts b/packages/multi-entry/src/utils.ts new file mode 100644 index 000000000..fff594069 --- /dev/null +++ b/packages/multi-entry/src/utils.ts @@ -0,0 +1,37 @@ +/* eslint-disable no-irregular-whitespace, no-continue */ +import { isDynamicPattern } from 'tinyglobby'; + +/** + * Transforms an array of patterns into an array of static directories. + * + * @example + * ["./src​/**​/*.js"] -> ["./src"] + * ["./{lib,utils}/index.js"] -> ["."] + */ +export function extractDirectories(patterns: string[]): string[] { + const directories = new Set(); + + for (const pattern of patterns) { + // Skip negated patterns + if (pattern.startsWith('!')) continue; + + const parts = pattern.split(/\/|\\/g); + let [dir] = parts; + + // If the pattern is dynamic from the beginning, skip it + if (isDynamicPattern(dir)) continue; + + // Join all the parts until the pattern is dynamic + for (const part of parts.slice(1)) { + const newDir = `${dir}/${part}`; + if (isDynamicPattern(newDir)) { + directories.add(dir); + break; + } + dir = newDir; + } + directories.add(dir); + } + + return [...directories]; +} diff --git a/packages/multi-entry/test/test.mjs b/packages/multi-entry/test/test.mjs index 399062053..03d70d495 100755 --- a/packages/multi-entry/test/test.mjs +++ b/packages/multi-entry/test/test.mjs @@ -147,3 +147,40 @@ test('deterministic output, regardless of input order', async (t) => { t.is(code1, code2); }); + +test('correctly extracts watch directories from glob patterns', async (t) => { + const plugin = multiEntry(); + const options = plugin.options({ + input: ['test/fixtures/*.js', 'src/**/*.js', './lib/{util,helper}.js'] + }); + + const watchedDirs = []; + await plugin.buildStart.call( + { + meta: { watchMode: true }, + addWatchFile: (dir) => { + watchedDirs.push(dir); + } + }, + options + ); + + t.deepEqual(watchedDirs, ['test/fixtures', 'src', './lib']); +}); + +test('does not watch directories when not in watch mode', async (t) => { + const plugin = multiEntry(); + const options = plugin.options({ input: 'test/fixtures/*.js' }); + + await plugin.buildStart.call( + { + meta: { watchMode: false }, + addWatchFile: () => { + t.fail('Should not call addWatchFile when not in watch mode'); + } + }, + options + ); + + t.pass('Should not attempt to watch files when not in watch mode'); +}); diff --git a/packages/multi-entry/types/index.d.ts b/packages/multi-entry/types/index.d.ts index 608ca9dc6..d8c909153 100644 --- a/packages/multi-entry/types/index.d.ts +++ b/packages/multi-entry/types/index.d.ts @@ -1,19 +1,18 @@ -import type { FilterPattern } from '@rollup/pluginutils'; import type { Plugin } from 'rollup'; -interface RollupMultiEntryOptions { +export interface RollupMultiEntryOptions { /** * A minimatch pattern, or array of patterns, which specifies the files in the build the plugin * should operate on. * By default all files are targeted. */ - include?: FilterPattern; + include?: string | string[]; /** * A minimatch pattern, or array of patterns, which specifies the files in the build the plugin * should _ignore_. * By default no files are ignored. */ - exclude?: FilterPattern; + exclude?: string | string[]; /** * - If `true`, instructs the plugin to export named exports to the bundle from all entries. * - If `false`, the plugin will not export any entry exports to the bundle. diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0692e1247..5c40c2454 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -477,9 +477,9 @@ importers: '@rollup/plugin-virtual': specifier: ^3.0.0 version: 3.0.0(rollup@4.0.0-24) - matched: - specifier: ^5.0.1 - version: 5.0.1 + tinyglobby: + specifier: ^0.2.14 + version: 0.2.14 devDependencies: rollup: specifier: ^4.0.0-24 @@ -2785,6 +2785,14 @@ packages: picomatch: optional: true + fdir@6.4.6: + resolution: {integrity: sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + figures@4.0.1: resolution: {integrity: sha512-rElJwkA/xS04Vfg+CaZodpso7VqBknOYbzi6I76hI4X80RUjkSxO2oAyPmGbuXUppywjqndOrQDl817hDnI++w==} engines: {node: '>=12'} @@ -3428,10 +3436,6 @@ packages: resolution: {integrity: sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==} engines: {node: '>=8'} - matched@5.0.1: - resolution: {integrity: sha512-E1fhSTPRyhAlNaNvGXAgZQlq1hL0bgYMTk/6bktVlIhzUnX/SZs7296ACdVeNJE8xFNGSuvd9IpI7vSnmcqLvw==} - engines: {node: '>=10'} - matcher@5.0.0: resolution: {integrity: sha512-s2EMBOWtXFc8dgqvoAzKJXxNHibcdJMV0gwqKUaw9E2JBJuGUK7DrNKrA6g/i+v72TT16+6sVm5mS3thaMLQUw==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -4481,6 +4485,10 @@ packages: resolution: {integrity: sha512-TIsDdtKo6+XrPtiTm1ssmMngN1sAhyKnTO2kunQWqNPWIVvCm15Wmw4SWInwTVgJ5u/Tr04+8Ei9TNcw4x4ONA==} engines: {node: '>=4'} + tinyglobby@0.2.14: + resolution: {integrity: sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==} + engines: {node: '>=12.0.0'} + to-fast-properties@2.0.0: resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==} engines: {node: '>=4'} @@ -7091,6 +7099,10 @@ snapshots: optionalDependencies: picomatch: 4.0.2 + fdir@6.4.6(picomatch@4.0.2): + optionalDependencies: + picomatch: 4.0.2 + figures@4.0.1: dependencies: escape-string-regexp: 5.0.0 @@ -7693,11 +7705,6 @@ snapshots: map-obj@4.3.0: {} - matched@5.0.1: - dependencies: - glob: 7.2.3 - picomatch: 2.3.1 - matcher@5.0.0: dependencies: escape-string-regexp: 5.0.0 @@ -8780,6 +8787,11 @@ snapshots: time-zone@1.0.0: {} + tinyglobby@0.2.14: + dependencies: + fdir: 6.4.6(picomatch@4.0.2) + picomatch: 4.0.2 + to-fast-properties@2.0.0: {} to-regex-range@5.0.1: