Skip to content

Commit

Permalink
[v3.0] Better esm config file support (#4574)
Browse files Browse the repository at this point in the history
* More precise native ESM support check

* Use import to load .js config file if package type is module

* Update and add tests handling type module

* Remove Node version check and simplify logic

* Update documentation

* Document how to replace __dirname and import JSON

Co-authored-by: Linus Miller <linus.miller@bitmill.io>
  • Loading branch information
lukastaegert and lohfu committed Jul 20, 2022
1 parent 89dbd5d commit 48b366b
Show file tree
Hide file tree
Showing 15 changed files with 136 additions and 68 deletions.
29 changes: 29 additions & 0 deletions LICENSE.md
Expand Up @@ -240,6 +240,35 @@ Repository: jonschlinkert/fill-range
---------------------------------------

## get-package-type
License: MIT
By: Corey Farrell
Repository: git+https://github.com/cfware/get-package-type.git

> MIT License
>
> Copyright (c) 2020 CFWare, LLC
>
> Permission is hereby granted, free of charge, to any person obtaining a copy
> of this software and associated documentation files (the "Software"), to deal
> in the Software without restriction, including without limitation the rights
> to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
> copies of the Software, and to permit persons to whom the Software is
> furnished to do so, subject to the following conditions:
>
> The above copyright notice and this permission notice shall be included in all
> copies or substantial portions of the Software.
>
> THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
> IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
> FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
> AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
> LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
> OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
> SOFTWARE.
---------------------------------------

## glob-parent
License: ISC
By: Gulp Team, Elan Shanker, Blaine Bublitz
Expand Down
13 changes: 4 additions & 9 deletions cli/run/loadConfigFile.ts
@@ -1,6 +1,6 @@
import { extname, isAbsolute } from 'path';
import { version } from 'process';
import { pathToFileURL } from 'url';
import getPackageType from 'get-package-type';
import * as rollup from '../../src/node-entry';
import type { MergedRollupOptions } from '../../src/rollup/types';
import { bold } from '../../src/utils/colors';
Expand All @@ -12,10 +12,6 @@ import { stderr } from '../logging';
import batchWarnings, { type BatchWarnings } from './batchWarnings';
import { addCommandPluginsToInputOptions, addPluginsFromCommandOption } from './commandPlugins';

function supportsNativeESM(): boolean {
return Number(/^v(\d+)/.exec(version)![1]) >= 13;
}

interface NodeModuleWithCompile extends NodeModule {
_compile(code: string, filename: string): any;
}
Expand Down Expand Up @@ -48,11 +44,10 @@ async function loadConfigFile(

const configFileExport =
commandOptions.configPlugin ||
!(extension === '.cjs' || (extension === '.mjs' && supportsNativeESM()))
// We always transpile the .js non-module case because many legacy code bases rely on this
(extension === '.js' && getPackageType.sync(fileName) !== 'module')
? await getDefaultFromTranspiledConfigFile(fileName, commandOptions)
: extension === '.cjs'
? getDefaultFromCjs(require(fileName))
: (await import(pathToFileURL(fileName).href)).default;
: getDefaultFromCjs((await import(pathToFileURL(fileName).href)).default);

return getConfigList(configFileExport, commandOptions);
}
Expand Down
104 changes: 56 additions & 48 deletions docs/01-command-line-reference.md
Expand Up @@ -18,17 +18,19 @@ export default {
};
```

Typically, it is called `rollup.config.js` and sits in the root directory of your project. Behind the scenes, Rollup will usually transpile and bundle this file and its relative dependencies to CommonJS before requiring it. This has the advantage that you can share code with an ES module code base while having full interoperability with the Node ecosystem.
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);

If you want to write your config 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 file. Furthermore if you are on Node 13+, changing the file extension to `.mjs` will also prevent Rollup from transpiling it but import the file as an ES module instead. See [using untranspiled config files](guide/en/#using-untranspiled-config-files) for more details and why you might want to do this.
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).

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.

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:

```
rollup --config rollup.config.ts --configPlugin typescript
```

Also have a look at [Config Intellisense](guide/en/#config-intellisense) for more ways to use TypeScript typings in your config files.
Using the `--configPlugin` option will always force your config file to be transpiled to CommonJS first. Also have a look at [Config Intellisense](guide/en/#config-intellisense) for more ways to use TypeScript typings in your config files.

Config files support the options listed below. Consult the [big list of options](guide/en/#big-list-of-options) for details on each option:

Expand Down Expand Up @@ -266,66 +268,72 @@ For interoperability, Rollup also supports loading configuration files from pack
rollup --config node:my-special-config
```

### Using untranspiled config files
### Caveats when using native Node ES modules

By default, Rollup will expect config files to be ES modules and bundle and transpile them and their relative imports to CommonJS before requiring them. This is a fast process and has the advantage that it is easy to share code between your configuration and an ES module code base. If you want to write your configuration as CommonJS instead, you can skip this process by using the `.cjs` extension:
Especially when upgrading from an older Rollup version, there are some things you need to be aware of when using a native ES module for your configuration file.

```javascript
// rollup.config.cjs
module.exports = {
input: 'src/main.js',
output: {
file: 'bundle.js',
format: 'cjs'
}
};
```
#### Getting the current directory

It may be pertinent if you want to use the config file not only from the command line, but also from your custom scripts programmatically.
With CommonJS files, people often use `__dirname` to access the current directory and resolve relative paths to absolute paths. This is not supported for native ES modules. Instead, we recommend the following approach e.g. to generate an absolute id for an external module:

On the other hand if you are using at least Node 13 and have `"type": "module"` in your `package.json` file, Rollup's transpilation will prevent your configuration file from importing packages that are themselves ES modules. In that case, changing your file extension to `.mjs` will instruct Rollup to import your configuration directly as an ES module. However, note that this is specific to Node 13+; on older Node versions, `.mjs` is treated just like `.js`.
```js
// rollup.config.js
import { fileURLToPath } from 'url'

There are some potential gotchas when using `.mjs` on Node 13+:
export default {
...,
// generates an absolute path for <currentdir>/src/some-external-file.js
external: [fileURLToPath(new URL('src/some-external-file.js', import.meta.url))]
};
```
- You will only get a default export from CommonJS plugins
- You may not be able to import JSON files such as your `package.json file`. There are four ways to go around this:
#### Importing package.json
- read and parse the JSON file yourself via
It can be useful to import your package file to e.g. mark your dependencies as "external" automatically. Depending on your Node version, there are different ways of doing that:
```
// rollup.config.mjs
import { readFileSync } from 'fs';
- For Node 17.5+, you can use an import assertion
const packageJson = JSON.parse(readFileSync('./package.json'));
...
```
```js
import pkg from './package.json' assert { type: 'json' };

- use `createRequire` via
export default {
input: 'src/main.js',
external: Object.keys(pkg.dependencies),
output: {
format: 'es',
dir: 'dist'
}
};
```
```
// rollup.config.mjs
import { createRequire } from 'module';
const require = createRequire(import.meta.url);
const packageJson = require('./package.json');
...
```
- For older Node version, you can use "createRequire"
- run Rollup CLI via
```js
import { createRequire } from 'module';
const require = createRequire(import.meta.url);
const pkg = require('./package.json');

```
node --experimental-json-modules ./node_modules/.bin/rollup --config
```
export default {
input: 'src/main.js',
external: Object.keys(pkg.dependencies),
output: {
format: 'es',
dir: 'dist'
}
};
```
- create a CommonJS wrapper that requires the JSON file:
- Or just directly read and parse the file from disk
```js
// load-package.cjs
module.exports = require('./package.json');
```js
// rollup.config.mjs
import { readFileSync } from 'fs';
import { fileURLToPath } from 'url';

// rollup.config.mjs
import pkg from './load-package.cjs';
...
```
const pkgFileName = fileURLToPath(new URL('./package.json', import.meta.url));
const pkg = JSON.parse(readFileSync(pkgFileName));
// ...
```
### Command line flags
Expand Down Expand Up @@ -473,7 +481,7 @@ Note for Typescript: make sure you have the Rollup config file in your `tsconfig
"include": ["src/**/*", "rollup.config.ts"],
```

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 `={...}`.
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.

#### `-v`/`--version`

Expand Down
8 changes: 4 additions & 4 deletions docs/999-big-list-of-options.md
Expand Up @@ -15,13 +15,13 @@ Either a function that takes an `id` and returns `true` (external) or `false` (n

```js
// rollup.config.js
import path from 'path';
import { fileURLToPath } from 'url'

export default {
...,
external: [
'some-externally-required-library',
path.resolve( __dirname, 'src/some-local-file-that-should-not-be-bundled.js' ),
fileURLToPath(new URL('src/some-local-file-that-should-not-be-bundled.js', import.meta.url)),
/node_modules/
]
};
Expand Down Expand Up @@ -183,8 +183,8 @@ To tell Rollup that a local file should be replaced by a global variable, use an
```js
// rollup.config.js
import path from 'path';
const externalId = path.resolve( __dirname, 'src/some-local-file-that-should-not-be-bundled.js' );
import { fileURLToPath } from 'url'
const externalId = fileURLToPath(new URL('src/some-local-file-that-should-not-be-bundled.js', import.meta.url))

export default {
...,
Expand Down
1 change: 1 addition & 0 deletions package-lock.json

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

1 change: 1 addition & 0 deletions package.json
Expand Up @@ -87,6 +87,7 @@
"execa": "^6.1.0",
"fixturify": "^2.1.1",
"fs-extra": "^10.1.0",
"get-package-type": "^0.1.0",
"hash.js": "^1.1.7",
"husky": "^8.0.1",
"is-reference": "^3.0.0",
Expand Down
3 changes: 1 addition & 2 deletions scripts/perf-init.js
@@ -1,12 +1,11 @@
/* eslint-disable no-console */

import path, { dirname } from 'path';
import { fileURLToPath } from 'url';
import { execa } from 'execa';
import fs from 'fs-extra';
import { findConfigFileName } from './find-config.js';

const TARGET_DIR = path.resolve(dirname(fileURLToPath(import.meta.url)), '..', 'perf');
const TARGET_DIR = fileURLToPath(new URL('../perf', import.meta.url).href);
const VALID_REPO = /^([^/\s#]+\/[^/\s#]+)(#([^/\s#]+))?$/;
const repoWithBranch = process.argv[2];

Expand Down
5 changes: 2 additions & 3 deletions scripts/perf.js
Expand Up @@ -2,7 +2,6 @@
/* global gc */

import { readFileSync, writeFileSync } from 'fs';
import path, { dirname } from 'path';
import { cwd } from 'process';
import { fileURLToPath } from 'url';
import { createColors } from 'colorette';
Expand All @@ -12,8 +11,8 @@ import { rollup } from '../dist/rollup.js';
import { findConfigFileName } from './find-config.js';

const initialDir = cwd();
const targetDir = path.resolve(dirname(fileURLToPath(import.meta.url)), '..', 'perf');
const perfFile = path.resolve(targetDir, 'rollup.perf.json');
const targetDir = fileURLToPath(new URL('../perf', import.meta.url).href);
const perfFile = fileURLToPath(new URL('../perf/rollup.perf.json', import.meta.url).href);
const { bold, underline, cyan, red, green } = createColors();
const MIN_ABSOLUTE_TIME_DEVIATION = 10;
const RELATIVE_DEVIATION_FOR_COLORING = 5;
Expand Down
2 changes: 1 addition & 1 deletion test/cli/samples/config-no-module/_config.js
Expand Up @@ -3,7 +3,7 @@ const { assertIncludes } = require('../../../utils.js');
module.exports = {
description: 'provides a helpful error message if a transpiled config is interpreted as "module"',
minNodeVersion: 13,
command: 'cd sub && rollup -c',
command: 'rollup -c',
error: () => true,
stderr: stderr =>
assertIncludes(
Expand Down
@@ -1,7 +1,7 @@
import { shebang } from 'rollup-plugin-thatworks';

export default {
input: 'main.js',
input: './sub/main.js',
output: { format: 'cjs' },
plugins: [shebang()]
};
18 changes: 18 additions & 0 deletions test/cli/samples/config-type-module/_config.js
@@ -0,0 +1,18 @@
const { assertIncludes } = require('../../../utils.js');

module.exports = {
description: 'tries to load .js config file if package type is "module"',
command: 'cd sub && rollup -c rollup.config.js',
error: () => true,
stderr: stderr => {
assertIncludes(
stderr,
'[!] ReferenceError: module is not defined in ES module scope\n' +
"This file is being treated as an ES module because it has a '.js' file extension and"
);
assertIncludes(
stderr,
'contains "type": "module". To treat it as a CommonJS script, rename it to use the \'.cjs\' file extension.'
);
}
};
1 change: 1 addition & 0 deletions test/cli/samples/config-type-module/sub/main.js
@@ -0,0 +1 @@
console.log(42);
3 changes: 3 additions & 0 deletions test/cli/samples/config-type-module/sub/package.json
@@ -0,0 +1,3 @@
{
"type": "module"
}
6 changes: 6 additions & 0 deletions test/cli/samples/config-type-module/sub/rollup.config.js
@@ -0,0 +1,6 @@
module.exports = {
input: 'main.js',
output: {
format: 'cjs'
}
};
8 changes: 8 additions & 0 deletions typings/declarations.d.ts
Expand Up @@ -99,3 +99,11 @@ declare module 'is-reference' {
value: Node;
};
}

declare module 'get-package-type' {
interface GetPackageType {
sync(fileName: string): 'module' | 'commonjs';
}
const getPackageType: GetPackageType;
export default getPackageType;
}

0 comments on commit 48b366b

Please sign in to comment.