ignotum 0.0.4 → 0.0.6
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 +3 -0
- package/dist/cli/bin.mjs +269 -78
- package/dist/cli/bin.mjs.map +1 -1
- package/dist/runtime/client.js +27 -2
- package/dist/runtime/client.js.map +1 -1
- package/package.json +3 -3
- package/src/cli/build/client.ts +27 -11
- package/src/cli/build/public.ts +186 -0
- package/src/cli/build/server.ts +1 -0
- package/src/cli/client-config.ts +7 -0
package/dist/cli/bin.mjs
CHANGED
|
@@ -29653,9 +29653,13 @@ Function$1.dual(2, (value, descriptor) => {
|
|
|
29653
29653
|
//#endregion
|
|
29654
29654
|
//#region ../contracts/dist/deployment.js
|
|
29655
29655
|
const ArtifactPath = Schema.String.check(Schema.isPattern(/^(?!\/)(?![A-Za-z]:\/)(?!.*(?:^|\/)\.\.(?:\/|$))(?!.*(?:^|\/)\.(?:\/|$))(?!.*\/\/)[^\\\0]+$/)).pipe(Schema.brand("ignotum/deployment/ArtifactPath"));
|
|
29656
|
+
const ClientPath = Schema.String.check(Schema.isPattern(/^\/(?!_ignotum(?:\/|$))(?:(?:[A-Za-z0-9._~-]+\/)*[A-Za-z0-9._~-]+\/?)?$/)).pipe(Schema.brand("ignotum/deployment/ClientPath"));
|
|
29656
29657
|
const Sha256 = Schema.String.check(Schema.isPattern(/^[0-9a-f]{64}$/)).pipe(Schema.brand("ignotum/deployment/Sha256"));
|
|
29657
29658
|
const ArtifactKind = Schema.Literals([
|
|
29658
29659
|
"ClientAsset",
|
|
29660
|
+
"ClientDocument",
|
|
29661
|
+
"ClientManifest",
|
|
29662
|
+
"ClientPublicFile",
|
|
29659
29663
|
"ClientShell",
|
|
29660
29664
|
"FunctionBundle",
|
|
29661
29665
|
"ServerManifest",
|
|
@@ -29675,10 +29679,28 @@ const ArtifactFile = Schema.Struct({
|
|
|
29675
29679
|
contentType: Schema.String,
|
|
29676
29680
|
contentEncoding: Schema.optional(Schema.String)
|
|
29677
29681
|
});
|
|
29682
|
+
const ClientRoute = Schema.Struct({
|
|
29683
|
+
pathname: ClientPath,
|
|
29684
|
+
artifact: ArtifactReference
|
|
29685
|
+
});
|
|
29678
29686
|
const DeploymentInventory = Schema.Struct({
|
|
29679
29687
|
formatVersion: Schema.Literal(1),
|
|
29680
29688
|
files: Schema.Array(ArtifactFile)
|
|
29681
29689
|
});
|
|
29690
|
+
const ClientManifest = Schema.Struct({
|
|
29691
|
+
formatVersion: Schema.Literal(1),
|
|
29692
|
+
shell: ArtifactReference,
|
|
29693
|
+
routes: Schema.Array(ClientRoute)
|
|
29694
|
+
});
|
|
29695
|
+
const ClientRoutingRoute = Schema.Struct({
|
|
29696
|
+
pathname: ClientPath,
|
|
29697
|
+
artifact: ArtifactPath
|
|
29698
|
+
});
|
|
29699
|
+
Schema.Struct({
|
|
29700
|
+
formatVersion: Schema.Literal(1),
|
|
29701
|
+
shell: ArtifactPath,
|
|
29702
|
+
routes: Schema.Array(ClientRoutingRoute)
|
|
29703
|
+
});
|
|
29682
29704
|
const SchemaSnapshotField = Schema.Struct({
|
|
29683
29705
|
name: Schema.String,
|
|
29684
29706
|
value: ValueDescriptor
|
|
@@ -29703,7 +29725,20 @@ const ServerBuildManifest = Schema.Struct({
|
|
|
29703
29725
|
functions: Schema.Array(ServerFunctionArtifact)
|
|
29704
29726
|
});
|
|
29705
29727
|
const deploymentInventoryPath = ArtifactPath.make("inventory.json");
|
|
29706
|
-
const
|
|
29728
|
+
const clientManifestPath = ArtifactPath.make("client/manifest.json");
|
|
29729
|
+
const clientShellPath = ArtifactPath.make("client/shell.html");
|
|
29730
|
+
const clientAssetPathPrefix = `${ArtifactPath.make("client/assets")}/`;
|
|
29731
|
+
const clientRoutePathPrefix = `${ArtifactPath.make("client/routes")}/`;
|
|
29732
|
+
const clientPublicFileExtensions = [
|
|
29733
|
+
".avif",
|
|
29734
|
+
".gif",
|
|
29735
|
+
".ico",
|
|
29736
|
+
".jpeg",
|
|
29737
|
+
".jpg",
|
|
29738
|
+
".pdf",
|
|
29739
|
+
".png",
|
|
29740
|
+
".webp"
|
|
29741
|
+
];
|
|
29707
29742
|
const serverManifestPath = ArtifactPath.make("server/manifest.json");
|
|
29708
29743
|
const schemaSnapshotPath = ArtifactPath.make("server/schema.json");
|
|
29709
29744
|
const deploymentArtifactLimits = {
|
|
@@ -29804,7 +29839,7 @@ var IdGenerator = class IdGenerator extends Context.Service()("@ignotum/shared/i
|
|
|
29804
29839
|
};
|
|
29805
29840
|
//#endregion
|
|
29806
29841
|
//#region package.json
|
|
29807
|
-
var version = "0.0.
|
|
29842
|
+
var version = "0.0.6";
|
|
29808
29843
|
//#endregion
|
|
29809
29844
|
//#region src/cli/codegen.ts
|
|
29810
29845
|
const generatedHeader = "// Generated by `ignotum codegen`. Do not edit.";
|
|
@@ -30161,19 +30196,19 @@ var InvalidDeploymentArtifact = class extends Schema.TaggedError()("InvalidDeplo
|
|
|
30161
30196
|
message: Schema.String,
|
|
30162
30197
|
path: Schema.optional(Schema.String)
|
|
30163
30198
|
}) {};
|
|
30164
|
-
const invalid$
|
|
30199
|
+
const invalid$3 = (message, path) => InvalidDeploymentArtifact.make(path === void 0 ? { message } : {
|
|
30165
30200
|
message,
|
|
30166
30201
|
path
|
|
30167
30202
|
});
|
|
30168
30203
|
const normalizeArtifactPath = Effect.fn("Deployment.normalizeArtifactPath")(function* (input) {
|
|
30169
30204
|
const normalized = input.replaceAll("\\", "/");
|
|
30170
|
-
return yield* Schema.decodeEffect(ArtifactPath)(normalized).pipe(Effect.mapError(() => invalid$
|
|
30205
|
+
return yield* Schema.decodeEffect(ArtifactPath)(normalized).pipe(Effect.mapError(() => invalid$3(`Invalid artifact path '${input}'.`, input)));
|
|
30171
30206
|
});
|
|
30172
30207
|
const normalizeFiles = Effect.fn("Deployment.normalizeFiles")(function* (inputs) {
|
|
30173
30208
|
const paths = /* @__PURE__ */ new Set();
|
|
30174
30209
|
return yield* Effect.forEach(inputs, (input) => Effect.gen(function* () {
|
|
30175
30210
|
const path = yield* normalizeArtifactPath(input.path);
|
|
30176
|
-
if (paths.has(path)) return yield* invalid$
|
|
30211
|
+
if (paths.has(path)) return yield* invalid$3(`Duplicate normalized artifact path '${path}'.`, path);
|
|
30177
30212
|
paths.add(path);
|
|
30178
30213
|
return {
|
|
30179
30214
|
path,
|
|
@@ -30182,22 +30217,31 @@ const normalizeFiles = Effect.fn("Deployment.normalizeFiles")(function* (inputs)
|
|
|
30182
30217
|
}));
|
|
30183
30218
|
});
|
|
30184
30219
|
const contentType = (path) => {
|
|
30185
|
-
|
|
30186
|
-
if (
|
|
30187
|
-
if (
|
|
30188
|
-
if (
|
|
30189
|
-
if (
|
|
30190
|
-
if (
|
|
30191
|
-
if (
|
|
30192
|
-
if (
|
|
30193
|
-
if (
|
|
30194
|
-
if (
|
|
30195
|
-
if (
|
|
30220
|
+
const lowerPath = path.toLowerCase();
|
|
30221
|
+
if (lowerPath.endsWith(".css")) return "text/css; charset=utf-8";
|
|
30222
|
+
if (lowerPath.endsWith(".html")) return "text/html; charset=utf-8";
|
|
30223
|
+
if (lowerPath.endsWith(".js") || lowerPath.endsWith(".mjs")) return "text/javascript; charset=utf-8";
|
|
30224
|
+
if (lowerPath.endsWith(".json") || lowerPath.endsWith(".map")) return "application/json; charset=utf-8";
|
|
30225
|
+
if (lowerPath.endsWith(".svg")) return "image/svg+xml";
|
|
30226
|
+
if (lowerPath.endsWith(".avif")) return "image/avif";
|
|
30227
|
+
if (lowerPath.endsWith(".gif")) return "image/gif";
|
|
30228
|
+
if (lowerPath.endsWith(".ico")) return "image/x-icon";
|
|
30229
|
+
if (lowerPath.endsWith(".png")) return "image/png";
|
|
30230
|
+
if (lowerPath.endsWith(".jpg") || lowerPath.endsWith(".jpeg")) return "image/jpeg";
|
|
30231
|
+
if (lowerPath.endsWith(".webp")) return "image/webp";
|
|
30232
|
+
if (lowerPath.endsWith(".pdf")) return "application/pdf";
|
|
30233
|
+
if (lowerPath.endsWith(".woff2")) return "font/woff2";
|
|
30234
|
+
if (lowerPath.endsWith(".woff")) return "font/woff";
|
|
30235
|
+
if (lowerPath.endsWith(".wasm")) return "application/wasm";
|
|
30196
30236
|
return "application/octet-stream";
|
|
30197
30237
|
};
|
|
30238
|
+
const isClientPublicFile = (path) => clientPublicFileExtensions.some((extension) => path.toLowerCase().endsWith(extension));
|
|
30198
30239
|
const artifactKind = (path) => {
|
|
30240
|
+
if (path === clientManifestPath) return "ClientManifest";
|
|
30199
30241
|
if (path === clientShellPath) return "ClientShell";
|
|
30200
|
-
if (path.startsWith(
|
|
30242
|
+
if (path.startsWith(clientAssetPathPrefix)) return "ClientAsset";
|
|
30243
|
+
if (path.startsWith(clientRoutePathPrefix) && path.endsWith(".html")) return "ClientDocument";
|
|
30244
|
+
if (path.startsWith(clientRoutePathPrefix) && isClientPublicFile(path)) return "ClientPublicFile";
|
|
30201
30245
|
if (path === serverManifestPath) return "ServerManifest";
|
|
30202
30246
|
if (path === schemaSnapshotPath) return "SchemaSnapshot";
|
|
30203
30247
|
if (path.startsWith("server/functions/") && path.endsWith(".mjs.map")) return "SourceMap";
|
|
@@ -30205,7 +30249,7 @@ const artifactKind = (path) => {
|
|
|
30205
30249
|
};
|
|
30206
30250
|
const makeArtifactFile = Effect.fn("Deployment.makeArtifactFile")(function* (input) {
|
|
30207
30251
|
const kind = artifactKind(input.path);
|
|
30208
|
-
if (kind === void 0) return yield* invalid$
|
|
30252
|
+
if (kind === void 0) return yield* invalid$3(`Unexpected artifact file '${input.path}'.`, input.path);
|
|
30209
30253
|
return {
|
|
30210
30254
|
path: input.path,
|
|
30211
30255
|
size: input.bytes.byteLength,
|
|
@@ -30222,35 +30266,36 @@ const makeInventoryFromNormalized = Effect.fn("Deployment.makeInventoryFromNorma
|
|
|
30222
30266
|
};
|
|
30223
30267
|
});
|
|
30224
30268
|
const validateDeploymentInventory = Effect.fn("Deployment.validateDeploymentInventory")(function* (inventory) {
|
|
30225
|
-
if (inventory.files.length > deploymentArtifactLimits.fileCount) return yield* invalid$
|
|
30269
|
+
if (inventory.files.length > deploymentArtifactLimits.fileCount) return yield* invalid$3(`Deployment inventories may contain at most ${deploymentArtifactLimits.fileCount} files.`);
|
|
30226
30270
|
const paths = /* @__PURE__ */ new Set();
|
|
30227
30271
|
let previousPath;
|
|
30228
30272
|
let serverBytes = 0;
|
|
30229
30273
|
let totalBytes = 0;
|
|
30230
30274
|
for (const file of inventory.files) {
|
|
30231
|
-
if (file.size > deploymentArtifactLimits.fileBytes) return yield* invalid$
|
|
30275
|
+
if (file.size > deploymentArtifactLimits.fileBytes) return yield* invalid$3(`Artifact file '${file.path}' exceeds the ${deploymentArtifactLimits.fileBytes} byte limit.`, file.path);
|
|
30232
30276
|
totalBytes += file.size;
|
|
30233
|
-
if (totalBytes > deploymentArtifactLimits.totalBytes) return yield* invalid$
|
|
30277
|
+
if (totalBytes > deploymentArtifactLimits.totalBytes) return yield* invalid$3(`Deployment artifacts exceed the ${deploymentArtifactLimits.totalBytes} byte limit.`);
|
|
30234
30278
|
if (file.path.startsWith("server/")) {
|
|
30235
30279
|
serverBytes += file.size;
|
|
30236
|
-
if (serverBytes > deploymentArtifactLimits.serverBytes) return yield* invalid$
|
|
30280
|
+
if (serverBytes > deploymentArtifactLimits.serverBytes) return yield* invalid$3(`Server artifacts exceed the ${deploymentArtifactLimits.serverBytes} byte limit.`);
|
|
30237
30281
|
}
|
|
30238
|
-
if (paths.has(file.path)) return yield* invalid$
|
|
30282
|
+
if (paths.has(file.path)) return yield* invalid$3(`Duplicate deployment inventory path '${file.path}'.`, file.path);
|
|
30239
30283
|
paths.add(file.path);
|
|
30240
|
-
if (previousPath !== void 0 && String$1.Order(previousPath, file.path) >= 0) return yield* invalid$
|
|
30284
|
+
if (previousPath !== void 0 && String$1.Order(previousPath, file.path) >= 0) return yield* invalid$3("Deployment inventory files must be sorted by path.", file.path);
|
|
30241
30285
|
previousPath = file.path;
|
|
30242
30286
|
const expectedKind = artifactKind(file.path);
|
|
30243
|
-
if (expectedKind === void 0 || expectedKind !== file.kind) return yield* invalid$
|
|
30287
|
+
if (expectedKind === void 0 || expectedKind !== file.kind) return yield* invalid$3(`Deployment inventory kind '${file.kind}' does not match '${file.path}'.`, file.path);
|
|
30244
30288
|
}
|
|
30245
30289
|
for (const path of [
|
|
30290
|
+
clientManifestPath,
|
|
30246
30291
|
clientShellPath,
|
|
30247
30292
|
serverManifestPath,
|
|
30248
30293
|
schemaSnapshotPath
|
|
30249
|
-
]) if (!paths.has(path)) return yield* invalid$
|
|
30294
|
+
]) if (!paths.has(path)) return yield* invalid$3(`Artifact file '${path}' is missing.`, path);
|
|
30250
30295
|
});
|
|
30251
30296
|
const makeDeploymentInventory = Effect.fn("Deployment.makeDeploymentInventory")(function* (inputs) {
|
|
30252
30297
|
const files = yield* normalizeFiles(inputs);
|
|
30253
|
-
if (files.some((file) => file.path === deploymentInventoryPath)) return yield* invalid$
|
|
30298
|
+
if (files.some((file) => file.path === deploymentInventoryPath)) return yield* invalid$3(`${deploymentInventoryPath} cannot include itself in the deployment inventory.`, deploymentInventoryPath);
|
|
30254
30299
|
const inventory = yield* makeInventoryFromNormalized(files);
|
|
30255
30300
|
const encoded = `${encodeCanonical(DeploymentInventory, inventory)}\n`;
|
|
30256
30301
|
return {
|
|
@@ -30267,35 +30312,53 @@ const artifactReference = Effect.fn("Deployment.artifactReference")(function* (p
|
|
|
30267
30312
|
});
|
|
30268
30313
|
const decodeJsonFile = (schema, file) => Effect.try({
|
|
30269
30314
|
try: () => utf8String(file.bytes),
|
|
30270
|
-
catch: () => invalid$
|
|
30271
|
-
}).pipe(Effect.flatMap(Schema.decodeEffect(Schema.fromJsonString(schema))), Effect.mapError(() => invalid$
|
|
30315
|
+
catch: () => invalid$3(`Artifact file '${file.path}' is not valid UTF-8.`, file.path)
|
|
30316
|
+
}).pipe(Effect.flatMap(Schema.decodeEffect(Schema.fromJsonString(schema))), Effect.mapError(() => invalid$3(`Artifact file '${file.path}' has invalid JSON.`, file.path)));
|
|
30272
30317
|
const findFile = (files, path) => {
|
|
30273
30318
|
const file = files.find((candidate) => candidate.path === path);
|
|
30274
|
-
return file === void 0 ? Effect.fail(invalid$
|
|
30319
|
+
return file === void 0 ? Effect.fail(invalid$3(`Artifact file '${path}' is missing.`, path)) : Effect.succeed(file);
|
|
30275
30320
|
};
|
|
30276
30321
|
const sameReference = (entry, reference) => entry.path === reference.path && entry.size === reference.size && entry.sha256 === reference.sha256;
|
|
30322
|
+
const validateClientManifest = Effect.fn("Deployment.validateClientManifest")(function* (manifest, inventory) {
|
|
30323
|
+
if (manifest.shell.path !== clientShellPath) return yield* invalid$3(`The client manifest must reference '${clientShellPath}' as its shell.`, manifest.shell.path);
|
|
30324
|
+
const entries = new Map(inventory.files.map((file) => [file.path, file]));
|
|
30325
|
+
const shellEntry = entries.get(manifest.shell.path);
|
|
30326
|
+
if (shellEntry === void 0 || shellEntry.kind !== "ClientShell" || !sameReference(shellEntry, manifest.shell)) return yield* invalid$3(`The client shell reference for '${manifest.shell.path}' does not match the inventory.`, manifest.shell.path);
|
|
30327
|
+
const referencedPaths = /* @__PURE__ */ new Set();
|
|
30328
|
+
let previousRoute;
|
|
30329
|
+
for (const route of manifest.routes) {
|
|
30330
|
+
if (previousRoute !== void 0 && String$1.Order(previousRoute, route.pathname) >= 0) return yield* invalid$3("Client routes must be sorted by pathname.", route.pathname);
|
|
30331
|
+
previousRoute = route.pathname;
|
|
30332
|
+
if (referencedPaths.has(route.artifact.path)) return yield* invalid$3(`Client artifact path '${route.artifact.path}' is referenced more than once.`, route.artifact.path);
|
|
30333
|
+
referencedPaths.add(route.artifact.path);
|
|
30334
|
+
const entry = entries.get(route.artifact.path);
|
|
30335
|
+
if (entry === void 0 || !sameReference(entry, route.artifact)) return yield* invalid$3(`The client route reference for '${route.artifact.path}' does not match the inventory.`, route.artifact.path);
|
|
30336
|
+
if (entry.kind !== "ClientDocument" && entry.kind !== "ClientPublicFile") return yield* invalid$3(`Client route '${route.pathname}' cannot reference artifact kind '${entry.kind}'.`, route.artifact.path);
|
|
30337
|
+
}
|
|
30338
|
+
for (const file of inventory.files) if ((file.kind === "ClientDocument" || file.kind === "ClientPublicFile") && !referencedPaths.has(file.path)) return yield* invalid$3(`Client route artifact '${file.path}' has no route.`, file.path);
|
|
30339
|
+
});
|
|
30277
30340
|
const validateManifest = Effect.fn("Deployment.validateManifest")(function* (manifest, inventory) {
|
|
30278
|
-
if (manifest.schema.path !== schemaSnapshotPath) return yield* invalid$
|
|
30341
|
+
if (manifest.schema.path !== schemaSnapshotPath) return yield* invalid$3(`The server manifest must reference '${schemaSnapshotPath}'.`, manifest.schema.path);
|
|
30279
30342
|
const entries = new Map(inventory.files.map((file) => [file.path, file]));
|
|
30280
30343
|
const addresses = /* @__PURE__ */ new Set();
|
|
30281
30344
|
const referencedPaths = /* @__PURE__ */ new Set([serverManifestPath, schemaSnapshotPath]);
|
|
30282
30345
|
const references = [manifest.schema];
|
|
30283
30346
|
for (const definition of manifest.functions) {
|
|
30284
|
-
if (addresses.has(definition.address)) return yield* invalid$
|
|
30347
|
+
if (addresses.has(definition.address)) return yield* invalid$3(`Duplicate function address '${definition.address}'.`);
|
|
30285
30348
|
addresses.add(definition.address);
|
|
30286
30349
|
references.push(definition.bundle, definition.sourceMap);
|
|
30287
30350
|
for (const reference of [definition.bundle, definition.sourceMap]) {
|
|
30288
|
-
if (referencedPaths.has(reference.path)) return yield* invalid$
|
|
30351
|
+
if (referencedPaths.has(reference.path)) return yield* invalid$3(`Server artifact path '${reference.path}' is referenced more than once.`, reference.path);
|
|
30289
30352
|
referencedPaths.add(reference.path);
|
|
30290
30353
|
}
|
|
30291
|
-
if (!definition.bundle.path.endsWith(".mjs")) return yield* invalid$
|
|
30292
|
-
if (definition.sourceMap.path !== `${definition.bundle.path}.map`) return yield* invalid$
|
|
30354
|
+
if (!definition.bundle.path.endsWith(".mjs")) return yield* invalid$3("Function bundles must use the .mjs extension.", definition.bundle.path);
|
|
30355
|
+
if (definition.sourceMap.path !== `${definition.bundle.path}.map`) return yield* invalid$3("A function source map must be adjacent to its bundle.", definition.sourceMap.path);
|
|
30293
30356
|
}
|
|
30294
30357
|
for (const reference of references) {
|
|
30295
30358
|
const entry = entries.get(reference.path);
|
|
30296
|
-
if (entry === void 0 || !sameReference(entry, reference)) return yield* invalid$
|
|
30359
|
+
if (entry === void 0 || !sameReference(entry, reference)) return yield* invalid$3(`The server reference for '${reference.path}' does not match the inventory.`, reference.path);
|
|
30297
30360
|
}
|
|
30298
|
-
for (const file of inventory.files) if (file.path.startsWith("server/") && !referencedPaths.has(file.path)) return yield* invalid$
|
|
30361
|
+
for (const file of inventory.files) if (file.path.startsWith("server/") && !referencedPaths.has(file.path)) return yield* invalid$3(`Unexpected server entrypoint '${file.path}'.`, file.path);
|
|
30299
30362
|
});
|
|
30300
30363
|
Effect.fn("Deployment.validateDeploymentMetadata")(function* (input) {
|
|
30301
30364
|
yield* validateDeploymentInventory(input.inventory);
|
|
@@ -30305,7 +30368,7 @@ Effect.fn("Deployment.validateDeploymentMetadata")(function* (input) {
|
|
|
30305
30368
|
};
|
|
30306
30369
|
const decodedInventory = yield* decodeJsonFile(DeploymentInventory, inventoryFile);
|
|
30307
30370
|
const expectedInventoryText = `${encodeCanonical(DeploymentInventory, input.inventory)}\n`;
|
|
30308
|
-
if (encodeCanonical(DeploymentInventory, decodedInventory) !== encodeCanonical(DeploymentInventory, input.inventory) || utf8String(input.inventoryBytes) !== expectedInventoryText) return yield* invalid$
|
|
30371
|
+
if (encodeCanonical(DeploymentInventory, decodedInventory) !== encodeCanonical(DeploymentInventory, input.inventory) || utf8String(input.inventoryBytes) !== expectedInventoryText) return yield* invalid$3("The uploaded deployment inventory is not canonical.");
|
|
30309
30372
|
const manifestFile = {
|
|
30310
30373
|
path: serverManifestPath,
|
|
30311
30374
|
bytes: input.manifestBytes
|
|
@@ -30314,17 +30377,28 @@ Effect.fn("Deployment.validateDeploymentMetadata")(function* (input) {
|
|
|
30314
30377
|
path: schemaSnapshotPath,
|
|
30315
30378
|
bytes: input.schemaBytes
|
|
30316
30379
|
};
|
|
30317
|
-
const
|
|
30380
|
+
const clientManifestFile = {
|
|
30381
|
+
path: clientManifestPath,
|
|
30382
|
+
bytes: input.clientManifestBytes
|
|
30383
|
+
};
|
|
30384
|
+
const metadataFiles = [
|
|
30385
|
+
clientManifestFile,
|
|
30386
|
+
manifestFile,
|
|
30387
|
+
schemaFile
|
|
30388
|
+
];
|
|
30318
30389
|
const entries = new Map(input.inventory.files.map((file) => [file.path, file]));
|
|
30319
30390
|
for (const file of metadataFiles) {
|
|
30320
30391
|
const entry = entries.get(file.path);
|
|
30321
30392
|
const actual = yield* makeArtifactFile(file);
|
|
30322
|
-
if (entry === void 0 || encodeCanonical(ArtifactFile, actual) !== encodeCanonical(ArtifactFile, entry)) return yield* invalid$
|
|
30393
|
+
if (entry === void 0 || encodeCanonical(ArtifactFile, actual) !== encodeCanonical(ArtifactFile, entry)) return yield* invalid$3(`Artifact file '${file.path}' does not match the deployment inventory.`, file.path);
|
|
30323
30394
|
}
|
|
30395
|
+
const clientManifest = yield* decodeJsonFile(ClientManifest, clientManifestFile);
|
|
30324
30396
|
const manifest = yield* decodeJsonFile(ServerBuildManifest, manifestFile);
|
|
30325
30397
|
const schema = yield* decodeJsonFile(SchemaSnapshot, schemaFile);
|
|
30398
|
+
yield* validateClientManifest(clientManifest, input.inventory);
|
|
30326
30399
|
yield* validateManifest(manifest, input.inventory);
|
|
30327
30400
|
return {
|
|
30401
|
+
clientManifest,
|
|
30328
30402
|
inventory: input.inventory,
|
|
30329
30403
|
manifest,
|
|
30330
30404
|
schema
|
|
@@ -30337,14 +30411,17 @@ const validateDeploymentArtifact = Effect.fn("Deployment.validateDeploymentArtif
|
|
|
30337
30411
|
const inventory = yield* decodeJsonFile(DeploymentInventory, inventoryFile);
|
|
30338
30412
|
yield* validateDeploymentInventory(inventory);
|
|
30339
30413
|
const actualInventory = yield* makeInventoryFromNormalized(payloadFiles);
|
|
30340
|
-
if (encodeCanonical(DeploymentInventory, inventory) !== encodeCanonical(DeploymentInventory, actualInventory)) return yield* invalid$
|
|
30414
|
+
if (encodeCanonical(DeploymentInventory, inventory) !== encodeCanonical(DeploymentInventory, actualInventory)) return yield* invalid$3("The deployment inventory does not match the artifact files.");
|
|
30415
|
+
const clientManifestFile = yield* findFile(files, clientManifestPath);
|
|
30341
30416
|
const manifestFile = yield* findFile(files, serverManifestPath);
|
|
30342
30417
|
const schemaFile = yield* findFile(files, schemaSnapshotPath);
|
|
30343
|
-
yield*
|
|
30418
|
+
const clientManifest = yield* decodeJsonFile(ClientManifest, clientManifestFile);
|
|
30344
30419
|
const manifest = yield* decodeJsonFile(ServerBuildManifest, manifestFile);
|
|
30345
30420
|
const schema = yield* decodeJsonFile(SchemaSnapshot, schemaFile);
|
|
30421
|
+
yield* validateClientManifest(clientManifest, inventory);
|
|
30346
30422
|
yield* validateManifest(manifest, inventory);
|
|
30347
30423
|
return {
|
|
30424
|
+
clientManifest,
|
|
30348
30425
|
inventory,
|
|
30349
30426
|
manifest,
|
|
30350
30427
|
schema
|
|
@@ -30364,7 +30441,7 @@ Effect.fn("Deployment.loadRuntimeDeploymentArtifact")(function* (read) {
|
|
|
30364
30441
|
bytes: yield* read(entry.path, entry.size)
|
|
30365
30442
|
};
|
|
30366
30443
|
const actual = yield* makeArtifactFile(input);
|
|
30367
|
-
if (encodeCanonical(ArtifactFile, actual) !== encodeCanonical(ArtifactFile, entry)) return yield* invalid$
|
|
30444
|
+
if (encodeCanonical(ArtifactFile, actual) !== encodeCanonical(ArtifactFile, entry)) return yield* invalid$3(`Artifact file '${entry.path}' does not match the deployment inventory.`, entry.path);
|
|
30368
30445
|
return input;
|
|
30369
30446
|
}), { concurrency: 4 });
|
|
30370
30447
|
const manifestFile = yield* findFile(serverFiles, serverManifestPath);
|
|
@@ -30420,7 +30497,7 @@ var AppConfigurationInvalid = class extends Schema.TaggedError()("AppConfigurati
|
|
|
30420
30497
|
cause: Schema.optional(Schema.Defect()),
|
|
30421
30498
|
message: Schema.String
|
|
30422
30499
|
}) {};
|
|
30423
|
-
const invalid$
|
|
30500
|
+
const invalid$2 = (message, cause) => AppConfigurationInvalid.make(cause === void 0 ? { message } : {
|
|
30424
30501
|
cause,
|
|
30425
30502
|
message
|
|
30426
30503
|
});
|
|
@@ -30430,16 +30507,16 @@ const loadAppConfiguration = Effect.fn("AppConfiguration.load")(function* (appDi
|
|
|
30430
30507
|
const path = yield* Path$1.Path;
|
|
30431
30508
|
const configPath = appConfigurationPath(appDirectory, path);
|
|
30432
30509
|
if (!(yield* fileSystem.exists(configPath))) return void 0;
|
|
30433
|
-
const text = yield* fileSystem.readFileString(configPath).pipe(Effect.mapError((cause) => invalid$
|
|
30434
|
-
return yield* Schema.decodeEffect(Schema.fromJsonString(AppConfiguration))(text).pipe(Effect.mapError((cause) => invalid$
|
|
30510
|
+
const text = yield* fileSystem.readFileString(configPath).pipe(Effect.mapError((cause) => invalid$2(`Could not read ${configPath}.`, cause)));
|
|
30511
|
+
return yield* Schema.decodeEffect(Schema.fromJsonString(AppConfiguration))(text).pipe(Effect.mapError((cause) => invalid$2(`${configPath} is not a valid app configuration.`, cause)));
|
|
30435
30512
|
});
|
|
30436
30513
|
const writeAppConfiguration = Effect.fn("AppConfiguration.write")(function* (appDirectory, configuration) {
|
|
30437
30514
|
const fileSystem = yield* FileSystem.FileSystem;
|
|
30438
30515
|
const path = yield* Path$1.Path;
|
|
30439
30516
|
const stateDirectory = path.join(appDirectory, ".ignotum");
|
|
30440
30517
|
const configPath = appConfigurationPath(appDirectory, path);
|
|
30441
|
-
const encoded = yield* Schema.encodeEffect(Schema.fromJsonString(AppConfiguration))(configuration).pipe(Effect.mapError((cause) => invalid$
|
|
30442
|
-
yield* fileSystem.makeDirectory(stateDirectory, { recursive: true }).pipe(Effect.mapError((cause) => invalid$
|
|
30518
|
+
const encoded = yield* Schema.encodeEffect(Schema.fromJsonString(AppConfiguration))(configuration).pipe(Effect.mapError((cause) => invalid$2("Could not encode the app configuration.", cause)));
|
|
30519
|
+
yield* fileSystem.makeDirectory(stateDirectory, { recursive: true }).pipe(Effect.mapError((cause) => invalid$2(`Could not create ${stateDirectory}.`, cause)));
|
|
30443
30520
|
yield* Effect.scoped(Effect.gen(function* () {
|
|
30444
30521
|
const temporaryPath = yield* fileSystem.makeTempFileScoped({
|
|
30445
30522
|
directory: stateDirectory,
|
|
@@ -30448,14 +30525,14 @@ const writeAppConfiguration = Effect.fn("AppConfiguration.write")(function* (app
|
|
|
30448
30525
|
});
|
|
30449
30526
|
yield* fileSystem.writeFileString(temporaryPath, `${encoded}\n`);
|
|
30450
30527
|
yield* fileSystem.rename(temporaryPath, configPath);
|
|
30451
|
-
})).pipe(Effect.mapError((cause) => invalid$
|
|
30528
|
+
})).pipe(Effect.mapError((cause) => invalid$2(`Could not write ${configPath}.`, cause)));
|
|
30452
30529
|
});
|
|
30453
30530
|
const promptForSlug = Effect.fn("AppConfiguration.promptForSlug")(function* () {
|
|
30454
30531
|
const value = yield* Prompt.run(Prompt.text({
|
|
30455
30532
|
message: "App slug",
|
|
30456
30533
|
validate: (input) => Schema.decodeEffect(AppSlug)(input).pipe(Effect.map(String), Effect.mapError(() => "Use lowercase letters, numbers, and hyphens, with no leading or trailing hyphen."))
|
|
30457
30534
|
}));
|
|
30458
|
-
return yield* Schema.decodeEffect(AppSlug)(value).pipe(Effect.mapError((cause) => invalid$
|
|
30535
|
+
return yield* Schema.decodeEffect(AppSlug)(value).pipe(Effect.mapError((cause) => invalid$2("The app slug is invalid.", cause)));
|
|
30459
30536
|
});
|
|
30460
30537
|
const sameApp = (configured, remote) => configured.appId === remote.id && configured.slug === remote.slug;
|
|
30461
30538
|
const configureApp = Effect.fn("AppConfiguration.configure")(function* (appDirectory, requestedSlug) {
|
|
@@ -30463,10 +30540,10 @@ const configureApp = Effect.fn("AppConfiguration.configure")(function* (appDirec
|
|
|
30463
30540
|
const hosted = yield* HostedControlConfiguration;
|
|
30464
30541
|
const configured = yield* loadAppConfiguration(appDirectory);
|
|
30465
30542
|
if (configured !== void 0) {
|
|
30466
|
-
if (configured.apiUrl.href !== hosted.apiUrl.href) return yield* invalid$
|
|
30467
|
-
if (requestedSlug !== void 0 && requestedSlug !== configured.slug) return yield* invalid$
|
|
30543
|
+
if (configured.apiUrl.href !== hosted.apiUrl.href) return yield* invalid$2(`.ignotum/app.json belongs to ${configured.apiUrl.href}, not ${hosted.apiUrl.href}.`);
|
|
30544
|
+
if (requestedSlug !== void 0 && requestedSlug !== configured.slug) return yield* invalid$2(`.ignotum/app.json is already linked to '${configured.slug}', not '${requestedSlug}'.`);
|
|
30468
30545
|
const remote = yield* control.getApp(configured.appId);
|
|
30469
|
-
if (!sameApp(configured, remote)) return yield* invalid$
|
|
30546
|
+
if (!sameApp(configured, remote)) return yield* invalid$2("The stored app identity does not match the Ignotum API.");
|
|
30470
30547
|
return configured;
|
|
30471
30548
|
}
|
|
30472
30549
|
const slug = requestedSlug ?? (yield* promptForSlug());
|
|
@@ -30510,6 +30587,11 @@ const validateClientFiles = Effect.fn("ClientConfig.validateFiles")(function* (a
|
|
|
30510
30587
|
const fileSystem = yield* FileSystem.FileSystem;
|
|
30511
30588
|
const path = yield* Path$1.Path;
|
|
30512
30589
|
const clientDirectory = path.join(appDirectory, "client");
|
|
30590
|
+
const nestedPublicDirectory = path.join(clientDirectory, "public");
|
|
30591
|
+
if (yield* fileSystem.exists(nestedPublicDirectory)) return yield* InvalidClientFile.make({
|
|
30592
|
+
message: `Client public files must be placed in the top-level public directory, not ${nestedPublicDirectory}.`,
|
|
30593
|
+
path: nestedPublicDirectory
|
|
30594
|
+
});
|
|
30513
30595
|
const entryPath = path.join(clientDirectory, clientEntryFileName);
|
|
30514
30596
|
if (!(yield* fileSystem.exists(entryPath))) return yield* ClientFileNotFound.make({
|
|
30515
30597
|
message: `Required client file not found: ${entryPath}`,
|
|
@@ -30584,7 +30666,7 @@ const location = (source, offset) => {
|
|
|
30584
30666
|
line
|
|
30585
30667
|
};
|
|
30586
30668
|
};
|
|
30587
|
-
const invalid = (entry, node, detail) => {
|
|
30669
|
+
const invalid$1 = (entry, node, detail) => {
|
|
30588
30670
|
const position = location(entry.source, node.start);
|
|
30589
30671
|
return InvalidClientEntry.make({
|
|
30590
30672
|
...position,
|
|
@@ -30604,26 +30686,26 @@ const extractClientEntryTitle = (entry) => {
|
|
|
30604
30686
|
const parserError = parsed.errors[0];
|
|
30605
30687
|
if (parserError !== void 0) {
|
|
30606
30688
|
const label = parserError.labels[0];
|
|
30607
|
-
return Result.fail(invalid(entry, { start: label?.start ?? 0 }, `The client entry is invalid: ${parserError.message}.`));
|
|
30689
|
+
return Result.fail(invalid$1(entry, { start: label?.start ?? 0 }, `The client entry is invalid: ${parserError.message}.`));
|
|
30608
30690
|
}
|
|
30609
|
-
if (!parsed.program.body.some((statement) => statement.type === "ImportDeclaration" && statement.source.value === "ignotum/client" && statement.importKind !== "type" && statement.specifiers.some((specifier) => specifier.type === "ImportSpecifier" && specifier.importKind !== "type" && specifier.imported.type === "Identifier" && specifier.imported.name === "app" && specifier.local.name === "app"))) return Result.fail(invalid(entry, parsed.program, "Import { app } from \"ignotum/client\" without an alias."));
|
|
30691
|
+
if (!parsed.program.body.some((statement) => statement.type === "ImportDeclaration" && statement.source.value === "ignotum/client" && statement.importKind !== "type" && statement.specifiers.some((specifier) => specifier.type === "ImportSpecifier" && specifier.importKind !== "type" && specifier.imported.type === "Identifier" && specifier.imported.name === "app" && specifier.local.name === "app"))) return Result.fail(invalid$1(entry, parsed.program, "Import { app } from \"ignotum/client\" without an alias."));
|
|
30610
30692
|
const defaultExports = parsed.program.body.filter((statement) => statement.type === "ExportDefaultDeclaration");
|
|
30611
30693
|
const defaultExport = defaultExports[0];
|
|
30612
|
-
if (defaultExports.length !== 1 || defaultExport === void 0) return Result.fail(invalid(entry, parsed.program, "The client entry needs one default export."));
|
|
30694
|
+
if (defaultExports.length !== 1 || defaultExport === void 0) return Result.fail(invalid$1(entry, parsed.program, "The client entry needs one default export."));
|
|
30613
30695
|
const call = defaultExport.declaration;
|
|
30614
|
-
if (call.type !== "CallExpression" || call.optional || call.typeArguments != null || call.callee.type !== "Identifier" || call.callee.name !== "app" || call.arguments.length !== 1) return Result.fail(invalid(entry, defaultExport, "The default export must be an app call."));
|
|
30696
|
+
if (call.type !== "CallExpression" || call.optional || call.typeArguments != null || call.callee.type !== "Identifier" || call.callee.name !== "app" || call.arguments.length !== 1) return Result.fail(invalid$1(entry, defaultExport, "The default export must be an app call."));
|
|
30615
30697
|
const definition = call.arguments[0];
|
|
30616
|
-
if (definition?.type !== "ObjectExpression" || definition.properties.length !== 2) return Result.fail(invalid(entry, call, "The app call needs exactly title and component properties."));
|
|
30698
|
+
if (definition?.type !== "ObjectExpression" || definition.properties.length !== 2) return Result.fail(invalid$1(entry, call, "The app call needs exactly title and component properties."));
|
|
30617
30699
|
const properties = definition.properties;
|
|
30618
|
-
if (!properties.every(isObjectProperty)) return Result.fail(invalid(entry, definition, "Spread properties are not supported."));
|
|
30700
|
+
if (!properties.every(isObjectProperty)) return Result.fail(invalid$1(entry, definition, "Spread properties are not supported."));
|
|
30619
30701
|
const titleProperties = properties.filter((property) => isNamedProperty(property, "title"));
|
|
30620
30702
|
const componentProperties = properties.filter((property) => isNamedProperty(property, "component"));
|
|
30621
|
-
if (titleProperties.length !== 1 || componentProperties.length !== 1) return Result.fail(invalid(entry, definition, "Use unquoted title and component properties with no extras."));
|
|
30703
|
+
if (titleProperties.length !== 1 || componentProperties.length !== 1) return Result.fail(invalid$1(entry, definition, "Use unquoted title and component properties with no extras."));
|
|
30622
30704
|
const titleProperty = titleProperties[0];
|
|
30623
|
-
if (titleProperty === void 0) return Result.fail(invalid(entry, definition, "The title property is missing."));
|
|
30705
|
+
if (titleProperty === void 0) return Result.fail(invalid$1(entry, definition, "The title property is missing."));
|
|
30624
30706
|
const title = titleProperty.value;
|
|
30625
|
-
if (title.type !== "Literal" || !Predicate.isString(title.value) || title.raw?.startsWith("\"") !== true && title.raw?.startsWith("'") !== true) return Result.fail(invalid(entry, title, "The title must be a quoted string literal."));
|
|
30626
|
-
if (title.value.trim().length === 0) return Result.fail(invalid(entry, title, "The title must contain a non-whitespace character."));
|
|
30707
|
+
if (title.type !== "Literal" || !Predicate.isString(title.value) || title.raw?.startsWith("\"") !== true && title.raw?.startsWith("'") !== true) return Result.fail(invalid$1(entry, title, "The title must be a quoted string literal."));
|
|
30708
|
+
if (title.value.trim().length === 0) return Result.fail(invalid$1(entry, title, "The title must contain a non-whitespace character."));
|
|
30627
30709
|
return Result.succeed(title.value);
|
|
30628
30710
|
};
|
|
30629
30711
|
const loadClientEntryTitle = Effect.fn("ClientEntry.loadTitle")(function* (path) {
|
|
@@ -30877,6 +30959,104 @@ const sourceMapPathTransform = Effect.fn("Deploy.sourceMapPathTransform")(functi
|
|
|
30877
30959
|
};
|
|
30878
30960
|
});
|
|
30879
30961
|
//#endregion
|
|
30962
|
+
//#region src/cli/build/public.ts
|
|
30963
|
+
var InvalidPublicFile = class extends Schema.TaggedError()("InvalidPublicFile", {
|
|
30964
|
+
message: Schema.String,
|
|
30965
|
+
path: Schema.String
|
|
30966
|
+
}) {};
|
|
30967
|
+
const pathSegmentPattern = /^[A-Za-z0-9._~-]+$/;
|
|
30968
|
+
const startsWith = (bytes, signature, offset = 0) => signature.every((byte, index) => bytes[offset + index] === byte);
|
|
30969
|
+
const asciiAt = (bytes, value, offset = 0) => startsWith(bytes, globalThis.Array.from(value, (character) => character.charCodeAt(0)), offset);
|
|
30970
|
+
const hasValidSignature = (extension, bytes) => {
|
|
30971
|
+
switch (extension) {
|
|
30972
|
+
case ".avif": {
|
|
30973
|
+
if (!asciiAt(bytes, "ftyp", 4)) return false;
|
|
30974
|
+
const headerLength = Math.min(bytes.length, 64);
|
|
30975
|
+
for (let offset = 8; offset + 4 <= headerLength; offset += 4) if (asciiAt(bytes, "avif", offset) || asciiAt(bytes, "avis", offset)) return true;
|
|
30976
|
+
return false;
|
|
30977
|
+
}
|
|
30978
|
+
case ".gif": return asciiAt(bytes, "GIF87a") || asciiAt(bytes, "GIF89a");
|
|
30979
|
+
case ".ico": return startsWith(bytes, [
|
|
30980
|
+
0,
|
|
30981
|
+
0,
|
|
30982
|
+
1,
|
|
30983
|
+
0
|
|
30984
|
+
]);
|
|
30985
|
+
case ".jpeg":
|
|
30986
|
+
case ".jpg": return startsWith(bytes, [
|
|
30987
|
+
255,
|
|
30988
|
+
216,
|
|
30989
|
+
255
|
|
30990
|
+
]);
|
|
30991
|
+
case ".pdf": return asciiAt(bytes, "%PDF-");
|
|
30992
|
+
case ".png": return startsWith(bytes, [
|
|
30993
|
+
137,
|
|
30994
|
+
80,
|
|
30995
|
+
78,
|
|
30996
|
+
71,
|
|
30997
|
+
13,
|
|
30998
|
+
10,
|
|
30999
|
+
26,
|
|
31000
|
+
10
|
|
31001
|
+
]);
|
|
31002
|
+
case ".webp": return asciiAt(bytes, "RIFF") && asciiAt(bytes, "WEBP", 8);
|
|
31003
|
+
default: return false;
|
|
31004
|
+
}
|
|
31005
|
+
};
|
|
31006
|
+
const invalid = (path, message) => InvalidPublicFile.make({
|
|
31007
|
+
message,
|
|
31008
|
+
path
|
|
31009
|
+
});
|
|
31010
|
+
const copyPublicFiles = Effect.fn("Deploy.copyPublicFiles")(function* (appDirectory, outputDirectory) {
|
|
31011
|
+
const fileSystem = yield* FileSystem.FileSystem;
|
|
31012
|
+
const path = yield* Path$1.Path;
|
|
31013
|
+
const publicDirectory = path.join(appDirectory, "public");
|
|
31014
|
+
if (!(yield* fileSystem.exists(publicDirectory))) return {
|
|
31015
|
+
files: 0,
|
|
31016
|
+
routes: []
|
|
31017
|
+
};
|
|
31018
|
+
if (Option.isSome(yield* Effect.option(fileSystem.readLink(publicDirectory)))) return yield* invalid(publicDirectory, "The top-level public directory cannot be a symbolic link.");
|
|
31019
|
+
if ((yield* fileSystem.stat(publicDirectory)).type !== "Directory") return yield* invalid(publicDirectory, "The top-level public path must be a directory.");
|
|
31020
|
+
const entries = (yield* fileSystem.readDirectory(publicDirectory, { recursive: true })).toSorted();
|
|
31021
|
+
const files = (yield* Effect.forEach(entries, (entry) => Effect.gen(function* () {
|
|
31022
|
+
const relativePath = entry.replaceAll("\\", "/");
|
|
31023
|
+
const source = path.join(publicDirectory, entry);
|
|
31024
|
+
if (Option.isSome(yield* Effect.option(fileSystem.readLink(source)))) return yield* invalid(source, "Public entries cannot be symbolic links.");
|
|
31025
|
+
const info = yield* fileSystem.stat(source);
|
|
31026
|
+
if (info.type === "Directory") return void 0;
|
|
31027
|
+
if (info.type !== "File") return yield* invalid(source, "Public entries must be regular files or directories.");
|
|
31028
|
+
const segments = relativePath.split("/");
|
|
31029
|
+
if (segments.some((segment) => !pathSegmentPattern.test(segment))) return yield* invalid(source, "Public file paths may only contain letters, numbers, dots, underscores, tildes, hyphens, and directory separators.");
|
|
31030
|
+
if (segments[0] === "_ignotum") return yield* invalid(source, "The /_ignotum namespace is reserved by the platform.");
|
|
31031
|
+
const extension = path.extname(relativePath).toLowerCase();
|
|
31032
|
+
if (!clientPublicFileExtensions.some((allowedExtension) => allowedExtension === extension)) return yield* invalid(source, `Unsupported public file type '${extension || "(none)"}'. Allowed types: ${clientPublicFileExtensions.join(", ")}.`);
|
|
31033
|
+
if (info.size > BigInt(deploymentArtifactLimits.fileBytes)) return yield* invalid(source, `Public files cannot exceed ${deploymentArtifactLimits.fileBytes} bytes.`);
|
|
31034
|
+
const bytes = yield* fileSystem.readFile(source);
|
|
31035
|
+
if (!hasValidSignature(extension, bytes)) return yield* invalid(source, `The contents do not match the '${extension}' file extension.`);
|
|
31036
|
+
const pathname = `/${relativePath}`;
|
|
31037
|
+
return {
|
|
31038
|
+
bytes,
|
|
31039
|
+
destination: path.join(outputDirectory, "routes", entry),
|
|
31040
|
+
route: {
|
|
31041
|
+
pathname: ClientPath.make(pathname),
|
|
31042
|
+
artifact: yield* artifactReference(ArtifactPath.make(`client/routes/${relativePath}`), bytes)
|
|
31043
|
+
},
|
|
31044
|
+
source
|
|
31045
|
+
};
|
|
31046
|
+
}))).filter((file) => file !== void 0);
|
|
31047
|
+
yield* Effect.forEach(files, (file) => Effect.gen(function* () {
|
|
31048
|
+
if (yield* fileSystem.exists(file.destination)) return yield* invalid(file.source, `Public route '${file.route.pathname}' overlaps generated client output.`);
|
|
31049
|
+
}));
|
|
31050
|
+
yield* Effect.forEach(files, (file) => Effect.gen(function* () {
|
|
31051
|
+
yield* fileSystem.makeDirectory(path.dirname(file.destination), { recursive: true });
|
|
31052
|
+
yield* fileSystem.writeFile(file.destination, file.bytes);
|
|
31053
|
+
}), { discard: true });
|
|
31054
|
+
return {
|
|
31055
|
+
files: files.length,
|
|
31056
|
+
routes: files.map((file) => file.route)
|
|
31057
|
+
};
|
|
31058
|
+
});
|
|
31059
|
+
//#endregion
|
|
30880
31060
|
//#region src/cli/build/client.ts
|
|
30881
31061
|
const buildClient = Effect.fn("Deploy.buildClient")(function* (appDirectory, outputDirectory) {
|
|
30882
31062
|
const fileSystem = yield* FileSystem.FileSystem;
|
|
@@ -30896,9 +31076,9 @@ const buildClient = Effect.fn("Deploy.buildClient")(function* (appDirectory, out
|
|
|
30896
31076
|
rolldownOptions: {
|
|
30897
31077
|
input: clientEntryId,
|
|
30898
31078
|
output: {
|
|
30899
|
-
assetFileNames: "assets/[name]-[hash][extname]",
|
|
30900
|
-
chunkFileNames: "assets/[name]-[hash].js",
|
|
30901
|
-
entryFileNames: "assets/
|
|
31079
|
+
assetFileNames: (asset) => asset.names.some((name) => name.endsWith(".css")) ? "assets/styles-[hash][extname]" : "assets/[name]-[hash][extname]",
|
|
31080
|
+
chunkFileNames: "assets/chunks/[name]-[hash].js",
|
|
31081
|
+
entryFileNames: "assets/main-[hash].js"
|
|
30902
31082
|
}
|
|
30903
31083
|
},
|
|
30904
31084
|
sourcemap: false
|
|
@@ -30934,25 +31114,35 @@ const buildClient = Effect.fn("Deploy.buildClient")(function* (appDirectory, out
|
|
|
30934
31114
|
let icon;
|
|
30935
31115
|
if (clientFiles.iconPath !== void 0) {
|
|
30936
31116
|
const bytes = yield* fileSystem.readFile(clientFiles.iconPath);
|
|
30937
|
-
const fileName = `icon-${yield* sha256(bytes)}.svg`;
|
|
31117
|
+
const fileName = `icon-${(yield* sha256(bytes)).slice(0, 16)}.svg`;
|
|
30938
31118
|
const assetsDirectory = path.join(outputDirectory, "assets");
|
|
30939
31119
|
yield* fileSystem.makeDirectory(assetsDirectory, { recursive: true });
|
|
30940
31120
|
yield* fileSystem.writeFile(path.join(assetsDirectory, fileName), bytes);
|
|
30941
|
-
icon = `/assets/${fileName}`;
|
|
31121
|
+
icon = `/_ignotum/assets/${fileName}`;
|
|
30942
31122
|
}
|
|
30943
|
-
const shellPath = path.join(outputDirectory, "
|
|
31123
|
+
const shellPath = path.join(outputDirectory, "shell.html");
|
|
30944
31124
|
const document = renderClientDocument({
|
|
30945
31125
|
icon,
|
|
30946
31126
|
scripts: [{
|
|
30947
|
-
source: entryFile
|
|
31127
|
+
source: `_ignotum/${entryFile}`,
|
|
30948
31128
|
type: "External"
|
|
30949
31129
|
}],
|
|
30950
|
-
styles,
|
|
31130
|
+
styles: styles.map((style) => `_ignotum/${style}`),
|
|
30951
31131
|
title
|
|
30952
31132
|
});
|
|
30953
|
-
|
|
31133
|
+
const shellBytes = utf8Bytes(document);
|
|
31134
|
+
yield* fileSystem.writeFile(shellPath, shellBytes);
|
|
31135
|
+
const publicBuild = yield* copyPublicFiles(appDirectory, outputDirectory);
|
|
31136
|
+
const manifest = {
|
|
31137
|
+
formatVersion: 1,
|
|
31138
|
+
shell: yield* artifactReference(clientShellPath, shellBytes),
|
|
31139
|
+
routes: publicBuild.routes
|
|
31140
|
+
};
|
|
31141
|
+
const manifestPath = path.join(outputDirectory, "manifest.json");
|
|
31142
|
+
yield* fileSystem.writeFileString(manifestPath, `${encodeCanonical(ClientManifest, manifest)}\n`);
|
|
30954
31143
|
return {
|
|
30955
|
-
assets: output.length + (icon === void 0 ? 0 : 1),
|
|
31144
|
+
assets: output.length + (icon === void 0 ? 0 : 1) + publicBuild.files,
|
|
31145
|
+
manifestPath,
|
|
30956
31146
|
shellPath
|
|
30957
31147
|
};
|
|
30958
31148
|
});
|
|
@@ -31235,6 +31425,7 @@ const buildServerFunction = Effect.fn("Deploy.buildServerFunction")(function* (a
|
|
|
31235
31425
|
logLevel: "warn",
|
|
31236
31426
|
mode: "production",
|
|
31237
31427
|
plugins: [boundaries, serverFunctionEntryPlugin(definition)],
|
|
31428
|
+
publicDir: false,
|
|
31238
31429
|
resolve: {
|
|
31239
31430
|
conditions: [...conditions],
|
|
31240
31431
|
noExternal: true,
|
|
@@ -31882,7 +32073,7 @@ var QueryInvalidation = class extends Context.Service()("@ignotum/runtime/sync/Q
|
|
|
31882
32073
|
//#region ../contracts/dist/runtime/transport.js
|
|
31883
32074
|
const runtimeInvocationPath = "/v1/invoke";
|
|
31884
32075
|
const runtimeRevisionPath = "/v1/revision";
|
|
31885
|
-
const appSyncPath =
|
|
32076
|
+
const appSyncPath = `/_ignotum/v1/sync`;
|
|
31886
32077
|
const RuntimeRequestPath = Schema.Literals([runtimeInvocationPath, runtimeRevisionPath]);
|
|
31887
32078
|
const RuntimeRequestTimestamp = Schema.FiniteFromString.pipe(Schema.check(Schema.isInt(), Schema.isGreaterThanOrEqualTo(0)), Schema.brand("ignotum/runtime/RequestTimestamp"));
|
|
31888
32079
|
const RuntimeRequestSignature = Sha256.pipe(Schema.brand("ignotum/runtime/RequestSignature"));
|
|
@@ -32810,13 +33001,13 @@ const agentAppFiles = [
|
|
|
32810
33001
|
path: ".agents/skills/ignotum/SKILL.md"
|
|
32811
33002
|
},
|
|
32812
33003
|
...[
|
|
32813
|
-
["client.md", "# Client\n\nIgnotum apps use JSX, hooks from `ignotum/client`, and Tailwind CSS. The required\n`client/index.tsx` file defines the browser title and root component:\n\n```tsx\nimport { app, Query, Result, useMutation, useQuery } from \"ignotum/client\";\n\nimport { api } from \"@/_generated/api.js\";\n\nfunction App() {\n return <main>My App</main>;\n}\n\nexport default app({\n title: \"My App\",\n component: App,\n});\n```\n\nThe title must be a non-empty quoted string in the `app(...)` definition so Ignotum can include it\nin the first HTML response.\n\n## Tailwind CSS\n\nIgnotum loads Tailwind CSS automatically. App code does not need to import a framework stylesheet.\n\nStyle JSX with Tailwind utility classes. Use the JSX `class` attribute:\n\n```tsx\nfunction App() {\n return (\n <main class=\"mx-auto max-w-xl px-6 py-16\">\n <h1 class=\"text-2xl font-semibold text-zinc-950\">Todos</h1>\n </main>\n );\n}\n```\n\nThe app does not need a Tailwind configuration file. Custom CSS files are ordinary client modules:\ngive them any filename and import them from app code when needed.\n\n## Run a query\n\n`useQuery` takes a generated query reference. Pass the typed arguments when the query declares\nthem:\n\n```tsx\nconst todos = useQuery(api.todos.list);\nconst todo = useQuery(api.todos.get, { id });\nconst selectedTodo = useQuery(api.todos.get, id === undefined ? Query.skip : { id });\n```\n\n`Query.skip` keeps a query pending without opening a subscription. Use it when the arguments are\nnot available yet.\n\nThe first value is pending. Match every state with `Result.match`:\n\n```tsx\nreturn Result.match(todos, {\n pending: () => <p class=\"text-zinc-500\">Loading...</p>,\n value: (items) => (\n <ul>\n {items.map((todo) => (\n <li key={todo.id}>{todo.text}</li>\n ))}\n </ul>\n ),\n});\n```\n\nIgnotum keeps an active query up to date. A successful mutation refreshes affected subscribed\nqueries in every open client. Queries that did not read the changed documents or tables do not run\nagain.\n\n## Run a mutation\n\n`useMutation` takes a generated mutation reference and returns a function:\n\n```tsx\nconst createTodo = useMutation(api.todos.create);\n\nvoid createTodo({ text }).then(\n Result.match({\n value: (id) => console.log(id),\n error: {\n InvalidTodoText: ({ text }) => console.log(`Invalid text: ${text}`),\n TodoLimitReached: ({ limit }) => console.log(`The limit is ${limit}`),\n },\n internalError: ({ requestId }) => console.log(`Request ${requestId} failed.`),\n }),\n);\n```\n\nAn argument-free mutation returns a zero-argument function:\n\n```tsx\nconst clearTodos = useMutation(api.todos.clear);\nvoid clearTodos();\n```\n\nThe client `Result` API only inspects completed server responses. Server-only operations such as\n`Result.fail`, `Result.succeed`, `Result.try`, `yield*`, and `.catch()` are not available here.\n\nApplication errors are handled by `error`, either with one function or an exhaustive map keyed by\n`_tag`. An `InternalServerError` contains a request ID and is thrown when `Result.match` has no\n`internalError` handler. During rendering, it reaches the nearest UI error boundary. A mutation can\nhandle it locally with `internalError`, as above. Operation-scoped protocol failures reject mutation\npromises or reach the nearest query UI error boundary. Ignotum retries transient connection failures,\nkeeps the latest query state, and replays pending mutations after the next connection handshake.\n\nHosted apps limit connections, subscriptions, active unique queries, and mutation traffic. See\n[Limits](limits.md) for the current values and retry retention.\n"],
|
|
32814
|
-
["deploy.md", "# Deploy\n\nSet your API token in your shell:\n\n```sh\nexport IGNOTUM_API_TOKEN=your-token\n```\n\nDo not put the token in the app directory or commit it to source control.\n\nOn the first deploy, pass the app slug:\n\n```sh\nnpx ignotum deploy --app my-app\n```\n\nIf the slug belongs to one of your apps, Ignotum links it. Otherwise, Ignotum creates the app.\nWithout `--app`, the first deploy asks for the slug interactively.\n\nIgnotum stores the app ID, slug, and API URL in `.ignotum/app.json`. It never stores the access\ntoken there. Later deploys reuse the saved app, so the command has no app flag:\n\n```sh\nnpx ignotum deploy\n```\n\nThere is no separate build command. `deploy` generates the current bindings and builds both parts\nof the application, uploads the artifact, and makes the new deployment active.\n\nThe client output contains
|
|
33004
|
+
["client.md", "# Client\n\nIgnotum apps use JSX, hooks from `ignotum/client`, and Tailwind CSS. The required\n`client/index.tsx` file defines the browser title and root component:\n\n```tsx\nimport { app, Query, Result, useMutation, useQuery } from \"ignotum/client\";\n\nimport { api } from \"@/_generated/api.js\";\n\nfunction App() {\n return <main>My App</main>;\n}\n\nexport default app({\n title: \"My App\",\n component: App,\n});\n```\n\nThe title must be a non-empty quoted string in the `app(...)` definition so Ignotum can include it\nin the first HTML response.\n\n## Tailwind CSS\n\nIgnotum loads Tailwind CSS automatically. App code does not need to import a framework stylesheet.\n\nStyle JSX with Tailwind utility classes. Use the JSX `class` attribute:\n\n```tsx\nfunction App() {\n return (\n <main class=\"mx-auto max-w-xl px-6 py-16\">\n <h1 class=\"text-2xl font-semibold text-zinc-950\">Todos</h1>\n </main>\n );\n}\n```\n\nThe app does not need a Tailwind configuration file. Custom CSS files are ordinary client modules:\ngive them any filename and import them from app code when needed.\n\n## Public files\n\nPut files that need their own URL in a top-level `public` directory next to `client` and `server`.\nThe directory structure becomes the URL structure. For example, `public/documents/manual.pdf` is\navailable at `/documents/manual.pdf` after deployment.\n\nPublic files are limited to AVIF, GIF, ICO, JPEG, PNG, WebP, and PDF. Ignotum checks that a file's\ncontents match its extension and rejects symbolic links, executable or code formats, and paths\nunder the reserved `_ignotum` name. Use `client/icon.svg` for the app favicon; arbitrary SVG files\nare not accepted in `public`. Put the directory at the app root—`client/public` is rejected.\n\n## Run a query\n\n`useQuery` takes a generated query reference. Pass the typed arguments when the query declares\nthem:\n\n```tsx\nconst todos = useQuery(api.todos.list);\nconst todo = useQuery(api.todos.get, { id });\nconst selectedTodo = useQuery(api.todos.get, id === undefined ? Query.skip : { id });\n```\n\n`Query.skip` keeps a query pending without opening a subscription. Use it when the arguments are\nnot available yet.\n\nThe first value is pending. Match every state with `Result.match`:\n\n```tsx\nreturn Result.match(todos, {\n pending: () => <p class=\"text-zinc-500\">Loading...</p>,\n value: (items) => (\n <ul>\n {items.map((todo) => (\n <li key={todo.id}>{todo.text}</li>\n ))}\n </ul>\n ),\n});\n```\n\nIgnotum keeps an active query up to date. A successful mutation refreshes affected subscribed\nqueries in every open client. Queries that did not read the changed documents or tables do not run\nagain.\n\n## Run a mutation\n\n`useMutation` takes a generated mutation reference and returns a function:\n\n```tsx\nconst createTodo = useMutation(api.todos.create);\n\nvoid createTodo({ text }).then(\n Result.match({\n value: (id) => console.log(id),\n error: {\n InvalidTodoText: ({ text }) => console.log(`Invalid text: ${text}`),\n TodoLimitReached: ({ limit }) => console.log(`The limit is ${limit}`),\n },\n internalError: ({ requestId }) => console.log(`Request ${requestId} failed.`),\n }),\n);\n```\n\nAn argument-free mutation returns a zero-argument function:\n\n```tsx\nconst clearTodos = useMutation(api.todos.clear);\nvoid clearTodos();\n```\n\nThe client `Result` API only inspects completed server responses. Server-only operations such as\n`Result.fail`, `Result.succeed`, `Result.try`, `yield*`, and `.catch()` are not available here.\n\nApplication errors are handled by `error`, either with one function or an exhaustive map keyed by\n`_tag`. An `InternalServerError` contains a request ID and is thrown when `Result.match` has no\n`internalError` handler. During rendering, it reaches the nearest UI error boundary. A mutation can\nhandle it locally with `internalError`, as above. Operation-scoped protocol failures reject mutation\npromises or reach the nearest query UI error boundary. Ignotum retries transient connection failures,\nkeeps the latest query state, and replays pending mutations after the next connection handshake.\n\nHosted apps limit connections, subscriptions, active unique queries, and mutation traffic. See\n[Limits](limits.md) for the current values and retry retention.\n"],
|
|
33005
|
+
["deploy.md", "# Deploy\n\nSet your API token in your shell:\n\n```sh\nexport IGNOTUM_API_TOKEN=your-token\n```\n\nDo not put the token in the app directory or commit it to source control.\n\nOn the first deploy, pass the app slug:\n\n```sh\nnpx ignotum deploy --app my-app\n```\n\nIf the slug belongs to one of your apps, Ignotum links it. Otherwise, Ignotum creates the app.\nWithout `--app`, the first deploy asks for the slug interactively.\n\nIgnotum stores the app ID, slug, and API URL in `.ignotum/app.json`. It never stores the access\ntoken there. Later deploys reuse the saved app, so the command has no app flag:\n\n```sh\nnpx ignotum deploy\n```\n\nThere is no separate build command. `deploy` generates the current bindings and builds both parts\nof the application, uploads the artifact, and makes the new deployment active.\n\nThe client output contains the SPA shell, generated assets, and validated files from the top-level\n`public` directory. The server output contains one bundle for every exported query or mutation.\nIgnotum writes both under `.ignotum/build` only after the complete build succeeds. If a rebuild\nfails, the previous successful output stays in place.\n\nUploads stream from disk. If an upload is interrupted after it begins, Ignotum leaves the active\ndeployment unchanged and prints the incomplete deployment ID. A rejected upload reports the\nAPI's reason. A successful deploy prints the app URL and deployment ID.\n\nThe CLI uses `https://api.ignotum.cloud` by default. Set `IGNOTUM_API_URL` only when targeting a\ndifferent Ignotum API, such as a local or development deployment.\n\nIgnotum rejects deployments that exceed the hosted file or artifact size limits. It also removes\nunfinished uploads and older inactive deployments after their retention periods. See\n[Limits](limits.md) for the current values.\n"],
|
|
32815
33006
|
["dev-server.md", "# Dev server\n\nRun the dev server from the app root:\n\n```sh\nnpx ignotum dev\n```\n\nIt expects these files:\n\n```text\nclient/index.tsx\nserver/schema.ts\n```\n\nThe dev server generates the client and server bindings, reads the `app(...)` definition from\n`client/index.tsx`, loads Tailwind automatically, and serves the app at\n<http://127.0.0.1:3210>. It reloads client and server changes and updates active queries after\nserver changes and successful mutations.\n\nYou do not need an HTML file, Vite configuration, Tailwind configuration, or framework stylesheet.\nAdd an optional `client/icon.svg` and Ignotum uses it as the favicon automatically. With no icon\nfile, the HTML contains no favicon link.\n\n## Flags\n\nUse flags to change the address or open the browser:\n\n```sh\nnpx ignotum dev --host 0.0.0.0 --port 3000 --open\n```\n\nThe defaults are host `127.0.0.1`, port `3210`, and no automatic browser opening.\n\n## Code generation\n\nThe dev server runs code generation when it starts. It updates generated files when you add or\nremove a server function file.\n\nRun code generation before typechecking without the dev server:\n\n```sh\nnpx ignotum codegen\nnpx tsc --noEmit\n```\n\nIgnotum creates:\n\n- `_generated/server.ts` with schema-bound `query`, `mutation`, and `values` exports;\n- `_generated/api.ts` with client references such as `api.todos.list`;\n- `_generated/types.ts` with `DataModel`, `Doc`, and `Id`.\n\nDo not edit generated files.\n\n## Local data\n\nData persists between dev-server restarts. Stop the server and reset that data with:\n\n```sh\nnpx ignotum dev db reset\n```\n"],
|
|
32816
33007
|
["getting-started.md", "# Getting started\n\nIgnotum requires Node.js 22.18 or newer.\n\nCreate an app:\n\n```sh\nnpx ignotum new my-app\ncd my-app\nnpx ignotum dev\n```\n\nOpen <http://127.0.0.1:3210>. The generated app is a small counter with a schema, a query, a\nmutation, and a JSX client.\n\n`ignotum new` installs dependencies with pnpm when it is available. It falls back to npm only when\npnpm is not installed. It generates `_generated`, initializes a Git repository, and creates an\n`Init` commit containing the generated files after the rest of the setup finishes.\n\nPass `.` to create the app in the current directory. The directory must be empty:\n\n```sh\nnpx ignotum new .\n```\n\nUse `--no-git` to skip Git or `--no-install` to skip dependency installation. You can install the\ndependencies later with the same pnpm and npm fallback behavior:\n\n```sh\nnpx ignotum install\n```\n\nRead [schema syntax](schema.md), [server functions](server-functions.md), and the\n[client guide](client.md) to build the app. The [manual setup](manual-setup.md) recreates the counter\napp without `ignotum new`. When it is ready, follow the [deploy guide](deploy.md) to create or link\nthe hosted app and publish it.\n"],
|
|
32817
33008
|
["index.md", "# Ignotum\n\nAn Ignotum app has a schema, server functions, and a client. Ignotum is opinionated about the\nclient tooling. Every app uses JSX, hooks from `ignotum/client`, and Tailwind CSS.\n\nThe current release supports local development and hosted deployment.\n\n- [Getting started](getting-started.md) creates and runs a counter app with `ignotum new`.\n- [Manual setup](manual-setup.md) recreates the generated counter app by hand.\n- [Schema syntax](schema.md) covers tables, fields, IDs, and generated document types.\n- [Values](values.md) lists every value validator and its TypeScript type.\n- [Server functions](server-functions.md) covers queries, mutations, database access, and errors.\n- [Client](client.md) covers queries, mutations, results, and Tailwind styling.\n- [Dev server](dev-server.md) covers local development, code generation, flags, and data reset.\n- [Deploy](deploy.md) creates or links an app, then uploads and activates a deployment.\n- [Limits](limits.md) lists hosted limits for functions, data, realtime updates, and deployments.\n"],
|
|
32818
33009
|
["limits.md", "# Limits\n\nThese limits apply to hosted Ignotum apps. The local dev server does not reproduce every hosted\nlimit, so an operation that works locally can still be rejected after deployment.\n\nIgnotum reports connection, traffic, time, result, and storage-quota failures as\n`ResourceLimitExceeded`. A database read or write that crosses a document or collection limit can\nappear as a temporary function failure. A deployment that exceeds a limit fails before activation,\nso the current deployment stays active.\n\n## Server functions\n\n| Limit | Value |\n| --------------------------------------- | ---------: |\n| Function arguments | 16 KiB |\n| Function result | 1 MiB |\n| Execution time | 10 seconds |\n| Memory | 32 MiB |\n| Stack | 512 KiB |\n| Ignotum operations during one execution | 1,000 |\n\nArgument and result sizes use their JSON representation. The result limit applies to successful\nresults and application errors.\n\nAn Ignotum operation is a call through the function context, such as a database read or write. A\nfunction stops when it reaches the execution time or operation limit.\n\nIgnotum retries one query execution when app data changes while it runs. That execution makes at\nmost four attempts and stops after 15 seconds. The query fails temporarily if it cannot read a\nconsistent result in that time.\n\n## App data\n\n| Limit | Value |\n| --------------------------------------- | -------------: |\n| Stored fields in one document | 256 KiB |\n| Documents returned by `collect()` | 1,000 |\n| Document fields returned by `collect()` | 1 MiB |\n| Stored app data | 64 MiB per app |\n\nThe 1 MiB function-result limit still applies to `collect()`. Document IDs and timestamps take some\nspace in that result, so a collection can reach the result limit before its fields reach 1 MiB.\n\nIf a mutation would take the app over its storage limit, Ignotum rolls back the whole mutation.\nThe quota covers stored documents and the records Ignotum keeps to process mutations safely.\nDeployment files do not count as stored app data.\n\n## Realtime connections and calls\n\n| Limit | Value |\n| ------------------------- | ---------------------------: |\n| Live connections | 256 per app |\n| Incoming realtime message | 64 KiB |\n| Subscriptions | 128 per connection |\n| Active unique queries | 64 per app |\n| Realtime query refresh | 4 attempts within 15 seconds |\n| Concurrent operations | 32 per app |\n| Mutation execution | 1 at a time per app |\n| Unresolved mutations | 32 per app |\n| Mutation calls | 60 per minute per app |\n\nA unique query is one function and argument combination. Several components or browser tabs can\nsubscribe to the same unique query without using another unique-query slot.\n\nA realtime refresh can repeat a query execution when app data changes again during the refresh.\n\nThe unresolved-mutation limit protects calls whose outcome is not known yet, such as a call waiting\nfor a retry after a connection failure. Once Ignotum receives a final result, that call no longer\ncounts toward the limit.\n\nIgnotum runs mutations for the same app one at a time. Queries and other live calls can still use\nthe remaining concurrent-operation slots.\n\n## Mutation retries\n\nThe client keeps the ID of a pending mutation and reuses it after a reconnect. Ignotum remembers up\nto 10,000 mutation results per app for seven days. During that period, a retry returns the recorded\nresult instead of running the mutation again.\n\nDo not treat this as permanent duplicate protection. A mutation may run again after its record has\nexpired or fallen outside the 10,000 most recent results.\n\n## Deployments\n\n| Limit | Value |\n| ------------------------------ | ------: |\n| Files listed in one deployment | 512 |\n| One listed file | 16 MiB |\n| Deployment inventory | 1 MiB |\n| Server files combined | 64 MiB |\n| Listed files combined | 128 MiB |\n\nIgnotum never deletes the active deployment. It also protects the three newest deployments that\ncompleted successfully. An older inactive deployment becomes eligible for deletion after seven\ndays. An unfinished upload becomes eligible after 24 hours.\n"],
|
|
32819
|
-
["manual-setup.md", "# Manual setup\n\nIgnotum requires Node.js 22.18 or newer. This guide recreates the counter app from `ignotum new`\nwithout running the app generator. It uses pnpm to install dependencies.\n\nCreate the app directory:\n\n```sh\nmkdir my-ignotum-app\ncd my-ignotum-app\n```\n\nCreate this structure:\n\n```text\nmy-ignotum-app/\n client/\n index.tsx\n icon.svg\n server/\n counter.ts\n schema.ts\n shared/\n utils.ts\n package.json\n tsconfig.json\n```\n\n## Configure the package\n\nAdd `package.json`:\n\n```json\n{\n \"name\": \"my-ignotum-app\",\n \"private\": true,\n \"version\": \"1.0.0\",\n \"type\": \"module\",\n \"scripts\": {\n \"typecheck\": \"ignotum codegen && tsc --noEmit\"\n },\n \"dependencies\": {\n \"ignotum\": \"latest\"\n },\n \"devDependencies\": {\n \"typescript\": \"^7.0.2\"\n },\n \"engines\": {\n \"node\": \">=22.18.0\"\n }\n}\n```\n\nInstall the dependencies:\n\n```sh\npnpm install\n```\n\n## Configure TypeScript\n\nAdd `tsconfig.json`:\n\n```json\n{\n \"compilerOptions\": {\n \"target\": \"ES2023\",\n \"lib\": [\"ES2023\", \"DOM\", \"DOM.Iterable\"],\n \"module\": \"NodeNext\",\n \"moduleResolution\": \"NodeNext\",\n \"jsx\": \"react-jsx\",\n \"jsxImportSource\": \"ignotum/client\",\n \"strict\": true,\n \"noEmit\": true,\n \"skipLibCheck\": true,\n \"paths\": {\n \"@/*\": [\"./*\"]\n }\n },\n \"include\": [\"_generated\", \"client\", \"server\", \"shared\"]\n}\n```\n\n## Add shared code\n\nAdd `shared/utils.ts`:\n\n```ts\nexport const counterIncrement = 1;\n```\n\nBoth the client and server can import files in `shared` through `@/shared`.\n\n## Define the schema\n\nAdd `server/schema.ts`:\n\n```ts\nimport { defineSchema } from \"ignotum/server\";\n\nexport default defineSchema(({ table, values }) => ({\n counters: table({\n value: values.number(),\n }),\n}));\n```\n\n## Add the server functions\n\nAdd `server/counter.ts`:\n\n```ts\nimport { mutation, query, values } from \"@/_generated/server.js\";\nimport { counterIncrement } from \"@/shared/utils.js\";\n\nexport const get = query({\n returns: values.number(),\n\n handler: function* (ctx) {\n const counters = yield* ctx.db.query(\"counters\").collect();\n return counters[0]?.value ?? 0;\n },\n});\n\nexport const increment = mutation({\n returns: values.number(),\n\n handler: function* (ctx) {\n const counters = yield* ctx.db.query(\"counters\").collect();\n const counter = counters[0];\n const value = (counter?.value ?? 0) + counterIncrement;\n\n if (counter === undefined) {\n yield* ctx.db.insert(\"counters\", { value });\n } else {\n yield* ctx.db.patch(\"counters\", counter.id, { value });\n }\n\n return value;\n },\n});\n```\n\nKeep the `.js` suffix on imports from `@/_generated`, even though the generated files use\nTypeScript.\n\n## Add the client\n\nAdd `client/index.tsx`:\n\n```tsx\nimport { app, Result, useMutation, useQuery } from \"ignotum/client\";\n\nimport { api } from \"@/_generated/api.js\";\nimport { counterIncrement } from \"@/shared/utils.js\";\n\nfunction App() {\n const count = useQuery(api.counter.get);\n const increment = useMutation(api.counter.increment);\n\n return (\n <main class=\"mx-auto max-w-sm px-6 py-20 text-center\">\n <h1 class=\"text-2xl font-semibold\">Counter</h1>\n {Result.match(count, {\n pending: () => <p class=\"mt-6\">Loading...</p>,\n value: (value) => (\n <>\n <p class=\"my-6 text-5xl tabular-nums\">{value}</p>\n <button\n class=\"rounded bg-zinc-900 px-4 py-2 text-white\"\n type=\"button\"\n onClick={() => void increment()}\n >\n Increment by {counterIncrement}\n </button>\n </>\n ),\n })}\n </main>\n );\n}\n\nexport default app({\n title: \"Counter\",\n component: App,\n});\n```\n\nIgnotum loads Tailwind automatically. You do not need an HTML file, Vite configuration, Tailwind\nconfiguration, or framework stylesheet. Custom CSS files are ordinary client modules and can use\nany filename when imported from app code.\n\nThe `client/icon.svg` file is optional. When present, Ignotum discovers it automatically and uses it\nas the favicon. When absent, the app has no favicon link.\n\n## Run the app\n\nStart the dev server:\n\n```sh\nnpx ignotum dev\n```\n\nOpen <http://127.0.0.1:3210>. Ignotum creates `_generated` before starting the app.\n\nRead [schema syntax](schema.md), [server functions](server-functions.md), and the\n[client guide](client.md) to continue building the app.\n"],
|
|
33010
|
+
["manual-setup.md", "# Manual setup\n\nIgnotum requires Node.js 22.18 or newer. This guide recreates the counter app from `ignotum new`\nwithout running the app generator. It uses pnpm to install dependencies.\n\nCreate the app directory:\n\n```sh\nmkdir my-ignotum-app\ncd my-ignotum-app\n```\n\nCreate this structure:\n\n```text\nmy-ignotum-app/\n client/\n index.tsx\n icon.svg\n public/\n manual.pdf\n server/\n counter.ts\n schema.ts\n shared/\n utils.ts\n package.json\n tsconfig.json\n```\n\n## Configure the package\n\nAdd `package.json`:\n\n```json\n{\n \"name\": \"my-ignotum-app\",\n \"private\": true,\n \"version\": \"1.0.0\",\n \"type\": \"module\",\n \"scripts\": {\n \"typecheck\": \"ignotum codegen && tsc --noEmit\"\n },\n \"dependencies\": {\n \"ignotum\": \"latest\"\n },\n \"devDependencies\": {\n \"typescript\": \"^7.0.2\"\n },\n \"engines\": {\n \"node\": \">=22.18.0\"\n }\n}\n```\n\nInstall the dependencies:\n\n```sh\npnpm install\n```\n\n## Configure TypeScript\n\nAdd `tsconfig.json`:\n\n```json\n{\n \"compilerOptions\": {\n \"target\": \"ES2023\",\n \"lib\": [\"ES2023\", \"DOM\", \"DOM.Iterable\"],\n \"module\": \"NodeNext\",\n \"moduleResolution\": \"NodeNext\",\n \"jsx\": \"react-jsx\",\n \"jsxImportSource\": \"ignotum/client\",\n \"strict\": true,\n \"noEmit\": true,\n \"skipLibCheck\": true,\n \"paths\": {\n \"@/*\": [\"./*\"]\n }\n },\n \"include\": [\"_generated\", \"client\", \"server\", \"shared\"]\n}\n```\n\n## Add shared code\n\nAdd `shared/utils.ts`:\n\n```ts\nexport const counterIncrement = 1;\n```\n\nBoth the client and server can import files in `shared` through `@/shared`.\n\n## Define the schema\n\nAdd `server/schema.ts`:\n\n```ts\nimport { defineSchema } from \"ignotum/server\";\n\nexport default defineSchema(({ table, values }) => ({\n counters: table({\n value: values.number(),\n }),\n}));\n```\n\n## Add the server functions\n\nAdd `server/counter.ts`:\n\n```ts\nimport { mutation, query, values } from \"@/_generated/server.js\";\nimport { counterIncrement } from \"@/shared/utils.js\";\n\nexport const get = query({\n returns: values.number(),\n\n handler: function* (ctx) {\n const counters = yield* ctx.db.query(\"counters\").collect();\n return counters[0]?.value ?? 0;\n },\n});\n\nexport const increment = mutation({\n returns: values.number(),\n\n handler: function* (ctx) {\n const counters = yield* ctx.db.query(\"counters\").collect();\n const counter = counters[0];\n const value = (counter?.value ?? 0) + counterIncrement;\n\n if (counter === undefined) {\n yield* ctx.db.insert(\"counters\", { value });\n } else {\n yield* ctx.db.patch(\"counters\", counter.id, { value });\n }\n\n return value;\n },\n});\n```\n\nKeep the `.js` suffix on imports from `@/_generated`, even though the generated files use\nTypeScript.\n\n## Add the client\n\nAdd `client/index.tsx`:\n\n```tsx\nimport { app, Result, useMutation, useQuery } from \"ignotum/client\";\n\nimport { api } from \"@/_generated/api.js\";\nimport { counterIncrement } from \"@/shared/utils.js\";\n\nfunction App() {\n const count = useQuery(api.counter.get);\n const increment = useMutation(api.counter.increment);\n\n return (\n <main class=\"mx-auto max-w-sm px-6 py-20 text-center\">\n <h1 class=\"text-2xl font-semibold\">Counter</h1>\n {Result.match(count, {\n pending: () => <p class=\"mt-6\">Loading...</p>,\n value: (value) => (\n <>\n <p class=\"my-6 text-5xl tabular-nums\">{value}</p>\n <button\n class=\"rounded bg-zinc-900 px-4 py-2 text-white\"\n type=\"button\"\n onClick={() => void increment()}\n >\n Increment by {counterIncrement}\n </button>\n </>\n ),\n })}\n </main>\n );\n}\n\nexport default app({\n title: \"Counter\",\n component: App,\n});\n```\n\nIgnotum loads Tailwind automatically. You do not need an HTML file, Vite configuration, Tailwind\nconfiguration, or framework stylesheet. Custom CSS files are ordinary client modules and can use\nany filename when imported from app code.\n\nThe `client/icon.svg` file is optional. When present, Ignotum discovers it automatically and uses it\nas the favicon. When absent, the app has no favicon link.\n\nThe top-level `public` directory is optional. Its AVIF, GIF, ICO, JPEG, PNG, WebP, and PDF files keep\ntheir relative paths as public URLs. Do not put this directory inside `client`.\n\n## Run the app\n\nStart the dev server:\n\n```sh\nnpx ignotum dev\n```\n\nOpen <http://127.0.0.1:3210>. Ignotum creates `_generated` before starting the app.\n\nRead [schema syntax](schema.md), [server functions](server-functions.md), and the\n[client guide](client.md) to continue building the app.\n"],
|
|
32820
33011
|
["schema.md", "# Schema syntax\n\nDefine the data model in `server/schema.ts`. The keys returned from `defineSchema` are table\nnames, and each `table` call defines that table's fields:\n\n```ts\nimport { defineSchema } from \"ignotum/server\";\n\nexport default defineSchema(({ table, values }) => ({\n users: table({\n name: values.string(),\n }),\n todos: table({\n text: values.string(),\n completed: values.boolean(),\n ownerId: values.optional(values.id(\"users\")),\n }),\n}));\n```\n\n## Field values\n\nRead [values](values.md) for the complete validator list and the TypeScript type produced by each\none. `values.id` only accepts a table declared in the same schema. Arrays and objects can be nested,\nand their contents can use any value validator.\n\n## System fields\n\nIgnotum adds three fields to every stored document:\n\n| Field | Type |\n| ----------- | ------------------------- |\n| `id` | The ID type for its table |\n| `createdAt` | `Date` |\n| `updatedAt` | `Date` |\n\nDo not declare these fields in a table. Do not pass them to `insert`, `patch`, or `replace`.\n\n## Generated types\n\n`_generated/types.ts` exports the data model, document, and ID types:\n\n```ts\nimport type { DataModel, Doc, Id } from \"@/_generated/types.js\";\n\ntype Todo = Doc<\"todos\">;\ntype TodoId = Id<\"todos\">;\n```\n\n`Doc<\"todos\">` includes the fields from the `todos` table and its three system fields. An\n`Id<\"todos\">` cannot be passed where an `Id<\"users\">` is required.\n"],
|
|
32821
33012
|
["server-functions.md", "# Server functions\n\nPut queries and mutations in `.ts` files directly inside `server`. The file name becomes the API\nmodule, and each exported function keeps its export name:\n\n```text\nserver/todos.ts -> api.todos.list\nserver/users.ts -> api.users.get\n```\n\nIgnotum ignores `schema.ts`, `index.ts`, test files, and names beginning with `_`.\n\nImport schema-bound builders from the generated server file. Import `Result` only when the module\nintroduces or catches typed errors:\n\n```ts\nimport { Result } from \"ignotum/server\";\n\nimport { mutation, query, values } from \"@/_generated/server.js\";\n```\n\nKeep the `.js` suffix on generated imports.\n\n## Define a function\n\nA function has optional argument, return, and public error schemas, plus a generator handler:\n\n```ts\nexport const getTitle = query({\n args: {\n id: values.id(\"todos\"),\n },\n returns: values.string(),\n\n handler: function* (ctx, args) {\n const todo = yield* ctx.db.get(\"todos\", args.id);\n return todo.text;\n },\n});\n```\n\nOmit `args` when the function takes no arguments. Omit `returns` when it returns nothing. An\nomitted `returns` only permits a `void` handler; Ignotum never infers an unchecked return schema.\n`yield*` waits for an Ignotum operation and propagates its typed application errors. Return\nsuccessful values with ordinary `return`.\n\nIgnotum validates arguments before running the handler. It also validates returned values and\npublic application errors before sending them to a client.\n\nAn argument-free query and a mutation with no return value can stay small:\n\n```ts\nconst Todo = values.doc(\"todos\");\n\nexport const list = query({\n returns: values.array(Todo),\n\n handler: function* (ctx) {\n return yield* ctx.db.query(\"todos\").collect();\n },\n});\n\nexport const remove = mutation({\n args: { id: values.id(\"todos\") },\n\n handler: function* (ctx, args) {\n yield* ctx.db.delete(\"todos\", args.id);\n },\n});\n```\n\nCall an argument-free query as `useQuery(api.todos.list)`. An argument-free mutation returns a\nzero-argument function:\n\n```ts\nconst clear = useMutation(api.todos.clear);\nvoid clear();\n```\n\n## Database reads\n\nUse `find` when a missing document is a normal result:\n\n```ts\nconst todo = yield * ctx.db.find(\"todos\", args.id);\n// Todo | undefined\n```\n\nUse `get` when the document should exist:\n\n```ts\nconst todo = yield * ctx.db.get(\"todos\", args.id);\n// Todo\n```\n\nA missing `get` fails with a typed `DocumentNotFound` value containing `table` and `id`. Collect a\nwhole table through a query:\n\n```ts\nconst todos = yield * ctx.db.query(\"todos\").collect();\n```\n\nQuery handlers only receive read methods.\n\nHosted apps limit document size, collection reads, function runtime, and stored app data. See\n[Limits](limits.md) for the current values.\n\n## Database writes\n\nMutation handlers receive the read methods and these writes:\n\n```ts\nconst id =\n yield *\n ctx.db.insert(\"todos\", {\n text: \"Learn Ignotum\",\n completed: false,\n });\n\nyield * ctx.db.patch(\"todos\", id, { completed: true });\n\nyield *\n ctx.db.replace(\"todos\", id, {\n text: \"Build an app\",\n completed: false,\n });\n\nyield * ctx.db.delete(\"todos\", id);\n```\n\n`patch` changes only supplied fields. `replace` requires every non-optional table field. Ignotum\nrolls back a mutation when its handler fails with a typed application error or encounters an\ninternal failure.\n\n## Application errors\n\nDefine an application error with `values.error`. Its name becomes `_tag`:\n\n```ts\nconst TodoNotFound = values.error(\"TodoNotFound\", {\n id: values.id(\"todos\"),\n});\n```\n\n`Result.fail` deliberately stops the operation with a typed error:\n\n```ts\nyield * Result.fail(TodoNotFound({ id: args.id }));\n```\n\nThe `errors` field is optional. If omitted, Ignotum infers the handler's remaining application\nerrors. If supplied, it is the public contract and the handler must conform to it:\n\n```ts\nexport const toggle = mutation({\n args: {\n id: values.id(\"todos\"),\n },\n returns: values.boolean(),\n errors: TodoNotFound,\n\n handler: function* (ctx, args) {\n const todo = yield* ctx.db.get(\"todos\", args.id).catch({\n DocumentNotFound: (error) => Result.fail(TodoNotFound({ id: error.id })),\n });\n\n const completed = !todo.completed;\n yield* ctx.db.patch(\"todos\", args.id, { completed });\n return completed;\n },\n});\n```\n\nCombine public errors with `values.union`:\n\n```ts\nerrors: values.union(InvalidTodoText, TodoLimitReached),\n```\n\n## Catch and recover\n\nEvery Result operation has a partial, tag-based `catch`. Handlers receive the narrowed error type.\nUnmatched errors continue through the channel:\n\n```ts\nconst settings =\n yield *\n loadSettings().catch({\n SettingsNotFound: () => defaultSettings,\n });\n```\n\nReturn a plain value to recover. Return `Result.fail(...)` to map one error to another. Unknown tag\nnames fail the TypeScript check.\n\nUse `Result.try` for one catch boundary around several operations:\n\n```ts\nconst author =\n yield *\n Result.try(function* () {\n const membership = yield* ctx.db.get(\"memberships\", topic.membershipId);\n return yield* ctx.db.get(\"users\", membership.userId);\n }).catch({\n DocumentNotFound: () => Result.fail(InvalidAuthor({ topicId: topic.id })),\n });\n```\n\nThere is no async variant. Ignotum operations always use `yield*` in server code.\n\n## Standalone results\n\n`Result.succeed` remains useful for helpers that return a Result:\n\n```ts\nfunction validateName(name: string) {\n if (name.length === 0) {\n return Result.fail(InvalidName({}));\n }\n\n return Result.succeed(name.trim());\n}\n```\n\nA handler can use the helper with `const name = yield* validateName(args.name)`. Normal handlers do\nnot wrap successful returns in `Result.succeed`.\n\n## Internal failures and defects\n\nDatabase outages, internal runtime failures, and thrown JavaScript exceptions are not application\nerrors. Ignotum logs their full cause and sends only:\n\n```ts\n{\n _tag: \"InternalServerError\",\n requestId: \"...\",\n}\n```\n\nEvery generated client function includes `InternalServerError` in its Result error union. The\nrequest ID links the client-visible failure to server logs without exposing private details.\n`InternalServerError` is reserved by Ignotum: never define it with `values.error` or include it in a\nfunction's `errors` schema.\n"],
|
|
32822
33013
|
["values.md", "# Values\n\nUse `values` validators to describe table fields, function arguments, return values, and application\nerrors. Each validator checks values at runtime and supplies the matching TypeScript type.\n\n| Validator | TypeScript type | Notes |\n| ----------------------------------------- | ------------------------------------- | ---------------------------------------------------------------------------------------------------- |\n| `values.string()` | `string` | |\n| `values.number()` | `number` | Accepts finite JavaScript numbers, including integers. |\n| `values.integer()` | `number` | Accepts safe integers. |\n| `values.boolean()` | `boolean` | |\n| `values.date()` | `Date` | Accepts valid JavaScript dates. |\n| `values.null()` | `null` | Accepts only `null`. |\n| `values.literal(value)` | The exact type of `value` | Accepts one string, finite number, or boolean value. |\n| `values.literals(first, second, ...rest)` | A union of the supplied literal types | Requires at least two string, finite number, or boolean values. |\n| `values.id(\"todos\")` | `Id<\"todos\">` | Accepts an ID for a table declared in the same schema. IDs for different tables are different types. |\n| `values.doc(\"todos\")` | `Doc<\"todos\">` | Accepts a complete document, including its `id`, `createdAt`, and `updatedAt` system fields. |\n| `values.optional(value)` | `T \\| undefined` | Makes an object or table field optional. The field may be omitted. |\n| `values.nullable(value)` | `T \\| null` | The value remains required unless it is also wrapped with `optional`. |\n| `values.array(value)` | `ReadonlyArray<T>` | Every item must match `value`. |\n| `values.object(fields)` | An object matching `fields` | Defines an object with known field names. |\n| `values.record(value)` | `Readonly<Record<string, T>>` | Defines an object with dynamic string keys whose values all match `value`. |\n| `values.union(...values)` | A union of the supplied types | Accepts a value matching any supplied validator. |\n| `values.never()` | `never` | No value can pass this validator. |\n| `values.error(\"Name\", fields)` | A tagged error object | Defines an application error whose `_tag` is the supplied name. |\n\n`T` means the TypeScript type produced by the wrapped validator.\n\n`values.doc` is available on the schema-bound `values` exported by `_generated/server.ts`. It is\nnot available while defining `server/schema.ts`, because the table definitions are still being\ncreated there.\n\n## Object transforms\n\nValidators created by `values.object(fields)` and `values.doc(\"table\")` support these chainable\nmethods:\n\n| Method | Result |\n| ------------------- | -------------------------------------------------------------------- |\n| `.pick(...keys)` | Keeps the listed fields. Every key must exist. |\n| `.omit(...keys)` | Removes the listed fields. Every key must exist. |\n| `.extend(fields)` | Adds fields. It rejects names that already exist. |\n| `.override(fields)` | Replaces validators for existing fields. It rejects new field names. |\n| `.partial()` | Makes every current field optional. |\n\nEach method works from the result of the previous method. Added fields can be picked, omitted, or\noverridden immediately. An omitted name can be added again with a different validator.\n\n```ts\nconst TodoInput = values.doc(\"todos\").omit(\"id\", \"createdAt\", \"updatedAt\").partial().extend({\n requestId: values.string(),\n});\n```\n\nUse `override` when changing an existing field. This makes replacements visible in the definition\nand prevents `extend` from silently weakening fields such as `id`:\n\n```ts\nconst EditableTodo = values.doc(\"todos\").override({\n title: values.optional(values.string()),\n});\n```\n\nThese methods are only available on fixed object validators. Arrays, records, unions, errors, and\nprimitive validators do not expose them. After transforming a document validator, its inferred type\nmatches the current fields in the chain rather than the complete document type.\n\n### Reuse embedded objects in the schema\n\nCreate a fixed object validator inside the `defineSchema` callback when several stored fields share\nan object shape. A transform can derive a stored variant without repeating its fields:\n\n```ts\ndefineSchema(({ table, values }) => {\n const Contact = values.object({\n email: values.string(),\n phone: values.string(),\n });\n\n return {\n users: table({\n contact: Contact,\n }),\n publicProfiles: table({\n contact: Contact.omit(\"phone\"),\n }),\n };\n});\n```\n\nThis pattern suits embedded objects stored by more than one table, including full and reduced\nversions of the same object. The base validator stays inside `defineSchema`, where `values.id` can\ncheck its table references against the completed schema.\n\n### Derive server function validators from documents\n\nUse the schema-bound `values` from `_generated/server.ts` when a function input or output follows a\nstored document. These validators know every table name and the complete document fields, including\n`id`, `createdAt`, and `updatedAt`.\n\nReusable server validators can live in an ignored server module such as `server/_validators.ts`:\n\n```ts\nimport { values } from \"@/_generated/server.js\";\n\nexport const TodoInput = values.doc(\"todos\").omit(\"id\", \"createdAt\", \"updatedAt\").partial();\n\nexport const PublicTodo = values.doc(\"todos\").omit(\"updatedAt\");\n```\n\nImport these validators into queries and mutations that need the same contract. This works well for\npatch inputs and document projections. Do not import generated validators into `server/schema.ts`.\nDocument validators depend on the schema, so importing them while defining that schema would create\na cycle.\n\n## Examples\n\n```ts\nconst TodoStatus = values.literals(\"pending\", \"completed\");\n\nconst Todo = values.doc(\"todos\");\n\nexport const list = query({\n returns: values.array(Todo),\n handler: function* (ctx) {\n return yield* ctx.db.query(\"todos\").collect();\n },\n});\n```\n\nUse the other validators to define reusable values that do not represent a stored document:\n\n```ts\nconst TodoInput = values.object({\n status: TodoStatus,\n scheduledAt: values.nullable(values.date()),\n scores: values.record(values.integer()),\n title: values.string(),\n});\n```\n\nUse `optional` when a field may be absent. Use `nullable` when a present field may contain `null`:\n\n```ts\nvalues.object({\n nickname: values.optional(values.string()),\n deletedAt: values.nullable(values.date()),\n});\n```\n"]
|