@akanjs/devkit 2.1.2-rc.1 → 2.2.0-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.
package/CHANGELOG.md ADDED
@@ -0,0 +1,9 @@
1
+ # @akanjs/devkit
2
+
3
+ ## 2.2.0
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies [cb5b07a]
8
+ - Updated dependencies [258284e]
9
+ - akanjs@2.2.0
@@ -135,8 +135,69 @@ 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 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`;
138
+ ? `import type { LayoutProps, PageProps } from "akanjs/client";
139
+ import { loadFonts } from "akanjs/client";
140
+ import { System } from "akanjs/ui";
141
+ import { env } from "@apps/${opts.appName}/env/env.client";
142
+ ${clientImport}${inheritedImport}${userImport}
143
+ // SSR builds (target=bun) load the full dictionary server-side and pass only the active locale to the client.
144
+ // CSR builds (target=browser) fold this branch to undefined, so the macro-seeded dictionary is used and the
145
+ // server-only dict module (which pulls @libs/*/server) is dead-code-eliminated out of the browser bundle.
146
+ const getActiveLocaleDictionary =
147
+ process.env.AKAN_PUBLIC_RENDER_ENV === "ssr" ? (await import("@apps/${opts.appName}/lib/dict")).getDictionary : undefined;
148
+ const userFonts = userLayout.fonts ?? inheritedLayout.fonts ?? [];
149
+ const defaultFonts = userFonts.filter((font) => font.default);
150
+ if (defaultFonts.length > 1) throw new Error("[route-convention] only one default font is allowed per root layout");
151
+ const defaultFont = defaultFonts[0];
152
+ const defaultFontClassName = defaultFont ? (defaultFont.className ?? \`font-\${defaultFont.name}\`) : undefined;
153
+
154
+ export async function generateHead(props: PageProps) {
155
+ if (userLayout.generateHead) return userLayout.generateHead(props);
156
+ if (userLayout.head !== undefined) return userLayout.head;
157
+ if (inheritedLayout.generateHead) return inheritedLayout.generateHead(props);
158
+ return inheritedLayout.head;
159
+ }
160
+
161
+ export const NotFound = userLayout.NotFound ?? inheritedLayout.NotFound;
162
+ export const Error = userLayout.Error ?? inheritedLayout.Error;
163
+
164
+ export default function GeneratedLayout({ children, params, searchParams }: LayoutProps) {
165
+ return (
166
+ <System.Provider
167
+ of={GeneratedLayout as never}
168
+ appName=${JSON.stringify(opts.appName)}
169
+ ${prefix ? `prefix=${JSON.stringify(prefix)}\n ` : ""}params={params}
170
+ manifest={userLayout.manifest ?? inheritedLayout.manifest}
171
+ env={env}
172
+ theme={userLayout.theme ?? inheritedLayout.theme}
173
+ fonts={loadFonts(userFonts)}
174
+ className={defaultFontClassName}
175
+ gaTrackingId={userLayout.gaTrackingId ?? inheritedLayout.gaTrackingId}
176
+ layoutStyle={userLayout.layoutStyle ?? inheritedLayout.layoutStyle}
177
+ reconnect={userLayout.reconnect ?? inheritedLayout.reconnect ?? false}
178
+ dictionary={getActiveLocaleDictionary ? getActiveLocaleDictionary(params.lang) : undefined}
179
+ >
180
+ <UserLayout params={params} searchParams={searchParams}>{children}</UserLayout>
181
+ </System.Provider>
182
+ );
183
+ }
184
+ `
185
+ : `import type { LayoutProps, PageProps } from "akanjs/client";
186
+ ${inheritedImport}${userImport}
187
+ export async function generateHead(props: PageProps) {
188
+ if (userLayout.generateHead) return userLayout.generateHead(props);
189
+ if (userLayout.head !== undefined) return userLayout.head;
190
+ if (inheritedLayout.generateHead) return inheritedLayout.generateHead(props);
191
+ return inheritedLayout.head;
192
+ }
193
+
194
+ export const NotFound = userLayout.NotFound ?? inheritedLayout.NotFound;
195
+ export const Error = userLayout.Error ?? inheritedLayout.Error;
196
+
197
+ export default function GeneratedLayout({ children, params, searchParams }: LayoutProps) {
198
+ return <UserLayout params={params} searchParams={searchParams}>{children}</UserLayout>;
199
+ }
200
+ `;
140
201
  await Bun.write(absPath, source);
141
202
  return absPath;
142
203
  }
