arcane-os 0.1.0-dev.5 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,2328 @@
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';
4
+ import path from 'node:path';
5
+ import {
6
+ authenticateWorkspaceRuntimeReceipt,
7
+ readVerifiedWorkspaceRuntimeFile
8
+ } from './workspace-runtime.mjs';
9
+ import {withWorkspaceOperationLock} from './workspace-operation-lock.mjs';
10
+
11
+ export const IMPORT_MAP_RELATIVE_PATH='modules/arcane.importmap.json';
12
+ export const MANAGED_IMPORT_MAP_ATTRIBUTE='data-arcane-import-map';
13
+
14
+ const JAVASCRIPT_EXTENSION=/\.(?:js|mjs)$/u;
15
+ const NODE_ONLY_MODULE='modules/CaseEvidenceIndexer.js';
16
+ const RUNTIME_STRONG_TYPE_IMPORT='../../node_modules/strong-type/index.js';
17
+ const SDK_BROWSER_ENTRY='sdk/event-manager.mjs';
18
+ const SDK_BROWSER_FILES=Object.freeze([
19
+ SDK_BROWSER_ENTRY,
20
+ 'sdk/dom-event-instrumentation.mjs',
21
+ 'sdk/dependencies/event-pubsub/index.js',
22
+ 'sdk/dependencies/event-pubsub/licence',
23
+ 'sdk/dependencies/event-pubsub/package.json',
24
+ 'sdk/dependencies/strong-type/index.js',
25
+ 'sdk/dependencies/strong-type/licence',
26
+ 'sdk/dependencies/strong-type/package.json'
27
+ ]);
28
+ const READ_ONLY_NO_FOLLOW=FS_CONSTANTS.O_RDONLY|(FS_CONSTANTS.O_NOFOLLOW??0);
29
+ const WRITE_NEW_NO_FOLLOW=FS_CONSTANTS.O_CREAT|FS_CONSTANTS.O_EXCL
30
+ |FS_CONSTANTS.O_WRONLY|(FS_CONSTANTS.O_NOFOLLOW??0);
31
+ const SAFE_APP_ID=/^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/u;
32
+
33
+ function fail(message,code='ARCANE_IMPORT_MAP_INVALID'){
34
+ const error=new Error(message);
35
+ error.code=code;
36
+ throw error;
37
+ }
38
+
39
+ function throwIfAborted(signal){
40
+ if(!signal?.aborted)return;
41
+ const error=signal.reason instanceof Error?signal.reason:new Error('Operation cancelled.');
42
+ error.code=error.code||'ARCANE_CANCELLED';
43
+ throw error;
44
+ }
45
+
46
+ async function emit(onEvent,event){
47
+ if(typeof onEvent==='function')await onEvent(Object.freeze(event));
48
+ }
49
+
50
+ function compareUtf8(left,right){
51
+ return Buffer.compare(Buffer.from(String(left),'utf8'),Buffer.from(String(right),'utf8'));
52
+ }
53
+
54
+ function collisionKey(value){
55
+ return value.normalize('NFC').toLowerCase();
56
+ }
57
+
58
+ function safeRelativePath(value,label='path'){
59
+ if(typeof value!=='string'||!value||value.includes('\\')||value.includes('\0')
60
+ ||path.posix.isAbsolute(value)||path.posix.normalize(value)!==value
61
+ ||value==='.'||value.startsWith('../')||value.includes('/../')){
62
+ fail(`Import-map ${label} is unsafe: ${String(value)}.`);
63
+ }
64
+ return value;
65
+ }
66
+
67
+ function decodedEscape(source,index){
68
+ const character=source[index];
69
+ if(/[1-9]/u.test(character)||(character==='0'&&/[0-9]/u.test(source[index+1]??''))){
70
+ fail('Import-map scan found a legacy octal or decimal string escape.');
71
+ }
72
+ const simple={b:'\b',f:'\f',n:'\n',r:'\r',t:'\t',v:'\v','0':'\0'};
73
+ if(Object.hasOwn(simple,character))return {value:simple[character],next:index+1};
74
+ if(character==='\n')return {value:'',next:index+1};
75
+ if(character==='\r')return {value:'',next:source[index+1]==='\n'?index+2:index+1};
76
+ if(character==='\u2028'||character==='\u2029')return {value:'',next:index+1};
77
+ if(character==='x'){
78
+ const digits=source.slice(index+1,index+3);
79
+ if(!/^[a-f0-9]{2}$/iu.test(digits))fail('Import-map scan found an invalid hexadecimal string escape.');
80
+ return {value:String.fromCodePoint(Number.parseInt(digits,16)),next:index+3};
81
+ }
82
+ if(character==='u'){
83
+ if(source[index+1]==='{'){
84
+ const close=source.indexOf('}',index+2);
85
+ const digits=close<0?'':source.slice(index+2,close);
86
+ if(!/^[a-f0-9]{1,6}$/iu.test(digits))fail('Import-map scan found an invalid Unicode string escape.');
87
+ const point=Number.parseInt(digits,16);
88
+ if(point>0x10ffff)fail('Import-map scan found an out-of-range Unicode string escape.');
89
+ return {value:String.fromCodePoint(point),next:close+1};
90
+ }
91
+ const digits=source.slice(index+1,index+5);
92
+ if(!/^[a-f0-9]{4}$/iu.test(digits))fail('Import-map scan found an invalid Unicode string escape.');
93
+ return {value:String.fromCodePoint(Number.parseInt(digits,16)),next:index+5};
94
+ }
95
+ return {value:character,next:index+1};
96
+ }
97
+
98
+ function stringToken(source,start){
99
+ const quote=source[start];
100
+ let value='';
101
+ let index=start+1;
102
+ while(index<source.length){
103
+ const character=source[index];
104
+ if(character===quote){
105
+ return {token:{type:'string',value,start,end:index+1},next:index+1};
106
+ }
107
+ if(LINE_TERMINATOR.test(character)){
108
+ fail(`Import-map scan found an unterminated string literal at offset ${String(start)}.`);
109
+ }
110
+ if(character==='\\'){
111
+ if(index+1>=source.length){
112
+ fail(`Import-map scan found an unterminated string escape at offset ${String(start)}.`);
113
+ }
114
+ const decoded=decodedEscape(source,index+1);
115
+ value+=decoded.value;
116
+ index=decoded.next;
117
+ continue;
118
+ }
119
+ value+=character;
120
+ index+=1;
121
+ }
122
+ fail(`Import-map scan found an unterminated string literal at offset ${String(start)}.`);
123
+ }
124
+
125
+ function templateChunk(source,start,{opening=false}={}){
126
+ let index=opening?start+1:start;
127
+ while(index<source.length){
128
+ if(source[index]==='`')return {complete:true,next:index+1};
129
+ if(source[index]==='\\'){
130
+ index+=2;
131
+ continue;
132
+ }
133
+ if(source[index]==='$'&&source[index+1]==='{'){
134
+ return {complete:false,next:index+2};
135
+ }
136
+ index+=1;
137
+ }
138
+ fail(`Import-map scan found an unterminated template literal at offset ${String(start)}.`);
139
+ }
140
+
141
+ const IDENTIFIER_START=/^(?:[$_]|\p{ID_Start})$/u;
142
+ const IDENTIFIER_CONTINUE=/^(?:[$_\u200c\u200d]|\p{ID_Continue})$/u;
143
+ const LINE_TERMINATOR=/[\n\r\u2028\u2029]/u;
144
+ const REGEX_PREFIX_KEYWORDS=new Set([
145
+ 'case','default','delete','do','else','extends','in','instanceof','new','return',
146
+ 'throw','typeof','void'
147
+ ]);
148
+
149
+ function sourceCharacter(source,index){
150
+ const point=source.codePointAt(index);
151
+ if(point==null)return '';
152
+ return String.fromCodePoint(point);
153
+ }
154
+
155
+ function identifierIsProperty(tokens,index){
156
+ const previous=tokens[index-1];
157
+ if(previous?.value==='.'||previous?.value==='?.')return true;
158
+ return previous?.value==='#'
159
+ &&(tokens[index-2]?.value==='.'||tokens[index-2]?.value==='?.');
160
+ }
161
+
162
+ function regexMayStart(previous,{
163
+ beforePrevious,
164
+ beforeBeforePrevious,
165
+ lineTerminatorBefore=false
166
+ }={}){
167
+ if(!previous)return true;
168
+ if(previous.slashGoalAfter==='regex'||previous.closesControl===true)return true;
169
+ if(previous.slashGoalAfter==='division')return false;
170
+ if(previous.slashGoalAfter==='ambiguous'){
171
+ fail(
172
+ `Import-map scan cannot determine whether the slash after offset ${String(previous.start)} `
173
+ +'begins a regular expression or continues an expression. Rewrite that boundary '
174
+ +'with an explicit statement or expression delimiter.'
175
+ );
176
+ }
177
+ if(previous.restrictedStatementLabel===true){
178
+ if(lineTerminatorBefore)return true;
179
+ fail(
180
+ `Import-map scan found a ${previous.restrictedStatementKind} label followed by a slash `
181
+ +`on the same line at offset ${String(previous.start)}.`
182
+ );
183
+ }
184
+ if(previous.type==='identifier'){
185
+ if(beforePrevious?.value==='.'||beforePrevious?.value==='?.'
186
+ ||(beforePrevious?.value==='#'
187
+ &&(beforeBeforePrevious?.value==='.'
188
+ ||beforeBeforePrevious?.value==='?.'))){
189
+ return false;
190
+ }
191
+ if(previous.contextualRegexPrefix===true)return true;
192
+ if(previous.value==='break'||previous.value==='continue'){
193
+ if(lineTerminatorBefore)return true;
194
+ fail(
195
+ `Import-map scan found ${previous.value} followed by a slash on the same line at `
196
+ +`offset ${String(previous.start)}. A label or statement boundary is required before `
197
+ +'a regular expression at this position.'
198
+ );
199
+ }
200
+ return REGEX_PREFIX_KEYWORDS.has(previous.value);
201
+ }
202
+ if(new Set(['regex','string','number','template']).has(previous.type))return false;
203
+ return !new Set([')',']','}','++','--']).has(previous.value);
204
+ }
205
+
206
+ function declarationPosition(tokens,index){
207
+ const previous=tokens[index-1];
208
+ if(!previous)return true;
209
+ if(previous.closesControl===true||previous.slashGoalAfter==='regex')return true;
210
+ if(new Set([';','{','}']).has(previous.value))return true;
211
+ return previous.type==='identifier'
212
+ &&new Set(['default','else','export']).has(previous.value);
213
+ }
214
+
215
+ function classHeader(tokens){
216
+ const depths={parenthesis:0,bracket:0,brace:0};
217
+ let topLevelAssignment=false;
218
+ for(let index=tokens.length-1;index>=0;index-=1){
219
+ const token=tokens[index];
220
+ if(token.value===')')depths.parenthesis+=1;
221
+ else if(token.value==='('){
222
+ if(depths.parenthesis===0)return null;
223
+ depths.parenthesis-=1;
224
+ }else if(token.value===']')depths.bracket+=1;
225
+ else if(token.value==='['){
226
+ if(depths.bracket===0)return null;
227
+ depths.bracket-=1;
228
+ }else if(token.value==='}')depths.brace+=1;
229
+ else if(token.value==='{'){
230
+ if(depths.brace===0)break;
231
+ depths.brace-=1;
232
+ }
233
+ if(depths.parenthesis!==0||depths.bracket!==0||depths.brace!==0)continue;
234
+ if(token.value===';')break;
235
+ if(token.value===':'||token.value==='=')topLevelAssignment=true;
236
+ if(token.type==='identifier'&&token.value==='class'
237
+ &&!identifierIsProperty(tokens,index)){
238
+ if(topLevelAssignment)return null;
239
+ return {
240
+ declaration:declarationPosition(tokens,index),
241
+ kind:'class'
242
+ };
243
+ }
244
+ }
245
+ return null;
246
+ }
247
+
248
+ function functionHeader(tokens,closingParenthesis,enclosingBraceKind){
249
+ const openIndex=closingParenthesis?.openTokenIndex;
250
+ if(!Number.isInteger(openIndex))return null;
251
+ for(let index=openIndex-1;index>=0;index-=1){
252
+ const token=tokens[index];
253
+ if(new Set([';', '{', '}']).has(token.value))break;
254
+ if(token.type==='identifier'&&token.value==='function'
255
+ &&!identifierIsProperty(tokens,index)){
256
+ const asyncToken=tokens[index-1];
257
+ const async=asyncToken?.type==='identifier'&&asyncToken.value==='async'
258
+ &&!identifierIsProperty(tokens,index-1);
259
+ return {
260
+ async,
261
+ declaration:declarationPosition(tokens,async?index-1:index),
262
+ generator:tokens.slice(index+1,openIndex).some(candidate=>candidate.value==='*')
263
+ };
264
+ }
265
+ }
266
+ if(enclosingBraceKind!=='class'&&enclosingBraceKind!=='object')return null;
267
+ const name=tokens[openIndex-1];
268
+ if(!name||!new Set(['identifier','string','number']).has(name.type))return null;
269
+ const beforeName=tokens[openIndex-2];
270
+ const async=beforeName?.type==='identifier'&&beforeName.value==='async';
271
+ const generator=beforeName?.value==='*'
272
+ ||(async&&tokens[openIndex-3]?.value==='*');
273
+ return {async,declaration:false,generator};
274
+ }
275
+
276
+ function arrowFunctionContext(tokens){
277
+ const arrow=tokens.at(-1);
278
+ if(arrow?.value!=='=>')return null;
279
+ const parameter=tokens.at(-2);
280
+ let async=false;
281
+ if(parameter?.value===')'&&Number.isInteger(parameter.openTokenIndex)){
282
+ const beforeOpen=tokens[parameter.openTokenIndex-1];
283
+ async=beforeOpen?.type==='identifier'&&beforeOpen.value==='async';
284
+ }else{
285
+ const beforeParameter=tokens.at(-3);
286
+ async=beforeParameter?.type==='identifier'&&beforeParameter.value==='async';
287
+ }
288
+ return {async,declaration:false,generator:false};
289
+ }
290
+
291
+ function openingBraceContext(tokens,braces){
292
+ const previous=tokens.at(-1);
293
+ const enclosingBraceKind=braces.at(-1)?.kind??null;
294
+ const arrow=arrowFunctionContext(tokens);
295
+ if(arrow){
296
+ return {kind:'function',functionContext:arrow,slashGoalAfter:'division'};
297
+ }
298
+ const header=functionHeader(tokens,previous,enclosingBraceKind);
299
+ if(header){
300
+ return {
301
+ kind:'function',
302
+ functionContext:header,
303
+ slashGoalAfter:header.declaration?'regex':'division'
304
+ };
305
+ }
306
+ const classContext=classHeader(tokens);
307
+ if(classContext){
308
+ return {
309
+ kind:'class',
310
+ functionContext:null,
311
+ slashGoalAfter:classContext.declaration?'regex':'division'
312
+ };
313
+ }
314
+ if(previous?.closesControl===true
315
+ ||(previous?.type==='identifier'
316
+ &&new Set(['do','else','finally','try']).has(previous.value))){
317
+ return {kind:'block',functionContext:null,slashGoalAfter:'regex'};
318
+ }
319
+ if(!previous||new Set([';','{']).has(previous.value)
320
+ ||previous.slashGoalAfter==='regex'){
321
+ return {kind:'block',functionContext:null,slashGoalAfter:'regex'};
322
+ }
323
+ if(new Set(['=', '(', '[', ',', '?']).has(previous.value)
324
+ ||(previous.type==='identifier'
325
+ &&new Set(['case','return','throw']).has(previous.value))){
326
+ return {kind:'object',functionContext:null,slashGoalAfter:'division'};
327
+ }
328
+ if(previous.value===':'){
329
+ if(enclosingBraceKind==='class'||enclosingBraceKind==='object'){
330
+ return {kind:'object',functionContext:null,slashGoalAfter:'division'};
331
+ }
332
+ return {kind:'ambiguous',functionContext:null,slashGoalAfter:'ambiguous'};
333
+ }
334
+ return {kind:'ambiguous',functionContext:null,slashGoalAfter:'ambiguous'};
335
+ }
336
+
337
+ function activeFunctionContext(braces){
338
+ for(let index=braces.length-1;index>=0;index-=1){
339
+ if(braces[index].kind==='function')return braces[index].functionContext;
340
+ }
341
+ return null;
342
+ }
343
+
344
+ function contextualForOf(tokens,parentheses){
345
+ const context=parentheses.at(-1);
346
+ if(context?.keyword!=='for'||context.sawSemicolon||context.sawOf)return false;
347
+ const previous=tokens.at(-1);
348
+ if(!previous||tokens.length-1<=context.openTokenIndex)return false;
349
+ if(previous.value==='.'||previous.value==='?.'||previous.value==='#')return false;
350
+ if(previous.type==='identifier'&&new Set(['const','let','var']).has(previous.value)){
351
+ return false;
352
+ }
353
+ return previous.type==='identifier'
354
+ ||new Set([']',')','}']).has(previous.value);
355
+ }
356
+
357
+ function functionParameterDeclaration(tokens,parentheses){
358
+ const context=parentheses.at(-1);
359
+ if(!Number.isInteger(context?.openTokenIndex))return false;
360
+ for(let index=context.openTokenIndex-1;index>=0;index-=1){
361
+ const token=tokens[index];
362
+ if(new Set([';','{','}']).has(token.value))return false;
363
+ if(token.type==='identifier'&&token.value==='function'
364
+ &&!identifierIsProperty(tokens,index))return true;
365
+ }
366
+ return false;
367
+ }
368
+
369
+ function laterVariableDeclarator(tokens){
370
+ if(tokens.at(-1)?.value!==',')return false;
371
+ for(let index=tokens.length-2;index>=0;index-=1){
372
+ const token=tokens[index];
373
+ if(tokens[index+1]?.lineBreakBefore===true)return false;
374
+ if(new Set([';','{','}']).has(token.value))return false;
375
+ if(token.type==='identifier'&&new Set(['const','let','var']).has(token.value)
376
+ &&!identifierIsProperty(tokens,index))return true;
377
+ }
378
+ return false;
379
+ }
380
+
381
+ function declaresContextualIdentifier(tokens,parentheses){
382
+ const previous=tokens.at(-1);
383
+ if(previous?.type==='identifier'
384
+ &&new Set(['const','let','var']).has(previous.value)
385
+ &&!identifierIsProperty(tokens,tokens.length-1))return true;
386
+ if(new Set(['{','[']).has(previous?.value)
387
+ &&tokens.at(-2)?.type==='identifier'
388
+ &&new Set(['const','let','var']).has(tokens.at(-2).value))return true;
389
+ return laterVariableDeclarator(tokens)||functionParameterDeclaration(tokens,parentheses);
390
+ }
391
+
392
+ function skipRegex(source,start){
393
+ let index=start+1;
394
+ let characterClass=false;
395
+ while(index<source.length){
396
+ const character=source[index];
397
+ if(LINE_TERMINATOR.test(character)){
398
+ fail(`Import-map scan found an unterminated regular expression at offset ${String(start)}.`);
399
+ }
400
+ if(character==='\\'){
401
+ index+=2;
402
+ continue;
403
+ }
404
+ if(character==='[')characterClass=true;
405
+ else if(character===']')characterClass=false;
406
+ else if(character==='/'&&!characterClass){
407
+ index+=1;
408
+ while(/[a-z]/u.test(source[index]??''))index+=1;
409
+ return index;
410
+ }
411
+ index+=1;
412
+ }
413
+ fail(`Import-map scan found an unterminated regular expression at offset ${String(start)}.`);
414
+ }
415
+
416
+ function tokenize(source){
417
+ const tokens=[];
418
+ let index=0;
419
+ let previous=null;
420
+ let braceDepth=0;
421
+ let lineTerminatorSinceToken=false;
422
+ const parentheses=[];
423
+ const braces=[];
424
+ const contextualBindings=new Set();
425
+ const templateExpressions=[];
426
+ while(index<source.length){
427
+ const character=source[index];
428
+ if(character==='#'&&source[index+1]==='!'
429
+ &&(index===0||(index===1&&source[0]==='\ufeff'))){
430
+ index+=2;
431
+ while(index<source.length&&!LINE_TERMINATOR.test(source[index]))index+=1;
432
+ continue;
433
+ }
434
+ if(character==='}'&&templateExpressions.at(-1)?.braceDepth===0){
435
+ const parsed=templateChunk(source,index+1);
436
+ const token={
437
+ type:'template',value:'`',start:index,end:parsed.next,braceDepth,
438
+ enclosingBraceKind:braces.at(-1)?.kind??null,
439
+ lineBreakBefore:lineTerminatorSinceToken
440
+ };
441
+ tokens.push(token);
442
+ if(parsed.complete)templateExpressions.pop();
443
+ previous=parsed.complete?token:null;
444
+ lineTerminatorSinceToken=false;
445
+ index=parsed.next;
446
+ continue;
447
+ }
448
+ if(/\s/u.test(character)){
449
+ if(LINE_TERMINATOR.test(character))lineTerminatorSinceToken=true;
450
+ index+=1;
451
+ continue;
452
+ }
453
+ if(character==='/'&&source[index+1]==='/'){
454
+ index+=2;
455
+ while(index<source.length&&!LINE_TERMINATOR.test(source[index]))index+=1;
456
+ continue;
457
+ }
458
+ if(character==='/'&&source[index+1]==='*'){
459
+ const close=source.indexOf('*/',index+2);
460
+ if(close<0)fail(`Import-map scan found an unterminated block comment at offset ${String(index)}.`);
461
+ if(LINE_TERMINATOR.test(source.slice(index+2,close))){
462
+ lineTerminatorSinceToken=true;
463
+ }
464
+ index=close+2;
465
+ continue;
466
+ }
467
+ if(character==='\''||character==='"'){
468
+ const parsed=stringToken(source,index);
469
+ parsed.token.braceDepth=braceDepth;
470
+ parsed.token.enclosingBraceKind=braces.at(-1)?.kind??null;
471
+ parsed.token.lineBreakBefore=lineTerminatorSinceToken;
472
+ tokens.push(parsed.token);
473
+ previous=parsed.token;
474
+ lineTerminatorSinceToken=false;
475
+ index=parsed.next;
476
+ continue;
477
+ }
478
+ if(character==='`'){
479
+ const parsed=templateChunk(source,index,{opening:true});
480
+ const token={
481
+ type:'template',value:'`',start:index,end:parsed.next,braceDepth,
482
+ enclosingBraceKind:braces.at(-1)?.kind??null,
483
+ lineBreakBefore:lineTerminatorSinceToken
484
+ };
485
+ tokens.push(token);
486
+ if(!parsed.complete)templateExpressions.push({braceDepth:0});
487
+ previous=parsed.complete?token:null;
488
+ lineTerminatorSinceToken=false;
489
+ index=parsed.next;
490
+ continue;
491
+ }
492
+ if(character==='/'&&regexMayStart(previous,{
493
+ beforePrevious:tokens.at(-2),
494
+ beforeBeforePrevious:tokens.at(-3),
495
+ lineTerminatorBefore:lineTerminatorSinceToken
496
+ })){
497
+ const end=skipRegex(source,index);
498
+ const token={
499
+ type:'regex',value:'/',start:index,end,braceDepth,
500
+ enclosingBraceKind:braces.at(-1)?.kind??null,
501
+ lineBreakBefore:lineTerminatorSinceToken
502
+ };
503
+ tokens.push(token);
504
+ previous=token;
505
+ lineTerminatorSinceToken=false;
506
+ index=end;
507
+ continue;
508
+ }
509
+ const identifierStart=sourceCharacter(source,index);
510
+ if(IDENTIFIER_START.test(identifierStart)){
511
+ let end=index+identifierStart.length;
512
+ while(end<source.length){
513
+ const continuation=sourceCharacter(source,end);
514
+ if(!IDENTIFIER_CONTINUE.test(continuation))break;
515
+ end+=continuation.length;
516
+ }
517
+ const token={
518
+ type:'identifier',value:source.slice(index,end),start:index,end,braceDepth,
519
+ enclosingBraceKind:braces.at(-1)?.kind??null,
520
+ lineBreakBefore:lineTerminatorSinceToken
521
+ };
522
+ const property=identifierIsProperty([...tokens,token],tokens.length);
523
+ if(!property&&previous?.type==='identifier'
524
+ &&new Set(['break','continue']).has(previous.value)
525
+ &&token.lineBreakBefore===false){
526
+ token.restrictedStatementLabel=true;
527
+ token.restrictedStatementKind=previous.value;
528
+ }
529
+ if(!property&&new Set(['await','yield']).has(token.value)
530
+ &&declaresContextualIdentifier(tokens,parentheses)){
531
+ contextualBindings.add(token.value);
532
+ token.slashGoalAfter='division';
533
+ }else if(!property&&token.value==='await'){
534
+ const functionContext=activeFunctionContext(braces);
535
+ if(functionContext?.async)token.contextualRegexPrefix=true;
536
+ else if(functionContext||contextualBindings.has('await')){
537
+ token.slashGoalAfter='division';
538
+ }else token.slashGoalAfter='ambiguous';
539
+ }else if(!property&&token.value==='yield'){
540
+ const functionContext=activeFunctionContext(braces);
541
+ if(functionContext?.generator)token.contextualRegexPrefix=true;
542
+ else if(functionContext||contextualBindings.has('yield')){
543
+ token.slashGoalAfter='division';
544
+ }else token.slashGoalAfter='ambiguous';
545
+ }else if(!property&&token.value==='of'&&contextualForOf(tokens,parentheses)){
546
+ token.contextualRegexPrefix=true;
547
+ parentheses.at(-1).sawOf=true;
548
+ }
549
+ tokens.push(token);
550
+ previous=token;
551
+ lineTerminatorSinceToken=false;
552
+ index=end;
553
+ continue;
554
+ }
555
+ if(character==='\\'){
556
+ fail(
557
+ `Import-map scan found an escaped JavaScript identifier at offset ${String(index)}. `
558
+ +'Escaped identifiers are outside the deterministic import scanner subset.'
559
+ );
560
+ }
561
+ if(/[0-9]/u.test(character)){
562
+ let end=index+1;
563
+ while(/[A-Za-z0-9_.]/u.test(source[end]??''))end+=1;
564
+ const token={
565
+ type:'number',value:source.slice(index,end),start:index,end,braceDepth,
566
+ enclosingBraceKind:braces.at(-1)?.kind??null,
567
+ lineBreakBefore:lineTerminatorSinceToken
568
+ };
569
+ tokens.push(token);
570
+ previous=token;
571
+ lineTerminatorSinceToken=false;
572
+ index=end;
573
+ continue;
574
+ }
575
+ const three=source.slice(index,index+3);
576
+ const two=source.slice(index,index+2);
577
+ const value=three==='...'?three:
578
+ new Set(['=>','?.','++','--','&&','||','??','==','!=','<=','>=','**']).has(two)
579
+ ?two:character;
580
+ const token={
581
+ type:'punctuator',value,start:index,end:index+value.length,braceDepth,
582
+ enclosingBraceKind:braces.at(-1)?.kind??null,
583
+ lineBreakBefore:lineTerminatorSinceToken
584
+ };
585
+ if(value==='('){
586
+ const directKeyword=previous?.type==='identifier'
587
+ &&!identifierIsProperty(tokens,tokens.length-1)
588
+ &&new Set(['catch','for','if','switch','while','with']).has(previous.value)
589
+ ?previous.value:null;
590
+ const keyword=directKeyword??(
591
+ previous?.type==='identifier'&&previous.value==='await'
592
+ &&tokens.at(-2)?.type==='identifier'&&tokens.at(-2).value==='for'
593
+ ?'for':null
594
+ );
595
+ parentheses.push({
596
+ keyword,
597
+ openTokenIndex:tokens.length,
598
+ sawOf:false,
599
+ sawSemicolon:false
600
+ });
601
+ }else if(value===')'){
602
+ const context=parentheses.pop();
603
+ token.closesControl=context?.keyword!=null;
604
+ token.openTokenIndex=context?.openTokenIndex;
605
+ }else if(value===';'&&parentheses.length>0){
606
+ parentheses.at(-1).sawSemicolon=true;
607
+ }
608
+ if(templateExpressions.length>0){
609
+ if(value==='{')templateExpressions.at(-1).braceDepth+=1;
610
+ else if(value==='}'&&templateExpressions.at(-1).braceDepth>0){
611
+ templateExpressions.at(-1).braceDepth-=1;
612
+ }
613
+ }
614
+ if(value==='{'){
615
+ const context=openingBraceContext(tokens,braces);
616
+ context.openTokenIndex=tokens.length;
617
+ token.openingBraceKind=context.kind;
618
+ braces.push(context);
619
+ braceDepth+=1;
620
+ }else if(value==='}'){
621
+ const context=braces.pop()??{
622
+ kind:'ambiguous',functionContext:null,slashGoalAfter:'ambiguous'
623
+ };
624
+ token.enclosingBraceKind=context.kind;
625
+ token.closedBraceKind=context.kind;
626
+ token.slashGoalAfter=context.slashGoalAfter;
627
+ if(braceDepth>0)braceDepth-=1;
628
+ }
629
+ tokens.push(token);
630
+ previous=token;
631
+ lineTerminatorSinceToken=false;
632
+ index=token.end;
633
+ }
634
+ return tokens;
635
+ }
636
+
637
+ function matchingToken(tokens,start,opening,closing){
638
+ let depth=0;
639
+ for(let index=start;index<tokens.length;index+=1){
640
+ if(tokens[index].value===opening)depth+=1;
641
+ else if(tokens[index].value===closing){
642
+ depth-=1;
643
+ if(depth===0)return index;
644
+ }
645
+ }
646
+ return -1;
647
+ }
648
+
649
+ function importIsMethodDefinition(tokens,index){
650
+ const current=tokens[index];
651
+ if(!new Set(['class','object']).has(current?.enclosingBraceKind)
652
+ ||tokens[index+1]?.value!=='(')return false;
653
+ const close=matchingToken(tokens,index+1,'(',')');
654
+ if(close<0||tokens[close+1]?.value!=='{')return false;
655
+ let cursor=index-1;
656
+ if(tokens[cursor]?.value==='#')cursor-=1;
657
+ if(tokens[cursor]?.value==='*')cursor-=1;
658
+ while(tokens[cursor]?.type==='identifier'
659
+ &&new Set(['async','get','set','static']).has(tokens[cursor].value))cursor-=1;
660
+ return new Set(['{','}',',',';']).has(tokens[cursor]?.value);
661
+ }
662
+
663
+ function sourceAfterFrom(tokens,start,end){
664
+ for(let index=start;index<end;index+=1){
665
+ if(tokens[index].type==='identifier'&&tokens[index].value==='from'
666
+ &&tokens[index+1]?.type==='string'){
667
+ return tokens[index+1];
668
+ }
669
+ }
670
+ return null;
671
+ }
672
+
673
+ function statementEnd(tokens,start){
674
+ const startDepth=tokens[start]?.braceDepth??0;
675
+ for(let index=start;index<tokens.length;index+=1){
676
+ if(tokens[index].value===';'&&tokens[index].braceDepth===startDepth)return index;
677
+ }
678
+ return tokens.length;
679
+ }
680
+
681
+ function topLevelCommas(tokens,start,end){
682
+ const commas=[];
683
+ const depths={parenthesis:0,bracket:0,brace:0};
684
+ for(let index=start;index<end;index+=1){
685
+ const value=tokens[index].value;
686
+ if(value==='(')depths.parenthesis+=1;
687
+ else if(value===')'&&depths.parenthesis>0)depths.parenthesis-=1;
688
+ else if(value==='[')depths.bracket+=1;
689
+ else if(value===']'&&depths.bracket>0)depths.bracket-=1;
690
+ else if(value==='{')depths.brace+=1;
691
+ else if(value==='}'&&depths.brace>0)depths.brace-=1;
692
+ else if(value===','&&depths.parenthesis===0&&depths.bracket===0&&depths.brace===0){
693
+ commas.push(index);
694
+ }
695
+ }
696
+ return commas;
697
+ }
698
+
699
+ function importRecord(kind,token){
700
+ return Object.freeze({kind,specifier:token.value,offset:token.start});
701
+ }
702
+
703
+ function nonliteralDynamic(importer,offset){
704
+ fail(
705
+ `Import-map scan found a nonliteral dynamic import in "${importer}" at offset ${String(offset)}. `
706
+ +'Replace import(expression) with a literal shipped specifier, then rerun arcane import-map.',
707
+ 'ARCANE_IMPORT_MAP_UNRESOLVED'
708
+ );
709
+ }
710
+
711
+ export function scanModuleImports(source,{importer='<module>'}={}){
712
+ if(typeof source!=='string')throw new TypeError('scanModuleImports source must be a string.');
713
+ const tokens=tokenize(source);
714
+ const imports=[];
715
+ let hasModuleSyntax=false;
716
+ for(let index=0;index<tokens.length;index+=1){
717
+ const current=tokens[index];
718
+ if(current.type!=='identifier'||(current.value!=='import'&&current.value!=='export')
719
+ ||identifierIsProperty(tokens,index))continue;
720
+ const next=tokens[index+1];
721
+ if(current.value==='import'){
722
+ if(next?.value==='.'){
723
+ hasModuleSyntax=true;
724
+ continue;
725
+ }
726
+ if(next?.value==='('){
727
+ if(importIsMethodDefinition(tokens,index))continue;
728
+ const close=matchingToken(tokens,index+1,'(',')');
729
+ if(close<0)nonliteralDynamic(importer,current.start);
730
+ const argument=tokens[index+2];
731
+ const commas=topLevelCommas(tokens,index+2,close);
732
+ const firstBoundary=commas[0]??close;
733
+ if(argument?.type!=='string'||firstBoundary!==index+3
734
+ ||commas.length>2
735
+ ||(commas.length===2
736
+ &&(commas[1]!==close-1||commas[1]===commas[0]+1))){
737
+ nonliteralDynamic(importer,current.start);
738
+ }
739
+ hasModuleSyntax=true;
740
+ imports.push(importRecord('dynamic',argument));
741
+ continue;
742
+ }
743
+ if(current.braceDepth!==0)continue;
744
+ if(next?.type==='string'){
745
+ hasModuleSyntax=true;
746
+ imports.push(importRecord('static',next));
747
+ continue;
748
+ }
749
+ const end=statementEnd(tokens,index+1);
750
+ const sourceToken=sourceAfterFrom(tokens,index+1,end);
751
+ if(!sourceToken){
752
+ fail(`Import-map scan found an import without a literal source in "${importer}".`);
753
+ }
754
+ hasModuleSyntax=true;
755
+ imports.push(importRecord('static',sourceToken));
756
+ continue;
757
+ }
758
+ if(current.braceDepth!==0)continue;
759
+ hasModuleSyntax=true;
760
+ if(next?.value==='*'){
761
+ const end=statementEnd(tokens,index+1);
762
+ const sourceToken=sourceAfterFrom(tokens,index+2,end);
763
+ if(!sourceToken){
764
+ fail(`Import-map scan found an export without a literal source in "${importer}".`);
765
+ }
766
+ imports.push(importRecord('export',sourceToken));
767
+ }else if(next?.value==='{'){
768
+ const cursor=matchingToken(tokens,index+1,'{','}');
769
+ if(cursor>=0&&tokens[cursor+1]?.type==='identifier'
770
+ &&tokens[cursor+1].value==='from'){
771
+ const sourceToken=tokens[cursor+2];
772
+ if(sourceToken?.type!=='string'){
773
+ fail(`Import-map scan found an export without a literal source in "${importer}".`);
774
+ }
775
+ imports.push(importRecord('export',sourceToken));
776
+ }
777
+ }
778
+ }
779
+ return Object.freeze({
780
+ hasModuleSyntax,
781
+ imports:Object.freeze(imports)
782
+ });
783
+ }
784
+
785
+ function stripQueryAndHash(specifier){
786
+ const query=specifier.indexOf('?');
787
+ const hash=specifier.indexOf('#');
788
+ const end=Math.min(query<0?specifier.length:query,hash<0?specifier.length:hash);
789
+ return specifier.slice(0,end);
790
+ }
791
+
792
+ function unresolved(importer,specifier,normalizedTarget,reason='is not in the shipped workspace runtime'){
793
+ fail(
794
+ `Import-map scan could not resolve "${specifier}" imported by "${importer}". `
795
+ +`Normalized target: "${normalizedTarget}" ${reason}. `
796
+ +'Materialize the authenticated dependency beneath workspace arcane/ or update the import '
797
+ +'to a shipped JavaScript file, then rerun arcane import-map.',
798
+ 'ARCANE_IMPORT_MAP_UNRESOLVED'
799
+ );
800
+ }
801
+
802
+ function resolveImport(importer,specifier,files){
803
+ const reachableSpecifier=stripQueryAndHash(specifier);
804
+ if(!reachableSpecifier)unresolved(importer,specifier,'<empty>');
805
+ if(reachableSpecifier.includes('%')){
806
+ unresolved(
807
+ importer,
808
+ specifier,
809
+ reachableSpecifier,
810
+ 'contains percent-encoded path bytes whose browser URL normalization is outside the '
811
+ +'deterministic shipped-runtime subset'
812
+ );
813
+ }
814
+ if(reachableSpecifier===RUNTIME_STRONG_TYPE_IMPORT){
815
+ const target='dependencies/strong-type/index.js';
816
+ if(reachableSpecifier!==specifier){
817
+ unresolved(
818
+ importer,
819
+ specifier,
820
+ target,
821
+ 'uses a query or fragment that cannot match its exact browser import-map key'
822
+ );
823
+ }
824
+ if(!files.has(target))unresolved(importer,specifier,target);
825
+ return {target,runtimeStrongType:true};
826
+ }
827
+ if(reachableSpecifier==='event-pubsub'){
828
+ const target='sdk/dependencies/event-pubsub/index.js';
829
+ if(reachableSpecifier!==specifier){
830
+ unresolved(
831
+ importer,
832
+ specifier,
833
+ target,
834
+ 'uses a query or fragment that cannot match its exact browser import-map key'
835
+ );
836
+ }
837
+ if(!files.has(target))unresolved(importer,specifier,target);
838
+ return {target,eventPubSub:true};
839
+ }
840
+ if(/[\u0000-\u0020\u007f\\]/u.test(reachableSpecifier)||reachableSpecifier.includes('//')){
841
+ unresolved(
842
+ importer,
843
+ specifier,
844
+ reachableSpecifier,
845
+ 'contains browser-preprocessed control/space/backslash bytes or an empty path segment'
846
+ );
847
+ }
848
+ if(!reachableSpecifier.startsWith('./')&&!reachableSpecifier.startsWith('../')){
849
+ unresolved(importer,specifier,reachableSpecifier,'is not a supported shipped bare specifier');
850
+ }
851
+ const runtimePrefix='/__arcane_runtime__/';
852
+ const runtimeOrigin='https://arcane.invalid';
853
+ let resolved;
854
+ try{
855
+ resolved=new URL(
856
+ reachableSpecifier,
857
+ `${runtimeOrigin}${runtimePrefix}${importer}`
858
+ );
859
+ }catch{
860
+ unresolved(importer,specifier,reachableSpecifier,'is not a valid browser-relative URL');
861
+ }
862
+ if(resolved.origin!==runtimeOrigin||!resolved.pathname.startsWith(runtimePrefix)){
863
+ unresolved(importer,specifier,resolved.pathname,'escapes the shipped workspace runtime');
864
+ }
865
+ let target;
866
+ try{target=decodeURIComponent(resolved.pathname.slice(runtimePrefix.length));}
867
+ catch{
868
+ unresolved(importer,specifier,resolved.pathname,'does not have a deterministic decoded URL path');
869
+ }
870
+ if(target==='.'||target.startsWith('../')||path.posix.isAbsolute(target)){
871
+ unresolved(importer,specifier,target,'escapes the shipped workspace runtime');
872
+ }
873
+ if(!files.has(target))unresolved(importer,specifier,target);
874
+ if(!JAVASCRIPT_EXTENSION.test(target)){
875
+ unresolved(importer,specifier,target,'is not a JavaScript module');
876
+ }
877
+ return {target,strongType:false};
878
+ }
879
+
880
+ function registerSpecifier(registry,specifier,target){
881
+ const key=collisionKey(specifier);
882
+ const existing=registry.get(key);
883
+ if(existing&&existing.specifier!==specifier||existing&&existing.target!==target){
884
+ fail(
885
+ `Import-map specifier collision: "${specifier}" (${target}) and `
886
+ +`"${existing.specifier}" (${existing.target}) normalize to the same case/NFC key. `
887
+ +'Rename one shipped module so every extensionless named specifier is unique.',
888
+ 'ARCANE_IMPORT_MAP_COLLISION'
889
+ );
890
+ }
891
+ registry.set(key,{specifier,target});
892
+ }
893
+
894
+ function validateInventory(files){
895
+ if(!Array.isArray(files))throw new TypeError('buildImportMap files must be an array.');
896
+ const exact=new Set();
897
+ const normalized=new Map();
898
+ for(const value of [...files].sort(compareUtf8)){
899
+ const relative=safeRelativePath(value,'runtime inventory path');
900
+ if(/[%?#\u0000-\u0020\u007f]/u.test(relative)||relative.includes('//')){
901
+ fail(
902
+ `Import-map runtime inventory path is not browser-URL-safe: ${relative}. `
903
+ +'Percent/delimiter bytes, control/space bytes, and empty path segments are not '
904
+ +'allowed in authenticated runtime filenames.'
905
+ );
906
+ }
907
+ if(exact.has(relative))fail(`Import-map runtime inventory repeats ${relative}.`);
908
+ exact.add(relative);
909
+ const key=collisionKey(relative);
910
+ const prior=normalized.get(key);
911
+ if(prior&&prior!==relative){
912
+ fail(
913
+ `Import-map runtime path collision: "${prior}" and "${relative}" normalize to `
914
+ +'the same case/NFC path. Rename one shipped file before regenerating the map.',
915
+ 'ARCANE_IMPORT_MAP_COLLISION'
916
+ );
917
+ }
918
+ normalized.set(key,relative);
919
+ }
920
+ return exact;
921
+ }
922
+
923
+ export async function buildImportMap({files,readFile,signal}={}){
924
+ if(typeof readFile!=='function')throw new TypeError('buildImportMap readFile must be a function.');
925
+ throwIfAborted(signal);
926
+ const inventory=validateInventory(files);
927
+ const candidates=[...inventory]
928
+ .filter(relative=>relative.startsWith('modules/')
929
+ &&!relative.slice('modules/'.length).includes('/')
930
+ &&JAVASCRIPT_EXTENSION.test(relative))
931
+ .sort(compareUtf8);
932
+ const scans=new Map();
933
+ async function scan(relative){
934
+ throwIfAborted(signal);
935
+ if(scans.has(relative))return scans.get(relative);
936
+ const bytes=await readFile(relative);
937
+ throwIfAborted(signal);
938
+ const source=Buffer.isBuffer(bytes)||bytes instanceof Uint8Array
939
+ ?Buffer.from(bytes).toString('utf8'):String(bytes);
940
+ const result=scanModuleImports(source,{importer:relative});
941
+ scans.set(relative,result);
942
+ return result;
943
+ }
944
+
945
+ const roots=[];
946
+ const excludedModules=[];
947
+ for(const relative of candidates){
948
+ const result=await scan(relative);
949
+ if(!result.hasModuleSyntax)continue;
950
+ if(relative===NODE_ONLY_MODULE){
951
+ excludedModules.push(relative);
952
+ continue;
953
+ }
954
+ roots.push(relative);
955
+ }
956
+ const hasSdkBrowserGraph=inventory.has(SDK_BROWSER_ENTRY);
957
+ if(hasSdkBrowserGraph){
958
+ for(const required of SDK_BROWSER_FILES){
959
+ if(!inventory.has(required)){
960
+ unresolved(SDK_BROWSER_ENTRY,'<authenticated SDK browser closure>',required);
961
+ }
962
+ }
963
+ for(const [packagePath,expectedName,expectedVersion] of [
964
+ ['dependencies/strong-type/package.json','strong-type','1.1.0'],
965
+ ['sdk/dependencies/event-pubsub/package.json','event-pubsub','6.1.0'],
966
+ ['sdk/dependencies/strong-type/package.json','strong-type','2.0.0']
967
+ ]){
968
+ if(!inventory.has(packagePath)){
969
+ unresolved(SDK_BROWSER_ENTRY,'<authenticated dependency identity>',packagePath);
970
+ }
971
+ let document;
972
+ try{document=JSON.parse(Buffer.from(await readFile(packagePath)).toString('utf8'));}
973
+ catch{
974
+ unresolved(SDK_BROWSER_ENTRY,'<authenticated dependency identity>',packagePath,'is not valid package JSON');
975
+ }
976
+ if(document?.name!==expectedName||document?.version!==expectedVersion){
977
+ unresolved(
978
+ SDK_BROWSER_ENTRY,
979
+ '<authenticated dependency identity>',
980
+ packagePath,
981
+ `must identify exactly as ${expectedName}@${expectedVersion}`
982
+ );
983
+ }
984
+ }
985
+ }
986
+
987
+ const namedRegistry=new Map();
988
+ for(const relative of roots){
989
+ const name=path.posix.basename(relative).replace(JAVASCRIPT_EXTENSION,'');
990
+ registerSpecifier(namedRegistry,`arcane/${name}`,`./arcane/${relative}`);
991
+ }
992
+ if(hasSdkBrowserGraph){
993
+ registerSpecifier(
994
+ namedRegistry,
995
+ 'arcane-os/event-manager',
996
+ './arcane/sdk/event-manager.mjs'
997
+ );
998
+ }
999
+
1000
+ const queue=[...roots,...(hasSdkBrowserGraph?[SDK_BROWSER_ENTRY]:[])];
1001
+ const reached=new Set();
1002
+ const entities=new Set();
1003
+ let usesRuntimeStrongType=false;
1004
+ let usesEventPubSub=false;
1005
+ while(queue.length>0){
1006
+ throwIfAborted(signal);
1007
+ const importer=queue.shift();
1008
+ if(reached.has(importer))continue;
1009
+ reached.add(importer);
1010
+ const result=await scan(importer);
1011
+ for(const imported of result.imports){
1012
+ const resolution=resolveImport(importer,imported.specifier,inventory);
1013
+ if(resolution.runtimeStrongType)usesRuntimeStrongType=true;
1014
+ if(resolution.eventPubSub)usesEventPubSub=true;
1015
+ if(resolution.target.startsWith('entities/'))entities.add(resolution.target);
1016
+ if(!reached.has(resolution.target))queue.push(resolution.target);
1017
+ }
1018
+ }
1019
+
1020
+ for(const relative of [...entities].sort(compareUtf8)){
1021
+ const name=path.posix.basename(relative).replace(JAVASCRIPT_EXTENSION,'');
1022
+ registerSpecifier(namedRegistry,`arcane/entities/${name}`,`./arcane/${relative}`);
1023
+ }
1024
+ if(usesRuntimeStrongType){
1025
+ registerSpecifier(
1026
+ namedRegistry,
1027
+ './node_modules/strong-type/index.js',
1028
+ './arcane/dependencies/strong-type/index.js'
1029
+ );
1030
+ }
1031
+ if(usesEventPubSub){
1032
+ registerSpecifier(
1033
+ namedRegistry,
1034
+ 'event-pubsub',
1035
+ './arcane/sdk/dependencies/event-pubsub/index.js'
1036
+ );
1037
+ }
1038
+ const imports={};
1039
+ for(const entry of [...namedRegistry.values()].sort((left,right)=>compareUtf8(left.specifier,right.specifier))){
1040
+ imports[entry.specifier]=entry.target;
1041
+ }
1042
+ return Object.freeze({
1043
+ imports:Object.freeze(imports),
1044
+ entryCount:Object.keys(imports).length,
1045
+ excludedModules:Object.freeze(excludedModules.sort(compareUtf8)),
1046
+ reachedFiles:Object.freeze([...reached].sort(compareUtf8))
1047
+ });
1048
+ }
1049
+
1050
+ function sameFileIdentity(left,right){
1051
+ return left.dev===right.dev&&left.ino===right.ino&&left.size===right.size
1052
+ &&left.mtimeNs===right.mtimeNs&&left.ctimeNs===right.ctimeNs
1053
+ &&left.nlink===right.nlink;
1054
+ }
1055
+
1056
+ function sameFileLocation(left,right){
1057
+ return left.dev===right.dev&&left.ino===right.ino;
1058
+ }
1059
+
1060
+ function sha256(bytes){
1061
+ return createHash('sha256').update(bytes).digest('hex');
1062
+ }
1063
+
1064
+ function pathInside(root,target){
1065
+ const relative=path.relative(root,target);
1066
+ return relative===''||(!relative.startsWith('..')&&!path.isAbsolute(relative));
1067
+ }
1068
+
1069
+ async function physicalRuntime(workspaceRoot,signal){
1070
+ const requestedRoot=path.join(workspaceRoot,'arcane');
1071
+ let rootInfo;
1072
+ try{rootInfo=await lstat(requestedRoot);}
1073
+ catch(error){
1074
+ if(error?.code==='ENOENT')fail(`Workspace Arcane runtime is missing: ${requestedRoot}.`);
1075
+ throw error;
1076
+ }
1077
+ if(rootInfo.isSymbolicLink()||!rootInfo.isDirectory()){
1078
+ fail('Workspace Arcane runtime must be a real directory, not a symbolic link or junction.');
1079
+ }
1080
+ const canonicalRoot=await realpath(requestedRoot);
1081
+ const files=[];
1082
+ async function visit(directory,relativeRoot=''){
1083
+ throwIfAborted(signal);
1084
+ const entries=await readdir(directory,{withFileTypes:true});
1085
+ entries.sort((left,right)=>compareUtf8(left.name,right.name));
1086
+ for(const entry of entries){
1087
+ throwIfAborted(signal);
1088
+ const relative=relativeRoot?`${relativeRoot}/${entry.name}`:entry.name;
1089
+ const absolute=path.join(directory,entry.name);
1090
+ const info=await lstat(absolute);
1091
+ if(info.isSymbolicLink()){
1092
+ fail(`Workspace Arcane runtime contains a symbolic link or junction: ${relative}.`);
1093
+ }
1094
+ if(info.isDirectory())await visit(absolute,relative);
1095
+ else if(info.isFile())files.push(relative);
1096
+ else fail(`Workspace Arcane runtime contains a non-file entry: ${relative}.`);
1097
+ }
1098
+ }
1099
+ await visit(canonicalRoot);
1100
+ return {
1101
+ files,
1102
+ async readFile(relative){
1103
+ throwIfAborted(signal);
1104
+ safeRelativePath(relative,'runtime read path');
1105
+ const absolute=path.resolve(canonicalRoot,...relative.split('/'));
1106
+ if(!pathInside(canonicalRoot,absolute))fail(`Import-map runtime read escapes arcane/: ${relative}.`);
1107
+ const before=await lstat(absolute,{bigint:true});
1108
+ if(before.isSymbolicLink()||!before.isFile()){
1109
+ fail(`Workspace Arcane runtime module is not a real file: ${relative}.`);
1110
+ }
1111
+ let handle;
1112
+ try{handle=await open(absolute,READ_ONLY_NO_FOLLOW);}
1113
+ catch(error){
1114
+ if(error?.code==='ELOOP')fail(`Workspace Arcane runtime module became a symlink: ${relative}.`);
1115
+ throw error;
1116
+ }
1117
+ try{
1118
+ const opened=await handle.stat({bigint:true});
1119
+ if(!sameFileIdentity(before,opened)){
1120
+ fail(`Workspace Arcane runtime module changed while opening: ${relative}.`);
1121
+ }
1122
+ const bytes=await handle.readFile();
1123
+ const after=await handle.stat({bigint:true});
1124
+ if(!sameFileIdentity(opened,after)){
1125
+ fail(`Workspace Arcane runtime module changed while reading: ${relative}.`);
1126
+ }
1127
+ const canonicalFile=await realpath(absolute);
1128
+ if(!pathInside(canonicalRoot,canonicalFile)){
1129
+ fail(`Workspace Arcane runtime module left its root: ${relative}.`);
1130
+ }
1131
+ return bytes;
1132
+ }finally{
1133
+ await handle.close();
1134
+ }
1135
+ }
1136
+ };
1137
+ }
1138
+
1139
+ function asciiLower(value){
1140
+ return String(value).replace(/[A-Z]/g,character=>
1141
+ String.fromCodePoint(character.codePointAt(0)+0x20));
1142
+ }
1143
+
1144
+ function trimHtmlAsciiWhitespace(value){
1145
+ return String(value).replace(/^[\t\n\f\r ]+|[\t\n\f\r ]+$/gu,'');
1146
+ }
1147
+
1148
+ function canonicalHtmlToken(value){
1149
+ return asciiLower(trimHtmlAsciiWhitespace(value));
1150
+ }
1151
+
1152
+ function htmlTagName(value){
1153
+ if(/[\u0000\p{White_Space}]/u.test(value)){
1154
+ fail('Application HTML contains a malformed structural tag name.');
1155
+ }
1156
+ return asciiLower(value);
1157
+ }
1158
+
1159
+ function parseTagAttributes(openTag){
1160
+ const attributes=new Map();
1161
+ const duplicates=new Set();
1162
+ Object.defineProperty(attributes,'duplicates',{value:duplicates});
1163
+ const tagHead=openTag.match(/^<[A-Za-z][^\t\n\f\r />]*(?=[\t\n\f\r />])/u);
1164
+ if(!tagHead)fail('Application HTML contains a malformed structural start tag.');
1165
+ let index=tagHead[0].length;
1166
+ while(index<openTag.length){
1167
+ while(/[\t\n\f\r ]/u.test(openTag[index]??''))index+=1;
1168
+ if(openTag[index]==='>'||index>=openTag.length)break;
1169
+ if(openTag[index]==='/'){
1170
+ index+=1;
1171
+ if(openTag[index]==='>')break;
1172
+ fail('Application HTML contains a nonterminal self-closing slash in a structural tag.');
1173
+ }
1174
+ const start=index;
1175
+ while(index<openTag.length&&!/[\t\n\f\r =>/]/u.test(openTag[index]))index+=1;
1176
+ const name=asciiLower(openTag.slice(start,index));
1177
+ while(/[\t\n\f\r ]/u.test(openTag[index]??''))index+=1;
1178
+ let value='';
1179
+ if(openTag[index]==='='){
1180
+ index+=1;
1181
+ while(/[\t\n\f\r ]/u.test(openTag[index]??''))index+=1;
1182
+ const quote=openTag[index];
1183
+ if(quote==='\''||quote==='"'){
1184
+ index+=1;
1185
+ const valueStart=index;
1186
+ while(index<openTag.length&&openTag[index]!==quote)index+=1;
1187
+ value=openTag.slice(valueStart,index);
1188
+ if(openTag[index]===quote)index+=1;
1189
+ }else{
1190
+ const valueStart=index;
1191
+ while(index<openTag.length&&!/[\t\n\f\r >]/u.test(openTag[index]))index+=1;
1192
+ value=openTag.slice(valueStart,index);
1193
+ }
1194
+ }
1195
+ if(name){
1196
+ if(attributes.has(name))duplicates.add(name);
1197
+ else attributes.set(name,value);
1198
+ }
1199
+ }
1200
+ return attributes;
1201
+ }
1202
+
1203
+ function decodeStructuralAttribute(value,label){
1204
+ const source=String(value);
1205
+ if(!source.includes('&'))return source;
1206
+ const decoded=source.replace(
1207
+ /&(?:#([0-9]+)|#x([a-f0-9]+)|(amp|apos|gt|lt|quot));/giu,
1208
+ (match,decimal,hex,named)=>{
1209
+ if(named){
1210
+ return {amp:'&',apos:"'",gt:'>',lt:'<',quot:'"'}[asciiLower(named)];
1211
+ }
1212
+ const point=Number.parseInt(decimal??hex,decimal?10:16);
1213
+ if(!Number.isSafeInteger(point)||point<=0||point>0x10ffff
1214
+ ||(point>=0xd800&&point<=0xdfff)){
1215
+ fail(`Application HTML ${label} contains an invalid character reference.`);
1216
+ }
1217
+ return String.fromCodePoint(point);
1218
+ }
1219
+ );
1220
+ if(decoded.includes('&')){
1221
+ fail(
1222
+ `Application HTML ${label} contains an unsupported or ambiguous character reference.`
1223
+ );
1224
+ }
1225
+ return decoded;
1226
+ }
1227
+
1228
+ function structuralAttribute(attributes,name,element){
1229
+ if(attributes.duplicates.has(name)){
1230
+ fail(`Application HTML ${element} repeats its ${name} attribute.`);
1231
+ }
1232
+ return decodeStructuralAttribute(attributes.get(name)??'',`${element} ${name}`);
1233
+ }
1234
+
1235
+ function scriptType(attributes){
1236
+ return canonicalHtmlToken(structuralAttribute(attributes,'type','script'));
1237
+ }
1238
+
1239
+ function htmlTagEnd(html,start){
1240
+ let state='before-attribute-name';
1241
+ let quote=null;
1242
+ for(let index=start;index<html.length;index+=1){
1243
+ const character=html[index];
1244
+ if(state==='quoted-attribute-value'){
1245
+ if(character===quote)quote=null;
1246
+ if(quote===null)state='after-quoted-attribute-value';
1247
+ continue;
1248
+ }
1249
+ if(character==='>')return index+1;
1250
+ const whitespace=/[\t\n\f\r ]/u.test(character);
1251
+ if(state==='before-attribute-name'){
1252
+ if(whitespace)continue;
1253
+ state=character==='/'?'self-closing-start-tag':'attribute-name';
1254
+ continue;
1255
+ }
1256
+ if(state==='attribute-name'){
1257
+ if(whitespace)state='after-attribute-name';
1258
+ else if(character==='=')state='before-attribute-value';
1259
+ else if(character==='/')state='self-closing-start-tag';
1260
+ continue;
1261
+ }
1262
+ if(state==='after-attribute-name'){
1263
+ if(whitespace)continue;
1264
+ if(character==='=')state='before-attribute-value';
1265
+ else if(character==='/')state='self-closing-start-tag';
1266
+ else state='attribute-name';
1267
+ continue;
1268
+ }
1269
+ if(state==='before-attribute-value'){
1270
+ if(whitespace)continue;
1271
+ if(character==='\''||character==='"'){
1272
+ quote=character;
1273
+ state='quoted-attribute-value';
1274
+ }else state='unquoted-attribute-value';
1275
+ continue;
1276
+ }
1277
+ if(state==='unquoted-attribute-value'){
1278
+ if(whitespace)state='before-attribute-name';
1279
+ continue;
1280
+ }
1281
+ if(state==='after-quoted-attribute-value'){
1282
+ if(whitespace)state='before-attribute-name';
1283
+ else if(character==='/')state='self-closing-start-tag';
1284
+ else state='attribute-name';
1285
+ continue;
1286
+ }
1287
+ if(state==='self-closing-start-tag'){
1288
+ if(whitespace)state='before-attribute-name';
1289
+ else state='attribute-name';
1290
+ }
1291
+ }
1292
+ fail('Application HTML contains a tag that reaches end of file before ">".');
1293
+ }
1294
+
1295
+ const RAW_TEXT_ELEMENTS=new Set(['iframe','noembed','noframes','script','style','xmp']);
1296
+ const RCDATA_ELEMENTS=new Set(['textarea','title']);
1297
+ const TEXT_ELEMENTS=new Set([...RAW_TEXT_ELEMENTS,...RCDATA_ELEMENTS]);
1298
+
1299
+ function rawElementEnd(html,tag,openEnd){
1300
+ const closePattern=new RegExp(`<\\/${tag}(?=[\\t\\n\\f\\r />]|$)`,'gi');
1301
+ closePattern.lastIndex=openEnd;
1302
+ const close=closePattern.exec(html);
1303
+ if(tag==='script'){
1304
+ const escapedStart=html.indexOf('<!--',openEnd);
1305
+ if(escapedStart>=0&&(!close||escapedStart<close.index)){
1306
+ fail(
1307
+ 'Application HTML contains legacy escaped script syntax, which is outside the '
1308
+ +'deterministic import-map HTML subset.'
1309
+ );
1310
+ }
1311
+ }
1312
+ if(!close)return {end:html.length,closed:false};
1313
+ const end=htmlTagEnd(html,close.index+close[0].length);
1314
+ const closeTag=html.slice(close.index,end);
1315
+ if(!new RegExp(`^<\\/${tag}[\\t\\n\\f\\r ]*>$`,'i').test(closeTag)){
1316
+ fail(`Application HTML contains a malformed </${tag}> end tag.`);
1317
+ }
1318
+ return {end,closed:true};
1319
+ }
1320
+
1321
+ function commentEnd(html,start){
1322
+ if(html.startsWith('<!-->',start)||html.startsWith('<!--->',start)){
1323
+ fail('Application HTML contains an abrupt comment close outside the deterministic subset.');
1324
+ }
1325
+ const canonical=html.indexOf('-->',start+4);
1326
+ const bang=html.indexOf('--!>',start+4);
1327
+ const close=canonical<0?bang:bang<0?canonical:Math.min(canonical,bang);
1328
+ if(close<0)fail('Application HTML contains an unterminated comment.');
1329
+ return close+(close===bang?4:3);
1330
+ }
1331
+
1332
+ function htmlTagHead(html,start){
1333
+ return html.slice(start).match(/^<\/?([A-Za-z][^\t\n\f\r />]*)(?=[\t\n\f\r />])/u);
1334
+ }
1335
+
1336
+ function validateEndTag(source,name){
1337
+ const match=source.match(/^<\/([A-Za-z][^\t\n\f\r />]*)[\t\n\f\r ]*>$/u);
1338
+ if(!match||asciiLower(match[1])!==name){
1339
+ fail(`Application HTML contains a malformed </${name}> end tag.`);
1340
+ }
1341
+ }
1342
+
1343
+ function rejectDeclarativeShadowTemplate(open){
1344
+ const attributes=parseTagAttributes(open);
1345
+ if(!attributes.has('shadowrootmode'))return;
1346
+ structuralAttribute(attributes,'shadowrootmode','template');
1347
+ fail(
1348
+ 'Application HTML contains declarative shadow DOM, whose connected module loads are '
1349
+ +'outside the deterministic import-map HTML subset.'
1350
+ );
1351
+ }
1352
+
1353
+ function selectElementEnd(html,openEnd){
1354
+ let cursor=openEnd;
1355
+ const elements=[];
1356
+ while(cursor<html.length){
1357
+ const start=html.indexOf('<',cursor);
1358
+ if(start<0){
1359
+ fail('Application HTML contains an unterminated <select> element.');
1360
+ }
1361
+ if(html.startsWith('<!--',start)){
1362
+ cursor=commentEnd(html,start);
1363
+ continue;
1364
+ }
1365
+ if(html.startsWith('<!',start)||html.startsWith('<?',start)){
1366
+ fail('Application HTML select contains an unsupported declaration or processing instruction.');
1367
+ }
1368
+ const head=htmlTagHead(html,start);
1369
+ if(!head){
1370
+ if(html.startsWith('</',start)||/^<[A-Za-z]/u.test(html.slice(start))){
1371
+ fail('Application HTML select contains a malformed tag.');
1372
+ }
1373
+ cursor=start+1;
1374
+ continue;
1375
+ }
1376
+ const name=htmlTagName(head[1]);
1377
+ const end=htmlTagEnd(html,start+head[0].length);
1378
+ const closing=html[start+1]==='/';
1379
+ if(closing)validateEndTag(html.slice(start,end),name);
1380
+ if(name==='frame'||name==='frameset'){
1381
+ fail(`Application HTML contains unsupported structural element <${name}>.`);
1382
+ }
1383
+ if(name==='select'){
1384
+ if(!closing){
1385
+ fail('Application HTML contains a nested <select> element.');
1386
+ }
1387
+ if(elements.length>0){
1388
+ fail(`Application HTML closes <select> before </${elements.at(-1)}> is present.`);
1389
+ }
1390
+ return end;
1391
+ }
1392
+ if(name!=='option'&&name!=='optgroup'){
1393
+ fail(
1394
+ `Application HTML select contains unsupported <${closing?'/':''}${name}> markup. `
1395
+ +'Only text, comments, option, and optgroup are accepted inside select.'
1396
+ );
1397
+ }
1398
+ if(closing){
1399
+ if(elements.at(-1)!==name){
1400
+ fail(`Application HTML select contains an unmatched </${name}> end tag.`);
1401
+ }
1402
+ elements.pop();
1403
+ }else{
1404
+ if(name==='optgroup'&&elements.length>0){
1405
+ fail('Application HTML select contains a nested or option-contained <optgroup>.');
1406
+ }
1407
+ if(name==='option'&&elements.at(-1)==='option'){
1408
+ fail('Application HTML select contains nested <option> elements.');
1409
+ }
1410
+ elements.push(name);
1411
+ }
1412
+ cursor=end;
1413
+ }
1414
+ fail('Application HTML contains an unterminated <select> element.');
1415
+ }
1416
+
1417
+ function nestedTemplateEnd(html,openEnd){
1418
+ let depth=1;
1419
+ let cursor=openEnd;
1420
+ while(cursor<html.length){
1421
+ const start=html.indexOf('<',cursor);
1422
+ if(start<0)return html.length;
1423
+ if(html.startsWith('<!--',start)){
1424
+ cursor=commentEnd(html,start);
1425
+ continue;
1426
+ }
1427
+ if(html.startsWith('<!',start)||html.startsWith('<?',start)){
1428
+ fail('Application HTML template contains an unsupported declaration or processing instruction.');
1429
+ }
1430
+ const head=htmlTagHead(html,start);
1431
+ if(!head){
1432
+ if(html.startsWith('</',start)||/^<[A-Za-z]/u.test(html.slice(start))){
1433
+ fail('Application HTML template contains a malformed tag.');
1434
+ }
1435
+ cursor=start+1;
1436
+ continue;
1437
+ }
1438
+ const name=htmlTagName(head[1]);
1439
+ const end=htmlTagEnd(html,start+head[0].length);
1440
+ const closing=html[start+1]==='/';
1441
+ if(closing)validateEndTag(html.slice(start,end),name);
1442
+ if(name==='frame'||name==='frameset'){
1443
+ fail(`Application HTML contains unsupported structural element <${name}>.`);
1444
+ }
1445
+ if(name==='select'){
1446
+ if(closing)fail('Application HTML contains an unmatched </select> end tag.');
1447
+ cursor=selectElementEnd(html,end);
1448
+ continue;
1449
+ }
1450
+ if(name==='svg'||name==='math'){
1451
+ fail(`Application HTML contains unsupported foreign-content element <${name}>.`);
1452
+ }
1453
+ if(!closing&&name==='noscript'){
1454
+ fail('Application HTML contains <noscript>, whose active parsing depends on browser mode.');
1455
+ }
1456
+ if(!closing&&name==='plaintext')return html.length;
1457
+ if(!closing&&TEXT_ELEMENTS.has(name)){
1458
+ cursor=rawElementEnd(html,name,end).end;
1459
+ continue;
1460
+ }
1461
+ if(name!=='template'){
1462
+ cursor=end;
1463
+ continue;
1464
+ }
1465
+ if(!closing)rejectDeclarativeShadowTemplate(html.slice(start,end));
1466
+ if(closing)depth-=1;
1467
+ else depth+=1;
1468
+ cursor=end;
1469
+ if(depth===0)return end;
1470
+ }
1471
+ return html.length;
1472
+ }
1473
+
1474
+ function scanHtmlStructure(html){
1475
+ const scripts=[];
1476
+ const links=[];
1477
+ const bases=[];
1478
+ const metas=[];
1479
+ let headClose=-1;
1480
+ let bodyClose=-1;
1481
+ let cursor=0;
1482
+ let sawDoctype=false;
1483
+ while(cursor<html.length){
1484
+ const start=html.indexOf('<',cursor);
1485
+ if(start<0)break;
1486
+ if(html.startsWith('<!--',start)){
1487
+ cursor=commentEnd(html,start);
1488
+ continue;
1489
+ }
1490
+ if(html.startsWith('<!',start)){
1491
+ const end=htmlTagEnd(html,start+2);
1492
+ const declaration=html.slice(start,end);
1493
+ if(!/^<!doctype[\t\n\f\r ]+html[\t\n\f\r ]*>$/i.test(declaration)
1494
+ ||sawDoctype
1495
+ ||!/^(?:\ufeff)?[\t\n\f\r ]*$/u.test(html.slice(0,start))){
1496
+ fail('Application HTML contains an unsupported or misplaced declaration.');
1497
+ }
1498
+ sawDoctype=true;
1499
+ cursor=end;
1500
+ continue;
1501
+ }
1502
+ if(html.startsWith('<?',start)){
1503
+ fail('Application HTML contains an unsupported processing instruction.');
1504
+ }
1505
+ const head=htmlTagHead(html,start);
1506
+ if(!head){
1507
+ if(html.startsWith('</',start)||/^<[A-Za-z]/u.test(html.slice(start))){
1508
+ fail('Application HTML contains a malformed tag.');
1509
+ }
1510
+ cursor=start+1;
1511
+ continue;
1512
+ }
1513
+ const tag=htmlTagName(head[1]);
1514
+ const closing=html[start+1]==='/';
1515
+ const openEnd=htmlTagEnd(html,start+head[0].length);
1516
+ const open=html.slice(start,openEnd);
1517
+ if(tag==='frame'||tag==='frameset'){
1518
+ fail(`Application HTML contains unsupported structural element <${tag}>.`);
1519
+ }
1520
+ if(tag==='select'){
1521
+ if(closing)fail('Application HTML contains an unmatched </select> end tag.');
1522
+ cursor=selectElementEnd(html,openEnd);
1523
+ continue;
1524
+ }
1525
+ if(tag==='svg'||tag==='math'){
1526
+ fail(`Application HTML contains unsupported foreign-content element <${tag}>.`);
1527
+ }
1528
+ if(closing){
1529
+ validateEndTag(open,tag);
1530
+ if(tag==='head'&&headClose<0)headClose=start;
1531
+ if(tag==='body'&&bodyClose<0)bodyClose=start;
1532
+ cursor=openEnd;
1533
+ continue;
1534
+ }
1535
+ if(tag==='link'){
1536
+ links.push({start,end:openEnd,open});
1537
+ cursor=openEnd;
1538
+ continue;
1539
+ }
1540
+ if(tag==='base'){
1541
+ bases.push({start,end:openEnd,open});
1542
+ cursor=openEnd;
1543
+ continue;
1544
+ }
1545
+ if(tag==='meta'){
1546
+ metas.push({start,end:openEnd,open});
1547
+ cursor=openEnd;
1548
+ continue;
1549
+ }
1550
+ if(tag==='template'){
1551
+ rejectDeclarativeShadowTemplate(open);
1552
+ cursor=nestedTemplateEnd(html,openEnd);
1553
+ continue;
1554
+ }
1555
+ if(tag==='noscript'){
1556
+ fail('Application HTML contains <noscript>, whose active parsing depends on browser mode.');
1557
+ }
1558
+ if(tag==='plaintext'){
1559
+ cursor=html.length;
1560
+ continue;
1561
+ }
1562
+ if(!TEXT_ELEMENTS.has(tag)){
1563
+ cursor=openEnd;
1564
+ continue;
1565
+ }
1566
+ const raw=rawElementEnd(html,tag,openEnd);
1567
+ if(tag==='script')scripts.push({
1568
+ start,
1569
+ openEnd,
1570
+ end:raw.end,
1571
+ open,
1572
+ closed:raw.closed
1573
+ });
1574
+ cursor=raw.end;
1575
+ }
1576
+ return {scripts,links,bases,metas,headClose,bodyClose};
1577
+ }
1578
+
1579
+ function removeManagedBlocks(html,blocks){
1580
+ let result=html;
1581
+ for(const block of [...blocks].sort((left,right)=>right.start-left.start)){
1582
+ let start=block.start;
1583
+ let end=block.end;
1584
+ const lineStart=result.lastIndexOf('\n',start-1)+1;
1585
+ if(/^[\t\n\f\r ]*$/u.test(result.slice(lineStart,start)))start=lineStart;
1586
+ const trailing=result.slice(end).match(/^(?:\r?\n)/u);
1587
+ if(trailing)end+=trailing[0].length;
1588
+ result=result.slice(0,start)+result.slice(end);
1589
+ }
1590
+ return result;
1591
+ }
1592
+
1593
+ function firstModulePosition(html){
1594
+ const structure=scanHtmlStructure(html);
1595
+ let first=-1;
1596
+ for(const script of structure.scripts){
1597
+ const attributes=parseTagAttributes(script.open);
1598
+ if(scriptType(attributes)==='module'
1599
+ &&(first<0||script.start<first))first=script.start;
1600
+ }
1601
+ for(const link of structure.links){
1602
+ const attributes=parseTagAttributes(link.open);
1603
+ const relationships=canonicalHtmlToken(structuralAttribute(attributes,'rel','link'))
1604
+ .split(/[\t\n\f\r ]+/u);
1605
+ if(relationships.includes('modulepreload')&&(first<0||link.start<first))first=link.start;
1606
+ }
1607
+ return first;
1608
+ }
1609
+
1610
+ function firstBlockingLoadPosition(html,{skipManaged=false}={}){
1611
+ const structure=scanHtmlStructure(html);
1612
+ let first=-1;
1613
+ for(const script of structure.scripts){
1614
+ const attributes=parseTagAttributes(script.open);
1615
+ structuralAttribute(attributes,'type','script');
1616
+ if(skipManaged&&attributes.has(MANAGED_IMPORT_MAP_ATTRIBUTE))continue;
1617
+ if(first<0||script.start<first)first=script.start;
1618
+ }
1619
+ for(const link of structure.links){
1620
+ const attributes=parseTagAttributes(link.open);
1621
+ const relationships=canonicalHtmlToken(structuralAttribute(attributes,'rel','link'))
1622
+ .split(/[\t\n\f\r ]+/u);
1623
+ if(relationships.includes('modulepreload')&&(first<0||link.start<first)){
1624
+ first=link.start;
1625
+ }
1626
+ }
1627
+ return first;
1628
+ }
1629
+
1630
+ export function inspectImportMapHtml(html){
1631
+ const source=String(html);
1632
+ const structure=scanHtmlStructure(source);
1633
+ const bases=structure.bases.map(base=>Object.freeze({
1634
+ start:base.start,
1635
+ end:base.end,
1636
+ href:structuralAttribute(parseTagAttributes(base.open),'href','base')
1637
+ }));
1638
+ const managedMaps=structure.scripts.filter(script=>{
1639
+ const attributes=parseTagAttributes(script.open);
1640
+ return attributes.has(MANAGED_IMPORT_MAP_ATTRIBUTE)
1641
+ &&scriptType(attributes)==='importmap';
1642
+ }).map(script=>Object.freeze({start:script.start,end:script.end}));
1643
+ const scripts=structure.scripts.map(script=>{
1644
+ const attributes=parseTagAttributes(script.open);
1645
+ return Object.freeze({
1646
+ start:script.start,
1647
+ end:script.end,
1648
+ type:scriptType(attributes),
1649
+ src:structuralAttribute(attributes,'src','script'),
1650
+ managed:attributes.has(MANAGED_IMPORT_MAP_ATTRIBUTE)
1651
+ });
1652
+ });
1653
+ const links=structure.links.map(link=>{
1654
+ const attributes=parseTagAttributes(link.open);
1655
+ return Object.freeze({
1656
+ start:link.start,
1657
+ end:link.end,
1658
+ rel:canonicalHtmlToken(structuralAttribute(attributes,'rel','link')),
1659
+ href:structuralAttribute(attributes,'href','link')
1660
+ });
1661
+ });
1662
+ const metas=structure.metas.map(meta=>{
1663
+ const attributes=parseTagAttributes(meta.open);
1664
+ return Object.freeze({
1665
+ start:meta.start,
1666
+ end:meta.end,
1667
+ name:canonicalHtmlToken(structuralAttribute(attributes,'name','meta')),
1668
+ content:structuralAttribute(attributes,'content','meta')
1669
+ });
1670
+ });
1671
+ return Object.freeze({
1672
+ bases:Object.freeze(bases),
1673
+ managedMaps:Object.freeze(managedMaps),
1674
+ scripts:Object.freeze(scripts),
1675
+ links:Object.freeze(links),
1676
+ metas:Object.freeze(metas),
1677
+ firstModulePosition:firstModulePosition(source)
1678
+ });
1679
+ }
1680
+
1681
+ function renderManagedHtml(html,json){
1682
+ const structure=scanHtmlStructure(html);
1683
+ const activeBases=structure.bases.map(base=>({
1684
+ ...base,
1685
+ href:structuralAttribute(parseTagAttributes(base.open),'href','base')
1686
+ }));
1687
+ if(activeBases.length!==1||activeBases[0].href!=='../../'){
1688
+ fail('Application HTML must contain exactly one active <base href="../../"> element.');
1689
+ }
1690
+ const complete=[];
1691
+ for(const script of structure.scripts){
1692
+ const attributes=parseTagAttributes(script.open);
1693
+ if(attributes.has(MANAGED_IMPORT_MAP_ATTRIBUTE)){
1694
+ if(scriptType(attributes)!=='importmap'){
1695
+ fail(`Managed ${MANAGED_IMPORT_MAP_ATTRIBUTE} script must use type="importmap".`);
1696
+ }
1697
+ if(!script.closed){
1698
+ fail(`Application HTML contains an unterminated ${MANAGED_IMPORT_MAP_ATTRIBUTE} script.`);
1699
+ }
1700
+ complete.push({start:script.start,end:script.end});
1701
+ }else if(scriptType(attributes)==='importmap'){
1702
+ fail(
1703
+ `Application HTML already contains an unmanaged import map. Remove it or add `
1704
+ +`${MANAGED_IMPORT_MAP_ATTRIBUTE}, then rerun arcane import-map.`
1705
+ );
1706
+ }
1707
+ }
1708
+ if(complete.length>1){
1709
+ fail(`Application HTML contains multiple ${MANAGED_IMPORT_MAP_ATTRIBUTE} scripts.`);
1710
+ }
1711
+ const withoutManaged=removeManagedBlocks(html,complete);
1712
+ const cleanedStructure=scanHtmlStructure(withoutManaged);
1713
+ const cleanedBases=cleanedStructure.bases.map(base=>({
1714
+ ...base,
1715
+ href:structuralAttribute(parseTagAttributes(base.open),'href','base')
1716
+ }));
1717
+ if(cleanedBases.length!==1||cleanedBases[0].href!=='../../'){
1718
+ fail('Application HTML must retain exactly one active <base href="../../"> element.');
1719
+ }
1720
+ const firstBlocking=firstBlockingLoadPosition(withoutManaged);
1721
+ if(firstBlocking>=0&&cleanedBases[0].start>firstBlocking){
1722
+ fail('Application base element must precede every classic script, module, and modulepreload.');
1723
+ }
1724
+ const insertionPosition=cleanedBases[0].end;
1725
+ const lineStart=withoutManaged.lastIndexOf('\n',Math.max(0,cleanedBases[0].start-1))+1;
1726
+ const linePrefix=withoutManaged.slice(lineStart,cleanedBases[0].start);
1727
+ const indent=/^[\t\n\f\r ]*$/u.test(linePrefix)?linePrefix:'';
1728
+ const newline=withoutManaged.includes('\r\n')?'\r\n':'\n';
1729
+ const block=`${newline}${indent}<script type="importmap" ${MANAGED_IMPORT_MAP_ATTRIBUTE}>\n${json}</script>`;
1730
+ const rendered=withoutManaged.slice(0,insertionPosition)+block
1731
+ +withoutManaged.slice(insertionPosition);
1732
+ const rescanned=scanHtmlStructure(rendered);
1733
+ const managedScripts=rescanned.scripts.filter(script=>{
1734
+ const attributes=parseTagAttributes(script.open);
1735
+ return attributes.has(MANAGED_IMPORT_MAP_ATTRIBUTE)
1736
+ &&scriptType(attributes)==='importmap';
1737
+ });
1738
+ if(managedScripts.length!==1){
1739
+ fail('Generated application HTML must contain exactly one active managed Arcane import map.');
1740
+ }
1741
+ const managedPosition=managedScripts[0].start;
1742
+ const renderedBases=rescanned.bases.map(base=>({
1743
+ ...base,
1744
+ href:structuralAttribute(parseTagAttributes(base.open),'href','base')
1745
+ }));
1746
+ if(renderedBases.length!==1||renderedBases[0].href!=='../../'
1747
+ ||renderedBases[0].end>managedPosition
1748
+ ||!/^[\t\n\f\r ]*$/u.test(rendered.slice(renderedBases[0].end,managedPosition))){
1749
+ fail('Generated Arcane import-map HTML has an invalid or late base element.');
1750
+ }
1751
+ const blockingPosition=firstBlockingLoadPosition(rendered,{skipManaged:true});
1752
+ if(blockingPosition>=0&&managedPosition>blockingPosition){
1753
+ fail('Generated Arcane import map is not before the first classic script, module, or modulepreload.');
1754
+ }
1755
+ return rendered;
1756
+ }
1757
+
1758
+ async function readRealFile(filePath,label){
1759
+ const state=await readRealFileState(filePath,label);
1760
+ return state.bytes;
1761
+ }
1762
+
1763
+ async function readRealFileState(filePath,label,{optional=false}={}){
1764
+ let info;
1765
+ try{info=await lstat(filePath,{bigint:true});}
1766
+ catch(error){
1767
+ if(optional&&error?.code==='ENOENT')return {exists:false,filePath};
1768
+ throw error;
1769
+ }
1770
+ if(info.isSymbolicLink()||!info.isFile())fail(`${label} must be a real file: ${filePath}.`);
1771
+ const handle=await open(filePath,READ_ONLY_NO_FOLLOW);
1772
+ try{
1773
+ const opened=await handle.stat({bigint:true});
1774
+ if(!sameFileIdentity(info,opened))fail(`${label} changed while opening: ${filePath}.`);
1775
+ const bytes=await handle.readFile();
1776
+ const after=await handle.stat({bigint:true});
1777
+ if(!sameFileIdentity(opened,after))fail(`${label} changed while reading: ${filePath}.`);
1778
+ return {exists:true,filePath,bytes,identity:after};
1779
+ }finally{
1780
+ await handle.close();
1781
+ }
1782
+ }
1783
+
1784
+ async function captureDirectoryState(root,directory,{create=false}={}){
1785
+ const resolvedRoot=path.resolve(root);
1786
+ const resolvedDirectory=path.resolve(directory);
1787
+ if(!pathInside(resolvedRoot,resolvedDirectory)){
1788
+ fail(`Import-map directory escapes its application root: ${resolvedDirectory}.`);
1789
+ }
1790
+ const rootInfo=await lstat(resolvedRoot,{bigint:true});
1791
+ if(rootInfo.isSymbolicLink()||!rootInfo.isDirectory()){
1792
+ fail(`Import-map application root must be a real directory: ${resolvedRoot}.`);
1793
+ }
1794
+ const canonicalRoot=await realpath(resolvedRoot);
1795
+ const canonicalRootInfo=await lstat(canonicalRoot,{bigint:true});
1796
+ if(canonicalRootInfo.isSymbolicLink()||!canonicalRootInfo.isDirectory()
1797
+ ||!sameDirectoryIdentity(rootInfo,canonicalRootInfo)){
1798
+ fail(`Import-map application root changed while authenticating: ${resolvedRoot}.`);
1799
+ }
1800
+ const entries=[{location:resolvedRoot,identity:canonicalRootInfo,canonical:canonicalRoot}];
1801
+ const relative=path.relative(resolvedRoot,resolvedDirectory);
1802
+ let current=resolvedRoot;
1803
+ let parent=entries[0];
1804
+ for(const part of relative.split(path.sep).filter(Boolean)){
1805
+ const parentBefore=await lstat(parent.location,{bigint:true});
1806
+ if(parentBefore.isSymbolicLink()||!parentBefore.isDirectory()
1807
+ ||!sameDirectoryIdentity(parentBefore,parent.identity)
1808
+ ||await realpath(parent.location)!==parent.canonical){
1809
+ fail(`Import-map directory changed before creating a child: ${parent.location}.`);
1810
+ }
1811
+ const child=path.join(current,part);
1812
+ if(create){
1813
+ try{await mkdir(child);}
1814
+ catch(error){if(error?.code!=='EEXIST')throw error;}
1815
+ }
1816
+ const info=await lstat(child,{bigint:true});
1817
+ if(info.isSymbolicLink()||!info.isDirectory()){
1818
+ fail(`Import-map directory must be a real directory: ${child}.`);
1819
+ }
1820
+ const canonical=await realpath(child);
1821
+ if(!pathInside(canonicalRoot,canonical)||path.dirname(canonical)!==parent.canonical){
1822
+ fail(`Import-map directory resolves outside its application root: ${child}.`);
1823
+ }
1824
+ const parentAfter=await lstat(parent.location,{bigint:true});
1825
+ if(parentAfter.isSymbolicLink()||!parentAfter.isDirectory()
1826
+ ||!sameDirectoryIdentity(parentAfter,parent.identity)
1827
+ ||await realpath(parent.location)!==parent.canonical){
1828
+ fail(`Import-map directory changed while creating a child: ${parent.location}.`);
1829
+ }
1830
+ const entry={location:child,identity:info,canonical};
1831
+ entries.push(entry);
1832
+ current=child;
1833
+ parent=entry;
1834
+ }
1835
+ return {root:resolvedRoot,directory:resolvedDirectory,entries};
1836
+ }
1837
+
1838
+ function sameDirectoryIdentity(left,right){
1839
+ return left.isDirectory()&&right.isDirectory()&&left.dev===right.dev&&left.ino===right.ino;
1840
+ }
1841
+
1842
+ async function assertDirectoryState(state){
1843
+ for(const entry of state.entries){
1844
+ const info=await lstat(entry.location,{bigint:true});
1845
+ if(info.isSymbolicLink()||!info.isDirectory()
1846
+ ||!sameDirectoryIdentity(info,entry.identity)
1847
+ ||await realpath(entry.location)!==entry.canonical){
1848
+ fail(`Import-map directory changed during generation: ${entry.location}.`);
1849
+ }
1850
+ }
1851
+ }
1852
+
1853
+ async function stageSibling(filePath,bytes,directoryState){
1854
+ await assertDirectoryState(directoryState);
1855
+ const staged=path.join(
1856
+ path.dirname(filePath),
1857
+ `.${path.basename(filePath)}.arcane-stage-${String(process.pid)}-${randomUUID()}`
1858
+ );
1859
+ const content=Buffer.from(bytes);
1860
+ let handle;
1861
+ let ownedIdentity;
1862
+ try{
1863
+ handle=await open(staged,WRITE_NEW_NO_FOLLOW,0o644);
1864
+ ownedIdentity=await handle.stat({bigint:true});
1865
+ await assertDirectoryState(directoryState);
1866
+ await handle.writeFile(content);
1867
+ await handle.sync();
1868
+ await handle.close();
1869
+ handle=null;
1870
+ await assertDirectoryState(directoryState);
1871
+ const identity=await lstat(staged,{bigint:true});
1872
+ if(identity.isSymbolicLink()||!identity.isFile()
1873
+ ||!sameFileLocation(identity,ownedIdentity)){
1874
+ fail(`Import-map staged file changed while it was written: ${staged}.`);
1875
+ }
1876
+ return {
1877
+ path:staged,
1878
+ identity,
1879
+ directoryState,
1880
+ byteLength:content.length,
1881
+ hash:sha256(content)
1882
+ };
1883
+ }catch(error){
1884
+ try{await handle?.close();}
1885
+ catch(cleanupError){error.cleanupError??=cleanupError;}
1886
+ if(ownedIdentity){
1887
+ try{
1888
+ const removed=await removeOwnedPath(staged,ownedIdentity,directoryState);
1889
+ if(!removed)fail(`Import-map staged file could not be safely cleaned: ${staged}.`);
1890
+ }catch(cleanupError){error.cleanupError??=cleanupError;}
1891
+ }
1892
+ throw error;
1893
+ }
1894
+ }
1895
+
1896
+ async function removeOwnedPath(filePath,identity,directoryState){
1897
+ try{await assertDirectoryState(directoryState);}
1898
+ catch{return false;}
1899
+ let current;
1900
+ try{current=await lstat(filePath,{bigint:true});}
1901
+ catch(error){
1902
+ if(error?.code==='ENOENT')return true;
1903
+ throw error;
1904
+ }
1905
+ if(current.isSymbolicLink()||!current.isFile()||!sameFileLocation(current,identity))return false;
1906
+ await rm(filePath);
1907
+ return true;
1908
+ }
1909
+
1910
+ async function verifiedFileAt(filePath,expected,label,{strictIdentity=true}={}){
1911
+ await assertDirectoryState(expected.directoryState);
1912
+ const before=await lstat(filePath,{bigint:true});
1913
+ if(before.isSymbolicLink()||!before.isFile()
1914
+ ||!sameFileLocation(before,expected.identity)
1915
+ ||strictIdentity&&!sameFileIdentity(before,expected.identity)){
1916
+ fail(`${label} changed before promotion.`);
1917
+ }
1918
+ let handle;
1919
+ try{handle=await open(filePath,READ_ONLY_NO_FOLLOW);}
1920
+ catch(error){
1921
+ if(error?.code==='ELOOP')fail(`${label} became a symbolic link before promotion.`);
1922
+ throw error;
1923
+ }
1924
+ let after;
1925
+ try{
1926
+ const opened=await handle.stat({bigint:true});
1927
+ if(!sameFileIdentity(before,opened))fail(`${label} changed while opening.`);
1928
+ const bytes=await handle.readFile();
1929
+ after=await handle.stat({bigint:true});
1930
+ if(!sameFileIdentity(opened,after)||bytes.length!==expected.byteLength
1931
+ ||sha256(bytes)!==expected.hash){
1932
+ fail(`${label} failed its identity or content check before promotion.`);
1933
+ }
1934
+ }finally{
1935
+ await handle.close();
1936
+ }
1937
+ const current=await lstat(filePath,{bigint:true});
1938
+ if(current.isSymbolicLink()||!current.isFile()||!sameFileIdentity(current,after)){
1939
+ fail(`${label} changed after verification.`);
1940
+ }
1941
+ await assertDirectoryState(expected.directoryState);
1942
+ return current;
1943
+ }
1944
+
1945
+ function originalDescriptor(state){
1946
+ return {
1947
+ identity:state.identity,
1948
+ directoryState:state.directoryState,
1949
+ byteLength:state.bytes.length,
1950
+ hash:sha256(state.bytes)
1951
+ };
1952
+ }
1953
+
1954
+ async function pathIsAbsent(filePath){
1955
+ try{
1956
+ await lstat(filePath);
1957
+ return false;
1958
+ }catch(error){
1959
+ if(error?.code==='ENOENT')return true;
1960
+ throw error;
1961
+ }
1962
+ }
1963
+
1964
+ async function restoreBackup(state,backup,label){
1965
+ const expected=originalDescriptor(state);
1966
+ await assertDirectoryState(state.directoryState);
1967
+ if(!await pathIsAbsent(state.filePath)){
1968
+ fail(`${label} changed before its import-map backup could be restored.`);
1969
+ }
1970
+ await verifiedFileAt(backup,expected,`${label} backup`,{strictIdentity:false});
1971
+ await rename(backup,state.filePath);
1972
+ await verifiedFileAt(state.filePath,expected,`${label} restored file`,{strictIdentity:false});
1973
+ }
1974
+
1975
+ async function pathStateUnchanged(state,label){
1976
+ if(!state.exists){
1977
+ try{
1978
+ await lstat(state.filePath);
1979
+ fail(`${label} appeared while the import map was being generated.`);
1980
+ }catch(error){
1981
+ if(error?.code!=='ENOENT')throw error;
1982
+ }
1983
+ return;
1984
+ }
1985
+ const current=await lstat(state.filePath,{bigint:true});
1986
+ if(current.isSymbolicLink()||!current.isFile()||!sameFileIdentity(current,state.identity)){
1987
+ fail(`${label} changed while the import map was being generated.`);
1988
+ }
1989
+ }
1990
+
1991
+ async function installStagedFile(state,staged,label){
1992
+ const backup=path.join(
1993
+ path.dirname(state.filePath),
1994
+ `.${path.basename(state.filePath)}.arcane-backup-${String(process.pid)}-${randomUUID()}`
1995
+ );
1996
+ let backedUp=false;
1997
+ let promoted=false;
1998
+ let installedIdentity=null;
1999
+ try{
2000
+ await assertDirectoryState(staged.directoryState);
2001
+ await pathStateUnchanged(state,label);
2002
+ await verifiedFileAt(staged.path,staged,`${label} staged file`);
2003
+ if(state.exists){
2004
+ await rename(state.filePath,backup);
2005
+ backedUp=true;
2006
+ await verifiedFileAt(
2007
+ backup,
2008
+ originalDescriptor(state),
2009
+ `${label} backup`,
2010
+ {strictIdentity:false}
2011
+ );
2012
+ }
2013
+ await verifiedFileAt(staged.path,staged,`${label} staged file`);
2014
+ await rename(staged.path,state.filePath);
2015
+ promoted=true;
2016
+ installedIdentity=await verifiedFileAt(
2017
+ state.filePath,
2018
+ staged,
2019
+ `${label} installed file`,
2020
+ {strictIdentity:false}
2021
+ );
2022
+ await assertDirectoryState(staged.directoryState);
2023
+ }catch(error){
2024
+ if(promoted){
2025
+ try{
2026
+ const removed=await removeOwnedPath(
2027
+ state.filePath,
2028
+ installedIdentity??staged.identity,
2029
+ staged.directoryState
2030
+ );
2031
+ if(!removed)fail(`${label} changed before its failed promotion could be removed.`);
2032
+ }catch(rollbackError){error.rollbackError??=rollbackError;}
2033
+ }
2034
+ if(backedUp){
2035
+ try{await restoreBackup(state,backup,label);}
2036
+ catch(rollbackError){error.rollbackError??=rollbackError;}
2037
+ }
2038
+ throw error;
2039
+ }
2040
+ return {
2041
+ async verify(){
2042
+ if(!installedIdentity)fail(`${label} was not installed before pair verification.`);
2043
+ return verifiedFileAt(
2044
+ state.filePath,
2045
+ {
2046
+ identity:installedIdentity,
2047
+ directoryState:staged.directoryState,
2048
+ byteLength:staged.byteLength,
2049
+ hash:staged.hash
2050
+ },
2051
+ `${label} committed file`
2052
+ );
2053
+ },
2054
+ async commit(){
2055
+ if(!backedUp)return;
2056
+ const removed=await removeOwnedPath(backup,state.identity,staged.directoryState);
2057
+ if(!removed)fail(`${label} backup changed before transaction cleanup.`);
2058
+ },
2059
+ async rollback(){
2060
+ await assertDirectoryState(staged.directoryState);
2061
+ if(!await pathIsAbsent(state.filePath)){
2062
+ const removed=await removeOwnedPath(
2063
+ state.filePath,
2064
+ installedIdentity,
2065
+ staged.directoryState
2066
+ );
2067
+ if(!removed){
2068
+ fail(`${label} changed before its import-map transaction could roll back.`);
2069
+ }
2070
+ }
2071
+ if(backedUp)await restoreBackup(state,backup,label);
2072
+ }
2073
+ };
2074
+ }
2075
+
2076
+ async function commitGeneratedPair({
2077
+ artifactState,
2078
+ entryState,
2079
+ artifactBytes,
2080
+ entryBytes,
2081
+ signal,
2082
+ onEvent
2083
+ }){
2084
+ const artifactStage=await stageSibling(
2085
+ artifactState.filePath,
2086
+ artifactBytes,
2087
+ artifactState.directoryState
2088
+ );
2089
+ let entryStage;
2090
+ let artifactInstall;
2091
+ let entryInstall;
2092
+ let failure;
2093
+ try{
2094
+ entryStage=await stageSibling(
2095
+ entryState.filePath,
2096
+ entryBytes,
2097
+ entryState.directoryState
2098
+ );
2099
+ await emit(onEvent,{type:'import-map.commit.staged'});
2100
+ throwIfAborted(signal);
2101
+ artifactInstall=await installStagedFile(
2102
+ artifactState,
2103
+ artifactStage,
2104
+ 'Import-map artifact'
2105
+ );
2106
+ entryInstall=await installStagedFile(entryState,entryStage,'Import-map application entry');
2107
+ await artifactInstall.verify();
2108
+ await entryInstall.verify();
2109
+ await emit(onEvent,{
2110
+ type:'import-map.commit.progress',
2111
+ paths:Object.freeze([artifactState.filePath,entryState.filePath])
2112
+ });
2113
+ throwIfAborted(signal);
2114
+ await artifactInstall.verify();
2115
+ await entryInstall.verify();
2116
+ }catch(error){
2117
+ if(entryInstall)await entryInstall.rollback().catch(rollback=>{error.rollbackError??=rollback;});
2118
+ if(artifactInstall){
2119
+ await artifactInstall.rollback().catch(rollback=>{error.rollbackError??=rollback;});
2120
+ }
2121
+ failure=error;
2122
+ }
2123
+ const cleanupErrors=[];
2124
+ if(!artifactInstall){
2125
+ try{
2126
+ const removed=await removeOwnedPath(
2127
+ artifactStage.path,
2128
+ artifactStage.identity,
2129
+ artifactStage.directoryState
2130
+ );
2131
+ if(!removed)fail(`Import-map artifact stage could not be safely cleaned: ${artifactStage.path}.`);
2132
+ }catch(error){cleanupErrors.push(error);}
2133
+ }
2134
+ if(entryStage&&!entryInstall){
2135
+ try{
2136
+ const removed=await removeOwnedPath(
2137
+ entryStage.path,
2138
+ entryStage.identity,
2139
+ entryStage.directoryState
2140
+ );
2141
+ if(!removed){
2142
+ fail(`Import-map application-entry stage could not be safely cleaned: ${entryStage.path}.`);
2143
+ }
2144
+ }catch(error){cleanupErrors.push(error);}
2145
+ }
2146
+ if(failure){
2147
+ if(cleanupErrors.length>0){
2148
+ failure.cleanupError??=cleanupErrors.length===1
2149
+ ?cleanupErrors[0]
2150
+ :new AggregateError(cleanupErrors,'Import-map transaction cleanup failed.');
2151
+ }
2152
+ throw failure;
2153
+ }
2154
+ if(cleanupErrors.length>0){
2155
+ throw new AggregateError(cleanupErrors,'Import-map transaction cleanup failed.');
2156
+ }
2157
+
2158
+ const cleanupWarnings=[];
2159
+ for(const installed of [entryInstall,artifactInstall]){
2160
+ try{await installed.commit();}
2161
+ catch(error){cleanupWarnings.push(error);}
2162
+ }
2163
+ await artifactInstall.verify();
2164
+ await entryInstall.verify();
2165
+ if(cleanupWarnings.length>0){
2166
+ return Object.freeze(cleanupWarnings.map(error=>String(error?.message??error)));
2167
+ }
2168
+ return Object.freeze([]);
2169
+ }
2170
+
2171
+ function resolvedAppRoot(workspaceRoot,appId,appRoot){
2172
+ if(!SAFE_APP_ID.test(appId??'')){
2173
+ fail(`Import-map app id must use lowercase letters, digits, and internal hyphens: ${String(appId)}.`);
2174
+ }
2175
+ const resolved=path.resolve(appRoot??path.join(workspaceRoot,'apps',appId));
2176
+ if(!pathInside(workspaceRoot,resolved))fail('Import-map application root must stay inside the workspace.');
2177
+ return resolved;
2178
+ }
2179
+
2180
+ async function generateImportMapUnlocked({
2181
+ workspaceRoot,
2182
+ appId,
2183
+ appRoot,
2184
+ entry='index.html',
2185
+ workspaceRuntimeReceipt,
2186
+ signal,
2187
+ onEvent
2188
+ }={}){
2189
+ if(typeof workspaceRoot!=='string'||workspaceRoot.trim()===''){
2190
+ throw new TypeError('generateImportMap workspaceRoot must be a nonempty string.');
2191
+ }
2192
+ throwIfAborted(signal);
2193
+ const resolvedWorkspace=path.resolve(workspaceRoot);
2194
+ const resolvedApp=resolvedAppRoot(resolvedWorkspace,appId,appRoot);
2195
+ const safeEntry=safeRelativePath(entry,'application entry');
2196
+ const entryPath=path.resolve(resolvedApp,...safeEntry.split('/'));
2197
+ if(!pathInside(resolvedApp,entryPath))fail('Import-map application entry escapes its app root.');
2198
+ const artifactPath=path.join(resolvedApp,...IMPORT_MAP_RELATIVE_PATH.split('/'));
2199
+ await emit(onEvent,{type:'import-map.started',appId,artifactPath,entryPath});
2200
+
2201
+ const entryDirectoryState=await captureDirectoryState(
2202
+ resolvedWorkspace,
2203
+ path.dirname(entryPath)
2204
+ );
2205
+ const entryState=await readRealFileState(entryPath,'Import-map application entry');
2206
+ entryState.directoryState=entryDirectoryState;
2207
+ const html=entryState.bytes.toString('utf8');
2208
+ // Reject malformed application structure before traversing the substantially larger runtime
2209
+ // graph. The real generated map is rendered and revalidated again before commit.
2210
+ renderManagedHtml(html,'{"imports":{}}\n');
2211
+ let runtime;
2212
+ if(workspaceRuntimeReceipt){
2213
+ await authenticateWorkspaceRuntimeReceipt(workspaceRuntimeReceipt,{
2214
+ workspaceRoot:resolvedWorkspace,
2215
+ signal
2216
+ });
2217
+ runtime={
2218
+ files:workspaceRuntimeReceipt.files.map(file=>file.path),
2219
+ readFile:relativePath=>readVerifiedWorkspaceRuntimeFile(workspaceRuntimeReceipt,{
2220
+ workspaceRoot:resolvedWorkspace,
2221
+ relativePath,
2222
+ signal
2223
+ })
2224
+ };
2225
+ }else{
2226
+ runtime=await physicalRuntime(resolvedWorkspace,signal);
2227
+ }
2228
+ for(const required of SDK_BROWSER_FILES){
2229
+ if(!runtime.files.includes(required)){
2230
+ fail(
2231
+ `Workspace Arcane runtime is missing the authenticated SDK browser file `
2232
+ +`"${required}". Materialize the current SDK runtime, then rerun arcane import-map.`,
2233
+ 'ARCANE_IMPORT_MAP_UNRESOLVED'
2234
+ );
2235
+ }
2236
+ }
2237
+ const built=await buildImportMap({files:runtime.files,readFile:runtime.readFile,signal});
2238
+ const document={imports:built.imports};
2239
+ const json=`${JSON.stringify(document,null,2).replaceAll('<','\\u003c')}\n`;
2240
+ const renderedHtml=renderManagedHtml(html,json);
2241
+
2242
+ throwIfAborted(signal);
2243
+ const artifactDirectoryState=await captureDirectoryState(
2244
+ resolvedWorkspace,
2245
+ path.dirname(artifactPath),
2246
+ {create:true}
2247
+ );
2248
+ const artifactState=await readRealFileState(
2249
+ artifactPath,
2250
+ 'Import-map artifact',
2251
+ {optional:true}
2252
+ );
2253
+ artifactState.directoryState=artifactDirectoryState;
2254
+ const cleanupWarnings=await commitGeneratedPair({
2255
+ artifactState,
2256
+ entryState,
2257
+ artifactBytes:Buffer.from(json,'utf8'),
2258
+ entryBytes:Buffer.from(renderedHtml,'utf8'),
2259
+ signal,
2260
+ onEvent
2261
+ });
2262
+ const committedFiles=Object.freeze([
2263
+ Object.freeze({
2264
+ role:'artifact',
2265
+ path:path.relative(resolvedWorkspace,artifactPath).split(path.sep).join('/'),
2266
+ bytes:Buffer.byteLength(json,'utf8'),
2267
+ sha256:sha256(Buffer.from(json,'utf8'))
2268
+ }),
2269
+ Object.freeze({
2270
+ role:'entry',
2271
+ path:path.relative(resolvedWorkspace,entryPath).split(path.sep).join('/'),
2272
+ bytes:Buffer.byteLength(renderedHtml,'utf8'),
2273
+ sha256:sha256(Buffer.from(renderedHtml,'utf8'))
2274
+ })
2275
+ ]);
2276
+ const receipt=Object.freeze({
2277
+ appId,
2278
+ artifactPath,
2279
+ artifactRelativePath:path.relative(resolvedWorkspace,artifactPath).split(path.sep).join('/'),
2280
+ entryPath,
2281
+ imports:built.imports,
2282
+ entryCount:built.entryCount,
2283
+ excludedModules:built.excludedModules,
2284
+ files:committedFiles,
2285
+ cleanupWarnings,
2286
+ committed:true
2287
+ });
2288
+ try{
2289
+ await emit(onEvent,{
2290
+ type:'import-map.completed',
2291
+ appId,
2292
+ artifactPath,
2293
+ entryPath,
2294
+ entryCount:receipt.entryCount,
2295
+ cleanupWarnings:receipt.cleanupWarnings,
2296
+ committed:true
2297
+ });
2298
+ }catch(error){
2299
+ return Object.freeze({
2300
+ ...receipt,
2301
+ eventDelivery:Object.freeze({
2302
+ status:'degraded',
2303
+ errorCode:'ARCANE_EVENT_DELIVERY_FAILED',
2304
+ message:String(error?.message??error)
2305
+ })
2306
+ });
2307
+ }
2308
+ return receipt;
2309
+ }
2310
+
2311
+ export async function generateImportMap(options={}){
2312
+ const {
2313
+ workspaceRoot,
2314
+ signal,
2315
+ onEvent,
2316
+ workspaceOperationLease
2317
+ }=options??{};
2318
+ if(typeof workspaceRoot!=='string'||workspaceRoot.trim()===''){
2319
+ throw new TypeError('generateImportMap workspaceRoot must be a nonempty string.');
2320
+ }
2321
+ return withWorkspaceOperationLock({
2322
+ workspaceRoot,
2323
+ operation:'import-map',
2324
+ signal,
2325
+ onEvent,
2326
+ workspaceOperationLease
2327
+ },()=>generateImportMapUnlocked(options));
2328
+ }