ignotum 0.0.10 → 0.0.12
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 +8 -3
- package/dist/cli/bin.mjs +563 -104
- package/dist/cli/bin.mjs.map +1 -1
- package/dist/runtime/{api-jCl8Hzry.d.ts → api-5XSrIeqW.d.ts} +4 -3
- package/dist/runtime/client.d.ts +6 -5
- package/dist/runtime/client.js +170 -75
- package/dist/runtime/client.js.map +1 -1
- package/dist/runtime/{descriptor-C5VA9qRl-BhD-WQeO.js → descriptor-XzDX2JDw-j3O6YHgW.js} +26 -6
- package/dist/runtime/descriptor-XzDX2JDw-j3O6YHgW.js.map +1 -0
- package/dist/runtime/{id-Cs82tq9Q-Caqfx54f.d.ts → id-Cs82tq9Q-CK-maMgN.d.ts} +13 -13
- package/dist/runtime/{index-D6VLTbDB.d.ts → index-CrWg4Z0y.d.ts} +132 -76
- package/dist/runtime/internal/api.d.ts +1 -1
- package/dist/runtime/internal/host.d.ts +18 -17
- package/dist/runtime/internal/host.js +11 -6
- package/dist/runtime/internal/host.js.map +1 -1
- package/dist/runtime/internal/server.d.ts +1 -1
- package/dist/runtime/internal/server.js +1 -1
- package/dist/runtime/internal/types.d.ts +1 -1
- package/dist/runtime/internal/types.js +1 -1
- package/dist/runtime/pagination-CX5IPbEi.d.ts +1 -0
- package/dist/runtime/{pagination-Bt3l7QaC-D_zI5zsu.d.ts → pagination-D-R9NR61-CpIoHFoI.d.ts} +8 -5
- package/dist/runtime/result-DIjKM-p4.d.ts +1 -0
- package/dist/runtime/{schema-C00wobuy.js → schema-B9XxyRO8.js} +132 -16
- package/dist/runtime/schema-B9XxyRO8.js.map +1 -0
- package/dist/runtime/server.d.ts +7 -3
- package/dist/runtime/server.js +5 -3
- package/dist/runtime/server.js.map +1 -1
- package/dist/runtime/{sync-bMQq9tXx.d.ts → sync-avN7NkcU.d.ts} +2 -2
- package/package.json +1 -1
- package/src/cli/agent-files.ts +2 -0
- package/src/cli/auth-client.ts +1 -1
- package/src/cli/bin.ts +13 -3
- package/src/cli/build/server.ts +102 -13
- package/src/cli/codegen.ts +7 -4
- package/src/cli/control-client.ts +30 -8
- package/src/cli/deploy.ts +7 -2
- package/src/cli/environment.ts +133 -0
- package/src/client/files.ts +43 -27
- package/src/client/hooks.ts +26 -17
- package/src/client/page-observers.ts +77 -0
- package/src/client/sync.ts +71 -59
- package/src/dev-runtime/functions.ts +127 -4
- package/src/dev-runtime/sync.ts +38 -9
- package/src/server/index.ts +11 -1
- package/dist/runtime/descriptor-C5VA9qRl-BhD-WQeO.js.map +0 -1
- package/dist/runtime/pagination-DcIkTOFs.d.ts +0 -1
- package/dist/runtime/schema-C00wobuy.js.map +0 -1
- package/src/dev-runtime/id.ts +0 -3
package/src/cli/build/server.ts
CHANGED
|
@@ -3,7 +3,18 @@ import process from "node:process";
|
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
4
|
|
|
5
5
|
import * as Config from "effect/Config";
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
Array,
|
|
8
|
+
Crypto,
|
|
9
|
+
Effect,
|
|
10
|
+
Encoding,
|
|
11
|
+
FileSystem,
|
|
12
|
+
Path,
|
|
13
|
+
Predicate,
|
|
14
|
+
Schema,
|
|
15
|
+
Stream,
|
|
16
|
+
String,
|
|
17
|
+
} from "effect";
|
|
7
18
|
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";
|
|
8
19
|
import { createServer, defaultServerConditions, normalizePath } from "vite";
|
|
9
20
|
import type { Plugin, ViteDevServer } from "vite";
|
|
@@ -11,7 +22,10 @@ import type { Plugin, ViteDevServer } from "vite";
|
|
|
11
22
|
import {
|
|
12
23
|
ServerBuildManifest,
|
|
13
24
|
SchemaSnapshot,
|
|
25
|
+
SchemaSnapshotField,
|
|
26
|
+
EnvironmentArtifact,
|
|
14
27
|
schemaSnapshotPath,
|
|
28
|
+
serverEnvironmentPath,
|
|
15
29
|
type ServerFunctionArtifact,
|
|
16
30
|
} from "@ignotum/contracts/deployment";
|
|
17
31
|
import { FunctionAddress } from "@ignotum/contracts/runtime/sync";
|
|
@@ -19,7 +33,9 @@ import type { FunctionKind } from "@ignotum/contracts/runtime/functions";
|
|
|
19
33
|
import {
|
|
20
34
|
descriptorContainsFile,
|
|
21
35
|
descriptorFields,
|
|
36
|
+
emptyEnv,
|
|
22
37
|
getValueDescriptor,
|
|
38
|
+
isDefinedEnv,
|
|
23
39
|
isDefinedSchema,
|
|
24
40
|
ValueDescriptor,
|
|
25
41
|
type DefinedSchema,
|
|
@@ -34,6 +50,7 @@ import {
|
|
|
34
50
|
} from "@ignotum/deployment";
|
|
35
51
|
|
|
36
52
|
import { inspectRuntimeFunctionDefinition } from "../../internal/function-definition.js";
|
|
53
|
+
import { loadEnvironment } from "../environment.js";
|
|
37
54
|
import { moduleBoundariesPlugin, BuildBoundaryViolation } from "../module-boundaries.js";
|
|
38
55
|
import {
|
|
39
56
|
causeMessage,
|
|
@@ -69,6 +86,11 @@ interface DiscoveredServerFunction {
|
|
|
69
86
|
}
|
|
70
87
|
|
|
71
88
|
const ServerDiscoveryManifest = Schema.Struct({
|
|
89
|
+
environment: Schema.Struct({
|
|
90
|
+
fields: Schema.Array(SchemaSnapshotField),
|
|
91
|
+
hasDefinition: Schema.Boolean,
|
|
92
|
+
values: Schema.Record(Schema.String, Schema.String),
|
|
93
|
+
}),
|
|
72
94
|
functions: Schema.Array(DiscoveredServerFunction),
|
|
73
95
|
schema: SchemaSnapshot,
|
|
74
96
|
});
|
|
@@ -77,6 +99,10 @@ const ServerDiscoveryManifestJson = Schema.fromJsonString(ServerDiscoveryManifes
|
|
|
77
99
|
const ServerModuleNamesJson = Schema.fromJsonString(Schema.Array(Schema.String));
|
|
78
100
|
|
|
79
101
|
export interface ServerBuildResult {
|
|
102
|
+
readonly environment: {
|
|
103
|
+
readonly bytes: Uint8Array;
|
|
104
|
+
readonly path: typeof serverEnvironmentPath;
|
|
105
|
+
};
|
|
80
106
|
readonly functions: number;
|
|
81
107
|
readonly manifestPath: string;
|
|
82
108
|
}
|
|
@@ -268,6 +294,7 @@ const discoverServerFunctionsInProcess = Effect.fn("Deploy.discoverServerFunctio
|
|
|
268
294
|
return yield* Effect.scoped(
|
|
269
295
|
Effect.gen(function* () {
|
|
270
296
|
const server = yield* acquireModuleRunner(appDirectory, conditions);
|
|
297
|
+
const fileSystem = yield* FileSystem.FileSystem;
|
|
271
298
|
const path = yield* Path.Path;
|
|
272
299
|
const schemaPath = path.join(appDirectory, "server", "schema.ts");
|
|
273
300
|
const loadedSchemaModule = yield* Effect.tryPromise({
|
|
@@ -295,14 +322,59 @@ const discoverServerFunctionsInProcess = Effect.fn("Deploy.discoverServerFunctio
|
|
|
295
322
|
message: "The default export from server/schema.ts is not an Ignotum schema.",
|
|
296
323
|
});
|
|
297
324
|
}
|
|
325
|
+
const environmentPath = path.join(appDirectory, "server", "env.ts");
|
|
326
|
+
const hasEnvironmentDefinition = yield* fileSystem.exists(environmentPath);
|
|
327
|
+
const environment = hasEnvironmentDefinition
|
|
328
|
+
? yield* Effect.gen(function* () {
|
|
329
|
+
const loadedEnvironmentModule = yield* Effect.tryPromise({
|
|
330
|
+
try: () => server.ssrLoadModule(normalizePath(environmentPath)),
|
|
331
|
+
catch: (cause) =>
|
|
332
|
+
ServerFunctionDiscoveryFailed.make({
|
|
333
|
+
cause,
|
|
334
|
+
message: "Could not load the application environment declaration.",
|
|
335
|
+
path: environmentPath,
|
|
336
|
+
}),
|
|
337
|
+
});
|
|
338
|
+
if (
|
|
339
|
+
!Predicate.isObject(loadedEnvironmentModule.default) ||
|
|
340
|
+
!isDefinedEnv(loadedEnvironmentModule.default)
|
|
341
|
+
) {
|
|
342
|
+
return yield* InvalidBuildOutput.make({
|
|
343
|
+
message: "The default export from server/env.ts is not an Ignotum environment.",
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
const loaded = yield* loadEnvironment(
|
|
347
|
+
appDirectory,
|
|
348
|
+
loadedEnvironmentModule.default,
|
|
349
|
+
true,
|
|
350
|
+
);
|
|
351
|
+
return {
|
|
352
|
+
fields: loadedEnvironmentModule.default.descriptors,
|
|
353
|
+
hasDefinition: true,
|
|
354
|
+
values: loaded.raw,
|
|
355
|
+
} as const;
|
|
356
|
+
})
|
|
357
|
+
: yield* loadEnvironment(appDirectory, emptyEnv, false).pipe(
|
|
358
|
+
Effect.map((loaded) => ({
|
|
359
|
+
fields: [],
|
|
360
|
+
hasDefinition: false,
|
|
361
|
+
values: loaded.raw,
|
|
362
|
+
})),
|
|
363
|
+
);
|
|
298
364
|
const modules = yield* Effect.forEach(moduleNames, (moduleName) =>
|
|
299
365
|
discoverModuleFunctions(server, appDirectory, moduleName),
|
|
300
366
|
);
|
|
301
367
|
return {
|
|
302
368
|
functions: Array.flatten(modules),
|
|
369
|
+
environment,
|
|
303
370
|
schema: schemaCandidate,
|
|
304
371
|
} satisfies {
|
|
305
372
|
readonly functions: ReadonlyArray<DiscoveredServerFunction>;
|
|
373
|
+
readonly environment: {
|
|
374
|
+
readonly fields: ReadonlyArray<SchemaSnapshotField>;
|
|
375
|
+
readonly hasDefinition: boolean;
|
|
376
|
+
readonly values: Readonly<Record<string, string>>;
|
|
377
|
+
};
|
|
306
378
|
readonly schema: DefinedSchema<Tables>;
|
|
307
379
|
};
|
|
308
380
|
}),
|
|
@@ -312,9 +384,7 @@ const discoverServerFunctionsInProcess = Effect.fn("Deploy.discoverServerFunctio
|
|
|
312
384
|
export const runServerDiscoveryWorker = Effect.fn("Deploy.runServerDiscoveryWorker")(function* (
|
|
313
385
|
appDirectory: string,
|
|
314
386
|
moduleNamesJson: string,
|
|
315
|
-
outputPath: string,
|
|
316
387
|
) {
|
|
317
|
-
const fileSystem = yield* FileSystem.FileSystem;
|
|
318
388
|
const moduleNames = yield* Schema.decodeEffect(ServerModuleNamesJson)(moduleNamesJson).pipe(
|
|
319
389
|
Effect.orDie,
|
|
320
390
|
);
|
|
@@ -327,11 +397,11 @@ export const runServerDiscoveryWorker = Effect.fn("Deploy.runServerDiscoveryWork
|
|
|
327
397
|
message: "The application schema does not have canonical Ignotum descriptors.",
|
|
328
398
|
}),
|
|
329
399
|
});
|
|
330
|
-
|
|
400
|
+
return yield* Schema.encodeEffect(ServerDiscoveryManifestJson)({
|
|
401
|
+
environment: discovered.environment,
|
|
331
402
|
functions: discovered.functions,
|
|
332
403
|
schema,
|
|
333
404
|
});
|
|
334
|
-
yield* fileSystem.writeFileString(outputPath, encoded);
|
|
335
405
|
});
|
|
336
406
|
|
|
337
407
|
const discoveryWorkerEntry = Effect.fn("Deploy.discoveryWorkerEntry")(function* () {
|
|
@@ -365,7 +435,6 @@ const discoverServerFunctions = Effect.fn("Deploy.discoverServerFunctionsIsolate
|
|
|
365
435
|
(temporary) =>
|
|
366
436
|
fileSystem.remove(temporary, { force: true, recursive: true }).pipe(Effect.orDie),
|
|
367
437
|
);
|
|
368
|
-
const outputPath = path.join(directory, "discovery.json");
|
|
369
438
|
const encodedModuleNames = yield* Schema.encodeEffect(ServerModuleNamesJson)(moduleNames);
|
|
370
439
|
const packageRoot = path.resolve(path.dirname(workerEntry), "../..");
|
|
371
440
|
const workspaceProbes: Array<string> = [];
|
|
@@ -387,23 +456,25 @@ const discoverServerFunctions = Effect.fn("Deploy.discoverServerFunctionsIsolate
|
|
|
387
456
|
const readPermissions = [
|
|
388
457
|
appDirectory,
|
|
389
458
|
nodeModulesDirectory(),
|
|
459
|
+
path.dirname(nodeModulesDirectory()),
|
|
390
460
|
packageRoot,
|
|
391
461
|
path.join(packageRoot, "node_modules"),
|
|
462
|
+
path.resolve(packageRoot, "../..", "node_modules"),
|
|
392
463
|
...workspaceProbes,
|
|
393
464
|
].map((allowedPath) => `--allow-fs-read=${allowedPath}`);
|
|
394
|
-
const
|
|
465
|
+
const handle = yield* spawner.spawn(
|
|
395
466
|
ChildProcess.make(
|
|
396
467
|
process.execPath,
|
|
397
468
|
[
|
|
398
469
|
"--permission",
|
|
399
470
|
"--allow-addons",
|
|
471
|
+
"--conditions=@ignotum/source",
|
|
400
472
|
...readPermissions,
|
|
401
473
|
`--allow-fs-write=${directory}`,
|
|
402
474
|
workerEntry,
|
|
403
475
|
"__ignotum_discover_server",
|
|
404
476
|
appDirectory,
|
|
405
477
|
encodedModuleNames,
|
|
406
|
-
outputPath,
|
|
407
478
|
],
|
|
408
479
|
{
|
|
409
480
|
cwd: appDirectory,
|
|
@@ -414,9 +485,14 @@ const discoverServerFunctions = Effect.fn("Deploy.discoverServerFunctionsIsolate
|
|
|
414
485
|
},
|
|
415
486
|
stderr: "inherit",
|
|
416
487
|
stdout: "ignore",
|
|
488
|
+
additionalFds: { fd3: { type: "output" } },
|
|
417
489
|
},
|
|
418
490
|
),
|
|
419
491
|
);
|
|
492
|
+
const [encoded, exitCode] = yield* Effect.all(
|
|
493
|
+
[Stream.mkString(Stream.decodeText(handle.getOutputFd(3))), handle.exitCode],
|
|
494
|
+
{ concurrency: "unbounded" },
|
|
495
|
+
);
|
|
420
496
|
if (exitCode !== ChildProcessSpawner.ExitCode(0)) {
|
|
421
497
|
return yield* ServerFunctionDiscoveryFailed.make({
|
|
422
498
|
cause: new Error(`Discovery worker exited with code ${exitCode}.`),
|
|
@@ -424,13 +500,12 @@ const discoverServerFunctions = Effect.fn("Deploy.discoverServerFunctionsIsolate
|
|
|
424
500
|
path: appDirectory,
|
|
425
501
|
});
|
|
426
502
|
}
|
|
427
|
-
const encoded = yield* fileSystem.readFileString(outputPath);
|
|
428
503
|
return yield* Schema.decodeEffect(ServerDiscoveryManifestJson)(encoded).pipe(
|
|
429
504
|
Effect.mapError((cause) =>
|
|
430
505
|
ServerFunctionDiscoveryFailed.make({
|
|
431
506
|
cause,
|
|
432
507
|
message: "The isolated build process returned invalid application metadata.",
|
|
433
|
-
path:
|
|
508
|
+
path: appDirectory,
|
|
434
509
|
}),
|
|
435
510
|
),
|
|
436
511
|
);
|
|
@@ -445,8 +520,8 @@ const serverFunctionEntryPlugin = (definition: DiscoveredServerFunction): Plugin
|
|
|
445
520
|
import { invoke } from "ignotum/internal/host"
|
|
446
521
|
import { ${definition.exportName} as definition } from ${encodeJavaScriptString(normalizePath(definition.modulePath))}
|
|
447
522
|
|
|
448
|
-
export default async function handler(encodedRequest) {
|
|
449
|
-
return invoke(definition, JSON.parse(encodedRequest), call)
|
|
523
|
+
export default async function handler(encodedRequest, encodedEnvironment) {
|
|
524
|
+
return invoke(definition, JSON.parse(encodedRequest), call, JSON.parse(encodedEnvironment))
|
|
450
525
|
}
|
|
451
526
|
`;
|
|
452
527
|
|
|
@@ -573,6 +648,15 @@ export const buildServer = Effect.fn("Deploy.buildServer")(function* (
|
|
|
573
648
|
const path = yield* Path.Path;
|
|
574
649
|
const conditions = yield* serverConditions();
|
|
575
650
|
const discovered = yield* discoverServerFunctions(appDirectory, functionModules);
|
|
651
|
+
const crypto = yield* Crypto.Crypto;
|
|
652
|
+
const environmentArtifact = {
|
|
653
|
+
formatVersion: 1,
|
|
654
|
+
nonce: Encoding.encodeBase64Url(yield* crypto.randomBytes(32)),
|
|
655
|
+
values: discovered.environment.values,
|
|
656
|
+
} satisfies EnvironmentArtifact;
|
|
657
|
+
const environmentBytes = utf8Bytes(
|
|
658
|
+
`${encodeCanonical(EnvironmentArtifact, environmentArtifact)}\n`,
|
|
659
|
+
);
|
|
576
660
|
|
|
577
661
|
yield* fileSystem.makeDirectory(outputDirectory, { recursive: true });
|
|
578
662
|
const encodedSnapshot = encodeSchemaSnapshot(discovered.schema);
|
|
@@ -584,7 +668,11 @@ export const buildServer = Effect.fn("Deploy.buildServer")(function* (
|
|
|
584
668
|
{ concurrency: 4 },
|
|
585
669
|
);
|
|
586
670
|
const manifest = {
|
|
587
|
-
formatVersion:
|
|
671
|
+
formatVersion: 3,
|
|
672
|
+
environment: {
|
|
673
|
+
artifact: yield* artifactReference(serverEnvironmentPath, environmentBytes),
|
|
674
|
+
fields: discovered.environment.fields,
|
|
675
|
+
},
|
|
588
676
|
schema: yield* artifactReference(schemaSnapshotPath, snapshotBytes),
|
|
589
677
|
functions,
|
|
590
678
|
} satisfies ServerBuildManifest;
|
|
@@ -598,6 +686,7 @@ export const buildServer = Effect.fn("Deploy.buildServer")(function* (
|
|
|
598
686
|
);
|
|
599
687
|
|
|
600
688
|
return {
|
|
689
|
+
environment: { bytes: environmentBytes, path: serverEnvironmentPath },
|
|
601
690
|
functions: functions.length,
|
|
602
691
|
manifestPath,
|
|
603
692
|
} satisfies ServerBuildResult;
|
package/src/cli/codegen.ts
CHANGED
|
@@ -41,6 +41,7 @@ const isFunctionModuleFile = (fileName: string): boolean =>
|
|
|
41
41
|
!fileName.endsWith(".test.ts") &&
|
|
42
42
|
!fileName.endsWith(".spec.ts") &&
|
|
43
43
|
fileName !== "index.ts" &&
|
|
44
|
+
fileName !== "env.ts" &&
|
|
44
45
|
fileName !== "schema.ts" &&
|
|
45
46
|
!fileName.startsWith("_");
|
|
46
47
|
|
|
@@ -57,13 +58,13 @@ const moduleNameFromFile = (filePath: string, fileName: string) => {
|
|
|
57
58
|
);
|
|
58
59
|
};
|
|
59
60
|
|
|
60
|
-
const renderServerBindings = (): string => `${generatedHeader}
|
|
61
|
+
const renderServerBindings = (hasEnvironment: boolean): string => `${generatedHeader}
|
|
61
62
|
|
|
62
63
|
import { bindSchema } from "ignotum/internal/server";
|
|
63
64
|
|
|
64
|
-
import schema from "../server/schema.js";
|
|
65
|
+
${hasEnvironment ? 'import environment from "../server/env.js";\n' : ""}import schema from "../server/schema.js";
|
|
65
66
|
|
|
66
|
-
export const { mutation, query, values } = bindSchema(schema);
|
|
67
|
+
export const { mutation, query, values } = bindSchema(schema${hasEnvironment ? ", environment" : ""});
|
|
67
68
|
`;
|
|
68
69
|
|
|
69
70
|
const renderTypes = (): string => `${generatedHeader}
|
|
@@ -157,6 +158,7 @@ export const generate = Effect.fn("Codegen.generate")(function* (appDirectory: s
|
|
|
157
158
|
const path = yield* Path.Path;
|
|
158
159
|
const serverDirectory = path.join(appDirectory, "server");
|
|
159
160
|
const schemaPath = path.join(serverDirectory, "schema.ts");
|
|
161
|
+
const environmentPath = path.join(serverDirectory, "env.ts");
|
|
160
162
|
const schemaExists = yield* fileSystem.exists(schemaPath);
|
|
161
163
|
|
|
162
164
|
if (!schemaExists) {
|
|
@@ -167,13 +169,14 @@ export const generate = Effect.fn("Codegen.generate")(function* (appDirectory: s
|
|
|
167
169
|
}
|
|
168
170
|
|
|
169
171
|
const entries = yield* fileSystem.readDirectory(serverDirectory);
|
|
172
|
+
const hasEnvironment = yield* fileSystem.exists(environmentPath);
|
|
170
173
|
const functionFiles = Array.sort(String.Order)(Array.filter(entries, isFunctionModuleFile));
|
|
171
174
|
const functionModules = yield* Effect.forEach(functionFiles, (fileName) =>
|
|
172
175
|
moduleNameFromFile(path.join(serverDirectory, fileName), fileName),
|
|
173
176
|
);
|
|
174
177
|
const outputs: ReadonlyArray<GeneratedOutput> = [
|
|
175
178
|
{
|
|
176
|
-
content: renderServerBindings(),
|
|
179
|
+
content: renderServerBindings(hasEnvironment),
|
|
177
180
|
path: path.join(appDirectory, "_generated", "server.ts"),
|
|
178
181
|
},
|
|
179
182
|
{
|
|
@@ -102,11 +102,12 @@ export class ControlClientUnavailable extends Schema.TaggedError<ControlClientUn
|
|
|
102
102
|
) {}
|
|
103
103
|
|
|
104
104
|
export interface DeploymentUploadFile {
|
|
105
|
+
readonly bytes?: Uint8Array;
|
|
105
106
|
readonly contentType: string;
|
|
106
107
|
readonly path: string;
|
|
107
108
|
readonly sha256: string;
|
|
108
109
|
readonly size: number;
|
|
109
|
-
readonly sourcePath
|
|
110
|
+
readonly sourcePath?: string;
|
|
110
111
|
}
|
|
111
112
|
|
|
112
113
|
interface ControlClientService {
|
|
@@ -168,6 +169,7 @@ export const deploymentUploadFiles = Effect.fn("ControlClient.deploymentUploadFi
|
|
|
168
169
|
inventorySha256: string,
|
|
169
170
|
inventorySize: number,
|
|
170
171
|
buildDirectory: string,
|
|
172
|
+
memoryFiles: ReadonlyMap<string, Uint8Array> = new Map(),
|
|
171
173
|
) {
|
|
172
174
|
const path = yield* Path.Path;
|
|
173
175
|
return [
|
|
@@ -178,7 +180,18 @@ export const deploymentUploadFiles = Effect.fn("ControlClient.deploymentUploadFi
|
|
|
178
180
|
size: inventorySize,
|
|
179
181
|
sourcePath: path.join(buildDirectory, deploymentInventoryPath),
|
|
180
182
|
},
|
|
181
|
-
...inventory.files.map((file) =>
|
|
183
|
+
...inventory.files.map((file) => {
|
|
184
|
+
const bytes = memoryFiles.get(file.path);
|
|
185
|
+
return bytes === undefined
|
|
186
|
+
? uploadFile(file, path.join(buildDirectory, file.path))
|
|
187
|
+
: {
|
|
188
|
+
bytes,
|
|
189
|
+
contentType: file.contentType,
|
|
190
|
+
path: file.path,
|
|
191
|
+
sha256: file.sha256,
|
|
192
|
+
size: file.size,
|
|
193
|
+
};
|
|
194
|
+
}),
|
|
182
195
|
];
|
|
183
196
|
});
|
|
184
197
|
|
|
@@ -304,17 +317,26 @@ export const controlClientLayer = Layer.effect(
|
|
|
304
317
|
deploymentId: DeploymentId,
|
|
305
318
|
file: DeploymentUploadFile,
|
|
306
319
|
) {
|
|
307
|
-
const
|
|
320
|
+
const baseRequest = HttpClientRequest.put(
|
|
308
321
|
endpoint(config.apiUrl, controlDeploymentFilePath(appId, deploymentId, file.path)),
|
|
309
322
|
).pipe(
|
|
310
323
|
HttpClientRequest.setHeader(controlUploadHeaderNames.contentLength, String(file.size)),
|
|
311
324
|
HttpClientRequest.setHeader(controlUploadHeaderNames.sha256, file.sha256),
|
|
312
|
-
HttpClientRequest.bodyFile(file.sourcePath, { contentType: file.contentType }),
|
|
313
|
-
Effect.provideService(FileSystem.FileSystem, fileSystem),
|
|
314
|
-
Effect.mapError((cause) =>
|
|
315
|
-
unavailable(`Deployment file '${file.path}' could not be opened.`, cause),
|
|
316
|
-
),
|
|
317
325
|
);
|
|
326
|
+
const request = yield* file.bytes === undefined
|
|
327
|
+
? file.sourcePath === undefined
|
|
328
|
+
? Effect.fail(unavailable(`Deployment file '${file.path}' has no upload source.`, {}))
|
|
329
|
+
: HttpClientRequest.bodyFile(baseRequest, file.sourcePath, {
|
|
330
|
+
contentType: file.contentType,
|
|
331
|
+
}).pipe(
|
|
332
|
+
Effect.provideService(FileSystem.FileSystem, fileSystem),
|
|
333
|
+
Effect.mapError((cause) =>
|
|
334
|
+
unavailable(`Deployment file '${file.path}' could not be opened.`, cause),
|
|
335
|
+
),
|
|
336
|
+
)
|
|
337
|
+
: Effect.succeed(
|
|
338
|
+
HttpClientRequest.bodyUint8Array(baseRequest, file.bytes, file.contentType),
|
|
339
|
+
);
|
|
318
340
|
const response = yield* executeIdempotent(request, DeploymentFileUploadResponse);
|
|
319
341
|
return response.status;
|
|
320
342
|
});
|
package/src/cli/deploy.ts
CHANGED
|
@@ -22,6 +22,7 @@ export interface DeploymentBuildResult {
|
|
|
22
22
|
readonly inventory: DeploymentInventory;
|
|
23
23
|
readonly inventoryBytes: Uint8Array;
|
|
24
24
|
readonly outputDirectory: string;
|
|
25
|
+
readonly memoryFiles?: ReadonlyMap<string, Uint8Array>;
|
|
25
26
|
readonly serverFunctions: number;
|
|
26
27
|
}
|
|
27
28
|
|
|
@@ -119,13 +120,15 @@ export const buildDeploymentArtifact = Effect.fn("Deploy.buildArtifact")(functio
|
|
|
119
120
|
const serverOutput = path.join(stagingDirectory, "server");
|
|
120
121
|
const client = yield* buildClient(appDirectory, clientOutput);
|
|
121
122
|
const server = yield* buildServer(appDirectory, serverOutput, codegen.functionModules);
|
|
122
|
-
const
|
|
123
|
+
const diskPayloadFiles = yield* readArtifactDirectory(stagingDirectory);
|
|
124
|
+
const payloadFiles = [...diskPayloadFiles, server.environment];
|
|
123
125
|
const inventory = yield* makeDeploymentInventory(payloadFiles);
|
|
124
126
|
yield* fileSystem.writeFile(
|
|
125
127
|
path.join(stagingDirectory, deploymentInventoryPath),
|
|
126
128
|
inventory.bytes,
|
|
127
129
|
);
|
|
128
|
-
const
|
|
130
|
+
const diskArtifactFiles = yield* readArtifactDirectory(stagingDirectory);
|
|
131
|
+
const artifactFiles = [...diskArtifactFiles, server.environment];
|
|
129
132
|
yield* validateDeploymentArtifact(artifactFiles);
|
|
130
133
|
yield* Effect.logInfo("Built deployment artifact").pipe(
|
|
131
134
|
Effect.annotateLogs({
|
|
@@ -140,6 +143,7 @@ export const buildDeploymentArtifact = Effect.fn("Deploy.buildArtifact")(functio
|
|
|
140
143
|
clientAssets: client.assets,
|
|
141
144
|
inventory: inventory.inventory,
|
|
142
145
|
inventoryBytes: inventory.bytes,
|
|
146
|
+
memoryFiles: new Map([[server.environment.path, server.environment.bytes]]),
|
|
143
147
|
outputDirectory: buildDirectory,
|
|
144
148
|
serverFunctions: server.functions,
|
|
145
149
|
} satisfies DeploymentBuildResult;
|
|
@@ -158,6 +162,7 @@ export const uploadDeployment = Effect.fn("Deploy.upload")(function* (
|
|
|
158
162
|
inventorySha256,
|
|
159
163
|
build.inventoryBytes.byteLength,
|
|
160
164
|
build.outputDirectory,
|
|
165
|
+
build.memoryFiles,
|
|
161
166
|
);
|
|
162
167
|
const deployment = yield* control.beginDeployment(configuration.appId, build.inventory);
|
|
163
168
|
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { Array, ConfigProvider, Effect, FileSystem, Path, Schema } from "effect";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
decodeDefinedEnvironment,
|
|
5
|
+
emptyEnv,
|
|
6
|
+
type DefinedEnv,
|
|
7
|
+
type EnvironmentType,
|
|
8
|
+
} from "@ignotum/contracts/schema";
|
|
9
|
+
|
|
10
|
+
export class EnvironmentConfigurationInvalid extends Schema.TaggedError<EnvironmentConfigurationInvalid>()(
|
|
11
|
+
"EnvironmentConfigurationInvalid",
|
|
12
|
+
{
|
|
13
|
+
message: Schema.String,
|
|
14
|
+
path: Schema.String,
|
|
15
|
+
},
|
|
16
|
+
) {}
|
|
17
|
+
|
|
18
|
+
export interface LoadedEnvironment<Environment extends DefinedEnv = DefinedEnv> {
|
|
19
|
+
readonly definition: Environment;
|
|
20
|
+
readonly raw: Readonly<Record<string, string>>;
|
|
21
|
+
readonly value: Readonly<EnvironmentType<Environment>>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface RawEnvironment {
|
|
25
|
+
readonly path: string;
|
|
26
|
+
readonly raw: Readonly<Record<string, string>>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const collectKeys = Effect.fn("Environment.collectKeys")(function* (
|
|
30
|
+
provider: ConfigProvider.ConfigProvider,
|
|
31
|
+
providerPath: ConfigProvider.Path = [],
|
|
32
|
+
nameParts: ReadonlyArray<string> = [],
|
|
33
|
+
): Effect.fn.Return<ReadonlyArray<string>, ConfigProvider.SourceError, never> {
|
|
34
|
+
const node = yield* provider.load(providerPath);
|
|
35
|
+
if (node === undefined) return [];
|
|
36
|
+
const own = node._tag === "Value" || node.value !== undefined ? [nameParts.join("_")] : [];
|
|
37
|
+
if (node._tag === "Value") return own;
|
|
38
|
+
if (node._tag === "Array") {
|
|
39
|
+
const children = yield* Effect.forEach(Array.range(0, node.length - 1), (index) =>
|
|
40
|
+
collectKeys(provider, [...providerPath, index], [...nameParts, index.toString()]),
|
|
41
|
+
);
|
|
42
|
+
return [...own, ...Array.flatten(children)];
|
|
43
|
+
}
|
|
44
|
+
const children = yield* Effect.forEach(Array.fromIterable(node.keys), (key) =>
|
|
45
|
+
collectKeys(provider, [...providerPath, key], [...nameParts, key]),
|
|
46
|
+
);
|
|
47
|
+
return [...own, ...Array.flatten(children)];
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
const nodeValue = (node: ConfigProvider.Node | undefined): string | undefined =>
|
|
51
|
+
node?._tag === "Value" ? node.value : node?.value;
|
|
52
|
+
|
|
53
|
+
export const readEnvironmentRaw = Effect.fn("Environment.readRaw")(function* (
|
|
54
|
+
appDirectory: string,
|
|
55
|
+
hasDefinition: boolean,
|
|
56
|
+
) {
|
|
57
|
+
const fileSystem = yield* FileSystem.FileSystem;
|
|
58
|
+
const path = yield* Path.Path;
|
|
59
|
+
const environmentPath = path.join(appDirectory, ".env.ignotum");
|
|
60
|
+
const exists = yield* fileSystem.exists(environmentPath);
|
|
61
|
+
if (!hasDefinition && exists) {
|
|
62
|
+
return yield* EnvironmentConfigurationInvalid.make({
|
|
63
|
+
message: ".env.ignotum exists but server/env.ts does not define its variables.",
|
|
64
|
+
path: environmentPath,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
if (!hasDefinition) {
|
|
68
|
+
return { path: environmentPath, raw: {} } satisfies RawEnvironment;
|
|
69
|
+
}
|
|
70
|
+
if (!exists) {
|
|
71
|
+
return yield* EnvironmentConfigurationInvalid.make({
|
|
72
|
+
message: "server/env.ts requires a project-root .env.ignotum file.",
|
|
73
|
+
path: environmentPath,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const provider = yield* ConfigProvider.fromDotEnv({
|
|
78
|
+
path: environmentPath,
|
|
79
|
+
preserveEmptyStrings: true,
|
|
80
|
+
}).pipe(
|
|
81
|
+
Effect.mapError(() =>
|
|
82
|
+
EnvironmentConfigurationInvalid.make({
|
|
83
|
+
message: "Ignotum could not read the project environment file.",
|
|
84
|
+
path: environmentPath,
|
|
85
|
+
}),
|
|
86
|
+
),
|
|
87
|
+
);
|
|
88
|
+
const keys = yield* collectKeys(provider).pipe(
|
|
89
|
+
Effect.mapError(() =>
|
|
90
|
+
EnvironmentConfigurationInvalid.make({
|
|
91
|
+
message: "Ignotum could not inspect the project environment file.",
|
|
92
|
+
path: environmentPath,
|
|
93
|
+
}),
|
|
94
|
+
),
|
|
95
|
+
);
|
|
96
|
+
const entries = yield* Effect.forEach(keys, (key) =>
|
|
97
|
+
provider.load([key]).pipe(
|
|
98
|
+
Effect.map((node) => [key, nodeValue(node)] as const),
|
|
99
|
+
Effect.mapError(() =>
|
|
100
|
+
EnvironmentConfigurationInvalid.make({
|
|
101
|
+
message: `Ignotum could not read environment variable '${key}'.`,
|
|
102
|
+
path: environmentPath,
|
|
103
|
+
}),
|
|
104
|
+
),
|
|
105
|
+
),
|
|
106
|
+
);
|
|
107
|
+
const raw = Object.fromEntries(
|
|
108
|
+
entries.flatMap(([key, value]) => (value === undefined ? [] : [[key, value] as const])),
|
|
109
|
+
);
|
|
110
|
+
return { path: environmentPath, raw } satisfies RawEnvironment;
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
export const loadEnvironment = Effect.fn("Environment.load")(function* (
|
|
114
|
+
appDirectory: string,
|
|
115
|
+
definition: DefinedEnv,
|
|
116
|
+
hasDefinition: boolean,
|
|
117
|
+
) {
|
|
118
|
+
const loaded = yield* readEnvironmentRaw(appDirectory, hasDefinition);
|
|
119
|
+
if (!hasDefinition) {
|
|
120
|
+
return {
|
|
121
|
+
definition: emptyEnv,
|
|
122
|
+
raw: {},
|
|
123
|
+
value: Object.freeze({}),
|
|
124
|
+
} satisfies LoadedEnvironment<typeof emptyEnv>;
|
|
125
|
+
}
|
|
126
|
+
const raw = loaded.raw;
|
|
127
|
+
const value = yield* decodeDefinedEnvironment(definition, raw).pipe(
|
|
128
|
+
Effect.mapError((error) =>
|
|
129
|
+
EnvironmentConfigurationInvalid.make({ message: error.message, path: loaded.path }),
|
|
130
|
+
),
|
|
131
|
+
);
|
|
132
|
+
return { definition, raw, value } satisfies LoadedEnvironment;
|
|
133
|
+
});
|
package/src/client/files.ts
CHANGED
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
type FileValue,
|
|
13
13
|
} from "@ignotum/contracts/schema/file";
|
|
14
14
|
import { IdGenerator } from "@ignotum/shared/id";
|
|
15
|
-
import { Effect, Predicate } from "effect";
|
|
15
|
+
import { Effect, Predicate, Schema } from "effect";
|
|
16
16
|
|
|
17
17
|
export const Files = {
|
|
18
18
|
url: (file: FileValue): string => {
|
|
@@ -27,13 +27,19 @@ export const Files = {
|
|
|
27
27
|
const formatByMime: Partial<Record<string, FileFormat>> = {};
|
|
28
28
|
for (const format of fileFormats) formatByMime[fileMimeTypes[format]] = format;
|
|
29
29
|
|
|
30
|
+
const MutationObject = Schema.ObjectKeyword.check(
|
|
31
|
+
Schema.makeFilter((value) => Object.prototype.toString.call(value) === "[object Object]", {
|
|
32
|
+
message: "Mutation objects must contain ordinary fields; built-in collections are unsupported.",
|
|
33
|
+
}),
|
|
34
|
+
);
|
|
35
|
+
|
|
30
36
|
const collectNativeFiles = (
|
|
31
37
|
// oxlint-disable-next-line anti-slop/no-unknown-parameters -- This is the recursive parser for generated mutation arguments and native File values.
|
|
32
38
|
value: unknown,
|
|
33
|
-
files:
|
|
39
|
+
files: Set<File>,
|
|
34
40
|
): void => {
|
|
35
41
|
if (value instanceof File) {
|
|
36
|
-
files.
|
|
42
|
+
files.add(value);
|
|
37
43
|
return;
|
|
38
44
|
}
|
|
39
45
|
if (isFileValue(value) || Predicate.isDate(value)) return;
|
|
@@ -42,7 +48,9 @@ const collectNativeFiles = (
|
|
|
42
48
|
return;
|
|
43
49
|
}
|
|
44
50
|
if (!Predicate.isObject(value)) return;
|
|
45
|
-
for (const child of Object.values(value))
|
|
51
|
+
for (const child of Object.values(Schema.decodeSync(MutationObject)(value))) {
|
|
52
|
+
collectNativeFiles(child, files);
|
|
53
|
+
}
|
|
46
54
|
};
|
|
47
55
|
|
|
48
56
|
const replaceNativeFiles = (
|
|
@@ -72,36 +80,42 @@ export const prepareMutationArguments = Effect.fn("SyncClient.prepareMutationArg
|
|
|
72
80
|
input: unknown,
|
|
73
81
|
) {
|
|
74
82
|
const ids = yield* IdGenerator;
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
83
|
+
const unique = yield* Effect.try(() => {
|
|
84
|
+
const files = new Set<File>();
|
|
85
|
+
collectNativeFiles(input, files);
|
|
86
|
+
if (files.size > fileLimits.filesPerMutation) {
|
|
87
|
+
throw new Error(`A mutation may upload at most ${fileLimits.filesPerMutation} files.`);
|
|
88
|
+
}
|
|
89
|
+
const totalBytes = [...files].reduce((total, file) => total + file.size, 0);
|
|
90
|
+
if (totalBytes > fileLimits.mutationBytes) {
|
|
91
|
+
throw new Error(`Mutation uploads may total at most ${fileLimits.mutationBytes} bytes.`);
|
|
92
|
+
}
|
|
93
|
+
return files;
|
|
94
|
+
});
|
|
85
95
|
const replacements = new Map<File, FileValue>();
|
|
86
96
|
const nativeFiles = new Map<FileId, File>();
|
|
87
97
|
for (const file of unique) {
|
|
88
|
-
const format =
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
98
|
+
const format = yield* Effect.try(() => {
|
|
99
|
+
const format = formatByMime[file.type.toLowerCase()];
|
|
100
|
+
if (format === undefined) {
|
|
101
|
+
throw new Error(
|
|
102
|
+
`File '${file.name}' has unsupported media type '${file.type || "unknown"}'.`,
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
if (file.size === 0 || file.size > fileLimits.fileBytes) {
|
|
106
|
+
throw new Error(`File '${file.name}' must be between 1 and ${fileLimits.fileBytes} bytes.`);
|
|
107
|
+
}
|
|
108
|
+
if (new TextEncoder().encode(file.name).byteLength > fileLimits.filenameBytes) {
|
|
109
|
+
throw new Error(`File '${file.name}' has a filename that is too long.`);
|
|
110
|
+
}
|
|
111
|
+
return format;
|
|
112
|
+
});
|
|
100
113
|
const id = yield* ids.generate(FileId);
|
|
101
114
|
replacements.set(file, makeFileValue(id, { format, name: file.name, size: file.size }));
|
|
102
115
|
nativeFiles.set(id, file);
|
|
103
116
|
}
|
|
104
|
-
const value =
|
|
117
|
+
const value =
|
|
118
|
+
unique.size === 0 ? input : yield* Effect.try(() => replaceNativeFiles(input, replacements));
|
|
105
119
|
return {
|
|
106
120
|
value,
|
|
107
121
|
nativeFiles,
|
|
@@ -121,6 +135,8 @@ export const applyFileGrants = (
|
|
|
121
135
|
grants: ReadonlyArray<FileGrant>,
|
|
122
136
|
// oxlint-disable-next-line anti-slop/no-unknown-returns -- The traversal preserves the decoded query result shape while attaching private symbols.
|
|
123
137
|
): unknown => {
|
|
138
|
+
if (grants.length === 0) return value;
|
|
139
|
+
|
|
124
140
|
const byId = new Map<FileId, string>();
|
|
125
141
|
for (const grant of grants) {
|
|
126
142
|
if (byId.has(grant.id)) throw new Error("A query file grant ID is duplicated.");
|