@akanjs/devkit 2.1.1 → 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.
@@ -7,7 +7,13 @@ import { AkanAppConfig, AkanLibConfig } from "./akanConfig";
7
7
  import type { DeepPartial, LibConfigResult } from "./types";
8
8
 
9
9
  const akanPackageJson = JSON.parse(
10
- fs.readFileSync(path.join(path.dirname(fileURLToPath(import.meta.url)), "../../../akanjs/package.json"), "utf8"),
10
+ fs.readFileSync(
11
+ path.join(
12
+ path.dirname(fileURLToPath(import.meta.url)),
13
+ "../../../akanjs/package.json",
14
+ ),
15
+ "utf8",
16
+ ),
11
17
  ) as PackageJson;
12
18
 
13
19
  const packageJson: PackageJson = {
@@ -34,7 +40,13 @@ const baseDevEnv = {
34
40
 
35
41
  describe("AkanAppConfig", () => {
36
42
  test("applies defaults for route domains, i18n, image, mobile, and imports", () => {
37
- const config = new AkanAppConfig(app, ["shared"], packageJson, {}, baseDevEnv);
43
+ const config = new AkanAppConfig(
44
+ app,
45
+ ["shared"],
46
+ packageJson,
47
+ {},
48
+ baseDevEnv,
49
+ );
38
50
 
39
51
  expect([...config.domains].sort()).toEqual([
40
52
  "portal-debug.akanjs.com",
@@ -61,7 +73,11 @@ describe("AkanAppConfig", () => {
61
73
  },
62
74
  });
63
75
  expect(config.barrelImports).toEqual(
64
- expect.arrayContaining(["@apps/portal/ui", "@libs/shared/server", "akanjs/common"]),
76
+ expect.arrayContaining([
77
+ "@apps/portal/ui",
78
+ "@libs/shared/server",
79
+ "akanjs/common",
80
+ ]),
65
81
  );
66
82
  expect(config.docker.content).toContain("ENV AKAN_PUBLIC_APP_NAME=portal");
67
83
  expect(process.env.AKAN_PUBLIC_DEFAULT_LOCALE).toBe("en");
@@ -75,10 +91,21 @@ describe("AkanAppConfig", () => {
75
91
  {
76
92
  routes: [
77
93
  { domains: { debug: ["Root.Local:8282"], qa: ["QA.Root.Local"] } },
78
- { basePath: "/admin/", domains: { debug: ["Admin.Local:8282"], main: ["Admin.Main.Local"] } },
94
+ {
95
+ basePath: "/admin/",
96
+ domains: {
97
+ debug: ["Admin.Local:8282"],
98
+ main: ["Admin.Main.Local"],
99
+ },
100
+ },
79
101
  ],
80
102
  i18n: { locales: ["ko", "en"], defaultLocale: "ko" },
81
- mobile: { appName: "Portal App", appId: "com.portal.mobile", version: "1.2.3", buildNum: 7 },
103
+ mobile: {
104
+ appName: "Portal App",
105
+ appId: "com.portal.mobile",
106
+ version: "1.2.3",
107
+ buildNum: 7,
108
+ },
82
109
  images: { qualities: [80, 90], dangerouslyAllowSVG: true },
83
110
  docker: {
84
111
  image: { amd64: "oven/bun:amd64", arm64: "oven/bun:arm64" },
@@ -102,7 +129,12 @@ describe("AkanAppConfig", () => {
102
129
  "admin.local",
103
130
  "admin.main.local",
104
131
  ]);
105
- expect([...config.branches].sort()).toEqual(["debug", "develop", "main", "qa"]);
132
+ expect([...config.branches].sort()).toEqual([
133
+ "debug",
134
+ "develop",
135
+ "main",
136
+ "qa",
137
+ ]);
106
138
  expect(config.i18n.defaultLocale).toBe("ko");
107
139
  expect(config.images.qualities).toEqual([80, 90]);
108
140
  expect(config.images.dangerouslyAllowSVG).toBe(true);
@@ -122,9 +154,17 @@ describe("AkanAppConfig", () => {
122
154
  });
123
155
 
124
156
  test("creates production package json and reports missing external versions", () => {
125
- const config = new AkanAppConfig(app, [], packageJson, { externalLibs: ["@external/runtime"] }, baseDevEnv);
157
+ const config = new AkanAppConfig(
158
+ app,
159
+ [],
160
+ packageJson,
161
+ { externalLibs: ["@external/runtime"] },
162
+ baseDevEnv,
163
+ );
126
164
 
127
- expect(config.getProductionPackageJson({ scripts: { start: "bun main.js" } })).toMatchObject({
165
+ expect(
166
+ config.getProductionPackageJson({ scripts: { start: "bun main.js" } }),
167
+ ).toMatchObject({
128
168
  name: "portal",
129
169
  main: "./main.js",
130
170
  scripts: { start: "bun main.js" },
@@ -145,11 +185,16 @@ describe("AkanAppConfig", () => {
145
185
  { externalLibs: ["missing-lib"] },
146
186
  baseDevEnv,
147
187
  );
148
- expect(() => brokenConfig.getProductionPackageJson()).toThrow("Dependency missing-lib not found");
188
+ expect(() => brokenConfig.getProductionPackageJson()).toThrow(
189
+ "Dependency missing-lib not found",
190
+ );
149
191
  });
150
192
 
151
193
  test("falls back to akanjs package versions for built-in runtime dependencies", () => {
152
- const runtimeDependencies = { ...akanPackageJson.dependencies, ...akanPackageJson.peerDependencies };
194
+ const runtimeDependencies = {
195
+ ...akanPackageJson.dependencies,
196
+ ...akanPackageJson.peerDependencies,
197
+ };
153
198
  const config = new AkanAppConfig(
154
199
  app,
155
200
  [],
@@ -168,44 +213,130 @@ describe("AkanAppConfig", () => {
168
213
  expect(config.getProductionPackageJson().dependencies).toEqual({
169
214
  react: runtimeDependencies.react,
170
215
  "react-dom": runtimeDependencies["react-dom"],
171
- "react-server-dom-webpack": runtimeDependencies["react-server-dom-webpack"],
216
+ "react-server-dom-webpack":
217
+ runtimeDependencies["react-server-dom-webpack"],
172
218
  croner: runtimeDependencies.croner,
173
219
  sharp: runtimeDependencies.sharp,
174
220
  });
175
221
  });
176
222
 
177
223
  test("adds backend runtime packages by database mode", () => {
178
- const runtimeDependencies = { ...akanPackageJson.dependencies, ...akanPackageJson.peerDependencies };
179
- const singleConfig = new AkanAppConfig(app, [], packageJson, { defaultDatabaseMode: "single" }, baseDevEnv);
180
- const multipleConfig = new AkanAppConfig(app, [], packageJson, { defaultDatabaseMode: "multiple" }, baseDevEnv);
181
- const clusterConfig = new AkanAppConfig(app, [], packageJson, { defaultDatabaseMode: "cluster" }, baseDevEnv);
224
+ const runtimeDependencies = {
225
+ ...akanPackageJson.dependencies,
226
+ ...akanPackageJson.peerDependencies,
227
+ };
228
+ const singleConfig = new AkanAppConfig(
229
+ app,
230
+ [],
231
+ packageJson,
232
+ { defaultDatabaseMode: "single" },
233
+ baseDevEnv,
234
+ );
235
+ const multipleConfig = new AkanAppConfig(
236
+ app,
237
+ [],
238
+ packageJson,
239
+ { defaultDatabaseMode: "multiple" },
240
+ baseDevEnv,
241
+ );
242
+ const clusterConfig = new AkanAppConfig(
243
+ app,
244
+ [],
245
+ packageJson,
246
+ { defaultDatabaseMode: "cluster" },
247
+ baseDevEnv,
248
+ );
182
249
 
183
250
  expect(singleConfig.getProductionPackageJson().dependencies).toMatchObject({
184
251
  croner: runtimeDependencies.croner,
185
252
  });
186
- expect(singleConfig.getProductionPackageJson().dependencies).not.toHaveProperty("ioredis");
187
- expect(singleConfig.getProductionPackageJson().dependencies).not.toHaveProperty("bullmq");
188
- expect(singleConfig.getProductionPackageJson().dependencies).not.toHaveProperty("@libsql/client");
189
- expect(singleConfig.getProductionPackageJson().dependencies).not.toHaveProperty("postgres");
190
- expect(singleConfig.getProductionPackageJson().dependencies).not.toHaveProperty("protobufjs");
253
+ expect(
254
+ singleConfig.getProductionPackageJson().dependencies,
255
+ ).not.toHaveProperty("ioredis");
256
+ expect(
257
+ singleConfig.getProductionPackageJson().dependencies,
258
+ ).not.toHaveProperty("bullmq");
259
+ expect(
260
+ singleConfig.getProductionPackageJson().dependencies,
261
+ ).not.toHaveProperty("@libsql/client");
262
+ expect(
263
+ singleConfig.getProductionPackageJson().dependencies,
264
+ ).not.toHaveProperty("postgres");
265
+ expect(
266
+ singleConfig.getProductionPackageJson().dependencies,
267
+ ).not.toHaveProperty("protobufjs");
191
268
 
192
- expect(multipleConfig.getProductionPackageJson().dependencies).toMatchObject({
269
+ expect(
270
+ multipleConfig.getProductionPackageJson().dependencies,
271
+ ).toMatchObject({
193
272
  "@libsql/client": runtimeDependencies["@libsql/client"],
194
273
  bullmq: runtimeDependencies.bullmq,
195
274
  croner: runtimeDependencies.croner,
196
275
  ioredis: runtimeDependencies.ioredis,
197
276
  protobufjs: runtimeDependencies.protobufjs,
198
277
  });
199
- expect(multipleConfig.getProductionPackageJson().dependencies).not.toHaveProperty("postgres");
278
+ expect(
279
+ multipleConfig.getProductionPackageJson().dependencies,
280
+ ).not.toHaveProperty("postgres");
200
281
 
201
- expect(clusterConfig.getProductionPackageJson().dependencies).toMatchObject({
202
- bullmq: runtimeDependencies.bullmq,
203
- croner: runtimeDependencies.croner,
204
- ioredis: runtimeDependencies.ioredis,
205
- postgres: runtimeDependencies.postgres,
206
- protobufjs: runtimeDependencies.protobufjs,
207
- });
208
- expect(clusterConfig.getProductionPackageJson().dependencies).not.toHaveProperty("@libsql/client");
282
+ expect(clusterConfig.getProductionPackageJson().dependencies).toMatchObject(
283
+ {
284
+ bullmq: runtimeDependencies.bullmq,
285
+ croner: runtimeDependencies.croner,
286
+ ioredis: runtimeDependencies.ioredis,
287
+ postgres: runtimeDependencies.postgres,
288
+ protobufjs: runtimeDependencies.protobufjs,
289
+ },
290
+ );
291
+ expect(
292
+ clusterConfig.getProductionPackageJson().dependencies,
293
+ ).not.toHaveProperty("@libsql/client");
294
+ });
295
+
296
+ test("resolves database mode runtime packages and missing install specs", () => {
297
+ const runtimeDependencies = {
298
+ ...akanPackageJson.dependencies,
299
+ ...akanPackageJson.peerDependencies,
300
+ };
301
+ const config = new AkanAppConfig(
302
+ app,
303
+ [],
304
+ {
305
+ name: "repo",
306
+ version: "1.0.0",
307
+ description: "repo",
308
+ dependencies: {
309
+ bullmq: "5.0.0",
310
+ },
311
+ devDependencies: {
312
+ ioredis: "5.0.0",
313
+ },
314
+ },
315
+ {},
316
+ baseDevEnv,
317
+ );
318
+
319
+ expect(config.getDatabaseModeRuntimePackages("single")).toEqual([]);
320
+ expect(config.getDatabaseModeRuntimePackages("multiple")).toEqual([
321
+ "@libsql/client",
322
+ "bullmq",
323
+ "ioredis",
324
+ "protobufjs",
325
+ ]);
326
+ expect(config.getDatabaseModeRuntimePackages("cluster")).toEqual([
327
+ "bullmq",
328
+ "ioredis",
329
+ "postgres",
330
+ "protobufjs",
331
+ ]);
332
+ expect(config.getMissingDatabaseModeDependencySpecs("multiple")).toEqual([
333
+ `@libsql/client@${runtimeDependencies["@libsql/client"]}`,
334
+ `protobufjs@${runtimeDependencies.protobufjs}`,
335
+ ]);
336
+ expect(config.getMissingDatabaseModeDependencySpecs("cluster")).toEqual([
337
+ `postgres@${runtimeDependencies.postgres}`,
338
+ `protobufjs@${runtimeDependencies.protobufjs}`,
339
+ ]);
209
340
  });
210
341
 
211
342
  test("normalizes multiple mobile targets and validates base paths", () => {
@@ -266,7 +397,11 @@ describe("AkanLibConfig", () => {
266
397
  const lib = { name: "shared" } as never;
267
398
  expect(new AkanLibConfig(lib, {}).externalLibs).toEqual([]);
268
399
 
269
- const config: DeepPartial<LibConfigResult> = { externalLibs: ["firebase-admin"] };
270
- expect(new AkanLibConfig(lib, config).externalLibs).toEqual(["firebase-admin"]);
400
+ const config: DeepPartial<LibConfigResult> = {
401
+ externalLibs: ["firebase-admin"],
402
+ };
403
+ expect(new AkanLibConfig(lib, config).externalLibs).toEqual([
404
+ "firebase-admin",
405
+ ]);
271
406
  });
272
407
  });
@@ -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
  }