akanjs 2.0.0-rc.7 → 2.0.1

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 (92) 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/templates/workspaceRoot/tsconfig.json.template +24 -21
  22. package/cli/workspace/workspace.command.ts +7 -9
  23. package/cli/workspace/workspace.runner.ts +3 -13
  24. package/cli/workspace/workspace.script.ts +24 -9
  25. package/client/csrTypes.ts +1 -1
  26. package/constant/fieldInfo.ts +1 -1
  27. package/constant/serialize.ts +7 -1
  28. package/devkit/capacitor.base.config.ts +1 -1
  29. package/devkit/capacitorApp.ts +5 -1
  30. package/devkit/commandDecorators/argMeta.ts +28 -14
  31. package/devkit/commandDecorators/command.ts +41 -15
  32. package/devkit/commandDecorators/commandBuilder.ts +78 -42
  33. package/devkit/commandDecorators/helpFormatter.ts +7 -4
  34. package/devkit/dependencyScanner.ts +121 -15
  35. package/devkit/executors.ts +35 -23
  36. package/devkit/frontendBuild/cssCompiler.ts +9 -3
  37. package/devkit/incrementalBuilder/incrementalBuilder.proc.ts +2 -1
  38. package/devkit/lint/no-deep-internal-import.grit +25 -0
  39. package/devkit/lint/no-import-external-library.grit +1 -0
  40. package/devkit/mobile/mobileTarget.ts +48 -8
  41. package/devkit/scanInfo.ts +4 -1
  42. package/devkit/src/capacitorApp.ts +277 -0
  43. package/devkit/transforms/barrelImportsPlugin.ts +6 -0
  44. package/fetch/client/fetchClient.ts +1 -0
  45. package/fetch/client/httpClient.ts +13 -1
  46. package/package.json +37 -31
  47. package/server/akanServer.ts +21 -7
  48. package/server/hmr/clientScript.ts +8 -5
  49. package/server/resolver/resolver.contract.fixture.ts +1 -1
  50. package/test/index.ts +14 -0
  51. package/test/signalTest.preload.ts +10 -0
  52. package/test/signalTestRuntime.ts +126 -0
  53. package/test/testServer.ts +130 -25
  54. package/ui/Constant/Doc.tsx +696 -0
  55. package/ui/Constant/Mermaid.tsx +149 -0
  56. package/ui/Constant/index.ts +6 -0
  57. package/ui/Constant/schemaDoc.ts +324 -0
  58. package/ui/Field.tsx +0 -1
  59. package/ui/Portal.tsx +2 -0
  60. package/ui/System/CSR.tsx +6 -5
  61. package/ui/System/SSR.tsx +1 -1
  62. package/ui/System/SelectLanguage.tsx +1 -1
  63. package/ui/index.ts +1 -0
  64. package/ui/styles.css +0 -1
  65. package/webkit/bootCsr.tsx +8 -5
  66. package/base/test-globals.d.ts +0 -4
  67. package/cli/templates/app/capacitor.config.ts.template +0 -8
  68. package/cli/templates/app/common/commonLogic.ts +0 -12
  69. package/cli/templates/app/common/index.ts +0 -10
  70. package/cli/templates/app/public/favicon.ico +0 -0
  71. package/cli/templates/app/public/icons/icon-128x128.png +0 -0
  72. package/cli/templates/app/public/icons/icon-144x144.png +0 -0
  73. package/cli/templates/app/public/icons/icon-152x152.png +0 -0
  74. package/cli/templates/app/public/icons/icon-192x192.png +0 -0
  75. package/cli/templates/app/public/icons/icon-256x256.png +0 -0
  76. package/cli/templates/app/public/icons/icon-384x384.png +0 -0
  77. package/cli/templates/app/public/icons/icon-48x48.png +0 -0
  78. package/cli/templates/app/public/icons/icon-512x512.png +0 -0
  79. package/cli/templates/app/public/icons/icon-72x72.png +0 -0
  80. package/cli/templates/app/public/icons/icon-96x96.png +0 -0
  81. package/cli/templates/app/public/logo.svg +0 -70
  82. package/cli/templates/app/public/manifest.json.template +0 -67
  83. package/cli/templates/app/srvkit/backendLogic.ts +0 -12
  84. package/cli/templates/app/srvkit/index.ts +0 -10
  85. package/cli/templates/app/ui/UiComponent.ts +0 -16
  86. package/cli/templates/app/ui/index.ts +0 -10
  87. package/cli/templates/app/webkit/frontendLogic.ts +0 -12
  88. package/cli/templates/app/webkit/index.ts +0 -10
  89. package/cli/templates/module/index.tsx +0 -44
  90. package/cli/templates/workspaceRoot/infra/app/Chart.yaml.template +0 -6
  91. package/cli/templates/workspaceRoot/infra/app/templates/frontend.yaml.template +0 -182
  92. package/cli/templates/workspaceRoot/infra/app/values/_common-values.yaml.template +0 -183
