@storyshelf/affected 0.6.2 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -24,6 +24,27 @@ const result = await computeAffected({ cwd, buildDir, baseSha, headSha });
24
24
 
25
25
  See the [StoryShelf documentation](https://github.com/GuptaSiddhant/StoryShelf#readme) for configuration details.
26
26
 
27
+ ## API
28
+
29
+ ```ts
30
+ import {
31
+ computeAffected, // full pipeline: git + index + stats + trace (never throws)
32
+ traceAffected, // graph + changed files + story files -> affected, or null for full
33
+ selectAffectedStories, // stories + affected paths -> { render, inherit }
34
+ changedFiles, // git diff baseline..HEAD plus staged/untracked
35
+ gitHeadSha, // local git HEAD, or null without git
36
+ gitBranchName, // local git branch, or null without git
37
+ gitRepoStatus, // "ok" | "shallow" | "no-git"
38
+ localSha, // synthetic unique sha (LOCAL_SHA_PREFIX) for non-git checkouts
39
+ loadDepGraph, // preview-stats.json -> bidirectional DepGraph (null when unusable)
40
+ loadStoryImportPaths, // index.json/stories.json -> story import paths
41
+ } from "@storyshelf/affected";
42
+ ```
43
+
44
+ `computeAffected({ cwd, buildDir, baseSha, headSha, untraced?, statsFile? })` returns `{ affectedImportPaths: string[] | null, changedFiles, baselineSha, fullReason }`. A `null` affected set means "render everything" — the contract is fail-open by design, which is what lets affected capture stay enabled by default.
45
+
46
+ Accepted stats shapes: Vite `preview-stats.json` module arrays (`id`/`importedIds`), record-shaped module maps, and Webpack-style `reasons` edges. Absolute stats paths match repo-relative changes by suffix.
47
+
27
48
  ## License
28
49
 
29
50
  MIT
package/dist/index.d.mts CHANGED
@@ -5,6 +5,7 @@ import { GitRepoStatus, LOCAL_BRANCH, LOCAL_SHA_PREFIX, MAX_CHANGED_FILES, chang
5
5
  import { STATS_FILENAME, buildGraph, graphModulesFor, loadDepGraph, loadStoryImportPaths, normalizePath, statsPathFor } from "./stats.mjs";
6
6
  import { matchesAnyGlob, traceAffected } from "./trace.mjs";
7
7
  import { StoryPartition, selectAffectedStories } from "./select.mjs";
8
+ import { PackageUsage, TrackedPackage, extractPackageUsage } from "./usage.mjs";
8
9
  //#region src/index.d.ts
9
10
  /**
10
11
  * Compute the affected story set for an upload. Never throws: every failure
@@ -15,5 +16,5 @@ import { StoryPartition, selectAffectedStories } from "./select.mjs";
15
16
  */
16
17
  declare function computeAffected(input: AffectedInput): Promise<AffectedResult>;
17
18
  //#endregion
18
- export { type AffectedInput, type AffectedResult, type DepGraph, type GitRepoStatus, LOCAL_BRANCH, LOCAL_SHA_PREFIX, MAX_CHANGED_FILES, STATS_FILENAME, type StoryLike, type StoryPartition, type TraceOptions, buildGraph, changedFiles, computeAffected, gitBranchName, gitHeadSha, gitRepoStatus, graphModulesFor, isShallowRepo, loadDepGraph, loadStoryImportPaths, localSha, matchesAnyGlob, normalizePath, selectAffectedStories, statsPathFor, traceAffected };
19
+ export { type AffectedInput, type AffectedResult, type DepGraph, type GitRepoStatus, LOCAL_BRANCH, LOCAL_SHA_PREFIX, MAX_CHANGED_FILES, type PackageUsage, STATS_FILENAME, type StoryLike, type StoryPartition, type TraceOptions, type TrackedPackage, buildGraph, changedFiles, computeAffected, extractPackageUsage, gitBranchName, gitHeadSha, gitRepoStatus, graphModulesFor, isShallowRepo, loadDepGraph, loadStoryImportPaths, localSha, matchesAnyGlob, normalizePath, selectAffectedStories, statsPathFor, traceAffected };
19
20
  //# sourceMappingURL=index.d.mts.map
package/dist/index.mjs CHANGED
@@ -5,6 +5,7 @@ import { LOCAL_BRANCH, LOCAL_SHA_PREFIX, MAX_CHANGED_FILES, changedFiles, gitBra
5
5
  import { STATS_FILENAME, buildGraph, graphModulesFor, loadDepGraph, loadStoryImportPaths, normalizePath, statsPathFor } from "./stats.mjs";
6
6
  import { matchesAnyGlob, traceAffected } from "./trace.mjs";
7
7
  import { selectAffectedStories } from "./select.mjs";
8
+ import { extractPackageUsage } from "./usage.mjs";
8
9
  //#region src/index.ts
9
10
  /**
10
11
  * Compute the affected story set for an upload. Never throws: every failure
@@ -70,6 +71,6 @@ async function computeSelective(input) {
70
71
  }
71
72
  }
72
73
  //#endregion
73
- export { LOCAL_BRANCH, LOCAL_SHA_PREFIX, MAX_CHANGED_FILES, STATS_FILENAME, buildGraph, changedFiles, computeAffected, gitBranchName, gitHeadSha, gitRepoStatus, graphModulesFor, isShallowRepo, loadDepGraph, loadStoryImportPaths, localSha, matchesAnyGlob, normalizePath, selectAffectedStories, statsPathFor, traceAffected };
74
+ export { LOCAL_BRANCH, LOCAL_SHA_PREFIX, MAX_CHANGED_FILES, STATS_FILENAME, buildGraph, changedFiles, computeAffected, extractPackageUsage, gitBranchName, gitHeadSha, gitRepoStatus, graphModulesFor, isShallowRepo, loadDepGraph, loadStoryImportPaths, localSha, matchesAnyGlob, normalizePath, selectAffectedStories, statsPathFor, traceAffected };
74
75
 
75
76
  //# sourceMappingURL=index.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":[],"sources":["../src/index.ts"],"sourcesContent":["import { changedFiles, gitRepoStatus, MAX_CHANGED_FILES } from \"./git.ts\";\nimport { loadDepGraph, loadStoryImportPaths, statsPathFor } from \"./stats.ts\";\nimport { traceAffected } from \"./trace.ts\";\nimport type { AffectedInput, AffectedResult } from \"./types.ts\";\n\n/**\n * Compute the affected story set for an upload. Never throws: every failure\n * degrades to a full render with a machine-readable `fullReason`, so\n * affected capture can stay enabled by default without new failure modes.\n *\n * @param input - Repository root, build dir, revisions, and trace options.\n */\nexport async function computeAffected(input: AffectedInput): Promise<AffectedResult> {\n try {\n return await computeSelective(input);\n } catch {\n return fullResult([], input.baseSha, \"affected-computation-failed\");\n }\n}\n\n/** Full-render result with a reason. */\nfunction fullResult(changed: string[], baselineSha: string | null, reason: string): AffectedResult {\n return { affectedImportPaths: null, changedFiles: changed, baselineSha, fullReason: reason };\n}\n\n/** Selective result for an affected import-path set. */\nfunction selectiveResult(changed: string[], baselineSha: string, paths: string[]): AffectedResult {\n return { affectedImportPaths: paths, changedFiles: changed, baselineSha, fullReason: null };\n}\n\n/** Guarded changed-file list, throwing a full-render reason when unusable. */\nfunction guardedChanged(input: AffectedInput): string[] {\n const repo = gitRepoStatus(input.cwd);\n if (repo !== \"ok\") {\n throw new Error(repo === \"shallow\" ? \"shallow-clone\" : \"not-a-git-repo\");\n }\n const changed = changedFiles(input.cwd, input.baseSha, input.headSha);\n if (changed.length > MAX_CHANGED_FILES) {\n throw new Error(\"too-many-changed-files\");\n }\n return changed;\n}\n\n/** Trace the affected set once inputs are validated. Throws reason strings. */\nasync function traceSelective(input: AffectedInput, changed: string[]): Promise<AffectedResult> {\n if (changed.length === 0) {\n return selectiveResult(changed, input.baseSha, []);\n }\n const stories = await loadStoryImportPaths(input.cwd, input.buildDir).catch(() => {\n throw new Error(\"story-index-unreadable\");\n });\n const graph = await loadDepGraph(statsPathFor(input.cwd, input.buildDir, input.statsFile));\n if (!graph) {\n throw new Error(\"dependency-graph-unavailable\");\n }\n const affected = traceAffected(graph, changed, stories, { untraced: input.untraced });\n if (affected === null) {\n throw new Error(\"global-file-changed\");\n }\n return selectiveResult(changed, input.baseSha, affected);\n}\n\n/** Selective computation with reason-string failures mapped to full renders. */\nasync function computeSelective(input: AffectedInput): Promise<AffectedResult> {\n let changed: string[] = [];\n try {\n changed = guardedChanged(input);\n return await traceSelective(input, changed);\n } catch (error) {\n const reason = error instanceof Error ? error.message : \"affected-computation-failed\";\n return fullResult(changed, input.baseSha, reason);\n }\n}\n\nexport type { AffectedInput, AffectedResult, DepGraph, StoryLike, TraceOptions } from \"./types.ts\";\nexport {\n changedFiles,\n gitBranchName,\n gitHeadSha,\n gitRepoStatus,\n isShallowRepo,\n LOCAL_BRANCH,\n LOCAL_SHA_PREFIX,\n localSha,\n MAX_CHANGED_FILES,\n} from \"./git.ts\";\nexport type { GitRepoStatus } from \"./git.ts\";\nexport {\n buildGraph,\n graphModulesFor,\n loadDepGraph,\n loadStoryImportPaths,\n normalizePath,\n STATS_FILENAME,\n statsPathFor,\n} from \"./stats.ts\";\nexport { matchesAnyGlob, traceAffected } from \"./trace.ts\";\nexport { selectAffectedStories, type StoryPartition } from \"./select.ts\";\n"],"mappings":";;;;;;;;;;;;;;;AAYA,eAAsB,gBAAgB,OAA+C;CACnF,IAAI;EACF,OAAO,MAAM,iBAAiB,KAAK;CACrC,QAAQ;EACN,OAAO,WAAW,CAAC,GAAG,MAAM,SAAS,6BAA6B;CACpE;AACF;;AAGA,SAAS,WAAW,SAAmB,aAA4B,QAAgC;CACjG,OAAO;EAAE,qBAAqB;EAAM,cAAc;EAAS;EAAa,YAAY;CAAO;AAC7F;;AAGA,SAAS,gBAAgB,SAAmB,aAAqB,OAAiC;CAChG,OAAO;EAAE,qBAAqB;EAAO,cAAc;EAAS;EAAa,YAAY;CAAK;AAC5F;;AAGA,SAAS,eAAe,OAAgC;CACtD,MAAM,OAAO,cAAc,MAAM,GAAG;CACpC,IAAI,SAAS,MACX,MAAM,IAAI,MAAM,SAAS,YAAY,kBAAkB,gBAAgB;CAEzE,MAAM,UAAU,aAAa,MAAM,KAAK,MAAM,SAAS,MAAM,OAAO;CACpE,IAAI,QAAQ,SAAA,KACV,MAAM,IAAI,MAAM,wBAAwB;CAE1C,OAAO;AACT;;AAGA,eAAe,eAAe,OAAsB,SAA4C;CAC9F,IAAI,QAAQ,WAAW,GACrB,OAAO,gBAAgB,SAAS,MAAM,SAAS,CAAC,CAAC;CAEnD,MAAM,UAAU,MAAM,qBAAqB,MAAM,KAAK,MAAM,QAAQ,CAAC,CAAC,YAAY;EAChF,MAAM,IAAI,MAAM,wBAAwB;CAC1C,CAAC;CACD,MAAM,QAAQ,MAAM,aAAa,aAAa,MAAM,KAAK,MAAM,UAAU,MAAM,SAAS,CAAC;CACzF,IAAI,CAAC,OACH,MAAM,IAAI,MAAM,8BAA8B;CAEhD,MAAM,WAAW,cAAc,OAAO,SAAS,SAAS,EAAE,UAAU,MAAM,SAAS,CAAC;CACpF,IAAI,aAAa,MACf,MAAM,IAAI,MAAM,qBAAqB;CAEvC,OAAO,gBAAgB,SAAS,MAAM,SAAS,QAAQ;AACzD;;AAGA,eAAe,iBAAiB,OAA+C;CAC7E,IAAI,UAAoB,CAAC;CACzB,IAAI;EACF,UAAU,eAAe,KAAK;EAC9B,OAAO,MAAM,eAAe,OAAO,OAAO;CAC5C,SAAS,OAAO;EACd,MAAM,SAAS,iBAAiB,QAAQ,MAAM,UAAU;EACxD,OAAO,WAAW,SAAS,MAAM,SAAS,MAAM;CAClD;AACF"}
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../src/index.ts"],"sourcesContent":["import { changedFiles, gitRepoStatus, MAX_CHANGED_FILES } from \"./git.ts\";\nimport { loadDepGraph, loadStoryImportPaths, statsPathFor } from \"./stats.ts\";\nimport { traceAffected } from \"./trace.ts\";\nimport type { AffectedInput, AffectedResult } from \"./types.ts\";\n\n/**\n * Compute the affected story set for an upload. Never throws: every failure\n * degrades to a full render with a machine-readable `fullReason`, so\n * affected capture can stay enabled by default without new failure modes.\n *\n * @param input - Repository root, build dir, revisions, and trace options.\n */\nexport async function computeAffected(input: AffectedInput): Promise<AffectedResult> {\n try {\n return await computeSelective(input);\n } catch {\n return fullResult([], input.baseSha, \"affected-computation-failed\");\n }\n}\n\n/** Full-render result with a reason. */\nfunction fullResult(changed: string[], baselineSha: string | null, reason: string): AffectedResult {\n return { affectedImportPaths: null, changedFiles: changed, baselineSha, fullReason: reason };\n}\n\n/** Selective result for an affected import-path set. */\nfunction selectiveResult(changed: string[], baselineSha: string, paths: string[]): AffectedResult {\n return { affectedImportPaths: paths, changedFiles: changed, baselineSha, fullReason: null };\n}\n\n/** Guarded changed-file list, throwing a full-render reason when unusable. */\nfunction guardedChanged(input: AffectedInput): string[] {\n const repo = gitRepoStatus(input.cwd);\n if (repo !== \"ok\") {\n throw new Error(repo === \"shallow\" ? \"shallow-clone\" : \"not-a-git-repo\");\n }\n const changed = changedFiles(input.cwd, input.baseSha, input.headSha);\n if (changed.length > MAX_CHANGED_FILES) {\n throw new Error(\"too-many-changed-files\");\n }\n return changed;\n}\n\n/** Trace the affected set once inputs are validated. Throws reason strings. */\nasync function traceSelective(input: AffectedInput, changed: string[]): Promise<AffectedResult> {\n if (changed.length === 0) {\n return selectiveResult(changed, input.baseSha, []);\n }\n const stories = await loadStoryImportPaths(input.cwd, input.buildDir).catch(() => {\n throw new Error(\"story-index-unreadable\");\n });\n const graph = await loadDepGraph(statsPathFor(input.cwd, input.buildDir, input.statsFile));\n if (!graph) {\n throw new Error(\"dependency-graph-unavailable\");\n }\n const affected = traceAffected(graph, changed, stories, { untraced: input.untraced });\n if (affected === null) {\n throw new Error(\"global-file-changed\");\n }\n return selectiveResult(changed, input.baseSha, affected);\n}\n\n/** Selective computation with reason-string failures mapped to full renders. */\nasync function computeSelective(input: AffectedInput): Promise<AffectedResult> {\n let changed: string[] = [];\n try {\n changed = guardedChanged(input);\n return await traceSelective(input, changed);\n } catch (error) {\n const reason = error instanceof Error ? error.message : \"affected-computation-failed\";\n return fullResult(changed, input.baseSha, reason);\n }\n}\n\nexport type { AffectedInput, AffectedResult, DepGraph, StoryLike, TraceOptions } from \"./types.ts\";\nexport {\n changedFiles,\n gitBranchName,\n gitHeadSha,\n gitRepoStatus,\n isShallowRepo,\n LOCAL_BRANCH,\n LOCAL_SHA_PREFIX,\n localSha,\n MAX_CHANGED_FILES,\n} from \"./git.ts\";\nexport type { GitRepoStatus } from \"./git.ts\";\nexport {\n buildGraph,\n graphModulesFor,\n loadDepGraph,\n loadStoryImportPaths,\n normalizePath,\n STATS_FILENAME,\n statsPathFor,\n} from \"./stats.ts\";\nexport { matchesAnyGlob, traceAffected } from \"./trace.ts\";\nexport { selectAffectedStories, type StoryPartition } from \"./select.ts\";\nexport { extractPackageUsage, type PackageUsage, type TrackedPackage } from \"./usage.ts\";\n"],"mappings":";;;;;;;;;;;;;;;;AAYA,eAAsB,gBAAgB,OAA+C;CACnF,IAAI;EACF,OAAO,MAAM,iBAAiB,KAAK;CACrC,QAAQ;EACN,OAAO,WAAW,CAAC,GAAG,MAAM,SAAS,6BAA6B;CACpE;AACF;;AAGA,SAAS,WAAW,SAAmB,aAA4B,QAAgC;CACjG,OAAO;EAAE,qBAAqB;EAAM,cAAc;EAAS;EAAa,YAAY;CAAO;AAC7F;;AAGA,SAAS,gBAAgB,SAAmB,aAAqB,OAAiC;CAChG,OAAO;EAAE,qBAAqB;EAAO,cAAc;EAAS;EAAa,YAAY;CAAK;AAC5F;;AAGA,SAAS,eAAe,OAAgC;CACtD,MAAM,OAAO,cAAc,MAAM,GAAG;CACpC,IAAI,SAAS,MACX,MAAM,IAAI,MAAM,SAAS,YAAY,kBAAkB,gBAAgB;CAEzE,MAAM,UAAU,aAAa,MAAM,KAAK,MAAM,SAAS,MAAM,OAAO;CACpE,IAAI,QAAQ,SAAA,KACV,MAAM,IAAI,MAAM,wBAAwB;CAE1C,OAAO;AACT;;AAGA,eAAe,eAAe,OAAsB,SAA4C;CAC9F,IAAI,QAAQ,WAAW,GACrB,OAAO,gBAAgB,SAAS,MAAM,SAAS,CAAC,CAAC;CAEnD,MAAM,UAAU,MAAM,qBAAqB,MAAM,KAAK,MAAM,QAAQ,CAAC,CAAC,YAAY;EAChF,MAAM,IAAI,MAAM,wBAAwB;CAC1C,CAAC;CACD,MAAM,QAAQ,MAAM,aAAa,aAAa,MAAM,KAAK,MAAM,UAAU,MAAM,SAAS,CAAC;CACzF,IAAI,CAAC,OACH,MAAM,IAAI,MAAM,8BAA8B;CAEhD,MAAM,WAAW,cAAc,OAAO,SAAS,SAAS,EAAE,UAAU,MAAM,SAAS,CAAC;CACpF,IAAI,aAAa,MACf,MAAM,IAAI,MAAM,qBAAqB;CAEvC,OAAO,gBAAgB,SAAS,MAAM,SAAS,QAAQ;AACzD;;AAGA,eAAe,iBAAiB,OAA+C;CAC7E,IAAI,UAAoB,CAAC;CACzB,IAAI;EACF,UAAU,eAAe,KAAK;EAC9B,OAAO,MAAM,eAAe,OAAO,OAAO;CAC5C,SAAS,OAAO;EACd,MAAM,SAAS,iBAAiB,QAAQ,MAAM,UAAU;EACxD,OAAO,WAAW,SAAS,MAAM,SAAS,MAAM;CAClD;AACF"}
package/dist/stats.mjs CHANGED
@@ -8,9 +8,20 @@ import { join, resolve } from "node:path";
8
8
  const STATS_FILENAME = "preview-stats.json";
9
9
  /** Candidate index files listing a built Storybook's stories. */
10
10
  const INDEX_CANDIDATES = ["stories.json", "index.json"];
11
+ /**
12
+ * Strip bundler module-id decoration so sub-modules fold into their file:
13
+ * webpack loader chains (`a-loader!b-loader??opts!./Button.vue`) keep only
14
+ * the resource after the last `!`, and `?query` suffixes
15
+ * (`Button.vue?vue&type=script`, `Card.svelte?svelte&type=style`) are dropped.
16
+ */
17
+ function stripQueryAndLoader(value) {
18
+ const resource = value.slice(value.lastIndexOf("!") + 1);
19
+ const queryStart = resource.indexOf("?");
20
+ return queryStart === -1 ? resource : resource.slice(0, queryStart);
21
+ }
11
22
  /** Normalize a stats path to a repo-relative posix form for comparison. */
12
23
  function normalizePath(value) {
13
- return value.replaceAll("\\", "/").replaceAll(/^\.\//gu, "").replaceAll(/^\//gu, "");
24
+ return stripQueryAndLoader(value).replaceAll("\\", "/").replaceAll(/^\.\//gu, "").replaceAll(/^\//gu, "");
14
25
  }
15
26
  /** True when stats module `candidate` refers to repo-relative file `target`. */
16
27
  function matchesFile(candidate, target) {
@@ -156,6 +167,6 @@ function graphModulesFor(graph, target) {
156
167
  return matched;
157
168
  }
158
169
  //#endregion
159
- export { STATS_FILENAME, buildGraph, graphModulesFor, loadDepGraph, loadStoryImportPaths, normalizePath, statsPathFor };
170
+ export { STATS_FILENAME, buildGraph, graphModulesFor, loadDepGraph, loadStoryImportPaths, normalizePath, statsPathFor, stripQueryAndLoader };
160
171
 
161
172
  //# sourceMappingURL=stats.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"stats.mjs","names":["record"],"sources":["../src/stats.ts"],"sourcesContent":["import { readFile } from \"node:fs/promises\";\nimport { join, resolve } from \"node:path\";\nimport type { DepGraph } from \"./types.ts\";\n\n/** Basename of the Vite stats file emitted next to a built Storybook. */\nexport const STATS_FILENAME = \"preview-stats.json\";\n\n/** Candidate index files listing a built Storybook's stories. */\nconst INDEX_CANDIDATES = [\"stories.json\", \"index.json\"];\n\n/** A single module entry in a stats file (Vite or Webpack shape). */\ninterface StatsModule {\n id?: unknown;\n name?: unknown;\n identifier?: unknown;\n imported?: unknown;\n importedIds?: unknown;\n dependencies?: unknown;\n reasons?: unknown;\n}\n\ninterface StoryIndex {\n entries?: Record<string, { importPath?: unknown }>;\n}\n\n/** Normalize a stats path to a repo-relative posix form for comparison. */\nexport function normalizePath(value: string): string {\n return value.replaceAll(\"\\\\\", \"/\").replaceAll(/^\\.\\//gu, \"\").replaceAll(/^\\//gu, \"\");\n}\n\n/** True when stats module `candidate` refers to repo-relative file `target`. */\nfunction matchesFile(candidate: string, target: string): boolean {\n if (candidate === target) {\n return true;\n }\n return candidate.endsWith(`/${target}`);\n}\n\n/** Coerce an unknown stats value to a list of normalized paths. */\nfunction asPathList(value: unknown): string[] {\n if (!Array.isArray(value)) {\n return [];\n }\n const paths: string[] = [];\n for (const entry of value) {\n if (typeof entry === \"string\" && entry.length > 0) {\n paths.push(normalizePath(entry));\n }\n }\n return paths;\n}\n\n/** Read a non-empty string field from a stats module entry. */\nfunction stringField(entry: StatsModule, key: \"id\" | \"name\" | \"identifier\"): string | null {\n const value = entry[key];\n return typeof value === \"string\" && value.length > 0 ? value : null;\n}\n\n/** Read the file a stats module entry refers to, if any. */\nfunction moduleFile(entry: StatsModule): string | null {\n const raw =\n stringField(entry, \"id\") ?? stringField(entry, \"name\") ?? stringField(entry, \"identifier\");\n return raw === null ? null : normalizePath(raw);\n}\n\n/** Forward imports declared by a stats module entry, if any. */\nfunction moduleImports(entry: StatsModule): string[] {\n return asPathList(entry.imported ?? entry.importedIds ?? entry.dependencies);\n}\n\n/** Dependent names declared via Webpack-style `reasons`, if any. */\nfunction moduleReasonNames(entry: StatsModule): string[] {\n if (!Array.isArray(entry.reasons)) {\n return [];\n }\n const names: string[] = [];\n for (const reason of entry.reasons) {\n if (typeof reason === \"object\" && reason !== null) {\n const record = reason as Record<string, unknown>;\n const name = record[\"moduleName\"] ?? record[\"module\"];\n if (typeof name === \"string\" && name.length > 0) {\n names.push(normalizePath(name));\n }\n }\n }\n return names;\n}\n\n/** Attach a record-shaped module entry to its name. */\nfunction entryWithName(name: string, value: unknown): StatsModule {\n if (typeof value === \"object\" && value !== null) {\n return { name, ...(value as StatsModule) };\n }\n return { name };\n}\n\n/** Extract module entries from a parsed stats document of unknown shape. */\nfunction moduleEntries(document: unknown): StatsModule[] {\n if (typeof document !== \"object\" || document === null) {\n return [];\n }\n const record = document as Record<string, unknown>;\n const raw = record[\"modules\"];\n if (Array.isArray(raw)) {\n return raw.filter((entry): entry is StatsModule => typeof entry === \"object\");\n }\n if (typeof raw === \"object\" && raw !== null) {\n return Object.entries(raw as Record<string, unknown>).map(([name, value]) =>\n entryWithName(name, value),\n );\n }\n return [];\n}\n\n/** Record one directed edge in the graph under construction. */\nfunction addEdge(\n imports: Map<string, Set<string>>,\n importedBy: Map<string, Set<string>>,\n from: string,\n to: string,\n): void {\n if (from === to) {\n return;\n }\n const forward = imports.get(from) ?? new Set<string>();\n forward.add(to);\n imports.set(from, forward);\n const reverse = importedBy.get(to) ?? new Set<string>();\n reverse.add(from);\n importedBy.set(to, reverse);\n}\n\n/** Add one stats entry's edges to the graph under construction. */\nfunction addEntryEdges(\n imports: Map<string, Set<string>>,\n importedBy: Map<string, Set<string>>,\n entry: StatsModule,\n): void {\n const file = moduleFile(entry);\n if (!file) {\n return;\n }\n for (const imported of moduleImports(entry)) {\n addEdge(imports, importedBy, file, imported);\n }\n for (const dependent of moduleReasonNames(entry)) {\n addEdge(imports, importedBy, dependent, file);\n }\n}\nfunction toGraph(\n imports: Map<string, Set<string>>,\n importedBy: Map<string, Set<string>>,\n): DepGraph {\n return {\n imports: Object.fromEntries([...imports].map(([file, set]) => [file, [...set].toSorted()])),\n importedBy: Object.fromEntries(\n [...importedBy].map(([file, set]) => [file, [...set].toSorted()]),\n ),\n };\n}\n\n/**\n * Build a bidirectional dependency graph from parsed stats modules.\n *\n * @param entries - Module entries from a Vite or Webpack stats document.\n * @returns Bidirectional graph, or `null` when no usable edges exist.\n */\nexport function buildGraph(entries: StatsModule[]): DepGraph | null {\n const imports = new Map<string, Set<string>>();\n const importedBy = new Map<string, Set<string>>();\n for (const entry of entries) {\n addEntryEdges(imports, importedBy, entry);\n }\n if (imports.size === 0) {\n return null;\n }\n return toGraph(imports, importedBy);\n}\n\n/**\n * Load and parse a stats file into a dependency graph.\n *\n * @param statsPath - Absolute or cwd-relative path to the stats JSON file.\n * @returns Graph, or `null` when the file is missing or unrecognized.\n */\nexport async function loadDepGraph(statsPath: string): Promise<DepGraph | null> {\n let raw: string;\n try {\n raw = await readFile(statsPath, \"utf8\");\n } catch {\n return null;\n }\n let document: unknown;\n try {\n document = JSON.parse(raw) as unknown;\n } catch {\n return null;\n }\n return buildGraph(moduleEntries(document));\n}\n\n/** Resolve the stats file path for a built Storybook directory. */\nexport function statsPathFor(cwd: string, buildDir: string, override?: string): string {\n if (override) {\n return resolve(cwd, override);\n }\n return join(resolve(cwd, buildDir), STATS_FILENAME);\n}\n\n/**\n * Load the deduplicated story import paths from a built Storybook's index.\n *\n * @param cwd - Repository root.\n * @param buildDir - Built Storybook directory.\n * @returns Normalized import paths; throws when no index is readable.\n */\nexport async function loadStoryImportPaths(cwd: string, buildDir: string): Promise<string[]> {\n const root = resolve(cwd, buildDir);\n const documents = await Promise.all(\n INDEX_CANDIDATES.map(async (name) => {\n try {\n return JSON.parse(await readFile(join(root, name), \"utf8\")) as StoryIndex;\n } catch {\n return null;\n }\n }),\n );\n const found = documents.find((document) => document !== null);\n if (!found?.entries) {\n throw new Error(`No Storybook index found in ${root}`);\n }\n const paths = new Set<string>();\n for (const entry of Object.values(found.entries)) {\n if (typeof entry.importPath === \"string\" && entry.importPath.length > 0) {\n paths.add(normalizePath(entry.importPath));\n }\n }\n return [...paths].toSorted();\n}\n\n/** Find graph modules referring to repo-relative file `target`. */\nexport function graphModulesFor(graph: DepGraph, target: string): string[] {\n const matched: string[] = [];\n for (const file of Object.keys(graph.imports)) {\n if (matchesFile(file, target)) {\n matched.push(file);\n }\n }\n for (const file of Object.keys(graph.importedBy)) {\n if (matchesFile(file, target) && !matched.includes(file)) {\n matched.push(file);\n }\n }\n return matched;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"stats.mjs","names":["record"],"sources":["../src/stats.ts"],"sourcesContent":["import { readFile } from \"node:fs/promises\";\nimport { join, resolve } from \"node:path\";\nimport type { DepGraph } from \"./types.ts\";\n\n/** Basename of the Vite stats file emitted next to a built Storybook. */\nexport const STATS_FILENAME = \"preview-stats.json\";\n\n/** Candidate index files listing a built Storybook's stories. */\nconst INDEX_CANDIDATES = [\"stories.json\", \"index.json\"];\n\n/** A single module entry in a stats file (Vite or Webpack shape). */\ninterface StatsModule {\n id?: unknown;\n name?: unknown;\n identifier?: unknown;\n imported?: unknown;\n importedIds?: unknown;\n dependencies?: unknown;\n reasons?: unknown;\n}\n\ninterface StoryIndex {\n entries?: Record<string, { importPath?: unknown }>;\n}\n\n/**\n * Strip bundler module-id decoration so sub-modules fold into their file:\n * webpack loader chains (`a-loader!b-loader??opts!./Button.vue`) keep only\n * the resource after the last `!`, and `?query` suffixes\n * (`Button.vue?vue&type=script`, `Card.svelte?svelte&type=style`) are dropped.\n */\nexport function stripQueryAndLoader(value: string): string {\n const resource = value.slice(value.lastIndexOf(\"!\") + 1);\n const queryStart = resource.indexOf(\"?\");\n return queryStart === -1 ? resource : resource.slice(0, queryStart);\n}\n\n/** Normalize a stats path to a repo-relative posix form for comparison. */\nexport function normalizePath(value: string): string {\n return stripQueryAndLoader(value)\n .replaceAll(\"\\\\\", \"/\")\n .replaceAll(/^\\.\\//gu, \"\")\n .replaceAll(/^\\//gu, \"\");\n}\n\n/** True when stats module `candidate` refers to repo-relative file `target`. */\nfunction matchesFile(candidate: string, target: string): boolean {\n if (candidate === target) {\n return true;\n }\n return candidate.endsWith(`/${target}`);\n}\n\n/** Coerce an unknown stats value to a list of normalized paths. */\nfunction asPathList(value: unknown): string[] {\n if (!Array.isArray(value)) {\n return [];\n }\n const paths: string[] = [];\n for (const entry of value) {\n if (typeof entry === \"string\" && entry.length > 0) {\n paths.push(normalizePath(entry));\n }\n }\n return paths;\n}\n\n/** Read a non-empty string field from a stats module entry. */\nfunction stringField(entry: StatsModule, key: \"id\" | \"name\" | \"identifier\"): string | null {\n const value = entry[key];\n return typeof value === \"string\" && value.length > 0 ? value : null;\n}\n\n/** Read the file a stats module entry refers to, if any. */\nfunction moduleFile(entry: StatsModule): string | null {\n const raw =\n stringField(entry, \"id\") ?? stringField(entry, \"name\") ?? stringField(entry, \"identifier\");\n return raw === null ? null : normalizePath(raw);\n}\n\n/** Forward imports declared by a stats module entry, if any. */\nfunction moduleImports(entry: StatsModule): string[] {\n return asPathList(entry.imported ?? entry.importedIds ?? entry.dependencies);\n}\n\n/** Dependent names declared via Webpack-style `reasons`, if any. */\nfunction moduleReasonNames(entry: StatsModule): string[] {\n if (!Array.isArray(entry.reasons)) {\n return [];\n }\n const names: string[] = [];\n for (const reason of entry.reasons) {\n if (typeof reason === \"object\" && reason !== null) {\n const record = reason as Record<string, unknown>;\n const name = record[\"moduleName\"] ?? record[\"module\"];\n if (typeof name === \"string\" && name.length > 0) {\n names.push(normalizePath(name));\n }\n }\n }\n return names;\n}\n\n/** Attach a record-shaped module entry to its name. */\nfunction entryWithName(name: string, value: unknown): StatsModule {\n if (typeof value === \"object\" && value !== null) {\n return { name, ...(value as StatsModule) };\n }\n return { name };\n}\n\n/** Extract module entries from a parsed stats document of unknown shape. */\nfunction moduleEntries(document: unknown): StatsModule[] {\n if (typeof document !== \"object\" || document === null) {\n return [];\n }\n const record = document as Record<string, unknown>;\n const raw = record[\"modules\"];\n if (Array.isArray(raw)) {\n return raw.filter((entry): entry is StatsModule => typeof entry === \"object\");\n }\n if (typeof raw === \"object\" && raw !== null) {\n return Object.entries(raw as Record<string, unknown>).map(([name, value]) =>\n entryWithName(name, value),\n );\n }\n return [];\n}\n\n/** Record one directed edge in the graph under construction. */\nfunction addEdge(\n imports: Map<string, Set<string>>,\n importedBy: Map<string, Set<string>>,\n from: string,\n to: string,\n): void {\n if (from === to) {\n return;\n }\n const forward = imports.get(from) ?? new Set<string>();\n forward.add(to);\n imports.set(from, forward);\n const reverse = importedBy.get(to) ?? new Set<string>();\n reverse.add(from);\n importedBy.set(to, reverse);\n}\n\n/** Add one stats entry's edges to the graph under construction. */\nfunction addEntryEdges(\n imports: Map<string, Set<string>>,\n importedBy: Map<string, Set<string>>,\n entry: StatsModule,\n): void {\n const file = moduleFile(entry);\n if (!file) {\n return;\n }\n for (const imported of moduleImports(entry)) {\n addEdge(imports, importedBy, file, imported);\n }\n for (const dependent of moduleReasonNames(entry)) {\n addEdge(imports, importedBy, dependent, file);\n }\n}\nfunction toGraph(\n imports: Map<string, Set<string>>,\n importedBy: Map<string, Set<string>>,\n): DepGraph {\n return {\n imports: Object.fromEntries([...imports].map(([file, set]) => [file, [...set].toSorted()])),\n importedBy: Object.fromEntries(\n [...importedBy].map(([file, set]) => [file, [...set].toSorted()]),\n ),\n };\n}\n\n/**\n * Build a bidirectional dependency graph from parsed stats modules.\n *\n * @param entries - Module entries from a Vite or Webpack stats document.\n * @returns Bidirectional graph, or `null` when no usable edges exist.\n */\nexport function buildGraph(entries: StatsModule[]): DepGraph | null {\n const imports = new Map<string, Set<string>>();\n const importedBy = new Map<string, Set<string>>();\n for (const entry of entries) {\n addEntryEdges(imports, importedBy, entry);\n }\n if (imports.size === 0) {\n return null;\n }\n return toGraph(imports, importedBy);\n}\n\n/**\n * Load and parse a stats file into a dependency graph.\n *\n * @param statsPath - Absolute or cwd-relative path to the stats JSON file.\n * @returns Graph, or `null` when the file is missing or unrecognized.\n */\nexport async function loadDepGraph(statsPath: string): Promise<DepGraph | null> {\n let raw: string;\n try {\n raw = await readFile(statsPath, \"utf8\");\n } catch {\n return null;\n }\n let document: unknown;\n try {\n document = JSON.parse(raw) as unknown;\n } catch {\n return null;\n }\n return buildGraph(moduleEntries(document));\n}\n\n/** Resolve the stats file path for a built Storybook directory. */\nexport function statsPathFor(cwd: string, buildDir: string, override?: string): string {\n if (override) {\n return resolve(cwd, override);\n }\n return join(resolve(cwd, buildDir), STATS_FILENAME);\n}\n\n/**\n * Load the deduplicated story import paths from a built Storybook's index.\n *\n * @param cwd - Repository root.\n * @param buildDir - Built Storybook directory.\n * @returns Normalized import paths; throws when no index is readable.\n */\nexport async function loadStoryImportPaths(cwd: string, buildDir: string): Promise<string[]> {\n const root = resolve(cwd, buildDir);\n const documents = await Promise.all(\n INDEX_CANDIDATES.map(async (name) => {\n try {\n return JSON.parse(await readFile(join(root, name), \"utf8\")) as StoryIndex;\n } catch {\n return null;\n }\n }),\n );\n const found = documents.find((document) => document !== null);\n if (!found?.entries) {\n throw new Error(`No Storybook index found in ${root}`);\n }\n const paths = new Set<string>();\n for (const entry of Object.values(found.entries)) {\n if (typeof entry.importPath === \"string\" && entry.importPath.length > 0) {\n paths.add(normalizePath(entry.importPath));\n }\n }\n return [...paths].toSorted();\n}\n\n/** Find graph modules referring to repo-relative file `target`. */\nexport function graphModulesFor(graph: DepGraph, target: string): string[] {\n const matched: string[] = [];\n for (const file of Object.keys(graph.imports)) {\n if (matchesFile(file, target)) {\n matched.push(file);\n }\n }\n for (const file of Object.keys(graph.importedBy)) {\n if (matchesFile(file, target) && !matched.includes(file)) {\n matched.push(file);\n }\n }\n return matched;\n}\n"],"mappings":"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"}
@@ -0,0 +1,31 @@
1
+ import __tsdown_shims_path from 'node:path';
2
+ import __tsdown_shims_url from 'node:url';
3
+ import { DepGraph } from "./types.mjs";
4
+ //#region src/usage.d.ts
5
+ /** A dependency package a story file reaches, and the first module that reaches it. */
6
+ interface PackageUsage {
7
+ /** Story file (repo-relative, as given by the Storybook index). */
8
+ storyFile: string;
9
+ packageName: string;
10
+ /** Graph module inside the package (a `node_modules/<pkg>/…` path or a workspace source file). */
11
+ modulePath: string;
12
+ }
13
+ /** Where a tracked package lives: always `node_modules`, plus a workspace dir when linked. */
14
+ interface TrackedPackage {
15
+ /** Repo-relative directory of a workspace-linked package (outside `node_modules`). */
16
+ root?: string;
17
+ }
18
+ /**
19
+ * Find which tracked dependency packages each story reaches through its
20
+ * import graph. Traversal stops at a package boundary (its internals are not
21
+ * walked) and records only the shallowest module per package, so a story
22
+ * yields at most one row per package.
23
+ *
24
+ * @param graph - Bidirectional module graph from the build's stats file.
25
+ * @param storyFiles - Story import paths from the Storybook index.
26
+ * @param packages - Packages to track (e.g. the project's declared dependencies).
27
+ */
28
+ declare function extractPackageUsage(graph: DepGraph, storyFiles: readonly string[], packages: Record<string, TrackedPackage>): PackageUsage[];
29
+ //#endregion
30
+ export { PackageUsage, TrackedPackage, extractPackageUsage };
31
+ //# sourceMappingURL=usage.d.mts.map
package/dist/usage.mjs ADDED
@@ -0,0 +1,66 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ import { graphModulesFor } from "./stats.mjs";
5
+ //#region src/usage.ts
6
+ const NODE_MODULES = "node_modules/";
7
+ /** Package name for a `…/node_modules/<pkg>/…` module path, handling scopes. */
8
+ function nodeModulesPackage(module) {
9
+ const start = module.lastIndexOf(NODE_MODULES);
10
+ if (start === -1) return null;
11
+ const [first, second] = module.slice(start + 13).split("/");
12
+ if (!first) return null;
13
+ return first.startsWith("@") && second ? `${first}/${second}` : first;
14
+ }
15
+ /** Tracked package that owns `module`, via `node_modules` or a workspace root. */
16
+ function owningPackage(module, tracked) {
17
+ const installed = nodeModulesPackage(module);
18
+ if (installed !== null) return tracked.has(installed) ? installed : null;
19
+ for (const [name, { root }] of tracked) if (root && module.startsWith(`${root}/`)) return name;
20
+ return null;
21
+ }
22
+ /** Classify one imported module: record a package hit, or queue it for expansion. */
23
+ function visitDep(walk, dep, storyFile, tracked) {
24
+ if (walk.seen.has(dep)) return;
25
+ walk.seen.add(dep);
26
+ const owner = owningPackage(dep, tracked);
27
+ if (owner === null) walk.next.push(dep);
28
+ else if (!walk.found.has(owner)) walk.found.set(owner, {
29
+ storyFile,
30
+ packageName: owner,
31
+ modulePath: dep
32
+ });
33
+ }
34
+ /** Breadth-first walk of one story's imports, recording the shallowest hit per package. */
35
+ function usageForStory(graph, storyFile, start, tracked) {
36
+ const walk = {
37
+ seen: new Set(start),
38
+ found: /* @__PURE__ */ new Map(),
39
+ next: []
40
+ };
41
+ let frontier = start;
42
+ while (frontier.length > 0) {
43
+ walk.next = [];
44
+ for (const dep of frontier.flatMap((module) => graph.imports[module] ?? [])) visitDep(walk, dep, storyFile, tracked);
45
+ frontier = walk.next;
46
+ }
47
+ return [...walk.found.values()];
48
+ }
49
+ /**
50
+ * Find which tracked dependency packages each story reaches through its
51
+ * import graph. Traversal stops at a package boundary (its internals are not
52
+ * walked) and records only the shallowest module per package, so a story
53
+ * yields at most one row per package.
54
+ *
55
+ * @param graph - Bidirectional module graph from the build's stats file.
56
+ * @param storyFiles - Story import paths from the Storybook index.
57
+ * @param packages - Packages to track (e.g. the project's declared dependencies).
58
+ */
59
+ function extractPackageUsage(graph, storyFiles, packages) {
60
+ const tracked = new Map(Object.entries(packages));
61
+ return storyFiles.flatMap((storyFile) => usageForStory(graph, storyFile, graphModulesFor(graph, storyFile), tracked));
62
+ }
63
+ //#endregion
64
+ export { extractPackageUsage };
65
+
66
+ //# sourceMappingURL=usage.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"usage.mjs","names":[],"sources":["../src/usage.ts"],"sourcesContent":["import { graphModulesFor } from \"./stats.ts\";\nimport type { DepGraph } from \"./types.ts\";\n\n/** A dependency package a story file reaches, and the first module that reaches it. */\nexport interface PackageUsage {\n /** Story file (repo-relative, as given by the Storybook index). */\n storyFile: string;\n packageName: string;\n /** Graph module inside the package (a `node_modules/<pkg>/…` path or a workspace source file). */\n modulePath: string;\n}\n\n/** Where a tracked package lives: always `node_modules`, plus a workspace dir when linked. */\nexport interface TrackedPackage {\n /** Repo-relative directory of a workspace-linked package (outside `node_modules`). */\n root?: string;\n}\n\nconst NODE_MODULES = \"node_modules/\";\n\n/** Package name for a `…/node_modules/<pkg>/…` module path, handling scopes. */\nfunction nodeModulesPackage(module: string): string | null {\n const start = module.lastIndexOf(NODE_MODULES);\n if (start === -1) {\n return null;\n }\n const [first, second] = module.slice(start + NODE_MODULES.length).split(\"/\");\n if (!first) {\n return null;\n }\n return first.startsWith(\"@\") && second ? `${first}/${second}` : first;\n}\n\n/** Tracked package that owns `module`, via `node_modules` or a workspace root. */\nfunction owningPackage(module: string, tracked: Map<string, TrackedPackage>): string | null {\n const installed = nodeModulesPackage(module);\n if (installed !== null) {\n return tracked.has(installed) ? installed : null;\n }\n for (const [name, { root }] of tracked) {\n if (root && module.startsWith(`${root}/`)) {\n return name;\n }\n }\n return null;\n}\n\n/** Mutable state of one story's breadth-first walk. */\ninterface Walk {\n seen: Set<string>;\n found: Map<string, PackageUsage>;\n next: string[];\n}\n\n/** Classify one imported module: record a package hit, or queue it for expansion. */\nfunction visitDep(\n walk: Walk,\n dep: string,\n storyFile: string,\n tracked: Map<string, TrackedPackage>,\n): void {\n if (walk.seen.has(dep)) {\n return;\n }\n walk.seen.add(dep);\n const owner = owningPackage(dep, tracked);\n if (owner === null) {\n walk.next.push(dep);\n } else if (!walk.found.has(owner)) {\n walk.found.set(owner, { storyFile, packageName: owner, modulePath: dep });\n }\n}\n\n/** Breadth-first walk of one story's imports, recording the shallowest hit per package. */\nfunction usageForStory(\n graph: DepGraph,\n storyFile: string,\n start: string[],\n tracked: Map<string, TrackedPackage>,\n): PackageUsage[] {\n const walk: Walk = { seen: new Set(start), found: new Map(), next: [] };\n let frontier = start;\n while (frontier.length > 0) {\n walk.next = [];\n for (const dep of frontier.flatMap((module) => graph.imports[module] ?? [])) {\n visitDep(walk, dep, storyFile, tracked);\n }\n frontier = walk.next;\n }\n return [...walk.found.values()];\n}\n\n/**\n * Find which tracked dependency packages each story reaches through its\n * import graph. Traversal stops at a package boundary (its internals are not\n * walked) and records only the shallowest module per package, so a story\n * yields at most one row per package.\n *\n * @param graph - Bidirectional module graph from the build's stats file.\n * @param storyFiles - Story import paths from the Storybook index.\n * @param packages - Packages to track (e.g. the project's declared dependencies).\n */\nexport function extractPackageUsage(\n graph: DepGraph,\n storyFiles: readonly string[],\n packages: Record<string, TrackedPackage>,\n): PackageUsage[] {\n const tracked = new Map(Object.entries(packages));\n return storyFiles.flatMap((storyFile) =>\n usageForStory(graph, storyFile, graphModulesFor(graph, storyFile), tracked),\n );\n}\n"],"mappings":";;;;;AAkBA,MAAM,eAAe;;AAGrB,SAAS,mBAAmB,QAA+B;CACzD,MAAM,QAAQ,OAAO,YAAY,YAAY;CAC7C,IAAI,UAAU,IACZ,OAAO;CAET,MAAM,CAAC,OAAO,UAAU,OAAO,MAAM,QAAQ,EAAmB,CAAC,CAAC,MAAM,GAAG;CAC3E,IAAI,CAAC,OACH,OAAO;CAET,OAAO,MAAM,WAAW,GAAG,KAAK,SAAS,GAAG,MAAM,GAAG,WAAW;AAClE;;AAGA,SAAS,cAAc,QAAgB,SAAqD;CAC1F,MAAM,YAAY,mBAAmB,MAAM;CAC3C,IAAI,cAAc,MAChB,OAAO,QAAQ,IAAI,SAAS,IAAI,YAAY;CAE9C,KAAK,MAAM,CAAC,MAAM,EAAE,WAAW,SAC7B,IAAI,QAAQ,OAAO,WAAW,GAAG,KAAK,EAAE,GACtC,OAAO;CAGX,OAAO;AACT;;AAUA,SAAS,SACP,MACA,KACA,WACA,SACM;CACN,IAAI,KAAK,KAAK,IAAI,GAAG,GACnB;CAEF,KAAK,KAAK,IAAI,GAAG;CACjB,MAAM,QAAQ,cAAc,KAAK,OAAO;CACxC,IAAI,UAAU,MACZ,KAAK,KAAK,KAAK,GAAG;MACb,IAAI,CAAC,KAAK,MAAM,IAAI,KAAK,GAC9B,KAAK,MAAM,IAAI,OAAO;EAAE;EAAW,aAAa;EAAO,YAAY;CAAI,CAAC;AAE5E;;AAGA,SAAS,cACP,OACA,WACA,OACA,SACgB;CAChB,MAAM,OAAa;EAAE,MAAM,IAAI,IAAI,KAAK;EAAG,uBAAO,IAAI,IAAI;EAAG,MAAM,CAAC;CAAE;CACtE,IAAI,WAAW;CACf,OAAO,SAAS,SAAS,GAAG;EAC1B,KAAK,OAAO,CAAC;EACb,KAAK,MAAM,OAAO,SAAS,SAAS,WAAW,MAAM,QAAQ,WAAW,CAAC,CAAC,GACxE,SAAS,MAAM,KAAK,WAAW,OAAO;EAExC,WAAW,KAAK;CAClB;CACA,OAAO,CAAC,GAAG,KAAK,MAAM,OAAO,CAAC;AAChC;;;;;;;;;;;AAYA,SAAgB,oBACd,OACA,YACA,UACgB;CAChB,MAAM,UAAU,IAAI,IAAI,OAAO,QAAQ,QAAQ,CAAC;CAChD,OAAO,WAAW,SAAS,cACzB,cAAc,OAAO,WAAW,gBAAgB,OAAO,SAAS,GAAG,OAAO,CAC5E;AACF"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@storyshelf/affected",
3
- "version": "0.6.2",
3
+ "version": "0.7.0",
4
4
  "description": "Affected capture for StoryShelf: dependency-graph tracing that selects the stories impacted by a change.",
5
5
  "keywords": [
6
6
  "affected",
@@ -27,9 +27,6 @@
27
27
  "dist"
28
28
  ],
29
29
  "type": "module",
30
- "engines": {
31
- "node": ">=24"
32
- },
33
30
  "sideEffects": false,
34
31
  "types": "./dist/index.d.mts",
35
32
  "exports": {
@@ -65,6 +62,9 @@
65
62
  "typescript": "^7.0.2",
66
63
  "vitest": "^5.0.0"
67
64
  },
65
+ "engines": {
66
+ "node": ">=24"
67
+ },
68
68
  "jsr": {
69
69
  "runtimeCompat": {
70
70
  "node": true,