@akanjs/devkit 3.0.0-beta.29 → 3.0.0-beta.30
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 +109 -92
- package/akanConfig/akanConfig.ts +183 -163
- package/akanConfig/types.ts +12 -6
- package/executors.test.ts +4 -3
- package/executors.ts +1 -1
- package/frontendBuild/ssrBaseArtifactBuilder.ts +5 -5
- package/mobile/desktopBin.test.ts +1 -1
- package/mobile/desktopServerStage.ts +4 -4
- package/mobile/mobileTarget.test.ts +10 -10
- package/mobile/mobileTarget.ts +17 -17
- package/mobile/nativeApp.test.ts +7 -7
- package/mobile/nativeApp.ts +11 -11
- package/mobile/nativeConfig.test.ts +47 -44
- package/mobile/nativeConfig.ts +29 -32
- package/mobile/nativeWebDir.test.ts +3 -3
- package/mobile/nativeWebDir.ts +4 -4
- package/package.json +2 -2
- package/workspaceLayout.test.ts +1 -1
- package/workspaceLayout.ts +2 -2
package/akanConfig/akanConfig.ts
CHANGED
|
@@ -14,8 +14,12 @@ import {
|
|
|
14
14
|
type AkanAssetsConfig,
|
|
15
15
|
type AkanBinConfig,
|
|
16
16
|
type AkanDatabaseConfig,
|
|
17
|
-
type
|
|
18
|
-
type
|
|
17
|
+
type AkanNativeAppConfig,
|
|
18
|
+
type AkanNativeAppResult,
|
|
19
|
+
type AkanNativeDeepLinks,
|
|
20
|
+
type AkanNativeSettings,
|
|
21
|
+
type AkanNativeTarget,
|
|
22
|
+
type AkanNativeUpdatesConfig,
|
|
19
23
|
type AkanRouteConfig,
|
|
20
24
|
type AkanWebConfig,
|
|
21
25
|
type AkanWebOption,
|
|
@@ -94,25 +98,18 @@ const normalizeDeepLinkDomain = (domain: string) => {
|
|
|
94
98
|
}
|
|
95
99
|
};
|
|
96
100
|
|
|
97
|
-
const normalizeDeepLinks = (deepLinks:
|
|
98
|
-
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
const sha256CertFingerprints = normalizeStringList(deepLinks.android?.sha256CertFingerprints as string[] | undefined);
|
|
103
|
-
if (!schemes && !domains && !teamId && !sha256CertFingerprints) return undefined;
|
|
104
|
-
return {
|
|
105
|
-
...(schemes ? { schemes } : {}),
|
|
106
|
-
...(domains ? { domains } : {}),
|
|
107
|
-
...(teamId ? { ios: { teamId } } : {}),
|
|
108
|
-
...(sha256CertFingerprints ? { android: { sha256CertFingerprints } } : {}),
|
|
109
|
-
} satisfies AkanMobileTargetConfig["deepLinks"];
|
|
101
|
+
const normalizeDeepLinks = (deepLinks: AkanNativeDeepLinks | undefined): AkanNativeDeepLinks | undefined => {
|
|
102
|
+
const schemes = normalizeStringList(deepLinks?.schemes);
|
|
103
|
+
const domains = normalizeStringList(deepLinks?.domains?.map(normalizeDeepLinkDomain));
|
|
104
|
+
if (!schemes && !domains) return undefined;
|
|
105
|
+
return { ...(schemes ? { schemes } : {}), ...(domains ? { domains } : {}) };
|
|
110
106
|
};
|
|
111
107
|
|
|
112
|
-
type AppConfigDeclaration = Omit<DeepPartial<AppConfigResult>, "docker" | "web" | "bin"> & {
|
|
108
|
+
type AppConfigDeclaration = Omit<DeepPartial<AppConfigResult>, "docker" | "web" | "bin" | "native"> & {
|
|
113
109
|
docker?: DockerOption;
|
|
114
110
|
web?: AkanWebOption;
|
|
115
111
|
bin?: AkanBinConfig;
|
|
112
|
+
native?: AkanNativeAppConfig;
|
|
116
113
|
};
|
|
117
114
|
|
|
118
115
|
type LibConfigDeclaration = Omit<DeepPartial<LibConfigResult>, "bin"> & { bin?: AkanBinConfig };
|
|
@@ -172,9 +169,9 @@ export class AkanAppConfig implements AppConfigResult {
|
|
|
172
169
|
i18n: AkanI18nConfig;
|
|
173
170
|
api: AkanApiConfig;
|
|
174
171
|
publicEnv: string[];
|
|
175
|
-
|
|
176
|
-
/** True only when the app's akan.config.ts
|
|
177
|
-
|
|
172
|
+
native: AkanNativeAppResult;
|
|
173
|
+
/** True only when the app's akan.config.ts declares a `native` section (vs. the synthesized default target). */
|
|
174
|
+
hasNativeConfig: boolean;
|
|
178
175
|
secrets: string[];
|
|
179
176
|
assets: AkanAssetsConfig;
|
|
180
177
|
/** Raw setting, resolved against the app's lib deps at sync time. */
|
|
@@ -240,8 +237,12 @@ export class AkanAppConfig implements AppConfigResult {
|
|
|
240
237
|
],
|
|
241
238
|
};
|
|
242
239
|
this.syncPageLibs = (config?.syncPageLibs as string[] | boolean | undefined) ?? false;
|
|
243
|
-
|
|
244
|
-
|
|
240
|
+
if ((config as { mobile?: unknown }).mobile !== undefined)
|
|
241
|
+
throw new Error(
|
|
242
|
+
`apps/${this.app.name}/akan.config.ts declares \`mobile\`, which is now \`native\`: the platform settings sit in native.ios, native.android and native.desktop, and a target overrides them under native.targets.<name>.`,
|
|
243
|
+
);
|
|
244
|
+
this.hasNativeConfig = Boolean(config.native);
|
|
245
|
+
this.native = this.#resolveNativeConfig(config.native);
|
|
245
246
|
this.web = this.#resolveWebConfig(config.web);
|
|
246
247
|
this.docker = AkanAppConfig.#resolveDocker(config.docker, libContributions.docker);
|
|
247
248
|
this.dockerfile = this.#makeDockerfile();
|
|
@@ -249,166 +250,185 @@ export class AkanAppConfig implements AppConfigResult {
|
|
|
249
250
|
#resolveWebConfig(web: AkanWebOption | undefined): AkanWebConfig {
|
|
250
251
|
const resolved = typeof web === "object" ? { ssr: true, csr: web.csr } : { ssr: web ?? true, csr: web ?? true };
|
|
251
252
|
// `akan build-ios` / `build-android` copy `dist/apps/<app>/csr/<target>.html` into the native project.
|
|
252
|
-
if (!resolved.csr && this.
|
|
253
|
+
if (!resolved.csr && this.hasNativeConfig)
|
|
253
254
|
throw new Error(
|
|
254
|
-
`apps/${this.app.name}/akan.config.ts turns the CSR bundle off but declares
|
|
255
|
+
`apps/${this.app.name}/akan.config.ts turns the CSR bundle off but declares a native section; the native apps ship that bundle. Drop the native section or leave CSR on.`,
|
|
255
256
|
);
|
|
256
257
|
return resolved;
|
|
257
258
|
}
|
|
258
|
-
#
|
|
259
|
-
const {
|
|
260
|
-
targets: rawTargets,
|
|
261
|
-
indexPath: _indexPath,
|
|
262
|
-
...rawMobile
|
|
263
|
-
} = (mobile ?? {}) as DeepPartial<AkanMobileConfig> & { indexPath?: unknown };
|
|
259
|
+
#resolveNativeConfig(native: AkanNativeAppConfig | undefined): AkanNativeAppResult {
|
|
264
260
|
const configPath = `apps/${this.app.name}/akan.config.ts`;
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
const
|
|
269
|
-
const
|
|
270
|
-
const
|
|
271
|
-
const
|
|
272
|
-
const
|
|
273
|
-
const
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
[defaultTargetName]: {},
|
|
277
|
-
},
|
|
278
|
-
);
|
|
261
|
+
const { targets: rawTargets, ...root } = native ?? {};
|
|
262
|
+
AkanAppConfig.#assertNativeKeys(root, "native", configPath);
|
|
263
|
+
AkanAppConfig.#assertFiles(root, "native", configPath);
|
|
264
|
+
const appName = root.appName ?? this.app.name;
|
|
265
|
+
const appId = root.appId ?? deriveDefaultAppId(this.baseDevEnv.repoName, this.app.name);
|
|
266
|
+
const version = root.version ?? "0.0.1";
|
|
267
|
+
const buildNum = root.buildNum ?? 1;
|
|
268
|
+
const named = rawTargets !== undefined && Object.keys(rawTargets).length > 0;
|
|
269
|
+
const entries: [string, AkanNativeSettings][] = named
|
|
270
|
+
? Object.entries(rawTargets)
|
|
271
|
+
: [[this.basePaths.has(this.app.name) ? this.app.name : "default", {}]];
|
|
279
272
|
const targets = Object.fromEntries(
|
|
280
|
-
|
|
281
|
-
const
|
|
282
|
-
|
|
283
|
-
AkanAppConfig.#
|
|
284
|
-
const fallbackBasePath = !
|
|
285
|
-
const basePath = (
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
}
|
|
291
|
-
const targetFiles = AkanAppConfig.#resolveMobileFiles(
|
|
292
|
-
target.files as AkanMobileTargetConfig["files"],
|
|
293
|
-
where,
|
|
294
|
-
configPath,
|
|
295
|
-
);
|
|
296
|
-
const fileName = (target.fileName ?? rawMobile.fileName) as string | undefined;
|
|
297
|
-
const targetUpdates = updates || target.updates ? { ...updates, ...target.updates } : undefined;
|
|
298
|
-
if (targetUpdates && (!targetUpdates.url || !targetUpdates.publicKey))
|
|
273
|
+
entries.map(([name, own]) => {
|
|
274
|
+
const where = named ? `native.targets.${name}` : "native";
|
|
275
|
+
AkanAppConfig.#assertNativeKeys(own ?? {}, where, configPath);
|
|
276
|
+
const merged = AkanAppConfig.#overlay(root, own ?? {});
|
|
277
|
+
const fallbackBasePath = !named && this.basePaths.has(name) ? name : undefined;
|
|
278
|
+
const basePath = (merged.basePath ?? fallbackBasePath)?.replace(/^\/+|\/+$/g, "") || undefined;
|
|
279
|
+
if (basePath && !this.basePaths.has(basePath))
|
|
280
|
+
throw new Error(`Native target '${name}' uses unknown basePath '${basePath}' in ${configPath}`);
|
|
281
|
+
const { updates } = merged;
|
|
282
|
+
if (updates && (!updates.url || !updates.publicKey))
|
|
299
283
|
throw new Error(
|
|
300
|
-
`${where}.updates in ${configPath} has no url or publicKey; give them in
|
|
284
|
+
`${where}.updates in ${configPath} has no url or publicKey${named ? "; give them in native.updates or the target's own" : ""}.`,
|
|
301
285
|
);
|
|
302
|
-
|
|
286
|
+
AkanAppConfig.#assertFiles(own ?? {}, where, configPath);
|
|
287
|
+
const {
|
|
288
|
+
basePath: _basePath,
|
|
289
|
+
indexPath: rawIndexPath,
|
|
290
|
+
deepLinks: rawDeepLinks,
|
|
291
|
+
updates: _updates,
|
|
292
|
+
...rest
|
|
293
|
+
} = merged;
|
|
294
|
+
const indexPath = normalizeIndexPath(rawIndexPath);
|
|
295
|
+
const deepLinks = normalizeDeepLinks(rawDeepLinks);
|
|
296
|
+
const { teamId: rawTeamId, ...iosRest } = merged.ios ?? {};
|
|
297
|
+
const teamId = rawTeamId?.trim();
|
|
298
|
+
const { sha256CertFingerprints: rawFingerprints, ...androidRest } = merged.android ?? {};
|
|
299
|
+
const fingerprints = normalizeStringList(rawFingerprints);
|
|
300
|
+
const target: AkanNativeTarget = {
|
|
301
|
+
...rest,
|
|
303
302
|
name,
|
|
304
|
-
basePath,
|
|
305
|
-
indexPath,
|
|
306
|
-
deepLinks,
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
version: target.version ?? version,
|
|
311
|
-
buildNum: target.buildNum ?? buildNum,
|
|
312
|
-
...(target.assets ? { assets: target.assets as AkanMobileTargetConfig["assets"] } : {}),
|
|
313
|
-
...(target.permissions ? { permissions: target.permissions as AkanMobileTargetConfig["permissions"] } : {}),
|
|
314
|
-
...(files || targetFiles ? { files: { ...files, ...targetFiles } } : {}),
|
|
315
|
-
...(native || target.native
|
|
316
|
-
? { native: AkanAppConfig.#mergeNative(native, target.native as AkanMobileTargetConfig["native"]) }
|
|
303
|
+
...(basePath ? { basePath } : {}),
|
|
304
|
+
...(indexPath ? { indexPath } : {}),
|
|
305
|
+
...(deepLinks ? { deepLinks } : {}),
|
|
306
|
+
...(merged.ios ? { ios: { ...iosRest, ...(teamId ? { teamId } : {}) } } : {}),
|
|
307
|
+
...(merged.android
|
|
308
|
+
? { android: { ...androidRest, ...(fingerprints ? { sha256CertFingerprints: fingerprints } : {}) } }
|
|
317
309
|
: {}),
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
310
|
+
appName: merged.appName ?? this.app.name,
|
|
311
|
+
appId: merged.appId ?? appId,
|
|
312
|
+
version: merged.version ?? version,
|
|
313
|
+
buildNum: merged.buildNum ?? buildNum,
|
|
314
|
+
...(updates ? { updates: updates as AkanNativeUpdatesConfig } : {}),
|
|
315
|
+
};
|
|
316
|
+
return [name, target];
|
|
321
317
|
}),
|
|
322
318
|
);
|
|
323
|
-
return {
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
319
|
+
return { appName, appId, ...(root.fileName ? { fileName: root.fileName } : {}), version, buildNum, targets };
|
|
320
|
+
}
|
|
321
|
+
/** `own` over `base`: plain objects merge key by key, and any other value, a list included, replaces. */
|
|
322
|
+
static #overlay(base: AkanNativeSettings, own: AkanNativeSettings): AkanNativeSettings {
|
|
323
|
+
const merge = (kept: unknown, value: unknown, top: boolean, key: string): unknown => {
|
|
324
|
+
if (!AkanAppConfig.#isPlainObject(kept) || !AkanAppConfig.#isPlainObject(value)) return value;
|
|
325
|
+
//? An icon or splash given as an object is one value: its image and color go together.
|
|
326
|
+
if (top && (key === "icon" || key === "splash")) return value;
|
|
327
|
+
const out: Record<string, unknown> = { ...kept };
|
|
328
|
+
for (const [inner, next] of Object.entries(value))
|
|
329
|
+
if (next !== undefined) out[inner] = merge(out[inner], next, false, inner);
|
|
330
|
+
return out;
|
|
333
331
|
};
|
|
332
|
+
const out: Record<string, unknown> = { ...base };
|
|
333
|
+
for (const [key, value] of Object.entries(own))
|
|
334
|
+
if (value !== undefined) out[key] = merge(out[key], value, true, key);
|
|
335
|
+
return out as AkanNativeSettings;
|
|
334
336
|
}
|
|
335
|
-
static
|
|
336
|
-
|
|
337
|
-
"basePath",
|
|
338
|
-
"indexPath",
|
|
339
|
-
"appName",
|
|
340
|
-
"appId",
|
|
341
|
-
"fileName",
|
|
342
|
-
"version",
|
|
343
|
-
"buildNum",
|
|
344
|
-
"assets",
|
|
345
|
-
"permissions",
|
|
346
|
-
"deepLinks",
|
|
347
|
-
"files",
|
|
348
|
-
"native",
|
|
349
|
-
"updates",
|
|
350
|
-
];
|
|
351
|
-
//* The Capacitor keys used to pass straight through; the native runtime takes typed ones, so a leftover is refused.
|
|
352
|
-
static #assertMobileKeys(section: object, known: string[], where: string, configPath: string) {
|
|
353
|
-
for (const key of Object.keys(section)) {
|
|
354
|
-
if (known.includes(key)) continue;
|
|
355
|
-
if (key === "plugins" || key === "ios" || key === "android")
|
|
356
|
-
throw new Error(
|
|
357
|
-
`${where}.${key} in ${configPath} is a Capacitor setting. The native runtime reads native.plugins, native.ios.{infoPlist,entitlements} and native.android.{manifest,application,activity,googleServices} instead.`,
|
|
358
|
-
);
|
|
359
|
-
throw new Error(`${where}.${key} in ${configPath} is not a mobile setting. Known: ${known.join(", ")}.`);
|
|
360
|
-
}
|
|
337
|
+
static #isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
338
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
361
339
|
}
|
|
362
|
-
static readonly #
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
340
|
+
static readonly #nativeKeys = {
|
|
341
|
+
native: [
|
|
342
|
+
"basePath",
|
|
343
|
+
"indexPath",
|
|
344
|
+
"appName",
|
|
345
|
+
"appId",
|
|
346
|
+
"fileName",
|
|
347
|
+
"version",
|
|
348
|
+
"buildNum",
|
|
349
|
+
"icon",
|
|
350
|
+
"splash",
|
|
351
|
+
"permissions",
|
|
352
|
+
"plugins",
|
|
353
|
+
"deepLinks",
|
|
354
|
+
"updates",
|
|
355
|
+
"ios",
|
|
356
|
+
"android",
|
|
357
|
+
"desktop",
|
|
358
|
+
],
|
|
359
|
+
ios: ["teamId", "infoPlist", "entitlements", "privacy", "files"],
|
|
360
|
+
android: [
|
|
361
|
+
"sha256CertFingerprints",
|
|
362
|
+
"googleServices",
|
|
363
|
+
"push",
|
|
364
|
+
"autoplay",
|
|
365
|
+
"manifest",
|
|
366
|
+
"application",
|
|
367
|
+
"activity",
|
|
368
|
+
"files",
|
|
369
|
+
],
|
|
370
|
+
desktop: ["server", "recovery", "window", "screenCapture"],
|
|
371
|
+
deepLinks: ["schemes", "domains"],
|
|
372
|
+
} as const;
|
|
373
|
+
//* Settings that moved when `mobile` became `native` are named with where they went, instead of "unknown".
|
|
374
|
+
static readonly #movedKeys: Readonly<Record<string, string>> = {
|
|
375
|
+
native: "the platform sections and plugins sit directly in the native section: ios, android, desktop, plugins",
|
|
376
|
+
assets: "icon and splash sit directly in the native section",
|
|
377
|
+
files:
|
|
378
|
+
"ios.files (keyed by the path in the app bundle) or android.files (keyed res/<type>/<file> or assets/<path>)",
|
|
379
|
+
push: "android.push",
|
|
380
|
+
privacy: "ios.privacy",
|
|
381
|
+
"deepLinks.ios": "ios.teamId",
|
|
382
|
+
"deepLinks.android": "android.sha256CertFingerprints",
|
|
383
|
+
};
|
|
384
|
+
static #assertNativeKeys(section: object, where: string, configPath: string) {
|
|
385
|
+
const check = (value: unknown, known: readonly string[], at: string, kind: string) => {
|
|
386
|
+
if (!AkanAppConfig.#isPlainObject(value)) return;
|
|
387
|
+
for (const key of Object.keys(value)) {
|
|
388
|
+
if (known.includes(key)) continue;
|
|
389
|
+
const moved = AkanAppConfig.#movedKeys[kind === "native" ? key : `${kind}.${key}`];
|
|
371
390
|
throw new Error(
|
|
372
|
-
|
|
391
|
+
moved
|
|
392
|
+
? `${at}.${key} in ${configPath} has moved: ${moved}.`
|
|
393
|
+
: `${at}.${key} in ${configPath} is not a native setting. Known: ${known.join(", ")}.`,
|
|
373
394
|
);
|
|
374
|
-
|
|
375
|
-
return files;
|
|
376
|
-
}
|
|
377
|
-
static #mergeNative(
|
|
378
|
-
base: AkanMobileTargetConfig["native"],
|
|
379
|
-
override: AkanMobileTargetConfig["native"],
|
|
380
|
-
): NonNullable<AkanMobileTargetConfig["native"]> {
|
|
381
|
-
const plugins = [...new Set([...(base?.plugins ?? []), ...(override?.plugins ?? [])])];
|
|
382
|
-
const ios = { ...base?.ios, ...override?.ios };
|
|
383
|
-
const android: NonNullable<NonNullable<AkanMobileTargetConfig["native"]>["android"]> = {
|
|
384
|
-
...base?.android,
|
|
385
|
-
...override?.android,
|
|
386
|
-
};
|
|
387
|
-
for (const key of ["manifest", "application", "activity"] as const) {
|
|
388
|
-
const merged = [...(base?.android?.[key] ?? []), ...(override?.android?.[key] ?? [])];
|
|
389
|
-
if (merged.length) android[key] = merged;
|
|
390
|
-
else delete android[key];
|
|
391
|
-
}
|
|
392
|
-
const pushAndroid = { ...base?.push?.android, ...override?.push?.android };
|
|
393
|
-
const privacy = { ...base?.privacy, ...override?.privacy };
|
|
394
|
-
const window = { ...base?.desktop?.window, ...override?.desktop?.window };
|
|
395
|
-
const desktop = {
|
|
396
|
-
...base?.desktop,
|
|
397
|
-
...override?.desktop,
|
|
398
|
-
...(Object.keys(window).length ? { window } : {}),
|
|
399
|
-
};
|
|
400
|
-
return {
|
|
401
|
-
...(plugins.length ? { plugins } : {}),
|
|
402
|
-
...(Object.keys(ios).length ? { ios } : {}),
|
|
403
|
-
...(Object.keys(android).length ? { android } : {}),
|
|
404
|
-
...(Object.keys(pushAndroid).length ? { push: { android: pushAndroid } } : {}),
|
|
405
|
-
...(Object.keys(privacy).length ? { privacy } : {}),
|
|
406
|
-
...(Object.keys(desktop).length ? { desktop } : {}),
|
|
395
|
+
}
|
|
407
396
|
};
|
|
397
|
+
check(section, AkanAppConfig.#nativeKeys.native, where, "native");
|
|
398
|
+
const settings = section as AkanNativeSettings;
|
|
399
|
+
check(settings.ios, AkanAppConfig.#nativeKeys.ios, `${where}.ios`, "ios");
|
|
400
|
+
check(settings.android, AkanAppConfig.#nativeKeys.android, `${where}.android`, "android");
|
|
401
|
+
check(settings.desktop, AkanAppConfig.#nativeKeys.desktop, `${where}.desktop`, "desktop");
|
|
402
|
+
check(settings.deepLinks, AkanAppConfig.#nativeKeys.deepLinks, `${where}.deepLinks`, "deepLinks");
|
|
408
403
|
}
|
|
409
|
-
#
|
|
410
|
-
|
|
411
|
-
|
|
404
|
+
static readonly #androidFilePattern = /^(?:res\/[^/]+\/[^/]+|assets\/.+)$/;
|
|
405
|
+
static #assertFiles(settings: AkanNativeSettings, where: string, configPath: string) {
|
|
406
|
+
const check = (
|
|
407
|
+
files: Record<string, string> | undefined,
|
|
408
|
+
at: string,
|
|
409
|
+
valid: (to: string) => boolean,
|
|
410
|
+
shape: string,
|
|
411
|
+
) => {
|
|
412
|
+
for (const [to, from] of Object.entries(files ?? {})) {
|
|
413
|
+
if (typeof from !== "string")
|
|
414
|
+
throw new Error(
|
|
415
|
+
`${at}["${to}"] in ${configPath} must name its source file, relative to the app folder, e.g. { "${shape}": "assets/chime.mp3" }.`,
|
|
416
|
+
);
|
|
417
|
+
if (!valid(to)) throw new Error(`${at}["${to}"] in ${configPath} must land at ${shape}.`);
|
|
418
|
+
}
|
|
419
|
+
};
|
|
420
|
+
check(
|
|
421
|
+
settings.ios?.files,
|
|
422
|
+
`${where}.ios.files`,
|
|
423
|
+
(to) => to.length > 0 && !to.startsWith("/") && !to.split("/").includes(".."),
|
|
424
|
+
"<path in the app bundle>",
|
|
425
|
+
);
|
|
426
|
+
check(
|
|
427
|
+
settings.android?.files,
|
|
428
|
+
`${where}.android.files`,
|
|
429
|
+
(to) => AkanAppConfig.#androidFilePattern.test(to),
|
|
430
|
+
"res/<type>/<file> or assets/<path>",
|
|
431
|
+
);
|
|
412
432
|
}
|
|
413
433
|
#applyRoutes(routes: AkanRouteConfig[] = []) {
|
|
414
434
|
for (const route of routes) {
|
|
@@ -695,7 +715,7 @@ export class AkanLibConfig implements LibConfigResult {
|
|
|
695
715
|
|
|
696
716
|
//! need to refactor
|
|
697
717
|
const shiftBuildNum = async (app: App, delta: number) => {
|
|
698
|
-
const { buildNum } = (await AkanAppConfig.from(app)).
|
|
718
|
+
const { buildNum } = (await AkanAppConfig.from(app)).native;
|
|
699
719
|
const akanConfigPath = path.join(app.cwdPath, "akan.config.ts");
|
|
700
720
|
const akanConfig = fs.readFileSync(akanConfigPath, "utf8");
|
|
701
721
|
fs.writeFileSync(akanConfigPath, akanConfig.replace(`buildNum: ${buildNum}`, `buildNum: ${buildNum + delta}`));
|
package/akanConfig/types.ts
CHANGED
|
@@ -8,10 +8,16 @@ export type {
|
|
|
8
8
|
AkanConfigFile,
|
|
9
9
|
AkanDatabaseConfig,
|
|
10
10
|
AkanExecutor,
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
11
|
+
AkanNativeAndroidConfig,
|
|
12
|
+
AkanNativeAppConfig,
|
|
13
|
+
AkanNativeAppId,
|
|
14
|
+
AkanNativeAppResult,
|
|
15
|
+
AkanNativeDeepLinks,
|
|
16
|
+
AkanNativeDesktopConfig,
|
|
17
|
+
AkanNativeIosConfig,
|
|
18
|
+
AkanNativeSettings,
|
|
19
|
+
AkanNativeTarget,
|
|
20
|
+
AkanNativeUpdatesConfig,
|
|
15
21
|
AkanNativeValue,
|
|
16
22
|
AkanPlugin,
|
|
17
23
|
AkanPluginNativeConfig,
|
|
@@ -39,8 +45,8 @@ export type {
|
|
|
39
45
|
LibConfigResult,
|
|
40
46
|
LibDockerConfig,
|
|
41
47
|
LibScanResult,
|
|
42
|
-
|
|
43
|
-
|
|
48
|
+
NativeEnv,
|
|
49
|
+
NativePermission,
|
|
44
50
|
PkgScanResult,
|
|
45
51
|
ScanResult,
|
|
46
52
|
WorkspaceScanResult,
|
package/executors.test.ts
CHANGED
|
@@ -338,8 +338,8 @@ describe("Workspace and app executor environment contracts", () => {
|
|
|
338
338
|
await writeFile(path.join(root, "apps/startclean/akan.config.ts"), "export default {};\n");
|
|
339
339
|
const akan = path.join(root, "apps/startclean/.akan");
|
|
340
340
|
const kept = [
|
|
341
|
-
"
|
|
342
|
-
"
|
|
341
|
+
"native/desktop/updates/macos-arm64/main.json",
|
|
342
|
+
"native/desktop/build/macos/App.app",
|
|
343
343
|
"cache/bin/ffmpeg",
|
|
344
344
|
];
|
|
345
345
|
const removed = [
|
|
@@ -347,6 +347,7 @@ describe("Workspace and app executor environment contracts", () => {
|
|
|
347
347
|
"generated/dict/index.ts",
|
|
348
348
|
"cache/cssCandidates.json",
|
|
349
349
|
"desktop/server/main.js",
|
|
350
|
+
"mobile/desktop/native/macos/App.app",
|
|
350
351
|
];
|
|
351
352
|
for (const file of [...kept, ...removed]) await writeText(path.join(akan, file), "x");
|
|
352
353
|
|
|
@@ -354,7 +355,7 @@ describe("Workspace and app executor environment contracts", () => {
|
|
|
354
355
|
await app.prepareCommand("start");
|
|
355
356
|
|
|
356
357
|
for (const file of kept) expect(existsSync(path.join(akan, file))).toBe(true);
|
|
357
|
-
for (const file of [...removed, "artifact", "generated", "desktop"])
|
|
358
|
+
for (const file of [...removed, "artifact", "generated", "desktop", "mobile"])
|
|
358
359
|
expect(existsSync(path.join(akan, file))).toBe(false);
|
|
359
360
|
});
|
|
360
361
|
|
package/executors.ts
CHANGED
|
@@ -1120,7 +1120,7 @@ export class AppExecutor extends SysExecutor {
|
|
|
1120
1120
|
return new AppExecutor({ workspace: executor instanceof WorkspaceExecutor ? executor : executor.workspace, name });
|
|
1121
1121
|
}
|
|
1122
1122
|
//* Not dev output: native builds and the update releases waiting to be uploaded, and the downloaded `bin` sources.
|
|
1123
|
-
static readonly #keptOnStart = [path.join(".akan", "
|
|
1123
|
+
static readonly #keptOnStart = [path.join(".akan", "native"), path.join(".akan", "cache", "bin")];
|
|
1124
1124
|
async #removeDevOutput(dir = ".akan") {
|
|
1125
1125
|
const entries = await readDirEntries(this.getPath(dir)).catch(() => [] as string[]);
|
|
1126
1126
|
await Promise.all(
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { optimize } from "@tailwindcss/node";
|
|
3
|
-
import type {
|
|
3
|
+
import type { AkanNativeAppId, AkanWebConfig } from "akanjs";
|
|
4
4
|
import type { BaseBuildArtifact } from "akanjs/server";
|
|
5
5
|
import { resolveSsrPageEntriesForApp } from "../artifact/implicitRootLayout";
|
|
6
6
|
import { computeRouteSeedIndex, type RouteSeedIndex, saveRouteSeedIndex } from "../artifact/routeSeedIndex";
|
|
@@ -85,15 +85,15 @@ export class SsrBaseArtifactBuilder {
|
|
|
85
85
|
i18n: akanConfig.i18n,
|
|
86
86
|
imageConfig: akanConfig.images,
|
|
87
87
|
web: SsrBaseArtifactBuilder.servedWeb(this.#command, akanConfig.web),
|
|
88
|
-
deepLinkAssociations: Object.values(akanConfig.
|
|
88
|
+
deepLinkAssociations: Object.values(akanConfig.native.targets)
|
|
89
89
|
.filter((target) => (target.deepLinks?.domains?.length ?? 0) > 0)
|
|
90
90
|
.map((target) => ({
|
|
91
91
|
targetName: target.name,
|
|
92
92
|
iosAppId: SsrBaseArtifactBuilder.#appIdOn(target.appId, "ios"),
|
|
93
93
|
androidAppId: SsrBaseArtifactBuilder.#appIdOn(target.appId, "android"),
|
|
94
94
|
domains: target.deepLinks?.domains ?? [],
|
|
95
|
-
iosTeamId: target.
|
|
96
|
-
androidSha256CertFingerprints: target.
|
|
95
|
+
iosTeamId: target.ios?.teamId,
|
|
96
|
+
androidSha256CertFingerprints: target.android?.sha256CertFingerprints,
|
|
97
97
|
})),
|
|
98
98
|
};
|
|
99
99
|
await Bun.write(path.join(this.#absArtifactDir, "base-artifact.json"), `${JSON.stringify(artifact, null, 2)}\n`);
|
|
@@ -224,7 +224,7 @@ export class SsrBaseArtifactBuilder {
|
|
|
224
224
|
}
|
|
225
225
|
|
|
226
226
|
// Checked against the written asset, not the compiled text: it is the only stylesheet an SSR render serves.
|
|
227
|
-
static #appIdOn(appId:
|
|
227
|
+
static #appIdOn(appId: AkanNativeAppId, platform: "ios" | "android"): string | null {
|
|
228
228
|
return typeof appId === "string" ? appId : (appId[platform] ?? appId.default ?? null);
|
|
229
229
|
}
|
|
230
230
|
|
|
@@ -28,7 +28,7 @@ const stage = async (
|
|
|
28
28
|
bin: AkanBinConfig,
|
|
29
29
|
{ libBins = [], libs = [] }: { libBins?: { lib: string; bin: AkanBinConfig }[]; libs?: string[] } = {},
|
|
30
30
|
) => {
|
|
31
|
-
const binDir = path.join(root, "apps/portal/.akan/
|
|
31
|
+
const binDir = path.join(root, "apps/portal/.akan/native/default/bin");
|
|
32
32
|
const config = { app: { name: "portal" }, bin, libBins } as never;
|
|
33
33
|
const carried = await new DesktopBin(fakeApp(path.join(root, "apps/portal"), libs), config).stage(binDir);
|
|
34
34
|
return { carried, binDir };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { cp, mkdir, readdir, rm, writeFile } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import type { AkanAppConfig,
|
|
3
|
+
import type { AkanAppConfig, NativeEnv } from "../akanConfig";
|
|
4
4
|
import type { App } from "../commandDecorators";
|
|
5
5
|
import type { PackageJson } from "../types";
|
|
6
6
|
import { DesktopBin } from "./desktopBin";
|
|
@@ -40,11 +40,11 @@ export class DesktopServerStage {
|
|
|
40
40
|
static assertCarriable(config: AkanAppConfig) {
|
|
41
41
|
if (!config.database.modes.includes("single"))
|
|
42
42
|
throw new Error(
|
|
43
|
-
`
|
|
43
|
+
`desktop.server in native puts the server in the app, where only database mode single runs (no Redis or Postgres); apps/${config.app.name}/akan.config.ts declares ${config.database.modes.join(", ")}.`,
|
|
44
44
|
);
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
static env(config: AkanAppConfig, environment:
|
|
47
|
+
static env(config: AkanAppConfig, environment: NativeEnv): Record<string, string> {
|
|
48
48
|
//? The launcher picks the port, and the log file stays on: a user's computer has no log collector.
|
|
49
49
|
const { PORT: _port, AKAN_LOG_TO_FILE: _fileLog, ...image } = config.getProductionEnv(environment);
|
|
50
50
|
return {
|
|
@@ -122,7 +122,7 @@ export class DesktopServerStage {
|
|
|
122
122
|
}
|
|
123
123
|
|
|
124
124
|
//? Copies the build beside what `install` put in the stage, so it runs after both.
|
|
125
|
-
async prepare(environment:
|
|
125
|
+
async prepare(environment: NativeEnv): Promise<DesktopServerBundle> {
|
|
126
126
|
const config = await this.app.getConfig();
|
|
127
127
|
const dist = this.app.dist.cwdPath;
|
|
128
128
|
await mkdir(this.dir, { recursive: true });
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
|
-
import type {
|
|
2
|
+
import type { AkanNativeTarget } from "../akanConfig";
|
|
3
3
|
import type { App } from "../commandDecorators";
|
|
4
4
|
import {
|
|
5
5
|
appIdsOf,
|
|
@@ -17,9 +17,9 @@ const target = {
|
|
|
17
17
|
appId: "com.akanjs.app",
|
|
18
18
|
version: "1.0.0",
|
|
19
19
|
buildNum: 1,
|
|
20
|
-
} as
|
|
20
|
+
} as AkanNativeTarget;
|
|
21
21
|
|
|
22
|
-
describe("
|
|
22
|
+
describe("native target helpers", () => {
|
|
23
23
|
test("maps deep links into target base paths without duplicating prefixes", () => {
|
|
24
24
|
expect(resolveMobilePath(target, "/order/123")).toBe("/akanjs/order/123");
|
|
25
25
|
expect(resolveMobilePath(target, "/akanjs/order/123")).toBe("/akanjs/order/123");
|
|
@@ -32,7 +32,7 @@ describe("mobile target helpers", () => {
|
|
|
32
32
|
expect(resolveAppId(appId, "android")).toBe("com.yeollege");
|
|
33
33
|
expect(resolveAppId("com.one.app", "macos")).toBe("com.one.app");
|
|
34
34
|
expect(() => resolveAppId({ ios: "com.puffinplanet.yeollege" }, "android")).toThrow(
|
|
35
|
-
"
|
|
35
|
+
"native.appId names no id for android",
|
|
36
36
|
);
|
|
37
37
|
expect(appIdsOf(appId)).toEqual(["com.yeollege", "com.puffinplanet.yeollege"]);
|
|
38
38
|
});
|
|
@@ -42,11 +42,11 @@ describe("mobile target helpers", () => {
|
|
|
42
42
|
expect(targetHtmlFilename({ ...target, basePath: undefined })).toBe("index.html");
|
|
43
43
|
});
|
|
44
44
|
|
|
45
|
-
test("uses the configured
|
|
45
|
+
test("uses the configured native target as the only choice when one target is configured", async () => {
|
|
46
46
|
const app = {
|
|
47
47
|
getConfig: async () => ({
|
|
48
48
|
basePaths: new Set(["akanjs", "soft", "office"]),
|
|
49
|
-
|
|
49
|
+
native: { targets: { akanjs: target } },
|
|
50
50
|
}),
|
|
51
51
|
} as unknown as App;
|
|
52
52
|
|
|
@@ -59,13 +59,13 @@ describe("mobile target helpers", () => {
|
|
|
59
59
|
const appWith = (basePaths: string[]) =>
|
|
60
60
|
({
|
|
61
61
|
name: "angelo",
|
|
62
|
-
getConfig: async () => ({ basePaths: new Set(basePaths),
|
|
62
|
+
getConfig: async () => ({ basePaths: new Set(basePaths), native: { targets: { default: template } } }),
|
|
63
63
|
}) as unknown as App;
|
|
64
64
|
|
|
65
65
|
const two = appWith(["soft", "office"]);
|
|
66
66
|
await expect(getMobileTargetChoices(two)).resolves.toEqual(["soft", "office"]);
|
|
67
67
|
await expect(resolveMobileTargets(two, undefined)).rejects.toThrow(
|
|
68
|
-
"Multiple
|
|
68
|
+
"Multiple native targets found for angelo. Pass --target <soft|office|all>.",
|
|
69
69
|
);
|
|
70
70
|
await expect(resolveMobileTargets(two, "office")).resolves.toEqual([
|
|
71
71
|
{ name: "office", config: { ...template, name: "office", basePath: "office" } },
|
|
@@ -85,11 +85,11 @@ describe("mobile target helpers", () => {
|
|
|
85
85
|
]);
|
|
86
86
|
});
|
|
87
87
|
|
|
88
|
-
test("resolves a route base path onto the default
|
|
88
|
+
test("resolves a route base path onto the default native target config", async () => {
|
|
89
89
|
const app = {
|
|
90
90
|
getConfig: async () => ({
|
|
91
91
|
basePaths: new Set(["akanjs", "soft", "office"]),
|
|
92
|
-
|
|
92
|
+
native: { targets: { akanjs: target } },
|
|
93
93
|
}),
|
|
94
94
|
} as unknown as App;
|
|
95
95
|
|