@onlook/storybook-plugin 0.4.0-beta.15 → 0.4.0-beta.17

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 CHANGED
@@ -1,5 +1,5 @@
1
1
  import { createRequire } from 'node:module';
2
- import path7, { dirname, join, relative } from 'path';
2
+ import path8, { dirname, join, relative } from 'path';
3
3
  import crypto, { createHash } from 'crypto';
4
4
  import fs5, { existsSync } from 'fs';
5
5
  import { fileURLToPath } from 'url';
@@ -61,7 +61,7 @@ function envContainmentPlugin(options) {
61
61
  const resolveOutcomes = /* @__PURE__ */ new Map();
62
62
  const roots = /* @__PURE__ */ new Set([normalizeSlashes(process.cwd())]);
63
63
  const matchTarget = (absPath) => {
64
- const norm = normalizeSlashes(path7.normalize(absPath));
64
+ const norm = normalizeSlashes(path8.normalize(absPath));
65
65
  for (const rel of byModule.keys()) {
66
66
  for (const root of roots) {
67
67
  const target = `${root}/${rel}`;
@@ -96,10 +96,10 @@ function envContainmentPlugin(options) {
96
96
  if (source.startsWith("\0")) return null;
97
97
  const cleaned = stripQuery(source);
98
98
  let direct = null;
99
- if (path7.isAbsolute(cleaned)) {
99
+ if (path8.isAbsolute(cleaned)) {
100
100
  direct = cleaned;
101
101
  } else if (importer && (cleaned.startsWith("./") || cleaned.startsWith("../"))) {
102
- direct = path7.resolve(path7.dirname(stripQuery(importer)), cleaned);
102
+ direct = path8.resolve(path8.dirname(stripQuery(importer)), cleaned);
103
103
  }
104
104
  if (direct) {
105
105
  const rel = matchTarget(direct);
@@ -108,7 +108,7 @@ function envContainmentPlugin(options) {
108
108
  }
109
109
  if (!specifierMightMatch(cleaned)) return null;
110
110
  if (typeof this.resolve !== "function") return null;
111
- const importerDir = importer ? path7.dirname(stripQuery(importer)) : "";
111
+ const importerDir = importer ? path8.dirname(stripQuery(importer)) : "";
112
112
  const outcomeKey = `${importerDir}|${source}`;
113
113
  const memoized = resolveOutcomes.get(outcomeKey);
114
114
  if (memoized !== void 0) return memoized;
@@ -328,7 +328,7 @@ function componentLocPlugin(options = {}) {
328
328
  sourceFilename: filepath
329
329
  });
330
330
  let mutated = false;
331
- const relativePath = path7.relative(root, filepath);
331
+ const relativePath = path8.relative(root, filepath);
332
332
  traverse(ast, {
333
333
  JSXElement(nodePath) {
334
334
  const opening = nodePath.node.openingElement;
@@ -387,9 +387,9 @@ function fastRefreshTolerantExportsPlugin() {
387
387
  }
388
388
  };
389
389
  }
390
- var CACHE_DIR = path7.join(process.cwd(), ".storybook-cache");
391
- var SCREENSHOTS_DIR = path7.join(CACHE_DIR, "screenshots");
392
- var MANIFEST_PATH = path7.join(CACHE_DIR, "manifest.json");
390
+ var CACHE_DIR = path8.join(process.cwd(), ".storybook-cache");
391
+ var SCREENSHOTS_DIR = path8.join(CACHE_DIR, "screenshots");
392
+ var MANIFEST_PATH = path8.join(CACHE_DIR, "manifest.json");
393
393
  var VIEWPORT_WIDTH = 1920;
394
394
  var VIEWPORT_HEIGHT = 1080;
395
395
  var MIN_COMPONENT_WIDTH = 420;
@@ -514,6 +514,52 @@ async function getBrowser() {
514
514
  }
515
515
  return browserPromise;
516
516
  }
517
+
518
+ // src/screenshot-service/utils/capture-semaphore/capture-semaphore.ts
519
+ function createSemaphore(limit) {
520
+ const max = Number.isFinite(limit) ? Math.max(1, Math.floor(limit)) : 1;
521
+ let active = 0;
522
+ const waiters = [];
523
+ const acquire = () => {
524
+ if (active < max) {
525
+ active += 1;
526
+ return Promise.resolve();
527
+ }
528
+ return new Promise((resolve) => {
529
+ waiters.push(() => {
530
+ active += 1;
531
+ resolve();
532
+ });
533
+ });
534
+ };
535
+ const release = () => {
536
+ active -= 1;
537
+ const next = waiters.shift();
538
+ if (next) next();
539
+ };
540
+ return {
541
+ async run(fn) {
542
+ await acquire();
543
+ try {
544
+ return await fn();
545
+ } finally {
546
+ release();
547
+ }
548
+ }
549
+ };
550
+ }
551
+ var DEFAULT_CAPTURE_CONCURRENCY = 3;
552
+ function resolveConcurrency(raw) {
553
+ if (!raw) return DEFAULT_CAPTURE_CONCURRENCY;
554
+ const parsed = Number.parseInt(raw, 10);
555
+ if (!Number.isInteger(parsed) || parsed < 1) return DEFAULT_CAPTURE_CONCURRENCY;
556
+ return parsed;
557
+ }
558
+ var captureSemaphore = createSemaphore(
559
+ resolveConcurrency(process.env.ONLOOK_CAPTURE_CONCURRENCY)
560
+ );
561
+
562
+ // src/screenshot-service/utils/screenshot/screenshot.ts
517
563
  function classifyPreviewSnapshot(s) {
518
564
  if (s.text.includes("Sorry, something went wrong")) return "errored";
519
565
  if (s.text.includes("Couldn't find story")) return "errored";
@@ -580,96 +626,98 @@ async function detectRenderState(page, opts = {}) {
580
626
  }
581
627
  }
582
628
  function getScreenshotPath(storyId, theme) {
583
- const storyDir = path7.join(SCREENSHOTS_DIR, storyId);
584
- return path7.join(storyDir, `${theme}.png`);
629
+ const storyDir = path8.join(SCREENSHOTS_DIR, storyId);
630
+ return path8.join(storyDir, `${theme}.png`);
585
631
  }
586
632
  async function captureScreenshotBuffer(storyId, theme, width = VIEWPORT_WIDTH, height = VIEWPORT_HEIGHT, storybookUrl = "http://localhost:6006", loadTimeoutMs = 9e4, renderTimeoutMs = 2e4) {
587
- const browser = await getBrowser();
588
- const context = await browser.newContext({
589
- viewport: { width, height },
590
- deviceScaleFactor: 2
591
- });
592
- const page = await context.newPage();
593
- try {
594
- const url = `${storybookUrl}/iframe.html?id=${storyId}&viewMode=story&globals=theme:${theme}`;
595
- await page.goto(url, { timeout: loadTimeoutMs });
596
- await page.waitForLoadState("domcontentloaded", { timeout: loadTimeoutMs });
597
- await page.waitForLoadState("load", { timeout: loadTimeoutMs });
598
- try {
599
- await page.waitForLoadState("networkidle", { timeout: 5e3 });
600
- } catch {
601
- }
602
- const renderState = await detectRenderState(page, { hardCapMs: renderTimeoutMs });
603
- await page.evaluate(() => document.fonts.ready);
604
- try {
605
- await page.evaluate(async () => {
606
- const images = document.querySelectorAll("img");
607
- await Promise.all(
608
- Array.from(images).map((img) => {
609
- if (img.complete) return Promise.resolve();
610
- return new Promise((resolve) => {
611
- img.addEventListener("load", resolve);
612
- img.addEventListener("error", resolve);
613
- setTimeout(resolve, 3e3);
614
- });
615
- })
616
- );
617
- });
618
- } catch {
619
- }
620
- const contentBounds = await page.evaluate(() => {
621
- const root = document.querySelector("#storybook-root");
622
- if (!root) return null;
623
- const children = root.querySelectorAll("*");
624
- if (children.length === 0) return null;
625
- let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
626
- children.forEach((child) => {
627
- const rect = child.getBoundingClientRect();
628
- if (rect.width === 0 || rect.height === 0) return;
629
- minX = Math.min(minX, rect.left);
630
- minY = Math.min(minY, rect.top);
631
- maxX = Math.max(maxX, rect.right);
632
- maxY = Math.max(maxY, rect.bottom);
633
- });
634
- if (minX === Infinity) return null;
635
- return {
636
- x: minX,
637
- y: minY,
638
- width: maxX - minX,
639
- height: maxY - minY
640
- };
633
+ return captureSemaphore.run(async () => {
634
+ const browser = await getBrowser();
635
+ const context = await browser.newContext({
636
+ viewport: { width, height },
637
+ deviceScaleFactor: 2
641
638
  });
642
- let screenshotBuffer;
643
- let resultBoundingBox = null;
644
- if (contentBounds && contentBounds.width > 0 && contentBounds.height > 0) {
645
- const PADDING = 20;
646
- const clippedWidth = Math.min(width, contentBounds.width + PADDING);
647
- const clippedHeight = Math.min(height, contentBounds.height + PADDING);
648
- resultBoundingBox = {
649
- width: Math.max(MIN_COMPONENT_WIDTH, Math.round(clippedWidth)),
650
- height: Math.max(MIN_COMPONENT_HEIGHT, Math.round(clippedHeight))
651
- };
652
- screenshotBuffer = await page.screenshot({
653
- type: "png",
654
- clip: {
655
- x: Math.max(0, contentBounds.x - 10),
656
- y: Math.max(0, contentBounds.y - 10),
657
- width: clippedWidth,
658
- height: clippedHeight
659
- }
639
+ const page = await context.newPage();
640
+ try {
641
+ const url = `${storybookUrl}/iframe.html?id=${storyId}&viewMode=story&globals=theme:${theme}`;
642
+ await page.goto(url, { timeout: loadTimeoutMs });
643
+ await page.waitForLoadState("domcontentloaded", { timeout: loadTimeoutMs });
644
+ await page.waitForLoadState("load", { timeout: loadTimeoutMs });
645
+ try {
646
+ await page.waitForLoadState("networkidle", { timeout: 5e3 });
647
+ } catch {
648
+ }
649
+ const renderState = await detectRenderState(page, { hardCapMs: renderTimeoutMs });
650
+ await page.evaluate(() => document.fonts.ready);
651
+ try {
652
+ await page.evaluate(async () => {
653
+ const images = document.querySelectorAll("img");
654
+ await Promise.all(
655
+ Array.from(images).map((img) => {
656
+ if (img.complete) return Promise.resolve();
657
+ return new Promise((resolve) => {
658
+ img.addEventListener("load", resolve);
659
+ img.addEventListener("error", resolve);
660
+ setTimeout(resolve, 3e3);
661
+ });
662
+ })
663
+ );
664
+ });
665
+ } catch {
666
+ }
667
+ const contentBounds = await page.evaluate(() => {
668
+ const root = document.querySelector("#storybook-root");
669
+ if (!root) return null;
670
+ const children = root.querySelectorAll("*");
671
+ if (children.length === 0) return null;
672
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
673
+ children.forEach((child) => {
674
+ const rect = child.getBoundingClientRect();
675
+ if (rect.width === 0 || rect.height === 0) return;
676
+ minX = Math.min(minX, rect.left);
677
+ minY = Math.min(minY, rect.top);
678
+ maxX = Math.max(maxX, rect.right);
679
+ maxY = Math.max(maxY, rect.bottom);
680
+ });
681
+ if (minX === Infinity) return null;
682
+ return {
683
+ x: minX,
684
+ y: minY,
685
+ width: maxX - minX,
686
+ height: maxY - minY
687
+ };
660
688
  });
661
- } else {
662
- screenshotBuffer = await page.screenshot({ type: "png" });
689
+ let screenshotBuffer;
690
+ let resultBoundingBox = null;
691
+ if (contentBounds && contentBounds.width > 0 && contentBounds.height > 0) {
692
+ const PADDING = 20;
693
+ const clippedWidth = Math.min(width, contentBounds.width + PADDING);
694
+ const clippedHeight = Math.min(height, contentBounds.height + PADDING);
695
+ resultBoundingBox = {
696
+ width: Math.max(MIN_COMPONENT_WIDTH, Math.round(clippedWidth)),
697
+ height: Math.max(MIN_COMPONENT_HEIGHT, Math.round(clippedHeight))
698
+ };
699
+ screenshotBuffer = await page.screenshot({
700
+ type: "png",
701
+ clip: {
702
+ x: Math.max(0, contentBounds.x - 10),
703
+ y: Math.max(0, contentBounds.y - 10),
704
+ width: clippedWidth,
705
+ height: clippedHeight
706
+ }
707
+ });
708
+ } else {
709
+ screenshotBuffer = await page.screenshot({ type: "png" });
710
+ }
711
+ return { buffer: screenshotBuffer, boundingBox: resultBoundingBox, renderState };
712
+ } finally {
713
+ await context.close();
663
714
  }
664
- return { buffer: screenshotBuffer, boundingBox: resultBoundingBox, renderState };
665
- } finally {
666
- await context.close();
667
- }
715
+ });
668
716
  }
669
717
  async function generateScreenshot(storyId, theme, storybookUrl = "http://localhost:6006", timeoutMs = 3e4) {
670
718
  try {
671
719
  ensureCacheDirectories();
672
- const storyDir = path7.join(SCREENSHOTS_DIR, storyId);
720
+ const storyDir = path8.join(SCREENSHOTS_DIR, storyId);
673
721
  if (!fs5.existsSync(storyDir)) {
674
722
  fs5.mkdirSync(storyDir, { recursive: true });
675
723
  }
@@ -771,7 +819,39 @@ async function waitForIndexAndBroadcast(port = 6006) {
771
819
  });
772
820
  }
773
821
 
822
+ // src/handlers/handleStoryFileChange/captureCoalescer/captureCoalescer.ts
823
+ function createCaptureCoalescer(runCapture) {
824
+ const inFlight2 = /* @__PURE__ */ new Map();
825
+ const trailing = /* @__PURE__ */ new Set();
826
+ const start = (storyId) => {
827
+ const done = (async () => {
828
+ try {
829
+ await runCapture(storyId);
830
+ } finally {
831
+ inFlight2.delete(storyId);
832
+ if (trailing.has(storyId)) {
833
+ trailing.delete(storyId);
834
+ await start(storyId);
835
+ }
836
+ }
837
+ })();
838
+ inFlight2.set(storyId, done);
839
+ return done;
840
+ };
841
+ return {
842
+ capture(storyId) {
843
+ const existing = inFlight2.get(storyId);
844
+ if (existing) {
845
+ trailing.add(storyId);
846
+ return existing;
847
+ }
848
+ return start(storyId);
849
+ }
850
+ };
851
+ }
852
+
774
853
  // src/handlers/handleStoryFileChange/handleStoryFileChange.ts
854
+ var STORYBOOK_URL = "http://localhost:6006";
775
855
  var cachedIndex = null;
776
856
  var indexFetchPromise = null;
777
857
  var pendingFiles = /* @__PURE__ */ new Set();
@@ -790,47 +870,46 @@ async function fetchStorybookIndex() {
790
870
  }
791
871
  function getStoriesForFile(filePath) {
792
872
  if (!cachedIndex) return [];
793
- const fileName = path7.basename(filePath);
873
+ const fileName = path8.basename(filePath);
794
874
  return Object.values(cachedIndex.entries).filter((entry) => entry.type === "story" && entry.importPath.endsWith(fileName)).map((entry) => entry.id);
795
875
  }
876
+ var latestStoryFiles = /* @__PURE__ */ new Map();
877
+ async function captureStoryAndUpdateManifest(storyId) {
878
+ const [lightResult] = await Promise.all([
879
+ generateScreenshot(storyId, "light", STORYBOOK_URL),
880
+ generateScreenshot(storyId, "dark", STORYBOOK_URL)
881
+ ]);
882
+ const boundingBox = lightResult?.boundingBox ?? void 0;
883
+ for (const file of latestStoryFiles.get(storyId) ?? []) {
884
+ const fileHash = computeFileHash(file);
885
+ updateManifest(storyId, file, fileHash, boundingBox);
886
+ }
887
+ }
888
+ var captureCoalescer = createCaptureCoalescer(captureStoryAndUpdateManifest);
796
889
  async function regenerateScreenshotsForFiles(files) {
797
890
  await fetchStorybookIndex();
798
- const allStoryIds = /* @__PURE__ */ new Set();
799
- const fileToStories = /* @__PURE__ */ new Map();
891
+ const storyToFiles = /* @__PURE__ */ new Map();
800
892
  for (const file of files) {
801
- const storyIds = getStoriesForFile(file);
802
- fileToStories.set(file, storyIds);
803
- for (const id of storyIds) {
804
- allStoryIds.add(id);
893
+ for (const storyId of getStoriesForFile(file)) {
894
+ const arr = storyToFiles.get(storyId) ?? [];
895
+ arr.push(file);
896
+ storyToFiles.set(storyId, arr);
805
897
  }
806
898
  }
807
- if (allStoryIds.size === 0) {
899
+ if (storyToFiles.size === 0) {
808
900
  console.log("[Screenshots] No stories found for changed files");
809
901
  return;
810
902
  }
811
903
  console.log(
812
- `[Screenshots] Regenerating ${allStoryIds.size} stories from ${files.length} files`
904
+ `[Screenshots] Regenerating ${storyToFiles.size} stories from ${files.length} files`
813
905
  );
814
- const storybookUrl = "http://localhost:6006";
815
- const storyBoundingBoxes = /* @__PURE__ */ new Map();
906
+ for (const [storyId, storyFiles] of storyToFiles) {
907
+ latestStoryFiles.set(storyId, storyFiles);
908
+ }
816
909
  await Promise.all(
817
- Array.from(allStoryIds).map(async (storyId) => {
818
- const [lightResult, _darkResult] = await Promise.all([
819
- generateScreenshot(storyId, "light", storybookUrl),
820
- generateScreenshot(storyId, "dark", storybookUrl)
821
- ]);
822
- if (lightResult) {
823
- storyBoundingBoxes.set(storyId, lightResult.boundingBox);
824
- }
825
- })
910
+ Array.from(storyToFiles.keys()).map((storyId) => captureCoalescer.capture(storyId))
826
911
  );
827
- for (const [file, storyIds] of fileToStories) {
828
- const fileHash = computeFileHash(file);
829
- storyIds.forEach((storyId) => {
830
- updateManifest(storyId, file, fileHash, storyBoundingBoxes.get(storyId));
831
- });
832
- }
833
- console.log(`[Screenshots] \u2713 Regenerated ${allStoryIds.size} stories`);
912
+ console.log(`[Screenshots] \u2713 Regenerated ${storyToFiles.size} stories`);
834
913
  }
835
914
  function handleStoryFileChange({ file, modules }) {
836
915
  if (file.endsWith(".stories.tsx") || file.endsWith(".stories.ts")) {
@@ -857,63 +936,258 @@ function handleStoryFileChange({ file, modules }) {
857
936
  }
858
937
  }
859
938
 
939
+ // src/package-mock/clerk-mock.ts
940
+ var VALID_IDENT_RE2 = /^[A-Za-z_$][\w$]*$/;
941
+ var RESERVED_HELPER_NAMES = /* @__PURE__ */ new Set([
942
+ "__user",
943
+ "__pass",
944
+ "__null",
945
+ "__clerk",
946
+ "__sessionId",
947
+ "__asyncNoop",
948
+ "__noop",
949
+ "createElement",
950
+ "Fragment"
951
+ ]);
952
+ var BUILDERS = {
953
+ "@clerk/nextjs": buildClerkMockSource,
954
+ "@clerk/clerk-react": buildClerkMockSource
955
+ };
956
+ function isKnownMockPackage(pkg) {
957
+ return pkg in BUILDERS;
958
+ }
959
+ function buildPackageMockSource(pkg, usedExports) {
960
+ const build = BUILDERS[pkg];
961
+ if (!build) throw new Error(`no package mock for '${pkg}'`);
962
+ return build(usedExports);
963
+ }
964
+ var CLERK_KNOWN_HOOKS = {
965
+ useAuth: "({ isLoaded: true, isSignedIn: true, userId: __user.id, sessionId: __sessionId, actor: null, orgId: null, orgRole: null, orgSlug: null, has: () => true, signOut: __asyncNoop, getToken: async () => null })",
966
+ useUser: "({ isLoaded: true, isSignedIn: true, user: __user })",
967
+ useSession: '({ isLoaded: true, isSignedIn: true, session: { id: __sessionId, status: "active", user: __user } })',
968
+ useSessionList: "({ isLoaded: true, sessions: [], setActive: __asyncNoop })",
969
+ useClerk: "__clerk",
970
+ useOrganization: "({ isLoaded: true, organization: null, membership: null, memberships: undefined })",
971
+ useOrganizationList: "({ isLoaded: true, userMemberships: undefined, setActive: __asyncNoop })",
972
+ useSignIn: "({ isLoaded: true, signIn: undefined, setActive: __asyncNoop })",
973
+ useSignUp: "({ isLoaded: true, signUp: undefined, setActive: __asyncNoop })"
974
+ };
975
+ var CLERK_CHILDREN_COMPONENTS = /* @__PURE__ */ new Set([
976
+ "ClerkProvider",
977
+ "ClerkLoaded",
978
+ "SignedIn",
979
+ "Protect",
980
+ "SignInButton",
981
+ "SignUpButton",
982
+ "SignOutButton",
983
+ "SignInWithMetamaskButton"
984
+ ]);
985
+ var CLERK_NULL_COMPONENTS = /* @__PURE__ */ new Set([
986
+ "SignedOut",
987
+ "ClerkLoading",
988
+ "RedirectToSignIn",
989
+ "RedirectToSignUp",
990
+ "RedirectToUserProfile",
991
+ "RedirectToOrganizationProfile",
992
+ "RedirectToCreateOrganization",
993
+ "AuthenticateWithRedirectCallback",
994
+ "UserButton",
995
+ "UserProfile",
996
+ "OrganizationSwitcher",
997
+ "OrganizationProfile",
998
+ "OrganizationList",
999
+ "CreateOrganization",
1000
+ "SignIn",
1001
+ "SignUp",
1002
+ "Waitlist",
1003
+ "GoogleOneTap"
1004
+ ]);
1005
+ var CLERK_PREAMBLE = [
1006
+ "// Auto-generated by @onlook/storybook-plugin (package mock). Zero-backend,",
1007
+ "// signed-in Clerk stand-in so components that call useUser()/useAuth() render",
1008
+ "// authenticated content in the canvas without a publishable key, clerk-js, or",
1009
+ "// a network round-trip. Swap back to the real @clerk/* package for live data.",
1010
+ "import { createElement, Fragment } from 'react';",
1011
+ "const __asyncNoop = async () => {};",
1012
+ "const __noop = () => {};",
1013
+ // No `: any` param annotation: the virtual module is served extensionless
1014
+ // (see header) so Vite parses it as PLAIN JS — a TS type annotation here is a
1015
+ // runtime `Unexpected token ':'` that breaks every story importing the package.
1016
+ "const __pass = (props) => createElement(Fragment, null, props ? props.children : undefined);",
1017
+ "const __null = () => null;",
1018
+ "const __sessionId = 'sess_onlookMock';",
1019
+ "// A plausible signed-in Clerk user. Fields cover the common UserResource",
1020
+ "// surface components read at render; no image URL (avoids a network fetch).",
1021
+ "const __user = {",
1022
+ " id: 'user_onlookMock',",
1023
+ " firstName: 'Ada',",
1024
+ " lastName: 'Lovelace',",
1025
+ " fullName: 'Ada Lovelace',",
1026
+ " username: 'ada',",
1027
+ " imageUrl: '',",
1028
+ " hasImage: false,",
1029
+ " primaryEmailAddressId: 'idn_onlookMock',",
1030
+ " primaryEmailAddress: { id: 'idn_onlookMock', emailAddress: 'ada@example.com' },",
1031
+ " emailAddresses: [{ id: 'idn_onlookMock', emailAddress: 'ada@example.com' }],",
1032
+ " primaryPhoneNumber: null,",
1033
+ " phoneNumbers: [],",
1034
+ " externalAccounts: [],",
1035
+ " organizationMemberships: [],",
1036
+ " publicMetadata: {},",
1037
+ " unsafeMetadata: {},",
1038
+ " reload: __asyncNoop,",
1039
+ " update: __asyncNoop,",
1040
+ "};",
1041
+ "const __clerk = {",
1042
+ " loaded: true,",
1043
+ " user: __user,",
1044
+ " session: { id: __sessionId, status: 'active', user: __user },",
1045
+ " organization: null,",
1046
+ " openSignIn: __noop,",
1047
+ " openSignUp: __noop,",
1048
+ " openUserProfile: __noop,",
1049
+ " openOrganizationProfile: __noop,",
1050
+ " closeSignIn: __noop,",
1051
+ " closeSignUp: __noop,",
1052
+ " signOut: __asyncNoop,",
1053
+ " setActive: __asyncNoop,",
1054
+ "};"
1055
+ ];
1056
+ function clerkExportLine(name) {
1057
+ if (name in CLERK_KNOWN_HOOKS) {
1058
+ return `export const ${name} = () => ${CLERK_KNOWN_HOOKS[name]};`;
1059
+ }
1060
+ if (CLERK_CHILDREN_COMPONENTS.has(name)) {
1061
+ return `export const ${name} = __pass;`;
1062
+ }
1063
+ if (CLERK_NULL_COMPONENTS.has(name)) {
1064
+ return `export const ${name} = __null;`;
1065
+ }
1066
+ if (/^use[A-Z]/.test(name)) {
1067
+ return `export const ${name} = () => ({ isLoaded: true, isSignedIn: true });`;
1068
+ }
1069
+ if (/^[A-Z]/.test(name)) {
1070
+ return `export const ${name} = __pass;`;
1071
+ }
1072
+ return `export const ${name} = undefined;`;
1073
+ }
1074
+ function buildClerkMockSource(usedExports) {
1075
+ const names = Array.from(
1076
+ new Set(
1077
+ usedExports.filter(
1078
+ (n) => VALID_IDENT_RE2.test(n) && n !== "default" && !RESERVED_HELPER_NAMES.has(n)
1079
+ )
1080
+ )
1081
+ ).sort();
1082
+ const lines = [
1083
+ ...CLERK_PREAMBLE,
1084
+ "",
1085
+ ...names.map(clerkExportLine),
1086
+ "",
1087
+ "// Rarely used, but `import Clerk from ...` must not fail: an inert object.",
1088
+ "export default {};",
1089
+ ""
1090
+ ];
1091
+ return lines.join("\n");
1092
+ }
1093
+
1094
+ // src/package-mock/package-mock.ts
1095
+ var PACKAGE_MOCK_PLUGIN_NAME = "onlook-package-mock";
1096
+ var PACKAGE_MOCK_VIRTUAL_PREFIX = "\0onlook-package-mock:";
1097
+ function stripQuery2(id) {
1098
+ const q = id.indexOf("?");
1099
+ return q === -1 ? id : id.slice(0, q);
1100
+ }
1101
+ function packageMockPlugin(options) {
1102
+ const entries = (options?.packages ?? []).filter(
1103
+ (e) => e && typeof e.package === "string" && e.package.length > 0 && // Only known auth packages get a functional mock — an unknown package
1104
+ // has no source builder, so silently drop it (honest skip) rather than
1105
+ // intercept it into a broken module.
1106
+ isKnownMockPackage(e.package)
1107
+ );
1108
+ if (entries.length === 0) return null;
1109
+ const usedByPackage = /* @__PURE__ */ new Map();
1110
+ for (const e of entries) usedByPackage.set(e.package, e.usedExports ?? []);
1111
+ return {
1112
+ name: PACKAGE_MOCK_PLUGIN_NAME,
1113
+ // `pre` so we intercept the specifier before Vite's resolve pipeline (and
1114
+ // before tsconfig-paths / node resolution) claims it.
1115
+ enforce: "pre",
1116
+ resolveId(source) {
1117
+ if (source.startsWith("\0")) return null;
1118
+ const cleaned = stripQuery2(source);
1119
+ if (!usedByPackage.has(cleaned)) return null;
1120
+ return PACKAGE_MOCK_VIRTUAL_PREFIX + cleaned;
1121
+ },
1122
+ load(id) {
1123
+ if (!id.startsWith(PACKAGE_MOCK_VIRTUAL_PREFIX)) return null;
1124
+ const pkg = id.slice(PACKAGE_MOCK_VIRTUAL_PREFIX.length);
1125
+ const used = usedByPackage.get(pkg);
1126
+ if (used === void 0) return null;
1127
+ return buildPackageMockSource(pkg, used);
1128
+ }
1129
+ };
1130
+ }
1131
+
860
1132
  // src/screenshot-service/utils/console-logs/console-logs.ts
861
1133
  async function captureConsoleLogs(storyId, theme = "light", storybookUrl = "http://localhost:6006", timeoutMs = 3e4) {
862
- const browser = await getBrowser();
863
- const context = await browser.newContext({
864
- viewport: { width: VIEWPORT_WIDTH, height: VIEWPORT_HEIGHT }
865
- });
866
- const page = await context.newPage();
867
- const logs = [];
868
- const pageErrors = [];
869
- const failedRequests = [];
870
- page.on("console", (msg) => {
871
- const level = msg.type();
872
- logs.push({
873
- level: level === "warning" ? "warn" : level,
874
- text: msg.text(),
875
- timestamp: Date.now()
1134
+ return captureSemaphore.run(async () => {
1135
+ const browser = await getBrowser();
1136
+ const context = await browser.newContext({
1137
+ viewport: { width: VIEWPORT_WIDTH, height: VIEWPORT_HEIGHT }
876
1138
  });
877
- });
878
- page.on("pageerror", (err) => {
879
- pageErrors.push(err.message);
880
- });
881
- page.on("response", (res) => {
882
- const status = res.status();
883
- if (status >= 400) {
884
- const req = res.request();
1139
+ const page = await context.newPage();
1140
+ const logs = [];
1141
+ const pageErrors = [];
1142
+ const failedRequests = [];
1143
+ page.on("console", (msg) => {
1144
+ const level = msg.type();
1145
+ logs.push({
1146
+ level: level === "warning" ? "warn" : level,
1147
+ text: msg.text(),
1148
+ timestamp: Date.now()
1149
+ });
1150
+ });
1151
+ page.on("pageerror", (err) => {
1152
+ pageErrors.push(err.message);
1153
+ });
1154
+ page.on("response", (res) => {
1155
+ const status = res.status();
1156
+ if (status >= 400) {
1157
+ const req = res.request();
1158
+ failedRequests.push({
1159
+ url: res.url(),
1160
+ status,
1161
+ method: req.method(),
1162
+ resourceType: req.resourceType(),
1163
+ failure: null
1164
+ });
1165
+ }
1166
+ });
1167
+ page.on("requestfailed", (req) => {
885
1168
  failedRequests.push({
886
- url: res.url(),
887
- status,
1169
+ url: req.url(),
1170
+ status: null,
888
1171
  method: req.method(),
889
1172
  resourceType: req.resourceType(),
890
- failure: null
1173
+ failure: req.failure()?.errorText ?? "request failed"
891
1174
  });
892
- }
893
- });
894
- page.on("requestfailed", (req) => {
895
- failedRequests.push({
896
- url: req.url(),
897
- status: null,
898
- method: req.method(),
899
- resourceType: req.resourceType(),
900
- failure: req.failure()?.errorText ?? "request failed"
901
1175
  });
902
- });
903
- const url = `${storybookUrl}/iframe.html?id=${storyId}&viewMode=story&globals=theme:${theme}`;
904
- try {
905
- await page.goto(url, { timeout: timeoutMs });
906
- await page.waitForLoadState("domcontentloaded", { timeout: timeoutMs });
907
- await page.waitForLoadState("load", { timeout: timeoutMs });
1176
+ const url = `${storybookUrl}/iframe.html?id=${storyId}&viewMode=story&globals=theme:${theme}`;
908
1177
  try {
909
- await page.waitForLoadState("networkidle", { timeout: 5e3 });
910
- } catch {
1178
+ await page.goto(url, { timeout: timeoutMs });
1179
+ await page.waitForLoadState("domcontentloaded", { timeout: timeoutMs });
1180
+ await page.waitForLoadState("load", { timeout: timeoutMs });
1181
+ try {
1182
+ await page.waitForLoadState("networkidle", { timeout: 5e3 });
1183
+ } catch {
1184
+ }
1185
+ await new Promise((r) => setTimeout(r, 1e3));
1186
+ return { logs, pageErrors, failedRequests, url };
1187
+ } finally {
1188
+ await context.close();
911
1189
  }
912
- await new Promise((r) => setTimeout(r, 1e3));
913
- return { logs, pageErrors, failedRequests, url };
914
- } finally {
915
- await context.close();
916
- }
1190
+ });
917
1191
  }
918
1192
 
919
1193
  // src/soft-story-rerender/soft-story-rerender.ts
@@ -985,8 +1259,8 @@ function softStoryRerenderPlugin() {
985
1259
  preview.onStoriesChanged({ importFn: newModule.importFn });
986
1260
  }
987
1261
  });`;
988
- const out = code.replace(pattern, (_match, quote, path8) => {
989
- return replacement.replace("$QUOTE$$PATH$$QUOTE$", `${quote}${path8}${quote}`);
1262
+ const out = code.replace(pattern, (_match, quote, path9) => {
1263
+ return replacement.replace("$QUOTE$$PATH$$QUOTE$", `${quote}${path9}${quote}`);
990
1264
  });
991
1265
  return out === code ? null : { code: out, map: null };
992
1266
  }
@@ -997,14 +1271,14 @@ function resolveTsconfigAliases(root) {
997
1271
  try {
998
1272
  const config = readTsconfigAliasConfig(root);
999
1273
  if (!config) return [];
1000
- const base = path7.resolve(root, config.baseUrl ?? ".");
1274
+ const base = path8.resolve(root, config.baseUrl ?? ".");
1001
1275
  const aliases = [];
1002
1276
  for (const [key, targets] of Object.entries(config.paths)) {
1003
1277
  const target = targets[0];
1004
1278
  if (!target) continue;
1005
1279
  if (key.endsWith("/*") && target.endsWith("/*")) {
1006
1280
  const findPrefix = key.slice(0, -1);
1007
- const targetDir = path7.resolve(base, target.slice(0, -2));
1281
+ const targetDir = path8.resolve(base, target.slice(0, -2));
1008
1282
  aliases.push({
1009
1283
  find: new RegExp(`^${escapeRegExp(findPrefix)}`),
1010
1284
  replacement: `${targetDir}/`
@@ -1012,7 +1286,7 @@ function resolveTsconfigAliases(root) {
1012
1286
  } else if (!key.includes("*") && !target.includes("*")) {
1013
1287
  aliases.push({
1014
1288
  find: new RegExp(`^${escapeRegExp(key)}$`),
1015
- replacement: path7.resolve(base, target)
1289
+ replacement: path8.resolve(base, target)
1016
1290
  });
1017
1291
  }
1018
1292
  }
@@ -1023,7 +1297,7 @@ function resolveTsconfigAliases(root) {
1023
1297
  }
1024
1298
  function readTsconfigAliasConfig(root) {
1025
1299
  for (const candidate of TSCONFIG_CANDIDATES) {
1026
- const abs = path7.join(root, candidate);
1300
+ const abs = path8.join(root, candidate);
1027
1301
  let text;
1028
1302
  try {
1029
1303
  text = fs5.readFileSync(abs, "utf-8");
@@ -1104,6 +1378,130 @@ function findGitRoot(startPath) {
1104
1378
  }
1105
1379
  return null;
1106
1380
  }
1381
+ function ifNoneMatchHits(header, etag) {
1382
+ if (!header) return false;
1383
+ return header.split(",").some((raw) => {
1384
+ const token = raw.trim();
1385
+ return token === etag || token === `W/${etag}`;
1386
+ });
1387
+ }
1388
+ function decideIndexResponse(body, ifNoneMatch) {
1389
+ const etag = `"${createHash("sha1").update(body).digest("hex")}"`;
1390
+ return { etag, notModified: ifNoneMatchHits(ifNoneMatch, etag) };
1391
+ }
1392
+ var STORY_FILE_PATTERN = /\.stories\.(js|jsx|mjs|ts|tsx|mdx)$/;
1393
+ function isStoryFile(file) {
1394
+ return STORY_FILE_PATTERN.test(file);
1395
+ }
1396
+ function missingFromIndex(index, files) {
1397
+ const importPaths = [];
1398
+ for (const entry of Object.values(index.entries ?? {})) {
1399
+ if (typeof entry.importPath === "string") importPaths.push(entry.importPath);
1400
+ }
1401
+ return files.filter((file) => {
1402
+ const base = path8.basename(file);
1403
+ return !importPaths.some((importPath) => importPath.endsWith(base));
1404
+ });
1405
+ }
1406
+ function portFromArgv(argv) {
1407
+ for (let i = 0; i < argv.length; i += 1) {
1408
+ const arg = argv[i];
1409
+ if (arg === void 0) continue;
1410
+ let raw;
1411
+ if (arg === "-p" || arg === "--port") {
1412
+ raw = argv[i + 1];
1413
+ } else {
1414
+ raw = /^(?:-p|--port)=(\d+)$/.exec(arg)?.[1];
1415
+ }
1416
+ if (raw === void 0) continue;
1417
+ const parsed = Number.parseInt(raw, 10);
1418
+ if (Number.isInteger(parsed) && parsed > 0 && parsed < 65536) return parsed;
1419
+ }
1420
+ return null;
1421
+ }
1422
+ async function defaultFetchIndex(port) {
1423
+ try {
1424
+ const res = await fetch(`http://localhost:${port}/index.json`, {
1425
+ signal: AbortSignal.timeout(2e3)
1426
+ });
1427
+ if (!res.ok) return null;
1428
+ return await res.json();
1429
+ } catch {
1430
+ return null;
1431
+ }
1432
+ }
1433
+ var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
1434
+ function createStoryIndexBroadcaster({
1435
+ ports,
1436
+ notify,
1437
+ fetchIndex = defaultFetchIndex,
1438
+ debounceMs = 500,
1439
+ probeIntervalMs = 500,
1440
+ probeCeilingMs = 15e3
1441
+ }) {
1442
+ const candidatePorts = [...new Set(ports)];
1443
+ const pendingAdds = /* @__PURE__ */ new Set();
1444
+ let addDebounce = null;
1445
+ let unlinkDebounce = null;
1446
+ let probing = false;
1447
+ const scheduleProbe = () => {
1448
+ if (addDebounce) clearTimeout(addDebounce);
1449
+ addDebounce = setTimeout(() => {
1450
+ addDebounce = null;
1451
+ void probeThenNotify();
1452
+ }, debounceMs);
1453
+ };
1454
+ const probeThenNotify = async () => {
1455
+ if (probing) return;
1456
+ probing = true;
1457
+ const files = Array.from(pendingAdds);
1458
+ const deadline = Date.now() + probeCeilingMs;
1459
+ try {
1460
+ if (files.length === 0) return;
1461
+ while (Date.now() < deadline) {
1462
+ for (const candidate of candidatePorts) {
1463
+ const index = await fetchIndex(candidate);
1464
+ if (index && missingFromIndex(index, files).length === 0) {
1465
+ console.log(
1466
+ "[STORYBOOK_PLUGIN] Index caught up with new story files \u2014 broadcasting",
1467
+ { files, port: candidate }
1468
+ );
1469
+ for (const file of files) pendingAdds.delete(file);
1470
+ notify();
1471
+ return;
1472
+ }
1473
+ }
1474
+ await sleep(probeIntervalMs);
1475
+ }
1476
+ console.warn("[STORYBOOK_PLUGIN] Index probe hit ceiling \u2014 broadcasting anyway", {
1477
+ files,
1478
+ probeCeilingMs
1479
+ });
1480
+ for (const file of files) pendingAdds.delete(file);
1481
+ notify();
1482
+ } finally {
1483
+ probing = false;
1484
+ if (pendingAdds.size > 0) scheduleProbe();
1485
+ }
1486
+ };
1487
+ return {
1488
+ onStoryFileAdded(file) {
1489
+ if (!isStoryFile(file)) return;
1490
+ pendingAdds.add(file);
1491
+ scheduleProbe();
1492
+ },
1493
+ onStoryFileRemoved(file) {
1494
+ if (!isStoryFile(file)) return;
1495
+ pendingAdds.delete(file);
1496
+ if (unlinkDebounce) clearTimeout(unlinkDebounce);
1497
+ unlinkDebounce = setTimeout(() => {
1498
+ unlinkDebounce = null;
1499
+ console.log("[STORYBOOK_PLUGIN] Story file removed \u2014 broadcasting", { file });
1500
+ notify();
1501
+ }, debounceMs);
1502
+ }
1503
+ };
1504
+ }
1107
1505
 
1108
1506
  // src/storybook-onlook-plugin.ts
1109
1507
  function errorCode(error) {
@@ -1170,7 +1568,7 @@ var TAILWIND_ENTRY_CANDIDATES = [
1170
1568
  var TAILWIND_DIRECTIVE_PATTERN = /@import\s+['"](?:tailwindcss|tailwindcss\/.+)['"]|@tailwind\s+(?:base|components|utilities)/;
1171
1569
  function detectTailwindEntryCss(cwd) {
1172
1570
  for (const rel of TAILWIND_ENTRY_CANDIDATES) {
1173
- const abs = path7.join(cwd, rel);
1571
+ const abs = path8.join(cwd, rel);
1174
1572
  try {
1175
1573
  if (!fs5.existsSync(abs)) continue;
1176
1574
  const head = fs5.readFileSync(abs, "utf-8").slice(0, 2048);
@@ -1183,15 +1581,8 @@ function detectTailwindEntryCss(cwd) {
1183
1581
  var serveMetadataAndScreenshots = (req, res, next) => {
1184
1582
  if (req.url === "/onbook-index.json") {
1185
1583
  console.log("[STORYBOOK_PLUGIN] Serving /onbook-index.json endpoint");
1186
- const cacheBuster = Date.now();
1187
1584
  console.log("[STORYBOOK_PLUGIN] Fetching http://localhost:6006/index.json");
1188
- fetch(`http://localhost:6006/index.json?_t=${cacheBuster}`, {
1189
- cache: "no-store",
1190
- headers: {
1191
- "Cache-Control": "no-cache",
1192
- Pragma: "no-cache"
1193
- }
1194
- }).then((response) => {
1585
+ fetch("http://localhost:6006/index.json").then((response) => {
1195
1586
  console.log("[STORYBOOK_PLUGIN] Storybook index fetch response", {
1196
1587
  status: response.status,
1197
1588
  ok: response.ok,
@@ -1210,12 +1601,21 @@ var serveMetadataAndScreenshots = (req, res, next) => {
1210
1601
  entryCount: Object.keys(indexData.entries || {}).length,
1211
1602
  hasMetadata: !!indexData.meta
1212
1603
  });
1213
- res.setHeader("Content-Type", "application/json");
1604
+ const body = JSON.stringify(indexData);
1605
+ const { etag, notModified } = decideIndexResponse(
1606
+ body,
1607
+ req.headers["if-none-match"]
1608
+ );
1214
1609
  res.setHeader("Access-Control-Allow-Origin", "*");
1215
- res.setHeader("Cache-Control", "no-store, no-cache, must-revalidate");
1216
- res.setHeader("Pragma", "no-cache");
1217
- res.setHeader("Expires", "0");
1218
- res.end(JSON.stringify(indexData));
1610
+ res.setHeader("ETag", etag);
1611
+ res.setHeader("Cache-Control", "no-cache");
1612
+ if (notModified) {
1613
+ res.statusCode = 304;
1614
+ res.end();
1615
+ return;
1616
+ }
1617
+ res.setHeader("Content-Type", "application/json");
1618
+ res.end(body);
1219
1619
  }).catch((error) => {
1220
1620
  console.error("[STORYBOOK_PLUGIN] Failed to fetch/extend index.json", {
1221
1621
  error: error instanceof Error ? error.message : String(error),
@@ -1307,7 +1707,7 @@ var serveMetadataAndScreenshots = (req, res, next) => {
1307
1707
  return;
1308
1708
  }
1309
1709
  if (req.url?.startsWith("/screenshots/")) {
1310
- const screenshotPath = path7.join(
1710
+ const screenshotPath = path8.join(
1311
1711
  process.cwd(),
1312
1712
  ".storybook-cache",
1313
1713
  req.url.replace("/screenshots/", "screenshots/")
@@ -1328,7 +1728,7 @@ var serveMetadataAndScreenshots = (req, res, next) => {
1328
1728
  `[STORYBOOK_PLUGIN] Generating screenshot on-demand: ${storyId}/${theme}`
1329
1729
  );
1330
1730
  captureScreenshotBuffer(storyId, theme).then(({ buffer }) => {
1331
- const storyDir = path7.join(
1731
+ const storyDir = path8.join(
1332
1732
  process.cwd(),
1333
1733
  ".storybook-cache",
1334
1734
  "screenshots",
@@ -1447,7 +1847,7 @@ function storybookOnlookPlugin(options = {}) {
1447
1847
  configureServer(server) {
1448
1848
  console.log("[STORYBOOK_PLUGIN] Configuring server middleware");
1449
1849
  server.middlewares.use(serveMetadataAndScreenshots);
1450
- const cachedManifestPath = path7.join(
1850
+ const cachedManifestPath = path8.join(
1451
1851
  process.cwd(),
1452
1852
  ".storybook-cache",
1453
1853
  "manifest.json"
@@ -1466,7 +1866,7 @@ function storybookOnlookPlugin(options = {}) {
1466
1866
  server.watcher.on("change", scheduleRefresh);
1467
1867
  const storyGlobs = options.storyGlobs ?? ["**/*.stories.@(js|jsx|mjs|ts|tsx|mdx)"];
1468
1868
  const resolvedStoryGlobs = storyGlobs.map(
1469
- (glob) => path7.isAbsolute(glob) ? glob : path7.join(process.cwd(), glob)
1869
+ (glob) => path8.isAbsolute(glob) ? glob : path8.join(process.cwd(), glob)
1470
1870
  );
1471
1871
  if (resolvedStoryGlobs.length > 0) {
1472
1872
  server.watcher.add(resolvedStoryGlobs);
@@ -1474,10 +1874,36 @@ function storybookOnlookPlugin(options = {}) {
1474
1874
  storyGlobs: resolvedStoryGlobs
1475
1875
  });
1476
1876
  }
1877
+ const probeCandidatePorts = [
1878
+ ...new Set(
1879
+ [portFromArgv(process.argv), port, server.config.server.port].filter(
1880
+ (p) => typeof p === "number"
1881
+ )
1882
+ )
1883
+ ];
1884
+ console.log("[STORYBOOK_PLUGIN] Story add/unlink index broadcast enabled", {
1885
+ probeCandidatePorts
1886
+ });
1887
+ const indexBroadcaster = createStoryIndexBroadcaster({
1888
+ ports: probeCandidatePorts,
1889
+ notify: notifyOnbookIndexChanged
1890
+ });
1891
+ let indexBroadcastWatcherReady = false;
1892
+ server.watcher.once("ready", () => {
1893
+ indexBroadcastWatcherReady = true;
1894
+ });
1895
+ server.watcher.on("add", (file) => {
1896
+ if (!indexBroadcastWatcherReady) return;
1897
+ indexBroadcaster.onStoryFileAdded(file);
1898
+ });
1899
+ server.watcher.on("unlink", (file) => {
1900
+ if (!indexBroadcastWatcherReady) return;
1901
+ indexBroadcaster.onStoryFileRemoved(file);
1902
+ });
1477
1903
  const tailwindEntryPath = (() => {
1478
1904
  if (options.tailwindEntryCss === false) return null;
1479
1905
  if (typeof options.tailwindEntryCss === "string") {
1480
- return path7.isAbsolute(options.tailwindEntryCss) ? options.tailwindEntryCss : path7.join(process.cwd(), options.tailwindEntryCss);
1906
+ return path8.isAbsolute(options.tailwindEntryCss) ? options.tailwindEntryCss : path8.join(process.cwd(), options.tailwindEntryCss);
1481
1907
  }
1482
1908
  return detectTailwindEntryCss(process.cwd());
1483
1909
  })();
@@ -1493,7 +1919,7 @@ function storybookOnlookPlugin(options = {}) {
1493
1919
  });
1494
1920
  server.watcher.on("add", (file) => {
1495
1921
  if (!/\.tsx?$/.test(file)) return;
1496
- const dir = path7.dirname(file);
1922
+ const dir = path8.dirname(file);
1497
1923
  const isNewDir = !seenDirs.has(dir);
1498
1924
  seenDirs.add(dir);
1499
1925
  if (!watcherReady || !isNewDir) return;
@@ -1524,15 +1950,17 @@ function storybookOnlookPlugin(options = {}) {
1524
1950
  configurePreviewServer(server) {
1525
1951
  console.log("[STORYBOOK_PLUGIN] Configuring preview server middleware");
1526
1952
  server.middlewares.use(serveMetadataAndScreenshots);
1527
- refreshManifest(path7.join(process.cwd(), ".storybook-cache", "manifest.json"));
1953
+ refreshManifest(path8.join(process.cwd(), ".storybook-cache", "manifest.json"));
1528
1954
  },
1529
1955
  handleHotUpdate: handleStoryFileChange
1530
1956
  };
1531
1957
  const envContainment = envContainmentPlugin(options.envContainment);
1958
+ const packageMock = packageMockPlugin(options.packageMock);
1532
1959
  const moduleLoadCatchAll = options.moduleLoadCatchAll === false ? null : moduleLoadCatchAllPlugin();
1533
1960
  return withDedupeApply(
1534
1961
  [
1535
1962
  ...envContainment ? [envContainment] : [],
1963
+ ...packageMock ? [packageMock] : [],
1536
1964
  ...moduleLoadCatchAll ? [moduleLoadCatchAll] : [],
1537
1965
  componentLocPlugin(),
1538
1966
  mainPlugin,