diff --git a/packages/core/src/config.ts b/packages/core/src/config.ts index 2b99c8866..0e6af6556 100644 --- a/packages/core/src/config.ts +++ b/packages/core/src/config.ts @@ -5,6 +5,7 @@ import { type RsbuildConfig, type RsbuildPlugin, type RsbuildPlugins, + type Rspack, defineConfig as defineRsbuildConfig, loadConfig as loadRsbuildConfig, mergeRsbuildConfig, @@ -38,6 +39,7 @@ import type { DeepRequired, ExcludesFalse, Format, + JsRedirect, LibConfig, LibOnlyConfig, PkgJson, @@ -50,6 +52,7 @@ import type { RslibConfigAsyncFn, RslibConfigExport, RslibConfigSyncFn, + RspackResolver, Shims, Syntax, } from './types'; @@ -63,6 +66,7 @@ import { isIntermediateOutputFormat, isObject, nodeBuiltInModules, + normalizeSlash, omit, pick, readPackageJson, @@ -956,64 +960,113 @@ const composeEntryConfig = async ( }; }; -const composeBundleConfig = ( +const composeBundlelessExternalConfig = ( jsExtension: string, redirect: Redirect, cssModulesAuto: CssLoaderOptionsAuto, bundle: boolean, -): RsbuildConfig => { - if (bundle) return {}; +): { + config: RsbuildConfig; + resolvedJsRedirect?: DeepRequired; +} => { + if (bundle) return { config: {} }; + + const isStyleRedirected = redirect.style ?? true; + const jsRedirectPath = redirect.js?.path ?? true; + const jsRedirectExtension = redirect.js?.extension ?? true; - const isStyleRedirect = redirect.style ?? true; + let resolver: RspackResolver | undefined; return { - output: { - externals: [ - (data: any, callback: any) => { - // Issuer is not empty string when the module is imported by another module. - // Prevent from externalizing entry modules here. - if (data.contextInfo.issuer) { - // Node.js ECMAScript module loader does no extension searching. - // Add a file extension according to autoExtension config - // when data.request is a relative path and do not have an extension. - // If data.request already have an extension, we replace it with new extension - // This may result in a change in semantics, - // user should use copy to keep origin file or use another separate entry to deal this - let request: string = data.request; - - const cssExternal = cssExternalHandler( - request, - callback, - jsExtension, - cssModulesAuto, - isStyleRedirect, - ); - - if (cssExternal !== false) { - return cssExternal; + resolvedJsRedirect: { + path: jsRedirectPath, + extension: jsRedirectExtension, + }, + config: { + output: { + externals: [ + async (data, callback) => { + const { request, getResolve, context, contextInfo } = data; + if (!request || !getResolve || !context || !contextInfo) { + return callback(); + } + + if (!resolver) { + resolver = (await getResolve()) as RspackResolver; } - if (request[0] === '.') { - const ext = extname(request); + // Issuer is not empty string when the module is imported by another module. + // Prevent from externalizing entry modules here. + if (contextInfo.issuer) { + let resolvedRequest: string = request; + + const cssExternal = cssExternalHandler( + resolvedRequest, + callback, + jsExtension, + cssModulesAuto, + isStyleRedirected, + ); + + if (cssExternal !== false) { + return cssExternal; + } + + if (jsRedirectPath) { + try { + resolvedRequest = await resolver(context, resolvedRequest); + } catch (e) { + // Do nothing, fallthrough to other external matches. + logger.debug( + `Failed to resolve ${resolvedRequest} with resolver`, + ); + } + + resolvedRequest = normalizeSlash( + path.relative( + path.dirname(contextInfo.issuer), + resolvedRequest, + ), + ); + + // Requests that fall through here cannot be matched by any other externals config ahead. + // Treat all these requests as relative import of source code. Node.js won't add the + // leading './' to the relative path resolved by `path.relative`. So add manually it here. + if (resolvedRequest[0] !== '.') { + resolvedRequest = `./${resolvedRequest}`; + } + } - if (ext) { - if (JS_EXTENSIONS_PATTERN.test(request)) { - request = request.replace(/\.[^.]+$/, jsExtension); + // Node.js ECMAScript module loader does no extension searching. + // Add a file extension according to autoExtension config + // when data.request is a relative path and do not have an extension. + // If data.request already have an extension, we replace it with new extension + // This may result in a change in semantics, + // user should use copy to keep origin file or use another separate entry to deal this + if (jsRedirectExtension) { + const ext = extname(resolvedRequest); + if (ext) { + if (JS_EXTENSIONS_PATTERN.test(resolvedRequest)) { + resolvedRequest = resolvedRequest.replace( + /\.[^.]+$/, + jsExtension, + ); + } else { + // If it does not match jsExtensionsPattern, we should do nothing, eg: ./foo.png + return callback(); + } } else { - // If it does not match jsExtensionsPattern, we should do nothing, eg: ./foo.png - return callback(); + resolvedRequest = `${resolvedRequest}${jsExtension}`; } - } else { - // TODO: add redirect.extension option - request = `${request}${jsExtension}`; } + + return callback(undefined, resolvedRequest); } - return callback(null, request); - } - callback(); - }, - ], + callback(); + }, + ] as Rspack.ExternalItem[], + }, }, }; }; @@ -1054,17 +1107,15 @@ const composeDtsConfig = async ( }; const composeTargetConfig = ( - target: RsbuildConfigOutputTarget, + userTarget: RsbuildConfigOutputTarget, format: Format, ): { config: RsbuildConfig; + externalsConfig: RsbuildConfig; target: RsbuildConfigOutputTarget; } => { - let defaultTarget = target; - if (!defaultTarget) { - defaultTarget = format === 'mf' ? 'web' : 'node'; - } - switch (defaultTarget) { + const target = userTarget ?? (format === 'mf' ? 'web' : 'node'); + switch (target) { case 'web': return { config: { @@ -1075,6 +1126,7 @@ const composeTargetConfig = ( }, }, target: 'web', + externalsConfig: {}, }; case 'node': return { @@ -1084,15 +1136,19 @@ const composeTargetConfig = ( target: ['node'], }, }, + output: { + target: 'node', + }, + }, + target: 'node', + externalsConfig: { 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. // https://github.com/webpack/webpack/blob/dd44b206a9c50f4b4cb4d134e1a0bd0387b159a3/lib/node/NodeTargetPlugin.js#L81 externals: nodeBuiltInModules, - target: 'node', }, }, - target: 'node', }; // TODO: Support `neutral` target, however Rsbuild don't list it as an option in the target field. // case 'neutral': @@ -1104,7 +1160,7 @@ const composeTargetConfig = ( // }, // }; default: - throw new Error(`Unsupported platform: ${defaultTarget}`); + throw new Error(`Unsupported platform: ${target}`); } }; @@ -1189,7 +1245,7 @@ async function composeLibRsbuildConfig( externalHelpers, pkgJson, ); - const externalsConfig = composeExternalsConfig( + const userExternalsConfig = composeExternalsConfig( format!, config.output?.externals, ); @@ -1198,16 +1254,17 @@ async function composeLibRsbuildConfig( jsExtension, dtsExtension, } = composeAutoExtensionConfig(config, autoExtension, pkgJson); - const bundleConfig = composeBundleConfig( + const { config: bundlelessExternalConfig } = composeBundlelessExternalConfig( jsExtension, redirect, cssModulesAuto, bundle, ); - const { config: targetConfig, target } = composeTargetConfig( - config.output?.target, - format!, - ); + const { + config: targetConfig, + externalsConfig: targetExternalsConfig, + target, + } = composeTargetConfig(config.output?.target, format!); const syntaxConfig = composeSyntaxConfig(target, config?.syntax); const autoExternalConfig = composeAutoExternalConfig({ format: format!, @@ -1231,7 +1288,7 @@ async function composeLibRsbuildConfig( const externalsWarnConfig = composeExternalsWarnConfig( format!, autoExternalConfig?.output?.externals, - externalsConfig?.output?.externals, + userExternalsConfig?.output?.externals, ); const minifyConfig = composeMinifyConfig(config); const bannerFooterConfig = composeBannerFooterConfig(banner, footer); @@ -1243,15 +1300,23 @@ async function composeLibRsbuildConfig( return mergeRsbuildConfig( formatConfig, shimsConfig, + syntaxConfig, externalHelpersConfig, - // externalsWarnConfig should before other externals config - externalsWarnConfig, - externalsConfig, - autoExternalConfig, autoExtensionConfig, - syntaxConfig, - bundleConfig, targetConfig, + // #region Externals configs + // The order of the externals config should come in the following order: + // 1. `externalsWarnConfig` should come before other externals config to touch the externalized modules first. + // 2. The externals config in `bundlelessExternalConfig` should present after other externals config as + // it relies on other externals config to bail out the externalized modules first then resolve + // the correct path for relative imports. + // 3. `userExternalsConfig` should present later to override the externals config of the ahead ones. + externalsWarnConfig, + autoExternalConfig, + targetExternalsConfig, + userExternalsConfig, + bundlelessExternalConfig, + // #endregion entryConfig, cssConfig, assetConfig, diff --git a/packages/core/src/css/cssConfig.ts b/packages/core/src/css/cssConfig.ts index 1a6dca064..b5e6ab94f 100644 --- a/packages/core/src/css/cssConfig.ts +++ b/packages/core/src/css/cssConfig.ts @@ -77,7 +77,7 @@ export function isCssGlobalFile( return !isCssModules; } -type ExternalCallback = (arg0?: null, arg1?: string) => void; +type ExternalCallback = (arg0?: undefined, arg1?: string) => void; export function cssExternalHandler( request: string, @@ -99,12 +99,12 @@ export function cssExternalHandler( if (request[0] === '.' && isCssFile(request)) { // preserve import './CounterButton.module.scss' if (!isStyleRedirect) { - return callback(null, request); + return callback(undefined, request); } if (isCssModulesRequest) { - return callback(null, request.replace(/\.[^.]+$/, jsExtension)); + return callback(undefined, request.replace(/\.[^.]+$/, jsExtension)); } - return callback(null, request.replace(/\.[^.]+$/, '.css')); + return callback(undefined, request.replace(/\.[^.]+$/, '.css')); } return false; diff --git a/packages/core/src/types/config/index.ts b/packages/core/src/types/config.ts similarity index 84% rename from packages/core/src/types/config/index.ts rename to packages/core/src/types/config.ts index 1c0d7839d..8a45c09ed 100644 --- a/packages/core/src/types/config/index.ts +++ b/packages/core/src/types/config.ts @@ -1,5 +1,6 @@ -import type { RsbuildConfig } from '@rsbuild/core'; +import type { RsbuildConfig, Rspack } from '@rsbuild/core'; import type { PluginDtsOptions } from 'rsbuild-plugin-dts'; +import type { GetAsyncFunctionFromUnion } from './utils'; export type Format = 'esm' | 'cjs' | 'umd' | 'mf'; @@ -28,6 +29,9 @@ export type RsbuildConfigEntry = NonNullable< NonNullable['entry'] >; export type RsbuildConfigEntryItem = RsbuildConfigEntry[string]; +export type RspackResolver = GetAsyncFunctionFromUnion< + ReturnType> +>; export type RsbuildConfigOutputTarget = NonNullable< RsbuildConfig['output'] @@ -74,12 +78,33 @@ export type Shims = { }; }; +export type JsRedirect = { + /** + * Whether to automatically redirect the import paths of JavaScript output files. + * @defaultValue `true` + */ + path?: boolean; + /** + * Whether to automatically add the file extension to import paths based on the JavaScript output files. + * @defaultValue `true` + */ + extension?: boolean; +}; + +// @ts-expect-error TODO: support dts redirect in the future +type DtsRedirect = { + path?: boolean; + extension?: boolean; +}; + export type Redirect = { - // TODO: support other redirects - // alias?: boolean; + /** Controls the redirect of the import paths of output JavaScript files. */ + js?: JsRedirect; + /** Whether to redirect the import path of the style file. */ style?: boolean; + // TODO: support other redirects // asset?: boolean; - // autoExtension?: boolean; + // dts?: DtsRedirect; }; export interface LibConfig extends RsbuildConfig { diff --git a/packages/core/src/types/utils.ts b/packages/core/src/types/utils.ts index 77e0b962c..ac9b0bb8f 100644 --- a/packages/core/src/types/utils.ts +++ b/packages/core/src/types/utils.ts @@ -12,3 +12,9 @@ export type DeepRequired = Required<{ }>; export type ExcludesFalse = (x: T | false | undefined | null) => x is T; + +export type GetAsyncFunctionFromUnion = T extends ( + ...args: any[] +) => Promise + ? T + : never; diff --git a/packages/core/src/utils/helper.ts b/packages/core/src/utils/helper.ts index 0fefa76a1..302ddbc93 100644 --- a/packages/core/src/utils/helper.ts +++ b/packages/core/src/utils/helper.ts @@ -241,3 +241,8 @@ export const isIntermediateOutputFormat = (format: Format): boolean => { }; export { color }; + +const windowsSlashRegex = /\\/g; +export function normalizeSlash(p: string): string { + return p.replace(windowsSlashRegex, '/'); +} diff --git a/packages/core/tests/__snapshots__/config.test.ts.snap b/packages/core/tests/__snapshots__/config.test.ts.snap index 0e4cbf584..4fe3fc0ed 100644 --- a/packages/core/tests/__snapshots__/config.test.ts.snap +++ b/packages/core/tests/__snapshots__/config.test.ts.snap @@ -174,7 +174,6 @@ exports[`Should compose create Rsbuild config correctly > Merge Rsbuild config 1 "experiments": { "outputModule": true, }, - "externalsType": "module-import", "module": { "parser": { "javascript": { @@ -211,6 +210,9 @@ exports[`Should compose create Rsbuild config correctly > Merge Rsbuild config 1 "node", ], }, + { + "externalsType": "module-import", + }, { "plugins": [ EntryChunkPlugin { @@ -414,7 +416,6 @@ exports[`Should compose create Rsbuild config correctly > Merge Rsbuild config 1 }, }, { - "externalsType": "commonjs-import", "module": { "parser": { "javascript": { @@ -443,6 +444,9 @@ exports[`Should compose create Rsbuild config correctly > Merge Rsbuild config 1 "node", ], }, + { + "externalsType": "commonjs-import", + }, { "plugins": [ EntryChunkPlugin { @@ -637,7 +641,6 @@ exports[`Should compose create Rsbuild config correctly > Merge Rsbuild config 1 }, }, { - "externalsType": "umd", "module": { "parser": { "javascript": { @@ -661,6 +664,9 @@ exports[`Should compose create Rsbuild config correctly > Merge Rsbuild config 1 "node", ], }, + { + "externalsType": "umd", + }, { "plugins": [ EntryChunkPlugin { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 526b09bbe..625757ce1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -349,7 +349,7 @@ importers: dependencies: magic-string: specifier: ^0.30.15 - version: 0.30.15 + version: 0.30.17 picocolors: specifier: 1.1.1 version: 1.1.1 @@ -489,12 +489,16 @@ importers: tests/integration/auto-extension/type-commonjs/false: {} + tests/integration/auto-extension/type-commonjs/false-bundleless: {} + tests/integration/auto-extension/type-module/config-override: {} tests/integration/auto-extension/type-module/default: {} tests/integration/auto-extension/type-module/false: {} + tests/integration/auto-extension/type-module/false-bundleless: {} + tests/integration/auto-external/default: dependencies: ora: @@ -713,6 +717,17 @@ importers: specifier: ^0.11.0 version: 0.11.0(@babel/core@7.26.0) + tests/integration/redirect/js: + devDependencies: + '@types/lodash': + specifier: ^4.17.13 + version: 4.17.13 + lodash: + specifier: ^4.17.21 + version: 4.17.21 + + tests/integration/redirect/style-false: {} + tests/integration/require/import-dynamic: {} tests/integration/require/require-as-expression: {} @@ -777,8 +792,6 @@ importers: specifier: 2.0.0 version: 2.0.0 - tests/integration/style/redirect-style-false: {} - tests/integration/style/sass/bundle: {} tests/integration/style/sass/bundle-false: {} @@ -4381,8 +4394,8 @@ packages: resolution: {integrity: sha512-rh+Zjr6DNfUYR3bPwJEnuwDdqMbxZW7LOQfUN4B54+Cl+0o5zaU9RJ6bcidfDtC1cWCZXQ+nvX8bf6bAji37QQ==} engines: {node: '>=12'} - magic-string@0.30.15: - resolution: {integrity: sha512-zXeaYRgZ6ldS1RJJUrMrYgNJ4fdwnyI6tVqoiIhyCyv5IVTK9BU8Ic2l253GGETQHxI4HNUwhJ3fjDhKqEoaAw==} + magic-string@0.30.17: + resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} make-dir@3.1.0: resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==} @@ -8291,7 +8304,7 @@ snapshots: dependencies: '@vitest/spy': 2.1.8 estree-walker: 3.0.3 - magic-string: 0.30.15 + magic-string: 0.30.17 optionalDependencies: vite: 5.3.3(@types/node@22.8.1)(terser@5.31.6) @@ -8307,7 +8320,7 @@ snapshots: '@vitest/snapshot@2.1.8': dependencies: '@vitest/pretty-format': 2.1.8 - magic-string: 0.30.15 + magic-string: 0.30.17 pathe: 1.1.2 '@vitest/spy@2.1.8': @@ -8353,7 +8366,7 @@ snapshots: '@vue/compiler-ssr': 3.5.13 '@vue/shared': 3.5.13 estree-walker: 2.0.2 - magic-string: 0.30.15 + magic-string: 0.30.17 postcss: 8.4.49 source-map-js: 1.2.1 @@ -10408,7 +10421,7 @@ snapshots: luxon@3.5.0: {} - magic-string@0.30.15: + magic-string@0.30.17: dependencies: '@jridgewell/sourcemap-codec': 1.5.0 @@ -11731,7 +11744,7 @@ snapshots: rollup-plugin-dts@6.1.1(rollup@4.18.1)(typescript@5.6.3): dependencies: - magic-string: 0.30.15 + magic-string: 0.30.17 rollup: 4.18.1 typescript: 5.6.3 optionalDependencies: @@ -11762,7 +11775,7 @@ snapshots: rsbuild-plugin-dts@0.1.4(@microsoft/api-extractor@7.48.1(@types/node@22.8.1))(@rsbuild/core@1.1.10)(typescript@5.6.3): dependencies: '@rsbuild/core': 1.1.10 - magic-string: 0.30.15 + magic-string: 0.30.17 picocolors: 1.1.1 tinyglobby: 0.2.10 optionalDependencies: @@ -12123,7 +12136,7 @@ snapshots: constants-browserify: 1.0.0 es-module-lexer: 1.5.4 fs-extra: 11.2.0 - magic-string: 0.30.15 + magic-string: 0.30.17 path-browserify: 1.0.1 process: 0.11.10 rsbuild-plugin-html-minifier-terser: 1.1.1(@rsbuild/core@1.1.10) @@ -12148,7 +12161,7 @@ snapshots: '@storybook/react-docgen-typescript-plugin': 1.0.1(typescript@5.6.3)(webpack@5.96.1) '@types/node': 18.19.64 find-up: 5.0.0 - magic-string: 0.30.15 + magic-string: 0.30.17 react: 19.0.0 react-docgen: 7.1.0 react-dom: 19.0.0(react@19.0.0) @@ -12682,7 +12695,7 @@ snapshots: chai: 5.1.2 debug: 4.3.7 expect-type: 1.1.0 - magic-string: 0.30.15 + magic-string: 0.30.17 pathe: 1.1.2 std-env: 3.8.0 tinybench: 2.9.0 diff --git a/scripts/dictionary.txt b/scripts/dictionary.txt index 800513793..da7eef768 100644 --- a/scripts/dictionary.txt +++ b/scripts/dictionary.txt @@ -51,6 +51,7 @@ jfif jiti jscpuprofile jsesc +jsxs koppers lightningcss liyincode diff --git a/tests/integration/auto-extension/__fixtures__/src/common.ts b/tests/integration/auto-extension/__fixtures__/src/common.ts deleted file mode 100644 index cc798ff50..000000000 --- a/tests/integration/auto-extension/__fixtures__/src/common.ts +++ /dev/null @@ -1 +0,0 @@ -export const a = 1; diff --git a/tests/integration/auto-extension/__fixtures__/src/foo.ts b/tests/integration/auto-extension/__fixtures__/src/foo.ts new file mode 100644 index 000000000..3329a7d97 --- /dev/null +++ b/tests/integration/auto-extension/__fixtures__/src/foo.ts @@ -0,0 +1 @@ +export const foo = 'foo'; diff --git a/tests/integration/auto-extension/__fixtures__/src/index.ts b/tests/integration/auto-extension/__fixtures__/src/index.ts index b5f5ad8ce..d75e8c5fb 100644 --- a/tests/integration/auto-extension/__fixtures__/src/index.ts +++ b/tests/integration/auto-extension/__fixtures__/src/index.ts @@ -1,3 +1,2 @@ -import { a } from './common'; - -console.log(a); +export { foo } from './foo'; +export * from './utils'; diff --git a/tests/integration/auto-extension/__fixtures__/src/utils/index.ts b/tests/integration/auto-extension/__fixtures__/src/utils/index.ts new file mode 100644 index 000000000..9f1738685 --- /dev/null +++ b/tests/integration/auto-extension/__fixtures__/src/utils/index.ts @@ -0,0 +1 @@ +export const bar = 'bar'; diff --git a/tests/integration/auto-extension/index.test.ts b/tests/integration/auto-extension/index.test.ts index fd617a583..e56fbf7fc 100644 --- a/tests/integration/auto-extension/index.test.ts +++ b/tests/integration/auto-extension/index.test.ts @@ -1,5 +1,5 @@ import { extname, join } from 'node:path'; -import { buildAndGetResults } from 'test-helper'; +import { buildAndGetResults, queryContent } from 'test-helper'; import { describe, expect, test } from 'vitest'; describe('autoExtension: true', () => { @@ -40,7 +40,7 @@ describe('should respect output.filename.js to override builtin logic', () => { const { entryFiles } = await buildAndGetResults({ fixturePath }); expect(extname(entryFiles.esm!)).toEqual('.mjs'); expect(entryFiles.cjs).toMatchInlineSnapshot( - `"/tests/integration/auto-extension/type-commonjs/config-override/dist/cjs/index.15d386b8.js"`, + `"/tests/integration/auto-extension/type-commonjs/config-override/dist/cjs/index.1310c114.js"`, ); }); @@ -48,8 +48,40 @@ describe('should respect output.filename.js to override builtin logic', () => { const fixturePath = join(__dirname, 'type-module', 'config-override'); const { entryFiles } = await buildAndGetResults({ fixturePath }); expect(entryFiles.esm).toMatchInlineSnapshot( - `"/tests/integration/auto-extension/type-module/config-override/dist/esm/index.d2068839.js"`, + `"/tests/integration/auto-extension/type-module/config-override/dist/esm/index.996a7edd.js"`, ); expect(extname(entryFiles.cjs!)).toEqual('.cjs'); }); }); + +describe('ESM output should add main files automatically', () => { + test('type is commonjs', async () => { + const fixturePath = join(__dirname, 'type-commonjs', 'false-bundleless'); + const { contents } = await buildAndGetResults({ fixturePath }); + const { path: indexFile } = queryContent(contents.esm, 'index.js', { + basename: true, + }); + + expect(await import(indexFile)).toMatchInlineSnapshot(` + { + "bar": "bar", + "foo": "foo", + } + `); + }); + + test('type is module', async () => { + const fixturePath = join(__dirname, 'type-module', 'false-bundleless'); + const { contents } = await buildAndGetResults({ fixturePath }); + const { path: indexFile } = queryContent(contents.esm, 'index.js', { + basename: true, + }); + + expect(await import(indexFile)).toMatchInlineSnapshot(` + { + "bar": "bar", + "foo": "foo", + } + `); + }); +}); diff --git a/tests/integration/auto-extension/type-commonjs/config-override/package.json b/tests/integration/auto-extension/type-commonjs/config-override/package.json index 0e8db2f59..cc3760a0b 100644 --- a/tests/integration/auto-extension/type-commonjs/config-override/package.json +++ b/tests/integration/auto-extension/type-commonjs/config-override/package.json @@ -1,5 +1,5 @@ { - "name": "auto-extension-config-override-commonjs-test", + "name": "auto-extension-commonjs-config-override-test", "version": "1.0.0", "private": true } diff --git a/tests/integration/auto-extension/type-commonjs/default/package.json b/tests/integration/auto-extension/type-commonjs/default/package.json index 4560b004e..1d447bb38 100644 --- a/tests/integration/auto-extension/type-commonjs/default/package.json +++ b/tests/integration/auto-extension/type-commonjs/default/package.json @@ -1,5 +1,5 @@ { - "name": "auto-extension-commonjs-test", + "name": "auto-extension-commonjs-default-test", "version": "1.0.0", "private": true } diff --git a/tests/integration/auto-extension/type-commonjs/false-bundleless/package.json b/tests/integration/auto-extension/type-commonjs/false-bundleless/package.json new file mode 100644 index 000000000..a940c8d46 --- /dev/null +++ b/tests/integration/auto-extension/type-commonjs/false-bundleless/package.json @@ -0,0 +1,6 @@ +{ + "name": "auto-extension-commonjs-false-bundleless-test", + "version": "1.0.0", + "private": true, + "type": "module" +} diff --git a/tests/integration/auto-extension/type-commonjs/false-bundleless/rslib.config.ts b/tests/integration/auto-extension/type-commonjs/false-bundleless/rslib.config.ts new file mode 100644 index 000000000..ffe22d8af --- /dev/null +++ b/tests/integration/auto-extension/type-commonjs/false-bundleless/rslib.config.ts @@ -0,0 +1,20 @@ +import { defineConfig } from '@rslib/core'; +import { generateBundleCjsConfig, generateBundleEsmConfig } from 'test-helper'; + +export default defineConfig({ + lib: [ + generateBundleEsmConfig({ + bundle: false, + autoExtension: false, + }), + generateBundleCjsConfig({ + bundle: false, + autoExtension: false, + }), + ], + source: { + entry: { + index: '../../__fixtures__/src', + }, + }, +}); diff --git a/tests/integration/auto-extension/type-commonjs/false/package.json b/tests/integration/auto-extension/type-commonjs/false/package.json index 7e90e60e9..99a8c4cef 100644 --- a/tests/integration/auto-extension/type-commonjs/false/package.json +++ b/tests/integration/auto-extension/type-commonjs/false/package.json @@ -1,5 +1,5 @@ { - "name": "auto-extension-false-commonjs-test", + "name": "auto-extension-commonjs-false--test", "version": "1.0.0", "private": true } diff --git a/tests/integration/auto-extension/type-module/config-override/package.json b/tests/integration/auto-extension/type-module/config-override/package.json index 778081e6d..4f1ddd497 100644 --- a/tests/integration/auto-extension/type-module/config-override/package.json +++ b/tests/integration/auto-extension/type-module/config-override/package.json @@ -1,5 +1,5 @@ { - "name": "auto-extension-config-override-module-test", + "name": "auto-extension-module-config-override-test", "version": "1.0.0", "private": true, "type": "module" diff --git a/tests/integration/auto-extension/type-module/default/package.json b/tests/integration/auto-extension/type-module/default/package.json index 3836bd261..1f060b22c 100644 --- a/tests/integration/auto-extension/type-module/default/package.json +++ b/tests/integration/auto-extension/type-module/default/package.json @@ -1,5 +1,5 @@ { - "name": "auto-extension-module-test", + "name": "auto-extension-module-default-test", "version": "1.0.0", "private": true, "type": "module" diff --git a/tests/integration/auto-extension/type-module/false-bundleless/package.json b/tests/integration/auto-extension/type-module/false-bundleless/package.json new file mode 100644 index 000000000..df0fea149 --- /dev/null +++ b/tests/integration/auto-extension/type-module/false-bundleless/package.json @@ -0,0 +1,6 @@ +{ + "name": "auto-extension-module-false-bundleless-test", + "version": "1.0.0", + "private": true, + "type": "module" +} diff --git a/tests/integration/auto-extension/type-module/false-bundleless/rslib.config.ts b/tests/integration/auto-extension/type-module/false-bundleless/rslib.config.ts new file mode 100644 index 000000000..ffe22d8af --- /dev/null +++ b/tests/integration/auto-extension/type-module/false-bundleless/rslib.config.ts @@ -0,0 +1,20 @@ +import { defineConfig } from '@rslib/core'; +import { generateBundleCjsConfig, generateBundleEsmConfig } from 'test-helper'; + +export default defineConfig({ + lib: [ + generateBundleEsmConfig({ + bundle: false, + autoExtension: false, + }), + generateBundleCjsConfig({ + bundle: false, + autoExtension: false, + }), + ], + source: { + entry: { + index: '../../__fixtures__/src', + }, + }, +}); diff --git a/tests/integration/auto-extension/type-module/false/package.json b/tests/integration/auto-extension/type-module/false/package.json index 5a33607e1..430ef9188 100644 --- a/tests/integration/auto-extension/type-module/false/package.json +++ b/tests/integration/auto-extension/type-module/false/package.json @@ -1,5 +1,5 @@ { - "name": "auto-extension-false-module-test", + "name": "auto-extension-module-false-test", "version": "1.0.0", "private": true, "type": "module" diff --git a/tests/integration/bundle-false/basic/src/index.ts b/tests/integration/bundle-false/basic/src/index.ts index 6eb6b6fb1..e218fd0f6 100644 --- a/tests/integration/bundle-false/basic/src/index.ts +++ b/tests/integration/bundle-false/basic/src/index.ts @@ -1,3 +1,7 @@ +import { mainFiles1 } from './mainFiles1'; +export { mainFiles1 }; + +export * from './mainFiles2'; export * from './utils/numbers'; export * from './utils/strings'; export * from './sum'; diff --git a/tests/integration/bundle-false/basic/src/mainFiles1/index.ts b/tests/integration/bundle-false/basic/src/mainFiles1/index.ts new file mode 100644 index 000000000..a7880c8da --- /dev/null +++ b/tests/integration/bundle-false/basic/src/mainFiles1/index.ts @@ -0,0 +1 @@ +export const mainFiles1 = 'mainFiles1'; diff --git a/tests/integration/bundle-false/basic/src/mainFiles2/index.ts b/tests/integration/bundle-false/basic/src/mainFiles2/index.ts new file mode 100644 index 000000000..8ccac37a3 --- /dev/null +++ b/tests/integration/bundle-false/basic/src/mainFiles2/index.ts @@ -0,0 +1 @@ +export const mainFiles2 = 'mainFiles2'; diff --git a/tests/integration/bundle-false/index.test.ts b/tests/integration/bundle-false/index.test.ts index 26af6c519..c216309ce 100644 --- a/tests/integration/bundle-false/index.test.ts +++ b/tests/integration/bundle-false/index.test.ts @@ -1,27 +1,70 @@ import { join } from 'node:path'; -import { buildAndGetResults } from 'test-helper'; +import { buildAndGetResults, queryContent } from 'test-helper'; import { expect, test } from 'vitest'; test('basic', async () => { const fixturePath = join(__dirname, 'basic'); - const { files } = await buildAndGetResults({ fixturePath }); + const { files, contents } = await buildAndGetResults({ fixturePath }); expect(files.esm).toMatchInlineSnapshot(` [ "/tests/integration/bundle-false/basic/dist/esm/index.js", + "/tests/integration/bundle-false/basic/dist/esm/mainFiles1/index.js", + "/tests/integration/bundle-false/basic/dist/esm/mainFiles2/index.js", "/tests/integration/bundle-false/basic/dist/esm/sum.js", "/tests/integration/bundle-false/basic/dist/esm/utils/numbers.js", "/tests/integration/bundle-false/basic/dist/esm/utils/strings.js", ] `); + + const { path: esmIndexPath } = queryContent(contents.esm, 'index.js', { + basename: true, + }); + + expect(await import(esmIndexPath)).toMatchInlineSnapshot(` + { + "mainFiles1": "mainFiles1", + "mainFiles2": "mainFiles2", + "num1": 1, + "num2": 2, + "num3": 3, + "numSum": 6, + "str1": "str1", + "str2": "str2", + "str3": "str3", + "strSum": "str1str2str3", + } + `); + expect(files.cjs).toMatchInlineSnapshot(` [ "/tests/integration/bundle-false/basic/dist/cjs/index.cjs", + "/tests/integration/bundle-false/basic/dist/cjs/mainFiles1/index.cjs", + "/tests/integration/bundle-false/basic/dist/cjs/mainFiles2/index.cjs", "/tests/integration/bundle-false/basic/dist/cjs/sum.cjs", "/tests/integration/bundle-false/basic/dist/cjs/utils/numbers.cjs", "/tests/integration/bundle-false/basic/dist/cjs/utils/strings.cjs", ] `); + + const { path: cjsIndexPath } = queryContent(contents.cjs, 'index.cjs', { + basename: true, + }); + + expect((await import(cjsIndexPath)).default).toMatchInlineSnapshot(` + { + "mainFiles1": "mainFiles1", + "mainFiles2": "mainFiles2", + "num1": 1, + "num2": 2, + "num3": 3, + "numSum": 6, + "str1": "str1", + "str2": "str2", + "str3": "str3", + "strSum": "str1str2str3", + } + `); }); test('single file', async () => { @@ -47,7 +90,7 @@ test('auto add js extension for relative import', async () => { // basic esm for (const importer of [ 'import * as __WEBPACK_EXTERNAL_MODULE__bar_js__ from "./bar.js";', - 'import * as __WEBPACK_EXTERNAL_MODULE__baz_js__ from "./baz.js";', + 'import * as __WEBPACK_EXTERNAL_MODULE__baz_js_js__ from "./baz.js.js";', 'import * as __WEBPACK_EXTERNAL_MODULE__foo_js__ from "./foo.js";', 'import * as __WEBPACK_EXTERNAL_MODULE__qux_js__ from "./qux.js";', ]) { @@ -57,7 +100,7 @@ test('auto add js extension for relative import', async () => { // basic cjs for (const requirer of [ 'const external_bar_cjs_namespaceObject = require("./bar.cjs");', - 'const external_baz_cjs_namespaceObject = require("./baz.cjs");', + 'const external_baz_js_cjs_namespaceObject = require("./baz.js.cjs");', 'const external_foo_cjs_namespaceObject = require("./foo.cjs");', 'const external_qux_cjs_namespaceObject = require("./qux.cjs");', ]) { @@ -67,7 +110,7 @@ test('auto add js extension for relative import', async () => { // using `autoExtension: false` along with `output.filename.js` - esm for (const importer of [ 'import * as __WEBPACK_EXTERNAL_MODULE__bar_mjs__ from "./bar.mjs";', - 'import * as __WEBPACK_EXTERNAL_MODULE__baz_mjs__ from "./baz.mjs";', + 'import * as __WEBPACK_EXTERNAL_MODULE__baz_js_mjs__ from "./baz.js.mjs";', 'import * as __WEBPACK_EXTERNAL_MODULE__foo_mjs__ from "./foo.mjs";', 'import * as __WEBPACK_EXTERNAL_MODULE__qux_mjs__ from "./qux.mjs";', ]) { @@ -77,7 +120,7 @@ test('auto add js extension for relative import', async () => { // using `autoExtension: false` along with `output.filename.js` - cjs for (const requirer of [ 'const external_bar_cjs_namespaceObject = require("./bar.cjs");', - 'const external_baz_cjs_namespaceObject = require("./baz.cjs");', + 'const external_baz_js_cjs_namespaceObject = require("./baz.js.cjs");', 'const external_foo_cjs_namespaceObject = require("./foo.cjs");', 'const external_qux_cjs_namespaceObject = require("./qux.cjs");', ]) { diff --git a/tests/integration/directive/index.test.ts b/tests/integration/directive/index.test.ts index d950abe85..510a7b56b 100644 --- a/tests/integration/directive/index.test.ts +++ b/tests/integration/directive/index.test.ts @@ -24,40 +24,46 @@ describe('shebang', async () => { describe('bundle-false', async () => { test('shebang at the beginning', async () => { - const index = queryContent(contents.esm2!, 'index.js', { + const { content: index } = queryContent(contents.esm2!, 'index.js', { basename: true, }); expect(index!.startsWith('#!/usr/bin/env node')).toBe(true); - const bar = queryContent(contents.esm2!, 'bar.js', { basename: true }); + const { content: bar } = queryContent(contents.esm2!, 'bar.js', { + basename: true, + }); expect(bar!.startsWith('#!/usr/bin/env node')).toBe(true); - const foo = queryContent(contents.esm2!, 'foo.js', { basename: true }); + const { content: foo } = queryContent(contents.esm2!, 'foo.js', { + basename: true, + }); expect(foo!.includes('#!')).toBe(false); }); test('shebang at the beginning even if minified', async () => { - const index = queryContent(contents.esm3!, 'index.js', { + const { content: index } = queryContent(contents.esm3!, 'index.js', { basename: true, }); expect(index!.startsWith('#!/usr/bin/env node')).toBe(true); - const bar = queryContent(contents.esm3!, 'bar.js', { + const { content: bar } = queryContent(contents.esm3!, 'bar.js', { basename: true, }); expect(bar!.startsWith('#!/usr/bin/env node')).toBe(true); - const foo = queryContent(contents.esm2!, 'foo.js', { basename: true }); + const { content: foo } = queryContent(contents.esm2!, 'foo.js', { + basename: true, + }); expect(foo!.includes('#!')).toBe(false); }); test.todo('shebang commented by JS parser should be striped', async () => { - const index = queryContent(contents.esm3!, 'index.js', { + const { content: index } = queryContent(contents.esm3!, 'index.js', { basename: true, }); expect(index!.includes('//#!')).toBe(false); - const bar = queryContent(contents.esm3!, 'bar.js', { + const { content: bar } = queryContent(contents.esm3!, 'bar.js', { basename: true, }); expect(bar!.includes('//#!')).toBe(false); @@ -87,18 +93,26 @@ describe('react', async () => { describe('bundle-false', async () => { test('React directive at the beginning', async () => { - const foo = queryContent(contents.esm0!, 'foo.js', { basename: true }); + const { content: foo } = queryContent(contents.esm0!, 'foo.js', { + basename: true, + }); expect(foo!.startsWith(`'use client';`)).toBe(true); - const bar = queryContent(contents.esm0!, 'bar.js', { basename: true }); + const { content: bar } = queryContent(contents.esm0!, 'bar.js', { + basename: true, + }); expect(bar!.startsWith(`'use server';`)).toBe(true); }); test('React directive at the beginning even if minified', async () => { - const foo = queryContent(contents.esm1!, 'foo.js', { basename: true }); + const { content: foo } = queryContent(contents.esm1!, 'foo.js', { + basename: true, + }); expect(foo!.startsWith(`'use client';`)).toBe(true); - const bar = queryContent(contents.esm1!, 'bar.js', { basename: true }); + const { content: bar } = queryContent(contents.esm1!, 'bar.js', { + basename: true, + }); expect(bar!.startsWith(`'use server';`)).toBe(true); }); }); diff --git a/tests/integration/entry/index.test.ts b/tests/integration/entry/index.test.ts index 238746e0a..db2a45288 100644 --- a/tests/integration/entry/index.test.ts +++ b/tests/integration/entry/index.test.ts @@ -39,7 +39,9 @@ test('multiple entry bundle', async () => { } `); - const index = queryContent(contents.esm, 'index.js', { basename: true }); + const { content: index } = queryContent(contents.esm, 'index.js', { + basename: true, + }); expect(index).toMatchInlineSnapshot(` "const shared = 'shared'; const foo = 'foo' + shared; @@ -48,7 +50,9 @@ test('multiple entry bundle', async () => { " `); - const foo = queryContent(contents.esm, 'foo.js', { basename: true }); + const { content: foo } = queryContent(contents.esm, 'foo.js', { + basename: true, + }); expect(foo).toMatchInlineSnapshot(` "const shared = 'shared'; const foo = 'foo' + shared; @@ -56,14 +60,18 @@ test('multiple entry bundle', async () => { " `); - const bar = queryContent(contents.esm, 'bar.js', { basename: true }); + const { content: bar } = queryContent(contents.esm, 'bar.js', { + basename: true, + }); expect(bar).toMatchInlineSnapshot(` "const bar = 'bar'; export { bar }; " `); - const shared = queryContent(contents.esm, 'shared.js', { basename: true }); + const { content: shared } = queryContent(contents.esm, 'shared.js', { + basename: true, + }); expect(shared).toMatchInlineSnapshot(` "const shared = 'shared'; export { shared }; diff --git a/tests/integration/redirect/js.test.ts b/tests/integration/redirect/js.test.ts new file mode 100644 index 000000000..32009b219 --- /dev/null +++ b/tests/integration/redirect/js.test.ts @@ -0,0 +1,103 @@ +import path from 'node:path'; +import { buildAndGetResults, queryContent } from 'test-helper'; +import { beforeAll, expect, test } from 'vitest'; + +let contents: Awaited>['contents']; + +beforeAll(async () => { + const fixturePath = path.resolve(__dirname, './js'); + contents = (await buildAndGetResults({ fixturePath })).contents; +}); + +test('redirect.js default', async () => { + const { content: indexContent, path: indexEsmPath } = queryContent( + contents.esm0!, + /esm\/index\.js/, + ); + const { path: indexCjsPath } = await queryContent( + contents.cjs0!, + /cjs\/index\.cjs/, + ); + + expect(indexContent).toMatchInlineSnapshot(` + "import * as __WEBPACK_EXTERNAL_MODULE_lodash__ from "lodash"; + import * as __WEBPACK_EXTERNAL_MODULE__bar_index_js__ from "./bar/index.js"; + import * as __WEBPACK_EXTERNAL_MODULE__foo_js__ from "./foo.js"; + import * as __WEBPACK_EXTERNAL_MODULE__baz_js__ from "./baz.js"; + const src_rslib_entry_ = __WEBPACK_EXTERNAL_MODULE_lodash__["default"].toUpper(__WEBPACK_EXTERNAL_MODULE__foo_js__.foo + __WEBPACK_EXTERNAL_MODULE__bar_index_js__.bar + __WEBPACK_EXTERNAL_MODULE__foo_js__.foo + __WEBPACK_EXTERNAL_MODULE__bar_index_js__.bar + __WEBPACK_EXTERNAL_MODULE__baz_js__.baz); + export { src_rslib_entry_ as default }; + " + `); + + const esmResult = await import(indexEsmPath); + const cjsResult = await import(indexCjsPath); + + expect(esmResult.default).toEqual(cjsResult.default); + expect(esmResult.default).toMatchInlineSnapshot(`"FOOBAR1FOOBAR1BAZ"`); +}); + +test('redirect.js.path false', async () => { + const { content: indexContent } = queryContent( + contents.esm1!, + /esm\/index\.js/, + ); + + expect(indexContent).toMatchInlineSnapshot(` + "import * as __WEBPACK_EXTERNAL_MODULE_lodash__ from "lodash"; + import * as __WEBPACK_EXTERNAL_MODULE__bar_js__ from "@/bar.js"; + import * as __WEBPACK_EXTERNAL_MODULE__foo_js__ from "@/foo.js"; + import * as __WEBPACK_EXTERNAL_MODULE__baz_js__ from "~/baz.js"; + import * as __WEBPACK_EXTERNAL_MODULE__bar_js__ from "./bar.js"; + import * as __WEBPACK_EXTERNAL_MODULE__foo_js__ from "./foo.js"; + const src_rslib_entry_ = __WEBPACK_EXTERNAL_MODULE_lodash__["default"].toUpper(__WEBPACK_EXTERNAL_MODULE__foo_js__.foo + __WEBPACK_EXTERNAL_MODULE__bar_js__.bar + __WEBPACK_EXTERNAL_MODULE__foo_js__.foo + __WEBPACK_EXTERNAL_MODULE__bar_js__.bar + __WEBPACK_EXTERNAL_MODULE__baz_js__.baz); + export { src_rslib_entry_ as default }; + " + `); +}); + +test('redirect.js.path with user override externals', async () => { + const { content: indexContent, path: indexEsmPath } = queryContent( + contents.esm2!, + /esm\/index\.js/, + ); + const { path: indexCjsPath } = await queryContent( + contents.cjs2!, + /cjs\/index\.cjs/, + ); + + expect(indexContent).toMatchInlineSnapshot(` + "import * as __WEBPACK_EXTERNAL_MODULE_lodash__ from "lodash"; + import * as __WEBPACK_EXTERNAL_MODULE__others_bar_index_js__ from "./others/bar/index.js"; + import * as __WEBPACK_EXTERNAL_MODULE__others_foo_js__ from "./others/foo.js"; + import * as __WEBPACK_EXTERNAL_MODULE__baz_js__ from "./baz.js"; + import * as __WEBPACK_EXTERNAL_MODULE__bar_index_js__ from "./bar/index.js"; + import * as __WEBPACK_EXTERNAL_MODULE__foo_js__ from "./foo.js"; + const src_rslib_entry_ = __WEBPACK_EXTERNAL_MODULE_lodash__["default"].toUpper(__WEBPACK_EXTERNAL_MODULE__foo_js__.foo + __WEBPACK_EXTERNAL_MODULE__bar_index_js__.bar + __WEBPACK_EXTERNAL_MODULE__others_foo_js__.foo + __WEBPACK_EXTERNAL_MODULE__others_bar_index_js__.bar + __WEBPACK_EXTERNAL_MODULE__baz_js__.baz); + export { src_rslib_entry_ as default }; + " + `); + + const esmResult = await import(indexEsmPath); + const cjsResult = await import(indexCjsPath); + + expect(esmResult.default).toEqual(cjsResult.default); + expect(esmResult.default).toMatchInlineSnapshot( + `"FOOBAR1OTHERFOOOTHERBAR2BAZ"`, // cspell:disable-line + ); +}); + +test('redirect.js.extension: false', async () => { + const { content: indexContent } = queryContent( + contents.esm3!, + /esm\/index\.js/, + ); + expect(indexContent).toMatchInlineSnapshot(` + "import * as __WEBPACK_EXTERNAL_MODULE_lodash__ from "lodash"; + import * as __WEBPACK_EXTERNAL_MODULE__bar_index_ts__ from "./bar/index.ts"; + import * as __WEBPACK_EXTERNAL_MODULE__foo_ts__ from "./foo.ts"; + import * as __WEBPACK_EXTERNAL_MODULE__baz_ts__ from "./baz.ts"; + const src_rslib_entry_ = __WEBPACK_EXTERNAL_MODULE_lodash__["default"].toUpper(__WEBPACK_EXTERNAL_MODULE__foo_ts__.foo + __WEBPACK_EXTERNAL_MODULE__bar_index_ts__.bar + __WEBPACK_EXTERNAL_MODULE__foo_ts__.foo + __WEBPACK_EXTERNAL_MODULE__bar_index_ts__.bar + __WEBPACK_EXTERNAL_MODULE__baz_ts__.baz); + export { src_rslib_entry_ as default }; + " + `); +}); diff --git a/tests/integration/redirect/js/package.json b/tests/integration/redirect/js/package.json new file mode 100644 index 000000000..0aa0be903 --- /dev/null +++ b/tests/integration/redirect/js/package.json @@ -0,0 +1,13 @@ +{ + "name": "redirect-js-test", + "version": "1.0.0", + "private": true, + "type": "module", + "devDependencies": { + "@types/lodash": "^4.17.13", + "lodash": "^4.17.21" + }, + "peerDependencies": { + "lodash": "^4.17.21" + } +} diff --git a/tests/integration/redirect/js/rslib.config.ts b/tests/integration/redirect/js/rslib.config.ts new file mode 100644 index 000000000..57f655801 --- /dev/null +++ b/tests/integration/redirect/js/rslib.config.ts @@ -0,0 +1,113 @@ +import { defineConfig } from '@rslib/core'; +import { generateBundleCjsConfig, generateBundleEsmConfig } from 'test-helper'; + +export default defineConfig({ + lib: [ + // 0 default + generateBundleEsmConfig({ + bundle: false, + output: { + distPath: { + root: 'dist/default/esm', + }, + }, + }), + generateBundleCjsConfig({ + bundle: false, + output: { + distPath: { + root: 'dist/default/cjs', + }, + }, + }), + // 1 js.path: false + generateBundleEsmConfig({ + bundle: false, + output: { + distPath: { + root: 'dist/js-path-false/esm', + }, + }, + redirect: { + js: { + path: false, + }, + }, + }), + generateBundleCjsConfig({ + bundle: false, + output: { + distPath: { + root: 'dist/js-path-false/cjs', + }, + }, + redirect: { + js: { + path: false, + }, + }, + }), + // 2 js.path with user override externals + generateBundleEsmConfig({ + bundle: false, + output: { + externals: { + '@/foo': './others/foo.js', + '@/bar': './others/bar/index.js', + }, + distPath: { + root: 'dist/js-path-externals-override/esm', + }, + }, + }), + generateBundleCjsConfig({ + bundle: false, + output: { + distPath: { + root: 'dist/js-path-externals-override/cjs', + }, + externals: { + '@/foo': './others/foo.cjs', + '@/bar': './others/bar/index.cjs', + }, + }, + }), + // 3 js.extension: false + generateBundleEsmConfig({ + bundle: false, + output: { + distPath: { + root: 'dist/js-extension-false/esm', + }, + }, + redirect: { + js: { + extension: false, + }, + }, + }), + generateBundleCjsConfig({ + bundle: false, + output: { + distPath: { + root: 'dist/js-extension-false/cjs', + }, + }, + redirect: { + js: { + extension: false, + }, + }, + }), + ], + resolve: { + alias: { + '~': './src', + }, + }, + source: { + entry: { + index: './src/**', + }, + }, +}); diff --git a/tests/integration/redirect/js/src/bar/index.ts b/tests/integration/redirect/js/src/bar/index.ts new file mode 100644 index 000000000..0b87c7fd0 --- /dev/null +++ b/tests/integration/redirect/js/src/bar/index.ts @@ -0,0 +1,3 @@ +import { value } from '../constant'; + +export const bar = 'bar' + value; diff --git a/tests/integration/redirect/js/src/baz.ts b/tests/integration/redirect/js/src/baz.ts new file mode 100644 index 000000000..6061cf077 --- /dev/null +++ b/tests/integration/redirect/js/src/baz.ts @@ -0,0 +1 @@ +export const baz = 'baz'; diff --git a/tests/integration/redirect/js/src/constant.ts b/tests/integration/redirect/js/src/constant.ts new file mode 100644 index 000000000..efeee5db1 --- /dev/null +++ b/tests/integration/redirect/js/src/constant.ts @@ -0,0 +1 @@ +export const value = 1; diff --git a/tests/integration/redirect/js/src/foo.ts b/tests/integration/redirect/js/src/foo.ts new file mode 100644 index 000000000..3329a7d97 --- /dev/null +++ b/tests/integration/redirect/js/src/foo.ts @@ -0,0 +1 @@ +export const foo = 'foo'; diff --git a/tests/integration/redirect/js/src/index.ts b/tests/integration/redirect/js/src/index.ts new file mode 100644 index 000000000..8762f61c6 --- /dev/null +++ b/tests/integration/redirect/js/src/index.ts @@ -0,0 +1,9 @@ +import lodash from 'lodash'; + +import { bar as bar2 } from '@/bar'; +import { foo as foo2 } from '@/foo'; +import { baz } from '~/baz'; +import { bar } from './bar'; +import { foo } from './foo'; + +export default lodash.toUpper(foo + bar + foo2 + bar2 + baz); diff --git a/tests/integration/redirect/js/src/others/bar/index.ts b/tests/integration/redirect/js/src/others/bar/index.ts new file mode 100644 index 000000000..96d00c427 --- /dev/null +++ b/tests/integration/redirect/js/src/others/bar/index.ts @@ -0,0 +1,3 @@ +import { value } from '../constant'; + +export const bar = 'otherBar' + value; diff --git a/tests/integration/redirect/js/src/others/constant.ts b/tests/integration/redirect/js/src/others/constant.ts new file mode 100644 index 000000000..44439d586 --- /dev/null +++ b/tests/integration/redirect/js/src/others/constant.ts @@ -0,0 +1 @@ +export const value = 2; diff --git a/tests/integration/redirect/js/src/others/foo.ts b/tests/integration/redirect/js/src/others/foo.ts new file mode 100644 index 000000000..23e046b59 --- /dev/null +++ b/tests/integration/redirect/js/src/others/foo.ts @@ -0,0 +1 @@ +export const foo = 'otherFoo'; diff --git a/tests/integration/redirect/js/tsconfig.json b/tests/integration/redirect/js/tsconfig.json new file mode 100644 index 000000000..2291f72e1 --- /dev/null +++ b/tests/integration/redirect/js/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "@rslib/tsconfig/base", + "compilerOptions": { + "baseUrl": "./", + "paths": { + "@/*": ["./src/*"] + } + }, + "include": ["src"] +} diff --git a/tests/integration/style/redirect-style-false/package.json b/tests/integration/redirect/style-false/package.json similarity index 53% rename from tests/integration/style/redirect-style-false/package.json rename to tests/integration/redirect/style-false/package.json index 788a8e737..227b360ac 100644 --- a/tests/integration/style/redirect-style-false/package.json +++ b/tests/integration/redirect/style-false/package.json @@ -1,5 +1,5 @@ { - "name": "css-bundle-false-redirect-style-false-test", + "name": "redirect-style-false-test", "version": "1.0.0", "private": true, "type": "module" diff --git a/tests/integration/style/redirect-style-false/rslib.config.ts b/tests/integration/redirect/style-false/rslib.config.ts similarity index 100% rename from tests/integration/style/redirect-style-false/rslib.config.ts rename to tests/integration/redirect/style-false/rslib.config.ts diff --git a/tests/integration/style/redirect-style-false/src/index.less b/tests/integration/redirect/style-false/src/index.less similarity index 100% rename from tests/integration/style/redirect-style-false/src/index.less rename to tests/integration/redirect/style-false/src/index.less diff --git a/tests/integration/style/redirect-style-false/src/index.ts b/tests/integration/redirect/style-false/src/index.ts similarity index 100% rename from tests/integration/style/redirect-style-false/src/index.ts rename to tests/integration/redirect/style-false/src/index.ts diff --git a/tests/integration/style/redirect-style-false/src/style.module.less b/tests/integration/redirect/style-false/src/style.module.less similarity index 100% rename from tests/integration/style/redirect-style-false/src/style.module.less rename to tests/integration/redirect/style-false/src/style.module.less diff --git a/tests/integration/style/redirect-style-false/index.test.ts b/tests/integration/redirect/style.test.ts similarity index 74% rename from tests/integration/style/redirect-style-false/index.test.ts rename to tests/integration/redirect/style.test.ts index 3961e1d5b..48bcf496c 100644 --- a/tests/integration/style/redirect-style-false/index.test.ts +++ b/tests/integration/redirect/style.test.ts @@ -1,14 +1,15 @@ +import path from 'node:path'; import { buildAndGetResults } from 'test-helper'; import { expectFileContainContent } from 'test-helper/vitest'; import { expect, test } from 'vitest'; test('should extract css successfully when using redirect.style = false', async () => { - const fixturePath = __dirname; + const fixturePath = path.resolve(__dirname, './style-false'); const { contents } = await buildAndGetResults({ fixturePath }); const esmFiles = Object.keys(contents.esm); expect(esmFiles).toMatchInlineSnapshot(` [ - "/tests/integration/style/redirect-style-false/dist/esm/index.js", + "/tests/integration/redirect/style-false/dist/esm/index.js", ] `); expectFileContainContent(contents.esm, 'index.js', 'import "./index.less";'); @@ -16,7 +17,7 @@ test('should extract css successfully when using redirect.style = false', async const cjsFiles = Object.keys(contents.cjs); expect(cjsFiles).toMatchInlineSnapshot(` [ - "/tests/integration/style/redirect-style-false/dist/cjs/index.cjs", + "/tests/integration/redirect/style-false/dist/cjs/index.cjs", ] `); expectFileContainContent( diff --git a/tests/scripts/shared.ts b/tests/scripts/shared.ts index 31a60cfbd..07ca4d101 100644 --- a/tests/scripts/shared.ts +++ b/tests/scripts/shared.ts @@ -327,7 +327,7 @@ export function queryContent( options: { basename?: boolean; } = {}, -): string | null { +): { path: string; content: string } { const useBasename = options?.basename ?? false; const matched = Object.entries(contents).find(([key]) => { const toQueried = useBasename ? basename(key) : key; @@ -337,10 +337,10 @@ export function queryContent( }); if (!matched) { - return null; + throw new Error(`Cannot find content for ${query}`); } - return matched[1]; + return { path: matched[0], content: matched[1] }; } export async function createTempFiles( diff --git a/website/docs/en/config/lib/redirect.mdx b/website/docs/en/config/lib/redirect.mdx index 20c8aa283..b48437afe 100644 --- a/website/docs/en/config/lib/redirect.mdx +++ b/website/docs/en/config/lib/redirect.mdx @@ -1,9 +1,23 @@ # lib.redirect +:::info + +Redirect is the unique configuration for bundleless mode (set [lib.bundle](/config/lib/bundle) to `false`). It will not take effect in bundle mode where all output files are packaged into a single file, eliminating the need for import path redirection. + +As bundleless mode is still under development, additional redirect configurations will be introduced in the future. + +::: + - **Type:** ```ts +type JsRedirect = { + path?: boolean; + extension?: boolean; +}; + type Redirect = { + js?: JsRedirect; style?: boolean; }; ``` @@ -12,39 +26,82 @@ type Redirect = { ```ts const defaultRedirect = { + js: { + path: true, + extension: true, + }, style: true, }; ``` -Configure the redirect of the import paths. +Configure the redirect for import paths in output files. In bundleless mode, there are often needs such as using aliases or automatically appending suffixes for ESM products. The `redirect` configuration is designed to address these issues. -When `bundle: false`, the import path is redirected to ensure that it points to the correct output. +Common scenarios that require redirect: -:::note +- Automatically convert `compilerOptions.paths` in tsconfig.json to correct relative path -As bundleless mode is still under development, additional redirect configurations will be introduced in the future. + For example, set `compilerOptions.paths` to `{ "@/*": ["src/*"] }` in tsconfig.json, the output file will be redirected to the correct relative path: + ```ts + import { foo } from '@/foo'; // source code of './src/bar.ts' ↓ + import { foo } from './foo.js'; // expected output of './dist/bar.js' + + import { foo } from '@/foo'; // source code of './src/utils/index.ts' ↓ + import { foo } from '../foo.js'; // expected output './dist/utils/index.js' + ``` + +- Automatically append file suffix + + For ESM products that run in Node.js, you must specify the exact full path for the module import to load correctly. Rslib will automatically add the suffix based on the output file. + + ```ts + import { foo } from './foo'; // source code of './src/bar.ts' ↓ + import { foo } from './foo.mjs'; // expected output of './dist/bar.js' + + import { foo } from './foo.ts'; // source code of './src/utils/index.ts' ↓ + import { foo } from './foo.mjs'; // expected output './dist/utils/index.js' + ``` + +## redirect.js + +Controls the redirect of the import paths of output JavaScript files. + +:::warning +When [output.externals](/config/rsbuild/output#outputexternals) is configured and a request is matched, neither `redirect.js.path` nor `redirect.js.extension` will take effect, and the final rewritten request path will be entirely controlled by [output.externals](/config/rsbuild/output#outputexternals). ::: -If you don't need these redirects, you can turn it off, and its import path will not be changed. - -```ts title="rslib.config.ts" -export default { - lib: [ - { - redirect: { - style: false, // Turn off the redirect of the style file - }, - }, - ], -}; -``` +### redirect.js.path + +Whether to automatically redirect the import paths of JavaScript output files. + +- **Type:** `boolean` +- **Default:** `true` + +When set to `true`, [resolve.alias](/config/rsbuild/resolve#resolvealias) and [resolve.aliasStrategy](/config/rsbuild/resolve#aliasstrategy) will take effect and applied in the rewritten import path of the output file. For TypeScript projects, you only need to configure [compilerOptions.paths](https://typescriptlang.org/tsconfig#paths) in the tsconfig.json file. + +When set to `false`, the import path will not be effected by [resolve.alias](/config/rsbuild/resolve#resolvealias), [resolve.aliasStrategy](/config/rsbuild/resolve#aliasstrategy) and tsconfig.json. + +### redirect.js.extension + +Whether to automatically add the file extension to import paths based on the JavaScript output files. + +- **Type:** `boolean` +- **Default:** `true` + +When set to `true`, the file extension will automatically be added to the rewritten import path of the output file, regardless of the original extension or whether it is specified in the import path. + +When set to `false`, the file extension will remain unchanged from the original import path in the rewritten import path of the output file (regardless of whether it is specified or specified as any value). ## redirect.style - **Type:** `boolean` - **Default:** `true` -Whether to redirect the import path of the style file. For example: +Whether to redirect the import path of the style file. + +For example, when importing a `.less` file, it will be rewritten to a `.css` file: -- `import './index.less'` will be rewritten to `import './index.css'` +```ts +import './index.less'; // source code ↓ +import './index.css'; // output file +``` diff --git a/website/docs/zh/config/lib/redirect.mdx b/website/docs/zh/config/lib/redirect.mdx index 668f7dacd..e2bf63a84 100644 --- a/website/docs/zh/config/lib/redirect.mdx +++ b/website/docs/zh/config/lib/redirect.mdx @@ -1,50 +1,107 @@ # lib.redirect -- **类型:** +:::info + +Redirect 是 bundleless 模式(将 [lib.bundle](/config/lib/bundle) 设置为 `false`)的特定配置。在 bundle 模式下不会生效,因为所有输出文件都被打包成一个文件。所以,导入路径不存在因而不需要重定向。 + +由于 bundleless 模式仍在开发中,未来将引入更多的重定向配置。 + +::: + +- **Type:** ```ts +type JsRedirect = { + path?: boolean; + extension?: boolean; +}; + type Redirect = { + js?: JsRedirect; style?: boolean; }; ``` -- **默认值:** +- **Default:** ```ts const defaultRedirect = { + js: { + path: true, + extension: true, + }, style: true, }; ``` -配置导入路径的重定向。 +配置输出文件中导入路径的重定向。在 bundleless 模式下,通常需要使用别名或自动添加 ESM 产物的后缀。`redirect` 配置旨在解决这些问题。 + +常见的需要 redirect 的场景: + +- 自动将 tsconfig.json 中 `compilerOptions.paths` 转换为正确的相对路径 + + 例如,在 tsconfig.json 中将 `compilerOptions.paths` 设置为 `{ "@/*": ["src/*"] }`,输出文件将被重定向到正确的相对路径: + + ```ts + import { foo } from '@/foo'; // './src/bar.ts' 的源码 ↓ + import { foo } from './foo.js'; // './dist/bar.js' 预期生成的代码 + + import { foo } from '@/foo'; // './src/utils/index.ts' 的源码 ↓ + import { foo } from '../foo.js'; // './dist/utils/index.js' 预期生成的代码 + ``` + +- 自动添加文件后缀 + + 对于在 Node.js 中运行的 ESM 产物,必须指定模块导入的完整路径才能正确加载。Rslib 将根据输出文件自动添加后缀。 + + ```ts + import { foo } from './foo'; // './src/bar.ts' 的源码 ↓ + import { foo } from './foo.mjs'; // './dist/bar.js' 预期生成的代码 -当 `bundle: false` 时,会对导入路径进行重定向,以确保它指向正确的输出。 + import { foo } from './foo.ts'; // './src/utils/index.ts' 的源码 ↓ + import { foo } from './foo.mjs'; // './dist/utils/index.js' 预期生成的代码 + ``` -:::note +## redirect.js -由于 bundleless 模式仍在开发中,未来会引入更多重定向配置。 +控制输出 JavaScript 文件导入路径的重定向。 +:::warning +当 [output.externals](/config/rsbuild/output#outputexternals) 被配置且请求被匹配时,`redirect.js.path` 和 `redirect.js.extension` 都不会生效,最终重写的请求路径将完全由 [output.externals](/config/rsbuild/output#outputexternals) 控制。 ::: -如果你不需要这些重定向,可以关闭它,这样导入路径将保持不变。 - -```ts title="rslib.config.ts" -export default { - lib: [ - { - redirect: { - style: false, // 关闭样式文件的重定向 - }, - }, - ], -}; -``` +### redirect.js.path + +是否自动重定向 JavaScript 输出文件的导入路径。 + +- **类型:** `boolean` +- **默认值:** `true` + +当设置为 `true` 时,[resolve.alias](/config/rsbuild/resolve#resolvealias) 和 [resolve.aliasStrategy](/config/rsbuild/resolve#aliasstrategy) 将生效并应用于输出文件的重写导入路径。对于 TypeScript 项目,您只需在 tsconfig.json 文件中配置 [compilerOptions.paths](https://typescriptlang.org/tsconfig#paths)。 + +当设置为 `false` 时,导入路径将不受 [resolve.alias](/config/rsbuild/resolve#resolvealias)、[resolve.aliasStrategy](/config/rsbuild/resolve#aliasstrategy) 和 tsconfig.json 的影响。 + +### redirect.js.extension + +是否根据 JavaScript 输出文件自动添加文件扩展名到导入路径。 + +- **类型:** `boolean` +- **默认值:** `true` + +当设置为 `true` 时,无论原始扩展名或导入路径中是否指定,文件扩展名都将自动添加到输出文件的重写导入路径中。 + +当设置为 `false` 时,文件扩展名将保持原始导入路径中的不变(无论是否指定或指定为任意值)。 ## redirect.style -- **类型:** `boolean` -- **默认值:** `true` +- **类型:** `boolean` +- **默认值:** `true` -是否重定向样式文件的导入路径。例如: +是否重定向样式文件的导入路径。 -- `import './index.less'` 将被重写为 `import './index.css'` +例如,当导入 `.less` 文件时,它将被重写为 `.css` 文件: + +```ts +import './index.less'; // source code ↓ +import './index.css'; // output file +```