arcane-os 0.53.0 → 0.54.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.
Files changed (38) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/NOTICE +4 -1
  3. package/README.md +1 -1
  4. package/bin/arcane-test.mjs +2 -1
  5. package/browser-runtime/ai/browser-decisions.mjs +405 -0
  6. package/browser-runtime/ai/decision-runtime.mjs +281 -0
  7. package/browser-runtime/ai/decision-worker.mjs +67 -0
  8. package/browser-runtime/dependencies/event-pubsub/dependencies/strong-type/index.js +1151 -0
  9. package/browser-runtime/dependencies/event-pubsub/dependencies/strong-type/licence +21 -0
  10. package/browser-runtime/dependencies/event-pubsub/dependencies/strong-type/package.json +61 -0
  11. package/browser-runtime/dependencies/event-pubsub/index.js +2 -2
  12. package/browser-runtime/dependencies/event-pubsub/package.json +11 -4
  13. package/browser-runtime/dependencies/ws-share/WS.js +128 -0
  14. package/browser-runtime/dependencies/ws-share/licence.md +27 -0
  15. package/browser-runtime/dependencies/ws-share/package.json +64 -0
  16. package/browser-runtime/websocket-client.mjs +1 -0
  17. package/docs/event-manager.md +7 -4
  18. package/docs/reference/ai/browser-decisions.md +184 -0
  19. package/docs/reference/inventory/package-api.json +30 -2
  20. package/docs/reference/inventory/runtime-modules.json +17 -4
  21. package/docs/reference/protocols.md +76 -1
  22. package/docs/reference/runtime-modules.md +45 -2
  23. package/docs/reference/sdk-api.md +133 -9
  24. package/docs/reference/websocket-client.md +132 -0
  25. package/node_modules/event-pubsub/CHANGELOG.md +12 -1
  26. package/node_modules/event-pubsub/MIGRATION.md +8 -5
  27. package/node_modules/event-pubsub/README.md +75 -19
  28. package/node_modules/event-pubsub/SECURITY.md +3 -3
  29. package/node_modules/event-pubsub/index.js +2 -2
  30. package/node_modules/event-pubsub/package.json +11 -4
  31. package/package.json +8 -5
  32. package/runtime/arcane/modules/HTMLImport.js +81 -26
  33. package/runtime/arcane/modules/HTMLImportScript.js +3 -0
  34. package/src/import-map.mjs +416 -59
  35. package/src/index.mjs +1 -0
  36. package/src/packager/core.mjs +24 -6
  37. package/src/packager/native-modules.mjs +458 -0
  38. package/src/testing-loader.mjs +41 -20
@@ -41,11 +41,13 @@ const STATIC_RUNTIME_PACKAGE_IMPORTS=new Map([
41
41
  ]);
