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.
- package/CHANGELOG.md +24 -0
- package/README.md +14 -8
- package/browser-runtime/pwa.mjs +215 -0
- package/docs/reference/README.md +3 -2
- package/docs/reference/asset-versioning.md +13 -0
- package/docs/reference/inventory/package-api.json +29 -1
- package/docs/reference/pwa.md +183 -0
- package/docs/reference/runtime-modules.md +7 -1
- package/docs/reference/sdk-api.md +45 -1
- package/package.json +3 -2
- package/runtime/arcane/modules/Errors.js +1 -1
- package/runtime/arcane/modules/HTMLImport.js +13 -3
- package/schemas/arcane-app.schema.json +28 -0
- package/schemas/arcane-package.schema.json +30 -0
- package/src/app-descriptor.mjs +10 -2
- package/src/dev-server.mjs +252 -13
- package/src/import-map.mjs +186 -11
- package/src/packager/core.mjs +140 -17
- package/src/pwa-worker.mjs +227 -0
- package/src/pwa.mjs +311 -0
- package/src/targets/index.mjs +1 -0
- package/src/toolchain.mjs +1 -0
package/src/import-map.mjs
CHANGED
|
@@ -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
|
|
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
|
|
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&¶meter.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
|
|
1361
|
-
|
|
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('&','&').replaceAll('"','"')
|
|
1823
|
+
.replaceAll("'",''').replaceAll('<','<');
|
|
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=
|
|
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
|
-
|
|
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),{
|
package/src/packager/core.mjs
CHANGED
|
@@ -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 {
|
|
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:[
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
753
|
-
|
|
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'
|
|
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
|
-
|
|
761
|
-
|
|
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
|
|
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`,
|