arcane-os 0.8.1 → 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.
@@ -23,6 +23,8 @@ const STATIC_RUNTIME_PACKAGE_IMPORTS=new Map([
23
23
  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
+ ['arcane-os/browser-device','sdk/browser-device.mjs'],
27
+ ['arcane-os/pwa','sdk/pwa.mjs'],
26
28
  ['arcane-os/speech-text','sdk/speech-text.mjs'],
27
29
  ['arcane-os/ai/browser-wasm',SDK_BROWSER_AI_ENTRY],
28
30
  ['arcane-os/ai/browser-speech',SDK_BROWSER_SPEECH_ENTRY]
@@ -802,7 +804,7 @@ export function versionAssetUrl(value,version=SDK_VERSION){
802
804
  }
803
805
 
804
806
  function assetUrlVersionEdits(value,version){
805
- if(!is.string(value)||!value||!version||value.startsWith('#')
807
+ if(!is.string(value)||!value||(!version&&version!==null)||value.startsWith('#')
806
808
  ||value.startsWith('//')||/^[A-Za-z][A-Za-z0-9+.-]*:/u.test(value)
807
809
  ||/^\s/u.test(value))return [];
808
810
  const fragmentStart=value.indexOf('#');
@@ -810,7 +812,9 @@ function assetUrlVersionEdits(value,version){
810
812
  const queryStart=address.indexOf('?');
811
813
  const pathname=queryStart<0?address:address.slice(0,queryStart);
812
814
  if(!pathname)return [];
813
- const versionValue=encodeURIComponent(String(version));
815
+ const clean=version===null;
816
+ if(clean&&queryStart<0)return [];
817
+ const versionValue=clean?'':encodeURIComponent(String(version));
814
818
  if(queryStart<0)return [{start:address.length,end:address.length,value:`?arcaneVersion=${versionValue}`}];
815
819
  const query=address.slice(queryStart+1);
816
820
  const edits=[];
@@ -823,9 +827,9 @@ function assetUrlVersionEdits(value,version){
823
827
  let decodedKey=key;
824
828
  try{decodedKey=decodeURIComponent(key.replaceAll('+',' '));}
825
829
  catch{decodedKey=key;}
826
- const remove=decodedKey==='v'||(decodedKey==='arcaneVersion'&&versionFound);
830
+ const remove=decodedKey==='v'||(decodedKey==='arcaneVersion'&&(clean||versionFound));
827
831
  parameters.push({start:offset,end:offset+parameter.length,remove});
828
- if(decodedKey==='arcaneVersion'&&!versionFound){
832
+ if(decodedKey==='arcaneVersion'&&!versionFound&&!clean){
829
833
  versionFound=true;
830
834
  edits.push({
831
835
  start:offset+(equals<0?parameter.length:equals+1),
@@ -835,6 +839,9 @@ function assetUrlVersionEdits(value,version){
835
839
  }
836
840
  offset+=parameter.length+1;
837
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:''}];
838
845
  // Remove adjacent obsolete fields together, including only their separator.
839
846
  // Other field spelling and source-level escapes remain untouched.
840
847
  for(let index=0;index<parameters.length;index+=1){
@@ -850,7 +857,7 @@ function assetUrlVersionEdits(value,version){
850
857
  const lastRetained=parameters.findLast(function retainedParameter(parameter){
851
858
  return !parameter.remove;
852
859
  });
853
- if(!versionFound)edits.push({
860
+ if(!clean&&!versionFound)edits.push({
854
861
  start:address.length,
855
862
  end:address.length,
856
863
  value:`${lastRetained&&lastRetained.end>lastRetained.start?'&':''}arcaneVersion=${versionValue}`
@@ -1160,6 +1167,66 @@ function importMapReferenceEdits(source,version,onReference){
1160
1167
  prependAliases(start,aliases,selectedEdits);
1161
1168
  return selectedEdits;
1162
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
+ }
1163
1230
  for(const property of objectProperties(0)){
1164
1231
  if(property.key==='imports')edits.push(...addImports(property.valueIndex));
1165
1232
  if(property.key==='scopes'){
@@ -1190,6 +1257,9 @@ export function rewriteAssetReferences(source,{filePath,version=SDK_VERSION,onRe
1190
1257
  if(extension==='.html'||extension==='.htm'){
1191
1258
  return applyReferenceEdits(source,htmlReferenceEdits(source,version,onReference));
1192
1259
  }
1260
+ if(extension==='.json'&&path.basename(String(filePath)).toLowerCase()==='arcane.importmap.json'){
1261
+ return applyReferenceEdits(source,importMapReferenceEdits(source,version,onReference));
1262
+ }
1193
1263
  return source;
1194
1264
  }
1195
1265
 
@@ -1355,9 +1425,11 @@ async function physicalRuntime(workspaceRoot,signal){
1355
1425
  return {files};
1356
1426
  }
1357
1427
 
1358
- async function managedImportMapBuild(resolvedWorkspace,signal){
1359
- const runtime=await physicalRuntime(resolvedWorkspace,signal);
1360
- 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
+ ]);
1361
1433
  const built=await buildImportMap({files:runtime.files,signal,version});
1362
1434
  const json=`${JSON.stringify({imports:built.imports},null,2).replaceAll('<','\\u003c')}\n`;
1363
1435
  return {built,json,version};
@@ -1415,11 +1487,13 @@ function parseTagAttributes(openTag){
1415
1487
  const start=index;
1416
1488
  while(index<openTag.length&&!/[\t\n\f\r =>/]/u.test(openTag[index]))index+=1;
1417
1489
  const name=asciiLower(openTag.slice(start,index));
1490
+ const nameEnd=index;
1418
1491
  while(/[\t\n\f\r ]/u.test(openTag[index]??''))index+=1;
1419
1492
  let value='';
1420
1493
  let valueStart=index;
1421
1494
  let valueEnd=index;
1422
1495
  let quote='';
1496
+ const assigned=openTag[index]==='=';
1423
1497
  if(openTag[index]==='='){
1424
1498
  index+=1;
1425
1499
  while(/[\t\n\f\r ]/u.test(openTag[index]??''))index+=1;
@@ -1443,7 +1517,7 @@ function parseTagAttributes(openTag){
1443
1517
  if(attributes.has(name))duplicates.add(name);
1444
1518
  else{
1445
1519
  attributes.set(name,value);
1446
- positions.set(name,{start:valueStart,end:valueEnd,quote});
1520
+ positions.set(name,{start:valueStart,end:valueEnd,quote,nameEnd,assigned});
1447
1521
  }
1448
1522
  }
1449
1523
  }
@@ -1735,6 +1809,101 @@ function scanHtmlStructure(html){
1735
1809
  return {scripts,links,bases,metas,elements,styles,headClose,bodyClose};
1736
1810
  }
1737
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
+
1738
1907
  function htmlAttributeView(value){
1739
1908
  let decoded='';
1740
1909
  const positions=[];
@@ -1814,7 +1983,7 @@ function htmlReferenceEdits(source,version,onReference){
1814
1983
  const linkResources=new Set(['stylesheet','modulepreload','preload','icon','manifest']);
1815
1984
  const base=structure.bases[0];
1816
1985
  const baseHref=base?structuralAttribute(parseTagAttributes(base.open),'href','base'):null;
1817
- 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='';
1818
1987
  function reportHtmlReference(reference){
1819
1988
  if(is.function(onReference))onReference({...reference,baseHref});
1820
1989
  }
@@ -2312,7 +2481,14 @@ async function generateImportMapUnlocked({
2312
2481
  renderManagedHtml(html,'{"imports":{}}\n',baseHref);
2313
2482
  documentStates.push({filePath:documentPath,html,label,baseHref});
2314
2483
  }
2315
- 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);
2316
2492
  const renderedDocuments=documentStates.map(item=>({
2317
2493
  ...item,
2318
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`,