@veryfront/ext-content-mdx 0.1.1190 → 0.1.1192
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/esm/deno.d.ts +6 -0
- package/esm/deno.js +13 -7
- package/esm/src/errors/error-registry/module.d.ts +4 -0
- package/esm/src/errors/error-registry/module.d.ts.map +1 -1
- package/esm/src/errors/error-registry/module.js +16 -0
- package/esm/src/errors/error-registry.d.ts +2 -0
- package/esm/src/errors/error-registry.d.ts.map +1 -1
- package/esm/src/errors/types.d.ts.map +1 -1
- package/esm/src/errors/types.js +5 -2
- package/esm/src/platform/compat/error-introspection.d.ts +12 -1
- package/esm/src/platform/compat/error-introspection.d.ts.map +1 -1
- package/esm/src/platform/compat/error-introspection.js +40 -5
- package/esm/src/platform/compat/path/basic-operations.d.ts.map +1 -1
- package/esm/src/platform/compat/path/basic-operations.js +6 -2
- package/esm/src/platform/compat/path/portable.d.ts.map +1 -1
- package/esm/src/platform/compat/path/portable.js +98 -54
- package/esm/src/platform/compat/path/resolution.d.ts.map +1 -1
- package/esm/src/platform/compat/path/resolution.js +14 -5
- package/esm/src/platform/compat/primordials/array.d.ts +8 -0
- package/esm/src/platform/compat/primordials/array.d.ts.map +1 -0
- package/esm/src/platform/compat/primordials/array.js +33 -0
- package/esm/src/transforms/import-rewriter/url-builder.d.ts +6 -0
- package/esm/src/transforms/import-rewriter/url-builder.d.ts.map +1 -1
- package/esm/src/transforms/import-rewriter/url-builder.js +96 -29
- package/esm/src/utils/base64url.d.ts.map +1 -1
- package/esm/src/utils/base64url.js +10 -4
- package/esm/src/utils/bundle-manifest.d.ts +19 -1
- package/esm/src/utils/bundle-manifest.d.ts.map +1 -1
- package/esm/src/utils/bundle-manifest.js +124 -27
- package/esm/src/utils/constants/limits.d.ts +1 -1
- package/esm/src/utils/constants/limits.d.ts.map +1 -1
- package/esm/src/utils/constants/limits.js +1 -1
- package/esm/src/utils/hash-utils.d.ts.map +1 -1
- package/esm/src/utils/hash-utils.js +32 -6
- package/esm/src/utils/id.d.ts +16 -0
- package/esm/src/utils/id.d.ts.map +1 -0
- package/esm/src/utils/id.js +69 -0
- package/esm/src/utils/import-lockfile.d.ts +52 -1
- package/esm/src/utils/import-lockfile.d.ts.map +1 -1
- package/esm/src/utils/import-lockfile.js +1042 -58
- package/esm/src/utils/index.d.ts +1 -1
- package/esm/src/utils/index.d.ts.map +1 -1
- package/esm/src/utils/index.js +1 -1
- package/esm/src/utils/logger/logger.d.ts +1 -0
- package/esm/src/utils/logger/logger.d.ts.map +1 -1
- package/esm/src/utils/logger/logger.js +359 -78
- package/esm/src/utils/logger/redact.d.ts +4 -2
- package/esm/src/utils/logger/redact.d.ts.map +1 -1
- package/esm/src/utils/logger/redact.js +300 -112
- package/esm/src/utils/logger/serialization.d.ts +3 -0
- package/esm/src/utils/logger/serialization.d.ts.map +1 -0
- package/esm/src/utils/logger/serialization.js +81 -0
- package/esm/src/utils/memoize.d.ts +1 -1
- package/esm/src/utils/memoize.d.ts.map +1 -1
- package/esm/src/utils/memoize.js +16 -5
- package/esm/src/utils/path-utils.d.ts.map +1 -1
- package/esm/src/utils/path-utils.js +4 -2
- package/esm/src/utils/sleep.d.ts +8 -0
- package/esm/src/utils/sleep.d.ts.map +1 -1
- package/esm/src/utils/sleep.js +11 -2
- package/esm/src/utils/timer.d.ts +10 -0
- package/esm/src/utils/timer.d.ts.map +1 -0
- package/esm/src/utils/timer.js +19 -0
- package/esm/src/utils/version-constant.d.ts +1 -1
- package/esm/src/utils/version-constant.js +1 -1
- package/package.json +2 -2
|
@@ -1,12 +1,340 @@
|
|
|
1
|
+
import * as dntShim from "../../_dnt.shims.js";
|
|
1
2
|
import { computeHash } from "./hash-utils.js";
|
|
3
|
+
import { generateUuid } from "./id.js";
|
|
2
4
|
import { serverLogger } from "./logger/index.js";
|
|
3
|
-
import { createFileSystem } from "../platform/compat/fs.js";
|
|
4
|
-
import {
|
|
5
|
+
import { createFileSystem, realPath as resolvePlatformRealPath, } from "../platform/compat/fs.js";
|
|
6
|
+
import { normalize } from "../platform/compat/path/resolution.js";
|
|
7
|
+
import { CACHE_ERROR, INVALID_ARGUMENT, LOCKFILE_FORMAT_MISMATCH, LOCKFILE_READ_ERROR, NETWORK_ERROR, } from "../errors/error-registry.js";
|
|
8
|
+
import { snapshotVeryfrontError } from "../errors/types.js";
|
|
5
9
|
const logger = serverLogger.component("lockfile");
|
|
6
10
|
const LOCKFILE_NAME = "veryfront.lock";
|
|
7
11
|
const LOCKFILE_VERSION = 1;
|
|
12
|
+
const LOCKFILE_CANONICALIZATION_TIMEOUT_MS = 1_000;
|
|
13
|
+
// Canonicalization failures are exceptional and must not create an unbounded
|
|
14
|
+
// process-wide journal. The limit applies only while a shared backing store
|
|
15
|
+
// cannot establish stable identities; healthy paths reconcile and release
|
|
16
|
+
// records as normal.
|
|
17
|
+
const MAX_UNRESOLVED_LOCKFILE_RECORDS = 1_024;
|
|
18
|
+
// Historical aliases are opportunistic. Never let an old or deleted project
|
|
19
|
+
// hold the shared filesystem queue for the normal one-second path timeout.
|
|
20
|
+
const HISTORICAL_LOCKFILE_CANONICALIZATION_TIMEOUT_MS = 25;
|
|
21
|
+
const MAX_HISTORICAL_RECONCILIATIONS_PER_ACCESS = 8;
|
|
22
|
+
const MAX_HISTORICAL_CANONICALIZATION_ATTEMPTS = 8;
|
|
23
|
+
const HISTORICAL_CANONICALIZATION_RETRY_BASE_MS = 250;
|
|
24
|
+
const HISTORICAL_CANONICALIZATION_RETRY_MAX_MS = 30_000;
|
|
25
|
+
const apply = Reflect.apply;
|
|
26
|
+
const NativeError = Error;
|
|
27
|
+
const NativeFinalizationRegistry = FinalizationRegistry;
|
|
28
|
+
const NativeJSON = JSON;
|
|
29
|
+
const NativeMap = Map;
|
|
30
|
+
const NativePromise = Promise;
|
|
31
|
+
const NativeURL = URL;
|
|
32
|
+
const NativeWeakMap = WeakMap;
|
|
33
|
+
const NativeWeakRef = WeakRef;
|
|
34
|
+
const arrayIsArray = Array.isArray;
|
|
35
|
+
const arraySort = Array.prototype.sort;
|
|
36
|
+
const finalizationRegistryRegister = FinalizationRegistry.prototype.register;
|
|
37
|
+
const finalizationRegistryUnregister = FinalizationRegistry.prototype.unregister;
|
|
38
|
+
const mapClear = Map.prototype.clear;
|
|
39
|
+
const mapDelete = Map.prototype.delete;
|
|
40
|
+
const mapEntries = Map.prototype.entries;
|
|
41
|
+
const mapGet = Map.prototype.get;
|
|
42
|
+
const mapSet = Map.prototype.set;
|
|
43
|
+
const jsonParse = JSON.parse;
|
|
44
|
+
const jsonStringify = JSON.stringify;
|
|
45
|
+
const nativeClearTimeout = clearTimeout;
|
|
46
|
+
const nativeSetTimeout = dntShim.setTimeout;
|
|
47
|
+
const numberIsSafeInteger = Number.isSafeInteger;
|
|
48
|
+
const objectCreate = Object.create;
|
|
49
|
+
const objectDefineProperty = Object.defineProperty;
|
|
50
|
+
const objectEntries = Object.entries;
|
|
51
|
+
const objectGetOwnPropertyDescriptor = Object.getOwnPropertyDescriptor;
|
|
52
|
+
const objectGetPrototypeOf = Object.getPrototypeOf;
|
|
53
|
+
const objectHasOwn = Object.hasOwn;
|
|
54
|
+
const objectKeys = Object.keys;
|
|
55
|
+
const mapIteratorNext = objectGetPrototypeOf(new NativeMap().entries()).next;
|
|
56
|
+
const mapSizeGetter = objectGetOwnPropertyDescriptor(Map.prototype, "size").get;
|
|
57
|
+
const promiseResolve = Promise.resolve;
|
|
58
|
+
const promiseReject = Promise.reject;
|
|
59
|
+
const promiseThen = Promise.prototype.then;
|
|
60
|
+
const weakMapGet = WeakMap.prototype.get;
|
|
61
|
+
const weakMapSet = WeakMap.prototype.set;
|
|
62
|
+
const weakRefDeref = WeakRef.prototype.deref;
|
|
63
|
+
const urlToString = URL.prototype.toString;
|
|
64
|
+
const dateNow = Date.now;
|
|
65
|
+
function getMapValue(map, key) {
|
|
66
|
+
return apply(mapGet, map, [key]);
|
|
67
|
+
}
|
|
68
|
+
function setMapValue(map, key, value) {
|
|
69
|
+
apply(mapSet, map, [key, value]);
|
|
70
|
+
}
|
|
71
|
+
function deleteMapValue(map, key) {
|
|
72
|
+
return apply(mapDelete, map, [key]);
|
|
73
|
+
}
|
|
74
|
+
function clearMap(map) {
|
|
75
|
+
apply(mapClear, map, []);
|
|
76
|
+
}
|
|
77
|
+
function getMapSize(map) {
|
|
78
|
+
return apply(mapSizeGetter, map, []);
|
|
79
|
+
}
|
|
80
|
+
function getWeakMapValue(map, key) {
|
|
81
|
+
return apply(weakMapGet, map, [key]);
|
|
82
|
+
}
|
|
83
|
+
function setWeakMapValue(map, key, value) {
|
|
84
|
+
apply(weakMapSet, map, [key, value]);
|
|
85
|
+
}
|
|
86
|
+
function dereference(reference) {
|
|
87
|
+
return apply(weakRefDeref, reference, []);
|
|
88
|
+
}
|
|
89
|
+
function registerFinalizer(registry, target, heldValue, unregisterToken) {
|
|
90
|
+
apply(finalizationRegistryRegister, registry, [target, heldValue, unregisterToken]);
|
|
91
|
+
}
|
|
92
|
+
function unregisterFinalizer(registry, unregisterToken) {
|
|
93
|
+
return apply(finalizationRegistryUnregister, registry, [unregisterToken]);
|
|
94
|
+
}
|
|
95
|
+
function resolveVoidPromise() {
|
|
96
|
+
return apply(promiseResolve, NativePromise, []);
|
|
97
|
+
}
|
|
98
|
+
function rejectPromise(reason) {
|
|
99
|
+
return apply(promiseReject, NativePromise, [reason]);
|
|
100
|
+
}
|
|
101
|
+
function thenPromise(promise, onFulfilled, onRejected) {
|
|
102
|
+
return apply(promiseThen, promise, [onFulfilled, onRejected]);
|
|
103
|
+
}
|
|
104
|
+
function chainPromise(promise, onFulfilled) {
|
|
105
|
+
return new NativePromise((resolve, reject) => {
|
|
106
|
+
thenPromise(promise, (value) => {
|
|
107
|
+
let next;
|
|
108
|
+
try {
|
|
109
|
+
next = onFulfilled(value);
|
|
110
|
+
}
|
|
111
|
+
catch (error) {
|
|
112
|
+
reject(error);
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
// Do not return `next` from this handler. Native promise adoption reads
|
|
116
|
+
// the live `.then` property, which tenant code can replace after this
|
|
117
|
+
// module loads. Apply the captured intrinsic explicitly instead.
|
|
118
|
+
thenPromise(next, resolve, reject);
|
|
119
|
+
}, reject);
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
function allPromises(promises) {
|
|
123
|
+
return new NativePromise((resolve, reject) => {
|
|
124
|
+
const results = [];
|
|
125
|
+
results.length = promises.length;
|
|
126
|
+
let remaining = promises.length;
|
|
127
|
+
if (remaining === 0) {
|
|
128
|
+
resolve(results);
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
for (let index = 0; index < promises.length; index++) {
|
|
132
|
+
thenPromise(promises[index], (value) => {
|
|
133
|
+
results[index] = value;
|
|
134
|
+
remaining--;
|
|
135
|
+
if (remaining === 0)
|
|
136
|
+
resolve(results);
|
|
137
|
+
}, reject);
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
function racePromiseWithTimeout(promise, timeoutValue, timeoutMs) {
|
|
142
|
+
let timeoutId;
|
|
143
|
+
const raced = new NativePromise((resolve, reject) => {
|
|
144
|
+
thenPromise(promise, resolve, reject);
|
|
145
|
+
timeoutId = nativeSetTimeout(() => resolve(timeoutValue), timeoutMs);
|
|
146
|
+
});
|
|
147
|
+
return {
|
|
148
|
+
promise: raced,
|
|
149
|
+
cancelTimeout: () => {
|
|
150
|
+
if (timeoutId !== undefined)
|
|
151
|
+
nativeClearTimeout(timeoutId);
|
|
152
|
+
},
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
function forEachMapEntry(map, callback) {
|
|
156
|
+
const iterator = apply(mapEntries, map, []);
|
|
157
|
+
while (true) {
|
|
158
|
+
const next = apply(mapIteratorNext, iterator, []);
|
|
159
|
+
if (next.done)
|
|
160
|
+
return;
|
|
161
|
+
if (callback(next.value[0], next.value[1]) === false)
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
function cloneLockfileEntry(entry) {
|
|
166
|
+
const sanitized = sanitizeLockfileEntry(entry);
|
|
167
|
+
if (sanitized === null)
|
|
168
|
+
throw lockfileInputError("invalid-structure");
|
|
169
|
+
return sanitized;
|
|
170
|
+
}
|
|
171
|
+
function defineImportEntry(imports, url, entry) {
|
|
172
|
+
// A null-prototype descriptor is required here. Tenant code can mutate
|
|
173
|
+
// Object.prototype in a shared runtime; inherited accessor/value fields can
|
|
174
|
+
// otherwise turn an ordinary data descriptor into an invalid descriptor.
|
|
175
|
+
const descriptor = objectCreate(null);
|
|
176
|
+
descriptor.value = entry;
|
|
177
|
+
descriptor.enumerable = true;
|
|
178
|
+
descriptor.configurable = true;
|
|
179
|
+
descriptor.writable = true;
|
|
180
|
+
objectDefineProperty(imports, url, descriptor);
|
|
181
|
+
}
|
|
182
|
+
function createImportDictionary(entries = [], prototype = "internal") {
|
|
183
|
+
const imports = prototype === "internal"
|
|
184
|
+
? objectCreate(null)
|
|
185
|
+
: {};
|
|
186
|
+
for (let index = 0; index < entries.length; index++) {
|
|
187
|
+
const entry = entries[index];
|
|
188
|
+
defineImportEntry(imports, entry[0], entry[1]);
|
|
189
|
+
}
|
|
190
|
+
return imports;
|
|
191
|
+
}
|
|
192
|
+
function cloneLockfileData(data, prototype = "internal") {
|
|
193
|
+
const sourceEntries = objectEntries(data.imports);
|
|
194
|
+
const entries = [];
|
|
195
|
+
entries.length = sourceEntries.length;
|
|
196
|
+
for (let index = 0; index < sourceEntries.length; index++) {
|
|
197
|
+
const sourceEntry = sourceEntries[index];
|
|
198
|
+
entries[index] = [sourceEntry[0], cloneLockfileEntry(sourceEntry[1])];
|
|
199
|
+
}
|
|
200
|
+
return {
|
|
201
|
+
version: LOCKFILE_VERSION,
|
|
202
|
+
imports: createImportDictionary(entries, prototype),
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
function createInternalLockfile() {
|
|
206
|
+
return { version: LOCKFILE_VERSION, imports: createImportDictionary() };
|
|
207
|
+
}
|
|
208
|
+
function isRecord(value) {
|
|
209
|
+
return typeof value === "object" && value !== null && !arrayIsArray(value);
|
|
210
|
+
}
|
|
211
|
+
function getOwnDataProperty(value, key) {
|
|
212
|
+
const descriptor = objectGetOwnPropertyDescriptor(value, key);
|
|
213
|
+
if (descriptor === undefined || !objectHasOwn(descriptor, "value"))
|
|
214
|
+
return undefined;
|
|
215
|
+
return { value: descriptor.value };
|
|
216
|
+
}
|
|
217
|
+
function sanitizeLockfileEntry(value) {
|
|
218
|
+
if (!isRecord(value))
|
|
219
|
+
return null;
|
|
220
|
+
const resolved = getOwnDataProperty(value, "resolved")?.value;
|
|
221
|
+
const integrity = getOwnDataProperty(value, "integrity")?.value;
|
|
222
|
+
if (typeof resolved !== "string" || typeof integrity !== "string")
|
|
223
|
+
return null;
|
|
224
|
+
const dependencies = getOwnDataProperty(value, "dependencies")?.value;
|
|
225
|
+
let sanitizedDependencies;
|
|
226
|
+
if (dependencies !== undefined) {
|
|
227
|
+
const sanitized = sanitizeStringArray(dependencies);
|
|
228
|
+
if (sanitized === null)
|
|
229
|
+
return null;
|
|
230
|
+
sanitizedDependencies = sanitized;
|
|
231
|
+
}
|
|
232
|
+
const fetchedAt = getOwnDataProperty(value, "fetchedAt")?.value;
|
|
233
|
+
if (fetchedAt !== undefined && typeof fetchedAt !== "string")
|
|
234
|
+
return null;
|
|
235
|
+
return {
|
|
236
|
+
resolved,
|
|
237
|
+
integrity,
|
|
238
|
+
...(sanitizedDependencies === undefined ? {} : { dependencies: sanitizedDependencies }),
|
|
239
|
+
...(fetchedAt === undefined ? {} : { fetchedAt }),
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
function sanitizeStringArray(value) {
|
|
243
|
+
if (!arrayIsArray(value))
|
|
244
|
+
return null;
|
|
245
|
+
const length = getOwnDataProperty(value, "length")?.value;
|
|
246
|
+
if (typeof length !== "number" || !numberIsSafeInteger(length) || length < 0)
|
|
247
|
+
return null;
|
|
248
|
+
const sanitized = [];
|
|
249
|
+
let index = 0;
|
|
250
|
+
const keys = objectKeys(value);
|
|
251
|
+
for (let keyIndex = 0; keyIndex < keys.length; keyIndex++) {
|
|
252
|
+
const key = keys[keyIndex];
|
|
253
|
+
if (key !== String(index))
|
|
254
|
+
continue;
|
|
255
|
+
const item = getOwnDataProperty(value, key)?.value;
|
|
256
|
+
if (typeof item !== "string")
|
|
257
|
+
return null;
|
|
258
|
+
objectDefineProperty(sanitized, index, {
|
|
259
|
+
value: item,
|
|
260
|
+
enumerable: true,
|
|
261
|
+
configurable: true,
|
|
262
|
+
writable: true,
|
|
263
|
+
});
|
|
264
|
+
index++;
|
|
265
|
+
}
|
|
266
|
+
return index === length ? sanitized : null;
|
|
267
|
+
}
|
|
268
|
+
function lockfileReadError(lockfilePath, reason, cause) {
|
|
269
|
+
const description = reason === "access-failed"
|
|
270
|
+
? "could not be accessed"
|
|
271
|
+
: reason === "invalid-json"
|
|
272
|
+
? "does not contain valid JSON"
|
|
273
|
+
: "has an invalid structure";
|
|
274
|
+
return LOCKFILE_READ_ERROR.create({
|
|
275
|
+
detail: `The lockfile ${description}. The file was left untouched.`,
|
|
276
|
+
cause,
|
|
277
|
+
context: { lockfilePath, reason },
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
function lockfileInputError(reason, cause) {
|
|
281
|
+
return INVALID_ARGUMENT.create({
|
|
282
|
+
detail: "The provided lockfile data has an invalid structure. The file was left untouched.",
|
|
283
|
+
cause,
|
|
284
|
+
context: { reason },
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
function lockfileFormatMismatch(lockfilePath, version) {
|
|
288
|
+
return LOCKFILE_FORMAT_MISMATCH.create({
|
|
289
|
+
detail: `The lockfile uses format version ${version}, but this ` +
|
|
290
|
+
`Veryfront build supports version ${LOCKFILE_VERSION}. The file was left untouched. ` +
|
|
291
|
+
"Upgrade Veryfront or migrate the lockfile before reading or modifying it.",
|
|
292
|
+
context: {
|
|
293
|
+
lockfilePath,
|
|
294
|
+
expectedVersion: LOCKFILE_VERSION,
|
|
295
|
+
actualVersion: version,
|
|
296
|
+
},
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
function sanitizeLockfileData(value, lockfilePath, invalidStructureError = () => lockfileReadError(lockfilePath, "invalid-structure")) {
|
|
300
|
+
if (!isRecord(value))
|
|
301
|
+
throw invalidStructureError();
|
|
302
|
+
const version = getOwnDataProperty(value, "version")?.value;
|
|
303
|
+
if (typeof version !== "number" || !numberIsSafeInteger(version)) {
|
|
304
|
+
throw invalidStructureError();
|
|
305
|
+
}
|
|
306
|
+
if (version !== LOCKFILE_VERSION) {
|
|
307
|
+
throw lockfileFormatMismatch(lockfilePath, version);
|
|
308
|
+
}
|
|
309
|
+
const parsedImports = getOwnDataProperty(value, "imports")?.value;
|
|
310
|
+
if (!isRecord(parsedImports)) {
|
|
311
|
+
throw invalidStructureError();
|
|
312
|
+
}
|
|
313
|
+
const imports = [];
|
|
314
|
+
const urls = objectKeys(parsedImports);
|
|
315
|
+
for (let index = 0; index < urls.length; index++) {
|
|
316
|
+
const url = urls[index];
|
|
317
|
+
const entry = getOwnDataProperty(parsedImports, url)?.value;
|
|
318
|
+
const sanitizedEntry = sanitizeLockfileEntry(entry);
|
|
319
|
+
if (sanitizedEntry === null) {
|
|
320
|
+
throw invalidStructureError();
|
|
321
|
+
}
|
|
322
|
+
imports[imports.length] = [url, sanitizedEntry];
|
|
323
|
+
}
|
|
324
|
+
return { version: LOCKFILE_VERSION, imports: createImportDictionary(imports) };
|
|
325
|
+
}
|
|
326
|
+
function parseLockfile(content, lockfilePath) {
|
|
327
|
+
let parsed;
|
|
328
|
+
try {
|
|
329
|
+
parsed = apply(jsonParse, NativeJSON, [content]);
|
|
330
|
+
}
|
|
331
|
+
catch (cause) {
|
|
332
|
+
throw lockfileReadError(lockfilePath, "invalid-json", cause);
|
|
333
|
+
}
|
|
334
|
+
return sanitizeLockfileData(parsed, lockfilePath);
|
|
335
|
+
}
|
|
8
336
|
export function createEmptyLockfile() {
|
|
9
|
-
return { version: LOCKFILE_VERSION, imports:
|
|
337
|
+
return { version: LOCKFILE_VERSION, imports: createImportDictionary([], "public") };
|
|
10
338
|
}
|
|
11
339
|
/** Compute integrity. */
|
|
12
340
|
export async function computeIntegrity(content) {
|
|
@@ -17,92 +345,747 @@ export async function verifyIntegrity(content, integrity) {
|
|
|
17
345
|
const computed = await computeIntegrity(content);
|
|
18
346
|
return computed === integrity;
|
|
19
347
|
}
|
|
348
|
+
const PLATFORM_FS_COORDINATION_KEY = "veryfront-platform-filesystem";
|
|
349
|
+
const adapterIdentityByInstance = new NativeWeakMap();
|
|
350
|
+
const lockfileAccessTails = new NativeMap();
|
|
351
|
+
const lockfileSharedStateReferences = new NativeMap();
|
|
352
|
+
const lockfileSharedStateAccessIdentities = new NativeWeakMap();
|
|
353
|
+
const lockfileCoordinationDomainByAdapter = new NativeWeakMap();
|
|
354
|
+
const sharedLockfileCoordinationDomains = new NativeMap();
|
|
355
|
+
const sharedLockfileCoordinationRegistry = new NativeFinalizationRegistry(({ accessKey, unregisterToken }) => {
|
|
356
|
+
const current = getMapValue(sharedLockfileCoordinationDomains, accessKey);
|
|
357
|
+
if (current?.unregisterToken === unregisterToken &&
|
|
358
|
+
dereference(current.reference) === undefined) {
|
|
359
|
+
deleteMapValue(sharedLockfileCoordinationDomains, accessKey);
|
|
360
|
+
}
|
|
361
|
+
});
|
|
362
|
+
let nextLockfileMutationSequence = 1n;
|
|
363
|
+
let nextLockfileSharedStateAccessIdentity = 1n;
|
|
364
|
+
const lockfileSharedStateRegistry = new NativeFinalizationRegistry(({ stateKey, unregisterToken }) => {
|
|
365
|
+
const current = getMapValue(lockfileSharedStateReferences, stateKey);
|
|
366
|
+
if (current?.unregisterToken === unregisterToken &&
|
|
367
|
+
dereference(current.reference) === undefined) {
|
|
368
|
+
deleteMapValue(lockfileSharedStateReferences, stateKey);
|
|
369
|
+
}
|
|
370
|
+
});
|
|
371
|
+
let nextAdapterIdentity = 1n;
|
|
372
|
+
function createLockfileSharedState() {
|
|
373
|
+
const state = objectCreate(null);
|
|
374
|
+
state.revision = 0n;
|
|
375
|
+
state.lastClearSequence = 0n;
|
|
376
|
+
return state;
|
|
377
|
+
}
|
|
378
|
+
function resolveLockfileSharedState(state) {
|
|
379
|
+
let root = state;
|
|
380
|
+
while (root.parent)
|
|
381
|
+
root = root.parent;
|
|
382
|
+
let current = state;
|
|
383
|
+
while (current.parent && current.parent !== root) {
|
|
384
|
+
const parent = current.parent;
|
|
385
|
+
current.parent = root;
|
|
386
|
+
current = parent;
|
|
387
|
+
}
|
|
388
|
+
return root;
|
|
389
|
+
}
|
|
390
|
+
function getLockfileSharedStateAccessKey(state) {
|
|
391
|
+
const root = resolveLockfileSharedState(state);
|
|
392
|
+
let identity = getWeakMapValue(lockfileSharedStateAccessIdentities, root);
|
|
393
|
+
if (identity === undefined) {
|
|
394
|
+
identity = nextLockfileSharedStateAccessIdentity++;
|
|
395
|
+
setWeakMapValue(lockfileSharedStateAccessIdentities, root, identity);
|
|
396
|
+
}
|
|
397
|
+
return `lockfile-state:${identity}`;
|
|
398
|
+
}
|
|
399
|
+
function registerLockfileSharedState(stateKey, state) {
|
|
400
|
+
const root = resolveLockfileSharedState(state);
|
|
401
|
+
const current = getMapValue(lockfileSharedStateReferences, stateKey);
|
|
402
|
+
if (current && dereference(current.reference) === root)
|
|
403
|
+
return;
|
|
404
|
+
if (current)
|
|
405
|
+
unregisterFinalizer(lockfileSharedStateRegistry, current.unregisterToken);
|
|
406
|
+
const unregisterToken = objectCreate(null);
|
|
407
|
+
setMapValue(lockfileSharedStateReferences, stateKey, {
|
|
408
|
+
reference: new NativeWeakRef(root),
|
|
409
|
+
unregisterToken,
|
|
410
|
+
});
|
|
411
|
+
registerFinalizer(lockfileSharedStateRegistry, root, { stateKey, unregisterToken }, unregisterToken);
|
|
412
|
+
}
|
|
413
|
+
function getLockfileSharedState(stateKeys) {
|
|
414
|
+
const uniqueStateKeys = [];
|
|
415
|
+
for (let stateKeyIndex = 0; stateKeyIndex < stateKeys.length; stateKeyIndex++) {
|
|
416
|
+
const stateKey = stateKeys[stateKeyIndex];
|
|
417
|
+
let duplicate = false;
|
|
418
|
+
for (let knownKeyIndex = 0; knownKeyIndex < uniqueStateKeys.length; knownKeyIndex++) {
|
|
419
|
+
const knownKey = uniqueStateKeys[knownKeyIndex];
|
|
420
|
+
if (knownKey === stateKey) {
|
|
421
|
+
duplicate = true;
|
|
422
|
+
break;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
if (!duplicate)
|
|
426
|
+
uniqueStateKeys[uniqueStateKeys.length] = stateKey;
|
|
427
|
+
}
|
|
428
|
+
const roots = [];
|
|
429
|
+
for (let stateKeyIndex = 0; stateKeyIndex < uniqueStateKeys.length; stateKeyIndex++) {
|
|
430
|
+
const stateKey = uniqueStateKeys[stateKeyIndex];
|
|
431
|
+
const reference = getMapValue(lockfileSharedStateReferences, stateKey)?.reference;
|
|
432
|
+
const referenced = reference ? dereference(reference) : undefined;
|
|
433
|
+
if (!referenced)
|
|
434
|
+
continue;
|
|
435
|
+
const root = resolveLockfileSharedState(referenced);
|
|
436
|
+
let duplicate = false;
|
|
437
|
+
for (let rootIndex = 0; rootIndex < roots.length; rootIndex++) {
|
|
438
|
+
const knownRoot = roots[rootIndex];
|
|
439
|
+
if (knownRoot === root) {
|
|
440
|
+
duplicate = true;
|
|
441
|
+
break;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
if (!duplicate)
|
|
445
|
+
roots[roots.length] = root;
|
|
446
|
+
}
|
|
447
|
+
const state = roots[0] ?? createLockfileSharedState();
|
|
448
|
+
for (let index = 1; index < roots.length; index++) {
|
|
449
|
+
const other = roots[index];
|
|
450
|
+
other.parent = state;
|
|
451
|
+
state.revision = (state.revision > other.revision ? state.revision : other.revision) + 1n;
|
|
452
|
+
state.lastClearSequence = state.lastClearSequence > other.lastClearSequence
|
|
453
|
+
? state.lastClearSequence
|
|
454
|
+
: other.lastClearSequence;
|
|
455
|
+
}
|
|
456
|
+
for (let stateKeyIndex = 0; stateKeyIndex < uniqueStateKeys.length; stateKeyIndex++) {
|
|
457
|
+
registerLockfileSharedState(uniqueStateKeys[stateKeyIndex], state);
|
|
458
|
+
}
|
|
459
|
+
return state;
|
|
460
|
+
}
|
|
461
|
+
function getAdapterInstanceIdentity(fs) {
|
|
462
|
+
let identity = getWeakMapValue(adapterIdentityByInstance, fs);
|
|
463
|
+
if (identity === undefined) {
|
|
464
|
+
identity = nextAdapterIdentity++;
|
|
465
|
+
setWeakMapValue(adapterIdentityByInstance, fs, identity);
|
|
466
|
+
}
|
|
467
|
+
return `instance:${identity}`;
|
|
468
|
+
}
|
|
469
|
+
function getLockfileCoordinationDomain(fs, accessKey, hasSharedCoordinationKey, managerDomain) {
|
|
470
|
+
if (hasSharedCoordinationKey) {
|
|
471
|
+
if (managerDomain)
|
|
472
|
+
return managerDomain;
|
|
473
|
+
const current = getMapValue(sharedLockfileCoordinationDomains, accessKey);
|
|
474
|
+
let domain = current ? dereference(current.reference) : undefined;
|
|
475
|
+
if (!domain) {
|
|
476
|
+
if (current) {
|
|
477
|
+
unregisterFinalizer(sharedLockfileCoordinationRegistry, current.unregisterToken);
|
|
478
|
+
}
|
|
479
|
+
domain = objectCreate(null);
|
|
480
|
+
domain.records = new NativeMap();
|
|
481
|
+
const unregisterToken = objectCreate(null);
|
|
482
|
+
setMapValue(sharedLockfileCoordinationDomains, accessKey, {
|
|
483
|
+
reference: new NativeWeakRef(domain),
|
|
484
|
+
unregisterToken,
|
|
485
|
+
});
|
|
486
|
+
registerFinalizer(sharedLockfileCoordinationRegistry, domain, { accessKey, unregisterToken }, unregisterToken);
|
|
487
|
+
}
|
|
488
|
+
return domain;
|
|
489
|
+
}
|
|
490
|
+
let domain = getWeakMapValue(lockfileCoordinationDomainByAdapter, fs);
|
|
491
|
+
if (!domain) {
|
|
492
|
+
domain = objectCreate(null);
|
|
493
|
+
domain.records = new NativeMap();
|
|
494
|
+
setWeakMapValue(lockfileCoordinationDomainByAdapter, fs, domain);
|
|
495
|
+
}
|
|
496
|
+
return domain;
|
|
497
|
+
}
|
|
498
|
+
async function resolveCanonicalLockfilePath(fs, projectDir, timeoutMs = LOCKFILE_CANONICALIZATION_TIMEOUT_MS, state) {
|
|
499
|
+
if (!fs.realPath)
|
|
500
|
+
return undefined;
|
|
501
|
+
if (state?.completed) {
|
|
502
|
+
const canonicalPath = state.completed.canonicalPath;
|
|
503
|
+
state.completed = undefined;
|
|
504
|
+
return canonicalPath;
|
|
505
|
+
}
|
|
506
|
+
let resolution = state?.inFlight;
|
|
507
|
+
if (!resolution) {
|
|
508
|
+
resolution = thenPromise(chainPromise(resolveVoidPromise(), () => fs.realPath(projectDir)), (canonicalProjectDir) => normalize(`${normalize(canonicalProjectDir)}/${LOCKFILE_NAME}`));
|
|
509
|
+
if (state) {
|
|
510
|
+
state.inFlight = resolution;
|
|
511
|
+
void thenPromise(resolution, (canonicalPath) => {
|
|
512
|
+
if (state.inFlight !== resolution)
|
|
513
|
+
return;
|
|
514
|
+
state.inFlight = undefined;
|
|
515
|
+
state.completed = { promise: resolution, canonicalPath };
|
|
516
|
+
}, () => {
|
|
517
|
+
if (state.inFlight === resolution)
|
|
518
|
+
state.inFlight = undefined;
|
|
519
|
+
});
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
const timedOut = Symbol("lockfile canonicalization timed out");
|
|
523
|
+
const race = racePromiseWithTimeout(resolution, timedOut, timeoutMs);
|
|
524
|
+
try {
|
|
525
|
+
const canonicalProjectDirResult = await race.promise;
|
|
526
|
+
if (canonicalProjectDirResult === timedOut)
|
|
527
|
+
return undefined;
|
|
528
|
+
if (state?.completed?.promise === resolution)
|
|
529
|
+
state.completed = undefined;
|
|
530
|
+
return canonicalProjectDirResult;
|
|
531
|
+
}
|
|
532
|
+
catch {
|
|
533
|
+
return undefined;
|
|
534
|
+
}
|
|
535
|
+
finally {
|
|
536
|
+
race.cancelTimeout();
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
function pruneCollectedCoordinationRecords(domain) {
|
|
540
|
+
forEachMapEntry(domain.records, (recordKey, record) => {
|
|
541
|
+
const liveState = record.state ? dereference(record.state) : undefined;
|
|
542
|
+
if (!record.retainedState && liveState === undefined) {
|
|
543
|
+
deleteMapValue(domain.records, recordKey);
|
|
544
|
+
}
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
function getUnresolvedCoordinationRecord(domain, projectDir, lockfilePath, logicalStateKey) {
|
|
548
|
+
const existing = getMapValue(domain.records, logicalStateKey);
|
|
549
|
+
if (existing)
|
|
550
|
+
return existing;
|
|
551
|
+
if (getMapSize(domain.records) >= MAX_UNRESOLVED_LOCKFILE_RECORDS) {
|
|
552
|
+
pruneCollectedCoordinationRecords(domain);
|
|
553
|
+
}
|
|
554
|
+
if (getMapSize(domain.records) >= MAX_UNRESOLVED_LOCKFILE_RECORDS) {
|
|
555
|
+
throw lockfileReadError(lockfilePath, "access-failed", new NativeError(`The shared lockfile coordinator reached its limit of ` +
|
|
556
|
+
`${MAX_UNRESOLVED_LOCKFILE_RECORDS} unresolved project paths`));
|
|
557
|
+
}
|
|
558
|
+
const record = objectCreate(null);
|
|
559
|
+
record.recordKey = logicalStateKey;
|
|
560
|
+
record.projectDir = projectDir;
|
|
561
|
+
record.logicalStateKey = logicalStateKey;
|
|
562
|
+
return record;
|
|
563
|
+
}
|
|
564
|
+
async function reconcileHistoricalCoordinationRecords(fs, domain, currentRecordKey, accessKey) {
|
|
565
|
+
const candidates = [];
|
|
566
|
+
const now = dateNow();
|
|
567
|
+
// Durable mutations take precedence over warmed-reader hints. A second pass
|
|
568
|
+
// fills the remaining bounded batch with live weak records.
|
|
569
|
+
for (let durablePass = 0; durablePass < 2; durablePass++) {
|
|
570
|
+
const durableOnly = durablePass === 0;
|
|
571
|
+
forEachMapEntry(domain.records, (knownRecordKey, knownRecord) => {
|
|
572
|
+
if (knownRecordKey === currentRecordKey)
|
|
573
|
+
return;
|
|
574
|
+
if (candidates.length >= MAX_HISTORICAL_RECONCILIATIONS_PER_ACCESS)
|
|
575
|
+
return false;
|
|
576
|
+
if (durableOnly !== (knownRecord.retainedState !== undefined))
|
|
577
|
+
return;
|
|
578
|
+
if ((knownRecord.historicalAttempts ?? 0) >=
|
|
579
|
+
MAX_HISTORICAL_CANONICALIZATION_ATTEMPTS ||
|
|
580
|
+
(knownRecord.historicalRetryAfter ?? 0) > now) {
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
583
|
+
const knownState = knownRecord.retainedState ??
|
|
584
|
+
(knownRecord.state ? dereference(knownRecord.state) : undefined);
|
|
585
|
+
if (!knownState) {
|
|
586
|
+
deleteMapValue(domain.records, knownRecordKey);
|
|
587
|
+
return;
|
|
588
|
+
}
|
|
589
|
+
candidates[candidates.length] = {
|
|
590
|
+
recordKey: knownRecordKey,
|
|
591
|
+
record: knownRecord,
|
|
592
|
+
state: knownState,
|
|
593
|
+
};
|
|
594
|
+
});
|
|
595
|
+
}
|
|
596
|
+
// Resolve one bounded batch concurrently, so N stale paths cost at most the
|
|
597
|
+
// historical timeout once rather than N times while the shared queue waits.
|
|
598
|
+
const canonicalizationPromises = [];
|
|
599
|
+
for (let index = 0; index < candidates.length; index++) {
|
|
600
|
+
const candidate = candidates[index];
|
|
601
|
+
let canonicalization = candidate.record.historicalCanonicalization;
|
|
602
|
+
if (!canonicalization) {
|
|
603
|
+
canonicalization = resolveCanonicalLockfilePath(fs, candidate.record.projectDir, HISTORICAL_LOCKFILE_CANONICALIZATION_TIMEOUT_MS);
|
|
604
|
+
candidate.record.historicalCanonicalization = canonicalization;
|
|
605
|
+
candidate.ownsCanonicalization = true;
|
|
606
|
+
}
|
|
607
|
+
candidate.canonicalization = canonicalization;
|
|
608
|
+
canonicalizationPromises[index] = canonicalization;
|
|
609
|
+
}
|
|
610
|
+
const canonicalPaths = await allPromises(canonicalizationPromises);
|
|
611
|
+
for (let index = 0; index < candidates.length; index++) {
|
|
612
|
+
const candidate = candidates[index];
|
|
613
|
+
// Concurrent healthy projects share the same bounded historical lookup.
|
|
614
|
+
// Only its owner mutates retry/state bookkeeping once it settles.
|
|
615
|
+
if (!candidate.ownsCanonicalization)
|
|
616
|
+
continue;
|
|
617
|
+
if (candidate.record.historicalCanonicalization === candidate.canonicalization) {
|
|
618
|
+
candidate.record.historicalCanonicalization = undefined;
|
|
619
|
+
}
|
|
620
|
+
const knownCanonicalPath = canonicalPaths[index];
|
|
621
|
+
if (knownCanonicalPath === undefined) {
|
|
622
|
+
const attempts = (candidate.record.historicalAttempts ?? 0) + 1;
|
|
623
|
+
candidate.record.historicalAttempts = attempts;
|
|
624
|
+
const retryDelay = HISTORICAL_CANONICALIZATION_RETRY_BASE_MS *
|
|
625
|
+
2 ** (attempts - 1);
|
|
626
|
+
candidate.record.historicalRetryAfter = now +
|
|
627
|
+
(retryDelay < HISTORICAL_CANONICALIZATION_RETRY_MAX_MS
|
|
628
|
+
? retryDelay
|
|
629
|
+
: HISTORICAL_CANONICALIZATION_RETRY_MAX_MS);
|
|
630
|
+
// Keep failures retryable without letting a permanently missing project
|
|
631
|
+
// starve records inserted later in the bounded journal. Attempts are
|
|
632
|
+
// finite here; the record's own manager can still retry its canonical
|
|
633
|
+
// identity on every operation and reconcile it without this path.
|
|
634
|
+
deleteMapValue(domain.records, candidate.recordKey);
|
|
635
|
+
setMapValue(domain.records, candidate.recordKey, candidate.record);
|
|
636
|
+
continue;
|
|
637
|
+
}
|
|
638
|
+
deleteMapValue(domain.records, candidate.recordKey);
|
|
639
|
+
getLockfileSharedState([
|
|
640
|
+
candidate.record.logicalStateKey,
|
|
641
|
+
apply(jsonStringify, NativeJSON, [[accessKey, knownCanonicalPath]]),
|
|
642
|
+
]);
|
|
643
|
+
}
|
|
644
|
+
return candidates.length > 0;
|
|
645
|
+
}
|
|
646
|
+
async function resolveLockfileCoordinationIdentity(fs, projectDir, lockfilePath, accessKey, hasSharedCoordinationKey, canonicalizationState, managerDomain) {
|
|
647
|
+
// A declared coordination key is the complete shared lock domain. Including
|
|
648
|
+
// a canonical path here would split adapters that share a backing store when
|
|
649
|
+
// only some of them implement realPath, recreating a read-merge-write race.
|
|
650
|
+
const logicalStateKey = apply(jsonStringify, NativeJSON, [[accessKey, lockfilePath]]);
|
|
651
|
+
const domain = getLockfileCoordinationDomain(fs, accessKey, hasSharedCoordinationKey, managerDomain);
|
|
652
|
+
if (!fs.realPath) {
|
|
653
|
+
if (!hasSharedCoordinationKey) {
|
|
654
|
+
return { coordinationDomain: domain, stateKeys: [logicalStateKey] };
|
|
655
|
+
}
|
|
656
|
+
const unresolvedRecord = getUnresolvedCoordinationRecord(domain, projectDir, lockfilePath, logicalStateKey);
|
|
657
|
+
return {
|
|
658
|
+
coordinationDomain: domain,
|
|
659
|
+
stateKeys: [logicalStateKey],
|
|
660
|
+
unresolvedRecord,
|
|
661
|
+
};
|
|
662
|
+
}
|
|
663
|
+
const recordKey = logicalStateKey;
|
|
664
|
+
const canonicalLockfilePath = await resolveCanonicalLockfilePath(fs, projectDir, LOCKFILE_CANONICALIZATION_TIMEOUT_MS, canonicalizationState);
|
|
665
|
+
if (canonicalLockfilePath === undefined) {
|
|
666
|
+
const unresolvedRecord = getUnresolvedCoordinationRecord(domain, projectDir, lockfilePath, logicalStateKey);
|
|
667
|
+
return {
|
|
668
|
+
coordinationDomain: domain,
|
|
669
|
+
stateKeys: [logicalStateKey],
|
|
670
|
+
unresolvedRecord,
|
|
671
|
+
};
|
|
672
|
+
}
|
|
673
|
+
// Historical aliases are reconciled incrementally. A stale or deleted
|
|
674
|
+
// project remains retryable but cannot fail an unrelated healthy project or
|
|
675
|
+
// monopolize the shared backing-store queue.
|
|
676
|
+
const reconciledUnresolvedRecords = await reconcileHistoricalCoordinationRecords(fs, domain, recordKey, accessKey);
|
|
677
|
+
return {
|
|
678
|
+
coordinationDomain: domain,
|
|
679
|
+
resolvedRecordKey: recordKey,
|
|
680
|
+
reconciledUnresolvedRecords,
|
|
681
|
+
stateKeys: [
|
|
682
|
+
logicalStateKey,
|
|
683
|
+
apply(jsonStringify, NativeJSON, [[accessKey, canonicalLockfilePath]]),
|
|
684
|
+
],
|
|
685
|
+
};
|
|
686
|
+
}
|
|
687
|
+
function compareLockfileImportKeys(left, right) {
|
|
688
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
689
|
+
}
|
|
690
|
+
function serializeLockfileAccess(accessKey, operation) {
|
|
691
|
+
const predecessor = getMapValue(lockfileAccessTails, accessKey) ?? resolveVoidPromise();
|
|
692
|
+
const result = chainPromise(predecessor, operation);
|
|
693
|
+
const tail = thenPromise(result, () => undefined, () => undefined);
|
|
694
|
+
setMapValue(lockfileAccessTails, accessKey, tail);
|
|
695
|
+
const release = () => {
|
|
696
|
+
if (getMapValue(lockfileAccessTails, accessKey) === tail) {
|
|
697
|
+
deleteMapValue(lockfileAccessTails, accessKey);
|
|
698
|
+
}
|
|
699
|
+
};
|
|
700
|
+
return thenPromise(result, (value) => {
|
|
701
|
+
release();
|
|
702
|
+
return value;
|
|
703
|
+
}, (error) => {
|
|
704
|
+
release();
|
|
705
|
+
throw error;
|
|
706
|
+
});
|
|
707
|
+
}
|
|
20
708
|
function createPlatformFSAdapter() {
|
|
21
709
|
const fs = createFileSystem();
|
|
710
|
+
const renameFile = fs.rename;
|
|
22
711
|
return {
|
|
712
|
+
coordinationKey: PLATFORM_FS_COORDINATION_KEY,
|
|
23
713
|
readFile(path) {
|
|
24
714
|
return fs.readTextFile(path);
|
|
25
715
|
},
|
|
26
716
|
writeFile(path, content) {
|
|
27
717
|
return fs.writeTextFile(path, content);
|
|
28
718
|
},
|
|
719
|
+
...(renameFile === undefined ? {} : {
|
|
720
|
+
rename(from, to) {
|
|
721
|
+
return apply(renameFile, fs, [from, to]);
|
|
722
|
+
},
|
|
723
|
+
}),
|
|
29
724
|
exists(path) {
|
|
30
725
|
return fs.exists(path);
|
|
31
726
|
},
|
|
32
727
|
remove(path) {
|
|
33
728
|
return fs.remove(path);
|
|
34
729
|
},
|
|
730
|
+
realPath(path) {
|
|
731
|
+
return resolvePlatformRealPath(path);
|
|
732
|
+
},
|
|
35
733
|
};
|
|
36
734
|
}
|
|
37
735
|
/** Create lockfile manager. */
|
|
38
736
|
export function createLockfileManager(projectDir, fsAdapter) {
|
|
39
737
|
const fs = fsAdapter ?? createPlatformFSAdapter();
|
|
40
|
-
const
|
|
738
|
+
const normalizedProjectDir = normalize(projectDir);
|
|
739
|
+
const lockfilePath = normalize(`${normalizedProjectDir}/${LOCKFILE_NAME}`);
|
|
740
|
+
const adapterIdentity = getAdapterInstanceIdentity(fs);
|
|
741
|
+
const coordinationKey = fs.coordinationKey;
|
|
742
|
+
const hasSharedCoordinationKey = coordinationKey !== undefined;
|
|
743
|
+
const accessKey = hasSharedCoordinationKey
|
|
744
|
+
? apply(jsonStringify, NativeJSON, [[`shared:${coordinationKey}`]])
|
|
745
|
+
: apply(jsonStringify, NativeJSON, [[adapterIdentity]]);
|
|
746
|
+
const logicalQueueKey = hasSharedCoordinationKey
|
|
747
|
+
? apply(jsonStringify, NativeJSON, [[accessKey, lockfilePath]])
|
|
748
|
+
: accessKey;
|
|
41
749
|
let cache = null;
|
|
42
|
-
let
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
750
|
+
let cacheRevision = -1n;
|
|
751
|
+
let managerOperationTail = resolveVoidPromise();
|
|
752
|
+
let managerCoordinationDomain;
|
|
753
|
+
let managerUnresolvedRecord;
|
|
754
|
+
let managerSharedState;
|
|
755
|
+
const canonicalizationState = objectCreate(null);
|
|
756
|
+
const pendingEntries = new NativeMap();
|
|
757
|
+
function serializeManagerOperation(operation) {
|
|
758
|
+
const result = chainPromise(managerOperationTail, operation);
|
|
759
|
+
managerOperationTail = thenPromise(result, () => undefined, () => undefined);
|
|
760
|
+
return result;
|
|
761
|
+
}
|
|
762
|
+
async function resolveStateUnderAccess() {
|
|
763
|
+
// Retry canonicalization for every serialized manager operation. A
|
|
764
|
+
// project path may not exist on the first access, and a later successful
|
|
765
|
+
// resolution must bridge the earlier logical state to canonical aliases.
|
|
766
|
+
const { coordinationDomain, stateKeys, unresolvedRecord, resolvedRecordKey, reconciledUnresolvedRecords, } = await resolveLockfileCoordinationIdentity(fs, normalizedProjectDir, lockfilePath, accessKey, hasSharedCoordinationKey, canonicalizationState, managerCoordinationDomain);
|
|
767
|
+
managerCoordinationDomain = coordinationDomain;
|
|
768
|
+
// A coordinationKey declares a shared backing store. Canonicalize the
|
|
769
|
+
// per-lockfile cache state independently so aliases invalidate each other
|
|
770
|
+
// without coupling separate projects.
|
|
771
|
+
managerSharedState = getLockfileSharedState(stateKeys);
|
|
772
|
+
if (resolvedRecordKey !== undefined) {
|
|
773
|
+
deleteMapValue(coordinationDomain.records, resolvedRecordKey);
|
|
774
|
+
}
|
|
775
|
+
managerUnresolvedRecord = unresolvedRecord;
|
|
776
|
+
if (unresolvedRecord) {
|
|
777
|
+
// A live warmed reader must be discoverable by a healthy alias so a
|
|
778
|
+
// later clear can invalidate its cache. Keep only the state weakly: dead
|
|
779
|
+
// readers and their adapters remain collectible.
|
|
780
|
+
unresolvedRecord.state = new NativeWeakRef(managerSharedState);
|
|
781
|
+
setMapValue(coordinationDomain.records, unresolvedRecord.recordKey, unresolvedRecord);
|
|
782
|
+
}
|
|
783
|
+
const state = resolveLockfileSharedState(managerSharedState);
|
|
784
|
+
return {
|
|
785
|
+
state,
|
|
786
|
+
// Queue by the reconciled state root rather than the manager's current
|
|
787
|
+
// spelling of the path. A shared adapter with realPath can bridge an
|
|
788
|
+
// alias owned by an adapter without realPath; both must then serialize
|
|
789
|
+
// through the same queue to protect the read-merge-write window.
|
|
790
|
+
accessQueueKey: getLockfileSharedStateAccessKey(state),
|
|
791
|
+
unresolved: unresolvedRecord !== undefined || reconciledUnresolvedRecords === true,
|
|
792
|
+
};
|
|
793
|
+
}
|
|
794
|
+
function retainUnresolvedMutation(state, durable) {
|
|
795
|
+
if (!managerCoordinationDomain || !managerUnresolvedRecord)
|
|
796
|
+
return;
|
|
797
|
+
managerUnresolvedRecord.state = new NativeWeakRef(state);
|
|
798
|
+
if (durable)
|
|
799
|
+
managerUnresolvedRecord.retainedState = state;
|
|
800
|
+
setMapValue(managerCoordinationDomain.records, managerUnresolvedRecord.recordKey, managerUnresolvedRecord);
|
|
801
|
+
}
|
|
802
|
+
function withLockfileAccess(operation) {
|
|
803
|
+
return serializeLockfileAccess(logicalQueueKey, () => chainPromise(resolveStateUnderAccess(), ({ state, accessQueueKey, unresolved }) => {
|
|
804
|
+
const resolvedAccessQueueKey = hasSharedCoordinationKey && unresolved
|
|
805
|
+
? accessKey
|
|
806
|
+
: accessQueueKey;
|
|
807
|
+
return resolvedAccessQueueKey === logicalQueueKey
|
|
808
|
+
? operation(state)
|
|
809
|
+
: serializeLockfileAccess(resolvedAccessQueueKey, () => operation(state));
|
|
810
|
+
}));
|
|
811
|
+
}
|
|
812
|
+
async function lockfileExists() {
|
|
46
813
|
try {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
const parsed = JSON.parse(content);
|
|
52
|
-
if (parsed.version !== LOCKFILE_VERSION) {
|
|
53
|
-
logger.warn(`[lockfile] Version mismatch, expected ${LOCKFILE_VERSION}, got ${parsed.version}`);
|
|
54
|
-
cache = createEmptyLockfile();
|
|
55
|
-
return cache;
|
|
56
|
-
}
|
|
57
|
-
cache = parsed;
|
|
58
|
-
return cache;
|
|
814
|
+
return await fs.exists(lockfilePath);
|
|
815
|
+
}
|
|
816
|
+
catch (cause) {
|
|
817
|
+
throw lockfileReadError(lockfilePath, "access-failed", cause);
|
|
59
818
|
}
|
|
60
|
-
|
|
61
|
-
|
|
819
|
+
}
|
|
820
|
+
async function readFromDisk() {
|
|
821
|
+
if (!(await lockfileExists()))
|
|
62
822
|
return null;
|
|
823
|
+
let content;
|
|
824
|
+
try {
|
|
825
|
+
content = await fs.readFile(lockfilePath);
|
|
63
826
|
}
|
|
827
|
+
catch (cause) {
|
|
828
|
+
throw lockfileReadError(lockfilePath, "access-failed", cause);
|
|
829
|
+
}
|
|
830
|
+
return parseLockfile(content, lockfilePath);
|
|
64
831
|
}
|
|
65
|
-
async function
|
|
66
|
-
|
|
832
|
+
async function readCurrentUnderAccess(state) {
|
|
833
|
+
forEachMapEntry(pendingEntries, (url, pending) => {
|
|
834
|
+
if (pending.sequence < state.lastClearSequence)
|
|
835
|
+
deleteMapValue(pendingEntries, url);
|
|
836
|
+
});
|
|
837
|
+
if (cacheRevision === state.revision)
|
|
838
|
+
return cache;
|
|
839
|
+
const data = await readFromDisk();
|
|
840
|
+
if (getMapSize(pendingEntries) === 0) {
|
|
841
|
+
cache = data;
|
|
842
|
+
}
|
|
843
|
+
else {
|
|
844
|
+
const resolvedCache = data ?? createInternalLockfile();
|
|
845
|
+
cache = resolvedCache;
|
|
846
|
+
forEachMapEntry(pendingEntries, (url, pending) => {
|
|
847
|
+
defineImportEntry(resolvedCache.imports, url, cloneLockfileEntry(pending.entry));
|
|
848
|
+
});
|
|
849
|
+
}
|
|
850
|
+
cacheRevision = state.revision;
|
|
851
|
+
return cache;
|
|
852
|
+
}
|
|
853
|
+
function readCurrent() {
|
|
854
|
+
// Cold and invalidated reads share the same access turn as write/remove
|
|
855
|
+
// operations, so peer managers cannot observe a partially replaced file.
|
|
856
|
+
return withLockfileAccess((state) => readCurrentUnderAccess(state));
|
|
857
|
+
}
|
|
858
|
+
function read() {
|
|
859
|
+
return serializeManagerOperation(async () => {
|
|
860
|
+
const data = await readCurrent();
|
|
861
|
+
return data === null ? null : cloneLockfileData(data, "public");
|
|
862
|
+
});
|
|
863
|
+
}
|
|
864
|
+
async function replaceLockfileAtomically(rename, serialized) {
|
|
865
|
+
// Stage the new content next to the lockfile and rename it into place, so
|
|
866
|
+
// readers observe either the previous or the new complete file and a
|
|
867
|
+
// crash mid-write can never leave a truncated lockfile behind.
|
|
868
|
+
const tempPath = `${lockfilePath}.${generateUuid()}.tmp`;
|
|
869
|
+
try {
|
|
870
|
+
await fs.writeFile(tempPath, serialized);
|
|
871
|
+
await rename(tempPath, lockfilePath);
|
|
872
|
+
}
|
|
873
|
+
catch (error) {
|
|
874
|
+
if (fs.remove) {
|
|
875
|
+
try {
|
|
876
|
+
await fs.remove(tempPath);
|
|
877
|
+
}
|
|
878
|
+
catch {
|
|
879
|
+
// Best-effort cleanup only; the original write/rename failure wins.
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
throw error;
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
async function writeToDisk(data) {
|
|
886
|
+
const sourceEntries = objectEntries(data.imports);
|
|
887
|
+
apply(arraySort, sourceEntries, [
|
|
888
|
+
(left, right) => compareLockfileImportKeys(left[0], right[0]),
|
|
889
|
+
]);
|
|
890
|
+
const sortedEntries = [];
|
|
891
|
+
sortedEntries.length = sourceEntries.length;
|
|
892
|
+
for (let index = 0; index < sourceEntries.length; index++) {
|
|
893
|
+
const sourceEntry = sourceEntries[index];
|
|
894
|
+
sortedEntries[index] = [sourceEntry[0], cloneLockfileEntry(sourceEntry[1])];
|
|
895
|
+
}
|
|
67
896
|
const sorted = {
|
|
68
|
-
version:
|
|
69
|
-
imports:
|
|
897
|
+
version: LOCKFILE_VERSION,
|
|
898
|
+
imports: createImportDictionary(sortedEntries),
|
|
70
899
|
};
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
data.imports
|
|
82
|
-
cache = data;
|
|
83
|
-
dirty = true;
|
|
84
|
-
}
|
|
85
|
-
async function has(url) {
|
|
86
|
-
const data = await read();
|
|
87
|
-
return url in (data?.imports ?? {});
|
|
88
|
-
}
|
|
89
|
-
async function clear() {
|
|
90
|
-
cache = createEmptyLockfile();
|
|
91
|
-
dirty = false;
|
|
92
|
-
if (!fs.remove)
|
|
93
|
-
return;
|
|
94
|
-
const exists = await fs.exists(lockfilePath);
|
|
95
|
-
if (!exists)
|
|
96
|
-
return;
|
|
97
|
-
await fs.remove(lockfilePath);
|
|
900
|
+
const serialized = `${apply(jsonStringify, NativeJSON, [sorted, null, 2])}\n`;
|
|
901
|
+
const renameFile = fs.rename;
|
|
902
|
+
if (renameFile) {
|
|
903
|
+
await replaceLockfileAtomically((from, to) => apply(renameFile, fs, [from, to]), serialized);
|
|
904
|
+
}
|
|
905
|
+
else {
|
|
906
|
+
// Adapters without rename cannot replace the file atomically; fall back
|
|
907
|
+
// to an in-place write and accept the torn-write risk for them.
|
|
908
|
+
await fs.writeFile(lockfilePath, serialized);
|
|
909
|
+
}
|
|
910
|
+
logger.debug(`Written ${objectKeys(data.imports).length} entries`);
|
|
98
911
|
}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
912
|
+
function write(data) {
|
|
913
|
+
return serializeManagerOperation(() => {
|
|
914
|
+
const snapshot = sanitizeLockfileData(data, lockfilePath, () => lockfileInputError("invalid-structure"));
|
|
915
|
+
return withLockfileAccess(async (state) => {
|
|
916
|
+
// Revalidate the existing file before replacing it. Unsupported,
|
|
917
|
+
// unreadable, and malformed files are always preserved.
|
|
918
|
+
await readFromDisk();
|
|
919
|
+
await writeToDisk(snapshot);
|
|
920
|
+
cache = snapshot;
|
|
921
|
+
clearMap(pendingEntries);
|
|
922
|
+
cacheRevision = ++state.revision;
|
|
923
|
+
retainUnresolvedMutation(state, true);
|
|
924
|
+
});
|
|
925
|
+
});
|
|
926
|
+
}
|
|
927
|
+
function get(url) {
|
|
928
|
+
return serializeManagerOperation(async () => {
|
|
929
|
+
const entry = (await readCurrent())?.imports[url];
|
|
930
|
+
return entry === undefined ? null : cloneLockfileEntry(entry);
|
|
931
|
+
});
|
|
932
|
+
}
|
|
933
|
+
function set(url, entry) {
|
|
934
|
+
let snapshot;
|
|
935
|
+
try {
|
|
936
|
+
snapshot = cloneLockfileEntry(entry);
|
|
937
|
+
}
|
|
938
|
+
catch (error) {
|
|
939
|
+
return rejectPromise(error);
|
|
940
|
+
}
|
|
941
|
+
return serializeManagerOperation(() => withLockfileAccess(async (state) => {
|
|
942
|
+
const data = (await readCurrentUnderAccess(state)) ?? createInternalLockfile();
|
|
943
|
+
defineImportEntry(data.imports, url, snapshot);
|
|
944
|
+
cache = data;
|
|
945
|
+
setMapValue(pendingEntries, url, {
|
|
946
|
+
entry: snapshot,
|
|
947
|
+
sequence: nextLockfileMutationSequence++,
|
|
948
|
+
});
|
|
949
|
+
retainUnresolvedMutation(state, false);
|
|
950
|
+
}));
|
|
951
|
+
}
|
|
952
|
+
function has(url) {
|
|
953
|
+
return serializeManagerOperation(async () => {
|
|
954
|
+
const data = await readCurrent();
|
|
955
|
+
return data !== null && objectHasOwn(data.imports, url);
|
|
956
|
+
});
|
|
957
|
+
}
|
|
958
|
+
function clear() {
|
|
959
|
+
return serializeManagerOperation(() => withLockfileAccess(async (state) => {
|
|
960
|
+
// Clear is an explicit destructive recovery path. Check access to the
|
|
961
|
+
// path, but do not parse content that the user has chosen to discard.
|
|
962
|
+
const existing = await lockfileExists();
|
|
963
|
+
let cleared = null;
|
|
964
|
+
if (existing) {
|
|
965
|
+
if (fs.remove)
|
|
966
|
+
await fs.remove(lockfilePath);
|
|
967
|
+
else {
|
|
968
|
+
cleared = createInternalLockfile();
|
|
969
|
+
await writeToDisk(cleared);
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
// State changes only after the access check and any requested deletion
|
|
973
|
+
// or fallback write have succeeded, so a failed clear leaves memory and
|
|
974
|
+
// disk untouched.
|
|
975
|
+
cache = cleared;
|
|
976
|
+
clearMap(pendingEntries);
|
|
977
|
+
state.lastClearSequence = nextLockfileMutationSequence++;
|
|
978
|
+
cacheRevision = ++state.revision;
|
|
979
|
+
retainUnresolvedMutation(state, true);
|
|
980
|
+
}));
|
|
981
|
+
}
|
|
982
|
+
function flush() {
|
|
983
|
+
return serializeManagerOperation(async () => {
|
|
984
|
+
if (getMapSize(pendingEntries) === 0)
|
|
985
|
+
return;
|
|
986
|
+
await withLockfileAccess(async (state) => {
|
|
987
|
+
forEachMapEntry(pendingEntries, (url, pending) => {
|
|
988
|
+
if (pending.sequence < state.lastClearSequence) {
|
|
989
|
+
deleteMapValue(pendingEntries, url);
|
|
990
|
+
}
|
|
991
|
+
});
|
|
992
|
+
if (getMapSize(pendingEntries) === 0) {
|
|
993
|
+
cache = await readFromDisk();
|
|
994
|
+
cacheRevision = state.revision;
|
|
995
|
+
return;
|
|
996
|
+
}
|
|
997
|
+
// Merge only this manager's pending entries onto the latest on-disk
|
|
998
|
+
// state while holding the canonical backing-store access turn. This
|
|
999
|
+
// makes the whole read-merge-write sequence atomic relative to other
|
|
1000
|
+
// in-process managers, including managers using path aliases.
|
|
1001
|
+
const snapshot = new NativeMap();
|
|
1002
|
+
forEachMapEntry(pendingEntries, (url, pending) => {
|
|
1003
|
+
setMapValue(snapshot, url, pending);
|
|
1004
|
+
});
|
|
1005
|
+
const merged = (await readFromDisk()) ?? createInternalLockfile();
|
|
1006
|
+
forEachMapEntry(snapshot, (url, pending) => {
|
|
1007
|
+
defineImportEntry(merged.imports, url, cloneLockfileEntry(pending.entry));
|
|
1008
|
+
});
|
|
1009
|
+
await writeToDisk(merged);
|
|
1010
|
+
forEachMapEntry(snapshot, (url, pending) => {
|
|
1011
|
+
if (getMapValue(pendingEntries, url) === pending) {
|
|
1012
|
+
deleteMapValue(pendingEntries, url);
|
|
1013
|
+
}
|
|
1014
|
+
});
|
|
1015
|
+
forEachMapEntry(pendingEntries, (url, pending) => {
|
|
1016
|
+
defineImportEntry(merged.imports, url, cloneLockfileEntry(pending.entry));
|
|
1017
|
+
});
|
|
1018
|
+
cache = merged;
|
|
1019
|
+
cacheRevision = ++state.revision;
|
|
1020
|
+
retainUnresolvedMutation(state, true);
|
|
1021
|
+
});
|
|
1022
|
+
});
|
|
103
1023
|
}
|
|
104
1024
|
return { read, write, get, set, has, clear, flush };
|
|
105
1025
|
}
|
|
1026
|
+
const lockfileReadWarningIssued = new NativeWeakMap();
|
|
1027
|
+
/**
|
|
1028
|
+
* Degrade an unreadable-lockfile failure (`lockfile-read-error`) on a build
|
|
1029
|
+
* hot path: warn once per manager, naming the file and the
|
|
1030
|
+
* `veryfront lock --clear` remedy, and report the error as absorbed. Every
|
|
1031
|
+
* other failure — most importantly `lockfile-format-mismatch`, a valid
|
|
1032
|
+
* lockfile written by a newer Veryfront build — is reported as not absorbed
|
|
1033
|
+
* so it keeps failing loudly.
|
|
1034
|
+
*/
|
|
1035
|
+
function absorbUnreadableLockfileError(lockfile, error) {
|
|
1036
|
+
const snapshot = snapshotVeryfrontError(error);
|
|
1037
|
+
if (snapshot?.slug !== LOCKFILE_READ_ERROR.slug)
|
|
1038
|
+
return false;
|
|
1039
|
+
if (getWeakMapValue(lockfileReadWarningIssued, lockfile) !== true) {
|
|
1040
|
+
setWeakMapValue(lockfileReadWarningIssued, lockfile, true);
|
|
1041
|
+
logger.warn(`Lockfile ${LOCKFILE_NAME} could not be read (${snapshot.message}); ` +
|
|
1042
|
+
"continuing this build without lockfile entries. " +
|
|
1043
|
+
"Run `veryfront lock --clear` to reset the unreadable or invalid lockfile.");
|
|
1044
|
+
}
|
|
1045
|
+
return true;
|
|
1046
|
+
}
|
|
1047
|
+
/**
|
|
1048
|
+
* Read a lockfile entry on a build or dev-server hot path.
|
|
1049
|
+
*
|
|
1050
|
+
* A lockfile written by a newer Veryfront build (`lockfile-format-mismatch`)
|
|
1051
|
+
* keeps failing loudly: proceeding around data the running build cannot
|
|
1052
|
+
* understand is exactly the overwrite hazard this module guards against. An
|
|
1053
|
+
* unreadable or malformed lockfile (`lockfile-read-error`) instead degrades to
|
|
1054
|
+
* a cache miss so builds keep working from fresh fetches; a warning naming
|
|
1055
|
+
* the lockfile and the `veryfront lock --clear` remedy is logged once per
|
|
1056
|
+
* manager instead of once per import.
|
|
1057
|
+
*/
|
|
1058
|
+
export async function getLockfileEntryForBuild(lockfile, url) {
|
|
1059
|
+
try {
|
|
1060
|
+
return await lockfile.get(url);
|
|
1061
|
+
}
|
|
1062
|
+
catch (error) {
|
|
1063
|
+
if (!absorbUnreadableLockfileError(lockfile, error))
|
|
1064
|
+
throw error;
|
|
1065
|
+
return null;
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
/**
|
|
1069
|
+
* Persist a lockfile entry from a build or dev-server hot path.
|
|
1070
|
+
*
|
|
1071
|
+
* Mirrors {@link getLockfileEntryForBuild}: when the on-disk lockfile is
|
|
1072
|
+
* unreadable or malformed (`lockfile-read-error`), the file is left untouched
|
|
1073
|
+
* for `veryfront lock --clear` and the entry is simply not recorded, so a
|
|
1074
|
+
* successful refetch still serves the build. Returns whether the entry was
|
|
1075
|
+
* staged; callers should only `flush()` after a `true` result. A newer-format
|
|
1076
|
+
* lockfile (`lockfile-format-mismatch`) keeps failing loudly.
|
|
1077
|
+
*/
|
|
1078
|
+
export async function setLockfileEntryForBuild(lockfile, url, entry) {
|
|
1079
|
+
try {
|
|
1080
|
+
await lockfile.set(url, entry);
|
|
1081
|
+
return true;
|
|
1082
|
+
}
|
|
1083
|
+
catch (error) {
|
|
1084
|
+
if (!absorbUnreadableLockfileError(lockfile, error))
|
|
1085
|
+
throw error;
|
|
1086
|
+
return false;
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
106
1089
|
const USER_AGENT_HEADERS = { "user-agent": "Mozilla/5.0 Veryfront/1.0" };
|
|
107
1090
|
export async function fetchWithLock(options) {
|
|
108
1091
|
const { lockfile, url, fetchFn = fetch, strict = false } = options;
|
|
@@ -178,7 +1161,8 @@ export function resolveImportUrl(specifier, baseUrl) {
|
|
|
178
1161
|
if (!specifier.startsWith("./") && !specifier.startsWith("../"))
|
|
179
1162
|
return null;
|
|
180
1163
|
try {
|
|
181
|
-
|
|
1164
|
+
const resolved = new NativeURL(specifier, baseUrl);
|
|
1165
|
+
return apply(urlToString, resolved, []);
|
|
182
1166
|
}
|
|
183
1167
|
catch (_) {
|
|
184
1168
|
/* expected: specifier may not be a valid relative URL */
|