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
15 changes: 15 additions & 0 deletions e2e/cases/auto-external/default/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { buildAndGetResults } from '@e2e/helper';
import { expect, test } from 'vitest';

test('auto external should works', async () => {
const fixturePath = __dirname;
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");',
);
});
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: './src/index.ts',
},
},
});
5 changes: 5 additions & 0 deletions e2e/cases/auto-external/default/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;
};
15 changes: 15 additions & 0 deletions e2e/cases/auto-external/false/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { buildAndGetResults } from '@e2e/helper';
import { expect, test } from 'vitest';

test('auto external false should works', async () => {
const fixturePath = __dirname;
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");',
);
});
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: './src/index.ts',
},
},
});
5 changes: 5 additions & 0 deletions e2e/cases/auto-external/false/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;
};
15 changes: 15 additions & 0 deletions e2e/cases/auto-external/with-externals/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { buildAndGetResults } from '@e2e/helper';
import { expect, test } from 'vitest';

test('auto external false should works', async () => {
const fixturePath = __dirname;
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: './src/index.ts',
},
},
});
5 changes: 5 additions & 0 deletions e2e/cases/auto-external/with-externals/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;
};
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: () => {},
},
};
29 changes: 21 additions & 8 deletions packages/core/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,21 @@ import { DEFAULT_CONFIG_NAME, DEFAULT_EXTENSIONS } from './constant';
import type {
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 { composeAutoExternal } from './utils/external';
import {
calcLongestCommonPath,
color,
nodeBuiltInModules,
readPackageJson,
} from './utils/helper';
import { logger } from './utils/logger';
import { transformSyntaxToBrowserslist } from './utils/syntax';

Expand Down Expand Up @@ -166,15 +171,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 +399,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 = composeAutoExternal({
9aoy marked this conversation as resolved.
Show resolved Hide resolved
autoExternal,
pkgJson,
userExternals: rsbuildConfig.output?.externals,
});
const entryConfig = await composeEntryConfig(
config.source?.entry,
config.bundle,
Expand All @@ -415,6 +427,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;
};
28 changes: 6 additions & 22 deletions packages/core/src/utils/extension.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,16 @@
import fs from 'node:fs';
import { resolve } from 'node:path';
import type { Format } from 'src/types/config';
import type { Format, PkgJson } from 'src/types';
import { logger } from './logger';

export const getDefaultExtension = (options: {
format: Format;
root: string;
pkgJson?: PkgJson;
autoExtension: boolean;
}): {
jsExtension: string;
dtsExtension: string;
isModule?: boolean;
} => {
const { format, root, autoExtension } = options;
const { format, pkgJson, autoExtension } = options;

let jsExtension = '.js';
let dtsExtension = '.d.ts';
Expand All @@ -24,31 +22,17 @@ export const getDefaultExtension = (options: {
};
}

const pkgJsonPath = resolve(root, './package.json');
if (!fs.existsSync(pkgJsonPath)) {
if (!pkgJson) {
logger.warn(
`package.json does not exist in ${pkgJsonPath}, autoExtension will not be applied.`,
'autoExtension configuration will not be applied due to read package.json failed',
);
return {
jsExtension,
dtsExtension,
};
}

let isModule = false;

try {
const json = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf8'));
isModule = json.type === 'module';
} catch (e) {
logger.warn(
`Failed to parse ${pkgJsonPath}, it might not be valid JSON, autoExtension will not be applied.`,
);
return {
jsExtension,
dtsExtension,
};
}
const isModule = pkgJson.type === 'module';

if (isModule && format === 'cjs') {
jsExtension = '.cjs';
Expand Down
58 changes: 58 additions & 0 deletions packages/core/src/utils/external.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import type { RsbuildConfig } from '@rsbuild/core';
import type { PkgJson } from '../types';
import type { AutoExternal } from '../types/config';
import { logger } from './logger';

export const composeAutoExternal = (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 &&
Object.prototype.toString.call(userExternals) === '[object Object]'
fi3ework marked this conversation as resolved.
Show resolved Hide resolved
? 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;
fi3ework marked this conversation as resolved.
Show resolved Hide resolved
}
return prev;
}, [])
.filter((name) => !userExternalKeys.includes(name));

return externals.length
? {
output: {
externals,
},
}
: {};
};
19 changes: 19 additions & 0 deletions packages/core/src/utils/helper.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import fs from 'node:fs';
import fsP from 'node:fs/promises';
import path from 'node:path';
import color from 'picocolors';
import type { PkgJson } from '../types';
import { logger } from './logger';

/**
* Node.js built-in modules.
Expand Down Expand Up @@ -99,4 +102,20 @@ async function calcLongestCommonPath(
return lca;
}

export const readPackageJson = (rootPath: string): undefined | PkgJson => {
const pkgJsonPath = path.resolve(rootPath, './package.json');

if (!fs.existsSync(pkgJsonPath)) {
logger.warn(`package.json does not exist in the ${rootPath} directory`);
return;
}

try {
return JSON.parse(fs.readFileSync(pkgJsonPath, 'utf8'));
} catch (err) {
logger.warn(`Failed to parse ${pkgJsonPath}, it might not be valid JSON`);
return;
}
};

export { color, calcLongestCommonPath };
Loading
Loading