diff --git a/biome.json b/biome.json index 1e776d755..b8cb0eac5 100644 --- a/biome.json +++ b/biome.json @@ -27,7 +27,8 @@ "javascript": { "formatter": { "quoteStyle": "single" - } + }, + "jsxRuntime": "reactClassic" }, "json": { "formatter": { diff --git a/examples/react-component-bundle-false/rslib.config.ts b/examples/react-component-bundle-false/rslib.config.ts index ab676d646..7dd310a6f 100644 --- a/examples/react-component-bundle-false/rslib.config.ts +++ b/examples/react-component-bundle-false/rslib.config.ts @@ -35,5 +35,12 @@ export default defineConfig({ }, }, ], - plugins: [pluginReact(), pluginSass()], + plugins: [ + pluginReact({ + swcReactOptions: { + runtime: 'classic', + }, + }), + pluginSass(), + ], }); diff --git a/examples/react-component-bundle-false/src/components/CounterButton/index.tsx b/examples/react-component-bundle-false/src/components/CounterButton/index.tsx index c528a2769..1b3260a4e 100644 --- a/examples/react-component-bundle-false/src/components/CounterButton/index.tsx +++ b/examples/react-component-bundle-false/src/components/CounterButton/index.tsx @@ -1,4 +1,6 @@ +import React from 'react'; import styles from './index.module.scss'; + interface CounterButtonProps { onClick: () => void; label: string; diff --git a/examples/react-component-bundle-false/src/index.tsx b/examples/react-component-bundle-false/src/index.tsx index 816f1310c..b7e472bb2 100644 --- a/examples/react-component-bundle-false/src/index.tsx +++ b/examples/react-component-bundle-false/src/index.tsx @@ -1,3 +1,4 @@ +import React from 'react'; import { CounterButton } from './components/CounterButton/index'; import { useCounter } from './useCounter'; import './index.scss'; diff --git a/examples/react-component-bundle-false/tsconfig.json b/examples/react-component-bundle-false/tsconfig.json index 78ba7070a..2142e121c 100644 --- a/examples/react-component-bundle-false/tsconfig.json +++ b/examples/react-component-bundle-false/tsconfig.json @@ -7,7 +7,7 @@ "esModuleInterop": true, "forceConsistentCasingInFileNames": true, "isolatedModules": true, - "jsx": "react-jsx", + "jsx": "react", "lib": ["DOM", "ESNext"], "moduleResolution": "node", "resolveJsonModule": true, diff --git a/examples/react-component-bundle/rslib.config.ts b/examples/react-component-bundle/rslib.config.ts index e7cfc9898..941177741 100644 --- a/examples/react-component-bundle/rslib.config.ts +++ b/examples/react-component-bundle/rslib.config.ts @@ -33,5 +33,12 @@ export default defineConfig({ }, }, ], - plugins: [pluginReact(), pluginSass()], + plugins: [ + pluginReact({ + swcReactOptions: { + runtime: 'classic', + }, + }), + pluginSass(), + ], }); diff --git a/examples/react-component-bundle/src/components/CounterButton/index.tsx b/examples/react-component-bundle/src/components/CounterButton/index.tsx index c528a2769..1b3260a4e 100644 --- a/examples/react-component-bundle/src/components/CounterButton/index.tsx +++ b/examples/react-component-bundle/src/components/CounterButton/index.tsx @@ -1,4 +1,6 @@ +import React from 'react'; import styles from './index.module.scss'; + interface CounterButtonProps { onClick: () => void; label: string; diff --git a/examples/react-component-bundle/src/index.tsx b/examples/react-component-bundle/src/index.tsx index 816f1310c..b7e472bb2 100644 --- a/examples/react-component-bundle/src/index.tsx +++ b/examples/react-component-bundle/src/index.tsx @@ -1,3 +1,4 @@ +import React from 'react'; import { CounterButton } from './components/CounterButton/index'; import { useCounter } from './useCounter'; import './index.scss'; diff --git a/examples/react-component-bundle/tsconfig.json b/examples/react-component-bundle/tsconfig.json index 78ba7070a..2142e121c 100644 --- a/examples/react-component-bundle/tsconfig.json +++ b/examples/react-component-bundle/tsconfig.json @@ -7,7 +7,7 @@ "esModuleInterop": true, "forceConsistentCasingInFileNames": true, "isolatedModules": true, - "jsx": "react-jsx", + "jsx": "react", "lib": ["DOM", "ESNext"], "moduleResolution": "node", "resolveJsonModule": true, diff --git a/examples/react-component-umd/README.md b/examples/react-component-umd/README.md new file mode 100644 index 000000000..38cfefa89 --- /dev/null +++ b/examples/react-component-umd/README.md @@ -0,0 +1,3 @@ +# @examples/react-component-umd + +This example demonstrates how to use Rslib to build a simple React component with umd format output. diff --git a/examples/react-component-umd/package.json b/examples/react-component-umd/package.json new file mode 100644 index 000000000..95eb055d3 --- /dev/null +++ b/examples/react-component-umd/package.json @@ -0,0 +1,19 @@ +{ + "name": "@examples/react-component-umd", + "private": true, + "main": "./dist/umd/index.js", + "unpkg": "./dist/umd/index.js", + "scripts": { + "build": "rslib build" + }, + "devDependencies": { + "@rsbuild/plugin-react": "^1.0.4", + "@rsbuild/plugin-sass": "^1.0.3", + "@rslib/core": "workspace:*", + "@types/react": "^18.3.11", + "react": "^18.3.1" + }, + "peerDependencies": { + "react": "*" + } +} diff --git a/examples/react-component-umd/rslib.config.ts b/examples/react-component-umd/rslib.config.ts new file mode 100644 index 000000000..97c1f0ebd --- /dev/null +++ b/examples/react-component-umd/rslib.config.ts @@ -0,0 +1,28 @@ +import { pluginReact } from '@rsbuild/plugin-react'; +import { pluginSass } from '@rsbuild/plugin-sass'; +import { defineConfig } from '@rslib/core'; + +export default defineConfig({ + lib: [ + { + format: 'umd', + umdName: 'RslibUmdExample', + output: { + externals: { + react: 'React', + }, + distPath: { + root: './dist/umd', + }, + }, + }, + ], + plugins: [ + pluginReact({ + swcReactOptions: { + runtime: 'classic', + }, + }), + pluginSass(), + ], +}); diff --git a/examples/react-component-umd/src/components/CounterButton/index.module.scss b/examples/react-component-umd/src/components/CounterButton/index.module.scss new file mode 100644 index 000000000..19301eef2 --- /dev/null +++ b/examples/react-component-umd/src/components/CounterButton/index.module.scss @@ -0,0 +1,3 @@ +.button { + background: yellow; +} diff --git a/examples/react-component-umd/src/components/CounterButton/index.tsx b/examples/react-component-umd/src/components/CounterButton/index.tsx new file mode 100644 index 000000000..1b3260a4e --- /dev/null +++ b/examples/react-component-umd/src/components/CounterButton/index.tsx @@ -0,0 +1,16 @@ +import React from 'react'; +import styles from './index.module.scss'; + +interface CounterButtonProps { + onClick: () => void; + label: string; +} + +export const CounterButton: React.FC = ({ + onClick, + label, +}) => ( + +); diff --git a/examples/react-component-umd/src/env.d.ts b/examples/react-component-umd/src/env.d.ts new file mode 100644 index 000000000..0506fbcb4 --- /dev/null +++ b/examples/react-component-umd/src/env.d.ts @@ -0,0 +1,4 @@ +declare module '*.module.scss' { + const classes: { [key: string]: string }; + export default classes; +} diff --git a/examples/react-component-umd/src/index.scss b/examples/react-component-umd/src/index.scss new file mode 100644 index 000000000..2e506a0ac --- /dev/null +++ b/examples/react-component-umd/src/index.scss @@ -0,0 +1,3 @@ +.counter-text { + font-size: 50px; +} diff --git a/examples/react-component-umd/src/index.tsx b/examples/react-component-umd/src/index.tsx new file mode 100644 index 000000000..b7e472bb2 --- /dev/null +++ b/examples/react-component-umd/src/index.tsx @@ -0,0 +1,16 @@ +import React from 'react'; +import { CounterButton } from './components/CounterButton/index'; +import { useCounter } from './useCounter'; +import './index.scss'; + +export const Counter: React.FC = () => { + const { count, increment, decrement } = useCounter(); + + return ( +
+

Counter: {count}

+ + +
+ ); +}; diff --git a/examples/react-component-umd/src/useCounter.tsx b/examples/react-component-umd/src/useCounter.tsx new file mode 100644 index 000000000..885dbdfe0 --- /dev/null +++ b/examples/react-component-umd/src/useCounter.tsx @@ -0,0 +1,10 @@ +import { useState } from 'react'; + +export const useCounter = (initialValue = 0) => { + const [count, setCount] = useState(initialValue); + + const increment = () => setCount((prev) => prev + 1); + const decrement = () => setCount((prev) => prev - 1); + + return { count, increment, decrement }; +}; diff --git a/examples/react-component-umd/tsconfig.json b/examples/react-component-umd/tsconfig.json new file mode 100644 index 000000000..2142e121c --- /dev/null +++ b/examples/react-component-umd/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "allowJs": true, + "baseUrl": ".", + "declaration": true, + "emitDeclarationOnly": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "isolatedModules": true, + "jsx": "react", + "lib": ["DOM", "ESNext"], + "moduleResolution": "node", + "resolveJsonModule": true, + "rootDir": "src", + "skipLibCheck": true, + "strict": true + }, + "exclude": ["**/node_modules"], + "include": ["src"] +} diff --git a/packages/core/src/config.ts b/packages/core/src/config.ts index a84216c8e..4f69016c5 100644 --- a/packages/core/src/config.ts +++ b/packages/core/src/config.ts @@ -33,6 +33,7 @@ import type { BannerAndFooter, Format, LibConfig, + LibOnlyConfig, PkgJson, Redirect, RsbuildConfigOutputTarget, @@ -449,7 +450,11 @@ export async function createConstantRsbuildConfig(): Promise { }); } -const composeFormatConfig = (format: Format): RsbuildConfig => { +const composeFormatConfig = ({ + format, + bundle = true, + umdName, +}: { format: Format; bundle?: boolean; umdName?: string }): RsbuildConfig => { const jsParserOptions = { cjs: { requireResolve: false, @@ -517,8 +522,14 @@ const composeFormatConfig = (format: Format): RsbuildConfig => { }, }, }; - case 'umd': - return { + case 'umd': { + if (bundle === false) { + throw new Error( + 'When using "umd" 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: RsbuildConfig = { tools: { rspack: { module: { @@ -529,13 +540,23 @@ const composeFormatConfig = (format: Format): RsbuildConfig => { }, }, output: { - library: { - type: 'umd', - }, + asyncChunks: false, + + library: umdName + ? { + type: 'umd', + name: umdName, + } + : { + type: 'umd', + }, }, }, }, }; + + return config; + } default: throw new Error(`Unsupported format: ${format}`); } @@ -785,7 +806,7 @@ const composeBundleConfig = ( jsExtension: string, redirect: Redirect, cssModulesAuto: CssLoaderOptionsAuto, - bundle = true, + bundle: boolean, ): RsbuildConfig => { if (bundle) return {}; @@ -957,15 +978,21 @@ async function composeLibRsbuildConfig(config: LibConfig, configPath: string) { const { format, shims, + bundle = true, banner = {}, footer = {}, autoExtension = true, autoExternal = true, externalHelpers = false, redirect = {}, + umdName, } = config; const shimsConfig = composeShimsConfig(format!, shims); - const formatConfig = composeFormatConfig(format!); + const formatConfig = composeFormatConfig({ + format: format!, + bundle, + umdName, + }); const externalHelpersConfig = composeExternalHelpersConfig( externalHelpers, pkgJson, @@ -983,7 +1010,7 @@ async function composeLibRsbuildConfig(config: LibConfig, configPath: string) { jsExtension, redirect, cssModulesAuto, - config.bundle, + bundle, ); const targetConfig = composeTargetConfig(config.output?.target); const syntaxConfig = composeSyntaxConfig( @@ -1086,19 +1113,20 @@ export async function composeCreateRsbuildConfig( config: mergeRsbuildConfig( constantRsbuildConfig, libRsbuildConfig, - omit(userConfig, [ - 'bundle', - 'format', - 'autoExtension', - 'autoExternal', - 'redirect', - 'syntax', - 'externalHelpers', - 'banner', - 'footer', - 'dts', - 'shims', - ]), + omit(userConfig, { + bundle: true, + format: true, + autoExtension: true, + autoExternal: true, + redirect: true, + syntax: true, + externalHelpers: true, + banner: true, + footer: true, + dts: true, + shims: true, + umdName: true, + }), ), }; }); diff --git a/packages/core/src/types/config/index.ts b/packages/core/src/types/config/index.ts index 38e1a9995..dcfe7eea4 100644 --- a/packages/core/src/types/config/index.ts +++ b/packages/core/src/types/config/index.ts @@ -80,8 +80,11 @@ export interface LibConfig extends RsbuildConfig { footer?: BannerAndFooter; shims?: Shims; dts?: Dts; + umdName?: string; } +export type LibOnlyConfig = Omit; + export interface RslibConfig extends RsbuildConfig { lib: LibConfig[]; } diff --git a/packages/core/src/utils/helper.ts b/packages/core/src/utils/helper.ts index 15d794bc9..00bdc66ea 100644 --- a/packages/core/src/utils/helper.ts +++ b/packages/core/src/utils/helper.ts @@ -147,16 +147,17 @@ export function pick( export function omit( obj: T, - keys: ReadonlyArray, -): Omit { + keysObj: Record, +): Omit { + type K = keyof U; return Object.keys(obj).reduce( (ret, key) => { - if (!keys.includes(key as U)) { - ret[key as keyof Omit] = obj[key as keyof Omit]; + if (keysObj[key as U] !== true) { + ret[key as keyof Omit] = obj[key as keyof Omit]; } return ret; }, - {} as Omit, + {} as Omit, ); } diff --git a/packages/core/tests/__snapshots__/config.test.ts.snap b/packages/core/tests/__snapshots__/config.test.ts.snap index 579de5fee..c4b17f0a4 100644 --- a/packages/core/tests/__snapshots__/config.test.ts.snap +++ b/packages/core/tests/__snapshots__/config.test.ts.snap @@ -404,6 +404,7 @@ exports[`Should compose create Rsbuild config correctly > Merge Rsbuild config 1 }, }, "output": { + "asyncChunks": false, "library": { "type": "umd", }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3d954ba1c..c18e12746 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -111,6 +111,24 @@ importers: specifier: ^18.3.1 version: 18.3.1 + examples/react-component-umd: + devDependencies: + '@rsbuild/plugin-react': + specifier: ^1.0.4 + version: 1.0.4(@rsbuild/core@1.0.14) + '@rsbuild/plugin-sass': + specifier: ^1.0.3 + version: 1.0.3(@rsbuild/core@1.0.14) + '@rslib/core': + specifier: workspace:* + version: link:../../packages/core + '@types/react': + specifier: ^18.3.11 + version: 18.3.11 + react: + specifier: ^18.3.1 + version: 18.3.1 + packages/core: dependencies: '@microsoft/api-extractor': @@ -287,6 +305,9 @@ importers: '@examples/react-component-bundle-false': specifier: workspace:* version: link:../../../examples/react-component-bundle-false + '@examples/react-component-umd': + specifier: workspace:* + version: link:../../../examples/react-component-umd tests/integration/alias: {} @@ -560,6 +581,23 @@ importers: tests/integration/tsconfig: {} + tests/integration/umd: {} + + tests/integration/umd-globals: + devDependencies: + react: + specifier: ^18.3.1 + version: 18.3.1 + react-aliased: + specifier: npm:react@18.3.0 + version: react@18.3.0 + + tests/integration/umd-library-name: + devDependencies: + react: + specifier: ^18.3.1 + version: 18.3.1 + tests/scripts: {} website: @@ -3881,6 +3919,10 @@ packages: react: '>=16.6.0' react-dom: '>=16.6.0' + react@18.3.0: + resolution: {integrity: sha512-RPutkJftSAldDibyrjuku7q11d3oy6wKOyPe5K1HA/HwwrXcEqBdHsLypkC2FFYjP7bPUa6gbzSBhw4sY2JcDg==} + engines: {node: '>=0.10.0'} + react@18.3.1: resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} engines: {node: '>=0.10.0'} @@ -8601,6 +8643,10 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) + react@18.3.0: + dependencies: + loose-envify: 1.4.0 + react@18.3.1: dependencies: loose-envify: 1.4.0 diff --git a/tests/README.md b/tests/README.md index 5aaaf4ecf..fdbae8ab8 100644 --- a/tests/README.md +++ b/tests/README.md @@ -23,7 +23,7 @@ Rslib will try to cover the common scenarios in the [integration test cases of M | dts-composite | ⚪️ | | | esbuildOptions | ⚫️ | | | externals | 🟢 | | -| format | 🟡 | Support `cjs` and `esm`, `umd` still need to be tested | +| format | 🟢 | | | input | 🟢 | | | jsx | ⚪️ | | | metafile | ⚫️ | | @@ -42,5 +42,5 @@ Rslib will try to cover the common scenarios in the [integration test cases of M | transformLodash | 🟢 | | | tsconfig | 🟢 | | | tsconfigExtends | 🟢 | | -| umdGlobals | ⚪️ | | -| umdModuleName | ⚪️ | | +| umdGlobals | 🟢 | | +| umdModuleName | 🟡 | lacks 1. non string type 2. auto transform to camel case | diff --git a/tests/benchmark/index.bench.ts b/tests/benchmark/index.bench.ts index 1d33cd117..8930a268e 100644 --- a/tests/benchmark/index.bench.ts +++ b/tests/benchmark/index.bench.ts @@ -1,29 +1,47 @@ +import type { RslibConfig } from '@rslib/core'; import { getCwdByExample, rslibBuild } from 'test-helper'; import { bench, describe } from 'vitest'; -describe('run rslib in examples', () => { +// Remove dts emitting before isolated declaration landed as it's out of our performance scope. +const disableDts = (rslibConfig: RslibConfig) => { + for (const libConfig of rslibConfig.lib!) { + libConfig.dts = undefined; + } +}; + +const iterations = process.env.CI ? 10 : 50; + +describe('benchmark Rslib in examples', () => { bench( 'examples/express-plugin', async () => { const cwd = getCwdByExample('express-plugin'); - await rslibBuild({ cwd }); + await rslibBuild({ cwd, modifyConfig: disableDts }); }, - { time: 5 }, + { iterations }, ); bench( 'examples/react-component-bundle', async () => { const cwd = getCwdByExample('react-component-bundle'); - await rslibBuild({ cwd }); + await rslibBuild({ cwd, modifyConfig: disableDts }); }, - { time: 5 }, + { iterations }, ); bench( 'examples/react-component-bundle-false', async () => { const cwd = getCwdByExample('react-component-bundle-false'); - await rslibBuild({ cwd }); + await rslibBuild({ cwd, modifyConfig: disableDts }); + }, + { iterations }, + ); + bench( + 'examples/react-component-umd', + async () => { + const cwd = getCwdByExample('react-component-bundle-false'); + await rslibBuild({ cwd, modifyConfig: disableDts }); }, - { time: 5 }, + { iterations }, ); }); diff --git a/tests/e2e/react-component/.gitignore b/tests/e2e/react-component/.gitignore new file mode 100644 index 000000000..364fdec1a --- /dev/null +++ b/tests/e2e/react-component/.gitignore @@ -0,0 +1 @@ +public/ diff --git a/tests/e2e/react-component/index.pw.test.ts b/tests/e2e/react-component/index.pw.test.ts index f842d0535..c5ba1b514 100644 --- a/tests/e2e/react-component/index.pw.test.ts +++ b/tests/e2e/react-component/index.pw.test.ts @@ -1,12 +1,17 @@ +import fs from 'node:fs'; +import path from 'node:path'; import { type Page, expect, test } from '@playwright/test'; import { dev } from 'test-helper/rsbuild'; +function getCwdByExample(exampleName: string) { + return path.join(__dirname, '../../../examples', exampleName); +} + async function counterCompShouldWork(page: Page) { const h2El = page.locator('h2'); await expect(h2El).toHaveText('Counter: 0'); const buttonEl = page.locator('#root button'); - const [subtractEl, addEl] = await buttonEl.all(); await expect(h2El).toHaveText('Counter: 0'); @@ -33,19 +38,31 @@ test('should render example "react-component-bundle" successfully', async ({ const rsbuild = await dev({ cwd: __dirname, page, - rsbuildConfig: { - source: { - entry: { - index: './src/bundle.tsx', - }, - }, - }, + environment: ['bundle'], }); await counterCompShouldWork(page); - await styleShouldWork(page); + await rsbuild.close(); +}); + +test('should render example "react-component-umd" successfully', async ({ + page, +}) => { + const umdPath = path.resolve( + getCwdByExample('react-component-umd'), + './dist/umd/index.js', + ); + fs.mkdirSync(path.resolve(__dirname, './public/umd'), { recursive: true }); + fs.copyFileSync(umdPath, path.resolve(__dirname, './public/umd/index.js')); + const rsbuild = await dev({ + cwd: __dirname, + page, + environment: ['umd'], + }); + + await counterCompShouldWork(page); await rsbuild.close(); }); @@ -55,18 +72,10 @@ test('should render example "react-component-bundle-false" successfully', async const rsbuild = await dev({ cwd: __dirname, page, - rsbuildConfig: { - source: { - entry: { - index: './src/bundleFalse.tsx', - }, - }, - }, + environment: ['bundleFalse'], }); await counterCompShouldWork(page); - await styleShouldWork(page); - await rsbuild.close(); }); diff --git a/tests/e2e/react-component/package.json b/tests/e2e/react-component/package.json index 7a870befc..a1083e0ff 100644 --- a/tests/e2e/react-component/package.json +++ b/tests/e2e/react-component/package.json @@ -2,8 +2,14 @@ "name": "react-component-e2e", "version": "1.0.0", "private": true, + "scripts": { + "dev:bundle": "../../node_modules/.bin/rsbuild dev --environment=bundle", + "dev:bundle-false": "../../node_modules/.bin/rsbuild dev --environment=bundleFalse", + "dev:umd": "../../node_modules/.bin/rsbuild dev --environment=umd" + }, "dependencies": { "@examples/react-component-bundle": "workspace:*", - "@examples/react-component-bundle-false": "workspace:*" + "@examples/react-component-bundle-false": "workspace:*", + "@examples/react-component-umd": "workspace:*" } } diff --git a/tests/e2e/react-component/rsbuild.config.ts b/tests/e2e/react-component/rsbuild.config.ts index c9962d33f..ff826363f 100644 --- a/tests/e2e/react-component/rsbuild.config.ts +++ b/tests/e2e/react-component/rsbuild.config.ts @@ -2,5 +2,63 @@ import { defineConfig } from '@rsbuild/core'; import { pluginReact } from '@rsbuild/plugin-react'; export default defineConfig({ + environments: { + bundle: { + source: { + entry: { + index: './src/bundle.tsx', + }, + }, + }, + bundleFalse: { + source: { + entry: { + index: './src/bundleFalse.tsx', + }, + }, + }, + umd: { + html: { + tags: [ + { + tag: 'script', + attrs: { + src: 'https://unpkg.com/react@18/umd/react.development.js', + }, + head: true, + append: true, + }, + { + tag: 'script', + attrs: { + src: 'https://unpkg.com/react-dom@18/umd/react-dom.development.js', + }, + head: true, + append: true, + }, + { + tag: 'script', + attrs: { + src: '/umd/index.js', + }, + head: true, + append: true, + }, + ], + }, + source: { + entry: { + index: './src/umd.tsx', + }, + }, + output: { + externals: { + react: 'window React', + 'react-dom': 'window ReactDom', + 'react-dom/client': 'window ReactDom', + }, + }, + }, + }, plugins: [pluginReact()], }); diff --git a/tests/e2e/react-component/src/umd.tsx b/tests/e2e/react-component/src/umd.tsx new file mode 100644 index 000000000..43f64c298 --- /dev/null +++ b/tests/e2e/react-component/src/umd.tsx @@ -0,0 +1,20 @@ +const React = window.React; +const ReactDOM = window.ReactDOM; + +// @ts-expect-error not types for UMD +const RslibUmdExample = window.RslibUmdExample; +const Counter = RslibUmdExample.Counter; + +const App = () => ( +
+ +
+); + +// @ts-expect-error not types for UMD +const root = ReactDOM.createRoot(document.getElementById('root')!); +root.render( + + + , +); diff --git a/tests/integration/umd-globals/index.test.ts b/tests/integration/umd-globals/index.test.ts new file mode 100644 index 000000000..b00bd4601 --- /dev/null +++ b/tests/integration/umd-globals/index.test.ts @@ -0,0 +1,12 @@ +import { buildAndGetResults } from 'test-helper'; +import { expect, test } from 'vitest'; + +test('correct read globals from CommonJS', async () => { + const fixturePath = __dirname; + const { entryFiles } = await buildAndGetResults({ + fixturePath, + }); + + const { fn } = require(entryFiles.umd); + expect(await fn('ok')).toBe('DEBUG:18.3.0/ok'); +}); diff --git a/tests/integration/umd-globals/package.json b/tests/integration/umd-globals/package.json new file mode 100644 index 000000000..930ebb27e --- /dev/null +++ b/tests/integration/umd-globals/package.json @@ -0,0 +1,12 @@ +{ + "name": "umd-globals-test", + "version": "1.0.0", + "private": true, + "devDependencies": { + "react": "^18.3.1", + "react-aliased": "npm:react@18.3.0" + }, + "peerDependencies": { + "react": "^18.3.1" + } +} diff --git a/tests/integration/umd-globals/rslib.config.ts b/tests/integration/umd-globals/rslib.config.ts new file mode 100644 index 000000000..499892d10 --- /dev/null +++ b/tests/integration/umd-globals/rslib.config.ts @@ -0,0 +1,16 @@ +import { defineConfig } from '@rslib/core'; +import { generateBundleUmdConfig } from 'test-helper'; + +export default defineConfig({ + lib: [generateBundleUmdConfig()], + output: { + externals: { + react: 'react-aliased', + }, + }, + source: { + entry: { + index: './src/index.js', + }, + }, +}); diff --git a/tests/integration/umd-globals/src/common.js b/tests/integration/umd-globals/src/common.js new file mode 100644 index 000000000..b4b6301c8 --- /dev/null +++ b/tests/integration/umd-globals/src/common.js @@ -0,0 +1 @@ +export const addPrefix = (prefix, str) => `${prefix}:${str}`; diff --git a/tests/integration/umd-globals/src/index.js b/tests/integration/umd-globals/src/index.js new file mode 100644 index 000000000..230e9f372 --- /dev/null +++ b/tests/integration/umd-globals/src/index.js @@ -0,0 +1,6 @@ +import React from 'react'; + +export const fn = async (str) => { + const { addPrefix } = await import('./common'); + return addPrefix('DEBUG', `${React.version}/${str}`); +}; diff --git a/tests/integration/umd-library-name/index.test.ts b/tests/integration/umd-library-name/index.test.ts new file mode 100644 index 000000000..883467a58 --- /dev/null +++ b/tests/integration/umd-library-name/index.test.ts @@ -0,0 +1,24 @@ +import vm from 'node:vm'; +import { buildAndGetResults } from 'test-helper'; +import { expect, test } from 'vitest'; + +test('correct read UMD name from CommonJS', async () => { + const fixturePath = __dirname; + const { entries } = await buildAndGetResults({ + fixturePath, + }); + + const mockGlobalThis = { + react: { + version: '1.2.3', + }, + }; + const context = vm.createContext({ + globalThis: mockGlobalThis, + }); + + vm.runInContext(entries.umd, context); + + // @ts-expect-error + expect(await mockGlobalThis.MyLibrary.fn('ok')).toBe('DEBUG:1.2.3/ok'); +}); diff --git a/tests/integration/umd-library-name/package.json b/tests/integration/umd-library-name/package.json new file mode 100644 index 000000000..9e9067c4b --- /dev/null +++ b/tests/integration/umd-library-name/package.json @@ -0,0 +1,11 @@ +{ + "name": "umd-library-name-test", + "version": "1.0.0", + "private": true, + "devDependencies": { + "react": "^18.3.1" + }, + "peerDependencies": { + "react": "^18.3.1" + } +} diff --git a/tests/integration/umd-library-name/rslib.config.ts b/tests/integration/umd-library-name/rslib.config.ts new file mode 100644 index 000000000..161c0e549 --- /dev/null +++ b/tests/integration/umd-library-name/rslib.config.ts @@ -0,0 +1,15 @@ +import { defineConfig } from '@rslib/core'; +import { generateBundleUmdConfig } from 'test-helper'; + +export default defineConfig({ + lib: [ + generateBundleUmdConfig({ + umdName: 'MyLibrary', + }), + ], + source: { + entry: { + index: './src/index.js', + }, + }, +}); diff --git a/tests/integration/umd-library-name/src/common.js b/tests/integration/umd-library-name/src/common.js new file mode 100644 index 000000000..b4b6301c8 --- /dev/null +++ b/tests/integration/umd-library-name/src/common.js @@ -0,0 +1 @@ +export const addPrefix = (prefix, str) => `${prefix}:${str}`; diff --git a/tests/integration/umd-library-name/src/index.js b/tests/integration/umd-library-name/src/index.js new file mode 100644 index 000000000..230e9f372 --- /dev/null +++ b/tests/integration/umd-library-name/src/index.js @@ -0,0 +1,6 @@ +import React from 'react'; + +export const fn = async (str) => { + const { addPrefix } = await import('./common'); + return addPrefix('DEBUG', `${React.version}/${str}`); +}; diff --git a/tests/integration/umd/index.test.ts b/tests/integration/umd/index.test.ts new file mode 100644 index 000000000..021b9fbac --- /dev/null +++ b/tests/integration/umd/index.test.ts @@ -0,0 +1,24 @@ +import { buildAndGetResults } from 'test-helper'; +import { expect, test } from 'vitest'; + +test('read UMD value in CommonJS', async () => { + const fixturePath = __dirname; + const { entryFiles } = await buildAndGetResults({ + fixturePath, + }); + + const fn = require(entryFiles.umd); + expect(fn('ok')).toBe('DEBUG:ok'); +}); + +test('throw error when using UMD with `bundle: false`', async () => { + const fixturePath = __dirname; + const build = buildAndGetResults({ + fixturePath, + configPath: './rslibBundleFalse.config.ts', + }); + + expect(build).rejects.toThrowErrorMatchingInlineSnapshot( + `[Error: When using "umd" 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/umd/package.json b/tests/integration/umd/package.json new file mode 100644 index 000000000..1bfc86471 --- /dev/null +++ b/tests/integration/umd/package.json @@ -0,0 +1,5 @@ +{ + "name": "umd-test", + "version": "1.0.0", + "private": true +} diff --git a/tests/integration/umd/rslib.config.ts b/tests/integration/umd/rslib.config.ts new file mode 100644 index 000000000..507642542 --- /dev/null +++ b/tests/integration/umd/rslib.config.ts @@ -0,0 +1,11 @@ +import { defineConfig } from '@rslib/core'; +import { generateBundleUmdConfig } from 'test-helper'; + +export default defineConfig({ + lib: [generateBundleUmdConfig()], + source: { + entry: { + index: './src/index.js', + }, + }, +}); diff --git a/tests/integration/umd/rslibBundleFalse.config.ts b/tests/integration/umd/rslibBundleFalse.config.ts new file mode 100644 index 000000000..914a0cdfd --- /dev/null +++ b/tests/integration/umd/rslibBundleFalse.config.ts @@ -0,0 +1,15 @@ +import { defineConfig } from '@rslib/core'; +import { generateBundleUmdConfig } from 'test-helper'; + +export default defineConfig({ + lib: [ + generateBundleUmdConfig({ + bundle: false, + }), + ], + source: { + entry: { + index: './src/index.js', + }, + }, +}); diff --git a/tests/integration/umd/src/index.js b/tests/integration/umd/src/index.js new file mode 100644 index 000000000..a33d9aa51 --- /dev/null +++ b/tests/integration/umd/src/index.js @@ -0,0 +1,3 @@ +const { addPrefix } = require('./utils'); + +module.exports = (str) => addPrefix('DEBUG:', str); diff --git a/tests/integration/umd/src/utils.js b/tests/integration/umd/src/utils.js new file mode 100644 index 000000000..280e6c37b --- /dev/null +++ b/tests/integration/umd/src/utils.js @@ -0,0 +1,5 @@ +const addPrefix = (prefix, str) => `${prefix}${str}`; + +module.exports = { + addPrefix, +}; diff --git a/tests/scripts/package.json b/tests/scripts/package.json index 77f4e201a..852b2edfa 100644 --- a/tests/scripts/package.json +++ b/tests/scripts/package.json @@ -2,6 +2,15 @@ "name": "test-helper", "version": "1.0.0", "private": true, + "type": "module", + "exports": { + ".": "./index.ts", + "./helper": "./helper.ts", + "./index": "./index.ts", + "./rsbuild": "./rsbuild.ts", + "./shared": "./shared.ts", + "./vitest": "./vitest.ts" + }, "main": "index.ts", "types": "index.ts" } diff --git a/tests/scripts/shared.ts b/tests/scripts/shared.ts index 8b86fdb26..bc4a389e6 100644 --- a/tests/scripts/shared.ts +++ b/tests/scripts/shared.ts @@ -43,6 +43,19 @@ export function generateBundleCjsConfig(config: LibConfig = {}): LibConfig { return mergeConfig(cjsBasicConfig, config)!; } +export function generateBundleUmdConfig(config: LibConfig = {}): LibConfig { + const umdBasicConfig: LibConfig = { + format: 'umd', + output: { + distPath: { + root: './dist/umd', + }, + }, + }; + + return mergeConfig(umdBasicConfig, config)!; +} + export type FormatType = Format | `${Format}${number}`; type FilePath = string; @@ -150,11 +163,17 @@ export async function getResults( export async function rslibBuild({ cwd, path, -}: { cwd: string; path?: string }) { + modifyConfig, +}: { + cwd: string; + path?: string; + modifyConfig?: (config: RslibConfig) => void; +}) { const rslibConfig = await loadConfig({ cwd, path, }); + modifyConfig?.(rslibConfig); process.chdir(cwd); const rsbuildInstance = await build(rslibConfig); return { rsbuildInstance, rslibConfig }; diff --git a/tests/vitest.config.ts b/tests/vitest.config.ts index 6db1de3c3..5dbbe9211 100644 --- a/tests/vitest.config.ts +++ b/tests/vitest.config.ts @@ -13,5 +13,6 @@ export default defineConfig({ include: ['./benchmark/**/*.bench.ts'], }, }, - plugins: [codspeedPlugin()], + // Don't run CodSpeed locally as no instruments are setup. + plugins: [!!process.env.CI && codspeedPlugin()].filter(Boolean), }); diff --git a/website/docs/en/guide/_meta.json b/website/docs/en/guide/_meta.json index 4fcd3dcdd..3fb5f7dc2 100644 --- a/website/docs/en/guide/_meta.json +++ b/website/docs/en/guide/_meta.json @@ -8,5 +8,10 @@ "type": "dir", "name": "basic", "label": "Basic" + }, + { + "type": "dir", + "name": "advanced", + "label": "Advanced" } ] diff --git a/website/docs/en/guide/advanced/_meta.json b/website/docs/en/guide/advanced/_meta.json new file mode 100644 index 000000000..fe51488c7 --- /dev/null +++ b/website/docs/en/guide/advanced/_meta.json @@ -0,0 +1 @@ +[] diff --git a/website/docs/en/guide/basic/UMD.mdx b/website/docs/en/guide/basic/UMD.mdx new file mode 100644 index 000000000..4cfe5fea9 --- /dev/null +++ b/website/docs/en/guide/basic/UMD.mdx @@ -0,0 +1,49 @@ +# UMD + +## Introduction + +UMD is a library that can be used in both the browser and Node.js environments. It is a combination of CommonJS and AMD. + +## How to build a UMD library? + +- Set the `output.format` to `umd` in the Rslib configuration file. +- If the library need to be exported with a name, set `output.umdName` to the name of the UMD library. +- Use `output.externals` to specify the external dependencies that the UMD library depends on, `lib.autoExtension` is enabled by default for UMD. + +## Examples + +The following Rslib config is an example to build a UMD library. + +- `output.format: 'umd'`: instruct Rslib to build in UMD format. +- `output.umdName: 'RslibUmdExample'`: set the export name of the UMD library. +- `output.externals.react: 'React'`: specify the external dependency `react` could be accessed by `window.React`. +- `runtime: 'classic'`: use the classic runtime of React to support applications that using React version under 18. + +```ts title="rslib.config.ts" {7-12,22} +import { pluginReact } from '@rsbuild/plugin-react'; +import { defineConfig } from '@rslib/core'; + +export default defineConfig({ + lib: [ + { + format: 'umd', + umdName: 'RslibUmdExample', + output: { + externals: { + react: 'React', + }, + distPath: { + root: './dist/umd', + }, + }, + }, + ], + plugins: [ + pluginReact({ + swcReactOptions: { + runtime: 'classic', + }, + }), + ], +}); +``` diff --git a/website/docs/en/guide/basic/_meta.json b/website/docs/en/guide/basic/_meta.json index df3df9223..a17e167b0 100644 --- a/website/docs/en/guide/basic/_meta.json +++ b/website/docs/en/guide/basic/_meta.json @@ -1 +1 @@ -["configure-rslib"] +["configure-rslib", "UMD"]