ignotum 0.0.17 → 0.0.19
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 +73 -62
- package/dist/cli/Undici-C6Asbalt.mjs +30 -0
- package/dist/cli/Undici-C6Asbalt.mjs.map +1 -0
- package/dist/cli/bin.mjs +739 -24023
- package/dist/cli/bin.mjs.map +1 -1
- package/dist/platform/_headers +1 -1
- package/dist/platform/_ignotum/v1.1/client.js +12 -0
- package/dist/runtime/{api-BnUKx9cj-D49Tf0rX.d.ts → api-BnUKx9cj-K-F-SOLs.d.ts} +4 -4
- package/dist/runtime/{api-C-tltz6s.js → api-CrrSjYL1.js} +2 -2
- package/dist/runtime/{api-C-tltz6s.js.map → api-CrrSjYL1.js.map} +1 -1
- package/dist/runtime/{bootstrap-DlNIfH-r.js → bootstrap-CLVnD5vS.js} +4 -4
- package/dist/runtime/bootstrap-CLVnD5vS.js.map +1 -0
- package/dist/runtime/{bootstrap-DjTwGJao-DBx5zdFI.d.ts → bootstrap-DjTwGJao-DPi768T4.d.ts} +3 -3
- package/dist/runtime/client.d.ts +9 -9
- package/dist/runtime/client.js +4 -4
- package/dist/runtime/id-BPFV3D3V.d.ts +1 -0
- package/dist/runtime/{id-7an3nxTu-DZJSM5xh.d.ts → id-D87q-n2D-CDMwTSUR.d.ts} +2 -2
- package/dist/runtime/{id-BrEnxY-r.js → id-rzjaCxeF.js} +11 -2
- package/dist/runtime/{id-BrEnxY-r.js.map → id-rzjaCxeF.js.map} +1 -1
- package/dist/runtime/{index-Biaw5vdF.d.ts → index-BWHInzfU.d.ts} +3 -3
- package/dist/runtime/internal/api.d.ts +1 -1
- package/dist/runtime/internal/api.js +1 -1
- package/dist/runtime/internal/bootstrap.d.ts +1 -1
- package/dist/runtime/internal/bootstrap.js +1 -1
- package/dist/runtime/internal/client.d.ts +1 -1
- package/dist/runtime/internal/client.js +4 -4
- package/dist/runtime/internal/client.js.map +1 -1
- package/dist/runtime/internal/host.d.ts +6 -6
- package/dist/runtime/internal/host.js +3 -3
- package/dist/runtime/internal/routes.d.ts +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 +2 -2
- package/dist/runtime/internal/types.js +1 -1
- package/dist/runtime/pagination-BPFV3D3V.d.ts +1 -0
- package/dist/runtime/{pagination--f4kMKf6.js → pagination-CfEleffG.js} +3 -2
- package/dist/runtime/pagination-CfEleffG.js.map +1 -0
- package/dist/runtime/result-Bq32FA7g.d.ts +1 -0
- package/dist/runtime/{router-C1eC9yy5.js → router-CfNPtoVO.js} +109 -60
- package/dist/runtime/router-CfNPtoVO.js.map +1 -0
- package/dist/runtime/{schema-BSjepoNF.js → schema-JtFb4BbG.js} +3 -3
- package/dist/runtime/{schema-BSjepoNF.js.map → schema-JtFb4BbG.js.map} +1 -1
- package/dist/runtime/server.d.ts +5 -5
- package/dist/runtime/server.js +2 -2
- package/dist/runtime/{sync-By2hX5ev.d.ts → sync-BMmV8oYt.d.ts} +2 -2
- package/dist/runtime/{sync-BwgS19Xy.js → sync-DUfp7S5R.js} +2 -2
- package/dist/runtime/{sync-BwgS19Xy.js.map → sync-DUfp7S5R.js.map} +1 -1
- package/dist/runtime/{types-DeCCyqLU-CHx7KFtx.d.ts → types-BeT6F0Pg-DMvRDN0c.d.ts} +5 -5
- package/package.json +10 -9
- package/src/cli/app-configuration.ts +44 -5
- package/src/cli/auth-client.ts +3 -3
- package/src/cli/build/managed-server.ts +3 -2
- package/src/cli/build/server.ts +2 -2
- package/src/cli/command.ts +19 -11
- package/src/cli/control-client.ts +2 -2
- package/src/cli/deploy.ts +27 -2
- package/src/cli/dev.ts +1 -1
- package/src/cli/managed-dev-platform.ts +13 -4
- package/src/cli/module-boundaries.ts +1 -1
- package/src/cli/new-app.ts +35 -6
- package/src/cli/package-manager.ts +1 -1
- package/src/cli/path.ts +11 -0
- package/src/cli/route-codegen.ts +2 -1
- package/src/cli/route-static.ts +10 -4
- package/src/cli/typecheck.ts +66 -0
- package/src/cli/upload-journal.ts +3 -3
- package/src/dev-runtime/database.ts +1 -1
- package/src/dev-runtime/files.ts +1 -1
- package/src/dev-runtime/managed-functions.ts +3 -2
- package/src/dev-runtime/managed-sync.ts +114 -105
- package/src/dev-runtime/managed-websocket.ts +11 -7
- package/src/dev-runtime/migrations.ts +2 -2
- package/src/dev-runtime/sync.ts +3 -3
- package/dist/platform/_ignotum/v1.0/client.js +0 -12
- package/dist/runtime/bootstrap-DlNIfH-r.js.map +0 -1
- package/dist/runtime/id-BI08JkmR.d.ts +0 -1
- package/dist/runtime/pagination--f4kMKf6.js.map +0 -1
- package/dist/runtime/pagination-BI08JkmR.d.ts +0 -1
- package/dist/runtime/result-BkziOG1L.d.ts +0 -1
- package/dist/runtime/router-C1eC9yy5.js.map +0 -1
package/src/cli/command.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { NodeHttpClient, NodeRuntime, NodeServices } from "@effect/platform-node";
|
|
2
|
-
import { Effect, Layer, Option, Path, Schema, Terminal } from "effect";
|
|
3
|
-
import { Argument, CliError, Command, Flag } from "effect/
|
|
2
|
+
import { Clock, Effect, Layer, Option, Path, Schema, Terminal } from "effect";
|
|
3
|
+
import { Argument, CliError, Command, Flag } from "effect/cli";
|
|
4
4
|
import { AppSlug } from "@ignotum/contracts/control";
|
|
5
5
|
import { IdGenerator } from "@ignotum/shared/id";
|
|
6
6
|
|
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
status as readAuthStatus,
|
|
14
14
|
} from "./auth-client.js";
|
|
15
15
|
import { ControlClient } from "./control-client.js";
|
|
16
|
-
import { deploy as runDeploy, resumeVersion } from "./deploy.js";
|
|
16
|
+
import { DeployProgress, deploy as runDeploy, resumeVersion } from "./deploy.js";
|
|
17
17
|
import { resetDevDatabase } from "../dev-runtime/dev-database.js";
|
|
18
18
|
import { dev as runDev } from "./dev.js";
|
|
19
19
|
import { createApp } from "./new-app.js";
|
|
@@ -118,9 +118,9 @@ const devDatabase = Command.make("db").pipe(
|
|
|
118
118
|
const dev = Command.make(
|
|
119
119
|
"dev",
|
|
120
120
|
{
|
|
121
|
-
host: Flag.
|
|
122
|
-
open: Flag.
|
|
123
|
-
port: Flag.
|
|
121
|
+
host: Flag.String("host").pipe(Flag.withDefault("127.0.0.1")),
|
|
122
|
+
open: Flag.Boolean("open").pipe(Flag.withDefault(false)),
|
|
123
|
+
port: Flag.Int("port").pipe(Flag.withSchema(DevPort), Flag.withDefault(3210)),
|
|
124
124
|
},
|
|
125
125
|
Effect.fn("dev")(function* ({ host, open, port }) {
|
|
126
126
|
const path = yield* Path.Path;
|
|
@@ -144,10 +144,11 @@ const dev = Command.make(
|
|
|
144
144
|
const deploy = Command.make(
|
|
145
145
|
"deploy",
|
|
146
146
|
{
|
|
147
|
-
resume: Flag.
|
|
147
|
+
resume: Flag.Boolean("resume").pipe(
|
|
148
148
|
Flag.withDescription("Resume the saved upload or activation."),
|
|
149
|
+
Flag.withDefault(false),
|
|
149
150
|
),
|
|
150
|
-
app: Flag.
|
|
151
|
+
app: Flag.String("app").pipe(
|
|
151
152
|
Flag.withDescription("Create or link this app slug on the first deploy."),
|
|
152
153
|
Flag.withSchema(AppSlug),
|
|
153
154
|
Flag.optional,
|
|
@@ -156,8 +157,14 @@ const deploy = Command.make(
|
|
|
156
157
|
Effect.fn("deploy")(function* ({ app, resume }) {
|
|
157
158
|
const path = yield* Path.Path;
|
|
158
159
|
const terminal = yield* Terminal.Terminal;
|
|
160
|
+
const startedAt = yield* Clock.currentTimeMillis;
|
|
161
|
+
const progress = Effect.fn("deploy.progress")(function* (message: string) {
|
|
162
|
+
const elapsed = ((yield* Clock.currentTimeMillis) - startedAt) / 1000;
|
|
163
|
+
yield* terminal.display(`${message} (${elapsed.toFixed(1)}s)\n`).pipe(Effect.orDie);
|
|
164
|
+
});
|
|
159
165
|
if (resume) {
|
|
160
166
|
const result = yield* resumeVersion(path.resolve("."), Option.getOrUndefined(app)).pipe(
|
|
167
|
+
Effect.provideService(DeployProgress, progress),
|
|
161
168
|
// @effect-diagnostics-next-line strictEffectProvide:off This command owns the authenticated control client.
|
|
162
169
|
Effect.provide(ControlClient.layer),
|
|
163
170
|
Effect.mapError((error) =>
|
|
@@ -170,6 +177,7 @@ const deploy = Command.make(
|
|
|
170
177
|
return;
|
|
171
178
|
}
|
|
172
179
|
const result = yield* runDeploy(path.resolve("."), Option.getOrUndefined(app)).pipe(
|
|
180
|
+
Effect.provideService(DeployProgress, progress),
|
|
173
181
|
// @effect-diagnostics-next-line strictEffectProvide:off The deploy subcommand owns this client layer for its full lifetime.
|
|
174
182
|
Effect.provide(ControlClient.layer),
|
|
175
183
|
Effect.mapError((error) =>
|
|
@@ -200,14 +208,14 @@ const install = Command.make(
|
|
|
200
208
|
const newApp = Command.make(
|
|
201
209
|
"new",
|
|
202
210
|
{
|
|
203
|
-
directory: Argument.
|
|
211
|
+
directory: Argument.String("directory").pipe(
|
|
204
212
|
Argument.withDescription('Directory to create. Use "." for the current directory.'),
|
|
205
213
|
),
|
|
206
|
-
git: Flag.
|
|
214
|
+
git: Flag.Boolean("git").pipe(
|
|
207
215
|
Flag.withDescription("Initialize a Git repository. Use --no-git to skip it."),
|
|
208
216
|
Flag.withDefault(true),
|
|
209
217
|
),
|
|
210
|
-
install: Flag.
|
|
218
|
+
install: Flag.Boolean("install").pipe(
|
|
211
219
|
Flag.withDescription("Install dependencies. Use --no-install to skip it."),
|
|
212
220
|
Flag.withDefault(true),
|
|
213
221
|
),
|
|
@@ -36,8 +36,8 @@ import {
|
|
|
36
36
|
Schedule,
|
|
37
37
|
Schema,
|
|
38
38
|
} from "effect";
|
|
39
|
-
import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/
|
|
40
|
-
import type { HttpClientRequest as HttpRequest } from "effect/
|
|
39
|
+
import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/http";
|
|
40
|
+
import type { HttpClientRequest as HttpRequest } from "effect/http/HttpClientRequest";
|
|
41
41
|
import { AuthCommandError, AuthCredentialStore } from "./auth-client.js";
|
|
42
42
|
|
|
43
43
|
export interface HostedControlConfig {
|
package/src/cli/deploy.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
import { Effect, Path, Schema } from "effect";
|
|
1
|
+
import { Context, Effect, Path, Schema } from "effect";
|
|
2
2
|
import type { AppSlug, ControlVersion } from "@ignotum/contracts/control";
|
|
3
3
|
import { VersionId, VersionNumber } from "@ignotum/contracts/runtime/identity";
|
|
4
4
|
import { IdGenerator } from "@ignotum/shared/id";
|
|
5
5
|
import { appUrl, configureApp, type AppConfiguration } from "./app-configuration.js";
|
|
6
6
|
import { buildManagedVersion, type ManagedVersionBuildResult } from "./build/managed.js";
|
|
7
|
+
import { generateClientRoutes } from "./route-codegen.js";
|
|
8
|
+
import { checkAppTypes } from "./typecheck.js";
|
|
7
9
|
import {
|
|
8
10
|
ControlClient,
|
|
9
11
|
ControlClientUnavailable,
|
|
@@ -13,6 +15,11 @@ import {
|
|
|
13
15
|
|
|
14
16
|
import { saveUpload, loadUpload, restoreUploadBuild, clearUpload } from "./upload-journal.js";
|
|
15
17
|
|
|
18
|
+
export const DeployProgress = Context.Reference<(message: string) => Effect.Effect<void>>(
|
|
19
|
+
"ignotum/cli/deploy/DeployProgress",
|
|
20
|
+
{ defaultValue: () => () => Effect.void },
|
|
21
|
+
);
|
|
22
|
+
|
|
16
23
|
export class VersionUploadInterrupted extends Schema.TaggedError<VersionUploadInterrupted>()(
|
|
17
24
|
"VersionUploadInterrupted",
|
|
18
25
|
{ cause: Schema.Defect(), versionId: VersionId, message: Schema.String },
|
|
@@ -65,7 +72,9 @@ export const uploadVersion = Effect.fn("Deploy.upload")(function* (
|
|
|
65
72
|
savedVersionId?: VersionId,
|
|
66
73
|
) {
|
|
67
74
|
const control = yield* ControlClient;
|
|
75
|
+
const progress = yield* DeployProgress;
|
|
68
76
|
const versionId = savedVersionId ?? (yield* (yield* IdGenerator).generate(VersionId));
|
|
77
|
+
yield* progress("Preparing upload");
|
|
69
78
|
const admission = yield* control
|
|
70
79
|
.beginVersion(configuration.appId, versionId, build.declaration)
|
|
71
80
|
.pipe(
|
|
@@ -112,9 +121,13 @@ export const uploadVersion = Effect.fn("Deploy.upload")(function* (
|
|
|
112
121
|
}
|
|
113
122
|
return Effect.succeed({ ...target, bytes } satisfies VersionUploadFile);
|
|
114
123
|
});
|
|
124
|
+
yield* progress(`Uploading 0/${files.length} files`);
|
|
115
125
|
yield* Effect.forEach(
|
|
116
126
|
files,
|
|
117
|
-
(file) =>
|
|
127
|
+
(file, index) =>
|
|
128
|
+
control
|
|
129
|
+
.uploadVersionFile(configuration.appId, versionId, file)
|
|
130
|
+
.pipe(Effect.andThen(() => progress(`Uploading ${index + 1}/${files.length} files`))),
|
|
118
131
|
// The hosted control plane runs within Cloudflare's per-request CPU budget.
|
|
119
132
|
// Serialize uploads so several validation-heavy requests cannot contend in
|
|
120
133
|
// the same isolate and exhaust that budget together.
|
|
@@ -129,6 +142,7 @@ export const uploadVersion = Effect.fn("Deploy.upload")(function* (
|
|
|
129
142
|
}),
|
|
130
143
|
}),
|
|
131
144
|
);
|
|
145
|
+
yield* progress("Validating version");
|
|
132
146
|
const version = yield* control.finalizeVersion(configuration.appId, versionId).pipe(
|
|
133
147
|
Effect.catchTags({
|
|
134
148
|
ControlClientUnavailable: (cause) =>
|
|
@@ -140,6 +154,7 @@ export const uploadVersion = Effect.fn("Deploy.upload")(function* (
|
|
|
140
154
|
}),
|
|
141
155
|
Effect.flatMap(readyVersion),
|
|
142
156
|
);
|
|
157
|
+
yield* progress(`Activating Version ${version.number}`);
|
|
143
158
|
const selection = yield* control
|
|
144
159
|
.activateVersion(configuration.appId, version.number)
|
|
145
160
|
.pipe(Effect.mapError((cause) => activationError(version, cause)));
|
|
@@ -159,7 +174,14 @@ export const deploy = Effect.fn("Deploy.run")(function* (
|
|
|
159
174
|
appDirectory: string,
|
|
160
175
|
requestedSlug: AppSlug | undefined,
|
|
161
176
|
) {
|
|
177
|
+
const progress = yield* DeployProgress;
|
|
178
|
+
yield* progress("Generating bindings");
|
|
179
|
+
yield* generateClientRoutes(appDirectory);
|
|
180
|
+
yield* progress("Checking TypeScript");
|
|
181
|
+
yield* checkAppTypes(appDirectory);
|
|
182
|
+
yield* progress("Building app");
|
|
162
183
|
const build = yield* buildVersionArtifact(appDirectory);
|
|
184
|
+
yield* progress("Linking app");
|
|
163
185
|
const configuration = yield* configureApp(appDirectory, requestedSlug);
|
|
164
186
|
const versionId = yield* (yield* IdGenerator).generate(VersionId);
|
|
165
187
|
yield* saveUpload(appDirectory, configuration, versionId, build);
|
|
@@ -172,6 +194,8 @@ export const resumeVersion = Effect.fn("Deploy.resume")(function* (
|
|
|
172
194
|
appDirectory: string,
|
|
173
195
|
requestedSlug?: AppSlug,
|
|
174
196
|
) {
|
|
197
|
+
const progress = yield* DeployProgress;
|
|
198
|
+
yield* progress("Restoring saved deployment");
|
|
175
199
|
const configuration = yield* configureApp(appDirectory, requestedSlug);
|
|
176
200
|
const control = yield* ControlClient;
|
|
177
201
|
const journal = yield* loadUpload(appDirectory, configuration);
|
|
@@ -189,6 +213,7 @@ export const resumeVersion = Effect.fn("Deploy.resume")(function* (
|
|
|
189
213
|
return { ...activated, appUrl: appUrl(configuration.slug) };
|
|
190
214
|
}
|
|
191
215
|
const version = yield* readyVersion(existing);
|
|
216
|
+
yield* progress(`Activating Version ${version.number}`);
|
|
192
217
|
const selection = yield* control
|
|
193
218
|
.activateVersion(configuration.appId, version.number)
|
|
194
219
|
.pipe(Effect.mapError((cause) => activationError(version, cause)));
|
package/src/cli/dev.ts
CHANGED
|
@@ -3,7 +3,7 @@ import prefresh from "@prefresh/vite";
|
|
|
3
3
|
import tailwindcss from "@tailwindcss/vite";
|
|
4
4
|
import { Crypto, Effect, FileSystem, Option, Path, Schema, Scope } from "effect";
|
|
5
5
|
import { createServer, normalizePath, type Plugin, type ViteDevServer } from "vite";
|
|
6
|
-
import { ChildProcessSpawner } from "effect/
|
|
6
|
+
import { ChildProcessSpawner } from "effect/process";
|
|
7
7
|
import {
|
|
8
8
|
PlatformRequirement,
|
|
9
9
|
revisionPrefix,
|
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { fileURLToPath } from "node:url";
|
|
3
3
|
import { Effect, FileSystem, Path, Schema } from "effect";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
revisionPrefix,
|
|
6
|
+
ServingSelection,
|
|
7
|
+
type PlatformRevision,
|
|
8
|
+
} from "@ignotum/contracts/deployment";
|
|
5
9
|
import { VersionId } from "@ignotum/contracts/runtime/identity";
|
|
6
10
|
import { bundledGuests } from "@ignotum/platform/guests";
|
|
7
11
|
import type { BundledGuest } from "@ignotum/platform/artifacts";
|
|
8
12
|
import { compilerExports } from "@ignotum/platform/compiler-exports";
|
|
9
13
|
import { platformImportsPlugin } from "@ignotum/platform/compiler";
|
|
14
|
+
import { revisions } from "@ignotum/platform/registry";
|
|
10
15
|
import { runViteBuild } from "./build/artifact.js";
|
|
11
16
|
|
|
12
17
|
export interface LocalPlatform {
|
|
@@ -15,9 +20,11 @@ export interface LocalPlatform {
|
|
|
15
20
|
readonly guest: BundledGuest;
|
|
16
21
|
}
|
|
17
22
|
|
|
23
|
+
const localPlatformRevision = revisions[0];
|
|
24
|
+
|
|
18
25
|
export const loadLocalPlatform = Effect.fn("ManagedDev.loadPlatform")(function* () {
|
|
19
26
|
const fs = yield* FileSystem.FileSystem;
|
|
20
|
-
const revision =
|
|
27
|
+
const revision = localPlatformRevision;
|
|
21
28
|
const guest = bundledGuests.find(
|
|
22
29
|
(candidate) =>
|
|
23
30
|
candidate.manifest.revision.major === revision.major &&
|
|
@@ -25,7 +32,9 @@ export const loadLocalPlatform = Effect.fn("ManagedDev.loadPlatform")(function*
|
|
|
25
32
|
);
|
|
26
33
|
if (guest === undefined) return yield* Effect.die("The local platform guest is missing.");
|
|
27
34
|
const browser = yield* fs.readFile(
|
|
28
|
-
fileURLToPath(
|
|
35
|
+
fileURLToPath(
|
|
36
|
+
new URL(`../../dist/platform${revisionPrefix(revision)}/client.js`, import.meta.url),
|
|
37
|
+
),
|
|
29
38
|
);
|
|
30
39
|
return { revision, browser, guest } satisfies LocalPlatform;
|
|
31
40
|
});
|
|
@@ -45,7 +54,7 @@ export const localServingSelection = (
|
|
|
45
54
|
formatVersion: 1,
|
|
46
55
|
appId: `app_${digest}`,
|
|
47
56
|
versionNumber: 1,
|
|
48
|
-
platform:
|
|
57
|
+
platform: localPlatformRevision,
|
|
49
58
|
activationGeneration,
|
|
50
59
|
deploymentEpoch,
|
|
51
60
|
dataHistoryId: `hist_${digest}`,
|
|
@@ -71,7 +71,7 @@ export const moduleBoundariesPlugin = Effect.fn("Deploy.moduleBoundariesPlugin")
|
|
|
71
71
|
const fileSystem = yield* FileSystem.FileSystem;
|
|
72
72
|
const runPromise = Effect.runPromiseWith(yield* Effect.context<never>());
|
|
73
73
|
const isolatedDiscovery =
|
|
74
|
-
(yield* Config.
|
|
74
|
+
(yield* Config.String("IGNOTUM_ISOLATED_DISCOVERY").pipe(Config.withDefault("0"))) === "1";
|
|
75
75
|
const realAppDirectory = isolatedDiscovery
|
|
76
76
|
? normalizePath(cleanModuleId(appDirectory))
|
|
77
77
|
: normalizePath(yield* fileSystem.realPath(cleanModuleId(appDirectory)));
|
package/src/cli/new-app.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Effect, FileSystem, Path, Schema, String } from "effect";
|
|
2
|
-
import { ChildProcess, ChildProcessSpawner } from "effect/
|
|
2
|
+
import { ChildProcess, ChildProcessSpawner } from "effect/process";
|
|
3
3
|
import cliPackageJson from "../../package.json" with { type: "json" };
|
|
4
4
|
|
|
5
5
|
import { agentAppFiles, type AppFile } from "./agent-files.js";
|
|
@@ -145,7 +145,7 @@ const icon = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
|
|
|
145
145
|
const utils = `export const counterIncrement = 1;
|
|
146
146
|
`;
|
|
147
147
|
|
|
148
|
-
const home = `import { Result, useMutation, useQuery } from "ignotum/client";
|
|
148
|
+
const home = `import { Result, useMutation, useQuery, useState } from "ignotum/client";
|
|
149
149
|
import { api } from "@/_generated/api.js";
|
|
150
150
|
import { route } from "@/_generated/client.js";
|
|
151
151
|
import { counterIncrement } from "@/shared/utils.js";
|
|
@@ -155,6 +155,24 @@ export default route({
|
|
|
155
155
|
component: function Counter() {
|
|
156
156
|
const count = useQuery(api.counter.get);
|
|
157
157
|
const increment = useMutation(api.counter.increment);
|
|
158
|
+
const [busy, setBusy] = useState(false);
|
|
159
|
+
const [error, setError] = useState("");
|
|
160
|
+
const incrementCounter = async () => {
|
|
161
|
+
if (busy) return;
|
|
162
|
+
setBusy(true);
|
|
163
|
+
setError("");
|
|
164
|
+
try {
|
|
165
|
+
Result.match(await increment(), {
|
|
166
|
+
value: () => {},
|
|
167
|
+
error: (failure) => setError("Increment failed: " + failure._tag),
|
|
168
|
+
internalError: (failure) => setError("Increment failed. Request ID: " + failure.requestId),
|
|
169
|
+
});
|
|
170
|
+
} catch {
|
|
171
|
+
setError("The increment could not be confirmed. Check the current counter before trying again.");
|
|
172
|
+
} finally {
|
|
173
|
+
setBusy(false);
|
|
174
|
+
}
|
|
175
|
+
};
|
|
158
176
|
|
|
159
177
|
return (
|
|
160
178
|
<section class="text-center">
|
|
@@ -165,15 +183,20 @@ export default route({
|
|
|
165
183
|
<>
|
|
166
184
|
<p class="my-6 text-5xl tabular-nums">{value}</p>
|
|
167
185
|
<button
|
|
168
|
-
class="rounded bg-zinc-900 px-4 py-2 text-white"
|
|
186
|
+
class="rounded bg-zinc-900 px-4 py-2 text-white disabled:opacity-50"
|
|
169
187
|
type="button"
|
|
170
|
-
|
|
188
|
+
disabled={busy}
|
|
189
|
+
onClick={() => void incrementCounter()}
|
|
171
190
|
>
|
|
172
|
-
Increment by
|
|
191
|
+
{busy ? "Incrementing..." : "Increment by " + counterIncrement}
|
|
173
192
|
</button>
|
|
174
193
|
</>
|
|
175
194
|
),
|
|
195
|
+
internalError: (failure) => (
|
|
196
|
+
<p role="alert" class="mt-6">Counter could not be loaded. Request ID: {failure.requestId}</p>
|
|
197
|
+
),
|
|
176
198
|
})}
|
|
199
|
+
{error !== "" && <p role="alert" class="mt-4">{error}</p>}
|
|
177
200
|
</section>
|
|
178
201
|
);
|
|
179
202
|
},
|
|
@@ -254,7 +277,9 @@ After generating bindings, run \`npx tsc --noEmit\` to check the app.
|
|
|
254
277
|
|
|
255
278
|
## Deploy
|
|
256
279
|
|
|
257
|
-
Sign in with \`npx ignotum auth login\`, then run \`npx ignotum deploy
|
|
280
|
+
Sign in with \`npx ignotum auth login\`, then run \`npx ignotum deploy\`.
|
|
281
|
+
The first deploy tries the app name saved when this project was created. Use
|
|
282
|
+
\`npx ignotum deploy --app another-slug\` to choose a different slug.
|
|
258
283
|
Later deployments use \`npx ignotum deploy\`. The command prints the hosted URL and confirms
|
|
259
284
|
that the numbered App Version is live. If activation cannot be confirmed, follow the printed
|
|
260
285
|
\`ignotum deploy --resume\` command to finish without creating another version.
|
|
@@ -278,6 +303,10 @@ export const appFiles = (appName: string): ReadonlyArray<AppFile> => [
|
|
|
278
303
|
{ content: schema, path: "server/schema.ts" },
|
|
279
304
|
{ content: utils, path: "shared/utils.ts" },
|
|
280
305
|
{ content: packageJson(appName), path: "package.json" },
|
|
306
|
+
{
|
|
307
|
+
content: `${JSON.stringify({ formatVersion: 1, suggestedSlug: appName }, null, 2)}\n`,
|
|
308
|
+
path: "ignotum.json",
|
|
309
|
+
},
|
|
281
310
|
{
|
|
282
311
|
content: `minimumReleaseAgeExclude:\n - ignotum@${cliPackageJson.version}\nallowBuilds:\n msgpackr-extract: false\n`,
|
|
283
312
|
path: "pnpm-workspace.yaml",
|
package/src/cli/path.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Path } from "effect";
|
|
2
|
+
|
|
3
|
+
export const PathContainment = {
|
|
4
|
+
contains(path: Path.Path, directory: string, child: string): boolean {
|
|
5
|
+
const relative = path.relative(directory, child);
|
|
6
|
+
return (
|
|
7
|
+
relative === "" ||
|
|
8
|
+
(!path.isAbsolute(relative) && relative !== ".." && !relative.startsWith(`..${path.sep}`))
|
|
9
|
+
);
|
|
10
|
+
},
|
|
11
|
+
};
|
package/src/cli/route-codegen.ts
CHANGED
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
writeOutput,
|
|
16
16
|
} from "./codegen.js";
|
|
17
17
|
import { createRouteExtractor, InvalidRouteSource, sourceError } from "./route-static.js";
|
|
18
|
+
import { PathContainment } from "./path.js";
|
|
18
19
|
|
|
19
20
|
export interface DiscoveredClientModule {
|
|
20
21
|
readonly path: string;
|
|
@@ -54,7 +55,7 @@ export const discoverClientRoutes = Effect.fn("Routes.discoverClientRoutes")(fun
|
|
|
54
55
|
const real = yield* fs
|
|
55
56
|
.realPath(directory)
|
|
56
57
|
.pipe(Effect.mapError(() => diagnostic(directory, "Cannot read route directory.")));
|
|
57
|
-
if (
|
|
58
|
+
if (!PathContainment.contains(path, clientDirectory, real))
|
|
58
59
|
return yield* diagnostic(directory, "Route trees cannot escape client/ through symlinks.");
|
|
59
60
|
if (seen.has(real)) return yield* diagnostic(directory, "Repeated or cyclic route directory.");
|
|
60
61
|
seen.add(real);
|
package/src/cli/route-static.ts
CHANGED
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
StaticMetadata,
|
|
6
6
|
UrlValueDescriptor,
|
|
7
7
|
} from "@ignotum/contracts/app-definition";
|
|
8
|
+
import { PathContainment } from "./path.js";
|
|
8
9
|
|
|
9
10
|
export class InvalidRouteSource extends Schema.TaggedError<InvalidRouteSource>()(
|
|
10
11
|
"InvalidRouteSource",
|
|
@@ -126,8 +127,8 @@ export const createRouteExtractor = Effect.fn("Routes.createExtractor")(function
|
|
|
126
127
|
.realPath(file)
|
|
127
128
|
.pipe(Effect.mapError(() => fail("Cannot read source module.")));
|
|
128
129
|
if (
|
|
129
|
-
!
|
|
130
|
-
|
|
130
|
+
!PathContainment.contains(path, appDirectory, real) ||
|
|
131
|
+
PathContainment.contains(path, path.join(appDirectory, "server"), real) ||
|
|
131
132
|
!/\.(ts|tsx)$/.test(real) ||
|
|
132
133
|
real.endsWith(".d.ts")
|
|
133
134
|
)
|
|
@@ -588,7 +589,9 @@ export const createRouteExtractor = Effect.fn("Routes.createExtractor")(function
|
|
|
588
589
|
`Unsupported URL validator '${method}' or argument count.`,
|
|
589
590
|
);
|
|
590
591
|
}
|
|
591
|
-
return yield* Schema.decodeUnknownEffect(UrlValueDescriptor
|
|
592
|
+
return yield* Schema.decodeUnknownEffect(UrlValueDescriptor, {
|
|
593
|
+
onExcessProperty: "error",
|
|
594
|
+
})(output).pipe(
|
|
592
595
|
Effect.mapError((error) =>
|
|
593
596
|
sourceError(module, node, field, `Invalid ${method} URL descriptor: ${String(error)}`),
|
|
594
597
|
),
|
|
@@ -659,7 +662,9 @@ export const createRouteExtractor = Effect.fn("Routes.createExtractor")(function
|
|
|
659
662
|
const extracted = yield* value(expression, name);
|
|
660
663
|
const validate =
|
|
661
664
|
name === "meta"
|
|
662
|
-
? Schema.decodeUnknownEffect(StaticMetadata.fields.meta
|
|
665
|
+
? Schema.decodeUnknownEffect(StaticMetadata.fields.meta, {
|
|
666
|
+
onExcessProperty: "error",
|
|
667
|
+
})(extracted).pipe(Effect.asVoid)
|
|
663
668
|
: name === "titleTemplate"
|
|
664
669
|
? Schema.decodeUnknownEffect(LayoutMetadata.fields.titleTemplate)(extracted).pipe(
|
|
665
670
|
Effect.asVoid,
|
|
@@ -676,6 +681,7 @@ export const createRouteExtractor = Effect.fn("Routes.createExtractor")(function
|
|
|
676
681
|
}
|
|
677
682
|
const decodedMetadata = yield* Schema.decodeUnknownEffect(
|
|
678
683
|
kind === "route" ? StaticMetadata : LayoutMetadata,
|
|
684
|
+
{ onExcessProperty: "error" },
|
|
679
685
|
)(Object.fromEntries(metadata)).pipe(
|
|
680
686
|
Effect.mapError((error) => sourceError(module, call, "metadata", String(error))),
|
|
681
687
|
);
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import process from "node:process";
|
|
3
|
+
import { Effect, FileSystem, Path, Schema } from "effect";
|
|
4
|
+
import { ChildProcess, ChildProcessSpawner } from "effect/process";
|
|
5
|
+
|
|
6
|
+
const CompilerPackage = Schema.Struct({ bin: Schema.Struct({ tsc: Schema.String }) });
|
|
7
|
+
|
|
8
|
+
export class AppTypecheckFailed extends Schema.TaggedError<AppTypecheckFailed>()(
|
|
9
|
+
"AppTypecheckFailed",
|
|
10
|
+
{ message: Schema.String, cause: Schema.optional(Schema.Defect()) },
|
|
11
|
+
) {}
|
|
12
|
+
|
|
13
|
+
export const checkAppTypes = Effect.fn("Typecheck.checkAppTypes")(function* (appDirectory: string) {
|
|
14
|
+
const fs = yield* FileSystem.FileSystem;
|
|
15
|
+
const path = yield* Path.Path;
|
|
16
|
+
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
|
|
17
|
+
const configuration = path.join(appDirectory, "tsconfig.json");
|
|
18
|
+
if (!(yield* fs.exists(configuration))) {
|
|
19
|
+
return yield* AppTypecheckFailed.make({
|
|
20
|
+
message: "Deployment requires a tsconfig.json in the app directory.",
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
const packagePath = yield* Effect.try({
|
|
24
|
+
try: () =>
|
|
25
|
+
createRequire(path.join(appDirectory, "package.json")).resolve("typescript/package.json"),
|
|
26
|
+
catch: (cause) =>
|
|
27
|
+
AppTypecheckFailed.make({
|
|
28
|
+
cause,
|
|
29
|
+
message:
|
|
30
|
+
"TypeScript is not installed for this app. Install TypeScript as a dev dependency, then deploy again.",
|
|
31
|
+
}),
|
|
32
|
+
});
|
|
33
|
+
const compiler = yield* fs.readFileString(packagePath).pipe(
|
|
34
|
+
Effect.flatMap(Schema.decodeEffect(Schema.fromJsonString(CompilerPackage))),
|
|
35
|
+
Effect.mapError((cause) =>
|
|
36
|
+
AppTypecheckFailed.make({
|
|
37
|
+
cause,
|
|
38
|
+
message: "Could not read the installed TypeScript compiler.",
|
|
39
|
+
}),
|
|
40
|
+
),
|
|
41
|
+
);
|
|
42
|
+
const exitCode = yield* spawner
|
|
43
|
+
.exitCode(
|
|
44
|
+
ChildProcess.make(
|
|
45
|
+
process.execPath,
|
|
46
|
+
[
|
|
47
|
+
path.resolve(path.dirname(packagePath), compiler.bin.tsc),
|
|
48
|
+
"--project",
|
|
49
|
+
configuration,
|
|
50
|
+
"--noEmit",
|
|
51
|
+
],
|
|
52
|
+
{ cwd: appDirectory, stdin: "ignore", stdout: "inherit", stderr: "inherit" },
|
|
53
|
+
),
|
|
54
|
+
)
|
|
55
|
+
.pipe(
|
|
56
|
+
Effect.mapError((cause) =>
|
|
57
|
+
AppTypecheckFailed.make({ cause, message: "Could not run the app's TypeScript compiler." }),
|
|
58
|
+
),
|
|
59
|
+
);
|
|
60
|
+
if (exitCode !== ChildProcessSpawner.ExitCode(0)) {
|
|
61
|
+
return yield* AppTypecheckFailed.make({
|
|
62
|
+
message:
|
|
63
|
+
"TypeScript checking failed. Fix the errors above before deploying. No version was uploaded.",
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
});
|
|
@@ -87,9 +87,9 @@ export const restoreUploadBuild = Effect.fn("UploadJournal.restoreBuild")(functi
|
|
|
87
87
|
const outputDirectory = path.join(appDirectory, ".ignotum/build");
|
|
88
88
|
const declarationBytes = yield* fs.readFile(path.join(outputDirectory, "declaration.json"));
|
|
89
89
|
if ((yield* sha256(declarationBytes)) !== journal.declarationSha256) return yield* invalid();
|
|
90
|
-
const declaration = yield* Schema.decodeEffect(Schema.fromJsonString(UploadDeclaration)
|
|
91
|
-
|
|
92
|
-
).pipe(Effect.mapError(invalid));
|
|
90
|
+
const declaration = yield* Schema.decodeEffect(Schema.fromJsonString(UploadDeclaration), {
|
|
91
|
+
onExcessProperty: "error",
|
|
92
|
+
})(new TextDecoder().decode(declarationBytes)).pipe(Effect.mapError(invalid));
|
|
93
93
|
const entries = yield* Effect.forEach(
|
|
94
94
|
declaration.definition.artifacts,
|
|
95
95
|
Effect.fn(function* (artifact) {
|
package/src/dev-runtime/files.ts
CHANGED
|
@@ -15,7 +15,7 @@ import { fileGrantUrlPrefix, fileUploadUrlPrefix } from "@ignotum/contracts/runt
|
|
|
15
15
|
import type { InvocationId } from "@ignotum/contracts/runtime/identity";
|
|
16
16
|
import { IdGenerator } from "@ignotum/shared/id";
|
|
17
17
|
import { Context, Effect, FileSystem, Layer, Option, Path, Schema } from "effect";
|
|
18
|
-
import { SqlClient } from "effect/
|
|
18
|
+
import { SqlClient } from "effect/sql";
|
|
19
19
|
|
|
20
20
|
interface Preparation {
|
|
21
21
|
readonly function: FunctionAddress;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { Base64 } from "effect/encoding";
|
|
1
2
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
2
|
-
import { Crypto, Effect,
|
|
3
|
+
import { Crypto, Effect, FileSystem, Path, Predicate, Schema } from "effect";
|
|
3
4
|
import type { ViteDevServer } from "vite";
|
|
4
5
|
import { parseSync } from "oxc-parser";
|
|
5
6
|
import { prepareManagedExecution, type ManagedExecution } from "@ignotum/runtime/quickjs";
|
|
@@ -165,7 +166,7 @@ export const createManagedFunctionLoader = Effect.fn("ManagedFunctions.createLoa
|
|
|
165
166
|
}
|
|
166
167
|
if (module.sourceMap !== undefined) {
|
|
167
168
|
rewritten = rewritten.replace(/^\/\/# sourceMappingURL=.*$/gm, "");
|
|
168
|
-
rewritten += `\n//# sourceMappingURL=data:application/json;base64,${
|
|
169
|
+
rewritten += `\n//# sourceMappingURL=data:application/json;base64,${Base64.encode(module.sourceMap.bytes)}\n`;
|
|
169
170
|
}
|
|
170
171
|
yield* fs.writeFileString(fileURLToPath(urls.get(module.id)!), rewritten);
|
|
171
172
|
}
|