arcane-os 0.5.11 → 0.5.13

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.
Files changed (36) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/README.md +8 -8
  3. package/docs/architecture.md +1 -1
  4. package/docs/reference/README.md +5 -5
  5. package/docs/reference/ai/browser-speech.md +73 -7
  6. package/docs/reference/ai/twin-cloud.md +1 -1
  7. package/docs/reference/asset-versioning.md +69 -0
  8. package/docs/reference/cli.md +1 -1
  9. package/docs/reference/core/arcane-ai-contracts.md +2 -3
  10. package/docs/reference/core/arcane-api.md +5 -5
  11. package/docs/reference/core/ollama-module.md +1 -1
  12. package/docs/reference/core/reference/arcane-api/platform-installation-users-system.md +2 -2
  13. package/docs/reference/inventory/package-api.json +1 -1
  14. package/docs/reference/inventory/runtime-components.json +1 -1
  15. package/docs/reference/inventory/runtime-modules.json +3 -3
  16. package/docs/reference/protocols.md +5 -5
  17. package/docs/reference/runtime-components.md +5 -2
  18. package/docs/reference/runtime-modules.md +39 -4
  19. package/docs/reference/sdk-api.md +6 -6
  20. package/package.json +1 -1
  21. package/runtime/arcane/components/calculator.html +1 -1
  22. package/runtime/arcane/components/chart.html +2 -7
  23. package/runtime/arcane/components/dashboard-config.html +3 -8
  24. package/runtime/arcane/components/file-manager.html +6 -6
  25. package/runtime/arcane/components/markdown-document.html +1 -6
  26. package/runtime/arcane/components/markdown-editor.html +2 -7
  27. package/runtime/arcane/components/media-embed.html +1 -1
  28. package/runtime/arcane/components/screen-capture.html +1 -1
  29. package/runtime/arcane/components/voice-transcription.html +4 -9
  30. package/runtime/arcane/modules/AI.js +59 -10
  31. package/runtime/arcane/modules/HTMLImport.js +184 -13
  32. package/src/dev-server.mjs +72 -5
  33. package/src/import-map.mjs +548 -16
  34. package/src/installed-sdk-runtime.mjs +1 -0
  35. package/src/packager/core.mjs +60 -1
  36. package/src/workspace-runtime.mjs +19 -7
@@ -1,11 +1,13 @@
1
1
  import {lstat,mkdir,readFile as readFileFromDisk,readdir,realpath,writeFile} from 'node:fs/promises';
2
2
  import path from 'node:path';
3
3
  import {pathToFileURL} from 'node:url';
4
+ import {SDK_VERSION} from './constants.mjs';
4
5
 
5
6
  export const IMPORT_MAP_RELATIVE_PATH='modules/arcane.importmap.json';
6
7
  export const MANAGED_IMPORT_MAP_ATTRIBUTE='data-arcane-import-map';
7
8
 
8
9
  const JAVASCRIPT_EXTENSION=/\.(?:js|mjs)$/u;
