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.
- package/CHANGELOG.md +27 -0
- package/README.md +86 -117
- package/bin/arcane-test.mjs +170 -46
- package/browser-runtime/ai/browser-speech-artifacts.mjs +887 -909
- package/browser-runtime/ai/browser-speech-providers.mjs +96 -152
- package/browser-runtime/ai/browser-wasm-llm-provider.mjs +627 -819
- package/browser-runtime/ai/browser-wasm.mjs +24 -35
- package/browser-runtime/ai/browser-wllama-runtime.mjs +64 -316
- package/browser-runtime/ai/model-controller.mjs +584 -181
- package/browser-runtime/ai/speech-worker-client.mjs +8 -146
- package/browser-runtime/ai/speech-worker-runtime.mjs +643 -363
- package/browser-runtime/dom-event-instrumentation.mjs +55 -147
- package/browser-runtime/event-manager.mjs +239 -624
- package/package.json +5 -6
- package/runtime/arcane/components/app-bar.html +3 -15
- package/runtime/arcane/components/assistant-panel.html +10 -10
- package/runtime/arcane/components/calculator.html +1 -1
- package/runtime/arcane/components/chat.html +1359 -135
- package/runtime/arcane/components/conversation-view.html +2 -2
- package/runtime/arcane/components/document-inspector.html +11 -17
- package/runtime/arcane/components/file-manager.html +13 -56
- package/runtime/arcane/components/markdown-document.html +82 -281
- package/runtime/arcane/components/markdown-editor.html +7 -10
- package/runtime/arcane/components/media-embed.html +6 -6
- package/runtime/arcane/components/screen-capture.html +4 -4
- package/runtime/arcane/components/source-explanation.html +2 -2
- package/runtime/arcane/components/speech.html +112 -68
- package/runtime/arcane/components/terminal-workspace.html +4 -4
- package/runtime/arcane/components/theme-editor.html +1 -1
- package/runtime/arcane/components/unified-inbox.html +2 -2
- package/runtime/arcane/components/voice-transcription.html +31 -21
- package/runtime/arcane/entities/Calculation.js +2 -3
- package/runtime/arcane/entities/Chat.js +228 -43
- package/runtime/arcane/entities/Preference.js +3 -5
- package/runtime/arcane/entities/Weather.js +5 -5
- package/runtime/arcane/modules/AI.js +1050 -427
- package/runtime/arcane/modules/AIProviderRuntime.js +658 -363
- package/runtime/arcane/modules/AIResponseLength.js +9 -19
- package/runtime/arcane/modules/AIRuntimeState.js +109 -72
- package/runtime/arcane/modules/ArcaneNavigationPolicy.js +45 -32
- package/runtime/arcane/modules/BrowserTestSuite.js +78 -122
- package/runtime/arcane/modules/CalculatorEngine.js +9 -9
- package/runtime/arcane/modules/CommunicationAppController.js +3 -7
- package/runtime/arcane/modules/ComponentContracts.js +30 -32
- package/runtime/arcane/modules/ConfiguredAIChatSession.js +281 -230
- package/runtime/arcane/modules/ConversationActionItems.js +26 -59
- package/runtime/arcane/modules/ConversationClosingReport.js +34 -61
- package/runtime/arcane/modules/ConversationTimebox.js +27 -15
- package/runtime/arcane/modules/DBOPFSDocumentLibrary.js +152 -344
- package/runtime/arcane/modules/DocumentLexicalSearch.js +25 -91
- package/runtime/arcane/modules/HTMLImport.js +54 -1
- package/runtime/arcane/modules/IsolatedModelQuestionRunner.js +40 -203
- package/runtime/arcane/modules/LocalAIReadiness.js +40 -60
- package/runtime/arcane/modules/LocalAIReadinessController.js +15 -13
- package/runtime/arcane/modules/MD.js +1 -45
- package/runtime/arcane/modules/Mail.js +51 -103
- package/runtime/arcane/modules/MailOutbox.mjs +95 -193
- package/runtime/arcane/modules/MailTransport.mjs +36 -57
- package/runtime/arcane/modules/ModelDefinition.js +22 -106
- package/runtime/arcane/modules/OpenMeteoWeatherProvider.js +39 -101
- package/runtime/arcane/modules/PersistentAIChatSession.js +281 -18
- package/runtime/arcane/modules/PreferenceStore.js +102 -30
- package/runtime/arcane/modules/RiskSignalAnalyzer.js +8 -9
- package/runtime/arcane/modules/ScopedOPFSCache.js +7 -42
- package/runtime/arcane/modules/ScreenCapture.js +175 -128
- package/runtime/arcane/modules/SpeechPlayback.js +46 -149
- package/runtime/arcane/modules/StaticDocumentCatalog.js +173 -407
- package/runtime/arcane/modules/ToolCallRouter.js +25 -12
- package/runtime/arcane/modules/YouTubeMedia.js +6 -5
- package/schemas/arcane-app-bundle.schema.json +13 -78
- package/schemas/arcane-app.schema.json +9 -25
- package/schemas/arcane-lock.schema.json +18 -151
- package/schemas/arcane-package.schema.json +2 -16
- package/schemas/native-build-plan.schema.json +119 -122
- package/src/app-descriptor.mjs +75 -132
- package/src/application-tests.mjs +200 -0
- package/src/cli/main.mjs +27 -46
- package/src/constants.mjs +3 -4
- package/src/dev-server.mjs +30 -324
- package/src/doctor.mjs +92 -154
- package/src/dom-event-instrumentation.mjs +55 -147
- package/src/errors.mjs +2 -3
- package/src/event-manager.mjs +239 -624
- package/src/event-queue.mjs +3 -3
- package/src/import-map.mjs +273 -1028
- package/src/index.mjs +14 -16
- package/src/installed-sdk-runtime.mjs +40 -62
- package/src/integrated-provider-loader.mjs +53 -382
- package/src/mail-api.mjs +0 -2
- package/src/mail-server.mjs +224 -580
- package/src/mail.mjs +4 -10
- package/src/native-plan.mjs +163 -598
- package/src/native-provider-loader.mjs +104 -1063
- package/src/packager/core.mjs +485 -3229
- package/src/process.mjs +5 -10
- package/src/release-bundle.mjs +292 -2405
- package/src/runtime.mjs +76 -396
- package/src/scaffold.mjs +30 -80
- package/src/sdk-browser-runtime.mjs +70 -626
- package/src/source-server.mjs +588 -0
- package/src/targets/index.mjs +78 -188
- package/src/templates/workspace-template.mjs +19 -135
- package/src/testing-loader.mjs +164 -0
- package/src/testing.mjs +1 -1
- package/src/toolchain.mjs +131 -544
- package/src/update-check.mjs +26 -64
- package/src/workspace-operation-lock.mjs +139 -430
- package/src/workspace-runtime.mjs +112 -779
- package/src/workspace.mjs +40 -302
- package/browser-runtime/ARCANE_SDK_BROWSER_RELEASE.json +0 -218
- package/browser-runtime/ai/ARCANE_AI_BROWSER_SPEECH_COMPONENTS.json +0 -203
- package/browser-runtime/ai/ARCANE_AI_BROWSER_WASM_COMPONENTS.json +0 -80
- package/browser-runtime/ai/internal/sha256.mjs +0 -166
- package/docs/architecture.md +0 -344
- package/docs/compatibility.md +0 -36
- package/docs/event-manager.md +0 -294
- package/docs/platform-targets.md +0 -108
- package/docs/publishing.md +0 -201
- package/docs/reference/README.md +0 -187
- package/docs/reference/ai/browser-speech-package-authority.json +0 -835
- package/docs/reference/ai/browser-speech.md +0 -1252
- package/docs/reference/ai/browser-wasm.md +0 -530
- package/docs/reference/arcane-ollama.md +0 -288
- package/docs/reference/availability-and-normalization.md +0 -183
- package/docs/reference/behavioral-testing.md +0 -133
- package/docs/reference/cli.md +0 -779
- package/docs/reference/core/README.md +0 -62
- package/docs/reference/core/arcane-ai-contracts.md +0 -907
- package/docs/reference/core/arcane-api.md +0 -601
- package/docs/reference/core/arcane-entities.md +0 -65
- package/docs/reference/core/arcane-events.md +0 -134
- package/docs/reference/core/ollama-module.md +0 -181
- package/docs/reference/core/reference/arcane-api/ai-and-ollama.md +0 -1909
- package/docs/reference/core/reference/arcane-api/applications-terminal-capabilities.md +0 -1057
- package/docs/reference/core/reference/arcane-api/core-and-events.md +0 -320
- package/docs/reference/core/reference/arcane-api/filesystem-storage-preferences-appearance.md +0 -610
- package/docs/reference/core/reference/arcane-api/namespaces.md +0 -1157
- package/docs/reference/core/reference/arcane-api/platform-installation-users-system.md +0 -1423
- package/docs/reference/core/reference/arcane-api/session-provisioning-diagnostics-development.md +0 -315
- package/docs/reference/event-manager.md +0 -1511
- package/docs/reference/inventory/package-api.json +0 -3284
- package/docs/reference/inventory/runtime-components.json +0 -1011
- package/docs/reference/inventory/runtime-entities.json +0 -26
- package/docs/reference/inventory/runtime-modules.json +0 -1431
- package/docs/reference/mail.md +0 -316
- package/docs/reference/protocols.md +0 -677
- package/docs/reference/runtime-components.md +0 -1366
- package/docs/reference/runtime-entities.md +0 -303
- package/docs/reference/runtime-modules.md +0 -2960
- package/docs/reference/sdk-api.md +0 -6694
- package/docs/roadmap.md +0 -79
- package/docs/work-amplification.md +0 -129
- package/runtime/ARCANE_RUNTIME_RELEASE.json +0 -826
package/src/import-map.mjs
CHANGED
|
@@ -1,62 +1,26 @@
|
|
|
1
|
-
import {
|
|
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
|
|
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
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
|
82
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
77
|
+
return [
|
|
118
78
|
entry,
|
|
119
|
-
...normalized.filter(relative=>relative!==entry).sort(
|
|
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
|
|
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)
|
|
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
|
-
|
|
802
|
-
|
|
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
|
|
782
|
+
return {
|
|
849
783
|
hasModuleSyntax,
|
|
850
|
-
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
|
-
|
|
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
|
|
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,
|
|
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
|
|
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(
|
|
1028
|
-
const
|
|
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
|
|
1044
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1059
|
-
|
|
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
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
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
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
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(
|
|
839
|
+
if(inventory.has('sdk/dom-event-instrumentation.mjs')){
|
|
1097
840
|
registerSpecifier(
|
|
1098
841
|
namedRegistry,
|
|
1099
|
-
'arcane-os/event-
|
|
1100
|
-
'./arcane/sdk/event-
|
|
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
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
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(
|
|
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(
|
|
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)=>
|
|
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
|
|
1172
|
-
imports
|
|
1173
|
-
|
|
1174
|
-
|
|
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)=>
|
|
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
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
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)
|
|
1452
|
-
|
|
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
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
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
|
-
|
|
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)
|
|
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
|
-
|
|
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)
|
|
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=>
|
|
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=>
|
|
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
|
|
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
|
|
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
|
|
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
|
|
1801
|
-
bases
|
|
1802
|
-
managedMaps
|
|
1803
|
-
scripts
|
|
1804
|
-
links
|
|
1805
|
-
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
|
|
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
|
|
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
|
-
|
|
1931
|
-
|
|
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
|
|
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)
|
|
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
|
|
1481
|
+
return {root:resolvedRoot,canonicalRoot,directory:resolvedDirectory};
|
|
1971
1482
|
}
|
|
1972
1483
|
|
|
1973
|
-
function
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
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
|
|
1989
|
-
await
|
|
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
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
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
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
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
|
-
|
|
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
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
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
|
|
2073
|
-
|
|
2074
|
-
|
|
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
|
-
|
|
2077
|
-
return current;
|
|
1535
|
+
return eventError;
|
|
2078
1536
|
}
|
|
2079
1537
|
|
|
2080
|
-
function
|
|
2081
|
-
|
|
2082
|
-
|
|
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
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
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
|
-
|
|
2106
|
-
|
|
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
|
-
|
|
2121
|
-
|
|
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
|
-
|
|
2127
|
-
const
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
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
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
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
|
-
|
|
2158
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
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=
|
|
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
|
|
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
|
-
|
|
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({
|
|
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
|
|
2377
|
-
const
|
|
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
|
-
|
|
1662
|
+
content:renderManagedHtml(item.html,json,item.baseHref)
|
|
2382
1663
|
}));
|
|
2383
1664
|
|
|
2384
1665
|
throwIfAborted(signal);
|
|
2385
|
-
const
|
|
2386
|
-
|
|
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
|
-
{
|
|
1669
|
+
{
|
|
1670
|
+
filePath:artifactPath,
|
|
1671
|
+
content:json,
|
|
1672
|
+
label:'Import-map artifact',
|
|
1673
|
+
createParent:true
|
|
1674
|
+
},
|
|
2400
1675
|
...renderedDocuments.map(item=>({
|
|
2401
|
-
|
|
2402
|
-
|
|
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
|
-
|
|
2410
|
-
|
|
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
|
-
|
|
2434
|
-
|
|
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
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
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(
|
|
2456
|
-
}
|
|
2457
|
-
}
|
|
1711
|
+
message:String(eventError?.message??eventError)
|
|
1712
|
+
}
|
|
1713
|
+
};
|
|
2458
1714
|
}
|
|
2459
|
-
return
|
|
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
|
|
2473
|
-
workspaceRoot,
|
|
2474
|
-
operation:'import-map',
|
|
2475
|
-
signal,
|
|
2476
|
-
onEvent,
|
|
2477
|
-
workspaceOperationLease
|
|
2478
|
-
},()=>generateImportMapUnlocked(options));
|
|
1723
|
+
return generateImportMapUnlocked(options);
|
|
2479
1724
|
}
|