@onlook/storybook-plugin 0.4.0-beta.2 → 0.4.0-beta.4

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.
Files changed (2) hide show
  1. package/dist/index.js +671 -177
  2. package/package.json +6 -3
package/dist/index.js CHANGED
@@ -1,6 +1,10 @@
1
- import fs5, { existsSync } from 'fs';
2
- import path6, { dirname, join, relative } from 'path';
1
+ import fs, { existsSync } from 'fs';
2
+ import path3, { dirname, join, relative } from 'path';
3
3
  import { fileURLToPath } from 'url';
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';
4
8
  import { withDefaultConfig } from 'react-docgen-typescript';
5
9
  import generateModule from '@babel/generator';
6
10
  import { parse } from '@babel/parser';
@@ -9,12 +13,612 @@ import * as t from '@babel/types';
9
13
  import crypto from 'crypto';
10
14
  import { chromium } from 'playwright';
11
15
 
12
- var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
13
- get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
14
- }) : x)(function(x) {
15
- if (typeof require !== "undefined") return require.apply(this, arguments);
16
- throw Error('Dynamic require of "' + x + '" is not supported');
17
- });
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
+ console.log("[ASG] Plugin created", {
546
+ preset: options.preset,
547
+ imports: options.imports,
548
+ isGenerateStoriesFileAtBuild: options.isGenerateStoriesFileAtBuild,
549
+ storiesFolder: options.storiesFolder,
550
+ projectRoot: projectRootDir,
551
+ cwd: process.cwd()
552
+ });
553
+ return {
554
+ name: PLUGIN_NAME,
555
+ async buildStart() {
556
+ console.log("[ASG] buildStart hook fired");
557
+ if (!options.isGenerateStoriesFileAtBuild) {
558
+ console.log("[ASG] Skipping \u2014 isGenerateStoriesFileAtBuild is false");
559
+ return;
560
+ }
561
+ const patterns = options.imports ?? ["src/**/*.tsx"];
562
+ const ignorePatterns = options.ignores ?? [];
563
+ console.log(`[ASG] Scanning for components: ${patterns.join(", ")}`);
564
+ const allFiles = await getAllFilePaths({
565
+ patterns,
566
+ ignorePatterns,
567
+ projectRootDir
568
+ });
569
+ const filesToProcess = allFiles.filter((filePath) => {
570
+ if (filePath.includes(".stories")) return false;
571
+ if (options.cacheEnabled !== false) {
572
+ const storyPath = getStoryFilePath(filePath, options.storiesFolder);
573
+ const storyExists = fs.existsSync(storyPath);
574
+ if (!hasFileChanged(filePath) && storyExists) return false;
575
+ }
576
+ return true;
577
+ });
578
+ console.log(
579
+ `[ASG] Found ${allFiles.length} files, ${filesToProcess.length} need processing`
580
+ );
581
+ let totalProcessed = 0;
582
+ for (let i = 0; i < filesToProcess.length; i += batchSize) {
583
+ const batch = filesToProcess.slice(i, i + batchSize);
584
+ const count = await processBatch(batch, options, projectRootDir, concurrency);
585
+ totalProcessed += count;
586
+ if (options.onProgress) {
587
+ options.onProgress(totalProcessed, filesToProcess.length);
588
+ }
589
+ if (i + batchSize < filesToProcess.length) {
590
+ await new Promise((r) => setTimeout(r, 0));
591
+ }
592
+ }
593
+ console.log(`[ASG] Generated stories for ${totalProcessed} components`);
594
+ },
595
+ async watchChange(id, change) {
596
+ if (change.event === "delete") return;
597
+ const normalizedId = id.replace(/\\/g, "/");
598
+ if (options.imports) {
599
+ const relativePath = normalizedId.replace(projectRootDir, "").replace(/^\//, "");
600
+ const matches = options.imports.some(
601
+ (pattern) => minimatch(relativePath, pattern)
602
+ );
603
+ if (!matches) return;
604
+ }
605
+ if (options.ignores) {
606
+ const relativePath = normalizedId.replace(projectRootDir, "").replace(/^\//, "");
607
+ const ignored = options.ignores.some(
608
+ (pattern) => minimatch(relativePath, pattern)
609
+ );
610
+ if (ignored) return;
611
+ }
612
+ mtimeCache.delete(normalizedId);
613
+ await genStoryFile({
614
+ options,
615
+ id: normalizedId,
616
+ projectRootDir
617
+ });
618
+ }
619
+ };
620
+ }
621
+ var index_default = { vite: createAutoStoryPlugin };
18
622
  var FIXED_MARKER = "// @onlook-fixed";
19
623
  var parser = withDefaultConfig({
20
624
  shouldExtractLiteralValuesFromEnum: true,
@@ -25,12 +629,12 @@ var parser = withDefaultConfig({
25
629
  }
26
630
  });
27
631
  function resolveComponentPath(storyFilePath) {
28
- const dir = path6.dirname(storyFilePath);
29
- const parentDir = path6.dirname(dir);
30
- const storyName = path6.basename(storyFilePath);
632
+ const dir = path3.dirname(storyFilePath);
633
+ const parentDir = path3.dirname(dir);
634
+ const storyName = path3.basename(storyFilePath);
31
635
  const componentName = storyName.replace(".stories.tsx", ".tsx");
32
- const componentPath = path6.join(parentDir, componentName);
33
- return fs5.existsSync(componentPath) ? componentPath : null;
636
+ const componentPath = path3.join(parentDir, componentName);
637
+ return fs.existsSync(componentPath) ? componentPath : null;
34
638
  }
35
639
  function generateArgTypes(componentPath) {
36
640
  try {
@@ -71,7 +675,7 @@ function generateArgTypes(componentPath) {
71
675
  }
72
676
  function enrichStoryFile(storyFilePath) {
73
677
  try {
74
- const content = fs5.readFileSync(storyFilePath, "utf-8");
678
+ const content = fs.readFileSync(storyFilePath, "utf-8");
75
679
  if (content.includes(FIXED_MARKER)) {
76
680
  return;
77
681
  }
@@ -90,8 +694,8 @@ function enrichStoryFile(storyFilePath) {
90
694
  export default meta;`
91
695
  );
92
696
  if (enriched !== content) {
93
- fs5.writeFileSync(storyFilePath, enriched);
94
- console.log(`[AutoStories] Enriched ${path6.basename(storyFilePath)} with argTypes`);
697
+ fs.writeFileSync(storyFilePath, enriched);
698
+ console.log(`[AutoStories] Enriched ${path3.basename(storyFilePath)} with argTypes`);
95
699
  }
96
700
  } catch (err) {
97
701
  console.error(`[AutoStories] Failed to enrich story: ${storyFilePath}`, err);
@@ -119,7 +723,7 @@ function componentLocPlugin(options = {}) {
119
723
  sourceFilename: filepath
120
724
  });
121
725
  let mutated = false;
122
- const relativePath = path6.relative(root, filepath);
726
+ const relativePath = path3.relative(root, filepath);
123
727
  traverse(ast, {
124
728
  JSXElement(nodePath) {
125
729
  const opening = nodePath.node.openingElement;
@@ -156,9 +760,9 @@ function componentLocPlugin(options = {}) {
156
760
  }
157
761
  };
158
762
  }
159
- var CACHE_DIR = path6.join(process.cwd(), ".storybook-cache");
160
- var SCREENSHOTS_DIR = path6.join(CACHE_DIR, "screenshots");
161
- var MANIFEST_PATH = path6.join(CACHE_DIR, "manifest.json");
763
+ var CACHE_DIR = path3.join(process.cwd(), ".storybook-cache");
764
+ var SCREENSHOTS_DIR = path3.join(CACHE_DIR, "screenshots");
765
+ var MANIFEST_PATH = path3.join(CACHE_DIR, "manifest.json");
162
766
  var VIEWPORT_WIDTH = 1920;
163
767
  var VIEWPORT_HEIGHT = 1080;
164
768
  var MIN_COMPONENT_WIDTH = 420;
@@ -166,30 +770,30 @@ var MIN_COMPONENT_HEIGHT = 280;
166
770
 
167
771
  // src/utils/fileSystem/fileSystem.ts
168
772
  function ensureCacheDirectories() {
169
- if (!fs5.existsSync(CACHE_DIR)) {
170
- fs5.mkdirSync(CACHE_DIR, { recursive: true });
773
+ if (!fs.existsSync(CACHE_DIR)) {
774
+ fs.mkdirSync(CACHE_DIR, { recursive: true });
171
775
  }
172
- if (!fs5.existsSync(SCREENSHOTS_DIR)) {
173
- fs5.mkdirSync(SCREENSHOTS_DIR, { recursive: true });
776
+ if (!fs.existsSync(SCREENSHOTS_DIR)) {
777
+ fs.mkdirSync(SCREENSHOTS_DIR, { recursive: true });
174
778
  }
175
779
  }
176
780
  function computeFileHash(filePath) {
177
- if (!fs5.existsSync(filePath)) {
781
+ if (!fs.existsSync(filePath)) {
178
782
  return "";
179
783
  }
180
- const content = fs5.readFileSync(filePath, "utf-8");
784
+ const content = fs.readFileSync(filePath, "utf-8");
181
785
  return crypto.createHash("sha256").update(content).digest("hex");
182
786
  }
183
787
  function loadManifest() {
184
- if (fs5.existsSync(MANIFEST_PATH)) {
185
- const content = fs5.readFileSync(MANIFEST_PATH, "utf-8");
788
+ if (fs.existsSync(MANIFEST_PATH)) {
789
+ const content = fs.readFileSync(MANIFEST_PATH, "utf-8");
186
790
  return JSON.parse(content);
187
791
  }
188
792
  return { stories: {} };
189
793
  }
190
794
  function saveManifest(manifest) {
191
795
  ensureCacheDirectories();
192
- fs5.writeFileSync(MANIFEST_PATH, JSON.stringify(manifest, null, 2));
796
+ fs.writeFileSync(MANIFEST_PATH, JSON.stringify(manifest, null, 2));
193
797
  }
194
798
  function updateManifest(storyId, sourcePath, fileHash, boundingBox) {
195
799
  const manifest = loadManifest();
@@ -215,8 +819,8 @@ async function getBrowser() {
215
819
  return browser;
216
820
  }
217
821
  function getScreenshotPath(storyId, theme) {
218
- const storyDir = path6.join(SCREENSHOTS_DIR, storyId);
219
- return path6.join(storyDir, `${theme}.png`);
822
+ const storyDir = path3.join(SCREENSHOTS_DIR, storyId);
823
+ return path3.join(storyDir, `${theme}.png`);
220
824
  }
221
825
  async function captureScreenshotBuffer(storyId, theme, width = VIEWPORT_WIDTH, height = VIEWPORT_HEIGHT, storybookUrl = "http://localhost:6006", timeoutMs = 3e4) {
222
826
  const browser2 = await getBrowser();
@@ -303,9 +907,9 @@ async function captureScreenshotBuffer(storyId, theme, width = VIEWPORT_WIDTH, h
303
907
  async function generateScreenshot(storyId, theme, storybookUrl = "http://localhost:6006", timeoutMs = 3e4) {
304
908
  try {
305
909
  ensureCacheDirectories();
306
- const storyDir = path6.join(SCREENSHOTS_DIR, storyId);
307
- if (!fs5.existsSync(storyDir)) {
308
- fs5.mkdirSync(storyDir, { recursive: true });
910
+ const storyDir = path3.join(SCREENSHOTS_DIR, storyId);
911
+ if (!fs.existsSync(storyDir)) {
912
+ fs.mkdirSync(storyDir, { recursive: true });
309
913
  }
310
914
  const screenshotPath = getScreenshotPath(storyId, theme);
311
915
  const { buffer, boundingBox } = await captureScreenshotBuffer(
@@ -316,7 +920,7 @@ async function generateScreenshot(storyId, theme, storybookUrl = "http://localho
316
920
  storybookUrl,
317
921
  timeoutMs
318
922
  );
319
- fs5.writeFileSync(screenshotPath, buffer);
923
+ fs.writeFileSync(screenshotPath, buffer);
320
924
  return { path: screenshotPath, boundingBox };
321
925
  } catch (error) {
322
926
  console.error(`Error generating screenshot for ${storyId} (${theme}):`, error);
@@ -343,7 +947,7 @@ async function fetchStorybookIndex() {
343
947
  }
344
948
  function getStoriesForFile(filePath) {
345
949
  if (!cachedIndex) return [];
346
- const fileName = path6.basename(filePath);
950
+ const fileName = path3.basename(filePath);
347
951
  return Object.values(cachedIndex.entries).filter((entry) => entry.type === "story" && entry.importPath.endsWith(fileName)).map((entry) => entry.id);
348
952
  }
349
953
  async function regenerateScreenshotsForFiles(files) {
@@ -432,18 +1036,6 @@ var DEFAULT_ALLOWED_ORIGINS = [
432
1036
  ];
433
1037
  var AUTO_STORIES_FOLDER = ".onlook-stories";
434
1038
  var storyRuntimeErrors = /* @__PURE__ */ new Map();
435
- var discoveryConfig = {
436
- imports: ["src/**/*.tsx"],
437
- ignores: []
438
- };
439
- function deriveTitleFromPath(filePath) {
440
- const parts = filePath.replace(/^src\//, "").split("/");
441
- parts.pop();
442
- return parts.filter((p) => !p.startsWith("(") && !p.startsWith("[")).join("/");
443
- }
444
- function toStoryId(title, name) {
445
- return `${title.replace(/\//g, "-").toLowerCase()}--${name.toLowerCase()}`;
446
- }
447
1039
  var serveMetadataAndScreenshots = (req, res, next) => {
448
1040
  if (req.url === "/onbook-health.json") {
449
1041
  const cacheBuster = Date.now();
@@ -515,124 +1107,9 @@ var serveMetadataAndScreenshots = (req, res, next) => {
515
1107
  });
516
1108
  return;
517
1109
  }
518
- if (req.url === "/onbook-components.json") {
519
- try {
520
- const { globSync } = __require("glob");
521
- const files = discoveryConfig.imports.flatMap(
522
- (pattern) => globSync(pattern, {
523
- ignore: discoveryConfig.ignores,
524
- cwd: process.cwd()
525
- })
526
- );
527
- const components = files.map((file) => ({
528
- filePath: file,
529
- componentName: path6.basename(file, path6.extname(file)),
530
- title: deriveTitleFromPath(file)
531
- }));
532
- res.setHeader("Content-Type", "application/json");
533
- res.setHeader("Access-Control-Allow-Origin", "*");
534
- res.end(JSON.stringify({ components }));
535
- } catch (error) {
536
- res.statusCode = 500;
537
- res.setHeader("Content-Type", "application/json");
538
- res.end(
539
- JSON.stringify({
540
- error: "Failed to discover components",
541
- details: String(error)
542
- })
543
- );
544
- }
545
- return;
546
- }
547
- if (req.url === "/onbook-generate-story" && req.method === "POST") {
548
- let body = "";
549
- req.on("data", (chunk) => {
550
- body += chunk.toString();
551
- });
552
- req.on("end", () => {
553
- try {
554
- const { filePath } = JSON.parse(body);
555
- if (!filePath) {
556
- res.statusCode = 400;
557
- res.setHeader("Content-Type", "application/json");
558
- res.end(JSON.stringify({ error: "filePath is required" }));
559
- return;
560
- }
561
- const absPath = path6.resolve(filePath);
562
- const dir = path6.dirname(absPath);
563
- const ext = path6.extname(absPath);
564
- const baseName = path6.basename(absPath, ext);
565
- const storyDir = path6.join(dir, AUTO_STORIES_FOLDER);
566
- const storyPath = path6.join(storyDir, `${baseName}.stories.tsx`);
567
- const title = deriveTitleFromPath(filePath);
568
- const storyId = toStoryId(title, "default");
569
- if (fs5.existsSync(storyPath)) {
570
- res.setHeader("Content-Type", "application/json");
571
- res.setHeader("Access-Control-Allow-Origin", "*");
572
- res.end(
573
- JSON.stringify({
574
- storyId,
575
- storyPath,
576
- importPath: `./${filePath.replace(ext, `/${AUTO_STORIES_FOLDER}/${baseName}.stories.tsx`)}`,
577
- existed: true
578
- })
579
- );
580
- return;
581
- }
582
- const storyContent = [
583
- "import type { Meta, StoryObj } from '@storybook/react';",
584
- `import ${baseName} from '../${baseName}';`,
585
- "",
586
- `const meta: Meta<typeof ${baseName}> = {`,
587
- ` title: '${title}',`,
588
- ` component: ${baseName},`,
589
- "};",
590
- "export default meta;",
591
- "",
592
- `type Story = StoryObj<typeof ${baseName}>;`,
593
- "",
594
- "export const Default: Story = {};",
595
- ""
596
- ].join("\n");
597
- if (!fs5.existsSync(storyDir)) {
598
- fs5.mkdirSync(storyDir, { recursive: true });
599
- }
600
- fs5.writeFileSync(storyPath, storyContent);
601
- console.log(`[STORYBOOK_PLUGIN] Generated story: ${storyPath}`);
602
- res.setHeader("Content-Type", "application/json");
603
- res.setHeader("Access-Control-Allow-Origin", "*");
604
- res.end(
605
- JSON.stringify({
606
- storyId,
607
- storyPath,
608
- importPath: `./${path6.relative(process.cwd(), storyPath)}`,
609
- existed: false
610
- })
611
- );
612
- } catch (error) {
613
- res.statusCode = 500;
614
- res.setHeader("Content-Type", "application/json");
615
- res.end(
616
- JSON.stringify({
617
- error: "Failed to generate story",
618
- details: String(error)
619
- })
620
- );
621
- }
622
- });
623
- return;
624
- }
625
- if (req.method === "OPTIONS" && req.url?.startsWith("/onbook-")) {
626
- res.setHeader("Access-Control-Allow-Origin", "*");
627
- res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
628
- res.setHeader("Access-Control-Allow-Headers", "Content-Type");
629
- res.statusCode = 204;
630
- res.end();
631
- return;
632
- }
633
1110
  if (req.url === "/onbook-index.json") {
634
1111
  console.log("[STORYBOOK_PLUGIN] Serving /onbook-index.json endpoint");
635
- const manifestPath = path6.join(process.cwd(), ".storybook-cache", "manifest.json");
1112
+ const manifestPath = path3.join(process.cwd(), ".storybook-cache", "manifest.json");
636
1113
  const cacheBuster = Date.now();
637
1114
  console.log("[STORYBOOK_PLUGIN] Fetching http://localhost:6006/index.json");
638
1115
  fetch(`http://localhost:6006/index.json?_t=${cacheBuster}`, {
@@ -649,7 +1126,7 @@ var serveMetadataAndScreenshots = (req, res, next) => {
649
1126
  });
650
1127
  return response.json();
651
1128
  }).then((indexData) => {
652
- const manifest = fs5.existsSync(manifestPath) ? JSON.parse(fs5.readFileSync(manifestPath, "utf-8")) : { stories: {} };
1129
+ const manifest = fs.existsSync(manifestPath) ? JSON.parse(fs.readFileSync(manifestPath, "utf-8")) : { stories: {} };
653
1130
  const defaultBoundingBox = { width: 1920, height: 1080 };
654
1131
  for (const [storyId, entry] of Object.entries(indexData.entries || {})) {
655
1132
  const manifestEntry = manifest.stories?.[storyId];
@@ -717,7 +1194,7 @@ var serveMetadataAndScreenshots = (req, res, next) => {
717
1194
  return;
718
1195
  }
719
1196
  if (req.url?.startsWith("/screenshots/")) {
720
- const screenshotPath = path6.join(
1197
+ const screenshotPath = path3.join(
721
1198
  process.cwd(),
722
1199
  ".storybook-cache",
723
1200
  req.url.replace("/screenshots/", "screenshots/")
@@ -726,11 +1203,11 @@ var serveMetadataAndScreenshots = (req, res, next) => {
726
1203
  const storyId = urlParts[0];
727
1204
  const themeFile = urlParts[1];
728
1205
  const theme = themeFile?.replace(".png", "");
729
- if (fs5.existsSync(screenshotPath)) {
1206
+ if (fs.existsSync(screenshotPath)) {
730
1207
  res.setHeader("Content-Type", "image/png");
731
1208
  res.setHeader("Access-Control-Allow-Origin", "*");
732
1209
  res.setHeader("Cache-Control", "public, max-age=3600");
733
- fs5.createReadStream(screenshotPath).pipe(res);
1210
+ fs.createReadStream(screenshotPath).pipe(res);
734
1211
  return;
735
1212
  }
736
1213
  if (storyId && theme && (theme === "light" || theme === "dark")) {
@@ -738,16 +1215,16 @@ var serveMetadataAndScreenshots = (req, res, next) => {
738
1215
  `[STORYBOOK_PLUGIN] Generating screenshot on-demand: ${storyId}/${theme}`
739
1216
  );
740
1217
  captureScreenshotBuffer(storyId, theme).then(({ buffer }) => {
741
- const storyDir = path6.join(
1218
+ const storyDir = path3.join(
742
1219
  process.cwd(),
743
1220
  ".storybook-cache",
744
1221
  "screenshots",
745
1222
  storyId
746
1223
  );
747
- if (!fs5.existsSync(storyDir)) {
748
- fs5.mkdirSync(storyDir, { recursive: true });
1224
+ if (!fs.existsSync(storyDir)) {
1225
+ fs.mkdirSync(storyDir, { recursive: true });
749
1226
  }
750
- fs5.writeFileSync(screenshotPath, buffer);
1227
+ fs.writeFileSync(screenshotPath, buffer);
751
1228
  res.setHeader("Content-Type", "image/png");
752
1229
  res.setHeader("Access-Control-Allow-Origin", "*");
753
1230
  res.setHeader("Cache-Control", "public, max-age=3600");
@@ -830,8 +1307,8 @@ function storybookOnlookPlugin(options = {}) {
830
1307
  };
831
1308
  const plugins = [componentLocPlugin(), mainPlugin];
832
1309
  if (options.autoStories !== false) {
833
- discoveryConfig.imports = options.autoStories ?? ["src/**/*.tsx"];
834
- discoveryConfig.ignores = options.autoStoriesIgnore ?? [
1310
+ const imports = options.autoStories ?? ["src/**/*.tsx"];
1311
+ const ignores = options.autoStoriesIgnore ?? [
835
1312
  "src/**/*.stories.tsx",
836
1313
  "src/**/*.stories.ts",
837
1314
  "src/**/*.test.tsx",
@@ -841,10 +1318,27 @@ function storybookOnlookPlugin(options = {}) {
841
1318
  "node_modules/**",
842
1319
  "**/.onlook-stories/**"
843
1320
  ];
844
- console.log("[STORYBOOK_PLUGIN] Component discovery configured", {
845
- imports: discoveryConfig.imports,
846
- ignores: discoveryConfig.ignores
1321
+ console.log("[STORYBOOK_PLUGIN] Auto-story generation enabled", {
1322
+ imports,
1323
+ ignores,
1324
+ storiesFolder: AUTO_STORIES_FOLDER
847
1325
  });
1326
+ try {
1327
+ plugins.push(
1328
+ index_default.vite({
1329
+ preset: "react",
1330
+ imports,
1331
+ ignores,
1332
+ storiesFolder: AUTO_STORIES_FOLDER,
1333
+ isGenerateStoriesFileAtBuild: true
1334
+ })
1335
+ );
1336
+ } catch (err) {
1337
+ console.error(
1338
+ "[STORYBOOK_PLUGIN] ASG plugin failed to initialize, continuing without auto-stories",
1339
+ err
1340
+ );
1341
+ }
848
1342
  }
849
1343
  return plugins;
850
1344
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onlook/storybook-plugin",
3
- "version": "0.4.0-beta.2",
3
+ "version": "0.4.0-beta.4",
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
- "glob": "^11.0.0",
40
+ "glob": "^10.3.10",
41
+ "minimatch": "^9.0.3",
41
42
  "playwright": "^1.52.0",
42
- "react-docgen-typescript": "^2.4.0"
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:*",