arcane-os 0.5.12 → 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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,17 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.5.13
6
+
7
+ - Derive local browser resource queries from the selected SDK package version
8
+ across managed import maps, runtime materialization, source serving and
9
+ packaged application resource graphs. Preserve existing query parameters,
10
+ fragments and ordinary caching; revalidate entry documents on navigation.
11
+ - Carry component and Worker module references through the same versioned
12
+ paths. Preserve fetched document/attachment content, CSS comments and text,
13
+ remote URLs, model data and user storage. Existing open documents adopt the
14
+ new resource graph on ordinary refresh or navigation, without a forced reload.
15
+
5
16
  ## 0.5.12
6
17
 
7
18
  - Add optional `voice`, `speed`, `pauseAfterMs`, and `waitForPlayback` fields to
@@ -0,0 +1,69 @@
1
+ # Release-derived browser asset URLs
2
+
3
+ The SDK's public import-map generator, runtime materializer, source server and
4
+ application packager use the selected SDK package version for local browser
5
+ resource references. The query field is `arcaneVersion`. Its value comes from
6
+ SDK package metadata, not a timestamp, content measurement, or application
7
+ constant.
8
+
9
+ For example, an existing `./arcane/modules/HTMLImport.js?v=6#module` reference
10
+ retains its application revision and fragment and gains
11
+ `&arcaneVersion=${version}` before the fragment. Regenerating for another SDK
12
+ release replaces only the `arcaneVersion` value. Existing query spelling and
13
+ application parameters remain intact.
14
+
15
+ ## Public tooling
16
+
17
+ - Run the installed SDK's `materializeInstalledSdkRuntime()` to refresh the
18
+ workspace runtime from that installation. Its generated local resource
19
+ references use the installed package version, including npm aliases.
20
+ - Run `arcane import-map` through the installed SDK to regenerate the managed
21
+ import map and its application HTML. Named SDK imports and URL-shaped module
22
+ entries point to versioned resources.
23
+ - `arcane dev` applies the same reference transformation to served source
24
+ without editing the source files. An explicit live SDK source mount uses the
25
+ version in that source checkout's `package.json`. Otherwise it uses the
26
+ materialized SDK version recorded by the workspace's existing semantic lock.
27
+ - The application packager applies resource versioning after its selected
28
+ adapter finishes. The resulting application can be served by an ordinary
29
+ static host without a JavaScript transformation service.
30
+
31
+ The shared transformation edits actual local module imports, resource URL
32
+ construction, HTML resource attributes and CSS resource references. It does not
33
+ change displayed text, prompts, application data, downloaded model content,
34
+ remote provider URLs, or arbitrary strings that happen to resemble filenames.
35
+ Worker entry references and their local module imports are versioned separately:
36
+ Workers do not inherit a document's import map. `HTMLImport` carries its own
37
+ release query into local dynamically loaded components and their resources.
38
+
39
+ Computed application URLs that have no statically identifiable resource
40
+ reference remain application-owned expressions. The SDK does not intercept
41
+ global `fetch`, `URL`, `Worker`, or DOM APIs. An import-map URL entry can resolve
42
+ an exact matching computed module URL, but is not a wildcard query rule.
43
+
44
+ ## Navigation and caching
45
+
46
+ The SDK server sends `Cache-Control: no-cache` for application entry/managed
47
+ HTML and managed import-map JSON. This allows storage but requests revalidation, so an ordinary navigation
48
+ or refresh obtains the current entry document and release URLs. Other assets
49
+ retain ordinary caching; no cache or user storage is cleared.
50
+
51
+ An independently configured static host must likewise revalidate entry HTML
52
+ and managed import-map JSON. The generated package supplies versioned resource
53
+ references; it cannot configure another server's HTTP headers.
54
+
55
+ An already-open document keeps modules it has evaluated. Versioned references
56
+ take effect on ordinary navigation or refresh; they do not replace live module
57
+ instances, force a reload, restart a model, or erase a conversation. The SDK does
58
+ not retain earlier installed runtime trees after materialization.
59
+
60
+ ## Scope and work
61
+
62
+ One invocation acts on the selected workspace/application. Runtime
63
+ materialization reuses its existing recursive copy; packaging reuses its
64
+ selected output inventory. Packaging follows resource references from the entry,
65
+ managed application pages and SDK runtime. Source serving recognizes those
66
+ resources and browser script/style/worker requests. Files included only as
67
+ documents or attachments are not transformed merely because they contain HTML,
68
+ JavaScript or CSS. Source serving transforms the requested resource response,
69
+ not the entire workspace, and leaves source files unchanged.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "arcane-os",
3
- "version": "0.5.12",
3
+ "version": "0.5.13",
4
4
  "description": "Arcane OS JavaScript SDK, project-local CLI, browser runtime, and repository-portable application packager.",
