akanjs 2.0.0-rc.7 → 2.0.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.
Files changed (87) hide show
  1. package/base/primitiveRegistry.ts +28 -2
  2. package/cli/application/application.command.ts +11 -3
  3. package/cli/application/application.runner.ts +17 -1
  4. package/cli/guidelines/databaseModule/databaseModule.instruction.md +1 -1
  5. package/cli/guidelines/modelConstant/modelConstant.instruction.md +5 -5
  6. package/cli/guidelines/modelDocument/modelDocument.instruction.md +34 -61
  7. package/cli/guidelines/modelService/modelService.instruction.md +1 -1
  8. package/cli/index.js +9321 -19222
  9. package/cli/library/library.runner.ts +14 -13
  10. package/cli/package/package.runner.ts +31 -6
  11. package/cli/package/package.script.ts +2 -2
  12. package/cli/templates/app/page/_index.tsx +200 -79
  13. package/cli/templates/app/page/_layout.tsx +0 -1
  14. package/cli/templates/app/public/favicon.ico.template +0 -0
  15. package/cli/templates/app/public/logo.png.template +0 -0
  16. package/cli/templates/module/__Model__.Zone.tsx +1 -1
  17. package/cli/templates/module/__model__.document.ts +1 -1
  18. package/cli/templates/workspaceRoot/.gitignore.template +1 -11
  19. package/cli/templates/workspaceRoot/biome.json.template +16 -0
  20. package/cli/templates/workspaceRoot/package.json.template +1 -5
  21. package/cli/workspace/workspace.command.ts +7 -9
  22. package/cli/workspace/workspace.runner.ts +3 -13
  23. package/cli/workspace/workspace.script.ts +24 -9
  24. package/client/csrTypes.ts +1 -1
  25. package/constant/fieldInfo.ts +1 -1
  26. package/constant/serialize.ts +7 -1
  27. package/devkit/capacitor.base.config.ts +1 -1
  28. package/devkit/capacitorApp.ts +5 -1
  29. package/devkit/commandDecorators/argMeta.ts +28 -14
  30. package/devkit/commandDecorators/command.ts +41 -15
  31. package/devkit/commandDecorators/commandBuilder.ts +78 -42
  32. package/devkit/commandDecorators/helpFormatter.ts +7 -4
  33. package/devkit/dependencyScanner.ts +121 -15
  34. package/devkit/executors.ts +35 -23
  35. package/devkit/frontendBuild/cssCompiler.ts +9 -3
  36. package/devkit/incrementalBuilder/incrementalBuilder.proc.ts +2 -1
  37. package/devkit/lint/no-deep-internal-import.grit +25 -0
  38. package/devkit/lint/no-import-external-library.grit +1 -0
  39. package/devkit/mobile/mobileTarget.ts +48 -8
  40. package/devkit/scanInfo.ts +4 -1
  41. package/devkit/src/capacitorApp.ts +277 -0
  42. package/devkit/transforms/barrelImportsPlugin.ts +6 -0
  43. package/fetch/client/fetchClient.ts +1 -0
  44. package/fetch/client/httpClient.ts +13 -1
  45. package/package.json +37 -31
  46. package/server/akanServer.ts +21 -7
  47. package/server/hmr/clientScript.ts +8 -5
  48. package/server/resolver/resolver.contract.fixture.ts +1 -1
  49. package/test/index.ts +14 -0
  50. package/test/signalTest.preload.ts +10 -0
  51. package/test/signalTestRuntime.ts +126 -0
  52. package/test/testServer.ts +130 -25
  53. package/ui/Constant/Doc.tsx +696 -0
  54. package/ui/Constant/Mermaid.tsx +149 -0
  55. package/ui/Constant/index.ts +6 -0
  56. package/ui/Constant/schemaDoc.ts +324 -0
  57. package/ui/Field.tsx +0 -1
  58. package/ui/Portal.tsx +2 -0
  59. package/ui/System/CSR.tsx +6 -5
  60. package/ui/System/SSR.tsx +1 -1
  61. package/ui/System/SelectLanguage.tsx +1 -1
  62. package/ui/index.ts +1 -0
  63. package/ui/styles.css +0 -1
  64. package/webkit/bootCsr.tsx +8 -5
  65. package/base/test-globals.d.ts +0 -4
  66. package/cli/templates/app/common/commonLogic.ts +0 -12
  67. package/cli/templates/app/common/index.ts +0 -10
  68. package/cli/templates/app/public/favicon.ico +0 -0
  69. package/cli/templates/app/public/icons/icon-128x128.png +0 -0
  70. package/cli/templates/app/public/icons/icon-144x144.png +0 -0
  71. package/cli/templates/app/public/icons/icon-152x152.png +0 -0
  72. package/cli/templates/app/public/icons/icon-192x192.png +0 -0
  73. package/cli/templates/app/public/icons/icon-256x256.png +0 -0
  74. package/cli/templates/app/public/icons/icon-384x384.png +0 -0
  75. package/cli/templates/app/public/icons/icon-48x48.png +0 -0
  76. package/cli/templates/app/public/icons/icon-512x512.png +0 -0
  77. package/cli/templates/app/public/icons/icon-72x72.png +0 -0
  78. package/cli/templates/app/public/icons/icon-96x96.png +0 -0
  79. package/cli/templates/app/public/logo.svg +0 -70
  80. package/cli/templates/app/public/manifest.json.template +0 -67
  81. package/cli/templates/app/srvkit/backendLogic.ts +0 -12
  82. package/cli/templates/app/srvkit/index.ts +0 -10
  83. package/cli/templates/app/ui/UiComponent.ts +0 -16
  84. package/cli/templates/app/ui/index.ts +0 -10
  85. package/cli/templates/app/webkit/frontendLogic.ts +0 -12
  86. package/cli/templates/app/webkit/index.ts +0 -10
  87. package/cli/templates/module/index.tsx +0 -44
