arcane-os 0.52.1 → 0.54.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +42 -0
- package/NOTICE +4 -1
- package/README.md +1 -1
- package/bin/arcane-test.mjs +2 -1
- package/browser-runtime/ai/browser-decisions.mjs +405 -0
- package/browser-runtime/ai/decision-runtime.mjs +281 -0
- package/browser-runtime/ai/decision-worker.mjs +67 -0
- package/browser-runtime/dependencies/event-pubsub/dependencies/strong-type/index.js +1151 -0
- package/browser-runtime/dependencies/event-pubsub/dependencies/strong-type/licence +21 -0
- package/browser-runtime/dependencies/event-pubsub/dependencies/strong-type/package.json +61 -0
- package/browser-runtime/dependencies/event-pubsub/index.js +2 -2
- package/browser-runtime/dependencies/event-pubsub/package.json +11 -4
- package/browser-runtime/dependencies/ws-share/WS.js +128 -0
- package/browser-runtime/dependencies/ws-share/licence.md +27 -0
- package/browser-runtime/dependencies/ws-share/package.json +64 -0
- package/browser-runtime/websocket-client.mjs +1 -0
- package/docs/event-manager.md +7 -4
- package/docs/reference/ai/browser-decisions.md +184 -0
- package/docs/reference/inventory/package-api.json +44 -2
- package/docs/reference/inventory/runtime-modules.json +17 -4
- package/docs/reference/protocols.md +76 -1
- package/docs/reference/runtime-modules.md +45 -2
- package/docs/reference/sdk-api.md +280 -6
- package/docs/reference/websocket-client.md +132 -0
- package/node_modules/event-pubsub/CHANGELOG.md +12 -1
- package/node_modules/event-pubsub/MIGRATION.md +8 -5
- package/node_modules/event-pubsub/README.md +75 -19
- package/node_modules/event-pubsub/SECURITY.md +3 -3
- package/node_modules/event-pubsub/index.js +2 -2
- package/node_modules/event-pubsub/package.json +11 -4
- package/package.json +9 -5
- package/runtime/arcane/modules/HTMLImport.js +81 -26
- package/runtime/arcane/modules/HTMLImportScript.js +3 -0
- package/src/import-map.mjs +416 -59
- package/src/index.mjs +2 -0
- package/src/packager/core.mjs +24 -6
- package/src/packager/native-modules.mjs +458 -0
- package/src/testing-loader.mjs +41 -20
- package/src/websocket.mjs +479 -0
package/src/index.mjs
CHANGED
|
@@ -146,6 +146,8 @@ export {
|
|
|
146
146
|
export {materializeInstalledSdkRuntime} from './installed-sdk-runtime.mjs';
|
|
147
147
|
export {startDevServer} from './dev-server.mjs';
|
|
148
148
|
export {startSourceExampleServer} from './source-server.mjs';
|
|
149
|
+
export {acceptWebSocket} from './websocket.mjs';
|
|
150
|
+
export {WS} from '../browser-runtime/websocket-client.mjs';
|
|
149
151
|
export {
|
|
150
152
|
NATIVE_BUILD_PLAN_PROTOCOL,
|
|
151
153
|
NATIVE_BUILDER_PROTOCOL,
|
package/src/packager/core.mjs
CHANGED
|
@@ -15,6 +15,7 @@ import {pathToFileURL} from 'node:url';
|
|
|
15
15
|
import {appRelativeRoot,resolveAppRoot,resolvePackageOutputRoot} from '../app-layout.mjs';
|
|
16
16
|
import {readInstalledSdkLayout} from '../sdk-runtime-layout.mjs';
|
|
17
17
|
import {withWorkspaceOperationLock} from '../workspace-operation-lock.mjs';
|
|
18
|
+
import {materializeNativeModules} from './native-modules.mjs';
|
|
18
19
|
import {
|
|
19
20
|
applyPwaEntryReferences,
|
|
20
21
|
inspectImportMapHtml,
|
|
@@ -421,7 +422,7 @@ async function assertContainedRealPath(root,candidate,label){
|
|
|
421
422
|
}
|
|
422
423
|
}
|
|
423
424
|
|
|
424
|
-
async function loadContext(requestedWorkspaceRoot,appId){
|
|
425
|
+
async function loadContext(requestedWorkspaceRoot,appId,{outputDirectory}={}){
|
|
425
426
|
const workspaceRoot=await realDirectory(normalizeWorkspaceRoot(requestedWorkspaceRoot),'Workspace root');
|
|
426
427
|
const rootConfigPath=path.join(workspaceRoot,ROOT_CONFIG_NAME);
|
|
427
428
|
const rootConfig=validateRootConfig(await readJson(rootConfigPath,ROOT_CONFIG_NAME),rootConfigPath);
|
|
@@ -431,6 +432,9 @@ async function loadContext(requestedWorkspaceRoot,appId){
|
|
|
431
432
|
await assertContainedRealPath(appsRoot,appRoot,appId);
|
|
432
433
|
const configPath=path.join(appRoot,APP_CONFIG_NAME);
|
|
433
434
|
const config=validateAppConfig(await readJson(configPath,`${appId}/${APP_CONFIG_NAME}`),appId,rootConfig,configPath);
|
|
435
|
+
if(outputDirectory!==undefined){
|
|
436
|
+
config.outputDirectory=normalizeRelativePath(outputDirectory,'package outputDirectory');
|
|
437
|
+
}
|
|
434
438
|
return {
|
|
435
439
|
workspaceRoot,
|
|
436
440
|
rootConfig,
|
|
@@ -841,7 +845,10 @@ async function replaceDirectory(stagingRoot,outputRoot){
|
|
|
841
845
|
}
|
|
842
846
|
|
|
843
847
|
async function packageWithContext(context,options={}){
|
|
844
|
-
const {signal,onEvent,browserPwa=true}=options;
|
|
848
|
+
const {signal,onEvent,browserPwa=true,moduleFormat='import-map'}=options;
|
|
849
|
+
if(!['import-map','native'].includes(moduleFormat)){
|
|
850
|
+
throw new TypeError('packageApp moduleFormat must be "import-map" or "native".');
|
|
851
|
+
}
|
|
845
852
|
const pwaEnabled=browserPwa&&context.config.pwa?.enabled===true;
|
|
846
853
|
const appPath=appRelativeRoot(context.rootConfig,context.appId);
|
|
847
854
|
const entryPath=appPackagePath(context,context.config.entry);
|
|
@@ -912,7 +919,18 @@ async function packageWithContext(context,options={}){
|
|
|
912
919
|
}else{
|
|
913
920
|
await copyBase();
|
|
914
921
|
}
|
|
915
|
-
|
|
922
|
+
let files=await listOutputFiles(stagingRoot,{signal});
|
|
923
|
+
if(moduleFormat==='native'){
|
|
924
|
+
files=await materializeNativeModules({
|
|
925
|
+
stagingRoot,
|
|
926
|
+
files,
|
|
927
|
+
documents:inspected.browserDocuments.map(function documentPath(document){return document.packagePath;}),
|
|
928
|
+
sharedFiles:records.filter(function sharedRecord(record){return record.appRelativePath===undefined;})
|
|
929
|
+
.map(function sharedPath(record){return record.destination;}),
|
|
930
|
+
signal,
|
|
931
|
+
onEvent
|
|
932
|
+
});
|
|
933
|
+
}
|
|
916
934
|
// Traverse actual browser resources after the adapter finishes. Files
|
|
917
935
|
// included only as application documents retain their original content.
|
|
918
936
|
const assetVersion=await readWorkspaceAssetVersion(context.workspaceRoot);
|
|
@@ -1114,7 +1132,7 @@ async function packageWithContext(context,options={}){
|
|
|
1114
1132
|
}
|
|
1115
1133
|
|
|
1116
1134
|
export async function packageApp(options={}){
|
|
1117
|
-
const context=await loadContext(options.workspaceRoot,options.appId);
|
|
1135
|
+
const context=await loadContext(options.workspaceRoot,options.appId,options);
|
|
1118
1136
|
const execute=()=>packageWithContext(context,options);
|
|
1119
1137
|
if(options.workspaceOperationLease)return execute();
|
|
1120
1138
|
return withWorkspaceOperationLock({
|
|
@@ -1125,9 +1143,9 @@ export async function packageApp(options={}){
|
|
|
1125
1143
|
},execute);
|
|
1126
1144
|
}
|
|
1127
1145
|
|
|
1128
|
-
export async function verifyApp({workspaceRoot,appId,signal,onEvent}={}){
|
|
1146
|
+
export async function verifyApp({workspaceRoot,appId,outputDirectory,signal,onEvent}={}){
|
|
1129
1147
|
throwIfAborted(signal);
|
|
1130
|
-
const context=await loadContext(workspaceRoot,appId);
|
|
1148
|
+
const context=await loadContext(workspaceRoot,appId,{outputDirectory});
|
|
1131
1149
|
const outputRoot=await realDirectory(context.outputRoot,'Package output');
|
|
1132
1150
|
const manifest=await readJson(path.join(outputRoot,RELEASE_MANIFEST_NAME),RELEASE_MANIFEST_NAME);
|
|
1133
1151
|
if(!isPlainObject(manifest)||manifest.schemaVersion!==1||manifest.kind!=='arcane-app-release'
|
|
@@ -0,0 +1,458 @@
|
|
|
1
|
+
import Is from 'strong-type';
|
|
2
|
+
import {mkdir,readFile,writeFile} from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import {
|
|
5
|
+
applyNativeReferenceEdits,
|
|
6
|
+
nativeModuleHtml,
|
|
7
|
+
resolveAssetReference,
|
|
8
|
+
rewriteNativeJavaScript,
|
|
9
|
+
rewriteNativeSrcset,
|
|
10
|
+
rewriteNativeStylesheet
|
|
11
|
+
} from '../import-map.mjs';
|
|
12
|
+
import {createHTMLImportScript} from '../../runtime/arcane/modules/HTMLImportScript.js';
|
|
13
|
+
|
|
14
|
+
const is=new Is(false);
|
|
15
|
+
const packageOrigin='https://arcane.invalid/';
|
|
16
|
+
const executableTypes=new Set([
|
|
17
|
+
'', 'module', 'application/ecmascript', 'application/javascript',
|
|
18
|
+
'application/x-ecmascript', 'application/x-javascript', 'text/ecmascript',
|
|
19
|
+
'text/javascript', 'text/javascript1.0', 'text/javascript1.1',
|
|
20
|
+
'text/javascript1.2', 'text/javascript1.3', 'text/javascript1.4',
|
|
21
|
+
'text/javascript1.5', 'text/jscript', 'text/livescript',
|
|
22
|
+
'text/x-ecmascript', 'text/x-javascript'
|
|
23
|
+
]);
|
|
24
|
+
|
|
25
|
+
function packageUrl(file){
|
|
26
|
+
return new URL(file.split('/').map(encodeURIComponent).join('/'),packageOrigin);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function fileAt(url){
|
|
30
|
+
return url.origin===new URL(packageOrigin).origin
|
|
31
|
+
?decodeURIComponent(url.pathname).substring(1):null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function relativeUrl(target,base){
|
|
35
|
+
if(target.origin!==base.origin)return target.href;
|
|
36
|
+
const relative=path.posix.relative(new URL('.',base).pathname,target.pathname);
|
|
37
|
+
const pathname=relative==='..'||relative.startsWith('../')?relative:`./${relative}`;
|
|
38
|
+
const trailing=target.pathname.endsWith('/')&&!pathname.endsWith('/')?'/':'';
|
|
39
|
+
return `${pathname}${trailing}${target.search}${target.hash}`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function urlSpecifier(value,base){
|
|
43
|
+
if(!/^(?:\.{1,2}\/|\/|[A-Za-z][A-Za-z0-9+.-]*:)/u.test(value))return null;
|
|
44
|
+
try{return new URL(value,base).href;}
|
|
45
|
+
catch{return null;}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function importEntries(entries,base){
|
|
49
|
+
const result=new Map();
|
|
50
|
+
for(const [key,value] of Object.entries(entries??{})){
|
|
51
|
+
const normalized=urlSpecifier(key,base)??key;
|
|
52
|
+
const target=is.string(value)?urlSpecifier(value,base):null;
|
|
53
|
+
result.set(normalized,key.endsWith('/')&&target!==null&&!target.endsWith('/')?null:target);
|
|
54
|
+
}
|
|
55
|
+
return result;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function documentMap(source,structure,base){
|
|
59
|
+
const imports=new Map();
|
|
60
|
+
const scopes=new Map();
|
|
61
|
+
for(const script of structure.scripts){
|
|
62
|
+
if(script.attribute('type').trim().toLowerCase()!=='importmap')continue;
|
|
63
|
+
const value=JSON.parse(source.substring(script.openEnd,script.contentEnd));
|
|
64
|
+
for(const [key,target] of importEntries(value.imports,base)){
|
|
65
|
+
if(!imports.has(key))imports.set(key,target);
|
|
66
|
+
}
|
|
67
|
+
for(const [scope,entries] of Object.entries(value.scopes??{})){
|
|
68
|
+
const address=new URL(scope,base).href;
|
|
69
|
+
if(!scopes.has(address))scopes.set(address,new Map());
|
|
70
|
+
const selected=scopes.get(address);
|
|
71
|
+
for(const [key,target] of importEntries(entries,base)){
|
|
72
|
+
if(!selected.has(key))selected.set(key,target);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return {imports,scopes};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function mappedSpecifier(entries,specifier,asUrl){
|
|
80
|
+
if(entries.has(specifier))return {matched:true,target:entries.get(specifier)};
|
|
81
|
+
if(asUrl!==null&&!['http:','https:','file:','ftp:','ws:','wss:'].includes(new URL(asUrl).protocol)){
|
|
82
|
+
return {matched:false};
|
|
83
|
+
}
|
|
84
|
+
const prefixes=[...entries.keys()].filter(function matchingPrefix(key){
|
|
85
|
+
return key.endsWith('/')&&specifier.startsWith(key);
|
|
86
|
+
}).sort(function longestFirst(left,right){return right.length-left.length;});
|
|
87
|
+
if(prefixes.length===0)return {matched:false};
|
|
88
|
+
const prefix=prefixes[0];
|
|
89
|
+
const target=entries.get(prefix);
|
|
90
|
+
const resolved=target===null?null:new URL(specifier.substring(prefix.length),target).href;
|
|
91
|
+
if(resolved!==null&&!resolved.startsWith(target)){
|
|
92
|
+
throw new TypeError(`Import ${specifier} traverses above its import-map prefix ${prefix}.`);
|
|
93
|
+
}
|
|
94
|
+
return {
|
|
95
|
+
matched:true,
|
|
96
|
+
target:resolved
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function resolveModule(specifier,importer,map){
|
|
101
|
+
const asUrl=urlSpecifier(specifier,importer);
|
|
102
|
+
const normalized=asUrl??specifier;
|
|
103
|
+
const scopes=[...map.scopes.keys()].filter(function applicableScope(scope){
|
|
104
|
+
return importer.href===scope||(scope.endsWith('/')&&importer.href.startsWith(scope));
|
|
105
|
+
}).sort(function mostSpecificFirst(left,right){return right.length-left.length;});
|
|
106
|
+
for(const scope of [...scopes,null]){
|
|
107
|
+
const result=mappedSpecifier(scope===null?map.imports:map.scopes.get(scope),normalized,asUrl);
|
|
108
|
+
if(!result.matched)continue;
|
|
109
|
+
if(result.target===null)throw new TypeError(`Import map blocks ${specifier} in ${importer.pathname}.`);
|
|
110
|
+
return new URL(result.target);
|
|
111
|
+
}
|
|
112
|
+
const address=urlSpecifier(specifier,importer);
|
|
113
|
+
if(address!==null)return new URL(address);
|
|
114
|
+
throw new TypeError(`Native module output cannot resolve ${specifier} in ${importer.pathname}.`);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function escapeAttribute(value){
|
|
118
|
+
return value.replaceAll('&','&').replaceAll('"','"')
|
|
119
|
+
.replaceAll("'",''').replaceAll('<','<').replaceAll('>','>');
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function setAttribute(element,name,value){
|
|
123
|
+
const position=element.attributes.positions.get(name);
|
|
124
|
+
if(position){
|
|
125
|
+
return {
|
|
126
|
+
start:element.start+position.start,
|
|
127
|
+
end:element.start+position.end,
|
|
128
|
+
value:position.quote?escapeAttribute(value)
|
|
129
|
+
:`${position.assigned?'':'='}"${escapeAttribute(value)}"`
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
const close=element.open.lastIndexOf('>');
|
|
133
|
+
const insertion=element.open[close-1]==='/'?close-1:close;
|
|
134
|
+
return {start:element.start+insertion,end:element.start+insertion,value:` ${name}="${escapeAttribute(value)}"`};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function scriptPath(file,index,module=false){
|
|
138
|
+
return `${file}.arcane-script-${index}.${module?'mjs':'js'}`;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function removeAttribute(element,name){
|
|
142
|
+
const position=element.attributes.positions.get(name);
|
|
143
|
+
return position?[position,...(position.additional??[])].map(function removeOccurrence(item){
|
|
144
|
+
return {
|
|
145
|
+
start:element.start+item.nameEnd-name.length,
|
|
146
|
+
end:element.start+(item.assigned?item.end+(item.quote?1:0):item.nameEnd),
|
|
147
|
+
value:''
|
|
148
|
+
};
|
|
149
|
+
}):[];
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** Materialize a native URL graph in the existing package stage, never in source. */
|
|
153
|
+
export async function materializeNativeModules({stagingRoot,files,documents,sharedFiles,signal,onEvent}){
|
|
154
|
+
const inventory=new Set(files);
|
|
155
|
+
const sharedComponents=new Set(sharedFiles.filter(function componentFile(file){
|
|
156
|
+
return /(?:^|\/)components\/.*\.html?$/iu.test(file);
|
|
157
|
+
}));
|
|
158
|
+
const sourceReads=new Map();
|
|
159
|
+
const nodes=[];
|
|
160
|
+
const contexts=[];
|
|
161
|
+
const nodeIndex=new Map();
|
|
162
|
+
|
|
163
|
+
async function readSource(file){
|
|
164
|
+
if(!sourceReads.has(file)){
|
|
165
|
+
sourceReads.set(file,readFile(path.join(stagingRoot,...file.split('/')),'utf8'));
|
|
166
|
+
}
|
|
167
|
+
return sourceReads.get(file);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function getNode(kind,address,context,{source,sourceKey,destination,importer}={}){
|
|
171
|
+
const file=fileAt(address);
|
|
172
|
+
if(file===null||(!inventory.has(file)&&source===undefined))return null;
|
|
173
|
+
const key=`${context.id}\n${kind}\n${sourceKey??address.href}`;
|
|
174
|
+
if(nodeIndex.has(key))return nodeIndex.get(key);
|
|
175
|
+
const node={
|
|
176
|
+
id:nodes.length,kind,address,context,file,source,
|
|
177
|
+
sourceKey:`${kind}\n${sourceKey??file}`,
|
|
178
|
+
desired:destination??file,
|
|
179
|
+
destination:destination??file,
|
|
180
|
+
importer:importer??address,
|
|
181
|
+
edges:[]
|
|
182
|
+
};
|
|
183
|
+
nodes.push(node);
|
|
184
|
+
nodeIndex.set(key,node);
|
|
185
|
+
return node;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
for(const file of [...new Set(documents)]){
|
|
189
|
+
signal?.throwIfAborted();
|
|
190
|
+
const source=await readSource(file);
|
|
191
|
+
const structure=nativeModuleHtml(source);
|
|
192
|
+
const address=packageUrl(file);
|
|
193
|
+
const baseElement=structure.bases.find(function firstHref(element){return element.attributes.has('href');});
|
|
194
|
+
const base=baseElement?new URL(baseElement.attribute('href'),address):address;
|
|
195
|
+
const context={id:contexts.length,address,base,map:documentMap(source,structure,base)};
|
|
196
|
+
contexts.push(context);
|
|
197
|
+
getNode('document',address,context,{source});
|
|
198
|
+
}
|
|
199
|
+
if(contexts.length===0)return files;
|
|
200
|
+
|
|
201
|
+
function resourceEdge(node,reference){
|
|
202
|
+
const module=reference.kind==='import'||reference.kind==='import-resolve';
|
|
203
|
+
const documentBase=node.kind==='worker'?node.address:node.context.base;
|
|
204
|
+
const target=module?resolveModule(reference.url,node.importer,node.context.map)
|
|
205
|
+
:resolveAssetReference(reference,{
|
|
206
|
+
ownerUrl:node.importer,
|
|
207
|
+
documentUrl:documentBase
|
|
208
|
+
});
|
|
209
|
+
let kind=null;
|
|
210
|
+
if(module||reference.kind==='script')kind=reference.kind==='script'?'worker':'script';
|
|
211
|
+
if(reference.kind==='component')kind='component';
|
|
212
|
+
if(['asset','fetch'].includes(reference.kind)&&sharedComponents.has(fileAt(target))){
|
|
213
|
+
kind='component';
|
|
214
|
+
}
|
|
215
|
+
if(reference.kind==='style'||/\.css(?:[?#]|$)/iu.test(target.href))kind='style';
|
|
216
|
+
const dependency=kind?getNode(kind,target,node.context):null;
|
|
217
|
+
return {
|
|
218
|
+
target,dependency,
|
|
219
|
+
baseKind:module||node.kind==='worker'?undefined:reference.baseKind
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function edgeUrl(node,edge){
|
|
224
|
+
const target=new URL(edge.target);
|
|
225
|
+
if(edge.dependency){
|
|
226
|
+
target.pathname=packageUrl(edge.dependency.destination).pathname;
|
|
227
|
+
}
|
|
228
|
+
const base=edge.baseKind==='document'?node.context.base:packageUrl(node.destination);
|
|
229
|
+
return relativeUrl(target,base);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function htmlRenderer(node,source){
|
|
233
|
+
const structure=nativeModuleHtml(source);
|
|
234
|
+
const component=node.kind==='component';
|
|
235
|
+
const scriptElements=new Set(structure.scripts.map(function scriptStart(script){return script.start;}));
|
|
236
|
+
const scriptPlans=[];
|
|
237
|
+
for(const [index,script] of structure.scripts.entries()){
|
|
238
|
+
const type=script.attribute('type').trim().toLowerCase();
|
|
239
|
+
if(type==='importmap'){
|
|
240
|
+
scriptPlans.push({script,remove:true});
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
if(!executableTypes.has(type))continue;
|
|
244
|
+
const src=script.attribute('src');
|
|
245
|
+
const hasSource=script.attributes.has('src');
|
|
246
|
+
if(!component&&hasSource){
|
|
247
|
+
scriptPlans.push({script,reference:{url:src,kind:'document-script',baseKind:'document'}});
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
if(!component&&node.context.base.origin!==new URL(packageOrigin).origin){
|
|
251
|
+
throw new TypeError(
|
|
252
|
+
`Native module output cannot externalize an inline script under a remote document base: ${node.file}`
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
const sourceAddress=hasSource?resolveAssetReference({
|
|
256
|
+
url:src,
|
|
257
|
+
baseKind:src.startsWith('./arcane/')?'component-runtime':undefined
|
|
258
|
+
},{ownerUrl:node.address,documentUrl:node.context.base})
|
|
259
|
+
:component?node.address:node.context.base;
|
|
260
|
+
const sourceFile=fileAt(sourceAddress);
|
|
261
|
+
scriptPlans.push({
|
|
262
|
+
script,index,sourceAddress,sourceFile,
|
|
263
|
+
body:hasSource?null:source.substring(script.openEnd,script.contentEnd),
|
|
264
|
+
module:type==='module'
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
return {structure,scriptPlans,scriptElements,component};
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
for(let index=0;index<nodes.length;index+=1){
|
|
271
|
+
signal?.throwIfAborted();
|
|
272
|
+
const node=nodes[index];
|
|
273
|
+
node.source??=await readSource(node.file);
|
|
274
|
+
if(node.kind==='script'||node.kind==='worker'){
|
|
275
|
+
node.render=function renderScript(reference){return rewriteNativeJavaScript(node.source,reference);};
|
|
276
|
+
}else if(node.kind==='style'){
|
|
277
|
+
node.render=function renderStyle(reference){return rewriteNativeStylesheet(node.source,reference);};
|
|
278
|
+
}else{
|
|
279
|
+
const {structure,scriptPlans,scriptElements,component}=htmlRenderer(node,node.source);
|
|
280
|
+
for(const plan of scriptPlans){
|
|
281
|
+
if(plan.remove||plan.reference)continue;
|
|
282
|
+
if(plan.body===null){
|
|
283
|
+
if(plan.sourceFile===null||!inventory.has(plan.sourceFile)){
|
|
284
|
+
throw new TypeError(`Packaged component script is unavailable: ${plan.sourceAddress.href}`);
|
|
285
|
+
}
|
|
286
|
+
plan.body=await readSource(plan.sourceFile);
|
|
287
|
+
}
|
|
288
|
+
plan.dependency=getNode('script',node.address,node.context,{
|
|
289
|
+
source:component?createHTMLImportScript(plan.body):plan.body,
|
|
290
|
+
sourceKey:`${node.kind}:${node.file}#script-${plan.index}`,
|
|
291
|
+
destination:scriptPath(node.file,plan.index,!component&&plan.module),
|
|
292
|
+
importer:plan.sourceAddress
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
node.render=function renderHtml(reference){
|
|
296
|
+
const edits=[];
|
|
297
|
+
for(const plan of scriptPlans){
|
|
298
|
+
const {script}=plan;
|
|
299
|
+
if(plan.remove){
|
|
300
|
+
edits.push({start:script.start,end:script.end,value:''});
|
|
301
|
+
continue;
|
|
302
|
+
}
|
|
303
|
+
let url;
|
|
304
|
+
if(plan.reference){
|
|
305
|
+
url=reference(plan.reference);
|
|
306
|
+
}else{
|
|
307
|
+
url=reference({
|
|
308
|
+
url:plan.sourceAddress.href,
|
|
309
|
+
kind:'generated-script',
|
|
310
|
+
dependency:plan.dependency,
|
|
311
|
+
baseKind:component?undefined:'document'
|
|
312
|
+
});
|
|
313
|
+
edits.push({start:script.openEnd,end:script.contentEnd,value:''});
|
|
314
|
+
}
|
|
315
|
+
edits.push(setAttribute(script,'src',url));
|
|
316
|
+
if(component)edits.push(setAttribute(script,'data-arcane-packaged-script',''));
|
|
317
|
+
else if(!plan.reference&&!plan.module){
|
|
318
|
+
// Inline classic bodies are parser-blocking even with these attributes.
|
|
319
|
+
// Their external form must retain that effective scheduling.
|
|
320
|
+
for(const name of ['async','defer']){
|
|
321
|
+
edits.push(...removeAttribute(script,name));
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
for(const element of structure.elements){
|
|
326
|
+
if(scriptElements.has(element.start))continue;
|
|
327
|
+
if(element.tag==='base'&&element.attributes.has('href')){
|
|
328
|
+
const target=new URL(element.attribute('href'),node.address);
|
|
329
|
+
edits.push(setAttribute(element,'href',relativeUrl(target,packageUrl(node.destination))));
|
|
330
|
+
continue;
|
|
331
|
+
}
|
|
332
|
+
const attributes=[];
|
|
333
|
+
if(element.tag==='html-import')attributes.push(['href','component']);
|
|
334
|
+
else if(['img','audio','video','source','track','iframe','embed','input'].includes(element.tag)){
|
|
335
|
+
attributes.push(['src','asset']);
|
|
336
|
+
}
|
|
337
|
+
if(element.tag==='video')attributes.push(['poster','asset']);
|
|
338
|
+
if(element.tag==='object')attributes.push(['data','asset']);
|
|
339
|
+
if(element.tag==='link'){
|
|
340
|
+
const relationships=element.attribute('rel').trim().toLowerCase().split(/\s+/u);
|
|
341
|
+
const kind=relationships.includes('stylesheet')?'style'
|
|
342
|
+
:relationships.includes('modulepreload')?'document-script':'asset';
|
|
343
|
+
attributes.push(['href',kind]);
|
|
344
|
+
}
|
|
345
|
+
if(['img','source'].includes(element.tag)&&element.attributes.has('srcset')){
|
|
346
|
+
const original=element.attribute('srcset');
|
|
347
|
+
const value=rewriteNativeSrcset(original,function nativeSrcsetReference(item){
|
|
348
|
+
return reference({...item,baseKind:'document'});
|
|
349
|
+
});
|
|
350
|
+
if(value!==original)edits.push(setAttribute(element,'srcset',value));
|
|
351
|
+
}
|
|
352
|
+
if(element.attributes.has('style')){
|
|
353
|
+
const original=element.attribute('style');
|
|
354
|
+
const value=rewriteNativeStylesheet(original,function inlineAttributeStyle(item){
|
|
355
|
+
return reference({...item,baseKind:'document'});
|
|
356
|
+
});
|
|
357
|
+
if(value!==original)edits.push(setAttribute(element,'style',value));
|
|
358
|
+
}
|
|
359
|
+
for(const [attribute,kind] of attributes){
|
|
360
|
+
if(!element.attributes.has(attribute))continue;
|
|
361
|
+
const url=element.attribute(attribute);
|
|
362
|
+
const value=reference({
|
|
363
|
+
url,kind,
|
|
364
|
+
baseKind:component&&url.startsWith('./arcane/')?'component-runtime':'document'
|
|
365
|
+
});
|
|
366
|
+
if(value!==url)edits.push(setAttribute(element,attribute,value));
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
for(const style of structure.styles){
|
|
370
|
+
const body=node.source.substring(style.start,style.end);
|
|
371
|
+
const content=rewriteNativeStylesheet(body,function inlineStyleReference(value){
|
|
372
|
+
return reference({
|
|
373
|
+
...value,
|
|
374
|
+
baseKind:component&&value.url.startsWith('./arcane/')?'component-runtime':'document'
|
|
375
|
+
});
|
|
376
|
+
});
|
|
377
|
+
if(content!==body)edits.push({start:style.start,end:style.end,value:content});
|
|
378
|
+
}
|
|
379
|
+
return applyNativeReferenceEdits(node.source,edits);
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
node.render(function collectReference(reference){
|
|
383
|
+
let edge;
|
|
384
|
+
if(reference.dependency){
|
|
385
|
+
edge={target:new URL(reference.url),dependency:reference.dependency,baseKind:reference.baseKind};
|
|
386
|
+
}else if(reference.kind==='document-script'){
|
|
387
|
+
const target=new URL(reference.url,node.context.base);
|
|
388
|
+
edge={target,dependency:getNode('script',target,node.context),baseKind:'document'};
|
|
389
|
+
}else{
|
|
390
|
+
edge=resourceEdge(node,reference);
|
|
391
|
+
// Fragment attributes are attached to the host document, not the fetched HTML.
|
|
392
|
+
if(reference.baseKind==='component-runtime')edge.baseKind='document';
|
|
393
|
+
}
|
|
394
|
+
node.edges.push(edge);
|
|
395
|
+
return reference.url;
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// Refine graph equivalence, including cycles, before assigning output paths.
|
|
400
|
+
// Documents with different maps share files only when their resolved graphs agree.
|
|
401
|
+
let groups=[];
|
|
402
|
+
const initial=new Map();
|
|
403
|
+
for(const node of nodes){
|
|
404
|
+
if(!initial.has(node.sourceKey))initial.set(node.sourceKey,initial.size);
|
|
405
|
+
node.group=initial.get(node.sourceKey);
|
|
406
|
+
}
|
|
407
|
+
let changed=true;
|
|
408
|
+
while(changed){
|
|
409
|
+
const partitions=new Map();
|
|
410
|
+
groups=[];
|
|
411
|
+
const next=[];
|
|
412
|
+
for(const node of nodes){
|
|
413
|
+
const signature=JSON.stringify([
|
|
414
|
+
node.group,
|
|
415
|
+
node.edges.map(function edgeSignature(edge){
|
|
416
|
+
return [edge.dependency?.group??null,edge.target.href,edge.baseKind,
|
|
417
|
+
edge.baseKind==='document'?node.context.base.href:null];
|
|
418
|
+
})
|
|
419
|
+
]);
|
|
420
|
+
if(!partitions.has(signature)){
|
|
421
|
+
partitions.set(signature,groups.length);
|
|
422
|
+
groups.push([]);
|
|
423
|
+
}
|
|
424
|
+
const group=partitions.get(signature);
|
|
425
|
+
groups[group].push(node);
|
|
426
|
+
next.push(group);
|
|
427
|
+
}
|
|
428
|
+
changed=nodes.some(function partitionChanged(node,index){return node.group!==next[index];});
|
|
429
|
+
for(const [index,node] of nodes.entries())node.group=next[index];
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
const assigned=new Set();
|
|
433
|
+
for(const group of groups){
|
|
434
|
+
const node=group[0];
|
|
435
|
+
let destination=node.desired;
|
|
436
|
+
let variant=1;
|
|
437
|
+
while(assigned.has(destination)||(inventory.has(destination)&&destination!==node.file)){
|
|
438
|
+
const extension=path.posix.extname(node.desired);
|
|
439
|
+
const stem=node.desired.substring(0,node.desired.length-extension.length);
|
|
440
|
+
destination=`${stem}.arcane-context-${variant++}${extension}`;
|
|
441
|
+
}
|
|
442
|
+
assigned.add(destination);
|
|
443
|
+
for(const member of group)member.destination=destination;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
for(const group of groups){
|
|
447
|
+
signal?.throwIfAborted();
|
|
448
|
+
const node=group[0];
|
|
449
|
+
let index=0;
|
|
450
|
+
const content=node.render(function nativeReference(){return edgeUrl(node,node.edges[index++]);});
|
|
451
|
+
const destination=path.join(stagingRoot,...node.destination.split('/'));
|
|
452
|
+
await mkdir(path.dirname(destination),{recursive:true});
|
|
453
|
+
await writeFile(destination,content,'utf8');
|
|
454
|
+
inventory.add(node.destination);
|
|
455
|
+
if(is.function(onEvent))await onEvent({type:'package.native-module.written',path:node.destination});
|
|
456
|
+
}
|
|
457
|
+
return [...inventory].sort();
|
|
458
|
+
}
|
package/src/testing-loader.mjs
CHANGED
|
@@ -15,6 +15,7 @@ const MANAGED_IMPORT_MAP_BOUNDARIES=new Set(['source','dist','test']);
|
|
|
15
15
|
|
|
16
16
|
let managedImports=null;
|
|
17
17
|
let managedUrlImports=null;
|
|
18
|
+
let managedScopes=[];
|
|
18
19
|
|
|
19
20
|
function loaderFailure(message,code='ARCANE_IMPORT_MAP_INVALID'){
|
|
20
21
|
const error=new Error(message);
|
|
@@ -109,19 +110,7 @@ function physicalManagedTarget(target,specifier,boundary,boundaryName){
|
|
|
109
110
|
return targetURL.href;
|
|
110
111
|
}
|
|
111
112
|
|
|
112
|
-
|
|
113
|
-
if(managedImportMap==null){
|
|
114
|
-
managedImports=null;
|
|
115
|
-
managedUrlImports=null;
|
|
116
|
-
return;
|
|
117
|
-
}
|
|
118
|
-
const imports=managedImportMap.imports;
|
|
119
|
-
if(managedImportMap.protocol!==MANAGED_IMPORT_MAP_PROTOCOL
|
|
120
|
-
||!MANAGED_IMPORT_MAP_BOUNDARIES.has(managedImportMap.boundary)
|
|
121
|
-
||imports===null||!is.object(imports)||is.array(imports)){
|
|
122
|
-
loaderFailure('Managed test import-map loader data is malformed.');
|
|
123
|
-
}
|
|
124
|
-
const boundary=physicalDirectory(managedImportMap.baseURL);
|
|
113
|
+
function managedBindings(imports,boundary,boundaryName){
|
|
125
114
|
const exact=new Map();
|
|
126
115
|
const urls=new Map();
|
|
127
116
|
for(const [specifier,target] of Object.entries(imports)){
|
|
@@ -132,7 +121,7 @@ export function initialize({managedImportMap}={}){
|
|
|
132
121
|
target,
|
|
133
122
|
specifier,
|
|
134
123
|
boundary,
|
|
135
|
-
|
|
124
|
+
boundaryName
|
|
136
125
|
);
|
|
137
126
|
exact.set(specifier,targetURL);
|
|
138
127
|
if(urlLikeSpecifier(specifier)){
|
|
@@ -145,20 +134,52 @@ export function initialize({managedImportMap}={}){
|
|
|
145
134
|
urls.set(keyURL,targetURL);
|
|
146
135
|
}
|
|
147
136
|
}
|
|
148
|
-
|
|
149
|
-
|
|
137
|
+
return {exact,urls};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function initialize({managedImportMap}={}){
|
|
141
|
+
if(managedImportMap==null){
|
|
142
|
+
managedImports=null;
|
|
143
|
+
managedUrlImports=null;
|
|
144
|
+
managedScopes=[];
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
const imports=managedImportMap.imports;
|
|
148
|
+
if(managedImportMap.protocol!==MANAGED_IMPORT_MAP_PROTOCOL
|
|
149
|
+
||!MANAGED_IMPORT_MAP_BOUNDARIES.has(managedImportMap.boundary)
|
|
150
|
+
||imports===null||!is.object(imports)||is.array(imports)){
|
|
151
|
+
loaderFailure('Managed test import-map loader data is malformed.');
|
|
152
|
+
}
|
|
153
|
+
const boundary=physicalDirectory(managedImportMap.baseURL);
|
|
154
|
+
const bindings=managedBindings(imports,boundary,managedImportMap.boundary);
|
|
155
|
+
const scopes=Object.entries(managedImportMap.scopes??{}).map(([scope,entries])=>({
|
|
156
|
+
url:new URL(scope,boundary.url).href,
|
|
157
|
+
...managedBindings(entries,boundary,managedImportMap.boundary)
|
|
158
|
+
})).sort((left,right)=>right.url.length-left.url.length);
|
|
159
|
+
managedImports=bindings.exact;
|
|
160
|
+
managedUrlImports=bindings.urls;
|
|
161
|
+
managedScopes=scopes;
|
|
150
162
|
}
|
|
151
163
|
|
|
152
164
|
function managedResolution(specifier,context){
|
|
153
165
|
if(managedImports===null)return null;
|
|
154
|
-
|
|
166
|
+
let requested=null;
|
|
155
167
|
if(urlLikeSpecifier(specifier)&&is.string(context.parentURL)){
|
|
156
|
-
let requested;
|
|
157
168
|
try{requested=new URL(specifier,context.parentURL).href;}
|
|
158
169
|
catch{requested=null;}
|
|
159
|
-
|
|
160
|
-
|
|
170
|
+
}
|
|
171
|
+
// Match the browser's most-specific dependency scope before global imports.
|
|
172
|
+
for(const scope of managedScopes){
|
|
173
|
+
if(context.parentURL!==scope.url
|
|
174
|
+
&&!(scope.url.endsWith('/')&&context.parentURL?.startsWith(scope.url))){
|
|
175
|
+
continue;
|
|
161
176
|
}
|
|
177
|
+
if(scope.exact.has(specifier))return scope.exact.get(specifier);
|
|
178
|
+
if(requested!==null&&scope.urls.has(requested))return scope.urls.get(requested);
|
|
179
|
+
}
|
|
180
|
+
if(managedImports.has(specifier))return managedImports.get(specifier);
|
|
181
|
+
if(requested!==null&&managedUrlImports.has(requested)){
|
|
182
|
+
return managedUrlImports.get(requested);
|
|
162
183
|
}
|
|
163
184
|
if(specifier.startsWith('arcane/')||specifier.startsWith('#arcane/')){
|
|
164
185
|
loaderFailure(
|