5
5
  "type": "module",
6
6
  "main": "./src/index.mjs",
@@ -1,7 +1,7 @@
1
1
  <link rel="stylesheet" href="./arcane/css/theme.css?v=1"><link rel="stylesheet" href="./arcane/css/primitives.css?v=1">
2
2
  <style>:host{display:block;max-width:34rem}.calculator{display:grid;gap:var(--arcane-space-3);padding:var(--arcane-space-4)}.display{display:grid;gap:var(--arcane-space-2)}.display input{font:600 1.3rem/1.4 ui-monospace,monospace;text-align:right}.result{min-height:2.5rem;margin:0;text-align:right;font-size:1.8rem;font-weight:750;overflow-wrap:anywhere}.keys{display:grid;gap:var(--arcane-space-2);grid-template-columns:repeat(5,1fr)}.keys button{min-width:0;padding:.7rem .35rem}.status{min-height:1.4rem;margin:0;color:var(--arcane-muted)}@media(max-width:28rem){.keys{grid-template-columns:repeat(4,1fr)}}</style>
3
3
  <section class="calculator arcane-card"><div class="display arcane-field"><label for="expression">Expression</label><input id="expression" inputmode="decimal" autocomplete="off" spellcheck="false" value=""><p id="result" class="result" aria-live="polite">0</p></div><div id="keys" class="keys" aria-label="Calculator keypad"></div><p id="status" class="status" role="status"></p></section>
4
- <script type="module">const host=this,root=host.shadowRoot,input=root.querySelector('#expression'),result=root.querySelector('#result'),status=root.querySelector('#status'),keys=root.querySelector('#keys');const componentURL=new URL(host.getAttribute('href')||'./arcane/components/calculator.html',document.baseURI);const {default:CalculatorEngine}=await import(new URL('../modules/CalculatorEngine.js',componentURL));const {createArcaneEventSource,projectArcaneDOMEvent}=await import('arcane-os/event-manager');const events=createArcaneEventSource(host,{source:'arcane.component.calculator',eventTypes:['calculation-complete','calculation-error','calculator-ready']}),lifecycleController=new AbortController();const engine=new CalculatorEngine(),definitions=['7','8','9','/','sqrt(','4','5','6','*','^','1','2','3','-','%','0','.','(',')','+','pi','e','C','⌫','='];let destroyed=false,operationSequence=0;
4
+ <script type="module">const host=this,root=host.shadowRoot,input=root.querySelector('#expression'),result=root.querySelector('#result'),status=root.querySelector('#status'),keys=root.querySelector('#keys');const {default:CalculatorEngine}=await import('../modules/CalculatorEngine.js');const {createArcaneEventSource,projectArcaneDOMEvent}=await import('arcane-os/event-manager');const events=createArcaneEventSource(host,{source:'arcane.component.calculator',eventTypes:['calculation-complete','calculation-error','calculator-ready']}),lifecycleController=new AbortController();const engine=new CalculatorEngine(),definitions=['7','8','9','/','sqrt(','4','5','6','*','^','1','2','3','-','%','0','.','(',')','+','pi','e','C','⌫','='];let destroyed=false,operationSequence=0;
5
5
  for(const value of definitions){const button=document.createElement('button');button.type='button';button.className=value==='='?'arcane-button':'arcane-button arcane-button--secondary';button.textContent=value;button.dataset.value=value;keys.append(button)}
6
6
  keys.addEventListener('click',event=>{const value=event.target.closest('[data-value]')?.dataset.value;if(!value)return;if(value==='C'){input.value='';result.textContent='0';status.textContent='';return}if(value==='⌫'){input.value=input.value.slice(0,-1);return}if(value==='='){calculate();return}input.value+=value;input.focus()},{signal:lifecycleController.signal});input.addEventListener('keydown',event=>{if(event.key==='Enter'){event.preventDefault();calculate()}},{signal:lifecycleController.signal});
7
7
  function nextOperationId(){operationSequence+=1;return `${events.descriptor.instanceId}:calculate:${operationSequence}`}
@@ -162,16 +162,11 @@
162
162
 
