akanjs 3.0.0-alpha.5 → 3.0.0-alpha.7
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/client/clientRuntime.ts +8 -0
- package/client/cn.ts +7 -0
- package/client/csrTypes.ts +2 -0
- package/client/frameDebug.ts +3 -2
- package/common/index.ts +5 -0
- package/common/routeConvention.ts +30 -0
- package/document/into.ts +2 -0
- package/fetch/client/fetchClient.ts +26 -2
- package/fetch/client/wsClient.ts +17 -2
- package/package.json +1 -1
- package/server/resolver/CascadeRunner.ts +1 -1
- package/server/resolver/database.resolver.ts +3 -0
- package/server/routeTreeBuilder.ts +2 -44
- package/server/webRouter.ts +96 -0
- package/store/types.ts +7 -0
- package/store/withSelector.ts +3 -5
- package/types/client/cn.d.ts +5 -0
- package/types/client/csrTypes.d.ts +2 -0
- package/types/common/index.d.ts +1 -1
- package/types/common/routeConvention.d.ts +8 -0
- package/types/document/into.d.ts +2 -0
- package/types/fetch/client/fetchClient.d.ts +1 -0
- package/types/store/baseSt.d.ts +1 -1
- package/types/store/types.d.ts +4 -0
- package/types/store/withSelector.d.ts +3 -5
- package/types/ui/CsrImage.d.ts +1 -1
- package/types/ui/Dropdown.d.ts +2 -0
- package/types/ui/Image.d.ts +3 -3
- package/types/ui/Layout/BottomInset.d.ts +2 -1
- package/types/ui/Layout/index.d.ts +1 -1
- package/types/ui/Signal/style.d.ts +1 -1
- package/types/ui/UiOverride/context.d.ts +12 -2
- package/types/ui/index.d.ts +2 -1
- package/types/ui/overlayLayer.d.ts +24 -0
- package/types/ui/recipe/badgeRecipe.d.ts +7 -3
- package/types/ui/recipe/buttonRecipe.d.ts +7 -3
- package/types/ui/recipe/inputRecipe.d.ts +5 -1
- package/types/webkit/index.d.ts +1 -0
- package/types/webkit/lazy.d.ts +12 -0
- package/types/webkit/useCsrValues.d.ts +3 -3
- package/types/webkit/useEscapeKey.d.ts +5 -0
- package/types/webkit/useFrameRuntime.d.ts +6 -1
- package/ui/Badge.tsx +2 -2
- package/ui/BottomSheet.tsx +5 -0
- package/ui/Button.tsx +3 -1
- package/ui/Constant/Doc.tsx +1 -1
- package/ui/CsrImage.tsx +1 -1
- package/ui/Data/ListContainer.tsx +1 -1
- package/ui/DatePicker.tsx +4 -3
- package/ui/Dialog/Modal.tsx +12 -15
- package/ui/DraggableList.tsx +3 -1
- package/ui/Dropdown.tsx +33 -11
- package/ui/Field.tsx +14 -8
- package/ui/Image.tsx +3 -3
- package/ui/Input.tsx +7 -5
- package/ui/Layout/BottomInset.tsx +6 -1
- package/ui/Loading/ProgressBar.tsx +8 -1
- package/ui/Menu.tsx +7 -8
- package/ui/Model/EditModal.tsx +37 -2
- package/ui/Model/SureToRemove.tsx +2 -1
- package/ui/Model/index_.tsx +42 -15
- package/ui/ObjectId.tsx +3 -4
- package/ui/Pagination.tsx +3 -4
- package/ui/Popconfirm.tsx +8 -7
- package/ui/Select.tsx +4 -3
- package/ui/System/CSR.tsx +6 -2
- package/ui/ToggleSelect.tsx +4 -3
- package/ui/Tooltip.tsx +2 -1
- package/ui/UiOverride/context.ts +12 -2
- package/ui/index.ts +8 -1
- package/ui/overlayLayer.ts +39 -0
- package/ui/recipe/badgeRecipe.ts +22 -3
- package/ui/recipe/buttonRecipe.ts +51 -2
- package/ui/recipe/factory.ts +2 -2
- package/ui/recipe/inputRecipe.ts +18 -4
- package/webkit/bootCsr.tsx +2 -33
- package/webkit/index.ts +1 -0
- package/webkit/lazy.tsx +22 -2
- package/webkit/useCsrValues.ts +121 -4
- package/webkit/useEscapeKey.tsx +42 -0
- package/webkit/useFrameRuntime.ts +65 -32
package/client/clientRuntime.ts
CHANGED
|
@@ -75,6 +75,13 @@ globalWithRuntime[CLIENT_RUNTIME_KEY] = state;
|
|
|
75
75
|
const missingRuntimeError = () =>
|
|
76
76
|
new Error("Akan client runtime is not registered. Import the generated app client first.");
|
|
77
77
|
|
|
78
|
+
const applyRuntimeErrorConstructor = (runtime: ClientRuntime) => {
|
|
79
|
+
const instance = (runtime.fetch as RuntimeFetch | undefined)?.instance as
|
|
80
|
+
| { setErrorConstructor?: (Err: unknown) => void }
|
|
81
|
+
| undefined;
|
|
82
|
+
if (typeof instance?.setErrorConstructor === "function") instance.setErrorConstructor(runtime.Err);
|
|
83
|
+
};
|
|
84
|
+
|
|
78
85
|
export const registerClientRuntime = <Runtime>(
|
|
79
86
|
runtime: Runtime,
|
|
80
87
|
{ scope = "app" }: { scope?: RuntimeScope } = {},
|
|
@@ -82,6 +89,7 @@ export const registerClientRuntime = <Runtime>(
|
|
|
82
89
|
if (state.scope === "app" && scope === "lib") return runtime;
|
|
83
90
|
state.runtime = runtime as ClientRuntime;
|
|
84
91
|
state.scope = scope;
|
|
92
|
+
applyRuntimeErrorConstructor(state.runtime);
|
|
85
93
|
return runtime;
|
|
86
94
|
};
|
|
87
95
|
|
package/client/cn.ts
CHANGED
|
@@ -38,10 +38,17 @@ export const colorTokens = [
|
|
|
38
38
|
"ring",
|
|
39
39
|
];
|
|
40
40
|
|
|
41
|
+
/**
|
|
42
|
+
* Akan's semantic radius tokens (`--radius-box` / `--radius-field` in ui/styles.css). Without them
|
|
43
|
+
* `cn("rounded-field", "rounded-full")` keeps both classes and stylesheet order decides the winner.
|
|
44
|
+
*/
|
|
45
|
+
export const radiusTokens = ["box", "field"];
|
|
46
|
+
|
|
41
47
|
const twMerge = extendTailwindMerge({
|
|
42
48
|
extend: {
|
|
43
49
|
theme: {
|
|
44
50
|
color: colorTokens,
|
|
51
|
+
radius: radiusTokens,
|
|
45
52
|
},
|
|
46
53
|
},
|
|
47
54
|
});
|
package/client/csrTypes.ts
CHANGED
|
@@ -455,6 +455,7 @@ export interface FrameLayoutState {
|
|
|
455
455
|
keyboard: KeyboardFrameState;
|
|
456
456
|
contentViewport: FrameContentViewportState;
|
|
457
457
|
keyboardAccessory: KeyboardAccessoryFrameState;
|
|
458
|
+
contentAnchor?: "bottom";
|
|
458
459
|
platformProfile: FramePlatformProfile;
|
|
459
460
|
zIndex: FrameLayerZIndex;
|
|
460
461
|
pageStateByPath: Map<string, PageState>;
|
|
@@ -463,6 +464,7 @@ export interface FrameSlotRegistration {
|
|
|
463
464
|
scope?: FrameSlotScope;
|
|
464
465
|
type: FrameSlotType;
|
|
465
466
|
role?: FrameSlotRole;
|
|
467
|
+
contentAnchor?: "bottom";
|
|
466
468
|
height?: number;
|
|
467
469
|
estimatedHeight?: number;
|
|
468
470
|
source?: "navbar" | "topInset" | "bottomInset" | "bottomTab" | (string & {});
|
package/client/frameDebug.ts
CHANGED
|
@@ -20,9 +20,10 @@ const isFrameDebugEnabled = () => {
|
|
|
20
20
|
export function debugFrame(event: string, payload: DebugPayload = {}) {
|
|
21
21
|
if (!isFrameDebugEnabled()) return;
|
|
22
22
|
debugSeq += 1;
|
|
23
|
-
|
|
23
|
+
const details = {
|
|
24
24
|
href: window.location.href,
|
|
25
25
|
now: Math.round(performance.now()),
|
|
26
26
|
...payload,
|
|
27
|
-
}
|
|
27
|
+
};
|
|
28
|
+
console.info(`[akan:frame:${debugSessionId}:${debugSeq}] ${event}`, details, JSON.stringify(details));
|
|
28
29
|
}
|
package/common/index.ts
CHANGED
|
@@ -36,12 +36,17 @@ export { randomPicks } from "./randomPicks";
|
|
|
36
36
|
export {
|
|
37
37
|
assertUniqueRoutePatterns,
|
|
38
38
|
compareRouteSpecificity,
|
|
39
|
+
getRouteExports,
|
|
39
40
|
isRouteSourceFile,
|
|
40
41
|
isSpecialRouteLeaf,
|
|
42
|
+
LAYOUT_ROUTE_EXPORTS,
|
|
41
43
|
matchRoutePattern,
|
|
42
44
|
normalizeRoutePattern,
|
|
45
|
+
PAGE_ROUTE_EXPORTS,
|
|
43
46
|
type ParsedRouteModuleKey,
|
|
44
47
|
parseRouteModuleKey,
|
|
48
|
+
RESERVED_ROUTE_CONFIG_EXPORTS,
|
|
49
|
+
ROOT_LAYOUT_ROUTE_EXPORTS,
|
|
45
50
|
type RouteModuleKind,
|
|
46
51
|
routeSegmentToPatternPart,
|
|
47
52
|
routeSegmentToTreePath,
|
|
@@ -9,6 +9,36 @@ const DIRECTORY_SCOPED_LEAVES = new Set(["_layout", "_index", "_overrides"]);
|
|
|
9
9
|
|
|
10
10
|
export type RouteModuleKind = "page" | "layout" | "overrides";
|
|
11
11
|
|
|
12
|
+
export const PAGE_ROUTE_EXPORTS: ReadonlySet<string> = new Set([
|
|
13
|
+
"default",
|
|
14
|
+
"pageConfig",
|
|
15
|
+
"head",
|
|
16
|
+
"metadata",
|
|
17
|
+
"generateHead",
|
|
18
|
+
"generateMetadata",
|
|
19
|
+
"Loading",
|
|
20
|
+
]);
|
|
21
|
+
export const LAYOUT_ROUTE_EXPORTS: ReadonlySet<string> = new Set([...PAGE_ROUTE_EXPORTS, "NotFound", "Error"]);
|
|
22
|
+
export const ROOT_LAYOUT_ROUTE_EXPORTS: ReadonlySet<string> = new Set([
|
|
23
|
+
...LAYOUT_ROUTE_EXPORTS,
|
|
24
|
+
"fonts",
|
|
25
|
+
"manifest",
|
|
26
|
+
"theme",
|
|
27
|
+
"reconnect",
|
|
28
|
+
"wsConnect",
|
|
29
|
+
"layoutStyle",
|
|
30
|
+
"gaTrackingId",
|
|
31
|
+
]);
|
|
32
|
+
/** Root-layout exports that are plain config rather than components, so a PascalCase check cannot allow them. */
|
|
33
|
+
export const RESERVED_ROUTE_CONFIG_EXPORTS: ReadonlySet<string> = new Set(
|
|
34
|
+
[...ROOT_LAYOUT_ROUTE_EXPORTS].filter((name) => name !== "default" && !/^[A-Z]/.test(name)),
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
export function getRouteExports(kind: "page" | "layout", { rootLayout = false } = {}): ReadonlySet<string> {
|
|
38
|
+
if (kind === "page") return PAGE_ROUTE_EXPORTS;
|
|
39
|
+
return rootLayout ? ROOT_LAYOUT_ROUTE_EXPORTS : LAYOUT_ROUTE_EXPORTS;
|
|
40
|
+
}
|
|
41
|
+
|
|
12
42
|
export interface ParsedRouteModuleKey {
|
|
13
43
|
key: string;
|
|
14
44
|
kind: RouteModuleKind;
|
package/document/into.ts
CHANGED
|
@@ -88,6 +88,8 @@ export type Mdl<
|
|
|
88
88
|
updateMany(query: _RawQuery, update: DocumentUpdateInput<_RawDoc>): Promise<UpdateResult>;
|
|
89
89
|
removeOne(query: _RawQuery): Promise<UpdateResult>;
|
|
90
90
|
removeMany(query: _RawQuery): Promise<UpdateResult>;
|
|
91
|
+
updateById(id: string, update: DocumentUpdateInput<_RawDoc>, options?: DocumentUpdateOptions): Promise<UpdateResult>;
|
|
92
|
+
removeById(id: string): Promise<UpdateResult>;
|
|
91
93
|
bulkWrite(operations: BulkWriteOperation<Raw, _RawDoc, _RawQuery>[]): Promise<UpdateResult>;
|
|
92
94
|
/** @deprecated Renamed to `count`. */
|
|
93
95
|
countDocuments(query: _RawQuery): Promise<number>;
|
|
@@ -37,6 +37,16 @@ export type FetchProxy<
|
|
|
37
37
|
FetchClient &
|
|
38
38
|
FetchType & { slice: SliceMetaObj; instance: FetchClient; _FetchType: FetchType; _SliceMetaObj: SliceMetaObj };
|
|
39
39
|
|
|
40
|
+
interface SharedClientState {
|
|
41
|
+
proxy: FetchProxy | null;
|
|
42
|
+
origin: string | null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const SHARED_CLIENT_KEY = Symbol.for("akanjs.fetch.sharedClient");
|
|
46
|
+
const globalWithSharedClient = globalThis as typeof globalThis & { [SHARED_CLIENT_KEY]?: SharedClientState };
|
|
47
|
+
const sharedClientState: SharedClientState = globalWithSharedClient[SHARED_CLIENT_KEY] ?? { proxy: null, origin: null };
|
|
48
|
+
globalWithSharedClient[SHARED_CLIENT_KEY] = sharedClientState;
|
|
49
|
+
|
|
40
50
|
type ClientSignalMap<SigType extends { fetch: any }> = {
|
|
41
51
|
[K in keyof SigType as SigType[K] extends DatabaseSignal<any, any, any, any>
|
|
42
52
|
? K
|
|
@@ -80,6 +90,19 @@ export class FetchClient {
|
|
|
80
90
|
FetchClient.#sharedSerializedSignal = {};
|
|
81
91
|
FetchClient.#sharedRegistryVersion++;
|
|
82
92
|
}
|
|
93
|
+
static resetSharedClient() {
|
|
94
|
+
sharedClientState.proxy = null;
|
|
95
|
+
sharedClientState.origin = null;
|
|
96
|
+
}
|
|
97
|
+
static #resolveSharedClientProxy(origin: string, Err?: ErrorConstructor) {
|
|
98
|
+
if (typeof window === "undefined") return null;
|
|
99
|
+
|
|
100
|
+
if (sharedClientState.proxy) return sharedClientState.origin === origin ? sharedClientState.proxy : null;
|
|
101
|
+
const proxy = FetchClient.#makeProxy<unknown, Record<string, SliceMeta>>(new FetchClient(origin, {}, {}, Err));
|
|
102
|
+
sharedClientState.proxy = proxy;
|
|
103
|
+
sharedClientState.origin = origin;
|
|
104
|
+
return proxy;
|
|
105
|
+
}
|
|
83
106
|
static #mergeSerializedSignalInto(
|
|
84
107
|
serializedSignal: { [key: string]: SerializedSignal },
|
|
85
108
|
refName: string,
|
|
@@ -700,10 +723,11 @@ export class FetchClient {
|
|
|
700
723
|
sig: ClientSignalMap<SigType>;
|
|
701
724
|
fetch: SigType["fetch"];
|
|
702
725
|
} {
|
|
703
|
-
|
|
726
|
+
const shared = base ?? FetchClient.#resolveSharedClientProxy(origin, Err);
|
|
727
|
+
if (shared) shared.instance.applySignal(serializedSignal);
|
|
704
728
|
if (base && Err) base.instance.setErrorConstructor(Err);
|
|
705
729
|
const proxy =
|
|
706
|
-
|
|
730
|
+
shared ??
|
|
707
731
|
FetchClient.#makeProxy<unknown, Record<string, SliceMeta>>(new FetchClient(origin, {}, serializedSignal, Err));
|
|
708
732
|
if (connect) proxy.instance.connect();
|
|
709
733
|
const sig = {} as any;
|
package/fetch/client/wsClient.ts
CHANGED
|
@@ -40,6 +40,8 @@ export class WsClient {
|
|
|
40
40
|
#roomSubscribeMap = new Map<string, SubscribeOption>();
|
|
41
41
|
#listenerMap = new Map<string, Set<Listener>>();
|
|
42
42
|
#destroyed = false;
|
|
43
|
+
#connectRequested = false;
|
|
44
|
+
#unconnectedWarnTimers = new Map<string, ReturnType<typeof setTimeout>>();
|
|
43
45
|
#jwt: string | null = null;
|
|
44
46
|
connected = false;
|
|
45
47
|
|
|
@@ -70,6 +72,7 @@ export class WsClient {
|
|
|
70
72
|
}
|
|
71
73
|
|
|
72
74
|
connect() {
|
|
75
|
+
this.#connectRequested = true;
|
|
73
76
|
if (this.#ws && this.#ws.readyState !== WebSocket.CLOSED) return;
|
|
74
77
|
this.logger.debug(`Connecting to ${this.url}`);
|
|
75
78
|
this.#destroyed = false;
|
|
@@ -187,10 +190,13 @@ export class WsClient {
|
|
|
187
190
|
destroy() {
|
|
188
191
|
this.logger.debug(`WebSocket destroying`);
|
|
189
192
|
this.#destroyed = true;
|
|
193
|
+
this.#connectRequested = false;
|
|
190
194
|
if (this.#reconnectTimer) {
|
|
191
195
|
clearTimeout(this.#reconnectTimer);
|
|
192
196
|
this.#reconnectTimer = null;
|
|
193
197
|
}
|
|
198
|
+
for (const timer of this.#unconnectedWarnTimers.values()) clearTimeout(timer);
|
|
199
|
+
this.#unconnectedWarnTimers.clear();
|
|
194
200
|
this.#ws?.close();
|
|
195
201
|
this.#ws = null;
|
|
196
202
|
}
|
|
@@ -231,9 +237,18 @@ export class WsClient {
|
|
|
231
237
|
}
|
|
232
238
|
#warnNotConnected(action: "emit" | "subscribe", key: string) {
|
|
233
239
|
console.warn(
|
|
234
|
-
`[akanjs] WebSocket is not connected. Call fetch.instance.connect() or
|
|
240
|
+
`[akanjs] WebSocket is not connected. Call fetch.instance.connect(), or drop the root layout "wsConnect = false", before ${action} "${key}".`,
|
|
235
241
|
);
|
|
236
242
|
}
|
|
243
|
+
#warnUnconnectedSubscribe(key: string) {
|
|
244
|
+
if (this.#connectRequested || this.#unconnectedWarnTimers.has(key)) return;
|
|
245
|
+
const timer = setTimeout(() => {
|
|
246
|
+
this.#unconnectedWarnTimers.delete(key);
|
|
247
|
+
if (this.#connectRequested || this.#destroyed) return;
|
|
248
|
+
this.#warnNotConnected("subscribe", key);
|
|
249
|
+
}, 0);
|
|
250
|
+
this.#unconnectedWarnTimers.set(key, timer);
|
|
251
|
+
}
|
|
237
252
|
emit(key: string, data: WsRequestPayload) {
|
|
238
253
|
if (this.#ws?.readyState !== WebSocket.OPEN) {
|
|
239
254
|
this.logger.warn("WebSocket not connected");
|
|
@@ -246,7 +261,7 @@ export class WsClient {
|
|
|
246
261
|
}
|
|
247
262
|
subscribe(option: { key: string; data: unknown[]; handleEvent: (data: unknown) => void }) {
|
|
248
263
|
const roomId = WsClient.makeRoomId(option.key, option.data);
|
|
249
|
-
if (!this.#ws) this.#
|
|
264
|
+
if (!this.#ws) this.#warnUnconnectedSubscribe(option.key);
|
|
250
265
|
if (!this.#roomSubscribeMap.has(roomId)) {
|
|
251
266
|
this.#roomSubscribeMap.set(roomId, { key: option.key, data: option.data, listener: new Set() });
|
|
252
267
|
if (this.#ws?.readyState === WebSocket.OPEN) {
|
package/package.json
CHANGED
|
@@ -186,7 +186,7 @@ export class CascadeRunner {
|
|
|
186
186
|
}
|
|
187
187
|
if (!lines.length) return;
|
|
188
188
|
const bulk = lines.filter((line) => line.endsWith("(bulk)")).length;
|
|
189
|
-
this.#logger.
|
|
189
|
+
this.#logger.verbose(`${lines.length} cascade edge(s), ${bulk} in one query`);
|
|
190
190
|
for (const line of lines) this.#logger.verbose(line);
|
|
191
191
|
}
|
|
192
192
|
|
|
@@ -240,6 +240,9 @@ export class DatabaseResolver {
|
|
|
240
240
|
updateMany: (query: QueryOf<any>, update: DocumentUpdateInput) => store.updateManyByQuery(query, update),
|
|
241
241
|
removeOne: (query: QueryOf<any>) => store.removeOneByQuery(query),
|
|
242
242
|
removeMany: (query: QueryOf<any>) => store.removeManyByQuery(query),
|
|
243
|
+
updateById: (id: string, update: DocumentUpdateInput, options?: { upsert?: boolean }) =>
|
|
244
|
+
store.updateOneByQuery({ id }, update, options),
|
|
245
|
+
removeById: (id: string) => store.removeOneByQuery({ id }),
|
|
243
246
|
|
|
244
247
|
countDocuments: (query: QueryOf<any>) => store.count(query),
|
|
245
248
|
bulkWrite: (
|
|
@@ -13,6 +13,7 @@ import type {
|
|
|
13
13
|
import {
|
|
14
14
|
assertUniqueRoutePatterns,
|
|
15
15
|
compareRouteSpecificity,
|
|
16
|
+
getRouteExports,
|
|
16
17
|
matchRoutePattern,
|
|
17
18
|
parseBasePaths,
|
|
18
19
|
parseRouteModuleKey,
|
|
@@ -49,44 +50,6 @@ export interface RouteModuleCacheStats {
|
|
|
49
50
|
}
|
|
50
51
|
|
|
51
52
|
export class RouteTreeBuilder {
|
|
52
|
-
static readonly #pageRouteExports = new Set([
|
|
53
|
-
"default",
|
|
54
|
-
"pageConfig",
|
|
55
|
-
"head",
|
|
56
|
-
"metadata",
|
|
57
|
-
"generateHead",
|
|
58
|
-
"generateMetadata",
|
|
59
|
-
"Loading",
|
|
60
|
-
]);
|
|
61
|
-
static readonly #rootLayoutExports = new Set([
|
|
62
|
-
"default",
|
|
63
|
-
"pageConfig",
|
|
64
|
-
"head",
|
|
65
|
-
"metadata",
|
|
66
|
-
"generateHead",
|
|
67
|
-
"generateMetadata",
|
|
68
|
-
"fonts",
|
|
69
|
-
"manifest",
|
|
70
|
-
"theme",
|
|
71
|
-
"reconnect",
|
|
72
|
-
"wsConnect",
|
|
73
|
-
"layoutStyle",
|
|
74
|
-
"gaTrackingId",
|
|
75
|
-
"Loading",
|
|
76
|
-
"NotFound",
|
|
77
|
-
"Error",
|
|
78
|
-
]);
|
|
79
|
-
static readonly #layoutRouteExports = new Set([
|
|
80
|
-
"default",
|
|
81
|
-
"pageConfig",
|
|
82
|
-
"head",
|
|
83
|
-
"metadata",
|
|
84
|
-
"generateHead",
|
|
85
|
-
"generateMetadata",
|
|
86
|
-
"Loading",
|
|
87
|
-
"NotFound",
|
|
88
|
-
"Error",
|
|
89
|
-
]);
|
|
90
53
|
static readonly #moduleCacheStats: RouteModuleCacheStats = {
|
|
91
54
|
moduleCount: 0,
|
|
92
55
|
loadedModuleCount: 0,
|
|
@@ -309,12 +272,7 @@ export class RouteTreeBuilder {
|
|
|
309
272
|
return;
|
|
310
273
|
}
|
|
311
274
|
const parsed = parseRouteModuleKey(key);
|
|
312
|
-
const allowed =
|
|
313
|
-
kind === "page"
|
|
314
|
-
? RouteTreeBuilder.#pageRouteExports
|
|
315
|
-
: parsed.isInternalRootLayout
|
|
316
|
-
? RouteTreeBuilder.#rootLayoutExports
|
|
317
|
-
: RouteTreeBuilder.#layoutRouteExports;
|
|
275
|
+
const allowed = getRouteExports(kind, { rootLayout: parsed.isInternalRootLayout });
|
|
318
276
|
for (const exportName of Object.keys(mod)) {
|
|
319
277
|
if (!allowed.has(exportName)) {
|
|
320
278
|
throw new Error(`[route-convention] unsupported export "${exportName}" in ${key}`);
|
package/server/webRouter.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import { pathToFileURL } from "node:url";
|
|
3
4
|
import { getEnv } from "akanjs/base";
|
|
4
5
|
import {
|
|
5
6
|
type AkanI18nConfig,
|
|
@@ -61,6 +62,18 @@ export const DEFAULT_HTML_RESULT_CACHE_MAX_BODY_BYTES = 2 * 1024 * 1024;
|
|
|
61
62
|
const ROUTE_CACHE_SWEEP_INTERVAL_MS = 60_000;
|
|
62
63
|
const APPLE_APP_SITE_ASSOCIATION_PATH = "/.well-known/apple-app-site-association";
|
|
63
64
|
const ANDROID_ASSET_LINKS_PATH = "/.well-known/assetlinks.json";
|
|
65
|
+
const FIREBASE_MESSAGING_SW_PATH = "/firebase-messaging-sw.js";
|
|
66
|
+
const FIREBASE_WEB_SDK_VERSION = "12.13.0";
|
|
67
|
+
|
|
68
|
+
interface FirebaseClientEnvConfig {
|
|
69
|
+
apiKey: string;
|
|
70
|
+
authDomain?: string;
|
|
71
|
+
projectId: string;
|
|
72
|
+
storageBucket?: string;
|
|
73
|
+
messagingSenderId: string;
|
|
74
|
+
appId: string;
|
|
75
|
+
vapidKey?: string;
|
|
76
|
+
}
|
|
64
77
|
|
|
65
78
|
export function createRscRedirectResponse(
|
|
66
79
|
location: string,
|
|
@@ -488,6 +501,16 @@ export class WebRouter {
|
|
|
488
501
|
WebRouter.#deepLinkAssociationResponse(ANDROID_ASSET_LINKS_PATH, this.#artifact, {
|
|
489
502
|
cacheControl: this.#prodMode ? "public, max-age=3600" : "no-store",
|
|
490
503
|
}) ?? new Response("Not Found", { status: 404 }),
|
|
504
|
+
[FIREBASE_MESSAGING_SW_PATH]: async () => {
|
|
505
|
+
this.#requestStats.staticAsset += 1;
|
|
506
|
+
const firebaseConfig = await WebRouter.#resolveFirebaseClientConfig();
|
|
507
|
+
return new Response(WebRouter.#createFirebaseMessagingServiceWorker(firebaseConfig), {
|
|
508
|
+
headers: {
|
|
509
|
+
"Content-Type": "application/javascript; charset=utf-8",
|
|
510
|
+
"Cache-Control": "no-store",
|
|
511
|
+
},
|
|
512
|
+
});
|
|
513
|
+
},
|
|
491
514
|
"/*": async (req) => {
|
|
492
515
|
const url = new URL(req.url);
|
|
493
516
|
if (WebRouter.#isImageOptimizerPath(url.pathname)) {
|
|
@@ -1008,6 +1031,79 @@ export class WebRouter {
|
|
|
1008
1031
|
return process.env.AKAN_APP_DIR ?? path.dirname(Bun.main);
|
|
1009
1032
|
}
|
|
1010
1033
|
|
|
1034
|
+
static async #resolveFirebaseClientConfig(): Promise<FirebaseClientEnvConfig | null> {
|
|
1035
|
+
const envPath = path.join(WebRouter.#resolveAppDir(), "env", "env.client.ts");
|
|
1036
|
+
if (!fs.existsSync(envPath)) return null;
|
|
1037
|
+
try {
|
|
1038
|
+
const envUrl = pathToFileURL(envPath);
|
|
1039
|
+
envUrl.searchParams.set("t", String(Date.now()));
|
|
1040
|
+
const envModule = (await import(envUrl.href)) as { env?: { firebase?: unknown } };
|
|
1041
|
+
return WebRouter.#normalizeFirebaseClientConfig(envModule.env?.firebase);
|
|
1042
|
+
} catch {
|
|
1043
|
+
return null;
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
static #normalizeFirebaseClientConfig(config: unknown): FirebaseClientEnvConfig | null {
|
|
1048
|
+
if (!config || typeof config !== "object") return null;
|
|
1049
|
+
const value = config as Partial<Record<keyof FirebaseClientEnvConfig, unknown>>;
|
|
1050
|
+
if (
|
|
1051
|
+
typeof value.apiKey !== "string" ||
|
|
1052
|
+
typeof value.projectId !== "string" ||
|
|
1053
|
+
typeof value.messagingSenderId !== "string" ||
|
|
1054
|
+
typeof value.appId !== "string"
|
|
1055
|
+
) {
|
|
1056
|
+
return null;
|
|
1057
|
+
}
|
|
1058
|
+
return {
|
|
1059
|
+
apiKey: value.apiKey,
|
|
1060
|
+
...(typeof value.authDomain === "string" ? { authDomain: value.authDomain } : {}),
|
|
1061
|
+
projectId: value.projectId,
|
|
1062
|
+
...(typeof value.storageBucket === "string" ? { storageBucket: value.storageBucket } : {}),
|
|
1063
|
+
messagingSenderId: value.messagingSenderId,
|
|
1064
|
+
appId: value.appId,
|
|
1065
|
+
};
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
static #createFirebaseMessagingServiceWorker(config: FirebaseClientEnvConfig | null): string {
|
|
1069
|
+
const configJson = JSON.stringify(config);
|
|
1070
|
+
return `/* Generated by Akan.js. Do not edit. */
|
|
1071
|
+
const firebaseConfig = ${configJson};
|
|
1072
|
+
|
|
1073
|
+
if (firebaseConfig) {
|
|
1074
|
+
importScripts("https://www.gstatic.com/firebasejs/${FIREBASE_WEB_SDK_VERSION}/firebase-app-compat.js");
|
|
1075
|
+
importScripts("https://www.gstatic.com/firebasejs/${FIREBASE_WEB_SDK_VERSION}/firebase-messaging-compat.js");
|
|
1076
|
+
|
|
1077
|
+
firebase.initializeApp(firebaseConfig);
|
|
1078
|
+
const messaging = firebase.messaging();
|
|
1079
|
+
|
|
1080
|
+
const notificationUrl = (payload) =>
|
|
1081
|
+
payload?.data?.url || payload?.fcmOptions?.link || payload?.notification?.click_action;
|
|
1082
|
+
|
|
1083
|
+
messaging.onBackgroundMessage((payload) => {
|
|
1084
|
+
const title = payload?.notification?.title || "";
|
|
1085
|
+
const options = {
|
|
1086
|
+
body: payload?.notification?.body,
|
|
1087
|
+
icon: payload?.notification?.icon,
|
|
1088
|
+
image: payload?.notification?.image,
|
|
1089
|
+
data: {
|
|
1090
|
+
url: notificationUrl(payload),
|
|
1091
|
+
FCM_MSG: payload,
|
|
1092
|
+
},
|
|
1093
|
+
};
|
|
1094
|
+
self.registration.showNotification(title, options);
|
|
1095
|
+
});
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
self.addEventListener("notificationclick", (event) => {
|
|
1099
|
+
const url = event.notification?.data?.url || event.notification?.data?.FCM_MSG?.data?.url;
|
|
1100
|
+
event.notification?.close();
|
|
1101
|
+
if (!url) return;
|
|
1102
|
+
event.waitUntil(clients.openWindow(url));
|
|
1103
|
+
});
|
|
1104
|
+
`;
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1011
1107
|
static #normalizeArtifact(artifact: BaseBuildArtifact, artifactDir: string): BaseBuildArtifact {
|
|
1012
1108
|
const normalizedArtifactDir = path.resolve(artifactDir);
|
|
1013
1109
|
const pagesBundlePath = WebRouter.#resolveArtifactPath(artifact.pagesBundlePath, normalizedArtifactDir);
|
package/store/types.ts
CHANGED
|
@@ -36,6 +36,13 @@ export type Get<State, Actions> = {
|
|
|
36
36
|
get: () => State & Actions;
|
|
37
37
|
};
|
|
38
38
|
|
|
39
|
+
type VoidAction<T> = T extends (...args: infer Args) => infer Ret
|
|
40
|
+
? [Ret] extends [PromiseLike<unknown>]
|
|
41
|
+
? (...args: Args) => Promise<void>
|
|
42
|
+
: (...args: Args) => void
|
|
43
|
+
: T;
|
|
44
|
+
export type VoidActions<Action> = { [K in keyof Action]: VoidAction<Action[K]> };
|
|
45
|
+
|
|
39
46
|
export type StoreSliceMap<SlceCls extends SliceCls> = SlceCls[typeof SLICE_META];
|
|
40
47
|
export type StoreSliceSuffix<SlceCls extends SliceCls, Suffix extends keyof StoreSliceMap<SlceCls>> = Suffix & string;
|
|
41
48
|
export type StoreSliceSuffixCap<SlceCls extends SliceCls, Suffix extends keyof StoreSliceMap<SlceCls>> = Capitalize<
|
package/store/withSelector.ts
CHANGED
|
@@ -2,7 +2,7 @@ import type { Prettify } from "akanjs/base";
|
|
|
2
2
|
import type { FieldState } from "akanjs/constant";
|
|
3
3
|
import type { RefObject } from "react";
|
|
4
4
|
import type { RootStoreCls } from "./rootStore";
|
|
5
|
-
import type { SliceStateAction } from "./types";
|
|
5
|
+
import type { SliceStateAction, VoidActions } from "./types";
|
|
6
6
|
|
|
7
7
|
type SetKey<Key extends string> = `set${Capitalize<Key>}`;
|
|
8
8
|
|
|
@@ -28,7 +28,7 @@ type WithSelectorsOf<State, WritableState, Action, InternalSliceObj> = {
|
|
|
28
28
|
use: {
|
|
29
29
|
[K in keyof State]: () => State[K];
|
|
30
30
|
};
|
|
31
|
-
do: Action & {
|
|
31
|
+
do: VoidActions<Action> & {
|
|
32
32
|
[K in keyof WritableState as K extends string ? SetKey<K> : never]: (value: FieldState<WritableState[K]>) => void;
|
|
33
33
|
};
|
|
34
34
|
get: () => State;
|
|
@@ -52,9 +52,7 @@ export interface SliceSelectors<RefName extends string, State, Action> {
|
|
|
52
52
|
[K in keyof State]: () => State[K];
|
|
53
53
|
};
|
|
54
54
|
do: Prettify<
|
|
55
|
-
{
|
|
56
|
-
[K in keyof Action]: Action[K];
|
|
57
|
-
} & {
|
|
55
|
+
VoidActions<Action> & {
|
|
58
56
|
[K in keyof State as K extends string ? SetKey<K> : never]: (value: FieldState<State[K]>) => void;
|
|
59
57
|
}
|
|
60
58
|
>;
|
package/types/client/cn.d.ts
CHANGED
|
@@ -6,6 +6,11 @@ import { type ClassNameValue } from "tailwind-merge";
|
|
|
6
6
|
* `cn("bg-primary", "bg-open")` correctly resolves to `"bg-open"` instead of keeping both.
|
|
7
7
|
*/
|
|
8
8
|
export declare const colorTokens: string[];
|
|
9
|
+
/**
|
|
10
|
+
* Akan's semantic radius tokens (`--radius-box` / `--radius-field` in ui/styles.css). Without them
|
|
11
|
+
* `cn("rounded-field", "rounded-full")` keeps both classes and stylesheet order decides the winner.
|
|
12
|
+
*/
|
|
13
|
+
export declare const radiusTokens: string[];
|
|
9
14
|
/** The one class-combining function: joins strings/arrays/conditional parts (`cond && "x"`) and
|
|
10
15
|
* resolves Tailwind conflicts with a shared tailwind-merge instance that knows Akan's semantic
|
|
11
16
|
* color tokens. clsx-style object syntax (`{ x: cond }`) is not supported — write `cond && "x"`. */
|
|
@@ -428,6 +428,7 @@ export interface FrameLayoutState {
|
|
|
428
428
|
keyboard: KeyboardFrameState;
|
|
429
429
|
contentViewport: FrameContentViewportState;
|
|
430
430
|
keyboardAccessory: KeyboardAccessoryFrameState;
|
|
431
|
+
contentAnchor?: "bottom";
|
|
431
432
|
platformProfile: FramePlatformProfile;
|
|
432
433
|
zIndex: FrameLayerZIndex;
|
|
433
434
|
pageStateByPath: Map<string, PageState>;
|
|
@@ -436,6 +437,7 @@ export interface FrameSlotRegistration {
|
|
|
436
437
|
scope?: FrameSlotScope;
|
|
437
438
|
type: FrameSlotType;
|
|
438
439
|
role?: FrameSlotRole;
|
|
440
|
+
contentAnchor?: "bottom";
|
|
439
441
|
height?: number;
|
|
440
442
|
estimatedHeight?: number;
|
|
441
443
|
source?: "navbar" | "topInset" | "bottomInset" | "bottomTab" | (string & {});
|
package/types/common/index.d.ts
CHANGED
|
@@ -23,7 +23,7 @@ export { pathGet } from "./pathGet.d.ts";
|
|
|
23
23
|
export { pathSet } from "./pathSet.d.ts";
|
|
24
24
|
export { randomPick } from "./randomPick.d.ts";
|
|
25
25
|
export { randomPicks } from "./randomPicks.d.ts";
|
|
26
|
-
export { assertUniqueRoutePatterns, compareRouteSpecificity, isRouteSourceFile, isSpecialRouteLeaf, matchRoutePattern, normalizeRoutePattern, type ParsedRouteModuleKey, parseRouteModuleKey, type RouteModuleKind, routeSegmentToPatternPart, routeSegmentToTreePath, tryParseRouteModuleKey, type ValidatePageSourceFileOptions, type ValidateSubRoutePageKeyOptions, validatePageSourceFile, validateSubRoutePageKey, } from "./routeConvention.d.ts";
|
|
26
|
+
export { assertUniqueRoutePatterns, compareRouteSpecificity, getRouteExports, isRouteSourceFile, isSpecialRouteLeaf, LAYOUT_ROUTE_EXPORTS, matchRoutePattern, normalizeRoutePattern, PAGE_ROUTE_EXPORTS, type ParsedRouteModuleKey, parseRouteModuleKey, RESERVED_ROUTE_CONFIG_EXPORTS, ROOT_LAYOUT_ROUTE_EXPORTS, type RouteModuleKind, routeSegmentToPatternPart, routeSegmentToTreePath, tryParseRouteModuleKey, type ValidatePageSourceFileOptions, type ValidateSubRoutePageKeyOptions, validatePageSourceFile, validateSubRoutePageKey, } from "./routeConvention.d.ts";
|
|
27
27
|
export { sleep } from "./sleep.d.ts";
|
|
28
28
|
export { splitVersion } from "./splitVersion.d.ts";
|
|
29
29
|
export { getBasePathFromPathname, parseBasePaths, parseSubRouteHosts, resolveSubRouteHosts } from "./subRoute.d.ts";
|
|
@@ -1,4 +1,12 @@
|
|
|
1
1
|
export type RouteModuleKind = "page" | "layout" | "overrides";
|
|
2
|
+
export declare const PAGE_ROUTE_EXPORTS: ReadonlySet<string>;
|
|
3
|
+
export declare const LAYOUT_ROUTE_EXPORTS: ReadonlySet<string>;
|
|
4
|
+
export declare const ROOT_LAYOUT_ROUTE_EXPORTS: ReadonlySet<string>;
|
|
5
|
+
/** Root-layout exports that are plain config rather than components, so a PascalCase check cannot allow them. */
|
|
6
|
+
export declare const RESERVED_ROUTE_CONFIG_EXPORTS: ReadonlySet<string>;
|
|
7
|
+
export declare function getRouteExports(kind: "page" | "layout", { rootLayout }?: {
|
|
8
|
+
rootLayout?: boolean | undefined;
|
|
9
|
+
}): ReadonlySet<string>;
|
|
2
10
|
export interface ParsedRouteModuleKey {
|
|
3
11
|
key: string;
|
|
4
12
|
kind: RouteModuleKind;
|
package/types/document/into.d.ts
CHANGED
|
@@ -62,6 +62,8 @@ export type Mdl<Doc, Raw, _RawDoc = DocumentModel<Raw>, _RawQuery extends Docume
|
|
|
62
62
|
updateMany(query: _RawQuery, update: DocumentUpdateInput<_RawDoc>): Promise<UpdateResult>;
|
|
63
63
|
removeOne(query: _RawQuery): Promise<UpdateResult>;
|
|
64
64
|
removeMany(query: _RawQuery): Promise<UpdateResult>;
|
|
65
|
+
updateById(id: string, update: DocumentUpdateInput<_RawDoc>, options?: DocumentUpdateOptions): Promise<UpdateResult>;
|
|
66
|
+
removeById(id: string): Promise<UpdateResult>;
|
|
65
67
|
bulkWrite(operations: BulkWriteOperation<Raw, _RawDoc, _RawQuery>[]): Promise<UpdateResult>;
|
|
66
68
|
/** @deprecated Renamed to `count`. */
|
|
67
69
|
countDocuments(query: _RawQuery): Promise<number>;
|
|
@@ -35,6 +35,7 @@ export declare class FetchClient {
|
|
|
35
35
|
[key: string]: SerializedSignal;
|
|
36
36
|
}, ErrorCls?: ErrorConstructor | undefined);
|
|
37
37
|
static resetSharedRegistry(): void;
|
|
38
|
+
static resetSharedClient(): void;
|
|
38
39
|
setErrorConstructor(ErrorCls?: ErrorConstructor): void;
|
|
39
40
|
applySignal(serializedSignal: {
|
|
40
41
|
[key: string]: SerializedSignal;
|
package/types/store/baseSt.d.ts
CHANGED
|
@@ -297,7 +297,7 @@ export declare const st: {
|
|
|
297
297
|
deviceToken: () => string;
|
|
298
298
|
currentPath: () => string;
|
|
299
299
|
};
|
|
300
|
-
do: RootStore & {
|
|
300
|
+
do: import("./types.d.ts").VoidActions<RootStore> & {
|
|
301
301
|
setCsrLoaded: (value: boolean) => void;
|
|
302
302
|
setPath: (value: string) => void;
|
|
303
303
|
setPathname: (value: string) => void;
|
package/types/store/types.d.ts
CHANGED
|
@@ -30,6 +30,10 @@ export interface SetPick<State = any> {
|
|
|
30
30
|
export type Get<State, Actions> = {
|
|
31
31
|
get: () => State & Actions;
|
|
32
32
|
};
|
|
33
|
+
type VoidAction<T> = T extends (...args: infer Args) => infer Ret ? [Ret] extends [PromiseLike<unknown>] ? (...args: Args) => Promise<void> : (...args: Args) => void : T;
|
|
34
|
+
export type VoidActions<Action> = {
|
|
35
|
+
[K in keyof Action]: VoidAction<Action[K]>;
|
|
36
|
+
};
|
|
33
37
|
export type StoreSliceMap<SlceCls extends SliceCls> = SlceCls[typeof SLICE_META];
|
|
34
38
|
export type StoreSliceSuffix<SlceCls extends SliceCls, Suffix extends keyof StoreSliceMap<SlceCls>> = Suffix & string;
|
|
35
39
|
export type StoreSliceSuffixCap<SlceCls extends SliceCls, Suffix extends keyof StoreSliceMap<SlceCls>> = Capitalize<StoreSliceSuffix<SlceCls, Suffix>>;
|
|
@@ -2,7 +2,7 @@ import type { Prettify } from "akanjs/base";
|
|
|
2
2
|
import type { FieldState } from "akanjs/constant";
|
|
3
3
|
import type { RefObject } from "react";
|
|
4
4
|
import type { RootStoreCls } from "./rootStore.d.ts";
|
|
5
|
-
import type { SliceStateAction } from "./types.d.ts";
|
|
5
|
+
import type { SliceStateAction, VoidActions } from "./types.d.ts";
|
|
6
6
|
type SetKey<Key extends string> = `set${Capitalize<Key>}`;
|
|
7
7
|
export type WithSelectors<RtStoreCls extends RootStoreCls> = RtStoreCls extends RootStoreCls<any, infer WritableState, infer Action, infer InternalSliceObj, any, any, infer State> ? WithSelectorsOf<State, WritableState, Action, InternalSliceObj> : never;
|
|
8
8
|
type WithSelectorsOf<State, WritableState, Action, InternalSliceObj> = {
|
|
@@ -18,7 +18,7 @@ type WithSelectorsOf<State, WritableState, Action, InternalSliceObj> = {
|
|
|
18
18
|
use: {
|
|
19
19
|
[K in keyof State]: () => State[K];
|
|
20
20
|
};
|
|
21
|
-
do: Action & {
|
|
21
|
+
do: VoidActions<Action> & {
|
|
22
22
|
[K in keyof WritableState as K extends string ? SetKey<K> : never]: (value: FieldState<WritableState[K]>) => void;
|
|
23
23
|
};
|
|
24
24
|
get: () => State;
|
|
@@ -32,9 +32,7 @@ export interface SliceSelectors<RefName extends string, State, Action> {
|
|
|
32
32
|
use: {
|
|
33
33
|
[K in keyof State]: () => State[K];
|
|
34
34
|
};
|
|
35
|
-
do: Prettify<{
|
|
36
|
-
[K in keyof Action]: Action[K];
|
|
37
|
-
} & {
|
|
35
|
+
do: Prettify<VoidActions<Action> & {
|
|
38
36
|
[K in keyof State as K extends string ? SetKey<K> : never]: (value: FieldState<State[K]>) => void;
|
|
39
37
|
}>;
|
|
40
38
|
get: () => State;
|
package/types/ui/CsrImage.d.ts
CHANGED
|
@@ -7,7 +7,7 @@ type CsrImageProps = Omit<ImgHTMLAttributes<HTMLImageElement>, "alt" | "src"> &
|
|
|
7
7
|
imageSize: [number, number];
|
|
8
8
|
abstractData?: string | null;
|
|
9
9
|
} | null;
|
|
10
|
-
abstractData?: string;
|
|
10
|
+
abstractData?: string | null;
|
|
11
11
|
priority?: boolean;
|
|
12
12
|
preload?: boolean;
|
|
13
13
|
quality?: number;
|
package/types/ui/Dropdown.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { type ReactNode } from "react";
|
|
2
|
+
/** Put this on a menu item that runs its own interaction (a switch, a copy button) to keep the menu open. */
|
|
3
|
+
export declare const DROPDOWN_KEEP_OPEN_ATTR = "data-dropdown-keep-open";
|
|
2
4
|
export interface DropdownProps {
|
|
3
5
|
/** Button/trigger content. */
|
|
4
6
|
value: ReactNode;
|