diff --git a/biome.json b/biome.json index 1e776d75..b8cb0eac 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 ab676d64..7dd310a6 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 c528a276..1b3260a4 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 816f1310..b7e472bb 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 78ba7070..2142e121 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 e7cfc989..94117774 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 c528a276..1b3260a4 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 816f1310..b7e472bb 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 78ba7070..2142e121 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 00000000..0bd27a52 --- /dev/null +++ b/examples/react-component-umd/README.md @@ -0,0 +1,3 @@ +# @examples/react-component + +This example demonstrates how to use Rslib to build a simple React component. diff --git a/examples/react-component-umd/package.json b/examples/react-component-umd/package.json new file mode 100644 index 00000000..95eb055d --- /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 00000000..6fcae0bf --- /dev/null +++ b/examples/react-component-umd/rslib.config.ts @@ -0,0 +1,30 @@ +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', + css: '.', + cssAsync: '.', + }, + }, + }, + ], + 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 00000000..19301eef --- /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 00000000..1b3260a4 --- /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 00000000..0506fbcb --- /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 00000000..2e506a0a --- /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 00000000..b7e472bb --- /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 00000000..885dbdfe --- /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 00000000..2142e121 --- /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 a84216c8..23603142 100644 --- a/packages/core/src/config.ts +++ b/packages/core/src/config.ts @@ -449,7 +449,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 +521,14 @@ const composeFormatConfig = (format: Format): RsbuildConfig => { }, }, }; - case 'umd': - return { + case 'umd': { + if (bundle === false) { + throw new Error( + 'When "format" is set to "umd", "bundle" must not be set to "false", consider setting "bundle" to "true" or remove the field, it\'s default value is "true".', + ); + } + + const config: RsbuildConfig = { tools: { rspack: { module: { @@ -529,13 +539,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 +805,7 @@ const composeBundleConfig = ( jsExtension: string, redirect: Redirect, cssModulesAuto: CssLoaderOptionsAuto, - bundle = true, + bundle: boolean, ): RsbuildConfig => { if (bundle) return {}; @@ -957,15 +977,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 +1009,7 @@ async function composeLibRsbuildConfig(config: LibConfig, configPath: string) { jsExtension, redirect, cssModulesAuto, - config.bundle, + bundle, ); const targetConfig = composeTargetConfig(config.output?.target); const syntaxConfig = composeSyntaxConfig( diff --git a/packages/core/src/types/config/index.ts b/packages/core/src/types/config/index.ts index 38e1a999..32c51ae0 100644 --- a/packages/core/src/types/config/index.ts +++ b/packages/core/src/types/config/index.ts @@ -80,6 +80,7 @@ export interface LibConfig extends RsbuildConfig { footer?: BannerAndFooter; shims?: Shims; dts?: Dts; + umdName?: string; } export interface RslibConfig extends RsbuildConfig { diff --git a/packages/core/tests/__snapshots__/config.test.ts.snap b/packages/core/tests/__snapshots__/config.test.ts.snap index 579de5fe..c4b17f0a 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 3d954ba1..c18e1274 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 5aaaf4ec..fdbae8ab 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 1d33cd11..8930a268 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 00000000..364fdec1 --- /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 f842d053..c5ba1b51 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 7a870bef..a1083e0f 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 c9962d33..ff826363 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 00000000..43f64c29 --- /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 00000000..b00bd460 --- /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 00000000..930ebb27 --- /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 00000000..499892d1 --- /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 00000000..b4b6301c --- /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 00000000..230e9f37 --- /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 00000000..883467a5 --- /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 00000000..9e9067c4 --- /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 00000000..161c0e54 --- /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 00000000..b4b6301c --- /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 00000000..230e9f37 --- /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 00000000..a02aa3c7 --- /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 "format" is set to "umd", "bundle" must not be set to "false", consider setting "bundle" to "true" or remove the field, it's default value is "true".]`, + ); +}); diff --git a/tests/integration/umd/package.json b/tests/integration/umd/package.json new file mode 100644 index 00000000..1bfc8647 --- /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 00000000..50764254 --- /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 00000000..914a0cdf --- /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 00000000..a33d9aa5 --- /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 00000000..280e6c37 --- /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 77f4e201..852b2edf 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 8b86fdb2..bc4a389e 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/website/docs/en/guide/_meta.json b/website/docs/en/guide/_meta.json index 4fcd3dcd..3fb5f7dc 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 00000000..fe51488c --- /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 00000000..4cfe5fea --- /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 df3df922..a17e167b 100644 --- a/website/docs/en/guide/basic/_meta.json +++ b/website/docs/en/guide/basic/_meta.json @@ -1 +1 @@ -["configure-rslib"] +["configure-rslib", "UMD"]