@@ -0,0 +1,126 @@
1
+ import type { BackendEnv } from "akanjs/base";
2
+ import type { FetchProxy } from "akanjs/fetch";
3
+ import type { AkanLib } from "akanjs/server";
4
+ import { TestServer, type TestServerOptions } from "./testServer";
5
+
6
+ export interface SignalTestTarget {
7
+ type: "app" | "lib";
8
+ name: string;
9
+ env: BackendEnv;
10
+ fetch: FetchProxy;
11
+ libs: AkanLib[];
12
+ }
13
+
14
+ export interface SignalTestContext<Fetch = FetchProxy> {
15
+ fetch: Fetch;
16
+ target: SignalTestTarget;
17
+ terminate: () => Promise<void>;
18
+ }
19
+
20
+ export type SignalTestOptions = Pick<TestServerOptions, "databaseMode" | "workerId" | "port" | "serverMode">;
21
+
22
+ type SignalServerModule = {
23
+ env: BackendEnv;
24
+ fetch: FetchProxy;
25
+ lib: AkanLib;
26
+ };
27
+
28
+ let currentContext: SignalTestContext | undefined;
29
+ let pendingContext: Promise<SignalTestContext> | undefined;
30
+ let terminatingContext: Promise<void> | undefined;
31
+ let configuredOptions: SignalTestOptions = {};
32
+
33
+ export const hasSignalTestContext = () => currentContext !== undefined || pendingContext !== undefined;
34
+
35
+ export const getSignalTestContext = <Fetch = FetchProxy>() => {
36
+ if (!currentContext) {
37
+ throw new Error("Signal test context is not initialized. Run through `akan test` or call setupSignalTestTarget().");
38
+ }
39
+ return currentContext as SignalTestContext<Fetch>;
40
+ };
41
+
42
+ export const getSignalTestFetch = <Fetch = FetchProxy>() => getSignalTestContext<Fetch>().fetch;
43
+
44
+ export const getOrSetupSignalTestContext = async <Fetch = FetchProxy>() =>
45
+ (await setupSignalTestTarget()) as SignalTestContext<Fetch>;
46
+
47
+ export const getOrSetupSignalTestFetch = async <Fetch = FetchProxy>() =>
48
+ (await getOrSetupSignalTestContext<Fetch>()).fetch;
49
+
50
+ export const configureSignalTest = (options: SignalTestOptions) => {
51
+ if (currentContext || pendingContext) {
52
+ throw new Error("configureSignalTest() must be called before the signal test context is initialized.");
53
+ }
54
+ configuredOptions = { ...configuredOptions, ...options };
55
+ };
56
+
57
+ export const terminateSignalTestContext = async () => {
58
+ if (terminatingContext) return terminatingContext;
59
+ const context = currentContext;
60
+ currentContext = undefined;
61
+ pendingContext = undefined;
62
+ terminatingContext = context?.terminate() ?? Promise.resolve();
63
+ await terminatingContext;
64
+ terminatingContext = undefined;
65
+ };
66
+
67
+ const importServerModule = async (type: "app" | "lib", name: string): Promise<SignalServerModule> => {
68
+ return type === "app" ? await import(`@apps/${name}/server`) : await import(`@libs/${name}/server`);
69
+ };
70
+
71
+ const importLibModule = async (name: string): Promise<SignalServerModule> => {
72
+ return await import(`@libs/${name}/server`);
73
+ };
74
+
75
+ export const setupSignalTestTarget = async <Fetch = FetchProxy>(
76
+ {
77
+ type = process.env.AKAN_TEST_TARGET_TYPE as "app" | "lib" | undefined,
78
+ name = process.env.AKAN_TEST_TARGET_NAME,
79
+ libNames = process.env.AKAN_TEST_LIBS?.split(",").filter(Boolean),
80
+ }: { type?: "app" | "lib"; name?: string; libNames?: string[] } = {},
81
+ options: SignalTestOptions = {},
82
+ ) => {
83
+ if (currentContext) return currentContext as SignalTestContext<Fetch>;
84
+ if (pendingContext) return pendingContext as Promise<SignalTestContext<Fetch>>;
85
+ if (!type || !name) throw new Error("Signal test target is not configured.");
86
+
87
+ pendingContext = (async () => {
88
+ terminatingContext = undefined;
89
+ const resolvedOptions = { ...configuredOptions, ...options };
90
+ const env: BackendEnv = {
91
+ repoName: process.env.AKAN_PUBLIC_REPO_NAME ?? "akansoft",
92
+ serveDomain: process.env.AKAN_PUBLIC_SERVE_DOMAIN ?? "akamir.com",
93
+ appName: name,
94
+ environment: "testing",
95
+ operationMode: "local",
96
+ tunnelUsername: process.env.SSH_TUNNEL_USERNAME ?? "root",
97
+ tunnelPassword: process.env.SSH_TUNNEL_PASSWORD ?? process.env.AKAN_PUBLIC_REPO_NAME ?? "akansoft",
98
+ };
99
+ TestServer.applyProcessEnv(env, resolvedOptions);
100
+
101
+ const [dependencyModules, targetModule] = await Promise.all([
102
+ Promise.all((libNames ?? []).filter((libName) => libName !== name).map(importLibModule)),
103
+ importServerModule(type, name),
104
+ ]);
105
+ const target: SignalTestTarget = {
106
+ type,
107
+ name,
108
+ env: targetModule.env,
109
+ fetch: targetModule.fetch,
110
+ libs: [...dependencyModules.map((mod) => mod.lib), targetModule.lib],
111
+ };
112
+ const testServer = new TestServer(target.env, target.libs, {
113
+ databaseMode: "memory",
114
+ ...resolvedOptions,
115
+ });
116
+ await testServer.init();
117
+ currentContext = {
118
+ fetch: target.fetch,
119
+ target,
120
+ terminate: () => testServer.terminate(),
121
+ };
122
+ return currentContext;
123
+ })();
124
+
125
+ return pendingContext as Promise<SignalTestContext<Fetch>>;
126
+ };
@@ -1,36 +1,99 @@
1
+ import { mkdtemp, rm } from "node:fs/promises";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
1
4
  import type { BackendEnv } from "akanjs/base";
