arcane-os 0.9.0 → 0.10.0
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/CHANGELOG.md +17 -0
- package/README.md +14 -8
- package/browser-runtime/pwa.mjs +215 -0
- package/docs/reference/README.md +3 -2
- package/docs/reference/asset-versioning.md +13 -0
- package/docs/reference/inventory/package-api.json +29 -1
- package/docs/reference/pwa.md +183 -0
- package/docs/reference/sdk-api.md +45 -1
- package/package.json +3 -2
- package/runtime/arcane/modules/HTMLImport.js +13 -3
- package/schemas/arcane-app.schema.json +28 -0
- package/schemas/arcane-package.schema.json +30 -0
- package/src/app-descriptor.mjs +10 -2
- package/src/dev-server.mjs +252 -13
- package/src/import-map.mjs +186 -11
- package/src/packager/core.mjs +140 -17
- package/src/pwa-worker.mjs +227 -0
- package/src/pwa.mjs +311 -0
- package/src/targets/index.mjs +1 -0
- package/src/toolchain.mjs +1 -0
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
export function createPwaWorkerScript(manifest) {
|
|
2
|
+
return `(${installPwaWorker.toString()})(${JSON.stringify(manifest, null, 4)});\n`;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
function installPwaWorker(manifest) {
|
|
6
|
+
const scope = self.registration.scope;
|
|
7
|
+
const cachePrefix = `arcane-pwa|${JSON.stringify(
|
|
8
|
+
[manifest.appId, scope]
|
|
9
|
+
)}|`;
|
|
10
|
+
const cacheName = cachePrefix + JSON.stringify(
|
|
11
|
+
[
|
|
12
|
+
manifest.appVersion,
|
|
13
|
+
manifest.sdkVersion,
|
|
14
|
+
manifest.mode,
|
|
15
|
+
manifest.revision
|
|
16
|
+
]
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
function cacheUrl(value) {
|
|
20
|
+
const url = new URL(value, scope);
|
|
21
|
+
// Cache matching excludes fragments; preserve queries and the original network request.
|
|
22
|
+
url.hash = '';
|
|
23
|
+
return url.href;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const assetUrls = [
|
|
27
|
+
...new Set(
|
|
28
|
+
manifest.assets.map(cacheUrl)
|
|
29
|
+
)
|
|
30
|
+
];
|
|
31
|
+
const ownedUrls = new Set(assetUrls);
|
|
32
|
+
const navigationAliases = new Map();
|
|
33
|
+
for (const [alias, asset] of Object.entries(manifest.navigationAliases)) {
|
|
34
|
+
navigationAliases.set(
|
|
35
|
+
cacheUrl(alias),
|
|
36
|
+
new URL(asset, scope).href
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function errorDetails(error) {
|
|
41
|
+
if (!(error instanceof Error)) {
|
|
42
|
+
return error;
|
|
43
|
+
}
|
|
44
|
+
const detail = {name: error.name, message: error.message, stack: error.stack};
|
|
45
|
+
if ('cause' in error) {
|
|
46
|
+
detail.cause = errorDetails(error.cause);
|
|
47
|
+
}
|
|
48
|
+
if ('errors' in error) {
|
|
49
|
+
detail.errors = Array.from(error.errors, errorDetails);
|
|
50
|
+
}
|
|
51
|
+
if ('url' in error) {
|
|
52
|
+
detail.url = error.url;
|
|
53
|
+
}
|
|
54
|
+
return detail;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function reportFailure(error) {
|
|
58
|
+
const clients = await self.clients.matchAll(
|
|
59
|
+
{type: 'window', includeUncontrolled: true}
|
|
60
|
+
);
|
|
61
|
+
const message = {type: 'arcane.pwa.error', error: errorDetails(error)};
|
|
62
|
+
for (const client of clients) {
|
|
63
|
+
if (client.url.startsWith(scope)) {
|
|
64
|
+
client.postMessage(message);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function populateCache() {
|
|
70
|
+
const cache = await caches.open(cacheName);
|
|
71
|
+
const failures = [];
|
|
72
|
+
let nextAsset = 0;
|
|
73
|
+
|
|
74
|
+
async function fetchAssets() {
|
|
75
|
+
while (nextAsset < assetUrls.length) {
|
|
76
|
+
const url = assetUrls[nextAsset++];
|
|
77
|
+
try {
|
|
78
|
+
const response = await fetch(
|
|
79
|
+
new Request(
|
|
80
|
+
url,
|
|
81
|
+
{cache: manifest.mode === 'development' ? 'no-cache' : 'reload'}
|
|
82
|
+
)
|
|
83
|
+
);
|
|
84
|
+
if (!response.ok) {
|
|
85
|
+
throw new Error(`PWA resource returned HTTP ${response.status} ${response.statusText}: ${url}`);
|
|
86
|
+
}
|
|
87
|
+
await cache.put(url, response);
|
|
88
|
+
} catch (cause) {
|
|
89
|
+
const error = new Error(
|
|
90
|
+
`PWA resource could not be cached: ${url}`,
|
|
91
|
+
{cause}
|
|
92
|
+
);
|
|
93
|
+
error.url = url;
|
|
94
|
+
failures.push(error);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const workers = [];
|
|
100
|
+
for (let index = 0; index < Math.min(4, assetUrls.length); index += 1) {
|
|
101
|
+
workers.push(
|
|
102
|
+
fetchAssets()
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
await Promise.all(workers);
|
|
106
|
+
if (failures.length > 0) {
|
|
107
|
+
throw new AggregateError(failures, 'The PWA resource generation could not be installed.');
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async function reportFailureAndReject(error) {
|
|
112
|
+
try {
|
|
113
|
+
await reportFailure(error);
|
|
114
|
+
} catch (reportError) {
|
|
115
|
+
console.error('PWA failure could not be sent to application clients.', reportError, error);
|
|
116
|
+
}
|
|
117
|
+
throw error;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function onInstall(event) {
|
|
121
|
+
event.waitUntil(
|
|
122
|
+
populateCache().catch(reportFailureAndReject)
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async function retireOldCaches() {
|
|
127
|
+
const names = await caches.keys();
|
|
128
|
+
const retirements = [];
|
|
129
|
+
for (const name of names) {
|
|
130
|
+
if (name.startsWith(cachePrefix) && name !== cacheName) {
|
|
131
|
+
retirements.push(
|
|
132
|
+
caches.delete(name)
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
await Promise.all(retirements);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function onActivate(event) {
|
|
140
|
+
event.waitUntil(
|
|
141
|
+
retireOldCaches().catch(reportFailureAndReject)
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async function releaseResource(request, url) {
|
|
146
|
+
const cache = await caches.open(cacheName);
|
|
147
|
+
const cached = await cache.match(url);
|
|
148
|
+
if (cached) {
|
|
149
|
+
return cached;
|
|
150
|
+
}
|
|
151
|
+
return fetch(
|
|
152
|
+
new Request(
|
|
153
|
+
request,
|
|
154
|
+
{cache: 'no-cache'}
|
|
155
|
+
)
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async function developmentResource(request, url) {
|
|
160
|
+
let response;
|
|
161
|
+
try {
|
|
162
|
+
response = await fetch(
|
|
163
|
+
new Request(
|
|
164
|
+
request,
|
|
165
|
+
{cache: 'no-cache'}
|
|
166
|
+
)
|
|
167
|
+
);
|
|
168
|
+
} catch (error) {
|
|
169
|
+
const cache = await caches.open(cacheName);
|
|
170
|
+
const cached = await cache.match(url);
|
|
171
|
+
if (cached) {
|
|
172
|
+
return {response: cached, fromNetwork: false, url};
|
|
173
|
+
}
|
|
174
|
+
throw error;
|
|
175
|
+
}
|
|
176
|
+
return {response, fromNetwork: true, url};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function resourceResponse(resource) {
|
|
180
|
+
return resource.response;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
async function saveDevelopmentResource(resource) {
|
|
184
|
+
if (resource.fromNetwork && resource.response.ok) {
|
|
185
|
+
const response = resource.response.clone();
|
|
186
|
+
const cache = await caches.open(cacheName);
|
|
187
|
+
await cache.put(resource.url, response);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function onFetch(event) {
|
|
192
|
+
const request = event.request;
|
|
193
|
+
if (request.method !== 'GET' || request.headers.has('range')) {
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
const requestUrl = cacheUrl(request.url);
|
|
197
|
+
const redirect = request.mode === 'navigate' ? navigationAliases.get(requestUrl) : null;
|
|
198
|
+
if (redirect && cacheUrl(redirect) !== requestUrl && ownedUrls.has(cacheUrl(redirect))) {
|
|
199
|
+
// Navigation retains its requested document URL unless it follows a redirect.
|
|
200
|
+
event.respondWith(
|
|
201
|
+
Response.redirect(redirect, 302)
|
|
202
|
+
);
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
const url = requestUrl;
|
|
206
|
+
if (!ownedUrls.has(url)) {
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
if (manifest.mode === 'development') {
|
|
210
|
+
const resource = developmentResource(request, url);
|
|
211
|
+
event.waitUntil(
|
|
212
|
+
resource.then(saveDevelopmentResource).catch(reportFailureAndReject)
|
|
213
|
+
);
|
|
214
|
+
event.respondWith(
|
|
215
|
+
resource.then(resourceResponse)
|
|
216
|
+
);
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
event.respondWith(
|
|
220
|
+
releaseResource(request, url).catch(reportFailureAndReject)
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
self.addEventListener('install', onInstall);
|
|
225
|
+
self.addEventListener('activate', onActivate);
|
|
226
|
+
self.addEventListener('fetch', onFetch);
|
|
227
|
+
}
|
package/src/pwa.mjs
ADDED
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
import Is from 'strong-type';
|
|
2
|
+
import {randomUUID} from 'node:crypto';
|
|
3
|
+
import {createPwaWorkerScript} from './pwa-worker.mjs';
|
|
4
|
+
import {versionAssetUrl} from './import-map.mjs';
|
|
5
|
+
|
|
6
|
+
const is = new Is(false);
|
|
7
|
+
|
|
8
|
+
export const PWA_MANIFEST_NAME = 'arcane.webmanifest';
|
|
9
|
+
export const PWA_OFFLINE_MANIFEST_NAME = 'arcane-offline.json';
|
|
10
|
+
export const PWA_WORKER_NAME = 'arcane-sw.js';
|
|
11
|
+
export const PWA_BOOTSTRAP_NAME = 'arcane-pwa.mjs';
|
|
12
|
+
|
|
13
|
+
function record(value, label) {
|
|
14
|
+
if (value === null || !is.object(value) || is.array(value)) {
|
|
15
|
+
throw new TypeError(`${label} must be an object.`);
|
|
16
|
+
}
|
|
17
|
+
return value;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function pathList(value, label) {
|
|
21
|
+
function invalidPath(item) {
|
|
22
|
+
return !is.string(item) || !item;
|
|
23
|
+
}
|
|
24
|
+
if (!is.array(value) || value.some(invalidPath)) {
|
|
25
|
+
throw new TypeError(`${label} must be an array of nonempty paths.`);
|
|
26
|
+
}
|
|
27
|
+
return [...value];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function normalizePwaConfig(value) {
|
|
31
|
+
if (value === undefined) return undefined;
|
|
32
|
+
record(value, 'pwa');
|
|
33
|
+
for (const key of Object.keys(value)) {
|
|
34
|
+
if (!['enabled', 'manifest', 'offline'].includes(key)) {
|
|
35
|
+
throw new TypeError(`pwa contains an unsupported field: ${key}.`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
if (value.enabled !== undefined && !is.boolean(value.enabled)) {
|
|
39
|
+
throw new TypeError('pwa.enabled must be a boolean.');
|
|
40
|
+
}
|
|
41
|
+
const manifest = record(
|
|
42
|
+
value.manifest === undefined ? {} : value.manifest,
|
|
43
|
+
'pwa.manifest'
|
|
44
|
+
);
|
|
45
|
+
const offline = record(
|
|
46
|
+
value.offline === undefined ? {} : value.offline,
|
|
47
|
+
'pwa.offline'
|
|
48
|
+
);
|
|
49
|
+
for (const key of Object.keys(offline)) {
|
|
50
|
+
if (key !== 'include' && key !== 'exclude') {
|
|
51
|
+
throw new TypeError(`pwa.offline contains an unsupported field: ${key}.`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return {
|
|
55
|
+
enabled: value.enabled === true,
|
|
56
|
+
manifest: {...manifest},
|
|
57
|
+
offline: {
|
|
58
|
+
include: pathList(
|
|
59
|
+
offline.include === undefined ? [] : offline.include,
|
|
60
|
+
'pwa.offline.include'
|
|
61
|
+
),
|
|
62
|
+
exclude: pathList(
|
|
63
|
+
offline.exclude === undefined ? [] : offline.exclude,
|
|
64
|
+
'pwa.offline.exclude'
|
|
65
|
+
)
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function selectedPath(file, selection) {
|
|
71
|
+
const prefix = selection.endsWith('/') ? selection : `${selection}/`;
|
|
72
|
+
return file === selection || file.startsWith(prefix);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function selectPwaFiles(files, pwa) {
|
|
76
|
+
const config = normalizePwaConfig(pwa);
|
|
77
|
+
const include = config?.offline.include ?? [];
|
|
78
|
+
const exclude = config?.offline.exclude ?? [];
|
|
79
|
+
return files.filter(
|
|
80
|
+
function selectedOfflineFile(file) {
|
|
81
|
+
function includedPath(selection) {
|
|
82
|
+
return selectedPath(file, selection);
|
|
83
|
+
}
|
|
84
|
+
function excludedPath(selection) {
|
|
85
|
+
return selectedPath(file, selection);
|
|
86
|
+
}
|
|
87
|
+
return (include.length === 0 || include.some(includedPath))
|
|
88
|
+
&& !exclude.some(excludedPath);
|
|
89
|
+
}
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function resourceUrl(basePath, file) {
|
|
94
|
+
const encoded = file.split('/').map(
|
|
95
|
+
function encodePathSegment(segment) {
|
|
96
|
+
return encodeURIComponent(segment);
|
|
97
|
+
}
|
|
98
|
+
).join('/');
|
|
99
|
+
return `${basePath.endsWith('/') ? basePath : `${basePath}/`}${encoded}`;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function json(value) {
|
|
103
|
+
return `${JSON.stringify(value, null, 2)}\n`;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function htmlAttribute(value) {
|
|
107
|
+
return value.replaceAll('&', '&').replaceAll('"', '"').replaceAll('<', '<');
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function manifestUrl(value, appBase) {
|
|
111
|
+
if (!is.string(value) || !value || value.startsWith('/')
|
|
112
|
+
|| /^[A-Za-z][A-Za-z0-9+.-]*:/u.test(value) || /^\s/u.test(value)) {
|
|
113
|
+
return value;
|
|
114
|
+
}
|
|
115
|
+
const base = new URL(appBase, 'https://arcane.invalid/');
|
|
116
|
+
const resolved = new URL(value, base);
|
|
117
|
+
const relative = `${resolved.pathname}${resolved.search}${resolved.hash}`;
|
|
118
|
+
return appBase.startsWith('/') ? relative : `.${relative}`;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function manifestImages(images, appBase) {
|
|
122
|
+
if (!is.array(images)) return images;
|
|
123
|
+
return images.map(
|
|
124
|
+
function mapManifestImage(image) {
|
|
125
|
+
if (image === null || !is.object(image) || is.array(image)) return image;
|
|
126
|
+
if (!Object.hasOwn(image, 'src')) return {...image};
|
|
127
|
+
const url = manifestUrl(image.src, appBase);
|
|
128
|
+
return {...image, src: versionAssetUrl(url, null)};
|
|
129
|
+
}
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function applicationManifest(value, appBase) {
|
|
134
|
+
const manifest = {...value};
|
|
135
|
+
for (const field of ['id', 'start_url', 'scope']) {
|
|
136
|
+
if (Object.hasOwn(manifest, field)) {
|
|
137
|
+
manifest[field] = manifestUrl(manifest[field], appBase);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
for (const field of ['icons', 'screenshots']) {
|
|
141
|
+
if (Object.hasOwn(manifest, field)) {
|
|
142
|
+
manifest[field] = manifestImages(manifest[field], appBase);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
if (is.array(manifest.shortcuts)) {
|
|
146
|
+
manifest.shortcuts = manifest.shortcuts.map(
|
|
147
|
+
function mapManifestShortcut(shortcut) {
|
|
148
|
+
if (shortcut === null || !is.object(shortcut) || is.array(shortcut)) return shortcut;
|
|
149
|
+
const mapped = {...shortcut};
|
|
150
|
+
if (Object.hasOwn(mapped, 'url')) {
|
|
151
|
+
mapped.url = manifestUrl(mapped.url, appBase);
|
|
152
|
+
}
|
|
153
|
+
if (Object.hasOwn(mapped, 'icons')) {
|
|
154
|
+
mapped.icons = manifestImages(mapped.icons, appBase);
|
|
155
|
+
}
|
|
156
|
+
return mapped;
|
|
157
|
+
}
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
return manifest;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function manifestAssets(manifest) {
|
|
164
|
+
const urls = [manifest.start_url];
|
|
165
|
+
function appendImages(images) {
|
|
166
|
+
if (!is.array(images)) return;
|
|
167
|
+
for (const image of images) {
|
|
168
|
+
if (is.string(image?.src)) urls.push(image.src);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
appendImages(manifest.icons);
|
|
172
|
+
appendImages(manifest.screenshots);
|
|
173
|
+
if (is.array(manifest.shortcuts)) {
|
|
174
|
+
for (const shortcut of manifest.shortcuts) {
|
|
175
|
+
if (is.string(shortcut?.url)) urls.push(shortcut.url);
|
|
176
|
+
appendImages(shortcut?.icons);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return urls.filter(
|
|
180
|
+
function manifestUrlString(url) {
|
|
181
|
+
return is.string(url);
|
|
182
|
+
}
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function assetPath(url) {
|
|
187
|
+
const resolved = new URL(url, 'https://arcane.invalid/');
|
|
188
|
+
return `${resolved.origin}${resolved.pathname}`;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export function createPwaArtifacts(
|
|
192
|
+
{
|
|
193
|
+
app,
|
|
194
|
+
sdkVersion,
|
|
195
|
+
pwa,
|
|
196
|
+
files = [],
|
|
197
|
+
assets,
|
|
198
|
+
basePath = './',
|
|
199
|
+
mode = 'release',
|
|
200
|
+
runtimeBase = './arcane/sdk/',
|
|
201
|
+
appBase,
|
|
202
|
+
navigationAliases,
|
|
203
|
+
revision
|
|
204
|
+
} = {}
|
|
205
|
+
) {
|
|
206
|
+
const config = normalizePwaConfig(pwa);
|
|
207
|
+
if (!config?.enabled) return null;
|
|
208
|
+
if (mode !== 'release' && mode !== 'development') {
|
|
209
|
+
throw new TypeError('PWA mode must be release or development.');
|
|
210
|
+
}
|
|
211
|
+
const entryUrl = new URL(app.entry, 'https://arcane.invalid/');
|
|
212
|
+
const applicationBase = appBase ?? (mode === 'development'
|
|
213
|
+
? new URL('./', entryUrl).pathname : './');
|
|
214
|
+
const manifest = applicationManifest(
|
|
215
|
+
{
|
|
216
|
+
id: applicationBase,
|
|
217
|
+
name: app.displayName,
|
|
218
|
+
short_name: app.displayName,
|
|
219
|
+
start_url: app.entry,
|
|
220
|
+
scope: applicationBase,
|
|
221
|
+
display: 'standalone',
|
|
222
|
+
...config.manifest
|
|
223
|
+
},
|
|
224
|
+
applicationBase
|
|
225
|
+
);
|
|
226
|
+
const generatedAssets = [
|
|
227
|
+
PWA_MANIFEST_NAME,
|
|
228
|
+
PWA_OFFLINE_MANIFEST_NAME,
|
|
229
|
+
PWA_BOOTSTRAP_NAME
|
|
230
|
+
].map(
|
|
231
|
+
function generatedResource(file) {
|
|
232
|
+
return resourceUrl(basePath, file);
|
|
233
|
+
}
|
|
234
|
+
);
|
|
235
|
+
const selectedFiles = selectPwaFiles(files, config);
|
|
236
|
+
const selectedAssets = [
|
|
237
|
+
...selectedFiles.map(
|
|
238
|
+
function packagedResource(file) {
|
|
239
|
+
return resourceUrl(basePath, file);
|
|
240
|
+
}
|
|
241
|
+
),
|
|
242
|
+
...(assets ?? []),
|
|
243
|
+
app.entry
|
|
244
|
+
];
|
|
245
|
+
const selectedPaths = new Set(
|
|
246
|
+
selectedAssets.map(assetPath)
|
|
247
|
+
);
|
|
248
|
+
// Manifest URLs may retain functional queries. Cache those variants only
|
|
249
|
+
// when their resource already belongs to the selected application inventory.
|
|
250
|
+
const metadataAssets = manifestAssets(manifest).filter(
|
|
251
|
+
function selectedManifestAsset(url) {
|
|
252
|
+
return selectedPaths.has(
|
|
253
|
+
assetPath(url)
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
);
|
|
257
|
+
const workerUrl = resourceUrl(basePath, PWA_WORKER_NAME);
|
|
258
|
+
const workerPath = new URL(workerUrl, 'https://arcane.invalid/').pathname;
|
|
259
|
+
const allAssets = new Set(
|
|
260
|
+
[
|
|
261
|
+
...selectedAssets,
|
|
262
|
+
...metadataAssets,
|
|
263
|
+
...generatedAssets
|
|
264
|
+
]
|
|
265
|
+
);
|
|
266
|
+
const offlineManifest = {
|
|
267
|
+
schemaVersion: 1,
|
|
268
|
+
appId: app.id,
|
|
269
|
+
appVersion: app.version,
|
|
270
|
+
sdkVersion,
|
|
271
|
+
revision: revision ?? (mode === 'development' ? 'development' : randomUUID()),
|
|
272
|
+
mode,
|
|
273
|
+
assets: [...allAssets].filter(
|
|
274
|
+
function excludeWorker(url) {
|
|
275
|
+
return new URL(url, 'https://arcane.invalid/').pathname !== workerPath;
|
|
276
|
+
}
|
|
277
|
+
),
|
|
278
|
+
navigationAliases: navigationAliases ?? {[basePath]: app.entry}
|
|
279
|
+
};
|
|
280
|
+
const bootstrap = `import {registerPwa} from ${JSON.stringify(`${runtimeBase}pwa.mjs`)};
|
|
281
|
+
|
|
282
|
+
const controller = registerPwa(
|
|
283
|
+
{
|
|
284
|
+
workerUrl: new URL('./${PWA_WORKER_NAME}', import.meta.url).href,
|
|
285
|
+
scope: new URL('./', import.meta.url).href
|
|
286
|
+
}
|
|
287
|
+
);
|
|
288
|
+
|
|
289
|
+
controller.ready.catch(
|
|
290
|
+
function reportPwaRegistrationFailure(error) {
|
|
291
|
+
console.error('Arcane PWA registration failed:', error);
|
|
292
|
+
}
|
|
293
|
+
);
|
|
294
|
+
`;
|
|
295
|
+
const entryAssets = {manifest: PWA_MANIFEST_NAME, bootstrap: PWA_BOOTSTRAP_NAME};
|
|
296
|
+
const manifestHref = resourceUrl(basePath, entryAssets.manifest);
|
|
297
|
+
const bootstrapHref = resourceUrl(basePath, entryAssets.bootstrap);
|
|
298
|
+
return {
|
|
299
|
+
manifest,
|
|
300
|
+
offlineManifest,
|
|
301
|
+
files: [
|
|
302
|
+
{path: PWA_MANIFEST_NAME, content: json(manifest)},
|
|
303
|
+
{path: PWA_OFFLINE_MANIFEST_NAME, content: json(offlineManifest)},
|
|
304
|
+
{path: PWA_WORKER_NAME, content: createPwaWorkerScript(offlineManifest)},
|
|
305
|
+
{path: PWA_BOOTSTRAP_NAME, content: bootstrap}
|
|
306
|
+
],
|
|
307
|
+
entryAssets,
|
|
308
|
+
entryMarkup: `<link rel="manifest" href="${htmlAttribute(manifestHref)}">\n`
|
|
309
|
+
+ `<script type="module" async data-arcane-pwa src="${htmlAttribute(bootstrapHref)}"></script>\n`
|
|
310
|
+
};
|
|
311
|
+
}
|
package/src/targets/index.mjs
CHANGED
package/src/toolchain.mjs
CHANGED