-
-
Notifications
You must be signed in to change notification settings - Fork 10.1k
AutoMigration: Fix sb10 migration when main config contains require
#32558
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
24f1e61
4da70a6
2e9a5c9
58ca51f
414611f
5c96558
384cbd0
01c9e29
25ab43b
aadcab7
b8c0048
b21567f
47c04c6
0988bbe
b03194d
0c1fb28
e91cd92
f635045
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,148 @@ | ||
| import { readFile, writeFile } from 'node:fs/promises'; | ||
|
|
||
| import { beforeEach, describe, expect, it, vi } from 'vitest'; | ||
|
|
||
| import { bannerComment } from '../helpers/mainConfigFile'; | ||
| import { fixFauxEsmRequire } from './fix-faux-esm-require'; | ||
|
|
||
| vi.mock('node:fs/promises', async (importOriginal) => ({ | ||
| ...(await importOriginal<typeof import('node:fs/promises')>()), | ||
| readFile: vi.fn(), | ||
| writeFile: vi.fn(), | ||
| })); | ||
|
|
||
| describe('fix-faux-esm-require', () => { | ||
| const mockReadFile = vi.mocked(readFile); | ||
| const mockWriteFile = vi.mocked(writeFile); | ||
|
|
||
| beforeEach(() => { | ||
| vi.clearAllMocks(); | ||
| }); | ||
|
|
||
| describe('check', () => { | ||
| it('should return null if no mainConfigPath', async () => { | ||
| const result = await fixFauxEsmRequire.check({ | ||
| mainConfigPath: undefined, | ||
| } as any); | ||
|
|
||
| expect(result).toBeNull(); | ||
| }); | ||
|
|
||
| it('should return null if file is not ESM', async () => { | ||
| const contentWithoutESM = ` | ||
| const config = require('./config'); | ||
| module.exports = config; | ||
| `; | ||
|
|
||
| mockReadFile.mockResolvedValue(contentWithoutESM); | ||
|
|
||
| const result = await fixFauxEsmRequire.check({ | ||
| mainConfigPath: 'main.js', | ||
| } as any); | ||
|
|
||
| expect(result).toBeNull(); | ||
| }); | ||
|
|
||
| it('should return null if file already has require banner', async () => { | ||
| const contentWithBanner = ` | ||
| import { createRequire } from "node:module"; | ||
| ${bannerComment} | ||
| const config = require('./some-config'); | ||
| `; | ||
|
|
||
| mockReadFile.mockResolvedValue(contentWithBanner); | ||
|
|
||
| const result = await fixFauxEsmRequire.check({ | ||
| mainConfigPath: 'main.js', | ||
| } as any); | ||
|
|
||
| expect(result).toBeNull(); | ||
| }); | ||
|
|
||
| it('should return null if file does not contain require usage', async () => { | ||
| const contentWithoutRequire = ` | ||
| import { addons } from '@storybook/addon-essentials'; | ||
| export default { | ||
| addons: ['@storybook/addon-essentials'], | ||
| }; | ||
| `; | ||
|
|
||
| mockReadFile.mockResolvedValue(contentWithoutRequire); | ||
|
|
||
| const result = await fixFauxEsmRequire.check({ | ||
| mainConfigPath: 'main.js', | ||
| } as any); | ||
|
|
||
| expect(result).toBeNull(); | ||
| }); | ||
|
|
||
| it('should return true if file is ESM with require usage', async () => { | ||
| const contentWithRequire = ` | ||
| import { addons } from '@storybook/addon-essentials'; | ||
| const config = require('./some-config'); | ||
| export default { | ||
| addons: ['@storybook/addon-essentials'], | ||
| }; | ||
| `; | ||
|
|
||
| mockReadFile.mockResolvedValue(contentWithRequire); | ||
|
|
||
| const result = await fixFauxEsmRequire.check({ | ||
| mainConfigPath: 'main.js', | ||
| } as any); | ||
|
|
||
| expect(result).toBe(true); | ||
| }); | ||
|
|
||
| it('should detect TypeScript config files', async () => { | ||
| const contentWithRequire = ` | ||
| import { addons } from '@storybook/addon-essentials'; | ||
| const config = require('./some-config'); | ||
| export default { | ||
| addons: ['@storybook/addon-essentials'], | ||
| }; | ||
| `; | ||
|
|
||
| mockReadFile.mockResolvedValue(contentWithRequire); | ||
|
|
||
| const result = await fixFauxEsmRequire.check({ | ||
| mainConfigPath: 'main.ts', | ||
| } as any); | ||
|
|
||
| expect(result).toBe(true); | ||
| }); | ||
| }); | ||
|
|
||
| describe('run', () => { | ||
| it('should add require banner to file', async () => { | ||
| const originalContent = ` | ||
| import { addons } from '@storybook/addon-essentials'; | ||
| const config = require('./some-config'); | ||
| export default { | ||
| addons: ['@storybook/addon-essentials'], | ||
| }; | ||
| `; | ||
|
|
||
| mockReadFile.mockResolvedValue(originalContent); | ||
|
|
||
| await fixFauxEsmRequire.run({ | ||
| dryRun: false, | ||
| mainConfigPath: 'main.js', | ||
| } as any); | ||
|
|
||
| expect(mockWriteFile).toHaveBeenCalledWith( | ||
| 'main.js', | ||
| expect.stringContaining('import { createRequire } from "node:module"') | ||
| ); | ||
| }); | ||
|
|
||
| it('should not write file in dry run mode', async () => { | ||
| await fixFauxEsmRequire.run({ | ||
| dryRun: true, | ||
| mainConfigPath: 'main.js', | ||
| } as any); | ||
|
|
||
| expect(mockWriteFile).not.toHaveBeenCalled(); | ||
| }); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,65 @@ | ||
| import { readFile, writeFile } from 'node:fs/promises'; | ||
|
|
||
| import { dedent } from 'ts-dedent'; | ||
|
ndelangen marked this conversation as resolved.
|
||
|
|
||
| import { | ||
| bannerComment, | ||
| containsESMUsage, | ||
| containsRequireUsage, | ||
| getRequireBanner, | ||
| hasRequireBanner, | ||
| } from '../helpers/mainConfigFile'; | ||
|
ndelangen marked this conversation as resolved.
|
||
| import type { Fix } from '../types'; | ||
|
|
||
| export const fixFauxEsmRequire = { | ||
| id: 'fix-faux-esm-require', | ||
| link: 'https://storybook.js.org/docs/faq#how-do-i-fix-module-resolution-in-special-environments', | ||
|
|
||
| async check({ mainConfigPath }) { | ||
| if (!mainConfigPath) { | ||
| return null; | ||
| } | ||
|
|
||
| // Read the raw file content to check for ESM syntax and require usage | ||
| const content = await readFile(mainConfigPath, 'utf-8'); | ||
|
|
||
| const isESM = containsESMUsage(content); | ||
| const isWithRequire = containsRequireUsage(content); | ||
| const isWithBanner = hasRequireBanner(content); | ||
|
|
||
| // Check if the file is ESM format based on content | ||
| if (!isESM) { | ||
| return null; | ||
| } | ||
|
|
||
| // Check if the file already has the require banner | ||
| if (isWithBanner) { | ||
| return null; | ||
| } | ||
|
|
||
| // Check if the file contains require usage | ||
| if (!isWithRequire) { | ||
| return null; | ||
| } | ||
|
|
||
| return true; | ||
| }, | ||
|
|
||
| prompt() { | ||
| return dedent`Main config is ESM but uses 'require'. This will break in Storybook 10; Adding compatibility banner`; | ||
| }, | ||
|
|
||
| async run({ dryRun, mainConfigPath }) { | ||
| if (dryRun) { | ||
| return; | ||
| } | ||
|
|
||
| const content = await readFile(mainConfigPath, 'utf-8'); | ||
| const banner = getRequireBanner(); | ||
| const comment = bannerComment; | ||
|
|
||
| const newContent = [banner, comment, content].join('\n'); | ||
|
|
||
| await writeFile(mainConfigPath, newContent); | ||
| }, | ||
| } satisfies Fix<true>; | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -228,3 +228,49 @@ export const updateMainConfig = async ( | |||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| /** Check if a file is in ESM format based on its content */ | ||||||||||||||||||||||||||||||||||||||
| export function containsESMUsage(content: string): boolean { | ||||||||||||||||||||||||||||||||||||||
| // For .js/.ts files, check the content for ESM syntax | ||||||||||||||||||||||||||||||||||||||
| // Check for ESM syntax indicators (multiline aware) | ||||||||||||||||||||||||||||||||||||||
| const hasImportStatement = | ||||||||||||||||||||||||||||||||||||||
| /^\s*import\s+/m.test(content) || | ||||||||||||||||||||||||||||||||||||||
| /^\s*import\s*{/m.test(content) || | ||||||||||||||||||||||||||||||||||||||
| /^\s*import\s*\(/m.test(content); | ||||||||||||||||||||||||||||||||||||||
| const hasExportStatement = | ||||||||||||||||||||||||||||||||||||||
| /^\s*export\s+/m.test(content) || | ||||||||||||||||||||||||||||||||||||||
| /^\s*export\s*{/m.test(content) || | ||||||||||||||||||||||||||||||||||||||
| /^\s*export\s*default/m.test(content); | ||||||||||||||||||||||||||||||||||||||
| const hasImportMeta = /import\.meta/.test(content); | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| // If any ESM syntax is found, it's likely an ESM file | ||||||||||||||||||||||||||||||||||||||
| return hasImportStatement || hasExportStatement || hasImportMeta; | ||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+236
to
+247
|
||||||||||||||||||||||||||||||||||||||
| const hasImportStatement = | |
| /^\s*import\s+/m.test(content) || | |
| /^\s*import\s*{/m.test(content) || | |
| /^\s*import\s*\(/m.test(content); | |
| const hasExportStatement = | |
| /^\s*export\s+/m.test(content) || | |
| /^\s*export\s*{/m.test(content) || | |
| /^\s*export\s*default/m.test(content); | |
| const hasImportMeta = /import\.meta/.test(content); | |
| // If any ESM syntax is found, it's likely an ESM file | |
| return hasImportStatement || hasExportStatement || hasImportMeta; | |
| const esmRegex = /^\s*(?:import(?:\s+|\s*[{(])|export(?:\s+|\s*[{]|\s*default))/m; | |
| const hasESMSyntax = esmRegex.test(content); | |
| const hasImportMeta = /import\.meta/.test(content); | |
| // If any ESM syntax is found, it's likely an ESM file | |
| return hasESMSyntax || hasImportMeta; |
Uh oh!
There was an error while loading. Please reload this page.