163
163
  <script type="module">
164
164
  const host=this;
165
- const componentURL=new URL(
166
- host.getAttribute('href')||'./arcane/components/chart.html',
167
- document.baseURI
168
- );
169
- const moduleURL=name=>new URL(`../modules/${name}`,componentURL).href;
170
- const {default:loadChartLibrary}=await import(moduleURL('ChartLibrary.js'));
165
+ const {default:loadChartLibrary}=await import('../modules/ChartLibrary.js');
171
166
  const {
172
167
  normalizeChartOptions,
173
168
  normalizeChartRows
174
- }=await import(moduleURL('ComponentContracts.js'));
169
+ }=await import('../modules/ComponentContracts.js');
175
170
  const {createArcaneEventSource,projectArcaneDOMEvent}=await import('arcane-os/event-manager');
176
171
  const events=createArcaneEventSource(host,{source:'arcane.component.chart',eventTypes:['chart-remove','chart-ready']});
177
172
  const lifecycleController=new AbortController();
@@ -11,18 +11,13 @@
11
11
 
12
12
  <script type="module">
13
13
  const host=this;
14
- const componentURL=new URL(
15
- host.getAttribute('href')||'./arcane/components/dashboard-config.html',
16
- document.baseURI
17
- );
18
- const moduleURL=name=>new URL(`../modules/${name}`,componentURL).href;
19
- const {default:waitForComponent}=await import(moduleURL('WaitForComponent.js'));
14
+ const {default:waitForComponent}=await import('../modules/WaitForComponent.js');
20
15
  const {
21
16
  effectiveDashboardVisibility,
22
17
  normalizeDashboardDefinitions,
23
18
  normalizeDashboardOptions,
24
19
  normalizeDashboardVisibility
25
- }=await import(moduleURL('ComponentContracts.js'));
20
+ }=await import('../modules/ComponentContracts.js');
26
21
  const {createArcaneEventSource,projectArcaneDOMEvent}=await import('arcane-os/event-manager');
27
22
  const events=createArcaneEventSource(host,{source:'arcane.component.dashboard-config',eventTypes:['dashboard-config-closed','dashboard-config-opened','dashboard-config-change','dashboard-config-ready']});
28
23
  const lifecycleController=new AbortController();
@@ -232,7 +227,7 @@
232
227
  const items=document.createElement('section');
233
228
 
234
229
  styles.rel='stylesheet';
235
- styles.href='./arcane/css/dashboard-config.css?v=2';
230
+ styles.href=new URL('./arcane/css/dashboard-config.css?v=2',document.baseURI).href;
236
231
  heading.dataset.dashboardConfigHeading='';
237
232
  message.dataset.dashboardConfigDescription='';
238
233
  empty.className='dashboard-config-empty';
@@ -825,8 +825,8 @@
825
825
  icon.className='tree-entry-icon';
826
826
  icon.alt='';
827
827
  icon.src=entry.kind==='directory'
828
- ?'./arcane/img/folder.svg'
829
- :'./arcane/img/doc.svg';
828
+ ?new URL('./arcane/img/folder.svg',document.baseURI).href
829
+ :new URL('./arcane/img/doc.svg',document.baseURI).href;
830
830
  name.className='tree-entry-name';
831
831
  name.innerText=entry.name;
832
832
  count.className='tree-file-count';
@@ -1404,7 +1404,7 @@
1404
1404
  deleteButton.innerText='Delete File';
1405
1405
  deleteIcon.alt='';
1406
1406
  deleteIcon.className='action-icon';
1407
- deleteIcon.src='./arcane/img/trash.svg';
1407
+ deleteIcon.src=new URL('./arcane/img/trash.svg',document.baseURI).href;
1408
1408
  deleteButton.prepend(deleteIcon);
