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.
- package/CHANGELOG.md +28 -0
- package/README.md +14 -8
- package/browser-runtime/browser-device.mjs +13 -0
- package/browser-runtime/pwa.mjs +215 -0
- package/docs/architecture.md +1 -1
- package/docs/reference/README.md +5 -3
- package/docs/reference/ai/browser-wasm.md +19 -0
- package/docs/reference/asset-versioning.md +13 -0
- package/docs/reference/inventory/package-api.json +45 -1
- package/docs/reference/pwa.md +183 -0
- package/docs/reference/sdk-api.md +86 -3
- package/package.json +4 -2
- 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 +187 -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
|
@@ -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
|
|
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
|
|
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&¶meter.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
|
|
1360
|
-
|
|
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('&','&').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
|
+
|
|
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=
|
|
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
|
-
|
|
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),{
|
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`,
|