akanjs 3.0.0-alpha.6 → 3.0.0-alpha.8
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/common/deepObjectify.ts +21 -7
- package/common/index.ts +5 -0
- package/common/pathSet.ts +17 -5
- package/common/routeConvention.ts +30 -0
- package/constant/crystalize.ts +3 -1
- 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/store/action.ts +1 -1
- package/store/storeInstance.ts +3 -1
- package/store/types.ts +7 -0
- package/store/withSelector.ts +3 -5
- package/types/common/deepObjectify.d.ts +4 -2
- 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/Image.d.ts +3 -3
- package/ui/CsrImage.tsx +1 -1
- package/ui/Image.tsx +3 -3
- package/webkit/bootCsr.tsx +2 -33
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/common/deepObjectify.ts
CHANGED
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
import { isDayjs } from "./isDayjs";
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
interface DeepObjectifyOption {
|
|
4
|
+
serializable?: boolean;
|
|
5
|
+
convertDate?: "string" | "number";
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const objectifyChild = (value: unknown, option: DeepObjectifyOption): unknown => {
|
|
9
|
+
const modelValue = value as { __ModelType__?: string } | null | undefined;
|
|
10
|
+
return modelValue?.__ModelType__ && !option.serializable ? value : deepObjectify(value, option);
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export const deepObjectify = <T = unknown>(obj: T | null | undefined, option: DeepObjectifyOption = {}): T => {
|
|
7
14
|
if (isDayjs(obj) || obj?.constructor === Date) {
|
|
8
15
|
if (!option.serializable && !option.convertDate) return obj as T;
|
|
9
16
|
if (option.convertDate === "string") return obj.toISOString() as T;
|
|
@@ -12,13 +19,20 @@ export const deepObjectify = <T = unknown>(
|
|
|
12
19
|
else return (isDayjs(obj) ? obj.toDate() : obj) as T;
|
|
13
20
|
} else if (Array.isArray(obj)) {
|
|
14
21
|
return obj.map((o: unknown) => deepObjectify(o, option)) as T;
|
|
22
|
+
} else if (obj instanceof Map) {
|
|
23
|
+
|
|
24
|
+
const entries = [...obj.entries()].map(
|
|
25
|
+
([key, value]: [string, unknown]) => [key, objectifyChild(value, option)] as const,
|
|
26
|
+
);
|
|
27
|
+
return (option.serializable ? Object.fromEntries(entries) : new Map(entries)) as T;
|
|
28
|
+
} else if (obj instanceof Set) {
|
|
29
|
+
const values = [...obj.values()].map((value: unknown) => objectifyChild(value, option));
|
|
30
|
+
return (option.serializable ? values : new Set(values)) as T;
|
|
15
31
|
} else if (obj && typeof obj === "object") {
|
|
16
32
|
const val: Record<string, unknown> = {};
|
|
17
33
|
const objRecord = obj as Record<string, unknown>;
|
|
18
34
|
Object.keys(obj).forEach((key) => {
|
|
19
|
-
|
|
20
|
-
if (fieldValue?.__ModelType__ && !option.serializable) val[key] = fieldValue;
|
|
21
|
-
else if (typeof objRecord[key] !== "function") val[key] = deepObjectify(fieldValue, option);
|
|
35
|
+
if (typeof objRecord[key] !== "function") val[key] = objectifyChild(objRecord[key], option);
|
|
22
36
|
});
|
|
23
37
|
return val as T;
|
|
24
38
|
} else {
|
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,
|
package/common/pathSet.ts
CHANGED
|
@@ -1,16 +1,28 @@
|
|
|
1
1
|
type MutableIndexable = Record<string | number, unknown>;
|
|
2
2
|
type PathSegment = string | number;
|
|
3
|
+
type Container = MutableIndexable | Map<PathSegment, unknown>;
|
|
3
4
|
|
|
4
5
|
const toPathSegments = (path: string | readonly PathSegment[]) =>
|
|
5
6
|
Array.isArray(path) ? [...path] : path.toString().match(/[^.[\]]+/g) || [];
|
|
6
7
|
|
|
8
|
+
const readChild = (container: Container, key: PathSegment) =>
|
|
9
|
+
container instanceof Map ? container.get(key) : container[key];
|
|
10
|
+
|
|
11
|
+
const writeChild = (container: Container, key: PathSegment, value: unknown) => {
|
|
12
|
+
if (container instanceof Map) container.set(key, value);
|
|
13
|
+
else container[key] = value;
|
|
14
|
+
};
|
|
15
|
+
|
|
7
16
|
export const pathSet = <T>(obj: T, path: string | readonly PathSegment[], value: unknown): T => {
|
|
8
17
|
if (Object(obj) !== obj) return obj;
|
|
9
18
|
const pathSegments = toPathSegments(path);
|
|
10
|
-
pathSegments.slice(0, -1).reduce<
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
19
|
+
const parent = pathSegments.slice(0, -1).reduce<Container>((a, c, i) => {
|
|
20
|
+
const child = readChild(a, c);
|
|
21
|
+
if (Object(child) === child) return child as Container;
|
|
22
|
+
const created = Math.abs(Number(pathSegments[i + 1])) >> 0 === +pathSegments[i + 1] ? [] : {};
|
|
23
|
+
writeChild(a, c, created);
|
|
24
|
+
return created as unknown as Container;
|
|
25
|
+
}, obj as Container);
|
|
26
|
+
writeChild(parent, pathSegments[pathSegments.length - 1], value);
|
|
15
27
|
return obj;
|
|
16
28
|
};
|
|
@@ -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/constant/crystalize.ts
CHANGED
|
@@ -30,8 +30,10 @@ export const crystalize = (field: FieldProps, value: unknown): unknown => {
|
|
|
30
30
|
isArray: false,
|
|
31
31
|
arrDepth: 0,
|
|
32
32
|
};
|
|
33
|
+
|
|
34
|
+
const entries = value instanceof Map ? [...value.entries()] : Object.entries(value as Record<string, unknown>);
|
|
33
35
|
return new Map(
|
|
34
|
-
|
|
36
|
+
entries.map(([key, val]: [string, unknown]) => [
|
|
35
37
|
key,
|
|
36
38
|
field.of
|
|
37
39
|
? applyFnToArrayObjects(val, (v: never) => crystalize(mapValueField, v))
|
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/store/action.ts
CHANGED
|
@@ -751,7 +751,7 @@ export const makeActions = (refName: string, slice: { [key: string]: SerializedS
|
|
|
751
751
|
const id = typeof modelOrId === "string" ? modelOrId : modelOrId.id;
|
|
752
752
|
this.set({ [names.modelFormLoading]: id, [names.modelModal]: modal ?? "edit" });
|
|
753
753
|
const model = await (fetch[names.model] as (...args: any[]) => Promise<Full>)(id, { onError });
|
|
754
|
-
const modelForm = deepObjectify<Input>(model as unknown as Input);
|
|
754
|
+
const modelForm = immerify(modelRef, deepObjectify<Input>(model as unknown as Input) as object) as Input;
|
|
755
755
|
this.set({
|
|
756
756
|
[names.model]: model,
|
|
757
757
|
[names.modelFormLoading]: false,
|
package/store/storeInstance.ts
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import { ACTION_META, STATE_DERIVED_META, STATE_INIT_META } from "akanjs/base";
|
|
2
2
|
import { Translator } from "akanjs/client";
|
|
3
3
|
import { capitalize, Logger, parseAkanI18nEnv } from "akanjs/common";
|
|
4
|
-
import { produce } from "immer";
|
|
4
|
+
import { enableMapSet, produce } from "immer";
|
|
5
5
|
import type { RefObject } from "react";
|
|
6
6
|
import { useEffect, useRef, useSyncExternalStore } from "./hooks";
|
|
7
7
|
import type { RootStoreCls } from "./rootStore";
|
|
8
8
|
import type { SliceStateKey } from "./state";
|
|
9
9
|
import { evaluateInitializers, type SearchParamsState, type StateDerivedMeta } from "./stateBuilder";
|
|
10
10
|
|
|
11
|
+
enableMapSet();
|
|
12
|
+
|
|
11
13
|
type StoreStateRecord = Record<string, unknown>;
|
|
12
14
|
type StoreAction = (...args: unknown[]) => unknown;
|
|
13
15
|
type TranslationParam = Record<string, string | number>;
|
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
|
>;
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
|
|
1
|
+
interface DeepObjectifyOption {
|
|
2
2
|
serializable?: boolean;
|
|
3
3
|
convertDate?: "string" | "number";
|
|
4
|
-
}
|
|
4
|
+
}
|
|
5
|
+
export declare const deepObjectify: <T = unknown>(obj: T | null | undefined, option?: DeepObjectifyOption) => T;
|
|
6
|
+
export {};
|
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/Image.d.ts
CHANGED
|
@@ -19,7 +19,7 @@ type AkanImageProps = NativeImageProps & {
|
|
|
19
19
|
/** Akan file object or file-like value with url and imageSize metadata. */
|
|
20
20
|
file?: ImageLikeFile;
|
|
21
21
|
/** Low-quality preview data. Overrides file.abstractData when provided. */
|
|
22
|
-
abstractData?: string;
|
|
22
|
+
abstractData?: string | null;
|
|
23
23
|
/** Accessible alt text. Defaults to "image" when omitted. */
|
|
24
24
|
alt?: string;
|
|
25
25
|
/** Image optimizer quality. Defaults to 75. */
|
|
@@ -34,11 +34,11 @@ type AkanImageProps = NativeImageProps & {
|
|
|
34
34
|
export declare const Image: ({ src, file, className, abstractData, alt, quality, priority, preload, unoptimized, ...props }: AkanImageProps & ({
|
|
35
35
|
src?: string;
|
|
36
36
|
file?: ProtoFile;
|
|
37
|
-
abstractData?: string;
|
|
37
|
+
abstractData?: string | null;
|
|
38
38
|
alt?: string;
|
|
39
39
|
} | {
|
|
40
40
|
src?: undefined;
|
|
41
|
-
abstractData?: string;
|
|
41
|
+
abstractData?: string | null;
|
|
42
42
|
file: {
|
|
43
43
|
url: string;
|
|
44
44
|
imageSize: [number, number];
|
package/ui/CsrImage.tsx
CHANGED
|
@@ -5,7 +5,7 @@ import type { ImgHTMLAttributes } from "react";
|
|
|
5
5
|
type CsrImageProps = Omit<ImgHTMLAttributes<HTMLImageElement>, "alt" | "src"> & {
|
|
6
6
|
src?: string;
|
|
7
7
|
file?: ProtoFile | { url: string; imageSize: [number, number]; abstractData?: string | null } | null;
|
|
8
|
-
abstractData?: string;
|
|
8
|
+
abstractData?: string | null;
|
|
9
9
|
priority?: boolean;
|
|
10
10
|
preload?: boolean;
|
|
11
11
|
quality?: number;
|
package/ui/Image.tsx
CHANGED
|
@@ -30,7 +30,7 @@ type AkanImageProps = NativeImageProps & {
|
|
|
30
30
|
/** Akan file object or file-like value with url and imageSize metadata. */
|
|
31
31
|
file?: ImageLikeFile;
|
|
32
32
|
/** Low-quality preview data. Overrides file.abstractData when provided. */
|
|
33
|
-
abstractData?: string;
|
|
33
|
+
abstractData?: string | null;
|
|
34
34
|
/** Accessible alt text. Defaults to "image" when omitted. */
|
|
35
35
|
alt?: string;
|
|
36
36
|
/** Image optimizer quality. Defaults to 75. */
|
|
@@ -59,12 +59,12 @@ export const Image = ({
|
|
|
59
59
|
| {
|
|
60
60
|
src?: string;
|
|
61
61
|
file?: ProtoFile;
|
|
62
|
-
abstractData?: string;
|
|
62
|
+
abstractData?: string | null;
|
|
63
63
|
alt?: string;
|
|
64
64
|
}
|
|
65
65
|
| {
|
|
66
66
|
src?: undefined;
|
|
67
|
-
abstractData?: string;
|
|
67
|
+
abstractData?: string | null;
|
|
68
68
|
file: { url: string; imageSize: [number, number]; abstractData?: string | null } | null;
|
|
69
69
|
alt?: string;
|
|
70
70
|
}
|
package/webkit/bootCsr.tsx
CHANGED
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
} from "akanjs/client";
|
|
19
19
|
import {
|
|
20
20
|
assertUniqueRoutePatterns,
|
|
21
|
+
getRouteExports,
|
|
21
22
|
Logger,
|
|
22
23
|
parseAkanI18nEnv,
|
|
23
24
|
parseBasePaths,
|
|
@@ -303,39 +304,7 @@ function validateRouteModuleExports(key: string, mod: RouteModule) {
|
|
|
303
304
|
if (!mod.default) throw new Error(`[route-convention] ${key} generated override wrapper has no default export`);
|
|
304
305
|
return;
|
|
305
306
|
}
|
|
306
|
-
const allowed =
|
|
307
|
-
parsed.kind === "page"
|
|
308
|
-
? new Set(["default", "pageConfig", "head", "metadata", "generateHead", "generateMetadata", "Loading"])
|
|
309
|
-
: parsed.isInternalRootLayout
|
|
310
|
-
? new Set([
|
|
311
|
-
"default",
|
|
312
|
-
"pageConfig",
|
|
313
|
-
"head",
|
|
314
|
-
"metadata",
|
|
315
|
-
"generateHead",
|
|
316
|
-
"generateMetadata",
|
|
317
|
-
"fonts",
|
|
318
|
-
"manifest",
|
|
319
|
-
"theme",
|
|
320
|
-
"reconnect",
|
|
321
|
-
"wsConnect",
|
|
322
|
-
"layoutStyle",
|
|
323
|
-
"gaTrackingId",
|
|
324
|
-
"Loading",
|
|
325
|
-
"NotFound",
|
|
326
|
-
"Error",
|
|
327
|
-
])
|
|
328
|
-
: new Set([
|
|
329
|
-
"default",
|
|
330
|
-
"pageConfig",
|
|
331
|
-
"head",
|
|
332
|
-
"metadata",
|
|
333
|
-
"generateHead",
|
|
334
|
-
"generateMetadata",
|
|
335
|
-
"Loading",
|
|
336
|
-
"NotFound",
|
|
337
|
-
"Error",
|
|
338
|
-
]);
|
|
307
|
+
const allowed = getRouteExports(parsed.kind, { rootLayout: parsed.isInternalRootLayout });
|
|
339
308
|
for (const exportName of Object.keys(mod)) {
|
|
340
309
|
if (!allowed.has(exportName)) {
|
|
341
310
|
throw new Error(`[route-convention] unsupported export "${exportName}" in ${key}`);
|