@akanjs/devkit 2.1.1-rc.2 → 2.1.2-rc.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.
@@ -47,8 +47,18 @@ const DEFAULT_OPTIMIZE_IMPORTS = [
47
47
  "mui-core",
48
48
  "react-icons/*",
49
49
  ];
50
- const WORKSPACE_BARREL_FACETS = ["ui", "webkit", "common", "client", "server"] as const;
51
- const SSR_RUNTIME_PACKAGES = ["react", "react-dom", "react-server-dom-webpack"] as const;
50
+ const WORKSPACE_BARREL_FACETS = [
51
+ "ui",
52
+ "webkit",
53
+ "common",
54
+ "client",
55
+ "server",
56
+ ] as const;
57
+ const SSR_RUNTIME_PACKAGES = [
58
+ "react",
59
+ "react-dom",
60
+ "react-server-dom-webpack",
61
+ ] as const;
52
62
  const NATIVE_RUNTIME_PACKAGES = ["sharp"] as const;
53
63
  const DEFAULT_BACKEND_RUNTIME_PACKAGES = ["croner"] as const;
54
64
  const DATABASE_MODE_RUNTIME_PACKAGES = {
@@ -111,19 +121,31 @@ export class AkanAppConfig implements AppConfigResult {
111
121
  this.barrelImports = [
112
122
  ...DEFAULT_BARREL_IMPORTS,
113
123
  ...WORKSPACE_BARREL_FACETS.map((facet) => `@apps/${app.name}/${facet}`),
114
- ...libs.flatMap((lib) => WORKSPACE_BARREL_FACETS.map((facet) => `@libs/${lib}/${facet}`)),
124
+ ...libs.flatMap((lib) =>
125
+ WORKSPACE_BARREL_FACETS.map((facet) => `@libs/${lib}/${facet}`),
126
+ ),
115
127
  ...(config?.barrelImports ?? []),
116
128
  ];
117
- this.optimizeImports = [...new Set([...DEFAULT_OPTIMIZE_IMPORTS, ...(config?.optimizeImports ?? [])])];
118
- this.images = mergeImageConfig(config?.images as Partial<AkanImageConfig> | undefined);
129
+ this.optimizeImports = [
130
+ ...new Set([
131
+ ...DEFAULT_OPTIMIZE_IMPORTS,
132
+ ...(config?.optimizeImports ?? []),
133
+ ]),
134
+ ];
135
+ this.images = mergeImageConfig(
136
+ config?.images as Partial<AkanImageConfig> | undefined,
137
+ );
119
138
  this.i18n = resolveAkanI18nConfig(config?.i18n);
120
139
  process.env.AKAN_PUBLIC_DEFAULT_LOCALE = this.i18n.defaultLocale;
121
140
  process.env.AKAN_PUBLIC_LOCALES = this.i18n.locales.join(",");
122
- this.publicEnv = (config?.publicEnv as string[] | undefined) ?? ([] as string[]);
141
+ this.publicEnv =
142
+ (config?.publicEnv as string[] | undefined) ?? ([] as string[]);
123
143
  this.mobile = this.#resolveMobileConfig(config.mobile);
124
144
  this.docker = this.#makeDockerContent(config?.docker ?? {});
125
145
  }
126
- #resolveMobileConfig(mobile: DeepPartial<AkanMobileConfig> | undefined): AkanMobileConfig {
146
+ #resolveMobileConfig(
147
+ mobile: DeepPartial<AkanMobileConfig> | undefined,
148
+ ): AkanMobileConfig {
127
149
  const { targets: rawTargets, ...rawMobile } = mobile ?? {};
128
150
  const appName = rawMobile.appName ?? this.app.name;
129
151
  const appId = rawMobile.appId ?? `com.${this.app.name}.app`;
@@ -138,8 +160,11 @@ export class AkanAppConfig implements AppConfigResult {
138
160
  const targets = Object.fromEntries(
139
161
  targetEntries.map(([name, rawTarget]) => {
140
162
  const target = rawTarget as DeepPartial<AkanMobileTargetConfig>;
141
- const fallbackBasePath = !rawTargets && this.basePaths.has(name) ? name : undefined;
142
- const basePath = (target.basePath ?? fallbackBasePath)?.replace(/^\/+|\/+$/g, "") || undefined;
163
+ const fallbackBasePath =
164
+ !rawTargets && this.basePaths.has(name) ? name : undefined;
165
+ const basePath =
166
+ (target.basePath ?? fallbackBasePath)?.replace(/^\/+|\/+$/g, "") ||
167
+ undefined;
143
168
  if (basePath && !this.basePaths.has(basePath)) {
144
169
  throw new Error(
145
170
  `Mobile target '${name}' uses unknown basePath '${basePath}' in apps/${this.app.name}/akan.config.ts`,
@@ -180,8 +205,11 @@ export class AkanAppConfig implements AppConfigResult {
180
205
  plugins: rawMobile.plugins,
181
206
  } as AkanMobileConfig;
182
207
  }
183
- #defaultMobileTargetName(rawTargets: DeepPartial<AkanMobileConfig>["targets"] | undefined) {
184
- if (rawTargets && Object.keys(rawTargets).length > 0) return Object.keys(rawTargets)[0] as string;
208
+ #defaultMobileTargetName(
209
+ rawTargets: DeepPartial<AkanMobileConfig>["targets"] | undefined,
210
+ ) {
211
+ if (rawTargets && Object.keys(rawTargets).length > 0)
212
+ return Object.keys(rawTargets)[0] as string;
185
213
  return this.basePaths.has(this.app.name) ? this.app.name : "default";
186
214
  }
187
215
  #applyRoutes(routes: AkanRouteConfig[] = []) {
@@ -190,28 +218,38 @@ export class AkanAppConfig implements AppConfigResult {
190
218
  const basePath = route.basePath.replace(/^\/+|\/+$/g, "");
191
219
  this.basePaths.add(basePath);
192
220
  const domains = this.subRoutes.getOrInsert(basePath, new Set());
193
- Object.keys(route.domains).forEach((branch) => void this.branches.add(branch));
221
+ Object.keys(route.domains).forEach(
222
+ (branch) => void this.branches.add(branch),
223
+ );
194
224
  Object.values(route.domains)
195
225
  .flat()
196
226
  .forEach((domain) => {
197
227
  if (domain) domains.add(domain.toLowerCase().replace(/:\d+$/, ""));
198
228
  });
199
229
  } else {
200
- Object.keys(route.domains).forEach((branch) => void this.branches.add(branch));
230
+ Object.keys(route.domains).forEach(
231
+ (branch) => void this.branches.add(branch),
232
+ );
201
233
  Object.values(route.domains)
202
234
  .flat()
203
235
  .forEach((domain) => {
204
- if (domain) this.domains.add(domain.toLowerCase().replace(/:\d+$/, ""));
236
+ if (domain)
237
+ this.domains.add(domain.toLowerCase().replace(/:\d+$/, ""));
205
238
  });
206
239
  }
207
240
  }
208
241
  const appName = this.app.name.toLowerCase();
209
242
  const serveDomain = this.baseDevEnv.serveDomain.toLowerCase();
210
243
  if (this.subRoutes.size === 0)
211
- this.branches.forEach((branch) => void this.domains.add(`${appName}-${branch}.${serveDomain}`));
244
+ this.branches.forEach(
245
+ (branch) =>
246
+ void this.domains.add(`${appName}-${branch}.${serveDomain}`),
247
+ );
212
248
  else
213
249
  Array.from(this.subRoutes.entries()).forEach(([basePath, domains]) => {
214
- this.branches.forEach((domain) => void domains.add(`${basePath}-${domain}.${serveDomain}`));
250
+ this.branches.forEach(
251
+ (domain) => void domains.add(`${basePath}-${domain}.${serveDomain}`),
252
+ );
215
253
  });
216
254
  }
217
255
  #getDockerRunScripts(runs: (string | { [key in Arch]?: string })[]) {
@@ -227,12 +265,25 @@ export class AkanAppConfig implements AppConfigResult {
227
265
  .join("\n");
228
266
  });
229
267
  }
230
- #getDockerImageScript(image: string | { [key in Arch]?: string }, defaultImage: string) {
268
+ #getDockerImageScript(
269
+ image: string | { [key in Arch]?: string },
270
+ defaultImage: string,
271
+ ) {
231
272
  if (typeof image === "string") return `FROM ${image}`;
232
- else return archs.map((arch) => `FROM ${image[arch] ?? defaultImage} AS ${arch}`).join("\n");
273
+ else
274
+ return archs
275
+ .map((arch) => `FROM ${image[arch] ?? defaultImage} AS ${arch}`)
276
+ .join("\n");
233
277
  }
