@hops-ops/distributed 4.7.0 → 4.9.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/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/sveltekit/index.d.ts +2 -0
- package/dist/sveltekit/index.js +2 -0
- package/dist/sveltekit/lifecycle.d.ts +57 -0
- package/dist/sveltekit/lifecycle.js +454 -0
- package/dist/sveltekit/replica.d.ts +5 -2
- package/dist/sveltekit/replica.js +30 -1
- package/dist/sveltekit/vite.d.ts +36 -0
- package/dist/sveltekit/vite.js +351 -13
- package/package.json +3 -2
package/dist/sveltekit/vite.js
CHANGED
|
@@ -1,12 +1,17 @@
|
|
|
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 { cp, 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';
|
|
6
7
|
const GENERATED_SVELTEKIT_MODULE = 'sveltekit.ts';
|
|
7
8
|
const MAX_COMMAND_OUTPUT_BYTES = 16 * 1024 * 1024;
|
|
9
|
+
const MAX_GENERATED_COMPARE_FILES = 20_000;
|
|
10
|
+
const MAX_GENERATED_COMPARE_BYTES = 128 * 1024 * 1024;
|
|
8
11
|
const MODULE_NAME = /^\$distributed(?:\/[A-Za-z0-9][A-Za-z0-9._-]*)*$/;
|
|
9
12
|
const COMPILER_LOCK = join('.svelte-kit', 'distributed', 'compiler.lock');
|
|
13
|
+
const GENERATION_META = 'distributed-generation';
|
|
14
|
+
const MAX_LIFECYCLE_STATE_BYTES = 1024 * 1024;
|
|
10
15
|
const COMPILER_COORDINATORS = Symbol.for('@hops-ops/distributed/sveltekit/compiler-coordinators/v1');
|
|
11
16
|
/** Vite proxy config for GraphQL HTTP and WebSocket traffic. */
|
|
12
17
|
export function distributedGraphqlProxy(options) {
|
|
@@ -22,6 +27,22 @@ export function distributedGraphqlProxy(options) {
|
|
|
22
27
|
}
|
|
23
28
|
return { [path]: { target, changeOrigin: true, ws: true } };
|
|
24
29
|
}
|
|
30
|
+
/** Lifecycle-only side channel for projects using committed generated clients. */
|
|
31
|
+
export function distributedLifecycle() {
|
|
32
|
+
let frameworkDist;
|
|
33
|
+
return {
|
|
34
|
+
name: '@hops-ops/distributed:lifecycle',
|
|
35
|
+
enforce: 'pre',
|
|
36
|
+
configResolved(config) {
|
|
37
|
+
frameworkDist = localFrameworkDist(config.root);
|
|
38
|
+
},
|
|
39
|
+
configureServer: configureLifecycleServer,
|
|
40
|
+
transformIndexHtml: lifecycleGenerationMeta,
|
|
41
|
+
handleHotUpdate(context) {
|
|
42
|
+
return suppressFrameworkHotUpdate(context, frameworkDist);
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
}
|
|
25
46
|
/** Generate every configured surface through the same transaction used by Vite. */
|
|
26
47
|
export async function generateDistributedSvelteKit(options) {
|
|
27
48
|
await runCompilerOnce(options, 'generate');
|
|
@@ -38,12 +59,14 @@ export async function checkDistributedSvelteKit(options) {
|
|
|
38
59
|
* coalesced, abortable, and always invoked without a shell.
|
|
39
60
|
*/
|
|
40
61
|
export function distributedSvelteKit(options) {
|
|
62
|
+
const lifecycleOwnsCompile = process.env.DISTRIBUTED_LIFECYCLE_DIR !== undefined;
|
|
41
63
|
let resolved;
|
|
42
64
|
let dirty = false;
|
|
43
65
|
let running;
|
|
44
66
|
let completedGeneration = 0;
|
|
45
67
|
let reloadedGeneration = 0;
|
|
46
68
|
let stopped = false;
|
|
69
|
+
let frameworkDist;
|
|
47
70
|
const children = new Set();
|
|
48
71
|
const cancellation = new AbortController();
|
|
49
72
|
let lock;
|
|
@@ -106,7 +129,17 @@ export function distributedSvelteKit(options) {
|
|
|
106
129
|
throw new Error('Distributed SvelteKit Vite plugin was configured more than once');
|
|
107
130
|
}
|
|
108
131
|
resolved = resolveIntegration(options, options.cwd ?? config.root);
|
|
132
|
+
frameworkDist = localFrameworkDist(config.root);
|
|
109
133
|
await validateResolvedPaths(resolved);
|
|
134
|
+
/*
|
|
135
|
+
* `distributed dev` has already staged this generation and owns every
|
|
136
|
+
* compiler input through its application watcher. Recompiling here would
|
|
137
|
+
* race the API process for Cargo and mutate generated output outside the
|
|
138
|
+
* lifecycle transaction. The virtual modules still resolve the staged
|
|
139
|
+
* files, while compiler inputs below are held for the supervisor reload.
|
|
140
|
+
*/
|
|
141
|
+
if (lifecycleOwnsCompile)
|
|
142
|
+
return;
|
|
110
143
|
/*
|
|
111
144
|
* SvelteKit post-build analysis loads Vite config in an isolated
|
|
112
145
|
* worker marked with SVELTEKIT_FORK. That pass reads framework
|
|
@@ -126,18 +159,26 @@ export function distributedSvelteKit(options) {
|
|
|
126
159
|
}
|
|
127
160
|
},
|
|
128
161
|
configureServer(server) {
|
|
162
|
+
configureLifecycleServer(server);
|
|
129
163
|
const integration = requireResolved(resolved);
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
164
|
+
if (!lifecycleOwnsCompile) {
|
|
165
|
+
const roots = integration.clients.flatMap((client) => [
|
|
166
|
+
...client.watchRoots,
|
|
167
|
+
...client.manifestWatchRoots
|
|
168
|
+
]);
|
|
169
|
+
if (roots.length > 0)
|
|
170
|
+
server.watcher.add(roots);
|
|
171
|
+
}
|
|
133
172
|
server.httpServer?.once('close', () => {
|
|
134
173
|
void stop();
|
|
135
174
|
});
|
|
136
175
|
},
|
|
137
176
|
buildStart() {
|
|
138
177
|
const integration = requireResolved(resolved);
|
|
178
|
+
if (lifecycleOwnsCompile)
|
|
179
|
+
return;
|
|
139
180
|
for (const client of integration.clients) {
|
|
140
|
-
for (const root of client.watchRoots)
|
|
181
|
+
for (const root of [...client.watchRoots, ...client.manifestWatchRoots])
|
|
141
182
|
this.addWatchFile(root);
|
|
142
183
|
}
|
|
143
184
|
},
|
|
@@ -151,10 +192,16 @@ export function distributedSvelteKit(options) {
|
|
|
151
192
|
return undefined;
|
|
152
193
|
return `export * from ${JSON.stringify(portablePath(client.entry))};\n`;
|
|
153
194
|
},
|
|
195
|
+
transformIndexHtml: lifecycleGenerationMeta,
|
|
154
196
|
async handleHotUpdate(context) {
|
|
197
|
+
const suppressed = suppressFrameworkHotUpdate(context, frameworkDist);
|
|
198
|
+
if (suppressed !== undefined)
|
|
199
|
+
return suppressed;
|
|
155
200
|
const integration = requireResolved(resolved);
|
|
156
|
-
if (!
|
|
201
|
+
if (!isCompilerInput(context.file, integration))
|
|
157
202
|
return undefined;
|
|
203
|
+
if (lifecycleOwnsCompile)
|
|
204
|
+
return [];
|
|
158
205
|
try {
|
|
159
206
|
await compile(`GraphQL change ${context.file}`);
|
|
160
207
|
}
|
|
@@ -173,19 +220,220 @@ export function distributedSvelteKit(options) {
|
|
|
173
220
|
context.server.moduleGraph.invalidateModule(module);
|
|
174
221
|
}
|
|
175
222
|
}
|
|
176
|
-
|
|
223
|
+
if (process.env.DISTRIBUTED_LIFECYCLE_DIR === undefined) {
|
|
224
|
+
context.server.ws.send({ type: 'full-reload', path: '*' });
|
|
225
|
+
}
|
|
177
226
|
reloadedGeneration = completedGeneration;
|
|
178
227
|
return [];
|
|
179
228
|
},
|
|
180
229
|
async watchChange(id) {
|
|
181
230
|
const integration = requireResolved(resolved);
|
|
182
|
-
if (
|
|
231
|
+
if (lifecycleOwnsCompile)
|
|
232
|
+
return;
|
|
233
|
+
if (isCompilerInput(id, integration)) {
|
|
183
234
|
await compile(`GraphQL watch change ${id}`);
|
|
184
235
|
}
|
|
185
236
|
},
|
|
186
237
|
closeBundle: stop
|
|
187
238
|
};
|
|
188
239
|
}
|
|
240
|
+
function localFrameworkDist(root) {
|
|
241
|
+
try {
|
|
242
|
+
const candidate = join(root, 'node_modules', '@hops-ops', 'distributed', 'dist');
|
|
243
|
+
return existsSync(candidate) ? realpathSync(candidate) : undefined;
|
|
244
|
+
}
|
|
245
|
+
catch {
|
|
246
|
+
return undefined;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
function suppressFrameworkHotUpdate(context, frameworkDist) {
|
|
250
|
+
if (process.env.DISTRIBUTED_LIFECYCLE_DIR === undefined ||
|
|
251
|
+
frameworkDist === undefined ||
|
|
252
|
+
!isWithin(frameworkDist, canonicalExistingPath(context.file)))
|
|
253
|
+
return undefined;
|
|
254
|
+
for (const module of context.modules ?? []) {
|
|
255
|
+
context.server.moduleGraph.invalidateModule(module);
|
|
256
|
+
}
|
|
257
|
+
return [];
|
|
258
|
+
}
|
|
259
|
+
function canonicalExistingPath(value) {
|
|
260
|
+
try {
|
|
261
|
+
return realpathSync(value);
|
|
262
|
+
}
|
|
263
|
+
catch {
|
|
264
|
+
return resolve(value);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
const CONTROL_ID = /^[A-Za-z0-9_:-]{16,128}$/;
|
|
268
|
+
const MAX_ACK_BYTES = 4096;
|
|
269
|
+
function lifecycleGenerationMeta() {
|
|
270
|
+
const configured = process.env.DISTRIBUTED_LIFECYCLE_DIR;
|
|
271
|
+
if (configured === undefined || !isAbsolute(configured))
|
|
272
|
+
return [];
|
|
273
|
+
try {
|
|
274
|
+
const encoded = readFileSync(join(resolve(configured), 'dev.json'));
|
|
275
|
+
if (encoded.byteLength > MAX_LIFECYCLE_STATE_BYTES)
|
|
276
|
+
return [];
|
|
277
|
+
const state = JSON.parse(encoded.toString('utf8'));
|
|
278
|
+
const generationId = state.active?.generationId;
|
|
279
|
+
if (typeof generationId !== 'string' ||
|
|
280
|
+
generationId.length === 0 ||
|
|
281
|
+
generationId.length > 512 ||
|
|
282
|
+
generationId !== generationId.trim() ||
|
|
283
|
+
/[\u0000-\u001f\u007f]/.test(generationId))
|
|
284
|
+
return [];
|
|
285
|
+
return [Object.freeze({
|
|
286
|
+
tag: 'meta',
|
|
287
|
+
attrs: Object.freeze({ name: GENERATION_META, content: generationId }),
|
|
288
|
+
injectTo: 'head-prepend'
|
|
289
|
+
})];
|
|
290
|
+
}
|
|
291
|
+
catch {
|
|
292
|
+
// A lifecycle file is atomically replaced. Missing or malformed state
|
|
293
|
+
// simply omits the hint; the browser falls back to its first poll.
|
|
294
|
+
return [];
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
function configureLifecycleServer(server) {
|
|
298
|
+
const configured = process.env.DISTRIBUTED_LIFECYCLE_DIR;
|
|
299
|
+
if (configured === undefined || !isAbsolute(configured))
|
|
300
|
+
return;
|
|
301
|
+
const lifecycleRoot = resolve(configured);
|
|
302
|
+
server.middlewares?.use('/__distributed/lifecycle', (request, response) => {
|
|
303
|
+
void handleLifecycleRequest(lifecycleRoot, request, response).catch(() => {
|
|
304
|
+
if (response.statusCode < 400)
|
|
305
|
+
response.statusCode = 500;
|
|
306
|
+
response.end();
|
|
307
|
+
});
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
async function handleLifecycleRequest(lifecycleRoot, request, response) {
|
|
311
|
+
response.setHeader('cache-control', 'no-store');
|
|
312
|
+
if (request.method === 'GET') {
|
|
313
|
+
const participant = singleHeader(request.headers['x-distributed-participant']);
|
|
314
|
+
if (participant !== undefined && CONTROL_ID.test(participant)) {
|
|
315
|
+
await writeParticipantHeartbeat(lifecycleRoot, participant).catch(() => undefined);
|
|
316
|
+
}
|
|
317
|
+
const state = await readLifecycleState(lifecycleRoot);
|
|
318
|
+
if (state === undefined) {
|
|
319
|
+
response.statusCode = 404;
|
|
320
|
+
response.end();
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
response.statusCode = 200;
|
|
324
|
+
response.setHeader('content-type', 'application/json');
|
|
325
|
+
response.end(state);
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
if (request.method === 'POST') {
|
|
329
|
+
const contentType = singleHeader(request.headers['content-type']);
|
|
330
|
+
if (contentType?.split(';', 1)[0]?.trim().toLowerCase() !== 'application/json') {
|
|
331
|
+
response.statusCode = 415;
|
|
332
|
+
response.end();
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
if (!sameOriginLifecycleRequest(request)) {
|
|
336
|
+
response.statusCode = 403;
|
|
337
|
+
response.end();
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
const body = JSON.parse(await readRequestBody(request, MAX_ACK_BYTES));
|
|
341
|
+
if (!CONTROL_ID.test(String(body.transitionId ?? '')) ||
|
|
342
|
+
!CONTROL_ID.test(String(body.participantId ?? '')) ||
|
|
343
|
+
typeof body.ok !== 'boolean') {
|
|
344
|
+
response.statusCode = 400;
|
|
345
|
+
response.end();
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
const stateSource = await readLifecycleState(lifecycleRoot);
|
|
349
|
+
const state = stateSource === undefined ? undefined : JSON.parse(stateSource);
|
|
350
|
+
if (state?.phase !== 'preparing' || state.transitionId !== body.transitionId) {
|
|
351
|
+
response.statusCode = 409;
|
|
352
|
+
response.end();
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
await writeControlJson(join(lifecycleRoot, 'dev-control', 'acks', String(body.transitionId)), `${String(body.participantId)}.json`, { ok: body.ok });
|
|
356
|
+
response.statusCode = 204;
|
|
357
|
+
response.end();
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
response.statusCode = 405;
|
|
361
|
+
response.end();
|
|
362
|
+
}
|
|
363
|
+
function writeParticipantHeartbeat(root, participant) {
|
|
364
|
+
return writeControlJson(join(root, 'dev-control', 'participants'), `${participant}.json`, { seenAtUnixMs: Date.now() });
|
|
365
|
+
}
|
|
366
|
+
async function readLifecycleState(root) {
|
|
367
|
+
const path = join(root, 'dev.json');
|
|
368
|
+
let metadata;
|
|
369
|
+
try {
|
|
370
|
+
metadata = await lstat(path);
|
|
371
|
+
}
|
|
372
|
+
catch (error) {
|
|
373
|
+
if (error.code === 'ENOENT')
|
|
374
|
+
return undefined;
|
|
375
|
+
throw error;
|
|
376
|
+
}
|
|
377
|
+
if (metadata.isSymbolicLink() || !metadata.isFile() || metadata.size > MAX_LIFECYCLE_STATE_BYTES) {
|
|
378
|
+
throw new Error('invalid Distributed lifecycle state file');
|
|
379
|
+
}
|
|
380
|
+
return readFile(path, 'utf8');
|
|
381
|
+
}
|
|
382
|
+
async function writeControlJson(directory, name, value) {
|
|
383
|
+
await mkdir(directory, { recursive: true });
|
|
384
|
+
const temporary = join(directory, `.${name}.${process.pid}.${Date.now()}.${randomUUID()}`);
|
|
385
|
+
await writeFile(temporary, `${JSON.stringify(value)}\n`, { flag: 'wx', mode: 0o600 });
|
|
386
|
+
await rename(temporary, join(directory, name));
|
|
387
|
+
}
|
|
388
|
+
function sameOriginLifecycleRequest(request) {
|
|
389
|
+
const origin = singleHeader(request.headers.origin);
|
|
390
|
+
const host = singleHeader(request.headers.host);
|
|
391
|
+
if (origin === undefined || host === undefined)
|
|
392
|
+
return false;
|
|
393
|
+
try {
|
|
394
|
+
const parsed = new URL(origin);
|
|
395
|
+
const forwarded = singleHeader(request.headers['x-forwarded-proto']);
|
|
396
|
+
const socket = request.socket;
|
|
397
|
+
const protocol = forwarded === 'http' || forwarded === 'https'
|
|
398
|
+
? `${forwarded}:`
|
|
399
|
+
: socket?.encrypted === true
|
|
400
|
+
? 'https:'
|
|
401
|
+
: 'http:';
|
|
402
|
+
return (parsed.protocol === protocol &&
|
|
403
|
+
parsed.host === host &&
|
|
404
|
+
parsed.origin === origin);
|
|
405
|
+
}
|
|
406
|
+
catch {
|
|
407
|
+
return false;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
function singleHeader(value) {
|
|
411
|
+
return typeof value === 'string' ? value : value?.[0];
|
|
412
|
+
}
|
|
413
|
+
function readRequestBody(request, maximum) {
|
|
414
|
+
return new Promise((resolvePromise, reject) => {
|
|
415
|
+
const chunks = [];
|
|
416
|
+
let bytes = 0;
|
|
417
|
+
let exceeded = false;
|
|
418
|
+
request.on('data', (chunk) => {
|
|
419
|
+
if (exceeded)
|
|
420
|
+
return;
|
|
421
|
+
bytes += chunk.byteLength;
|
|
422
|
+
if (bytes > maximum) {
|
|
423
|
+
exceeded = true;
|
|
424
|
+
chunks.length = 0;
|
|
425
|
+
reject(new Error('lifecycle acknowledgement exceeds bound'));
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
chunks.push(chunk);
|
|
429
|
+
});
|
|
430
|
+
request.on('error', reject);
|
|
431
|
+
request.on('end', () => {
|
|
432
|
+
if (!exceeded)
|
|
433
|
+
resolvePromise(Buffer.concat(chunks).toString('utf8'));
|
|
434
|
+
});
|
|
435
|
+
});
|
|
436
|
+
}
|
|
189
437
|
async function runCompilerOnce(options, mode) {
|
|
190
438
|
const integration = resolveIntegration(options, options.cwd ?? process.cwd());
|
|
191
439
|
await validateResolvedPaths(integration);
|
|
@@ -287,7 +535,8 @@ function resolveIntegration(options, fallbackCwd) {
|
|
|
287
535
|
routes: Object.freeze(routes),
|
|
288
536
|
out,
|
|
289
537
|
entry: join(out, GENERATED_SVELTEKIT_MODULE),
|
|
290
|
-
watchRoots: Object.freeze(documentWatchRoots(cwd, documents, out))
|
|
538
|
+
watchRoots: Object.freeze(documentWatchRoots(cwd, documents, out)),
|
|
539
|
+
manifestWatchRoots: Object.freeze(manifestWatchRoots(cwd, client.manifest))
|
|
291
540
|
});
|
|
292
541
|
});
|
|
293
542
|
return Object.freeze({
|
|
@@ -352,6 +601,32 @@ function documentWatchRoots(cwd, documents, out) {
|
|
|
352
601
|
}
|
|
353
602
|
return [...roots].sort();
|
|
354
603
|
}
|
|
604
|
+
function manifestWatchRoots(cwd, manifest) {
|
|
605
|
+
const manifestPath = typeof manifest === 'string'
|
|
606
|
+
? resolve(cwd, manifest)
|
|
607
|
+
: (() => {
|
|
608
|
+
const index = manifest.args.indexOf('--manifest-path');
|
|
609
|
+
return index === -1 || manifest.args[index + 1] === undefined
|
|
610
|
+
? undefined
|
|
611
|
+
: resolve(cwd, manifest.args[index + 1]);
|
|
612
|
+
})();
|
|
613
|
+
if (manifestPath === undefined)
|
|
614
|
+
return [];
|
|
615
|
+
const root = dirname(manifestPath);
|
|
616
|
+
return [manifestPath, join(root, 'src'), join(root, 'crates')]
|
|
617
|
+
.filter((candidate) => existsSync(candidate))
|
|
618
|
+
.sort();
|
|
619
|
+
}
|
|
620
|
+
function isCompilerInput(file, integration) {
|
|
621
|
+
if (isGraphqlInput(file, integration))
|
|
622
|
+
return true;
|
|
623
|
+
const absolute = resolve(integration.cwd, file);
|
|
624
|
+
const basenameValue = basename(absolute);
|
|
625
|
+
if (!absolute.endsWith('.rs') && basenameValue !== 'Cargo.toml' && basenameValue !== 'Cargo.lock') {
|
|
626
|
+
return false;
|
|
627
|
+
}
|
|
628
|
+
return integration.clients.some((client) => client.manifestWatchRoots.some((root) => isWithin(root, absolute)));
|
|
629
|
+
}
|
|
355
630
|
function isGraphqlInput(file, integration) {
|
|
356
631
|
const absolute = resolve(integration.cwd, file);
|
|
357
632
|
if ((!absolute.endsWith('.graphql') && !absolute.endsWith('.gql')) ||
|
|
@@ -366,7 +641,8 @@ function isGraphqlInput(file, integration) {
|
|
|
366
641
|
async function compileTransaction(integration, children, signal) {
|
|
367
642
|
throwIfAborted(signal);
|
|
368
643
|
await validateResolvedPaths(integration);
|
|
369
|
-
const
|
|
644
|
+
const transactionRoot = await compilerTransactionRoot(integration);
|
|
645
|
+
const transaction = await mkdtemp(join(transactionRoot, '.distributed-sveltekit-'));
|
|
370
646
|
const staged = [];
|
|
371
647
|
try {
|
|
372
648
|
for (const [index, client] of integration.clients.entries()) {
|
|
@@ -406,7 +682,7 @@ async function compileTransaction(integration, children, signal) {
|
|
|
406
682
|
output
|
|
407
683
|
];
|
|
408
684
|
await runCommand(integration, args, children, signal);
|
|
409
|
-
await validateGeneratedEntrypoint(
|
|
685
|
+
await validateGeneratedEntrypoint(transaction, output, client.module);
|
|
410
686
|
staged.push({
|
|
411
687
|
client,
|
|
412
688
|
output,
|
|
@@ -424,7 +700,8 @@ async function compileTransaction(integration, children, signal) {
|
|
|
424
700
|
async function checkTransaction(integration, children, signal) {
|
|
425
701
|
throwIfAborted(signal);
|
|
426
702
|
await validateResolvedPaths(integration);
|
|
427
|
-
const
|
|
703
|
+
const transactionRoot = await compilerTransactionRoot(integration);
|
|
704
|
+
const transaction = await mkdtemp(join(transactionRoot, '.distributed-sveltekit-check-'));
|
|
428
705
|
try {
|
|
429
706
|
for (const [index, client] of integration.clients.entries()) {
|
|
430
707
|
throwIfAborted(signal);
|
|
@@ -450,6 +727,14 @@ async function checkTransaction(integration, children, signal) {
|
|
|
450
727
|
await rm(transaction, { recursive: true, force: true });
|
|
451
728
|
}
|
|
452
729
|
}
|
|
730
|
+
async function compilerTransactionRoot(integration) {
|
|
731
|
+
const lifecycle = process.env.DISTRIBUTED_LIFECYCLE_DIR;
|
|
732
|
+
const root = lifecycle !== undefined && isAbsolute(lifecycle)
|
|
733
|
+
? resolve(lifecycle)
|
|
734
|
+
: integration.cwd;
|
|
735
|
+
await mkdir(root, { recursive: true });
|
|
736
|
+
return root;
|
|
737
|
+
}
|
|
453
738
|
async function materializeManifest(integration, client, transaction, index, children, signal) {
|
|
454
739
|
throwIfAborted(signal);
|
|
455
740
|
if (typeof client.manifest === 'string') {
|
|
@@ -499,6 +784,9 @@ async function commitOutputs(integration, staged, signal) {
|
|
|
499
784
|
throwIfAborted(signal);
|
|
500
785
|
await mkdir(dirname(item.client.out), { recursive: true });
|
|
501
786
|
await validateNearestExistingParent(integration.cwd, item.client.out);
|
|
787
|
+
if (item.hadOutput && await generatedTreesEqual(item.client.out, item.output)) {
|
|
788
|
+
continue;
|
|
789
|
+
}
|
|
502
790
|
if (item.hadOutput)
|
|
503
791
|
await rename(item.client.out, item.backup);
|
|
504
792
|
try {
|
|
@@ -522,6 +810,56 @@ async function commitOutputs(integration, staged, signal) {
|
|
|
522
810
|
throw error;
|
|
523
811
|
}
|
|
524
812
|
}
|
|
813
|
+
async function generatedTreesEqual(left, right) {
|
|
814
|
+
const budget = { files: 0, bytes: 0 };
|
|
815
|
+
const compare = async (leftDirectory, rightDirectory) => {
|
|
816
|
+
const [leftEntries, rightEntries] = await Promise.all([
|
|
817
|
+
readdir(leftDirectory, { withFileTypes: true }),
|
|
818
|
+
readdir(rightDirectory, { withFileTypes: true })
|
|
819
|
+
]);
|
|
820
|
+
leftEntries.sort((a, b) => a.name.localeCompare(b.name));
|
|
821
|
+
rightEntries.sort((a, b) => a.name.localeCompare(b.name));
|
|
822
|
+
if (leftEntries.length !== rightEntries.length)
|
|
823
|
+
return false;
|
|
824
|
+
for (let index = 0; index < leftEntries.length; index += 1) {
|
|
825
|
+
const leftEntry = leftEntries[index];
|
|
826
|
+
const rightEntry = rightEntries[index];
|
|
827
|
+
if (leftEntry.name !== rightEntry.name ||
|
|
828
|
+
leftEntry.isDirectory() !== rightEntry.isDirectory() ||
|
|
829
|
+
leftEntry.isFile() !== rightEntry.isFile())
|
|
830
|
+
return false;
|
|
831
|
+
if (!leftEntry.isDirectory() && !leftEntry.isFile())
|
|
832
|
+
return false;
|
|
833
|
+
const leftPath = join(leftDirectory, leftEntry.name);
|
|
834
|
+
const rightPath = join(rightDirectory, rightEntry.name);
|
|
835
|
+
if (leftEntry.isDirectory()) {
|
|
836
|
+
if (!await compare(leftPath, rightPath))
|
|
837
|
+
return false;
|
|
838
|
+
continue;
|
|
839
|
+
}
|
|
840
|
+
budget.files += 1;
|
|
841
|
+
if (budget.files > MAX_GENERATED_COMPARE_FILES)
|
|
842
|
+
return false;
|
|
843
|
+
const [leftMetadata, rightMetadata] = await Promise.all([
|
|
844
|
+
lstat(leftPath),
|
|
845
|
+
lstat(rightPath)
|
|
846
|
+
]);
|
|
847
|
+
if (leftMetadata.size !== rightMetadata.size)
|
|
848
|
+
return false;
|
|
849
|
+
budget.bytes += leftMetadata.size;
|
|
850
|
+
if (budget.bytes > MAX_GENERATED_COMPARE_BYTES)
|
|
851
|
+
return false;
|
|
852
|
+
const [leftBytes, rightBytes] = await Promise.all([
|
|
853
|
+
readFile(leftPath),
|
|
854
|
+
readFile(rightPath)
|
|
855
|
+
]);
|
|
856
|
+
if (!leftBytes.equals(rightBytes))
|
|
857
|
+
return false;
|
|
858
|
+
}
|
|
859
|
+
return true;
|
|
860
|
+
};
|
|
861
|
+
return compare(left, right);
|
|
862
|
+
}
|
|
525
863
|
async function runCommand(integration, args, children, signal) {
|
|
526
864
|
throwIfAborted(signal);
|
|
527
865
|
const argv = [...integration.commandArgs, ...args];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hops-ops/distributed",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.9.0",
|
|
4
4
|
"description": "Typed GraphQL client, causal replica, command runtime, and framework adapters for Distributed services",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "UNLICENSED",
|
|
@@ -51,7 +51,8 @@
|
|
|
51
51
|
},
|
|
52
52
|
"dependencies": {
|
|
53
53
|
"@graphql-typed-document-node/core": "^3.2.0",
|
|
54
|
-
"graphql": "^16.14.0"
|
|
54
|
+
"graphql": "^16.14.0",
|
|
55
|
+
"wasm-pack": "0.15.0"
|
|
55
56
|
},
|
|
56
57
|
"peerDependencies": {
|
|
57
58
|
"react": "^18.2.0 || ^19.0.0",
|