@akanjs/devkit 2.3.11 → 2.3.12-rc.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.
@@ -264,6 +264,8 @@ describe("AkanAppConfig", () => {
264
264
  `postgres@${runtimeDependencies.postgres}`,
265
265
  `protobufjs@${runtimeDependencies.protobufjs}`,
266
266
  ]);
267
+ expect(config.getMobileRuntimePackages()).toEqual(["firebase"]);
268
+ expect(config.getMissingMobileDependencySpecs()).toEqual([`firebase@${runtimeDependencies.firebase}`]);
267
269
  });
268
270
 
269
271
  test("normalizes multiple mobile targets and validates base paths", () => {
@@ -51,6 +51,9 @@ const WORKSPACE_BARREL_FACETS = ["ui", "webkit", "common", "client", "server"] a
51
51
  const SSR_RUNTIME_PACKAGES = ["react", "react-dom", "react-server-dom-webpack"] as const;
52
52
  const NATIVE_RUNTIME_PACKAGES = ["sharp"] as const;
53
53
  const DEFAULT_BACKEND_RUNTIME_PACKAGES = ["croner"] as const;
54
+ // Native-only client packages that are not bundled into the base bootstrap; installed
55
+ // on demand when a mobile target is built or started (e.g. firebase for push tokens).
56
+ const MOBILE_RUNTIME_PACKAGES = ["firebase"] as const;
54
57
  const DATABASE_MODE_RUNTIME_PACKAGES = {
55
58
  single: [],
56
59
  multiple: ["@libsql/client", "bullmq", "ioredis", "protobufjs"],
@@ -60,6 +63,7 @@ const AKAN_RUNTIME_PACKAGES = new Set<string>([
60
63
  ...SSR_RUNTIME_PACKAGES,
61
64
  ...NATIVE_RUNTIME_PACKAGES,
62
65
  ...DEFAULT_BACKEND_RUNTIME_PACKAGES,
66
+ ...MOBILE_RUNTIME_PACKAGES,
63
67
  ...Object.values(DATABASE_MODE_RUNTIME_PACKAGES).flat(),
64
68
  ]);
65
69
  const DEFAULT_AKAN_IMAGE_CONFIG: AkanImageConfig = {
@@ -360,11 +364,20 @@ CMD [${command.map((c) => `"${c}"`).join(",")}]`;
360
364
  return [...DATABASE_MODE_RUNTIME_PACKAGES[databaseMode]];
361
365
  }
362
366
  getMissingDatabaseModeDependencySpecs(databaseMode: DatabaseMode = this.defaultDatabaseMode) {
367
+ return this.#getMissingDependencySpecs(this.getDatabaseModeRuntimePackages(databaseMode));
368
+ }
369
+ getMobileRuntimePackages() {
370
+ return [...MOBILE_RUNTIME_PACKAGES];
371
+ }
372
+ getMissingMobileDependencySpecs() {
373
+ return this.#getMissingDependencySpecs(this.getMobileRuntimePackages());
374
+ }
375
+ #getMissingDependencySpecs(libs: readonly string[]) {
363
376
  const rootDependencies = {
364
377
  ...this.rootPackageJson.dependencies,
365
378
  ...this.rootPackageJson.devDependencies,
366
379
  };
367
- return this.getDatabaseModeRuntimePackages(databaseMode)
380
+ return libs
368
381
  .filter((lib) => !rootDependencies[lib])
369
382
  .map((lib) => {
370
383
  const version = this.#resolveProductionDependencyVersion(lib);
@@ -0,0 +1,66 @@
1
+ import { afterEach, describe, expect, test } from "bun:test";
2
+ import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import { DevGeneratedIndexSync } from "./devGeneratedIndexSync";
6
+
7
+ const tempRoots: string[] = [];
8
+
9
+ const makeTempRoot = async () => {
10
+ const root = await mkdtemp(path.join(os.tmpdir(), "akan-generated-index-"));
11
+ tempRoots.push(root);
12
+ return root;
13
+ };
14
+
15
+ const seedFacet = async (root: string, facet: string, { files, dirs }: { files: string[]; dirs: string[] }) => {
16
+ const dir = path.join(root, "libs", "util", facet);
17
+ await mkdir(dir, { recursive: true });
18
+ await Promise.all(files.map((name) => writeFile(path.join(dir, name), "export const x = 1;\n")));
19
+ await Promise.all(dirs.map((name) => mkdir(path.join(dir, name), { recursive: true })));
20
+ return dir;
21
+ };
22
+
23
+ const barrelFor = async (root: string, facet: string, seed: { files: string[]; dirs: string[]; trigger: string }) => {
24
+ const dir = await seedFacet(root, facet, seed);
25
+ const sync = new DevGeneratedIndexSync({ workspaceRoot: root });
26
+ const result = await sync.syncForBatch([path.join(dir, seed.trigger)]);
27
+ expect(result.errors).toEqual([]);
28
+ return readFile(path.join(dir, "index.ts"), "utf8");
29
+ };
30
+
31
+ afterEach(async () => {
32
+ await Promise.all(tempRoots.splice(0).map((root) => rm(root, { recursive: true, force: true })));
33
+ });
34
+
35
+ describe("DevGeneratedIndexSync facet barrels", () => {
36
+ test("camelCase facets export only clean camelCase names", async () => {
37
+ const content = await barrelFor(await makeTempRoot(), "srvkit", {
38
+ files: [
39
+ "aes.ts",
40
+ "cloudflareApi.ts",
41
+ "cloudflareApi.helper.ts", // dotted → skipped
42
+ "pushNotificationServer.type.ts", // dotted → skipped
43
+ "PushNotificationServer.ts", // PascalCase in camel facet → skipped
44
+ "my_snake.ts", // snake_case → skipped
45
+ "kebab-case.ts", // kebab-case → skipped
46
+ ],
47
+ dirs: ["storageApi", "BadDir"], // PascalCase dir → skipped
48
+ trigger: "aes.ts",
49
+ });
50
+ expect(content).toBe(`export * from "./aes";\nexport * from "./cloudflareApi";\nexport * from "./storageApi";\n`);
51
+ });
52
+
53
+ test("ui facet exports only clean PascalCase names", async () => {
54
+ const content = await barrelFor(await makeTempRoot(), "ui", {
55
+ files: [
56
+ "Globe.tsx",
57
+ "AkanLogo.tsx",
58
+ "Globe_Dynamic.tsx", // underscore → skipped
59
+ "lowerStart.tsx", // camelCase in ui facet → skipped
60
+ ],
61
+ dirs: ["Code", "badDir"], // camelCase dir → skipped
62
+ trigger: "Globe.tsx",
63
+ });
64
+ expect(content).toBe(`export * from "./AkanLogo";\nexport * from "./Code";\nexport * from "./Globe";\n`);
65
+ });
66
+ });
@@ -4,6 +4,10 @@ import path from "node:path";
4
4
  const BARREL_FACETS = new Set(["common", "srvkit", "ui", "webkit"]);
5
5
  const FACET_SOURCE_FILE_RE = /\.(ts|tsx)$/;
6
6
  const FACET_EXCLUDED_FILE_RE = /(^index\.tsx?$|\.d\.ts$|\.(test|spec)\.(ts|tsx)$|\.css$|\.scss$|\.sass$)/;
7
+ // `ui` exports PascalCase names only; `common`/`srvkit`/`webkit` export camelCase names only. Names with
8
+ // dots, underscores, or hyphens (e.g. `foo.helper`, `Globe_Dynamic`, `kebab-case`) match neither and are skipped.
9
+ const FACET_PASCAL_CASE_RE = /^[A-Z][A-Za-z0-9]*$/;
10
+ const FACET_CAMEL_CASE_RE = /^[a-z][A-Za-z0-9]*$/;
7
11
  const MODULE_UI_TYPES = ["Template", "Unit", "Util", "View", "Zone"] as const;
8
12
  const SERVICE_UI_TYPES = ["Util", "Zone"] as const;
9
13
  const SCALAR_UI_TYPES = ["Template", "Unit"] as const;
@@ -109,15 +113,17 @@ export class DevGeneratedIndexSync {
109
113
  }
110
114
 
111
115
  async #facetContent(dir: string): Promise<string | null> {
116
+ const nameCasePattern = path.basename(dir) === "ui" ? FACET_PASCAL_CASE_RE : FACET_CAMEL_CASE_RE;
112
117
  const entries = await readdir(dir, { withFileTypes: true }).catch(() => []);
113
118
  const exportNames = entries
114
119
  .flatMap((entry) => {
115
120
  const name = entry.name;
116
121
  if (name.startsWith(".")) return [];
117
- if (entry.isDirectory()) return [name];
122
+ if (entry.isDirectory()) return nameCasePattern.test(name) ? [name] : [];
118
123
  if (!entry.isFile()) return [];
119
124
  if (!FACET_SOURCE_FILE_RE.test(name) || FACET_EXCLUDED_FILE_RE.test(name)) return [];
120
- return [name.replace(FACET_SOURCE_FILE_RE, "")];
125
+ const exportName = name.replace(FACET_SOURCE_FILE_RE, "");
126
+ return nameCasePattern.test(exportName) ? [exportName] : [];
121
127
  })
122
128
  .sort();
123
129
  if (exportNames.length === 0) return null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@akanjs/devkit",
3
- "version": "2.3.11",
3
+ "version": "2.3.12-rc.1",
4
4
  "sourceType": "module",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -32,7 +32,7 @@
32
32
  "@langchain/openai": "^1.4.6",
33
33
  "@tailwindcss/node": "^4.3.0",
34
34
  "@trapezedev/project": "^7.1.4",
35
- "akanjs": "2.3.11",
35
+ "akanjs": "2.3.12-rc.1",
36
36
  "chalk": "^5.6.2",
37
37
  "commander": "^14.0.3",
38
38
  "daisyui": "5.5.23",