arcane-os 0.3.0 → 0.3.2

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 (153) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +86 -117
  3. package/bin/arcane-test.mjs +170 -46
  4. package/browser-runtime/ai/browser-speech-artifacts.mjs +887 -909
  5. package/browser-runtime/ai/browser-speech-providers.mjs +96 -152
  6. package/browser-runtime/ai/browser-wasm-llm-provider.mjs +627 -819
  7. package/browser-runtime/ai/browser-wasm.mjs +24 -35
  8. package/browser-runtime/ai/browser-wllama-runtime.mjs +64 -316
  9. package/browser-runtime/ai/model-controller.mjs +584 -181
  10. package/browser-runtime/ai/speech-worker-client.mjs +8 -146
  11. package/browser-runtime/ai/speech-worker-runtime.mjs +643 -363
  12. package/browser-runtime/dom-event-instrumentation.mjs +55 -147
  13. package/browser-runtime/event-manager.mjs +239 -624
  14. package/package.json +5 -6
  15. package/runtime/arcane/components/app-bar.html +3 -15
  16. package/runtime/arcane/components/assistant-panel.html +10 -10
  17. package/runtime/arcane/components/calculator.html +1 -1
  18. package/runtime/arcane/components/chat.html +1359 -135
  19. package/runtime/arcane/components/conversation-view.html +2 -2
  20. package/runtime/arcane/components/document-inspector.html +11 -17
  21. package/runtime/arcane/components/file-manager.html +13 -56
  22. package/runtime/arcane/components/markdown-document.html +82 -281
  23. package/runtime/arcane/components/markdown-editor.html +7 -10
  24. package/runtime/arcane/components/media-embed.html +6 -6
  25. package/runtime/arcane/components/screen-capture.html +4 -4
  26. package/runtime/arcane/components/source-explanation.html +2 -2
  27. package/runtime/arcane/components/speech.html +112 -68
  28. package/runtime/arcane/components/terminal-workspace.html +4 -4
  29. package/runtime/arcane/components/theme-editor.html +1 -1
  30. package/runtime/arcane/components/unified-inbox.html +2 -2
  31. package/runtime/arcane/components/voice-transcription.html +31 -21
  32. package/runtime/arcane/entities/Calculation.js +2 -3
  33. package/runtime/arcane/entities/Chat.js +228 -43
  34. package/runtime/arcane/entities/Preference.js +3 -5
  35. package/runtime/arcane/entities/Weather.js +5 -5
  36. package/runtime/arcane/modules/AI.js +1050 -427
  37. package/runtime/arcane/modules/AIProviderRuntime.js +658 -363
  38. package/runtime/arcane/modules/AIResponseLength.js +9 -19
  39. package/runtime/arcane/modules/AIRuntimeState.js +109 -72
  40. package/runtime/arcane/modules/ArcaneNavigationPolicy.js +45 -32
  41. package/runtime/arcane/modules/BrowserTestSuite.js +78 -122
  42. package/runtime/arcane/modules/CalculatorEngine.js +9 -9
  43. package/runtime/arcane/modules/CommunicationAppController.js +3 -7
  44. package/runtime/arcane/modules/ComponentContracts.js +30 -32
  45. package/runtime/arcane/modules/ConfiguredAIChatSession.js +281 -230
  46. package/runtime/arcane/modules/ConversationActionItems.js +26 -59
  47. package/runtime/arcane/modules/ConversationClosingReport.js +34 -61
  48. package/runtime/arcane/modules/ConversationTimebox.js +27 -15
  49. package/runtime/arcane/modules/DBOPFSDocumentLibrary.js +152 -344
  50. package/runtime/arcane/modules/DocumentLexicalSearch.js +25 -91
  51. package/runtime/arcane/modules/HTMLImport.js +54 -1
  52. package/runtime/arcane/modules/IsolatedModelQuestionRunner.js +40 -203
  53. package/runtime/arcane/modules/LocalAIReadiness.js +40 -60
  54. package/runtime/arcane/modules/LocalAIReadinessController.js +15 -13
  55. package/runtime/arcane/modules/MD.js +1 -45
  56. package/runtime/arcane/modules/Mail.js +51 -103
  57. package/runtime/arcane/modules/MailOutbox.mjs +95 -193
  58. package/runtime/arcane/modules/MailTransport.mjs +36 -57
  59. package/runtime/arcane/modules/ModelDefinition.js +22 -106
  60. package/runtime/arcane/modules/OpenMeteoWeatherProvider.js +39 -101
  61. package/runtime/arcane/modules/PersistentAIChatSession.js +281 -18
  62. package/runtime/arcane/modules/PreferenceStore.js +102 -30
  63. package/runtime/arcane/modules/RiskSignalAnalyzer.js +8 -9
  64. package/runtime/arcane/modules/ScopedOPFSCache.js +7 -42
  65. package/runtime/arcane/modules/ScreenCapture.js +175 -128
  66. package/runtime/arcane/modules/SpeechPlayback.js +46 -149
  67. package/runtime/arcane/modules/StaticDocumentCatalog.js +173 -407
  68. package/runtime/arcane/modules/ToolCallRouter.js +25 -12
  69. package/runtime/arcane/modules/YouTubeMedia.js +6 -5
  70. package/schemas/arcane-app-bundle.schema.json +13 -78
  71. package/schemas/arcane-app.schema.json +9 -25
  72. package/schemas/arcane-lock.schema.json +18 -151
  73. package/schemas/arcane-package.schema.json +2 -16
  74. package/schemas/native-build-plan.schema.json +119 -122
  75. package/src/app-descriptor.mjs +75 -132
  76. package/src/application-tests.mjs +200 -0
  77. package/src/cli/main.mjs +27 -46
  78. package/src/constants.mjs +3 -4
  79. package/src/dev-server.mjs +30 -324
  80. package/src/doctor.mjs +92 -154
  81. package/src/dom-event-instrumentation.mjs +55 -147
  82. package/src/errors.mjs +2 -3
  83. package/src/event-manager.mjs +239 -624
  84. package/src/event-queue.mjs +3 -3
  85. package/src/import-map.mjs +273 -1028
  86. package/src/index.mjs +14 -16
  87. package/src/installed-sdk-runtime.mjs +40 -62
  88. package/src/integrated-provider-loader.mjs +53 -382
  89. package/src/mail-api.mjs +0 -2
  90. package/src/mail-server.mjs +224 -580
  91. package/src/mail.mjs +4 -10
  92. package/src/native-plan.mjs +163 -598
  93. package/src/native-provider-loader.mjs +104 -1063
  94. package/src/packager/core.mjs +485 -3229
  95. package/src/process.mjs +5 -10
  96. package/src/release-bundle.mjs +292 -2405
  97. package/src/runtime.mjs +76 -396
  98. package/src/scaffold.mjs +30 -80
  99. package/src/sdk-browser-runtime.mjs +70 -626
  100. package/src/source-server.mjs +588 -0
  101. package/src/targets/index.mjs +78 -188
  102. package/src/templates/workspace-template.mjs +19 -135
  103. package/src/testing-loader.mjs +164 -0
  104. package/src/testing.mjs +1 -1
  105. package/src/toolchain.mjs +131 -544
  106. package/src/update-check.mjs +26 -64
  107. package/src/workspace-operation-lock.mjs +139 -430
  108. package/src/workspace-runtime.mjs +112 -779
  109. package/src/workspace.mjs +40 -302
  110. package/browser-runtime/ARCANE_SDK_BROWSER_RELEASE.json +0 -218
  111. package/browser-runtime/ai/ARCANE_AI_BROWSER_SPEECH_COMPONENTS.json +0 -203
  112. package/browser-runtime/ai/ARCANE_AI_BROWSER_WASM_COMPONENTS.json +0 -80
  113. package/browser-runtime/ai/internal/sha256.mjs +0 -166
  114. package/docs/architecture.md +0 -344
  115. package/docs/compatibility.md +0 -36
  116. package/docs/event-manager.md +0 -294
  117. package/docs/platform-targets.md +0 -108
  118. package/docs/publishing.md +0 -201
  119. package/docs/reference/README.md +0 -187
  120. package/docs/reference/ai/browser-speech-package-authority.json +0 -835
  121. package/docs/reference/ai/browser-speech.md +0 -1252
  122. package/docs/reference/ai/browser-wasm.md +0 -530
  123. package/docs/reference/arcane-ollama.md +0 -288
  124. package/docs/reference/availability-and-normalization.md +0 -183
  125. package/docs/reference/behavioral-testing.md +0 -133
  126. package/docs/reference/cli.md +0 -779
  127. package/docs/reference/core/README.md +0 -62
  128. package/docs/reference/core/arcane-ai-contracts.md +0 -907
  129. package/docs/reference/core/arcane-api.md +0 -601
  130. package/docs/reference/core/arcane-entities.md +0 -65
  131. package/docs/reference/core/arcane-events.md +0 -134
  132. package/docs/reference/core/ollama-module.md +0 -181
  133. package/docs/reference/core/reference/arcane-api/ai-and-ollama.md +0 -1909
  134. package/docs/reference/core/reference/arcane-api/applications-terminal-capabilities.md +0 -1057
  135. package/docs/reference/core/reference/arcane-api/core-and-events.md +0 -320
  136. package/docs/reference/core/reference/arcane-api/filesystem-storage-preferences-appearance.md +0 -610
  137. package/docs/reference/core/reference/arcane-api/namespaces.md +0 -1157
  138. package/docs/reference/core/reference/arcane-api/platform-installation-users-system.md +0 -1423
  139. package/docs/reference/core/reference/arcane-api/session-provisioning-diagnostics-development.md +0 -315
  140. package/docs/reference/event-manager.md +0 -1511
  141. package/docs/reference/inventory/package-api.json +0 -3284
  142. package/docs/reference/inventory/runtime-components.json +0 -1011
  143. package/docs/reference/inventory/runtime-entities.json +0 -26
  144. package/docs/reference/inventory/runtime-modules.json +0 -1431
  145. package/docs/reference/mail.md +0 -316
  146. package/docs/reference/protocols.md +0 -677
  147. package/docs/reference/runtime-components.md +0 -1366
  148. package/docs/reference/runtime-entities.md +0 -303
  149. package/docs/reference/runtime-modules.md +0 -2960
  150. package/docs/reference/sdk-api.md +0 -6694
  151. package/docs/roadmap.md +0 -79
  152. package/docs/work-amplification.md +0 -129
  153. package/runtime/ARCANE_RUNTIME_RELEASE.json +0 -826
@@ -1,62 +1,26 @@
1
- import {createHash,randomUUID} from 'node:crypto';
2
- import {constants as FS_CONSTANTS} from 'node:fs';
3
- import {lstat,mkdir,open,readdir,realpath,rename,rm} from 'node:fs/promises';
1
+ import {lstat,mkdir,readFile as readFileFromDisk,readdir,realpath,writeFile} from 'node:fs/promises';
4
2
  import path from 'node:path';
5
- import {
6
- authenticateWorkspaceRuntimeReceipt,
7
- readVerifiedWorkspaceRuntimeFile
8
- } from './workspace-runtime.mjs';
9
- import {withWorkspaceOperationLock} from './workspace-operation-lock.mjs';
3
+ import {pathToFileURL} from 'node:url';
10
4
 
11
5
  export const IMPORT_MAP_RELATIVE_PATH='modules/arcane.importmap.json';
12
6
  export const MANAGED_IMPORT_MAP_ATTRIBUTE='data-arcane-import-map';
13
7
 
14
8
  const JAVASCRIPT_EXTENSION=/\.(?:js|mjs)$/u;
15
9
  const NODE_ONLY_MODULE='modules/CaseEvidenceIndexer.js';
