diff --git a/packages/core/src/config.ts b/packages/core/src/config.ts index 8d79f21b4..a5ee65312 100644 --- a/packages/core/src/config.ts +++ b/packages/core/src/config.ts @@ -7,6 +7,7 @@ import { type RsbuildPlugin, type RsbuildPlugins, type Rspack, + type ToolsConfig, defineConfig as defineRsbuildConfig, loadConfig as loadRsbuildConfig, mergeRsbuildConfig, @@ -679,6 +680,39 @@ const composeFormatConfig = ({ return config; } + case 'iife': { + if (bundle === false) { + throw new Error( + 'When using "iife" format, "bundle" must be set to "true". Since the default value for "bundle" is "true", so you can either explicitly set it to "true" or remove the field entirely.', + ); + } + + const config: EnvironmentConfig = { + tools: { + rspack: { + module: { + parser: { + javascript: { + importMeta: false, + }, + }, + }, + output: { + iife: true, + asyncChunks: false, + library: { + type: 'modern-module', + }, + }, + optimization: { + nodeEnv: process.env.NODE_ENV, + }, + }, + }, + }; + + return config; + } case 'mf': if (bundle === false) { throw new Error( @@ -789,6 +823,7 @@ const composeShimsConfig = ( }; break; case 'umd': + case 'iife': case 'mf': break; default: @@ -813,7 +848,7 @@ const composeExternalsConfig = ( // Rspack's externals as they will not be merged from different fields. All externals // should to be unified and merged together in the future. - const externalsTypeMap = { + const externalsTypeMap: Record = { esm: 'module-import', cjs: 'commonjs-import', umd: 'umd', @@ -821,28 +856,45 @@ const composeExternalsConfig = ( // If use 'umd', the judgement conditions may be affected by other packages that define variables like 'define'. // Therefore, we use 'global' to satisfy both web and node environments. mf: 'global', - } as const; + iife: 'global', + }; + + const globalObjectMap: Record = { + esm: undefined, + cjs: undefined, + umd: undefined, + mf: undefined, + iife: 'globalThis', + }; + + const rspackConfig: ToolsConfig['rspack'] = {}; + const rsbuildConfig: EnvironmentConfig = {}; switch (format) { case 'esm': case 'cjs': case 'umd': case 'mf': - return { - output: externals - ? { - externals, - } - : {}, - tools: { - rspack: { - externalsType: externalsTypeMap[format], - }, - }, - }; + case 'iife': + rsbuildConfig.output = externals ? { externals } : {}; + rspackConfig.externalsType = externalsTypeMap[format]; + if (globalObjectMap[format]) { + rspackConfig.output = { + globalObject: globalObjectMap[format], + }; + } + + break; default: throw new Error(`Unsupported format: ${format}`); } + + return { + ...rsbuildConfig, + tools: { + rspack: rspackConfig, + }, + }; }; const composeAutoExtensionConfig = ( diff --git a/packages/core/src/types/config.ts b/packages/core/src/types/config.ts index 89f3928d9..11ef8ffdb 100644 --- a/packages/core/src/types/config.ts +++ b/packages/core/src/types/config.ts @@ -6,7 +6,7 @@ import type { } from '@rsbuild/core'; import type { GetAsyncFunctionFromUnion } from './utils'; -export type Format = 'esm' | 'cjs' | 'umd' | 'mf'; +export type Format = 'esm' | 'cjs' | 'umd' | 'mf' | 'iife'; export type FixedEcmaVersions = | 'es5' diff --git a/packages/core/tests/__snapshots__/config.test.ts.snap b/packages/core/tests/__snapshots__/config.test.ts.snap index 0f21dc262..4639ea790 100644 --- a/packages/core/tests/__snapshots__/config.test.ts.snap +++ b/packages/core/tests/__snapshots__/config.test.ts.snap @@ -724,6 +724,231 @@ exports[`Should compose create Rsbuild config correctly > Merge Rsbuild config i }, "format": "umd", }, + { + "config": { + "dev": { + "progressBar": false, + }, + "output": { + "distPath": { + "css": "./", + "cssAsync": "./", + "js": "./", + "jsAsync": "./", + }, + "externals": [ + "assert", + "assert/strict", + "async_hooks", + "buffer", + "child_process", + "cluster", + "console", + "constants", + "crypto", + "dgram", + "diagnostics_channel", + "dns", + "dns/promises", + "domain", + "events", + "fs", + "fs/promises", + "http", + "http2", + "https", + "inspector", + "inspector/promises", + "module", + "net", + "os", + "path", + "path/posix", + "path/win32", + "perf_hooks", + "process", + "punycode", + "querystring", + "readline", + "readline/promises", + "repl", + "stream", + "stream/consumers", + "stream/promises", + "stream/web", + "string_decoder", + "sys", + "timers", + "timers/promises", + "tls", + "trace_events", + "tty", + "url", + "util", + "util/types", + "v8", + "vm", + "wasi", + "worker_threads", + "zlib", + /\\^node:/, + "pnpapi", + ], + "filename": { + "js": "[name].js", + }, + "filenameHash": false, + "minify": { + "css": false, + "js": true, + "jsOptions": { + "minimizerOptions": { + "compress": { + "dead_code": true, + "defaults": false, + "toplevel": true, + "unused": true, + }, + "format": { + "comments": "some", + "preserve_annotations": true, + }, + "mangle": false, + "minify": false, + }, + }, + }, + "overrideBrowserslist": [ + "last 1 node versions", + ], + "target": "node", + }, + "performance": { + "chunkSplit": { + "strategy": "custom", + }, + }, + "plugins": [ + { + "name": "rsbuild:lib-entry-chunk", + "setup": [Function], + }, + ], + "resolve": { + "alias": { + "bar": "bar", + "foo": "foo", + }, + }, + "source": { + "entry": {}, + "preEntry": "./a.js", + }, + "tools": { + "htmlPlugin": false, + "rspack": [ + { + "experiments": { + "rspackFuture": { + "bundlerInfo": { + "force": false, + }, + }, + }, + "optimization": { + "moduleIds": "named", + "nodeEnv": false, + "splitChunks": { + "chunks": "async", + }, + }, + "resolve": { + "extensionAlias": { + ".cjs": [ + ".cts", + ".cjs", + ], + ".js": [ + ".ts", + ".tsx", + ".js", + ".jsx", + ], + ".jsx": [ + ".tsx", + ".jsx", + ], + ".mjs": [ + ".mts", + ".mjs", + ], + }, + }, + }, + { + "module": { + "parser": { + "javascript": { + "importMeta": false, + }, + }, + }, + "optimization": { + "nodeEnv": "test", + }, + "output": { + "asyncChunks": false, + "iife": true, + "library": { + "type": "modern-module", + }, + }, + }, + [Function], + { + "target": [ + "node", + ], + }, + { + "externalsType": "global", + "output": { + "globalObject": "globalThis", + }, + }, + { + "plugins": [ + EntryChunkPlugin { + "contextToWatch": null, + "enabledImportMetaUrlShim": false, + "reactDirectives": {}, + "shebangChmod": 493, + "shebangEntries": {}, + "shebangInjectedAssets": Set {}, + "shimsInjectedAssets": Set {}, + }, + ], + }, + { + "resolve": { + "extensionAlias": { + ".js": [ + ".ts", + ".tsx", + ], + }, + }, + }, + ], + "swc": { + "jsc": { + "externalHelpers": false, + }, + }, + }, + }, + "format": "iife", + }, { "config": { "dev": { diff --git a/packages/core/tests/config.test.ts b/packages/core/tests/config.test.ts index c8a6adf8a..5958764cd 100644 --- a/packages/core/tests/config.test.ts +++ b/packages/core/tests/config.test.ts @@ -177,6 +177,9 @@ describe('Should compose create Rsbuild config correctly', () => { { format: 'umd', }, + { + format: 'iife', + }, { format: 'mf', plugins: [pluginModuleFederation({})], diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3bb645528..39f15aaf2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -789,6 +789,8 @@ importers: tests/integration/format/mf-bundle-false: {} + tests/integration/iife: {} + tests/integration/json: {} tests/integration/minify/config/disabled: {} diff --git a/tests/integration/format/default/rslib.config.ts b/tests/integration/format/default/rslib.config.ts index 2996ff400..b2cd0b887 100644 --- a/tests/integration/format/default/rslib.config.ts +++ b/tests/integration/format/default/rslib.config.ts @@ -3,6 +3,7 @@ import { generateBundleCjsConfig, generateBundleEsmConfig } from 'test-helper'; export default defineConfig({ lib: [ + // ESM generateBundleEsmConfig({ output: { distPath: { @@ -10,6 +11,7 @@ export default defineConfig({ }, }, }), + // CJS generateBundleCjsConfig({ output: { distPath: { @@ -17,6 +19,7 @@ export default defineConfig({ }, }, }), + // ESM bundleless generateBundleEsmConfig({ bundle: false, output: { @@ -25,6 +28,7 @@ export default defineConfig({ }, }, }), + // CJS bundleless generateBundleCjsConfig({ bundle: false, output: { diff --git a/tests/integration/iife/index.test.ts b/tests/integration/iife/index.test.ts new file mode 100644 index 000000000..bc4c86cc7 --- /dev/null +++ b/tests/integration/iife/index.test.ts @@ -0,0 +1,45 @@ +import { buildAndGetResults } from 'test-helper'; +import { expect, test } from 'vitest'; + +declare global { + var globalHelper: any; + var addPrefix: any; +} + +test('iife', async () => { + process.env.NODE_ENV = 'production'; + const fixturePath = __dirname; + const { entryFiles, entries } = await buildAndGetResults({ + fixturePath, + }); + + globalThis.globalHelper = { helperName: 'HELPER_NAME' }; + require(entryFiles.iife); + expect(globalThis.addPrefix('ok')).toBe('production: HELPER_NAMEok'); + delete process.env.NODE_ENV; + delete globalThis.globalHelper; + + expect(entries.iife).toMatchInlineSnapshot( + ` + "(()=>{ + "use strict"; + const external_globalHelper_namespaceObject = globalThis.globalHelper; + const addPrefix = (prefix, str, env)=>\`\${env}: \${prefix}\${str}\`; + globalThis.addPrefix = (str)=>addPrefix(external_globalHelper_namespaceObject.helperName, str, "production"); + })(); + " + `, + ); +}); + +test('throw error when using iife with `bundle: false`', async () => { + const fixturePath = __dirname; + const build = buildAndGetResults({ + fixturePath, + configPath: './rslibBundleFalse.config.ts', + }); + + await expect(build).rejects.toThrowErrorMatchingInlineSnapshot( + `[Error: When using "iife" format, "bundle" must be set to "true". Since the default value for "bundle" is "true", so you can either explicitly set it to "true" or remove the field entirely.]`, + ); +}); diff --git a/tests/integration/iife/package.json b/tests/integration/iife/package.json new file mode 100644 index 000000000..b464314e5 --- /dev/null +++ b/tests/integration/iife/package.json @@ -0,0 +1,5 @@ +{ + "name": "iife-test", + "version": "1.0.0", + "private": true +} diff --git a/tests/integration/iife/rslib.config.ts b/tests/integration/iife/rslib.config.ts new file mode 100644 index 000000000..c795a68d9 --- /dev/null +++ b/tests/integration/iife/rslib.config.ts @@ -0,0 +1,12 @@ +import { defineConfig } from '@rslib/core'; +import { generateBundleIifeConfig } from 'test-helper'; + +export default defineConfig({ + lib: [ + generateBundleIifeConfig({ + output: { + externals: ['globalHelper'], + }, + }), + ], +}); diff --git a/tests/integration/iife/rslibBundleFalse.config.ts b/tests/integration/iife/rslibBundleFalse.config.ts new file mode 100644 index 000000000..ca9935de9 --- /dev/null +++ b/tests/integration/iife/rslibBundleFalse.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from '@rslib/core'; +import { generateBundleIifeConfig } from 'test-helper'; + +export default defineConfig({ + lib: [ + generateBundleIifeConfig({ + bundle: false, + }), + ], +}); diff --git a/tests/integration/iife/src/index.ts b/tests/integration/iife/src/index.ts new file mode 100644 index 000000000..9d624e539 --- /dev/null +++ b/tests/integration/iife/src/index.ts @@ -0,0 +1,7 @@ +import { helperName } from 'globalHelper'; +import { addPrefix } from './utils'; + +// @ts-ignore +globalThis.addPrefix = (str: string) => + // @ts-ignore + addPrefix(helperName, str, process.env.NODE_ENV); diff --git a/tests/integration/iife/src/utils.ts b/tests/integration/iife/src/utils.ts new file mode 100644 index 000000000..344a05ce7 --- /dev/null +++ b/tests/integration/iife/src/utils.ts @@ -0,0 +1,4 @@ +const addPrefix = (prefix: string, str: string, env: string) => + `${env}: ${prefix}${str}`; + +export { addPrefix }; diff --git a/tests/scripts/shared.ts b/tests/scripts/shared.ts index 86f4c8d95..98820b59c 100644 --- a/tests/scripts/shared.ts +++ b/tests/scripts/shared.ts @@ -73,6 +73,19 @@ export function generateBundleUmdConfig(config: LibConfig = {}): LibConfig { return mergeConfig(umdBasicConfig, config)!; } +export function generateBundleIifeConfig(config: LibConfig = {}): LibConfig { + const iifeBasicConfig: LibConfig = { + format: 'iife', + output: { + distPath: { + root: './dist/iife', + }, + }, + }; + + return mergeConfig(iifeBasicConfig, config)!; +} + export type FormatType = Format | `${Format}${number}`; type FilePath = string; @@ -101,6 +114,7 @@ export async function getResults( cjs: 0, umd: 0, mf: 0, + iife: 0, }; let mfExposeEntry: string | undefined; let key = ''; diff --git a/website/docs/en/config/lib/format.mdx b/website/docs/en/config/lib/format.mdx index 95dd58464..24f3e1a52 100644 --- a/website/docs/en/config/lib/format.mdx +++ b/website/docs/en/config/lib/format.mdx @@ -1,6 +1,6 @@ # lib.format -- **Type:** `'esm' | 'cjs' | 'umd' | 'mf'` +- **Type:** `'esm' | 'cjs' | 'umd' | 'mf' | 'iife'` - **Default:** `'esm'` Specify the output format for the generated JavaScript output files. @@ -10,11 +10,12 @@ For different output formats, Rslib uses the following default value of [output. - `esm`:[modern-module](https://rspack.dev/config/output#type-modern-module) - `cjs`:[commonjs-static](https://rspack.dev/config/output#type-commonjs-static) - `umd`:[umd](https://rspack.dev/config/output#type-umd) +- `iife`: [modern-module](https://rspack.dev/config/output#type-modern-module) with [output.iife](https://rspack.dev/config/output#outputiife) enabled. See [Output Format](/guide/basic/output-format) and [Module Federation](/guide/advanced/module-federation) for more details. ::: note -The `umd` format only works when [bundle](/config/lib/bundle) is set to `true`. +The `umd`, `mf` and `iife` formats only work when [bundle](/config/lib/bundle) is set to `true`. ::: diff --git a/website/docs/en/guide/basic/output-format.mdx b/website/docs/en/guide/basic/output-format.mdx index 977e04397..4fe012450 100644 --- a/website/docs/en/guide/basic/output-format.mdx +++ b/website/docs/en/guide/basic/output-format.mdx @@ -135,3 +135,30 @@ export default defineConfig({ ### What is MF? MF stands for Module Federation. + +## IIFE + +{/* The following documentation is taken from https://esbuild.github.io/api/#format-iife */} + +The iife format stands for "immediately-invoked function expression" and is intended to be run in the browser. Wrapping your code in a function expression ensures that any variables in your code don't accidentally conflict with variables in the global scope. If your entry point has exports that you want to expose as a global in the browser, you can configure that global's name using the global name setting. + +In IIFE format, [output.globalObject](https://rspack.dev/config/output#outputglobalobject) is set to [globalThis](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/globalThis) by default. The `import` statements that match [externals](/config/rsbuild/output#outputexternals) in the source code will be transformed to access properties through `globalThis`. You can override [output.globalObject](https://rspack.dev/config/output#outputglobalobject) to any value. + +When specifying the `iife` format, the source code and corresponding output are as follows: + +```js title="source code" +// parent-sdk is marked as externals +// externals: ['parent-sdk'] +import { version } from 'parent-sdk'; +alert(version); +``` + +```js title="IIFE output" +( + () => { + 'use strict'; + const external_parent_sdk_namespaceObject = globalThis['parent-sdk']; + alert(external_parent_sdk_namespaceObject.version); + }, +)(); +``` diff --git a/website/docs/zh/config/lib/format.mdx b/website/docs/zh/config/lib/format.mdx index fc3377fb3..f882901e2 100644 --- a/website/docs/zh/config/lib/format.mdx +++ b/website/docs/zh/config/lib/format.mdx @@ -1,6 +1,6 @@ # lib.format -- **类型:** `'esm' | 'cjs' | 'umd' | 'mf'` +- **类型:** `'esm' | 'cjs' | 'umd' | 'mf' | 'iife'` - **默认值:** `'esm'` 指定生成的 JavaScript 产物的输出格式。 @@ -10,9 +10,10 @@ - `esm`:[modern-module](https://rspack.dev/zh/config/output#type-modern-module) - `cjs`:[commonjs-static](https://rspack.dev/zh/config/output#type-commonjs-static) - `umd`:[umd](https://rspack.dev/zh/config/output#type-umd) +- `iife`:开启了 [output.iife](https://rspack.dev/zh/config/output#outputiife) 的 [modern-module](https://rspack.dev/zh/config/output#type-modern-module) 更多详情请参考 [输出格式](/guide/basic/output-format) 和 [模块联邦](/guide/advanced/module-federation)。 ::: note -`umd` 格式仅在 [bundle](/config/lib/bundle) 设置为 `true` 时有效。 +`umd`、`mf` 以及 `iife` 格式仅在 [bundle](/config/lib/bundle) 设置为 `true` 时有效。 ::: diff --git a/website/docs/zh/guide/basic/output-format.mdx b/website/docs/zh/guide/basic/output-format.mdx index 3663314c2..6bfbc73cd 100644 --- a/website/docs/zh/guide/basic/output-format.mdx +++ b/website/docs/zh/guide/basic/output-format.mdx @@ -130,3 +130,30 @@ export default defineConfig({ ### 什么是 MF? MF 代表 Module Federation。 + +## IIFE + +{/* 以下文档复制自 https://esbuild.github.io/api/#format-iife */} + +IIFE 格式代表「立即调用函数表达式」,旨在浏览器中运行。将代码包裹在函数表达式中,可确保代码中的任何变量不会意外与全局作用域中的变量发生冲突。若你的入口点有需要在浏览器中作为全局变量暴露的导出内容,可通过全局名称设置来配置该全局变量的名称。 + +在 IIFE 格式下,[output.globalObject](https://rspack.dev/zh/config/output#outputglobalobject) 默认设置为 [globalThis](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/globalThis),源码中命中了 [externals](/config/rsbuild/output#outputexternals) 的 `import` 语句将被转换为通过 `globalThis` 上的属性访问,你可以覆盖 [output.globalObject](https://rspack.dev/zh/config/output#outputglobalobject) 为任意值。 + +指定 `iife` 格式时源码及对应产物如下: + +```js title="源码" +// externals 已经将 parent-sdk 作为外部依赖 +// externals: ['parent-sdk'] +import { version } from 'parent-sdk'; +alert(version); +``` + +```js title="IIFE 产物" +( + () => { + 'use strict'; + const external_parent_sdk_namespaceObject = globalThis['parent-sdk']; + alert(external_parent_sdk_namespaceObject.version); + }, +)(); +```