@absolutejs/absolute 0.20.0-beta.3 → 0.20.0-beta.30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +167 -0
- package/dist/angular/browser.js +15 -1
- package/dist/angular/browser.js.map +3 -3
- package/dist/angular/components/core/streamingSlotRegistrar.js +1 -1
- package/dist/angular/components/core/streamingSlotRegistry.js +2 -2
- package/dist/angular/index.js +341 -22
- package/dist/angular/index.js.map +8 -5
- package/dist/angular/server.js +341 -22
- package/dist/angular/server.js.map +8 -5
- package/dist/build.js +2058 -1105
- package/dist/build.js.map +17 -13
- package/dist/cli/index.js +4077 -1139
- package/dist/dev/client/cssUtils.ts +16 -2
- package/dist/dev/client/handlers/rebuild.ts +11 -1
- package/dist/dev/client/hmrClient.ts +32 -3
- package/dist/dev/client/hmrTiming.ts +14 -7
- package/dist/dev/client/syncDevtools.ts +237 -0
- package/dist/index.js +2483 -1367
- package/dist/index.js.map +27 -22
- package/dist/mobile/browser.js +123 -1
- package/dist/mobile/browser.js.map +6 -4
- package/dist/mobile/index.js +2663 -247
- package/dist/mobile/index.js.map +25 -13
- package/dist/mobile/remoteMacAgentEntry.js +29 -0
- package/dist/mobile/shellAuth.js +36 -0
- package/dist/mobile/shellBootstrap.js +601 -0
- package/dist/mobile/shellSync.js +173 -0
- package/dist/src/angular/pageHandler.d.ts +3 -0
- package/dist/src/build/pwa.d.ts +16 -0
- package/dist/src/cli/config/server.d.ts +1 -1
- package/dist/src/core/devBuild.d.ts +1 -0
- package/dist/src/core/pageHandlers.d.ts +11 -2
- package/dist/src/core/prepare.d.ts +24 -0
- package/dist/src/mobile/androidEmulatorController.d.ts +6 -1
- package/dist/src/mobile/androidTestReport.d.ts +14 -0
- package/dist/src/mobile/androidUpgradeConformance.d.ts +47 -0
- package/dist/src/mobile/androidWebView.d.ts +1 -0
- package/dist/src/mobile/browser.d.ts +1 -0
- package/dist/src/mobile/buildPipeline.d.ts +1 -0
- package/dist/src/mobile/capacitorBundle.d.ts +22 -1
- package/dist/src/mobile/client.d.ts +4 -0
- package/dist/src/mobile/config.d.ts +1 -0
- package/dist/src/mobile/devDeviceAdapter.d.ts +3 -0
- package/dist/src/mobile/deviceCapabilities.d.ts +46 -0
- package/dist/src/mobile/index.d.ts +7 -0
- package/dist/src/mobile/iosTestReport.d.ts +23 -0
- package/dist/src/mobile/nativeAuth.d.ts +17 -0
- package/dist/src/mobile/nativeBackgroundSync.d.ts +4 -0
- package/dist/src/mobile/nativeDeviceCapabilities.d.ts +6 -0
- package/dist/src/mobile/nativeTestReport.d.ts +85 -0
- package/dist/src/mobile/releaseArtifact.d.ts +2 -0
- package/dist/src/mobile/remoteMacAgent.d.ts +2 -0
- package/dist/src/mobile/remoteMacAgentEntry.d.ts +1 -0
- package/dist/src/mobile/remoteMacProtocol.d.ts +114 -0
- package/dist/src/mobile/remoteMacWire.d.ts +2 -0
- package/dist/src/mobile/shellAuth.d.ts +15 -0
- package/dist/src/mobile/shellBootstrap.d.ts +22 -1
- package/dist/src/mobile/shellPush.d.ts +11 -0
- package/dist/src/mobile/shellSync.d.ts +47 -0
- package/dist/src/mobile/staticDocument.d.ts +5 -0
- package/dist/src/mobile/syncRemediation.d.ts +10 -0
- package/dist/src/mobile/syncSchema.d.ts +9 -0
- package/dist/src/mobile/transport.d.ts +16 -1
- package/dist/src/plugins/imageOptimizer.d.ts +1 -1
- package/dist/src/svelte/pageHandler.d.ts +3 -0
- package/dist/src/utils/imageProcessing.d.ts +3 -0
- package/dist/src/utils/loadConfig.d.ts +1 -0
- package/dist/src/vue/pageHandler.d.ts +3 -0
- package/dist/svelte/index.js +312 -23
- package/dist/svelte/index.js.map +7 -4
- package/dist/svelte/server.js +307 -18
- package/dist/svelte/server.js.map +7 -4
- package/dist/types/build.d.ts +29 -0
- package/dist/vue/index.js +312 -23
- package/dist/vue/index.js.map +7 -4
- package/dist/vue/server.js +307 -18
- package/dist/vue/server.js.map +7 -4
- package/package.json +37 -10
|
@@ -0,0 +1,601 @@
|
|
|
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 initialNavigationPath = (entry) => {
|
|
440
|
+
const current = `${location.pathname}${location.search}${location.hash}`;
|
|
441
|
+
return current === "/" || current === "/index.html" ? entry : current;
|
|
442
|
+
};
|
|
443
|
+
var navigationGeneration = 0;
|
|
444
|
+
var notifyCapacitorSystemBarsDomReady = () => {
|
|
445
|
+
const provider = Reflect.get(globalThis, "CapacitorSystemBarsAndroidInterface");
|
|
446
|
+
if (typeof provider !== "object" || provider === null)
|
|
447
|
+
return;
|
|
448
|
+
const onDOMReady = Reflect.get(provider, "onDOMReady");
|
|
449
|
+
if (typeof onDOMReady !== "function")
|
|
450
|
+
return;
|
|
451
|
+
try {
|
|
452
|
+
onDOMReady.call(provider);
|
|
453
|
+
} catch {}
|
|
454
|
+
};
|
|
455
|
+
var readManifest = async () => {
|
|
456
|
+
const response = await fetch(MANIFEST_PATH, { cache: "no-store" });
|
|
457
|
+
if (!response.ok) {
|
|
458
|
+
throw new TypeError(`Mobile manifest failed with HTTP ${response.status}.`);
|
|
459
|
+
}
|
|
460
|
+
const manifest = await response.json();
|
|
461
|
+
return manifest;
|
|
462
|
+
};
|
|
463
|
+
var renderStatus = (message) => {
|
|
464
|
+
document.title = "AbsoluteJS";
|
|
465
|
+
document.head.innerHTML = '<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover">';
|
|
466
|
+
document.body.innerHTML = `<main id="${STATUS_ID}" role="status"></main>`;
|
|
467
|
+
const status = document.getElementById(STATUS_ID);
|
|
468
|
+
if (status)
|
|
469
|
+
status.textContent = message;
|
|
470
|
+
};
|
|
471
|
+
var renderPageTarget = () => {
|
|
472
|
+
document.title = "AbsoluteJS";
|
|
473
|
+
document.head.innerHTML = '<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover">';
|
|
474
|
+
document.body.innerHTML = '<div id="root"></div>';
|
|
475
|
+
};
|
|
476
|
+
var localBundleUrl = (manifest, pageId, contract) => {
|
|
477
|
+
const page = manifest.pages.find((candidate) => candidate.pageId === pageId && candidate.contract === contract);
|
|
478
|
+
if (!page) {
|
|
479
|
+
throw new TypeError(`The embedded app does not contain ${pageId} contract ${contract}.`);
|
|
480
|
+
}
|
|
481
|
+
const url = new URL(page.localBundlePath, document.baseURI);
|
|
482
|
+
url.searchParams.set("absoluteNavigation", String(navigationGeneration += 1));
|
|
483
|
+
return url.href;
|
|
484
|
+
};
|
|
485
|
+
var installLocalPageStyle = (manifest, pageId, contract) => {
|
|
486
|
+
const page = manifest.pages.find((candidate) => candidate.pageId === pageId && candidate.contract === contract);
|
|
487
|
+
if (!page?.localStylePath)
|
|
488
|
+
return;
|
|
489
|
+
const link = document.createElement("link");
|
|
490
|
+
link.rel = "stylesheet";
|
|
491
|
+
link.href = new URL(page.localStylePath, document.baseURI).href;
|
|
492
|
+
link.dataset.absoluteMobilePageStyle = page.bundleHash;
|
|
493
|
+
document.head.appendChild(link);
|
|
494
|
+
};
|
|
495
|
+
var navigate = async (manifest, path, pushHistory, fetchImpl) => {
|
|
496
|
+
await disposeAbsoluteMobilePage();
|
|
497
|
+
renderStatus("Loading…");
|
|
498
|
+
const envelope = await fetchAbsoluteMobilePage(manifest, path, {
|
|
499
|
+
...fetchImpl ? { fetch: fetchImpl } : {}
|
|
500
|
+
});
|
|
501
|
+
const activation = await activateAbsoluteMobilePage(envelope, {
|
|
502
|
+
loadPage: ({ contract, pageId }) => {
|
|
503
|
+
const page = manifest.pages.find((candidate) => candidate.pageId === pageId && candidate.contract === contract);
|
|
504
|
+
if (!page) {
|
|
505
|
+
throw new TypeError(`The embedded app does not contain ${pageId} contract ${contract}.`);
|
|
506
|
+
}
|
|
507
|
+
if (page.framework === "html" || page.framework === "htmx") {
|
|
508
|
+
return installAbsoluteMobileStaticDocument(manifest, page, localBundleUrl(manifest, pageId, contract));
|
|
509
|
+
}
|
|
510
|
+
renderPageTarget();
|
|
511
|
+
installLocalPageStyle(manifest, pageId, contract);
|
|
512
|
+
return import(localBundleUrl(manifest, pageId, contract));
|
|
513
|
+
}
|
|
514
|
+
});
|
|
515
|
+
if (activation.kind === "upgrade-required") {
|
|
516
|
+
renderStatus("This app version must be updated to continue.");
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
if (pushHistory)
|
|
520
|
+
history.pushState({ absoluteMobile: true }, "", path);
|
|
521
|
+
};
|
|
522
|
+
var installAnchorNavigation = (manifest, onNavigate) => {
|
|
523
|
+
const handleClick = (event) => {
|
|
524
|
+
if (!(event.target instanceof Element))
|
|
525
|
+
return;
|
|
526
|
+
const anchor = event.target.closest("a[href]");
|
|
527
|
+
if (!(anchor instanceof HTMLAnchorElement) || anchor.target)
|
|
528
|
+
return;
|
|
529
|
+
try {
|
|
530
|
+
const path = resolveAbsoluteMobileNavigation(manifest, anchor.href, location.origin);
|
|
531
|
+
if (!path)
|
|
532
|
+
return;
|
|
533
|
+
event.preventDefault();
|
|
534
|
+
onNavigate(path);
|
|
535
|
+
} catch {}
|
|
536
|
+
};
|
|
537
|
+
addEventListener("click", handleClick);
|
|
538
|
+
return () => removeEventListener("click", handleClick);
|
|
539
|
+
};
|
|
540
|
+
var navigateDeepLink = (manifest, onNavigate, url, authRedirectUri) => {
|
|
541
|
+
if (!url)
|
|
542
|
+
return;
|
|
543
|
+
if (authRedirectUri) {
|
|
544
|
+
const actual = new URL(url);
|
|
545
|
+
const redirect = new URL(authRedirectUri);
|
|
546
|
+
if (actual.protocol === redirect.protocol && actual.host === redirect.host && actual.pathname === redirect.pathname)
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
onNavigate(resolveAbsoluteMobileDeepLink(manifest, url));
|
|
550
|
+
};
|
|
551
|
+
var deepLinkListener = (manifest, onNavigate, authRedirectUri) => ({ url }) => navigateDeepLink(manifest, onNavigate, url, authRedirectUri);
|
|
552
|
+
var installDeepLinks = async (manifest, onNavigate, authRedirectUri) => {
|
|
553
|
+
let listener;
|
|
554
|
+
try {
|
|
555
|
+
listener = await App.addListener("appUrlOpen", deepLinkListener(manifest, onNavigate, authRedirectUri));
|
|
556
|
+
const launch = await App.getLaunchUrl();
|
|
557
|
+
navigateDeepLink(manifest, onNavigate, launch?.url, authRedirectUri);
|
|
558
|
+
} catch {}
|
|
559
|
+
return listener;
|
|
560
|
+
};
|
|
561
|
+
var navigateWithFailureState = async (manifest, path, pushHistory, reinstallBrowserNavigation, fetchImpl) => {
|
|
562
|
+
try {
|
|
563
|
+
await navigate(manifest, path, pushHistory, fetchImpl);
|
|
564
|
+
} catch (error) {
|
|
565
|
+
console.error("[Absolute Mobile] Navigation failed:", error);
|
|
566
|
+
renderStatus("Unable to load this page. Check your connection and retry.");
|
|
567
|
+
} finally {
|
|
568
|
+
reinstallBrowserNavigation();
|
|
569
|
+
}
|
|
570
|
+
};
|
|
571
|
+
var startAbsoluteMobileShell = async (options = {}) => {
|
|
572
|
+
notifyCapacitorSystemBarsDomReady();
|
|
573
|
+
const manifest = await readManifest();
|
|
574
|
+
const auth = manifest.auth && options.createAuth ? await options.createAuth(manifest.auth, {
|
|
575
|
+
beforeSignOut: options.beforeSignOut
|
|
576
|
+
}) : undefined;
|
|
577
|
+
if (auth)
|
|
578
|
+
options.connectPush?.(auth);
|
|
579
|
+
if (auth && manifest.sync?.socketTickets)
|
|
580
|
+
options.installSync?.(auth, manifest.sync);
|
|
581
|
+
let removeAnchorNavigation = () => {
|
|
582
|
+
return;
|
|
583
|
+
};
|
|
584
|
+
const handlePopState = () => {
|
|
585
|
+
navigateWithFailureState(manifest, `${location.pathname}${location.search}${location.hash}`, false, reinstallBrowserNavigation, auth?.fetch);
|
|
586
|
+
};
|
|
587
|
+
const reinstallBrowserNavigation = () => {
|
|
588
|
+
removeAnchorNavigation();
|
|
589
|
+
removeEventListener("popstate", handlePopState);
|
|
590
|
+
removeAnchorNavigation = installAnchorNavigation(manifest, onNavigate);
|
|
591
|
+
addEventListener("popstate", handlePopState);
|
|
592
|
+
};
|
|
593
|
+
const onNavigate = (path) => {
|
|
594
|
+
navigateWithFailureState(manifest, path, true, reinstallBrowserNavigation, auth?.fetch);
|
|
595
|
+
};
|
|
596
|
+
await navigateWithFailureState(manifest, initialNavigationPath(manifest.entry), false, reinstallBrowserNavigation, auth?.fetch);
|
|
597
|
+
await installDeepLinks(manifest, onNavigate, auth?.redirectUri);
|
|
598
|
+
};
|
|
599
|
+
export {
|
|
600
|
+
startAbsoluteMobileShell
|
|
601
|
+
};
|