arcane-os 0.40.0 → 0.40.1
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 +1 -1
- package/browser-runtime/ai/digitalocean-speech.mjs +59 -19
- package/browser-runtime/ai/speech-worker-runtime.mjs +2 -3
- package/docs/reference/ai/browser-speech.md +15 -2
- package/docs/reference/pwa.md +16 -1
- package/docs/reference/runtime-components.md +14 -4
- package/package.json +1 -1
- package/runtime/arcane/components/speech.html +17 -18
- package/src/dev-server.mjs +19 -11
- package/src/import-map.mjs +20 -1
- package/src/packager/core.mjs +8 -5
- package/src/toolchain.mjs +11 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.40.1
|
|
4
|
+
|
|
5
|
+
- Include complete selected local resource URL variants in generated static
|
|
6
|
+
PWA inventories. Reuse the source resource graph, resolve shared component
|
|
7
|
+
styles and modules against their runtime root, and retain speech worker mode
|
|
8
|
+
queries. Application selection, source content and output locations remain
|
|
9
|
+
unchanged; regenerate through the public import-map workflow to receive the
|
|
10
|
+
corrected offline inventory.
|
|
11
|
+
- Space DigitalOcean FAL inference request starts by one second while retaining
|
|
12
|
+
four concurrent jobs and ordered playback. Retry a readable rate-limit
|
|
13
|
+
rejection once, honoring longer readable Retry-After values, and retry a
|
|
14
|
+
same-job status network failure once. An ambiguous submission network failure
|
|
15
|
+
is surfaced without submitting another potentially paid job.
|
|
16
|
+
- Preserve the user's enabled speech intent after a synthesis, playback or
|
|
17
|
+
provider lifecycle failure. A failed job leaves unrelated queued jobs intact;
|
|
18
|
+
explicit Retry, Load, Mute and Cancel remain user-controlled.
|
|
19
|
+
|
|
3
20
|
## 0.40.0
|
|
4
21
|
|
|
5
22
|
- Add optional `arcane-package.json` `outputDirectory` to select the final
|
package/README.md
CHANGED
|
@@ -19,7 +19,7 @@ version-locked SDK runtime, while an integrated Arcane checkout uses its live
|
|
|
19
19
|
`arcane/` runtime. Both profiles share the theme, packaging, event, cancellation,
|
|
20
20
|
and browser run contracts while retaining their selected application layout.
|
|
21
21
|
|
|
22
|
-
This checkout defines the `0.40.
|
|
22
|
+
This checkout defines the `0.40.1` SDK contract. Applications pin one exact npm
|
|
23
23
|
version and lockfile; registry state is deliberately not baked into application
|
|
24
24
|
artifacts.
|
|
25
25
|
|
|
@@ -113,6 +113,7 @@ export function createDigitalOceanFalTTSProvider({
|
|
|
113
113
|
let generation = 0;
|
|
114
114
|
let unloadOperation = null;
|
|
115
115
|
let disposeOperation = null;
|
|
116
|
+
let nextInferenceStart = 0;
|
|
116
117
|
|
|
117
118
|
function authority() {
|
|
118
119
|
return {
|
|
@@ -214,12 +215,62 @@ export function createDigitalOceanFalTTSProvider({
|
|
|
214
215
|
};
|
|
215
216
|
}
|
|
216
217
|
|
|
218
|
+
async function fetchInference(url, options, operation) {
|
|
219
|
+
const signal = options.signal;
|
|
220
|
+
let retryError = null;
|
|
221
|
+
while (true) {
|
|
222
|
+
// Space request starts across jobs, not their completion. Remote jobs
|
|
223
|
+
// still run concurrently and the runtime retains ordered playback.
|
|
224
|
+
while (true) {
|
|
225
|
+
assertNotAborted(signal);
|
|
226
|
+
const delay = nextInferenceStart - Date.now();
|
|
227
|
+
if (delay <= 0) break;
|
|
228
|
+
await waitForStatus(delay, signal);
|
|
229
|
+
}
|
|
230
|
+
nextInferenceStart = Date.now() + POLL_INTERVAL_MS;
|
|
231
|
+
let response;
|
|
232
|
+
try {
|
|
233
|
+
response = await fetchImpl(url, options);
|
|
234
|
+
} catch (error) {
|
|
235
|
+
assertNotAborted(signal);
|
|
236
|
+
// A lost POST response may represent an accepted paid job. Only
|
|
237
|
+
// an identified job's status read can retry a network failure.
|
|
238
|
+
if (!retryError && options.method === 'GET' && error instanceof TypeError) {
|
|
239
|
+
nextInferenceStart = Math.max(nextInferenceStart, Date.now() + POLL_INTERVAL_MS);
|
|
240
|
+
retryError = error;
|
|
241
|
+
console.warn('Retrying the cloud speech status read once.', error);
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
throw error;
|
|
245
|
+
}
|
|
246
|
+
assertNotAborted(signal);
|
|
247
|
+
if (response.status === 429) {
|
|
248
|
+
// A readable rejection is safe to retry. Its cooldown also
|
|
249
|
+
// applies to waiting siblings, including when this retry ends.
|
|
250
|
+
const delay = Math.max(POLL_INTERVAL_MS, retryAfterDelay(response) ?? POLL_INTERVAL_MS);
|
|
251
|
+
nextInferenceStart = Math.max(nextInferenceStart, Date.now() + delay);
|
|
252
|
+
}
|
|
253
|
+
try {
|
|
254
|
+
await requireSuccessfulResponse(response, signal, operation);
|
|
255
|
+
} catch (error) {
|
|
256
|
+
assertNotAborted(signal);
|
|
257
|
+
if (!retryError && response.status === 429) {
|
|
258
|
+
retryError = error;
|
|
259
|
+
console.warn(`Retrying the cloud speech ${operation} once.`, error);
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
throw error;
|
|
263
|
+
}
|
|
264
|
+
return response;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
217
268
|
async function synthesize(payload, controller, requestGeneration) {
|
|
218
269
|
const signal = controller.signal;
|
|
219
270
|
const apiKey = await readApiKey(signal);
|
|
220
271
|
assertNotAborted(signal);
|
|
221
272
|
const headers = {Authorization: `Bearer ${apiKey}`};
|
|
222
|
-
const response = await
|
|
273
|
+
const response = await fetchInference(
|
|
223
274
|
INVOKE_URL,
|
|
224
275
|
{
|
|
225
276
|
method: 'POST',
|
|
@@ -235,9 +286,9 @@ export function createDigitalOceanFalTTSProvider({
|
|
|
235
286
|
}
|
|
236
287
|
),
|
|
237
288
|
signal
|
|
238
|
-
}
|
|
289
|
+
},
|
|
290
|
+
'submission'
|
|
239
291
|
);
|
|
240
|
-
await requireSuccessfulResponse(response, signal, 'submission');
|
|
241
292
|
let result = await response.json();
|
|
242
293
|
assertNotAborted(signal);
|
|
243
294
|
const requestId = result?.request_id;
|
|
@@ -249,22 +300,11 @@ export function createDigitalOceanFalTTSProvider({
|
|
|
249
300
|
);
|
|
250
301
|
}
|
|
251
302
|
await waitForStatus(POLL_INTERVAL_MS, signal);
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
);
|
|
258
|
-
assertNotAborted(signal);
|
|
259
|
-
const retryDelay = statusResponse.status === 429
|
|
260
|
-
? retryAfterDelay(statusResponse)
|
|
261
|
-
: null;
|
|
262
|
-
if (retryDelay === null) break;
|
|
263
|
-
// Only status reads may be retried; submitting twice can create two paid jobs.
|
|
264
|
-
await statusResponse.text();
|
|
265
|
-
await waitForStatus(retryDelay, signal);
|
|
266
|
-
} while (true);
|
|
267
|
-
await requireSuccessfulResponse(statusResponse, signal, 'status read');
|
|
303
|
+
const statusResponse = await fetchInference(
|
|
304
|
+
`${INVOKE_URL}/${encodeURIComponent(requestId)}`,
|
|
305
|
+
{method: 'GET', headers, signal},
|
|
306
|
+
'status read'
|
|
307
|
+
);
|
|
268
308
|
result = await statusResponse.json();
|
|
269
309
|
assertNotAborted(signal);
|
|
270
310
|
}
|
|
@@ -844,11 +844,10 @@ function validateConfiguration(configuration, role) {
|
|
|
844
844
|
function nestedWorkerUrl(role) {
|
|
845
845
|
const url = new URL(
|
|
846
846
|
role === "stt"
|
|
847
|
-
? "./browser-whisper-worker.mjs"
|
|
848
|
-
: "./browser-kokoro-worker.mjs",
|
|
847
|
+
? "./browser-whisper-worker.mjs?arcaneSpeechWorkerMode=artifact-module-worker"
|
|
848
|
+
: "./browser-kokoro-worker.mjs?arcaneSpeechWorkerMode=artifact-module-worker",
|
|
849
849
|
import.meta.url,
|
|
850
850
|
);
|
|
851
|
-
url.searchParams.set("arcaneSpeechWorkerMode", "artifact-module-worker");
|
|
852
851
|
return url;
|
|
853
852
|
}
|
|
854
853
|
|
|
@@ -56,8 +56,21 @@ punctuation boundary should submit promptly. This is incremental playback of
|
|
|
56
56
|
completed synthesis jobs, not a live audio stream from DigitalOcean.
|
|
57
57
|
|
|
58
58
|
The transport submits `POST https://inference.do-ai.run/v1/async-invoke`, then
|
|
59
|
-
reads that job until its completed `output.audio.url` is available.
|
|
60
|
-
|
|
59
|
+
reads that job until its completed `output.audio.url` is available. One provider
|
|
60
|
+
spaces inference request starts at least one second apart across its concurrent
|
|
61
|
+
jobs; it does not wait for one remote job to finish before starting another.
|
|
62
|
+
A readable `429` rejection permits one retry of the same submission or status
|
|
63
|
+
request after at least one second. A longer readable `Retry-After` postpones
|
|
64
|
+
all pending inference starts for that provider. A failed status Fetch may also
|
|
65
|
+
retry once for the same accepted job. Repeated failures surface their complete
|
|
66
|
+
diagnostics without an endless retry loop; a failed job does not cancel siblings.
|
|
67
|
+
Retry diagnostics remain in the developer console, outside conversation history.
|
|
68
|
+
|
|
69
|
+
An ambiguous submission network failure is not automatically resubmitted: a
|
|
70
|
+
browser cannot distinguish a rejected preflight from an accepted paid job whose
|
|
71
|
+
response was lost. This contract has no submission idempotency mechanism, and
|
|
72
|
+
unreadable response headers cannot supply a server cooldown. Pacing reduces local
|
|
73
|
+
request bursts; it does not claim to free upstream concurrency slots or fix CORS.
|
|
61
74
|
The audio download never receives the inference Authorization header.
|
|
62
75
|
The selected multilingual-v2 service supports speed `0.7` through `1.2`;
|
|
63
76
|
unsupported speeds reject rather than being silently clamped. Complete
|
package/docs/reference/pwa.md
CHANGED
|
@@ -93,6 +93,16 @@ The application entry and generated PWA shell records are retained. Select the
|
|
|
93
93
|
resources needed by every offline page, including its shared modules and styles.
|
|
94
94
|
The worker script itself is never an application cache entry.
|
|
95
95
|
|
|
96
|
+
For direct installed-package root applications, public `arcane import-map`
|
|
97
|
+
generation follows the same source resource graph as `arcane dev` before writing
|
|
98
|
+
the static offline inventory. Imports from unvisited pages, shared runtime
|
|
99
|
+
modules, component styles, and statically declared Worker URLs retain their
|
|
100
|
+
authored query values. SDK component `./arcane/` resource attributes resolve from
|
|
101
|
+
that component's actual runtime root, including installed package aliases and
|
|
102
|
+
nested component directories; ordinary application document bases keep their
|
|
103
|
+
normal meaning. The graph selects resources already present in the configured
|
|
104
|
+
routes. It does not invent resource paths or enumerate user-specific page URLs.
|
|
105
|
+
|
|
96
106
|
These settings describe published application resources. They do not select
|
|
97
107
|
user uploads, provider responses, live API requests, saved conversations,
|
|
98
108
|
preferences, IndexedDB, OPFS, or a model owner's download/cache lifecycle.
|
|
@@ -180,7 +190,7 @@ without loading certificates; see [explicit HTTP development](cli.md#explicit-ht
|
|
|
180
190
|
`--public` selects the IPv4 wildcard bind address; it does not enable PWA
|
|
181
191
|
configuration, change manifest metadata, or determine browser installability.
|
|
182
192
|
|
|
183
|
-
|
|
193
|
+
Live source inventory work begins when the browser requests the worker or current
|
|
184
194
|
offline manifest, after the page can start. It traverses the selected route
|
|
185
195
|
inventory once for that request, follows page and runtime resource references
|
|
186
196
|
to retain selected query variants, and shares an in-flight traversal with
|
|
@@ -189,6 +199,11 @@ document corpus bodies remain under their existing owner. It does not rebuild
|
|
|
189
199
|
the application. Installation metadata and bootstrap requests reuse the current
|
|
190
200
|
generated bundle.
|
|
191
201
|
|
|
202
|
+
An explicit root import-map refresh also performs that traversal to produce the
|
|
203
|
+
committed static PWA files. It opens no server or listener. Its generated
|
|
204
|
+
inventory is available to ordinary static hosting without first visiting each
|
|
205
|
+
page through the development server.
|
|
206
|
+
|
|
192
207
|
The SDK owns version information in `arcane-offline.json`:
|
|
193
208
|
|
|
194
209
|
| Field | Owner and update rule |
|
|
@@ -1566,8 +1566,10 @@ boundary applies to STT activation, transcription, TTS lifecycle, synthesis,
|
|
|
1566
1566
|
and playback failures.
|
|
1567
1567
|
|
|
1568
1568
|
`reportTTSError()` recognizes `synthesis`, `decode`, `playback-start`, and
|
|
1569
|
-
`playback-resume`. It
|
|
1570
|
-
|
|
1569
|
+
`playback-resume`. It preserves the user's mute choice, sticky provider
|
|
1570
|
+
readiness, and accepted sibling speech jobs. The AI runtime owns settlement of
|
|
1571
|
+
the failed job; reporting an error neither stops the queue nor retries it. The
|
|
1572
|
+
component emits `speech-synthesis-error` with the exact boundary,
|
|
1571
1573
|
stable reason/code, generic user-safe status, and complete Error in the local
|
|
1572
1574
|
event detail.
|
|
1573
1575
|
|
|
@@ -1600,8 +1602,16 @@ same capture operation id rather than inventing a second correlation boundary.
|
|
|
1600
1602
|
User Unmute calls the shared `AI.setSpeechMuted(false)` lifecycle owner before
|
|
1601
1603
|
or with publishing the TTS load intent, so the runtime can legally load TTS.
|
|
1602
1604
|
Configured `initialMuted:false` records that same unmute intent even when the
|
|
1603
|
-
TTS route is still unselected or loading
|
|
1604
|
-
|
|
1605
|
+
TTS route is still unselected or loading. Public `muted` expresses the user's
|
|
1606
|
+
choice; role availability and the AI runtime still prevent playback until the
|
|
1607
|
+
selected role reaches `ready`, when the waiting intent is applied. A failed
|
|
1608
|
+
load ends its activation attempt without changing that choice or starting
|
|
1609
|
+
another attempt. Retry voice explicitly starts a new attempt, including when
|
|
1610
|
+
the preserved choice is unmuted; an unloaded selected voice offers Load voice.
|
|
1611
|
+
When a role returns to ready, permitted queued playback resumes through the
|
|
1612
|
+
existing AI owner without repeating activation or replaying failed jobs.
|
|
1613
|
+
Synthesis, decode, and playback failures also
|
|
1614
|
+
preserve that choice and do not automatically replay failed speech.
|
|
1605
1615
|
Mute calls `AI.setSpeechMuted(true)`, stops playback, cancels active TTS work,
|
|
1606
1616
|
and unloads the selected TTS role. Lifecycle failures remain visible through
|
|
1607
1617
|
`speech-tts-lifecycle-error` and sticky role state.
|
package/package.json
CHANGED
|
@@ -518,7 +518,7 @@
|
|
|
518
518
|
requestUserMute();
|
|
519
519
|
} else {
|
|
520
520
|
pendingUnmute = true;
|
|
521
|
-
host.muted =
|
|
521
|
+
host.muted = false;
|
|
522
522
|
if (!settleSuccessfulUnmute()) {
|
|
523
523
|
synchronizeAIMutedState();
|
|
524
524
|
continuePendingUnmute();
|
|
@@ -567,21 +567,24 @@
|
|
|
567
567
|
if (sttBecameUnready) {
|
|
568
568
|
cancelSTTOperation('runtime-unready');
|
|
569
569
|
}
|
|
570
|
-
if (ttsBecameUnready) {
|
|
570
|
+
if (ttsBecameUnready && ttsRole.state !== 'error') {
|
|
571
571
|
pendingUnmute = false;
|
|
572
|
-
host.muted = true;
|
|
573
572
|
stopTTSPlayback();
|
|
574
573
|
}
|
|
575
|
-
if (ttsRole.state === 'error'
|
|
574
|
+
if (ttsRole.state === 'error') {
|
|
576
575
|
pendingUnmute = false;
|
|
577
|
-
host.muted = true;
|
|
578
|
-
stopTTSPlayback();
|
|
579
576
|
}
|
|
580
577
|
|
|
581
|
-
settleSuccessfulUnmute();
|
|
578
|
+
const unmuteSettled = settleSuccessfulUnmute();
|
|
582
579
|
continuePendingUnmute();
|
|
583
580
|
|
|
584
581
|
synchronizeAIMutedState();
|
|
582
|
+
if (!unmuteSettled
|
|
583
|
+
&& previousTTSRole.state !== 'ready'
|
|
584
|
+
&& ttsRole.state === 'ready'
|
|
585
|
+
&& !host.muted) {
|
|
586
|
+
resumeTTSPlayback();
|
|
587
|
+
}
|
|
585
588
|
renderControls();
|
|
586
589
|
renderStatus();
|
|
587
590
|
}
|
|
@@ -720,6 +723,9 @@
|
|
|
720
723
|
if (pendingTTSIntent === 'load') {
|
|
721
724
|
label = 'Cancel voice load';
|
|
722
725
|
description = 'Cancel loading the selected voice.';
|
|
726
|
+
} else if (!host.muted) {
|
|
727
|
+
label = 'Load voice';
|
|
728
|
+
description = 'Load the selected voice.';
|
|
723
729
|
}
|
|
724
730
|
} else if (ttsRole.state === 'error') {
|
|
725
731
|
disabled = !providerSelected;
|
|
@@ -960,7 +966,7 @@
|
|
|
960
966
|
return;
|
|
961
967
|
}
|
|
962
968
|
|
|
963
|
-
if (host.muted) {
|
|
969
|
+
if (host.muted || ['unloaded', 'error'].includes(ttsRole.state)) {
|
|
964
970
|
void requestUserUnmute();
|
|
965
971
|
} else {
|
|
966
972
|
requestUserMute();
|
|
@@ -976,7 +982,7 @@
|
|
|
976
982
|
}
|
|
977
983
|
const wasMuted = host.muted;
|
|
978
984
|
pendingUnmute = false;
|
|
979
|
-
const shouldUnload = !wasMuted
|
|
985
|
+
const shouldUnload = (!wasMuted && is.function(globalThis.ai?.setSpeechMuted))
|
|
980
986
|
|| pendingTTSIntent === 'load'
|
|
981
987
|
|| ['loading', 'ready', 'error'].includes(ttsRole.state);
|
|
982
988
|
host.muted = true;
|
|
@@ -1004,9 +1010,9 @@
|
|
|
1004
1010
|
) {
|
|
1005
1011
|
return false;
|
|
1006
1012
|
}
|
|
1013
|
+
host.muted = false;
|
|
1007
1014
|
if (!is.function(globalThis.ai?.setSpeechMuted)) {
|
|
1008
1015
|
pendingUnmute = true;
|
|
1009
|
-
host.muted = true;
|
|
1010
1016
|
renderControls();
|
|
1011
1017
|
renderStatus();
|
|
1012
1018
|
return false;
|
|
@@ -1021,8 +1027,7 @@
|
|
|
1021
1027
|
&& ['unloaded', 'loading', 'error'].includes(ttsRole.state)
|
|
1022
1028
|
);
|
|
1023
1029
|
if (!canRequestLoad) {
|
|
1024
|
-
|
|
1025
|
-
stopTTSPlayback();
|
|
1030
|
+
pendingUnmute = false;
|
|
1026
1031
|
reportTTSLifecycleError(
|
|
1027
1032
|
new Error('No selected speech provider is available to load.'),
|
|
1028
1033
|
'load'
|
|
@@ -1038,8 +1043,6 @@
|
|
|
1038
1043
|
}
|
|
1039
1044
|
if (!requested) {
|
|
1040
1045
|
pendingUnmute = false;
|
|
1041
|
-
host.muted = true;
|
|
1042
|
-
stopTTSPlayback();
|
|
1043
1046
|
renderControls();
|
|
1044
1047
|
renderStatus();
|
|
1045
1048
|
return false;
|
|
@@ -1117,8 +1120,6 @@
|
|
|
1117
1120
|
pendingTTSIntent = null;
|
|
1118
1121
|
if (action === 'load') {
|
|
1119
1122
|
pendingUnmute = false;
|
|
1120
|
-
host.muted = true;
|
|
1121
|
-
stopTTSPlayback();
|
|
1122
1123
|
}
|
|
1123
1124
|
reportTTSLifecycleError(error, action, operationId);
|
|
1124
1125
|
return false;
|
|
@@ -1211,8 +1212,6 @@
|
|
|
1211
1212
|
: 'synthesis';
|
|
1212
1213
|
const failure = failures[normalizedBoundary];
|
|
1213
1214
|
const message = visibleErrorMessage(error, failure.message);
|
|
1214
|
-
host.muted = true;
|
|
1215
|
-
stopTTSPlayback();
|
|
1216
1215
|
localStatus = {
|
|
1217
1216
|
message,
|
|
1218
1217
|
tone: 'error'
|
package/src/dev-server.mjs
CHANGED
|
@@ -13,7 +13,7 @@ import {APP_CONFIG_NAME, validateAppConfig} from './packager/core.mjs';
|
|
|
13
13
|
import {createEventQueue} from './event-queue.mjs';
|
|
14
14
|
import {
|
|
15
15
|
applyPwaEntryReferences,inspectImportMapHtml,MANAGED_IMPORT_MAP_ATTRIBUTE,
|
|
16
|
-
readWorkspaceAssetVersion,rewriteAssetReferences,versionAssetUrl
|
|
16
|
+
readWorkspaceAssetVersion,resolveAssetReference,rewriteAssetReferences,versionAssetUrl
|
|
17
17
|
} from './import-map.mjs';
|
|
18
18
|
import {createPwaArtifacts,selectPwaFiles} from './pwa.mjs';
|
|
19
19
|
|
|
@@ -658,14 +658,15 @@ async function sourcePwaAssets(routeSet, mappings, signal, resourceUrls, resourc
|
|
|
658
658
|
}
|
|
659
659
|
}
|
|
660
660
|
}
|
|
661
|
-
for (const
|
|
661
|
+
for (const reference of references) {
|
|
662
|
+
const {url, kind} = reference;
|
|
662
663
|
if (kind === 'import' && !/^(?:\.{1,2}\/|\/)/u.test(url)) continue;
|
|
663
664
|
let target;
|
|
664
|
-
let base;
|
|
665
665
|
try {
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
666
|
+
target = resolveAssetReference(
|
|
667
|
+
{...reference, url: versionAssetUrl(url, null)},
|
|
668
|
+
{ownerUrl: current.url, documentUrl, managedMap}
|
|
669
|
+
);
|
|
669
670
|
} catch {
|
|
670
671
|
// Non-URL source values retain their authored behavior.
|
|
671
672
|
continue;
|
|
@@ -695,6 +696,11 @@ async function sourcePwaAssets(routeSet, mappings, signal, resourceUrls, resourc
|
|
|
695
696
|
return [...selectedUrls].sort();
|
|
696
697
|
}
|
|
697
698
|
|
|
699
|
+
export async function collectSourcePwaAssets({workspaceRoot,appId,signal,onEvent}={}){
|
|
700
|
+
const routes=await sourceRoutes(workspaceRoot,appId,{signal,onEvent});
|
|
701
|
+
return sourcePwaAssets(routes,routes.mappings,signal,new Set(),new Set());
|
|
702
|
+
}
|
|
703
|
+
|
|
698
704
|
function closeDevelopmentListeners(fileServer, tlsServer) {
|
|
699
705
|
const closeTls = tlsServer ? new Promise(
|
|
700
706
|
function closeRawTlsListener(resolve, reject) {
|
|
@@ -1236,13 +1242,16 @@ async function startOwnedDevServer({
|
|
|
1236
1242
|
.includes(request.headers['sec-fetch-dest']);
|
|
1237
1243
|
const rewrite=runtimeResource||entryDocument||browserResource||managedMap
|
|
1238
1244
|
||resourcePaths.has(target.path);
|
|
1239
|
-
const onReference = function observeServedResource(
|
|
1245
|
+
const onReference = function observeServedResource(reference) {
|
|
1246
|
+
const {url,kind,baseKind}=reference;
|
|
1240
1247
|
if (pwaEnabled && kind !== 'fetch'
|
|
1241
1248
|
&& !(kind === 'import' && !/^(?:\.{1,2}\/|\/)/u.test(url))) {
|
|
1242
1249
|
try {
|
|
1243
1250
|
const documentUrl = new URL(target.path, 'http://arcane.invalid');
|
|
1244
|
-
const
|
|
1245
|
-
|
|
1251
|
+
const resource = resolveAssetReference(
|
|
1252
|
+
{...reference,url:versionAssetUrl(url,null)},
|
|
1253
|
+
{ownerUrl:documentUrl}
|
|
1254
|
+
);
|
|
1246
1255
|
if (resource.origin === documentUrl.origin) {
|
|
1247
1256
|
pwaResourceUrls.add(`${resource.pathname}${resource.search}`);
|
|
1248
1257
|
}
|
|
@@ -1254,8 +1263,7 @@ async function startOwnedDevServer({
|
|
|
1254
1263
|
||(kind==='import'&&!/^(?:\.{1,2}\/|\/)/u.test(url)))return;
|
|
1255
1264
|
try{
|
|
1256
1265
|
const documentUrl=new URL(target.path,'http://arcane.invalid');
|
|
1257
|
-
const
|
|
1258
|
-
const resource=new URL(url,base);
|
|
1266
|
+
const resource=resolveAssetReference(reference,{ownerUrl:documentUrl});
|
|
1259
1267
|
if(resource.origin===documentUrl.origin
|
|
1260
1268
|
&&/\.(?:m?js|html?|css)$/iu.test(resource.pathname)){
|
|
1261
1269
|
resourcePaths.add(decodeURIComponent(resource.pathname));
|
package/src/import-map.mjs
CHANGED
|
@@ -1281,6 +1281,23 @@ export function rewriteAssetReferences(source,{filePath,version=SDK_VERSION,onRe
|
|
|
1281
1281
|
return source;
|
|
1282
1282
|
}
|
|
1283
1283
|
|
|
1284
|
+
export function resolveAssetReference({url,baseHref,baseKind},{ownerUrl,documentUrl=ownerUrl,managedMap=false}){
|
|
1285
|
+
if(baseKind==='component-runtime'&&url.startsWith('./arcane/')){
|
|
1286
|
+
const owner=new URL(ownerUrl);
|
|
1287
|
+
const marker='/arcane/components/';
|
|
1288
|
+
const componentIndex=owner.pathname.lastIndexOf(marker);
|
|
1289
|
+
if(componentIndex>=0){
|
|
1290
|
+
// HTMLImport resolves these component attributes against their
|
|
1291
|
+
// installed runtime, independently of the host document's base.
|
|
1292
|
+
const runtimeRoot=new URL(owner.pathname.slice(0,componentIndex+'/arcane/'.length),owner.origin);
|
|
1293
|
+
return new URL(url.slice('./arcane/'.length),runtimeRoot);
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
const base=baseHref?new URL(baseHref,ownerUrl)
|
|
1297
|
+
:managedMap||baseKind==='document'?documentUrl:ownerUrl;
|
|
1298
|
+
return new URL(url,base);
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1284
1301
|
function registerSpecifier(registry,specifier,target){
|
|
1285
1302
|
registry.set(specifier,{specifier,target});
|
|
1286
1303
|
}
|
|
@@ -2071,7 +2088,9 @@ function htmlReferenceEdits(source,version,onReference){
|
|
|
2071
2088
|
:element.tag==='link'&&relationships.includes('preload')
|
|
2072
2089
|
&&['script','worker','style'].includes(destination)?destination==='style'?'style':'script':'asset';
|
|
2073
2090
|
const view=htmlAttributeView(original);
|
|
2074
|
-
if(view)reportAssetReference(reportHtmlReference,view.decoded,kind
|
|
2091
|
+
if(view)reportAssetReference(reportHtmlReference,view.decoded,kind,
|
|
2092
|
+
(name==='href'||name==='src')&&view.decoded.startsWith('./arcane/')
|
|
2093
|
+
?'component-runtime':undefined);
|
|
2075
2094
|
const value=versionHtmlAttribute(original,version);
|
|
2076
2095
|
if(value===original)continue;
|
|
2077
2096
|
const position=attributes.positions.get(name);
|
package/src/packager/core.mjs
CHANGED
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
applyPwaEntryReferences,
|
|
20
20
|
inspectImportMapHtml,
|
|
21
21
|
readWorkspaceAssetVersion,
|
|
22
|
+
resolveAssetReference,
|
|
22
23
|
rewriteAssetReferences,
|
|
23
24
|
versionAssetUrl
|
|
24
25
|
} from '../import-map.mjs';
|
|
@@ -967,17 +968,19 @@ async function packageWithContext(context,options={}){
|
|
|
967
968
|
resource={references,...(pwaDocumentSources.has(relative)?{content}:{})};
|
|
968
969
|
resources.set(relative,resource);
|
|
969
970
|
}
|
|
970
|
-
for(const
|
|
971
|
+
for(const reference of resource.references){
|
|
972
|
+
const {url,kind,baseHref}=reference;
|
|
971
973
|
const traversable=kind!=='fetch'&&(kind!=='asset'||/\.css(?:[?#]|$)/iu.test(url))
|
|
972
974
|
&&(kind!=='import'||/^(?:\.{1,2}\/|\/)/u.test(url));
|
|
973
975
|
if(!pwaEnabled&&!traversable)continue;
|
|
974
976
|
if(kind==='import'&&!/^(?:\.{1,2}\/|\/)/u.test(url))continue;
|
|
975
977
|
try{
|
|
976
978
|
const ownerUrl=new URL(packageResourceUrl(relative),entryUrl.origin);
|
|
977
|
-
const
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
979
|
+
const target=resolveAssetReference(reference,{
|
|
980
|
+
ownerUrl,
|
|
981
|
+
documentUrl:current.documentUrl,
|
|
982
|
+
managedMap:path.posix.basename(relative)==='arcane.importmap.json'
|
|
983
|
+
});
|
|
981
984
|
if(target.origin===entryUrl.origin){
|
|
982
985
|
const file=decodeURIComponent(target.pathname).replace(/^\//u,'');
|
|
983
986
|
if(pwaEnabled&&offlineInventory.has(file)){
|
package/src/toolchain.mjs
CHANGED
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
validateWorkspace
|
|
12
12
|
} from './workspace.mjs';
|
|
13
13
|
import {loadArcaneIntegratedProvider} from './integrated-provider-loader.mjs';
|
|
14
|
-
import {startDevServer} from './dev-server.mjs';
|
|
14
|
+
import {collectSourcePwaAssets,startDevServer} from './dev-server.mjs';
|
|
15
15
|
import {applyPwaEntryReferences,generateImportMap,readApplicationTestImportMapContext} from './import-map.mjs';
|
|
16
16
|
import {createPwaArtifacts} from './pwa.mjs';
|
|
17
17
|
import {readInstalledSdkLayout} from './sdk-runtime-layout.mjs';
|
|
@@ -337,11 +337,21 @@ async function refreshRootApplicationFiles(prepared,inspected,importMap,{signal,
|
|
|
337
337
|
...inspected.files.filter(file=>file!=='index.html'||manifest.include.includes('index.html')),
|
|
338
338
|
importMap.artifactRelativePath
|
|
339
339
|
])];
|
|
340
|
+
const sourceOrigin='http://arcane.invalid';
|
|
341
|
+
const selectedPaths=new Set(files.map(function selectedStaticPath(file){
|
|
342
|
+
return new URL(`/${file.split('/').map(encodeURIComponent).join('/')}`,sourceOrigin).pathname;
|
|
343
|
+
}));
|
|
344
|
+
const assets=installed?.direct&&manifest.pwa?.enabled
|
|
345
|
+
?(await collectSourcePwaAssets({workspaceRoot,appId,signal,onEvent})).filter(function selectedStaticVariant(value){
|
|
346
|
+
const url=new URL(value,sourceOrigin);
|
|
347
|
+
return url.origin===sourceOrigin&&selectedPaths.has(url.pathname);
|
|
348
|
+
}):[];
|
|
340
349
|
const pwa=installed?.direct&&manifest.pwa?.enabled?createPwaArtifacts({
|
|
341
350
|
app:{id:appId,displayName:manifest.displayName,version:manifest.version,entry},
|
|
342
351
|
sdkVersion:installed.version,
|
|
343
352
|
pwa:manifest.pwa,
|
|
344
353
|
files,
|
|
354
|
+
assets,
|
|
345
355
|
basePath:'/',
|
|
346
356
|
appBase:'/',
|
|
347
357
|
installationId:`/apps/${appId}/`,
|