arcane-os 0.5.11 → 0.5.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/README.md +8 -8
  3. package/docs/architecture.md +1 -1
  4. package/docs/reference/README.md +5 -5
  5. package/docs/reference/ai/browser-speech.md +73 -7
  6. package/docs/reference/ai/twin-cloud.md +1 -1
  7. package/docs/reference/asset-versioning.md +69 -0
  8. package/docs/reference/cli.md +1 -1
  9. package/docs/reference/core/arcane-ai-contracts.md +2 -3
  10. package/docs/reference/core/arcane-api.md +5 -5
  11. package/docs/reference/core/ollama-module.md +1 -1
  12. package/docs/reference/core/reference/arcane-api/platform-installation-users-system.md +2 -2
  13. package/docs/reference/inventory/package-api.json +1 -1
  14. package/docs/reference/inventory/runtime-components.json +1 -1
  15. package/docs/reference/inventory/runtime-modules.json +3 -3
  16. package/docs/reference/protocols.md +5 -5
  17. package/docs/reference/runtime-components.md +5 -2
  18. package/docs/reference/runtime-modules.md +39 -4
  19. package/docs/reference/sdk-api.md +6 -6
  20. package/package.json +1 -1
  21. package/runtime/arcane/components/calculator.html +1 -1
  22. package/runtime/arcane/components/chart.html +2 -7
  23. package/runtime/arcane/components/dashboard-config.html +3 -8
  24. package/runtime/arcane/components/file-manager.html +6 -6
  25. package/runtime/arcane/components/markdown-document.html +1 -6
  26. package/runtime/arcane/components/markdown-editor.html +2 -7
  27. package/runtime/arcane/components/media-embed.html +1 -1
  28. package/runtime/arcane/components/screen-capture.html +1 -1
  29. package/runtime/arcane/components/voice-transcription.html +4 -9
  30. package/runtime/arcane/modules/AI.js +59 -10
  31. package/runtime/arcane/modules/HTMLImport.js +184 -13
  32. package/src/dev-server.mjs +72 -5
  33. package/src/import-map.mjs +548 -16
  34. package/src/installed-sdk-runtime.mjs +1 -0
  35. package/src/packager/core.mjs +60 -1
  36. package/src/workspace-runtime.mjs +19 -7
@@ -12,6 +12,44 @@ globalThis[htmlImportHostRegistryKey]=htmlImportHostRegistry;
12
12
 
13
13
  let htmlImportScriptId=0;
14
14
 