234
278
  #makeDockerContent(docker: DeepPartial<DockerConfig>): DockerConfig {
235
- if (docker.content) return { content: docker.content, image: {}, preRuns: [], postRuns: [], command: [] };
279
+ if (docker.content)
280
+ return {
281
+ content: docker.content,
282
+ image: {},
283
+ preRuns: [],
284
+ postRuns: [],
285
+ command: [],
286
+ };
236
287
  const preRunScripts = this.#getDockerRunScripts(docker.preRuns ?? []);
237
288
  const postRunScripts = this.#getDockerRunScripts(docker.postRuns ?? []);
238
289
 
@@ -264,12 +315,20 @@ ENV AKAN_PUBLIC_LOCALES=${this.i18n.locales.join(",")}
264
315
  ENV AKAN_PUBLIC_OPERATION_MODE=cloud
265
316
 
266
317
  CMD [${command.map((c) => `"${c}"`).join(",")}]`;
267
- return { content, image: imageScript, preRuns: docker.preRuns ?? [], postRuns: docker.postRuns ?? [], command };
318
+ return {
319
+ content,
320
+ image: imageScript,
321
+ preRuns: docker.preRuns ?? [],
322
+ postRuns: docker.postRuns ?? [],
323
+ command,
324
+ };
268
325
  }
269
326
  static async from(app: App) {
270
327
  const [configImp, baseDevEnv, libs, rootPackageJson] = await Promise.all([
271
328
  import(`${app.cwdPath}/akan.config.ts`).then((mod) => mod.default),
272
- WorkspaceExecutor.getBaseDevEnv(path.join(app.workspace.workspaceRoot, ".env")),
329
+ WorkspaceExecutor.getBaseDevEnv(
330
+ path.join(app.workspace.workspaceRoot, ".env"),
331
+ ),
273
332
  app.workspace.getLibs(),
274
333
  app.workspace.getPackageJson(),
275
334
  ]);
@@ -277,11 +336,16 @@ CMD [${command.map((c) => `"${c}"`).join(",")}]`;
277
336
  return new AkanAppConfig(app, libs, rootPackageJson, config, baseDevEnv);
