@akanjs/devkit 3.0.0-beta.17 → 3.0.0-beta.19
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/abstractDoc.ts +5 -4
- package/akanApp/akanApp.host.ts +23 -9
- package/akanConfig/akanConfig.test.ts +55 -30
- package/akanConfig/akanConfig.ts +29 -17
- package/akanConfig/types.ts +1 -0
- package/applicationBuildRunner.test.ts +5 -3
- package/applicationBuildRunner.ts +33 -6
- package/artifact/implicitRootLayout.test.ts +6 -11
- package/artifact/implicitRootLayout.ts +0 -15
- package/artifact/routeSeedIndex.test.ts +7 -6
- package/biome.base.json +6 -1
- package/capacitorApp.test.ts +28 -2
- package/capacitorApp.ts +25 -4
- package/cloud/constants.ts +23 -0
- package/cloud/globalConfig.ts +13 -0
- package/codeAgent/agent/AkanCodeServices.ts +38 -12
- package/codeAgent/agent/CodeAgent.ts +94 -30
- package/codeAgent/agent/CodeAgentAsks.ts +3 -1
- package/codeAgent/agent/CodeAgentEventMapper.ts +9 -1
- package/codeAgent/agent/CodeAgentProxy.ts +91 -0
- package/codeAgent/agent/CodeAgentRpcHost.ts +29 -9
- package/codeAgent/agent/CodeAgentRpcListener.ts +117 -0
- package/codeAgent/agent/CodeAgentSuspended.ts +92 -0
- package/codeAgent/agent/akanCodeModel.ts +1 -1
- package/codeAgent/agent/akanCodePaths.ts +23 -3
- package/codeAgent/agent/akanContextFiles.test.ts +2 -1
- package/codeAgent/agent/akanSystemPrompt.ts +5 -0
- package/codeAgent/agent/codeAgentContract.test.ts +17 -0
- package/codeAgent/agent/codeAgentProxy.test.ts +74 -0
- package/codeAgent/agent/codeAgentRpcListener.test.ts +113 -0
- package/codeAgent/agent/codeAgentSuspended.test.ts +47 -0
- package/codeAgent/skills/akan-code/SKILL.md +122 -0
- package/codeAgent/skills/akan-module/SKILL.md +2 -2
- package/codeAgent/skills/akan-store/SKILL.md +6 -2
- package/codeAgent/tools/AkanCodePlugins.ts +19 -3
- package/codeAgent/tools/AskToolPack.ts +111 -0
- package/codeAgent/tools/McpClient.ts +51 -5
- package/codeAgent/tools/McpOAuth.ts +268 -0
- package/codeAgent/tools/McpOAuthLoopback.ts +77 -0
- package/codeAgent/tools/McpServerConfig.ts +69 -34
- package/codeAgent/tools/McpSignIn.ts +161 -0
- package/codeAgent/tools/McpTokenStore.ts +92 -0
- package/codeAgent/tools/McpToolPack.ts +16 -3
- package/codeAgent/tools/askToolPack.test.ts +119 -0
- package/codeAgent/tools/mcpOAuth.test.ts +324 -0
- package/codeAgent/tools/mcpServerConfig.test.ts +106 -12
- package/commandDecorators/command.ts +2 -0
- package/commandDecorators/commandDecorators.test.ts +1 -1
- package/commandDecorators/helpFormatter.ts +2 -1
- package/dependencyScanner.ts +4 -2
- package/devkitUtils.test.ts +1 -1
- package/executors.linkFallback.test.ts +103 -0
- package/executors.test.ts +18 -46
- package/executors.ts +31 -8
- package/frontendBuild/autoImportSync.test.ts +13 -0
- package/frontendBuild/autoImportSync.ts +19 -1
- package/frontendBuild/buildRouteClient.test.ts +52 -1
- package/frontendBuild/cssImportResolver.ts +1 -1
- package/frontendBuild/fontOptimizer.test.ts +1 -1
- package/frontendBuild/frontendBuild.test.ts +19 -17
- package/frontendBuild/hmrWatcher.test.ts +1 -1
- package/frontendBuild/routeClientBuilder.ts +27 -10
- package/frontendBuild/sourceMtimeIndex.test.ts +6 -5
- package/getDirname.test.ts +4 -2
- package/incrementalBuilder/devWatchBatch.test.ts +7 -6
- package/incrementalBuilder/incrementalBuilder.proc.ts +1 -0
- package/index.ts +0 -1
- package/integration/devStabilityHarness.test.ts +7 -2
- package/lint/gritRules.test.ts +2 -1
- package/lint/no-deep-internal-import.grit +2 -2
- package/lint/no-redeclare-predefined-endpoint.grit +2 -1
- package/linter.ts +4 -1
- package/package.json +2 -2
- package/platformTest/BunTestReport.test.ts +60 -0
- package/platformTest/BunTestReport.ts +65 -0
- package/platformTest/IdleWatchedProcess.test.ts +64 -0
- package/platformTest/IdleWatchedProcess.ts +77 -0
- package/platformTest/LinuxDockerTarget.ts +120 -0
- package/platformTest/PlatformTestRun.test.ts +68 -0
- package/platformTest/PlatformTestRun.ts +141 -0
- package/platformTest/PlatformTestTarget.ts +120 -0
- package/platformTest/TestSnapshot.test.ts +80 -0
- package/platformTest/TestSnapshot.ts +107 -0
- package/platformTest/WindowsSshTarget.ts +167 -0
- package/prompter.ts +0 -7
- package/qualityScanner.test.ts +3 -2
- package/qualityScanner.ts +1 -1
- package/routeSourceValidator.test.ts +0 -1
- package/routeSourceValidator.ts +0 -12
- package/scanInfo.ts +5 -2
- package/subspace.test.ts +5 -2
- package/subspace.ts +19 -8
- package/workflow/executor.ts +52 -0
- package/workflow/rolloutGate.test.ts +1 -1
- package/workflow/source.ts +19 -13
- package/guideline.ts +0 -19
package/abstractDoc.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import path from "node:path";
|
|
1
2
|
import type { SysExecutor } from "./executors";
|
|
2
3
|
|
|
3
4
|
export type AbstractKind = "domain" | "service" | "scalar" | "other";
|
|
@@ -5,10 +6,8 @@ export type AbstractKind = "domain" | "service" | "scalar" | "other";
|
|
|
5
6
|
/** One `*.abstract.md` file: the agent-facing summary a module carries next to its source. */
|
|
6
7
|
export class AbstractDoc {
|
|
7
8
|
static readonly suffix = ".abstract.md";
|
|
8
|
-
/** `akan quality` warns above this
|
|
9
|
+
/** `akan quality` warns above this. */
|
|
9
10
|
static readonly maxLines = 300;
|
|
10
|
-
/** `akan compact` floor, and the line budget it compacts down to, so a compacted file is never a candidate again. */
|
|
11
|
-
static readonly compactMinLines = 40;
|
|
12
11
|
// Abstracts only live in facet folders; globbing the sys root instead would walk node_modules.
|
|
13
12
|
static readonly #facetRoots = "{lib,ui,webkit,srvkit,common,plugin}";
|
|
14
13
|
|
|
@@ -27,7 +26,9 @@ export class AbstractDoc {
|
|
|
27
26
|
return folder.startsWith("_") ? "service" : "domain";
|
|
28
27
|
}
|
|
29
28
|
static async findAll(sys: SysExecutor, { module }: { module?: string | null } = {}) {
|
|
30
|
-
const filePaths = (await sys.getAllFiles(`${AbstractDoc.#facetRoots}/**/*${AbstractDoc.suffix}`))
|
|
29
|
+
const filePaths = (await sys.getAllFiles(`${AbstractDoc.#facetRoots}/**/*${AbstractDoc.suffix}`))
|
|
30
|
+
.map((filePath) => filePath.split(path.sep).join("/"))
|
|
31
|
+
.sort();
|
|
31
32
|
const docs = await Promise.all(filePaths.map((filePath) => AbstractDoc.read(sys, filePath)));
|
|
32
33
|
if (!module) return docs;
|
|
33
34
|
return docs.filter((doc) => doc.moduleName === module || doc.folderName === module);
|
package/akanApp/akanApp.host.ts
CHANGED
|
@@ -621,28 +621,37 @@ export class AkanAppHost {
|
|
|
621
621
|
);
|
|
622
622
|
}
|
|
623
623
|
/**
|
|
624
|
-
* Another process's RSS, read from the OS rather than asked of the process.
|
|
625
|
-
* `ps` otherwise (macOS has no `/proc`). Null when it cannot be read, which callers treat
|
|
626
|
-
* "no new information" rather than as zero.
|
|
624
|
+
* Another process's RSS, read from the OS rather than asked of the process. `tasklist` on Windows, `/proc`
|
|
625
|
+
* where it exists, `ps` otherwise (macOS has no `/proc`). Null when it cannot be read, which callers treat
|
|
626
|
+
* as "no new information" rather than as zero.
|
|
627
627
|
*/
|
|
628
628
|
static async readProcessRssBytes(pid: number): Promise<number | null> {
|
|
629
|
+
if (process.platform === "win32")
|
|
630
|
+
return await AkanAppHost.#readRssKbVia(
|
|
631
|
+
["tasklist", "/FI", `PID eq ${pid}`, "/FO", "CSV", "/NH"],
|
|
632
|
+
AkanAppHost.#tasklistRssKb,
|
|
633
|
+
);
|
|
629
634
|
const status = await Bun.file(`/proc/${pid}/status`)
|
|
630
635
|
.text()
|
|
631
636
|
.catch(() => null);
|
|
632
637
|
const vmRssKb = status === null ? null : /VmRSS:\s+(\d+) kB/.exec(status)?.[1];
|
|
633
638
|
if (vmRssKb) return Number(vmRssKb) * 1024;
|
|
634
|
-
return await AkanAppHost.#
|
|
639
|
+
return await AkanAppHost.#readRssKbVia(["ps", "-o", "rss=", "-p", String(pid)], (output) => Number(output.trim()));
|
|
635
640
|
}
|
|
636
641
|
/**
|
|
637
|
-
* `ps`, bounded. An absent
|
|
638
|
-
* information" — but a `ps` that never answers was not: the only caller awaits it after a 20s settle,
|
|
642
|
+
* `ps` or `tasklist`, bounded. An absent tool is already handled — it answers `null`, which callers read as
|
|
643
|
+
* "no new information" — but a `ps` that never answers was not: the only caller awaits it after a 20s settle,
|
|
639
644
|
* so the recycle it was about to commit simply never happened, silently. The harness has hit exactly
|
|
640
645
|
* this hang while shelling out to `ps` under load.
|
|
641
646
|
*/
|
|
642
|
-
static async #
|
|
647
|
+
static async #readRssKbVia(
|
|
648
|
+
command: string[],
|
|
649
|
+
parseKb: (output: string) => number,
|
|
650
|
+
timeoutMs = PS_RSS_TIMEOUT_MS,
|
|
651
|
+
): Promise<number | null> {
|
|
643
652
|
let proc: Bun.Subprocess<"ignore", "pipe", "ignore">;
|
|
644
653
|
try {
|
|
645
|
-
proc = Bun.spawn(
|
|
654
|
+
proc = Bun.spawn(command, { stdio: ["ignore", "pipe", "ignore"] });
|
|
646
655
|
} catch {
|
|
647
656
|
return null;
|
|
648
657
|
}
|
|
@@ -650,7 +659,7 @@ export class AkanAppHost {
|
|
|
650
659
|
try {
|
|
651
660
|
const output = await new Response(proc.stdout).text();
|
|
652
661
|
await proc.exited;
|
|
653
|
-
const rssKb =
|
|
662
|
+
const rssKb = parseKb(output);
|
|
654
663
|
return Number.isFinite(rssKb) && rssKb > 0 ? rssKb * 1024 : null;
|
|
655
664
|
} catch {
|
|
656
665
|
return null;
|
|
@@ -658,6 +667,11 @@ export class AkanAppHost {
|
|
|
658
667
|
clearTimeout(killer);
|
|
659
668
|
}
|
|
660
669
|
}
|
|
670
|
+
/** `Mem Usage` is the working set — what `process.memoryUsage.rss()` reports on Windows — in locale-grouped KB. */
|
|
671
|
+
static #tasklistRssKb(output: string): number {
|
|
672
|
+
const memUsage = /"([^"]*)"\s*$/m.exec(output)?.[1];
|
|
673
|
+
return memUsage ? Number(memUsage.replace(/\D/g, "")) : Number.NaN;
|
|
674
|
+
}
|
|
661
675
|
#cancelRssRecycle(): void {
|
|
662
676
|
this.#rssRecycleReason = null;
|
|
663
677
|
this.#rssRecycleOver = null;
|
|
@@ -249,31 +249,65 @@ describe("AkanAppConfig", () => {
|
|
|
249
249
|
...akanPackageJson.dependencies,
|
|
250
250
|
...akanPackageJson.peerDependencies,
|
|
251
251
|
};
|
|
252
|
-
const
|
|
253
|
-
|
|
254
|
-
const
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
expect(multipleConfig
|
|
263
|
-
"@libsql/client": runtimeDependencies["@libsql/client"],
|
|
252
|
+
const configOf = (database: { modes: ("single" | "multiple" | "cluster")[] }) =>
|
|
253
|
+
new AkanAppConfig(app, [], packageJson, { database }, baseDevEnv);
|
|
254
|
+
const dependenciesOf = (config: AkanAppConfig) => config.getProductionPackageJson().dependencies ?? {};
|
|
255
|
+
const singleConfig = configOf({ modes: ["single"] });
|
|
256
|
+
const multipleConfig = configOf({ modes: ["multiple"] });
|
|
257
|
+
const clusterConfig = configOf({ modes: ["cluster"] });
|
|
258
|
+
const edgeAndCloud = configOf({ modes: ["single", "cluster"] });
|
|
259
|
+
|
|
260
|
+
for (const driver of ["ioredis", "bullmq", "postgres", "@libsql/client", "protobufjs"])
|
|
261
|
+
expect(dependenciesOf(singleConfig)).not.toHaveProperty(driver);
|
|
262
|
+
expect(dependenciesOf(multipleConfig)).toMatchObject({
|
|
264
263
|
bullmq: runtimeDependencies.bullmq,
|
|
265
264
|
ioredis: runtimeDependencies.ioredis,
|
|
266
|
-
protobufjs: runtimeDependencies.protobufjs,
|
|
267
265
|
});
|
|
268
|
-
expect(
|
|
269
|
-
|
|
270
|
-
expect(clusterConfig.getProductionPackageJson().dependencies).toMatchObject({
|
|
266
|
+
expect(dependenciesOf(clusterConfig)).toMatchObject({
|
|
271
267
|
bullmq: runtimeDependencies.bullmq,
|
|
272
268
|
ioredis: runtimeDependencies.ioredis,
|
|
273
269
|
postgres: runtimeDependencies.postgres,
|
|
274
|
-
protobufjs: runtimeDependencies.protobufjs,
|
|
275
270
|
});
|
|
276
|
-
|
|
271
|
+
// multiple opens the same SQLite as single and Redis replaced protobuf on the wire, so neither ships by default.
|
|
272
|
+
for (const config of [multipleConfig, clusterConfig])
|
|
273
|
+
for (const driver of ["@libsql/client", "protobufjs"]) expect(dependenciesOf(config)).not.toHaveProperty(driver);
|
|
274
|
+
expect(dependenciesOf(multipleConfig)).not.toHaveProperty("postgres");
|
|
275
|
+
// One image for an edge site and a cloud cluster carries both modes' drivers, and says which it carries.
|
|
276
|
+
expect(dependenciesOf(edgeAndCloud)).toMatchObject({ postgres: runtimeDependencies.postgres });
|
|
277
|
+
expect(edgeAndCloud.dockerfile).toContain("ENV AKAN_DATABASE_MODES=single,cluster");
|
|
278
|
+
expect(singleConfig.dockerfile).toContain("ENV AKAN_DATABASE_MODES=single");
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
test("runs single when the app declares no mode", () => {
|
|
282
|
+
expect(new AkanAppConfig(app, [], packageJson, {}, baseDevEnv).database.modes).toEqual(["single"]);
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
test("refuses a mode name that is not one of the three, naming the file", () => {
|
|
286
|
+
expect(
|
|
287
|
+
() => new AkanAppConfig(app, [], packageJson, { database: { modes: ["clsuter" as "cluster"] } }, baseDevEnv),
|
|
288
|
+
).toThrow("database.modes in apps/portal/akan.config.ts");
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
test("resolves a command's mode within the declared ones", () => {
|
|
292
|
+
const previous = process.env.AKAN_DATABASE_MODE;
|
|
293
|
+
try {
|
|
294
|
+
const config = new AkanAppConfig(
|
|
295
|
+
app,
|
|
296
|
+
[],
|
|
297
|
+
packageJson,
|
|
298
|
+
{ database: { modes: ["single", "cluster"] } },
|
|
299
|
+
baseDevEnv,
|
|
300
|
+
);
|
|
301
|
+
delete process.env.AKAN_DATABASE_MODE;
|
|
302
|
+
expect(config.resolveDatabaseMode()).toBe("single");
|
|
303
|
+
process.env.AKAN_DATABASE_MODE = "cluster";
|
|
304
|
+
expect(config.resolveDatabaseMode()).toBe("cluster");
|
|
305
|
+
process.env.AKAN_DATABASE_MODE = "multiple";
|
|
306
|
+
expect(() => config.resolveDatabaseMode()).toThrow('Add "multiple" to database.modes');
|
|
307
|
+
} finally {
|
|
308
|
+
if (previous === undefined) delete process.env.AKAN_DATABASE_MODE;
|
|
309
|
+
else process.env.AKAN_DATABASE_MODE = previous;
|
|
310
|
+
}
|
|
277
311
|
});
|
|
278
312
|
|
|
279
313
|
test("resolves database mode runtime packages and missing install specs", () => {
|
|
@@ -300,20 +334,11 @@ describe("AkanAppConfig", () => {
|
|
|
300
334
|
);
|
|
301
335
|
|
|
302
336
|
expect(config.getDatabaseModeRuntimePackages("single")).toEqual([]);
|
|
303
|
-
expect(config.getDatabaseModeRuntimePackages("multiple")).toEqual([
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
"ioredis",
|
|
307
|
-
"protobufjs",
|
|
308
|
-
]);
|
|
309
|
-
expect(config.getDatabaseModeRuntimePackages("cluster")).toEqual(["bullmq", "ioredis", "postgres", "protobufjs"]);
|
|
310
|
-
expect(config.getMissingDatabaseModeDependencySpecs("multiple")).toEqual([
|
|
311
|
-
`@libsql/client@${runtimeDependencies["@libsql/client"]}`,
|
|
312
|
-
`protobufjs@${runtimeDependencies.protobufjs}`,
|
|
313
|
-
]);
|
|
337
|
+
expect(config.getDatabaseModeRuntimePackages("multiple")).toEqual(["bullmq", "ioredis"]);
|
|
338
|
+
expect(config.getDatabaseModeRuntimePackages("cluster")).toEqual(["bullmq", "ioredis", "postgres"]);
|
|
339
|
+
expect(config.getMissingDatabaseModeDependencySpecs("multiple")).toEqual([]);
|
|
314
340
|
expect(config.getMissingDatabaseModeDependencySpecs("cluster")).toEqual([
|
|
315
341
|
`postgres@${runtimeDependencies.postgres}`,
|
|
316
|
-
`protobufjs@${runtimeDependencies.protobufjs}`,
|
|
317
342
|
]);
|
|
318
343
|
// The workspace-root install covers the toolchain/runtime plus every app Capacitor plugin
|
|
319
344
|
// (deduped — "@capacitor/core" appears in both source lists).
|
package/akanConfig/akanConfig.ts
CHANGED
|
@@ -2,7 +2,7 @@ import fs from "node:fs";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
4
|
import type { AkanPlugin } from "akanjs";
|
|
5
|
-
import { normalizeRoutePrefix } from "akanjs/base";
|
|
5
|
+
import { DatabaseModes, normalizeRoutePrefix } from "akanjs/base";
|
|
6
6
|
import { type AkanI18nConfig, resolveAkanI18nConfig } from "akanjs/common";
|
|
7
7
|
import type { AkanImageConfig } from "akanjs/server";
|
|
8
8
|
import type { App, Lib } from "../commandDecorators";
|
|
@@ -11,6 +11,7 @@ import type { BaseDevEnv, PackageJson } from "../types";
|
|
|
11
11
|
import {
|
|
12
12
|
type AkanApiConfig,
|
|
13
13
|
type AkanAssetsConfig,
|
|
14
|
+
type AkanDatabaseConfig,
|
|
14
15
|
type AkanMobileConfig,
|
|
15
16
|
type AkanMobileTargetConfig,
|
|
16
17
|
type AkanRouteConfig,
|
|
@@ -93,16 +94,6 @@ const MOBILE_APP_CAPACITOR_PLUGINS = [
|
|
|
93
94
|
"@capacitor/push-notifications",
|
|
94
95
|
"capacitor-plugin-safe-area",
|
|
95
96
|
] as const;
|
|
96
|
-
const DATABASE_MODE_RUNTIME_PACKAGES = {
|
|
97
|
-
single: [],
|
|
98
|
-
multiple: ["@libsql/client", "bullmq", "ioredis", "protobufjs"],
|
|
99
|
-
cluster: ["bullmq", "ioredis", "postgres", "protobufjs"],
|
|
100
|
-
} satisfies Record<DatabaseMode, readonly string[]>;
|
|
101
|
-
const AKAN_RUNTIME_PACKAGES = new Set<string>([
|
|
102
|
-
...SSR_RUNTIME_PACKAGES,
|
|
103
|
-
...MOBILE_RUNTIME_PACKAGES,
|
|
104
|
-
...Object.values(DATABASE_MODE_RUNTIME_PACKAGES).flat(),
|
|
105
|
-
]);
|
|
106
97
|
const DEFAULT_AKAN_IMAGE_CONFIG: AkanImageConfig = {
|
|
107
98
|
deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
|
|
108
99
|
imageSizes: [32, 48, 64, 96, 128, 256, 384],
|
|
@@ -211,7 +202,7 @@ export class AkanAppConfig implements AppConfigResult {
|
|
|
211
202
|
docker: DockerConfig;
|
|
212
203
|
/** The Dockerfile `akan build` writes: the declared string verbatim, or one assembled from the parts. */
|
|
213
204
|
dockerfile: string;
|
|
214
|
-
|
|
205
|
+
database: AkanDatabaseConfig;
|
|
215
206
|
web: AkanWebConfig;
|
|
216
207
|
externalLibs: string[];
|
|
217
208
|
barrelImports: string[];
|
|
@@ -250,7 +241,7 @@ export class AkanAppConfig implements AppConfigResult {
|
|
|
250
241
|
this.baseDevEnv = baseDevEnv;
|
|
251
242
|
this.plugins = plugins;
|
|
252
243
|
this.#applyRoutes(config?.routes);
|
|
253
|
-
this.
|
|
244
|
+
this.database = AkanAppConfig.#database(app, config);
|
|
254
245
|
this.externalLibs = [...new Set([...(config?.externalLibs ?? []), ...libContributions.externalLibs])];
|
|
255
246
|
this.barrelImports = [
|
|
256
247
|
...DEFAULT_BARREL_IMPORTS,
|
|
@@ -458,6 +449,7 @@ ENV AKAN_PUBLIC_LOCALES=${this.i18n.locales.join(",")}
|
|
|
458
449
|
ENV AKAN_PUBLIC_API_PREFIX=${this.api.prefix}
|
|
459
450
|
ENV AKAN_PUBLIC_WS_PREFIX=${this.api.websocketPrefix}
|
|
460
451
|
ENV AKAN_PUBLIC_OPERATION_MODE=cloud
|
|
452
|
+
ENV AKAN_DATABASE_MODES=${this.database.modes.join(",")}
|
|
461
453
|
ENV AKAN_LOG_TO_FILE=0
|
|
462
454
|
${webEnvLines}
|
|
463
455
|
CMD [${command.map((c) => `"${c}"`).join(",")}]`;
|
|
@@ -526,14 +518,31 @@ CMD [${command.map((c) => `"${c}"`).join(",")}]`;
|
|
|
526
518
|
return akanPackageJson.dependencies?.[lib] ?? akanPackageJson.peerDependencies?.[lib];
|
|
527
519
|
}
|
|
528
520
|
#getProductionRuntimePackages() {
|
|
529
|
-
return [
|
|
521
|
+
return [
|
|
522
|
+
...this.externalLibs,
|
|
523
|
+
...SSR_RUNTIME_PACKAGES,
|
|
524
|
+
...this.database.modes.flatMap((mode) => this.getDatabaseModeRuntimePackages(mode)),
|
|
525
|
+
];
|
|
530
526
|
}
|
|
531
|
-
getDatabaseModeRuntimePackages(databaseMode: DatabaseMode
|
|
532
|
-
return [...
|
|
527
|
+
getDatabaseModeRuntimePackages(databaseMode: DatabaseMode) {
|
|
528
|
+
return [...DatabaseModes.drivers[databaseMode]];
|
|
533
529
|
}
|
|
534
|
-
getMissingDatabaseModeDependencySpecs(databaseMode: DatabaseMode
|
|
530
|
+
getMissingDatabaseModeDependencySpecs(databaseMode: DatabaseMode) {
|
|
535
531
|
return this.#getMissingDependencySpecs(this.getDatabaseModeRuntimePackages(databaseMode));
|
|
536
532
|
}
|
|
533
|
+
/** The mode a CLI command runs this app in: the shell's `AKAN_DATABASE_MODE` if the app declares it, else the first. */
|
|
534
|
+
resolveDatabaseMode() {
|
|
535
|
+
return DatabaseModes.resolve({
|
|
536
|
+
requested: process.env.AKAN_DATABASE_MODE,
|
|
537
|
+
declared: this.database.modes.join(","),
|
|
538
|
+
local: true,
|
|
539
|
+
});
|
|
540
|
+
}
|
|
541
|
+
static #database(app: App, config: AppConfigDeclaration): AkanDatabaseConfig {
|
|
542
|
+
const declared = config?.database?.modes?.filter((mode): mode is DatabaseMode => !!mode);
|
|
543
|
+
const modes = declared?.length ? declared : ["single"];
|
|
544
|
+
return { modes: DatabaseModes.parseList(modes.join(","), `database.modes in apps/${app.name}/akan.config.ts`) };
|
|
545
|
+
}
|
|
537
546
|
getMobileRuntimePackages() {
|
|
538
547
|
// The app Capacitor plugins are installed at the workspace root too, so bun can resolve the
|
|
539
548
|
// app's "*" declarations to a hoisted, peer-pinned version instead of fetching latest.
|
|
@@ -558,6 +567,9 @@ CMD [${command.map((c) => `"${c}"`).join(",")}]`;
|
|
|
558
567
|
return `${lib}@${version}`;
|
|
559
568
|
});
|
|
560
569
|
}
|
|
570
|
+
get akanVersion() {
|
|
571
|
+
return getAkanPackageJson().version;
|
|
572
|
+
}
|
|
561
573
|
getProductionPackageJson(data: Partial<PackageJson> = {}): PackageJson {
|
|
562
574
|
return {
|
|
563
575
|
name: this.app.name,
|
package/akanConfig/types.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
2
|
import { rm } from "node:fs/promises";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
3
5
|
import { AKAN_BACKEND_MINIFY, AKAN_OPTIONAL_BACKEND_EXTERNALS } from "./applicationBuildRunner";
|
|
4
6
|
|
|
5
7
|
describe("ApplicationBuildRunner", () => {
|
|
@@ -16,8 +18,8 @@ describe("ApplicationBuildRunner", () => {
|
|
|
16
18
|
});
|
|
17
19
|
|
|
18
20
|
test("a bundler that mangles identifiers loses the class name a Logger reads", async () => {
|
|
19
|
-
const dir =
|
|
20
|
-
const entry =
|
|
21
|
+
const dir = path.join(os.tmpdir(), `akan-minify-${Bun.randomUUIDv7()}`);
|
|
22
|
+
const entry = path.join(dir, "entry.ts");
|
|
21
23
|
await Bun.write(
|
|
22
24
|
entry,
|
|
23
25
|
`const serve = () => class Service { readonly name = this.constructor.name; };
|
|
@@ -26,7 +28,7 @@ console.info(new SampleService().name);
|
|
|
26
28
|
`,
|
|
27
29
|
);
|
|
28
30
|
const build = async (minify: Bun.BuildConfig["minify"]) => {
|
|
29
|
-
const result = await Bun.build({ entrypoints: [entry], target: "bun", minify, outdir:
|
|
31
|
+
const result = await Bun.build({ entrypoints: [entry], target: "bun", minify, outdir: path.join(dir, "out") });
|
|
30
32
|
expect(result.success).toBe(true);
|
|
31
33
|
const proc = Bun.spawn([process.execPath, result.outputs[0].path], { stdout: "pipe" });
|
|
32
34
|
return (await new Response(proc.stdout).text()).trim();
|
|
@@ -191,9 +191,29 @@ export class ApplicationBuildRunner {
|
|
|
191
191
|
await Promise.all([
|
|
192
192
|
this.#app.dist.writeJson("package.json", akanConfig.getProductionPackageJson()),
|
|
193
193
|
this.#app.dist.writeFile(`${this.#app.dist.cwdPath}/Dockerfile`, akanConfig.dockerfile),
|
|
194
|
+
this.#app.dist.writeJson("akan.build.json", {
|
|
195
|
+
buildId: await this.#resolveBuildId(),
|
|
196
|
+
akanVersion: akanConfig.akanVersion,
|
|
197
|
+
builtAt: new Date().toISOString(),
|
|
198
|
+
}),
|
|
194
199
|
]);
|
|
195
200
|
}
|
|
196
201
|
|
|
202
|
+
//* Read back at runtime as `buildId` on the ops channel; a deployment's own AKAN_BUILD_ID still wins over it there.
|
|
203
|
+
async #resolveBuildId() {
|
|
204
|
+
const fromEnv = process.env.AKAN_BUILD_ID?.trim();
|
|
205
|
+
if (fromEnv) return fromEnv;
|
|
206
|
+
const root = this.#app.workspace.workspaceRoot;
|
|
207
|
+
const sha = Bun.spawnSync(["git", "rev-parse", "--short=12", "HEAD"], { cwd: root, stderr: "ignore" });
|
|
208
|
+
if (sha.exitCode !== 0) return null;
|
|
209
|
+
const dirty = Bun.spawnSync(["git", "status", "--porcelain", "--untracked-files=no"], {
|
|
210
|
+
cwd: root,
|
|
211
|
+
stderr: "ignore",
|
|
212
|
+
});
|
|
213
|
+
const suffix = dirty.exitCode === 0 && dirty.stdout.toString().trim() ? "-dirty" : "";
|
|
214
|
+
return `${sha.stdout.toString().trim()}${suffix}`;
|
|
215
|
+
}
|
|
216
|
+
|
|
197
217
|
async #buildBackend() {
|
|
198
218
|
const akanConfig = await this.#app.getConfig();
|
|
199
219
|
const backendExternals = [
|
|
@@ -204,15 +224,21 @@ export class ApplicationBuildRunner {
|
|
|
204
224
|
for (const entrypoint of backendEntryPoints) {
|
|
205
225
|
if (!(await Bun.file(entrypoint).exists())) throw new Error(`Backend entrypoint not found: ${entrypoint}`);
|
|
206
226
|
}
|
|
207
|
-
const
|
|
208
|
-
entrypoints: backendEntryPoints,
|
|
227
|
+
const backendConfig = {
|
|
209
228
|
outdir: this.#app.dist.cwdPath,
|
|
210
229
|
target: "bun",
|
|
211
230
|
minify: AKAN_BACKEND_MINIFY,
|
|
212
231
|
naming: { entry: "[name].[ext]", chunk: "chunk-[hash].[ext]" },
|
|
213
232
|
define: { "process.env.NODE_ENV": JSON.stringify("production") },
|
|
214
233
|
plugins: backendExternals.length > 0 ? [this.#createExternalSpecifiersPlugin(backendExternals)] : [],
|
|
215
|
-
}
|
|
234
|
+
} satisfies Omit<Bun.BuildConfig, "entrypoints">;
|
|
235
|
+
//* Built apart so main.js keeps its own module copies; splitting moves lazy vendor `import()`s out of the boot
|
|
236
|
+
//* parse (minimal: server.js import 79ms → 24ms, 59MB → 36MB RSS).
|
|
237
|
+
const [mainResult, serverResult] = [
|
|
238
|
+
await this.#buildOrThrow("backend", { ...backendConfig, entrypoints: [backendEntryPoints[0]] }),
|
|
239
|
+
await this.#buildOrThrow("backend", { ...backendConfig, entrypoints: [backendEntryPoints[1]], splitting: true }),
|
|
240
|
+
];
|
|
241
|
+
const backendResult = { outputs: [...mainResult.outputs, ...serverResult.outputs] };
|
|
216
242
|
// Nothing spawns the RSC worker without SSR, so an api-only image does not carry it.
|
|
217
243
|
const rscWorkerResult = web.ssr
|
|
218
244
|
? await this.#buildOrThrow("rsc-worker", {
|
|
@@ -246,11 +272,12 @@ export class ApplicationBuildRunner {
|
|
|
246
272
|
async #writeConsoleShim() {
|
|
247
273
|
await Bun.write(
|
|
248
274
|
path.join(this.#app.dist.cwdPath, "console.js"),
|
|
249
|
-
`
|
|
250
|
-
|
|
275
|
+
`process.env.AKAN_COMMAND_TYPE = "console";
|
|
276
|
+
const { cnst, db, dict, option, server, sig, srv } = await import("./server.js");
|
|
277
|
+
const { assertAkanConsoleAllowed, startAkanConsole } = await import("./console-runtime.js");
|
|
251
278
|
|
|
252
279
|
const run = async () => {
|
|
253
|
-
assertAkanConsoleAllowed(
|
|
280
|
+
assertAkanConsoleAllowed();
|
|
254
281
|
await server.start({ listen: false, web: false });
|
|
255
282
|
try {
|
|
256
283
|
await startAkanConsole(server, { globals: { cnst, db, dict, option, sig, srv } });
|
|
@@ -31,11 +31,11 @@ describe("resolveSsrPageEntries", () => {
|
|
|
31
31
|
await write(path.join(appRoot, "env", "env.client.ts"), "export const env = {};\n");
|
|
32
32
|
await write(
|
|
33
33
|
rootLayoutPath,
|
|
34
|
-
'export const theme = "dark";\nexport const fonts = [{ name: "pretendard" }];\nexport const
|
|
34
|
+
'export const theme = "dark";\nexport const fonts = [{ name: "pretendard" }];\nexport const head = null;\n',
|
|
35
35
|
);
|
|
36
36
|
await write(
|
|
37
37
|
groupedLayoutPath,
|
|
38
|
-
|
|
38
|
+
"export function generateHead() { return null; }\nexport default function Layout({ children }) { return children; }\n",
|
|
39
39
|
);
|
|
40
40
|
|
|
41
41
|
const entries = await resolveSsrPageEntries({
|
|
@@ -54,15 +54,10 @@ describe("resolveSsrPageEntries", () => {
|
|
|
54
54
|
expect(generatedSource).toContain('import * as inheritedModule from "../../../page/_layout.tsx";');
|
|
55
55
|
expect(generatedSource).toContain("const inheritedLayout = resolveRouteModule(inheritedModule as never");
|
|
56
56
|
expect(generatedSource).not.toContain("<System.Provider");
|
|
57
|
-
expect(generatedSource).toContain("export async function
|
|
58
|
-
expect(generatedSource).toContain("if (userLayout.
|
|
59
|
-
expect(generatedSource).toContain("
|
|
60
|
-
expect(generatedSource).toContain(
|
|
61
|
-
"if (inheritedLayout.generateMetadata) return inheritedLayout.generateMetadata(props);",
|
|
62
|
-
);
|
|
63
|
-
expect(generatedSource).toContain("return inheritedLayout.metadata;");
|
|
64
|
-
expect(generatedSource).not.toContain("Object.keys(userLayout.metadata)");
|
|
65
|
-
expect(generatedSource).not.toContain("Object.keys(inheritedLayout.metadata)");
|
|
57
|
+
expect(generatedSource).toContain("export async function generateHead(props: PageProps)");
|
|
58
|
+
expect(generatedSource).toContain("if (userLayout.generateHead) return userLayout.generateHead(props);");
|
|
59
|
+
expect(generatedSource).toContain("return inheritedLayout.head;");
|
|
60
|
+
expect(generatedSource).not.toContain("generateMetadata");
|
|
66
61
|
expect(generatedSource).toContain("export const NotFound = userLayout.NotFound ?? inheritedLayout.NotFound;");
|
|
67
62
|
expect(generatedSource).toContain("export const Error = userLayout.Error ?? inheritedLayout.Error;");
|
|
68
63
|
expect(generatedSource).toContain("export const pageConfig = userLayout.pageConfig ?? inheritedLayout.pageConfig;");
|
|
@@ -236,13 +236,6 @@ export async function generateHead(props: PageProps) {
|
|
|
236
236
|
return inheritedLayout.head;
|
|
237
237
|
}
|
|
238
238
|
|
|
239
|
-
export async function generateMetadata(props: PageProps) {
|
|
240
|
-
if (userLayout.generateMetadata) return userLayout.generateMetadata(props);
|
|
241
|
-
if (userLayout.metadata !== undefined) return userLayout.metadata;
|
|
242
|
-
if (inheritedLayout.generateMetadata) return inheritedLayout.generateMetadata(props);
|
|
243
|
-
return inheritedLayout.metadata;
|
|
244
|
-
}
|
|
245
|
-
|
|
246
239
|
export const NotFound = userLayout.NotFound ?? inheritedLayout.NotFound;
|
|
247
240
|
export const Error = userLayout.Error ?? inheritedLayout.Error;
|
|
248
241
|
export const pageConfig = userLayout.pageConfig ?? inheritedLayout.pageConfig;
|
|
@@ -258,7 +251,6 @@ ${layoutBinding} return (
|
|
|
258
251
|
theme={userLayout.theme ?? inheritedLayout.theme}
|
|
259
252
|
fonts={loadFonts(userFonts)}
|
|
260
253
|
className={defaultFontClassName}
|
|
261
|
-
gaTrackingId={userLayout.gaTrackingId ?? inheritedLayout.gaTrackingId}
|
|
262
254
|
layoutStyle={userLayout.layoutStyle ?? inheritedLayout.layoutStyle}
|
|
263
255
|
reconnect={userLayout.reconnect ?? inheritedLayout.reconnect ?? false}
|
|
264
256
|
wsConnect={userLayout.wsConnect ?? inheritedLayout.wsConnect ?? true}
|
|
@@ -280,13 +272,6 @@ export async function generateHead(props: PageProps) {
|
|
|
280
272
|
return inheritedLayout.head;
|
|
281
273
|
}
|
|
282
274
|
|
|
283
|
-
export async function generateMetadata(props: PageProps) {
|
|
284
|
-
if (userLayout.generateMetadata) return userLayout.generateMetadata(props);
|
|
285
|
-
if (userLayout.metadata !== undefined) return userLayout.metadata;
|
|
286
|
-
if (inheritedLayout.generateMetadata) return inheritedLayout.generateMetadata(props);
|
|
287
|
-
return inheritedLayout.metadata;
|
|
288
|
-
}
|
|
289
|
-
|
|
290
275
|
export const NotFound = userLayout.NotFound ?? inheritedLayout.NotFound;
|
|
291
276
|
export const Error = userLayout.Error ?? inheritedLayout.Error;
|
|
292
277
|
export const pageConfig = userLayout.pageConfig ?? inheritedLayout.pageConfig;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import path from "node:path";
|
|
2
3
|
import { computeRouteSeedIndex, serializeRouteSeedIndexForArtifact } from "./routeSeedIndex";
|
|
3
4
|
|
|
4
5
|
describe("computeRouteSeedIndex", () => {
|
|
@@ -19,10 +20,10 @@ describe("computeRouteSeedIndex", () => {
|
|
|
19
20
|
]);
|
|
20
21
|
|
|
21
22
|
const leaf = index.entries.find((entry) => entry.routeId === "/:lang/foo");
|
|
22
|
-
expect(leaf?.seeds).toContain("/app/page/foo.tsx");
|
|
23
|
-
expect(leaf?.seeds).not.toContain("/app/page/foo/_layout.tsx");
|
|
24
|
-
expect(leaf?.seeds).toContain("/app/.akan/generated/root-layouts/__root_layout.tsx");
|
|
25
|
-
expect(leaf?.seeds).toContain("/app/page/_layout.tsx");
|
|
23
|
+
expect(leaf?.seeds).toContain(path.resolve("/app/page/foo.tsx"));
|
|
24
|
+
expect(leaf?.seeds).not.toContain(path.resolve("/app/page/foo/_layout.tsx"));
|
|
25
|
+
expect(leaf?.seeds).toContain(path.resolve("/app/.akan/generated/root-layouts/__root_layout.tsx"));
|
|
26
|
+
expect(leaf?.seeds).toContain(path.resolve("/app/page/_layout.tsx"));
|
|
26
27
|
|
|
27
28
|
expect(() =>
|
|
28
29
|
computeRouteSeedIndex([
|
|
@@ -42,8 +43,8 @@ describe("computeRouteSeedIndex", () => {
|
|
|
42
43
|
{ key: "./robots.txt.tsx", moduleAbsPath: "/app/page/robots.txt.tsx" },
|
|
43
44
|
]);
|
|
44
45
|
const robots = index.entries.find((entry) => entry.routeId === "/robots.txt");
|
|
45
|
-
expect(robots?.seeds).not.toContain("/app/.akan/generated/root-layouts/__root_layout.tsx");
|
|
46
|
-
expect(robots?.seeds).not.toContain("/app/page/_layout.tsx");
|
|
46
|
+
expect(robots?.seeds).not.toContain(path.resolve("/app/.akan/generated/root-layouts/__root_layout.tsx"));
|
|
47
|
+
expect(robots?.seeds).not.toContain(path.resolve("/app/page/_layout.tsx"));
|
|
47
48
|
});
|
|
48
49
|
|
|
49
50
|
test("serializes seed paths relative to the artifact directory", () => {
|
package/biome.base.json
CHANGED
|
@@ -154,7 +154,12 @@
|
|
|
154
154
|
]
|
|
155
155
|
},
|
|
156
156
|
{
|
|
157
|
-
"includes": [
|
|
157
|
+
"includes": [
|
|
158
|
+
"{apps,libs}/**/page/**/*.ts",
|
|
159
|
+
"{apps,libs}/**/page/**/*.tsx",
|
|
160
|
+
"{apps,libs}/**/*.Unit.tsx",
|
|
161
|
+
"{apps,libs}/**/*.View.tsx"
|
|
162
|
+
],
|
|
158
163
|
"plugins": ["./node_modules/@akanjs/devkit/lint/non-scalar-props-restricted.grit"]
|
|
159
164
|
},
|
|
160
165
|
{
|
package/capacitorApp.test.ts
CHANGED
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
sanitizeIosNativeRunEnv,
|
|
25
25
|
selectLocalDevHost,
|
|
26
26
|
sortIosRunTargets,
|
|
27
|
+
toIosInfoPlistUsageDescriptions,
|
|
27
28
|
writeRootCapacitorConfig,
|
|
28
29
|
} from "./capacitorApp";
|
|
29
30
|
|
|
@@ -234,7 +235,7 @@ describe("iOS native run helpers", () => {
|
|
|
234
235
|
expect(deviceCommand.xcodebuildArgs).toContain("id=device-1");
|
|
235
236
|
expect(deviceCommand.xcodebuildArgs).toContain("App QA");
|
|
236
237
|
expect(deviceCommand.appPath).toBe(
|
|
237
|
-
"/repo/apps/minimal/ios/DerivedData/device-1/Build/Products/Debug-iphoneos/App.app",
|
|
238
|
+
path.join("/repo/apps/minimal/ios/DerivedData/device-1/Build/Products/Debug-iphoneos/App.app"),
|
|
238
239
|
);
|
|
239
240
|
|
|
240
241
|
const simulatorCommand = buildIosNativeRunCommand({
|
|
@@ -243,7 +244,7 @@ describe("iOS native run helpers", () => {
|
|
|
243
244
|
});
|
|
244
245
|
expect(simulatorCommand.xcodebuildArgs).toContain("platform=iOS Simulator,id=sim-1");
|
|
245
246
|
expect(simulatorCommand.appPath).toBe(
|
|
246
|
-
"/repo/apps/minimal/ios/DerivedData/sim-1/Build/Products/Debug-iphonesimulator/App.app",
|
|
247
|
+
path.join("/repo/apps/minimal/ios/DerivedData/sim-1/Build/Products/Debug-iphonesimulator/App.app"),
|
|
247
248
|
);
|
|
248
249
|
});
|
|
249
250
|
|
|
@@ -370,3 +371,28 @@ describe("raiseGradleMinSdkVersion", () => {
|
|
|
370
371
|
expect(raiseGradleMinSdkVersion("minSdkVersion = 28", 26)).toBeNull();
|
|
371
372
|
});
|
|
372
373
|
});
|
|
374
|
+
|
|
375
|
+
describe("toIosInfoPlistUsageDescriptions", () => {
|
|
376
|
+
test("writes Apple's real Info.plist keys, not NS + the description name", () => {
|
|
377
|
+
expect(
|
|
378
|
+
toIosInfoPlistUsageDescriptions({
|
|
379
|
+
cameraUsageDescription: "camera",
|
|
380
|
+
photoAddUsageDescription: "add",
|
|
381
|
+
photoUsageDescription: "read",
|
|
382
|
+
locationAlwaysUsageDescription: "always",
|
|
383
|
+
}),
|
|
384
|
+
).toEqual({
|
|
385
|
+
NSCameraUsageDescription: "camera",
|
|
386
|
+
NSPhotoLibraryAddUsageDescription: "add",
|
|
387
|
+
NSPhotoLibraryUsageDescription: "read",
|
|
388
|
+
NSLocationAlwaysAndWhenInUseUsageDescription: "always",
|
|
389
|
+
NSLocationAlwaysUsageDescription: "always",
|
|
390
|
+
});
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
test("keeps the NS prefix convention for a key it does not know", () => {
|
|
394
|
+
expect(toIosInfoPlistUsageDescriptions({ bluetoothAlwaysUsageDescription: "ble" })).toEqual({
|
|
395
|
+
NSBluetoothAlwaysUsageDescription: "ble",
|
|
396
|
+
});
|
|
397
|
+
});
|
|
398
|
+
});
|
package/capacitorApp.ts
CHANGED
|
@@ -489,7 +489,7 @@ export function classifyIosRunFailure(log: string): IosRunFailureClassification
|
|
|
489
489
|
kind: "unknown",
|
|
490
490
|
title: "iOS native run failed.",
|
|
491
491
|
detail:
|
|
492
|
-
"Review the xcodebuild/devicectl output above. You can retry with --
|
|
492
|
+
"Review the xcodebuild/devicectl output above. You can retry with --no-allow-provisioning-updates to use the conservative path.",
|
|
493
493
|
};
|
|
494
494
|
}
|
|
495
495
|
|
|
@@ -597,6 +597,29 @@ export function raiseGradleMinSdkVersion(content: string, floor: number = ANDROI
|
|
|
597
597
|
return content.replace(/(minSdkVersion\s*=\s*)\d+/, `$1${floor}`);
|
|
598
598
|
}
|
|
599
599
|
|
|
600
|
+
//* Apple's Info.plist keys do not follow the description names (photo → NSPhotoLibrary…), so each one is spelled out.
|
|
601
|
+
const iosUsageDescriptionKeys = {
|
|
602
|
+
cameraUsageDescription: ["NSCameraUsageDescription"],
|
|
603
|
+
photoAddUsageDescription: ["NSPhotoLibraryAddUsageDescription"],
|
|
604
|
+
photoUsageDescription: ["NSPhotoLibraryUsageDescription"],
|
|
605
|
+
contactsUsageDescription: ["NSContactsUsageDescription"],
|
|
606
|
+
//? iOS 11+ reads the AlwaysAndWhenInUse key; the Always key only serves iOS 10 and below.
|
|
607
|
+
locationAlwaysUsageDescription: ["NSLocationAlwaysAndWhenInUseUsageDescription", "NSLocationAlwaysUsageDescription"],
|
|
608
|
+
locationWhenInUseUsageDescription: ["NSLocationWhenInUseUsageDescription"],
|
|
609
|
+
microphoneUsageDescription: ["NSMicrophoneUsageDescription"],
|
|
610
|
+
speechRecognitionUsageDescription: ["NSSpeechRecognitionUsageDescription"],
|
|
611
|
+
} as const;
|
|
612
|
+
|
|
613
|
+
export const toIosInfoPlistUsageDescriptions = (descriptions: { [key: string]: string }) =>
|
|
614
|
+
Object.fromEntries(
|
|
615
|
+
Object.entries(descriptions).flatMap(([key, value]) => {
|
|
616
|
+
const plistKeys: readonly string[] = Object.hasOwn(iosUsageDescriptionKeys, key)
|
|
617
|
+
? iosUsageDescriptionKeys[key as keyof typeof iosUsageDescriptionKeys]
|
|
618
|
+
: [`NS${capitalize(key)}`];
|
|
619
|
+
return plistKeys.map((plistKey) => [plistKey, value]);
|
|
620
|
+
}),
|
|
621
|
+
);
|
|
622
|
+
|
|
600
623
|
const mergeAllowNavigation = (configured: unknown, localIp: string | undefined) => {
|
|
601
624
|
const values = Array.isArray(configured)
|
|
602
625
|
? configured.filter((value): value is string => typeof value === "string")
|
|
@@ -1469,9 +1492,7 @@ export class CapacitorApp {
|
|
|
1469
1492
|
if (next !== content) await editor.setContent(next).save();
|
|
1470
1493
|
}
|
|
1471
1494
|
async #setPermissionInIos(permissions: { [key: string]: string }) {
|
|
1472
|
-
const updateNs =
|
|
1473
|
-
Object.entries(permissions).map(([key, value]) => [`NS${capitalize(key)}`, value]),
|
|
1474
|
-
);
|
|
1495
|
+
const updateNs = toIosInfoPlistUsageDescriptions(permissions);
|
|
1475
1496
|
await Promise.all([
|
|
1476
1497
|
this.project.ios.updateInfoPlist(this.iosTargetName, "Debug", updateNs),
|
|
1477
1498
|
this.project.ios.updateInfoPlist(this.iosTargetName, "Release", updateNs),
|