@onlook/storybook-plugin 0.3.5 → 0.3.6
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 +49 -223
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
1
|
+
import fs4, { existsSync } from 'fs';
|
|
2
|
+
import path2, { 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,7 +7,6 @@ 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';
|
|
11
10
|
import { readCsf } from 'storybook/internal/csf-tools';
|
|
12
11
|
|
|
13
12
|
// src/storybook-onlook-plugin.ts
|
|
@@ -33,7 +32,7 @@ function componentLocPlugin(options = {}) {
|
|
|
33
32
|
sourceFilename: filepath
|
|
34
33
|
});
|
|
35
34
|
let mutated = false;
|
|
36
|
-
const relativePath =
|
|
35
|
+
const relativePath = path2.relative(root, filepath);
|
|
37
36
|
traverse(ast, {
|
|
38
37
|
JSXElement(nodePath) {
|
|
39
38
|
const opening = nodePath.node.openingElement;
|
|
@@ -70,9 +69,9 @@ function componentLocPlugin(options = {}) {
|
|
|
70
69
|
}
|
|
71
70
|
};
|
|
72
71
|
}
|
|
73
|
-
var CACHE_DIR =
|
|
74
|
-
var SCREENSHOTS_DIR =
|
|
75
|
-
var MANIFEST_PATH =
|
|
72
|
+
var CACHE_DIR = path2.join(process.cwd(), ".storybook-cache");
|
|
73
|
+
var SCREENSHOTS_DIR = path2.join(CACHE_DIR, "screenshots");
|
|
74
|
+
var MANIFEST_PATH = path2.join(CACHE_DIR, "manifest.json");
|
|
76
75
|
var VIEWPORT_WIDTH = 1920;
|
|
77
76
|
var VIEWPORT_HEIGHT = 1080;
|
|
78
77
|
var MIN_COMPONENT_WIDTH = 420;
|
|
@@ -80,30 +79,30 @@ var MIN_COMPONENT_HEIGHT = 280;
|
|
|
80
79
|
|
|
81
80
|
// src/utils/fileSystem/fileSystem.ts
|
|
82
81
|
function ensureCacheDirectories() {
|
|
83
|
-
if (!
|
|
84
|
-
|
|
82
|
+
if (!fs4.existsSync(CACHE_DIR)) {
|
|
83
|
+
fs4.mkdirSync(CACHE_DIR, { recursive: true });
|
|
85
84
|
}
|
|
86
|
-
if (!
|
|
87
|
-
|
|
85
|
+
if (!fs4.existsSync(SCREENSHOTS_DIR)) {
|
|
86
|
+
fs4.mkdirSync(SCREENSHOTS_DIR, { recursive: true });
|
|
88
87
|
}
|
|
89
88
|
}
|
|
90
89
|
function computeFileHash(filePath) {
|
|
91
|
-
if (!
|
|
90
|
+
if (!fs4.existsSync(filePath)) {
|
|
92
91
|
return "";
|
|
93
92
|
}
|
|
94
|
-
const content =
|
|
93
|
+
const content = fs4.readFileSync(filePath, "utf-8");
|
|
95
94
|
return crypto.createHash("sha256").update(content).digest("hex");
|
|
96
95
|
}
|
|
97
96
|
function loadManifest() {
|
|
98
|
-
if (
|
|
99
|
-
const content =
|
|
97
|
+
if (fs4.existsSync(MANIFEST_PATH)) {
|
|
98
|
+
const content = fs4.readFileSync(MANIFEST_PATH, "utf-8");
|
|
100
99
|
return JSON.parse(content);
|
|
101
100
|
}
|
|
102
101
|
return { stories: {} };
|
|
103
102
|
}
|
|
104
103
|
function saveManifest(manifest) {
|
|
105
104
|
ensureCacheDirectories();
|
|
106
|
-
|
|
105
|
+
fs4.writeFileSync(MANIFEST_PATH, JSON.stringify(manifest, null, 2));
|
|
107
106
|
}
|
|
108
107
|
function updateManifest(storyId, sourcePath, fileHash, boundingBox) {
|
|
109
108
|
const manifest = loadManifest();
|
|
@@ -129,8 +128,8 @@ async function getBrowser() {
|
|
|
129
128
|
return browser;
|
|
130
129
|
}
|
|
131
130
|
function getScreenshotPath(storyId, theme) {
|
|
132
|
-
const storyDir =
|
|
133
|
-
return
|
|
131
|
+
const storyDir = path2.join(SCREENSHOTS_DIR, storyId);
|
|
132
|
+
return path2.join(storyDir, `${theme}.png`);
|
|
134
133
|
}
|
|
135
134
|
async function captureScreenshotBuffer(storyId, theme, width = VIEWPORT_WIDTH, height = VIEWPORT_HEIGHT, storybookUrl = "http://localhost:6006", timeoutMs = 3e4) {
|
|
136
135
|
const browser2 = await getBrowser();
|
|
@@ -217,9 +216,9 @@ async function captureScreenshotBuffer(storyId, theme, width = VIEWPORT_WIDTH, h
|
|
|
217
216
|
async function generateScreenshot(storyId, theme, storybookUrl = "http://localhost:6006", timeoutMs = 3e4) {
|
|
218
217
|
try {
|
|
219
218
|
ensureCacheDirectories();
|
|
220
|
-
const storyDir =
|
|
221
|
-
if (!
|
|
222
|
-
|
|
219
|
+
const storyDir = path2.join(SCREENSHOTS_DIR, storyId);
|
|
220
|
+
if (!fs4.existsSync(storyDir)) {
|
|
221
|
+
fs4.mkdirSync(storyDir, { recursive: true });
|
|
223
222
|
}
|
|
224
223
|
const screenshotPath = getScreenshotPath(storyId, theme);
|
|
225
224
|
const { buffer, boundingBox } = await captureScreenshotBuffer(
|
|
@@ -230,7 +229,7 @@ async function generateScreenshot(storyId, theme, storybookUrl = "http://localho
|
|
|
230
229
|
storybookUrl,
|
|
231
230
|
timeoutMs
|
|
232
231
|
);
|
|
233
|
-
|
|
232
|
+
fs4.writeFileSync(screenshotPath, buffer);
|
|
234
233
|
return { path: screenshotPath, boundingBox };
|
|
235
234
|
} catch (error) {
|
|
236
235
|
console.error(`Error generating screenshot for ${storyId} (${theme}):`, error);
|
|
@@ -257,7 +256,7 @@ async function fetchStorybookIndex() {
|
|
|
257
256
|
}
|
|
258
257
|
function getStoriesForFile(filePath) {
|
|
259
258
|
if (!cachedIndex) return [];
|
|
260
|
-
const fileName =
|
|
259
|
+
const fileName = path2.basename(filePath);
|
|
261
260
|
return Object.values(cachedIndex.entries).filter((entry) => entry.type === "story" && entry.importPath.endsWith(fileName)).map((entry) => entry.id);
|
|
262
261
|
}
|
|
263
262
|
async function regenerateScreenshotsForFiles(files) {
|
|
@@ -322,196 +321,6 @@ function handleStoryFileChange({ file, modules }) {
|
|
|
322
321
|
return modules;
|
|
323
322
|
}
|
|
324
323
|
}
|
|
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
|
-
}
|
|
515
324
|
function findGitRoot(startPath) {
|
|
516
325
|
let currentPath = startPath;
|
|
517
326
|
while (currentPath !== dirname(currentPath)) {
|
|
@@ -537,7 +346,7 @@ var DEFAULT_ALLOWED_ORIGINS = [
|
|
|
537
346
|
var serveMetadataAndScreenshots = (req, res, next) => {
|
|
538
347
|
if (req.url === "/onbook-index.json") {
|
|
539
348
|
console.log("[STORYBOOK_PLUGIN] Serving /onbook-index.json endpoint");
|
|
540
|
-
const manifestPath =
|
|
349
|
+
const manifestPath = path2.join(process.cwd(), ".storybook-cache", "manifest.json");
|
|
541
350
|
const cacheBuster = Date.now();
|
|
542
351
|
console.log("[STORYBOOK_PLUGIN] Fetching http://localhost:6006/index.json");
|
|
543
352
|
fetch(`http://localhost:6006/index.json?_t=${cacheBuster}`, {
|
|
@@ -554,7 +363,7 @@ var serveMetadataAndScreenshots = (req, res, next) => {
|
|
|
554
363
|
});
|
|
555
364
|
return response.json();
|
|
556
365
|
}).then((indexData) => {
|
|
557
|
-
const manifest =
|
|
366
|
+
const manifest = fs4.existsSync(manifestPath) ? JSON.parse(fs4.readFileSync(manifestPath, "utf-8")) : { stories: {} };
|
|
558
367
|
const defaultBoundingBox = { width: 1920, height: 1080 };
|
|
559
368
|
for (const [storyId, entry] of Object.entries(indexData.entries || {})) {
|
|
560
369
|
const manifestEntry = manifest.stories?.[storyId];
|
|
@@ -622,7 +431,7 @@ var serveMetadataAndScreenshots = (req, res, next) => {
|
|
|
622
431
|
return;
|
|
623
432
|
}
|
|
624
433
|
if (req.url?.startsWith("/screenshots/")) {
|
|
625
|
-
const screenshotPath =
|
|
434
|
+
const screenshotPath = path2.join(
|
|
626
435
|
process.cwd(),
|
|
627
436
|
".storybook-cache",
|
|
628
437
|
req.url.replace("/screenshots/", "screenshots/")
|
|
@@ -631,11 +440,11 @@ var serveMetadataAndScreenshots = (req, res, next) => {
|
|
|
631
440
|
const storyId = urlParts[0];
|
|
632
441
|
const themeFile = urlParts[1];
|
|
633
442
|
const theme = themeFile?.replace(".png", "");
|
|
634
|
-
if (
|
|
443
|
+
if (fs4.existsSync(screenshotPath)) {
|
|
635
444
|
res.setHeader("Content-Type", "image/png");
|
|
636
445
|
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
637
446
|
res.setHeader("Cache-Control", "public, max-age=3600");
|
|
638
|
-
|
|
447
|
+
fs4.createReadStream(screenshotPath).pipe(res);
|
|
639
448
|
return;
|
|
640
449
|
}
|
|
641
450
|
if (storyId && theme && (theme === "light" || theme === "dark")) {
|
|
@@ -643,16 +452,16 @@ var serveMetadataAndScreenshots = (req, res, next) => {
|
|
|
643
452
|
`[STORYBOOK_PLUGIN] Generating screenshot on-demand: ${storyId}/${theme}`
|
|
644
453
|
);
|
|
645
454
|
captureScreenshotBuffer(storyId, theme).then(({ buffer }) => {
|
|
646
|
-
const storyDir =
|
|
455
|
+
const storyDir = path2.join(
|
|
647
456
|
process.cwd(),
|
|
648
457
|
".storybook-cache",
|
|
649
458
|
"screenshots",
|
|
650
459
|
storyId
|
|
651
460
|
);
|
|
652
|
-
if (!
|
|
653
|
-
|
|
461
|
+
if (!fs4.existsSync(storyDir)) {
|
|
462
|
+
fs4.mkdirSync(storyDir, { recursive: true });
|
|
654
463
|
}
|
|
655
|
-
|
|
464
|
+
fs4.writeFileSync(screenshotPath, buffer);
|
|
656
465
|
res.setHeader("Content-Type", "image/png");
|
|
657
466
|
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
658
467
|
res.setHeader("Cache-Control", "public, max-age=3600");
|
|
@@ -709,6 +518,23 @@ function storybookOnlookPlugin(options = {}) {
|
|
|
709
518
|
cors: {
|
|
710
519
|
origin: allowedOrigins
|
|
711
520
|
}
|
|
521
|
+
},
|
|
522
|
+
optimizeDeps: {
|
|
523
|
+
// Collapse lucide-react's icon graph into a single prebundled chunk.
|
|
524
|
+
// Without these two directives, repos that import both `lucide-react`
|
|
525
|
+
// AND `lucide-react/dynamic` become two co-equal esbuild scan entries
|
|
526
|
+
// sharing ~5,800 icon modules — esbuild's code-splitting then shatters
|
|
527
|
+
// the shared graph per-icon, producing ~1,770 HTTP requests per iframe
|
|
528
|
+
// and tripping Chrome's URLLoader scheduler on canvases with multiple
|
|
529
|
+
// frames (net::ERR_INSUFFICIENT_RESOURCES / "Failed to fetch
|
|
530
|
+
// dynamically imported module"). Parent: ONL-832. Fix: ONL-875.
|
|
531
|
+
//
|
|
532
|
+
// `include: ['lucide-react']` pins it as a single scan entry that
|
|
533
|
+
// esbuild bundles into one chunk. `exclude: ['lucide-react/dynamic']`
|
|
534
|
+
// keeps the DynamicIcon pathway out of prebundling so it can't become
|
|
535
|
+
// a second shared-graph entry.
|
|
536
|
+
include: ["lucide-react"],
|
|
537
|
+
exclude: ["lucide-react/dynamic"]
|
|
712
538
|
}
|
|
713
539
|
};
|
|
714
540
|
},
|
|
@@ -728,7 +554,7 @@ function storybookOnlookPlugin(options = {}) {
|
|
|
728
554
|
},
|
|
729
555
|
handleHotUpdate: handleStoryFileChange
|
|
730
556
|
};
|
|
731
|
-
return [
|
|
557
|
+
return [componentLocPlugin(), mainPlugin];
|
|
732
558
|
}
|
|
733
559
|
var tolerantCsfIndexer = {
|
|
734
560
|
test: /(stories|story)\.(m?js|ts)x?$/,
|