akanjs 3.1.0-rc.8 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +547 -0
- package/README.ko.md +2 -2
- package/README.md +2 -2
- package/client/cookie.ts +5 -0
- package/client/csrLocation.ts +17 -72
- package/client/csrTypes.ts +19 -4
- package/client/href.ts +8 -0
- package/client/makePageProto.tsx +3 -2
- package/client/route/PageDefinition.ts +103 -10
- package/client/route/RouteDefinition.ts +44 -3
- package/client/route/RouteGuards.ts +64 -0
- package/client/route/index.ts +1 -0
- package/client/route/resolveRouteModule.ts +8 -0
- package/client/route/routeArgs.ts +9 -0
- package/client/router.ts +51 -2
- package/client/translator.ts +16 -0
- package/client/viewportMeta.ts +23 -8
- package/common/Logger.ts +6 -4
- package/common/authCookie.ts +13 -0
- package/common/authToken.ts +1 -12
- package/common/codeAgentWire.ts +8 -1
- package/common/index.ts +2 -8
- package/common/routeConvention.ts +2 -0
- package/constant/inputParser.ts +1 -1
- package/dictionary/base.dictionary.ts +11 -0
- package/document/database.ts +3 -0
- package/document/documentQuery.ts +21 -0
- package/document/into.ts +2 -0
- package/document/liveRowScope.ts +4 -8
- package/document/noDocumentError.ts +6 -1
- package/document/queryEvaluator.ts +5 -8
- package/fetch/appScope.ts +54 -0
- package/fetch/client/fetchClient.ts +46 -29
- package/fetch/client/httpClient.ts +38 -10
- package/fetch/client/sliceInitHandle.ts +2 -3
- package/fetch/client/wsClient.ts +3 -1
- package/fetch/fetchType/endpointFetch.type.ts +11 -9
- package/fetch/index.ts +1 -0
- package/index.ts +2 -0
- package/package.json +32 -22
- package/server/akanApp.ts +12 -3
- package/server/akanServer.ts +10 -5
- package/server/index.ts +1 -0
- package/server/mcp/McpApps.ts +44 -50
- package/server/mcp/McpDispatcher.ts +3 -3
- package/server/mcp/McpPages.ts +93 -19
- package/server/mcp/McpRouter.ts +14 -11
- package/server/memoryLimit.ts +4 -0
- package/server/migration/docPath.ts +121 -0
- package/server/migration/index.ts +15 -0
- package/server/migration/migrationChangeLog.ts +86 -0
- package/server/migration/migrationChanges.ts +70 -0
- package/server/migration/migrationCommand.ts +179 -0
- package/server/migration/migrationContext.ts +53 -0
- package/server/migration/migrationDefinition.ts +87 -0
- package/server/migration/migrationError.ts +45 -0
- package/server/migration/migrationLedger.ts +172 -0
- package/server/migration/migrationLock.ts +103 -0
- package/server/migration/migrationParams.ts +122 -0
- package/server/migration/migrationPulse.ts +39 -0
- package/server/migration/migrationRunner.ts +525 -0
- package/server/migration/migrationScope.ts +93 -0
- package/server/migration/migrationTable.ts +258 -0
- package/server/migration/migrations.ts +57 -0
- package/server/ops/migrateJobs.ts +145 -0
- package/server/ops/opsCommand.ts +30 -1
- package/server/ops/opsRoute.ts +39 -7
- package/server/ops/opsToken.ts +7 -2
- package/server/resolver/CascadeRunner.ts +15 -2
- package/server/resolver/database.resolver.ts +4 -0
- package/server/resolver/service.resolver.ts +3 -0
- package/server/resolver/signal.resolver.ts +41 -31
- package/server/routeElementComposer.tsx +33 -6
- package/server/routeState.ts +1 -0
- package/server/routeTreeBuilder.ts +5 -0
- package/server/rscClient.tsx +67 -22
- package/server/rscClientFetch.ts +8 -3
- package/server/rscNavigationState.ts +2 -2
- package/server/rscPagePrompts.ts +66 -16
- package/server/rscScrollRestoration.ts +21 -8
- package/server/rscWorker.tsx +79 -32
- package/server/rscWorkerHost.ts +12 -1
- package/server/tunnel/TunnelAgent.ts +2 -1
- package/server/webRouter.ts +33 -27
- package/service/agent.service.ts +21 -1
- package/service/endpointCacheKeys.ts +35 -0
- package/service/index.ts +1 -0
- package/service/liveSyncHub.ts +2 -1
- package/service/predefinedAdaptor/anthropicLlm.ts +37 -17
- package/service/predefinedAdaptor/database.adaptor.ts +98 -25
- package/service/predefinedAdaptor/documentTransfer.ts +50 -0
- package/service/predefinedAdaptor/endpointCacheControl.adaptor.ts +19 -0
- package/service/predefinedAdaptor/index.ts +1 -0
- package/service/predefinedAdaptor/llm.adaptor.ts +7 -0
- package/service/predefinedAdaptor/openaiDialect.ts +53 -17
- package/service/predefinedAdaptor/openaiLlm.ts +1 -1
- package/service/predefinedAdaptor/solidQueue.adaptor.ts +30 -9
- package/service/predefinedAdaptor/sql/MigrationTables.ts +90 -0
- package/service/predefinedAdaptor/sql/QueryCompiler.ts +16 -14
- package/service/predefinedAdaptor/sql/RawDocumentTable.ts +200 -0
- package/service/predefinedAdaptor/sql/SqlDocumentStore.ts +218 -44
- package/service/predefinedAdaptor/sql/SqliteInsightSession.ts +4 -1
- package/service/predefinedAdaptor/sql/dialect/postgres.ts +13 -3
- package/service/predefinedAdaptor/sql/dialect/sqlite.ts +13 -2
- package/service/predefinedAdaptor/sql/driver/bunSqlite.ts +4 -5
- package/service/predefinedAdaptor/sql/types.ts +22 -1
- package/service/predefinedAdaptor/sql/values.ts +19 -0
- package/service/predefinedAdaptor/storage.adaptor.ts +19 -0
- package/service/serviceModule.ts +3 -0
- package/service/types.ts +1 -0
- package/signal/TaggedJson.ts +26 -4
- package/signal/endpointCache.ts +16 -6
- package/signal/exception.ts +4 -1
- package/signal/mcp/mcpProtocol.ts +7 -0
- package/signal/mcp/pagePrompt.ts +18 -1
- package/signal/signalContext.ts +7 -10
- package/store/action.ts +181 -73
- package/store/agent/ScreenImage.ts +166 -0
- package/store/agent/StoreSurfaceSource.ts +41 -2
- package/store/agent/index.ts +2 -1
- package/store/databaseStateNames.ts +1 -0
- package/store/formUploadOf.ts +41 -0
- package/store/index.ts +1 -0
- package/store/liveInsert.ts +5 -1
- package/store/sliceKeys.ts +1 -0
- package/store/state.ts +15 -3
- package/store/stateBuilder.ts +7 -1
- package/store/storeInstance.ts +22 -4
- package/types/client/csrLocation.d.ts +3 -7
- package/types/client/csrTypes.d.ts +22 -3
- package/types/client/href.d.ts +3 -0
- package/types/client/route/PageDefinition.d.ts +21 -4
- package/types/client/route/RouteDefinition.d.ts +4 -0
- package/types/client/route/RouteGuards.d.ts +15 -0
- package/types/client/route/index.d.ts +1 -0
- package/types/client/route/resolveRouteModule.d.ts +2 -0
- package/types/client/route/routeArgs.d.ts +5 -0
- package/types/client/route/routeBuilders.d.ts +1 -1
- package/types/client/router.d.ts +9 -1
- package/types/client/translator.d.ts +2 -0
- package/types/common/authCookie.d.ts +5 -0
- package/types/common/authToken.d.ts +0 -5
- package/types/common/codeAgentWire.d.ts +7 -0
- package/types/common/index.d.ts +2 -1
- package/types/dictionary/base.dictionary.d.ts +1 -1
- package/types/dictionary/dictionary.d.ts +8 -8
- package/types/document/database.d.ts +3 -0
- package/types/document/documentQuery.d.ts +1 -0
- package/types/document/into.d.ts +2 -0
- package/types/document/noDocumentError.d.ts +6 -1
- package/types/fetch/appScope.d.ts +18 -0
- package/types/fetch/client/fetchClient.d.ts +1 -1
- package/types/fetch/fetchType/endpointFetch.type.d.ts +1 -1
- package/types/fetch/index.d.ts +1 -0
- package/types/index.d.ts +5 -0
- package/types/server/akanServer.d.ts +10 -5
- package/types/server/index.d.ts +1 -0
- package/types/server/mcp/McpApps.d.ts +14 -8
- package/types/server/mcp/McpPages.d.ts +6 -3
- package/types/server/migration/docPath.d.ts +20 -0
- package/types/server/migration/index.d.ts +15 -0
- package/types/server/migration/migrationChangeLog.d.ts +27 -0
- package/types/server/migration/migrationChanges.d.ts +27 -0
- package/types/server/migration/migrationCommand.d.ts +33 -0
- package/types/server/migration/migrationContext.d.ts +28 -0
- package/types/server/migration/migrationDefinition.d.ts +42 -0
- package/types/server/migration/migrationError.d.ts +15 -0
- package/types/server/migration/migrationLedger.d.ts +51 -0
- package/types/server/migration/migrationLock.d.ts +34 -0
- package/types/server/migration/migrationParams.d.ts +41 -0
- package/types/server/migration/migrationPulse.d.ts +15 -0
- package/types/server/migration/migrationRunner.d.ts +93 -0
- package/types/server/migration/migrationScope.d.ts +42 -0
- package/types/server/migration/migrationTable.d.ts +52 -0
- package/types/server/migration/migrations.d.ts +12 -0
- package/types/server/migration.d.ts +1 -0
- package/types/server/ops/migrateJobs.d.ts +36 -0
- package/types/server/ops/opsCommand.d.ts +10 -1
- package/types/server/ops/opsRoute.d.ts +4 -1
- package/types/server/ops/opsToken.d.ts +3 -0
- package/types/server/routeElementComposer.d.ts +10 -1
- package/types/server/routeState.d.ts +1 -0
- package/types/server/rscClientFetch.d.ts +2 -0
- package/types/server/rscNavigationState.d.ts +1 -1
- package/types/server/rscPagePrompts.d.ts +3 -1
- package/types/server/rscScrollRestoration.d.ts +2 -0
- package/types/server/rscWorkerHost.d.ts +2 -1
- package/types/server/webRouter.d.ts +3 -0
- package/types/service/agent.service.d.ts +2 -0
- package/types/service/endpointCacheKeys.d.ts +8 -0
- package/types/service/index.d.ts +1 -0
- package/types/service/predefinedAdaptor/anthropicLlm.d.ts +9 -4
- package/types/service/predefinedAdaptor/database.adaptor.d.ts +21 -2
- package/types/service/predefinedAdaptor/documentTransfer.d.ts +1 -0
- package/types/service/predefinedAdaptor/endpointCacheControl.adaptor.d.ts +9 -0
- package/types/service/predefinedAdaptor/index.d.ts +1 -0
- package/types/service/predefinedAdaptor/llm.adaptor.d.ts +7 -0
- package/types/service/predefinedAdaptor/openaiDialect.d.ts +20 -5
- package/types/service/predefinedAdaptor/solidQueue.adaptor.d.ts +1 -0
- package/types/service/predefinedAdaptor/sql/MigrationTables.d.ts +17 -0
- package/types/service/predefinedAdaptor/sql/QueryCompiler.d.ts +0 -1
- package/types/service/predefinedAdaptor/sql/RawDocumentTable.d.ts +38 -0
- package/types/service/predefinedAdaptor/sql/SqlDocumentStore.d.ts +1 -0
- package/types/service/predefinedAdaptor/sql/dialect/postgres.d.ts +2 -1
- package/types/service/predefinedAdaptor/sql/dialect/sqlite.d.ts +3 -1
- package/types/service/predefinedAdaptor/sql/driver/bunSqlite.d.ts +2 -2
- package/types/service/predefinedAdaptor/sql/types.d.ts +26 -0
- package/types/service/predefinedAdaptor/sql/values.d.ts +1 -0
- package/types/service/predefinedAdaptor/storage.adaptor.d.ts +8 -0
- package/types/service/types.d.ts +1 -0
- package/types/signal/exception.d.ts +4 -1
- package/types/signal/mcp/mcpProtocol.d.ts +6 -0
- package/types/signal/mcp/pagePrompt.d.ts +16 -0
- package/types/signal/signalContext.d.ts +0 -2
- package/types/store/action.d.ts +2 -0
- package/types/store/agent/ScreenImage.d.ts +27 -0
- package/types/store/agent/StoreSurfaceSource.d.ts +1 -1
- package/types/store/agent/index.d.ts +2 -1
- package/types/store/databaseStateNames.d.ts +1 -0
- package/types/store/formUploadOf.d.ts +9 -0
- package/types/store/index.d.ts +1 -0
- package/types/store/liveInsert.d.ts +3 -1
- package/types/store/sliceKeys.d.ts +1 -0
- package/types/store/state.d.ts +10 -4
- package/types/store/stateBuilder.d.ts +1 -0
- package/types/ui/Agent/Composer.d.ts +2 -0
- package/types/ui/Agent/Menu.d.ts +2 -0
- package/types/ui/CsrImage.d.ts +2 -0
- package/types/ui/Image.d.ts +3 -1
- package/types/ui/Layout/FrameSlotPortal.d.ts +1 -1
- package/types/ui/Load/sliceClaim.d.ts +28 -0
- package/types/ui/Model/Edit.d.ts +3 -2
- package/types/ui/Model/editSeed.d.ts +4 -0
- package/types/ui/System/App.d.ts +17 -0
- package/types/ui/System/index.d.ts +2 -0
- package/types/ui/recipe/badgeRecipe.d.ts +2 -2
- package/types/ui/recipe/buttonRecipe.d.ts +2 -2
- package/types/ui/sliceNamesOf.d.ts +2 -0
- package/types/vendor/@akanjs/native/packages/cli/src/lib/compat.d.ts +6 -0
- package/types/vendor/@akanjs/native/packages/cli/src/lib/toolchains.d.ts +2 -2
- package/types/vendor/use-agentic/Compaction.d.ts +3 -1
- package/types/vendor/use-agentic/Reference.d.ts +7 -2
- package/types/vendor/use-agentic/ToolOutput.d.ts +6 -1
- package/types/vendor/use-agentic/ToolReturn.d.ts +18 -0
- package/types/vendor/use-agentic/Transcript.d.ts +7 -0
- package/types/vendor/use-agentic/index.d.ts +1 -0
- package/types/vendor/use-agentic/types.d.ts +6 -0
- package/types/webkit/CsrGuards.d.ts +25 -0
- package/types/webkit/CsrRouteTable.d.ts +6 -0
- package/types/webkit/CsrStack.d.ts +7 -0
- package/types/webkit/McpAppBoot.d.ts +32 -0
- package/types/webkit/McpAppBridge.d.ts +10 -1
- package/types/webkit/McpAppFetchBridge.d.ts +3 -1
- package/types/webkit/McpAppView.d.ts +4 -1
- package/types/webkit/PageNotFound.d.ts +5 -0
- package/types/webkit/RenderLayer.d.ts +1 -3
- package/types/webkit/index.d.ts +1 -0
- package/types/webkit/useDebounce.d.ts +1 -1
- package/ui/Agent/Chat.tsx +1 -0
- package/ui/Agent/Composer.tsx +2 -0
- package/ui/Agent/Menu.tsx +5 -1
- package/ui/Agent/agentSessionOf.ts +15 -8
- package/ui/Agent/mentionDraft.ts +2 -3
- package/ui/Agent/useReferenceMenu.ts +1 -0
- package/ui/Agent/useSlashMenu.ts +1 -0
- package/ui/CsrImage.tsx +3 -1
- package/ui/Dialog/LegacyModal.tsx +7 -5
- package/ui/Dialog/Modal.tsx +7 -4
- package/ui/Field/Collection.tsx +2 -1
- package/ui/Image.tsx +4 -4
- package/ui/Layout/BottomInset.tsx +2 -11
- package/ui/Layout/FrameSlotPortal.tsx +1 -3
- package/ui/Layout/Navbar.tsx +7 -22
- package/ui/Layout/TopInset.tsx +3 -4
- package/ui/Load/Units.tsx +34 -12
- package/ui/Load/sliceClaim.ts +164 -0
- package/ui/Model/Edit.tsx +22 -3
- package/ui/Model/EditModal.tsx +22 -6
- package/ui/Model/LoadStore.tsx +1 -1
- package/ui/Model/NewWrapper_Client.tsx +1 -1
- package/ui/Model/ViewEditModal.tsx +9 -3
- package/ui/Model/editSeed.ts +28 -0
- package/ui/System/App.tsx +43 -0
- package/ui/System/CSR.tsx +1 -10
- package/ui/System/Client.tsx +3 -0
- package/ui/System/index.tsx +3 -0
- package/ui/Tab/Provider.tsx +6 -8
- package/ui/styles.css +10 -0
- package/vendor/@akanjs/native/native/ios/Sources/AkanNativeBridge.swift +3 -2
- package/vendor/@akanjs/native/packages/cli/src/lib/compat.ts +6 -2
- package/vendor/@akanjs/native/packages/cli/src/lib/native-plugins.ts +1 -1
- package/vendor/@akanjs/native/packages/cli/src/lib/toolchains.ts +1 -1
- package/vendor/@akanjs/native/packages/cli/src/lib/updates.ts +3 -3
- package/vendor/@akanjs/native/plugins/sqlite/src/desktop.ts +10 -5
- package/vendor/@akanjs/native/plugins/updates/src/desktop.ts +45 -15
- package/vendor/use-agentic/CHANGELOG.md +31 -0
- package/vendor/use-agentic/Compaction.ts +38 -7
- package/vendor/use-agentic/Reference.ts +14 -10
- package/vendor/use-agentic/ToolOutput.ts +25 -3
- package/vendor/use-agentic/ToolReturn.ts +32 -0
- package/vendor/use-agentic/ToolRunner.ts +15 -13
- package/vendor/use-agentic/Transcript.ts +38 -2
- package/vendor/use-agentic/WIRE.md +20 -0
- package/vendor/use-agentic/index.ts +1 -0
- package/vendor/use-agentic/types.ts +6 -0
- package/webkit/CsrGuards.ts +112 -0
- package/webkit/CsrRouteTable.ts +71 -16
- package/webkit/CsrStack.ts +47 -1
- package/webkit/McpAppAgent.ts +14 -2
- package/webkit/McpAppBoot.tsx +129 -0
- package/webkit/McpAppBridge.ts +30 -6
- package/webkit/McpAppFetchBridge.ts +8 -2
- package/webkit/McpAppView.ts +39 -28
- package/webkit/PageNotFound.tsx +24 -0
- package/webkit/RenderLayer.tsx +106 -30
- package/webkit/bootCsr.tsx +4 -43
- package/webkit/index.ts +1 -0
- package/webkit/useCsrValues.ts +66 -6
- package/webkit/useDebounce.ts +23 -14
- package/webkit/useEscapeKey.tsx +1 -1
- package/webkit/useLocation.ts +21 -19
- package/types/webkit/EmbeddedLayout.d.ts +0 -10
- package/webkit/EmbeddedLayout.tsx +0 -82
package/README.ko.md
CHANGED
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
```text
|
|
23
23
|
Akan.js(https://akanjs.com) 워크스페이스를 새로 만들어 줘.
|
|
24
24
|
|
|
25
|
-
1. Bun 1.4 이상이 설치돼 있는지 확인해 줘(`bun --version`). 없거나 버전이 낮으면 https://bun.sh 안내대로 설치하거나 업그레이드해 줘.
|
|
25
|
+
1. Bun 1.4.4 이상이 설치돼 있는지 확인해 줘(`bun --version`). 없거나 버전이 낮으면 https://bun.sh 안내대로 설치하거나 업그레이드해 줘.
|
|
26
26
|
2. 워크스페이스 이름과 첫 앱 이름을 물어봐 줘. 짧은 영문 소문자로, 아래에 적어 뒀다면 그걸 쓰면 돼.
|
|
27
27
|
3. 이 디렉터리에서 `bunx create-akan-workspace@latest <워크스페이스> --app <앱>`을 실행해 줘. akan CLI를 전역으로 설치하고, ./<워크스페이스>를 만들어 의존성까지 설치해.
|
|
28
28
|
4. ./<워크스페이스> 안에서 `akan start <앱>`으로 개발 서버를 백그라운드에 띄우고, 출력된 주소(기본 http://localhost:8282)가 열리는지 확인해 줘.
|
|
@@ -35,7 +35,7 @@ Claude Code·Codex·Cursor에 등록된 Akan MCP 서버입니다. 워크스페
|
|
|
35
35
|
|
|
36
36
|
### 터미널로 시작하기
|
|
37
37
|
|
|
38
|
-
[Bun](https://bun.sh) `>=1.4.
|
|
38
|
+
[Bun](https://bun.sh) `>=1.4.4`이 필요합니다.
|
|
39
39
|
|
|
40
40
|
```bash
|
|
41
41
|
bunx create-akan-workspace@latest
|
package/README.md
CHANGED
|
@@ -22,7 +22,7 @@ or let the agent ask.
|
|
|
22
22
|
```text
|
|
23
23
|
Set up a new Akan.js workspace for me (https://akanjs.com).
|
|
24
24
|
|
|
25
|
-
1. Check that Bun 1.4 or newer is installed (`bun --version`). If it isn't, install or upgrade it as https://bun.sh describes.
|
|
25
|
+
1. Check that Bun 1.4.4 or newer is installed (`bun --version`). If it isn't, install or upgrade it as https://bun.sh describes.
|
|
26
26
|
2. Ask me for a workspace name and a first app name, short and lowercase, unless I wrote them below.
|
|
27
27
|
3. In this directory, run `bunx create-akan-workspace@latest <workspace> --app <app>`. It installs the akan CLI globally, creates ./<workspace> and installs its dependencies.
|
|
28
28
|
4. Inside ./<workspace>, start the dev server in the background with `akan start <app>` and check that the URL it prints (http://localhost:8282 by default) loads.
|
|
@@ -34,7 +34,7 @@ Akan MCP server registered for Claude Code, Codex and Cursor. Reopen the agent i
|
|
|
34
34
|
|
|
35
35
|
### In a terminal
|
|
36
36
|
|
|
37
|
-
Requires [Bun](https://bun.sh) `>=1.4.
|
|
37
|
+
Requires [Bun](https://bun.sh) `>=1.4.4`.
|
|
38
38
|
|
|
39
39
|
```bash
|
|
40
40
|
bunx create-akan-workspace@latest
|
package/client/cookie.ts
CHANGED
|
@@ -128,6 +128,9 @@ export interface GetOption {
|
|
|
128
128
|
interface SetAuthOption {
|
|
129
129
|
jwt: string;
|
|
130
130
|
}
|
|
131
|
+
const announceAuth = () => {
|
|
132
|
+
if (typeof window !== "undefined") window.dispatchEvent(new Event("akan:auth"));
|
|
133
|
+
};
|
|
131
134
|
/** Sets the active auth token on fetch, the cookie jar (except on a CSR client), and client storage. */
|
|
132
135
|
export const setAuth = ({ jwt }: SetAuthOption) => {
|
|
133
136
|
fetch.setJwt(jwt);
|
|
@@ -137,6 +140,7 @@ export const setAuth = ({ jwt }: SetAuthOption) => {
|
|
|
137
140
|
|
|
138
141
|
removeCookie(legacyAuthTokenKey);
|
|
139
142
|
void secretStorage.removeItem(legacyAuthTokenKey);
|
|
143
|
+
announceAuth();
|
|
140
144
|
};
|
|
141
145
|
|
|
142
146
|
interface InitAuthOption {
|
|
@@ -162,4 +166,5 @@ export const resetAuth = () => {
|
|
|
162
166
|
removeCookie(legacyAuthTokenKey);
|
|
163
167
|
void secretStorage.removeItem(authTokenKey());
|
|
164
168
|
void secretStorage.removeItem(legacyAuthTokenKey);
|
|
169
|
+
announceAuth();
|
|
165
170
|
};
|
package/client/csrLocation.ts
CHANGED
|
@@ -1,120 +1,65 @@
|
|
|
1
|
-
interface CsrLocationEntry {
|
|
2
|
-
href: string;
|
|
3
|
-
state: unknown;
|
|
4
|
-
}
|
|
5
|
-
|
|
6
|
-
export interface CsrMemoryOwner {
|
|
7
|
-
leave: (href: string) => void;
|
|
8
|
-
reload: () => void;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
1
|
declare global {
|
|
12
2
|
var __AKAN_CSR_LOCATION__: CsrLocation | undefined;
|
|
13
3
|
|
|
14
4
|
var __AKAN_EMBED__: { kind: "mcp-app"; origin?: string; account?: Record<string, unknown> | null } | undefined;
|
|
15
5
|
}
|
|
16
6
|
|
|
7
|
+
/** The document's location and history, as the CSR router reads and moves them. */
|
|
17
8
|
export class CsrLocation {
|
|
18
|
-
|
|
19
|
-
#
|
|
20
|
-
|
|
21
|
-
get isMemory() {
|
|
22
|
-
return this.#memory !== null;
|
|
23
|
-
}
|
|
9
|
+
|
|
10
|
+
#view: URL | null = null;
|
|
24
11
|
|
|
25
|
-
/**
|
|
26
|
-
|
|
27
|
-
this.#
|
|
28
|
-
this.#owner = owner;
|
|
12
|
+
/** Reads `href`, a path on the app's origin, as this document's location from now on; a view never moves. */
|
|
13
|
+
view(href: string) {
|
|
14
|
+
this.#view = new URL(href, "http://view.invalid");
|
|
29
15
|
}
|
|
30
16
|
|
|
31
17
|
/** Path, search and hash, without the origin. */
|
|
32
18
|
get href() {
|
|
33
|
-
|
|
34
|
-
if (entry) return entry.href;
|
|
35
|
-
return `${window.location.pathname}${window.location.search}${window.location.hash}`;
|
|
19
|
+
return `${this.pathname}${this.search}${this.hash}`;
|
|
36
20
|
}
|
|
37
21
|
|
|
38
22
|
get pathname() {
|
|
39
|
-
|
|
40
|
-
return this.href.split(/[?#]/)[0] ?? "/";
|
|
23
|
+
return (this.#view ?? window.location).pathname;
|
|
41
24
|
}
|
|
42
25
|
|
|
43
26
|
get search() {
|
|
44
|
-
|
|
45
|
-
const [beforeHash] = this.href.split("#");
|
|
46
|
-
const at = beforeHash?.indexOf("?") ?? -1;
|
|
47
|
-
return at >= 0 ? (beforeHash?.slice(at) ?? "") : "";
|
|
27
|
+
return (this.#view ?? window.location).search;
|
|
48
28
|
}
|
|
49
29
|
|
|
50
30
|
get hash() {
|
|
51
|
-
|
|
52
|
-
const at = this.href.indexOf("#");
|
|
53
|
-
return at >= 0 ? this.href.slice(at) : "";
|
|
31
|
+
return (this.#view ?? window.location).hash;
|
|
54
32
|
}
|
|
55
33
|
|
|
56
34
|
get state(): unknown {
|
|
57
|
-
|
|
58
|
-
return entry ? entry.state : window.history.state;
|
|
35
|
+
return window.history.state;
|
|
59
36
|
}
|
|
60
37
|
|
|
61
38
|
/** `href` as a route reads it: an absolute URL on this document's origin loses the origin, anything else is kept. */
|
|
62
39
|
strip(href: string) {
|
|
63
|
-
if (this.#memory) return href;
|
|
64
40
|
const origin = window.location.origin;
|
|
65
41
|
return origin !== "null" && href.startsWith(origin) ? href.slice(origin.length) : href;
|
|
66
42
|
}
|
|
67
43
|
|
|
68
44
|
push(state: unknown, href: string) {
|
|
69
|
-
|
|
70
|
-
if (!memory) {
|
|
71
|
-
window.history.pushState(state, "", href);
|
|
72
|
-
return;
|
|
73
|
-
}
|
|
74
|
-
memory.entries = [...memory.entries.slice(0, memory.idx + 1), { href, state }];
|
|
75
|
-
memory.idx = memory.entries.length - 1;
|
|
45
|
+
window.history.pushState(state, "", href);
|
|
76
46
|
}
|
|
77
47
|
|
|
78
48
|
replace(state: unknown, href?: string) {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
if (href === undefined) window.history.replaceState(state, "");
|
|
82
|
-
else window.history.replaceState(state, "", href);
|
|
83
|
-
return;
|
|
84
|
-
}
|
|
85
|
-
const current = memory.entries[memory.idx];
|
|
86
|
-
memory.entries[memory.idx] = { href: href ?? current?.href ?? "/", state };
|
|
49
|
+
if (href === undefined) window.history.replaceState(state, "");
|
|
50
|
+
else window.history.replaceState(state, "", href);
|
|
87
51
|
}
|
|
88
52
|
|
|
89
53
|
back() {
|
|
90
|
-
|
|
91
|
-
if (!memory) {
|
|
92
|
-
window.history.back();
|
|
93
|
-
return;
|
|
94
|
-
}
|
|
95
|
-
if (memory.idx === 0) return;
|
|
96
|
-
memory.idx -= 1;
|
|
97
|
-
const state = memory.entries[memory.idx]?.state ?? null;
|
|
98
|
-
|
|
99
|
-
setTimeout(() => window.dispatchEvent(new PopStateEvent("popstate", { state })), 0);
|
|
54
|
+
window.history.back();
|
|
100
55
|
}
|
|
101
56
|
|
|
102
57
|
assign(href: string) {
|
|
103
|
-
|
|
104
|
-
window.location.assign(href);
|
|
105
|
-
return;
|
|
106
|
-
}
|
|
107
|
-
this.#owner.leave(href);
|
|
58
|
+
window.location.assign(href);
|
|
108
59
|
}
|
|
109
60
|
|
|
110
61
|
reload() {
|
|
111
|
-
|
|
112
|
-
else this.#owner.reload();
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
#entry() {
|
|
116
|
-
const memory = this.#memory;
|
|
117
|
-
return memory ? memory.entries[memory.idx] : undefined;
|
|
62
|
+
window.location.reload();
|
|
118
63
|
}
|
|
119
64
|
}
|
|
120
65
|
|
package/client/csrTypes.ts
CHANGED
|
@@ -109,6 +109,13 @@ export type PageLoadingRender = (props: PageLoadingProps) => PromiseOrObject<Rea
|
|
|
109
109
|
export type LayoutLoadingRender = (props: LayoutLoadingProps) => PromiseOrObject<ReactNode>;
|
|
110
110
|
export type LayoutNotFoundRender = (props: LayoutNotFoundProps) => PromiseOrObject<ReactNode>;
|
|
111
111
|
export type LayoutErrorRender = (props: LayoutErrorProps) => PromiseOrObject<ReactNode>;
|
|
112
|
+
export interface RouteGuardProps {
|
|
113
|
+
params: Record<string, string>;
|
|
114
|
+
searchParams: Record<string, string | string[]>;
|
|
115
|
+
/** MCP is listing the screen, so no argument is known: a guard reading one throws `RouteArgPending`. */
|
|
116
|
+
listing?: boolean;
|
|
117
|
+
}
|
|
118
|
+
export type RouteGuard = (props: RouteGuardProps) => Promise<void>;
|
|
112
119
|
export interface RouteRender {
|
|
113
120
|
render: LayoutRender | PageRender;
|
|
114
121
|
isAsync?: boolean;
|
|
@@ -125,6 +132,8 @@ export interface RouteRender {
|
|
|
125
132
|
resolveHead?: ResolveHead;
|
|
126
133
|
/** Parses the declared args without rendering, so a path value the type refuses answers not-found before any byte. */
|
|
127
134
|
checkArgs?: (props: PageProps) => Promise<void>;
|
|
135
|
+
/** The route's `.guard()`, which `RouteGuards.run` calls root layout first, ahead of every layout body. */
|
|
136
|
+
guard?: RouteGuard;
|
|
128
137
|
getPageConfig?: () => PromiseOrObject<PageConfig | undefined>;
|
|
129
138
|
getLayoutPageConfig?: () => PromiseOrObject<PageConfig | undefined>;
|
|
130
139
|
/** A root layout's `theme`, readable before it renders. */
|
|
@@ -175,12 +184,14 @@ export interface PageModule {
|
|
|
175
184
|
head?: Head;
|
|
176
185
|
generateHead?: GenerateHead;
|
|
177
186
|
Loading?: PageLoadingRender;
|
|
187
|
+
guard?: RouteGuard;
|
|
178
188
|
}
|
|
179
189
|
export interface LayoutModule {
|
|
180
190
|
default?: LayoutRender;
|
|
181
191
|
pageConfig?: PageConfig;
|
|
182
192
|
head?: Head;
|
|
183
193
|
generateHead?: GenerateHead;
|
|
194
|
+
guard?: RouteGuard;
|
|
184
195
|
fonts?: ReactFont[];
|
|
185
196
|
manifest?: WebAppManifest;
|
|
186
197
|
theme?: string;
|
|
@@ -279,6 +290,12 @@ export interface History {
|
|
|
279
290
|
cachedLocationMap: Map<string, Location>;
|
|
280
291
|
idx: number;
|
|
281
292
|
dormant?: Set<string>;
|
|
293
|
+
account?: CsrStackAccount;
|
|
294
|
+
}
|
|
295
|
+
export interface CsrStackAccount {
|
|
296
|
+
token: string | null;
|
|
297
|
+
generation: number;
|
|
298
|
+
leaving?: { key: string; generation: number };
|
|
282
299
|
}
|
|
283
300
|
|
|
284
301
|
export interface RouterProps {
|
|
@@ -360,10 +377,6 @@ export const usePathCtx = () => {
|
|
|
360
377
|
return contextValues;
|
|
361
378
|
};
|
|
362
379
|
|
|
363
|
-
/** Inside a layout's own markup that an MCP view hides while the page is drawn on its own: frame chrome draws nothing. */
|
|
364
|
-
export const hiddenLayoutContext = sharedContext<boolean>("hiddenLayout", false);
|
|
365
|
-
export const useInHiddenLayout = () => useContext(hiddenLayoutContext);
|
|
366
|
-
|
|
367
380
|
export type PageActivity = "current" | "prev" | "pending" | "hidden";
|
|
368
381
|
export interface PageActivityState {
|
|
369
382
|
activity: PageActivity;
|
|
@@ -485,5 +498,7 @@ export interface LayoutFallbackRoute {
|
|
|
485
498
|
export interface RouteGuide {
|
|
486
499
|
pathSegment: string;
|
|
487
500
|
pathRoute?: PathRoute;
|
|
501
|
+
/** CSR: the page a path under this node that no route matches draws, made for the pathname that was asked for. */
|
|
502
|
+
notFoundRouteOf?: (pathname: string) => PathRoute;
|
|
488
503
|
children: { [key: string]: RouteGuide };
|
|
489
504
|
}
|
package/client/href.ts
CHANGED
|
@@ -25,6 +25,14 @@ export class Href {
|
|
|
25
25
|
return /^https?:/i.test(href);
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
/** Whether an http(s) href resolves, against `base`, to another origin than `base`'s. Any other href stays a path in
|
|
29
|
+
* the app; no base, as outside a request, leaves for anywhere. */
|
|
30
|
+
static leavesOrigin(href: string, base: string | undefined): boolean {
|
|
31
|
+
if (!Href.hasWebScheme(href)) return false;
|
|
32
|
+
const origin = base === undefined ? undefined : Href.#parse(base)?.origin;
|
|
33
|
+
return !origin || Href.#parse(href, base)?.origin !== origin;
|
|
34
|
+
}
|
|
35
|
+
|
|
28
36
|
/** Canonical absolute URL for an external href: follows the browser on an http(s) document base; on a native
|
|
29
37
|
* `app://` base an absolute URL resolves as written and a network path resolves against https:. */
|
|
30
38
|
static toExternalUrl(href: string, base: string): string {
|
package/client/makePageProto.tsx
CHANGED
|
@@ -28,11 +28,12 @@ const getPageInfo = (): { locale: string; path: string } => {
|
|
|
28
28
|
const activePath = Translator.getActivePath();
|
|
29
29
|
if (activePath) return { locale: activeLocale ?? defaultLocale, path: activePath };
|
|
30
30
|
if (getEnv().side !== "server") {
|
|
31
|
-
const
|
|
31
|
+
const pathname = Translator.getRenderPath() ?? csrLocation.pathname;
|
|
32
|
+
const [, firstSegment = "", ...rest] = pathname.split("/");
|
|
32
33
|
const hasLocalePrefix = localeSet.has(firstSegment);
|
|
33
34
|
|
|
34
35
|
const locale = activeLocale ?? (hasLocalePrefix ? firstSegment : defaultLocale);
|
|
35
|
-
return { locale, path:
|
|
36
|
+
return { locale, path: hasLocalePrefix ? `/${rest.join("/")}` : pathname };
|
|
36
37
|
}
|
|
37
38
|
const h = untrackedHeaders();
|
|
38
39
|
|
|
@@ -1,8 +1,13 @@
|
|
|
1
|
+
import type { PromiseOrObject } from "akanjs/base";
|
|
2
|
+
import { AppScope } from "akanjs/fetch";
|
|
3
|
+
import type { ReactNode } from "react";
|
|
1
4
|
import { type RouteArgsShape, RouteDefinition } from "./RouteDefinition";
|
|
2
5
|
import type {
|
|
6
|
+
RouteArgInfo,
|
|
3
7
|
RouteArgOption,
|
|
4
8
|
RouteArgType,
|
|
5
9
|
RouteArgValue,
|
|
10
|
+
RouteBaseArgs,
|
|
6
11
|
RoutePromptArgument,
|
|
7
12
|
RoutePromptMeta,
|
|
8
13
|
RouteSearchType,
|
|
@@ -13,7 +18,19 @@ interface PagePrompt {
|
|
|
13
18
|
description: string;
|
|
14
19
|
}
|
|
15
20
|
|
|
16
|
-
|
|
21
|
+
interface PageApp {
|
|
22
|
+
name: string;
|
|
23
|
+
description: string;
|
|
24
|
+
load: (args: never) => PromiseOrObject<ReactNode>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** `never` for a name the page's render already receives, so a clash fails to compile as well as at definition. */
|
|
28
|
+
type FreeName<Name extends string, Taken> = Name extends "lang" | keyof Taken ? never : Name;
|
|
29
|
+
|
|
30
|
+
export class PageDefinition<
|
|
31
|
+
Args extends RouteArgsShape = Record<never, never>,
|
|
32
|
+
Apps extends RouteArgsShape = Record<never, never>,
|
|
33
|
+
> extends RouteDefinition<Args> {
|
|
17
34
|
/** MCP names an entry by `^[A-Za-z0-9_-]{1,64}$`; a prompt name outside it never reaches a client's slash menu. */
|
|
18
35
|
static readonly promptName = /^[A-Za-z0-9_-]{1,64}$/;
|
|
19
36
|
/** Appended to a list argument's description: the flat string map of `prompts/get` has no other spelling. */
|
|
@@ -21,33 +38,109 @@ export class PageDefinition<Args extends RouteArgsShape = Record<never, never>>
|
|
|
21
38
|
|
|
22
39
|
readonly kind = "page" as const;
|
|
23
40
|
#prompt?: PagePrompt;
|
|
41
|
+
readonly #apps: PageApp[] = [];
|
|
24
42
|
|
|
25
|
-
param<Name extends string, Type extends RouteArgType>(
|
|
43
|
+
param<Name extends string, Type extends RouteArgType>(
|
|
44
|
+
name: Name & FreeName<Name, Apps>,
|
|
45
|
+
type: Type,
|
|
46
|
+
option: RouteArgOption = {},
|
|
47
|
+
) {
|
|
26
48
|
this.declare({ kind: "param", name, type, list: false, desc: option.desc });
|
|
27
|
-
return this as unknown as PageDefinition<Args & { [Key in Name]: RouteArgValue<Type> }>;
|
|
49
|
+
return this as unknown as PageDefinition<Args & { [Key in Name]: RouteArgValue<Type> }, Apps>;
|
|
28
50
|
}
|
|
29
51
|
|
|
30
|
-
search<Name extends string, Type extends RouteSearchType>(
|
|
52
|
+
search<Name extends string, Type extends RouteSearchType>(
|
|
53
|
+
name: Name & FreeName<Name, Apps>,
|
|
54
|
+
type: Type,
|
|
55
|
+
option: RouteArgOption = {},
|
|
56
|
+
) {
|
|
31
57
|
const list = Array.isArray(type);
|
|
32
58
|
this.declare({ kind: "search", name, type: (list ? type[0] : type) as RouteArgType, list, desc: option.desc });
|
|
33
|
-
return this as unknown as PageDefinition<Args & { [Key in Name]?: RouteArgValue<Type> }>;
|
|
59
|
+
return this as unknown as PageDefinition<Args & { [Key in Name]?: RouteArgValue<Type> }, Apps>;
|
|
34
60
|
}
|
|
35
61
|
|
|
36
62
|
/** Publishes this screen as an MCP prompt: the description is the whole instruction a model receives. English. */
|
|
37
63
|
prompt(name: string, description: string) {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
64
|
+
PageDefinition.#assertEntry("prompt", name, description);
|
|
65
|
+
if (this.#apps.some((app) => app.name === name))
|
|
66
|
+
throw new Error(`[route-convention] page() names "${name}" as both an app and its prompt; MCP lists both`);
|
|
41
67
|
this.#prompt = { name, description };
|
|
42
68
|
return this;
|
|
43
69
|
}
|
|
44
70
|
|
|
71
|
+
/**
|
|
72
|
+
* Publishes one component of this screen as an MCP App: a read-only tool whose arguments are the page's `.param()`
|
|
73
|
+
* and `.search()`, and whose result a host draws with `load`'s element. `load` may only query. On the web the
|
|
74
|
+
* element arrives as the render argument `name`. English description.
|
|
75
|
+
*/
|
|
76
|
+
app<Name extends string>(
|
|
77
|
+
name: Name & FreeName<Name, Args & Apps>,
|
|
78
|
+
description: string,
|
|
79
|
+
load: (args: RouteBaseArgs & Args) => PromiseOrObject<ReactNode>,
|
|
80
|
+
) {
|
|
81
|
+
PageDefinition.#assertEntry("app", name, description);
|
|
82
|
+
if (name === "lang")
|
|
83
|
+
throw new Error(`[route-convention] app "lang" would hide the lang every route receives; name it otherwise`);
|
|
84
|
+
if (this.args.some((arg) => arg.name === name) || this.#apps.some((app) => app.name === name))
|
|
85
|
+
throw new Error(
|
|
86
|
+
`[route-convention] page() names "${name}" twice; an app's element arrives as the render argument of its name`,
|
|
87
|
+
);
|
|
88
|
+
if (this.#prompt?.name === name)
|
|
89
|
+
throw new Error(`[route-convention] page() names "${name}" as both an app and its prompt; MCP lists both`);
|
|
90
|
+
this.#apps.push({ name, description, load: load as PageApp["load"] });
|
|
91
|
+
return this as unknown as PageDefinition<Args, Apps & { [Key in Name]: ReactNode }>;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
override render(render: (args: RouteBaseArgs & Args & Apps) => PromiseOrObject<ReactNode>) {
|
|
95
|
+
return super.render(render as never);
|
|
96
|
+
}
|
|
97
|
+
|
|
45
98
|
get promptMeta(): RoutePromptMeta | undefined {
|
|
46
99
|
if (!this.#prompt) return undefined;
|
|
47
|
-
|
|
100
|
+
return { ...this.#prompt, arguments: this.#entryArguments() };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
get appMetas(): RoutePromptMeta[] {
|
|
104
|
+
return this.#apps.map(({ name, description }) => ({ name, description, arguments: this.#entryArguments() }));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** One app's element for these args, alone: what an MCP call runs after the route guards, and its view draws. */
|
|
108
|
+
async loadApp(name: string, args: RouteBaseArgs & Args): Promise<ReactNode> {
|
|
109
|
+
const app = this.#apps.find((candidate) => candidate.name === name);
|
|
110
|
+
if (!app) throw new Error(`[route-convention] this page declares no app "${name}"`);
|
|
111
|
+
return await PageDefinition.#load(app, args);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
protected override declare(arg: RouteArgInfo) {
|
|
115
|
+
if (this.#apps.some((app) => app.name === arg.name))
|
|
116
|
+
throw new Error(
|
|
117
|
+
`[route-convention] page() names "${arg.name}" twice; an app's element arrives as the render argument of its name`,
|
|
118
|
+
);
|
|
119
|
+
super.declare(arg);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
protected override renderArgsOf(args: RouteBaseArgs & Args): PromiseOrObject<RouteBaseArgs & Args> {
|
|
123
|
+
if (!this.#apps.length) return args;
|
|
124
|
+
return Promise.all(this.#apps.map((app) => PageDefinition.#load(app, args))).then((elements) => ({
|
|
125
|
+
...args,
|
|
126
|
+
...Object.fromEntries(this.#apps.map((app, idx) => [app.name, elements[idx]])),
|
|
127
|
+
}));
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
#entryArguments(): RoutePromptArgument[] {
|
|
131
|
+
return this.args.map((arg) => {
|
|
48
132
|
const description = [arg.desc, arg.list ? PageDefinition.commaSeparated : undefined].filter(Boolean).join(" ");
|
|
49
133
|
return { name: arg.name, ...(description ? { description } : {}), required: arg.kind === "param" };
|
|
50
134
|
});
|
|
51
|
-
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
static async #load(app: PageApp, args: object): Promise<ReactNode> {
|
|
138
|
+
return await AppScope.run(app.name, () => app.load(args as never));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
static #assertEntry(kind: "prompt" | "app", name: string, description: string) {
|
|
142
|
+
if (!PageDefinition.promptName.test(name))
|
|
143
|
+
throw new Error(`[route-convention] ${kind} name "${name}" must match ${PageDefinition.promptName}`);
|
|
144
|
+
if (!description.trim()) throw new Error(`[route-convention] ${kind} "${name}" needs a description`);
|
|
52
145
|
}
|
|
53
146
|
}
|
|
@@ -9,9 +9,10 @@ import {
|
|
|
9
9
|
import { parseAkanI18nEnv } from "akanjs/common";
|
|
10
10
|
import { deserialize } from "akanjs/constant";
|
|
11
11
|
import type { ReactNode } from "react";
|
|
12
|
-
import type { Head, LayoutModule, PageConfig, PageModule } from "../csrTypes";
|
|
12
|
+
import type { Head, LayoutModule, PageConfig, PageModule, RouteGuardProps } from "../csrTypes";
|
|
13
13
|
import { AkanNotFoundError } from "../router";
|
|
14
|
-
import {
|
|
14
|
+
import { Translator } from "../translator";
|
|
15
|
+
import { RouteArgError, type RouteArgInfo, type RouteArgInput, RouteArgPending, type RouteBaseArgs } from "./routeArgs";
|
|
15
16
|
|
|
16
17
|
export type RouteArgsShape = Record<string, unknown>;
|
|
17
18
|
export type RouteKind = "page" | "layout" | "rootLayout";
|
|
@@ -44,6 +45,7 @@ export abstract class RouteDefinition<
|
|
|
44
45
|
#config?: PageConfig;
|
|
45
46
|
|
|
46
47
|
#head?: HeadStage<never>;
|
|
48
|
+
#guard?: (args: never) => void | Promise<void>;
|
|
47
49
|
#loading?: (args: never) => PromiseOrObject<ReactNode>;
|
|
48
50
|
#render?: (args: never) => PromiseOrObject<ReactNode>;
|
|
49
51
|
#renderRoute?: {
|
|
@@ -60,6 +62,13 @@ export abstract class RouteDefinition<
|
|
|
60
62
|
this.#head = head as HeadStage<never>;
|
|
61
63
|
return this;
|
|
62
64
|
}
|
|
65
|
+
/** Runs before any layout body; refuses with `router.redirect()` or `router.notFound()`. */
|
|
66
|
+
guard(guard: (args: RouteBaseArgs & Args) => void | Promise<void>) {
|
|
67
|
+
if (this.#guard)
|
|
68
|
+
throw new Error(`[route-convention] ${this.kind}() declares .guard() twice; run every check from one guard`);
|
|
69
|
+
this.#guard = guard as (args: never) => void | Promise<void>;
|
|
70
|
+
return this;
|
|
71
|
+
}
|
|
63
72
|
loading(render: (args: RouteBaseArgs & Args & Extra) => PromiseOrObject<ReactNode>) {
|
|
64
73
|
this.#loading = render as (args: never) => PromiseOrObject<ReactNode>;
|
|
65
74
|
return this;
|
|
@@ -127,10 +136,12 @@ export abstract class RouteDefinition<
|
|
|
127
136
|
if (!render)
|
|
128
137
|
throw new Error(`[route-convention] a ${this.kind}() chain ends with .render(), and this one has none`);
|
|
129
138
|
const head = this.#head;
|
|
139
|
+
const guard = this.#guard;
|
|
130
140
|
const loading = this.#loading;
|
|
131
141
|
const module: PageModule & LayoutModule = {
|
|
132
142
|
default: this.#renderRouteOf(render) as never,
|
|
133
143
|
...(this.#config ? { pageConfig: this.#config } : {}),
|
|
144
|
+
...(guard ? { guard: this.#guardRouteOf(guard) } : {}),
|
|
134
145
|
...(head === undefined
|
|
135
146
|
? {}
|
|
136
147
|
: typeof head === "function"
|
|
@@ -150,10 +161,28 @@ export abstract class RouteDefinition<
|
|
|
150
161
|
return module;
|
|
151
162
|
}
|
|
152
163
|
|
|
164
|
+
/** What `.render()` receives beyond the declared args; a page adds its apps' elements. */
|
|
165
|
+
protected renderArgsOf(args: RouteBaseArgs & Args & Extra): PromiseOrObject<RouteBaseArgs & Args & Extra> {
|
|
166
|
+
return args;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
#guardRouteOf(guard: (args: never) => void | Promise<void>) {
|
|
170
|
+
const route = async (props: RouteGuardProps) => await guard(this.#guardArgsOf(props) as never);
|
|
171
|
+
Object.defineProperty(route, routeArgsMarker, { value: this.args.map(({ kind, name }) => ({ kind, name })) });
|
|
172
|
+
return route;
|
|
173
|
+
}
|
|
174
|
+
|
|
153
175
|
#renderRouteOf(render: (args: never) => PromiseOrObject<ReactNode>) {
|
|
154
176
|
const kept = this.#renderRoute;
|
|
155
177
|
if (kept?.render === render && kept.argCount === this.args.length) return kept.route;
|
|
156
|
-
|
|
178
|
+
|
|
179
|
+
const route = async (props: RouteRenderProps) => {
|
|
180
|
+
const args = this.renderArgsOf(this.#argsOf(props));
|
|
181
|
+
if (!(args instanceof Promise)) return await render(args as never);
|
|
182
|
+
const path = Translator.getRenderPath();
|
|
183
|
+
const resolved = await args;
|
|
184
|
+
return await Translator.renderAt(path, () => render(resolved as never));
|
|
185
|
+
};
|
|
157
186
|
Object.defineProperty(route, routeArgsMarker, { value: this.args.map(({ kind, name }) => ({ kind, name })) });
|
|
158
187
|
this.#renderRoute = { render, argCount: this.args.length, route };
|
|
159
188
|
return route;
|
|
@@ -190,6 +219,18 @@ export abstract class RouteDefinition<
|
|
|
190
219
|
return (this.kind === "page" ? args : { ...args, children: props.children }) as RouteBaseArgs & Args & Extra;
|
|
191
220
|
}
|
|
192
221
|
|
|
222
|
+
#guardArgsOf({ params, searchParams, listing }: RouteGuardProps): RouteBaseArgs & Args {
|
|
223
|
+
const { lang = parseAkanI18nEnv().defaultLocale } = params;
|
|
224
|
+
if (!listing) return { lang, ...this.resolveArgs({ params, searchParams }) } as RouteBaseArgs & Args;
|
|
225
|
+
const declared = new Set(this.args.map((arg) => arg.name));
|
|
226
|
+
return new Proxy({ lang } as RouteBaseArgs & Args, {
|
|
227
|
+
get: (target, key, receiver) => {
|
|
228
|
+
if (typeof key === "string" && declared.has(key)) throw new RouteArgPending(key);
|
|
229
|
+
return Reflect.get(target, key, receiver);
|
|
230
|
+
},
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
|
|
193
234
|
/** One value where a list was declared is a one-item list (`?tags=a`); under `strict` a list is comma-separated. */
|
|
194
235
|
static #lift(arg: RouteArgInfo, raw: string | string[], strict: boolean): unknown {
|
|
195
236
|
if (!arg.list) return Array.isArray(raw) ? raw[0] : raw;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { RouteGuard, RouteGuardProps, RouteRender } from "../csrTypes";
|
|
2
|
+
import { AkanNotFoundError } from "../router";
|
|
3
|
+
import { routeArgsMarker } from "./RouteDefinition";
|
|
4
|
+
|
|
5
|
+
/** Runs a route's `.guard()` stages: the root layout's first, the page's last, each before any layout body. */
|
|
6
|
+
export class RouteGuards {
|
|
7
|
+
static readonly #refusedAt = new WeakMap<object, number>();
|
|
8
|
+
|
|
9
|
+
/** The first refusal stops the chain and is thrown: a redirect, a not-found, or the error a guard leaked. */
|
|
10
|
+
static async run(renders: (RouteRender | undefined)[], props: RouteGuardProps): Promise<void> {
|
|
11
|
+
for (const [idx, render] of renders.entries()) {
|
|
12
|
+
if (!render?.guard) continue;
|
|
13
|
+
try {
|
|
14
|
+
await render.guard(props);
|
|
15
|
+
} catch (error) {
|
|
16
|
+
const refusal = RouteGuards.refusalOf(error);
|
|
17
|
+
|
|
18
|
+
const thrown = typeof refusal === "object" && refusal !== null ? refusal : new Error(String(refusal));
|
|
19
|
+
RouteGuards.#refusedAt.set(thrown, idx);
|
|
20
|
+
throw thrown;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** The index, in the renders `run` was given, whose guard threw `error`; `undefined` for anything else. */
|
|
26
|
+
static refusedAt(error: unknown): number | undefined {
|
|
27
|
+
return typeof error === "object" && error !== null ? RouteGuards.#refusedAt.get(error) : undefined;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Whether MCP may list the screen to this caller: a guard that reads an argument leaves the verdict to the call. */
|
|
31
|
+
static async admits(renders: (RouteRender | undefined)[], props: RouteGuardProps): Promise<boolean> {
|
|
32
|
+
for (const render of renders) {
|
|
33
|
+
if (!render?.guard) continue;
|
|
34
|
+
try {
|
|
35
|
+
await render.guard({ ...props, listing: true });
|
|
36
|
+
} catch (error) {
|
|
37
|
+
if (!RouteGuards.isArgPending(error)) return false;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
static refusalOf(error: unknown): unknown {
|
|
44
|
+
const status = (error as { statusCode?: unknown } | null)?.statusCode;
|
|
45
|
+
return status === 403 || status === 404 ? new AkanNotFoundError() : error;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
static isArgPending(error: unknown): boolean {
|
|
49
|
+
return (error as { digest?: unknown } | null)?.digest === "AKAN_ROUTE_ARG_PENDING";
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** One guard running each of `guards` in order, or none when no guard is given. */
|
|
53
|
+
static compose(...guards: (RouteGuard | undefined)[]): RouteGuard | undefined {
|
|
54
|
+
const chain = guards.filter((guard): guard is RouteGuard => !!guard);
|
|
55
|
+
if (chain.length <= 1) return chain[0];
|
|
56
|
+
const composed: RouteGuard = async (props) => {
|
|
57
|
+
for (const guard of chain) await guard(props);
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const reads = chain.map((guard) => (guard as { [routeArgsMarker]?: unknown[] })[routeArgsMarker]);
|
|
61
|
+
if (reads.every(Array.isArray)) Object.defineProperty(composed, routeArgsMarker, { value: reads.flat() });
|
|
62
|
+
return composed;
|
|
63
|
+
}
|
|
64
|
+
}
|
package/client/route/index.ts
CHANGED
|
@@ -2,6 +2,7 @@ export * from "./LayoutDefinition";
|
|
|
2
2
|
export * from "./PageDefinition";
|
|
3
3
|
export * from "./RootLayoutDefinition";
|
|
4
4
|
export * from "./RouteDefinition";
|
|
5
|
+
export * from "./RouteGuards";
|
|
5
6
|
export * from "./RouteHead";
|
|
6
7
|
export * from "./resolveRouteModule";
|
|
7
8
|
export * from "./routeArgs";
|
|
@@ -40,3 +40,11 @@ export const resolveRouteModule = (
|
|
|
40
40
|
if (pattern) definition.assertPattern(pattern, key);
|
|
41
41
|
return { module: definition.toRouteModule(), definition };
|
|
42
42
|
};
|
|
43
|
+
|
|
44
|
+
/** The export sets allow `guard` for an unfolded chain and the generated root layout; the legacy shape never gained it. */
|
|
45
|
+
export const assertGuardOnChain = (label: string, resolved: ResolvedRouteModule): ResolvedRouteModule => {
|
|
46
|
+
if (resolved.definition || !("guard" in resolved.module)) return resolved;
|
|
47
|
+
throw new Error(
|
|
48
|
+
`[route-convention] ${label} exports guard beside a legacy default; declare it with .guard() on its chain`,
|
|
49
|
+
);
|
|
50
|
+
};
|
|
@@ -49,3 +49,12 @@ export class RouteArgError extends Error {
|
|
|
49
49
|
this.arg = arg;
|
|
50
50
|
}
|
|
51
51
|
}
|
|
52
|
+
|
|
53
|
+
/** A guard read an argument while MCP listed its screen, when none is known yet: the call decides with them. */
|
|
54
|
+
export class RouteArgPending extends Error {
|
|
55
|
+
readonly digest = "AKAN_ROUTE_ARG_PENDING";
|
|
56
|
+
constructor(name: string) {
|
|
57
|
+
super(`Argument "${name}" is not known while the screen is listed`);
|
|
58
|
+
this.name = "RouteArgPending";
|
|
59
|
+
}
|
|
60
|
+
}
|