Skip to content

Commit

Permalink
fix: do not use CJS methods in ESM
Browse files Browse the repository at this point in the history
  • Loading branch information
fi3ework committed Oct 15, 2024
1 parent 86f16f9 commit c325e22
Show file tree
Hide file tree
Showing 51 changed files with 629 additions and 158 deletions.
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@
"test": "pnpm run test:unit && pnpm run test:integration && pnpm run test:e2e",
"test:benchmark": "cd ./tests && pnpm run test:benchmark",
"test:e2e": "pnpm run build:examples && cd tests && pnpm run test:e2e",
"test:integration": "vitest run --project integration",
"test:integration:watch": "vitest --project integration",
"test:integration": "NODE_OPTIONS='--experimental-vm-modules' vitest run --project integration",
"test:integration:watch": "NODE_OPTIONS='--experimental-vm-modules' vitest --project integration",
"test:unit": "vitest run --project unit*",
"test:unit:watch": "vitest --project unit*",
"testu": "pnpm run test:unit -u && pnpm run test:integration -u",
Expand Down
117 changes: 104 additions & 13 deletions packages/core/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,19 +24,24 @@ import {
cssExternalHandler,
isCssGlobalFile,
} from './css/cssConfig';
import { pluginCjsShim } from './plugins/cjsShim';
import {
pluginCjsImportMetaUrlShim,
pluginEsmRequireShim,
} from './plugins/shims';
import type {
AutoExternal,
BannerAndFooter,
Format,
LibConfig,
PkgJson,
Redirect,
ResolvedShims,
RsbuildConfigOutputTarget,
RslibConfig,
RslibConfigAsyncFn,
RslibConfigExport,
RslibConfigSyncFn,
Shims,
Syntax,
} from './types';
import { getDefaultExtension } from './utils/extension';
Expand Down Expand Up @@ -447,12 +452,16 @@ export async function createConstantRsbuildConfig(): Promise<RsbuildConfig> {

const composeFormatConfig = (format: Format): RsbuildConfig => {
const jsParserOptions = {
importMeta: false,
requireResolve: false,
requireDynamic: false,
requireAsExpression: false,
importDynamic: false,
};
cjs: {
requireResolve: false,
requireDynamic: false,
requireAsExpression: false,
},
esm: {
importMeta: false,
importDynamic: false,
},
} as const;

switch (format) {
case 'esm':
Expand All @@ -461,7 +470,10 @@ const composeFormatConfig = (format: Format): RsbuildConfig => {
rspack: {
module: {
parser: {
javascript: jsParserOptions,
javascript: {
...jsParserOptions.esm,
...jsParserOptions.cjs,
},
},
},
optimization: {
Expand All @@ -486,12 +498,11 @@ const composeFormatConfig = (format: Format): RsbuildConfig => {
};
case 'cjs':
return {
plugins: [pluginCjsShim()],
tools: {
rspack: {
module: {
parser: {
javascript: jsParserOptions,
javascript: { ...jsParserOptions.esm, ...jsParserOptions.cjs },
},
},
output: {
Expand Down Expand Up @@ -531,6 +542,85 @@ const composeFormatConfig = (format: Format): RsbuildConfig => {
}
};

const resolveShims = (shims?: Shims) => {
const resolvedShims = {
cjs: {
'import.meta.url': true,
},
esm: {
__filename: true,
__dirname: true,
require: false,
},
};

if (!shims) {
return resolvedShims;
}

if (shims.cjs) {
if (typeof shims.cjs === 'boolean') {
if (shims.cjs === true) {
resolvedShims.cjs['import.meta.url'] = true;
} else {
resolvedShims.cjs['import.meta.url'] = false;
}
} else {
resolvedShims.cjs['import.meta.url'] =
shims.cjs['import.meta.url'] ?? false;
}
}

if (shims.esm) {
if (typeof shims.esm === 'boolean') {
if (shims.esm === true) {
resolvedShims.esm.__filename = true;
resolvedShims.esm.__dirname = true;
resolvedShims.esm.require = true;
}
} else {
resolvedShims.esm.__filename = shims.esm.__filename ?? false;
resolvedShims.esm.__dirname = shims.esm.__dirname ?? false;
resolvedShims.esm.require = shims.esm.require ?? false;
}
}

return resolvedShims;
};

const composeShimsConfig = (
format: Format,
resolvedShims: ResolvedShims,
): RsbuildConfig => {
switch (format) {
case 'esm':
return {
tools: {
rspack: {
node: {
// "__dirname" and "__filename" shims will automatically be enabled when `output.module` is `true`
__dirname: resolvedShims.esm.__dirname ? 'node-module' : false,
__filename: resolvedShims.esm.__filename ? 'node-module' : false,
},
},
},
plugins: [resolvedShims.esm.require && pluginEsmRequireShim()].filter(
Boolean,
),
};
case 'cjs':
return {
plugins: [
resolvedShims.cjs['import.meta.url'] && pluginCjsImportMetaUrlShim(),
].filter(Boolean),
};
case 'umd':
return {};
default:
throw new Error(`Unsupported format: ${format}`);
}
};

export const composeModuleImportWarn = (request: string): string => {
return `The externalized commonjs request ${color.green(`"${request}"`)} will use ${color.blue('"module"')} external type in ESM format. If you want to specify other external type, considering set the request and type with ${color.blue('"output.externals"')}.`;
};
Expand Down Expand Up @@ -832,9 +922,6 @@ const composeTargetConfig = (
tools: {
rspack: {
target: ['node'],
// "__dirname" and "__filename" shims will automatically be enabled when `output.module` is `true`,
// and leave them as-is in the rest of the cases. Leave the comments here to explain the behavior.
// { node: { __dirname: ..., __filename: ... } }
},
},
output: {
Expand Down Expand Up @@ -915,6 +1002,8 @@ async function composeLibRsbuildConfig(config: LibConfig, configPath: string) {
externalHelpers = false,
redirect = {},
} = config;
const resolvedShims = resolveShims(config.shims);
const shimsConfig = composeShimsConfig(format!, resolvedShims);
const formatConfig = composeFormatConfig(format!);
const externalHelpersConfig = composeExternalHelpersConfig(
externalHelpers,
Expand Down Expand Up @@ -967,6 +1056,7 @@ async function composeLibRsbuildConfig(config: LibConfig, configPath: string) {

return mergeRsbuildConfig(
formatConfig,
shimsConfig,
externalHelpersConfig,
// externalsWarnConfig should before other externals config
externalsWarnConfig,
Expand Down Expand Up @@ -1046,6 +1136,7 @@ export async function composeCreateRsbuildConfig(
'banner',
'footer',
'dts',
'shims',
]),
),
};
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { RsbuildPlugin } from '@rsbuild/core';
import { type RsbuildPlugin, rspack } from '@rsbuild/core';

const importMetaUrlShim = `/*#__PURE__*/ (function () {
return typeof document === 'undefined'
Expand All @@ -11,9 +11,8 @@ const importMetaUrlShim = `/*#__PURE__*/ (function () {
// - Replace `import.meta.url` with `importMetaUrl`.
// - Inject `importMetaUrl` to the end of the module (can't inject at the beginning because of `"use strict";`).
// This is a short-term solution, and we hope to provide built-in polyfills like `node.__filename` on Rspack side.
export const pluginCjsShim = (): RsbuildPlugin => ({
export const pluginCjsImportMetaUrlShim = (): RsbuildPlugin => ({
name: 'rsbuild-plugin-cjs-shim',

setup(api) {
api.modifyEnvironmentConfig((config) => {
config.source.define = {
Expand All @@ -23,3 +22,26 @@ export const pluginCjsShim = (): RsbuildPlugin => ({
});
},
});

const requireShim = `// Rslib ESM shims
import __rslib_shim_module__ from 'module';
const require = /*#__PURE__*/ __rslib_shim_module__.createRequire(import.meta.url);
`;

export const pluginEsmRequireShim = (): RsbuildPlugin => ({
name: 'rsbuild-plugin-esm-shim',
setup(api) {
api.modifyRspackConfig((config) => {
config.plugins ??= [];
config.plugins.push(
new rspack.BannerPlugin({
banner: requireShim,
// Just before minify stage, to perform tree shaking.
stage: rspack.Compilation.PROCESS_ASSETS_STAGE_OPTIMIZE - 1,
raw: true,
include: /\.(js|cjs)$/,
}),
);
});
},
});
21 changes: 21 additions & 0 deletions packages/core/src/types/config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,26 @@ export type BannerAndFooter = {
dts?: string;
};

export type Shims = {
cjs?:
| boolean
| {
'import.meta.url'?: boolean;
};
esm?:
| boolean
| {
__filename?: boolean;
__dirname?: boolean;
require?: boolean;
};
};

export type ResolvedShims = {
cjs: Required<NonNullable<Exclude<Shims['cjs'], boolean>>>;
esm: Required<NonNullable<Exclude<Shims['esm'], boolean>>>;
};

export type Redirect = {
// TODO: support other redirects
// alias?: boolean;
Expand All @@ -67,6 +87,7 @@ export interface LibConfig extends RsbuildConfig {
externalHelpers?: boolean;
banner?: BannerAndFooter;
footer?: BannerAndFooter;
shims?: Shims;
dts?: Dts;
}

Expand Down
5 changes: 5 additions & 0 deletions packages/core/tests/__snapshots__/config.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ exports[`Should compose create Rsbuild config correctly > Merge Rsbuild config 1
"not dead",
],
},
"plugins": [],
"source": {
"alias": {
"bar": "bar",
Expand Down Expand Up @@ -110,6 +111,10 @@ exports[`Should compose create Rsbuild config correctly > Merge Rsbuild config 1
},
},
},
"node": {
"__dirname": "node-module",
"__filename": "node-module",
},
"optimization": {
"concatenateModules": true,
"sideEffects": "flag",
Expand Down
6 changes: 3 additions & 3 deletions tests/benchmark/index.bench.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,23 @@ describe('run rslib in examples', () => {
'examples/express-plugin',
async () => {
const cwd = getCwdByExample('express-plugin');
await rslibBuild(cwd);
await rslibBuild({ cwd });
},
{ time: 5 },
);
bench(
'examples/react-component-bundle',
async () => {
const cwd = getCwdByExample('react-component-bundle');
await rslibBuild(cwd);
await rslibBuild({ cwd });
},
{ time: 5 },
);
bench(
'examples/react-component-bundle-false',
async () => {
const cwd = getCwdByExample('react-component-bundle-false');
await rslibBuild(cwd);
await rslibBuild({ cwd });
},
{ time: 5 },
);
Expand Down
2 changes: 1 addition & 1 deletion tests/integration/alias/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { expect, test } from 'vitest';

test('source.alias', async () => {
const fixturePath = __dirname;
const { entries } = await buildAndGetResults(fixturePath);
const { entries } = await buildAndGetResults({ fixturePath });

expect(entries.esm).toContain('hello world');
expect(entries.cjs).toContain('hello world');
Expand Down
10 changes: 5 additions & 5 deletions tests/integration/asset/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { expect, test } from 'vitest';

test('set the size threshold to inline static assets', async () => {
const fixturePath = join(__dirname, 'limit');
const { contents } = await buildAndGetResults(fixturePath);
const { contents } = await buildAndGetResults({ fixturePath });

// inline when bundle
expect(Object.values(contents.esm0!)[0]).toContain(
Expand All @@ -29,7 +29,7 @@ test('set the size threshold to inline static assets', async () => {

test('set the assets name', async () => {
const fixturePath = join(__dirname, 'name');
const { contents } = await buildAndGetResults(fixturePath);
const { contents } = await buildAndGetResults({ fixturePath });

// bundle
expect(Object.values(contents.esm0!)[0]).toContain(
Expand All @@ -44,7 +44,7 @@ test('set the assets name', async () => {

test('set the assets output path', async () => {
const fixturePath = join(__dirname, 'path');
const { contents } = await buildAndGetResults(fixturePath);
const { contents } = await buildAndGetResults({ fixturePath });

// bundle
expect(Object.values(contents.esm0!)[0]).toContain(
Expand All @@ -59,7 +59,7 @@ test('set the assets output path', async () => {

test('set the assets public path', async () => {
const fixturePath = join(__dirname, 'public-path');
const { contents } = await buildAndGetResults(fixturePath);
const { contents } = await buildAndGetResults({ fixturePath });

// bundle
expect(Object.values(contents.esm0!)[0]).toContain(
Expand All @@ -74,7 +74,7 @@ test('set the assets public path', async () => {

test('use svgr', async () => {
const fixturePath = join(__dirname, 'svgr');
const { contents } = await buildAndGetResults(fixturePath);
const { contents } = await buildAndGetResults({ fixturePath });

// bundle -- default export with react query
expect(Object.values(contents.esm0!)[0]).toMatchSnapshot();
Expand Down
2 changes: 1 addition & 1 deletion tests/integration/async-chunks/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { expect, test } from 'vitest';

test('should get correct value from async chunks', async () => {
const fixturePath = join(__dirname, 'default');
const { entryFiles } = await buildAndGetResults(fixturePath);
const { entryFiles } = await buildAndGetResults({ fixturePath });

for (const format of ['esm', 'cjs']) {
const { foo } = await import(entryFiles[format]);
Expand Down
Loading

0 comments on commit c325e22

Please sign in to comment.