Skip to content

Commit c74b88c

Browse files
committed
[v3.0] Always try to load config files via Node if possible (#4621)
* Support ES modules in bundles config files * Always try to load config files directly if possible * Fix build * Fix test * Debug test
1 parent fae9d80 commit c74b88c

File tree

115 files changed

+400
-222
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

115 files changed

+400
-222
lines changed

LICENSE.md

-29
Original file line numberDiff line numberDiff line change
@@ -240,35 +240,6 @@ Repository: jonschlinkert/fill-range
240240
241241
---------------------------------------
242242

243-
## get-package-type
244-
License: MIT
245-
By: Corey Farrell
246-
Repository: git+https://github.com/cfware/get-package-type.git
247-
248-
> MIT License
249-
>
250-
> Copyright (c) 2020 CFWare, LLC
251-
>
252-
> Permission is hereby granted, free of charge, to any person obtaining a copy
253-
> of this software and associated documentation files (the "Software"), to deal
254-
> in the Software without restriction, including without limitation the rights
255-
> to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
256-
> copies of the Software, and to permit persons to whom the Software is
257-
> furnished to do so, subject to the following conditions:
258-
>
259-
> The above copyright notice and this permission notice shall be included in all
260-
> copies or substantial portions of the Software.
261-
>
262-
> THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
263-
> IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
264-
> FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
265-
> AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
266-
> LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
267-
> OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
268-
> SOFTWARE.
269-
270-
---------------------------------------
271-
272243
## glob-parent
273244
License: ISC
274245
By: Gulp Team, Elan Shanker, Blaine Bublitz

build-plugins/clean-before-write.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { remove } from 'fs-extra';
1+
import fs from 'fs-extra';
22
import type { Plugin } from 'rollup';
33

44
export default function cleanBeforeWrite(dir: string): Plugin {
@@ -7,7 +7,7 @@ export default function cleanBeforeWrite(dir: string): Plugin {
77
generateBundle(_options, _bundle, isWrite) {
88
if (isWrite) {
99
// Only remove before first write, but make all writes wait on the removal
10-
removePromise ||= remove(dir);
10+
removePromise ||= fs.remove(dir);
1111
return removePromise;
1212
}
1313
},

build-plugins/copy-types.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { resolve } from 'node:path';
2-
import { readFile } from 'fs-extra';
2+
import fs from 'fs-extra';
33
import type { Plugin } from 'rollup';
44

55
export default function copyTypes(fileName: string): Plugin {
@@ -8,7 +8,7 @@ export default function copyTypes(fileName: string): Plugin {
88
if (isWrite) {
99
this.emitFile({
1010
fileName,
11-
source: await readFile(resolve('src/rollup/types.d.ts'), 'utf8'),
11+
source: await fs.readFile(resolve('src/rollup/types.d.ts'), 'utf8'),
1212
type: 'asset'
1313
});
1414
}

build-plugins/esm-dynamic-import.ts

+4-2
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
import type { Plugin } from 'rollup';
22

3+
const expectedImports = 3;
4+
35
export default function esmDynamicImport(): Plugin {
46
let importsFound = 0;
57
return {
68
generateBundle() {
7-
if (importsFound !== 2) {
9+
if (importsFound !== expectedImports) {
810
throw new Error(
9-
'Could not find 2 dynamic import in "loadConfigFile.ts" and "commandPlugin.ts", were the files renamed or modified?'
11+
`Could not find ${expectedImports} dynamic imports in "loadConfigFile.ts" and "commandPlugin.ts", found ${importsFound}.`
1012
);
1113
}
1214
},

build-plugins/get-banner.ts

+6-5
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import { exec } from 'node:child_process';
2+
import { promises as fs } from 'node:fs';
23
import { env } from 'node:process';
34
import { promisify } from 'node:util';
4-
import { version } from '../package.json';
55

66
const execPromise = promisify(exec);
77

8-
function generateBanner(commitHash: string): string {
8+
function generateBanner(commitHash: string, version: string): string {
99
const date = new Date(
1010
env.SOURCE_DATE_EPOCH ? 1000 * +env.SOURCE_DATE_EPOCH : Date.now()
1111
).toUTCString();
@@ -24,7 +24,8 @@ function generateBanner(commitHash: string): string {
2424
let getBannerPromise: Promise<string> | null = null;
2525

2626
export default async function getBanner(): Promise<string> {
27-
return (getBannerPromise ||= execPromise('git rev-parse HEAD').then(({ stdout }) =>
28-
generateBanner(stdout.trim())
29-
));
27+
return (getBannerPromise ||= Promise.all([
28+
execPromise('git rev-parse HEAD'),
29+
fs.readFile(new URL('../package.json', import.meta.url), 'utf8')
30+
]).then(([{ stdout }, pkg]) => generateBanner(stdout.trim(), JSON.parse(pkg).version)));
3031
}

cli/run/loadConfigFile.ts

+69-51
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,32 @@
1-
import { extname, isAbsolute } from 'node:path';
1+
import { promises as fs } from 'node:fs';
2+
import { dirname, isAbsolute, join } from 'node:path';
3+
import process from 'node:process';
24
import { pathToFileURL } from 'node:url';
3-
import getPackageType from 'get-package-type';
45
import * as rollup from '../../src/node-entry';
56
import type { MergedRollupOptions } from '../../src/rollup/types';
67
import { bold } from '../../src/utils/colors';
7-
import { errMissingConfig, error, errTranspiledEsmConfig } from '../../src/utils/error';
8+
import {
9+
errCannotBundleConfigAsEsm,
10+
errCannotLoadConfigAsCjs,
11+
errCannotLoadConfigAsEsm,
12+
errMissingConfig,
13+
error
14+
} from '../../src/utils/error';
815
import { mergeOptions } from '../../src/utils/options/mergeOptions';
916
import type { GenericConfigObject } from '../../src/utils/options/options';
1017
import relativeId from '../../src/utils/relativeId';
1118
import { stderr } from '../logging';
1219
import batchWarnings, { type BatchWarnings } from './batchWarnings';
1320
import { addCommandPluginsToInputOptions, addPluginsFromCommandOption } from './commandPlugins';
1421

15-
interface NodeModuleWithCompile extends NodeModule {
16-
_compile(code: string, filename: string): any;
17-
}
18-
1922
export async function loadConfigFile(
2023
fileName: string,
2124
commandOptions: any = {}
2225
): Promise<{ options: MergedRollupOptions[]; warnings: BatchWarnings }> {
23-
const configs = await loadConfigsFromFile(fileName, commandOptions);
26+
const configs = await getConfigList(
27+
getDefaultFromCjs(await getConfigFileExport(fileName, commandOptions)),
28+
commandOptions
29+
);
2430
const warnings = batchWarnings();
2531
try {
2632
const normalizedConfigs: MergedRollupOptions[] = [];
@@ -36,29 +42,51 @@ export async function loadConfigFile(
3642
}
3743
}
3844

39-
async function loadConfigsFromFile(
40-
fileName: string,
41-
commandOptions: Record<string, unknown>
42-
): Promise<GenericConfigObject[]> {
43-
const extension = extname(fileName);
44-
45-
const configFileExport =
46-
commandOptions.configPlugin ||
47-
// We always transpile the .js non-module case because many legacy code bases rely on this
48-
(extension === '.js' && getPackageType.sync(fileName) !== 'module')
49-
? await getDefaultFromTranspiledConfigFile(fileName, commandOptions)
50-
: getDefaultFromCjs((await import(pathToFileURL(fileName).href)).default);
51-
52-
return getConfigList(configFileExport, commandOptions);
45+
async function getConfigFileExport(fileName: string, commandOptions: Record<string, unknown>) {
46+
if (commandOptions.configPlugin || commandOptions.bundleConfigAsCjs) {
47+
try {
48+
return await loadTranspiledConfigFile(fileName, commandOptions);
49+
} catch (err: any) {
50+
if (err.message.includes('not defined in ES module scope')) {
51+
return error(errCannotBundleConfigAsEsm(err));
52+
}
53+
throw err;
54+
}
55+
}
56+
let cannotLoadEsm = false;
57+
const handleWarning = (warning: Error): void => {
58+
if (warning.message.includes('To load an ES module')) {
59+
cannotLoadEsm = true;
60+
}
61+
};
62+
process.on('warning', handleWarning);
63+
try {
64+
const fileUrl = pathToFileURL(fileName);
65+
if (process.env.ROLLUP_WATCH) {
66+
// We are adding the current date to allow reloads in watch mode
67+
fileUrl.search = `?${Date.now()}`;
68+
}
69+
return (await import(fileUrl.href)).default;
70+
} catch (err: any) {
71+
if (cannotLoadEsm) {
72+
return error(errCannotLoadConfigAsCjs(err));
73+
}
74+
if (err.message.includes('not defined in ES module scope')) {
75+
return error(errCannotLoadConfigAsEsm(err));
76+
}
77+
throw err;
78+
} finally {
79+
process.off('warning', handleWarning);
80+
}
5381
}
5482

5583
function getDefaultFromCjs(namespace: GenericConfigObject): unknown {
56-
return namespace.__esModule ? namespace.default : namespace;
84+
return namespace.default || namespace;
5785
}
5886

59-
async function getDefaultFromTranspiledConfigFile(
87+
async function loadTranspiledConfigFile(
6088
fileName: string,
61-
commandOptions: Record<string, unknown>
89+
{ bundleConfigAsCjs, configPlugin, silent }: Record<string, unknown>
6290
): Promise<unknown> {
6391
const warnings = batchWarnings();
6492
const inputOptions = {
@@ -69,17 +97,17 @@ async function getDefaultFromTranspiledConfigFile(
6997
plugins: [],
7098
treeshake: false
7199
};
72-
await addPluginsFromCommandOption(commandOptions.configPlugin, inputOptions);
100+
await addPluginsFromCommandOption(configPlugin, inputOptions);
73101
const bundle = await rollup.rollup(inputOptions);
74-
if (!commandOptions.silent && warnings.count > 0) {
102+
if (!silent && warnings.count > 0) {
75103
stderr(bold(`loaded ${relativeId(fileName)} with warnings`));
76104
warnings.flush();
77105
}
78106
const {
79107
output: [{ code }]
80108
} = await bundle.generate({
81109
exports: 'named',
82-
format: 'cjs',
110+
format: bundleConfigAsCjs ? 'cjs' : 'es',
83111
plugins: [
84112
{
85113
name: 'transpile-import-meta',
@@ -94,32 +122,22 @@ async function getDefaultFromTranspiledConfigFile(
94122
}
95123
]
96124
});
97-
return loadConfigFromBundledFile(fileName, code);
125+
return loadConfigFromWrittenFile(
126+
join(dirname(fileName), `rollup.config-${Date.now()}.${bundleConfigAsCjs ? 'cjs' : 'mjs'}`),
127+
code
128+
);
98129
}
99130

100-
function loadConfigFromBundledFile(fileName: string, bundledCode: string): unknown {
101-
const resolvedFileName = require.resolve(fileName);
102-
const extension = extname(resolvedFileName);
103-
const defaultLoader = require.extensions[extension];
104-
require.extensions[extension] = (module: NodeModule, requiredFileName: string) => {
105-
if (requiredFileName === resolvedFileName) {
106-
(module as NodeModuleWithCompile)._compile(bundledCode, requiredFileName);
107-
} else {
108-
if (defaultLoader) {
109-
defaultLoader(module, requiredFileName);
110-
}
111-
}
112-
};
113-
delete require.cache[resolvedFileName];
131+
async function loadConfigFromWrittenFile(
132+
bundledFileName: string,
133+
bundledCode: string
134+
): Promise<unknown> {
135+
await fs.writeFile(bundledFileName, bundledCode);
114136
try {
115-
const config = getDefaultFromCjs(require(fileName));
116-
require.extensions[extension] = defaultLoader;
117-
return config;
118-
} catch (err: any) {
119-
if (err.code === 'ERR_REQUIRE_ESM') {
120-
return error(errTranspiledEsmConfig(fileName));
121-
}
122-
throw err;
137+
return (await import(pathToFileURL(bundledFileName).href)).default;
138+
} finally {
139+
// Not awaiting here saves some ms while potentially hiding a non-critical error
140+
fs.unlink(bundledFileName);
123141
}
124142
}
125143

docs/01-command-line-reference.md

+12-6
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,9 @@ export default {
1818
};
1919
```
2020

21-
Typically, it is called `rollup.config.js` or `rollup.config.mjs` and sits in the root directory of your project. If you use the `.mjs` extension or have `type: "module"` in your `package.json` file, Rollup will directly use Node to import it, which is now the recommended way to define Rollup configurations. Note that there are some [caveats when using native Node ES modules](guide/en/#caveats-when-using-native-node-es-modules);
21+
Typically, it is called `rollup.config.js` or `rollup.config.mjs` and sits in the root directory of your project. Unless the [`--configPlugin`](guide/en/#--configplugin-plugin) or [`--bundleConfigAsCjs`](guide/en/#--bundleconfigascjs) options are used, Rollup will directly use Node to import the file. Note that there are some [caveats when using native Node ES modules](guide/en/#caveats-when-using-native-node-es-modules) as Rollup will observe [Node ESM semantics](https://nodejs.org/docs/latest-v14.x/api/packages.html#packages_determining_module_system).
2222

23-
Otherwise, Rollup will transpile and bundle this file and its relative dependencies to CommonJS before requiring it to ensure compatibility with legacy code bases that use ES module syntax without properly respecting [Node ESM semantics](https://nodejs.org/docs/latest-v14.x/api/packages.html#packages_determining_module_system).
24-
25-
If you want to write your configuration as a CommonJS module using `require` and `module.exports`, you should change the file extension to `.cjs`, which will prevent Rollup from trying to transpile the CommonJS file.
23+
If you want to write your configuration as a CommonJS module using `require` and `module.exports`, you should change the file extension to `.cjs`.
2624

2725
You can also use other languages for your configuration files like TypeScript. To do that, install a corresponding Rollup plugin like `@rollup/plugin-typescript` and use the [`--configPlugin`](guide/en/#--configplugin-plugin) option:
2826

@@ -245,7 +243,7 @@ Besides `RollupOptions` and the `defineConfig` helper that encapsulates this typ
245243
- `Plugin`: A plugin object that provides a `name` and some hooks. All hooks are fully typed to aid in plugin development.
246244
- `PluginImpl`: A function that maps an options object to a plugin object. Most public Rollup plugins follow this pattern.
247245

248-
You can also directly write your config in TypeScript via the [`--configPlugin`](guide/en/#--configplugin-plugin) option. With TypeScript you can import the `RollupOptions` type directly:
246+
You can also directly write your config in TypeScript via the [`--configPlugin`](guide/en/#--configplugin-plugin) option. With TypeScript, you can import the `RollupOptions` type directly:
249247

250248
```typescript
251249
import type { RollupOptions } from 'rollup';
@@ -480,7 +478,15 @@ Note for Typescript: make sure you have the Rollup config file in your `tsconfig
480478
"include": ["src/**/*", "rollup.config.ts"],
481479
```
482480

483-
This option supports the same syntax as the [`--plugin`](guide/en/#-p-plugin---plugin-plugin) option i.e., you can specify the option multiple times, you can omit the `@rollup/plugin-` prefix and just write `typescript` and you can specify plugin options via `={...}`. Using this option will make Rollup transpile your configuration file to CommonJS first before executing it.
481+
This option supports the same syntax as the [`--plugin`](guide/en/#-p-plugin---plugin-plugin) option i.e., you can specify the option multiple times, you can omit the `@rollup/plugin-` prefix and just write `typescript` and you can specify plugin options via `={...}`.
482+
483+
Using this option will make Rollup transpile your configuration file to an ES module first before executing it. To transpile to CommonJS instead, also pass the [`--bundleConfigAsCjs`](guide/en/#--bundleconfigascjs) option.
484+
485+
#### `--bundleConfigAsCjs`
486+
487+
This option will force your configuration to be transpiled to CommonJS.
488+
489+
This allows you to use CommonJS idioms like `__dirname` or `require.resolve` in your configuration even if the configuration itself is written as an ES module.
484490

485491
#### `-v`/`--version`
486492

package-lock.json

+3-4
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+1-2
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,6 @@
8585
"eslint-plugin-prettier": "^4.2.1",
8686
"fixturify": "^2.1.1",
8787
"fs-extra": "^10.1.0",
88-
"get-package-type": "^0.1.0",
8988
"github-api": "^3.4.0",
9089
"hash.js": "^1.1.7",
9190
"husky": "^8.0.1",
@@ -125,7 +124,7 @@
125124
"dist/es/package.json"
126125
],
127126
"engines": {
128-
"node": ">=14.13.1",
127+
"node": ">=14.18.0",
129128
"npm": ">=8.0.0"
130129
},
131130
"exports": {

src/rollup/types.d.ts

+1
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ export interface RollupLog {
2828
pluginCode?: string;
2929
pos?: number;
3030
reexporter?: string;
31+
stack?: string;
3132
url?: string;
3233
}
3334

0 commit comments

Comments
 (0)