@absolutejs/absolute 0.20.0-beta.6 → 0.20.0-beta.8
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/cli/index.js +35 -11
- package/dist/mobile/shellAuth.js +41 -0
- package/dist/mobile/shellBootstrap.js +581 -0
- package/dist/mobile/shellSync.js +57 -0
- package/dist/src/mobile/shellAuth.d.ts +3 -0
- package/dist/src/mobile/shellBootstrap.d.ts +6 -1
- package/dist/src/mobile/shellSync.d.ts +15 -1
- package/package.json +12 -10
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// @bun
|
|
2
2
|
var __require = import.meta.require;
|
|
3
3
|
|
|
4
|
-
// .angular-partial-tmp-
|
|
4
|
+
// .angular-partial-tmp-lF9auc/src/core/streamingSlotRegistrar.ts
|
|
5
5
|
var STREAMING_SLOT_REGISTRAR_KEY = Symbol.for("absolutejs.streamingSlotRegistrar");
|
|
6
6
|
var STREAMING_SLOT_WARNING_STORAGE_KEY = Symbol.for("absolutejs.streamingSlotWarningController");
|
|
7
7
|
var STREAMING_SLOT_COLLECTION_STORAGE_KEY = Symbol.for("absolutejs.streamingSlotCollectionController");
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// @bun
|
|
2
2
|
var __require = import.meta.require;
|
|
3
3
|
|
|
4
|
-
// .angular-partial-tmp-
|
|
4
|
+
// .angular-partial-tmp-lF9auc/src/core/streamingSlotRegistrar.ts
|
|
5
5
|
var STREAMING_SLOT_REGISTRAR_KEY = Symbol.for("absolutejs.streamingSlotRegistrar");
|
|
6
6
|
var STREAMING_SLOT_WARNING_STORAGE_KEY = Symbol.for("absolutejs.streamingSlotWarningController");
|
|
7
7
|
var STREAMING_SLOT_COLLECTION_STORAGE_KEY = Symbol.for("absolutejs.streamingSlotCollectionController");
|
|
@@ -48,7 +48,7 @@ var warnMissingStreamingSlotCollector = (primitiveName) => {
|
|
|
48
48
|
getWarningController()?.maybeWarn(primitiveName);
|
|
49
49
|
};
|
|
50
50
|
|
|
51
|
-
// .angular-partial-tmp-
|
|
51
|
+
// .angular-partial-tmp-lF9auc/src/core/streamingSlotRegistry.ts
|
|
52
52
|
var STREAMING_SLOT_STORAGE_KEY = Symbol.for("absolutejs.streamingSlotAsyncLocalStorage");
|
|
53
53
|
var isObjectRecord2 = (value) => Boolean(value) && typeof value === "object";
|
|
54
54
|
var isAsyncLocalStorage = (value) => isObjectRecord2(value) && ("getStore" in value) && typeof value.getStore === "function" && ("run" in value) && typeof value.run === "function";
|
package/dist/cli/index.js
CHANGED
|
@@ -16840,10 +16840,36 @@ var exports_mobile = {};
|
|
|
16840
16840
|
__export(exports_mobile, {
|
|
16841
16841
|
runMobile: () => runMobile
|
|
16842
16842
|
});
|
|
16843
|
-
import { access as access11, mkdir as mkdir13, writeFile as writeFile13 } from "fs/promises";
|
|
16843
|
+
import { access as access11, mkdir as mkdir13, readFile as readFile16, writeFile as writeFile13 } from "fs/promises";
|
|
16844
16844
|
import { join as join47, resolve as resolve39 } from "path";
|
|
16845
16845
|
import { createInterface } from "readline/promises";
|
|
16846
|
-
var NOT_FOUND2 = -1, isRecord15 = (value) => typeof value === "object" && value !== null && !Array.isArray(value), CAPACITOR_PACKAGES, CAPACITOR_PACKAGE_SPECS,
|
|
16846
|
+
var NOT_FOUND2 = -1, isRecord15 = (value) => typeof value === "object" && value !== null && !Array.isArray(value), CAPACITOR_PACKAGES, CAPACITOR_PACKAGE_SPECS, CAPACITOR_SYNC_PACKAGE_SPECS, packageNameFromSpec = (spec) => spec.slice(0, spec.lastIndexOf("@")), directProjectPackages = async (projectRoot) => {
|
|
16847
|
+
const manifest = JSON.parse(await readFile16(join47(projectRoot, "package.json"), "utf8"));
|
|
16848
|
+
if (!isRecord15(manifest))
|
|
16849
|
+
throw new TypeError("Application package.json must contain an object.");
|
|
16850
|
+
const names = new Set;
|
|
16851
|
+
for (const field of ["dependencies", "devDependencies"]) {
|
|
16852
|
+
const dependencies = Reflect.get(manifest, field);
|
|
16853
|
+
if (isRecord15(dependencies))
|
|
16854
|
+
for (const name of Object.keys(dependencies))
|
|
16855
|
+
names.add(name);
|
|
16856
|
+
}
|
|
16857
|
+
return names;
|
|
16858
|
+
}, ensureCapacitorPackages = async (projectRoot, args) => {
|
|
16859
|
+
const specs = [
|
|
16860
|
+
...CAPACITOR_PACKAGE_SPECS,
|
|
16861
|
+
...projectUsesAbsoluteSync(projectRoot) ? CAPACITOR_SYNC_PACKAGE_SPECS : []
|
|
16862
|
+
];
|
|
16863
|
+
const installed = await directProjectPackages(projectRoot);
|
|
16864
|
+
const missing = specs.filter((spec) => !installed.has(packageNameFromSpec(spec)));
|
|
16865
|
+
if (missing.length === 0)
|
|
16866
|
+
return;
|
|
16867
|
+
const approved = args.includes("--yes") || await confirmInstall("Capacitor and the AbsoluteJS native adapters are missing. Install the tested mobile toolchain now?");
|
|
16868
|
+
if (!approved)
|
|
16869
|
+
throw new TypeError(`Mobile initialization requires: bun add ${missing.join(" ")}`);
|
|
16870
|
+
if (!installPackages(projectRoot, missing))
|
|
16871
|
+
throw new TypeError("Failed to install the AbsoluteJS mobile toolchain.");
|
|
16872
|
+
}, valueAfter = (args, flag) => {
|
|
16847
16873
|
const index = args.indexOf(flag);
|
|
16848
16874
|
return index === NOT_FOUND2 ? undefined : args[index + 1];
|
|
16849
16875
|
}, valuesAfter = (args, flag) => args.flatMap((value, index) => {
|
|
@@ -16916,15 +16942,7 @@ var NOT_FOUND2 = -1, isRecord15 = (value) => typeof value === "object" && value
|
|
|
16916
16942
|
console.log(removed ? `Removed remote Mac profile ${args[1]}.` : `Remote Mac profile ${args[1]} was not found.`);
|
|
16917
16943
|
}, initialize = async (args) => {
|
|
16918
16944
|
const { mobile, projectRoot } = await loadMobile(valueAfter(args, "--config"));
|
|
16919
|
-
|
|
16920
|
-
await access11(join47(projectRoot, "node_modules", ".bin", "cap"));
|
|
16921
|
-
} catch {
|
|
16922
|
-
const approved = args.includes("--yes") || await confirmInstall("Capacitor and the AbsoluteJS native device adapter are missing. Install the tested mobile toolchain now?");
|
|
16923
|
-
if (!approved)
|
|
16924
|
-
throw new TypeError(`Mobile initialization requires: bun add ${CAPACITOR_PACKAGE_SPECS.join(" ")}`);
|
|
16925
|
-
if (!installPackages(projectRoot, CAPACITOR_PACKAGE_SPECS))
|
|
16926
|
-
throw new TypeError("Failed to install the AbsoluteJS mobile toolchain.");
|
|
16927
|
-
}
|
|
16945
|
+
await ensureCapacitorPackages(projectRoot, args);
|
|
16928
16946
|
const generated = await writeAbsoluteCapacitorConfig(mobile, {
|
|
16929
16947
|
force: args.includes("--force"),
|
|
16930
16948
|
projectRoot
|
|
@@ -16936,6 +16954,7 @@ var NOT_FOUND2 = -1, isRecord15 = (value) => typeof value === "object" && value
|
|
|
16936
16954
|
await applyAbsoluteNativeDeepLinks(mobile);
|
|
16937
16955
|
}, sync = async (args) => {
|
|
16938
16956
|
const { mobile, projectRoot } = await loadMobile(valueAfter(args, "--config"));
|
|
16957
|
+
await ensureCapacitorPackages(projectRoot, args);
|
|
16939
16958
|
const platform6 = args.find((value) => value === "android" || value === "ios");
|
|
16940
16959
|
const platforms = platform6 ? [platform6] : mobile.platforms;
|
|
16941
16960
|
if (platforms.includes("android"))
|
|
@@ -17876,6 +17895,7 @@ var init_mobile = __esm(() => {
|
|
|
17876
17895
|
init_utils();
|
|
17877
17896
|
init_getDurationString();
|
|
17878
17897
|
init_remoteMacProtocol();
|
|
17898
|
+
init_nativeAuth();
|
|
17879
17899
|
CAPACITOR_PACKAGES = [
|
|
17880
17900
|
"@capacitor/core",
|
|
17881
17901
|
"@capacitor/app",
|
|
@@ -17900,6 +17920,10 @@ var init_mobile = __esm(() => {
|
|
|
17900
17920
|
"@absolutejs/devices@0.0.2",
|
|
17901
17921
|
"@absolutejs/devices-capacitor@0.1.2"
|
|
17902
17922
|
];
|
|
17923
|
+
CAPACITOR_SYNC_PACKAGE_SPECS = [
|
|
17924
|
+
"@absolutejs/sync-capacitor@0.2.0",
|
|
17925
|
+
"@capacitor-community/sqlite@8.1.1"
|
|
17926
|
+
];
|
|
17903
17927
|
});
|
|
17904
17928
|
|
|
17905
17929
|
// src/cli/scripts/typecheck.ts
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// src/mobile/shellAuth.ts
|
|
2
|
+
import { lifecycle, links, secureStorage } from "@absolutejs/devices";
|
|
3
|
+
import { installCapacitorDeviceAdapter } from "@absolutejs/devices-capacitor";
|
|
4
|
+
import {
|
|
5
|
+
createMobileAuthClient,
|
|
6
|
+
createMobileAuthTransport,
|
|
7
|
+
installAuthClientRuntimeTransport
|
|
8
|
+
} from "@absolutejs/auth/client/mobile";
|
|
9
|
+
var installed = false;
|
|
10
|
+
var createAbsoluteMobileShellAuth = async (config) => {
|
|
11
|
+
if (!installed) {
|
|
12
|
+
installCapacitorDeviceAdapter({
|
|
13
|
+
storagePrefix: `absolutejs.${config.clientId}.`
|
|
14
|
+
});
|
|
15
|
+
installed = true;
|
|
16
|
+
}
|
|
17
|
+
const client = createMobileAuthClient({
|
|
18
|
+
allowedOrigins: [config.issuer],
|
|
19
|
+
clientId: config.clientId,
|
|
20
|
+
issuer: config.issuer,
|
|
21
|
+
lifecycle,
|
|
22
|
+
links,
|
|
23
|
+
redirectUri: config.redirectUri,
|
|
24
|
+
resource: config.issuer,
|
|
25
|
+
scopes: config.scopes,
|
|
26
|
+
storage: secureStorage
|
|
27
|
+
});
|
|
28
|
+
await client.start();
|
|
29
|
+
const principal = await client.principal();
|
|
30
|
+
installAuthClientRuntimeTransport(createMobileAuthTransport(client, { baseUrl: config.issuer }));
|
|
31
|
+
return {
|
|
32
|
+
fetch: client.fetchOptional,
|
|
33
|
+
onPrincipalChange: client.onPrincipalChange,
|
|
34
|
+
principal,
|
|
35
|
+
redirectUri: config.redirectUri,
|
|
36
|
+
socketTicket: client.socketTicket
|
|
37
|
+
};
|
|
38
|
+
};
|
|
39
|
+
export {
|
|
40
|
+
createAbsoluteMobileShellAuth
|
|
41
|
+
};
|
|
@@ -0,0 +1,581 @@
|
|
|
1
|
+
// src/mobile/shellBootstrap.ts
|
|
2
|
+
import { App } from "@capacitor/app";
|
|
3
|
+
|
|
4
|
+
// src/mobile/producerContextState.ts
|
|
5
|
+
var ABSOLUTE_MOBILE_PRODUCER_STORAGE_KEY = Symbol.for("absolutejs.mobileProducerAsyncLocalStorage");
|
|
6
|
+
var frameworks = new Set([
|
|
7
|
+
"angular",
|
|
8
|
+
"ember",
|
|
9
|
+
"html",
|
|
10
|
+
"htmx",
|
|
11
|
+
"react",
|
|
12
|
+
"svelte",
|
|
13
|
+
"vue"
|
|
14
|
+
]);
|
|
15
|
+
|
|
16
|
+
// src/mobile/pageProtocol.ts
|
|
17
|
+
var ABSOLUTE_MOBILE_PAGE_MEDIA_TYPE = "application/vnd.absolute.page+json";
|
|
18
|
+
var ABSOLUTE_MOBILE_PAGE_PROTOCOL_VERSION = 1;
|
|
19
|
+
var MOBILE_PAGE_REQUEST_HEADERS = {
|
|
20
|
+
appBuild: "x-absolute-mobile-app-build",
|
|
21
|
+
pageBundle: "x-absolute-mobile-page-bundle",
|
|
22
|
+
pageContracts: "x-absolute-mobile-page-contracts",
|
|
23
|
+
pageId: "x-absolute-mobile-page-id",
|
|
24
|
+
protocol: "x-absolute-mobile-protocol",
|
|
25
|
+
runtime: "x-absolute-mobile-runtime"
|
|
26
|
+
};
|
|
27
|
+
var headerVaryValues = [
|
|
28
|
+
"Accept",
|
|
29
|
+
MOBILE_PAGE_REQUEST_HEADERS.protocol,
|
|
30
|
+
MOBILE_PAGE_REQUEST_HEADERS.runtime,
|
|
31
|
+
MOBILE_PAGE_REQUEST_HEADERS.appBuild,
|
|
32
|
+
MOBILE_PAGE_REQUEST_HEADERS.pageBundle,
|
|
33
|
+
MOBILE_PAGE_REQUEST_HEADERS.pageContracts,
|
|
34
|
+
MOBILE_PAGE_REQUEST_HEADERS.pageId
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
// src/mobile/client.ts
|
|
38
|
+
class AbsoluteMobilePageProtocolError extends Error {
|
|
39
|
+
code;
|
|
40
|
+
constructor(code, message) {
|
|
41
|
+
super(message);
|
|
42
|
+
this.name = "AbsoluteMobilePageProtocolError";
|
|
43
|
+
this.code = code;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
var disposeAbsoluteMobilePage = async (target = window) => {
|
|
47
|
+
const dispose = target.__ABSOLUTE_PAGE_DISPOSE__;
|
|
48
|
+
target.__ABSOLUTE_PAGE_DISPOSE__ = undefined;
|
|
49
|
+
target.__ABSOLUTE_PAGE_READY__ = undefined;
|
|
50
|
+
if (dispose)
|
|
51
|
+
await dispose();
|
|
52
|
+
};
|
|
53
|
+
var frameworks2 = new Set([
|
|
54
|
+
"angular",
|
|
55
|
+
"ember",
|
|
56
|
+
"html",
|
|
57
|
+
"htmx",
|
|
58
|
+
"react",
|
|
59
|
+
"svelte",
|
|
60
|
+
"vue"
|
|
61
|
+
]);
|
|
62
|
+
var upgradeReasons = new Set([
|
|
63
|
+
"app-release",
|
|
64
|
+
"page-contract",
|
|
65
|
+
"protocol",
|
|
66
|
+
"runtime"
|
|
67
|
+
]);
|
|
68
|
+
var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
69
|
+
var isFramework = (value) => typeof value === "string" && frameworks2.has(value);
|
|
70
|
+
var isUpgradeReason = (value) => typeof value === "string" && upgradeReasons.has(value);
|
|
71
|
+
var parsePageResult = (value) => {
|
|
72
|
+
if (typeof value.contract !== "string" || !isFramework(value.framework) || typeof value.pageId !== "string" || typeof value.status !== "number") {
|
|
73
|
+
throw new AbsoluteMobilePageProtocolError("invalid-envelope", "The mobile page response is missing required page metadata.");
|
|
74
|
+
}
|
|
75
|
+
if (!isRecord(value.props)) {
|
|
76
|
+
throw new AbsoluteMobilePageProtocolError("invalid-props", "The mobile page response must contain an object props value.");
|
|
77
|
+
}
|
|
78
|
+
return {
|
|
79
|
+
contract: value.contract,
|
|
80
|
+
framework: value.framework,
|
|
81
|
+
kind: "page",
|
|
82
|
+
pageId: value.pageId,
|
|
83
|
+
props: value.props,
|
|
84
|
+
status: value.status
|
|
85
|
+
};
|
|
86
|
+
};
|
|
87
|
+
var parseUpgradeResult = (value) => {
|
|
88
|
+
if (typeof value.pageId !== "string" || !isUpgradeReason(value.reason)) {
|
|
89
|
+
throw new AbsoluteMobilePageProtocolError("invalid-envelope", "The update response is missing its page or reason.");
|
|
90
|
+
}
|
|
91
|
+
return {
|
|
92
|
+
kind: "upgrade-required",
|
|
93
|
+
pageId: value.pageId,
|
|
94
|
+
reason: value.reason,
|
|
95
|
+
supportedContracts: Array.isArray(value.supportedContracts) ? value.supportedContracts.filter((contract) => typeof contract === "string") : undefined,
|
|
96
|
+
supportedRuntimes: Array.isArray(value.supportedRuntimes) ? value.supportedRuntimes.filter((runtime) => typeof runtime === "string") : undefined
|
|
97
|
+
};
|
|
98
|
+
};
|
|
99
|
+
var activateAbsoluteMobilePage = async (value, options) => {
|
|
100
|
+
const envelope = parseAbsoluteMobilePageEnvelope(value);
|
|
101
|
+
if (envelope.response.kind === "upgrade-required") {
|
|
102
|
+
return envelope.response;
|
|
103
|
+
}
|
|
104
|
+
if (envelope.response.kind !== "page") {
|
|
105
|
+
throw new AbsoluteMobilePageProtocolError("invalid-envelope", "Expected a renderable mobile page response.");
|
|
106
|
+
}
|
|
107
|
+
const target = options.target ?? window;
|
|
108
|
+
await disposeAbsoluteMobilePage(target);
|
|
109
|
+
target.__INITIAL_PROPS__ = envelope.response.props;
|
|
110
|
+
target.__ABS_ANGULAR_REQUEST_CONTEXT__ = envelope.response.props;
|
|
111
|
+
target.__ABSOLUTE_PAGE_RENDER_MODE__ = "client";
|
|
112
|
+
await options.loadPage({
|
|
113
|
+
contract: envelope.response.contract,
|
|
114
|
+
pageId: envelope.response.pageId
|
|
115
|
+
});
|
|
116
|
+
if (target.__ABSOLUTE_PAGE_READY__) {
|
|
117
|
+
await target.__ABSOLUTE_PAGE_READY__;
|
|
118
|
+
}
|
|
119
|
+
return {
|
|
120
|
+
contract: envelope.response.contract,
|
|
121
|
+
kind: "rendered",
|
|
122
|
+
pageId: envelope.response.pageId
|
|
123
|
+
};
|
|
124
|
+
};
|
|
125
|
+
var parseAbsoluteMobilePageEnvelope = (value) => {
|
|
126
|
+
if (!isRecord(value) || !isRecord(value.response)) {
|
|
127
|
+
throw new AbsoluteMobilePageProtocolError("invalid-envelope", "The server did not return an AbsoluteJS mobile page envelope.");
|
|
128
|
+
}
|
|
129
|
+
if (value.protocol !== ABSOLUTE_MOBILE_PAGE_PROTOCOL_VERSION) {
|
|
130
|
+
throw new AbsoluteMobilePageProtocolError("invalid-envelope", `Unsupported mobile page protocol ${String(value.protocol)}.`);
|
|
131
|
+
}
|
|
132
|
+
const { kind } = value.response;
|
|
133
|
+
if (kind === "page") {
|
|
134
|
+
return {
|
|
135
|
+
protocol: ABSOLUTE_MOBILE_PAGE_PROTOCOL_VERSION,
|
|
136
|
+
response: parsePageResult(value.response)
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
if (kind === "upgrade-required") {
|
|
140
|
+
return {
|
|
141
|
+
protocol: ABSOLUTE_MOBILE_PAGE_PROTOCOL_VERSION,
|
|
142
|
+
response: parseUpgradeResult(value.response)
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
if (kind === "invalid-request") {
|
|
146
|
+
throw new AbsoluteMobilePageProtocolError("server-rejected-request", typeof value.response.message === "string" ? value.response.message : "The server rejected the mobile page request.");
|
|
147
|
+
}
|
|
148
|
+
if (kind === "error") {
|
|
149
|
+
throw new AbsoluteMobilePageProtocolError("server-error", "The server could not create a compatible mobile page representation.");
|
|
150
|
+
}
|
|
151
|
+
throw new AbsoluteMobilePageProtocolError("invalid-envelope", `Unknown mobile page response kind ${String(kind)}.`);
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
// src/mobile/routeMatcher.ts
|
|
155
|
+
var REGEXP_SPECIAL_CHARACTERS = /[.*+?^${}()|[\]\\]/g;
|
|
156
|
+
var routeSegmentPattern = (segment) => {
|
|
157
|
+
if (segment === "*")
|
|
158
|
+
return ".*";
|
|
159
|
+
if (segment.startsWith(":") && segment.endsWith("?"))
|
|
160
|
+
return "[^/]*";
|
|
161
|
+
if (segment.startsWith(":"))
|
|
162
|
+
return "[^/]+";
|
|
163
|
+
return segment.replace(REGEXP_SPECIAL_CHARACTERS, "\\$&");
|
|
164
|
+
};
|
|
165
|
+
var matchesAbsoluteMobileRoutePattern = (pattern, pathname) => {
|
|
166
|
+
const expression = pattern.split("/").map(routeSegmentPattern).join("/");
|
|
167
|
+
return new RegExp(`^${expression}/?$`).test(pathname);
|
|
168
|
+
};
|
|
169
|
+
var resolveAbsoluteMobileRoute = (routes, pathname, method = "GET") => routes.find((route) => route.method === method && matchesAbsoluteMobileRoutePattern(route.pattern, pathname));
|
|
170
|
+
|
|
171
|
+
// src/mobile/transport.ts
|
|
172
|
+
var pageForPathname = (manifest, pathname) => {
|
|
173
|
+
const route = resolveAbsoluteMobileRoute(manifest.routes, pathname);
|
|
174
|
+
if (!route) {
|
|
175
|
+
throw new TypeError(`No embedded mobile page owns ${pathname}.`);
|
|
176
|
+
}
|
|
177
|
+
const page = manifest.pages.find((candidate) => candidate.pageId === route.pageId);
|
|
178
|
+
if (!page) {
|
|
179
|
+
throw new TypeError(`Mobile route ${route.pattern} references missing page ${route.pageId}.`);
|
|
180
|
+
}
|
|
181
|
+
return page;
|
|
182
|
+
};
|
|
183
|
+
var canonicalBackendUrl = (manifest, path) => {
|
|
184
|
+
const origin = new URL(manifest.productionOrigin);
|
|
185
|
+
const url = new URL(path, `${origin.origin}/`);
|
|
186
|
+
if (url.origin !== origin.origin) {
|
|
187
|
+
throw new TypeError("Mobile transport cannot leave productionOrigin.");
|
|
188
|
+
}
|
|
189
|
+
url.hash = "";
|
|
190
|
+
return url;
|
|
191
|
+
};
|
|
192
|
+
var createAbsoluteMobilePageRequest = (manifest, path, options = {}) => {
|
|
193
|
+
const url = canonicalBackendUrl(manifest, path);
|
|
194
|
+
const page = pageForPathname(manifest, url.pathname);
|
|
195
|
+
const headers = new Headers(options.headers);
|
|
196
|
+
headers.set("accept", ABSOLUTE_MOBILE_PAGE_MEDIA_TYPE);
|
|
197
|
+
headers.set(MOBILE_PAGE_REQUEST_HEADERS.appBuild, manifest.appBuild);
|
|
198
|
+
headers.set(MOBILE_PAGE_REQUEST_HEADERS.pageBundle, page.bundleHash);
|
|
199
|
+
headers.set(MOBILE_PAGE_REQUEST_HEADERS.pageContracts, page.contract);
|
|
200
|
+
headers.set(MOBILE_PAGE_REQUEST_HEADERS.pageId, page.pageId);
|
|
201
|
+
headers.set(MOBILE_PAGE_REQUEST_HEADERS.protocol, String(ABSOLUTE_MOBILE_PAGE_PROTOCOL_VERSION));
|
|
202
|
+
headers.set(MOBILE_PAGE_REQUEST_HEADERS.runtime, manifest.runtime);
|
|
203
|
+
return new Request(url, { headers, method: "GET" });
|
|
204
|
+
};
|
|
205
|
+
var fetchAbsoluteMobilePage = async (manifest, path, options = {}) => {
|
|
206
|
+
const request = createAbsoluteMobilePageRequest(manifest, path, options);
|
|
207
|
+
const response = await (options.fetch ?? globalThis.fetch)(request);
|
|
208
|
+
const value = await response.json();
|
|
209
|
+
return value;
|
|
210
|
+
};
|
|
211
|
+
var resolveAbsoluteMobileDeepLink = (manifest, value) => {
|
|
212
|
+
const url = new URL(value);
|
|
213
|
+
if (url.username || url.password) {
|
|
214
|
+
throw new TypeError("Mobile deep links cannot contain credentials.");
|
|
215
|
+
}
|
|
216
|
+
const isHttpsHost = url.protocol === "https:" && manifest.deepLinkHosts.includes(url.hostname.toLowerCase());
|
|
217
|
+
const isCustomScheme = manifest.deepLinkScheme !== undefined && url.protocol === `${manifest.deepLinkScheme}:`;
|
|
218
|
+
if (!isHttpsHost && !isCustomScheme) {
|
|
219
|
+
throw new TypeError("Mobile deep link is outside the configured app URLs.");
|
|
220
|
+
}
|
|
221
|
+
return `${url.pathname || "/"}${url.search}${url.hash}`;
|
|
222
|
+
};
|
|
223
|
+
var resolveAbsoluteMobileNavigation = (manifest, value, localOrigin) => {
|
|
224
|
+
const url = new URL(value, `${localOrigin}/`);
|
|
225
|
+
const local = new URL(localOrigin);
|
|
226
|
+
const production = new URL(manifest.productionOrigin);
|
|
227
|
+
const matches = (candidate, allowed) => candidate.protocol === allowed.protocol && candidate.host === allowed.host;
|
|
228
|
+
if (!matches(url, local) && !matches(url, production)) {
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
return `${url.pathname}${url.search}${url.hash}`;
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
// src/mobile/staticDocument.ts
|
|
235
|
+
var HTMX_REQUEST_ATTRIBUTE_PATTERN = /(\s(?:action|formaction|hx-(?:delete|get|patch|post|put))\s*=\s*["'])(\/[^"']*)(["'])/giu;
|
|
236
|
+
var BLOCKED_FRAGMENT_ELEMENTS = 'script,base,iframe,object,embed,link[rel="modulepreload"],meta[http-equiv="refresh"]';
|
|
237
|
+
var HTMX_REQUEST_ATTRIBUTES = new Set([
|
|
238
|
+
"hx-delete",
|
|
239
|
+
"hx-get",
|
|
240
|
+
"hx-patch",
|
|
241
|
+
"hx-post",
|
|
242
|
+
"hx-put"
|
|
243
|
+
]);
|
|
244
|
+
var URL_ATTRIBUTES = new Set(["action", "formaction", "href", "src"]);
|
|
245
|
+
var HEX_RADIX = 16;
|
|
246
|
+
var SCRIPT_PLACEHOLDER_ATTRIBUTE = "data-absolute-mobile-script";
|
|
247
|
+
var bytesToHex = (bytes) => [...bytes].map((byte) => byte.toString(HEX_RADIX).padStart(2, "0")).join("");
|
|
248
|
+
var hashAbsoluteMobileStaticDocument = async (source) => bytesToHex(new Uint8Array(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(source))));
|
|
249
|
+
var rewriteAbsoluteMobileHtmxRequests = (source, productionOrigin) => source.replace(HTMX_REQUEST_ATTRIBUTE_PATTERN, (_match, prefix, path, quote) => `${prefix}${new URL(path, productionOrigin).href}${quote}`);
|
|
250
|
+
var backendUrl = (value, productionOrigin) => {
|
|
251
|
+
try {
|
|
252
|
+
const expected = new URL(productionOrigin);
|
|
253
|
+
const candidate = new URL(value, expected);
|
|
254
|
+
return candidate.origin === expected.origin ? candidate.href : undefined;
|
|
255
|
+
} catch {
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
};
|
|
259
|
+
var sanitizeFragmentAttribute = (element, attribute, productionOrigin) => {
|
|
260
|
+
const name = attribute.name.toLowerCase();
|
|
261
|
+
const isExecutable = name === "srcdoc" || name.startsWith("on") || name.startsWith("hx-on");
|
|
262
|
+
const isUnsafeUrl = (HTMX_REQUEST_ATTRIBUTES.has(name) || URL_ATTRIBUTES.has(name)) && !backendUrl(attribute.value, productionOrigin);
|
|
263
|
+
if (isExecutable || isUnsafeUrl)
|
|
264
|
+
element.removeAttribute(attribute.name);
|
|
265
|
+
else if (HTMX_REQUEST_ATTRIBUTES.has(name) || URL_ATTRIBUTES.has(name)) {
|
|
266
|
+
element.setAttribute(attribute.name, backendUrl(attribute.value, productionOrigin) ?? attribute.value);
|
|
267
|
+
}
|
|
268
|
+
};
|
|
269
|
+
var sanitizeAbsoluteMobileHtmxFragment = (source, productionOrigin, parse = (value) => new DOMParser().parseFromString(value, "text/html")) => {
|
|
270
|
+
const parsed = parse(source);
|
|
271
|
+
parsed.querySelectorAll(BLOCKED_FRAGMENT_ELEMENTS).forEach((element) => {
|
|
272
|
+
element.remove();
|
|
273
|
+
});
|
|
274
|
+
parsed.querySelectorAll("*").forEach((element) => {
|
|
275
|
+
for (const attribute of [...element.attributes]) {
|
|
276
|
+
sanitizeFragmentAttribute(element, attribute, productionOrigin);
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
return parsed.body.innerHTML;
|
|
280
|
+
};
|
|
281
|
+
var installHtmxFragmentBoundary = (productionOrigin) => {
|
|
282
|
+
const validateUrl = (event) => {
|
|
283
|
+
const detail = Reflect.get(event, "detail");
|
|
284
|
+
if (typeof detail !== "object" || detail === null) {
|
|
285
|
+
event.preventDefault();
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
const url = Reflect.get(detail, "url");
|
|
289
|
+
if (!(url instanceof URL) || !backendUrl(url.href, productionOrigin)) {
|
|
290
|
+
event.preventDefault();
|
|
291
|
+
}
|
|
292
|
+
};
|
|
293
|
+
const configureRequest = (event) => {
|
|
294
|
+
const detail = Reflect.get(event, "detail");
|
|
295
|
+
if (typeof detail !== "object" || detail === null)
|
|
296
|
+
return;
|
|
297
|
+
const path = Reflect.get(detail, "path");
|
|
298
|
+
if (typeof path !== "string")
|
|
299
|
+
return;
|
|
300
|
+
const resolved = backendUrl(path, productionOrigin);
|
|
301
|
+
if (resolved)
|
|
302
|
+
Reflect.set(detail, "path", resolved);
|
|
303
|
+
};
|
|
304
|
+
const sanitize = (event) => {
|
|
305
|
+
const detail = Reflect.get(event, "detail");
|
|
306
|
+
if (typeof detail !== "object" || detail === null)
|
|
307
|
+
return;
|
|
308
|
+
const serverResponse = Reflect.get(detail, "serverResponse");
|
|
309
|
+
if (typeof serverResponse !== "string")
|
|
310
|
+
return;
|
|
311
|
+
Reflect.set(detail, "serverResponse", sanitizeAbsoluteMobileHtmxFragment(serverResponse, productionOrigin));
|
|
312
|
+
};
|
|
313
|
+
addEventListener("htmx:validateUrl", validateUrl);
|
|
314
|
+
addEventListener("htmx:configRequest", configureRequest);
|
|
315
|
+
addEventListener("htmx:beforeSwap", sanitize);
|
|
316
|
+
return () => {
|
|
317
|
+
removeEventListener("htmx:validateUrl", validateUrl);
|
|
318
|
+
removeEventListener("htmx:configRequest", configureRequest);
|
|
319
|
+
removeEventListener("htmx:beforeSwap", sanitize);
|
|
320
|
+
};
|
|
321
|
+
};
|
|
322
|
+
var isRecord2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
323
|
+
var replaceAttributes = (target, source) => {
|
|
324
|
+
for (const attribute of [...target.attributes]) {
|
|
325
|
+
target.removeAttribute(attribute.name);
|
|
326
|
+
}
|
|
327
|
+
for (const attribute of [...source.attributes]) {
|
|
328
|
+
target.setAttribute(attribute.name, attribute.value);
|
|
329
|
+
}
|
|
330
|
+
};
|
|
331
|
+
var applyScriptAttribute = (element, name, value, localUrl) => {
|
|
332
|
+
if (name === SCRIPT_PLACEHOLDER_ATTRIBUTE)
|
|
333
|
+
return;
|
|
334
|
+
if (name === "src") {
|
|
335
|
+
const source = new URL(value, localUrl);
|
|
336
|
+
const navigation = new URL(localUrl).searchParams.get("absoluteNavigation");
|
|
337
|
+
if (navigation)
|
|
338
|
+
source.searchParams.set("absoluteNavigation", navigation);
|
|
339
|
+
element.src = source.href;
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
element.setAttribute(name, value);
|
|
343
|
+
};
|
|
344
|
+
var executableScript = (script, localUrl) => {
|
|
345
|
+
const element = document.createElement("script");
|
|
346
|
+
for (const [name, value] of script.attributes) {
|
|
347
|
+
applyScriptAttribute(element, name, value, localUrl);
|
|
348
|
+
}
|
|
349
|
+
element.textContent = script.content;
|
|
350
|
+
return element;
|
|
351
|
+
};
|
|
352
|
+
var executeTrustedDocumentScript = async (placeholder, script, localUrl) => {
|
|
353
|
+
const executable = executableScript(script, localUrl);
|
|
354
|
+
if (executable.type === "module" && executable.src !== "") {
|
|
355
|
+
placeholder.remove();
|
|
356
|
+
await import(executable.src);
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
const waitsForLoad = executable.src !== "" || executable.type === "module";
|
|
360
|
+
const loaded = waitsForLoad ? new Promise((resolve, reject) => {
|
|
361
|
+
executable.addEventListener("load", () => resolve(), {
|
|
362
|
+
once: true
|
|
363
|
+
});
|
|
364
|
+
executable.addEventListener("error", () => reject(new TypeError(`Embedded document script failed: ${executable.src || "inline module"}.`)), { once: true });
|
|
365
|
+
}) : Promise.resolve();
|
|
366
|
+
placeholder.replaceWith(executable);
|
|
367
|
+
await loaded;
|
|
368
|
+
};
|
|
369
|
+
var configureHtmxDocument = (parsed) => {
|
|
370
|
+
let meta = parsed.head.querySelector('meta[name="htmx-config"]');
|
|
371
|
+
if (!meta) {
|
|
372
|
+
meta = parsed.createElement("meta");
|
|
373
|
+
meta.name = "htmx-config";
|
|
374
|
+
parsed.head.appendChild(meta);
|
|
375
|
+
}
|
|
376
|
+
let authorConfig = {};
|
|
377
|
+
try {
|
|
378
|
+
const parsedConfig = JSON.parse(meta.content || "{}");
|
|
379
|
+
if (isRecord2(parsedConfig))
|
|
380
|
+
authorConfig = parsedConfig;
|
|
381
|
+
} catch {}
|
|
382
|
+
meta.content = JSON.stringify({
|
|
383
|
+
...authorConfig,
|
|
384
|
+
selfRequestsOnly: false
|
|
385
|
+
});
|
|
386
|
+
};
|
|
387
|
+
var executeTrustedDocumentScripts = async (scripts, localUrl, index = 0) => {
|
|
388
|
+
const script = scripts[index];
|
|
389
|
+
if (!script)
|
|
390
|
+
return;
|
|
391
|
+
const placeholder = document.querySelector(`script[${SCRIPT_PLACEHOLDER_ATTRIBUTE}="${index}"]`);
|
|
392
|
+
if (placeholder) {
|
|
393
|
+
await executeTrustedDocumentScript(placeholder, script, localUrl);
|
|
394
|
+
}
|
|
395
|
+
await executeTrustedDocumentScripts(scripts, localUrl, index + 1);
|
|
396
|
+
};
|
|
397
|
+
var installTrustedDocument = async (source, localUrl, options) => {
|
|
398
|
+
const parsed = new DOMParser().parseFromString(source, "text/html");
|
|
399
|
+
if (options.htmx)
|
|
400
|
+
configureHtmxDocument(parsed);
|
|
401
|
+
const scripts = [...parsed.querySelectorAll("script")].map((script, index) => {
|
|
402
|
+
const definition = {
|
|
403
|
+
attributes: [...script.attributes].map((attribute) => [
|
|
404
|
+
attribute.name,
|
|
405
|
+
attribute.value
|
|
406
|
+
]),
|
|
407
|
+
content: script.textContent ?? ""
|
|
408
|
+
};
|
|
409
|
+
script.removeAttribute("src");
|
|
410
|
+
script.type = "application/x-absolute-mobile-script";
|
|
411
|
+
script.setAttribute(SCRIPT_PLACEHOLDER_ATTRIBUTE, String(index));
|
|
412
|
+
return definition;
|
|
413
|
+
});
|
|
414
|
+
replaceAttributes(document.documentElement, parsed.documentElement);
|
|
415
|
+
document.head.replaceChildren(...[...parsed.head.childNodes].map((node) => document.importNode(node, true)));
|
|
416
|
+
document.body.replaceWith(document.importNode(parsed.body, true));
|
|
417
|
+
await executeTrustedDocumentScripts(scripts, localUrl);
|
|
418
|
+
};
|
|
419
|
+
var installAbsoluteMobileStaticDocument = async (manifest, page, localUrl) => {
|
|
420
|
+
const response = await fetch(localUrl, { cache: "no-store" });
|
|
421
|
+
if (!response.ok) {
|
|
422
|
+
throw new TypeError(`Embedded ${page.framework} document failed with HTTP ${response.status}.`);
|
|
423
|
+
}
|
|
424
|
+
const source = await response.text();
|
|
425
|
+
const actualHash = await hashAbsoluteMobileStaticDocument(source);
|
|
426
|
+
if (actualHash !== page.bundleHash) {
|
|
427
|
+
throw new TypeError(`Embedded ${page.framework} document failed integrity.`);
|
|
428
|
+
}
|
|
429
|
+
const disposeHtmxBoundary = page.framework === "htmx" ? installHtmxFragmentBoundary(manifest.productionOrigin) : undefined;
|
|
430
|
+
const ready = installTrustedDocument(page.framework === "htmx" ? rewriteAbsoluteMobileHtmxRequests(source, manifest.productionOrigin) : source, localUrl, { htmx: page.framework === "htmx" });
|
|
431
|
+
Reflect.set(window, "__ABSOLUTE_PAGE_READY__", ready);
|
|
432
|
+
Reflect.set(window, "__ABSOLUTE_PAGE_DISPOSE__", () => disposeHtmxBoundary?.());
|
|
433
|
+
await ready;
|
|
434
|
+
};
|
|
435
|
+
|
|
436
|
+
// src/mobile/shellBootstrap.ts
|
|
437
|
+
var MANIFEST_PATH = "./absolute-mobile-manifest.json";
|
|
438
|
+
var STATUS_ID = "absolute-mobile-status";
|
|
439
|
+
var navigationGeneration = 0;
|
|
440
|
+
var readManifest = async () => {
|
|
441
|
+
const response = await fetch(MANIFEST_PATH, { cache: "no-store" });
|
|
442
|
+
if (!response.ok) {
|
|
443
|
+
throw new TypeError(`Mobile manifest failed with HTTP ${response.status}.`);
|
|
444
|
+
}
|
|
445
|
+
const manifest = await response.json();
|
|
446
|
+
return manifest;
|
|
447
|
+
};
|
|
448
|
+
var renderStatus = (message) => {
|
|
449
|
+
document.title = "AbsoluteJS";
|
|
450
|
+
document.head.innerHTML = '<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover">';
|
|
451
|
+
document.body.innerHTML = `<main id="${STATUS_ID}" role="status"></main>`;
|
|
452
|
+
const status = document.getElementById(STATUS_ID);
|
|
453
|
+
if (status)
|
|
454
|
+
status.textContent = message;
|
|
455
|
+
};
|
|
456
|
+
var renderPageTarget = () => {
|
|
457
|
+
document.title = "AbsoluteJS";
|
|
458
|
+
document.head.innerHTML = '<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover">';
|
|
459
|
+
document.body.innerHTML = '<div id="root"></div>';
|
|
460
|
+
};
|
|
461
|
+
var localBundleUrl = (manifest, pageId, contract) => {
|
|
462
|
+
const page = manifest.pages.find((candidate) => candidate.pageId === pageId && candidate.contract === contract);
|
|
463
|
+
if (!page) {
|
|
464
|
+
throw new TypeError(`The embedded app does not contain ${pageId} contract ${contract}.`);
|
|
465
|
+
}
|
|
466
|
+
const url = new URL(page.localBundlePath, document.baseURI);
|
|
467
|
+
url.searchParams.set("absoluteNavigation", String(navigationGeneration += 1));
|
|
468
|
+
return url.href;
|
|
469
|
+
};
|
|
470
|
+
var installLocalPageStyle = (manifest, pageId, contract) => {
|
|
471
|
+
const page = manifest.pages.find((candidate) => candidate.pageId === pageId && candidate.contract === contract);
|
|
472
|
+
if (!page?.localStylePath)
|
|
473
|
+
return;
|
|
474
|
+
const link = document.createElement("link");
|
|
475
|
+
link.rel = "stylesheet";
|
|
476
|
+
link.href = new URL(page.localStylePath, document.baseURI).href;
|
|
477
|
+
link.dataset.absoluteMobilePageStyle = page.bundleHash;
|
|
478
|
+
document.head.appendChild(link);
|
|
479
|
+
};
|
|
480
|
+
var navigate = async (manifest, path, pushHistory, fetchImpl) => {
|
|
481
|
+
await disposeAbsoluteMobilePage();
|
|
482
|
+
renderStatus("Loading…");
|
|
483
|
+
const envelope = await fetchAbsoluteMobilePage(manifest, path, {
|
|
484
|
+
...fetchImpl ? { fetch: fetchImpl } : {}
|
|
485
|
+
});
|
|
486
|
+
const activation = await activateAbsoluteMobilePage(envelope, {
|
|
487
|
+
loadPage: ({ contract, pageId }) => {
|
|
488
|
+
const page = manifest.pages.find((candidate) => candidate.pageId === pageId && candidate.contract === contract);
|
|
489
|
+
if (!page) {
|
|
490
|
+
throw new TypeError(`The embedded app does not contain ${pageId} contract ${contract}.`);
|
|
491
|
+
}
|
|
492
|
+
if (page.framework === "html" || page.framework === "htmx") {
|
|
493
|
+
return installAbsoluteMobileStaticDocument(manifest, page, localBundleUrl(manifest, pageId, contract));
|
|
494
|
+
}
|
|
495
|
+
renderPageTarget();
|
|
496
|
+
installLocalPageStyle(manifest, pageId, contract);
|
|
497
|
+
return import(localBundleUrl(manifest, pageId, contract));
|
|
498
|
+
}
|
|
499
|
+
});
|
|
500
|
+
if (activation.kind === "upgrade-required") {
|
|
501
|
+
renderStatus("This app version must be updated to continue.");
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
if (pushHistory)
|
|
505
|
+
history.pushState({ absoluteMobile: true }, "", path);
|
|
506
|
+
};
|
|
507
|
+
var installAnchorNavigation = (manifest, onNavigate) => {
|
|
508
|
+
const handleClick = (event) => {
|
|
509
|
+
if (!(event.target instanceof Element))
|
|
510
|
+
return;
|
|
511
|
+
const anchor = event.target.closest("a[href]");
|
|
512
|
+
if (!(anchor instanceof HTMLAnchorElement) || anchor.target)
|
|
513
|
+
return;
|
|
514
|
+
try {
|
|
515
|
+
const path = resolveAbsoluteMobileNavigation(manifest, anchor.href, location.origin);
|
|
516
|
+
if (!path)
|
|
517
|
+
return;
|
|
518
|
+
event.preventDefault();
|
|
519
|
+
onNavigate(path);
|
|
520
|
+
} catch {}
|
|
521
|
+
};
|
|
522
|
+
addEventListener("click", handleClick);
|
|
523
|
+
return () => removeEventListener("click", handleClick);
|
|
524
|
+
};
|
|
525
|
+
var navigateDeepLink = (manifest, onNavigate, url, authRedirectUri) => {
|
|
526
|
+
if (!url)
|
|
527
|
+
return;
|
|
528
|
+
if (authRedirectUri) {
|
|
529
|
+
const actual = new URL(url);
|
|
530
|
+
const redirect = new URL(authRedirectUri);
|
|
531
|
+
if (actual.protocol === redirect.protocol && actual.host === redirect.host && actual.pathname === redirect.pathname)
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
onNavigate(resolveAbsoluteMobileDeepLink(manifest, url));
|
|
535
|
+
};
|
|
536
|
+
var deepLinkListener = (manifest, onNavigate, authRedirectUri) => ({ url }) => navigateDeepLink(manifest, onNavigate, url, authRedirectUri);
|
|
537
|
+
var installDeepLinks = async (manifest, onNavigate, authRedirectUri) => {
|
|
538
|
+
let listener;
|
|
539
|
+
try {
|
|
540
|
+
listener = await App.addListener("appUrlOpen", deepLinkListener(manifest, onNavigate, authRedirectUri));
|
|
541
|
+
const launch = await App.getLaunchUrl();
|
|
542
|
+
navigateDeepLink(manifest, onNavigate, launch?.url, authRedirectUri);
|
|
543
|
+
} catch {}
|
|
544
|
+
return listener;
|
|
545
|
+
};
|
|
546
|
+
var navigateWithFailureState = async (manifest, path, pushHistory, reinstallBrowserNavigation, fetchImpl) => {
|
|
547
|
+
try {
|
|
548
|
+
await navigate(manifest, path, pushHistory, fetchImpl);
|
|
549
|
+
} catch (error) {
|
|
550
|
+
console.error("[Absolute Mobile] Navigation failed:", error);
|
|
551
|
+
renderStatus("Unable to load this page. Check your connection and retry.");
|
|
552
|
+
} finally {
|
|
553
|
+
reinstallBrowserNavigation();
|
|
554
|
+
}
|
|
555
|
+
};
|
|
556
|
+
var startAbsoluteMobileShell = async (options = {}) => {
|
|
557
|
+
const manifest = await readManifest();
|
|
558
|
+
const auth = manifest.auth && options.createAuth ? await options.createAuth(manifest.auth) : undefined;
|
|
559
|
+
if (auth && manifest.sync?.socketTickets)
|
|
560
|
+
options.installSync?.(auth);
|
|
561
|
+
let removeAnchorNavigation = () => {
|
|
562
|
+
return;
|
|
563
|
+
};
|
|
564
|
+
const handlePopState = () => {
|
|
565
|
+
navigateWithFailureState(manifest, `${location.pathname}${location.search}${location.hash}`, false, reinstallBrowserNavigation, auth?.fetch);
|
|
566
|
+
};
|
|
567
|
+
const reinstallBrowserNavigation = () => {
|
|
568
|
+
removeAnchorNavigation();
|
|
569
|
+
removeEventListener("popstate", handlePopState);
|
|
570
|
+
removeAnchorNavigation = installAnchorNavigation(manifest, onNavigate);
|
|
571
|
+
addEventListener("popstate", handlePopState);
|
|
572
|
+
};
|
|
573
|
+
const onNavigate = (path) => {
|
|
574
|
+
navigateWithFailureState(manifest, path, true, reinstallBrowserNavigation, auth?.fetch);
|
|
575
|
+
};
|
|
576
|
+
await navigateWithFailureState(manifest, manifest.entry, false, reinstallBrowserNavigation, auth?.fetch);
|
|
577
|
+
await installDeepLinks(manifest, onNavigate, auth?.redirectUri);
|
|
578
|
+
};
|
|
579
|
+
export {
|
|
580
|
+
startAbsoluteMobileShell
|
|
581
|
+
};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// src/mobile/shellSync.ts
|
|
2
|
+
import { lifecycle, network } from "@absolutejs/devices";
|
|
3
|
+
import {
|
|
4
|
+
createCapacitorSyncLocalStore,
|
|
5
|
+
installCapacitorSyncLifecycle
|
|
6
|
+
} from "@absolutejs/sync-capacitor";
|
|
7
|
+
import { installSyncClientRuntimeTransport } from "@absolutejs/sync/client/runtime";
|
|
8
|
+
var reloadShell = () => globalThis.location.reload();
|
|
9
|
+
var ABSOLUTE_MOBILE_SYNC_STATUS_EVENT = "absolute:sync-status";
|
|
10
|
+
var reportShellStatus = (status) => globalThis.dispatchEvent(new CustomEvent(ABSOLUTE_MOBILE_SYNC_STATUS_EVENT, { detail: status }));
|
|
11
|
+
var installAbsoluteMobileShellSync = (auth, options = {}) => {
|
|
12
|
+
const namespace = auth.principal?.namespace;
|
|
13
|
+
const store = namespace ? options.createStore?.() ?? createCapacitorSyncLocalStore() : undefined;
|
|
14
|
+
const installLifecycle = options.installLifecycle ?? installCapacitorSyncLifecycle;
|
|
15
|
+
const reportStatus = options.reportStatus ?? reportShellStatus;
|
|
16
|
+
const removeTransport = installSyncClientRuntimeTransport({
|
|
17
|
+
...namespace && store ? { durable: { namespace, store } } : {},
|
|
18
|
+
socketTicket: auth.socketTicket,
|
|
19
|
+
registerClient: (client) => {
|
|
20
|
+
let active2 = true;
|
|
21
|
+
const removeStatus = client.subscribeStatus(reportStatus);
|
|
22
|
+
const reportLifecycleFailure = (error) => console.error("[Absolute Mobile] Sync lifecycle installation failed:", error);
|
|
23
|
+
const removal = installLifecycle({
|
|
24
|
+
client,
|
|
25
|
+
lifecycle,
|
|
26
|
+
network
|
|
27
|
+
}).catch((error) => {
|
|
28
|
+
reportLifecycleFailure(error);
|
|
29
|
+
return;
|
|
30
|
+
});
|
|
31
|
+
return () => {
|
|
32
|
+
if (!active2)
|
|
33
|
+
return;
|
|
34
|
+
active2 = false;
|
|
35
|
+
removeStatus();
|
|
36
|
+
removal.then((remove) => remove?.());
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
const reload = options.reload ?? reloadShell;
|
|
41
|
+
const removePrincipalListener = auth.onPrincipalChange((principal) => {
|
|
42
|
+
if (principal?.namespace !== namespace)
|
|
43
|
+
reload();
|
|
44
|
+
});
|
|
45
|
+
let active = true;
|
|
46
|
+
return () => {
|
|
47
|
+
if (!active)
|
|
48
|
+
return;
|
|
49
|
+
active = false;
|
|
50
|
+
removePrincipalListener();
|
|
51
|
+
removeTransport();
|
|
52
|
+
};
|
|
53
|
+
};
|
|
54
|
+
export {
|
|
55
|
+
installAbsoluteMobileShellSync,
|
|
56
|
+
ABSOLUTE_MOBILE_SYNC_STATUS_EVENT
|
|
57
|
+
};
|
|
@@ -1,7 +1,10 @@
|
|
|
1
|
+
import { type MobileAuthPrincipal } from '@absolutejs/auth/client/mobile';
|
|
1
2
|
import type { AbsoluteMobileAuthManifest } from './nativeAuth';
|
|
2
3
|
import type { AbsoluteMobileFetch } from './transport';
|
|
3
4
|
export type AbsoluteMobileShellAuth = {
|
|
4
5
|
fetch: AbsoluteMobileFetch;
|
|
6
|
+
onPrincipalChange: (listener: (principal: MobileAuthPrincipal | null) => void) => () => void;
|
|
7
|
+
principal: MobileAuthPrincipal | null;
|
|
5
8
|
redirectUri: string;
|
|
6
9
|
socketTicket: (audience?: string) => Promise<string>;
|
|
7
10
|
};
|
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
import { type AbsoluteMobileFetch, type AbsoluteMobileClientManifest } from './transport';
|
|
2
2
|
export type AbsoluteMobileShellAuthRuntime = {
|
|
3
3
|
fetch: AbsoluteMobileFetch;
|
|
4
|
+
onPrincipalChange: (listener: (principal: AbsoluteMobileShellPrincipal | null) => void) => () => void;
|
|
5
|
+
principal: AbsoluteMobileShellPrincipal | null;
|
|
4
6
|
redirectUri: string;
|
|
5
7
|
socketTicket: (audience?: string) => Promise<string>;
|
|
6
8
|
};
|
|
9
|
+
export type AbsoluteMobileShellPrincipal = {
|
|
10
|
+
namespace: string;
|
|
11
|
+
};
|
|
7
12
|
export type AbsoluteMobileShellOptions = {
|
|
8
13
|
createAuth?: (config: NonNullable<AbsoluteMobileClientManifest['auth']>) => Promise<AbsoluteMobileShellAuthRuntime>;
|
|
9
|
-
installSync?: (auth: AbsoluteMobileShellAuthRuntime) => void;
|
|
14
|
+
installSync?: (auth: AbsoluteMobileShellAuthRuntime) => void | (() => void);
|
|
10
15
|
};
|
|
11
16
|
export declare const startAbsoluteMobileShell: (options?: AbsoluteMobileShellOptions) => Promise<void>;
|
|
@@ -1,2 +1,16 @@
|
|
|
1
|
+
import { type CapacitorSyncLifecycleOptions } from '@absolutejs/sync-capacitor';
|
|
2
|
+
import type { SyncClientStatus, SyncLocalStore } from '@absolutejs/sync/client';
|
|
1
3
|
import type { AbsoluteMobileShellAuthRuntime } from './shellBootstrap';
|
|
2
|
-
export
|
|
4
|
+
export type AbsoluteMobileShellSyncOptions = {
|
|
5
|
+
createStore?: () => SyncLocalStore;
|
|
6
|
+
installLifecycle?: (options: CapacitorSyncLifecycleOptions) => Promise<() => void>;
|
|
7
|
+
reload?: () => void;
|
|
8
|
+
reportStatus?: (status: SyncClientStatus) => void;
|
|
9
|
+
};
|
|
10
|
+
export declare const ABSOLUTE_MOBILE_SYNC_STATUS_EVENT = "absolute:sync-status";
|
|
11
|
+
/**
|
|
12
|
+
* Provisions unchanged Sync clients with native durability, Auth isolation, and
|
|
13
|
+
* foreground/connectivity reconnects. An identity change reloads the shell so
|
|
14
|
+
* no page-held client can retain the previous account's in-memory rows.
|
|
15
|
+
*/
|
|
16
|
+
export declare const installAbsoluteMobileShellSync: (auth: AbsoluteMobileShellAuthRuntime, options?: AbsoluteMobileShellSyncOptions) => () => void;
|
package/package.json
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
"url": "https://github.com/absolutejs/absolutejs/issues"
|
|
8
8
|
},
|
|
9
9
|
"dependencies": {
|
|
10
|
-
"@absolutejs/auth": "0.
|
|
10
|
+
"@absolutejs/auth": "0.70.0",
|
|
11
11
|
"@absolutejs/devices": "0.0.2",
|
|
12
12
|
"@absolutejs/devices-capacitor": "0.1.2",
|
|
13
13
|
"@capacitor/browser": "8.0.4",
|
|
@@ -25,7 +25,8 @@
|
|
|
25
25
|
"devDependencies": {
|
|
26
26
|
"@absolutejs/manifest": "0.9.0",
|
|
27
27
|
"@absolutejs/scoped-state": "0.2.2",
|
|
28
|
-
"@absolutejs/sync": "2.
|
|
28
|
+
"@absolutejs/sync": "2.22.2",
|
|
29
|
+
"@absolutejs/sync-capacitor": "0.2.0",
|
|
29
30
|
"@angular/animations": "21.2.6",
|
|
30
31
|
"@angular/cdk": "21.2.6",
|
|
31
32
|
"@angular/common": "21.2.6",
|
|
@@ -39,6 +40,7 @@
|
|
|
39
40
|
"@angular/router": "21.2.6",
|
|
40
41
|
"@angular/ssr": "21.2.5",
|
|
41
42
|
"@babel/preset-typescript": "^7.28.5",
|
|
43
|
+
"@capacitor-community/sqlite": "8.1.1",
|
|
42
44
|
"@capacitor/android": "8.5.0",
|
|
43
45
|
"@capacitor/app": "8.1.1",
|
|
44
46
|
"@capacitor/cli": "8.5.0",
|
|
@@ -272,12 +274,12 @@
|
|
|
272
274
|
"main": "./dist/index.js",
|
|
273
275
|
"name": "@absolutejs/absolute",
|
|
274
276
|
"optionalDependencies": {
|
|
275
|
-
"@absolutejs/native-darwin-arm64": "0.20.0-beta.
|
|
276
|
-
"@absolutejs/native-darwin-x64": "0.20.0-beta.
|
|
277
|
-
"@absolutejs/native-linux-arm64": "0.20.0-beta.
|
|
278
|
-
"@absolutejs/native-linux-x64": "0.20.0-beta.
|
|
279
|
-
"@absolutejs/native-windows-arm64": "0.20.0-beta.
|
|
280
|
-
"@absolutejs/native-windows-x64": "0.20.0-beta.
|
|
277
|
+
"@absolutejs/native-darwin-arm64": "0.20.0-beta.8",
|
|
278
|
+
"@absolutejs/native-darwin-x64": "0.20.0-beta.8",
|
|
279
|
+
"@absolutejs/native-linux-arm64": "0.20.0-beta.8",
|
|
280
|
+
"@absolutejs/native-linux-x64": "0.20.0-beta.8",
|
|
281
|
+
"@absolutejs/native-windows-arm64": "0.20.0-beta.8",
|
|
282
|
+
"@absolutejs/native-windows-x64": "0.20.0-beta.8"
|
|
281
283
|
},
|
|
282
284
|
"overrides": {
|
|
283
285
|
"@sinclair/typebox": "0.34.52",
|
|
@@ -287,7 +289,7 @@
|
|
|
287
289
|
"typebox": "1.3.16"
|
|
288
290
|
},
|
|
289
291
|
"peerDependencies": {
|
|
290
|
-
"@absolutejs/sync": ">=2.
|
|
292
|
+
"@absolutejs/sync": ">=2.22.2 <3",
|
|
291
293
|
"@angular/animations": "^21.0.0",
|
|
292
294
|
"@angular/common": "^21.0.0",
|
|
293
295
|
"@angular/compiler": "^21.0.0",
|
|
@@ -488,7 +490,7 @@
|
|
|
488
490
|
]
|
|
489
491
|
}
|
|
490
492
|
},
|
|
491
|
-
"version": "0.20.0-beta.
|
|
493
|
+
"version": "0.20.0-beta.8",
|
|
492
494
|
"workspaces": [
|
|
493
495
|
"tests/fixtures/*",
|
|
494
496
|
"tests/fixtures/_packages/*"
|