|
1 | 1 | import fs from 'fs'; |
2 | 2 | import path from 'path'; |
| 3 | +import { createRequire } from 'module'; |
3 | 4 | import { StoryUIConfig, DEFAULT_CONFIG, createStoryUIConfig } from '../story-ui.config.js'; |
4 | 5 |
|
| 6 | +// Create require function for ESM compatibility |
| 7 | +const require = createRequire(import.meta.url); |
| 8 | + |
5 | 9 | // Config cache to prevent excessive loading |
6 | 10 | let cachedConfig: StoryUIConfig | null = null; |
7 | 11 | let configLoadTime: number = 0; |
@@ -37,47 +41,64 @@ export function loadUserConfig(): StoryUIConfig { |
37 | 41 | } else { |
38 | 42 | console.log(`Loading Story UI config from: ${configPath}`); |
39 | 43 | } |
40 | | - // Read and evaluate the config file |
41 | | - const configContent = fs.readFileSync(configPath, 'utf-8'); |
42 | | - |
43 | | - // Handle both CommonJS and ES modules |
44 | | - if (configContent.includes('module.exports') || configContent.includes('export default')) { |
45 | | - // Create a temporary module context |
46 | | - const module = { exports: {} }; |
47 | | - const exports = module.exports; |
48 | | - |
49 | | - // For ES modules, convert to CommonJS for evaluation |
50 | | - let evalContent = configContent; |
51 | | - if (configContent.includes('export default')) { |
52 | | - evalContent = configContent.replace(/export\s+default\s+/, 'module.exports = '); |
53 | | - } |
54 | 44 |
|
55 | | - // Evaluate the config file content |
56 | | - eval(evalContent); |
57 | | - |
58 | | - const userConfig = module.exports as any; |
59 | | - const config = createStoryUIConfig(userConfig.default || userConfig); |
60 | | - |
61 | | - // Detect Storybook framework if not already specified |
62 | | - if (!config.storybookFramework) { |
63 | | - const packageJsonPath = path.join(process.cwd(), 'package.json'); |
64 | | - if (fs.existsSync(packageJsonPath)) { |
65 | | - try { |
66 | | - const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8')); |
67 | | - const dependencies = { ...packageJson.dependencies, ...packageJson.devDependencies }; |
68 | | - config.storybookFramework = detectStorybookFramework(dependencies); |
69 | | - } catch (error) { |
70 | | - console.warn('Failed to detect Storybook framework:', error); |
71 | | - } |
| 45 | + // Use require() for safe config loading (no eval) |
| 46 | + // Clear require cache to ensure fresh config on reload |
| 47 | + const resolvedPath = path.resolve(configPath); |
| 48 | + delete require.cache[resolvedPath]; |
| 49 | + |
| 50 | + let userConfig: any; |
| 51 | + try { |
| 52 | + // eslint-disable-next-line @typescript-eslint/no-var-requires |
| 53 | + const loadedModule = require(resolvedPath); |
| 54 | + userConfig = loadedModule.default || loadedModule; |
| 55 | + } catch (requireError) { |
| 56 | + // If require() fails (e.g., ESM project with CJS config), fall back to parsing |
| 57 | + // This handles "type": "module" projects with module.exports configs |
| 58 | + const configContent = fs.readFileSync(configPath, 'utf-8'); |
| 59 | + |
| 60 | + // Try to extract the config object from CommonJS module.exports |
| 61 | + // Match module.exports = { ... } with potential trailing semicolons and whitespace |
| 62 | + const match = configContent.match(/module\.exports\s*=\s*(\{[\s\S]*\})\s*;*/); |
| 63 | + if (match) { |
| 64 | + try { |
| 65 | + // Clean the config object: remove JS comments for JSON.parse compatibility |
| 66 | + let configObj = match[1] |
| 67 | + .replace(/\/\/[^\n]*/g, '') // Remove single-line comments |
| 68 | + .replace(/,(\s*[}\]])/g, '$1'); // Remove trailing commas |
| 69 | + |
| 70 | + // Use Function constructor (safer than eval, runs in isolated scope) |
| 71 | + const configFn = new Function(`return ${configObj}`); |
| 72 | + userConfig = configFn(); |
| 73 | + } catch (parseError) { |
| 74 | + console.warn(`Failed to parse config from ${configPath}:`, parseError); |
| 75 | + throw requireError; // Re-throw original error |
| 76 | + } |
| 77 | + } else { |
| 78 | + throw requireError; // Re-throw if we can't parse it |
| 79 | + } |
| 80 | + } |
| 81 | + const config = createStoryUIConfig(userConfig); |
| 82 | + |
| 83 | + // Detect Storybook framework if not already specified |
| 84 | + if (!config.storybookFramework) { |
| 85 | + const packageJsonPath = path.join(process.cwd(), 'package.json'); |
| 86 | + if (fs.existsSync(packageJsonPath)) { |
| 87 | + try { |
| 88 | + const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8')); |
| 89 | + const dependencies = { ...packageJson.dependencies, ...packageJson.devDependencies }; |
| 90 | + config.storybookFramework = detectStorybookFramework(dependencies); |
| 91 | + } catch (error) { |
| 92 | + console.warn('Failed to detect Storybook framework:', error); |
72 | 93 | } |
73 | 94 | } |
| 95 | + } |
74 | 96 |
|
75 | | - // Cache the loaded config |
76 | | - cachedConfig = config; |
77 | | - configLoadTime = now; |
| 97 | + // Cache the loaded config |
| 98 | + cachedConfig = config; |
| 99 | + configLoadTime = now; |
78 | 100 |
|
79 | | - return config; |
80 | | - } |
| 101 | + return config; |
81 | 102 | } catch (error) { |
82 | 103 | console.warn(`Failed to load config from ${configPath}:`, error); |
83 | 104 | } |
|
0 commit comments