@valbuild/server 0.147.0 → 0.148.1
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 +115 -0
- package/browser/dist/valbuild-server-browser.cjs.dev.js +2 -2
- package/browser/dist/valbuild-server-browser.cjs.prod.js +2 -2
- package/browser/dist/valbuild-server-browser.esm.js +2 -2
- package/dist/{ValRouterCore-7ffcc08c.cjs.prod.js → ValRouterCore-42772c21.cjs.prod.js} +980 -53
- package/dist/{ValRouterCore-f5004f8f.esm.js → ValRouterCore-7ca3dc68.esm.js} +972 -54
- package/dist/{ValRouterCore-3fd3d444.cjs.dev.js → ValRouterCore-9bdaaf89.cjs.dev.js} +980 -53
- package/dist/declarations/src/ValOps.d.ts +32 -11
- package/dist/declarations/src/ValOpsMemory.d.ts +4 -4
- package/dist/declarations/src/ValServer.d.ts +1 -2
- package/dist/declarations/src/checkRemoteRef.d.ts +40 -2
- package/dist/declarations/src/createFixPatch.d.ts +25 -3
- package/dist/declarations/src/fixFiles.d.ts +42 -0
- package/dist/declarations/src/fixHandlers.d.ts +14 -0
- package/dist/declarations/src/getSettings.d.ts +1 -0
- package/dist/declarations/src/index.d.ts +1 -0
- package/dist/declarations/src/platform.d.ts +16 -0
- package/dist/declarations/src/remoteFileCheck.d.ts +116 -0
- package/dist/declarations/src/remoteHostCheck.d.ts +101 -0
- package/dist/declarations/src/studioValidation.d.ts +84 -0
- package/dist/declarations/src/uploadRemoteFile.d.ts +1 -1
- package/dist/declarations/src/videosetFixes.d.ts +3 -2
- package/dist/{getJsonEntryAtCommit-df65d861.cjs.prod.js → getJsonEntryAtCommit-2e36e805.cjs.prod.js} +3 -4
- package/dist/{getJsonEntryAtCommit-1e08d724.esm.js → getJsonEntryAtCommit-40b6d9c1.esm.js} +3 -4
- package/dist/{getJsonEntryAtCommit-e96beb9e.cjs.dev.js → getJsonEntryAtCommit-64f67a50.cjs.dev.js} +3 -4
- package/dist/valbuild-server.cjs.dev.js +1352 -695
- package/dist/valbuild-server.cjs.prod.js +1352 -695
- package/dist/valbuild-server.esm.js +1352 -696
- package/package.json +4 -5
|
@@ -12,7 +12,7 @@ var path = require('path');
|
|
|
12
12
|
var fs = require('fs');
|
|
13
13
|
var vm = require('node:vm');
|
|
14
14
|
var node_module = require('node:module');
|
|
15
|
-
var ValRouterCore = require('./ValRouterCore-
|
|
15
|
+
var ValRouterCore = require('./ValRouterCore-42772c21.cjs.prod.js');
|
|
16
16
|
var server = require('@valbuild/ui/server');
|
|
17
17
|
var z = require('zod');
|
|
18
18
|
var zodValidationError = require('zod-validation-error');
|
|
@@ -23,10 +23,10 @@ var buffer = require('buffer');
|
|
|
23
23
|
var http = require('node:http');
|
|
24
24
|
var https = require('node:https');
|
|
25
25
|
var net = require('node:net');
|
|
26
|
-
var sucrase = require('sucrase');
|
|
27
26
|
var sizeOf = require('image-size');
|
|
28
27
|
var http$1 = require('http');
|
|
29
28
|
var https$1 = require('https');
|
|
29
|
+
var sucrase = require('sucrase');
|
|
30
30
|
var fs$1 = require('fs/promises');
|
|
31
31
|
|
|
32
32
|
function _interopDefault (e) { return e && e.__esModule ? e : { 'default': e }; }
|
|
@@ -3189,8 +3189,8 @@ class ValOpsFS extends ValRouterCore.ValOps {
|
|
|
3189
3189
|
// Cheap, and this is the one path that creates staging directories, so it
|
|
3190
3190
|
// is where the ones nobody claimed get noticed.
|
|
3191
3191
|
sweepStaleUploads(patchesDir);
|
|
3192
|
-
const buffer = ValRouterCore.bufferFromDataUrl(data);
|
|
3193
|
-
if (!buffer) {
|
|
3192
|
+
const buffer$1 = ValRouterCore.bufferFromDataUrl(data, base64 => buffer.Buffer.from(base64, "base64"));
|
|
3193
|
+
if (!buffer$1) {
|
|
3194
3194
|
return {
|
|
3195
3195
|
error: {
|
|
3196
3196
|
message: "Could not create buffer from data url. Not a data url? First chars were: " + data.slice(0, 20)
|
|
@@ -3198,7 +3198,7 @@ class ValOpsFS extends ValRouterCore.ValOps {
|
|
|
3198
3198
|
};
|
|
3199
3199
|
}
|
|
3200
3200
|
this.host.writeUf8File(metadataFilePath, JSON.stringify(metadata));
|
|
3201
|
-
this.host.writeBinaryFile(patchFilePath, buffer);
|
|
3201
|
+
this.host.writeBinaryFile(patchFilePath, buffer$1);
|
|
3202
3202
|
return {
|
|
3203
3203
|
patchId,
|
|
3204
3204
|
filePath
|
|
@@ -9258,568 +9258,279 @@ function readableError(error) {
|
|
|
9258
9258
|
}
|
|
9259
9259
|
}
|
|
9260
9260
|
|
|
9261
|
-
/** The platform every server on Node runs with. See {@link ValServerPlatform}. */
|
|
9262
|
-
const nodePlatform = {
|
|
9263
|
-
createOps: createValOps,
|
|
9264
|
-
probeUrl: url => probeUrl(url),
|
|
9265
|
-
jwt: {
|
|
9266
|
-
verifyJwt,
|
|
9267
|
-
decodeJwtWithoutVerifying,
|
|
9268
|
-
encodeJwt,
|
|
9269
|
-
getExpire
|
|
9270
|
-
},
|
|
9271
|
-
readValLoginToken,
|
|
9272
|
-
resolveRemoteFileAuth
|
|
9273
|
-
};
|
|
9274
|
-
|
|
9275
|
-
async function createValServer(valModules, route, opts, config, callbacks, formatter,
|
|
9276
9261
|
/**
|
|
9277
|
-
*
|
|
9278
|
-
*
|
|
9279
|
-
|
|
9280
|
-
|
|
9281
|
-
|
|
9282
|
-
*
|
|
9283
|
-
*
|
|
9262
|
+
* The size and length of an ISO base media file (`.mp4`, `.m4v`, `.mov`),
|
|
9263
|
+
* read from its headers. Nothing is decoded.
|
|
9264
|
+
*
|
|
9265
|
+
* Hand-written rather than a media library, deliberately: `@valbuild/server`
|
|
9266
|
+
* is loaded by every app that runs Val, and the four numbers wanted here are
|
|
9267
|
+
* three small boxes away from the start of the `moov` box:
|
|
9268
|
+
*
|
|
9269
|
+
* moov > mvhd timescale and duration (version 0 and 1)
|
|
9270
|
+
* moov > mvex > mehd duration of a FRAGMENTED file, whose mvhd says 0
|
|
9271
|
+
* moov > trak > tkhd width and height (16.16 fixed point) and the
|
|
9272
|
+
* display matrix, for a rotated phone video
|
|
9273
|
+
* moov > trak > mdia > hdlr `vide`: which trak is the picture
|
|
9274
|
+
*
|
|
9275
|
+
* Only the box headers are read on the way to `moov`, and then `moov` itself,
|
|
9276
|
+
* so a multi-GB file costs a few small reads: `mdat` is seeked past, never
|
|
9277
|
+
* loaded. That is what {@link ByteSource} is for.
|
|
9284
9278
|
*/
|
|
9285
|
-
|
|
9286
|
-
|
|
9287
|
-
|
|
9288
|
-
|
|
9289
|
-
|
|
9290
|
-
|
|
9291
|
-
|
|
9292
|
-
}
|
|
9279
|
+
|
|
9280
|
+
/** Random access to a file's bytes, so a large `mdat` can be skipped. */
|
|
9281
|
+
|
|
9282
|
+
function bufferByteSource(buffer) {
|
|
9283
|
+
return {
|
|
9284
|
+
size: buffer.length,
|
|
9285
|
+
read: (offset, length) => buffer.subarray(offset, offset + length)
|
|
9286
|
+
};
|
|
9293
9287
|
}
|
|
9294
9288
|
|
|
9295
|
-
// TODO: remove
|
|
9296
9289
|
/**
|
|
9297
|
-
*
|
|
9298
|
-
*
|
|
9299
|
-
* This is the only thing in this module that touches either, and it is a local
|
|
9300
|
-
* development convenience: scanning upwards for a `.git` to guess the commit and
|
|
9301
|
-
* branch. A static import put `fs` in the module graph of everything reaching
|
|
9302
|
-
* `createValApiRouter` -- which is every server integration, including ones that
|
|
9303
|
-
* run where there is no filesystem. Workerd provides no `fs`, so such a build
|
|
9304
|
-
* could not be bundled at all without stubbing it.
|
|
9305
|
-
*
|
|
9306
|
-
* The `await import` costs nothing here: the only caller is the CLI, on a
|
|
9307
|
-
* machine that has both.
|
|
9290
|
+
* Run `read` against a file on disk, reading only what it asks for. The file
|
|
9291
|
+
* is closed afterwards whatever happens.
|
|
9308
9292
|
*/
|
|
9309
|
-
|
|
9310
|
-
const
|
|
9311
|
-
|
|
9312
|
-
|
|
9313
|
-
|
|
9314
|
-
|
|
9315
|
-
|
|
9316
|
-
|
|
9317
|
-
|
|
9318
|
-
|
|
9319
|
-
|
|
9320
|
-
|
|
9321
|
-
|
|
9322
|
-
|
|
9323
|
-
|
|
9324
|
-
|
|
9325
|
-
|
|
9326
|
-
|
|
9327
|
-
const branchName = match[1];
|
|
9328
|
-
return {
|
|
9329
|
-
branch: branchName,
|
|
9330
|
-
commit: await readCommit(currentDir, branchName)
|
|
9331
|
-
};
|
|
9332
|
-
} else {
|
|
9333
|
-
return {
|
|
9334
|
-
commit: undefined,
|
|
9335
|
-
branch: undefined
|
|
9336
|
-
};
|
|
9293
|
+
function withFileByteSource(absolutePath, read) {
|
|
9294
|
+
const fd = fs__default["default"].openSync(absolutePath, "r");
|
|
9295
|
+
try {
|
|
9296
|
+
const size = fs__default["default"].fstatSync(fd).size;
|
|
9297
|
+
return read({
|
|
9298
|
+
size,
|
|
9299
|
+
read: (offset, length) => {
|
|
9300
|
+
const wanted = Math.max(0, Math.min(length, size - offset));
|
|
9301
|
+
const buffer = Buffer.alloc(wanted);
|
|
9302
|
+
let filled = 0;
|
|
9303
|
+
while (filled < wanted) {
|
|
9304
|
+
const n = fs__default["default"].readSync(fd, buffer, filled, wanted - filled, offset + filled);
|
|
9305
|
+
if (n === 0) {
|
|
9306
|
+
break;
|
|
9307
|
+
}
|
|
9308
|
+
filled += n;
|
|
9309
|
+
}
|
|
9310
|
+
return buffer.subarray(0, filled);
|
|
9337
9311
|
}
|
|
9338
|
-
}
|
|
9339
|
-
|
|
9312
|
+
});
|
|
9313
|
+
} finally {
|
|
9314
|
+
fs__default["default"].closeSync(fd);
|
|
9315
|
+
}
|
|
9316
|
+
}
|
|
9317
|
+
/**
|
|
9318
|
+
* A `moov` larger than this is not a header worth reading into memory. A
|
|
9319
|
+
* two-hour film's is a few MB; this is far past any real one.
|
|
9320
|
+
*/
|
|
9321
|
+
const MAX_MOOV_SIZE = 64 * 1024 * 1024;
|
|
9340
9322
|
|
|
9341
|
-
|
|
9342
|
-
|
|
9343
|
-
|
|
9344
|
-
|
|
9345
|
-
|
|
9346
|
-
|
|
9323
|
+
/**
|
|
9324
|
+
* Whatever of {@link IsoBmffMetadata} the file declares. A field that cannot be
|
|
9325
|
+
* read is absent; nothing here throws on a file that is not ISO BMFF.
|
|
9326
|
+
*/
|
|
9327
|
+
function readIsoBmffMetadata(source) {
|
|
9328
|
+
const moov = findTopLevelBox(source, "moov");
|
|
9329
|
+
if (!moov) {
|
|
9330
|
+
return {};
|
|
9331
|
+
}
|
|
9332
|
+
const metadata = {};
|
|
9333
|
+
const mvhd = childBox(moov, "mvhd");
|
|
9334
|
+
const timescaleAndDuration = mvhd ? readMvhd(mvhd) : undefined;
|
|
9335
|
+
if (timescaleAndDuration) {
|
|
9336
|
+
const {
|
|
9337
|
+
timescale,
|
|
9338
|
+
duration
|
|
9339
|
+
} = timescaleAndDuration;
|
|
9340
|
+
if (duration > 0) {
|
|
9341
|
+
metadata.duration = duration / timescale;
|
|
9342
|
+
} else {
|
|
9343
|
+
// A fragmented file (CMAF) says 0 in mvhd and puts the length in mehd,
|
|
9344
|
+
// in the same timescale.
|
|
9345
|
+
const mehd = childBox(childBox(moov, "mvex"), "mehd");
|
|
9346
|
+
const fragmentDuration = mehd ? readMehd(mehd) : undefined;
|
|
9347
|
+
if (fragmentDuration !== undefined && fragmentDuration > 0) {
|
|
9348
|
+
metadata.duration = fragmentDuration / timescale;
|
|
9347
9349
|
}
|
|
9348
|
-
return findGitHead(parentDir, depth + 1);
|
|
9349
9350
|
}
|
|
9350
9351
|
}
|
|
9351
|
-
|
|
9352
|
-
|
|
9353
|
-
|
|
9354
|
-
|
|
9355
|
-
|
|
9356
|
-
|
|
9357
|
-
|
|
9358
|
-
|
|
9359
|
-
|
|
9360
|
-
|
|
9361
|
-
|
|
9362
|
-
|
|
9363
|
-
async function readCommit(gitDir, branchName) {
|
|
9364
|
-
const {
|
|
9365
|
-
promises: fs
|
|
9366
|
-
} = await Promise.resolve().then(function () { return /*#__PURE__*/_interopNamespace(require('fs')); });
|
|
9367
|
-
const path = await Promise.resolve().then(function () { return /*#__PURE__*/_interopNamespace(require('path')); });
|
|
9368
|
-
try {
|
|
9369
|
-
return (await fs.readFile(path.join(gitDir, ".git", "refs", "heads", branchName), "utf-8")).trim();
|
|
9370
|
-
} catch {
|
|
9371
|
-
return undefined;
|
|
9352
|
+
for (const trak of childBoxes(moov, "trak")) {
|
|
9353
|
+
const hdlr = childBox(childBox(trak, "mdia"), "hdlr");
|
|
9354
|
+
if (!hdlr || hdlr.length < 12 || hdlr.toString("latin1", 8, 12) !== "vide") {
|
|
9355
|
+
continue;
|
|
9356
|
+
}
|
|
9357
|
+
const tkhd = childBox(trak, "tkhd");
|
|
9358
|
+
const size = tkhd ? readTkhd(tkhd) : undefined;
|
|
9359
|
+
if (size && size.width > 0 && size.height > 0) {
|
|
9360
|
+
metadata.width = size.width;
|
|
9361
|
+
metadata.height = size.height;
|
|
9362
|
+
break;
|
|
9363
|
+
}
|
|
9372
9364
|
}
|
|
9365
|
+
return metadata;
|
|
9373
9366
|
}
|
|
9374
|
-
function createValApiRouter(route, valServerPromise, convert) {
|
|
9375
|
-
const uiRequestHandler = server.createUIRequestHandler();
|
|
9376
|
-
return ValRouterCore.createValRequestHandler(route, valServerPromise, convert, uiRequestHandler);
|
|
9377
|
-
}
|
|
9378
|
-
|
|
9379
|
-
const JsFileLookupMapping = [
|
|
9380
|
-
// NOTE: first one matching will be used
|
|
9381
|
-
[".cjs.d.ts", [".esm.js", ".mjs.js"]], [".cjs.js", [".esm.js", ".mjs.js"]], [".cjs", [".mjs"]], [".d.ts", [".js", ".esm.js", ".mjs.js"]]];
|
|
9382
|
-
const MAX_CACHE_SIZE = 100 * 1024 * 1024; // 100 mb
|
|
9383
|
-
const MAX_OBJECT_KEY_SIZE = 2 ** 27; // https://stackoverflow.com/questions/13367391/is-there-a-limit-on-length-of-the-key-string-in-js-object
|
|
9384
9367
|
|
|
9385
|
-
|
|
9386
|
-
|
|
9387
|
-
|
|
9388
|
-
|
|
9389
|
-
|
|
9390
|
-
|
|
9391
|
-
|
|
9392
|
-
|
|
9393
|
-
|
|
9394
|
-
|
|
9395
|
-
|
|
9396
|
-
|
|
9397
|
-
|
|
9398
|
-
rmFile: fs__default["default"].rmSync,
|
|
9399
|
-
readBuffer: fileName => {
|
|
9400
|
-
try {
|
|
9401
|
-
return fs__default["default"].readFileSync(fileName);
|
|
9402
|
-
} catch {
|
|
9368
|
+
/** The payload (after the header) of the first top-level box of a type. */
|
|
9369
|
+
function findTopLevelBox(source, type) {
|
|
9370
|
+
let offset = 0;
|
|
9371
|
+
while (offset + 8 <= source.size) {
|
|
9372
|
+
const header = source.read(offset, 16);
|
|
9373
|
+
if (header.length < 8) {
|
|
9374
|
+
return undefined;
|
|
9375
|
+
}
|
|
9376
|
+
let size = header.readUInt32BE(0);
|
|
9377
|
+
const boxType = header.toString("latin1", 4, 8);
|
|
9378
|
+
let headerSize = 8;
|
|
9379
|
+
if (size === 1) {
|
|
9380
|
+
if (header.length < 16) {
|
|
9403
9381
|
return undefined;
|
|
9404
9382
|
}
|
|
9383
|
+
size = Number(header.readBigUInt64BE(8));
|
|
9384
|
+
headerSize = 16;
|
|
9385
|
+
} else if (size === 0) {
|
|
9386
|
+
size = source.size - offset;
|
|
9405
9387
|
}
|
|
9406
|
-
|
|
9407
|
-
|
|
9408
|
-
this.compilerOptions = compilerOptions;
|
|
9409
|
-
this.sourceFileHandler = sourceFileHandler;
|
|
9410
|
-
this.host = host;
|
|
9411
|
-
this.disableCache = disableCache;
|
|
9412
|
-
this.cache = {};
|
|
9413
|
-
this.cacheSize = 0;
|
|
9414
|
-
}
|
|
9415
|
-
getModule(modulePath) {
|
|
9416
|
-
if (!modulePath) {
|
|
9417
|
-
throw Error(`Illegal module path: "${modulePath}"`);
|
|
9388
|
+
if (size < headerSize) {
|
|
9389
|
+
return undefined;
|
|
9418
9390
|
}
|
|
9419
|
-
|
|
9420
|
-
|
|
9421
|
-
|
|
9422
|
-
}
|
|
9423
|
-
let compiledCode;
|
|
9424
|
-
if (this.cache[code] && !this.disableCache) {
|
|
9425
|
-
// TODO: use hash instead of code as key
|
|
9426
|
-
compiledCode = this.cache[code];
|
|
9427
|
-
} else {
|
|
9428
|
-
compiledCode = sucrase.transform(code, {
|
|
9429
|
-
filePath: modulePath,
|
|
9430
|
-
disableESTransforms: true,
|
|
9431
|
-
transforms: ["typescript"]
|
|
9432
|
-
}).code;
|
|
9433
|
-
if (!this.disableCache) {
|
|
9434
|
-
if (this.cacheSize > MAX_CACHE_SIZE) {
|
|
9435
|
-
console.warn("Cache size exceeded, clearing cache");
|
|
9436
|
-
this.cache = {};
|
|
9437
|
-
this.cacheSize = 0;
|
|
9438
|
-
}
|
|
9439
|
-
if (code.length < MAX_OBJECT_KEY_SIZE) {
|
|
9440
|
-
this.cache[code] = compiledCode;
|
|
9441
|
-
this.cacheSize += code.length + compiledCode.length; // code is mostly ASCII so 1 byte per char
|
|
9442
|
-
}
|
|
9391
|
+
if (boxType === type) {
|
|
9392
|
+
if (size - headerSize > MAX_MOOV_SIZE) {
|
|
9393
|
+
return undefined;
|
|
9443
9394
|
}
|
|
9395
|
+
const payload = source.read(offset + headerSize, size - headerSize);
|
|
9396
|
+
return payload.length === size - headerSize ? payload : undefined;
|
|
9444
9397
|
}
|
|
9445
|
-
|
|
9398
|
+
offset += size;
|
|
9446
9399
|
}
|
|
9447
|
-
|
|
9448
|
-
|
|
9449
|
-
|
|
9450
|
-
|
|
9451
|
-
|
|
9452
|
-
|
|
9453
|
-
|
|
9454
|
-
|
|
9455
|
-
|
|
9456
|
-
|
|
9457
|
-
|
|
9400
|
+
return undefined;
|
|
9401
|
+
}
|
|
9402
|
+
|
|
9403
|
+
/** Each child box of a container payload, as `[type, payload]`. */
|
|
9404
|
+
function* boxesIn(payload) {
|
|
9405
|
+
let offset = 0;
|
|
9406
|
+
while (offset + 8 <= payload.length) {
|
|
9407
|
+
let size = payload.readUInt32BE(offset);
|
|
9408
|
+
const type = payload.toString("latin1", offset + 4, offset + 8);
|
|
9409
|
+
let headerSize = 8;
|
|
9410
|
+
if (size === 1) {
|
|
9411
|
+
if (offset + 16 > payload.length) {
|
|
9412
|
+
return;
|
|
9458
9413
|
}
|
|
9459
|
-
|
|
9414
|
+
size = Number(payload.readBigUInt64BE(offset + 8));
|
|
9415
|
+
headerSize = 16;
|
|
9416
|
+
} else if (size === 0) {
|
|
9417
|
+
size = payload.length - offset;
|
|
9460
9418
|
}
|
|
9461
|
-
|
|
9462
|
-
|
|
9463
|
-
const followedPath = ((_this$host$realpath = (_this$host = this.host).realpath) === null || _this$host$realpath === void 0 ? void 0 : _this$host$realpath.call(_this$host, filePath)) ?? filePath;
|
|
9464
|
-
if (!followedPath) {
|
|
9465
|
-
throw Error(`File path was empty: "${filePath}", containing file: "${containingFilePath}", requested module: "${requestedModuleName}"`);
|
|
9419
|
+
if (size < headerSize || offset + size > payload.length) {
|
|
9420
|
+
return;
|
|
9466
9421
|
}
|
|
9467
|
-
|
|
9422
|
+
yield [type, payload.subarray(offset + headerSize, offset + size)];
|
|
9423
|
+
offset += size;
|
|
9468
9424
|
}
|
|
9469
|
-
|
|
9470
|
-
|
|
9471
|
-
|
|
9472
|
-
|
|
9473
|
-
|
|
9474
|
-
|
|
9475
|
-
|
|
9476
|
-
|
|
9477
|
-
|
|
9478
|
-
if (!requiresReplacements) {
|
|
9479
|
-
if (this.host.fileExists(filePath)) {
|
|
9480
|
-
return {
|
|
9481
|
-
match: filePath
|
|
9482
|
-
};
|
|
9483
|
-
}
|
|
9425
|
+
}
|
|
9426
|
+
function childBoxes(payload, type) {
|
|
9427
|
+
if (!payload) {
|
|
9428
|
+
return [];
|
|
9429
|
+
}
|
|
9430
|
+
const found = [];
|
|
9431
|
+
for (const [boxType, child] of boxesIn(payload)) {
|
|
9432
|
+
if (boxType === type) {
|
|
9433
|
+
found.push(child);
|
|
9484
9434
|
}
|
|
9485
|
-
|
|
9486
|
-
|
|
9487
|
-
|
|
9488
|
-
|
|
9489
|
-
|
|
9490
|
-
|
|
9491
|
-
|
|
9492
|
-
|
|
9493
|
-
|
|
9494
|
-
|
|
9495
|
-
|
|
9496
|
-
|
|
9497
|
-
|
|
9498
|
-
|
|
9435
|
+
}
|
|
9436
|
+
return found;
|
|
9437
|
+
}
|
|
9438
|
+
function childBox(payload, type) {
|
|
9439
|
+
return childBoxes(payload, type)[0];
|
|
9440
|
+
}
|
|
9441
|
+
|
|
9442
|
+
/** "Unknown" in a version 1 box. Not a literal: `0x…n` needs an ES2020 target. */
|
|
9443
|
+
const ALL_ONES_64 = BigInt("0xffffffffffffffff");
|
|
9444
|
+
function readMvhd(mvhd) {
|
|
9445
|
+
const version = mvhd[0];
|
|
9446
|
+
if (version === 1) {
|
|
9447
|
+
if (mvhd.length < 32) return undefined;
|
|
9448
|
+
const timescale = mvhd.readUInt32BE(20);
|
|
9449
|
+
const duration = mvhd.readBigUInt64BE(24);
|
|
9450
|
+
if (timescale === 0 || duration === ALL_ONES_64) {
|
|
9451
|
+
return undefined;
|
|
9499
9452
|
}
|
|
9500
9453
|
return {
|
|
9501
|
-
|
|
9502
|
-
|
|
9454
|
+
timescale,
|
|
9455
|
+
duration: Number(duration)
|
|
9503
9456
|
};
|
|
9504
9457
|
}
|
|
9458
|
+
if (mvhd.length < 20) return undefined;
|
|
9459
|
+
const timescale = mvhd.readUInt32BE(12);
|
|
9460
|
+
const duration = mvhd.readUInt32BE(16);
|
|
9461
|
+
// All ones means "unknown" in a version 0 box.
|
|
9462
|
+
if (timescale === 0 || duration === 0xffffffff) {
|
|
9463
|
+
return undefined;
|
|
9464
|
+
}
|
|
9465
|
+
return {
|
|
9466
|
+
timescale,
|
|
9467
|
+
duration
|
|
9468
|
+
};
|
|
9469
|
+
}
|
|
9470
|
+
function readMehd(mehd) {
|
|
9471
|
+
if (mehd[0] === 1) {
|
|
9472
|
+
return mehd.length >= 12 ? Number(mehd.readBigUInt64BE(4)) : undefined;
|
|
9473
|
+
}
|
|
9474
|
+
return mehd.length >= 8 ? mehd.readUInt32BE(4) : undefined;
|
|
9505
9475
|
}
|
|
9506
9476
|
|
|
9507
9477
|
/**
|
|
9508
|
-
*
|
|
9509
|
-
*
|
|
9478
|
+
* The size a track is SHOWN at: tkhd's width and height, swapped when the
|
|
9479
|
+
* display matrix turns the picture a quarter turn — a phone records upright
|
|
9480
|
+
* video sideways and says so here, and a page lays it out as it is shown.
|
|
9510
9481
|
*/
|
|
9511
|
-
|
|
9512
|
-
|
|
9513
|
-
|
|
9514
|
-
|
|
9515
|
-
|
|
9516
|
-
|
|
9517
|
-
|
|
9518
|
-
readDirectory(rootDir, extensions, excludes, includes, depth) {
|
|
9519
|
-
return this.valFS.readDirectory(rootDir, extensions, excludes, includes, depth);
|
|
9520
|
-
}
|
|
9521
|
-
rmFile(fileName) {
|
|
9522
|
-
this.valFS.rmFile(fileName);
|
|
9523
|
-
}
|
|
9524
|
-
writeFile(fileName, text, encoding) {
|
|
9525
|
-
this.valFS.writeFile(fileName, text, encoding);
|
|
9526
|
-
}
|
|
9527
|
-
getCurrentDirectory() {
|
|
9528
|
-
return this.currentDirectory;
|
|
9529
|
-
}
|
|
9530
|
-
getCanonicalFileName(fileName) {
|
|
9531
|
-
if (path__default["default"].isAbsolute(fileName)) {
|
|
9532
|
-
return path__default["default"].normalize(fileName);
|
|
9533
|
-
}
|
|
9534
|
-
return path__default["default"].resolve(this.getCurrentDirectory(), fileName);
|
|
9535
|
-
}
|
|
9536
|
-
fileExists(fileName) {
|
|
9537
|
-
return this.valFS.fileExists(fileName);
|
|
9538
|
-
}
|
|
9539
|
-
readFile(fileName) {
|
|
9540
|
-
return this.valFS.readFile(fileName);
|
|
9541
|
-
}
|
|
9542
|
-
readBuffer(fileName) {
|
|
9543
|
-
return this.valFS.readBuffer(fileName);
|
|
9544
|
-
}
|
|
9545
|
-
realpath(path) {
|
|
9546
|
-
return this.valFS.realpath(path);
|
|
9482
|
+
function readTkhd(tkhd) {
|
|
9483
|
+
// Past the version-specific times and ids, then 16 bytes of reserved,
|
|
9484
|
+
// layer, alternate group, volume and reserved, then the 36-byte matrix.
|
|
9485
|
+
const matrixAt = tkhd[0] === 1 ? 4 + 32 + 16 : 4 + 20 + 16;
|
|
9486
|
+
const widthAt = matrixAt + 36;
|
|
9487
|
+
if (tkhd.length < widthAt + 8) {
|
|
9488
|
+
return undefined;
|
|
9547
9489
|
}
|
|
9490
|
+
const a = tkhd.readInt32BE(matrixAt);
|
|
9491
|
+
const b = tkhd.readInt32BE(matrixAt + 4);
|
|
9492
|
+
const c = tkhd.readInt32BE(matrixAt + 12);
|
|
9493
|
+
const d = tkhd.readInt32BE(matrixAt + 16);
|
|
9494
|
+
const width = Math.round(tkhd.readUInt32BE(widthAt) / 0x10000);
|
|
9495
|
+
const height = Math.round(tkhd.readUInt32BE(widthAt + 4) / 0x10000);
|
|
9496
|
+
const quarterTurn = a === 0 && d === 0 && b !== 0 && c !== 0;
|
|
9497
|
+
return quarterTurn ? {
|
|
9498
|
+
width: height,
|
|
9499
|
+
height: width
|
|
9500
|
+
} : {
|
|
9501
|
+
width,
|
|
9502
|
+
height
|
|
9503
|
+
};
|
|
9548
9504
|
}
|
|
9549
9505
|
|
|
9550
9506
|
/**
|
|
9551
|
-
* The size and length of
|
|
9552
|
-
* read from its headers. Nothing is decoded.
|
|
9507
|
+
* The size and length of a Matroska file (`.webm`, and `.mkv`, which is the
|
|
9508
|
+
* same container), read from its headers. Nothing is decoded.
|
|
9553
9509
|
*
|
|
9554
|
-
* Hand-written
|
|
9555
|
-
*
|
|
9556
|
-
*
|
|
9510
|
+
* Hand-written for the same reason `isoBmff.ts` is: `@valbuild/server` is
|
|
9511
|
+
* loaded by every app that runs Val, and what is wanted here is a handful of
|
|
9512
|
+
* integers two levels into the file:
|
|
9557
9513
|
*
|
|
9558
|
-
*
|
|
9559
|
-
*
|
|
9560
|
-
*
|
|
9561
|
-
*
|
|
9562
|
-
*
|
|
9514
|
+
* EBML the header; a file without one is not Matroska
|
|
9515
|
+
* Segment > Info TimecodeScale and Duration
|
|
9516
|
+
* Segment > Tracks > TrackEntry TrackType 1 (video) >
|
|
9517
|
+
* Video PixelWidth / PixelHeight, and DisplayWidth /
|
|
9518
|
+
* DisplayHeight when they are in pixels
|
|
9563
9519
|
*
|
|
9564
|
-
*
|
|
9565
|
-
*
|
|
9566
|
-
*
|
|
9567
|
-
|
|
9568
|
-
|
|
9569
|
-
|
|
9570
|
-
|
|
9571
|
-
|
|
9572
|
-
|
|
9573
|
-
|
|
9574
|
-
|
|
9575
|
-
|
|
9576
|
-
|
|
9577
|
-
|
|
9578
|
-
/**
|
|
9579
|
-
* Run `read` against a file on disk, reading only what it asks for. The file
|
|
9580
|
-
* is closed afterwards whatever happens.
|
|
9581
|
-
*/
|
|
9582
|
-
function withFileByteSource(absolutePath, read) {
|
|
9583
|
-
const fd = fs__default["default"].openSync(absolutePath, "r");
|
|
9584
|
-
try {
|
|
9585
|
-
const size = fs__default["default"].fstatSync(fd).size;
|
|
9586
|
-
return read({
|
|
9587
|
-
size,
|
|
9588
|
-
read: (offset, length) => {
|
|
9589
|
-
const wanted = Math.max(0, Math.min(length, size - offset));
|
|
9590
|
-
const buffer = Buffer.alloc(wanted);
|
|
9591
|
-
let filled = 0;
|
|
9592
|
-
while (filled < wanted) {
|
|
9593
|
-
const n = fs__default["default"].readSync(fd, buffer, filled, wanted - filled, offset + filled);
|
|
9594
|
-
if (n === 0) {
|
|
9595
|
-
break;
|
|
9596
|
-
}
|
|
9597
|
-
filled += n;
|
|
9598
|
-
}
|
|
9599
|
-
return buffer.subarray(0, filled);
|
|
9600
|
-
}
|
|
9601
|
-
});
|
|
9602
|
-
} finally {
|
|
9603
|
-
fs__default["default"].closeSync(fd);
|
|
9604
|
-
}
|
|
9605
|
-
}
|
|
9606
|
-
/**
|
|
9607
|
-
* A `moov` larger than this is not a header worth reading into memory. A
|
|
9608
|
-
* two-hour film's is a few MB; this is far past any real one.
|
|
9609
|
-
*/
|
|
9610
|
-
const MAX_MOOV_SIZE = 64 * 1024 * 1024;
|
|
9611
|
-
|
|
9612
|
-
/**
|
|
9613
|
-
* Whatever of {@link IsoBmffMetadata} the file declares. A field that cannot be
|
|
9614
|
-
* read is absent; nothing here throws on a file that is not ISO BMFF.
|
|
9615
|
-
*/
|
|
9616
|
-
function readIsoBmffMetadata(source) {
|
|
9617
|
-
const moov = findTopLevelBox(source, "moov");
|
|
9618
|
-
if (!moov) {
|
|
9619
|
-
return {};
|
|
9620
|
-
}
|
|
9621
|
-
const metadata = {};
|
|
9622
|
-
const mvhd = childBox(moov, "mvhd");
|
|
9623
|
-
const timescaleAndDuration = mvhd ? readMvhd(mvhd) : undefined;
|
|
9624
|
-
if (timescaleAndDuration) {
|
|
9625
|
-
const {
|
|
9626
|
-
timescale,
|
|
9627
|
-
duration
|
|
9628
|
-
} = timescaleAndDuration;
|
|
9629
|
-
if (duration > 0) {
|
|
9630
|
-
metadata.duration = duration / timescale;
|
|
9631
|
-
} else {
|
|
9632
|
-
// A fragmented file (CMAF) says 0 in mvhd and puts the length in mehd,
|
|
9633
|
-
// in the same timescale.
|
|
9634
|
-
const mehd = childBox(childBox(moov, "mvex"), "mehd");
|
|
9635
|
-
const fragmentDuration = mehd ? readMehd(mehd) : undefined;
|
|
9636
|
-
if (fragmentDuration !== undefined && fragmentDuration > 0) {
|
|
9637
|
-
metadata.duration = fragmentDuration / timescale;
|
|
9638
|
-
}
|
|
9639
|
-
}
|
|
9640
|
-
}
|
|
9641
|
-
for (const trak of childBoxes(moov, "trak")) {
|
|
9642
|
-
const hdlr = childBox(childBox(trak, "mdia"), "hdlr");
|
|
9643
|
-
if (!hdlr || hdlr.length < 12 || hdlr.toString("latin1", 8, 12) !== "vide") {
|
|
9644
|
-
continue;
|
|
9645
|
-
}
|
|
9646
|
-
const tkhd = childBox(trak, "tkhd");
|
|
9647
|
-
const size = tkhd ? readTkhd(tkhd) : undefined;
|
|
9648
|
-
if (size && size.width > 0 && size.height > 0) {
|
|
9649
|
-
metadata.width = size.width;
|
|
9650
|
-
metadata.height = size.height;
|
|
9651
|
-
break;
|
|
9652
|
-
}
|
|
9653
|
-
}
|
|
9654
|
-
return metadata;
|
|
9655
|
-
}
|
|
9656
|
-
|
|
9657
|
-
/** The payload (after the header) of the first top-level box of a type. */
|
|
9658
|
-
function findTopLevelBox(source, type) {
|
|
9659
|
-
let offset = 0;
|
|
9660
|
-
while (offset + 8 <= source.size) {
|
|
9661
|
-
const header = source.read(offset, 16);
|
|
9662
|
-
if (header.length < 8) {
|
|
9663
|
-
return undefined;
|
|
9664
|
-
}
|
|
9665
|
-
let size = header.readUInt32BE(0);
|
|
9666
|
-
const boxType = header.toString("latin1", 4, 8);
|
|
9667
|
-
let headerSize = 8;
|
|
9668
|
-
if (size === 1) {
|
|
9669
|
-
if (header.length < 16) {
|
|
9670
|
-
return undefined;
|
|
9671
|
-
}
|
|
9672
|
-
size = Number(header.readBigUInt64BE(8));
|
|
9673
|
-
headerSize = 16;
|
|
9674
|
-
} else if (size === 0) {
|
|
9675
|
-
size = source.size - offset;
|
|
9676
|
-
}
|
|
9677
|
-
if (size < headerSize) {
|
|
9678
|
-
return undefined;
|
|
9679
|
-
}
|
|
9680
|
-
if (boxType === type) {
|
|
9681
|
-
if (size - headerSize > MAX_MOOV_SIZE) {
|
|
9682
|
-
return undefined;
|
|
9683
|
-
}
|
|
9684
|
-
const payload = source.read(offset + headerSize, size - headerSize);
|
|
9685
|
-
return payload.length === size - headerSize ? payload : undefined;
|
|
9686
|
-
}
|
|
9687
|
-
offset += size;
|
|
9688
|
-
}
|
|
9689
|
-
return undefined;
|
|
9690
|
-
}
|
|
9691
|
-
|
|
9692
|
-
/** Each child box of a container payload, as `[type, payload]`. */
|
|
9693
|
-
function* boxesIn(payload) {
|
|
9694
|
-
let offset = 0;
|
|
9695
|
-
while (offset + 8 <= payload.length) {
|
|
9696
|
-
let size = payload.readUInt32BE(offset);
|
|
9697
|
-
const type = payload.toString("latin1", offset + 4, offset + 8);
|
|
9698
|
-
let headerSize = 8;
|
|
9699
|
-
if (size === 1) {
|
|
9700
|
-
if (offset + 16 > payload.length) {
|
|
9701
|
-
return;
|
|
9702
|
-
}
|
|
9703
|
-
size = Number(payload.readBigUInt64BE(offset + 8));
|
|
9704
|
-
headerSize = 16;
|
|
9705
|
-
} else if (size === 0) {
|
|
9706
|
-
size = payload.length - offset;
|
|
9707
|
-
}
|
|
9708
|
-
if (size < headerSize || offset + size > payload.length) {
|
|
9709
|
-
return;
|
|
9710
|
-
}
|
|
9711
|
-
yield [type, payload.subarray(offset + headerSize, offset + size)];
|
|
9712
|
-
offset += size;
|
|
9713
|
-
}
|
|
9714
|
-
}
|
|
9715
|
-
function childBoxes(payload, type) {
|
|
9716
|
-
if (!payload) {
|
|
9717
|
-
return [];
|
|
9718
|
-
}
|
|
9719
|
-
const found = [];
|
|
9720
|
-
for (const [boxType, child] of boxesIn(payload)) {
|
|
9721
|
-
if (boxType === type) {
|
|
9722
|
-
found.push(child);
|
|
9723
|
-
}
|
|
9724
|
-
}
|
|
9725
|
-
return found;
|
|
9726
|
-
}
|
|
9727
|
-
function childBox(payload, type) {
|
|
9728
|
-
return childBoxes(payload, type)[0];
|
|
9729
|
-
}
|
|
9730
|
-
|
|
9731
|
-
/** "Unknown" in a version 1 box. Not a literal: `0x…n` needs an ES2020 target. */
|
|
9732
|
-
const ALL_ONES_64 = BigInt("0xffffffffffffffff");
|
|
9733
|
-
function readMvhd(mvhd) {
|
|
9734
|
-
const version = mvhd[0];
|
|
9735
|
-
if (version === 1) {
|
|
9736
|
-
if (mvhd.length < 32) return undefined;
|
|
9737
|
-
const timescale = mvhd.readUInt32BE(20);
|
|
9738
|
-
const duration = mvhd.readBigUInt64BE(24);
|
|
9739
|
-
if (timescale === 0 || duration === ALL_ONES_64) {
|
|
9740
|
-
return undefined;
|
|
9741
|
-
}
|
|
9742
|
-
return {
|
|
9743
|
-
timescale,
|
|
9744
|
-
duration: Number(duration)
|
|
9745
|
-
};
|
|
9746
|
-
}
|
|
9747
|
-
if (mvhd.length < 20) return undefined;
|
|
9748
|
-
const timescale = mvhd.readUInt32BE(12);
|
|
9749
|
-
const duration = mvhd.readUInt32BE(16);
|
|
9750
|
-
// All ones means "unknown" in a version 0 box.
|
|
9751
|
-
if (timescale === 0 || duration === 0xffffffff) {
|
|
9752
|
-
return undefined;
|
|
9753
|
-
}
|
|
9754
|
-
return {
|
|
9755
|
-
timescale,
|
|
9756
|
-
duration
|
|
9757
|
-
};
|
|
9758
|
-
}
|
|
9759
|
-
function readMehd(mehd) {
|
|
9760
|
-
if (mehd[0] === 1) {
|
|
9761
|
-
return mehd.length >= 12 ? Number(mehd.readBigUInt64BE(4)) : undefined;
|
|
9762
|
-
}
|
|
9763
|
-
return mehd.length >= 8 ? mehd.readUInt32BE(4) : undefined;
|
|
9764
|
-
}
|
|
9765
|
-
|
|
9766
|
-
/**
|
|
9767
|
-
* The size a track is SHOWN at: tkhd's width and height, swapped when the
|
|
9768
|
-
* display matrix turns the picture a quarter turn — a phone records upright
|
|
9769
|
-
* video sideways and says so here, and a page lays it out as it is shown.
|
|
9770
|
-
*/
|
|
9771
|
-
function readTkhd(tkhd) {
|
|
9772
|
-
// Past the version-specific times and ids, then 16 bytes of reserved,
|
|
9773
|
-
// layer, alternate group, volume and reserved, then the 36-byte matrix.
|
|
9774
|
-
const matrixAt = tkhd[0] === 1 ? 4 + 32 + 16 : 4 + 20 + 16;
|
|
9775
|
-
const widthAt = matrixAt + 36;
|
|
9776
|
-
if (tkhd.length < widthAt + 8) {
|
|
9777
|
-
return undefined;
|
|
9778
|
-
}
|
|
9779
|
-
const a = tkhd.readInt32BE(matrixAt);
|
|
9780
|
-
const b = tkhd.readInt32BE(matrixAt + 4);
|
|
9781
|
-
const c = tkhd.readInt32BE(matrixAt + 12);
|
|
9782
|
-
const d = tkhd.readInt32BE(matrixAt + 16);
|
|
9783
|
-
const width = Math.round(tkhd.readUInt32BE(widthAt) / 0x10000);
|
|
9784
|
-
const height = Math.round(tkhd.readUInt32BE(widthAt + 4) / 0x10000);
|
|
9785
|
-
const quarterTurn = a === 0 && d === 0 && b !== 0 && c !== 0;
|
|
9786
|
-
return quarterTurn ? {
|
|
9787
|
-
width: height,
|
|
9788
|
-
height: width
|
|
9789
|
-
} : {
|
|
9790
|
-
width,
|
|
9791
|
-
height
|
|
9792
|
-
};
|
|
9793
|
-
}
|
|
9794
|
-
|
|
9795
|
-
/**
|
|
9796
|
-
* The size and length of a Matroska file (`.webm`, and `.mkv`, which is the
|
|
9797
|
-
* same container), read from its headers. Nothing is decoded.
|
|
9798
|
-
*
|
|
9799
|
-
* Hand-written for the same reason `isoBmff.ts` is: `@valbuild/server` is
|
|
9800
|
-
* loaded by every app that runs Val, and what is wanted here is a handful of
|
|
9801
|
-
* integers two levels into the file:
|
|
9802
|
-
*
|
|
9803
|
-
* EBML the header; a file without one is not Matroska
|
|
9804
|
-
* Segment > Info TimecodeScale and Duration
|
|
9805
|
-
* Segment > Tracks > TrackEntry TrackType 1 (video) >
|
|
9806
|
-
* Video PixelWidth / PixelHeight, and DisplayWidth /
|
|
9807
|
-
* DisplayHeight when they are in pixels
|
|
9808
|
-
*
|
|
9809
|
-
* Matroska is EBML: every element is a variable-length ID, a variable-length
|
|
9810
|
-
* size and a payload. Only element HEADERS are read on the way through the
|
|
9811
|
-
* Segment, and then the payloads of Info and Tracks, which are a few hundred
|
|
9812
|
-
* bytes. The Clusters — the frames, which are the file — are never read: the
|
|
9813
|
-
* walk stops at the first one.
|
|
9814
|
-
*
|
|
9815
|
-
* That is allowed because the spec (RFC 9559, section 6.3) requires the first
|
|
9816
|
-
* Info and Tracks to come before the first Cluster, or to be named by a
|
|
9817
|
-
* SeekHead that does. A file written by a browser's MediaRecorder, or by
|
|
9818
|
-
* anything else that streams, does not know its own length when it starts:
|
|
9819
|
-
* its Segment and Clusters have the "unknown" size, and its Info has no
|
|
9820
|
-
* Duration. The Segment is then read as running to the end of the file, and
|
|
9821
|
-
* the duration is left out rather than counted from the Clusters — a caller
|
|
9822
|
-
* reports it, as it does for a fragmented mp4.
|
|
9520
|
+
* Matroska is EBML: every element is a variable-length ID, a variable-length
|
|
9521
|
+
* size and a payload. Only element HEADERS are read on the way through the
|
|
9522
|
+
* Segment, and then the payloads of Info and Tracks, which are a few hundred
|
|
9523
|
+
* bytes. The Clusters — the frames, which are the file — are never read: the
|
|
9524
|
+
* walk stops at the first one.
|
|
9525
|
+
*
|
|
9526
|
+
* That is allowed because the spec (RFC 9559, section 6.3) requires the first
|
|
9527
|
+
* Info and Tracks to come before the first Cluster, or to be named by a
|
|
9528
|
+
* SeekHead that does. A file written by a browser's MediaRecorder, or by
|
|
9529
|
+
* anything else that streams, does not know its own length when it starts:
|
|
9530
|
+
* its Segment and Clusters have the "unknown" size, and its Info has no
|
|
9531
|
+
* Duration. The Segment is then read as running to the end of the file, and
|
|
9532
|
+
* the duration is left out rather than counted from the Clusters — a caller
|
|
9533
|
+
* reports it, as it does for a fragmented mp4.
|
|
9823
9534
|
*/
|
|
9824
9535
|
|
|
9825
9536
|
const EBML_HEADER = 0x1a45dfa3;
|
|
@@ -10544,7 +10255,18 @@ function getValidationErrorFileRef(validationError) {
|
|
|
10544
10255
|
}
|
|
10545
10256
|
|
|
10546
10257
|
const textEncoder$1 = new TextEncoder();
|
|
10547
|
-
async function checkRemoteRef(
|
|
10258
|
+
async function checkRemoteRef(
|
|
10259
|
+
/**
|
|
10260
|
+
* Unused: a re-derived ref keeps the host it already names (see below).
|
|
10261
|
+
* Kept so the exported signature does not change.
|
|
10262
|
+
*/
|
|
10263
|
+
_remoteHost, ref, projectRoot, schema, metadata,
|
|
10264
|
+
/**
|
|
10265
|
+
* Where the bytes come from when the ref has to be re-derived. Defaults to
|
|
10266
|
+
* a download cached under `projectRoot`; the Studio passes one that keeps
|
|
10267
|
+
* nothing on disk (`FixFiles.readRemoteFile`).
|
|
10268
|
+
*/
|
|
10269
|
+
readRemote = (ref, fileExt, fileHash) => getFileBufferFromRemote(ref, fileExt, fileHash, projectRoot)) {
|
|
10548
10270
|
const remoteRefDataRes = core.Internal.remote.splitRemoteRef(ref);
|
|
10549
10271
|
if (remoteRefDataRes.status === "error") {
|
|
10550
10272
|
return remoteRefDataRes;
|
|
@@ -10589,7 +10311,7 @@ async function checkRemoteRef(remoteHost, ref, projectRoot, schema, metadata) {
|
|
|
10589
10311
|
};
|
|
10590
10312
|
}
|
|
10591
10313
|
// Validation hash does not match, so we need to re-validate the remote content
|
|
10592
|
-
const fileBufferRes = await
|
|
10314
|
+
const fileBufferRes = await readRemote(ref, fileExt, remoteRefDataRes.fileHash);
|
|
10593
10315
|
|
|
10594
10316
|
// We could not download the remote file for some reason (for example, the file hash is wrong)
|
|
10595
10317
|
if (fileBufferRes.status === "error") {
|
|
@@ -10615,7 +10337,7 @@ async function checkRemoteRef(remoteHost, ref, projectRoot, schema, metadata) {
|
|
|
10615
10337
|
} catch (err) {
|
|
10616
10338
|
return {
|
|
10617
10339
|
status: "error",
|
|
10618
|
-
error: `Failed to extract metadata from remote file: ${err instanceof Error ? err.message : "Unknown error"}. Cached file: ${fileBufferRes.cachedFilePath}`
|
|
10340
|
+
error: `Failed to extract metadata from remote file: ${err instanceof Error ? err.message : "Unknown error"}${fileBufferRes.cachedFilePath ? `. Cached file: ${fileBufferRes.cachedFilePath}` : ""}`
|
|
10619
10341
|
};
|
|
10620
10342
|
}
|
|
10621
10343
|
const updatedMetadata = {
|
|
@@ -10634,7 +10356,11 @@ async function checkRemoteRef(remoteHost, ref, projectRoot, schema, metadata) {
|
|
|
10634
10356
|
return {
|
|
10635
10357
|
status: "fix-required",
|
|
10636
10358
|
metadata: updatedMetadata,
|
|
10637
|
-
ref
|
|
10359
|
+
// On the host the ref was already on. Re-deriving a ref changes what it
|
|
10360
|
+
// says about the file, never where it is served from: a Hosted project's
|
|
10361
|
+
// file is in R2 and served only at its own address, and moving a ref to
|
|
10362
|
+
// another host is a fix of its own (`remote:rewrite-host`).
|
|
10363
|
+
ref: core.Internal.remote.createRemoteRef(remoteRefDataRes.remoteHost, {
|
|
10638
10364
|
publicProjectId: remoteRefDataRes.projectId,
|
|
10639
10365
|
coreVersion,
|
|
10640
10366
|
bucket: remoteRefDataRes.bucket,
|
|
@@ -10653,6 +10379,12 @@ function getCachedRemoteFilePath(fileExt, currentFileHash, remoteFileCacheDir) {
|
|
|
10653
10379
|
const remoteFilePath = path__default["default"].join(remoteFileCacheDir, currentFileHash + "." + fileExt);
|
|
10654
10380
|
return remoteFilePath;
|
|
10655
10381
|
}
|
|
10382
|
+
|
|
10383
|
+
/**
|
|
10384
|
+
* A remote file's bytes, and the hash they actually have — which is the one a
|
|
10385
|
+
* re-derived ref is made from, whatever hash the old ref claimed.
|
|
10386
|
+
*/
|
|
10387
|
+
|
|
10656
10388
|
async function getFileBufferFromRemote(ref, fileExt, currentFileHash, projectRoot) {
|
|
10657
10389
|
const remoteFileCacheDir = getCachedRemoteFileDir(projectRoot);
|
|
10658
10390
|
const remoteFilePath = getCachedRemoteFilePath(fileExt, currentFileHash, remoteFileCacheDir);
|
|
@@ -10691,14 +10423,55 @@ async function getFileBufferFromRemote(ref, fileExt, currentFileHash, projectRoo
|
|
|
10691
10423
|
cachedFilePath: remoteFilePath
|
|
10692
10424
|
};
|
|
10693
10425
|
}
|
|
10426
|
+
|
|
10427
|
+
/**
|
|
10428
|
+
* Download a remote file to `filePath`, from where its ref says -- and, for a
|
|
10429
|
+
* platform ref whose host fails, from the same file on the project's own
|
|
10430
|
+
* `valstart.dev` address, which always serves it (`fallbackRefOf`). The bytes
|
|
10431
|
+
* from the fallback are checked against the ref's file hash before they are
|
|
10432
|
+
* kept: a different host answering is only safe because of that check.
|
|
10433
|
+
*
|
|
10434
|
+
* Rejects with the FIRST host's error when both fail: that is the one the ref
|
|
10435
|
+
* names, and the one a person can recognise.
|
|
10436
|
+
*/
|
|
10694
10437
|
async function downloadFileFromRemote(ref, filePath) {
|
|
10695
|
-
|
|
10696
|
-
|
|
10438
|
+
try {
|
|
10439
|
+
return await downloadFromUrl(ref, filePath);
|
|
10440
|
+
} catch (err) {
|
|
10441
|
+
const fallback = ValRouterCore.fallbackRefOf(ref);
|
|
10442
|
+
const parsed = core.Internal.remote.splitRemoteRef(ref);
|
|
10443
|
+
if (fallback === null || parsed.status === "error") {
|
|
10444
|
+
throw err;
|
|
10445
|
+
}
|
|
10446
|
+
try {
|
|
10447
|
+
const res = await downloadFromUrl(fallback, filePath);
|
|
10448
|
+
if (res.status === "error") {
|
|
10449
|
+
throw err;
|
|
10450
|
+
}
|
|
10451
|
+
} catch {
|
|
10452
|
+
throw err;
|
|
10453
|
+
}
|
|
10454
|
+
const bytes = await fs__default["default"].promises.readFile(filePath);
|
|
10455
|
+
if (core.Internal.remote.getFileHash(bytes) !== parsed.fileHash) {
|
|
10456
|
+
await fs__default["default"].promises.unlink(filePath).catch(() => undefined);
|
|
10457
|
+
throw new Error(`${ref}. Its host failed, and the file at ${fallback} is not the one the ref names.`, {
|
|
10458
|
+
cause: err
|
|
10459
|
+
});
|
|
10460
|
+
}
|
|
10461
|
+
return {
|
|
10462
|
+
status: "success",
|
|
10463
|
+
filePath
|
|
10464
|
+
};
|
|
10465
|
+
}
|
|
10466
|
+
}
|
|
10467
|
+
async function downloadFromUrl(ref, filePath) {
|
|
10468
|
+
return new Promise((resolve, reject) => {
|
|
10469
|
+
const url = new URL(ref);
|
|
10697
10470
|
const client = url.protocol === "https:" ? https__default$1["default"] : http__default$1["default"];
|
|
10698
10471
|
const request = client.get(url, response => {
|
|
10699
10472
|
if (response.statusCode && response.statusCode >= 300 && response.statusCode < 400 && response.headers.location) {
|
|
10700
10473
|
// Handle redirects
|
|
10701
|
-
return
|
|
10474
|
+
return downloadFromUrl(response.headers.location, filePath).then(resolve).catch(reject);
|
|
10702
10475
|
}
|
|
10703
10476
|
if (response.statusCode == 404) {
|
|
10704
10477
|
return reject(new Error(`${ref}. File not found. The URL is most likely incorrect.`));
|
|
@@ -10719,6 +10492,45 @@ async function downloadFileFromRemote(ref, filePath) {
|
|
|
10719
10492
|
});
|
|
10720
10493
|
}
|
|
10721
10494
|
|
|
10495
|
+
/**
|
|
10496
|
+
* Where `createFixPatch` reads a project file's bytes, and where it puts a
|
|
10497
|
+
* remote file it was asked to download.
|
|
10498
|
+
*
|
|
10499
|
+
* Every path is the file's path from the project root, the way a source names
|
|
10500
|
+
* it: `/public/val/logo.png`, never an absolute path on some disk. That is the
|
|
10501
|
+
* point: a project in http mode, or one the platform serves from a Worker, has
|
|
10502
|
+
* no disk to join a path onto, and a fix that reads a file has to work there
|
|
10503
|
+
* too. `diskFixFiles` is the one the CLI and the language server use, and does
|
|
10504
|
+
* exactly what `createFixPatch` did with `fs` before this existed.
|
|
10505
|
+
*
|
|
10506
|
+
* Not yet covered, and still read from `projectRoot` directly: the CLI's
|
|
10507
|
+
* listing of a media directory for files it does not track, and
|
|
10508
|
+
* `videos:check-all-files` (`videosetCheckAllFilesPatch`), which lists one.
|
|
10509
|
+
* See `docs/plans/studio-validate.md`.
|
|
10510
|
+
*/
|
|
10511
|
+
|
|
10512
|
+
/** {@link FixFiles} over a project on disk. */
|
|
10513
|
+
function diskFixFiles(projectRoot) {
|
|
10514
|
+
const absolute = filePath => path__default["default"].join(projectRoot, filePath);
|
|
10515
|
+
return {
|
|
10516
|
+
readFile: filePath => fs__default["default"].promises.readFile(absolute(filePath)),
|
|
10517
|
+
readVideoMetadata: filePath => extractVideoMetadataFromFile(absolute(filePath)),
|
|
10518
|
+
// Kept under `.val/remote-file-cache`, so the next run does not download
|
|
10519
|
+
// it again.
|
|
10520
|
+
readRemoteFile: (ref, fileExt, fileHash) => getFileBufferFromRemote(ref, fileExt, fileHash, projectRoot),
|
|
10521
|
+
saveRemoteFile: async (url, filePath) => {
|
|
10522
|
+
const target = absolute(filePath);
|
|
10523
|
+
await fs__default["default"].promises.mkdir(path__default["default"].dirname(target), {
|
|
10524
|
+
recursive: true
|
|
10525
|
+
});
|
|
10526
|
+
const res = await downloadFileFromRemote(url, target);
|
|
10527
|
+
return res.status === "error" ? res : {
|
|
10528
|
+
status: "success"
|
|
10529
|
+
};
|
|
10530
|
+
}
|
|
10531
|
+
};
|
|
10532
|
+
}
|
|
10533
|
+
|
|
10722
10534
|
const textEncoder = new TextEncoder();
|
|
10723
10535
|
|
|
10724
10536
|
/**
|
|
@@ -10755,6 +10567,7 @@ async function openRemoteUploadSession(ctx) {
|
|
|
10755
10567
|
let publicProjectId = ctx.publicProjectId;
|
|
10756
10568
|
let remoteFileBuckets = ctx.remoteFileBuckets;
|
|
10757
10569
|
let remoteFilesCounter = ctx.remoteFilesCounter;
|
|
10570
|
+
let remoteHost = ctx.remote.remoteHost;
|
|
10758
10571
|
if (!publicProjectId || !remoteFileBuckets) {
|
|
10759
10572
|
if (!projectName) {
|
|
10760
10573
|
return fail("Project name not found. Add project name to val.config or set the VAL_PROJECT environment variable");
|
|
@@ -10767,6 +10580,7 @@ async function openRemoteUploadSession(ctx) {
|
|
|
10767
10580
|
}
|
|
10768
10581
|
publicProjectId = settingsRes.data.publicProjectId;
|
|
10769
10582
|
remoteFileBuckets = settingsRes.data.remoteFileBuckets.map(b => b.bucket);
|
|
10583
|
+
remoteHost = settingsRes.data.remoteHost ?? remoteHost;
|
|
10770
10584
|
}
|
|
10771
10585
|
if (!publicProjectId) {
|
|
10772
10586
|
return fail("Could not get public project id");
|
|
@@ -10787,7 +10601,8 @@ async function openRemoteUploadSession(ctx) {
|
|
|
10787
10601
|
publicProjectId,
|
|
10788
10602
|
bucket,
|
|
10789
10603
|
remoteFileBuckets,
|
|
10790
|
-
remoteFilesCounter
|
|
10604
|
+
remoteFilesCounter,
|
|
10605
|
+
remoteHost
|
|
10791
10606
|
}
|
|
10792
10607
|
};
|
|
10793
10608
|
}
|
|
@@ -10809,7 +10624,7 @@ async function uploadBytesToRemote(ctx, session, relativeFilePath, fileBuffer, m
|
|
|
10809
10624
|
const fileHash = core.Internal.remote.getFileHash(fileBuffer);
|
|
10810
10625
|
const coreVersion = core.Internal.VERSION.core || "unknown";
|
|
10811
10626
|
const fileExt = ValRouterCore.getFileExt(relativeFilePath);
|
|
10812
|
-
const ref = core.Internal.remote.createRemoteRef(
|
|
10627
|
+
const ref = core.Internal.remote.createRemoteRef(session.remoteHost, {
|
|
10813
10628
|
publicProjectId: session.publicProjectId,
|
|
10814
10629
|
coreVersion,
|
|
10815
10630
|
bucket: session.bucket,
|
|
@@ -12327,7 +12142,7 @@ async function videosetCheckAllFilesPatch({
|
|
|
12327
12142
|
* `alt` and anything already there are someone's, and stay.
|
|
12328
12143
|
*/
|
|
12329
12144
|
async function videosetAddMetadataPatch({
|
|
12330
|
-
|
|
12145
|
+
files,
|
|
12331
12146
|
sourcePath,
|
|
12332
12147
|
validationError,
|
|
12333
12148
|
moduleSource,
|
|
@@ -12361,7 +12176,7 @@ async function videosetAddMetadataPatch({
|
|
|
12361
12176
|
}
|
|
12362
12177
|
let metadata;
|
|
12363
12178
|
try {
|
|
12364
|
-
metadata = galleryEntryOf(key).remote ? await extractVideoMetadataFromUrl(key, nameForTypeOf(key)) : await
|
|
12179
|
+
metadata = galleryEntryOf(key).remote ? await extractVideoMetadataFromUrl(key, nameForTypeOf(key)) : await files.readVideoMetadata(key);
|
|
12365
12180
|
} catch (err) {
|
|
12366
12181
|
return fail(`Failed to read video metadata from ${key}: ${err instanceof Error ? err.message : String(err)}`);
|
|
12367
12182
|
}
|
|
@@ -12459,9 +12274,10 @@ function mediaValueOf(value) {
|
|
|
12459
12274
|
async function createFixPatch(config, apply, sourcePath, validationError, remoteFiles, moduleSource, moduleSchema) {
|
|
12460
12275
|
const remainingErrors = [];
|
|
12461
12276
|
const patch$1 = [];
|
|
12277
|
+
const files = config.files ?? diskFixFiles(config.projectRoot);
|
|
12462
12278
|
for (const fix of validationError.fixes || []) {
|
|
12463
12279
|
if (fix === "image:check-metadata" || fix === "image:add-metadata") {
|
|
12464
|
-
const imageMetadata = await getImageMetadata(
|
|
12280
|
+
const imageMetadata = await getImageMetadata(files, validationError);
|
|
12465
12281
|
if (imageMetadata.width === undefined || imageMetadata.height === undefined) {
|
|
12466
12282
|
remainingErrors.push({
|
|
12467
12283
|
...validationError,
|
|
@@ -12528,7 +12344,7 @@ async function createFixPatch(config, apply, sourcePath, validationError, remote
|
|
|
12528
12344
|
}
|
|
12529
12345
|
}
|
|
12530
12346
|
} else if (fix === "file:add-metadata" || fix === "file:check-metadata") {
|
|
12531
|
-
const fileMetadata = await getFileMetadata(
|
|
12347
|
+
const fileMetadata = await getFileMetadata(files, validationError);
|
|
12532
12348
|
if (fileMetadata === undefined) {
|
|
12533
12349
|
remainingErrors.push({
|
|
12534
12350
|
...validationError,
|
|
@@ -12589,7 +12405,7 @@ async function createFixPatch(config, apply, sourcePath, validationError, remote
|
|
|
12589
12405
|
}
|
|
12590
12406
|
let metadata;
|
|
12591
12407
|
try {
|
|
12592
|
-
metadata = await getVideoMetadata(
|
|
12408
|
+
metadata = await getVideoMetadata(files, fileRef);
|
|
12593
12409
|
} catch (err) {
|
|
12594
12410
|
remainingErrors.push({
|
|
12595
12411
|
...validationError,
|
|
@@ -12625,7 +12441,7 @@ async function createFixPatch(config, apply, sourcePath, validationError, remote
|
|
|
12625
12441
|
// A set entry's twin of `video:add-metadata`: the file is the entry's
|
|
12626
12442
|
// KEY, and only what is missing is written.
|
|
12627
12443
|
const fixed = await videosetAddMetadataPatch({
|
|
12628
|
-
|
|
12444
|
+
files,
|
|
12629
12445
|
sourcePath,
|
|
12630
12446
|
validationError,
|
|
12631
12447
|
moduleSource,
|
|
@@ -12649,9 +12465,9 @@ async function createFixPatch(config, apply, sourcePath, validationError, remote
|
|
|
12649
12465
|
let metadata = remoteFile.metadata;
|
|
12650
12466
|
if (!metadata) {
|
|
12651
12467
|
if (fix === "image:upload-remote") {
|
|
12652
|
-
metadata = await getImageMetadata(
|
|
12468
|
+
metadata = await getImageMetadata(files, validationError);
|
|
12653
12469
|
} else if (fix === "file:upload-remote") {
|
|
12654
|
-
metadata = await getFileMetadata(
|
|
12470
|
+
metadata = await getFileMetadata(files, validationError);
|
|
12655
12471
|
}
|
|
12656
12472
|
}
|
|
12657
12473
|
if (!metadata) {
|
|
@@ -12785,11 +12601,7 @@ async function createFixPatch(config, apply, sourcePath, validationError, remote
|
|
|
12785
12601
|
});
|
|
12786
12602
|
continue;
|
|
12787
12603
|
}
|
|
12788
|
-
const
|
|
12789
|
-
await fs__default["default"].promises.mkdir(path__default["default"].dirname(absoluteFilePath), {
|
|
12790
|
-
recursive: true
|
|
12791
|
-
});
|
|
12792
|
-
const res = await downloadFileFromRemote(url, absoluteFilePath);
|
|
12604
|
+
const res = await files.saveRemoteFile(url, filePath);
|
|
12793
12605
|
if (res.status === "error") {
|
|
12794
12606
|
remainingErrors.push({
|
|
12795
12607
|
...validationError,
|
|
@@ -12825,10 +12637,9 @@ async function createFixPatch(config, apply, sourcePath, validationError, remote
|
|
|
12825
12637
|
// image as unreadable, and with `--fix` REMOVED it from the gallery.
|
|
12826
12638
|
continue;
|
|
12827
12639
|
}
|
|
12828
|
-
const filename = path__default["default"].join(config.projectRoot, entry.localPath);
|
|
12829
12640
|
let buffer;
|
|
12830
12641
|
try {
|
|
12831
|
-
buffer =
|
|
12642
|
+
buffer = await files.readFile(entry.localPath);
|
|
12832
12643
|
} catch {
|
|
12833
12644
|
if (apply) {
|
|
12834
12645
|
const removePath = patch.sourceToPatchPath(sourcePath).concat([entryKey]);
|
|
@@ -12841,16 +12652,16 @@ async function createFixPatch(config, apply, sourcePath, validationError, remote
|
|
|
12841
12652
|
} else {
|
|
12842
12653
|
remainingErrors.push({
|
|
12843
12654
|
...validationError,
|
|
12844
|
-
message: `Could not read file: ${
|
|
12655
|
+
message: `Could not read file: ${path__default["default"].join(config.projectRoot, entry.localPath)} - file might not exist or can not be accessed`,
|
|
12845
12656
|
fixes: undefined
|
|
12846
12657
|
});
|
|
12847
12658
|
}
|
|
12848
12659
|
continue;
|
|
12849
12660
|
}
|
|
12850
12661
|
if (fix === "images:check-all-files") {
|
|
12851
|
-
const actualMetadata = await extractImageMetadata(
|
|
12662
|
+
const actualMetadata = await extractImageMetadata(entry.localPath, buffer);
|
|
12852
12663
|
const stored = storedEntry;
|
|
12853
|
-
const metadataIsCorrect =
|
|
12664
|
+
const metadataIsCorrect = mediaDirEntryMismatches("images", stored, actualMetadata).length === 0;
|
|
12854
12665
|
if (!metadataIsCorrect) {
|
|
12855
12666
|
if (apply) {
|
|
12856
12667
|
patch$1.push({
|
|
@@ -12875,9 +12686,9 @@ async function createFixPatch(config, apply, sourcePath, validationError, remote
|
|
|
12875
12686
|
}
|
|
12876
12687
|
}
|
|
12877
12688
|
} else if (fix === "files:check-all-files") {
|
|
12878
|
-
const actualMetadata = await extractFileMetadata(
|
|
12689
|
+
const actualMetadata = await extractFileMetadata(entry.localPath);
|
|
12879
12690
|
const stored = storedEntry;
|
|
12880
|
-
const metadataIsCorrect = stored.
|
|
12691
|
+
const metadataIsCorrect = mediaDirEntryMismatches("files", stored, actualMetadata).length === 0;
|
|
12881
12692
|
if (!metadataIsCorrect) {
|
|
12882
12693
|
if (apply) {
|
|
12883
12694
|
patch$1.push({
|
|
@@ -12935,7 +12746,27 @@ async function createFixPatch(config, apply, sourcePath, validationError, remote
|
|
|
12935
12746
|
schema: schemaAtPath
|
|
12936
12747
|
} = core.Internal.resolvePath(modulePath, moduleSource, moduleSchema);
|
|
12937
12748
|
if (schemaAtPath.type === "image" || schemaAtPath.type === "file") {
|
|
12938
|
-
|
|
12749
|
+
// First, whether the ref's host still serves its project at all: a
|
|
12750
|
+
// ref on a domain the project dropped is pointed at the project's own
|
|
12751
|
+
// address, not re-derived from bytes nobody can download from there.
|
|
12752
|
+
const moved = await ValRouterCore.movedRemoteHost(v.path, config.askHostProject);
|
|
12753
|
+
if (moved) {
|
|
12754
|
+
if (apply) {
|
|
12755
|
+
patch$1.push({
|
|
12756
|
+
op: "replace",
|
|
12757
|
+
path: patch.sourceToPatchPath(sourcePath).concat(["path"]),
|
|
12758
|
+
value: moved.ref
|
|
12759
|
+
});
|
|
12760
|
+
} else {
|
|
12761
|
+
remainingErrors.push({
|
|
12762
|
+
...validationError,
|
|
12763
|
+
message: `${moved.why}. Run 'val validate --fix' to point it at ${moved.to}, which always serves it.`,
|
|
12764
|
+
fixes: ["remote:rewrite-host"]
|
|
12765
|
+
});
|
|
12766
|
+
}
|
|
12767
|
+
continue;
|
|
12768
|
+
}
|
|
12769
|
+
const res = await checkRemoteRef(config.remoteHost, v.path, config.projectRoot, schemaAtPath, v.metadata, files.readRemoteFile);
|
|
12939
12770
|
if (res.status === "success") ; else if (res.status === "error") {
|
|
12940
12771
|
remainingErrors.push({
|
|
12941
12772
|
...validationError,
|
|
@@ -12964,162 +12795,966 @@ async function createFixPatch(config, apply, sourcePath, validationError, remote
|
|
|
12964
12795
|
fixes: undefined
|
|
12965
12796
|
});
|
|
12966
12797
|
}
|
|
12967
|
-
} else {
|
|
12968
|
-
remainingErrors.push({
|
|
12969
|
-
...validationError,
|
|
12970
|
-
message: "Could not check remote ref: schema type is not image or file: " + (schemaAtPath === null || schemaAtPath === void 0 ? void 0 : schemaAtPath.type),
|
|
12971
|
-
fixes: undefined
|
|
12972
|
-
});
|
|
12798
|
+
} else {
|
|
12799
|
+
remainingErrors.push({
|
|
12800
|
+
...validationError,
|
|
12801
|
+
message: "Could not check remote ref: schema type is not image or file: " + (schemaAtPath === null || schemaAtPath === void 0 ? void 0 : schemaAtPath.type),
|
|
12802
|
+
fixes: undefined
|
|
12803
|
+
});
|
|
12804
|
+
}
|
|
12805
|
+
} else if (fix === "remote:rewrite-host") {
|
|
12806
|
+
// The new ref is derived from the old one alone, so nothing the error
|
|
12807
|
+
// carries is trusted: the value is read from the module, and only a
|
|
12808
|
+
// platform ref that is not already on the project's own address moves.
|
|
12809
|
+
const [, modulePath] = core.Internal.splitModuleFilePathAndModulePath(sourcePath);
|
|
12810
|
+
const current = moduleSource !== undefined && moduleSchema !== undefined ? core.Internal.resolvePath(modulePath, moduleSource, moduleSchema).source : undefined;
|
|
12811
|
+
const ref = typeof current === "object" && current !== null && "path" in current && typeof current.path === "string" ? current.path : undefined;
|
|
12812
|
+
const parsed = ref === undefined ? undefined : core.Internal.remote.splitRemoteRef(ref);
|
|
12813
|
+
const rewritten = ref !== undefined && (parsed === null || parsed === void 0 ? void 0 : parsed.status) === "success" && ValRouterCore.needsHostCheck(parsed.remoteHost, parsed.projectId) ? ValRouterCore.rewriteRefHost(ref, ValRouterCore.fallbackRemoteHost(parsed.projectId)) : null;
|
|
12814
|
+
if (rewritten === null) {
|
|
12815
|
+
remainingErrors.push({
|
|
12816
|
+
...validationError,
|
|
12817
|
+
message: "Cannot point this at the project's own address: it is not a platform ref on another host",
|
|
12818
|
+
fixes: undefined
|
|
12819
|
+
});
|
|
12820
|
+
continue;
|
|
12821
|
+
}
|
|
12822
|
+
patch$1.push({
|
|
12823
|
+
op: "replace",
|
|
12824
|
+
path: patch.sourceToPatchPath(sourcePath).concat(["path"]),
|
|
12825
|
+
value: rewritten
|
|
12826
|
+
});
|
|
12827
|
+
} else if (fix === "view:check-module") {
|
|
12828
|
+
// The schema is the authority: it names the module, and there is exactly
|
|
12829
|
+
// one valid pointer. So this is written rather than reported — nobody has
|
|
12830
|
+
// a decision to make here.
|
|
12831
|
+
const [, modulePath] = core.Internal.splitModuleFilePathAndModulePath(sourcePath);
|
|
12832
|
+
if (moduleSource === undefined || moduleSchema === undefined) {
|
|
12833
|
+
remainingErrors.push({
|
|
12834
|
+
...validationError,
|
|
12835
|
+
message: "Unexpected error while checking a view (no module source or schema)",
|
|
12836
|
+
fixes: undefined
|
|
12837
|
+
});
|
|
12838
|
+
continue;
|
|
12839
|
+
}
|
|
12840
|
+
const {
|
|
12841
|
+
schema: schemaAtPath
|
|
12842
|
+
} = core.Internal.resolvePath(modulePath, moduleSource, moduleSchema);
|
|
12843
|
+
if (schemaAtPath.type !== "view") {
|
|
12844
|
+
remainingErrors.push({
|
|
12845
|
+
...validationError,
|
|
12846
|
+
message: `Could not fix view: schema at ${sourcePath} is '${schemaAtPath.type}', not a view`,
|
|
12847
|
+
fixes: undefined
|
|
12848
|
+
});
|
|
12849
|
+
continue;
|
|
12850
|
+
}
|
|
12851
|
+
if (apply) {
|
|
12852
|
+
patch$1.push({
|
|
12853
|
+
op: "replace",
|
|
12854
|
+
path: core.Internal.createPatchPath(modulePath),
|
|
12855
|
+
value: {
|
|
12856
|
+
view: schemaAtPath.moduleFilePath
|
|
12857
|
+
}
|
|
12858
|
+
});
|
|
12859
|
+
} else {
|
|
12860
|
+
remainingErrors.push({
|
|
12861
|
+
...validationError,
|
|
12862
|
+
message: `This view points at the wrong module. Expected '${schemaAtPath.moduleFilePath}'.`
|
|
12863
|
+
});
|
|
12864
|
+
}
|
|
12865
|
+
}
|
|
12866
|
+
}
|
|
12867
|
+
if (!validationError.fixes || validationError.fixes.length === 0) {
|
|
12868
|
+
remainingErrors.push(validationError);
|
|
12869
|
+
}
|
|
12870
|
+
return {
|
|
12871
|
+
patch: patch$1,
|
|
12872
|
+
remainingErrors
|
|
12873
|
+
};
|
|
12874
|
+
}
|
|
12875
|
+
function getRemoteValueFromValidationError(v) {
|
|
12876
|
+
if (v.value && typeof v.value !== "object") {
|
|
12877
|
+
return {
|
|
12878
|
+
success: false,
|
|
12879
|
+
message: "Unexpected error while checking remote (not an object)"
|
|
12880
|
+
};
|
|
12881
|
+
}
|
|
12882
|
+
if (!v.value) {
|
|
12883
|
+
return {
|
|
12884
|
+
success: false,
|
|
12885
|
+
message: "Unexpected error while checking remote (no value)"
|
|
12886
|
+
};
|
|
12887
|
+
}
|
|
12888
|
+
if (typeof v.value !== "object" || v.value === null || !("path" in v.value)) {
|
|
12889
|
+
return {
|
|
12890
|
+
success: false,
|
|
12891
|
+
message: "Unexpected error while checking remote (no path in value)"
|
|
12892
|
+
};
|
|
12893
|
+
}
|
|
12894
|
+
if (typeof v.value.path !== "string") {
|
|
12895
|
+
return {
|
|
12896
|
+
success: false,
|
|
12897
|
+
message: "Unexpected error while checking remote (path is not a string)"
|
|
12898
|
+
};
|
|
12899
|
+
}
|
|
12900
|
+
// Everything but the path is what Val read from the bytes.
|
|
12901
|
+
const {
|
|
12902
|
+
path,
|
|
12903
|
+
...metadata
|
|
12904
|
+
} = v.value;
|
|
12905
|
+
return {
|
|
12906
|
+
success: true,
|
|
12907
|
+
path,
|
|
12908
|
+
metadata
|
|
12909
|
+
};
|
|
12910
|
+
}
|
|
12911
|
+
|
|
12912
|
+
/** The fields of a video that are read from its bytes, in the order written. */
|
|
12913
|
+
const VIDEO_METADATA_FIELDS = ["mimeType", "width", "height", "duration"];
|
|
12914
|
+
|
|
12915
|
+
/**
|
|
12916
|
+
* The video a `video:add-metadata` error is about, read from the module at its
|
|
12917
|
+
* path when the module is at hand — the schema there decides that it IS a
|
|
12918
|
+
* video — and from the error's own value otherwise.
|
|
12919
|
+
*/
|
|
12920
|
+
function currentVideoValue(sourcePath, validationError, moduleSource, moduleSchema) {
|
|
12921
|
+
if (moduleSource !== undefined && moduleSchema !== undefined) {
|
|
12922
|
+
const [, modulePath] = core.Internal.splitModuleFilePathAndModulePath(sourcePath);
|
|
12923
|
+
const resolved = core.Internal.resolvePath(modulePath, moduleSource, moduleSchema);
|
|
12924
|
+
if (resolved.schema.type !== "video") {
|
|
12925
|
+
return {
|
|
12926
|
+
status: "error",
|
|
12927
|
+
message: `Could not add video metadata: schema at ${sourcePath} is '${resolved.schema.type}', not a video`
|
|
12928
|
+
};
|
|
12929
|
+
}
|
|
12930
|
+
const value = mediaValueOf(resolved.source);
|
|
12931
|
+
return value ? {
|
|
12932
|
+
status: "ok",
|
|
12933
|
+
value
|
|
12934
|
+
} : {
|
|
12935
|
+
status: "error",
|
|
12936
|
+
message: "Video is not an object!"
|
|
12937
|
+
};
|
|
12938
|
+
}
|
|
12939
|
+
const value = mediaValueOf(validationError.value);
|
|
12940
|
+
return value ? {
|
|
12941
|
+
status: "ok",
|
|
12942
|
+
value
|
|
12943
|
+
} : {
|
|
12944
|
+
status: "error",
|
|
12945
|
+
message: "Video is not an object!"
|
|
12946
|
+
};
|
|
12947
|
+
}
|
|
12948
|
+
async function getVideoMetadata(files, fileRef) {
|
|
12949
|
+
// Not read into memory: only the headers are, and a video is mostly not
|
|
12950
|
+
// headers. A remote video's headers are read over HTTP, with Range
|
|
12951
|
+
// requests, so it is not downloaded either.
|
|
12952
|
+
if (core.Internal.isRemoteMediaPath(fileRef)) {
|
|
12953
|
+
return extractVideoMetadataFromUrl(fileRef, nameForTypeOf(fileRef));
|
|
12954
|
+
}
|
|
12955
|
+
return files.readVideoMetadata(fileRef);
|
|
12956
|
+
}
|
|
12957
|
+
async function getImageMetadata(files, validationError) {
|
|
12958
|
+
const fileRef = getValidationErrorFileRef(validationError);
|
|
12959
|
+
if (!fileRef) {
|
|
12960
|
+
// TODO:
|
|
12961
|
+
throw Error("Cannot fix image without a file reference");
|
|
12962
|
+
}
|
|
12963
|
+
return extractImageMetadata(fileRef, await files.readFile(fileRef));
|
|
12964
|
+
}
|
|
12965
|
+
async function getFileMetadata(files, validationError) {
|
|
12966
|
+
const fileRef = getValidationErrorFileRef(validationError);
|
|
12967
|
+
if (!fileRef) {
|
|
12968
|
+
// TODO:
|
|
12969
|
+
throw Error("Cannot fix file without a file reference");
|
|
12970
|
+
}
|
|
12971
|
+
return extractFileMetadata(fileRef, await files.readFile(fileRef));
|
|
12972
|
+
}
|
|
12973
|
+
|
|
12974
|
+
/**
|
|
12975
|
+
* What an image or file directory's entry records that its bytes disagree
|
|
12976
|
+
* with, as `[field, recorded, actual]`. The one comparison: the CLI's
|
|
12977
|
+
* `*:check-all-files` fix and the Studio's full check both ask it.
|
|
12978
|
+
*/
|
|
12979
|
+
function mediaDirEntryMismatches(type, stored, actual) {
|
|
12980
|
+
const fields = type === "images" ? ["width", "height", "mimeType"] : ["mimeType"];
|
|
12981
|
+
return fields.filter(field => stored[field] !== actual[field]).map(field => [field, stored[field], actual[field]]);
|
|
12982
|
+
}
|
|
12983
|
+
|
|
12984
|
+
/**
|
|
12985
|
+
* The Studio's full check, one module at a time: what `/validate` and
|
|
12986
|
+
* `/validate/fix` answer with. See `docs/plans/studio-validate.md`.
|
|
12987
|
+
*
|
|
12988
|
+
* Both take the sources the caller is looking at — base plus the patches it
|
|
12989
|
+
* names — so an error is about the content on the editor's screen, and a fix
|
|
12990
|
+
* is a patch on top of it.
|
|
12991
|
+
*/
|
|
12992
|
+
|
|
12993
|
+
/**
|
|
12994
|
+
* Every error in one module: its sources against its schema, and its local
|
|
12995
|
+
* files against their bytes.
|
|
12996
|
+
*
|
|
12997
|
+
* Not filtered the way the publish gate filters (`partitionValidationErrors`):
|
|
12998
|
+
* the errors the gate hides because "the server repairs them on save" are the
|
|
12999
|
+
* ones this check exists to show, since nothing repairs them in http mode.
|
|
13000
|
+
*
|
|
13001
|
+
* Remote files are checked too: a stale ref is an error with a fix, and a
|
|
13002
|
+
* host that does not serve them is reported once, as a host, rather than as
|
|
13003
|
+
* an error on every image it holds (`checkRemoteFiles`).
|
|
13004
|
+
*
|
|
13005
|
+
* Not yet here: the media directories' own entries, step 5 of the plan.
|
|
13006
|
+
*/
|
|
13007
|
+
async function checkModuleForStudio(ops, input) {
|
|
13008
|
+
const {
|
|
13009
|
+
moduleFilePath,
|
|
13010
|
+
schemas,
|
|
13011
|
+
sources
|
|
13012
|
+
} = input;
|
|
13013
|
+
if (!schemas[moduleFilePath]) {
|
|
13014
|
+
return {
|
|
13015
|
+
status: "unknown-module"
|
|
13016
|
+
};
|
|
13017
|
+
}
|
|
13018
|
+
const validation = await ops.validateSources(schemas, sources, undefined, [moduleFilePath]);
|
|
13019
|
+
const moduleErrors = validation.errors[moduleFilePath];
|
|
13020
|
+
if (moduleErrors !== null && moduleErrors !== void 0 && moduleErrors.invalidSource) {
|
|
13021
|
+
return {
|
|
13022
|
+
status: "invalid-source",
|
|
13023
|
+
message: moduleErrors.invalidSource.message
|
|
13024
|
+
};
|
|
13025
|
+
}
|
|
13026
|
+
const errors = {};
|
|
13027
|
+
const add = (sourcePath, found) => {
|
|
13028
|
+
errors[sourcePath] = (errors[sourcePath] ?? []).concat(found);
|
|
13029
|
+
};
|
|
13030
|
+
for (const [sourcePath, found] of Object.entries((moduleErrors === null || moduleErrors === void 0 ? void 0 : moduleErrors.validations) ?? {})) {
|
|
13031
|
+
add(sourcePath, found);
|
|
13032
|
+
}
|
|
13033
|
+
// `validateSources` hands back every local file whose metadata it could
|
|
13034
|
+
// not judge without the bytes — which is every image that has metadata at
|
|
13035
|
+
// all. Reading them is what turns "has metadata" into "has the RIGHT
|
|
13036
|
+
// metadata".
|
|
13037
|
+
const fileErrors = await ops.validateFiles(schemas, sources, validation.files, input.fileLastUpdatedByPatchId);
|
|
13038
|
+
for (const [sourcePath, found] of Object.entries(fileErrors)) {
|
|
13039
|
+
add(sourcePath, found);
|
|
13040
|
+
}
|
|
13041
|
+
const remote = await ops.validateRemoteFiles(schemas, sources, validation.remoteFiles, input.fileLastUpdatedByPatchId, input.network);
|
|
13042
|
+
for (const [sourcePath, found] of Object.entries(remote.errors)) {
|
|
13043
|
+
add(sourcePath, found);
|
|
13044
|
+
}
|
|
13045
|
+
const mediaDir = await checkMediaDirEntries(ops, input);
|
|
13046
|
+
for (const [sourcePath, found] of Object.entries(mediaDir.errors)) {
|
|
13047
|
+
add(sourcePath, found);
|
|
13048
|
+
}
|
|
13049
|
+
return {
|
|
13050
|
+
status: "ok",
|
|
13051
|
+
errors,
|
|
13052
|
+
remoteHosts: [...remote.hosts, ...mediaDir.hosts]
|
|
13053
|
+
};
|
|
13054
|
+
}
|
|
13055
|
+
|
|
13056
|
+
/** A module that is an `s.imagedir()`, `s.filedir()` or `s.videodir()`. */
|
|
13057
|
+
|
|
13058
|
+
function mediaDirOf(input) {
|
|
13059
|
+
var _input$schemas$input$;
|
|
13060
|
+
const schema = (_input$schemas$input$ = input.schemas[input.moduleFilePath]) === null || _input$schemas$input$ === void 0 ? void 0 : _input$schemas$input$["executeSerialize"]();
|
|
13061
|
+
const source = input.sources[input.moduleFilePath];
|
|
13062
|
+
if ((schema === null || schema === void 0 ? void 0 : schema.type) !== "record" || !schema.mediaType || typeof schema.dir !== "string" || !isEntries(source)) {
|
|
13063
|
+
return undefined;
|
|
13064
|
+
}
|
|
13065
|
+
return {
|
|
13066
|
+
type: schema.mediaType,
|
|
13067
|
+
dir: schema.dir,
|
|
13068
|
+
entries: source
|
|
13069
|
+
};
|
|
13070
|
+
}
|
|
13071
|
+
function isEntries(source) {
|
|
13072
|
+
return typeof source === "object" && source !== null && !Array.isArray(source);
|
|
13073
|
+
}
|
|
13074
|
+
|
|
13075
|
+
/**
|
|
13076
|
+
* A media directory's own entries against their files: what
|
|
13077
|
+
* `*:check-all-files` asks, minus the listing of the directory on disk.
|
|
13078
|
+
*
|
|
13079
|
+
* Only what the directory TRACKS is checked. Listing the directory for files
|
|
13080
|
+
* it does not track stays the CLI's: in a managed project nothing puts a file
|
|
13081
|
+
* there but the directory itself, and in a connected one the files are in the
|
|
13082
|
+
* repository, where `val validate` sees them. See
|
|
13083
|
+
* `docs/plans/studio-validate.md`, "Media directories are checked by what
|
|
13084
|
+
* they track".
|
|
13085
|
+
*
|
|
13086
|
+
* - **A local entry** is read through the ops, from the pending patch that
|
|
13087
|
+
* wrote it if there is one. Gone: the row's **Fix** removes the entry. Wrong
|
|
13088
|
+
* size or type: the row's **Fix** writes what the bytes say. Both are
|
|
13089
|
+
* `*:check-all-files`, built for that one entry (see `fixForStudio`).
|
|
13090
|
+
* - **A video entry** is only asked whether its file is there, and gets no
|
|
13091
|
+
* fix: what a videodir records is read in the browser from the upload, and
|
|
13092
|
+
* is never compared with the file (`videosetCheckAllFilesPatch` says why).
|
|
13093
|
+
* - **A remote entry** is asked of its host, as any remote file is, but only
|
|
13094
|
+
* whether the host has it (`existenceOnly`).
|
|
13095
|
+
*/
|
|
13096
|
+
async function checkMediaDirEntries(ops, input, onlyKey) {
|
|
13097
|
+
const errors = {};
|
|
13098
|
+
const mediaDir = mediaDirOf(input);
|
|
13099
|
+
if (!mediaDir) {
|
|
13100
|
+
return {
|
|
13101
|
+
errors,
|
|
13102
|
+
hosts: []
|
|
13103
|
+
};
|
|
13104
|
+
}
|
|
13105
|
+
const files = opsFixFiles(ops, input.fileLastUpdatedByPatchId);
|
|
13106
|
+
const remoteEntries = {};
|
|
13107
|
+
const type = mediaDir.type;
|
|
13108
|
+
const keys = Object.keys(mediaDir.entries).filter(key => onlyKey === undefined || key === onlyKey).sort();
|
|
13109
|
+
for (const key of keys) {
|
|
13110
|
+
const entryPath = core.Internal.createValPathOfItem(input.moduleFilePath, key);
|
|
13111
|
+
if (!entryPath) {
|
|
13112
|
+
continue;
|
|
13113
|
+
}
|
|
13114
|
+
const entry = galleryEntryOf(key);
|
|
13115
|
+
if (entry.remote) {
|
|
13116
|
+
remoteEntries[entryPath] = {
|
|
13117
|
+
path: key
|
|
13118
|
+
};
|
|
13119
|
+
continue;
|
|
13120
|
+
}
|
|
13121
|
+
if (type === "videos") {
|
|
13122
|
+
// A video: there or not, and nothing else. See above.
|
|
13123
|
+
const fromPatch = input.fileLastUpdatedByPatchId[entry.localPath] ?? null;
|
|
13124
|
+
const reader = await ops.openBinaryFile(entry.localPath, fromPatch);
|
|
13125
|
+
if (reader === null) {
|
|
13126
|
+
errors[entryPath] = [{
|
|
13127
|
+
message: "The file is gone, but the directory still lists it. Remove it from the directory, or put the file back.",
|
|
13128
|
+
keyError: true
|
|
13129
|
+
}];
|
|
13130
|
+
}
|
|
13131
|
+
continue;
|
|
13132
|
+
}
|
|
13133
|
+
const checkFix = type === "images" ? "images:check-all-files" : "files:check-all-files";
|
|
13134
|
+
let buffer;
|
|
13135
|
+
try {
|
|
13136
|
+
buffer = await files.readFile(entry.localPath);
|
|
13137
|
+
} catch {
|
|
13138
|
+
errors[entryPath] = [{
|
|
13139
|
+
message: "The file is gone, but the directory still lists it. Fix removes it from the directory.",
|
|
13140
|
+
keyError: true,
|
|
13141
|
+
fixes: [checkFix]
|
|
13142
|
+
}];
|
|
13143
|
+
continue;
|
|
13144
|
+
}
|
|
13145
|
+
const stored = mediaDir.entries[key];
|
|
13146
|
+
if (typeof stored !== "object" || stored === null) {
|
|
13147
|
+
// The record schema already reports an entry that is not an object.
|
|
13148
|
+
continue;
|
|
13149
|
+
}
|
|
13150
|
+
const actual = type === "images" ? await extractImageMetadata(entry.localPath, buffer) : await extractFileMetadata(entry.localPath);
|
|
13151
|
+
const mismatches = mediaDirEntryMismatches(type, {
|
|
13152
|
+
...stored
|
|
13153
|
+
}, actual);
|
|
13154
|
+
if (mismatches.length > 0) {
|
|
13155
|
+
errors[entryPath] = [{
|
|
13156
|
+
message: `Does not match the file: ${mismatches.map(([field, recorded, read]) => `${field} ${String(recorded ?? "missing")} → ${String(read)}`).join(", ")}.`,
|
|
13157
|
+
keyError: true,
|
|
13158
|
+
fixes: [checkFix]
|
|
13159
|
+
}];
|
|
13160
|
+
}
|
|
13161
|
+
}
|
|
13162
|
+
if (Object.keys(remoteEntries).length === 0) {
|
|
13163
|
+
return {
|
|
13164
|
+
errors,
|
|
13165
|
+
hosts: []
|
|
13166
|
+
};
|
|
13167
|
+
}
|
|
13168
|
+
const sampleByHost = ops.sourceMode() === "managed";
|
|
13169
|
+
const published = sampleByHost ? (await ops.getSources()).sources[input.moduleFilePath] : undefined;
|
|
13170
|
+
const remote = await ValRouterCore.checkRemoteFiles({
|
|
13171
|
+
remoteFiles: remoteEntries,
|
|
13172
|
+
existenceOnly: true,
|
|
13173
|
+
schemaAt: () => undefined,
|
|
13174
|
+
// A directory's entry IS its key: published when the published directory
|
|
13175
|
+
// has the same key.
|
|
13176
|
+
isPublished: (_, ref) => typeof published === "object" && published !== null && !Array.isArray(published) && ref in published,
|
|
13177
|
+
pendingBytes: ops.pendingBytesOf(input.fileLastUpdatedByPatchId),
|
|
13178
|
+
sampleByHost,
|
|
13179
|
+
...input.network
|
|
13180
|
+
});
|
|
13181
|
+
for (const [path, found] of Object.entries(remote.errors)) {
|
|
13182
|
+
errors[path] = (errors[path] ?? []).concat(found.map(error => ({
|
|
13183
|
+
...error,
|
|
13184
|
+
keyError: true
|
|
13185
|
+
})));
|
|
13186
|
+
}
|
|
13187
|
+
return {
|
|
13188
|
+
errors,
|
|
13189
|
+
hosts: remote.hosts
|
|
13190
|
+
};
|
|
13191
|
+
}
|
|
13192
|
+
/**
|
|
13193
|
+
* The patch that fixes one error, built from the server's own reading of the
|
|
13194
|
+
* module rather than from the error the caller saw.
|
|
13195
|
+
*
|
|
13196
|
+
* The caller names the error by where it is and which fix it wants; the error
|
|
13197
|
+
* itself is found again here. That is what keeps a stale or hand-written
|
|
13198
|
+
* request from steering `createFixPatch` (CLAUDE.md, "Adding a ValidationFix
|
|
13199
|
+
* code"), and it is what lets an error reported at a FIELD be fixed at the
|
|
13200
|
+
* media value it belongs to: `validateFiles` reports a wrong width at
|
|
13201
|
+
* `…"hero"."width"`, while the fix rewrites `…"hero"`.
|
|
13202
|
+
*
|
|
13203
|
+
* Only {@link isStudioFix} codes are built. The others need a disk or a
|
|
13204
|
+
* remote host this server may not have, and are the CLI's for now.
|
|
13205
|
+
*/
|
|
13206
|
+
async function fixForStudio(ops, input) {
|
|
13207
|
+
var _validation$errors$mo, _input$network;
|
|
13208
|
+
const {
|
|
13209
|
+
moduleFilePath,
|
|
13210
|
+
schemas,
|
|
13211
|
+
sources,
|
|
13212
|
+
fix
|
|
13213
|
+
} = input;
|
|
13214
|
+
if (!internal.isStudioFix(fix)) {
|
|
13215
|
+
return {
|
|
13216
|
+
status: "not-in-studio"
|
|
13217
|
+
};
|
|
13218
|
+
}
|
|
13219
|
+
const schema = schemas[moduleFilePath];
|
|
13220
|
+
if (!schema) {
|
|
13221
|
+
return {
|
|
13222
|
+
status: "unknown-module"
|
|
13223
|
+
};
|
|
13224
|
+
}
|
|
13225
|
+
const validation = await ops.validateSources(schemas, sources, undefined, [moduleFilePath]);
|
|
13226
|
+
const found = await findError(ops, ((_validation$errors$mo = validation.errors[moduleFilePath]) === null || _validation$errors$mo === void 0 ? void 0 : _validation$errors$mo.validations) ?? {}, validation.files, validation.remoteFiles, input);
|
|
13227
|
+
if (!found) {
|
|
13228
|
+
return {
|
|
13229
|
+
status: "not-found"
|
|
13230
|
+
};
|
|
13231
|
+
}
|
|
13232
|
+
const res = await createFixPatch({
|
|
13233
|
+
// Nothing a Studio fix runs reads `projectRoot`: every byte goes through
|
|
13234
|
+
// `files`. An empty root means a fix that slipped past that fails to
|
|
13235
|
+
// find its file rather than reading one off this server's disk.
|
|
13236
|
+
projectRoot: "",
|
|
13237
|
+
// A re-derived ref stays on the host it was on. Moving refs to another
|
|
13238
|
+
// host is its own fix (step 6 of the plan), not a side effect of this.
|
|
13239
|
+
remoteHost: found.remoteHost ?? core.DEFAULT_VAL_REMOTE_HOST,
|
|
13240
|
+
files: opsFixFiles(ops, input.fileLastUpdatedByPatchId),
|
|
13241
|
+
askHostProject: (_input$network = input.network) === null || _input$network === void 0 ? void 0 : _input$network.askHostProject
|
|
13242
|
+
}, true, found.sourcePath, found.error, {}, found.moduleSource ?? sources[moduleFilePath], schema["executeSerialize"]());
|
|
13243
|
+
if (!res) {
|
|
13244
|
+
return {
|
|
13245
|
+
status: "not-found"
|
|
13246
|
+
};
|
|
13247
|
+
}
|
|
13248
|
+
return {
|
|
13249
|
+
status: "ok",
|
|
13250
|
+
patch: res.patch,
|
|
13251
|
+
remainingErrors: res.remainingErrors
|
|
13252
|
+
};
|
|
13253
|
+
}
|
|
13254
|
+
async function findError(ops, validations, files, remoteFiles, input) {
|
|
13255
|
+
var _validations$sourcePa;
|
|
13256
|
+
const {
|
|
13257
|
+
sourcePath,
|
|
13258
|
+
fix
|
|
13259
|
+
} = input;
|
|
13260
|
+
const reported = (_validations$sourcePa = validations[sourcePath]) === null || _validations$sourcePa === void 0 ? void 0 : _validations$sourcePa.find(error => {
|
|
13261
|
+
var _error$fixes;
|
|
13262
|
+
return (_error$fixes = error.fixes) === null || _error$fixes === void 0 ? void 0 : _error$fixes.includes(fix);
|
|
13263
|
+
});
|
|
13264
|
+
if (reported) {
|
|
13265
|
+
return {
|
|
13266
|
+
sourcePath,
|
|
13267
|
+
error: reported
|
|
13268
|
+
};
|
|
13269
|
+
}
|
|
13270
|
+
if (fix === "images:check-all-files" || fix === "files:check-all-files") {
|
|
13271
|
+
var _checked$errors$sourc;
|
|
13272
|
+
// One entry, checked again on its own: a row whose file someone has since
|
|
13273
|
+
// put back, or whose entry someone already fixed, is "not found".
|
|
13274
|
+
const mediaDir = mediaDirOf(input);
|
|
13275
|
+
const [, modulePath] = core.Internal.splitModuleFilePathAndModulePath(sourcePath);
|
|
13276
|
+
const [key, ...rest] = modulePath ? core.Internal.splitModulePath(modulePath) : [];
|
|
13277
|
+
if (!mediaDir || key === undefined || rest.length > 0) {
|
|
13278
|
+
return undefined;
|
|
13279
|
+
}
|
|
13280
|
+
const checked = await checkMediaDirEntries(ops, input, key);
|
|
13281
|
+
const error = (_checked$errors$sourc = checked.errors[sourcePath]) === null || _checked$errors$sourc === void 0 ? void 0 : _checked$errors$sourc.find(candidate => {
|
|
13282
|
+
var _candidate$fixes;
|
|
13283
|
+
return (_candidate$fixes = candidate.fixes) === null || _candidate$fixes === void 0 ? void 0 : _candidate$fixes.includes(fix);
|
|
13284
|
+
});
|
|
13285
|
+
if (!error) {
|
|
13286
|
+
return undefined;
|
|
13287
|
+
}
|
|
13288
|
+
return {
|
|
13289
|
+
// `createFixPatch` fixes a directory at its root, every entry it is
|
|
13290
|
+
// handed: so it is handed this one.
|
|
13291
|
+
sourcePath: core.Internal.parentOfSourcePath(sourcePath),
|
|
13292
|
+
error: {
|
|
13293
|
+
message: error.message,
|
|
13294
|
+
value: {
|
|
13295
|
+
dir: mediaDir.dir,
|
|
13296
|
+
type: mediaDir.type
|
|
13297
|
+
},
|
|
13298
|
+
fixes: [fix]
|
|
13299
|
+
},
|
|
13300
|
+
moduleSource: {
|
|
13301
|
+
[key]: mediaDir.entries[key]
|
|
13302
|
+
}
|
|
13303
|
+
};
|
|
13304
|
+
}
|
|
13305
|
+
if (fix === "image:check-remote" || fix === "file:check-remote" || fix === "remote:rewrite-host") {
|
|
13306
|
+
var _checked$errors$sourc2;
|
|
13307
|
+
// Every remote value is in `remoteFiles`, sound or not, so check it again
|
|
13308
|
+
// before rewriting it: a ref someone already fixed, or whose host serves
|
|
13309
|
+
// the project again, is "not found", not a download and an identical ref.
|
|
13310
|
+
const value = remoteFiles[sourcePath];
|
|
13311
|
+
if (!value) {
|
|
13312
|
+
return undefined;
|
|
13313
|
+
}
|
|
13314
|
+
const checked = await ops.validateRemoteFiles(input.schemas, input.sources, {
|
|
13315
|
+
[sourcePath]: value
|
|
13316
|
+
}, input.fileLastUpdatedByPatchId, input.network);
|
|
13317
|
+
const found = (_checked$errors$sourc2 = checked.errors[sourcePath]) === null || _checked$errors$sourc2 === void 0 ? void 0 : _checked$errors$sourc2.find(error => {
|
|
13318
|
+
var _error$fixes2;
|
|
13319
|
+
return (_error$fixes2 = error.fixes) === null || _error$fixes2 === void 0 ? void 0 : _error$fixes2.includes(fix);
|
|
13320
|
+
});
|
|
13321
|
+
const parsed = core.Internal.remote.splitRemoteRef(value.path);
|
|
13322
|
+
if (!found || parsed.status === "error") {
|
|
13323
|
+
return undefined;
|
|
13324
|
+
}
|
|
13325
|
+
return {
|
|
13326
|
+
sourcePath,
|
|
13327
|
+
error: found,
|
|
13328
|
+
remoteHost: parsed.remoteHost
|
|
13329
|
+
};
|
|
13330
|
+
}
|
|
13331
|
+
if (fix !== "image:check-metadata" && fix !== "file:check-metadata") {
|
|
13332
|
+
return undefined;
|
|
13333
|
+
}
|
|
13334
|
+
// A metadata mismatch is reported by `validateFiles`, at the field that is
|
|
13335
|
+
// wrong, and always as `image:check-metadata` — even for a file, which has
|
|
13336
|
+
// no width to read. So the media value is found from the field, and the fix
|
|
13337
|
+
// is chosen by what its schema says it is.
|
|
13338
|
+
for (const [mediaPathS, value] of Object.entries(files)) {
|
|
13339
|
+
const mediaPath = mediaPathS;
|
|
13340
|
+
const type = mediaTypeAt(mediaPath, input);
|
|
13341
|
+
if (!type) {
|
|
13342
|
+
continue;
|
|
13343
|
+
}
|
|
13344
|
+
const fieldPaths = ValRouterCore.getFieldsForType(type).map(field => core.Internal.createValPathOfItem(mediaPath, field));
|
|
13345
|
+
if (mediaPath === sourcePath || fieldPaths.includes(sourcePath)) {
|
|
13346
|
+
// Every image with metadata is in `files`, right or wrong — being there
|
|
13347
|
+
// only means "not yet compared with its bytes". So compare it, and fix
|
|
13348
|
+
// only one that is actually wrong: a stale request for an image someone
|
|
13349
|
+
// has since fixed is "not found", not an empty patch.
|
|
13350
|
+
const mismatches = await ops.validateFiles(input.schemas, input.sources, {
|
|
13351
|
+
[mediaPath]: value
|
|
13352
|
+
}, input.fileLastUpdatedByPatchId);
|
|
13353
|
+
if (Object.keys(mismatches).length === 0) {
|
|
13354
|
+
return undefined;
|
|
13355
|
+
}
|
|
13356
|
+
return {
|
|
13357
|
+
sourcePath: mediaPath,
|
|
13358
|
+
error: {
|
|
13359
|
+
message: "Metadata does not match the file",
|
|
13360
|
+
value,
|
|
13361
|
+
fixes: [type === "image" ? "image:check-metadata" : "file:check-metadata"]
|
|
13362
|
+
}
|
|
13363
|
+
};
|
|
13364
|
+
}
|
|
13365
|
+
}
|
|
13366
|
+
return undefined;
|
|
13367
|
+
}
|
|
13368
|
+
function mediaTypeAt(sourcePath, {
|
|
13369
|
+
moduleFilePath,
|
|
13370
|
+
schemas,
|
|
13371
|
+
sources
|
|
13372
|
+
}) {
|
|
13373
|
+
const schema = schemas[moduleFilePath];
|
|
13374
|
+
if (!schema) {
|
|
13375
|
+
return undefined;
|
|
13376
|
+
}
|
|
13377
|
+
const [, modulePath] = core.Internal.splitModuleFilePathAndModulePath(sourcePath);
|
|
13378
|
+
try {
|
|
13379
|
+
const {
|
|
13380
|
+
schema: atPath
|
|
13381
|
+
} = core.Internal.resolvePath(modulePath, sources[moduleFilePath], schema["executeSerialize"]());
|
|
13382
|
+
return atPath.type === "image" || atPath.type === "file" ? atPath.type : undefined;
|
|
13383
|
+
} catch {
|
|
13384
|
+
return undefined;
|
|
13385
|
+
}
|
|
13386
|
+
}
|
|
13387
|
+
|
|
13388
|
+
/**
|
|
13389
|
+
* {@link FixFiles} over whatever store this server has: the disk in fs mode,
|
|
13390
|
+
* the content service in http mode. A file a pending patch wrote is read from
|
|
13391
|
+
* that patch, as `validateFiles` reads it, so a fix is made from the bytes
|
|
13392
|
+
* the check was made from.
|
|
13393
|
+
*/
|
|
13394
|
+
function opsFixFiles(ops, fileLastUpdatedByPatchId) {
|
|
13395
|
+
const readFile = async filePath => {
|
|
13396
|
+
const fromPatch = fileLastUpdatedByPatchId[filePath];
|
|
13397
|
+
const buffer = fromPatch ? await ops.getBase64EncodedBinaryFileFromPatch(filePath, fromPatch.patchId, fromPatch.remote) : await ops.getBinaryFile(filePath);
|
|
13398
|
+
if (buffer === null) {
|
|
13399
|
+
throw new Error(`Could not read ${filePath}`);
|
|
13400
|
+
}
|
|
13401
|
+
// A view of the same bytes, not a copy: the readers below take a Buffer,
|
|
13402
|
+
// and ops answer with a Uint8Array because they run in a browser too.
|
|
13403
|
+
return Buffer.from(buffer.buffer, buffer.byteOffset, buffer.byteLength);
|
|
13404
|
+
};
|
|
13405
|
+
return {
|
|
13406
|
+
readFile,
|
|
13407
|
+
readVideoMetadata: async filePath => extractVideoMetadata(filePath, await readFile(filePath)),
|
|
13408
|
+
readRemoteFile: readRemoteFileOverHttp,
|
|
13409
|
+
saveRemoteFile: async () => ({
|
|
13410
|
+
status: "error",
|
|
13411
|
+
error: "Downloading a remote file is not available in the Studio yet. Run `val validate --fix` in the project."
|
|
13412
|
+
})
|
|
13413
|
+
};
|
|
13414
|
+
}
|
|
13415
|
+
|
|
13416
|
+
/**
|
|
13417
|
+
* A remote file's bytes, downloaded and kept nowhere: this server may have no
|
|
13418
|
+
* disk to keep them on (http mode, a Worker on the platform).
|
|
13419
|
+
*
|
|
13420
|
+
* A platform ref whose host fails is read from the project's own
|
|
13421
|
+
* `valstart.dev` address instead (`fallbackRefOf`), and those bytes are kept
|
|
13422
|
+
* only if they hash to what the ref names: a different host answering is safe
|
|
13423
|
+
* because of that check, and only because of it.
|
|
13424
|
+
*/
|
|
13425
|
+
const readRemoteFileOverHttp = async ref => {
|
|
13426
|
+
const first = await fetchBytes(ref);
|
|
13427
|
+
if (first.status === "success") {
|
|
13428
|
+
return first;
|
|
13429
|
+
}
|
|
13430
|
+
const fallback = ValRouterCore.fallbackRefOf(ref);
|
|
13431
|
+
const parsed = core.Internal.remote.splitRemoteRef(ref);
|
|
13432
|
+
if (fallback === null || parsed.status === "error") {
|
|
13433
|
+
return first;
|
|
13434
|
+
}
|
|
13435
|
+
const second = await fetchBytes(fallback);
|
|
13436
|
+
if (second.status === "success" && second.fileHash === parsed.fileHash) {
|
|
13437
|
+
return second;
|
|
13438
|
+
}
|
|
13439
|
+
return first;
|
|
13440
|
+
};
|
|
13441
|
+
async function fetchBytes(url) {
|
|
13442
|
+
try {
|
|
13443
|
+
const res = await fetch(url, {
|
|
13444
|
+
signal: AbortSignal.timeout(30_000)
|
|
13445
|
+
});
|
|
13446
|
+
if (!res.ok) {
|
|
13447
|
+
return {
|
|
13448
|
+
status: "error",
|
|
13449
|
+
error: `The file could not be downloaded from its address: HTTP ${res.status}.`
|
|
13450
|
+
};
|
|
13451
|
+
}
|
|
13452
|
+
const fileBuffer = Buffer.from(await res.arrayBuffer());
|
|
13453
|
+
return {
|
|
13454
|
+
status: "success",
|
|
13455
|
+
fileBuffer,
|
|
13456
|
+
fileHash: core.Internal.remote.getFileHash(fileBuffer)
|
|
13457
|
+
};
|
|
13458
|
+
} catch (err) {
|
|
13459
|
+
return {
|
|
13460
|
+
status: "error",
|
|
13461
|
+
error: `The file could not be downloaded from its address: ${err instanceof Error ? err.message : "unknown error"}.`
|
|
13462
|
+
};
|
|
13463
|
+
}
|
|
13464
|
+
}
|
|
13465
|
+
|
|
13466
|
+
/** The platform every server on Node runs with. See {@link ValServerPlatform}. */
|
|
13467
|
+
const nodePlatform = {
|
|
13468
|
+
createOps: createValOps,
|
|
13469
|
+
probeUrl: url => probeUrl(url),
|
|
13470
|
+
jwt: {
|
|
13471
|
+
verifyJwt,
|
|
13472
|
+
decodeJwtWithoutVerifying,
|
|
13473
|
+
encodeJwt,
|
|
13474
|
+
getExpire
|
|
13475
|
+
},
|
|
13476
|
+
bytesToBase64: bytes => Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength).toString("base64"),
|
|
13477
|
+
readValLoginToken,
|
|
13478
|
+
resolveRemoteFileAuth,
|
|
13479
|
+
studioValidation: {
|
|
13480
|
+
checkModuleForStudio,
|
|
13481
|
+
fixForStudio
|
|
13482
|
+
}
|
|
13483
|
+
};
|
|
13484
|
+
|
|
13485
|
+
async function createValServer(valModules, route, opts, config, callbacks, formatter,
|
|
13486
|
+
/**
|
|
13487
|
+
* Called after a save has applied its patches. EXPERIMENTAL — see
|
|
13488
|
+
* `ValServerOptions.commitPrepared`.
|
|
13489
|
+
*/
|
|
13490
|
+
commitPrepared,
|
|
13491
|
+
/**
|
|
13492
|
+
* What a publish does in http mode. EXPERIMENTAL — see
|
|
13493
|
+
* `ValServerOptions.publishOverride`.
|
|
13494
|
+
*/
|
|
13495
|
+
publishOverride) {
|
|
13496
|
+
const valServerConfig = await initHandlerOptions(route, opts, config);
|
|
13497
|
+
return ValRouterCore.ValServer(valModules, {
|
|
13498
|
+
formatter,
|
|
13499
|
+
commitPrepared,
|
|
13500
|
+
publishOverride,
|
|
13501
|
+
...valServerConfig
|
|
13502
|
+
}, callbacks, nodePlatform);
|
|
13503
|
+
}
|
|
13504
|
+
|
|
13505
|
+
// TODO: remove
|
|
13506
|
+
/**
|
|
13507
|
+
* `fs` and `path` are imported INSIDE this function, not at the top of the file.
|
|
13508
|
+
*
|
|
13509
|
+
* This is the only thing in this module that touches either, and it is a local
|
|
13510
|
+
* development convenience: scanning upwards for a `.git` to guess the commit and
|
|
13511
|
+
* branch. A static import put `fs` in the module graph of everything reaching
|
|
13512
|
+
* `createValApiRouter` -- which is every server integration, including ones that
|
|
13513
|
+
* run where there is no filesystem. Workerd provides no `fs`, so such a build
|
|
13514
|
+
* could not be bundled at all without stubbing it.
|
|
13515
|
+
*
|
|
13516
|
+
* The `await import` costs nothing here: the only caller is the CLI, on a
|
|
13517
|
+
* machine that has both.
|
|
13518
|
+
*/
|
|
13519
|
+
async function safeReadGit(cwd) {
|
|
13520
|
+
const {
|
|
13521
|
+
promises: fs
|
|
13522
|
+
} = await Promise.resolve().then(function () { return /*#__PURE__*/_interopNamespace(require('fs')); });
|
|
13523
|
+
const path = await Promise.resolve().then(function () { return /*#__PURE__*/_interopNamespace(require('path')); });
|
|
13524
|
+
async function findGitHead(currentDir, depth) {
|
|
13525
|
+
const gitHeadPath = path.join(currentDir, ".git", "HEAD");
|
|
13526
|
+
if (depth > 1000) {
|
|
13527
|
+
console.error(`Reached max depth while scanning for .git folder. Current working dir: ${cwd}.`);
|
|
13528
|
+
return {
|
|
13529
|
+
commit: undefined,
|
|
13530
|
+
branch: undefined
|
|
13531
|
+
};
|
|
13532
|
+
}
|
|
13533
|
+
try {
|
|
13534
|
+
const headContents = await fs.readFile(gitHeadPath, "utf-8");
|
|
13535
|
+
const match = headContents.match(/^ref: refs\/heads\/(.+)/);
|
|
13536
|
+
if (match) {
|
|
13537
|
+
const branchName = match[1];
|
|
13538
|
+
return {
|
|
13539
|
+
branch: branchName,
|
|
13540
|
+
commit: await readCommit(currentDir, branchName)
|
|
13541
|
+
};
|
|
13542
|
+
} else {
|
|
13543
|
+
return {
|
|
13544
|
+
commit: undefined,
|
|
13545
|
+
branch: undefined
|
|
13546
|
+
};
|
|
13547
|
+
}
|
|
13548
|
+
} catch {
|
|
13549
|
+
const parentDir = path.dirname(currentDir);
|
|
13550
|
+
|
|
13551
|
+
// We've reached the root directory
|
|
13552
|
+
if (parentDir === currentDir) {
|
|
13553
|
+
return {
|
|
13554
|
+
commit: undefined,
|
|
13555
|
+
branch: undefined
|
|
13556
|
+
};
|
|
13557
|
+
}
|
|
13558
|
+
return findGitHead(parentDir, depth + 1);
|
|
13559
|
+
}
|
|
13560
|
+
}
|
|
13561
|
+
try {
|
|
13562
|
+
return findGitHead(cwd, 0);
|
|
13563
|
+
} catch (err) {
|
|
13564
|
+
console.error("Error while reading .git", err);
|
|
13565
|
+
return {
|
|
13566
|
+
commit: undefined,
|
|
13567
|
+
branch: undefined
|
|
13568
|
+
};
|
|
13569
|
+
}
|
|
13570
|
+
}
|
|
13571
|
+
|
|
13572
|
+
/** Only reached from {@link safeReadGit}; same reason for the local imports. */
|
|
13573
|
+
async function readCommit(gitDir, branchName) {
|
|
13574
|
+
const {
|
|
13575
|
+
promises: fs
|
|
13576
|
+
} = await Promise.resolve().then(function () { return /*#__PURE__*/_interopNamespace(require('fs')); });
|
|
13577
|
+
const path = await Promise.resolve().then(function () { return /*#__PURE__*/_interopNamespace(require('path')); });
|
|
13578
|
+
try {
|
|
13579
|
+
return (await fs.readFile(path.join(gitDir, ".git", "refs", "heads", branchName), "utf-8")).trim();
|
|
13580
|
+
} catch {
|
|
13581
|
+
return undefined;
|
|
13582
|
+
}
|
|
13583
|
+
}
|
|
13584
|
+
function createValApiRouter(route, valServerPromise, convert) {
|
|
13585
|
+
const uiRequestHandler = server.createUIRequestHandler();
|
|
13586
|
+
return ValRouterCore.createValRequestHandler(route, valServerPromise, convert, uiRequestHandler);
|
|
13587
|
+
}
|
|
13588
|
+
|
|
13589
|
+
const JsFileLookupMapping = [
|
|
13590
|
+
// NOTE: first one matching will be used
|
|
13591
|
+
[".cjs.d.ts", [".esm.js", ".mjs.js"]], [".cjs.js", [".esm.js", ".mjs.js"]], [".cjs", [".mjs"]], [".d.ts", [".js", ".esm.js", ".mjs.js"]]];
|
|
13592
|
+
const MAX_CACHE_SIZE = 100 * 1024 * 1024; // 100 mb
|
|
13593
|
+
const MAX_OBJECT_KEY_SIZE = 2 ** 27; // https://stackoverflow.com/questions/13367391/is-there-a-limit-on-length-of-the-key-string-in-js-object
|
|
13594
|
+
|
|
13595
|
+
class ValModuleLoader {
|
|
13596
|
+
cache;
|
|
13597
|
+
cacheSize;
|
|
13598
|
+
constructor(projectRoot, compilerOptions,
|
|
13599
|
+
// TODO: remove this?
|
|
13600
|
+
sourceFileHandler, host = {
|
|
13601
|
+
...ts__default["default"].sys,
|
|
13602
|
+
writeFile: (fileName, data, encoding) => {
|
|
13603
|
+
fs__default["default"].mkdirSync(path__default["default"].dirname(fileName), {
|
|
13604
|
+
recursive: true
|
|
13605
|
+
});
|
|
13606
|
+
fs__default["default"].writeFileSync(fileName, typeof data === "string" ? data : new Uint8Array(data), encoding);
|
|
13607
|
+
},
|
|
13608
|
+
rmFile: fs__default["default"].rmSync,
|
|
13609
|
+
readBuffer: fileName => {
|
|
13610
|
+
try {
|
|
13611
|
+
return fs__default["default"].readFileSync(fileName);
|
|
13612
|
+
} catch {
|
|
13613
|
+
return undefined;
|
|
13614
|
+
}
|
|
13615
|
+
}
|
|
13616
|
+
}, disableCache = false) {
|
|
13617
|
+
this.projectRoot = projectRoot;
|
|
13618
|
+
this.compilerOptions = compilerOptions;
|
|
13619
|
+
this.sourceFileHandler = sourceFileHandler;
|
|
13620
|
+
this.host = host;
|
|
13621
|
+
this.disableCache = disableCache;
|
|
13622
|
+
this.cache = {};
|
|
13623
|
+
this.cacheSize = 0;
|
|
13624
|
+
}
|
|
13625
|
+
getModule(modulePath) {
|
|
13626
|
+
if (!modulePath) {
|
|
13627
|
+
throw Error(`Illegal module path: "${modulePath}"`);
|
|
13628
|
+
}
|
|
13629
|
+
const code = this.host.readFile(modulePath);
|
|
13630
|
+
if (!code) {
|
|
13631
|
+
throw Error(`Could not read file "${modulePath}"`);
|
|
13632
|
+
}
|
|
13633
|
+
let compiledCode;
|
|
13634
|
+
if (this.cache[code] && !this.disableCache) {
|
|
13635
|
+
// TODO: use hash instead of code as key
|
|
13636
|
+
compiledCode = this.cache[code];
|
|
13637
|
+
} else {
|
|
13638
|
+
compiledCode = sucrase.transform(code, {
|
|
13639
|
+
filePath: modulePath,
|
|
13640
|
+
disableESTransforms: true,
|
|
13641
|
+
transforms: ["typescript"]
|
|
13642
|
+
}).code;
|
|
13643
|
+
if (!this.disableCache) {
|
|
13644
|
+
if (this.cacheSize > MAX_CACHE_SIZE) {
|
|
13645
|
+
console.warn("Cache size exceeded, clearing cache");
|
|
13646
|
+
this.cache = {};
|
|
13647
|
+
this.cacheSize = 0;
|
|
13648
|
+
}
|
|
13649
|
+
if (code.length < MAX_OBJECT_KEY_SIZE) {
|
|
13650
|
+
this.cache[code] = compiledCode;
|
|
13651
|
+
this.cacheSize += code.length + compiledCode.length; // code is mostly ASCII so 1 byte per char
|
|
13652
|
+
}
|
|
12973
13653
|
}
|
|
12974
|
-
}
|
|
12975
|
-
|
|
12976
|
-
|
|
12977
|
-
|
|
12978
|
-
|
|
12979
|
-
|
|
12980
|
-
|
|
12981
|
-
|
|
12982
|
-
|
|
12983
|
-
|
|
12984
|
-
|
|
12985
|
-
|
|
13654
|
+
}
|
|
13655
|
+
return compiledCode;
|
|
13656
|
+
}
|
|
13657
|
+
resolveModulePath(containingFilePath, requestedModuleName) {
|
|
13658
|
+
var _this$host$realpath, _this$host;
|
|
13659
|
+
let sourceFileName = this.sourceFileHandler.resolveSourceModulePath(containingFilePath, requestedModuleName);
|
|
13660
|
+
if (requestedModuleName === "@vercel/stega") {
|
|
13661
|
+
sourceFileName = this.sourceFileHandler.resolveSourceModulePath(containingFilePath, "@vercel/stega").replace("stega/dist", "stega/dist/esm");
|
|
13662
|
+
}
|
|
13663
|
+
const matches = this.findMatchingJsFile(sourceFileName);
|
|
13664
|
+
if (matches.match === false) {
|
|
13665
|
+
let debugInfo = "";
|
|
13666
|
+
if (sourceFileName.includes("val.config")) {
|
|
13667
|
+
debugInfo = `\n@valbuild directory scan:\n${this.host.readDirectory("/", ["js", "ts", "json"], [], ["**/@valbuild/*"]).join("\n")}`;
|
|
12986
13668
|
}
|
|
12987
|
-
|
|
12988
|
-
|
|
12989
|
-
|
|
12990
|
-
|
|
12991
|
-
|
|
12992
|
-
|
|
12993
|
-
|
|
12994
|
-
|
|
12995
|
-
|
|
12996
|
-
|
|
13669
|
+
throw Error(`Could not find matching js file for module "${requestedModuleName}" requested by: "${containingFilePath}". Tried:\n${matches.tried.join("\n")}${debugInfo}`);
|
|
13670
|
+
}
|
|
13671
|
+
const filePath = matches.match;
|
|
13672
|
+
// resolve all symlinks (preconstruct for example symlinks the dist folder)
|
|
13673
|
+
const followedPath = ((_this$host$realpath = (_this$host = this.host).realpath) === null || _this$host$realpath === void 0 ? void 0 : _this$host$realpath.call(_this$host, filePath)) ?? filePath;
|
|
13674
|
+
if (!followedPath) {
|
|
13675
|
+
throw Error(`File path was empty: "${filePath}", containing file: "${containingFilePath}", requested module: "${requestedModuleName}"`);
|
|
13676
|
+
}
|
|
13677
|
+
return followedPath;
|
|
13678
|
+
}
|
|
13679
|
+
findMatchingJsFile(filePath) {
|
|
13680
|
+
let requiresReplacements = false;
|
|
13681
|
+
for (const [currentEnding] of JsFileLookupMapping) {
|
|
13682
|
+
if (filePath.endsWith(currentEnding)) {
|
|
13683
|
+
requiresReplacements = true;
|
|
13684
|
+
break;
|
|
12997
13685
|
}
|
|
12998
|
-
|
|
12999
|
-
|
|
13000
|
-
|
|
13001
|
-
|
|
13002
|
-
|
|
13003
|
-
|
|
13686
|
+
}
|
|
13687
|
+
// avoid unnecessary calls to fileExists if we don't need to replace anything
|
|
13688
|
+
if (!requiresReplacements) {
|
|
13689
|
+
if (this.host.fileExists(filePath)) {
|
|
13690
|
+
return {
|
|
13691
|
+
match: filePath
|
|
13692
|
+
};
|
|
13693
|
+
}
|
|
13694
|
+
}
|
|
13695
|
+
const tried = [];
|
|
13696
|
+
for (const [currentEnding, replacements] of JsFileLookupMapping) {
|
|
13697
|
+
if (filePath.endsWith(currentEnding)) {
|
|
13698
|
+
for (const replacement of replacements) {
|
|
13699
|
+
const newFilePath = filePath.slice(0, -currentEnding.length) + replacement;
|
|
13700
|
+
if (this.host.fileExists(newFilePath)) {
|
|
13701
|
+
return {
|
|
13702
|
+
match: newFilePath
|
|
13703
|
+
};
|
|
13704
|
+
} else {
|
|
13705
|
+
tried.push(newFilePath);
|
|
13004
13706
|
}
|
|
13005
|
-
}
|
|
13006
|
-
} else {
|
|
13007
|
-
remainingErrors.push({
|
|
13008
|
-
...validationError,
|
|
13009
|
-
message: `This view points at the wrong module. Expected '${schemaAtPath.moduleFilePath}'.`
|
|
13010
|
-
});
|
|
13707
|
+
}
|
|
13011
13708
|
}
|
|
13012
13709
|
}
|
|
13013
|
-
}
|
|
13014
|
-
if (!validationError.fixes || validationError.fixes.length === 0) {
|
|
13015
|
-
remainingErrors.push(validationError);
|
|
13016
|
-
}
|
|
13017
|
-
return {
|
|
13018
|
-
patch: patch$1,
|
|
13019
|
-
remainingErrors
|
|
13020
|
-
};
|
|
13021
|
-
}
|
|
13022
|
-
function getRemoteValueFromValidationError(v) {
|
|
13023
|
-
if (v.value && typeof v.value !== "object") {
|
|
13024
|
-
return {
|
|
13025
|
-
success: false,
|
|
13026
|
-
message: "Unexpected error while checking remote (not an object)"
|
|
13027
|
-
};
|
|
13028
|
-
}
|
|
13029
|
-
if (!v.value) {
|
|
13030
|
-
return {
|
|
13031
|
-
success: false,
|
|
13032
|
-
message: "Unexpected error while checking remote (no value)"
|
|
13033
|
-
};
|
|
13034
|
-
}
|
|
13035
|
-
if (typeof v.value !== "object" || v.value === null || !("path" in v.value)) {
|
|
13036
|
-
return {
|
|
13037
|
-
success: false,
|
|
13038
|
-
message: "Unexpected error while checking remote (no path in value)"
|
|
13039
|
-
};
|
|
13040
|
-
}
|
|
13041
|
-
if (typeof v.value.path !== "string") {
|
|
13042
13710
|
return {
|
|
13043
|
-
|
|
13044
|
-
|
|
13711
|
+
match: false,
|
|
13712
|
+
tried: tried.concat(filePath)
|
|
13045
13713
|
};
|
|
13046
13714
|
}
|
|
13047
|
-
// Everything but the path is what Val read from the bytes.
|
|
13048
|
-
const {
|
|
13049
|
-
path,
|
|
13050
|
-
...metadata
|
|
13051
|
-
} = v.value;
|
|
13052
|
-
return {
|
|
13053
|
-
success: true,
|
|
13054
|
-
path,
|
|
13055
|
-
metadata
|
|
13056
|
-
};
|
|
13057
13715
|
}
|
|
13058
13716
|
|
|
13059
|
-
/** The fields of a video that are read from its bytes, in the order written. */
|
|
13060
|
-
const VIDEO_METADATA_FIELDS = ["mimeType", "width", "height", "duration"];
|
|
13061
|
-
|
|
13062
13717
|
/**
|
|
13063
|
-
*
|
|
13064
|
-
*
|
|
13065
|
-
* video — and from the error's own value otherwise.
|
|
13718
|
+
* An implementation of methods in the various ts.*Host interfaces
|
|
13719
|
+
* that uses ValFS to resolve modules and read/write files.
|
|
13066
13720
|
*/
|
|
13067
|
-
|
|
13068
|
-
|
|
13069
|
-
|
|
13070
|
-
|
|
13071
|
-
|
|
13072
|
-
|
|
13073
|
-
|
|
13074
|
-
|
|
13075
|
-
|
|
13721
|
+
|
|
13722
|
+
class ValFSHost {
|
|
13723
|
+
constructor(valFS, currentDirectory) {
|
|
13724
|
+
this.valFS = valFS;
|
|
13725
|
+
this.currentDirectory = currentDirectory;
|
|
13726
|
+
}
|
|
13727
|
+
useCaseSensitiveFileNames = true;
|
|
13728
|
+
readDirectory(rootDir, extensions, excludes, includes, depth) {
|
|
13729
|
+
return this.valFS.readDirectory(rootDir, extensions, excludes, includes, depth);
|
|
13730
|
+
}
|
|
13731
|
+
rmFile(fileName) {
|
|
13732
|
+
this.valFS.rmFile(fileName);
|
|
13733
|
+
}
|
|
13734
|
+
writeFile(fileName, text, encoding) {
|
|
13735
|
+
this.valFS.writeFile(fileName, text, encoding);
|
|
13736
|
+
}
|
|
13737
|
+
getCurrentDirectory() {
|
|
13738
|
+
return this.currentDirectory;
|
|
13739
|
+
}
|
|
13740
|
+
getCanonicalFileName(fileName) {
|
|
13741
|
+
if (path__default["default"].isAbsolute(fileName)) {
|
|
13742
|
+
return path__default["default"].normalize(fileName);
|
|
13076
13743
|
}
|
|
13077
|
-
|
|
13078
|
-
return value ? {
|
|
13079
|
-
status: "ok",
|
|
13080
|
-
value
|
|
13081
|
-
} : {
|
|
13082
|
-
status: "error",
|
|
13083
|
-
message: "Video is not an object!"
|
|
13084
|
-
};
|
|
13744
|
+
return path__default["default"].resolve(this.getCurrentDirectory(), fileName);
|
|
13085
13745
|
}
|
|
13086
|
-
|
|
13087
|
-
|
|
13088
|
-
status: "ok",
|
|
13089
|
-
value
|
|
13090
|
-
} : {
|
|
13091
|
-
status: "error",
|
|
13092
|
-
message: "Video is not an object!"
|
|
13093
|
-
};
|
|
13094
|
-
}
|
|
13095
|
-
async function getVideoMetadata(projectRoot, fileRef) {
|
|
13096
|
-
// Not read into memory: only the headers are, and a video is mostly not
|
|
13097
|
-
// headers. A remote video's headers are read over HTTP, with Range
|
|
13098
|
-
// requests, so it is not downloaded either.
|
|
13099
|
-
if (core.Internal.isRemoteMediaPath(fileRef)) {
|
|
13100
|
-
return extractVideoMetadataFromUrl(fileRef, nameForTypeOf(fileRef));
|
|
13746
|
+
fileExists(fileName) {
|
|
13747
|
+
return this.valFS.fileExists(fileName);
|
|
13101
13748
|
}
|
|
13102
|
-
|
|
13103
|
-
|
|
13104
|
-
async function getImageMetadata(projectRoot, validationError) {
|
|
13105
|
-
const fileRef = getValidationErrorFileRef(validationError);
|
|
13106
|
-
if (!fileRef) {
|
|
13107
|
-
// TODO:
|
|
13108
|
-
throw Error("Cannot fix image without a file reference");
|
|
13749
|
+
readFile(fileName) {
|
|
13750
|
+
return this.valFS.readFile(fileName);
|
|
13109
13751
|
}
|
|
13110
|
-
|
|
13111
|
-
|
|
13112
|
-
|
|
13113
|
-
|
|
13114
|
-
|
|
13115
|
-
const fileRef = getValidationErrorFileRef(validationError);
|
|
13116
|
-
if (!fileRef) {
|
|
13117
|
-
// TODO:
|
|
13118
|
-
throw Error("Cannot fix file without a file reference");
|
|
13752
|
+
readBuffer(fileName) {
|
|
13753
|
+
return this.valFS.readBuffer(fileName);
|
|
13754
|
+
}
|
|
13755
|
+
realpath(path) {
|
|
13756
|
+
return this.valFS.realpath(path);
|
|
13119
13757
|
}
|
|
13120
|
-
const filename = path__default["default"].join(projectRoot, fileRef);
|
|
13121
|
-
fs__default["default"].readFileSync(filename);
|
|
13122
|
-
return extractFileMetadata(fileRef);
|
|
13123
13758
|
}
|
|
13124
13759
|
|
|
13125
13760
|
/**
|
|
@@ -13802,6 +14437,26 @@ async function handleViewCheckModule(ctx) {
|
|
|
13802
14437
|
shouldApplyPatch: true
|
|
13803
14438
|
};
|
|
13804
14439
|
}
|
|
14440
|
+
|
|
14441
|
+
/**
|
|
14442
|
+
* `remote:rewrite-host`: a platform ref whose host no longer serves its
|
|
14443
|
+
* project. The server's full check reports it, and so does `createFixPatch`'s
|
|
14444
|
+
* `image:check-remote` branch, which is how `val validate` finds it: that
|
|
14445
|
+
* branch asks the host first. Either way the patch is `createFixPatch`'s, and
|
|
14446
|
+
* needs nothing but the ref -- the new host is derived from it.
|
|
14447
|
+
*/
|
|
14448
|
+
async function handleRemoteRewriteHost(ctx) {
|
|
14449
|
+
if (!ctx.fix) {
|
|
14450
|
+
return {
|
|
14451
|
+
success: true,
|
|
14452
|
+
fixableErrorMessage: `${ctx.validationError.message} ` + "Run 'val validate --fix' to point it at the project's own address."
|
|
14453
|
+
};
|
|
14454
|
+
}
|
|
14455
|
+
return {
|
|
14456
|
+
success: true,
|
|
14457
|
+
shouldApplyPatch: true
|
|
14458
|
+
};
|
|
14459
|
+
}
|
|
13805
14460
|
async function handleExternalUpload() {
|
|
13806
14461
|
return {
|
|
13807
14462
|
success: true,
|
|
@@ -13843,7 +14498,8 @@ const currentFixHandlers = {
|
|
|
13843
14498
|
"files:check-all-files": handleCheckAllFiles,
|
|
13844
14499
|
"jsonValues:extract-entry": handleJsonValuesExtractEntry,
|
|
13845
14500
|
"external:upload": handleExternalUpload,
|
|
13846
|
-
"view:check-module": handleViewCheckModule
|
|
14501
|
+
"view:check-module": handleViewCheckModule,
|
|
14502
|
+
"remote:rewrite-host": handleRemoteRewriteHost
|
|
13847
14503
|
};
|
|
13848
14504
|
const deprecatedFixHandlers = {
|
|
13849
14505
|
"image:replace-metadata": handleFileMetadata
|
|
@@ -14612,6 +15268,7 @@ Object.defineProperty(exports, 'hasRemoteFileSchema', {
|
|
|
14612
15268
|
});
|
|
14613
15269
|
exports.InMemoryPatchStore = ValRouterCore.InMemoryPatchStore;
|
|
14614
15270
|
exports.ValOpsMemory = ValRouterCore.ValOpsMemory;
|
|
15271
|
+
exports.askOncePerHost = ValRouterCore.askOncePerHost;
|
|
14615
15272
|
exports.classifyJsonValuesOp = ValRouterCore.classifyJsonValuesOp;
|
|
14616
15273
|
exports.formatPatchSourceError = ValRouterCore.formatPatchSourceError;
|
|
14617
15274
|
exports.getFileExt = ValRouterCore.getFileExt;
|