diff --git a/.changeset/small-onions-hear.md b/.changeset/small-onions-hear.md new file mode 100644 index 0000000000..97bc6bc0c8 --- /dev/null +++ b/.changeset/small-onions-hear.md @@ -0,0 +1,7 @@ +--- +"@lynx-js/react-rsbuild-plugin": patch +--- + +Better [zustand](https://github.com/pmndrs/zustand) support by creating an alias for `use-sync-external-store`. + +See [lynx-family/lynx-stack#893](https://github.com/lynx-family/lynx-stack/issues/893) for more details. diff --git a/packages/rspeedy/plugin-react/package.json b/packages/rspeedy/plugin-react/package.json index f05398d895..5be06e1bce 100644 --- a/packages/rspeedy/plugin-react/package.json +++ b/packages/rspeedy/plugin-react/package.json @@ -43,6 +43,7 @@ "@lynx-js/react-webpack-plugin": "workspace:*", "@lynx-js/runtime-wrapper-webpack-plugin": "workspace:*", "@lynx-js/template-webpack-plugin": "workspace:*", + "@lynx-js/use-sync-external-store": "workspace:*", "background-only": "workspace:^" }, "devDependencies": { diff --git a/packages/rspeedy/plugin-react/src/backgroundOnly.ts b/packages/rspeedy/plugin-react/src/backgroundOnly.ts index f8a385774e..adeb21c345 100644 --- a/packages/rspeedy/plugin-react/src/backgroundOnly.ts +++ b/packages/rspeedy/plugin-react/src/backgroundOnly.ts @@ -6,7 +6,6 @@ import { fileURLToPath } from 'node:url' import type { RsbuildPluginAPI } from '@rsbuild/core' -import { createLazyResolver } from '@lynx-js/react-alias-rsbuild-plugin' import { LAYERS } from '@lynx-js/react-webpack-plugin' const DETECT_IMPORT_ERROR = 'react:detect-import-error' @@ -17,22 +16,15 @@ const ALIAS_BACKGROUND_ONLY_BACKGROUND = export function applyBackgroundOnly( api: RsbuildPluginAPI, ): void { - const __dirname = path.dirname(fileURLToPath(import.meta.url)) + api.modifyBundlerChain(async chain => { + const __dirname = path.dirname(fileURLToPath(import.meta.url)) - const backgroundResolve = createLazyResolver( - __dirname, - ['import'], - ) - const mainThreadResolve = createLazyResolver( - __dirname, - ['lepus'], - ) + const { resolve, resolveMainThread } = await import('./resolve.js') - api.modifyBundlerChain(async chain => { - const backgroundOnly = { - background: await backgroundResolve('background-only'), - mainThread: await mainThreadResolve('background-only'), - } + const [backgroundOnly, backgroundOnlyMainThread] = await Promise.all([ + resolve('background-only'), + resolveMainThread('background-only'), + ]) chain .module @@ -42,7 +34,7 @@ export function applyBackgroundOnly( .alias .set( 'background-only$', - backgroundOnly.mainThread, + backgroundOnlyMainThread, ) chain @@ -53,13 +45,13 @@ export function applyBackgroundOnly( .alias .set( 'background-only$', - backgroundOnly.background, + backgroundOnly, ) chain .module .rule(DETECT_IMPORT_ERROR) - .test(backgroundOnly.mainThread) + .test(backgroundOnlyMainThread) .issuerLayer(LAYERS.MAIN_THREAD) .use(DETECT_IMPORT_ERROR) .loader(path.resolve(__dirname, 'loaders/invalid-import-error-loader')) diff --git a/packages/rspeedy/plugin-react/src/pluginReactLynx.ts b/packages/rspeedy/plugin-react/src/pluginReactLynx.ts index af90fd158b..857074f6d5 100644 --- a/packages/rspeedy/plugin-react/src/pluginReactLynx.ts +++ b/packages/rspeedy/plugin-react/src/pluginReactLynx.ts @@ -30,6 +30,7 @@ import { applyLoaders } from './loaders.js' import { applyRefresh } from './refresh.js' import { applySplitChunksRule } from './splitChunks.js' import { applySWC } from './swc.js' +import { applyUseSyncExternalStore } from './useSyncExternalStore.js' import { validateConfig } from './validate.js' /** @@ -374,6 +375,7 @@ export function pluginReactLynx( applyRefresh(api) applySplitChunksRule(api) applySWC(api) + applyUseSyncExternalStore(api) api.modifyRsbuildConfig((config, { mergeRsbuildConfig }) => { const userConfig = api.getRsbuildConfig('original') diff --git a/packages/rspeedy/plugin-react/src/resolve.ts b/packages/rspeedy/plugin-react/src/resolve.ts new file mode 100644 index 0000000000..aa23a57217 --- /dev/null +++ b/packages/rspeedy/plugin-react/src/resolve.ts @@ -0,0 +1,21 @@ +// 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 path from 'node:path' +import { fileURLToPath } from 'node:url' + +import { createLazyResolver } from '@lynx-js/react-alias-rsbuild-plugin' + +const __dirname = path.dirname(fileURLToPath(import.meta.url)) + +export const resolve: (request: string) => Promise = createLazyResolver( + __dirname, + ['import'], +) + +export const resolveMainThread: (request: string) => Promise = + createLazyResolver( + __dirname, + ['lepus'], + ) diff --git a/packages/rspeedy/plugin-react/src/useSyncExternalStore.ts b/packages/rspeedy/plugin-react/src/useSyncExternalStore.ts new file mode 100644 index 0000000000..407b31fedd --- /dev/null +++ b/packages/rspeedy/plugin-react/src/useSyncExternalStore.ts @@ -0,0 +1,28 @@ +// 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 applyUseSyncExternalStore(api: RsbuildPluginAPI): void { + api.modifyBundlerChain(async chain => { + const { resolve } = await import('./resolve.js') + const useSyncExternalStoreEntries = [ + 'use-sync-external-store', + 'use-sync-external-store/with-selector', + 'use-sync-external-store/shim', + 'use-sync-external-store/shim/with-selector', + ] + + await Promise.all( + useSyncExternalStoreEntries.map(entry => + resolve(`@lynx-js/${entry}`).then(value => { + chain + .resolve + .alias + .set(`${entry}$`, value) + }) + ), + ) + }) +} diff --git a/packages/rspeedy/plugin-react/test/config.test.ts b/packages/rspeedy/plugin-react/test/config.test.ts index 91865d0d35..4a2e635080 100644 --- a/packages/rspeedy/plugin-react/test/config.test.ts +++ b/packages/rspeedy/plugin-react/test/config.test.ts @@ -122,6 +122,22 @@ describe('Config', () => { 'preact/compat/scheduler$', expect.stringContaining('/preact/compat/scheduler.mjs'), ) + expect(config.resolve.alias).toHaveProperty( + 'use-sync-external-store$', + expect.stringContaining('/use-sync-external-store/index.js'), + ) + expect(config.resolve.alias).toHaveProperty( + 'use-sync-external-store/with-selector$', + expect.stringContaining('/use-sync-external-store/with-selector.js'), + ) + expect(config.resolve.alias).toHaveProperty( + 'use-sync-external-store/shim$', + expect.stringContaining('/use-sync-external-store/index.js'), + ) + expect(config.resolve.alias).toHaveProperty( + 'use-sync-external-store/shim/with-selector$', + expect.stringContaining('/use-sync-external-store/with-selector.js'), + ) }) test('alias with production', async () => { @@ -176,6 +192,23 @@ describe('Config', () => { expect(config.resolve.alias).not.toHaveProperty( '@lynx-js/react/refresh$', ) + + expect(config.resolve.alias).toHaveProperty( + 'use-sync-external-store$', + expect.stringContaining('/use-sync-external-store/index.js'), + ) + expect(config.resolve.alias).toHaveProperty( + 'use-sync-external-store/with-selector$', + expect.stringContaining('/use-sync-external-store/with-selector.js'), + ) + expect(config.resolve.alias).toHaveProperty( + 'use-sync-external-store/shim$', + expect.stringContaining('/use-sync-external-store/index.js'), + ) + expect(config.resolve.alias).toHaveProperty( + 'use-sync-external-store/shim/with-selector$', + expect.stringContaining('/use-sync-external-store/with-selector.js'), + ) }) test('extensionAlias with tsConfig', async () => { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 93e9d0174e..b136688305 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -418,6 +418,9 @@ importers: '@lynx-js/template-webpack-plugin': specifier: workspace:* version: link:../../webpack/template-webpack-plugin + '@lynx-js/use-sync-external-store': + specifier: workspace:* + version: link:../../use-sync-external-store background-only: specifier: workspace:^ version: link:../../background-only