diff --git a/.changeset/easy-waves-pump.md b/.changeset/easy-waves-pump.md
new file mode 100644
index 0000000000..6b6239c210
--- /dev/null
+++ b/.changeset/easy-waves-pump.md
@@ -0,0 +1,6 @@
+---
+"@lynx-js/react-webpack-plugin": patch
+"@lynx-js/react-rsbuild-plugin": patch
+---
+
+Support rstest for testing library using a dedicated testing loader.
diff --git a/.changeset/ninety-pants-tease.md b/.changeset/ninety-pants-tease.md
new file mode 100644
index 0000000000..644f29a76b
--- /dev/null
+++ b/.changeset/ninety-pants-tease.md
@@ -0,0 +1,5 @@
+---
+"create-rspeedy": patch
+---
+
+Add Rstest ReactLynx Testing Library template.
diff --git a/.changeset/red-lamps-arrive.md b/.changeset/red-lamps-arrive.md
new file mode 100644
index 0000000000..cac3bda3b2
--- /dev/null
+++ b/.changeset/red-lamps-arrive.md
@@ -0,0 +1,29 @@
+---
+"@lynx-js/react": patch
+---
+
+Support rstest for testing library, you can use rstest with RLTL now:
+
+Create a config file `rstest.config.ts` with the following content:
+
+```ts
+import { defineConfig, RstestConfig } from '@rstest/core';
+import lynxConfig from './lynx.config.js';
+
+export default defineConfig({
+ ...lynxConfig as RstestConfig,
+ testEnvironment: 'jsdom',
+ setupFiles: [
+ require.resolve('@lynx-js/react/testing-library/setupFiles/rstest'),
+ ],
+ globals: true,
+});
+```
+
+And then use rstest as usual:
+
+```bash
+$ rstest
+```
+
+For more usage detail, see https://rstest.rs/
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 6cf145009d..bb03185b0c 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -158,6 +158,12 @@ jobs:
pnpm run build --mode development
pnpm run lint
pnpm run test
+ npx --registry http://localhost:4873 create-rspeedy-canary@latest --template react-rstest-rltl --dir create-rspeedy-regression-rstest-rltl
+ cd create-rspeedy-regression-rstest-rltl
+ pnpm install --registry=http://localhost:4873
+ pnpm run build
+ pnpm run build --mode development
+ pnpm run test
test-react:
needs: build
uses: ./.github/workflows/workflow-test.yml
@@ -181,6 +187,23 @@ jobs:
--no-cache
--logHeapUsage
--silent
+ test-rstest:
+ needs: build
+ uses: ./.github/workflows/workflow-test.yml
+ strategy:
+ matrix:
+ runs-on:
+ - name: Ubuntu
+ label: lynx-ubuntu-24.04-medium
+ - name: Windows
+ label: lynx-windows-2022-large
+ name: Rstest (${{ matrix.runs-on.name }})
+ secrets:
+ CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
+ with:
+ runs-on: ${{ matrix.runs-on.label }}
+ run: |
+ pnpm -r run rstest --testTimeout=50000
test-rust:
uses: ./.github/workflows/rust.yml
secrets:
@@ -241,6 +264,7 @@ jobs:
- test-api
- test-publish
- test-react
+ - test-rstest
- test-rust
- test-type
- test-typos
diff --git a/package.json b/package.json
index df47ba5b64..82c5c44ccd 100644
--- a/package.json
+++ b/package.json
@@ -21,6 +21,7 @@
"@rsbuild/core": "catalog:rsbuild",
"@rslib/core": "^0.12.2",
"@rspack/core": "catalog:rspack",
+ "@rstest/core": "catalog:rstest",
"@svitejs/changesets-changelog-github-compact": "^1.2.0",
"@tsconfig/node20": "^20.1.6",
"@tsconfig/strictest": "^2.0.5",
diff --git a/packages/react/package.json b/packages/react/package.json
index 7f58d2a44b..fd9e2840d7 100644
--- a/packages/react/package.json
+++ b/packages/react/package.json
@@ -101,9 +101,10 @@
"default": "./testing-library/dist/pure.js"
},
"./testing-library/vitest-config": {
- "types": "./testing-library/types/vitest-config.d.ts",
+ "types": "./testing-library/dist/vitest.config.d.ts",
"default": "./testing-library/dist/vitest.config.js"
},
+ "./testing-library/setupFiles/rstest": "./testing-library/dist/setupFiles/rstest.js",
"./package.json": "./package.json"
},
"types": "./types/react.d.ts",
diff --git a/packages/react/testing-library/.npmignore b/packages/react/testing-library/.npmignore
index cb433b7a07..07238e1865 100644
--- a/packages/react/testing-library/.npmignore
+++ b/packages/react/testing-library/.npmignore
@@ -1,4 +1,3 @@
*
-!dist/*
-!dist/env/*
+!dist/**/*
!types/*
diff --git a/packages/react/testing-library/package.json b/packages/react/testing-library/package.json
index 25e657b557..1eb3be4976 100644
--- a/packages/react/testing-library/package.json
+++ b/packages/react/testing-library/package.json
@@ -6,12 +6,15 @@
"scripts": {
"build": "rslib build",
"dev": "rslib build --watch",
+ "rstest": "rstest",
"test": "vitest",
"test:ui": "vitest --ui"
},
"devDependencies": {
"@lynx-js/react": "workspace:*",
+ "@lynx-js/react-rsbuild-plugin": "workspace:*",
"@lynx-js/testing-environment": "workspace:*",
+ "@rsbuild/core": "catalog:rsbuild",
"@testing-library/dom": "^10.4.1",
"@testing-library/jest-dom": "^6.7.0"
}
diff --git a/packages/react/testing-library/rslib.config.ts b/packages/react/testing-library/rslib.config.ts
index 07636e9466..34c8fdc1b8 100644
--- a/packages/react/testing-library/rslib.config.ts
+++ b/packages/react/testing-library/rslib.config.ts
@@ -1,16 +1,17 @@
-import { defineConfig, type rsbuild } from '@rslib/core';
+import { defineConfig } from '@rslib/core';
export default defineConfig({
lib: [
{
format: 'esm',
syntax: 'es2022',
- dts: false,
+ dts: true,
bundle: true,
source: {
entry: {
'pure': './src/pure.jsx',
- 'env/vitest': './src/env/vitest.ts',
+ 'env/index': './src/env/index.ts',
+ 'plugins/index': './src/plugins/index.ts',
},
},
output: {
@@ -29,9 +30,13 @@ export default defineConfig({
bundle: false,
source: {
entry: {
- 'index': './src/index.jsx',
- 'vitest.config': './src/vitest.config.js',
- 'vitest-global-setup': './src/vitest-global-setup.js',
+ 'index': [
+ './src/index.jsx',
+ './src/vitest.config.ts',
+ './src/env/vitest.ts',
+ './src/env/rstest.ts',
+ './src/setupFiles/**/*.js',
+ ],
},
},
output: {
diff --git a/packages/react/testing-library/rstest.config.ts b/packages/react/testing-library/rstest.config.ts
new file mode 100644
index 0000000000..863b56ffba
--- /dev/null
+++ b/packages/react/testing-library/rstest.config.ts
@@ -0,0 +1,36 @@
+import { defineConfig } from '@rstest/core';
+import { pluginReactLynx } from '@lynx-js/react-rsbuild-plugin';
+
+export default defineConfig({
+ tools: {
+ swc: {
+ jsc: {
+ transform: {
+ useDefineForClassFields: true,
+ },
+ },
+ },
+ },
+ plugins: [
+ pluginReactLynx(),
+ ],
+ source: {
+ define: {
+ __RSTEST__: 'true',
+ __ALOG__: 'true',
+ },
+ },
+ testEnvironment: 'jsdom',
+ setupFiles: [
+ require.resolve('@lynx-js/react/testing-library/setupFiles/rstest'),
+ ],
+ globals: true,
+ resolve: {
+ // in order to make our test case work for
+ // both vitest and rstest, we need to alias
+ // `vitest` to `@rstest/core`
+ alias: {
+ vitest: require.resolve('./vitest-polyfill.cjs'),
+ },
+ },
+});
diff --git a/packages/react/testing-library/src/__tests__/act.test.jsx b/packages/react/testing-library/src/__tests__/act.test.jsx
index 3b1aab590f..cd049de9cc 100644
--- a/packages/react/testing-library/src/__tests__/act.test.jsx
+++ b/packages/react/testing-library/src/__tests__/act.test.jsx
@@ -201,7 +201,7 @@ test('fireEvent triggers useEffect calls', async () => {
[
"rLynxChange",
{
- "data": "{"patchList":[{"snapshotPatch":[0,"__Card__:__snapshot_e8d0a_test_4",2,4,2,[1],0,null,3,4,3,[0],1,2,3,null,1,-1,2,null],"id":2}]}",
+ "data": "{"patchList":[{"snapshotPatch":[0,"__Card__:__snapshot_268b9_test_4",2,4,2,[1],0,null,3,4,3,[0],1,2,3,null,1,-1,2,null],"id":2}]}",
"patchOptions": {
"isHydration": true,
"pipelineOptions": {
@@ -236,7 +236,7 @@ test('fireEvent triggers useEffect calls', async () => {
],
"extraProps": undefined,
"id": 2,
- "type": "__Card__:__snapshot_e8d0a_test_4",
+ "type": "__Card__:__snapshot_268b9_test_4",
"values": [
"2:0:",
],
@@ -261,7 +261,7 @@ test('fireEvent triggers useEffect calls', async () => {
],
"extraProps": undefined,
"id": 2,
- "type": "__Card__:__snapshot_e8d0a_test_4",
+ "type": "__Card__:__snapshot_268b9_test_4",
"values": [
"2:0:",
],
@@ -285,7 +285,7 @@ test('fireEvent triggers useEffect calls', async () => {
[
"rLynxChange",
{
- "data": "{"patchList":[{"snapshotPatch":[0,"__Card__:__snapshot_e8d0a_test_4",2,4,2,[1],0,null,3,4,3,[0],1,2,3,null,1,-1,2,null],"id":2}]}",
+ "data": "{"patchList":[{"snapshotPatch":[0,"__Card__:__snapshot_268b9_test_4",2,4,2,[1],0,null,3,4,3,[0],1,2,3,null,1,-1,2,null],"id":2}]}",
"patchOptions": {
"isHydration": true,
"pipelineOptions": {
diff --git a/packages/react/testing-library/src/__tests__/alog.test.jsx b/packages/react/testing-library/src/__tests__/alog.test.jsx
index 64f5f08b04..4990aa0575 100644
--- a/packages/react/testing-library/src/__tests__/alog.test.jsx
+++ b/packages/react/testing-library/src/__tests__/alog.test.jsx
@@ -6,8 +6,16 @@ import { act } from 'preact/test-utils';
describe('alog', () => {
test('should log', async () => {
- vi.spyOn(lynxTestingEnv.mainThread.console, 'alog');
- vi.spyOn(lynxTestingEnv.backgroundThread.console, 'alog');
+ const originalAlog = console.alog;
+ let mainThreadALogCalls = [];
+ let backgroundThreadALogCalls = [];
+ console.alog = (...args) => {
+ if (__MAIN_THREAD__) {
+ mainThreadALogCalls.push(args);
+ } else {
+ backgroundThreadALogCalls.push(args);
+ }
+ };
let _setCount;
function App() {
@@ -35,7 +43,7 @@ describe('alog', () => {
enableBackgroundThread: true,
});
- expect(lynxTestingEnv.mainThread.console.alog.mock.calls).toMatchInlineSnapshot(`
+ expect(mainThreadALogCalls).toMatchInlineSnapshot(`
[
[
"[MainThread Component Render] name: ClassComponent",
@@ -48,53 +56,53 @@ describe('alog', () => {
],
]
`);
- expect(lynxTestingEnv.backgroundThread.console.alog.mock.calls).toMatchInlineSnapshot(`
+ expect(backgroundThreadALogCalls).toMatchInlineSnapshot(`
[
[
- "[BackgroundThread Component Render] name: ClassComponent, uniqID: __Card__:__snapshot_426db_test_2, __id: 6",
+ "[BackgroundThread Component Render] name: ClassComponent, uniqID: __Card__:__snapshot_d6fb6_test_2, __id: 6",
],
[
- "[BackgroundThread Component Render] name: FunctionComponent, uniqID: __Card__:__snapshot_426db_test_3, __id: 7",
+ "[BackgroundThread Component Render] name: FunctionComponent, uniqID: __Card__:__snapshot_d6fb6_test_3, __id: 7",
],
[
- "[BackgroundThread Component Render] name: App, uniqID: __Card__:__snapshot_426db_test_1, __id: 2",
+ "[BackgroundThread Component Render] name: App, uniqID: __Card__:__snapshot_d6fb6_test_1, __id: 2",
],
[
- "[BackgroundThread Component Render] name: Fragment, uniqID: __Card__:__snapshot_426db_test_1, __id: 2",
+ "[BackgroundThread Component Render] name: Fragment, uniqID: __Card__:__snapshot_d6fb6_test_1, __id: 2",
],
]
`);
- lynxTestingEnv.mainThread.console.alog.mockClear();
- lynxTestingEnv.backgroundThread.console.alog.mockClear();
+ mainThreadALogCalls = [];
+ backgroundThreadALogCalls = [];
act(() => {
_setCount(0);
});
- expect(lynxTestingEnv.mainThread.console.alog.mock.calls).toMatchInlineSnapshot(`[]`);
- expect(lynxTestingEnv.backgroundThread.console.alog.mock.calls).toMatchInlineSnapshot(`[]`);
-
- lynxTestingEnv.mainThread.console.alog.mockClear();
- lynxTestingEnv.backgroundThread.console.alog.mockClear();
+ mainThreadALogCalls = [];
+ backgroundThreadALogCalls = [];
act(() => {
_setCount(1);
});
- expect(lynxTestingEnv.mainThread.console.alog.mock.calls).toMatchInlineSnapshot(`[]`);
- expect(lynxTestingEnv.backgroundThread.console.alog.mock.calls).toMatchInlineSnapshot(`
+ expect(mainThreadALogCalls).toMatchInlineSnapshot(`[]`);
+ expect(backgroundThreadALogCalls).toMatchInlineSnapshot(`
[
[
- "[BackgroundThread Component Render] name: ClassComponent, uniqID: __Card__:__snapshot_426db_test_2, __id: -5",
+ "[BackgroundThread Component Render] name: ClassComponent, uniqID: __Card__:__snapshot_d6fb6_test_2, __id: -5",
],
[
- "[BackgroundThread Component Render] name: FunctionComponent, uniqID: __Card__:__snapshot_426db_test_3, __id: -6",
+ "[BackgroundThread Component Render] name: FunctionComponent, uniqID: __Card__:__snapshot_d6fb6_test_3, __id: -6",
],
[
- "[BackgroundThread Component Render] name: App, uniqID: __Card__:__snapshot_426db_test_1, __id: -2",
+ "[BackgroundThread Component Render] name: App, uniqID: __Card__:__snapshot_d6fb6_test_1, __id: -2",
],
]
`);
+
+ // Cleanup
+ console.alog = originalAlog;
});
});
diff --git a/packages/react/testing-library/src/__tests__/auto-cleanup-skip.test.jsx b/packages/react/testing-library/src/__tests__/auto-cleanup-skip.test.jsx
index ac369899e4..d729637359 100644
--- a/packages/react/testing-library/src/__tests__/auto-cleanup-skip.test.jsx
+++ b/packages/react/testing-library/src/__tests__/auto-cleanup-skip.test.jsx
@@ -1,3 +1,5 @@
+import { beforeAll, test, expect } from 'vitest';
+
let render;
beforeAll(async () => {
diff --git a/packages/react/testing-library/src/__tests__/cleanup.test.jsx b/packages/react/testing-library/src/__tests__/cleanup.test.jsx
index 00bd089395..ffbebe04ed 100644
--- a/packages/react/testing-library/src/__tests__/cleanup.test.jsx
+++ b/packages/react/testing-library/src/__tests__/cleanup.test.jsx
@@ -1,4 +1,4 @@
-import { expect } from 'vitest';
+import { expect, vi } from 'vitest';
import { render, cleanup } from '..';
import { Component } from '@lynx-js/react';
diff --git a/packages/react/testing-library/src/__tests__/css/index.test.jsx b/packages/react/testing-library/src/__tests__/css/index.test.jsx
index e7e7c7c99b..ba72bea45b 100644
--- a/packages/react/testing-library/src/__tests__/css/index.test.jsx
+++ b/packages/react/testing-library/src/__tests__/css/index.test.jsx
@@ -34,8 +34,8 @@ describe('CSS', () => {
`);
});
it('should render a component with CSS module styles object', () => {
- // Assert stable shape (prefix) rather than exact hash
- expect(style3.baz).toMatch(/^_baz_[a-z0-9]+$/);
+ // to be an string
+ expect(style3.baz).toBeTypeOf('string');
const TestComponent = () => Hello World;
const { container } = render();
diff --git a/packages/react/testing-library/src/__tests__/end-to-end.test.jsx b/packages/react/testing-library/src/__tests__/end-to-end.test.jsx
index d27a95f785..184a7e6e45 100644
--- a/packages/react/testing-library/src/__tests__/end-to-end.test.jsx
+++ b/packages/react/testing-library/src/__tests__/end-to-end.test.jsx
@@ -1,6 +1,6 @@
import '@testing-library/jest-dom';
import { Component } from 'preact';
-import { expect } from 'vitest';
+import { expect, vi } from 'vitest';
import { render, screen, waitForElementToBeRemoved } from '..';
import { snapshotInstanceManager } from '../../../runtime/lib/snapshot.js';
@@ -67,7 +67,7 @@ test('state change will cause re-render', async () => {
"children": undefined,
"extraProps": undefined,
"id": 2,
- "type": "__Card__:__snapshot_354a3_test_1",
+ "type": "__Card__:__snapshot_f46c5_test_1",
"values": undefined,
},
],
@@ -80,7 +80,7 @@ test('state change will cause re-render', async () => {
"children": undefined,
"extraProps": undefined,
"id": 2,
- "type": "__Card__:__snapshot_354a3_test_1",
+ "type": "__Card__:__snapshot_f46c5_test_1",
"values": undefined,
},
}
@@ -100,7 +100,7 @@ test('state change will cause re-render', async () => {
[
"rLynxChange",
{
- "data": "{"patchList":[{"snapshotPatch":[0,"__Card__:__snapshot_354a3_test_1",2,1,-1,2,null],"id":2}]}",
+ "data": "{"patchList":[{"snapshotPatch":[0,"__Card__:__snapshot_f46c5_test_1",2,1,-1,2,null],"id":2}]}",
"patchOptions": {
"isHydration": true,
"pipelineOptions": {
@@ -118,7 +118,7 @@ test('state change will cause re-render', async () => {
[
"rLynxChange",
{
- "data": "{"patchList":[{"id":3,"snapshotPatch":[2,-1,2,0,"__Card__:__snapshot_354a3_test_2",3,0,null,4,3,4,0,"Hello World",1,3,4,null,1,-1,3,null]}]}",
+ "data": "{"patchList":[{"id":3,"snapshotPatch":[2,-1,2,0,"__Card__:__snapshot_f46c5_test_2",3,0,null,4,3,4,0,"Hello World",1,3,4,null,1,-1,3,null]}]}",
"patchOptions": {
"pipelineOptions": {
"dsl": "reactLynx",
diff --git a/packages/react/testing-library/src/__tests__/lazy-bundle/index.test.jsx b/packages/react/testing-library/src/__tests__/lazy-bundle/index.test.jsx
index 1bec1f2b7c..0ac7837925 100644
--- a/packages/react/testing-library/src/__tests__/lazy-bundle/index.test.jsx
+++ b/packages/react/testing-library/src/__tests__/lazy-bundle/index.test.jsx
@@ -1,6 +1,6 @@
import '@testing-library/jest-dom';
import { expect, it } from 'vitest';
-import { render, screen, waitForElementToBeRemoved } from '@lynx-js/react/testing-library';
+import { render, screen, waitForElementToBeRemoved } from '../..';
import { Suspense, lazy } from '@lynx-js/react';
import { createRequire } from 'node:module';
import { describe } from 'node:test';
@@ -14,7 +14,7 @@ function LazyComponentLoader({ url }) {
return (
loading...}>
-
+ {(typeof __RSTEST__ !== 'undefined' && __RSTEST__) ? null : }
);
}
@@ -47,17 +47,29 @@ describe('lazy bundle', () => {
timeout: 50_000,
});
- expect(container.firstChild).toMatchInlineSnapshot(`
-
-
-
- Hello from LazyComponent
-
-
- Hello from LazyComponent
-
-
-
- `);
+ if (typeof __RSTEST__ !== 'undefined' && __RSTEST__) {
+ expect(container.firstChild).toMatchInlineSnapshot(`
+
+
+
+ Hello from LazyComponent
+
+
+
+ `);
+ } else {
+ expect(container.firstChild).toMatchInlineSnapshot(`
+
+
+
+ Hello from LazyComponent
+
+
+ Hello from LazyComponent
+
+
+
+ `);
+ }
});
});
diff --git a/packages/react/testing-library/src/__tests__/list.test.jsx b/packages/react/testing-library/src/__tests__/list.test.jsx
index 34d70b545d..02efb83412 100644
--- a/packages/react/testing-library/src/__tests__/list.test.jsx
+++ b/packages/react/testing-library/src/__tests__/list.test.jsx
@@ -3,7 +3,7 @@
// LICENSE file in the root directory of this source tree.
import { act } from 'preact/test-utils';
-import { describe, expect } from 'vitest';
+import { describe, expect, vi } from 'vitest';
import { Component, useState } from '@lynx-js/react';
@@ -31,7 +31,7 @@ describe('list', () => {
expect(container).toMatchInlineSnapshot(`
`);
@@ -42,7 +42,7 @@ describe('list', () => {
expect(container).toMatchInlineSnapshot(`
{
expect(container).toMatchInlineSnapshot(`
{
expect(container).toMatchInlineSnapshot(`
{
expect(container).toMatchInlineSnapshot(`
{
>
@@ -395,7 +395,95 @@ describe('list', () => {
expect(list).toMatchInlineSnapshot(`
+
+
+
+
+ 4
+
+
+ 4
+
+
+
+
+ hello
+
+
+
+
+
+
+
+
+ 5
+
+
+ 5
+
+
+
+
+ hello
+
+
+
+
+
+
+
+
+ 2
+
+
+ 2
+
+
+
+
+ hello
+
+
+
+
+
+
+
+
+ 1
+
+
+ 1
+
+
+
+
+ hello
+
+
+
+
+
+ `);
+
+ expect(list).toMatchInlineSnapshot(`
+
should render as normal', () => {
`);
@@ -535,7 +623,7 @@ describe('list - deferred should render as normal', () => {
`);
@@ -547,7 +635,7 @@ describe('list - deferred should render as normal', () => {
should render as normal', () => {
`);
@@ -716,7 +804,7 @@ describe('list - deferred should render as normal', () => {
`);
@@ -736,7 +824,7 @@ describe('list - deferred should render as normal', () => {
`);
@@ -755,7 +843,7 @@ describe('list - deferred should render as normal', () => {
{
it('getJSModule should work', () => {
diff --git a/packages/react/testing-library/src/__tests__/rerender.test.jsx b/packages/react/testing-library/src/__tests__/rerender.test.jsx
index 753b990ff0..049acee533 100644
--- a/packages/react/testing-library/src/__tests__/rerender.test.jsx
+++ b/packages/react/testing-library/src/__tests__/rerender.test.jsx
@@ -1,6 +1,6 @@
import '@testing-library/jest-dom';
import { render } from '..';
-import { expect } from 'vitest';
+import { expect, vi } from 'vitest';
import { useEffect, useState } from '@lynx-js/react';
test('rerender will re-render the element', async () => {
diff --git a/packages/react/testing-library/src/__tests__/worklet.test.jsx b/packages/react/testing-library/src/__tests__/worklet.test.jsx
index 23400559e7..de779325a8 100644
--- a/packages/react/testing-library/src/__tests__/worklet.test.jsx
+++ b/packages/react/testing-library/src/__tests__/worklet.test.jsx
@@ -77,7 +77,7 @@ describe('worklet', () => {
[
"rLynxChange",
{
- "data": "{"patchList":[{"snapshotPatch":[3,-2,0,{"_wkltId":"a45f:test:2","_workletType":"main-thread","_execId":1}],"id":2}]}",
+ "data": "{"patchList":[{"snapshotPatch":[3,-2,0,{"_wkltId":"15ab:test:2","_workletType":"main-thread","_execId":1}],"id":2}]}",
"patchOptions": {
"isHydration": true,
"pipelineOptions": {
@@ -159,7 +159,7 @@ describe('worklet', () => {
[
"rLynxChange",
{
- "data": "{"patchList":[{"snapshotPatch":[3,-2,1,{"_c":{"props":{"main-thread:onClick":{"_wkltId":"a45f:test:3"}}},"_wkltId":"a45f:test:4","_execId":1}],"id":2}]}",
+ "data": "{"patchList":[{"snapshotPatch":[3,-2,1,{"_c":{"props":{"main-thread:onClick":{"_wkltId":"15ab:test:3"}}},"_wkltId":"15ab:test:4","_execId":1}],"id":2}]}",
"patchOptions": {
"isHydration": true,
"pipelineOptions": {
@@ -249,7 +249,7 @@ describe('worklet', () => {
[
"rLynxChange",
{
- "data": "{"patchList":[{"snapshotPatch":[3,-2,0,{"_c":{"props":{"main-thread:onScroll":{"_wkltId":"a45f:test:5"}}},"_wkltId":"a45f:test:6","_execId":1}],"id":2}]}",
+ "data": "{"patchList":[{"snapshotPatch":[3,-2,0,{"_c":{"props":{"main-thread:onScroll":{"_wkltId":"15ab:test:5"}}},"_wkltId":"15ab:test:6","_execId":1}],"id":2}]}",
"patchOptions": {
"isHydration": true,
"pipelineOptions": {
@@ -343,7 +343,7 @@ describe('worklet', () => {
[
"rLynxChange",
{
- "data": "{"patchList":[{"snapshotPatch":[3,-2,0,{"_wkltId":"a45f:test:8","_jsFn":{"_jsFn1":{"_jsFnId":2,"_fn":"[BackgroundFunction]"}},"_execId":1}],"id":2}]}",
+ "data": "{"patchList":[{"snapshotPatch":[3,-2,0,{"_wkltId":"15ab:test:8","_jsFn":{"_jsFn1":{"_jsFnId":2,"_fn":"[BackgroundFunction]"}},"_execId":1}],"id":2}]}",
"patchOptions": {
"isHydration": true,
"pipelineOptions": {
@@ -458,7 +458,7 @@ describe('worklet', () => {
[
"rLynxChange",
{
- "data": "{"patchList":[{"snapshotPatch":[3,-2,0,{"_wvid":1},3,-2,1,{"_c":{"ref":{"_wvid":1},"num":{"_wvid":2}},"_wkltId":"a45f:test:9","_execId":1}],"id":2}]}",
+ "data": "{"patchList":[{"snapshotPatch":[3,-2,0,{"_wvid":1},3,-2,1,{"_c":{"ref":{"_wvid":1},"num":{"_wvid":2}},"_wkltId":"15ab:test:9","_execId":1}],"id":2}]}",
"patchOptions": {
"isHydration": true,
"pipelineOptions": {
diff --git a/packages/react/testing-library/src/env/index.ts b/packages/react/testing-library/src/env/index.ts
new file mode 100644
index 0000000000..672db9f107
--- /dev/null
+++ b/packages/react/testing-library/src/env/index.ts
@@ -0,0 +1 @@
+export { LynxTestingEnv } from '@lynx-js/testing-environment';
diff --git a/packages/react/testing-library/src/env/rstest.ts b/packages/react/testing-library/src/env/rstest.ts
new file mode 100644
index 0000000000..d4b2359730
--- /dev/null
+++ b/packages/react/testing-library/src/env/rstest.ts
@@ -0,0 +1,8 @@
+import { LynxTestingEnv } from './index.js';
+
+// @ts-ignore
+global.jsdom = {
+ window,
+};
+const lynxTestingEnv = new LynxTestingEnv();
+global.lynxTestingEnv = lynxTestingEnv;
diff --git a/packages/react/testing-library/src/env/vitest.ts b/packages/react/testing-library/src/env/vitest.ts
index cf89eb9fdb..47da348ef0 100644
--- a/packages/react/testing-library/src/env/vitest.ts
+++ b/packages/react/testing-library/src/env/vitest.ts
@@ -1,3 +1,26 @@
-import env from '@lynx-js/testing-environment/env/vitest';
+import { builtinEnvironments, type Environment } from 'vitest/environments';
+import { LynxTestingEnv } from './index.js';
+
+const env: Environment = {
+ name: 'lynxTestingEnv',
+ transformMode: 'web',
+ async setup(global) {
+ const fakeGlobal: {
+ jsdom?: any;
+ } = {};
+ await builtinEnvironments.jsdom.setup(fakeGlobal, {});
+ global.jsdom = fakeGlobal.jsdom;
+
+ const lynxTestingEnv = new LynxTestingEnv();
+ global.lynxTestingEnv = lynxTestingEnv;
+
+ return {
+ teardown(global) {
+ delete global.lynxTestingEnv;
+ delete global.jsdom;
+ },
+ };
+ },
+};
export default env;
diff --git a/packages/react/testing-library/src/plugins/index.ts b/packages/react/testing-library/src/plugins/index.ts
new file mode 100644
index 0000000000..d42531e979
--- /dev/null
+++ b/packages/react/testing-library/src/plugins/index.ts
@@ -0,0 +1 @@
+export { testingLibraryPlugin as vitestTestingLibraryPlugin } from './vitest.js';
diff --git a/packages/react/testing-library/src/plugins/vitest.ts b/packages/react/testing-library/src/plugins/vitest.ts
new file mode 100644
index 0000000000..da1cef7660
--- /dev/null
+++ b/packages/react/testing-library/src/plugins/vitest.ts
@@ -0,0 +1,178 @@
+// Copyright 2025 The Lynx Authors. All rights reserved.
+// Licensed under the Apache License Version 2.0 that can be found in the
+// LICENSE file in the root directory of this source tree.
+import type { ResolvedConfig, Vite } from 'vitest/node';
+import { VitestPackageInstaller } from 'vitest/node';
+import path from 'node:path';
+import { fileURLToPath } from 'node:url';
+import { createRequire } from 'node:module';
+
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = path.dirname(__filename);
+const require = createRequire(import.meta.url);
+
+export interface TestingLibraryOptions {
+ /**
+ * The package name of the ReactLynx runtime package.
+ *
+ * @default `@lynx-js/react`
+ */
+ runtimePkgName?: string;
+}
+
+export function testingLibraryPlugin(
+ options?: TestingLibraryOptions,
+): Vite.Plugin {
+ const runtimeOSSPkgName = '@lynx-js/react';
+ const runtimePkgName = options?.runtimePkgName ?? runtimeOSSPkgName;
+ const runtimeDir = path.dirname(
+ require.resolve(`${runtimePkgName}/package.json`),
+ );
+ const runtimeOSSDir = path.dirname(
+ require.resolve(`${runtimeOSSPkgName}/package.json`, {
+ paths: [runtimeDir],
+ }),
+ );
+
+ const preactDir = path.dirname(
+ require.resolve('preact/package.json', {
+ paths: [runtimeOSSDir],
+ }),
+ );
+
+ const runtimeOSSAlias = generateAlias(
+ runtimeOSSPkgName,
+ runtimeOSSDir,
+ runtimeDir,
+ );
+ let runtimeAlias: Vite.Alias[] = [];
+ if (runtimePkgName !== runtimeOSSPkgName) {
+ runtimeAlias = generateAlias(runtimePkgName, runtimeDir, __dirname);
+ }
+ const preactAlias = generateAlias('preact', preactDir, runtimeOSSDir);
+
+ let config: ResolvedConfig;
+
+ return {
+ name: 'transformReactLynxPlugin',
+ enforce: 'pre',
+ async buildStart() {
+ await ensurePackagesInstalled();
+ },
+ transform(sourceText, sourcePath) {
+ const id = sourcePath;
+ // Only transform JS files
+ // Using the same regex as rspack's `CHAIN_ID.RULE.JS` rule
+ const regex = /\.(?:js|jsx|mjs|cjs|ts|tsx|mts|cts)(\?.*)?$/;
+ if (!regex.test(id)) return null;
+
+ const { transformReactLynxSync } = require(
+ '@lynx-js/react/transform',
+ ) as typeof import('@lynx-js/react/transform');
+ // relativePath should be stable between different runs with different cwd
+ const relativePath = normalizeSlashes(
+ path.relative(config.root, sourcePath),
+ );
+ const basename = path.basename(sourcePath);
+ const result = transformReactLynxSync(sourceText, {
+ mode: 'test',
+ pluginName: '',
+ filename: basename,
+ sourcemap: true,
+ snapshot: {
+ preserveJsx: false,
+ runtimePkg: `${runtimePkgName}/internal`,
+ jsxImportSource: runtimePkgName,
+ filename: relativePath,
+ target: 'MIXED',
+ },
+ // snapshot: true,
+ directiveDCE: false,
+ defineDCE: false,
+ shake: false,
+ compat: false,
+ worklet: {
+ filename: relativePath,
+ runtimePkg: `${runtimePkgName}/internal`,
+ target: 'MIXED',
+ },
+ refresh: false,
+ cssScope: false,
+ });
+
+ if (result.errors.length > 0) {
+ // https://rollupjs.org/plugin-development/#this-error
+ result.errors.forEach((error) => {
+ this.error(error.text ?? 'Unknown error', {
+ line: 1,
+ column: 1,
+ ...error.location,
+ });
+ });
+ }
+ if (result.warnings.length > 0) {
+ result.warnings.forEach((warning) => {
+ this.warn(warning.text ?? 'Unknown warning', {
+ line: 1,
+ column: 1,
+ ...warning.location,
+ });
+ });
+ }
+
+ return {
+ code: result.code,
+ map: result.map!,
+ };
+ },
+ configResolved(_config) {
+ // @ts-ignore
+ config = _config;
+ },
+ config: () => ({
+ test: {
+ environment: require.resolve(
+ `${runtimeOSSDir}/testing-library/dist/env/vitest`,
+ ),
+ globals: true,
+ setupFiles: [
+ require.resolve('../setupFiles/vitest'),
+ ],
+ alias: [...runtimeOSSAlias, ...runtimeAlias, ...preactAlias],
+ },
+ }),
+ };
+}
+
+async function ensurePackagesInstalled() {
+ const installer = new VitestPackageInstaller();
+ const installed = await installer.ensureInstalled('jsdom', process.cwd());
+ if (!installed) {
+ console.log('ReactLynx Testing Library requires jsdom to be installed.');
+ process.exit(1);
+ }
+}
+
+function generateAlias(pkgName: string, pkgDir: string, resolveDir: string) {
+ const pkgExports = require(path.join(pkgDir, 'package.json')).exports;
+ if (!pkgExports || typeof pkgExports !== 'object') {
+ return [];
+ }
+ const pkgAlias: Vite.Alias[] = [];
+ Object.keys(pkgExports).forEach((key) => {
+ const name = path.posix.join(pkgName, key);
+ // Escape special regex characters in the package name
+ const escapedName = name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
+ pkgAlias.push({
+ find: new RegExp('^' + escapedName + '$'),
+ replacement: require.resolve(name, {
+ paths: [resolveDir],
+ }),
+ });
+ });
+ return pkgAlias;
+}
+
+function normalizeSlashes(file: string) {
+ return file.replaceAll(path.win32.sep, '/');
+}
diff --git a/packages/react/testing-library/src/setupFiles/common/bootstrap.js b/packages/react/testing-library/src/setupFiles/common/bootstrap.js
new file mode 100644
index 0000000000..f85071e11f
--- /dev/null
+++ b/packages/react/testing-library/src/setupFiles/common/bootstrap.js
@@ -0,0 +1,6 @@
+globalThis.onInjectMainThreadGlobals(
+ globalThis.lynxTestingEnv.mainThread.globalThis,
+);
+globalThis.onInjectBackgroundThreadGlobals(
+ globalThis.lynxTestingEnv.backgroundThread.globalThis,
+);
diff --git a/packages/react/testing-library/src/vitest-global-setup.js b/packages/react/testing-library/src/setupFiles/common/runtime-setup.js
similarity index 73%
rename from packages/react/testing-library/src/vitest-global-setup.js
rename to packages/react/testing-library/src/setupFiles/common/runtime-setup.js
index 0e1a451f53..8b520906aa 100644
--- a/packages/react/testing-library/src/vitest-global-setup.js
+++ b/packages/react/testing-library/src/setupFiles/common/runtime-setup.js
@@ -1,21 +1,21 @@
import { options } from 'preact';
-import { BackgroundSnapshotInstance } from '../../runtime/lib/backgroundSnapshot.js';
-import { clearCommitTaskId, replaceCommitHook } from '../../runtime/lib/lifecycle/patch/commit.js';
-import { deinitGlobalSnapshotPatch } from '../../runtime/lib/lifecycle/patch/snapshotPatch.js';
-import { injectUpdateMainThread } from '../../runtime/lib/lifecycle/patch/updateMainThread.js';
-import { injectCalledByNative } from '../../runtime/lib/lynx/calledByNative.js';
-import { flushDelayedLifecycleEvents, injectTt } from '../../runtime/lib/lynx/tt.js';
-import { setRoot } from '../../runtime/lib/root.js';
+import { BackgroundSnapshotInstance } from '../../../../runtime/lib/backgroundSnapshot.js';
+import { clearCommitTaskId, replaceCommitHook } from '../../../../runtime/lib/lifecycle/patch/commit.js';
+import { deinitGlobalSnapshotPatch } from '../../../../runtime/lib/lifecycle/patch/snapshotPatch.js';
+import { injectUpdateMainThread } from '../../../../runtime/lib/lifecycle/patch/updateMainThread.js';
+import { injectCalledByNative } from '../../../../runtime/lib/lynx/calledByNative.js';
+import { flushDelayedLifecycleEvents, injectTt } from '../../../../runtime/lib/lynx/tt.js';
+import { setRoot } from '../../../../runtime/lib/root.js';
import {
SnapshotInstance,
backgroundSnapshotInstanceManager,
snapshotInstanceManager,
-} from '../../runtime/lib/snapshot.js';
-import { destroyWorklet } from '../../runtime/lib/worklet/destroy.js';
-import { initApiEnv } from '../../worklet-runtime/lib/api/lynxApi.js';
-import { initEventListeners } from '../../worklet-runtime/lib/listeners.js';
-import { initWorklet } from '../../worklet-runtime/lib/workletRuntime.js';
+} from '../../../../runtime/lib/snapshot.js';
+import { destroyWorklet } from '../../../../runtime/lib/worklet/destroy.js';
+import { initApiEnv } from '../../../../worklet-runtime/lib/api/lynxApi.js';
+import { initEventListeners } from '../../../../worklet-runtime/lib/listeners.js';
+import { initWorklet } from '../../../../worklet-runtime/lib/workletRuntime.js';
const {
onInjectMainThreadGlobals,
@@ -120,14 +120,6 @@ globalThis.onInjectBackgroundThreadGlobals = (target) => {
injectTt();
globalThis.lynxCoreInject = oldLynxCoreInject;
- target.lynx.requireModuleAsync = async (url, callback) => {
- try {
- callback(null, await __vite_ssr_dynamic_import__(url));
- } catch (err) {
- callback(err, null);
- }
- };
-
// re-init global snapshot patch to undefined
deinitGlobalSnapshotPatch();
clearCommitTaskId();
@@ -161,10 +153,3 @@ globalThis.onSwitchedToBackgroundThread = () => {
setRoot(globalThis.__root);
options.document = globalThis._document;
};
-
-globalThis.onInjectMainThreadGlobals(
- globalThis.lynxTestingEnv.mainThread.globalThis,
-);
-globalThis.onInjectBackgroundThreadGlobals(
- globalThis.lynxTestingEnv.backgroundThread.globalThis,
-);
diff --git a/packages/react/testing-library/src/setupFiles/inner/rstest.js b/packages/react/testing-library/src/setupFiles/inner/rstest.js
new file mode 100644
index 0000000000..6f8c194217
--- /dev/null
+++ b/packages/react/testing-library/src/setupFiles/inner/rstest.js
@@ -0,0 +1,21 @@
+import { LynxTestingEnv } from '../../env';
+
+global.jsdom = {
+ window,
+};
+const lynxTestingEnv = new LynxTestingEnv();
+global.lynxTestingEnv = lynxTestingEnv;
+
+const {
+ onInjectBackgroundThreadGlobals,
+} = globalThis;
+
+globalThis.onInjectBackgroundThreadGlobals = (target) => {
+ if (onInjectBackgroundThreadGlobals) {
+ onInjectBackgroundThreadGlobals(target);
+ }
+
+ target.lynx.requireModuleAsync = async (url, callback) => {
+ throw new Error('lynx.requireModuleAsync not implemented for rstest');
+ };
+};
diff --git a/packages/react/testing-library/src/setupFiles/inner/vitest.js b/packages/react/testing-library/src/setupFiles/inner/vitest.js
new file mode 100644
index 0000000000..b8421fa88f
--- /dev/null
+++ b/packages/react/testing-library/src/setupFiles/inner/vitest.js
@@ -0,0 +1,17 @@
+const {
+ onInjectBackgroundThreadGlobals,
+} = globalThis;
+
+globalThis.onInjectBackgroundThreadGlobals = (target) => {
+ if (onInjectBackgroundThreadGlobals) {
+ onInjectBackgroundThreadGlobals(target);
+ }
+
+ target.lynx.requireModuleAsync = async (url, callback) => {
+ try {
+ callback(null, await __vite_ssr_dynamic_import__(url));
+ } catch (err) {
+ callback(err, null);
+ }
+ };
+};
diff --git a/packages/react/testing-library/src/setupFiles/rstest.js b/packages/react/testing-library/src/setupFiles/rstest.js
new file mode 100644
index 0000000000..f938cfa636
--- /dev/null
+++ b/packages/react/testing-library/src/setupFiles/rstest.js
@@ -0,0 +1,4 @@
+import '../env/rstest.js';
+import './common/runtime-setup.js';
+import './inner/rstest.js';
+import './common/bootstrap.js';
diff --git a/packages/react/testing-library/src/setupFiles/vitest.js b/packages/react/testing-library/src/setupFiles/vitest.js
new file mode 100644
index 0000000000..a1080d35a5
--- /dev/null
+++ b/packages/react/testing-library/src/setupFiles/vitest.js
@@ -0,0 +1,3 @@
+import './common/runtime-setup.js';
+import './inner/vitest.js';
+import './common/bootstrap.js';
diff --git a/packages/react/testing-library/src/vitest.config.js b/packages/react/testing-library/src/vitest.config.js
deleted file mode 100644
index 1f17c49000..0000000000
--- a/packages/react/testing-library/src/vitest.config.js
+++ /dev/null
@@ -1,160 +0,0 @@
-import { defineConfig } from 'vitest/config';
-import { VitestPackageInstaller } from 'vitest/node';
-import path from 'path';
-import { fileURLToPath } from 'url';
-import { createRequire } from 'module';
-
-const __filename = fileURLToPath(import.meta.url);
-const __dirname = path.dirname(__filename);
-const require = createRequire(import.meta.url);
-
-async function ensurePackagesInstalled() {
- const installer = new VitestPackageInstaller();
- const installed = await installer.ensureInstalled(
- 'jsdom',
- process.cwd(),
- );
- if (!installed) {
- console.log('ReactLynx Testing Library requires jsdom to be installed.');
- process.exit(1);
- }
-}
-
-/**
- * @returns {import('vitest/config').ViteUserConfig}
- */
-export const createVitestConfig = async (options) => {
- await ensurePackagesInstalled();
-
- const runtimeOSSPkgName = '@lynx-js/react';
- const runtimePkgName = options?.runtimePkgName ?? runtimeOSSPkgName;
- const runtimeDir = path.dirname(require.resolve(`${runtimePkgName}/package.json`));
- const runtimeOSSDir = path.dirname(
- require.resolve(`${runtimeOSSPkgName}/package.json`, {
- paths: [runtimeDir],
- }),
- );
- const preactDir = path.dirname(
- require.resolve('preact/package.json', {
- paths: [runtimeOSSDir],
- }),
- );
-
- const generateAlias = (pkgName, pkgDir, resolveDir) => {
- const pkgExports = require(path.join(pkgDir, 'package.json')).exports;
- const pkgAlias = [];
- Object.keys(pkgExports).forEach((key) => {
- const name = path.posix.join(pkgName, key);
- pkgAlias.push({
- find: new RegExp('^' + name + '$'),
- replacement: require.resolve(name, {
- paths: [resolveDir],
- }),
- });
- });
- return pkgAlias;
- };
-
- const runtimeOSSAlias = generateAlias(runtimeOSSPkgName, runtimeOSSDir, runtimeDir);
- let runtimeAlias = [];
- if (runtimePkgName !== runtimeOSSPkgName) {
- runtimeAlias = generateAlias(runtimePkgName, runtimeDir, __dirname);
- }
- const preactAlias = generateAlias('preact', preactDir, runtimeOSSDir);
-
- function transformReactLynxPlugin() {
- return {
- name: 'transformReactLynxPlugin',
- enforce: 'pre',
- transform(sourceText, sourcePath) {
- const id = sourcePath;
- // Only transform JS files
- // Using the same regex as rspack's `CHAIN_ID.RULE.JS` rule
- const regex = /\.(?:js|jsx|mjs|cjs|ts|tsx|mts|cts)(\?.*)?$/;
- if (!regex.test(id)) return null;
-
- const { transformReactLynxSync } = require(
- '@lynx-js/react/transform',
- );
- // relativePath should be stable between different runs with different cwd
- const relativePath = normalizeSlashes(path.relative(
- __dirname,
- sourcePath,
- ));
- const basename = path.basename(sourcePath);
- const result = transformReactLynxSync(sourceText, {
- mode: 'test',
- pluginName: '',
- filename: basename,
- sourcemap: true,
- snapshot: {
- preserveJsx: false,
- runtimePkg: `${runtimePkgName}/internal`,
- jsxImportSource: runtimePkgName,
- filename: relativePath,
- target: 'MIXED',
- },
- // snapshot: true,
- directiveDCE: false,
- defineDCE: false,
- shake: false,
- compat: false,
- worklet: {
- filename: relativePath,
- runtimePkg: `${runtimePkgName}/internal`,
- target: 'MIXED',
- },
- refresh: false,
- cssScope: false,
- });
- if (result.errors.length > 0) {
- // https://rollupjs.org/plugin-development/#this-error
- result.errors.forEach(error => {
- this.error(
- error.text,
- error.location,
- );
- });
- }
- if (result.warnings.length > 0) {
- result.warnings.forEach(warning => {
- this.warn(
- warning.text,
- warning.location,
- );
- });
- }
-
- return {
- code: result.code,
- map: result.map,
- };
- },
- };
- }
-
- return defineConfig({
- server: {
- fs: {
- allow: [
- path.join(__dirname, '..'),
- ],
- },
- },
- plugins: [
- transformReactLynxPlugin(),
- ],
- test: {
- environment: require.resolve(
- './env/vitest',
- ),
- globals: true,
- setupFiles: [path.join(__dirname, 'vitest-global-setup')],
- alias: [...runtimeOSSAlias, ...runtimeAlias, ...preactAlias],
- },
- });
-};
-
-function normalizeSlashes(file) {
- return file.replaceAll(path.win32.sep, '/');
-}
diff --git a/packages/react/testing-library/src/vitest.config.ts b/packages/react/testing-library/src/vitest.config.ts
new file mode 100644
index 0000000000..afa39105de
--- /dev/null
+++ b/packages/react/testing-library/src/vitest.config.ts
@@ -0,0 +1,11 @@
+import { defineConfig, type ViteUserConfig } from 'vitest/config';
+import { vitestTestingLibraryPlugin } from './plugins/index.js';
+import type { TestingLibraryOptions } from './plugins/vitest.js';
+
+export function createVitestConfig(options?: TestingLibraryOptions): ViteUserConfig {
+ return defineConfig({
+ plugins: [
+ vitestTestingLibraryPlugin(options),
+ ],
+ });
+}
diff --git a/packages/react/testing-library/types/vitest-config.d.ts b/packages/react/testing-library/types/vitest-config.d.ts
deleted file mode 100644
index 472c2f22c7..0000000000
--- a/packages/react/testing-library/types/vitest-config.d.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-import type { ViteUserConfig } from 'vitest/config.js';
-
-export interface CreateVitestConfigOptions {
- /**
- * The package name of the ReactLynx runtime package.
- *
- * @default `@lynx-js/react`
- */
- runtimePkgName?: string;
-}
-
-export function createVitestConfig(options?: CreateVitestConfigOptions): Promise;
diff --git a/packages/react/testing-library/vitest-polyfill.cjs b/packages/react/testing-library/vitest-polyfill.cjs
new file mode 100644
index 0000000000..647f2e4a8d
--- /dev/null
+++ b/packages/react/testing-library/vitest-polyfill.cjs
@@ -0,0 +1,6 @@
+// in order to make our test case work for
+// both vitest and rstest, we need to alias
+// `vitest` to `@rstest/core`
+
+global['@rstest/core'].vi = global['@rstest/core'].rs;
+module.exports = global['@rstest/core'];
diff --git a/packages/rspeedy/create-rspeedy/package.json b/packages/rspeedy/create-rspeedy/package.json
index 17440b1fc6..bca8243ffc 100644
--- a/packages/rspeedy/create-rspeedy/package.json
+++ b/packages/rspeedy/create-rspeedy/package.json
@@ -42,7 +42,8 @@
"@lynx-js/react": "workspace:^",
"@lynx-js/react-rsbuild-plugin": "workspace:^",
"@lynx-js/rspeedy": "workspace:^",
- "@rsbuild/plugin-type-check": "1.2.4"
+ "@rsbuild/plugin-type-check": "1.2.4",
+ "@rstest/core": "catalog:rstest"
},
"engines": {
"node": ">=18"
diff --git a/packages/rspeedy/create-rspeedy/src/index.ts b/packages/rspeedy/create-rspeedy/src/index.ts
index e96f4d98c5..2091d22f36 100644
--- a/packages/rspeedy/create-rspeedy/src/index.ts
+++ b/packages/rspeedy/create-rspeedy/src/index.ts
@@ -8,7 +8,7 @@ import path from 'node:path'
import { fileURLToPath } from 'node:url'
import type { Argv } from 'create-rstack'
-import { checkCancel, create, multiselect, select } from 'create-rstack'
+import { checkCancel, create, select } from 'create-rstack'
type LANG = 'js' | 'ts'
@@ -67,22 +67,31 @@ async function getTemplateName({ template }: Argv) {
}),
)
- const tools = checkCancel(
- await multiselect({
- message:
- 'Select development tools (Use to select, to continue)',
- required: false,
+ const tools = []
+ const testingTools = checkCancel(
+ await select({
+ message: 'Select testing framework',
options: [
{
value: 'vitest-rltl',
- label: 'Add ReactLynx Testing Library for unit testing',
+ label: 'Vitest',
+ },
+ {
+ value: 'rstest-rltl',
+ label: 'Rstest',
+ hint: 'recommended',
+ },
+ {
+ value: '',
+ label: 'Skip',
},
],
- initialValues: [
- 'vitest-rltl',
- ],
+ initialValue: 'vitest-rltl',
}),
)
+ if (testingTools) {
+ tools.push(testingTools)
+ }
return composeTemplateName({
template: 'react',
diff --git a/packages/rspeedy/create-rspeedy/template-react-rstest-rltl-js/lynx.config.js b/packages/rspeedy/create-rspeedy/template-react-rstest-rltl-js/lynx.config.js
new file mode 100644
index 0000000000..17d2cecc5c
--- /dev/null
+++ b/packages/rspeedy/create-rspeedy/template-react-rstest-rltl-js/lynx.config.js
@@ -0,0 +1,19 @@
+import { defineConfig } from '@lynx-js/rspeedy'
+
+import { pluginQRCode } from '@lynx-js/qrcode-rsbuild-plugin'
+import { pluginReactLynx } from '@lynx-js/react-rsbuild-plugin'
+
+export default defineConfig({
+ source: {
+ entry: './src/index.jsx',
+ },
+ plugins: [
+ pluginQRCode({
+ schema(url) {
+ // We use `?fullscreen=true` to open the page in LynxExplorer in full screen mode
+ return `${url}?fullscreen=true`
+ },
+ }),
+ pluginReactLynx(),
+ ],
+})
diff --git a/packages/rspeedy/create-rspeedy/template-react-rstest-rltl-js/package.json b/packages/rspeedy/create-rspeedy/template-react-rstest-rltl-js/package.json
new file mode 100644
index 0000000000..4a1c98b1b7
--- /dev/null
+++ b/packages/rspeedy/create-rspeedy/template-react-rstest-rltl-js/package.json
@@ -0,0 +1,25 @@
+{
+ "name": "rspeedy-react-js",
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "build": "rspeedy build",
+ "dev": "rspeedy dev",
+ "preview": "rspeedy preview",
+ "test": "rstest run"
+ },
+ "dependencies": {
+ "@lynx-js/react": "workspace:*"
+ },
+ "devDependencies": {
+ "@lynx-js/qrcode-rsbuild-plugin": "workspace:*",
+ "@lynx-js/react-rsbuild-plugin": "workspace:*",
+ "@lynx-js/rspeedy": "workspace:*",
+ "@rstest/core": "catalog:rstest",
+ "@testing-library/jest-dom": "^6.6.3",
+ "jsdom": "^26.1.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+}
diff --git a/packages/rspeedy/create-rspeedy/template-react-rstest-rltl-js/rstest.config.js b/packages/rspeedy/create-rspeedy/template-react-rstest-rltl-js/rstest.config.js
new file mode 100644
index 0000000000..ec22dfed14
--- /dev/null
+++ b/packages/rspeedy/create-rspeedy/template-react-rstest-rltl-js/rstest.config.js
@@ -0,0 +1,11 @@
+import { defineConfig } from '@rstest/core'
+import lynxConfig from './lynx.config.js'
+
+export default defineConfig({
+ ...lynxConfig,
+ testEnvironment: 'jsdom',
+ setupFiles: [
+ require.resolve('@lynx-js/react/testing-library/setupFiles/rstest'),
+ ],
+ globals: true,
+})
diff --git a/packages/rspeedy/create-rspeedy/template-react-rstest-rltl-js/src/App.jsx b/packages/rspeedy/create-rspeedy/template-react-rstest-rltl-js/src/App.jsx
new file mode 100644
index 0000000000..7618894b24
--- /dev/null
+++ b/packages/rspeedy/create-rspeedy/template-react-rstest-rltl-js/src/App.jsx
@@ -0,0 +1,53 @@
+import { useCallback, useEffect, useState } from '@lynx-js/react'
+
+import './App.css'
+import arrow from './assets/arrow.png'
+import lynxLogo from './assets/lynx-logo.png'
+import reactLynxLogo from './assets/react-logo.png'
+
+export function App(props) {
+ const [alterLogo, setAlterLogo] = useState(false)
+
+ useEffect(() => {
+ console.info('Hello, ReactLynx')
+ props.onMounted?.()
+ }, [])
+
+ const onTap = useCallback(() => {
+ 'background only'
+ setAlterLogo(!alterLogo)
+ }, [alterLogo])
+
+ return (
+
+
+
+
+
+ {alterLogo
+ ?
+ : }
+
+ React
+ on Lynx
+
+
+
+ Tap the logo and have fun!
+
+ Edit
+ {' src/App.tsx '}
+
+ to see updates!
+
+
+
+
+
+ )
+}
diff --git a/packages/rspeedy/create-rspeedy/template-react-rstest-rltl-js/src/__tests__/index.test.jsx b/packages/rspeedy/create-rspeedy/template-react-rstest-rltl-js/src/__tests__/index.test.jsx
new file mode 100644
index 0000000000..e7f2736b0b
--- /dev/null
+++ b/packages/rspeedy/create-rspeedy/template-react-rstest-rltl-js/src/__tests__/index.test.jsx
@@ -0,0 +1,102 @@
+// Copyright 2024 The Lynx Authors. All rights reserved.
+// Licensed under the Apache License Version 2.0 that can be found in the
+// LICENSE file in the root directory of this source tree.
+import '@testing-library/jest-dom'
+import { expect, test, rstest } from '@rstest/core'
+import { render, getQueriesForElement } from '@lynx-js/react/testing-library'
+
+import { App } from '../App.jsx'
+
+test('App', async () => {
+ const cb = rstest.fn()
+
+ render(
+ {
+ cb(`__MAIN_THREAD__: ${__MAIN_THREAD__}`)
+ }}
+ />,
+ )
+ expect(cb).toBeCalledTimes(1)
+ expect(cb.mock.calls).toMatchInlineSnapshot(`
+ [
+ [
+ "__MAIN_THREAD__: false",
+ ],
+ ]
+ `)
+ expect(elementTree.root).toMatchInlineSnapshot(`
+
+
+
+
+
+
+
+
+
+ React
+
+
+ on Lynx
+
+
+
+
+
+ Tap the logo and have fun!
+
+
+ Edit
+
+ src/App.tsx
+
+ to see updates!
+
+
+
+
+
+
+ `)
+ const {
+ findByText,
+ } = getQueriesForElement(elementTree.root)
+ const element = await findByText('Tap the logo and have fun!')
+ expect(element).toBeInTheDocument()
+ expect(element).toMatchInlineSnapshot(`
+
+ Tap the logo and have fun!
+
+ `)
+})
diff --git a/packages/rspeedy/create-rspeedy/template-react-rstest-rltl-js/src/index.jsx b/packages/rspeedy/create-rspeedy/template-react-rstest-rltl-js/src/index.jsx
new file mode 100644
index 0000000000..ab7f2c6da0
--- /dev/null
+++ b/packages/rspeedy/create-rspeedy/template-react-rstest-rltl-js/src/index.jsx
@@ -0,0 +1,9 @@
+import { root } from '@lynx-js/react'
+
+import { App } from './App.jsx'
+
+root.render()
+
+if (import.meta.webpackHot) {
+ import.meta.webpackHot.accept()
+}
diff --git a/packages/rspeedy/create-rspeedy/template-react-rstest-rltl-ts/lynx.config.ts b/packages/rspeedy/create-rspeedy/template-react-rstest-rltl-ts/lynx.config.ts
new file mode 100644
index 0000000000..057fa166d2
--- /dev/null
+++ b/packages/rspeedy/create-rspeedy/template-react-rstest-rltl-ts/lynx.config.ts
@@ -0,0 +1,18 @@
+import { defineConfig } from '@lynx-js/rspeedy'
+
+import { pluginQRCode } from '@lynx-js/qrcode-rsbuild-plugin'
+import { pluginReactLynx } from '@lynx-js/react-rsbuild-plugin'
+import { pluginTypeCheck } from '@rsbuild/plugin-type-check'
+
+export default defineConfig({
+ plugins: [
+ pluginQRCode({
+ schema(url) {
+ // We use `?fullscreen=true` to open the page in LynxExplorer in full screen mode
+ return `${url}?fullscreen=true`
+ },
+ }),
+ pluginReactLynx(),
+ pluginTypeCheck(),
+ ],
+})
diff --git a/packages/rspeedy/create-rspeedy/template-react-rstest-rltl-ts/package.json b/packages/rspeedy/create-rspeedy/template-react-rstest-rltl-ts/package.json
new file mode 100644
index 0000000000..643273986b
--- /dev/null
+++ b/packages/rspeedy/create-rspeedy/template-react-rstest-rltl-ts/package.json
@@ -0,0 +1,29 @@
+{
+ "name": "rspeedy-react-ts",
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "build": "rspeedy build",
+ "dev": "rspeedy dev",
+ "preview": "rspeedy preview",
+ "test": "rstest run"
+ },
+ "dependencies": {
+ "@lynx-js/react": "workspace:*"
+ },
+ "devDependencies": {
+ "@lynx-js/qrcode-rsbuild-plugin": "workspace:*",
+ "@lynx-js/react-rsbuild-plugin": "workspace:*",
+ "@lynx-js/rspeedy": "workspace:*",
+ "@lynx-js/types": "3.3.0",
+ "@rsbuild/plugin-type-check": "1.2.3",
+ "@rstest/core": "catalog:rstest",
+ "@testing-library/jest-dom": "^6.6.3",
+ "@types/react": "^18.3.23",
+ "jsdom": "^26.1.0",
+ "typescript": "~5.8.3"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+}
diff --git a/packages/rspeedy/create-rspeedy/template-react-rstest-rltl-ts/rstest.config.ts b/packages/rspeedy/create-rspeedy/template-react-rstest-rltl-ts/rstest.config.ts
new file mode 100644
index 0000000000..788a77ba69
--- /dev/null
+++ b/packages/rspeedy/create-rspeedy/template-react-rstest-rltl-ts/rstest.config.ts
@@ -0,0 +1,11 @@
+import { defineConfig, type RstestConfig } from '@rstest/core'
+import lynxConfig from './lynx.config.js'
+
+export default defineConfig({
+ ...lynxConfig as RstestConfig,
+ testEnvironment: 'jsdom',
+ setupFiles: [
+ require.resolve('@lynx-js/react/testing-library/setupFiles/rstest'),
+ ],
+ globals: true,
+})
diff --git a/packages/rspeedy/create-rspeedy/template-react-rstest-rltl-ts/src/App.tsx b/packages/rspeedy/create-rspeedy/template-react-rstest-rltl-ts/src/App.tsx
new file mode 100644
index 0000000000..fb6ed15fc9
--- /dev/null
+++ b/packages/rspeedy/create-rspeedy/template-react-rstest-rltl-ts/src/App.tsx
@@ -0,0 +1,55 @@
+import { useCallback, useEffect, useState } from '@lynx-js/react'
+
+import './App.css'
+import arrow from './assets/arrow.png'
+import lynxLogo from './assets/lynx-logo.png'
+import reactLynxLogo from './assets/react-logo.png'
+
+export function App(props: {
+ onMounted?: () => void
+}) {
+ const [alterLogo, setAlterLogo] = useState(false)
+
+ useEffect(() => {
+ console.info('Hello, ReactLynx')
+ props.onMounted?.()
+ }, [])
+
+ const onTap = useCallback(() => {
+ 'background only'
+ setAlterLogo(!alterLogo)
+ }, [alterLogo])
+
+ return (
+
+
+
+
+
+ {alterLogo
+ ?
+ : }
+
+ React
+ on Lynx
+
+
+
+ Tap the logo and have fun!
+
+ Edit
+ {' src/App.tsx '}
+
+ to see updates!
+
+
+
+
+
+ )
+}
diff --git a/packages/rspeedy/create-rspeedy/template-react-rstest-rltl-ts/src/__tests__/index.test.tsx b/packages/rspeedy/create-rspeedy/template-react-rstest-rltl-ts/src/__tests__/index.test.tsx
new file mode 100644
index 0000000000..c7c43c3cb5
--- /dev/null
+++ b/packages/rspeedy/create-rspeedy/template-react-rstest-rltl-ts/src/__tests__/index.test.tsx
@@ -0,0 +1,102 @@
+// Copyright 2024 The Lynx Authors. All rights reserved.
+// Licensed under the Apache License Version 2.0 that can be found in the
+// LICENSE file in the root directory of this source tree.
+import '@testing-library/jest-dom'
+import { expect, test, rstest } from '@rstest/core'
+import { render, getQueriesForElement } from '@lynx-js/react/testing-library'
+
+import { App } from '../App.js'
+
+test('App', async () => {
+ const cb = rstest.fn()
+
+ render(
+ {
+ cb(`__MAIN_THREAD__: ${__MAIN_THREAD__}`)
+ }}
+ />,
+ )
+ expect(cb).toBeCalledTimes(1)
+ expect(cb.mock.calls).toMatchInlineSnapshot(`
+ [
+ [
+ "__MAIN_THREAD__: false",
+ ],
+ ]
+ `)
+ expect(elementTree.root).toMatchInlineSnapshot(`
+
+
+
+
+
+
+
+
+
+ React
+
+
+ on Lynx
+
+
+
+
+
+ Tap the logo and have fun!
+
+
+ Edit
+
+ src/App.tsx
+
+ to see updates!
+
+
+
+
+
+
+ `)
+ const {
+ findByText,
+ } = getQueriesForElement(elementTree.root!)
+ const element = await findByText('Tap the logo and have fun!')
+ expect(element).toBeInTheDocument()
+ expect(element).toMatchInlineSnapshot(`
+
+ Tap the logo and have fun!
+
+ `)
+})
diff --git a/packages/rspeedy/create-rspeedy/template-react-rstest-rltl-ts/src/index.tsx b/packages/rspeedy/create-rspeedy/template-react-rstest-rltl-ts/src/index.tsx
new file mode 100644
index 0000000000..465aeacec6
--- /dev/null
+++ b/packages/rspeedy/create-rspeedy/template-react-rstest-rltl-ts/src/index.tsx
@@ -0,0 +1,9 @@
+import { root } from '@lynx-js/react'
+
+import { App } from './App.js'
+
+root.render()
+
+if (import.meta.webpackHot) {
+ import.meta.webpackHot.accept()
+}
diff --git a/packages/rspeedy/create-rspeedy/template-react-rstest-rltl-ts/src/rspeedy-env.d.ts b/packages/rspeedy/create-rspeedy/template-react-rstest-rltl-ts/src/rspeedy-env.d.ts
new file mode 100644
index 0000000000..1c813a68b0
--- /dev/null
+++ b/packages/rspeedy/create-rspeedy/template-react-rstest-rltl-ts/src/rspeedy-env.d.ts
@@ -0,0 +1 @@
+///
diff --git a/packages/rspeedy/create-rspeedy/template-react-rstest-rltl-ts/tsconfig.json b/packages/rspeedy/create-rspeedy/template-react-rstest-rltl-ts/tsconfig.json
new file mode 100644
index 0000000000..d9460d5509
--- /dev/null
+++ b/packages/rspeedy/create-rspeedy/template-react-rstest-rltl-ts/tsconfig.json
@@ -0,0 +1,17 @@
+{
+ "compilerOptions": {
+ "jsx": "react-jsx",
+ "jsxImportSource": "@lynx-js/react",
+
+ "module": "node18",
+ "moduleResolution": "node16",
+
+ "strict": true,
+ "isolatedModules": true,
+ "verbatimModuleSyntax": true,
+
+ "esModuleInterop": true,
+ "skipLibCheck": true,
+ },
+ "exclude": ["dist/"],
+}
diff --git a/packages/rspeedy/create-rspeedy/template-react-vitest-rltl-js/vitest.config.js b/packages/rspeedy/create-rspeedy/template-react-vitest-rltl-js/vitest.config.js
index 98425e53c0..7af00982cb 100644
--- a/packages/rspeedy/create-rspeedy/template-react-vitest-rltl-js/vitest.config.js
+++ b/packages/rspeedy/create-rspeedy/template-react-vitest-rltl-js/vitest.config.js
@@ -1,7 +1,7 @@
import { defineConfig, mergeConfig } from 'vitest/config'
import { createVitestConfig } from '@lynx-js/react/testing-library/vitest-config'
-const defaultConfig = await createVitestConfig()
+const defaultConfig = createVitestConfig()
const config = defineConfig({
test: {},
})
diff --git a/packages/rspeedy/create-rspeedy/template-react-vitest-rltl-ts/vitest.config.ts b/packages/rspeedy/create-rspeedy/template-react-vitest-rltl-ts/vitest.config.ts
index 98425e53c0..7af00982cb 100644
--- a/packages/rspeedy/create-rspeedy/template-react-vitest-rltl-ts/vitest.config.ts
+++ b/packages/rspeedy/create-rspeedy/template-react-vitest-rltl-ts/vitest.config.ts
@@ -1,7 +1,7 @@
import { defineConfig, mergeConfig } from 'vitest/config'
import { createVitestConfig } from '@lynx-js/react/testing-library/vitest-config'
-const defaultConfig = await createVitestConfig()
+const defaultConfig = createVitestConfig()
const config = defineConfig({
test: {},
})
diff --git a/packages/rspeedy/plugin-react/package.json b/packages/rspeedy/plugin-react/package.json
index a55b81f4c5..290ac6f274 100644
--- a/packages/rspeedy/plugin-react/package.json
+++ b/packages/rspeedy/plugin-react/package.json
@@ -44,7 +44,8 @@
"@lynx-js/runtime-wrapper-webpack-plugin": "workspace:*",
"@lynx-js/template-webpack-plugin": "workspace:*",
"@lynx-js/use-sync-external-store": "workspace:*",
- "background-only": "workspace:^"
+ "background-only": "workspace:^",
+ "tiny-invariant": "^1.3.3"
},
"devDependencies": {
"@lynx-js/react": "workspace:*",
diff --git a/packages/rspeedy/plugin-react/src/entry.ts b/packages/rspeedy/plugin-react/src/entry.ts
index 0a62f082f9..b055f5d7ee 100644
--- a/packages/rspeedy/plugin-react/src/entry.ts
+++ b/packages/rspeedy/plugin-react/src/entry.ts
@@ -12,7 +12,6 @@ import type {
import type { UndefinedOnPartialDeep } from 'type-fest'
import { LAYERS, ReactWebpackPlugin } from '@lynx-js/react-webpack-plugin'
-import type { ExposedAPI } from '@lynx-js/rspeedy'
import { RuntimeWrapperWebpackPlugin } from '@lynx-js/runtime-wrapper-webpack-plugin'
import {
LynxEncodePlugin,
@@ -21,6 +20,7 @@ import {
} from '@lynx-js/template-webpack-plugin'
import type { PluginReactLynxOptions } from './pluginReactLynx.js'
+import { getRspeedyAPI } from './rspeedy-api.js'
const PLUGIN_NAME_REACT = 'lynx:react'
const PLUGIN_NAME_TEMPLATE = 'lynx:template'
@@ -58,202 +58,202 @@ export function applyEntry(
experimental_isLazyBundle,
} = options
- const { config, logger } = api.useExposed(
- Symbol.for('rspeedy.api'),
- )!
api.modifyBundlerChain((chain, { environment, isDev, isProd }) => {
- const entries = chain.entryPoints.entries() ?? {}
- const isLynx = environment.name === 'lynx'
- const isWeb = environment.name === 'web'
-
- chain.entryPoints.clear()
-
+ let finalFirstScreenSyncTiming = firstScreenSyncTiming
const mainThreadChunks: string[] = []
-
- Object.entries(entries).forEach(([entryName, entryPoint]) => {
- const { imports } = getChunks(entryName, entryPoint.values())
-
- const templateFilename = (
- typeof config.output?.filename === 'object'
- ? config.output.filename.bundle ?? config.output.filename.template
- : config.output?.filename
- ) ?? '[name].[platform].bundle'
-
- // We do not use `${entryName}__background` since the default CSS name is `[name]/[name].css`.
- // We would like to avoid adding `__background` to the output CSS filename.
- const mainThreadEntry = `${entryName}__main-thread`
-
- const mainThreadName = path.posix.join(
- isLynx
- // TODO: config intermediate
- ? DEFAULT_DIST_PATH_INTERMEDIATE
- // For non-Lynx environment, the entry is not deleted.
- // So we do not put it in the intermediate.
- : '',
- `${entryName}/main-thread.js`,
- )
-
- const backgroundName = path.posix.join(
- isLynx
- // TODO: config intermediate
- ? DEFAULT_DIST_PATH_INTERMEDIATE
- // For non-Lynx environment, the entry is not deleted.
- // So we do not put it in the intermediate.
- : '',
- getBackgroundFilename(
- entryName,
- environment.config,
- isProd,
- experimental_isLazyBundle,
- ),
- )
-
- const backgroundEntry = entryName
-
- mainThreadChunks.push(mainThreadName)
-
- chain
- .entry(mainThreadEntry)
- .add({
- layer: LAYERS.MAIN_THREAD,
- import: imports,
- filename: mainThreadName,
- })
- .when(isDev && !isWeb, entry => {
- const require = createRequire(import.meta.url)
- // use prepend to make sure it does not affect the exports
- // from the entry
- entry
- .prepend({
- layer: LAYERS.MAIN_THREAD,
- import: require.resolve(
- '@lynx-js/css-extract-webpack-plugin/runtime/hotModuleReplacement.lepus.cjs',
- ),
- })
- })
- .end()
- .entry(backgroundEntry)
- .add({
- layer: LAYERS.BACKGROUND,
- import: imports,
- filename: backgroundName,
- })
- // in standalone lazy bundle mode, we do not add
- // other entries to avoid wrongly exporting from other entries
- .when(isDev && !isWeb, entry => {
- // use prepend to make sure it does not affect the exports
- // from the entry
- entry
- // This is aliased in `@lynx-js/rspeedy`
- .prepend({
- layer: LAYERS.BACKGROUND,
- import: '@rspack/core/hot/dev-server',
- })
- .prepend({
- layer: LAYERS.BACKGROUND,
- import: '@lynx-js/webpack-dev-transport/client',
- })
- // This is aliased in `./refresh.ts`
- .prepend({
- layer: LAYERS.BACKGROUND,
- import: '@lynx-js/react/refresh',
- })
- })
- .end()
- .plugin(`${PLUGIN_NAME_TEMPLATE}-${entryName}`)
- .use(LynxTemplatePlugin, [{
- dsl: 'react_nodiff',
- chunks: [mainThreadEntry, backgroundEntry],
- filename: templateFilename.replaceAll('[name]', entryName).replaceAll(
- '[platform]',
- environment.name,
- ),
- intermediate: path.posix.join(
- DEFAULT_DIST_PATH_INTERMEDIATE,
- entryName,
- ),
- customCSSInheritanceList,
- debugInfoOutside,
- defaultDisplayLinear,
- enableA11y: true,
- enableAccessibilityElement,
- enableICU,
- enableCSSInheritance,
- enableCSSInvalidation,
- enableCSSSelector,
- enableNewGesture,
- enableParallelElement,
- enableRemoveCSSScope: enableRemoveCSSScope ?? true,
- pipelineSchedulerConfig,
- removeDescendantSelectorScope,
- targetSdkVersion,
-
- experimental_isLazyBundle,
- cssPlugins: [],
- }])
- .end()
- })
-
const rsbuildConfig = api.getRsbuildConfig()
- const userConfig = api.getRsbuildConfig('original')
-
const enableChunkSplitting =
rsbuildConfig.performance?.chunkSplit?.strategy !== 'all-in-one'
- let finalFirstScreenSyncTiming = firstScreenSyncTiming
-
- if (isLynx) {
- let inlineScripts
- if (experimental_isLazyBundle) {
- // TODO: support inlineScripts in lazyBundle
- inlineScripts = true
- } else {
- inlineScripts = environment.config.output?.inlineScripts
- ?? !enableChunkSplitting
+ const isRstest = api.context.callerName === 'rstest'
+
+ if (!isRstest) {
+ const { config } = getRspeedyAPI(api)
+
+ const entries = chain.entryPoints.entries() ?? {}
+ const isLynx = environment.name === 'lynx'
+ const isWeb = environment.name === 'web'
+
+ chain.entryPoints.clear()
+
+ Object.entries(entries).forEach(([entryName, entryPoint]) => {
+ const { imports } = getChunks(entryName, entryPoint.values())
+
+ const templateFilename = (
+ typeof config.output?.filename === 'object'
+ ? config.output.filename.bundle ?? config.output.filename.template
+ : config.output?.filename
+ ) ?? '[name].[platform].bundle'
+
+ // We do not use `${entryName}__background` since the default CSS name is `[name]/[name].css`.
+ // We would like to avoid adding `__background` to the output CSS filename.
+ const mainThreadEntry = `${entryName}__main-thread`
+
+ const mainThreadName = path.posix.join(
+ isLynx
+ // TODO: config intermediate
+ ? DEFAULT_DIST_PATH_INTERMEDIATE
+ // For non-Lynx environment, the entry is not deleted.
+ // So we do not put it in the intermediate.
+ : '',
+ `${entryName}/main-thread.js`,
+ )
+
+ const backgroundName = path.posix.join(
+ isLynx
+ // TODO: config intermediate
+ ? DEFAULT_DIST_PATH_INTERMEDIATE
+ // For non-Lynx environment, the entry is not deleted.
+ // So we do not put it in the intermediate.
+ : '',
+ getBackgroundFilename(
+ entryName,
+ environment.config,
+ isProd,
+ experimental_isLazyBundle,
+ ),
+ )
+
+ const backgroundEntry = entryName
+
+ mainThreadChunks.push(mainThreadName)
+
+ chain
+ .entry(mainThreadEntry)
+ .add({
+ layer: LAYERS.MAIN_THREAD,
+ import: imports,
+ filename: mainThreadName,
+ })
+ .when(isDev && !isWeb, entry => {
+ const require = createRequire(import.meta.url)
+ // use prepend to make sure it does not affect the exports
+ // from the entry
+ entry
+ .prepend({
+ layer: LAYERS.MAIN_THREAD,
+ import: require.resolve(
+ '@lynx-js/css-extract-webpack-plugin/runtime/hotModuleReplacement.lepus.cjs',
+ ),
+ })
+ })
+ .end()
+ .entry(backgroundEntry)
+ .add({
+ layer: LAYERS.BACKGROUND,
+ import: imports,
+ filename: backgroundName,
+ })
+ // in standalone lazy bundle mode, we do not add
+ // other entries to avoid wrongly exporting from other entries
+ .when(isDev && !isWeb, entry => {
+ // use prepend to make sure it does not affect the exports
+ // from the entry
+ entry
+ // This is aliased in `@lynx-js/rspeedy`
+ .prepend({
+ layer: LAYERS.BACKGROUND,
+ import: '@rspack/core/hot/dev-server',
+ })
+ .prepend({
+ layer: LAYERS.BACKGROUND,
+ import: '@lynx-js/webpack-dev-transport/client',
+ })
+ // This is aliased in `./refresh.ts`
+ .prepend({
+ layer: LAYERS.BACKGROUND,
+ import: '@lynx-js/react/refresh',
+ })
+ })
+ .end()
+ .plugin(`${PLUGIN_NAME_TEMPLATE}-${entryName}`)
+ .use(LynxTemplatePlugin, [{
+ dsl: 'react_nodiff',
+ chunks: [mainThreadEntry, backgroundEntry],
+ filename: templateFilename.replaceAll('[name]', entryName)
+ .replaceAll(
+ '[platform]',
+ environment.name,
+ ),
+ intermediate: path.posix.join(
+ DEFAULT_DIST_PATH_INTERMEDIATE,
+ entryName,
+ ),
+ customCSSInheritanceList,
+ debugInfoOutside,
+ defaultDisplayLinear,
+ enableA11y: true,
+ enableAccessibilityElement,
+ enableICU,
+ enableCSSInheritance,
+ enableCSSInvalidation,
+ enableCSSSelector,
+ enableNewGesture,
+ enableParallelElement,
+ enableRemoveCSSScope: enableRemoveCSSScope ?? true,
+ pipelineSchedulerConfig,
+ removeDescendantSelectorScope,
+ targetSdkVersion,
+
+ experimental_isLazyBundle,
+ cssPlugins: [],
+ }])
+ .end()
+ })
+
+ if (isLynx) {
+ let inlineScripts
+ if (experimental_isLazyBundle) {
+ // TODO: support inlineScripts in lazyBundle
+ inlineScripts = true
+ } else {
+ inlineScripts = environment.config.output?.inlineScripts
+ ?? !enableChunkSplitting
+ }
+
+ if (inlineScripts !== true) {
+ finalFirstScreenSyncTiming = 'jsReady'
+ }
+
+ chain
+ .plugin(PLUGIN_NAME_RUNTIME_WRAPPER)
+ .use(RuntimeWrapperWebpackPlugin, [{
+ injectVars(vars) {
+ const UNUSED_VARS = new Set([
+ 'Card',
+ 'Component',
+ 'ReactLynx',
+ 'Behavior',
+ ])
+ return vars.map(name => {
+ if (UNUSED_VARS.has(name)) {
+ return `__${name}`
+ }
+ return name
+ })
+ },
+ targetSdkVersion,
+ // Inject runtime wrapper for all `.js` but not `main-thread.js` and `main-thread.[hash].js`.
+ test: /^(?!.*main-thread(?:\.[A-Fa-f0-9]*)?\.js$).*\.js$/,
+ experimental_isLazyBundle,
+ }])
+ .end()
+ .plugin(`${LynxEncodePlugin.name}`)
+ .use(LynxEncodePlugin, [{ inlineScripts }])
+ .end()
}
- if (inlineScripts !== true) {
- finalFirstScreenSyncTiming = 'jsReady'
+ if (isWeb) {
+ chain
+ .plugin(PLUGIN_NAME_WEB)
+ .use(WebEncodePlugin, [])
+ .end()
}
-
- chain
- .plugin(PLUGIN_NAME_RUNTIME_WRAPPER)
- .use(RuntimeWrapperWebpackPlugin, [{
- injectVars(vars) {
- const UNUSED_VARS = new Set([
- 'Card',
- 'Component',
- 'ReactLynx',
- 'Behavior',
- ])
- return vars.map(name => {
- if (UNUSED_VARS.has(name)) {
- return `__${name}`
- }
- return name
- })
- },
- targetSdkVersion,
- // Inject runtime wrapper for all `.js` but not `main-thread.js` and `main-thread.[hash].js`.
- test: /^(?!.*main-thread(?:\.[A-Fa-f0-9]*)?\.js$).*\.js$/,
- experimental_isLazyBundle,
- }])
- .end()
- .plugin(`${LynxEncodePlugin.name}`)
- .use(LynxEncodePlugin, [{ inlineScripts }])
- .end()
- }
-
- if (isWeb) {
- chain
- .plugin(PLUGIN_NAME_WEB)
- .use(WebEncodePlugin, [])
- .end()
}
let extractStr = originalExtractStr
if (enableChunkSplitting && originalExtractStr) {
- logger.warn(
+ ;(api.logger ?? console).warn(
'`extractStr` is changed to `false` because it is only supported in `all-in-one` chunkSplit strategy, please set `performance.chunkSplit.strategy` to `all-in-one` to use `extractStr.`',
)
extractStr = false
@@ -274,6 +274,7 @@ export function applyEntry(
}])
function getDefaultProfile(): boolean | undefined {
+ const userConfig = api.getRsbuildConfig('original')
if (userConfig.performance?.profile !== undefined) {
return userConfig.performance.profile
}
diff --git a/packages/rspeedy/plugin-react/src/loaders.ts b/packages/rspeedy/plugin-react/src/loaders.ts
index 5184e1a69e..a907c80059 100644
--- a/packages/rspeedy/plugin-react/src/loaders.ts
+++ b/packages/rspeedy/plugin-react/src/loaders.ts
@@ -7,19 +7,65 @@ import { LAYERS, ReactWebpackPlugin } from '@lynx-js/react-webpack-plugin'
import type { PluginReactLynxOptions } from './pluginReactLynx.js'
-export function applyLoaders(
+function getLoaderOptions(
api: RsbuildPluginAPI,
options: Required,
-): void {
+ isMainThread = false,
+) {
+ const { output } = api.getRsbuildConfig()
+
+ const inlineSourcesContent: boolean = output?.sourceMap === true || !(
+ // `false`
+ output?.sourceMap === false
+ // `false`
+ || output?.sourceMap?.js === false
+ // explicitly disable source content
+ || output?.sourceMap?.js?.includes('nosources')
+ )
+
const {
compat,
enableRemoveCSSScope,
- shake,
defineDCE,
+ shake,
experimental_isLazyBundle,
} = options
+ return {
+ compat,
+ enableRemoveCSSScope,
+ isDynamicComponent: experimental_isLazyBundle,
+ inlineSourcesContent,
+ defineDCE,
+ ...isMainThread
+ ? {
+ shake,
+ }
+ : {},
+ }
+}
+
+const TESTING_RULE_NAME = 'react:testing'
+export function applyTestingLoaders(
+ api: RsbuildPluginAPI,
+ options: Required,
+): void {
+ api.modifyBundlerChain((chain, { CHAIN_ID }) => {
+ const rule = chain.module.rules.get(CHAIN_ID.RULE.JS)
+
+ rule
+ .use(TESTING_RULE_NAME)
+ .loader(ReactWebpackPlugin.loaders.TESTING)
+ .options(getLoaderOptions(api, options))
+ .end()
+ })
+}
+
+export function applyLoaders(
+ api: RsbuildPluginAPI,
+ options: Required,
+): void {
api.modifyBundlerChain((chain, { CHAIN_ID }) => {
const experiments = chain.get(
'experiments',
@@ -39,17 +85,6 @@ export function applyLoaders(
// - Webpack: None
const uses = rule.uses.entries() ?? {}
- const { output } = api.getRsbuildConfig()
-
- const inlineSourcesContent: boolean = output?.sourceMap === true || !(
- // `false`
- output?.sourceMap === false
- // `false`
- || output?.sourceMap?.js === false
- // explicitly disable source content
- || output?.sourceMap?.js?.includes('nosources')
- )
-
const backgroundRule = rule.oneOf(LAYERS.BACKGROUND)
// dprint-ignore
backgroundRule
@@ -59,13 +94,7 @@ export function applyLoaders(
.end()
.use(LAYERS.BACKGROUND)
.loader(ReactWebpackPlugin.loaders.BACKGROUND)
- .options({
- compat,
- enableRemoveCSSScope,
- isDynamicComponent: experimental_isLazyBundle,
- inlineSourcesContent,
- defineDCE,
- })
+ .options(getLoaderOptions(api, options))
.end()
const mainThreadRule = rule.oneOf(LAYERS.MAIN_THREAD)
@@ -97,14 +126,7 @@ export function applyLoaders(
})
.use(LAYERS.MAIN_THREAD)
.loader(ReactWebpackPlugin.loaders.MAIN_THREAD)
- .options({
- compat,
- enableRemoveCSSScope,
- inlineSourcesContent,
- isDynamicComponent: experimental_isLazyBundle,
- shake,
- defineDCE,
- })
+ .options(getLoaderOptions(api, options, true))
.end()
// Clear the Rsbuild default loader.
diff --git a/packages/rspeedy/plugin-react/src/pluginReactLynx.ts b/packages/rspeedy/plugin-react/src/pluginReactLynx.ts
index dd8abd3b7c..45a77e217e 100644
--- a/packages/rspeedy/plugin-react/src/pluginReactLynx.ts
+++ b/packages/rspeedy/plugin-react/src/pluginReactLynx.ts
@@ -20,15 +20,15 @@ import type {
ShakeVisitorConfig,
} from '@lynx-js/react-transform'
import { LAYERS } from '@lynx-js/react-webpack-plugin'
-import type { ExposedAPI } from '@lynx-js/rspeedy'
import { applyBackgroundOnly } from './backgroundOnly.js'
import { applyCSS } from './css.js'
import { applyEntry } from './entry.js'
import { applyGenerator } from './generator.js'
import { applyLazy } from './lazy.js'
-import { applyLoaders } from './loaders.js'
+import { applyLoaders, applyTestingLoaders } from './loaders.js'
import { applyRefresh } from './refresh.js'
+import { applyRstest } from './rstest.js'
import { applySplitChunksRule } from './splitChunks.js'
import { applySWC } from './swc.js'
import { applyUseSyncExternalStore } from './useSyncExternalStore.js'
@@ -372,11 +372,22 @@ export function pluginReactLynx(
name: 'lynx:react',
pre: ['lynx:rsbuild:plugin-api'],
setup(api) {
- applyCSS(api, resolvedOptions)
+ const isRstest = api.context.callerName === 'rstest'
+
+ if (isRstest) {
+ applyRstest(api)
+ }
+ if (!isRstest) {
+ applyCSS(api, resolvedOptions)
+ }
applyEntry(api, resolvedOptions)
applyBackgroundOnly(api)
applyGenerator(api, resolvedOptions)
- applyLoaders(api, resolvedOptions)
+ if (isRstest) {
+ applyTestingLoaders(api, resolvedOptions)
+ } else {
+ applyLoaders(api, resolvedOptions)
+ }
applyRefresh(api)
applySplitChunksRule(api)
applySWC(api)
@@ -420,20 +431,16 @@ export function pluginReactLynx(
applyLazy(api)
}
- const rspeedyAPIs = api.useExposed(
- Symbol.for('rspeedy.api'),
- )!
-
const require = createRequire(import.meta.url)
const { version } = require('../package.json') as { version: string }
- rspeedyAPIs.debug(() => {
- const webpackPluginPath = require.resolve(
- '@lynx-js/react-webpack-plugin',
- )
- return `Using @lynx-js/react-webpack-plugin v${version} at ${webpackPluginPath}`
- })
+ const webpackPluginPath = require.resolve(
+ '@lynx-js/react-webpack-plugin',
+ )
+ api.logger?.debug(
+ `Using @lynx-js/react-webpack-plugin v${version} at ${webpackPluginPath}`,
+ )
},
},
]
diff --git a/packages/rspeedy/plugin-react/src/rspeedy-api.ts b/packages/rspeedy/plugin-react/src/rspeedy-api.ts
new file mode 100644
index 0000000000..5a2de15cc6
--- /dev/null
+++ b/packages/rspeedy/plugin-react/src/rspeedy-api.ts
@@ -0,0 +1,14 @@
+// Copyright 2025 The Lynx Authors. All rights reserved.
+// Licensed under the Apache License Version 2.0 that can be found in the
+// LICENSE file in the root directory of this source tree.
+import invariant from 'tiny-invariant'
+
+import type { ExposedAPI, RsbuildPluginAPI } from '@lynx-js/rspeedy'
+
+export function getRspeedyAPI(api: RsbuildPluginAPI): ExposedAPI {
+ const rspeedyAPI = api.useExposed(
+ Symbol.for('rspeedy.api'),
+ )!
+ invariant(rspeedyAPI, 'Should have rspeedy.api')
+ return rspeedyAPI
+}
diff --git a/packages/rspeedy/plugin-react/src/rstest.ts b/packages/rspeedy/plugin-react/src/rstest.ts
new file mode 100644
index 0000000000..6797a23039
--- /dev/null
+++ b/packages/rspeedy/plugin-react/src/rstest.ts
@@ -0,0 +1,15 @@
+// Copyright 2025 The Lynx Authors. All rights reserved.
+// Licensed under the Apache License Version 2.0 that can be found in the
+// LICENSE file in the root directory of this source tree.
+import type { RsbuildPluginAPI } from '@rsbuild/core'
+
+export function applyRstest(api: RsbuildPluginAPI): void {
+ api.modifyRsbuildConfig((config, { mergeRsbuildConfig }) => {
+ return mergeRsbuildConfig({
+ output: {
+ // Allow using Node.js internal modules in testing framework directly
+ externals: /^node:/,
+ },
+ }, config)
+ })
+}
diff --git a/packages/rspeedy/plugin-react/test/config.test.ts b/packages/rspeedy/plugin-react/test/config.test.ts
index 20175e3c36..1eb266ad8b 100644
--- a/packages/rspeedy/plugin-react/test/config.test.ts
+++ b/packages/rspeedy/plugin-react/test/config.test.ts
@@ -2232,6 +2232,75 @@ describe('Config', () => {
expect(ReactLynxWebpackPlugin?.options.profile).toBe(false)
})
})
+
+ describe('callerName: rstest', async () => {
+ const { pluginReactLynx } = await import('../src/pluginReactLynx.js')
+
+ const rsbuild = await createRspeedy({
+ rspeedyConfig: {
+ plugins: [
+ pluginReactLynx(),
+ ],
+ },
+ callerName: 'rstest',
+ })
+ const [config] = await rsbuild.initConfigs()
+ interface Rule {
+ test?: RegExp
+ use?: Array<{ loader: string }>
+ [key: string]: unknown
+ }
+
+ const rules = config?.module?.rules as Rule[] | undefined
+
+ test('css rules should be rsbuild default', () => {
+ expect(
+ rules?.filter((rule: Rule) =>
+ rule
+ && typeof rule === 'object'
+ && rule.test
+ && rule.test.toString() === (/\.css$/).toString()
+ ).map((rule: Rule) =>
+ (rule?.use?.map((u: { loader: string }) => u.loader)) ?? []
+ ),
+ ).toMatchInlineSnapshot(`
+ [
+ [
+ "/node_modules//@rspack/core/dist/cssExtractLoader.js",
+ "/node_modules//@rsbuild/core/compiled/css-loader/index.js",
+ "builtin:lightningcss-loader",
+ ],
+ [
+ "/node_modules//@rsbuild/core/compiled/css-loader/index.js",
+ "builtin:lightningcss-loader",
+ ],
+ [],
+ ]
+ `)
+ })
+ test('js loaders should be testing loaders', () => {
+ expect(
+ rules?.filter((rule: Rule) =>
+ rule
+ && typeof rule === 'object'
+ && rule.test
+ && rule.test.toString()
+ === (/\.(?:js|jsx|mjs|cjs|ts|tsx|mts|cts)$/).toString()
+ ).map((rule: Rule) =>
+ (rule?.use?.map((u: { loader: string }) => u.loader)) ?? []
+ ),
+ ).toMatchInlineSnapshot(`
+ [
+ [
+ "builtin:swc-loader",
+ "/packages/webpack/react-webpack-plugin/lib/loaders/testing.js",
+ ],
+ [],
+ [],
+ ]
+ `)
+ })
+ })
})
describe('MPA Config', () => {
diff --git a/packages/testing-library/examples/basic/package.json b/packages/testing-library/examples/basic/package.json
index ed5ed19db6..e77dc1abaf 100644
--- a/packages/testing-library/examples/basic/package.json
+++ b/packages/testing-library/examples/basic/package.json
@@ -6,6 +6,7 @@
"scripts": {
"build": "rspeedy build",
"dev": "rspeedy dev",
+ "rstest": "rstest",
"test": "vitest",
"test:type": "vitest --typecheck.only"
},
diff --git a/packages/testing-library/examples/basic/rstest.config.ts b/packages/testing-library/examples/basic/rstest.config.ts
new file mode 100644
index 0000000000..278ef168b2
--- /dev/null
+++ b/packages/testing-library/examples/basic/rstest.config.ts
@@ -0,0 +1,11 @@
+import { defineConfig, RstestConfig } from '@rstest/core';
+import lynxConfig from './lynx.config.js';
+
+export default defineConfig({
+ ...lynxConfig as RstestConfig,
+ testEnvironment: 'jsdom',
+ setupFiles: [
+ require.resolve('@lynx-js/react/testing-library/setupFiles/rstest'),
+ ],
+ globals: true,
+});
diff --git a/packages/testing-library/examples/basic/src/__tests__/index.test.tsx b/packages/testing-library/examples/basic/src/__tests__/index.test.tsx
index d877834a08..1b6a7c4992 100644
--- a/packages/testing-library/examples/basic/src/__tests__/index.test.tsx
+++ b/packages/testing-library/examples/basic/src/__tests__/index.test.tsx
@@ -2,7 +2,6 @@
// Licensed under the Apache License Version 2.0 that can be found in the
// LICENSE file in the root directory of this source tree.
import '@testing-library/jest-dom';
-import { expect, test, vi } from 'vitest';
import { render, getQueriesForElement } from '@lynx-js/react/testing-library';
// @ts-expect-error preact is aliased to the dep of @lynx-js/react
import { Component as PreacComponent } from 'preact';
@@ -10,8 +9,15 @@ import { Component } from '@lynx-js/react';
import { App } from '../App.jsx';
+let fn;
+if (typeof rstest !== 'undefined') {
+ fn = rstest.fn;
+} else {
+ fn = vi.fn;
+}
+
test('App', async () => {
- const cb = vi.fn();
+ const cb = fn();
render(
+///
+///
diff --git a/packages/testing-library/examples/basic/vitest.config.ts b/packages/testing-library/examples/basic/vitest.config.ts
index cbe326697f..242ad24f04 100644
--- a/packages/testing-library/examples/basic/vitest.config.ts
+++ b/packages/testing-library/examples/basic/vitest.config.ts
@@ -1,7 +1,7 @@
import { defineConfig, mergeConfig } from 'vitest/config';
import { createVitestConfig } from '@lynx-js/react/testing-library/vitest-config';
-const defaultConfig = await createVitestConfig({
+const defaultConfig = createVitestConfig({
runtimePkgName: '@lynx-js/react',
});
const config = defineConfig({
diff --git a/packages/testing-library/testing-environment/src/index.ts b/packages/testing-library/testing-environment/src/index.ts
index 1dc6e37f62..293d17b537 100644
--- a/packages/testing-library/testing-environment/src/index.ts
+++ b/packages/testing-library/testing-environment/src/index.ts
@@ -5,11 +5,10 @@
* notably the {@link https://lynxjs.org/api/engine/element-api | Element PAPI} and {@link https://lynxjs.org/guide/spec#dual-threaded-model | Dual-threaded Model} for use with Node.js.
*/
-import EventEmitter from 'events';
+import EventEmitter from 'node:events';
import { JSDOM } from 'jsdom';
import { createGlobalThis, LynxGlobalThis } from './lynx/GlobalThis.js';
import { initElementTree } from './lynx/ElementPAPI.js';
-import { Console } from 'console';
import { GlobalEventEmitter } from './lynx/GlobalEventEmitter.js';
export { initElementTree } from './lynx/ElementPAPI.js';
export type { LynxElement } from './lynx/ElementPAPI.js';
@@ -205,18 +204,6 @@ function createPolyfills() {
};
}
-function createPreconfiguredConsole() {
- const console = new Console(
- process.stdout,
- process.stderr,
- );
- console.profile = () => {};
- console.profileEnd = () => {};
- // @ts-expect-error Lynx has console.alog
- console.alog = () => {};
- return console;
-}
-
function injectMainThreadGlobals(target?: any, polyfills?: any) {
__injectElementApi(target);
@@ -265,7 +252,10 @@ function injectMainThreadGlobals(target?: any, polyfills?: any) {
target.requestAnimationFrame = setTimeout;
target.cancelAnimationFrame = clearTimeout;
- target.console = createPreconfiguredConsole();
+ target.console.profile = console.profile = () => {};
+ target.console.profileEnd = console.profileEnd = () => {};
+ // @ts-expect-error Lynx has console.alog
+ target.console.alog = console.alog = () => {};
target.__LoadLepusChunk = __LoadLepusChunk;
@@ -377,7 +367,10 @@ function injectBackgroundThreadGlobals(target?: any, polyfills?: any) {
target.requestAnimationFrame = setTimeout;
target.cancelAnimationFrame = clearTimeout;
- target.console = createPreconfiguredConsole();
+ target.console.profile = console.profile = () => {};
+ target.console.profileEnd = console.profileEnd = () => {};
+ // @ts-expect-error Lynx has console.alog
+ target.console.alog = console.alog = () => {};
// TODO: user-configurable
target.SystemInfo = {
diff --git a/packages/use-sync-external-store/vitest.config.ts b/packages/use-sync-external-store/vitest.config.ts
index 94b3576d8e..352dc3f58d 100644
--- a/packages/use-sync-external-store/vitest.config.ts
+++ b/packages/use-sync-external-store/vitest.config.ts
@@ -3,7 +3,7 @@ import type { UserWorkspaceConfig } from 'vitest/config';
import { createVitestConfig } from '@lynx-js/react/testing-library/vitest-config';
-const defaultConfig = await createVitestConfig();
+const defaultConfig = createVitestConfig();
const config: UserWorkspaceConfig = defineProject({
test: {
diff --git a/packages/webpack/react-webpack-plugin/etc/react-webpack-plugin.api.md b/packages/webpack/react-webpack-plugin/etc/react-webpack-plugin.api.md
index 0abc7b9051..9e11f448df 100644
--- a/packages/webpack/react-webpack-plugin/etc/react-webpack-plugin.api.md
+++ b/packages/webpack/react-webpack-plugin/etc/react-webpack-plugin.api.md
@@ -37,7 +37,7 @@ export class ReactWebpackPlugin {
constructor(options?: ReactWebpackPluginOptions | undefined);
apply(compiler: Compiler): void;
static defaultOptions: Readonly>;
- static loaders: Record;
+ static loaders: Record;
}
// @public
diff --git a/packages/webpack/react-webpack-plugin/src/ReactWebpackPlugin.ts b/packages/webpack/react-webpack-plugin/src/ReactWebpackPlugin.ts
index be1be1a2ea..9c7ab433fa 100644
--- a/packages/webpack/react-webpack-plugin/src/ReactWebpackPlugin.ts
+++ b/packages/webpack/react-webpack-plugin/src/ReactWebpackPlugin.ts
@@ -114,9 +114,10 @@ class ReactWebpackPlugin {
*
* @public
*/
- static loaders: Record = {
+ static loaders: Record = {
BACKGROUND: require.resolve('../lib/loaders/background.js'),
MAIN_THREAD: require.resolve('../lib/loaders/main-thread.js'),
+ TESTING: require.resolve('../lib/loaders/testing.js'),
};
constructor(
diff --git a/packages/webpack/react-webpack-plugin/src/loaders/options.ts b/packages/webpack/react-webpack-plugin/src/loaders/options.ts
index 2106b140d4..54f5325a66 100644
--- a/packages/webpack/react-webpack-plugin/src/loaders/options.ts
+++ b/packages/webpack/react-webpack-plugin/src/loaders/options.ts
@@ -14,12 +14,12 @@ import type {
} from '@lynx-js/react/transform';
const PLUGIN_NAME = 'react:webpack';
-const JSX_IMPORT_SOURCE = {
+export const JSX_IMPORT_SOURCE = {
MAIN_THREAD: '@lynx-js/react/lepus',
BACKGROUND: '@lynx-js/react',
};
const PUBLIC_RUNTIME_PKG = '@lynx-js/react';
-const RUNTIME_PKG = '@lynx-js/react/internal';
+export const RUNTIME_PKG = '@lynx-js/react/internal';
const OLD_RUNTIME_PKG = '@lynx-js/react-runtime';
const COMPONENT_PKG = '@lynx-js/react-components';
diff --git a/packages/webpack/react-webpack-plugin/src/loaders/testing.ts b/packages/webpack/react-webpack-plugin/src/loaders/testing.ts
new file mode 100644
index 0000000000..443489c9fc
--- /dev/null
+++ b/packages/webpack/react-webpack-plugin/src/loaders/testing.ts
@@ -0,0 +1,119 @@
+// Copyright 2024 The Lynx Authors. All rights reserved.
+// Licensed under the Apache License Version 2.0 that can be found in the
+// LICENSE file in the root directory of this source tree.
+import { createRequire } from 'node:module';
+import path from 'node:path';
+
+import type { LoaderContext } from '@rspack/core';
+
+import { JSX_IMPORT_SOURCE, RUNTIME_PKG } from './options.js';
+import type { ReactLoaderOptions } from './options.js';
+
+function normalizeSlashes(file: string) {
+ return file.replaceAll(path.win32.sep, '/');
+}
+
+function testingLoader(
+ this: LoaderContext,
+ content: string,
+): void {
+ const require = createRequire(import.meta.url);
+ const { transformPath = '@lynx-js/react/transform' } = this.getOptions();
+ const { transformReactLynxSync } = require(
+ transformPath,
+ ) as typeof import('@lynx-js/react/transform');
+ const filename = normalizeSlashes(
+ path.relative(
+ this.rootContext,
+ this.resourcePath,
+ ),
+ );
+ const result = transformReactLynxSync(
+ content,
+ {
+ mode: 'test',
+ pluginName: '',
+ filename: this.resourcePath,
+ sourcemap: true,
+ snapshot: {
+ preserveJsx: false,
+ runtimePkg: RUNTIME_PKG,
+ jsxImportSource: JSX_IMPORT_SOURCE.BACKGROUND,
+ filename,
+ target: 'MIXED',
+ },
+ // snapshot: true,
+ directiveDCE: false,
+ defineDCE: false,
+ shake: false,
+ compat: false,
+ worklet: {
+ filename,
+ runtimePkg: RUNTIME_PKG,
+ target: 'MIXED',
+ },
+ refresh: false,
+ cssScope: false,
+ },
+ );
+
+ if (result.errors.length > 0) {
+ for (const error of result.errors) {
+ if (this.experiments?.emitDiagnostic) {
+ // Rspack with `emitDiagnostic` API
+ try {
+ this.experiments.emitDiagnostic({
+ message: error.text!,
+ sourceCode: content,
+ location: {
+ line: error.location?.line ?? 1,
+ column: error.location?.column ?? 0,
+ length: error.location?.length ?? 0,
+ text: error.text ?? '',
+ },
+ severity: 'error',
+ });
+ } catch {
+ // Rspack may throw on invalid line & column when containing UTF-8.
+ // We catch it up here.
+ this.emitError(new Error(error.text));
+ }
+ } else {
+ // Webpack or legacy Rspack
+ this.emitError(new Error(error.text));
+ }
+ }
+ this.callback(new Error('react-transform failed'));
+
+ return;
+ }
+
+ for (const warning of result.warnings) {
+ if (this.experiments?.emitDiagnostic) {
+ // Rspack with `emitDiagnostic` API
+ try {
+ this.experiments.emitDiagnostic({
+ message: warning.text!,
+ sourceCode: content,
+ location: {
+ line: warning.location?.line ?? 1,
+ column: warning.location?.column ?? 0,
+ length: warning.location?.length ?? 0,
+ text: warning.text ?? '',
+ },
+ severity: 'warning',
+ });
+ } catch {
+ // Rspack may throw on invalid line & column when containing UTF-8.
+ // We catch it up here.
+ this.emitWarning(new Error(warning.text));
+ }
+ } else {
+ // Webpack or legacy Rspack
+ this.emitWarning(new Error(warning.text));
+ }
+ }
+ this.callback(null, result.code, result.map);
+}
+
+export default testingLoader;
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 4b733b4737..87743986e4 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -20,6 +20,10 @@ catalogs:
'@rspack/test-tools':
specifier: 1.4.11
version: 1.4.11
+ rstest:
+ '@rstest/core':
+ specifier: 0.0.10
+ version: 0.0.10
overrides:
'@rspack/core': 1.4.11
@@ -64,6 +68,9 @@ importers:
'@rspack/core':
specifier: 1.4.11
version: 1.4.11(@swc/helpers@0.5.17)
+ '@rstest/core':
+ specifier: catalog:rstest
+ version: 0.0.10(jsdom@26.1.0)
'@svitejs/changesets-changelog-github-compact':
specifier: ^1.2.0
version: 1.2.0
@@ -255,9 +262,15 @@ importers:
'@lynx-js/react':
specifier: workspace:*
version: link:..
+ '@lynx-js/react-rsbuild-plugin':
+ specifier: workspace:*
+ version: link:../../rspeedy/plugin-react
'@lynx-js/testing-environment':
specifier: workspace:*
version: link:../../testing-library/testing-environment
+ '@rsbuild/core':
+ specifier: catalog:rsbuild
+ version: 1.4.15
'@testing-library/dom':
specifier: ^10.4.1
version: 10.4.1
@@ -389,6 +402,9 @@ importers:
'@rsbuild/plugin-type-check':
specifier: 1.2.4
version: 1.2.4(@rsbuild/core@1.4.15)(@rspack/core@1.4.11(@swc/helpers@0.5.17))(typescript@5.9.2)
+ '@rstest/core':
+ specifier: catalog:rstest
+ version: 0.0.10(jsdom@26.1.0)
packages/rspeedy/plugin-qrcode:
devDependencies:
@@ -437,6 +453,9 @@ importers:
background-only:
specifier: workspace:^
version: link:../../background-only
+ tiny-invariant:
+ specifier: ^1.3.3
+ version: 1.3.3
devDependencies:
'@lynx-js/react':
specifier: workspace:*
@@ -2493,6 +2512,11 @@ packages:
engines: {node: '>=16.10.0'}
hasBin: true
+ '@rsbuild/core@1.4.9':
+ resolution: {integrity: sha512-LvF0YQ2IQf6ddDQQCkWxgPxHJFrZT8bvwwsHYo8K9g8KJTlrrstMV85lU3DROaH5tm98jN3zYZIOCbqQzklx5g==}
+ engines: {node: '>=16.10.0'}
+ hasBin: true
+
'@rsbuild/core@1.5.0-beta.4':
resolution: {integrity: sha512-h1jqpjZunalsdxTcJCbY8DovLu6F4MQgsYdZyxDjUp0xuggQayi1tpcE6MhLs3WWa077g+LZ2Am4gKt/pl/W9Q==}
engines: {node: '>=18.12.0'}
@@ -2760,6 +2784,19 @@ packages:
'@rstack-dev/doc-ui@1.10.10':
resolution: {integrity: sha512-ShZpNViiBKMYIT28vFmYP/SPp/oxhP4fCXTVYWWatas6AxJ1AIMunnWAl930u2YnP4D7UgxHopleMx/cLGhTwA==}
+ '@rstest/core@0.0.10':
+ resolution: {integrity: sha512-ytJ9LTCWARtBbt/+4RLV/lRgPCoXto0pazQ5lRdtv6M2gUvCZnIJKW2J2ioqQaVv0LXn5gqCCopoQYYFFE7eBQ==}
+ engines: {node: '>=18.0.0'}
+ hasBin: true
+ peerDependencies:
+ happy-dom: '*'
+ jsdom: '*'
+ peerDependenciesMeta:
+ happy-dom:
+ optional: true
+ jsdom:
+ optional: true
+
'@rtsao/scc@1.1.0':
resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==}
@@ -2840,8 +2877,8 @@ packages:
'@sinclair/typebox@0.27.8':
resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==}
- '@sinclair/typebox@0.34.37':
- resolution: {integrity: sha512-2TRuQVgQYfy+EzHRTIvkhv2ADEouJ2xNS/Vq+W5EuuewBdOrvATvljZTxHWZSTYr2sTjTHpGvucaGAt67S2akw==}
+ '@sinclair/typebox@0.34.38':
+ resolution: {integrity: sha512-HpkxMmc2XmZKhvaKIZZThlHmx1L0I/V1hWK1NubtlFnr6ZqdiOpV72TKudZUNQjZNsyDBay72qFEhEvb+bcwcA==}
'@socket.io/component-emitter@3.1.2':
resolution: {integrity: sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==}
@@ -3663,6 +3700,9 @@ packages:
resolution: {integrity: sha512-/hls/a309aZCc0itqP6uhoR+5DsKSlJVfB8Opd2BY9Ndghs84IScTunlyidyF4r2Xe3lQttnfBNIDjaNpj6mTw==}
hasBin: true
+ birpc@2.5.0:
+ resolution: {integrity: sha512-VSWO/W6nNQdyP520F1mhf+Lc2f8pjGQOtoHHm7Ze8Go1kX7akpVIrtTa0fn+HB0QJEDVacl6aO08YE0PgXfdnQ==}
+
bl@4.1.0:
resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==}
@@ -3769,9 +3809,9 @@ packages:
ccount@2.0.1:
resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==}
- chai@5.2.0:
- resolution: {integrity: sha512-mCuXncKXk5iCLhfhwTc0izo0gtEmpz5CtG2y8GiOINBlMVS6v8TMRc5TaLWKS6692m9+dVVfzgeVxR5UxWHTYw==}
- engines: {node: '>=12'}
+ chai@5.2.1:
+ resolution: {integrity: sha512-5nFxhUrX0PqtyogoYOA8IPswy5sZFTOsBFl/9bNsmDLgsxYTzSZQJDPppDnZPTQbzSEm0hqGjWPzRemQCYbD6A==}
+ engines: {node: '>=18'}
chalk@4.1.2:
resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
@@ -3984,6 +4024,9 @@ packages:
core-js-compat@3.45.0:
resolution: {integrity: sha512-gRoVMBawZg0OnxaVv3zpqLLxaHmsubEGyTnqdpI/CEBvX4JadI1dMSHxagThprYRtSVbuQxvi6iUatdPxohHpA==}
+ core-js@3.44.0:
+ resolution: {integrity: sha512-aFCtd4l6GvAXwVEh3XbbVqJGHDJt0OZRa+5ePGx3LLwi12WfexqQxcsohb2wgsa/92xtl19Hd66G/L+TaAxDMw==}
+
core-js@3.45.0:
resolution: {integrity: sha512-c2KZL9lP4DjkN3hk/an4pWn5b5ZefhRJnAc42n6LJ19kSnbeRbdQZE5dSeE2LBol1OwJD3X1BQvFTAsa8ReeDA==}
@@ -9086,7 +9129,7 @@ snapshots:
'@jest/schemas@30.0.5':
dependencies:
- '@sinclair/typebox': 0.34.37
+ '@sinclair/typebox': 0.34.38
'@jest/transform@29.7.0':
dependencies:
@@ -9542,6 +9585,14 @@ snapshots:
core-js: 3.45.0
jiti: 2.5.1
+ '@rsbuild/core@1.4.9':
+ dependencies:
+ '@rspack/core': 1.4.11(@swc/helpers@0.5.17)
+ '@rspack/lite-tapable': 1.0.1
+ '@swc/helpers': 0.5.17
+ core-js: 3.44.0
+ jiti: 2.5.1
+
'@rsbuild/core@1.5.0-beta.4':
dependencies:
'@rspack/core': 1.4.11(@swc/helpers@0.5.17)
@@ -10007,6 +10058,20 @@ snapshots:
- react
- react-dom
+ '@rstest/core@0.0.10(jsdom@26.1.0)':
+ dependencies:
+ '@rsbuild/core': 1.4.9
+ '@types/chai': 5.2.2
+ '@vitest/expect': 3.2.4
+ '@vitest/snapshot': 3.2.4
+ birpc: 2.5.0
+ chai: 5.2.1
+ pathe: 2.0.3
+ std-env: 3.9.0
+ tinypool: 1.1.1
+ optionalDependencies:
+ jsdom: 26.1.0
+
'@rtsao/scc@1.1.0': {}
'@rushstack/node-core-library@4.0.2(@types/node@24.3.0)':
@@ -10126,7 +10191,7 @@ snapshots:
'@sinclair/typebox@0.27.8': {}
- '@sinclair/typebox@0.34.37': {}
+ '@sinclair/typebox@0.34.38': {}
'@socket.io/component-emitter@3.1.2': {}
@@ -10662,7 +10727,7 @@ snapshots:
'@types/chai': 5.2.2
'@vitest/spy': 3.2.4
'@vitest/utils': 3.2.4
- chai: 5.2.0
+ chai: 5.2.1
tinyrainbow: 2.0.0
'@vitest/mocker@3.2.4(vite@5.4.2(@types/node@24.3.0)(sass-embedded@1.90.0)(sass@1.90.0)(terser@5.31.6))':
@@ -11045,6 +11110,8 @@ snapshots:
binaryen@123.0.0: {}
+ birpc@2.5.0: {}
+
bl@4.1.0:
dependencies:
buffer: 5.7.1
@@ -11169,7 +11236,7 @@ snapshots:
ccount@2.0.1: {}
- chai@5.2.0:
+ chai@5.2.1:
dependencies:
assertion-error: 2.0.1
check-error: 2.1.1
@@ -11365,6 +11432,8 @@ snapshots:
dependencies:
browserslist: 4.25.2
+ core-js@3.44.0: {}
+
core-js@3.45.0: {}
core-util-is@1.0.3: {}
@@ -16146,7 +16215,7 @@ snapshots:
'@vitest/snapshot': 3.2.4
'@vitest/spy': 3.2.4
'@vitest/utils': 3.2.4
- chai: 5.2.0
+ chai: 5.2.1
debug: 4.4.1
expect-type: 1.2.1
magic-string: 0.30.17
diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml
index 6e3bd5dc9e..37d10fbffd 100644
--- a/pnpm-workspace.yaml
+++ b/pnpm-workspace.yaml
@@ -38,6 +38,10 @@ catalogs:
"@rspack/core": "1.4.11"
"@rspack/test-tools": "1.4.11"
+ # Rstest monorepo packages
+ rstest:
+ "@rstest/core": "0.0.10"
+
overrides:
"@rspack/core": "$@rspack/core"
"@rsbuild/core>@rspack/core": "$@rspack/core"