@descent-vtt/spec-brief 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/LICENSE +21 -0
  3. package/README.md +269 -0
  4. package/bin/spec-brief.js +19 -0
  5. package/dist/apply.d.ts +26 -0
  6. package/dist/apply.js +71 -0
  7. package/dist/apply.js.map +1 -0
  8. package/dist/archive.d.ts +81 -0
  9. package/dist/archive.js +333 -0
  10. package/dist/archive.js.map +1 -0
  11. package/dist/brief.d.ts +60 -0
  12. package/dist/brief.js +152 -0
  13. package/dist/brief.js.map +1 -0
  14. package/dist/cli.d.ts +35 -0
  15. package/dist/cli.js +411 -0
  16. package/dist/cli.js.map +1 -0
  17. package/dist/collisions.d.ts +50 -0
  18. package/dist/collisions.js +127 -0
  19. package/dist/collisions.js.map +1 -0
  20. package/dist/config.d.ts +94 -0
  21. package/dist/config.js +353 -0
  22. package/dist/config.js.map +1 -0
  23. package/dist/corpus.d.ts +41 -0
  24. package/dist/corpus.js +154 -0
  25. package/dist/corpus.js.map +1 -0
  26. package/dist/engine.d.ts +121 -0
  27. package/dist/engine.js +276 -0
  28. package/dist/engine.js.map +1 -0
  29. package/dist/frontmatter.d.ts +68 -0
  30. package/dist/frontmatter.js +311 -0
  31. package/dist/frontmatter.js.map +1 -0
  32. package/dist/fs.d.ts +59 -0
  33. package/dist/fs.js +189 -0
  34. package/dist/fs.js.map +1 -0
  35. package/dist/git.d.ts +59 -0
  36. package/dist/git.js +131 -0
  37. package/dist/git.js.map +1 -0
  38. package/dist/glob.d.ts +79 -0
  39. package/dist/glob.js +465 -0
  40. package/dist/glob.js.map +1 -0
  41. package/dist/index.d.ts +24 -0
  42. package/dist/index.js +26 -0
  43. package/dist/index.js.map +1 -0
  44. package/dist/integrity.d.ts +11 -0
  45. package/dist/integrity.js +20 -0
  46. package/dist/integrity.js.map +1 -0
  47. package/dist/links.d.ts +38 -0
  48. package/dist/links.js +142 -0
  49. package/dist/links.js.map +1 -0
  50. package/dist/lint.d.ts +38 -0
  51. package/dist/lint.js +90 -0
  52. package/dist/lint.js.map +1 -0
  53. package/dist/markdown.d.ts +65 -0
  54. package/dist/markdown.js +274 -0
  55. package/dist/markdown.js.map +1 -0
  56. package/dist/plugins.d.ts +16 -0
  57. package/dist/plugins.js +77 -0
  58. package/dist/plugins.js.map +1 -0
  59. package/dist/report.d.ts +38 -0
  60. package/dist/report.js +244 -0
  61. package/dist/report.js.map +1 -0
  62. package/dist/rules.d.ts +58 -0
  63. package/dist/rules.js +448 -0
  64. package/dist/rules.js.map +1 -0
  65. package/dist/scaffold.d.ts +25 -0
  66. package/dist/scaffold.js +81 -0
  67. package/dist/scaffold.js.map +1 -0
  68. package/dist/schema.d.ts +47 -0
  69. package/dist/schema.js +195 -0
  70. package/dist/schema.js.map +1 -0
  71. package/dist/text.d.ts +40 -0
  72. package/dist/text.js +95 -0
  73. package/dist/text.js.map +1 -0
  74. package/dist/types.d.ts +30 -0
  75. package/dist/types.js +5 -0
  76. package/dist/types.js.map +1 -0
  77. package/package.json +76 -0
  78. package/schema.json +321 -0