package/capacitorApp.ts CHANGED
@@ -27,11 +27,12 @@ export class CapacitorApp {
27
27
  readonly iosRootPath = "ios";
28
28
  readonly iosProjectPath = "ios/App";
29
29
  readonly androidRootPath = "android";
30
+ readonly androidAssetsPath = "android/app/src/main/assets";
30
31
  constructor(
31
32
  private readonly app: AppExecutor,
32
33
  readonly target: AkanMobileTargetConfig,
33
34
  ) {
34
- this.targetRootPath = path.posix.join("mobile", this.target.name);
35
+ this.targetRootPath = path.posix.join(".akan", "mobile", this.target.name);
35
36
  this.targetRoot = path.join(this.app.cwdPath, this.targetRootPath);
36
37
  this.targetWebRoot = path.join(this.targetRoot, "www");
37
38
  this.targetAssetRoot = path.join(this.targetRoot, "assets");
@@ -111,6 +112,8 @@ export class CapacitorApp {
111
112
  await this.#applyLinks();
112
113
  await this.project.commit();
113
114
  await this.#generateAssets({ operation, env });
115
+ await this.#ensureAndroidAssetsDir();
116
+ await this.#ensureAndroidDebugKeystore();
114
117
  await this.#spawnMobile("npx", ["cap", "sync", "android"], { operation, env });
115
118
  }
116
119
 
@@ -166,12 +169,40 @@ export class CapacitorApp {
166
169
  await this.app.spawn(gradleCommand, [assembleType === "apk" ? "assembleRelease" : "bundleRelease"], {
167
170
  stdio: "inherit",
168
171
  cwd: path.join(this.app.cwdPath, this.androidRootPath),
169
- env: this.#commandEnv("release", env),
172
+ env: await this.#commandEnv("release", env),
170
173
  });
171
174
  }
172
175
  async openAndroid() {
173
176
  await this.#spawnMobile("npx", ["cap", "open", "android"], { operation: "local", env: "local" });
174
177
  }
178
+ async #ensureAndroidAssetsDir() {
179
+ await mkdir(path.join(this.app.cwdPath, this.androidAssetsPath), { recursive: true });
180
+ }
181
+ async #ensureAndroidDebugKeystore() {
182
+ const keystorePath = path.join(this.app.cwdPath, this.androidRootPath, "app/debug.keystore");
183
+ if (await Bun.file(keystorePath).exists()) return;
184
+
185
+ await this.#spawn("keytool", [
186
+ "-genkeypair",
187
+ "-v",
188
+ "-keystore",
189
+ keystorePath,
190
+ "-storepass",
191
+ "android",
192
+ "-alias",
193
+ "androiddebugkey",
194
+ "-keypass",
195
+ "android",
196
+ "-keyalg",
197
+ "RSA",
198
+ "-keysize",
199
+ "2048",
200
+ "-validity",
201
+ "10000",
202
+ "-dname",
203
+ "CN=Android Debug,O=Android,C=US",
204
+ ]);
205
+ }
175
206
  async syncAndroid(options: { regenerate?: boolean } = {}) {
176
207
  await this.prepareWww();
177
208
  await this.#prepareAndroid({ operation: "release", env: "debug", ...options });
@@ -217,13 +248,13 @@ export class CapacitorApp {
217
248
  .split(path.sep)
218
249
  .join("/");
219
250
  const content = `import type { AppScanResult } from "akanjs";
220
- import { withBase } from "akanjs/capacitor.base.config";
251
+ import { withBase } from "${process.env.USE_AKANJS_PKGS === "true" ? "../../pkgs/" : ""}akanjs/capacitor.base.config";
221
252
  import appInfo from "${appInfoPath.startsWith(".") ? appInfoPath : `./${appInfoPath}`}";
222
253
 
223
254
  export default withBase(
224
255
  (config, target) => ({
225
256
  ...config,
226
- webDir: \`mobile/\${target.name}/www\`,
257
+ webDir: \`.akan/mobile/\${target.name}/www\`,
227
258
  android: {
228
259
  ...config.android,
229
260
  path: "android",
@@ -331,12 +362,14 @@ export default withBase(
331
362
  );
332
363
  }
333
364
  }
334
- #commandEnv(operation: "local" | "release", env: "local" | "debug" | "develop" | "main") {
365
+ async #commandEnv(operation: "local" | "release", env: "local" | "debug" | "develop" | "main") {
366
+ const devPort = operation === "local" ? (await this.app.getDevPort()).toString() : undefined;
335
367
  return this.app.getCommandEnv({
336
368
  APP_OPERATION_MODE: operation,
337
369
  AKAN_PUBLIC_OPERATION_MODE: env === "local" ? "local" : "cloud",
338
370
  AKAN_PUBLIC_ENV: env,
339
371
  AKAN_MOBILE_TARGET: this.target.name,
372
+ ...(devPort ? { PORT: devPort, AKAN_PUBLIC_CLIENT_PORT: devPort, AKAN_PUBLIC_SERVER_PORT: devPort } : {}),
340
373
  });
341
374
  }
342
375
  async #spawn(command: string, args: string[] = [], options: Parameters<AppExecutor["spawn"]>[2] = {}) {
@@ -350,7 +383,7 @@ export default withBase(
350
383
  ) {
351
384
  return await this.#spawn(command, args, {
352
385
  ...options,
353
- env: { ...this.#commandEnv(operation, env), ...options.env },
386
+ env: { ...(await this.#commandEnv(operation, env)), ...options.env },
354
387
  });
355
388
  }
356
389
  async addCamera() {
@@ -3,9 +3,8 @@ import type { SupportedLlmModel } from "../aiEditor";
3
3
 
4
4
  export const basePath = `${Bun.env.HOME ?? Bun.env.USERPROFILE}/.akan`;
5
5
  export const configPath = `${basePath}/config.json`;
6
- export const akanCloudHost =
7
- process.env.AKAN_PUBLIC_OPERATION_MODE === "local" ? "http://localhost" : "https://cloud.akanjs.com";
8
- export const akanCloudUrl = `${akanCloudHost}${process.env.AKAN_PUBLIC_OPERATION_MODE === "local" ? ":8282" : ""}/api`;
6
+ export const akanCloudHost = process.env.USE_AKANJS_PKGS === "true" ? "http://localhost" : "https://cloud.akanjs.com";
7
+ export const akanCloudUrl = `${akanCloudHost}${process.env.USE_AKANJS_PKGS === "true" ? ":8282" : ""}/api`;
9
8
 
10
9
  export interface HostConfig {
11
10
  auth?: {
@@ -153,6 +153,21 @@ void bootCsr(pages);
153
153
  jsFiles.push(jsPath);
154
154
  return await Bun.file(jsPath).text();
155
155
  });
156
+ const bundledCss = (
157
+ await Promise.all(
158
+ cssFiles.map((cssFile) =>
159
+ Bun.file(cssFile)
160
+ .text()
161
+ .catch(() => ""),
162
+ ),
163
+ )
164
+ )
165
+ .filter(Boolean)
166
+ .join("\n");
167
+ if (bundledCss) {
168
+ const style = CsrArtifactBuilder.createInlineStyle(bundledCss);
169
+ if (!next.includes(style)) next = CsrArtifactBuilder.injectBeforeHeadEnd(next, style);
170
+ }
156
171
  if (cssAsset) {
157
172
  const cssPath = path.join(
158
173
  this.#command === "build" ? this.#app.dist.cwdPath : this.#app.cwdPath,
@@ -58,13 +58,41 @@ describe("PagesEntrySourceGenerator", () => {
58
58
  'import * as page0 from "/repo/apps/demo/page/_index.tsx";',
59
59
  'import * as page1 from "/repo/apps/demo/page/admin.tsx";',
60
60
  "export const pages = {",
61
- ' "./_index.tsx": async () => page0,',
62
- ' "./admin.tsx": async () => page1,',
61
+ ' "./_index.tsx": { loader: async () => page0, isAsyncDefault: false },',
62
+ ' "./admin.tsx": { loader: async () => page1, isAsyncDefault: false },',
63
63
  "};",
64
64
  "",
65
65
  ].join("\n"),
66
66
  );
67
67
  });
68
+
69
+ test("marks async default exports for static CSR bundles", async () => {
70
+ const root = await makeTempRoot();
71
+ const indexPath = path.join(root, "page/_index.tsx");
72
+ const adminPath = path.join(root, "page/admin.tsx");
73
+ const typedPath = path.join(root, "page/typed.tsx");
74
+ const expressionPath = path.join(root, "page/expression.tsx");
75
+ const namedExportPath = path.join(root, "page/named-export.tsx");
76
+ await write(indexPath, "export default async function Page() { return null; }");
77
+ await write(adminPath, "const Admin = async () => null;\nexport default Admin;");
78
+ await write(typedPath, "const Typed: () => Promise<null> = async () => null;\nexport default Typed;");
79
+ await write(expressionPath, "export default async () => null;");
80
+ await write(namedExportPath, "async function NamedExport() { return null; }\nexport { NamedExport as default };");
81
+
82
+ const source = PagesEntrySourceGenerator.generateStatic([
83
+ { key: "./_index.tsx", moduleAbsPath: indexPath },
84
+ { key: "./admin.tsx", moduleAbsPath: adminPath },
85
+ { key: "./typed.tsx", moduleAbsPath: typedPath },
86
+ { key: "./expression.tsx", moduleAbsPath: expressionPath },
87
+ { key: "./named-export.tsx", moduleAbsPath: namedExportPath },
88
+ ]);
89
+
90
+ expect(source).toContain('"./_index.tsx": { loader: async () => page0, isAsyncDefault: true },');
91
+ expect(source).toContain('"./admin.tsx": { loader: async () => page1, isAsyncDefault: true },');
92
+ expect(source).toContain('"./typed.tsx": { loader: async () => page2, isAsyncDefault: true },');
93
+ expect(source).toContain('"./expression.tsx": { loader: async () => page3, isAsyncDefault: true },');
94
+ expect(source).toContain('"./named-export.tsx": { loader: async () => page4, isAsyncDefault: true },');
95
+ });
68
96
  });
69
97
 
70
98
  describe("CsrArtifactBuilder", () => {
@@ -117,7 +145,8 @@ describe("SsrBaseArtifactBuilder", () => {
117
145
  const development = await prepareCssAsset("start", "", css);
118
146
  const production = await prepareCssAsset("build", "", css);
119
147
 
120
- expect(development).toBe(css);
148
+ expect(development).toContain(".card");
149
+ expect(development).toContain("color: red");
121
150
  expect(production.length).toBeLessThan(css.length);
122
151
  expect(production).toContain(".card{");
123
152
  expect(production).not.toContain("\n ");
@@ -1,4 +1,6 @@
1
+ import fs from "node:fs";
1
2
  import path from "node:path";
3
+ import ts from "typescript";
2
4
  import type { PageEntry } from "../artifact/implicitRootLayout";
3
5
 
4
6
  export class PagesEntrySourceGenerator {
@@ -29,9 +31,89 @@ export class PagesEntrySourceGenerator {
29
31
  const absPath = path.resolve(moduleAbsPath);
30
32
  return `import * as page${index} from ${JSON.stringify(absPath)};`;
31
33
  });
32
- const entries = this.#pageEntries.map(({ key }, index) => {
33
- return ` ${JSON.stringify(key)}: async () => page${index},`;
34
+ const entries = this.#pageEntries.map(({ key, moduleAbsPath }, index) => {
35
+ const isAsyncDefault = PagesEntrySourceGenerator.#hasAsyncDefaultExport(moduleAbsPath);
36
+ return ` ${JSON.stringify(key)}: { loader: async () => page${index}, isAsyncDefault: ${isAsyncDefault} },`;
34
37
  });
35
38
  return `${imports.join("\n")}\nexport const pages = {\n${entries.join("\n")}\n};\n`;
36
39
  }
40
+
41
+ static #hasAsyncDefaultExport(moduleAbsPath: string): boolean {
42
+ try {
43
+ const source = fs.readFileSync(path.resolve(moduleAbsPath), "utf8");
44
+ const sourceFile = ts.createSourceFile(
45
+ moduleAbsPath,
46
+ source,
47
+ ts.ScriptTarget.Latest,
48
+ true,
49
+ PagesEntrySourceGenerator.#scriptKind(moduleAbsPath),
50
+ );
51
+ return PagesEntrySourceGenerator.#sourceFileHasAsyncDefaultExport(sourceFile);
52
+ } catch {
53
+ return false;
54
+ }
55
+ }
56
+
57
+ static #sourceFileHasAsyncDefaultExport(sourceFile: ts.SourceFile): boolean {
58
+ const asyncBindings = new Map<string, boolean>();
59
+ let defaultIdentifier: string | null = null;
60
+
61
+ for (const statement of sourceFile.statements) {
62
+ if (ts.isFunctionDeclaration(statement)) {
63
+ if (PagesEntrySourceGenerator.#hasModifier(statement, ts.SyntaxKind.DefaultKeyword)) {
64
+ return PagesEntrySourceGenerator.#hasModifier(statement, ts.SyntaxKind.AsyncKeyword);
65
+ }
66
+ if (statement.name) {
67
+ asyncBindings.set(
68
+ statement.name.text,
69
+ PagesEntrySourceGenerator.#hasModifier(statement, ts.SyntaxKind.AsyncKeyword),
70
+ );
71
+ }
72
+ continue;
73
+ }
74
+
75
+ if (ts.isVariableStatement(statement)) {
76
+ for (const declaration of statement.declarationList.declarations) {
77
+ if (!ts.isIdentifier(declaration.name)) continue;
78
+ asyncBindings.set(
79
+ declaration.name.text,
80
+ PagesEntrySourceGenerator.#isAsyncFunctionExpression(declaration.initializer),
81
+ );
82
+ }
83
+ continue;
84
+ }
85
+
86
+ if (ts.isExportAssignment(statement)) {
87
+ if (PagesEntrySourceGenerator.#isAsyncFunctionExpression(statement.expression)) return true;
88
+ if (ts.isIdentifier(statement.expression)) defaultIdentifier = statement.expression.text;
89
+ continue;
90
+ }
91
+
92
+ if (ts.isExportDeclaration(statement) && statement.exportClause && ts.isNamedExports(statement.exportClause)) {
93
+ const exportClause = statement.exportClause;
94
+ for (const specifier of exportClause.elements) {
95
+ if (specifier.name.text !== "default") continue;
96
+ defaultIdentifier = specifier.propertyName?.text ?? specifier.name.text;
97
+ }
98
+ }
99
+ }
100
+
101
+ return defaultIdentifier ? asyncBindings.get(defaultIdentifier) === true : false;
102
+ }
103
+
104
+ static #hasModifier(node: ts.Node, kind: ts.SyntaxKind): boolean {
105
+ return ts.canHaveModifiers(node) && (ts.getModifiers(node)?.some((modifier) => modifier.kind === kind) ?? false);
106
+ }
107
+
108
+ static #isAsyncFunctionExpression(node?: ts.Expression): boolean {
109
+ return Boolean(
110
+ node &&
111
+ (ts.isArrowFunction(node) || ts.isFunctionExpression(node)) &&
112
+ PagesEntrySourceGenerator.#hasModifier(node, ts.SyntaxKind.AsyncKeyword),
113
+ );
114
+ }
115
+
116
+ static #scriptKind(moduleAbsPath: string): ts.ScriptKind {
117
+ return moduleAbsPath.endsWith(".tsx") || moduleAbsPath.endsWith(".jsx") ? ts.ScriptKind.TSX : ts.ScriptKind.TS;
118
+ }
37
119
  }
@@ -18,8 +18,7 @@ export interface BuildSsrBaseArtifactResult {
18
18
  }
19
19
 
20
20
  export function prepareCssAsset(command: "build" | "start", basePath: string, cssText: string): string {
21
- if (command !== "build") return cssText;
22
- return optimize(cssText, { file: `${basePath || "root"}.css`, minify: true }).code;
21
+ return optimize(cssText, { file: `${basePath || "root"}.css`, minify: command === "build" }).code;
23
22
  }
24
23
 
25
24
  export class SsrBaseArtifactBuilder {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@akanjs/devkit",
3
- "version": "2.1.2-rc.1",
3
+ "version": "2.2.0-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.1.2-rc.1",
35
+ "akanjs": "2.2.0-rc.1",
36
36
  "chalk": "^5.6.2",
37
37
  "commander": "^14.0.3",
38
38
  "daisyui": "^5.5.20",