@dirwell/core 0.0.1-alpha.1 → 0.0.2
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/README.md +27 -2
- package/dist/client.d.mts +1 -1
- package/dist/client.mjs +1 -0
- package/dist/client.mjs.map +1 -1
- package/dist/index.d.mts +33 -13
- package/dist/index.mjs +503 -99
- package/dist/index.mjs.map +1 -1
- package/dist/{listing-CnkSglVT.d.mts → listing-CG4b6tOt.d.mts} +42 -3
- package/package.json +7 -2
package/dist/index.mjs
CHANGED
|
@@ -1,9 +1,313 @@
|
|
|
1
1
|
import { t as copyJsonObject } from "./json-Bvj6xQLq.mjs";
|
|
2
|
-
import { cp, lstat, mkdir, mkdtemp, open, readFile, readdir, readlink, realpath, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
2
|
+
import { cp, lstat, mkdir, mkdtemp, open, opendir, readFile, readdir, readlink, realpath, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
|
-
import { createHash } from "node:crypto";
|
|
5
4
|
import picomatch from "picomatch";
|
|
5
|
+
import ignore from "ignore";
|
|
6
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
6
7
|
import { constants } from "node:fs";
|
|
8
|
+
//#region src/filters.ts
|
|
9
|
+
/** Resolve configuration paths once, before a host changes working directories. */
|
|
10
|
+
function resolveIgnoreFiles(value = [], cwd = process.cwd()) {
|
|
11
|
+
if (!Array.isArray(value)) throw new TypeError("ignoreFiles must be an array");
|
|
12
|
+
return value.map((input) => {
|
|
13
|
+
const file = typeof input === "string" ? { path: input } : input;
|
|
14
|
+
if (typeof file !== "object" || file === null || Array.isArray(file)) throw new TypeError("ignoreFiles entries must be paths or objects");
|
|
15
|
+
for (const key of Object.keys(file)) if (![
|
|
16
|
+
"path",
|
|
17
|
+
"baseDir",
|
|
18
|
+
"optional"
|
|
19
|
+
].includes(key)) throw new TypeError(`Unsupported ignoreFiles key "${key}"`);
|
|
20
|
+
for (const key of ["path", "baseDir"]) if ((key === "path" || file[key] !== void 0) && (typeof file[key] !== "string" || file[key].length === 0)) throw new TypeError(`ignoreFiles.${key} must be a non-empty string`);
|
|
21
|
+
if (file.optional !== void 0 && typeof file.optional !== "boolean") throw new TypeError("ignoreFiles.optional must be a boolean");
|
|
22
|
+
const filename = path.resolve(cwd, file.path);
|
|
23
|
+
return {
|
|
24
|
+
path: filename,
|
|
25
|
+
baseDir: path.resolve(cwd, file.baseDir ?? path.dirname(filename)),
|
|
26
|
+
optional: file.optional ?? false
|
|
27
|
+
};
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
async function createIgnoreMatcher(files) {
|
|
31
|
+
const rules = [];
|
|
32
|
+
const scopes = /* @__PURE__ */ new Map();
|
|
33
|
+
let hasRules = false;
|
|
34
|
+
for (const file of resolveIgnoreFiles(files)) {
|
|
35
|
+
let contents;
|
|
36
|
+
try {
|
|
37
|
+
contents = await readFile(file.path, "utf8");
|
|
38
|
+
} catch (error) {
|
|
39
|
+
if (file.optional && error instanceof Error && "code" in error && error.code === "ENOENT") continue;
|
|
40
|
+
throw error;
|
|
41
|
+
}
|
|
42
|
+
const base = await realpath(file.baseDir);
|
|
43
|
+
if (contents.split(/\r?\n/).some((line) => line.trim() !== "" && !line.startsWith("#"))) hasRules = true;
|
|
44
|
+
let matcher = scopes.get(base);
|
|
45
|
+
if (matcher === void 0) {
|
|
46
|
+
matcher = ignore();
|
|
47
|
+
scopes.set(base, matcher);
|
|
48
|
+
rules.push({
|
|
49
|
+
base,
|
|
50
|
+
matcher
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
matcher.add(contents);
|
|
54
|
+
}
|
|
55
|
+
rules.sort((left, right) => left.base.split(path.sep).length - right.base.split(path.sep).length);
|
|
56
|
+
const matches = (filename, directory) => {
|
|
57
|
+
let ignored = false;
|
|
58
|
+
for (const rule of rules) {
|
|
59
|
+
if (!isWithin(rule.base, filename)) continue;
|
|
60
|
+
const relative = path.relative(rule.base, filename).split(path.sep).join("/");
|
|
61
|
+
if (relative === "") continue;
|
|
62
|
+
const result = rule.matcher.test(relative + (directory ? "/" : ""));
|
|
63
|
+
if (result.ignored) ignored = true;
|
|
64
|
+
else if (result.unignored) ignored = false;
|
|
65
|
+
}
|
|
66
|
+
return ignored;
|
|
67
|
+
};
|
|
68
|
+
return Object.assign(matches, { hasRules });
|
|
69
|
+
}
|
|
70
|
+
function normalizePathPatterns(value, name) {
|
|
71
|
+
if (value === void 0) return [];
|
|
72
|
+
const patterns = typeof value === "string" ? [value] : value;
|
|
73
|
+
if (!Array.isArray(patterns) || patterns.some((pattern) => typeof pattern !== "string" || pattern.length === 0 || pattern.startsWith("/") || /^[A-Za-z]:/.test(pattern) || pattern.startsWith("!") || pattern.includes("\\") || pattern.split("/").some((segment) => segment === "." || segment === ".."))) throw new TypeError(`${name} must contain root-relative glob patterns using forward slashes`);
|
|
74
|
+
return patterns;
|
|
75
|
+
}
|
|
76
|
+
function isWithin(parent, candidate) {
|
|
77
|
+
const relative = path.relative(parent, candidate);
|
|
78
|
+
return relative === "" || relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative);
|
|
79
|
+
}
|
|
80
|
+
var SelectionBudgetError = class extends Error {
|
|
81
|
+
name = "SelectionBudgetError";
|
|
82
|
+
};
|
|
83
|
+
/** Select physical source paths; parent directories remain when a descendant matches. */
|
|
84
|
+
async function selectSourcePaths(sourceDir, outputDir, include, exclude, ignoredPaths = [], ignoreFiles = [], preparedIgnore, budget) {
|
|
85
|
+
const isIgnored = preparedIgnore ?? await createIgnoreMatcher(ignoreFiles);
|
|
86
|
+
const canonicalRoot = await realpath(sourceDir);
|
|
87
|
+
const ignored = ignoredPaths.map((value) => path.resolve(value));
|
|
88
|
+
const included = normalizePathPatterns(include, "include").map((pattern) => picomatch(pattern, {
|
|
89
|
+
dot: true,
|
|
90
|
+
nonegate: true
|
|
91
|
+
}));
|
|
92
|
+
const excluded = normalizePathPatterns(exclude, "exclude").map((pattern) => picomatch(pattern, {
|
|
93
|
+
dot: true,
|
|
94
|
+
nonegate: true
|
|
95
|
+
}));
|
|
96
|
+
const selected = /* @__PURE__ */ new Set([""]);
|
|
97
|
+
const symlinks = /* @__PURE__ */ new Map();
|
|
98
|
+
let entries = 0;
|
|
99
|
+
let directories = 0;
|
|
100
|
+
async function visit(directory, relativeDir, inheritedInclude) {
|
|
101
|
+
budget?.signal.throwIfAborted();
|
|
102
|
+
if (budget) {
|
|
103
|
+
const physical = await realpath(directory);
|
|
104
|
+
if (!isWithin(canonicalRoot, physical)) return false;
|
|
105
|
+
if (path.relative(canonicalRoot, physical).split(path.sep).join("/") !== relativeDir) return false;
|
|
106
|
+
}
|
|
107
|
+
if (budget && !await budget.allowPath(relativeDir)) return false;
|
|
108
|
+
if (budget && ++directories > budget.maxDirectories) throw new SelectionBudgetError("Directory scan budget exceeded");
|
|
109
|
+
let hasSelected = false;
|
|
110
|
+
for await (const entry of await opendir(directory)) {
|
|
111
|
+
budget?.signal.throwIfAborted();
|
|
112
|
+
if (budget && ++entries > budget.maxEntries) throw new SelectionBudgetError("Entry scan budget exceeded");
|
|
113
|
+
const absolutePath = path.join(directory, entry.name);
|
|
114
|
+
if (isWithin(outputDir, absolutePath) || ignored.some((value) => isWithin(value, absolutePath))) continue;
|
|
115
|
+
const relativePath = path.posix.join(relativeDir, entry.name);
|
|
116
|
+
if (budget && !await budget.allowPath(relativePath)) continue;
|
|
117
|
+
const directoryLike = entry.isDirectory();
|
|
118
|
+
const matches = (matcher) => matcher(relativePath) || directoryLike && matcher(`${relativePath}/`);
|
|
119
|
+
if (excluded.some(matches) || isIgnored(path.join(canonicalRoot, relativePath), directoryLike)) continue;
|
|
120
|
+
const includedHere = inheritedInclude || included.some(matches);
|
|
121
|
+
if (directoryLike) {
|
|
122
|
+
const hasSelectedChild = await visit(absolutePath, relativePath, includedHere);
|
|
123
|
+
if (included.length === 0 || includedHere || hasSelectedChild) {
|
|
124
|
+
selected.add(relativePath);
|
|
125
|
+
hasSelected = true;
|
|
126
|
+
}
|
|
127
|
+
} else if (included.length === 0 || includedHere) {
|
|
128
|
+
selected.add(relativePath);
|
|
129
|
+
hasSelected = true;
|
|
130
|
+
if (entry.isSymbolicLink()) symlinks.set(relativePath, await readlink(absolutePath));
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return hasSelected;
|
|
134
|
+
}
|
|
135
|
+
if (!isIgnored(canonicalRoot, true)) await visit(sourceDir, "", false);
|
|
136
|
+
const mirroredSymlinks = /* @__PURE__ */ new Set();
|
|
137
|
+
for (const [relativePath, target] of symlinks) {
|
|
138
|
+
if (path.isAbsolute(target)) continue;
|
|
139
|
+
const targetPath = path.resolve(sourceDir, path.dirname(relativePath), target);
|
|
140
|
+
if (!isWithin(sourceDir, targetPath)) continue;
|
|
141
|
+
const targetRelativePath = path.relative(sourceDir, targetPath).split(path.sep).join("/");
|
|
142
|
+
if (selected.has(targetRelativePath)) mirroredSymlinks.add(relativePath);
|
|
143
|
+
}
|
|
144
|
+
return {
|
|
145
|
+
selected,
|
|
146
|
+
symlinks: new Set(symlinks.keys()),
|
|
147
|
+
mirroredSymlinks
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
//#endregion
|
|
151
|
+
//#region src/validation.ts
|
|
152
|
+
function optionObject(value, name) {
|
|
153
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) throw new TypeError(`${name} must be an object`);
|
|
154
|
+
return value;
|
|
155
|
+
}
|
|
156
|
+
function distance(left, right) {
|
|
157
|
+
let previous = Array.from({ length: right.length + 1 }, (_, index) => index);
|
|
158
|
+
for (let i = 1; i <= left.length; i++) {
|
|
159
|
+
const current = [i];
|
|
160
|
+
for (let j = 1; j <= right.length; j++) current[j] = Math.min(current[j - 1] + 1, previous[j] + 1, previous[j - 1] + (left[i - 1] === right[j - 1] ? 0 : 1));
|
|
161
|
+
previous = current;
|
|
162
|
+
}
|
|
163
|
+
return previous[right.length];
|
|
164
|
+
}
|
|
165
|
+
function optionKeys(value, allowed, name) {
|
|
166
|
+
for (const key of Object.keys(value)) {
|
|
167
|
+
if (allowed.includes(key)) continue;
|
|
168
|
+
const suggestion = allowed.map((candidate) => ({
|
|
169
|
+
candidate,
|
|
170
|
+
distance: distance(key, candidate)
|
|
171
|
+
})).sort((left, right) => left.distance - right.distance).find((item) => item.distance <= 2)?.candidate;
|
|
172
|
+
throw new TypeError(`Unsupported option "${name}.${key}".${suggestion ? ` Did you mean "${name}.${suggestion}"?` : ` Supported keys: ${allowed.join(", ")}.`}`);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
function scalar(value, type, name) {
|
|
176
|
+
if (value !== void 0 && typeof value !== type) throw new TypeError(`${name} must be a ${type}`);
|
|
177
|
+
}
|
|
178
|
+
function choice(value, allowed, name) {
|
|
179
|
+
if (value !== void 0 && !allowed.includes(value)) throw new TypeError(`${name} must be one of ${allowed.join(", ")}`);
|
|
180
|
+
}
|
|
181
|
+
function positiveInteger(value, name) {
|
|
182
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 1) throw new TypeError(`${name} must be a positive safe integer`);
|
|
183
|
+
}
|
|
184
|
+
function validateListingPlan(value) {
|
|
185
|
+
const plan = optionObject(value, "listing");
|
|
186
|
+
optionKeys(plan, ["html", "data"], "listing");
|
|
187
|
+
for (const [key, strategy, size] of [[
|
|
188
|
+
"html",
|
|
189
|
+
"paged",
|
|
190
|
+
"pageSize"
|
|
191
|
+
], [
|
|
192
|
+
"data",
|
|
193
|
+
"blocks",
|
|
194
|
+
"blockSize"
|
|
195
|
+
]]) {
|
|
196
|
+
const section = optionObject(plan[key], `listing.${key}`);
|
|
197
|
+
optionKeys(section, section.strategy === strategy ? ["strategy", size] : ["strategy"], `listing.${key}`);
|
|
198
|
+
if (section.strategy !== "all" && section.strategy !== strategy) throw new TypeError(`listing.${key}.strategy must be all or ${strategy}`);
|
|
199
|
+
if (section.strategy === strategy) positiveInteger(section[size], `listing.${key}.${size}`);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
function selection(value, name) {
|
|
203
|
+
for (const key of ["include", "exclude"]) if (value[key] !== void 0 && typeof value[key] !== "string" && (!Array.isArray(value[key]) || value[key].some((item) => typeof item !== "string"))) throw new TypeError(`${name}.${key} must be a string or array of strings`);
|
|
204
|
+
resolveIgnoreFiles(value.ignoreFiles);
|
|
205
|
+
scalar(value.entryData, "function", `${name}.entryData`);
|
|
206
|
+
if (value.sort !== void 0) {
|
|
207
|
+
const sort = optionObject(value.sort, `${name}.sort`);
|
|
208
|
+
optionKeys(sort, [
|
|
209
|
+
"field",
|
|
210
|
+
"direction",
|
|
211
|
+
"directoriesFirst",
|
|
212
|
+
"nameMode"
|
|
213
|
+
], `${name}.sort`);
|
|
214
|
+
scalar(sort.directoriesFirst, "boolean", `${name}.sort.directoriesFirst`);
|
|
215
|
+
choice(sort.field, [
|
|
216
|
+
"name",
|
|
217
|
+
"size",
|
|
218
|
+
"modified"
|
|
219
|
+
], `${name}.sort.field`);
|
|
220
|
+
choice(sort.direction, ["asc", "desc"], `${name}.sort.direction`);
|
|
221
|
+
choice(sort.nameMode, [
|
|
222
|
+
"natural",
|
|
223
|
+
"locale",
|
|
224
|
+
"unicode"
|
|
225
|
+
], `${name}.sort.nameMode`);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
function requiredString(value, name) {
|
|
229
|
+
if (typeof value !== "string" || !value.length) throw new TypeError(`${name} must be a non-empty string`);
|
|
230
|
+
}
|
|
231
|
+
function validateTheme(value, name = "options.theme") {
|
|
232
|
+
if (value === void 0) throw new TypeError("Core requires an explicit theme");
|
|
233
|
+
const theme = optionObject(value, name);
|
|
234
|
+
requiredString(theme.name, `${name}.name`);
|
|
235
|
+
if (typeof theme.render !== "function") throw new TypeError(`${name}.render must be a function`);
|
|
236
|
+
scalar(theme.entryData, "function", `${name}.entryData`);
|
|
237
|
+
scalar(theme.searchIndex, "boolean", `${name}.searchIndex`);
|
|
238
|
+
if (theme.listing !== void 0) {
|
|
239
|
+
const listing = optionObject(theme.listing, `${name}.listing`);
|
|
240
|
+
optionKeys(listing, ["pagination"], `${name}.listing`);
|
|
241
|
+
if (typeof listing.pagination !== "boolean") throw new TypeError(`${name}.listing.pagination must be a boolean`);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
const generateKeys = {
|
|
245
|
+
sourceDir: true,
|
|
246
|
+
outputDir: true,
|
|
247
|
+
theme: true,
|
|
248
|
+
include: true,
|
|
249
|
+
exclude: true,
|
|
250
|
+
ignoreFiles: true,
|
|
251
|
+
sort: true,
|
|
252
|
+
entryData: true,
|
|
253
|
+
fileHref: true,
|
|
254
|
+
dryRun: true,
|
|
255
|
+
outputPolicy: true,
|
|
256
|
+
listing: true,
|
|
257
|
+
metadata: true,
|
|
258
|
+
base: true,
|
|
259
|
+
mode: true,
|
|
260
|
+
mirror: true,
|
|
261
|
+
outputName: true,
|
|
262
|
+
urlStrategy: true,
|
|
263
|
+
symlinks: true
|
|
264
|
+
};
|
|
265
|
+
function validateGenerateOptions(value) {
|
|
266
|
+
const options = optionObject(value, "options");
|
|
267
|
+
optionKeys(options, Object.keys(generateKeys), "options");
|
|
268
|
+
requiredString(options.sourceDir, "options.sourceDir");
|
|
269
|
+
requiredString(options.outputDir, "options.outputDir");
|
|
270
|
+
validateTheme(options.theme);
|
|
271
|
+
selection(options, "options");
|
|
272
|
+
for (const key of ["fileHref", "metadata"]) scalar(options[key], "function", `options.${key}`);
|
|
273
|
+
for (const key of ["dryRun", "mirror"]) scalar(options[key], "boolean", `options.${key}`);
|
|
274
|
+
scalar(options.base, "string", "options.base");
|
|
275
|
+
choice(options.mode, ["ssg", "mpa"], "options.mode");
|
|
276
|
+
choice(options.urlStrategy, Object.keys({
|
|
277
|
+
base: true,
|
|
278
|
+
"html-base": true,
|
|
279
|
+
relative: true
|
|
280
|
+
}), "options.urlStrategy");
|
|
281
|
+
choice(options.outputPolicy, ["owned", "replace"], "options.outputPolicy");
|
|
282
|
+
if (options.outputName !== void 0 && typeof options.outputName !== "string" && typeof options.outputName !== "function") throw new TypeError("options.outputName must be a string or function");
|
|
283
|
+
if (options.listing !== void 0 && typeof options.listing !== "function") validateListingPlan(options.listing);
|
|
284
|
+
if (options.symlinks !== void 0) {
|
|
285
|
+
const links = optionObject(options.symlinks, "options.symlinks");
|
|
286
|
+
optionKeys(links, [
|
|
287
|
+
"follow",
|
|
288
|
+
"boundary",
|
|
289
|
+
"onCycle"
|
|
290
|
+
], "options.symlinks");
|
|
291
|
+
scalar(links.follow, "boolean", "options.symlinks.follow");
|
|
292
|
+
choice(links.boundary, ["root", "anywhere"], "options.symlinks.boundary");
|
|
293
|
+
choice(links.onCycle, ["error", "skip"], "options.symlinks.onCycle");
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
const serviceKeys = {
|
|
297
|
+
root: true,
|
|
298
|
+
include: true,
|
|
299
|
+
exclude: true,
|
|
300
|
+
ignoreFiles: true,
|
|
301
|
+
sort: true,
|
|
302
|
+
entryData: true
|
|
303
|
+
};
|
|
304
|
+
function validateDirectoryOptions(value) {
|
|
305
|
+
const options = optionObject(value, "options");
|
|
306
|
+
optionKeys(options, Object.keys(serviceKeys), "options");
|
|
307
|
+
requiredString(options.root, "options.root");
|
|
308
|
+
selection(options, "options");
|
|
309
|
+
}
|
|
310
|
+
//#endregion
|
|
7
311
|
//#region src/filesystem.ts
|
|
8
312
|
function entryKind(stats) {
|
|
9
313
|
if (stats.isDirectory()) return "directory";
|
|
@@ -78,6 +382,7 @@ async function describeEntry({ absolutePath, relativePath, rootRealPath, ancesto
|
|
|
78
382
|
function projectListingEntry(entry, context) {
|
|
79
383
|
const link = entry.symlink;
|
|
80
384
|
return {
|
|
385
|
+
...entry.publicData === void 0 ? {} : { data: copyJsonObject(entry.publicData) },
|
|
81
386
|
id: entry.relativePath,
|
|
82
387
|
name: entry.name,
|
|
83
388
|
kind: entry.kind,
|
|
@@ -94,18 +399,13 @@ function projectListingEntry(entry, context) {
|
|
|
94
399
|
}
|
|
95
400
|
};
|
|
96
401
|
}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
if (!record(value) || !record(value.html) || !record(value.data)) throw new TypeError("listing must contain html and data plans");
|
|
105
|
-
if (value.html.strategy === "paged") positiveInteger(value.html.pageSize, "listing.html.pageSize");
|
|
106
|
-
else if (value.html.strategy !== "all") throw new TypeError("listing.html.strategy must be \"all\" or \"paged\"");
|
|
107
|
-
if (value.data.strategy === "blocks") positiveInteger(value.data.blockSize, "listing.data.blockSize");
|
|
108
|
-
else if (value.data.strategy !== "all") throw new TypeError("listing.data.strategy must be \"all\" or \"blocks\"");
|
|
402
|
+
/** Annotation providers receive public facts, not transport URLs or raw stat objects. */
|
|
403
|
+
function entryDataFacts(entry) {
|
|
404
|
+
const { href: _href, exitsExplorer: _exits, data: _data, ...facts } = projectListingEntry(entry, {
|
|
405
|
+
hrefFor: () => null,
|
|
406
|
+
exitsExplorerFor: () => false
|
|
407
|
+
});
|
|
408
|
+
return facts;
|
|
109
409
|
}
|
|
110
410
|
function listingRevision(entries, sort, hrefBase, blockSize) {
|
|
111
411
|
return createHash("sha256").update(JSON.stringify({
|
|
@@ -117,69 +417,87 @@ function listingRevision(entries, sort, hrefBase, blockSize) {
|
|
|
117
417
|
})).digest("hex");
|
|
118
418
|
}
|
|
119
419
|
//#endregion
|
|
120
|
-
//#region src/
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
const ignored = ignoredPaths.map((value) => path.resolve(value));
|
|
134
|
-
const included = normalizePathPatterns(include, "include").map((pattern) => picomatch(pattern, {
|
|
135
|
-
dot: true,
|
|
136
|
-
nonegate: true
|
|
137
|
-
}));
|
|
138
|
-
const excluded = normalizePathPatterns(exclude, "exclude").map((pattern) => picomatch(pattern, {
|
|
139
|
-
dot: true,
|
|
140
|
-
nonegate: true
|
|
141
|
-
}));
|
|
142
|
-
const selected = /* @__PURE__ */ new Set([""]);
|
|
143
|
-
const symlinks = /* @__PURE__ */ new Map();
|
|
144
|
-
async function visit(directory, relativeDir, inheritedInclude) {
|
|
145
|
-
let hasSelected = false;
|
|
146
|
-
for (const entry of await readdir(directory, { withFileTypes: true })) {
|
|
147
|
-
const absolutePath = path.join(directory, entry.name);
|
|
148
|
-
if (isWithin(outputDir, absolutePath) || ignored.some((value) => isWithin(value, absolutePath))) continue;
|
|
149
|
-
const relativePath = path.posix.join(relativeDir, entry.name);
|
|
150
|
-
const directoryLike = entry.isDirectory();
|
|
151
|
-
const matches = (matcher) => matcher(relativePath) || directoryLike && matcher(`${relativePath}/`);
|
|
152
|
-
if (excluded.some(matches)) continue;
|
|
153
|
-
const includedHere = inheritedInclude || included.some(matches);
|
|
154
|
-
if (directoryLike) {
|
|
155
|
-
const hasSelectedChild = await visit(absolutePath, relativePath, includedHere);
|
|
156
|
-
if (included.length === 0 || includedHere || hasSelectedChild) {
|
|
157
|
-
selected.add(relativePath);
|
|
158
|
-
hasSelected = true;
|
|
159
|
-
}
|
|
160
|
-
} else if (included.length === 0 || includedHere) {
|
|
161
|
-
selected.add(relativePath);
|
|
162
|
-
hasSelected = true;
|
|
163
|
-
if (entry.isSymbolicLink()) symlinks.set(relativePath, await readlink(absolutePath));
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
return hasSelected;
|
|
420
|
+
//#region src/publication.ts
|
|
421
|
+
const outputMarker = "__dirwell/ownership.json";
|
|
422
|
+
const outputMarkerContents = "{\"type\":\"dirwell-output\",\"version\":1}\n";
|
|
423
|
+
async function lockOutput(output) {
|
|
424
|
+
const canonical = await canonicalPath(output);
|
|
425
|
+
const lockPath = path.join(path.dirname(canonical), `.${path.basename(canonical)}-dirwell.lock`);
|
|
426
|
+
await mkdir(path.dirname(lockPath), { recursive: true });
|
|
427
|
+
let handle;
|
|
428
|
+
try {
|
|
429
|
+
handle = await open(lockPath, "wx");
|
|
430
|
+
} catch (error) {
|
|
431
|
+
if (error instanceof Error && "code" in error && error.code === "EEXIST") throw new Error(`Output is locked by another build: ${lockPath}. Wait for it to finish. Remove a stale lock only after confirming its owner has stopped; --force does not bypass locks.`);
|
|
432
|
+
throw error;
|
|
167
433
|
}
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
const targetRelativePath = path.relative(sourceDir, targetPath).split(path.sep).join("/");
|
|
175
|
-
if (selected.has(targetRelativePath)) mirroredSymlinks.add(relativePath);
|
|
434
|
+
try {
|
|
435
|
+
await handle.writeFile(JSON.stringify({ pid: process.pid }));
|
|
436
|
+
} catch (error) {
|
|
437
|
+
await handle.close();
|
|
438
|
+
await rm(lockPath);
|
|
439
|
+
throw error;
|
|
176
440
|
}
|
|
177
441
|
return {
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
442
|
+
lockPath,
|
|
443
|
+
async release() {
|
|
444
|
+
await handle.close();
|
|
445
|
+
await rm(lockPath);
|
|
446
|
+
}
|
|
181
447
|
};
|
|
182
448
|
}
|
|
449
|
+
async function canonicalPath(filename) {
|
|
450
|
+
try {
|
|
451
|
+
return await realpath(filename);
|
|
452
|
+
} catch (error) {
|
|
453
|
+
if (!(error instanceof Error && "code" in error && error.code === "ENOENT")) throw error;
|
|
454
|
+
const parent = path.dirname(filename);
|
|
455
|
+
if (parent === filename) throw error;
|
|
456
|
+
return path.join(await canonicalPath(parent), path.basename(filename));
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
/** Force permits output ownership replacement, never source containment violations. */
|
|
460
|
+
async function checkOutput(source, output, policy = "owned") {
|
|
461
|
+
if (policy !== "owned" && policy !== "replace") throw new TypeError("outputPolicy must be owned or replace");
|
|
462
|
+
if (isInsideRoot(await canonicalPath(output), await realpath(source))) throw new Error("outputDir must not contain the sourceDir");
|
|
463
|
+
let stat;
|
|
464
|
+
try {
|
|
465
|
+
stat = await lstat(output);
|
|
466
|
+
} catch (error) {
|
|
467
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") return false;
|
|
468
|
+
throw error;
|
|
469
|
+
}
|
|
470
|
+
if (stat.isSymbolicLink() || !stat.isDirectory()) throw new Error("outputDir must be a directory, not a file or symlink");
|
|
471
|
+
if ((await readdir(output)).length === 0 || policy === "replace") return true;
|
|
472
|
+
try {
|
|
473
|
+
const marker = path.join(output, outputMarker);
|
|
474
|
+
if (!(await lstat(path.dirname(marker))).isSymbolicLink() && !(await lstat(marker)).isSymbolicLink() && await readFile(marker, "utf8") === "{\"type\":\"dirwell-output\",\"version\":1}\n") return true;
|
|
475
|
+
} catch (error) {
|
|
476
|
+
if (!(error instanceof Error && "code" in error && error.code === "ENOENT")) throw error;
|
|
477
|
+
}
|
|
478
|
+
throw new Error("Output is nonempty and not owned by Dirwell. Choose another directory or explicitly replace it with --force (-f) / outputPolicy: 'replace'.");
|
|
479
|
+
}
|
|
480
|
+
/** Keep a recoverable previous tree until the new tree is installed. */
|
|
481
|
+
async function publishOutput(staged, output, source, policy = "owned") {
|
|
482
|
+
const exists = await checkOutput(source, output, policy);
|
|
483
|
+
await mkdir(path.dirname(output), { recursive: true });
|
|
484
|
+
const backup = path.join(path.dirname(output), `.${path.basename(output)}-previous-${randomUUID()}`);
|
|
485
|
+
if (exists) await rename(output, backup);
|
|
486
|
+
try {
|
|
487
|
+
await rename(staged, output);
|
|
488
|
+
} catch (error) {
|
|
489
|
+
if (exists) try {
|
|
490
|
+
await rename(backup, output);
|
|
491
|
+
} catch (restoreError) {
|
|
492
|
+
throw new AggregateError([error, restoreError], `Publication failed; previous output is retained at ${backup}`);
|
|
493
|
+
}
|
|
494
|
+
throw error;
|
|
495
|
+
}
|
|
496
|
+
if (exists) await rm(backup, {
|
|
497
|
+
recursive: true,
|
|
498
|
+
force: true
|
|
499
|
+
});
|
|
500
|
+
}
|
|
183
501
|
//#endregion
|
|
184
502
|
//#region src/generator.ts
|
|
185
503
|
const indexPattern = /^index\.html?$/i;
|
|
@@ -250,10 +568,28 @@ async function generateExplorer(options) {
|
|
|
250
568
|
}
|
|
251
569
|
/** Internal entry point for hosts that stage previews outside their published output. */
|
|
252
570
|
async function generateExplorerSkippingPaths(options, ignoredPaths) {
|
|
253
|
-
|
|
254
|
-
const
|
|
571
|
+
validateGenerateOptions(options);
|
|
572
|
+
const source = path.resolve(options.sourceDir), output = path.resolve(options.outputDir);
|
|
573
|
+
await checkOutput(source, output, options.outputPolicy);
|
|
574
|
+
if (options.dryRun) return generateExplorerUnlocked(options, ignoredPaths);
|
|
575
|
+
const lock = await lockOutput(output);
|
|
576
|
+
const lexicalLock = path.join(path.dirname(output), path.basename(lock.lockPath));
|
|
577
|
+
try {
|
|
578
|
+
return await generateExplorerUnlocked(options, [
|
|
579
|
+
...ignoredPaths,
|
|
580
|
+
lock.lockPath,
|
|
581
|
+
lexicalLock
|
|
582
|
+
]);
|
|
583
|
+
} finally {
|
|
584
|
+
await lock.release();
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
async function generateExplorerUnlocked(options, ignoredPaths) {
|
|
588
|
+
const sourceDir = await realpath(path.resolve(options.sourceDir));
|
|
589
|
+
const outputDir = await canonicalPath(path.resolve(options.outputDir));
|
|
255
590
|
const ignoredSourcePaths = ignoredPaths.map((value) => path.resolve(value));
|
|
256
591
|
const rootRealPath = await realpath(sourceDir);
|
|
592
|
+
const replacesOutput = await checkOutput(sourceDir, outputDir, options.outputPolicy);
|
|
257
593
|
const outputName = options.outputName ?? defaultOutputName;
|
|
258
594
|
const resolveOutputName = typeof outputName === "string" ? () => outputName : outputName;
|
|
259
595
|
const theme = options.theme;
|
|
@@ -264,7 +600,7 @@ async function generateExplorerSkippingPaths(options, ignoredPaths) {
|
|
|
264
600
|
const followSymlinks = options.symlinks?.follow ?? false;
|
|
265
601
|
const boundary = options.symlinks?.boundary ?? "root";
|
|
266
602
|
const onCycle = options.symlinks?.onCycle ?? "skip";
|
|
267
|
-
const filtersActive = options.include !== void 0 || options.exclude !== void 0 || ignoredSourcePaths.length > 0;
|
|
603
|
+
const filtersActive = options.include !== void 0 || options.exclude !== void 0 || (options.ignoreFiles?.length ?? 0) > 0 || ignoredSourcePaths.length > 0;
|
|
268
604
|
const plans = [];
|
|
269
605
|
if (isInsideRoot(outputDir, sourceDir)) throw new Error("outputDir must not contain the sourceDir");
|
|
270
606
|
try {
|
|
@@ -274,13 +610,13 @@ async function generateExplorerSkippingPaths(options, ignoredPaths) {
|
|
|
274
610
|
if (error instanceof Error && "code" in error && error.code === "ENOENT") {} else throw error;
|
|
275
611
|
}
|
|
276
612
|
const outputIsWithinSource = isInsideRoot(sourceDir, outputDir);
|
|
277
|
-
const selection = await selectSourcePaths(sourceDir, outputDir, options.include, options.exclude, ignoredSourcePaths);
|
|
613
|
+
const selection = await selectSourcePaths(sourceDir, outputDir, options.include, options.exclude, ignoredSourcePaths, options.ignoreFiles);
|
|
278
614
|
const stageParent = outputIsWithinSource ? path.dirname(sourceDir) : path.dirname(outputDir);
|
|
279
|
-
await mkdir(stageParent, { recursive: true });
|
|
280
|
-
const stagedOutputDir = await mkdtemp(path.join(stageParent, `.${path.basename(outputDir)}-dirwell-`));
|
|
615
|
+
if (!options.dryRun) await mkdir(stageParent, { recursive: true });
|
|
616
|
+
const stagedOutputDir = options.dryRun ? null : await mkdtemp(path.join(stageParent, `.${path.basename(outputDir)}-dirwell-`));
|
|
281
617
|
const buildOutputDir = stagedOutputDir;
|
|
282
618
|
try {
|
|
283
|
-
if (options.mirror ?? true) {
|
|
619
|
+
if (!options.dryRun && (options.mirror ?? true)) {
|
|
284
620
|
await rm(buildOutputDir, {
|
|
285
621
|
recursive: true,
|
|
286
622
|
force: true
|
|
@@ -297,7 +633,7 @@ async function generateExplorerSkippingPaths(options, ignoredPaths) {
|
|
|
297
633
|
});
|
|
298
634
|
}
|
|
299
635
|
} catch (error) {
|
|
300
|
-
await rm(stagedOutputDir, {
|
|
636
|
+
if (stagedOutputDir !== null) await rm(stagedOutputDir, {
|
|
301
637
|
recursive: true,
|
|
302
638
|
force: true
|
|
303
639
|
});
|
|
@@ -317,14 +653,23 @@ async function generateExplorerSkippingPaths(options, ignoredPaths) {
|
|
|
317
653
|
const relativePath = path.relative(rootRealPath, path.join(physicalRealPath, name));
|
|
318
654
|
return selection.selected.has(relativePath.split(path.sep).join("/"));
|
|
319
655
|
});
|
|
320
|
-
const entries = await Promise.all(names.map((name) =>
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
656
|
+
const entries = await Promise.all(names.map(async (name) => {
|
|
657
|
+
const entry = await describeEntry({
|
|
658
|
+
absolutePath: path.join(physicalDir, name),
|
|
659
|
+
relativePath: path.posix.join(logicalDir, name),
|
|
660
|
+
rootRealPath,
|
|
661
|
+
ancestors: nextAncestors,
|
|
662
|
+
followSymlinks,
|
|
663
|
+
selectedPaths: filtersActive ? selection.selected : null
|
|
664
|
+
});
|
|
665
|
+
return options.entryData === void 0 ? entry : {
|
|
666
|
+
...entry,
|
|
667
|
+
publicData: copyJsonObject(await options.entryData(entryDataFacts(entry), {
|
|
668
|
+
operation: "build",
|
|
669
|
+
directoryPath: logicalDir
|
|
670
|
+
}))
|
|
671
|
+
};
|
|
672
|
+
}));
|
|
328
673
|
entries.sort((left, right) => compareEntries(left, right, options.sort) || (options.listing === void 0 ? 0 : compareUnicode(left.relativePath, right.relativePath)));
|
|
329
674
|
const describedCurrent = await describeEntry({
|
|
330
675
|
absolutePath: physicalDir,
|
|
@@ -388,6 +733,20 @@ async function generateExplorerSkippingPaths(options, ignoredPaths) {
|
|
|
388
733
|
}
|
|
389
734
|
try {
|
|
390
735
|
await visit("", sourceDir, /* @__PURE__ */ new Set());
|
|
736
|
+
const report = {
|
|
737
|
+
sourceDir,
|
|
738
|
+
outputDir,
|
|
739
|
+
theme: theme.name,
|
|
740
|
+
dryRun: options.dryRun ?? false,
|
|
741
|
+
mirror: options.mirror ?? true,
|
|
742
|
+
replacesOutput,
|
|
743
|
+
selectedPaths: [...selection.selected].filter(Boolean).sort(),
|
|
744
|
+
pages: plans.map((plan) => ({
|
|
745
|
+
directory: plan.logicalDir,
|
|
746
|
+
outputName: plan.outputName
|
|
747
|
+
}))
|
|
748
|
+
};
|
|
749
|
+
if (options.dryRun) return report;
|
|
391
750
|
const generatedPages = new Map(plans.filter((plan) => plan.outputName !== null).map((plan) => [plan.logicalDir, plan.outputName]));
|
|
392
751
|
const generatedDirectories = new Set(generatedPages.keys());
|
|
393
752
|
const pagePathForDirectory = (logicalPath) => {
|
|
@@ -406,6 +765,15 @@ async function generateExplorerSkippingPaths(options, ignoredPaths) {
|
|
|
406
765
|
const sharedAssets = /* @__PURE__ */ new Map();
|
|
407
766
|
const rawLinkFiles = /* @__PURE__ */ new Map();
|
|
408
767
|
const searchEntries = /* @__PURE__ */ new Map();
|
|
768
|
+
const fileHrefs = /* @__PURE__ */ new Map();
|
|
769
|
+
const mappedFileHref = (logicalPath) => {
|
|
770
|
+
if (!fileHrefs.has(logicalPath)) {
|
|
771
|
+
const href = options.fileHref(logicalPath);
|
|
772
|
+
if (href !== null && (typeof href !== "string" || href.length === 0 || href.includes("\\") || Array.from(href).some((character) => character.charCodeAt(0) < 32) || !["http:", "https:"].includes(new URL(href, "https://dirwell.invalid/").protocol))) throw new TypeError("fileHref must return a synchronous HTTP(S), root-relative or relative URL, or null");
|
|
773
|
+
fileHrefs.set(logicalPath, href);
|
|
774
|
+
}
|
|
775
|
+
return fileHrefs.get(logicalPath);
|
|
776
|
+
};
|
|
409
777
|
for (const { directory, logicalDir } of theme.searchIndex === false ? [] : plans) for (const entry of directory.entries) {
|
|
410
778
|
if (searchEntries.has(entry.relativePath)) continue;
|
|
411
779
|
const targetLogicalPath = isUnavailableSymlink(entry) ? null : entry.symlink?.targetRelativePath !== null && entry.symlink !== null ? entry.symlink.targetRelativePath : path.posix.join(logicalDir, entry.name);
|
|
@@ -416,7 +784,10 @@ async function generateExplorerSkippingPaths(options, ignoredPaths) {
|
|
|
416
784
|
const filename = rawLinkFilename(entry);
|
|
417
785
|
rawLinkFiles.set(filename, entry.symlink.target);
|
|
418
786
|
href = `raw-links/${filename}`;
|
|
419
|
-
} else if (linkedPath !== null)
|
|
787
|
+
} else if (linkedPath !== null) {
|
|
788
|
+
href = linkedPath === "" ? "../" : `../${encodeLogicalPath(linkedPath)}${directoryLike && linkedPath === targetLogicalPath ? "/" : ""}`;
|
|
789
|
+
if (!directoryLike && options.fileHref !== void 0) href = mappedFileHref(linkedPath);
|
|
790
|
+
}
|
|
420
791
|
searchEntries.set(entry.relativePath, {
|
|
421
792
|
exitsExplorer: isUnavailableSymlink(entry) || targetLogicalPath !== null && (!directoryLike || !generatedDirectories.has(targetLogicalPath)),
|
|
422
793
|
href,
|
|
@@ -460,6 +831,7 @@ async function generateExplorerSkippingPaths(options, ignoredPaths) {
|
|
|
460
831
|
const targetLogicalPath = targetLogicalPathFor(entry);
|
|
461
832
|
if (targetLogicalPath === null) return null;
|
|
462
833
|
const directoryLike = entry.kind === "directory" || entry.symlink?.targetKind === "directory";
|
|
834
|
+
if (!directoryLike && options.fileHref !== void 0) return mappedFileHref(targetLogicalPath);
|
|
463
835
|
return hrefForLogicalPath(targetLogicalPath, directoryLike);
|
|
464
836
|
};
|
|
465
837
|
const baseContext = {
|
|
@@ -652,14 +1024,12 @@ async function generateExplorerSkippingPaths(options, ignoredPaths) {
|
|
|
652
1024
|
version: 3
|
|
653
1025
|
}));
|
|
654
1026
|
}
|
|
655
|
-
await
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
await mkdir(path.dirname(outputDir), { recursive: true });
|
|
660
|
-
await rename(stagedOutputDir, outputDir);
|
|
1027
|
+
await mkdir(path.join(buildOutputDir, "__dirwell"), { recursive: true });
|
|
1028
|
+
await writeFile(path.join(buildOutputDir, outputMarker), outputMarkerContents);
|
|
1029
|
+
await publishOutput(buildOutputDir, outputDir, sourceDir, options.outputPolicy);
|
|
1030
|
+
return report;
|
|
661
1031
|
} finally {
|
|
662
|
-
await rm(stagedOutputDir, {
|
|
1032
|
+
if (stagedOutputDir !== null) await rm(stagedOutputDir, {
|
|
663
1033
|
recursive: true,
|
|
664
1034
|
force: true
|
|
665
1035
|
});
|
|
@@ -692,8 +1062,17 @@ function assertLogicalPath(value) {
|
|
|
692
1062
|
}
|
|
693
1063
|
/** Live reads; no watcher, recursive stat snapshot, output tree or implicit cache. */
|
|
694
1064
|
async function createDirectoryService(options) {
|
|
1065
|
+
validateDirectoryOptions(options);
|
|
695
1066
|
const root = await realpath(options.root);
|
|
696
|
-
const
|
|
1067
|
+
const ignoreFiles = resolveIgnoreFiles(options.ignoreFiles);
|
|
1068
|
+
await createIgnoreMatcher(ignoreFiles);
|
|
1069
|
+
normalizePathPatterns(options.include, "include");
|
|
1070
|
+
normalizePathPatterns(options.exclude, "exclude");
|
|
1071
|
+
const selected = async () => {
|
|
1072
|
+
const matcher = await createIgnoreMatcher(ignoreFiles);
|
|
1073
|
+
if (normalizePathPatterns(options.include, "include").length === 0 && normalizePathPatterns(options.exclude, "exclude").length === 0 && !matcher.hasRules) return null;
|
|
1074
|
+
return (await selectSourcePaths(root, path.join(root, "__dirwell"), options.include, options.exclude, [], ignoreFiles, matcher)).selected;
|
|
1075
|
+
};
|
|
697
1076
|
async function describe(logicalPath, selection) {
|
|
698
1077
|
assertLogicalPath(logicalPath);
|
|
699
1078
|
if (selection !== null && !selection.has(logicalPath)) throw new DirectoryAccessError("Unavailable path");
|
|
@@ -715,6 +1094,23 @@ async function createDirectoryService(options) {
|
|
|
715
1094
|
});
|
|
716
1095
|
}
|
|
717
1096
|
return {
|
|
1097
|
+
async scan(budget) {
|
|
1098
|
+
if (!budget || !Number.isSafeInteger(budget.maxEntries) || budget.maxEntries < 1 || !Number.isSafeInteger(budget.maxDirectories) || budget.maxDirectories < 1 || !(budget.signal instanceof AbortSignal) || typeof budget.allowPath !== "function") throw new TypeError("scan requires positive entry/directory limits, an AbortSignal and allowPath");
|
|
1099
|
+
const selection = (await selectSourcePaths(root, path.join(root, "__dirwell"), options.include, options.exclude, [], ignoreFiles, void 0, budget)).selected;
|
|
1100
|
+
const entries = [];
|
|
1101
|
+
for (const logical of [...selection].sort()) {
|
|
1102
|
+
budget.signal.throwIfAborted();
|
|
1103
|
+
if (!logical) continue;
|
|
1104
|
+
try {
|
|
1105
|
+
const entry = await describe(logical, selection);
|
|
1106
|
+
if (entry.kind === "file" || entry.kind === "directory") entries.push(entry);
|
|
1107
|
+
} catch (error) {
|
|
1108
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") continue;
|
|
1109
|
+
throw error;
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
return entries;
|
|
1113
|
+
},
|
|
718
1114
|
async resolvePath(logicalPath) {
|
|
719
1115
|
const selection = await selected();
|
|
720
1116
|
const entry = await describe(logicalPath, selection);
|
|
@@ -762,12 +1158,20 @@ async function createDirectoryService(options) {
|
|
|
762
1158
|
if (!isInsideRoot(root, physical)) throw new DirectoryAccessError("Unavailable directory");
|
|
763
1159
|
const names = (await readdir(physical)).filter((name) => logicalPath !== "" || name !== "__dirwell");
|
|
764
1160
|
const entries = (await Promise.all(names.filter((name) => selection === null || selection.has(path.posix.join(logicalPath, name))).map(async (name) => {
|
|
1161
|
+
let entry;
|
|
765
1162
|
try {
|
|
766
|
-
|
|
1163
|
+
entry = await describe(path.posix.join(logicalPath, name), selection);
|
|
767
1164
|
} catch (error) {
|
|
768
1165
|
if (error instanceof Error && "code" in error && error.code === "ENOENT") return null;
|
|
769
1166
|
throw error;
|
|
770
1167
|
}
|
|
1168
|
+
return options.entryData === void 0 ? entry : {
|
|
1169
|
+
...entry,
|
|
1170
|
+
publicData: copyJsonObject(await options.entryData(entryDataFacts(entry), {
|
|
1171
|
+
operation: "server",
|
|
1172
|
+
directoryPath: logicalPath
|
|
1173
|
+
}))
|
|
1174
|
+
};
|
|
771
1175
|
}))).filter((entry) => entry !== null);
|
|
772
1176
|
entries.sort((left, right) => compareEntries(left, right, options.sort) || (left.relativePath < right.relativePath ? -1 : left.relativePath > right.relativePath ? 1 : 0));
|
|
773
1177
|
return {
|
|
@@ -784,6 +1188,6 @@ async function createDirectoryService(options) {
|
|
|
784
1188
|
};
|
|
785
1189
|
}
|
|
786
1190
|
//#endregion
|
|
787
|
-
export { DirectoryAccessError, compareEntries, copyJsonObject, createDirectoryService, defaultOutputName, escapeHtml, generateExplorer, generateExplorerSkippingPaths, listingRevision, projectListingEntry, readGeneratedPage, validateListingPlan };
|
|
1191
|
+
export { DirectoryAccessError, SelectionBudgetError, compareEntries, copyJsonObject, createDirectoryService, defaultOutputName, escapeHtml, generateExplorer, generateExplorerSkippingPaths, listingRevision, projectListingEntry, readGeneratedPage, resolveIgnoreFiles, validateListingPlan };
|
|
788
1192
|
|
|
789
1193
|
//# sourceMappingURL=index.mjs.map
|