@onlook/storybook-plugin 0.3.3 → 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/README.md CHANGED
@@ -76,6 +76,27 @@ Screenshots are saved to `.storybook-cache/screenshots/`.
76
76
 
77
77
  The plugin auto-disables when `CHROMATIC` or `CI` environment variables are set.
78
78
 
79
+ ## Publishing
80
+
81
+ Three release paths:
82
+
83
+ ```bash
84
+ # 1. Ship the current version to npm as `latest` (default dist-tag).
85
+ bun run publish-pkg
86
+
87
+ # 2. Ship under the `next` dist-tag — for testing against preview deploys
88
+ # without affecting consumers pinned to ^x.y.z ranges. Consumers opt in
89
+ # via `@onlook/storybook-plugin@next`.
90
+ bun run publish-pkg:next
91
+
92
+ # 3. Promote a previously-published version to `latest`. Reads the version
93
+ # from package.json, so bump first if you want to promote something else.
94
+ bun run promote-latest
95
+ ```
96
+
97
+ The typical flow for risky changes: `publish-pkg:next` → test against a
98
+ Vercel preview deploy → `promote-latest` once confident.
99
+
79
100
  ## License
80
101
 
81
102
  MIT
package/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { PluginOption } from 'vite';
2
+ import { Indexer } from 'storybook/internal/types';
2
3
 