1409
1409
  deleteButton.addEventListener(
1410
1410
  'click',
@@ -1610,7 +1610,7 @@
1610
1610
 
1611
1611
  fileIcon.alt='';
1612
1612
  fileIcon.className='icon file-icon';
1613
- fileIcon.src='./arcane/img/doc.svg';
1613
+ fileIcon.src=new URL('./arcane/img/doc.svg',document.baseURI).href;
1614
1614
  fileDetails.className='file-details';
1615
1615
  fileLabel.className='file-name';
1616
1616
  fileLabel.innerText=fileName;
@@ -1633,7 +1633,7 @@
1633
1633
 
1634
1634
  deleteIcon.alt='';
1635
1635
  deleteIcon.className='action-icon';
1636
- deleteIcon.src='./arcane/img/trash.svg';
1636
+ deleteIcon.src=new URL('./arcane/img/trash.svg',document.baseURI).href;
1637
1637
 
1638
1638
  openButton.addEventListener(
1639
1639
  'click',
@@ -1933,7 +1933,7 @@
1933
1933
  directory.dataset.directory=tableName;
1934
1934
  folder.className='file folder';
1935
1935
  folderIcon.className='icon';
1936
- folderIcon.src='./arcane/img/folder.svg';
1936
+ folderIcon.src=new URL('./arcane/img/folder.svg',document.baseURI).href;
1937
1937
  folderName.innerText=tableName;
1938
1938
  fileCount.className='file-count';
1939
1939
  fileCount.innerText=`${files.length} ${files.length===1?'file':'files'}`;
@@ -214,12 +214,7 @@
214
214
  const host=this;
215
215
  host.ready=false;
216
216
 
217
- const componentURL=new URL(
218
- host.getAttribute('href')||'./arcane/components/markdown-document.html',
219
- document.baseURI
220
- );
221
- const moduleURL=name=>new URL(`../modules/${name}`,componentURL).href;
222
- const {default:MD}=await import(moduleURL('MD.js?v=2'));
217
+ const {default:MD}=await import('../modules/MD.js?v=2');
223
218
  const {createArcaneEventSource,projectArcaneDOMEvent}=await import('arcane-os/event-manager');
224
219
  const events=createArcaneEventSource(host,{source:'arcane.component.markdown-document',eventTypes:['markdown-document-state','markdown-document-empty','markdown-document-error','markdown-document-loading','markdown-document-rendered','markdown-document-navigate','markdown-document-ready']});
225
220
  const lifecycleController=new AbortController();
@@ -75,16 +75,11 @@
75
75
 
76
76
  <script type="module">
77
77
  const host=this;
78
- const componentURL=new URL(
79
- host.getAttribute('href')||'./arcane/components/markdown-editor.html',
80
- document.baseURI
81
- );
82
- const moduleURL=name=>new URL(`../modules/${name}`,componentURL).href;
83
- const {default:MD}=await import(moduleURL('MD.js?v=2'));
78
+ const {default:MD}=await import('../modules/MD.js?v=2');
84
79
  const {
85
80
  applyMarkdownFormat,
86
81
  normalizeMarkdownOptions
87
- }=await import(moduleURL('ComponentContracts.js'));
82
+ }=await import('../modules/ComponentContracts.js');
88
83
  const {createArcaneEventSource,projectArcaneDOMEvent}=await import('arcane-os/event-manager');
89
84
  const events=createArcaneEventSource(host,{source:'arcane.component.markdown-editor',eventTypes:['markdown-editor-change','markdown-editor-saved','markdown-editor-ready']});
90
85
  const lifecycleController=new AbortController();
@@ -1,7 +1,7 @@
1
1
  <link rel="stylesheet" href="./arcane/css/theme.css?v=1"><link rel="stylesheet" href="./arcane/css/primitives.css?v=1">
2
2
  <style>:host{display:block}.player-shell{display:grid;gap:var(--arcane-space-4)}.loader{display:flex;gap:var(--arcane-space-2)}.loader input{min-width:0;width:100%}.player{aspect-ratio:16/9;display:grid;place-items:center;background:var(--arcane-surface-muted);border:1px solid var(--arcane-border);border-radius:var(--arcane-radius-large)}iframe{width:100%;height:100%;border:0}.empty{text-align:center;color:var(--arcane-muted);padding:var(--arcane-space-5)}.footer{align-items:center;display:flex;gap:var(--arcane-space-3);justify-content:space-between}.status{margin:0;color:var(--arcane-muted)}@media(max-width:36rem){.loader,.footer{align-items:stretch;flex-direction:column}}</style>
3
3
  <section class="player-shell"><form id="loader" class="loader"><label class="visually-hidden" for="mediaUrl">Video or playlist address</label><input id="mediaUrl" type="text" inputmode="url" autocomplete="off" spellcheck="false" placeholder="Paste a YouTube video or playlist URL"><button class="arcane-button" type="submit">Play</button></form><div class="player"><div id="empty" class="empty"><strong>Choose something to play</strong><p>Paste a YouTube or YouTube Music video or playlist address.</p></div><iframe id="frame" title="Embedded media player" hidden allow="autoplay; encrypted-media; picture-in-picture; fullscreen" allowfullscreen></iframe></div><div class="footer"><p id="status" class="status" role="status" aria-live="polite">Nothing loaded.</p><button id="platform" class="arcane-button arcane-button--secondary" type="button">Open platform</button></div></section>
4
- <script type="module">const host=this,root=host.shadowRoot,form=root.querySelector('#loader'),input=root.querySelector('#mediaUrl'),frame=root.querySelector('#frame'),empty=root.querySelector('#empty'),status=root.querySelector('#status');const componentURL=new URL(host.getAttribute('href')||'./arcane/components/media-embed.html',document.baseURI);const {parseYouTubeMedia,youtubeEmbedUrl}=await import(new URL('../modules/YouTubeMedia.js',componentURL));const {createArcaneEventSource,projectArcaneDOMEvent}=await import('arcane-os/event-manager');const events=createArcaneEventSource(host,{source:'arcane.component.media-embed',eventTypes:['media-open-platform','media-load','media-error','media-embed-ready']}),lifecycleController=new AbortController();let platformUrl='https://www.youtube.com/',privacyEnhanced=false,destroyed=false,operationSequence=0;
4
+ <script type="module">const host=this,root=host.shadowRoot,form=root.querySelector('#loader'),input=root.querySelector('#mediaUrl'),frame=root.querySelector('#frame'),empty=root.querySelector('#empty'),status=root.querySelector('#status');const {parseYouTubeMedia,youtubeEmbedUrl}=await import('../modules/YouTubeMedia.js');const {createArcaneEventSource,projectArcaneDOMEvent}=await import('arcane-os/event-manager');const events=createArcaneEventSource(host,{source:'arcane.component.media-embed',eventTypes:['media-open-platform','media-load','media-error','media-embed-ready']}),lifecycleController=new AbortController();let platformUrl='https://www.youtube.com/',privacyEnhanced=false,destroyed=false,operationSequence=0;
5
5
  function nextOperationId(kind){operationSequence+=1;return `${events.descriptor.instanceId}:${kind}:${operationSequence}`}
6
6
  function emit(type,detail={},publicDetail={},operationId=null){if(destroyed)return false;const publication=events.dispatch(type,detail,{publicDetail,operationId});return projectArcaneDOMEvent(host,publication.occurrence,{bubbles:true,composed:true})}
7
7
  host.configure=options=>{if(destroyed)return false;platformUrl=String(options?.platformUrl||platformUrl);if(Object.hasOwn(options||{},'privacyEnhanced'))privacyEnhanced=options.privacyEnhanced===true;if(options?.placeholder)input.placeholder=String(options.placeholder);if(options?.openLabel)root.querySelector('#platform').textContent=String(options.openLabel);if(options?.initialValue){input.value=String(options.initialValue);load(input.value)}return host};host.load=load;host.destroy=destroy;
@@ -1,7 +1,7 @@
1
1
  <link rel="stylesheet" href="./arcane/css/theme.css?v=1"><link rel="stylesheet" href="./arcane/css/primitives.css?v=1">
2
2
  <style>:host{display:block}.capture{display:grid;gap:var(--arcane-space-4)}.actions{display:grid;gap:var(--arcane-space-3);grid-template-columns:repeat(3,minmax(0,1fr))}.action{display:grid;gap:var(--arcane-space-2);padding:var(--arcane-space-4);text-align:left}.action strong{font-size:1.05rem}.action span{color:var(--arcane-muted);font-size:.85rem}.recording{align-items:center;display:flex;gap:var(--arcane-space-3);justify-content:space-between;padding:var(--arcane-space-3);border:1px solid var(--arcane-border);border-radius:var(--arcane-radius-medium)}.preview{display:grid;place-items:center;min-height:16rem;overflow:auto;background:var(--arcane-surface-muted);border:1px solid var(--arcane-border);border-radius:var(--arcane-radius-large)}.preview :where(img,video){display:block}.empty{color:var(--arcane-muted);text-align:center}.status{margin:0;color:var(--arcane-muted)}@media(max-width:44rem){.actions{grid-template-columns:1fr}}</style>
3
3
  <section class="capture"><div class="actions"><button class="action arcane-card" type="button" data-capture="image"><strong>Screenshot</strong><span>Capture the selected display as PNG.</span></button><button class="action arcane-card" type="button" data-capture="video"><strong>Record video</strong><span>Record the selected display in a browser-supported video format.</span></button><button class="action arcane-card" type="button" data-capture="gif"><strong>Record GIF</strong><span>Record the selected display as an animated GIF.</span></button></div><div id="recording" class="recording" hidden><span id="recordingLabel">Recording…</span><button id="stop" class="arcane-button" type="button">Stop recording</button></div><div id="preview" class="preview"><div class="empty"><strong>No capture yet</strong><p>Your capture remains on this device unless you choose to save it.</p></div></div><p id="status" class="status" role="status" aria-live="polite">Choose a capture type. Your browser will ask which display to share.</p></section>
4
- <script type="module">const host=this,root=host.shadowRoot,preview=root.querySelector('#preview'),recording=root.querySelector('#recording'),status=root.querySelector('#status');const componentURL=new URL(host.getAttribute('href')||'./arcane/components/screen-capture.html',document.baseURI);const {default:ScreenCapture}=await import(new URL('../modules/ScreenCapture.js',componentURL));const {createArcaneEventSource,projectArcaneDOMEvent}=await import('arcane-os/event-manager');const events=createArcaneEventSource(host,{source:'arcane.component.screen-capture',eventTypes:['screen-capture-result','screen-capture-ready']});const capture=new ScreenCapture(),listeners=new AbortController();let destroyed=false,operationGeneration=0,previewUrl='';
4
+ <script type="module">const host=this,root=host.shadowRoot,preview=root.querySelector('#preview'),recording=root.querySelector('#recording'),status=root.querySelector('#status');const {default:ScreenCapture}=await import('../modules/ScreenCapture.js');const {createArcaneEventSource,projectArcaneDOMEvent}=await import('arcane-os/event-manager');const events=createArcaneEventSource(host,{source:'arcane.component.screen-capture',eventTypes:['screen-capture-result','screen-capture-ready']});const capture=new ScreenCapture(),listeners=new AbortController();let destroyed=false,operationGeneration=0,previewUrl='';
5
5
  function emit(type,detail={},publicDetail={},operationId=null){if(destroyed)return false;const publication=events.dispatch(type,detail,{operationId,publicDetail});return projectArcaneDOMEvent(host,publication.occurrence,{bubbles:true,composed:true})}
6
6
  function current(generation){return !destroyed&&generation===operationGeneration}
7
7
  function cleanupCaptureStart(originalError=null){const cleanupErrors=[];if(capture.recorder&&capture.recorder.state!=='inactive'){try{capture.recorder.stop()}catch(error){cleanupErrors.push(error)}}try{capture.stopTracks(capture.stream)}catch(error){cleanupErrors.push(error)}try{capture.reset()}catch(error){cleanupErrors.push(error)}for(const error of cleanupErrors)console.error('Error cleaning up rejected screen capture start:',error);return originalError}
@@ -84,21 +84,16 @@
84
84
  ]
