ignotum 0.0.13 → 0.0.15
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 +6 -6
- package/dist/cli/bin.mjs +6111 -4448
- package/dist/cli/bin.mjs.map +1 -1
- package/dist/cli/emscripten-module-D5GzfUNv.mjs +533 -0
- package/dist/cli/emscripten-module-D5GzfUNv.mjs.map +1 -0
- package/dist/cli/ffi-7IElU4Co.mjs +148 -0
- package/dist/cli/ffi-7IElU4Co.mjs.map +1 -0
- package/dist/cli/managed-release-WHYLZwaS.mjs +10902 -0
- package/dist/cli/managed-release-WHYLZwaS.mjs.map +1 -0
- package/dist/cli/managed-release.d.mts +1 -0
- package/dist/cli/managed-release.mjs +14 -0
- package/dist/cli/managed-release.mjs.map +1 -0
- package/dist/cli/module-ES6BEMUI-D36I4NVn.mjs +1289 -0
- package/dist/cli/module-ES6BEMUI-D36I4NVn.mjs.map +1 -0
- package/dist/runtime/{api-Cv3hMbzo.d.ts → api-B4fwp5a2.d.ts} +4 -4
- package/dist/runtime/bootstrap-BISQSn_O.js +916 -0
- package/dist/runtime/bootstrap-BISQSn_O.js.map +1 -0
- package/dist/runtime/bootstrap-BTFScKAc.d.ts +545 -0
- package/dist/runtime/client.d.ts +50 -15
- package/dist/runtime/client.js +7 -1138
- package/dist/runtime/client.js.map +1 -1
- package/dist/runtime/descriptor-CTY4mtwS-cS43EbwS.js +269 -0
- package/dist/runtime/descriptor-CTY4mtwS-cS43EbwS.js.map +1 -0
- package/dist/runtime/{id-Bt9XWRGL.js → id--1m0NuUL.js} +4 -4
- package/dist/runtime/id--1m0NuUL.js.map +1 -0
- package/dist/runtime/{id-BzFHf3Wo-DGPCjgrf.d.ts → id-7an3nxTu-DZJSM5xh.d.ts} +2 -2
- package/dist/runtime/id-CBOt2kDo.d.ts +1 -0
- package/dist/runtime/identity-QjtJRxBD.d.ts +2 -0
- package/dist/runtime/image-Djtjy4Z8.js +19 -0
- package/dist/runtime/image-Djtjy4Z8.js.map +1 -0
- package/dist/runtime/index-D4seNHlX.d.ts +184 -0
- package/dist/runtime/internal/api.d.ts +1 -1
- package/dist/runtime/internal/api.js +30 -1
- package/dist/runtime/internal/api.js.map +1 -0
- package/dist/runtime/internal/bootstrap.d.ts +2 -0
- package/dist/runtime/internal/bootstrap.js +2 -0
- package/dist/runtime/internal/client.d.ts +2 -0
- package/dist/runtime/internal/client.js +75 -0
- package/dist/runtime/internal/client.js.map +1 -0
- package/dist/runtime/internal/host.d.ts +25 -8
- package/dist/runtime/internal/host.js +27 -6
- package/dist/runtime/internal/host.js.map +1 -1
- package/dist/runtime/internal/routes.d.ts +2 -0
- package/dist/runtime/internal/routes.js +2 -0
- 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 -1
- package/dist/runtime/internal/types.js +1 -1
- package/dist/runtime/pagination-CBOt2kDo.d.ts +1 -0
- package/dist/runtime/pagination-D3qd6s9N.js +33 -0
- package/dist/runtime/pagination-D3qd6s9N.js.map +1 -0
- package/dist/runtime/result-BkziOG1L.d.ts +1 -0
- package/dist/runtime/router-BSBI1oN1.js +2377 -0
- package/dist/runtime/router-BSBI1oN1.js.map +1 -0
- package/dist/runtime/routes-Ce8TVP-T.js +690 -0
- package/dist/runtime/routes-Ce8TVP-T.js.map +1 -0
- package/dist/runtime/{schema-1Zs03-iS.js → schema-D1wOqiNH.js} +40 -12
- package/dist/runtime/schema-D1wOqiNH.js.map +1 -0
- package/dist/runtime/server.d.ts +5 -5
- package/dist/runtime/server.js +2 -2
- package/dist/runtime/{sync-Bs8J3fIr.d.ts → sync-D-GK2sv9.d.ts} +3 -2
- package/dist/runtime/{api-CAgKDij7.js → sync-DzUwA8W9.js} +22 -41
- package/dist/runtime/sync-DzUwA8W9.js.map +1 -0
- package/dist/runtime/types-DeCCyqLU-BIm7rU-0.d.ts +341 -0
- package/package.json +29 -4
- package/src/cli/agent-files.ts +14 -8
- package/src/cli/build/managed-client.ts +411 -0
- package/src/cli/build/managed-server.ts +294 -0
- package/src/cli/build/managed.ts +67 -0
- package/src/cli/build/output.ts +47 -0
- package/src/cli/build/server.ts +51 -220
- package/src/cli/client-config.ts +1 -140
- package/src/cli/client-styles.ts +1 -0
- package/src/cli/codegen.ts +9 -4
- package/src/cli/command.ts +28 -5
- package/src/cli/control-client.ts +84 -101
- package/src/cli/deploy.ts +133 -177
- package/src/cli/dev.ts +506 -107
- package/src/cli/image-assets.ts +499 -0
- package/src/cli/managed-client-boundaries.ts +103 -0
- package/src/cli/managed-dev-platform.ts +118 -0
- package/src/cli/managed-exports.ts +219 -0
- package/src/cli/managed-release-bin.ts +13 -0
- package/src/cli/managed-release.ts +260 -0
- package/src/cli/module-boundaries.ts +11 -1
- package/src/cli/new-app.ts +111 -87
- package/src/cli/route-codegen.ts +311 -0
- package/src/cli/route-static.ts +710 -0
- package/src/client/bootstrap.ts +110 -0
- package/src/client/errors.ts +13 -7
- package/src/client/files.ts +64 -0
- package/src/client/id.ts +9 -5
- package/src/client/image.ts +28 -0
- package/src/client/index.ts +25 -2
- package/src/client/managed-client.ts +729 -0
- package/src/client/managed-upgrade.ts +283 -0
- package/src/client/recovery-journal.ts +195 -0
- package/src/client/route-boundaries.ts +184 -0
- package/src/client/router-history.ts +152 -0
- package/src/client/router-store.ts +644 -0
- package/src/client/router.ts +324 -0
- package/src/client/routes.ts +28 -0
- package/src/client/sync.ts +66 -592
- package/src/dev-runtime/database.ts +77 -2
- package/src/dev-runtime/functions.ts +3 -3
- package/src/dev-runtime/id.ts +19 -4
- package/src/dev-runtime/managed-functions.ts +255 -0
- package/src/dev-runtime/managed-sync.ts +263 -0
- package/src/dev-runtime/managed-websocket.ts +71 -0
- package/src/dev-runtime/migrations.ts +20 -0
- package/src/dev-runtime/sync.ts +236 -236
- package/src/image-assets.d.ts +36 -0
- package/src/internal/client.ts +186 -0
- package/src/internal/host.ts +1 -1
- package/src/internal/http-paths.ts +0 -3
- package/src/internal/routes.ts +45 -0
- package/dist/runtime/api-CAgKDij7.js.map +0 -1
- package/dist/runtime/descriptor-Cyo9FP9n-C0SRVXNW.js +0 -154
- package/dist/runtime/descriptor-Cyo9FP9n-C0SRVXNW.js.map +0 -1
- package/dist/runtime/id-Bt9XWRGL.js.map +0 -1
- package/dist/runtime/id-Dz0apuB3.d.ts +0 -1
- package/dist/runtime/index-D54flWtH.d.ts +0 -402
- package/dist/runtime/pagination-DnKg3dkI-r5ZUxBBx.d.ts +0 -112
- package/dist/runtime/pagination-Dz0apuB3.d.ts +0 -1
- package/dist/runtime/result-DKAA4gpS.d.ts +0 -1
- package/dist/runtime/schema-1Zs03-iS.js.map +0 -1
- package/src/cli/build/client.ts +0 -119
- package/src/cli/build/public.ts +0 -186
- package/src/cli/client-entry.ts +0 -152
- package/src/cli/client-plugin.ts +0 -136
- package/src/client/app.ts +0 -15
package/src/cli/new-app.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { Effect, FileSystem, Path, Schema, String } from "effect";
|
|
|
2
2
|
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";
|
|
3
3
|
|
|
4
4
|
import { agentAppFiles, type AppFile } from "./agent-files.js";
|
|
5
|
-
import {
|
|
5
|
+
import { generateClientRoutes } from "./route-codegen.js";
|
|
6
6
|
import { installDependencies, type PackageManager } from "./package-manager.js";
|
|
7
7
|
|
|
8
8
|
export interface NewAppOptions {
|
|
@@ -135,43 +135,6 @@ export const increment = mutation({
|
|
|
135
135
|
});
|
|
136
136
|
`;
|
|
137
137
|
|
|
138
|
-
const app = `import { app, Result, useMutation, useQuery } from "ignotum/client";
|
|
139
|
-
|
|
140
|
-
import { api } from "@/_generated/api.js";
|
|
141
|
-
import { counterIncrement } from "@/shared/utils.js";
|
|
142
|
-
|
|
143
|
-
function App() {
|
|
144
|
-
const count = useQuery(api.counter.get);
|
|
145
|
-
const increment = useMutation(api.counter.increment);
|
|
146
|
-
|
|
147
|
-
return (
|
|
148
|
-
<main class="mx-auto max-w-sm px-6 py-20 text-center">
|
|
149
|
-
<h1 class="text-2xl font-semibold">Counter</h1>
|
|
150
|
-
{Result.match(count, {
|
|
151
|
-
pending: () => <p class="mt-6">Loading...</p>,
|
|
152
|
-
value: (value) => (
|
|
153
|
-
<>
|
|
154
|
-
<p class="my-6 text-5xl tabular-nums">{value}</p>
|
|
155
|
-
<button
|
|
156
|
-
class="rounded bg-zinc-900 px-4 py-2 text-white"
|
|
157
|
-
type="button"
|
|
158
|
-
onClick={() => void increment()}
|
|
159
|
-
>
|
|
160
|
-
Increment by {counterIncrement}
|
|
161
|
-
</button>
|
|
162
|
-
</>
|
|
163
|
-
),
|
|
164
|
-
})}
|
|
165
|
-
</main>
|
|
166
|
-
);
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
export default app({
|
|
170
|
-
title: "Counter",
|
|
171
|
-
component: App,
|
|
172
|
-
});
|
|
173
|
-
`;
|
|
174
|
-
|
|
175
138
|
const icon = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
|
|
176
139
|
<rect width="64" height="64" rx="14" fill="#18181b" />
|
|
177
140
|
<path d="M20 32h24M32 20v24" stroke="#fafafa" stroke-width="6" stroke-linecap="round" />
|
|
@@ -181,73 +144,134 @@ const icon = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
|
|
|
181
144
|
const utils = `export const counterIncrement = 1;
|
|
182
145
|
`;
|
|
183
146
|
|
|
184
|
-
const
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
## Getting started
|
|
189
|
-
|
|
190
|
-
You need Node.js 22.18 or newer. The app generator installs dependencies by default. If you
|
|
191
|
-
created the app with \`--no-install\`, install them now:
|
|
147
|
+
const home = `import { Result, useMutation, useQuery } from "ignotum/client";
|
|
148
|
+
import { api } from "@/_generated/api.js";
|
|
149
|
+
import { route } from "@/_generated/client.js";
|
|
150
|
+
import { counterIncrement } from "@/shared/utils.js";
|
|
192
151
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
152
|
+
export default route({
|
|
153
|
+
title: "Counter",
|
|
154
|
+
component: function Counter() {
|
|
155
|
+
const count = useQuery(api.counter.get);
|
|
156
|
+
const increment = useMutation(api.counter.increment);
|
|
157
|
+
|
|
158
|
+
return (
|
|
159
|
+
<section class="text-center">
|
|
160
|
+
<h1 class="text-2xl font-semibold">Counter</h1>
|
|
161
|
+
{Result.match(count, {
|
|
162
|
+
pending: () => <p class="mt-6">Loading...</p>,
|
|
163
|
+
value: (value) => (
|
|
164
|
+
<>
|
|
165
|
+
<p class="my-6 text-5xl tabular-nums">{value}</p>
|
|
166
|
+
<button
|
|
167
|
+
class="rounded bg-zinc-900 px-4 py-2 text-white"
|
|
168
|
+
type="button"
|
|
169
|
+
onClick={() => void increment()}
|
|
170
|
+
>
|
|
171
|
+
Increment by {counterIncrement}
|
|
172
|
+
</button>
|
|
173
|
+
</>
|
|
174
|
+
),
|
|
175
|
+
})}
|
|
176
|
+
</section>
|
|
177
|
+
);
|
|
178
|
+
},
|
|
179
|
+
});
|
|
180
|
+
`;
|
|
196
181
|
|
|
197
|
-
|
|
182
|
+
const rootLayout = `import { Link } from "ignotum/client";
|
|
183
|
+
import { layout } from "@/_generated/client.js";
|
|
184
|
+
import { routes } from "@/_generated/routes.js";
|
|
185
|
+
import "./styles.css";
|
|
186
|
+
|
|
187
|
+
export default layout({
|
|
188
|
+
meta: { description: "A live counter built with Ignotum." },
|
|
189
|
+
component: function RootLayout({ children }) {
|
|
190
|
+
return (
|
|
191
|
+
<div class="mx-auto max-w-sm px-6 py-12">
|
|
192
|
+
<nav aria-label="Main" class="mb-10 flex justify-center gap-6">
|
|
193
|
+
<Link to={routes.index}>Counter</Link>
|
|
194
|
+
<Link to={routes.about}>About</Link>
|
|
195
|
+
</nav>
|
|
196
|
+
<main>{children}</main>
|
|
197
|
+
</div>
|
|
198
|
+
);
|
|
199
|
+
},
|
|
200
|
+
});
|
|
201
|
+
`;
|
|
198
202
|
|
|
199
|
-
|
|
203
|
+
const about = `import { Link } from "ignotum/client";
|
|
204
|
+
import { route } from "@/_generated/client.js";
|
|
205
|
+
import { routes } from "@/_generated/routes.js";
|
|
206
|
+
|
|
207
|
+
export default route({
|
|
208
|
+
title: "About",
|
|
209
|
+
component: function About() {
|
|
210
|
+
return (
|
|
211
|
+
<section>
|
|
212
|
+
<h1 class="text-2xl font-semibold">About this counter</h1>
|
|
213
|
+
<p class="my-6">Open the counter in two tabs to see live updates.</p>
|
|
214
|
+
<Link to={routes.index}>Back to the counter</Link>
|
|
215
|
+
</section>
|
|
216
|
+
);
|
|
217
|
+
},
|
|
218
|
+
});
|
|
219
|
+
`;
|
|
200
220
|
|
|
201
|
-
|
|
202
|
-
npx ignotum dev
|
|
203
|
-
\`\`\`
|
|
221
|
+
const readme = (appName: string) => `# ${appName}
|
|
204
222
|
|
|
205
|
-
|
|
223
|
+
An Ignotum counter app with live updates and two pages.
|
|
206
224
|
|
|
207
|
-
|
|
208
|
-
and updates them when the schema or server functions change.
|
|
225
|
+
## App files
|
|
209
226
|
|
|
210
|
-
|
|
227
|
+
- \`client/index.tsx\` defines the home page with \`route(...)\`.
|
|
228
|
+
- \`client/about.tsx\` defines the About page at \`/about\`.
|
|
229
|
+
- \`client/+layout.tsx\` shares navigation and styles across pages. Put shared providers here.
|
|
230
|
+
- \`client/styles.css\` contains app-wide styles. Ignotum supplies Tailwind CSS automatically.
|
|
231
|
+
- \`client/icon.svg\` is the optional app icon.
|
|
232
|
+
- \`server/schema.ts\` defines tables and \`server/counter.ts\` defines the counter functions.
|
|
233
|
+
- \`shared/utils.ts\` contains code used by the client and server.
|
|
234
|
+
- \`_generated\` contains generated bindings. Never edit these files.
|
|
211
235
|
|
|
212
|
-
|
|
236
|
+
Import \`route\`, \`layout\`, and URL \`values\` from \`@/_generated/client.js\`.
|
|
237
|
+
Import \`routes\` from \`@/_generated/routes.js\` and pass a reference to \`Link\`:
|
|
213
238
|
|
|
214
|
-
\`\`\`
|
|
215
|
-
|
|
216
|
-
npx ignotum deploy --app my-app
|
|
239
|
+
\`\`\`tsx
|
|
240
|
+
<Link to={routes.about}>About</Link>
|
|
217
241
|
\`\`\`
|
|
218
242
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
## App files
|
|
243
|
+
Page filenames determine URLs. Put helpers and shared components in files or folders starting
|
|
244
|
+
with \`_\` so they do not become pages. Keep the \`.js\` suffix on TypeScript imports.
|
|
223
245
|
|
|
224
|
-
|
|
225
|
-
- \`server/counter.ts\` defines the query and mutation used by the counter.
|
|
226
|
-
- \`client/index.tsx\` defines the app title and UI.
|
|
227
|
-
- \`client/icon.svg\` is the optional app icon.
|
|
228
|
-
- Ignotum loads Tailwind CSS automatically. Custom CSS files are ordinary client modules.
|
|
229
|
-
- \`shared/utils.ts\` contains code shared across the app.
|
|
230
|
-
- \`_generated\` contains Ignotum's generated types and bindings. Do not edit it by hand.
|
|
231
|
-
- \`.agents/skills/ignotum\` contains the app skill and references used by coding agents.
|
|
246
|
+
## Development
|
|
232
247
|
|
|
233
|
-
|
|
248
|
+
Use Node.js 22.18 or newer. Install dependencies before starting development.
|
|
249
|
+
Run \`npx ignotum install\` if dependencies are missing, then \`npx ignotum dev\`.
|
|
250
|
+
Open <http://127.0.0.1:3210>. The development server generates server, client, and route bindings
|
|
251
|
+
and updates them after edits. Run \`npx ignotum codegen\` to regenerate them separately.
|
|
252
|
+
After generating bindings, run \`npx tsc --noEmit\` to check the app.
|
|
234
253
|
|
|
235
|
-
|
|
236
|
-
npx ignotum codegen
|
|
237
|
-
npx tsc --noEmit
|
|
238
|
-
\`\`\`
|
|
254
|
+
## Deploy
|
|
239
255
|
|
|
240
|
-
|
|
241
|
-
|
|
256
|
+
Sign in with \`npx ignotum auth login\`, then run \`npx ignotum deploy --app my-app\`.
|
|
257
|
+
Later deployments use \`npx ignotum deploy\`. The command prints the hosted URL and confirms
|
|
258
|
+
that the numbered App Version is live. If activation cannot be confirmed, follow the printed
|
|
259
|
+
\`ignotum deploy --resume <version-id>\` command to finish without creating another version.
|
|
242
260
|
|
|
243
|
-
##
|
|
261
|
+
## Coding agents
|
|
244
262
|
|
|
245
|
-
|
|
246
|
-
|
|
263
|
+
Read \`AGENTS.md\` and \`.agents/skills/ignotum/SKILL.md\` before editing.
|
|
264
|
+
For Claude Code, rename \`AGENTS.md\` to \`CLAUDE.md\` and \`.agents\` to \`.claude\`.
|
|
247
265
|
`;
|
|
248
266
|
|
|
249
|
-
const appFiles = (appName: string): ReadonlyArray<AppFile> => [
|
|
250
|
-
{ content:
|
|
267
|
+
export const appFiles = (appName: string): ReadonlyArray<AppFile> => [
|
|
268
|
+
{ content: home, path: "client/index.tsx" },
|
|
269
|
+
{ content: rootLayout, path: "client/+layout.tsx" },
|
|
270
|
+
{ content: about, path: "client/about.tsx" },
|
|
271
|
+
{
|
|
272
|
+
content: "body { margin: 0; background: #fafafa; color: #18181b; }\n",
|
|
273
|
+
path: "client/styles.css",
|
|
274
|
+
},
|
|
251
275
|
{ content: icon, path: "client/icon.svg" },
|
|
252
276
|
{ content: counterFunctions, path: "server/counter.ts" },
|
|
253
277
|
{ content: schema, path: "server/schema.ts" },
|
|
@@ -376,7 +400,7 @@ export const createApp = Effect.fn("NewApp.createApp")(function* (options: NewAp
|
|
|
376
400
|
discard: true,
|
|
377
401
|
});
|
|
378
402
|
|
|
379
|
-
yield*
|
|
403
|
+
yield* generateClientRoutes(appDirectory);
|
|
380
404
|
|
|
381
405
|
const packageManager = options.install ? yield* installDependencies(appDirectory) : null;
|
|
382
406
|
|
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
import { Effect, FileSystem, Path, Schema } from "effect";
|
|
2
|
+
import { createServer } from "vite";
|
|
3
|
+
import {
|
|
4
|
+
AppModuleId,
|
|
5
|
+
LayoutDefinition,
|
|
6
|
+
RouteDefinition,
|
|
7
|
+
type UrlField,
|
|
8
|
+
} from "@ignotum/contracts/app-definition";
|
|
9
|
+
import { compilePatterns, parsePattern } from "@ignotum/shared/routing/pattern";
|
|
10
|
+
import {
|
|
11
|
+
generate,
|
|
12
|
+
generatedHeader,
|
|
13
|
+
generatedImageTypes,
|
|
14
|
+
inspectOutput,
|
|
15
|
+
writeOutput,
|
|
16
|
+
} from "./codegen.js";
|
|
17
|
+
import { createRouteExtractor, InvalidRouteSource, sourceError } from "./route-static.js";
|
|
18
|
+
|
|
19
|
+
export interface DiscoveredClientModule {
|
|
20
|
+
readonly path: string;
|
|
21
|
+
readonly relativePath: string;
|
|
22
|
+
readonly id: AppModuleId;
|
|
23
|
+
readonly kind: "route" | "layout";
|
|
24
|
+
readonly referenceKeys: ReadonlyArray<string>;
|
|
25
|
+
}
|
|
26
|
+
export interface ClientRouteDiscovery {
|
|
27
|
+
readonly routes: ReadonlyArray<RouteDefinition>;
|
|
28
|
+
readonly layouts: ReadonlyArray<LayoutDefinition>;
|
|
29
|
+
readonly modules: ReadonlyArray<DiscoveredClientModule>;
|
|
30
|
+
readonly dependencies: ReadonlyArray<string>;
|
|
31
|
+
}
|
|
32
|
+
const byName = (a: string, b: string) => (a < b ? -1 : a > b ? 1 : 0);
|
|
33
|
+
const contains = (directory: string, child: string) =>
|
|
34
|
+
directory === "/" || child === directory || child.startsWith(`${directory}/`);
|
|
35
|
+
const toPattern = (parts: ReadonlyArray<string>) =>
|
|
36
|
+
`/${parts.map((part) => (part.startsWith("$$") ? `*${part.slice(2)}` : part.startsWith("$") ? `:${part.slice(1)}` : part)).join("/")}`;
|
|
37
|
+
const diagnostic = (file: string, message: string) =>
|
|
38
|
+
sourceError({ path: file, source: "" }, { start: 0 }, "route", message);
|
|
39
|
+
const candidate = (name: string) =>
|
|
40
|
+
/\.(ts|tsx)$/.test(name) && !/\.(test|spec|d)\.(ts|tsx)$/.test(name) && !name.startsWith("_");
|
|
41
|
+
|
|
42
|
+
export const discoverClientRoutes = Effect.fn("Routes.discoverClientRoutes")(function* (
|
|
43
|
+
directory: string,
|
|
44
|
+
) {
|
|
45
|
+
const fs = yield* FileSystem.FileSystem;
|
|
46
|
+
const path = yield* Path.Path;
|
|
47
|
+
const appDirectory = yield* fs.realPath(directory);
|
|
48
|
+
const clientDirectory = path.join(appDirectory, "client");
|
|
49
|
+
const files: string[] = [];
|
|
50
|
+
const seen = new Set<string>();
|
|
51
|
+
const visit = Effect.fn("Routes.visitDirectory")(function* (
|
|
52
|
+
directory: string,
|
|
53
|
+
): Effect.fn.Return<void, InvalidRouteSource> {
|
|
54
|
+
const real = yield* fs
|
|
55
|
+
.realPath(directory)
|
|
56
|
+
.pipe(Effect.mapError(() => diagnostic(directory, "Cannot read route directory.")));
|
|
57
|
+
if (real !== clientDirectory && !real.startsWith(`${clientDirectory}/`))
|
|
58
|
+
return yield* diagnostic(directory, "Route trees cannot escape client/ through symlinks.");
|
|
59
|
+
if (seen.has(real)) return yield* diagnostic(directory, "Repeated or cyclic route directory.");
|
|
60
|
+
seen.add(real);
|
|
61
|
+
const entries = yield* fs
|
|
62
|
+
.readDirectory(directory)
|
|
63
|
+
.pipe(Effect.mapError(() => diagnostic(directory, "Cannot read route directory.")));
|
|
64
|
+
for (const name of entries.sort(byName)) {
|
|
65
|
+
if (name.startsWith("_") || name.startsWith(".")) continue;
|
|
66
|
+
const file = path.join(directory, name);
|
|
67
|
+
const info = yield* fs
|
|
68
|
+
.stat(file)
|
|
69
|
+
.pipe(Effect.mapError(() => diagnostic(file, "Cannot inspect route source.")));
|
|
70
|
+
if (info.type === "Directory") yield* visit(file);
|
|
71
|
+
else if (info.type === "File" && candidate(name)) files.push(file);
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
yield* visit(clientDirectory);
|
|
75
|
+
if (
|
|
76
|
+
!files.some(
|
|
77
|
+
(file) =>
|
|
78
|
+
path.dirname(file) === clientDirectory && /^index\.(ts|tsx)$/.test(path.basename(file)),
|
|
79
|
+
)
|
|
80
|
+
)
|
|
81
|
+
return yield* diagnostic(
|
|
82
|
+
clientDirectory,
|
|
83
|
+
"A client/index.ts or client/index.tsx home page is required.",
|
|
84
|
+
);
|
|
85
|
+
return yield* Effect.scoped(
|
|
86
|
+
Effect.gen(function* () {
|
|
87
|
+
// Resolve only. No transforms, SSR runner, user Vite config, or app module evaluation.
|
|
88
|
+
const server = yield* Effect.acquireRelease(
|
|
89
|
+
Effect.tryPromise({
|
|
90
|
+
try: () =>
|
|
91
|
+
createServer({
|
|
92
|
+
root: appDirectory,
|
|
93
|
+
configFile: false,
|
|
94
|
+
envFile: false,
|
|
95
|
+
appType: "custom",
|
|
96
|
+
logLevel: "silent",
|
|
97
|
+
css: { postcss: {} },
|
|
98
|
+
optimizeDeps: { noDiscovery: true },
|
|
99
|
+
resolve: { tsconfigPaths: true },
|
|
100
|
+
server: { middlewareMode: true, watch: null, hmr: false },
|
|
101
|
+
}),
|
|
102
|
+
catch: () => diagnostic(clientDirectory, "Cannot initialize the app import resolver."),
|
|
103
|
+
}),
|
|
104
|
+
(server) => Effect.promise(() => server.close()),
|
|
105
|
+
);
|
|
106
|
+
const extractor = yield* createRouteExtractor(appDirectory, (specifier, importer) =>
|
|
107
|
+
Effect.tryPromise({
|
|
108
|
+
try: async () => {
|
|
109
|
+
const resolved = await server.environments.client.pluginContainer.resolveId(
|
|
110
|
+
specifier,
|
|
111
|
+
importer,
|
|
112
|
+
);
|
|
113
|
+
if (
|
|
114
|
+
resolved === null ||
|
|
115
|
+
(resolved.external !== undefined && resolved.external !== false) ||
|
|
116
|
+
resolved.id.includes("?")
|
|
117
|
+
)
|
|
118
|
+
throw new Error("Unresolved app module");
|
|
119
|
+
return resolved.id;
|
|
120
|
+
},
|
|
121
|
+
catch: () =>
|
|
122
|
+
diagnostic(importer, `Cannot resolve '${specifier}' as an app source module.`),
|
|
123
|
+
}),
|
|
124
|
+
);
|
|
125
|
+
const modules: DiscoveredClientModule[] = [];
|
|
126
|
+
const layouts: LayoutDefinition[] = [];
|
|
127
|
+
const pending: {
|
|
128
|
+
readonly file: string;
|
|
129
|
+
readonly scope: string;
|
|
130
|
+
readonly pattern: string;
|
|
131
|
+
readonly extracted: Effect.Success<ReturnType<typeof extractor.extract>>;
|
|
132
|
+
readonly module: DiscoveredClientModule;
|
|
133
|
+
}[] = [];
|
|
134
|
+
for (const file of files.sort(byName)) {
|
|
135
|
+
const relativePath = path.relative(clientDirectory, file).split(path.sep).join("/");
|
|
136
|
+
const keys = relativePath.replace(/\.(ts|tsx)$/, "").split("/");
|
|
137
|
+
const kind = keys.at(-1) === "+layout" ? "layout" : "route";
|
|
138
|
+
if (keys.some((key) => key.startsWith("+") && key !== "+layout"))
|
|
139
|
+
return yield* diagnostic(file, "Unsupported reserved route file name.");
|
|
140
|
+
const scope = toPattern(keys.slice(0, -1));
|
|
141
|
+
const pattern = toPattern(
|
|
142
|
+
keys.at(-1) === "index" || kind === "layout" ? keys.slice(0, -1) : keys,
|
|
143
|
+
);
|
|
144
|
+
const parsed = yield* parsePattern(pattern).pipe(
|
|
145
|
+
Effect.mapError((error) => diagnostic(file, error.message)),
|
|
146
|
+
);
|
|
147
|
+
// Source names can contain $, +, Unicode, and punctuation. Module identities use a reversible hex encoding.
|
|
148
|
+
const id = yield* Schema.decodeUnknownEffect(AppModuleId)(
|
|
149
|
+
`app/client/m${Buffer.from(relativePath).toString("hex")}.js`,
|
|
150
|
+
).pipe(
|
|
151
|
+
Effect.mapError(() =>
|
|
152
|
+
diagnostic(file, "Route source name exceeds module identity limits."),
|
|
153
|
+
),
|
|
154
|
+
);
|
|
155
|
+
const module = {
|
|
156
|
+
path: file,
|
|
157
|
+
relativePath,
|
|
158
|
+
id,
|
|
159
|
+
kind,
|
|
160
|
+
referenceKeys: kind === "route" ? keys : [],
|
|
161
|
+
} satisfies DiscoveredClientModule;
|
|
162
|
+
modules.push(module);
|
|
163
|
+
const extracted = yield* extractor.extract(file, kind);
|
|
164
|
+
if (kind === "layout") {
|
|
165
|
+
const layout = yield* Schema.decodeUnknownEffect(LayoutDefinition)({
|
|
166
|
+
id: `layout:${relativePath}`,
|
|
167
|
+
scope: parsed.pattern,
|
|
168
|
+
module: { module: id, exportName: "default" },
|
|
169
|
+
metadata: extracted.metadata,
|
|
170
|
+
boundaries: extracted.boundaries,
|
|
171
|
+
}).pipe(Effect.mapError((error) => diagnostic(file, String(error))));
|
|
172
|
+
layouts.push(layout);
|
|
173
|
+
} else pending.push({ file, scope, pattern, extracted, module });
|
|
174
|
+
}
|
|
175
|
+
const nestedLayouts = layouts.map((layout) => {
|
|
176
|
+
const parent = layouts
|
|
177
|
+
.filter((other) => other.id !== layout.id && contains(other.scope, layout.scope))
|
|
178
|
+
.sort((a, b) => b.scope.length - a.scope.length)[0]?.id;
|
|
179
|
+
// An explicit undefined encodes as null and changes the persisted definition identity.
|
|
180
|
+
return parent === undefined ? layout : { ...layout, parent };
|
|
181
|
+
});
|
|
182
|
+
const routes: RouteDefinition[] = [];
|
|
183
|
+
for (const { file, scope, pattern, extracted, module } of pending) {
|
|
184
|
+
const parsed = yield* parsePattern(pattern);
|
|
185
|
+
const params: ReadonlyArray<UrlField> =
|
|
186
|
+
extracted.params ??
|
|
187
|
+
parsed.segments.flatMap((segment) =>
|
|
188
|
+
segment.kind === "Literal"
|
|
189
|
+
? []
|
|
190
|
+
: [
|
|
191
|
+
{
|
|
192
|
+
name: segment.name,
|
|
193
|
+
value:
|
|
194
|
+
segment.kind === "Parameter"
|
|
195
|
+
? { type: "string" as const }
|
|
196
|
+
: { type: "array" as const, value: { type: "string" as const } },
|
|
197
|
+
},
|
|
198
|
+
],
|
|
199
|
+
);
|
|
200
|
+
const route = yield* Schema.decodeUnknownEffect(RouteDefinition)({
|
|
201
|
+
id: `route:${module.relativePath}`,
|
|
202
|
+
pattern,
|
|
203
|
+
directoryScope: scope,
|
|
204
|
+
precedence: parsed.precedence,
|
|
205
|
+
params,
|
|
206
|
+
searchParams: extracted.searchParams,
|
|
207
|
+
page: { module: module.id, exportName: "default" },
|
|
208
|
+
layouts: nestedLayouts
|
|
209
|
+
.filter((layout) => contains(layout.scope, scope))
|
|
210
|
+
.sort((a, b) => a.scope.length - b.scope.length)
|
|
211
|
+
.map((layout) => layout.id),
|
|
212
|
+
metadata: extracted.metadata,
|
|
213
|
+
boundaries: { error: extracted.boundaries.error, invalid: extracted.boundaries.invalid },
|
|
214
|
+
}).pipe(Effect.mapError((error) => diagnostic(file, String(error))));
|
|
215
|
+
routes.push(route);
|
|
216
|
+
}
|
|
217
|
+
yield* compilePatterns(routes, nestedLayouts).pipe(
|
|
218
|
+
Effect.mapError((error) => diagnostic(clientDirectory, error.message)),
|
|
219
|
+
);
|
|
220
|
+
return {
|
|
221
|
+
routes,
|
|
222
|
+
layouts: nestedLayouts,
|
|
223
|
+
modules,
|
|
224
|
+
dependencies: [...extractor.dependencies].sort(byName),
|
|
225
|
+
} satisfies ClientRouteDiscovery;
|
|
226
|
+
}),
|
|
227
|
+
);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
export const renderClientBindings = (): string => `${generatedHeader}
|
|
231
|
+
|
|
232
|
+
${generatedImageTypes}
|
|
233
|
+
|
|
234
|
+
import { bindSchema } from "ignotum/internal/client";
|
|
235
|
+
import type { DataModel } from "./types.js";
|
|
236
|
+
|
|
237
|
+
export const { route, layout, values } = bindSchema<DataModel>();
|
|
238
|
+
`;
|
|
239
|
+
interface ReferenceTree {
|
|
240
|
+
reference?: string;
|
|
241
|
+
readonly children: Map<string, ReferenceTree>;
|
|
242
|
+
}
|
|
243
|
+
export const renderRouteReferences = (discovery: ClientRouteDiscovery): string => {
|
|
244
|
+
const tree: ReferenceTree = { children: new Map() };
|
|
245
|
+
const references: string[] = [];
|
|
246
|
+
for (const [index, route] of discovery.routes.entries()) {
|
|
247
|
+
const module = discovery.modules.find((module) => module.id === route.page.module)!;
|
|
248
|
+
const name = `route${index}`;
|
|
249
|
+
const params = route.params
|
|
250
|
+
.map(
|
|
251
|
+
(field) =>
|
|
252
|
+
`${JSON.stringify(field.name)}: ${field.value.type === "array" ? "readonly string[]" : "string"}`,
|
|
253
|
+
)
|
|
254
|
+
.join("; ");
|
|
255
|
+
references.push(
|
|
256
|
+
`const ${name} = createRouteReference<typeof import(${JSON.stringify(`../client/${module.relativePath.replace(/\.(ts|tsx)$/, ".js")}`)}).default, { ${params} }>(${JSON.stringify(route, null, 2)});`,
|
|
257
|
+
);
|
|
258
|
+
let cursor = tree;
|
|
259
|
+
for (const key of module.referenceKeys) {
|
|
260
|
+
let child = cursor.children.get(key);
|
|
261
|
+
if (child === undefined) {
|
|
262
|
+
child = { children: new Map() };
|
|
263
|
+
cursor.children.set(key, child);
|
|
264
|
+
}
|
|
265
|
+
cursor = child;
|
|
266
|
+
}
|
|
267
|
+
cursor.reference = name;
|
|
268
|
+
}
|
|
269
|
+
const render = (node: ReferenceTree): string => {
|
|
270
|
+
const object = `{ ${[...node.children].map(([key, child]) => `[${JSON.stringify(key)}]: ${render(child)}`).join(", ")} }`;
|
|
271
|
+
return node.reference === undefined
|
|
272
|
+
? object
|
|
273
|
+
: node.children.size === 0
|
|
274
|
+
? node.reference
|
|
275
|
+
: `Object.assign(${node.reference}, ${object})`;
|
|
276
|
+
};
|
|
277
|
+
return `${generatedHeader}\n\nimport { createRouteReference } from "ignotum/internal/client";\n\n${references.join("\n\n")}\n\nexport const routes = ${render(tree)};\n`;
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
export const generateClientRoutes = Effect.fn("Routes.generateClientRoutes")(function* (
|
|
281
|
+
appDirectory: string,
|
|
282
|
+
) {
|
|
283
|
+
const path = yield* Path.Path;
|
|
284
|
+
const server = yield* generate(appDirectory);
|
|
285
|
+
const client = {
|
|
286
|
+
path: path.join(appDirectory, "_generated/client.ts"),
|
|
287
|
+
content: renderClientBindings(),
|
|
288
|
+
};
|
|
289
|
+
const clientInspection = yield* inspectOutput(client);
|
|
290
|
+
if (clientInspection === "write") yield* writeOutput(client);
|
|
291
|
+
const discovery = yield* discoverClientRoutes(appDirectory);
|
|
292
|
+
const routes = {
|
|
293
|
+
path: path.join(appDirectory, "_generated/routes.ts"),
|
|
294
|
+
content: renderRouteReferences(discovery),
|
|
295
|
+
};
|
|
296
|
+
const routesInspection = yield* inspectOutput(routes);
|
|
297
|
+
if (routesInspection === "write") yield* writeOutput(routes);
|
|
298
|
+
return {
|
|
299
|
+
...discovery,
|
|
300
|
+
written: [
|
|
301
|
+
...server.written,
|
|
302
|
+
...(clientInspection === "write" ? [client.path] : []),
|
|
303
|
+
...(routesInspection === "write" ? [routes.path] : []),
|
|
304
|
+
],
|
|
305
|
+
unchanged: [
|
|
306
|
+
...server.unchanged,
|
|
307
|
+
...(clientInspection === "unchanged" ? [client.path] : []),
|
|
308
|
+
...(routesInspection === "unchanged" ? [routes.path] : []),
|
|
309
|
+
],
|
|
310
|
+
};
|
|
311
|
+
});
|