@absolutejs/absolute 0.20.0-beta.0 → 0.20.0-beta.2
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/dist/angular/components/core/streamingSlotRegistrar.js +1 -1
- package/dist/angular/components/core/streamingSlotRegistry.js +2 -2
- package/dist/angular/index.js +14 -1
- package/dist/angular/index.js.map +3 -3
- package/dist/angular/server.js +14 -1
- package/dist/angular/server.js.map +3 -3
- package/dist/build.js +206 -47
- package/dist/build.js.map +20 -19
- package/dist/cli/config/server.js +2 -2
- package/dist/cli/index.js +16506 -11326
- package/dist/dev/client/cssUtils.ts +98 -41
- package/dist/dev/client/handlers/html.ts +75 -15
- package/dist/dev/client/handlers/htmx.ts +43 -8
- package/dist/dev/client/handlers/react.ts +114 -32
- package/dist/dev/client/handlers/rebuild.ts +28 -8
- package/dist/dev/client/handlers/svelte.ts +33 -50
- package/dist/dev/client/handlers/vue.ts +36 -45
- package/dist/dev/client/hmrClient.ts +38 -6
- package/dist/dev/client/hmrTiming.ts +141 -0
- package/dist/dev/serverBootstrap.js +20 -11
- package/dist/index.js +953 -423
- package/dist/index.js.map +27 -23
- package/dist/mobile/browser.js.map +1 -1
- package/dist/mobile/index.js +2667 -260
- package/dist/mobile/index.js.map +27 -7
- package/dist/react/index.js +16 -3
- package/dist/react/index.js.map +4 -4
- package/dist/react/jsxDevRuntimeCompat.js +22 -2
- package/dist/react/jsxDevRuntimeCompat.js.map +5 -4
- package/dist/react/jsxRuntimeCompat.js +90 -0
- package/dist/react/jsxRuntimeCompat.js.map +10 -0
- package/dist/react/server.js +16 -3
- package/dist/react/server.js.map +4 -4
- package/dist/src/build/compileVue.d.ts +1 -1
- package/dist/src/build/resolveBuildMode.d.ts +7 -0
- package/dist/src/cli/scripts/dev.d.ts +4 -1
- package/dist/src/cli/scripts/mobile.d.ts +1 -0
- package/dist/src/cli/utils.d.ts +2 -2
- package/dist/src/core/build.d.ts +1 -1
- package/dist/src/dev/clientManager.d.ts +7 -0
- package/dist/src/dev/configResolver.d.ts +2 -0
- package/dist/src/dev/moduleMapper.d.ts +1 -1
- package/dist/src/dev/moduleServer.d.ts +2 -1
- package/dist/src/dev/pathUtils.d.ts +1 -1
- package/dist/src/dev/reactComponentClassifier.d.ts +1 -1
- package/dist/src/mobile/androidConformance.d.ts +42 -0
- package/dist/src/mobile/androidEmulatorController.d.ts +100 -0
- package/dist/src/mobile/androidNativeWatcher.d.ts +19 -0
- package/dist/src/mobile/androidRelease.d.ts +51 -0
- package/dist/src/mobile/androidWebView.d.ts +46 -0
- package/dist/src/mobile/associationFiles.d.ts +49 -0
- package/dist/src/mobile/buildMetadata.d.ts +10 -0
- package/dist/src/mobile/buildPipeline.d.ts +8 -0
- package/dist/src/mobile/buildRelease.d.ts +24 -0
- package/dist/src/mobile/capacitorBundle.d.ts +28 -0
- package/dist/src/mobile/capacitorProject.d.ts +9 -0
- package/dist/src/mobile/client.d.ts +1 -1
- package/dist/src/mobile/config.d.ts +17 -0
- package/dist/src/mobile/emulatorDoctor.d.ts +25 -0
- package/dist/src/mobile/emulatorInstaller.d.ts +41 -0
- package/dist/src/mobile/index.d.ts +14 -0
- package/dist/src/mobile/iosRelease.d.ts +61 -0
- package/dist/src/mobile/materializedBundle.d.ts +14 -0
- package/dist/src/mobile/nativeDeepLinks.d.ts +5 -0
- package/dist/src/mobile/releaseArtifact.d.ts +1 -0
- package/dist/src/mobile/releaseDoctor.d.ts +13 -0
- package/dist/src/mobile/releasePublisher.d.ts +130 -0
- package/dist/src/mobile/routeMatcher.d.ts +3 -0
- package/dist/src/mobile/routeMetadataTransform.d.ts +13 -0
- package/dist/src/mobile/shellBootstrap.d.ts +1 -0
- package/dist/src/mobile/transport.d.ts +31 -0
- package/dist/src/react/hooks/useMediaQuery.d.ts +1 -1
- package/dist/src/react/jsxDevRuntimeCompat.d.ts +1 -1
- package/dist/src/react/jsxRuntimeCompat.d.ts +7 -0
- package/dist/src/react/pageHandler.d.ts +3 -0
- package/dist/src/utils/loadConfig.d.ts +1 -0
- package/dist/src/utils/logger.d.ts +2 -0
- package/dist/src/utils/userAgentFunctions.d.ts +1 -1
- package/dist/svelte/index.js +14 -1
- package/dist/svelte/index.js.map +3 -3
- package/dist/svelte/server.js +14 -1
- package/dist/svelte/server.js.map +3 -3
- package/dist/types/build.d.ts +45 -0
- package/dist/types/cli.d.ts +2 -0
- package/dist/types/globals.d.ts +14 -0
- package/dist/types/messages.d.ts +12 -0
- package/dist/vue/index.js +14 -1
- package/dist/vue/index.js.map +3 -3
- package/dist/vue/server.js +14 -1
- package/dist/vue/server.js.map +3 -3
- package/package.json +35 -7
package/dist/mobile/index.js
CHANGED
|
@@ -60,6 +60,105 @@ var __legacyMetadataTS = (k, v) => {
|
|
|
60
60
|
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
61
61
|
var __require = import.meta.require;
|
|
62
62
|
|
|
63
|
+
// src/constants.ts
|
|
64
|
+
var ANGULAR_INIT_TIMEOUT_MS = 500, ANSI_ESCAPE_CODE = 27, ANSI_ESCAPE_LENGTH = 3, ASCII_SPACE = 32, BASE_36_RADIX = 36, BUN_BUILD_WARNING_SUPPRESSION = "wildcard sideEffects are not supported yet", TAILWIND_BUN_CSS_WARNING_PATTERN, BODY_SLICE_LENGTH = 2000, BYTES_PER_KILOBYTE = 1024, CLI_ARGS_OFFSET = 3, CSS_ERROR_RESOLVE_DELAY_MS = 50, CSS_MAX_CHECK_ATTEMPTS = 10, CSS_MAX_PARSE_TIMEOUT_MS = 500, CSS_SHEET_READY_TIMEOUT_MS = 100, DEFAULT_CHUNK_SIZE = 16384, DEFAULT_DEBOUNCE_MS = 15, DEFAULT_PORT = 3000, DEV_SERVER_RESTART_DEBOUNCE_MS = 100, DOM_UPDATE_DELAY_MS = 50, FILE_PROTOCOL_PREFIX_LENGTH = 7, FOCUS_ID_PREFIX_LENGTH = 3, FOCUS_IDX_PREFIX_LENGTH = 4, FOCUS_NAME_PREFIX_LENGTH = 5, ESLINT_STUDIO_DEFAULT_HOST = "eslint.absolute.localhost", ESLINT_STUDIO_DEFAULT_PORT = 4099, CONFIG_DEFAULT_HOST = "config.absolute.localhost", CONFIG_DEFAULT_PORT = 4099, HMR_UPDATE_TIMEOUT_MS = 2000, HOOK_SIGNATURE_LENGTH = 12, EXCLUDE_LAST_OFFSET = -1, HTTP_STATUS_OK = 200, HTTP_STATUS_BAD_REQUEST = 400, HTTP_STATUS_NOT_FOUND = 404, HOURS_IN_DAY = 24, HOURS_IN_HALF_DAY = 12, IMAGE_DEFAULT_DEVICE_SIZES, IMAGE_DEFAULT_IMAGE_SIZES, IMAGE_DEFAULT_QUALITY = 75, IMAGE_GLOB_SUFFIX_LENGTH = 2, INSTANCE_PROBE_TIMEOUT_MS = 250, MAX_ERROR_LENGTH = 200, MAX_RECONNECT_ATTEMPTS = 60, MILLISECONDS_IN_A_SECOND = 1000, MINUTES_IN_AN_HOUR = 60, SECONDS_IN_A_MINUTE = 60, MILLISECONDS_IN_A_MINUTE, MILLISECONDS_IN_A_DAY, OVERLAY_FADE_DURATION_MS = 150, PING_INTERVAL_MS = 30000, LIST_LOG_TAIL_MAX_BYTES = 65536, LIST_TUI_COLUMN_GAP = 2, LIST_TUI_DEFAULT_HEIGHT = 28, LIST_TUI_DEFAULT_WIDTH = 100, LIST_TUI_ESCAPE_SEQUENCE_TIMEOUT_MS = 30, LIST_TUI_FOOTER_LINE_COUNT = 2, LIST_TUI_MARKER_WIDTH = 2, LIST_TUI_MIN_LOG_HEIGHT = 3, LIST_TUI_MIN_URL_WIDTH = 16, LIST_TUI_RENDER_DEBOUNCE_MS = 16, LIST_TUI_STATUS_MESSAGE_TIMEOUT_MS = 4000, LIST_WATCH_REFRESH_MS = 1000, RAF_BATCH_COUNT = 3, RANDOM_ID_END_INDEX = 11, REBUILD_BATCH_DELAY_MS = 10, REBUILD_RELOAD_DELAY_MS = 200, RECONNECT_INITIAL_DELAY_MS = 500, RECONNECT_POLL_INTERVAL_MS = 300, REACT_STREAM_SLOT_FAST_DELAY_MS = 5, REACT_STREAM_SLOT_SLOW_DELAY_MS = 20, SIGINT_EXIT_CODE = 130, SIGTERM_EXIT_CODE = 143, SVELTE_CSS_LOAD_TIMEOUT_MS = 500, TIME_PRECISION = 2, TWO_THIRDS, UNFOUND_INDEX = -1, WEBSOCKET_NORMAL_CLOSURE = 1000, DEFAULT_WEBSOCKET_IDLE_TIMEOUT_SECONDS = 240, DEFAULT_HTTP_IDLE_TIMEOUT_SECONDS = 240, MAX_HTTP_IDLE_TIMEOUT_SECONDS = 255, WORKSPACE_COMMAND_ARGS_OFFSET = 3, WORKSPACE_FAILURE_LOG_PRINT_LIMIT = 30, WORKSPACE_FAILURE_RECENT_LOG_LIMIT = 60, WORKSPACE_READY_ATTEMPT_TIMEOUT_MS = 5000, WORKSPACE_READY_PROBE_INTERVAL_MS = 250, WORKSPACE_READY_TIMEOUT_MS = 30000, WORKSPACE_SHUTDOWN_TIMEOUT_MS = 1e4, WORKSPACE_TUI_DEFAULT_HEIGHT = 28, WORKSPACE_TUI_DEFAULT_WIDTH = 100, WORKSPACE_TUI_ESCAPE_SEQUENCE_TIMEOUT_MS = 30, WORKSPACE_TUI_FOOTER_LINE_COUNT = 3, WORKSPACE_TUI_MIN_LOG_HEIGHT = 3, WORKSPACE_TUI_MIN_SERVICE_NAME_WIDTH = 7, WORKSPACE_TUI_MIN_TARGET_WIDTH = 8, WORKSPACE_TUI_MIN_WRAP_WIDTH = 12, WORKSPACE_TUI_PROMPT_CURSOR_OFFSET = 3, WORKSPACE_TUI_RECENT_LOG_LIMIT = 40, WORKSPACE_TUI_RENDER_DEBOUNCE_MS = 16, WORKSPACE_TUI_STATUS_WIDTH = 10, WORKSPACE_TUI_TARGET_PADDING_WIDTH = 6, WORKSPACE_TUI_VISIBILITY_WIDTH = 8;
|
|
65
|
+
var init_constants = __esm(() => {
|
|
66
|
+
TAILWIND_BUN_CSS_WARNING_PATTERN = /invalid @ rule encountered: '@(theme|tailwind|source|utility|variant|custom-variant|apply|reference|plugin|config)'/;
|
|
67
|
+
IMAGE_DEFAULT_DEVICE_SIZES = [
|
|
68
|
+
640,
|
|
69
|
+
750,
|
|
70
|
+
828,
|
|
71
|
+
1080,
|
|
72
|
+
1200,
|
|
73
|
+
1920,
|
|
74
|
+
2048,
|
|
75
|
+
3840
|
|
76
|
+
];
|
|
77
|
+
IMAGE_DEFAULT_IMAGE_SIZES = [16, 32, 48, 64, 96, 128, 256, 384];
|
|
78
|
+
MILLISECONDS_IN_A_MINUTE = MILLISECONDS_IN_A_SECOND * SECONDS_IN_A_MINUTE;
|
|
79
|
+
MILLISECONDS_IN_A_DAY = MILLISECONDS_IN_A_SECOND * SECONDS_IN_A_MINUTE * MINUTES_IN_AN_HOUR * HOURS_IN_DAY;
|
|
80
|
+
TWO_THIRDS = 2 / 3;
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// src/utils/getDurationString.ts
|
|
84
|
+
var getDurationString = (duration) => {
|
|
85
|
+
let durationString;
|
|
86
|
+
if (duration < MILLISECONDS_IN_A_SECOND) {
|
|
87
|
+
durationString = `${duration.toFixed(TIME_PRECISION)}ms`;
|
|
88
|
+
} else if (duration < MILLISECONDS_IN_A_MINUTE) {
|
|
89
|
+
durationString = `${(duration / MILLISECONDS_IN_A_SECOND).toFixed(TIME_PRECISION)}s`;
|
|
90
|
+
} else {
|
|
91
|
+
const totalSeconds = Math.round(duration / MILLISECONDS_IN_A_SECOND);
|
|
92
|
+
const minutes = Math.floor(totalSeconds / 60);
|
|
93
|
+
const seconds = totalSeconds % 60;
|
|
94
|
+
durationString = seconds === 0 ? `${minutes}m` : `${minutes}m ${seconds}s`;
|
|
95
|
+
}
|
|
96
|
+
return durationString;
|
|
97
|
+
};
|
|
98
|
+
var init_getDurationString = __esm(() => {
|
|
99
|
+
init_constants();
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// src/utils/startupBanner.ts
|
|
103
|
+
var colors, MONTHS, formatTimestamp = () => {
|
|
104
|
+
const now = new Date;
|
|
105
|
+
const month = MONTHS[now.getMonth()];
|
|
106
|
+
const day = now.getDate().toString().padStart(2, "0");
|
|
107
|
+
let hours = now.getHours();
|
|
108
|
+
const minutes = now.getMinutes().toString().padStart(2, "0");
|
|
109
|
+
const seconds = now.getSeconds().toString().padStart(2, "0");
|
|
110
|
+
const ampm = hours >= HOURS_IN_HALF_DAY ? "PM" : "AM";
|
|
111
|
+
hours = hours % HOURS_IN_HALF_DAY || HOURS_IN_HALF_DAY;
|
|
112
|
+
return `${month} ${day} ${hours}:${minutes}:${seconds} ${ampm}`;
|
|
113
|
+
}, startupBanner = (options) => {
|
|
114
|
+
const {
|
|
115
|
+
version,
|
|
116
|
+
readyDuration,
|
|
117
|
+
buildDuration,
|
|
118
|
+
port,
|
|
119
|
+
host,
|
|
120
|
+
networkUrl,
|
|
121
|
+
protocol = "http"
|
|
122
|
+
} = options;
|
|
123
|
+
const name = `${colors.cyan}${colors.bold}ABSOLUTEJS${colors.reset}`;
|
|
124
|
+
const ver = `${colors.dim}v${version}${colors.reset}`;
|
|
125
|
+
const time = `${colors.dim}ready in${colors.reset} ${colors.bold}${getDurationString(readyDuration)}${colors.reset}`;
|
|
126
|
+
const build = typeof buildDuration === "number" && Number.isFinite(buildDuration) ? ` ${colors.dim}(build ${getDurationString(buildDuration)})${colors.reset}` : "";
|
|
127
|
+
console.log("");
|
|
128
|
+
console.log(` ${name} ${ver} ${time}${build}`);
|
|
129
|
+
console.log("");
|
|
130
|
+
console.log(` ${colors.green}\u279C${colors.reset} ${colors.bold}Local:${colors.reset} ${protocol}://${host === "0.0.0.0" ? "localhost" : host}:${port}/`);
|
|
131
|
+
if (networkUrl) {
|
|
132
|
+
console.log(` ${colors.green}\u279C${colors.reset} ${colors.bold}Network:${colors.reset} ${networkUrl}`);
|
|
133
|
+
}
|
|
134
|
+
console.log("");
|
|
135
|
+
};
|
|
136
|
+
var init_startupBanner = __esm(() => {
|
|
137
|
+
init_constants();
|
|
138
|
+
init_getDurationString();
|
|
139
|
+
colors = {
|
|
140
|
+
bold: "\x1B[1m",
|
|
141
|
+
cyan: "\x1B[36m",
|
|
142
|
+
dim: "\x1B[2m",
|
|
143
|
+
green: "\x1B[32m",
|
|
144
|
+
reset: "\x1B[0m"
|
|
145
|
+
};
|
|
146
|
+
MONTHS = [
|
|
147
|
+
"Jan",
|
|
148
|
+
"Feb",
|
|
149
|
+
"Mar",
|
|
150
|
+
"Apr",
|
|
151
|
+
"May",
|
|
152
|
+
"Jun",
|
|
153
|
+
"Jul",
|
|
154
|
+
"Aug",
|
|
155
|
+
"Sep",
|
|
156
|
+
"Oct",
|
|
157
|
+
"Nov",
|
|
158
|
+
"Dec"
|
|
159
|
+
];
|
|
160
|
+
});
|
|
161
|
+
|
|
63
162
|
// src/mobile/artifactStore.ts
|
|
64
163
|
import { createHash as createHash2 } from "crypto";
|
|
65
164
|
import {
|
|
@@ -152,6 +251,7 @@ var parseCompatibilityPage = (value) => {
|
|
|
152
251
|
}
|
|
153
252
|
return {
|
|
154
253
|
bundleHash: readString(value.bundleHash, "page.bundleHash"),
|
|
254
|
+
bundlePath: readString(value.bundlePath, "page.bundlePath"),
|
|
155
255
|
contract: readString(value.contract, "page.contract"),
|
|
156
256
|
framework: value.framework,
|
|
157
257
|
pageId: readString(value.pageId, "page.pageId"),
|
|
@@ -189,6 +289,7 @@ var validateProducerModule = (module) => {
|
|
|
189
289
|
};
|
|
190
290
|
var normalizePage = (page) => ({
|
|
191
291
|
bundleHash: requireNonEmpty(page.bundleHash, "page.bundleHash"),
|
|
292
|
+
bundlePath: requireNonEmpty(page.bundlePath, "page.bundlePath"),
|
|
192
293
|
contract: requireNonEmpty(page.contract, "page.contract"),
|
|
193
294
|
framework: page.framework,
|
|
194
295
|
pageId: requireNonEmpty(page.pageId, "page.pageId"),
|
|
@@ -513,183 +614,1546 @@ var verifyAbsoluteMobileCompatibilityProducer = async (release, maxProducerBytes
|
|
|
513
614
|
}
|
|
514
615
|
return release;
|
|
515
616
|
};
|
|
516
|
-
// src/mobile/
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
617
|
+
// src/mobile/androidRelease.ts
|
|
618
|
+
import { createHash as createHash4 } from "crypto";
|
|
619
|
+
import {
|
|
620
|
+
access as access3,
|
|
621
|
+
copyFile as copyFile2,
|
|
622
|
+
mkdir as mkdir3,
|
|
623
|
+
mkdtemp as mkdtemp2,
|
|
624
|
+
readFile as readFile4,
|
|
625
|
+
rename as rename4,
|
|
626
|
+
rm as rm3,
|
|
627
|
+
stat,
|
|
628
|
+
writeFile as writeFile4
|
|
629
|
+
} from "fs/promises";
|
|
630
|
+
import { dirname as dirname2, isAbsolute as isAbsolute2, join as join4, relative as relative3, resolve as resolve3, sep as sep2 } from "path";
|
|
631
|
+
|
|
632
|
+
// src/mobile/emulatorDoctor.ts
|
|
633
|
+
import { homedir } from "os";
|
|
634
|
+
import { join as join2 } from "path";
|
|
635
|
+
|
|
636
|
+
// src/cli/utils.ts
|
|
637
|
+
import { existsSync, readFileSync } from "fs";
|
|
638
|
+
init_constants();
|
|
639
|
+
init_startupBanner();
|
|
640
|
+
var isWSLEnvironment = () => {
|
|
641
|
+
try {
|
|
642
|
+
const release = readFileSync("/proc/version", "utf-8");
|
|
643
|
+
return /microsoft|wsl/i.test(release);
|
|
644
|
+
} catch {
|
|
645
|
+
return false;
|
|
532
646
|
}
|
|
533
|
-
|
|
534
|
-
|
|
647
|
+
};
|
|
648
|
+
|
|
649
|
+
// src/mobile/emulatorDoctor.ts
|
|
650
|
+
var windowsPathToWsl = (path) => {
|
|
651
|
+
const match = /^([a-z]):[\\/](.*)$/i.exec(path.trim());
|
|
652
|
+
if (!match)
|
|
653
|
+
return path.trim().replaceAll("\\", "/");
|
|
654
|
+
return `/mnt/${match[1]?.toLowerCase()}/${match[2]?.replaceAll("\\", "/")}`;
|
|
655
|
+
};
|
|
656
|
+
var windowsLocalAppDataFromWsl = () => {
|
|
657
|
+
try {
|
|
658
|
+
const result = Bun.spawnSync(["cmd.exe", "/d", "/c", "echo", "%LOCALAPPDATA%"], { stderr: "ignore", stdout: "pipe" });
|
|
659
|
+
if (result.exitCode !== 0)
|
|
660
|
+
return;
|
|
661
|
+
const output = result.stdout.toString().trim();
|
|
662
|
+
if (!output || output === "%LOCALAPPDATA%")
|
|
663
|
+
return;
|
|
664
|
+
return windowsPathToWsl(output);
|
|
665
|
+
} catch {
|
|
535
666
|
return;
|
|
536
667
|
}
|
|
537
|
-
|
|
668
|
+
};
|
|
669
|
+
var absoluteManagedAndroidSdkRoot = (host, env = process.env) => {
|
|
670
|
+
if (host === "windows") {
|
|
671
|
+
return join2(env.LOCALAPPDATA ?? join2(homedir(), "AppData", "Local"), "AbsoluteJS", "Android", "Sdk");
|
|
672
|
+
}
|
|
673
|
+
if (host === "wsl") {
|
|
674
|
+
const localAppData = windowsLocalAppDataFromWsl();
|
|
675
|
+
if (localAppData) {
|
|
676
|
+
return join2(localAppData, "AbsoluteJS", "Android", "Sdk");
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
return join2(homedir(), ".absolutejs", "android-sdk");
|
|
680
|
+
};
|
|
681
|
+
var detectAbsoluteMobileHost = (platform = process.platform, wsl = isWSLEnvironment()) => {
|
|
682
|
+
if (platform === "darwin")
|
|
683
|
+
return "macos";
|
|
684
|
+
if (platform === "win32")
|
|
685
|
+
return "windows";
|
|
686
|
+
if (platform === "linux" && wsl)
|
|
687
|
+
return "wsl";
|
|
688
|
+
return "linux";
|
|
538
689
|
};
|
|
539
690
|
|
|
540
|
-
// src/mobile/
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
691
|
+
// src/mobile/androidEmulatorController.ts
|
|
692
|
+
import {
|
|
693
|
+
access as access2,
|
|
694
|
+
copyFile,
|
|
695
|
+
lstat,
|
|
696
|
+
mkdir as mkdir2,
|
|
697
|
+
readFile as readFile3,
|
|
698
|
+
readdir as readdir2,
|
|
699
|
+
readlink,
|
|
700
|
+
realpath,
|
|
701
|
+
rename as rename3,
|
|
702
|
+
rm as rm2,
|
|
703
|
+
writeFile as writeFile3
|
|
704
|
+
} from "fs/promises";
|
|
705
|
+
import { createHash as createHash3, randomUUID } from "crypto";
|
|
706
|
+
import {
|
|
707
|
+
dirname,
|
|
708
|
+
isAbsolute,
|
|
709
|
+
join as join3,
|
|
710
|
+
relative as relative2,
|
|
711
|
+
resolve as resolve2,
|
|
712
|
+
sep,
|
|
713
|
+
win32
|
|
714
|
+
} from "path";
|
|
715
|
+
|
|
716
|
+
// src/mobile/capacitorProject.ts
|
|
717
|
+
import { access, readFile as readFile2, rename as rename2, writeFile as writeFile2 } from "fs/promises";
|
|
718
|
+
import { relative, resolve } from "path";
|
|
719
|
+
var CONFIG_FILE = "capacitor.config.ts";
|
|
720
|
+
var portableRelative = (root, path) => relative(root, path).replaceAll("\\", "/");
|
|
721
|
+
var capacitorConfigSource = (config, projectRoot) => `import type { CapacitorConfig } from '@capacitor/cli';
|
|
722
|
+
|
|
723
|
+
const config: CapacitorConfig = {
|
|
724
|
+
appId: ${JSON.stringify(config.appId)},
|
|
725
|
+
appName: ${JSON.stringify(config.appName)},
|
|
726
|
+
webDir: ${JSON.stringify(portableRelative(projectRoot, config.bundleDirectory))},
|
|
727
|
+
android: {
|
|
728
|
+
path: ${JSON.stringify(portableRelative(projectRoot, `${config.nativeProjectDirectory}/android`))}
|
|
729
|
+
},
|
|
730
|
+
ios: {
|
|
731
|
+
path: ${JSON.stringify(portableRelative(projectRoot, `${config.nativeProjectDirectory}/ios`))}
|
|
732
|
+
}
|
|
554
733
|
};
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
734
|
+
|
|
735
|
+
export default config;
|
|
736
|
+
`;
|
|
737
|
+
var exists = async (path) => {
|
|
738
|
+
try {
|
|
739
|
+
await access(path);
|
|
740
|
+
return true;
|
|
741
|
+
} catch {
|
|
558
742
|
return false;
|
|
559
|
-
|
|
743
|
+
}
|
|
560
744
|
};
|
|
561
|
-
var
|
|
562
|
-
const
|
|
563
|
-
|
|
745
|
+
var writeAbsoluteCapacitorConfig = async (config, options) => {
|
|
746
|
+
const projectRoot = resolve(options.projectRoot);
|
|
747
|
+
const destination = resolve(projectRoot, CONFIG_FILE);
|
|
748
|
+
const source = capacitorConfigSource(config, projectRoot);
|
|
749
|
+
if (await exists(destination)) {
|
|
750
|
+
const current = await readFile2(destination, "utf8");
|
|
751
|
+
if (current === source)
|
|
752
|
+
return { changed: false, path: destination };
|
|
753
|
+
if (!options.force) {
|
|
754
|
+
throw new TypeError(`${CONFIG_FILE} already exists and differs; rerun with --force only after reviewing it.`);
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
const temporary = `${destination}.${crypto.randomUUID()}.tmp`;
|
|
758
|
+
await writeFile2(temporary, source, { flag: "wx" });
|
|
759
|
+
await rename2(temporary, destination);
|
|
760
|
+
return { changed: true, path: destination };
|
|
564
761
|
};
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
762
|
+
|
|
763
|
+
// src/mobile/androidEmulatorController.ts
|
|
764
|
+
init_getDurationString();
|
|
765
|
+
var HASH_RADIX = 16;
|
|
766
|
+
var EXECUTABLE_MODE_MASK = 73;
|
|
767
|
+
var NATIVE_PUBLIC_PATH_SEGMENTS = 5;
|
|
768
|
+
var CAPACITOR_PROJECT_DIRECTORY_PATTERN = /project\(['"](:[^'"]+)['"]\)\.projectDir\s*=\s*new File\(['"]([^'"]+)['"]\)/gu;
|
|
769
|
+
var pathExists = async (path) => {
|
|
770
|
+
try {
|
|
771
|
+
await access2(path);
|
|
772
|
+
return true;
|
|
773
|
+
} catch {
|
|
774
|
+
return false;
|
|
568
775
|
}
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
776
|
+
};
|
|
777
|
+
var forwardCommandOutput = (stream, destination) => stream.pipeTo(new WritableStream({
|
|
778
|
+
write: (chunk) => {
|
|
779
|
+
destination.write(chunk);
|
|
780
|
+
}
|
|
781
|
+
}));
|
|
782
|
+
var runCommand = async (command, options = {}) => {
|
|
783
|
+
const subprocess = Bun.spawn(command, {
|
|
784
|
+
cwd: options.cwd,
|
|
785
|
+
env: options.env,
|
|
786
|
+
signal: options.signal,
|
|
787
|
+
stderr: "pipe",
|
|
788
|
+
stdin: "ignore",
|
|
789
|
+
stdout: "pipe"
|
|
790
|
+
});
|
|
791
|
+
const [exitCode] = await Promise.all([
|
|
792
|
+
subprocess.exited,
|
|
793
|
+
forwardCommandOutput(subprocess.stdout, process.stdout),
|
|
794
|
+
forwardCommandOutput(subprocess.stderr, process.stderr)
|
|
795
|
+
]);
|
|
796
|
+
return exitCode;
|
|
797
|
+
};
|
|
798
|
+
var captureCommand = (command, options = {}) => {
|
|
799
|
+
try {
|
|
800
|
+
const result = Bun.spawnSync(command, {
|
|
801
|
+
cwd: options.cwd,
|
|
802
|
+
env: options.env,
|
|
803
|
+
stderr: "pipe",
|
|
804
|
+
stdout: "pipe"
|
|
805
|
+
});
|
|
576
806
|
return {
|
|
577
|
-
|
|
578
|
-
|
|
807
|
+
exitCode: result.exitCode,
|
|
808
|
+
stderr: result.stderr.toString(),
|
|
809
|
+
stdout: result.stdout.toString()
|
|
579
810
|
};
|
|
580
|
-
}
|
|
581
|
-
if (!runtime) {
|
|
811
|
+
} catch (error) {
|
|
582
812
|
return {
|
|
583
|
-
|
|
584
|
-
|
|
813
|
+
exitCode: 1,
|
|
814
|
+
stderr: error instanceof Error ? error.message : String(error),
|
|
815
|
+
stdout: ""
|
|
585
816
|
};
|
|
586
817
|
}
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
818
|
+
};
|
|
819
|
+
var ASCII_ESCAPE = 27;
|
|
820
|
+
var ANSI_SEQUENCE_PATTERN = new RegExp(`${String.fromCharCode(ASCII_ESCAPE)}(?:\\[[0-?]*[ -/]*[@-~]|[@-_])`, "gu");
|
|
821
|
+
var requireSuccess = async (command, label, run, options) => {
|
|
822
|
+
const exitCode = await run(command, options);
|
|
823
|
+
throwIfAborted(options?.signal);
|
|
824
|
+
if (exitCode !== 0)
|
|
825
|
+
throw new Error(`${label} failed (exit ${exitCode}).`);
|
|
826
|
+
};
|
|
827
|
+
var throwIfAborted = (signal) => {
|
|
828
|
+
if (!signal?.aborted)
|
|
829
|
+
return;
|
|
830
|
+
throw new DOMException("Android development startup was cancelled.", "AbortError");
|
|
831
|
+
};
|
|
832
|
+
var nativeDependencySources = async (nativeDirectory) => {
|
|
833
|
+
const settings = await readFile3(join3(nativeDirectory, "capacitor.settings.gradle"), "utf8");
|
|
834
|
+
const pattern = new RegExp(CAPACITOR_PROJECT_DIRECTORY_PATTERN.source, CAPACITOR_PROJECT_DIRECTORY_PATTERN.flags);
|
|
835
|
+
const dependencies = [...settings.matchAll(pattern)].map((match) => ({
|
|
836
|
+
name: (match[1] ?? "").slice(1).replaceAll(/[^a-zA-Z0-9_.-]/gu, "_"),
|
|
837
|
+
source: resolve2(nativeDirectory, match[2] ?? "")
|
|
838
|
+
}));
|
|
839
|
+
if (dependencies.length === 0) {
|
|
840
|
+
throw new Error("Capacitor Android settings did not declare any native dependencies.");
|
|
592
841
|
}
|
|
593
|
-
|
|
842
|
+
return { dependencies, settings };
|
|
843
|
+
};
|
|
844
|
+
var shouldIgnoreNativePath = (relativePath, ignorePublicBundle) => {
|
|
845
|
+
const parts = relativePath.split(sep);
|
|
846
|
+
if (parts.includes(".gradle") || parts.includes("build") || parts.includes(".absolutejs-dependencies")) {
|
|
847
|
+
return true;
|
|
848
|
+
}
|
|
849
|
+
return ignorePublicBundle && parts.slice(0, NATIVE_PUBLIC_PATH_SEGMENTS).join("/") === "app/src/main/assets/public";
|
|
850
|
+
};
|
|
851
|
+
var collectNativePath = async (root, label, path, isDirectory, isFile, isSymbolicLink, ignorePublicBundle) => {
|
|
852
|
+
const relativePath = relative2(root, path);
|
|
853
|
+
if (shouldIgnoreNativePath(relativePath, ignorePublicBundle))
|
|
854
|
+
return [];
|
|
855
|
+
const identity = `${label}:${relativePath.split(sep).join("/")}\x00`;
|
|
856
|
+
if (isDirectory) {
|
|
857
|
+
return collectNativeDirectory(root, label, path, ignorePublicBundle);
|
|
858
|
+
}
|
|
859
|
+
if (isSymbolicLink) {
|
|
860
|
+
return [`link\x00${identity}${await readlink(path)}\x00`];
|
|
861
|
+
}
|
|
862
|
+
if (!isFile)
|
|
863
|
+
return [];
|
|
864
|
+
const [metadata, contents] = await Promise.all([
|
|
865
|
+
lstat(path),
|
|
866
|
+
readFile3(path)
|
|
867
|
+
]);
|
|
868
|
+
const contentDigest = createHash3("sha256").update(contents).digest("hex");
|
|
869
|
+
return [
|
|
870
|
+
`file\x00${identity}${metadata.mode & EXECUTABLE_MODE_MASK}\x00${contentDigest}\x00`
|
|
871
|
+
];
|
|
872
|
+
};
|
|
873
|
+
var collectNativeDirectory = async (root, label, directory, ignorePublicBundle) => {
|
|
874
|
+
const entries = await readdir2(directory, { withFileTypes: true });
|
|
875
|
+
entries.sort((left, right) => left.name.localeCompare(right.name));
|
|
876
|
+
const records = await Promise.all(entries.map((entry) => collectNativePath(root, label, join3(directory, entry.name), entry.isDirectory(), entry.isFile(), entry.isSymbolicLink(), ignorePublicBundle)));
|
|
877
|
+
return records.flat();
|
|
878
|
+
};
|
|
879
|
+
var hashNativeTree = async (root, label, ignorePublicBundle) => {
|
|
880
|
+
const resolvedRoot = await realpath(root);
|
|
881
|
+
const records = await collectNativeDirectory(resolvedRoot, label, resolvedRoot, ignorePublicBundle);
|
|
882
|
+
return createHash3("sha256").update(records.join("")).digest("hex");
|
|
883
|
+
};
|
|
884
|
+
var fingerprintAbsoluteAndroidNativeProject = async (project) => {
|
|
885
|
+
const { dependencies } = await nativeDependencySources(project.nativeDirectory);
|
|
886
|
+
const roots = [
|
|
887
|
+
{
|
|
888
|
+
ignorePublicBundle: true,
|
|
889
|
+
label: "android",
|
|
890
|
+
source: project.nativeDirectory
|
|
891
|
+
},
|
|
892
|
+
...dependencies.sort((left, right) => left.name.localeCompare(right.name)).map((dependency) => ({
|
|
893
|
+
ignorePublicBundle: false,
|
|
894
|
+
label: `dependency:${dependency.name}`,
|
|
895
|
+
source: dependency.source
|
|
896
|
+
}))
|
|
897
|
+
];
|
|
898
|
+
const treeDigests = await Promise.all(roots.map((root) => hashNativeTree(root.source, root.label, root.ignorePublicBundle)));
|
|
899
|
+
return createHash3("sha256").update(treeDigests.join("\x00")).digest("hex");
|
|
900
|
+
};
|
|
901
|
+
var windowsPathFromWsl = (path, capture) => {
|
|
902
|
+
const result = capture(["wslpath", "-w", path]);
|
|
903
|
+
if (result.exitCode !== 0 || !result.stdout.trim()) {
|
|
904
|
+
throw new Error(`Could not translate WSL path for Windows: ${path}`);
|
|
905
|
+
}
|
|
906
|
+
return result.stdout.trim();
|
|
907
|
+
};
|
|
908
|
+
var mirroredCapacitorDependencies = async (project, capture) => {
|
|
909
|
+
const settingsPath = join3(project.nativeDirectory, "capacitor.settings.gradle");
|
|
910
|
+
const settings = await readFile3(settingsPath, "utf8");
|
|
911
|
+
const dependencies = [];
|
|
912
|
+
const rewrittenSettings = settings.replace(CAPACITOR_PROJECT_DIRECTORY_PATTERN, (_statement, projectName, sourcePath) => {
|
|
913
|
+
const name = projectName.slice(1).replaceAll(/[^a-zA-Z0-9_.-]/gu, "_");
|
|
914
|
+
const source = resolve2(project.nativeDirectory, sourcePath);
|
|
915
|
+
dependencies.push({
|
|
916
|
+
name,
|
|
917
|
+
windowsSource: windowsPathFromWsl(source, capture)
|
|
918
|
+
});
|
|
919
|
+
return `project('${projectName}').projectDir = new File('./.absolutejs-dependencies/${name}')`;
|
|
920
|
+
});
|
|
921
|
+
if (dependencies.length === 0) {
|
|
922
|
+
throw new Error("Capacitor Android settings did not declare any native dependencies.");
|
|
923
|
+
}
|
|
924
|
+
return { dependencies, rewrittenSettings };
|
|
925
|
+
};
|
|
926
|
+
var encodedWindowsGradleCommand = (windowsSource, windowsDirectory, windowsAndroidRoot, dependencies, rewrittenSettings, task, gradleArguments) => {
|
|
927
|
+
const sourceDirectory = Buffer.from(windowsSource, "utf8").toString("base64");
|
|
928
|
+
const buildDirectory = Buffer.from(windowsDirectory, "utf8").toString("base64");
|
|
929
|
+
const androidRoot = Buffer.from(windowsAndroidRoot, "utf8").toString("base64");
|
|
930
|
+
const dependencyData = Buffer.from(JSON.stringify(dependencies), "utf8").toString("base64");
|
|
931
|
+
const settingsData = Buffer.from(rewrittenSettings, "utf8").toString("base64");
|
|
932
|
+
const argumentsData = Buffer.from(JSON.stringify(gradleArguments), "utf8").toString("base64");
|
|
933
|
+
const source = [
|
|
934
|
+
"$ErrorActionPreference = 'Stop'",
|
|
935
|
+
`$source = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('${sourceDirectory}'))`,
|
|
936
|
+
`$directory = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('${buildDirectory}'))`,
|
|
937
|
+
`$androidHome = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('${androidRoot}'))`,
|
|
938
|
+
`$dependencies = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('${dependencyData}')) | ConvertFrom-Json`,
|
|
939
|
+
`$settings = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('${settingsData}'))`,
|
|
940
|
+
`$gradleArguments = @([Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('${argumentsData}')) | ConvertFrom-Json)`,
|
|
941
|
+
"$env:ANDROID_HOME = $androidHome",
|
|
942
|
+
"$env:ANDROID_SDK_ROOT = $androidHome",
|
|
943
|
+
"New-Item -ItemType Directory -Force -Path $directory | Out-Null",
|
|
944
|
+
"& robocopy.exe $source $directory /MIR /XD .gradle build .absolutejs-dependencies /NFL /NDL /NJH /NJS /NP",
|
|
945
|
+
"$copyExit = $LASTEXITCODE",
|
|
946
|
+
"if ($copyExit -ge 8) { exit $copyExit }",
|
|
947
|
+
"foreach ($dependency in @($dependencies)) { $target = Join-Path $directory ('.absolutejs-dependencies\\' + $dependency.name); New-Item -ItemType Directory -Force -Path $target | Out-Null; & robocopy.exe $dependency.windowsSource $target /MIR /XD .gradle build /NFL /NDL /NJH /NJS /NP; if ($LASTEXITCODE -ge 8) { exit $LASTEXITCODE } }",
|
|
948
|
+
"[IO.File]::WriteAllText((Join-Path $directory 'capacitor.settings.gradle'), $settings)",
|
|
949
|
+
"$wrapper = Join-Path $directory 'gradlew.bat'",
|
|
950
|
+
`& $wrapper --no-daemon --console=plain -p $directory @gradleArguments ${task}`,
|
|
951
|
+
"if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }",
|
|
952
|
+
"exit 0"
|
|
953
|
+
].join("; ");
|
|
954
|
+
return Buffer.from(source, "utf16le").toString("base64");
|
|
955
|
+
};
|
|
956
|
+
var gradleArtifactPath = (nativeDirectory, task, windows = false) => {
|
|
957
|
+
const pathJoin = windows ? win32.join : join3;
|
|
958
|
+
if (task === "assembleDebug") {
|
|
959
|
+
return pathJoin(nativeDirectory, "app", "build", "outputs", "apk", "debug", "app-debug.apk");
|
|
960
|
+
}
|
|
961
|
+
if (task === "assembleRelease") {
|
|
962
|
+
return pathJoin(nativeDirectory, "app", "build", "outputs", "apk", "release", "app-release.apk");
|
|
963
|
+
}
|
|
964
|
+
return pathJoin(nativeDirectory, "app", "build", "outputs", "bundle", "release", "app-release.aab");
|
|
965
|
+
};
|
|
966
|
+
var resolveGradleArtifactPath = async (nativeDirectory, task) => {
|
|
967
|
+
const primary = gradleArtifactPath(nativeDirectory, task);
|
|
968
|
+
if (task !== "assembleRelease" || await pathExists(primary)) {
|
|
969
|
+
return primary;
|
|
970
|
+
}
|
|
971
|
+
return join3(nativeDirectory, "app", "build", "outputs", "apk", "release", "app-release-unsigned.apk");
|
|
972
|
+
};
|
|
973
|
+
var buildAbsoluteAndroidGradleArtifact = async (options) => {
|
|
974
|
+
const { project, task } = options;
|
|
975
|
+
const capture = options.capture ?? captureCommand;
|
|
976
|
+
const run = options.run ?? runCommand;
|
|
977
|
+
const env = options.env ?? process.env;
|
|
978
|
+
const gradleArguments = options.gradleArguments ?? [];
|
|
979
|
+
if (project.host === "wsl") {
|
|
980
|
+
const windowsSource = windowsPathFromWsl(project.nativeDirectory, capture);
|
|
981
|
+
const buildId = Bun.hash(project.projectRoot).toString(HASH_RADIX);
|
|
982
|
+
const managedBuildDirectory = resolve2(project.androidRoot, "..", "..", "Builds", `${project.config.appId}-${buildId}`);
|
|
983
|
+
const windowsDirectory = windowsPathFromWsl(managedBuildDirectory, capture);
|
|
984
|
+
const windowsAndroidRoot = windowsPathFromWsl(project.androidRoot, capture);
|
|
985
|
+
const { dependencies, rewrittenSettings } = await mirroredCapacitorDependencies(project, capture);
|
|
986
|
+
await requireSuccess([
|
|
987
|
+
"powershell.exe",
|
|
988
|
+
"-NoProfile",
|
|
989
|
+
"-EncodedCommand",
|
|
990
|
+
encodedWindowsGradleCommand(windowsSource, windowsDirectory, windowsAndroidRoot, dependencies, rewrittenSettings, task, gradleArguments)
|
|
991
|
+
], "Android Gradle build", run, { env, signal: options.signal });
|
|
992
|
+
const artifactPath2 = await resolveGradleArtifactPath(managedBuildDirectory, task);
|
|
993
|
+
const unsigned = artifactPath2.endsWith("-unsigned.apk");
|
|
594
994
|
return {
|
|
595
|
-
|
|
596
|
-
|
|
995
|
+
artifactPath: artifactPath2,
|
|
996
|
+
installPath: unsigned ? win32.join(windowsDirectory, "app", "build", "outputs", "apk", "release", "app-release-unsigned.apk") : gradleArtifactPath(windowsDirectory, task, true)
|
|
597
997
|
};
|
|
598
998
|
}
|
|
599
|
-
|
|
999
|
+
const wrapper = project.host === "windows" ? "gradlew.bat" : "./gradlew";
|
|
1000
|
+
await requireSuccess([wrapper, "--no-daemon", "--console=plain", ...gradleArguments, task], "Android Gradle build", run, { cwd: project.nativeDirectory, env, signal: options.signal });
|
|
1001
|
+
const artifactPath = await resolveGradleArtifactPath(project.nativeDirectory, task);
|
|
1002
|
+
return { artifactPath, installPath: artifactPath };
|
|
1003
|
+
};
|
|
1004
|
+
|
|
1005
|
+
// src/mobile/androidRelease.ts
|
|
1006
|
+
var ABSOLUTE_ANDROID_RELEASE_FORMAT = 1;
|
|
1007
|
+
var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1008
|
+
var requireManifest = (value) => {
|
|
1009
|
+
if (!isRecord(value) || typeof value.appBuild !== "string" || typeof value.appId !== "string" || typeof value.runtime !== "string") {
|
|
1010
|
+
throw new TypeError("Invalid embedded AbsoluteJS mobile manifest.");
|
|
1011
|
+
}
|
|
1012
|
+
return {
|
|
1013
|
+
appBuild: value.appBuild,
|
|
1014
|
+
appId: value.appId,
|
|
1015
|
+
runtime: value.runtime
|
|
1016
|
+
};
|
|
1017
|
+
};
|
|
1018
|
+
var pathExists2 = async (path) => {
|
|
1019
|
+
try {
|
|
1020
|
+
await access3(path);
|
|
1021
|
+
return true;
|
|
1022
|
+
} catch {
|
|
1023
|
+
return false;
|
|
1024
|
+
}
|
|
1025
|
+
};
|
|
1026
|
+
var defaultCapture = (command, options = {}) => {
|
|
1027
|
+
try {
|
|
1028
|
+
const result = Bun.spawnSync(command, {
|
|
1029
|
+
cwd: options.cwd,
|
|
1030
|
+
env: options.env,
|
|
1031
|
+
stderr: "pipe",
|
|
1032
|
+
stdin: "ignore",
|
|
1033
|
+
stdout: "pipe"
|
|
1034
|
+
});
|
|
600
1035
|
return {
|
|
601
|
-
|
|
602
|
-
|
|
1036
|
+
exitCode: result.exitCode,
|
|
1037
|
+
stderr: result.stderr.toString(),
|
|
1038
|
+
stdout: result.stdout.toString()
|
|
603
1039
|
};
|
|
604
|
-
}
|
|
605
|
-
if (!pageId) {
|
|
1040
|
+
} catch (error) {
|
|
606
1041
|
return {
|
|
607
|
-
|
|
608
|
-
|
|
1042
|
+
exitCode: 1,
|
|
1043
|
+
stderr: error instanceof Error ? error.message : String(error),
|
|
1044
|
+
stdout: ""
|
|
609
1045
|
};
|
|
610
1046
|
}
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
if (
|
|
615
|
-
return
|
|
616
|
-
|
|
617
|
-
|
|
1047
|
+
};
|
|
1048
|
+
var verifyAabSignature = (artifactPath, capture, jarsigner) => {
|
|
1049
|
+
const executable = jarsigner === undefined ? Bun.which("jarsigner") : jarsigner;
|
|
1050
|
+
if (!executable)
|
|
1051
|
+
return null;
|
|
1052
|
+
const result = capture([
|
|
1053
|
+
executable,
|
|
1054
|
+
"-J-Duser.language=en",
|
|
1055
|
+
"-J-Duser.country=US",
|
|
1056
|
+
"-verify",
|
|
1057
|
+
artifactPath
|
|
1058
|
+
]);
|
|
1059
|
+
return result.exitCode === 0 && /jar verified/iu.test(result.stdout);
|
|
1060
|
+
};
|
|
1061
|
+
var sha256File = async (path) => createHash4("sha256").update(await readFile4(path)).digest("hex");
|
|
1062
|
+
var safeOutputDirectory = (projectRoot, requested) => {
|
|
1063
|
+
const root = resolve3(projectRoot);
|
|
1064
|
+
const output = resolve3(root, requested ?? ".absolutejs/mobile/releases/android");
|
|
1065
|
+
const projectRelative = relative3(root, output);
|
|
1066
|
+
if (projectRelative === ".." || projectRelative.startsWith(`..${sep2}`) || isAbsolute2(projectRelative)) {
|
|
1067
|
+
throw new TypeError("mobile build --outdir must remain inside the project.");
|
|
1068
|
+
}
|
|
1069
|
+
return output;
|
|
1070
|
+
};
|
|
1071
|
+
var installRelease = async (artifactPath, metadata, outputRoot) => {
|
|
1072
|
+
const releaseRoot = join4(outputRoot, metadata.releaseId);
|
|
1073
|
+
const artifactName = "app-release.aab";
|
|
1074
|
+
const destination = join4(releaseRoot, artifactName);
|
|
1075
|
+
if (await pathExists2(releaseRoot)) {
|
|
1076
|
+
const existing = requireManifestIdentity(JSON.parse(await readFile4(join4(releaseRoot, "release.json"), "utf8")), metadata);
|
|
1077
|
+
const [installedBytes, installedSha256] = await Promise.all([
|
|
1078
|
+
stat(destination).then(({ size }) => size),
|
|
1079
|
+
sha256File(destination)
|
|
1080
|
+
]);
|
|
1081
|
+
if (installedBytes !== metadata.bytes || installedSha256 !== metadata.sha256) {
|
|
1082
|
+
throw new TypeError(`Immutable Android release ${metadata.releaseId} artifact is missing or modified.`);
|
|
1083
|
+
}
|
|
1084
|
+
return { artifactPath: destination, metadata: existing, releaseRoot };
|
|
1085
|
+
}
|
|
1086
|
+
await mkdir3(dirname2(releaseRoot), { recursive: true });
|
|
1087
|
+
const staging = await mkdtemp2(join4(dirname2(releaseRoot), ".android-stage-"));
|
|
1088
|
+
try {
|
|
1089
|
+
await copyFile2(artifactPath, join4(staging, artifactName));
|
|
1090
|
+
const complete = {
|
|
1091
|
+
...metadata,
|
|
1092
|
+
artifact: artifactName
|
|
618
1093
|
};
|
|
1094
|
+
await writeFile4(join4(staging, "release.json"), `${JSON.stringify(complete, null, "\t")}
|
|
1095
|
+
`, { flag: "wx" });
|
|
1096
|
+
await rename4(staging, releaseRoot);
|
|
1097
|
+
return { artifactPath: destination, metadata: complete, releaseRoot };
|
|
1098
|
+
} finally {
|
|
1099
|
+
await rm3(staging, { force: true, recursive: true }).catch(() => {
|
|
1100
|
+
return;
|
|
1101
|
+
});
|
|
619
1102
|
}
|
|
620
|
-
return {
|
|
621
|
-
client: {
|
|
622
|
-
appBuild,
|
|
623
|
-
pageBundle,
|
|
624
|
-
pageContracts,
|
|
625
|
-
pageId,
|
|
626
|
-
protocol: Number(protocolValue),
|
|
627
|
-
runtime
|
|
628
|
-
},
|
|
629
|
-
kind: "mobile"
|
|
630
|
-
};
|
|
631
1103
|
};
|
|
632
|
-
var
|
|
633
|
-
|
|
634
|
-
"
|
|
635
|
-
|
|
636
|
-
}
|
|
637
|
-
|
|
638
|
-
|
|
1104
|
+
var requireManifestIdentity = (value, expected) => {
|
|
1105
|
+
if (!isRecord(value)) {
|
|
1106
|
+
throw new TypeError("Existing Android release metadata is invalid.");
|
|
1107
|
+
}
|
|
1108
|
+
const { artifact } = value;
|
|
1109
|
+
if (artifact !== "app-release.aab" || Object.entries(expected).some(([key, expectedValue]) => Reflect.get(value, key) !== expectedValue)) {
|
|
1110
|
+
throw new TypeError(`Immutable Android release ${expected.releaseId} does not match its content.`);
|
|
1111
|
+
}
|
|
1112
|
+
return { ...expected, artifact };
|
|
639
1113
|
};
|
|
640
|
-
var
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
if (
|
|
657
|
-
|
|
1114
|
+
var buildAbsoluteAndroidRelease = async (options) => {
|
|
1115
|
+
if (options.versionCode !== undefined && options.prepareVersionCode) {
|
|
1116
|
+
throw new TypeError("Android release versionCode and prepareVersionCode cannot be combined.");
|
|
1117
|
+
}
|
|
1118
|
+
if (options.versionCode !== undefined && (!Number.isSafeInteger(options.versionCode) || options.versionCode < 1 || options.versionCode > 2100000000)) {
|
|
1119
|
+
throw new TypeError("Android versionCode must be an integer from 1 through 2100000000.");
|
|
1120
|
+
}
|
|
1121
|
+
const projectRoot = resolve3(options.projectRoot);
|
|
1122
|
+
const host = options.host ?? detectAbsoluteMobileHost();
|
|
1123
|
+
const androidRoot = options.androidRoot ?? process.env.ANDROID_HOME ?? process.env.ANDROID_SDK_ROOT ?? absoluteManagedAndroidSdkRoot(host);
|
|
1124
|
+
const nativeDirectory = join4(options.config.nativeProjectDirectory, "android");
|
|
1125
|
+
const manifest = requireManifest(JSON.parse(await readFile4(join4(options.config.bundleDirectory, "absolute-mobile-manifest.json"), "utf8")));
|
|
1126
|
+
if (manifest.appId !== options.config.appId) {
|
|
1127
|
+
throw new TypeError("Embedded mobile manifest appId does not match mobile.appId.");
|
|
1128
|
+
}
|
|
1129
|
+
let { versionCode } = options;
|
|
1130
|
+
if (options.prepareVersionCode) {
|
|
1131
|
+
const nativeFingerprint = await fingerprintAbsoluteAndroidNativeProject({ nativeDirectory });
|
|
1132
|
+
const buildIdentity = createHash4("sha256").update(`${manifest.appBuild}\x00${nativeFingerprint}`).digest("hex");
|
|
1133
|
+
versionCode = await options.prepareVersionCode(buildIdentity);
|
|
1134
|
+
}
|
|
1135
|
+
if (versionCode !== undefined && (!Number.isSafeInteger(versionCode) || versionCode < 1 || versionCode > 2100000000)) {
|
|
1136
|
+
throw new TypeError("Android versionCode must be an integer from 1 through 2100000000.");
|
|
1137
|
+
}
|
|
1138
|
+
const { artifactPath } = await buildAbsoluteAndroidGradleArtifact({
|
|
1139
|
+
capture: options.capture,
|
|
1140
|
+
gradleArguments: versionCode === undefined ? [] : [`-Pandroid.injected.version.code=${versionCode}`],
|
|
1141
|
+
project: {
|
|
1142
|
+
androidRoot,
|
|
1143
|
+
config: options.config,
|
|
1144
|
+
host,
|
|
1145
|
+
nativeDirectory,
|
|
1146
|
+
projectRoot
|
|
1147
|
+
},
|
|
1148
|
+
run: options.run,
|
|
1149
|
+
task: "bundleRelease"
|
|
1150
|
+
});
|
|
1151
|
+
if (!await pathExists2(artifactPath)) {
|
|
1152
|
+
throw new TypeError(`Android Gradle did not produce the expected App Bundle: ${artifactPath}`);
|
|
658
1153
|
}
|
|
659
|
-
const
|
|
660
|
-
|
|
661
|
-
|
|
1154
|
+
const capture = options.capture ?? defaultCapture;
|
|
1155
|
+
const signed = verifyAabSignature(artifactPath, capture, options.jarsigner);
|
|
1156
|
+
if (signed === null && !options.allowUnsigned) {
|
|
1157
|
+
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.");
|
|
662
1158
|
}
|
|
663
|
-
|
|
1159
|
+
if (!signed && !options.allowUnsigned) {
|
|
1160
|
+
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.");
|
|
1161
|
+
}
|
|
1162
|
+
const [bytes, sha256] = await Promise.all([
|
|
1163
|
+
stat(artifactPath).then(({ size }) => size),
|
|
1164
|
+
sha256File(artifactPath)
|
|
1165
|
+
]);
|
|
1166
|
+
const releaseId = `amobile_android_${sha256}`;
|
|
1167
|
+
const metadata = {
|
|
1168
|
+
appBuild: manifest.appBuild,
|
|
1169
|
+
appId: manifest.appId,
|
|
1170
|
+
bytes,
|
|
1171
|
+
engine: "capacitor",
|
|
1172
|
+
format: ABSOLUTE_ANDROID_RELEASE_FORMAT,
|
|
1173
|
+
platform: "android",
|
|
1174
|
+
releaseId,
|
|
1175
|
+
runtime: manifest.runtime,
|
|
1176
|
+
sha256,
|
|
1177
|
+
signed: signed === true,
|
|
1178
|
+
type: "aab",
|
|
1179
|
+
...versionCode === undefined ? {} : { versionCode }
|
|
1180
|
+
};
|
|
1181
|
+
return installRelease(artifactPath, metadata, safeOutputDirectory(projectRoot, options.outputDirectory));
|
|
664
1182
|
};
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
1183
|
+
// src/mobile/iosRelease.ts
|
|
1184
|
+
import { createHash as createHash5 } from "crypto";
|
|
1185
|
+
import {
|
|
1186
|
+
access as access4,
|
|
1187
|
+
copyFile as copyFile3,
|
|
1188
|
+
mkdir as mkdir4,
|
|
1189
|
+
mkdtemp as mkdtemp3,
|
|
1190
|
+
readdir as readdir3,
|
|
1191
|
+
readFile as readFile5,
|
|
1192
|
+
rename as rename5,
|
|
1193
|
+
rm as rm4,
|
|
1194
|
+
stat as stat2,
|
|
1195
|
+
writeFile as writeFile5
|
|
1196
|
+
} from "fs/promises";
|
|
1197
|
+
import { dirname as dirname3, isAbsolute as isAbsolute3, join as join5, relative as relative4, resolve as resolve4, sep as sep3 } from "path";
|
|
1198
|
+
var ABSOLUTE_IOS_RELEASE_FORMAT = 1;
|
|
1199
|
+
var isRecord2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1200
|
+
var requireManifest2 = (value) => {
|
|
1201
|
+
if (!isRecord2(value) || typeof value.appBuild !== "string" || typeof value.appId !== "string" || typeof value.runtime !== "string") {
|
|
1202
|
+
throw new TypeError("Invalid embedded AbsoluteJS mobile manifest.");
|
|
672
1203
|
}
|
|
1204
|
+
return {
|
|
1205
|
+
appBuild: value.appBuild,
|
|
1206
|
+
appId: value.appId,
|
|
1207
|
+
runtime: value.runtime
|
|
1208
|
+
};
|
|
1209
|
+
};
|
|
1210
|
+
var pathExists3 = async (path) => {
|
|
673
1211
|
try {
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
return envelopeResponse({
|
|
679
|
-
contract: page.contract,
|
|
680
|
-
framework: page.framework,
|
|
681
|
-
kind: "page",
|
|
682
|
-
pageId: page.pageId,
|
|
683
|
-
props: normalizeJsonValue(currentRepresentation.mapProps(input.props)),
|
|
684
|
-
status: input.status ?? OK_STATUS
|
|
685
|
-
}, input.status ?? OK_STATUS);
|
|
686
|
-
} catch (error) {
|
|
687
|
-
console.error(`[Mobile] Archived producer ${context.releaseId} failed for ${page.pageId}:`, error);
|
|
688
|
-
return createAbsoluteMobilePageErrorResponse(page.pageId);
|
|
1212
|
+
await access4(path);
|
|
1213
|
+
return true;
|
|
1214
|
+
} catch {
|
|
1215
|
+
return false;
|
|
689
1216
|
}
|
|
690
1217
|
};
|
|
691
|
-
var
|
|
692
|
-
|
|
1218
|
+
var defaultRun = async (command, options = {}) => {
|
|
1219
|
+
const process2 = Bun.spawn(command, {
|
|
1220
|
+
cwd: options.cwd,
|
|
1221
|
+
env: options.env,
|
|
1222
|
+
stderr: "inherit",
|
|
1223
|
+
stdin: "inherit",
|
|
1224
|
+
stdout: "inherit"
|
|
1225
|
+
});
|
|
1226
|
+
return process2.exited;
|
|
1227
|
+
};
|
|
1228
|
+
var defaultCapture2 = (command, options = {}) => {
|
|
1229
|
+
try {
|
|
1230
|
+
const result = Bun.spawnSync(command, {
|
|
1231
|
+
cwd: options.cwd,
|
|
1232
|
+
env: options.env,
|
|
1233
|
+
stderr: "pipe",
|
|
1234
|
+
stdin: "ignore",
|
|
1235
|
+
stdout: "pipe"
|
|
1236
|
+
});
|
|
1237
|
+
return {
|
|
1238
|
+
exitCode: result.exitCode,
|
|
1239
|
+
stderr: result.stderr.toString(),
|
|
1240
|
+
stdout: result.stdout.toString()
|
|
1241
|
+
};
|
|
1242
|
+
} catch (error) {
|
|
1243
|
+
return {
|
|
1244
|
+
exitCode: 1,
|
|
1245
|
+
stderr: error instanceof Error ? error.message : String(error),
|
|
1246
|
+
stdout: ""
|
|
1247
|
+
};
|
|
1248
|
+
}
|
|
1249
|
+
};
|
|
1250
|
+
var ignoredFingerprintDirectories = new Set([
|
|
1251
|
+
"Pods",
|
|
1252
|
+
"DerivedData",
|
|
1253
|
+
"build",
|
|
1254
|
+
"xcuserdata"
|
|
1255
|
+
]);
|
|
1256
|
+
var fingerprintFiles = async (root, current = root) => {
|
|
1257
|
+
const entries = await readdir3(current, { withFileTypes: true });
|
|
1258
|
+
const nested = await Promise.all(entries.sort((left, right) => left.name.localeCompare(right.name)).map(async (entry) => {
|
|
1259
|
+
const path = join5(current, entry.name);
|
|
1260
|
+
const projectRelative = relative4(root, path).replaceAll("\\", "/");
|
|
1261
|
+
const ignored = entry.isDirectory() && (ignoredFingerprintDirectories.has(entry.name) || projectRelative === "App/App/public");
|
|
1262
|
+
if (ignored)
|
|
1263
|
+
return [];
|
|
1264
|
+
if (entry.isDirectory())
|
|
1265
|
+
return fingerprintFiles(root, path);
|
|
1266
|
+
return entry.isFile() ? [path] : [];
|
|
1267
|
+
}));
|
|
1268
|
+
return nested.flat();
|
|
1269
|
+
};
|
|
1270
|
+
var fingerprintAbsoluteIosNativeProject = async (nativeDirectory) => {
|
|
1271
|
+
const hasher = createHash5("sha256");
|
|
1272
|
+
const files = await fingerprintFiles(nativeDirectory);
|
|
1273
|
+
const contents = await Promise.all(files.map((file) => readFile5(file)));
|
|
1274
|
+
files.forEach((file, index) => {
|
|
1275
|
+
hasher.update(relative4(nativeDirectory, file).replaceAll("\\", "/"));
|
|
1276
|
+
hasher.update("\x00");
|
|
1277
|
+
hasher.update(contents[index] ?? new Uint8Array);
|
|
1278
|
+
hasher.update("\x00");
|
|
1279
|
+
});
|
|
1280
|
+
return hasher.digest("hex");
|
|
1281
|
+
};
|
|
1282
|
+
var safeOutputDirectory2 = (projectRoot, requested) => {
|
|
1283
|
+
const root = resolve4(projectRoot);
|
|
1284
|
+
const output = resolve4(root, requested ?? ".absolutejs/mobile/releases/ios");
|
|
1285
|
+
const projectRelative = relative4(root, output);
|
|
1286
|
+
if (projectRelative === ".." || projectRelative.startsWith(`..${sep3}`) || isAbsolute3(projectRelative)) {
|
|
1287
|
+
throw new TypeError("mobile build --outdir must remain inside the project.");
|
|
1288
|
+
}
|
|
1289
|
+
return output;
|
|
1290
|
+
};
|
|
1291
|
+
var sha256File2 = async (path) => createHash5("sha256").update(await readFile5(path)).digest("hex");
|
|
1292
|
+
var findByExtension = async (root, extension) => {
|
|
1293
|
+
if (!await pathExists3(root))
|
|
1294
|
+
return;
|
|
1295
|
+
const entries = await readdir3(root, { withFileTypes: true });
|
|
1296
|
+
const matches = await Promise.all(entries.map(async (entry) => {
|
|
1297
|
+
const path = join5(root, entry.name);
|
|
1298
|
+
if (entry.isDirectory() && entry.name.endsWith(extension))
|
|
1299
|
+
return path;
|
|
1300
|
+
if (entry.isFile() && entry.name.endsWith(extension))
|
|
1301
|
+
return path;
|
|
1302
|
+
return entry.isDirectory() ? findByExtension(path, extension) : undefined;
|
|
1303
|
+
}));
|
|
1304
|
+
return matches.find((match) => match !== undefined);
|
|
1305
|
+
};
|
|
1306
|
+
var exportOptions = () => `<?xml version="1.0" encoding="UTF-8"?>
|
|
1307
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
1308
|
+
<plist version="1.0"><dict>
|
|
1309
|
+
<key>destination</key><string>export</string>
|
|
1310
|
+
<key>manageAppVersionAndBuildNumber</key><false/>
|
|
1311
|
+
<key>method</key><string>app-store-connect</string>
|
|
1312
|
+
<key>signingStyle</key><string>automatic</string>
|
|
1313
|
+
<key>stripSwiftSymbols</key><true/>
|
|
1314
|
+
<key>uploadSymbols</key><true/>
|
|
1315
|
+
</dict></plist>
|
|
1316
|
+
`;
|
|
1317
|
+
var requireBuildNumber = (value) => {
|
|
1318
|
+
if (value !== undefined && (!Number.isSafeInteger(value) || value < 1))
|
|
1319
|
+
throw new TypeError("iOS build number must be a positive integer.");
|
|
1320
|
+
return value;
|
|
1321
|
+
};
|
|
1322
|
+
var installRelease2 = async (artifactPath, metadata, outputRoot) => {
|
|
1323
|
+
const releaseRoot = join5(outputRoot, metadata.releaseId);
|
|
1324
|
+
const destination = join5(releaseRoot, "App.ipa");
|
|
1325
|
+
if (await pathExists3(releaseRoot)) {
|
|
1326
|
+
const value = JSON.parse(await readFile5(join5(releaseRoot, "release.json"), "utf8"));
|
|
1327
|
+
if (!isRecord2(value) || value.artifact !== "App.ipa" || Object.entries(metadata).some(([key, expected]) => Reflect.get(value, key) !== expected)) {
|
|
1328
|
+
throw new TypeError(`Immutable iOS release ${metadata.releaseId} does not match its content.`);
|
|
1329
|
+
}
|
|
1330
|
+
const [bytes, sha256] = await Promise.all([
|
|
1331
|
+
stat2(destination).then(({ size }) => size),
|
|
1332
|
+
sha256File2(destination)
|
|
1333
|
+
]);
|
|
1334
|
+
if (bytes !== metadata.bytes || sha256 !== metadata.sha256)
|
|
1335
|
+
throw new TypeError(`Immutable iOS release ${metadata.releaseId} artifact is missing or modified.`);
|
|
1336
|
+
return {
|
|
1337
|
+
artifactPath: destination,
|
|
1338
|
+
metadata: {
|
|
1339
|
+
...metadata,
|
|
1340
|
+
artifact: "App.ipa"
|
|
1341
|
+
},
|
|
1342
|
+
releaseRoot
|
|
1343
|
+
};
|
|
1344
|
+
}
|
|
1345
|
+
await mkdir4(dirname3(releaseRoot), { recursive: true });
|
|
1346
|
+
const staging = await mkdtemp3(join5(dirname3(releaseRoot), ".ios-stage-"));
|
|
1347
|
+
try {
|
|
1348
|
+
await copyFile3(artifactPath, join5(staging, "App.ipa"));
|
|
1349
|
+
const complete = {
|
|
1350
|
+
...metadata,
|
|
1351
|
+
artifact: "App.ipa"
|
|
1352
|
+
};
|
|
1353
|
+
await writeFile5(join5(staging, "release.json"), `${JSON.stringify(complete, null, "\t")}
|
|
1354
|
+
`, { flag: "wx" });
|
|
1355
|
+
await rename5(staging, releaseRoot);
|
|
1356
|
+
return { artifactPath: destination, metadata: complete, releaseRoot };
|
|
1357
|
+
} finally {
|
|
1358
|
+
await rm4(staging, { force: true, recursive: true }).catch(() => {
|
|
1359
|
+
return;
|
|
1360
|
+
});
|
|
1361
|
+
}
|
|
1362
|
+
};
|
|
1363
|
+
var buildAbsoluteIosRelease = async (options) => {
|
|
1364
|
+
if (options.buildNumber !== undefined && options.prepareBuildNumber)
|
|
1365
|
+
throw new TypeError("iOS release buildNumber and prepareBuildNumber cannot be combined.");
|
|
1366
|
+
if ((options.host ?? detectAbsoluteMobileHost()) !== "macos")
|
|
1367
|
+
throw new TypeError("iOS release builds require macOS and Xcode.");
|
|
1368
|
+
const marketingVersion = options.config.iosVersion;
|
|
1369
|
+
if (!marketingVersion)
|
|
1370
|
+
throw new TypeError("iOS release builds require mobile.ios.version in absolutejs.config.ts.");
|
|
1371
|
+
const manifest = requireManifest2(JSON.parse(await readFile5(join5(options.config.bundleDirectory, "absolute-mobile-manifest.json"), "utf8")));
|
|
1372
|
+
if (manifest.appId !== options.config.appId)
|
|
1373
|
+
throw new TypeError("Embedded mobile manifest appId does not match mobile.appId.");
|
|
1374
|
+
const nativeDirectory = join5(options.config.nativeProjectDirectory, "ios");
|
|
1375
|
+
let buildNumber = requireBuildNumber(options.buildNumber);
|
|
1376
|
+
if (options.prepareBuildNumber) {
|
|
1377
|
+
const nativeFingerprint = await fingerprintAbsoluteIosNativeProject(nativeDirectory);
|
|
1378
|
+
const buildIdentity = createHash5("sha256").update(`${manifest.appBuild}\x00${nativeFingerprint}\x00${marketingVersion}`).digest("hex");
|
|
1379
|
+
buildNumber = requireBuildNumber(await options.prepareBuildNumber(buildIdentity));
|
|
1380
|
+
}
|
|
1381
|
+
const stagingParent = resolve4(options.projectRoot, ".absolutejs/mobile");
|
|
1382
|
+
await mkdir4(stagingParent, { recursive: true });
|
|
1383
|
+
const staging = await mkdtemp3(join5(stagingParent, ".ios-build-"));
|
|
1384
|
+
const archivePath = join5(staging, "App.xcarchive");
|
|
1385
|
+
const exportPath = join5(staging, "export");
|
|
1386
|
+
const exportPlist = join5(staging, "ExportOptions.plist");
|
|
1387
|
+
await mkdir4(exportPath, { recursive: true });
|
|
1388
|
+
await writeFile5(exportPlist, exportOptions());
|
|
1389
|
+
const run = options.run ?? defaultRun;
|
|
1390
|
+
try {
|
|
1391
|
+
const versionArguments = [
|
|
1392
|
+
`MARKETING_VERSION=${marketingVersion}`,
|
|
1393
|
+
...buildNumber === undefined ? [] : [`CURRENT_PROJECT_VERSION=${buildNumber}`]
|
|
1394
|
+
];
|
|
1395
|
+
const archiveExit = await run([
|
|
1396
|
+
"xcodebuild",
|
|
1397
|
+
"-workspace",
|
|
1398
|
+
join5(nativeDirectory, "App", "App.xcworkspace"),
|
|
1399
|
+
"-scheme",
|
|
1400
|
+
"App",
|
|
1401
|
+
"-configuration",
|
|
1402
|
+
"Release",
|
|
1403
|
+
"-destination",
|
|
1404
|
+
"generic/platform=iOS",
|
|
1405
|
+
"-archivePath",
|
|
1406
|
+
archivePath,
|
|
1407
|
+
...versionArguments,
|
|
1408
|
+
"archive"
|
|
1409
|
+
], { cwd: nativeDirectory });
|
|
1410
|
+
if (archiveExit !== 0)
|
|
1411
|
+
throw new TypeError("Xcode failed to archive the iOS app.");
|
|
1412
|
+
const archivedApp = await findByExtension(join5(archivePath, "Products", "Applications"), ".app");
|
|
1413
|
+
const capture = options.capture ?? defaultCapture2;
|
|
1414
|
+
const signed = archivedApp ? capture([
|
|
1415
|
+
"codesign",
|
|
1416
|
+
"--verify",
|
|
1417
|
+
"--deep",
|
|
1418
|
+
"--strict",
|
|
1419
|
+
archivedApp
|
|
1420
|
+
]).exitCode === 0 : false;
|
|
1421
|
+
if (!signed && !options.allowUnsigned)
|
|
1422
|
+
throw new TypeError("Xcode produced an unsigned iOS archive. Configure signing in the source-owned Xcode project, or pass --unsigned only for a non-publishable build.");
|
|
1423
|
+
const exportExit = await run([
|
|
1424
|
+
"xcodebuild",
|
|
1425
|
+
"-exportArchive",
|
|
1426
|
+
"-archivePath",
|
|
1427
|
+
archivePath,
|
|
1428
|
+
"-exportPath",
|
|
1429
|
+
exportPath,
|
|
1430
|
+
"-exportOptionsPlist",
|
|
1431
|
+
exportPlist
|
|
1432
|
+
], { cwd: nativeDirectory });
|
|
1433
|
+
if (exportExit !== 0)
|
|
1434
|
+
throw new TypeError("Xcode failed to export the App Store IPA.");
|
|
1435
|
+
const artifactPath = await findByExtension(exportPath, ".ipa");
|
|
1436
|
+
if (!artifactPath)
|
|
1437
|
+
throw new TypeError("Xcode did not produce an exported IPA.");
|
|
1438
|
+
const [bytes, sha256] = await Promise.all([
|
|
1439
|
+
stat2(artifactPath).then(({ size }) => size),
|
|
1440
|
+
sha256File2(artifactPath)
|
|
1441
|
+
]);
|
|
1442
|
+
const releaseId = `amobile_ios_${sha256}`;
|
|
1443
|
+
return await installRelease2(artifactPath, {
|
|
1444
|
+
appBuild: manifest.appBuild,
|
|
1445
|
+
appId: manifest.appId,
|
|
1446
|
+
...buildNumber === undefined ? {} : { buildNumber },
|
|
1447
|
+
bytes,
|
|
1448
|
+
engine: "capacitor",
|
|
1449
|
+
format: 1,
|
|
1450
|
+
marketingVersion,
|
|
1451
|
+
platform: "ios",
|
|
1452
|
+
releaseId,
|
|
1453
|
+
runtime: manifest.runtime,
|
|
1454
|
+
sha256,
|
|
1455
|
+
signed,
|
|
1456
|
+
type: "ipa"
|
|
1457
|
+
}, safeOutputDirectory2(options.projectRoot, options.outputDirectory));
|
|
1458
|
+
} finally {
|
|
1459
|
+
await rm4(staging, { force: true, recursive: true }).catch(() => {
|
|
1460
|
+
return;
|
|
1461
|
+
});
|
|
1462
|
+
}
|
|
1463
|
+
};
|
|
1464
|
+
// src/mobile/associationFiles.ts
|
|
1465
|
+
import {
|
|
1466
|
+
access as access5,
|
|
1467
|
+
mkdir as mkdir5,
|
|
1468
|
+
readFile as readFile6,
|
|
1469
|
+
rename as rename6,
|
|
1470
|
+
rm as rm5,
|
|
1471
|
+
writeFile as writeFile6
|
|
1472
|
+
} from "fs/promises";
|
|
1473
|
+
import { resolve as resolve6 } from "path";
|
|
1474
|
+
import { Elysia } from "elysia";
|
|
1475
|
+
|
|
1476
|
+
// src/mobile/config.ts
|
|
1477
|
+
import { resolve as resolve5 } from "path";
|
|
1478
|
+
var APP_ID_PATTERN = /^[A-Za-z][\w]*(?:\.[A-Za-z][\w]*)+$/;
|
|
1479
|
+
var SCHEME_PATTERN = /^[a-z][a-z0-9+.-]*$/;
|
|
1480
|
+
var APPLE_APP_ID_PREFIX_PATTERN = /^[A-Z0-9]{10}$/;
|
|
1481
|
+
var CERTIFICATE_FINGERPRINT_PATTERN = /^[0-9A-F]{64}$/;
|
|
1482
|
+
var HOSTNAME_PATTERN = /^(?=.{1,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)(?:\.(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?))*$/;
|
|
1483
|
+
var resolveProjectPath = (projectRoot, value, field) => {
|
|
1484
|
+
const root = resolve5(projectRoot);
|
|
1485
|
+
const path = resolve5(root, value);
|
|
1486
|
+
if (path !== root && !path.startsWith(`${root}/`)) {
|
|
1487
|
+
throw new TypeError(`${field} must remain inside the project root.`);
|
|
1488
|
+
}
|
|
1489
|
+
return path;
|
|
1490
|
+
};
|
|
1491
|
+
var requireText = (value, field) => {
|
|
1492
|
+
const normalized = value.trim();
|
|
1493
|
+
if (!normalized)
|
|
1494
|
+
throw new TypeError(`${field} must not be empty.`);
|
|
1495
|
+
return normalized;
|
|
1496
|
+
};
|
|
1497
|
+
var normalizeEntry = (entry) => {
|
|
1498
|
+
const normalized = requireText(entry ?? "/", "mobile.entry");
|
|
1499
|
+
if (!normalized.startsWith("/") || normalized.startsWith("//")) {
|
|
1500
|
+
throw new TypeError("mobile.entry must be an absolute application path.");
|
|
1501
|
+
}
|
|
1502
|
+
return normalized;
|
|
1503
|
+
};
|
|
1504
|
+
var normalizeProductionOrigin = (value) => {
|
|
1505
|
+
const parsed = new URL(requireText(value, "mobile.server.productionOrigin"));
|
|
1506
|
+
if (parsed.protocol !== "https:") {
|
|
1507
|
+
throw new TypeError("mobile.server.productionOrigin must use HTTPS in production.");
|
|
1508
|
+
}
|
|
1509
|
+
if (parsed.username || parsed.password || parsed.pathname !== "/" || parsed.search || parsed.hash) {
|
|
1510
|
+
throw new TypeError("mobile.server.productionOrigin must be an origin without credentials, path, query, or hash.");
|
|
1511
|
+
}
|
|
1512
|
+
return parsed.origin;
|
|
1513
|
+
};
|
|
1514
|
+
var normalizePlatforms = (platforms) => {
|
|
1515
|
+
const normalized = [
|
|
1516
|
+
...new Set(platforms ?? ["ios", "android"])
|
|
1517
|
+
];
|
|
1518
|
+
if (normalized.length === 0) {
|
|
1519
|
+
throw new TypeError("mobile.platforms must contain at least one platform.");
|
|
1520
|
+
}
|
|
1521
|
+
return normalized;
|
|
1522
|
+
};
|
|
1523
|
+
var normalizeHosts = (hosts, productionOrigin) => {
|
|
1524
|
+
const normalizeHostname = (host) => {
|
|
1525
|
+
const value = requireText(host, "mobile.deepLinks.hosts").toLowerCase();
|
|
1526
|
+
if (!HOSTNAME_PATTERN.test(value)) {
|
|
1527
|
+
throw new TypeError("mobile.deepLinks.hosts entries must be valid hostnames without ports, wildcards, or paths.");
|
|
1528
|
+
}
|
|
1529
|
+
return value;
|
|
1530
|
+
};
|
|
1531
|
+
const normalized = new Set([
|
|
1532
|
+
normalizeHostname(new URL(productionOrigin).hostname)
|
|
1533
|
+
]);
|
|
1534
|
+
for (const host of hosts ?? []) {
|
|
1535
|
+
normalized.add(normalizeHostname(host));
|
|
1536
|
+
}
|
|
1537
|
+
return [...normalized].sort();
|
|
1538
|
+
};
|
|
1539
|
+
var normalizeAppleAppIdPrefix = (value) => {
|
|
1540
|
+
if (value === undefined)
|
|
1541
|
+
return;
|
|
1542
|
+
const normalized = requireText(value, "mobile.deepLinks.apple.appIdPrefix").toUpperCase();
|
|
1543
|
+
if (!APPLE_APP_ID_PREFIX_PATTERN.test(normalized)) {
|
|
1544
|
+
throw new TypeError("mobile.deepLinks.apple.appIdPrefix must contain ten letters or digits.");
|
|
1545
|
+
}
|
|
1546
|
+
return normalized;
|
|
1547
|
+
};
|
|
1548
|
+
var normalizeIosVersion = (value) => {
|
|
1549
|
+
if (value === undefined)
|
|
1550
|
+
return;
|
|
1551
|
+
const normalized = requireText(value, "mobile.ios.version");
|
|
1552
|
+
if (!/^\d+(?:\.\d+){0,2}$/u.test(normalized)) {
|
|
1553
|
+
throw new TypeError("mobile.ios.version must contain one to three dot-separated integer components, for example 1.4.0.");
|
|
1554
|
+
}
|
|
1555
|
+
return normalized;
|
|
1556
|
+
};
|
|
1557
|
+
var normalizeCertificateFingerprints = (values) => [
|
|
1558
|
+
...new Set((values ?? []).map((value) => requireText(value, "mobile.deepLinks.android.sha256CertificateFingerprints").replaceAll(":", "").toUpperCase()).map((value) => {
|
|
1559
|
+
if (!CERTIFICATE_FINGERPRINT_PATTERN.test(value)) {
|
|
1560
|
+
throw new TypeError("mobile.deepLinks.android.sha256CertificateFingerprints entries must be SHA-256 certificate fingerprints.");
|
|
1561
|
+
}
|
|
1562
|
+
return value.match(/.{2}/g)?.join(":") ?? value;
|
|
1563
|
+
}))
|
|
1564
|
+
].sort();
|
|
1565
|
+
var normalizeAbsoluteMobileConfig = (config, projectRoot) => {
|
|
1566
|
+
const appId = requireText(config.appId, "mobile.appId");
|
|
1567
|
+
if (!APP_ID_PATTERN.test(appId)) {
|
|
1568
|
+
throw new TypeError("mobile.appId must use reverse-domain notation, for example com.example.app.");
|
|
1569
|
+
}
|
|
1570
|
+
const productionOrigin = normalizeProductionOrigin(config.server.productionOrigin);
|
|
1571
|
+
const deepLinkScheme = config.deepLinks?.scheme?.trim().toLowerCase();
|
|
1572
|
+
if (deepLinkScheme && !SCHEME_PATTERN.test(deepLinkScheme)) {
|
|
1573
|
+
throw new TypeError("mobile.deepLinks.scheme is not a valid URL scheme.");
|
|
1574
|
+
}
|
|
1575
|
+
return {
|
|
1576
|
+
androidCertificateFingerprints: normalizeCertificateFingerprints(config.deepLinks?.android?.sha256CertificateFingerprints),
|
|
1577
|
+
appId,
|
|
1578
|
+
appleAppIdPrefix: normalizeAppleAppIdPrefix(config.deepLinks?.apple?.appIdPrefix),
|
|
1579
|
+
appName: requireText(config.appName, "mobile.appName"),
|
|
1580
|
+
bundleDirectory: resolveProjectPath(projectRoot, config.bundleDirectory ?? ".absolutejs/mobile/web", "mobile.bundleDirectory"),
|
|
1581
|
+
deepLinkHosts: normalizeHosts(config.deepLinks?.hosts, productionOrigin),
|
|
1582
|
+
deepLinkScheme,
|
|
1583
|
+
engine: "capacitor",
|
|
1584
|
+
entry: normalizeEntry(config.entry),
|
|
1585
|
+
iosVersion: normalizeIosVersion(config.ios?.version),
|
|
1586
|
+
nativeProjectDirectory: resolveProjectPath(projectRoot, config.nativeProject?.directory ?? "mobile", "mobile.nativeProject.directory"),
|
|
1587
|
+
platforms: normalizePlatforms(config.platforms),
|
|
1588
|
+
productionOrigin
|
|
1589
|
+
};
|
|
1590
|
+
};
|
|
1591
|
+
|
|
1592
|
+
// src/mobile/associationFiles.ts
|
|
1593
|
+
var JSON_HEADERS = {
|
|
1594
|
+
"cache-control": "public, max-age=300, must-revalidate",
|
|
1595
|
+
"content-type": "application/json; charset=utf-8"
|
|
1596
|
+
};
|
|
1597
|
+
var OWNERSHIP_FILE = ".absolutejs-mobile-associations.json";
|
|
1598
|
+
var HTTP_OK = 200;
|
|
1599
|
+
var VERIFY_TIMEOUT_MS = 1e4;
|
|
1600
|
+
var ANDROID_ASSOCIATION_PATH = "/.well-known/assetlinks.json";
|
|
1601
|
+
var APPLE_ASSOCIATION_PATH = "/.well-known/apple-app-site-association";
|
|
1602
|
+
var missingIdentity = (field, platform) => new TypeError(`${field} is required to publish ${platform} deep-link association files.`);
|
|
1603
|
+
var createAppleDocument = (config, requireAll) => {
|
|
1604
|
+
if (!config.platforms.includes("ios"))
|
|
1605
|
+
return;
|
|
1606
|
+
if (!config.appleAppIdPrefix && requireAll) {
|
|
1607
|
+
throw missingIdentity("mobile.deepLinks.apple.appIdPrefix", "iOS");
|
|
1608
|
+
}
|
|
1609
|
+
if (!config.appleAppIdPrefix)
|
|
1610
|
+
return;
|
|
1611
|
+
return {
|
|
1612
|
+
applinks: {
|
|
1613
|
+
details: [
|
|
1614
|
+
{
|
|
1615
|
+
appIDs: [
|
|
1616
|
+
`${config.appleAppIdPrefix}.${config.appId}`
|
|
1617
|
+
],
|
|
1618
|
+
components: [{ "/": "/*" }]
|
|
1619
|
+
}
|
|
1620
|
+
]
|
|
1621
|
+
}
|
|
1622
|
+
};
|
|
1623
|
+
};
|
|
1624
|
+
var createAndroidDocument = (config, requireAll) => {
|
|
1625
|
+
if (!config.platforms.includes("android"))
|
|
1626
|
+
return;
|
|
1627
|
+
if (config.androidCertificateFingerprints.length === 0 && requireAll) {
|
|
1628
|
+
throw missingIdentity("mobile.deepLinks.android.sha256CertificateFingerprints", "Android");
|
|
1629
|
+
}
|
|
1630
|
+
if (config.androidCertificateFingerprints.length === 0)
|
|
1631
|
+
return;
|
|
1632
|
+
return [
|
|
1633
|
+
{
|
|
1634
|
+
relation: ["delegate_permission/common.handle_all_urls"],
|
|
1635
|
+
target: {
|
|
1636
|
+
namespace: "android_app",
|
|
1637
|
+
package_name: config.appId,
|
|
1638
|
+
sha256_cert_fingerprints: config.androidCertificateFingerprints
|
|
1639
|
+
}
|
|
1640
|
+
}
|
|
1641
|
+
];
|
|
1642
|
+
};
|
|
1643
|
+
var createAbsoluteMobileAssociationDocuments = (config, options = {}) => {
|
|
1644
|
+
const documents = {};
|
|
1645
|
+
const requireAll = options.requireAll === true;
|
|
1646
|
+
const android = createAndroidDocument(config, requireAll);
|
|
1647
|
+
const apple = createAppleDocument(config, requireAll);
|
|
1648
|
+
if (android)
|
|
1649
|
+
documents.android = android;
|
|
1650
|
+
if (apple)
|
|
1651
|
+
documents.apple = apple;
|
|
1652
|
+
return documents;
|
|
1653
|
+
};
|
|
1654
|
+
var jsonResponse = (value) => new Response(`${JSON.stringify(value)}
|
|
1655
|
+
`, { headers: JSON_HEADERS });
|
|
1656
|
+
var createAbsoluteMobileAssociationPlugin = (mobile, projectRoot, options = {}) => {
|
|
1657
|
+
const plugin = new Elysia({ name: "absolutejs-mobile-associations" });
|
|
1658
|
+
if (!mobile)
|
|
1659
|
+
return plugin;
|
|
1660
|
+
const config = normalizeAbsoluteMobileConfig(mobile, projectRoot);
|
|
1661
|
+
const documents = createAbsoluteMobileAssociationDocuments(config, options);
|
|
1662
|
+
if (documents.apple) {
|
|
1663
|
+
plugin.get(APPLE_ASSOCIATION_PATH, () => jsonResponse(documents.apple));
|
|
1664
|
+
}
|
|
1665
|
+
if (documents.android) {
|
|
1666
|
+
plugin.get(ANDROID_ASSOCIATION_PATH, () => jsonResponse(documents.android));
|
|
1667
|
+
}
|
|
1668
|
+
return plugin;
|
|
1669
|
+
};
|
|
1670
|
+
var writeAtomic = async (path, source) => {
|
|
1671
|
+
let current;
|
|
1672
|
+
try {
|
|
1673
|
+
current = await readFile6(path, "utf8");
|
|
1674
|
+
} catch (error) {
|
|
1675
|
+
if (!(error instanceof Error) || !("code" in error) || error.code !== "ENOENT") {
|
|
1676
|
+
throw error;
|
|
1677
|
+
}
|
|
1678
|
+
}
|
|
1679
|
+
if (current === source)
|
|
1680
|
+
return false;
|
|
1681
|
+
const temporary = `${path}.${crypto.randomUUID()}.tmp`;
|
|
1682
|
+
await writeFile6(temporary, source, { flag: "wx" });
|
|
1683
|
+
await rename6(temporary, path);
|
|
1684
|
+
return true;
|
|
1685
|
+
};
|
|
1686
|
+
var exists2 = async (path) => {
|
|
1687
|
+
try {
|
|
1688
|
+
await access5(path);
|
|
1689
|
+
return true;
|
|
1690
|
+
} catch {
|
|
1691
|
+
return false;
|
|
1692
|
+
}
|
|
1693
|
+
};
|
|
1694
|
+
var assertOwnedOutput = async (root) => {
|
|
1695
|
+
const path = resolve6(root, OWNERSHIP_FILE);
|
|
1696
|
+
let ownership;
|
|
1697
|
+
try {
|
|
1698
|
+
ownership = JSON.parse(await readFile6(path, "utf8"));
|
|
1699
|
+
} catch {
|
|
1700
|
+
throw new TypeError(`Association output ${root} already exists and is not owned by AbsoluteJS.`);
|
|
1701
|
+
}
|
|
1702
|
+
if (typeof ownership !== "object" || ownership === null || Reflect.get(ownership, "format") !== 1) {
|
|
1703
|
+
throw new TypeError(`Association output ${root} has an unsupported ownership manifest.`);
|
|
1704
|
+
}
|
|
1705
|
+
};
|
|
1706
|
+
var publishGeneratedDirectory = async (temporary, root) => {
|
|
1707
|
+
const hasCurrent = await exists2(root);
|
|
1708
|
+
if (hasCurrent)
|
|
1709
|
+
await assertOwnedOutput(root);
|
|
1710
|
+
const backup = `${root}.${crypto.randomUUID()}.previous`;
|
|
1711
|
+
if (hasCurrent)
|
|
1712
|
+
await rename6(root, backup);
|
|
1713
|
+
try {
|
|
1714
|
+
await rename6(temporary, root);
|
|
1715
|
+
} catch (error) {
|
|
1716
|
+
if (hasCurrent)
|
|
1717
|
+
await rename6(backup, root);
|
|
1718
|
+
throw error;
|
|
1719
|
+
}
|
|
1720
|
+
if (hasCurrent)
|
|
1721
|
+
await rm5(backup, { force: true, recursive: true });
|
|
1722
|
+
};
|
|
1723
|
+
var materializeHost = async (root, host, files) => {
|
|
1724
|
+
const directory = resolve6(root, host, ".well-known");
|
|
1725
|
+
await mkdir5(directory, { recursive: true });
|
|
1726
|
+
return Promise.all(files.map(async ([name, document]) => {
|
|
1727
|
+
const path = resolve6(directory, name);
|
|
1728
|
+
await writeAtomic(path, `${JSON.stringify(document, null, 2)}
|
|
1729
|
+
`);
|
|
1730
|
+
return path;
|
|
1731
|
+
}));
|
|
1732
|
+
};
|
|
1733
|
+
var associationEndpoints = (config, documents) => config.deepLinkHosts.flatMap((host) => {
|
|
1734
|
+
const endpoints = [];
|
|
1735
|
+
if (documents.android)
|
|
1736
|
+
endpoints.push({
|
|
1737
|
+
document: documents.android,
|
|
1738
|
+
host,
|
|
1739
|
+
path: ANDROID_ASSOCIATION_PATH,
|
|
1740
|
+
platform: "Android"
|
|
1741
|
+
});
|
|
1742
|
+
if (documents.apple)
|
|
1743
|
+
endpoints.push({
|
|
1744
|
+
document: documents.apple,
|
|
1745
|
+
host,
|
|
1746
|
+
path: APPLE_ASSOCIATION_PATH,
|
|
1747
|
+
platform: "Apple"
|
|
1748
|
+
});
|
|
1749
|
+
return endpoints;
|
|
1750
|
+
});
|
|
1751
|
+
var materializeAbsoluteMobileAssociationFiles = async (config, outputDirectory) => {
|
|
1752
|
+
const root = resolve6(outputDirectory);
|
|
1753
|
+
const temporary = `${root}.${crypto.randomUUID()}.tmp`;
|
|
1754
|
+
const documents = createAbsoluteMobileAssociationDocuments(config, {
|
|
1755
|
+
requireAll: true
|
|
1756
|
+
});
|
|
1757
|
+
const files = [];
|
|
1758
|
+
if (documents.android)
|
|
1759
|
+
files.push(["assetlinks.json", documents.android]);
|
|
1760
|
+
if (documents.apple) {
|
|
1761
|
+
files.push(["apple-app-site-association", documents.apple]);
|
|
1762
|
+
}
|
|
1763
|
+
await mkdir5(temporary, { recursive: true });
|
|
1764
|
+
try {
|
|
1765
|
+
const temporaryPaths = (await Promise.all(config.deepLinkHosts.map((host) => materializeHost(temporary, host, files)))).flat();
|
|
1766
|
+
await writeAtomic(resolve6(temporary, OWNERSHIP_FILE), `${JSON.stringify({ format: 1, hosts: config.deepLinkHosts }, null, 2)}
|
|
1767
|
+
`);
|
|
1768
|
+
await publishGeneratedDirectory(temporary, root);
|
|
1769
|
+
const written = temporaryPaths.map((path) => resolve6(root, path.slice(temporary.length + 1)));
|
|
1770
|
+
return { root, written };
|
|
1771
|
+
} catch (error) {
|
|
1772
|
+
await rm5(temporary, { force: true, recursive: true });
|
|
1773
|
+
throw error;
|
|
1774
|
+
}
|
|
1775
|
+
};
|
|
1776
|
+
var verifyAbsoluteMobileAssociationFiles = async (config, request = globalThis.fetch) => {
|
|
1777
|
+
const documents = createAbsoluteMobileAssociationDocuments(config, {
|
|
1778
|
+
requireAll: true
|
|
1779
|
+
});
|
|
1780
|
+
const results = await Promise.all(associationEndpoints(config, documents).map(async (endpoint) => {
|
|
1781
|
+
const url = `https://${endpoint.host}${endpoint.path}`;
|
|
1782
|
+
const response = await request(url, {
|
|
1783
|
+
redirect: "manual",
|
|
1784
|
+
signal: AbortSignal.timeout(VERIFY_TIMEOUT_MS)
|
|
1785
|
+
});
|
|
1786
|
+
if (response.status !== HTTP_OK) {
|
|
1787
|
+
throw new TypeError(`${endpoint.platform} association verification failed for ${url}: HTTP ${response.status}; redirects are not allowed.`);
|
|
1788
|
+
}
|
|
1789
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
1790
|
+
if (!contentType.toLowerCase().startsWith("application/json")) {
|
|
1791
|
+
throw new TypeError(`${endpoint.platform} association verification failed for ${url}: expected application/json.`);
|
|
1792
|
+
}
|
|
1793
|
+
let document;
|
|
1794
|
+
try {
|
|
1795
|
+
document = await response.json();
|
|
1796
|
+
} catch {
|
|
1797
|
+
throw new TypeError(`${endpoint.platform} association verification failed for ${url}: invalid JSON.`);
|
|
1798
|
+
}
|
|
1799
|
+
if (JSON.stringify(document) !== JSON.stringify(endpoint.document)) {
|
|
1800
|
+
throw new TypeError(`${endpoint.platform} association verification failed for ${url}: the hosted identity does not match mobile config.`);
|
|
1801
|
+
}
|
|
1802
|
+
return { platform: endpoint.platform, url };
|
|
1803
|
+
}));
|
|
1804
|
+
return { results };
|
|
1805
|
+
};
|
|
1806
|
+
// src/mobile/buildMetadata.ts
|
|
1807
|
+
var ABSOLUTE_MOBILE_ROUTE_DETAIL = "x-absolute-mobile";
|
|
1808
|
+
var frameworks2 = new Set([
|
|
1809
|
+
"angular",
|
|
1810
|
+
"ember",
|
|
1811
|
+
"html",
|
|
1812
|
+
"htmx",
|
|
1813
|
+
"react",
|
|
1814
|
+
"svelte",
|
|
1815
|
+
"vue"
|
|
1816
|
+
]);
|
|
1817
|
+
var isRecord3 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1818
|
+
var isPageFramework2 = (value) => typeof value === "string" && frameworks2.has(value);
|
|
1819
|
+
var parseAbsoluteMobileBuildPageMetadata = (value) => {
|
|
1820
|
+
if (!isRecord3(value))
|
|
1821
|
+
return;
|
|
1822
|
+
if (typeof value.bundleKey !== "string" || typeof value.contract !== "string" || !isPageFramework2(value.framework) || typeof value.pageId !== "string" || typeof value.propsSchemaHash !== "string") {
|
|
1823
|
+
return;
|
|
1824
|
+
}
|
|
1825
|
+
return {
|
|
1826
|
+
bundleKey: value.bundleKey,
|
|
1827
|
+
contract: value.contract,
|
|
1828
|
+
framework: value.framework,
|
|
1829
|
+
pageId: value.pageId,
|
|
1830
|
+
propsSchemaHash: value.propsSchemaHash
|
|
1831
|
+
};
|
|
1832
|
+
};
|
|
1833
|
+
// src/mobile/buildPipeline.ts
|
|
1834
|
+
import { readFile as readFile10 } from "fs/promises";
|
|
1835
|
+
import { join as join9, resolve as resolve9 } from "path";
|
|
1836
|
+
import { pathToFileURL as pathToFileURL2 } from "url";
|
|
1837
|
+
|
|
1838
|
+
// src/mobile/buildRelease.ts
|
|
1839
|
+
import { createHash as createHash6 } from "crypto";
|
|
1840
|
+
import { readFile as readFile7 } from "fs/promises";
|
|
1841
|
+
import { join as join6, relative as relative5, resolve as resolve7 } from "path";
|
|
1842
|
+
var sha256 = (bytes) => createHash6("sha256").update(bytes).digest("hex");
|
|
1843
|
+
var readPageMetadata = (route) => parseAbsoluteMobileBuildPageMetadata(route.hooks?.detail?.[ABSOLUTE_MOBILE_ROUTE_DETAIL]);
|
|
1844
|
+
var resolveAssetPath = (buildDirectory, assetPath) => {
|
|
1845
|
+
const resolvedBuildDirectory = resolve7(buildDirectory);
|
|
1846
|
+
const resolvedAsset = resolve7(assetPath);
|
|
1847
|
+
if (resolvedAsset.startsWith(`${resolvedBuildDirectory}/`)) {
|
|
1848
|
+
return resolvedAsset;
|
|
1849
|
+
}
|
|
1850
|
+
return join6(buildDirectory, assetPath.replace(/^\/+/, ""));
|
|
1851
|
+
};
|
|
1852
|
+
var pageFor = async (metadata, manifest, buildDirectory) => {
|
|
1853
|
+
const assetPath = manifest[metadata.bundleKey];
|
|
1854
|
+
if (!assetPath) {
|
|
1855
|
+
throw new TypeError(`Mobile page ${metadata.pageId} references missing manifest asset ${metadata.bundleKey}.`);
|
|
1856
|
+
}
|
|
1857
|
+
const resolvedAssetPath = resolveAssetPath(buildDirectory, assetPath);
|
|
1858
|
+
const bytes = await readFile7(resolvedAssetPath);
|
|
1859
|
+
const bundlePath = `/${relative5(resolve7(buildDirectory), resolvedAssetPath).replaceAll("\\", "/")}`;
|
|
1860
|
+
return {
|
|
1861
|
+
bundleHash: sha256(bytes),
|
|
1862
|
+
bundlePath,
|
|
1863
|
+
contract: metadata.contract,
|
|
1864
|
+
framework: metadata.framework,
|
|
1865
|
+
pageId: metadata.pageId,
|
|
1866
|
+
propsSchemaHash: metadata.propsSchemaHash
|
|
1867
|
+
};
|
|
1868
|
+
};
|
|
1869
|
+
var buildAbsoluteMobileCompatibilityRelease = async (options) => {
|
|
1870
|
+
const [captured, producerBytes] = await Promise.all([
|
|
1871
|
+
captureAbsoluteMobileRouteGraph(options.app),
|
|
1872
|
+
readFile7(options.producerPath)
|
|
1873
|
+
]);
|
|
1874
|
+
if (captured.length === 0) {
|
|
1875
|
+
throw new TypeError("No instrumented AbsoluteJS mobile page routes were found in the finalized Elysia route graph.");
|
|
1876
|
+
}
|
|
1877
|
+
const metadataByPage = new Map;
|
|
1878
|
+
for (const { metadata } of captured) {
|
|
1879
|
+
const prior = metadataByPage.get(metadata.pageId);
|
|
1880
|
+
if (prior && JSON.stringify(prior) !== JSON.stringify(metadata)) {
|
|
1881
|
+
throw new TypeError(`Mobile page ${metadata.pageId} has inconsistent generated contracts.`);
|
|
1882
|
+
}
|
|
1883
|
+
metadataByPage.set(metadata.pageId, metadata);
|
|
1884
|
+
}
|
|
1885
|
+
const pages = await Promise.all([...metadataByPage.values()].map((metadata) => pageFor(metadata, options.manifest, options.buildDirectory)));
|
|
1886
|
+
const producerHash = sha256(producerBytes);
|
|
1887
|
+
const appBuild = `ambuild_${sha256(new TextEncoder().encode(JSON.stringify({
|
|
1888
|
+
pages: pages.map(({ bundleHash, bundlePath, contract, pageId }) => ({
|
|
1889
|
+
bundleHash,
|
|
1890
|
+
bundlePath,
|
|
1891
|
+
contract,
|
|
1892
|
+
pageId
|
|
1893
|
+
})),
|
|
1894
|
+
producerHash,
|
|
1895
|
+
runtime: options.runtime
|
|
1896
|
+
})))}`;
|
|
1897
|
+
const priorAppIds = new Set((options.previousArtifacts ?? []).map(({ appId }) => appId));
|
|
1898
|
+
if (priorAppIds.size > 1 || priorAppIds.size === 1 && !priorAppIds.has(options.appId)) {
|
|
1899
|
+
throw new TypeError("Previous mobile compatibility artifacts belong to another app.");
|
|
1900
|
+
}
|
|
1901
|
+
const unchanged = options.previousArtifacts?.find((artifact2) => artifact2.appBuild === appBuild && artifact2.runtime === options.runtime);
|
|
1902
|
+
const generation = unchanged?.generation ?? Math.max(0, ...(options.previousArtifacts ?? []).map((artifact2) => artifact2.generation)) + 1;
|
|
1903
|
+
const artifact = createAbsoluteMobileCompatibilityArtifact({
|
|
1904
|
+
appBuild,
|
|
1905
|
+
appId: options.appId,
|
|
1906
|
+
generation,
|
|
1907
|
+
pages,
|
|
1908
|
+
producer: {
|
|
1909
|
+
bundleHash: producerHash,
|
|
1910
|
+
bytes: producerBytes.byteLength,
|
|
1911
|
+
exportName: options.producerExport ?? "server",
|
|
1912
|
+
module: options.producerModule ?? "producer.js"
|
|
1913
|
+
},
|
|
1914
|
+
routes: captured.map(({ route }) => route),
|
|
1915
|
+
runtime: options.runtime
|
|
1916
|
+
});
|
|
1917
|
+
return { artifact, producer: new Blob([producerBytes]) };
|
|
1918
|
+
};
|
|
1919
|
+
var captureAbsoluteMobileRouteGraph = async (app) => {
|
|
1920
|
+
if (app.modules)
|
|
1921
|
+
await app.modules;
|
|
1922
|
+
app.compile();
|
|
1923
|
+
const captured = app.routes.flatMap((route) => {
|
|
1924
|
+
const metadata = readPageMetadata(route);
|
|
1925
|
+
if (!metadata || route.method !== "GET" && route.method !== "HEAD") {
|
|
1926
|
+
return [];
|
|
1927
|
+
}
|
|
1928
|
+
return [
|
|
1929
|
+
{
|
|
1930
|
+
metadata,
|
|
1931
|
+
route: {
|
|
1932
|
+
method: route.method === "GET" ? "GET" : "HEAD",
|
|
1933
|
+
pageId: metadata.pageId,
|
|
1934
|
+
pattern: route.path
|
|
1935
|
+
}
|
|
1936
|
+
}
|
|
1937
|
+
];
|
|
1938
|
+
});
|
|
1939
|
+
const identities = new Map;
|
|
1940
|
+
for (const { metadata, route } of captured) {
|
|
1941
|
+
const prior = identities.get(`${route.method}:${route.pattern}`);
|
|
1942
|
+
if (prior && prior !== metadata.pageId) {
|
|
1943
|
+
throw new TypeError(`Mobile route ${route.method} ${route.pattern} resolves to multiple pages.`);
|
|
1944
|
+
}
|
|
1945
|
+
identities.set(`${route.method}:${route.pattern}`, metadata.pageId);
|
|
1946
|
+
}
|
|
1947
|
+
return captured;
|
|
1948
|
+
};
|
|
1949
|
+
|
|
1950
|
+
// src/mobile/capacitorBundle.ts
|
|
1951
|
+
import {
|
|
1952
|
+
copyFile as copyFile4,
|
|
1953
|
+
mkdir as mkdir6,
|
|
1954
|
+
mkdtemp as mkdtemp4,
|
|
1955
|
+
readFile as readFile8,
|
|
1956
|
+
rename as rename7,
|
|
1957
|
+
rm as rm6,
|
|
1958
|
+
writeFile as writeFile7
|
|
1959
|
+
} from "fs/promises";
|
|
1960
|
+
import { existsSync as existsSync2 } from "fs";
|
|
1961
|
+
import { basename, dirname as dirname4, extname, join as join7, resolve as resolve8 } from "path";
|
|
1962
|
+
|
|
1963
|
+
// src/mobile/routeMatcher.ts
|
|
1964
|
+
var REGEXP_SPECIAL_CHARACTERS = /[.*+?^${}()|[\]\\]/g;
|
|
1965
|
+
var routeSegmentPattern = (segment) => {
|
|
1966
|
+
if (segment === "*")
|
|
1967
|
+
return ".*";
|
|
1968
|
+
if (segment.startsWith(":") && segment.endsWith("?"))
|
|
1969
|
+
return "[^/]*";
|
|
1970
|
+
if (segment.startsWith(":"))
|
|
1971
|
+
return "[^/]+";
|
|
1972
|
+
return segment.replace(REGEXP_SPECIAL_CHARACTERS, "\\$&");
|
|
1973
|
+
};
|
|
1974
|
+
var matchesAbsoluteMobileRoutePattern = (pattern, pathname) => {
|
|
1975
|
+
const expression = pattern.split("/").map(routeSegmentPattern).join("/");
|
|
1976
|
+
return new RegExp(`^${expression}/?$`).test(pathname);
|
|
1977
|
+
};
|
|
1978
|
+
var resolveAbsoluteMobileRoute = (routes, pathname, method = "GET") => routes.find((route) => route.method === method && matchesAbsoluteMobileRoutePattern(route.pattern, pathname));
|
|
1979
|
+
|
|
1980
|
+
// src/mobile/producerContextState.ts
|
|
1981
|
+
var ABSOLUTE_MOBILE_PRODUCER_STORAGE_KEY = Symbol.for("absolutejs.mobileProducerAsyncLocalStorage");
|
|
1982
|
+
var frameworks3 = new Set([
|
|
1983
|
+
"angular",
|
|
1984
|
+
"ember",
|
|
1985
|
+
"html",
|
|
1986
|
+
"htmx",
|
|
1987
|
+
"react",
|
|
1988
|
+
"svelte",
|
|
1989
|
+
"vue"
|
|
1990
|
+
]);
|
|
1991
|
+
var isCompatibilityPage = (value) => typeof value === "object" && value !== null && ("bundleHash" in value) && typeof value.bundleHash === "string" && ("contract" in value) && typeof value.contract === "string" && ("framework" in value) && typeof value.framework === "string" && frameworks3.has(value.framework) && ("pageId" in value) && typeof value.pageId === "string" && ("propsSchemaHash" in value) && typeof value.propsSchemaHash === "string";
|
|
1992
|
+
var getCurrentAbsoluteMobileProducerContext = () => {
|
|
1993
|
+
const value = Reflect.get(globalThis, ABSOLUTE_MOBILE_PRODUCER_STORAGE_KEY);
|
|
1994
|
+
if (typeof value !== "object" || value === null || !("getStore" in value) || typeof value.getStore !== "function") {
|
|
1995
|
+
return;
|
|
1996
|
+
}
|
|
1997
|
+
const context = value.getStore();
|
|
1998
|
+
if (typeof context !== "object" || context === null || !("page" in context) || !isCompatibilityPage(context.page) || !("releaseId" in context) || typeof context.releaseId !== "string") {
|
|
1999
|
+
return;
|
|
2000
|
+
}
|
|
2001
|
+
return { page: context.page, releaseId: context.releaseId };
|
|
2002
|
+
};
|
|
2003
|
+
|
|
2004
|
+
// src/mobile/pageProtocol.ts
|
|
2005
|
+
var ABSOLUTE_MOBILE_PAGE_MEDIA_TYPE = "application/vnd.absolute.page+json";
|
|
2006
|
+
var ABSOLUTE_MOBILE_PAGE_PROTOCOL_VERSION = 1;
|
|
2007
|
+
var BAD_REQUEST_STATUS = 400;
|
|
2008
|
+
var OK_STATUS = 200;
|
|
2009
|
+
var SERVER_ERROR_STATUS = 500;
|
|
2010
|
+
var UPGRADE_REQUIRED_STATUS = 426;
|
|
2011
|
+
var MOBILE_PAGE_REQUEST_HEADERS = {
|
|
2012
|
+
appBuild: "x-absolute-mobile-app-build",
|
|
2013
|
+
pageBundle: "x-absolute-mobile-page-bundle",
|
|
2014
|
+
pageContracts: "x-absolute-mobile-page-contracts",
|
|
2015
|
+
pageId: "x-absolute-mobile-page-id",
|
|
2016
|
+
protocol: "x-absolute-mobile-protocol",
|
|
2017
|
+
runtime: "x-absolute-mobile-runtime"
|
|
2018
|
+
};
|
|
2019
|
+
var parseMediaType = (value) => value.split(";", 1)[0]?.trim().toLowerCase();
|
|
2020
|
+
var acceptsAbsoluteMobilePage = (request) => {
|
|
2021
|
+
if (!request)
|
|
2022
|
+
return false;
|
|
2023
|
+
return (request.headers.get("accept") ?? "").split(",").some((value) => parseMediaType(value) === ABSOLUTE_MOBILE_PAGE_MEDIA_TYPE);
|
|
2024
|
+
};
|
|
2025
|
+
var readRequiredHeader = (request, name) => {
|
|
2026
|
+
const value = request.headers.get(name)?.trim();
|
|
2027
|
+
return value ? value : undefined;
|
|
2028
|
+
};
|
|
2029
|
+
var parseAbsoluteMobilePageRequest = (request) => {
|
|
2030
|
+
if (!request || !acceptsAbsoluteMobilePage(request)) {
|
|
2031
|
+
return { kind: "not-mobile" };
|
|
2032
|
+
}
|
|
2033
|
+
const protocolValue = readRequiredHeader(request, MOBILE_PAGE_REQUEST_HEADERS.protocol);
|
|
2034
|
+
const runtime = readRequiredHeader(request, MOBILE_PAGE_REQUEST_HEADERS.runtime);
|
|
2035
|
+
const appBuild = readRequiredHeader(request, MOBILE_PAGE_REQUEST_HEADERS.appBuild);
|
|
2036
|
+
const pageBundle = readRequiredHeader(request, MOBILE_PAGE_REQUEST_HEADERS.pageBundle);
|
|
2037
|
+
const pageContractsValue = readRequiredHeader(request, MOBILE_PAGE_REQUEST_HEADERS.pageContracts);
|
|
2038
|
+
const pageId = readRequiredHeader(request, MOBILE_PAGE_REQUEST_HEADERS.pageId);
|
|
2039
|
+
if (!protocolValue || !/^\d+$/.test(protocolValue)) {
|
|
2040
|
+
return {
|
|
2041
|
+
kind: "invalid",
|
|
2042
|
+
message: `Missing or invalid ${MOBILE_PAGE_REQUEST_HEADERS.protocol} header.`
|
|
2043
|
+
};
|
|
2044
|
+
}
|
|
2045
|
+
if (!runtime) {
|
|
2046
|
+
return {
|
|
2047
|
+
kind: "invalid",
|
|
2048
|
+
message: `Missing ${MOBILE_PAGE_REQUEST_HEADERS.runtime} header.`
|
|
2049
|
+
};
|
|
2050
|
+
}
|
|
2051
|
+
if (!appBuild) {
|
|
2052
|
+
return {
|
|
2053
|
+
kind: "invalid",
|
|
2054
|
+
message: `Missing ${MOBILE_PAGE_REQUEST_HEADERS.appBuild} header.`
|
|
2055
|
+
};
|
|
2056
|
+
}
|
|
2057
|
+
if (!pageBundle) {
|
|
2058
|
+
return {
|
|
2059
|
+
kind: "invalid",
|
|
2060
|
+
message: `Missing ${MOBILE_PAGE_REQUEST_HEADERS.pageBundle} header.`
|
|
2061
|
+
};
|
|
2062
|
+
}
|
|
2063
|
+
if (!pageContractsValue) {
|
|
2064
|
+
return {
|
|
2065
|
+
kind: "invalid",
|
|
2066
|
+
message: `Missing ${MOBILE_PAGE_REQUEST_HEADERS.pageContracts} header.`
|
|
2067
|
+
};
|
|
2068
|
+
}
|
|
2069
|
+
if (!pageId) {
|
|
2070
|
+
return {
|
|
2071
|
+
kind: "invalid",
|
|
2072
|
+
message: `Missing ${MOBILE_PAGE_REQUEST_HEADERS.pageId} header.`
|
|
2073
|
+
};
|
|
2074
|
+
}
|
|
2075
|
+
const pageContracts = [
|
|
2076
|
+
...new Set(pageContractsValue.split(",").map((value) => value.trim()).filter(Boolean))
|
|
2077
|
+
];
|
|
2078
|
+
if (pageContracts.length === 0) {
|
|
2079
|
+
return {
|
|
2080
|
+
kind: "invalid",
|
|
2081
|
+
message: `${MOBILE_PAGE_REQUEST_HEADERS.pageContracts} must contain at least one contract.`
|
|
2082
|
+
};
|
|
2083
|
+
}
|
|
2084
|
+
return {
|
|
2085
|
+
client: {
|
|
2086
|
+
appBuild,
|
|
2087
|
+
pageBundle,
|
|
2088
|
+
pageContracts,
|
|
2089
|
+
pageId,
|
|
2090
|
+
protocol: Number(protocolValue),
|
|
2091
|
+
runtime
|
|
2092
|
+
},
|
|
2093
|
+
kind: "mobile"
|
|
2094
|
+
};
|
|
2095
|
+
};
|
|
2096
|
+
var responseHeaders = () => {
|
|
2097
|
+
const headers = new Headers({
|
|
2098
|
+
"cache-control": "no-store",
|
|
2099
|
+
"content-type": `${ABSOLUTE_MOBILE_PAGE_MEDIA_TYPE}; version=${ABSOLUTE_MOBILE_PAGE_PROTOCOL_VERSION}`
|
|
2100
|
+
});
|
|
2101
|
+
headerVaryValues.forEach((value) => headers.append("vary", value));
|
|
2102
|
+
return headers;
|
|
2103
|
+
};
|
|
2104
|
+
var headerVaryValues = [
|
|
2105
|
+
"Accept",
|
|
2106
|
+
MOBILE_PAGE_REQUEST_HEADERS.protocol,
|
|
2107
|
+
MOBILE_PAGE_REQUEST_HEADERS.runtime,
|
|
2108
|
+
MOBILE_PAGE_REQUEST_HEADERS.appBuild,
|
|
2109
|
+
MOBILE_PAGE_REQUEST_HEADERS.pageBundle,
|
|
2110
|
+
MOBILE_PAGE_REQUEST_HEADERS.pageContracts,
|
|
2111
|
+
MOBILE_PAGE_REQUEST_HEADERS.pageId
|
|
2112
|
+
];
|
|
2113
|
+
var envelopeResponse = (response, status) => new Response(JSON.stringify({
|
|
2114
|
+
protocol: ABSOLUTE_MOBILE_PAGE_PROTOCOL_VERSION,
|
|
2115
|
+
response
|
|
2116
|
+
}), { headers: responseHeaders(), status });
|
|
2117
|
+
var isRecord4 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2118
|
+
var normalizeJsonValue = (value) => {
|
|
2119
|
+
const serialized = JSON.stringify(value);
|
|
2120
|
+
if (serialized === undefined) {
|
|
2121
|
+
throw new TypeError("Mobile page props must be JSON-serializable.");
|
|
2122
|
+
}
|
|
2123
|
+
const parsed = JSON.parse(serialized);
|
|
2124
|
+
if (!isRecord4(parsed)) {
|
|
2125
|
+
throw new TypeError("Mobile page props must serialize to an object.");
|
|
2126
|
+
}
|
|
2127
|
+
return parsed;
|
|
2128
|
+
};
|
|
2129
|
+
var finalizeArchivedMobilePage = (context, input) => {
|
|
2130
|
+
const { page } = context;
|
|
2131
|
+
if (page.pageId !== input.compatibility.pageId || page.framework !== input.compatibility.framework) {
|
|
2132
|
+
return envelopeResponse({
|
|
2133
|
+
kind: "invalid-request",
|
|
2134
|
+
message: "Archived producer page identity does not match its release artifact."
|
|
2135
|
+
}, BAD_REQUEST_STATUS);
|
|
2136
|
+
}
|
|
2137
|
+
try {
|
|
2138
|
+
const [currentRepresentation] = input.compatibility.representations;
|
|
2139
|
+
if (!currentRepresentation) {
|
|
2140
|
+
throw new TypeError("Mobile page has no current representation.");
|
|
2141
|
+
}
|
|
2142
|
+
return envelopeResponse({
|
|
2143
|
+
contract: page.contract,
|
|
2144
|
+
framework: page.framework,
|
|
2145
|
+
kind: "page",
|
|
2146
|
+
pageId: page.pageId,
|
|
2147
|
+
props: normalizeJsonValue(currentRepresentation.mapProps(input.props)),
|
|
2148
|
+
status: input.status ?? OK_STATUS
|
|
2149
|
+
}, input.status ?? OK_STATUS);
|
|
2150
|
+
} catch (error) {
|
|
2151
|
+
console.error(`[Mobile] Archived producer ${context.releaseId} failed for ${page.pageId}:`, error);
|
|
2152
|
+
return createAbsoluteMobilePageErrorResponse(page.pageId);
|
|
2153
|
+
}
|
|
2154
|
+
};
|
|
2155
|
+
var createAbsoluteMobileInvalidRequestResponse = (message) => envelopeResponse({ kind: "invalid-request", message }, BAD_REQUEST_STATUS);
|
|
2156
|
+
var createAbsoluteMobilePageErrorResponse = (pageId) => envelopeResponse({
|
|
693
2157
|
code: "representation-failed",
|
|
694
2158
|
kind: "error",
|
|
695
2159
|
pageId,
|
|
@@ -767,7 +2231,7 @@ class AbsoluteMobilePageProtocolError extends Error {
|
|
|
767
2231
|
this.code = code;
|
|
768
2232
|
}
|
|
769
2233
|
}
|
|
770
|
-
var
|
|
2234
|
+
var frameworks4 = new Set([
|
|
771
2235
|
"angular",
|
|
772
2236
|
"ember",
|
|
773
2237
|
"html",
|
|
@@ -782,14 +2246,14 @@ var upgradeReasons = new Set([
|
|
|
782
2246
|
"protocol",
|
|
783
2247
|
"runtime"
|
|
784
2248
|
]);
|
|
785
|
-
var
|
|
786
|
-
var isFramework = (value) => typeof value === "string" &&
|
|
2249
|
+
var isRecord5 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2250
|
+
var isFramework = (value) => typeof value === "string" && frameworks4.has(value);
|
|
787
2251
|
var isUpgradeReason = (value) => typeof value === "string" && upgradeReasons.has(value);
|
|
788
2252
|
var parsePageResult = (value) => {
|
|
789
2253
|
if (typeof value.contract !== "string" || !isFramework(value.framework) || typeof value.pageId !== "string" || typeof value.status !== "number") {
|
|
790
2254
|
throw new AbsoluteMobilePageProtocolError("invalid-envelope", "The mobile page response is missing required page metadata.");
|
|
791
2255
|
}
|
|
792
|
-
if (!
|
|
2256
|
+
if (!isRecord5(value.props)) {
|
|
793
2257
|
throw new AbsoluteMobilePageProtocolError("invalid-props", "The mobile page response must contain an object props value.");
|
|
794
2258
|
}
|
|
795
2259
|
return {
|
|
@@ -835,7 +2299,7 @@ var activateAbsoluteMobilePage = async (value, options) => {
|
|
|
835
2299
|
};
|
|
836
2300
|
};
|
|
837
2301
|
var parseAbsoluteMobilePageEnvelope = (value) => {
|
|
838
|
-
if (!
|
|
2302
|
+
if (!isRecord5(value) || !isRecord5(value.response)) {
|
|
839
2303
|
throw new AbsoluteMobilePageProtocolError("invalid-envelope", "The server did not return an AbsoluteJS mobile page envelope.");
|
|
840
2304
|
}
|
|
841
2305
|
if (value.protocol !== ABSOLUTE_MOBILE_PAGE_PROTOCOL_VERSION) {
|
|
@@ -862,122 +2326,246 @@ var parseAbsoluteMobilePageEnvelope = (value) => {
|
|
|
862
2326
|
}
|
|
863
2327
|
throw new AbsoluteMobilePageProtocolError("invalid-envelope", `Unknown mobile page response kind ${String(kind)}.`);
|
|
864
2328
|
};
|
|
865
|
-
// src/mobile/compatibilityDispatcher.ts
|
|
866
|
-
import { Elysia } from "elysia";
|
|
867
2329
|
|
|
868
|
-
// src/mobile/
|
|
869
|
-
|
|
870
|
-
var
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
2330
|
+
// src/mobile/transport.ts
|
|
2331
|
+
var ABSOLUTE_MOBILE_CLIENT_MANIFEST_FORMAT = 1;
|
|
2332
|
+
var pageForPathname = (manifest, pathname) => {
|
|
2333
|
+
const route = resolveAbsoluteMobileRoute(manifest.routes, pathname);
|
|
2334
|
+
if (!route) {
|
|
2335
|
+
throw new TypeError(`No embedded mobile page owns ${pathname}.`);
|
|
2336
|
+
}
|
|
2337
|
+
const page = manifest.pages.find((candidate) => candidate.pageId === route.pageId);
|
|
2338
|
+
if (!page) {
|
|
2339
|
+
throw new TypeError(`Mobile route ${route.pattern} references missing page ${route.pageId}.`);
|
|
2340
|
+
}
|
|
2341
|
+
return page;
|
|
874
2342
|
};
|
|
875
|
-
var
|
|
876
|
-
const
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
2343
|
+
var canonicalBackendUrl = (manifest, path) => {
|
|
2344
|
+
const origin = new URL(manifest.productionOrigin);
|
|
2345
|
+
const url = new URL(path, `${origin.origin}/`);
|
|
2346
|
+
if (url.origin !== origin.origin) {
|
|
2347
|
+
throw new TypeError("Mobile transport cannot leave productionOrigin.");
|
|
2348
|
+
}
|
|
2349
|
+
url.hash = "";
|
|
2350
|
+
return url;
|
|
2351
|
+
};
|
|
2352
|
+
var createAbsoluteMobilePageRequest = (manifest, path, options = {}) => {
|
|
2353
|
+
const url = canonicalBackendUrl(manifest, path);
|
|
2354
|
+
const page = pageForPathname(manifest, url.pathname);
|
|
2355
|
+
const headers = new Headers(options.headers);
|
|
2356
|
+
headers.set("accept", ABSOLUTE_MOBILE_PAGE_MEDIA_TYPE);
|
|
2357
|
+
headers.set(MOBILE_PAGE_REQUEST_HEADERS.appBuild, manifest.appBuild);
|
|
2358
|
+
headers.set(MOBILE_PAGE_REQUEST_HEADERS.pageBundle, page.bundleHash);
|
|
2359
|
+
headers.set(MOBILE_PAGE_REQUEST_HEADERS.pageContracts, page.contract);
|
|
2360
|
+
headers.set(MOBILE_PAGE_REQUEST_HEADERS.pageId, page.pageId);
|
|
2361
|
+
headers.set(MOBILE_PAGE_REQUEST_HEADERS.protocol, String(ABSOLUTE_MOBILE_PAGE_PROTOCOL_VERSION));
|
|
2362
|
+
headers.set(MOBILE_PAGE_REQUEST_HEADERS.runtime, manifest.runtime);
|
|
2363
|
+
return new Request(url, { headers, method: "GET" });
|
|
2364
|
+
};
|
|
2365
|
+
var fetchAbsoluteMobilePage = async (manifest, path, options = {}) => {
|
|
2366
|
+
const request = createAbsoluteMobilePageRequest(manifest, path, options);
|
|
2367
|
+
const response = await (options.fetch ?? globalThis.fetch)(request);
|
|
2368
|
+
const value = await response.json();
|
|
2369
|
+
return value;
|
|
2370
|
+
};
|
|
2371
|
+
var navigateAbsoluteMobilePage = async (manifest, path, options) => {
|
|
2372
|
+
const envelope = await fetchAbsoluteMobilePage(manifest, path, options);
|
|
2373
|
+
return activateAbsoluteMobilePage(envelope, options);
|
|
2374
|
+
};
|
|
2375
|
+
var resolveAbsoluteMobileDeepLink = (manifest, value) => {
|
|
2376
|
+
const url = new URL(value);
|
|
2377
|
+
if (url.username || url.password) {
|
|
2378
|
+
throw new TypeError("Mobile deep links cannot contain credentials.");
|
|
2379
|
+
}
|
|
2380
|
+
const isHttpsHost = url.protocol === "https:" && manifest.deepLinkHosts.includes(url.hostname.toLowerCase());
|
|
2381
|
+
const isCustomScheme = manifest.deepLinkScheme !== undefined && url.protocol === `${manifest.deepLinkScheme}:`;
|
|
2382
|
+
if (!isHttpsHost && !isCustomScheme) {
|
|
2383
|
+
throw new TypeError("Mobile deep link is outside the configured app URLs.");
|
|
2384
|
+
}
|
|
2385
|
+
return `${url.pathname || "/"}${url.search}${url.hash}`;
|
|
882
2386
|
};
|
|
883
|
-
var runWithAbsoluteMobileProducer = (context, callback) => ensureProducerStorage().run(context, callback);
|
|
884
2387
|
|
|
885
|
-
// src/mobile/
|
|
886
|
-
var
|
|
887
|
-
var
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
2388
|
+
// src/mobile/capacitorBundle.ts
|
|
2389
|
+
var MANIFEST_FILE = "absolute-mobile-manifest.json";
|
|
2390
|
+
var BOOTSTRAP_FILE = "absolute-mobile-bootstrap.js";
|
|
2391
|
+
var INDEX_FILE = "index.html";
|
|
2392
|
+
var CLIENT_IMPORT_PATTERN = /(?:\bfrom\s*|\bimport\s*\(\s*|\bimport\s*)["'](\/[^"']+)["']/gu;
|
|
2393
|
+
var errorHasCode2 = (error, code) => typeof error === "object" && error !== null && Reflect.get(error, "code") === code;
|
|
2394
|
+
var shellBootstrapModule = () => {
|
|
2395
|
+
const candidate = ["js", "ts"].map((extension) => join7(import.meta.dir, `shellBootstrap.${extension}`)).find(existsSync2);
|
|
2396
|
+
if (candidate)
|
|
2397
|
+
return candidate;
|
|
2398
|
+
throw new TypeError("AbsoluteJS mobile shell bootstrap module is missing.");
|
|
895
2399
|
};
|
|
896
|
-
var
|
|
897
|
-
|
|
898
|
-
|
|
2400
|
+
var escapeHtml = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """);
|
|
2401
|
+
var indexHtml = (appName) => `<!doctype html>
|
|
2402
|
+
<html>
|
|
2403
|
+
<head>
|
|
2404
|
+
<meta charset="utf-8">
|
|
2405
|
+
<meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover">
|
|
2406
|
+
<meta name="color-scheme" content="light dark">
|
|
2407
|
+
<title>${escapeHtml(appName)}</title>
|
|
2408
|
+
</head>
|
|
2409
|
+
<body>
|
|
2410
|
+
<main id="absolute-mobile-status" role="status">Starting\u2026</main>
|
|
2411
|
+
<script type="module" src="./${BOOTSTRAP_FILE}"></script>
|
|
2412
|
+
</body>
|
|
2413
|
+
</html>
|
|
2414
|
+
`;
|
|
2415
|
+
var sourceAssetPath = (buildDirectory, bundlePath) => {
|
|
2416
|
+
const root = resolve8(buildDirectory);
|
|
2417
|
+
const asset = resolve8(root, bundlePath.replace(/^\/+/, ""));
|
|
2418
|
+
if (!asset.startsWith(`${root}/`)) {
|
|
2419
|
+
throw new TypeError("Mobile page bundle escaped the build directory.");
|
|
2420
|
+
}
|
|
2421
|
+
return asset;
|
|
899
2422
|
};
|
|
900
|
-
var
|
|
901
|
-
const
|
|
902
|
-
|
|
2423
|
+
var buildShellBootstrap = async (staging) => {
|
|
2424
|
+
const modulePath = shellBootstrapModule();
|
|
2425
|
+
const entryPath = join7(staging, ".absolute-mobile-entry.ts");
|
|
2426
|
+
await writeFile7(entryPath, `import { startAbsoluteMobileShell } from ${JSON.stringify(modulePath)};
|
|
2427
|
+
void startAbsoluteMobileShell();
|
|
2428
|
+
`);
|
|
2429
|
+
const build = await Bun.build({
|
|
2430
|
+
entrypoints: [entryPath],
|
|
2431
|
+
minify: true,
|
|
2432
|
+
outdir: staging,
|
|
2433
|
+
target: "browser"
|
|
2434
|
+
});
|
|
2435
|
+
if (!build.success || build.outputs.length !== 1) {
|
|
2436
|
+
throw new AggregateError(build.logs, "Failed to build the AbsoluteJS Capacitor shell.");
|
|
2437
|
+
}
|
|
2438
|
+
await rename7(build.outputs[0]?.path ?? "", join7(staging, BOOTSTRAP_FILE));
|
|
2439
|
+
await rm6(entryPath, { force: true });
|
|
903
2440
|
};
|
|
904
|
-
var
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
2441
|
+
var removePreviousBundle = async (backup, moved) => {
|
|
2442
|
+
if (!moved)
|
|
2443
|
+
return;
|
|
2444
|
+
await rm6(backup, { force: true, recursive: true });
|
|
2445
|
+
};
|
|
2446
|
+
var restorePreviousBundle = async (backup, destination, moved) => {
|
|
2447
|
+
if (!moved)
|
|
2448
|
+
return;
|
|
2449
|
+
await rename7(backup, destination);
|
|
2450
|
+
};
|
|
2451
|
+
var installBundle = async (staging, destination) => {
|
|
2452
|
+
const backup = `${destination}.previous-${crypto.randomUUID()}`;
|
|
2453
|
+
let movedPrevious = false;
|
|
2454
|
+
try {
|
|
2455
|
+
await rename7(destination, backup);
|
|
2456
|
+
movedPrevious = true;
|
|
2457
|
+
} catch (error) {
|
|
2458
|
+
if (!errorHasCode2(error, "ENOENT"))
|
|
912
2459
|
throw error;
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
2460
|
+
}
|
|
2461
|
+
try {
|
|
2462
|
+
await rename7(staging, destination);
|
|
2463
|
+
await removePreviousBundle(backup, movedPrevious);
|
|
2464
|
+
} catch (error) {
|
|
2465
|
+
await restorePreviousBundle(backup, destination, movedPrevious);
|
|
2466
|
+
throw error;
|
|
2467
|
+
}
|
|
917
2468
|
};
|
|
918
|
-
var
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
2469
|
+
var copyClientPage = async (page, buildDirectory, staging, copiedDependencies) => {
|
|
2470
|
+
if (page.framework !== "react") {
|
|
2471
|
+
throw new TypeError(`Capacitor spike currently supports React pages; ${page.pageId} is ${page.framework}.`);
|
|
2472
|
+
}
|
|
2473
|
+
const extension = extname(page.bundlePath) || ".js";
|
|
2474
|
+
const localBundlePath = `./pages/${page.bundleHash}${extension}`;
|
|
2475
|
+
const source = sourceAssetPath(buildDirectory, page.bundlePath);
|
|
2476
|
+
await copyFile4(source, join7(staging, localBundlePath));
|
|
2477
|
+
await copyAbsoluteClientDependencies(source, buildDirectory, staging, copiedDependencies);
|
|
2478
|
+
return { ...page, localBundlePath };
|
|
2479
|
+
};
|
|
2480
|
+
var absoluteClientImports = async (sourcePath) => {
|
|
2481
|
+
const source = await readFile8(sourcePath, "utf8");
|
|
2482
|
+
return [...source.matchAll(CLIENT_IMPORT_PATTERN)].flatMap((match) => {
|
|
2483
|
+
const [specifier] = match.slice(1);
|
|
2484
|
+
return specifier ? [specifier.split(/[?#]/u, 1)[0] ?? specifier] : [];
|
|
2485
|
+
});
|
|
2486
|
+
};
|
|
2487
|
+
var copyAbsoluteClientDependency = async (specifier, buildDirectory, staging, copied) => {
|
|
2488
|
+
if (copied.has(specifier))
|
|
2489
|
+
return;
|
|
2490
|
+
copied.add(specifier);
|
|
2491
|
+
const source = sourceAssetPath(buildDirectory, specifier);
|
|
2492
|
+
const destination = join7(staging, specifier.replace(/^\/+/, ""));
|
|
2493
|
+
await mkdir6(dirname4(destination), { recursive: true });
|
|
2494
|
+
await copyFile4(source, destination);
|
|
2495
|
+
await copyAbsoluteClientDependencies(source, buildDirectory, staging, copied);
|
|
2496
|
+
};
|
|
2497
|
+
var copyAbsoluteClientDependencies = async (sourcePath, buildDirectory, staging, copied) => {
|
|
2498
|
+
const dependencies = await absoluteClientImports(sourcePath);
|
|
2499
|
+
await Promise.all(dependencies.map((specifier) => copyAbsoluteClientDependency(specifier, buildDirectory, staging, copied)));
|
|
2500
|
+
};
|
|
2501
|
+
var materializeAbsoluteCapacitorWebBundle = async (options) => {
|
|
2502
|
+
if (!resolveAbsoluteMobileRoute(options.artifact.routes, new URL(options.config.entry, "https://absolute.invalid").pathname)) {
|
|
2503
|
+
throw new TypeError(`mobile.entry ${options.config.entry} is not a captured mobile page route.`);
|
|
2504
|
+
}
|
|
2505
|
+
const destination = options.config.bundleDirectory;
|
|
2506
|
+
await mkdir6(dirname4(destination), { recursive: true });
|
|
2507
|
+
const staging = await mkdtemp4(join7(dirname4(destination), `.${basename(destination)}.stage-`));
|
|
2508
|
+
try {
|
|
2509
|
+
const pageDirectory = join7(staging, "pages");
|
|
2510
|
+
await mkdir6(pageDirectory, { recursive: true });
|
|
2511
|
+
const copiedDependencies = new Set;
|
|
2512
|
+
const pages = await Promise.all(options.artifact.pages.map((page) => copyClientPage(page, options.buildDirectory, staging, copiedDependencies)));
|
|
2513
|
+
const manifest = {
|
|
2514
|
+
appBuild: options.artifact.appBuild,
|
|
2515
|
+
appId: options.config.appId,
|
|
2516
|
+
appName: options.config.appName,
|
|
2517
|
+
deepLinkHosts: options.config.deepLinkHosts,
|
|
2518
|
+
deepLinkScheme: options.config.deepLinkScheme,
|
|
2519
|
+
entry: options.config.entry,
|
|
2520
|
+
format: ABSOLUTE_MOBILE_CLIENT_MANIFEST_FORMAT,
|
|
2521
|
+
pages,
|
|
2522
|
+
productionOrigin: options.config.productionOrigin,
|
|
2523
|
+
routes: options.artifact.routes,
|
|
2524
|
+
runtime: options.artifact.runtime
|
|
2525
|
+
};
|
|
2526
|
+
await Promise.all([
|
|
2527
|
+
writeFile7(join7(staging, MANIFEST_FILE), `${JSON.stringify(manifest, null, "\t")}
|
|
2528
|
+
`),
|
|
2529
|
+
writeFile7(join7(staging, INDEX_FILE), indexHtml(options.config.appName)),
|
|
2530
|
+
buildShellBootstrap(staging)
|
|
2531
|
+
]);
|
|
2532
|
+
await installBundle(staging, destination);
|
|
2533
|
+
return manifest;
|
|
2534
|
+
} catch (error) {
|
|
2535
|
+
await rm6(staging, { force: true, recursive: true });
|
|
2536
|
+
throw error;
|
|
922
2537
|
}
|
|
923
|
-
const resolveProducer = createProducerResolver(options.loadProducer);
|
|
924
|
-
return new Elysia({ name: "absolutejs-mobile-compatibility-dispatcher" }).request(async ({ request }) => {
|
|
925
|
-
if (getCurrentAbsoluteMobileProducerContext())
|
|
926
|
-
return;
|
|
927
|
-
const parsed = parseAbsoluteMobilePageRequest(request);
|
|
928
|
-
if (parsed.kind !== "mobile")
|
|
929
|
-
return;
|
|
930
|
-
const resolved = resolveAbsoluteMobileCompatibilityRelease(parsed.client, artifacts);
|
|
931
|
-
if (resolved.kind === "upgrade-required") {
|
|
932
|
-
return createAbsoluteMobileUpgradeResponse(resolved.result);
|
|
933
|
-
}
|
|
934
|
-
if (!artifactOwnsRequest(resolved.artifact, parsed.client.pageId, request)) {
|
|
935
|
-
return createAbsoluteMobileInvalidRequestResponse("The requested URL is not assigned to this mobile page.");
|
|
936
|
-
}
|
|
937
|
-
if (resolved.artifact.releaseId === options.currentReleaseId) {
|
|
938
|
-
return;
|
|
939
|
-
}
|
|
940
|
-
try {
|
|
941
|
-
const producer = await resolveProducer(resolved.artifact);
|
|
942
|
-
return runWithAbsoluteMobileProducer({
|
|
943
|
-
page: resolved.page,
|
|
944
|
-
releaseId: resolved.artifact.releaseId
|
|
945
|
-
}, () => producer.handle(request));
|
|
946
|
-
} catch (error) {
|
|
947
|
-
console.error(`[Mobile] Failed to load retained producer ${resolved.artifact.releaseId}:`, error);
|
|
948
|
-
return createAbsoluteMobilePageErrorResponse(parsed.client.pageId);
|
|
949
|
-
}
|
|
950
|
-
}).as("global");
|
|
951
2538
|
};
|
|
2539
|
+
|
|
952
2540
|
// src/mobile/materializedBundle.ts
|
|
953
|
-
import { createHash as
|
|
2541
|
+
import { createHash as createHash7 } from "crypto";
|
|
954
2542
|
import {
|
|
955
|
-
access,
|
|
956
|
-
mkdir as
|
|
957
|
-
mkdtemp as
|
|
958
|
-
readFile as
|
|
959
|
-
rename as
|
|
960
|
-
rm as
|
|
961
|
-
writeFile as
|
|
2543
|
+
access as access6,
|
|
2544
|
+
mkdir as mkdir7,
|
|
2545
|
+
mkdtemp as mkdtemp5,
|
|
2546
|
+
readFile as readFile9,
|
|
2547
|
+
rename as rename8,
|
|
2548
|
+
rm as rm7,
|
|
2549
|
+
writeFile as writeFile8
|
|
962
2550
|
} from "fs/promises";
|
|
963
|
-
import { dirname, join as
|
|
2551
|
+
import { dirname as dirname5, join as join8, resolve as resolvePath2 } from "path";
|
|
964
2552
|
import { pathToFileURL } from "url";
|
|
965
2553
|
var ABSOLUTE_MOBILE_MATERIALIZED_BUNDLE_FORMAT = 1;
|
|
966
2554
|
var CURRENT_BUNDLE_FILE = "current.json";
|
|
967
2555
|
var BUNDLES_DIRECTORY = "bundles";
|
|
968
2556
|
var ARTIFACT_FILE2 = "artifact.json";
|
|
969
2557
|
var BUNDLE_ID_PATTERN = /^amb_[a-f0-9]{64}$/;
|
|
970
|
-
var
|
|
971
|
-
var
|
|
2558
|
+
var isRecord6 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2559
|
+
var errorHasCode3 = (error, code) => typeof error === "object" && error !== null && Reflect.get(error, "code") === code;
|
|
972
2560
|
var bundleIdFor = (currentReleaseId, releases) => {
|
|
973
2561
|
const identity = JSON.stringify({
|
|
974
2562
|
currentReleaseId,
|
|
975
2563
|
releases: releases.map(({ releaseId }) => releaseId)
|
|
976
2564
|
});
|
|
977
|
-
return `amb_${
|
|
2565
|
+
return `amb_${createHash7("sha256").update(identity).digest("hex")}`;
|
|
978
2566
|
};
|
|
979
2567
|
var parseBundleIndex = (value) => {
|
|
980
|
-
if (!
|
|
2568
|
+
if (!isRecord6(value) || value.format !== ABSOLUTE_MOBILE_MATERIALIZED_BUNDLE_FORMAT || typeof value.bundleId !== "string" || !BUNDLE_ID_PATTERN.test(value.bundleId) || typeof value.currentReleaseId !== "string" || !Array.isArray(value.releases)) {
|
|
981
2569
|
throw new TypeError("Invalid materialized mobile compatibility bundle.");
|
|
982
2570
|
}
|
|
983
2571
|
const releases = value.releases.map(parseAbsoluteMobileCompatibilityArtifact);
|
|
@@ -1000,31 +2588,31 @@ var parseBundleIndex = (value) => {
|
|
|
1000
2588
|
};
|
|
1001
2589
|
};
|
|
1002
2590
|
var writeRelease = async (root, release) => {
|
|
1003
|
-
const directory =
|
|
1004
|
-
const producerPath =
|
|
1005
|
-
await
|
|
2591
|
+
const directory = join8(root, release.artifact.releaseId);
|
|
2592
|
+
const producerPath = join8(directory, release.artifact.producer.module);
|
|
2593
|
+
await mkdir7(dirname5(producerPath), { recursive: true });
|
|
1006
2594
|
await Promise.all([
|
|
1007
|
-
|
|
2595
|
+
writeFile8(join8(directory, ARTIFACT_FILE2), `${JSON.stringify(release.artifact, null, "\t")}
|
|
1008
2596
|
`),
|
|
1009
|
-
|
|
2597
|
+
writeFile8(producerPath, new Uint8Array(await release.producer.arrayBuffer()))
|
|
1010
2598
|
]);
|
|
1011
2599
|
};
|
|
1012
2600
|
var installImmutableBundle = async (bundlesRoot, bundleId, releases) => {
|
|
1013
|
-
const destination =
|
|
2601
|
+
const destination = join8(bundlesRoot, bundleId);
|
|
1014
2602
|
try {
|
|
1015
|
-
await
|
|
2603
|
+
await access6(destination);
|
|
1016
2604
|
return destination;
|
|
1017
2605
|
} catch (error) {
|
|
1018
|
-
if (!
|
|
2606
|
+
if (!errorHasCode3(error, "ENOENT"))
|
|
1019
2607
|
throw error;
|
|
1020
2608
|
}
|
|
1021
|
-
const staging = await
|
|
2609
|
+
const staging = await mkdtemp5(join8(bundlesRoot, ".stage-"));
|
|
1022
2610
|
try {
|
|
1023
2611
|
await Promise.all(releases.map((release) => writeRelease(staging, release)));
|
|
1024
|
-
await
|
|
2612
|
+
await rename8(staging, destination);
|
|
1025
2613
|
} catch (error) {
|
|
1026
|
-
await
|
|
1027
|
-
if (
|
|
2614
|
+
await rm7(staging, { force: true, recursive: true });
|
|
2615
|
+
if (errorHasCode3(error, "EEXIST") || errorHasCode3(error, "ENOTEMPTY")) {
|
|
1028
2616
|
return destination;
|
|
1029
2617
|
}
|
|
1030
2618
|
throw error;
|
|
@@ -1034,7 +2622,7 @@ var installImmutableBundle = async (bundlesRoot, bundleId, releases) => {
|
|
|
1034
2622
|
var readCompatibilityModule = (modulePath) => import(pathToFileURL(modulePath).href);
|
|
1035
2623
|
var resolveProducerHandler = (loaded, exportName) => {
|
|
1036
2624
|
const value = loaded[exportName];
|
|
1037
|
-
if (!
|
|
2625
|
+
if (!isRecord6(value) || typeof value.handle !== "function") {
|
|
1038
2626
|
throw new TypeError(`Compatibility producer export ${exportName} must expose handle(request).`);
|
|
1039
2627
|
}
|
|
1040
2628
|
const { handle } = value;
|
|
@@ -1052,15 +2640,15 @@ var resolveProducerHandler = (loaded, exportName) => {
|
|
|
1052
2640
|
};
|
|
1053
2641
|
var loadAbsoluteMobileMaterializedBundle = async (root) => {
|
|
1054
2642
|
const resolvedRoot = resolvePath2(root);
|
|
1055
|
-
const serialized = await
|
|
2643
|
+
const serialized = await readFile9(join8(resolvedRoot, CURRENT_BUNDLE_FILE), "utf8");
|
|
1056
2644
|
const parsed = JSON.parse(serialized);
|
|
1057
2645
|
const index = parseBundleIndex(parsed);
|
|
1058
|
-
const bundleRoot =
|
|
2646
|
+
const bundleRoot = join8(resolvedRoot, BUNDLES_DIRECTORY, index.bundleId);
|
|
1059
2647
|
return {
|
|
1060
2648
|
artifacts: index.releases,
|
|
1061
2649
|
currentReleaseId: index.currentReleaseId,
|
|
1062
2650
|
loadProducer: async (artifact) => {
|
|
1063
|
-
const modulePath =
|
|
2651
|
+
const modulePath = join8(bundleRoot, artifact.releaseId, artifact.producer.module);
|
|
1064
2652
|
await verifyAbsoluteMobileCompatibilityProducer({
|
|
1065
2653
|
artifact,
|
|
1066
2654
|
producer: Bun.file(modulePath)
|
|
@@ -1087,8 +2675,8 @@ var materializeAbsoluteMobileCompatibilityBundle = async (input) => {
|
|
|
1087
2675
|
return release;
|
|
1088
2676
|
});
|
|
1089
2677
|
const root = resolvePath2(input.root);
|
|
1090
|
-
const bundlesRoot =
|
|
1091
|
-
await
|
|
2678
|
+
const bundlesRoot = join8(root, BUNDLES_DIRECTORY);
|
|
2679
|
+
await mkdir7(bundlesRoot, { recursive: true });
|
|
1092
2680
|
const bundleId = bundleIdFor(input.currentReleaseId, artifacts);
|
|
1093
2681
|
await installImmutableBundle(bundlesRoot, bundleId, orderedReleases);
|
|
1094
2682
|
const index = {
|
|
@@ -1097,44 +2685,863 @@ var materializeAbsoluteMobileCompatibilityBundle = async (input) => {
|
|
|
1097
2685
|
format: ABSOLUTE_MOBILE_MATERIALIZED_BUNDLE_FORMAT,
|
|
1098
2686
|
releases: artifacts
|
|
1099
2687
|
};
|
|
1100
|
-
const pointerPath =
|
|
1101
|
-
const temporaryPointerPath =
|
|
1102
|
-
await
|
|
2688
|
+
const pointerPath = join8(root, CURRENT_BUNDLE_FILE);
|
|
2689
|
+
const temporaryPointerPath = join8(root, `.current-${crypto.randomUUID()}.json`);
|
|
2690
|
+
await writeFile8(temporaryPointerPath, `${JSON.stringify(index, null, "\t")}
|
|
1103
2691
|
`, { flag: "wx" });
|
|
1104
|
-
await
|
|
2692
|
+
await rename8(temporaryPointerPath, pointerPath);
|
|
1105
2693
|
return index;
|
|
1106
2694
|
};
|
|
2695
|
+
var readAbsoluteMobileMaterializedReleases = async (root) => {
|
|
2696
|
+
const resolvedRoot = resolvePath2(root);
|
|
2697
|
+
try {
|
|
2698
|
+
const serialized = await readFile9(join8(resolvedRoot, CURRENT_BUNDLE_FILE), "utf8");
|
|
2699
|
+
const parsed = JSON.parse(serialized);
|
|
2700
|
+
const index = parseBundleIndex(parsed);
|
|
2701
|
+
const bundleRoot = join8(resolvedRoot, BUNDLES_DIRECTORY, index.bundleId);
|
|
2702
|
+
return Promise.all(index.releases.map(async (artifact) => {
|
|
2703
|
+
const producer = Bun.file(join8(bundleRoot, artifact.releaseId, artifact.producer.module));
|
|
2704
|
+
await verifyAbsoluteMobileCompatibilityProducer({
|
|
2705
|
+
artifact,
|
|
2706
|
+
producer
|
|
2707
|
+
});
|
|
2708
|
+
return { artifact, producer };
|
|
2709
|
+
}));
|
|
2710
|
+
} catch (error) {
|
|
2711
|
+
if (errorHasCode3(error, "ENOENT"))
|
|
2712
|
+
return [];
|
|
2713
|
+
throw error;
|
|
2714
|
+
}
|
|
2715
|
+
};
|
|
2716
|
+
|
|
2717
|
+
// src/mobile/buildPipeline.ts
|
|
2718
|
+
var isElysiaApp = (value) => typeof value === "object" && value !== null && typeof Reflect.get(value, "compile") === "function" && Array.isArray(Reflect.get(value, "routes"));
|
|
2719
|
+
var isStringRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value) && Object.values(value).every((entry) => typeof entry === "string");
|
|
2720
|
+
var serverExportName = (loaded, app) => {
|
|
2721
|
+
if (loaded.server === app)
|
|
2722
|
+
return "server";
|
|
2723
|
+
if (loaded.app === app)
|
|
2724
|
+
return "app";
|
|
2725
|
+
return "default";
|
|
2726
|
+
};
|
|
2727
|
+
var restoreBuildDirectory = (previous) => {
|
|
2728
|
+
if (previous !== undefined) {
|
|
2729
|
+
process.env.ABSOLUTE_BUILD_DIR = previous;
|
|
2730
|
+
return;
|
|
2731
|
+
}
|
|
2732
|
+
delete process.env.ABSOLUTE_BUILD_DIR;
|
|
2733
|
+
};
|
|
2734
|
+
var requireRelease = (releases, releaseId) => {
|
|
2735
|
+
const release = releases.get(releaseId);
|
|
2736
|
+
if (!release) {
|
|
2737
|
+
throw new TypeError(`Missing retained mobile release ${releaseId}.`);
|
|
2738
|
+
}
|
|
2739
|
+
return release;
|
|
2740
|
+
};
|
|
2741
|
+
var loadServerApp = async (producerPath) => {
|
|
2742
|
+
const loaded = await import(`${pathToFileURL2(producerPath).href}?absolute-mobile-capture=${crypto.randomUUID()}`);
|
|
2743
|
+
const candidates = [loaded.server, loaded.app, loaded.default];
|
|
2744
|
+
const app = candidates.find(isElysiaApp);
|
|
2745
|
+
if (!app) {
|
|
2746
|
+
throw new TypeError("Mobile builds require the server entry to export its Elysia app as server, app, or default.");
|
|
2747
|
+
}
|
|
2748
|
+
const exportName = serverExportName(loaded, app);
|
|
2749
|
+
return { app, exportName };
|
|
2750
|
+
};
|
|
2751
|
+
var finalizeAbsoluteMobileCompatibilityBuild = async (options) => {
|
|
2752
|
+
const buildDirectory = resolve9(options.buildDirectory);
|
|
2753
|
+
const mobile = normalizeAbsoluteMobileConfig(options.mobile, options.projectRoot);
|
|
2754
|
+
const root = join9(buildDirectory, ".absolutejs", "mobile-compatibility");
|
|
2755
|
+
const [manifestSource, previous] = await Promise.all([
|
|
2756
|
+
readFile10(join9(buildDirectory, "manifest.json"), "utf8"),
|
|
2757
|
+
readAbsoluteMobileMaterializedReleases(root)
|
|
2758
|
+
]);
|
|
2759
|
+
const manifest = JSON.parse(manifestSource);
|
|
2760
|
+
if (!isStringRecord(manifest)) {
|
|
2761
|
+
throw new TypeError("Invalid AbsoluteJS build manifest for mobile capture.");
|
|
2762
|
+
}
|
|
2763
|
+
const previousBuildDirectory = process.env.ABSOLUTE_BUILD_DIR;
|
|
2764
|
+
process.env.ABSOLUTE_BUILD_DIR = buildDirectory;
|
|
2765
|
+
let loaded;
|
|
2766
|
+
try {
|
|
2767
|
+
loaded = await loadServerApp(resolve9(options.producerPath));
|
|
2768
|
+
} finally {
|
|
2769
|
+
restoreBuildDirectory(previousBuildDirectory);
|
|
2770
|
+
}
|
|
2771
|
+
const current = await buildAbsoluteMobileCompatibilityRelease({
|
|
2772
|
+
app: loaded.app,
|
|
2773
|
+
appId: mobile.appId,
|
|
2774
|
+
buildDirectory,
|
|
2775
|
+
manifest,
|
|
2776
|
+
previousArtifacts: previous.map(({ artifact }) => artifact),
|
|
2777
|
+
producerExport: loaded.exportName,
|
|
2778
|
+
producerPath: resolve9(options.producerPath),
|
|
2779
|
+
runtime: String(ABSOLUTE_MOBILE_PAGE_PROTOCOL_VERSION)
|
|
2780
|
+
});
|
|
2781
|
+
const releasesById = new Map([current, ...previous].map((release) => [
|
|
2782
|
+
release.artifact.releaseId,
|
|
2783
|
+
release
|
|
2784
|
+
]));
|
|
2785
|
+
const retained = retainAbsoluteMobileCompatibilityArtifacts([...releasesById.values()].map(({ artifact }) => artifact));
|
|
2786
|
+
await materializeAbsoluteMobileCompatibilityBundle({
|
|
2787
|
+
currentReleaseId: current.artifact.releaseId,
|
|
2788
|
+
releases: retained.map((artifact) => requireRelease(releasesById, artifact.releaseId)),
|
|
2789
|
+
root
|
|
2790
|
+
});
|
|
2791
|
+
await materializeAbsoluteCapacitorWebBundle({
|
|
2792
|
+
artifact: current.artifact,
|
|
2793
|
+
buildDirectory,
|
|
2794
|
+
config: mobile
|
|
2795
|
+
});
|
|
2796
|
+
return current.artifact;
|
|
2797
|
+
};
|
|
2798
|
+
// src/mobile/compatibilityDispatcher.ts
|
|
2799
|
+
import { Elysia as Elysia2 } from "elysia";
|
|
2800
|
+
|
|
2801
|
+
// src/mobile/producerContext.ts
|
|
2802
|
+
import { AsyncLocalStorage } from "async_hooks";
|
|
2803
|
+
var isProducerStorage = (value) => typeof value === "object" && value !== null && ("getStore" in value) && typeof value.getStore === "function" && ("run" in value) && typeof value.run === "function";
|
|
2804
|
+
var getProducerStorage = () => {
|
|
2805
|
+
const value = Reflect.get(globalThis, ABSOLUTE_MOBILE_PRODUCER_STORAGE_KEY);
|
|
2806
|
+
return isProducerStorage(value) ? value : undefined;
|
|
2807
|
+
};
|
|
2808
|
+
var ensureProducerStorage = () => {
|
|
2809
|
+
const existing = getProducerStorage();
|
|
2810
|
+
if (existing)
|
|
2811
|
+
return existing;
|
|
2812
|
+
const storage = new AsyncLocalStorage;
|
|
2813
|
+
Reflect.set(globalThis, ABSOLUTE_MOBILE_PRODUCER_STORAGE_KEY, storage);
|
|
2814
|
+
return storage;
|
|
2815
|
+
};
|
|
2816
|
+
var runWithAbsoluteMobileProducer = (context, callback) => ensureProducerStorage().run(context, callback);
|
|
2817
|
+
|
|
2818
|
+
// src/mobile/compatibilityDispatcher.ts
|
|
2819
|
+
var artifactOwnsRequest = (artifact, pageId, request) => {
|
|
2820
|
+
const { pathname } = new URL(request.url);
|
|
2821
|
+
return artifact.routes.some((route) => route.pageId === pageId && route.method === request.method && matchesAbsoluteMobileRoutePattern(route.pattern, pathname));
|
|
2822
|
+
};
|
|
2823
|
+
var createProducerResolver = (loadProducer) => {
|
|
2824
|
+
const producers = new Map;
|
|
2825
|
+
return (artifact) => {
|
|
2826
|
+
const cached = producers.get(artifact.releaseId);
|
|
2827
|
+
if (cached)
|
|
2828
|
+
return cached;
|
|
2829
|
+
const loading = loadProducer(artifact).catch((error) => {
|
|
2830
|
+
producers.delete(artifact.releaseId);
|
|
2831
|
+
throw error;
|
|
2832
|
+
});
|
|
2833
|
+
producers.set(artifact.releaseId, loading);
|
|
2834
|
+
return loading;
|
|
2835
|
+
};
|
|
2836
|
+
};
|
|
2837
|
+
var createAbsoluteMobileCompatibilityDispatcher = (options) => {
|
|
2838
|
+
const artifacts = retainAbsoluteMobileCompatibilityArtifacts(options.artifacts);
|
|
2839
|
+
if (!artifacts.some(({ releaseId }) => releaseId === options.currentReleaseId)) {
|
|
2840
|
+
throw new TypeError("currentReleaseId must identify a retained compatibility artifact.");
|
|
2841
|
+
}
|
|
2842
|
+
const resolveProducer = createProducerResolver(options.loadProducer);
|
|
2843
|
+
return new Elysia2({ name: "absolutejs-mobile-compatibility-dispatcher" }).request(async ({ request }) => {
|
|
2844
|
+
if (getCurrentAbsoluteMobileProducerContext())
|
|
2845
|
+
return;
|
|
2846
|
+
const parsed = parseAbsoluteMobilePageRequest(request);
|
|
2847
|
+
if (parsed.kind !== "mobile")
|
|
2848
|
+
return;
|
|
2849
|
+
const resolved = resolveAbsoluteMobileCompatibilityRelease(parsed.client, artifacts);
|
|
2850
|
+
if (resolved.kind === "upgrade-required") {
|
|
2851
|
+
return createAbsoluteMobileUpgradeResponse(resolved.result);
|
|
2852
|
+
}
|
|
2853
|
+
if (!artifactOwnsRequest(resolved.artifact, parsed.client.pageId, request)) {
|
|
2854
|
+
return createAbsoluteMobileInvalidRequestResponse("The requested URL is not assigned to this mobile page.");
|
|
2855
|
+
}
|
|
2856
|
+
if (resolved.artifact.releaseId === options.currentReleaseId) {
|
|
2857
|
+
return;
|
|
2858
|
+
}
|
|
2859
|
+
try {
|
|
2860
|
+
const producer = await resolveProducer(resolved.artifact);
|
|
2861
|
+
return runWithAbsoluteMobileProducer({
|
|
2862
|
+
page: resolved.page,
|
|
2863
|
+
releaseId: resolved.artifact.releaseId
|
|
2864
|
+
}, () => producer.handle(request));
|
|
2865
|
+
} catch (error) {
|
|
2866
|
+
console.error(`[Mobile] Failed to load retained producer ${resolved.artifact.releaseId}:`, error);
|
|
2867
|
+
return createAbsoluteMobilePageErrorResponse(parsed.client.pageId);
|
|
2868
|
+
}
|
|
2869
|
+
}).as("global");
|
|
2870
|
+
};
|
|
2871
|
+
// src/mobile/nativeDeepLinks.ts
|
|
2872
|
+
import { readFile as readFile11, rename as rename9, writeFile as writeFile9 } from "fs/promises";
|
|
2873
|
+
import { join as join10 } from "path";
|
|
2874
|
+
var START_MARKER = "<!-- absolutejs:deep-links:start -->";
|
|
2875
|
+
var END_MARKER = "<!-- absolutejs:deep-links:end -->";
|
|
2876
|
+
var IOS_ENTITLEMENTS = "App/AbsoluteJS.entitlements";
|
|
2877
|
+
var NOT_FOUND = -1;
|
|
2878
|
+
var escapeXml = (value) => value.replaceAll("&", "&").replaceAll('"', """).replaceAll("'", "'").replaceAll("<", "<").replaceAll(">", ">");
|
|
2879
|
+
var writeChangedFile = async (path, source) => {
|
|
2880
|
+
const current = await readFile11(path, "utf8");
|
|
2881
|
+
if (current === source)
|
|
2882
|
+
return false;
|
|
2883
|
+
const temporary = `${path}.${crypto.randomUUID()}.tmp`;
|
|
2884
|
+
await writeFile9(temporary, source, { flag: "wx" });
|
|
2885
|
+
await rename9(temporary, path);
|
|
2886
|
+
return true;
|
|
2887
|
+
};
|
|
2888
|
+
var replaceManagedRegion = (source, region, insertAt) => {
|
|
2889
|
+
const start = source.indexOf(START_MARKER);
|
|
2890
|
+
const end = source.indexOf(END_MARKER);
|
|
2891
|
+
if (start === NOT_FOUND !== (end === NOT_FOUND) || start !== NOT_FOUND && end < start) {
|
|
2892
|
+
throw new TypeError("AbsoluteJS deep-link ownership markers are malformed.");
|
|
2893
|
+
}
|
|
2894
|
+
if (start !== NOT_FOUND) {
|
|
2895
|
+
const lineStart = source.lastIndexOf(`
|
|
2896
|
+
`, start) + 1;
|
|
2897
|
+
const nextLine = source.indexOf(`
|
|
2898
|
+
`, end + END_MARKER.length);
|
|
2899
|
+
const lineEnd = nextLine === NOT_FOUND ? source.length : nextLine + 1;
|
|
2900
|
+
return `${source.slice(0, lineStart)}${region}${source.slice(lineEnd)}`;
|
|
2901
|
+
}
|
|
2902
|
+
const index = insertAt();
|
|
2903
|
+
if (index === NOT_FOUND) {
|
|
2904
|
+
throw new TypeError("Could not find a safe native project location for deep-link configuration.");
|
|
2905
|
+
}
|
|
2906
|
+
return `${source.slice(0, index)}${region}${source.slice(index)}`;
|
|
2907
|
+
};
|
|
2908
|
+
var androidRegion = (config) => {
|
|
2909
|
+
const hosts = config.deepLinkHosts.map((host) => ` <data android:scheme="https" android:host="${escapeXml(host)}" />`).join(`
|
|
2910
|
+
`);
|
|
2911
|
+
const customScheme = config.deepLinkScheme ? `
|
|
2912
|
+
|
|
2913
|
+
<intent-filter>
|
|
2914
|
+
<action android:name="android.intent.action.VIEW" />
|
|
2915
|
+
<category android:name="android.intent.category.DEFAULT" />
|
|
2916
|
+
<category android:name="android.intent.category.BROWSABLE" />
|
|
2917
|
+
<data android:scheme="${escapeXml(config.deepLinkScheme)}" />
|
|
2918
|
+
</intent-filter>` : "";
|
|
2919
|
+
return ` ${START_MARKER}
|
|
2920
|
+
<intent-filter android:autoVerify="true">
|
|
2921
|
+
<action android:name="android.intent.action.VIEW" />
|
|
2922
|
+
<category android:name="android.intent.category.DEFAULT" />
|
|
2923
|
+
<category android:name="android.intent.category.BROWSABLE" />
|
|
2924
|
+
${hosts}
|
|
2925
|
+
</intent-filter>${customScheme}
|
|
2926
|
+
${END_MARKER}
|
|
2927
|
+
`;
|
|
2928
|
+
};
|
|
2929
|
+
var configureAndroid = async (config) => {
|
|
2930
|
+
const path = join10(config.nativeProjectDirectory, "android/app/src/main/AndroidManifest.xml");
|
|
2931
|
+
const source = await readFile11(path, "utf8");
|
|
2932
|
+
const mainActivity = source.indexOf('android:name=".MainActivity"');
|
|
2933
|
+
if (mainActivity === NOT_FOUND) {
|
|
2934
|
+
throw new TypeError("Android MainActivity was not found.");
|
|
2935
|
+
}
|
|
2936
|
+
const activityEnd = source.indexOf("</activity>", mainActivity);
|
|
2937
|
+
const updated = replaceManagedRegion(source, androidRegion(config), () => activityEnd === NOT_FOUND ? NOT_FOUND : source.lastIndexOf(`
|
|
2938
|
+
`, activityEnd) + 1);
|
|
2939
|
+
return writeChangedFile(path, updated);
|
|
2940
|
+
};
|
|
2941
|
+
var iosSchemeRegion = (scheme) => ` ${START_MARKER}
|
|
2942
|
+
<key>CFBundleURLTypes</key>
|
|
2943
|
+
<array>
|
|
2944
|
+
<dict>
|
|
2945
|
+
<key>CFBundleURLName</key>
|
|
2946
|
+
<string>absolutejs</string>
|
|
2947
|
+
<key>CFBundleURLSchemes</key>
|
|
2948
|
+
<array>
|
|
2949
|
+
<string>${escapeXml(scheme)}</string>
|
|
2950
|
+
</array>
|
|
2951
|
+
</dict>
|
|
2952
|
+
</array>
|
|
2953
|
+
${END_MARKER}
|
|
2954
|
+
`;
|
|
2955
|
+
var configureIosInfo = async (config) => {
|
|
2956
|
+
const path = join10(config.nativeProjectDirectory, "ios/App/App/Info.plist");
|
|
2957
|
+
const source = await readFile11(path, "utf8");
|
|
2958
|
+
const region = config.deepLinkScheme ? iosSchemeRegion(config.deepLinkScheme) : ` ${START_MARKER}
|
|
2959
|
+
${END_MARKER}
|
|
2960
|
+
`;
|
|
2961
|
+
const updated = replaceManagedRegion(source, region, () => source.lastIndexOf("</dict>"));
|
|
2962
|
+
return writeChangedFile(path, updated);
|
|
2963
|
+
};
|
|
2964
|
+
var iosEntitlementsSource = (config) => {
|
|
2965
|
+
const domains = config.deepLinkHosts.map((host) => ` <string>applinks:${escapeXml(host)}</string>`).join(`
|
|
2966
|
+
`);
|
|
2967
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
2968
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
2969
|
+
<plist version="1.0">
|
|
2970
|
+
<dict>
|
|
2971
|
+
<key>com.apple.developer.associated-domains</key>
|
|
2972
|
+
<array>
|
|
2973
|
+
${domains}
|
|
2974
|
+
</array>
|
|
2975
|
+
</dict>
|
|
2976
|
+
</plist>
|
|
2977
|
+
`;
|
|
2978
|
+
};
|
|
2979
|
+
var configureIosEntitlements = async (config) => {
|
|
2980
|
+
const path = join10(config.nativeProjectDirectory, "ios/App/AbsoluteJS.entitlements");
|
|
2981
|
+
let current = "";
|
|
2982
|
+
try {
|
|
2983
|
+
current = await readFile11(path, "utf8");
|
|
2984
|
+
} catch (error) {
|
|
2985
|
+
if (!(error instanceof Error) || !("code" in error) || error.code !== "ENOENT") {
|
|
2986
|
+
throw error;
|
|
2987
|
+
}
|
|
2988
|
+
}
|
|
2989
|
+
const source = iosEntitlementsSource(config);
|
|
2990
|
+
if (current === source)
|
|
2991
|
+
return false;
|
|
2992
|
+
const temporary = `${path}.${crypto.randomUUID()}.tmp`;
|
|
2993
|
+
await writeFile9(temporary, source, { flag: "wx" });
|
|
2994
|
+
await rename9(temporary, path);
|
|
2995
|
+
return true;
|
|
2996
|
+
};
|
|
2997
|
+
var configureIosProject = async (config) => {
|
|
2998
|
+
const path = join10(config.nativeProjectDirectory, "ios/App/App.xcodeproj/project.pbxproj");
|
|
2999
|
+
const source = await readFile11(path, "utf8");
|
|
3000
|
+
const declarations = [
|
|
3001
|
+
...source.matchAll(/CODE_SIGN_ENTITLEMENTS = ([^;]+);/g)
|
|
3002
|
+
].map((match) => match[1]);
|
|
3003
|
+
if (declarations.some((value) => value !== IOS_ENTITLEMENTS)) {
|
|
3004
|
+
throw new TypeError("The iOS target already uses a different entitlements file; merge associated domains there before continuing.");
|
|
3005
|
+
}
|
|
3006
|
+
if (declarations.length > 0)
|
|
3007
|
+
return false;
|
|
3008
|
+
let count = 0;
|
|
3009
|
+
const updated = source.replaceAll("INFOPLIST_FILE = App/Info.plist;", () => {
|
|
3010
|
+
count += 1;
|
|
3011
|
+
return `CODE_SIGN_ENTITLEMENTS = ${IOS_ENTITLEMENTS};
|
|
3012
|
+
INFOPLIST_FILE = App/Info.plist;`;
|
|
3013
|
+
});
|
|
3014
|
+
if (count === 0) {
|
|
3015
|
+
throw new TypeError("The iOS application target build settings were not found.");
|
|
3016
|
+
}
|
|
3017
|
+
return writeChangedFile(path, updated);
|
|
3018
|
+
};
|
|
3019
|
+
var configureIos = async (config) => {
|
|
3020
|
+
const changed = await Promise.all([
|
|
3021
|
+
configureIosInfo(config),
|
|
3022
|
+
configureIosEntitlements(config),
|
|
3023
|
+
configureIosProject(config)
|
|
3024
|
+
]);
|
|
3025
|
+
return changed.some(Boolean);
|
|
3026
|
+
};
|
|
3027
|
+
var applyAbsoluteNativeDeepLinks = async (config, platforms = config.platforms) => {
|
|
3028
|
+
const results = await Promise.all(platforms.map(async (platform) => {
|
|
3029
|
+
const didChange = platform === "android" ? await configureAndroid(config) : await configureIos(config);
|
|
3030
|
+
return { didChange, platform };
|
|
3031
|
+
}));
|
|
3032
|
+
return {
|
|
3033
|
+
changed: results.filter(({ didChange }) => didChange).map(({ platform }) => platform)
|
|
3034
|
+
};
|
|
3035
|
+
};
|
|
3036
|
+
// src/mobile/releasePublisher.ts
|
|
3037
|
+
import { access as access7 } from "fs/promises";
|
|
3038
|
+
import { isAbsolute as isAbsolute4, relative as relative6, resolve as resolve10, sep as sep4 } from "path";
|
|
3039
|
+
import { pathToFileURL as pathToFileURL3 } from "url";
|
|
3040
|
+
var prepareAbsoluteIosRelease = async (publisher, options) => {
|
|
3041
|
+
if (typeof publisher.prepareIosRelease !== "function") {
|
|
3042
|
+
throw new TypeError("App Store Connect publishing requires a registry module created with @absolutejs/deploy/app-store-connect.");
|
|
3043
|
+
}
|
|
3044
|
+
const { buildNumber } = await publisher.prepareIosRelease(options);
|
|
3045
|
+
if (!Number.isSafeInteger(buildNumber) || buildNumber < 1) {
|
|
3046
|
+
throw new TypeError("App Store Connect publisher returned an invalid iOS build number.");
|
|
3047
|
+
}
|
|
3048
|
+
return buildNumber;
|
|
3049
|
+
};
|
|
3050
|
+
var prepareAbsoluteAndroidRelease = async (publisher, options) => {
|
|
3051
|
+
if (typeof publisher.prepareAndroidRelease !== "function") {
|
|
3052
|
+
throw new TypeError("Google Play publishing requires a registry module created with @absolutejs/deploy/google-play.");
|
|
3053
|
+
}
|
|
3054
|
+
const prepared = await publisher.prepareAndroidRelease(options);
|
|
3055
|
+
const { versionCode } = prepared;
|
|
3056
|
+
if (typeof versionCode !== "number" || !Number.isSafeInteger(versionCode) || versionCode < 1 || versionCode > 2100000000) {
|
|
3057
|
+
throw new TypeError("Google Play publisher returned an invalid Android versionCode.");
|
|
3058
|
+
}
|
|
3059
|
+
return versionCode;
|
|
3060
|
+
};
|
|
3061
|
+
var isRecord7 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3062
|
+
var isPublisher = (value) => isRecord7(value) && typeof value.publish === "function";
|
|
3063
|
+
var publisherModulePath = (projectRoot, requested) => {
|
|
3064
|
+
const root = resolve10(projectRoot);
|
|
3065
|
+
const path = resolve10(root, requested);
|
|
3066
|
+
const projectRelative = relative6(root, path);
|
|
3067
|
+
if (projectRelative === ".." || projectRelative.startsWith(`..${sep4}`) || isAbsolute4(projectRelative)) {
|
|
3068
|
+
throw new TypeError("mobile publish --registry must remain inside the project.");
|
|
3069
|
+
}
|
|
3070
|
+
return path;
|
|
3071
|
+
};
|
|
3072
|
+
var loadAbsoluteNativeReleasePublisher = async (projectRoot, requestedModulePath) => {
|
|
3073
|
+
const modulePath = publisherModulePath(projectRoot, requestedModulePath);
|
|
3074
|
+
await access7(modulePath).catch(() => {
|
|
3075
|
+
throw new TypeError(`Native release registry module does not exist: ${modulePath}`);
|
|
3076
|
+
});
|
|
3077
|
+
const loaded = await import(pathToFileURL3(modulePath).href);
|
|
3078
|
+
const publisher = isRecord7(loaded) ? loaded.default ?? loaded.registry : undefined;
|
|
3079
|
+
if (!isPublisher(publisher)) {
|
|
3080
|
+
throw new TypeError("Native release registry module must default-export a registry with publish(options).");
|
|
3081
|
+
}
|
|
3082
|
+
return publisher;
|
|
3083
|
+
};
|
|
3084
|
+
var publishAbsoluteAndroidRelease = async (options) => {
|
|
3085
|
+
const publisher = await loadAbsoluteNativeReleasePublisher(options.projectRoot, options.modulePath);
|
|
3086
|
+
const publication = await publisher.publish({
|
|
3087
|
+
allowUnsigned: options.allowUnsigned,
|
|
3088
|
+
channel: options.channel,
|
|
3089
|
+
googlePlay: options.googlePlay,
|
|
3090
|
+
releaseRoot: options.release.releaseRoot,
|
|
3091
|
+
signal: options.signal
|
|
3092
|
+
});
|
|
3093
|
+
const expected = options.release.metadata;
|
|
3094
|
+
const actual = publication.record?.metadata;
|
|
3095
|
+
if (!actual || actual.appId !== expected.appId || actual.platform !== "android" || actual.releaseId !== expected.releaseId || actual.sha256 !== expected.sha256 || actual.signed !== expected.signed || actual.versionCode !== expected.versionCode || typeof publication.reused !== "boolean") {
|
|
3096
|
+
throw new TypeError("Native release registry returned a different Android release identity.");
|
|
3097
|
+
}
|
|
3098
|
+
if (options.channel !== undefined && (publication.channel?.channel !== options.channel || publication.channel.releaseId !== expected.releaseId)) {
|
|
3099
|
+
throw new TypeError("Native release registry did not promote the requested channel.");
|
|
3100
|
+
}
|
|
3101
|
+
const { googlePlay } = publication;
|
|
3102
|
+
if (options.googlePlay) {
|
|
3103
|
+
if (!expected.versionCode || !googlePlay || googlePlay.receipt.provider !== "google-play" || googlePlay.receipt.packageName !== expected.appId || googlePlay.receipt.releaseId !== expected.releaseId || googlePlay.receipt.sha256 !== expected.sha256 || googlePlay.receipt.stage !== "committed" || googlePlay.receipt.intent.track !== options.googlePlay.track || typeof googlePlay.receipt.versionCode !== "string" || !/^\d+$/.test(googlePlay.receipt.versionCode) || Number(googlePlay.receipt.versionCode) !== expected.versionCode || typeof googlePlay.reused !== "boolean") {
|
|
3104
|
+
throw new TypeError("Native release publisher did not commit the requested Google Play release.");
|
|
3105
|
+
}
|
|
3106
|
+
}
|
|
3107
|
+
return publication;
|
|
3108
|
+
};
|
|
3109
|
+
var publishAbsoluteIosRelease = async (options) => {
|
|
3110
|
+
const publisher = await loadAbsoluteNativeReleasePublisher(options.projectRoot, options.modulePath);
|
|
3111
|
+
const publication = await publisher.publish({
|
|
3112
|
+
allowUnsigned: options.allowUnsigned,
|
|
3113
|
+
appStoreConnect: options.appStoreConnect,
|
|
3114
|
+
channel: options.channel,
|
|
3115
|
+
releaseRoot: options.release.releaseRoot,
|
|
3116
|
+
signal: options.signal
|
|
3117
|
+
});
|
|
3118
|
+
const expected = options.release.metadata;
|
|
3119
|
+
const actual = publication.record?.metadata;
|
|
3120
|
+
if (!actual || actual.appId !== expected.appId || actual.platform !== "ios" || actual.releaseId !== expected.releaseId || actual.sha256 !== expected.sha256 || actual.signed !== expected.signed || actual.buildNumber !== expected.buildNumber || actual.marketingVersion !== expected.marketingVersion || typeof publication.reused !== "boolean") {
|
|
3121
|
+
throw new TypeError("Native release registry returned a different iOS release identity.");
|
|
3122
|
+
}
|
|
3123
|
+
if (options.channel !== undefined && (publication.channel?.channel !== options.channel || publication.channel.releaseId !== expected.releaseId)) {
|
|
3124
|
+
throw new TypeError("Native release registry did not promote the requested channel.");
|
|
3125
|
+
}
|
|
3126
|
+
if (options.appStoreConnect) {
|
|
3127
|
+
const distributed = publication.appStoreConnect;
|
|
3128
|
+
if (!expected.buildNumber || !distributed || distributed.receipt.provider !== "app-store-connect" || distributed.receipt.releaseId !== expected.releaseId || distributed.receipt.sha256 !== expected.sha256 || distributed.receipt.buildNumber !== expected.buildNumber || distributed.receipt.marketingVersion !== expected.marketingVersion || !["distributed", "review-submitted"].includes(distributed.receipt.stage) || JSON.stringify([...distributed.receipt.intent.groups].sort()) !== JSON.stringify([...options.appStoreConnect.groups ?? []].sort()) || distributed.receipt.intent.submitForReview !== (options.appStoreConnect.submitForReview ?? false) || typeof distributed.reused !== "boolean") {
|
|
3129
|
+
throw new TypeError("Native release publisher did not complete the requested App Store Connect release.");
|
|
3130
|
+
}
|
|
3131
|
+
}
|
|
3132
|
+
return publication;
|
|
3133
|
+
};
|
|
3134
|
+
// src/mobile/routeMetadataTransform.ts
|
|
3135
|
+
import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
|
|
3136
|
+
import { dirname as dirname6, extname as extname2, relative as relative7, resolve as resolve11 } from "path";
|
|
3137
|
+
import ts from "typescript";
|
|
3138
|
+
var ROUTE_METHODS = new Set(["get", "head"]);
|
|
3139
|
+
var SOURCE_FILTER = /\.[cm]?[jt]sx?$/;
|
|
3140
|
+
var PAGE_HANDLER = "handleReactPageRequest";
|
|
3141
|
+
var posixPath = (value) => value.replace(/\\/g, "/");
|
|
3142
|
+
var findTsconfig = (entry, projectRoot) => ts.findConfigFile(dirname6(entry), existsSync3, "tsconfig.json") ?? ts.findConfigFile(projectRoot, existsSync3, "tsconfig.json");
|
|
3143
|
+
var createProgram = (entry, projectRoot) => {
|
|
3144
|
+
const configPath = findTsconfig(entry, projectRoot);
|
|
3145
|
+
if (!configPath) {
|
|
3146
|
+
return ts.createProgram([entry], {
|
|
3147
|
+
allowJs: true,
|
|
3148
|
+
jsx: ts.JsxEmit.ReactJSX,
|
|
3149
|
+
module: ts.ModuleKind.ESNext,
|
|
3150
|
+
moduleResolution: ts.ModuleResolutionKind.Bundler,
|
|
3151
|
+
target: ts.ScriptTarget.ESNext
|
|
3152
|
+
});
|
|
3153
|
+
}
|
|
3154
|
+
const parsed = ts.parseJsonConfigFileContent(ts.readConfigFile(configPath, (path) => readFileSync2(path, "utf8")).config, ts.sys, dirname6(configPath));
|
|
3155
|
+
if (!parsed.fileNames.includes(entry))
|
|
3156
|
+
parsed.fileNames.push(entry);
|
|
3157
|
+
return ts.createProgram(parsed.fileNames, parsed.options);
|
|
3158
|
+
};
|
|
3159
|
+
var propertyName = (property) => {
|
|
3160
|
+
if (!("name" in property) || !property.name)
|
|
3161
|
+
return;
|
|
3162
|
+
if (ts.isIdentifier(property.name))
|
|
3163
|
+
return property.name.text;
|
|
3164
|
+
if (ts.isStringLiteralLike(property.name))
|
|
3165
|
+
return property.name.text;
|
|
3166
|
+
return;
|
|
3167
|
+
};
|
|
3168
|
+
var objectPropertyExpression = (object, name) => {
|
|
3169
|
+
const property = object.properties.find((candidate) => propertyName(candidate) === name);
|
|
3170
|
+
if (property && ts.isPropertyAssignment(property)) {
|
|
3171
|
+
return property.initializer;
|
|
3172
|
+
}
|
|
3173
|
+
if (property && ts.isShorthandPropertyAssignment(property)) {
|
|
3174
|
+
return property.name;
|
|
3175
|
+
}
|
|
3176
|
+
return;
|
|
3177
|
+
};
|
|
3178
|
+
var serializeType = (type, checker, ancestors = new Set) => {
|
|
3179
|
+
if (type.flags & ts.TypeFlags.Any)
|
|
3180
|
+
return { type: "any" };
|
|
3181
|
+
if (type.flags & ts.TypeFlags.Unknown)
|
|
3182
|
+
return { type: "unknown" };
|
|
3183
|
+
if (type.flags & ts.TypeFlags.Never)
|
|
3184
|
+
return { type: "never" };
|
|
3185
|
+
if (type.flags & ts.TypeFlags.StringLike)
|
|
3186
|
+
return { type: "string" };
|
|
3187
|
+
if (type.flags & ts.TypeFlags.NumberLike)
|
|
3188
|
+
return { type: "number" };
|
|
3189
|
+
if (type.flags & ts.TypeFlags.BooleanLike)
|
|
3190
|
+
return { type: "boolean" };
|
|
3191
|
+
if (type.flags & ts.TypeFlags.BigIntLike)
|
|
3192
|
+
return { type: "bigint" };
|
|
3193
|
+
if (type.flags & ts.TypeFlags.Null)
|
|
3194
|
+
return { type: "null" };
|
|
3195
|
+
if (type.flags & ts.TypeFlags.Undefined)
|
|
3196
|
+
return { type: "undefined" };
|
|
3197
|
+
if (type.isUnion()) {
|
|
3198
|
+
return {
|
|
3199
|
+
anyOf: type.types.map((member) => serializeType(member, checker, ancestors)).sort((left, right) => JSON.stringify(left).localeCompare(JSON.stringify(right)))
|
|
3200
|
+
};
|
|
3201
|
+
}
|
|
3202
|
+
if (type.isIntersection()) {
|
|
3203
|
+
return {
|
|
3204
|
+
allOf: type.types.map((member) => serializeType(member, checker, ancestors)).sort((left, right) => JSON.stringify(left).localeCompare(JSON.stringify(right)))
|
|
3205
|
+
};
|
|
3206
|
+
}
|
|
3207
|
+
if (ancestors.has(type)) {
|
|
3208
|
+
return {
|
|
3209
|
+
ref: checker.typeToString(type, undefined, ts.TypeFormatFlags.NoTruncation)
|
|
3210
|
+
};
|
|
3211
|
+
}
|
|
3212
|
+
ancestors.add(type);
|
|
3213
|
+
const arrayElement = checker.getIndexTypeOfType(type, ts.IndexKind.Number);
|
|
3214
|
+
const properties = checker.getPropertiesOfType(type);
|
|
3215
|
+
let schema;
|
|
3216
|
+
if (arrayElement && properties.some(({ name }) => name === "length")) {
|
|
3217
|
+
schema = {
|
|
3218
|
+
items: serializeType(arrayElement, checker, ancestors),
|
|
3219
|
+
type: "array"
|
|
3220
|
+
};
|
|
3221
|
+
} else if (properties.length > 0) {
|
|
3222
|
+
const entries = properties.filter(({ name }) => !name.startsWith("__")).map((property) => {
|
|
3223
|
+
const declaration = property.valueDeclaration ?? property.declarations?.[0];
|
|
3224
|
+
const propertyType = declaration ? checker.getTypeOfSymbolAtLocation(property, declaration) : checker.getDeclaredTypeOfSymbol(property);
|
|
3225
|
+
return [
|
|
3226
|
+
property.name,
|
|
3227
|
+
{
|
|
3228
|
+
optional: Boolean(property.flags & ts.SymbolFlags.Optional),
|
|
3229
|
+
schema: serializeType(propertyType, checker, ancestors)
|
|
3230
|
+
}
|
|
3231
|
+
];
|
|
3232
|
+
}).sort(([left], [right]) => left.localeCompare(right));
|
|
3233
|
+
schema = { properties: Object.fromEntries(entries), type: "object" };
|
|
3234
|
+
} else {
|
|
3235
|
+
schema = {
|
|
3236
|
+
type: checker.typeToString(type, undefined, ts.TypeFormatFlags.NoTruncation)
|
|
3237
|
+
};
|
|
3238
|
+
}
|
|
3239
|
+
ancestors.delete(type);
|
|
3240
|
+
return schema;
|
|
3241
|
+
};
|
|
3242
|
+
var pagePropsType = (pageExpression, propsExpression, checker) => {
|
|
3243
|
+
const pageType = checker.getTypeAtLocation(pageExpression);
|
|
3244
|
+
const [signature] = pageType.getCallSignatures();
|
|
3245
|
+
const [parameter] = signature?.parameters ?? [];
|
|
3246
|
+
const declaration = parameter?.valueDeclaration ?? parameter?.declarations?.[0];
|
|
3247
|
+
if (parameter && declaration) {
|
|
3248
|
+
return checker.getTypeOfSymbolAtLocation(parameter, declaration);
|
|
3249
|
+
}
|
|
3250
|
+
return propsExpression ? checker.getTypeAtLocation(propsExpression) : checker.getTypeAtLocation(pageExpression);
|
|
3251
|
+
};
|
|
3252
|
+
var resolvePageIdentity = (expression, sourceFile, checker, projectRoot) => {
|
|
3253
|
+
let symbol = checker.getSymbolAtLocation(expression);
|
|
3254
|
+
if (symbol?.flags && symbol.flags & ts.SymbolFlags.Alias) {
|
|
3255
|
+
symbol = checker.getAliasedSymbol(symbol);
|
|
3256
|
+
}
|
|
3257
|
+
const declaration = symbol?.declarations?.[0];
|
|
3258
|
+
const file = declaration?.getSourceFile().fileName ?? sourceFile.fileName;
|
|
3259
|
+
const exportedName = symbol?.name ?? expression.getText(sourceFile);
|
|
3260
|
+
const source = posixPath(relative7(projectRoot, file));
|
|
3261
|
+
return `${source}#${exportedName}`;
|
|
3262
|
+
};
|
|
3263
|
+
var resolveAlias = (symbol, checker) => {
|
|
3264
|
+
if (!(symbol.flags & ts.SymbolFlags.Alias))
|
|
3265
|
+
return symbol;
|
|
3266
|
+
return checker.getAliasedSymbol(symbol);
|
|
3267
|
+
};
|
|
3268
|
+
var assetKey = (expression, checker, seen = new Set) => {
|
|
3269
|
+
if (!expression)
|
|
3270
|
+
return;
|
|
3271
|
+
if (ts.isIdentifier(expression)) {
|
|
3272
|
+
const unresolved = ts.isShorthandPropertyAssignment(expression.parent) ? checker.getShorthandAssignmentValueSymbol(expression.parent) : checker.getSymbolAtLocation(expression);
|
|
3273
|
+
if (!unresolved)
|
|
3274
|
+
return;
|
|
3275
|
+
const symbol = resolveAlias(unresolved, checker);
|
|
3276
|
+
if (seen.has(symbol))
|
|
3277
|
+
return;
|
|
3278
|
+
seen.add(symbol);
|
|
3279
|
+
const declaration = symbol.valueDeclaration ?? symbol.declarations?.[0];
|
|
3280
|
+
if (!declaration || !ts.isVariableDeclaration(declaration))
|
|
3281
|
+
return;
|
|
3282
|
+
return assetKey(declaration.initializer, checker, seen);
|
|
3283
|
+
}
|
|
3284
|
+
if (!ts.isCallExpression(expression))
|
|
3285
|
+
return;
|
|
3286
|
+
if (!ts.isIdentifier(expression.expression) || expression.expression.text !== "asset") {
|
|
3287
|
+
return;
|
|
3288
|
+
}
|
|
3289
|
+
const [, key] = expression.arguments;
|
|
3290
|
+
return key && ts.isStringLiteralLike(key) ? key.text : undefined;
|
|
3291
|
+
};
|
|
3292
|
+
var findPageCall = (nodes) => {
|
|
3293
|
+
let found;
|
|
3294
|
+
const visit = (candidate) => {
|
|
3295
|
+
if (found)
|
|
3296
|
+
return;
|
|
3297
|
+
if (ts.isCallExpression(candidate) && ts.isIdentifier(candidate.expression) && candidate.expression.text === PAGE_HANDLER) {
|
|
3298
|
+
found = candidate;
|
|
3299
|
+
return;
|
|
3300
|
+
}
|
|
3301
|
+
ts.forEachChild(candidate, visit);
|
|
3302
|
+
};
|
|
3303
|
+
for (const node of nodes)
|
|
3304
|
+
visit(node);
|
|
3305
|
+
return found;
|
|
3306
|
+
};
|
|
3307
|
+
var isProjectSource = (sourceFile, resolvedFile, projectRoot) => !sourceFile.isDeclarationFile && !resolvedFile.includes("/node_modules/") && resolvedFile.startsWith(`${projectRoot}/`);
|
|
3308
|
+
var analyzeRouteCall = (node, sourceFile, checker, projectRoot) => {
|
|
3309
|
+
const callee = node.expression;
|
|
3310
|
+
if (!ts.isPropertyAccessExpression(callee))
|
|
3311
|
+
return;
|
|
3312
|
+
if (!ROUTE_METHODS.has(callee.name.text))
|
|
3313
|
+
return;
|
|
3314
|
+
const [routePath] = node.arguments;
|
|
3315
|
+
if (!routePath || !ts.isStringLiteralLike(routePath))
|
|
3316
|
+
return;
|
|
3317
|
+
const pageCall = findPageCall(node.arguments.slice(1));
|
|
3318
|
+
const [input] = pageCall?.arguments ?? [];
|
|
3319
|
+
if (!pageCall || !input || !ts.isObjectLiteralExpression(input)) {
|
|
3320
|
+
return;
|
|
3321
|
+
}
|
|
3322
|
+
const page = objectPropertyExpression(input, "Page");
|
|
3323
|
+
if (!page)
|
|
3324
|
+
return;
|
|
3325
|
+
const props = objectPropertyExpression(input, "props");
|
|
3326
|
+
const index = objectPropertyExpression(input, "index");
|
|
3327
|
+
const bundleKey = assetKey(index, checker);
|
|
3328
|
+
if (!bundleKey)
|
|
3329
|
+
return;
|
|
3330
|
+
const pageId = resolvePageIdentity(page, sourceFile, checker, projectRoot);
|
|
3331
|
+
const schema = serializeType(pagePropsType(page, props, checker), checker);
|
|
3332
|
+
const propsSchemaHash = hashAbsoluteMobilePropsSchema(schema);
|
|
3333
|
+
const metadata = {
|
|
3334
|
+
bundleKey,
|
|
3335
|
+
contract: `react:${pageId}:${propsSchemaHash}`,
|
|
3336
|
+
framework: "react",
|
|
3337
|
+
pageId,
|
|
3338
|
+
propsSchemaHash
|
|
3339
|
+
};
|
|
3340
|
+
const result = {
|
|
3341
|
+
metadata,
|
|
3342
|
+
pageCallStart: pageCall.getStart(sourceFile),
|
|
3343
|
+
routeCallSpan: `${node.getStart(sourceFile)}:${node.end}`
|
|
3344
|
+
};
|
|
3345
|
+
return result;
|
|
3346
|
+
};
|
|
3347
|
+
var analyzeSourceFile = (sourceFile, checker, projectRoot) => {
|
|
3348
|
+
const analysis = {
|
|
3349
|
+
byPageCall: new Map,
|
|
3350
|
+
byRouteCall: new Map
|
|
3351
|
+
};
|
|
3352
|
+
const visit = (node) => {
|
|
3353
|
+
const result = ts.isCallExpression(node) ? analyzeRouteCall(node, sourceFile, checker, projectRoot) : undefined;
|
|
3354
|
+
if (result) {
|
|
3355
|
+
analysis.byPageCall.set(result.pageCallStart, result);
|
|
3356
|
+
analysis.byRouteCall.set(result.routeCallSpan, result);
|
|
3357
|
+
}
|
|
3358
|
+
ts.forEachChild(node, visit);
|
|
3359
|
+
};
|
|
3360
|
+
ts.forEachChild(sourceFile, visit);
|
|
3361
|
+
return analysis;
|
|
3362
|
+
};
|
|
3363
|
+
var analyzeProgram = (program, projectRoot) => {
|
|
3364
|
+
const checker = program.getTypeChecker();
|
|
3365
|
+
const analyzed = new Map;
|
|
3366
|
+
for (const sourceFile of program.getSourceFiles()) {
|
|
3367
|
+
const resolvedFile = resolve11(sourceFile.fileName);
|
|
3368
|
+
if (!isProjectSource(sourceFile, resolvedFile, projectRoot))
|
|
3369
|
+
continue;
|
|
3370
|
+
const analysis = analyzeSourceFile(sourceFile, checker, projectRoot);
|
|
3371
|
+
if (analysis.byPageCall.size > 0)
|
|
3372
|
+
analyzed.set(resolvedFile, analysis);
|
|
3373
|
+
}
|
|
3374
|
+
return analyzed;
|
|
3375
|
+
};
|
|
3376
|
+
var metadataExpression = (metadata) => ts.factory.createObjectLiteralExpression(Object.entries(metadata).map(([key, item]) => ts.factory.createPropertyAssignment(ts.factory.createStringLiteral(key), ts.factory.createStringLiteral(item))), false);
|
|
3377
|
+
var routeOptions = (existing, metadata) => {
|
|
3378
|
+
const detail = ts.factory.createObjectLiteralExpression([
|
|
3379
|
+
ts.factory.createPropertyAssignment(ts.factory.createStringLiteral(ABSOLUTE_MOBILE_ROUTE_DETAIL), metadataExpression(metadata))
|
|
3380
|
+
]);
|
|
3381
|
+
if (!existing) {
|
|
3382
|
+
return ts.factory.createObjectLiteralExpression([
|
|
3383
|
+
ts.factory.createPropertyAssignment("detail", detail)
|
|
3384
|
+
]);
|
|
3385
|
+
}
|
|
3386
|
+
return ts.factory.createObjectLiteralExpression([
|
|
3387
|
+
ts.factory.createSpreadAssignment(existing),
|
|
3388
|
+
ts.factory.createPropertyAssignment("detail", ts.factory.createObjectLiteralExpression([
|
|
3389
|
+
ts.factory.createSpreadAssignment(ts.factory.createPropertyAccessExpression(existing, "detail")),
|
|
3390
|
+
ts.factory.createPropertyAssignment(ts.factory.createStringLiteral(ABSOLUTE_MOBILE_ROUTE_DETAIL), metadataExpression(metadata))
|
|
3391
|
+
]))
|
|
3392
|
+
]);
|
|
3393
|
+
};
|
|
3394
|
+
var transformPageCall = (node, page) => {
|
|
3395
|
+
if (!page)
|
|
3396
|
+
return;
|
|
3397
|
+
const [input] = node.arguments;
|
|
3398
|
+
if (!input || !ts.isObjectLiteralExpression(input))
|
|
3399
|
+
return;
|
|
3400
|
+
return ts.factory.updateCallExpression(node, node.expression, node.typeArguments, [
|
|
3401
|
+
ts.factory.updateObjectLiteralExpression(input, [
|
|
3402
|
+
...input.properties,
|
|
3403
|
+
ts.factory.createPropertyAssignment("__absoluteMobile", metadataExpression(page.metadata))
|
|
3404
|
+
]),
|
|
3405
|
+
...node.arguments.slice(1)
|
|
3406
|
+
]);
|
|
3407
|
+
};
|
|
3408
|
+
var transformRouteCall = (node, route) => {
|
|
3409
|
+
if (!route)
|
|
3410
|
+
return;
|
|
3411
|
+
const [path, maybeOptions, maybeHandler, ...rest] = node.arguments;
|
|
3412
|
+
if (!path || !maybeOptions)
|
|
3413
|
+
return;
|
|
3414
|
+
const options = maybeHandler ? maybeOptions : undefined;
|
|
3415
|
+
const handler = maybeHandler ?? maybeOptions;
|
|
3416
|
+
return ts.factory.updateCallExpression(node, node.expression, node.typeArguments, [path, routeOptions(options, route.metadata), handler, ...rest]);
|
|
3417
|
+
};
|
|
3418
|
+
var transformFile = (source, fileName, analysis) => {
|
|
3419
|
+
const sourceFile = ts.createSourceFile(fileName, source, ts.ScriptTarget.Latest, true, fileName.endsWith("x") ? ts.ScriptKind.TSX : ts.ScriptKind.TS);
|
|
3420
|
+
const transformer = (context) => {
|
|
3421
|
+
const visit = (node) => {
|
|
3422
|
+
if (!ts.isCallExpression(node)) {
|
|
3423
|
+
return ts.visitEachChild(node, visit, context);
|
|
3424
|
+
}
|
|
3425
|
+
const transformedChildren = ts.visitEachChild(node, visit, context);
|
|
3426
|
+
const page = analysis.byPageCall.get(node.getStart(sourceFile));
|
|
3427
|
+
const transformedPage = transformPageCall(transformedChildren, page);
|
|
3428
|
+
if (transformedPage)
|
|
3429
|
+
return transformedPage;
|
|
3430
|
+
const route = analysis.byRouteCall.get(`${node.getStart(sourceFile)}:${node.end}`);
|
|
3431
|
+
const transformedRoute = transformRouteCall(transformedChildren, route);
|
|
3432
|
+
if (transformedRoute)
|
|
3433
|
+
return transformedRoute;
|
|
3434
|
+
return transformedChildren;
|
|
3435
|
+
};
|
|
3436
|
+
return (node) => ts.visitNode(node, visit, ts.isSourceFile) ?? node;
|
|
3437
|
+
};
|
|
3438
|
+
const result = ts.transform(sourceFile, [transformer]);
|
|
3439
|
+
try {
|
|
3440
|
+
const [transformed] = result.transformed;
|
|
3441
|
+
if (!transformed)
|
|
3442
|
+
throw new TypeError("Mobile route transform failed.");
|
|
3443
|
+
return ts.createPrinter().printFile(transformed);
|
|
3444
|
+
} finally {
|
|
3445
|
+
result.dispose();
|
|
3446
|
+
}
|
|
3447
|
+
};
|
|
3448
|
+
var ABSOLUTE_MOBILE_TRANSFORM_PROTOCOL = ABSOLUTE_MOBILE_PAGE_PROTOCOL_VERSION;
|
|
3449
|
+
var createAbsoluteMobileRouteMetadataPlugin = (options) => {
|
|
3450
|
+
const projectRoot = resolve11(options.projectRoot ?? process.cwd());
|
|
3451
|
+
const entry = resolve11(options.entry);
|
|
3452
|
+
const analyzed = analyzeProgram(createProgram(entry, projectRoot), projectRoot);
|
|
3453
|
+
return {
|
|
3454
|
+
name: "absolute-mobile-route-metadata",
|
|
3455
|
+
setup(build) {
|
|
3456
|
+
build.onLoad({ filter: SOURCE_FILTER }, async ({ path }) => {
|
|
3457
|
+
const analysis = analyzed.get(resolve11(path));
|
|
3458
|
+
if (!analysis)
|
|
3459
|
+
return;
|
|
3460
|
+
const source = await Bun.file(path).text();
|
|
3461
|
+
return {
|
|
3462
|
+
contents: transformFile(source, path, analysis),
|
|
3463
|
+
loader: extname2(path).endsWith("x") ? "tsx" : "ts"
|
|
3464
|
+
};
|
|
3465
|
+
});
|
|
3466
|
+
}
|
|
3467
|
+
};
|
|
3468
|
+
};
|
|
3469
|
+
var inspectAbsoluteMobileRouteMetadata = (options) => {
|
|
3470
|
+
const projectRoot = resolve11(options.projectRoot ?? process.cwd());
|
|
3471
|
+
const entry = resolve11(options.entry);
|
|
3472
|
+
const analyzed = analyzeProgram(createProgram(entry, projectRoot), projectRoot);
|
|
3473
|
+
return [...analyzed.entries()].flatMap(([file, analysis]) => [...analysis.byRouteCall.values()].map(({ metadata }) => ({
|
|
3474
|
+
file: posixPath(relative7(projectRoot, file)),
|
|
3475
|
+
metadata
|
|
3476
|
+
})));
|
|
3477
|
+
};
|
|
1107
3478
|
export {
|
|
3479
|
+
writeAbsoluteCapacitorConfig,
|
|
1108
3480
|
verifyAbsoluteMobileCompatibilityProducer,
|
|
3481
|
+
verifyAbsoluteMobileAssociationFiles,
|
|
1109
3482
|
runWithAbsoluteMobileProducer,
|
|
1110
3483
|
retainAbsoluteMobileCompatibilityArtifacts,
|
|
3484
|
+
resolveAbsoluteMobileRoute,
|
|
3485
|
+
resolveAbsoluteMobileDeepLink,
|
|
1111
3486
|
resolveAbsoluteMobileCompatibilityRelease,
|
|
3487
|
+
readAbsoluteMobileMaterializedReleases,
|
|
3488
|
+
publishAbsoluteIosRelease,
|
|
3489
|
+
publishAbsoluteAndroidRelease,
|
|
3490
|
+
prepareAbsoluteIosRelease,
|
|
3491
|
+
prepareAbsoluteAndroidRelease,
|
|
1112
3492
|
parseAbsoluteMobilePageRequest,
|
|
1113
3493
|
parseAbsoluteMobilePageEnvelope,
|
|
1114
3494
|
parseAbsoluteMobileCompatibilityArtifact,
|
|
3495
|
+
parseAbsoluteMobileBuildPageMetadata,
|
|
3496
|
+
normalizeAbsoluteMobileConfig,
|
|
3497
|
+
navigateAbsoluteMobilePage,
|
|
1115
3498
|
materializeAbsoluteMobileCompatibilityBundle,
|
|
3499
|
+
materializeAbsoluteMobileAssociationFiles,
|
|
3500
|
+
materializeAbsoluteCapacitorWebBundle,
|
|
3501
|
+
matchesAbsoluteMobileRoutePattern,
|
|
3502
|
+
loadAbsoluteNativeReleasePublisher,
|
|
1116
3503
|
loadAbsoluteMobileMaterializedBundle,
|
|
3504
|
+
inspectAbsoluteMobileRouteMetadata,
|
|
1117
3505
|
hashAbsoluteMobilePropsSchema,
|
|
1118
3506
|
getCurrentAbsoluteMobileProducerContext,
|
|
3507
|
+
fingerprintAbsoluteIosNativeProject,
|
|
1119
3508
|
finalizeAbsoluteMobilePage,
|
|
3509
|
+
finalizeAbsoluteMobileCompatibilityBuild,
|
|
3510
|
+
fetchAbsoluteMobilePage,
|
|
1120
3511
|
createAbsoluteMobileUpgradeResponse,
|
|
3512
|
+
createAbsoluteMobileRouteMetadataPlugin,
|
|
3513
|
+
createAbsoluteMobilePageRequest,
|
|
1121
3514
|
createAbsoluteMobilePageErrorResponse,
|
|
1122
3515
|
createAbsoluteMobileInvalidRequestResponse,
|
|
1123
3516
|
createAbsoluteMobileFileArtifactStore,
|
|
1124
3517
|
createAbsoluteMobileCompatibilityDispatcher,
|
|
1125
3518
|
createAbsoluteMobileCompatibilityArtifact,
|
|
1126
3519
|
createAbsoluteMobileBlobArtifactStore,
|
|
3520
|
+
createAbsoluteMobileAssociationPlugin,
|
|
3521
|
+
createAbsoluteMobileAssociationDocuments,
|
|
1127
3522
|
carryForwardAbsoluteMobileCompatibilityReleases,
|
|
3523
|
+
captureAbsoluteMobileRouteGraph,
|
|
3524
|
+
buildAbsoluteMobileCompatibilityRelease,
|
|
3525
|
+
buildAbsoluteIosRelease,
|
|
3526
|
+
buildAbsoluteAndroidRelease,
|
|
3527
|
+
applyAbsoluteNativeDeepLinks,
|
|
1128
3528
|
activateAbsoluteMobilePage,
|
|
1129
3529
|
acceptsAbsoluteMobilePage,
|
|
1130
3530
|
MOBILE_PAGE_REQUEST_HEADERS,
|
|
1131
3531
|
AbsoluteMobilePageProtocolError,
|
|
3532
|
+
APPLE_ASSOCIATION_PATH,
|
|
3533
|
+
ANDROID_ASSOCIATION_PATH,
|
|
3534
|
+
ABSOLUTE_MOBILE_TRANSFORM_PROTOCOL,
|
|
3535
|
+
ABSOLUTE_MOBILE_ROUTE_DETAIL,
|
|
1132
3536
|
ABSOLUTE_MOBILE_RETAINED_GENERATIONS,
|
|
1133
3537
|
ABSOLUTE_MOBILE_PAGE_PROTOCOL_VERSION,
|
|
1134
3538
|
ABSOLUTE_MOBILE_PAGE_MEDIA_TYPE,
|
|
1135
3539
|
ABSOLUTE_MOBILE_MATERIALIZED_BUNDLE_FORMAT,
|
|
1136
|
-
ABSOLUTE_MOBILE_COMPATIBILITY_FORMAT
|
|
3540
|
+
ABSOLUTE_MOBILE_COMPATIBILITY_FORMAT,
|
|
3541
|
+
ABSOLUTE_MOBILE_CLIENT_MANIFEST_FORMAT,
|
|
3542
|
+
ABSOLUTE_IOS_RELEASE_FORMAT,
|
|
3543
|
+
ABSOLUTE_ANDROID_RELEASE_FORMAT
|
|
1137
3544
|
};
|
|
1138
3545
|
|
|
1139
|
-
//# debugId=
|
|
3546
|
+
//# debugId=8F6E00755BF98A2E64756E2164756E21
|
|
1140
3547
|
//# sourceMappingURL=index.js.map
|