UNPKG

5.91 kBJavaScriptView Raw
1const path = require('path');
2const fs = require('fs');
3const glob = require('glob');
4const prettier = require('prettier');
5const normalizeStories = require('@storybook/core-common').normalizeStories;
6
7const cwd = process.cwd();
8const supportedExtensions = ['js', 'jsx', 'ts', 'tsx', 'cjs', 'mjs'];
9
10// we clear decorators as a workaround for global decorators getting infinitely applied on HMR
11const previewImports = `
12 import { decorators, parameters } from './preview';
13
14 if (decorators) {
15 if(__DEV__){
16 // stops the warning from showing on every HMR
17 require('react-native').LogBox.ignoreLogs([
18 '\`clearDecorators\` is deprecated and will be removed in Storybook 7.0',
19 ]);
20 }
21 // workaround for global decorators getting infinitely applied on HMR, see https://github.com/storybookjs/react-native/issues/185
22 clearDecorators();
23 decorators.forEach((decorator) => addDecorator(decorator));
24 }
25
26 if (parameters) {
27 addParameters(parameters);
28 }
29`;
30
31function normalizeExcludePaths(paths) {
32 // automatically convert a string to an array of a single string
33 if (typeof paths === 'string') {
34 return [paths];
35 }
36
37 // ensure the paths is an array and if any items exists, they are strings
38 if (Array.isArray(paths) && paths.every((p) => typeof p === 'string')) {
39 return paths;
40 }
41
42 // when the paths aren't a string or an (empty) array of strings, return
43 return undefined;
44}
45
46function requireUncached(module) {
47 delete require.cache[require.resolve(module)];
48 return require(module);
49}
50
51function getMain({ configPath }) {
52 const fileExtension = getFilePathExtension({ configPath }, 'main');
53 if (fileExtension === null) {
54 throw new Error('main config file not found');
55 }
56 const mainPath = path.resolve(cwd, configPath, `main.${fileExtension}`);
57
58 return requireUncached(mainPath);
59}
60
61function getFilePathExtension({ configPath }, fileName) {
62 for (const ext of supportedExtensions) {
63 const filePath = path.resolve(cwd, configPath, `${fileName}.${ext}`);
64 if (fs.existsSync(filePath)) {
65 return ext;
66 }
67 }
68 return null;
69}
70
71function getPreviewExists({ configPath }) {
72 return !!getFilePathExtension({ configPath }, 'preview');
73}
74
75function ensureRelativePathHasDot(relativePath) {
76 return relativePath.startsWith('.') ? relativePath : `./${relativePath}`;
77}
78
79function writeRequires({ configPath, absolute = false }) {
80 const storybookRequiresLocation = path.resolve(cwd, configPath, 'storybook.requires.js');
81
82 const mainImport = getMain({ configPath });
83 const main = mainImport.default ?? mainImport;
84 const reactNativeOptions = main.reactNativeOptions;
85 const excludePaths = reactNativeOptions && reactNativeOptions.excludePaths;
86 const normalizedExcludePaths = normalizeExcludePaths(excludePaths);
87
88 const storiesSpecifiers = normalizeStories(main.stories, {
89 configDir: configPath,
90 workingDir: cwd,
91 });
92
93 const storyRequires = storiesSpecifiers.reduce((acc, specifier) => {
94 const paths = glob
95 .sync(specifier.files, {
96 cwd: path.resolve(cwd, specifier.directory),
97 absolute,
98 // default to always ignore (exclude) anything in node_modules
99 ignore: normalizedExcludePaths !== undefined ? normalizedExcludePaths : ['**/node_modules'],
100 })
101 .map((storyPath) => {
102 const pathWithDirectory = path.join(specifier.directory, storyPath);
103 const requirePath = absolute
104 ? storyPath
105 : ensureRelativePathHasDot(path.relative(configPath, pathWithDirectory));
106
107 const absolutePath = absolute ? requirePath : path.resolve(configPath, requirePath);
108 const pathRelativeToCwd = path.relative(cwd, absolutePath);
109
110 const normalizePathForWindows = (str) =>
111 path.sep === '\\' ? str.replace(/\\/g, '/') : str;
112
113 return `"./${normalizePathForWindows(
114 pathRelativeToCwd
115 )}": require("${normalizePathForWindows(requirePath)}")`;
116 });
117 return [...acc, ...paths];
118 }, []);
119
120 fs.writeFileSync(storybookRequiresLocation, '');
121
122 const previewExists = getPreviewExists({ configPath });
123
124 let previewJs = previewExists ? previewImports : '';
125
126 const path_obj_str = `{${storyRequires.join(',')}}`;
127
128 const registerAddons = main.addons?.map((addon) => `import "${addon}/register";`).join('\n');
129 let enhancersImport = '';
130 let enhancers = '';
131
132 // TODO: implement presets or something similar
133 if (main.addons.includes('@storybook/addon-ondevice-actions')) {
134 enhancersImport =
135 'import { argsEnhancers } from "@storybook/addon-actions/dist/modern/preset/addArgs"';
136
137 // try/catch is a temporary fix for https://github.com/storybookjs/react-native/issues/327 until a fix is found
138 enhancers = `
139 try {
140 argsEnhancers.forEach(enhancer => addArgsEnhancer(enhancer));
141 } catch{}
142 `;
143 }
144
145 const normalizedStories = normalizeStories(main.stories, {
146 configDir: configPath,
147 workingDir: cwd,
148 }).map((specifier) => ({
149 ...specifier,
150 importPathMatcher: specifier.importPathMatcher.source,
151 }));
152
153 const globalStories = `global.STORIES = ${JSON.stringify(normalizedStories)}`;
154
155 const fileContent = `
156 /* do not change this file, it is auto generated by storybook. */
157
158 import { configure, addDecorator, addParameters, addArgsEnhancer, clearDecorators } from '@storybook/react-native';
159
160 ${globalStories}
161
162 ${registerAddons}
163
164 ${enhancersImport}
165
166 ${previewJs}
167
168 ${enhancers}
169
170 const getStories=() => {
171 return ${path_obj_str};
172 }
173
174 configure(getStories, module, false)
175 `;
176
177 const formattedFileContent = prettier.format(fileContent, { parser: 'babel' });
178
179 fs.writeFileSync(storybookRequiresLocation, formattedFileContent, {
180 encoding: 'utf8',
181 flag: 'w',
182 });
183}
184
185module.exports = {
186 writeRequires,
187 getMain,
188 getPreviewExists,
189 getFilePathExtension,
190};