10
+ const RESOURCE_EXTENSION=/\.(?:m?js|html?|css|wasm|svg|png|gif|jpe?g|webp|avif|ico|bmp|woff2?|ttf|otf|eot|mp3|wav|ogg|oga|opus|m4a|mp4|webm|ogv|vtt)(?:[?#]|$)/iu;
9
11
  const PERSISTENT_CHAT_IMPORT='#arcane/persistent-ai-chat-session';
10
12
  const PERSISTENT_CHAT_MODULE='modules/PersistentAIChatSession.js';
11
13
  const SDK_BROWSER_ENTRY='sdk/event-manager.mjs';
@@ -17,6 +19,7 @@ const STATIC_RUNTIME_PACKAGE_IMPORTS=new Map([
17
19
  ]);
18
20
  const SDK_BROWSER_SELF_IMPORTS=new Map([
19
21
  ['arcane-os/event-manager',SDK_BROWSER_ENTRY],
22
+ ['arcane-os/logging','sdk/logging.mjs'],
20
23
  ['arcane-os/ai/browser-wasm',SDK_BROWSER_AI_ENTRY],
21
24
  ['arcane-os/ai/browser-speech',SDK_BROWSER_SPEECH_ENTRY]
22
25
  ]);
@@ -646,6 +649,7 @@ function tokenize(source){
646
649
  function matchingToken(tokens,start,opening,closing){
647
650
  let depth=0;
648
651
  for(let index=start;index<tokens.length;index+=1){
652
+ if(tokens[index].type!=='punctuator')continue;
649
653
  if(tokens[index].value===opening)depth+=1;
650
654
  else if(tokens[index].value===closing){
651
655
  depth-=1;
@@ -691,6 +695,7 @@ function topLevelCommas(tokens,start,end){
691
695
  const commas=[];
692
696
  const depths={parenthesis:0,bracket:0,brace:0};
693
697
  for(let index=start;index<end;index+=1){
698
+ if(tokens[index].type!=='punctuator')continue;
694
699
  const value=tokens[index].value;
695
700
  if(value==='(')depths.parenthesis+=1;
696
701
  else if(value===')'&&depths.parenthesis>0)depths.parenthesis-=1;
@@ -711,7 +716,10 @@ function importRecord(kind,token){
711
716
 
712
717
  export function scanModuleImports(source,{importer='<module>'}={}){
713
718
  if(typeof source!=='string')throw new TypeError('scanModuleImports source must be a string.');
714
- const tokens=tokenize(source);
719
+ return moduleImportsFromTokens(tokenize(source),importer);
720
+ }
721
+
722
+ function moduleImportsFromTokens(tokens,importer){
715
723
  const imports=[];
716
724
  let hasModuleSyntax=false;
717
725
  for(let index=0;index<tokens.length;index+=1){
@@ -784,6 +792,348 @@ export function scanModuleImports(source,{importer='<module>'}={}){
784
792
  };
785
793
  }
786
794
 
795
+ export function versionAssetUrl(value,version=SDK_VERSION){
796
+ if(typeof value!=='string')return value;
797
+ return applyReferenceEdits(value,assetUrlVersionEdits(value,version));
798
+ }
799
+
800
+ function assetUrlVersionEdits(value,version){
801
+ if(typeof value!=='string'||!value||!version||value.startsWith('#')
802
+ ||value.startsWith('//')||/^[A-Za-z][A-Za-z0-9+.-]*:/u.test(value)
803
+ ||/^\s/u.test(value))return [];
804
+ const fragmentStart=value.indexOf('#');
805
+ const address=fragmentStart<0?value:value.slice(0,fragmentStart);
806
+ const queryStart=address.indexOf('?');
807
+ const pathname=queryStart<0?address:address.slice(0,queryStart);
808
+ if(!pathname)return [];
809
+ const versionValue=encodeURIComponent(String(version));
810
+ if(queryStart<0)return [{start:address.length,end:address.length,value:`?arcaneVersion=${versionValue}`}];
811
+ const query=address.slice(queryStart+1);
812
+ const edits=[];
813
+ let offset=queryStart+1;
814
+ for(const parameter of query.split('&')){
815
+ const equals=parameter.indexOf('=');
816
+ const key=equals<0?parameter:parameter.slice(0,equals);
817
+ let decodedKey=key;
818
+ try{decodedKey=decodeURIComponent(key.replaceAll('+',' '));}
819
+ catch{decodedKey=key;}
820
+ if(decodedKey==='arcaneVersion'){
821
+ edits.push({
822
+ start:offset+(equals<0?parameter.length:equals+1),
823
+ end:offset+parameter.length,
824
+ value:`${equals<0?'=':''}${versionValue}`
825
+ });
826
+ }
827
+ offset+=parameter.length+1;
828
+ }
829
+ if(edits.length===0)edits.push({
830
+ start:address.length,
831
+ end:address.length,
832
+ value:`${query&&!query.endsWith('&')?'&':''}arcaneVersion=${versionValue}`
833
+ });
834
+ return edits;
835
+ }
836
+
837
+ function applyReferenceEdits(source,edits){
838
+ let result=source;
839
+ for(const edit of edits.sort(function descendingReferenceOffset(left,right){
840
+ return right.start-left.start;
841
+ })){
842
+ result=result.slice(0,edit.start)+edit.value+result.slice(edit.end);
843
+ }
844
+ return result;
845
+ }
846
+
847
+ function stringReferenceEdit(source,token,version){
848
+ const urlEdits=assetUrlVersionEdits(token.value,version);
849
+ if(urlEdits.length===0)return null;
850
+ const quote=source[token.start];
851
+ const positions=[];
852
+ let cursor=token.start+1;
853
+ while(cursor<token.end-1){
854
+ const start=cursor;
855
+ if(source[cursor]==='\\'){
856
+ const decoded=decodedEscape(source,cursor+1);
857
+ for(let index=0;index<decoded.value.length;index+=1)positions.push(start-token.start);
858
+ cursor=decoded.next;
859
+ }else{
860
+ positions.push(cursor-token.start);
861
+ cursor+=1;
862
+ }
863
+ }
864
+ positions.push(token.end-token.start-1);
865
+ const edits=urlEdits.map(function authoredStringReference(edit){
866
+ return {
867
+ start:positions[edit.start],
868
+ end:positions[edit.end],
869
+ value:edit.value.replaceAll('\\','\\\\').replaceAll(quote,`\\${quote}`)
870
+ };
871
+ });
872
+ const original=source.slice(token.start,token.end);
873
+ const value=applyReferenceEdits(original,edits);
874
+ return value===original?null:{start:token.start,end:token.end,value};
875
+ }
876
+
877
+ function callArgumentTokens(tokens,opening){
878
+ const closing=matchingToken(tokens,opening,'(',')');
879
+ if(closing<0)return [];
880
+ const separators=topLevelCommas(tokens,opening+1,closing);
881
+ const boundaries=[opening,...separators,closing];
882
+ const argumentsList=[];
883
+ for(let index=0;index<boundaries.length-1;index+=1){
884
+ argumentsList.push(tokens.slice(boundaries[index]+1,boundaries[index+1]));
885
+ }
886
+ return argumentsList;
887
+ }
888
+
889
+ function literalExpressionResults(tokens){
890
+ if(tokens.length===1&&tokens[0].type==='string')return tokens;
891
+ if(tokens[0]?.value==='('&&matchingToken(tokens,0,'(',')')===tokens.length-1){
892
+ return literalExpressionResults(tokens.slice(1,-1));
893
+ }
894
+ let depth=0;
895
+ let conditional=-1;
896
+ let nestedConditionals=0;
897
+ for(let index=0;index<tokens.length;index+=1){
898
+ if(tokens[index].type!=='punctuator')continue;
899
+ const value=tokens[index].value;
900
+ if(['(','[','{'].includes(value)){depth+=1;continue;}
901
+ if([')',']','}'].includes(value)){depth-=1;continue;}
902
+ if(depth!==0)continue;
903
+ if(value==='?'){
904
+ if(conditional<0)conditional=index;
905
+ else nestedConditionals+=1;
906
+ }else if(value===':'&&conditional>=0){
907
+ if(nestedConditionals>0){nestedConditionals-=1;continue;}
908
+ return [
909
+ ...literalExpressionResults(tokens.slice(conditional+1,index)),
910
+ ...literalExpressionResults(tokens.slice(index+1))
911
+ ];
912
+ }
913
+ }
914
+ return [];
915
+ }
916
+
917
+ function globalResourceCallee(tokens,index){
918
+ if(!identifierIsProperty(tokens,index))return index;
919
+ if((tokens[index-1]?.value==='.'||tokens[index-1]?.value==='?.')
920
+ &&new Set(['globalThis','window','self']).has(tokens[index-2]?.value)
921
+ &&!identifierIsProperty(tokens,index-2))return index-2;
922
+ return -1;
923
+ }
924
+
925
+ function localResourceBase(tokens){
926
+ if(!tokens)return false;
927
+ if(tokens.some(function nonExpressionToken(token){return !['identifier','punctuator'].includes(token.type);}))return false;
928
+ const base=tokens.map(function baseToken(token){return token.value;}).join('');
929
+ return [
930
+ 'import.meta.url','document.baseURI','location.href','window.location.href',
931
+ 'globalThis.location.href','self.location.href','window.document.baseURI',
932
+ 'globalThis.document.baseURI'
933
+ ].includes(base);
934
+ }
935
+
936
+ function literalResourceFetch(argumentsList){
937
+ if(argumentsList.length===1)return true;
938
+ const options=argumentsList[1];
939
+ if(options?.[0]?.value!=='{'||options.at(-1)?.value!=='}')return false;
940
+ for(let index=1;index<options.length-1;index+=1){
941
+ const token=options[index];
942
+ if(token.value==='...'||(token.value==='.'&&options[index+1]?.value==='.'))return false;
943
+ if(token.value==='body')return false;
944
+ if(options[index+1]?.value!==':')continue;
945
+ if(token.value==='method'&&(options[index+2]?.type!=='string'
946
+ ||!['GET','HEAD'].includes(options[index+2].value.toUpperCase())))return false;
947
+ }
948
+ return true;
949
+ }
950
+
951
+ function reportAssetReference(onReference,url,kind,baseKind){
952
+ if(typeof onReference==='function')onReference({url,kind,baseHref:null,...(baseKind?{baseKind}:{})});
953
+ }
954
+
955
+ function javascriptReferenceEdits(source,version,onReference){
956
+ const tokens=tokenize(source);
957
+ const selected=new Map();
958
+ const imports=moduleImportsFromTokens(tokens,'asset source').imports;
959
+ const byOffset=new Map(tokens.map(function tokenByOffset(token){
960
+ return [token.start,token];
961
+ }));
962
+ for(const entry of imports){
963
+ reportAssetReference(onReference,entry.specifier,'import');
964
+ if(/^(?:\.{1,2}\/|\/)/u.test(entry.specifier)){
965
+ selected.set(entry.offset,byOffset.get(entry.offset));
966
+ }
967
+ }
968
+ for(let index=0;index<tokens.length;index+=1){
969
+ const token=tokens[index];
970
+ if(token.type!=='identifier'||tokens[index+1]?.value!=='(')continue;
971
+ if(!['URL','Worker','SharedWorker','importScripts','fetch'].includes(token.value))continue;
972
+ const callee=globalResourceCallee(tokens,index);
973
+ if(callee<0)continue;
974
+ const constructor=tokens[callee-1]?.value==='new';
975
+ if(['URL','Worker','SharedWorker'].includes(token.value)&&!constructor)continue;
976
+ const argumentsList=callArgumentTokens(tokens,index+1);
977
+ if(token.value==='fetch'&&!literalResourceFetch(argumentsList))continue;
978
+ if(token.value==='URL'&&!localResourceBase(argumentsList[1]))continue;
979
+ const selectedArguments=token.value==='importScripts'?argumentsList:argumentsList.slice(0,1);
980
+ for(const argument of selectedArguments){
981
+ for(const literal of literalExpressionResults(argument)){
982
+ if((token.value==='fetch'||token.value==='URL')
983
+ &&!RESOURCE_EXTENSION.test(literal.value))continue;
984
+ if((token.value==='fetch'||token.value==='URL')
985
+ &&/\.html?(?:[?#]|$)/iu.test(literal.value))continue;
986
+ const kind=token.value==='fetch'?'fetch'
987
+ :token.value==='URL'&&!/\.(?:js|mjs)(?:[?#]|$)/iu.test(literal.value)?'asset':'script';
988
+ const documentBase=['Worker','SharedWorker','fetch'].includes(token.value)
989
+ ||(token.value==='URL'&&argumentsList[1]?.[0]?.value!=='import');
990
+ reportAssetReference(onReference,literal.value,kind,documentBase?'document':undefined);
991
+ selected.set(literal.start,literal);
992
+ }
993
+ }
994
+ }
995
+ return [...selected.values()].map(function versionJavaScriptReference(token){
996
+ return stringReferenceEdit(source,token,version);
997
+ }).filter(Boolean);
998
+ }
999
+
1000
+ function cssStringEnd(source,start){
1001
+ const quote=source[start];
1002
+ let cursor=start+1;
1003
+ while(cursor<source.length){
1004
+ if(source[cursor]==='\\'){cursor+=2;continue;}
1005
+ if(source[cursor]===quote)return cursor+1;
1006
+ cursor+=1;
1007
+ }
1008
+ return source.length;
1009
+ }
1010
+
1011
+ function cssReferenceEdits(source,version,onReference,versionReference=versionAssetUrl){
1012
+ const edits=[];
1013
+ let cursor=0;
1014
+ let importUrlStart=-1;
1015
+ function skipSpaceAndComments(start){
1016
+ let position=start;
1017
+ while(position<source.length){
1018
+ if(/\s/u.test(source[position])){position+=1;continue;}
1019
+ if(source.startsWith('/*',position)){
1020
+ const close=source.indexOf('*/',position+2);
1021
+ position=close<0?source.length:close+2;
1022
+ continue;
1023
+ }
1024
+ break;
1025
+ }
1026
+ return position;
1027
+ }
1028
+ function addReference(start,end,kind='asset'){
1029
+ const original=source.slice(start,end);
1030
+ if(original.includes('\\'))return;
1031
+ reportAssetReference(onReference,original,kind);
1032
+ const value=versionReference(original,version);
1033
+ if(value!==original)edits.push({start,end,value});
1034
+ }
1035
+ while(cursor<source.length){
1036
+ if(source.startsWith('/*',cursor)){
1037
+ const end=source.indexOf('*/',cursor+2);
1038
+ cursor=end<0?source.length:end+2;
1039
+ continue;
1040
+ }
1041
+ if(source[cursor]==='"'||source[cursor]==="'"){
1042
+ cursor=cssStringEnd(source,cursor);
1043
+ continue;
1044
+ }
1045
+ const rest=source.slice(cursor);
1046
+ if(/^@import(?![\w-])/iu.test(rest)){
1047
+ const start=skipSpaceAndComments(cursor+7);
1048
+ importUrlStart=start;
1049
+ if(source[start]==='"'||source[start]==="'"){
1050
+ const end=cssStringEnd(source,start);
1051
+ addReference(start+1,end-1,'style');
1052
+ cursor=end;
1053
+ continue;
1054
+ }
1055
+ }
1056
+ if(!/[\w-]/u.test(source[cursor-1]??'')&&/^url\s*\(/iu.test(rest)){
1057
+ const kind=cursor===importUrlStart?'style':'asset';
1058
+ const open=source.indexOf('(',cursor);
1059
+ const start=skipSpaceAndComments(open+1);
1060
+ if(source[start]==='"'||source[start]==="'"){
1061
+ const end=cssStringEnd(source,start);
1062
+ addReference(start+1,end-1,kind);
1063
+ cursor=end;
1064
+ }else{
1065
+ let end=start;
1066
+ while(end<source.length&&source[end]!==')'){
1067
+ end+=source[end]==='\\'?2:1;
1068
+ }
1069
+ let valueEnd=Math.min(end,source.length);
1070
+ while(valueEnd>start&&/\s/u.test(source[valueEnd-1]))valueEnd-=1;
1071
+ addReference(start,valueEnd,kind);
1072
+ cursor=Math.min(end+1,source.length);
1073
+ }
1074
+ continue;
1075
+ }
1076
+ cursor+=1;
1077
+ }
1078
+ return edits;
1079
+ }
1080
+
1081
+ function importMapReferenceEdits(source,version,onReference){
1082
+ const tokens=tokenize(source);
1083
+ const edits=[];
1084
+ function objectProperties(start){
1085
+ if(tokens[start]?.type!=='punctuator'||tokens[start].value!=='{')return [];
1086
+ const properties=[];
1087
+ let cursor=start+1;
1088
+ while(cursor<tokens.length&&!(tokens[cursor].type==='punctuator'&&tokens[cursor].value==='}')){
1089
+ const key=tokens[cursor];
1090
+ if(key.type!=='string'||tokens[cursor+1]?.value!==':')return [];
1091
+ const valueIndex=cursor+2;
1092
+ const value=tokens[valueIndex];
1093
+ if(!value)return [];
1094
+ properties.push({key:key.value,valueIndex,value});
1095
+ let end=valueIndex;
1096
+ if(value.type==='punctuator'&&(value.value==='{'||value.value==='[')){
1097
+ end=matchingToken(tokens,valueIndex,value.value,value.value==='{'?'}':']');
1098
+ if(end<0)return [];
1099
+ }
1100
+ cursor=end+1;
1101
+ if(tokens[cursor]?.value===',')cursor+=1;
1102
+ else if(tokens[cursor]?.value!=='}')return [];
1103
+ }
1104
+ return properties;
1105
+ }
1106
+ function addImports(start){
1107
+ for(const property of objectProperties(start)){
1108
+ if(property.value.type!=='string')continue;
1109
+ if(property.key.endsWith('/')||property.value.value.split(/[?#]/u)[0].endsWith('/'))continue;
1110
+ reportAssetReference(onReference,property.value.value,'import');
1111
+ const edit=stringReferenceEdit(source,property.value,version);
1112
+ if(edit)edits.push(edit);
1113
+ }
1114
+ }
1115
+ for(const property of objectProperties(0)){
1116
+ if(property.key==='imports')addImports(property.valueIndex);
1117
+ if(property.key==='scopes'){
1118
+ for(const scope of objectProperties(property.valueIndex))addImports(scope.valueIndex);
1119
+ }
1120
+ }
1121
+ return edits;
1122
+ }
1123
+
1124
+ export function rewriteAssetReferences(source,{filePath,version=SDK_VERSION,onReference}={}){
1125
+ if(typeof source!=='string')throw new TypeError('Asset reference source must be a string.');
1126
+ const extension=path.extname(String(filePath??'')).toLowerCase();
1127
+ if(extension==='.js'||extension==='.mjs'){
1128
+ return applyReferenceEdits(source,javascriptReferenceEdits(source,version,onReference));
1129
+ }
1130
+ if(extension==='.css')return applyReferenceEdits(source,cssReferenceEdits(source,version,onReference));
1131
+ if(extension==='.html'||extension==='.htm'){
1132
+ return applyReferenceEdits(source,htmlReferenceEdits(source,version,onReference));
1133
+ }
1134
+ return source;
1135
+ }
1136
+
787
1137
  function registerSpecifier(registry,specifier,target){
788
1138
  registry.set(specifier,{specifier,target});
789
1139
  }
@@ -798,7 +1148,7 @@ function validateInventory(files){
798
1148
  return exact;
799
1149
  }
800
1150
 
801
- export async function buildImportMap({files,signal}={}){
1151
+ export async function buildImportMap({files,signal,version=SDK_VERSION}={}){
802
1152
  throwIfAborted(signal);
803
1153
  const inventory=validateInventory(files);
804
1154
  const modules=[...inventory]
@@ -858,9 +1208,14 @@ export async function buildImportMap({files,signal}={}){
858
1208
  './arcane/sdk/dependencies/event-pubsub/index.js'
859
1209
  );
860
1210
  }
1211
+ for(const relative of [...inventory].sort(compareText)){
1212
+ if(JAVASCRIPT_EXTENSION.test(relative)){
1213
+ registerSpecifier(namedRegistry,`./arcane/${relative}`,`./arcane/${relative}`);
1214
+ }
1215
+ }
861
1216
  const imports={};
862
1217
  for(const entry of [...namedRegistry.values()].sort((left,right)=>compareText(left.specifier,right.specifier))){
863
- imports[entry.specifier]=entry.target;
1218
+ imports[entry.specifier]=versionAssetUrl(entry.target,version);
864
1219
  }
865
1220
  return {
866
1221
  imports,
@@ -918,9 +1273,22 @@ async function physicalRuntime(workspaceRoot,signal){
918
1273
 
919
1274
  async function managedImportMapBuild(resolvedWorkspace,signal){
920
1275
  const runtime=await physicalRuntime(resolvedWorkspace,signal);
921
- const built=await buildImportMap({files:runtime.files,signal});
1276
+ const version=await readWorkspaceAssetVersion(resolvedWorkspace);
1277
+ const built=await buildImportMap({files:runtime.files,signal,version});
922
1278
  const json=`${JSON.stringify({imports:built.imports},null,2).replaceAll('<','\\u003c')}\n`;
923
- return {built,json};
1279
+ return {built,json,version};
1280
+ }
1281
+
1282
+ export async function readWorkspaceAssetVersion(workspaceRoot){
1283
+ let source;
1284
+ try{source=await readFileFromDisk(path.join(workspaceRoot,'arcane.lock.json'),'utf8');}
1285
+ catch(error){
1286
+ if(error?.code==='ENOENT')return SDK_VERSION;
1287
+ throw error;
1288
+ }
1289
+ const document=JSON.parse(source);
1290
+ const version=document?.sdk?.version;
1291
+ return typeof version==='string'&&version?version:SDK_VERSION;
924
1292
  }
925
1293
 
926
1294
  function asciiLower(value){
@@ -946,7 +1314,9 @@ function htmlTagName(value){
946
1314
  function parseTagAttributes(openTag){
947
1315
  const attributes=new Map();
948
1316
  const duplicates=new Set();
1317
+ const positions=new Map();
949
1318
  Object.defineProperty(attributes,'duplicates',{value:duplicates});
1319
+ Object.defineProperty(attributes,'positions',{value:positions});
950
1320
  const tagHead=openTag.match(/^<[A-Za-z][^\t\n\f\r />]*(?=[\t\n\f\r />])/u);
951
1321
  if(!tagHead)fail('Application HTML contains a malformed structural start tag.');
952
1322
  let index=tagHead[0].length;
@@ -963,25 +1333,34 @@ function parseTagAttributes(openTag){
963
1333
  const name=asciiLower(openTag.slice(start,index));
964
1334
  while(/[\t\n\f\r ]/u.test(openTag[index]??''))index+=1;
965
1335
  let value='';
1336
+ let valueStart=index;
1337
+ let valueEnd=index;
1338
+ let quote='';
966
1339
  if(openTag[index]==='='){
967
1340
  index+=1;
968
1341
  while(/[\t\n\f\r ]/u.test(openTag[index]??''))index+=1;
969
- const quote=openTag[index];
1342
+ quote=openTag[index];
970
1343
  if(quote==='\''||quote==='"'){
971
1344
  index+=1;
972
- const valueStart=index;
1345
+ valueStart=index;
973
1346
  while(index<openTag.length&&openTag[index]!==quote)index+=1;
1347
+ valueEnd=index;
974
1348
  value=openTag.slice(valueStart,index);
975
1349
  if(openTag[index]===quote)index+=1;
976
1350
  }else{
977
- const valueStart=index;
1351
+ quote='';
1352
+ valueStart=index;
978
1353
  while(index<openTag.length&&!/[\t\n\f\r >]/u.test(openTag[index]))index+=1;
1354
+ valueEnd=index;
979
1355
  value=openTag.slice(valueStart,index);
980
1356
  }
981
1357
  }
982
1358
  if(name){
983
1359
  if(attributes.has(name))duplicates.add(name);
984
- else attributes.set(name,value);
1360
+ else{
1361
+ attributes.set(name,value);
1362
+ positions.set(name,{start:valueStart,end:valueEnd,quote});
1363
+ }
985
1364
  }
986
1365
  }
987
1366
  return attributes;
@@ -1004,7 +1383,9 @@ function decodeStructuralAttribute(value,label){
1004
1383
  return String.fromCodePoint(point);
1005
1384
  }
1006
1385
  );
1007
- if(decoded.includes('&')){
1386
+ if(/&(?:#[^;\s&]*|[A-Za-z][A-Za-z0-9]*);/u.test(source.replace(
1387
+ /&(?:#[0-9]+|#x[a-f0-9]+|amp|apos|gt|lt|quot);/giu,''
1388
+ ))){
1008
1389
  fail(
1009
1390
  `Application HTML ${label} contains an unsupported or ambiguous character reference.`
1010
1391
  );
@@ -1087,13 +1468,13 @@ function rawElementEnd(html,tag,openEnd){
1087
1468
  const closePattern=new RegExp(`<\\/${tag}(?=[\\t\\n\\f\\r />]|$)`,'gi');
1088
1469
  closePattern.lastIndex=openEnd;
1089
1470
  const close=closePattern.exec(html);
1090
- if(!close)return {end:html.length,closed:false};
1471
+ if(!close)return {end:html.length,contentEnd:html.length,closed:false};
1091
1472
  const end=htmlTagEnd(html,close.index+close[0].length);
1092
1473
  const closeTag=html.slice(close.index,end);
1093
1474
  if(!new RegExp(`^<\\/${tag}[\\t\\n\\f\\r ]*>$`,'i').test(closeTag)){
1094
1475
  fail(`Application HTML contains a malformed </${tag}> end tag.`);
1095
1476
  }
1096
- return {end,closed:true};
1477
+ return {end,contentEnd:close.index,closed:true};
1097
1478
  }
1098
1479
 
1099
1480
  function commentEnd(html,start){
@@ -1179,6 +1560,8 @@ function scanHtmlStructure(html){
1179
1560
  const links=[];
1180
1561
  const bases=[];
1181
1562
  const metas=[];
1563
+ const elements=[];
1564
+ const styles=[];
1182
1565
  let headClose=-1;
1183
1566
  let bodyClose=-1;
1184
1567
  let cursor=0;
@@ -1225,6 +1608,7 @@ function scanHtmlStructure(html){
1225
1608
  cursor=openEnd;
1226
1609
  continue;
1227
1610
  }
1611
+ if(tag!=='template')elements.push({tag,start,end:openEnd,open});
1228
1612
  if(tag==='link'){
1229
1613
  links.push({start,end:openEnd,open});
1230
1614
  cursor=openEnd;
@@ -1256,13 +1640,158 @@ function scanHtmlStructure(html){
1256
1640
  if(tag==='script')scripts.push({
1257
1641
  start,
1258
1642
  openEnd,
1643
+ contentEnd:raw.contentEnd,
1259
1644
  end:raw.end,
1260
1645
  open,
1261
1646
  closed:raw.closed
1262
1647
  });
1648
+ if(tag==='style')styles.push({start:openEnd,end:raw.contentEnd});
1263
1649
  cursor=raw.end;
1264
1650
  }
1265
- return {scripts,links,bases,metas,headClose,bodyClose};
1651
+ return {scripts,links,bases,metas,elements,styles,headClose,bodyClose};
1652
+ }
1653
+
1654
+ function htmlAttributeView(value){
1655
+ let decoded='';
1656
+ const positions=[];
1657
+ for(let cursor=0;cursor<value.length;){
1658
+ const entity=value.slice(cursor).match(/^&(?:#([0-9]+)|#x([a-f0-9]+)|(amp|apos|gt|lt|quot));/iu);
1659
+ if(entity){
1660
+ const point=entity[1]||entity[2]
1661
+ ?Number.parseInt(entity[1]??entity[2],entity[1]?10:16)
1662
+ :null;
1663
+ if(point!==null&&(!Number.isSafeInteger(point)||point<=0||point>0x10ffff))return null;
1664
+ const character=point===null
1665
+ ?{amp:'&',apos:"'",gt:'>',lt:'<',quot:'"'}[asciiLower(entity[3])]
1666
+ :String.fromCodePoint(point);
1667
+ for(let index=0;index<character.length;index+=1)positions.push(cursor);
1668
+ decoded+=character;
1669
+ cursor+=entity[0].length;
1670
+ }else{
1671
+ if(/^&[A-Za-z][A-Za-z0-9]*;/u.test(value.slice(cursor)))return null;
1672
+ positions.push(cursor);
1673
+ decoded+=value[cursor++];
1674
+ }
1675
+ }
1676
+ positions.push(value.length);
1677
+ return {decoded,positions};
1678
+ }
1679
+
1680
+ function versionHtmlAttribute(value,version){
1681
+ // Map decoded URL positions back to authored HTML so existing query spelling survives.
1682
+ const view=htmlAttributeView(value);
1683
+ if(!view)return value;
1684
+ const {decoded,positions}=view;
1685
+ const edits=assetUrlVersionEdits(decoded,version).map(function authoredAttributeEdit(edit){
1686
+ return {
1687
+ start:positions[edit.start],
1688
+ end:positions[edit.end],
1689
+ value:edit.value.replaceAll('&','&amp;')
1690
+ };
1691
+ });
1692
+ return applyReferenceEdits(value,edits);
1693
+ }
1694
+
1695
+ function htmlStyleReferenceEdits(source,version,onReference){
1696
+ const view=htmlAttributeView(source);
1697
+ if(!view)return [];
1698
+ const edits=cssReferenceEdits(view.decoded,version,onReference);
1699
+ return edits.map(function authoredStyleReference(edit){
1700
+ const start=view.positions[edit.start];
1701
+ const end=view.positions[edit.end];
1702
+ return {start,end,value:versionHtmlAttribute(source.slice(start,end),version)};
1703
+ });
1704
+ }
1705
+
1706
+ function srcsetReferenceEdits(source,version,onReference){
1707
+ const edits=[];
1708
+ let cursor=0;
1709
+ while(cursor<source.length){
1710
+ while(/[\t\n\f\r ,]/u.test(source[cursor]??''))cursor+=1;
1711
+ const start=cursor;
1712
+ while(cursor<source.length&&!/[\t\n\f\r ]/u.test(source[cursor]))cursor+=1;
1713
+ let end=cursor;
1714
+ while(end>start&&source[end-1]===',')end-=1;
1715
+ const original=source.slice(start,end);
1716
+ const view=htmlAttributeView(original);
1717
+ if(view)reportAssetReference(onReference,view.decoded,'asset');
1718
+ const value=versionHtmlAttribute(original,version);
1719
+ if(value!==original)edits.push({start,end,value});
1720
+ if(end<cursor)continue;
1721
+ while(cursor<source.length&&source[cursor]!==',')cursor+=1;
1722
+ }
1723
+ return edits;
1724
+ }
1725
+
1726
+ function htmlReferenceEdits(source,version,onReference){
1727
+ const structure=scanHtmlStructure(source);
1728
+ const edits=[];
1729
+ const sourceTags=new Set(['script','html-import','img','audio','video','source','track','iframe','embed']);
1730
+ const linkResources=new Set(['stylesheet','modulepreload','preload','icon','manifest']);
1731
+ const base=structure.bases[0];
1732
+ const baseHref=base?structuralAttribute(parseTagAttributes(base.open),'href','base'):null;
1733
+ if(baseHref&&(/^[A-Za-z][A-Za-z0-9+.-]*:/u.test(baseHref)||baseHref.startsWith('//')))version=null;
1734
+ function reportHtmlReference(reference){
1735
+ if(typeof onReference==='function')onReference({...reference,baseHref});
1736
+ }
1737
+ function addNestedEdits(offset,nested){
1738
+ for(const edit of nested)edits.push({...edit,start:offset+edit.start,end:offset+edit.end});
1739
+ }
1740
+ for(const element of structure.elements){
1741
+ const attributes=parseTagAttributes(element.open);
1742
+ const selected=[];
1743
+ const activeScript=element.tag!=='script'
1744
+ ||['','module','text/javascript','application/javascript'].includes(scriptType(attributes));
1745
+ if(sourceTags.has(element.tag)&&element.tag!=='html-import'&&activeScript)selected.push('src');
1746
+ if(element.tag==='html-import')selected.push('href');
1747
+ if(element.tag==='video')selected.push('poster');
1748
+ if(element.tag==='object')selected.push('data');
1749
+ if(element.tag==='input'&&canonicalHtmlToken(attributes.get('type')??'')==='image')selected.push('src');
1750
+ const relationships=canonicalHtmlToken(attributes.get('rel')??'').split(/[\t\n\f\r ]+/u);
1751
+ if(element.tag==='link'){
1752
+ if(relationships.some(function loadsResource(relationship){return linkResources.has(relationship);})){selected.push('href');}
1753
+ }
1754
+ for(const name of selected){
1755
+ const original=attributes.get(name);
1756
+ if(!original)continue;
1757
+ const destination=canonicalHtmlToken(attributes.get('as')??'');
1758
+ const kind=element.tag==='script'?'script'
1759
+ :element.tag==='html-import'?'component'
1760
+ :element.tag==='iframe'?'document'
1761
+ :element.tag==='link'&&relationships.includes('stylesheet')?'style'
1762
+ :element.tag==='link'&&relationships.includes('modulepreload')?'script'
1763
+ :element.tag==='link'&&relationships.includes('preload')
1764
+ &&['script','worker','style'].includes(destination)?destination==='style'?'style':'script':'asset';
1765
+ const view=htmlAttributeView(original);
1766
+ if(view)reportAssetReference(reportHtmlReference,view.decoded,kind);
1767
+ const value=versionHtmlAttribute(original,version);
1768
+ if(value===original)continue;
1769
+ const position=attributes.positions.get(name);
1770
+ edits.push({start:element.start+position.start,end:element.start+position.end,value});
1771
+ }
1772
+ if((element.tag==='img'||element.tag==='source')&&attributes.has('srcset')){
1773
+ addNestedEdits(element.start+attributes.positions.get('srcset').start,
1774
+ srcsetReferenceEdits(attributes.get('srcset'),version,reportHtmlReference));
1775
+ }
1776
+ if(attributes.has('style')){
1777
+ addNestedEdits(element.start+attributes.positions.get('style').start,
1778
+ htmlStyleReferenceEdits(attributes.get('style'),version,reportHtmlReference));
1779
+ }
1780
+ }
1781
+ for(const script of structure.scripts){
1782
+ const attributes=parseTagAttributes(script.open);
1783
+ if(attributes.has('src'))continue;
1784
+ const type=scriptType(attributes);
1785
+ const body=source.slice(script.openEnd,script.contentEnd);
1786
+ if(type==='importmap')addNestedEdits(script.openEnd,importMapReferenceEdits(body,version,reportHtmlReference));
1787
+ else if(['','module','text/javascript','application/javascript'].includes(type)){
1788
+ addNestedEdits(script.openEnd,javascriptReferenceEdits(body,version,reportHtmlReference));
1789
+ }
1790
+ }
1791
+ for(const style of structure.styles){
1792
+ addNestedEdits(style.start,cssReferenceEdits(source.slice(style.start,style.end),version,reportHtmlReference));
1793
+ }
1794
+ return edits;
1266
1795
  }
1267
1796
 
1268
1797
  function removeManagedBlocks(html,blocks){
@@ -1579,7 +2108,7 @@ export async function createApplicationTestImportMapContext({
1579
2108
  fail(`Application test import-map entry is invalid: ${String(specifier)}.`);
1580
2109
  }
1581
2110
  const relative=safeRelativePath(
1582
- target.slice(2),
2111
+ decodeURIComponent(target.slice(2).split(/[?#]/u)[0]),
1583
2112
  `application test import-map target for ${specifier}`
1584
2113
  );
1585
2114
  if(boundary==='source'&&/^(?:dist|test)\//u.test(relative)){
@@ -1699,10 +2228,13 @@ async function generateImportMapUnlocked({
1699
2228
  renderManagedHtml(html,'{"imports":{}}\n',baseHref);
1700
2229
  documentStates.push({filePath:documentPath,html,label,baseHref});
1701
2230
  }
1702
- const {built,json}=await managedImportMapBuild(resolvedWorkspace,signal);
2231
+ const {built,json,version}=await managedImportMapBuild(resolvedWorkspace,signal);
1703
2232
  const renderedDocuments=documentStates.map(item=>({
1704
2233
  ...item,
1705
- content:renderManagedHtml(item.html,json,item.baseHref)
2234
+ content:rewriteAssetReferences(renderManagedHtml(item.html,json,item.baseHref),{
2235
+ filePath:item.filePath,
2236
+ version
2237
+ })
1706
2238
  }));
1707
2239
 
1708
2240
  throwIfAborted(signal);