@akanjs/devkit 2.1.0-rc.1 → 2.1.0-rc.11
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/akanConfig/akanConfig.test.ts +36 -0
- package/akanConfig/akanConfig.ts +22 -2
- package/akanConfig/types.ts +1 -1
- package/applicationBuildRunner.test.ts +10 -0
- package/applicationBuildRunner.ts +27 -20
- package/applicationTestPreload.ts +40 -0
- package/capacitorApp.ts +1 -1
- package/commandDecorators/command.ts +29 -3
- package/devkitUtils.test.ts +21 -0
- package/executors.test.ts +36 -1
- package/executors.ts +90 -3
- package/frontendBuild/buildRouteClient.test.ts +2 -4
- package/frontendBuild/cssImportResolver.ts +48 -12
- package/frontendBuild/routeClientBuilder.ts +1 -5
- package/frontendBuild/ssrBaseArtifactBuilder.ts +19 -3
- package/incrementalBuilder/incrementalBuilder.host.ts +1 -0
- package/incrementalBuilder/incrementalBuilder.proc.ts +1 -1
- package/index.ts +1 -0
- package/lint/no-import-external-library.grit +1 -0
- package/package.json +4 -7
- package/src/capacitorApp.ts +1 -1
- package/typecheck/typecheck.proc.ts +14 -0
- package/capacitor.base.config.ts +0 -88
|
@@ -132,6 +132,7 @@ describe("AkanAppConfig", () => {
|
|
|
132
132
|
react: "19.0.0",
|
|
133
133
|
"react-dom": "19.0.0",
|
|
134
134
|
"react-server-dom-webpack": "19.0.0",
|
|
135
|
+
croner: akanPackageJson.peerDependencies?.croner,
|
|
135
136
|
sharp: "1.0.0",
|
|
136
137
|
"@external/runtime": "2.0.0",
|
|
137
138
|
},
|
|
@@ -168,10 +169,45 @@ describe("AkanAppConfig", () => {
|
|
|
168
169
|
react: runtimeDependencies.react,
|
|
169
170
|
"react-dom": runtimeDependencies["react-dom"],
|
|
170
171
|
"react-server-dom-webpack": runtimeDependencies["react-server-dom-webpack"],
|
|
172
|
+
croner: runtimeDependencies.croner,
|
|
171
173
|
sharp: runtimeDependencies.sharp,
|
|
172
174
|
});
|
|
173
175
|
});
|
|
174
176
|
|
|
177
|
+
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);
|
|
182
|
+
|
|
183
|
+
expect(singleConfig.getProductionPackageJson().dependencies).toMatchObject({
|
|
184
|
+
croner: runtimeDependencies.croner,
|
|
185
|
+
});
|
|
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");
|
|
191
|
+
|
|
192
|
+
expect(multipleConfig.getProductionPackageJson().dependencies).toMatchObject({
|
|
193
|
+
"@libsql/client": runtimeDependencies["@libsql/client"],
|
|
194
|
+
bullmq: runtimeDependencies.bullmq,
|
|
195
|
+
croner: runtimeDependencies.croner,
|
|
196
|
+
ioredis: runtimeDependencies.ioredis,
|
|
197
|
+
protobufjs: runtimeDependencies.protobufjs,
|
|
198
|
+
});
|
|
199
|
+
expect(multipleConfig.getProductionPackageJson().dependencies).not.toHaveProperty("postgres");
|
|
200
|
+
|
|
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");
|
|
209
|
+
});
|
|
210
|
+
|
|
175
211
|
test("normalizes multiple mobile targets and validates base paths", () => {
|
|
176
212
|
const config = new AkanAppConfig(
|
|
177
213
|
app,
|
package/akanConfig/akanConfig.ts
CHANGED
|
@@ -50,7 +50,18 @@ const DEFAULT_OPTIMIZE_IMPORTS = [
|
|
|
50
50
|
const WORKSPACE_BARREL_FACETS = ["ui", "webkit", "common", "client", "server"] as const;
|
|
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
|
-
const
|
|
53
|
+
const DEFAULT_BACKEND_RUNTIME_PACKAGES = ["croner"] as const;
|
|
54
|
+
const DATABASE_MODE_RUNTIME_PACKAGES = {
|
|
55
|
+
single: [],
|
|
56
|
+
multiple: ["@libsql/client", "bullmq", "ioredis", "protobufjs"],
|
|
57
|
+
cluster: ["bullmq", "ioredis", "postgres", "protobufjs"],
|
|
58
|
+
} satisfies Record<DatabaseMode, readonly string[]>;
|
|
59
|
+
const AKAN_RUNTIME_PACKAGES = new Set<string>([
|
|
60
|
+
...SSR_RUNTIME_PACKAGES,
|
|
61
|
+
...NATIVE_RUNTIME_PACKAGES,
|
|
62
|
+
...DEFAULT_BACKEND_RUNTIME_PACKAGES,
|
|
63
|
+
...Object.values(DATABASE_MODE_RUNTIME_PACKAGES).flat(),
|
|
64
|
+
]);
|
|
54
65
|
const DEFAULT_AKAN_IMAGE_CONFIG: AkanImageConfig = {
|
|
55
66
|
deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
|
|
56
67
|
imageSizes: [32, 48, 64, 96, 128, 256, 384],
|
|
@@ -272,6 +283,15 @@ CMD [${command.map((c) => `"${c}"`).join(",")}]`;
|
|
|
272
283
|
if (AKAN_RUNTIME_PACKAGES.has(lib))
|
|
273
284
|
return akanPackageJson.dependencies?.[lib] ?? akanPackageJson.peerDependencies?.[lib];
|
|
274
285
|
}
|
|
286
|
+
#getProductionRuntimePackages() {
|
|
287
|
+
return [
|
|
288
|
+
...this.externalLibs,
|
|
289
|
+
...SSR_RUNTIME_PACKAGES,
|
|
290
|
+
...NATIVE_RUNTIME_PACKAGES,
|
|
291
|
+
...DEFAULT_BACKEND_RUNTIME_PACKAGES,
|
|
292
|
+
...DATABASE_MODE_RUNTIME_PACKAGES[this.defaultDatabaseMode],
|
|
293
|
+
];
|
|
294
|
+
}
|
|
275
295
|
getProductionPackageJson(data: Partial<PackageJson> = {}): PackageJson {
|
|
276
296
|
return {
|
|
277
297
|
name: this.app.name,
|
|
@@ -279,7 +299,7 @@ CMD [${command.map((c) => `"${c}"`).join(",")}]`;
|
|
|
279
299
|
version: "1.0.0",
|
|
280
300
|
main: "./main.js",
|
|
281
301
|
dependencies: Object.fromEntries(
|
|
282
|
-
[...new Set(
|
|
302
|
+
[...new Set(this.#getProductionRuntimePackages())].map((lib) => {
|
|
283
303
|
const version = this.#resolveProductionDependencyVersion(lib);
|
|
284
304
|
if (!version) throw new Error(`Dependency ${lib} not found in package.json`);
|
|
285
305
|
return [lib, version];
|
package/akanConfig/types.ts
CHANGED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { AKAN_OPTIONAL_BACKEND_EXTERNALS } from "./applicationBuildRunner";
|
|
3
|
+
|
|
4
|
+
describe("ApplicationBuildRunner", () => {
|
|
5
|
+
test("externalizes Akan optional backend dependencies", () => {
|
|
6
|
+
expect(AKAN_OPTIONAL_BACKEND_EXTERNALS).toEqual(
|
|
7
|
+
expect.arrayContaining(["@libsql/client", "bullmq", "croner", "ioredis", "postgres", "protobufjs"]),
|
|
8
|
+
);
|
|
9
|
+
});
|
|
10
|
+
});
|
|
@@ -47,26 +47,20 @@ const SSR_RENDER_EXTERNALS = [
|
|
|
47
47
|
"react/jsx-dev-runtime",
|
|
48
48
|
"react-dom",
|
|
49
49
|
"react-dom/server.browser",
|
|
50
|
+
"react-server-dom-webpack",
|
|
51
|
+
"react-server-dom-webpack/server.node",
|
|
50
52
|
"react-server-dom-webpack/client.node",
|
|
51
53
|
"react-server-dom-webpack/client.browser",
|
|
52
54
|
] as const;
|
|
53
55
|
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
console.error(result.message);
|
|
63
|
-
process.exit(1);
|
|
64
|
-
}
|
|
65
|
-
} catch (error) {
|
|
66
|
-
console.error(error instanceof Error ? error.message : String(error));
|
|
67
|
-
process.exit(1);
|
|
68
|
-
}
|
|
69
|
-
`;
|
|
56
|
+
export const AKAN_OPTIONAL_BACKEND_EXTERNALS = [
|
|
57
|
+
"@libsql/client",
|
|
58
|
+
"bullmq",
|
|
59
|
+
"croner",
|
|
60
|
+
"ioredis",
|
|
61
|
+
"postgres",
|
|
62
|
+
"protobufjs",
|
|
63
|
+
] as const;
|
|
70
64
|
|
|
71
65
|
export class ApplicationBuildRunner {
|
|
72
66
|
#app: App;
|
|
@@ -178,7 +172,9 @@ export class ApplicationBuildRunner {
|
|
|
178
172
|
|
|
179
173
|
async #buildBackend() {
|
|
180
174
|
const akanConfig = await this.#app.getConfig();
|
|
181
|
-
const backendExternals = [
|
|
175
|
+
const backendExternals = [
|
|
176
|
+
...new Set([...akanConfig.externalLibs, ...SSR_RENDER_EXTERNALS, ...AKAN_OPTIONAL_BACKEND_EXTERNALS]),
|
|
177
|
+
];
|
|
182
178
|
const backendEntryPoints = [`${this.#app.cwdPath}/main.ts`, `${this.#app.cwdPath}/server.ts`];
|
|
183
179
|
for (const entrypoint of backendEntryPoints) {
|
|
184
180
|
if (!(await Bun.file(entrypoint).exists())) throw new Error(`Backend entrypoint not found: ${entrypoint}`);
|
|
@@ -200,8 +196,7 @@ export class ApplicationBuildRunner {
|
|
|
200
196
|
conditions: ["react-server"],
|
|
201
197
|
// `akan build` must embed production react-server-dom regardless of the shell's NODE_ENV.
|
|
202
198
|
define: { "process.env.NODE_ENV": JSON.stringify("production") },
|
|
203
|
-
plugins:
|
|
204
|
-
akanConfig.externalLibs.length > 0 ? [this.#createExternalSpecifiersPlugin(akanConfig.externalLibs)] : [],
|
|
199
|
+
plugins: backendExternals.length > 0 ? [this.#createExternalSpecifiersPlugin(backendExternals)] : [],
|
|
205
200
|
});
|
|
206
201
|
return {
|
|
207
202
|
entrypoints: backendEntryPoints.length + 1,
|
|
@@ -258,7 +253,8 @@ export class ApplicationBuildRunner {
|
|
|
258
253
|
}
|
|
259
254
|
|
|
260
255
|
async #checkProjectInChildProcess(tsconfigPath: string) {
|
|
261
|
-
const
|
|
256
|
+
const entry = await this.#resolveTypecheckWorkerEntry();
|
|
257
|
+
const proc = Bun.spawn([process.execPath, entry], {
|
|
262
258
|
cwd: this.#app.workspace.workspaceRoot,
|
|
263
259
|
env: this.#app.getCommandEnv({
|
|
264
260
|
AKAN_COMMAND_TYPE: "typecheck",
|
|
@@ -275,6 +271,17 @@ export class ApplicationBuildRunner {
|
|
|
275
271
|
if (exitCode !== 0) throw new Error((stderr || stdout).trim() || `Typecheck failed with exit code ${exitCode}`);
|
|
276
272
|
}
|
|
277
273
|
|
|
274
|
+
async #resolveTypecheckWorkerEntry() {
|
|
275
|
+
const candidates = [
|
|
276
|
+
path.join(this.#app.workspace.workspaceRoot, "pkgs/@akanjs/devkit/typecheck/typecheck.proc.ts"),
|
|
277
|
+
path.join(this.#app.workspace.workspaceRoot, "node_modules/@akanjs/devkit/typecheck/typecheck.proc.ts"),
|
|
278
|
+
path.join(import.meta.dir, "typecheck.proc.js"),
|
|
279
|
+
path.join(import.meta.dir, "typecheck.proc.ts"),
|
|
280
|
+
];
|
|
281
|
+
for (const candidate of candidates) if (await Bun.file(candidate).exists()) return candidate;
|
|
282
|
+
throw new Error(`[cli] typecheck worker entry not found; looked in: ${candidates.join(", ")}`);
|
|
283
|
+
}
|
|
284
|
+
|
|
278
285
|
async #buildOrThrow(label: string, config: Bun.BuildConfig): Promise<Bun.BuildOutput> {
|
|
279
286
|
const result = await Bun.build(config);
|
|
280
287
|
if (!result.success) throw new AggregateError(result.logs, `[${label}] Bun.build failed`);
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
|
|
3
|
+
export const SIGNAL_TEST_PRELOAD_PATH = "test/signalTest.preload.ts";
|
|
4
|
+
|
|
5
|
+
export interface SignalTestPreloadTarget {
|
|
6
|
+
cwdPath: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export async function resolveSignalTestPreloadPath(target: SignalTestPreloadTarget) {
|
|
10
|
+
const candidates: string[] = [];
|
|
11
|
+
const addResolvedPackageCandidate = (basePath: string) => {
|
|
12
|
+
try {
|
|
13
|
+
candidates.push(
|
|
14
|
+
path.join(path.dirname(Bun.resolveSync("akanjs/package.json", basePath)), SIGNAL_TEST_PRELOAD_PATH),
|
|
15
|
+
);
|
|
16
|
+
} catch {
|
|
17
|
+
// Source workspaces and published installs can resolve Akan packages from different roots.
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
addResolvedPackageCandidate(target.cwdPath);
|
|
22
|
+
addResolvedPackageCandidate(process.cwd());
|
|
23
|
+
addResolvedPackageCandidate(path.dirname(Bun.main));
|
|
24
|
+
addResolvedPackageCandidate(import.meta.dir);
|
|
25
|
+
|
|
26
|
+
candidates.push(
|
|
27
|
+
path.join(target.cwdPath, "../../node_modules/akanjs", SIGNAL_TEST_PRELOAD_PATH),
|
|
28
|
+
path.join(target.cwdPath, "../../pkgs/akanjs", SIGNAL_TEST_PRELOAD_PATH),
|
|
29
|
+
path.join(process.cwd(), "node_modules/akanjs", SIGNAL_TEST_PRELOAD_PATH),
|
|
30
|
+
path.join(process.cwd(), "pkgs/akanjs", SIGNAL_TEST_PRELOAD_PATH),
|
|
31
|
+
path.join(path.dirname(Bun.main), "../../akanjs", SIGNAL_TEST_PRELOAD_PATH),
|
|
32
|
+
path.resolve(import.meta.dir, "../../akanjs", SIGNAL_TEST_PRELOAD_PATH),
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
for (const candidate of [...new Set(candidates)]) {
|
|
36
|
+
if (await Bun.file(candidate).exists()) return candidate;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
throw new Error(`Failed to locate ${SIGNAL_TEST_PRELOAD_PATH} from ${target.cwdPath}`);
|
|
40
|
+
}
|
package/capacitorApp.ts
CHANGED
|
@@ -217,7 +217,7 @@ export class CapacitorApp {
|
|
|
217
217
|
.split(path.sep)
|
|
218
218
|
.join("/");
|
|
219
219
|
const content = `import type { AppScanResult } from "akanjs";
|
|
220
|
-
import { withBase } from "
|
|
220
|
+
import { withBase } from "akanjs/capacitor.base.config";
|
|
221
221
|
import appInfo from "${appInfoPath.startsWith(".") ? appInfoPath : `./${appInfoPath}`}";
|
|
222
222
|
|
|
223
223
|
export default withBase(
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { confirm, input, select } from "@inquirer/prompts";
|
|
2
1
|
import path from "node:path";
|
|
2
|
+
import { confirm, input, select } from "@inquirer/prompts";
|
|
3
3
|
import { Logger } from "akanjs/common";
|
|
4
4
|
import chalk from "chalk";
|
|
5
5
|
import { type Command, program } from "commander";
|
|
@@ -19,6 +19,32 @@ import { formatCommandHelp, formatHelp } from "./helpFormatter";
|
|
|
19
19
|
import { type CommandCls, getTargetMetas } from "./targetMeta";
|
|
20
20
|
|
|
21
21
|
const camelToKebabCase = (str: string) => str.replace(/([A-Z])/g, "-$1").toLowerCase();
|
|
22
|
+
const loggedCliErrorObjects = new WeakSet<object>();
|
|
23
|
+
const loggedCliErrorMessages = new Set<string>();
|
|
24
|
+
|
|
25
|
+
const formatCliError = (error: unknown): string => {
|
|
26
|
+
if (error instanceof Error) return error.message || error.name;
|
|
27
|
+
if (typeof error === "string") return error.trim() || "Unknown error";
|
|
28
|
+
if (error === null || error === undefined) return "Unknown error";
|
|
29
|
+
try {
|
|
30
|
+
const json = JSON.stringify(error);
|
|
31
|
+
if (json) return json;
|
|
32
|
+
} catch {
|
|
33
|
+
return String(error);
|
|
34
|
+
}
|
|
35
|
+
return String(error) || "Unknown error";
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const printCliError = (error: unknown) => {
|
|
39
|
+
if (typeof error === "object" && error !== null) {
|
|
40
|
+
if (loggedCliErrorObjects.has(error)) return;
|
|
41
|
+
loggedCliErrorObjects.add(error);
|
|
42
|
+
}
|
|
43
|
+
const message = formatCliError(error);
|
|
44
|
+
if (loggedCliErrorMessages.has(message)) return;
|
|
45
|
+
loggedCliErrorMessages.add(message);
|
|
46
|
+
Logger.rawLog(`\n${chalk.red(message)}`);
|
|
47
|
+
};
|
|
22
48
|
|
|
23
49
|
const handleOption = (programCommand: Command, argMeta: ArgMeta) => {
|
|
24
50
|
const {
|
|
@@ -219,6 +245,7 @@ const getInternalArgumentValue = async (
|
|
|
219
245
|
|
|
220
246
|
export const runCommands = async (...commands: CommandCls[]) => {
|
|
221
247
|
process.on("unhandledRejection", (error) => {
|
|
248
|
+
printCliError(error);
|
|
222
249
|
process.exit(1);
|
|
223
250
|
});
|
|
224
251
|
const __dirname = getDirname(import.meta.url);
|
|
@@ -339,8 +366,7 @@ It may cause unexpected behavior. Run \`akan update\` to update latest akanjs.`,
|
|
|
339
366
|
await targetMeta.handler.call(cmd, ...commandArgs);
|
|
340
367
|
Logger.rawLog();
|
|
341
368
|
} catch (e) {
|
|
342
|
-
|
|
343
|
-
Logger.rawLog(`\n${chalk.red(errMsg)}`);
|
|
369
|
+
printCliError(e);
|
|
344
370
|
throw e;
|
|
345
371
|
}
|
|
346
372
|
});
|
package/devkitUtils.test.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
|
3
3
|
import os from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { ApplicationBuildReporter } from "./applicationBuildReporter";
|
|
6
|
+
import { resolveSignalTestPreloadPath } from "./applicationTestPreload";
|
|
6
7
|
import { TypeScriptDependencyScanner } from "./dependencyScanner";
|
|
7
8
|
import { extractDependencies } from "./extractDeps";
|
|
8
9
|
import { getModelFileData } from "./getModelFileData";
|
|
@@ -84,6 +85,26 @@ describe("extractDependencies", () => {
|
|
|
84
85
|
});
|
|
85
86
|
});
|
|
86
87
|
|
|
88
|
+
describe("resolveSignalTestPreloadPath", () => {
|
|
89
|
+
test("resolves the preload file from an installed akanjs package", async () => {
|
|
90
|
+
const root = await makeTempRoot();
|
|
91
|
+
const libDir = path.join(root, "libs/shared");
|
|
92
|
+
await write(
|
|
93
|
+
path.join(root, "node_modules/akanjs/package.json"),
|
|
94
|
+
JSON.stringify({
|
|
95
|
+
name: "akanjs",
|
|
96
|
+
version: "0.0.0",
|
|
97
|
+
exports: { "./package.json": "./package.json" },
|
|
98
|
+
}),
|
|
99
|
+
);
|
|
100
|
+
await write(path.join(root, "node_modules/akanjs/test/signalTest.preload.ts"), "export {};\n");
|
|
101
|
+
|
|
102
|
+
await expect(resolveSignalTestPreloadPath({ cwdPath: libDir })).resolves.toContain(
|
|
103
|
+
"node_modules/akanjs/test/signalTest.preload.ts",
|
|
104
|
+
);
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
87
108
|
describe("TypeScriptDependencyScanner", () => {
|
|
88
109
|
test("separates monorepo package, lib, runtime, and type-only dependencies", async () => {
|
|
89
110
|
const root = await makeTempRoot();
|
package/executors.test.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { mkdir, mkdtemp, readFile, rm, stat, writeFile } from "node:fs/promises"
|
|
|
3
3
|
import os from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { AkanAppConfig } from "./akanConfig";
|
|
6
|
-
import { AppExecutor, Executor, PkgExecutor, WorkspaceExecutor } from "./executors";
|
|
6
|
+
import { AppExecutor, CommandExecutionError, Executor, PkgExecutor, WorkspaceExecutor } from "./executors";
|
|
7
7
|
import { AppInfo } from "./scanInfo";
|
|
8
8
|
import type { PackageJson } from "./types";
|
|
9
9
|
|
|
@@ -48,6 +48,41 @@ afterEach(async () => {
|
|
|
48
48
|
});
|
|
49
49
|
|
|
50
50
|
describe("Executor filesystem helpers", () => {
|
|
51
|
+
test("reports command failures with command context and captured output", async () => {
|
|
52
|
+
const root = await makeTempRoot();
|
|
53
|
+
const exec = new Executor("fixture", root);
|
|
54
|
+
|
|
55
|
+
let error: unknown;
|
|
56
|
+
try {
|
|
57
|
+
await exec.spawn(process.execPath, ["--eval", "console.error('spawn failed'); process.exit(7)"]);
|
|
58
|
+
} catch (caught) {
|
|
59
|
+
error = caught;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
expect(error).toBeInstanceOf(CommandExecutionError);
|
|
63
|
+
expect((error as CommandExecutionError).message).toContain(`Command failed: ${process.execPath}`);
|
|
64
|
+
expect((error as CommandExecutionError).message).toContain(`cwd: ${root}`);
|
|
65
|
+
expect((error as CommandExecutionError).message).toContain("exit code: 7");
|
|
66
|
+
expect((error as CommandExecutionError).message).toContain("spawn failed");
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test("reports inherited stdio command failures with a fallback message", async () => {
|
|
70
|
+
const root = await makeTempRoot();
|
|
71
|
+
const exec = new Executor("fixture", root);
|
|
72
|
+
|
|
73
|
+
let error: unknown;
|
|
74
|
+
try {
|
|
75
|
+
await exec.spawn(process.execPath, ["--eval", "process.exit(3)"], { stdio: "inherit" });
|
|
76
|
+
} catch (caught) {
|
|
77
|
+
error = caught;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
expect(error).toBeInstanceOf(CommandExecutionError);
|
|
81
|
+
expect((error as CommandExecutionError).message).toContain(`Command failed: ${process.execPath}`);
|
|
82
|
+
expect((error as CommandExecutionError).message).toContain(`cwd: ${root}`);
|
|
83
|
+
expect((error as CommandExecutionError).message).toContain("exit code: 3");
|
|
84
|
+
});
|
|
85
|
+
|
|
51
86
|
test("resolves paths and reads/writes files relative to cwd", async () => {
|
|
52
87
|
const root = await makeTempRoot();
|
|
53
88
|
const exec = new Executor("fixture", root);
|
package/executors.ts
CHANGED
|
@@ -64,6 +64,58 @@ const staticTemplateFileExtensions = new Set([
|
|
|
64
64
|
".xml",
|
|
65
65
|
]);
|
|
66
66
|
|
|
67
|
+
const formatCommandArg = (value: string) => (/^[\w@%+=:,./-]+$/.test(value) ? value : JSON.stringify(value));
|
|
68
|
+
|
|
69
|
+
const formatCommandForDisplay = (command: string, args: string[] = []) =>
|
|
70
|
+
[command, ...args].map(formatCommandArg).join(" ");
|
|
71
|
+
|
|
72
|
+
export interface CommandExecutionErrorOptions {
|
|
73
|
+
command: string;
|
|
74
|
+
args?: string[];
|
|
75
|
+
cwd: string;
|
|
76
|
+
code: number | null;
|
|
77
|
+
signal: string | null;
|
|
78
|
+
stdout?: string;
|
|
79
|
+
stderr?: string;
|
|
80
|
+
cause?: unknown;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export class CommandExecutionError extends Error {
|
|
84
|
+
command: string;
|
|
85
|
+
args: string[];
|
|
86
|
+
cwd: string;
|
|
87
|
+
code: number | null;
|
|
88
|
+
signal: string | null;
|
|
89
|
+
stdout: string;
|
|
90
|
+
stderr: string;
|
|
91
|
+
|
|
92
|
+
constructor({
|
|
93
|
+
command,
|
|
94
|
+
args = [],
|
|
95
|
+
cwd,
|
|
96
|
+
code,
|
|
97
|
+
signal,
|
|
98
|
+
stdout = "",
|
|
99
|
+
stderr = "",
|
|
100
|
+
cause,
|
|
101
|
+
}: CommandExecutionErrorOptions) {
|
|
102
|
+
const displayCommand = formatCommandForDisplay(command, args);
|
|
103
|
+
const status = signal ? `signal: ${signal}` : `exit code: ${code ?? "unknown"}`;
|
|
104
|
+
const output = (stderr || stdout).trim();
|
|
105
|
+
super([`Command failed: ${displayCommand}`, `cwd: ${cwd}`, status, output ? `\n${output}` : ""].join("\n"), {
|
|
106
|
+
cause,
|
|
107
|
+
});
|
|
108
|
+
this.name = "CommandExecutionError";
|
|
109
|
+
this.command = command;
|
|
110
|
+
this.args = args;
|
|
111
|
+
this.cwd = cwd;
|
|
112
|
+
this.code = code;
|
|
113
|
+
this.signal = signal;
|
|
114
|
+
this.stdout = stdout;
|
|
115
|
+
this.stderr = stderr;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
67
119
|
export const execEmoji = {
|
|
68
120
|
workspace: "🏠",
|
|
69
121
|
app: "🚀",
|
|
@@ -197,22 +249,31 @@ export class Executor {
|
|
|
197
249
|
Logger.raw(chalk.red(data.toString()));
|
|
198
250
|
}
|
|
199
251
|
exec(command: string, options: ExecOptions = {}) {
|
|
252
|
+
const cwd = options.cwd?.toString() ?? this.cwdPath;
|
|
200
253
|
const proc = exec(command, { cwd: this.cwdPath, ...options });
|
|
254
|
+
let stdout = "";
|
|
255
|
+
let stderr = "";
|
|
201
256
|
proc.stdout?.on("data", (data: Buffer) => {
|
|
257
|
+
stdout += data.toString();
|
|
202
258
|
this.#stdout(data);
|
|
203
259
|
});
|
|
204
260
|
proc.stderr?.on("data", (data: Buffer) => {
|
|
261
|
+
stderr += data.toString();
|
|
205
262
|
this.#stdout(data); // 정상로그도 stderr로 나옴
|
|
206
263
|
});
|
|
207
264
|
return new Promise((resolve, reject) => {
|
|
265
|
+
proc.on("error", (error) => {
|
|
266
|
+
reject(new CommandExecutionError({ command, cwd, code: null, signal: null, stdout, stderr, cause: error }));
|
|
267
|
+
});
|
|
208
268
|
proc.on("exit", (code, signal) => {
|
|
209
|
-
if (!!code || signal) reject({ code, signal });
|
|
269
|
+
if (!!code || signal) reject(new CommandExecutionError({ command, cwd, code, signal, stdout, stderr }));
|
|
210
270
|
else resolve({ code, signal });
|
|
211
271
|
});
|
|
212
272
|
});
|
|
213
273
|
}
|
|
214
274
|
|
|
215
275
|
spawn(command: string, args: string[] = [], options: SpawnOptions = {}): Promise<string> {
|
|
276
|
+
const cwd = options.cwd?.toString() ?? this.cwdPath;
|
|
216
277
|
const proc = spawn(command, args, {
|
|
217
278
|
cwd: this.cwdPath,
|
|
218
279
|
// stdio: "inherit",
|
|
@@ -232,8 +293,14 @@ export class Executor {
|
|
|
232
293
|
this.#stdout(data); // 정상로그도 stderr로 나옴
|
|
233
294
|
});
|
|
234
295
|
return new Promise((resolve, reject) => {
|
|
296
|
+
proc.on("error", (error) => {
|
|
297
|
+
reject(
|
|
298
|
+
new CommandExecutionError({ command, args, cwd, code: null, signal: null, stdout, stderr, cause: error }),
|
|
299
|
+
);
|
|
300
|
+
});
|
|
235
301
|
proc.on("close", (code, signal) => {
|
|
236
|
-
if (code !== 0 || signal)
|
|
302
|
+
if (code !== 0 || signal)
|
|
303
|
+
reject(new CommandExecutionError({ command, args, cwd, code, signal, stdout, stderr }));
|
|
237
304
|
else resolve(stdout);
|
|
238
305
|
});
|
|
239
306
|
});
|
|
@@ -247,20 +314,40 @@ export class Executor {
|
|
|
247
314
|
return proc;
|
|
248
315
|
}
|
|
249
316
|
fork(modulePath: string, args: string[] = [], options: ForkOptions = {}) {
|
|
317
|
+
const cwd = options.cwd?.toString() ?? this.cwdPath;
|
|
250
318
|
const proc = fork(modulePath, args, {
|
|
251
319
|
cwd: this.cwdPath,
|
|
252
320
|
// stdio: ["ignore", "inherit", "inherit", "ipc"],
|
|
253
321
|
...options,
|
|
254
322
|
});
|
|
323
|
+
let stdout = "";
|
|
324
|
+
let stderr = "";
|
|
255
325
|
proc.stdout?.on("data", (data: Buffer) => {
|
|
326
|
+
stdout += data.toString();
|
|
256
327
|
this.#stdout(data);
|
|
257
328
|
});
|
|
258
329
|
proc.stderr?.on("data", (data: Buffer) => {
|
|
330
|
+
stderr += data.toString();
|
|
259
331
|
this.#stderr(data);
|
|
260
332
|
});
|
|
261
333
|
return new Promise((resolve, reject) => {
|
|
334
|
+
proc.on("error", (error) => {
|
|
335
|
+
reject(
|
|
336
|
+
new CommandExecutionError({
|
|
337
|
+
command: modulePath,
|
|
338
|
+
args,
|
|
339
|
+
cwd,
|
|
340
|
+
code: null,
|
|
341
|
+
signal: null,
|
|
342
|
+
stdout,
|
|
343
|
+
stderr,
|
|
344
|
+
cause: error,
|
|
345
|
+
}),
|
|
346
|
+
);
|
|
347
|
+
});
|
|
262
348
|
proc.on("exit", (code, signal) => {
|
|
263
|
-
if (!!code || signal)
|
|
349
|
+
if (!!code || signal)
|
|
350
|
+
reject(new CommandExecutionError({ command: modulePath, args, cwd, code, signal, stdout, stderr }));
|
|
264
351
|
else resolve({ code, signal });
|
|
265
352
|
});
|
|
266
353
|
});
|
|
@@ -72,14 +72,12 @@ describe("route client store bootstrap", () => {
|
|
|
72
72
|
expect(CLIENT_BUNDLE_NAMING.asset.includes("[name]")).toBe(false);
|
|
73
73
|
});
|
|
74
74
|
|
|
75
|
-
test("
|
|
75
|
+
test("keeps start SSR client React imports bare and rewrites resolved fetch imports", () => {
|
|
76
76
|
const aliases = RouteClientBuilder.resolveSsrClientRuntimeAliases();
|
|
77
77
|
|
|
78
|
-
expect(Object.keys(aliases)).toEqual(
|
|
78
|
+
expect(Object.keys(aliases)).not.toEqual(
|
|
79
79
|
expect.arrayContaining(["react", "react-dom", "react-dom/client", "react/jsx-runtime", "react/jsx-dev-runtime"]),
|
|
80
80
|
);
|
|
81
|
-
expect(aliases.react).toBe(Bun.resolveSync("react", RouteClientBuilder.resolveAkanServerEntry()));
|
|
82
|
-
expect(aliases.react).not.toBe("react");
|
|
83
81
|
expect(aliases[Bun.resolveSync("akanjs/fetch", RouteClientBuilder.resolveAkanServerEntry())]).toBe("akanjs/fetch");
|
|
84
82
|
});
|
|
85
83
|
|
|
@@ -24,9 +24,9 @@ export class CssImportResolver {
|
|
|
24
24
|
|
|
25
25
|
async resolve(id: string, fromBase: string): Promise<string | null> {
|
|
26
26
|
for (const resolve of [
|
|
27
|
+
() => this.#resolveWithTsconfig(id),
|
|
27
28
|
() => this.#resolveWithBun(id, fromBase),
|
|
28
29
|
() => this.#resolveWithRequire(id, fromBase),
|
|
29
|
-
() => this.#resolveWithTsconfig(id),
|
|
30
30
|
() => this.#resolvePackageStyle(id, fromBase),
|
|
31
31
|
]) {
|
|
32
32
|
const resolved = await resolve();
|
|
@@ -36,21 +36,27 @@ export class CssImportResolver {
|
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
#resolveWithBun(id: string, fromBase: string): string | null {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
39
|
+
for (const base of this.#resolutionBases(fromBase)) {
|
|
40
|
+
try {
|
|
41
|
+
const resolved = Bun.resolveSync(id, base);
|
|
42
|
+
if (CssImportResolver.isCssFile(resolved)) return resolved;
|
|
43
|
+
} catch {
|
|
44
|
+
// Try the next known package resolution root.
|
|
45
|
+
}
|
|
44
46
|
}
|
|
47
|
+
return null;
|
|
45
48
|
}
|
|
46
49
|
|
|
47
50
|
#resolveWithRequire(id: string, fromBase: string): string | null {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
51
|
+
for (const base of this.#resolutionBases(fromBase)) {
|
|
52
|
+
try {
|
|
53
|
+
const resolved = require.resolve(id, { paths: [base] });
|
|
54
|
+
if (CssImportResolver.isCssFile(resolved)) return resolved;
|
|
55
|
+
} catch {
|
|
56
|
+
// Try the next known package resolution root.
|
|
57
|
+
}
|
|
53
58
|
}
|
|
59
|
+
return null;
|
|
54
60
|
}
|
|
55
61
|
|
|
56
62
|
async #resolveWithTsconfig(id: string): Promise<string | null> {
|
|
@@ -77,8 +83,25 @@ export class CssImportResolver {
|
|
|
77
83
|
async #resolvePackageStyle(id: string, fromBase: string): Promise<string | null> {
|
|
78
84
|
const pkgName = CssImportResolver.getPackageName(id);
|
|
79
85
|
if (!pkgName) return null;
|
|
86
|
+
for (const base of this.#resolutionBases(fromBase)) {
|
|
87
|
+
try {
|
|
88
|
+
const pkgPath = require.resolve(`${pkgName}/package.json`, { paths: [base] });
|
|
89
|
+
const resolved = await this.#resolvePackageStyleFromPackageJson(id, pkgName, pkgPath);
|
|
90
|
+
if (resolved) return resolved;
|
|
91
|
+
} catch {
|
|
92
|
+
// Try the next known package resolution root.
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
for (const pkgPath of this.#packageJsonCandidates(pkgName)) {
|
|
96
|
+
const resolved = await this.#resolvePackageStyleFromPackageJson(id, pkgName, pkgPath);
|
|
97
|
+
if (resolved) return resolved;
|
|
98
|
+
}
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async #resolvePackageStyleFromPackageJson(id: string, pkgName: string, pkgPath: string): Promise<string | null> {
|
|
80
103
|
try {
|
|
81
|
-
|
|
104
|
+
if (!(await Bun.file(pkgPath).exists())) return null;
|
|
82
105
|
const pkgDir = path.dirname(pkgPath);
|
|
83
106
|
const pkg = await Bun.file(pkgPath).json();
|
|
84
107
|
const subpath = id === pkgName ? "." : `.${id.slice(pkgName.length)}`;
|
|
@@ -96,6 +119,19 @@ export class CssImportResolver {
|
|
|
96
119
|
}
|
|
97
120
|
}
|
|
98
121
|
|
|
122
|
+
#resolutionBases(fromBase: string): string[] {
|
|
123
|
+
return [fromBase, this.#workspaceRoot, path.dirname(Bun.main), path.resolve(path.dirname(Bun.main), "../..")];
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
#packageJsonCandidates(pkgName: string): string[] {
|
|
127
|
+
return [
|
|
128
|
+
path.join(this.#workspaceRoot, "pkgs", pkgName, "package.json"),
|
|
129
|
+
path.join(this.#workspaceRoot, "node_modules", pkgName, "package.json"),
|
|
130
|
+
path.join(path.dirname(Bun.main), "node_modules", pkgName, "package.json"),
|
|
131
|
+
path.join(path.dirname(Bun.main), "../../", pkgName, "package.json"),
|
|
132
|
+
];
|
|
133
|
+
}
|
|
134
|
+
|
|
99
135
|
async #firstExisting(basePath: string): Promise<string | null> {
|
|
100
136
|
for (const suffix of CSS_IMPORT_EXTS) {
|
|
101
137
|
const candidate = `${basePath}${suffix}`;
|
|
@@ -224,11 +224,7 @@ export class RouteClientBuilder {
|
|
|
224
224
|
|
|
225
225
|
static resolveSsrClientRuntimeAliases(): Record<string, string> {
|
|
226
226
|
const serverEntry = RouteClientBuilder.resolveAkanServerEntry();
|
|
227
|
-
|
|
228
|
-
SSR_CLIENT_ALIAS_EXTERNALS.map((specifier) => [specifier, Bun.resolveSync(specifier, serverEntry)]),
|
|
229
|
-
) as Record<string, string>;
|
|
230
|
-
aliases[Bun.resolveSync("akanjs/fetch", serverEntry)] = "akanjs/fetch";
|
|
231
|
-
return aliases;
|
|
227
|
+
return { [Bun.resolveSync("akanjs/fetch", serverEntry)]: "akanjs/fetch" };
|
|
232
228
|
}
|
|
233
229
|
|
|
234
230
|
static resolveSsrClientExternalOptions(command: "build" | "start"): {
|
|
@@ -92,15 +92,31 @@ export class SsrBaseArtifactBuilder {
|
|
|
92
92
|
return { rscClientUrl, vendorMap };
|
|
93
93
|
}
|
|
94
94
|
async #resolveAkanServerPath() {
|
|
95
|
-
const candidates = [
|
|
95
|
+
const candidates: string[] = [];
|
|
96
|
+
try {
|
|
97
|
+
candidates.push(path.dirname(Bun.resolveSync("akanjs/server", this.#app.workspace.workspaceRoot)));
|
|
98
|
+
} catch {
|
|
99
|
+
// Source workspaces and bundled CLI execution have different resolution roots; try explicit candidates below.
|
|
100
|
+
}
|
|
101
|
+
candidates.push(
|
|
96
102
|
path.join(this.#app.workspace.workspaceRoot, "pkgs/akanjs/server"),
|
|
103
|
+
path.join(this.#app.workspace.workspaceRoot, "node_modules/akanjs/server"),
|
|
104
|
+
);
|
|
105
|
+
try {
|
|
106
|
+
candidates.push(path.dirname(Bun.resolveSync("akanjs/server", path.dirname(Bun.main))));
|
|
107
|
+
} catch {
|
|
108
|
+
// Published CLI installs may hoist dependencies differently; explicit Bun.main candidates cover that.
|
|
109
|
+
}
|
|
110
|
+
candidates.push(
|
|
111
|
+
path.join(path.dirname(Bun.main), "node_modules/akanjs/server"),
|
|
112
|
+
path.join(path.dirname(Bun.main), "../../akanjs/server"),
|
|
97
113
|
path.resolve(import.meta.dir, "../../server"),
|
|
98
114
|
path.resolve(import.meta.dir, "../server"),
|
|
99
|
-
|
|
115
|
+
);
|
|
100
116
|
for (const candidate of candidates) {
|
|
101
117
|
if (await Bun.file(path.join(candidate, "rscClient.tsx")).exists()) return candidate;
|
|
102
118
|
}
|
|
103
|
-
|
|
119
|
+
throw new Error(`[base-artifact] failed to locate akanjs/server; looked in: ${candidates.join(", ")}`);
|
|
104
120
|
}
|
|
105
121
|
|
|
106
122
|
async #buildStyleAssets(): Promise<{
|
|
@@ -143,6 +143,7 @@ export class IncrementalBuilderHost {
|
|
|
143
143
|
app.workspace.workspaceRoot,
|
|
144
144
|
"node_modules/@akanjs/devkit/incrementalBuilder/incrementalBuilder.proc.ts",
|
|
145
145
|
),
|
|
146
|
+
path.join(import.meta.dir, "incrementalBuilder.proc.js"),
|
|
146
147
|
path.join(import.meta.dir, "incrementalBuilder.proc.ts"),
|
|
147
148
|
];
|
|
148
149
|
for (const c of candidates)
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
-
import { Logger } from "akanjs/common";
|
|
3
2
|
import {
|
|
4
3
|
type App,
|
|
5
4
|
AppExecutor,
|
|
@@ -16,6 +15,7 @@ import {
|
|
|
16
15
|
WatchRootResolver,
|
|
17
16
|
WorkspaceExecutor,
|
|
18
17
|
} from "@akanjs/devkit";
|
|
18
|
+
import { Logger } from "akanjs/common";
|
|
19
19
|
import type {
|
|
20
20
|
BaseBuildArtifact,
|
|
21
21
|
BuilderEvent,
|
package/index.ts
CHANGED
|
@@ -4,6 +4,7 @@ export * from "./akanConfig";
|
|
|
4
4
|
export * from "./applicationBuildReporter";
|
|
5
5
|
export * from "./applicationBuildRunner";
|
|
6
6
|
export * from "./applicationReleasePackager";
|
|
7
|
+
export * from "./applicationTestPreload";
|
|
7
8
|
export * from "./artifact";
|
|
8
9
|
export * from "./auth";
|
|
9
10
|
export * from "./builder";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@akanjs/devkit",
|
|
3
|
-
"version": "2.1.0-rc.
|
|
3
|
+
"version": "2.1.0-rc.11",
|
|
4
4
|
"sourceType": "module",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"publishConfig": {
|
|
@@ -23,11 +23,6 @@
|
|
|
23
23
|
"default": "./index.ts"
|
|
24
24
|
},
|
|
25
25
|
"./package.json": "./package.json",
|
|
26
|
-
"./capacitor.base.config": {
|
|
27
|
-
"import": "./capacitor.base.config.ts",
|
|
28
|
-
"types": "./capacitor.base.config.ts",
|
|
29
|
-
"default": "./capacitor.base.config.ts"
|
|
30
|
-
},
|
|
31
26
|
"./*": "./*"
|
|
32
27
|
},
|
|
33
28
|
"dependencies": {
|
|
@@ -36,9 +31,10 @@
|
|
|
36
31
|
"@langchain/deepseek": "^1.0.26",
|
|
37
32
|
"@langchain/openai": "^1.4.6",
|
|
38
33
|
"@trapezedev/project": "^7.1.4",
|
|
39
|
-
"akanjs": "2.1.0-rc.
|
|
34
|
+
"akanjs": "2.1.0-rc.10",
|
|
40
35
|
"chalk": "^5.6.2",
|
|
41
36
|
"commander": "^14.0.3",
|
|
37
|
+
"daisyui": "^5.5.20",
|
|
42
38
|
"fontaine": "^0.8.0",
|
|
43
39
|
"fonteditor-core": "^2.6.3",
|
|
44
40
|
"ignore": "^7.0.5",
|
|
@@ -47,6 +43,7 @@
|
|
|
47
43
|
"ora": "^9.4.0",
|
|
48
44
|
"ssh2": "^1.17.0",
|
|
49
45
|
"subset-font": "^2.5.0",
|
|
46
|
+
"tailwind-scrollbar": "^4.0.2",
|
|
50
47
|
"tailwindcss": "^4.3.0",
|
|
51
48
|
"typescript": "^6.0.3"
|
|
52
49
|
},
|
package/src/capacitorApp.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
+
import { type AppExecutor, FileSys } from "@akanjs/devkit";
|
|
1
2
|
import type { CapacitorConfig } from "@capacitor/cli";
|
|
2
3
|
import { MobileProject } from "@trapezedev/project";
|
|
3
4
|
import type { AndroidProject } from "@trapezedev/project/dist/android/project";
|
|
4
5
|
import type { IosProject } from "@trapezedev/project/dist/ios/project";
|
|
5
6
|
import { capitalize } from "akanjs/common";
|
|
6
|
-
import { type AppExecutor, FileSys } from "@akanjs/devkit";
|
|
7
7
|
|
|
8
8
|
import { FileEditor } from "./fileEditor";
|
|
9
9
|
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { TypeChecker } from "../typeChecker";
|
|
2
|
+
|
|
3
|
+
try {
|
|
4
|
+
const configPath = process.env.AKAN_TYPECHECK_TSCONFIG;
|
|
5
|
+
if (!configPath) throw new Error("AKAN_TYPECHECK_TSCONFIG is required");
|
|
6
|
+
const result = TypeChecker.checkProject(configPath);
|
|
7
|
+
if (result.errors.length > 0) {
|
|
8
|
+
console.error(result.message);
|
|
9
|
+
process.exit(1);
|
|
10
|
+
}
|
|
11
|
+
} catch (error) {
|
|
12
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
13
|
+
process.exit(1);
|
|
14
|
+
}
|
package/capacitor.base.config.ts
DELETED
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
import os from "node:os";
|
|
2
|
-
import type { CapacitorConfig } from "@capacitor/cli";
|
|
3
|
-
import type { AkanMobileTargetConfig, AppScanResult } from "akanjs";
|
|
4
|
-
|
|
5
|
-
const getLocalIP = () => {
|
|
6
|
-
const interfaces = os.networkInterfaces();
|
|
7
|
-
for (const interfaceName in interfaces) {
|
|
8
|
-
const iface = interfaces[interfaceName];
|
|
9
|
-
if (!iface) continue;
|
|
10
|
-
for (const alias of iface) {
|
|
11
|
-
if (alias.family === "IPv4" && !alias.internal) return alias.address;
|
|
12
|
-
}
|
|
13
|
-
}
|
|
14
|
-
return "127.0.0.1"; // fallback to localhost if no suitable IP found
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
const normalizeBasePath = (basePath: string | undefined) => basePath?.replace(/^\/+|\/+$/g, "");
|
|
18
|
-
|
|
19
|
-
const routeBasePaths = (appInfo: AppScanResult) =>
|
|
20
|
-
new Set(
|
|
21
|
-
appInfo.routes
|
|
22
|
-
.map((route) => route.replace(/^\.\//, "").split("/")[0])
|
|
23
|
-
.filter((segment): segment is string => !!segment && !segment.startsWith("_") && !segment.startsWith("(")),
|
|
24
|
-
);
|
|
25
|
-
|
|
26
|
-
const resolveTarget = (appInfo: AppScanResult, targetName = process.env.AKAN_MOBILE_TARGET) => {
|
|
27
|
-
const targets = appInfo.akanConfig.mobile.targets;
|
|
28
|
-
if (!targets || Object.keys(targets).length === 0) throw new Error("Akan mobile target metadata is missing.");
|
|
29
|
-
if (targetName) {
|
|
30
|
-
const target = targets[targetName];
|
|
31
|
-
if (!target) {
|
|
32
|
-
const basePath = normalizeBasePath(targetName);
|
|
33
|
-
const [template] = Object.values(targets);
|
|
34
|
-
if (basePath && template && routeBasePaths(appInfo).has(basePath))
|
|
35
|
-
return { ...template, name: basePath, basePath };
|
|
36
|
-
throw new Error(`Akan mobile target '${targetName}' was not found.`);
|
|
37
|
-
}
|
|
38
|
-
return target;
|
|
39
|
-
}
|
|
40
|
-
const entries = Object.entries(targets);
|
|
41
|
-
if (entries.length !== 1) throw new Error("AKAN_MOBILE_TARGET is required when multiple mobile targets exist.");
|
|
42
|
-
return entries[0]?.[1] as AkanMobileTargetConfig;
|
|
43
|
-
};
|
|
44
|
-
|
|
45
|
-
const localCsrUrl = (ip: string, target: AkanMobileTargetConfig) => {
|
|
46
|
-
const basePath = normalizeBasePath(target.basePath);
|
|
47
|
-
const port = process.env.AKAN_PUBLIC_CLIENT_PORT ?? process.env.PORT ?? "8282";
|
|
48
|
-
return `http://${ip}:${port}/${basePath ? `${basePath}` : ""}?csr=true`;
|
|
49
|
-
};
|
|
50
|
-
|
|
51
|
-
export const withBase = (
|
|
52
|
-
configImp: (config: CapacitorConfig, target: AkanMobileTargetConfig) => CapacitorConfig = (config) => config,
|
|
53
|
-
appData?: AppScanResult,
|
|
54
|
-
targetName?: string,
|
|
55
|
-
) => {
|
|
56
|
-
const ip = getLocalIP();
|
|
57
|
-
const appInfo = appData;
|
|
58
|
-
if (!appInfo) throw new Error("withBase requires apps/<app>/akan.app.json metadata.");
|
|
59
|
-
const target = resolveTarget(appInfo, targetName);
|
|
60
|
-
const baseConfig: CapacitorConfig = {
|
|
61
|
-
...target,
|
|
62
|
-
appId: target.appId,
|
|
63
|
-
appName: target.appName,
|
|
64
|
-
webDir: "dist",
|
|
65
|
-
server:
|
|
66
|
-
process.env.APP_OPERATION_MODE !== "release"
|
|
67
|
-
? {
|
|
68
|
-
androidScheme: "http",
|
|
69
|
-
url: localCsrUrl(ip, target),
|
|
70
|
-
cleartext: true,
|
|
71
|
-
allowNavigation: [ip, "localhost"],
|
|
72
|
-
}
|
|
73
|
-
: {
|
|
74
|
-
allowNavigation: ["*"],
|
|
75
|
-
},
|
|
76
|
-
plugins: {
|
|
77
|
-
CapacitorCookies: { enabled: true },
|
|
78
|
-
...target.plugins,
|
|
79
|
-
},
|
|
80
|
-
android: {
|
|
81
|
-
...target.android,
|
|
82
|
-
},
|
|
83
|
-
ios: {
|
|
84
|
-
...target.ios,
|
|
85
|
-
},
|
|
86
|
-
};
|
|
87
|
-
return configImp(baseConfig, target);
|
|
88
|
-
};
|