16
- const RUNTIME_STRONG_TYPE_IMPORT='../../node_modules/strong-type/index.js';
17
10
  const PERSISTENT_CHAT_IMPORT='#arcane/persistent-ai-chat-session';
18
11
  const PERSISTENT_CHAT_MODULE='modules/PersistentAIChatSession.js';
19
12
  const SDK_BROWSER_ENTRY='sdk/event-manager.mjs';
20
13
  const SDK_BROWSER_AI_ENTRY='sdk/ai/browser-wasm.mjs';
21
14
  const SDK_BROWSER_SPEECH_ENTRY='sdk/ai/browser-speech.mjs';
22
- const SDK_BROWSER_SPEECH_WORKER_RUNTIME='sdk/ai/speech-worker-runtime.mjs';
15
+ const STATIC_RUNTIME_PACKAGE_IMPORTS=new Map([
16
+ ['arcane-os/preference-store','modules/PreferenceStore.js'],
17
+ ['arcane-os/speech-playback','modules/SpeechPlayback.js']
18
+ ]);
23
19
  const SDK_BROWSER_SELF_IMPORTS=new Map([
24
20
  ['arcane-os/event-manager',SDK_BROWSER_ENTRY],
25
21
  ['arcane-os/ai/browser-wasm',SDK_BROWSER_AI_ENTRY],
26
22
  ['arcane-os/ai/browser-speech',SDK_BROWSER_SPEECH_ENTRY]
27
23
  ]);