@@ -0,0 +1,149 @@
1
+ "use client";
2
+
3
+ import { clsx } from "akanjs/client";
4
+ import { useEffect, useId, useRef, useState } from "react";
5
+
6
+ interface MermaidProps {
7
+ chart: string;
8
+ title?: string;
9
+ className?: string;
10
+ highlightNodes?: string[];
11
+ onSelectNode?: (nodeId: string) => void;
12
+ }
13
+
14
+ const lightThemeFallback = {
15
+ base100: "#ffffff",
16
+ base200: "#f5f5f5",
17
+ base300: "#d4d4d4",
18
+ baseContent: "#1f2937",
19
+ primary: "#3b82f6",
20
+ };
21
+
22
+ const darkThemeFallback = {
23
+ base100: "#1f2937",
24
+ base200: "#111827",
25
+ base300: "#374151",
26
+ baseContent: "#f9fafb",
27
+ primary: "#60a5fa",
28
+ };
29
+
30
+ export const Mermaid = ({ chart, title, className, highlightNodes = [], onSelectNode }: MermaidProps) => {
31
+ const reactId = useId();
32
+ const containerRef = useRef<HTMLDivElement>(null);
33
+ const [error, setError] = useState<string | null>(null);
34
+
35
+ useEffect(() => {
36
+ let cancelled = false;
37
+ const render = async () => {
38
+ try {
39
+ const { default: mermaid } = await import("mermaid");
40
+ const colors = getThemeColors();
41
+ mermaid.initialize({
42
+ startOnLoad: false,
43
+ securityLevel: "strict",
44
+ theme: "base",
45
+ themeVariables: {
46
+ background: colors.base100,
47
+ mainBkg: colors.base100,
48
+ primaryColor: colors.base100,
49
+ primaryTextColor: colors.baseContent,
50
+ primaryBorderColor: colors.baseContent,
51
+ lineColor: colors.baseContent,
52
+ secondaryColor: colors.base200,
53
+ tertiaryColor: colors.base300,
54
+ },
55
+ });
56
+ const id = `mermaid-${reactId.replace(/[^a-zA-Z0-9_-]/g, "")}`;
57
+ const result = await mermaid.render(id, chart.trim());
58
+ if (cancelled) return;
59
+ const parsed = new DOMParser().parseFromString(result.svg, "text/html");
60
+ const svg = parsed.querySelector("svg");
61
+ if (!svg) throw new Error("Mermaid did not return an SVG.");
62
+ applyHighlights(svg, highlightNodes, colors.primary);
63
+ applyNodeSelection(svg, onSelectNode);
64
+ const container = containerRef.current;
65
+ if (container) container.replaceChildren(document.importNode(svg, true));
66
+ setError(null);
67
+ } catch (err) {
68
+ if (cancelled) return;
69
+ containerRef.current?.replaceChildren();
70
+ setError(err instanceof Error ? err.message : String(err));
71
+ }
72
+ };
73
+ void render();
74
+ const observer = new MutationObserver(() => void render());
75
+ observer.observe(document.documentElement, { attributes: true, attributeFilter: ["class", "data-theme", "style"] });
76
+ return () => {
77
+ cancelled = true;
78
+ observer.disconnect();
79
+ };
80
+ }, [chart, highlightNodes, onSelectNode, reactId]);
81
+
82
+ return (
83
+ <div className={clsx("my-4 overflow-hidden rounded-xl border border-base-300 bg-base-200/40", className)}>
84
+ {title ? (
85
+ <div className="border-base-300 border-b px-4 py-2 font-bold text-base-content/70 text-sm">{title}</div>
86
+ ) : null}
87
+ <div className="overflow-x-auto p-4">
88
+ {error ? (
89
+ <pre className="whitespace-pre-wrap text-error text-sm">{error}</pre>
90
+ ) : (
91
+ <div ref={containerRef} className="min-w-fit text-base-content" />
92
+ )}
93
+ </div>
94
+ </div>
95
+ );
96
+ };
97
+
98
+ function getThemeColors() {
99
+ const root = document.documentElement;
100
+ const style = getComputedStyle(root);
101
+ const fallback = isDarkTheme(root) ? darkThemeFallback : lightThemeFallback;
102
+
103
+ return {
104
+ base100: getMermaidColor(style, "--color-base-100", fallback.base100),
105
+ base200: getMermaidColor(style, "--color-base-200", fallback.base200),
106
+ base300: getMermaidColor(style, "--color-base-300", fallback.base300),
107
+ baseContent: getMermaidColor(style, "--color-base-content", fallback.baseContent),
108
+ primary: getMermaidColor(style, "--color-primary", fallback.primary),
109
+ };
110
+ }
111
+
112
+ function getMermaidColor(style: CSSStyleDeclaration, token: string, fallback: string): string {
113
+ const color = style.getPropertyValue(token).trim();
114
+ if (!color) return fallback;
115
+ return isMermaidColor(color) ? color : fallback;
116
+ }
117
+
118
+ function isMermaidColor(color: string): boolean {
119
+ return /^(#[\da-f]{3,8}|rgba?\(|hsla?\(|[a-z]+$)/i.test(color);
120
+ }
121
+
122
+ function isDarkTheme(root: HTMLElement): boolean {
123
+ return root.dataset.theme === "dark" || root.classList.contains("dark");
124
+ }
125
+
126
+ function applyHighlights(svg: Element, nodeIds: string[], color: string): void {
127
+ if (nodeIds.length === 0) return;
128
+ const ids = new Set(nodeIds);
129
+ for (const node of svg.querySelectorAll<SVGGElement>(".node")) {
130
+ const id = getMermaidNodeId(node);
131
+ if (!ids.has(id)) continue;
132
+ node.querySelectorAll<SVGElement>("rect, circle, ellipse, polygon, path").forEach((shape) => {
133
+ shape.setAttribute("stroke", color);
134
+ shape.setAttribute("stroke-width", "3px");
135
+ });
136
+ }
137
+ }
138
+
139
+ function applyNodeSelection(svg: Element, onSelectNode?: (nodeId: string) => void): void {
140
+ if (!onSelectNode) return;
141
+ for (const node of svg.querySelectorAll<SVGGElement>(".node")) {
142
+ node.style.cursor = "pointer";
143
+ node.addEventListener("click", () => onSelectNode(getMermaidNodeId(node)));
144
+ }
145
+ }
146
+
147
+ function getMermaidNodeId(node: SVGGElement): string {
148
+ return node.id.replace(/^flowchart-/, "").replace(/-\d+$/, "");
149
+ }
@@ -0,0 +1,6 @@
1
+ import Doc from "./Doc";
2
+ import { Mermaid } from "./Mermaid";
3
+
4
+ export * from "./schemaDoc";
5
+
6
+ export const Constant = { Doc, Mermaid };
@@ -0,0 +1,324 @@
1
+ import {
2
+ type Cls,
3
+ type EnumInstance,
4
+ FIELD_META,
5
+ getNonArrayModel,
6
+ PrimitiveRegistry,
7
+ type PrimitiveScalar,
8
+ } from "akanjs/base";
9
+ import { capitalize } from "akanjs/common";
10
+ import { type ConstantCls, type ConstantField, type ConstantModel, ConstantRegistry } from "akanjs/constant";
11
+
12
+ export const databaseModelVariants = ["input", "object", "full", "light", "insight"] as const;
13
+ export type DatabaseModelVariant = (typeof databaseModelVariants)[number];
14
+
15
+ export interface ConstantSchemaOptions {
16
+ models?: string[];
17
+ scalars?: string[];
18
+ enums?: string[];
19
+ }
20
+
21
+ export interface ConstantSchemaDoc {
22
+ databases: DatabaseSchema[];
23
+ scalars: ScalarSchema[];
24
+ enums: EnumSchema[];
25
+ relations: RelationSchema[];
26
+ }
27
+
28
+ export interface DatabaseSchema {
29
+ kind: "database";
30
+ refName: string;
31
+ modelName: string;
32
+ variants: Record<DatabaseModelVariant, ModelVariantSchema>;
33
+ }
34
+
35
+ export interface ScalarSchema {
36
+ kind: "scalar";
37
+ refName: string;
38
+ modelName: string;
39
+ modelRef: ConstantCls;
40
+ fields: FieldSchema[];
41
+ }
42
+
43
+ export interface ModelVariantSchema {
44
+ refName: string;
45
+ variant: DatabaseModelVariant | "scalar";
46
+ modelName: string;
47
+ modelRef: ConstantCls;
48
+ fields: FieldSchema[];
49
+ }
50
+
51
+ export interface EnumSchema {
52
+ key: string;
53
+ refName: string;
54
+ typeName: string;
55
+ values: (string | number)[];
56
+ enumRef: EnumInstance;
57
+ usedBy: EnumUsage[];
58
+ }
59
+
60
+ export interface EnumUsage {
61
+ refName: string;
62
+ variant: DatabaseModelVariant | "scalar";
63
+ fieldKey: string;
64
+ }
65
+
66
+ export interface RelationSchema {
67
+ sourceRefName: string;
68
+ sourceVariant: DatabaseModelVariant | "scalar";
69
+ targetRefName: string;
70
+ targetKind: "database" | "scalar";
71
+ fieldKey: string;
72
+ relationType: string;
73
+ external: boolean;
74
+ }
75
+
76
+ export interface FieldSchema {
77
+ key: string;
78
+ typeLabel: string;
79
+ typeRefName?: string;
80
+ typeKind: "primitive" | "database" | "scalar" | "map" | "unknown";
81
+ required: boolean;
82
+ nullable: boolean;
83
+ arrDepth: number;
84
+ fieldType: "property" | "hidden" | "resolve";
85
+ select: boolean;
86
+ immutable: boolean;
87
+ ref?: string;
88
+ refPath?: string;
89
+ refType?: "child" | "parent" | "relation";
90
+ relationLabel?: string;
91
+ enumRefName?: string;
92
+ enumValues?: (string | number)[];
93
+ defaultLabel?: string;
94
+ exampleLabel?: string;
95
+ constraints: string[];
96
+ text?: "search" | "filter";
97
+ meta: Record<string, unknown>;
98
+ raw: ConstantField;
99
+ }
100
+
101
+ export const getConstantSchemaDoc = (options: ConstantSchemaOptions = {}): ConstantSchemaDoc => {
102
+ const databases = getSelectedEntries(ConstantRegistry.database, options.models).map(([refName, model]) =>
103
+ buildDatabaseSchema(refName, model),
104
+ );
105
+ const scalars = getSelectedEntries(ConstantRegistry.scalar, options.scalars).map(([refName, scalar]) => {
106
+ const modelRef = scalar.model;
107
+ return {
108
+ kind: "scalar" as const,
109
+ refName,
110
+ modelName: getModelName(modelRef, refName),
111
+ modelRef,
112
+ fields: getFields(modelRef, "scalar"),
113
+ };
114
+ });
115
+ const selectedEnums = getSelectedEntries(ConstantRegistry.enum, options.enums);
116
+ const enumUsages = collectEnumUsages(databases, scalars);
117
+ const enums = selectedEnums.map(([key, enumRef]) => ({
118
+ key,
119
+ refName: enumRef.refName,
120
+ typeName: PrimitiveRegistry.getName(enumRef.type as typeof PrimitiveScalar),
121
+ values: [...enumRef.values] as (string | number)[],
122
+ enumRef,
123
+ usedBy: enumUsages.get(enumRef) ?? [],
124
+ }));
125
+ const relations = collectRelations(databases, scalars);
126
+ return { databases, scalars, enums, relations };
127
+ };
128
+
129
+ export const getDefaultVariant = (database: DatabaseSchema): ModelVariantSchema => database.variants.full;
130
+
131
+ export const getVariantTitle = (variant: DatabaseModelVariant | "scalar") => {
132
+ if (variant === "scalar") return "Scalar";
133
+ return capitalize(variant);
134
+ };
135
+
136
+ const buildDatabaseSchema = (refName: string, model: ConstantModel): DatabaseSchema => {
137
+ const variants = Object.fromEntries(
138
+ databaseModelVariants.map((variant) => {
139
+ const modelRef = model[variant];
140
+ return [
141
+ variant,
142
+ {
143
+ refName,
144
+ variant,
145
+ modelName: ConstantRegistry.getModelName(modelRef),
146
+ modelRef,
147
+ fields: getFields(modelRef, variant),
148
+ },
149
+ ];
150
+ }),
151
+ ) as Record<DatabaseModelVariant, ModelVariantSchema>;
152
+ return { kind: "database", refName, modelName: ConstantRegistry.getModelName(model.full), variants };
153
+ };
154
+
155
+ const getFields = (modelRef: ConstantCls, variant: DatabaseModelVariant | "scalar"): FieldSchema[] =>
156
+ Object.entries(modelRef[FIELD_META]).map(([key, field]) => buildFieldSchema(key, field, variant));
157
+
158
+ const buildFieldSchema = (
159
+ key: string,
160
+ field: ConstantField,
161
+ _variant: DatabaseModelVariant | "scalar",
162
+ ): FieldSchema => {
163
+ const props = field.getProps();
164
+ const typeInfo = getTypeInfo(props.modelRef, props.arrDepth, props.isMap, props.of as Cls | Cls[] | undefined);
165
+ const enumValues = props.enum ? ([...props.enum.values] as (string | number)[]) : undefined;
166
+ const relationLabel = props.refType ?? (typeInfo.typeKind === "database" ? "reference" : undefined);
167
+ return {
168
+ key,
169
+ typeLabel: typeInfo.typeLabel,
170
+ typeRefName: typeInfo.typeRefName,
171
+ typeKind: typeInfo.typeKind,
172
+ required: !props.nullable,
173
+ nullable: props.nullable,
174
+ arrDepth: props.arrDepth,
175
+ fieldType: props.fieldType,
176
+ select: props.select,
177
+ immutable: props.immutable,
178
+ ref: props.ref,
179
+ refPath: props.refPath,
180
+ refType: props.refType,
181
+ relationLabel,
182
+ enumRefName: props.enum?.refName,
183
+ enumValues,
184
+ defaultLabel: stringifyMetaValue(props.default),
185
+ exampleLabel: stringifyMetaValue(props.example),
186
+ constraints: getConstraints(props),
187
+ text: props.text,
188
+ meta: props.meta as Record<string, unknown>,
189
+ raw: field,
190
+ };
191
+ };
192
+
193
+ const getTypeInfo = (
194
+ modelRef: Cls,
195
+ arrDepth: number,
196
+ isMap: boolean,
197
+ of?: Cls | Cls[],
198
+ ): Pick<FieldSchema, "typeKind" | "typeLabel" | "typeRefName"> => {
199
+ if (isMap) {
200
+ const mapValue = of ? getNestedTypeInfo(of) : { label: "Unknown", refName: undefined };
201
+ return { typeKind: "map", typeLabel: `Map<String, ${mapValue.label}>`, typeRefName: mapValue.refName };
202
+ }
203
+ const refName = ConstantRegistry.getRefName(modelRef, { allowEmpty: true });
204
+ if (PrimitiveRegistry.has(modelRef)) {
205
+ return {
206
+ typeKind: "primitive",
207
+ typeLabel: formatArrayType(PrimitiveRegistry.getName(modelRef as typeof PrimitiveScalar), arrDepth),
208
+ };
209
+ }
210
+ if (refName && ConstantRegistry.database.has(refName)) {
211
+ return {
212
+ typeKind: "database",
213
+ typeLabel: formatArrayType(ConstantRegistry.getModelName(modelRef), arrDepth),
214
+ typeRefName: refName,
215
+ };
216
+ }
217
+ if (refName && ConstantRegistry.scalar.has(refName)) {
218
+ return {
219
+ typeKind: "scalar",
220
+ typeLabel: formatArrayType(getModelName(modelRef, refName), arrDepth),
221
+ typeRefName: refName,
222
+ };
223
+ }
224
+ return { typeKind: "unknown", typeLabel: formatArrayType("Unknown", arrDepth), typeRefName: refName };
225
+ };
226
+
227
+ const getModelName = (modelRef: Cls, refName: string) => {
228
+ if ((modelRef as ConstantCls).modelType) return ConstantRegistry.getModelName(modelRef);
229
+ return capitalize(refName);
230
+ };
231
+
232
+ const getNestedTypeInfo = (model: Cls | Cls[]) => {
233
+ const [modelRef, arrDepth] = getNonArrayModel(model as Cls);
234
+ const info = getTypeInfo(modelRef as Cls, arrDepth, modelRef === Map);
235
+ return { label: info.typeLabel, refName: info.typeRefName };
236
+ };
237
+
238
+ const formatArrayType = (name: string, arrDepth: number) => `${"[".repeat(arrDepth)}${name}${"]".repeat(arrDepth)}`;
239
+
240
+ const stringifyMetaValue = (value: unknown): string | undefined => {
241
+ if (value === undefined) return undefined;
242
+ if (typeof value === "function") return "[function]";
243
+ if (typeof value === "string") return value;
244
+ try {
245
+ return JSON.stringify(value);
246
+ } catch {
247
+ return String(value);
248
+ }
249
+ };
250
+
251
+ const getConstraints = (props: ReturnType<ConstantField["getProps"]>) =>
252
+ [
253
+ props.min !== undefined ? `min ${props.min}` : null,
254
+ props.max !== undefined ? `max ${props.max}` : null,
255
+ props.minlength !== undefined ? `minlength ${props.minlength}` : null,
256
+ props.maxlength !== undefined ? `maxlength ${props.maxlength}` : null,
257
+ props.text ? `text:${props.text}` : null,
258
+ props.validate ? "custom validate" : null,
259
+ props.accumulate ? "accumulate" : null,
260
+ ].filter((constraint): constraint is string => !!constraint);
261
+
262
+ const getSelectedEntries = <Value>(map: Map<string, Value>, selected?: string[]): [string, Value][] => {
263
+ if (!selected?.length) return [...map.entries()].sort(([a], [b]) => a.localeCompare(b));
264
+ return selected.flatMap((key) => {
265
+ const value = map.get(key);
266
+ return value ? ([[key, value]] as [string, Value][]) : [];
267
+ });
268
+ };
269
+
270
+ const collectEnumUsages = (databases: DatabaseSchema[], scalars: ScalarSchema[]) => {
271
+ const usages = new Map<EnumInstance, EnumUsage[]>();
272
+ const addUsage = (field: FieldSchema, usage: EnumUsage) => {
273
+ const enumRef = field.raw.enum;
274
+ if (!enumRef) return;
275
+ usages.set(enumRef, [...(usages.get(enumRef) ?? []), usage]);
276
+ };
277
+ databases.forEach((database) => {
278
+ Object.values(database.variants).forEach((variant) => {
279
+ variant.fields.forEach((field) => {
280
+ addUsage(field, { refName: database.refName, variant: variant.variant, fieldKey: field.key });
281
+ });
282
+ });
283
+ });
284
+ scalars.forEach((scalar) => {
285
+ scalar.fields.forEach((field) => {
286
+ addUsage(field, { refName: scalar.refName, variant: "scalar", fieldKey: field.key });
287
+ });
288
+ });
289
+ return usages;
290
+ };
291
+
292
+ const collectRelations = (databases: DatabaseSchema[], scalars: ScalarSchema[]): RelationSchema[] => {
293
+ const selectedDatabases = new Set(databases.map((database) => database.refName));
294
+ const selectedScalars = new Set(scalars.map((scalar) => scalar.refName));
295
+ const relations: RelationSchema[] = [];
296
+ const addRelations = (refName: string, variant: DatabaseModelVariant | "scalar", fields: FieldSchema[]) => {
297
+ fields.forEach((field) => {
298
+ if (
299
+ !field.typeRefName ||
300
+ (field.typeKind !== "database" && field.typeKind !== "scalar" && field.typeKind !== "map")
301
+ )
302
+ return;
303
+ const targetKind = ConstantRegistry.database.has(field.typeRefName) ? "database" : "scalar";
304
+ const selected =
305
+ targetKind === "database" ? selectedDatabases.has(field.typeRefName) : selectedScalars.has(field.typeRefName);
306
+ relations.push({
307
+ sourceRefName: refName,
308
+ sourceVariant: variant,
309
+ targetRefName: field.typeRefName,
310
+ targetKind,
311
+ fieldKey: field.key,
312
+ relationType: field.relationLabel ?? field.typeKind,
313
+ external: !selected,
314
+ });
315
+ });
316
+ };
317
+ databases.forEach((database) => {
318
+ addRelations(database.refName, "full", database.variants.full.fields);
319
+ });
320
+ scalars.forEach((scalar) => {
321
+ addRelations(scalar.refName, "scalar", scalar.fields);
322
+ });
323
+ return relations;
324
+ };
package/ui/Field.tsx CHANGED
@@ -678,7 +678,6 @@ const Tags = ({
678
678
  />
679
679
  ) : !disabled ? (
680
680
  <div
681
-
682
681
  className="flex items-center gap-2 rounded-full bg-success px-2 py-1 text-success-content text-xs duration-200 hover:cursor-pointer hover:opacity-80"
683
682
  onClick={() => {
684
683
  setInputVisible(true);
package/ui/Portal.tsx CHANGED
@@ -1,3 +1,5 @@
1
+ "use client";
2
+
1
3
  import { createPortal } from "react-dom";
2
4
 
3
5
  interface PortalProps {
package/ui/System/CSR.tsx CHANGED
@@ -15,7 +15,7 @@ import {
15
15
  import { st } from "akanjs/store";
16
16
  import { animated } from "akanjs/ui";
17
17
  import { useFetch, usePushNoti } from "akanjs/webkit";
18
- import { memo, type ReactNode, useEffect, useRef } from "react";
18
+ import { createElement, memo, type ReactNode, useEffect, useRef } from "react";
19
19
  import { createPortal } from "react-dom";
20
20
 
21
21
  import { FontFace } from "../FontFace";
@@ -305,9 +305,7 @@ const CSRPageContainer = ({ pathRoute, prefix, layoutStyle }: CSRPageContainerPr
305
305
  <animated.div
306
306
  id={`pageContainer-${pathRoute.path}`}
307
307
  style={{ ...(page?.containerStyle ?? {}), zIndex }}
308
- className={clsx("absolute top-0 left-0 isolate", {
309
- "w-full": layoutStyle === "web",
310
- "w-screen": layoutStyle === "mobile",
308
+ className={clsx("absolute top-0 left-0 isolate w-screen", {
311
309
  absolute: pageType !== "current",
312
310
  hidden: pageType === "cached",
313
311
  "pointer-events-none": pageType === "prev",
@@ -398,11 +396,14 @@ const RenderLayer = memo(({ renders, index, params, searchParams }: RenderLayerP
398
396
  <RenderLayer renders={renders} index={index + 1} params={params} searchParams={searchParams} />
399
397
  );
400
398
  const routeRender = renders[index];
399
+ const isAsyncRender = routeRender?.render.constructor.name === "AsyncFunction";
401
400
  const resultRef = useRef<ReactNode | Promise<ReactNode> | null>(null);
402
- if (resultRef.current === null) {
401
+ if (isAsyncRender && resultRef.current === null) {
403
402
  resultRef.current = routeRender?.render({ children, params, searchParams } as never) ?? null;
404
403
  }
405
404
  const { fulfilled, value: Component } = useFetch(resultRef.current);
405
+ if (!routeRender) return null;
406
+ if (!isAsyncRender) return createElement(routeRender.render as never, { children, params, searchParams } as never);
406
407
  if (!fulfilled || !Component) return <>{composeLoadingFallback(renders.slice(index), params)}</>;
407
408
  return <>{Component}</>;
408
409
  });
package/ui/System/SSR.tsx CHANGED
@@ -119,7 +119,7 @@ const SSRWrapper = ({
119
119
  <div id="pageContainer">
120
120
  <div
121
121
  id="pageContent"
122
- className={clsx("relative isolate bg-base-100", {
122
+ className={clsx("relative isolate", {
123
123
  "w-full": layoutStyle === "web",
124
124
  "left-1/2 h-screen w-[600px] -translate-x-1/2": layoutStyle === "mobile",
125
125
  })}
@@ -22,7 +22,7 @@ export const SelectLanguage = ({ className, languages = parseAkanI18nEnv().local
22
22
  id="select-language"
23
23
  tabIndex={0}
24
24
  role="button"
25
- className="btn btn-ghost btn-sm mx-2 my-auto min-h-0 rounded-btn px-3 font-medium text-xs md:mx-4"
25
+ className="btn btn-ghost btn-sm mx-2 my-auto min-h-0 border-none px-3 font-medium text-xs md:mx-4"
26
26
  >
27
27
  {languageNames[lang as keyof typeof languageNames]}
28
28
  </div>
package/ui/index.ts CHANGED
@@ -3,6 +3,7 @@ export { BottomSheet, type BottomSheetRef } from "./BottomSheet";
3
3
  export { Button } from "./Button";
4
4
  export { ClientSide } from "./ClientSide";
5
5
  export { Clipboard } from "./Clipboard";
6
+ export { Constant } from "./Constant";
6
7
  export { Copy } from "./Copy";
7
8
  export { CsrImage } from "./CsrImage";
8
9
  export { Data } from "./Data";
package/ui/styles.css CHANGED
@@ -1,6 +1,5 @@
1
1
  @source "./**/*";
2
2
  @plugin "tailwind-scrollbar";
3
- @plugin "tailwindcss-radix";
4
3
 
5
4
  html,
6
5
  body {
@@ -23,7 +23,7 @@ import {
23
23
  parseRouteModuleKey,
24
24
  routeSegmentToTreePath,
25
25
  } from "akanjs/common";
26
- import { memo, type ReactNode, useRef } from "react";
26
+ import { createElement, memo, type ReactNode, useRef } from "react";
27
27
  import * as ReactDOM from "react-dom/client";
28
28
  import { useCsrValues } from "./useCsrValues";
29
29
  import { useFetch } from "./useFetch";
@@ -48,11 +48,14 @@ const RootRenderLayer = memo(({ renders, index, params, searchParams }: RootRend
48
48
  <RootRenderLayer renders={renders} index={index + 1} params={params} searchParams={searchParams} />
49
49
  );
50
50
  const routeRender = renders[index];
51
+ const isAsyncRender = routeRender?.render.constructor.name === "AsyncFunction";
51
52
  const resultRef = useRef<ReactNode | Promise<ReactNode> | null>(null);
52
- if (resultRef.current === null) {
53
+ if (isAsyncRender && resultRef.current === null) {
53
54
  resultRef.current = routeRender?.render({ children, params, searchParams } as never) ?? null;
54
55
  }
55
56
  const { fulfilled, value: Layout } = useFetch(resultRef.current);
57
+ if (!routeRender) return null;
58
+ if (!isAsyncRender) return createElement(routeRender.render as never, { children, params, searchParams } as never);
56
59
  if (!fulfilled || !Layout) return <>{composeLoadingFallback(renders.slice(index), params)}</>;
57
60
  return Layout;
58
61
  });
@@ -101,7 +104,7 @@ export const bootCsr = async (context: Record<string, () => Promise<RouteModule>
101
104
  if (pageContent.default) pages[key] = pageContent;
102
105
  }),
103
106
  );
104
- const getPageState = (csrConfig?: PageConfig) => {
107
+ const getPageState = (PageConfig?: PageConfig) => {
105
108
  const {
106
109
  transition,
107
110
  safeArea,
@@ -111,7 +114,7 @@ export const bootCsr = async (context: Record<string, () => Promise<RouteModule>
111
114
  cache,
112
115
  topSafeAreaColor,
113
116
  bottomSafeAreaColor,
114
- }: PageConfig = csrConfig ?? {};
117
+ }: PageConfig = PageConfig ?? {};
115
118
  const pageState: PageState = {
116
119
  transition: transition ?? "none",
117
120
  topSafeArea:
@@ -159,7 +162,7 @@ export const bootCsr = async (context: Record<string, () => Promise<RouteModule>
159
162
  pageIncludesOwnLayout: parsed.leaf === "_index",
160
163
  isSpecialRoute: parsed.isSpecialRoute,
161
164
  pageState: getPageState((page as RouteModuleWithConfig).pageConfig),
162
- csrConfig: (page as RouteModuleWithConfig).pageConfig,
165
+ PageConfig: (page as RouteModuleWithConfig).pageConfig,
163
166
  }),
164
167
  } as Route);
165
168
  }
@@ -1,4 +0,0 @@
1
- declare const afterAll: typeof import("bun:test").afterAll;
2
- declare const describe: typeof import("bun:test").describe;
3
- declare const expect: typeof import("bun:test").expect;
4
- declare const test: typeof import("bun:test").test;
@@ -1,12 +0,0 @@
1
- import type { AppInfo, LibInfo } from "akanjs/devkit";
2
-
3
- interface Dict {
4
- appName: string;
5
- }
6
- export default function getContent(scanInfo: AppInfo | LibInfo | null, dict: Dict) {
7
- return `
8
-
9
- export const someCommonLogic = () => {
10
- };
11
- `;
12
- }
@@ -1,10 +0,0 @@
1
- import type { AppInfo, LibInfo } from "akanjs/devkit";
2
-
3
- interface Dict {
4
- [key: string]: string;
5
- }
6
- export default function getContent(scanInfo: AppInfo | LibInfo | null, dict: Dict = {}) {
7
- return `
8
- export * from "./commonLogic";
9
- `;
10
- }