From de940e1afb66102b31db0ba6cd1691c4c4447d14 Mon Sep 17 00:00:00 2001 From: fi3ework Date: Wed, 16 Oct 2024 02:37:46 +0800 Subject: [PATCH 1/5] feat: support "umd" format --- biome.json | 3 +- .../rslib.config.ts | 9 ++- .../src/components/CounterButton/index.tsx | 2 + .../src/index.tsx | 1 + .../tsconfig.json | 2 +- .../react-component-bundle/rslib.config.ts | 9 ++- .../src/components/CounterButton/index.tsx | 2 + examples/react-component-bundle/src/index.tsx | 1 + examples/react-component-bundle/tsconfig.json | 2 +- examples/react-component-umd/README.md | 3 + examples/react-component-umd/package.json | 19 ++++++ examples/react-component-umd/rslib.config.ts | 30 ++++++++++ .../CounterButton/index.module.scss | 3 + .../src/components/CounterButton/index.tsx | 16 +++++ examples/react-component-umd/src/env.d.ts | 4 ++ examples/react-component-umd/src/index.scss | 3 + examples/react-component-umd/src/index.tsx | 16 +++++ .../react-component-umd/src/useCounter.tsx | 10 ++++ examples/react-component-umd/tsconfig.json | 20 +++++++ packages/core/src/config.ts | 44 +++++++++++--- packages/core/src/types/config/index.ts | 1 + .../tests/__snapshots__/config.test.ts.snap | 1 + pnpm-lock.yaml | 46 +++++++++++++++ tests/README.md | 6 +- tests/benchmark/index.bench.ts | 32 +++++++--- tests/e2e/react-component/.gitignore | 1 + tests/e2e/react-component/index.pw.test.ts | 45 ++++++++------ tests/e2e/react-component/package.json | 8 ++- tests/e2e/react-component/rsbuild.config.ts | 58 +++++++++++++++++++ tests/e2e/react-component/src/umd.tsx | 20 +++++++ tests/integration/umd-globals/index.test.ts | 12 ++++ tests/integration/umd-globals/package.json | 12 ++++ tests/integration/umd-globals/rslib.config.ts | 16 +++++ tests/integration/umd-globals/src/common.js | 1 + tests/integration/umd-globals/src/index.js | 6 ++ .../umd-library-name/index.test.ts | 24 ++++++++ .../integration/umd-library-name/package.json | 11 ++++ .../umd-library-name/rslib.config.ts | 15 +++++ .../umd-library-name/src/common.js | 1 + .../integration/umd-library-name/src/index.js | 6 ++ tests/integration/umd/index.test.ts | 24 ++++++++ tests/integration/umd/package.json | 5 ++ tests/integration/umd/rslib.config.ts | 11 ++++ .../umd/rslibBundleFalse.config.ts | 15 +++++ tests/integration/umd/src/index.js | 3 + tests/integration/umd/src/utils.js | 5 ++ tests/scripts/package.json | 9 +++ tests/scripts/shared.ts | 21 ++++++- tests/vitest.config.ts | 3 +- website/docs/en/guide/_meta.json | 5 ++ website/docs/en/guide/advanced/_meta.json | 1 + website/docs/en/guide/basic/UMD.mdx | 49 ++++++++++++++++ website/docs/en/guide/basic/_meta.json | 2 +- 53 files changed, 628 insertions(+), 46 deletions(-) create mode 100644 examples/react-component-umd/README.md create mode 100644 examples/react-component-umd/package.json create mode 100644 examples/react-component-umd/rslib.config.ts create mode 100644 examples/react-component-umd/src/components/CounterButton/index.module.scss create mode 100644 examples/react-component-umd/src/components/CounterButton/index.tsx create mode 100644 examples/react-component-umd/src/env.d.ts create mode 100644 examples/react-component-umd/src/index.scss create mode 100644 examples/react-component-umd/src/index.tsx create mode 100644 examples/react-component-umd/src/useCounter.tsx create mode 100644 examples/react-component-umd/tsconfig.json create mode 100644 tests/e2e/react-component/.gitignore create mode 100644 tests/e2e/react-component/src/umd.tsx create mode 100644 tests/integration/umd-globals/index.test.ts create mode 100644 tests/integration/umd-globals/package.json create mode 100644 tests/integration/umd-globals/rslib.config.ts create mode 100644 tests/integration/umd-globals/src/common.js create mode 100644 tests/integration/umd-globals/src/index.js create mode 100644 tests/integration/umd-library-name/index.test.ts create mode 100644 tests/integration/umd-library-name/package.json create mode 100644 tests/integration/umd-library-name/rslib.config.ts create mode 100644 tests/integration/umd-library-name/src/common.js create mode 100644 tests/integration/umd-library-name/src/index.js create mode 100644 tests/integration/umd/index.test.ts create mode 100644 tests/integration/umd/package.json create mode 100644 tests/integration/umd/rslib.config.ts create mode 100644 tests/integration/umd/rslibBundleFalse.config.ts create mode 100644 tests/integration/umd/src/index.js create mode 100644 tests/integration/umd/src/utils.js create mode 100644 website/docs/en/guide/advanced/_meta.json create mode 100644 website/docs/en/guide/basic/UMD.mdx 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..0bd27a52c --- /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 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..6fcae0bfd --- /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 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..236031429 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 38e1a9995..32c51ae0c 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 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..a02aa3c7c --- /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 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"] From 063022d50c7c926b15a52b37c59eda8e9083dfdf Mon Sep 17 00:00:00 2001 From: Wei Date: Thu, 17 Oct 2024 14:15:20 +0800 Subject: [PATCH 2/5] Update examples/react-component-umd/README.md Co-authored-by: Timeless0911 <50201324+Timeless0911@users.noreply.github.com> --- examples/react-component-umd/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/react-component-umd/README.md b/examples/react-component-umd/README.md index 0bd27a52c..33009b3e1 100644 --- a/examples/react-component-umd/README.md +++ b/examples/react-component-umd/README.md @@ -1,3 +1,3 @@ -# @examples/react-component +# @examples/react-component-umd This example demonstrates how to use Rslib to build a simple React component. From ea38be445470d911ffbaefe6457b342b6b0c04ee Mon Sep 17 00:00:00 2001 From: Wei Date: Thu, 17 Oct 2024 14:15:32 +0800 Subject: [PATCH 3/5] Update examples/react-component-umd/README.md Co-authored-by: Timeless0911 <50201324+Timeless0911@users.noreply.github.com> --- examples/react-component-umd/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/react-component-umd/README.md b/examples/react-component-umd/README.md index 33009b3e1..38cfefa89 100644 --- a/examples/react-component-umd/README.md +++ b/examples/react-component-umd/README.md @@ -1,3 +1,3 @@ # @examples/react-component-umd -This example demonstrates how to use Rslib to build a simple React component. +This example demonstrates how to use Rslib to build a simple React component with umd format output. From 158acce2e1ecfdb4e60249aed701c56f1934e656 Mon Sep 17 00:00:00 2001 From: Wei Date: Thu, 17 Oct 2024 14:15:44 +0800 Subject: [PATCH 4/5] Update packages/core/src/config.ts Co-authored-by: Timeless0911 <50201324+Timeless0911@users.noreply.github.com> --- packages/core/src/config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/config.ts b/packages/core/src/config.ts index 236031429..fc8486a67 100644 --- a/packages/core/src/config.ts +++ b/packages/core/src/config.ts @@ -524,7 +524,7 @@ const composeFormatConfig = ({ 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".', + '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.', ); } From e9b87d39071af4328fc37357d113dbc30df0ea27 Mon Sep 17 00:00:00 2001 From: fi3ework Date: Thu, 17 Oct 2024 14:56:50 +0800 Subject: [PATCH 5/5] up --- examples/react-component-umd/rslib.config.ts | 2 -- packages/core/src/config.ts | 28 +++++++++++--------- packages/core/src/types/config/index.ts | 2 ++ packages/core/src/utils/helper.ts | 11 ++++---- tests/integration/umd/index.test.ts | 2 +- 5 files changed, 24 insertions(+), 21 deletions(-) diff --git a/examples/react-component-umd/rslib.config.ts b/examples/react-component-umd/rslib.config.ts index 6fcae0bfd..97c1f0ebd 100644 --- a/examples/react-component-umd/rslib.config.ts +++ b/examples/react-component-umd/rslib.config.ts @@ -13,8 +13,6 @@ export default defineConfig({ }, distPath: { root: './dist/umd', - css: '.', - cssAsync: '.', }, }, }, diff --git a/packages/core/src/config.ts b/packages/core/src/config.ts index fc8486a67..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, @@ -1112,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 32c51ae0c..dcfe7eea4 100644 --- a/packages/core/src/types/config/index.ts +++ b/packages/core/src/types/config/index.ts @@ -83,6 +83,8 @@ export interface LibConfig extends RsbuildConfig { 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/tests/integration/umd/index.test.ts b/tests/integration/umd/index.test.ts index a02aa3c7c..021b9fbac 100644 --- a/tests/integration/umd/index.test.ts +++ b/tests/integration/umd/index.test.ts @@ -19,6 +19,6 @@ test('throw error when using UMD with `bundle: false`', async () => { }); 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".]`, + `[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.]`, ); });