85
85
  }
86
86
  );
87
- const componentURL=new URL(
88
- host.getAttribute('href')||'./arcane/components/voice-transcription.html',
89
- document.baseURI
90
- );
91
- const moduleURL=name=>new URL(`../modules/${name}`,componentURL).href;
92
- const {default:MD}=await import(moduleURL('MD.js?v=2'));
87
+ const {default:MD}=await import('../modules/MD.js?v=2');
93
88
  const {
94
89
  appendTranscription,
95
90
  createSTTActivationController,
96
91
  normalizeVoiceOptions
97
- }=await import(moduleURL('ComponentContracts.js'));
92
+ }=await import('../modules/ComponentContracts.js');
98
93
  const {
99
94
  requestAIRuntimeIntent,
100
95
  subscribeAIRuntimeState
101
- }=await import(moduleURL('AIRuntimeState.js'));
96
+ }=await import('../modules/AIRuntimeState.js');
102
97
  const description=host.shadowRoot.querySelector('#description');
103
98
  const transcriptElement=host.shadowRoot.querySelector('#transcript');
104
99
  const status=host.shadowRoot.querySelector('#status');
@@ -598,7 +593,7 @@
598
593
  return options.transcribe(file,context);
599
594
  }
600
595
  if(typeof globalThis.ai?.fetchSTT!=='function'){
601
- await import(moduleURL('AI.js'));
596
+ await import('../modules/AI.js');
602
597
  }
603
598
  if(typeof globalThis.ai?.fetchSTT!=='function'){
604
599
  const error=new Error('Speech transcription is not configured.');
@@ -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',