Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(bundle): support autoExternal configuration #74

Merged
merged 12 commits into from
Aug 9, 2024
6 changes: 6 additions & 0 deletions e2e/cases/auto-external/default/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"name": "@e2e/auto-external-default",
"dependencies": {
"react": "^18.3.1"
}
}
11 changes: 11 additions & 0 deletions e2e/cases/auto-external/default/rslib.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { generateBundleCjsConfig, generateBundleEsmConfig } from '@e2e/helper';
import { defineConfig } from '@rslib/core';

export default defineConfig({
lib: [generateBundleEsmConfig(__dirname), generateBundleCjsConfig(__dirname)],
source: {
entry: {
main: '../fixtures/src/index.ts',
},
},
});
6 changes: 6 additions & 0 deletions e2e/cases/auto-external/external-sub-path/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"name": "@e2e/auto-external-with-sub-path",
"dependencies": {
"react": "^18.3.1"
}
}
11 changes: 11 additions & 0 deletions e2e/cases/auto-external/external-sub-path/rslib.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { generateBundleCjsConfig, generateBundleEsmConfig } from '@e2e/helper';
import { defineConfig } from '@rslib/core';

export default defineConfig({
lib: [generateBundleEsmConfig(__dirname), generateBundleCjsConfig(__dirname)],
source: {
entry: {
main: './src/index.ts',
},
},
});
6 changes: 6 additions & 0 deletions e2e/cases/auto-external/external-sub-path/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import React from 'react';
import ReactJsx from 'react/jsx-runtime';

export const foo = () => {
return [React.version, ReactJsx.jsx];
};
6 changes: 6 additions & 0 deletions e2e/cases/auto-external/false/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"name": "@e2e/auto-external-false",
"dependencies": {
"react": "^18.3.1"
}
}
20 changes: 20 additions & 0 deletions e2e/cases/auto-external/false/rslib.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { generateBundleCjsConfig, generateBundleEsmConfig } from '@e2e/helper';
import { defineConfig } from '@rslib/core';

export default defineConfig({
lib: [
{
...generateBundleEsmConfig(__dirname),
autoExternal: false,
},
{
...generateBundleCjsConfig(__dirname),
autoExternal: false,
},
],
source: {
entry: {
main: '../fixtures/src/index.ts',
},
},
});
5 changes: 5 additions & 0 deletions e2e/cases/auto-external/fixtures/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import React from 'react';

export const foo = () => {
return React.version;
};
61 changes: 61 additions & 0 deletions e2e/cases/auto-external/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { join } from 'node:path';
import { buildAndGetResults } from '@e2e/helper';
import { expect, test } from 'vitest';

test('auto external default should works', async () => {
const fixturePath = join(__dirname, 'default');
const { entries } = await buildAndGetResults(fixturePath);

expect(entries.esm).toContain(
'import * as __WEBPACK_EXTERNAL_MODULE_react__ from "react"',
);

expect(entries.cjs).toContain(
'var external_react_namespaceObject = require("react");',
);
});

test('auto external sub path should works', async () => {
const fixturePath = join(__dirname, 'external-sub-path');
const { entries } = await buildAndGetResults(fixturePath);

expect(entries.esm).toContain(
'import * as __WEBPACK_EXTERNAL_MODULE_react__ from "react"',
);
expect(entries.esm).toContain(
'import * as __WEBPACK_EXTERNAL_MODULE_react_jsx_runtime__ from "react/jsx-runtime"',
);

expect(entries.cjs).toContain(
'var external_react_namespaceObject = require("react");',
);
expect(entries.cjs).toContain(
'var jsx_runtime_namespaceObject = require("react/jsx-runtime");',
);
});

test('auto external false should works', async () => {
const fixturePath = join(__dirname, 'false');
const { entries } = await buildAndGetResults(fixturePath);

expect(entries.esm).not.toContain(
'import * as __WEBPACK_EXTERNAL_MODULE_react__ from "react"',
);

expect(entries.cjs).not.toContain(
'var external_react_namespaceObject = require("react");',
);
});

test('externals should overrides auto external', async () => {
const fixturePath = join(__dirname, 'with-externals');
const { entries } = await buildAndGetResults(fixturePath);

expect(entries.esm).toContain(
'import * as __WEBPACK_EXTERNAL_MODULE_react1__ from "react1"',
);

expect(entries.cjs).toContain(
'var external_react1_namespaceObject = require("react1");',
);
});
6 changes: 6 additions & 0 deletions e2e/cases/auto-external/with-externals/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"name": "@e2e/auto-external-with-externals",
"dependencies": {
"react": "^18.3.1"
}
}
16 changes: 16 additions & 0 deletions e2e/cases/auto-external/with-externals/rslib.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { generateBundleCjsConfig, generateBundleEsmConfig } from '@e2e/helper';
import { defineConfig } from '@rslib/core';

