arcane-os 0.5.12 → 0.5.14

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 (54) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/browser-runtime/ai/browser-wasm-llm-provider.mjs +11 -10
  3. package/browser-runtime/ai/browser-wllama-runtime.mjs +2 -1
  4. package/browser-runtime/ai/model-controller.mjs +6 -5
  5. package/browser-runtime/ai/speech-worker-client.mjs +45 -3
  6. package/browser-runtime/event-manager.mjs +2 -1
  7. package/browser-runtime/logging.mjs +58 -0
  8. package/docs/reference/README.md +8 -7
  9. package/docs/reference/ai/browser-speech.md +46 -1
  10. package/docs/reference/asset-versioning.md +69 -0
  11. package/docs/reference/inventory/package-api.json +30 -2
  12. package/docs/reference/runtime-modules.md +18 -0
  13. package/docs/reference/sdk-api.md +94 -8
  14. package/package.json +3 -2
  15. package/runtime/arcane/components/app-bar.html +3 -1
  16. package/runtime/arcane/components/calculator.html +1 -1
  17. package/runtime/arcane/components/chart.html +2 -7
  18. package/runtime/arcane/components/chat.html +22 -20
  19. package/runtime/arcane/components/dashboard-config.html +6 -9
  20. package/runtime/arcane/components/data-maintenance.html +4 -2
  21. package/runtime/arcane/components/data-view.html +3 -1
  22. package/runtime/arcane/components/directory-picker.html +3 -1
  23. package/runtime/arcane/components/file-manager.html +17 -15
  24. package/runtime/arcane/components/header.html +5 -3
  25. package/runtime/arcane/components/markdown-document.html +4 -7
  26. package/runtime/arcane/components/markdown-editor.html +6 -9
  27. package/runtime/arcane/components/media-embed.html +1 -1
  28. package/runtime/arcane/components/modal.html +3 -1
  29. package/runtime/arcane/components/screen-capture.html +4 -2
  30. package/runtime/arcane/components/speech.html +10 -8
  31. package/runtime/arcane/components/table.html +3 -1
  32. package/runtime/arcane/components/voice-transcription.html +12 -15
  33. package/runtime/arcane/entities/Chat.js +5 -4
  34. package/runtime/arcane/entities/User.js +2 -1
  35. package/runtime/arcane/modules/AI.js +442 -292
  36. package/runtime/arcane/modules/AIProviderRuntime.js +359 -82
  37. package/runtime/arcane/modules/CommunicationAppController.js +2 -1
  38. package/runtime/arcane/modules/ComponentContracts.js +3 -2
  39. package/runtime/arcane/modules/ConversationTimebox.js +2 -1
  40. package/runtime/arcane/modules/DBOPFS.js +5 -4
  41. package/runtime/arcane/modules/Errors.js +3 -14
  42. package/runtime/arcane/modules/HTMLImport.js +188 -16
  43. package/runtime/arcane/modules/LocalAIReadinessController.js +2 -1
  44. package/runtime/arcane/modules/MD.js +3 -2
  45. package/runtime/arcane/modules/MailOutbox.mjs +2 -1
  46. package/runtime/arcane/modules/PersistentAIChatSession.js +2 -1
  47. package/runtime/arcane/modules/ScreenCapture.js +2 -1
  48. package/runtime/arcane/modules/ThemeBootstrap.js +3 -2
  49. package/runtime/arcane/modules/ToolCallRouter.js +2 -1
  50. package/src/dev-server.mjs +72 -5
  51. package/src/import-map.mjs +603 -16
  52. package/src/installed-sdk-runtime.mjs +1 -0
  53. package/src/packager/core.mjs +60 -1
  54. 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,10 +792,405 @@ 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,urlEdits=assetUrlVersionEdits(token.value,version)){
848
+ if(urlEdits.length===0)return null;
849
+ const quote=source[token.start];
850
+ const positions=[];
851
+ let cursor=token.start+1;
852
+ while(cursor<token.end-1){
853
+ const start=cursor;
854
+ if(source[cursor]==='\\'){
855
+ const decoded=decodedEscape(source,cursor+1);
856
+ for(let index=0;index<decoded.value.length;index+=1)positions.push(start-token.start);
857
+ cursor=decoded.next;
858
+ }else{
859
+ positions.push(cursor-token.start);
860
+ cursor+=1;
861
+ }
862
+ }
863
+ positions.push(token.end-token.start-1);
864
+ const edits=urlEdits.map(function authoredStringReference(edit){
865
+ return {
866
+ start:positions[edit.start],
867
+ end:positions[edit.end],
868
+ value:edit.value.replaceAll('\\','\\\\').replaceAll(quote,`\\${quote}`)
869
+ };
870
+ });
871
+ const original=source.slice(token.start,token.end);
872
+ const value=applyReferenceEdits(original,edits);
873
+ return value===original?null:{start:token.start,end:token.end,value};
874
+ }
875
+
876
+ function callArgumentTokens(tokens,opening){
877
+ const closing=matchingToken(tokens,opening,'(',')');
878
+ if(closing<0)return [];
879
+ const separators=topLevelCommas(tokens,opening+1,closing);
880
+ const boundaries=[opening,...separators,closing];
881
+ const argumentsList=[];
882
+ for(let index=0;index<boundaries.length-1;index+=1){
883
+ argumentsList.push(tokens.slice(boundaries[index]+1,boundaries[index+1]));
884
+ }
885
+ return argumentsList;
886
+ }
887
+
888
+ function literalExpressionResults(tokens){
889
+ if(tokens.length===1&&tokens[0].type==='string')return tokens;
890
+ if(tokens[0]?.value==='('&&matchingToken(tokens,0,'(',')')===tokens.length-1){
891
+ return literalExpressionResults(tokens.slice(1,-1));
892
+ }
893
+ let depth=0;
894
+ let conditional=-1;
895
+ let nestedConditionals=0;
896
+ for(let index=0;index<tokens.length;index+=1){
897
+ if(tokens[index].type!=='punctuator')continue;
898
+ const value=tokens[index].value;
899
+ if(['(','[','{'].includes(value)){depth+=1;continue;}
900
+ if([')',']','}'].includes(value)){depth-=1;continue;}
901
+ if(depth!==0)continue;
902
+ if(value==='?'){
903
+ if(conditional<0)conditional=index;
904
+ else nestedConditionals+=1;
905
+ }else if(value===':'&&conditional>=0){
906
+ if(nestedConditionals>0){nestedConditionals-=1;continue;}
907
+ return [
908
+ ...literalExpressionResults(tokens.slice(conditional+1,index)),
909
+ ...literalExpressionResults(tokens.slice(index+1))
910
+ ];
911
+ }
912
+ }
913
+ return [];
914
+ }
915
+
916
+ function globalResourceCallee(tokens,index){
917
+ if(!identifierIsProperty(tokens,index))return index;
918
+ if((tokens[index-1]?.value==='.'||tokens[index-1]?.value==='?.')
919
+ &&new Set(['globalThis','window','self']).has(tokens[index-2]?.value)
920
+ &&!identifierIsProperty(tokens,index-2))return index-2;
921
+ return -1;
922
+ }
923
+
924
+ function localResourceBase(tokens){
925
+ if(!tokens)return false;
926
+ if(tokens.some(function nonExpressionToken(token){return !['identifier','punctuator'].includes(token.type);}))return false;
927
+ const base=tokens.map(function baseToken(token){return token.value;}).join('');
928
+ return [
929
+ 'import.meta.url','document.baseURI','location.href','window.location.href',
930
+ 'globalThis.location.href','self.location.href','window.document.baseURI',
931
+ 'globalThis.document.baseURI'
932
+ ].includes(base);
933
+ }
934
+
935
+ function literalResourceFetch(argumentsList){
936
+ if(argumentsList.length===1)return true;
937
+ const options=argumentsList[1];
938
+ if(options?.[0]?.value!=='{'||options.at(-1)?.value!=='}')return false;
939
+ for(let index=1;index<options.length-1;index+=1){
940
+ const token=options[index];
941
+ if(token.value==='...'||(token.value==='.'&&options[index+1]?.value==='.'))return false;
942
+ if(token.value==='body')return false;
943
+ if(options[index+1]?.value!==':')continue;
944
+ if(token.value==='method'&&(options[index+2]?.type!=='string'
945
+ ||!['GET','HEAD'].includes(options[index+2].value.toUpperCase())))return false;
946
+ }
947
+ return true;
948
+ }
949
+
950
+ function reportAssetReference(onReference,url,kind,baseKind){
951
+ if(typeof onReference==='function')onReference({url,kind,baseHref:null,...(baseKind?{baseKind}:{})});
952
+ }
953
+
954
+ function javascriptReferenceEdits(source,version,onReference){
955
+ const tokens=tokenize(source);
956
+ const selected=new Map();
957
+ const imports=moduleImportsFromTokens(tokens,'asset source').imports;
958
+ const byOffset=new Map(tokens.map(function tokenByOffset(token){
959
+ return [token.start,token];
960
+ }));
961
+ for(const entry of imports){
962
+ reportAssetReference(onReference,entry.specifier,'import');
963
+ if(/^(?:\.{1,2}\/|\/)/u.test(entry.specifier)){
964
+ selected.set(entry.offset,byOffset.get(entry.offset));
965
+ }
966
+ }
967
+ for(let index=0;index<tokens.length;index+=1){
968
+ const token=tokens[index];
969
+ if(token.type!=='identifier'||tokens[index+1]?.value!=='(')continue;
970
+ if(!['URL','Worker','SharedWorker','importScripts','fetch'].includes(token.value))continue;
971
+ const callee=globalResourceCallee(tokens,index);
972
+ if(callee<0)continue;
973
+ const constructor=tokens[callee-1]?.value==='new';
974
+ if(['URL','Worker','SharedWorker'].includes(token.value)&&!constructor)continue;
975
+ const argumentsList=callArgumentTokens(tokens,index+1);
976
+ if(token.value==='fetch'&&!literalResourceFetch(argumentsList))continue;
977
+ if(token.value==='URL'&&!localResourceBase(argumentsList[1]))continue;
978
+ const selectedArguments=token.value==='importScripts'?argumentsList:argumentsList.slice(0,1);
979
+ for(const argument of selectedArguments){
980
+ for(const literal of literalExpressionResults(argument)){
981
+ if((token.value==='fetch'||token.value==='URL')
982
+ &&!RESOURCE_EXTENSION.test(literal.value))continue;
983
+ if((token.value==='fetch'||token.value==='URL')
984
+ &&/\.html?(?:[?#]|$)/iu.test(literal.value))continue;
985
+ const kind=token.value==='fetch'?'fetch'
986
+ :token.value==='URL'&&!/\.(?:js|mjs)(?:[?#]|$)/iu.test(literal.value)?'asset':'script';
987
+ const documentBase=['Worker','SharedWorker','fetch'].includes(token.value)
988
+ ||(token.value==='URL'&&argumentsList[1]?.[0]?.value!=='import');
989
+ reportAssetReference(onReference,literal.value,kind,documentBase?'document':undefined);
990
+ selected.set(literal.start,literal);
991
+ }
992
+ }
993
+ }
994
+ return [...selected.values()].map(function versionJavaScriptReference(token){
995
+ return stringReferenceEdit(source,token,version);
996
+ }).filter(Boolean);
997
+ }
998
+
999
+ function cssStringEnd(source,start){
1000
+ const quote=source[start];
1001
+ let cursor=start+1;
1002
+ while(cursor<source.length){
1003
+ if(source[cursor]==='\\'){cursor+=2;continue;}
1004
+ if(source[cursor]===quote)return cursor+1;
1005
+ cursor+=1;
1006
+ }
1007
+ return source.length;
1008
+ }
1009
+
1010
+ function cssReferenceEdits(source,version,onReference,versionReference=versionAssetUrl){
1011
+ const edits=[];
1012
+ let cursor=0;
1013
+ let importUrlStart=-1;
1014
+ function skipSpaceAndComments(start){
1015
+ let position=start;
1016
+ while(position<source.length){
1017
+ if(/\s/u.test(source[position])){position+=1;continue;}
1018
+ if(source.startsWith('/*',position)){
1019
+ const close=source.indexOf('*/',position+2);
1020
+ position=close<0?source.length:close+2;
1021
+ continue;
1022
+ }
1023
+ break;
1024
+ }
1025
+ return position;
1026
+ }
1027
+ function addReference(start,end,kind='asset'){
1028
+ const original=source.slice(start,end);
1029
+ if(original.includes('\\'))return;
1030
+ reportAssetReference(onReference,original,kind);
1031
+ const value=versionReference(original,version);
1032
+ if(value!==original)edits.push({start,end,value});
1033
+ }
1034
+ while(cursor<source.length){
1035
+ if(source.startsWith('/*',cursor)){
1036
+ const end=source.indexOf('*/',cursor+2);
1037
+ cursor=end<0?source.length:end+2;
1038
+ continue;
1039
+ }
1040
+ if(source[cursor]==='"'||source[cursor]==="'"){
1041
+ cursor=cssStringEnd(source,cursor);
1042
+ continue;
1043
+ }
1044
+ const rest=source.slice(cursor);
1045
+ if(/^@import(?![\w-])/iu.test(rest)){
1046
+ const start=skipSpaceAndComments(cursor+7);
1047
+ importUrlStart=start;
1048
+ if(source[start]==='"'||source[start]==="'"){
1049
+ const end=cssStringEnd(source,start);
1050
+ addReference(start+1,end-1,'style');
1051
+ cursor=end;
1052
+ continue;
1053
+ }
1054
+ }
1055
+ if(!/[\w-]/u.test(source[cursor-1]??'')&&/^url\s*\(/iu.test(rest)){
1056
+ const kind=cursor===importUrlStart?'style':'asset';
1057
+ const open=source.indexOf('(',cursor);
1058
+ const start=skipSpaceAndComments(open+1);
1059
+ if(source[start]==='"'||source[start]==="'"){
1060
+ const end=cssStringEnd(source,start);
1061
+ addReference(start+1,end-1,kind);
1062
+ cursor=end;
1063
+ }else{
1064
+ let end=start;
1065
+ while(end<source.length&&source[end]!==')'){
1066
+ end+=source[end]==='\\'?2:1;
1067
+ }
1068
+ let valueEnd=Math.min(end,source.length);
1069
+ while(valueEnd>start&&/\s/u.test(source[valueEnd-1]))valueEnd-=1;
1070
+ addReference(start,valueEnd,kind);
1071
+ cursor=Math.min(end+1,source.length);
1072
+ }
1073
+ continue;
1074
+ }
1075
+ cursor+=1;
1076
+ }
1077
+ return edits;
1078
+ }
1079
+
1080
+ function importMapReferenceEdits(source,version,onReference){
1081
+ const tokens=tokenize(source);
1082
+ const edits=[];
1083
+ function objectProperties(start){
1084
+ if(tokens[start]?.type!=='punctuator'||tokens[start].value!=='{')return [];
1085
+ const properties=[];
1086
+ let cursor=start+1;
1087
+ while(cursor<tokens.length&&!(tokens[cursor].type==='punctuator'&&tokens[cursor].value==='}')){
1088
+ const key=tokens[cursor];
1089
+ if(key.type!=='string'||tokens[cursor+1]?.value!==':')return [];
1090
+ const valueIndex=cursor+2;
1091
+ const value=tokens[valueIndex];
1092
+ if(!value)return [];
1093
+ let end=valueIndex;
1094
+ if(value.type==='punctuator'&&(value.value==='{'||value.value==='[')){
1095
+ end=matchingToken(tokens,valueIndex,value.value,value.value==='{'?'}':']');
1096
+ if(end<0)return [];
1097
+ }
1098
+ properties.push({key:key.value,keyToken:key,valueIndex,value,end:tokens[end].end});
1099
+ cursor=end+1;
1100
+ if(tokens[cursor]?.value===',')cursor+=1;
1101
+ else if(tokens[cursor]?.value!=='}')return [];
1102
+ }
1103
+ return properties;
1104
+ }
1105
+ function addAlias(property,keys,value,aliases,alias=versionImportMapSpecifier(property.key,version)){
1106
+ if(alias===property.key||keys.has(alias))return;
1107
+ const keyEdit=stringReferenceEdit(source,property.keyToken,version,importMapUrlVersionEdits(property.key,version));
1108
+ if(!keyEdit)return;
1109
+ keys.add(alias);
1110
+ aliases.push(`${keyEdit.value}: ${value}`);
1111
+ }
1112
+ function prependAliases(start,aliases,selectedEdits){
1113
+ if(aliases.length===0)return;
1114
+ // Authored keys retain priority even when different spellings normalize to one URL.
1115
+ const offset=tokens[start].end;
1116
+ selectedEdits.push({start:offset,end:offset,value:`${aliases.join(', ')}, `});
1117
+ }
1118
+ function addImports(start){
1119
+ const selectedEdits=[];
1120
+ const aliases=[];
1121
+ const properties=objectProperties(start);
1122
+ const keys=new Set(properties.map(function importKey(property){return property.key;}));
1123
+ for(const property of properties){
1124
+ if(property.value.value==='null'&&property.value.type==='identifier'){
1125
+ addAlias(property,keys,'null',aliases);
1126
+ continue;
1127
+ }
1128
+ if(property.value.type!=='string')continue;
1129
+ if(property.key.endsWith('/'))continue;
1130
+ reportAssetReference(onReference,property.value.value,'import');
1131
+ const edit=property.value.value.split(/[?#]/u)[0].endsWith('/')
1132
+ ?null:stringReferenceEdit(source,property.value,version);
1133
+ if(edit)selectedEdits.push(edit);
1134
+ addAlias(property,keys,edit?.value??source.slice(property.value.start,property.value.end),aliases);
1135
+ }
1136
+ prependAliases(start,aliases,selectedEdits);
1137
+ return selectedEdits;
1138
+ }
1139
+ for(const property of objectProperties(0)){
1140
+ if(property.key==='imports')edits.push(...addImports(property.valueIndex));
1141
+ if(property.key==='scopes'){
1142
+ const scopes=objectProperties(property.valueIndex);
1143
+ const aliases=[];
1144
+ const keys=new Set(scopes.map(function scopeKey(scope){return scope.key;}));
1145
+ for(const scope of scopes){
1146
+ const scopeEdits=addImports(scope.valueIndex);
1147
+ edits.push(...scopeEdits);
1148
+ const value=applyReferenceEdits(source.slice(scope.value.start,scope.end),scopeEdits.map(function scopeRelativeEdit(edit){
1149
+ return {...edit,start:edit.start-scope.value.start,end:edit.end-scope.value.start};
1150
+ }));
1151
+ if(!scope.key.endsWith('/'))addAlias(scope,keys,value,aliases,versionImportMapUrl(scope.key,version));
1152
+ }
1153
+ prependAliases(property.valueIndex,aliases,edits);
1154
+ }
1155
+ }
1156
+ return edits;
1157
+ }
1158
+
1159
+ export function rewriteAssetReferences(source,{filePath,version=SDK_VERSION,onReference}={}){
1160
+ if(typeof source!=='string')throw new TypeError('Asset reference source must be a string.');
1161
+ const extension=path.extname(String(filePath??'')).toLowerCase();
1162
+ if(extension==='.js'||extension==='.mjs'){
1163
+ return applyReferenceEdits(source,javascriptReferenceEdits(source,version,onReference));
1164
+ }
1165
+ if(extension==='.css')return applyReferenceEdits(source,cssReferenceEdits(source,version,onReference));
1166
+ if(extension==='.html'||extension==='.htm'){
1167
+ return applyReferenceEdits(source,htmlReferenceEdits(source,version,onReference));
1168
+ }
1169
+ return source;
1170
+ }
1171
+
787
1172
  function registerSpecifier(registry,specifier,target){
788
1173
  registry.set(specifier,{specifier,target});
789
1174
  }
790
1175
 
1176
+ function versionImportMapSpecifier(specifier,version){
1177
+ if(specifier.endsWith('/')||!/^(?:\.{1,2}\/|\/|[A-Za-z][A-Za-z0-9+.-]*:\/\/)/u.test(specifier))return specifier;
1178
+ return versionImportMapUrl(specifier,version);
1179
+ }
1180
+
1181
+ function versionImportMapUrl(value,version){
1182
+ return applyReferenceEdits(value,importMapUrlVersionEdits(value,version));
1183
+ }
1184
+
1185
+ function importMapUrlVersionEdits(value,version){
1186
+ // Absolute map keys can match a relative import resolved against the document.
1187
+ const authority=/^(?:[A-Za-z][A-Za-z0-9+.-]*:)?\/\/[^/?#]*/u.exec(value)?.[0];
1188
+ const offset=authority?.length??0;
1189
+ return assetUrlVersionEdits(value.slice(offset),version).map(function absoluteKeyEdit(edit){
1190
+ return {...edit,start:edit.start+offset,end:edit.end+offset};
1191
+ });
1192
+ }
1193
+
791
1194
  function validateInventory(files){
792
1195
  if(!Array.isArray(files))throw new TypeError('buildImportMap files must be an array.');
793
1196
  const exact=new Set();
@@ -798,7 +1201,7 @@ function validateInventory(files){
798
1201
  return exact;
799
1202
  }
800
1203
 
801
- export async function buildImportMap({files,signal}={}){
1204
+ export async function buildImportMap({files,signal,version=SDK_VERSION}={}){
802
1205
  throwIfAborted(signal);
803
1206
  const inventory=validateInventory(files);
804
1207
  const modules=[...inventory]
@@ -858,9 +1261,16 @@ export async function buildImportMap({files,signal}={}){
858
1261
  './arcane/sdk/dependencies/event-pubsub/index.js'
859
1262
  );
860
1263
  }
1264
+ for(const relative of [...inventory].sort(compareText)){
1265
+ if(JAVASCRIPT_EXTENSION.test(relative)){
1266
+ registerSpecifier(namedRegistry,`./arcane/${relative}`,`./arcane/${relative}`);
1267
+ }
1268
+ }
861
1269
  const imports={};
862
1270
  for(const entry of [...namedRegistry.values()].sort((left,right)=>compareText(left.specifier,right.specifier))){
863
- imports[entry.specifier]=entry.target;
1271
+ const target=versionAssetUrl(entry.target,version);
1272
+ imports[entry.specifier]=target;
1273
+ imports[versionImportMapSpecifier(entry.specifier,version)]=target;
864
1274
  }
865
1275
  return {
866
1276
  imports,
@@ -918,9 +1328,22 @@ async function physicalRuntime(workspaceRoot,signal){
918
1328
 
919
1329
  async function managedImportMapBuild(resolvedWorkspace,signal){
920
1330
  const runtime=await physicalRuntime(resolvedWorkspace,signal);
921
- const built=await buildImportMap({files:runtime.files,signal});
1331
+ const version=await readWorkspaceAssetVersion(resolvedWorkspace);
1332
+ const built=await buildImportMap({files:runtime.files,signal,version});
922
1333
  const json=`${JSON.stringify({imports:built.imports},null,2).replaceAll('<','\\u003c')}\n`;
923
- return {built,json};
1334
+ return {built,json,version};
1335
+ }
1336
+
1337
+ export async function readWorkspaceAssetVersion(workspaceRoot){
1338
+ let source;
1339
+ try{source=await readFileFromDisk(path.join(workspaceRoot,'arcane.lock.json'),'utf8');}
1340
+ catch(error){
1341
+ if(error?.code==='ENOENT')return SDK_VERSION;
1342
+ throw error;
1343
+ }
1344
+ const document=JSON.parse(source);
1345
+ const version=document?.sdk?.version;
1346
+ return typeof version==='string'&&version?version:SDK_VERSION;
924
1347
  }
925
1348
 
926
1349
  function asciiLower(value){
@@ -946,7 +1369,9 @@ function htmlTagName(value){
946
1369
  function parseTagAttributes(openTag){
947
1370
  const attributes=new Map();
948
1371
  const duplicates=new Set();
1372
+ const positions=new Map();
949
1373
  Object.defineProperty(attributes,'duplicates',{value:duplicates});
1374
+ Object.defineProperty(attributes,'positions',{value:positions});
950
1375
  const tagHead=openTag.match(/^<[A-Za-z][^\t\n\f\r />]*(?=[\t\n\f\r />])/u);
951
1376
  if(!tagHead)fail('Application HTML contains a malformed structural start tag.');
952
1377
  let index=tagHead[0].length;
@@ -963,25 +1388,34 @@ function parseTagAttributes(openTag){
963
1388
  const name=asciiLower(openTag.slice(start,index));
964
1389
  while(/[\t\n\f\r ]/u.test(openTag[index]??''))index+=1;
965
1390
  let value='';
1391
+ let valueStart=index;
1392
+ let valueEnd=index;
1393
+ let quote='';
966
1394
  if(openTag[index]==='='){
967
1395
  index+=1;
968
1396
  while(/[\t\n\f\r ]/u.test(openTag[index]??''))index+=1;
969
- const quote=openTag[index];
1397
+ quote=openTag[index];
970
1398
  if(quote==='\''||quote==='"'){
971
1399
  index+=1;
972
- const valueStart=index;
1400
+ valueStart=index;
973
1401
  while(index<openTag.length&&openTag[index]!==quote)index+=1;
1402
+ valueEnd=index;
974
1403
  value=openTag.slice(valueStart,index);
975
1404
  if(openTag[index]===quote)index+=1;
976
1405
  }else{
977
- const valueStart=index;
1406
+ quote='';
1407
+ valueStart=index;
978
1408
  while(index<openTag.length&&!/[\t\n\f\r >]/u.test(openTag[index]))index+=1;
1409
+ valueEnd=index;
979
1410
  value=openTag.slice(valueStart,index);
980
1411
  }
981
1412
  }
982
1413
  if(name){
983
1414
  if(attributes.has(name))duplicates.add(name);
984
- else attributes.set(name,value);
1415
+ else{
1416
+ attributes.set(name,value);
1417
+ positions.set(name,{start:valueStart,end:valueEnd,quote});
1418
+ }
985
1419
  }
986
1420
  }
987
1421
  return attributes;
@@ -1004,7 +1438,9 @@ function decodeStructuralAttribute(value,label){
1004
1438
  return String.fromCodePoint(point);
1005
1439
  }
1006
1440
  );
1007
- if(decoded.includes('&')){
1441
+ if(/&(?:#[^;\s&]*|[A-Za-z][A-Za-z0-9]*);/u.test(source.replace(
1442
+ /&(?:#[0-9]+|#x[a-f0-9]+|amp|apos|gt|lt|quot);/giu,''
1443
+ ))){
1008
1444
  fail(
1009
1445
  `Application HTML ${label} contains an unsupported or ambiguous character reference.`
1010
1446
  );
@@ -1087,13 +1523,13 @@ function rawElementEnd(html,tag,openEnd){
1087
1523
  const closePattern=new RegExp(`<\\/${tag}(?=[\\t\\n\\f\\r />]|$)`,'gi');
1088
1524
  closePattern.lastIndex=openEnd;
1089
1525
  const close=closePattern.exec(html);
1090
- if(!close)return {end:html.length,closed:false};
1526
+ if(!close)return {end:html.length,contentEnd:html.length,closed:false};
1091
1527
  const end=htmlTagEnd(html,close.index+close[0].length);
1092
1528
  const closeTag=html.slice(close.index,end);
1093
1529
  if(!new RegExp(`^<\\/${tag}[\\t\\n\\f\\r ]*>$`,'i').test(closeTag)){
1094
1530
  fail(`Application HTML contains a malformed </${tag}> end tag.`);
1095
1531
  }
1096
- return {end,closed:true};
1532
+ return {end,contentEnd:close.index,closed:true};
1097
1533
  }
1098
1534
 
1099
1535
  function commentEnd(html,start){
@@ -1179,6 +1615,8 @@ function scanHtmlStructure(html){
1179
1615
  const links=[];
1180
1616
  const bases=[];
1181
1617
  const metas=[];
1618
+ const elements=[];
1619
+ const styles=[];
1182
1620
  let headClose=-1;
1183
1621
  let bodyClose=-1;
1184
1622
  let cursor=0;
@@ -1225,6 +1663,7 @@ function scanHtmlStructure(html){
1225
1663
  cursor=openEnd;
1226
1664
  continue;
1227
1665
  }
1666
+ if(tag!=='template')elements.push({tag,start,end:openEnd,open});
1228
1667
  if(tag==='link'){
1229
1668
  links.push({start,end:openEnd,open});
1230
1669
  cursor=openEnd;
@@ -1256,13 +1695,158 @@ function scanHtmlStructure(html){
1256
1695
  if(tag==='script')scripts.push({
1257
1696
  start,
1258
1697
  openEnd,
1698
+ contentEnd:raw.contentEnd,
1259
1699
  end:raw.end,
1260
1700
  open,
1261
1701
  closed:raw.closed
1262
1702
  });
1703
+ if(tag==='style')styles.push({start:openEnd,end:raw.contentEnd});
1263
1704
  cursor=raw.end;
1264
1705
  }
1265
- return {scripts,links,bases,metas,headClose,bodyClose};
1706
+ return {scripts,links,bases,metas,elements,styles,headClose,bodyClose};
1707
+ }
1708
+
1709
+ function htmlAttributeView(value){
1710
+ let decoded='';
1711
+ const positions=[];
1712
+ for(let cursor=0;cursor<value.length;){
1713
+ const entity=value.slice(cursor).match(/^&(?:#([0-9]+)|#x([a-f0-9]+)|(amp|apos|gt|lt|quot));/iu);
1714
+ if(entity){
1715
+ const point=entity[1]||entity[2]
1716
+ ?Number.parseInt(entity[1]??entity[2],entity[1]?10:16)
1717
+ :null;
1718
+ if(point!==null&&(!Number.isSafeInteger(point)||point<=0||point>0x10ffff))return null;
1719
+ const character=point===null
1720
+ ?{amp:'&',apos:"'",gt:'>',lt:'<',quot:'"'}[asciiLower(entity[3])]
1721
+ :String.fromCodePoint(point);
1722
+ for(let index=0;index<character.length;index+=1)positions.push(cursor);
1723
+ decoded+=character;
1724
+ cursor+=entity[0].length;
1725
+ }else{
1726
+ if(/^&[A-Za-z][A-Za-z0-9]*;/u.test(value.slice(cursor)))return null;
1727
+ positions.push(cursor);
1728
+ decoded+=value[cursor++];
1729
+ }
1730
+ }
1731
+ positions.push(value.length);
1732
+ return {decoded,positions};
1733
+ }
1734
+
1735
+ function versionHtmlAttribute(value,version){
1736
+ // Map decoded URL positions back to authored HTML so existing query spelling survives.
1737
+ const view=htmlAttributeView(value);
1738
+ if(!view)return value;
1739
+ const {decoded,positions}=view;
1740
+ const edits=assetUrlVersionEdits(decoded,version).map(function authoredAttributeEdit(edit){
1741
+ return {
1742
+ start:positions[edit.start],
1743
+ end:positions[edit.end],
1744
+ value:edit.value.replaceAll('&','&amp;')
1745
+ };
1746
+ });
1747
+ return applyReferenceEdits(value,edits);
1748
+ }
1749
+
1750
+ function htmlStyleReferenceEdits(source,version,onReference){
1751
+ const view=htmlAttributeView(source);
1752
+ if(!view)return [];
1753
+ const edits=cssReferenceEdits(view.decoded,version,onReference);
1754
+ return edits.map(function authoredStyleReference(edit){
1755
+ const start=view.positions[edit.start];
1756
+ const end=view.positions[edit.end];
1757
+ return {start,end,value:versionHtmlAttribute(source.slice(start,end),version)};
1758
+ });
1759
+ }
1760
+
1761
+ function srcsetReferenceEdits(source,version,onReference){
1762
+ const edits=[];
1763
+ let cursor=0;
1764
+ while(cursor<source.length){
1765
+ while(/[\t\n\f\r ,]/u.test(source[cursor]??''))cursor+=1;
1766
+ const start=cursor;
1767
+ while(cursor<source.length&&!/[\t\n\f\r ]/u.test(source[cursor]))cursor+=1;
1768
+ let end=cursor;
1769
+ while(end>start&&source[end-1]===',')end-=1;
1770
+ const original=source.slice(start,end);
1771
+ const view=htmlAttributeView(original);
1772
+ if(view)reportAssetReference(onReference,view.decoded,'asset');
1773
+ const value=versionHtmlAttribute(original,version);
1774
+ if(value!==original)edits.push({start,end,value});
1775
+ if(end<cursor)continue;
1776
+ while(cursor<source.length&&source[cursor]!==',')cursor+=1;
1777
+ }
1778
+ return edits;
1779
+ }
1780
+
1781
+ function htmlReferenceEdits(source,version,onReference){
1782
+ const structure=scanHtmlStructure(source);
1783
+ const edits=[];
1784
+ const sourceTags=new Set(['script','html-import','img','audio','video','source','track','iframe','embed']);
1785
+ const linkResources=new Set(['stylesheet','modulepreload','preload','icon','manifest']);
1786
+ const base=structure.bases[0];
1787
+ const baseHref=base?structuralAttribute(parseTagAttributes(base.open),'href','base'):null;
1788
+ if(baseHref&&(/^[A-Za-z][A-Za-z0-9+.-]*:/u.test(baseHref)||baseHref.startsWith('//')))version=null;
1789
+ function reportHtmlReference(reference){
1790
+ if(typeof onReference==='function')onReference({...reference,baseHref});
1791
+ }
1792
+ function addNestedEdits(offset,nested){
1793
+ for(const edit of nested)edits.push({...edit,start:offset+edit.start,end:offset+edit.end});
1794
+ }
1795
+ for(const element of structure.elements){
1796
+ const attributes=parseTagAttributes(element.open);
1797
+ const selected=[];
1798
+ const activeScript=element.tag!=='script'
1799
+ ||['','module','text/javascript','application/javascript'].includes(scriptType(attributes));
1800
+ if(sourceTags.has(element.tag)&&element.tag!=='html-import'&&activeScript)selected.push('src');
1801
+ if(element.tag==='html-import')selected.push('href');
1802
+ if(element.tag==='video')selected.push('poster');
1803
+ if(element.tag==='object')selected.push('data');
1804
+ if(element.tag==='input'&&canonicalHtmlToken(attributes.get('type')??'')==='image')selected.push('src');
1805
+ const relationships=canonicalHtmlToken(attributes.get('rel')??'').split(/[\t\n\f\r ]+/u);
1806
+ if(element.tag==='link'){
1807
+ if(relationships.some(function loadsResource(relationship){return linkResources.has(relationship);})){selected.push('href');}
1808
+ }
1809
+ for(const name of selected){
1810
+ const original=attributes.get(name);
1811
+ if(!original)continue;
1812
+ const destination=canonicalHtmlToken(attributes.get('as')??'');
1813
+ const kind=element.tag==='script'?'script'
1814
+ :element.tag==='html-import'?'component'
1815
+ :element.tag==='iframe'?'document'
1816
+ :element.tag==='link'&&relationships.includes('stylesheet')?'style'
1817
+ :element.tag==='link'&&relationships.includes('modulepreload')?'script'
1818
+ :element.tag==='link'&&relationships.includes('preload')
1819
+ &&['script','worker','style'].includes(destination)?destination==='style'?'style':'script':'asset';
1820
+ const view=htmlAttributeView(original);
1821
+ if(view)reportAssetReference(reportHtmlReference,view.decoded,kind);
1822
+ const value=versionHtmlAttribute(original,version);
1823
+ if(value===original)continue;
1824
+ const position=attributes.positions.get(name);
1825
+ edits.push({start:element.start+position.start,end:element.start+position.end,value});
1826
+ }
1827
+ if((element.tag==='img'||element.tag==='source')&&attributes.has('srcset')){
1828
+ addNestedEdits(element.start+attributes.positions.get('srcset').start,
1829
+ srcsetReferenceEdits(attributes.get('srcset'),version,reportHtmlReference));
1830
+ }
1831
+ if(attributes.has('style')){
1832
+ addNestedEdits(element.start+attributes.positions.get('style').start,
1833
+ htmlStyleReferenceEdits(attributes.get('style'),version,reportHtmlReference));
1834
+ }
1835
+ }
1836
+ for(const script of structure.scripts){
1837
+ const attributes=parseTagAttributes(script.open);
1838
+ if(attributes.has('src'))continue;
1839
+ const type=scriptType(attributes);
1840
+ const body=source.slice(script.openEnd,script.contentEnd);
1841
+ if(type==='importmap')addNestedEdits(script.openEnd,importMapReferenceEdits(body,version,reportHtmlReference));
1842
+ else if(['','module','text/javascript','application/javascript'].includes(type)){
1843
+ addNestedEdits(script.openEnd,javascriptReferenceEdits(body,version,reportHtmlReference));
1844
+ }
1845
+ }
1846
+ for(const style of structure.styles){
1847
+ addNestedEdits(style.start,cssReferenceEdits(source.slice(style.start,style.end),version,reportHtmlReference));
1848
+ }
1849
+ return edits;
1266
1850
  }
1267
1851
 
1268
1852
  function removeManagedBlocks(html,blocks){
@@ -1579,7 +2163,7 @@ export async function createApplicationTestImportMapContext({
1579
2163
  fail(`Application test import-map entry is invalid: ${String(specifier)}.`);
1580
2164
  }
1581
2165
  const relative=safeRelativePath(
1582
- target.slice(2),
2166
+ decodeURIComponent(target.slice(2).split(/[?#]/u)[0]),
1583
2167
  `application test import-map target for ${specifier}`
1584
2168
  );
1585
2169
  if(boundary==='source'&&/^(?:dist|test)\//u.test(relative)){
@@ -1699,10 +2283,13 @@ async function generateImportMapUnlocked({
1699
2283
  renderManagedHtml(html,'{"imports":{}}\n',baseHref);
1700
2284
  documentStates.push({filePath:documentPath,html,label,baseHref});
1701
2285
  }
1702
- const {built,json}=await managedImportMapBuild(resolvedWorkspace,signal);
2286
+ const {built,json,version}=await managedImportMapBuild(resolvedWorkspace,signal);
1703
2287
  const renderedDocuments=documentStates.map(item=>({
1704
2288
  ...item,
1705
- content:renderManagedHtml(item.html,json,item.baseHref)
2289
+ content:rewriteAssetReferences(renderManagedHtml(item.html,json,item.baseHref),{
2290
+ filePath:item.filePath,
2291
+ version
2292
+ })
1706
2293
  }));
1707
2294
 
1708
2295
  throwIfAborted(signal);