@crvy/strybk 0.0.5 → 0.0.7

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/CHANGELOG.md CHANGED
@@ -5,6 +5,51 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.0.7] - 2026-08-26
9
+
10
+ ### Added
11
+
12
+ - Nest one describe per story title segment
13
+
14
+ ### Documentation
15
+
16
+ - Add nested describe titles design spec
17
+ - Add nested describe titles implementation plan
18
+ - Note nested describes and --grep caveat
19
+
20
+ ### Fixed
21
+
22
+ - Escape backslashes in generated titles and story names
23
+
24
+ ### Miscellaneous
25
+
26
+ - Upgrade bun to 1.4.0
27
+ ## [0.0.6] - 2026-08-24
28
+
29
+ ### Added
30
+
31
+ - Add creevey regex deserialization helpers
32
+ - Add creevey skip-option matching
33
+ - Resolve creevey story params from extracted stories
34
+ - Extract merged storybook params at runtime
35
+ - Render captureElement/skip-aware generated tests
36
+ - Add creevey fixture and worker story cache
37
+
38
+ ### Changed
39
+
40
+ - Drop generate-time creevey source-parsing
41
+ - Unexport shouldSkipByOption helper
42
+
43
+ ### Documentation
44
+
45
+ - Add captureElement + runtime creevey resolution design spec
46
+ - Add captureElement runtime-resolution implementation plan
47
+ - Document creevey capture/skip/ignoreElements and Playwright prerequisite
48
+ - Add migration note and clarify skip in-dimension matching
49
+
50
+ ### Testing
51
+
52
+ - Cover _stories and creevey fixture wiring
8
53
  ## [0.0.5] - 2026-07-06
9
54
 
10
55
  ### Added
package/README.md CHANGED
@@ -36,6 +36,37 @@ export default defineConfig({
36
36
  });
37
37
  ```
38
38
 
39
+ ## Generated tests
40
+
41
+ Each generated spec renders a Playwright test per story. At runtime, `parameters.creevey` (read from the running Storybook, merged across global / kind / story levels) drives capture and skip behavior — no Storybook addon required:
42
+
43
+ Generated suites nest one `test.describe` per title segment — `Components/Button` becomes `describe('Components') > describe('Button')` — so the Playwright HTML reporter shows a collapsible tree. Snapshot filenames are unaffected. Note that `--grep` patterns containing `/` no longer match (Playwright greps the space-joined title path); grep by a single segment instead, e.g. `--grep CommentLine`.
44
+
45
+ - `captureElement: '<selector>'` — captures `page.locator('<selector>')`.
46
+ - `captureElement: null` (or unset) — captures the viewport.
47
+ - `skip: { '<reason>': { in, kinds, stories } }` — marks the test skipped with `<reason>`. Note: `in` matches the **Playwright project name** (not the browser engine), so with the default project name `chromium`, a rule like `{ in: 'chrome' }` won't match — name your Playwright projects to line up with your `in:` rules, or scope rules via `kinds`/`stories`.
48
+ - `ignoreElements: '<selector>' | ['<selector>']` — masks those elements via `toHaveScreenshot({ mask })`.
49
+
50
+ Example:
51
+
52
+ ```ts
53
+ // stories/MyModal.stories.tsx
54
+ export default {
55
+ title: "MyModal",
56
+ parameters: { creevey: { captureElement: "#storybook-root" } },
57
+ };
58
+
59
+ export const Default = {
60
+ parameters: { creevey: { ignoreElements: ".timestamp" } },
61
+ };
62
+ ```
63
+
64
+ **Prerequisites for running specs:** `@playwright/test` (peer dependency) and browser binaries (`npx playwright install`). The `crvy-strybk generate` command itself needs neither — only a running Storybook to fetch `index.json`.
65
+
66
+ ## Upgrading from 0.0.x
67
+
68
+ `metadataExtractors` has been removed. `skip` and `captureElement` now resolve automatically at runtime — remove any `metadataExtractors: ["creevey"]` line from your `strybk.config.ts` and re-run `crvy-strybk generate`. Skipped stories now appear as `skipped` in Playwright reports rather than being omitted from the spec file.
69
+
39
70
  ## Changelog
40
71
 
41
72
  Preview the next changelog entry:
@@ -9,6 +9,5 @@ export interface StrybkConfig {
9
9
  }) => string;
10
10
  generatedRegionName?: string;
11
11
  deleteOrphans?: boolean;
12
- metadataExtractors?: "creevey"[];
13
12
  }
14
13
  export declare function defineConfig(config: StrybkConfig): StrybkConfig;
@@ -2,7 +2,6 @@ export function defineConfig(config) {
2
2
  return {
3
3
  generatedRegionName: "auto-screenshots",
4
4
  deleteOrphans: true,
5
- metadataExtractors: [],
6
5
  ...config,
7
6
  };
8
7
  }
@@ -1,6 +1,4 @@
1
- import { readFileSync } from "node:fs";
2
1
  import { discoverStoryFiles } from "./discover.js";
3
- import { extractCreeveyMetadata, FILE_POLICY_KEY } from "./metadata.js";
4
2
  import { renderScreenshotSpec } from "./render.js";
5
3
  const normalizePath = (value) => value.replace(/\\/gu, "/").replace(/^\.\//u, "");
6
4
  const matchesByImportPath = (filePath, importPath) => {
@@ -13,40 +11,20 @@ const resolveStoryTitle = (storyFile, indexEntries) => {
13
11
  return pathMatch?.title ?? null;
14
12
  };
15
13
  const escapeForRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
16
- const toStoryIdSegment = (value) => value
17
- .replace(/([a-z0-9])([A-Z])/gu, "$1-$2")
18
- .replace(/[^a-zA-Z0-9]+/gu, "-")
19
- .replace(/^-+|-+$/gu, "")
20
- .toLowerCase();
21
- const isStorySkipped = (story, storyMetadata) => {
22
- if (story.exportName !== undefined) {
23
- return storyMetadata[story.exportName]?.skip === true;
24
- }
25
- const storyIdSegment = story.id.split("--").slice(1).join("--");
26
- return Object.entries(storyMetadata).some(([exportName, policy]) => policy.skip === true && toStoryIdSegment(exportName) === storyIdSegment);
27
- };
28
14
  export async function generateScreenshots(args) {
29
15
  const storyFiles = await discoverStoryFiles(args.config.storyGlobs, args.configDir === undefined ? {} : { cwd: args.configDir });
30
16
  const generatedRegionName = args.config.generatedRegionName ?? "auto-screenshots";
31
17
  const manualRegionPattern = new RegExp(`// @generated-end ${escapeForRegExp(generatedRegionName)}\\s*([\\s\\S]*)$`, "u");