export default defineConfig({
lib: [generateBundleEsmConfig(__dirname), generateBundleCjsConfig(__dirname)],
output: {
externals: {
react: 'react1',
},
},
source: {
entry: {
main: '../fixtures/src/index.ts',
},
},
});
3 changes: 0 additions & 3 deletions examples/express-plugin/rslib.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,6 @@ export default defineConfig({
},
},
output: {
externals: {
express: 'express',
},
target: 'node',
},
});
6 changes: 0 additions & 6 deletions examples/react-component/rslib.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,5 @@ export default defineConfig({
main: './src/index.tsx',
},
},
output: {
externals: {
react: 'react',
'react/jsx-runtime': 'react/jsx-runtime',
},
},
plugins: [pluginReact()],
});
5 changes: 5 additions & 0 deletions packages/core/__mocks__/rslog.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module.exports = {
logger: {
warn: () => {},
},
};
87 changes: 79 additions & 8 deletions packages/core/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,24 @@ import {
import glob from 'fast-glob';
import { DEFAULT_CONFIG_NAME, DEFAULT_EXTENSIONS } from './constant';
import type {
AutoExternal,
Format,
LibConfig,
PkgJson,
RslibConfig,
RslibConfigAsyncFn,
RslibConfigExport,
RslibConfigSyncFn,
Syntax,
} from './types/config';
} from './types';
import { getDefaultExtension } from './utils/extension';
import { calcLongestCommonPath } from './utils/helper';
import { color } from './utils/helper';
import { nodeBuiltInModules } from './utils/helper';
import {
calcLongestCommonPath,
color,
isObject,
nodeBuiltInModules,
readPackageJson,
} from './utils/helper';
import { logger } from './utils/logger';
import { transformSyntaxToBrowserslist } from './utils/syntax';

Expand Down Expand Up @@ -77,6 +83,63 @@ export async function loadConfig(
return content as RslibConfig;
}

export const composeAutoExternalConfig = (options: {
autoExternal: AutoExternal;
pkgJson?: PkgJson;
userExternals?: NonNullable<RsbuildConfig['output']>['externals'];
}): RsbuildConfig => {
const { autoExternal, pkgJson, userExternals } = options;

if (!autoExternal) {
return {};
}

if (!pkgJson) {
logger.warn(
'autoExternal configuration will not be applied due to read package.json failed',
);
return {};
}

const externalOptions = {
dependencies: true,
peerDependencies: true,
devDependencies: false,
...(autoExternal === true ? {} : autoExternal),
};

// User externals configuration has higher priority than autoExternal
// eg: autoExternal: ['react'], user: output: { externals: { react: 'react-1' } }
// Only handle the case where the externals type is object, string / string[] does not need to be processed, other types are too complex.
const userExternalKeys =
userExternals && isObject(userExternals) ? Object.keys(userExternals) : [];

const externals = (
['dependencies', 'peerDependencies', 'devDependencies'] as const
)
.reduce<string[]>((prev, type) => {
if (externalOptions[type]) {
return pkgJson[type] ? prev.concat(Object.keys(pkgJson[type]!)) : prev;
}
return prev;
}, [])
.filter((name) => !userExternalKeys.includes(name));

const uniqueExternals = Array.from(new Set(externals));

return externals.length
? {
output: {
externals: [
// Exclude dependencies, e.g. `react`, `react/jsx-runtime`
...uniqueExternals.map((dep) => new RegExp(`^${dep}($|\\/|\\\\)`)),
...uniqueExternals,
],
},
}
: {};
};

export async function createInternalRsbuildConfig(): Promise<RsbuildConfig> {
return defineRsbuildConfig({
dev: {
Expand Down Expand Up @@ -166,15 +229,15 @@ const composeFormatConfig = (format: Format): RsbuildConfig => {

const composeAutoExtensionConfig = (
format: Format,
root: string,
autoExtension: boolean,
pkgJson?: PkgJson,
): {
config: RsbuildConfig;
dtsExtension: string;
} => {
const { jsExtension, dtsExtension } = getDefaultExtension({
format,
root,
pkgJson,
autoExtension,
});

Expand Down Expand Up @@ -394,17 +457,24 @@ async function composeLibRsbuildConfig(
configPath: string,
) {
const config = mergeRsbuildConfig<LibConfig>(rsbuildConfig, libConfig);
const rootPath = dirname(configPath);
const pkgJson = readPackageJson(rootPath);

const { format, autoExtension = true } = config;
const { format, autoExtension = true, autoExternal = true } = config;
const formatConfig = composeFormatConfig(format!);
const { config: autoExtensionConfig, dtsExtension } =
composeAutoExtensionConfig(format!, dirname(configPath), autoExtension);
composeAutoExtensionConfig(format!, autoExtension, pkgJson);
const bundleConfig = composeBundleConfig(config.bundle);
const targetConfig = composeTargetConfig(config.output?.target);
const syntaxConfig = composeSyntaxConfig(
config.output?.syntax,
config.output?.target,
);
const autoExternalConfig = composeAutoExternalConfig({
autoExternal,
pkgJson,
userExternals: rsbuildConfig.output?.externals,
});
const entryConfig = await composeEntryConfig(
config.source?.entry,
config.bundle,
Expand All @@ -415,6 +485,7 @@ async function composeLibRsbuildConfig(
return mergeRsbuildConfig(
formatConfig,
autoExtensionConfig,
autoExternalConfig,
syntaxConfig,
bundleConfig,
targetConfig,
Expand Down
9 changes: 9 additions & 0 deletions packages/core/src/types/config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,19 @@ export type Dts =
}
| false;

export type AutoExternal =
| boolean
| {
dependencies?: boolean;
devDependencies?: boolean;
peerDependencies?: boolean;
};

export interface LibConfig extends RsbuildConfig {
bundle?: boolean;
format?: Format;
autoExtension?: boolean;
autoExternal?: AutoExternal;
output?: RsbuildConfig['output'] & {
/** Support esX and browserslist query */
syntax?: Syntax;
Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/types/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './config';
export * from './utils';
6 changes: 6 additions & 0 deletions packages/core/src/types/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export type PkgJson = {
dependencies?: Record<string, string>;
peerDependencies?: Record<string, string>;
devDependencies?: Record<string, string>;
[key: string]: unknown;
};
Loading
Loading