diff --git a/e2e/README.md b/e2e/README.md index c2ed1a1df..fa707b835 100644 --- a/e2e/README.md +++ b/e2e/README.md @@ -12,7 +12,7 @@ Rslib will try to cover the common scenarios in the [integration test cases of M | --------------- | ------ | ----------------------------------------------------------------------------------------------------------------------- | | alias | 🟢 | | | asset | ⚪️ | | -| autoExtension | ⚪️ | | +| autoExtension | 🟡 | Support js extension, dts extension need to be supported in the future | | autoExternal | ⚪️ | | | banner-footer | ⚪️ | | | buildType | ⚪️ | | diff --git a/e2e/cases/alias/index.test.ts b/e2e/cases/alias/index.test.ts index 95b96c45f..d4cdd07a3 100644 --- a/e2e/cases/alias/index.test.ts +++ b/e2e/cases/alias/index.test.ts @@ -1,14 +1,14 @@ import { expect, test } from 'vitest'; -import { buildAndGetResults } from '#shared'; +import { buildAndGetEntryJsResults } from '#shared'; test('alias should work', async () => { const fixturePath = __dirname; - const { entries } = await buildAndGetResults(fixturePath); + const { contents } = await buildAndGetEntryJsResults(fixturePath); - expect(entries.esm).toContain('hello world'); - expect(entries.cjs).toContain('hello world'); + expect(contents.esm).toContain('hello world'); + expect(contents.cjs).toContain('hello world'); // simple artifacts check - expect(entries.esm).toMatchSnapshot(); - expect(entries.cjs).toMatchSnapshot(); + expect(contents.esm).toMatchSnapshot(); + expect(contents.cjs).toMatchSnapshot(); }); diff --git a/e2e/cases/autoExtension/index.test.ts b/e2e/cases/autoExtension/index.test.ts new file mode 100644 index 000000000..e026c5944 --- /dev/null +++ b/e2e/cases/autoExtension/index.test.ts @@ -0,0 +1,17 @@ +import { extname, join } from 'node:path'; +import { expect, test } from 'vitest'; +import { buildAndGetEntryJsResults } from '#shared'; + +test('autoExtension generate .mjs in build artifacts with esm format when type is commonjs', async () => { + const fixturePath = join(__dirname, 'type-commonjs'); + const { files } = await buildAndGetEntryJsResults(fixturePath); + expect(extname(files.esm!)).toEqual('.mjs'); + expect(extname(files.cjs!)).toEqual('.js'); +}); + +test('autoExtension generate .cjs in build artifacts with cjs format when type is module', async () => { + const fixturePath = join(__dirname, 'type-module'); + const { files } = await buildAndGetEntryJsResults(fixturePath); + expect(extname(files.esm!)).toEqual('.js'); + expect(extname(files.cjs!)).toEqual('.cjs'); +}); diff --git a/e2e/cases/autoExtension/type-commonjs/package.json b/e2e/cases/autoExtension/type-commonjs/package.json new file mode 100644 index 000000000..4560b004e --- /dev/null +++ b/e2e/cases/autoExtension/type-commonjs/package.json @@ -0,0 +1,5 @@ +{ + "name": "auto-extension-commonjs-test", + "version": "1.0.0", + "private": true +} diff --git a/e2e/cases/autoExtension/type-commonjs/rslib.config.ts b/e2e/cases/autoExtension/type-commonjs/rslib.config.ts new file mode 100644 index 000000000..73a7b5abe --- /dev/null +++ b/e2e/cases/autoExtension/type-commonjs/rslib.config.ts @@ -0,0 +1,22 @@ +import { join } from 'node:path'; +import { defineConfig } from '@rslib/core'; +import { + generateBundleCjsConfig, + generateBundleEsmConfig, +} from '../../../scripts/shared'; + +export default defineConfig({ + lib: [ + generateBundleEsmConfig(__dirname, { + autoExtension: true, + }), + generateBundleCjsConfig(__dirname, { + autoExtension: true, + }), + ], + source: { + entry: { + main: join(__dirname, 'src/index.ts'), + }, + }, +}); diff --git a/e2e/cases/autoExtension/type-commonjs/src/common.ts b/e2e/cases/autoExtension/type-commonjs/src/common.ts new file mode 100644 index 000000000..cc798ff50 --- /dev/null +++ b/e2e/cases/autoExtension/type-commonjs/src/common.ts @@ -0,0 +1 @@ +export const a = 1; diff --git a/e2e/cases/autoExtension/type-commonjs/src/index.ts b/e2e/cases/autoExtension/type-commonjs/src/index.ts new file mode 100644 index 000000000..b5f5ad8ce --- /dev/null +++ b/e2e/cases/autoExtension/type-commonjs/src/index.ts @@ -0,0 +1,3 @@ +import { a } from './common'; + +console.log(a); diff --git a/e2e/cases/autoExtension/type-module/package.json b/e2e/cases/autoExtension/type-module/package.json new file mode 100644 index 000000000..3836bd261 --- /dev/null +++ b/e2e/cases/autoExtension/type-module/package.json @@ -0,0 +1,6 @@ +{ + "name": "auto-extension-module-test", + "version": "1.0.0", + "private": true, + "type": "module" +} diff --git a/e2e/cases/autoExtension/type-module/rslib.config.ts b/e2e/cases/autoExtension/type-module/rslib.config.ts new file mode 100644 index 000000000..73a7b5abe --- /dev/null +++ b/e2e/cases/autoExtension/type-module/rslib.config.ts @@ -0,0 +1,22 @@ +import { join } from 'node:path'; +import { defineConfig } from '@rslib/core'; +import { + generateBundleCjsConfig, + generateBundleEsmConfig, +} from '../../../scripts/shared'; + +export default defineConfig({ + lib: [ + generateBundleEsmConfig(__dirname, { + autoExtension: true, + }), + generateBundleCjsConfig(__dirname, { + autoExtension: true, + }), + ], + source: { + entry: { + main: join(__dirname, 'src/index.ts'), + }, + }, +}); diff --git a/e2e/cases/autoExtension/type-module/src/common.ts b/e2e/cases/autoExtension/type-module/src/common.ts new file mode 100644 index 000000000..cc798ff50 --- /dev/null +++ b/e2e/cases/autoExtension/type-module/src/common.ts @@ -0,0 +1 @@ +export const a = 1; diff --git a/e2e/cases/autoExtension/type-module/src/index.ts b/e2e/cases/autoExtension/type-module/src/index.ts new file mode 100644 index 000000000..b5f5ad8ce --- /dev/null +++ b/e2e/cases/autoExtension/type-module/src/index.ts @@ -0,0 +1,3 @@ +import { a } from './common'; + +console.log(a); diff --git a/e2e/cases/define/index.test.ts b/e2e/cases/define/index.test.ts index 35c34e9f8..a844522ae 100644 --- a/e2e/cases/define/index.test.ts +++ b/e2e/cases/define/index.test.ts @@ -1,13 +1,13 @@ import { expect, test } from 'vitest'; -import { buildAndGetResults } from '#shared'; +import { buildAndGetEntryJsResults } from '#shared'; test('define should work', async () => { const fixturePath = __dirname; - const { entries } = await buildAndGetResults(fixturePath); + const { contents } = await buildAndGetEntryJsResults(fixturePath); - expect(entries.esm).not.toContain('console.info(VERSION)'); - expect(entries.esm).toContain('1.0.0'); + expect(contents.esm).not.toContain('console.info(VERSION)'); + expect(contents.esm).toContain('1.0.0'); - expect(entries.cjs).not.toContain('console.info(VERSION)'); - expect(entries.cjs).toContain('1.0.0'); + expect(contents.cjs).not.toContain('console.info(VERSION)'); + expect(contents.cjs).toContain('1.0.0'); }); diff --git a/e2e/cases/externals/browser/index.test.ts b/e2e/cases/externals/browser/index.test.ts index dcba5e151..158de97bb 100644 --- a/e2e/cases/externals/browser/index.test.ts +++ b/e2e/cases/externals/browser/index.test.ts @@ -1,9 +1,9 @@ import { join } from 'node:path'; import { expect, test } from 'vitest'; -import { buildAndGetResults } from '#shared'; +import { buildAndGetEntryJsResults } from '#shared'; test('should fail when platform is not "node"', async () => { const fixturePath = join(__dirname); - const build = buildAndGetResults(fixturePath); + const build = buildAndGetEntryJsResults(fixturePath); await expect(build).rejects.toThrowError('Rspack build failed!'); }); diff --git a/e2e/cases/externals/node/index.test.ts b/e2e/cases/externals/node/index.test.ts index 9ae253cb6..8e5bcdc52 100644 --- a/e2e/cases/externals/node/index.test.ts +++ b/e2e/cases/externals/node/index.test.ts @@ -1,17 +1,17 @@ import { join } from 'node:path'; import { expect, test } from 'vitest'; -import { buildAndGetResults } from '#shared'; +import { buildAndGetEntryJsResults } from '#shared'; test('auto externalize Node.js built-in modules when platform is "node"', async () => { const fixturePath = join(__dirname); - const { entries } = await buildAndGetResults(fixturePath); + const { contents } = await buildAndGetEntryJsResults(fixturePath); for (const external of [ 'import * as __WEBPACK_EXTERNAL_MODULE_fs__ from "fs"', 'import * as __WEBPACK_EXTERNAL_MODULE_node_assert__ from "node:assert"', 'import * as __WEBPACK_EXTERNAL_MODULE_react__ from "react"', ]) { - expect(entries.esm).toContain(external); + expect(contents.esm).toContain(external); } for (const external of [ @@ -19,6 +19,6 @@ test('auto externalize Node.js built-in modules when platform is "node"', async 'var external_node_assert_namespaceObject = require("node:assert");', 'var external_react_namespaceObject = require("react");', ]) { - expect(entries.cjs).toContain(external); + expect(contents.cjs).toContain(external); } }); diff --git a/e2e/scripts/shared.ts b/e2e/scripts/shared.ts index cca73fd17..da256b60c 100644 --- a/e2e/scripts/shared.ts +++ b/e2e/scripts/shared.ts @@ -38,7 +38,8 @@ export function generateBundleCjsConfig( } export async function getEntryJsResults(rslibConfig: RslibConfig) { - const results: Record = {}; + const files: Record = {}; + const contents: Record = {}; for (const libConfig of rslibConfig.lib) { const result = await globContentJSON(libConfig?.output?.distPath?.root!, { @@ -46,19 +47,28 @@ export async function getEntryJsResults(rslibConfig: RslibConfig) { ignore: ['/**/*.map'], }); - const entryJs = Object.keys(result).find((file) => file.endsWith('.js')); + const entryJs = Object.keys(result).find((file) => + /\.(js|cjs|mjs)$/.test(file), + ); if (entryJs) { - results[libConfig.format!] = result[entryJs]!; + files[libConfig.format!] = entryJs; + contents[libConfig.format!] = result[entryJs]!; } } - return results; + return { + files, + contents, + }; } -export const buildAndGetResults = async (fixturePath: string) => { +export const buildAndGetEntryJsResults = async (fixturePath: string) => { const rslibConfig = await loadConfig(join(fixturePath, 'rslib.config.ts')); await build(rslibConfig); - const entries = await getEntryJsResults(rslibConfig); - return { entries }; + const results = await getEntryJsResults(rslibConfig); + return { + contents: results.contents, + files: results.files, + }; }; diff --git a/packages/core/src/config.ts b/packages/core/src/config.ts index 2ca0e7aa0..75d2d69fa 100644 --- a/packages/core/src/config.ts +++ b/packages/core/src/config.ts @@ -10,11 +10,13 @@ import { DEFAULT_CONFIG_NAME, DEFAULT_EXTENSIONS } from './constant'; import type { Format, LibConfig, + Platform, RslibConfig, RslibConfigAsyncFn, RslibConfigExport, RslibConfigSyncFn, } from './types/config'; +import { getDefaultExtension } from './utils/extension'; import { color } from './utils/helper'; import { nodeBuiltInModules } from './utils/helper'; import { logger } from './utils/logger'; @@ -90,15 +92,10 @@ export async function createInternalRsbuildConfig(): Promise { }); } -export function convertLibConfigToRsbuildConfig( - libConfig: LibConfig, - rsbuildConfig: RsbuildConfig, -): RsbuildConfig { - const { format, platform = 'browser' } = libConfig; - let formatConfig: RsbuildConfig = {}; +const getDefaultFormatConfig = (format: Format): RsbuildConfig => { switch (format) { case 'esm': - formatConfig = { + return { tools: { rspack: { externalsType: 'module', @@ -118,9 +115,8 @@ export function convertLibConfigToRsbuildConfig( }, }, }; - break; case 'cjs': - formatConfig = { + return { tools: { rspack: { externalsType: 'commonjs', @@ -132,9 +128,8 @@ export function convertLibConfigToRsbuildConfig( }, }, }; - break; case 'umd': - formatConfig = { + return { tools: { rspack: { externalsType: 'umd', @@ -146,18 +141,17 @@ export function convertLibConfigToRsbuildConfig( }, }, }; - break; default: - throw new Error(`Unsupported format: ${libConfig.format}`); + throw new Error(`Unsupported format: ${format}`); } +}; - let platformConfig: RsbuildConfig = {}; +const getDefaultPlatformConfig = (platform: Platform): RsbuildConfig => { switch (platform) { case 'browser': - platformConfig = {}; - break; + return {}; case 'node': - platformConfig = { + return { output: { // When output.target is 'node', Node.js's built-in will be treated as externals of type `node-commonjs`. // Simply override the built-in modules to make them external. @@ -166,15 +160,53 @@ export function convertLibConfigToRsbuildConfig( target: 'node', }, }; - break; case 'neutral': - platformConfig = {}; - break; + return {}; default: - throw new Error(`Unsupported platform: ${libConfig.platform}`); + throw new Error(`Unsupported platform: ${platform}`); } +}; + +const getDefaultAutoExtensionConfig = ( + format: Format, + root: string, + autoExtension: boolean, +): RsbuildConfig => { + const { jsExtension } = getDefaultExtension({ + format, + root, + autoExtension, + }); - return mergeRsbuildConfig(rsbuildConfig, formatConfig, platformConfig); + return { + output: { + filename: { + js: `[name]${jsExtension}`, + }, + }, + }; +}; + +export function convertLibConfigToRsbuildConfig( + libConfig: LibConfig, + rsbuildConfig: RsbuildConfig, +): RsbuildConfig { + const { format, platform = 'browser', autoExtension = false } = libConfig; + + const formatConfig = getDefaultFormatConfig(format!); + const platformConfig = getDefaultPlatformConfig(platform); + const autoExtensionConfig = getDefaultAutoExtensionConfig( + format!, + dirname(rsbuildConfig._privateMeta?.configFilePath ?? process.cwd()), + autoExtension, + ); + + return mergeRsbuildConfig( + rsbuildConfig, + formatConfig, + platformConfig, + autoExtensionConfig, + ); } export async function composeCreateRsbuildConfig( diff --git a/packages/core/src/types/config/index.ts b/packages/core/src/types/config/index.ts index 0d9cd6137..b3dad7b31 100644 --- a/packages/core/src/types/config/index.ts +++ b/packages/core/src/types/config/index.ts @@ -1,10 +1,12 @@ import type { RsbuildConfig } from '@rsbuild/core'; export type Format = 'esm' | 'cjs' | 'umd'; +export type Platform = 'node' | 'browser' | 'neutral'; export interface LibConfig extends RsbuildConfig { format?: Format; - platform?: 'node' | 'browser' | 'neutral'; + platform?: Platform; + autoExtension?: boolean; } export interface RslibConfig extends RsbuildConfig { diff --git a/packages/core/src/utils/extension.ts b/packages/core/src/utils/extension.ts new file mode 100644 index 000000000..a185e573a --- /dev/null +++ b/packages/core/src/utils/extension.ts @@ -0,0 +1,53 @@ +import fs from 'node:fs'; +import { resolve } from 'node:path'; +import type { Format } from 'src/types/config'; +import { logger } from './logger'; + +export const getDefaultExtension = (options: { + format: Format; + root: string; + autoExtension: boolean; +}) => { + const { format, root, autoExtension } = options; + + let jsExtension = '.js'; + let dtsExtension = '.d.ts'; + + if (!autoExtension) { + return { + jsExtension, + dtsExtension, + }; + } + + let isModule = false; + + try { + const json = JSON.parse( + fs.readFileSync(resolve(root, './package.json'), 'utf8'), + ); + isModule = json.type === 'module'; + } catch (e) { + logger.warn(`package.json is broken in ${root}`); + return { + jsExtension, + dtsExtension, + }; + } + + if (isModule && format === 'cjs') { + jsExtension = '.cjs'; + dtsExtension = '.d.cts'; + } + + if (!isModule && format === 'esm') { + jsExtension = '.mjs'; + dtsExtension = '.d.mts'; + } + + return { + jsExtension, + dtsExtension, + isModule, + }; +}; diff --git a/packages/core/tests/__snapshots__/config.test.ts.snap b/packages/core/tests/__snapshots__/config.test.ts.snap index c562e0ceb..b3d4a5827 100644 --- a/packages/core/tests/__snapshots__/config.test.ts.snap +++ b/packages/core/tests/__snapshots__/config.test.ts.snap @@ -10,6 +10,9 @@ exports[`Should compose create Rsbuild config correctly > Merge Rsbuild config 1 "distPath": { "js": "./", }, + "filename": { + "js": "[name].js", + }, "filenameHash": false, "minify": false, }, @@ -44,6 +47,9 @@ exports[`Should compose create Rsbuild config correctly > Merge Rsbuild config 1 "distPath": { "js": "./", }, + "filename": { + "js": "[name].js", + }, "filenameHash": false, "minify": false, }, @@ -82,6 +88,9 @@ exports[`Should compose create Rsbuild config correctly > Merge Rsbuild config 1 "distPath": { "js": "./", }, + "filename": { + "js": "[name].js", + }, "filenameHash": false, "minify": false, },