@hops-ops/distributed 4.9.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/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 +5 -3
- package/dist/sveltekit/index.js +4 -2
- package/dist/sveltekit/islands/boundaries.d.ts +104 -0
- package/dist/sveltekit/islands/boundaries.js +734 -0
- package/dist/sveltekit/operation-identity.d.ts +4 -0
- package/dist/sveltekit/operation-identity.js +10 -0
- package/dist/sveltekit/replica.d.ts +24 -0
- package/dist/sveltekit/replica.js +72 -5
- package/dist/sveltekit/server-replica.d.ts +10 -26
- package/dist/sveltekit/server-replica.js +159 -132
- package/dist/sveltekit/vite.d.ts +10 -3
- package/dist/sveltekit/vite.js +298 -29
- package/package.json +2 -2
package/dist/sveltekit/vite.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { type DistributedSvelteKitBoundaryRegistration } from './islands/boundaries.js';
|
|
2
|
+
export { analyzeDistributedSvelteKitBoundaries, validateDistributedSvelteKitBoundaryPlan, type DistributedIslandInventory, type DistributedIslandPlanInput, type DistributedSvelteKitBoundary, type DistributedSvelteKitBoundaryAnalysisClient, type DistributedSvelteKitBoundaryAnalysisOptions, type DistributedSvelteKitBoundaryOccurrence, type DistributedSvelteKitBoundaryPlan, type DistributedSvelteKitBoundaryRegistration } from './islands/boundaries.js';
|
|
1
3
|
export type DistributedGraphqlProxyOptions = {
|
|
2
4
|
/** Absolute Distributed API origin, for example `http://127.0.0.1:8791`. */
|
|
3
5
|
target: string;
|
|
@@ -28,8 +30,8 @@ export type DistributedSvelteKitClientCompiler = Readonly<{
|
|
|
28
30
|
surface?: string;
|
|
29
31
|
/** GraphQL globs passed verbatim as repeated `distributed client --documents`. */
|
|
30
32
|
documents: readonly string[];
|
|
31
|
-
/**
|
|
32
|
-
|
|
33
|
+
/** Typed fallback when static component ownership cannot be proven. */
|
|
34
|
+
boundaries?: readonly DistributedSvelteKitBoundaryRegistration[];
|
|
33
35
|
/** Compiler-owned artifact directory, relative to `cwd` by default. */
|
|
34
36
|
out: string;
|
|
35
37
|
}>;
|
|
@@ -40,6 +42,12 @@ export type DistributedSvelteKitViteOptions = Readonly<{
|
|
|
40
42
|
command?: string;
|
|
41
43
|
/** Prefix argv, e.g. `cargo run ... --`; never interpreted by a shell. */
|
|
42
44
|
commandArgs?: readonly string[];
|
|
45
|
+
/** SvelteKit route source root. Defaults to `src/routes`. */
|
|
46
|
+
routesDir?: string;
|
|
47
|
+
/** SvelteKit library source root. Defaults to `src/lib`. */
|
|
48
|
+
libDir?: string;
|
|
49
|
+
/** Additional project-local `$name` aliases used by static component imports. */
|
|
50
|
+
aliases?: Readonly<Record<string, string>>;
|
|
43
51
|
clients: readonly DistributedSvelteKitClientCompiler[];
|
|
44
52
|
}>;
|
|
45
53
|
type ViteWebSocketLike = Readonly<{
|
|
@@ -136,4 +144,3 @@ export declare function distributedSvelteKit(options: DistributedSvelteKitViteOp
|
|
|
136
144
|
* generation and is safe to call from `svelte.config.js`.
|
|
137
145
|
*/
|
|
138
146
|
export declare function distributedSvelteKitAliases(options: Pick<DistributedSvelteKitViteOptions, 'cwd' | 'clients'>): Readonly<Record<string, string>>;
|
|
139
|
-
export {};
|
package/dist/sveltekit/vite.js
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
2
|
import { randomUUID } from 'node:crypto';
|
|
3
3
|
import { existsSync, lstatSync, readFileSync, realpathSync } from 'node:fs';
|
|
4
|
-
import {
|
|
4
|
+
import { lstat, mkdir, mkdtemp, open, readFile, readdir, realpath, rename, rm, unlink, writeFile } from 'node:fs/promises';
|
|
5
5
|
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
6
6
|
import { isMainThread } from 'node:worker_threads';
|
|
7
|
+
import { analyzeDistributedSvelteKitBoundaries, validateDistributedSvelteKitBoundaryPlan } from './islands/boundaries.js';
|
|
8
|
+
export { analyzeDistributedSvelteKitBoundaries, validateDistributedSvelteKitBoundaryPlan } from './islands/boundaries.js';
|
|
7
9
|
const GENERATED_SVELTEKIT_MODULE = 'sveltekit.ts';
|
|
10
|
+
const GENERATED_BOUNDARIES_MODULE = 'boundaries.ts';
|
|
8
11
|
const MAX_COMMAND_OUTPUT_BYTES = 16 * 1024 * 1024;
|
|
9
12
|
const MAX_GENERATED_COMPARE_FILES = 20_000;
|
|
10
13
|
const MAX_GENERATED_COMPARE_BYTES = 128 * 1024 * 1024;
|
|
@@ -162,10 +165,14 @@ export function distributedSvelteKit(options) {
|
|
|
162
165
|
configureLifecycleServer(server);
|
|
163
166
|
const integration = requireResolved(resolved);
|
|
164
167
|
if (!lifecycleOwnsCompile) {
|
|
165
|
-
const roots =
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
168
|
+
const roots = [
|
|
169
|
+
integration.routesDir,
|
|
170
|
+
integration.libDir,
|
|
171
|
+
...integration.clients.flatMap((client) => [
|
|
172
|
+
...client.watchRoots,
|
|
173
|
+
...client.manifestWatchRoots
|
|
174
|
+
])
|
|
175
|
+
];
|
|
169
176
|
if (roots.length > 0)
|
|
170
177
|
server.watcher.add(roots);
|
|
171
178
|
}
|
|
@@ -177,6 +184,8 @@ export function distributedSvelteKit(options) {
|
|
|
177
184
|
const integration = requireResolved(resolved);
|
|
178
185
|
if (lifecycleOwnsCompile)
|
|
179
186
|
return;
|
|
187
|
+
this.addWatchFile(integration.routesDir);
|
|
188
|
+
this.addWatchFile(integration.libDir);
|
|
180
189
|
for (const client of integration.clients) {
|
|
181
190
|
for (const root of [...client.watchRoots, ...client.manifestWatchRoots])
|
|
182
191
|
this.addWatchFile(root);
|
|
@@ -203,7 +212,7 @@ export function distributedSvelteKit(options) {
|
|
|
203
212
|
if (lifecycleOwnsCompile)
|
|
204
213
|
return [];
|
|
205
214
|
try {
|
|
206
|
-
await compile(`GraphQL change ${context.file}`);
|
|
215
|
+
await compile(`GraphQL/Svelte change ${context.file}`);
|
|
207
216
|
}
|
|
208
217
|
catch (error) {
|
|
209
218
|
context.server.ws.send({
|
|
@@ -231,7 +240,7 @@ export function distributedSvelteKit(options) {
|
|
|
231
240
|
if (lifecycleOwnsCompile)
|
|
232
241
|
return;
|
|
233
242
|
if (isCompilerInput(id, integration)) {
|
|
234
|
-
await compile(`GraphQL watch change ${id}`);
|
|
243
|
+
await compile(`GraphQL/Svelte watch change ${id}`);
|
|
235
244
|
}
|
|
236
245
|
},
|
|
237
246
|
closeBundle: stop
|
|
@@ -488,6 +497,14 @@ function resolveIntegration(options, fallbackCwd) {
|
|
|
488
497
|
}
|
|
489
498
|
const modules = new Set();
|
|
490
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
|
+
})));
|
|
491
508
|
const clients = options.clients.map((client, index) => {
|
|
492
509
|
if (client === null || typeof client !== 'object') {
|
|
493
510
|
throw new TypeError(`Distributed client[${index}] must be an object`);
|
|
@@ -515,7 +532,21 @@ function resolveIntegration(options, fallbackCwd) {
|
|
|
515
532
|
throw new TypeError(`Distributed client \`${client.module}\` requires at least one GraphQL document glob`);
|
|
516
533
|
}
|
|
517
534
|
const documents = client.documents.map((document, documentIndex) => nonempty(document, `${client.module} documents[${documentIndex}]`));
|
|
518
|
-
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
|
+
});
|
|
519
550
|
validateManifestSource(client.module, client.manifest);
|
|
520
551
|
const out = containedPath(cwd, client.out, `${client.module} generated output`);
|
|
521
552
|
if (out === cwd) {
|
|
@@ -527,14 +558,16 @@ function resolveIntegration(options, fallbackCwd) {
|
|
|
527
558
|
}
|
|
528
559
|
}
|
|
529
560
|
outputs.push(out);
|
|
561
|
+
const adapterOut = join(cwd, '.svelte-kit', 'distributed', 'clients', Buffer.from(client.module).toString('base64url'));
|
|
530
562
|
return Object.freeze({
|
|
531
563
|
module: client.module,
|
|
532
564
|
manifest: client.manifest,
|
|
533
565
|
selector,
|
|
534
566
|
documents: Object.freeze(documents),
|
|
535
|
-
|
|
567
|
+
boundaries: Object.freeze(boundaries),
|
|
536
568
|
out,
|
|
537
569
|
entry: join(out, GENERATED_SVELTEKIT_MODULE),
|
|
570
|
+
adapterOut,
|
|
538
571
|
watchRoots: Object.freeze(documentWatchRoots(cwd, documents, out)),
|
|
539
572
|
manifestWatchRoots: Object.freeze(manifestWatchRoots(cwd, client.manifest))
|
|
540
573
|
});
|
|
@@ -548,6 +581,9 @@ function resolveIntegration(options, fallbackCwd) {
|
|
|
548
581
|
}
|
|
549
582
|
return argument;
|
|
550
583
|
})),
|
|
584
|
+
routesDir,
|
|
585
|
+
libDir,
|
|
586
|
+
aliases,
|
|
551
587
|
clients: Object.freeze(clients)
|
|
552
588
|
});
|
|
553
589
|
}
|
|
@@ -629,6 +665,11 @@ function isCompilerInput(file, integration) {
|
|
|
629
665
|
}
|
|
630
666
|
function isGraphqlInput(file, integration) {
|
|
631
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
|
+
}
|
|
632
673
|
if ((!absolute.endsWith('.graphql') && !absolute.endsWith('.gql')) ||
|
|
633
674
|
!isWithin(integration.cwd, absolute)) {
|
|
634
675
|
return false;
|
|
@@ -656,17 +697,12 @@ async function compileTransaction(integration, children, signal) {
|
|
|
656
697
|
throw new Error(`generated output ${client.out} must be a real directory`);
|
|
657
698
|
}
|
|
658
699
|
hadOutput = true;
|
|
659
|
-
await cp(client.out, output, {
|
|
660
|
-
recursive: true,
|
|
661
|
-
errorOnExist: true,
|
|
662
|
-
force: false,
|
|
663
|
-
dereference: false
|
|
664
|
-
});
|
|
665
700
|
}
|
|
666
701
|
catch (error) {
|
|
667
702
|
if (!isMissing(error))
|
|
668
703
|
throw error;
|
|
669
704
|
}
|
|
705
|
+
await mkdir(output, { recursive: true });
|
|
670
706
|
const args = [
|
|
671
707
|
'client',
|
|
672
708
|
'--manifest',
|
|
@@ -677,7 +713,6 @@ async function compileTransaction(integration, children, signal) {
|
|
|
677
713
|
'--documents',
|
|
678
714
|
document
|
|
679
715
|
]),
|
|
680
|
-
...client.routes.flatMap((route) => ['--route', route]),
|
|
681
716
|
'--out',
|
|
682
717
|
output
|
|
683
718
|
];
|
|
@@ -687,9 +722,24 @@ async function compileTransaction(integration, children, signal) {
|
|
|
687
722
|
client,
|
|
688
723
|
output,
|
|
689
724
|
backup: join(transaction, `backup-${index}`),
|
|
690
|
-
hadOutput
|
|
725
|
+
hadOutput,
|
|
726
|
+
adapterOutput: join(transaction, `adapter-${index}`),
|
|
727
|
+
adapterBackup: join(transaction, `adapter-backup-${index}`),
|
|
728
|
+
hadAdapterOutput: await realDirectoryExists(client.adapterOut)
|
|
691
729
|
});
|
|
692
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
|
+
}
|
|
693
743
|
throwIfAborted(signal);
|
|
694
744
|
await commitOutputs(integration, staged, signal);
|
|
695
745
|
}
|
|
@@ -703,12 +753,14 @@ async function checkTransaction(integration, children, signal) {
|
|
|
703
753
|
const transactionRoot = await compilerTransactionRoot(integration);
|
|
704
754
|
const transaction = await mkdtemp(join(transactionRoot, '.distributed-sveltekit-check-'));
|
|
705
755
|
try {
|
|
756
|
+
const staged = [];
|
|
706
757
|
for (const [index, client] of integration.clients.entries()) {
|
|
707
758
|
throwIfAborted(signal);
|
|
708
759
|
const manifest = await materializeManifest(integration, client, transaction, index, children, signal);
|
|
760
|
+
const output = join(transaction, `output-${index}`);
|
|
761
|
+
await mkdir(output, { recursive: true });
|
|
709
762
|
await runCommand(integration, [
|
|
710
763
|
'client',
|
|
711
|
-
'--check',
|
|
712
764
|
'--manifest',
|
|
713
765
|
manifest,
|
|
714
766
|
client.selector[0],
|
|
@@ -717,10 +769,24 @@ async function checkTransaction(integration, children, signal) {
|
|
|
717
769
|
'--documents',
|
|
718
770
|
document
|
|
719
771
|
]),
|
|
720
|
-
...client.routes.flatMap((route) => ['--route', route]),
|
|
721
772
|
'--out',
|
|
722
|
-
|
|
773
|
+
output
|
|
723
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);
|
|
724
790
|
}
|
|
725
791
|
}
|
|
726
792
|
finally {
|
|
@@ -735,6 +801,195 @@ async function compilerTransactionRoot(integration) {
|
|
|
735
801
|
await mkdir(root, { recursive: true });
|
|
736
802
|
return root;
|
|
737
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
|
+
}
|
|
738
993
|
async function materializeManifest(integration, client, transaction, index, children, signal) {
|
|
739
994
|
throwIfAborted(signal);
|
|
740
995
|
if (typeof client.manifest === 'string') {
|
|
@@ -778,23 +1033,37 @@ async function validateGeneratedEntrypoint(cwd, output, module) {
|
|
|
778
1033
|
async function commitOutputs(integration, staged, signal) {
|
|
779
1034
|
throwIfAborted(signal);
|
|
780
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
|
+
]);
|
|
781
1050
|
const applied = [];
|
|
782
1051
|
try {
|
|
783
|
-
for (const item of
|
|
1052
|
+
for (const item of outputs) {
|
|
784
1053
|
throwIfAborted(signal);
|
|
785
|
-
await mkdir(dirname(item.
|
|
786
|
-
await validateNearestExistingParent(integration.cwd, item.
|
|
787
|
-
if (item.hadOutput && await generatedTreesEqual(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)) {
|
|
788
1057
|
continue;
|
|
789
1058
|
}
|
|
790
1059
|
if (item.hadOutput)
|
|
791
|
-
await rename(item.
|
|
1060
|
+
await rename(item.target, item.backup);
|
|
792
1061
|
try {
|
|
793
|
-
await rename(item.output, item.
|
|
1062
|
+
await rename(item.output, item.target);
|
|
794
1063
|
}
|
|
795
1064
|
catch (error) {
|
|
796
1065
|
if (item.hadOutput)
|
|
797
|
-
await rename(item.backup, item.
|
|
1066
|
+
await rename(item.backup, item.target);
|
|
798
1067
|
throw error;
|
|
799
1068
|
}
|
|
800
1069
|
applied.push(item);
|
|
@@ -803,9 +1072,9 @@ async function commitOutputs(integration, staged, signal) {
|
|
|
803
1072
|
}
|
|
804
1073
|
catch (error) {
|
|
805
1074
|
for (const item of [...applied].reverse()) {
|
|
806
|
-
await rm(item.
|
|
1075
|
+
await rm(item.target, { recursive: true, force: true });
|
|
807
1076
|
if (item.hadOutput)
|
|
808
|
-
await rename(item.backup, item.
|
|
1077
|
+
await rename(item.backup, item.target);
|
|
809
1078
|
}
|
|
810
1079
|
throw error;
|
|
811
1080
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hops-ops/distributed",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.10.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",
|
|
@@ -79,7 +79,7 @@
|
|
|
79
79
|
"typescript": "5.9.3"
|
|
80
80
|
},
|
|
81
81
|
"engines": {
|
|
82
|
-
"node": ">=20.
|
|
82
|
+
"node": ">=20.3.0"
|
|
83
83
|
},
|
|
84
84
|
"publishConfig": {
|
|
85
85
|
"access": "public"
|