@crvy/strybk 0.0.4 → 0.0.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/CHANGELOG.md +31 -0
- package/README.md +29 -0
- package/dist/src/cli.d.ts +3 -0
- package/dist/src/cli.js +22 -0
- package/dist/src/config.d.ts +0 -1
- package/dist/src/config.js +0 -1
- package/dist/src/generate/index.js +2 -24
- package/dist/src/generate/render.js +12 -3
- package/dist/src/playwright/fixtures.d.ts +2 -0
- package/dist/src/playwright/fixtures.js +16 -0
- package/dist/src/playwright/index.d.ts +1 -0
- package/dist/src/storybook/creeveyParams.d.ts +45 -0
- package/dist/src/storybook/creeveyParams.js +63 -0
- package/dist/src/storybook/extract.d.ts +4 -0
- package/dist/src/storybook/extract.js +11 -0
- package/package.json +1 -1
- package/dist/src/generate/metadata.d.ts +0 -5
- package/dist/src/generate/metadata.js +0 -124
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,37 @@ 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.6] - 2026-08-24
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- Add creevey regex deserialization helpers
|
|
13
|
+
- Add creevey skip-option matching
|
|
14
|
+
- Resolve creevey story params from extracted stories
|
|
15
|
+
- Extract merged storybook params at runtime
|
|
16
|
+
- Render captureElement/skip-aware generated tests
|
|
17
|
+
- Add creevey fixture and worker story cache
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
|
|
21
|
+
- Drop generate-time creevey source-parsing
|
|
22
|
+
- Unexport shouldSkipByOption helper
|
|
23
|
+
|
|
24
|
+
### Documentation
|
|
25
|
+
|
|
26
|
+
- Add captureElement + runtime creevey resolution design spec
|
|
27
|
+
- Add captureElement runtime-resolution implementation plan
|
|
28
|
+
- Document creevey capture/skip/ignoreElements and Playwright prerequisite
|
|
29
|
+
- Add migration note and clarify skip in-dimension matching
|
|
30
|
+
|
|
31
|
+
### Testing
|
|
32
|
+
|
|
33
|
+
- Cover _stories and creevey fixture wiring
|
|
34
|
+
## [0.0.5] - 2026-07-06
|
|
35
|
+
|
|
36
|
+
### Added
|
|
37
|
+
|
|
38
|
+
- **cli:** Add --help/-h flag to print usage and exit 0
|
|
8
39
|
## [0.0.4] - 2026-07-03
|
|
9
40
|
|
|
10
41
|
### Other
|
package/README.md
CHANGED
|
@@ -36,6 +36,35 @@ 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
|
+
- `captureElement: '<selector>'` — captures `page.locator('<selector>')`.
|
|
44
|
+
- `captureElement: null` (or unset) — captures the viewport.
|
|
45
|
+
- `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`.
|
|
46
|
+
- `ignoreElements: '<selector>' | ['<selector>']` — masks those elements via `toHaveScreenshot({ mask })`.
|
|
47
|
+
|
|
48
|
+
Example:
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
// stories/MyModal.stories.tsx
|
|
52
|
+
export default {
|
|
53
|
+
title: "MyModal",
|
|
54
|
+
parameters: { creevey: { captureElement: "#storybook-root" } },
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export const Default = {
|
|
58
|
+
parameters: { creevey: { ignoreElements: ".timestamp" } },
|
|
59
|
+
};
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
**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`.
|
|
63
|
+
|
|
64
|
+
## Upgrading from 0.0.x
|
|
65
|
+
|
|
66
|
+
`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.
|
|
67
|
+
|
|
39
68
|
## Changelog
|
|
40
69
|
|
|
41
70
|
Preview the next changelog entry:
|
package/dist/src/cli.d.ts
CHANGED
|
@@ -4,6 +4,9 @@ export interface GenerateCliArgs {
|
|
|
4
4
|
configPath: string;
|
|
5
5
|
dryRun: boolean;
|
|
6
6
|
}
|
|
7
|
+
export declare const HELP_TEXT = "Usage: crvy-strybk generate --config <path> [options]\n\nGenerate Playwright screenshot spec files from a running Storybook index.\n\nThe Storybook server must be running at the configured `storybookUrl` so that\nindex.json can be fetched at generate time.\n\nOptions:\n --config <path> Path to strybk.config.ts (required)\n --dry-run Compute outputs without writing files\n -h, --help Show this help message\n";
|
|
8
|
+
export declare function printHelp(): void;
|
|
9
|
+
export declare function wantsHelp(argv: string[]): boolean;
|
|
7
10
|
export declare function parseCliArgs(argv: string[]): GenerateCliArgs;
|
|
8
11
|
export declare function runCli(argv: string[]): Promise<Array<{
|
|
9
12
|
outputPath: string;
|
package/dist/src/cli.js
CHANGED
|
@@ -99,6 +99,24 @@ const writeGeneratedFiles = (outputs) => {
|
|
|
99
99
|
writeFileSync(output.outputPath, output.content);
|
|
100
100
|
}
|
|
101
101
|
};
|
|
102
|
+
export const HELP_TEXT = `Usage: crvy-strybk generate --config <path> [options]
|
|
103
|
+
|
|
104
|
+
Generate Playwright screenshot spec files from a running Storybook index.
|
|
105
|
+
|
|
106
|
+
The Storybook server must be running at the configured \`storybookUrl\` so that
|
|
107
|
+
index.json can be fetched at generate time.
|
|
108
|
+
|
|
109
|
+
Options:
|
|
110
|
+
--config <path> Path to strybk.config.ts (required)
|
|
111
|
+
--dry-run Compute outputs without writing files
|
|
112
|
+
-h, --help Show this help message
|
|
113
|
+
`;
|
|
114
|
+
export function printHelp() {
|
|
115
|
+
console.log(HELP_TEXT);
|
|
116
|
+
}
|
|
117
|
+
export function wantsHelp(argv) {
|
|
118
|
+
return argv.includes("--help") || argv.includes("-h");
|
|
119
|
+
}
|
|
102
120
|
export function parseCliArgs(argv) {
|
|
103
121
|
const [command, ...options] = argv;
|
|
104
122
|
if (command !== "generate") {
|
|
@@ -155,6 +173,10 @@ export async function runCli(argv) {
|
|
|
155
173
|
return outputs;
|
|
156
174
|
}
|
|
157
175
|
export async function main(argv = process.argv.slice(2)) {
|
|
176
|
+
if (wantsHelp(argv)) {
|
|
177
|
+
printHelp();
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
158
180
|
try {
|
|
159
181
|
const outputs = await runCli(argv);
|
|
160
182
|
const dryRun = argv.includes("--dry-run");
|
package/dist/src/config.d.ts
CHANGED
package/dist/src/config.js
CHANGED
|
@@ -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 (
|
|
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
|
|
36
|
+
stories,
|
|
59
37
|
manualRegion,
|
|
60
38
|
}),
|
|
61
39
|
},
|
|
@@ -1,8 +1,17 @@
|
|
|
1
1
|
const escapeSingleQuotes = (value) => value.replace(/'/gu, "\\'");
|
|
2
|
+
const renderTest = (story) => [
|
|
3
|
+
` test('${escapeSingleQuotes(story.name)}', async ({ sharedPage, creevey }) => {`,
|
|
4
|
+
` const { skip, reason, captureElement, ignoreElements } = creevey.params('${story.id}');`,
|
|
5
|
+
` test.skip(skip, reason);`,
|
|
6
|
+
` await switchStory(sharedPage, '${story.id}');`,
|
|
7
|
+
` const target = captureElement ? sharedPage.locator(captureElement) : sharedPage;`,
|
|
8
|
+
` await expect(target).toHaveScreenshot({`,
|
|
9
|
+
` mask: ignoreElements.map((selector) => sharedPage.locator(selector)),`,
|
|
10
|
+
` });`,
|
|
11
|
+
` });`,
|
|
12
|
+
].join("\n");
|
|
2
13
|
export function renderScreenshotSpec(args) {
|
|
3
14
|
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");
|
|
15
|
+
const tests = args.stories.map(renderTest).join("\n\n");
|
|
7
16
|
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}`;
|
|
8
17
|
}
|
|
@@ -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,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,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
|
-
}
|