storybook-onbook-plugin 0.1.0

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 (65) hide show
  1. package/README.md +64 -0
  2. package/dist/component-loc/component-loc.d.ts +7 -0
  3. package/dist/component-loc/component-loc.d.ts.map +1 -0
  4. package/dist/component-loc/component-loc.js +58 -0
  5. package/dist/component-loc/index.d.ts +2 -0
  6. package/dist/component-loc/index.d.ts.map +1 -0
  7. package/dist/component-loc/index.js +1 -0
  8. package/dist/index.d.ts +2 -0
  9. package/dist/index.d.ts.map +1 -0
  10. package/dist/index.js +1 -0
  11. package/dist/onlook-plugin/handlers/handleStoryFileChange/handleStoryFileChange.d.ts +3 -0
  12. package/dist/onlook-plugin/handlers/handleStoryFileChange/handleStoryFileChange.d.ts.map +1 -0
  13. package/dist/onlook-plugin/handlers/handleStoryFileChange/handleStoryFileChange.js +100 -0
  14. package/dist/onlook-plugin/handlers/handleStoryFileChange/index.d.ts +2 -0
  15. package/dist/onlook-plugin/handlers/handleStoryFileChange/index.d.ts.map +1 -0
  16. package/dist/onlook-plugin/handlers/handleStoryFileChange/index.js +1 -0
  17. package/dist/onlook-plugin/index.d.ts +2 -0
  18. package/dist/onlook-plugin/index.d.ts.map +1 -0
  19. package/dist/onlook-plugin/index.js +1 -0
  20. package/dist/onlook-plugin/screenshot-service/constants.d.ts +8 -0
  21. package/dist/onlook-plugin/screenshot-service/constants.d.ts.map +1 -0
  22. package/dist/onlook-plugin/screenshot-service/constants.js +11 -0
  23. package/dist/onlook-plugin/screenshot-service/index.d.ts +5 -0
  24. package/dist/onlook-plugin/screenshot-service/index.d.ts.map +1 -0
  25. package/dist/onlook-plugin/screenshot-service/index.js +5 -0
  26. package/dist/onlook-plugin/screenshot-service/screenshot-service.d.ts +8 -0
  27. package/dist/onlook-plugin/screenshot-service/screenshot-service.d.ts.map +1 -0
  28. package/dist/onlook-plugin/screenshot-service/screenshot-service.js +36 -0
  29. package/dist/onlook-plugin/screenshot-service/types.d.ts +18 -0
  30. package/dist/onlook-plugin/screenshot-service/types.d.ts.map +1 -0
  31. package/dist/onlook-plugin/screenshot-service/types.js +0 -0
  32. package/dist/onlook-plugin/screenshot-service/utils/browser/browser.d.ts +10 -0
  33. package/dist/onlook-plugin/screenshot-service/utils/browser/browser.d.ts.map +1 -0
  34. package/dist/onlook-plugin/screenshot-service/utils/browser/browser.js +29 -0
  35. package/dist/onlook-plugin/screenshot-service/utils/browser/index.d.ts +2 -0
  36. package/dist/onlook-plugin/screenshot-service/utils/browser/index.d.ts.map +1 -0
  37. package/dist/onlook-plugin/screenshot-service/utils/browser/index.js +1 -0
  38. package/dist/onlook-plugin/screenshot-service/utils/screenshot/index.d.ts +2 -0
  39. package/dist/onlook-plugin/screenshot-service/utils/screenshot/index.d.ts.map +1 -0
  40. package/dist/onlook-plugin/screenshot-service/utils/screenshot/index.js +1 -0
  41. package/dist/onlook-plugin/screenshot-service/utils/screenshot/screenshot.d.ts +26 -0
  42. package/dist/onlook-plugin/screenshot-service/utils/screenshot/screenshot.d.ts.map +1 -0
  43. package/dist/onlook-plugin/screenshot-service/utils/screenshot/screenshot.js +128 -0
  44. package/dist/onlook-plugin/storybook-onlook-plugin.d.ts +9 -0
  45. package/dist/onlook-plugin/storybook-onlook-plugin.d.ts.map +1 -0
  46. package/dist/onlook-plugin/storybook-onlook-plugin.js +151 -0
  47. package/dist/onlook-plugin/utils/fileSystem/fileSystem.d.ts +9 -0
  48. package/dist/onlook-plugin/utils/fileSystem/fileSystem.d.ts.map +1 -0
  49. package/dist/onlook-plugin/utils/fileSystem/fileSystem.js +24 -0
  50. package/dist/onlook-plugin/utils/fileSystem/index.d.ts +2 -0
  51. package/dist/onlook-plugin/utils/fileSystem/index.d.ts.map +1 -0
  52. package/dist/onlook-plugin/utils/fileSystem/index.js +1 -0
  53. package/dist/onlook-plugin/utils/findGitRoot/findGitRoot.d.ts +5 -0
  54. package/dist/onlook-plugin/utils/findGitRoot/findGitRoot.d.ts.map +1 -0
  55. package/dist/onlook-plugin/utils/findGitRoot/findGitRoot.js +15 -0
  56. package/dist/onlook-plugin/utils/findGitRoot/index.d.ts +2 -0
  57. package/dist/onlook-plugin/utils/findGitRoot/index.d.ts.map +1 -0
  58. package/dist/onlook-plugin/utils/findGitRoot/index.js +1 -0
  59. package/dist/onlook-plugin/utils/manifest/index.d.ts +2 -0
  60. package/dist/onlook-plugin/utils/manifest/index.d.ts.map +1 -0
  61. package/dist/onlook-plugin/utils/manifest/index.js +1 -0
  62. package/dist/onlook-plugin/utils/manifest/manifest.d.ts +21 -0
  63. package/dist/onlook-plugin/utils/manifest/manifest.d.ts.map +1 -0
  64. package/dist/onlook-plugin/utils/manifest/manifest.js +44 -0
  65. package/package.json +40 -0
