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(` + + + + + + + + + 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(` + + + + + + + + + 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"