@@ -0,0 +1,311 @@
1
+ /**
2
+ * A front-matter reader and editor for the flat subset of YAML briefs use.
3
+ *
4
+ * Supported: `key: value` at the top level; plain, single-quoted and
5
+ * double-quoted scalars; inline `[a, b]` sequences; block `- item` sequences;
6
+ * `#` comments. Plain scalars follow the YAML 1.2 core schema, so `yes` is a
7
+ * word and `035` keeps its leading zero for whoever reads it as text.
8
+ *
9
+ * Anything richer - nested mappings, block scalars, anchors, tags, a value
10
+ * continued onto the next line - is recognised and reported as unsupported
11
+ * rather than guessed at. A YAML library would parse more, and would be the
12
+ * largest dependency of a package that has none; it would also not give back
13
+ * the line of every value, and an edit that must leave every other line of a
14
+ * file untouched needs exactly that.
15
+ */
16
+ const OPEN = /^---[ \t]*$/;
17
+ const CLOSE = /^(?:---|\.\.\.)[ \t]*$/;
18
+ const KEY = /^([A-Za-z_][\w.-]*)[ \t]*:(?:[ \t]+(.*))?$/;
19
+ const SEQUENCE_ITEM = /^([ \t]*)-(?:[ \t]+(.*))?$/;
20
+ const BLANK_OR_COMMENT = /^[ \t]*(?:#.*)?$/;
21
+ export function keyName(key) {
22
+ return key.toLowerCase().replace(/[-_]/g, '');
23
+ }
24
+ /** Reads the front matter at the top of `lines`, or `null` when there is none. */
25
+ export function readFrontMatter(lines) {
26
+ if (lines.length === 0 || !OPEN.test(lines[0]))
27
+ return null;
28
+ let close = -1;
29
+ for (let i = 1; i < lines.length; i += 1) {
30
+ if (CLOSE.test(lines[i])) {
31
+ close = i;
32
+ break;
33
+ }
34
+ }
35
+ if (close < 0) {
36
+ return {
37
+ close: -1,
38
+ entries: [],
39
+ problems: [{ line: 0, message: 'the front matter opened on line 1 is never closed' }],
40
+ };
41
+ }
42
+ const entries = [];
43
+ const problems = [];
44
+ const seen = new Map();
45
+ let i = 1;
46
+ while (i < close) {
47
+ const line = lines[i];
48
+ if (BLANK_OR_COMMENT.test(line)) {
49
+ i += 1;
50
+ continue;
51
+ }
52
+ const match = KEY.exec(line);
53
+ if (!match) {
54
+ problems.push({
55
+ line: i,
56
+ message: /^\s/.test(line) ? 'an indented line belongs to no key' : 'not a "key: value" line',
57
+ });
58
+ i += 1;
59
+ continue;
60
+ }
61
+ const key = match[1];
62
+ const inline = stripLeadingComment((match[2] ?? '').trim());
63
+ let end = i + 1;
64
+ while (end < close && belongsToBlock(lines[end]))
65
+ end += 1;
66
+ while (end > i + 1 && BLANK_OR_COMMENT.test(lines[end - 1]))
67
+ end -= 1;
68
+ const block = lines.slice(i + 1, end);
69
+ let value;
70
+ if (inline.length > 0) {
71
+ value = block.some((l) => !BLANK_OR_COMMENT.test(l))
72
+ ? unsupported('the value continues on the next line; keep it on one line, or quote it')
73
+ : parseInline(inline);
74
+ }
75
+ else {
76
+ value = parseBlock(block);
77
+ }
78
+ const name = keyName(key);
79
+ const previous = seen.get(name);
80
+ if (previous !== undefined) {
81
+ problems.push({ line: i, message: `"${key}" is declared twice (first on line ${previous + 1})` });
82
+ }
83
+ else {
84
+ seen.set(name, i);
85
+ }
86
+ entries.push({ key, name, line: i, end, value });
87
+ i = end;
88
+ }
89
+ return { close, entries, problems };
90
+ }
91
+ /** A line under a key is part of its value when it is indented or starts a sequence item. */
92
+ function belongsToBlock(line) {
93
+ return /^[ \t]/.test(line) || /^-(?:[ \t]|$)/.test(line) || line.trim() === '';
94
+ }
95
+ function stripLeadingComment(text) {
96
+ return text.startsWith('#') ? '' : text;
97
+ }
98
+ function unsupported(reason) {
99
+ return { kind: 'unsupported', reason };
100
+ }
101
+ function parseBlock(block) {
102
+ const content = block.filter((l) => !BLANK_OR_COMMENT.test(l));
103
+ if (content.length === 0)
104
+ return { kind: 'scalar', scalar: { text: '', quoted: false } };
105
+ const first = SEQUENCE_ITEM.exec(content[0]);
106
+ if (!first) {
107
+ return KEY.test(content[0].trim())
108
+ ? unsupported('nested mappings are not supported; flatten the key')
109
+ : unsupported('the value continues on the next line; keep it on one line, or quote it');
110
+ }
111
+ const indent = first[1].length;
112
+ const items = [];
113
+ for (const line of content) {
114
+ const item = SEQUENCE_ITEM.exec(line);
115
+ if (!item || item[1].length !== indent) {
116
+ return unsupported('a list item is continued or nested; keep each item on one line');
117
+ }
118
+ const text = stripLeadingComment((item[2] ?? '').trim());
119
+ if (text.length === 0)
120
+ return unsupported('a list item is empty');
121
+ const parsed = parseInline(text);
122
+ if (parsed.kind !== 'scalar') {
123
+ return parsed.kind === 'list' ? unsupported('nested lists are not supported') : parsed;
124
+ }
125
+ items.push(parsed.scalar);
126
+ }
127
+ return { kind: 'list', items };
128
+ }
129
+ /** Parses a value written on the key's own line. `text` is trimmed and non-empty. */
130
+ export function parseInline(text) {
131
+ const head = text.charAt(0);
132
+ if (head === '"' || head === "'") {
133
+ const quoted = head === '"' ? readDoubleQuoted(text) : readSingleQuoted(text);
134
+ if (typeof quoted === 'string')
135
+ return unsupported(quoted);
136
+ return trailingIsComment(text.slice(quoted.next))
137
+ ? { kind: 'scalar', scalar: { text: quoted.text, quoted: true } }
138
+ : unsupported('text follows a closing quote');
139
+ }
140
+ if (head === '[')
141
+ return parseFlowSequence(text);
142
+ if (head === '{')
143
+ return unsupported('inline mappings are not supported');
144
+ if (head === '|' || head === '>')
145
+ return unsupported('block scalars are not supported; keep the value on one line');
146
+ if (head === '&' || head === '*' || head === '!')
147
+ return unsupported('anchors, aliases and tags are not supported');
148
+ if (head === '@' || head === '`')
149
+ return unsupported(`a plain value cannot start with "${head}"; quote it`);
150
+ const plain = stripTrailingComment(text);
151
+ if (/:(?:\s|$)/.test(plain))
152
+ return unsupported('a plain value cannot contain ": "; quote it');
153
+ if (/^-(?:\s|$)/.test(plain))
154
+ return unsupported('a list must start on the line after its key');
155
+ return { kind: 'scalar', scalar: { text: plain, quoted: false } };
156
+ }
157
+ function stripTrailingComment(text) {
158
+ const hash = text.search(/\s#/);
159
+ return (hash < 0 ? text : text.slice(0, hash)).trim();
160
+ }
161
+ function trailingIsComment(rest) {
162
+ return /^\s*(?:#.*)?$/.test(rest) && (rest.trim() === '' || /^\s/.test(rest));
163
+ }
164
+ const ESCAPES = {
165
+ '"': '"',
166
+ '\\': '\\',
167
+ '/': '/',
168
+ '0': '\0',
169
+ b: '\b',
170
+ f: '\f',
171
+ n: '\n',
172
+ r: '\r',
173
+ t: '\t',
174
+ };
175
+ const HEX_ESCAPE_LENGTH = { x: 2, u: 4, U: 8 };
176
+ /** Reads a double-quoted scalar starting at index 0, or returns why it cannot. */
177
+ function readDoubleQuoted(text) {
178
+ let out = '';
179
+ for (let i = 1; i < text.length; i += 1) {
180
+ const ch = text.charAt(i);
181
+ if (ch === '"')
182
+ return { text: out, next: i + 1 };
183
+ if (ch !== '\\') {
184
+ out += ch;
185
+ continue;
186
+ }
187
+ const code = text.charAt(i + 1);
188
+ const simple = ESCAPES[code];
189
+ if (simple !== undefined) {
190
+ out += simple;
191
+ i += 1;
192
+ continue;
193
+ }
194
+ const width = HEX_ESCAPE_LENGTH[code];
195
+ const hex = width === undefined ? '' : text.slice(i + 2, i + 2 + width);
196
+ if (width === undefined || hex.length !== width || !/^[0-9A-Fa-f]+$/.test(hex)) {
197
+ return `"\\${code}" is not an escape this reader knows`;
198
+ }
199
+ const point = Number.parseInt(hex, 16);
200
+ if (point > 0x10ffff)
201
+ return `"\\${code}${hex}" is not a character`;
202
+ out += String.fromCodePoint(point);
203
+ i += 1 + width;
204
+ }
205
+ return 'a double-quoted value is never closed';
206
+ }
207
+ function readSingleQuoted(text) {
208
+ let out = '';
209
+ for (let i = 1; i < text.length; i += 1) {
210
+ const ch = text.charAt(i);
211
+ if (ch !== "'") {
212
+ out += ch;
213
+ continue;
214
+ }
215
+ if (text.charAt(i + 1) === "'") {
216
+ out += "'";
217
+ i += 1;
218
+ continue;
219
+ }
220
+ return { text: out, next: i + 1 };
221
+ }
222
+ return 'a single-quoted value is never closed';
223
+ }
224
+ function parseFlowSequence(text) {
225
+ const items = [];
226
+ let i = 1;
227
+ for (;;) {
228
+ while (i < text.length && /\s/.test(text.charAt(i)))
229
+ i += 1;
230
+ if (i >= text.length)
231
+ return unsupported('an inline list is never closed; keep it on one line');
232
+ const ch = text.charAt(i);
233
+ if (ch === ']') {
234
+ return trailingIsComment(text.slice(i + 1))
235
+ ? { kind: 'list', items }
236
+ : unsupported('text follows the end of an inline list');
237
+ }
238
+ if (ch === '[' || ch === '{')
239
+ return unsupported('nested lists and mappings are not supported');
240
+ if (ch === ',')
241
+ return unsupported('an inline list has an empty item');
242
+ let item;
243
+ if (ch === '"' || ch === "'") {
244
+ const rest = text.slice(i);
245
+ const quoted = ch === '"' ? readDoubleQuoted(rest) : readSingleQuoted(rest);
246
+ if (typeof quoted === 'string')
247
+ return unsupported(quoted);
248
+ item = { text: quoted.text, quoted: true };
249
+ i += quoted.next;
250
+ }
251
+ else {
252
+ let j = i;
253
+ while (j < text.length && !',]'.includes(text.charAt(j)))
254
+ j += 1;
255
+ const plain = text.slice(i, j).trim();
256
+ if (/[[{]/.test(plain) || /:(?:\s|$)/.test(plain) || plain.includes(' #')) {
257
+ return unsupported(`"${plain}" needs quoting inside an inline list`);
258
+ }
259
+ item = { text: plain, quoted: false };
260
+ i = j;
261
+ }
262
+ items.push(item);
263
+ while (i < text.length && /\s/.test(text.charAt(i)))
264
+ i += 1;
265
+ if (i >= text.length)
266
+ return unsupported('an inline list is never closed; keep it on one line');
267
+ // After a comma the next pass reads an item or, legally in YAML, the bracket.
268
+ if (text.charAt(i) === ',')
269
+ i += 1;
270
+ else if (text.charAt(i) !== ']')
271
+ return unsupported('inline list items must be separated by commas');
272
+ }
273
+ }
274
+ /** Null in the YAML 1.2 core schema: empty, `~` or `null` in any of its three spellings. */
275
+ export function isNull(scalar) {
276
+ return !scalar.quoted && ['', '~', 'null', 'Null', 'NULL'].includes(scalar.text);
277
+ }
278
+ /** Renders a string as a scalar that reads back as the same string. */
279
+ export function renderScalar(value) {
280
+ const reserved = /^(?:true|false|null|yes|no|on|off|~)$/i.test(value);
281
+ const plain = /^[A-Za-z][A-Za-z0-9 ._/+-]*$/.test(value) && !value.endsWith(' ');
282
+ return plain && !reserved ? value : JSON.stringify(value);
283
+ }
284
+ export function findEntry(frontMatter, key) {
285
+ const name = keyName(key);
286
+ return frontMatter?.entries.find((entry) => entry.name === name);
287
+ }
288
+ /**
289
+ * Sets one key, leaving every other line as it was. An existing entry keeps the
290
+ * spelling of its key; a new one goes last. A file with no front matter gains
291
+ * a block.
292
+ */
293
+ export function setEntry(lines, frontMatter, key, rendered) {
294
+ if (frontMatter === null)
295
+ return ['---', `${key}: ${rendered}`, '---', ...lines];
296
+ if (frontMatter.close < 0)
297
+ throw new Error('cannot edit front matter that is never closed');
298
+ const entry = findEntry(frontMatter, key);
299
+ if (entry === undefined) {
300
+ return [...lines.slice(0, frontMatter.close), `${key}: ${rendered}`, ...lines.slice(frontMatter.close)];
301
+ }
302
+ return [...lines.slice(0, entry.line), `${entry.key}: ${rendered}`, ...lines.slice(entry.end)];
303
+ }
304
+ /** Removes one key and its value lines; a key that is absent changes nothing. */
305
+ export function removeEntry(lines, frontMatter, key) {
306
+ const entry = findEntry(frontMatter, key);
307
+ if (entry === undefined)
308
+ return [...lines];
309
+ return [...lines.slice(0, entry.line), ...lines.slice(entry.end)];
310
+ }
311
+ //# sourceMappingURL=frontmatter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"frontmatter.js","sourceRoot":"","sources":["../src/frontmatter.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * A front-matter reader and editor for the flat subset of YAML briefs use.\n *\n * Supported: `key: value` at the top level; plain, single-quoted and\n * double-quoted scalars; inline `[a, b]` sequences; block `- item` sequences;\n * `#` comments. Plain scalars follow the YAML 1.2 core schema, so `yes` is a\n * word and `035` keeps its leading zero for whoever reads it as text.\n *\n * Anything richer - nested mappings, block scalars, anchors, tags, a value\n * continued onto the next line - is recognised and reported as unsupported\n * rather than guessed at. A YAML library would parse more, and would be the\n * largest dependency of a package that has none; it would also not give back\n * the line of every value, and an edit that must leave every other line of a\n * file untouched needs exactly that.\n */\n\nexport interface YamlScalar {\n readonly text: string;\n readonly quoted: boolean;\n}\n\nexport type YamlValue =\n | { readonly kind: 'scalar'; readonly scalar: YamlScalar }\n | { readonly kind: 'list'; readonly items: readonly YamlScalar[] }\n | { readonly kind: 'unsupported'; readonly reason: string };\n\nexport interface FrontMatterEntry {\n /** The key as written. */\n readonly key: string;\n /** The key compared: lower case, without `-` or `_`, so `depends-on` is `dependsOn`. */\n readonly name: string;\n /** 0-based line of the key. */\n readonly line: number;\n /** 0-based line after the entry's last line. */\n readonly end: number;\n readonly value: YamlValue;\n}\n\nexport interface FrontMatterProblem {\n readonly line: number;\n readonly message: string;\n}\n\nexport interface FrontMatter {\n /** 0-based line of the closing delimiter, or -1 when the block is never closed. */\n readonly close: number;\n readonly entries: readonly FrontMatterEntry[];\n readonly problems: readonly FrontMatterProblem[];\n}\n\nconst OPEN = /^---[ \\t]*$/;\nconst CLOSE = /^(?:---|\\.\\.\\.)[ \\t]*$/;\nconst KEY = /^([A-Za-z_][\\w.-]*)[ \\t]*:(?:[ \\t]+(.*))?$/;\nconst SEQUENCE_ITEM = /^([ \\t]*)-(?:[ \\t]+(.*))?$/;\nconst BLANK_OR_COMMENT = /^[ \\t]*(?:#.*)?$/;\n\nexport function keyName(key: string): string {\n return key.toLowerCase().replace(/[-_]/g, '');\n}\n\n/** Reads the front matter at the top of `lines`, or `null` when there is none. */\nexport function readFrontMatter(lines: readonly string[]): FrontMatter | null {\n if (lines.length === 0 || !OPEN.test(lines[0] as string)) return null;\n let close = -1;\n for (let i = 1; i < lines.length; i += 1) {\n if (CLOSE.test(lines[i] as string)) {\n close = i;\n break;\n }\n }\n if (close < 0) {\n return {\n close: -1,\n entries: [],\n problems: [{ line: 0, message: 'the front matter opened on line 1 is never closed' }],\n };\n }\n\n const entries: FrontMatterEntry[] = [];\n const problems: FrontMatterProblem[] = [];\n const seen = new Map<string, number>();\n let i = 1;\n while (i < close) {\n const line = lines[i] as string;\n if (BLANK_OR_COMMENT.test(line)) {\n i += 1;\n continue;\n }\n const match = KEY.exec(line);\n if (!match) {\n problems.push({\n line: i,\n message: /^\\s/.test(line) ? 'an indented line belongs to no key' : 'not a \"key: value\" line',\n });\n i += 1;\n continue;\n }\n const key = match[1] as string;\n const inline = stripLeadingComment((match[2] ?? '').trim());\n let end = i + 1;\n while (end < close && belongsToBlock(lines[end] as string)) end += 1;\n while (end > i + 1 && BLANK_OR_COMMENT.test(lines[end - 1] as string)) end -= 1;\n const block = lines.slice(i + 1, end);\n\n let value: YamlValue;\n if (inline.length > 0) {\n value = block.some((l) => !BLANK_OR_COMMENT.test(l))\n ? unsupported('the value continues on the next line; keep it on one line, or quote it')\n : parseInline(inline);\n } else {\n value = parseBlock(block);\n }\n\n const name = keyName(key);\n const previous = seen.get(name);\n if (previous !== undefined) {\n problems.push({ line: i, message: `\"${key}\" is declared twice (first on line ${previous + 1})` });\n } else {\n seen.set(name, i);\n }\n entries.push({ key, name, line: i, end, value });\n i = end;\n }\n return { close, entries, problems };\n}\n\n/** A line under a key is part of its value when it is indented or starts a sequence item. */\nfunction belongsToBlock(line: string): boolean {\n return /^[ \\t]/.test(line) || /^-(?:[ \\t]|$)/.test(line) || line.trim() === '';\n}\n\nfunction stripLeadingComment(text: string): string {\n return text.startsWith('#') ? '' : text;\n}\n\nfunction unsupported(reason: string): YamlValue {\n return { kind: 'unsupported', reason };\n}\n\nfunction parseBlock(block: readonly string[]): YamlValue {\n const content = block.filter((l) => !BLANK_OR_COMMENT.test(l));\n if (content.length === 0) return { kind: 'scalar', scalar: { text: '', quoted: false } };\n const first = SEQUENCE_ITEM.exec(content[0] as string);\n if (!first) {\n return KEY.test((content[0] as string).trim())\n ? unsupported('nested mappings are not supported; flatten the key')\n : unsupported('the value continues on the next line; keep it on one line, or quote it');\n }\n const indent = (first[1] as string).length;\n const items: YamlScalar[] = [];\n for (const line of content) {\n const item = SEQUENCE_ITEM.exec(line);\n if (!item || (item[1] as string).length !== indent) {\n return unsupported('a list item is continued or nested; keep each item on one line');\n }\n const text = stripLeadingComment((item[2] ?? '').trim());\n if (text.length === 0) return unsupported('a list item is empty');\n const parsed = parseInline(text);\n if (parsed.kind !== 'scalar') {\n return parsed.kind === 'list' ? unsupported('nested lists are not supported') : parsed;\n }\n items.push(parsed.scalar);\n }\n return { kind: 'list', items };\n}\n\n/** Parses a value written on the key's own line. `text` is trimmed and non-empty. */\nexport function parseInline(text: string): YamlValue {\n const head = text.charAt(0);\n if (head === '\"' || head === \"'\") {\n const quoted = head === '\"' ? readDoubleQuoted(text) : readSingleQuoted(text);\n if (typeof quoted === 'string') return unsupported(quoted);\n return trailingIsComment(text.slice(quoted.next))\n ? { kind: 'scalar', scalar: { text: quoted.text, quoted: true } }\n : unsupported('text follows a closing quote');\n }\n if (head === '[') return parseFlowSequence(text);\n if (head === '{') return unsupported('inline mappings are not supported');\n if (head === '|' || head === '>') return unsupported('block scalars are not supported; keep the value on one line');\n if (head === '&' || head === '*' || head === '!') return unsupported('anchors, aliases and tags are not supported');\n if (head === '@' || head === '`') return unsupported(`a plain value cannot start with \"${head}\"; quote it`);\n const plain = stripTrailingComment(text);\n if (/:(?:\\s|$)/.test(plain)) return unsupported('a plain value cannot contain \": \"; quote it');\n if (/^-(?:\\s|$)/.test(plain)) return unsupported('a list must start on the line after its key');\n return { kind: 'scalar', scalar: { text: plain, quoted: false } };\n}\n\nfunction stripTrailingComment(text: string): string {\n const hash = text.search(/\\s#/);\n return (hash < 0 ? text : text.slice(0, hash)).trim();\n}\n\nfunction trailingIsComment(rest: string): boolean {\n return /^\\s*(?:#.*)?$/.test(rest) && (rest.trim() === '' || /^\\s/.test(rest));\n}\n\ninterface Quoted {\n readonly text: string;\n /** Index just past the closing quote. */\n readonly next: number;\n}\n\nconst ESCAPES: Readonly<Record<string, string>> = {\n '\"': '\"',\n '\\\\': '\\\\',\n '/': '/',\n '0': '\\0',\n b: '\\b',\n f: '\\f',\n n: '\\n',\n r: '\\r',\n t: '\\t',\n};\n\nconst HEX_ESCAPE_LENGTH: Readonly<Record<string, number>> = { x: 2, u: 4, U: 8 };\n\n/** Reads a double-quoted scalar starting at index 0, or returns why it cannot. */\nfunction readDoubleQuoted(text: string): Quoted | string {\n let out = '';\n for (let i = 1; i < text.length; i += 1) {\n const ch = text.charAt(i);\n if (ch === '\"') return { text: out, next: i + 1 };\n if (ch !== '\\\\') {\n out += ch;\n continue;\n }\n const code = text.charAt(i + 1);\n const simple = ESCAPES[code];\n if (simple !== undefined) {\n out += simple;\n i += 1;\n continue;\n }\n const width = HEX_ESCAPE_LENGTH[code];\n const hex = width === undefined ? '' : text.slice(i + 2, i + 2 + width);\n if (width === undefined || hex.length !== width || !/^[0-9A-Fa-f]+$/.test(hex)) {\n return `\"\\\\${code}\" is not an escape this reader knows`;\n }\n const point = Number.parseInt(hex, 16);\n if (point > 0x10ffff) return `\"\\\\${code}${hex}\" is not a character`;\n out += String.fromCodePoint(point);\n i += 1 + width;\n }\n return 'a double-quoted value is never closed';\n}\n\nfunction readSingleQuoted(text: string): Quoted | string {\n let out = '';\n for (let i = 1; i < text.length; i += 1) {\n const ch = text.charAt(i);\n if (ch !== \"'\") {\n out += ch;\n continue;\n }\n if (text.charAt(i + 1) === \"'\") {\n out += \"'\";\n i += 1;\n continue;\n }\n return { text: out, next: i + 1 };\n }\n return 'a single-quoted value is never closed';\n}\n\nfunction parseFlowSequence(text: string): YamlValue {\n const items: YamlScalar[] = [];\n let i = 1;\n for (;;) {\n while (i < text.length && /\\s/.test(text.charAt(i))) i += 1;\n if (i >= text.length) return unsupported('an inline list is never closed; keep it on one line');\n const ch = text.charAt(i);\n if (ch === ']') {\n return trailingIsComment(text.slice(i + 1))\n ? { kind: 'list', items }\n : unsupported('text follows the end of an inline list');\n }\n if (ch === '[' || ch === '{') return unsupported('nested lists and mappings are not supported');\n if (ch === ',') return unsupported('an inline list has an empty item');\n let item: YamlScalar;\n if (ch === '\"' || ch === \"'\") {\n const rest = text.slice(i);\n const quoted = ch === '\"' ? readDoubleQuoted(rest) : readSingleQuoted(rest);\n if (typeof quoted === 'string') return unsupported(quoted);\n item = { text: quoted.text, quoted: true };\n i += quoted.next;\n } else {\n let j = i;\n while (j < text.length && !',]'.includes(text.charAt(j))) j += 1;\n const plain = text.slice(i, j).trim();\n if (/[[{]/.test(plain) || /:(?:\\s|$)/.test(plain) || plain.includes(' #')) {\n return unsupported(`\"${plain}\" needs quoting inside an inline list`);\n }\n item = { text: plain, quoted: false };\n i = j;\n }\n items.push(item);\n while (i < text.length && /\\s/.test(text.charAt(i))) i += 1;\n if (i >= text.length) return unsupported('an inline list is never closed; keep it on one line');\n // After a comma the next pass reads an item or, legally in YAML, the bracket.\n if (text.charAt(i) === ',') i += 1;\n else if (text.charAt(i) !== ']') return unsupported('inline list items must be separated by commas');\n }\n}\n\n/** Null in the YAML 1.2 core schema: empty, `~` or `null` in any of its three spellings. */\nexport function isNull(scalar: YamlScalar): boolean {\n return !scalar.quoted && ['', '~', 'null', 'Null', 'NULL'].includes(scalar.text);\n}\n\n/** Renders a string as a scalar that reads back as the same string. */\nexport function renderScalar(value: string): string {\n const reserved = /^(?:true|false|null|yes|no|on|off|~)$/i.test(value);\n const plain = /^[A-Za-z][A-Za-z0-9 ._/+-]*$/.test(value) && !value.endsWith(' ');\n return plain && !reserved ? value : JSON.stringify(value);\n}\n\nexport function findEntry(frontMatter: FrontMatter | null, key: string): FrontMatterEntry | undefined {\n const name = keyName(key);\n return frontMatter?.entries.find((entry) => entry.name === name);\n}\n\n/**\n * Sets one key, leaving every other line as it was. An existing entry keeps the\n * spelling of its key; a new one goes last. A file with no front matter gains\n * a block.\n */\nexport function setEntry(\n lines: readonly string[],\n frontMatter: FrontMatter | null,\n key: string,\n rendered: string,\n): string[] {\n if (frontMatter === null) return ['---', `${key}: ${rendered}`, '---', ...lines];\n if (frontMatter.close < 0) throw new Error('cannot edit front matter that is never closed');\n const entry = findEntry(frontMatter, key);\n if (entry === undefined) {\n return [...lines.slice(0, frontMatter.close), `${key}: ${rendered}`, ...lines.slice(frontMatter.close)];\n }\n return [...lines.slice(0, entry.line), `${entry.key}: ${rendered}`, ...lines.slice(entry.end)];\n}\n\n/** Removes one key and its value lines; a key that is absent changes nothing. */\nexport function removeEntry(lines: readonly string[], frontMatter: FrontMatter | null, key: string): string[] {\n const entry = findEntry(frontMatter, key);\n if (entry === undefined) return [...lines];\n return [...lines.slice(0, entry.line), ...lines.slice(entry.end)];\n}\n"]}
package/dist/fs.d.ts ADDED
@@ -0,0 +1,59 @@
1
+ /**
2
+ * The filesystem, behind an interface.
3
+ *
4
+ * Every path crossing this boundary is repository-relative and POSIX; the
5
+ * Node implementation is the only place a host path exists. The in-memory
6
+ * implementation is not a test double bolted on afterwards: it is how a
7
+ * harness asks "what would archiving this do" against files that were never
8
+ * written, and how the transaction's rollback is exercised with a failure on
9
+ * exactly the operation that matters.
10
+ */
11
+ export interface FileSystem {
12
+ /** Contents, or `null` when there is no such file. */
13
+ read(path: string): Promise<string | null>;
14
+ /** Writes a file, creating its directory. Replaces an existing file atomically where the platform allows. */
15
+ write(path: string, content: string): Promise<void>;
16
+ /** Removes a file. Removing a file that is absent is an error. */
17
+ remove(path: string): Promise<void>;
18
+ /** Names of the files directly inside a directory, or `null` when it does not exist. */
19
+ list(directory: string): Promise<string[] | null>;
20
+ /** Every file beneath the root, as sorted repository-relative paths, skipping {@link IGNORED_DIRECTORIES}. */
21
+ walk(): Promise<string[]>;
22
+ exists(path: string): Promise<boolean>;
23
+ }
24
+ /**
25
+ * The path as the filesystem names it: links and junctions followed, and on
26
+ * Windows short names such as `RUNNER~1` expanded. Git reports the top of a
27
+ * work tree this way, so a root compared with it has to be named the same.
28
+ * A path that does not exist is returned as given, for the caller to report.
29
+ */
30
+ export declare function canonicalPath(path: string): Promise<string>;
31
+ /** Directories a walk never enters: build output and other tools' state. */
32
+ export declare const IGNORED_DIRECTORIES: ReadonlySet<string>;
33
+ export declare class NodeFileSystem implements FileSystem {
34
+ readonly root: string;
35
+ constructor(root: string);
36
+ private host;
37
+ read(path: string): Promise<string | null>;
38
+ write(path: string, content: string): Promise<void>;
39
+ remove(path: string): Promise<void>;
40
+ list(directory: string): Promise<string[] | null>;
41
+ walk(): Promise<string[]>;
42
+ exists(path: string): Promise<boolean>;
43
+ }
44
+ export type FileOperation = 'write' | 'remove';
45
+ /**
46
+ * Files in a `Map`. `fail` lets a test make one chosen operation throw, which
47
+ * is the only honest way to prove a rollback restores what it claims to.
48
+ */
49
+ export declare class MemoryFileSystem implements FileSystem {
50
+ readonly files: Map<string, string>;
51
+ fail: ((operation: FileOperation, path: string) => boolean) | undefined;
52
+ constructor(files?: Readonly<Record<string, string>>);
53
+ read(path: string): Promise<string | null>;
54
+ write(path: string, content: string): Promise<void>;
55
+ remove(path: string): Promise<void>;
56
+ list(directory: string): Promise<string[] | null>;
57
+ walk(): Promise<string[]>;
58
+ exists(path: string): Promise<boolean>;
59
+ }
package/dist/fs.js ADDED
@@ -0,0 +1,189 @@
1
+ /**
2
+ * The filesystem, behind an interface.
3
+ *
4
+ * Every path crossing this boundary is repository-relative and POSIX; the
5
+ * Node implementation is the only place a host path exists. The in-memory
6
+ * implementation is not a test double bolted on afterwards: it is how a
7
+ * harness asks "what would archiving this do" against files that were never
8
+ * written, and how the transaction's rollback is exercised with a failure on
9
+ * exactly the operation that matters.
10
+ */
11
+ import { randomBytes } from 'node:crypto';
12
+ import { mkdir, readdir, readFile, realpath, rename, rm, stat, writeFile } from 'node:fs/promises';
13
+ import { dirname, join, relative, resolve, sep } from 'node:path';
14
+ import { normalisePath as repoPath } from './links.js';
15
+ /**
16
+ * The path as the filesystem names it: links and junctions followed, and on
17
+ * Windows short names such as `RUNNER~1` expanded. Git reports the top of a
18
+ * work tree this way, so a root compared with it has to be named the same.
19
+ * A path that does not exist is returned as given, for the caller to report.
20
+ */
21
+ export async function canonicalPath(path) {
22
+ try {
23
+ return await realpath(path);
24
+ }
25
+ catch {
26
+ return path;
27
+ }
28
+ }
29
+ /** Directories a walk never enters: build output and other tools' state. */
30
+ export const IGNORED_DIRECTORIES = new Set([
31
+ '.git',
32
+ '.hg',
33
+ '.svn',
34
+ 'node_modules',
35
+ 'dist',
36
+ 'build',
37
+ 'coverage',
38
+ 'target',
39
+ '.venv',
40
+ '__pycache__',
41
+ '.stryker-tmp',
42
+ ]);
43
+ /** Errors a rename on Windows raises while another process briefly holds the file. */
44
+ const TRANSIENT = new Set(['EPERM', 'EACCES', 'EBUSY']);
45
+ const RENAME_ATTEMPTS = 6;
46
+ async function pause(ms) {
47
+ await new Promise((done) => setTimeout(done, ms));
48
+ }
49
+ export class NodeFileSystem {
50
+ root;
51
+ constructor(root) {
52
+ this.root = resolve(root);
53
+ }
54
+ host(path) {
55
+ const normal = repoPath(path);
56
+ return normal === '' ? this.root : join(this.root, ...normal.split('/'));
57
+ }
58
+ async read(path) {
59
+ try {
60
+ return await readFile(this.host(path), 'utf8');
61
+ }
62
+ catch (error) {
63
+ if (error.code === 'ENOENT')
64
+ return null;
65
+ throw error;
66
+ }
67
+ }
68
+ async write(path, content) {
69
+ const target = this.host(path);
70
+ await mkdir(dirname(target), { recursive: true });
71
+ // Written beside the target and renamed over it, so a reader never sees half a file.
72
+ const temporary = join(dirname(target), `.${randomBytes(6).toString('hex')}.spec-brief.tmp`);
73
+ await writeFile(temporary, content, { encoding: 'utf8', flag: 'wx' });
74
+ try {
75
+ for (let attempt = 1;; attempt += 1) {
76
+ try {
77
+ await rename(temporary, target);
78
+ return;
79
+ }
80
+ catch (error) {
81
+ const code = error.code ?? '';
82
+ if (!TRANSIENT.has(code) || attempt >= RENAME_ATTEMPTS)
83
+ throw error;
84
+ await pause(15 * attempt);
85
+ }
86
+ }
87
+ }
88
+ catch (error) {
89
+ await rm(temporary, { force: true });
90
+ throw error;
91
+ }
92
+ }
93
+ async remove(path) {
94
+ await rm(this.host(path));
95
+ }
96
+ async list(directory) {
97
+ try {
98
+ const entries = await readdir(this.host(directory), { withFileTypes: true });
99
+ return entries.filter((e) => e.isFile()).map((e) => e.name);
100
+ }
101
+ catch (error) {
102
+ const code = error.code;
103
+ if (code === 'ENOENT' || code === 'ENOTDIR')
104
+ return null;
105
+ /* v8 ignore next -- a permission error, which a portable test cannot provoke; it must not read as "no briefs". */
106
+ throw error;
107
+ }
108
+ }
109
+ async walk() {
110
+ const found = [];
111
+ const visit = async (directory) => {
112
+ const entries = await readdir(directory, { withFileTypes: true });
113
+ for (const entry of entries) {
114
+ const full = join(directory, entry.name);
115
+ if (entry.isDirectory()) {
116
+ if (!IGNORED_DIRECTORIES.has(entry.name))
117
+ await visit(full);
118
+ }
119
+ else if (entry.isFile()) {
120
+ found.push(relative(this.root, full).split(sep).join('/'));
121
+ }
122
+ }
123
+ };
124
+ await visit(this.root);
125
+ return found.sort();
126
+ }
127
+ async exists(path) {
128
+ try {
129
+ await stat(this.host(path));
130
+ return true;
131
+ }
132
+ catch {
133
+ return false;
134
+ }
135
+ }
136
+ }
137
+ /**
138
+ * Files in a `Map`. `fail` lets a test make one chosen operation throw, which
139
+ * is the only honest way to prove a rollback restores what it claims to.
140
+ */
141
+ export class MemoryFileSystem {
142
+ files;
143
+ fail;
144
+ constructor(files = {}) {
145
+ this.files = new Map(Object.entries(files).map(([path, content]) => [repoPath(path), content]));
146
+ this.fail = undefined;
147
+ }
148
+ read(path) {
149
+ return Promise.resolve(this.files.get(repoPath(path)) ?? null);
150
+ }
151
+ write(path, content) {
152
+ const normal = repoPath(path);
153
+ if (this.fail?.('write', normal) === true)
154
+ return Promise.reject(new Error(`injected failure writing ${normal}`));
155
+ this.files.set(normal, content);
156
+ return Promise.resolve();
157
+ }
158
+ remove(path) {
159
+ const normal = repoPath(path);
160
+ if (this.fail?.('remove', normal) === true)
161
+ return Promise.reject(new Error(`injected failure removing ${normal}`));
162
+ if (!this.files.delete(normal))
163
+ return Promise.reject(new Error(`${normal} does not exist`));
164
+ return Promise.resolve();
165
+ }
166
+ list(directory) {
167
+ const prefix = repoPath(directory);
168
+ const names = [];
169
+ let isDirectory = prefix === '';
170
+ for (const path of this.files.keys()) {
171
+ if (prefix !== '' && !path.startsWith(`${prefix}/`))
172
+ continue;
173
+ isDirectory = true;
174
+ const rest = prefix === '' ? path : path.slice(prefix.length + 1);
175
+ if (!rest.includes('/'))
176
+ names.push(rest);
177
+ }
178
+ return Promise.resolve(isDirectory ? names.sort() : null);
179
+ }
180
+ walk() {
181
+ const paths = [...this.files.keys()].filter((path) => !path.split('/').some((part) => IGNORED_DIRECTORIES.has(part)));
182
+ return Promise.resolve(paths.sort());
183
+ }
184
+ exists(path) {
185
+ const normal = repoPath(path);
186
+ return Promise.resolve(this.files.has(normal) || [...this.files.keys()].some((p) => normal === '' || p.startsWith(`${normal}/`)));
187
+ }
188
+ }
189
+ //# sourceMappingURL=fs.js.map
package/dist/fs.js.map ADDED
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fs.js","sourceRoot":"","sources":["../src/fs.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The filesystem, behind an interface.\n *\n * Every path crossing this boundary is repository-relative and POSIX; the\n * Node implementation is the only place a host path exists. The in-memory\n * implementation is not a test double bolted on afterwards: it is how a\n * harness asks \"what would archiving this do\" against files that were never\n * written, and how the transaction's rollback is exercised with a failure on\n * exactly the operation that matters.\n */\n\nimport { randomBytes } from 'node:crypto';\nimport { mkdir, readdir, readFile, realpath, rename, rm, stat, writeFile } from 'node:fs/promises';\nimport { dirname, join, relative, resolve, sep } from 'node:path';\n\nimport { normalisePath as repoPath } from './links.js';\n\nexport interface FileSystem {\n /** Contents, or `null` when there is no such file. */\n read(path: string): Promise<string | null>;\n /** Writes a file, creating its directory. Replaces an existing file atomically where the platform allows. */\n write(path: string, content: string): Promise<void>;\n /** Removes a file. Removing a file that is absent is an error. */\n remove(path: string): Promise<void>;\n /** Names of the files directly inside a directory, or `null` when it does not exist. */\n list(directory: string): Promise<string[] | null>;\n /** Every file beneath the root, as sorted repository-relative paths, skipping {@link IGNORED_DIRECTORIES}. */\n walk(): Promise<string[]>;\n exists(path: string): Promise<boolean>;\n}\n\n/**\n * The path as the filesystem names it: links and junctions followed, and on\n * Windows short names such as `RUNNER~1` expanded. Git reports the top of a\n * work tree this way, so a root compared with it has to be named the same.\n * A path that does not exist is returned as given, for the caller to report.\n */\nexport async function canonicalPath(path: string): Promise<string> {\n try {\n return await realpath(path);\n } catch {\n return path;\n }\n}\n\n/** Directories a walk never enters: build output and other tools' state. */\nexport const IGNORED_DIRECTORIES: ReadonlySet<string> = new Set([\n '.git',\n '.hg',\n '.svn',\n 'node_modules',\n 'dist',\n 'build',\n 'coverage',\n 'target',\n '.venv',\n '__pycache__',\n '.stryker-tmp',\n]);\n\n/** Errors a rename on Windows raises while another process briefly holds the file. */\nconst TRANSIENT = new Set(['EPERM', 'EACCES', 'EBUSY']);\nconst RENAME_ATTEMPTS = 6;\n\nasync function pause(ms: number): Promise<void> {\n await new Promise((done) => setTimeout(done, ms));\n}\n\nexport class NodeFileSystem implements FileSystem {\n readonly root: string;\n\n constructor(root: string) {\n this.root = resolve(root);\n }\n\n private host(path: string): string {\n const normal = repoPath(path);\n return normal === '' ? this.root : join(this.root, ...normal.split('/'));\n }\n\n async read(path: string): Promise<string | null> {\n try {\n return await readFile(this.host(path), 'utf8');\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code === 'ENOENT') return null;\n throw error;\n }\n }\n\n async write(path: string, content: string): Promise<void> {\n const target = this.host(path);\n await mkdir(dirname(target), { recursive: true });\n // Written beside the target and renamed over it, so a reader never sees half a file.\n const temporary = join(dirname(target), `.${randomBytes(6).toString('hex')}.spec-brief.tmp`);\n await writeFile(temporary, content, { encoding: 'utf8', flag: 'wx' });\n try {\n for (let attempt = 1; ; attempt += 1) {\n try {\n await rename(temporary, target);\n return;\n } catch (error) {\n const code = (error as NodeJS.ErrnoException).code ?? '';\n if (!TRANSIENT.has(code) || attempt >= RENAME_ATTEMPTS) throw error;\n await pause(15 * attempt);\n }\n }\n } catch (error) {\n await rm(temporary, { force: true });\n throw error;\n }\n }\n\n async remove(path: string): Promise<void> {\n await rm(this.host(path));\n }\n\n async list(directory: string): Promise<string[] | null> {\n try {\n const entries = await readdir(this.host(directory), { withFileTypes: true });\n return entries.filter((e) => e.isFile()).map((e) => e.name);\n } catch (error) {\n const code = (error as NodeJS.ErrnoException).code;\n if (code === 'ENOENT' || code === 'ENOTDIR') return null;\n /* v8 ignore next -- a permission error, which a portable test cannot provoke; it must not read as \"no briefs\". */\n throw error;\n }\n }\n\n async walk(): Promise<string[]> {\n const found: string[] = [];\n const visit = async (directory: string): Promise<void> => {\n const entries = await readdir(directory, { withFileTypes: true });\n for (const entry of entries) {\n const full = join(directory, entry.name);\n if (entry.isDirectory()) {\n if (!IGNORED_DIRECTORIES.has(entry.name)) await visit(full);\n } else if (entry.isFile()) {\n found.push(relative(this.root, full).split(sep).join('/'));\n }\n }\n };\n await visit(this.root);\n return found.sort();\n }\n\n async exists(path: string): Promise<boolean> {\n try {\n await stat(this.host(path));\n return true;\n } catch {\n return false;\n }\n }\n}\n\nexport type FileOperation = 'write' | 'remove';\n\n/**\n * Files in a `Map`. `fail` lets a test make one chosen operation throw, which\n * is the only honest way to prove a rollback restores what it claims to.\n */\nexport class MemoryFileSystem implements FileSystem {\n readonly files: Map<string, string>;\n fail: ((operation: FileOperation, path: string) => boolean) | undefined;\n\n constructor(files: Readonly<Record<string, string>> = {}) {\n this.files = new Map(Object.entries(files).map(([path, content]) => [repoPath(path), content]));\n this.fail = undefined;\n }\n\n read(path: string): Promise<string | null> {\n return Promise.resolve(this.files.get(repoPath(path)) ?? null);\n }\n\n write(path: string, content: string): Promise<void> {\n const normal = repoPath(path);\n if (this.fail?.('write', normal) === true) return Promise.reject(new Error(`injected failure writing ${normal}`));\n this.files.set(normal, content);\n return Promise.resolve();\n }\n\n remove(path: string): Promise<void> {\n const normal = repoPath(path);\n if (this.fail?.('remove', normal) === true) return Promise.reject(new Error(`injected failure removing ${normal}`));\n if (!this.files.delete(normal)) return Promise.reject(new Error(`${normal} does not exist`));\n return Promise.resolve();\n }\n\n list(directory: string): Promise<string[] | null> {\n const prefix = repoPath(directory);\n const names: string[] = [];\n let isDirectory = prefix === '';\n for (const path of this.files.keys()) {\n if (prefix !== '' && !path.startsWith(`${prefix}/`)) continue;\n isDirectory = true;\n const rest = prefix === '' ? path : path.slice(prefix.length + 1);\n if (!rest.includes('/')) names.push(rest);\n }\n return Promise.resolve(isDirectory ? names.sort() : null);\n }\n\n walk(): Promise<string[]> {\n const paths = [...this.files.keys()].filter((path) => !path.split('/').some((part) => IGNORED_DIRECTORIES.has(part)));\n return Promise.resolve(paths.sort());\n }\n\n exists(path: string): Promise<boolean> {\n const normal = repoPath(path);\n return Promise.resolve(\n this.files.has(normal) || [...this.files.keys()].some((p) => normal === '' || p.startsWith(`${normal}/`)),\n );\n }\n}\n"]}