@akanjs/devkit 2.1.2-rc.1 → 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 +9 -0
- package/capacitorApp.ts +39 -6
- package/cloud/constants.ts +2 -3
- package/frontendBuild/csrArtifactBuilder.ts +15 -0
- package/frontendBuild/frontendBuild.test.ts +32 -3
- package/frontendBuild/pagesEntrySourceGenerator.ts +84 -2
- package/frontendBuild/ssrBaseArtifactBuilder.ts +1 -2
- package/package.json +2 -2
package/CHANGELOG.md
ADDED
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:
|
|
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/constants.ts
CHANGED
|
@@ -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
|
-
|
|
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).
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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",
|