Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/multi-entry/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
6 changes: 4 additions & 2 deletions packages/multi-entry/rollup.config.mjs
Original file line number Diff line number Diff line change
@@ -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()]
};
79 changes: 0 additions & 79 deletions packages/multi-entry/src/index.js

This file was deleted.

82 changes: 82 additions & 0 deletions packages/multi-entry/src/index.ts
Original file line number Diff line number Diff line change
@@ -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);
}
};
}
37 changes: 37 additions & 0 deletions packages/multi-entry/src/utils.ts
Original file line number Diff line number Diff line change
@@ -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<string>();

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];
}
37 changes: 37 additions & 0 deletions packages/multi-entry/test/test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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');
});
7 changes: 3 additions & 4 deletions packages/multi-entry/types/index.d.ts
Original file line number Diff line number Diff line change
@@ -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[];
Copy link
Contributor Author

@GauBen GauBen Jul 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not a breaking change, providing a regex would cause an undefined behavior at this line:

const patterns = config.include.concat(config.exclude.map((pattern) => `!${pattern}`));

/**
* - 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.
Expand Down
36 changes: 24 additions & 12 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading