@crvy/strybk 0.0.1 → 0.0.3

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,4 +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.3] - 2026-06-18
9
+
10
+ ### Added
11
+
12
+ - Match stories by importPath and resolve globs via configDir
13
+ ## [0.0.2] - 2026-06-11
14
+ ## [0.0.1] - 2026-06-11
15
+
16
+ ### Added
17
+
18
+ - Add lean config and generator core
19
+ - Support creevey skip metadata extraction
20
+ - Add Storybook channel driver and switchStory helper
21
+ - Add shared-page Playwright fixtures
22
+ - Add strybk CLI
23
+ - Simplify generated spec imports to use @crvy/strybk directly
24
+
25
+ ### Documentation
26
+
27
+ - Correct Yarn 4 local link instructions
28
+ - Add minimal config example and improve fetch error message
29
+
30
+ ### Fixed
31
+
32
+ - Default generated region marker in renderer
33
+ - Harden creevey metadata filtering
34
+ - Support meta-level creevey skip
35
+ - Harden Storybook channel driver
36
+ - Keep switch timeout through font readiness
37
+ - Preserve sharedPage fixture typing
38
+ - Hide internal worker page fixture
39
+ - Preserve built-in playwright fixture types
40
+ - Harden linked playwright runtime and generation
41
+
42
+ ### Miscellaneous
43
+
44
+ - Bootstrap strybk package
45
+ - Align strybk bootstrap with plan
46
+ - Fix strybk bin entry
47
+ - Make strybk bootstrap manifest honest
48
+ - Tighten strybk bootstrap packaging
49
+ - Ignore strybk build artifacts
50
+ - Sync strybk lockfile
51
+ - Build strybk before packing
52
+ - Sync current workspace
53
+ - Align publish setup with crvy-rprtr
54
+ - Bump version to 0.0.1 and update bin field format
8
55
  ## [Unreleased]
package/README.md CHANGED
@@ -25,17 +25,29 @@ bun run check
25
25
  Generate screenshot specs from a Storybook index:
26
26
 
27
27
  ```sh
28
- strybk generate --config ./strybk.config.ts
28
+ crvy-strybk generate --config ./strybk.config.ts
29
29
  ```
30
30
 
31
31
  Use `--dry-run` to compute outputs without writing files:
32
32
 
33
33
  ```sh
34
- strybk generate --config ./strybk.config.ts --dry-run
34
+ crvy-strybk generate --config ./strybk.config.ts --dry-run
35
35
  ```
36
36
 
37
37
  The config module should export a `StrybkConfig` object, typically as the default export from `defineConfig(...)`.
38
38
 
39
+ Minimal `strybk.config.ts`:
40
+
41
+ ```ts
42
+ import { defineConfig } from "@crvy/strybk";
43
+
44
+ export default defineConfig({
45
+ storybookUrl: "http://localhost:6006",
46
+ storyGlobs: ["src/**/*.stories.tsx"],
47
+ resolveSpecPath: ({ storyFilePath }) => storyFilePath.replace(/\.stories\.tsx$/, ".spec.ts"),
48
+ });
49
+ ```
50
+
39
51
  ## Changelog
40
52
 
41
53
  Preview the next changelog entry:
@@ -69,7 +81,7 @@ Link it into the consumer project:
69
81
 
70
82
  ```sh
71
83
  cd /path/to/consumer-project
72
- bun link strybk
84
+ bun link @crvy/strybk
73
85
  ```
74
86
 
75
87
  If you want to persist the link in the consumer's manifest, Bun supports a `link:` dependency entry:
@@ -77,7 +89,7 @@ If you want to persist the link in the consumer's manifest, Bun supports a `link
77
89
  ```json
78
90
  {
79
91
  "dependencies": {
80
- "strybk": "link:strybk"
92
+ "@crvy/strybk": "link:strybk"
81
93
  }
82
94
  }
83
95
  ```
package/dist/src/cli.js CHANGED
@@ -32,8 +32,7 @@ const resolveConfigExport = (moduleNamespace) => {
32
32
  const isStrybkConfig = (value) => isRecord(value) &&
33
33
  typeof value.storybookUrl === "string" &&
34
34
  Array.isArray(value.storyGlobs) &&
35
- typeof value.resolveSpecPath === "function" &&
36
- typeof value.resolveHarnessImports === "function";
35
+ typeof value.resolveSpecPath === "function";
37
36
  const getIndexEntriesRecord = (payload) => {
38
37
  if (!isRecord(payload)) {
39
38
  throw new Error("Storybook index response must be an object");
@@ -54,14 +53,15 @@ const toStoryIndexEntry = (value) => {
54
53
  }
55
54
  if (typeof value.id !== "string" ||
56
55
  typeof value.title !== "string" ||
57
- typeof value.name !== "string") {
56
+ typeof value.name !== "string" ||
57
+ typeof value.importPath !== "string") {
58
58
  return null;
59
59
  }
60
60
  return {
61
61
  id: value.id,
62
62
  title: value.title,
63
63
  name: value.name,
64
- importPath: typeof value.importPath === "string" ? value.importPath : undefined,
64
+ importPath: value.importPath,
65
65
  exportName: typeof value.exportName === "string" ? value.exportName : undefined,
66
66
  };
67
67
  };
@@ -77,9 +77,15 @@ const loadConfig = async (configPath) => {
77
77
  };
78
78
  const fetchStoryIndex = async (config) => {
79
79
  const indexUrl = resolveStorybookIndexUrl(config.storybookUrl);
80
- const response = await fetch(indexUrl);
80
+ let response;
81
+ try {
82
+ response = await fetch(indexUrl);
83
+ }
84
+ catch (error) {
85
+ throw new Error(`Failed to fetch Storybook index at ${indexUrl.toString()} — is Storybook running?`, { cause: error });
86
+ }
81
87
  if (!response.ok) {
82
- throw new Error(`Failed to fetch ${indexUrl.toString()}: ${response.status} ${response.statusText}`.trim());
88
+ throw new Error(`Failed to fetch Storybook index at ${indexUrl.toString()}: ${response.status} ${response.statusText}`.trim());
83
89
  }
84
90
  const payload = asUnknown(await response.json());
85
91
  const entries = getIndexEntriesRecord(payload);
@@ -139,6 +145,7 @@ export async function runCli(argv) {
139
145
  const indexEntries = await fetchStoryIndex(config);
140
146
  const outputs = await generateScreenshots({
141
147
  config,
148
+ configDir: dirname(resolve(cliArgs.configPath)),
142
149
  indexEntries,
143
150
  readExistingFile,
144
151
  });
@@ -7,12 +7,6 @@ export interface StrybkConfig {
7
7
  resolveSpecPath: (args: {
8
8
  storyFilePath: string;
9
9
  }) => string;
10
- resolveHarnessImports: (args: {
11
- outputPath: string;
12
- }) => {
13
- fixturesImport: string;
14
- switchStoryImport: string;
15
- };
16
10
  generatedRegionName?: string;
17
11
  deleteOrphans?: boolean;
18
12
  metadataExtractors?: "creevey"[];
@@ -1,5 +1,6 @@
1
1
  export interface StoryFile {
2
2
  filePath: string;
3
- title: string;
4
3
  }
5
- export declare function discoverStoryFiles(patterns: string[]): Promise<StoryFile[]>;
4
+ export declare function discoverStoryFiles(patterns: string[], options?: {
5
+ cwd?: string;
6
+ }): Promise<StoryFile[]>;
@@ -1,13 +1,6 @@
1
- import { readFileSync } from "node:fs";
2
1
  import { glob } from "glob";
3
- export async function discoverStoryFiles(patterns) {
4
- const files = await glob(patterns, { absolute: true });
5
- return files.flatMap((filePath) => {
6
- const content = readFileSync(filePath, "utf8");
7
- const titleMatch = content.match(/title:\s*['"]([^'"]+)['"]/u);
8
- if (!titleMatch) {
9
- return [];
10
- }
11
- return [{ filePath, title: titleMatch[1] }];
12
- });
2
+ export async function discoverStoryFiles(patterns, options = {}) {
3
+ const cwd = options.cwd ?? process.cwd();
4
+ const files = await glob(patterns, { absolute: true, cwd });
5
+ return files.map((filePath) => ({ filePath }));
13
6
  }
@@ -3,13 +3,14 @@ export interface StoryIndexEntry {
3
3
  id: string;
4
4
  title: string;
5
5
  name: string;
6
- importPath?: string;
6
+ importPath: string;
7
7
  exportName?: string;
8
8
  }
9
9
  export declare function generateScreenshots(args: {
10
10
  config: StrybkConfig;
11
11
  indexEntries: StoryIndexEntry[];
12
12
  readExistingFile?: (filePath: string) => string | null;
13
+ configDir?: string;
13
14
  }): Promise<Array<{
14
15
  outputPath: string;
15
16
  content: string;
@@ -2,6 +2,16 @@ import { readFileSync } from "node:fs";
2
2
  import { discoverStoryFiles } from "./discover.js";
3
3
  import { extractCreeveyMetadata, FILE_POLICY_KEY } from "./metadata.js";
4
4
  import { renderScreenshotSpec } from "./render.js";
5
+ const normalizePath = (value) => value.replace(/\\/gu, "/").replace(/^\.\//u, "");
6
+ const matchesByImportPath = (filePath, importPath) => {
7
+ const normalizedImport = normalizePath(importPath);
8
+ const normalizedFile = normalizePath(filePath);
9
+ return normalizedFile === normalizedImport || normalizedFile.endsWith(`/${normalizedImport}`);
10
+ };
11
+ const resolveStoryTitle = (storyFile, indexEntries) => {
12
+ const pathMatch = indexEntries.find((entry) => matchesByImportPath(storyFile.filePath, entry.importPath));
13
+ return pathMatch?.title ?? null;
14
+ };
5
15
  const escapeForRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
6
16
  const toStoryIdSegment = (value) => value
7
17
  .replace(/([a-z0-9])([A-Z])/gu, "$1-$2")
@@ -16,12 +26,16 @@ const isStorySkipped = (story, storyMetadata) => {
16
26
  return Object.entries(storyMetadata).some(([exportName, policy]) => policy.skip === true && toStoryIdSegment(exportName) === storyIdSegment);
17
27
  };
18
28
  export async function generateScreenshots(args) {
19
- const storyFiles = await discoverStoryFiles(args.config.storyGlobs);
29
+ const storyFiles = await discoverStoryFiles(args.config.storyGlobs, args.configDir === undefined ? {} : { cwd: args.configDir });
20
30
  const generatedRegionName = args.config.generatedRegionName ?? "auto-screenshots";
21
31
  const manualRegionPattern = new RegExp(`// @generated-end ${escapeForRegExp(generatedRegionName)}\\s*([\\s\\S]*)$`, "u");
22
32
  const shouldExtractCreeveyMetadata = args.config.metadataExtractors?.includes("creevey") ?? false;
23
33
  return storyFiles.flatMap((storyFile) => {
24
- const stories = args.indexEntries.filter((entry) => entry.title === storyFile.title);
34
+ const title = resolveStoryTitle(storyFile, args.indexEntries);
35
+ if (title === null) {
36
+ return [];
37
+ }
38
+ const stories = args.indexEntries.filter((entry) => entry.title === title);
25
39
  const storyMetadata = shouldExtractCreeveyMetadata
26
40
  ? extractCreeveyMetadata(readFileSync(storyFile.filePath, "utf8"))
27
41
  : {};
@@ -35,15 +49,12 @@ export async function generateScreenshots(args) {
35
49
  if (filteredStories.length === 0 && manualRegion.length === 0) {
36
50
  return [];
37
51
  }
38
- const harnessImports = args.config.resolveHarnessImports({ outputPath });
39
52
  return [
40
53
  {
41
54
  outputPath,
42
55
  content: renderScreenshotSpec({
43
56
  config: args.config,
44
- fixturesImport: harnessImports.fixturesImport,
45
- switchStoryImport: harnessImports.switchStoryImport,
46
- title: storyFile.title,
57
+ title,
47
58
  stories: filteredStories,
48
59
  manualRegion,
49
60
  }),
@@ -5,8 +5,6 @@ export interface RenderableStory {
5
5
  }
6
6
  export declare function renderScreenshotSpec(args: {
7
7
  config: StrybkConfig;
8
- fixturesImport: string;
9
- switchStoryImport: string;
10
8
  title: string;
11
9
  stories: RenderableStory[];
12
10
  manualRegion: string;
@@ -4,5 +4,5 @@ export function renderScreenshotSpec(args) {
4
4
  const tests = args.stories
5
5
  .map((story) => ` test('${escapeSingleQuotes(story.name)}', async ({ sharedPage }) => {\n await switchStory(sharedPage, '${story.id}');\n await expect(sharedPage).toHaveScreenshot();\n });`)
6
6
  .join("\n\n");
7
- return `import { test, expect } from '${args.fixturesImport}';\nimport { switchStory } from '${args.switchStoryImport}';\n\n// @generated-begin ${generatedRegionName}\ntest.describe('${escapeSingleQuotes(args.title)}', () => {\n${tests}\n});\n// @generated-end ${generatedRegionName}\n\n${args.manualRegion}`;
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}`;
8
8
  }
@@ -1,4 +1,4 @@
1
1
  export { defineConfig } from "./config.js";
2
2
  export type { StrybkConfig, StorybookGlobals } from "./config.js";
3
3
  export { generateScreenshots } from "./generate/index.js";
4
- export { createStrybkFixtures, switchStory } from "./playwright/index.js";
4
+ export { test, expect, switchStory } from "./playwright/index.js";
package/dist/src/index.js CHANGED
@@ -1,3 +1,3 @@
1
1
  export { defineConfig } from "./config.js";
2
2
  export { generateScreenshots } from "./generate/index.js";
3
- export { createStrybkFixtures, switchStory } from "./playwright/index.js";
3
+ export { test, expect, switchStory } from "./playwright/index.js";
@@ -1,2 +1,5 @@
1
- export { createStrybkFixtures } from "./fixtures.js";
2
1
  export { switchStory } from "./switchStory.js";
2
+ export declare const test: import("playwright/test").TestType<import("playwright/test").PlaywrightTestArgs & import("playwright/test").PlaywrightTestOptions & {
3
+ sharedPage: import("playwright-core").Page;
4
+ }, import("playwright/test").PlaywrightWorkerArgs & import("playwright/test").PlaywrightWorkerOptions>;
5
+ export declare const expect: import("playwright/test").Expect<{}>;
@@ -1,2 +1,5 @@
1
- export { createStrybkFixtures } from "./fixtures.js";
1
+ import { createStrybkFixtures } from "./fixtures.js";
2
2
  export { switchStory } from "./switchStory.js";
3
+ const fixtures = createStrybkFixtures();
4
+ export const test = fixtures.test;
5
+ export const expect = fixtures.expect;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crvy/strybk",
3
- "version": "0.0.1",
3
+ "version": "0.0.3",
4
4
  "description": "Generator-first Playwright screenshot testing for Storybook.",
5
5
  "keywords": [
6
6
  "crvy",
@@ -20,7 +20,7 @@
20
20
  "url": "git+https://github.com/creevey/strybk.git"
21
21
  },
22
22
  "bin": {
23
- "strybk": "./dist/src/cli.js"
23
+ "crvy-strybk": "./dist/src/cli.js"
24
24
  },
25
25
  "files": [
26
26
  "dist/",
@@ -89,7 +89,7 @@
89
89
  },
90
90
  "engines": {
91
91
  "bun": ">=1.3.0",
92
- "node": ">=24"
92
+ "node": ">=22"
93
93
  },
94
94
  "packageManager": "bun@1.3.13"
95
95
  }