diff --git a/.github/workflows/reusable-build-test.yml b/.github/workflows/reusable-build-test.yml index 653b70db4e73..dd71aee68af9 100644 --- a/.github/workflows/reusable-build-test.yml +++ b/.github/workflows/reusable-build-test.yml @@ -16,7 +16,7 @@ on: jobs: e2e: name: E2E Testing - if: inputs.target == 'x86_64-unknown-linux-gnu' + if: inputs.target == 'x86_64-unknown-linux-gnu' || inputs.target == 'wasm32-wasip1-threads' runs-on: ${{ fromJSON(inputs.runner) }} defaults: run: @@ -51,6 +51,7 @@ jobs: - name: Run e2e uses: ./.github/actions/docker/run + if: inputs.target == 'x86_64-unknown-linux-gnu' with: # Jammy uses ubuntu 22.04 # If this is to change, make sure to upgrade the ubuntu version in GitHub Actions @@ -63,6 +64,22 @@ jobs: pnpm run build:js pnpm run test:e2e + - name: Run e2e for @rspack/browser + uses: ./.github/actions/docker/run + if: inputs.target == 'wasm32-wasip1-threads' + with: + # Jammy uses ubuntu 22.04 + # If this is to change, make sure to upgrade the ubuntu version in GitHub Actions + image: mcr.microsoft.com/playwright:v1.57.0-jammy + # .cache is required by download artifact, and mount in ./.github/actions/docker/run + # .tool_cache is required by pnpm + options: -v ${{ runner.tool_cache }}:${{runner.tool_cache}} + script: | + export PATH=${{ steps.calculate-node-bin-path.outputs.path }}:$PATH + export WASM=1 + pnpm run build:js + pnpm run test:e2e + test: runs-on: ${{ fromJSON(inputs.runner) }} timeout-minutes: 60 diff --git a/packages/rspack/rslib.browser.config.mts b/packages/rspack/rslib.browser.config.mts index 2d1a16262fd5..0555f966f887 100644 --- a/packages/rspack/rslib.browser.config.mts +++ b/packages/rspack/rslib.browser.config.mts @@ -23,7 +23,7 @@ export default defineConfig({ }, }, { - format: 'esm', + format: 'iife', syntax: 'es2021', dts: false, autoExtension: false, @@ -109,6 +109,10 @@ export default defineConfig({ /src[/\\]loader-runner[/\\]service\.ts/, path.resolve('./src/browser/service.ts'), ), + new rspack.NormalModuleReplacementPlugin( + /src[/\\]builtin-plugin[/\\]lazy-compilation[/\\]middleware\.ts/, + path.resolve('./src/browser/middleware.ts'), + ), ); }, }, diff --git a/packages/rspack/src/browser/middleware.ts b/packages/rspack/src/browser/middleware.ts new file mode 100644 index 000000000000..3d66bf2e1d03 --- /dev/null +++ b/packages/rspack/src/browser/middleware.ts @@ -0,0 +1,3 @@ +export const lazyCompilationMiddleware = () => { + throw new Error('lazy compilation middleware is not supported in browser'); +}; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 97be27569e2b..ba5e48313f72 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -648,6 +648,9 @@ importers: '@playwright/test': specifier: 1.57.0 version: 1.57.0 + '@rspack/browser': + specifier: workspace:* + version: link:../../packages/rspack-browser '@rspack/core': specifier: workspace:* version: link:../../packages/rspack diff --git a/tests/e2e/browser-cases/basic-react/index.test.ts b/tests/e2e/browser-cases/basic-react/index.test.ts new file mode 100644 index 000000000000..3f3283034ae0 --- /dev/null +++ b/tests/e2e/browser-cases/basic-react/index.test.ts @@ -0,0 +1,13 @@ +import { test, expect } from '@/fixtures'; + +test('@rspack/browser should bundle react app successfully', async ({ + page, +}) => { + // There should be a long bundle result + await expect + .poll(async () => { + const text = await page.locator('#output').innerText(); + return text.length; + }) + .toBeGreaterThan(500); +}); diff --git a/tests/e2e/browser-cases/basic-react/rspack.config.js b/tests/e2e/browser-cases/basic-react/rspack.config.js new file mode 100644 index 000000000000..0be1285f81d4 --- /dev/null +++ b/tests/e2e/browser-cases/basic-react/rspack.config.js @@ -0,0 +1,25 @@ +const rspack = require('@rspack/core'); + +/** @type {import("@rspack/core").Configuration} */ +module.exports = { + entry: './src/index.js', + context: __dirname, + mode: 'development', + plugins: [new rspack.HtmlRspackPlugin()], + module: { + rules: [ + { + test: /\.js$/, + exclude: [/node_modules/], + include: [/src/], + loader: 'builtin:swc-loader', + }, + ], + }, + devServer: { + headers: { + 'Cross-Origin-Opener-Policy': 'same-origin', + 'Cross-Origin-Embedder-Policy': 'require-corp', + }, + }, +}; diff --git a/tests/e2e/browser-cases/basic-react/src/files.js b/tests/e2e/browser-cases/basic-react/src/files.js new file mode 100644 index 000000000000..71aaffcdec2f --- /dev/null +++ b/tests/e2e/browser-cases/basic-react/src/files.js @@ -0,0 +1,213 @@ +import { BrowserHttpImportEsmPlugin } from '@rspack/browser'; + +export const files = { + './src/assets/react.svg': ``, + './src/main.jsx': `import React from "react"; +import ReactDOM from "react-dom/client"; +import App from "./App"; +import "./index.css"; + +ReactDOM.createRoot(document.getElementById("root")).render( + + + +); +`, + './src/index.css': `:root { + font-family: Inter, Avenir, Helvetica, Arial, sans-serif; + font-size: 16px; + line-height: 24px; + font-weight: 400; + + color-scheme: light dark; + color: rgba(255, 255, 255, 0.87); + background-color: #242424; + + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + -webkit-text-size-adjust: 100%; +} + +a { + font-weight: 500; + color: #646cff; + text-decoration: inherit; +} +a:hover { + color: #535bf2; +} + +body { + margin: 0; + display: flex; + place-items: center; + min-width: 320px; + min-height: 100vh; +} + +h1 { + font-size: 3.2em; + line-height: 1.1; +} + +button { + border-radius: 8px; + border: 1px solid transparent; + padding: 0.6em 1.2em; + font-size: 1em; + font-weight: 500; + font-family: inherit; + background-color: #1a1a1a; + cursor: pointer; + transition: border-color 0.25s; +} +button:hover { + border-color: #646cff; +} +button:focus, +button:focus-visible { + outline: 4px auto -webkit-focus-ring-color; +} + +@media (prefers-color-scheme: light) { + :root { + color: #213547; + background-color: #ffffff; + } + a:hover { + color: #747bff; + } + button { + background-color: #f9f9f9; + } +} +`, + './src/App.jsx': `import React from "react"; +import { useState } from "react"; +import reactLogo from "./assets/react.svg"; +import "./App.css"; + +function App() { + const [count, setCount] = useState(0); + + return ( +
+
+ + React logo + +
+

