@norskvideo/ctl-dev-kit 0.1.48 → 0.1.50
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/conventions/checks.yml
CHANGED
|
@@ -7,9 +7,10 @@
|
|
|
7
7
|
# fenced CLAUDE.md core, flake.nix, the shared config files, the manifest
|
|
8
8
|
# seed. Copies exist so a human reads them on GitHub with no tooling; the
|
|
9
9
|
# gate is the only thing keeping them true.
|
|
10
|
-
# - quality: the repo's own lint + typecheck + unit tests
|
|
11
|
-
# package.json script interface every product carries. The
|
|
12
|
-
# clean" convention in the CLAUDE core has no teeth without
|
|
10
|
+
# - quality: the repo's own lint + typecheck + unit tests + docs path lint,
|
|
11
|
+
# via the shared package.json script interface every product carries. The
|
|
12
|
+
# "whole project clean" convention in the CLAUDE core has no teeth without
|
|
13
|
+
# this; docs:check is what keeps README/CLAUDE/docs paths true.
|
|
13
14
|
#
|
|
14
15
|
# Both run the same proven way build-image does — inside `nix develop .#build`
|
|
15
16
|
# (which provides bun) on the x64 self-hosted pool — but skip the Docker image
|
|
@@ -74,6 +75,7 @@ jobs:
|
|
|
74
75
|
bun run lint
|
|
75
76
|
bun run typecheck
|
|
76
77
|
bun run test:unit
|
|
78
|
+
bun run docs:check
|
|
77
79
|
'
|
|
78
80
|
|
|
79
81
|
# Report this pipeline's result to the aggregated product CI dashboard
|
|
@@ -44,7 +44,7 @@ container port 4321), then re-add it into the daemon —
|
|
|
44
44
|
under \`tests/unit/__snapshots__/\` (the generator cannot compose without the
|
|
45
45
|
Studio packages installed). Commit them; from then on any emission change is
|
|
46
46
|
a reviewable fixture diff.
|
|
47
|
-
3. \`bun run check:drift && bun run test:unit && bun run typecheck\`
|
|
47
|
+
3. \`bun run check:drift && bun run docs:check && bun run test:unit && bun run typecheck\`
|
|
48
48
|
|
|
49
49
|
## Version pins
|
|
50
50
|
|
|
@@ -68,8 +68,9 @@ refreshed \`~/.norsk-ctl/product-templates/<name>/compose.yml\` carries the pins
|
|
|
68
68
|
## Build / test — the five layers
|
|
69
69
|
|
|
70
70
|
- **Layer 1 — typed factories** (\`shared/src/workflow/components.ts\`): configs
|
|
71
|
-
\`import type\`d from each built-in's
|
|
72
|
-
|
|
71
|
+
\`import type\`d from each built-in's OpenAPI-generated types
|
|
72
|
+
(\`@norskvideo/norsk-studio-built-ins/lib/<component>/_gen/types\`); drift
|
|
73
|
+
from Studio's schema is a \`bun run typecheck\` failure.
|
|
73
74
|
- **Layer 2 — \`builder.validate()\`**: the composer throws on any issue. Never
|
|
74
75
|
demote it to a warning; declare out-of-library components in
|
|
75
76
|
\`COMPONENT_STUBS\` (SDK \`stubbedLibrary\`) instead.
|
|
@@ -79,8 +80,8 @@ refreshed \`~/.norsk-ctl/product-templates/<name>/compose.yml\` carries the pins
|
|
|
79
80
|
- **Layer 4 — studio-load round-trip** (\`tests/unit/loads-in-studio.test.ts\`):
|
|
80
81
|
emitted YAML through Studio's real \`document.load()\`.
|
|
81
82
|
- **Layer 5 — integration**: not scaffolded. Decide + record here whether this
|
|
82
|
-
product carries one (unattended nightly bumps need it; see
|
|
83
|
-
\`conventions/integration-testing.md\`).
|
|
83
|
+
product carries one (unattended nightly bumps need it; see
|
|
84
|
+
\`node_modules/@norskvideo/ctl-dev-kit/conventions/integration-testing.md\`).
|
|
84
85
|
|
|
85
86
|
Byte-snapshots (\`tests/unit/workflow-snapshot.test.ts\`) pin the emitted wire
|
|
86
87
|
bytes; \`UPDATE_SNAPSHOTS=1\` refreshes deliberately.
|
|
@@ -90,6 +91,9 @@ bytes; \`UPDATE_SNAPSHOTS=1\` refreshes deliberately.
|
|
|
90
91
|
runs the built container and asserts the surface \`product add\` probes.
|
|
91
92
|
- \`bun run check:drift\` — the shared-conventions gate; CI runs it plus
|
|
92
93
|
lint/typecheck/test:unit via the drift-gated \`checks.yml\`.
|
|
94
|
+
- \`bun run docs:check\` — every backticked repo path in the README, this
|
|
95
|
+
file and the markdown under docs/ must exist (\`<!-- path-allow -->\` exempts
|
|
96
|
+
a line that names something outside the tree).
|
|
93
97
|
`;
|
|
94
98
|
}
|
|
95
99
|
|
|
@@ -111,6 +115,7 @@ function rootPackageJson(ctx: ShapeContext): string {
|
|
|
111
115
|
"test:unit": "bun test shared/src/ backend/src/ tests/unit/",
|
|
112
116
|
"test:image": "bun test tests/image/",
|
|
113
117
|
"check:drift": "bun run node_modules/@norskvideo/ctl-dev-kit/conventions/check-drift.ts",
|
|
118
|
+
"docs:check": "bun run node_modules/@norskvideo/ctl-dev-kit/docs/path-existence.ts",
|
|
114
119
|
"sync:drift": "bun run node_modules/@norskvideo/ctl-dev-kit/conventions/sync-drift.ts",
|
|
115
120
|
typecheck: "bunx tsc --noEmit -p shared && bunx tsc --noEmit -p backend && bunx tsc --noEmit -p .",
|
|
116
121
|
build: "bun run lint && bun run typecheck && bun run build:no-lint",
|
package/create-product/cli.ts
CHANGED
|
@@ -73,7 +73,7 @@ export function main(argv: string[]): void {
|
|
|
73
73
|
Next steps (also in the generated CLAUDE.md):
|
|
74
74
|
1. cd ${dir} && git init && bun install
|
|
75
75
|
2. UPDATE_SNAPSHOTS=1 bun run test:unit # bake the byte-snapshot fixtures, commit them
|
|
76
|
-
3. bun run check:drift && bun run test:unit && bun run typecheck
|
|
76
|
+
3. bun run check:drift && bun run docs:check && bun run test:unit && bun run typecheck
|
|
77
77
|
4. Replace the starter graph in shared/src/workflow/ with the real one,
|
|
78
78
|
keeping INVARIANTS.md and its rules tests in step.
|
|
79
79
|
`);
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
// Path-existence lint over the hand-written docs (fleet review 02 s11.1). Every
|
|
2
|
+
// backticked token in README.md, CLAUDE.md and docs/**/*.md that looks like a
|
|
3
|
+
// repo path must resolve to something in the tree. Rot in these files is
|
|
4
|
+
// invisible to every other gate: a CLAUDE.md that points an agent at a file
|
|
5
|
+
// deleted two refactors ago still reads as authoritative.
|
|
6
|
+
//
|
|
7
|
+
// "The tree" is what git tracks or would track (cached + untracked, minus
|
|
8
|
+
// ignored), so the answer is the same on a fresh CI checkout as on a dev box
|
|
9
|
+
// with build output lying around. A path that git IGNORES is a declared output
|
|
10
|
+
// (frontend/dist, docs/generated) rather than rot, so it counts as existing;
|
|
11
|
+
// deciding how an index may link into generated output is the review's s13
|
|
12
|
+
// follow-up, not this lint's call. Outside a git repo the tree is a filesystem
|
|
13
|
+
// walk and nothing is ignored.
|
|
14
|
+
import { readdirSync, readFileSync, realpathSync } from "node:fs";
|
|
15
|
+
import { basename, dirname, join, posix } from "node:path";
|
|
16
|
+
|
|
17
|
+
export interface PathFinding {
|
|
18
|
+
file: string;
|
|
19
|
+
line: number;
|
|
20
|
+
token: string;
|
|
21
|
+
message: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface PathExistenceOptions {
|
|
25
|
+
// Tracked paths relative to repoRoot; loaded via loadTree() when omitted.
|
|
26
|
+
tree?: string[];
|
|
27
|
+
// Extra bases a path may be relative to, on top of the repo root and the
|
|
28
|
+
// citing doc's own directory (ctl's CLAUDE.md writes paths relative to
|
|
29
|
+
// packages/norsk-ctl, and says so).
|
|
30
|
+
roots?: string[];
|
|
31
|
+
skipPrefixes?: string[];
|
|
32
|
+
// Which of the given candidate paths are gitignored; defaults to asking git.
|
|
33
|
+
ignored?: (candidates: string[]) => Set<string>;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export const PATH_ALLOW = "<!-- path-allow -->";
|
|
37
|
+
// The fenced core a product's CLAUDE.md copies verbatim from the dev-kit is
|
|
38
|
+
// fleet-generic prose under its own gate (check-drift); the paths it names are
|
|
39
|
+
// conventions, not claims about this repo's tree.
|
|
40
|
+
const CORE_BEGIN = "<!-- BEGIN ctl-shared-conventions v1 -->";
|
|
41
|
+
const CORE_END = "<!-- END ctl-shared-conventions v1 -->";
|
|
42
|
+
|
|
43
|
+
// The five prefixes the review names (its `@norskvideo/` widened to every
|
|
44
|
+
// scoped npm name), plus the classes they imply: any absolute path (container
|
|
45
|
+
// paths, URL paths) and the GitHub/Docker Hub org prefixes that read like a
|
|
46
|
+
// path but name a repo or an image.
|
|
47
|
+
export const DEFAULT_SKIP_PREFIXES = [
|
|
48
|
+
"~/",
|
|
49
|
+
"/api/",
|
|
50
|
+
"http",
|
|
51
|
+
"@",
|
|
52
|
+
"node_modules/",
|
|
53
|
+
"/",
|
|
54
|
+
"id3as/",
|
|
55
|
+
"norskvideo/",
|
|
56
|
+
"ghcr.io/",
|
|
57
|
+
"docker.io/",
|
|
58
|
+
];
|
|
59
|
+
|
|
60
|
+
const PATH_WITH_SLASH = /^[\w@./-]+\/[\w./*-]+$/;
|
|
61
|
+
// A bare filename is only a repo-path claim when its extension says "source":
|
|
62
|
+
// bare config names (config.yaml, default.yaml, package.json) far more often
|
|
63
|
+
// name daemon state or a file inside the container.
|
|
64
|
+
const SOURCE_EXTENSIONS = "md|mdx|ts|tsx|js|mjs|cjs|sh|nix";
|
|
65
|
+
const BARE_FILENAME = new RegExp(`^[\\w.-]+\\.(${SOURCE_EXTENSIONS})$`);
|
|
66
|
+
// `path:12`, `path:12-15`, `path#anchor` are citations of a path, not a path.
|
|
67
|
+
const CITATION_SUFFIX = /(:\d+(-\d+)?|#[\w-]+)$/;
|
|
68
|
+
const INLINE_CODE = /`([^`\n]+)`/g;
|
|
69
|
+
|
|
70
|
+
export function extractPathTokens(line: string): string[] {
|
|
71
|
+
const tokens: string[] = [];
|
|
72
|
+
for (const match of line.matchAll(INLINE_CODE)) {
|
|
73
|
+
const raw = match[1]?.trim() ?? "";
|
|
74
|
+
const token = raw.replace(CITATION_SUFFIX, "");
|
|
75
|
+
if (PATH_WITH_SLASH.test(token) || BARE_FILENAME.test(token)) tokens.push(token);
|
|
76
|
+
}
|
|
77
|
+
return tokens;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function docFilesFor(tree: string[], exclude: string[] = []): string[] {
|
|
81
|
+
const excluded = exclude.map((g) => new Bun.Glob(g));
|
|
82
|
+
return tree.filter((path) => {
|
|
83
|
+
const inScope =
|
|
84
|
+
path === "README.md" ||
|
|
85
|
+
path === "CLAUDE.md" ||
|
|
86
|
+
(path.startsWith("docs/") &&
|
|
87
|
+
path.endsWith(".md") &&
|
|
88
|
+
!path.startsWith("docs/internal/") &&
|
|
89
|
+
!path.startsWith("docs/generated/"));
|
|
90
|
+
return inScope && !excluded.some((g) => g.match(path));
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const WALK_SKIP = new Set(["node_modules", ".git"]);
|
|
95
|
+
|
|
96
|
+
function walk(root: string, rel = ""): string[] {
|
|
97
|
+
const out: string[] = [];
|
|
98
|
+
for (const entry of readdirSync(join(root, rel), { withFileTypes: true })) {
|
|
99
|
+
if (WALK_SKIP.has(entry.name)) continue;
|
|
100
|
+
const path = rel ? `${rel}/${entry.name}` : entry.name;
|
|
101
|
+
if (entry.isDirectory()) out.push(...walk(root, path));
|
|
102
|
+
else out.push(path);
|
|
103
|
+
}
|
|
104
|
+
return out;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function gitLines(repoRoot: string, args: string[], stdin?: string): string[] | undefined {
|
|
108
|
+
const git = Bun.spawnSync(["git", "-C", repoRoot, ...args], {
|
|
109
|
+
stdin: stdin === undefined ? undefined : Buffer.from(stdin),
|
|
110
|
+
});
|
|
111
|
+
// check-ignore exits 1 when nothing matched, which is an answer, not a failure.
|
|
112
|
+
if (git.exitCode !== 0 && git.exitCode !== 1) return undefined;
|
|
113
|
+
return git.stdout
|
|
114
|
+
.toString()
|
|
115
|
+
.split("\n")
|
|
116
|
+
.filter((l) => l.length > 0);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Only a repo's own toplevel is git's to answer: a root nested somewhere inside
|
|
120
|
+
// another repo (a generated skeleton under an ignored scratch dir) would come
|
|
121
|
+
// back empty from ls-files, so it is walked instead.
|
|
122
|
+
function isGitToplevel(repoRoot: string): boolean {
|
|
123
|
+
const top = gitLines(repoRoot, ["rev-parse", "--show-toplevel"])?.[0];
|
|
124
|
+
return top !== undefined && realpathSync(top) === realpathSync(repoRoot);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function loadTree(repoRoot: string): string[] {
|
|
128
|
+
if (!isGitToplevel(repoRoot)) return walk(repoRoot);
|
|
129
|
+
return gitLines(repoRoot, ["ls-files", "--cached", "--others", "--exclude-standard"]) ?? walk(repoRoot);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// A directory pattern (`dist/`) only matches a path git knows is a directory,
|
|
133
|
+
// and a path that is not on disk has no type, so ask about both spellings.
|
|
134
|
+
export function gitIgnored(repoRoot: string): (candidates: string[]) => Set<string> {
|
|
135
|
+
return (candidates) => {
|
|
136
|
+
if (candidates.length === 0 || !isGitToplevel(repoRoot)) return new Set();
|
|
137
|
+
const asked = candidates.flatMap((c) => [c, `${c}/`]);
|
|
138
|
+
const hits = gitLines(repoRoot, ["check-ignore", "--stdin"], `${asked.join("\n")}\n`) ?? [];
|
|
139
|
+
return new Set(hits.map((h) => h.replace(/\/$/, "")));
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
class Tree {
|
|
144
|
+
private readonly files: Set<string>;
|
|
145
|
+
private readonly dirs: Set<string>;
|
|
146
|
+
private readonly basenames: Set<string>;
|
|
147
|
+
|
|
148
|
+
constructor(entries: string[]) {
|
|
149
|
+
this.files = new Set(entries);
|
|
150
|
+
this.dirs = new Set<string>();
|
|
151
|
+
this.basenames = new Set<string>();
|
|
152
|
+
for (const file of entries) {
|
|
153
|
+
this.basenames.add(basename(file));
|
|
154
|
+
let dir = dirname(file);
|
|
155
|
+
while (dir !== "." && dir !== "/" && !this.dirs.has(dir)) {
|
|
156
|
+
this.dirs.add(dir);
|
|
157
|
+
dir = dirname(dir);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
has(path: string, directoryOnly: boolean): boolean {
|
|
163
|
+
if (path === "" || path === ".") return true;
|
|
164
|
+
if (path.includes("*")) {
|
|
165
|
+
const glob = new Bun.Glob(path);
|
|
166
|
+
for (const dir of this.dirs) if (glob.match(dir)) return true;
|
|
167
|
+
if (directoryOnly) return false;
|
|
168
|
+
for (const file of this.files) if (glob.match(file)) return true;
|
|
169
|
+
return false;
|
|
170
|
+
}
|
|
171
|
+
if (this.dirs.has(path)) return true;
|
|
172
|
+
return !directoryOnly && this.files.has(path);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
hasBasename(name: string): boolean {
|
|
176
|
+
return this.basenames.has(name);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function isSkipped(token: string, skipPrefixes: string[]): boolean {
|
|
181
|
+
return skipPrefixes.some((p) => token.startsWith(p));
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
interface Miss {
|
|
185
|
+
file: string;
|
|
186
|
+
line: number;
|
|
187
|
+
token: string;
|
|
188
|
+
bases: string[];
|
|
189
|
+
candidates: string[];
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export function checkPathExistence(
|
|
193
|
+
repoRoot: string,
|
|
194
|
+
files: string[],
|
|
195
|
+
{
|
|
196
|
+
tree: entries,
|
|
197
|
+
roots = [],
|
|
198
|
+
skipPrefixes = DEFAULT_SKIP_PREFIXES,
|
|
199
|
+
ignored = gitIgnored(repoRoot),
|
|
200
|
+
}: PathExistenceOptions = {},
|
|
201
|
+
): PathFinding[] {
|
|
202
|
+
const tree = new Tree(entries ?? loadTree(repoRoot));
|
|
203
|
+
const misses: Miss[] = [];
|
|
204
|
+
|
|
205
|
+
for (const file of files) {
|
|
206
|
+
const bases = unique([dirname(file) === "." ? "" : dirname(file), "", ...roots]);
|
|
207
|
+
const lines = readFileSync(join(repoRoot, file), "utf8").split("\n");
|
|
208
|
+
let inFence = false;
|
|
209
|
+
let inCore = false;
|
|
210
|
+
lines.forEach((line, index) => {
|
|
211
|
+
if (/^\s*(```|~~~)/.test(line)) {
|
|
212
|
+
inFence = !inFence;
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
if (line.includes(CORE_BEGIN)) inCore = true;
|
|
216
|
+
if (line.includes(CORE_END)) inCore = false;
|
|
217
|
+
if (inFence || inCore || line.includes(PATH_ALLOW)) return;
|
|
218
|
+
for (const token of extractPathTokens(line)) {
|
|
219
|
+
if (isSkipped(token, skipPrefixes)) continue;
|
|
220
|
+
if (!token.includes("/")) {
|
|
221
|
+
if (!tree.hasBasename(token)) misses.push({ file, line: index + 1, token, bases, candidates: [] });
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
const directoryOnly = token.endsWith("/");
|
|
225
|
+
const candidates = bases
|
|
226
|
+
.map((base) => posix.normalize(posix.join(base, token.replace(/\/+$/, ""))))
|
|
227
|
+
.filter((c) => !c.startsWith(".."));
|
|
228
|
+
// Every reading leaves the repo (`../norsk-ctl`): not this tree's claim.
|
|
229
|
+
if (candidates.length === 0) continue;
|
|
230
|
+
if (candidates.some((c) => tree.has(c, directoryOnly))) continue;
|
|
231
|
+
misses.push({ file, line: index + 1, token, bases, candidates });
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// One git round-trip for every miss, not one per miss.
|
|
237
|
+
const declaredOutputs = ignored(unique(misses.flatMap((m) => m.candidates)));
|
|
238
|
+
return misses
|
|
239
|
+
.filter((m) => !m.candidates.some((c) => declaredOutputs.has(c)))
|
|
240
|
+
.map(({ file, line, token, bases }) => {
|
|
241
|
+
const looked = bases.map((b) => (b === "" ? "<repo root>" : `${b}/`)).join(", ");
|
|
242
|
+
return {
|
|
243
|
+
file,
|
|
244
|
+
line,
|
|
245
|
+
token,
|
|
246
|
+
message: `${file}:${line}: \`${token}\` does not exist (looked under ${looked}). Fix the path, or append ${PATH_ALLOW} to the line if it is not a repo path.`,
|
|
247
|
+
};
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function unique<T>(xs: T[]): T[] {
|
|
252
|
+
return [...new Set(xs)];
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function usage(): never {
|
|
256
|
+
console.error(
|
|
257
|
+
"usage: path-existence.ts [repoRoot] [--root <dir>]... [--exclude <glob>]...\n" +
|
|
258
|
+
" --root extra base a doc path may be relative to (repeatable)\n" +
|
|
259
|
+
" --exclude glob of docs to leave out, e.g. docs/review/** (repeatable)",
|
|
260
|
+
);
|
|
261
|
+
process.exit(2);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (import.meta.main) {
|
|
265
|
+
const args = process.argv.slice(2);
|
|
266
|
+
let repoRoot = process.cwd();
|
|
267
|
+
const roots: string[] = [];
|
|
268
|
+
const exclude: string[] = [];
|
|
269
|
+
for (let i = 0; i < args.length; i++) {
|
|
270
|
+
const arg = args[i] as string;
|
|
271
|
+
if (arg === "--root") roots.push(args[++i] ?? usage());
|
|
272
|
+
else if (arg === "--exclude") exclude.push(args[++i] ?? usage());
|
|
273
|
+
else if (arg.startsWith("-")) usage();
|
|
274
|
+
else repoRoot = arg;
|
|
275
|
+
}
|
|
276
|
+
const tree = loadTree(repoRoot);
|
|
277
|
+
const files = docFilesFor(tree, exclude);
|
|
278
|
+
const findings = checkPathExistence(repoRoot, files, { tree, roots });
|
|
279
|
+
if (findings.length === 0) {
|
|
280
|
+
console.log(`docs path-check: ${files.length} file(s), every cited repo path exists.`);
|
|
281
|
+
process.exit(0);
|
|
282
|
+
}
|
|
283
|
+
console.error(`docs path-check FAILED (${findings.length} dead path(s) across ${files.length} file(s)):`);
|
|
284
|
+
for (const f of findings) console.error(` - ${f.message}`);
|
|
285
|
+
process.exit(1);
|
|
286
|
+
}
|
package/package.json
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@norskvideo/ctl-dev-kit",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.50",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
"./package.json": "./package.json",
|
|
7
7
|
"./testing/invariants": "./testing/invariants.ts",
|
|
8
8
|
"./testing/byte-snapshot": "./testing/byte-snapshot.ts",
|
|
9
9
|
"./create-product": "./create-product/create-product.ts",
|
|
10
|
+
"./docs/path-existence": "./docs/path-existence.ts",
|
|
10
11
|
"./doc-guide": "./doc-guide/doc-guide.js",
|
|
11
12
|
"./doc-guide/build-manual": "./doc-guide/build-manual.js",
|
|
12
13
|
"./doc-guide/guides-config": "./doc-guide/guides-config.js",
|