2
5
  import { Logger, sleep } from "akanjs/common";
6
+ import { type AkanLib, AkanServer } from "akanjs/server";
3
7
 
4
8
  const MAX_RETRY = 5;
5
9
  const TEST_LISTEN_PORT_BASE = 38080;
6
- const TEST_REDIS_PORT_BASE = 38082;
7
10
  const MIN_ACTIVATION_TIME = 0;
8
11
  const MAX_ACTIVATION_TIME = 30000;
9
12
 
13
+ type TestDatabaseMode = "memory" | "tempFile";
14
+
15
+ export interface TestServerOptions {
16
+ workerId?: number;
17
+ port?: number;
18
+ databaseMode?: TestDatabaseMode;
19
+ serverMode?: "federation" | "batch" | "all";
20
+ listen?: boolean;
21
+ web?: boolean;
22
+ }
23
+
24
+ const TEST_ENV_KEYS = [
25
+ "NODE_ENV",
26
+ "SERVER_MODE",
27
+ "PORT",
28
+ "AKAN_PUBLIC_APP_NAME",
29
+ "AKAN_PUBLIC_REPO_NAME",
30
+ "AKAN_PUBLIC_SERVE_DOMAIN",
31
+ "AKAN_PUBLIC_ENV",
32
+ "AKAN_PUBLIC_OPERATION_MODE",
33
+ "AKAN_PUBLIC_SERVER_PORT",
34
+ "SERVER_HTTP_PROTOCOL",
35
+ ] as const;
36
+
37
+ const resolveWorkerId = (workerId?: number) => {
38
+ if (workerId !== undefined) return workerId;
39
+ const bunWorkerId = Number(process.env.BUN_WORKER_ID);
40
+ if (Number.isInteger(bunWorkerId) && bunWorkerId > 0) return bunWorkerId;
41
+ return Math.max(process.pid % 1000, 1);
42
+ };
43
+
10
44
  export class TestServer {
11
45
  readonly #logger = new Logger("TestServer");
12
- readonly #redis: any;
13
-
46
+ readonly #libs: AkanLib[];
14
47
  readonly #env: BackendEnv;
48
+ readonly #databaseMode: TestDatabaseMode;
49
+ readonly #serverMode: "federation" | "batch" | "all";
50
+ readonly #listen: boolean;
51
+ readonly #web: boolean;
52
+ readonly #port: number;
53
+ readonly #previousEnv = new Map<(typeof TEST_ENV_KEYS)[number], string | undefined>();
15
54
  workerId: number;
16
55
  #startAt = Date.now();
17
-
18
- #portOffset = 0;
19
- static initClient(env: BackendEnv, workerId = parseInt(process.env.JEST_WORKER_ID ?? "0")) {
20
- if (workerId === 0) throw new Error("TestClient should not be used in main thread");
56
+ #server?: AkanServer;
57
+ #tempDir?: string;
58
+ static initClient(env: BackendEnv, workerId?: number) {
59
+ TestServer.applyProcessEnv(env, {
60
+ workerId,
61
+ port: TEST_LISTEN_PORT_BASE + resolveWorkerId(workerId),
62
+ serverMode: "all",
63
+ });
21
64
  }
22
- constructor(
23
-
65
+ static applyProcessEnv(
24
66
  env: BackendEnv,
25
- workerId?: number,
67
+ {
68
+ workerId,
69
+ port,
70
+ serverMode = "all",
71
+ }: { workerId?: number; port?: number; serverMode?: "federation" | "batch" | "all" } = {},
26
72
  ) {
27
- this.workerId = workerId ?? parseInt(process.env.JEST_WORKER_ID ?? "0");
28
- if (this.workerId === 0) throw new Error("TestServer should not be used in main thread");
29
- this.#portOffset = this.workerId * 1000;
30
- this.#redis = 1 as any;
73
+ const resolvedPort = port ?? TEST_LISTEN_PORT_BASE + resolveWorkerId(workerId);
74
+ process.env.NODE_ENV = "test";
75
+ process.env.SERVER_MODE = serverMode;
76
+ process.env.PORT = String(resolvedPort);
77
+ process.env.AKAN_PUBLIC_APP_NAME = env.appName;
78
+ process.env.AKAN_PUBLIC_REPO_NAME = env.repoName;
79
+ process.env.AKAN_PUBLIC_SERVE_DOMAIN = env.serveDomain;
80
+ process.env.AKAN_PUBLIC_ENV = env.environment;
81
+ process.env.AKAN_PUBLIC_OPERATION_MODE = env.operationMode;
82
+ process.env.AKAN_PUBLIC_SERVER_PORT = String(resolvedPort);
83
+ process.env.SERVER_HTTP_PROTOCOL = "http:";
84
+ }
85
+ constructor(env: BackendEnv, libs: AkanLib | AkanLib[], options: TestServerOptions = {}) {
86
+ this.workerId = resolveWorkerId(options.workerId);
87
+ this.#port = options.port ?? TEST_LISTEN_PORT_BASE + this.workerId;
31
88
  this.#env = { ...env };
89
+ this.#libs = Array.isArray(libs) ? libs : [libs];
90
+ this.#databaseMode = options.databaseMode ?? "memory";
91
+ this.#serverMode = options.serverMode ?? "all";
92
+ this.#listen = options.listen ?? true;
93
+ this.#web = options.web ?? false;
32
94
  }
33
95
  async init() {
96
+ let lastError: unknown;
34
97
  for (let i = 0; i < MAX_RETRY; i++) {
35
98
  try {
36
99
  const watchdog = setTimeout(() => {
@@ -40,43 +103,85 @@ export class TestServer {
40
103
  clearTimeout(watchdog);
41
104
  return;
42
105
  } catch (e) {
106
+ lastError = e;
43
107
  this.#logger.error(e as string);
44
108
  await this.terminate();
45
109
  }
46
110
  }
111
+ throw lastError instanceof Error ? lastError : new Error("TestServer Init Failed");
47
112
  }
48
113
  async #init() {
49
114
  const now = Date.now();
50
115
  this.#logger.log(`Test System #${this.workerId} Initializing...`);
51
- const port = TEST_LISTEN_PORT_BASE + this.#portOffset;
52
- const [redisHost, redisPort] = await Promise.all([this.#redis.getHost(), this.#redis.getPort()]);
53
- this.#env.port = port;
116
+ this.#rememberProcessEnv();
117
+ TestServer.applyProcessEnv(this.#env, { workerId: this.workerId, port: this.#port, serverMode: this.#serverMode });
118
+ const { databaseFilePath, solidFilePath } = await this.#makeDatabaseFiles();
119
+ this.#env.port = this.#port;
54
120
  this.#env.database = {
55
121
  driver: "sqlite",
56
- sqlite: { filePath: `${process.cwd()}/local/test/${this.#env.appName}_${this.workerId}.db` },
122
+ sqlite: {
123
+ filePath: databaseFilePath,
124
+ journalMode: this.#databaseMode === "memory" ? "MEMORY" : "WAL",
125
+ synchronous: this.#databaseMode === "memory" ? "OFF" : "NORMAL",
126
+ foreignKeys: true,
127
+ },
128
+ };
129
+ this.#env.solid = {
130
+ filePath: solidFilePath,
131
+ journalMode: this.#databaseMode === "memory" ? "MEMORY" : "WAL",
132
+ synchronous: this.#databaseMode === "memory" ? "OFF" : "NORMAL",
133
+ cleanupIntervalMs: 60_000,
134
+ queuePollIntervalMs: 60_000,
135
+ queueLeaseMs: 30_000,
57
136
  };
58
-
59
137
  this.#env.onCleanup = async () => {
60
138
  await this.cleanup();
61
139
  };
62
-
63
- this.#logger.log(`Test System #${this.workerId} Initialized, SQLite, Redis: ${redisHost}:${redisPort}`);
140
+ this.#server = new AkanServer(this.#env.appName, this.#env, this.#serverMode, ...this.#libs);
141
+ await this.#server.start({ listen: this.#listen, web: this.#web });
142
+ this.#logger.log(`Test System #${this.workerId} Initialized, SQLite: ${this.#databaseMode}`);
64
143
  this.#startAt = Date.now();
65
144
  this.#logger.log(`Test System #${this.workerId} Activation Time: ${this.#startAt - now}ms`);
66
145
  }
67
146
  async cleanup() {
68
- this.#logger.log("SQLite test database cleanup is handled by file replacement.");
147
+ this.#logger.log("SQLite test database cleanup is handled by server termination.");
69
148
  }
70
149
  async terminate() {
71
150
  const now = Date.now();
72
151
  const elapsed = now - this.#startAt;
73
- await sleep(50);
74
-
152
+ await sleep(50);
153
+ await this.#server?.stop();
154
+ this.#server = undefined;
155
+ if (this.#tempDir) {
156
+ await rm(this.#tempDir, { recursive: true, force: true });
157
+ this.#tempDir = undefined;
158
+ }
159
+ this.#restoreProcessEnv();
75
160
  if (elapsed < MIN_ACTIVATION_TIME) {
76
161
  this.#logger.log(`waiting for ${MIN_ACTIVATION_TIME - elapsed}`);
77
162
  await sleep(MIN_ACTIVATION_TIME - elapsed);
78
163
  }
79
- await Promise.all([this.#redis.stop()]);
80
164
  this.#logger.log(`System Terminated in ${Date.now() - now}ms`);
81
165
  }
166
+ #rememberProcessEnv() {
167
+ this.#previousEnv.clear();
168
+ TEST_ENV_KEYS.forEach((key) => {
169
+ this.#previousEnv.set(key, process.env[key]);
170
+ });
171
+ }
172
+ #restoreProcessEnv() {
173
+ this.#previousEnv.forEach((value, key) => {
174
+ if (value === undefined) delete process.env[key];
175
+ else process.env[key] = value;
176
+ });
177
+ this.#previousEnv.clear();
178
+ }
179
+ async #makeDatabaseFiles() {
180
+ if (this.#databaseMode === "memory") return { databaseFilePath: ":memory:", solidFilePath: ":memory:" };
181
+ this.#tempDir = await mkdtemp(join(tmpdir(), `akan-${this.#env.appName}-${this.workerId}-`));
182
+ return {
183
+ databaseFilePath: join(this.#tempDir, `${this.#env.appName}.db`),
184
+ solidFilePath: join(this.#tempDir, `${this.#env.appName}.solid.db`),
185
+ };
186
+ }
82
187
  }