@trackunit/repo-checks 0.0.0-stage → 0.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +7 -0
- package/LICENSE.txt +191 -0
- package/README.md +319 -2
- package/bin/index.d.ts +2 -0
- package/bin/index.js +34 -0
- package/migrations/entry.js +3 -0
- package/migrations.json +3 -0
- package/package.json +34 -3
- package/src/cli.d.ts +27 -0
- package/src/cli.js +109 -0
- package/src/index.d.ts +8 -0
- package/src/index.js +21 -0
- package/src/lib/config-error.d.ts +8 -0
- package/src/lib/config-error.js +16 -0
- package/src/lib/config.d.ts +22 -0
- package/src/lib/config.js +61 -0
- package/src/lib/doc-references/case-fix.d.ts +21 -0
- package/src/lib/doc-references/case-fix.js +66 -0
- package/src/lib/doc-references/check-doc-references.d.ts +28 -0
- package/src/lib/doc-references/check-doc-references.js +147 -0
- package/src/lib/doc-references/classify.d.ts +49 -0
- package/src/lib/doc-references/classify.js +59 -0
- package/src/lib/doc-references/discover.d.ts +6 -0
- package/src/lib/doc-references/discover.js +84 -0
- package/src/lib/doc-references/extract.d.ts +29 -0
- package/src/lib/doc-references/extract.js +46 -0
- package/src/lib/doc-references/finding.d.ts +24 -0
- package/src/lib/doc-references/finding.js +11 -0
- package/src/lib/doc-references/fs-errors.d.ts +14 -0
- package/src/lib/doc-references/fs-errors.js +25 -0
- package/src/lib/doc-references/headings.d.ts +79 -0
- package/src/lib/doc-references/headings.js +219 -0
- package/src/lib/doc-references/parse.d.ts +72 -0
- package/src/lib/doc-references/parse.js +72 -0
- package/src/lib/doc-references/placeholders.d.ts +74 -0
- package/src/lib/doc-references/placeholders.js +176 -0
- package/src/lib/doc-references/repo-root.d.ts +18 -0
- package/src/lib/doc-references/repo-root.js +36 -0
- package/src/lib/doc-references/resolver.d.ts +96 -0
- package/src/lib/doc-references/resolver.js +430 -0
- package/src/lib/doc-references/settings.d.ts +163 -0
- package/src/lib/doc-references/settings.js +167 -0
- package/src/lib/doc-references/suppressions.d.ts +8 -0
- package/src/lib/doc-references/suppressions.js +24 -0
- package/src/lib/format.d.ts +13 -0
- package/src/lib/format.js +33 -0
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.hasAnchor = exports.readDocumentAnchors = exports.knownDocumentAnchors = exports.documentAnchors = exports.MAX_ANCHOR_DOCUMENT_BYTES = void 0;
|
|
4
|
+
const tslib_1 = require("tslib");
|
|
5
|
+
const fs = tslib_1.__importStar(require("fs"));
|
|
6
|
+
const github_slugger_1 = tslib_1.__importDefault(require("github-slugger"));
|
|
7
|
+
const parse_1 = require("./parse");
|
|
8
|
+
const resolver_1 = require("./resolver");
|
|
9
|
+
/**
|
|
10
|
+
* The fragment ids a markdown document defines: upstream's anchors plus GitHub's. Upstream's are
|
|
11
|
+
* those `@eslint/markdown`'s `no-missing-link-fragments` computes for in-document `#anchor` links,
|
|
12
|
+
* so every fragment that passes there passes here too. GitHub's only add anchors, so they can only
|
|
13
|
+
* remove false positives.
|
|
14
|
+
*
|
|
15
|
+
* The document is parsed with GFM and YAML frontmatter, as `parseMarkdown` parses it (so a
|
|
16
|
+
* leading `---` block never yields a heading, while `+++` fences are plain markdown, as on GitHub),
|
|
17
|
+
* and one `github-slugger` per document slugs, in document order:
|
|
18
|
+
* - every heading (ATX or setext): the concatenated `value` of its non-HTML descendants, so
|
|
19
|
+
* inline code, emphasis, and link text count while image alt text and inline HTML don't; a
|
|
20
|
+
* trailing custom `{#id}` replaces the text;
|
|
21
|
+
* - in every HTML node, outside `<!-- comments -->`: each `id=`/`name=` attribute value, and the
|
|
22
|
+
* tag-stripped text of each `<h1>`-`<h6>` element.
|
|
23
|
+
* The slugger suffixes repeats (`setup`, `setup-1`, ...) across all of these, and `top` is always
|
|
24
|
+
* defined. `.mdx` targets are parsed with the MDX extensions too (`parseMarkdown` decides by the
|
|
25
|
+
* file extension): ESM and `{expressions}` yield no headings, markdown inside JSX children does, and
|
|
26
|
+
* a JSX element's string `id`/`name` attribute counts like the HTML one. An `{expression}` inside a
|
|
27
|
+
* heading adds no text, as in the ids Storybook's `rehype-slug` gives the rendered heading, so
|
|
28
|
+
* `# Hi {name}` defines `hi-` (the space before the expression stays), not `hi` or `hi-name`. A
|
|
29
|
+
* target that is not valid MDX, has MDX whose rendering parsing can't see (`knownDocumentAnchors`),
|
|
30
|
+
* or is larger than `MAX_ANCHOR_DOCUMENT_BYTES`, has no known anchors.
|
|
31
|
+
*
|
|
32
|
+
* GitHub's anchors, on top of those:
|
|
33
|
+
* - each explicit `id`/`name` value as written (lower-cased), since GitHub keeps `.` and `:`
|
|
34
|
+
* (`v1.2.0`, `a:b`) that slugging drops;
|
|
35
|
+
* - every markdown heading slugged by a second `github-slugger` of its own, over its full text
|
|
36
|
+
* with emoji shortcodes removed: GitHub has no `{#id}` (`## Setup {#install}` is `setup-install`)
|
|
37
|
+
* and renders `:rocket:` as an emoji the slug drops (`## :rocket: Deploy` is `-deploy`);
|
|
38
|
+
* - footnote ids as GitHub renders them: `fn-<label>` per definition, and `fnref-<label>`,
|
|
39
|
+
* `fnref-<label>-2`, ... per reference.
|
|
40
|
+
* `hasAnchor` also accepts each of these behind GitHub's `user-content-` prefix.
|
|
41
|
+
*
|
|
42
|
+
* `.mdx` documents get the same GFM and frontmatter reading, which deliberately differs from a
|
|
43
|
+
* Storybook build without those plugins: there `# Title[^1]` keeps `[^1]` as text (`title1`, not
|
|
44
|
+
* `title`), and a leading `---` block renders as a rule and a setext heading. So a `.mdx` document
|
|
45
|
+
* with frontmatter, or with a footnote reference inside a heading, has no known anchors
|
|
46
|
+
* (`knownDocumentAnchors`).
|
|
47
|
+
*/
|
|
48
|
+
/**
|
|
49
|
+
* Documents above this size are skipped rather than parsed: a 1.1 MB CHANGELOG took 2 s and 417 MB
|
|
50
|
+
* of heap. A skipped target has no known anchors, so it is never reported.
|
|
51
|
+
*/
|
|
52
|
+
exports.MAX_ANCHOR_DOCUMENT_BYTES = 1024 * 1024;
|
|
53
|
+
const CUSTOM_HEADING_ID_RE = /\{#(?<id>[^}\s]+)\}\s*$/u;
|
|
54
|
+
const HTML_HEADING_RE = /<h(?<depth>[1-6])[^>]*>(?<children>[\s\S]*?)<\/h\k<depth>\s*>/giu;
|
|
55
|
+
const HTML_ID_NAME_RE = /(?<!<)<[^>]+\s(?:id|name)\s*=\s*["']?(?<id>[^"'\s>]+)["']?/giu;
|
|
56
|
+
const HTML_TAG_RE = /<\/?[a-z0-9]+(?:-[a-z0-9]+)*(?:\s(?:[^>"']|"[^"]*"|'[^']*')*)?(?:\/\s*)?>/giu;
|
|
57
|
+
const HTML_COMMENT_RE = /<!--[\s\S]*?-->/gu;
|
|
58
|
+
const GITHUB_LINE_REFERENCE_RE = /^L\d+(?:C\d+)?(?:-L\d+(?:C\d+)?)?$/u;
|
|
59
|
+
const EMOJI_SHORTCODE_RE = /:[a-z0-9_+-]+:/giu;
|
|
60
|
+
const GITHUB_USER_CONTENT_PREFIX = "user-content-";
|
|
61
|
+
const JSX_ELEMENT_TYPES = new Set(["mdxJsxFlowElement", "mdxJsxTextElement"]);
|
|
62
|
+
/** The string `id`/`name` attribute values of a JSX element, the MDX counterpart of HTML's `id=`/`name=`. */
|
|
63
|
+
const jsxAnchorAttributes = (node) => JSX_ELEMENT_TYPES.has(node.type)
|
|
64
|
+
? (node.attributes ?? []).flatMap(attribute => attribute.type === "mdxJsxAttribute" &&
|
|
65
|
+
(attribute.name === "id" || attribute.name === "name") &&
|
|
66
|
+
typeof attribute.value === "string"
|
|
67
|
+
? [attribute.value]
|
|
68
|
+
: [])
|
|
69
|
+
: [];
|
|
70
|
+
const HEADING_TEXT_SKIPPED_TYPES = new Set(["html", "mdxTextExpression"]);
|
|
71
|
+
const headingText = (node) => (node.children ?? [])
|
|
72
|
+
.map(child => (HEADING_TEXT_SKIPPED_TYPES.has(child.type) ? "" : (child.value ?? "") + headingText(child)))
|
|
73
|
+
.join("");
|
|
74
|
+
/**
|
|
75
|
+
* The fragment ids an already-parsed document defines (see the module comment): upstream's anchors
|
|
76
|
+
* (every HTML node's, and every heading's once its own HTML is collected), plus GitHub's.
|
|
77
|
+
*/
|
|
78
|
+
const documentAnchors = (root) => {
|
|
79
|
+
const anchors = new Set(["top"]);
|
|
80
|
+
const slugger = new github_slugger_1.default();
|
|
81
|
+
const githubSlugger = new github_slugger_1.default();
|
|
82
|
+
const footnoteReferences = new Map();
|
|
83
|
+
const addExplicit = (id) => {
|
|
84
|
+
anchors.add(slugger.slug(id));
|
|
85
|
+
anchors.add(id.toLowerCase());
|
|
86
|
+
};
|
|
87
|
+
const visit = (node) => {
|
|
88
|
+
if (node.type === "html") {
|
|
89
|
+
const html = (node.value ?? "").replace(HTML_COMMENT_RE, "");
|
|
90
|
+
for (const match of html.matchAll(HTML_ID_NAME_RE))
|
|
91
|
+
addExplicit(match.groups?.id ?? "");
|
|
92
|
+
for (const match of html.matchAll(HTML_HEADING_RE)) {
|
|
93
|
+
anchors.add(slugger.slug((match.groups?.children ?? "").replace(HTML_TAG_RE, "")));
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
for (const id of jsxAnchorAttributes(node))
|
|
97
|
+
addExplicit(id);
|
|
98
|
+
const footnoteLabel = (node.label ?? node.identifier)?.toLowerCase();
|
|
99
|
+
if (footnoteLabel !== undefined && node.type === "footnoteDefinition")
|
|
100
|
+
anchors.add(`fn-${footnoteLabel}`);
|
|
101
|
+
if (footnoteLabel !== undefined && node.type === "footnoteReference") {
|
|
102
|
+
const count = (footnoteReferences.get(footnoteLabel) ?? 0) + 1;
|
|
103
|
+
footnoteReferences.set(footnoteLabel, count);
|
|
104
|
+
anchors.add(count === 1 ? `fnref-${footnoteLabel}` : `fnref-${footnoteLabel}-${count}`);
|
|
105
|
+
}
|
|
106
|
+
node.children?.forEach(visit);
|
|
107
|
+
if (node.type === "heading") {
|
|
108
|
+
const text = headingText(node);
|
|
109
|
+
anchors.add(slugger.slug(CUSTOM_HEADING_ID_RE.exec(text)?.groups?.id ?? text));
|
|
110
|
+
anchors.add(githubSlugger.slug(text.replace(EMOJI_SHORTCODE_RE, "")));
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
visit(root);
|
|
114
|
+
return anchors;
|
|
115
|
+
};
|
|
116
|
+
exports.documentAnchors = documentAnchors;
|
|
117
|
+
const MDX_EXPRESSION_TYPES = new Set(["mdxFlowExpression", "mdxTextExpression"]);
|
|
118
|
+
/**
|
|
119
|
+
* A built-in HTML element MDX renders as itself; anything else is a component (`Meta`,
|
|
120
|
+
* `Docs.Block`) or a custom element (`doc-toc`), either of which may render anything.
|
|
121
|
+
*/
|
|
122
|
+
const INTRINSIC_JSX_NAME_RE = /^[a-z][a-z0-9]*$/u;
|
|
123
|
+
const JSX_HEADING_NAME_RE = /^h[1-6]$/u;
|
|
124
|
+
/** A spread, or any `{expression}` value (`children={...}`, `dangerouslySetInnerHTML={...}`), may render anchors. */
|
|
125
|
+
const isOpaqueJsxAttribute = (attribute) => attribute.type !== "mdxJsxAttribute" || (typeof attribute.value === "object" && attribute.value !== null);
|
|
126
|
+
/** Whether `node` may render anchors, or headings with ids, that parsing the document can't see. */
|
|
127
|
+
const isOpaqueMdxNode = (node) => {
|
|
128
|
+
if (node.type === "mdxjsEsm")
|
|
129
|
+
return true;
|
|
130
|
+
if (MDX_EXPRESSION_TYPES.has(node.type))
|
|
131
|
+
return !(0, parse_1.isCommentExpression)(node);
|
|
132
|
+
if (!JSX_ELEMENT_TYPES.has(node.type) || node.name === null || node.name === undefined)
|
|
133
|
+
return false;
|
|
134
|
+
return (!INTRINSIC_JSX_NAME_RE.test(node.name) ||
|
|
135
|
+
JSX_HEADING_NAME_RE.test(node.name) ||
|
|
136
|
+
(node.attributes ?? []).some(isOpaqueJsxAttribute));
|
|
137
|
+
};
|
|
138
|
+
const hasOpaqueMdx = (node) => isOpaqueMdxNode(node) || (node.children ?? []).some(hasOpaqueMdx);
|
|
139
|
+
const hasFootnoteReference = (node) => node.type === "footnoteReference" || (node.children ?? []).some(hasFootnoteReference);
|
|
140
|
+
/** GFM and frontmatter readings Storybook may not share (see the module comment), so an `.mdx`'s anchors there are unknown. */
|
|
141
|
+
const hasStorybookReadingGap = (node) => node.type === "yaml" ||
|
|
142
|
+
(node.type === "heading" && hasFootnoteReference(node)) ||
|
|
143
|
+
(node.children ?? []).some(hasStorybookReadingGap);
|
|
144
|
+
/**
|
|
145
|
+
* The fragment ids a parsed document defines, as `documentAnchors` computes them, or `undefined`
|
|
146
|
+
* when MDX in it may render anchors that parsing can't see, so a caller never reports a fragment
|
|
147
|
+
* that the rendered page might define. That is any ESM (an import brings in components or content,
|
|
148
|
+
* an `export default` wraps the page in a layout); any JSX element that is a component (`<Meta>`,
|
|
149
|
+
* Storybook's `<Title />` and `<Stories />`, `<Markdown>{...}</Markdown>`) or a custom element
|
|
150
|
+
* (`<doc-toc>`); a JSX `<h1>`-`<h6>`, whose rendered id may differ between renderers (GitHub,
|
|
151
|
+
* Storybook); a spread or any `{expression}` attribute value; and any `{expression}` holding code
|
|
152
|
+
* (see `isCommentExpression`), which covers a heading like `# Hi {name}`. In a `.mdx` `file`,
|
|
153
|
+
* frontmatter and a footnote reference inside a heading count too (see the module comment).
|
|
154
|
+
* Built-in HTML elements with string attributes and comment-only expressions are seen in full.
|
|
155
|
+
* Plain markdown has none of these nodes, and its frontmatter and footnotes read as on GitHub, so a
|
|
156
|
+
* `.md` document's anchors are always known.
|
|
157
|
+
*/
|
|
158
|
+
const knownDocumentAnchors = (root, file) => hasOpaqueMdx(root) || ((0, parse_1.isMdxFile)(file) && hasStorybookReadingGap(root)) ? undefined : (0, exports.documentAnchors)(root);
|
|
159
|
+
exports.knownDocumentAnchors = knownDocumentAnchors;
|
|
160
|
+
/** Per-process cache keyed by absolute path, so each target is parsed once per run; revalidated by mtime and size. */
|
|
161
|
+
const documentCache = new Map();
|
|
162
|
+
const parseAnchors = (absolutePath, text) => {
|
|
163
|
+
try {
|
|
164
|
+
return (0, exports.knownDocumentAnchors)((0, parse_1.parseMarkdown)(text, absolutePath), absolutePath);
|
|
165
|
+
}
|
|
166
|
+
catch {
|
|
167
|
+
return undefined;
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
/**
|
|
171
|
+
* The fragment ids `absolutePath` defines (see the module comment), or `undefined` when it can't be
|
|
172
|
+
* read or parsed, is larger than `MAX_ANCHOR_DOCUMENT_BYTES`, or has MDX whose rendered anchors
|
|
173
|
+
* can't be seen (see `knownDocumentAnchors`), so a caller never reports against a document it
|
|
174
|
+
* didn't fully inspect.
|
|
175
|
+
*/
|
|
176
|
+
const readDocumentAnchors = (absolutePath) => {
|
|
177
|
+
try {
|
|
178
|
+
const { mtimeMs, size } = fs.statSync(absolutePath);
|
|
179
|
+
if (size > exports.MAX_ANCHOR_DOCUMENT_BYTES) {
|
|
180
|
+
documentCache.delete(absolutePath);
|
|
181
|
+
return undefined;
|
|
182
|
+
}
|
|
183
|
+
const cached = documentCache.get(absolutePath);
|
|
184
|
+
if (cached?.mtimeMs === mtimeMs && cached.size === size && cached.readAtMs - mtimeMs > resolver_1.RACY_WINDOW_MS) {
|
|
185
|
+
return cached.anchors;
|
|
186
|
+
}
|
|
187
|
+
const readAtMs = Date.now();
|
|
188
|
+
const anchors = parseAnchors(absolutePath, fs.readFileSync(absolutePath, "utf8"));
|
|
189
|
+
documentCache.set(absolutePath, { mtimeMs, size, readAtMs, anchors });
|
|
190
|
+
return anchors;
|
|
191
|
+
}
|
|
192
|
+
catch {
|
|
193
|
+
documentCache.delete(absolutePath);
|
|
194
|
+
return undefined;
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
exports.readDocumentAnchors = readDocumentAnchors;
|
|
198
|
+
/**
|
|
199
|
+
* Whether a written fragment names one of `anchors`, compared as `no-missing-link-fragments` does by
|
|
200
|
+
* default: percent-decoded (a malformed escape as written) and case-insensitively. GitHub line
|
|
201
|
+
* references (`L10`, `L10C2-L12C5`) always match, and so does an anchor behind the `user-content-`
|
|
202
|
+
* prefix GitHub gives the ids it renders (`#user-content-setup`).
|
|
203
|
+
*/
|
|
204
|
+
const hasAnchor = (anchors, fragment) => {
|
|
205
|
+
let decoded = fragment;
|
|
206
|
+
try {
|
|
207
|
+
decoded = decodeURIComponent(fragment);
|
|
208
|
+
}
|
|
209
|
+
catch {
|
|
210
|
+
// checked as written
|
|
211
|
+
}
|
|
212
|
+
const lowerCase = decoded.toLowerCase();
|
|
213
|
+
return (GITHUB_LINE_REFERENCE_RE.test(decoded) ||
|
|
214
|
+
anchors.has(lowerCase) ||
|
|
215
|
+
(lowerCase.startsWith(GITHUB_USER_CONTENT_PREFIX) &&
|
|
216
|
+
anchors.has(lowerCase.slice(GITHUB_USER_CONTENT_PREFIX.length))));
|
|
217
|
+
};
|
|
218
|
+
exports.hasAnchor = hasAnchor;
|
|
219
|
+
//# sourceMappingURL=headings.js.map
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/** A 1-based source location, as mdast reports it. */
|
|
2
|
+
export interface MarkdownPoint {
|
|
3
|
+
readonly line: number;
|
|
4
|
+
readonly column: number;
|
|
5
|
+
readonly offset?: number;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* The structural subset of an mdast node the doc-reference checks read. Declared here rather than
|
|
9
|
+
* imported from `mdast` so the library doesn't depend on the parser's type packages.
|
|
10
|
+
*/
|
|
11
|
+
export interface MarkdownNode {
|
|
12
|
+
readonly type: string;
|
|
13
|
+
/** Text content: `code`, `inlineCode`, `yaml`, `html`, `text`, and similar leaves. */
|
|
14
|
+
readonly value?: string;
|
|
15
|
+
/** Destination of `link`, `image`, and `definition`. */
|
|
16
|
+
readonly url?: string;
|
|
17
|
+
readonly children?: ReadonlyArray<MarkdownNode>;
|
|
18
|
+
readonly position?: {
|
|
19
|
+
readonly start: MarkdownPoint;
|
|
20
|
+
readonly end: MarkdownPoint;
|
|
21
|
+
};
|
|
22
|
+
/** The label as written, on `footnoteDefinition` and `footnoteReference` (and other reference nodes). */
|
|
23
|
+
readonly label?: string | null;
|
|
24
|
+
/** The normalised label, on the same nodes as `label`. */
|
|
25
|
+
readonly identifier?: string;
|
|
26
|
+
/** An MDX JSX element's name (`div`, `Meta`, `Docs.Block`), or `null` for a fragment. */
|
|
27
|
+
readonly name?: string | null;
|
|
28
|
+
/** On MDX JSX elements only. */
|
|
29
|
+
readonly attributes?: ReadonlyArray<{
|
|
30
|
+
readonly type: string;
|
|
31
|
+
readonly name?: string;
|
|
32
|
+
readonly value?: unknown;
|
|
33
|
+
}>;
|
|
34
|
+
/** On MDX ESM and `{expression}` nodes: the program the parser built from `value`. */
|
|
35
|
+
readonly data?: {
|
|
36
|
+
readonly estree?: {
|
|
37
|
+
readonly body: ReadonlyArray<unknown>;
|
|
38
|
+
} | null;
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Whether `node` is an MDX `{expression}` holding no code, only comments if anything (`{/* note *\/}`,
|
|
43
|
+
* `{// note}`), so it renders nothing; its source text, comments included, is its `value`. Decided
|
|
44
|
+
* from the program the parser built, so `{/* a *\/ x /* b *\/}` is code; an expression without one is.
|
|
45
|
+
*/
|
|
46
|
+
export declare const isCommentExpression: (node: MarkdownNode) => boolean;
|
|
47
|
+
/** A document that could not be parsed, located at the 1-based line and column the parser stopped at. */
|
|
48
|
+
export declare class DocumentParseError extends Error {
|
|
49
|
+
readonly file: string;
|
|
50
|
+
readonly line: number;
|
|
51
|
+
readonly column: number;
|
|
52
|
+
readonly reason: string;
|
|
53
|
+
/**
|
|
54
|
+
* @param file The document, as it was passed to the parser.
|
|
55
|
+
* @param line 1-based line the parser stopped at.
|
|
56
|
+
* @param column 1-based column the parser stopped at.
|
|
57
|
+
* @param reason The parser's message.
|
|
58
|
+
*/
|
|
59
|
+
constructor(file: string, line: number, column: number, reason: string);
|
|
60
|
+
}
|
|
61
|
+
/** Whether `file` is parsed as MDX: its extension is `.mdx`, in any case. */
|
|
62
|
+
export declare const isMdxFile: (file: string) => boolean;
|
|
63
|
+
/**
|
|
64
|
+
* Parse a markdown document into mdast with GFM and YAML frontmatter, the way every document this
|
|
65
|
+
* library checks is read: a leading `---` block becomes a `yaml` node rather than content, while `+++` fences stay plain
|
|
66
|
+
* markdown, as on GitHub. A `.mdx` `file` is parsed with the MDX extensions too, as Storybook's MDX 3
|
|
67
|
+
* compiler reads it: ESM becomes `mdxjsEsm`, JSX `mdxJsxFlowElement`/`mdxJsxTextElement` (markdown in
|
|
68
|
+
* their children stays mdast; attributes are not children), and `{expressions}`, comments included,
|
|
69
|
+
* `mdxFlowExpression`/`mdxTextExpression` leaves. Plain markdown always parses; invalid MDX throws a
|
|
70
|
+
* {@link DocumentParseError} naming `file` and the location.
|
|
71
|
+
*/
|
|
72
|
+
export declare const parseMarkdown: (source: string, file: string) => MarkdownNode;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.parseMarkdown = exports.isMdxFile = exports.DocumentParseError = exports.isCommentExpression = void 0;
|
|
4
|
+
const mdast_util_from_markdown_1 = require("mdast-util-from-markdown");
|
|
5
|
+
const mdast_util_frontmatter_1 = require("mdast-util-frontmatter");
|
|
6
|
+
const mdast_util_gfm_1 = require("mdast-util-gfm");
|
|
7
|
+
const mdast_util_mdx_1 = require("mdast-util-mdx");
|
|
8
|
+
const micromark_extension_frontmatter_1 = require("micromark-extension-frontmatter");
|
|
9
|
+
const micromark_extension_gfm_1 = require("micromark-extension-gfm");
|
|
10
|
+
const micromark_extension_mdxjs_1 = require("micromark-extension-mdxjs");
|
|
11
|
+
const MDX_EXPRESSION_TYPES = new Set(["mdxFlowExpression", "mdxTextExpression"]);
|
|
12
|
+
/**
|
|
13
|
+
* Whether `node` is an MDX `{expression}` holding no code, only comments if anything (`{/* note *\/}`,
|
|
14
|
+
* `{// note}`), so it renders nothing; its source text, comments included, is its `value`. Decided
|
|
15
|
+
* from the program the parser built, so `{/* a *\/ x /* b *\/}` is code; an expression without one is.
|
|
16
|
+
*/
|
|
17
|
+
const isCommentExpression = (node) => MDX_EXPRESSION_TYPES.has(node.type) && node.data?.estree?.body.length === 0;
|
|
18
|
+
exports.isCommentExpression = isCommentExpression;
|
|
19
|
+
/** A document that could not be parsed, located at the 1-based line and column the parser stopped at. */
|
|
20
|
+
class DocumentParseError extends Error {
|
|
21
|
+
/**
|
|
22
|
+
* @param file The document, as it was passed to the parser.
|
|
23
|
+
* @param line 1-based line the parser stopped at.
|
|
24
|
+
* @param column 1-based column the parser stopped at.
|
|
25
|
+
* @param reason The parser's message.
|
|
26
|
+
*/
|
|
27
|
+
constructor(file, line, column, reason) {
|
|
28
|
+
super(`${file}:${line}:${column}: ${reason}`);
|
|
29
|
+
this.file = file;
|
|
30
|
+
this.line = line;
|
|
31
|
+
this.column = column;
|
|
32
|
+
this.reason = reason;
|
|
33
|
+
this.name = "DocumentParseError";
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
exports.DocumentParseError = DocumentParseError;
|
|
37
|
+
const positiveInteger = (value) => typeof value === "number" && Number.isInteger(value) && value > 0 ? value : undefined;
|
|
38
|
+
const toParseError = (error, file) => {
|
|
39
|
+
const message = typeof error === "object" && error !== null ? error : {};
|
|
40
|
+
const reason = typeof message.reason === "string"
|
|
41
|
+
? message.reason
|
|
42
|
+
: typeof message.message === "string"
|
|
43
|
+
? message.message
|
|
44
|
+
: String(error);
|
|
45
|
+
return new DocumentParseError(file, positiveInteger(message.line) ?? 1, positiveInteger(message.column) ?? 1, reason);
|
|
46
|
+
};
|
|
47
|
+
/** Whether `file` is parsed as MDX: its extension is `.mdx`, in any case. */
|
|
48
|
+
const isMdxFile = (file) => file.toLowerCase().endsWith(".mdx");
|
|
49
|
+
exports.isMdxFile = isMdxFile;
|
|
50
|
+
/**
|
|
51
|
+
* Parse a markdown document into mdast with GFM and YAML frontmatter, the way every document this
|
|
52
|
+
* library checks is read: a leading `---` block becomes a `yaml` node rather than content, while `+++` fences stay plain
|
|
53
|
+
* markdown, as on GitHub. A `.mdx` `file` is parsed with the MDX extensions too, as Storybook's MDX 3
|
|
54
|
+
* compiler reads it: ESM becomes `mdxjsEsm`, JSX `mdxJsxFlowElement`/`mdxJsxTextElement` (markdown in
|
|
55
|
+
* their children stays mdast; attributes are not children), and `{expressions}`, comments included,
|
|
56
|
+
* `mdxFlowExpression`/`mdxTextExpression` leaves. Plain markdown always parses; invalid MDX throws a
|
|
57
|
+
* {@link DocumentParseError} naming `file` and the location.
|
|
58
|
+
*/
|
|
59
|
+
const parseMarkdown = (source, file) => {
|
|
60
|
+
const mdx = (0, exports.isMdxFile)(file);
|
|
61
|
+
try {
|
|
62
|
+
return (0, mdast_util_from_markdown_1.fromMarkdown)(source, {
|
|
63
|
+
extensions: [(0, micromark_extension_gfm_1.gfm)(), (0, micromark_extension_frontmatter_1.frontmatter)(["yaml"]), ...(mdx ? [(0, micromark_extension_mdxjs_1.mdxjs)()] : [])],
|
|
64
|
+
mdastExtensions: [(0, mdast_util_gfm_1.gfmFromMarkdown)(), (0, mdast_util_frontmatter_1.frontmatterFromMarkdown)(["yaml"]), ...(mdx ? [(0, mdast_util_mdx_1.mdxFromMarkdown)()] : [])],
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
throw toParseError(error, file);
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
exports.parseMarkdown = parseMarkdown;
|
|
72
|
+
//# sourceMappingURL=parse.js.map
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Placeholder conventions in documented paths, such as `docs/<skill>/SKILL.md`.
|
|
3
|
+
*
|
|
4
|
+
* A reference's path part is compiled segment by segment (split on `/`). A segment without a
|
|
5
|
+
* placeholder is `literal`; one with placeholders is a `pattern` of text and named slots. Each slot
|
|
6
|
+
* matches one or more characters of a single directory entry name, so it never matches across `/`
|
|
7
|
+
* and never matches the empty string. Slots with the same name, in any segment and from any
|
|
8
|
+
* convention, must bind to the same value. Text beside a slot (`eval-` in `eval-<skill>`) is matched
|
|
9
|
+
* case-exactly, so a case difference there reads as no match rather than a case mismatch.
|
|
10
|
+
*
|
|
11
|
+
* Named conventions:
|
|
12
|
+
* - `angle`: `<name>`.
|
|
13
|
+
* - `curly`: `{name}`, but not a brace pair that is part of `{{name}}`.
|
|
14
|
+
* - `double-curly`: `{{name}}`.
|
|
15
|
+
* - `colon`: `:name` (a letter or `_`, then word characters), only at the start of a segment, so
|
|
16
|
+
* `/:id/` and `:id.md` are placeholders but `app:build` is literal.
|
|
17
|
+
* - `path-to`: a leading `path/to/` (after any `./`) followed by at least one segment marks the
|
|
18
|
+
* whole reference as illustrative, so it is never checked (see `isIllustrativeReference`).
|
|
19
|
+
*
|
|
20
|
+
* Any other string is a custom regex, applied within a segment, whose single capture group is the
|
|
21
|
+
* placeholder name, e.g. `\[(\w+)\]` for `[slug]`. Where conventions overlap at a position, the
|
|
22
|
+
* longest match wins. `*` is not a placeholder: globs are skipped before resolution.
|
|
23
|
+
*/
|
|
24
|
+
export declare const NAMED_PLACEHOLDERS: readonly ["angle", "curly", "double-curly", "colon", "path-to"];
|
|
25
|
+
export type NamedPlaceholder = (typeof NAMED_PLACEHOLDERS)[number];
|
|
26
|
+
/** Every named convention; a single constant so measurement can narrow the defaults in one place. */
|
|
27
|
+
export declare const DEFAULT_PLACEHOLDERS: ReadonlyArray<string>;
|
|
28
|
+
export type PatternPart = {
|
|
29
|
+
readonly kind: "text";
|
|
30
|
+
readonly value: string;
|
|
31
|
+
} | {
|
|
32
|
+
readonly kind: "slot";
|
|
33
|
+
readonly name: string;
|
|
34
|
+
};
|
|
35
|
+
export type SegmentPattern = {
|
|
36
|
+
readonly kind: "literal";
|
|
37
|
+
readonly value: string;
|
|
38
|
+
} | {
|
|
39
|
+
readonly kind: "pattern";
|
|
40
|
+
readonly parts: ReadonlyArray<PatternPart>;
|
|
41
|
+
};
|
|
42
|
+
export interface CompiledReference {
|
|
43
|
+
readonly segments: ReadonlyArray<SegmentPattern>;
|
|
44
|
+
}
|
|
45
|
+
/** Placeholder name to the entry-name substring it matched. */
|
|
46
|
+
export type Bindings = ReadonlyMap<string, string>;
|
|
47
|
+
/** The number of capture groups in a regex source, or `undefined` when it doesn't compile. */
|
|
48
|
+
export declare const countCaptureGroups: (source: string) => number | undefined;
|
|
49
|
+
/**
|
|
50
|
+
* Whether `pathPart` starts with `path/to/` (after any `./`) and names something below it, with
|
|
51
|
+
* `path-to` enabled. Such a reference stands for "some file in your project", not a real path.
|
|
52
|
+
*/
|
|
53
|
+
export declare const isIllustrativeReference: (pathPart: string, conventions: ReadonlyArray<string>) => boolean;
|
|
54
|
+
/**
|
|
55
|
+
* Compile a reference's path part (fragment stripped, URL-decoded, leading `/` removed) into
|
|
56
|
+
* per-segment matchers. Returns `undefined` when it has no placeholder under `conventions`, so the
|
|
57
|
+
* literal resolution applies unchanged. `.`, `..`, and empty segments stay literal; the resolver
|
|
58
|
+
* applies them lexically.
|
|
59
|
+
*/
|
|
60
|
+
export declare const compileReference: (pathPart: string, conventions: ReadonlyArray<string>) => CompiledReference | undefined;
|
|
61
|
+
/**
|
|
62
|
+
* Whether some segment has two placeholders with no text between them (`<a><b>.md`). Such a split
|
|
63
|
+
* is arbitrary and its regex backtracks polynomially on names that don't match, so it is skipped.
|
|
64
|
+
*/
|
|
65
|
+
export declare const hasAdjacentSlots: ({ segments }: CompiledReference) => boolean;
|
|
66
|
+
/**
|
|
67
|
+
* A matcher for one directory entry name against `segment`, given the names bound by earlier
|
|
68
|
+
* segments. It returns the extended bindings, or `undefined` when the name doesn't match. Already
|
|
69
|
+
* bound names must reappear verbatim. When a segment has several unbound slots, only the regex
|
|
70
|
+
* engine's first split is tried (e.g. `<a>-<b>` on `x-y-z` binds `a=x-y`).
|
|
71
|
+
*/
|
|
72
|
+
export declare const createSegmentMatcher: (segment: Extract<SegmentPattern, {
|
|
73
|
+
kind: "pattern";
|
|
74
|
+
}>, bindings: Bindings) => ((name: string) => Bindings | undefined);
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Placeholder conventions in documented paths, such as `docs/<skill>/SKILL.md`.
|
|
4
|
+
*
|
|
5
|
+
* A reference's path part is compiled segment by segment (split on `/`). A segment without a
|
|
6
|
+
* placeholder is `literal`; one with placeholders is a `pattern` of text and named slots. Each slot
|
|
7
|
+
* matches one or more characters of a single directory entry name, so it never matches across `/`
|
|
8
|
+
* and never matches the empty string. Slots with the same name, in any segment and from any
|
|
9
|
+
* convention, must bind to the same value. Text beside a slot (`eval-` in `eval-<skill>`) is matched
|
|
10
|
+
* case-exactly, so a case difference there reads as no match rather than a case mismatch.
|
|
11
|
+
*
|
|
12
|
+
* Named conventions:
|
|
13
|
+
* - `angle`: `<name>`.
|
|
14
|
+
* - `curly`: `{name}`, but not a brace pair that is part of `{{name}}`.
|
|
15
|
+
* - `double-curly`: `{{name}}`.
|
|
16
|
+
* - `colon`: `:name` (a letter or `_`, then word characters), only at the start of a segment, so
|
|
17
|
+
* `/:id/` and `:id.md` are placeholders but `app:build` is literal.
|
|
18
|
+
* - `path-to`: a leading `path/to/` (after any `./`) followed by at least one segment marks the
|
|
19
|
+
* whole reference as illustrative, so it is never checked (see `isIllustrativeReference`).
|
|
20
|
+
*
|
|
21
|
+
* Any other string is a custom regex, applied within a segment, whose single capture group is the
|
|
22
|
+
* placeholder name, e.g. `\[(\w+)\]` for `[slug]`. Where conventions overlap at a position, the
|
|
23
|
+
* longest match wins. `*` is not a placeholder: globs are skipped before resolution.
|
|
24
|
+
*/
|
|
25
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
26
|
+
exports.createSegmentMatcher = exports.hasAdjacentSlots = exports.compileReference = exports.isIllustrativeReference = exports.countCaptureGroups = exports.DEFAULT_PLACEHOLDERS = exports.NAMED_PLACEHOLDERS = void 0;
|
|
27
|
+
exports.NAMED_PLACEHOLDERS = ["angle", "curly", "double-curly", "colon", "path-to"];
|
|
28
|
+
/** Every named convention; a single constant so measurement can narrow the defaults in one place. */
|
|
29
|
+
exports.DEFAULT_PLACEHOLDERS = exports.NAMED_PLACEHOLDERS;
|
|
30
|
+
/** `path-to` marks a whole reference, so it has no in-segment token. */
|
|
31
|
+
const NAMED_TOKEN_SOURCES = {
|
|
32
|
+
angle: "<([^<>]+)>",
|
|
33
|
+
curly: "(?<!\\{)\\{([^{}]+)\\}(?!\\})",
|
|
34
|
+
"double-curly": "\\{\\{([^{}]+)\\}\\}",
|
|
35
|
+
colon: ":([A-Za-z_]\\w*)",
|
|
36
|
+
"path-to": undefined,
|
|
37
|
+
};
|
|
38
|
+
const isNamedPlaceholder = (convention) => exports.NAMED_PLACEHOLDERS.some(named => named === convention);
|
|
39
|
+
/** The number of capture groups in a regex source, or `undefined` when it doesn't compile. */
|
|
40
|
+
const countCaptureGroups = (source) => {
|
|
41
|
+
try {
|
|
42
|
+
return (new RegExp(`${source}|`).exec("")?.length ?? 1) - 1;
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
exports.countCaptureGroups = countCaptureGroups;
|
|
49
|
+
const scannerFor = (convention) => {
|
|
50
|
+
const source = isNamedPlaceholder(convention) ? NAMED_TOKEN_SOURCES[convention] : convention;
|
|
51
|
+
return source === undefined
|
|
52
|
+
? undefined
|
|
53
|
+
: { regex: new RegExp(source, "y"), segmentStartOnly: convention === "colon" };
|
|
54
|
+
};
|
|
55
|
+
const compiledScanners = new WeakMap();
|
|
56
|
+
const scannersFor = (conventions) => {
|
|
57
|
+
let scanners = compiledScanners.get(conventions);
|
|
58
|
+
if (!scanners) {
|
|
59
|
+
scanners = conventions.flatMap(convention => scannerFor(convention) ?? []);
|
|
60
|
+
compiledScanners.set(conventions, scanners);
|
|
61
|
+
}
|
|
62
|
+
return scanners;
|
|
63
|
+
};
|
|
64
|
+
/** The longest placeholder token starting at `index`, if any. */
|
|
65
|
+
const tokenAt = (segment, index, scanners) => {
|
|
66
|
+
let best;
|
|
67
|
+
for (const { regex, segmentStartOnly } of scanners) {
|
|
68
|
+
if (segmentStartOnly && index !== 0)
|
|
69
|
+
continue;
|
|
70
|
+
regex.lastIndex = index;
|
|
71
|
+
const match = regex.exec(segment);
|
|
72
|
+
const name = match?.[1];
|
|
73
|
+
if (match && name && match[0].length > (best?.length ?? 0)) {
|
|
74
|
+
best = { length: match[0].length, name };
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return best;
|
|
78
|
+
};
|
|
79
|
+
const compileSegment = (segment, scanners) => {
|
|
80
|
+
const parts = [];
|
|
81
|
+
let text = "";
|
|
82
|
+
let index = 0;
|
|
83
|
+
while (index < segment.length) {
|
|
84
|
+
const token = tokenAt(segment, index, scanners);
|
|
85
|
+
if (token) {
|
|
86
|
+
if (text !== "")
|
|
87
|
+
parts.push({ kind: "text", value: text });
|
|
88
|
+
text = "";
|
|
89
|
+
parts.push({ kind: "slot", name: token.name });
|
|
90
|
+
index += token.length;
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
text += segment[index];
|
|
94
|
+
index++;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
if (!parts.some(part => part.kind === "slot")) {
|
|
98
|
+
return { kind: "literal", value: segment };
|
|
99
|
+
}
|
|
100
|
+
if (text !== "")
|
|
101
|
+
parts.push({ kind: "text", value: text });
|
|
102
|
+
return { kind: "pattern", parts };
|
|
103
|
+
};
|
|
104
|
+
/**
|
|
105
|
+
* Whether `pathPart` starts with `path/to/` (after any `./`) and names something below it, with
|
|
106
|
+
* `path-to` enabled. Such a reference stands for "some file in your project", not a real path.
|
|
107
|
+
*/
|
|
108
|
+
const isIllustrativeReference = (pathPart, conventions) => {
|
|
109
|
+
if (!conventions.includes("path-to"))
|
|
110
|
+
return false;
|
|
111
|
+
const segments = pathPart.split("/");
|
|
112
|
+
let start = 0;
|
|
113
|
+
while (segments[start] === ".")
|
|
114
|
+
start++;
|
|
115
|
+
return segments[start] === "path" && segments[start + 1] === "to" && segments.slice(start + 2).join("/") !== "";
|
|
116
|
+
};
|
|
117
|
+
exports.isIllustrativeReference = isIllustrativeReference;
|
|
118
|
+
/**
|
|
119
|
+
* Compile a reference's path part (fragment stripped, URL-decoded, leading `/` removed) into
|
|
120
|
+
* per-segment matchers. Returns `undefined` when it has no placeholder under `conventions`, so the
|
|
121
|
+
* literal resolution applies unchanged. `.`, `..`, and empty segments stay literal; the resolver
|
|
122
|
+
* applies them lexically.
|
|
123
|
+
*/
|
|
124
|
+
const compileReference = (pathPart, conventions) => {
|
|
125
|
+
const scanners = scannersFor(conventions);
|
|
126
|
+
const segments = pathPart.split("/").map(segment => compileSegment(segment, scanners));
|
|
127
|
+
return segments.some(segment => segment.kind !== "literal") ? { segments } : undefined;
|
|
128
|
+
};
|
|
129
|
+
exports.compileReference = compileReference;
|
|
130
|
+
/**
|
|
131
|
+
* Whether some segment has two placeholders with no text between them (`<a><b>.md`). Such a split
|
|
132
|
+
* is arbitrary and its regex backtracks polynomially on names that don't match, so it is skipped.
|
|
133
|
+
*/
|
|
134
|
+
const hasAdjacentSlots = ({ segments }) => segments.some(segment => segment.kind === "pattern" &&
|
|
135
|
+
segment.parts.some((part, index) => part.kind === "slot" && segment.parts[index + 1]?.kind === "slot"));
|
|
136
|
+
exports.hasAdjacentSlots = hasAdjacentSlots;
|
|
137
|
+
const escapeRegExp = (text) => text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
138
|
+
/**
|
|
139
|
+
* A matcher for one directory entry name against `segment`, given the names bound by earlier
|
|
140
|
+
* segments. It returns the extended bindings, or `undefined` when the name doesn't match. Already
|
|
141
|
+
* bound names must reappear verbatim. When a segment has several unbound slots, only the regex
|
|
142
|
+
* engine's first split is tried (e.g. `<a>-<b>` on `x-y-z` binds `a=x-y`).
|
|
143
|
+
*/
|
|
144
|
+
const createSegmentMatcher = (segment, bindings) => {
|
|
145
|
+
const groups = new Map();
|
|
146
|
+
const source = segment.parts
|
|
147
|
+
.map(part => {
|
|
148
|
+
if (part.kind === "text")
|
|
149
|
+
return escapeRegExp(part.value);
|
|
150
|
+
const bound = bindings.get(part.name);
|
|
151
|
+
if (bound !== undefined)
|
|
152
|
+
return escapeRegExp(bound);
|
|
153
|
+
const group = groups.get(part.name);
|
|
154
|
+
if (group !== undefined)
|
|
155
|
+
return `\\k<${group}>`;
|
|
156
|
+
const newGroup = `p${groups.size}`;
|
|
157
|
+
groups.set(part.name, newGroup);
|
|
158
|
+
return `(?<${newGroup}>[^/]+)`;
|
|
159
|
+
})
|
|
160
|
+
.join("");
|
|
161
|
+
const regex = new RegExp(`^${source}$`);
|
|
162
|
+
return name => {
|
|
163
|
+
const match = regex.exec(name);
|
|
164
|
+
if (!match)
|
|
165
|
+
return undefined;
|
|
166
|
+
const next = new Map(bindings);
|
|
167
|
+
for (const [placeholder, group] of groups) {
|
|
168
|
+
const value = match.groups?.[group];
|
|
169
|
+
if (value !== undefined)
|
|
170
|
+
next.set(placeholder, value);
|
|
171
|
+
}
|
|
172
|
+
return next;
|
|
173
|
+
};
|
|
174
|
+
};
|
|
175
|
+
exports.createSegmentMatcher = createSegmentMatcher;
|
|
176
|
+
//# sourceMappingURL=placeholders.js.map
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export interface RepoRootOptions {
|
|
2
|
+
/** Explicit repository root; a relative value resolves against `cwd`. */
|
|
3
|
+
readonly rootDir?: string;
|
|
4
|
+
/** Working directory a relative `rootDir` resolves against. Defaults to `process.cwd()`. */
|
|
5
|
+
readonly cwd?: string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Find the repository root for a directory: `rootDir` when configured, otherwise `startDir` or its
|
|
9
|
+
* nearest ancestor containing a `.git` directory or a `.git` file (a git worktree or submodule).
|
|
10
|
+
* Returns `undefined` when no repository encloses `startDir`.
|
|
11
|
+
*/
|
|
12
|
+
export declare const findRepoRootFromDir: (startDir: string, { rootDir, cwd }?: RepoRootOptions) => string | undefined;
|
|
13
|
+
/**
|
|
14
|
+
* Find the repository root for a checked document, as {@link findRepoRootFromDir} does from the
|
|
15
|
+
* document's directory. Walks up from the document rather than from `process.cwd()`, so checking
|
|
16
|
+
* from a subdirectory or a worktree resolves the same root.
|
|
17
|
+
*/
|
|
18
|
+
export declare const findRepoRoot: (documentPath: string, options?: RepoRootOptions) => string | undefined;
|