Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions code/core/src/common/utils/load-main-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,9 @@ export async function loadMainConfig({
if (!(e instanceof Error)) {
throw e;
}
if (e.message.includes('require is not defined')) {
if (e.message.includes('not defined in ES module scope')) {
logger.info(
'Loading main config failed, trying a temporary fix, Please ensure the main config is valid ESM'
'Loading main config failed as the file does not seem to be valid ESM. Trying a temporary fix, please ensure the main config is valid ESM.'
);
const comment =
'// end of Storybook 10 migration assistant header, you can delete the above code';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { readFile, writeFile } from 'node:fs/promises';

import { beforeEach, describe, expect, it, vi } from 'vitest';

import { bannerComment } from '../helpers/mainConfigFile';
import { bannerComment, containsDirnameUsage } from '../helpers/mainConfigFile';
import { fixFauxEsmRequire } from './fix-faux-esm-require';

vi.mock('node:fs/promises', async (importOriginal) => ({
Expand Down Expand Up @@ -76,7 +76,7 @@ describe('fix-faux-esm-require', () => {
expect(result).toBeNull();
});

it('should return true if file is ESM with require usage', async () => {
it('should return BannerConfig if file is ESM with require usage', async () => {
const contentWithRequire = `
import { addons } from '@storybook/addon-essentials';
const config = require('./some-config');
Expand All @@ -91,7 +91,33 @@ describe('fix-faux-esm-require', () => {
mainConfigPath: 'main.js',
} as any);

expect(result).toBe(true);
expect(result).toEqual({
hasRequireUsage: true,
hasUnderscoreDirname: false,
hasUnderscoreFilename: false,
});
});

it('should return BannerConfig if file is ESM with __dirname usage but no definition', async () => {
const contentWithDirname = `
import { addons } from '@storybook/addon-essentials';
const configPath = path.join(__dirname, 'config.js');
export default {
addons: ['@storybook/addon-essentials'],
};
`;

mockReadFile.mockResolvedValue(contentWithDirname);

const result = await fixFauxEsmRequire.check({
mainConfigPath: 'main.js',
} as any);

expect(result).toEqual({
hasRequireUsage: false,
hasUnderscoreDirname: true,
hasUnderscoreFilename: false,
});
});

it('should detect TypeScript config files', async () => {
Expand All @@ -109,7 +135,96 @@ describe('fix-faux-esm-require', () => {
mainConfigPath: 'main.ts',
} as any);

expect(result).toBe(true);
expect(result).toEqual({
hasRequireUsage: true,
hasUnderscoreDirname: false,
hasUnderscoreFilename: false,
});
});
});

describe('containsDirnameUsage', () => {
it('should detect __dirname in actual code', () => {
const content = `
const path = require('path');
const configPath = path.join(__dirname, 'config.js');
`;
expect(containsDirnameUsage(content)).toBe(true);
});

it('should not detect __dirname in double-quoted strings', () => {
const content = `
const message = "This is __dirname in a string";
const another = "path/to/__dirname/file.js";
`;
expect(containsDirnameUsage(content)).toBe(false);
});

it('should not detect __dirname in single-quoted strings', () => {
const content = `
const message = 'This is __dirname in a string';
const another = 'path/to/__dirname/file.js';
`;
expect(containsDirnameUsage(content)).toBe(false);
});

it('should not detect __dirname in backtick strings', () => {
const content = `
const message = \`This is __dirname in a string\`;
const another = \`path/to/__dirname/file.js\`;
`;
expect(containsDirnameUsage(content)).toBe(false);
});

it('should not detect __dirname in single-line comments', () => {
const content = `
// This is __dirname in a comment
const config = { addons: [] };
`;
expect(containsDirnameUsage(content)).toBe(false);
});

it('should not detect __dirname in multi-line comments', () => {
const content = `
/* This is __dirname in a
multi-line comment */
const config = { addons: [] };
`;
expect(containsDirnameUsage(content)).toBe(false);
});

it('should handle // inside strings correctly', () => {
const content = `
const url = "https://example.com//path";
const config = { addons: [] };
`;
expect(containsDirnameUsage(content)).toBe(false);
});

it('should detect __dirname when it appears after string with //', () => {
const content = `
const url = "https://example.com//path";
const configPath = path.join(__dirname, 'config.js');
`;
expect(containsDirnameUsage(content)).toBe(true);
});

it('should handle escaped quotes in strings', () => {
const content = `
const message = "This is \\"__dirname\\" in a string";
const config = { addons: [] };
`;
expect(containsDirnameUsage(content)).toBe(false);
});

it('should handle mixed quote types', () => {
const content = `
const double = "This is __dirname in double quotes";
const single = 'This is __dirname in single quotes';
const backtick = \`This is __dirname in backticks\`;
const actual = path.join(__dirname, 'config.js');
`;
expect(containsDirnameUsage(content)).toBe(true);
});
});

Expand All @@ -128,6 +243,11 @@ describe('fix-faux-esm-require', () => {
await fixFauxEsmRequire.run({
dryRun: false,
mainConfigPath: 'main.js',
result: {
hasRequireUsage: true,
hasUnderscoreDirname: false,
hasUnderscoreFilename: false,
},
} as any);

expect(mockWriteFile).toHaveBeenCalledWith(
Expand All @@ -136,13 +256,181 @@ describe('fix-faux-esm-require', () => {
);
});

it('should add __dirname support when needed', async () => {
const originalContent = `
import { addons } from '@storybook/addon-essentials';
const config = require('./some-config');
const configPath = path.join(__dirname, 'config.js');
export default {
addons: ['@storybook/addon-essentials'],
};
`;

mockReadFile.mockResolvedValue(originalContent);

await fixFauxEsmRequire.run({
dryRun: false,
mainConfigPath: 'main.js',
result: {
hasRequireUsage: true,
hasUnderscoreDirname: true,
hasUnderscoreFilename: false,
},
} as any);

const writtenContent = mockWriteFile.mock.calls[0][1];
expect(writtenContent).toMatchInlineSnapshot(`
"
import { fileURLToPath } from "node:url";
import { dirname } from "node:path";
import { createRequire } from "node:module";
import { addons } from '@storybook/addon-essentials';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const require = createRequire(import.meta.url);
const config = require('./some-config');
const configPath = path.join(__dirname, 'config.js');
export default {
addons: ['@storybook/addon-essentials'],
};
"
`);
});
Comment thread
ndelangen marked this conversation as resolved.

it('should not add duplicate imports when they already exist', async () => {
const originalContent = `
import { dirname } from "node:path";
import { addons } from '@storybook/addon-essentials';
const config = require('./some-config');
const configPath = path.join(__dirname, 'config.js');
export default {
addons: ['@storybook/addon-essentials'],
};
`;

mockReadFile.mockResolvedValue(originalContent);

await fixFauxEsmRequire.run({
dryRun: false,
mainConfigPath: 'main.js',
result: {
hasRequireUsage: true,
hasUnderscoreDirname: true,
hasUnderscoreFilename: false,
},
} as any);

const writtenContent = mockWriteFile.mock.calls[0][1];

expect(writtenContent).toMatchInlineSnapshot(`
"
import { fileURLToPath } from "node:url";
import { createRequire } from "node:module";
import { dirname } from "node:path";
import { addons } from '@storybook/addon-essentials';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const require = createRequire(import.meta.url);
const config = require('./some-config');
const configPath = path.join(__dirname, 'config.js');
export default {
addons: ['@storybook/addon-essentials'],
};
"
`);
});

it('should not write file in dry run mode', async () => {
await fixFauxEsmRequire.run({
dryRun: true,
mainConfigPath: 'main.js',
result: {
hasRequireUsage: true,
hasUnderscoreDirname: false,
hasUnderscoreFilename: false,
},
} as any);

expect(mockWriteFile).not.toHaveBeenCalled();
});

it('should not add duplicate __filename/__dirname declarations when they already exist', async () => {
const originalContent = `
import { addons } from '@storybook/addon-essentials';
const __filename = 'existing-filename';
const __dirname = 'existing-dirname';
const config = require('./some-config');
const configPath = path.join(__dirname, 'config.js');
export default {
addons: ['@storybook/addon-essentials'],
};
`;

mockReadFile.mockResolvedValue(originalContent);

await fixFauxEsmRequire.run({
dryRun: false,
mainConfigPath: 'main.js',
result: {
hasRequireUsage: true,
hasUnderscoreDirname: true,
hasUnderscoreFilename: true,
},
} as any);

const writtenContent = mockWriteFile.mock.calls[0][1];
expect(writtenContent).toMatchInlineSnapshot(`
"
import { fileURLToPath } from "node:url";
import { dirname } from "node:path";
import { createRequire } from "node:module";
import { addons } from '@storybook/addon-essentials';
const require = createRequire(import.meta.url);
const __filename = 'existing-filename';
const __dirname = 'existing-dirname';
const config = require('./some-config');
const configPath = path.join(__dirname, 'config.js');
export default {
addons: ['@storybook/addon-essentials'],
};
"
`);
});

it('should not add duplicate require declaration when it already exists', async () => {
const originalContent = `
import { addons } from '@storybook/addon-essentials';
const require = createRequire(import.meta.url);
const config = require('./some-config');
export default {
addons: ['@storybook/addon-essentials'],
};
`;

mockReadFile.mockResolvedValue(originalContent);

await fixFauxEsmRequire.run({
dryRun: false,
mainConfigPath: 'main.js',
result: {
hasRequireUsage: true,
hasUnderscoreDirname: false,
hasUnderscoreFilename: false,
},
} as any);

const writtenContent = mockWriteFile.mock.calls[0][1];
expect(writtenContent).toMatchInlineSnapshot(`
"
import { createRequire } from "node:module";
import { addons } from '@storybook/addon-essentials';
const require = createRequire(import.meta.url);
const config = require('./some-config');
export default {
addons: ['@storybook/addon-essentials'],
};
"
`);
});
});
});
Loading