Rspack + React

+
+ +

+ Edit src/App.jsx and save to test HMR +

+
+

+ Click on the Rspack and React logos to learn more +

+
+ ); +} + +export default App; +`, + './src/App.css': `#root { + max-width: 1280px; + margin: 0 auto; + padding: 2rem; + text-align: center; +} + +.logo { + height: 6em; + padding: 1.5em; + will-change: filter; +} +.logo:hover { + filter: drop-shadow(0 0 2em #646cffaa); +} +.logo.react:hover { + filter: drop-shadow(0 0 2em #61dafbaa); +} + +@keyframes logo-spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + +@media (prefers-reduced-motion: no-preference) { + a > .logo { + animation: logo-spin infinite 20s linear; + } +} + +.card { + padding: 2em; +} + +.read-the-docs { + color: #888; +} +`, +}; + +export const config = { + mode: 'development', + devtool: false, + entry: './src/main.jsx', + resolve: { + extensions: ['.js', '.jsx', '.json'], + }, + module: { + rules: [ + { + test: /\.jsx$/, + use: { + loader: 'builtin:swc-loader', + options: { + jsc: { + parser: { + syntax: 'ecmascript', + jsx: true, + }, + transform: { + react: { + pragma: 'React.createElement', + pragmaFrag: 'React.Fragment', + throwIfNamespace: true, + development: false, + }, + }, + }, + }, + }, + type: 'javascript/auto', + }, + { + test: /\.(png|svg|jpg)$/, + type: 'asset/resource', + }, + ], + }, + plugins: [ + new BrowserHttpImportEsmPlugin({ + domain: 'https://esm.sh', + }), + ], + experiments: { + css: true, + buildHttp: { + allowedUris: ['https://'], + }, + }, +}; diff --git a/tests/e2e/browser-cases/basic-react/src/index.js b/tests/e2e/browser-cases/basic-react/src/index.js new file mode 100644 index 000000000000..765bc499eb55 --- /dev/null +++ b/tests/e2e/browser-cases/basic-react/src/index.js @@ -0,0 +1,20 @@ +import { rspack, builtinMemFs } from '@rspack/browser'; +import { files, config } from './files'; + +builtinMemFs.volume.fromJSON({ + ...files, +}); + +const promise = new Promise((resolve) => { + rspack(config, () => { + const json = builtinMemFs.volume.toJSON(); + + const outputDOM = document.createElement('div'); + outputDOM.id = 'output'; + outputDOM.innerHTML = json['/dist/main.js']; + document.body.appendChild(outputDOM); + resolve(); + }); +}); + +await promise; diff --git a/tests/e2e/package.json b/tests/e2e/package.json index 69d3b0601d72..cbd3de6a195b 100644 --- a/tests/e2e/package.json +++ b/tests/e2e/package.json @@ -12,6 +12,7 @@ "@playwright/test": "1.57.0", "core-js": "3.47.0", "@rspack/core": "workspace:*", + "@rspack/browser": "workspace:*", "@rspack/dev-server": "~1.1.5", "@rspack/plugin-react-refresh": "^1.6.0", "@swc/helpers": "0.5.18", diff --git a/tests/e2e/playwright.config.ts b/tests/e2e/playwright.config.ts index d3c7b7a9782e..7ba417d1c2ef 100644 --- a/tests/e2e/playwright.config.ts +++ b/tests/e2e/playwright.config.ts @@ -5,7 +5,7 @@ const TIMEOUT = 60 * 1000; export default defineConfig({ // Look for test files in the "fixtures" directory, relative to this configuration file. - testDir: './cases', + testDir: process.env.WASM ? './browser-cases' : './cases', // globalSetup: require.resolve("./scripts/globalSetup"), diff --git a/tests/e2e/tsconfig.json b/tests/e2e/tsconfig.json index f08a76f1ce7f..dea9f7fd0e50 100644 --- a/tests/e2e/tsconfig.json +++ b/tests/e2e/tsconfig.json @@ -1,6 +1,13 @@ { "extends": "../../tsconfig.base.json", - "include": ["custom.d.ts", "./scripts", "./fixtures", "./cases", "./utils"], + "include": [ + "custom.d.ts", + "./scripts", + "./fixtures", + "./cases", + "./browser-cases", + "./utils" + ], "compilerOptions": { "strict": true, "baseUrl": ".",