Skip to content

Commit 10fcde7

Browse files
authored
feat: compatible with Rspack 2.x, Rsbuild 2.x, Modern.js 3.x (#439)
1 parent 1948283 commit 10fcde7

File tree

40 files changed

+2303
-3147
lines changed

40 files changed

+2303
-3147
lines changed

packages/addon-modernjs/package.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,16 +47,16 @@
4747
"rslog": "1.3.2"
4848
},
4949
"devDependencies": {
50-
"@modern-js/app-tools": "^2.70.4",
51-
"@rsbuild/core": "^1.7.2",
50+
"@modern-js/app-tools": "^3.0.2",
51+
"@rsbuild/core": "^2.0.0-beta.4",
5252
"@types/node": "^22.0.0",
5353
"storybook": "10.2.3",
5454
"storybook-builder-rsbuild": "workspace:*",
5555
"typescript": "^5.9.3"
5656
},
5757
"peerDependencies": {
58-
"@modern-js/app-tools": "^2.67.9",
59-
"@rsbuild/core": "^1.5.0",
58+
"@modern-js/app-tools": "^2.67.9 || ^3.0.0-0",
59+
"@rsbuild/core": "^1.5.0 || ^2.0.0-0",
6060
"storybook-builder-rsbuild": "*"
6161
},
6262
"peerDependenciesMeta": {

packages/addon-modernjs/src/preset.ts

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import type {
1616
import type { AddonOptions } from './types'
1717

1818
type BaseOptions = Parameters<RsbuildFinal>[1]
19+
type BuilderAdapterParams = Parameters<typeof builderPluginAdapterBasic>[0]
1920

2021
const MODERN_META_NAME = 'modern-js'
2122
const MODERN_CONFIG_FILE = 'modern.config.ts'
@@ -64,13 +65,12 @@ export const rsbuildFinal: StorybookConfigRsbuild['rsbuildFinal'] = async (
6465
await checkDependency()
6566

6667
const cwd = process.cwd()
67-
const { config: resolveConfig, getAppContext } = await createStorybookOptions<
68-
AppTools<'shared'>
69-
>({
70-
cwd,
71-
configFile: options.configPath || MODERN_CONFIG_FILE,
72-
metaName: MODERN_META_NAME,
73-
})
68+
const { config: resolveConfig, getAppContext } =
69+
await createStorybookOptions<AppTools>({
70+
cwd,
71+
configFile: options.configPath || MODERN_CONFIG_FILE,
72+
metaName: MODERN_META_NAME,
73+
})
7474

7575
const nonStandardConfig = {
7676
...resolveConfig,
@@ -85,9 +85,9 @@ export const rsbuildFinal: StorybookConfigRsbuild['rsbuildFinal'] = async (
8585
)
8686

8787
const appContext = getAppContext()
88-
const adapterParams = {
89-
appContext,
90-
normalizedConfig: resolveConfig as AppNormalizedConfig<'rspack'>,
88+
const adapterParams: BuilderAdapterParams = {
89+
appContext: appContext as BuilderAdapterParams['appContext'],
90+
normalizedConfig: resolveConfig as AppNormalizedConfig,
9191
}
9292

9393
// Inject the extra rsbuild plugins

packages/addon-rslib/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,15 +42,15 @@
4242
"prepare": "pnpm run build"
4343
},
4444
"devDependencies": {
45-
"@rsbuild/core": "^1.7.2",
45+
"@rsbuild/core": "^2.0.0-beta.4",
4646
"@rslib/core": "^0.19.4",
4747
"@types/node": "^22.0.0",
4848
"storybook": "10.2.3",
4949
"storybook-builder-rsbuild": "workspace:*",
5050
"typescript": "^5.9.3"
5151
},
5252
"peerDependencies": {
53-
"@rsbuild/core": "^1.5.0",
53+
"@rsbuild/core": "^1.5.0 || ^2.0.0-0",
5454
"@rslib/core": ">= 0.1.1 || >= 0.2",
5555
"storybook-builder-rsbuild": "*"
5656
},

packages/builder-rsbuild/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@
6969
"util-deprecate": "^1.0.2"
7070
},
7171
"devDependencies": {
72-
"@rsbuild/core": "^1.7.2",
72+
"@rsbuild/core": "^2.0.0-beta.4",
7373
"@storybook/core-webpack": "10.2.3",
7474
"@types/fs-extra": "^11.0.4",
7575
"@types/node": "^22.0.0",
@@ -81,7 +81,7 @@
8181
"typescript": "^5.9.3"
8282
},
8383
"peerDependencies": {
84-
"@rsbuild/core": "^1.5.0",
84+
"@rsbuild/core": "^1.5.0 || ^2.0.0-0",
8585
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
8686
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
8787
"storybook": "^10.1.0"
Lines changed: 293 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,293 @@
1+
import { isAbsolute, relative } from 'node:path'
2+
3+
const isRecord = (val: unknown): val is Record<string, unknown> =>
4+
val != null && typeof val === 'object' && Array.isArray(val) === false
5+
6+
// Keep this aligned with Chromatic TurboSnap's minimal stats contract:
7+
// https://github.com/chromaui/chromatic-cli/blob/1426eaec411747af076deca87e7b30c26a9f699a/node-src/types.ts#L377-L390
8+
type ChromaticReason = {
9+
moduleName: string
10+
}
11+
12+
type ChromaticModule = {
13+
id: string | number | null
14+
name: string
15+
modules?: Array<Pick<ChromaticModule, 'name'>>
16+
reasons?: ChromaticReason[]
17+
}
18+
19+
type ChromaticMinimalStatsJson = {
20+
modules: ChromaticModule[]
21+
}
22+
23+
type StatsToJson = (options?: unknown, forToString?: unknown) => unknown
24+
25+
type StatsWithToJson = {
26+
toJson?: unknown
27+
}
28+
29+
const toPosixPath = (filePath: string): string => filePath.split('\\').join('/')
30+
31+
const toNormalizedModulePath = (modulePath: unknown): string | null => {
32+
if (typeof modulePath !== 'string' || modulePath.length === 0) {
33+
return null
34+
}
35+
36+
if (isAbsolute(modulePath)) {
37+
return toPosixPath(relative(process.cwd(), modulePath))
38+
}
39+
40+
return toPosixPath(modulePath)
41+
}
42+
43+
const toAbsolutePathFromIdentifier = (identifier: unknown): string | null => {
44+
if (typeof identifier !== 'string' || identifier.length === 0) {
45+
return null
46+
}
47+
48+
const withoutLoaders = identifier.slice(identifier.lastIndexOf('!') + 1)
49+
const withoutHash = withoutLoaders.split('|')[0]
50+
const withoutQuery = withoutHash.split('?')[0]
51+
return isAbsolute(withoutQuery) ? withoutQuery : null
52+
}
53+
54+
const toReasonModuleName = (reason: Record<string, unknown>): string | null => {
55+
const moduleName =
56+
typeof reason.moduleName === 'string' ? reason.moduleName : null
57+
58+
if (
59+
typeof moduleName === 'string' &&
60+
/\s\+\s\d+\smodules?$/.test(moduleName) === false
61+
) {
62+
return moduleName
63+
}
64+
65+
const fallbackAbsolutePath =
66+
toAbsolutePathFromIdentifier(reason.resolvedModuleIdentifier) ??
67+
toAbsolutePathFromIdentifier(reason.moduleIdentifier)
68+
const fallbackModuleName = toNormalizedModulePath(fallbackAbsolutePath)
69+
70+
if (
71+
typeof fallbackModuleName === 'string' &&
72+
fallbackModuleName.includes('node_modules/') === false
73+
) {
74+
return fallbackModuleName
75+
}
76+
77+
return moduleName
78+
}
79+
80+
const isConcatenatedModuleName = (moduleName: string): boolean =>
81+
/\s\+\s\d+\smodules?$/.test(moduleName)
82+
83+
const toChromaticModule = (
84+
moduleInfo: Record<string, unknown>,
85+
): ChromaticModule | null => {
86+
const rawName = typeof moduleInfo.name === 'string' ? moduleInfo.name : null
87+
const normalizedNameForCondition = toNormalizedModulePath(
88+
moduleInfo.nameForCondition,
89+
)
90+
const name =
91+
moduleInfo.id == null && normalizedNameForCondition
92+
? normalizedNameForCondition
93+
: (rawName ?? normalizedNameForCondition)
94+
95+
if (typeof name !== 'string') {
96+
return null
97+
}
98+
99+
const normalizedOriginalId =
100+
typeof moduleInfo.id === 'string' || typeof moduleInfo.id === 'number'
101+
? moduleInfo.id
102+
: null
103+
const normalizedId = normalizedOriginalId ?? name
104+
105+
const normalizedReasons = Array.isArray(moduleInfo.reasons)
106+
? Array.from(
107+
new Set(
108+
moduleInfo.reasons
109+
.filter(isRecord)
110+
.map(toReasonModuleName)
111+
.filter(
112+
(moduleName): moduleName is string =>
113+
typeof moduleName === 'string',
114+
),
115+
),
116+
).map((moduleName) => ({
117+
moduleName,
118+
}))
119+
: []
120+
121+
const normalizedNestedModules = Array.isArray(moduleInfo.modules)
122+
? Array.from(
123+
new Set(
124+
moduleInfo.modules
125+
.filter(isRecord)
126+
.map((nestedModule) => toNormalizedModulePath(nestedModule.name))
127+
.filter(
128+
(nestedModuleName): nestedModuleName is string =>
129+
typeof nestedModuleName === 'string',
130+
),
131+
),
132+
).map((nestedModuleName) => ({
133+
name: nestedModuleName,
134+
}))
135+
: []
136+
137+
if (
138+
normalizedId === null &&
139+
normalizedReasons.length === 0 &&
140+
normalizedNestedModules.length === 0
141+
) {
142+
return null
143+
}
144+
145+
return {
146+
id: normalizedId,
147+
name,
148+
...(normalizedNestedModules.length > 0
149+
? { modules: normalizedNestedModules }
150+
: {}),
151+
...(normalizedReasons.length > 0 ? { reasons: normalizedReasons } : {}),
152+
}
153+
}
154+
155+
const collectModuleEntries = (
156+
moduleEntries: unknown[],
157+
collected: Record<string, unknown>[],
158+
): void => {
159+
for (const entry of moduleEntries) {
160+
if (!isRecord(entry)) {
161+
continue
162+
}
163+
164+
if (
165+
typeof entry.name === 'string' &&
166+
(entry.id !== undefined ||
167+
Array.isArray(entry.reasons) ||
168+
Array.isArray(entry.modules))
169+
) {
170+
collected.push(entry)
171+
}
172+
173+
if (Array.isArray(entry.children)) {
174+
collectModuleEntries(entry.children, collected)
175+
}
176+
177+
if (Array.isArray(entry.modules)) {
178+
collectModuleEntries(entry.modules, collected)
179+
}
180+
}
181+
}
182+
183+
export const withChromaticMinimalContract = (statsJson: unknown): unknown => {
184+
if (!isRecord(statsJson)) {
185+
return statsJson
186+
}
187+
188+
const modules = statsJson.modules
189+
if (!Array.isArray(modules)) {
190+
return statsJson
191+
}
192+
193+
const normalizedStatsJson = statsJson as ChromaticMinimalStatsJson &
194+
Record<string, unknown>
195+
const flattenedModules: Record<string, unknown>[] = []
196+
collectModuleEntries(modules, flattenedModules)
197+
198+
const additionalModules: ChromaticModule[] = []
199+
const visited = new Set<string>(
200+
modules
201+
.filter(isRecord)
202+
.map((moduleInfo) => {
203+
const name =
204+
typeof moduleInfo.name === 'string' ? moduleInfo.name : undefined
205+
const id =
206+
typeof moduleInfo.id === 'string' ||
207+
typeof moduleInfo.id === 'number' ||
208+
moduleInfo.id === null
209+
? moduleInfo.id
210+
: undefined
211+
212+
if (!name) {
213+
return null
214+
}
215+
216+
return `${String(id)}::${name}`
217+
})
218+
.filter((moduleKey): moduleKey is string => Boolean(moduleKey)),
219+
)
220+
221+
for (const moduleInfo of flattenedModules) {
222+
const normalizedModule = toChromaticModule(moduleInfo)
223+
if (!normalizedModule) {
224+
continue
225+
}
226+
227+
const moduleKey = `${String(normalizedModule.id)}::${normalizedModule.name}`
228+
if (visited.has(moduleKey)) {
229+
continue
230+
}
231+
232+
visited.add(moduleKey)
233+
additionalModules.push(normalizedModule)
234+
235+
if (
236+
isConcatenatedModuleName(normalizedModule.name) &&
237+
Array.isArray(normalizedModule.modules)
238+
) {
239+
for (const nestedModule of normalizedModule.modules) {
240+
const nestedModuleKey = `${nestedModule.name}::${nestedModule.name}`
241+
242+
if (visited.has(nestedModuleKey)) {
243+
continue
244+
}
245+
246+
visited.add(nestedModuleKey)
247+
additionalModules.push({
248+
id: nestedModule.name,
249+
name: nestedModule.name,
250+
...(Array.isArray(normalizedModule.reasons) &&
251+
normalizedModule.reasons.length > 0
252+
? { reasons: normalizedModule.reasons }
253+
: {}),
254+
})
255+
}
256+
}
257+
}
258+
259+
if (additionalModules.length > 0) {
260+
normalizedStatsJson.modules = [...modules, ...additionalModules]
261+
}
262+
263+
return normalizedStatsJson
264+
}
265+
266+
export const withStatsJsonCompat = <T extends StatsWithToJson>(stats: T): T => {
267+
const originalToJsonCandidate = stats.toJson
268+
if (typeof originalToJsonCandidate !== 'function') {
269+
return stats
270+
}
271+
const originalToJson = originalToJsonCandidate.bind(stats) as StatsToJson
272+
273+
const toJsonWithCompat: StatsToJson = (options, forToString) => {
274+
if (options == null || typeof options === 'object') {
275+
const statsJson = originalToJson(
276+
{
277+
all: true,
278+
modules: true,
279+
reasons: true,
280+
...(options as Record<string, unknown>),
281+
},
282+
forToString,
283+
)
284+
285+
return withChromaticMinimalContract(statsJson)
286+
}
287+
288+
return withChromaticMinimalContract(originalToJson(options, forToString))
289+
}
290+
291+
stats.toJson = toJsonWithCompat
292+
return stats
293+
}

0 commit comments

Comments
 (0)