Skip to content

Commit

Permalink
Support ES modules in bundles config files
Browse files Browse the repository at this point in the history
  • Loading branch information
lukastaegert committed Aug 24, 2022
1 parent eac06ac commit 281f468
Show file tree
Hide file tree
Showing 12 changed files with 50 additions and 48 deletions.
6 changes: 4 additions & 2 deletions build-plugins/esm-dynamic-import.ts
@@ -1,12 +1,14 @@
import type { Plugin } from 'rollup';

const expectedImports = 3;

export default function esmDynamicImport(): Plugin {
let importsFound = 0;
return {
generateBundle() {
if (importsFound !== 2) {
if (importsFound !== expectedImports) {
throw new Error(
'Could not find 2 dynamic import in "loadConfigFile.ts" and "commandPlugin.ts", were the files renamed or modified?'
`Could not find ${expectedImports} dynamic imports in "loadConfigFile.ts" and "commandPlugin.ts", found ${importsFound}.`
);
}
},
Expand Down
52 changes: 17 additions & 35 deletions cli/run/loadConfigFile.ts
@@ -1,21 +1,18 @@
import { extname, isAbsolute } from 'node:path';
import { promises as fs } from 'node:fs';
import { dirname, extname, isAbsolute, join } from 'node:path';
import { pathToFileURL } from 'node: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';
import { errMissingConfig, error, errTranspiledEsmConfig } from '../../src/utils/error';
import { errMissingConfig, error } from '../../src/utils/error';
import { mergeOptions } from '../../src/utils/options/mergeOptions';
import type { GenericConfigObject } from '../../src/utils/options/options';
import relativeId from '../../src/utils/relativeId';
import { stderr } from '../logging';
import batchWarnings, { type BatchWarnings } from './batchWarnings';
import { addCommandPluginsToInputOptions, addPluginsFromCommandOption } from './commandPlugins';

interface NodeModuleWithCompile extends NodeModule {
_compile(code: string, filename: string): any;
}

export async function loadConfigFile(
fileName: string,
commandOptions: any = {}
Expand Down Expand Up @@ -45,18 +42,18 @@ async function loadConfigsFromFile(
const configFileExport =
commandOptions.configPlugin ||
// 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)
: getDefaultFromCjs((await import(pathToFileURL(fileName).href)).default);
(extension === '.js' && (await getPackageType(fileName)) !== 'module')
? await loadTranspiledConfigFile(fileName, commandOptions)
: (await import(pathToFileURL(fileName).href)).default;

return getConfigList(configFileExport, commandOptions);
return getConfigList(getDefaultFromCjs(configFileExport), commandOptions);
}

function getDefaultFromCjs(namespace: GenericConfigObject): unknown {
return namespace.__esModule ? namespace.default : namespace;
return namespace.default || namespace;
}

async function getDefaultFromTranspiledConfigFile(
async function loadTranspiledConfigFile(
fileName: string,
commandOptions: Record<string, unknown>
): Promise<unknown> {
Expand All @@ -79,7 +76,7 @@ async function getDefaultFromTranspiledConfigFile(
output: [{ code }]
} = await bundle.generate({
exports: 'named',
format: 'cjs',
format: 'es',
plugins: [
{
name: 'transpile-import-meta',
Expand All @@ -97,29 +94,14 @@ async function getDefaultFromTranspiledConfigFile(
return loadConfigFromBundledFile(fileName, code);
}

function loadConfigFromBundledFile(fileName: string, bundledCode: string): unknown {
const resolvedFileName = require.resolve(fileName);
const extension = extname(resolvedFileName);
const defaultLoader = require.extensions[extension];
require.extensions[extension] = (module: NodeModule, requiredFileName: string) => {
if (requiredFileName === resolvedFileName) {
(module as NodeModuleWithCompile)._compile(bundledCode, requiredFileName);
} else {
if (defaultLoader) {
defaultLoader(module, requiredFileName);
}
}
};
delete require.cache[resolvedFileName];
async function loadConfigFromBundledFile(fileName: string, bundledCode: string): Promise<unknown> {
const bundledFileName = join(dirname(fileName), `rollup.config-${Date.now()}.mjs`);
await fs.writeFile(bundledFileName, bundledCode);
try {
const config = getDefaultFromCjs(require(fileName));
require.extensions[extension] = defaultLoader;
return config;
} catch (err: any) {
if (err.code === 'ERR_REQUIRE_ESM') {
return error(errTranspiledEsmConfig(fileName));
}
throw err;
return (await import(pathToFileURL(bundledFileName).href)).default;
} finally {
// Not awaiting here saves some ms while potentially hiding a non-critical error
fs.unlink(bundledFileName);
}
}

Expand Down
10 changes: 0 additions & 10 deletions src/utils/error.ts
Expand Up @@ -100,7 +100,6 @@ const ADDON_ERROR = 'ADDON_ERROR',
SOURCEMAP_ERROR = 'SOURCEMAP_ERROR',
SYNTHETIC_NAMED_EXPORTS_NEED_NAMESPACE_EXPORT = 'SYNTHETIC_NAMED_EXPORTS_NEED_NAMESPACE_EXPORT',
THIS_IS_UNDEFINED = 'THIS_IS_UNDEFINED',
TRANSPILED_ESM_CONFIG = 'TRANSPILED_ESM_CONFIG',
UNEXPECTED_NAMED_IMPORT = 'UNEXPECTED_NAMED_IMPORT',
UNKNOWN_OPTION = 'UNKNOWN_OPTION',
UNRESOLVED_ENTRY = 'UNRESOLVED_ENTRY',
Expand Down Expand Up @@ -756,15 +755,6 @@ export function errThisIsUndefined(): RollupLog {
};
}

export function errTranspiledEsmConfig(fileName: string): RollupLog {
return {
code: TRANSPILED_ESM_CONFIG,
message: `While loading the Rollup configuration from "${relativeId(
fileName
)}", Node tried to require an ES module from a CommonJS file, which is not supported. A common cause is if there is a package.json file with "type": "module" in the same folder. You can try to fix this by changing the extension of your configuration file to ".cjs" or ".mjs" depending on the content, which will prevent Rollup from trying to preprocess the file but rather hand it to Node directly.`
};
}

export function errUnexpectedNamedImport(
id: string,
imported: string,
Expand Down
1 change: 1 addition & 0 deletions test/cli/node_modules/esm-dep/index.js

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

4 changes: 4 additions & 0 deletions test/cli/node_modules/esm-dep/package.json

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

1 change: 1 addition & 0 deletions test/cli/node_modules_rename_me/esm-dep/index.js
@@ -0,0 +1 @@
export default 'esm-default';
4 changes: 4 additions & 0 deletions test/cli/node_modules_rename_me/esm-dep/package.json
@@ -0,0 +1,4 @@
{
"main": "index.js",
"type": "module"
}
4 changes: 4 additions & 0 deletions test/cli/samples/import-esm-package/_config.js
@@ -0,0 +1,4 @@
module.exports = {
description: 'allows to import ESM dependencies from transpiled config files',
command: "rollup --config --configPlugin '{transform: c => c}'"
};
2 changes: 2 additions & 0 deletions test/cli/samples/import-esm-package/_expected/main.js
@@ -0,0 +1,2 @@
/* esm-default */
assert.ok(true);
1 change: 1 addition & 0 deletions test/cli/samples/import-esm-package/main.js
@@ -0,0 +1 @@
assert.ok(true);
10 changes: 10 additions & 0 deletions test/cli/samples/import-esm-package/rollup.config.js
@@ -0,0 +1,10 @@
import esmDep from 'esm-dep';

export default {
input: 'main.js',
output: {
banner: `/* ${esmDep} */`,
format: 'es',
dir: '_actual'
}
};
3 changes: 2 additions & 1 deletion typings/declarations.d.ts
Expand Up @@ -102,7 +102,8 @@ declare module 'is-reference' {

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

0 comments on commit 281f468

Please sign in to comment.