@akanjs/devkit 2.1.2-rc.0 → 2.2.0-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.
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
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() {
package/cloud/cloudApi.ts CHANGED
@@ -70,9 +70,10 @@ export class CloudApi {
70
70
  const data = await this.#api.post<boolean>(`/uploadEnv/${devProjectId}`, formData);
71
71
  return data;
72
72
  }
73
- async downloadEnv(devProjectId: string): Promise<void> {
73
+ async downloadEnv(devProjectId: string): Promise<unknown> {
74
74
  const localPath = `${this.#workspace.workspaceRoot}/local/env.tar`;
75
75
  await this.#api.getFile(`/downloadEnv/${devProjectId}`, localPath);
76
+ return localPath;
76
77
  }
77
78
  async getRemoteAuthToken(remoteId: string): Promise<AccessToken | null> {
78
79
  try {
@@ -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.0",
3
+ "version": "2.2.0-rc.0",
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.0",
35
+ "akanjs": "2.2.0-rc.0",
36
36
  "chalk": "^5.6.2",
37
37
  "commander": "^14.0.3",
38
38
  "daisyui": "^5.5.20",