@storyshelf/affected 0.5.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/LICENSE +21 -0
- package/README.md +29 -0
- package/dist/git.d.mts +47 -0
- package/dist/git.mjs +121 -0
- package/dist/git.mjs.map +1 -0
- package/dist/index.d.mts +19 -0
- package/dist/index.mjs +75 -0
- package/dist/index.mjs.map +1 -0
- package/dist/select.d.mts +24 -0
- package/dist/select.mjs +44 -0
- package/dist/select.mjs.map +1 -0
- package/dist/stats.d.mts +47 -0
- package/dist/stats.mjs +161 -0
- package/dist/stats.mjs.map +1 -0
- package/dist/trace.d.mts +19 -0
- package/dist/trace.mjs +166 -0
- package/dist/trace.mjs.map +1 -0
- package/dist/types.d.mts +57 -0
- package/package.json +74 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Siddhant Gupta
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# @storyshelf/affected
|
|
2
|
+
|
|
3
|
+
Affected capture for StoryShelf: traces the dependency graph of a change to select only the stories it impacts.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
nub add @storyshelf/affected
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
or
|
|
12
|
+
|
|
13
|
+
```sh
|
|
14
|
+
npm install @storyshelf/affected
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Usage
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
import { computeAffected } from "@storyshelf/affected";
|
|
21
|
+
|
|
22
|
+
const result = await computeAffected({ cwd, buildDir, baseSha, headSha });
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
See the [StoryShelf documentation](https://github.com/GuptaSiddhant/StoryShelf#readme) for configuration details.
|
|
26
|
+
|
|
27
|
+
## License
|
|
28
|
+
|
|
29
|
+
MIT
|
package/dist/git.d.mts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import __tsdown_shims_path from 'node:path';
|
|
2
|
+
import __tsdown_shims_url from 'node:url';
|
|
3
|
+
//#region src/git.d.ts
|
|
4
|
+
/** Upper bound on changed files before falling back to a full render. */
|
|
5
|
+
declare const MAX_CHANGED_FILES = 10000;
|
|
6
|
+
/** Prefix marking CLI-synthesized identities for checkouts without git. */
|
|
7
|
+
declare const LOCAL_SHA_PREFIX = "local-";
|
|
8
|
+
/** Default branch for synthesized identities (isolates local experiments). */
|
|
9
|
+
declare const LOCAL_BRANCH = "local";
|
|
10
|
+
/** Repository git status relevant to affected capture. */
|
|
11
|
+
type GitRepoStatus = "ok" | "shallow" | "no-git";
|
|
12
|
+
/**
|
|
13
|
+
* Probe whether the checkout can support diffing: a full repo (`ok`), a
|
|
14
|
+
* shallow clone (`shallow`), or missing git / not a repository (`no-git`).
|
|
15
|
+
*/
|
|
16
|
+
declare function gitRepoStatus(cwd: string): GitRepoStatus;
|
|
17
|
+
/**
|
|
18
|
+
* True when the checkout cannot support diffing (shallow clone, missing git,
|
|
19
|
+
* or not a repository). Callers treat this as "render everything".
|
|
20
|
+
*
|
|
21
|
+
* @param cwd - Repository root to probe.
|
|
22
|
+
*/
|
|
23
|
+
declare function isShallowRepo(cwd: string): boolean;
|
|
24
|
+
/**
|
|
25
|
+
* Repo-relative files changed between `base` and `head`, including staged
|
|
26
|
+
* and untracked working-tree files. Throws when git cannot diff.
|
|
27
|
+
*
|
|
28
|
+
* @param cwd - Repository root.
|
|
29
|
+
* @param base - Ancestor commit (baseline).
|
|
30
|
+
* @param head - Current commit.
|
|
31
|
+
*/
|
|
32
|
+
declare function changedFiles(cwd: string, base: string, head: string): string[];
|
|
33
|
+
/**
|
|
34
|
+
* Current commit of the checkout, or `null` when git is missing, `cwd` is
|
|
35
|
+
* not a repository, or HEAD is unborn.
|
|
36
|
+
*/
|
|
37
|
+
declare function gitHeadSha(cwd: string): string | null;
|
|
38
|
+
/**
|
|
39
|
+
* Current branch of the checkout, or `null` when detached, unborn, or git
|
|
40
|
+
* is unavailable.
|
|
41
|
+
*/
|
|
42
|
+
declare function gitBranchName(cwd: string): string | null;
|
|
43
|
+
/** Synthesize a unique local sha for checkouts without git. */
|
|
44
|
+
declare function localSha(): string;
|
|
45
|
+
//#endregion
|
|
46
|
+
export { GitRepoStatus, LOCAL_BRANCH, LOCAL_SHA_PREFIX, MAX_CHANGED_FILES, changedFiles, gitBranchName, gitHeadSha, gitRepoStatus, isShallowRepo, localSha };
|
|
47
|
+
//# sourceMappingURL=git.d.mts.map
|
package/dist/git.mjs
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
import { execFileSync } from "node:child_process";
|
|
5
|
+
import { randomUUID } from "node:crypto";
|
|
6
|
+
//#region src/git.ts
|
|
7
|
+
/** Upper bound on changed files before falling back to a full render. */
|
|
8
|
+
const MAX_CHANGED_FILES = 1e4;
|
|
9
|
+
/** Prefix marking CLI-synthesized identities for checkouts without git. */
|
|
10
|
+
const LOCAL_SHA_PREFIX = "local-";
|
|
11
|
+
/** Default branch for synthesized identities (isolates local experiments). */
|
|
12
|
+
const LOCAL_BRANCH = "local";
|
|
13
|
+
/** Run a git subcommand, returning trimmed stdout. Throws on failure. */
|
|
14
|
+
function git(cwd, args) {
|
|
15
|
+
return execFileSync("git", args, {
|
|
16
|
+
cwd,
|
|
17
|
+
encoding: "utf8"
|
|
18
|
+
}).trim();
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Probe whether the checkout can support diffing: a full repo (`ok`), a
|
|
22
|
+
* shallow clone (`shallow`), or missing git / not a repository (`no-git`).
|
|
23
|
+
*/
|
|
24
|
+
function gitRepoStatus(cwd) {
|
|
25
|
+
let shallow;
|
|
26
|
+
try {
|
|
27
|
+
shallow = git(cwd, ["rev-parse", "--is-shallow-repository"]);
|
|
28
|
+
} catch {
|
|
29
|
+
return "no-git";
|
|
30
|
+
}
|
|
31
|
+
return shallow === "true" ? "shallow" : "ok";
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* True when the checkout cannot support diffing (shallow clone, missing git,
|
|
35
|
+
* or not a repository). Callers treat this as "render everything".
|
|
36
|
+
*
|
|
37
|
+
* @param cwd - Repository root to probe.
|
|
38
|
+
*/
|
|
39
|
+
function isShallowRepo(cwd) {
|
|
40
|
+
return gitRepoStatus(cwd) !== "ok";
|
|
41
|
+
}
|
|
42
|
+
/** Split `git --name-only` output into a deduplicated file list. */
|
|
43
|
+
function parseNameOnly(output) {
|
|
44
|
+
const files = /* @__PURE__ */ new Set();
|
|
45
|
+
for (const line of output.split("\n")) {
|
|
46
|
+
const trimmed = line.trim();
|
|
47
|
+
if (trimmed.length > 0) files.add(trimmed);
|
|
48
|
+
}
|
|
49
|
+
return [...files];
|
|
50
|
+
}
|
|
51
|
+
/** Committed changes between two revisions (two-dot range). */
|
|
52
|
+
function committedChanges(cwd, base, head) {
|
|
53
|
+
return parseNameOnly(git(cwd, [
|
|
54
|
+
"diff",
|
|
55
|
+
"--name-only",
|
|
56
|
+
`${base}..${head}`
|
|
57
|
+
]));
|
|
58
|
+
}
|
|
59
|
+
/** Staged and unstaged changes plus untracked files (never committed). */
|
|
60
|
+
function workingTreeChanges(cwd) {
|
|
61
|
+
const againstHead = parseNameOnly(git(cwd, [
|
|
62
|
+
"diff",
|
|
63
|
+
"--name-only",
|
|
64
|
+
"HEAD"
|
|
65
|
+
]));
|
|
66
|
+
const untracked = parseNameOnly(git(cwd, [
|
|
67
|
+
"ls-files",
|
|
68
|
+
"--others",
|
|
69
|
+
"--exclude-standard"
|
|
70
|
+
]));
|
|
71
|
+
return [.../* @__PURE__ */ new Set([...againstHead, ...untracked])];
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Repo-relative files changed between `base` and `head`, including staged
|
|
75
|
+
* and untracked working-tree files. Throws when git cannot diff.
|
|
76
|
+
*
|
|
77
|
+
* @param cwd - Repository root.
|
|
78
|
+
* @param base - Ancestor commit (baseline).
|
|
79
|
+
* @param head - Current commit.
|
|
80
|
+
*/
|
|
81
|
+
function changedFiles(cwd, base, head) {
|
|
82
|
+
return [.../* @__PURE__ */ new Set([...committedChanges(cwd, base, head), ...workingTreeChanges(cwd)])].toSorted();
|
|
83
|
+
}
|
|
84
|
+
/** Run a git subcommand, returning trimmed stdout or `null` on any failure. */
|
|
85
|
+
function tryGit(cwd, args) {
|
|
86
|
+
try {
|
|
87
|
+
const output = git(cwd, args);
|
|
88
|
+
return output.length > 0 ? output : null;
|
|
89
|
+
} catch {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Current commit of the checkout, or `null` when git is missing, `cwd` is
|
|
95
|
+
* not a repository, or HEAD is unborn.
|
|
96
|
+
*/
|
|
97
|
+
function gitHeadSha(cwd) {
|
|
98
|
+
return tryGit(cwd, ["rev-parse", "HEAD"]);
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Current branch of the checkout, or `null` when detached, unborn, or git
|
|
102
|
+
* is unavailable.
|
|
103
|
+
*/
|
|
104
|
+
function gitBranchName(cwd) {
|
|
105
|
+
const branch = tryGit(cwd, ["branch", "--show-current"]);
|
|
106
|
+
if (branch) return branch;
|
|
107
|
+
const abbrev = tryGit(cwd, [
|
|
108
|
+
"rev-parse",
|
|
109
|
+
"--abbrev-ref",
|
|
110
|
+
"HEAD"
|
|
111
|
+
]);
|
|
112
|
+
return abbrev && abbrev !== "HEAD" ? abbrev : null;
|
|
113
|
+
}
|
|
114
|
+
/** Synthesize a unique local sha for checkouts without git. */
|
|
115
|
+
function localSha() {
|
|
116
|
+
return `${LOCAL_SHA_PREFIX}${randomUUID().replaceAll("-", "").slice(0, 12)}`;
|
|
117
|
+
}
|
|
118
|
+
//#endregion
|
|
119
|
+
export { LOCAL_BRANCH, LOCAL_SHA_PREFIX, MAX_CHANGED_FILES, changedFiles, gitBranchName, gitHeadSha, gitRepoStatus, isShallowRepo, localSha };
|
|
120
|
+
|
|
121
|
+
//# sourceMappingURL=git.mjs.map
|
package/dist/git.mjs.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"git.mjs","names":[],"sources":["../src/git.ts"],"sourcesContent":["import { execFileSync } from \"node:child_process\";\nimport { randomUUID } from \"node:crypto\";\n\n/** Upper bound on changed files before falling back to a full render. */\nexport const MAX_CHANGED_FILES = 10_000;\n\n/** Prefix marking CLI-synthesized identities for checkouts without git. */\nexport const LOCAL_SHA_PREFIX = \"local-\";\n\n/** Default branch for synthesized identities (isolates local experiments). */\nexport const LOCAL_BRANCH = \"local\";\n\n/** Run a git subcommand, returning trimmed stdout. Throws on failure. */\nfunction git(cwd: string, args: string[]): string {\n return execFileSync(\"git\", args, { cwd, encoding: \"utf8\" }).trim();\n}\n\n/** Repository git status relevant to affected capture. */\nexport type GitRepoStatus = \"ok\" | \"shallow\" | \"no-git\";\n/**\n * Probe whether the checkout can support diffing: a full repo (`ok`), a\n * shallow clone (`shallow`), or missing git / not a repository (`no-git`).\n */\nexport function gitRepoStatus(cwd: string): GitRepoStatus {\n let shallow: string | null;\n try {\n shallow = git(cwd, [\"rev-parse\", \"--is-shallow-repository\"]);\n } catch {\n return \"no-git\";\n }\n return shallow === \"true\" ? \"shallow\" : \"ok\";\n}\n\n/**\n * True when the checkout cannot support diffing (shallow clone, missing git,\n * or not a repository). Callers treat this as \"render everything\".\n *\n * @param cwd - Repository root to probe.\n */\nexport function isShallowRepo(cwd: string): boolean {\n return gitRepoStatus(cwd) !== \"ok\";\n}\n\n/** Split `git --name-only` output into a deduplicated file list. */\nfunction parseNameOnly(output: string): string[] {\n const files = new Set<string>();\n for (const line of output.split(\"\\n\")) {\n const trimmed = line.trim();\n if (trimmed.length > 0) {\n files.add(trimmed);\n }\n }\n return [...files];\n}\n\n/** Committed changes between two revisions (two-dot range). */\nfunction committedChanges(cwd: string, base: string, head: string): string[] {\n return parseNameOnly(git(cwd, [\"diff\", \"--name-only\", `${base}..${head}`]));\n}\n\n/** Staged and unstaged changes plus untracked files (never committed). */\nfunction workingTreeChanges(cwd: string): string[] {\n const againstHead = parseNameOnly(git(cwd, [\"diff\", \"--name-only\", \"HEAD\"]));\n const untracked = parseNameOnly(git(cwd, [\"ls-files\", \"--others\", \"--exclude-standard\"]));\n return [...new Set([...againstHead, ...untracked])];\n}\n\n/**\n * Repo-relative files changed between `base` and `head`, including staged\n * and untracked working-tree files. Throws when git cannot diff.\n *\n * @param cwd - Repository root.\n * @param base - Ancestor commit (baseline).\n * @param head - Current commit.\n */\nexport function changedFiles(cwd: string, base: string, head: string): string[] {\n const files = new Set([...committedChanges(cwd, base, head), ...workingTreeChanges(cwd)]);\n return [...files].toSorted();\n}\n\n/** Run a git subcommand, returning trimmed stdout or `null` on any failure. */\nfunction tryGit(cwd: string, args: string[]): string | null {\n try {\n const output = git(cwd, args);\n return output.length > 0 ? output : null;\n } catch {\n return null;\n }\n}\n\n/**\n * Current commit of the checkout, or `null` when git is missing, `cwd` is\n * not a repository, or HEAD is unborn.\n */\nexport function gitHeadSha(cwd: string): string | null {\n return tryGit(cwd, [\"rev-parse\", \"HEAD\"]);\n}\n\n/**\n * Current branch of the checkout, or `null` when detached, unborn, or git\n * is unavailable.\n */\nexport function gitBranchName(cwd: string): string | null {\n const branch = tryGit(cwd, [\"branch\", \"--show-current\"]);\n if (branch) {\n return branch;\n }\n const abbrev = tryGit(cwd, [\"rev-parse\", \"--abbrev-ref\", \"HEAD\"]);\n return abbrev && abbrev !== \"HEAD\" ? abbrev : null;\n}\n\n/** Synthesize a unique local sha for checkouts without git. */\nexport function localSha(): string {\n return `${LOCAL_SHA_PREFIX}${randomUUID().replaceAll(\"-\", \"\").slice(0, 12)}`;\n}\n"],"mappings":";;;;;;;AAIA,MAAa,oBAAoB;;AAGjC,MAAa,mBAAmB;;AAGhC,MAAa,eAAe;;AAG5B,SAAS,IAAI,KAAa,MAAwB;CAChD,OAAO,aAAa,OAAO,MAAM;EAAE;EAAK,UAAU;CAAO,CAAC,CAAC,CAAC,KAAK;AACnE;;;;;AAQA,SAAgB,cAAc,KAA4B;CACxD,IAAI;CACJ,IAAI;EACF,UAAU,IAAI,KAAK,CAAC,aAAa,yBAAyB,CAAC;CAC7D,QAAQ;EACN,OAAO;CACT;CACA,OAAO,YAAY,SAAS,YAAY;AAC1C;;;;;;;AAQA,SAAgB,cAAc,KAAsB;CAClD,OAAO,cAAc,GAAG,MAAM;AAChC;;AAGA,SAAS,cAAc,QAA0B;CAC/C,MAAM,wBAAQ,IAAI,IAAY;CAC9B,KAAK,MAAM,QAAQ,OAAO,MAAM,IAAI,GAAG;EACrC,MAAM,UAAU,KAAK,KAAK;EAC1B,IAAI,QAAQ,SAAS,GACnB,MAAM,IAAI,OAAO;CAErB;CACA,OAAO,CAAC,GAAG,KAAK;AAClB;;AAGA,SAAS,iBAAiB,KAAa,MAAc,MAAwB;CAC3E,OAAO,cAAc,IAAI,KAAK;EAAC;EAAQ;EAAe,GAAG,KAAK,IAAI;CAAM,CAAC,CAAC;AAC5E;;AAGA,SAAS,mBAAmB,KAAuB;CACjD,MAAM,cAAc,cAAc,IAAI,KAAK;EAAC;EAAQ;EAAe;CAAM,CAAC,CAAC;CAC3E,MAAM,YAAY,cAAc,IAAI,KAAK;EAAC;EAAY;EAAY;CAAoB,CAAC,CAAC;CACxF,OAAO,CAAC,mBAAG,IAAI,IAAI,CAAC,GAAG,aAAa,GAAG,SAAS,CAAC,CAAC;AACpD;;;;;;;;;AAUA,SAAgB,aAAa,KAAa,MAAc,MAAwB;CAE9E,OAAO,CAAC,mBAAG,IADO,IAAI,CAAC,GAAG,iBAAiB,KAAK,MAAM,IAAI,GAAG,GAAG,mBAAmB,GAAG,CAAC,CACxE,CAAC,CAAC,CAAC,SAAS;AAC7B;;AAGA,SAAS,OAAO,KAAa,MAA+B;CAC1D,IAAI;EACF,MAAM,SAAS,IAAI,KAAK,IAAI;EAC5B,OAAO,OAAO,SAAS,IAAI,SAAS;CACtC,QAAQ;EACN,OAAO;CACT;AACF;;;;;AAMA,SAAgB,WAAW,KAA4B;CACrD,OAAO,OAAO,KAAK,CAAC,aAAa,MAAM,CAAC;AAC1C;;;;;AAMA,SAAgB,cAAc,KAA4B;CACxD,MAAM,SAAS,OAAO,KAAK,CAAC,UAAU,gBAAgB,CAAC;CACvD,IAAI,QACF,OAAO;CAET,MAAM,SAAS,OAAO,KAAK;EAAC;EAAa;EAAgB;CAAM,CAAC;CAChE,OAAO,UAAU,WAAW,SAAS,SAAS;AAChD;;AAGA,SAAgB,WAAmB;CACjC,OAAO,GAAG,mBAAmB,WAAW,CAAC,CAAC,WAAW,KAAK,EAAE,CAAC,CAAC,MAAM,GAAG,EAAE;AAC3E"}
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import __tsdown_shims_path from 'node:path';
|
|
2
|
+
import __tsdown_shims_url from 'node:url';
|
|
3
|
+
import { AffectedInput, AffectedResult, DepGraph, StoryLike, TraceOptions } from "./types.mjs";
|
|
4
|
+
import { GitRepoStatus, LOCAL_BRANCH, LOCAL_SHA_PREFIX, MAX_CHANGED_FILES, changedFiles, gitBranchName, gitHeadSha, gitRepoStatus, isShallowRepo, localSha } from "./git.mjs";
|
|
5
|
+
import { STATS_FILENAME, buildGraph, graphModulesFor, loadDepGraph, loadStoryImportPaths, normalizePath, statsPathFor } from "./stats.mjs";
|
|
6
|
+
import { matchesAnyGlob, traceAffected } from "./trace.mjs";
|
|
7
|
+
import { StoryPartition, selectAffectedStories } from "./select.mjs";
|
|
8
|
+
//#region src/index.d.ts
|
|
9
|
+
/**
|
|
10
|
+
* Compute the affected story set for an upload. Never throws: every failure
|
|
11
|
+
* degrades to a full render with a machine-readable `fullReason`, so
|
|
12
|
+
* affected capture can stay enabled by default without new failure modes.
|
|
13
|
+
*
|
|
14
|
+
* @param input - Repository root, build dir, revisions, and trace options.
|
|
15
|
+
*/
|
|
16
|
+
declare function computeAffected(input: AffectedInput): Promise<AffectedResult>;
|
|
17
|
+
//#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
|
+
//# sourceMappingURL=index.d.mts.map
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
import { LOCAL_BRANCH, LOCAL_SHA_PREFIX, MAX_CHANGED_FILES, changedFiles, gitBranchName, gitHeadSha, gitRepoStatus, isShallowRepo, localSha } from "./git.mjs";
|
|
5
|
+
import { STATS_FILENAME, buildGraph, graphModulesFor, loadDepGraph, loadStoryImportPaths, normalizePath, statsPathFor } from "./stats.mjs";
|
|
6
|
+
import { matchesAnyGlob, traceAffected } from "./trace.mjs";
|
|
7
|
+
import { selectAffectedStories } from "./select.mjs";
|
|
8
|
+
//#region src/index.ts
|
|
9
|
+
/**
|
|
10
|
+
* Compute the affected story set for an upload. Never throws: every failure
|
|
11
|
+
* degrades to a full render with a machine-readable `fullReason`, so
|
|
12
|
+
* affected capture can stay enabled by default without new failure modes.
|
|
13
|
+
*
|
|
14
|
+
* @param input - Repository root, build dir, revisions, and trace options.
|
|
15
|
+
*/
|
|
16
|
+
async function computeAffected(input) {
|
|
17
|
+
try {
|
|
18
|
+
return await computeSelective(input);
|
|
19
|
+
} catch {
|
|
20
|
+
return fullResult([], input.baseSha, "affected-computation-failed");
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
/** Full-render result with a reason. */
|
|
24
|
+
function fullResult(changed, baselineSha, reason) {
|
|
25
|
+
return {
|
|
26
|
+
affectedImportPaths: null,
|
|
27
|
+
changedFiles: changed,
|
|
28
|
+
baselineSha,
|
|
29
|
+
fullReason: reason
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
/** Selective result for an affected import-path set. */
|
|
33
|
+
function selectiveResult(changed, baselineSha, paths) {
|
|
34
|
+
return {
|
|
35
|
+
affectedImportPaths: paths,
|
|
36
|
+
changedFiles: changed,
|
|
37
|
+
baselineSha,
|
|
38
|
+
fullReason: null
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
/** Guarded changed-file list, throwing a full-render reason when unusable. */
|
|
42
|
+
function guardedChanged(input) {
|
|
43
|
+
const repo = gitRepoStatus(input.cwd);
|
|
44
|
+
if (repo !== "ok") throw new Error(repo === "shallow" ? "shallow-clone" : "not-a-git-repo");
|
|
45
|
+
const changed = changedFiles(input.cwd, input.baseSha, input.headSha);
|
|
46
|
+
if (changed.length > 1e4) throw new Error("too-many-changed-files");
|
|
47
|
+
return changed;
|
|
48
|
+
}
|
|
49
|
+
/** Trace the affected set once inputs are validated. Throws reason strings. */
|
|
50
|
+
async function traceSelective(input, changed) {
|
|
51
|
+
if (changed.length === 0) return selectiveResult(changed, input.baseSha, []);
|
|
52
|
+
const stories = await loadStoryImportPaths(input.cwd, input.buildDir).catch(() => {
|
|
53
|
+
throw new Error("story-index-unreadable");
|
|
54
|
+
});
|
|
55
|
+
const graph = await loadDepGraph(statsPathFor(input.cwd, input.buildDir, input.statsFile));
|
|
56
|
+
if (!graph) throw new Error("dependency-graph-unavailable");
|
|
57
|
+
const affected = traceAffected(graph, changed, stories, { untraced: input.untraced });
|
|
58
|
+
if (affected === null) throw new Error("global-file-changed");
|
|
59
|
+
return selectiveResult(changed, input.baseSha, affected);
|
|
60
|
+
}
|
|
61
|
+
/** Selective computation with reason-string failures mapped to full renders. */
|
|
62
|
+
async function computeSelective(input) {
|
|
63
|
+
let changed = [];
|
|
64
|
+
try {
|
|
65
|
+
changed = guardedChanged(input);
|
|
66
|
+
return await traceSelective(input, changed);
|
|
67
|
+
} catch (error) {
|
|
68
|
+
const reason = error instanceof Error ? error.message : "affected-computation-failed";
|
|
69
|
+
return fullResult(changed, input.baseSha, reason);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
//#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
|
+
|
|
75
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +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"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import __tsdown_shims_path from 'node:path';
|
|
2
|
+
import __tsdown_shims_url from 'node:url';
|
|
3
|
+
import { StoryLike } from "./types.mjs";
|
|
4
|
+
//#region src/select.d.ts
|
|
5
|
+
/** Stories split into render-now versus inherit-from-baseline sets. */
|
|
6
|
+
interface StoryPartition<T> {
|
|
7
|
+
/** Stories to capture in this build. */
|
|
8
|
+
render: T[];
|
|
9
|
+
/** Stories to inherit unchanged from their baseline. */
|
|
10
|
+
inherit: T[];
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Partition stories into render versus inherit sets.
|
|
14
|
+
*
|
|
15
|
+
* Stories without an import path always render (they cannot be matched).
|
|
16
|
+
* A `null` affected set renders everything (full capture).
|
|
17
|
+
*
|
|
18
|
+
* @param stories - Discovered stories for the build.
|
|
19
|
+
* @param affectedImportPaths - Affected import paths, or `null` for full.
|
|
20
|
+
*/
|
|
21
|
+
declare function selectAffectedStories<T extends StoryLike>(stories: readonly T[], affectedImportPaths: readonly string[] | null): StoryPartition<T>;
|
|
22
|
+
//#endregion
|
|
23
|
+
export { StoryPartition, selectAffectedStories };
|
|
24
|
+
//# sourceMappingURL=select.d.mts.map
|
package/dist/select.mjs
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
import { normalizePath } from "./stats.mjs";
|
|
5
|
+
//#region src/select.ts
|
|
6
|
+
/** Sort stories into render versus inherit buckets for an affected set. */
|
|
7
|
+
function partitionByPath(stories, affected, render, inherit) {
|
|
8
|
+
for (const story of stories) {
|
|
9
|
+
const path = storyPath(story);
|
|
10
|
+
if (path === null || affected.has(path)) render.push(story);
|
|
11
|
+
else inherit.push(story);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
/** Import path of a story record, normalized for affected comparison. */
|
|
15
|
+
function storyPath(story) {
|
|
16
|
+
if (typeof story.importPath !== "string" || story.importPath.length === 0) return null;
|
|
17
|
+
return normalizePath(story.importPath);
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Partition stories into render versus inherit sets.
|
|
21
|
+
*
|
|
22
|
+
* Stories without an import path always render (they cannot be matched).
|
|
23
|
+
* A `null` affected set renders everything (full capture).
|
|
24
|
+
*
|
|
25
|
+
* @param stories - Discovered stories for the build.
|
|
26
|
+
* @param affectedImportPaths - Affected import paths, or `null` for full.
|
|
27
|
+
*/
|
|
28
|
+
function selectAffectedStories(stories, affectedImportPaths) {
|
|
29
|
+
const render = [];
|
|
30
|
+
const inherit = [];
|
|
31
|
+
if (affectedImportPaths === null) return {
|
|
32
|
+
render: [...stories],
|
|
33
|
+
inherit
|
|
34
|
+
};
|
|
35
|
+
partitionByPath(stories, new Set(affectedImportPaths.map((path) => normalizePath(path))), render, inherit);
|
|
36
|
+
return {
|
|
37
|
+
render,
|
|
38
|
+
inherit
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
//#endregion
|
|
42
|
+
export { selectAffectedStories };
|
|
43
|
+
|
|
44
|
+
//# sourceMappingURL=select.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"select.mjs","names":[],"sources":["../src/select.ts"],"sourcesContent":["import { normalizePath } from \"./stats.ts\";\nimport type { StoryLike } from \"./types.ts\";\n\n/** Stories split into render-now versus inherit-from-baseline sets. */\nexport interface StoryPartition<T> {\n /** Stories to capture in this build. */\n render: T[];\n /** Stories to inherit unchanged from their baseline. */\n inherit: T[];\n}\n\n/** Sort stories into render versus inherit buckets for an affected set. */\nfunction partitionByPath<T extends StoryLike>(\n stories: readonly T[],\n affected: ReadonlySet<string>,\n render: T[],\n inherit: T[],\n): void {\n for (const story of stories) {\n const path = storyPath(story);\n if (path === null || affected.has(path)) {\n render.push(story);\n } else {\n inherit.push(story);\n }\n }\n}\n\n/** Import path of a story record, normalized for affected comparison. */\nfunction storyPath(story: StoryLike): string | null {\n if (typeof story.importPath !== \"string\" || story.importPath.length === 0) {\n return null;\n }\n return normalizePath(story.importPath);\n}\n\n/**\n * Partition stories into render versus inherit sets.\n *\n * Stories without an import path always render (they cannot be matched).\n * A `null` affected set renders everything (full capture).\n *\n * @param stories - Discovered stories for the build.\n * @param affectedImportPaths - Affected import paths, or `null` for full.\n */\nexport function selectAffectedStories<T extends StoryLike>(\n stories: readonly T[],\n affectedImportPaths: readonly string[] | null,\n): StoryPartition<T> {\n const render: T[] = [];\n const inherit: T[] = [];\n if (affectedImportPaths === null) {\n return { render: [...stories], inherit };\n }\n partitionByPath(\n stories,\n new Set(affectedImportPaths.map((path) => normalizePath(path))),\n render,\n inherit,\n );\n return { render, inherit };\n}\n"],"mappings":";;;;;;AAYA,SAAS,gBACP,SACA,UACA,QACA,SACM;CACN,KAAK,MAAM,SAAS,SAAS;EAC3B,MAAM,OAAO,UAAU,KAAK;EAC5B,IAAI,SAAS,QAAQ,SAAS,IAAI,IAAI,GACpC,OAAO,KAAK,KAAK;OAEjB,QAAQ,KAAK,KAAK;CAEtB;AACF;;AAGA,SAAS,UAAU,OAAiC;CAClD,IAAI,OAAO,MAAM,eAAe,YAAY,MAAM,WAAW,WAAW,GACtE,OAAO;CAET,OAAO,cAAc,MAAM,UAAU;AACvC;;;;;;;;;;AAWA,SAAgB,sBACd,SACA,qBACmB;CACnB,MAAM,SAAc,CAAC;CACrB,MAAM,UAAe,CAAC;CACtB,IAAI,wBAAwB,MAC1B,OAAO;EAAE,QAAQ,CAAC,GAAG,OAAO;EAAG;CAAQ;CAEzC,gBACE,SACA,IAAI,IAAI,oBAAoB,KAAK,SAAS,cAAc,IAAI,CAAC,CAAC,GAC9D,QACA,OACF;CACA,OAAO;EAAE;EAAQ;CAAQ;AAC3B"}
|
package/dist/stats.d.mts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
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/stats.d.ts
|
|
5
|
+
/** Basename of the Vite stats file emitted next to a built Storybook. */
|
|
6
|
+
declare const STATS_FILENAME = "preview-stats.json";
|
|
7
|
+
/** A single module entry in a stats file (Vite or Webpack shape). */
|
|
8
|
+
interface StatsModule {
|
|
9
|
+
id?: unknown;
|
|
10
|
+
name?: unknown;
|
|
11
|
+
identifier?: unknown;
|
|
12
|
+
imported?: unknown;
|
|
13
|
+
importedIds?: unknown;
|
|
14
|
+
dependencies?: unknown;
|
|
15
|
+
reasons?: unknown;
|
|
16
|
+
}
|
|
17
|
+
/** Normalize a stats path to a repo-relative posix form for comparison. */
|
|
18
|
+
declare function normalizePath(value: string): string;
|
|
19
|
+
/**
|
|
20
|
+
* Build a bidirectional dependency graph from parsed stats modules.
|
|
21
|
+
*
|
|
22
|
+
* @param entries - Module entries from a Vite or Webpack stats document.
|
|
23
|
+
* @returns Bidirectional graph, or `null` when no usable edges exist.
|
|
24
|
+
*/
|
|
25
|
+
declare function buildGraph(entries: StatsModule[]): DepGraph | null;
|
|
26
|
+
/**
|
|
27
|
+
* Load and parse a stats file into a dependency graph.
|
|
28
|
+
*
|
|
29
|
+
* @param statsPath - Absolute or cwd-relative path to the stats JSON file.
|
|
30
|
+
* @returns Graph, or `null` when the file is missing or unrecognized.
|
|
31
|
+
*/
|
|
32
|
+
declare function loadDepGraph(statsPath: string): Promise<DepGraph | null>;
|
|
33
|
+
/** Resolve the stats file path for a built Storybook directory. */
|
|
34
|
+
declare function statsPathFor(cwd: string, buildDir: string, override?: string): string;
|
|
35
|
+
/**
|
|
36
|
+
* Load the deduplicated story import paths from a built Storybook's index.
|
|
37
|
+
*
|
|
38
|
+
* @param cwd - Repository root.
|
|
39
|
+
* @param buildDir - Built Storybook directory.
|
|
40
|
+
* @returns Normalized import paths; throws when no index is readable.
|
|
41
|
+
*/
|
|
42
|
+
declare function loadStoryImportPaths(cwd: string, buildDir: string): Promise<string[]>;
|
|
43
|
+
/** Find graph modules referring to repo-relative file `target`. */
|
|
44
|
+
declare function graphModulesFor(graph: DepGraph, target: string): string[];
|
|
45
|
+
//#endregion
|
|
46
|
+
export { STATS_FILENAME, buildGraph, graphModulesFor, loadDepGraph, loadStoryImportPaths, normalizePath, statsPathFor };
|
|
47
|
+
//# sourceMappingURL=stats.d.mts.map
|
package/dist/stats.mjs
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
import { readFile } from "node:fs/promises";
|
|
5
|
+
import { join, resolve } from "node:path";
|
|
6
|
+
//#region src/stats.ts
|
|
7
|
+
/** Basename of the Vite stats file emitted next to a built Storybook. */
|
|
8
|
+
const STATS_FILENAME = "preview-stats.json";
|
|
9
|
+
/** Candidate index files listing a built Storybook's stories. */
|
|
10
|
+
const INDEX_CANDIDATES = ["stories.json", "index.json"];
|
|
11
|
+
/** Normalize a stats path to a repo-relative posix form for comparison. */
|
|
12
|
+
function normalizePath(value) {
|
|
13
|
+
return value.replaceAll("\\", "/").replaceAll(/^\.\//gu, "").replaceAll(/^\//gu, "");
|
|
14
|
+
}
|
|
15
|
+
/** True when stats module `candidate` refers to repo-relative file `target`. */
|
|
16
|
+
function matchesFile(candidate, target) {
|
|
17
|
+
if (candidate === target) return true;
|
|
18
|
+
return candidate.endsWith(`/${target}`);
|
|
19
|
+
}
|
|
20
|
+
/** Coerce an unknown stats value to a list of normalized paths. */
|
|
21
|
+
function asPathList(value) {
|
|
22
|
+
if (!Array.isArray(value)) return [];
|
|
23
|
+
const paths = [];
|
|
24
|
+
for (const entry of value) if (typeof entry === "string" && entry.length > 0) paths.push(normalizePath(entry));
|
|
25
|
+
return paths;
|
|
26
|
+
}
|
|
27
|
+
/** Read a non-empty string field from a stats module entry. */
|
|
28
|
+
function stringField(entry, key) {
|
|
29
|
+
const value = entry[key];
|
|
30
|
+
return typeof value === "string" && value.length > 0 ? value : null;
|
|
31
|
+
}
|
|
32
|
+
/** Read the file a stats module entry refers to, if any. */
|
|
33
|
+
function moduleFile(entry) {
|
|
34
|
+
const raw = stringField(entry, "id") ?? stringField(entry, "name") ?? stringField(entry, "identifier");
|
|
35
|
+
return raw === null ? null : normalizePath(raw);
|
|
36
|
+
}
|
|
37
|
+
/** Forward imports declared by a stats module entry, if any. */
|
|
38
|
+
function moduleImports(entry) {
|
|
39
|
+
return asPathList(entry.imported ?? entry.importedIds ?? entry.dependencies);
|
|
40
|
+
}
|
|
41
|
+
/** Dependent names declared via Webpack-style `reasons`, if any. */
|
|
42
|
+
function moduleReasonNames(entry) {
|
|
43
|
+
if (!Array.isArray(entry.reasons)) return [];
|
|
44
|
+
const names = [];
|
|
45
|
+
for (const reason of entry.reasons) if (typeof reason === "object" && reason !== null) {
|
|
46
|
+
const record = reason;
|
|
47
|
+
const name = record["moduleName"] ?? record["module"];
|
|
48
|
+
if (typeof name === "string" && name.length > 0) names.push(normalizePath(name));
|
|
49
|
+
}
|
|
50
|
+
return names;
|
|
51
|
+
}
|
|
52
|
+
/** Attach a record-shaped module entry to its name. */
|
|
53
|
+
function entryWithName(name, value) {
|
|
54
|
+
if (typeof value === "object" && value !== null) return {
|
|
55
|
+
name,
|
|
56
|
+
...value
|
|
57
|
+
};
|
|
58
|
+
return { name };
|
|
59
|
+
}
|
|
60
|
+
/** Extract module entries from a parsed stats document of unknown shape. */
|
|
61
|
+
function moduleEntries(document) {
|
|
62
|
+
if (typeof document !== "object" || document === null) return [];
|
|
63
|
+
const raw = document["modules"];
|
|
64
|
+
if (Array.isArray(raw)) return raw.filter((entry) => typeof entry === "object");
|
|
65
|
+
if (typeof raw === "object" && raw !== null) return Object.entries(raw).map(([name, value]) => entryWithName(name, value));
|
|
66
|
+
return [];
|
|
67
|
+
}
|
|
68
|
+
/** Record one directed edge in the graph under construction. */
|
|
69
|
+
function addEdge(imports, importedBy, from, to) {
|
|
70
|
+
if (from === to) return;
|
|
71
|
+
const forward = imports.get(from) ?? /* @__PURE__ */ new Set();
|
|
72
|
+
forward.add(to);
|
|
73
|
+
imports.set(from, forward);
|
|
74
|
+
const reverse = importedBy.get(to) ?? /* @__PURE__ */ new Set();
|
|
75
|
+
reverse.add(from);
|
|
76
|
+
importedBy.set(to, reverse);
|
|
77
|
+
}
|
|
78
|
+
/** Add one stats entry's edges to the graph under construction. */
|
|
79
|
+
function addEntryEdges(imports, importedBy, entry) {
|
|
80
|
+
const file = moduleFile(entry);
|
|
81
|
+
if (!file) return;
|
|
82
|
+
for (const imported of moduleImports(entry)) addEdge(imports, importedBy, file, imported);
|
|
83
|
+
for (const dependent of moduleReasonNames(entry)) addEdge(imports, importedBy, dependent, file);
|
|
84
|
+
}
|
|
85
|
+
function toGraph(imports, importedBy) {
|
|
86
|
+
return {
|
|
87
|
+
imports: Object.fromEntries([...imports].map(([file, set]) => [file, [...set].toSorted()])),
|
|
88
|
+
importedBy: Object.fromEntries([...importedBy].map(([file, set]) => [file, [...set].toSorted()]))
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Build a bidirectional dependency graph from parsed stats modules.
|
|
93
|
+
*
|
|
94
|
+
* @param entries - Module entries from a Vite or Webpack stats document.
|
|
95
|
+
* @returns Bidirectional graph, or `null` when no usable edges exist.
|
|
96
|
+
*/
|
|
97
|
+
function buildGraph(entries) {
|
|
98
|
+
const imports = /* @__PURE__ */ new Map();
|
|
99
|
+
const importedBy = /* @__PURE__ */ new Map();
|
|
100
|
+
for (const entry of entries) addEntryEdges(imports, importedBy, entry);
|
|
101
|
+
if (imports.size === 0) return null;
|
|
102
|
+
return toGraph(imports, importedBy);
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Load and parse a stats file into a dependency graph.
|
|
106
|
+
*
|
|
107
|
+
* @param statsPath - Absolute or cwd-relative path to the stats JSON file.
|
|
108
|
+
* @returns Graph, or `null` when the file is missing or unrecognized.
|
|
109
|
+
*/
|
|
110
|
+
async function loadDepGraph(statsPath) {
|
|
111
|
+
let raw;
|
|
112
|
+
try {
|
|
113
|
+
raw = await readFile(statsPath, "utf8");
|
|
114
|
+
} catch {
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
let document;
|
|
118
|
+
try {
|
|
119
|
+
document = JSON.parse(raw);
|
|
120
|
+
} catch {
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
return buildGraph(moduleEntries(document));
|
|
124
|
+
}
|
|
125
|
+
/** Resolve the stats file path for a built Storybook directory. */
|
|
126
|
+
function statsPathFor(cwd, buildDir, override) {
|
|
127
|
+
if (override) return resolve(cwd, override);
|
|
128
|
+
return join(resolve(cwd, buildDir), STATS_FILENAME);
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Load the deduplicated story import paths from a built Storybook's index.
|
|
132
|
+
*
|
|
133
|
+
* @param cwd - Repository root.
|
|
134
|
+
* @param buildDir - Built Storybook directory.
|
|
135
|
+
* @returns Normalized import paths; throws when no index is readable.
|
|
136
|
+
*/
|
|
137
|
+
async function loadStoryImportPaths(cwd, buildDir) {
|
|
138
|
+
const root = resolve(cwd, buildDir);
|
|
139
|
+
const found = (await Promise.all(INDEX_CANDIDATES.map(async (name) => {
|
|
140
|
+
try {
|
|
141
|
+
return JSON.parse(await readFile(join(root, name), "utf8"));
|
|
142
|
+
} catch {
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
}))).find((document) => document !== null);
|
|
146
|
+
if (!found?.entries) throw new Error(`No Storybook index found in ${root}`);
|
|
147
|
+
const paths = /* @__PURE__ */ new Set();
|
|
148
|
+
for (const entry of Object.values(found.entries)) if (typeof entry.importPath === "string" && entry.importPath.length > 0) paths.add(normalizePath(entry.importPath));
|
|
149
|
+
return [...paths].toSorted();
|
|
150
|
+
}
|
|
151
|
+
/** Find graph modules referring to repo-relative file `target`. */
|
|
152
|
+
function graphModulesFor(graph, target) {
|
|
153
|
+
const matched = [];
|
|
154
|
+
for (const file of Object.keys(graph.imports)) if (matchesFile(file, target)) matched.push(file);
|
|
155
|
+
for (const file of Object.keys(graph.importedBy)) if (matchesFile(file, target) && !matched.includes(file)) matched.push(file);
|
|
156
|
+
return matched;
|
|
157
|
+
}
|
|
158
|
+
//#endregion
|
|
159
|
+
export { STATS_FILENAME, buildGraph, graphModulesFor, loadDepGraph, loadStoryImportPaths, normalizePath, statsPathFor };
|
|
160
|
+
|
|
161
|
+
//# sourceMappingURL=stats.mjs.map
|
|
@@ -0,0 +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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"}
|
package/dist/trace.d.mts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import __tsdown_shims_path from 'node:path';
|
|
2
|
+
import __tsdown_shims_url from 'node:url';
|
|
3
|
+
import { DepGraph, TraceOptions } from "./types.mjs";
|
|
4
|
+
//#region src/trace.d.ts
|
|
5
|
+
/** True when `path` matches any of the given glob patterns. */
|
|
6
|
+
declare function matchesAnyGlob(path: string, patterns: readonly string[]): boolean;
|
|
7
|
+
/**
|
|
8
|
+
* Trace changed files through the dependency graph to story files.
|
|
9
|
+
*
|
|
10
|
+
* @param graph - Bidirectional module graph of the built Storybook.
|
|
11
|
+
* @param changedFiles - Repo-relative changed files.
|
|
12
|
+
* @param storyFiles - Normalized story import paths from the story index.
|
|
13
|
+
* @param options - Untraced globs excluded from tracing.
|
|
14
|
+
* @returns Affected story files, or `null` when everything must render.
|
|
15
|
+
*/
|
|
16
|
+
declare function traceAffected(graph: DepGraph, changedFiles: string[], storyFiles: readonly string[], options?: TraceOptions): string[] | null;
|
|
17
|
+
//#endregion
|
|
18
|
+
export { matchesAnyGlob, traceAffected };
|
|
19
|
+
//# sourceMappingURL=trace.d.mts.map
|
package/dist/trace.mjs
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
import { graphModulesFor } from "./stats.mjs";
|
|
5
|
+
//#region src/trace.ts
|
|
6
|
+
/** Basenames whose change invalidates every story (global render setup). */
|
|
7
|
+
const GLOBAL_BASENAMES = /* @__PURE__ */ new Set([
|
|
8
|
+
"preview.js",
|
|
9
|
+
"preview.jsx",
|
|
10
|
+
"preview.ts",
|
|
11
|
+
"preview.tsx",
|
|
12
|
+
"preview.mjs",
|
|
13
|
+
"preview.cjs",
|
|
14
|
+
"preview.mts",
|
|
15
|
+
"preview.cts",
|
|
16
|
+
"manager.js",
|
|
17
|
+
"manager.jsx",
|
|
18
|
+
"manager.ts",
|
|
19
|
+
"manager.tsx"
|
|
20
|
+
]);
|
|
21
|
+
/** Config basenames whose change may add, remove, or re-glob stories. */
|
|
22
|
+
const CONFIG_BASENAMES = /* @__PURE__ */ new Set([
|
|
23
|
+
"main.js",
|
|
24
|
+
"main.ts",
|
|
25
|
+
"main.mjs",
|
|
26
|
+
"main.cjs",
|
|
27
|
+
"main.mts"
|
|
28
|
+
]);
|
|
29
|
+
/** Lockfiles whose change means dependency versions cannot be resolved. */
|
|
30
|
+
const LOCKFILE_BASENAMES = /* @__PURE__ */ new Set([
|
|
31
|
+
"package.json",
|
|
32
|
+
"package-lock.json",
|
|
33
|
+
"pnpm-lock.yaml",
|
|
34
|
+
"yarn.lock",
|
|
35
|
+
"bun.lock",
|
|
36
|
+
"bun.lockb",
|
|
37
|
+
"deno.lock"
|
|
38
|
+
]);
|
|
39
|
+
/** Basename of a repo-relative path. */
|
|
40
|
+
function basename(path) {
|
|
41
|
+
const index = path.lastIndexOf("/");
|
|
42
|
+
return index === -1 ? path : path.slice(index + 1);
|
|
43
|
+
}
|
|
44
|
+
/** True when a changed file forces a full render of every story. */
|
|
45
|
+
function isGlobalFile(path) {
|
|
46
|
+
const base = basename(path);
|
|
47
|
+
if (GLOBAL_BASENAMES.has(base) || CONFIG_BASENAMES.has(base) || LOCKFILE_BASENAMES.has(base)) return true;
|
|
48
|
+
return path.includes(".storybook/") && (CONFIG_BASENAMES.has(base) || GLOBAL_BASENAMES.has(base));
|
|
49
|
+
}
|
|
50
|
+
/** Regex-special chars escaped when translating globs (kept discrete: no `${`). */
|
|
51
|
+
const REGEX_SPECIALS = /* @__PURE__ */ new Set([
|
|
52
|
+
"+",
|
|
53
|
+
"^",
|
|
54
|
+
"$",
|
|
55
|
+
"{",
|
|
56
|
+
"}",
|
|
57
|
+
"(",
|
|
58
|
+
")",
|
|
59
|
+
"|",
|
|
60
|
+
"[",
|
|
61
|
+
"]",
|
|
62
|
+
"\\",
|
|
63
|
+
"."
|
|
64
|
+
]);
|
|
65
|
+
/** Translate one glob token at `index` into regex text and advance width. */
|
|
66
|
+
function translateGlobToken(pattern, index) {
|
|
67
|
+
const char = pattern[index];
|
|
68
|
+
if (char === "*") return pattern[index + 1] === "*" ? {
|
|
69
|
+
text: ".*",
|
|
70
|
+
advance: 2
|
|
71
|
+
} : {
|
|
72
|
+
text: "[^/]*",
|
|
73
|
+
advance: 1
|
|
74
|
+
};
|
|
75
|
+
if (char === "?") return {
|
|
76
|
+
text: "[^/]",
|
|
77
|
+
advance: 1
|
|
78
|
+
};
|
|
79
|
+
if (char !== void 0 && REGEX_SPECIALS.has(char)) return {
|
|
80
|
+
text: `\\${char}`,
|
|
81
|
+
advance: 1
|
|
82
|
+
};
|
|
83
|
+
return {
|
|
84
|
+
text: char ?? "",
|
|
85
|
+
advance: 1
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
/** Escape regex syntax while translating `*`, `**`, and `?` globs. */
|
|
89
|
+
function globToRegExp(pattern) {
|
|
90
|
+
const parts = [];
|
|
91
|
+
let index = 0;
|
|
92
|
+
while (index < pattern.length) {
|
|
93
|
+
const token = translateGlobToken(pattern, index);
|
|
94
|
+
parts.push(token.text);
|
|
95
|
+
index += token.advance;
|
|
96
|
+
}
|
|
97
|
+
return new RegExp(`^${parts.join("")}$`, "u");
|
|
98
|
+
}
|
|
99
|
+
/** True when `path` matches any of the given glob patterns. */
|
|
100
|
+
function matchesAnyGlob(path, patterns) {
|
|
101
|
+
return patterns.some((pattern) => globToRegExp(pattern).test(path));
|
|
102
|
+
}
|
|
103
|
+
/** Queue reverse-edge dependents not yet visited. */
|
|
104
|
+
function pushUnseen(seen, pending, dependents) {
|
|
105
|
+
for (const dependent of dependents) if (!seen.has(dependent)) {
|
|
106
|
+
seen.add(dependent);
|
|
107
|
+
pending.push(dependent);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
/** Fold one graph node into the affected set and queue its dependents. */
|
|
111
|
+
function processNode(graph, storyOf, seen, pending, affected, current) {
|
|
112
|
+
const story = storyOf.get(current);
|
|
113
|
+
if (story !== void 0) affected.add(story);
|
|
114
|
+
pushUnseen(seen, pending, graph.importedBy[current] ?? []);
|
|
115
|
+
}
|
|
116
|
+
/** Collect every story file reachable from `roots` via reverse edges. */
|
|
117
|
+
function reachableStories(graph, roots, storyOf) {
|
|
118
|
+
const seen = new Set(roots);
|
|
119
|
+
const pending = [...roots];
|
|
120
|
+
const affected = /* @__PURE__ */ new Set();
|
|
121
|
+
while (pending.length > 0) {
|
|
122
|
+
const current = pending.pop();
|
|
123
|
+
if (current === void 0) break;
|
|
124
|
+
processNode(graph, storyOf, seen, pending, affected, current);
|
|
125
|
+
}
|
|
126
|
+
return affected;
|
|
127
|
+
}
|
|
128
|
+
/** Map every graph node to its story file (suffix match for absolute stats). */
|
|
129
|
+
function storyNodeMap(graph, storyFiles) {
|
|
130
|
+
const nodes = /* @__PURE__ */ new Set([...Object.keys(graph.imports), ...Object.keys(graph.importedBy)]);
|
|
131
|
+
const map = /* @__PURE__ */ new Map();
|
|
132
|
+
for (const story of storyFiles) for (const node of nodes) if ((node === story || node.endsWith(`/${story}`)) && !map.has(node)) map.set(node, story);
|
|
133
|
+
return map;
|
|
134
|
+
}
|
|
135
|
+
/** Root modules for tracing: changed modules plus directly changed stories. */
|
|
136
|
+
function traceRoots(graph, relevant, storyOf) {
|
|
137
|
+
const roots = changedModules(graph, relevant);
|
|
138
|
+
for (const file of relevant) if (storyOf.has(file)) roots.push(file);
|
|
139
|
+
return [...new Set(roots)];
|
|
140
|
+
}
|
|
141
|
+
/** Map changed files to graph modules, dropping files outside the graph. */
|
|
142
|
+
function changedModules(graph, changed) {
|
|
143
|
+
const modules = /* @__PURE__ */ new Set();
|
|
144
|
+
for (const file of changed) for (const module of graphModulesFor(graph, file)) modules.add(module);
|
|
145
|
+
return [...modules];
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Trace changed files through the dependency graph to story files.
|
|
149
|
+
*
|
|
150
|
+
* @param graph - Bidirectional module graph of the built Storybook.
|
|
151
|
+
* @param changedFiles - Repo-relative changed files.
|
|
152
|
+
* @param storyFiles - Normalized story import paths from the story index.
|
|
153
|
+
* @param options - Untraced globs excluded from tracing.
|
|
154
|
+
* @returns Affected story files, or `null` when everything must render.
|
|
155
|
+
*/
|
|
156
|
+
function traceAffected(graph, changedFiles, storyFiles, options = {}) {
|
|
157
|
+
const untraced = options.untraced ?? [];
|
|
158
|
+
const relevant = changedFiles.filter((file) => !matchesAnyGlob(file, untraced));
|
|
159
|
+
if (relevant.find((file) => isGlobalFile(file)) !== void 0) return null;
|
|
160
|
+
const storyOf = storyNodeMap(graph, storyFiles);
|
|
161
|
+
return [...reachableStories(graph, traceRoots(graph, relevant, storyOf), storyOf)].toSorted();
|
|
162
|
+
}
|
|
163
|
+
//#endregion
|
|
164
|
+
export { matchesAnyGlob, traceAffected };
|
|
165
|
+
|
|
166
|
+
//# sourceMappingURL=trace.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"trace.mjs","names":[],"sources":["../src/trace.ts"],"sourcesContent":["import { graphModulesFor } from \"./stats.ts\";\nimport type { DepGraph, TraceOptions } from \"./types.ts\";\n\n/** Basenames whose change invalidates every story (global render setup). */\nconst GLOBAL_BASENAMES = new Set([\n \"preview.js\",\n \"preview.jsx\",\n \"preview.ts\",\n \"preview.tsx\",\n \"preview.mjs\",\n \"preview.cjs\",\n \"preview.mts\",\n \"preview.cts\",\n \"manager.js\",\n \"manager.jsx\",\n \"manager.ts\",\n \"manager.tsx\",\n]);\n\n/** Config basenames whose change may add, remove, or re-glob stories. */\nconst CONFIG_BASENAMES = new Set([\"main.js\", \"main.ts\", \"main.mjs\", \"main.cjs\", \"main.mts\"]);\n\n/** Lockfiles whose change means dependency versions cannot be resolved. */\nconst LOCKFILE_BASENAMES = new Set([\n \"package.json\",\n \"package-lock.json\",\n \"pnpm-lock.yaml\",\n \"yarn.lock\",\n \"bun.lock\",\n \"bun.lockb\",\n \"deno.lock\",\n]);\n\n/** Basename of a repo-relative path. */\nfunction basename(path: string): string {\n const index = path.lastIndexOf(\"/\");\n return index === -1 ? path : path.slice(index + 1);\n}\n\n/** True when a changed file forces a full render of every story. */\nfunction isGlobalFile(path: string): boolean {\n const base = basename(path);\n if (GLOBAL_BASENAMES.has(base) || CONFIG_BASENAMES.has(base) || LOCKFILE_BASENAMES.has(base)) {\n return true;\n }\n return path.includes(\".storybook/\") && (CONFIG_BASENAMES.has(base) || GLOBAL_BASENAMES.has(base));\n}\n\n/** Regex-special chars escaped when translating globs (kept discrete: no `${`). */\nconst REGEX_SPECIALS = new Set([\"+\", \"^\", \"$\", \"{\", \"}\", \"(\", \")\", \"|\", \"[\", \"]\", \"\\\\\", \".\"]);\n\n/** Translate one glob token at `index` into regex text and advance width. */\nfunction translateGlobToken(pattern: string, index: number): { text: string; advance: number } {\n const char = pattern[index];\n if (char === \"*\") {\n return pattern[index + 1] === \"*\" ? { text: \".*\", advance: 2 } : { text: \"[^/]*\", advance: 1 };\n }\n if (char === \"?\") {\n return { text: \"[^/]\", advance: 1 };\n }\n if (char !== undefined && REGEX_SPECIALS.has(char)) {\n return { text: `\\\\${char}`, advance: 1 };\n }\n return { text: char ?? \"\", advance: 1 };\n}\n\n/** Escape regex syntax while translating `*`, `**`, and `?` globs. */\nfunction globToRegExp(pattern: string): RegExp {\n const parts: string[] = [];\n let index = 0;\n while (index < pattern.length) {\n const token = translateGlobToken(pattern, index);\n parts.push(token.text);\n index += token.advance;\n }\n return new RegExp(`^${parts.join(\"\")}$`, \"u\");\n}\n\n/** True when `path` matches any of the given glob patterns. */\nexport function matchesAnyGlob(path: string, patterns: readonly string[]): boolean {\n return patterns.some((pattern) => globToRegExp(pattern).test(path));\n}\n\n/** Queue reverse-edge dependents not yet visited. */\nfunction pushUnseen(seen: Set<string>, pending: string[], dependents: readonly string[]): void {\n for (const dependent of dependents) {\n if (!seen.has(dependent)) {\n seen.add(dependent);\n pending.push(dependent);\n }\n }\n}\n\n/** Fold one graph node into the affected set and queue its dependents. */\nfunction processNode(\n graph: DepGraph,\n storyOf: ReadonlyMap<string, string>,\n seen: Set<string>,\n pending: string[],\n affected: Set<string>,\n current: string,\n): void {\n const story = storyOf.get(current);\n if (story !== undefined) {\n affected.add(story);\n }\n pushUnseen(seen, pending, graph.importedBy[current] ?? []);\n}\n\n/** Collect every story file reachable from `roots` via reverse edges. */\nfunction reachableStories(\n graph: DepGraph,\n roots: string[],\n storyOf: ReadonlyMap<string, string>,\n): Set<string> {\n const seen = new Set<string>(roots);\n const pending = [...roots];\n const affected = new Set<string>();\n while (pending.length > 0) {\n const current = pending.pop();\n if (current === undefined) {\n break;\n }\n processNode(graph, storyOf, seen, pending, affected, current);\n }\n return affected;\n}\n\n/** Map every graph node to its story file (suffix match for absolute stats). */\nfunction storyNodeMap(graph: DepGraph, storyFiles: readonly string[]): Map<string, string> {\n const nodes = new Set([...Object.keys(graph.imports), ...Object.keys(graph.importedBy)]);\n const map = new Map<string, string>();\n for (const story of storyFiles) {\n for (const node of nodes) {\n if ((node === story || node.endsWith(`/${story}`)) && !map.has(node)) {\n map.set(node, story);\n }\n }\n }\n return map;\n}\n/** Root modules for tracing: changed modules plus directly changed stories. */\nfunction traceRoots(\n graph: DepGraph,\n relevant: string[],\n storyOf: ReadonlyMap<string, string>,\n): string[] {\n const roots = changedModules(graph, relevant);\n for (const file of relevant) {\n if (storyOf.has(file)) {\n roots.push(file);\n }\n }\n return [...new Set(roots)];\n}\n/** Map changed files to graph modules, dropping files outside the graph. */\nfunction changedModules(graph: DepGraph, changed: string[]): string[] {\n const modules = new Set<string>();\n for (const file of changed) {\n for (const module of graphModulesFor(graph, file)) {\n modules.add(module);\n }\n }\n return [...modules];\n}\n\n/**\n * Trace changed files through the dependency graph to story files.\n *\n * @param graph - Bidirectional module graph of the built Storybook.\n * @param changedFiles - Repo-relative changed files.\n * @param storyFiles - Normalized story import paths from the story index.\n * @param options - Untraced globs excluded from tracing.\n * @returns Affected story files, or `null` when everything must render.\n */\nexport function traceAffected(\n graph: DepGraph,\n changedFiles: string[],\n storyFiles: readonly string[],\n options: TraceOptions = {},\n): string[] | null {\n const untraced = options.untraced ?? [];\n const relevant = changedFiles.filter((file) => !matchesAnyGlob(file, untraced));\n const forcing = relevant.find((file) => isGlobalFile(file));\n if (forcing !== undefined) {\n return null;\n }\n const storyOf = storyNodeMap(graph, storyFiles);\n const roots = traceRoots(graph, relevant, storyOf);\n return [...reachableStories(graph, roots, storyOf)].toSorted();\n}\n"],"mappings":"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"}
|
package/dist/types.d.mts
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import __tsdown_shims_path from 'node:path';
|
|
2
|
+
import __tsdown_shims_url from 'node:url';
|
|
3
|
+
//#region src/types.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Shared types for affected capture: dependency graphs, trace inputs, and
|
|
6
|
+
* the affected-set result. Zero runtime dependencies by design — both the
|
|
7
|
+
* server (`@storyshelf/core`) and the CI client (`storyshelf`) consume this
|
|
8
|
+
* package without inheriting each other's dependency trees.
|
|
9
|
+
*/
|
|
10
|
+
/** A module dependency graph with edges in both directions. */
|
|
11
|
+
interface DepGraph {
|
|
12
|
+
/** Forward edges: file path → files it directly imports. */
|
|
13
|
+
imports: Record<string, string[]>;
|
|
14
|
+
/** Reverse edges: file path → files that directly import it. */
|
|
15
|
+
importedBy: Record<string, string[]>;
|
|
16
|
+
}
|
|
17
|
+
/** Options tuning how changed files trace to stories. */
|
|
18
|
+
interface TraceOptions {
|
|
19
|
+
/** Glob patterns (e.g. `**literal*.generated.ts`) excluded from tracing. */
|
|
20
|
+
untraced?: string[];
|
|
21
|
+
}
|
|
22
|
+
/** Inputs for computing the affected story set of a change. */
|
|
23
|
+
interface AffectedInput {
|
|
24
|
+
/** Repository root used for `git` commands. */
|
|
25
|
+
cwd: string;
|
|
26
|
+
/** Built Storybook directory holding `index.json` and stats. */
|
|
27
|
+
buildDir: string;
|
|
28
|
+
/** Ancestor commit the current build is compared against. */
|
|
29
|
+
baseSha: string;
|
|
30
|
+
/** Current commit being uploaded. */
|
|
31
|
+
headSha: string;
|
|
32
|
+
/** Glob patterns excluded from tracing. */
|
|
33
|
+
untraced?: string[];
|
|
34
|
+
/** Explicit stats file path (defaults to `<buildDir>/preview-stats.json`). */
|
|
35
|
+
statsFile?: string;
|
|
36
|
+
}
|
|
37
|
+
/** The affected story set of a change. */
|
|
38
|
+
interface AffectedResult {
|
|
39
|
+
/**
|
|
40
|
+
* Story import paths to render, or `null` when the whole Storybook must be
|
|
41
|
+
* rendered (missing history, global file change, unreadable graph, …).
|
|
42
|
+
*/
|
|
43
|
+
affectedImportPaths: string[] | null;
|
|
44
|
+
/** Repo-relative files changed between `baseSha` and `headSha`. */
|
|
45
|
+
changedFiles: string[];
|
|
46
|
+
/** Ancestor commit the diff was computed against. */
|
|
47
|
+
baselineSha: string | null;
|
|
48
|
+
/** Machine-readable reason for a full render (`null` when selective). */
|
|
49
|
+
fullReason: string | null;
|
|
50
|
+
}
|
|
51
|
+
/** A story-like record carrying the file it was imported from. */
|
|
52
|
+
interface StoryLike {
|
|
53
|
+
importPath?: string | null;
|
|
54
|
+
}
|
|
55
|
+
//#endregion
|
|
56
|
+
export { AffectedInput, AffectedResult, DepGraph, StoryLike, TraceOptions };
|
|
57
|
+
//# sourceMappingURL=types.d.mts.map
|
package/package.json
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@storyshelf/affected",
|
|
3
|
+
"version": "0.5.3",
|
|
4
|
+
"description": "Affected capture for StoryShelf: dependency-graph tracing that selects the stories impacted by a change.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"affected",
|
|
7
|
+
"dependency-graph",
|
|
8
|
+
"selective",
|
|
9
|
+
"storybook",
|
|
10
|
+
"visual-testing"
|
|
11
|
+
],
|
|
12
|
+
"homepage": "https://github.com/GuptaSiddhant/StoryShelf#readme",
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/GuptaSiddhant/StoryShelf/issues"
|
|
15
|
+
},
|
|
16
|
+
"license": "MIT",
|
|
17
|
+
"author": {
|
|
18
|
+
"name": "Siddhant Gupta",
|
|
19
|
+
"url": "https://guptasiddhant.com"
|
|
20
|
+
},
|
|
21
|
+
"repository": {
|
|
22
|
+
"type": "git",
|
|
23
|
+
"url": "git+https://github.com/GuptaSiddhant/StoryShelf.git",
|
|
24
|
+
"directory": "packages/affected"
|
|
25
|
+
},
|
|
26
|
+
"files": [
|
|
27
|
+
"dist"
|
|
28
|
+
],
|
|
29
|
+
"type": "module",
|
|
30
|
+
"sideEffects": false,
|
|
31
|
+
"types": "./dist/index.d.mts",
|
|
32
|
+
"exports": {
|
|
33
|
+
".": {
|
|
34
|
+
"source": "./src/index.ts",
|
|
35
|
+
"default": "./dist/index.mjs"
|
|
36
|
+
},
|
|
37
|
+
"./package.json": "./package.json"
|
|
38
|
+
},
|
|
39
|
+
"publishConfig": {
|
|
40
|
+
"exports": {
|
|
41
|
+
".": "./dist/index.mjs",
|
|
42
|
+
"./package.json": "./package.json"
|
|
43
|
+
},
|
|
44
|
+
"access": "public"
|
|
45
|
+
},
|
|
46
|
+
"scripts": {
|
|
47
|
+
"build": "tsdown",
|
|
48
|
+
"dev": "tsdown -w",
|
|
49
|
+
"fmt": "oxfmt ./src",
|
|
50
|
+
"lint": "oxlint --type-aware --type-check ./src",
|
|
51
|
+
"test": "vitest run",
|
|
52
|
+
"prepublishOnly": "nub run build"
|
|
53
|
+
},
|
|
54
|
+
"dependencies": {},
|
|
55
|
+
"devDependencies": {
|
|
56
|
+
"@types/node": "^26.0.0",
|
|
57
|
+
"@vitest/coverage-v8": "^4.0.0",
|
|
58
|
+
"oxfmt": "^0.65.0",
|
|
59
|
+
"oxlint": "^1.80.0",
|
|
60
|
+
"oxlint-tsgolint": "^7.0.2001",
|
|
61
|
+
"tsdown": "^0.22.14",
|
|
62
|
+
"typescript": "^7.0.2",
|
|
63
|
+
"vitest": "^5.0.0"
|
|
64
|
+
},
|
|
65
|
+
"jsr": {
|
|
66
|
+
"runtimeCompat": {
|
|
67
|
+
"node": true,
|
|
68
|
+
"deno": true,
|
|
69
|
+
"bun": true,
|
|
70
|
+
"workerd": false,
|
|
71
|
+
"browser": false
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|