package/README.md ADDED
@@ -0,0 +1,64 @@
1
+ # storybook-onbook-plugin
2
+
3
+ Vite plugin for Storybook that adds component location tracking, E2B sandbox HMR support, and screenshot capture APIs.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install storybook-onbook-plugin
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ Add to your `.storybook/main.ts`:
14
+
15
+ ```ts
16
+ import { storybookOnlookPlugin } from 'storybook-onbook-plugin';
17
+
18
+ export default {
19
+ // ... your config
20
+ async viteFinal(config) {
21
+ const { mergeConfig } = await import('vite');
22
+ return mergeConfig(config, {
23
+ plugins: [storybookOnlookPlugin()],
24
+ });
25
+ },
26
+ };
27
+ ```
28
+
29
+ ## Features
30
+
31
+ ### Component Location Tracking
32
+
33
+ Adds `data-component-file` attributes to React components with their source file paths, enabling click-to-source functionality.
34
+
35
+ ### E2B Sandbox HMR
36
+
37
+ Configures Vite HMR for E2B sandboxes (WSS protocol, port 443 routing).
38
+
39
+ ### Screenshot API
40
+
41
+ - `GET /api/capture-screenshot?storyId=<id>&theme=light|dark&width=800&height=600` - Capture on-demand screenshots
42
+ - `GET /screenshots/<path>` - Serve cached screenshots
43
+ - `GET /onbook-index.json` - Extended Storybook index with bounding box metadata
44
+
45
+ ### CORS
46
+
47
+ Pre-configured for `https://app.onlook.ai`, `localhost:3000`, and `localhost:6006`.
48
+
49
+ ## Options
50
+
51
+ ```ts
52
+ storybookOnlookPlugin({
53
+ port: 6006, // Storybook port (default: 6006)
54
+ allowedOrigins: [], // Additional CORS origins
55
+ });
56
+ ```
57
+
58
+ ## CI/Chromatic
59
+
60
+ The plugin auto-disables when `CHROMATIC` or `CI` environment variables are set.
61
+
62
+ ## License
63
+
64
+ MIT
@@ -0,0 +1,7 @@
1
+ import type { Plugin } from 'vite';
2
+ type ComponentLocPluginOptions = {
3
+ include?: RegExp;
4
+ };
5
+ export declare function componentLocPlugin(options?: ComponentLocPluginOptions): Plugin;
6
+ export default componentLocPlugin;
7
+ //# sourceMappingURL=component-loc.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"component-loc.d.ts","sourceRoot":"","sources":["../../src/component-loc/component-loc.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,MAAM,CAAC;AAEnC,KAAK,yBAAyB,GAAG;IAC/B,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB,CAAC;AAEF,wBAAgB,kBAAkB,CAAC,OAAO,GAAE,yBAA8B,GAAG,MAAM,CAgFlF;AAED,eAAe,kBAAkB,CAAC"}
@@ -0,0 +1,58 @@
1
+ import path from 'node:path';
2
+ import generateModule from '@babel/generator';
3
+ import { parse } from '@babel/parser';
4
+ import traverseModule from '@babel/traverse';
5
+ import * as t from '@babel/types';
6
+ export function componentLocPlugin(options = {}) {
7
+ const include = options.include ?? /\.(jsx|tsx)$/;
8
+ // @babel/traverse and @babel/generator are CommonJS modules with default exports.
9
+ // When imported in an ESM context, the default export may be on `.default` or directly on the module.
10
+ // This workaround handles both cases to ensure compatibility across different bundler configurations.
11
+ const traverse = traverseModule.default ??
12
+ traverseModule;
13
+ const generate = generateModule.default ??
14
+ generateModule;
15
+ let root;
16
+ return {
17
+ name: 'onbook-component-loc',
18
+ enforce: 'pre',
19
+ apply: 'serve',
20
+ configResolved(config) {
21
+ root = config.root;
22
+ },
23
+ transform(code, id) {
24
+ const filepath = id.split('?', 1)[0];
25
+ if (!filepath || filepath.includes('node_modules'))
26
+ return null;
27
+ if (!include.test(filepath))
28
+ return null;
29
+ const ast = parse(code, {
30
+ sourceType: 'module',
31
+ plugins: ['jsx', 'typescript'],
32
+ sourceFilename: filepath,
33
+ });
34
+ let mutated = false;
35
+ // Get relative path from project root
36
+ const relativePath = path.relative(root, filepath);
37
+ traverse(ast, {
38
+ JSXElement(nodePath) {
39
+ const opening = nodePath.node.openingElement;
40
+ const element = nodePath.node;
41
+ if (!opening.loc || !element.loc)
42
+ return;
43
+ const alreadyTagged = opening.attributes.some((attribute) => t.isJSXAttribute(attribute) &&
44
+ attribute.name.name === 'data-component-file');
45
+ if (alreadyTagged)
46
+ return;
47
+ opening.attributes.push(t.jsxAttribute(t.jsxIdentifier('data-component-file'), t.stringLiteral(relativePath)), t.jsxAttribute(t.jsxIdentifier('data-component-start'), t.stringLiteral(`${element.loc.start.line}:${element.loc.start.column}`)), t.jsxAttribute(t.jsxIdentifier('data-component-end'), t.stringLiteral(`${element.loc.end.line}:${element.loc.end.column}`)));
48
+ mutated = true;
49
+ },
50
+ });
51
+ if (!mutated)
52
+ return null;
53
+ const output = generate(ast, { retainLines: true, sourceMaps: true, sourceFileName: id }, code);
54
+ return { code: output.code, map: output.map };
55
+ },
56
+ };
57
+ }
58
+ export default componentLocPlugin;
@@ -0,0 +1,2 @@
1
+ export { componentLocPlugin } from './component-loc';
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/component-loc/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC"}
@@ -0,0 +1 @@
1
+ export { componentLocPlugin } from './component-loc';
@@ -0,0 +1,2 @@
1
+ export { storybookOnlookPlugin, type OnlookPluginOptions } from './onlook-plugin/index';
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,qBAAqB,EAAE,KAAK,mBAAmB,EAAE,MAAM,uBAAuB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ export { storybookOnlookPlugin } from './onlook-plugin/index';
@@ -0,0 +1,3 @@
1
+ import type { HmrContext } from 'vite';
2
+ export declare function handleStoryFileChange({ file, modules }: HmrContext): import("vite").ModuleNode[] | undefined;
3
+ //# sourceMappingURL=handleStoryFileChange.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"handleStoryFileChange.d.ts","sourceRoot":"","sources":["../../../../src/onlook-plugin/handlers/handleStoryFileChange/handleStoryFileChange.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,MAAM,CAAC;AA4FvC,wBAAgB,qBAAqB,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,EAAE,UAAU,2CAiClE"}
@@ -0,0 +1,100 @@
1
+ import path from 'node:path';
2
+ import { generateScreenshot } from '../../screenshot-service/index';
3
+ import { computeFileHash } from '../../utils/fileSystem/index';
4
+ import { updateManifest } from '../../utils/manifest/index';
5
+ // Cache for Storybook's index.json
6
+ let cachedIndex = null;
7
+ let indexFetchPromise = null;
8
+ // Debounce state
9
+ const pendingFiles = new Set();
10
+ let debounceTimer = null;
11
+ const DEBOUNCE_MS = 500;
12
+ async function fetchStorybookIndex() {
13
+ try {
14
+ const response = await fetch('http://localhost:6006/index.json');
15
+ if (response.ok) {
16
+ cachedIndex = await response.json();
17
+ console.log('[Screenshots] Cached Storybook index');
18
+ }
19
+ }
20
+ catch (error) {
21
+ console.error('[Screenshots] Failed to fetch Storybook index:', error);
22
+ }
23
+ }
24
+ function getStoriesForFile(filePath) {
25
+ if (!cachedIndex)
26
+ return [];
27
+ // Normalize the file path to match Storybook's importPath format
28
+ const fileName = path.basename(filePath);
29
+ return Object.values(cachedIndex.entries)
30
+ .filter((entry) => entry.type === 'story' && entry.importPath.endsWith(fileName))
31
+ .map((entry) => entry.id);
32
+ }
33
+ async function regenerateScreenshotsForFiles(files) {
34
+ // Refresh index before regenerating (in case new stories were added)
35
+ await fetchStorybookIndex();
36
+ const allStoryIds = new Set();
37
+ const fileToStories = new Map();
38
+ for (const file of files) {
39
+ const storyIds = getStoriesForFile(file);
40
+ fileToStories.set(file, storyIds);
41
+ for (const id of storyIds) {
42
+ allStoryIds.add(id);
43
+ }
44
+ }
45
+ if (allStoryIds.size === 0) {
46
+ console.log('[Screenshots] No stories found for changed files');
47
+ return;
48
+ }
49
+ console.log(`[Screenshots] Regenerating ${allStoryIds.size} stories from ${files.length} files`);
50
+ const storybookUrl = 'http://localhost:6006';
51
+ // Regenerate screenshots for each story (both themes) and collect bounding boxes
52
+ const storyBoundingBoxes = new Map();
53
+ await Promise.all(Array.from(allStoryIds).map(async (storyId) => {
54
+ const [lightResult, _darkResult] = await Promise.all([
55
+ generateScreenshot(storyId, 'light', storybookUrl),
56
+ generateScreenshot(storyId, 'dark', storybookUrl),
57
+ ]);
58
+ // Use bounding box from light theme
59
+ if (lightResult) {
60
+ storyBoundingBoxes.set(storyId, lightResult.boundingBox);
61
+ }
62
+ }));
63
+ // Update manifest with new hashes and bounding boxes
64
+ for (const [file, storyIds] of fileToStories) {
65
+ const fileHash = computeFileHash(file);
66
+ storyIds.forEach((storyId) => {
67
+ updateManifest(storyId, file, fileHash, storyBoundingBoxes.get(storyId));
68
+ });
69
+ }
70
+ console.log(`[Screenshots] ✓ Regenerated ${allStoryIds.size} stories`);
71
+ }
72
+ export function handleStoryFileChange({ file, modules }) {
73
+ // Detect story file changes
74
+ if (file.endsWith('.stories.tsx') || file.endsWith('.stories.ts')) {
75
+ console.log(`[Screenshots] Story file changed: ${file}`);
76
+ // Initialize index cache on first change
77
+ if (!cachedIndex && !indexFetchPromise) {
78
+ indexFetchPromise = fetchStorybookIndex();
79
+ }
80
+ // Add to pending files
81
+ pendingFiles.add(file);
82
+ // Debounce regeneration
83
+ if (debounceTimer) {
84
+ clearTimeout(debounceTimer);
85
+ }
86
+ debounceTimer = setTimeout(async () => {
87
+ const files = Array.from(pendingFiles);
88
+ pendingFiles.clear();
89
+ debounceTimer = null;
90
+ try {
91
+ await regenerateScreenshotsForFiles(files);
92
+ }
93
+ catch (error) {
94
+ console.error('[Screenshots] Error regenerating screenshots:', error);
95
+ }
96
+ }, DEBOUNCE_MS);
97
+ // Return modules to trigger HMR update in Storybook
98
+ return modules;
99
+ }
100
+ }
@@ -0,0 +1,2 @@
1
+ export { handleStoryFileChange } from './handleStoryFileChange';
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/onlook-plugin/handlers/handleStoryFileChange/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,qBAAqB,EAAE,MAAM,yBAAyB,CAAC"}
@@ -0,0 +1 @@
1
+ export { handleStoryFileChange } from './handleStoryFileChange';
@@ -0,0 +1,2 @@
1
+ export { storybookOnlookPlugin, type OnlookPluginOptions } from './storybook-onlook-plugin';
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/onlook-plugin/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,qBAAqB,EAAE,KAAK,mBAAmB,EAAE,MAAM,2BAA2B,CAAC"}
@@ -0,0 +1 @@
1
+ export { storybookOnlookPlugin } from './storybook-onlook-plugin';
@@ -0,0 +1,8 @@
1
+ export declare const CACHE_DIR: string;
2
+ export declare const SCREENSHOTS_DIR: string;
3
+ export declare const MANIFEST_PATH: string;
4
+ export declare const VIEWPORT_WIDTH = 1920;
5
+ export declare const VIEWPORT_HEIGHT = 1080;
6
+ export declare const MIN_COMPONENT_WIDTH = 420;
7
+ export declare const MIN_COMPONENT_HEIGHT = 280;
8
+ //# sourceMappingURL=constants.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../../../src/onlook-plugin/screenshot-service/constants.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,SAAS,QAA+C,CAAC;AACtE,eAAO,MAAM,eAAe,QAAsC,CAAC;AACnE,eAAO,MAAM,aAAa,QAAwC,CAAC;AAInE,eAAO,MAAM,cAAc,OAAO,CAAC;AACnC,eAAO,MAAM,eAAe,OAAO,CAAC;AAGpC,eAAO,MAAM,mBAAmB,MAAM,CAAC;AACvC,eAAO,MAAM,oBAAoB,MAAM,CAAC"}
@@ -0,0 +1,11 @@
1
+ import path from 'node:path';
2
+ export const CACHE_DIR = path.join(process.cwd(), '.storybook-cache');
3
+ export const SCREENSHOTS_DIR = path.join(CACHE_DIR, 'screenshots');
4
+ export const MANIFEST_PATH = path.join(CACHE_DIR, 'manifest.json');
5
+ // Full HD viewport so page-level mocks render correctly
6
+ // Screenshot clips to actual component size, so this doesn't affect file size
7
+ export const VIEWPORT_WIDTH = 1920;
8
+ export const VIEWPORT_HEIGHT = 1080;
9
+ // Minimum dimensions for components on canvas
10
+ export const MIN_COMPONENT_WIDTH = 420;
11
+ export const MIN_COMPONENT_HEIGHT = 280;
@@ -0,0 +1,5 @@
1
+ export { generateAllScreenshots } from './screenshot-service';
2
+ export type { Manifest, ScreenshotMetadata } from './types';
3
+ export { closeBrowser } from './utils/browser/index';
4
+ export { generateScreenshot, getScreenshotPath, screenshotExists, } from './utils/screenshot/index';
5
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/onlook-plugin/screenshot-service/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,sBAAsB,EAAE,MAAM,sBAAsB,CAAC;AAE9D,YAAY,EAAE,QAAQ,EAAE,kBAAkB,EAAE,MAAM,SAAS,CAAC;AAE5D,OAAO,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AACrD,OAAO,EACL,kBAAkB,EAClB,iBAAiB,EACjB,gBAAgB,GACjB,MAAM,0BAA0B,CAAC"}
@@ -0,0 +1,5 @@
1
+ // Main API
2
+ export { generateAllScreenshots } from './screenshot-service';
3
+ // Re-export screenshot utilities
4
+ export { closeBrowser } from './utils/browser/index';
5
+ export { generateScreenshot, getScreenshotPath, screenshotExists, } from './utils/screenshot/index';
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Generate screenshots for all stories (parallelized for speed)
3
+ */
4
+ export declare function generateAllScreenshots(stories: Array<{
5
+ id: string;
6
+ importPath: string;
7
+ }>, storybookUrl?: string): Promise<void>;
8
+ //# sourceMappingURL=screenshot-service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"screenshot-service.d.ts","sourceRoot":"","sources":["../../../src/onlook-plugin/screenshot-service/screenshot-service.ts"],"names":[],"mappings":"AAKA;;GAEG;AACH,wBAAsB,sBAAsB,CAC1C,OAAO,EAAE,KAAK,CAAC;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,CAAC,EAClD,YAAY,GAAE,MAAgC,GAC7C,OAAO,CAAC,IAAI,CAAC,CAsCf"}
@@ -0,0 +1,36 @@
1
+ import { computeFileHash } from '../utils/fileSystem/index';
2
+ import { updateManifest } from '../utils/manifest/index';
3
+ import { closeBrowser } from './utils/browser/index';
4
+ import { generateScreenshot } from './utils/screenshot/index';
5
+ /**
6
+ * Generate screenshots for all stories (parallelized for speed)
7
+ */
8
+ export async function generateAllScreenshots(stories, storybookUrl = 'http://localhost:6006') {
9
+ console.log(`Generating screenshots for ${stories.length} stories...`);
10
+ // Process stories in batches for better performance
11
+ // Higher than CPU count since work is I/O-bound (waiting for page render)
12
+ const BATCH_SIZE = 10;
13
+ const batches = [];
14
+ for (let i = 0; i < stories.length; i += BATCH_SIZE) {
15
+ batches.push(stories.slice(i, i + BATCH_SIZE));
16
+ }
17
+ let completed = 0;
18
+ for (const batch of batches) {
19
+ await Promise.all(batch.map(async (story) => {
20
+ // Generate both light and dark in parallel for each story
21
+ const [lightResult, darkResult] = await Promise.all([
22
+ generateScreenshot(story.id, 'light', storybookUrl),
23
+ generateScreenshot(story.id, 'dark', storybookUrl),
24
+ ]);
25
+ if (lightResult && darkResult) {
26
+ const fileHash = computeFileHash(story.importPath);
27
+ // Use bounding box from light theme (should be same for both)
28
+ updateManifest(story.id, story.importPath, fileHash, lightResult.boundingBox);
29
+ }
30
+ completed++;
31
+ console.log(`[${completed}/${stories.length}] Generated screenshots for ${story.id}`);
32
+ }));
33
+ }
34
+ await closeBrowser();
35
+ console.log('Screenshot generation complete!');
36
+ }
@@ -0,0 +1,18 @@
1
+ export interface BoundingBox {
2
+ width: number;
3
+ height: number;
4
+ }
5
+ export interface ScreenshotMetadata {
6
+ fileHash: string;
7
+ lastGenerated: string;
8
+ sourcePath: string;
9
+ screenshots: {
10
+ light: string;
11
+ dark: string;
12
+ };
13
+ boundingBox?: BoundingBox;
14
+ }
15
+ export interface Manifest {
16
+ stories: Record<string, ScreenshotMetadata>;
17
+ }
18
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/onlook-plugin/screenshot-service/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,kBAAkB;IACjC,QAAQ,EAAE,MAAM,CAAC;IACjB,aAAa,EAAE,MAAM,CAAC;IACtB,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE;QACX,KAAK,EAAE,MAAM,CAAC;QACd,IAAI,EAAE,MAAM,CAAC;KACd,CAAC;IACF,WAAW,CAAC,EAAE,WAAW,CAAC;CAC3B;AAED,MAAM,WAAW,QAAQ;IACvB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,kBAAkB,CAAC,CAAC;CAC7C"}
File without changes
@@ -0,0 +1,10 @@
1
+ import { type Browser } from 'playwright';
2
+ /**
3
+ * Initialize browser instance
4
+ */
5
+ export declare function getBrowser(): Promise<Browser>;
6
+ /**
7
+ * Close browser instance
8
+ */
9
+ export declare function closeBrowser(): Promise<void>;
10
+ //# sourceMappingURL=browser.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"browser.d.ts","sourceRoot":"","sources":["../../../../../src/onlook-plugin/screenshot-service/utils/browser/browser.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,OAAO,EAAY,MAAM,YAAY,CAAC;AAIpD;;GAEG;AACH,wBAAsB,UAAU,IAAI,OAAO,CAAC,OAAO,CAAC,CAOnD;AAED;;GAEG;AACH,wBAAsB,YAAY,kBAUjC"}
@@ -0,0 +1,29 @@
1
+ import { chromium } from 'playwright';
2
+ let browser = null;
3
+ /**
4
+ * Initialize browser instance
5
+ */
6
+ export async function getBrowser() {
7
+ if (!browser) {
8
+ browser = await chromium.launch({
9
+ headless: true,
10
+ });
11
+ }
12
+ return browser;
13
+ }
14
+ /**
15
+ * Close browser instance
16
+ */
17
+ export async function closeBrowser() {
18
+ if (browser) {
19
+ try {
20
+ await browser.close();
21
+ }
22
+ catch (error) {
23
+ console.error('Error closing browser:', error);
24
+ }
25
+ finally {
26
+ browser = null;
27
+ }
28
+ }
29
+ }
@@ -0,0 +1,2 @@
1
+ export { closeBrowser, getBrowser } from './browser';
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/onlook-plugin/screenshot-service/utils/browser/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC"}
@@ -0,0 +1 @@
1
+ export { closeBrowser, getBrowser } from './browser';
@@ -0,0 +1,2 @@
1
+ export { captureScreenshotBuffer, generateScreenshot, getScreenshotPath, screenshotExists, } from './screenshot';
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/onlook-plugin/screenshot-service/utils/screenshot/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,uBAAuB,EACvB,kBAAkB,EAClB,iBAAiB,EACjB,gBAAgB,GACjB,MAAM,cAAc,CAAC"}
@@ -0,0 +1 @@
1
+ export { captureScreenshotBuffer, generateScreenshot, getScreenshotPath, screenshotExists, } from './screenshot';
@@ -0,0 +1,26 @@
1
+ import type { BoundingBox } from '../../types';
2
+ export interface ScreenshotResult {
3
+ buffer: Buffer;
4
+ boundingBox: BoundingBox | null;
5
+ }
6
+ export interface GenerateScreenshotResult {
7
+ path: string;
8
+ boundingBox: BoundingBox | null;
9
+ }
10
+ /**
11
+ * Get screenshot file path
12
+ */
13
+ export declare function getScreenshotPath(storyId: string, theme: 'light' | 'dark'): string;
14
+ /**
15
+ * Check if screenshot exists
16
+ */
17
+ export declare function screenshotExists(storyId: string, theme: 'light' | 'dark'): boolean;
18
+ /**
19
+ * Capture a screenshot and return it as a Buffer with bounding box info
20
+ */
21
+ export declare function captureScreenshotBuffer(storyId: string, theme: 'light' | 'dark', width?: number, height?: number, storybookUrl?: string): Promise<ScreenshotResult>;
22
+ /**
23
+ * Generate a screenshot for a story and save to disk
24
+ */
25
+ export declare function generateScreenshot(storyId: string, theme: 'light' | 'dark', storybookUrl?: string): Promise<GenerateScreenshotResult | null>;
26
+ //# sourceMappingURL=screenshot.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"screenshot.d.ts","sourceRoot":"","sources":["../../../../../src/onlook-plugin/screenshot-service/utils/screenshot/screenshot.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAG/C,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,WAAW,GAAG,IAAI,CAAC;CACjC;AAED,MAAM,WAAW,wBAAwB;IACvC,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,WAAW,GAAG,IAAI,CAAC;CACjC;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,MAAM,CAGlF;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,OAAO,CAGlF;AAED;;GAEG;AACH,wBAAsB,uBAAuB,CAC3C,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,OAAO,GAAG,MAAM,EACvB,KAAK,GAAE,MAAuB,EAC9B,MAAM,GAAE,MAAwB,EAChC,YAAY,GAAE,MAAgC,GAC7C,OAAO,CAAC,gBAAgB,CAAC,CAiG3B;AAED;;GAEG;AACH,wBAAsB,kBAAkB,CACtC,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,OAAO,GAAG,MAAM,EACvB,YAAY,GAAE,MAAgC,GAC7C,OAAO,CAAC,wBAAwB,GAAG,IAAI,CAAC,CAwB1C"}
@@ -0,0 +1,128 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { ensureCacheDirectories } from '../../../utils/fileSystem/index';
4
+ import { MIN_COMPONENT_HEIGHT, MIN_COMPONENT_WIDTH, SCREENSHOTS_DIR, VIEWPORT_HEIGHT, VIEWPORT_WIDTH, } from '../../constants';
5
+ import { getBrowser } from '../browser/index';
6
+ /**
7
+ * Get screenshot file path
8
+ */
9
+ export function getScreenshotPath(storyId, theme) {
10
+ const storyDir = path.join(SCREENSHOTS_DIR, storyId);
11
+ return path.join(storyDir, `${theme}.png`);
12
+ }
13
+ /**
14
+ * Check if screenshot exists
15
+ */
16
+ export function screenshotExists(storyId, theme) {
17
+ const screenshotPath = getScreenshotPath(storyId, theme);
18
+ return fs.existsSync(screenshotPath);
19
+ }
20
+ /**
21
+ * Capture a screenshot and return it as a Buffer with bounding box info
22
+ */
23
+ export async function captureScreenshotBuffer(storyId, theme, width = VIEWPORT_WIDTH, height = VIEWPORT_HEIGHT, storybookUrl = 'http://localhost:6006') {
24
+ const browser = await getBrowser();
25
+ const context = await browser.newContext({
26
+ viewport: { width, height },
27
+ deviceScaleFactor: 2,
28
+ });
29
+ const page = await context.newPage();
30
+ try {
31
+ // Navigate to story iframe URL
32
+ const url = `${storybookUrl}/iframe.html?id=${storyId}&viewMode=story&globals=theme:${theme}`;
33
+ await page.goto(url, { timeout: 15000 });
34
+ // Wait for page to be fully ready (matching @storybook/test-runner's waitForPageReady)
35
+ await page.waitForLoadState('domcontentloaded');
36
+ await page.waitForLoadState('load');
37
+ await page.waitForLoadState('networkidle');
38
+ await page.evaluate(() => document.fonts.ready);
39
+ // Wait for all images to be fully loaded and decoded
40
+ await page.evaluate(async () => {
41
+ const images = document.querySelectorAll('img');
42
+ await Promise.all(Array.from(images).map((img) => {
43
+ if (img.complete)
44
+ return Promise.resolve();
45
+ return new Promise((resolve) => {
46
+ img.addEventListener('load', resolve);
47
+ img.addEventListener('error', resolve);
48
+ });
49
+ }));
50
+ });
51
+ // Calculate the bounding box of all content inside #storybook-root
52
+ const contentBounds = await page.evaluate(() => {
53
+ const root = document.querySelector('#storybook-root');
54
+ if (!root)
55
+ return null;
56
+ // Get the bounding rect of all children combined
57
+ const children = root.querySelectorAll('*');
58
+ if (children.length === 0)
59
+ return null;
60
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
61
+ children.forEach((child) => {
62
+ const rect = child.getBoundingClientRect();
63
+ if (rect.width === 0 || rect.height === 0)
64
+ return;
65
+ minX = Math.min(minX, rect.left);
66
+ minY = Math.min(minY, rect.top);
67
+ maxX = Math.max(maxX, rect.right);
68
+ maxY = Math.max(maxY, rect.bottom);
69
+ });
70
+ if (minX === Infinity)
71
+ return null;
72
+ return {
73
+ x: minX,
74
+ y: minY,
75
+ width: maxX - minX,
76
+ height: maxY - minY,
77
+ };
78
+ });
79
+ let screenshotBuffer;
80
+ let resultBoundingBox = null;
81
+ if (contentBounds && contentBounds.width > 0 && contentBounds.height > 0) {
82
+ const PADDING = 20; // 10px each side
83
+ const clippedWidth = Math.min(width, contentBounds.width + PADDING);
84
+ const clippedHeight = Math.min(height, contentBounds.height + PADDING);
85
+ // Store dimensions that match the actual screenshot (including padding)
86
+ resultBoundingBox = {
87
+ width: Math.max(MIN_COMPONENT_WIDTH, Math.round(clippedWidth)),
88
+ height: Math.max(MIN_COMPONENT_HEIGHT, Math.round(clippedHeight)),
89
+ };
90
+ screenshotBuffer = await page.screenshot({
91
+ type: 'png',
92
+ clip: {
93
+ x: Math.max(0, contentBounds.x - 10),
94
+ y: Math.max(0, contentBounds.y - 10),
95
+ width: clippedWidth,
96
+ height: clippedHeight,
97
+ },
98
+ });
99
+ }
100
+ else {
101
+ screenshotBuffer = await page.screenshot({ type: 'png' });
102
+ }
103
+ return { buffer: screenshotBuffer, boundingBox: resultBoundingBox };
104
+ }
105
+ finally {
106
+ await context.close();
107
+ }
108
+ }
109
+ /**
110
+ * Generate a screenshot for a story and save to disk
111
+ */
112
+ export async function generateScreenshot(storyId, theme, storybookUrl = 'http://localhost:6006') {
113
+ try {
114
+ ensureCacheDirectories();
115
+ const storyDir = path.join(SCREENSHOTS_DIR, storyId);
116
+ if (!fs.existsSync(storyDir)) {
117
+ fs.mkdirSync(storyDir, { recursive: true });
118
+ }
119
+ const screenshotPath = getScreenshotPath(storyId, theme);
120
+ const { buffer, boundingBox } = await captureScreenshotBuffer(storyId, theme, VIEWPORT_WIDTH, VIEWPORT_HEIGHT, storybookUrl);
121
+ fs.writeFileSync(screenshotPath, buffer);
122
+ return { path: screenshotPath, boundingBox };
123
+ }
124
+ catch (error) {
125
+ console.error(`Error generating screenshot for ${storyId} (${theme}):`, error);
126
+ return null;
127
+ }
128
+ }
@@ -0,0 +1,9 @@
1
+ import type { PluginOption } from 'vite';
2
+ export type OnlookPluginOptions = {
3
+ /** Storybook port (default: 6006) */
4
+ port?: number;
5
+ /** Additional allowed origins for CORS (merged with defaults) */
6
+ allowedOrigins?: string[];
7
+ };
8
+ export declare function storybookOnlookPlugin(options?: OnlookPluginOptions): PluginOption[];
9
+ //# sourceMappingURL=storybook-onlook-plugin.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"storybook-onlook-plugin.d.ts","sourceRoot":"","sources":["../../src/onlook-plugin/storybook-onlook-plugin.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAU,YAAY,EAAiB,MAAM,MAAM,CAAC;AAoBhE,MAAM,MAAM,mBAAmB,GAAG;IAChC,qCAAqC;IACrC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,iEAAiE;IACjE,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;CAC3B,CAAC;AAqHF,wBAAgB,qBAAqB,CAAC,OAAO,GAAE,mBAAwB,GAAG,YAAY,EAAE,CA2CvF"}
@@ -0,0 +1,151 @@
1
+ import fs from 'node:fs';
2
+ import path, { dirname, join, relative } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { componentLocPlugin } from '../component-loc/index';
5
+ import { handleStoryFileChange } from './handlers/handleStoryFileChange/index';
6
+ import { captureScreenshotBuffer } from './screenshot-service/utils/screenshot/index';
7
+ import { findGitRoot } from './utils/findGitRoot/index';
8
+ // Calculate storybook location relative to git root
9
+ const __dirname = dirname(fileURLToPath(import.meta.url));
10
+ const storybookDir = join(__dirname, '..');
11
+ const gitRoot = findGitRoot(storybookDir);
12
+ const storybookLocation = gitRoot ? relative(gitRoot, storybookDir) : '';
13
+ const repoRoot = gitRoot || process.cwd();
14
+ // Default allowed origins for CORS
15
+ const DEFAULT_ALLOWED_ORIGINS = [
16
+ 'https://app.onlook.ai',
17
+ 'http://localhost:3000',
18
+ 'http://localhost:6006',
19
+ ];
20
+ const serveMetadataAndScreenshots = (req, res, next) => {
21
+ // Consolidated endpoint: Storybook index + metadata + boundingBox
22
+ if (req.url === '/onbook-index.json') {
23
+ const manifestPath = path.join(process.cwd(), '.storybook-cache', 'manifest.json');
24
+ // Fetch Storybook's index.json and enrich it
25
+ fetch('http://localhost:6006/index.json')
26
+ .then((response) => response.json())
27
+ // biome-ignore lint/suspicious/noExplicitAny: Storybook index.json structure
28
+ .then((indexData) => {
29
+ // Load manifest for bounding box data
30
+ const manifest = fs.existsSync(manifestPath)
31
+ ? JSON.parse(fs.readFileSync(manifestPath, 'utf-8'))
32
+ : { stories: {} };
33
+ // Default viewport size as fallback
34
+ const defaultBoundingBox = { width: 1920, height: 1080 };
35
+ // Add boundingBox to each entry
36
+ for (const [storyId, entry] of Object.entries(indexData.entries || {})) {
37
+ const manifestEntry = manifest.stories?.[storyId];
38
+ // biome-ignore lint/suspicious/noExplicitAny: extending Storybook entry
39
+ entry.boundingBox = manifestEntry?.boundingBox || defaultBoundingBox;
40
+ }
41
+ // Add metadata
42
+ indexData.meta = { storybookLocation, repoRoot };
43
+ res.setHeader('Content-Type', 'application/json');
44
+ res.setHeader('Access-Control-Allow-Origin', '*');
45
+ res.end(JSON.stringify(indexData));
46
+ })
47
+ .catch((error) => {
48
+ console.error('Failed to fetch/extend index.json:', error);
49
+ res.statusCode = 500;
50
+ res.setHeader('Content-Type', 'application/json');
51
+ res.end(JSON.stringify({ error: 'Failed to fetch index', details: String(error) }));
52
+ });
53
+ return;
54
+ }
55
+ // On-demand screenshot capture API
56
+ if (req.url?.startsWith('/api/capture-screenshot')) {
57
+ const url = new URL(req.url, 'http://localhost');
58
+ const storyId = url.searchParams.get('storyId');
59
+ const theme = (url.searchParams.get('theme') || 'light');
60
+ const width = parseInt(url.searchParams.get('width') || '800', 10);
61
+ const height = parseInt(url.searchParams.get('height') || '600', 10);
62
+ if (!storyId) {
63
+ res.statusCode = 400;
64
+ res.setHeader('Content-Type', 'application/json');
65
+ res.end(JSON.stringify({ error: 'storyId is required' }));
66
+ return;
67
+ }
68
+ // Validate theme
69
+ if (theme !== 'light' && theme !== 'dark') {
70
+ res.statusCode = 400;
71
+ res.setHeader('Content-Type', 'application/json');
72
+ res.end(JSON.stringify({ error: 'theme must be "light" or "dark"' }));
73
+ return;
74
+ }
75
+ // Capture screenshot asynchronously
76
+ captureScreenshotBuffer(storyId, theme, width, height)
77
+ .then(({ buffer }) => {
78
+ res.setHeader('Content-Type', 'image/png');
79
+ res.setHeader('Access-Control-Allow-Origin', '*');
80
+ res.setHeader('Cache-Control', 'no-cache');
81
+ res.end(buffer);
82
+ })
83
+ .catch((error) => {
84
+ console.error('Screenshot capture error:', error);
85
+ res.statusCode = 500;
86
+ res.setHeader('Content-Type', 'application/json');
87
+ res.end(JSON.stringify({
88
+ error: 'Failed to capture screenshot',
89
+ details: String(error),
90
+ }));
91
+ });
92
+ return;
93
+ }
94
+ // Serve screenshots from cache
95
+ if (req.url?.startsWith('/screenshots/')) {
96
+ const screenshotPath = path.join(process.cwd(), '.storybook-cache', req.url.replace('/screenshots/', 'screenshots/'));
97
+ if (fs.existsSync(screenshotPath)) {
98
+ res.setHeader('Content-Type', 'image/png');
99
+ res.setHeader('Access-Control-Allow-Origin', '*');
100
+ res.setHeader('Cache-Control', 'public, max-age=3600');
101
+ fs.createReadStream(screenshotPath).pipe(res);
102
+ }
103
+ else {
104
+ res.statusCode = 404;
105
+ res.end('Screenshot not found');
106
+ }
107
+ return;
108
+ }
109
+ next();
110
+ };
111
+ export function storybookOnlookPlugin(options = {}) {
112
+ // Auto-disable in CI/Chromatic static builds
113
+ if (process.env.CHROMATIC || process.env.CI) {
114
+ return [];
115
+ }
116
+ const port = options.port ?? 6006;
117
+ const allowedOrigins = [
118
+ ...DEFAULT_ALLOWED_ORIGINS,
119
+ ...(options.allowedOrigins ?? []),
120
+ ];
121
+ const mainPlugin = {
122
+ name: 'storybook-onlook-plugin',
123
+ config() {
124
+ return {
125
+ server: {
126
+ // E2B sandbox HMR configuration
127
+ hmr: {
128
+ // E2B sandboxes use HTTPS, so we need secure WebSocket
129
+ protocol: 'wss',
130
+ // E2B routes through standard HTTPS port 443
131
+ clientPort: 443,
132
+ // The actual Storybook server port inside the sandbox
133
+ port,
134
+ },
135
+ cors: {
136
+ origin: allowedOrigins,
137
+ },
138
+ },
139
+ };
140
+ },
141
+ configureServer(server) {
142
+ server.middlewares.use(serveMetadataAndScreenshots);
143
+ },
144
+ configurePreviewServer(server) {
145
+ server.middlewares.use(serveMetadataAndScreenshots);
146
+ },
147
+ handleHotUpdate: handleStoryFileChange,
148
+ };
149
+ // componentLocPlugin has enforce: 'pre' so it runs before other transforms
150
+ return [componentLocPlugin(), mainPlugin];
151
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Ensure cache directories exist
3
+ */
4
+ export declare function ensureCacheDirectories(): void;
5
+ /**
6
+ * Compute file hash
7
+ */
8
+ export declare function computeFileHash(filePath: string): string;
9
+ //# sourceMappingURL=fileSystem.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fileSystem.d.ts","sourceRoot":"","sources":["../../../../src/onlook-plugin/utils/fileSystem/fileSystem.ts"],"names":[],"mappings":"AAIA;;GAEG;AACH,wBAAgB,sBAAsB,SAOrC;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAMxD"}
@@ -0,0 +1,24 @@
1
+ import crypto from 'node:crypto';
2
+ import fs from 'node:fs';
3
+ import { CACHE_DIR, SCREENSHOTS_DIR } from '../../screenshot-service/constants';
4
+ /**
5
+ * Ensure cache directories exist
6
+ */
7
+ export function ensureCacheDirectories() {
8
+ if (!fs.existsSync(CACHE_DIR)) {
9
+ fs.mkdirSync(CACHE_DIR, { recursive: true });
10
+ }
11
+ if (!fs.existsSync(SCREENSHOTS_DIR)) {
12
+ fs.mkdirSync(SCREENSHOTS_DIR, { recursive: true });
13
+ }
14
+ }
15
+ /**
16
+ * Compute file hash
17
+ */
18
+ export function computeFileHash(filePath) {
19
+ if (!fs.existsSync(filePath)) {
20
+ return '';
21
+ }
22
+ const content = fs.readFileSync(filePath, 'utf-8');
23
+ return crypto.createHash('sha256').update(content).digest('hex');
24
+ }
@@ -0,0 +1,2 @@
1
+ export { computeFileHash, ensureCacheDirectories } from './fileSystem';
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/onlook-plugin/utils/fileSystem/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,sBAAsB,EAAE,MAAM,cAAc,CAAC"}
@@ -0,0 +1 @@
1
+ export { computeFileHash, ensureCacheDirectories } from './fileSystem';
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Find the git repository root by walking up the directory tree
3
+ */
4
+ export declare function findGitRoot(startPath: string): string | null;
5
+ //# sourceMappingURL=findGitRoot.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"findGitRoot.d.ts","sourceRoot":"","sources":["../../../../src/onlook-plugin/utils/findGitRoot/findGitRoot.ts"],"names":[],"mappings":"AAGA;;GAEG;AACH,wBAAgB,WAAW,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAW5D"}
@@ -0,0 +1,15 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { dirname, join } from 'node:path';
3
+ /**
4
+ * Find the git repository root by walking up the directory tree
5
+ */
6
+ export function findGitRoot(startPath) {
7
+ let currentPath = startPath;
8
+ while (currentPath !== dirname(currentPath)) {
9
+ if (existsSync(join(currentPath, '.git'))) {
10
+ return currentPath;
11
+ }
12
+ currentPath = dirname(currentPath);
13
+ }
14
+ return null;
15
+ }
@@ -0,0 +1,2 @@
1
+ export { findGitRoot } from './findGitRoot';
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/onlook-plugin/utils/findGitRoot/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC"}
@@ -0,0 +1 @@
1
+ export { findGitRoot } from './findGitRoot';
@@ -0,0 +1,2 @@
1
+ export { getManifestEntry, loadManifest, saveManifest, updateManifest } from './manifest';
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/onlook-plugin/utils/manifest/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,YAAY,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC"}
@@ -0,0 +1 @@
1
+ export { getManifestEntry, loadManifest, saveManifest, updateManifest } from './manifest';
@@ -0,0 +1,21 @@
1
+ import type { Manifest, ScreenshotMetadata } from '../../screenshot-service/types';
2
+ /**
3
+ * Load manifest from disk
4
+ */
5
+ export declare function loadManifest(): Manifest;
6
+ /**
7
+ * Save manifest to disk
8
+ */
9
+ export declare function saveManifest(manifest: Manifest): void;
10
+ /**
11
+ * Get manifest entry for a story
12
+ */
13
+ export declare function getManifestEntry(storyId: string): ScreenshotMetadata | null;
14
+ /**
15
+ * Update manifest for a story
16
+ */
17
+ export declare function updateManifest(storyId: string, sourcePath: string, fileHash: string, boundingBox?: {
18
+ width: number;
19
+ height: number;
20
+ } | null): void;
21
+ //# sourceMappingURL=manifest.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"manifest.d.ts","sourceRoot":"","sources":["../../../../src/onlook-plugin/utils/manifest/manifest.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,QAAQ,EAAE,kBAAkB,EAAE,MAAM,gCAAgC,CAAC;AAGnF;;GAEG;AACH,wBAAgB,YAAY,IAAI,QAAQ,CAMvC;AAED;;GAEG;AACH,wBAAgB,YAAY,CAAC,QAAQ,EAAE,QAAQ,QAG9C;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,kBAAkB,GAAG,IAAI,CAG3E;AAED;;GAEG;AACH,wBAAgB,cAAc,CAC5B,OAAO,EAAE,MAAM,EACf,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,MAAM,EAChB,WAAW,CAAC,EAAE;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,QAgBvD"}
@@ -0,0 +1,44 @@
1
+ import fs from 'node:fs';
2
+ import { MANIFEST_PATH } from '../../screenshot-service/constants';
3
+ import { ensureCacheDirectories } from '../fileSystem/index';
4
+ /**
5
+ * Load manifest from disk
6
+ */
7
+ export function loadManifest() {
8
+ if (fs.existsSync(MANIFEST_PATH)) {
9
+ const content = fs.readFileSync(MANIFEST_PATH, 'utf-8');
10
+ return JSON.parse(content);
11
+ }
12
+ return { stories: {} };
13
+ }
14
+ /**
15
+ * Save manifest to disk
16
+ */
17
+ export function saveManifest(manifest) {
18
+ ensureCacheDirectories();
19
+ fs.writeFileSync(MANIFEST_PATH, JSON.stringify(manifest, null, 2));
20
+ }
21
+ /**
22
+ * Get manifest entry for a story
23
+ */
24
+ export function getManifestEntry(storyId) {
25
+ const manifest = loadManifest();
26
+ return manifest.stories[storyId] || null;
27
+ }
28
+ /**
29
+ * Update manifest for a story
30
+ */
31
+ export function updateManifest(storyId, sourcePath, fileHash, boundingBox) {
32
+ const manifest = loadManifest();
33
+ manifest.stories[storyId] = {
34
+ fileHash,
35
+ lastGenerated: new Date().toISOString(),
36
+ sourcePath,
37
+ screenshots: {
38
+ light: `screenshots/${storyId}/light.png`,
39
+ dark: `screenshots/${storyId}/dark.png`,
40
+ },
41
+ boundingBox: boundingBox ?? undefined,
42
+ };
43
+ saveManifest(manifest);
44
+ }
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "storybook-onbook-plugin",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "exports": {
6
+ ".": {
7
+ "types": "./dist/index.d.ts",
8
+ "import": "./dist/index.js"
9
+ }
10
+ },
11
+ "files": [
12
+ "dist",
13
+ "README.md"
14
+ ],
15
+ "scripts": {
16
+ "build": "tsc",
17
+ "clean": "git clean -xdf .cache .turbo dist node_modules",
18
+ "typecheck": "tsc --noEmit",
19
+ "prepublishOnly": "bun run build",
20
+ "publish-pkg": "npm publish --access public"
21
+ },
22
+ "dependencies": {
23
+ "@babel/generator": "^7.26.9",
24
+ "@babel/parser": "^7.26.9",
25
+ "@babel/traverse": "^7.26.9",
26
+ "@babel/types": "^7.26.9",
27
+ "playwright": "^1.52.0"
28
+ },
29
+ "devDependencies": {
30
+ "@onbook/tsconfig": "workspace:*",
31
+ "@types/babel__generator": "^7.6.8",
32
+ "@types/babel__traverse": "^7.20.6",
33
+ "@types/node": "^22.15.32",
34
+ "typescript": "5.8.3",
35
+ "vite": "^6.3.5"
36
+ },
37
+ "peerDependencies": {
38
+ "vite": "^5.0.0 || ^6.0.0"
39
+ }
40
+ }