@onlook/storybook-plugin 0.4.0-beta.1 → 0.4.0-beta.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +637 -41
- package/package.json +6 -3
package/dist/index.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
1
|
+
import fs, { existsSync } from 'fs';
|
|
2
|
+
import path3, { dirname, join, relative } from 'path';
|
|
3
3
|
import { fileURLToPath } from 'url';
|
|
4
|
-
import
|
|
4
|
+
import { minimatch } from 'minimatch';
|
|
5
|
+
import * as prettier from 'prettier';
|
|
6
|
+
import { Project, SyntaxKind, ts, TypeFlags } from 'ts-morph';
|
|
7
|
+
import { glob } from 'glob';
|
|
5
8
|
import { withDefaultConfig } from 'react-docgen-typescript';
|
|
6
9
|
import generateModule from '@babel/generator';
|
|
7
10
|
import { parse } from '@babel/parser';
|
|
@@ -11,6 +14,599 @@ import crypto from 'crypto';
|
|
|
11
14
|
import { chromium } from 'playwright';
|
|
12
15
|
|
|
13
16
|
// src/storybook-onlook-plugin.ts
|
|
17
|
+
function getComponentInfo(componentDir, projectRootDir) {
|
|
18
|
+
const fileParseInfo = path3.parse(componentDir);
|
|
19
|
+
const prefixExtRegex = new RegExp(`(\\.\\w+)+(?=\\${fileParseInfo.ext}$)`);
|
|
20
|
+
const prefixExtRegexMatch = fileParseInfo.base.match(prefixExtRegex);
|
|
21
|
+
const prefixExt = prefixExtRegexMatch ? prefixExtRegexMatch[0] : void 0;
|
|
22
|
+
const fileName = fileParseInfo.name.replace(prefixExt || "", "");
|
|
23
|
+
const componentName = fileName === "index" ? fileParseInfo.dir.split("/").pop() : fileName;
|
|
24
|
+
let relativeSourceFilePath = componentDir.replace(projectRootDir, "");
|
|
25
|
+
if (relativeSourceFilePath.startsWith("/") || relativeSourceFilePath.startsWith("\\")) {
|
|
26
|
+
relativeSourceFilePath = componentDir.replace(projectRootDir, "").slice(1);
|
|
27
|
+
}
|
|
28
|
+
return {
|
|
29
|
+
fileBase: fileParseInfo.base,
|
|
30
|
+
fileExt: fileParseInfo.ext,
|
|
31
|
+
fileName,
|
|
32
|
+
filePrefixExt: prefixExt,
|
|
33
|
+
componentName,
|
|
34
|
+
relativeSourceFilePath
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
function pascalCase(str) {
|
|
38
|
+
return str.replace(/[_\-.\s]+(.)?/g, (_, c) => c ? c.toUpperCase() : "").replace(/^(.)/, (_, c) => c.toUpperCase());
|
|
39
|
+
}
|
|
40
|
+
function removeQuotesAndWrapWithDoubleQuotes(str) {
|
|
41
|
+
const quoteRemovedStr = str.replace(/"/g, "").replace(/'/g, "");
|
|
42
|
+
return quoteRemovedStr;
|
|
43
|
+
}
|
|
44
|
+
function getTypeFlagsName(flags) {
|
|
45
|
+
const keys = Object.keys(TypeFlags);
|
|
46
|
+
const setFlags = keys.find((key) => flags === TypeFlags[key]);
|
|
47
|
+
return setFlags || "err";
|
|
48
|
+
}
|
|
49
|
+
function getReactPropTypes({
|
|
50
|
+
sourceFile,
|
|
51
|
+
componentName
|
|
52
|
+
}) {
|
|
53
|
+
if (!componentName) {
|
|
54
|
+
return {
|
|
55
|
+
propTypes: void 0
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
let propsPattern = "component-props";
|
|
59
|
+
const pascalComponentName = pascalCase(componentName);
|
|
60
|
+
const propsType = sourceFile.getTypeAlias(`${pascalComponentName}Props`);
|
|
61
|
+
const propsInterface = sourceFile.getInterface(`${pascalComponentName}Props`);
|
|
62
|
+
const propsOnlyType = sourceFile.getTypeAlias("Props");
|
|
63
|
+
const propsOnlyInterface = sourceFile.getInterface("Props");
|
|
64
|
+
const propsInline = sourceFile.getVariableDeclaration(pascalComponentName)?.getInitializerIfKindOrThrow(ts.SyntaxKind.ArrowFunction);
|
|
65
|
+
const props = propsType?.getType() || propsInterface?.getType() || propsOnlyType?.getType() || propsOnlyInterface?.getType() || propsInline?.getParameters()[0]?.getType();
|
|
66
|
+
if (!props) {
|
|
67
|
+
return {
|
|
68
|
+
propTypes: []
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
let propsProperties = [];
|
|
72
|
+
const isPropsIntersection = props.isIntersection();
|
|
73
|
+
if (isPropsIntersection) {
|
|
74
|
+
propsProperties = [];
|
|
75
|
+
const intersectionTypes = props.getIntersectionTypes();
|
|
76
|
+
intersectionTypes.forEach((intersectionType) => {
|
|
77
|
+
const intersectionTypeText = intersectionType.getText();
|
|
78
|
+
if (intersectionTypeText.includes("HTMLAttributes")) return;
|
|
79
|
+
return propsProperties.push(...intersectionType.getProperties());
|
|
80
|
+
});
|
|
81
|
+
} else {
|
|
82
|
+
propsProperties = props.getProperties();
|
|
83
|
+
}
|
|
84
|
+
if (propsOnlyType || propsOnlyInterface) propsPattern = "props";
|
|
85
|
+
if (propsInline) propsPattern = "inline";
|
|
86
|
+
const propTypes = propsProperties.map((prop) => {
|
|
87
|
+
const propName = prop.getName();
|
|
88
|
+
const propType = prop.getValueDeclaration()?.getType();
|
|
89
|
+
if (!propType) {
|
|
90
|
+
return {
|
|
91
|
+
name: propName,
|
|
92
|
+
type: ["err"],
|
|
93
|
+
isOptional: prop.isOptional(),
|
|
94
|
+
value: []
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
if (propType.isUnion() && !propType.isBoolean()) {
|
|
98
|
+
const unionTypes = propType.getUnionTypes();
|
|
99
|
+
const type = Array.from(
|
|
100
|
+
new Set(unionTypes.map((union) => getTypeFlagsName(union.getFlags().valueOf())))
|
|
101
|
+
);
|
|
102
|
+
return {
|
|
103
|
+
name: propName,
|
|
104
|
+
type,
|
|
105
|
+
isOptional: prop.isOptional(),
|
|
106
|
+
value: unionTypes.map(
|
|
107
|
+
(union) => removeQuotesAndWrapWithDoubleQuotes(union.getText())
|
|
108
|
+
)
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
return {
|
|
112
|
+
name: propName,
|
|
113
|
+
type: [prop.getValueDeclaration().getType().getText()],
|
|
114
|
+
isOptional: prop.isOptional(),
|
|
115
|
+
value: []
|
|
116
|
+
};
|
|
117
|
+
});
|
|
118
|
+
return {
|
|
119
|
+
propsPattern,
|
|
120
|
+
propTypes
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
var errorDefinition = {
|
|
124
|
+
// Common
|
|
125
|
+
EC00: {
|
|
126
|
+
title: "Unknown error",
|
|
127
|
+
isCustomDetail: true
|
|
128
|
+
},
|
|
129
|
+
EC01: {
|
|
130
|
+
title: "Not yet supported",
|
|
131
|
+
detail: "This preset name is included in the preset bust not yet supported. Please wait for support.",
|
|
132
|
+
isCustomDetail: false
|
|
133
|
+
},
|
|
134
|
+
EC02: {
|
|
135
|
+
title: "Preset is not supported",
|
|
136
|
+
isCustomDetail: true
|
|
137
|
+
},
|
|
138
|
+
EC03: {
|
|
139
|
+
title: "Unable to get component name or file path correctly.",
|
|
140
|
+
detail: "Please check the file path and try again.",
|
|
141
|
+
isCustomDetail: false
|
|
142
|
+
},
|
|
143
|
+
EC04: {
|
|
144
|
+
title: "Could not find argTypes",
|
|
145
|
+
detail: "Error in genReactStoryFile.",
|
|
146
|
+
isCustomDetail: false
|
|
147
|
+
},
|
|
148
|
+
EC05: {
|
|
149
|
+
title: "Could not find meta",
|
|
150
|
+
isCustomDetail: true
|
|
151
|
+
},
|
|
152
|
+
EC06: {
|
|
153
|
+
title: "Could not find initializer",
|
|
154
|
+
detail: "Could not get initializer of ObjectLiteralExpression.",
|
|
155
|
+
isCustomDetail: false
|
|
156
|
+
},
|
|
157
|
+
EC07: {
|
|
158
|
+
title: "Could not find component",
|
|
159
|
+
isCustomDetail: true
|
|
160
|
+
},
|
|
161
|
+
EC08: {
|
|
162
|
+
title: "Could not scan directory",
|
|
163
|
+
isCustomDetail: true
|
|
164
|
+
},
|
|
165
|
+
EC09: {
|
|
166
|
+
title: "Reading directory failed",
|
|
167
|
+
isCustomDetail: true
|
|
168
|
+
},
|
|
169
|
+
EC10: {
|
|
170
|
+
title: "Could not get property from stories",
|
|
171
|
+
isCustomDetail: true
|
|
172
|
+
},
|
|
173
|
+
EC11: {
|
|
174
|
+
title: "File is defective.",
|
|
175
|
+
detail: "An error occurred during abstract syntax tree parsing.\nPlease check your file for problems.",
|
|
176
|
+
isCustomDetail: false
|
|
177
|
+
},
|
|
178
|
+
// Lit
|
|
179
|
+
EL01: {
|
|
180
|
+
title: "Failed to save file",
|
|
181
|
+
isCustomDetail: true
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
var throwErr = (params) => {
|
|
185
|
+
const { errorCode, detail } = params;
|
|
186
|
+
const detailText = errorDefinition[errorCode].isCustomDetail ? detail : errorDefinition[errorCode].detail;
|
|
187
|
+
console.error(`[ASG:${errorCode}] ${errorDefinition[errorCode].title}
|
|
188
|
+
${detailText}`);
|
|
189
|
+
};
|
|
190
|
+
async function genReactStoryFile({
|
|
191
|
+
componentName,
|
|
192
|
+
fileBase,
|
|
193
|
+
fileName,
|
|
194
|
+
filePrefixExt,
|
|
195
|
+
path: path42,
|
|
196
|
+
fileExt,
|
|
197
|
+
relativeSourceFilePath,
|
|
198
|
+
sourceFile,
|
|
199
|
+
prettierConfigPath,
|
|
200
|
+
storiesFolder
|
|
201
|
+
}) {
|
|
202
|
+
if (!componentName || !fileBase) {
|
|
203
|
+
throwErr({
|
|
204
|
+
errorCode: "EC03"
|
|
205
|
+
});
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
const { propTypes } = getReactPropTypes({
|
|
209
|
+
sourceFile,
|
|
210
|
+
componentName
|
|
211
|
+
});
|
|
212
|
+
const pascalComponentName = pascalCase(componentName);
|
|
213
|
+
if (!propTypes) {
|
|
214
|
+
throwErr({
|
|
215
|
+
errorCode: "EC04"
|
|
216
|
+
});
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
const defaultExportDeclaration = sourceFile.getExportedDeclarations();
|
|
220
|
+
let isDefaultExportComponent = false;
|
|
221
|
+
defaultExportDeclaration.forEach((declaration, exportName) => {
|
|
222
|
+
if (exportName === "default") {
|
|
223
|
+
const defaultExportName = declaration[0]?.getSymbol()?.getName();
|
|
224
|
+
isDefaultExportComponent = defaultExportName === pascalComponentName;
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
const pathToComponent = storiesFolder ? "../" : "./";
|
|
228
|
+
const initialCode = `
|
|
229
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
230
|
+
|
|
231
|
+
${isDefaultExportComponent ? `import ${pascalComponentName} from "${pathToComponent}${fileName}${filePrefixExt || ""}";` : `import { ${pascalComponentName} } from "${pathToComponent}${fileName}${filePrefixExt || ""}";`}
|
|
232
|
+
|
|
233
|
+
const meta: Meta<typeof ${pascalComponentName}> = {
|
|
234
|
+
title: "components/${pascalComponentName}",
|
|
235
|
+
component: (args) => <${componentName} {...args} />,
|
|
236
|
+
tags: ["autodocs"],
|
|
237
|
+
args: {},
|
|
238
|
+
argTypes: {},
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
export default meta;
|
|
242
|
+
type Story = StoryObj<typeof meta>;
|
|
243
|
+
|
|
244
|
+
export const Primary: Story = {};
|
|
245
|
+
`;
|
|
246
|
+
const componentCode = `${pascalComponentName}`;
|
|
247
|
+
const args = {};
|
|
248
|
+
propTypes.forEach((prop) => {
|
|
249
|
+
if (prop.isOptional) return args[prop.name] = "undefined";
|
|
250
|
+
let value = prop.value.length > 0 ? `"${prop.value[0]}"` : "undefined";
|
|
251
|
+
if (prop.type.includes("boolean")) value = true;
|
|
252
|
+
args[prop.name] = value;
|
|
253
|
+
});
|
|
254
|
+
const argTypes = {};
|
|
255
|
+
propTypes.forEach((prop) => {
|
|
256
|
+
if (prop.type[0] === "boolean") {
|
|
257
|
+
return argTypes[prop.name] = {
|
|
258
|
+
control: "boolean"
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
if (prop.type[0] === "object") {
|
|
262
|
+
return argTypes[prop.name] = {
|
|
263
|
+
control: "object"
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
if (prop.value.length > 1) {
|
|
267
|
+
return argTypes[prop.name] = {
|
|
268
|
+
control: "select",
|
|
269
|
+
options: prop.value
|
|
270
|
+
};
|
|
271
|
+
} else {
|
|
272
|
+
if (prop.type[0] === "string") {
|
|
273
|
+
return argTypes[prop.name] = {
|
|
274
|
+
control: "text"
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
if (prop.type[0] === "number") {
|
|
278
|
+
return argTypes[prop.name] = {
|
|
279
|
+
control: "number"
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
});
|
|
284
|
+
return {
|
|
285
|
+
fileOptions: {
|
|
286
|
+
componentName,
|
|
287
|
+
fileBase,
|
|
288
|
+
fileName,
|
|
289
|
+
filePrefixExt,
|
|
290
|
+
path: path42,
|
|
291
|
+
fileExt,
|
|
292
|
+
relativeSourceFilePath,
|
|
293
|
+
sourceFile,
|
|
294
|
+
prettierConfigPath
|
|
295
|
+
},
|
|
296
|
+
generateOptions: {
|
|
297
|
+
fileExt: ".stories.tsx",
|
|
298
|
+
initialCode,
|
|
299
|
+
meta: {
|
|
300
|
+
component: componentCode,
|
|
301
|
+
args,
|
|
302
|
+
argTypes
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
function createLightProject() {
|
|
308
|
+
return new Project({
|
|
309
|
+
compilerOptions: { allowJs: true },
|
|
310
|
+
useInMemoryFileSystem: true
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
var resolvedPrettierConfig = null;
|
|
314
|
+
async function getPrettierConfig(prettierConfigPath) {
|
|
315
|
+
if (resolvedPrettierConfig) return resolvedPrettierConfig;
|
|
316
|
+
resolvedPrettierConfig = prettierConfigPath ? await prettier.resolveConfig(prettierConfigPath) : {
|
|
317
|
+
semi: true,
|
|
318
|
+
trailingComma: "all",
|
|
319
|
+
singleQuote: false,
|
|
320
|
+
printWidth: 80,
|
|
321
|
+
tabWidth: 2,
|
|
322
|
+
endOfLine: "lf"
|
|
323
|
+
};
|
|
324
|
+
return resolvedPrettierConfig ?? {};
|
|
325
|
+
}
|
|
326
|
+
async function genStoryFile({
|
|
327
|
+
options,
|
|
328
|
+
id,
|
|
329
|
+
projectRootDir
|
|
330
|
+
}) {
|
|
331
|
+
if (id.includes(".stories")) return;
|
|
332
|
+
const {
|
|
333
|
+
fileBase,
|
|
334
|
+
fileName,
|
|
335
|
+
fileExt,
|
|
336
|
+
filePrefixExt,
|
|
337
|
+
componentName,
|
|
338
|
+
relativeSourceFilePath
|
|
339
|
+
} = getComponentInfo(id, projectRootDir);
|
|
340
|
+
try {
|
|
341
|
+
const sourceCode = fs.readFileSync(id, "utf-8");
|
|
342
|
+
const sourceProject = createLightProject();
|
|
343
|
+
const sourceFile = sourceProject.createSourceFile(fileBase || "temp.tsx", sourceCode);
|
|
344
|
+
let genStoryFileOptions;
|
|
345
|
+
if (options.preset === "react") {
|
|
346
|
+
genStoryFileOptions = await genReactStoryFile({
|
|
347
|
+
componentName,
|
|
348
|
+
fileBase,
|
|
349
|
+
fileName,
|
|
350
|
+
path: id,
|
|
351
|
+
fileExt,
|
|
352
|
+
filePrefixExt,
|
|
353
|
+
relativeSourceFilePath,
|
|
354
|
+
sourceFile,
|
|
355
|
+
prettierConfigPath: options.prettierConfigPath,
|
|
356
|
+
storiesFolder: options.storiesFolder
|
|
357
|
+
});
|
|
358
|
+
} else {
|
|
359
|
+
throwErr({
|
|
360
|
+
errorCode: "EC02",
|
|
361
|
+
detail: `Preset ${options.preset} is not supported in this fork. Only "react" is supported.`
|
|
362
|
+
});
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
if (!genStoryFileOptions) {
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
const storiesFilePath = genStoryFileOptions.fileOptions.path.replace(
|
|
369
|
+
genStoryFileOptions.fileOptions.filePrefixExt ? genStoryFileOptions.fileOptions.filePrefixExt : `${genStoryFileOptions.fileOptions.fileExt}`,
|
|
370
|
+
genStoryFileOptions.generateOptions.fileExt
|
|
371
|
+
);
|
|
372
|
+
let storiesFolderPath = "";
|
|
373
|
+
let storiesFilePathWithStoriesFolder = "";
|
|
374
|
+
if (options.storiesFolder) {
|
|
375
|
+
const splitStoriesFilePath = storiesFilePath.split("/");
|
|
376
|
+
const fileNameWithStoriesFolder = `${options.storiesFolder}/${splitStoriesFilePath[splitStoriesFilePath.length - 1]}`;
|
|
377
|
+
storiesFilePathWithStoriesFolder = storiesFilePath.replace(
|
|
378
|
+
`${genStoryFileOptions.fileOptions.fileName}${genStoryFileOptions.generateOptions.fileExt}`,
|
|
379
|
+
fileNameWithStoriesFolder
|
|
380
|
+
);
|
|
381
|
+
storiesFolderPath = storiesFilePath.replace(
|
|
382
|
+
`${genStoryFileOptions.fileOptions.fileName}${genStoryFileOptions.generateOptions.fileExt}`,
|
|
383
|
+
options.storiesFolder || ""
|
|
384
|
+
);
|
|
385
|
+
}
|
|
386
|
+
const storiesFilePathFinal = options.storiesFolder ? storiesFilePathWithStoriesFolder : storiesFilePath;
|
|
387
|
+
const storyExists = fs.existsSync(storiesFilePathFinal);
|
|
388
|
+
if (!storyExists) {
|
|
389
|
+
if (options.storiesFolder) {
|
|
390
|
+
fs.mkdirSync(storiesFolderPath, { recursive: true });
|
|
391
|
+
}
|
|
392
|
+
fs.writeFileSync(
|
|
393
|
+
storiesFilePathFinal,
|
|
394
|
+
genStoryFileOptions.generateOptions.initialCode
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
const storiesProject = new Project();
|
|
398
|
+
const storiesSourceFile = storiesProject.addSourceFileAtPath(storiesFilePathFinal);
|
|
399
|
+
const meta = storiesSourceFile.getVariableDeclaration("meta");
|
|
400
|
+
if (!meta || !meta.getInitializerIfKind(SyntaxKind.ObjectLiteralExpression)) {
|
|
401
|
+
throwErr({
|
|
402
|
+
errorCode: "EC05",
|
|
403
|
+
detail: `Could not find meta in file ${storiesSourceFile.getFilePath()}`
|
|
404
|
+
});
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
const initializer = meta.getInitializerIfKindOrThrow(
|
|
408
|
+
SyntaxKind.ObjectLiteralExpression
|
|
409
|
+
);
|
|
410
|
+
if (!initializer) {
|
|
411
|
+
throwErr({ errorCode: "EC06" });
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
if (genStoryFileOptions.generateOptions.meta.render) {
|
|
415
|
+
let renderProperty = initializer.getProperty("render");
|
|
416
|
+
while (!renderProperty) {
|
|
417
|
+
initializer.addPropertyAssignment({
|
|
418
|
+
name: "render",
|
|
419
|
+
initializer: "() => {}"
|
|
420
|
+
});
|
|
421
|
+
renderProperty = initializer.getProperty("render");
|
|
422
|
+
}
|
|
423
|
+
renderProperty.set({
|
|
424
|
+
initializer: genStoryFileOptions.generateOptions.meta.render
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
if (genStoryFileOptions.generateOptions.meta.component) {
|
|
428
|
+
let componentProperty = initializer.getProperty("component");
|
|
429
|
+
while (!componentProperty) {
|
|
430
|
+
initializer.addPropertyAssignment({
|
|
431
|
+
name: "component",
|
|
432
|
+
initializer: "null"
|
|
433
|
+
});
|
|
434
|
+
componentProperty = initializer.getProperty("component");
|
|
435
|
+
}
|
|
436
|
+
componentProperty.set({
|
|
437
|
+
initializer: genStoryFileOptions.generateOptions.meta.component
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
if (genStoryFileOptions.generateOptions.meta.args) {
|
|
441
|
+
let argsProperty = initializer.getProperty("args");
|
|
442
|
+
while (!argsProperty) {
|
|
443
|
+
initializer.addPropertyAssignment({
|
|
444
|
+
name: "args",
|
|
445
|
+
initializer: "{}"
|
|
446
|
+
});
|
|
447
|
+
argsProperty = initializer.getProperty("args");
|
|
448
|
+
}
|
|
449
|
+
const argText = Object.entries(genStoryFileOptions.generateOptions.meta.args).map((x) => x.join(":")).join(", ");
|
|
450
|
+
argsProperty.set({ initializer: `{ ${argText} }` });
|
|
451
|
+
}
|
|
452
|
+
if (genStoryFileOptions.generateOptions.meta.argTypes) {
|
|
453
|
+
let argTypesProperty = initializer.getProperty("argTypes");
|
|
454
|
+
while (!argTypesProperty) {
|
|
455
|
+
initializer.addPropertyAssignment({
|
|
456
|
+
name: "argTypes",
|
|
457
|
+
initializer: "{}"
|
|
458
|
+
});
|
|
459
|
+
argTypesProperty = initializer.getProperty("argTypes");
|
|
460
|
+
}
|
|
461
|
+
const argTypesText = JSON.stringify(
|
|
462
|
+
genStoryFileOptions.generateOptions.meta.argTypes,
|
|
463
|
+
null,
|
|
464
|
+
""
|
|
465
|
+
);
|
|
466
|
+
argTypesProperty.set({ initializer: `${argTypesText}` });
|
|
467
|
+
}
|
|
468
|
+
await storiesSourceFile.save();
|
|
469
|
+
const fileContent = fs.readFileSync(storiesFilePathFinal, "utf-8");
|
|
470
|
+
const config = await getPrettierConfig(
|
|
471
|
+
genStoryFileOptions.fileOptions.prettierConfigPath
|
|
472
|
+
);
|
|
473
|
+
const formattedContent = await prettier.format(fileContent, {
|
|
474
|
+
...config,
|
|
475
|
+
parser: "typescript"
|
|
476
|
+
});
|
|
477
|
+
fs.writeFileSync(storiesFilePathFinal, formattedContent);
|
|
478
|
+
} catch (err) {
|
|
479
|
+
console.warn(`[ASG] Failed to generate story for ${id}:`, err);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
async function getAllFilePaths({
|
|
483
|
+
patterns,
|
|
484
|
+
ignorePatterns,
|
|
485
|
+
projectRootDir
|
|
486
|
+
}) {
|
|
487
|
+
const fullPatterns = patterns.map(
|
|
488
|
+
(p) => path3.join(projectRootDir, p).replace(/\\/g, "/")
|
|
489
|
+
);
|
|
490
|
+
const ignoreFullPatterns = ignorePatterns?.map(
|
|
491
|
+
(p) => path3.join(projectRootDir, p).replace(/\\/g, "/")
|
|
492
|
+
);
|
|
493
|
+
const filePaths = await glob(fullPatterns, {
|
|
494
|
+
ignore: ignoreFullPatterns,
|
|
495
|
+
nodir: true
|
|
496
|
+
});
|
|
497
|
+
return filePaths.map((p) => p.replace(/\\/g, "/"));
|
|
498
|
+
}
|
|
499
|
+
var PLUGIN_NAME = "auto-story-generator";
|
|
500
|
+
var DEFAULT_BATCH_SIZE = 20;
|
|
501
|
+
var DEFAULT_CONCURRENCY = 4;
|
|
502
|
+
var mtimeCache = /* @__PURE__ */ new Map();
|
|
503
|
+
function hasFileChanged(filePath) {
|
|
504
|
+
try {
|
|
505
|
+
const stat = fs.statSync(filePath);
|
|
506
|
+
const mtime = stat.mtimeMs;
|
|
507
|
+
const cached = mtimeCache.get(filePath);
|
|
508
|
+
if (cached === mtime) return false;
|
|
509
|
+
mtimeCache.set(filePath, mtime);
|
|
510
|
+
return true;
|
|
511
|
+
} catch {
|
|
512
|
+
mtimeCache.delete(filePath);
|
|
513
|
+
return true;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
function getStoryFilePath(filePath, storiesFolder) {
|
|
517
|
+
const parsed = path3.parse(filePath);
|
|
518
|
+
const storyName = `${parsed.name}.stories.tsx`;
|
|
519
|
+
if (storiesFolder) {
|
|
520
|
+
return path3.join(parsed.dir, storiesFolder, storyName);
|
|
521
|
+
}
|
|
522
|
+
return path3.join(parsed.dir, storyName);
|
|
523
|
+
}
|
|
524
|
+
async function processBatch(files, options, projectRootDir, concurrency) {
|
|
525
|
+
let processed = 0;
|
|
526
|
+
for (let i = 0; i < files.length; i += concurrency) {
|
|
527
|
+
const chunk = files.slice(i, i + concurrency);
|
|
528
|
+
await Promise.all(
|
|
529
|
+
chunk.map(async (filePath) => {
|
|
530
|
+
await genStoryFile({
|
|
531
|
+
options,
|
|
532
|
+
id: filePath,
|
|
533
|
+
projectRootDir
|
|
534
|
+
});
|
|
535
|
+
processed++;
|
|
536
|
+
})
|
|
537
|
+
);
|
|
538
|
+
}
|
|
539
|
+
return processed;
|
|
540
|
+
}
|
|
541
|
+
function createAutoStoryPlugin(options) {
|
|
542
|
+
const projectRootDir = (options.projectRoot ?? process.cwd()).replace(/\\/g, "/");
|
|
543
|
+
const batchSize = options.batchSize ?? DEFAULT_BATCH_SIZE;
|
|
544
|
+
const concurrency = options.concurrency ?? DEFAULT_CONCURRENCY;
|
|
545
|
+
return {
|
|
546
|
+
name: PLUGIN_NAME,
|
|
547
|
+
async buildStart() {
|
|
548
|
+
if (!options.isGenerateStoriesFileAtBuild) return;
|
|
549
|
+
const patterns = options.imports ?? ["src/**/*.tsx"];
|
|
550
|
+
const ignorePatterns = options.ignores ?? [];
|
|
551
|
+
console.log(`[ASG] Scanning for components: ${patterns.join(", ")}`);
|
|
552
|
+
const allFiles = await getAllFilePaths({
|
|
553
|
+
patterns,
|
|
554
|
+
ignorePatterns,
|
|
555
|
+
projectRootDir
|
|
556
|
+
});
|
|
557
|
+
const filesToProcess = allFiles.filter((filePath) => {
|
|
558
|
+
if (filePath.includes(".stories")) return false;
|
|
559
|
+
if (options.cacheEnabled !== false) {
|
|
560
|
+
const storyPath = getStoryFilePath(filePath, options.storiesFolder);
|
|
561
|
+
const storyExists = fs.existsSync(storyPath);
|
|
562
|
+
if (!hasFileChanged(filePath) && storyExists) return false;
|
|
563
|
+
}
|
|
564
|
+
return true;
|
|
565
|
+
});
|
|
566
|
+
console.log(
|
|
567
|
+
`[ASG] Found ${allFiles.length} files, ${filesToProcess.length} need processing`
|
|
568
|
+
);
|
|
569
|
+
let totalProcessed = 0;
|
|
570
|
+
for (let i = 0; i < filesToProcess.length; i += batchSize) {
|
|
571
|
+
const batch = filesToProcess.slice(i, i + batchSize);
|
|
572
|
+
const count = await processBatch(batch, options, projectRootDir, concurrency);
|
|
573
|
+
totalProcessed += count;
|
|
574
|
+
if (options.onProgress) {
|
|
575
|
+
options.onProgress(totalProcessed, filesToProcess.length);
|
|
576
|
+
}
|
|
577
|
+
if (i + batchSize < filesToProcess.length) {
|
|
578
|
+
await new Promise((r) => setTimeout(r, 0));
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
console.log(`[ASG] Generated stories for ${totalProcessed} components`);
|
|
582
|
+
},
|
|
583
|
+
async watchChange(id, change) {
|
|
584
|
+
if (change.event === "delete") return;
|
|
585
|
+
const normalizedId = id.replace(/\\/g, "/");
|
|
586
|
+
if (options.imports) {
|
|
587
|
+
const relativePath = normalizedId.replace(projectRootDir, "").replace(/^\//, "");
|
|
588
|
+
const matches = options.imports.some(
|
|
589
|
+
(pattern) => minimatch(relativePath, pattern)
|
|
590
|
+
);
|
|
591
|
+
if (!matches) return;
|
|
592
|
+
}
|
|
593
|
+
if (options.ignores) {
|
|
594
|
+
const relativePath = normalizedId.replace(projectRootDir, "").replace(/^\//, "");
|
|
595
|
+
const ignored = options.ignores.some(
|
|
596
|
+
(pattern) => minimatch(relativePath, pattern)
|
|
597
|
+
);
|
|
598
|
+
if (ignored) return;
|
|
599
|
+
}
|
|
600
|
+
mtimeCache.delete(normalizedId);
|
|
601
|
+
await genStoryFile({
|
|
602
|
+
options,
|
|
603
|
+
id: normalizedId,
|
|
604
|
+
projectRootDir
|
|
605
|
+
});
|
|
606
|
+
}
|
|
607
|
+
};
|
|
608
|
+
}
|
|
609
|
+
var index_default = { vite: createAutoStoryPlugin };
|
|
14
610
|
var FIXED_MARKER = "// @onlook-fixed";
|
|
15
611
|
var parser = withDefaultConfig({
|
|
16
612
|
shouldExtractLiteralValuesFromEnum: true,
|
|
@@ -21,12 +617,12 @@ var parser = withDefaultConfig({
|
|
|
21
617
|
}
|
|
22
618
|
});
|
|
23
619
|
function resolveComponentPath(storyFilePath) {
|
|
24
|
-
const dir =
|
|
25
|
-
const parentDir =
|
|
26
|
-
const storyName =
|
|
620
|
+
const dir = path3.dirname(storyFilePath);
|
|
621
|
+
const parentDir = path3.dirname(dir);
|
|
622
|
+
const storyName = path3.basename(storyFilePath);
|
|
27
623
|
const componentName = storyName.replace(".stories.tsx", ".tsx");
|
|
28
|
-
const componentPath =
|
|
29
|
-
return
|
|
624
|
+
const componentPath = path3.join(parentDir, componentName);
|
|
625
|
+
return fs.existsSync(componentPath) ? componentPath : null;
|
|
30
626
|
}
|
|
31
627
|
function generateArgTypes(componentPath) {
|
|
32
628
|
try {
|
|
@@ -67,7 +663,7 @@ function generateArgTypes(componentPath) {
|
|
|
67
663
|
}
|
|
68
664
|
function enrichStoryFile(storyFilePath) {
|
|
69
665
|
try {
|
|
70
|
-
const content =
|
|
666
|
+
const content = fs.readFileSync(storyFilePath, "utf-8");
|
|
71
667
|
if (content.includes(FIXED_MARKER)) {
|
|
72
668
|
return;
|
|
73
669
|
}
|
|
@@ -86,8 +682,8 @@ function enrichStoryFile(storyFilePath) {
|
|
|
86
682
|
export default meta;`
|
|
87
683
|
);
|
|
88
684
|
if (enriched !== content) {
|
|
89
|
-
|
|
90
|
-
console.log(`[AutoStories] Enriched ${
|
|
685
|
+
fs.writeFileSync(storyFilePath, enriched);
|
|
686
|
+
console.log(`[AutoStories] Enriched ${path3.basename(storyFilePath)} with argTypes`);
|
|
91
687
|
}
|
|
92
688
|
} catch (err) {
|
|
93
689
|
console.error(`[AutoStories] Failed to enrich story: ${storyFilePath}`, err);
|
|
@@ -115,7 +711,7 @@ function componentLocPlugin(options = {}) {
|
|
|
115
711
|
sourceFilename: filepath
|
|
116
712
|
});
|
|
117
713
|
let mutated = false;
|
|
118
|
-
const relativePath =
|
|
714
|
+
const relativePath = path3.relative(root, filepath);
|
|
119
715
|
traverse(ast, {
|
|
120
716
|
JSXElement(nodePath) {
|
|
121
717
|
const opening = nodePath.node.openingElement;
|
|
@@ -152,9 +748,9 @@ function componentLocPlugin(options = {}) {
|
|
|
152
748
|
}
|
|
153
749
|
};
|
|
154
750
|
}
|
|
155
|
-
var CACHE_DIR =
|
|
156
|
-
var SCREENSHOTS_DIR =
|
|
157
|
-
var MANIFEST_PATH =
|
|
751
|
+
var CACHE_DIR = path3.join(process.cwd(), ".storybook-cache");
|
|
752
|
+
var SCREENSHOTS_DIR = path3.join(CACHE_DIR, "screenshots");
|
|
753
|
+
var MANIFEST_PATH = path3.join(CACHE_DIR, "manifest.json");
|
|
158
754
|
var VIEWPORT_WIDTH = 1920;
|
|
159
755
|
var VIEWPORT_HEIGHT = 1080;
|
|
160
756
|
var MIN_COMPONENT_WIDTH = 420;
|
|
@@ -162,30 +758,30 @@ var MIN_COMPONENT_HEIGHT = 280;
|
|
|
162
758
|
|
|
163
759
|
// src/utils/fileSystem/fileSystem.ts
|
|
164
760
|
function ensureCacheDirectories() {
|
|
165
|
-
if (!
|
|
166
|
-
|
|
761
|
+
if (!fs.existsSync(CACHE_DIR)) {
|
|
762
|
+
fs.mkdirSync(CACHE_DIR, { recursive: true });
|
|
167
763
|
}
|
|
168
|
-
if (!
|
|
169
|
-
|
|
764
|
+
if (!fs.existsSync(SCREENSHOTS_DIR)) {
|
|
765
|
+
fs.mkdirSync(SCREENSHOTS_DIR, { recursive: true });
|
|
170
766
|
}
|
|
171
767
|
}
|
|
172
768
|
function computeFileHash(filePath) {
|
|
173
|
-
if (!
|
|
769
|
+
if (!fs.existsSync(filePath)) {
|
|
174
770
|
return "";
|
|
175
771
|
}
|
|
176
|
-
const content =
|
|
772
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
177
773
|
return crypto.createHash("sha256").update(content).digest("hex");
|
|
178
774
|
}
|
|
179
775
|
function loadManifest() {
|
|
180
|
-
if (
|
|
181
|
-
const content =
|
|
776
|
+
if (fs.existsSync(MANIFEST_PATH)) {
|
|
777
|
+
const content = fs.readFileSync(MANIFEST_PATH, "utf-8");
|
|
182
778
|
return JSON.parse(content);
|
|
183
779
|
}
|
|
184
780
|
return { stories: {} };
|
|
185
781
|
}
|
|
186
782
|
function saveManifest(manifest) {
|
|
187
783
|
ensureCacheDirectories();
|
|
188
|
-
|
|
784
|
+
fs.writeFileSync(MANIFEST_PATH, JSON.stringify(manifest, null, 2));
|
|
189
785
|
}
|
|
190
786
|
function updateManifest(storyId, sourcePath, fileHash, boundingBox) {
|
|
191
787
|
const manifest = loadManifest();
|
|
@@ -211,8 +807,8 @@ async function getBrowser() {
|
|
|
211
807
|
return browser;
|
|
212
808
|
}
|
|
213
809
|
function getScreenshotPath(storyId, theme) {
|
|
214
|
-
const storyDir =
|
|
215
|
-
return
|
|
810
|
+
const storyDir = path3.join(SCREENSHOTS_DIR, storyId);
|
|
811
|
+
return path3.join(storyDir, `${theme}.png`);
|
|
216
812
|
}
|
|
217
813
|
async function captureScreenshotBuffer(storyId, theme, width = VIEWPORT_WIDTH, height = VIEWPORT_HEIGHT, storybookUrl = "http://localhost:6006", timeoutMs = 3e4) {
|
|
218
814
|
const browser2 = await getBrowser();
|
|
@@ -299,9 +895,9 @@ async function captureScreenshotBuffer(storyId, theme, width = VIEWPORT_WIDTH, h
|
|
|
299
895
|
async function generateScreenshot(storyId, theme, storybookUrl = "http://localhost:6006", timeoutMs = 3e4) {
|
|
300
896
|
try {
|
|
301
897
|
ensureCacheDirectories();
|
|
302
|
-
const storyDir =
|
|
303
|
-
if (!
|
|
304
|
-
|
|
898
|
+
const storyDir = path3.join(SCREENSHOTS_DIR, storyId);
|
|
899
|
+
if (!fs.existsSync(storyDir)) {
|
|
900
|
+
fs.mkdirSync(storyDir, { recursive: true });
|
|
305
901
|
}
|
|
306
902
|
const screenshotPath = getScreenshotPath(storyId, theme);
|
|
307
903
|
const { buffer, boundingBox } = await captureScreenshotBuffer(
|
|
@@ -312,7 +908,7 @@ async function generateScreenshot(storyId, theme, storybookUrl = "http://localho
|
|
|
312
908
|
storybookUrl,
|
|
313
909
|
timeoutMs
|
|
314
910
|
);
|
|
315
|
-
|
|
911
|
+
fs.writeFileSync(screenshotPath, buffer);
|
|
316
912
|
return { path: screenshotPath, boundingBox };
|
|
317
913
|
} catch (error) {
|
|
318
914
|
console.error(`Error generating screenshot for ${storyId} (${theme}):`, error);
|
|
@@ -339,7 +935,7 @@ async function fetchStorybookIndex() {
|
|
|
339
935
|
}
|
|
340
936
|
function getStoriesForFile(filePath) {
|
|
341
937
|
if (!cachedIndex) return [];
|
|
342
|
-
const fileName =
|
|
938
|
+
const fileName = path3.basename(filePath);
|
|
343
939
|
return Object.values(cachedIndex.entries).filter((entry) => entry.type === "story" && entry.importPath.endsWith(fileName)).map((entry) => entry.id);
|
|
344
940
|
}
|
|
345
941
|
async function regenerateScreenshotsForFiles(files) {
|
|
@@ -501,7 +1097,7 @@ var serveMetadataAndScreenshots = (req, res, next) => {
|
|
|
501
1097
|
}
|
|
502
1098
|
if (req.url === "/onbook-index.json") {
|
|
503
1099
|
console.log("[STORYBOOK_PLUGIN] Serving /onbook-index.json endpoint");
|
|
504
|
-
const manifestPath =
|
|
1100
|
+
const manifestPath = path3.join(process.cwd(), ".storybook-cache", "manifest.json");
|
|
505
1101
|
const cacheBuster = Date.now();
|
|
506
1102
|
console.log("[STORYBOOK_PLUGIN] Fetching http://localhost:6006/index.json");
|
|
507
1103
|
fetch(`http://localhost:6006/index.json?_t=${cacheBuster}`, {
|
|
@@ -518,7 +1114,7 @@ var serveMetadataAndScreenshots = (req, res, next) => {
|
|
|
518
1114
|
});
|
|
519
1115
|
return response.json();
|
|
520
1116
|
}).then((indexData) => {
|
|
521
|
-
const manifest =
|
|
1117
|
+
const manifest = fs.existsSync(manifestPath) ? JSON.parse(fs.readFileSync(manifestPath, "utf-8")) : { stories: {} };
|
|
522
1118
|
const defaultBoundingBox = { width: 1920, height: 1080 };
|
|
523
1119
|
for (const [storyId, entry] of Object.entries(indexData.entries || {})) {
|
|
524
1120
|
const manifestEntry = manifest.stories?.[storyId];
|
|
@@ -586,7 +1182,7 @@ var serveMetadataAndScreenshots = (req, res, next) => {
|
|
|
586
1182
|
return;
|
|
587
1183
|
}
|
|
588
1184
|
if (req.url?.startsWith("/screenshots/")) {
|
|
589
|
-
const screenshotPath =
|
|
1185
|
+
const screenshotPath = path3.join(
|
|
590
1186
|
process.cwd(),
|
|
591
1187
|
".storybook-cache",
|
|
592
1188
|
req.url.replace("/screenshots/", "screenshots/")
|
|
@@ -595,11 +1191,11 @@ var serveMetadataAndScreenshots = (req, res, next) => {
|
|
|
595
1191
|
const storyId = urlParts[0];
|
|
596
1192
|
const themeFile = urlParts[1];
|
|
597
1193
|
const theme = themeFile?.replace(".png", "");
|
|
598
|
-
if (
|
|
1194
|
+
if (fs.existsSync(screenshotPath)) {
|
|
599
1195
|
res.setHeader("Content-Type", "image/png");
|
|
600
1196
|
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
601
1197
|
res.setHeader("Cache-Control", "public, max-age=3600");
|
|
602
|
-
|
|
1198
|
+
fs.createReadStream(screenshotPath).pipe(res);
|
|
603
1199
|
return;
|
|
604
1200
|
}
|
|
605
1201
|
if (storyId && theme && (theme === "light" || theme === "dark")) {
|
|
@@ -607,16 +1203,16 @@ var serveMetadataAndScreenshots = (req, res, next) => {
|
|
|
607
1203
|
`[STORYBOOK_PLUGIN] Generating screenshot on-demand: ${storyId}/${theme}`
|
|
608
1204
|
);
|
|
609
1205
|
captureScreenshotBuffer(storyId, theme).then(({ buffer }) => {
|
|
610
|
-
const storyDir =
|
|
1206
|
+
const storyDir = path3.join(
|
|
611
1207
|
process.cwd(),
|
|
612
1208
|
".storybook-cache",
|
|
613
1209
|
"screenshots",
|
|
614
1210
|
storyId
|
|
615
1211
|
);
|
|
616
|
-
if (!
|
|
617
|
-
|
|
1212
|
+
if (!fs.existsSync(storyDir)) {
|
|
1213
|
+
fs.mkdirSync(storyDir, { recursive: true });
|
|
618
1214
|
}
|
|
619
|
-
|
|
1215
|
+
fs.writeFileSync(screenshotPath, buffer);
|
|
620
1216
|
res.setHeader("Content-Type", "image/png");
|
|
621
1217
|
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
622
1218
|
res.setHeader("Cache-Control", "public, max-age=3600");
|
|
@@ -717,7 +1313,7 @@ function storybookOnlookPlugin(options = {}) {
|
|
|
717
1313
|
});
|
|
718
1314
|
try {
|
|
719
1315
|
plugins.push(
|
|
720
|
-
|
|
1316
|
+
index_default.vite({
|
|
721
1317
|
preset: "react",
|
|
722
1318
|
imports,
|
|
723
1319
|
ignores,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@onlook/storybook-plugin",
|
|
3
|
-
"version": "0.4.0-beta.
|
|
3
|
+
"version": "0.4.0-beta.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"onlook-storybook": "./dist/cli/index.js"
|
|
@@ -37,9 +37,12 @@
|
|
|
37
37
|
"@babel/traverse": "^7.26.9",
|
|
38
38
|
"@babel/types": "^7.26.9",
|
|
39
39
|
"@drizzle-team/brocli": "^0.11.0",
|
|
40
|
-
"
|
|
40
|
+
"glob": "^10.3.10",
|
|
41
|
+
"minimatch": "^9.0.3",
|
|
41
42
|
"playwright": "^1.52.0",
|
|
42
|
-
"
|
|
43
|
+
"prettier": "^3.2.5",
|
|
44
|
+
"react-docgen-typescript": "^2.4.0",
|
|
45
|
+
"ts-morph": "^21.0.1"
|
|
43
46
|
},
|
|
44
47
|
"devDependencies": {
|
|
45
48
|
"@onbook/tsconfig": "workspace:*",
|