@hops-ops/distributed 4.8.0 → 4.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +46 -8
- package/dist/generation.d.ts +15 -0
- package/dist/generation.js +41 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/protocol.d.ts +2 -0
- package/dist/protocol.js +5 -0
- package/dist/replica/command-runtime/create.js +11 -0
- package/dist/replica/command-runtime/errors.js +2 -0
- package/dist/replica/command-runtime/types.d.ts +7 -1
- package/dist/replica/distributed-replica/impl-protocol.d.ts +1 -0
- package/dist/replica/distributed-replica/impl-protocol.js +1 -0
- package/dist/replica/distributed-replica/impl.js +11 -0
- package/dist/replica/distributed-replica/watch.js +13 -1
- package/dist/replica/index.d.ts +1 -1
- package/dist/replica/types.d.ts +38 -0
- package/dist/sveltekit/boundary-lifecycle.d.ts +37 -0
- package/dist/sveltekit/boundary-lifecycle.js +355 -0
- package/dist/sveltekit/boundary-variables.d.ts +57 -0
- package/dist/sveltekit/boundary-variables.js +290 -0
- package/dist/sveltekit/context.d.ts +3 -0
- package/dist/sveltekit/context.js +8 -0
- package/dist/sveltekit/index.d.ts +7 -3
- package/dist/sveltekit/index.js +6 -2
- package/dist/sveltekit/islands/boundaries.d.ts +104 -0
- package/dist/sveltekit/islands/boundaries.js +734 -0
- package/dist/sveltekit/lifecycle.d.ts +57 -0
- package/dist/sveltekit/lifecycle.js +454 -0
- package/dist/sveltekit/operation-identity.d.ts +4 -0
- package/dist/sveltekit/operation-identity.js +10 -0
- package/dist/sveltekit/replica.d.ts +29 -2
- package/dist/sveltekit/replica.js +102 -6
- package/dist/sveltekit/server-replica.d.ts +10 -26
- package/dist/sveltekit/server-replica.js +159 -132
- package/dist/sveltekit/vite.d.ts +46 -3
- package/dist/sveltekit/vite.js +643 -36
- package/package.json +4 -3
package/dist/sveltekit/vite.js
CHANGED
|
@@ -1,12 +1,20 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import { existsSync, lstatSync, readFileSync, realpathSync } from 'node:fs';
|
|
4
|
+
import { lstat, mkdir, mkdtemp, open, readFile, readdir, realpath, rename, rm, unlink, writeFile } from 'node:fs/promises';
|
|
4
5
|
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
5
6
|
import { isMainThread } from 'node:worker_threads';
|
|
7
|
+
import { analyzeDistributedSvelteKitBoundaries, validateDistributedSvelteKitBoundaryPlan } from './islands/boundaries.js';
|
|
8
|
+
export { analyzeDistributedSvelteKitBoundaries, validateDistributedSvelteKitBoundaryPlan } from './islands/boundaries.js';
|
|
6
9
|
const GENERATED_SVELTEKIT_MODULE = 'sveltekit.ts';
|
|
10
|
+
const GENERATED_BOUNDARIES_MODULE = 'boundaries.ts';
|
|
7
11
|
const MAX_COMMAND_OUTPUT_BYTES = 16 * 1024 * 1024;
|
|
12
|
+
const MAX_GENERATED_COMPARE_FILES = 20_000;
|
|
13
|
+
const MAX_GENERATED_COMPARE_BYTES = 128 * 1024 * 1024;
|
|
8
14
|
const MODULE_NAME = /^\$distributed(?:\/[A-Za-z0-9][A-Za-z0-9._-]*)*$/;
|
|
9
15
|
const COMPILER_LOCK = join('.svelte-kit', 'distributed', 'compiler.lock');
|
|
16
|
+
const GENERATION_META = 'distributed-generation';
|
|
17
|
+
const MAX_LIFECYCLE_STATE_BYTES = 1024 * 1024;
|
|
10
18
|
const COMPILER_COORDINATORS = Symbol.for('@hops-ops/distributed/sveltekit/compiler-coordinators/v1');
|
|
11
19
|
/** Vite proxy config for GraphQL HTTP and WebSocket traffic. */
|
|
12
20
|
export function distributedGraphqlProxy(options) {
|
|
@@ -22,6 +30,22 @@ export function distributedGraphqlProxy(options) {
|
|
|
22
30
|
}
|
|
23
31
|
return { [path]: { target, changeOrigin: true, ws: true } };
|
|
24
32
|
}
|
|
33
|
+
/** Lifecycle-only side channel for projects using committed generated clients. */
|
|
34
|
+
export function distributedLifecycle() {
|
|
35
|
+
let frameworkDist;
|
|
36
|
+
return {
|
|
37
|
+
name: '@hops-ops/distributed:lifecycle',
|
|
38
|
+
enforce: 'pre',
|
|
39
|
+
configResolved(config) {
|
|
40
|
+
frameworkDist = localFrameworkDist(config.root);
|
|
41
|
+
},
|
|
42
|
+
configureServer: configureLifecycleServer,
|
|
43
|
+
transformIndexHtml: lifecycleGenerationMeta,
|
|
44
|
+
handleHotUpdate(context) {
|
|
45
|
+
return suppressFrameworkHotUpdate(context, frameworkDist);
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
}
|
|
25
49
|
/** Generate every configured surface through the same transaction used by Vite. */
|
|
26
50
|
export async function generateDistributedSvelteKit(options) {
|
|
27
51
|
await runCompilerOnce(options, 'generate');
|
|
@@ -38,12 +62,14 @@ export async function checkDistributedSvelteKit(options) {
|
|
|
38
62
|
* coalesced, abortable, and always invoked without a shell.
|
|
39
63
|
*/
|
|
40
64
|
export function distributedSvelteKit(options) {
|
|
65
|
+
const lifecycleOwnsCompile = process.env.DISTRIBUTED_LIFECYCLE_DIR !== undefined;
|
|
41
66
|
let resolved;
|
|
42
67
|
let dirty = false;
|
|
43
68
|
let running;
|
|
44
69
|
let completedGeneration = 0;
|
|
45
70
|
let reloadedGeneration = 0;
|
|
46
71
|
let stopped = false;
|
|
72
|
+
let frameworkDist;
|
|
47
73
|
const children = new Set();
|
|
48
74
|
const cancellation = new AbortController();
|
|
49
75
|
let lock;
|
|
@@ -106,7 +132,17 @@ export function distributedSvelteKit(options) {
|
|
|
106
132
|
throw new Error('Distributed SvelteKit Vite plugin was configured more than once');
|
|
107
133
|
}
|
|
108
134
|
resolved = resolveIntegration(options, options.cwd ?? config.root);
|
|
135
|
+
frameworkDist = localFrameworkDist(config.root);
|
|
109
136
|
await validateResolvedPaths(resolved);
|
|
137
|
+
/*
|
|
138
|
+
* `distributed dev` has already staged this generation and owns every
|
|
139
|
+
* compiler input through its application watcher. Recompiling here would
|
|
140
|
+
* race the API process for Cargo and mutate generated output outside the
|
|
141
|
+
* lifecycle transaction. The virtual modules still resolve the staged
|
|
142
|
+
* files, while compiler inputs below are held for the supervisor reload.
|
|
143
|
+
*/
|
|
144
|
+
if (lifecycleOwnsCompile)
|
|
145
|
+
return;
|
|
110
146
|
/*
|
|
111
147
|
* SvelteKit post-build analysis loads Vite config in an isolated
|
|
112
148
|
* worker marked with SVELTEKIT_FORK. That pass reads framework
|
|
@@ -126,18 +162,32 @@ export function distributedSvelteKit(options) {
|
|
|
126
162
|
}
|
|
127
163
|
},
|
|
128
164
|
configureServer(server) {
|
|
165
|
+
configureLifecycleServer(server);
|
|
129
166
|
const integration = requireResolved(resolved);
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
167
|
+
if (!lifecycleOwnsCompile) {
|
|
168
|
+
const roots = [
|
|
169
|
+
integration.routesDir,
|
|
170
|
+
integration.libDir,
|
|
171
|
+
...integration.clients.flatMap((client) => [
|
|
172
|
+
...client.watchRoots,
|
|
173
|
+
...client.manifestWatchRoots
|
|
174
|
+
])
|
|
175
|
+
];
|
|
176
|
+
if (roots.length > 0)
|
|
177
|
+
server.watcher.add(roots);
|
|
178
|
+
}
|
|
133
179
|
server.httpServer?.once('close', () => {
|
|
134
180
|
void stop();
|
|
135
181
|
});
|
|
136
182
|
},
|
|
137
183
|
buildStart() {
|
|
138
184
|
const integration = requireResolved(resolved);
|
|
185
|
+
if (lifecycleOwnsCompile)
|
|
186
|
+
return;
|
|
187
|
+
this.addWatchFile(integration.routesDir);
|
|
188
|
+
this.addWatchFile(integration.libDir);
|
|
139
189
|
for (const client of integration.clients) {
|
|
140
|
-
for (const root of client.watchRoots)
|
|
190
|
+
for (const root of [...client.watchRoots, ...client.manifestWatchRoots])
|
|
141
191
|
this.addWatchFile(root);
|
|
142
192
|
}
|
|
143
193
|
},
|
|
@@ -151,12 +201,18 @@ export function distributedSvelteKit(options) {
|
|
|
151
201
|
return undefined;
|
|
152
202
|
return `export * from ${JSON.stringify(portablePath(client.entry))};\n`;
|
|
153
203
|
},
|
|
204
|
+
transformIndexHtml: lifecycleGenerationMeta,
|
|
154
205
|
async handleHotUpdate(context) {
|
|
206
|
+
const suppressed = suppressFrameworkHotUpdate(context, frameworkDist);
|
|
207
|
+
if (suppressed !== undefined)
|
|
208
|
+
return suppressed;
|
|
155
209
|
const integration = requireResolved(resolved);
|
|
156
|
-
if (!
|
|
210
|
+
if (!isCompilerInput(context.file, integration))
|
|
157
211
|
return undefined;
|
|
212
|
+
if (lifecycleOwnsCompile)
|
|
213
|
+
return [];
|
|
158
214
|
try {
|
|
159
|
-
await compile(`GraphQL change ${context.file}`);
|
|
215
|
+
await compile(`GraphQL/Svelte change ${context.file}`);
|
|
160
216
|
}
|
|
161
217
|
catch (error) {
|
|
162
218
|
context.server.ws.send({
|
|
@@ -173,19 +229,220 @@ export function distributedSvelteKit(options) {
|
|
|
173
229
|
context.server.moduleGraph.invalidateModule(module);
|
|
174
230
|
}
|
|
175
231
|
}
|
|
176
|
-
|
|
232
|
+
if (process.env.DISTRIBUTED_LIFECYCLE_DIR === undefined) {
|
|
233
|
+
context.server.ws.send({ type: 'full-reload', path: '*' });
|
|
234
|
+
}
|
|
177
235
|
reloadedGeneration = completedGeneration;
|
|
178
236
|
return [];
|
|
179
237
|
},
|
|
180
238
|
async watchChange(id) {
|
|
181
239
|
const integration = requireResolved(resolved);
|
|
182
|
-
if (
|
|
183
|
-
|
|
240
|
+
if (lifecycleOwnsCompile)
|
|
241
|
+
return;
|
|
242
|
+
if (isCompilerInput(id, integration)) {
|
|
243
|
+
await compile(`GraphQL/Svelte watch change ${id}`);
|
|
184
244
|
}
|
|
185
245
|
},
|
|
186
246
|
closeBundle: stop
|
|
187
247
|
};
|
|
188
248
|
}
|
|
249
|
+
function localFrameworkDist(root) {
|
|
250
|
+
try {
|
|
251
|
+
const candidate = join(root, 'node_modules', '@hops-ops', 'distributed', 'dist');
|
|
252
|
+
return existsSync(candidate) ? realpathSync(candidate) : undefined;
|
|
253
|
+
}
|
|
254
|
+
catch {
|
|
255
|
+
return undefined;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
function suppressFrameworkHotUpdate(context, frameworkDist) {
|
|
259
|
+
if (process.env.DISTRIBUTED_LIFECYCLE_DIR === undefined ||
|
|
260
|
+
frameworkDist === undefined ||
|
|
261
|
+
!isWithin(frameworkDist, canonicalExistingPath(context.file)))
|
|
262
|
+
return undefined;
|
|
263
|
+
for (const module of context.modules ?? []) {
|
|
264
|
+
context.server.moduleGraph.invalidateModule(module);
|
|
265
|
+
}
|
|
266
|
+
return [];
|
|
267
|
+
}
|
|
268
|
+
function canonicalExistingPath(value) {
|
|
269
|
+
try {
|
|
270
|
+
return realpathSync(value);
|
|
271
|
+
}
|
|
272
|
+
catch {
|
|
273
|
+
return resolve(value);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
const CONTROL_ID = /^[A-Za-z0-9_:-]{16,128}$/;
|
|
277
|
+
const MAX_ACK_BYTES = 4096;
|
|
278
|
+
function lifecycleGenerationMeta() {
|
|
279
|
+
const configured = process.env.DISTRIBUTED_LIFECYCLE_DIR;
|
|
280
|
+
if (configured === undefined || !isAbsolute(configured))
|
|
281
|
+
return [];
|
|
282
|
+
try {
|
|
283
|
+
const encoded = readFileSync(join(resolve(configured), 'dev.json'));
|
|
284
|
+
if (encoded.byteLength > MAX_LIFECYCLE_STATE_BYTES)
|
|
285
|
+
return [];
|
|
286
|
+
const state = JSON.parse(encoded.toString('utf8'));
|
|
287
|
+
const generationId = state.active?.generationId;
|
|
288
|
+
if (typeof generationId !== 'string' ||
|
|
289
|
+
generationId.length === 0 ||
|
|
290
|
+
generationId.length > 512 ||
|
|
291
|
+
generationId !== generationId.trim() ||
|
|
292
|
+
/[\u0000-\u001f\u007f]/.test(generationId))
|
|
293
|
+
return [];
|
|
294
|
+
return [Object.freeze({
|
|
295
|
+
tag: 'meta',
|
|
296
|
+
attrs: Object.freeze({ name: GENERATION_META, content: generationId }),
|
|
297
|
+
injectTo: 'head-prepend'
|
|
298
|
+
})];
|
|
299
|
+
}
|
|
300
|
+
catch {
|
|
301
|
+
// A lifecycle file is atomically replaced. Missing or malformed state
|
|
302
|
+
// simply omits the hint; the browser falls back to its first poll.
|
|
303
|
+
return [];
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
function configureLifecycleServer(server) {
|
|
307
|
+
const configured = process.env.DISTRIBUTED_LIFECYCLE_DIR;
|
|
308
|
+
if (configured === undefined || !isAbsolute(configured))
|
|
309
|
+
return;
|
|
310
|
+
const lifecycleRoot = resolve(configured);
|
|
311
|
+
server.middlewares?.use('/__distributed/lifecycle', (request, response) => {
|
|
312
|
+
void handleLifecycleRequest(lifecycleRoot, request, response).catch(() => {
|
|
313
|
+
if (response.statusCode < 400)
|
|
314
|
+
response.statusCode = 500;
|
|
315
|
+
response.end();
|
|
316
|
+
});
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
async function handleLifecycleRequest(lifecycleRoot, request, response) {
|
|
320
|
+
response.setHeader('cache-control', 'no-store');
|
|
321
|
+
if (request.method === 'GET') {
|
|
322
|
+
const participant = singleHeader(request.headers['x-distributed-participant']);
|
|
323
|
+
if (participant !== undefined && CONTROL_ID.test(participant)) {
|
|
324
|
+
await writeParticipantHeartbeat(lifecycleRoot, participant).catch(() => undefined);
|
|
325
|
+
}
|
|
326
|
+
const state = await readLifecycleState(lifecycleRoot);
|
|
327
|
+
if (state === undefined) {
|
|
328
|
+
response.statusCode = 404;
|
|
329
|
+
response.end();
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
response.statusCode = 200;
|
|
333
|
+
response.setHeader('content-type', 'application/json');
|
|
334
|
+
response.end(state);
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
if (request.method === 'POST') {
|
|
338
|
+
const contentType = singleHeader(request.headers['content-type']);
|
|
339
|
+
if (contentType?.split(';', 1)[0]?.trim().toLowerCase() !== 'application/json') {
|
|
340
|
+
response.statusCode = 415;
|
|
341
|
+
response.end();
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
if (!sameOriginLifecycleRequest(request)) {
|
|
345
|
+
response.statusCode = 403;
|
|
346
|
+
response.end();
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
const body = JSON.parse(await readRequestBody(request, MAX_ACK_BYTES));
|
|
350
|
+
if (!CONTROL_ID.test(String(body.transitionId ?? '')) ||
|
|
351
|
+
!CONTROL_ID.test(String(body.participantId ?? '')) ||
|
|
352
|
+
typeof body.ok !== 'boolean') {
|
|
353
|
+
response.statusCode = 400;
|
|
354
|
+
response.end();
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
const stateSource = await readLifecycleState(lifecycleRoot);
|
|
358
|
+
const state = stateSource === undefined ? undefined : JSON.parse(stateSource);
|
|
359
|
+
if (state?.phase !== 'preparing' || state.transitionId !== body.transitionId) {
|
|
360
|
+
response.statusCode = 409;
|
|
361
|
+
response.end();
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
await writeControlJson(join(lifecycleRoot, 'dev-control', 'acks', String(body.transitionId)), `${String(body.participantId)}.json`, { ok: body.ok });
|
|
365
|
+
response.statusCode = 204;
|
|
366
|
+
response.end();
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
response.statusCode = 405;
|
|
370
|
+
response.end();
|
|
371
|
+
}
|
|
372
|
+
function writeParticipantHeartbeat(root, participant) {
|
|
373
|
+
return writeControlJson(join(root, 'dev-control', 'participants'), `${participant}.json`, { seenAtUnixMs: Date.now() });
|
|
374
|
+
}
|
|
375
|
+
async function readLifecycleState(root) {
|
|
376
|
+
const path = join(root, 'dev.json');
|
|
377
|
+
let metadata;
|
|
378
|
+
try {
|
|
379
|
+
metadata = await lstat(path);
|
|
380
|
+
}
|
|
381
|
+
catch (error) {
|
|
382
|
+
if (error.code === 'ENOENT')
|
|
383
|
+
return undefined;
|
|
384
|
+
throw error;
|
|
385
|
+
}
|
|
386
|
+
if (metadata.isSymbolicLink() || !metadata.isFile() || metadata.size > MAX_LIFECYCLE_STATE_BYTES) {
|
|
387
|
+
throw new Error('invalid Distributed lifecycle state file');
|
|
388
|
+
}
|
|
389
|
+
return readFile(path, 'utf8');
|
|
390
|
+
}
|
|
391
|
+
async function writeControlJson(directory, name, value) {
|
|
392
|
+
await mkdir(directory, { recursive: true });
|
|
393
|
+
const temporary = join(directory, `.${name}.${process.pid}.${Date.now()}.${randomUUID()}`);
|
|
394
|
+
await writeFile(temporary, `${JSON.stringify(value)}\n`, { flag: 'wx', mode: 0o600 });
|
|
395
|
+
await rename(temporary, join(directory, name));
|
|
396
|
+
}
|
|
397
|
+
function sameOriginLifecycleRequest(request) {
|
|
398
|
+
const origin = singleHeader(request.headers.origin);
|
|
399
|
+
const host = singleHeader(request.headers.host);
|
|
400
|
+
if (origin === undefined || host === undefined)
|
|
401
|
+
return false;
|
|
402
|
+
try {
|
|
403
|
+
const parsed = new URL(origin);
|
|
404
|
+
const forwarded = singleHeader(request.headers['x-forwarded-proto']);
|
|
405
|
+
const socket = request.socket;
|
|
406
|
+
const protocol = forwarded === 'http' || forwarded === 'https'
|
|
407
|
+
? `${forwarded}:`
|
|
408
|
+
: socket?.encrypted === true
|
|
409
|
+
? 'https:'
|
|
410
|
+
: 'http:';
|
|
411
|
+
return (parsed.protocol === protocol &&
|
|
412
|
+
parsed.host === host &&
|
|
413
|
+
parsed.origin === origin);
|
|
414
|
+
}
|
|
415
|
+
catch {
|
|
416
|
+
return false;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
function singleHeader(value) {
|
|
420
|
+
return typeof value === 'string' ? value : value?.[0];
|
|
421
|
+
}
|
|
422
|
+
function readRequestBody(request, maximum) {
|
|
423
|
+
return new Promise((resolvePromise, reject) => {
|
|
424
|
+
const chunks = [];
|
|
425
|
+
let bytes = 0;
|
|
426
|
+
let exceeded = false;
|
|
427
|
+
request.on('data', (chunk) => {
|
|
428
|
+
if (exceeded)
|
|
429
|
+
return;
|
|
430
|
+
bytes += chunk.byteLength;
|
|
431
|
+
if (bytes > maximum) {
|
|
432
|
+
exceeded = true;
|
|
433
|
+
chunks.length = 0;
|
|
434
|
+
reject(new Error('lifecycle acknowledgement exceeds bound'));
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
chunks.push(chunk);
|
|
438
|
+
});
|
|
439
|
+
request.on('error', reject);
|
|
440
|
+
request.on('end', () => {
|
|
441
|
+
if (!exceeded)
|
|
442
|
+
resolvePromise(Buffer.concat(chunks).toString('utf8'));
|
|
443
|
+
});
|
|
444
|
+
});
|
|
445
|
+
}
|
|
189
446
|
async function runCompilerOnce(options, mode) {
|
|
190
447
|
const integration = resolveIntegration(options, options.cwd ?? process.cwd());
|
|
191
448
|
await validateResolvedPaths(integration);
|
|
@@ -240,6 +497,14 @@ function resolveIntegration(options, fallbackCwd) {
|
|
|
240
497
|
}
|
|
241
498
|
const modules = new Set();
|
|
242
499
|
const outputs = [];
|
|
500
|
+
const routesDir = containedPath(cwd, options.routesDir ?? 'src/routes', 'routesDir');
|
|
501
|
+
const libDir = containedPath(cwd, options.libDir ?? 'src/lib', 'libDir');
|
|
502
|
+
const aliases = Object.freeze(Object.fromEntries(Object.entries(options.aliases ?? {}).map(([key, value]) => {
|
|
503
|
+
if (!/^\$[A-Za-z0-9_-]+$/.test(key)) {
|
|
504
|
+
throw new TypeError(`Distributed SvelteKit alias \`${key}\` must be a single $name segment`);
|
|
505
|
+
}
|
|
506
|
+
return [key, portablePath(relative(cwd, containedPath(cwd, value, `alias ${key}`)))];
|
|
507
|
+
})));
|
|
243
508
|
const clients = options.clients.map((client, index) => {
|
|
244
509
|
if (client === null || typeof client !== 'object') {
|
|
245
510
|
throw new TypeError(`Distributed client[${index}] must be an object`);
|
|
@@ -267,7 +532,21 @@ function resolveIntegration(options, fallbackCwd) {
|
|
|
267
532
|
throw new TypeError(`Distributed client \`${client.module}\` requires at least one GraphQL document glob`);
|
|
268
533
|
}
|
|
269
534
|
const documents = client.documents.map((document, documentIndex) => nonempty(document, `${client.module} documents[${documentIndex}]`));
|
|
270
|
-
const
|
|
535
|
+
const boundaries = (client.boundaries ?? []).map((boundary, boundaryIndex) => {
|
|
536
|
+
if (boundary === null ||
|
|
537
|
+
typeof boundary !== 'object' ||
|
|
538
|
+
(boundary.kind !== 'page' && boundary.kind !== 'layout')) {
|
|
539
|
+
throw new TypeError(`${client.module} boundaries[${boundaryIndex}] is invalid`);
|
|
540
|
+
}
|
|
541
|
+
return Object.freeze({
|
|
542
|
+
operation: nonempty(boundary.operation, `${client.module} boundaries[${boundaryIndex}].operation`),
|
|
543
|
+
route: nonempty(boundary.route, `${client.module} boundaries[${boundaryIndex}].route`),
|
|
544
|
+
kind: boundary.kind,
|
|
545
|
+
...(boundary.variables === undefined
|
|
546
|
+
? {}
|
|
547
|
+
: { variables: boundary.variables })
|
|
548
|
+
});
|
|
549
|
+
});
|
|
271
550
|
validateManifestSource(client.module, client.manifest);
|
|
272
551
|
const out = containedPath(cwd, client.out, `${client.module} generated output`);
|
|
273
552
|
if (out === cwd) {
|
|
@@ -279,15 +558,18 @@ function resolveIntegration(options, fallbackCwd) {
|
|
|
279
558
|
}
|
|
280
559
|
}
|
|
281
560
|
outputs.push(out);
|
|
561
|
+
const adapterOut = join(cwd, '.svelte-kit', 'distributed', 'clients', Buffer.from(client.module).toString('base64url'));
|
|
282
562
|
return Object.freeze({
|
|
283
563
|
module: client.module,
|
|
284
564
|
manifest: client.manifest,
|
|
285
565
|
selector,
|
|
286
566
|
documents: Object.freeze(documents),
|
|
287
|
-
|
|
567
|
+
boundaries: Object.freeze(boundaries),
|
|
288
568
|
out,
|
|
289
569
|
entry: join(out, GENERATED_SVELTEKIT_MODULE),
|
|
290
|
-
|
|
570
|
+
adapterOut,
|
|
571
|
+
watchRoots: Object.freeze(documentWatchRoots(cwd, documents, out)),
|
|
572
|
+
manifestWatchRoots: Object.freeze(manifestWatchRoots(cwd, client.manifest))
|
|
291
573
|
});
|
|
292
574
|
});
|
|
293
575
|
return Object.freeze({
|
|
@@ -299,6 +581,9 @@ function resolveIntegration(options, fallbackCwd) {
|
|
|
299
581
|
}
|
|
300
582
|
return argument;
|
|
301
583
|
})),
|
|
584
|
+
routesDir,
|
|
585
|
+
libDir,
|
|
586
|
+
aliases,
|
|
302
587
|
clients: Object.freeze(clients)
|
|
303
588
|
});
|
|
304
589
|
}
|
|
@@ -352,8 +637,39 @@ function documentWatchRoots(cwd, documents, out) {
|
|
|
352
637
|
}
|
|
353
638
|
return [...roots].sort();
|
|
354
639
|
}
|
|
640
|
+
function manifestWatchRoots(cwd, manifest) {
|
|
641
|
+
const manifestPath = typeof manifest === 'string'
|
|
642
|
+
? resolve(cwd, manifest)
|
|
643
|
+
: (() => {
|
|
644
|
+
const index = manifest.args.indexOf('--manifest-path');
|
|
645
|
+
return index === -1 || manifest.args[index + 1] === undefined
|
|
646
|
+
? undefined
|
|
647
|
+
: resolve(cwd, manifest.args[index + 1]);
|
|
648
|
+
})();
|
|
649
|
+
if (manifestPath === undefined)
|
|
650
|
+
return [];
|
|
651
|
+
const root = dirname(manifestPath);
|
|
652
|
+
return [manifestPath, join(root, 'src'), join(root, 'crates')]
|
|
653
|
+
.filter((candidate) => existsSync(candidate))
|
|
654
|
+
.sort();
|
|
655
|
+
}
|
|
656
|
+
function isCompilerInput(file, integration) {
|
|
657
|
+
if (isGraphqlInput(file, integration))
|
|
658
|
+
return true;
|
|
659
|
+
const absolute = resolve(integration.cwd, file);
|
|
660
|
+
const basenameValue = basename(absolute);
|
|
661
|
+
if (!absolute.endsWith('.rs') && basenameValue !== 'Cargo.toml' && basenameValue !== 'Cargo.lock') {
|
|
662
|
+
return false;
|
|
663
|
+
}
|
|
664
|
+
return integration.clients.some((client) => client.manifestWatchRoots.some((root) => isWithin(root, absolute)));
|
|
665
|
+
}
|
|
355
666
|
function isGraphqlInput(file, integration) {
|
|
356
667
|
const absolute = resolve(integration.cwd, file);
|
|
668
|
+
if (absolute.endsWith('.svelte') &&
|
|
669
|
+
(isWithin(integration.routesDir, absolute) ||
|
|
670
|
+
isWithin(integration.libDir, absolute))) {
|
|
671
|
+
return true;
|
|
672
|
+
}
|
|
357
673
|
if ((!absolute.endsWith('.graphql') && !absolute.endsWith('.gql')) ||
|
|
358
674
|
!isWithin(integration.cwd, absolute)) {
|
|
359
675
|
return false;
|
|
@@ -366,7 +682,8 @@ function isGraphqlInput(file, integration) {
|
|
|
366
682
|
async function compileTransaction(integration, children, signal) {
|
|
367
683
|
throwIfAborted(signal);
|
|
368
684
|
await validateResolvedPaths(integration);
|
|
369
|
-
const
|
|
685
|
+
const transactionRoot = await compilerTransactionRoot(integration);
|
|
686
|
+
const transaction = await mkdtemp(join(transactionRoot, '.distributed-sveltekit-'));
|
|
370
687
|
const staged = [];
|
|
371
688
|
try {
|
|
372
689
|
for (const [index, client] of integration.clients.entries()) {
|
|
@@ -380,17 +697,12 @@ async function compileTransaction(integration, children, signal) {
|
|
|
380
697
|
throw new Error(`generated output ${client.out} must be a real directory`);
|
|
381
698
|
}
|
|
382
699
|
hadOutput = true;
|
|
383
|
-
await cp(client.out, output, {
|
|
384
|
-
recursive: true,
|
|
385
|
-
errorOnExist: true,
|
|
386
|
-
force: false,
|
|
387
|
-
dereference: false
|
|
388
|
-
});
|
|
389
700
|
}
|
|
390
701
|
catch (error) {
|
|
391
702
|
if (!isMissing(error))
|
|
392
703
|
throw error;
|
|
393
704
|
}
|
|
705
|
+
await mkdir(output, { recursive: true });
|
|
394
706
|
const args = [
|
|
395
707
|
'client',
|
|
396
708
|
'--manifest',
|
|
@@ -401,19 +713,33 @@ async function compileTransaction(integration, children, signal) {
|
|
|
401
713
|
'--documents',
|
|
402
714
|
document
|
|
403
715
|
]),
|
|
404
|
-
...client.routes.flatMap((route) => ['--route', route]),
|
|
405
716
|
'--out',
|
|
406
717
|
output
|
|
407
718
|
];
|
|
408
719
|
await runCommand(integration, args, children, signal);
|
|
409
|
-
await validateGeneratedEntrypoint(
|
|
720
|
+
await validateGeneratedEntrypoint(transaction, output, client.module);
|
|
410
721
|
staged.push({
|
|
411
722
|
client,
|
|
412
723
|
output,
|
|
413
724
|
backup: join(transaction, `backup-${index}`),
|
|
414
|
-
hadOutput
|
|
725
|
+
hadOutput,
|
|
726
|
+
adapterOutput: join(transaction, `adapter-${index}`),
|
|
727
|
+
adapterBackup: join(transaction, `adapter-backup-${index}`),
|
|
728
|
+
hadAdapterOutput: await realDirectoryExists(client.adapterOut)
|
|
415
729
|
});
|
|
416
730
|
}
|
|
731
|
+
const plans = await analyzeStagedBoundaries(integration, staged);
|
|
732
|
+
const plansByModule = new Map(plans.map((plan) => [plan.module, plan]));
|
|
733
|
+
for (const item of staged) {
|
|
734
|
+
const plan = plansByModule.get(item.client.module);
|
|
735
|
+
if (plan === undefined) {
|
|
736
|
+
throw new Error(`Distributed SvelteKit boundary analysis returned no plan for ${item.client.module}`);
|
|
737
|
+
}
|
|
738
|
+
await writeFile(join(item.output, GENERATED_BOUNDARIES_MODULE), boundaryModuleSource(plan), { encoding: 'utf8', flag: 'wx' });
|
|
739
|
+
await exposeBoundaryModule(item.output);
|
|
740
|
+
await mkdir(item.adapterOutput, { recursive: true });
|
|
741
|
+
await writeFile(join(item.adapterOutput, 'boundaries.json'), boundaryPlanSource(plan), { encoding: 'utf8', flag: 'wx' });
|
|
742
|
+
}
|
|
417
743
|
throwIfAborted(signal);
|
|
418
744
|
await commitOutputs(integration, staged, signal);
|
|
419
745
|
}
|
|
@@ -424,14 +750,17 @@ async function compileTransaction(integration, children, signal) {
|
|
|
424
750
|
async function checkTransaction(integration, children, signal) {
|
|
425
751
|
throwIfAborted(signal);
|
|
426
752
|
await validateResolvedPaths(integration);
|
|
427
|
-
const
|
|
753
|
+
const transactionRoot = await compilerTransactionRoot(integration);
|
|
754
|
+
const transaction = await mkdtemp(join(transactionRoot, '.distributed-sveltekit-check-'));
|
|
428
755
|
try {
|
|
756
|
+
const staged = [];
|
|
429
757
|
for (const [index, client] of integration.clients.entries()) {
|
|
430
758
|
throwIfAborted(signal);
|
|
431
759
|
const manifest = await materializeManifest(integration, client, transaction, index, children, signal);
|
|
760
|
+
const output = join(transaction, `output-${index}`);
|
|
761
|
+
await mkdir(output, { recursive: true });
|
|
432
762
|
await runCommand(integration, [
|
|
433
763
|
'client',
|
|
434
|
-
'--check',
|
|
435
764
|
'--manifest',
|
|
436
765
|
manifest,
|
|
437
766
|
client.selector[0],
|
|
@@ -440,16 +769,227 @@ async function checkTransaction(integration, children, signal) {
|
|
|
440
769
|
'--documents',
|
|
441
770
|
document
|
|
442
771
|
]),
|
|
443
|
-
...client.routes.flatMap((route) => ['--route', route]),
|
|
444
772
|
'--out',
|
|
445
|
-
|
|
773
|
+
output
|
|
446
774
|
], children, signal);
|
|
775
|
+
await validateGeneratedEntrypoint(integration.cwd, output, client.module);
|
|
776
|
+
staged.push(Object.freeze({ client, output }));
|
|
777
|
+
}
|
|
778
|
+
const plans = await analyzeStagedBoundaries(integration, staged);
|
|
779
|
+
const plansByModule = new Map(plans.map((plan) => [plan.module, plan]));
|
|
780
|
+
for (const [index, client] of integration.clients.entries()) {
|
|
781
|
+
const output = staged[index].output;
|
|
782
|
+
const plan = plansByModule.get(client.module);
|
|
783
|
+
if (plan === undefined) {
|
|
784
|
+
throw new Error(`Distributed SvelteKit boundary analysis returned no plan for ${client.module}`);
|
|
785
|
+
}
|
|
786
|
+
await writeFile(join(output, GENERATED_BOUNDARIES_MODULE), boundaryModuleSource(plan), { encoding: 'utf8', flag: 'wx' });
|
|
787
|
+
await exposeBoundaryModule(output);
|
|
788
|
+
await compareGeneratedTrees(client.out, output, client.module);
|
|
789
|
+
await validateAdapterBoundaryPlan(client, plan);
|
|
447
790
|
}
|
|
448
791
|
}
|
|
449
792
|
finally {
|
|
450
793
|
await rm(transaction, { recursive: true, force: true });
|
|
451
794
|
}
|
|
452
795
|
}
|
|
796
|
+
async function compilerTransactionRoot(integration) {
|
|
797
|
+
const lifecycle = process.env.DISTRIBUTED_LIFECYCLE_DIR;
|
|
798
|
+
const root = lifecycle !== undefined && isAbsolute(lifecycle)
|
|
799
|
+
? resolve(lifecycle)
|
|
800
|
+
: integration.cwd;
|
|
801
|
+
await mkdir(root, { recursive: true });
|
|
802
|
+
return root;
|
|
803
|
+
}
|
|
804
|
+
async function validateAdapterBoundaryPlan(client, plan) {
|
|
805
|
+
let actual;
|
|
806
|
+
try {
|
|
807
|
+
actual = await readFile(join(client.adapterOut, 'boundaries.json'), 'utf8');
|
|
808
|
+
}
|
|
809
|
+
catch (error) {
|
|
810
|
+
/*
|
|
811
|
+
* `.svelte-kit` is SvelteKit-owned build state. A production build may
|
|
812
|
+
* replace it after our Vite startup generation, while the durable client
|
|
813
|
+
* tree (including boundaries.ts) remains current and was checked above.
|
|
814
|
+
*/
|
|
815
|
+
if (isMissing(error))
|
|
816
|
+
return;
|
|
817
|
+
throw error;
|
|
818
|
+
}
|
|
819
|
+
let persisted;
|
|
820
|
+
try {
|
|
821
|
+
persisted = JSON.parse(actual);
|
|
822
|
+
}
|
|
823
|
+
catch {
|
|
824
|
+
throw new Error(`[distributed.island.boundary_plan_invalid] ${client.module} boundaries.json is not valid JSON`);
|
|
825
|
+
}
|
|
826
|
+
validateDistributedSvelteKitBoundaryPlan(persisted, client.module);
|
|
827
|
+
const expected = boundaryPlanSource(plan);
|
|
828
|
+
if (actual !== expected) {
|
|
829
|
+
throw new Error(`Distributed SvelteKit boundary plan for ${client.module} is stale; run generation without check`);
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
async function analyzeStagedBoundaries(integration, staged) {
|
|
833
|
+
return await analyzeDistributedSvelteKitBoundaries({
|
|
834
|
+
cwd: integration.cwd,
|
|
835
|
+
routesDir: portablePath(relative(integration.cwd, integration.routesDir)),
|
|
836
|
+
libDir: portablePath(relative(integration.cwd, integration.libDir)),
|
|
837
|
+
aliases: integration.aliases,
|
|
838
|
+
clients: await Promise.all(staged.map(async ({ client, output }) => ({
|
|
839
|
+
module: client.module,
|
|
840
|
+
inventory: await readIslandInventory(integration.cwd, output),
|
|
841
|
+
explicitBoundaries: client.boundaries
|
|
842
|
+
})))
|
|
843
|
+
});
|
|
844
|
+
}
|
|
845
|
+
async function readIslandInventory(cwd, output) {
|
|
846
|
+
const path = join(output, 'islands.json');
|
|
847
|
+
const metadata = await lstat(path);
|
|
848
|
+
if (metadata.isSymbolicLink() || !metadata.isFile()) {
|
|
849
|
+
throw new Error(`Distributed island inventory ${portablePath(relative(cwd, path))} must be a regular file`);
|
|
850
|
+
}
|
|
851
|
+
const canonicalRoot = await realpath(cwd);
|
|
852
|
+
const canonical = await realpath(path);
|
|
853
|
+
if (!isWithin(canonicalRoot, canonical)) {
|
|
854
|
+
throw new Error('Distributed island inventory escaped the project root');
|
|
855
|
+
}
|
|
856
|
+
return JSON.parse(await readFile(canonical, 'utf8'));
|
|
857
|
+
}
|
|
858
|
+
function boundaryPlanSource(plan) {
|
|
859
|
+
return `${JSON.stringify(plan, null, 2)}\n`;
|
|
860
|
+
}
|
|
861
|
+
function boundaryModuleSource(plan) {
|
|
862
|
+
validateDistributedSvelteKitBoundaryPlan(plan, plan.module);
|
|
863
|
+
const occurrences = plan.boundaries.flatMap((boundary) => boundary.islands.map((island) => ({ boundary, island })));
|
|
864
|
+
const artifacts = new Map();
|
|
865
|
+
for (const { island } of occurrences) {
|
|
866
|
+
if (!/^operations\/[A-Za-z0-9._-]+\.ts$/.test(island.modulePath) ||
|
|
867
|
+
!/^[_A-Za-z][_0-9A-Za-z]*$/.test(island.exportName)) {
|
|
868
|
+
throw new Error(`[distributed.island.boundary_plan_invalid] ${island.graphqlSource} has an unsafe generated artifact reference`);
|
|
869
|
+
}
|
|
870
|
+
const key = `${island.modulePath}\u0000${island.exportName}`;
|
|
871
|
+
if (!artifacts.has(key)) {
|
|
872
|
+
artifacts.set(key, Object.freeze({
|
|
873
|
+
alias: `DistributedBoundaryArtifact_${artifacts.size}`,
|
|
874
|
+
module: island.modulePath.slice(0, -3),
|
|
875
|
+
exportName: island.exportName
|
|
876
|
+
}));
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
const imports = [...artifacts.values()].map(({ alias, module, exportName }) => `import { ${exportName} as ${alias} } from './${module}.js';`);
|
|
880
|
+
const definitions = occurrences.map(({ boundary, island }, index) => {
|
|
881
|
+
const artifact = artifacts.get(`${island.modulePath}\u0000${island.exportName}`);
|
|
882
|
+
const discovery = island.reason === 'static_component_import' ? 'component' : island.reason;
|
|
883
|
+
return [
|
|
884
|
+
`const DistributedBoundaryBinding_${index} = defineDistributedBoundaryBinding(`,
|
|
885
|
+
` ${artifact.alias},`,
|
|
886
|
+
` ${JSON.stringify(island.binding.sources, null, 2)} as const`,
|
|
887
|
+
`);`,
|
|
888
|
+
`const DistributedBoundaryOperation_${index} = defineDistributedBoundaryOperation(`,
|
|
889
|
+
` ${JSON.stringify({
|
|
890
|
+
operation: island.operation,
|
|
891
|
+
route: boundary.route,
|
|
892
|
+
kind: boundary.kind,
|
|
893
|
+
sourcePath: island.graphqlSource,
|
|
894
|
+
discovery
|
|
895
|
+
}, null, 2)} as const,`,
|
|
896
|
+
` ${artifact.alias},`,
|
|
897
|
+
` DistributedBoundaryBinding_${index}`,
|
|
898
|
+
`);`
|
|
899
|
+
].join('\n');
|
|
900
|
+
});
|
|
901
|
+
const operations = occurrences.map((_, index) => ` DistributedBoundaryOperation_${index}`);
|
|
902
|
+
return [
|
|
903
|
+
'/** GENERATED by the Distributed SvelteKit boundary planner. Do not edit. */',
|
|
904
|
+
"import { defineDistributedBoundaryBinding, defineDistributedBoundaryOperation } from '@hops-ops/distributed/sveltekit';",
|
|
905
|
+
...imports,
|
|
906
|
+
'',
|
|
907
|
+
`export const DISTRIBUTED_BOUNDARY_PLAN = ${JSON.stringify(plan, null, 2)} as const;`,
|
|
908
|
+
'',
|
|
909
|
+
...definitions,
|
|
910
|
+
'',
|
|
911
|
+
'/** Executable SSR/browser ownership assembled from the same boundary plan. */',
|
|
912
|
+
`export const DISTRIBUTED_BOUNDARY_OPERATIONS = ${operations.length === 0 ? '[]' : `[\n${operations.join(',\n')}\n]`} as const;`,
|
|
913
|
+
''
|
|
914
|
+
].join('\n');
|
|
915
|
+
}
|
|
916
|
+
async function exposeBoundaryModule(output) {
|
|
917
|
+
const path = join(output, GENERATED_SVELTEKIT_MODULE);
|
|
918
|
+
const source = await readFile(path, 'utf8');
|
|
919
|
+
if (!source.startsWith('/** GENERATED by distributed client. Do not edit. */')) {
|
|
920
|
+
throw new Error('generated SvelteKit entrypoint is missing its ownership marker');
|
|
921
|
+
}
|
|
922
|
+
await writeFile(path, `${source.trimEnd()}\n\nexport { DISTRIBUTED_BOUNDARY_OPERATIONS, DISTRIBUTED_BOUNDARY_PLAN } from './boundaries.js';\n`, 'utf8');
|
|
923
|
+
}
|
|
924
|
+
async function compareGeneratedTrees(actualRoot, expectedRoot, module) {
|
|
925
|
+
const [actual, expected] = await Promise.all([
|
|
926
|
+
readGeneratedTree(actualRoot),
|
|
927
|
+
readGeneratedTree(expectedRoot)
|
|
928
|
+
]);
|
|
929
|
+
const drift = [];
|
|
930
|
+
for (const [path, contents] of expected) {
|
|
931
|
+
const current = actual.get(path);
|
|
932
|
+
if (current === undefined)
|
|
933
|
+
drift.push(`missing ${path}`);
|
|
934
|
+
else if (current !== contents)
|
|
935
|
+
drift.push(`changed ${path}`);
|
|
936
|
+
}
|
|
937
|
+
for (const path of actual.keys()) {
|
|
938
|
+
if (!expected.has(path))
|
|
939
|
+
drift.push(`unexpected ${path}`);
|
|
940
|
+
}
|
|
941
|
+
if (drift.length > 0) {
|
|
942
|
+
throw new Error(`Distributed SvelteKit client ${module} is stale:\n ${drift.sort().join('\n ')}\nrun generation without check`);
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
async function readGeneratedTree(root) {
|
|
946
|
+
const rootMetadata = await lstat(root);
|
|
947
|
+
if (rootMetadata.isSymbolicLink() || !rootMetadata.isDirectory()) {
|
|
948
|
+
throw new Error(`generated output ${root} must be a real directory`);
|
|
949
|
+
}
|
|
950
|
+
const files = new Map();
|
|
951
|
+
const pending = [
|
|
952
|
+
{ absolute: root, relative: '' }
|
|
953
|
+
];
|
|
954
|
+
while (pending.length > 0) {
|
|
955
|
+
const directory = pending.pop();
|
|
956
|
+
for (const entry of await readdir(directory.absolute, { withFileTypes: true })) {
|
|
957
|
+
const relativePath = directory.relative.length === 0
|
|
958
|
+
? entry.name
|
|
959
|
+
: `${directory.relative}/${entry.name}`;
|
|
960
|
+
const absolutePath = join(directory.absolute, entry.name);
|
|
961
|
+
if (entry.isSymbolicLink()) {
|
|
962
|
+
throw new Error(`generated output contains unsupported symlink ${relativePath}`);
|
|
963
|
+
}
|
|
964
|
+
if (entry.isDirectory()) {
|
|
965
|
+
pending.push({ absolute: absolutePath, relative: relativePath });
|
|
966
|
+
continue;
|
|
967
|
+
}
|
|
968
|
+
if (!entry.isFile()) {
|
|
969
|
+
throw new Error(`generated output contains unsupported entry ${relativePath}`);
|
|
970
|
+
}
|
|
971
|
+
files.set(relativePath, await readFile(absolutePath, 'utf8'));
|
|
972
|
+
if (files.size > 8_192) {
|
|
973
|
+
throw new Error('generated output exceeds 8192 files');
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
return files;
|
|
978
|
+
}
|
|
979
|
+
async function realDirectoryExists(path) {
|
|
980
|
+
try {
|
|
981
|
+
const metadata = await lstat(path);
|
|
982
|
+
if (metadata.isSymbolicLink() || !metadata.isDirectory()) {
|
|
983
|
+
throw new Error(`Distributed SvelteKit adapter output ${path} must be a real directory`);
|
|
984
|
+
}
|
|
985
|
+
return true;
|
|
986
|
+
}
|
|
987
|
+
catch (error) {
|
|
988
|
+
if (isMissing(error))
|
|
989
|
+
return false;
|
|
990
|
+
throw error;
|
|
991
|
+
}
|
|
992
|
+
}
|
|
453
993
|
async function materializeManifest(integration, client, transaction, index, children, signal) {
|
|
454
994
|
throwIfAborted(signal);
|
|
455
995
|
if (typeof client.manifest === 'string') {
|
|
@@ -493,20 +1033,37 @@ async function validateGeneratedEntrypoint(cwd, output, module) {
|
|
|
493
1033
|
async function commitOutputs(integration, staged, signal) {
|
|
494
1034
|
throwIfAborted(signal);
|
|
495
1035
|
await validateResolvedPaths(integration);
|
|
1036
|
+
const outputs = staged.flatMap((item) => [
|
|
1037
|
+
{
|
|
1038
|
+
target: item.client.out,
|
|
1039
|
+
output: item.output,
|
|
1040
|
+
backup: item.backup,
|
|
1041
|
+
hadOutput: item.hadOutput
|
|
1042
|
+
},
|
|
1043
|
+
{
|
|
1044
|
+
target: item.client.adapterOut,
|
|
1045
|
+
output: item.adapterOutput,
|
|
1046
|
+
backup: item.adapterBackup,
|
|
1047
|
+
hadOutput: item.hadAdapterOutput
|
|
1048
|
+
}
|
|
1049
|
+
]);
|
|
496
1050
|
const applied = [];
|
|
497
1051
|
try {
|
|
498
|
-
for (const item of
|
|
1052
|
+
for (const item of outputs) {
|
|
499
1053
|
throwIfAborted(signal);
|
|
500
|
-
await mkdir(dirname(item.
|
|
501
|
-
await validateNearestExistingParent(integration.cwd, item.
|
|
1054
|
+
await mkdir(dirname(item.target), { recursive: true });
|
|
1055
|
+
await validateNearestExistingParent(integration.cwd, item.target);
|
|
1056
|
+
if (item.hadOutput && await generatedTreesEqual(item.target, item.output)) {
|
|
1057
|
+
continue;
|
|
1058
|
+
}
|
|
502
1059
|
if (item.hadOutput)
|
|
503
|
-
await rename(item.
|
|
1060
|
+
await rename(item.target, item.backup);
|
|
504
1061
|
try {
|
|
505
|
-
await rename(item.output, item.
|
|
1062
|
+
await rename(item.output, item.target);
|
|
506
1063
|
}
|
|
507
1064
|
catch (error) {
|
|
508
1065
|
if (item.hadOutput)
|
|
509
|
-
await rename(item.backup, item.
|
|
1066
|
+
await rename(item.backup, item.target);
|
|
510
1067
|
throw error;
|
|
511
1068
|
}
|
|
512
1069
|
applied.push(item);
|
|
@@ -515,13 +1072,63 @@ async function commitOutputs(integration, staged, signal) {
|
|
|
515
1072
|
}
|
|
516
1073
|
catch (error) {
|
|
517
1074
|
for (const item of [...applied].reverse()) {
|
|
518
|
-
await rm(item.
|
|
1075
|
+
await rm(item.target, { recursive: true, force: true });
|
|
519
1076
|
if (item.hadOutput)
|
|
520
|
-
await rename(item.backup, item.
|
|
1077
|
+
await rename(item.backup, item.target);
|
|
521
1078
|
}
|
|
522
1079
|
throw error;
|
|
523
1080
|
}
|
|
524
1081
|
}
|
|
1082
|
+
async function generatedTreesEqual(left, right) {
|
|
1083
|
+
const budget = { files: 0, bytes: 0 };
|
|
1084
|
+
const compare = async (leftDirectory, rightDirectory) => {
|
|
1085
|
+
const [leftEntries, rightEntries] = await Promise.all([
|
|
1086
|
+
readdir(leftDirectory, { withFileTypes: true }),
|
|
1087
|
+
readdir(rightDirectory, { withFileTypes: true })
|
|
1088
|
+
]);
|
|
1089
|
+
leftEntries.sort((a, b) => a.name.localeCompare(b.name));
|
|
1090
|
+
rightEntries.sort((a, b) => a.name.localeCompare(b.name));
|
|
1091
|
+
if (leftEntries.length !== rightEntries.length)
|
|
1092
|
+
return false;
|
|
1093
|
+
for (let index = 0; index < leftEntries.length; index += 1) {
|
|
1094
|
+
const leftEntry = leftEntries[index];
|
|
1095
|
+
const rightEntry = rightEntries[index];
|
|
1096
|
+
if (leftEntry.name !== rightEntry.name ||
|
|
1097
|
+
leftEntry.isDirectory() !== rightEntry.isDirectory() ||
|
|
1098
|
+
leftEntry.isFile() !== rightEntry.isFile())
|
|
1099
|
+
return false;
|
|
1100
|
+
if (!leftEntry.isDirectory() && !leftEntry.isFile())
|
|
1101
|
+
return false;
|
|
1102
|
+
const leftPath = join(leftDirectory, leftEntry.name);
|
|
1103
|
+
const rightPath = join(rightDirectory, rightEntry.name);
|
|
1104
|
+
if (leftEntry.isDirectory()) {
|
|
1105
|
+
if (!await compare(leftPath, rightPath))
|
|
1106
|
+
return false;
|
|
1107
|
+
continue;
|
|
1108
|
+
}
|
|
1109
|
+
budget.files += 1;
|
|
1110
|
+
if (budget.files > MAX_GENERATED_COMPARE_FILES)
|
|
1111
|
+
return false;
|
|
1112
|
+
const [leftMetadata, rightMetadata] = await Promise.all([
|
|
1113
|
+
lstat(leftPath),
|
|
1114
|
+
lstat(rightPath)
|
|
1115
|
+
]);
|
|
1116
|
+
if (leftMetadata.size !== rightMetadata.size)
|
|
1117
|
+
return false;
|
|
1118
|
+
budget.bytes += leftMetadata.size;
|
|
1119
|
+
if (budget.bytes > MAX_GENERATED_COMPARE_BYTES)
|
|
1120
|
+
return false;
|
|
1121
|
+
const [leftBytes, rightBytes] = await Promise.all([
|
|
1122
|
+
readFile(leftPath),
|
|
1123
|
+
readFile(rightPath)
|
|
1124
|
+
]);
|
|
1125
|
+
if (!leftBytes.equals(rightBytes))
|
|
1126
|
+
return false;
|
|
1127
|
+
}
|
|
1128
|
+
return true;
|
|
1129
|
+
};
|
|
1130
|
+
return compare(left, right);
|
|
1131
|
+
}
|
|
525
1132
|
async function runCommand(integration, args, children, signal) {
|
|
526
1133
|
throwIfAborted(signal);
|
|
527
1134
|
const argv = [...integration.commandArgs, ...args];
|