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.
- package/README.md +64 -0
- package/dist/component-loc/component-loc.d.ts +7 -0
- package/dist/component-loc/component-loc.d.ts.map +1 -0
- package/dist/component-loc/component-loc.js +58 -0
- package/dist/component-loc/index.d.ts +2 -0
- package/dist/component-loc/index.d.ts.map +1 -0
- package/dist/component-loc/index.js +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1 -0
- package/dist/onlook-plugin/handlers/handleStoryFileChange/handleStoryFileChange.d.ts +3 -0
- package/dist/onlook-plugin/handlers/handleStoryFileChange/handleStoryFileChange.d.ts.map +1 -0
- package/dist/onlook-plugin/handlers/handleStoryFileChange/handleStoryFileChange.js +100 -0
- package/dist/onlook-plugin/handlers/handleStoryFileChange/index.d.ts +2 -0
- package/dist/onlook-plugin/handlers/handleStoryFileChange/index.d.ts.map +1 -0
- package/dist/onlook-plugin/handlers/handleStoryFileChange/index.js +1 -0
- package/dist/onlook-plugin/index.d.ts +2 -0
- package/dist/onlook-plugin/index.d.ts.map +1 -0
- package/dist/onlook-plugin/index.js +1 -0
- package/dist/onlook-plugin/screenshot-service/constants.d.ts +8 -0
- package/dist/onlook-plugin/screenshot-service/constants.d.ts.map +1 -0
- package/dist/onlook-plugin/screenshot-service/constants.js +11 -0
- package/dist/onlook-plugin/screenshot-service/index.d.ts +5 -0
- package/dist/onlook-plugin/screenshot-service/index.d.ts.map +1 -0
- package/dist/onlook-plugin/screenshot-service/index.js +5 -0
- package/dist/onlook-plugin/screenshot-service/screenshot-service.d.ts +8 -0
- package/dist/onlook-plugin/screenshot-service/screenshot-service.d.ts.map +1 -0
- package/dist/onlook-plugin/screenshot-service/screenshot-service.js +36 -0
- package/dist/onlook-plugin/screenshot-service/types.d.ts +18 -0
- package/dist/onlook-plugin/screenshot-service/types.d.ts.map +1 -0
- package/dist/onlook-plugin/screenshot-service/types.js +0 -0
- package/dist/onlook-plugin/screenshot-service/utils/browser/browser.d.ts +10 -0
- package/dist/onlook-plugin/screenshot-service/utils/browser/browser.d.ts.map +1 -0
- package/dist/onlook-plugin/screenshot-service/utils/browser/browser.js +29 -0
- package/dist/onlook-plugin/screenshot-service/utils/browser/index.d.ts +2 -0
- package/dist/onlook-plugin/screenshot-service/utils/browser/index.d.ts.map +1 -0
- package/dist/onlook-plugin/screenshot-service/utils/browser/index.js +1 -0
- package/dist/onlook-plugin/screenshot-service/utils/screenshot/index.d.ts +2 -0
- package/dist/onlook-plugin/screenshot-service/utils/screenshot/index.d.ts.map +1 -0
- package/dist/onlook-plugin/screenshot-service/utils/screenshot/index.js +1 -0
- package/dist/onlook-plugin/screenshot-service/utils/screenshot/screenshot.d.ts +26 -0
- package/dist/onlook-plugin/screenshot-service/utils/screenshot/screenshot.d.ts.map +1 -0
- package/dist/onlook-plugin/screenshot-service/utils/screenshot/screenshot.js +128 -0
- package/dist/onlook-plugin/storybook-onlook-plugin.d.ts +9 -0
- package/dist/onlook-plugin/storybook-onlook-plugin.d.ts.map +1 -0
- package/dist/onlook-plugin/storybook-onlook-plugin.js +151 -0
- package/dist/onlook-plugin/utils/fileSystem/fileSystem.d.ts +9 -0
- package/dist/onlook-plugin/utils/fileSystem/fileSystem.d.ts.map +1 -0
- package/dist/onlook-plugin/utils/fileSystem/fileSystem.js +24 -0
- package/dist/onlook-plugin/utils/fileSystem/index.d.ts +2 -0
- package/dist/onlook-plugin/utils/fileSystem/index.d.ts.map +1 -0
- package/dist/onlook-plugin/utils/fileSystem/index.js +1 -0
- package/dist/onlook-plugin/utils/findGitRoot/findGitRoot.d.ts +5 -0
- package/dist/onlook-plugin/utils/findGitRoot/findGitRoot.d.ts.map +1 -0
- package/dist/onlook-plugin/utils/findGitRoot/findGitRoot.js +15 -0
- package/dist/onlook-plugin/utils/findGitRoot/index.d.ts +2 -0
- package/dist/onlook-plugin/utils/findGitRoot/index.d.ts.map +1 -0
- package/dist/onlook-plugin/utils/findGitRoot/index.js +1 -0
- package/dist/onlook-plugin/utils/manifest/index.d.ts +2 -0
- package/dist/onlook-plugin/utils/manifest/index.d.ts.map +1 -0
- package/dist/onlook-plugin/utils/manifest/index.js +1 -0
- package/dist/onlook-plugin/utils/manifest/manifest.d.ts +21 -0
- package/dist/onlook-plugin/utils/manifest/manifest.d.ts.map +1 -0
- package/dist/onlook-plugin/utils/manifest/manifest.js +44 -0
- 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 @@
|
|
|
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';
|
package/dist/index.d.ts
ADDED
|
@@ -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 @@
|
|
|
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 @@
|
|
|
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 @@
|
|
|
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,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 @@
|
|
|
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 @@
|
|
|
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 @@
|
|
|
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 @@
|
|
|
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 @@
|
|
|
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 @@
|
|
|
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 @@
|
|
|
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
|
+
}
|