@williamthorsen/toolbelt.filesystem 0.8.3 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.readyup/kits/default.js +514 -0
- package/.readyup/manifest.json +100 -0
- package/CHANGELOG.md +78 -14
- package/README.md +96 -147
- package/dist/esm/1-proposed/index.d.ts +0 -1
- package/dist/esm/1-proposed/index.js +0 -1
- package/dist/esm/3-candidate/index.d.ts +1 -1
- package/dist/esm/3-candidate/index.js +1 -1
- package/dist/esm/{1-proposed → 3-candidate}/writeAtomic.d.ts +3 -3
- package/dist/esm/4-release/listDirectoryChain.d.ts +1 -1
- package/dist/esm/4-release/loadConfigCascade.d.ts +1 -1
- package/dist/esm/4-release/reconcileFile.d.ts +1 -1
- package/dist/esm/4-release/reconcileFileFromFile.d.ts +1 -1
- package/package.json +10 -2
- package/dist/esm/3-candidate/createTempTree.d.ts +0 -85
- package/dist/esm/3-candidate/createTempTree.js +0 -148
- /package/dist/esm/{1-proposed → 3-candidate}/writeAtomic.js +0 -0
|
@@ -0,0 +1,514 @@
|
|
|
1
|
+
/** @noformat -- @generated. Do not edit. Compiled by rdy. */
|
|
2
|
+
/* eslint-disable */
|
|
3
|
+
export const __readyupVersion = "0.36.0";
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
// ../adoption/src/conventions/path-predicates.ts
|
|
7
|
+
var BIN_DIRECTORY = /(?:^|\/)bin\//;
|
|
8
|
+
var JS_TS_EXTENSION = /\.[cm]?[jt]sx?$/;
|
|
9
|
+
var TEST_DIRECTORY = /(?:^|\/)__tests__\//;
|
|
10
|
+
var TEST_SUFFIX = /\.(?:spec|test)\.[cm]?[jt]sx?$/;
|
|
11
|
+
function isAdoptableSource(path) {
|
|
12
|
+
return isJsTsSource(path) && !isBinWrapper(path) && !isTestFile(path) && !isInTestDirectory(path);
|
|
13
|
+
}
|
|
14
|
+
function isBinWrapper(path) {
|
|
15
|
+
return BIN_DIRECTORY.test(path);
|
|
16
|
+
}
|
|
17
|
+
function isInTestDirectory(path) {
|
|
18
|
+
return TEST_DIRECTORY.test(path);
|
|
19
|
+
}
|
|
20
|
+
function isJsTsSource(path) {
|
|
21
|
+
return JS_TS_EXTENSION.test(path);
|
|
22
|
+
}
|
|
23
|
+
function isTestFile(path) {
|
|
24
|
+
return TEST_SUFFIX.test(path);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// ../adoption/src/conventions/site-handoffs.ts
|
|
28
|
+
function isManifestSearch(names) {
|
|
29
|
+
return names.includes("package.json");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// ../adoption/src/kits/defineAdoptionKit.ts
|
|
33
|
+
import { defineRdyKit } from "readyup";
|
|
34
|
+
import {
|
|
35
|
+
buildFindingReport,
|
|
36
|
+
countPackageUsage,
|
|
37
|
+
readTrackedSources
|
|
38
|
+
} from "readyup/check-utils";
|
|
39
|
+
var NOT_A_REPO = "the project is not a git working tree, and these checks read the files that git tracks";
|
|
40
|
+
var NOTHING_TO_REPORT = { findings: [] };
|
|
41
|
+
function defineAdoptionKit(spec) {
|
|
42
|
+
assertCheckIdsAreUnique();
|
|
43
|
+
const cache = {};
|
|
44
|
+
const adoptedPackage = { exportNames: spec.exportNames, packageName: spec.packageName };
|
|
45
|
+
const kitScope = { noSourcesReason: spec.noSourcesReason, pathFilter: spec.pathFilter };
|
|
46
|
+
const pathFiltersByKind = mapPathFiltersByKind();
|
|
47
|
+
const sweptPathFilters = [
|
|
48
|
+
.../* @__PURE__ */ new Set([spec.pathFilter, ...spec.checks.map((check) => resolveScope(check).pathFilter)])
|
|
49
|
+
];
|
|
50
|
+
return defineRdyKit({
|
|
51
|
+
description: spec.description,
|
|
52
|
+
defaultSeverity: "warn",
|
|
53
|
+
checklists: [
|
|
54
|
+
{
|
|
55
|
+
name: "adoption",
|
|
56
|
+
checks: spec.checks.map((check) => ({
|
|
57
|
+
name: check.name,
|
|
58
|
+
id: check.id,
|
|
59
|
+
...check.severity !== void 0 && { severity: check.severity },
|
|
60
|
+
skip: () => skipUnlessProjectHoldsSources(resolveScope(check)),
|
|
61
|
+
check: () => reportKinds(check.kinds),
|
|
62
|
+
fix: check.fix
|
|
63
|
+
}))
|
|
64
|
+
}
|
|
65
|
+
]
|
|
66
|
+
});
|
|
67
|
+
function assertCheckIdsAreUnique() {
|
|
68
|
+
const seen = /* @__PURE__ */ new Set();
|
|
69
|
+
const duplicated = /* @__PURE__ */ new Set();
|
|
70
|
+
for (const { id } of spec.checks) {
|
|
71
|
+
if (seen.has(id)) duplicated.add(id);
|
|
72
|
+
seen.add(id);
|
|
73
|
+
}
|
|
74
|
+
if (duplicated.size > 0) {
|
|
75
|
+
const ids = [...duplicated].toSorted().join(", ");
|
|
76
|
+
throw new Error(`${spec.packageName}'s kit gives one id to more than one check: ${ids}`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
function isSweptPath(path) {
|
|
80
|
+
return sweptPathFilters.some((pathFilter) => pathFilter(path));
|
|
81
|
+
}
|
|
82
|
+
function loadSummary() {
|
|
83
|
+
cache.summary ??= readProject();
|
|
84
|
+
return cache.summary;
|
|
85
|
+
}
|
|
86
|
+
function mapPathFiltersByKind() {
|
|
87
|
+
const pathFilters = /* @__PURE__ */ new Map();
|
|
88
|
+
const conflicted = /* @__PURE__ */ new Set();
|
|
89
|
+
for (const check of spec.checks) {
|
|
90
|
+
const { pathFilter } = resolveScope(check);
|
|
91
|
+
for (const kind of check.kinds) {
|
|
92
|
+
const assigned = pathFilters.get(kind);
|
|
93
|
+
if (assigned !== void 0 && assigned !== pathFilter) conflicted.add(kind);
|
|
94
|
+
pathFilters.set(kind, pathFilter);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
if (conflicted.size > 0) {
|
|
98
|
+
const kinds = [...conflicted].toSorted().join(", ");
|
|
99
|
+
throw new Error(`${spec.packageName}'s kit reads one kind through more than one path filter: ${kinds}`);
|
|
100
|
+
}
|
|
101
|
+
return pathFilters;
|
|
102
|
+
}
|
|
103
|
+
async function readProject() {
|
|
104
|
+
const sources = await readTrackedSources(isSweptPath);
|
|
105
|
+
if (sources === void 0) return void 0;
|
|
106
|
+
return {
|
|
107
|
+
adoptedCount: countPackageUsage(sources, adoptedPackage),
|
|
108
|
+
findings: sources.flatMap(
|
|
109
|
+
(source) => spec.detect(source.text).filter((site) => (pathFiltersByKind.get(site.kind) ?? spec.pathFilter)(source.path)).map((site) => ({ ...site, path: source.path }))
|
|
110
|
+
),
|
|
111
|
+
sources
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
async function reportKinds(kinds) {
|
|
115
|
+
const summary = await loadSummary();
|
|
116
|
+
if (summary === void 0) return NOTHING_TO_REPORT;
|
|
117
|
+
return buildFindingReport({
|
|
118
|
+
adoptedCount: summary.adoptedCount,
|
|
119
|
+
findings: summary.findings,
|
|
120
|
+
ownImplementation: { ...adoptedPackage, sources: summary.sources },
|
|
121
|
+
shouldReport: (finding) => kinds.includes(finding.kind)
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
function resolveScope(check) {
|
|
125
|
+
return check.pathFilter === void 0 ? kitScope : { noSourcesReason: check.noSourcesReason, pathFilter: check.pathFilter };
|
|
126
|
+
}
|
|
127
|
+
async function skipUnlessProjectHoldsSources(scope) {
|
|
128
|
+
const summary = await loadSummary();
|
|
129
|
+
if (summary === void 0) return NOT_A_REPO;
|
|
130
|
+
return summary.sources.some((source) => scope.pathFilter(source.path)) ? false : scope.noSourcesReason;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// ../adoption/src/portable/condenseWhitespace.ts
|
|
135
|
+
function condenseWhitespace(text) {
|
|
136
|
+
return text.replaceAll(/\s+/g, " ");
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// ../adoption/src/portable/listDirectoryAscents.ts
|
|
140
|
+
import { getLineAtOffset } from "readyup/check-utils";
|
|
141
|
+
|
|
142
|
+
// ../adoption/src/portable/readAnchoredWindow.ts
|
|
143
|
+
function readAnchoredWindow(source, offset, lengths) {
|
|
144
|
+
return {
|
|
145
|
+
after: condenseWhitespace(source.slice(offset, offset + lengths.lookahead)),
|
|
146
|
+
before: condenseWhitespace(source.slice(Math.max(0, offset - lengths.lookbehind), offset))
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// ../adoption/src/portable/readBalancedGroup.ts
|
|
151
|
+
var BRACES = { close: "}", open: "{" };
|
|
152
|
+
var PARENTHESES = { close: ")", open: "(" };
|
|
153
|
+
function readBalancedGroup(source, from, delimiters) {
|
|
154
|
+
const start = source.indexOf(delimiters.open, from);
|
|
155
|
+
if (start === -1) return void 0;
|
|
156
|
+
let depth = 0;
|
|
157
|
+
for (let index = start; index < source.length; index += 1) {
|
|
158
|
+
if (source[index] === delimiters.open) depth += 1;
|
|
159
|
+
else if (source[index] === delimiters.close) {
|
|
160
|
+
depth -= 1;
|
|
161
|
+
if (depth === 0) return { end: index + 1, start };
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return void 0;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// ../adoption/src/portable/readLiteral.ts
|
|
168
|
+
function readLiteral(source, span) {
|
|
169
|
+
const start = span?.[0];
|
|
170
|
+
const end = span?.[1];
|
|
171
|
+
return start === void 0 || end === void 0 ? void 0 : source.slice(start + 1, end - 1);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// ../adoption/src/portable/listDirectoryAscents.ts
|
|
175
|
+
var ASSIGNED_TARGET = /(?<target>[A-Za-z_$][\w$]*) ?= ?$/;
|
|
176
|
+
var ASSIGNMENT_WINDOW = { lookahead: 0, lookbehind: 80 };
|
|
177
|
+
var BRACKET_CLOSERS = /* @__PURE__ */ new Set([")", "]", "}"]);
|
|
178
|
+
var BRACKET_OPENERS = /* @__PURE__ */ new Set(["(", "[", "{"]);
|
|
179
|
+
var DECLARATION = /\b(?:const|let|var)\s+(?<name>[A-Za-z_$][\w$]*)\s*(?::[^=;\n]*)?=(?![=>])/g;
|
|
180
|
+
var DECLARED_NAME = /\b(?:const|let|var)\s+(?<name>[A-Za-z_$][\w$]*)/g;
|
|
181
|
+
var DIRNAME_ASCENT = /(?:[A-Za-z_$][\w$]*\s*\.\s*)?\bdirname\s*\(\s*(?<subject>[A-Za-z_$][\w$]*)\s*\)/g;
|
|
182
|
+
var LEADING_SEPARATOR = /^[/\\]+/;
|
|
183
|
+
var LEVEL_PROBE = /\b(?:access|exists|lstat|readdir|readFile|stat)(?:Sync)?\s*\(/g;
|
|
184
|
+
var LOOP_KEYWORD = /\b(?<keyword>do|for|while)\b/g;
|
|
185
|
+
var PATH_TOKEN = /(?<quoted>(?<quote>['"])[^'"]*\k<quote>)|`|(?<![\w$.])(?<name>[A-Za-z_$][\w$]*)/g;
|
|
186
|
+
var REASSIGNMENT = /(?<![\w$.])(?<!\b(?:const|let|var)\s+)(?<name>[A-Za-z_$][\w$]*)\s*(?:\*\*|<<|>>>?|&&|\|\||\?\?|[-+*/%&|^])?=(?![=>])/g;
|
|
187
|
+
var SEPARATOR_RUN = /[/\\]+/g;
|
|
188
|
+
var SIMPLE_ASSIGNMENT = /(?<![\w$])(?<target>[A-Za-z_$][\w$]*)\s*=(?!=)\s*(?<value>[A-Za-z_$][\w$]*)(?![\w$.([])/g;
|
|
189
|
+
var STRING_CONSTANT = /\bconst\s+(?<name>[A-Za-z_$][\w$]*)\s*(?::\s*string\s*)?=\s*(?<literal>(?<quote>['"`])[^'"`$]*\k<quote>)\s*(?:as\s+const\s*)?(?=[;,)\n]|$)/dg;
|
|
190
|
+
var WHITESPACE = /\s/;
|
|
191
|
+
function listDirectoryAscents(code, source) {
|
|
192
|
+
const constants = listStringConstants(code, source);
|
|
193
|
+
const ascents = listLoops(code).flatMap((loop) => describeAscent(code, source, loop, constants) ?? []);
|
|
194
|
+
return ascents.filter((ascent) => ascents.every((other) => other === ascent || !isNested(other.loop, ascent.loop))).map((ascent) => ({ line: getLineAtOffset(code, ascent.loop.start), probedNames: ascent.probedNames }));
|
|
195
|
+
}
|
|
196
|
+
function countMatchesByName(text, pattern) {
|
|
197
|
+
const counts = /* @__PURE__ */ new Map();
|
|
198
|
+
for (const match of text.matchAll(pattern)) {
|
|
199
|
+
const name = match.groups?.["name"];
|
|
200
|
+
if (name !== void 0) counts.set(name, (counts.get(name) ?? 0) + 1);
|
|
201
|
+
}
|
|
202
|
+
return counts;
|
|
203
|
+
}
|
|
204
|
+
function describeAscent(code, source, loop, constants) {
|
|
205
|
+
const subject = findAscendedBinding(code.slice(loop.start, loop.end));
|
|
206
|
+
if (subject === void 0) return void 0;
|
|
207
|
+
const scope = { bindings: listLoopBindings(code, source, loop, subject, constants), constants };
|
|
208
|
+
return { loop, probedNames: listProbedNames(code, source, loop, subject, scope) };
|
|
209
|
+
}
|
|
210
|
+
function expandPathParts(parts, scope, subject) {
|
|
211
|
+
return parts.flatMap((part) => {
|
|
212
|
+
if (part.kind === "text" || part.name === subject) return [part];
|
|
213
|
+
const constant = scope.constants.find((candidate) => candidate.name === part.name);
|
|
214
|
+
if (constant !== void 0) return [{ kind: "text", text: constant.value }];
|
|
215
|
+
return scope.bindings.find((candidate) => candidate.name === part.name)?.parts ?? [part];
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
function findAscendedBinding(region) {
|
|
219
|
+
const assignments = listSimpleAssignments(region);
|
|
220
|
+
for (const match of region.matchAll(DIRNAME_ASCENT)) {
|
|
221
|
+
const subject = match.groups?.["subject"];
|
|
222
|
+
const target = readAssignedTarget(region, match.index);
|
|
223
|
+
if (subject === void 0 || target === void 0) continue;
|
|
224
|
+
if (target === subject) return subject;
|
|
225
|
+
const isCarriedBack = assignments.some(
|
|
226
|
+
(assignment) => assignment.target === subject && assignment.value === target
|
|
227
|
+
);
|
|
228
|
+
if (isCarriedBack) return subject;
|
|
229
|
+
}
|
|
230
|
+
return void 0;
|
|
231
|
+
}
|
|
232
|
+
function findBodyStart(code, afterKeyword, isDoLoop) {
|
|
233
|
+
let index = afterKeyword;
|
|
234
|
+
if (!isDoLoop) {
|
|
235
|
+
const head = readBalancedGroup(code, index, PARENTHESES);
|
|
236
|
+
if (head === void 0) return void 0;
|
|
237
|
+
index = head.end;
|
|
238
|
+
}
|
|
239
|
+
while (index < code.length && WHITESPACE.test(code.charAt(index))) index += 1;
|
|
240
|
+
return code.charAt(index) === "{" ? index : void 0;
|
|
241
|
+
}
|
|
242
|
+
function findExpressionEnd(code, from, limit) {
|
|
243
|
+
let depth = 0;
|
|
244
|
+
for (let index = from; index < limit; index += 1) {
|
|
245
|
+
const character = code.charAt(index);
|
|
246
|
+
if (BRACKET_OPENERS.has(character)) depth += 1;
|
|
247
|
+
else if (BRACKET_CLOSERS.has(character)) {
|
|
248
|
+
if (depth === 0) return index;
|
|
249
|
+
depth -= 1;
|
|
250
|
+
} else if ((character === "," || character === ";") && depth === 0) return index;
|
|
251
|
+
}
|
|
252
|
+
return limit;
|
|
253
|
+
}
|
|
254
|
+
function isNested(inner, outer) {
|
|
255
|
+
return outer.start <= inner.start && inner.end <= outer.end;
|
|
256
|
+
}
|
|
257
|
+
function listLoopBindings(code, source, loop, subject, constants) {
|
|
258
|
+
const region = code.slice(loop.start, loop.end);
|
|
259
|
+
const declarationCounts = countMatchesByName(region, DECLARED_NAME);
|
|
260
|
+
const reassignmentCounts = countMatchesByName(region, REASSIGNMENT);
|
|
261
|
+
const bindings = [];
|
|
262
|
+
for (const match of region.matchAll(DECLARATION)) {
|
|
263
|
+
const name = match.groups?.["name"];
|
|
264
|
+
if (name === void 0 || declarationCounts.get(name) !== 1 || reassignmentCounts.has(name)) continue;
|
|
265
|
+
const valueStart = loop.start + match.index + match[0].length;
|
|
266
|
+
const parts = readPathParts(code, source, valueStart, findExpressionEnd(code, valueStart, loop.end));
|
|
267
|
+
if (parts === void 0) continue;
|
|
268
|
+
bindings.push({ name, parts: expandPathParts(parts, { bindings, constants }, subject) });
|
|
269
|
+
}
|
|
270
|
+
return bindings;
|
|
271
|
+
}
|
|
272
|
+
function listLoops(code) {
|
|
273
|
+
const loops = [];
|
|
274
|
+
for (const match of code.matchAll(LOOP_KEYWORD)) {
|
|
275
|
+
const bodyStart = findBodyStart(code, match.index + match[0].length, match.groups?.["keyword"] === "do");
|
|
276
|
+
const body = bodyStart === void 0 ? void 0 : readBalancedGroup(code, bodyStart, BRACES);
|
|
277
|
+
if (body !== void 0) loops.push({ end: body.end, start: match.index });
|
|
278
|
+
}
|
|
279
|
+
return loops;
|
|
280
|
+
}
|
|
281
|
+
function listProbedNames(code, source, loop, subject, scope) {
|
|
282
|
+
const names = [];
|
|
283
|
+
let isProbing = false;
|
|
284
|
+
for (const match of code.slice(loop.start, loop.end).matchAll(LEVEL_PROBE)) {
|
|
285
|
+
const argumentList = readBalancedGroup(code, loop.start + match.index + match[0].length - 1, PARENTHESES);
|
|
286
|
+
if (argumentList === void 0) continue;
|
|
287
|
+
const pathStart = argumentList.start + 1;
|
|
288
|
+
const parts = readPathParts(code, source, pathStart, findExpressionEnd(code, pathStart, argumentList.end - 1));
|
|
289
|
+
const expanded = parts === void 0 ? [] : expandPathParts(parts, scope, subject);
|
|
290
|
+
const subjectIndex = expanded.findIndex((part) => part.kind === "read" && part.name === subject);
|
|
291
|
+
if (subjectIndex === -1) continue;
|
|
292
|
+
isProbing = true;
|
|
293
|
+
const name = readProbedName(expanded.slice(subjectIndex + 1));
|
|
294
|
+
if (name !== void 0) names.push(name);
|
|
295
|
+
}
|
|
296
|
+
return isProbing ? names : void 0;
|
|
297
|
+
}
|
|
298
|
+
function listSimpleAssignments(region) {
|
|
299
|
+
const assignments = [];
|
|
300
|
+
for (const match of region.matchAll(SIMPLE_ASSIGNMENT)) {
|
|
301
|
+
const target = match.groups?.["target"];
|
|
302
|
+
const value = match.groups?.["value"];
|
|
303
|
+
if (target !== void 0 && value !== void 0) assignments.push({ target, value });
|
|
304
|
+
}
|
|
305
|
+
return assignments;
|
|
306
|
+
}
|
|
307
|
+
function listStringConstants(code, source) {
|
|
308
|
+
const constants = [];
|
|
309
|
+
const declarationCounts = countMatchesByName(code, DECLARED_NAME);
|
|
310
|
+
for (const match of code.matchAll(STRING_CONSTANT)) {
|
|
311
|
+
const name = match.groups?.["name"];
|
|
312
|
+
const value = readLiteral(source, match.indices?.groups?.["literal"]);
|
|
313
|
+
if (name === void 0 || value === void 0) continue;
|
|
314
|
+
if (declarationCounts.get(name) === 1) constants.push({ name, value });
|
|
315
|
+
}
|
|
316
|
+
return constants;
|
|
317
|
+
}
|
|
318
|
+
function readAssignedTarget(region, offset) {
|
|
319
|
+
const { before } = readAnchoredWindow(region, offset, ASSIGNMENT_WINDOW);
|
|
320
|
+
return ASSIGNED_TARGET.exec(before)?.groups?.["target"];
|
|
321
|
+
}
|
|
322
|
+
function readPathParts(code, source, start, end) {
|
|
323
|
+
const parts = [];
|
|
324
|
+
let resumeAt = start;
|
|
325
|
+
for (const match of code.slice(start, end).matchAll(PATH_TOKEN)) {
|
|
326
|
+
const offset = start + match.index;
|
|
327
|
+
if (offset < resumeAt) continue;
|
|
328
|
+
const name = match.groups?.["name"];
|
|
329
|
+
const quoted = match.groups?.["quoted"];
|
|
330
|
+
if (name !== void 0) {
|
|
331
|
+
parts.push({ kind: "read", name });
|
|
332
|
+
} else if (quoted !== void 0) {
|
|
333
|
+
parts.push({ kind: "text", text: source.slice(offset + 1, offset + quoted.length - 1) });
|
|
334
|
+
} else {
|
|
335
|
+
const template = readTemplateParts(code, source, offset, end);
|
|
336
|
+
if (template === void 0) return void 0;
|
|
337
|
+
parts.push(...template.parts);
|
|
338
|
+
resumeAt = template.end;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
return parts;
|
|
342
|
+
}
|
|
343
|
+
function readProbedName(parts) {
|
|
344
|
+
const segments = [];
|
|
345
|
+
for (const part of parts) {
|
|
346
|
+
if (part.kind === "read") return void 0;
|
|
347
|
+
if (part.text !== "") segments.push(part.text);
|
|
348
|
+
}
|
|
349
|
+
const name = segments.join("/").replaceAll(SEPARATOR_RUN, "/").replace(LEADING_SEPARATOR, "");
|
|
350
|
+
return name === "" ? void 0 : name;
|
|
351
|
+
}
|
|
352
|
+
function readTemplateParts(code, source, open, end) {
|
|
353
|
+
const parts = [];
|
|
354
|
+
let textStart = open + 1;
|
|
355
|
+
for (; ; ) {
|
|
356
|
+
const close = code.indexOf("`", textStart);
|
|
357
|
+
if (close === -1 || close >= end) return void 0;
|
|
358
|
+
const interpolation = code.indexOf("${", textStart);
|
|
359
|
+
if (interpolation === -1 || interpolation > close) {
|
|
360
|
+
parts.push({ kind: "text", text: source.slice(textStart, close) });
|
|
361
|
+
return { end: close + 1, parts };
|
|
362
|
+
}
|
|
363
|
+
parts.push({ kind: "text", text: source.slice(textStart, interpolation) });
|
|
364
|
+
const group = readBalancedGroup(code, interpolation + 1, BRACES);
|
|
365
|
+
if (group === void 0 || group.end > end) return void 0;
|
|
366
|
+
const interpolated = readPathParts(code, source, group.start + 1, group.end - 1);
|
|
367
|
+
if (interpolated === void 0) return void 0;
|
|
368
|
+
parts.push(...interpolated);
|
|
369
|
+
textStart = group.end;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// ../adoption/src/portable/listFunctionBodies.ts
|
|
374
|
+
var FUNCTION_HEAD = /(?:function\s+(?<declared>\w+)\s*(?:<[^<>]*>\s*)?\(|(?:const|let|var)\s+(?<bound>\w+)[^=;]*=\s*(?:async\s+)?(?:function\s*)?(?:<[^<>]*>\s*)?\((?<arrowParameters>[^)]*)\)[^=;{]*=>)/g;
|
|
375
|
+
var PLAIN_PARAMETER = /^\s*(?<name>[A-Za-z_$][\w$]*)\s*(?=[,:=?]|$)/;
|
|
376
|
+
function listFunctionBodies(source) {
|
|
377
|
+
const bodies = [];
|
|
378
|
+
FUNCTION_HEAD.lastIndex = 0;
|
|
379
|
+
let head = FUNCTION_HEAD.exec(source);
|
|
380
|
+
while (head !== null) {
|
|
381
|
+
const name = head.groups?.["declared"] ?? head.groups?.["bound"];
|
|
382
|
+
const from = findBodySearchStart(source, head);
|
|
383
|
+
const body = from === void 0 ? void 0 : readBalancedGroup(source, from, BRACES);
|
|
384
|
+
if (name !== void 0 && from !== void 0 && body !== void 0 && !source.slice(from, body.start).includes(";")) {
|
|
385
|
+
const firstParameter = findFirstParameterName(readParameterText(source, head));
|
|
386
|
+
bodies.push({
|
|
387
|
+
bodyEnd: body.end,
|
|
388
|
+
bodyStart: body.start,
|
|
389
|
+
...firstParameter !== void 0 && { firstParameter },
|
|
390
|
+
headStart: head.index,
|
|
391
|
+
name
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
head = FUNCTION_HEAD.exec(source);
|
|
395
|
+
}
|
|
396
|
+
return bodies;
|
|
397
|
+
}
|
|
398
|
+
function findBodySearchStart(source, head) {
|
|
399
|
+
if (head.groups?.["declared"] === void 0) return head.index + head[0].length;
|
|
400
|
+
return readBalancedGroup(source, head.index, PARENTHESES)?.end;
|
|
401
|
+
}
|
|
402
|
+
function findFirstParameterName(parameterText) {
|
|
403
|
+
if (parameterText === void 0) return void 0;
|
|
404
|
+
return PLAIN_PARAMETER.exec(parameterText)?.groups?.["name"];
|
|
405
|
+
}
|
|
406
|
+
function readParameterText(source, head) {
|
|
407
|
+
const arrowParameters = head.groups?.["arrowParameters"];
|
|
408
|
+
if (arrowParameters !== void 0) return arrowParameters;
|
|
409
|
+
const group = readBalancedGroup(source, head.index, PARENTHESES);
|
|
410
|
+
return group === void 0 ? void 0 : source.slice(group.start + 1, group.end - 1);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// ../adoption/src/mod.ts
|
|
414
|
+
import { blankNonCode, getLineAtOffset as getLineAtOffset2 } from "readyup/check-utils";
|
|
415
|
+
|
|
416
|
+
// src/readiness/adoptedExports.ts
|
|
417
|
+
var ADOPTED_EXPORTS = [
|
|
418
|
+
"findDirectoryChainMatch",
|
|
419
|
+
"listDirectoryChain",
|
|
420
|
+
"listDirectoryChainMatches",
|
|
421
|
+
"loadConfigCascade",
|
|
422
|
+
"reconcileFile",
|
|
423
|
+
"reconcileFileFromFile",
|
|
424
|
+
"replaceFileExtension",
|
|
425
|
+
"writeAtomic"
|
|
426
|
+
];
|
|
427
|
+
|
|
428
|
+
// src/readiness/listAtomicWriteSites.ts
|
|
429
|
+
var BOUND_PATH_ARGUMENT = /^\s*(?<name>[A-Za-z_$][\w$]*)\s*(?:,|$)/;
|
|
430
|
+
var RENAME_CALL = /\brename(?:Sync)?\s*\(/g;
|
|
431
|
+
var WRITE_CALL = /\bwriteFile(?:Sync)?\s*\(/g;
|
|
432
|
+
function listAtomicWriteSites(code) {
|
|
433
|
+
const claims = /* @__PURE__ */ new Map();
|
|
434
|
+
for (const fn of listFunctionBodies(code)) {
|
|
435
|
+
const body = code.slice(fn.bodyStart, fn.bodyEnd);
|
|
436
|
+
const writes = listPathArguments(body, WRITE_CALL);
|
|
437
|
+
const bodyLength = fn.bodyEnd - fn.bodyStart;
|
|
438
|
+
for (const rename of listPathArguments(body, RENAME_CALL)) {
|
|
439
|
+
const isPaired = writes.some((write) => write.name === rename.name && write.offset < rename.offset);
|
|
440
|
+
if (!isPaired) continue;
|
|
441
|
+
const offset = fn.bodyStart + rename.offset;
|
|
442
|
+
const claimed = claims.get(offset);
|
|
443
|
+
if (claimed !== void 0 && claimed.bodyLength <= bodyLength) continue;
|
|
444
|
+
claims.set(offset, {
|
|
445
|
+
bodyLength,
|
|
446
|
+
offset,
|
|
447
|
+
site: { kind: "temp-write-rename", line: getLineAtOffset2(code, offset), symbol: fn.name }
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
return claims.values().toArray().toSorted((a, b) => a.offset - b.offset).map((claim) => claim.site);
|
|
452
|
+
}
|
|
453
|
+
function listPathArguments(body, pattern) {
|
|
454
|
+
const calls = [];
|
|
455
|
+
for (const match of body.matchAll(pattern)) {
|
|
456
|
+
const argumentList = readBalancedGroup(body, match.index + match[0].length - 1, PARENTHESES);
|
|
457
|
+
if (argumentList === void 0) continue;
|
|
458
|
+
const name = BOUND_PATH_ARGUMENT.exec(body.slice(argumentList.start + 1, argumentList.end - 1))?.groups?.["name"];
|
|
459
|
+
if (name !== void 0) calls.push({ name, offset: match.index });
|
|
460
|
+
}
|
|
461
|
+
return calls;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// src/readiness/listChainWalkSites.ts
|
|
465
|
+
function listChainWalkSites(code, source) {
|
|
466
|
+
return listDirectoryAscents(code, source).flatMap(({ line, probedNames }) => {
|
|
467
|
+
if (probedNames === void 0) return [{ kind: "chain-walk", line }];
|
|
468
|
+
return isManifestSearch(probedNames) ? [] : [{ kind: "chain-probe", line }];
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// src/readiness/listFilesystemIdioms.ts
|
|
473
|
+
function listFilesystemIdioms(source) {
|
|
474
|
+
const code = blankNonCode(source);
|
|
475
|
+
const sites = [
|
|
476
|
+
...listAtomicWriteSites(code),
|
|
477
|
+
...listChainWalkSites(code, source)
|
|
478
|
+
];
|
|
479
|
+
return sites.toSorted((a, b) => a.line - b.line);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// .readyup/kits/default.ts
|
|
483
|
+
var PACKAGE_NAME = "@williamthorsen/toolbelt.filesystem";
|
|
484
|
+
var PACKAGING_README_URL = "https://github.com/williamthorsen/toolbelt/tree/main/packages/packaging#readme";
|
|
485
|
+
var README_URL = "https://github.com/williamthorsen/toolbelt/tree/main/packages/filesystem#readme";
|
|
486
|
+
var default_default = defineAdoptionKit({
|
|
487
|
+
description: `Adoption checks for a project consuming ${PACKAGE_NAME}`,
|
|
488
|
+
detect: listFilesystemIdioms,
|
|
489
|
+
exportNames: ADOPTED_EXPORTS,
|
|
490
|
+
noSourcesReason: "the project holds no JavaScript or TypeScript sources outside the exempt paths",
|
|
491
|
+
packageName: PACKAGE_NAME,
|
|
492
|
+
// A test writes these shapes deliberately, and a bootstrap wrapper hand-rolls what it reaches for so that its
|
|
493
|
+
// build-first message survives an incomplete install.
|
|
494
|
+
pathFilter: isAdoptableSource,
|
|
495
|
+
checks: [
|
|
496
|
+
{
|
|
497
|
+
name: "No source writes a file atomically by hand",
|
|
498
|
+
id: "no-hand-rolled-atomic-write",
|
|
499
|
+
kinds: ["temp-write-rename"],
|
|
500
|
+
severity: "recommend",
|
|
501
|
+
fix: `Replace the write and rename named above with writeAtomic from ${PACKAGE_NAME}/candidate, called as await writeAtomic(filePath, content). Check where the temp file is staged before taking the substitution as cosmetic: rename is atomic only within one filesystem, so a temp file under the system temporary directory fails with EXDEV the moment the target lives on another volume. writeAtomic stages beside the target, creates missing parent directories, copies an existing target's permission bits onto the replacement, and removes the temp file on failure. It fsyncs nothing, so it promises no torn reads rather than survival of a power loss. Reference: ${README_URL}`
|
|
502
|
+
},
|
|
503
|
+
{
|
|
504
|
+
name: "No source walks to the filesystem root by hand",
|
|
505
|
+
id: "no-hand-rolled-directory-walk",
|
|
506
|
+
kinds: ["chain-probe", "chain-walk"],
|
|
507
|
+
severity: "recommend",
|
|
508
|
+
fix: `Replace the loop named above with the directory-chain function that matches what it does, all three from ${PACKAGE_NAME}. A loop that only ascends takes listDirectoryChain, which returns the levels as strings and reads nothing from disk. A loop that probes each level for a name takes findDirectoryChainMatch where it stops at the nearest match, and listDirectoryChainMatches where every level's match matters; the first touches no level beyond the one that matches. All three take a stopAtDir that bounds the ascent, which a hand-rolled loop usually runs without. A loop probing for package.json is left to toolbelt.packaging, whose own kit reports it: ${PACKAGING_README_URL}. Reference: ${README_URL}`
|
|
509
|
+
}
|
|
510
|
+
]
|
|
511
|
+
});
|
|
512
|
+
export {
|
|
513
|
+
default_default as default
|
|
514
|
+
};
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 1,
|
|
3
|
+
"kits": [
|
|
4
|
+
{
|
|
5
|
+
"checklists": [
|
|
6
|
+
"adoption"
|
|
7
|
+
],
|
|
8
|
+
"description": "Adoption checks for a project consuming @williamthorsen/toolbelt.filesystem",
|
|
9
|
+
"esbuildVersion": "0.28.2",
|
|
10
|
+
"inputs": [
|
|
11
|
+
{
|
|
12
|
+
"hash": "1dc83619",
|
|
13
|
+
"kind": "module",
|
|
14
|
+
"path": "../../adoption/src/conventions/path-predicates.ts"
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"hash": "0237a871",
|
|
18
|
+
"kind": "module",
|
|
19
|
+
"path": "../../adoption/src/conventions/site-handoffs.ts"
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
"hash": "8fb83c2d",
|
|
23
|
+
"kind": "module",
|
|
24
|
+
"path": "../../adoption/src/kits/defineAdoptionKit.ts"
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"hash": "10596562",
|
|
28
|
+
"kind": "module",
|
|
29
|
+
"path": "../../adoption/src/mod.ts"
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
"hash": "6fdaacac",
|
|
33
|
+
"kind": "module",
|
|
34
|
+
"path": "../../adoption/src/portable/condenseWhitespace.ts"
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
"hash": "0661f0dd",
|
|
38
|
+
"kind": "module",
|
|
39
|
+
"path": "../../adoption/src/portable/listDirectoryAscents.ts"
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
"hash": "07c8358f",
|
|
43
|
+
"kind": "module",
|
|
44
|
+
"path": "../../adoption/src/portable/listFunctionBodies.ts"
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
"hash": "2d9f1edb",
|
|
48
|
+
"kind": "module",
|
|
49
|
+
"path": "../../adoption/src/portable/listTemplateLiterals.ts"
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
"hash": "b0751395",
|
|
53
|
+
"kind": "module",
|
|
54
|
+
"path": "../../adoption/src/portable/readAnchoredWindow.ts"
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
"hash": "0eb1daf1",
|
|
58
|
+
"kind": "module",
|
|
59
|
+
"path": "../../adoption/src/portable/readBalancedGroup.ts"
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
"hash": "e9427d50",
|
|
63
|
+
"kind": "module",
|
|
64
|
+
"path": "../../adoption/src/portable/readLiteral.ts"
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
"hash": "3c3de241",
|
|
68
|
+
"kind": "module",
|
|
69
|
+
"path": "kits/default.ts"
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
"hash": "065da837",
|
|
73
|
+
"kind": "module",
|
|
74
|
+
"path": "../src/readiness/adoptedExports.ts"
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
"hash": "454f687a",
|
|
78
|
+
"kind": "module",
|
|
79
|
+
"path": "../src/readiness/listAtomicWriteSites.ts"
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
"hash": "8c76b74e",
|
|
83
|
+
"kind": "module",
|
|
84
|
+
"path": "../src/readiness/listChainWalkSites.ts"
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
"hash": "612ff5f3",
|
|
88
|
+
"kind": "module",
|
|
89
|
+
"path": "../src/readiness/listFilesystemIdioms.ts"
|
|
90
|
+
}
|
|
91
|
+
],
|
|
92
|
+
"name": "default",
|
|
93
|
+
"path": "kits/default.js",
|
|
94
|
+
"readyupVersion": "0.36.0",
|
|
95
|
+
"source": "kits/default.ts",
|
|
96
|
+
"sourceHash": "3c3de241",
|
|
97
|
+
"targetHash": "3f2a9a3c"
|
|
98
|
+
}
|
|
99
|
+
]
|
|
100
|
+
}
|