278
337
  }
279
338
  #resolveProductionDependencyVersion(lib: string) {
280
- const rootVersion = this.rootPackageJson.dependencies?.[lib] ?? this.rootPackageJson.devDependencies?.[lib];
339
+ const rootVersion =
340
+ this.rootPackageJson.dependencies?.[lib] ??
341
+ this.rootPackageJson.devDependencies?.[lib];
281
342
  if (rootVersion) return rootVersion;
282
343
  const akanPackageJson = getAkanPackageJson();
283
344
  if (AKAN_RUNTIME_PACKAGES.has(lib))
284
- return akanPackageJson.dependencies?.[lib] ?? akanPackageJson.peerDependencies?.[lib];
345
+ return (
346
+ akanPackageJson.dependencies?.[lib] ??
347
+ akanPackageJson.peerDependencies?.[lib]
348
+ );
285
349
  }
286
350
  #getProductionRuntimePackages() {
287
351
  return [
@@ -289,9 +353,30 @@ CMD [${command.map((c) => `"${c}"`).join(",")}]`;
289
353
  ...SSR_RUNTIME_PACKAGES,
290
354
  ...NATIVE_RUNTIME_PACKAGES,
291
355
  ...DEFAULT_BACKEND_RUNTIME_PACKAGES,
292
- ...DATABASE_MODE_RUNTIME_PACKAGES[this.defaultDatabaseMode],
356
+ ...this.getDatabaseModeRuntimePackages(),
293
357
  ];
294
358
  }
359
+ getDatabaseModeRuntimePackages(
360
+ databaseMode: DatabaseMode = this.defaultDatabaseMode,
361
+ ) {
362
+ return [...DATABASE_MODE_RUNTIME_PACKAGES[databaseMode]];
363
+ }
364
+ getMissingDatabaseModeDependencySpecs(
365
+ databaseMode: DatabaseMode = this.defaultDatabaseMode,
366
+ ) {
367
+ const rootDependencies = {
368
+ ...this.rootPackageJson.dependencies,
369
+ ...this.rootPackageJson.devDependencies,
370
+ };
371
+ return this.getDatabaseModeRuntimePackages(databaseMode)
372
+ .filter((lib) => !rootDependencies[lib])
373
+ .map((lib) => {
374
+ const version = this.#resolveProductionDependencyVersion(lib);
375
+ if (!version)
376
+ throw new Error(`Dependency ${lib} not found in package.json`);
377
+ return `${lib}@${version}`;
378
+ });
379
+ }
295
380
  getProductionPackageJson(data: Partial<PackageJson> = {}): PackageJson {
296
381
  return {
297
382
  name: this.app.name,
@@ -301,7 +386,8 @@ CMD [${command.map((c) => `"${c}"`).join(",")}]`;
301
386
  dependencies: Object.fromEntries(
302
387
  [...new Set(this.#getProductionRuntimePackages())].map((lib) => {
303
388
  const version = this.#resolveProductionDependencyVersion(lib);
304
- if (!version) throw new Error(`Dependency ${lib} not found in package.json`);
389
+ if (!version)
390
+ throw new Error(`Dependency ${lib} not found in package.json`);
305
391
  return [lib, version];
306
392
  }),
307
393
  ),
@@ -327,17 +413,26 @@ function getAkanPackageJson() {
327
413
  }
328
414
  for (const packageJsonPath of packageJsonPaths) {
329
415
  try {
330
- akanPackageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8")) as PackageJson;
416
+ akanPackageJson = JSON.parse(
417
+ fs.readFileSync(packageJsonPath, "utf8"),
418
+ ) as PackageJson;
331
419
  return akanPackageJson;
332
420
  } catch {
333
421
  // Try the next known layout: source package first, bundled CLI package second.
334
422
  }
335
423
  }
336
- akanPackageJson = { name: "akanjs", version: "0.0.0", description: "akanjs", dependencies: {} };
424
+ akanPackageJson = {
425
+ name: "akanjs",
426
+ version: "0.0.0",
427
+ description: "akanjs",
428
+ dependencies: {},
429
+ };
337
430
  return akanPackageJson;
338
431
  }
339
432
 