3
4
  type OnlookPluginOptions = {
4
5
  /** Storybook port (default: 6006) */
@@ -8,4 +9,6 @@ type OnlookPluginOptions = {
8
9
  };
9
10
  declare function storybookOnlookPlugin(options?: OnlookPluginOptions): PluginOption[];
10
11
 
11
- export { type OnlookPluginOptions, storybookOnlookPlugin };
12
+ declare const tolerantCsfIndexer: Indexer;
13
+
14
+ export { type OnlookPluginOptions, storybookOnlookPlugin, tolerantCsfIndexer };
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
- import fs4, { existsSync } from 'fs';
2
- import path2, { dirname, join, relative } from 'path';
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,8 @@ 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
+ import { readCsf } from 'storybook/internal/csf-tools';
10
12
 
11
13
  // src/storybook-onlook-plugin.ts
12
14
  function componentLocPlugin(options = {}) {
@@ -31,7 +33,7 @@ function componentLocPlugin(options = {}) {
31
33
  sourceFilename: filepath
32
34
  });
33
35
  let mutated = false;
34
- const relativePath = path2.relative(root, filepath);
36
+ const relativePath = path5.relative(root, filepath);
35
37
  traverse(ast, {
36
38
  JSXElement(nodePath) {
37
39
  const opening = nodePath.node.openingElement;
@@ -68,9 +70,9 @@ function componentLocPlugin(options = {}) {
68
70
  }
69
71
  };
70
72
  }
71
- var CACHE_DIR = path2.join(process.cwd(), ".storybook-cache");
72
- var SCREENSHOTS_DIR = path2.join(CACHE_DIR, "screenshots");
73
- var MANIFEST_PATH = path2.join(CACHE_DIR, "manifest.json");
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");
74
76
  var VIEWPORT_WIDTH = 1920;
75
77
  var VIEWPORT_HEIGHT = 1080;
76
78
  var MIN_COMPONENT_WIDTH = 420;
@@ -78,30 +80,30 @@ var MIN_COMPONENT_HEIGHT = 280;
78
80
 
79
81
  // src/utils/fileSystem/fileSystem.ts
80
82
  function ensureCacheDirectories() {
81
- if (!fs4.existsSync(CACHE_DIR)) {
82
- fs4.mkdirSync(CACHE_DIR, { recursive: true });
83
+ if (!fs5.existsSync(CACHE_DIR)) {
84
+ fs5.mkdirSync(CACHE_DIR, { recursive: true });
83
85
  }
84
- if (!fs4.existsSync(SCREENSHOTS_DIR)) {
85
- fs4.mkdirSync(SCREENSHOTS_DIR, { recursive: true });
86
+ if (!fs5.existsSync(SCREENSHOTS_DIR)) {
87
+ fs5.mkdirSync(SCREENSHOTS_DIR, { recursive: true });
86
88
  }
87
89
  }
88
90
  function computeFileHash(filePath) {
89
- if (!fs4.existsSync(filePath)) {
91
+ if (!fs5.existsSync(filePath)) {
90
92
  return "";
91
93
  }
92
- const content = fs4.readFileSync(filePath, "utf-8");
94
+ const content = fs5.readFileSync(filePath, "utf-8");
93
95
  return crypto.createHash("sha256").update(content).digest("hex");
94
96
  }
95
97
  function loadManifest() {
96
- if (fs4.existsSync(MANIFEST_PATH)) {
97
- const content = fs4.readFileSync(MANIFEST_PATH, "utf-8");
98
+ if (fs5.existsSync(MANIFEST_PATH)) {
99
+ const content = fs5.readFileSync(MANIFEST_PATH, "utf-8");
98
100
  return JSON.parse(content);
99
101
  }
100
102
  return { stories: {} };
101
103
  }
102
104
  function saveManifest(manifest) {
103
105
  ensureCacheDirectories();
104
- fs4.writeFileSync(MANIFEST_PATH, JSON.stringify(manifest, null, 2));
106
+ fs5.writeFileSync(MANIFEST_PATH, JSON.stringify(manifest, null, 2));
105
107
  }
106
108
  function updateManifest(storyId, sourcePath, fileHash, boundingBox) {
107
109
  const manifest = loadManifest();
@@ -127,8 +129,8 @@ async function getBrowser() {
127
129
  return browser;
128
130
  }
129
131
  function getScreenshotPath(storyId, theme) {
130
- const storyDir = path2.join(SCREENSHOTS_DIR, storyId);
131
- return path2.join(storyDir, `${theme}.png`);
132
+ const storyDir = path5.join(SCREENSHOTS_DIR, storyId);
133
+ return path5.join(storyDir, `${theme}.png`);
132
134
  }
133
135
  async function captureScreenshotBuffer(storyId, theme, width = VIEWPORT_WIDTH, height = VIEWPORT_HEIGHT, storybookUrl = "http://localhost:6006", timeoutMs = 3e4) {
134
136
  const browser2 = await getBrowser();
@@ -215,9 +217,9 @@ async function captureScreenshotBuffer(storyId, theme, width = VIEWPORT_WIDTH, h
215
217
  async function generateScreenshot(storyId, theme, storybookUrl = "http://localhost:6006", timeoutMs = 3e4) {
216
218
  try {
217
219
  ensureCacheDirectories();
218
- const storyDir = path2.join(SCREENSHOTS_DIR, storyId);
219
- if (!fs4.existsSync(storyDir)) {
220
- fs4.mkdirSync(storyDir, { recursive: true });
220
+ const storyDir = path5.join(SCREENSHOTS_DIR, storyId);
221
+ if (!fs5.existsSync(storyDir)) {
222
+ fs5.mkdirSync(storyDir, { recursive: true });
221
223
  }
222
224
  const screenshotPath = getScreenshotPath(storyId, theme);
223
225
  const { buffer, boundingBox } = await captureScreenshotBuffer(
@@ -228,7 +230,7 @@ async function generateScreenshot(storyId, theme, storybookUrl = "http://localho
228
230
  storybookUrl,
229
231
  timeoutMs
230
232
  );
231
- fs4.writeFileSync(screenshotPath, buffer);
233
+ fs5.writeFileSync(screenshotPath, buffer);
232
234
  return { path: screenshotPath, boundingBox };
233
235
  } catch (error) {
234
236
  console.error(`Error generating screenshot for ${storyId} (${theme}):`, error);
@@ -255,7 +257,7 @@ async function fetchStorybookIndex() {
255
257
  }
256
258
  function getStoriesForFile(filePath) {
257
259
  if (!cachedIndex) return [];
258
- const fileName = path2.basename(filePath);
260
+ const fileName = path5.basename(filePath);
259
261
  return Object.values(cachedIndex.entries).filter((entry) => entry.type === "story" && entry.importPath.endsWith(fileName)).map((entry) => entry.id);
260
262
  }
261
263
  async function regenerateScreenshotsForFiles(files) {
@@ -320,6 +322,196 @@ function handleStoryFileChange({ file, modules }) {
320
322
  return modules;
321
323
  }
322
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
+ }
323
515
  function findGitRoot(startPath) {
324
516
  let currentPath = startPath;
325
517
  while (currentPath !== dirname(currentPath)) {
@@ -345,7 +537,7 @@ var DEFAULT_ALLOWED_ORIGINS = [
345
537
  var serveMetadataAndScreenshots = (req, res, next) => {
346
538
  if (req.url === "/onbook-index.json") {
347
539
  console.log("[STORYBOOK_PLUGIN] Serving /onbook-index.json endpoint");
348
- const manifestPath = path2.join(process.cwd(), ".storybook-cache", "manifest.json");
540
+ const manifestPath = path5.join(process.cwd(), ".storybook-cache", "manifest.json");
349
541
  const cacheBuster = Date.now();
350
542
  console.log("[STORYBOOK_PLUGIN] Fetching http://localhost:6006/index.json");
351
543
  fetch(`http://localhost:6006/index.json?_t=${cacheBuster}`, {
@@ -362,7 +554,7 @@ var serveMetadataAndScreenshots = (req, res, next) => {
362
554
  });
363
555
  return response.json();
364
556
  }).then((indexData) => {
365
- const manifest = fs4.existsSync(manifestPath) ? JSON.parse(fs4.readFileSync(manifestPath, "utf-8")) : { stories: {} };
557
+ const manifest = fs5.existsSync(manifestPath) ? JSON.parse(fs5.readFileSync(manifestPath, "utf-8")) : { stories: {} };
366
558
  const defaultBoundingBox = { width: 1920, height: 1080 };
367
559
  for (const [storyId, entry] of Object.entries(indexData.entries || {})) {
368
560
  const manifestEntry = manifest.stories?.[storyId];
@@ -430,7 +622,7 @@ var serveMetadataAndScreenshots = (req, res, next) => {
430
622
  return;
431
623
  }
432
624
  if (req.url?.startsWith("/screenshots/")) {
433
- const screenshotPath = path2.join(
625
+ const screenshotPath = path5.join(
434
626
  process.cwd(),
435
627
  ".storybook-cache",
436
628
  req.url.replace("/screenshots/", "screenshots/")
@@ -439,11 +631,11 @@ var serveMetadataAndScreenshots = (req, res, next) => {
439
631
  const storyId = urlParts[0];
440
632
  const themeFile = urlParts[1];
441
633
  const theme = themeFile?.replace(".png", "");
442
- if (fs4.existsSync(screenshotPath)) {
634
+ if (fs5.existsSync(screenshotPath)) {
443
635
  res.setHeader("Content-Type", "image/png");
444
636
  res.setHeader("Access-Control-Allow-Origin", "*");
445
637
  res.setHeader("Cache-Control", "public, max-age=3600");
446
- fs4.createReadStream(screenshotPath).pipe(res);
638
+ fs5.createReadStream(screenshotPath).pipe(res);
447
639
  return;
448
640
  }
449
641
  if (storyId && theme && (theme === "light" || theme === "dark")) {
@@ -451,16 +643,16 @@ var serveMetadataAndScreenshots = (req, res, next) => {
451
643
  `[STORYBOOK_PLUGIN] Generating screenshot on-demand: ${storyId}/${theme}`
452
644
  );
453
645
  captureScreenshotBuffer(storyId, theme).then(({ buffer }) => {
454
- const storyDir = path2.join(
646
+ const storyDir = path5.join(
455
647
  process.cwd(),
456
648
  ".storybook-cache",
457
649
  "screenshots",
458
650
  storyId
459
651
  );
460
- if (!fs4.existsSync(storyDir)) {
461
- fs4.mkdirSync(storyDir, { recursive: true });
652
+ if (!fs5.existsSync(storyDir)) {
653
+ fs5.mkdirSync(storyDir, { recursive: true });
462
654
  }
463
- fs4.writeFileSync(screenshotPath, buffer);
655
+ fs5.writeFileSync(screenshotPath, buffer);
464
656
  res.setHeader("Content-Type", "image/png");
465
657
  res.setHeader("Access-Control-Allow-Origin", "*");
466
658
  res.setHeader("Cache-Control", "public, max-age=3600");
@@ -536,7 +728,19 @@ function storybookOnlookPlugin(options = {}) {
536
728
  },
537
729
  handleHotUpdate: handleStoryFileChange
538
730
  };
539
- return [componentLocPlugin(), mainPlugin];
731
+ return [lucideBarrelPlugin(), componentLocPlugin(), mainPlugin];
540
732
  }
733
+ var tolerantCsfIndexer = {
734
+ test: /(stories|story)\.(m?js|ts)x?$/,
735
+ createIndex: async (fileName, options) => {
736
+ try {
737
+ return (await readCsf(fileName, options)).parse().indexInputs;
738
+ } catch (err) {
739
+ const msg = err instanceof Error ? err.message : String(err);
740
+ console.warn("[onbook] Skipping broken story file:", fileName, msg);
741
+ return [];
742
+ }
743
+ }
744
+ };
541
745
 
542
- export { storybookOnlookPlugin };
746
+ export { storybookOnlookPlugin, tolerantCsfIndexer };
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "@onlook/storybook-plugin",
3
- "version": "0.3.3",
3
+ "version": "0.3.5",
4
4
  "type": "module",
5
5
  "bin": {
6
- "onlook-storybook": "./dist/cli/index.js"
6
+ "onlook-storybook": "dist/cli/index.js"
7
7
  },
8
8
  "exports": {
9
9
  ".": {
@@ -29,7 +29,9 @@
29
29
  "typecheck": "tsc --noEmit",
30
30
  "prepublishOnly": "bun run build && bun scripts/prepublish.ts",
31
31
  "postpublish": "bun scripts/postpublish.ts",
32
- "publish-pkg": "npm publish"
32
+ "publish-pkg": "npm publish",
33
+ "publish-pkg:next": "npm publish --tag next",
34
+ "promote-latest": "npm dist-tag add @onlook/storybook-plugin@$(node -p \"require('./package.json').version\") latest"
33
35
  },
34
36
  "dependencies": {
35
37
  "@babel/generator": "^7.26.9",
@@ -45,6 +47,7 @@
45
47
  "@types/babel__traverse": "^7.20.6",
46
48
  "@types/node": "^22.15.32",
47
49
  "bun-types": "^1.3.5",
50
+ "storybook": "^10.1.11",
48
51
  "tsup": "^8.5.1",
49
52
  "typescript": "5.8.3",
50
53
  "vite": "^6.3.5"
@@ -53,6 +56,7 @@
53
56
  "access": "public"
54
57
  },
55
58
  "peerDependencies": {
59
+ "storybook": "^10.0.0",
56
60
  "vite": "^5.0.0 || ^6.0.0"
57
61
  }
58
62
  }