28
- const SDK_BROWSER_FILES=Object.freeze([
29
- 'sdk/ai/ARCANE_AI_BROWSER_WASM_COMPONENTS.json',
30
- 'sdk/ai/browser-kokoro-worker.mjs',
31
- 'sdk/ai/browser-speech-artifacts.mjs',
32
- 'sdk/ai/browser-speech-providers.mjs',
33
- SDK_BROWSER_SPEECH_ENTRY,
34
- 'sdk/ai/browser-wasm-llm-provider.mjs',
35
- SDK_BROWSER_AI_ENTRY,
36
- 'sdk/ai/browser-whisper-worker.mjs',
37
- 'sdk/ai/browser-wllama-runtime.mjs',
38
- 'sdk/ai/internal/sha256.mjs',
39
- 'sdk/ai/model-controller.mjs',
40
- 'sdk/ai/speech-worker-client.mjs',
41
- SDK_BROWSER_SPEECH_WORKER_RUNTIME,
42
- 'sdk/ai/wllama/LICENCE',
43
- 'sdk/ai/wllama/index.mjs',
44
- 'sdk/ai/wllama/llama.cpp-LICENSE',
45
- 'sdk/ai/wllama/wllama.wasm',
46
- SDK_BROWSER_ENTRY,
47
- 'sdk/dom-event-instrumentation.mjs',
48
- 'sdk/dependencies/event-pubsub/index.js',
49
- 'sdk/dependencies/event-pubsub/licence',
50
- 'sdk/dependencies/event-pubsub/package.json',
51
- 'sdk/dependencies/strong-type/index.js',
52
- 'sdk/dependencies/strong-type/licence',
53
- 'sdk/dependencies/strong-type/package.json'
54
- ]);
55
- const READ_ONLY_NO_FOLLOW=FS_CONSTANTS.O_RDONLY|(FS_CONSTANTS.O_NOFOLLOW??0);
56
- const WRITE_NEW_NO_FOLLOW=FS_CONSTANTS.O_CREAT|FS_CONSTANTS.O_EXCL
57
- |FS_CONSTANTS.O_WRONLY|(FS_CONSTANTS.O_NOFOLLOW??0);
58
- const SAFE_APP_ID=/^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/u;
59
-
60
24
  function fail(message,code='ARCANE_IMPORT_MAP_INVALID'){
61
25
  const error=new Error(message);
62
26
  error.code=code;
@@ -71,15 +35,21 @@ function throwIfAborted(signal){
71
35
  }
72
36
 
73
37
  async function emit(onEvent,event){
74
- if(typeof onEvent==='function')await onEvent(Object.freeze(event));
75
- }
76
-
77
- function compareUtf8(left,right){
78
- return Buffer.compare(Buffer.from(String(left),'utf8'),Buffer.from(String(right),'utf8'));
38
+ if(typeof onEvent!=='function')return null;
39
+ try{
40
+ await onEvent(event);
41
+ return null;
42
+ }catch(error){
43
+ return error;
44
+ }
79
45
  }
80
46
 
81
- function collisionKey(value){
82
- return value.normalize('NFC').toLowerCase();
47
+ function compareText(left,right){
48
+ const leftText=String(left);
49
+ const rightText=String(right);
50
+ if(leftText<rightText)return -1;
51
+ if(leftText>rightText)return 1;
52
+ return 0;
83
53
  }
84
54
 
85
55
  function safeRelativePath(value,label='path'){
@@ -92,32 +62,22 @@ function safeRelativePath(value,label='path'){
92
62
  }
93
63
 
94
64
  function normalizedDocumentPaths(entry,documents){
95
- if(documents===undefined)return Object.freeze([entry]);
65
+ if(documents===undefined)return [entry];
96
66
  if(!Array.isArray(documents)||documents.length===0){
97
67
  fail('Import-map documents must be a non-empty array of application-relative paths.');
98
68
  }
99
69
  const normalized=[];
100
- const identities=new Map();
101
70
  for(const [index,value] of documents.entries()){
102
71
  const relative=safeRelativePath(value,`documents[${String(index)}]`);
103
- const key=collisionKey(relative);
104
- const prior=identities.get(key);
105
- if(prior!==undefined){
106
- fail(
107
- `Import-map documents contain a duplicate or portable path collision: `
108
- +`${prior} and ${relative}.`
109
- );
110
- }
111
- identities.set(key,relative);
112
- normalized.push(relative);
72
+ if(!normalized.includes(relative))normalized.push(relative);
113
73
  }
114
74
  if(!normalized.includes(entry)){
115
75
  fail(`Import-map documents must include the configured application entry: ${entry}.`);
116
76
  }
117
- return Object.freeze([
77
+ return [
118
78
  entry,
119
- ...normalized.filter(relative=>relative!==entry).sort(compareUtf8)
120
- ]);
79
+ ...normalized.filter(relative=>relative!==entry).sort(compareText)
80
+ ];
121
81
  }
122
82
 
123
83
  function decodedEscape(source,index){
@@ -608,12 +568,6 @@ function tokenize(source){
608
568
  index=end;
609
569
  continue;
610
570
  }
611
- if(character==='\\'){
612
- fail(
613
- `Import-map scan found an escaped JavaScript identifier at offset ${String(index)}. `
614
- +'Escaped identifiers are outside the deterministic import scanner subset.'
615
- );
616
- }
617
571
  if(/[0-9]/u.test(character)){
618
572
  let end=index+1;
619
573
  while(/[A-Za-z0-9_.]/u.test(source[end]??''))end+=1;
@@ -753,23 +707,7 @@ function topLevelCommas(tokens,start,end){
753
707
  }
754
708
 
755
709
  function importRecord(kind,token){
756
- return Object.freeze({kind,specifier:token.value,offset:token.start});
757
- }
758
-
759
- function nonliteralDynamic(importer,offset){
760
- fail(
761
- `Import-map scan found a nonliteral dynamic import in "${importer}" at offset ${String(offset)}. `
762
- +'Replace import(expression) with a literal shipped specifier, then rerun arcane import-map.',
763
- 'ARCANE_IMPORT_MAP_UNRESOLVED'
764
- );
765
- }
766
-
767
- function isAuthorizedSpeechRuntimeImport(tokens,index,close,importer){
768
- if(importer!==SDK_BROWSER_SPEECH_WORKER_RUNTIME||close!==index+5)return false;
769
- const [binding,dot,moduleUrl]=tokens.slice(index+2,close);
770
- return binding?.type==='identifier'&&['entry','target'].includes(binding.value)
771
- &&dot?.value==='.'
772
- &&moduleUrl?.type==='identifier'&&moduleUrl.value==='moduleUrl';
710
+ return {kind,specifier:token.value,offset:token.start};
773
711
  }
774
712
 
775
713
  export function scanModuleImports(source,{importer='<module>'}={}){
@@ -789,8 +727,9 @@ export function scanModuleImports(source,{importer='<module>'}={}){
789
727
  }
790
728
  if(next?.value==='('){
791
729
  if(importIsMethodDefinition(tokens,index))continue;
730
+ hasModuleSyntax=true;
792
731
  const close=matchingToken(tokens,index+1,'(',')');
793
- if(close<0)nonliteralDynamic(importer,current.start);
732
+ if(close<0)continue;
794
733
  const argument=tokens[index+2];
795
734
  const commas=topLevelCommas(tokens,index+2,close);
796
735
  const firstBoundary=commas[0]??close;
@@ -798,14 +737,9 @@ export function scanModuleImports(source,{importer='<module>'}={}){
798
737
  ||commas.length>2
799
738
  ||(commas.length===2
800
739
  &&(commas[1]!==close-1||commas[1]===commas[0]+1))){
801
- if(isAuthorizedSpeechRuntimeImport(tokens,index,close,importer)){
802
- hasModuleSyntax=true;
803
- index=close;
804
- continue;
805
- }
806
- nonliteralDynamic(importer,current.start);
740
+ index=close;
741
+ continue;
807
742
  }
808
- hasModuleSyntax=true;
809
743
  imports.push(importRecord('dynamic',argument));
810
744
  continue;
811
745
  }
@@ -845,319 +779,85 @@ export function scanModuleImports(source,{importer='<module>'}={}){
845
779
  }
846
780
  }
847
781
  }
848
- return Object.freeze({
782
+ return {
849
783
  hasModuleSyntax,
850
- imports:Object.freeze(imports)
851
- });
852
- }
853
-
854
- function stripQueryAndHash(specifier){
855
- const query=specifier.indexOf('?');
856
- const hash=specifier.indexOf('#');
857
- const end=Math.min(query<0?specifier.length:query,hash<0?specifier.length:hash);
858
- return specifier.slice(0,end);
859
- }
860
-
861
- function unresolved(importer,specifier,normalizedTarget,reason='is not in the shipped workspace runtime'){
862
- fail(
863
- `Import-map scan could not resolve "${specifier}" imported by "${importer}". `
864
- +`Normalized target: "${normalizedTarget}" ${reason}. `
865
- +'Materialize the authenticated dependency beneath workspace arcane/ or update the import '
866
- +'to a shipped JavaScript file, then rerun arcane import-map.',
867
- 'ARCANE_IMPORT_MAP_UNRESOLVED'
868
- );
869
- }
870
-
871
- function resolveImport(importer,specifier,files){
872
- if(specifier===PERSISTENT_CHAT_IMPORT){
873
- if(!files.has(PERSISTENT_CHAT_MODULE)){
874
- unresolved(importer,specifier,PERSISTENT_CHAT_MODULE);
875
- }
876
- return {target:PERSISTENT_CHAT_MODULE,persistentChat:true};
877
- }
878
- if(specifier.startsWith(PERSISTENT_CHAT_IMPORT)){
879
- unresolved(
880
- importer,
881
- specifier,
882
- PERSISTENT_CHAT_MODULE,
883
- 'does not match the exact browser import-map key'
884
- );
885
- }
886
- const reachableSpecifier=stripQueryAndHash(specifier);
887
- if(!reachableSpecifier)unresolved(importer,specifier,'<empty>');
888
- if(reachableSpecifier.includes('%')){
889
- unresolved(
890
- importer,
891
- specifier,
892
- reachableSpecifier,
893
- 'contains percent-encoded path bytes whose browser URL normalization is outside the '
894
- +'deterministic shipped-runtime subset'
895
- );
896
- }
897
- const sdkBrowserTarget=SDK_BROWSER_SELF_IMPORTS.get(reachableSpecifier);
898
- if(sdkBrowserTarget){
899
- if(reachableSpecifier!==specifier){
900
- unresolved(
901
- importer,
902
- specifier,
903
- sdkBrowserTarget,
904
- 'uses a query or fragment that cannot match its exact browser import-map key'
905
- );
906
- }
907
- if(!files.has(sdkBrowserTarget))unresolved(importer,specifier,sdkBrowserTarget);
908
- return {target:sdkBrowserTarget};
909
- }
910
- if(reachableSpecifier===RUNTIME_STRONG_TYPE_IMPORT){
911
- const target='dependencies/strong-type/index.js';
912
- if(reachableSpecifier!==specifier){
913
- unresolved(
914
- importer,
915
- specifier,
916
- target,
917
- 'uses a query or fragment that cannot match its exact browser import-map key'
918
- );
919
- }
920
- if(!files.has(target))unresolved(importer,specifier,target);
921
- return {target,runtimeStrongType:true};
922
- }
923
- if(reachableSpecifier==='event-pubsub'){
924
- const target='sdk/dependencies/event-pubsub/index.js';
925
- if(reachableSpecifier!==specifier){
926
- unresolved(
927
- importer,
928
- specifier,
929
- target,
930
- 'uses a query or fragment that cannot match its exact browser import-map key'
931
- );
932
- }
933
- if(!files.has(target))unresolved(importer,specifier,target);
934
- return {target,eventPubSub:true};
935
- }
936
- if(/[\u0000-\u0020\u007f\\]/u.test(reachableSpecifier)||reachableSpecifier.includes('//')){
937
- unresolved(
938
- importer,
939
- specifier,
940
- reachableSpecifier,
941
- 'contains browser-preprocessed control/space/backslash bytes or an empty path segment'
942
- );
943
- }
944
- if(!reachableSpecifier.startsWith('./')&&!reachableSpecifier.startsWith('../')){
945
- unresolved(importer,specifier,reachableSpecifier,'is not a supported shipped bare specifier');
946
- }
947
- const runtimePrefix='/__arcane_runtime__/';
948
- const runtimeOrigin='https://arcane.invalid';
949
- let resolved;
950
- try{
951
- resolved=new URL(
952
- reachableSpecifier,
953
- `${runtimeOrigin}${runtimePrefix}${importer}`
954
- );
955
- }catch{
956
- unresolved(importer,specifier,reachableSpecifier,'is not a valid browser-relative URL');
957
- }
958
- if(resolved.origin!==runtimeOrigin||!resolved.pathname.startsWith(runtimePrefix)){
959
- unresolved(importer,specifier,resolved.pathname,'escapes the shipped workspace runtime');
960
- }
961
- let target;
962
- try{target=decodeURIComponent(resolved.pathname.slice(runtimePrefix.length));}
963
- catch{
964
- unresolved(importer,specifier,resolved.pathname,'does not have a deterministic decoded URL path');
965
- }
966
- if(target==='.'||target.startsWith('../')||path.posix.isAbsolute(target)){
967
- unresolved(importer,specifier,target,'escapes the shipped workspace runtime');
968
- }
969
- if(!files.has(target))unresolved(importer,specifier,target);
970
- if(!JAVASCRIPT_EXTENSION.test(target)){
971
- unresolved(importer,specifier,target,'is not a JavaScript module');
972
- }
973
- return {target,strongType:false};
784
+ imports
785
+ };
974
786
  }
975
787
 
976
788
  function registerSpecifier(registry,specifier,target){
977
- const key=collisionKey(specifier);
978
- const existing=registry.get(key);
979
- if(existing&&existing.specifier!==specifier||existing&&existing.target!==target){
980
- fail(
981
- `Import-map specifier collision: "${specifier}" (${target}) and `
982
- +`"${existing.specifier}" (${existing.target}) normalize to the same case/NFC key. `
983
- +'Rename one shipped module so every extensionless named specifier is unique.',
984
- 'ARCANE_IMPORT_MAP_COLLISION'
985
- );
986
- }
987
- registry.set(key,{specifier,target});
789
+ registry.set(specifier,{specifier,target});
988
790
  }
989
791
 
990
792
  function validateInventory(files){
991
793
  if(!Array.isArray(files))throw new TypeError('buildImportMap files must be an array.');
992
794
  const exact=new Set();
993
- const normalized=new Map();
994
- for(const value of [...files].sort(compareUtf8)){
795
+ for(const value of [...files].sort(compareText)){
995
796
  const relative=safeRelativePath(value,'runtime inventory path');
996
- if(/[%?#\u0000-\u0020\u007f]/u.test(relative)||relative.includes('//')){
997
- fail(
998
- `Import-map runtime inventory path is not browser-URL-safe: ${relative}. `
999
- +'Percent/delimiter bytes, control/space bytes, and empty path segments are not '
1000
- +'allowed in authenticated runtime filenames.'
1001
- );
1002
- }
1003
- if(exact.has(relative))fail(`Import-map runtime inventory repeats ${relative}.`);
1004
797
  exact.add(relative);
1005
- const key=collisionKey(relative);
1006
- const prior=normalized.get(key);
1007
- if(prior&&prior!==relative){
1008
- fail(
1009
- `Import-map runtime path collision: "${prior}" and "${relative}" normalize to `
1010
- +'the same case/NFC path. Rename one shipped file before regenerating the map.',
1011
- 'ARCANE_IMPORT_MAP_COLLISION'
1012
- );
1013
- }
1014
- normalized.set(key,relative);
1015
798
  }
1016
799
  return exact;
1017
800
  }
1018
801
 
1019
- export async function buildImportMap({files,readFile,signal}={}){
1020
- if(typeof readFile!=='function')throw new TypeError('buildImportMap readFile must be a function.');
802
+ export async function buildImportMap({files,signal}={}){
1021
803
  throwIfAborted(signal);
1022
804
  const inventory=validateInventory(files);
1023
- const candidates=[...inventory]
805
+ const modules=[...inventory]
1024
806
  .filter(relative=>relative.startsWith('modules/')
1025
807
  &&!relative.slice('modules/'.length).includes('/')
1026
808
  &&JAVASCRIPT_EXTENSION.test(relative))
1027
- .sort(compareUtf8);
1028
- const scans=new Map();
1029
- async function scan(relative){
1030
- throwIfAborted(signal);
1031
- if(scans.has(relative))return scans.get(relative);
1032
- const bytes=await readFile(relative);
1033
- throwIfAborted(signal);
1034
- const source=Buffer.isBuffer(bytes)||bytes instanceof Uint8Array
1035
- ?Buffer.from(bytes).toString('utf8'):String(bytes);
1036
- const result=scanModuleImports(source,{importer:relative});
1037
- scans.set(relative,result);
1038
- return result;
1039
- }
1040
-
1041
- const roots=[];
809
+ .sort(compareText);
810
+ const namedRegistry=new Map();
1042
811
  const excludedModules=[];
1043
- for(const relative of candidates){
1044
- const result=await scan(relative);
1045
- if(!result.hasModuleSyntax)continue;
812
+ for(const relative of modules){
813
+ throwIfAborted(signal);
1046
814
  if(relative===NODE_ONLY_MODULE){
1047
815
  excludedModules.push(relative);
1048
816
  continue;
1049
817
  }
1050
- roots.push(relative);
1051
- }
1052
- const hasSdkBrowserGraph=inventory.has(SDK_BROWSER_ENTRY);
1053
- const hasSdkAiGraph=inventory.has(SDK_BROWSER_AI_ENTRY);
1054
- const hasSdkSpeechGraph=inventory.has(SDK_BROWSER_SPEECH_ENTRY);
1055
- if(hasSdkAiGraph&&!hasSdkBrowserGraph){
1056
- unresolved(SDK_BROWSER_AI_ENTRY,'<authenticated SDK browser closure>',SDK_BROWSER_ENTRY);
818
+ const name=path.posix.basename(relative).replace(JAVASCRIPT_EXTENSION,'');
819
+ registerSpecifier(namedRegistry,`arcane/${name}`,`./arcane/${relative}`);
1057
820
  }
1058
- if(hasSdkSpeechGraph&&!hasSdkAiGraph){
1059
- unresolved(SDK_BROWSER_SPEECH_ENTRY,'<authenticated SDK browser closure>',SDK_BROWSER_AI_ENTRY);
821
+ const entities=[...inventory].filter(relative=>relative.startsWith('entities/')
822
+ &&!relative.slice('entities/'.length).includes('/')
823
+ &&JAVASCRIPT_EXTENSION.test(relative)).sort(compareText);
824
+ for(const relative of entities){
825
+ throwIfAborted(signal);
826
+ const name=path.posix.basename(relative).replace(JAVASCRIPT_EXTENSION,'');
827
+ registerSpecifier(namedRegistry,`arcane/entities/${name}`,`./arcane/${relative}`);
1060
828
  }
1061
- if(hasSdkBrowserGraph){
1062
- for(const required of SDK_BROWSER_FILES){
1063
- if(!inventory.has(required)){
1064
- unresolved(SDK_BROWSER_ENTRY,'<authenticated SDK browser closure>',required);
1065
- }
1066
- }
1067
- for(const [packagePath,expectedName,expectedVersion] of [
1068
- ['dependencies/strong-type/package.json','strong-type','1.1.0'],
1069
- ['sdk/dependencies/event-pubsub/package.json','event-pubsub','6.1.0'],
1070
- ['sdk/dependencies/strong-type/package.json','strong-type','2.0.0']
1071
- ]){
1072
- if(!inventory.has(packagePath)){
1073
- unresolved(SDK_BROWSER_ENTRY,'<authenticated dependency identity>',packagePath);
1074
- }
1075
- let document;
1076
- try{document=JSON.parse(Buffer.from(await readFile(packagePath)).toString('utf8'));}
1077
- catch{
1078
- unresolved(SDK_BROWSER_ENTRY,'<authenticated dependency identity>',packagePath,'is not valid package JSON');
1079
- }
1080
- if(document?.name!==expectedName||document?.version!==expectedVersion){
1081
- unresolved(
1082
- SDK_BROWSER_ENTRY,
1083
- '<authenticated dependency identity>',
1084
- packagePath,
1085
- `must identify exactly as ${expectedName}@${expectedVersion}`
1086
- );
1087
- }
829
+ for(const [specifier,relative] of STATIC_RUNTIME_PACKAGE_IMPORTS){
830
+ if(inventory.has(relative)){
831
+ registerSpecifier(namedRegistry,specifier,`./arcane/${relative}`);
1088
832
  }
1089
833
  }
1090
-
1091
- const namedRegistry=new Map();
1092
- for(const relative of roots){
1093
- const name=path.posix.basename(relative).replace(JAVASCRIPT_EXTENSION,'');
1094
- registerSpecifier(namedRegistry,`arcane/${name}`,`./arcane/${relative}`);
834
+ for(const [specifier,relative] of SDK_BROWSER_SELF_IMPORTS){
835
+ if(inventory.has(relative)){
836
+ registerSpecifier(namedRegistry,specifier,`./arcane/${relative}`);
837
+ }
1095
838
  }
1096
- if(hasSdkBrowserGraph){
839
+ if(inventory.has('sdk/dom-event-instrumentation.mjs')){
1097
840
  registerSpecifier(
1098
841
  namedRegistry,
1099
- 'arcane-os/event-manager',
1100
- './arcane/sdk/event-manager.mjs'
842
+ 'arcane-os/dom-event-instrumentation',
843
+ './arcane/sdk/dom-event-instrumentation.mjs'
1101
844
  );
1102
- if(hasSdkAiGraph){
1103
- registerSpecifier(
1104
- namedRegistry,
1105
- 'arcane-os/ai/browser-wasm',
1106
- './arcane/sdk/ai/browser-wasm.mjs'
1107
- );
1108
- registerSpecifier(
1109
- namedRegistry,
1110
- 'arcane-os/ai/browser-speech',
1111
- './arcane/sdk/ai/browser-speech.mjs'
1112
- );
1113
- }
1114
845
  }
1115
-
1116
- const sdkRoots=hasSdkBrowserGraph
1117
- ?[
1118
- SDK_BROWSER_ENTRY,
1119
- ...(hasSdkAiGraph?[SDK_BROWSER_AI_ENTRY]:[]),
1120
- ...(hasSdkSpeechGraph?[SDK_BROWSER_SPEECH_ENTRY]:[]),
1121
- ]:[];
1122
- const queue=[...roots,...sdkRoots];
1123
- const reached=new Set();
1124
- const entities=new Set();
1125
- let usesRuntimeStrongType=false;
1126
- let usesEventPubSub=false;
1127
- while(queue.length>0){
1128
- throwIfAborted(signal);
1129
- const importer=queue.shift();
1130
- if(reached.has(importer))continue;
1131
- reached.add(importer);
1132
- const result=await scan(importer);
1133
- for(const imported of result.imports){
1134
- const resolution=resolveImport(importer,imported.specifier,inventory);
1135
- if(resolution.runtimeStrongType)usesRuntimeStrongType=true;
1136
- if(resolution.eventPubSub)usesEventPubSub=true;
1137
- if(resolution.persistentChat){
1138
- registerSpecifier(
1139
- namedRegistry,
1140
- PERSISTENT_CHAT_IMPORT,
1141
- './arcane/modules/PersistentAIChatSession.js'
1142
- );
1143
- }
1144
- if(resolution.target.startsWith('entities/'))entities.add(resolution.target);
1145
- if(!reached.has(resolution.target))queue.push(resolution.target);
1146
- }
1147
- }
1148
-
1149
- for(const relative of [...entities].sort(compareUtf8)){
1150
- const name=path.posix.basename(relative).replace(JAVASCRIPT_EXTENSION,'');
1151
- registerSpecifier(namedRegistry,`arcane/entities/${name}`,`./arcane/${relative}`);
846
+ if(inventory.has(PERSISTENT_CHAT_MODULE)){
847
+ registerSpecifier(
848
+ namedRegistry,
849
+ PERSISTENT_CHAT_IMPORT,
850
+ './arcane/modules/PersistentAIChatSession.js'
851
+ );
1152
852
  }
1153
- if(usesRuntimeStrongType){
853
+ if(inventory.has('dependencies/strong-type/index.js')){
1154
854
  registerSpecifier(
1155
855
  namedRegistry,
1156
856
  './node_modules/strong-type/index.js',
1157
857
  './arcane/dependencies/strong-type/index.js'
1158
858
  );
1159
859
  }
1160
- if(usesEventPubSub){
860
+ if(inventory.has('sdk/dependencies/event-pubsub/index.js')){
1161
861
  registerSpecifier(
1162
862
  namedRegistry,
1163
863
  'event-pubsub',
@@ -1165,29 +865,13 @@ export async function buildImportMap({files,readFile,signal}={}){
1165
865
  );
1166
866
  }
1167
867
  const imports={};
1168
- for(const entry of [...namedRegistry.values()].sort((left,right)=>compareUtf8(left.specifier,right.specifier))){
868
+ for(const entry of [...namedRegistry.values()].sort((left,right)=>compareText(left.specifier,right.specifier))){
1169
869
  imports[entry.specifier]=entry.target;
1170
870
  }
1171
- return Object.freeze({
1172
- imports:Object.freeze(imports),
1173
- entryCount:Object.keys(imports).length,
1174
- excludedModules:Object.freeze(excludedModules.sort(compareUtf8)),
1175
- reachedFiles:Object.freeze([...reached].sort(compareUtf8))
1176
- });
1177
- }
1178
-
1179
- function sameFileIdentity(left,right){
1180
- return left.dev===right.dev&&left.ino===right.ino&&left.size===right.size
1181
- &&left.mtimeNs===right.mtimeNs&&left.ctimeNs===right.ctimeNs
1182
- &&left.nlink===right.nlink;
1183
- }
1184
-
1185
- function sameFileLocation(left,right){
1186
- return left.dev===right.dev&&left.ino===right.ino;
1187
- }
1188
-
1189
- function sha256(bytes){
1190
- return createHash('sha256').update(bytes).digest('hex');
871
+ return {
872
+ imports,
873
+ excludedModules:excludedModules.sort(compareText)
874
+ };
1191
875
  }
1192
876
 
1193
877
  function pathInside(root,target){
@@ -1195,6 +879,12 @@ function pathInside(root,target){
1195
879
  return relative===''||(!relative.startsWith('..')&&!path.isAbsolute(relative));
1196
880
  }
1197
881
 
882
+ function samePath(left,right){
883
+ const a=path.resolve(left);
884
+ const b=path.resolve(right);
885
+ return process.platform==='win32'?a.toLowerCase()===b.toLowerCase():a===b;
886
+ }
887
+
1198
888
  async function physicalRuntime(workspaceRoot,signal){
1199
889
  const requestedRoot=path.join(workspaceRoot,'arcane');
1200
890
  let rootInfo;
@@ -1207,11 +897,14 @@ async function physicalRuntime(workspaceRoot,signal){
1207
897
  fail('Workspace Arcane runtime must be a real directory, not a symbolic link or junction.');
1208
898
  }
1209
899
  const canonicalRoot=await realpath(requestedRoot);
900
+ if(!samePath(requestedRoot,canonicalRoot)){
901
+ fail('Workspace Arcane runtime must stay inside its physical workspace directory.');
902
+ }
1210
903
  const files=[];
1211
904
  async function visit(directory,relativeRoot=''){
1212
905
  throwIfAborted(signal);
1213
906
  const entries=await readdir(directory,{withFileTypes:true});
1214
- entries.sort((left,right)=>compareUtf8(left.name,right.name));
907
+ entries.sort((left,right)=>compareText(left.name,right.name));
1215
908
  for(const entry of entries){
1216
909
  throwIfAborted(signal);
1217
910
  const relative=relativeRoot?`${relativeRoot}/${entry.name}`:entry.name;
@@ -1226,43 +919,14 @@ async function physicalRuntime(workspaceRoot,signal){
1226
919
  }
1227
920
  }
1228
921
  await visit(canonicalRoot);
1229
- return {
1230
- files,
1231
- async readFile(relative){
1232
- throwIfAborted(signal);
1233
- safeRelativePath(relative,'runtime read path');
1234
- const absolute=path.resolve(canonicalRoot,...relative.split('/'));
1235
- if(!pathInside(canonicalRoot,absolute))fail(`Import-map runtime read escapes arcane/: ${relative}.`);
1236
- const before=await lstat(absolute,{bigint:true});
1237
- if(before.isSymbolicLink()||!before.isFile()){
1238
- fail(`Workspace Arcane runtime module is not a real file: ${relative}.`);
1239
- }
1240
- let handle;
1241
- try{handle=await open(absolute,READ_ONLY_NO_FOLLOW);}
1242
- catch(error){
1243
- if(error?.code==='ELOOP')fail(`Workspace Arcane runtime module became a symlink: ${relative}.`);
1244
- throw error;
1245
- }
1246
- try{
1247
- const opened=await handle.stat({bigint:true});
1248
- if(!sameFileIdentity(before,opened)){
1249
- fail(`Workspace Arcane runtime module changed while opening: ${relative}.`);
1250
- }
1251
- const bytes=await handle.readFile();
1252
- const after=await handle.stat({bigint:true});
1253
- if(!sameFileIdentity(opened,after)){
1254
- fail(`Workspace Arcane runtime module changed while reading: ${relative}.`);
1255
- }
1256
- const canonicalFile=await realpath(absolute);
1257
- if(!pathInside(canonicalRoot,canonicalFile)){
1258
- fail(`Workspace Arcane runtime module left its root: ${relative}.`);
1259
- }
1260
- return bytes;
1261
- }finally{
1262
- await handle.close();
1263
- }
1264
- }
1265
- };
922
+ return {files};
923
+ }
924
+
925
+ async function managedImportMapBuild(resolvedWorkspace,signal){
926
+ const runtime=await physicalRuntime(resolvedWorkspace,signal);
927
+ const built=await buildImportMap({files:runtime.files,signal});
928
+ const json=`${JSON.stringify({imports:built.imports},null,2).replaceAll('<','\\u003c')}\n`;
929
+ return {built,json};
1266
930
  }
1267
931
 
1268
932
  function asciiLower(value){
@@ -1421,7 +1085,7 @@ function htmlTagEnd(html,start){
1421
1085
  fail('Application HTML contains a tag that reaches end of file before ">".');
1422
1086
  }
1423
1087
 
1424
- const RAW_TEXT_ELEMENTS=new Set(['iframe','noembed','noframes','script','style','xmp']);
1088
+ const RAW_TEXT_ELEMENTS=new Set(['iframe','noembed','noframes','noscript','script','style','xmp']);
1425
1089
  const RCDATA_ELEMENTS=new Set(['textarea','title']);
1426
1090
  const TEXT_ELEMENTS=new Set([...RAW_TEXT_ELEMENTS,...RCDATA_ELEMENTS]);
1427
1091
 
@@ -1429,15 +1093,6 @@ function rawElementEnd(html,tag,openEnd){
1429
1093
  const closePattern=new RegExp(`<\\/${tag}(?=[\\t\\n\\f\\r />]|$)`,'gi');
1430
1094
  closePattern.lastIndex=openEnd;
1431
1095
  const close=closePattern.exec(html);
1432
- if(tag==='script'){
1433
- const escapedStart=html.indexOf('<!--',openEnd);
1434
- if(escapedStart>=0&&(!close||escapedStart<close.index)){
1435
- fail(
1436
- 'Application HTML contains legacy escaped script syntax, which is outside the '
1437
- +'deterministic import-map HTML subset.'
1438
- );
1439
- }
1440
- }
1441
1096
  if(!close)return {end:html.length,closed:false};
1442
1097
  const end=htmlTagEnd(html,close.index+close[0].length);
1443
1098
  const closeTag=html.slice(close.index,end);
@@ -1448,9 +1103,8 @@ function rawElementEnd(html,tag,openEnd){
1448
1103
  }
1449
1104
 
1450
1105
  function commentEnd(html,start){
1451
- if(html.startsWith('<!-->',start)||html.startsWith('<!--->',start)){
1452
- fail('Application HTML contains an abrupt comment close outside the deterministic subset.');
1453
- }
1106
+ if(html.startsWith('<!-->',start))return start+5;
1107
+ if(html.startsWith('<!--->',start))return start+6;
1454
1108
  const canonical=html.indexOf('-->',start+4);
1455
1109
  const bang=html.indexOf('--!>',start+4);
1456
1110
  const close=canonical<0?bang:bang<0?canonical:Math.min(canonical,bang);
@@ -1469,78 +1123,10 @@ function validateEndTag(source,name){
1469
1123
  }
1470
1124
  }
1471
1125
 
1472
- function rejectDeclarativeShadowTemplate(open){
1473
- const attributes=parseTagAttributes(open);
1474
- if(!attributes.has('shadowrootmode'))return;
1475
- structuralAttribute(attributes,'shadowrootmode','template');
1476
- fail(
1477
- 'Application HTML contains declarative shadow DOM, whose connected module loads are '
1478
- +'outside the deterministic import-map HTML subset.'
1479
- );
1480
- }
1481
-
1482
1126
  function selectElementEnd(html,openEnd){
1483
- let cursor=openEnd;
1484
- const elements=[];
1485
- while(cursor<html.length){
1486
- const start=html.indexOf('<',cursor);
1487
- if(start<0){
1488
- fail('Application HTML contains an unterminated <select> element.');
1489
- }
1490
- if(html.startsWith('<!--',start)){
1491
- cursor=commentEnd(html,start);
1492
- continue;
1493
- }
1494
- if(html.startsWith('<!',start)||html.startsWith('<?',start)){
1495
- fail('Application HTML select contains an unsupported declaration or processing instruction.');
1496
- }
1497
- const head=htmlTagHead(html,start);
1498
- if(!head){
1499
- if(html.startsWith('</',start)||/^<[A-Za-z]/u.test(html.slice(start))){
1500
- fail('Application HTML select contains a malformed tag.');
1501
- }
1502
- cursor=start+1;
1503
- continue;
1504
- }
1505
- const name=htmlTagName(head[1]);
1506
- const end=htmlTagEnd(html,start+head[0].length);
1507
- const closing=html[start+1]==='/';
1508
- if(closing)validateEndTag(html.slice(start,end),name);
1509
- if(name==='frame'||name==='frameset'){
1510
- fail(`Application HTML contains unsupported structural element <${name}>.`);
1511
- }
1512
- if(name==='select'){
1513
- if(!closing){
1514
- fail('Application HTML contains a nested <select> element.');
1515
- }
1516
- if(elements.length>0){
1517
- fail(`Application HTML closes <select> before </${elements.at(-1)}> is present.`);
1518
- }
1519
- return end;
1520
- }
1521
- if(name!=='option'&&name!=='optgroup'){
1522
- fail(
1523
- `Application HTML select contains unsupported <${closing?'/':''}${name}> markup. `
1524
- +'Only text, comments, option, and optgroup are accepted inside select.'
1525
- );
1526
- }
1527
- if(closing){
1528
- if(elements.at(-1)!==name){
1529
- fail(`Application HTML select contains an unmatched </${name}> end tag.`);
1530
- }
1531
- elements.pop();
1532
- }else{
1533
- if(name==='optgroup'&&elements.length>0){
1534
- fail('Application HTML select contains a nested or option-contained <optgroup>.');
1535
- }
1536
- if(name==='option'&&elements.at(-1)==='option'){
1537
- fail('Application HTML select contains nested <option> elements.');
1538
- }
1539
- elements.push(name);
1540
- }
1541
- cursor=end;
1542
- }
1543
- fail('Application HTML contains an unterminated <select> element.');
1127
+ const selected=rawElementEnd(html,'select',openEnd);
1128
+ if(!selected.closed)fail('Application HTML contains an unterminated <select> element.');
1129
+ return selected.end;
1544
1130
  }
1545
1131
 
1546
1132
  function nestedTemplateEnd(html,openEnd){
@@ -1554,7 +1140,8 @@ function nestedTemplateEnd(html,openEnd){
1554
1140
  continue;
1555
1141
  }
1556
1142
  if(html.startsWith('<!',start)||html.startsWith('<?',start)){
1557
- fail('Application HTML template contains an unsupported declaration or processing instruction.');
1143
+ cursor=htmlTagEnd(html,start+2);
1144
+ continue;
1558
1145
  }
1559
1146
  const head=htmlTagHead(html,start);
1560
1147
  if(!head){
@@ -1568,20 +1155,14 @@ function nestedTemplateEnd(html,openEnd){
1568
1155
  const end=htmlTagEnd(html,start+head[0].length);
1569
1156
  const closing=html[start+1]==='/';
1570
1157
  if(closing)validateEndTag(html.slice(start,end),name);
1571
- if(name==='frame'||name==='frameset'){
1572
- fail(`Application HTML contains unsupported structural element <${name}>.`);
1573
- }
1574
1158
  if(name==='select'){
1575
- if(closing)fail('Application HTML contains an unmatched </select> end tag.');
1159
+ if(closing){
1160
+ cursor=end;
1161
+ continue;
1162
+ }
1576
1163
  cursor=selectElementEnd(html,end);
1577
1164
  continue;
1578
1165
  }
1579
- if(name==='svg'||name==='math'){
1580
- fail(`Application HTML contains unsupported foreign-content element <${name}>.`);
1581
- }
1582
- if(!closing&&name==='noscript'){
1583
- fail('Application HTML contains <noscript>, whose active parsing depends on browser mode.');
1584
- }
1585
1166
  if(!closing&&name==='plaintext')return html.length;
1586
1167
  if(!closing&&TEXT_ELEMENTS.has(name)){
1587
1168
  cursor=rawElementEnd(html,name,end).end;
@@ -1591,7 +1172,6 @@ function nestedTemplateEnd(html,openEnd){
1591
1172
  cursor=end;
1592
1173
  continue;
1593
1174
  }
1594
- if(!closing)rejectDeclarativeShadowTemplate(html.slice(start,end));
1595
1175
  if(closing)depth-=1;
1596
1176
  else depth+=1;
1597
1177
  cursor=end;
@@ -1608,7 +1188,6 @@ function scanHtmlStructure(html){
1608
1188
  let headClose=-1;
1609
1189
  let bodyClose=-1;
1610
1190
  let cursor=0;
1611
- let sawDoctype=false;
1612
1191
  while(cursor<html.length){
1613
1192
  const start=html.indexOf('<',cursor);
1614
1193
  if(start<0)break;
@@ -1618,18 +1197,12 @@ function scanHtmlStructure(html){
1618
1197
  }
1619
1198
  if(html.startsWith('<!',start)){
1620
1199
  const end=htmlTagEnd(html,start+2);
1621
- const declaration=html.slice(start,end);
1622
- if(!/^<!doctype[\t\n\f\r ]+html[\t\n\f\r ]*>$/i.test(declaration)
1623
- ||sawDoctype
1624
- ||!/^(?:\ufeff)?[\t\n\f\r ]*$/u.test(html.slice(0,start))){
1625
- fail('Application HTML contains an unsupported or misplaced declaration.');
1626
- }
1627
- sawDoctype=true;
1628
1200
  cursor=end;
1629
1201
  continue;
1630
1202
  }
1631
1203
  if(html.startsWith('<?',start)){
1632
- fail('Application HTML contains an unsupported processing instruction.');
1204
+ cursor=htmlTagEnd(html,start+2);
1205
+ continue;
1633
1206
  }
1634
1207
  const head=htmlTagHead(html,start);
1635
1208
  if(!head){
@@ -1643,17 +1216,14 @@ function scanHtmlStructure(html){
1643
1216
  const closing=html[start+1]==='/';
1644
1217
  const openEnd=htmlTagEnd(html,start+head[0].length);
1645
1218
  const open=html.slice(start,openEnd);
1646
- if(tag==='frame'||tag==='frameset'){
1647
- fail(`Application HTML contains unsupported structural element <${tag}>.`);
1648
- }
1649
1219
  if(tag==='select'){
1650
- if(closing)fail('Application HTML contains an unmatched </select> end tag.');
1220
+ if(closing){
1221
+ cursor=openEnd;
1222
+ continue;
1223
+ }
1651
1224
  cursor=selectElementEnd(html,openEnd);
1652
1225
  continue;
1653
1226
  }
1654
- if(tag==='svg'||tag==='math'){
1655
- fail(`Application HTML contains unsupported foreign-content element <${tag}>.`);
1656
- }
1657
1227
  if(closing){
1658
1228
  validateEndTag(open,tag);
1659
1229
  if(tag==='head'&&headClose<0)headClose=start;
@@ -1677,13 +1247,9 @@ function scanHtmlStructure(html){
1677
1247
  continue;
1678
1248
  }
1679
1249
  if(tag==='template'){
1680
- rejectDeclarativeShadowTemplate(open);
1681
1250
  cursor=nestedTemplateEnd(html,openEnd);
1682
1251
  continue;
1683
1252
  }
1684
- if(tag==='noscript'){
1685
- fail('Application HTML contains <noscript>, whose active parsing depends on browser mode.');
1686
- }
1687
1253
  if(tag==='plaintext'){
1688
1254
  cursor=html.length;
1689
1255
  continue;
@@ -1759,7 +1325,7 @@ function firstBlockingLoadPosition(html,{skipManaged=false}={}){
1759
1325
  export function inspectImportMapHtml(html){
1760
1326
  const source=String(html);
1761
1327
  const structure=scanHtmlStructure(source);
1762
- const bases=structure.bases.map(base=>Object.freeze({
1328
+ const bases=structure.bases.map(base=>({
1763
1329
  start:base.start,
1764
1330
  end:base.end,
1765
1331
  href:structuralAttribute(parseTagAttributes(base.open),'href','base')
@@ -1768,43 +1334,43 @@ export function inspectImportMapHtml(html){
1768
1334
  const attributes=parseTagAttributes(script.open);
1769
1335
  return attributes.has(MANAGED_IMPORT_MAP_ATTRIBUTE)
1770
1336
  &&scriptType(attributes)==='importmap';
1771
- }).map(script=>Object.freeze({start:script.start,end:script.end}));
1337
+ }).map(script=>({start:script.start,end:script.end}));
1772
1338
  const scripts=structure.scripts.map(script=>{
1773
1339
  const attributes=parseTagAttributes(script.open);
1774
- return Object.freeze({
1340
+ return {
1775
1341
  start:script.start,
1776
1342
  end:script.end,
1777
1343
  type:scriptType(attributes),
1778
1344
  src:structuralAttribute(attributes,'src','script'),
1779
1345
  managed:attributes.has(MANAGED_IMPORT_MAP_ATTRIBUTE)
1780
- });
1346
+ };
1781
1347
  });
1782
1348
  const links=structure.links.map(link=>{
1783
1349
  const attributes=parseTagAttributes(link.open);
1784
- return Object.freeze({
1350
+ return {
1785
1351
  start:link.start,
1786
1352
  end:link.end,
1787
1353
  rel:canonicalHtmlToken(structuralAttribute(attributes,'rel','link')),
1788
1354
  href:structuralAttribute(attributes,'href','link')
1789
- });
1355
+ };
1790
1356
  });
1791
1357
  const metas=structure.metas.map(meta=>{
1792
1358
  const attributes=parseTagAttributes(meta.open);
1793
- return Object.freeze({
1359
+ return {
1794
1360
  start:meta.start,
1795
1361
  end:meta.end,
1796
1362
  name:canonicalHtmlToken(structuralAttribute(attributes,'name','meta')),
1797
1363
  content:structuralAttribute(attributes,'content','meta')
1798
- });
1364
+ };
1799
1365
  });
1800
- return Object.freeze({
1801
- bases:Object.freeze(bases),
1802
- managedMaps:Object.freeze(managedMaps),
1803
- scripts:Object.freeze(scripts),
1804
- links:Object.freeze(links),
1805
- metas:Object.freeze(metas),
1366
+ return {
1367
+ bases,
1368
+ managedMaps,
1369
+ scripts,
1370
+ links,
1371
+ metas,
1806
1372
  firstModulePosition:firstModulePosition(source)
1807
- });
1373
+ };
1808
1374
  }
1809
1375
 
1810
1376
  function documentBaseHref(relative){
@@ -1826,23 +1392,13 @@ function renderManagedHtml(html,json,baseHref='../../'){
1826
1392
  for(const script of structure.scripts){
1827
1393
  const attributes=parseTagAttributes(script.open);
1828
1394
  if(attributes.has(MANAGED_IMPORT_MAP_ATTRIBUTE)){
1829
- if(scriptType(attributes)!=='importmap'){
1830
- fail(`Managed ${MANAGED_IMPORT_MAP_ATTRIBUTE} script must use type="importmap".`);
1831
- }
1395
+ if(scriptType(attributes)!=='importmap')continue;
1832
1396
  if(!script.closed){
1833
1397
  fail(`Application HTML contains an unterminated ${MANAGED_IMPORT_MAP_ATTRIBUTE} script.`);
1834
1398
  }
1835
1399
  complete.push({start:script.start,end:script.end});
1836
- }else if(scriptType(attributes)==='importmap'){
1837
- fail(
1838
- `Application HTML already contains an unmanaged import map. Remove it or add `
1839
- +`${MANAGED_IMPORT_MAP_ATTRIBUTE}, then rerun arcane import-map.`
1840
- );
1841
1400
  }
1842
1401
  }
1843
- if(complete.length>1){
1844
- fail(`Application HTML contains multiple ${MANAGED_IMPORT_MAP_ATTRIBUTE} scripts.`);
1845
- }
1846
1402
  const withoutManaged=removeManagedBlocks(html,complete);
1847
1403
  const cleanedStructure=scanHtmlStructure(withoutManaged);
1848
1404
  const cleanedBases=cleanedStructure.bases.map(base=>({
@@ -1890,414 +1446,174 @@ function renderManagedHtml(html,json,baseHref='../../'){
1890
1446
  return rendered;
1891
1447
  }
1892
1448
 
1893
- async function readRealFile(filePath,label){
1894
- const state=await readRealFileState(filePath,label);
1895
- return state.bytes;
1896
- }
1897
-
1898
- async function readRealFileState(filePath,label,{optional=false}={}){
1899
- let info;
1900
- try{info=await lstat(filePath,{bigint:true});}
1901
- catch(error){
1902
- if(optional&&error?.code==='ENOENT')return {exists:false,filePath};
1903
- throw error;
1904
- }
1905
- if(info.isSymbolicLink()||!info.isFile())fail(`${label} must be a real file: ${filePath}.`);
1906
- const handle=await open(filePath,READ_ONLY_NO_FOLLOW);
1907
- try{
1908
- const opened=await handle.stat({bigint:true});
1909
- if(!sameFileIdentity(info,opened))fail(`${label} changed while opening: ${filePath}.`);
1910
- const bytes=await handle.readFile();
1911
- const after=await handle.stat({bigint:true});
1912
- if(!sameFileIdentity(opened,after))fail(`${label} changed while reading: ${filePath}.`);
1913
- return {exists:true,filePath,bytes,identity:after};
1914
- }finally{
1915
- await handle.close();
1916
- }
1917
- }
1918
-
1919
- async function captureDirectoryState(root,directory,{create=false}={}){
1449
+ async function physicalDirectory(root,directory,{create=false}={}){
1920
1450
  const resolvedRoot=path.resolve(root);
1921
1451
  const resolvedDirectory=path.resolve(directory);
1922
1452
  if(!pathInside(resolvedRoot,resolvedDirectory)){
1923
1453
  fail(`Import-map directory escapes its application root: ${resolvedDirectory}.`);
1924
1454
  }
1925
- const rootInfo=await lstat(resolvedRoot,{bigint:true});
1455
+ const rootInfo=await lstat(resolvedRoot);
1926
1456
  if(rootInfo.isSymbolicLink()||!rootInfo.isDirectory()){
1927
1457
  fail(`Import-map application root must be a real directory: ${resolvedRoot}.`);
1928
1458
  }
1929
1459
  const canonicalRoot=await realpath(resolvedRoot);
1930
- const canonicalRootInfo=await lstat(canonicalRoot,{bigint:true});
1931
- if(canonicalRootInfo.isSymbolicLink()||!canonicalRootInfo.isDirectory()
1932
- ||!sameDirectoryIdentity(rootInfo,canonicalRootInfo)){
1933
- fail(`Import-map application root changed while authenticating: ${resolvedRoot}.`);
1460
+ if(!samePath(resolvedRoot,canonicalRoot)){
1461
+ fail(`Import-map application root must be one physical directory: ${resolvedRoot}.`);
1934
1462
  }
1935
- const entries=[{location:resolvedRoot,identity:canonicalRootInfo,canonical:canonicalRoot}];
1936
1463
  const relative=path.relative(resolvedRoot,resolvedDirectory);
1937
1464
  let current=resolvedRoot;
1938
- let parent=entries[0];
1939
1465
  for(const part of relative.split(path.sep).filter(Boolean)){
1940
- const parentBefore=await lstat(parent.location,{bigint:true});
1941
- if(parentBefore.isSymbolicLink()||!parentBefore.isDirectory()
1942
- ||!sameDirectoryIdentity(parentBefore,parent.identity)
1943
- ||await realpath(parent.location)!==parent.canonical){
1944
- fail(`Import-map directory changed before creating a child: ${parent.location}.`);
1945
- }
1946
1466
  const child=path.join(current,part);
1947
1467
  if(create){
1948
1468
  try{await mkdir(child);}
1949
1469
  catch(error){if(error?.code!=='EEXIST')throw error;}
1950
1470
  }
1951
- const info=await lstat(child,{bigint:true});
1471
+ const info=await lstat(child);
1952
1472
  if(info.isSymbolicLink()||!info.isDirectory()){
1953
1473
  fail(`Import-map directory must be a real directory: ${child}.`);
1954
1474
  }
1955
1475
  const canonical=await realpath(child);
1956
- if(!pathInside(canonicalRoot,canonical)||path.dirname(canonical)!==parent.canonical){
1476
+ if(!pathInside(canonicalRoot,canonical)){
1957
1477
  fail(`Import-map directory resolves outside its application root: ${child}.`);
1958
1478
  }
1959
- const parentAfter=await lstat(parent.location,{bigint:true});
1960
- if(parentAfter.isSymbolicLink()||!parentAfter.isDirectory()
1961
- ||!sameDirectoryIdentity(parentAfter,parent.identity)
1962
- ||await realpath(parent.location)!==parent.canonical){
1963
- fail(`Import-map directory changed while creating a child: ${parent.location}.`);
1964
- }
1965
- const entry={location:child,identity:info,canonical};
1966
- entries.push(entry);
1967
1479
  current=child;
1968
- parent=entry;
1969
1480
  }
1970
- return {root:resolvedRoot,directory:resolvedDirectory,entries};
1481
+ return {root:resolvedRoot,canonicalRoot,directory:resolvedDirectory};
1971
1482
  }
1972
1483
 
1973
- function sameDirectoryIdentity(left,right){
1974
- return left.isDirectory()&&right.isDirectory()&&left.dev===right.dev&&left.ino===right.ino;
1975
- }
1976
-
1977
- async function assertDirectoryState(state){
1978
- for(const entry of state.entries){
1979
- const info=await lstat(entry.location,{bigint:true});
1980
- if(info.isSymbolicLink()||!info.isDirectory()
1981
- ||!sameDirectoryIdentity(info,entry.identity)
1982
- ||await realpath(entry.location)!==entry.canonical){
1983
- fail(`Import-map directory changed during generation: ${entry.location}.`);
1984
- }
1484
+ async function readPhysicalTextFile(root,filePath,label){
1485
+ const directory=await physicalDirectory(root,path.dirname(filePath));
1486
+ const info=await lstat(filePath);
1487
+ if(info.isSymbolicLink()||!info.isFile()){
1488
+ fail(`${label} must be a real file: ${filePath}.`);
1985
1489
  }
1490
+ const canonicalFile=await realpath(filePath);
1491
+ if(!pathInside(directory.canonicalRoot,canonicalFile)){
1492
+ fail(`${label} must stay inside its application root: ${filePath}.`);
1493
+ }
1494
+ return readFileFromDisk(filePath,'utf8');
1986
1495
  }
1987
1496
 
1988
- async function stageSibling(filePath,bytes,directoryState){
1989
- await assertDirectoryState(directoryState);
1990
- const staged=path.join(
1991
- path.dirname(filePath),
1992
- `.${path.basename(filePath)}.arcane-stage-${String(process.pid)}-${randomUUID()}`
1993
- );
1994
- const content=Buffer.from(bytes);
1995
- let handle;
1996
- let ownedIdentity;
1497
+ async function assertPhysicalTextDestination(root,filePath,label,{createParent=false}={}){
1498
+ const directory=await physicalDirectory(root,path.dirname(filePath),{create:createParent});
1997
1499
  try{
1998
- handle=await open(staged,WRITE_NEW_NO_FOLLOW,0o644);
1999
- ownedIdentity=await handle.stat({bigint:true});
2000
- await assertDirectoryState(directoryState);
2001
- await handle.writeFile(content);
2002
- await handle.sync();
2003
- await handle.close();
2004
- handle=null;
2005
- await assertDirectoryState(directoryState);
2006
- const identity=await lstat(staged,{bigint:true});
2007
- if(identity.isSymbolicLink()||!identity.isFile()
2008
- ||!sameFileLocation(identity,ownedIdentity)){
2009
- fail(`Import-map staged file changed while it was written: ${staged}.`);
1500
+ const info=await lstat(filePath);
1501
+ if(info.isSymbolicLink()||!info.isFile()){
1502
+ fail(`${label} destination must be a real file when present.`);
2010
1503
  }
2011
- return {
2012
- path:staged,
2013
- identity,
2014
- directoryState,
2015
- byteLength:content.length,
2016
- hash:sha256(content)
2017
- };
2018
- }catch(error){
2019
- try{await handle?.close();}
2020
- catch(cleanupError){error.cleanupError??=cleanupError;}
2021
- if(ownedIdentity){
2022
- try{
2023
- const removed=await removeOwnedPath(staged,ownedIdentity,directoryState);
2024
- if(!removed)fail(`Import-map staged file could not be safely cleaned: ${staged}.`);
2025
- }catch(cleanupError){error.cleanupError??=cleanupError;}
1504
+ const canonicalFile=await realpath(filePath);
1505
+ if(!pathInside(directory.canonicalRoot,canonicalFile)){
1506
+ fail(`${label} destination must stay inside its application root.`);
2026
1507
  }
2027
- throw error;
2028
- }
2029
- }
2030
-
2031
- async function removeOwnedPath(filePath,identity,directoryState){
2032
- try{await assertDirectoryState(directoryState);}
2033
- catch{return false;}
2034
- let current;
2035
- try{current=await lstat(filePath,{bigint:true});}
2036
- catch(error){
2037
- if(error?.code==='ENOENT')return true;
2038
- throw error;
1508
+ }catch(error){
1509
+ if(error?.code!=='ENOENT')throw error;
2039
1510
  }
2040
- if(current.isSymbolicLink()||!current.isFile()||!sameFileLocation(current,identity))return false;
2041
- await rm(filePath);
2042
- return true;
2043
1511
  }
2044
1512
 
2045
- async function verifiedFileAt(filePath,expected,label,{strictIdentity=true}={}){
2046
- await assertDirectoryState(expected.directoryState);
2047
- const before=await lstat(filePath,{bigint:true});
2048
- if(before.isSymbolicLink()||!before.isFile()
2049
- ||!sameFileLocation(before,expected.identity)
2050
- ||strictIdentity&&!sameFileIdentity(before,expected.identity)){
2051
- fail(`${label} changed before promotion.`);
2052
- }
2053
- let handle;
2054
- try{handle=await open(filePath,READ_ONLY_NO_FOLLOW);}
2055
- catch(error){
2056
- if(error?.code==='ELOOP')fail(`${label} became a symbolic link before promotion.`);
2057
- throw error;
2058
- }
2059
- let after;
2060
- try{
2061
- const opened=await handle.stat({bigint:true});
2062
- if(!sameFileIdentity(before,opened))fail(`${label} changed while opening.`);
2063
- const bytes=await handle.readFile();
2064
- after=await handle.stat({bigint:true});
2065
- if(!sameFileIdentity(opened,after)||bytes.length!==expected.byteLength
2066
- ||sha256(bytes)!==expected.hash){
2067
- fail(`${label} failed its identity or content check before promotion.`);
2068
- }
2069
- }finally{
2070
- await handle.close();
1513
+ async function writeGeneratedFiles({root,files,signal,onEvent}){
1514
+ for(const file of files){
1515
+ throwIfAborted(signal);
1516
+ await assertPhysicalTextDestination(
1517
+ root,
1518
+ file.filePath,
1519
+ file.label,
1520
+ {createParent:file.createParent===true}
1521
+ );
2071
1522
  }
2072
- const current=await lstat(filePath,{bigint:true});
2073
- if(current.isSymbolicLink()||!current.isFile()||!sameFileIdentity(current,after)){
2074
- fail(`${label} changed after verification.`);
1523
+ const paths=[];
1524
+ let eventError=null;
1525
+ for(const file of files){
1526
+ throwIfAborted(signal);
1527
+ await writeFile(file.filePath,file.content,'utf8');
1528
+ paths.push(file.filePath);
1529
+ const currentEventError=await emit(onEvent,{
1530
+ type:'import-map.write.progress',
1531
+ paths:[...paths]
1532
+ });
1533
+ eventError??=currentEventError;
2075
1534
  }
2076
- await assertDirectoryState(expected.directoryState);
2077
- return current;
1535
+ return eventError;
2078
1536
  }
2079
1537
 
2080
- function originalDescriptor(state){
2081
- return {
2082
- identity:state.identity,
2083
- directoryState:state.directoryState,
2084
- byteLength:state.bytes.length,
2085
- hash:sha256(state.bytes)
2086
- };
2087
- }
2088
-
2089
- async function pathIsAbsent(filePath){
2090
- try{
2091
- await lstat(filePath);
2092
- return false;
2093
- }catch(error){
2094
- if(error?.code==='ENOENT')return true;
2095
- throw error;
1538
+ function resolvedAppRoot(workspaceRoot,appId,appRoot){
1539
+ if(typeof appId!=='string'||appId.trim()===''){
1540
+ throw new TypeError('Import-map app id must be a nonempty string.');
2096
1541
  }
1542
+ const resolved=path.resolve(appRoot??path.join(workspaceRoot,'apps',appId));
1543
+ if(!pathInside(workspaceRoot,resolved))fail('Import-map application root must stay inside the workspace.');
1544
+ return resolved;
2097
1545
  }
2098
1546
 
2099
- async function restoreBackup(state,backup,label){
2100
- const expected=originalDescriptor(state);
2101
- await assertDirectoryState(state.directoryState);
2102
- if(!await pathIsAbsent(state.filePath)){
2103
- fail(`${label} changed before its import-map backup could be restored.`);
1547
+ export async function createApplicationTestImportMapContext({
1548
+ applicationRoot,
1549
+ boundary='source',
1550
+ imports={},
1551
+ signal
1552
+ }={}){
1553
+ if(typeof applicationRoot!=='string'||applicationRoot.trim()===''){
1554
+ throw new TypeError('applicationRoot must be a nonempty string.');
2104
1555
  }
2105
- await verifiedFileAt(backup,expected,`${label} backup`,{strictIdentity:false});
2106
- await rename(backup,state.filePath);
2107
- await verifiedFileAt(state.filePath,expected,`${label} restored file`,{strictIdentity:false});
2108
- }
2109
-
2110
- async function pathStateUnchanged(state,label){
2111
- if(!state.exists){
2112
- try{
2113
- await lstat(state.filePath);
2114
- fail(`${label} appeared while the import map was being generated.`);
2115
- }catch(error){
2116
- if(error?.code!=='ENOENT')throw error;
2117
- }
2118
- return;
1556
+ if(!['source','dist','test'].includes(boundary)){
1557
+ throw new TypeError('boundary must be source, dist, or test.');
2119
1558
  }
2120
- const current=await lstat(state.filePath,{bigint:true});
2121
- if(current.isSymbolicLink()||!current.isFile()||!sameFileIdentity(current,state.identity)){
2122
- fail(`${label} changed while the import map was being generated.`);
1559
+ if(imports===null||typeof imports!=='object'||Array.isArray(imports)){
1560
+ throw new TypeError('imports must be a plain object.');
2123
1561
  }
2124
- }
2125
-
2126
- async function installStagedFile(state,staged,label){
2127
- const backup=path.join(
2128
- path.dirname(state.filePath),
2129
- `.${path.basename(state.filePath)}.arcane-backup-${String(process.pid)}-${randomUUID()}`
2130
- );
2131
- let backedUp=false;
2132
- let promoted=false;
2133
- let installedIdentity=null;
2134
- try{
2135
- await assertDirectoryState(staged.directoryState);
2136
- await pathStateUnchanged(state,label);
2137
- await verifiedFileAt(staged.path,staged,`${label} staged file`);
2138
- if(state.exists){
2139
- await rename(state.filePath,backup);
2140
- backedUp=true;
2141
- await verifiedFileAt(
2142
- backup,
2143
- originalDescriptor(state),
2144
- `${label} backup`,
2145
- {strictIdentity:false}
2146
- );
1562
+ throwIfAborted(signal);
1563
+ const requestedApplicationRoot=path.resolve(applicationRoot);
1564
+ const applicationInfo=await lstat(requestedApplicationRoot);
1565
+ const canonicalApplicationRoot=await realpath(requestedApplicationRoot);
1566
+ if(applicationInfo.isSymbolicLink()||!applicationInfo.isDirectory()
1567
+ ||!samePath(requestedApplicationRoot,canonicalApplicationRoot)){
1568
+ fail('Application test import-map root must be one physical application directory.');
1569
+ }
1570
+ const requestedBase=boundary==='source'
1571
+ ?canonicalApplicationRoot
1572
+ :path.join(canonicalApplicationRoot,boundary);
1573
+ const baseInfo=await lstat(requestedBase);
1574
+ const canonicalBase=await realpath(requestedBase);
1575
+ if(baseInfo.isSymbolicLink()||!baseInfo.isDirectory()
1576
+ ||!samePath(requestedBase,canonicalBase)
1577
+ ||!pathInside(canonicalApplicationRoot,canonicalBase)){
1578
+ fail(`Application ${boundary} import-map base must be one physical app-owned directory.`);
1579
+ }
1580
+ const selectedImports={};
1581
+ for(const [specifier,target] of Object.entries(imports)){
1582
+ throwIfAborted(signal);
1583
+ if(typeof specifier!=='string'||specifier===''||typeof target!=='string'
1584
+ ||!target.startsWith('./')){
1585
+ fail(`Application test import-map entry is invalid: ${String(specifier)}.`);
2147
1586
  }
2148
- await verifiedFileAt(staged.path,staged,`${label} staged file`);
2149
- await rename(staged.path,state.filePath);
2150
- promoted=true;
2151
- installedIdentity=await verifiedFileAt(
2152
- state.filePath,
2153
- staged,
2154
- `${label} installed file`,
2155
- {strictIdentity:false}
1587
+ const relative=safeRelativePath(
1588
+ target.slice(2),
1589
+ `application test import-map target for ${specifier}`
2156
1590
  );
2157
- await assertDirectoryState(staged.directoryState);
2158
- }catch(error){
2159
- if(promoted){
2160
- try{
2161
- const removed=await removeOwnedPath(
2162
- state.filePath,
2163
- installedIdentity??staged.identity,
2164
- staged.directoryState
2165
- );
2166
- if(!removed)fail(`${label} changed before its failed promotion could be removed.`);
2167
- }catch(rollbackError){error.rollbackError??=rollbackError;}
2168
- }
2169
- if(backedUp){
2170
- try{await restoreBackup(state,backup,label);}
2171
- catch(rollbackError){error.rollbackError??=rollbackError;}
1591
+ if(boundary==='source'&&/^(?:dist|test)\//u.test(relative)){
1592
+ fail(`Source import-map target selects another application boundary: ${specifier}.`);
2172
1593
  }
2173
- throw error;
1594
+ const candidate=path.resolve(canonicalBase,...relative.split('/'));
1595
+ const targetInfo=await lstat(candidate);
1596
+ const canonicalTarget=await realpath(candidate);
1597
+ if(targetInfo.isSymbolicLink()||!targetInfo.isFile()
1598
+ ||!samePath(candidate,canonicalTarget)||!pathInside(canonicalBase,canonicalTarget)){
1599
+ fail(`Application test import-map target leaves its physical ${boundary} directory: ${specifier}.`);
1600
+ }
1601
+ selectedImports[specifier]=target;
2174
1602
  }
2175
1603
  return {
2176
- async verify(){
2177
- if(!installedIdentity)fail(`${label} was not installed before pair verification.`);
2178
- return verifiedFileAt(
2179
- state.filePath,
2180
- {
2181
- identity:installedIdentity,
2182
- directoryState:staged.directoryState,
2183
- byteLength:staged.byteLength,
2184
- hash:staged.hash
2185
- },
2186
- `${label} committed file`
2187
- );
2188
- },
2189
- async commit(){
2190
- if(!backedUp)return;
2191
- const removed=await removeOwnedPath(backup,state.identity,staged.directoryState);
2192
- if(!removed)fail(`${label} backup changed before transaction cleanup.`);
2193
- },
2194
- async rollback(){
2195
- await assertDirectoryState(staged.directoryState);
2196
- if(!await pathIsAbsent(state.filePath)){
2197
- const removed=await removeOwnedPath(
2198
- state.filePath,
2199
- installedIdentity,
2200
- staged.directoryState
2201
- );
2202
- if(!removed){
2203
- fail(`${label} changed before its import-map transaction could roll back.`);
2204
- }
2205
- }
2206
- if(backedUp)await restoreBackup(state,backup,label);
2207
- }
1604
+ protocol:'arcane-test-import-map/1',
1605
+ boundary,
1606
+ baseURL:pathToFileURL(`${canonicalBase}${path.sep}`).href,
1607
+ imports:selectedImports
2208
1608
  };
2209
1609
  }
2210
1610
 
2211
- async function commitGeneratedFiles({
2212
- files,
2213
- signal,
2214
- onEvent
2215
- }){
2216
- const staged=[];
2217
- const installed=[];
2218
- let failure;
2219
- try{
2220
- for(const file of files){
2221
- throwIfAborted(signal);
2222
- staged.push(await stageSibling(
2223
- file.state.filePath,
2224
- file.bytes,
2225
- file.state.directoryState
2226
- ));
2227
- }
2228
- await emit(onEvent,{type:'import-map.commit.staged'});
2229
- throwIfAborted(signal);
2230
- for(const [index,file] of files.entries()){
2231
- installed.push(await installStagedFile(file.state,staged[index],file.label));
2232
- throwIfAborted(signal);
2233
- }
2234
- for(const transaction of installed)await transaction.verify();
2235
- await emit(onEvent,{
2236
- type:'import-map.commit.progress',
2237
- paths:Object.freeze(files.map(file=>file.state.filePath))
2238
- });
2239
- throwIfAborted(signal);
2240
- for(const transaction of installed)await transaction.verify();
2241
- }catch(error){
2242
- for(const transaction of [...installed].reverse()){
2243
- await transaction.rollback().catch(rollback=>{error.rollbackError??=rollback;});
2244
- }
2245
- failure=error;
2246
- }
2247
- const cleanupErrors=[];
2248
- for(const [index,stage] of staged.entries()){
2249
- if(index<installed.length)continue;
2250
- try{
2251
- const removed=await removeOwnedPath(
2252
- stage.path,
2253
- stage.identity,
2254
- stage.directoryState
2255
- );
2256
- if(!removed){
2257
- fail(`${files[index].label} stage could not be safely cleaned: ${stage.path}.`);
2258
- }
2259
- }catch(error){cleanupErrors.push(error);}
2260
- }
2261
- if(failure){
2262
- if(cleanupErrors.length>0){
2263
- failure.cleanupError??=cleanupErrors.length===1
2264
- ?cleanupErrors[0]
2265
- :new AggregateError(cleanupErrors,'Import-map transaction cleanup failed.');
2266
- }
2267
- throw failure;
2268
- }
2269
- if(cleanupErrors.length>0){
2270
- throw new AggregateError(cleanupErrors,'Import-map transaction cleanup failed.');
2271
- }
2272
-
2273
- const cleanupWarnings=[];
2274
- for(const transaction of [...installed].reverse()){
2275
- try{await transaction.commit();}
2276
- catch(error){cleanupWarnings.push(error);}
2277
- }
2278
- for(const transaction of installed)await transaction.verify();
2279
- if(cleanupWarnings.length>0){
2280
- return Object.freeze(cleanupWarnings.map(error=>String(error?.message??error)));
2281
- }
2282
- return Object.freeze([]);
2283
- }
2284
-
2285
- function resolvedAppRoot(workspaceRoot,appId,appRoot){
2286
- if(!SAFE_APP_ID.test(appId??'')){
2287
- fail(`Import-map app id must use lowercase letters, digits, and internal hyphens: ${String(appId)}.`);
2288
- }
2289
- const resolved=path.resolve(appRoot??path.join(workspaceRoot,'apps',appId));
2290
- if(!pathInside(workspaceRoot,resolved))fail('Import-map application root must stay inside the workspace.');
2291
- return resolved;
2292
- }
2293
-
2294
1611
  async function generateImportMapUnlocked({
2295
1612
  workspaceRoot,
2296
1613
  appId,
2297
1614
  appRoot,
2298
1615
  entry='index.html',
2299
1616
  documents,
2300
- workspaceRuntimeReceipt,
2301
1617
  signal,
2302
1618
  onEvent
2303
1619
  }={}){
@@ -2307,24 +1623,24 @@ async function generateImportMapUnlocked({
2307
1623
  throwIfAborted(signal);
2308
1624
  const resolvedWorkspace=path.resolve(workspaceRoot);
2309
1625
  const resolvedApp=resolvedAppRoot(resolvedWorkspace,appId,appRoot);
1626
+ await physicalDirectory(resolvedWorkspace,resolvedApp);
2310
1627
  const safeEntry=safeRelativePath(entry,'application entry');
2311
1628
  const safeDocuments=normalizedDocumentPaths(safeEntry,documents);
2312
- const documentPaths=Object.freeze(safeDocuments.map(relative=>{
1629
+ const documentPaths=safeDocuments.map(relative=>{
2313
1630
  const documentPath=path.resolve(resolvedApp,...relative.split('/'));
2314
1631
  if(!pathInside(resolvedApp,documentPath)){
2315
1632
  fail(`Import-map application document escapes its app root: ${relative}.`);
2316
1633
  }
2317
1634
  return documentPath;
2318
- }));
1635
+ });
2319
1636
  const entryPath=documentPaths[0];
2320
1637
  const artifactPath=path.join(resolvedApp,...IMPORT_MAP_RELATIVE_PATH.split('/'));
2321
- await emit(onEvent,{
1638
+ let eventError=await emit(onEvent,{
2322
1639
  type:'import-map.started',
2323
1640
  appId,
2324
1641
  artifactPath,
2325
1642
  entryPath,
2326
- documentPaths,
2327
- documentCount:documentPaths.length
1643
+ documentPaths
2328
1644
  });
2329
1645
 
2330
1646
  const documentStates=[];
@@ -2333,94 +1649,40 @@ async function generateImportMapUnlocked({
2333
1649
  const label=index===0
2334
1650
  ?'Import-map application entry'
2335
1651
  :`Import-map application document ${safeDocuments[index]}`;
2336
- const directoryState=await captureDirectoryState(
2337
- resolvedWorkspace,
2338
- path.dirname(documentPath)
2339
- );
2340
- const state=await readRealFileState(documentPath,label);
1652
+ const html=await readPhysicalTextFile(resolvedApp,documentPath,label);
2341
1653
  throwIfAborted(signal);
2342
- state.directoryState=directoryState;
2343
- const html=state.bytes.toString('utf8');
2344
- // Reject malformed application structure before traversing the substantially larger
2345
- // runtime graph. The real generated map is rendered and revalidated before commit.
1654
+ // Reject malformed application structure before traversing the runtime inventory.
2346
1655
  const baseHref=documentBaseHref(safeDocuments[index]);
2347
1656
  renderManagedHtml(html,'{"imports":{}}\n',baseHref);
2348
- documentStates.push({state,html,label,baseHref});
2349
- }
2350
- let runtime;
2351
- if(workspaceRuntimeReceipt){
2352
- await authenticateWorkspaceRuntimeReceipt(workspaceRuntimeReceipt,{
2353
- workspaceRoot:resolvedWorkspace,
2354
- signal
2355
- });
2356
- runtime={
2357
- files:workspaceRuntimeReceipt.files.map(file=>file.path),
2358
- readFile:relativePath=>readVerifiedWorkspaceRuntimeFile(workspaceRuntimeReceipt,{
2359
- workspaceRoot:resolvedWorkspace,
2360
- relativePath,
2361
- signal
2362
- })
2363
- };
2364
- }else{
2365
- runtime=await physicalRuntime(resolvedWorkspace,signal);
2366
- }
2367
- for(const required of SDK_BROWSER_FILES){
2368
- if(!runtime.files.includes(required)){
2369
- fail(
2370
- `Workspace Arcane runtime is missing the authenticated SDK browser file `
2371
- +`"${required}". Materialize the current SDK runtime, then rerun arcane import-map.`,
2372
- 'ARCANE_IMPORT_MAP_UNRESOLVED'
2373
- );
2374
- }
1657
+ documentStates.push({filePath:documentPath,html,label,baseHref});
2375
1658
  }
2376
- const built=await buildImportMap({files:runtime.files,readFile:runtime.readFile,signal});
2377
- const document={imports:built.imports};
2378
- const json=`${JSON.stringify(document,null,2).replaceAll('<','\\u003c')}\n`;
2379
- const renderedDocuments=documentStates.map(item=>Object.freeze({
1659
+ const {built,json}=await managedImportMapBuild(resolvedWorkspace,signal);
1660
+ const renderedDocuments=documentStates.map(item=>({
2380
1661
  ...item,
2381
- bytes:Buffer.from(renderManagedHtml(item.html,json,item.baseHref),'utf8')
1662
+ content:renderManagedHtml(item.html,json,item.baseHref)
2382
1663
  }));
2383
1664
 
2384
1665
  throwIfAborted(signal);
2385
- const artifactDirectoryState=await captureDirectoryState(
2386
- resolvedWorkspace,
2387
- path.dirname(artifactPath),
2388
- {create:true}
2389
- );
2390
- const artifactState=await readRealFileState(
2391
- artifactPath,
2392
- 'Import-map artifact',
2393
- {optional:true}
2394
- );
2395
- artifactState.directoryState=artifactDirectoryState;
2396
- const artifactBytes=Buffer.from(json,'utf8');
2397
- const cleanupWarnings=await commitGeneratedFiles({
1666
+ const writeEventError=await writeGeneratedFiles({
1667
+ root:resolvedApp,
2398
1668
  files:[
2399
- {state:artifactState,bytes:artifactBytes,label:'Import-map artifact'},
1669
+ {
1670
+ filePath:artifactPath,
1671
+ content:json,
1672
+ label:'Import-map artifact',
1673
+ createParent:true
1674
+ },
2400
1675
  ...renderedDocuments.map(item=>({
2401
- state:item.state,
2402
- bytes:item.bytes,
1676
+ filePath:item.filePath,
1677
+ content:item.content,
2403
1678
  label:item.label
2404
1679
  }))
2405
1680
  ],
2406
1681
  signal,
2407
1682
  onEvent
2408
1683
  });
2409
- const committedFiles=Object.freeze([
2410
- Object.freeze({
2411
- role:'artifact',
2412
- path:path.relative(resolvedWorkspace,artifactPath).split(path.sep).join('/'),
2413
- bytes:artifactBytes.length,
2414
- sha256:sha256(artifactBytes)
2415
- }),
2416
- ...renderedDocuments.map((item,index)=>Object.freeze({
2417
- role:index===0?'entry':'document',
2418
- path:path.relative(resolvedWorkspace,item.state.filePath).split(path.sep).join('/'),
2419
- bytes:item.bytes.length,
2420
- sha256:sha256(item.bytes)
2421
- }))
2422
- ]);
2423
- const receipt=Object.freeze({
1684
+ eventError??=writeEventError;
1685
+ const result={
2424
1686
  appId,
2425
1687
  artifactPath,
2426
1688
  artifactRelativePath:path.relative(resolvedWorkspace,artifactPath).split(path.sep).join('/'),
@@ -2428,52 +1690,35 @@ async function generateImportMapUnlocked({
2428
1690
  documentPaths,
2429
1691
  documentCount:documentPaths.length,
2430
1692
  imports:built.imports,
2431
- entryCount:built.entryCount,
2432
1693
  excludedModules:built.excludedModules,
2433
- files:committedFiles,
2434
- cleanupWarnings,
1694
+ committed:true
1695
+ };
1696
+ const completedEventError=await emit(onEvent,{
1697
+ type:'import-map.completed',
1698
+ appId,
1699
+ artifactPath,
1700
+ entryPath,
1701
+ documentPaths,
2435
1702
  committed:true
2436
1703
  });
2437
- try{
2438
- await emit(onEvent,{
2439
- type:'import-map.completed',
2440
- appId,
2441
- artifactPath,
2442
- entryPath,
2443
- documentPaths,
2444
- documentCount:receipt.documentCount,
2445
- entryCount:receipt.entryCount,
2446
- cleanupWarnings:receipt.cleanupWarnings,
2447
- committed:true
2448
- });
2449
- }catch(error){
2450
- return Object.freeze({
2451
- ...receipt,
2452
- eventDelivery:Object.freeze({
1704
+ eventError??=completedEventError;
1705
+ if(eventError){
1706
+ return {
1707
+ ...result,
1708
+ eventDelivery:{
2453
1709
  status:'degraded',
2454
1710
  errorCode:'ARCANE_EVENT_DELIVERY_FAILED',
2455
- message:String(error?.message??error)
2456
- })
2457
- });
1711
+ message:String(eventError?.message??eventError)
1712
+ }
1713
+ };
2458
1714
  }
2459
- return receipt;
1715
+ return result;
2460
1716
  }
2461
1717
 
2462
1718
  export async function generateImportMap(options={}){
2463
- const {
2464
- workspaceRoot,
2465
- signal,
2466
- onEvent,
2467
- workspaceOperationLease
2468
- }=options??{};
1719
+ const {workspaceRoot}=options??{};
2469
1720
  if(typeof workspaceRoot!=='string'||workspaceRoot.trim()===''){
2470
1721
  throw new TypeError('generateImportMap workspaceRoot must be a nonempty string.');
2471
1722
  }
2472
- return withWorkspaceOperationLock({
2473
- workspaceRoot,
2474
- operation:'import-map',
2475
- signal,
2476
- onEvent,
2477
- workspaceOperationLease
2478
- },()=>generateImportMapUnlocked(options));
1723
+ return generateImportMapUnlocked(options);
2479
1724
  }