340
- function mergeImageConfig(config: Partial<AkanImageConfig> = {}): AkanImageConfig {
433
+ function mergeImageConfig(
434
+ config: Partial<AkanImageConfig> = {},
435
+ ): AkanImageConfig {
341
436
  return {
342
437
  ...DEFAULT_AKAN_IMAGE_CONFIG,
343
438
  ...config,
@@ -345,8 +440,10 @@ function mergeImageConfig(config: Partial<AkanImageConfig> = {}): AkanImageConfi
345
440
  imageSizes: config.imageSizes ?? DEFAULT_AKAN_IMAGE_CONFIG.imageSizes,
346
441
  formats: config.formats ?? DEFAULT_AKAN_IMAGE_CONFIG.formats,
347
442
  qualities: config.qualities ?? DEFAULT_AKAN_IMAGE_CONFIG.qualities,
348
- remotePatterns: config.remotePatterns ?? DEFAULT_AKAN_IMAGE_CONFIG.remotePatterns,
349
- localPatterns: config.localPatterns ?? DEFAULT_AKAN_IMAGE_CONFIG.localPatterns,
443
+ remotePatterns:
444
+ config.remotePatterns ?? DEFAULT_AKAN_IMAGE_CONFIG.remotePatterns,
445
+ localPatterns:
446
+ config.localPatterns ?? DEFAULT_AKAN_IMAGE_CONFIG.localPatterns,
350
447
  };
351
448
  }
352
449
 
@@ -358,7 +455,9 @@ export class AkanLibConfig implements LibConfigResult {
358
455
  this.externalLibs = config?.externalLibs ?? [];
359
456
  }
360
457
  static async from(lib: Lib) {
361
- const [configImp] = await Promise.all([import(`${lib.cwdPath}/akan.config.ts`).then((mod) => mod.default)]);
458
+ const [configImp] = await Promise.all([
459
+ import(`${lib.cwdPath}/akan.config.ts`).then((mod) => mod.default),
460
+ ]);
362
461
  const config = typeof configImp === "function" ? configImp(lib) : configImp;
363
462
  return new AkanLibConfig(lib, config);
364
463
  }
@@ -46,6 +46,8 @@ describe("resolveSsrPageEntries", () => {
46
46
  const generatedSource = await Bun.file(groupedRoot?.moduleAbsPath ?? "").text();
47
47
  expect(generatedSource).toContain('import * as inheritedLayout from "../../../page/_layout.tsx";');
48
48
  expect(generatedSource).not.toContain("<System.Provider");
49
+ expect(generatedSource).toContain("export const NotFound = userLayout.NotFound ?? inheritedLayout.NotFound;");
50
+ expect(generatedSource).toContain("export const Error = userLayout.Error ?? inheritedLayout.Error;");
49
51
  expect(generatedSource).toContain(
50
52
  "<UserLayout params={params} searchParams={searchParams}>{children}</UserLayout>",
51
53
  );
@@ -135,8 +135,8 @@ async function writeGeneratedRootLayoutFile(opts: {
135
135
  ? `import UserLayout, * as userLayout from ${JSON.stringify(sourceSpecifier)};\n`
136
136
  : "const UserLayout = ({ children }) => children;\nconst userLayout = {};\n";
137
137
  const source = opts.includeSystemProvider
138
- ? `import type { LayoutProps, PageProps } from "akanjs/client";\nimport { loadFonts } from "akanjs/client";\nimport { System } from "akanjs/ui";\nimport { env } from "@apps/${opts.appName}/env/env.client";\n${clientImport}${inheritedImport}${userImport}\nconst userFonts = userLayout.fonts ?? inheritedLayout.fonts ?? [];\nconst defaultFonts = userFonts.filter((font) => font.default);\nif (defaultFonts.length > 1) throw new Error("[route-convention] only one default font is allowed per root layout");\nconst defaultFont = defaultFonts[0];\nconst defaultFontClassName = defaultFont ? (defaultFont.className ?? \`font-\${defaultFont.name}\`) : undefined;\n\nexport async function generateHead(props: PageProps) {\n if (userLayout.generateHead) return userLayout.generateHead(props);\n if (userLayout.head !== undefined) return userLayout.head;\n if (inheritedLayout.generateHead) return inheritedLayout.generateHead(props);\n return inheritedLayout.head;\n}\n\nexport default function GeneratedLayout({ children, params, searchParams }: LayoutProps) {\n return (\n <System.Provider\n of={GeneratedLayout as never}\n appName=${JSON.stringify(opts.appName)}\n ${prefix ? `prefix=${JSON.stringify(prefix)}\n ` : ""}params={params}\n manifest={userLayout.manifest ?? inheritedLayout.manifest}\n env={env}\n theme={userLayout.theme ?? inheritedLayout.theme}\n fonts={loadFonts(userFonts)}\n className={defaultFontClassName}\n gaTrackingId={userLayout.gaTrackingId ?? inheritedLayout.gaTrackingId}\n layoutStyle={userLayout.layoutStyle ?? inheritedLayout.layoutStyle}\n reconnect={userLayout.reconnect ?? inheritedLayout.reconnect ?? false}\n >\n <UserLayout params={params} searchParams={searchParams}>{children}</UserLayout>\n </System.Provider>\n );\n}\n`
139
- : `import type { LayoutProps, PageProps } from "akanjs/client";\n${inheritedImport}${userImport}\nexport async function generateHead(props: PageProps) {\n if (userLayout.generateHead) return userLayout.generateHead(props);\n if (userLayout.head !== undefined) return userLayout.head;\n if (inheritedLayout.generateHead) return inheritedLayout.generateHead(props);\n return inheritedLayout.head;\n}\n\nexport default function GeneratedLayout({ children, params, searchParams }: LayoutProps) {\n return <UserLayout params={params} searchParams={searchParams}>{children}</UserLayout>;\n}\n`;
138
+ ? `import type { LayoutProps, PageProps } from "akanjs/client";\nimport { loadFonts } from "akanjs/client";\nimport { System } from "akanjs/ui";\nimport { env } from "@apps/${opts.appName}/env/env.client";\n${clientImport}${inheritedImport}${userImport}\nconst userFonts = userLayout.fonts ?? inheritedLayout.fonts ?? [];\nconst defaultFonts = userFonts.filter((font) => font.default);\nif (defaultFonts.length > 1) throw new Error("[route-convention] only one default font is allowed per root layout");\nconst defaultFont = defaultFonts[0];\nconst defaultFontClassName = defaultFont ? (defaultFont.className ?? \`font-\${defaultFont.name}\`) : undefined;\n\nexport async function generateHead(props: PageProps) {\n if (userLayout.generateHead) return userLayout.generateHead(props);\n if (userLayout.head !== undefined) return userLayout.head;\n if (inheritedLayout.generateHead) return inheritedLayout.generateHead(props);\n return inheritedLayout.head;\n}\n\nexport const NotFound = userLayout.NotFound ?? inheritedLayout.NotFound;\nexport const Error = userLayout.Error ?? inheritedLayout.Error;\n\nexport default function GeneratedLayout({ children, params, searchParams }: LayoutProps) {\n return (\n <System.Provider\n of={GeneratedLayout as never}\n appName=${JSON.stringify(opts.appName)}\n ${prefix ? `prefix=${JSON.stringify(prefix)}\n ` : ""}params={params}\n manifest={userLayout.manifest ?? inheritedLayout.manifest}\n env={env}\n theme={userLayout.theme ?? inheritedLayout.theme}\n fonts={loadFonts(userFonts)}\n className={defaultFontClassName}\n gaTrackingId={userLayout.gaTrackingId ?? inheritedLayout.gaTrackingId}\n layoutStyle={userLayout.layoutStyle ?? inheritedLayout.layoutStyle}\n reconnect={userLayout.reconnect ?? inheritedLayout.reconnect ?? false}\n >\n <UserLayout params={params} searchParams={searchParams}>{children}</UserLayout>\n </System.Provider>\n );\n}\n`
139
+ : `import type { LayoutProps, PageProps } from "akanjs/client";\n${inheritedImport}${userImport}\nexport async function generateHead(props: PageProps) {\n if (userLayout.generateHead) return userLayout.generateHead(props);\n if (userLayout.head !== undefined) return userLayout.head;\n if (inheritedLayout.generateHead) return inheritedLayout.generateHead(props);\n return inheritedLayout.head;\n}\n\nexport const NotFound = userLayout.NotFound ?? inheritedLayout.NotFound;\nexport const Error = userLayout.Error ?? inheritedLayout.Error;\n\nexport default function GeneratedLayout({ children, params, searchParams }: LayoutProps) {\n return <UserLayout params={params} searchParams={searchParams}>{children}</UserLayout>;\n}\n`;
140
140
  await Bun.write(absPath, source);
141
141
  return absPath;
142
142
  }
package/cloud/cloudApi.ts CHANGED
@@ -1,9 +1,5 @@
1
- import {
2
- type AccessToken,
3
- type AccessTokenDto,
4
- akanCloudHost,
5
- type HostConfig,
6
- } from "./constants";
1
+ import type { Workspace } from "../commandDecorators";
2
+ import { type AccessToken, type AccessTokenDto, akanCloudHost, type HostConfig } from "./constants";
7
3
  import { GlobalConfig } from "./globalConfig";
8
4
 
9
5
  class HttpClient {
@@ -13,10 +9,7 @@ class HttpClient {
13
9
  this.baseUrl = baseUrl;
14
10
  this.headers = headers;
15
11
  }
16
- async get<T>(
17
- url: string,
18
- { headers }: { headers?: Record<string, string> } = {},
19
- ): Promise<T> {
12
+ async get<T>(url: string, { headers }: { headers?: Record<string, string> } = {}): Promise<T> {
20
13
  const response = await fetch(`${this.baseUrl}${url}`, {
21
14
  headers: {
22
15
  "Content-Type": "application/json",
@@ -26,25 +19,14 @@ class HttpClient {
26
19
  });
27
20
  return response.json();
28
21
  }
29
- async getFile(
30
- url: string,
31
- localPath: string,
32
- headers?: Record<string, string>,
33
- ): Promise<void> {
22
+ async getFile(url: string, localPath: string, headers?: Record<string, string>): Promise<void> {
34
23
  const response = await fetch(`${this.baseUrl}${url}`, {
35
24
  headers: { ...this.headers, ...headers },
36
25
  });
37
- if (!response.ok)
38
- throw new Error(
39
- `Failed to download file: ${response.status} ${response.statusText}`,
40
- );
26
+ if (!response.ok) throw new Error(`Failed to download file: ${response.status} ${response.statusText}`);
41
27
  await Bun.write(localPath, response);
42
28
  }
43
- async post<T>(
44
- url: string,
45
- data: unknown,
46
- { headers }: { headers?: Record<string, string> } = {},
47
- ): Promise<T> {
29
+ async post<T>(url: string, data: unknown, { headers }: { headers?: Record<string, string> } = {}): Promise<T> {
48
30
  const isFormData = data instanceof FormData;
49
31
  const response = await fetch(`${this.baseUrl}${url}`, {
50
32
  method: "POST",
@@ -64,12 +46,14 @@ class HttpClient {
64
46
  export class CloudApi {
65
47
  readonly #api: HttpClient;
66
48
  #accessToken: AccessToken | null = null;
49
+ #workspace: Workspace;
67
50
 
68
- static async fromHost(host?: string) {
51
+ static async fromHost(workspace: Workspace, host?: string) {
69
52
  const hostConfig = await GlobalConfig.getHostConfig(host);
70
- return new CloudApi(hostConfig);
53
+ return new CloudApi(workspace, hostConfig);
71
54
  }
72
- constructor(hostConfig: HostConfig) {
55
+ constructor(workspace: Workspace, hostConfig: HostConfig) {
56
+ this.#workspace = workspace;
73
57
  const host = akanCloudHost;
74
58
  this.#api = new HttpClient(`${host}/api`);
75
59
  this.#accessToken = hostConfig.auth?.accessToken ?? null;
@@ -83,25 +67,20 @@ export class CloudApi {
83
67
  const formData = new FormData();
84
68
  formData.append("devProjectId", devProjectId);
85
69
  formData.append("file", file);
86
- const data = await this.#api.post<boolean>(
87
- `/uploadEnv/${devProjectId}`,
88
- formData,
89
- );
70
+ const data = await this.#api.post<boolean>(`/uploadEnv/${devProjectId}`, formData);
90
71
  return data;
91
72
  }
92
- async downloadEnv(devProjectId: string, localPath: string): Promise<void> {
73
+ async downloadEnv(devProjectId: string): Promise<void> {
74
+ const localPath = `${this.#workspace.workspaceRoot}/local/env.tar`;
93
75
  await this.#api.getFile(`/downloadEnv/${devProjectId}`, localPath);
94
76
  }
95
77
  async getRemoteAuthToken(remoteId: string): Promise<AccessToken | null> {
96
78
  try {
97
79
  if (this.#accessToken) {
98
- if (GlobalConfig.needRefreshToken(this.#accessToken))
99
- return await this.refreshAuthToken();
100
- else return await this.refreshAuthToken();
80
+ if (GlobalConfig.needRefreshToken(this.#accessToken)) return await this.#refreshAuthToken();
81
+ else return await this.#refreshAuthToken();
101
82
  }
102
- const accessToken = await this.#api.get<AccessTokenDto>(
103
- `/getRemoteAuthToken/${remoteId}`,
104
- );
83
+ const accessToken = await this.#api.get<AccessTokenDto>(`/getRemoteAuthToken/${remoteId}`);
105
84
  this.#accessToken = GlobalConfig.toAccessToken(accessToken);
106
85
  this.#api.setHeaders({
107
86
  Authorization: `Bearer ${this.#accessToken.jwt}`,
@@ -111,22 +90,20 @@ export class CloudApi {
111
90
  return null;
112
91
  }
113
92
  }
114
- async refreshAuthToken(): Promise<AccessToken> {
115
- const response = await this.#api.post<AccessTokenDto>(
116
- `/refreshRemoteAuthToken`,
117
- {
118
- refreshToken: this.#accessToken?.refreshToken,
119
- },
120
- );
93
+ async #refreshAuthToken(): Promise<AccessToken> {
94
+ const refreshToken = this.#accessToken?.refreshToken;
95
+ if (!refreshToken) throw new Error("No refresh token");
96
+ return await this.refreshAuthToken(refreshToken);
97
+ }
98
+ async refreshAuthToken(refreshToken: string): Promise<AccessToken> {
99
+ const response = await this.#api.post<AccessTokenDto>(`/refreshRemoteAuthToken`, { refreshToken });
121
100
  this.#accessToken = GlobalConfig.toAccessToken(response);
122
101
  this.#api.setHeaders({ Authorization: `Bearer ${this.#accessToken.jwt}` });
123
102
  return this.#accessToken;
124
103
  }
125
104
  async getRemoteSelf(): Promise<{ id: string; nickname: string } | null> {
126
105
  try {
127
- const data = await this.#api.get<{ id: string; nickname: string }>(
128
- `/getRemoteSelf`,
129
- );
106
+ const data = await this.#api.get<{ id: string; nickname: string }>(`/getRemoteSelf`);
130
107
  return data;
131
108
  } catch {
132
109
  return null;
package/executors.test.ts CHANGED
@@ -251,6 +251,53 @@ describe("Workspace and app executor environment contracts", () => {
251
251
  expect((await stat(path.join(root, "dist/apps/demo/private"))).isDirectory()).toBe(true);
252
252
  expect((await stat(path.join(root, "dist/apps/demo/public"))).isDirectory()).toBe(true);
253
253
  });
254
+
255
+ test("assigns start command ports from sorted app order", async () => {
256
+ const root = await makeTempRoot();
257
+ process.env.AKAN_PUBLIC_REPO_NAME = "repo";
258
+ process.env.AKAN_PUBLIC_SERVE_DOMAIN = "example.com";
259
+ process.env.AKAN_PUBLIC_ENV = "local";
260
+
261
+ await writeJson(path.join(root, "package.json"), rootPackageJson());
262
+ for (const appName of ["minimal", "akan"]) {
263
+ await mkdir(path.join(root, `apps/${appName}`), { recursive: true });
264
+ await writeFile(
265
+ path.join(root, `apps/${appName}/akan.config.ts`),
266
+ [
267
+ "export default {",
268
+ ` routes: [{ basePath: "${appName}", domains: { debug: ["${appName}.local:8282"] } }],`,
269
+ "};",
270
+ "",
271
+ ].join("\n"),
272
+ );
273
+ }
274
+
275
+ const workspace = new WorkspaceExecutor({ workspaceRoot: root, repoName: "repo" });
276
+ const akan = AppExecutor.from(workspace, "akan");
277
+ const minimal = AppExecutor.from(workspace, "minimal");
278
+
279
+ const akanStart = await akan.prepareCommand("start");
280
+ expect(akanStart.env.PORT).toBe("8282");
281
+ expect(akanStart.env.AKAN_PUBLIC_CLIENT_PORT).toBe("8282");
282
+ expect(akanStart.env.AKAN_PUBLIC_SERVER_PORT).toBe("8282");
283
+
284
+ const minimalStart = await minimal.prepareCommand("start");
285
+ expect(minimalStart.env.PORT).toBe("8283");
286
+ expect(minimalStart.env.AKAN_PUBLIC_CLIENT_PORT).toBe("8283");
287
+ expect(minimalStart.env.AKAN_PUBLIC_SERVER_PORT).toBe("8283");
288
+
289
+ process.env.PORT_OFFSET = "3";
290
+
291
+ const offsetAkanStart = await akan.prepareCommand("start");
292
+ expect(offsetAkanStart.env.PORT).toBe("8285");
293
+ expect(offsetAkanStart.env.AKAN_PUBLIC_CLIENT_PORT).toBe("8285");
294
+ expect(offsetAkanStart.env.AKAN_PUBLIC_SERVER_PORT).toBe("8285");
295
+
296
+ const offsetMinimalStart = await minimal.prepareCommand("start");
297
+ expect(offsetMinimalStart.env.PORT).toBe("8286");
298
+ expect(offsetMinimalStart.env.AKAN_PUBLIC_CLIENT_PORT).toBe("8286");
299
+ expect(offsetMinimalStart.env.AKAN_PUBLIC_SERVER_PORT).toBe("8286");
300
+ });
254
301
  });
255
302
 
256
303
  describe("PkgExecutor package generation", () => {
package/executors.ts CHANGED
@@ -163,8 +163,10 @@ const ROOT_LAYOUT_EXPORTS = new Set([
163
163
  "layoutStyle",
164
164
  "gaTrackingId",
165
165
  "Loading",
166
+ "NotFound",
167
+ "Error",
166
168
  ]);
167
- const LAYOUT_ROUTE_EXPORTS = new Set(["default", "head", "generateHead", "Loading"]);
169
+ const LAYOUT_ROUTE_EXPORTS = new Set(["default", "head", "generateHead", "Loading", "NotFound", "Error"]);
168
170
 
169
171
  function validateRouteSourceExports(
170
172
  source: string,
@@ -739,6 +741,51 @@ export class Executor {
739
741
  const message = typeChecker.formatDiagnostics(fileDiagnostics);
740
742
  return { fileDiagnostics, fileErrors, fileWarnings, message };
741
743
  }
744
+ async typeCheckAsync(filePath: string) {
745
+ const path = this.getPath(filePath);
746
+ const entry = await this.#resolveTypecheckWorkerEntry();
747
+ const proc = Bun.spawn([process.execPath, entry], {
748
+ cwd: this.cwdPath,
749
+ env: {
750
+ ...process.env,
751
+ AKAN_TYPECHECK_CWD: this.cwdPath,
752
+ AKAN_TYPECHECK_FILE: path,
753
+ },
754
+ stdout: "pipe",
755
+ stderr: "pipe",
756
+ });
757
+ const [stdout, stderr, exitCode] = await Promise.all([
758
+ new Response(proc.stdout).text(),
759
+ new Response(proc.stderr).text(),
760
+ proc.exited,
761
+ ]);
762
+ if (exitCode !== 0) throw new Error((stderr || stdout).trim() || `Typecheck failed with exit code ${exitCode}`);
763
+
764
+ const result = JSON.parse(stdout) as {
765
+ fileDiagnosticsCount: number;
766
+ fileErrorsCount: number;
767
+ fileWarningsCount: number;
768
+ message: string;
769
+ };
770
+ return {
771
+ fileDiagnostics: Array.from({ length: result.fileDiagnosticsCount }),
772
+ fileErrors: Array.from({ length: result.fileErrorsCount }),
773
+ fileWarnings: Array.from({ length: result.fileWarningsCount }),
774
+ message: result.message,
775
+ };
776
+ }
777
+ async #resolveTypecheckWorkerEntry() {
778
+ const dirname = getDirname(import.meta.url);
779
+ const candidates = [
780
+ path.join(process.cwd(), "pkgs/@akanjs/devkit/typecheck/typecheck.proc.ts"),
781
+ path.join(process.cwd(), "node_modules/@akanjs/devkit/typecheck/typecheck.proc.ts"),
782
+ path.join(dirname, "typecheck/typecheck.proc.ts"),
783
+ path.join(dirname, "typecheck.proc.js"),
784
+ path.join(dirname, "typecheck.proc.ts"),
785
+ ];
786
+ for (const candidate of candidates) if (await Bun.file(candidate).exists()) return candidate;
787
+ throw new Error(`[devkit] typecheck worker entry not found; looked in: ${candidates.join(", ")}`);
788
+ }
742
789
  getLinter() {
743
790
  this.linter ??= new Linter(this.cwdPath);
744
791
  return this.linter;
@@ -811,7 +858,15 @@ export class WorkspaceExecutor extends Executor {
811
858
  | "local"
812
859
  | undefined;
813
860
  if (!env) throw new Error("AKAN_PUBLIC_ENV is not set");
814
- return { ...(appName ? { appName } : {}), workspaceRoot, repoName, serveDomain, env, portOffset, workspaceId };
861
+ return {
862
+ ...(appName ? { appName } : {}),
863
+ workspaceRoot,
864
+ repoName,
865
+ serveDomain,
866
+ env,
867
+ portOffset,
868
+ workspaceId,
869
+ };
815
870
  }
816
871
  getWorkspaceId<AllowEmpty extends boolean = false>({
817
872
  allowEmpty,
@@ -1259,6 +1314,13 @@ export class AppExecutor extends SysExecutor {
1259
1314
  getEnv() {
1260
1315
  return WorkspaceExecutor.getBaseDevEnv().env;
1261
1316
  }
1317
+ async getDevPort() {
1318
+ const basePort = 8282;
1319
+ const appNames = (await this.workspace.getApps()).sort((a, b) => a.localeCompare(b));
1320
+ const appIndex = Math.max(appNames.indexOf(this.name), 0);
1321
+ const portOffset = WorkspaceExecutor.getBaseDevEnv().portOffset;
1322
+ return basePort + appIndex + portOffset;
1323
+ }
1262
1324
  getCommandEnv(env: Record<string, string> = {}): Record<string, string> {
1263
1325
  const basePort = 8282;
1264
1326
  const portOffset = WorkspaceExecutor.getBaseDevEnv().portOffset;
@@ -1290,7 +1352,12 @@ export class AppExecutor extends SysExecutor {
1290
1352
  this.cp("public", `${this.dist.cwdPath}/public`),
1291
1353
  ]);
1292
1354
  } else await this.removeDir(".akan");
1293
- const env = this.getCommandEnv({ AKAN_COMMAND_TYPE: type, ...routeEnv });
1355
+ const devPort = type === "start" ? (await this.getDevPort()).toString() : undefined;
1356
+ const env = this.getCommandEnv({
1357
+ AKAN_COMMAND_TYPE: type,
1358
+ ...routeEnv,
1359
+ ...(devPort ? { PORT: devPort, AKAN_PUBLIC_CLIENT_PORT: devPort, AKAN_PUBLIC_SERVER_PORT: devPort } : {}),
1360
+ });
1294
1361
  return { env };
1295
1362
  }
1296
1363
  #publicEnv: Record<string, string> | null = null;
package/index.ts CHANGED
@@ -9,6 +9,7 @@ export * from "./artifact";
9
9
  export * from "./builder";
10
10
  export * from "./capacitorApp";
11
11
  export * from "./cloud";
12
+ export * from "./cloud";
12
13
  export * from "./commandDecorators";
13
14
  export * from "./createTunnel";
14
15
  export * from "./dependencyScanner";
@@ -34,4 +35,3 @@ export * from "./types";
34
35
  export * from "./ui";
35
36
  export * from "./uploadRelease";
36
37
  export * from "./useStdoutDimensions";
37
- export * from "./cloud";