15
+ const htmlImportModuleURL=new URL(import.meta.url);
16
+ const htmlImportAssetVersion=htmlImportModuleURL.searchParams.get('arcaneVersion');
17
+
18
+ function versionComponentResource(value,baseHref){
19
+ if(!htmlImportAssetVersion||typeof value!=='string'||!value||value.startsWith('#')){
20
+ return value;
21
+ }
22
+ let resolvedURL;
23
+ try{
24
+ resolvedURL=new URL(value,baseHref);
25
+ }catch{
26
+ return value;
27
+ }
28
+ if(resolvedURL.protocol!==htmlImportModuleURL.protocol
29
+ ||resolvedURL.origin!==htmlImportModuleURL.origin){
30
+ return value;
31
+ }
32
+
33
+ const fragmentIndex=value.indexOf('#');
34
+ const fragment=fragmentIndex<0?'':value.slice(fragmentIndex);
35
+ const resource=fragmentIndex<0?value:value.slice(0,fragmentIndex);
36
+ const queryIndex=resource.indexOf('?');
37
+ const pathname=queryIndex<0?resource:resource.slice(0,queryIndex);
38
+ const query=queryIndex<0?'':resource.slice(queryIndex+1);
39
+ const versionField=`arcaneVersion=${encodeURIComponent(htmlImportAssetVersion)}`;
40
+ let replaced=false;
41
+ const fields=query?query.split('&').map(function versionQueryField(field){
42
+ if(!new URLSearchParams(field).has('arcaneVersion'))return field;
43
+ if(replaced)return null;
44
+ replaced=true;
45
+ return versionField;
46
+ }).filter(function retainQueryField(field){
47
+ return field!==null;
48
+ }):[];
49
+ if(!replaced)fields.push(versionField);
50
+ return `${pathname}?${fields.join('&')}${fragment}`;
51
+ }
52
+
15
53
  function componentRuntimeRoot(resolvedHref){
16
54
  const componentURL=new URL(resolvedHref);
17
55
  const componentMarker='/arcane/components/';
@@ -32,28 +70,157 @@ function resolveComponentResource(value,runtimeRoot){
32
70
  }
33
71
 
34
72
  function resolveComponentStyleResources(styleText,runtimeRoot){
35
- if(!runtimeRoot)return styleText;
36
- return styleText.replace(
37
- /url\(\s*(['"]?)\.\/arcane\/([^)'"\s]+)\1\s*\)/gu,
38
- function resolveStyleURL(_match,quote,resourcePath){
39
- const resolved=new URL(resourcePath,runtimeRoot).href;
40
- return `url(${quote}${resolved}${quote})`;
73
+ if(!runtimeRoot&&!htmlImportAssetVersion)return styleText;
74
+ const parts=[];
75
+ let copiedThrough=0;
76
+ let index=0;
77
+
78
+ function afterComment(start){
79
+ const end=styleText.indexOf('*/',start+2);
80
+ return end<0?styleText.length:end+2;
81
+ }
82
+
83
+ function afterEscape(start){
84
+ let end=start+1;
85
+ if(/[\da-f]/iu.test(styleText[end]||'')){
86
+ const limit=end+6;
87
+ while(end<limit&&/[\da-f]/iu.test(styleText[end]||''))end+=1;
88
+ if(/[\t\n\f\r ]/u.test(styleText[end]||'')){
89
+ end+=styleText[end]==='\r'&&styleText[end+1]==='\n'?2:1;
90
+ }
91
+ return end;
41
92
  }
42
- );
93
+ return end+(styleText[end]==='\r'&&styleText[end+1]==='\n'?2:1);
94
+ }
95
+
96
+ function afterString(start){
97
+ const quote=styleText[start];
98
+ let end=start+1;
99
+ while(end<styleText.length){
100
+ if(styleText[end]==='\\'){
101
+ end=afterEscape(end);
102
+ }else if(styleText[end]===quote){
103
+ return end+1;
104
+ }else if(/[\n\r\f]/u.test(styleText[end])){
105
+ return end;
106
+ }else{
107
+ end+=1;
108
+ }
109
+ }
110
+ return styleText.length;
111
+ }
112
+
113
+ function afterWhitespaceAndComments(start){
114
+ let end=start;
115
+ while(end<styleText.length){
116
+ if(/[\t\n\f\r ]/u.test(styleText[end])){
117
+ end+=1;
118
+ }else if(styleText.startsWith('/*',end)){
119
+ end=afterComment(end);
120
+ }else{
121
+ break;
122
+ }
123
+ }
124
+ return end;
125
+ }
126
+
127
+ function appendResource(start,end){
128
+ const resourcePath=styleText.slice(start,end);
129
+ // CSS escapes retain their original spelling until their URL syntax is decoded.
130
+ if(resourcePath.includes('\\'))return;
131
+ const resolved=versionComponentResource(
132
+ resolveComponentResource(resourcePath,runtimeRoot),
133
+ document.baseURI
134
+ );
135
+ if(resolved===resourcePath)return;
136
+ parts.push(styleText.slice(copiedThrough,start),resolved);
137
+ copiedThrough=end;
138
+ }
139
+
140
+ while(index<styleText.length){
141
+ if(styleText.startsWith('/*',index)){
142
+ index=afterComment(index);
143
+ continue;
144
+ }
145
+ if(styleText[index]==='"'||styleText[index]==="'"){
146
+ index=afterString(index);
147
+ continue;
148
+ }
149
+
150
+ const atRule=styleText[index]==='@';
151
+ const hashToken=styleText[index]==='#';
152
+ const nameStart=atRule||hashToken?index+1:index;
153
+ let nameEnd=nameStart;
154
+ while(nameEnd<styleText.length&&/[-\w\u0080-\uFFFF\\]/u.test(styleText[nameEnd])){
155
+ nameEnd=styleText[nameEnd]==='\\'?afterEscape(nameEnd):nameEnd+1;
156
+ }
157
+ if(nameEnd===nameStart){
158
+ index+=1;
159
+ continue;
160
+ }
161
+ const name=styleText.slice(nameStart,nameEnd).toLowerCase();
162
+ index=nameEnd;
163
+ if(hashToken)continue;
164
+ if(atRule&&name==='import'){
165
+ const start=afterWhitespaceAndComments(nameEnd);
166
+ if(styleText[start]==='"'||styleText[start]==="'"){
167
+ const end=afterString(start);
168
+ if(styleText[end-1]===styleText[start])appendResource(start+1,end-1);
169
+ index=end;
170
+ }
171
+ continue;
172
+ }
173
+ if(atRule||name!=='url'||styleText[nameEnd]!=='(')continue;
174
+
175
+ let start=nameEnd+1;
176
+ while(start<styleText.length&&/[\t\n\f\r ]/u.test(styleText[start]))start+=1;
177
+ if(styleText[start]==='"'||styleText[start]==="'"){
178
+ const end=afterString(start);
179
+ const close=afterWhitespaceAndComments(end);
180
+ if(styleText[end-1]===styleText[start]&&styleText[close]===')'){
181
+ appendResource(start+1,end-1);
182
+ index=close+1;
183
+ }else{
184
+ index=end;
185
+ }
186
+ continue;
187
+ }
188
+
189
+ let end=start;
190
+ while(end<styleText.length&&!/[\t\n\f\r )'"(]/u.test(styleText[end])){
191
+ end=styleText[end]==='\\'?afterEscape(end):end+1;
192
+ }
193
+ let close=end;
194
+ while(close<styleText.length&&/[\t\n\f\r ]/u.test(styleText[close]))close+=1;
195
+ if(styleText[close]===')'){
196
+ appendResource(start,end);
197
+ index=close+1;
198
+ }else{
199
+ index=end;
200
+ }
201
+ }
202
+ if(parts.length===0)return styleText;
203
+ parts.push(styleText.slice(copiedThrough));
204
+ return parts.join('');
43
205
  }
44
206
 
45
207
  function createComponentFragment(html,resolvedHref){
46
208
  const template=document.createElement('template');
47
209
  template.innerHTML=html;
48
210
  const runtimeRoot=componentRuntimeRoot(resolvedHref);
49
- if(!runtimeRoot)return template.content;
50
211
 
51
212
  for(const element of template.content.querySelectorAll('[href],[src]')){
52
213
  for(const attribute of ['href','src']){
53
214
  if(!element.hasAttribute(attribute))continue;
54
215
  const value=element.getAttribute(attribute);
55
216
  const resolved=resolveComponentResource(value,runtimeRoot);
56
- if(resolved!==value)element.setAttribute(attribute,resolved);
217
+ const navigation=attribute==='href'
218
+ &&['a','area','base'].includes(element.localName);
219
+ const versioned=navigation?resolved:versionComponentResource(
220
+ resolved,
221
+ element.localName==='script'?resolvedHref:document.baseURI
222
+ );
223
+ if(versioned!==value)element.setAttribute(attribute,versioned);
57
224
  }
58
225
  }
59
226
  for(const style of template.content.querySelectorAll('style')){
@@ -69,7 +236,8 @@ function resolveRelativeDynamicImports(source,baseHref){
69
236
  return source.replace(
70
237
  /\bimport\s*\(\s*(['"])(\.{1,2}\/[^'"]+)\1\s*\)/gu,
71
238
  function resolveDynamicImport(_match,_quote,specifier){
72
- return `import(${JSON.stringify(new URL(specifier,baseHref).href)})`;
239
+ const resolved=versionComponentResource(new URL(specifier,baseHref).href,baseHref);
240
+ return `import(${JSON.stringify(resolved)})`;
73
241
  }
74
242
  );
75
243
  }
@@ -138,8 +306,8 @@ class HTMLImport extends HTMLElement {
138
306
 
139
307
  try{
140
308
  const resolvedURL=new URL(href,document.baseURI);
141
- resolvedHref=resolvedURL.href;
142
- const response=await fetch(resolvedURL.href,{
309
+ resolvedHref=versionComponentResource(resolvedURL.href,document.baseURI);
310
+ const response=await fetch(resolvedHref,{
143
311
  cache:'default',
144
312
  method:'GET',
145
313
  signal:controller.signal
@@ -308,7 +476,10 @@ class HTMLImport extends HTMLElement {
308
476
  let sourceBaseHref=contentBaseHref;
309
477
  const scriptSource=script.getAttribute('src');
310
478
  if(scriptSource!==null){
311
- const requestedScriptHref=new URL(scriptSource,contentBaseHref).href;
479
+ const requestedScriptHref=versionComponentResource(
480
+ new URL(scriptSource,contentBaseHref).href,
481
+ contentBaseHref
482
+ );
312
483
  const response=await fetch(requestedScriptHref,{
313
484
  cache:'default',
314
485
  method:'GET',
@@ -1,14 +1,19 @@
1
1
  import {constants as FS_CONSTANTS} from 'node:fs';
2
- import {lstat,open,realpath} from 'node:fs/promises';
2
+ import {lstat,open,readFile,realpath} from 'node:fs/promises';
3
3
  import http from 'node:http';
4
4
  import path from 'node:path';
5
5
  import {resolveWorkspace} from './workspace.mjs';
6
6
  import {createEventQueue} from './event-queue.mjs';
7
+ import {
8
+ inspectImportMapHtml,MANAGED_IMPORT_MAP_ATTRIBUTE,
9
+ readWorkspaceAssetVersion,rewriteAssetReferences
10
+ } from './import-map.mjs';
7
11
 
8
12
  const MIME_TYPES=new Map([
9
13
  ['.css','text/css; charset=utf-8'],
10
14
  ['.gif','image/gif'],
11
15
  ['.html','text/html; charset=utf-8'],
16
+ ['.htm','text/html; charset=utf-8'],
12
17
  ['.ico','image/x-icon'],
13
18
  ['.jpeg','image/jpeg'],
14
19
  ['.jpg','image/jpeg'],
@@ -308,11 +313,21 @@ async function openSafeFile(root,segments){
308
313
  }
309
314
  }
310
315
 
311
- async function sendFile(response,opened,{head=false}={}){
316
+ async function sendFile(response,opened,{head=false,assetVersion,revalidate=false,onReference}={}){
312
317
  const extension=path.extname(opened.candidate).toLowerCase();
318
+ const content=assetVersion&&/\.(?:m?js|html?|css)$/iu.test(extension)
319
+ ?Buffer.from(rewriteAssetReferences(opened.content.toString('utf8'),{
320
+ filePath:opened.candidate,
321
+ version:assetVersion,
322
+ onReference
323
+ }),'utf8')
324
+ :opened.content;
325
+ // Entry documents must discover the current generated graph on navigation.
326
+ // no-cache permits storage and revalidation; ordinary asset caching is unchanged.
313
327
  response.writeHead(200,{
314
328
  'content-type':MIME_TYPES.get(extension)||'application/octet-stream',
315
- 'content-length':opened.content.byteLength
329
+ 'content-length':content.byteLength,
330
+ ...(revalidate?{'cache-control':'no-cache'}:{})
316
331
  });
317
332
  await new Promise((resolve,reject)=>{
318
333
  let settled=false;
@@ -336,7 +351,7 @@ async function sendFile(response,opened,{head=false}={}){
336
351
  response.once('error',failed);
337
352
  response.once('finish',completed);
338
353
  response.once('close',completed);
339
- response.end(head?undefined:opened.content);
354
+ response.end(head?undefined:content);
340
355
  });
341
356
  }
342
357
 
@@ -509,11 +524,23 @@ async function startOwnedDevServer({
509
524
  })
510
525
  :await packagedRoutes(releaseRoot);
511
526
  const mappings=deterministicMappings(routeSet.mappings);
527
+ async function selectedAssetVersion(){
528
+ if(mode!=='source')return undefined;
529
+ return sdkRuntimeSourceRoot===undefined
530
+ ?readWorkspaceAssetVersion(routeSet.workspaceRoot)
531
+ :JSON.parse(await readFile(
532
+ path.join(routeSet.runtime.sourceRoot,'package.json'),'utf8'
533
+ )).version;
534
+ }
535
+ let assetVersion=await selectedAssetVersion();
512
536
  for(const mapping of mappings){
513
537
  const info=await lstat(mapping.root);
514
538
  if(info.isSymbolicLink()||!info.isDirectory())fail(`Server route root must be a real directory: ${mapping.root}.`);
515
539
  mapping.root=await realpath(mapping.root);
516
540
  }
541
+ // Remember actual resource edges as their owners are served, not every
542
+ // HTML/JS/CSS file in an application's document or attachment corpus.
543
+ const resourcePaths=new Set([routeSet.startPath]);
517
544
  const requestTasks=new Set();
518
545
  const runFileWork=createFileWorkLimiter();
519
546
  const server=http.createServer((request,response)=>{
@@ -541,7 +568,47 @@ async function startOwnedDevServer({
541
568
  await runFileWork(async()=>{
542
569
  const opened=await openSafeFile(mapping.root,relative);
543
570
  if(!opened){deny(response,404,'Not found.');return;}
544
- await sendFile(response,opened,{head:request.method==='HEAD'});
571
+ const extension=path.extname(opened.candidate).toLowerCase();
572
+ const html=extension==='.html'||extension==='.htm';
573
+ let managedDocument=false;
574
+ if(html&&opened.content.includes(MANAGED_IMPORT_MAP_ATTRIBUTE)){
575
+ try{
576
+ managedDocument=inspectImportMapHtml(
577
+ opened.content.toString('utf8')
578
+ ).managedMaps.length>0;
579
+ }catch{
580
+ // An unrelated document is not a source validation surface.
581
+ }
582
+ }
583
+ const entryDocument=target.path===routeSet.startPath||managedDocument;
584
+ const managedMap=path.basename(opened.candidate)==='arcane.importmap.json';
585
+ // A live server can span an SDK upgrade. Refresh the small
586
+ // version record on navigation, not on each resource request.
587
+ if(entryDocument||managedMap)assetVersion=await selectedAssetVersion();
588
+ const runtimeResource=segments[0]==='arcane';
589
+ const browserResource=['script','style','worker','sharedworker','serviceworker']
590
+ .includes(request.headers['sec-fetch-dest']);
591
+ const rewrite=runtimeResource||entryDocument||browserResource
592
+ ||resourcePaths.has(target.path);
593
+ const onReference=({url,kind,baseHref,baseKind})=>{
594
+ if(baseKind==='document'||kind==='fetch'||(kind==='asset'&&!/\.css(?:[?#]|$)/iu.test(url))
595
+ ||(kind==='import'&&!/^(?:\.{1,2}\/|\/)/u.test(url)))return;
596
+ try{
597
+ const documentUrl=new URL(target.path,'http://arcane.invalid');
598
+ const base=baseHref?new URL(baseHref,documentUrl):documentUrl;
599
+ const resource=new URL(url,base);
600
+ if(resource.origin===documentUrl.origin
601
+ &&/\.(?:m?js|html?|css)$/iu.test(resource.pathname)){
602
+ resourcePaths.add(decodeURIComponent(resource.pathname));
603
+ }
604
+ }catch{ /* Non-URL values remain under their existing owner. */ }
605
+ };
606
+ await sendFile(response,opened,{
607
+ head:request.method==='HEAD',
608
+ assetVersion:rewrite?assetVersion:undefined,
609
+ revalidate:entryDocument||managedMap,
610
+ onReference
611
+ });
545
612
  });
546
613
  })().catch(async error=>{
547
614
  await events.enqueue({type:'server.request.failed',message:error.message});