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 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.0` SDK contract. Applications pin one exact npm
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 fetchImpl(
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
- let statusResponse;
253
- do {
254
- statusResponse = await fetchImpl(
255
- `${INVOKE_URL}/${encodeURIComponent(requestId)}`,
256
- {method: 'GET', headers, signal}
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. Each
60
- segment is submitted once; only status reads retry a `429` with `Retry-After`.
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
@@ -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
- Source inventory work begins when the browser requests the worker or current
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 mutes and stops the failed operation, preserves sticky
1570
- provider readiness, and emits `speech-synthesis-error` with the exact boundary,
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; the component remains publicly muted
1604
- until the selected role reaches `ready`, then applies the preserved intent.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "arcane-os",
3
- "version": "0.40.0",
3
+ "version": "0.40.1",
4
4
  "description": "Arcane OS JavaScript SDK, project-local CLI, browser runtime, and repository-portable application packager.",
5
5
  "type": "module",
6
6
  "main": "./src/index.mjs",
@@ -518,7 +518,7 @@
518
518
  requestUserMute();
519
519
  } else {
520
520
  pendingUnmute = true;
521
- host.muted = true;
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' && (pendingUnmute || !host.muted)) {
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
- host.muted = true;
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'
@@ -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 {url, kind, baseHref, baseKind} of references) {
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
- base = baseHref ? new URL(baseHref, current.url)
667
- : managedMap || baseKind === 'document' ? documentUrl : current.url;
668
- target = new URL(versionAssetUrl(url, null), base);
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({url,kind,baseHref,baseKind}) {
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 base = baseHref ? new URL(baseHref, documentUrl) : documentUrl;
1245
- const resource = new URL(versionAssetUrl(url, null), base);
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 base=baseHref?new URL(baseHref,documentUrl):documentUrl;
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));
@@ -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);
@@ -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 {url,kind,baseHref,baseKind} of resource.references){
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 base=baseHref?new URL(baseHref,ownerUrl)
978
- :baseKind==='document'||path.posix.basename(relative)==='arcane.importmap.json'
979
- ?current.documentUrl:ownerUrl;
980
- const target=new URL(url,base);
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}/`,