32
- const shouldExtractCreeveyMetadata = args.config.metadataExtractors?.includes("creevey") ?? false;
33
18
  return storyFiles.flatMap((storyFile) => {
34
19
  const title = resolveStoryTitle(storyFile, args.indexEntries);
35
20
  if (title === null) {
36
21
  return [];
37
22
  }
38
23
  const stories = args.indexEntries.filter((entry) => entry.title === title);
39
- const storyMetadata = shouldExtractCreeveyMetadata
40
- ? extractCreeveyMetadata(readFileSync(storyFile.filePath, "utf8"))
41
- : {};
42
- const isFileSkipped = storyMetadata[FILE_POLICY_KEY]?.skip === true;
43
- const filteredStories = isFileSkipped
44
- ? []
45
- : stories.filter((story) => !isStorySkipped(story, storyMetadata));
46
24
  const outputPath = args.config.resolveSpecPath({ storyFilePath: storyFile.filePath });
47
25
  const existing = args.readExistingFile?.(outputPath) ?? null;
48
26
  const manualRegion = existing?.match(manualRegionPattern)?.[1]?.trim() ?? "";
49
- if (filteredStories.length === 0 && manualRegion.length === 0) {
27
+ if (stories.length === 0 && manualRegion.length === 0) {
50
28
  return [];
51
29
  }
52
30
  return [
@@ -55,7 +33,7 @@ export async function generateScreenshots(args) {
55
33
  content: renderScreenshotSpec({
56
34
  config: args.config,
57
35
  title,
58
- stories: filteredStories,
36
+ stories,
59
37
  manualRegion,
60
38
  }),
61
39
  },
@@ -1,8 +1,38 @@
1
- const escapeSingleQuotes = (value) => value.replace(/'/gu, "\\'");
1
+ const escapeSingleQuotes = (value) => value.replace(/\\/gu, "\\\\").replace(/'/gu, "\\'");
2
+ const splitTitleSegments = (title) => title
3
+ .split("/")
4
+ .map((segment) => segment.trim())
5
+ .filter((segment) => segment.length > 0);
6
+ const renderTest = (story, depth) => {
7
+ const indent = " ".repeat(depth);
8
+ return [
9
+ `${indent}test('${escapeSingleQuotes(story.name)}', async ({ sharedPage, creevey }) => {`,
10
+ `${indent} const { skip, reason, captureElement, ignoreElements } = creevey.params('${story.id}');`,
11
+ `${indent} test.skip(skip, reason);`,
12
+ `${indent} await switchStory(sharedPage, '${story.id}');`,
13
+ `${indent} const target = captureElement ? sharedPage.locator(captureElement) : sharedPage;`,
14
+ `${indent} await expect(target).toHaveScreenshot({`,
15
+ `${indent} mask: ignoreElements.map((selector) => sharedPage.locator(selector)),`,
16
+ `${indent} });`,
17
+ `${indent}});`,
18
+ ].join("\n");
19
+ };
20
+ const wrapInDescribes = (segments, tests, depth) => {
21
+ const indent = " ".repeat(depth);
22
+ const [segment, ...rest] = segments;
23
+ if (segment === undefined) {
24
+ return tests;
25
+ }
26
+ return [
27
+ `${indent}test.describe('${escapeSingleQuotes(segment)}', () => {`,
28
+ wrapInDescribes(rest, tests, depth + 1),
29
+ `${indent}});`,
30
+ ].join("\n");
31
+ };
2
32
  export function renderScreenshotSpec(args) {
3
33
  const generatedRegionName = args.config.generatedRegionName ?? "auto-screenshots";
4
- const tests = args.stories
5
- .map((story) => ` test('${escapeSingleQuotes(story.name)}', async ({ sharedPage }) => {\n await switchStory(sharedPage, '${story.id}');\n await expect(sharedPage).toHaveScreenshot();\n });`)
6
- .join("\n\n");
7
- return `import { test, expect, switchStory } from '@crvy/strybk';\n\n// @generated-begin ${generatedRegionName}\ntest.describe('${escapeSingleQuotes(args.title)}', () => {\n${tests}\n});\n// @generated-end ${generatedRegionName}\n\n${args.manualRegion}`;
34
+ const segments = splitTitleSegments(args.title);
35
+ const tests = args.stories.map((story) => renderTest(story, segments.length)).join("\n\n");
36
+ const body = wrapInDescribes(segments, tests, 0);
37
+ return `import { test, expect, switchStory } from '@crvy/strybk';\n\n// @generated-begin ${generatedRegionName}\n${body}\n// @generated-end ${generatedRegionName}\n\n${args.manualRegion}`;
8
38
  }
@@ -1,6 +1,8 @@
1
1
  import type { Page, PlaywrightTestArgs, PlaywrightTestOptions, PlaywrightWorkerArgs, PlaywrightWorkerOptions, TestType } from "@playwright/test";
2
+ import type { CreeveyApi } from "../storybook/creeveyParams.js";
2
3
  type StrybkFixtures = {
3
4
  sharedPage: Page;
5
+ creevey: CreeveyApi;
4
6
  };
5
7
  type PublicStrybkTest = TestType<PlaywrightTestArgs & PlaywrightTestOptions & StrybkFixtures, PlaywrightWorkerArgs & PlaywrightWorkerOptions>;
6
8
  type StrybkFixtureHandles = {
@@ -1,4 +1,6 @@
1
+ import { resolveCreeveyStory } from "../storybook/creeveyParams.js";
1
2
  import { createChannelDriver } from "../storybook/channelDriver.js";
3
+ import { extractStories } from "../storybook/extract.js";
2
4
  import { loadPlaywrightTestRuntime } from "./runtime.js";
3
5
  const animationDisablerStyles = [
4
6
  "*, *::before, *::after {",
@@ -67,6 +69,13 @@ export const createStrybkFixtures = () => {
67
69
  },
68
70
  { scope: "worker" },
69
71
  ],
72
+ _stories: [
73
+ async ({ _workerPage }, use) => {
74
+ const stories = await extractStories(_workerPage);
75
+ await use(stories);
76
+ },
77
+ { scope: "worker" },
78
+ ],
70
79
  sharedPage: async ({ _workerPage }, use, testInfo) => {
71
80
  const storybookGlobals = getStorybookGlobals(testInfo);
72
81
  if (storybookGlobals !== undefined) {
@@ -76,6 +85,13 @@ export const createStrybkFixtures = () => {
76
85
  await use(_workerPage);
77
86
  await restoreSharedPageBaseline(_workerPage, testInfo.project.use.baseURL);
78
87
  },
88
+ creevey: async ({ _stories }, use, testInfo) => {
89
+ const browser = testInfo.project.name;
90
+ const api = {
91
+ params: (storyId) => resolveCreeveyStory(_stories, storyId, browser),
92
+ };
93
+ await use(api);
94
+ },
79
95
  });
80
96
  return {
81
97
  expect,
@@ -1,5 +1,6 @@
1
1
  export { switchStory } from "./switchStory.js";
2
2
  export declare const test: import("playwright/test").TestType<import("playwright/test").PlaywrightTestArgs & import("playwright/test").PlaywrightTestOptions & {
3
3
  sharedPage: import("playwright-core").Page;
4
+ creevey: import("../storybook/creeveyParams.js").CreeveyApi;
4
5
  }, import("playwright/test").PlaywrightWorkerArgs & import("playwright/test").PlaywrightWorkerOptions>;
5
6
  export declare const expect: import("playwright/test").Expect<{}>;
@@ -0,0 +1,45 @@
1
+ export interface SerializedRegExp {
2
+ __regexp: true;
3
+ source: string;
4
+ flags: string;
5
+ }
6
+ export declare const isSerializedRegExp: (value: unknown) => value is SerializedRegExp;
7
+ export declare const deserializeRegExp: ({ source, flags }: SerializedRegExp) => RegExp;
8
+ export interface SkipOption {
9
+ in?: string | string[] | RegExp | SerializedRegExp;
10
+ kinds?: string | string[] | RegExp | SerializedRegExp;
11
+ stories?: string | string[] | RegExp | SerializedRegExp;
12
+ }
13
+ export type SkipOptions = boolean | string | Record<string, SkipOption | SkipOption[]>;
14
+ export declare const shouldSkip: (browser: string, meta: {
15
+ title: string;
16
+ name: string;
17
+ }, skipOptions: SkipOptions) => boolean | string;
18
+ export interface CreeveyStoryParams {
19
+ captureElement?: string | null;
20
+ ignoreElements?: string | string[] | null;
21
+ skip?: SkipOptions;
22
+ }
23
+ export interface NormalizedCreeveyParams {
24
+ skip: boolean;
25
+ reason?: string;
26
+ captureElement: string | null;
27
+ ignoreElements: string[];
28
+ }
29
+ export interface StoriesRaw {
30
+ [storyId: string]: {
31
+ title: string;
32
+ name: string;
33
+ parameters?: {
34
+ creevey?: CreeveyStoryParams;
35
+ };
36
+ };
37
+ }
38
+ export interface CreeveyApi {
39
+ params(storyId: string): NormalizedCreeveyParams;
40
+ }
41
+ export declare const normalizeCreeveyParams: (raw: CreeveyStoryParams | undefined, browser: string, meta: {
42
+ title: string;
43
+ name: string;
44
+ }) => NormalizedCreeveyParams;
45
+ export declare const resolveCreeveyStory: (stories: StoriesRaw, storyId: string, browser: string) => NormalizedCreeveyParams;
@@ -0,0 +1,63 @@
1
+ export const isSerializedRegExp = (value) => typeof value === "object" && value !== null && Reflect.get(value, "__regexp") === true;
2
+ export const deserializeRegExp = ({ source, flags }) => new RegExp(source, flags);
3
+ const matchBy = (pattern, value) => (typeof pattern === "string" && pattern === value) ||
4
+ (Array.isArray(pattern) && pattern.includes(value)) ||
5
+ (pattern instanceof RegExp && pattern.test(value)) ||
6
+ (isSerializedRegExp(pattern) && deserializeRegExp(pattern).test(value)) ||
7
+ pattern === undefined;
8
+ const shouldSkipByOption = (browser, meta, skipOption, reason) => {
9
+ if (Array.isArray(skipOption)) {
10
+ for (const option of skipOption) {
11
+ const result = shouldSkipByOption(browser, meta, option, reason);
12
+ if (result !== false) {
13
+ return result;
14
+ }
15
+ }
16
+ return false;
17
+ }
18
+ const { in: browsers, kinds, stories } = skipOption;
19
+ const skipByBrowser = matchBy(browsers, browser);
20
+ const skipByKind = matchBy(kinds, meta.title);
21
+ const skipByStory = matchBy(stories, meta.name);
22
+ return skipByBrowser && skipByKind && skipByStory && reason;
23
+ };
24
+ export const shouldSkip = (browser, meta, skipOptions) => {
25
+ if (typeof skipOptions !== "object") {
26
+ return skipOptions;
27
+ }
28
+ for (const reason of Object.keys(skipOptions)) {
29
+ const result = shouldSkipByOption(browser, meta, skipOptions[reason], reason);
30
+ if (result !== false) {
31
+ return result;
32
+ }
33
+ }
34
+ return false;
35
+ };
36
+ const toArray = (value) => {
37
+ if (value === null || value === undefined) {
38
+ return [];
39
+ }
40
+ return Array.isArray(value) ? value : [value];
41
+ };
42
+ export const normalizeCreeveyParams = (raw, browser, meta) => {
43
+ if (raw === undefined) {
44
+ return { skip: false, captureElement: null, ignoreElements: [] };
45
+ }
46
+ const skipResult = raw.skip === undefined ? false : shouldSkip(browser, meta, raw.skip);
47
+ return {
48
+ skip: skipResult !== false,
49
+ reason: typeof skipResult === "string" ? skipResult : undefined,
50
+ captureElement: raw.captureElement === undefined ? null : raw.captureElement,
51
+ ignoreElements: toArray(raw.ignoreElements),
52
+ };
53
+ };
54
+ export const resolveCreeveyStory = (stories, storyId, browser) => {
55
+ const story = stories[storyId];
56
+ if (story === undefined) {
57
+ throw new Error(`Story '${storyId}' not found in extracted Storybook stories`);
58
+ }
59
+ return normalizeCreeveyParams(story.parameters?.creevey, browser, {
60
+ title: story.title,
61
+ name: story.name,
62
+ });
63
+ };
@@ -0,0 +1,4 @@
1
+ import type { Page } from "@playwright/test";
2
+ import type { StoriesRaw } from "./creeveyParams.js";
3
+ export declare const toStoriesRaw: (value: unknown) => StoriesRaw;
4
+ export declare const extractStories: (page: Page) => Promise<StoriesRaw>;
@@ -0,0 +1,11 @@
1
+ const isStoriesRaw = (value) => typeof value === "object" && value !== null;
2
+ export const toStoriesRaw = (value) => {
3
+ if (!isStoriesRaw(value)) {
4
+ throw new Error("Storybook preview not available; is Storybook fully loaded?");
5
+ }
6
+ return value;
7
+ };
8
+ export const extractStories = async (page) => toStoriesRaw(await page.evaluate(() => {
9
+ const preview = window.__STORYBOOK_PREVIEW__;
10
+ return preview?.extract?.();
11
+ }));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crvy/strybk",
3
- "version": "0.0.5",
3
+ "version": "0.0.7",
4
4
  "description": "Generator-first Playwright screenshot testing for Storybook.",
5
5
  "keywords": [
6
6
  "crvy",
@@ -72,7 +72,7 @@
72
72
  },
73
73
  "devDependencies": {
74
74
  "@playwright/test": "1.59.1",
75
- "@types/bun": "^1.3.7",
75
+ "@types/bun": "^1.4.0",
76
76
  "@types/node": "25.9.0",
77
77
  "git-cliff": "2.13.1",
78
78
  "jscpd": "^4.0.8",
@@ -88,8 +88,8 @@
88
88
  "@playwright/test": ">=1.59.0"
89
89
  },
90
90
  "engines": {
91
- "bun": ">=1.3.0",
91
+ "bun": ">=1.4.0",
92
92
  "node": ">=22"
93
93
  },
94
- "packageManager": "bun@1.3.13"
94
+ "packageManager": "bun@1.4.0"
95
95
  }
@@ -1,5 +0,0 @@
1
- export interface StoryPolicy {
2
- skip?: boolean;
3
- }
4
- export declare const FILE_POLICY_KEY = "__file__";
5
- export declare function extractCreeveyMetadata(source: string): Record<string, StoryPolicy>;
@@ -1,124 +0,0 @@
1
- export const FILE_POLICY_KEY = "__file__";
2
- const skipWhitespace = (source, startIndex) => {
3
- let index = startIndex;
4
- while (/\s/u.test(source[index] ?? "")) {
5
- index += 1;
6
- }
7
- return index;
8
- };
9
- const extractObjectLiteral = (source, startIndex) => {
10
- const objectStartIndex = skipWhitespace(source, startIndex);
11
- if (source[objectStartIndex] !== "{") {
12
- return null;
13
- }
14
- let depth = 0;
15
- let inSingleQuote = false;
16
- let inDoubleQuote = false;
17
- let inTemplateString = false;
18
- let inLineComment = false;
19
- let inBlockComment = false;
20
- for (let index = objectStartIndex; index < source.length; index += 1) {
21
- const char = source[index] ?? "";
22
- const nextChar = source[index + 1] ?? "";
23
- const previousChar = source[index - 1] ?? "";
24
- if (inLineComment) {
25
- if (char === "\n") {
26
- inLineComment = false;
27
- }
28
- continue;
29
- }
30
- if (inBlockComment) {
31
- if (previousChar === "*" && char === "/") {
32
- inBlockComment = false;
33
- }
34
- continue;
35
- }
36
- if (inSingleQuote) {
37
- if (char === "'" && previousChar !== "\\") {
38
- inSingleQuote = false;
39
- }
40
- continue;
41
- }
42
- if (inDoubleQuote) {
43
- if (char === '"' && previousChar !== "\\") {
44
- inDoubleQuote = false;
45
- }
46
- continue;
47
- }
48
- if (inTemplateString) {
49
- if (char === "`" && previousChar !== "\\") {
50
- inTemplateString = false;
51
- }
52
- continue;
53
- }
54
- if (char === "/" && nextChar === "/") {
55
- inLineComment = true;
56
- index += 1;
57
- continue;
58
- }
59
- if (char === "/" && nextChar === "*") {
60
- inBlockComment = true;
61
- index += 1;
62
- continue;
63
- }
64
- if (char === "'") {
65
- inSingleQuote = true;
66
- continue;
67
- }
68
- if (char === '"') {
69
- inDoubleQuote = true;
70
- continue;
71
- }
72
- if (char === "`") {
73
- inTemplateString = true;
74
- continue;
75
- }
76
- if (char === "{") {
77
- depth += 1;
78
- continue;
79
- }
80
- if (char === "}") {
81
- depth -= 1;
82
- if (depth === 0) {
83
- return source.slice(objectStartIndex, index + 1);
84
- }
85
- }
86
- }
87
- return null;
88
- };
89
- const extractSkipPolicy = (source, scope) => {
90
- if (source === null) {
91
- return undefined;
92
- }
93
- const pattern = scope === "parameters"
94
- ? /\bcreevey\s*:\s*\{[\s\S]*?\bskip\s*:\s*(true|false)\b/u
95
- : /\bparameters\s*:\s*\{[\s\S]*?\bcreevey\s*:\s*\{[\s\S]*?\bskip\s*:\s*(true|false)\b/u;
96
- const skipMatch = source.match(pattern);
97
- return skipMatch ? { skip: skipMatch[1] === "true" } : undefined;
98
- };
99
- const collectPolicies = (source, pattern, scope) => Array.from(source.matchAll(pattern)).flatMap((match) => {
100
- const index = match.index ?? 0;
101
- const policy = extractSkipPolicy(extractObjectLiteral(source, index + match[0].length), scope);
102
- return policy ? [[index, match[1], policy]] : [];
103
- });
104
- export function extractCreeveyMetadata(source) {
105
- const storyPolicies = [
106
- ...collectPolicies(source, /(\w+)\.parameters\s*=/gu, "parameters"),
107
- ...collectPolicies(source, /export\s+const\s+(\w+)(?:\s*:\s*[^=]+)?\s*=/gu, "object"),
108
- ];
109
- const constPolicies = new Map(collectPolicies(source, /const\s+(\w+)(?:\s*:\s*[^=]+)?\s*=/gu, "object").map(([, name, policy]) => [name, policy]));
110
- const filePolicies = [
111
- ...Array.from(source.matchAll(/export\s+default\b/gu)).flatMap((match) => {
112
- const index = match.index ?? 0;
113
- const policy = extractSkipPolicy(extractObjectLiteral(source, index + match[0].length), "object");
114
- return policy ? [[index, FILE_POLICY_KEY, policy]] : [];
115
- }),
116
- ...Array.from(source.matchAll(/export\s+default\s+(\w+)\s*;/gu)).flatMap((match) => {
117
- const policy = constPolicies.get(match[1]);
118
- return policy ? [[match.index ?? 0, FILE_POLICY_KEY, policy]] : [];
119
- }),
120
- ];
121
- return Object.fromEntries([...storyPolicies, ...filePolicies]
122
- .sort((left, right) => left[0] - right[0])
123
- .map(([, name, policy]) => [name, policy]));
124
- }