@onlook/storybook-plugin 0.3.4 → 0.3.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +223 -32
- package/package.json +2 -1
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
1
|
+
import fs5, { existsSync } from 'fs';
|
|
2
|
+
import path5, { dirname, join, relative } from 'path';
|
|
3
3
|
import { fileURLToPath } from 'url';
|
|
4
4
|
import generateModule from '@babel/generator';
|
|
5
5
|
import { parse } from '@babel/parser';
|
|
@@ -7,6 +7,7 @@ import traverseModule from '@babel/traverse';
|
|
|
7
7
|
import * as t from '@babel/types';
|
|
8
8
|
import crypto from 'crypto';
|
|
9
9
|
import { chromium } from 'playwright';
|
|
10
|
+
import { createRequire } from 'module';
|
|
10
11
|
import { readCsf } from 'storybook/internal/csf-tools';
|
|
11
12
|
|
|
12
13
|
// src/storybook-onlook-plugin.ts
|
|
@@ -32,7 +33,7 @@ function componentLocPlugin(options = {}) {
|
|
|
32
33
|
sourceFilename: filepath
|
|
33
34
|
});
|
|
34
35
|
let mutated = false;
|
|
35
|
-
const relativePath =
|
|
36
|
+
const relativePath = path5.relative(root, filepath);
|
|
36
37
|
traverse(ast, {
|
|
37
38
|
JSXElement(nodePath) {
|
|
38
39
|
const opening = nodePath.node.openingElement;
|
|
@@ -69,9 +70,9 @@ function componentLocPlugin(options = {}) {
|
|
|
69
70
|
}
|
|
70
71
|
};
|
|
71
72
|
}
|
|
72
|
-
var CACHE_DIR =
|
|
73
|
-
var SCREENSHOTS_DIR =
|
|
74
|
-
var MANIFEST_PATH =
|
|
73
|
+
var CACHE_DIR = path5.join(process.cwd(), ".storybook-cache");
|
|
74
|
+
var SCREENSHOTS_DIR = path5.join(CACHE_DIR, "screenshots");
|
|
75
|
+
var MANIFEST_PATH = path5.join(CACHE_DIR, "manifest.json");
|
|
75
76
|
var VIEWPORT_WIDTH = 1920;
|
|
76
77
|
var VIEWPORT_HEIGHT = 1080;
|
|
77
78
|
var MIN_COMPONENT_WIDTH = 420;
|
|
@@ -79,30 +80,30 @@ var MIN_COMPONENT_HEIGHT = 280;
|
|
|
79
80
|
|
|
80
81
|
// src/utils/fileSystem/fileSystem.ts
|
|
81
82
|
function ensureCacheDirectories() {
|
|
82
|
-
if (!
|
|
83
|
-
|
|
83
|
+
if (!fs5.existsSync(CACHE_DIR)) {
|
|
84
|
+
fs5.mkdirSync(CACHE_DIR, { recursive: true });
|
|
84
85
|
}
|
|
85
|
-
if (!
|
|
86
|
-
|
|
86
|
+
if (!fs5.existsSync(SCREENSHOTS_DIR)) {
|
|
87
|
+
fs5.mkdirSync(SCREENSHOTS_DIR, { recursive: true });
|
|
87
88
|
}
|
|
88
89
|
}
|
|
89
90
|
function computeFileHash(filePath) {
|
|
90
|
-
if (!
|
|
91
|
+
if (!fs5.existsSync(filePath)) {
|
|
91
92
|
return "";
|
|
92
93
|
}
|
|
93
|
-
const content =
|
|
94
|
+
const content = fs5.readFileSync(filePath, "utf-8");
|
|
94
95
|
return crypto.createHash("sha256").update(content).digest("hex");
|
|
95
96
|
}
|
|
96
97
|
function loadManifest() {
|
|
97
|
-
if (
|
|
98
|
-
const content =
|
|
98
|
+
if (fs5.existsSync(MANIFEST_PATH)) {
|
|
99
|
+
const content = fs5.readFileSync(MANIFEST_PATH, "utf-8");
|
|
99
100
|
return JSON.parse(content);
|
|
100
101
|
}
|
|
101
102
|
return { stories: {} };
|
|
102
103
|
}
|
|
103
104
|
function saveManifest(manifest) {
|
|
104
105
|
ensureCacheDirectories();
|
|
105
|
-
|
|
106
|
+
fs5.writeFileSync(MANIFEST_PATH, JSON.stringify(manifest, null, 2));
|
|
106
107
|
}
|
|
107
108
|
function updateManifest(storyId, sourcePath, fileHash, boundingBox) {
|
|
108
109
|
const manifest = loadManifest();
|
|
@@ -128,8 +129,8 @@ async function getBrowser() {
|
|
|
128
129
|
return browser;
|
|
129
130
|
}
|
|
130
131
|
function getScreenshotPath(storyId, theme) {
|
|
131
|
-
const storyDir =
|
|
132
|
-
return
|
|
132
|
+
const storyDir = path5.join(SCREENSHOTS_DIR, storyId);
|
|
133
|
+
return path5.join(storyDir, `${theme}.png`);
|
|
133
134
|
}
|
|
134
135
|
async function captureScreenshotBuffer(storyId, theme, width = VIEWPORT_WIDTH, height = VIEWPORT_HEIGHT, storybookUrl = "http://localhost:6006", timeoutMs = 3e4) {
|
|
135
136
|
const browser2 = await getBrowser();
|
|
@@ -216,9 +217,9 @@ async function captureScreenshotBuffer(storyId, theme, width = VIEWPORT_WIDTH, h
|
|
|
216
217
|
async function generateScreenshot(storyId, theme, storybookUrl = "http://localhost:6006", timeoutMs = 3e4) {
|
|
217
218
|
try {
|
|
218
219
|
ensureCacheDirectories();
|
|
219
|
-
const storyDir =
|
|
220
|
-
if (!
|
|
221
|
-
|
|
220
|
+
const storyDir = path5.join(SCREENSHOTS_DIR, storyId);
|
|
221
|
+
if (!fs5.existsSync(storyDir)) {
|
|
222
|
+
fs5.mkdirSync(storyDir, { recursive: true });
|
|
222
223
|
}
|
|
223
224
|
const screenshotPath = getScreenshotPath(storyId, theme);
|
|
224
225
|
const { buffer, boundingBox } = await captureScreenshotBuffer(
|
|
@@ -229,7 +230,7 @@ async function generateScreenshot(storyId, theme, storybookUrl = "http://localho
|
|
|
229
230
|
storybookUrl,
|
|
230
231
|
timeoutMs
|
|
231
232
|
);
|
|
232
|
-
|
|
233
|
+
fs5.writeFileSync(screenshotPath, buffer);
|
|
233
234
|
return { path: screenshotPath, boundingBox };
|
|
234
235
|
} catch (error) {
|
|
235
236
|
console.error(`Error generating screenshot for ${storyId} (${theme}):`, error);
|
|
@@ -256,7 +257,7 @@ async function fetchStorybookIndex() {
|
|
|
256
257
|
}
|
|
257
258
|
function getStoriesForFile(filePath) {
|
|
258
259
|
if (!cachedIndex) return [];
|
|
259
|
-
const fileName =
|
|
260
|
+
const fileName = path5.basename(filePath);
|
|
260
261
|
return Object.values(cachedIndex.entries).filter((entry) => entry.type === "story" && entry.importPath.endsWith(fileName)).map((entry) => entry.id);
|
|
261
262
|
}
|
|
262
263
|
async function regenerateScreenshotsForFiles(files) {
|
|
@@ -321,6 +322,196 @@ function handleStoryFileChange({ file, modules }) {
|
|
|
321
322
|
return modules;
|
|
322
323
|
}
|
|
323
324
|
}
|
|
325
|
+
function buildLucideExportMap(barrelSource, barrelRelativeDir) {
|
|
326
|
+
const ast = parse(barrelSource, { sourceType: "module" });
|
|
327
|
+
const map = /* @__PURE__ */ new Map();
|
|
328
|
+
for (const node of ast.program.body) {
|
|
329
|
+
if (node.type !== "ExportNamedDeclaration") continue;
|
|
330
|
+
if (!node.source) continue;
|
|
331
|
+
const rawSource = node.source.value;
|
|
332
|
+
const subpath = path5.posix.join(barrelRelativeDir, rawSource.replace(/^\.\//, ""));
|
|
333
|
+
for (const spec of node.specifiers) {
|
|
334
|
+
if (spec.type !== "ExportSpecifier") continue;
|
|
335
|
+
const exportedName = spec.exported.type === "Identifier" ? spec.exported.name : null;
|
|
336
|
+
if (!exportedName) continue;
|
|
337
|
+
const localName = spec.local.name;
|
|
338
|
+
if (localName === "default") {
|
|
339
|
+
map.set(exportedName, { kind: "default", subpath });
|
|
340
|
+
} else {
|
|
341
|
+
map.set(exportedName, { kind: "named", subpath, name: localName });
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
return map;
|
|
346
|
+
}
|
|
347
|
+
function transformLucideImports(code, filepath, exportMap) {
|
|
348
|
+
if (!code.includes("lucide-react")) return null;
|
|
349
|
+
if (!/\bimport\b[^;]*['"]lucide-react['"]/.test(code)) return null;
|
|
350
|
+
const traverse = traverseModule.default ?? traverseModule;
|
|
351
|
+
const generate = generateModule.default ?? generateModule;
|
|
352
|
+
const ast = parse(code, {
|
|
353
|
+
sourceType: "module",
|
|
354
|
+
plugins: ["jsx", "typescript"],
|
|
355
|
+
sourceFilename: filepath
|
|
356
|
+
});
|
|
357
|
+
let mutated = false;
|
|
358
|
+
traverse(ast, {
|
|
359
|
+
ImportDeclaration(nodePath) {
|
|
360
|
+
const node = nodePath.node;
|
|
361
|
+
if (node.source.value !== "lucide-react") return;
|
|
362
|
+
if (node.importKind === "type") return;
|
|
363
|
+
const rewritten = [];
|
|
364
|
+
const preserved = [];
|
|
365
|
+
for (const spec of node.specifiers) {
|
|
366
|
+
if (spec.type !== "ImportSpecifier") {
|
|
367
|
+
preserved.push(spec);
|
|
368
|
+
continue;
|
|
369
|
+
}
|
|
370
|
+
if (spec.importKind === "type") {
|
|
371
|
+
preserved.push(spec);
|
|
372
|
+
continue;
|
|
373
|
+
}
|
|
374
|
+
const importedName = spec.imported.type === "Identifier" ? spec.imported.name : null;
|
|
375
|
+
if (!importedName) {
|
|
376
|
+
preserved.push(spec);
|
|
377
|
+
continue;
|
|
378
|
+
}
|
|
379
|
+
const resolution = exportMap.get(importedName);
|
|
380
|
+
if (!resolution) {
|
|
381
|
+
preserved.push(spec);
|
|
382
|
+
continue;
|
|
383
|
+
}
|
|
384
|
+
const subpathSource = t.stringLiteral(`lucide-react/${resolution.subpath}`);
|
|
385
|
+
if (resolution.kind === "default") {
|
|
386
|
+
rewritten.push(
|
|
387
|
+
t.importDeclaration(
|
|
388
|
+
[t.importDefaultSpecifier(t.identifier(spec.local.name))],
|
|
389
|
+
subpathSource
|
|
390
|
+
)
|
|
391
|
+
);
|
|
392
|
+
} else {
|
|
393
|
+
rewritten.push(
|
|
394
|
+
t.importDeclaration(
|
|
395
|
+
[
|
|
396
|
+
t.importSpecifier(
|
|
397
|
+
t.identifier(spec.local.name),
|
|
398
|
+
t.identifier(resolution.name)
|
|
399
|
+
)
|
|
400
|
+
],
|
|
401
|
+
subpathSource
|
|
402
|
+
)
|
|
403
|
+
);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
if (rewritten.length === 0) return;
|
|
407
|
+
mutated = true;
|
|
408
|
+
if (preserved.length > 0) {
|
|
409
|
+
const residual = t.importDeclaration(preserved, t.stringLiteral("lucide-react"));
|
|
410
|
+
nodePath.replaceWithMultiple([...rewritten, residual]);
|
|
411
|
+
} else {
|
|
412
|
+
nodePath.replaceWithMultiple(rewritten);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
});
|
|
416
|
+
if (!mutated) return null;
|
|
417
|
+
const output = generate(
|
|
418
|
+
ast,
|
|
419
|
+
{ retainLines: true, sourceMaps: true, sourceFileName: filepath },
|
|
420
|
+
code
|
|
421
|
+
);
|
|
422
|
+
return { code: output.code, map: output.map };
|
|
423
|
+
}
|
|
424
|
+
async function resolveLucideBarrel(config) {
|
|
425
|
+
try {
|
|
426
|
+
const resolver = config.createResolver({ asSrc: false });
|
|
427
|
+
const resolved = await resolver("lucide-react", path5.join(config.root, "index.js"));
|
|
428
|
+
if (resolved && fs5.existsSync(resolved)) return resolved;
|
|
429
|
+
} catch {
|
|
430
|
+
}
|
|
431
|
+
try {
|
|
432
|
+
const req = createRequire(path5.join(config.root, "package.json"));
|
|
433
|
+
return req.resolve("lucide-react");
|
|
434
|
+
} catch {
|
|
435
|
+
return null;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
function computeBarrelRelativeDir(barrelPath) {
|
|
439
|
+
let dir = path5.dirname(barrelPath);
|
|
440
|
+
while (dir !== path5.dirname(dir)) {
|
|
441
|
+
const pj = path5.join(dir, "package.json");
|
|
442
|
+
if (fs5.existsSync(pj)) {
|
|
443
|
+
try {
|
|
444
|
+
const pkg = JSON.parse(fs5.readFileSync(pj, "utf-8"));
|
|
445
|
+
if (pkg.name === "lucide-react") {
|
|
446
|
+
const rel = path5.relative(dir, barrelPath).split(path5.sep).join("/");
|
|
447
|
+
return `${path5.posix.dirname(rel)}/`;
|
|
448
|
+
}
|
|
449
|
+
} catch {
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
dir = path5.dirname(dir);
|
|
453
|
+
}
|
|
454
|
+
return null;
|
|
455
|
+
}
|
|
456
|
+
var PLUGIN_NAME = "onbook-lucide-barrel";
|
|
457
|
+
function lucideBarrelPlugin() {
|
|
458
|
+
let exportMap = /* @__PURE__ */ new Map();
|
|
459
|
+
let enabled = false;
|
|
460
|
+
let warnedTransformOnce = false;
|
|
461
|
+
return {
|
|
462
|
+
name: PLUGIN_NAME,
|
|
463
|
+
enforce: "pre",
|
|
464
|
+
apply: "serve",
|
|
465
|
+
config() {
|
|
466
|
+
return {
|
|
467
|
+
optimizeDeps: {
|
|
468
|
+
exclude: ["lucide-react"]
|
|
469
|
+
}
|
|
470
|
+
};
|
|
471
|
+
},
|
|
472
|
+
async configResolved(config) {
|
|
473
|
+
const barrelPath = await resolveLucideBarrel(config);
|
|
474
|
+
if (!barrelPath) {
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
const relativeDir = computeBarrelRelativeDir(barrelPath);
|
|
478
|
+
if (!relativeDir) {
|
|
479
|
+
config.logger.warn(
|
|
480
|
+
`[${PLUGIN_NAME}] Could not locate lucide-react package root from ${barrelPath}; plugin is a no-op.`
|
|
481
|
+
);
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
try {
|
|
485
|
+
const source = fs5.readFileSync(barrelPath, "utf-8");
|
|
486
|
+
exportMap = buildLucideExportMap(source, relativeDir);
|
|
487
|
+
enabled = true;
|
|
488
|
+
} catch (e) {
|
|
489
|
+
config.logger.warn(
|
|
490
|
+
`[${PLUGIN_NAME}] Failed to parse lucide-react barrel at ${barrelPath}: ${e instanceof Error ? e.message : String(e)}`
|
|
491
|
+
);
|
|
492
|
+
}
|
|
493
|
+
},
|
|
494
|
+
transform(code, id) {
|
|
495
|
+
if (!enabled) return null;
|
|
496
|
+
const filepath = id.split("?", 1)[0];
|
|
497
|
+
if (!filepath || filepath.includes("node_modules")) return null;
|
|
498
|
+
if (!/\.(tsx?|jsx?)$/.test(filepath)) return null;
|
|
499
|
+
try {
|
|
500
|
+
const result = transformLucideImports(code, filepath, exportMap);
|
|
501
|
+
if (!result) return null;
|
|
502
|
+
return { code: result.code, map: result.map };
|
|
503
|
+
} catch (e) {
|
|
504
|
+
if (!warnedTransformOnce) {
|
|
505
|
+
warnedTransformOnce = true;
|
|
506
|
+
console.warn(
|
|
507
|
+
`[${PLUGIN_NAME}] Transform error for ${id}: ${e instanceof Error ? e.message : String(e)} (further errors suppressed)`
|
|
508
|
+
);
|
|
509
|
+
}
|
|
510
|
+
return null;
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
};
|
|
514
|
+
}
|
|
324
515
|
function findGitRoot(startPath) {
|
|
325
516
|
let currentPath = startPath;
|
|
326
517
|
while (currentPath !== dirname(currentPath)) {
|
|
@@ -346,7 +537,7 @@ var DEFAULT_ALLOWED_ORIGINS = [
|
|
|
346
537
|
var serveMetadataAndScreenshots = (req, res, next) => {
|
|
347
538
|
if (req.url === "/onbook-index.json") {
|
|
348
539
|
console.log("[STORYBOOK_PLUGIN] Serving /onbook-index.json endpoint");
|
|
349
|
-
const manifestPath =
|
|
540
|
+
const manifestPath = path5.join(process.cwd(), ".storybook-cache", "manifest.json");
|
|
350
541
|
const cacheBuster = Date.now();
|
|
351
542
|
console.log("[STORYBOOK_PLUGIN] Fetching http://localhost:6006/index.json");
|
|
352
543
|
fetch(`http://localhost:6006/index.json?_t=${cacheBuster}`, {
|
|
@@ -363,7 +554,7 @@ var serveMetadataAndScreenshots = (req, res, next) => {
|
|
|
363
554
|
});
|
|
364
555
|
return response.json();
|
|
365
556
|
}).then((indexData) => {
|
|
366
|
-
const manifest =
|
|
557
|
+
const manifest = fs5.existsSync(manifestPath) ? JSON.parse(fs5.readFileSync(manifestPath, "utf-8")) : { stories: {} };
|
|
367
558
|
const defaultBoundingBox = { width: 1920, height: 1080 };
|
|
368
559
|
for (const [storyId, entry] of Object.entries(indexData.entries || {})) {
|
|
369
560
|
const manifestEntry = manifest.stories?.[storyId];
|
|
@@ -431,7 +622,7 @@ var serveMetadataAndScreenshots = (req, res, next) => {
|
|
|
431
622
|
return;
|
|
432
623
|
}
|
|
433
624
|
if (req.url?.startsWith("/screenshots/")) {
|
|
434
|
-
const screenshotPath =
|
|
625
|
+
const screenshotPath = path5.join(
|
|
435
626
|
process.cwd(),
|
|
436
627
|
".storybook-cache",
|
|
437
628
|
req.url.replace("/screenshots/", "screenshots/")
|
|
@@ -440,11 +631,11 @@ var serveMetadataAndScreenshots = (req, res, next) => {
|
|
|
440
631
|
const storyId = urlParts[0];
|
|
441
632
|
const themeFile = urlParts[1];
|
|
442
633
|
const theme = themeFile?.replace(".png", "");
|
|
443
|
-
if (
|
|
634
|
+
if (fs5.existsSync(screenshotPath)) {
|
|
444
635
|
res.setHeader("Content-Type", "image/png");
|
|
445
636
|
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
446
637
|
res.setHeader("Cache-Control", "public, max-age=3600");
|
|
447
|
-
|
|
638
|
+
fs5.createReadStream(screenshotPath).pipe(res);
|
|
448
639
|
return;
|
|
449
640
|
}
|
|
450
641
|
if (storyId && theme && (theme === "light" || theme === "dark")) {
|
|
@@ -452,16 +643,16 @@ var serveMetadataAndScreenshots = (req, res, next) => {
|
|
|
452
643
|
`[STORYBOOK_PLUGIN] Generating screenshot on-demand: ${storyId}/${theme}`
|
|
453
644
|
);
|
|
454
645
|
captureScreenshotBuffer(storyId, theme).then(({ buffer }) => {
|
|
455
|
-
const storyDir =
|
|
646
|
+
const storyDir = path5.join(
|
|
456
647
|
process.cwd(),
|
|
457
648
|
".storybook-cache",
|
|
458
649
|
"screenshots",
|
|
459
650
|
storyId
|
|
460
651
|
);
|
|
461
|
-
if (!
|
|
462
|
-
|
|
652
|
+
if (!fs5.existsSync(storyDir)) {
|
|
653
|
+
fs5.mkdirSync(storyDir, { recursive: true });
|
|
463
654
|
}
|
|
464
|
-
|
|
655
|
+
fs5.writeFileSync(screenshotPath, buffer);
|
|
465
656
|
res.setHeader("Content-Type", "image/png");
|
|
466
657
|
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
467
658
|
res.setHeader("Cache-Control", "public, max-age=3600");
|
|
@@ -537,7 +728,7 @@ function storybookOnlookPlugin(options = {}) {
|
|
|
537
728
|
},
|
|
538
729
|
handleHotUpdate: handleStoryFileChange
|
|
539
730
|
};
|
|
540
|
-
return [componentLocPlugin(), mainPlugin];
|
|
731
|
+
return [lucideBarrelPlugin(), componentLocPlugin(), mainPlugin];
|
|
541
732
|
}
|
|
542
733
|
var tolerantCsfIndexer = {
|
|
543
734
|
test: /(stories|story)\.(m?js|ts)x?$/,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@onlook/storybook-plugin",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.5",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"onlook-storybook": "dist/cli/index.js"
|
|
@@ -47,6 +47,7 @@
|
|
|
47
47
|
"@types/babel__traverse": "^7.20.6",
|
|
48
48
|
"@types/node": "^22.15.32",
|
|
49
49
|
"bun-types": "^1.3.5",
|
|
50
|
+
"storybook": "^10.1.11",
|
|
50
51
|
"tsup": "^8.5.1",
|
|
51
52
|
"typescript": "5.8.3",
|
|
52
53
|
"vite": "^6.3.5"
|