@johnhenry/sitefile 0.0.0-stage → 0.0.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/CHANGELOG.md +48 -0
- package/LICENSE +21 -0
- package/README.md +475 -2
- package/lib/src/archive-writers.d.ts +31 -0
- package/lib/src/archive-writers.js +197 -0
- package/lib/src/attest.d.ts +126 -0
- package/lib/src/attest.js +220 -0
- package/lib/src/bake.d.ts +74 -0
- package/lib/src/bake.js +372 -0
- package/lib/src/base-image.d.ts +76 -0
- package/lib/src/base-image.js +138 -0
- package/lib/src/build-args.d.ts +26 -0
- package/lib/src/build-args.js +164 -0
- package/lib/src/cache-limit.d.ts +54 -0
- package/lib/src/cache-limit.js +239 -0
- package/lib/src/casefold-data.d.ts +2 -0
- package/lib/src/casefold-data.js +217 -0
- package/lib/src/cbor.d.ts +39 -0
- package/lib/src/cbor.js +174 -0
- package/lib/src/checklinks.d.ts +42 -0
- package/lib/src/checklinks.js +118 -0
- package/lib/src/cli/bake.d.ts +2 -0
- package/lib/src/cli/bake.js +179 -0
- package/lib/src/cli/build-image.d.ts +21 -0
- package/lib/src/cli/build-image.js +42 -0
- package/lib/src/cli/cache.d.ts +8 -0
- package/lib/src/cli/cache.js +109 -0
- package/lib/src/cli/common.d.ts +29 -0
- package/lib/src/cli/common.js +100 -0
- package/lib/src/cli/git-context.d.ts +18 -0
- package/lib/src/cli/git-context.js +48 -0
- package/lib/src/cli/images.d.ts +6 -0
- package/lib/src/cli/images.js +285 -0
- package/lib/src/cli/index.d.ts +10 -0
- package/lib/src/cli/index.js +95 -0
- package/lib/src/cli/outdated.d.ts +5 -0
- package/lib/src/cli/outdated.js +94 -0
- package/lib/src/cli/registry.d.ts +2 -0
- package/lib/src/cli/registry.js +122 -0
- package/lib/src/cli/serve-worker.d.ts +2 -0
- package/lib/src/cli/serve-worker.js +42 -0
- package/lib/src/cli/servers.d.ts +15 -0
- package/lib/src/cli/servers.js +451 -0
- package/lib/src/cli/vendor.d.ts +2 -0
- package/lib/src/cli/vendor.js +92 -0
- package/lib/src/cli/watch.d.ts +26 -0
- package/lib/src/cli/watch.js +81 -0
- package/lib/src/cli.d.ts +12 -0
- package/lib/src/cli.js +486 -0
- package/lib/src/csv.d.ts +27 -0
- package/lib/src/csv.js +117 -0
- package/lib/src/deflate.d.ts +76 -0
- package/lib/src/deflate.js +604 -0
- package/lib/src/digest.d.ts +3 -0
- package/lib/src/digest.js +7 -0
- package/lib/src/errors.d.ts +34 -0
- package/lib/src/errors.js +20 -0
- package/lib/src/executor.d.ts +386 -0
- package/lib/src/executor.js +2193 -0
- package/lib/src/explain.d.ts +64 -0
- package/lib/src/explain.js +445 -0
- package/lib/src/export-formats.d.ts +16 -0
- package/lib/src/export-formats.js +52 -0
- package/lib/src/exporter.d.ts +103 -0
- package/lib/src/exporter.js +676 -0
- package/lib/src/fingerprint.d.ts +40 -0
- package/lib/src/fingerprint.js +168 -0
- package/lib/src/frontmatter.d.ts +22 -0
- package/lib/src/frontmatter.js +45 -0
- package/lib/src/index.d.ts +12 -0
- package/lib/src/index.js +12 -0
- package/lib/src/language/ast.d.ts +353 -0
- package/lib/src/language/ast.js +57 -0
- package/lib/src/language/parser.d.ts +3 -0
- package/lib/src/language/parser.js +612 -0
- package/lib/src/language/scanner.d.ts +110 -0
- package/lib/src/language/scanner.js +449 -0
- package/lib/src/language/validate.d.ts +13 -0
- package/lib/src/language/validate.js +367 -0
- package/lib/src/links.d.ts +79 -0
- package/lib/src/links.js +583 -0
- package/lib/src/lint.d.ts +10 -0
- package/lib/src/lint.js +223 -0
- package/lib/src/markdown.d.ts +29 -0
- package/lib/src/markdown.js +612 -0
- package/lib/src/media.d.ts +82 -0
- package/lib/src/media.js +650 -0
- package/lib/src/memo.d.ts +57 -0
- package/lib/src/memo.js +93 -0
- package/lib/src/oci.d.ts +178 -0
- package/lib/src/oci.js +380 -0
- package/lib/src/outdated.d.ts +46 -0
- package/lib/src/outdated.js +195 -0
- package/lib/src/pages.d.ts +10 -0
- package/lib/src/pages.js +54 -0
- package/lib/src/paths.d.ts +39 -0
- package/lib/src/paths.js +101 -0
- package/lib/src/planner.d.ts +297 -0
- package/lib/src/planner.js +410 -0
- package/lib/src/plugins/cache.d.ts +70 -0
- package/lib/src/plugins/cache.js +226 -0
- package/lib/src/plugins/exec.d.ts +51 -0
- package/lib/src/plugins/exec.js +144 -0
- package/lib/src/plugins/host.d.ts +75 -0
- package/lib/src/plugins/host.js +277 -0
- package/lib/src/plugins/wasm.d.ts +23 -0
- package/lib/src/plugins/wasm.js +249 -0
- package/lib/src/profile.d.ts +42 -0
- package/lib/src/profile.js +68 -0
- package/lib/src/ref.d.ts +40 -0
- package/lib/src/ref.js +79 -0
- package/lib/src/registry-auth.d.ts +32 -0
- package/lib/src/registry-auth.js +151 -0
- package/lib/src/registry-cache.d.ts +71 -0
- package/lib/src/registry-cache.js +333 -0
- package/lib/src/registry-ops.d.ts +51 -0
- package/lib/src/registry-ops.js +248 -0
- package/lib/src/registry.d.ts +109 -0
- package/lib/src/registry.js +505 -0
- package/lib/src/render.d.ts +134 -0
- package/lib/src/render.js +576 -0
- package/lib/src/resources/archive.d.ts +11 -0
- package/lib/src/resources/archive.js +306 -0
- package/lib/src/resources/bundle-data.d.ts +16 -0
- package/lib/src/resources/bundle-data.js +50 -0
- package/lib/src/resources/convert.d.ts +12 -0
- package/lib/src/resources/convert.js +63 -0
- package/lib/src/resources/documents.d.ts +35 -0
- package/lib/src/resources/documents.js +122 -0
- package/lib/src/resources/git.d.ts +103 -0
- package/lib/src/resources/git.js +720 -0
- package/lib/src/resources/https.d.ts +40 -0
- package/lib/src/resources/https.js +256 -0
- package/lib/src/resources/ignore.d.ts +33 -0
- package/lib/src/resources/ignore.js +119 -0
- package/lib/src/resources/local.d.ts +64 -0
- package/lib/src/resources/local.js +248 -0
- package/lib/src/resources/lockfile.d.ts +52 -0
- package/lib/src/resources/lockfile.js +172 -0
- package/lib/src/resources/netpolicy.d.ts +6 -0
- package/lib/src/resources/netpolicy.js +82 -0
- package/lib/src/resources/types.d.ts +63 -0
- package/lib/src/resources/types.js +13 -0
- package/lib/src/resources/vendor.d.ts +94 -0
- package/lib/src/resources/vendor.js +296 -0
- package/lib/src/resources/wbn-reader.d.ts +44 -0
- package/lib/src/resources/wbn-reader.js +302 -0
- package/lib/src/schema.d.ts +25 -0
- package/lib/src/schema.js +169 -0
- package/lib/src/secrets.d.ts +21 -0
- package/lib/src/secrets.js +103 -0
- package/lib/src/serve-rules.d.ts +29 -0
- package/lib/src/serve-rules.js +122 -0
- package/lib/src/server.d.ts +45 -0
- package/lib/src/server.js +246 -0
- package/lib/src/servers.d.ts +71 -0
- package/lib/src/servers.js +139 -0
- package/lib/src/site.d.ts +60 -0
- package/lib/src/site.js +169 -0
- package/lib/src/store.d.ts +151 -0
- package/lib/src/store.js +596 -0
- package/lib/src/tree.d.ts +50 -0
- package/lib/src/tree.js +134 -0
- package/lib/src/values.d.ts +7 -0
- package/lib/src/values.js +37 -0
- package/lib/src/vendor.d.ts +19 -0
- package/lib/src/vendor.js +145 -0
- package/lib/src/watch.d.ts +44 -0
- package/lib/src/watch.js +137 -0
- package/lib/src/wbn.d.ts +5 -0
- package/lib/src/wbn.js +147 -0
- package/lib/src/yaml.d.ts +34 -0
- package/lib/src/yaml.js +562 -0
- package/package.json +58 -4
|
@@ -0,0 +1,676 @@
|
|
|
1
|
+
import { randomBytes } from 'node:crypto';
|
|
2
|
+
import { promises as fs } from 'node:fs';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { SitefileError } from './errors.js';
|
|
6
|
+
/**
|
|
7
|
+
* The temp directory starts empty and the tree has no two paths equal under
|
|
8
|
+
* case folding, so EEXIST/ENOTDIR/EISDIR here means the destination file
|
|
9
|
+
* system treats two different tree paths as one name (a folding or
|
|
10
|
+
* normalization rule wider than Sitefile's). Report the artifact path, not the
|
|
11
|
+
* temp path.
|
|
12
|
+
*/
|
|
13
|
+
export function occupied(e, p) {
|
|
14
|
+
const code = e?.code;
|
|
15
|
+
if (code !== 'EEXIST' && code !== 'ENOTDIR' && code !== 'EISDIR')
|
|
16
|
+
return e;
|
|
17
|
+
return new SitefileError('E_EXPORT', `cannot write '${p}': the destination file system already has an entry by that name (it folds case or normalizes Unicode more widely than Sitefile's checks)`, undefined, { cause: e });
|
|
18
|
+
}
|
|
19
|
+
/** Matches the exporter's temporary (`tmp`) and backup (`old`) sibling names. */
|
|
20
|
+
export const TEMP_PREFIX_PATTERN = /^\..+\.sitefile-(?:tmp|old)-[0-9a-f]+$/;
|
|
21
|
+
/** True if `name` is a temp/backup sibling the exporter creates for `outBasename`. */
|
|
22
|
+
export function isExporterTempName(name, outBasename) {
|
|
23
|
+
const tmp = `.${outBasename}.sitefile-tmp-`;
|
|
24
|
+
const old = `.${outBasename}.sitefile-old-`;
|
|
25
|
+
return (name.startsWith(tmp) || name.startsWith(old)) && TEMP_PREFIX_PATTERN.test(name);
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Materialize `tree` as a directory at `outDir`. Files are written into a
|
|
29
|
+
* fresh temporary sibling, which is then swapped in; on failure the previous
|
|
30
|
+
* destination (if any) is left in place.
|
|
31
|
+
*/
|
|
32
|
+
export async function exportDirectory(tree, outDir, options) {
|
|
33
|
+
const outAbs = path.resolve(outDir);
|
|
34
|
+
const outReal = await realpathMissing(outAbs);
|
|
35
|
+
const contextReal = await realpathOr(path.resolve(options.contextDir), 'build context');
|
|
36
|
+
// --- destination safety --------------------------------------------------
|
|
37
|
+
const home = await realpathOrRaw(os.homedir());
|
|
38
|
+
assertSafeDestination(outReal, contextReal, home);
|
|
39
|
+
const st = await lstatOrUndefined(outReal);
|
|
40
|
+
if (st?.isSymbolicLink()) {
|
|
41
|
+
// Never write through a final symlink; but if it points at something
|
|
42
|
+
// dangerous, say so.
|
|
43
|
+
const target = await fs.realpath(outReal).catch(() => undefined);
|
|
44
|
+
if (target !== undefined)
|
|
45
|
+
assertSafeDestination(target, contextReal, home);
|
|
46
|
+
}
|
|
47
|
+
if (st) {
|
|
48
|
+
if (st.isSymbolicLink()) {
|
|
49
|
+
throw new SitefileError('E_EXPORT', `export destination '${outReal}' is a symbolic link`);
|
|
50
|
+
}
|
|
51
|
+
if (!st.isDirectory()) {
|
|
52
|
+
throw new SitefileError('E_EXPORT', `export destination '${outReal}' exists and is not a directory`);
|
|
53
|
+
}
|
|
54
|
+
if (!isInside(outReal, contextReal) && !options.force) {
|
|
55
|
+
const entries = await fs.readdir(outReal);
|
|
56
|
+
if (entries.length > 0) {
|
|
57
|
+
throw new SitefileError('E_EXPORT_UNSAFE', `export destination '${outReal}' is outside the build context and not empty; pass --force to replace it`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
// --- validate paths before touching the disk ------------------------------
|
|
62
|
+
const entries = tree.entries();
|
|
63
|
+
const planned = [];
|
|
64
|
+
for (const [p, node] of entries) {
|
|
65
|
+
if (!p.startsWith('/'))
|
|
66
|
+
throw new SitefileError('E_EXPORT', `invalid artifact path '${p}': not absolute`);
|
|
67
|
+
const segments = p.slice(1).split('/');
|
|
68
|
+
for (const seg of segments) {
|
|
69
|
+
if (seg === '' || seg === '.' || seg === '..' || seg.includes('\0') || seg.includes('\\')) {
|
|
70
|
+
throw new SitefileError('E_EXPORT', `invalid artifact path '${p}'`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
planned.push({ path: p, segments, bytes: node.bytes, mode: node.mode, digest: node.digest });
|
|
74
|
+
}
|
|
75
|
+
// --- watch mode: apply only the changes when the destination is as we left it
|
|
76
|
+
const state = options.incremental;
|
|
77
|
+
if (state) {
|
|
78
|
+
const applied = await exportIncremental(planned, outReal, st, state, options);
|
|
79
|
+
if (applied)
|
|
80
|
+
return applied;
|
|
81
|
+
}
|
|
82
|
+
const stamps = state ? new Map() : undefined;
|
|
83
|
+
// --- write into a temp sibling, then swap ---------------------------------
|
|
84
|
+
//
|
|
85
|
+
// Race hardening (issue #5). Node has no openat/renameat2, so every check
|
|
86
|
+
// below is path-based and a hostile process with write access to the
|
|
87
|
+
// destination's parent can still win a window of a few syscalls. What we do
|
|
88
|
+
// is shrink that window and make it detectable:
|
|
89
|
+
// 1. capture dev/ino of the parent directory and of the existing `out`;
|
|
90
|
+
// 2. create tmp/old siblings only in that verified parent;
|
|
91
|
+
// 3. right before the swap, re-check the parent's real path + identity,
|
|
92
|
+
// and that `out` is still the same real directory (or still absent);
|
|
93
|
+
// 4. after moving `out` aside, confirm what we moved is what we validated;
|
|
94
|
+
// 5. after moving tmp into place, confirm `out` is the tree we wrote;
|
|
95
|
+
// 6. only ever recursively delete a path whose dev/ino we verified.
|
|
96
|
+
// Residual risk: (a) the two renames are not atomic together, so there is a
|
|
97
|
+
// moment where `out` does not exist; (b) between a check and the following
|
|
98
|
+
// rename an attacker can still swap a path component, which we only detect
|
|
99
|
+
// after the fact (steps 4-5) and never silently "fix"; (c) renaming onto an
|
|
100
|
+
// originally-absent `out` that an attacker just created as an empty
|
|
101
|
+
// directory succeeds on POSIX; (d) if the parent itself is moved away, the
|
|
102
|
+
// temp dir moves with it and cannot be located for cleanup.
|
|
103
|
+
const parent = path.dirname(outReal);
|
|
104
|
+
const base = path.basename(outReal);
|
|
105
|
+
const tmp = path.join(parent, `.${base}.sitefile-tmp-${randomHex()}`);
|
|
106
|
+
const old = path.join(parent, `.${base}.sitefile-old-${randomHex()}`);
|
|
107
|
+
try {
|
|
108
|
+
await fs.mkdir(parent, { recursive: true });
|
|
109
|
+
}
|
|
110
|
+
catch (e) {
|
|
111
|
+
throw new SitefileError('E_EXPORT', `cannot create '${parent}': ${e.message}`, undefined, { cause: e });
|
|
112
|
+
}
|
|
113
|
+
const parentId = await verifiedParentId(parent);
|
|
114
|
+
const outId = st ? idOf(st) : undefined;
|
|
115
|
+
try {
|
|
116
|
+
await fs.mkdir(tmp);
|
|
117
|
+
}
|
|
118
|
+
catch (e) {
|
|
119
|
+
throw new SitefileError('E_EXPORT', `cannot create '${tmp}': ${e.message}`, undefined, { cause: e });
|
|
120
|
+
}
|
|
121
|
+
let tmpId;
|
|
122
|
+
try {
|
|
123
|
+
const tst = await fs.lstat(tmp);
|
|
124
|
+
if (!tst.isDirectory() || tst.isSymbolicLink())
|
|
125
|
+
throw unsafe(`temporary directory '${tmp}' is not a real directory`);
|
|
126
|
+
tmpId = idOf(tst);
|
|
127
|
+
await assertParentUnchanged(parent, parentId);
|
|
128
|
+
}
|
|
129
|
+
catch (e) {
|
|
130
|
+
await removeIfSame(tmp, undefined);
|
|
131
|
+
throw e;
|
|
132
|
+
}
|
|
133
|
+
let oldMoved = false;
|
|
134
|
+
let swapped = false;
|
|
135
|
+
try {
|
|
136
|
+
for (const { path: p, segments, bytes, mode } of planned) {
|
|
137
|
+
const target = path.join(tmp, ...segments);
|
|
138
|
+
try {
|
|
139
|
+
await fs.mkdir(path.dirname(target), { recursive: true });
|
|
140
|
+
await fs.writeFile(target, bytes, { flag: 'wx' });
|
|
141
|
+
}
|
|
142
|
+
catch (e) {
|
|
143
|
+
throw occupied(e, p);
|
|
144
|
+
}
|
|
145
|
+
await fs.chmod(target, mode);
|
|
146
|
+
// Renaming the temp directory leaves its files' inodes (and ctimes) as they are now.
|
|
147
|
+
stamps?.set(p, stampOf(await fs.lstat(target)));
|
|
148
|
+
}
|
|
149
|
+
if (options.beforeSwap)
|
|
150
|
+
await options.beforeSwap();
|
|
151
|
+
// Re-validate immediately before the swap.
|
|
152
|
+
await assertParentUnchanged(parent, parentId);
|
|
153
|
+
await assertSameId(tmp, tmpId, 'temporary directory');
|
|
154
|
+
const cur = await lstatOrUndefined(outReal);
|
|
155
|
+
if (outId) {
|
|
156
|
+
if (!cur || cur.isSymbolicLink() || !cur.isDirectory() || !sameId(idOf(cur), outId)) {
|
|
157
|
+
throw unsafe(`export destination '${outReal}' changed during export; aborting`);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
else if (cur) {
|
|
161
|
+
throw unsafe(`export destination '${outReal}' appeared during export; aborting`);
|
|
162
|
+
}
|
|
163
|
+
if ((await lstatOrUndefined(old)) !== undefined)
|
|
164
|
+
throw unsafe(`backup path '${old}' already exists; aborting`);
|
|
165
|
+
if (outId) {
|
|
166
|
+
await fs.rename(outReal, old);
|
|
167
|
+
oldMoved = true;
|
|
168
|
+
// What we moved aside must be the directory we validated.
|
|
169
|
+
const moved = await lstatOrUndefined(old);
|
|
170
|
+
if (!moved || moved.isSymbolicLink() || !moved.isDirectory() || !sameId(idOf(moved), outId)) {
|
|
171
|
+
throw unsafe(`export destination '${outReal}' was replaced during the swap; aborting`);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
await fs.rename(tmp, outReal);
|
|
175
|
+
swapped = true;
|
|
176
|
+
const now = await lstatOrUndefined(outReal);
|
|
177
|
+
if (!now || now.isSymbolicLink() || !now.isDirectory() || !sameId(idOf(now), tmpId)) {
|
|
178
|
+
throw unsafe(`export destination '${outReal}' is not the directory that was written; ` +
|
|
179
|
+
(oldMoved ? `previous output preserved at '${old}'` : 'inspect it manually'));
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
catch (e) {
|
|
183
|
+
if (!swapped) {
|
|
184
|
+
await removeIfSame(tmp, tmpId);
|
|
185
|
+
if (oldMoved) {
|
|
186
|
+
// Roll back only if the slot is free and the backup is what we validated.
|
|
187
|
+
const slot = await lstatOrUndefined(outReal);
|
|
188
|
+
const bak = await lstatOrUndefined(old);
|
|
189
|
+
if (!slot && bak && outId && sameId(idOf(bak), outId))
|
|
190
|
+
await fs.rename(old, outReal).catch(() => { });
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
if (e instanceof SitefileError)
|
|
194
|
+
throw e;
|
|
195
|
+
throw new SitefileError('E_EXPORT', `failed to export to '${outReal}': ${e.message}`, undefined, {
|
|
196
|
+
cause: e,
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
if (oldMoved) {
|
|
200
|
+
// Best effort: the new output is already in place. Only delete the exact
|
|
201
|
+
// directory we validated and moved.
|
|
202
|
+
await removeIfSame(old, outId);
|
|
203
|
+
}
|
|
204
|
+
if (state && stamps) {
|
|
205
|
+
const files = new Map();
|
|
206
|
+
for (const e of planned)
|
|
207
|
+
files.set(e.path, { digest: e.digest, mode: e.mode, stamp: stamps.get(e.path) });
|
|
208
|
+
state.snapshot = { outReal, outId: tmpId, parentId, files, dirs: dirsOf(planned) };
|
|
209
|
+
}
|
|
210
|
+
return { outDir: outReal, files: planned.length };
|
|
211
|
+
}
|
|
212
|
+
// --- incremental export (watch mode) ---------------------------------------
|
|
213
|
+
//
|
|
214
|
+
// A watch session keeps an ExportState: the destination's identity and, for every file it
|
|
215
|
+
// exported, the tree digest and mode plus the file's lstat stamp (dev, ino, size, mtime,
|
|
216
|
+
// ctime, permission bits) as it was right after Sitefile wrote it. The next export of the
|
|
217
|
+
// session first checks that the destination still holds exactly that: the same real
|
|
218
|
+
// directory and parent (dev/ino), every recorded directory and file and nothing else (one
|
|
219
|
+
// readdir per directory), each file with its recorded stamp (one lstat per file; ctime
|
|
220
|
+
// changes on any write, chmod or rename, even when mtime is restored). If so, it writes only
|
|
221
|
+
// the files whose digest or mode changed and removes the files and directories that are
|
|
222
|
+
// gone; otherwise (nothing recorded, the destination moved or was modified, an entry that
|
|
223
|
+
// would collide on a case-folding or normalizing file system, or more than half of the files
|
|
224
|
+
// change) it falls back to the full write-and-swap above.
|
|
225
|
+
//
|
|
226
|
+
// Applying: the changed files are written to a temporary sibling first (the slow part, the
|
|
227
|
+
// destination untouched); then deleted files are unlinked, emptied directories removed
|
|
228
|
+
// (deepest first), new directories created (shallowest first) and each staged file renamed
|
|
229
|
+
// over its path, every step re-checking that what it replaces is the file it recorded. Each
|
|
230
|
+
// file is therefore always either its old or its new version (rename is atomic), and the
|
|
231
|
+
// destination holds a mix of two builds only while those renames run. If a step fails, the
|
|
232
|
+
// full swap runs, which replaces the whole destination atomically.
|
|
233
|
+
/** What a watch session's last directory export left on disk; see {@link ExportOptions.incremental}. */
|
|
234
|
+
export class ExportState {
|
|
235
|
+
/** @internal */
|
|
236
|
+
snapshot;
|
|
237
|
+
/** Forget the last export: the next one rewrites everything. */
|
|
238
|
+
clear() {
|
|
239
|
+
this.snapshot = undefined;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
function stampOf(st) {
|
|
243
|
+
return { dev: st.dev, ino: st.ino, size: st.size, mtimeMs: st.mtimeMs, ctimeMs: st.ctimeMs, mode: st.mode };
|
|
244
|
+
}
|
|
245
|
+
function sameStamp(st, r) {
|
|
246
|
+
return (st !== undefined &&
|
|
247
|
+
st.isFile() &&
|
|
248
|
+
st.dev === r.dev &&
|
|
249
|
+
st.ino === r.ino &&
|
|
250
|
+
st.size === r.size &&
|
|
251
|
+
st.mtimeMs === r.mtimeMs &&
|
|
252
|
+
st.ctimeMs === r.ctimeMs &&
|
|
253
|
+
st.mode === r.mode);
|
|
254
|
+
}
|
|
255
|
+
/** Every directory that holds a file of `files`, below the root. */
|
|
256
|
+
function dirsOf(files) {
|
|
257
|
+
const dirs = new Set();
|
|
258
|
+
for (const { path: p } of files) {
|
|
259
|
+
for (let i = p.indexOf('/', 1); i !== -1; i = p.indexOf('/', i + 1))
|
|
260
|
+
dirs.add(p.slice(0, i));
|
|
261
|
+
}
|
|
262
|
+
return dirs;
|
|
263
|
+
}
|
|
264
|
+
const depth = (p) => p.split('/').length;
|
|
265
|
+
/** `fn` over `items`, at most `limit` at a time. */
|
|
266
|
+
async function eachLimit(items, limit, fn) {
|
|
267
|
+
let next = 0;
|
|
268
|
+
const worker = async () => {
|
|
269
|
+
while (next < items.length)
|
|
270
|
+
await fn(items[next++]);
|
|
271
|
+
};
|
|
272
|
+
await Promise.all(Array.from({ length: Math.min(limit, items.length) }, worker));
|
|
273
|
+
}
|
|
274
|
+
/** True if `outReal` holds exactly the recorded directories and files, each file as recorded. */
|
|
275
|
+
async function matchesSnapshot(outReal, snap) {
|
|
276
|
+
const files = [];
|
|
277
|
+
let dirs = 0;
|
|
278
|
+
const pending = [''];
|
|
279
|
+
while (pending.length) {
|
|
280
|
+
const rel = pending.pop();
|
|
281
|
+
for (const e of await fs.readdir(path.join(outReal, rel), { withFileTypes: true })) {
|
|
282
|
+
const p = `${rel}/${e.name}`;
|
|
283
|
+
if (e.isDirectory()) {
|
|
284
|
+
if (!snap.dirs.has(p))
|
|
285
|
+
return false;
|
|
286
|
+
dirs++;
|
|
287
|
+
pending.push(p);
|
|
288
|
+
}
|
|
289
|
+
else if (e.isFile()) {
|
|
290
|
+
const r = snap.files.get(p);
|
|
291
|
+
if (!r)
|
|
292
|
+
return false;
|
|
293
|
+
files.push([p, r.stamp]);
|
|
294
|
+
}
|
|
295
|
+
else {
|
|
296
|
+
return false; // a symlink, socket, ...: never ours
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
if (dirs !== snap.dirs.size || files.length !== snap.files.size)
|
|
301
|
+
return false;
|
|
302
|
+
let ok = true;
|
|
303
|
+
await eachLimit(files, 64, async ([p, stamp]) => {
|
|
304
|
+
if (ok && !sameStamp(await lstatOrUndefined(path.join(outReal, p)), stamp))
|
|
305
|
+
ok = false;
|
|
306
|
+
});
|
|
307
|
+
return ok;
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* Apply only the changes since the recorded export (see the comment above). Returns
|
|
311
|
+
* undefined to ask for the full swap; the destination is then either untouched or, if
|
|
312
|
+
* applying failed part way, about to be replaced whole.
|
|
313
|
+
*/
|
|
314
|
+
async function exportIncremental(planned, outReal, st, state, options) {
|
|
315
|
+
const snap = state.snapshot;
|
|
316
|
+
state.snapshot = undefined; // recorded again only after a successful export
|
|
317
|
+
if (!snap || !st || snap.outReal !== outReal || st.isSymbolicLink() || !st.isDirectory() || !sameId(idOf(st), snap.outId))
|
|
318
|
+
return undefined;
|
|
319
|
+
const next = new Set(planned.map((e) => e.path));
|
|
320
|
+
const writes = planned.filter((e) => {
|
|
321
|
+
const r = snap.files.get(e.path);
|
|
322
|
+
return !r || r.digest !== e.digest || r.mode !== e.mode;
|
|
323
|
+
});
|
|
324
|
+
const deletes = [...snap.files.keys()].filter((p) => !next.has(p));
|
|
325
|
+
if ((writes.length + deletes.length) * 2 > Math.max(planned.length, snap.files.size))
|
|
326
|
+
return undefined;
|
|
327
|
+
const nextDirs = dirsOf(planned);
|
|
328
|
+
const removeDirs = [...snap.dirs].filter((d) => !nextDirs.has(d)).sort((a, b) => depth(b) - depth(a));
|
|
329
|
+
const makeDirs = [...nextDirs].filter((d) => !snap.dirs.has(d)).sort((a, b) => depth(a) - depth(b));
|
|
330
|
+
const parent = path.dirname(outReal);
|
|
331
|
+
const at = (p) => path.join(outReal, ...p.slice(1).split('/'));
|
|
332
|
+
try {
|
|
333
|
+
await assertParentUnchanged(parent, snap.parentId);
|
|
334
|
+
if (!(await matchesSnapshot(outReal, snap)))
|
|
335
|
+
return undefined;
|
|
336
|
+
// A new path must not exist yet, except as the very file or directory it replaces;
|
|
337
|
+
// anything else is another spelling of an existing entry (case folding, Unicode
|
|
338
|
+
// normalization): the full export reports that as a clean build does.
|
|
339
|
+
const going = new Set([...deletes, ...removeDirs]);
|
|
340
|
+
for (const p of [...writes.filter((e) => !snap.files.has(e.path)).map((e) => e.path), ...makeDirs]) {
|
|
341
|
+
if ((await lstatOrUndefined(at(p))) !== undefined && !going.has(p))
|
|
342
|
+
return undefined;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
catch {
|
|
346
|
+
return undefined;
|
|
347
|
+
}
|
|
348
|
+
// Stage the new contents next to the destination.
|
|
349
|
+
const tmp = path.join(parent, `.${path.basename(outReal)}.sitefile-tmp-${randomHex()}`);
|
|
350
|
+
let tmpId;
|
|
351
|
+
try {
|
|
352
|
+
await fs.mkdir(tmp);
|
|
353
|
+
const tst = await fs.lstat(tmp);
|
|
354
|
+
if (!tst.isDirectory() || tst.isSymbolicLink())
|
|
355
|
+
return undefined;
|
|
356
|
+
tmpId = idOf(tst);
|
|
357
|
+
}
|
|
358
|
+
catch {
|
|
359
|
+
await removeIfSame(tmp, undefined);
|
|
360
|
+
return undefined;
|
|
361
|
+
}
|
|
362
|
+
const staged = [];
|
|
363
|
+
try {
|
|
364
|
+
for (const e of writes) {
|
|
365
|
+
const file = path.join(tmp, String(staged.length));
|
|
366
|
+
await fs.writeFile(file, e.bytes, { flag: 'wx' });
|
|
367
|
+
await fs.chmod(file, e.mode);
|
|
368
|
+
staged.push({ e, file, id: idOf(await fs.lstat(file)) });
|
|
369
|
+
}
|
|
370
|
+
if (options.beforeApply)
|
|
371
|
+
await options.beforeApply();
|
|
372
|
+
}
|
|
373
|
+
catch {
|
|
374
|
+
await removeIfSame(tmp, tmpId);
|
|
375
|
+
return undefined;
|
|
376
|
+
}
|
|
377
|
+
// Apply.
|
|
378
|
+
const files = new Map(snap.files);
|
|
379
|
+
try {
|
|
380
|
+
await assertParentUnchanged(parent, snap.parentId);
|
|
381
|
+
await assertSameId(outReal, snap.outId, 'export destination');
|
|
382
|
+
for (const p of deletes) {
|
|
383
|
+
const t = at(p);
|
|
384
|
+
if (!sameStamp(await lstatOrUndefined(t), snap.files.get(p).stamp))
|
|
385
|
+
throw unsafe(`'${t}' changed during export`);
|
|
386
|
+
await fs.unlink(t);
|
|
387
|
+
files.delete(p);
|
|
388
|
+
}
|
|
389
|
+
for (const d of removeDirs)
|
|
390
|
+
await fs.rmdir(at(d)); // fails unless emptied
|
|
391
|
+
for (const d of makeDirs) {
|
|
392
|
+
await fs.mkdir(at(d));
|
|
393
|
+
const ds = await fs.lstat(at(d));
|
|
394
|
+
if (ds.isSymbolicLink() || !ds.isDirectory())
|
|
395
|
+
throw unsafe(`'${at(d)}' is not a directory`);
|
|
396
|
+
}
|
|
397
|
+
for (const { e, file, id } of staged) {
|
|
398
|
+
const t = at(e.path);
|
|
399
|
+
const cur = await lstatOrUndefined(t);
|
|
400
|
+
const r = snap.files.get(e.path);
|
|
401
|
+
if (r ? !sameStamp(cur, r.stamp) : cur !== undefined)
|
|
402
|
+
throw unsafe(`'${t}' changed during export`);
|
|
403
|
+
await fs.rename(file, t);
|
|
404
|
+
const now = await fs.lstat(t);
|
|
405
|
+
if (now.isSymbolicLink() || !now.isFile() || !sameId(idOf(now), id))
|
|
406
|
+
throw unsafe(`'${t}' is not the file that was written`);
|
|
407
|
+
files.set(e.path, { digest: e.digest, mode: e.mode, stamp: stampOf(now) });
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
catch {
|
|
411
|
+
await removeIfSame(tmp, tmpId);
|
|
412
|
+
return undefined; // the full swap replaces whatever was applied
|
|
413
|
+
}
|
|
414
|
+
await removeIfSame(tmp, tmpId);
|
|
415
|
+
state.snapshot = { outReal, outId: snap.outId, parentId: snap.parentId, files, dirs: nextDirs };
|
|
416
|
+
return { outDir: outReal, files: planned.length, incremental: { written: writes.length, removed: deletes.length } };
|
|
417
|
+
}
|
|
418
|
+
/**
|
|
419
|
+
* Write `data` as a single file at `outFile` (zip, tar, tgz, wbn). Same
|
|
420
|
+
* destination rules as {@link exportDirectory}: never the filesystem root, the
|
|
421
|
+
* home directory, the build context or a parent of it; never through a symlink;
|
|
422
|
+
* a directory is refused; an existing file outside the context needs
|
|
423
|
+
* `force`. The bytes go to a temporary sibling that is renamed into place after
|
|
424
|
+
* the parent and destination are re-validated; only that exact temp file (by
|
|
425
|
+
* dev/ino) is ever deleted, and nothing already at `outFile` is removed except
|
|
426
|
+
* by the replacing rename.
|
|
427
|
+
*/
|
|
428
|
+
export async function exportFile(data, outFile, options) {
|
|
429
|
+
const outAbs = path.resolve(outFile);
|
|
430
|
+
const outReal = await realpathMissing(outAbs);
|
|
431
|
+
const contextReal = await realpathOr(path.resolve(options.contextDir), 'build context');
|
|
432
|
+
const home = await realpathOrRaw(os.homedir());
|
|
433
|
+
assertSafeDestination(outReal, contextReal, home);
|
|
434
|
+
const st = await lstatOrUndefined(outReal);
|
|
435
|
+
if (st?.isSymbolicLink()) {
|
|
436
|
+
const target = await fs.realpath(outReal).catch(() => undefined);
|
|
437
|
+
if (target !== undefined)
|
|
438
|
+
assertSafeDestination(target, contextReal, home);
|
|
439
|
+
throw new SitefileError('E_EXPORT', `export destination '${outReal}' is a symbolic link`);
|
|
440
|
+
}
|
|
441
|
+
if (st) {
|
|
442
|
+
if (st.isDirectory()) {
|
|
443
|
+
throw new SitefileError('E_EXPORT', `export destination '${outReal}' is a directory; an archive export needs a file path`);
|
|
444
|
+
}
|
|
445
|
+
if (!st.isFile()) {
|
|
446
|
+
throw new SitefileError('E_EXPORT', `export destination '${outReal}' exists and is not a regular file`);
|
|
447
|
+
}
|
|
448
|
+
if (!isInside(outReal, contextReal) && !options.force) {
|
|
449
|
+
throw new SitefileError('E_EXPORT_UNSAFE', `export destination '${outReal}' already exists outside the build context; pass --force to replace it`);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
const parent = path.dirname(outReal);
|
|
453
|
+
const base = path.basename(outReal);
|
|
454
|
+
const tmp = path.join(parent, `.${base}.sitefile-tmp-${randomHex()}`);
|
|
455
|
+
try {
|
|
456
|
+
await fs.mkdir(parent, { recursive: true });
|
|
457
|
+
}
|
|
458
|
+
catch (e) {
|
|
459
|
+
throw new SitefileError('E_EXPORT', `cannot create '${parent}': ${e.message}`, undefined, { cause: e });
|
|
460
|
+
}
|
|
461
|
+
const parentId = await verifiedParentId(parent);
|
|
462
|
+
const outId = st ? idOf(st) : undefined;
|
|
463
|
+
// dev/ino alone can't tell a replaced file from the original: Linux often gives a file
|
|
464
|
+
// created right after an unlink the same inode number. Size and the nanosecond ctime
|
|
465
|
+
// can't survive a replacement (or any write) unchanged.
|
|
466
|
+
const outStamp = st ? await replacementStampOf(outReal) : undefined;
|
|
467
|
+
let tmpId;
|
|
468
|
+
let renamed = false;
|
|
469
|
+
try {
|
|
470
|
+
const fh = await fs.open(tmp, 'wx', 0o644);
|
|
471
|
+
try {
|
|
472
|
+
tmpId = idOf(await fh.stat());
|
|
473
|
+
await fh.writeFile(data);
|
|
474
|
+
await fh.chmod(0o644);
|
|
475
|
+
}
|
|
476
|
+
finally {
|
|
477
|
+
await fh.close();
|
|
478
|
+
}
|
|
479
|
+
if (options.beforeSwap)
|
|
480
|
+
await options.beforeSwap();
|
|
481
|
+
await assertParentUnchanged(parent, parentId);
|
|
482
|
+
const t = await lstatOrUndefined(tmp);
|
|
483
|
+
if (!t || t.isSymbolicLink() || !t.isFile() || !sameId(idOf(t), tmpId)) {
|
|
484
|
+
throw unsafe(`temporary file '${tmp}' changed during export; aborting`);
|
|
485
|
+
}
|
|
486
|
+
const cur = await lstatOrUndefined(outReal);
|
|
487
|
+
if (outId) {
|
|
488
|
+
if (!cur ||
|
|
489
|
+
cur.isSymbolicLink() ||
|
|
490
|
+
!cur.isFile() ||
|
|
491
|
+
!sameId(idOf(cur), outId) ||
|
|
492
|
+
!sameReplacementStamp(await replacementStampOf(outReal), outStamp)) {
|
|
493
|
+
throw unsafe(`export destination '${outReal}' changed during export; aborting`);
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
else if (cur) {
|
|
497
|
+
throw unsafe(`export destination '${outReal}' appeared during export; aborting`);
|
|
498
|
+
}
|
|
499
|
+
await fs.rename(tmp, outReal);
|
|
500
|
+
renamed = true;
|
|
501
|
+
const now = await lstatOrUndefined(outReal);
|
|
502
|
+
if (!now || now.isSymbolicLink() || !now.isFile() || !sameId(idOf(now), tmpId)) {
|
|
503
|
+
throw unsafe(`export destination '${outReal}' is not the file that was written; inspect it manually`);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
catch (e) {
|
|
507
|
+
if (!renamed && tmpId)
|
|
508
|
+
await removeFileIfSame(tmp, tmpId);
|
|
509
|
+
if (e instanceof SitefileError)
|
|
510
|
+
throw e;
|
|
511
|
+
throw new SitefileError('E_EXPORT', `failed to export to '${outReal}': ${e.message}`, undefined, { cause: e });
|
|
512
|
+
}
|
|
513
|
+
return { outFile: outReal, bytes: data.byteLength };
|
|
514
|
+
}
|
|
515
|
+
async function replacementStampOf(p) {
|
|
516
|
+
try {
|
|
517
|
+
const st = await fs.lstat(p, { bigint: true });
|
|
518
|
+
return { size: st.size, ctimeNs: st.ctimeNs };
|
|
519
|
+
}
|
|
520
|
+
catch {
|
|
521
|
+
return undefined;
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
function sameReplacementStamp(a, b) {
|
|
525
|
+
return a !== undefined && b !== undefined && a.size === b.size && a.ctimeNs === b.ctimeNs;
|
|
526
|
+
}
|
|
527
|
+
/** Remove `p` only if it is a regular file with the identity we created. */
|
|
528
|
+
async function removeFileIfSame(p, expected) {
|
|
529
|
+
try {
|
|
530
|
+
const st = await lstatOrUndefined(p);
|
|
531
|
+
if (!st || st.isSymbolicLink() || !st.isFile() || !sameId(idOf(st), expected))
|
|
532
|
+
return;
|
|
533
|
+
await fs.unlink(p);
|
|
534
|
+
}
|
|
535
|
+
catch {
|
|
536
|
+
// best effort
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
function idOf(st) {
|
|
540
|
+
return { dev: st.dev, ino: st.ino };
|
|
541
|
+
}
|
|
542
|
+
function sameId(a, b) {
|
|
543
|
+
return a.dev === b.dev && a.ino === b.ino;
|
|
544
|
+
}
|
|
545
|
+
function unsafe(message) {
|
|
546
|
+
return new SitefileError('E_EXPORT_UNSAFE', message);
|
|
547
|
+
}
|
|
548
|
+
/** The parent must be a real directory (no symlink in its path); return its identity. */
|
|
549
|
+
async function verifiedParentId(parent) {
|
|
550
|
+
const real = await fs.realpath(parent);
|
|
551
|
+
if (real !== parent)
|
|
552
|
+
throw unsafe(`export parent '${parent}' resolves to '${real}'; aborting`);
|
|
553
|
+
const st = await fs.stat(parent);
|
|
554
|
+
if (!st.isDirectory())
|
|
555
|
+
throw unsafe(`export parent '${parent}' is not a directory`);
|
|
556
|
+
return idOf(st);
|
|
557
|
+
}
|
|
558
|
+
async function assertParentUnchanged(parent, expected) {
|
|
559
|
+
let now;
|
|
560
|
+
try {
|
|
561
|
+
now = await verifiedParentId(parent);
|
|
562
|
+
}
|
|
563
|
+
catch (e) {
|
|
564
|
+
if (e instanceof SitefileError)
|
|
565
|
+
throw e;
|
|
566
|
+
throw unsafe(`export parent '${parent}' changed during export: ${e.message}`);
|
|
567
|
+
}
|
|
568
|
+
if (!sameId(now, expected))
|
|
569
|
+
throw unsafe(`export parent '${parent}' was replaced during export; aborting`);
|
|
570
|
+
}
|
|
571
|
+
async function assertSameId(p, expected, what) {
|
|
572
|
+
const st = await lstatOrUndefined(p);
|
|
573
|
+
if (!st || st.isSymbolicLink() || !st.isDirectory() || !sameId(idOf(st), expected)) {
|
|
574
|
+
throw unsafe(`${what} '${p}' changed during export; aborting`);
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
/**
|
|
578
|
+
* Recursively remove `p` only if it is a real directory whose dev/ino equals
|
|
579
|
+
* `expected`. With no `expected` (freshly created, still empty), only an empty
|
|
580
|
+
* real directory is removed. Never follows or deletes through a symlink.
|
|
581
|
+
*/
|
|
582
|
+
async function removeIfSame(p, expected) {
|
|
583
|
+
try {
|
|
584
|
+
const st = await lstatOrUndefined(p);
|
|
585
|
+
if (!st || st.isSymbolicLink() || !st.isDirectory())
|
|
586
|
+
return;
|
|
587
|
+
if (expected === undefined) {
|
|
588
|
+
await fs.rmdir(p);
|
|
589
|
+
return;
|
|
590
|
+
}
|
|
591
|
+
if (!sameId(idOf(st), expected))
|
|
592
|
+
return;
|
|
593
|
+
await fs.rm(p, { recursive: true, force: true });
|
|
594
|
+
}
|
|
595
|
+
catch {
|
|
596
|
+
// best effort
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
function assertSafeDestination(out, contextReal, home) {
|
|
600
|
+
if (out === path.parse(out).root) {
|
|
601
|
+
throw new SitefileError('E_EXPORT_UNSAFE', `refusing to export to the filesystem root '${out}'`);
|
|
602
|
+
}
|
|
603
|
+
if (out === contextReal) {
|
|
604
|
+
throw new SitefileError('E_EXPORT_UNSAFE', `refusing to export to the build context itself ('${out}')`);
|
|
605
|
+
}
|
|
606
|
+
if (isInside(contextReal, out)) {
|
|
607
|
+
throw new SitefileError('E_EXPORT_UNSAFE', `refusing to export to '${out}': it contains the build context '${contextReal}'`);
|
|
608
|
+
}
|
|
609
|
+
if (out === home || isInside(home, out)) {
|
|
610
|
+
throw new SitefileError('E_EXPORT_UNSAFE', `refusing to export to '${out}': it is or contains the home directory`);
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
function randomHex() {
|
|
614
|
+
return randomBytes(6).toString('hex');
|
|
615
|
+
}
|
|
616
|
+
/** True if `child` is strictly inside `dir`. */
|
|
617
|
+
function isInside(child, dir) {
|
|
618
|
+
const rel = path.relative(dir, child);
|
|
619
|
+
return rel !== '' && !rel.startsWith('..') && !path.isAbsolute(rel);
|
|
620
|
+
}
|
|
621
|
+
async function lstatOrUndefined(p) {
|
|
622
|
+
try {
|
|
623
|
+
return await fs.lstat(p);
|
|
624
|
+
}
|
|
625
|
+
catch (e) {
|
|
626
|
+
const code = e.code;
|
|
627
|
+
if (code === 'ENOENT' || code === 'ENOTDIR')
|
|
628
|
+
return undefined;
|
|
629
|
+
throw e;
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
/**
|
|
633
|
+
* Resolve the real location of `abs` without following a final symlink:
|
|
634
|
+
* realpath the nearest existing ancestor of its parent and rejoin the rest.
|
|
635
|
+
*/
|
|
636
|
+
async function realpathMissing(abs) {
|
|
637
|
+
const root = path.parse(abs).root;
|
|
638
|
+
if (abs === root)
|
|
639
|
+
return abs;
|
|
640
|
+
const rest = [path.basename(abs)];
|
|
641
|
+
let dir = path.dirname(abs);
|
|
642
|
+
for (;;) {
|
|
643
|
+
try {
|
|
644
|
+
const real = await fs.realpath(dir);
|
|
645
|
+
return path.join(real, ...rest);
|
|
646
|
+
}
|
|
647
|
+
catch (e) {
|
|
648
|
+
const code = e.code;
|
|
649
|
+
if (code !== 'ENOENT' && code !== 'ENOTDIR')
|
|
650
|
+
throw e;
|
|
651
|
+
if (dir === root)
|
|
652
|
+
return path.join(root, ...rest);
|
|
653
|
+
rest.unshift(path.basename(dir));
|
|
654
|
+
dir = path.dirname(dir);
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
async function realpathOr(p, what) {
|
|
659
|
+
try {
|
|
660
|
+
return await fs.realpath(p);
|
|
661
|
+
}
|
|
662
|
+
catch (e) {
|
|
663
|
+
throw new SitefileError('E_EXPORT', `cannot resolve ${what} '${p}': ${e.message}`, undefined, {
|
|
664
|
+
cause: e,
|
|
665
|
+
});
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
async function realpathOrRaw(p) {
|
|
669
|
+
try {
|
|
670
|
+
return await fs.realpath(p);
|
|
671
|
+
}
|
|
672
|
+
catch {
|
|
673
|
+
return path.resolve(p);
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
//# sourceMappingURL=exporter.js.map
|