@absolutejs/absolute 0.20.0-beta.37 → 0.20.0-beta.39
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/README.md +20 -0
- package/dist/angular/components/core/streamingSlotRegistrar.js +1 -1
- package/dist/angular/components/core/streamingSlotRegistry.js +2 -2
- package/dist/cli/index.js +1193 -282
- package/dist/mobile/index.js +594 -49
- package/dist/mobile/index.js.map +8 -7
- package/dist/mobile/remoteMacAgentEntry.js +6 -6
- package/dist/src/mobile/androidRelease.d.ts +6 -0
- package/dist/src/mobile/ciWorkflow.d.ts +26 -0
- package/dist/src/mobile/index.d.ts +1 -0
- package/dist/src/mobile/iosRelease.d.ts +1 -0
- package/dist/src/mobile/mobileBundleInspection.d.ts +55 -0
- package/dist/src/mobile/mobileInspect.d.ts +42 -16
- package/dist/src/mobile/releaseDoctor.d.ts +23 -1
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -2589,7 +2589,14 @@ import {
|
|
|
2589
2589
|
writeFile as writeFile3
|
|
2590
2590
|
} from "fs/promises";
|
|
2591
2591
|
import { dirname as dirname4, isAbsolute as isAbsolute2, join as join9, relative as relative3, resolve as resolve6, sep as sep2 } from "path";
|
|
2592
|
-
var
|
|
2592
|
+
var developmentTeamArgument = (value) => {
|
|
2593
|
+
if (value === undefined)
|
|
2594
|
+
return;
|
|
2595
|
+
const team = value.trim().toUpperCase();
|
|
2596
|
+
if (!/^[A-Z0-9]{10}$/u.test(team))
|
|
2597
|
+
throw new TypeError("iOS development team must contain ten letters or digits.");
|
|
2598
|
+
return `DEVELOPMENT_TEAM=${team}`;
|
|
2599
|
+
}, isRecord2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value), requireManifest = (value) => {
|
|
2593
2600
|
if (!isRecord2(value) || typeof value.appBuild !== "string" || typeof value.appId !== "string" || typeof value.runtime !== "string") {
|
|
2594
2601
|
throw new TypeError("Invalid embedded AbsoluteJS mobile manifest.");
|
|
2595
2602
|
}
|
|
@@ -2762,8 +2769,10 @@ var isRecord2 = (value) => typeof value === "object" && value !== null && !Array
|
|
|
2762
2769
|
await writeFile3(exportPlist, exportOptions());
|
|
2763
2770
|
const run = options.run ?? defaultRun2;
|
|
2764
2771
|
try {
|
|
2772
|
+
const developmentTeam = developmentTeamArgument(options.developmentTeam);
|
|
2765
2773
|
const versionArguments = [
|
|
2766
2774
|
`MARKETING_VERSION=${marketingVersion}`,
|
|
2775
|
+
...developmentTeam ? [developmentTeam] : [],
|
|
2767
2776
|
...buildNumber === undefined ? [] : [`CURRENT_PROJECT_VERSION=${buildNumber}`]
|
|
2768
2777
|
];
|
|
2769
2778
|
const archiveExit = await run([
|
|
@@ -6411,12 +6420,25 @@ var MANIFEST_FILE = "absolute-mobile-manifest.json", BOOTSTRAP_FILE = "absolute-
|
|
|
6411
6420
|
if (candidate)
|
|
6412
6421
|
return candidate;
|
|
6413
6422
|
throw new TypeError("AbsoluteJS mobile push shell module is missing.");
|
|
6414
|
-
}, escapeHtml = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """),
|
|
6423
|
+
}, escapeHtml = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """), contentSecurityPolicy = (productionOrigin) => {
|
|
6424
|
+
const backend = new URL(productionOrigin);
|
|
6425
|
+
const socketOrigin = `${backend.protocol === "https:" ? "wss:" : "ws:"}//${backend.host}`;
|
|
6426
|
+
return [
|
|
6427
|
+
"default-src 'self' data: blob: https:",
|
|
6428
|
+
"base-uri 'none'",
|
|
6429
|
+
"object-src 'none'",
|
|
6430
|
+
"script-src 'self'",
|
|
6431
|
+
"style-src 'self' 'unsafe-inline'",
|
|
6432
|
+
`connect-src 'self' ${backend.origin} ${socketOrigin}`,
|
|
6433
|
+
"form-action 'none'"
|
|
6434
|
+
].join("; ");
|
|
6435
|
+
}, indexHtml = (appName, productionOrigin) => `<!doctype html>
|
|
6415
6436
|
<html>
|
|
6416
6437
|
<head>
|
|
6417
6438
|
<meta charset="utf-8">
|
|
6418
6439
|
<meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover">
|
|
6419
6440
|
<meta name="color-scheme" content="light dark">
|
|
6441
|
+
<meta http-equiv="Content-Security-Policy" content="${escapeHtml(contentSecurityPolicy(productionOrigin))}">
|
|
6420
6442
|
<title>${escapeHtml(appName)}</title>
|
|
6421
6443
|
</head>
|
|
6422
6444
|
<body>
|
|
@@ -6635,7 +6657,7 @@ void startAbsoluteMobileShell(${push ? `{ createAuth: (config, options) => creat
|
|
|
6635
6657
|
await Promise.all([
|
|
6636
6658
|
writeFile7(join17(staging, MANIFEST_FILE), `${JSON.stringify(manifest, null, "\t")}
|
|
6637
6659
|
`),
|
|
6638
|
-
writeFile7(join17(staging, INDEX_FILE), indexHtml(options.config.appName)),
|
|
6660
|
+
writeFile7(join17(staging, INDEX_FILE), indexHtml(options.config.appName, options.config.productionOrigin)),
|
|
6639
6661
|
buildShellBootstrap(staging, options.auth !== undefined, options.auth !== undefined && options.sync === true, `absolutejs.${options.auth?.clientId ?? options.config.appId}.`, options.deviceCapabilities, options.projectRoot)
|
|
6640
6662
|
]);
|
|
6641
6663
|
await installBundle(staging, destination);
|
|
@@ -18048,28 +18070,173 @@ var DEFAULT_ROUTE_TIMEOUT_MS = 30000, DEFAULT_HMR_TIMEOUT_MS = 30000, routeExpre
|
|
|
18048
18070
|
return apply;
|
|
18049
18071
|
};
|
|
18050
18072
|
|
|
18051
|
-
// src/mobile/
|
|
18052
|
-
import {
|
|
18053
|
-
import {
|
|
18054
|
-
|
|
18073
|
+
// src/mobile/mobileBundleInspection.ts
|
|
18074
|
+
import { createHash as createHash12 } from "crypto";
|
|
18075
|
+
import { access as access8, readFile as readFile16, stat as stat2 } from "fs/promises";
|
|
18076
|
+
import { join as join49, relative as relative24, resolve as resolve39 } from "path";
|
|
18077
|
+
var MOBILE_FRAMEWORKS, SHA256_PATTERN, isObject2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value), portablePath = (projectRoot, path) => {
|
|
18078
|
+
const value = relative24(resolve39(projectRoot), resolve39(path)).replaceAll("\\", "/");
|
|
18079
|
+
return value || ".";
|
|
18080
|
+
}, pathExists5 = async (path) => {
|
|
18055
18081
|
try {
|
|
18056
18082
|
await access8(path);
|
|
18057
18083
|
return true;
|
|
18058
18084
|
} catch {
|
|
18059
18085
|
return false;
|
|
18060
18086
|
}
|
|
18087
|
+
}, readObject = async (path) => {
|
|
18088
|
+
const value = JSON.parse(await readFile16(path, "utf8"));
|
|
18089
|
+
if (!isObject2(value))
|
|
18090
|
+
throw new TypeError("JSON root must be an object.");
|
|
18091
|
+
return value;
|
|
18092
|
+
}, requireString = (value, field) => {
|
|
18093
|
+
if (typeof value !== "string" || value.length === 0)
|
|
18094
|
+
throw new TypeError(`${field} must be a non-empty string.`);
|
|
18095
|
+
return value;
|
|
18096
|
+
}, requireStringArray = (value, field) => {
|
|
18097
|
+
if (!Array.isArray(value) || !value.every((item) => typeof item === "string"))
|
|
18098
|
+
throw new TypeError(`${field} must be a string array.`);
|
|
18099
|
+
return value;
|
|
18100
|
+
}, requireBundleFile = async (root, value, field, expectedHash) => {
|
|
18101
|
+
const portable = requireString(value, field);
|
|
18102
|
+
const path = resolve39(root, portable);
|
|
18103
|
+
const normalizedRoot = resolve39(root);
|
|
18104
|
+
if (path === normalizedRoot || !path.startsWith(`${normalizedRoot}/`))
|
|
18105
|
+
throw new TypeError(`${field} must remain inside the mobile bundle.`);
|
|
18106
|
+
if (!(await stat2(path).catch(() => {
|
|
18107
|
+
return;
|
|
18108
|
+
}))?.isFile())
|
|
18109
|
+
throw new TypeError(`${field} does not exist in the mobile bundle.`);
|
|
18110
|
+
if (expectedHash !== undefined) {
|
|
18111
|
+
if (!SHA256_PATTERN.test(expectedHash))
|
|
18112
|
+
throw new TypeError(`${field} has an invalid SHA-256 digest.`);
|
|
18113
|
+
const actual = createHash12("sha256").update(await readFile16(path)).digest("hex");
|
|
18114
|
+
if (actual !== expectedHash)
|
|
18115
|
+
throw new TypeError(`${field} failed its SHA-256 integrity check.`);
|
|
18116
|
+
}
|
|
18117
|
+
return portable;
|
|
18118
|
+
}, inspectAbsoluteMobileBundle = async (config, projectRoot) => {
|
|
18119
|
+
const manifestPath = join49(config.bundleDirectory, "absolute-mobile-manifest.json");
|
|
18120
|
+
const manifest = portablePath(projectRoot, manifestPath);
|
|
18121
|
+
if (!await pathExists5(manifestPath))
|
|
18122
|
+
return { manifest, status: "missing" };
|
|
18123
|
+
try {
|
|
18124
|
+
const value = await readObject(manifestPath);
|
|
18125
|
+
if (value.format !== ABSOLUTE_MOBILE_CLIENT_MANIFEST_FORMAT)
|
|
18126
|
+
throw new TypeError("format is not supported by this runtime.");
|
|
18127
|
+
if (requireString(value.appId, "appId") !== config.appId)
|
|
18128
|
+
throw new TypeError("appId does not match the effective mobile config.");
|
|
18129
|
+
if (requireString(value.productionOrigin, "productionOrigin") !== config.productionOrigin)
|
|
18130
|
+
throw new TypeError("productionOrigin does not match the effective mobile config.");
|
|
18131
|
+
const appBuild = requireString(value.appBuild, "appBuild");
|
|
18132
|
+
const runtime = requireString(value.runtime, "runtime");
|
|
18133
|
+
if (runtime !== String(ABSOLUTE_MOBILE_PAGE_PROTOCOL_VERSION))
|
|
18134
|
+
throw new TypeError("runtime is not supported by this AbsoluteJS build.");
|
|
18135
|
+
const capabilities = requireStringArray(value.deviceCapabilities, "deviceCapabilities").sort();
|
|
18136
|
+
if (!Array.isArray(value.pages) || !Array.isArray(value.routes))
|
|
18137
|
+
throw new TypeError("pages and routes must be arrays.");
|
|
18138
|
+
const pageIds = new Set;
|
|
18139
|
+
const frameworks7 = new Set;
|
|
18140
|
+
await Promise.all(value.pages.map(async (candidate) => {
|
|
18141
|
+
if (!isObject2(candidate))
|
|
18142
|
+
throw new TypeError("pages contains an invalid entry.");
|
|
18143
|
+
const pageId = requireString(candidate.pageId, "page.pageId");
|
|
18144
|
+
if (pageIds.has(pageId))
|
|
18145
|
+
throw new TypeError("page.pageId values must be unique.");
|
|
18146
|
+
pageIds.add(pageId);
|
|
18147
|
+
const framework = requireString(candidate.framework, "page.framework");
|
|
18148
|
+
if (!MOBILE_FRAMEWORKS.has(framework))
|
|
18149
|
+
throw new TypeError("page.framework is unsupported.");
|
|
18150
|
+
frameworks7.add(framework);
|
|
18151
|
+
const bundleHash = requireString(candidate.bundleHash, "page.bundleHash");
|
|
18152
|
+
requireString(candidate.contract, "page.contract");
|
|
18153
|
+
requireString(candidate.propsSchemaHash, "page.propsSchemaHash");
|
|
18154
|
+
await requireBundleFile(config.bundleDirectory, candidate.localBundlePath, "page.localBundlePath", bundleHash);
|
|
18155
|
+
if (candidate.localStylePath !== undefined) {
|
|
18156
|
+
const styleHash = requireString(candidate.styleBundleHash, "page.styleBundleHash");
|
|
18157
|
+
await requireBundleFile(config.bundleDirectory, candidate.localStylePath, "page.localStylePath", styleHash);
|
|
18158
|
+
} else if (candidate.styleBundleHash !== undefined)
|
|
18159
|
+
throw new TypeError("page.styleBundleHash requires page.localStylePath.");
|
|
18160
|
+
}));
|
|
18161
|
+
const routes = value.routes.map((candidate) => {
|
|
18162
|
+
if (!isObject2(candidate))
|
|
18163
|
+
throw new TypeError("routes contains an invalid entry.");
|
|
18164
|
+
const { method } = candidate;
|
|
18165
|
+
if (method !== "GET" && method !== "HEAD")
|
|
18166
|
+
throw new TypeError("route.method must be GET or HEAD.");
|
|
18167
|
+
const pageId = requireString(candidate.pageId, "route.pageId");
|
|
18168
|
+
if (!pageIds.has(pageId))
|
|
18169
|
+
throw new TypeError("route.pageId references a missing page.");
|
|
18170
|
+
return {
|
|
18171
|
+
method,
|
|
18172
|
+
pageId,
|
|
18173
|
+
pattern: requireString(candidate.pattern, "route.pattern")
|
|
18174
|
+
};
|
|
18175
|
+
});
|
|
18176
|
+
await Promise.all(["index.html", "absolute-mobile-bootstrap.js"].map((file) => requireBundleFile(config.bundleDirectory, file, file)));
|
|
18177
|
+
const entryPath = new URL(config.entry, "https://absolute.invalid").pathname;
|
|
18178
|
+
const entryResolved = resolveAbsoluteMobileRoute(routes, entryPath) !== undefined;
|
|
18179
|
+
if (!entryResolved)
|
|
18180
|
+
throw new TypeError("entry is not owned by an embedded route.");
|
|
18181
|
+
return {
|
|
18182
|
+
appBuild,
|
|
18183
|
+
auth: isObject2(value.auth),
|
|
18184
|
+
capabilities,
|
|
18185
|
+
entryResolved,
|
|
18186
|
+
frameworks: [...frameworks7].sort(),
|
|
18187
|
+
manifest,
|
|
18188
|
+
pageCount: value.pages.length,
|
|
18189
|
+
routeCount: value.routes.length,
|
|
18190
|
+
runtime,
|
|
18191
|
+
status: "valid",
|
|
18192
|
+
sync: isObject2(value.sync)
|
|
18193
|
+
};
|
|
18194
|
+
} catch (error) {
|
|
18195
|
+
return {
|
|
18196
|
+
issue: error instanceof Error ? error.message : "The embedded mobile manifest is invalid.",
|
|
18197
|
+
manifest,
|
|
18198
|
+
status: "invalid"
|
|
18199
|
+
};
|
|
18200
|
+
}
|
|
18201
|
+
};
|
|
18202
|
+
var init_mobileBundleInspection = __esm(() => {
|
|
18203
|
+
init_pageProtocol();
|
|
18204
|
+
init_routeMatcher();
|
|
18205
|
+
init_transport();
|
|
18206
|
+
MOBILE_FRAMEWORKS = new Set([
|
|
18207
|
+
"angular",
|
|
18208
|
+
"ember",
|
|
18209
|
+
"html",
|
|
18210
|
+
"htmx",
|
|
18211
|
+
"react",
|
|
18212
|
+
"svelte",
|
|
18213
|
+
"vue"
|
|
18214
|
+
]);
|
|
18215
|
+
SHA256_PATTERN = /^[a-f0-9]{64}$/u;
|
|
18216
|
+
});
|
|
18217
|
+
|
|
18218
|
+
// src/mobile/releaseDoctor.ts
|
|
18219
|
+
import { access as access9, readFile as readFile17, readdir as readdir4 } from "fs/promises";
|
|
18220
|
+
import { dirname as dirname29, extname as extname8, join as join50, relative as relative25 } from "path";
|
|
18221
|
+
var ABSOLUTE_MOBILE_COMPLIANCE_REPORT_FORMAT = 1, HMR_ASSET_PATTERN, RELEASE_ASSET_EXTENSIONS, EXACT_VERSION_PATTERN, NOT_FOUND3 = -1, LOCK_FILES, MANUAL_REVIEW, pathExists6 = async (path) => {
|
|
18222
|
+
try {
|
|
18223
|
+
await access9(path);
|
|
18224
|
+
return true;
|
|
18225
|
+
} catch {
|
|
18226
|
+
return false;
|
|
18227
|
+
}
|
|
18061
18228
|
}, inspectReleaseAsset = async (path, isDirectory, isFile2) => {
|
|
18062
18229
|
if (isDirectory)
|
|
18063
18230
|
return findHmrAsset(path);
|
|
18064
18231
|
if (!isFile2 || !RELEASE_ASSET_EXTENSIONS.has(extname8(path)))
|
|
18065
18232
|
return;
|
|
18066
|
-
const source = await
|
|
18233
|
+
const source = await readFile17(path, "utf8");
|
|
18067
18234
|
return HMR_ASSET_PATTERN.test(source) ? path : undefined;
|
|
18068
18235
|
}, findHmrAsset = async (root) => {
|
|
18069
|
-
if (!await
|
|
18236
|
+
if (!await pathExists6(root))
|
|
18070
18237
|
return;
|
|
18071
18238
|
const entries = await readdir4(root, { withFileTypes: true });
|
|
18072
|
-
const matches = await Promise.all(entries.map((entry) => inspectReleaseAsset(
|
|
18239
|
+
const matches = await Promise.all(entries.map((entry) => inspectReleaseAsset(join50(root, entry.name), entry.isDirectory(), entry.isFile())));
|
|
18073
18240
|
return matches.find((match) => match !== undefined);
|
|
18074
18241
|
}, pass = (id, detail, path) => ({ detail, id, path, status: "pass" }), fail5 = (id, detail, path, remediation) => ({
|
|
18075
18242
|
detail,
|
|
@@ -18077,7 +18244,70 @@ var HMR_ASSET_PATTERN, RELEASE_ASSET_EXTENSIONS, pathExists5 = async (path) => {
|
|
|
18077
18244
|
path,
|
|
18078
18245
|
remediation,
|
|
18079
18246
|
status: "fail"
|
|
18080
|
-
}),
|
|
18247
|
+
}), warn = (id, detail, path, remediation) => ({
|
|
18248
|
+
detail,
|
|
18249
|
+
id,
|
|
18250
|
+
path,
|
|
18251
|
+
remediation,
|
|
18252
|
+
status: "warn"
|
|
18253
|
+
}), readJsonObject = async (path) => {
|
|
18254
|
+
const value = JSON.parse(await readFile17(path, "utf8"));
|
|
18255
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
18256
|
+
throw new TypeError("JSON root must be an object.");
|
|
18257
|
+
return Object.fromEntries(Object.entries(value));
|
|
18258
|
+
}, packageDeclarations = (manifest) => {
|
|
18259
|
+
const declarations = new Map;
|
|
18260
|
+
for (const field of ["dependencies", "devDependencies"]) {
|
|
18261
|
+
const value = manifest[field];
|
|
18262
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
18263
|
+
continue;
|
|
18264
|
+
for (const [name, version2] of Object.entries(value))
|
|
18265
|
+
if (typeof version2 === "string")
|
|
18266
|
+
declarations.set(name, version2);
|
|
18267
|
+
}
|
|
18268
|
+
return declarations;
|
|
18269
|
+
}, versionCore = (version2) => version2.split("-")[0]?.split(".").slice(0, 2).join("."), capacitorVersionCheck = async (config, projectRoot) => {
|
|
18270
|
+
const manifestPath = join50(projectRoot, "package.json");
|
|
18271
|
+
try {
|
|
18272
|
+
const manifest = await readJsonObject(manifestPath);
|
|
18273
|
+
const declarations = packageDeclarations(manifest);
|
|
18274
|
+
const names = [
|
|
18275
|
+
"@capacitor/core",
|
|
18276
|
+
"@capacitor/cli",
|
|
18277
|
+
...config.platforms.map((platform6) => `@capacitor/${platform6}`)
|
|
18278
|
+
];
|
|
18279
|
+
const versions = await Promise.all(names.map(async (name) => {
|
|
18280
|
+
const declared = declarations.get(name);
|
|
18281
|
+
if (!declared || !EXACT_VERSION_PATTERN.test(declared))
|
|
18282
|
+
throw new TypeError(`${name} must be a direct exact dependency.`);
|
|
18283
|
+
const installed = await readJsonObject(join50(projectRoot, "node_modules", name, "package.json"));
|
|
18284
|
+
if (installed.version !== declared)
|
|
18285
|
+
throw new TypeError(`${name} does not match its installed version.`);
|
|
18286
|
+
return declared;
|
|
18287
|
+
}));
|
|
18288
|
+
const lines = new Set(versions.map(versionCore));
|
|
18289
|
+
if (lines.size !== 1)
|
|
18290
|
+
throw new TypeError("Capacitor core, CLI, and platform packages must use one major/minor line.");
|
|
18291
|
+
return pass("mobile.capacitor-versions", `Capacitor core, CLI, and configured platforms are pinned and aligned on ${versions[0]}.`, manifestPath);
|
|
18292
|
+
} catch (error) {
|
|
18293
|
+
return fail5("mobile.capacitor-versions", error instanceof Error ? error.message : "Capacitor package versions could not be validated.", manifestPath, "Pin @capacitor/core, @capacitor/cli, and each configured platform to exact versions on the same major/minor line, then reinstall.");
|
|
18294
|
+
}
|
|
18295
|
+
}, dependencyLockCheck = async (projectRoot) => {
|
|
18296
|
+
const present = (await Promise.all(LOCK_FILES.map(async (name) => ({
|
|
18297
|
+
exists: await pathExists6(join50(projectRoot, name)),
|
|
18298
|
+
name
|
|
18299
|
+
})))).find(({ exists: exists3 }) => exists3);
|
|
18300
|
+
return present ? pass("mobile.dependency-lock", `Dependency graph is locked by ${present.name}.`, join50(projectRoot, present.name)) : fail5("mobile.dependency-lock", "No supported dependency lockfile is present.", projectRoot, "Install dependencies with the project package manager and commit its lockfile before release.");
|
|
18301
|
+
}, productionOriginCheck = (config, projectRoot) => {
|
|
18302
|
+
const origin = new URL(config.productionOrigin);
|
|
18303
|
+
return origin.protocol === "https:" ? pass("mobile.production-origin", "Production transport uses an HTTPS origin.") : fail5("mobile.production-origin", "A loopback development origin cannot be used for a signed release.", projectRoot, "Configure mobile.server.productionOrigin with the deployed HTTPS origin.");
|
|
18304
|
+
}, associationIdentityCheck = (config, projectRoot) => {
|
|
18305
|
+
const missing = [
|
|
18306
|
+
...config.platforms.includes("ios") && !config.appleAppIdPrefix ? ["mobile.deepLinks.apple.appIdPrefix"] : [],
|
|
18307
|
+
...config.platforms.includes("android") && config.androidCertificateFingerprints.length === 0 ? ["mobile.deepLinks.android.sha256CertificateFingerprints"] : []
|
|
18308
|
+
];
|
|
18309
|
+
return missing.length === 0 ? pass("mobile.association-identities", "Deep-link association identities are configured for every release platform.") : fail5("mobile.association-identities", `Release association identity is missing: ${missing.join(", ")}.`, projectRoot, "Configure the Apple application prefix and every Android signing-certificate SHA-256 fingerprint used for release.");
|
|
18310
|
+
}, journalReleaseCheck = async (journalPath, platform6) => await pathExists6(journalPath) ? fail5(`${platform6}.dev-journal`, "A live-reload recovery journal is still active.", journalPath, "Close the development session or run mobile doctor again after stale-session repair.") : pass(`${platform6}.dev-journal`, "No live-reload recovery journal is active.", journalPath), parsedJson = (source) => {
|
|
18081
18311
|
try {
|
|
18082
18312
|
const parsed = JSON.parse(source);
|
|
18083
18313
|
return parsed;
|
|
@@ -18099,30 +18329,138 @@ var HMR_ASSET_PATTERN, RELEASE_ASSET_EXTENSIONS, pathExists5 = async (path) => {
|
|
|
18099
18329
|
const allowNavigation = Reflect.get(server, "allowNavigation");
|
|
18100
18330
|
return typeof Reflect.get(server, "url") === "string" || Reflect.get(server, "cleartext") === true || Array.isArray(allowNavigation) && allowNavigation.length > 0;
|
|
18101
18331
|
}, capacitorConfigReleaseCheck = async (nativeConfigPath) => {
|
|
18102
|
-
if (!await
|
|
18332
|
+
if (!await pathExists6(nativeConfigPath)) {
|
|
18103
18333
|
return fail5("android.capacitor-config", "The generated Android Capacitor config is missing.", nativeConfigPath, "Run `absolute mobile sync android` before release validation.");
|
|
18104
18334
|
}
|
|
18105
|
-
const unsafe = isUnsafeCapacitorConfig(await
|
|
18335
|
+
const unsafe = isUnsafeCapacitorConfig(await readFile17(nativeConfigPath, "utf8"));
|
|
18106
18336
|
return unsafe ? fail5("android.capacitor-config", "Android Capacitor config contains a development server URL, cleartext transport, navigation allowlist, or invalid JSON.", nativeConfigPath, "Run `absolute mobile sync android`; do not ship development transport overrides.") : pass("android.capacitor-config", "Android Capacitor config contains no development transport overrides.", nativeConfigPath);
|
|
18337
|
+
}, capacitorIdentityCheck = async (config, platform6, nativeConfigPath) => {
|
|
18338
|
+
try {
|
|
18339
|
+
const parsed = await readJsonObject(nativeConfigPath);
|
|
18340
|
+
if (parsed.appId !== config.appId || parsed.appName !== config.appName)
|
|
18341
|
+
throw new TypeError(`${platform6} packaged application identity does not match mobile config.`);
|
|
18342
|
+
return pass(`${platform6}.app-identity`, `Packaged ${platform6} application identity matches mobile config.`, nativeConfigPath);
|
|
18343
|
+
} catch (error) {
|
|
18344
|
+
return fail5(`${platform6}.app-identity`, error instanceof Error ? error.message : `${platform6} application identity could not be validated.`, nativeConfigPath, `Run \`absolute mobile sync ${platform6}\` and review the generated Capacitor config.`);
|
|
18345
|
+
}
|
|
18107
18346
|
}, manifestReleaseCheck = async (manifestPath) => {
|
|
18108
|
-
if (!await
|
|
18347
|
+
if (!await pathExists6(manifestPath)) {
|
|
18109
18348
|
return fail5("android.cleartext", "The Android manifest is missing.", manifestPath, "Run `absolute mobile sync android` before release validation.");
|
|
18110
18349
|
}
|
|
18111
|
-
const source = await
|
|
18350
|
+
const source = await readFile17(manifestPath, "utf8");
|
|
18112
18351
|
const cleartext = /android:usesCleartextTraffic=["']true["']/u.test(source);
|
|
18113
18352
|
const networkConfigName = source.match(/android:networkSecurityConfig=["']@xml\/([a-z0-9_]+)["']/u)?.[1];
|
|
18114
|
-
const networkConfigPath = networkConfigName ?
|
|
18353
|
+
const networkConfigPath = networkConfigName ? join50(dirname29(manifestPath), "res", "xml", `${networkConfigName}.xml`) : undefined;
|
|
18115
18354
|
const developmentTrustReference = /android:networkSecurityConfig=["']@xml\/absolutejs_dev_network_security["']/u.test(source);
|
|
18116
|
-
const developmentTrustContents = networkConfigPath ? await
|
|
18355
|
+
const developmentTrustContents = networkConfigPath ? await readFile17(networkConfigPath, "utf8").then((value) => value.includes("@raw/absolutejs_dev_ca")).catch(() => false) : false;
|
|
18117
18356
|
const developmentTrust = developmentTrustReference || developmentTrustContents;
|
|
18118
18357
|
return cleartext || developmentTrust ? fail5("android.cleartext", developmentTrust ? "Android still references the AbsoluteJS development certificate authority." : "Android explicitly permits cleartext traffic.", manifestPath, "Run `absolute mobile sync android`; do not ship development transport or trust overrides.") : pass("android.cleartext", "Android does not explicitly permit cleartext traffic.", manifestPath);
|
|
18119
18358
|
}, hmrAssetsReleaseCheck = async (publicRoot) => {
|
|
18120
18359
|
const hmrAsset = await findHmrAsset(publicRoot);
|
|
18121
18360
|
return hmrAsset ? fail5("android.hmr-assets", "A packaged Android asset contains the development HMR client.", hmrAsset, "Rebuild the production mobile bundle and run Capacitor sync again.") : pass("android.hmr-assets", "Packaged Android assets contain no development HMR markers.", publicRoot);
|
|
18361
|
+
}, embeddedBundleReleaseCheck = async (config, projectRoot, platform6, publicRoot) => {
|
|
18362
|
+
const inspection = await inspectAbsoluteMobileBundle({ ...config, bundleDirectory: publicRoot }, projectRoot);
|
|
18363
|
+
if (inspection.status === "valid")
|
|
18364
|
+
return pass(`${platform6}.bundle-integrity`, `Packaged mobile manifest, runtime, routes, and ${inspection.pageCount ?? 0} page asset(s) passed structural and SHA-256 validation.`, join50(publicRoot, "absolute-mobile-manifest.json"));
|
|
18365
|
+
return fail5(`${platform6}.bundle-integrity`, inspection.status === "missing" ? "The packaged mobile manifest is missing." : `The packaged mobile bundle is invalid: ${inspection.issue ?? "unknown validation error"}`, join50(publicRoot, "absolute-mobile-manifest.json"), "Rebuild the production mobile bundle and run Capacitor sync for this platform.");
|
|
18366
|
+
}, contentSecurityPolicyCheck = async (config, platform6, publicRoot) => {
|
|
18367
|
+
const path = join50(publicRoot, "index.html");
|
|
18368
|
+
try {
|
|
18369
|
+
const source = await readFile17(path, "utf8");
|
|
18370
|
+
const requirements = [
|
|
18371
|
+
"Content-Security-Policy",
|
|
18372
|
+
"default-src 'self'",
|
|
18373
|
+
"base-uri 'none'",
|
|
18374
|
+
"object-src 'none'",
|
|
18375
|
+
"script-src 'self'",
|
|
18376
|
+
"form-action 'none'",
|
|
18377
|
+
config.productionOrigin
|
|
18378
|
+
];
|
|
18379
|
+
const missing = requirements.filter((value) => !source.includes(value));
|
|
18380
|
+
if (missing.length > 0)
|
|
18381
|
+
throw new TypeError("Packaged shell CSP is missing a required AbsoluteJS directive or backend origin.");
|
|
18382
|
+
return pass(`${platform6}.content-security-policy`, "Packaged shell CSP restricts scripts, objects, base URLs, forms, and backend connections.", path);
|
|
18383
|
+
} catch (error) {
|
|
18384
|
+
return fail5(`${platform6}.content-security-policy`, error instanceof Error ? error.message : "Packaged shell CSP could not be validated.", path, "Rebuild the production mobile bundle with the AbsoluteJS-generated shell.");
|
|
18385
|
+
}
|
|
18386
|
+
}, sourceFiles = async (root, extensions) => {
|
|
18387
|
+
if (!await pathExists6(root))
|
|
18388
|
+
return [];
|
|
18389
|
+
const files = await Array.fromAsync(new Bun.Glob("**/*").scan({ cwd: root, onlyFiles: true }));
|
|
18390
|
+
return files.filter((file) => extensions.has(extname8(file))).map((file) => join50(root, file));
|
|
18391
|
+
}, containsPattern = async (paths, pattern) => {
|
|
18392
|
+
const sources = await Promise.all(paths.map((path) => readFile17(path, "utf8")));
|
|
18393
|
+
const index = sources.findIndex((source) => pattern.test(source));
|
|
18394
|
+
return index === NOT_FOUND3 ? undefined : paths[index];
|
|
18395
|
+
}, androidNativeSecurityCheck = async (androidRoot) => {
|
|
18396
|
+
const manifestPath = join50(androidRoot, "app/src/main/AndroidManifest.xml");
|
|
18397
|
+
try {
|
|
18398
|
+
const manifest = await readFile17(manifestPath, "utf8");
|
|
18399
|
+
if (/android:debuggable=["']true["']/u.test(manifest))
|
|
18400
|
+
throw new TypeError("Android release manifest explicitly enables application debugging.");
|
|
18401
|
+
const sources = await sourceFiles(join50(androidRoot, "app/src/main"), new Set([".java", ".kt"]));
|
|
18402
|
+
const debugSource = await containsPattern(sources, /setWebContentsDebuggingEnabled\s*\(\s*true\s*\)/u);
|
|
18403
|
+
if (debugSource)
|
|
18404
|
+
return fail5("android.native-debugging", "Android application source unconditionally enables WebView debugging.", debugSource, "Remove the unconditional WebView debugging call; use the platform debug-build behavior during development.");
|
|
18405
|
+
return pass("android.native-debugging", "Android does not explicitly enable app or WebView debugging in release source.", manifestPath);
|
|
18406
|
+
} catch (error) {
|
|
18407
|
+
return fail5("android.native-debugging", error instanceof Error ? error.message : "Android native debugging configuration could not be validated.", manifestPath, "Remove explicit release debugging settings and rerun mobile sync.");
|
|
18408
|
+
}
|
|
18409
|
+
}, androidExportedComponentsCheck = async (manifestPath) => {
|
|
18410
|
+
const source = await readFile17(manifestPath, "utf8").catch(() => "");
|
|
18411
|
+
const exported = [
|
|
18412
|
+
...source.matchAll(/<(?:activity|activity-alias|provider|receiver|service)\b[^>]*>/giu)
|
|
18413
|
+
].map(([tag]) => tag).filter((tag) => /android:exported=["']true["']/iu.test(tag)).map((tag) => tag.match(/android:name=["']([^"']+)["']/iu)?.[1]).filter((name) => Boolean(name) && name !== ".MainActivity");
|
|
18414
|
+
return exported.length === 0 ? pass("android.exported-components", "No non-launcher Android component is explicitly exported.", manifestPath) : warn("android.exported-components", `${exported.length} non-launcher Android component(s) are exported and require manual authorization review.`, manifestPath, "Confirm each exported component is intentional, permission-protected where appropriate, and documented in the mobile threat model review.");
|
|
18415
|
+
}, androidDeepLinkProjectionCheck = async (config, manifestPath) => {
|
|
18416
|
+
try {
|
|
18417
|
+
const source = await readFile17(manifestPath, "utf8");
|
|
18418
|
+
const required = [
|
|
18419
|
+
'android:autoVerify="true"',
|
|
18420
|
+
"android.intent.category.BROWSABLE",
|
|
18421
|
+
...config.deepLinkHosts.map((host2) => `android:scheme="https" android:host="${host2}"`),
|
|
18422
|
+
...config.deepLinkScheme ? [`android:scheme="${config.deepLinkScheme}"`] : []
|
|
18423
|
+
];
|
|
18424
|
+
if (required.some((value) => !source.includes(value)))
|
|
18425
|
+
throw new TypeError("Android App Link or custom-scheme projection does not match mobile config.");
|
|
18426
|
+
return pass("android.deep-links", "Android verified links and custom scheme match the effective mobile config.", manifestPath);
|
|
18427
|
+
} catch (error) {
|
|
18428
|
+
return fail5("android.deep-links", error instanceof Error ? error.message : "Android deep-link projection could not be validated.", manifestPath, "Run `absolute mobile sync android` and review the AbsoluteJS-owned deep-link region.");
|
|
18429
|
+
}
|
|
18430
|
+
}, iosNativeSecurityCheck = async (iosRoot) => {
|
|
18431
|
+
const entitlementsPath = join50(iosRoot, "App/AbsoluteJS.entitlements");
|
|
18432
|
+
const entitlements = await readFile17(entitlementsPath, "utf8").catch(() => "");
|
|
18433
|
+
if (/<key>get-task-allow<\/key>\s*<true\s*\/>/u.test(entitlements) || /<key>com\.apple\.security\.get-task-allow<\/key>\s*<true\s*\/>/u.test(entitlements))
|
|
18434
|
+
return fail5("ios.native-debugging", "iOS source entitlements explicitly permit debugger attachment.", entitlementsPath, "Remove get-task-allow from source entitlements; Xcode supplies development entitlements only to debug builds.");
|
|
18435
|
+
const sources = await sourceFiles(join50(iosRoot, "App"), new Set([".m", ".mm", ".swift"]));
|
|
18436
|
+
const debugSource = await containsPattern(sources, /\.isInspectable\s*=\s*true|setInspectable\s*\(\s*true\s*\)/u);
|
|
18437
|
+
if (debugSource)
|
|
18438
|
+
return fail5("ios.native-debugging", "iOS application source unconditionally enables WebView inspection.", debugSource, "Remove unconditional WebView inspection from release source.");
|
|
18439
|
+
return pass("ios.native-debugging", "iOS source does not enable release debugger attachment or WebView inspection.", entitlementsPath);
|
|
18440
|
+
}, iosDeepLinkProjectionCheck = async (config, iosRoot) => {
|
|
18441
|
+
const infoPath = join50(iosRoot, "App/App/Info.plist");
|
|
18442
|
+
const entitlementsPath = join50(iosRoot, "App/AbsoluteJS.entitlements");
|
|
18443
|
+
const projectPath = join50(iosRoot, "App/App.xcodeproj/project.pbxproj");
|
|
18444
|
+
try {
|
|
18445
|
+
const [info2, entitlements, project] = await Promise.all([
|
|
18446
|
+
readFile17(infoPath, "utf8"),
|
|
18447
|
+
readFile17(entitlementsPath, "utf8"),
|
|
18448
|
+
readFile17(projectPath, "utf8")
|
|
18449
|
+
]);
|
|
18450
|
+
if (config.deepLinkScheme && (!info2.includes("<key>CFBundleURLTypes</key>") || !info2.includes(`<string>${config.deepLinkScheme}</string>`)))
|
|
18451
|
+
throw new TypeError("iOS custom URL scheme does not match mobile config.");
|
|
18452
|
+
if (config.deepLinkHosts.some((host2) => !entitlements.includes(`<string>applinks:${host2}</string>`)))
|
|
18453
|
+
throw new TypeError("iOS associated domains do not match mobile config.");
|
|
18454
|
+
if (!project.includes("CODE_SIGN_ENTITLEMENTS = App/AbsoluteJS.entitlements;"))
|
|
18455
|
+
throw new TypeError("iOS target does not sign the AbsoluteJS entitlements file.");
|
|
18456
|
+
return pass("ios.deep-links", "iOS universal links, custom scheme, and signed entitlements match mobile config.", entitlementsPath);
|
|
18457
|
+
} catch (error) {
|
|
18458
|
+
return fail5("ios.deep-links", error instanceof Error ? error.message : "iOS deep-link projection could not be validated.", entitlementsPath, "Run `absolute mobile sync ios` and review the AbsoluteJS-owned deep-link and entitlement projection.");
|
|
18459
|
+
}
|
|
18122
18460
|
}, syncSchemaReleaseCheck = (projectRoot) => {
|
|
18123
18461
|
if (!projectUsesAbsoluteSync(projectRoot))
|
|
18124
18462
|
return;
|
|
18125
|
-
const manifestPath =
|
|
18463
|
+
const manifestPath = join50(projectRoot, "package.json");
|
|
18126
18464
|
try {
|
|
18127
18465
|
const schema = discoverAbsoluteSyncSchema(projectRoot);
|
|
18128
18466
|
const versions = schema.components.map((component2) => `${component2.id}@${component2.version}`).join(", ");
|
|
@@ -18144,8 +18482,8 @@ var HMR_ASSET_PATTERN, RELEASE_ASSET_EXTENSIONS, pathExists5 = async (path) => {
|
|
|
18144
18482
|
}, IOS_USAGE_KEYS, androidDevicePermissionCheck = async (config, permissions) => {
|
|
18145
18483
|
if (!config.platforms.includes("android") || permissions.length === 0)
|
|
18146
18484
|
return;
|
|
18147
|
-
const path =
|
|
18148
|
-
const source = await
|
|
18485
|
+
const path = join50(config.nativeProjectDirectory, "android/app/src/main/AndroidManifest.xml");
|
|
18486
|
+
const source = await readFile17(path, "utf8");
|
|
18149
18487
|
const missing = permissions.filter((permission) => !source.includes(`android:name="${permission}"`) && !source.includes(`android:name='${permission}'`));
|
|
18150
18488
|
if (missing.length === 0)
|
|
18151
18489
|
return;
|
|
@@ -18153,14 +18491,44 @@ var HMR_ASSET_PATTERN, RELEASE_ASSET_EXTENSIONS, pathExists5 = async (path) => {
|
|
|
18153
18491
|
}, iosDevicePermissionCheck = async (config, purposes) => {
|
|
18154
18492
|
if (!config.platforms.includes("ios") || purposes.length === 0)
|
|
18155
18493
|
return;
|
|
18156
|
-
const path =
|
|
18157
|
-
const source = await
|
|
18494
|
+
const path = join50(config.nativeProjectDirectory, "ios/App/App/Info.plist");
|
|
18495
|
+
const source = await readFile17(path, "utf8");
|
|
18158
18496
|
const missing = purposes.filter((purpose) => !source.includes(`<key>${IOS_USAGE_KEYS[purpose]}</key>`));
|
|
18159
18497
|
if (missing.length === 0)
|
|
18160
18498
|
return;
|
|
18161
18499
|
return fail5("mobile.device-capabilities", `iOS is missing usage descriptions for: ${missing.join(", ")}.`, path, "Run `absolute mobile sync ios` to regenerate detected device usage descriptions.");
|
|
18500
|
+
}, iosCapabilityProjectionCheck = async (config, requirements) => {
|
|
18501
|
+
if (!config.platforms.includes("ios"))
|
|
18502
|
+
return;
|
|
18503
|
+
const appRoot = join50(config.nativeProjectDirectory, "ios/App/App");
|
|
18504
|
+
const infoPath = join50(appRoot, "Info.plist");
|
|
18505
|
+
const info2 = await readFile17(infoPath, "utf8").catch(() => "");
|
|
18506
|
+
if (requirements.iosSystemBars && !/<key>UIViewControllerBasedStatusBarAppearance<\/key>\s*<true\s*\/>/u.test(info2))
|
|
18507
|
+
return fail5("mobile.device-capabilities", "iOS system-bar capability is missing its required view-controller setting.", infoPath, "Run `absolute mobile sync ios` to regenerate native capability settings.");
|
|
18508
|
+
if (requirements.iosPrivacyAccessedApis.length > 0) {
|
|
18509
|
+
const privacyPath = join50(appRoot, "PrivacyInfo.xcprivacy");
|
|
18510
|
+
const projectPath = join50(config.nativeProjectDirectory, "ios/App/App.xcodeproj/project.pbxproj");
|
|
18511
|
+
const [privacy, project] = await Promise.all([
|
|
18512
|
+
readFile17(privacyPath, "utf8").catch(() => ""),
|
|
18513
|
+
readFile17(projectPath, "utf8").catch(() => "")
|
|
18514
|
+
]);
|
|
18515
|
+
const missing = requirements.iosPrivacyAccessedApis.some(({ api, reasons }) => !privacy.includes(`<string>${api}</string>`) || reasons.some((reason) => !privacy.includes(`<string>${reason}</string>`)));
|
|
18516
|
+
if (missing || !project.includes("PrivacyInfo.xcprivacy in Resources"))
|
|
18517
|
+
return fail5("mobile.device-capabilities", "iOS privacy manifest or target membership does not match detected native capabilities.", privacyPath, "Run `absolute mobile sync ios` to regenerate and target PrivacyInfo.xcprivacy.");
|
|
18518
|
+
}
|
|
18519
|
+
if (requirements.iosPushNotifications) {
|
|
18520
|
+
const entitlementsPath = join50(config.nativeProjectDirectory, "ios/App/AbsoluteJS.entitlements");
|
|
18521
|
+
const delegatePath = join50(appRoot, "AppDelegate.swift");
|
|
18522
|
+
const [entitlements, delegate] = await Promise.all([
|
|
18523
|
+
readFile17(entitlementsPath, "utf8").catch(() => ""),
|
|
18524
|
+
readFile17(delegatePath, "utf8").catch(() => "")
|
|
18525
|
+
]);
|
|
18526
|
+
if (!entitlements.includes("<key>aps-environment</key>") || !delegate.includes("capacitorDidRegisterForRemoteNotifications") || !delegate.includes("capacitorDidFailToRegisterForRemoteNotifications"))
|
|
18527
|
+
return fail5("mobile.device-capabilities", "iOS push entitlement or AppDelegate forwarding does not match detected capabilities.", entitlementsPath, "Run `absolute mobile sync ios` to regenerate native push integration.");
|
|
18528
|
+
}
|
|
18529
|
+
return;
|
|
18162
18530
|
}, deviceCapabilityReleaseCheck = async (config, projectRoot) => {
|
|
18163
|
-
const manifestPath =
|
|
18531
|
+
const manifestPath = join50(projectRoot, "package.json");
|
|
18164
18532
|
try {
|
|
18165
18533
|
const plan = resolveAbsoluteDeviceCapabilityPlan(projectRoot);
|
|
18166
18534
|
assertAbsoluteDeviceCapabilityPackages(projectRoot, plan);
|
|
@@ -18171,32 +18539,41 @@ var HMR_ASSET_PATTERN, RELEASE_ASSET_EXTENSIONS, pathExists5 = async (path) => {
|
|
|
18171
18539
|
const iosCheck = await iosDevicePermissionCheck(config, requirements.iosUsageDescriptions);
|
|
18172
18540
|
if (iosCheck)
|
|
18173
18541
|
return iosCheck;
|
|
18542
|
+
const iosProjectionCheck = await iosCapabilityProjectionCheck(config, requirements);
|
|
18543
|
+
if (iosProjectionCheck)
|
|
18544
|
+
return iosProjectionCheck;
|
|
18174
18545
|
return pass("mobile.device-capabilities", plan.capabilities.length > 0 ? `Native provider packages and permission declarations match detected capabilities: ${plan.capabilities.join(", ")}.` : "No optional native device capabilities are used.", manifestPath);
|
|
18175
18546
|
} catch (error) {
|
|
18176
18547
|
return fail5("mobile.device-capabilities", error instanceof Error ? error.message : "Native device capability provisioning is invalid.", manifestPath, "Run `absolute mobile sync` and approve the exact capability plugins before releasing.");
|
|
18177
18548
|
}
|
|
18178
18549
|
}, inspectAndroidRelease = async (config, projectRoot) => {
|
|
18179
|
-
const androidRoot =
|
|
18180
|
-
const nativeConfigPath =
|
|
18181
|
-
const manifestPath =
|
|
18182
|
-
const publicRoot =
|
|
18183
|
-
const journalPath =
|
|
18550
|
+
const androidRoot = join50(config.nativeProjectDirectory, "android");
|
|
18551
|
+
const nativeConfigPath = join50(androidRoot, "app", "src", "main", "assets", "capacitor.config.json");
|
|
18552
|
+
const manifestPath = join50(androidRoot, "app", "src", "main", "AndroidManifest.xml");
|
|
18553
|
+
const publicRoot = join50(androidRoot, "app", "src", "main", "assets", "public");
|
|
18554
|
+
const journalPath = join50(projectRoot, ".absolutejs", "mobile", "dev-session", "journal.json");
|
|
18184
18555
|
const checks = await Promise.all([
|
|
18185
18556
|
journalReleaseCheck(journalPath, "android"),
|
|
18186
18557
|
capacitorConfigReleaseCheck(nativeConfigPath),
|
|
18558
|
+
capacitorIdentityCheck(config, "android", nativeConfigPath),
|
|
18187
18559
|
manifestReleaseCheck(manifestPath),
|
|
18188
|
-
hmrAssetsReleaseCheck(publicRoot)
|
|
18560
|
+
hmrAssetsReleaseCheck(publicRoot),
|
|
18561
|
+
embeddedBundleReleaseCheck(config, projectRoot, "android", publicRoot),
|
|
18562
|
+
contentSecurityPolicyCheck(config, "android", publicRoot),
|
|
18563
|
+
androidNativeSecurityCheck(androidRoot),
|
|
18564
|
+
androidExportedComponentsCheck(manifestPath),
|
|
18565
|
+
androidDeepLinkProjectionCheck(config, manifestPath)
|
|
18189
18566
|
]);
|
|
18190
18567
|
return checks.map((check2) => ({
|
|
18191
18568
|
...check2,
|
|
18192
|
-
path: check2.path ?
|
|
18569
|
+
path: check2.path ? relative25(projectRoot, check2.path).replaceAll("\\", "/") || "." : undefined
|
|
18193
18570
|
}));
|
|
18194
18571
|
}, inspectIosRelease = async (config, projectRoot) => {
|
|
18195
|
-
const iosAppRoot =
|
|
18196
|
-
const nativeConfigPath =
|
|
18197
|
-
const infoPath =
|
|
18198
|
-
const publicRoot =
|
|
18199
|
-
const journalPath =
|
|
18572
|
+
const iosAppRoot = join50(config.nativeProjectDirectory, "ios", "App", "App");
|
|
18573
|
+
const nativeConfigPath = join50(iosAppRoot, "capacitor.config.json");
|
|
18574
|
+
const infoPath = join50(iosAppRoot, "Info.plist");
|
|
18575
|
+
const publicRoot = join50(iosAppRoot, "public");
|
|
18576
|
+
const journalPath = join50(projectRoot, ".absolutejs", "mobile", "ios-dev-session", "journal.json");
|
|
18200
18577
|
const checks = [
|
|
18201
18578
|
await journalReleaseCheck(journalPath, "ios")
|
|
18202
18579
|
];
|
|
@@ -18205,27 +18582,59 @@ var HMR_ASSET_PATTERN, RELEASE_ASSET_EXTENSIONS, pathExists5 = async (path) => {
|
|
|
18205
18582
|
} else {
|
|
18206
18583
|
checks.push(pass("ios.marketing-version", `The iOS marketing version is ${config.iosVersion}.`));
|
|
18207
18584
|
}
|
|
18208
|
-
if (!await
|
|
18585
|
+
if (!await pathExists6(nativeConfigPath)) {
|
|
18209
18586
|
checks.push(fail5("ios.capacitor-config", "The generated iOS Capacitor config is missing.", nativeConfigPath, "Run `absolute mobile sync ios` before release validation."));
|
|
18210
|
-
} else if (isUnsafeCapacitorConfig(await
|
|
18587
|
+
} else if (isUnsafeCapacitorConfig(await readFile17(nativeConfigPath, "utf8"))) {
|
|
18211
18588
|
checks.push(fail5("ios.capacitor-config", "iOS Capacitor config contains a development server URL, cleartext transport, navigation allowlist, or invalid JSON.", nativeConfigPath, "Run `absolute mobile sync ios`; do not ship development transport overrides."));
|
|
18212
18589
|
} else {
|
|
18213
18590
|
checks.push(pass("ios.capacitor-config", "iOS Capacitor config contains no development transport overrides.", nativeConfigPath));
|
|
18214
18591
|
}
|
|
18215
|
-
|
|
18592
|
+
checks.push(await capacitorIdentityCheck(config, "ios", nativeConfigPath));
|
|
18593
|
+
if (!await pathExists6(infoPath)) {
|
|
18216
18594
|
checks.push(fail5("ios.transport-security", "The iOS Info.plist is missing.", infoPath, "Run `absolute mobile sync ios` before release validation."));
|
|
18217
18595
|
} else {
|
|
18218
|
-
const info2 = await
|
|
18596
|
+
const info2 = await readFile17(infoPath, "utf8");
|
|
18219
18597
|
checks.push(/<key>NSAllowsArbitraryLoads<\/key>\s*<true\s*\/>/u.test(info2) ? fail5("ios.transport-security", "iOS App Transport Security permits arbitrary network loads.", infoPath, "Remove NSAllowsArbitraryLoads from the release Info.plist.") : pass("ios.transport-security", "iOS App Transport Security does not permit arbitrary loads.", infoPath));
|
|
18220
18598
|
}
|
|
18221
18599
|
const hmrAsset = await findHmrAsset(publicRoot);
|
|
18222
18600
|
checks.push(hmrAsset ? fail5("ios.hmr-assets", "A packaged iOS asset contains the development HMR client.", hmrAsset, "Rebuild the production mobile bundle and run Capacitor sync again.") : pass("ios.hmr-assets", "Packaged iOS assets contain no development HMR markers.", publicRoot));
|
|
18601
|
+
checks.push(await embeddedBundleReleaseCheck(config, projectRoot, "ios", publicRoot), await contentSecurityPolicyCheck(config, "ios", publicRoot), await iosNativeSecurityCheck(join50(config.nativeProjectDirectory, "ios")), await iosDeepLinkProjectionCheck(config, join50(config.nativeProjectDirectory, "ios")));
|
|
18223
18602
|
return checks.map((check2) => ({
|
|
18224
18603
|
...check2,
|
|
18225
|
-
path: check2.path ?
|
|
18604
|
+
path: check2.path ? relative25(projectRoot, check2.path).replaceAll("\\", "/") || "." : undefined
|
|
18226
18605
|
}));
|
|
18606
|
+
}, createAbsoluteMobileComplianceReport = (config, result) => {
|
|
18607
|
+
const summary = {
|
|
18608
|
+
failed: result.checks.filter(({ status: status2 }) => status2 === "fail").length,
|
|
18609
|
+
passed: result.checks.filter(({ status: status2 }) => status2 === "pass").length,
|
|
18610
|
+
warnings: result.checks.filter(({ status: status2 }) => status2 === "warn").length
|
|
18611
|
+
};
|
|
18612
|
+
return {
|
|
18613
|
+
app: {
|
|
18614
|
+
appId: config.appId,
|
|
18615
|
+
engine: config.engine,
|
|
18616
|
+
platforms: [...config.platforms],
|
|
18617
|
+
productionOrigin: config.productionOrigin
|
|
18618
|
+
},
|
|
18619
|
+
checks: result.checks.map(({ id, status: status2 }) => ({ id, status: status2 })),
|
|
18620
|
+
format: ABSOLUTE_MOBILE_COMPLIANCE_REPORT_FORMAT,
|
|
18621
|
+
manualReview: [...MANUAL_REVIEW],
|
|
18622
|
+
ready: result.ready,
|
|
18623
|
+
summary
|
|
18624
|
+
};
|
|
18227
18625
|
}, inspectAbsoluteMobileRelease = async (config, projectRoot) => {
|
|
18228
|
-
const
|
|
18626
|
+
const globalChecks = await Promise.all([
|
|
18627
|
+
Promise.resolve(productionOriginCheck(config, projectRoot)),
|
|
18628
|
+
Promise.resolve(associationIdentityCheck(config, projectRoot)),
|
|
18629
|
+
dependencyLockCheck(projectRoot),
|
|
18630
|
+
capacitorVersionCheck(config, projectRoot)
|
|
18631
|
+
]);
|
|
18632
|
+
const checks = globalChecks.map((check2) => ({
|
|
18633
|
+
...check2,
|
|
18634
|
+
path: check2.path ? relative25(projectRoot, check2.path).replaceAll("\\", "/") || "." : undefined
|
|
18635
|
+
}));
|
|
18636
|
+
if (config.platforms.includes("android"))
|
|
18637
|
+
checks.push(...await inspectAndroidRelease(config, projectRoot));
|
|
18229
18638
|
if (config.platforms.includes("ios")) {
|
|
18230
18639
|
checks.push(...await inspectIosRelease(config, projectRoot));
|
|
18231
18640
|
}
|
|
@@ -18233,25 +18642,41 @@ var HMR_ASSET_PATTERN, RELEASE_ASSET_EXTENSIONS, pathExists5 = async (path) => {
|
|
|
18233
18642
|
if (syncSchema) {
|
|
18234
18643
|
checks.push({
|
|
18235
18644
|
...syncSchema,
|
|
18236
|
-
path: syncSchema.path ?
|
|
18645
|
+
path: syncSchema.path ? relative25(projectRoot, syncSchema.path).replaceAll("\\", "/") || "." : undefined
|
|
18237
18646
|
});
|
|
18238
18647
|
}
|
|
18239
18648
|
const deviceCapabilities = await deviceCapabilityReleaseCheck(config, projectRoot);
|
|
18240
18649
|
checks.push({
|
|
18241
18650
|
...deviceCapabilities,
|
|
18242
|
-
path: deviceCapabilities.path ?
|
|
18651
|
+
path: deviceCapabilities.path ? relative25(projectRoot, deviceCapabilities.path).replaceAll("\\", "/") || "." : undefined
|
|
18243
18652
|
});
|
|
18244
18653
|
return {
|
|
18245
18654
|
checks,
|
|
18246
|
-
ready: checks.length > 0 && checks.every((check2) => check2.status
|
|
18655
|
+
ready: checks.length > 0 && checks.every((check2) => check2.status !== "fail")
|
|
18247
18656
|
};
|
|
18248
18657
|
};
|
|
18249
18658
|
var init_releaseDoctor = __esm(() => {
|
|
18659
|
+
init_mobileBundleInspection();
|
|
18250
18660
|
init_nativeAuth();
|
|
18251
18661
|
init_syncSchema();
|
|
18252
18662
|
init_deviceCapabilities();
|
|
18253
18663
|
HMR_ASSET_PATTERN = /(?:__HMR_WS__|hmr-timing|__absolute_target|absolutejs-error-overlay)/u;
|
|
18254
18664
|
RELEASE_ASSET_EXTENSIONS = new Set([".html", ".js", ".mjs"]);
|
|
18665
|
+
EXACT_VERSION_PATTERN = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/u;
|
|
18666
|
+
LOCK_FILES = [
|
|
18667
|
+
"bun.lock",
|
|
18668
|
+
"bun.lockb",
|
|
18669
|
+
"package-lock.json",
|
|
18670
|
+
"pnpm-lock.yaml",
|
|
18671
|
+
"yarn.lock"
|
|
18672
|
+
];
|
|
18673
|
+
MANUAL_REVIEW = [
|
|
18674
|
+
"physical-device",
|
|
18675
|
+
"store-privacy-questionnaire",
|
|
18676
|
+
"privacy-policy",
|
|
18677
|
+
"signing-key-custody",
|
|
18678
|
+
"native-sdk-data-practices"
|
|
18679
|
+
];
|
|
18255
18680
|
IOS_USAGE_KEYS = {
|
|
18256
18681
|
camera: "NSCameraUsageDescription",
|
|
18257
18682
|
"location-always": "NSLocationAlwaysAndWhenInUseUsageDescription",
|
|
@@ -18262,19 +18687,19 @@ var init_releaseDoctor = __esm(() => {
|
|
|
18262
18687
|
});
|
|
18263
18688
|
|
|
18264
18689
|
// src/mobile/androidRelease.ts
|
|
18265
|
-
import { createHash as
|
|
18690
|
+
import { createHash as createHash13 } from "crypto";
|
|
18266
18691
|
import {
|
|
18267
|
-
access as
|
|
18692
|
+
access as access10,
|
|
18268
18693
|
copyFile as copyFile5,
|
|
18269
18694
|
mkdir as mkdir12,
|
|
18270
18695
|
mkdtemp as mkdtemp5,
|
|
18271
|
-
readFile as
|
|
18696
|
+
readFile as readFile18,
|
|
18272
18697
|
rename as rename12,
|
|
18273
18698
|
rm as rm8,
|
|
18274
|
-
stat as
|
|
18699
|
+
stat as stat3,
|
|
18275
18700
|
writeFile as writeFile14
|
|
18276
18701
|
} from "fs/promises";
|
|
18277
|
-
import { dirname as dirname30, isAbsolute as isAbsolute7, join as
|
|
18702
|
+
import { dirname as dirname30, isAbsolute as isAbsolute7, join as join51, relative as relative26, resolve as resolve40, sep as sep6 } from "path";
|
|
18278
18703
|
var ABSOLUTE_ANDROID_RELEASE_FORMAT = 1, isRecord13 = (value) => typeof value === "object" && value !== null && !Array.isArray(value), requireManifest2 = (value) => {
|
|
18279
18704
|
if (!isRecord13(value) || typeof value.appBuild !== "string" || typeof value.appId !== "string" || typeof value.runtime !== "string") {
|
|
18280
18705
|
throw new TypeError("Invalid embedded AbsoluteJS mobile manifest.");
|
|
@@ -18284,9 +18709,9 @@ var ABSOLUTE_ANDROID_RELEASE_FORMAT = 1, isRecord13 = (value) => typeof value ==
|
|
|
18284
18709
|
appId: value.appId,
|
|
18285
18710
|
runtime: value.runtime
|
|
18286
18711
|
};
|
|
18287
|
-
},
|
|
18712
|
+
}, pathExists7 = async (path) => {
|
|
18288
18713
|
try {
|
|
18289
|
-
await
|
|
18714
|
+
await access10(path);
|
|
18290
18715
|
return true;
|
|
18291
18716
|
} catch {
|
|
18292
18717
|
return false;
|
|
@@ -18324,22 +18749,36 @@ var ABSOLUTE_ANDROID_RELEASE_FORMAT = 1, isRecord13 = (value) => typeof value ==
|
|
|
18324
18749
|
artifactPath
|
|
18325
18750
|
]);
|
|
18326
18751
|
return result.exitCode === 0 && /jar verified/iu.test(result.stdout);
|
|
18327
|
-
},
|
|
18328
|
-
const
|
|
18329
|
-
|
|
18330
|
-
|
|
18752
|
+
}, signAab = (artifactPath, capture, jarsigner, signing) => {
|
|
18753
|
+
const result = capture([
|
|
18754
|
+
jarsigner,
|
|
18755
|
+
"-keystore",
|
|
18756
|
+
signing.keystorePath,
|
|
18757
|
+
"-storepass:env",
|
|
18758
|
+
signing.storePasswordEnvironment,
|
|
18759
|
+
"-keypass:env",
|
|
18760
|
+
signing.keyPasswordEnvironment,
|
|
18761
|
+
artifactPath,
|
|
18762
|
+
signing.keyAlias
|
|
18763
|
+
]);
|
|
18764
|
+
if (result.exitCode !== 0)
|
|
18765
|
+
throw new TypeError("jarsigner could not sign the Android App Bundle with the configured CI identity.");
|
|
18766
|
+
}, sha256File2 = async (path) => createHash13("sha256").update(await readFile18(path)).digest("hex"), safeOutputDirectory2 = (projectRoot, requested) => {
|
|
18767
|
+
const root = resolve40(projectRoot);
|
|
18768
|
+
const output = resolve40(root, requested ?? ".absolutejs/mobile/releases/android");
|
|
18769
|
+
const projectRelative = relative26(root, output);
|
|
18331
18770
|
if (projectRelative === ".." || projectRelative.startsWith(`..${sep6}`) || isAbsolute7(projectRelative)) {
|
|
18332
18771
|
throw new TypeError("mobile build --outdir must remain inside the project.");
|
|
18333
18772
|
}
|
|
18334
18773
|
return output;
|
|
18335
18774
|
}, installRelease2 = async (artifactPath, metadata, outputRoot) => {
|
|
18336
|
-
const releaseRoot =
|
|
18775
|
+
const releaseRoot = join51(outputRoot, metadata.releaseId);
|
|
18337
18776
|
const artifactName = "app-release.aab";
|
|
18338
|
-
const destination =
|
|
18339
|
-
if (await
|
|
18340
|
-
const existing = requireManifestIdentity(JSON.parse(await
|
|
18777
|
+
const destination = join51(releaseRoot, artifactName);
|
|
18778
|
+
if (await pathExists7(releaseRoot)) {
|
|
18779
|
+
const existing = requireManifestIdentity(JSON.parse(await readFile18(join51(releaseRoot, "release.json"), "utf8")), metadata);
|
|
18341
18780
|
const [installedBytes, installedSha256] = await Promise.all([
|
|
18342
|
-
|
|
18781
|
+
stat3(destination).then(({ size }) => size),
|
|
18343
18782
|
sha256File2(destination)
|
|
18344
18783
|
]);
|
|
18345
18784
|
if (installedBytes !== metadata.bytes || installedSha256 !== metadata.sha256) {
|
|
@@ -18348,14 +18787,14 @@ var ABSOLUTE_ANDROID_RELEASE_FORMAT = 1, isRecord13 = (value) => typeof value ==
|
|
|
18348
18787
|
return { artifactPath: destination, metadata: existing, releaseRoot };
|
|
18349
18788
|
}
|
|
18350
18789
|
await mkdir12(dirname30(releaseRoot), { recursive: true });
|
|
18351
|
-
const staging = await mkdtemp5(
|
|
18790
|
+
const staging = await mkdtemp5(join51(dirname30(releaseRoot), ".android-stage-"));
|
|
18352
18791
|
try {
|
|
18353
|
-
await copyFile5(artifactPath,
|
|
18792
|
+
await copyFile5(artifactPath, join51(staging, artifactName));
|
|
18354
18793
|
const complete = {
|
|
18355
18794
|
...metadata,
|
|
18356
18795
|
artifact: artifactName
|
|
18357
18796
|
};
|
|
18358
|
-
await writeFile14(
|
|
18797
|
+
await writeFile14(join51(staging, "release.json"), `${JSON.stringify(complete, null, "\t")}
|
|
18359
18798
|
`, { flag: "wx" });
|
|
18360
18799
|
await rename12(staging, releaseRoot);
|
|
18361
18800
|
return { artifactPath: destination, metadata: complete, releaseRoot };
|
|
@@ -18380,18 +18819,18 @@ var ABSOLUTE_ANDROID_RELEASE_FORMAT = 1, isRecord13 = (value) => typeof value ==
|
|
|
18380
18819
|
if (options.versionCode !== undefined && (!Number.isSafeInteger(options.versionCode) || options.versionCode < 1 || options.versionCode > 2100000000)) {
|
|
18381
18820
|
throw new TypeError("Android versionCode must be an integer from 1 through 2100000000.");
|
|
18382
18821
|
}
|
|
18383
|
-
const projectRoot =
|
|
18822
|
+
const projectRoot = resolve40(options.projectRoot);
|
|
18384
18823
|
const host2 = options.host ?? detectAbsoluteMobileHost();
|
|
18385
18824
|
const androidRoot = options.androidRoot ?? process.env.ANDROID_HOME ?? process.env.ANDROID_SDK_ROOT ?? absoluteManagedAndroidSdkRoot(host2);
|
|
18386
|
-
const nativeDirectory =
|
|
18387
|
-
const manifest = requireManifest2(JSON.parse(await
|
|
18825
|
+
const nativeDirectory = join51(options.config.nativeProjectDirectory, "android");
|
|
18826
|
+
const manifest = requireManifest2(JSON.parse(await readFile18(join51(options.config.bundleDirectory, "absolute-mobile-manifest.json"), "utf8")));
|
|
18388
18827
|
if (manifest.appId !== options.config.appId) {
|
|
18389
18828
|
throw new TypeError("Embedded mobile manifest appId does not match mobile.appId.");
|
|
18390
18829
|
}
|
|
18391
18830
|
let { versionCode } = options;
|
|
18392
18831
|
if (options.prepareVersionCode) {
|
|
18393
18832
|
const nativeFingerprint = await fingerprintAbsoluteAndroidNativeProject({ nativeDirectory });
|
|
18394
|
-
const buildIdentity =
|
|
18833
|
+
const buildIdentity = createHash13("sha256").update(`${manifest.appBuild}\x00${nativeFingerprint}`).digest("hex");
|
|
18395
18834
|
versionCode = await options.prepareVersionCode(buildIdentity);
|
|
18396
18835
|
}
|
|
18397
18836
|
if (versionCode !== undefined && (!Number.isSafeInteger(versionCode) || versionCode < 1 || versionCode > 2100000000)) {
|
|
@@ -18410,11 +18849,20 @@ var ABSOLUTE_ANDROID_RELEASE_FORMAT = 1, isRecord13 = (value) => typeof value ==
|
|
|
18410
18849
|
run: options.run,
|
|
18411
18850
|
task: "bundleRelease"
|
|
18412
18851
|
});
|
|
18413
|
-
if (!await
|
|
18852
|
+
if (!await pathExists7(artifactPath)) {
|
|
18414
18853
|
throw new TypeError(`Android Gradle did not produce the expected App Bundle: ${artifactPath}`);
|
|
18415
18854
|
}
|
|
18416
18855
|
const capture = options.capture ?? defaultCapture4;
|
|
18417
|
-
const
|
|
18856
|
+
const jarsigner = options.jarsigner === undefined ? Bun.which("jarsigner") : options.jarsigner;
|
|
18857
|
+
let signed2 = verifyAabSignature(artifactPath, capture, jarsigner);
|
|
18858
|
+
if (signed2 === false && options.signing) {
|
|
18859
|
+
if (!jarsigner)
|
|
18860
|
+
throw new TypeError("Could not sign the Android App Bundle because jarsigner is unavailable.");
|
|
18861
|
+
signAab(artifactPath, capture, jarsigner, options.signing);
|
|
18862
|
+
signed2 = verifyAabSignature(artifactPath, capture, jarsigner);
|
|
18863
|
+
if (!signed2)
|
|
18864
|
+
throw new TypeError("Android App Bundle signature verification failed after CI signing.");
|
|
18865
|
+
}
|
|
18418
18866
|
if (signed2 === null && !options.allowUnsigned) {
|
|
18419
18867
|
throw new TypeError("Could not verify the Android App Bundle signature because jarsigner is unavailable. Install a JDK, or use --unsigned only for a non-publishable build.");
|
|
18420
18868
|
}
|
|
@@ -18422,7 +18870,7 @@ var ABSOLUTE_ANDROID_RELEASE_FORMAT = 1, isRecord13 = (value) => typeof value ==
|
|
|
18422
18870
|
throw new TypeError("Android Gradle produced an unsigned App Bundle. Configure the release signingConfig in the source-owned Android project (prefer external Gradle properties), or pass --unsigned only for a non-publishable build.");
|
|
18423
18871
|
}
|
|
18424
18872
|
const [bytes, sha2562] = await Promise.all([
|
|
18425
|
-
|
|
18873
|
+
stat3(artifactPath).then(({ size }) => size),
|
|
18426
18874
|
sha256File2(artifactPath)
|
|
18427
18875
|
]);
|
|
18428
18876
|
const releaseId = `amobile_android_${sha2562}`;
|
|
@@ -18486,7 +18934,7 @@ var absoluteIosDeviceAcceptanceCommands = (options) => {
|
|
|
18486
18934
|
};
|
|
18487
18935
|
|
|
18488
18936
|
// src/mobile/iosConformance.ts
|
|
18489
|
-
import { readFile as
|
|
18937
|
+
import { readFile as readFile19, stat as stat4 } from "fs/promises";
|
|
18490
18938
|
var HMR_LINE, parseAbsoluteIosHmrLog = (line) => {
|
|
18491
18939
|
const match = HMR_LINE.exec(line);
|
|
18492
18940
|
if (!match)
|
|
@@ -18515,13 +18963,13 @@ var HMR_LINE, parseAbsoluteIosHmrLog = (line) => {
|
|
|
18515
18963
|
const sleep2 = options.sleep ?? Bun.sleep;
|
|
18516
18964
|
const timeoutMs = options.timeoutMs ?? 30000;
|
|
18517
18965
|
const deadline = Date.now() + timeoutMs;
|
|
18518
|
-
let offset = options.startOffset ?? await
|
|
18966
|
+
let offset = options.startOffset ?? await stat4(options.logPath).then(({ size }) => size).catch(() => 0);
|
|
18519
18967
|
let buffered = "";
|
|
18520
18968
|
const poll = async () => {
|
|
18521
18969
|
if (Date.now() > deadline)
|
|
18522
18970
|
throw new Error(`No iOS native HMR acknowledgement was observed within ${timeoutMs}ms.`);
|
|
18523
18971
|
options.signal?.throwIfAborted();
|
|
18524
|
-
const contents = await
|
|
18972
|
+
const contents = await readFile19(options.logPath).catch(() => Buffer.alloc(0));
|
|
18525
18973
|
if (contents.byteLength < offset) {
|
|
18526
18974
|
offset = 0;
|
|
18527
18975
|
buffered = "";
|
|
@@ -18545,8 +18993,8 @@ var init_iosConformance = __esm(() => {
|
|
|
18545
18993
|
});
|
|
18546
18994
|
|
|
18547
18995
|
// src/mobile/nativeTestReport.ts
|
|
18548
|
-
import { mkdir as mkdir13, readFile as
|
|
18549
|
-
import { join as
|
|
18996
|
+
import { mkdir as mkdir13, readFile as readFile20, writeFile as writeFile15 } from "fs/promises";
|
|
18997
|
+
import { join as join52 } from "path";
|
|
18550
18998
|
var secretPattern, bearerPattern, coordinatePattern, nativeCredentialPattern, sanitizeNativeReportText = (value) => value.replace(nativeCredentialPattern, "[REDACTED]").replace(bearerPattern, "Bearer [REDACTED]").replace(secretPattern, "$1$2[REDACTED]").replace(coordinatePattern, "$1$2[REDACTED]").replace(/(https?:\/\/[^\s?#]+)[?#][^\s]*/giu, "$1?[REDACTED]"), markdownCell = (value) => sanitizeNativeReportText(value).replaceAll("|", "\\|").replaceAll(`
|
|
18551
18999
|
`, "<br>"), createAbsoluteNativeAutomatedChecks = (run) => {
|
|
18552
19000
|
const target = `${run.targetKind} ${run.targetId}`;
|
|
@@ -18648,7 +19096,7 @@ var secretPattern, bearerPattern, coordinatePattern, nativeCredentialPattern, sa
|
|
|
18648
19096
|
reportVersion: 1,
|
|
18649
19097
|
run: options.run
|
|
18650
19098
|
}), readPackageVersionForNativeReport = async (packageJsonPath) => {
|
|
18651
|
-
const manifest = JSON.parse(await
|
|
19099
|
+
const manifest = JSON.parse(await readFile20(packageJsonPath, "utf8"));
|
|
18652
19100
|
if (typeof manifest !== "object" || manifest === null)
|
|
18653
19101
|
return "unknown";
|
|
18654
19102
|
const version2 = Reflect.get(manifest, "version");
|
|
@@ -18685,8 +19133,8 @@ ${table(report.manualChecks)}
|
|
|
18685
19133
|
`;
|
|
18686
19134
|
}, writeAbsoluteNativeTestReport = async (directory, report) => {
|
|
18687
19135
|
await mkdir13(directory, { recursive: true });
|
|
18688
|
-
const jsonPath =
|
|
18689
|
-
const markdownPath =
|
|
19136
|
+
const jsonPath = join52(directory, "report.json");
|
|
19137
|
+
const markdownPath = join52(directory, "report.md");
|
|
18690
19138
|
await Promise.all([
|
|
18691
19139
|
writeFile15(jsonPath, `${JSON.stringify(report, null, 2)}
|
|
18692
19140
|
`),
|
|
@@ -18904,8 +19352,8 @@ var init_androidTestReport = __esm(() => {
|
|
|
18904
19352
|
});
|
|
18905
19353
|
|
|
18906
19354
|
// src/mobile/releasePublisher.ts
|
|
18907
|
-
import { access as
|
|
18908
|
-
import { isAbsolute as isAbsolute8, relative as
|
|
19355
|
+
import { access as access11 } from "fs/promises";
|
|
19356
|
+
import { isAbsolute as isAbsolute8, relative as relative27, resolve as resolve41, sep as sep7 } from "path";
|
|
18909
19357
|
import { pathToFileURL as pathToFileURL2 } from "url";
|
|
18910
19358
|
var prepareAbsoluteIosRelease = async (publisher, options) => {
|
|
18911
19359
|
if (typeof publisher.prepareIosRelease !== "function") {
|
|
@@ -18927,16 +19375,16 @@ var prepareAbsoluteIosRelease = async (publisher, options) => {
|
|
|
18927
19375
|
}
|
|
18928
19376
|
return versionCode;
|
|
18929
19377
|
}, isRecord14 = (value) => typeof value === "object" && value !== null && !Array.isArray(value), isPublisher = (value) => isRecord14(value) && typeof value.publish === "function", publisherModulePath = (projectRoot, requested) => {
|
|
18930
|
-
const root =
|
|
18931
|
-
const path =
|
|
18932
|
-
const projectRelative =
|
|
19378
|
+
const root = resolve41(projectRoot);
|
|
19379
|
+
const path = resolve41(root, requested);
|
|
19380
|
+
const projectRelative = relative27(root, path);
|
|
18933
19381
|
if (projectRelative === ".." || projectRelative.startsWith(`..${sep7}`) || isAbsolute8(projectRelative)) {
|
|
18934
19382
|
throw new TypeError("mobile publish --registry must remain inside the project.");
|
|
18935
19383
|
}
|
|
18936
19384
|
return path;
|
|
18937
19385
|
}, loadAbsoluteNativeReleasePublisher = async (projectRoot, requestedModulePath) => {
|
|
18938
19386
|
const modulePath = publisherModulePath(projectRoot, requestedModulePath);
|
|
18939
|
-
await
|
|
19387
|
+
await access11(modulePath).catch(() => {
|
|
18940
19388
|
throw new TypeError(`Native release registry module does not exist: ${modulePath}`);
|
|
18941
19389
|
});
|
|
18942
19390
|
const loaded = await import(pathToFileURL2(modulePath).href);
|
|
@@ -18997,133 +19445,36 @@ var prepareAbsoluteIosRelease = async (publisher, options) => {
|
|
|
18997
19445
|
var init_releasePublisher = () => {};
|
|
18998
19446
|
|
|
18999
19447
|
// src/mobile/mobileInspect.ts
|
|
19000
|
-
import { access as
|
|
19001
|
-
import { join as
|
|
19002
|
-
var ABSOLUTE_MOBILE_INSPECTION_FORMAT = 1, MOBILE_PACKAGE_NAMES,
|
|
19003
|
-
const value =
|
|
19448
|
+
import { access as access12, readFile as readFile21 } from "fs/promises";
|
|
19449
|
+
import { join as join53, relative as relative28, resolve as resolve42 } from "path";
|
|
19450
|
+
var ABSOLUTE_MOBILE_INSPECTION_FORMAT = 1, MOBILE_PACKAGE_NAMES, isObject3 = (value) => typeof value === "object" && value !== null && !Array.isArray(value), portablePath2 = (projectRoot, path) => {
|
|
19451
|
+
const value = relative28(resolve42(projectRoot), resolve42(path)).replaceAll("\\", "/");
|
|
19004
19452
|
return value || ".";
|
|
19005
|
-
},
|
|
19453
|
+
}, pathExists8 = async (path) => {
|
|
19006
19454
|
try {
|
|
19007
|
-
await
|
|
19455
|
+
await access12(path);
|
|
19008
19456
|
return true;
|
|
19009
19457
|
} catch {
|
|
19010
19458
|
return false;
|
|
19011
19459
|
}
|
|
19012
|
-
},
|
|
19013
|
-
const value = JSON.parse(await
|
|
19014
|
-
if (!
|
|
19460
|
+
}, readObject2 = async (path) => {
|
|
19461
|
+
const value = JSON.parse(await readFile21(path, "utf8"));
|
|
19462
|
+
if (!isObject3(value))
|
|
19015
19463
|
throw new TypeError("JSON root must be an object.");
|
|
19016
19464
|
return value;
|
|
19017
|
-
}, requireString = (value, field) => {
|
|
19018
|
-
if (typeof value !== "string" || value.length === 0)
|
|
19019
|
-
throw new TypeError(`${field} must be a non-empty string.`);
|
|
19020
|
-
return value;
|
|
19021
|
-
}, requireStringArray = (value, field) => {
|
|
19022
|
-
if (!Array.isArray(value) || !value.every((item) => typeof item === "string"))
|
|
19023
|
-
throw new TypeError(`${field} must be a string array.`);
|
|
19024
|
-
return value;
|
|
19025
|
-
}, requireBundleFile = async (root, value, field) => {
|
|
19026
|
-
const portable = requireString(value, field);
|
|
19027
|
-
const path = resolve41(root, portable);
|
|
19028
|
-
const normalizedRoot = resolve41(root);
|
|
19029
|
-
if (path === normalizedRoot || !path.startsWith(`${normalizedRoot}/`))
|
|
19030
|
-
throw new TypeError(`${field} must remain inside the mobile bundle.`);
|
|
19031
|
-
if (!(await stat4(path).catch(() => {
|
|
19032
|
-
return;
|
|
19033
|
-
}))?.isFile())
|
|
19034
|
-
throw new TypeError(`${field} does not exist in the mobile bundle.`);
|
|
19035
|
-
return portable;
|
|
19036
|
-
}, inspectBundle = async (config, projectRoot) => {
|
|
19037
|
-
const manifestPath = join52(config.bundleDirectory, "absolute-mobile-manifest.json");
|
|
19038
|
-
const manifest = portablePath(projectRoot, manifestPath);
|
|
19039
|
-
if (!await pathExists7(manifestPath))
|
|
19040
|
-
return { manifest, status: "missing" };
|
|
19041
|
-
try {
|
|
19042
|
-
const value = await readObject(manifestPath);
|
|
19043
|
-
if (value.format !== 1)
|
|
19044
|
-
throw new TypeError("format is not supported by this runtime.");
|
|
19045
|
-
if (requireString(value.appId, "appId") !== config.appId)
|
|
19046
|
-
throw new TypeError("appId does not match the effective mobile config.");
|
|
19047
|
-
if (requireString(value.productionOrigin, "productionOrigin") !== config.productionOrigin)
|
|
19048
|
-
throw new TypeError("productionOrigin does not match the effective mobile config.");
|
|
19049
|
-
const appBuild = requireString(value.appBuild, "appBuild");
|
|
19050
|
-
const runtime = requireString(value.runtime, "runtime");
|
|
19051
|
-
const capabilities = requireStringArray(value.deviceCapabilities, "deviceCapabilities").sort();
|
|
19052
|
-
if (!Array.isArray(value.pages) || !Array.isArray(value.routes))
|
|
19053
|
-
throw new TypeError("pages and routes must be arrays.");
|
|
19054
|
-
const pageIds = new Set;
|
|
19055
|
-
const frameworks7 = new Set;
|
|
19056
|
-
await Promise.all(value.pages.map(async (candidate) => {
|
|
19057
|
-
if (!isObject2(candidate))
|
|
19058
|
-
throw new TypeError("pages contains an invalid entry.");
|
|
19059
|
-
const pageId = requireString(candidate.pageId, "page.pageId");
|
|
19060
|
-
if (pageIds.has(pageId))
|
|
19061
|
-
throw new TypeError("page.pageId values must be unique.");
|
|
19062
|
-
pageIds.add(pageId);
|
|
19063
|
-
const framework = requireString(candidate.framework, "page.framework");
|
|
19064
|
-
if (!MOBILE_FRAMEWORKS.has(framework))
|
|
19065
|
-
throw new TypeError("page.framework is unsupported.");
|
|
19066
|
-
frameworks7.add(framework);
|
|
19067
|
-
requireString(candidate.bundleHash, "page.bundleHash");
|
|
19068
|
-
requireString(candidate.contract, "page.contract");
|
|
19069
|
-
requireString(candidate.propsSchemaHash, "page.propsSchemaHash");
|
|
19070
|
-
await requireBundleFile(config.bundleDirectory, candidate.localBundlePath, "page.localBundlePath");
|
|
19071
|
-
if (candidate.localStylePath !== undefined)
|
|
19072
|
-
await requireBundleFile(config.bundleDirectory, candidate.localStylePath, "page.localStylePath");
|
|
19073
|
-
}));
|
|
19074
|
-
const routes = value.routes.map((candidate) => {
|
|
19075
|
-
if (!isObject2(candidate))
|
|
19076
|
-
throw new TypeError("routes contains an invalid entry.");
|
|
19077
|
-
const { method } = candidate;
|
|
19078
|
-
if (method !== "GET" && method !== "HEAD")
|
|
19079
|
-
throw new TypeError("route.method must be GET or HEAD.");
|
|
19080
|
-
const pageId = requireString(candidate.pageId, "route.pageId");
|
|
19081
|
-
if (!pageIds.has(pageId))
|
|
19082
|
-
throw new TypeError("route.pageId references a missing page.");
|
|
19083
|
-
return {
|
|
19084
|
-
method,
|
|
19085
|
-
pageId,
|
|
19086
|
-
pattern: requireString(candidate.pattern, "route.pattern")
|
|
19087
|
-
};
|
|
19088
|
-
});
|
|
19089
|
-
await Promise.all(["index.html", "absolute-mobile-bootstrap.js"].map((file) => requireBundleFile(config.bundleDirectory, file, file)));
|
|
19090
|
-
const entryPath = new URL(config.entry, "https://absolute.invalid").pathname;
|
|
19091
|
-
const entryResolved = resolveAbsoluteMobileRoute(routes, entryPath) !== undefined;
|
|
19092
|
-
if (!entryResolved)
|
|
19093
|
-
throw new TypeError("entry is not owned by an embedded route.");
|
|
19094
|
-
return {
|
|
19095
|
-
appBuild,
|
|
19096
|
-
auth: isObject2(value.auth),
|
|
19097
|
-
capabilities,
|
|
19098
|
-
entryResolved,
|
|
19099
|
-
frameworks: [...frameworks7].sort(),
|
|
19100
|
-
manifest,
|
|
19101
|
-
pageCount: value.pages.length,
|
|
19102
|
-
routeCount: value.routes.length,
|
|
19103
|
-
runtime,
|
|
19104
|
-
status: "valid",
|
|
19105
|
-
sync: isObject2(value.sync)
|
|
19106
|
-
};
|
|
19107
|
-
} catch (error) {
|
|
19108
|
-
return {
|
|
19109
|
-
issue: error instanceof Error ? error.message : "The embedded mobile manifest is invalid.",
|
|
19110
|
-
manifest,
|
|
19111
|
-
status: "invalid"
|
|
19112
|
-
};
|
|
19113
|
-
}
|
|
19114
19465
|
}, addPackageDeclarations = (declarations, value) => {
|
|
19115
|
-
if (!
|
|
19466
|
+
if (!isObject3(value))
|
|
19116
19467
|
return;
|
|
19117
19468
|
for (const [name, declared] of Object.entries(value).filter((entry) => typeof entry[1] === "string"))
|
|
19118
19469
|
declarations.set(name, declared);
|
|
19119
19470
|
}, packageInspections = async (projectRoot, additionalNames) => {
|
|
19120
|
-
const project = await
|
|
19471
|
+
const project = await readObject2(join53(projectRoot, "package.json"));
|
|
19121
19472
|
const declarations = new Map;
|
|
19122
19473
|
for (const field of ["dependencies", "devDependencies"])
|
|
19123
19474
|
addPackageDeclarations(declarations, project[field]);
|
|
19124
19475
|
const names = [...new Set([...declarations.keys(), ...additionalNames])].filter((name) => MOBILE_PACKAGE_NAMES.has(name) || name.startsWith("@capacitor/") || additionalNames.includes(name)).sort();
|
|
19125
19476
|
return Promise.all(names.map(async (name) => {
|
|
19126
|
-
const installedManifest = await
|
|
19477
|
+
const installedManifest = await readObject2(join53(projectRoot, "node_modules", name, "package.json")).catch(() => {
|
|
19127
19478
|
return;
|
|
19128
19479
|
});
|
|
19129
19480
|
const installed = installedManifest?.version;
|
|
@@ -19134,7 +19485,7 @@ var ABSOLUTE_MOBILE_INSPECTION_FORMAT = 1, MOBILE_PACKAGE_NAMES, MOBILE_FRAMEWOR
|
|
|
19134
19485
|
};
|
|
19135
19486
|
}));
|
|
19136
19487
|
}, inspectAbsoluteMobileProject = async (config, projectRoot, options = {}) => {
|
|
19137
|
-
const bundle = await
|
|
19488
|
+
const bundle = await inspectAbsoluteMobileBundle(config, projectRoot);
|
|
19138
19489
|
let currentCapabilities = [];
|
|
19139
19490
|
let capabilityIssue;
|
|
19140
19491
|
let plugins = [];
|
|
@@ -19160,22 +19511,22 @@ var ABSOLUTE_MOBILE_INSPECTION_FORMAT = 1, MOBILE_PACKAGE_NAMES, MOBILE_FRAMEWOR
|
|
|
19160
19511
|
config: {
|
|
19161
19512
|
appId: config.appId,
|
|
19162
19513
|
appName: config.appName,
|
|
19163
|
-
bundleDirectory:
|
|
19514
|
+
bundleDirectory: portablePath2(projectRoot, config.bundleDirectory),
|
|
19164
19515
|
deepLinkHosts: config.deepLinkHosts,
|
|
19165
19516
|
deepLinkScheme: config.deepLinkScheme,
|
|
19166
19517
|
engine: config.engine,
|
|
19167
19518
|
entry: config.entry,
|
|
19168
19519
|
iosVersion: config.iosVersion,
|
|
19169
|
-
nativeProjectDirectory:
|
|
19520
|
+
nativeProjectDirectory: portablePath2(projectRoot, config.nativeProjectDirectory),
|
|
19170
19521
|
platforms: config.platforms,
|
|
19171
19522
|
productionOrigin: config.productionOrigin
|
|
19172
19523
|
},
|
|
19173
19524
|
format: ABSOLUTE_MOBILE_INSPECTION_FORMAT,
|
|
19174
19525
|
nativeProjects: await Promise.all(config.platforms.map(async (platform6) => {
|
|
19175
|
-
const path =
|
|
19526
|
+
const path = join53(config.nativeProjectDirectory, platform6);
|
|
19176
19527
|
return {
|
|
19177
|
-
initialized: await
|
|
19178
|
-
path:
|
|
19528
|
+
initialized: await pathExists8(path),
|
|
19529
|
+
path: portablePath2(projectRoot, path),
|
|
19179
19530
|
platform: platform6
|
|
19180
19531
|
};
|
|
19181
19532
|
})),
|
|
@@ -19227,7 +19578,7 @@ var ABSOLUTE_MOBILE_INSPECTION_FORMAT = 1, MOBILE_PACKAGE_NAMES, MOBILE_FRAMEWOR
|
|
|
19227
19578
|
var init_mobileInspect = __esm(() => {
|
|
19228
19579
|
init_deviceCapabilities();
|
|
19229
19580
|
init_releaseDoctor();
|
|
19230
|
-
|
|
19581
|
+
init_mobileBundleInspection();
|
|
19231
19582
|
MOBILE_PACKAGE_NAMES = new Set([
|
|
19232
19583
|
"@absolutejs/absolute",
|
|
19233
19584
|
"@absolutejs/auth",
|
|
@@ -19239,15 +19590,484 @@ var init_mobileInspect = __esm(() => {
|
|
|
19239
19590
|
"@absolutejs/sync-capacitor",
|
|
19240
19591
|
"@capacitor-community/sqlite"
|
|
19241
19592
|
]);
|
|
19242
|
-
|
|
19243
|
-
|
|
19244
|
-
|
|
19245
|
-
|
|
19246
|
-
|
|
19247
|
-
|
|
19248
|
-
|
|
19249
|
-
|
|
19593
|
+
});
|
|
19594
|
+
|
|
19595
|
+
// src/mobile/ciWorkflow.ts
|
|
19596
|
+
import { existsSync as existsSync42 } from "fs";
|
|
19597
|
+
import { access as access13, mkdir as mkdir14, readFile as readFile22, writeFile as writeFile16 } from "fs/promises";
|
|
19598
|
+
import { dirname as dirname31, extname as extname9, relative as relative29, resolve as resolve43, sep as sep8 } from "path";
|
|
19599
|
+
var ABSOLUTE_MOBILE_CI_WORKFLOW_FORMAT = 1, SECRET_NAME_PATTERN, CI_ENV_INDENTATION = 6, RESERVED_SECRET_NAMES, exists3 = async (path) => {
|
|
19600
|
+
try {
|
|
19601
|
+
await access13(path);
|
|
19602
|
+
return true;
|
|
19603
|
+
} catch {
|
|
19604
|
+
return false;
|
|
19605
|
+
}
|
|
19606
|
+
}, yamlString = (value) => `'${value.replaceAll("'", "''")}'`, projectPath = (projectRoot, value, field, options = {}) => {
|
|
19607
|
+
const root = resolve43(projectRoot);
|
|
19608
|
+
const path = resolve43(root, value);
|
|
19609
|
+
const portable = relative29(root, path).replaceAll("\\", "/");
|
|
19610
|
+
if (portable === ".." || portable.startsWith(`..${sep8}`) || portable.startsWith("../") || portable === "") {
|
|
19611
|
+
throw new TypeError(`${field} must remain inside the project root.`);
|
|
19612
|
+
}
|
|
19613
|
+
if (/\r|\n/u.test(portable) || portable.startsWith("-"))
|
|
19614
|
+
throw new TypeError(`${field} contains an unsafe path.`);
|
|
19615
|
+
if (!options.allowMissing && !existsSync42(path))
|
|
19616
|
+
throw new TypeError(`${field} does not exist inside the project.`);
|
|
19617
|
+
return portable;
|
|
19618
|
+
}, workflowOutputPath = (projectRoot, value) => {
|
|
19619
|
+
const root = resolve43(projectRoot);
|
|
19620
|
+
const workflows = resolve43(root, ".github/workflows");
|
|
19621
|
+
const path = resolve43(root, value ?? ".github/workflows/absolute-mobile.yml");
|
|
19622
|
+
const portable = relative29(workflows, path);
|
|
19623
|
+
if (portable === ".." || portable.startsWith(`..${sep8}`) || extname9(path) !== ".yml" && extname9(path) !== ".yaml") {
|
|
19624
|
+
throw new TypeError("mobile ci github --output must be a .yml or .yaml file inside .github/workflows.");
|
|
19625
|
+
}
|
|
19626
|
+
return path;
|
|
19627
|
+
}, normalizeSecretEnvironment = (values = []) => {
|
|
19628
|
+
const names = [...new Set(values)].sort();
|
|
19629
|
+
for (const name of names) {
|
|
19630
|
+
if (!SECRET_NAME_PATTERN.test(name))
|
|
19631
|
+
throw new TypeError("mobile ci github --secret-env values must be uppercase environment variable names.");
|
|
19632
|
+
if (name.startsWith("GITHUB_") || name.startsWith("RUNNER_") || name.startsWith("ACTIONS_") || RESERVED_SECRET_NAMES.has(name)) {
|
|
19633
|
+
throw new TypeError(`mobile ci github --secret-env cannot replace reserved variable ${name}.`);
|
|
19634
|
+
}
|
|
19635
|
+
}
|
|
19636
|
+
return names;
|
|
19637
|
+
}, customSecretEnvironment = (names, indentation = CI_ENV_INDENTATION) => names.map((name) => `${" ".repeat(indentation)}${name}: \${{ secrets.${name} }}`).join(`
|
|
19638
|
+
`), commandEnvironment = (options) => ` ABSOLUTE_CONFIG_PATH: ${yamlString(options.configPath ?? "")}
|
|
19639
|
+
ABSOLUTE_REGISTRY_MODULE: ${yamlString(options.registryModule)}
|
|
19640
|
+
ABSOLUTE_SERVER_ENTRY: ${yamlString(options.serverEntry)}`, appendConfigArgument = `if [[ -n "$ABSOLUTE_CONFIG_PATH" ]]; then
|
|
19641
|
+
args+=(--config "$ABSOLUTE_CONFIG_PATH")
|
|
19642
|
+
fi`, installSteps = ` - name: Check out source
|
|
19643
|
+
uses: actions/checkout@v6
|
|
19644
|
+
- name: Install Bun
|
|
19645
|
+
uses: oven-sh/setup-bun@v2
|
|
19646
|
+
- name: Install exact dependencies
|
|
19647
|
+
run: bun ci`, bundleAuditSteps, releaseAuditSteps = (platform6) => ` - name: Run redacted mobile release audit
|
|
19648
|
+
id: mobile-release-audit
|
|
19649
|
+
continue-on-error: true
|
|
19650
|
+
shell: bash
|
|
19651
|
+
run: |
|
|
19652
|
+
mkdir -p .absolutejs/mobile-ci
|
|
19653
|
+
args=(bunx absolute mobile doctor release ${platform6} --json)
|
|
19654
|
+
${appendConfigArgument}
|
|
19655
|
+
"\${args[@]}" > .absolutejs/mobile-ci/compliance.json
|
|
19656
|
+
- name: Upload mobile compliance report
|
|
19657
|
+
if: always()
|
|
19658
|
+
uses: actions/upload-artifact@v7
|
|
19659
|
+
with:
|
|
19660
|
+
name: absolute-mobile-compliance-\${{ github.job }}
|
|
19661
|
+
path: .absolutejs/mobile-ci/compliance.json
|
|
19662
|
+
if-no-files-found: error
|
|
19663
|
+
retention-days: 30
|
|
19664
|
+
include-hidden-files: true
|
|
19665
|
+
- name: Enforce mobile release audit
|
|
19666
|
+
if: steps.mobile-release-audit.outcome != 'success'
|
|
19667
|
+
run: exit 1`, platformInput = (platforms) => {
|
|
19668
|
+
const choices = platforms.length === 2 ? ["all", ...platforms] : platforms;
|
|
19669
|
+
return ` platform:
|
|
19670
|
+
description: Native platform to build
|
|
19671
|
+
required: true
|
|
19672
|
+
type: choice
|
|
19673
|
+
default: ${choices[0]}
|
|
19674
|
+
options:
|
|
19675
|
+
${choices.map((value) => ` - ${value}`).join(`
|
|
19676
|
+
`)}`;
|
|
19677
|
+
}, publishingInputs = (platforms, includePublishing) => {
|
|
19678
|
+
if (!includePublishing)
|
|
19679
|
+
return "";
|
|
19680
|
+
const fields = [
|
|
19681
|
+
` publish:
|
|
19682
|
+
description: Publish through mobile.release.ts after the signed build
|
|
19683
|
+
required: true
|
|
19684
|
+
type: boolean
|
|
19685
|
+
default: false`,
|
|
19686
|
+
` channel:
|
|
19687
|
+
description: Optional AbsoluteJS immutable release channel
|
|
19688
|
+
required: false
|
|
19689
|
+
type: string`
|
|
19690
|
+
];
|
|
19691
|
+
if (platforms.includes("android"))
|
|
19692
|
+
fields.push(` play_track:
|
|
19693
|
+
description: Optional Google Play track
|
|
19694
|
+
required: true
|
|
19695
|
+
type: choice
|
|
19696
|
+
default: registry-only
|
|
19697
|
+
options:
|
|
19698
|
+
- registry-only
|
|
19699
|
+
- internal
|
|
19700
|
+
- alpha
|
|
19701
|
+
- beta
|
|
19702
|
+
- production`);
|
|
19703
|
+
if (platforms.includes("ios")) {
|
|
19704
|
+
fields.push(` testflight_group:
|
|
19705
|
+
description: Optional internal or external TestFlight group
|
|
19706
|
+
required: false
|
|
19707
|
+
type: string`);
|
|
19708
|
+
fields.push(` submit_testflight_review:
|
|
19709
|
+
description: Explicitly submit an external TestFlight build for review
|
|
19710
|
+
required: true
|
|
19711
|
+
type: boolean
|
|
19712
|
+
default: false`);
|
|
19713
|
+
}
|
|
19714
|
+
return `
|
|
19715
|
+
${fields.join(`
|
|
19716
|
+
`)}`;
|
|
19717
|
+
}, jobCondition = (platform6) => `github.event_name == 'workflow_dispatch' && (inputs.platform == 'all' || inputs.platform == '${platform6}')`, androidJob = (options) => {
|
|
19718
|
+
const custom = customSecretEnvironment(options.customSecrets);
|
|
19719
|
+
const publishEnvironment = options.includePublishing ? `
|
|
19720
|
+
ABSOLUTE_PUBLISH: \${{ inputs.publish }}
|
|
19721
|
+
ABSOLUTE_RELEASE_CHANNEL: \${{ inputs.channel }}
|
|
19722
|
+
ABSOLUTE_PLAY_TRACK: \${{ inputs.play_track }}
|
|
19723
|
+
ABSOLUTE_GOOGLE_CREDENTIALS_BASE64: \${{ secrets.ABSOLUTE_GOOGLE_CREDENTIALS_BASE64 }}
|
|
19724
|
+
GOOGLE_APPLICATION_CREDENTIALS: \${{ runner.temp }}/absolute-google-credentials.json` : "";
|
|
19725
|
+
const publishCommand = options.includePublishing ? `if [[ "$ABSOLUTE_PUBLISH" == "true" ]]; then
|
|
19726
|
+
args=(bunx absolute mobile publish android "$ABSOLUTE_SERVER_ENTRY" --registry "$ABSOLUTE_REGISTRY_MODULE")
|
|
19727
|
+
if [[ -n "$ABSOLUTE_RELEASE_CHANNEL" ]]; then
|
|
19728
|
+
args+=(--channel "$ABSOLUTE_RELEASE_CHANNEL")
|
|
19729
|
+
fi
|
|
19730
|
+
if [[ "$ABSOLUTE_PLAY_TRACK" != "registry-only" ]]; then
|
|
19731
|
+
args+=(--play-track "$ABSOLUTE_PLAY_TRACK")
|
|
19732
|
+
fi
|
|
19733
|
+
else
|
|
19734
|
+
args=(bunx absolute mobile build android "$ABSOLUTE_SERVER_ENTRY")
|
|
19735
|
+
fi` : `args=(bunx absolute mobile build android "$ABSOLUTE_SERVER_ENTRY")`;
|
|
19736
|
+
const googleSetup = options.includePublishing ? `
|
|
19737
|
+
if [[ "$ABSOLUTE_PUBLISH" == "true" && "$ABSOLUTE_PLAY_TRACK" != "registry-only" ]]; then
|
|
19738
|
+
if [[ -z "$ABSOLUTE_GOOGLE_CREDENTIALS_BASE64" ]]; then
|
|
19739
|
+
echo "ABSOLUTE_GOOGLE_CREDENTIALS_BASE64 is required for Google Play publication." >&2
|
|
19740
|
+
exit 1
|
|
19741
|
+
fi
|
|
19742
|
+
printf '%s' "$ABSOLUTE_GOOGLE_CREDENTIALS_BASE64" | base64 --decode > "$GOOGLE_APPLICATION_CREDENTIALS"
|
|
19743
|
+
chmod 600 "$GOOGLE_APPLICATION_CREDENTIALS"
|
|
19744
|
+
fi` : "";
|
|
19745
|
+
return `
|
|
19746
|
+
android:
|
|
19747
|
+
name: Signed Android release
|
|
19748
|
+
needs: validate
|
|
19749
|
+
if: \${{ ${jobCondition("android")} }}
|
|
19750
|
+
runs-on: ubuntu-latest
|
|
19751
|
+
environment: absolute-mobile-release
|
|
19752
|
+
permissions:
|
|
19753
|
+
contents: read
|
|
19754
|
+
id-token: write
|
|
19755
|
+
attestations: write
|
|
19756
|
+
env:
|
|
19757
|
+
ABSOLUTE_ANDROID_KEYSTORE_BASE64: \${{ secrets.ABSOLUTE_ANDROID_KEYSTORE_BASE64 }}
|
|
19758
|
+
ABSOLUTE_ANDROID_KEYSTORE_PASSWORD: \${{ secrets.ABSOLUTE_ANDROID_KEYSTORE_PASSWORD }}
|
|
19759
|
+
ABSOLUTE_ANDROID_KEY_ALIAS: \${{ secrets.ABSOLUTE_ANDROID_KEY_ALIAS }}
|
|
19760
|
+
ABSOLUTE_ANDROID_KEY_PASSWORD: \${{ secrets.ABSOLUTE_ANDROID_KEY_PASSWORD }}
|
|
19761
|
+
ABSOLUTE_ANDROID_KEYSTORE_PATH: \${{ runner.temp }}/absolute-release.jks${publishEnvironment}${custom ? `
|
|
19762
|
+
${custom}` : ""}
|
|
19763
|
+
${commandEnvironment({ configPath: undefined, registryModule: "", serverEntry: "" })}
|
|
19764
|
+
steps:
|
|
19765
|
+
${installSteps}
|
|
19766
|
+
- name: Provision Android signing
|
|
19767
|
+
shell: bash
|
|
19768
|
+
run: |
|
|
19769
|
+
required=(
|
|
19770
|
+
ABSOLUTE_ANDROID_KEYSTORE_BASE64
|
|
19771
|
+
ABSOLUTE_ANDROID_KEYSTORE_PASSWORD
|
|
19772
|
+
ABSOLUTE_ANDROID_KEY_ALIAS
|
|
19773
|
+
ABSOLUTE_ANDROID_KEY_PASSWORD
|
|
19774
|
+
)
|
|
19775
|
+
for name in "\${required[@]}"; do
|
|
19776
|
+
if [[ -z "\${!name}" ]]; then
|
|
19777
|
+
echo "$name is required in the absolute-mobile-release environment." >&2
|
|
19778
|
+
exit 1
|
|
19779
|
+
fi
|
|
19780
|
+
done
|
|
19781
|
+
printf '%s' "$ABSOLUTE_ANDROID_KEYSTORE_BASE64" | base64 --decode > "\${{ runner.temp }}/absolute-release.jks"
|
|
19782
|
+
chmod 600 "\${{ runner.temp }}/absolute-release.jks"${googleSetup}
|
|
19783
|
+
- name: Build or publish Android
|
|
19784
|
+
shell: bash
|
|
19785
|
+
run: |
|
|
19786
|
+
${publishCommand}
|
|
19787
|
+
${appendConfigArgument}
|
|
19788
|
+
"\${args[@]}"
|
|
19789
|
+
${releaseAuditSteps("android")}
|
|
19790
|
+
- name: Attest Android App Bundle
|
|
19791
|
+
if: inputs.attest
|
|
19792
|
+
uses: actions/attest@v4
|
|
19793
|
+
with:
|
|
19794
|
+
subject-path: .absolutejs/mobile/releases/android/**/app-release.aab
|
|
19795
|
+
- name: Upload Android release
|
|
19796
|
+
uses: actions/upload-artifact@v7
|
|
19797
|
+
with:
|
|
19798
|
+
name: absolute-mobile-android
|
|
19799
|
+
path: .absolutejs/mobile/releases/android/
|
|
19800
|
+
if-no-files-found: error
|
|
19801
|
+
retention-days: 14
|
|
19802
|
+
include-hidden-files: true
|
|
19803
|
+
- name: Remove Android credentials
|
|
19804
|
+
if: always()
|
|
19805
|
+
shell: bash
|
|
19806
|
+
run: |
|
|
19807
|
+
rm -f "\${{ runner.temp }}/absolute-release.jks"
|
|
19808
|
+
rm -f "\${{ runner.temp }}/absolute-google-credentials.json"`;
|
|
19809
|
+
}, iosJob = (options) => {
|
|
19810
|
+
const custom = customSecretEnvironment(options.customSecrets);
|
|
19811
|
+
const publishEnvironment = options.includePublishing ? `
|
|
19812
|
+
ABSOLUTE_PUBLISH: \${{ inputs.publish }}
|
|
19813
|
+
ABSOLUTE_RELEASE_CHANNEL: \${{ inputs.channel }}
|
|
19814
|
+
ABSOLUTE_TESTFLIGHT_GROUP: \${{ inputs.testflight_group }}
|
|
19815
|
+
ABSOLUTE_TESTFLIGHT_SUBMIT_REVIEW: \${{ inputs.submit_testflight_review }}
|
|
19816
|
+
APP_STORE_CONNECT_ISSUER_ID: \${{ secrets.APP_STORE_CONNECT_ISSUER_ID }}
|
|
19817
|
+
APP_STORE_CONNECT_KEY_ID: \${{ secrets.APP_STORE_CONNECT_KEY_ID }}
|
|
19818
|
+
APP_STORE_CONNECT_PRIVATE_KEY_BASE64: \${{ secrets.APP_STORE_CONNECT_PRIVATE_KEY_BASE64 }}
|
|
19819
|
+
APP_STORE_CONNECT_PRIVATE_KEY_PATH: \${{ runner.temp }}/AuthKey_AbsoluteJS.p8` : "";
|
|
19820
|
+
const publishCommand = options.includePublishing ? `if [[ "$ABSOLUTE_PUBLISH" == "true" ]]; then
|
|
19821
|
+
args=(bunx absolute mobile publish ios "$ABSOLUTE_SERVER_ENTRY" --registry "$ABSOLUTE_REGISTRY_MODULE")
|
|
19822
|
+
if [[ -n "$ABSOLUTE_RELEASE_CHANNEL" ]]; then
|
|
19823
|
+
args+=(--channel "$ABSOLUTE_RELEASE_CHANNEL")
|
|
19824
|
+
fi
|
|
19825
|
+
if [[ -n "$ABSOLUTE_TESTFLIGHT_GROUP" ]]; then
|
|
19826
|
+
args+=(--testflight-group "$ABSOLUTE_TESTFLIGHT_GROUP")
|
|
19827
|
+
fi
|
|
19828
|
+
if [[ "$ABSOLUTE_TESTFLIGHT_SUBMIT_REVIEW" == "true" ]]; then
|
|
19829
|
+
args+=(--testflight-submit-review)
|
|
19830
|
+
fi
|
|
19831
|
+
else
|
|
19832
|
+
args=(bunx absolute mobile build ios "$ABSOLUTE_SERVER_ENTRY")
|
|
19833
|
+
fi` : `args=(bunx absolute mobile build ios "$ABSOLUTE_SERVER_ENTRY")`;
|
|
19834
|
+
const appStoreSetup = options.includePublishing ? `
|
|
19835
|
+
if [[ "$ABSOLUTE_PUBLISH" == "true" && -n "$ABSOLUTE_TESTFLIGHT_GROUP" ]]; then
|
|
19836
|
+
required+=(APP_STORE_CONNECT_ISSUER_ID APP_STORE_CONNECT_KEY_ID APP_STORE_CONNECT_PRIVATE_KEY_BASE64)
|
|
19837
|
+
fi` : "";
|
|
19838
|
+
const appStoreDecode = options.includePublishing ? `
|
|
19839
|
+
if [[ "$ABSOLUTE_PUBLISH" == "true" && -n "$ABSOLUTE_TESTFLIGHT_GROUP" ]]; then
|
|
19840
|
+
printf '%s' "$APP_STORE_CONNECT_PRIVATE_KEY_BASE64" | base64 --decode > "$APP_STORE_CONNECT_PRIVATE_KEY_PATH"
|
|
19841
|
+
chmod 600 "$APP_STORE_CONNECT_PRIVATE_KEY_PATH"
|
|
19842
|
+
fi` : "";
|
|
19843
|
+
return `
|
|
19844
|
+
ios:
|
|
19845
|
+
name: Signed iOS release
|
|
19846
|
+
needs: validate
|
|
19847
|
+
if: \${{ ${jobCondition("ios")} }}
|
|
19848
|
+
runs-on: macos-latest
|
|
19849
|
+
environment: absolute-mobile-release
|
|
19850
|
+
permissions:
|
|
19851
|
+
contents: read
|
|
19852
|
+
id-token: write
|
|
19853
|
+
attestations: write
|
|
19854
|
+
env:
|
|
19855
|
+
ABSOLUTE_IOS_CERTIFICATE_BASE64: \${{ secrets.ABSOLUTE_IOS_CERTIFICATE_BASE64 }}
|
|
19856
|
+
ABSOLUTE_IOS_CERTIFICATE_PASSWORD: \${{ secrets.ABSOLUTE_IOS_CERTIFICATE_PASSWORD }}
|
|
19857
|
+
ABSOLUTE_IOS_PROVISIONING_PROFILE_BASE64: \${{ secrets.ABSOLUTE_IOS_PROVISIONING_PROFILE_BASE64 }}
|
|
19858
|
+
ABSOLUTE_IOS_KEYCHAIN_PASSWORD: \${{ secrets.ABSOLUTE_IOS_KEYCHAIN_PASSWORD }}${publishEnvironment}${custom ? `
|
|
19859
|
+
${custom}` : ""}
|
|
19860
|
+
ABSOLUTE_IOS_DEVELOPMENT_TEAM: \${{ secrets.ABSOLUTE_IOS_DEVELOPMENT_TEAM }}
|
|
19861
|
+
${commandEnvironment({ configPath: undefined, registryModule: "", serverEntry: "" })}
|
|
19862
|
+
steps:
|
|
19863
|
+
${installSteps}
|
|
19864
|
+
- name: Provision iOS signing
|
|
19865
|
+
shell: bash
|
|
19866
|
+
run: |
|
|
19867
|
+
required=(
|
|
19868
|
+
ABSOLUTE_IOS_CERTIFICATE_BASE64
|
|
19869
|
+
ABSOLUTE_IOS_CERTIFICATE_PASSWORD
|
|
19870
|
+
ABSOLUTE_IOS_PROVISIONING_PROFILE_BASE64
|
|
19871
|
+
ABSOLUTE_IOS_KEYCHAIN_PASSWORD
|
|
19872
|
+
ABSOLUTE_IOS_DEVELOPMENT_TEAM
|
|
19873
|
+
)${appStoreSetup}
|
|
19874
|
+
for name in "\${required[@]}"; do
|
|
19875
|
+
if [[ -z "\${!name}" ]]; then
|
|
19876
|
+
echo "$name is required in the absolute-mobile-release environment." >&2
|
|
19877
|
+
exit 1
|
|
19878
|
+
fi
|
|
19879
|
+
done
|
|
19880
|
+
CERTIFICATE_PATH="\${{ runner.temp }}/absolute-signing.p12"
|
|
19881
|
+
PROFILE_PATH="\${{ runner.temp }}/absolute.mobileprovision"
|
|
19882
|
+
KEYCHAIN_PATH="\${{ runner.temp }}/absolute-signing.keychain-db"
|
|
19883
|
+
PROFILE_DESTINATION="$HOME/Library/MobileDevice/Provisioning Profiles/absolute.mobileprovision"
|
|
19884
|
+
printf '%s' "$ABSOLUTE_IOS_CERTIFICATE_BASE64" | base64 --decode > "$CERTIFICATE_PATH"
|
|
19885
|
+
printf '%s' "$ABSOLUTE_IOS_PROVISIONING_PROFILE_BASE64" | base64 --decode > "$PROFILE_PATH"
|
|
19886
|
+
security create-keychain -p "$ABSOLUTE_IOS_KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
|
|
19887
|
+
security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH"
|
|
19888
|
+
security unlock-keychain -p "$ABSOLUTE_IOS_KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
|
|
19889
|
+
security import "$CERTIFICATE_PATH" -P "$ABSOLUTE_IOS_CERTIFICATE_PASSWORD" -A -t cert -f pkcs12 -k "$KEYCHAIN_PATH"
|
|
19890
|
+
security set-key-partition-list -S apple-tool:,apple: -k "$ABSOLUTE_IOS_KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
|
|
19891
|
+
security list-keychain -d user -s "$KEYCHAIN_PATH"
|
|
19892
|
+
mkdir -p "$(dirname "$PROFILE_DESTINATION")"
|
|
19893
|
+
cp "$PROFILE_PATH" "$PROFILE_DESTINATION"${appStoreDecode}
|
|
19894
|
+
- name: Build or publish iOS
|
|
19895
|
+
shell: bash
|
|
19896
|
+
run: |
|
|
19897
|
+
${publishCommand}
|
|
19898
|
+
${appendConfigArgument}
|
|
19899
|
+
"\${args[@]}"
|
|
19900
|
+
${releaseAuditSteps("ios")}
|
|
19901
|
+
- name: Attest iOS IPA
|
|
19902
|
+
if: inputs.attest
|
|
19903
|
+
uses: actions/attest@v4
|
|
19904
|
+
with:
|
|
19905
|
+
subject-path: .absolutejs/mobile/releases/ios/**/App.ipa
|
|
19906
|
+
- name: Upload iOS release
|
|
19907
|
+
uses: actions/upload-artifact@v7
|
|
19908
|
+
with:
|
|
19909
|
+
name: absolute-mobile-ios
|
|
19910
|
+
path: .absolutejs/mobile/releases/ios/
|
|
19911
|
+
if-no-files-found: error
|
|
19912
|
+
retention-days: 14
|
|
19913
|
+
include-hidden-files: true
|
|
19914
|
+
- name: Remove iOS credentials
|
|
19915
|
+
if: always()
|
|
19916
|
+
shell: bash
|
|
19917
|
+
run: |
|
|
19918
|
+
security delete-keychain "\${{ runner.temp }}/absolute-signing.keychain-db" 2>/dev/null || true
|
|
19919
|
+
rm -f "$HOME/Library/MobileDevice/Provisioning Profiles/absolute.mobileprovision"
|
|
19920
|
+
rm -f "\${{ runner.temp }}/absolute-signing.p12"
|
|
19921
|
+
rm -f "\${{ runner.temp }}/absolute.mobileprovision"
|
|
19922
|
+
rm -f "\${{ runner.temp }}/AuthKey_AbsoluteJS.p8"`;
|
|
19923
|
+
}, requiredSecrets = (platforms, includePublishing, custom) => [
|
|
19924
|
+
...platforms.includes("android") ? [
|
|
19925
|
+
"ABSOLUTE_ANDROID_KEYSTORE_BASE64",
|
|
19926
|
+
"ABSOLUTE_ANDROID_KEYSTORE_PASSWORD",
|
|
19927
|
+
"ABSOLUTE_ANDROID_KEY_ALIAS",
|
|
19928
|
+
"ABSOLUTE_ANDROID_KEY_PASSWORD",
|
|
19929
|
+
...includePublishing ? ["ABSOLUTE_GOOGLE_CREDENTIALS_BASE64"] : []
|
|
19930
|
+
] : [],
|
|
19931
|
+
...platforms.includes("ios") ? [
|
|
19932
|
+
"ABSOLUTE_IOS_CERTIFICATE_BASE64",
|
|
19933
|
+
"ABSOLUTE_IOS_CERTIFICATE_PASSWORD",
|
|
19934
|
+
"ABSOLUTE_IOS_PROVISIONING_PROFILE_BASE64",
|
|
19935
|
+
"ABSOLUTE_IOS_KEYCHAIN_PASSWORD",
|
|
19936
|
+
"ABSOLUTE_IOS_DEVELOPMENT_TEAM",
|
|
19937
|
+
...includePublishing ? [
|
|
19938
|
+
"APP_STORE_CONNECT_ISSUER_ID",
|
|
19939
|
+
"APP_STORE_CONNECT_KEY_ID",
|
|
19940
|
+
"APP_STORE_CONNECT_PRIVATE_KEY_BASE64"
|
|
19941
|
+
] : []
|
|
19942
|
+
] : [],
|
|
19943
|
+
...custom
|
|
19944
|
+
], createAbsoluteMobileGithubWorkflow = (options) => {
|
|
19945
|
+
const platforms = [
|
|
19946
|
+
...options.config.platforms
|
|
19947
|
+
].sort();
|
|
19948
|
+
const includePublishing = options.includePublishing === true;
|
|
19949
|
+
const customSecrets = normalizeSecretEnvironment(options.secretEnvironment);
|
|
19950
|
+
const serverEntry = projectPath(options.projectRoot, options.serverEntry ?? "server.ts", "mobile ci github server entry");
|
|
19951
|
+
const configPath2 = options.configPath ? projectPath(options.projectRoot, options.configPath, "mobile ci github --config") : undefined;
|
|
19952
|
+
const registryModule = projectPath(options.projectRoot, options.registryModule ?? "mobile.release.ts", "mobile ci github --registry", { allowMissing: !includePublishing });
|
|
19953
|
+
const environment = commandEnvironment({
|
|
19954
|
+
configPath: configPath2,
|
|
19955
|
+
registryModule,
|
|
19956
|
+
serverEntry
|
|
19957
|
+
});
|
|
19958
|
+
let workflow = `# Generated by AbsoluteJS. Regenerate with: absolute mobile ci github${includePublishing ? " --publish" : ""}
|
|
19959
|
+
name: AbsoluteJS Mobile
|
|
19960
|
+
|
|
19961
|
+
on:
|
|
19962
|
+
pull_request:
|
|
19963
|
+
workflow_dispatch:
|
|
19964
|
+
inputs:
|
|
19965
|
+
${platformInput(platforms)}
|
|
19966
|
+
attest:
|
|
19967
|
+
description: Generate GitHub artifact provenance attestations
|
|
19968
|
+
required: true
|
|
19969
|
+
type: boolean
|
|
19970
|
+
default: false${publishingInputs(platforms, includePublishing)}
|
|
19971
|
+
|
|
19972
|
+
concurrency:
|
|
19973
|
+
group: absolute-mobile-\${{ github.repository }}
|
|
19974
|
+
cancel-in-progress: false
|
|
19975
|
+
|
|
19976
|
+
jobs:
|
|
19977
|
+
validate:
|
|
19978
|
+
name: Validate mobile release inputs
|
|
19979
|
+
runs-on: ubuntu-latest
|
|
19980
|
+
permissions:
|
|
19981
|
+
contents: read
|
|
19982
|
+
env:
|
|
19983
|
+
${environment}
|
|
19984
|
+
steps:
|
|
19985
|
+
${installSteps}
|
|
19986
|
+
${bundleAuditSteps}${platforms.includes("android") ? androidJob({ customSecrets, includePublishing }) : ""}${platforms.includes("ios") ? iosJob({ customSecrets, includePublishing }) : ""}
|
|
19987
|
+
`;
|
|
19988
|
+
const replacements = new Map([
|
|
19989
|
+
[
|
|
19990
|
+
"ABSOLUTE_CONFIG_PATH: ''",
|
|
19991
|
+
`ABSOLUTE_CONFIG_PATH: ${yamlString(configPath2 ?? "")}`
|
|
19992
|
+
],
|
|
19993
|
+
[
|
|
19994
|
+
"ABSOLUTE_REGISTRY_MODULE: ''",
|
|
19995
|
+
`ABSOLUTE_REGISTRY_MODULE: ${yamlString(registryModule)}`
|
|
19996
|
+
],
|
|
19997
|
+
[
|
|
19998
|
+
"ABSOLUTE_SERVER_ENTRY: ''",
|
|
19999
|
+
`ABSOLUTE_SERVER_ENTRY: ${yamlString(serverEntry)}`
|
|
20000
|
+
]
|
|
20001
|
+
]);
|
|
20002
|
+
for (const [placeholder, replacement] of replacements)
|
|
20003
|
+
workflow = workflow.replaceAll(placeholder, replacement);
|
|
20004
|
+
return {
|
|
20005
|
+
requiredSecrets: requiredSecrets(platforms, includePublishing, customSecrets),
|
|
20006
|
+
workflow
|
|
20007
|
+
};
|
|
20008
|
+
}, writeAbsoluteMobileGithubWorkflow = async (options) => {
|
|
20009
|
+
const path = workflowOutputPath(options.projectRoot, options.outputPath);
|
|
20010
|
+
const generated = createAbsoluteMobileGithubWorkflow(options);
|
|
20011
|
+
const previous = await exists3(path) ? await readFile22(path, "utf8") : undefined;
|
|
20012
|
+
if (previous !== undefined && previous !== generated.workflow && !options.force)
|
|
20013
|
+
throw new TypeError(`${relative29(options.projectRoot, path)} already exists and differs. Rerun with --force to replace the generated workflow.`);
|
|
20014
|
+
if (previous !== generated.workflow) {
|
|
20015
|
+
await mkdir14(dirname31(path), { recursive: true });
|
|
20016
|
+
await writeFile16(path, generated.workflow);
|
|
20017
|
+
}
|
|
20018
|
+
return {
|
|
20019
|
+
changed: previous !== generated.workflow,
|
|
20020
|
+
format: ABSOLUTE_MOBILE_CI_WORKFLOW_FORMAT,
|
|
20021
|
+
path,
|
|
20022
|
+
platforms: [...options.config.platforms].sort(),
|
|
20023
|
+
publishing: options.includePublishing === true,
|
|
20024
|
+
requiredSecrets: generated.requiredSecrets
|
|
20025
|
+
};
|
|
20026
|
+
};
|
|
20027
|
+
var init_ciWorkflow = __esm(() => {
|
|
20028
|
+
SECRET_NAME_PATTERN = /^[A-Z][A-Z0-9_]*$/u;
|
|
20029
|
+
RESERVED_SECRET_NAMES = new Set([
|
|
20030
|
+
"ABSOLUTE_ANDROID_KEYSTORE_BASE64",
|
|
20031
|
+
"ABSOLUTE_ANDROID_KEYSTORE_PASSWORD",
|
|
20032
|
+
"ABSOLUTE_ANDROID_KEY_ALIAS",
|
|
20033
|
+
"ABSOLUTE_ANDROID_KEY_PASSWORD",
|
|
20034
|
+
"ABSOLUTE_GOOGLE_CREDENTIALS_BASE64",
|
|
20035
|
+
"ABSOLUTE_IOS_CERTIFICATE_BASE64",
|
|
20036
|
+
"ABSOLUTE_IOS_CERTIFICATE_PASSWORD",
|
|
20037
|
+
"ABSOLUTE_IOS_PROVISIONING_PROFILE_BASE64",
|
|
20038
|
+
"ABSOLUTE_IOS_KEYCHAIN_PASSWORD",
|
|
20039
|
+
"ABSOLUTE_IOS_DEVELOPMENT_TEAM",
|
|
20040
|
+
"APP_STORE_CONNECT_ISSUER_ID",
|
|
20041
|
+
"APP_STORE_CONNECT_KEY_ID",
|
|
20042
|
+
"APP_STORE_CONNECT_PRIVATE_KEY_BASE64"
|
|
19250
20043
|
]);
|
|
20044
|
+
bundleAuditSteps = ` - name: Prepare production mobile bundle
|
|
20045
|
+
shell: bash
|
|
20046
|
+
run: |
|
|
20047
|
+
args=(bunx absolute prepare "$ABSOLUTE_SERVER_ENTRY" --outdir .absolutejs/mobile-ci/server)
|
|
20048
|
+
${appendConfigArgument}
|
|
20049
|
+
"\${args[@]}"
|
|
20050
|
+
- name: Run cryptographic mobile bundle audit
|
|
20051
|
+
id: mobile-bundle-audit
|
|
20052
|
+
continue-on-error: true
|
|
20053
|
+
shell: bash
|
|
20054
|
+
run: |
|
|
20055
|
+
mkdir -p .absolutejs/mobile-ci
|
|
20056
|
+
args=(bunx absolute mobile inspect --json --require-bundle)
|
|
20057
|
+
${appendConfigArgument}
|
|
20058
|
+
"\${args[@]}" > .absolutejs/mobile-ci/inspection.json
|
|
20059
|
+
- name: Upload mobile inspection report
|
|
20060
|
+
if: always()
|
|
20061
|
+
uses: actions/upload-artifact@v7
|
|
20062
|
+
with:
|
|
20063
|
+
name: absolute-mobile-inspection
|
|
20064
|
+
path: .absolutejs/mobile-ci/inspection.json
|
|
20065
|
+
if-no-files-found: error
|
|
20066
|
+
retention-days: 30
|
|
20067
|
+
include-hidden-files: true
|
|
20068
|
+
- name: Enforce mobile bundle audit
|
|
20069
|
+
if: steps.mobile-bundle-audit.outcome != 'success'
|
|
20070
|
+
run: exit 1`;
|
|
19251
20071
|
});
|
|
19252
20072
|
|
|
19253
20073
|
// src/cli/scripts/mobile.ts
|
|
@@ -19255,11 +20075,11 @@ var exports_mobile = {};
|
|
|
19255
20075
|
__export(exports_mobile, {
|
|
19256
20076
|
runMobile: () => runMobile
|
|
19257
20077
|
});
|
|
19258
|
-
import { access as
|
|
19259
|
-
import { join as
|
|
20078
|
+
import { access as access14, mkdir as mkdir15, readFile as readFile23, writeFile as writeFile17 } from "fs/promises";
|
|
20079
|
+
import { join as join54, relative as relative30, resolve as resolve44 } from "path";
|
|
19260
20080
|
import { createInterface } from "readline/promises";
|
|
19261
|
-
var
|
|
19262
|
-
const manifest = JSON.parse(await
|
|
20081
|
+
var NOT_FOUND4 = -1, isRecord15 = (value) => typeof value === "object" && value !== null && !Array.isArray(value), CAPACITOR_PACKAGES, CAPACITOR_PACKAGE_SPECS, CAPACITOR_SYNC_PACKAGE_SPECS, packageNameFromSpec = (spec) => spec.slice(0, spec.lastIndexOf("@")), directProjectPackages = async (projectRoot) => {
|
|
20082
|
+
const manifest = JSON.parse(await readFile23(join54(projectRoot, "package.json"), "utf8"));
|
|
19263
20083
|
if (!isRecord15(manifest))
|
|
19264
20084
|
throw new TypeError("Application package.json must contain an object.");
|
|
19265
20085
|
const names = new Set;
|
|
@@ -19272,7 +20092,7 @@ var NOT_FOUND3 = -1, isRecord15 = (value) => typeof value === "object" && value
|
|
|
19272
20092
|
return names;
|
|
19273
20093
|
}, resolvedPackageVersion = async (projectRoot, packageName) => {
|
|
19274
20094
|
try {
|
|
19275
|
-
const manifest = JSON.parse(await
|
|
20095
|
+
const manifest = JSON.parse(await readFile23(join54(projectRoot, "node_modules", packageName, "package.json"), "utf8"));
|
|
19276
20096
|
return isRecord15(manifest) && typeof manifest.version === "string" ? manifest.version : undefined;
|
|
19277
20097
|
} catch {
|
|
19278
20098
|
return;
|
|
@@ -19303,7 +20123,7 @@ var NOT_FOUND3 = -1, isRecord15 = (value) => typeof value === "object" && value
|
|
|
19303
20123
|
await installApprovedPackages(projectRoot, args, `AbsoluteJS detected native device capabilities (${capabilityPlan.capabilities.join(", ")}). Install only their required Capacitor plugins now?`, capabilityPackages);
|
|
19304
20124
|
}, valueAfter = (args, flag) => {
|
|
19305
20125
|
const index = args.indexOf(flag);
|
|
19306
|
-
return index ===
|
|
20126
|
+
return index === NOT_FOUND4 ? undefined : args[index + 1];
|
|
19307
20127
|
}, valuesAfter = (args, flag) => args.flatMap((value, index) => {
|
|
19308
20128
|
const next = args[index + 1];
|
|
19309
20129
|
return value === flag && next !== undefined ? [next] : [];
|
|
@@ -19313,9 +20133,9 @@ var NOT_FOUND3 = -1, isRecord15 = (value) => typeof value === "object" && value
|
|
|
19313
20133
|
}
|
|
19314
20134
|
return value;
|
|
19315
20135
|
}, capacitorExecutable = async (projectRoot) => {
|
|
19316
|
-
const executable =
|
|
20136
|
+
const executable = join54(projectRoot, "node_modules", ".bin", "cap");
|
|
19317
20137
|
try {
|
|
19318
|
-
await
|
|
20138
|
+
await access14(executable);
|
|
19319
20139
|
return executable;
|
|
19320
20140
|
} catch {
|
|
19321
20141
|
throw new TypeError(`Capacitor is not installed in this app. Run: bun add ${CAPACITOR_PACKAGES.join(" ")}`);
|
|
@@ -19342,11 +20162,19 @@ var NOT_FOUND3 = -1, isRecord15 = (value) => typeof value === "object" && value
|
|
|
19342
20162
|
const report = await inspectAbsoluteMobileProject(mobile, projectRoot, {
|
|
19343
20163
|
absolutejsVersion: await absolutejsVersionForReport()
|
|
19344
20164
|
});
|
|
20165
|
+
const requireBundle = () => {
|
|
20166
|
+
if (!args.includes("--require-bundle"))
|
|
20167
|
+
return;
|
|
20168
|
+
if (report.bundle.status !== "valid" || report.capabilities.issue || report.capabilities.embeddedMatchesCurrent !== true)
|
|
20169
|
+
throw new TypeError("Mobile bundle validation failed. Regenerate the production bundle and resolve capability drift.");
|
|
20170
|
+
};
|
|
19345
20171
|
if (args.includes("--json")) {
|
|
19346
20172
|
console.log(JSON.stringify(report, null, 2));
|
|
20173
|
+
requireBundle();
|
|
19347
20174
|
return report;
|
|
19348
20175
|
}
|
|
19349
20176
|
console.log(renderAbsoluteMobileProjectInspection(report).trimEnd());
|
|
20177
|
+
requireBundle();
|
|
19350
20178
|
return report;
|
|
19351
20179
|
}, remoteProfilePath = () => process.env.ABSOLUTE_REMOTE_MAC_PROFILE_PATH || undefined, pairRemoteMac = async (args) => {
|
|
19352
20180
|
if (args[0] !== "mac" || !args[1] || !args[2])
|
|
@@ -19412,7 +20240,7 @@ var NOT_FOUND3 = -1, isRecord15 = (value) => typeof value === "object" && value
|
|
|
19412
20240
|
await applyAbsoluteNativeBackgroundSync(projectRoot, mobile, platforms);
|
|
19413
20241
|
}, associations = async (args) => {
|
|
19414
20242
|
const { mobile, projectRoot } = await loadMobile(valueAfter(args, "--config"));
|
|
19415
|
-
const outputDirectory =
|
|
20243
|
+
const outputDirectory = resolve44(projectRoot, valueAfter(args, "--outdir") ?? ".absolutejs/mobile/associations");
|
|
19416
20244
|
if (args.includes("--verify")) {
|
|
19417
20245
|
const result2 = await verifyAbsoluteMobileAssociationFiles(mobile);
|
|
19418
20246
|
console.log(`Verified ${result2.results.length} hosted association files`);
|
|
@@ -19423,6 +20251,59 @@ var NOT_FOUND3 = -1, isRecord15 = (value) => typeof value === "object" && value
|
|
|
19423
20251
|
}
|
|
19424
20252
|
const result = await materializeAbsoluteMobileAssociationFiles(mobile, outputDirectory);
|
|
19425
20253
|
console.log(`Generated ${result.written.length} association files in ${result.root}`);
|
|
20254
|
+
}, mobileCiServerEntry = (args) => {
|
|
20255
|
+
const valueFlags = new Set([
|
|
20256
|
+
"--config",
|
|
20257
|
+
"--output",
|
|
20258
|
+
"--registry",
|
|
20259
|
+
"--secret-env"
|
|
20260
|
+
]);
|
|
20261
|
+
const skipped = new Set;
|
|
20262
|
+
args.forEach((value, index) => {
|
|
20263
|
+
if (!valueFlags.has(value))
|
|
20264
|
+
return;
|
|
20265
|
+
skipped.add(index);
|
|
20266
|
+
skipped.add(index + 1);
|
|
20267
|
+
});
|
|
20268
|
+
return args.find((value, index) => !skipped.has(index) && value !== "github" && !value.startsWith("-")) ?? DEFAULT_SERVER_ENTRY;
|
|
20269
|
+
}, generateGithubCi = async (args) => {
|
|
20270
|
+
if (args[0] !== "github")
|
|
20271
|
+
throw new TypeError("Usage: absolute mobile ci github [server-entry] [--publish] [--registry module] [--secret-env NAME] [--output path] [--force] [--json] [--config path]");
|
|
20272
|
+
const configPath2 = valueAfter(args, "--config");
|
|
20273
|
+
const { mobile, projectRoot } = await loadMobile(configPath2);
|
|
20274
|
+
const result = await writeAbsoluteMobileGithubWorkflow({
|
|
20275
|
+
config: mobile,
|
|
20276
|
+
configPath: configPath2,
|
|
20277
|
+
force: args.includes("--force"),
|
|
20278
|
+
includePublishing: args.includes("--publish"),
|
|
20279
|
+
outputPath: valueAfter(args, "--output"),
|
|
20280
|
+
projectRoot,
|
|
20281
|
+
registryModule: valueAfter(args, "--registry"),
|
|
20282
|
+
secretEnvironment: valuesAfter(args, "--secret-env"),
|
|
20283
|
+
serverEntry: mobileCiServerEntry(args)
|
|
20284
|
+
});
|
|
20285
|
+
sendTelemetryEvent("mobile:ci-generated", {
|
|
20286
|
+
platformCount: result.platforms.length,
|
|
20287
|
+
provider: "github-actions",
|
|
20288
|
+
publishing: result.publishing
|
|
20289
|
+
});
|
|
20290
|
+
const publicResult = {
|
|
20291
|
+
changed: result.changed,
|
|
20292
|
+
format: result.format,
|
|
20293
|
+
path: relative30(projectRoot, result.path).replaceAll("\\", "/"),
|
|
20294
|
+
platforms: result.platforms,
|
|
20295
|
+
publishing: result.publishing,
|
|
20296
|
+
requiredSecrets: result.requiredSecrets
|
|
20297
|
+
};
|
|
20298
|
+
if (args.includes("--json")) {
|
|
20299
|
+
console.log(JSON.stringify(publicResult, null, 2));
|
|
20300
|
+
return publicResult;
|
|
20301
|
+
}
|
|
20302
|
+
console.log(`${result.changed ? "Generated" : "Verified"} ${publicResult.path} for ${result.platforms.join(" and ")}.`);
|
|
20303
|
+
console.log("Create a protected GitHub environment named absolute-mobile-release, then add these secrets:");
|
|
20304
|
+
result.requiredSecrets.forEach((name) => console.log(` ${name}`));
|
|
20305
|
+
console.log("Pull requests run the secret-free release audit. Signed builds and optional publishing run only through manual workflow dispatch.");
|
|
20306
|
+
return publicResult;
|
|
19426
20307
|
}, doctorMark = (status2) => {
|
|
19427
20308
|
if (status2 === "pass")
|
|
19428
20309
|
return "\x1B[32m\u2713\x1B[0m";
|
|
@@ -19455,9 +20336,11 @@ var NOT_FOUND3 = -1, isRecord15 = (value) => typeof value === "object" && value
|
|
|
19455
20336
|
}
|
|
19456
20337
|
}, runReleaseDoctor = async (args) => {
|
|
19457
20338
|
const { mobile, projectRoot } = await loadMobile(valueAfter(args, "--config"));
|
|
19458
|
-
const
|
|
20339
|
+
const platform6 = args.find((value) => value === "android" || value === "ios");
|
|
20340
|
+
const effectiveMobile = platform6 ? { ...mobile, platforms: [platform6] } : mobile;
|
|
20341
|
+
const result = await inspectAbsoluteMobileRelease(effectiveMobile, projectRoot);
|
|
19459
20342
|
if (args.includes("--json")) {
|
|
19460
|
-
console.log(JSON.stringify(result, null, 2));
|
|
20343
|
+
console.log(JSON.stringify(createAbsoluteMobileComplianceReport(effectiveMobile, result), null, 2));
|
|
19461
20344
|
} else {
|
|
19462
20345
|
result.checks.forEach((check2) => {
|
|
19463
20346
|
console.log(`${doctorMark(check2.status)} ${check2.detail}${check2.path ? ` (${check2.path})` : ""}`);
|
|
@@ -19465,8 +20348,8 @@ var NOT_FOUND3 = -1, isRecord15 = (value) => typeof value === "object" && value
|
|
|
19465
20348
|
console.log(` ${check2.remediation}`);
|
|
19466
20349
|
});
|
|
19467
20350
|
console.log(result.ready ? `
|
|
19468
|
-
Mobile release
|
|
19469
|
-
Mobile release
|
|
20351
|
+
Mobile release security and compliance checks passed.` : `
|
|
20352
|
+
Mobile release security and compliance checks failed.`);
|
|
19470
20353
|
}
|
|
19471
20354
|
if (!result.ready) {
|
|
19472
20355
|
throw new TypeError("Mobile release validation failed. Resolve every failed check before signing or publishing the app.");
|
|
@@ -19613,6 +20496,27 @@ Mobile release transport checks failed.`);
|
|
|
19613
20496
|
status: check2.status
|
|
19614
20497
|
})));
|
|
19615
20498
|
throw new TypeError("Android release validation failed before Gradle signing.");
|
|
20499
|
+
}, androidCiSigning = () => {
|
|
20500
|
+
const keyAlias = process.env.ABSOLUTE_ANDROID_KEY_ALIAS;
|
|
20501
|
+
const keyPassword = process.env.ABSOLUTE_ANDROID_KEY_PASSWORD;
|
|
20502
|
+
const keystorePath = process.env.ABSOLUTE_ANDROID_KEYSTORE_PATH;
|
|
20503
|
+
const storePassword = process.env.ABSOLUTE_ANDROID_KEYSTORE_PASSWORD;
|
|
20504
|
+
const supplied = [
|
|
20505
|
+
keyAlias,
|
|
20506
|
+
keyPassword,
|
|
20507
|
+
keystorePath,
|
|
20508
|
+
storePassword
|
|
20509
|
+
].filter((value) => value !== undefined && value !== "").length;
|
|
20510
|
+
if (supplied === 0)
|
|
20511
|
+
return;
|
|
20512
|
+
if (!keyAlias || !keyPassword || !keystorePath || !storePassword)
|
|
20513
|
+
throw new TypeError("AbsoluteJS CI signing requires ABSOLUTE_ANDROID_KEYSTORE_PATH, ABSOLUTE_ANDROID_KEYSTORE_PASSWORD, ABSOLUTE_ANDROID_KEY_ALIAS, and ABSOLUTE_ANDROID_KEY_PASSWORD together.");
|
|
20514
|
+
return {
|
|
20515
|
+
keyAlias,
|
|
20516
|
+
keyPasswordEnvironment: "ABSOLUTE_ANDROID_KEY_PASSWORD",
|
|
20517
|
+
keystorePath,
|
|
20518
|
+
storePasswordEnvironment: "ABSOLUTE_ANDROID_KEYSTORE_PASSWORD"
|
|
20519
|
+
};
|
|
19616
20520
|
}, buildAndroid = async (args, prepareVersionCode) => {
|
|
19617
20521
|
const configPath2 = valueAfter(args, "--config");
|
|
19618
20522
|
const { mobile, projectRoot } = await loadMobile(configPath2);
|
|
@@ -19639,13 +20543,14 @@ Mobile release transport checks failed.`);
|
|
|
19639
20543
|
config: mobile,
|
|
19640
20544
|
outputDirectory: valueAfter(args, "--outdir"),
|
|
19641
20545
|
projectRoot,
|
|
20546
|
+
signing: androidCiSigning(),
|
|
19642
20547
|
...prepareVersionCode === undefined ? {} : { prepareVersionCode }
|
|
19643
20548
|
});
|
|
19644
20549
|
success = true;
|
|
19645
20550
|
const durationMs = Math.round(performance.now() - startedAt);
|
|
19646
20551
|
console.log(`Built ${release.metadata.signed ? "signed" : "unsigned"} Android App Bundle in ${getDurationString(durationMs)}.`);
|
|
19647
20552
|
console.log(`Artifact: ${release.artifactPath}`);
|
|
19648
|
-
console.log(`Metadata: ${
|
|
20553
|
+
console.log(`Metadata: ${join54(release.releaseRoot, "release.json")}`);
|
|
19649
20554
|
return release;
|
|
19650
20555
|
} finally {
|
|
19651
20556
|
sendTelemetryEvent("mobile:android-release-build", {
|
|
@@ -19740,6 +20645,7 @@ Mobile release transport checks failed.`);
|
|
|
19740
20645
|
const release = await buildAbsoluteIosRelease({
|
|
19741
20646
|
allowUnsigned: args.includes("--unsigned"),
|
|
19742
20647
|
config: mobile,
|
|
20648
|
+
developmentTeam: process.env.ABSOLUTE_IOS_DEVELOPMENT_TEAM,
|
|
19743
20649
|
outputDirectory: valueAfter(args, "--outdir"),
|
|
19744
20650
|
...prepareBuildNumber === undefined ? {} : { prepareBuildNumber },
|
|
19745
20651
|
projectRoot
|
|
@@ -19748,7 +20654,7 @@ Mobile release transport checks failed.`);
|
|
|
19748
20654
|
const durationMs = Math.round(performance.now() - startedAt);
|
|
19749
20655
|
console.log(`Built ${release.metadata.signed ? "signed" : "unsigned"} iOS IPA ${release.metadata.marketingVersion}${release.metadata.buildNumber ? ` (${release.metadata.buildNumber})` : ""} in ${getDurationString(durationMs)}.`);
|
|
19750
20656
|
console.log(`Artifact: ${release.artifactPath}`);
|
|
19751
|
-
console.log(`Metadata: ${
|
|
20657
|
+
console.log(`Metadata: ${join54(release.releaseRoot, "release.json")}`);
|
|
19752
20658
|
return release;
|
|
19753
20659
|
} finally {
|
|
19754
20660
|
sendTelemetryEvent("mobile:ios-release-build", {
|
|
@@ -19855,7 +20761,7 @@ Mobile release transport checks failed.`);
|
|
|
19855
20761
|
checks.push({
|
|
19856
20762
|
id: "sync.storage-schema",
|
|
19857
20763
|
label: `Offline schema ${schema.components.map((component2) => `${component2.id}@${component2.version}`).join(", ")}`,
|
|
19858
|
-
path:
|
|
20764
|
+
path: join54(projectRoot, "package.json"),
|
|
19859
20765
|
platform: "host",
|
|
19860
20766
|
status: "pass"
|
|
19861
20767
|
});
|
|
@@ -19863,7 +20769,7 @@ Mobile release transport checks failed.`);
|
|
|
19863
20769
|
checks.push({
|
|
19864
20770
|
id: "sync.storage-schema",
|
|
19865
20771
|
label: "Offline schema metadata is invalid",
|
|
19866
|
-
path:
|
|
20772
|
+
path: join54(projectRoot, "package.json"),
|
|
19867
20773
|
platform: "host",
|
|
19868
20774
|
remediation: error instanceof Error ? error.message : String(error),
|
|
19869
20775
|
status: "fail"
|
|
@@ -19948,7 +20854,7 @@ Emulator setup verification:`);
|
|
|
19948
20854
|
}
|
|
19949
20855
|
return { https: args.includes("--https"), port };
|
|
19950
20856
|
}
|
|
19951
|
-
const instances = listLiveInstances().filter((instance2) =>
|
|
20857
|
+
const instances = listLiveInstances().filter((instance2) => resolve44(instance2.cwd) === resolve44(projectRoot) && instance2.source === "dev" && instance2.port !== null);
|
|
19952
20858
|
if (instances.length !== 1) {
|
|
19953
20859
|
throw new TypeError(instances.length === 0 ? "No running AbsoluteJS dev server was found for this project. Start `bun dev`, wait for Android to report ready, then run `absolute mobile test android`." : "Multiple dev servers are running for this project. Select one with mobile test android --port <port>.");
|
|
19954
20860
|
}
|
|
@@ -19991,8 +20897,8 @@ Emulator setup verification:`);
|
|
|
19991
20897
|
}
|
|
19992
20898
|
return selected;
|
|
19993
20899
|
}, safeArtifactRoot = (projectRoot, value) => {
|
|
19994
|
-
const root =
|
|
19995
|
-
if (root !== projectRoot && !root.startsWith(`${
|
|
20900
|
+
const root = resolve44(projectRoot, value ?? ".absolutejs/mobile/test-artifacts");
|
|
20901
|
+
if (root !== projectRoot && !root.startsWith(`${resolve44(projectRoot)}/`)) {
|
|
19996
20902
|
throw new TypeError("mobile test --artifacts must remain inside the project.");
|
|
19997
20903
|
}
|
|
19998
20904
|
return root;
|
|
@@ -20016,12 +20922,12 @@ Emulator setup verification:`);
|
|
|
20016
20922
|
timeoutMs
|
|
20017
20923
|
});
|
|
20018
20924
|
}, writeAndroidFailureArtifacts = async (options) => {
|
|
20019
|
-
await
|
|
20020
|
-
const screenshot = options.session ? await options.session.screenshot(
|
|
20925
|
+
await mkdir15(options.artifactRoot, { recursive: true });
|
|
20926
|
+
const screenshot = options.session ? await options.session.screenshot(join54(options.artifactRoot, "android-failure.png")).catch(() => {
|
|
20021
20927
|
return;
|
|
20022
20928
|
}) : undefined;
|
|
20023
|
-
const diagnosticPath =
|
|
20024
|
-
await
|
|
20929
|
+
const diagnosticPath = join54(options.artifactRoot, "android-failure.json");
|
|
20930
|
+
await writeFile17(diagnosticPath, `${JSON.stringify({
|
|
20025
20931
|
diagnostics: options.session?.diagnostics ?? [],
|
|
20026
20932
|
error: options.error instanceof Error ? options.error.message : String(options.error),
|
|
20027
20933
|
platform: "android",
|
|
@@ -20084,7 +20990,7 @@ Emulator setup verification:`);
|
|
|
20084
20990
|
console.log(JSON.stringify(report, null, 2));
|
|
20085
20991
|
else
|
|
20086
20992
|
printAndroidTestReport(report);
|
|
20087
|
-
const screenshot = reportRoot ? await session.screenshot(
|
|
20993
|
+
const screenshot = reportRoot ? await session.screenshot(join54(artifactRoot, "android-emulator.png")) : undefined;
|
|
20088
20994
|
await writeRequestedAndroidReport({
|
|
20089
20995
|
adb,
|
|
20090
20996
|
args,
|
|
@@ -20152,14 +21058,14 @@ Emulator setup verification:`);
|
|
|
20152
21058
|
const port = Number(explicit);
|
|
20153
21059
|
if (!Number.isInteger(port) || port < 1 || port > 65535)
|
|
20154
21060
|
throw new TypeError("mobile test --port must be a valid TCP port.");
|
|
20155
|
-
const instance2 = listLiveInstances().find((candidate) =>
|
|
21061
|
+
const instance2 = listLiveInstances().find((candidate) => resolve44(candidate.cwd) === resolve44(projectRoot) && candidate.source === "dev" && candidate.port === port);
|
|
20156
21062
|
return {
|
|
20157
21063
|
https: instance2?.https ?? args.includes("--https"),
|
|
20158
21064
|
instance: instance2,
|
|
20159
21065
|
port
|
|
20160
21066
|
};
|
|
20161
21067
|
}
|
|
20162
|
-
const instances = listLiveInstances().filter((instance2) =>
|
|
21068
|
+
const instances = listLiveInstances().filter((instance2) => resolve44(instance2.cwd) === resolve44(projectRoot) && instance2.source === "dev" && instance2.port !== null);
|
|
20163
21069
|
if (instances.length !== 1)
|
|
20164
21070
|
throw new TypeError(instances.length === 0 ? "No running AbsoluteJS dev server was found for this project. Start `bun dev`, wait for iOS to report ready, then run `absolute mobile test ios`." : "Multiple dev servers are running for this project. Select one with mobile test ios --port <port>.");
|
|
20165
21071
|
const [instance] = instances;
|
|
@@ -20214,7 +21120,7 @@ Emulator setup verification:`);
|
|
|
20214
21120
|
if (!instance)
|
|
20215
21121
|
return;
|
|
20216
21122
|
const index = instance.command.indexOf("--ios-device");
|
|
20217
|
-
return index ===
|
|
21123
|
+
return index === NOT_FOUND4 ? undefined : instance.command[index + 1];
|
|
20218
21124
|
}, physicalIosCapture = async (options) => {
|
|
20219
21125
|
const remoteName = valueAfter(options.args, "--remote");
|
|
20220
21126
|
if (remoteName && options.instance.iosRemoteMac && remoteName !== options.instance.iosRemoteMac)
|
|
@@ -20273,8 +21179,8 @@ Emulator setup verification:`);
|
|
|
20273
21179
|
throw new Error(`${label} failed: ${result.stderr.trim() || result.stdout.trim() || `status ${result.exitCode}`}`);
|
|
20274
21180
|
return result;
|
|
20275
21181
|
}, writeIosFailureArtifacts = async (options) => {
|
|
20276
|
-
await
|
|
20277
|
-
const screenshot =
|
|
21182
|
+
await mkdir15(options.artifactRoot, { recursive: true });
|
|
21183
|
+
const screenshot = join54(options.artifactRoot, "ios-failure.png");
|
|
20278
21184
|
const screenshotResult = captureCommand4([
|
|
20279
21185
|
options.xcrun,
|
|
20280
21186
|
"simctl",
|
|
@@ -20283,8 +21189,8 @@ Emulator setup verification:`);
|
|
|
20283
21189
|
"screenshot",
|
|
20284
21190
|
screenshot
|
|
20285
21191
|
]);
|
|
20286
|
-
const diagnosticPath =
|
|
20287
|
-
await
|
|
21192
|
+
const diagnosticPath = join54(options.artifactRoot, "ios-failure.json");
|
|
21193
|
+
await writeFile17(diagnosticPath, `${JSON.stringify({
|
|
20288
21194
|
appId: options.appId,
|
|
20289
21195
|
error: options.error instanceof Error ? options.error.message : String(options.error),
|
|
20290
21196
|
platform: "ios",
|
|
@@ -20300,7 +21206,7 @@ Emulator setup verification:`);
|
|
|
20300
21206
|
};
|
|
20301
21207
|
}, nativeReportRoot = (args, projectRoot, platform6) => {
|
|
20302
21208
|
const index = args.indexOf("--report");
|
|
20303
|
-
if (index ===
|
|
21209
|
+
if (index === NOT_FOUND4)
|
|
20304
21210
|
return;
|
|
20305
21211
|
const candidate = args[index + 1];
|
|
20306
21212
|
const explicit = candidate?.startsWith("--") ? undefined : candidate;
|
|
@@ -20309,8 +21215,8 @@ Emulator setup verification:`);
|
|
|
20309
21215
|
}, absolutejsVersionForReport = async () => {
|
|
20310
21216
|
let absolutejsVersion = process.env.ABSOLUTE_VERSION ?? "unknown";
|
|
20311
21217
|
const versions = await Promise.all([
|
|
20312
|
-
|
|
20313
|
-
|
|
21218
|
+
resolve44(import.meta.dir, "..", "..", "package.json"),
|
|
21219
|
+
resolve44(import.meta.dir, "..", "..", "..", "package.json")
|
|
20314
21220
|
].map((candidate) => readPackageVersionForIosReport(candidate).catch(() => "unknown")));
|
|
20315
21221
|
for (const version2 of versions) {
|
|
20316
21222
|
if (version2 === "unknown")
|
|
@@ -20528,8 +21434,8 @@ Emulator setup verification:`);
|
|
|
20528
21434
|
mobile.appId
|
|
20529
21435
|
], "iOS app launch");
|
|
20530
21436
|
await waitForIosHmrClient({ https, port, timeoutMs });
|
|
20531
|
-
await
|
|
20532
|
-
const screenshot =
|
|
21437
|
+
await mkdir15(artifactRoot, { recursive: true });
|
|
21438
|
+
const screenshot = join54(artifactRoot, "ios-simulator.png");
|
|
20533
21439
|
requireCapturedCommand([xcrun, "simctl", "io", simulator.udid, "screenshot", screenshot], "iOS simulator screenshot");
|
|
20534
21440
|
const hmrApply = await waitForRequestedIosHmr(args, instance, timeoutMs);
|
|
20535
21441
|
const report = {
|
|
@@ -20638,6 +21544,10 @@ Emulator setup verification:`);
|
|
|
20638
21544
|
await associations(args.slice(1));
|
|
20639
21545
|
return;
|
|
20640
21546
|
}
|
|
21547
|
+
if (command === "ci") {
|
|
21548
|
+
await generateGithubCi(args.slice(1));
|
|
21549
|
+
return;
|
|
21550
|
+
}
|
|
20641
21551
|
if (command === "doctor") {
|
|
20642
21552
|
await doctor(args.slice(1));
|
|
20643
21553
|
return;
|
|
@@ -20670,7 +21580,7 @@ Emulator setup verification:`);
|
|
|
20670
21580
|
await publishIos(args.slice(2));
|
|
20671
21581
|
return;
|
|
20672
21582
|
}
|
|
20673
|
-
throw new TypeError("Usage: absolute mobile <pair mac <name> <user@host> [--port n] [--workspace path] | remotes [--json] | unpair mac <name> | init [--no-native] [--force] | sync [ios|android] | inspect [--json] | associations [--outdir dir] [--verify] | doctor [ios|android|release] [--remote name] [--json|--fix [--yes]] | build <android|ios> [server-entry] [--outdir dir] [--web-outdir dir] [--unsigned] | publish android [server-entry] [--registry module] [--channel name] [--play-track track] [--play-status completed|draft|halted|in-progress] [--play-rollout fraction] [--play-name name] [--play-notes language=text] [--play-update-priority 0..5] [--play-hold-review] [--play-cancel-existing-review] [--outdir dir] [--web-outdir dir] [--unsigned] | publish ios [server-entry] [--registry module] [--channel name] [--testflight-group name-or-id] [--testflight-notes locale=text] [--testflight-submit-review] [--outdir dir] [--web-outdir dir] [--unsigned] | test android [--route path] [--wait-for-hmr] [--report [dir]] [--timeout ms] [--port n] [--serial id] [--artifacts dir] [--json] | test ios [--device identifier [--remote name] | --udid id] [--wait-for-hmr] [--report [dir]] [--timeout ms] [--port n] [--artifacts dir] [--json]> [--config path]");
|
|
21583
|
+
throw new TypeError("Usage: absolute mobile <pair mac <name> <user@host> [--port n] [--workspace path] | remotes [--json] | unpair mac <name> | init [--no-native] [--force] | sync [ios|android] | inspect [--json] [--require-bundle] | associations [--outdir dir] [--verify] | ci github [server-entry] [--publish] [--registry module] [--secret-env NAME] [--output path] [--force] [--json] | doctor [ios|android|release [ios|android]] [--remote name] [--json|--fix [--yes]] | build <android|ios> [server-entry] [--outdir dir] [--web-outdir dir] [--unsigned] | publish android [server-entry] [--registry module] [--channel name] [--play-track track] [--play-status completed|draft|halted|in-progress] [--play-rollout fraction] [--play-name name] [--play-notes language=text] [--play-update-priority 0..5] [--play-hold-review] [--play-cancel-existing-review] [--outdir dir] [--web-outdir dir] [--unsigned] | publish ios [server-entry] [--registry module] [--channel name] [--testflight-group name-or-id] [--testflight-notes locale=text] [--testflight-submit-review] [--outdir dir] [--web-outdir dir] [--unsigned] | test android [--route path] [--wait-for-hmr] [--report [dir]] [--timeout ms] [--port n] [--serial id] [--artifacts dir] [--json] | test ios [--device identifier [--remote name] | --udid id] [--wait-for-hmr] [--report [dir]] [--timeout ms] [--port n] [--artifacts dir] [--json]> [--config path]");
|
|
20674
21584
|
};
|
|
20675
21585
|
var init_mobile = __esm(() => {
|
|
20676
21586
|
init_dependencies();
|
|
@@ -20705,6 +21615,7 @@ var init_mobile = __esm(() => {
|
|
|
20705
21615
|
init_syncSchema();
|
|
20706
21616
|
init_deviceCapabilities();
|
|
20707
21617
|
init_mobileInspect();
|
|
21618
|
+
init_ciWorkflow();
|
|
20708
21619
|
CAPACITOR_PACKAGES = [
|
|
20709
21620
|
"@capacitor/core",
|
|
20710
21621
|
"@capacitor/app",
|
|
@@ -20740,11 +21651,11 @@ var exports_typecheck = {};
|
|
|
20740
21651
|
__export(exports_typecheck, {
|
|
20741
21652
|
typecheck: () => typecheck
|
|
20742
21653
|
});
|
|
20743
|
-
import { resolve as
|
|
20744
|
-
import { existsSync as
|
|
20745
|
-
import { mkdir as
|
|
20746
|
-
var isCommandService3 = (service) => service.kind === "command" || Array.isArray(service.command), resolveConfigPath = (configPath2) =>
|
|
20747
|
-
if (!
|
|
21654
|
+
import { resolve as resolve45, join as join55 } from "path";
|
|
21655
|
+
import { existsSync as existsSync43, readFileSync as readFileSync40 } from "fs";
|
|
21656
|
+
import { mkdir as mkdir16, writeFile as writeFile18 } from "fs/promises";
|
|
21657
|
+
var isCommandService3 = (service) => service.kind === "command" || Array.isArray(service.command), resolveConfigPath = (configPath2) => resolve45(configPath2 ?? process.env.ABSOLUTE_CONFIG ?? "absolute.config.ts"), getTypecheckTargets = async (configPath2) => {
|
|
21658
|
+
if (!existsSync43(resolveConfigPath(configPath2))) {
|
|
20748
21659
|
const defaultService = {};
|
|
20749
21660
|
return [defaultService];
|
|
20750
21661
|
}
|
|
@@ -20765,8 +21676,8 @@ var isCommandService3 = (service) => service.kind === "command" || Array.isArray
|
|
|
20765
21676
|
const exitCode = await proc.exited;
|
|
20766
21677
|
return { exitCode, name, output: (stdout + stderr).trim() };
|
|
20767
21678
|
}, shellEscape = (value) => `'${value.replaceAll("'", "'\\''")}'`, runShell = async (name, command) => run(name, ["/bin/bash", "-lc", command]), findBin = (name) => {
|
|
20768
|
-
const local =
|
|
20769
|
-
return
|
|
21679
|
+
const local = resolve45("node_modules", ".bin", name);
|
|
21680
|
+
return existsSync43(local) ? local : null;
|
|
20770
21681
|
}, ANSI_COLOR_REGEX, ANSI_PURPLE_REGEX, ANSI_CYAN_REGEX, ANSI_TOKEN_END_REGEX, stripAnsi4 = (str) => str.replace(ANSI_COLOR_REGEX, ""), formatSvelteOutput = (output) => {
|
|
20771
21682
|
const cwd = `${process.cwd()}/`;
|
|
20772
21683
|
const summaryMatch = stripAnsi4(output).match(/svelte-check found (\d+) error/);
|
|
@@ -20813,15 +21724,15 @@ Found ${errorCount} error${suffix}.`;
|
|
|
20813
21724
|
return formatted;
|
|
20814
21725
|
}, ABSOLUTE_INTERNAL_EXCLUDES, resolveAbsoluteTypeFile = (fileName) => {
|
|
20815
21726
|
const candidates = [
|
|
20816
|
-
|
|
20817
|
-
|
|
20818
|
-
|
|
20819
|
-
|
|
21727
|
+
resolve45("node_modules/@absolutejs/absolute/dist/types", fileName),
|
|
21728
|
+
resolve45(import.meta.dir, "../types", fileName),
|
|
21729
|
+
resolve45(import.meta.dir, "../../types", fileName),
|
|
21730
|
+
resolve45(import.meta.dir, "../../../types", fileName)
|
|
20820
21731
|
];
|
|
20821
|
-
return candidates.find((candidate) =>
|
|
21732
|
+
return candidates.find((candidate) => existsSync43(candidate)) ?? candidates[0];
|
|
20822
21733
|
}, ABSOLUTE_TYPECHECK_FILES, readProjectTsconfig = () => {
|
|
20823
21734
|
try {
|
|
20824
|
-
return JSON.parse(readFileSync40(
|
|
21735
|
+
return JSON.parse(readFileSync40(resolve45("tsconfig.json"), "utf-8"));
|
|
20825
21736
|
} catch {
|
|
20826
21737
|
return {};
|
|
20827
21738
|
}
|
|
@@ -20849,27 +21760,27 @@ Found ${errorCount} error${suffix}.`;
|
|
|
20849
21760
|
console.error("\x1B[31m\u2717\x1B[0m vue-tsc is required for Vue type checking. Install it: bun add -d vue-tsc");
|
|
20850
21761
|
process.exit(1);
|
|
20851
21762
|
}
|
|
20852
|
-
const vueTsconfigPath =
|
|
20853
|
-
await
|
|
21763
|
+
const vueTsconfigPath = join55(cacheDir, "tsconfig.vue-check.json");
|
|
21764
|
+
await writeFile18(vueTsconfigPath, JSON.stringify({
|
|
20854
21765
|
compilerOptions: {
|
|
20855
21766
|
rootDir: ".."
|
|
20856
21767
|
},
|
|
20857
21768
|
exclude: getProjectTypecheckExcludes(),
|
|
20858
|
-
extends:
|
|
21769
|
+
extends: resolve45("tsconfig.json"),
|
|
20859
21770
|
include: getProjectTypecheckIncludes()
|
|
20860
21771
|
}, null, "\t"));
|
|
20861
21772
|
const base = [
|
|
20862
21773
|
vueTscBin,
|
|
20863
21774
|
"--noEmit",
|
|
20864
21775
|
"--project",
|
|
20865
|
-
|
|
21776
|
+
resolve45(vueTsconfigPath),
|
|
20866
21777
|
"--pretty"
|
|
20867
21778
|
];
|
|
20868
21779
|
const cached = await run("vue-tsc", [
|
|
20869
21780
|
...base,
|
|
20870
21781
|
"--incremental",
|
|
20871
21782
|
"--tsBuildInfoFile",
|
|
20872
|
-
|
|
21783
|
+
join55(cacheDir, "vue-tsc.tsbuildinfo")
|
|
20873
21784
|
]);
|
|
20874
21785
|
if (cached.exitCode === 0 || cached.output.length > 0)
|
|
20875
21786
|
return cached;
|
|
@@ -20880,8 +21791,8 @@ Found ${errorCount} error${suffix}.`;
|
|
|
20880
21791
|
console.error("\x1B[31m\u2717\x1B[0m @angular/compiler-cli is required for Angular type checking. Install it: bun add -d @angular/compiler-cli");
|
|
20881
21792
|
process.exit(1);
|
|
20882
21793
|
}
|
|
20883
|
-
const angularTsconfigPath =
|
|
20884
|
-
await
|
|
21794
|
+
const angularTsconfigPath = join55(cacheDir, "tsconfig.angular-check.json");
|
|
21795
|
+
await writeFile18(angularTsconfigPath, JSON.stringify({
|
|
20885
21796
|
angularCompilerOptions: {
|
|
20886
21797
|
strictTemplates: true
|
|
20887
21798
|
},
|
|
@@ -20890,32 +21801,32 @@ Found ${errorCount} error${suffix}.`;
|
|
|
20890
21801
|
rootDir: ".."
|
|
20891
21802
|
},
|
|
20892
21803
|
exclude: ABSOLUTE_INTERNAL_EXCLUDES.map(toGeneratedConfigPath),
|
|
20893
|
-
extends:
|
|
21804
|
+
extends: resolve45("tsconfig.json"),
|
|
20894
21805
|
include: [`../${angularDir}/**/*`]
|
|
20895
21806
|
}, null, "\t"));
|
|
20896
|
-
return runShell("ngc", `${shellEscape(ngcBin)} -p ${shellEscape(
|
|
21807
|
+
return runShell("ngc", `${shellEscape(ngcBin)} -p ${shellEscape(resolve45(angularTsconfigPath))}`);
|
|
20897
21808
|
}, buildTscCheck = (cacheDir) => {
|
|
20898
21809
|
const tscBin = findBin("tsc");
|
|
20899
21810
|
if (!tscBin) {
|
|
20900
21811
|
console.error("\x1B[31m\u2717\x1B[0m typescript is required for type checking. Install it: bun add -d typescript");
|
|
20901
21812
|
process.exit(1);
|
|
20902
21813
|
}
|
|
20903
|
-
const tscConfigPath =
|
|
20904
|
-
return
|
|
21814
|
+
const tscConfigPath = join55(cacheDir, "tsconfig.typecheck.json");
|
|
21815
|
+
return writeFile18(tscConfigPath, JSON.stringify({
|
|
20905
21816
|
compilerOptions: {
|
|
20906
21817
|
rootDir: ".."
|
|
20907
21818
|
},
|
|
20908
21819
|
exclude: getProjectTypecheckExcludes(),
|
|
20909
|
-
extends:
|
|
21820
|
+
extends: resolve45("tsconfig.json"),
|
|
20910
21821
|
include: getProjectTypecheckIncludes()
|
|
20911
21822
|
}, null, "\t")).then(() => run("tsc", [
|
|
20912
21823
|
tscBin,
|
|
20913
21824
|
"--noEmit",
|
|
20914
21825
|
"--project",
|
|
20915
|
-
|
|
21826
|
+
resolve45(tscConfigPath),
|
|
20916
21827
|
"--incremental",
|
|
20917
21828
|
"--tsBuildInfoFile",
|
|
20918
|
-
|
|
21829
|
+
join55(cacheDir, "tsc.tsbuildinfo"),
|
|
20919
21830
|
"--pretty"
|
|
20920
21831
|
]));
|
|
20921
21832
|
}, buildSvelteCheck = async (cacheDir, svelteDir) => {
|
|
@@ -20924,16 +21835,16 @@ Found ${errorCount} error${suffix}.`;
|
|
|
20924
21835
|
console.error("\x1B[31m\u2717\x1B[0m svelte-check is required for Svelte type checking. Install it: bun add -d svelte-check");
|
|
20925
21836
|
process.exit(1);
|
|
20926
21837
|
}
|
|
20927
|
-
const svelteTsconfigPath =
|
|
20928
|
-
await
|
|
20929
|
-
extends:
|
|
21838
|
+
const svelteTsconfigPath = join55(cacheDir, "tsconfig.svelte-check.json");
|
|
21839
|
+
await writeFile18(svelteTsconfigPath, JSON.stringify({
|
|
21840
|
+
extends: resolve45("tsconfig.json"),
|
|
20930
21841
|
files: ABSOLUTE_TYPECHECK_FILES,
|
|
20931
21842
|
include: [`../${svelteDir}/**/*`]
|
|
20932
21843
|
}, null, "\t"));
|
|
20933
21844
|
return run("svelte-check", [
|
|
20934
21845
|
svelteBin,
|
|
20935
21846
|
"--tsconfig",
|
|
20936
|
-
|
|
21847
|
+
resolve45(svelteTsconfigPath),
|
|
20937
21848
|
"--threshold",
|
|
20938
21849
|
"error",
|
|
20939
21850
|
"--compiler-warnings",
|
|
@@ -20954,7 +21865,7 @@ Found ${errorCount} error${suffix}.`;
|
|
|
20954
21865
|
...new Set(targets.map((config) => config.angularDirectory).filter((dir) => typeof dir === "string" && dir.length > 0))
|
|
20955
21866
|
];
|
|
20956
21867
|
const cacheDir = ".absolutejs";
|
|
20957
|
-
await
|
|
21868
|
+
await mkdir16(cacheDir, { recursive: true });
|
|
20958
21869
|
const checks = [];
|
|
20959
21870
|
checks.push(hasVue ? buildVueTscCheck(cacheDir) : buildTscCheck(cacheDir));
|
|
20960
21871
|
for (const svelteDir of hasSvelte ? svelteDirs : []) {
|
|
@@ -21127,11 +22038,11 @@ var DEFAULT_RELAY_PORT = 8787, DEFAULT_REQUEST_TIMEOUT_MS = 30000, headersToObje
|
|
|
21127
22038
|
url: url.pathname + url.search,
|
|
21128
22039
|
...bodyBytes && bodyBytes.length > 0 ? { bodyBase64: Buffer.from(bodyBytes).toString("base64") } : {}
|
|
21129
22040
|
};
|
|
21130
|
-
const responsePromise = new Promise((
|
|
21131
|
-
pending.set(id,
|
|
22041
|
+
const responsePromise = new Promise((resolve46) => {
|
|
22042
|
+
pending.set(id, resolve46);
|
|
21132
22043
|
});
|
|
21133
22044
|
client.send(encodeTunnelMessage(message));
|
|
21134
|
-
const timeout = new Promise((
|
|
22045
|
+
const timeout = new Promise((resolve46) => setTimeout(() => resolve46({ id, message: "timeout", type: "error" }), requestTimeoutMs));
|
|
21135
22046
|
const result = await Promise.race([responsePromise, timeout]);
|
|
21136
22047
|
pending.delete(id);
|
|
21137
22048
|
if (result.type === "error") {
|
|
@@ -25044,7 +25955,7 @@ if (command === "dev") {
|
|
|
25044
25955
|
console.error(" prepare [entry] [--outdir dir] Build production assets and server without launching");
|
|
25045
25956
|
console.error(" start [entry] [--outdir dir] [--prebuilt] Start production server");
|
|
25046
25957
|
console.error(" compile [entry] [--outdir dir] [--outfile path] Compile standalone executable");
|
|
25047
|
-
console.error(" mobile <init|sync|inspect|pair|remotes|doctor|test> Manage Capacitor projects, simulators, physical devices, Remote Macs, guided setup, and deep links");
|
|
25958
|
+
console.error(" mobile <init|sync|inspect|ci|pair|remotes|doctor|test> Manage Capacitor projects, CI, simulators, physical devices, Remote Macs, guided setup, and deep links");
|
|
25048
25959
|
console.error(" config [--port n] Open the unified config UI (ESLint, tsconfig, Prettier)");
|
|
25049
25960
|
console.error(" db <backup|restore|seed> Backup/restore any Postgres DB (ORM-agnostic, upsert by PK) or run the seed script");
|
|
25050
25961
|
console.error(" doctor [--fix] [--json] Diagnose the project (bun, type graph, config, framework dirs, env, port)");
|