arcane-os 0.9.0 → 0.10.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.
@@ -24,6 +24,7 @@ const SDK_BROWSER_SELF_IMPORTS=new Map([
24
24
  ['arcane-os/event-manager',SDK_BROWSER_ENTRY],
25
25
  ['arcane-os/logging','sdk/logging.mjs'],
26
26
  ['arcane-os/browser-device','sdk/browser-device.mjs'],
27
+ ['arcane-os/pwa','sdk/pwa.mjs'],
27
28
  ['arcane-os/speech-text','sdk/speech-text.mjs'],
28
29
  ['arcane-os/ai/browser-wasm',SDK_BROWSER_AI_ENTRY],
29
30
  ['arcane-os/ai/browser-speech',SDK_BROWSER_SPEECH_ENTRY]
@@ -803,7 +804,7 @@ export function versionAssetUrl(value,version=SDK_VERSION){
803
804
  }
804
805
 
805
806
  function assetUrlVersionEdits(value,version){
806
- if(!is.string(value)||!value||!version||value.startsWith('#')
807
+ if(!is.string(value)||!value||(!version&&version!==null)||value.startsWith('#')
807
808
  ||value.startsWith('//')||/^[A-Za-z][A-Za-z0-9+.-]*:/u.test(value)
808
809
  ||/^\s/u.test(value))return [];
809
810
  const fragmentStart=value.indexOf('#');
@@ -811,7 +812,9 @@ function assetUrlVersionEdits(value,version){
811
812
  const queryStart=address.indexOf('?');
812
813
  const pathname=queryStart<0?address:address.slice(0,queryStart);
813
814
  if(!pathname)return [];
814
- const versionValue=encodeURIComponent(String(version));
815
+ const clean=version===null;
816
+ if(clean&&queryStart<0)return [];
817
+ const versionValue=clean?'':encodeURIComponent(String(version));
815
818
  if(queryStart<0)return [{start:address.length,end:address.length,value:`?arcaneVersion=${versionValue}`}];
816
819
  const query=address.slice(queryStart+1);
817
820
  const edits=[];
@@ -824,9 +827,9 @@ function assetUrlVersionEdits(value,version){
824
827
  let decodedKey=key;
825
828
  try{decodedKey=decodeURIComponent(key.replaceAll('+',' '));}
826
829
  catch{decodedKey=key;}
827
- const remove=decodedKey==='v'||(decodedKey==='arcaneVersion'&&versionFound);
830
+ const remove=decodedKey==='v'||(decodedKey==='arcaneVersion'&&(clean||versionFound));
828
831
  parameters.push({start:offset,end:offset+parameter.length,remove});
829
- if(decodedKey==='arcaneVersion'&&!versionFound){
832
+ if(decodedKey==='arcaneVersion'&&!versionFound&&!clean){
830
833
  versionFound=true;
831
834
  edits.push({
832
835
  start:offset+(equals<0?parameter.length:equals+1),
@@ -836,6 +839,9 @@ function assetUrlVersionEdits(value,version){
836
839
  }
837
840
  offset+=parameter.length+1;
838
841
  }
842
+ if(clean&&!parameters.some(function hasRemainingField(parameter){
843
+ return !parameter.remove&&parameter.end>parameter.start;
844
+ }))return [{start:queryStart,end:address.length,value:''}];
839
845
  // Remove adjacent obsolete fields together, including only their separator.
840
846
  // Other field spelling and source-level escapes remain untouched.
841
847
  for(let index=0;index<parameters.length;index+=1){
@@ -851,7 +857,7 @@ function assetUrlVersionEdits(value,version){
851
857
  const lastRetained=parameters.findLast(function retainedParameter(parameter){
852
858
  return !parameter.remove;
853
859
  });
854
- if(!versionFound)edits.push({
860
+ if(!clean&&!versionFound)edits.push({
855
861
  start:address.length,
856
862
  end:address.length,
857
863
  value:`${lastRetained&&lastRetained.end>lastRetained.start?'&':''}arcaneVersion=${versionValue}`
@@ -1161,6 +1167,66 @@ function importMapReferenceEdits(source,version,onReference){
1161
1167
  prependAliases(start,aliases,selectedEdits);
1162
1168
  return selectedEdits;
1163
1169
  }
1170
+ if(version===null){
1171
+ function cleanProperties(properties,{scopes=false}={}){
1172
+ const groups=new Map();
1173
+ const removed=new Set();
1174
+ for(const property of properties){
1175
+ const key=scopes
1176
+ ?versionImportMapUrl(property.key,null)
1177
+ :versionImportMapSpecifier(property.key,null);
1178
+ if(!groups.has(key))groups.set(key,[]);
1179
+ groups.get(key).push(property);
1180
+ }
1181
+ for(const [key,group] of groups){
1182
+ if(group.every(function unchangedKey(property){return property.key===key;}))continue;
1183
+ // An authored clean key owns the target when old generated aliases converge.
1184
+ const selected=group.findLast(function existingCleanKey(property){
1185
+ return property.key===key;
1186
+ })??group.at(-1);
1187
+ for(const property of group){
1188
+ if(property!==selected)removed.add(property);
1189
+ }
1190
+ const edit=stringReferenceEdit(
1191
+ source,
1192
+ selected.keyToken,
1193
+ null,
1194
+ importMapUrlVersionEdits(selected.key,null)
1195
+ );
1196
+ if(edit)edits.push(edit);
1197
+ }
1198
+ for(let index=0;index<properties.length;index+=1){
1199
+ if(!removed.has(properties[index]))continue;
1200
+ const first=index;
1201
+ while(removed.has(properties[index+1]))index+=1;
1202
+ edits.push({
1203
+ start:first===0?properties[first].keyToken.start:properties[first-1].end,
1204
+ end:first===0&&index<properties.length-1
1205
+ ?properties[index+1].keyToken.start:properties[index].end,
1206
+ value:''
1207
+ });
1208
+ }
1209
+ return properties.filter(function retainedProperty(property){return !removed.has(property);});
1210
+ }
1211
+ function cleanImports(start){
1212
+ const properties=cleanProperties(objectProperties(start));
1213
+ for(const property of properties){
1214
+ if(property.value.type!=='string'||property.key.endsWith('/'))continue;
1215
+ reportAssetReference(onReference,property.value.value,'import');
1216
+ if(property.value.value.split(/[?#]/u)[0].endsWith('/'))continue;
1217
+ const edit=stringReferenceEdit(source,property.value,null);
1218
+ if(edit)edits.push(edit);
1219
+ }
1220
+ }
1221
+ for(const property of objectProperties(0)){
1222
+ if(property.key==='imports')cleanImports(property.valueIndex);
1223
+ if(property.key==='scopes'){
1224
+ const scopes=cleanProperties(objectProperties(property.valueIndex),{scopes:true});
1225
+ for(const scope of scopes)cleanImports(scope.valueIndex);
1226
+ }
1227
+ }
1228
+ return edits;
1229
+ }
1164
1230
  for(const property of objectProperties(0)){
1165
1231
  if(property.key==='imports')edits.push(...addImports(property.valueIndex));
1166
1232
  if(property.key==='scopes'){
@@ -1191,6 +1257,9 @@ export function rewriteAssetReferences(source,{filePath,version=SDK_VERSION,onRe
1191
1257
  if(extension==='.html'||extension==='.htm'){
1192
1258
  return applyReferenceEdits(source,htmlReferenceEdits(source,version,onReference));
1193
1259
  }
1260
+ if(extension==='.json'&&path.basename(String(filePath)).toLowerCase()==='arcane.importmap.json'){
1261
+ return applyReferenceEdits(source,importMapReferenceEdits(source,version,onReference));
1262
+ }
1194
1263
  return source;
1195
1264
  }
1196
1265
 
@@ -1356,9 +1425,11 @@ async function physicalRuntime(workspaceRoot,signal){
1356
1425
  return {files};
1357
1426
  }
1358
1427
 
1359
- async function managedImportMapBuild(resolvedWorkspace,signal){
1360
- const runtime=await physicalRuntime(resolvedWorkspace,signal);
1361
- const version=await readWorkspaceAssetVersion(resolvedWorkspace);
1428
+ async function managedImportMapBuild(resolvedWorkspace,signal,pwaEnabled=false){
1429
+ const [runtime,version]=await Promise.all([
1430
+ physicalRuntime(resolvedWorkspace,signal),
1431
+ pwaEnabled?null:readWorkspaceAssetVersion(resolvedWorkspace)
1432
+ ]);
1362
1433
  const built=await buildImportMap({files:runtime.files,signal,version});
1363
1434
  const json=`${JSON.stringify({imports:built.imports},null,2).replaceAll('<','\\u003c')}\n`;
1364
1435
  return {built,json,version};
@@ -1416,11 +1487,13 @@ function parseTagAttributes(openTag){
1416
1487
  const start=index;
1417
1488
  while(index<openTag.length&&!/[\t\n\f\r =>/]/u.test(openTag[index]))index+=1;
1418
1489
  const name=asciiLower(openTag.slice(start,index));
1490
+ const nameEnd=index;
1419
1491
  while(/[\t\n\f\r ]/u.test(openTag[index]??''))index+=1;
1420
1492
  let value='';
1421
1493
  let valueStart=index;
1422
1494
  let valueEnd=index;
1423
1495
  let quote='';
1496
+ const assigned=openTag[index]==='=';
1424
1497
  if(openTag[index]==='='){
1425
1498
  index+=1;
1426
1499
  while(/[\t\n\f\r ]/u.test(openTag[index]??''))index+=1;
@@ -1444,7 +1517,7 @@ function parseTagAttributes(openTag){
1444
1517
  if(attributes.has(name))duplicates.add(name);
1445
1518
  else{
1446
1519
  attributes.set(name,value);
1447
- positions.set(name,{start:valueStart,end:valueEnd,quote});
1520
+ positions.set(name,{start:valueStart,end:valueEnd,quote,nameEnd,assigned});
1448
1521
  }
1449
1522
  }
1450
1523
  }
@@ -1736,6 +1809,101 @@ function scanHtmlStructure(html){
1736
1809
  return {scripts,links,bases,metas,elements,styles,headClose,bodyClose};
1737
1810
  }
1738
1811
 
1812
+ /** Update only active PWA entry references; registration never orders application startup. */
1813
+ export function applyPwaEntryReferences(html,{manifestUrl,bootstrapUrl}={}){
1814
+ if(!is.string(html))throw new TypeError('PWA entry HTML must be a string.');
1815
+ if(!is.string(manifestUrl)||!manifestUrl||!is.string(bootstrapUrl)||!bootstrapUrl){
1816
+ throw new TypeError('PWA manifestUrl and bootstrapUrl must be nonempty strings.');
1817
+ }
1818
+ const structure=scanHtmlStructure(html);
1819
+ const edits=[];
1820
+ const additions=[];
1821
+ function attributeText(value){
1822
+ return value.replaceAll('&','&amp;').replaceAll('"','&quot;')
1823
+ .replaceAll("'",'&#39;').replaceAll('<','&lt;');
1824
+ }
1825
+ function updateTag(element,values,booleans=[]){
1826
+ const attributes=parseTagAttributes(element.open);
1827
+ const tagEdits=[];
1828
+ const missing=[];
1829
+ for(const [name,value] of Object.entries(values)){
1830
+ const position=attributes.positions.get(name);
1831
+ const encoded=attributeText(value);
1832
+ if(!position){
1833
+ missing.push(`${name}="${encoded}"`);
1834
+ }else if(!position.assigned){
1835
+ tagEdits.push({start:position.nameEnd,end:position.nameEnd,value:`="${encoded}"`});
1836
+ }else{
1837
+ tagEdits.push({
1838
+ start:position.start,
1839
+ end:position.end,
1840
+ value:position.quote?encoded:`"${encoded}"`
1841
+ });
1842
+ }
1843
+ }
1844
+ for(const name of booleans){
1845
+ if(!attributes.has(name))missing.push(name);
1846
+ }
1847
+ let updated=applyReferenceEdits(element.open,tagEdits);
1848
+ if(missing.length){
1849
+ const end=updated.length-1;
1850
+ const slash=end-1;
1851
+ const positions=parseTagAttributes(updated).positions;
1852
+ const slashIsValue=[...positions.values()].some(function trailingAttributeSlash(position){
1853
+ return position.assigned&&position.start<=slash&&position.end>slash;
1854
+ });
1855
+ const close=updated[slash]==='/'&&!slashIsValue?slash:end;
1856
+ updated=updated.slice(0,close)+` ${missing.join(' ')}`+updated.slice(close);
1857
+ }
1858
+ edits.push({start:element.start,end:element.start+element.open.length,value:updated});
1859
+ }
1860
+ let manifestFound=false;
1861
+ for(const link of structure.links){
1862
+ const attributes=parseTagAttributes(link.open);
1863
+ const raw=attributes.get('rel')??'';
1864
+ const view=htmlAttributeView(raw);
1865
+ if(!view)continue;
1866
+ const manifestTokens=[...view.decoded.matchAll(/[^\t\n\f\r ]+/gu)]
1867
+ .filter(function manifestRelationship(token){return asciiLower(token[0])==='manifest';});
1868
+ if(manifestTokens.length===0)continue;
1869
+ if(!manifestFound){
1870
+ manifestFound=true;
1871
+ updateTag(link,{href:manifestUrl});
1872
+ continue;
1873
+ }
1874
+ // Retire duplicate manifest relationships while retaining other link attributes and roles.
1875
+ const position=attributes.positions.get('rel');
1876
+ for(const token of manifestTokens){
1877
+ edits.push({
1878
+ start:link.start+position.start+view.positions[token.index],
1879
+ end:link.start+position.start+view.positions[token.index+token[0].length],
1880
+ value:''
1881
+ });
1882
+ }
1883
+ }
1884
+ if(!manifestFound)additions.push(`<link rel="manifest" href="${attributeText(manifestUrl)}">`);
1885
+ let bootstrapFound=false;
1886
+ for(const script of structure.scripts){
1887
+ if(!parseTagAttributes(script.open).has('data-arcane-pwa'))continue;
1888
+ if(bootstrapFound){
1889
+ edits.push({start:script.start,end:script.end,value:''});
1890
+ continue;
1891
+ }
1892
+ bootstrapFound=true;
1893
+ updateTag(script,{type:'module',src:bootstrapUrl},['async']);
1894
+ }
1895
+ if(!bootstrapFound){
1896
+ additions.push(`<script type="module" async data-arcane-pwa src="${attributeText(bootstrapUrl)}"></script>`);
1897
+ }
1898
+ if(additions.length){
1899
+ const newline=html.includes('\r\n')?'\r\n':'\n';
1900
+ const offset=structure.headClose>=0?structure.headClose
1901
+ :structure.bodyClose>=0?structure.bodyClose:html.length;
1902
+ edits.push({start:offset,end:offset,value:`${additions.join(newline)}${newline}`});
1903
+ }
1904
+ return applyReferenceEdits(html,edits);
1905
+ }
1906
+
1739
1907
  function htmlAttributeView(value){
1740
1908
  let decoded='';
1741
1909
  const positions=[];
@@ -1815,7 +1983,7 @@ function htmlReferenceEdits(source,version,onReference){
1815
1983
  const linkResources=new Set(['stylesheet','modulepreload','preload','icon','manifest']);
1816
1984
  const base=structure.bases[0];
1817
1985
  const baseHref=base?structuralAttribute(parseTagAttributes(base.open),'href','base'):null;
1818
- if(baseHref&&(/^[A-Za-z][A-Za-z0-9+.-]*:/u.test(baseHref)||baseHref.startsWith('//')))version=null;
1986
+ if(baseHref&&(/^[A-Za-z][A-Za-z0-9+.-]*:/u.test(baseHref)||baseHref.startsWith('//')))version='';
1819
1987
  function reportHtmlReference(reference){
1820
1988
  if(is.function(onReference))onReference({...reference,baseHref});
1821
1989
  }
@@ -2313,7 +2481,14 @@ async function generateImportMapUnlocked({
2313
2481
  renderManagedHtml(html,'{"imports":{}}\n',baseHref);
2314
2482
  documentStates.push({filePath:documentPath,html,label,baseHref});
2315
2483
  }
2316
- const {built,json,version}=await managedImportMapBuild(resolvedWorkspace,signal);
2484
+ let pwaEnabled=false;
2485
+ try{
2486
+ const packageSource=await readFileFromDisk(path.join(resolvedApp,'arcane-package.json'),'utf8');
2487
+ pwaEnabled=JSON.parse(packageSource)?.pwa?.enabled===true;
2488
+ }catch(error){
2489
+ if(error?.code!=='ENOENT')throw error;
2490
+ }
2491
+ const {built,json,version}=await managedImportMapBuild(resolvedWorkspace,signal,pwaEnabled);
2317
2492
  const renderedDocuments=documentStates.map(item=>({
2318
2493
  ...item,
2319
2494
  content:rewriteAssetReferences(renderManagedHtml(item.html,json,item.baseHref),{
@@ -13,7 +13,22 @@ import {
13
13
  import path from 'node:path';
14
14
  import {pathToFileURL} from 'node:url';
15
15
  import {withWorkspaceOperationLock} from '../workspace-operation-lock.mjs';
16
- import {inspectImportMapHtml,readWorkspaceAssetVersion,rewriteAssetReferences} from '../import-map.mjs';
16
+ import {
17
+ applyPwaEntryReferences,
18
+ inspectImportMapHtml,
19
+ readWorkspaceAssetVersion,
20
+ rewriteAssetReferences,
21
+ versionAssetUrl
22
+ } from '../import-map.mjs';
23
+ import {
24
+ createPwaArtifacts,
25
+ normalizePwaConfig,
26
+ selectPwaFiles,
27
+ PWA_MANIFEST_NAME,
28
+ PWA_OFFLINE_MANIFEST_NAME,
29
+ PWA_WORKER_NAME,
30
+ PWA_BOOTSTRAP_NAME
31
+ } from '../pwa.mjs';
17
32
 
18
33
  const is = new Is(false);
19
34
 
@@ -286,7 +301,7 @@ function normalizeOptionalRecord(value,label){
286
301
  export function validateAppConfig(value,appId,rootConfig,configPath=`apps/${appId}/${APP_CONFIG_NAME}`){
287
302
  assertOnlyKeys(value,new Set([
288
303
  'schemaVersion','id','displayName','version','entry','strategy','security',
289
- 'localAIModelPolicy','include','exclude','shared','adapter'
304
+ 'localAIModelPolicy','include','exclude','shared','adapter','pwa'
290
305
  ]),`${appId}/${APP_CONFIG_NAME}`);
291
306
  if(value.schemaVersion!==1)fail(`${appId}/${APP_CONFIG_NAME}.schemaVersion must be 1.`);
292
307
  if(value.id!==appId||!APP_ID_PATTERN.test(value.id)){
@@ -331,6 +346,7 @@ export function validateAppConfig(value,appId,rootConfig,configPath=`apps/${appI
331
346
  version:value.version,
332
347
  entry,
333
348
  strategy:value.strategy,
349
+ ...(value.pwa===undefined?{}:{pwa:normalizePwaConfig(value.pwa)}),
334
350
  ...(value.security===undefined?{}:{security:normalizeOptionalRecord(
335
351
  value.security,
336
352
  `${appId}/${APP_CONFIG_NAME}.security`
@@ -541,6 +557,7 @@ async function inspectContext(context,{signal}={}){
541
557
  version:context.config.version,
542
558
  entry:context.config.entry,
543
559
  strategy:context.config.strategy,
560
+ ...(context.config.pwa===undefined?{}:{pwa:copyJson(context.config.pwa)}),
544
561
  include:[...context.config.include],
545
562
  exclude:[...context.config.exclude],
546
563
  shared:[...context.config.shared],
@@ -625,7 +642,7 @@ async function loadAdapter(context){
625
642
  return module;
626
643
  }
627
644
 
628
- function releaseManifest(context,files){
645
+ function releaseManifest(context,files,pwaArtifacts){
629
646
  return {
630
647
  schemaVersion:1,
631
648
  kind:'arcane-app-release',
@@ -637,6 +654,13 @@ function releaseManifest(context,files){
637
654
  entry:context.config.entry,
638
655
  strategy:context.config.strategy,
639
656
  shared:[...context.config.shared],
657
+ ...(pwaArtifacts?{pwa:{
658
+ manifest:pwaArtifacts.entryAssets.manifest,
659
+ offlineManifest:PWA_OFFLINE_MANIFEST_NAME,
660
+ worker:PWA_WORKER_NAME,
661
+ sdkVersion:pwaArtifacts.offlineManifest.sdkVersion,
662
+ revision:pwaArtifacts.offlineManifest.revision
663
+ }}:{}),
640
664
  ...(context.config.security===undefined?{}:{security:copyJson(context.config.security)}),
641
665
  ...(context.config.localAIModelPolicy===undefined?{}:{
642
666
  localAIModelPolicy:copyJson(context.config.localAIModelPolicy)
@@ -669,7 +693,8 @@ async function replaceDirectory(stagingRoot,outputRoot){
669
693
  }
670
694
 
671
695
  async function packageWithContext(context,options={}){
672
- const {signal,onEvent}=options;
696
+ const {signal,onEvent,browserPwa=true}=options;
697
+ const pwaEnabled=browserPwa&&context.config.pwa?.enabled===true;
673
698
  const inspected=await inspectContext(context,{signal});
674
699
  if(options.dryRun){
675
700
  return {
@@ -677,7 +702,15 @@ async function packageWithContext(context,options={}){
677
702
  version:context.config.version,
678
703
  output:inspected.output,
679
704
  dryRun:true,
680
- files:[...inspected.files]
705
+ files:[
706
+ ...inspected.files,
707
+ ...(pwaEnabled?[
708
+ PWA_MANIFEST_NAME,
709
+ PWA_OFFLINE_MANIFEST_NAME,
710
+ PWA_WORKER_NAME,
711
+ PWA_BOOTSTRAP_NAME
712
+ ]:[])
713
+ ].sort(compareText)
681
714
  };
682
715
  }
683
716
  await mkdir(context.distRoot,{recursive:true});
@@ -709,24 +742,68 @@ async function packageWithContext(context,options={}){
709
742
  const files=await listOutputFiles(stagingRoot,{signal});
710
743
  // Traverse actual browser resources after the adapter finishes. Files
711
744
  // included only as application documents retain their original content.
712
- const version=await readWorkspaceAssetVersion(context.workspaceRoot);
745
+ const assetVersion=await readWorkspaceAssetVersion(context.workspaceRoot);
746
+ const version=pwaEnabled?null:assetVersion;
713
747
  const inventory=new Set(files);
748
+ const offlineInventory=pwaEnabled?new Set(selectPwaFiles(files,context.config.pwa)):null;
749
+ const offlineReferences=new Set();
714
750
  const entryUrl=new URL(context.config.entry,'http://arcane.invalid/');
715
- const entryDocument=inspected.browserDocuments.find(document=>document.path===context.config.entry);
751
+ const pwaDocumentSources=new Map();
752
+ if(pwaEnabled){
753
+ const documentPaths=new Set([context.config.entry]);
754
+ for(const selected of context.config.include){
755
+ if(/\.html?$/iu.test(selected)&&inventory.has(selected))documentPaths.add(selected);
756
+ }
757
+ for(const document of inspected.browserDocuments){
758
+ const appDocument=context.config.include.some(function includesAppDocument(selected){
759
+ return sameOrDescendant(document.path,selected);
760
+ });
761
+ if(appDocument&&document.managedMaps.length>0&&inventory.has(document.path)){
762
+ documentPaths.add(document.path);
763
+ }
764
+ }
765
+ await Promise.all(
766
+ [...documentPaths].map(
767
+ async function readPwaDocument(documentPath) {
768
+ const source = await readFile(
769
+ path.join(stagingRoot, ...documentPath.split('/')),
770
+ 'utf8'
771
+ );
772
+ pwaDocumentSources.set(
773
+ documentPath,
774
+ {source, inspected: inspectImportMapHtml(source)}
775
+ );
776
+ }
777
+ )
778
+ );
779
+ }
780
+ const entryDocument=pwaEnabled?pwaDocumentSources.get(context.config.entry).inspected
781
+ :inspected.browserDocuments.find(function matchingEntryDocument(document){
782
+ return document.path===context.config.entry;
783
+ });
716
784
  const documentUrl=entryDocument?.bases[0]?.href
717
785
  ?new URL(entryDocument.bases[0].href,entryUrl):entryUrl;
718
786
  const pending=[{file:context.config.entry,documentUrl}];
719
787
  for(const file of files){
720
788
  if(/^arcane\/(?:modules|entities|components|css|sdk|dependencies)\//u.test(file)
721
789
  &&/\.(?:m?js|html?|css)$/iu.test(file))pending.push({file,documentUrl});
790
+ if(pwaEnabled&&path.posix.basename(file)==='arcane.importmap.json'){
791
+ pending.push({file,documentUrl});
792
+ }
722
793
  }
723
794
  for(const document of inspected.browserDocuments){
724
- if(document.managedMaps.length>0){
795
+ if(document.managedMaps.length>0&&!pwaDocumentSources.has(document.path)){
725
796
  const url=new URL(document.path,entryUrl.origin);
726
797
  pending.push({file:document.path,documentUrl:document.bases[0]?.href
727
798
  ?new URL(document.bases[0].href,url):url});
728
799
  }
729
800
  }
801
+ for(const [file,document] of pwaDocumentSources){
802
+ if(file===context.config.entry)continue;
803
+ const url=new URL(file,entryUrl.origin);
804
+ pending.push({file,documentUrl:document.inspected.bases[0]?.href
805
+ ?new URL(document.inspected.bases[0].href,url):url});
806
+ }
730
807
  const visited=new Set();
731
808
  const resources=new Map();
732
809
  for(const current of pending){
@@ -734,31 +811,39 @@ async function packageWithContext(context,options={}){
734
811
  throwIfAborted(signal);
735
812
  const contextKey=`${relative}\n${current.documentUrl.href}`;
736
813
  if(visited.has(contextKey)||!inventory.has(relative)
737
- ||!/\.(?:m?js|html?|css)$/iu.test(relative))continue;
814
+ ||(!/\.(?:m?js|html?|css)$/iu.test(relative)
815
+ &&!(pwaEnabled&&path.posix.basename(relative)==='arcane.importmap.json')))continue;
738
816
  visited.add(contextKey);
739
817
  const filePath=path.join(stagingRoot,...relative.split('/'));
740
818
  let resource=resources.get(relative);
741
819
  if(!resource){
742
- const original=await readFile(filePath,'utf8');
820
+ const original=pwaDocumentSources.get(relative)?.source??await readFile(filePath,'utf8');
743
821
  const references=[];
744
822
  const content=rewriteAssetReferences(original,{
745
823
  filePath:relative,version,onReference:reference=>references.push(reference)
746
824
  });
747
825
  if(content!==original)await writeFile(filePath,content,'utf8');
748
- resource={references};
826
+ resource={references,...(pwaDocumentSources.has(relative)?{content}:{})};
749
827
  resources.set(relative,resource);
750
828
  }
751
829
  for(const {url,kind,baseHref,baseKind} of resource.references){
752
- if(kind==='fetch'||(kind==='asset'&&!/\.css(?:[?#]|$)/iu.test(url))
753
- ||(kind==='import'&&!/^(?:\.{1,2}\/|\/)/u.test(url)))continue;
830
+ const traversable=kind!=='fetch'&&(kind!=='asset'||/\.css(?:[?#]|$)/iu.test(url))
831
+ &&(kind!=='import'||/^(?:\.{1,2}\/|\/)/u.test(url));
832
+ if(!pwaEnabled&&!traversable)continue;
833
+ if(kind==='import'&&!/^(?:\.{1,2}\/|\/)/u.test(url))continue;
754
834
  try{
755
835
  const ownerUrl=new URL(relative,entryUrl.origin);
756
836
  const base=baseHref?new URL(baseHref,ownerUrl)
757
- :baseKind==='document'?current.documentUrl:ownerUrl;
837
+ :baseKind==='document'||path.posix.basename(relative)==='arcane.importmap.json'
838
+ ?current.documentUrl:ownerUrl;
758
839
  const target=new URL(url,base);
759
840
  if(target.origin===entryUrl.origin){
760
- pending.push({
761
- file:decodeURIComponent(target.pathname).replace(/^\//u,''),
841
+ const file=decodeURIComponent(target.pathname).replace(/^\//u,'');
842
+ if(pwaEnabled&&offlineInventory.has(file)){
843
+ offlineReferences.add(versionAssetUrl(`.${target.pathname}${target.search}`,null));
844
+ }
845
+ if(traversable)pending.push({
846
+ file,
762
847
  documentUrl:kind==='document'?target
763
848
  :baseHref?new URL(baseHref,ownerUrl):current.documentUrl
764
849
  });
@@ -772,7 +857,45 @@ async function packageWithContext(context,options={}){
772
857
  if(!files.some(file=>pathKey(file)===pathKey(context.config.entry))){
773
858
  fail(`Package output is missing its entry file: ${context.config.entry}.`);
774
859
  }
775
- const manifest=releaseManifest(context,files);
860
+ const pwaArtifacts=pwaEnabled?createPwaArtifacts({
861
+ app:{
862
+ id:context.appId,
863
+ displayName:context.config.displayName,
864
+ version:context.config.version,
865
+ entry:context.config.entry
866
+ },
867
+ sdkVersion:assetVersion,
868
+ pwa:context.config.pwa,
869
+ files,
870
+ assets:[...offlineReferences]
871
+ }):null;
872
+ if(pwaArtifacts){
873
+ for(const artifact of pwaArtifacts.files){
874
+ if(inventory.has(artifact.path)){
875
+ fail(`Package content overlaps generated PWA file: ${artifact.path}.`);
876
+ }
877
+ await writeFile(path.join(stagingRoot,artifact.path),artifact.content,'utf8');
878
+ files.push(artifact.path);
879
+ }
880
+ for(const [documentPath,document] of pwaDocumentSources){
881
+ const documentUrl=new URL(documentPath,entryUrl.origin);
882
+ const outputBase=document.inspected.bases[0]?.href
883
+ ?new URL(document.inspected.bases[0].href,documentUrl):documentUrl;
884
+ const outputDirectory=new URL('./',outputBase).pathname;
885
+ function entryReference(relative){
886
+ const target=path.posix.relative(outputDirectory,`/${relative}`);
887
+ return target.startsWith('.')?target:`./${target}`;
888
+ }
889
+ const content=resources.get(documentPath)?.content??document.source;
890
+ await writeFile(path.join(stagingRoot,...documentPath.split('/')),
891
+ applyPwaEntryReferences(content,{
892
+ manifestUrl:entryReference(pwaArtifacts.entryAssets.manifest),
893
+ bootstrapUrl:entryReference(pwaArtifacts.entryAssets.bootstrap)
894
+ }),'utf8');
895
+ }
896
+ files.sort(compareText);
897
+ }
898
+ const manifest=releaseManifest(context,files,pwaArtifacts);
776
899
  await writeFile(
777
900
  path.join(stagingRoot,RELEASE_MANIFEST_NAME),
778
901
  `${JSON.stringify(manifest,null,2)}\n`,