42
42
  const SDK_BROWSER_SELF_IMPORTS=new Map([
43
43
  ['arcane-os/event-manager',SDK_BROWSER_ENTRY],
44
+ ['arcane-os/websocket-client','sdk/websocket-client.mjs'],
44
45
  ['arcane-os/logging','sdk/logging.mjs'],
45
46
  ['arcane-os/browser-device','sdk/browser-device.mjs'],
46
47
  ['arcane-os/pwa','sdk/pwa.mjs'],
47
48
  ['arcane-os/speech-text','sdk/speech-text.mjs'],
48
49
  ['arcane-os/ai/browser-wasm',SDK_BROWSER_AI_ENTRY],
50
+ ['arcane-os/ai/browser-decisions','sdk/ai/browser-decisions.mjs'],
49
51
  ['arcane-os/ai/tool-text-stream','sdk/ai/tool-text-stream.mjs'],
50
52
  ['arcane-os/ai/twin-cloud','sdk/ai/twin-cloud.mjs'],
51
53
  ['arcane-os/ai/browser-speech',SDK_BROWSER_SPEECH_ENTRY]
@@ -946,17 +948,234 @@ function literalExpressionResults(tokens){
946
948
  return [];
947
949
  }
948
950
 
949
- function globalResourceCallee(tokens,index){
950
- if(!identifierIsProperty(tokens,index))return index;
951
- if((tokens[index-1]?.value==='.'||tokens[index-1]?.value==='?.')
952
- &&new Set(['globalThis','window','self']).has(tokens[index-2]?.value)
953
- &&!identifierIsProperty(tokens,index-2))return index-2;
954
- return -1;
951
+ function resourceBindingScopes(tokens){
952
+ const relevant=new Set([
953
+ 'fetch','URL','Worker','SharedWorker','importScripts',
954
+ 'globalThis','window','self','document','location'
955
+ ]);
956
+ const bindings=new Map();
957
+ const closing=new Map();
958
+ const opening=new Map();
959
+ const delimiters=[];
960
+ for(let index=0;index<tokens.length;index+=1){
961
+ if(tokens[index].type!=='punctuator')continue;
962
+ const value=tokens[index].value;
963
+ if(['(','[','{'].includes(value))delimiters.push(index);
964
+ else if([')',']','}'].includes(value)){
965
+ const start=delimiters.pop();
966
+ if(start!==undefined){closing.set(start,index);opening.set(index,start);}
967
+ }
968
+ }
969
+
970
+ const root={start:0,end:tokens.length,kind:'module',parent:null};
971
+ const scopeAt=[];
972
+ let scope=root;
973
+ for(let index=0;index<tokens.length;index+=1){
974
+ scopeAt[index]=scope;
975
+ if(tokens[index].type!=='punctuator')continue;
976
+ if(tokens[index].value==='{'){
977
+ scope={
978
+ start:index,end:closing.get(index)??tokens.length,
979
+ kind:tokens[index].openingBraceKind,parent:scope
980
+ };
981
+ }else if(tokens[index].value==='}')scope=scope.parent??root;
982
+ }
983
+
984
+ function bind(index,owner){
985
+ const token=tokens[index];
986
+ if(token?.type!=='identifier'||!relevant.has(token.value))return;
987
+ const entries=bindings.get(token.value)??[];
988
+ entries.push({
989
+ start:tokens[owner.start]?.start??0,
990
+ end:tokens[owner.end]?.end??tokens.at(-1)?.end??0
991
+ });
992
+ bindings.set(token.value,entries);
993
+ }
994
+
995
+ function patterns(start,end,owner){
996
+ const boundaries=[start-1,...topLevelCommas(tokens,start,end),end];
997
+ for(let index=0;index<boundaries.length-1;index+=1){
998
+ pattern(boundaries[index]+1,boundaries[index+1],owner);
999
+ }
1000
+ }
1001
+
1002
+ function pattern(start,end,owner){
1003
+ if(start>=end)return;
1004
+ if(tokens[start]?.value==='...')start+=1;
1005
+ const token=tokens[start];
1006
+ if(token?.type==='identifier'){bind(start,owner);return;}
1007
+ const close=closing.get(start);
1008
+ if(close===undefined||close>=end)return;
1009
+ if(token.value==='['){patterns(start+1,close,owner);return;}
1010
+ if(token.value!=='{')return;
1011
+ const boundaries=[start,...topLevelCommas(tokens,start+1,close),close];
1012
+ for(let index=0;index<boundaries.length-1;index+=1){
1013
+ const entry=boundaries[index]+1;
1014
+ const finish=boundaries[index+1];
1015
+ const keyEnd=tokens[entry]?.value==='['?closing.get(entry):entry;
1016
+ const value=tokens[keyEnd+1]?.value===':'?keyEnd+2:entry;
1017
+ pattern(value,finish,owner);
1018
+ }
1019
+ }
1020
+
1021
+ function expressionEnd(start,{declaration=false}={}){
1022
+ let index=start;
1023
+ while(index<tokens.length){
1024
+ const token=tokens[index];
1025
+ if(token.type==='punctuator'){
1026
+ if([';',')',']','}'].includes(token.value))break;
1027
+ if(!declaration&&token.value===',')break;
1028
+ if(closing.has(index)){index=closing.get(index)+1;continue;}
1029
+ }
1030
+ if(declaration&&token.type==='identifier'&&token.lineBreakBefore
1031
+ &&['const','let','var','export','return'].includes(token.value))break;
1032
+ index+=1;
1033
+ }
1034
+ return index;
1035
+ }
1036
+
1037
+ function namedDefinitionEnd(start) {
1038
+ let index = start + 1;
1039
+ while (index < tokens.length) {
1040
+ const token = tokens[index];
1041
+ if (token.type === 'punctuator' && token.value === '{') return closing.get(index);
1042
+ if (closing.has(index)) {
1043
+ index = closing.get(index) + 1;
1044
+ continue;
1045
+ }
1046
+ if (token.type === 'identifier' && ['function', 'class'].includes(token.value)
1047
+ && !identifierIsProperty(tokens, index)) {
1048
+ const end = namedDefinitionEnd(index);
1049
+ if (end === undefined) return undefined;
1050
+ index = end + 1;
1051
+ continue;
1052
+ }
1053
+ index += 1;
1054
+ }
1055
+ return undefined;
1056
+ }
1057
+
1058
+ function namedDefinitionIsDeclaration(index) {
1059
+ const token = tokens[index];
1060
+ const async = token.value === 'function' && tokens[index - 1]?.value === 'async'
1061
+ && !token.lineBreakBefore && !identifierIsProperty(tokens, index - 1);
1062
+ const start = async ? index - 1 : index;
1063
+ if (declarationPosition(tokens, start)) return true;
1064
+ const previous = tokens[start - 1];
1065
+ if (previous?.value === ':' && previous.enclosingBraceKind !== 'object') {
1066
+ if (declarationPosition(tokens, start - 2)) return true;
1067
+ let conditionals = 0;
1068
+ for (let cursor = start - 2; cursor >= scopeAt[index].start; cursor -= 1) {
1069
+ if (opening.has(cursor)) {
1070
+ cursor = opening.get(cursor);
1071
+ continue;
1072
+ }
1073
+ const candidate = tokens[cursor];
1074
+ const value = candidate.value;
1075
+ if (value === ':') conditionals += 1;
1076
+ else if (value === '?') {
1077
+ if (conditionals === 0) return false;
1078
+ conditionals -= 1;
1079
+ } else if (candidate.type === 'identifier' && ['case', 'default'].includes(value)
1080
+ && !identifierIsProperty(tokens, cursor)) return conditionals === 0;
1081
+ else if ([';', '{', '}'].includes(value)) break;
1082
+ }
1083
+ }
1084
+ // An omitted semicolon must not turn a following declaration into an expression.
1085
+ if (!tokens[start].lineBreakBefore || !previous) return false;
1086
+ if (['string', 'number', 'regex', 'template'].includes(previous.type)) return true;
1087
+ if (previous.type === 'identifier') {
1088
+ if (identifierIsProperty(tokens, start - 1)) return true;
1089
+ if (['return', 'throw', 'yield'].includes(previous.value)) return true;
1090
+ if (previous.contextualRegexPrefix === true) return false;
1091
+ if (previous.value === 'await' && previous.slashGoalAfter !== 'division') return false;
1092
+ return !REGEX_PREFIX_KEYWORDS.has(previous.value);
1093
+ }
1094
+ return [')', ']', '++', '--'].includes(previous.value);
1095
+ }
1096
+
1097
+ for(let index=0;index<tokens.length;index+=1){
1098
+ const token=tokens[index];
1099
+ if(token.type==='punctuator'&&token.value==='{'&&tokens[index-1]?.value===')'
1100
+ &&tokens[index-1]?.closesControl!==true){
1101
+ const start=opening.get(index-1);
1102
+ if(start!==undefined){
1103
+ patterns(start+1,index-1,{start,end:closing.get(index)??tokens.length});
1104
+ }
1105
+ }
1106
+ if(token.type==='punctuator'&&token.value==='=>'){
1107
+ const previous=index-1;
1108
+ const parameterStart=tokens[previous]?.value===')'?opening.get(previous):previous;
1109
+ if(parameterStart!==undefined){
1110
+ const end=tokens[index+1]?.value==='{'?closing.get(index+1):expressionEnd(index+1);
1111
+ const owner={start:parameterStart,end:end??tokens.length};
1112
+ if(tokens[previous]?.value===')')patterns(parameterStart+1,previous,owner);
1113
+ else pattern(parameterStart,index,owner);
1114
+ }
1115
+ }
1116
+ if(token.type!=='identifier'||identifierIsProperty(tokens,index))continue;
1117
+ if(['const','let','var'].includes(token.value)){
1118
+ let owner=scopeAt[index];
1119
+ if(token.value==='var'){
1120
+ while(owner.parent&&owner.kind!=='function')owner=owner.parent;
1121
+ }
1122
+ // Loop bindings conservatively retain the enclosing lexical scope.
1123
+ patterns(index+1,expressionEnd(index+1,{declaration:true}),owner);
1124
+ }else if(token.value==='function'||token.value==='class'){
1125
+ const name = tokens[index + 1]?.value === '*' ? index + 2 : index + 1;
1126
+ if (tokens[name]?.type !== 'identifier' || !relevant.has(tokens[name].value)) continue;
1127
+ const end = namedDefinitionIsDeclaration(index) ? undefined : namedDefinitionEnd(index);
1128
+ // Expression names belong to their own parameters/heritage and body only.
1129
+ const owner = end === undefined ? scopeAt[index] : {start: index, end};
1130
+ bind(name, owner);
1131
+ }else if(token.value==='catch'&&tokens[index+1]?.value==='('){
1132
+ const close=closing.get(index+1);
1133
+ const body=close+1;
1134
+ if(close!==undefined&&tokens[body]?.value==='{'){
1135
+ patterns(index+2,close,{start:index+1,end:closing.get(body)??tokens.length});
1136
+ }
1137
+ }else if(token.value==='import'&&!['(','.'].includes(tokens[index+1]?.value)
1138
+ &&tokens[index+1]?.type!=='string'){
1139
+ let cursor=index+1;
1140
+ if(tokens[cursor]?.type==='identifier'){bind(cursor,root);cursor+=1;}
1141
+ if(tokens[cursor]?.value===',')cursor+=1;
1142
+ if(tokens[cursor]?.value==='*'&&tokens[cursor+1]?.value==='as')bind(cursor+2,root);
1143
+ else if(tokens[cursor]?.value==='{'){
1144
+ const close=closing.get(cursor)??cursor;
1145
+ const boundaries=[cursor,...topLevelCommas(tokens,cursor+1,close),close];
1146
+ for(let entry=0;entry<boundaries.length-1;entry+=1){
1147
+ const start=boundaries[entry]+1;
1148
+ bind(tokens[start+1]?.value==='as'?start+2:start,root);
1149
+ }
1150
+ }
1151
+ }
1152
+ }
1153
+ return bindings;
1154
+ }
1155
+
1156
+ function locallyBoundResource(token,bindings){
1157
+ const owners=bindings.get(token.value)??[];
1158
+ return owners.some(function containsReference(owner){
1159
+ return token.start>=owner.start&&token.start<=owner.end;
1160
+ });
1161
+ }
1162
+
1163
+ function globalResourceCallee(tokens,index,bindings){
1164
+ let root=index;
1165
+ if(identifierIsProperty(tokens,index)){
1166
+ if((tokens[index-1]?.value!=='.'&&tokens[index-1]?.value!=='?.')
1167
+ ||!['globalThis','window','self'].includes(tokens[index-2]?.value)
1168
+ ||identifierIsProperty(tokens,index-2))return -1;
1169
+ root=index-2;
1170
+ }
1171
+ if(locallyBoundResource(tokens[root],bindings))return -1;
1172
+ return root;
955
1173
  }
956
1174
 
957
- function localResourceBase(tokens){
1175
+ function localResourceBase(tokens,bindings){
958
1176
  if(!tokens)return false;
959
1177
  if(tokens.some(function nonExpressionToken(token){return !['identifier','punctuator'].includes(token.type);}))return false;
1178
+ if(tokens[0]?.value!=='import'&&tokens[0]&&locallyBoundResource(tokens[0],bindings))return false;
960
1179
  const base=tokens.map(function baseToken(token){return token.value;}).join('');
961
1180
  return [
962
1181
  'import.meta.url','document.baseURI','location.href','window.location.href',
@@ -984,8 +1203,9 @@ function reportAssetReference(onReference,url,kind,baseKind){
984
1203
  if(is.function(onReference))onReference({url,kind,baseHref:null,...(baseKind?{baseKind}:{})});
985
1204
  }
986
1205
 
987
- function javascriptReferenceEdits(source,version,onReference){
1206
+ function javascriptReferenceEdits(source,version,onReference,resolveReference){
988
1207
  const tokens=tokenize(source);
1208
+ const resourceBindings=resourceBindingScopes(tokens);
989
1209
  const selected=new Map();
990
1210
  const imports=moduleImportsFromTokens(tokens,'asset source').imports;
991
1211
  const byOffset=new Map(tokens.map(function tokenByOffset(token){
@@ -993,38 +1213,55 @@ function javascriptReferenceEdits(source,version,onReference){
993
1213
  }));
994
1214
  for(const entry of imports){
995
1215
  reportAssetReference(onReference,entry.specifier,'import');
996
- if(/^(?:\.{1,2}\/|\/)/u.test(entry.specifier)){
997
- selected.set(entry.offset,byOffset.get(entry.offset));
1216
+ if(resolveReference||/^(?:\.{1,2}\/|\/)/u.test(entry.specifier)){
1217
+ selected.set(entry.offset,{token:byOffset.get(entry.offset),kind:'import'});
998
1218
  }
999
1219
  }
1000
1220
  for(let index=0;index<tokens.length;index+=1){
1001
1221
  const token=tokens[index];
1222
+ if(resolveReference&&token.type==='identifier'&&token.value==='import'
1223
+ &&!identifierIsProperty(tokens,index)&&tokens[index+1]?.value==='.'
1224
+ &&tokens[index+2]?.value==='meta'&&tokens[index+3]?.value==='.'
1225
+ &&tokens[index+4]?.value==='resolve'&&tokens[index+5]?.value==='('
1226
+ &&tokens[index+6]?.type==='string'&&tokens[index+7]?.value===')'){
1227
+ const literal=tokens[index+6];
1228
+ selected.set(literal.start,{token:literal,kind:'import-resolve'});
1229
+ }
1002
1230
  if(token.type!=='identifier'||tokens[index+1]?.value!=='(')continue;
1003
1231
  if(!['URL','Worker','SharedWorker','importScripts','fetch'].includes(token.value))continue;
1004
- const callee=globalResourceCallee(tokens,index);
1232
+ const callee=globalResourceCallee(tokens,index,resourceBindings);
1005
1233
  if(callee<0)continue;
1006
1234
  const constructor=tokens[callee-1]?.value==='new';
1007
1235
  if(['URL','Worker','SharedWorker'].includes(token.value)&&!constructor)continue;
1008
1236
  const argumentsList=callArgumentTokens(tokens,index+1);
1009
1237
  if(token.value==='fetch'&&!literalResourceFetch(argumentsList))continue;
1010
- if(token.value==='URL'&&!localResourceBase(argumentsList[1]))continue;
1238
+ if(token.value==='URL'&&!localResourceBase(argumentsList[1],resourceBindings))continue;
1011
1239
  const selectedArguments=token.value==='importScripts'?argumentsList:argumentsList.slice(0,1);
1012
1240
  for(const argument of selectedArguments){
1013
1241
  for(const literal of literalExpressionResults(argument)){
1014
1242
  if((token.value==='fetch'||token.value==='URL')
1015
1243
  &&!RESOURCE_EXTENSION.test(literal.value))continue;
1016
1244
  if((token.value==='fetch'||token.value==='URL')
1017
- &&/\.html?(?:[?#]|$)/iu.test(literal.value))continue;
1245
+ &&!resolveReference&&/\.html?(?:[?#]|$)/iu.test(literal.value))continue;
1018
1246
  const kind=token.value==='fetch'?'fetch'
1019
1247
  :token.value==='URL'&&!/\.(?:js|mjs)(?:[?#]|$)/iu.test(literal.value)?'asset':'script';
1020
1248
  const documentBase=['Worker','SharedWorker','fetch'].includes(token.value)
1021
1249
  ||(token.value==='URL'&&argumentsList[1]?.[0]?.value!=='import');
1022
1250
  reportAssetReference(onReference,literal.value,kind,documentBase?'document':undefined);
1023
- selected.set(literal.start,literal);
1251
+ selected.set(literal.start,{
1252
+ token:literal,
1253
+ kind,
1254
+ baseKind:documentBase?'document':undefined
1255
+ });
1024
1256
  }
1025
1257
  }
1026
1258
  }
1027
- return [...selected.values()].map(function versionJavaScriptReference(token){
1259
+ return [...selected.values()].map(function versionJavaScriptReference({token,kind,baseKind}){
1260
+ if(resolveReference){
1261
+ const value=resolveReference({url:token.value,kind,baseKind});
1262
+ if(value===undefined||value===token.value)return null;
1263
+ return stringReferenceEdit(source,token,null,[{start:0,end:token.value.length,value}]);
1264
+ }
1028
1265
  return stringReferenceEdit(source,token,version);
1029
1266
  }).filter(Boolean);
1030
1267
  }
@@ -1061,7 +1298,7 @@ function cssReferenceEdits(source,version,onReference,versionReference=versionAs
1061
1298
  const original=source.slice(start,end);
1062
1299
  if(original.includes('\\'))return;
1063
1300
  reportAssetReference(onReference,original,kind);
1064
- const value=versionReference(original,version);
1301
+ const value=versionReference(original,version,kind);
1065
1302
  if(value!==original)edits.push({start,end,value});
1066
1303
  }
1067
1304
  while(cursor<source.length){
@@ -1233,6 +1470,19 @@ function registerSpecifier(registry,specifier,target){
1233
1470
  registry.set(specifier,{specifier,target});
1234
1471
  }
1235
1472
 
1473
+ function translateImportMapScopes(scopes, translateUrl) {
1474
+ if (scopes === undefined) return undefined;
1475
+ const translated = {};
1476
+ for (const [scope, entries] of Object.entries(scopes)) {
1477
+ const imports = {};
1478
+ for (const [specifier, target] of Object.entries(entries)) {
1479
+ imports[translateUrl(specifier)] = translateUrl(target);
1480
+ }
1481
+ translated[translateUrl(scope)] = imports;
1482
+ }
1483
+ return translated;
1484
+ }
1485
+
1236
1486
  function versionImportMapSpecifier(specifier,version){
1237
1487
  if(specifier.endsWith('/')||!/^(?:\.{1,2}\/|\/|[A-Za-z][A-Za-z0-9+.-]*:\/\/)/u.test(specifier))return specifier;
1238
1488
  return versionImportMapUrl(specifier,version);
@@ -1328,6 +1578,13 @@ export async function buildImportMap({files,signal,version=SDK_VERSION,encodePat
1328
1578
  './arcane/sdk/dependencies/event-pubsub/index.js'
1329
1579
  );
1330
1580
  }
1581
+ if (inventory.has('sdk/dependencies/ws-share/WS.js')) {
1582
+ registerSpecifier(
1583
+ namedRegistry,
1584
+ 'ws-share',
1585
+ './arcane/sdk/dependencies/ws-share/WS.js'
1586
+ );
1587
+ }
1331
1588
  for(const relative of [...inventory].sort(compareText)){
1332
1589
  if(JAVASCRIPT_EXTENSION.test(relative)){
1333
1590
  const target=runtimeTarget(relative);
@@ -1343,10 +1600,22 @@ export async function buildImportMap({files,signal,version=SDK_VERSION,encodePat
1343
1600
  imports[entry.specifier]=target;
1344
1601
  imports[versionImportMapSpecifier(entry.specifier,version)]=target;
1345
1602
  }
1346
- return {
1603
+ const result = {
1347
1604
  imports,
1348
1605
  excludedModules:[]
1349
1606
  };
1607
+ if (inventory.has('sdk/dependencies/event-pubsub/index.js')
1608
+ && inventory.has('sdk/dependencies/event-pubsub/dependencies/strong-type/index.js')) {
1609
+ result.scopes = {
1610
+ './arcane/sdk/dependencies/event-pubsub/': {
1611
+ 'strong-type': versionAssetUrl(
1612
+ './arcane/sdk/dependencies/event-pubsub/dependencies/strong-type/index.js',
1613
+ version
1614
+ )
1615
+ }
1616
+ };
1617
+ }
1618
+ return result;
1350
1619
  }
1351
1620
 
1352
1621
  function pathInside(root,target){
@@ -1426,8 +1695,13 @@ async function managedImportMapBuild(resolvedWorkspace,resolvedApp,signal){
1426
1695
  if(physicalCompatibilityUrl)imports[physicalCompatibilityUrl]=selected;
1427
1696
  }
1428
1697
  built.imports=imports;
1698
+ if (built.scopes !== undefined) {
1699
+ built.scopes = translateImportMapScopes(built.scopes, installedBrowserUrl);
1700
+ }
1429
1701
  }
1430
- const json=`${JSON.stringify({imports:built.imports},null,2).replaceAll('<','\\u003c')}\n`;
1702
+ const document = {imports:built.imports};
1703
+ if (built.scopes !== undefined) document.scopes = built.scopes;
1704
+ const json=`${JSON.stringify(document,null,2).replaceAll('<','\\u003c')}\n`;
1431
1705
  return {built,json,version};
1432
1706
  }
1433
1707
 
@@ -1512,10 +1786,16 @@ function parseTagAttributes(openTag){
1512
1786
  }
1513
1787
  }
1514
1788
  if(name){
1515
- if(attributes.has(name))duplicates.add(name);
1789
+ const position={start:valueStart,end:valueEnd,quote,nameEnd,assigned};
1790
+ if(attributes.has(name)){
1791
+ duplicates.add(name);
1792
+ const original=positions.get(name);
1793
+ original.additional??=[];
1794
+ original.additional.push(position);
1795
+ }
1516
1796
  else{
1517
1797
  attributes.set(name,value);
1518
- positions.set(name,{start:valueStart,end:valueEnd,quote,nameEnd,assigned});
1798
+ positions.set(name,position);
1519
1799
  }
1520
1800
  }
1521
1801
  }
@@ -1954,11 +2234,12 @@ function htmlStyleReferenceEdits(source,version,onReference){
1954
2234
  });
1955
2235
  }
1956
2236
 
1957
- function srcsetReferenceEdits(source,version,onReference){
2237
+ function srcsetReferenceEdits(source,version,onReference,versionReference=versionHtmlAttribute){
1958
2238
  const edits=[];
1959
2239
  let cursor=0;
1960
2240
  while(cursor<source.length){
1961
2241
  while(/[\t\n\f\r ,]/u.test(source[cursor]??''))cursor+=1;
2242
+ if(cursor===source.length)break;
1962
2243
  const start=cursor;
1963
2244
  while(cursor<source.length&&!/[\t\n\f\r ]/u.test(source[cursor]))cursor+=1;
1964
2245
  let end=cursor;
@@ -1966,7 +2247,7 @@ function srcsetReferenceEdits(source,version,onReference){
1966
2247
  const original=source.slice(start,end);
1967
2248
  const view=htmlAttributeView(original);
1968
2249
  if(view)reportAssetReference(onReference,view.decoded,'asset');
1969
- const value=versionHtmlAttribute(original,version);
2250
+ const value=versionReference(original,version);
1970
2251
  if(value!==original)edits.push({start,end,value});
1971
2252
  if(end<cursor)continue;
1972
2253
  while(cursor<source.length&&source[cursor]!==',')cursor+=1;
@@ -2150,6 +2431,52 @@ export function inspectImportMapHtml(html){
2150
2431
  };
2151
2432
  }
2152
2433
 
2434
+ // Packaging shares the same lexical and HTML owners as managed browser maps.
2435
+ // These internal helpers edit only executable/resource references, never prose.
2436
+ export function nativeModuleHtml(source){
2437
+ const structure=scanHtmlStructure(source);
2438
+ function withAttributes(element){
2439
+ const attributes=parseTagAttributes(element.open);
2440
+ return {
2441
+ ...element,
2442
+ attributes,
2443
+ attribute:function attribute(name){
2444
+ return structuralAttribute(attributes,name,element.tag??'script');
2445
+ }
2446
+ };
2447
+ }
2448
+ return {
2449
+ ...structure,
2450
+ scripts:structure.scripts.map(withAttributes),
2451
+ elements:structure.elements.map(withAttributes),
2452
+ bases:structure.bases.map(withAttributes)
2453
+ };
2454
+ }
2455
+
2456
+ export function rewriteNativeJavaScript(source,resolveReference){
2457
+ return applyReferenceEdits(source,javascriptReferenceEdits(source,null,null,resolveReference));
2458
+ }
2459
+
2460
+ export function rewriteNativeStylesheet(source,resolveReference){
2461
+ return applyReferenceEdits(source,cssReferenceEdits(source,null,null,
2462
+ function nativeStyleReference(url,_version,kind){
2463
+ return resolveReference({url,kind})??url;
2464
+ }
2465
+ ));
2466
+ }
2467
+
2468
+ export function rewriteNativeSrcset(source,resolveReference){
2469
+ return applyReferenceEdits(source,srcsetReferenceEdits(source,null,null,
2470
+ function nativeSrcsetReference(url){
2471
+ return resolveReference({url,kind:'asset'})??url;
2472
+ }
2473
+ ));
2474
+ }
2475
+
2476
+ export function applyNativeReferenceEdits(source,edits){
2477
+ return applyReferenceEdits(source,edits);
2478
+ }
2479
+
2153
2480
  function renderManagedHtml(html,json,baseHref='../../'){
2154
2481
  const structure=scanHtmlStructure(html);
2155
2482
  const activeBases=structure.bases.map(base=>({
@@ -2326,6 +2653,7 @@ export async function createApplicationTestImportMapContext({
2326
2653
  applicationRoot,
2327
2654
  boundary='source',
2328
2655
  imports={},
2656
+ scopes,
2329
2657
  signal
2330
2658
  }={}){
2331
2659
  if(!is.string(applicationRoot)||applicationRoot.trim()===''){
@@ -2337,6 +2665,9 @@ export async function createApplicationTestImportMapContext({
2337
2665
  if(imports===null||!is.object(imports)||is.array(imports)){
2338
2666
  throw new TypeError('imports must be a plain object.');
2339
2667
  }
2668
+ if (scopes !== undefined && (scopes === null || !is.object(scopes) || is.array(scopes))) {
2669
+ throw new TypeError('scopes must be an object.');
2670
+ }
2340
2671
  throwIfAborted(signal);
2341
2672
  const requestedApplicationRoot=path.resolve(applicationRoot);
2342
2673
  const applicationInfo=await lstat(requestedApplicationRoot);
@@ -2355,35 +2686,48 @@ export async function createApplicationTestImportMapContext({
2355
2686
  ||!pathInside(canonicalApplicationRoot,canonicalBase)){
2356
2687
  fail(`Application ${boundary} import-map base must be one physical app-owned directory.`);
2357
2688
  }
2358
- const selectedImports={};
2359
- for(const [specifier,target] of Object.entries(imports)){
2360
- throwIfAborted(signal);
2361
- if(!is.string(specifier)||specifier===''||!is.string(target)
2362
- ||!target.startsWith('./')){
2363
- fail(`Application test import-map entry is invalid: ${String(specifier)}.`);
2364
- }
2365
- const relative=safeRelativePath(
2366
- decodeURIComponent(target.slice(2).split(/[?#]/u)[0]),
2367
- `application test import-map target for ${specifier}`
2368
- );
2369
- if(boundary==='source'&&/^(?:dist|test)\//u.test(relative)){
2370
- fail(`Source import-map target selects another application boundary: ${specifier}.`);
2371
- }
2372
- const candidate=path.resolve(canonicalBase,...relative.split('/'));
2373
- const targetInfo=await lstat(candidate);
2374
- const canonicalTarget=await realpath(candidate);
2375
- if(targetInfo.isSymbolicLink()||!targetInfo.isFile()
2376
- ||!samePath(candidate,canonicalTarget)||!pathInside(canonicalBase,canonicalTarget)){
2377
- fail(`Application test import-map target leaves its physical ${boundary} directory: ${specifier}.`);
2689
+ async function selectImports(entries) {
2690
+ const selected = {};
2691
+ for (const [specifier, target] of Object.entries(entries)) {
2692
+ throwIfAborted(signal);
2693
+ if (!is.string(specifier) || specifier === '' || !is.string(target)
2694
+ || !target.startsWith('./')) {
2695
+ fail(`Application test import-map entry is invalid: ${String(specifier)}.`);
2696
+ }
2697
+ const relative = safeRelativePath(
2698
+ decodeURIComponent(target.slice(2).split(/[?#]/u)[0]),
2699
+ `application test import-map target for ${specifier}`
2700
+ );
2701
+ if (boundary === 'source' && /^(?:dist|test)\//u.test(relative)) {
2702
+ fail(`Source import-map target selects another application boundary: ${specifier}.`);
2703
+ }
2704
+ const candidate = path.resolve(canonicalBase, ...relative.split('/'));
2705
+ const targetInfo = await lstat(candidate);
2706
+ const canonicalTarget = await realpath(candidate);
2707
+ if (targetInfo.isSymbolicLink() || !targetInfo.isFile()
2708
+ || !samePath(candidate, canonicalTarget) || !pathInside(canonicalBase, canonicalTarget)) {
2709
+ fail(`Application test import-map target leaves its physical ${boundary} directory: ${specifier}.`);
2710
+ }
2711
+ selected[specifier] = target;
2378
2712
  }
2379
- selectedImports[specifier]=target;
2713
+ return selected;
2380
2714
  }
2381
- return {
2715
+ const result = {
2382
2716
  protocol:'arcane-test-import-map/1',
2383
2717
  boundary,
2384
2718
  baseURL:pathToFileURL(`${canonicalBase}${path.sep}`).href,
2385
- imports:selectedImports
2719
+ imports:await selectImports(imports)
2386
2720
  };
2721
+ if (scopes !== undefined) {
2722
+ result.scopes = {};
2723
+ for (const [scope, entries] of Object.entries(scopes)) {
2724
+ if (scope === '' || entries === null || !is.object(entries) || is.array(entries)) {
2725
+ fail(`Application test import-map scope is invalid: ${scope}.`);
2726
+ }
2727
+ result.scopes[scope] = await selectImports(entries);
2728
+ }
2729
+ }
2730
+ return result;
2387
2731
  }
2388
2732
 
2389
2733
  export async function readApplicationTestImportMapContext({
@@ -2428,16 +2772,20 @@ export async function readApplicationTestImportMapContext({
2428
2772
  fail('Application test import-map artifact must contain an imports object.');
2429
2773
  }
2430
2774
  const installed=await readInstalledSdkLayout(resolvedWorkspaceRoot);
2431
- const imports=installed?Object.fromEntries(Object.entries(document.imports).map(
2775
+ function installedApplicationTestUrl(value) {
2776
+ return installed && is.string(value) && value.startsWith('./')
2777
+ ? `./${installedRuntimeTarget(value.slice(2), installed)}` : value;
2778
+ }
2779
+ const imports=Object.fromEntries(Object.entries(document.imports).map(
2432
2780
  function installedApplicationTestTarget([specifier,target]){
2433
- return [specifier,is.string(target)&&target.startsWith('./')
2434
- ?`./${installedRuntimeTarget(target.slice(2),installed)}`:target];
2781
+ return [specifier,installedApplicationTestUrl(target)];
2435
2782
  }
2436
- )):document.imports;
2783
+ ));
2437
2784
  return createApplicationTestImportMapContext({
2438
2785
  applicationRoot:resolvedWorkspaceRoot,
2439
2786
  boundary:'source',
2440
2787
  imports,
2788
+ scopes:translateImportMapScopes(document.scopes, installedApplicationTestUrl),
2441
2789
  signal
2442
2790
  });
2443
2791
  }
@@ -2509,7 +2857,7 @@ function documentRelativeUrl(target, base) {
2509
2857
  return `${pathname}${target.search}${target.hash}`;
2510
2858
  }
2511
2859
 
2512
- function documentImports(imports, context, baseHref, relative) {
2860
+ function documentImportMap(importMap, context, baseHref, relative) {
2513
2861
  const documentUrl = new URL(encodedUrlPath(relative), context.deployment);
2514
2862
  // Match URL parser normalization only for resolving the base; preserve authored HTML.
2515
2863
  const baseAddress = (baseHref ?? '').replace(/[\t\r\n]/gu, '').replace(
@@ -2545,16 +2893,20 @@ function documentImports(imports, context, baseHref, relative) {
2545
2893
  return value;
2546
2894
  }
2547
2895
  const rebased = {};
2548
- for (const [specifier, target] of Object.entries(imports)) {
2896
+ for (const [specifier, target] of Object.entries(importMap.imports)) {
2549
2897
  rebased[rebaseUrl(specifier)] = rebaseUrl(target);
2550
2898
  }
2551
- return rebased;
2899
+ const result = {imports:rebased};
2900
+ if (importMap.scopes !== undefined) {
2901
+ result.scopes = translateImportMapScopes(importMap.scopes, rebaseUrl);
2902
+ }
2903
+ return result;
2552
2904
  }
2553
2905
 
2554
- function renderDocumentImportMap(html, imports, state) {
2906
+ function renderDocumentImportMap(html, importMap, state) {
2555
2907
  const {structure, managed, firstLoad, base} = state;
2556
2908
  const newline = html.includes('\r\n') ? '\r\n' : '\n';
2557
- const json = JSON.stringify({imports}, null, 2).replaceAll('<', '\\u003c');
2909
+ const json = JSON.stringify(importMap, null, 2).replaceAll('<', '\\u003c');
2558
2910
  const block = `<script type="importmap" ${MANAGED_IMPORT_MAP_ATTRIBUTE}>${newline}${json}${newline}</script>`;
2559
2911
  const firstManaged = managed[0];
2560
2912
  const replaceInPlace = firstManaged
@@ -2691,13 +3043,15 @@ export async function generateDocumentImportMaps({
2691
3043
  const rendered = documentStates.map(
2692
3044
  function renderSelectedDocument(document) {
2693
3045
  throwIfAborted(signal);
2694
- const imports = documentImports(built.imports, context, document.state.base?.href, document.path);
2695
- return {
3046
+ const importMap = documentImportMap(built, context, document.state.base?.href, document.path);
3047
+ const result = {
2696
3048
  path:document.path,
2697
3049
  filePath:document.filePath,
2698
- imports,
2699
- html:renderDocumentImportMap(document.html, imports, document.state)
3050
+ imports:importMap.imports,
3051
+ html:renderDocumentImportMap(document.html, importMap, document.state)
2700
3052
  };
3053
+ if (importMap.scopes !== undefined) result.scopes = importMap.scopes;
3054
+ return result;
2701
3055
  }
2702
3056
  );
2703
3057
  const paths = [];
@@ -2721,11 +3075,13 @@ export async function generateDocumentImportMaps({
2721
3075
  documentCount:documentPaths.length,
2722
3076
  documents:rendered.map(
2723
3077
  function documentResult(document) {
2724
- return {
3078
+ const result = {
2725
3079
  path:document.path,
2726
3080
  filePath:document.filePath,
2727
3081
  imports:document.imports
2728
3082
  };
3083
+ if (document.scopes !== undefined) result.scopes = document.scopes;
3084
+ return result;
2729
3085
  }
2730
3086
  ),
2731
3087
  committed:true
@@ -2836,6 +3192,7 @@ async function generateImportMapUnlocked({
2836
3192
  excludedModules:built.excludedModules,
2837
3193
  committed:true
2838
3194
  };
3195
+ if (built.scopes !== undefined) result.scopes = built.scopes;
2839
3196
  const completedEventError=await emit(onEvent,{
2840
3197
  type:'import-map.completed',
2841
3198
  appId,