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.
- package/base/primitiveRegistry.ts +28 -2
- package/cli/application/application.command.ts +11 -3
- package/cli/application/application.runner.ts +17 -1
- package/cli/guidelines/databaseModule/databaseModule.instruction.md +1 -1
- package/cli/guidelines/modelConstant/modelConstant.instruction.md +5 -5
- package/cli/guidelines/modelDocument/modelDocument.instruction.md +34 -61
- package/cli/guidelines/modelService/modelService.instruction.md +1 -1
- package/cli/index.js +9321 -19222
- package/cli/library/library.runner.ts +14 -13
- package/cli/package/package.runner.ts +31 -6
- package/cli/package/package.script.ts +2 -2
- package/cli/templates/app/page/_index.tsx +200 -79
- package/cli/templates/app/page/_layout.tsx +0 -1
- package/cli/templates/app/public/favicon.ico.template +0 -0
- package/cli/templates/app/public/logo.png.template +0 -0
- package/cli/templates/module/__Model__.Zone.tsx +1 -1
- package/cli/templates/module/__model__.document.ts +1 -1
- package/cli/templates/workspaceRoot/.gitignore.template +1 -11
- package/cli/templates/workspaceRoot/biome.json.template +16 -0
- package/cli/templates/workspaceRoot/package.json.template +1 -5
- package/cli/workspace/workspace.command.ts +7 -9
- package/cli/workspace/workspace.runner.ts +3 -13
- package/cli/workspace/workspace.script.ts +24 -9
- package/client/csrTypes.ts +1 -1
- package/constant/fieldInfo.ts +1 -1
- package/constant/serialize.ts +7 -1
- package/devkit/capacitor.base.config.ts +1 -1
- package/devkit/capacitorApp.ts +5 -1
- package/devkit/commandDecorators/argMeta.ts +28 -14
- package/devkit/commandDecorators/command.ts +41 -15
- package/devkit/commandDecorators/commandBuilder.ts +78 -42
- package/devkit/commandDecorators/helpFormatter.ts +7 -4
- package/devkit/dependencyScanner.ts +121 -15
- package/devkit/executors.ts +35 -23
- package/devkit/frontendBuild/cssCompiler.ts +9 -3
- package/devkit/incrementalBuilder/incrementalBuilder.proc.ts +2 -1
- package/devkit/lint/no-deep-internal-import.grit +25 -0
- package/devkit/lint/no-import-external-library.grit +1 -0
- package/devkit/mobile/mobileTarget.ts +48 -8
- package/devkit/scanInfo.ts +4 -1
- package/devkit/src/capacitorApp.ts +277 -0
- package/devkit/transforms/barrelImportsPlugin.ts +6 -0
- package/fetch/client/fetchClient.ts +1 -0
- package/fetch/client/httpClient.ts +13 -1
- package/package.json +37 -31
- package/server/akanServer.ts +21 -7
- package/server/hmr/clientScript.ts +8 -5
- package/server/resolver/resolver.contract.fixture.ts +1 -1
- package/test/index.ts +14 -0
- package/test/signalTest.preload.ts +10 -0
- package/test/signalTestRuntime.ts +126 -0
- package/test/testServer.ts +130 -25
- package/ui/Constant/Doc.tsx +696 -0
- package/ui/Constant/Mermaid.tsx +149 -0
- package/ui/Constant/index.ts +6 -0
- package/ui/Constant/schemaDoc.ts +324 -0
- package/ui/Field.tsx +0 -1
- package/ui/Portal.tsx +2 -0
- package/ui/System/CSR.tsx +6 -5
- package/ui/System/SSR.tsx +1 -1
- package/ui/System/SelectLanguage.tsx +1 -1
- package/ui/index.ts +1 -0
- package/ui/styles.css +0 -1
- package/webkit/bootCsr.tsx +8 -5
- package/base/test-globals.d.ts +0 -4
- package/cli/templates/app/common/commonLogic.ts +0 -12
- package/cli/templates/app/common/index.ts +0 -10
- package/cli/templates/app/public/favicon.ico +0 -0
- package/cli/templates/app/public/icons/icon-128x128.png +0 -0
- package/cli/templates/app/public/icons/icon-144x144.png +0 -0
- package/cli/templates/app/public/icons/icon-152x152.png +0 -0
- package/cli/templates/app/public/icons/icon-192x192.png +0 -0
- package/cli/templates/app/public/icons/icon-256x256.png +0 -0
- package/cli/templates/app/public/icons/icon-384x384.png +0 -0
- package/cli/templates/app/public/icons/icon-48x48.png +0 -0
- package/cli/templates/app/public/icons/icon-512x512.png +0 -0
- package/cli/templates/app/public/icons/icon-72x72.png +0 -0
- package/cli/templates/app/public/icons/icon-96x96.png +0 -0
- package/cli/templates/app/public/logo.svg +0 -70
- package/cli/templates/app/public/manifest.json.template +0 -67
- package/cli/templates/app/srvkit/backendLogic.ts +0 -12
- package/cli/templates/app/srvkit/index.ts +0 -10
- package/cli/templates/app/ui/UiComponent.ts +0 -16
- package/cli/templates/app/ui/index.ts +0 -10
- package/cli/templates/app/webkit/frontendLogic.ts +0 -12
- package/cli/templates/app/webkit/index.ts +0 -10
- 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,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
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
|
|
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
|
|
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
package/webkit/bootCsr.tsx
CHANGED
|
@@ -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 = (
|
|
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 =
|
|
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
|
-
|
|
165
|
+
PageConfig: (page as RouteModuleWithConfig).pageConfig,
|
|
163
166
|
}),
|
|
164
167
|
} as Route);
|
|
165
168
|
}
|
package/base/test-globals.d.ts
DELETED
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|