arcane-os 0.25.0 → 0.27.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.
@@ -352,6 +352,7 @@
352
352
  //importing Document at the end of the file to avoid blocking;
353
353
 
354
354
  const host=this;
355
+ const componentHref=new URL(host.getAttribute('href'),document.baseURI);
355
356
  const {default:waitForComponent}=await import('../modules/WaitForComponent.js');
356
357
  const {createArcaneEventSource,projectArcaneDOMEvent}=await import('arcane-os/event-manager');
357
358
  const events=createArcaneEventSource(host,{source:'arcane.component.file-manager',eventTypes:['file-manager-action','file-manager-select','file-manager-open','file-manager-ready']});
@@ -830,8 +831,8 @@
830
831
  icon.className='tree-entry-icon';
831
832
  icon.alt='';
832
833
  icon.src=entry.kind==='directory'
833
- ?new URL('./arcane/img/folder.svg',document.baseURI).href
834
- :new URL('./arcane/img/doc.svg',document.baseURI).href;
834
+ ?new URL('../img/folder.svg',componentHref).href
835
+ :new URL('../img/doc.svg',componentHref).href;
835
836
  name.className='tree-entry-name';
836
837
  name.innerText=entry.name;
837
838
  count.className='tree-file-count';
@@ -1409,7 +1410,7 @@
1409
1410
  deleteButton.innerText='Delete File';
1410
1411
  deleteIcon.alt='';
1411
1412
  deleteIcon.className='action-icon';
1412
- deleteIcon.src=new URL('./arcane/img/trash.svg',document.baseURI).href;
1413
+ deleteIcon.src=new URL('../img/trash.svg',componentHref).href;
1413
1414
  deleteButton.prepend(deleteIcon);
1414
1415
  deleteButton.addEventListener(
1415
1416
  'click',
@@ -1615,7 +1616,7 @@
1615
1616
 
1616
1617
  fileIcon.alt='';
1617
1618
  fileIcon.className='icon file-icon';
1618
- fileIcon.src=new URL('./arcane/img/doc.svg',document.baseURI).href;
1619
+ fileIcon.src=new URL('../img/doc.svg',componentHref).href;
1619
1620
  fileDetails.className='file-details';
1620
1621
  fileLabel.className='file-name';
1621
1622
  fileLabel.innerText=fileName;
@@ -1638,7 +1639,7 @@
1638
1639
 
1639
1640
  deleteIcon.alt='';
1640
1641
  deleteIcon.className='action-icon';
1641
- deleteIcon.src=new URL('./arcane/img/trash.svg',document.baseURI).href;
1642
+ deleteIcon.src=new URL('../img/trash.svg',componentHref).href;
1642
1643
 
1643
1644
  openButton.addEventListener(
1644
1645
  'click',
@@ -1938,7 +1939,7 @@
1938
1939
  directory.dataset.directory=tableName;
1939
1940
  folder.className='file folder';
1940
1941
  folderIcon.className='icon';
1941
- folderIcon.src=new URL('./arcane/img/folder.svg',document.baseURI).href;
1942
+ folderIcon.src=new URL('../img/folder.svg',componentHref).href;
1942
1943
  folderName.innerText=tableName;
1943
1944
  fileCount.className='file-count';
1944
1945
  fileCount.innerText=`${files.length} ${files.length===1?'file':'files'}`;
@@ -0,0 +1,19 @@
1
+ export {
2
+ default,
3
+ default as Mail,
4
+ resolveMailConfig
5
+ } from './Mail.js';
6
+ export {
7
+ MAIL_OUTBOX_IDEMPOTENCY_WINDOW_MS,
8
+ MAIL_OUTBOX_PROTOCOL,
9
+ MAIL_OUTBOX_STATES,
10
+ MAIL_OUTBOX_TABLE,
11
+ MailOutbox,
12
+ createMailOutbox
13
+ } from './MailOutbox.mjs';
14
+ export {
15
+ MailTransportError,
16
+ normalizeMailEndpoint,
17
+ sendMailReport,
18
+ serializeMailReport
19
+ } from './MailTransport.mjs';
@@ -1,4 +1,5 @@
1
1
  import Is from 'strong-type';
2
+ import {resolveAppRoot} from './app-layout.mjs';
2
3
  import {isDeepStrictEqual} from 'node:util';
3
4
  import {readFile, writeFile} from 'node:fs/promises';
4
5
  import path from 'node:path';
@@ -427,7 +428,8 @@ async function readJsonFile(filePath,label,{optional=false}={}){
427
428
 
428
429
  export async function refreshAppPackageProjection({workspaceRoot, appId, signal, onEvent}) {
429
430
  throwIfAborted(signal);
430
- const appRoot = path.join(workspaceRoot, 'apps', appId);
431
+ const rootConfig=await readJsonFile(path.join(workspaceRoot,'arcane-packager.json'),'arcane-packager.json');
432
+ const appRoot = resolveAppRoot(workspaceRoot,rootConfig,appId);
431
433
  const descriptorPath = path.join(appRoot, APP_DESCRIPTOR_NAME);
432
434
  const authored = await readJsonFile(
433
435
  descriptorPath,
@@ -0,0 +1,32 @@
1
+ import path from 'node:path';
2
+
3
+ // The app identity is independent of whether its files occupy a workspace root.
4
+ export function appRelativeRoot(config, appId) {
5
+ return config.appsRoot === '.' ? '' : `apps/${appId}`;
6
+ }
7
+
8
+ export function resolveAppRoot(workspaceRoot, config, appId) {
9
+ return path.resolve(workspaceRoot, appRelativeRoot(config, appId));
10
+ }
11
+
12
+ export function appBaseHref(workspaceRoot, appRoot, document = 'index.html') {
13
+ const directory = path.dirname(path.resolve(appRoot, document));
14
+ const relative = path.relative(directory, workspaceRoot).split(path.sep).join('/');
15
+ return relative ? `${relative}/` : './';
16
+ }
17
+
18
+ export function rootAppNavigation(appId, entry, documents = []) {
19
+ const redirects=new Map();
20
+ for(const document of documents)redirects.set(`apps/${appId}/${document}`,document);
21
+ redirects.set(`apps/${appId}/index.html`,entry);
22
+ return [...redirects].map(([file,target])=>{
23
+ const relative=path.posix.relative(path.posix.dirname(file),target);
24
+ const destination=JSON.stringify(relative).replaceAll('<','\\u003c');
25
+ return {
26
+ path:file,target:`/${target}`,
27
+ content:'<!doctype html>\n<!-- Arcane root application navigation -->\n'
28
+ +'<meta charset="utf-8">\n<title>Opening application</title>\n'
29
+ +`<script>const target=new URL(${destination},location.href);target.search=location.search;target.hash=location.hash;location.replace(target.href);</script>\n`
30
+ };
31
+ });
32
+ }
@@ -124,7 +124,9 @@ async function selectedTestFiles({workspaceRoot,workspaceMode,appRoot,signal}){
124
124
  if(workspaceMode==='external'){
125
125
  await collectOptionalTests(path.join(workspaceRoot,'test'),workspaceRoot,files,signal);
126
126
  }
127
- if(appRoot)await collectOptionalTests(path.join(appRoot,'test'),appRoot,files,signal);
127
+ if(appRoot&&(workspaceMode!=='external'||!samePath(workspaceRoot,appRoot))){
128
+ await collectOptionalTests(path.join(appRoot,'test'),appRoot,files,signal);
129
+ }
128
130
  return [...files].sort();
129
131
  }
130
132
 
package/src/cli/main.mjs CHANGED
@@ -14,6 +14,7 @@ const VALUE_OPTIONS=new Set([
14
14
  'display-name',
15
15
  'workspace',
16
16
  'app',
17
+ 'apps-root',
17
18
  'sdk-runtime-source',
18
19
  'arcane-root',
19
20
  'host',
@@ -60,8 +61,8 @@ const MAX_NODE_TIMER_DELAY_MS=2_147_483_647;
60
61
  export const HELP_TEXT=`Arcane OS application SDK ${SDK_VERSION}
61
62
 
62
63
  Usage:
63
- ${CLI_NAME} new <id> [--path <directory>] [--display-name <name>] [--target <target>] [--git]
64
- ${CLI_NAME} init [id] [--workspace <directory>] [--display-name <name>] [--target <target>]
64
+ ${CLI_NAME} new <id> [--path <directory>] [--apps-root apps|.] [--display-name <name>] [--target <target>] [--git]
65
+ ${CLI_NAME} init [id] [--workspace <directory>] [--apps-root apps|.] [--display-name <name>] [--target <target>]
65
66
  ${CLI_NAME} upgrade [--workspace <directory>] [--app <id>]
66
67
  ${CLI_NAME} doctor [--workspace <directory>] [--arcane-root <directory>]
67
68
  ${CLI_NAME} import-map [--workspace <directory>] [--app <id>]
@@ -421,15 +422,6 @@ function readScope(value){
421
422
  return scope;
422
423
  }
423
424
 
424
- function inferredAppId(workspaceRoot){
425
- const id=path.basename(workspaceRoot)
426
- .normalize('NFKD')
427
- .toLowerCase()
428
- .replace(/[^a-z0-9]+/gu,'-')
429
- .replace(/^-+|-+$/gu,'');
430
- return id||'arcane-app';
431
- }
432
-
433
425
  function noExtraPositionals(command,positionals,expected=0){
434
426
  if(positionals.length>expected){
435
427
  usage(`Unexpected argument for ${command}.`);
@@ -503,6 +495,9 @@ function operationOptions(command,parsed,cwd){
503
495
  if(command!=='mail'&&flags.has('report-stdin')){
504
496
  usage('--report-stdin is supported only by mail send.');
505
497
  }
498
+ if(values['apps-root']!==undefined&&!['new','init'].includes(command)){
499
+ usage('--apps-root is supported only by new and init.');
500
+ }
506
501
 
507
502
  if(command==='new'){
508
503
  const appId=positionals[0];
@@ -511,6 +506,7 @@ function operationOptions(command,parsed,cwd){
511
506
  return {
512
507
  targetPath:path.resolve(cwd,values.path??appId),
513
508
  appId,
509
+ appsRoot:values['apps-root']??'apps',
514
510
  displayName:values['display-name'],
515
511
  target:values.target??'browser',
516
512
  initializeGit:flags.has('git')
@@ -520,7 +516,8 @@ function operationOptions(command,parsed,cwd){
520
516
  noExtraPositionals(command,positionals,1);
521
517
  return {
522
518
  workspaceRoot,
523
- appId:positionals[0]??values.app??inferredAppId(workspaceRoot),
519
+ appId:positionals[0]??values.app,
520
+ appsRoot:values['apps-root'],
524
521
  displayName:values['display-name'],
525
522
  target:values.target??'browser'
526
523
  };
@@ -6,6 +6,8 @@ import https from 'node:https';
6
6
  import os from 'node:os';
7
7
  import path from 'node:path';
8
8
  import {resolveWorkspace} from './workspace.mjs';
9
+ import {appRelativeRoot} from './app-layout.mjs';
10
+ import {readInstalledSdkLayout} from './sdk-runtime-layout.mjs';
9
11
  import {APP_DESCRIPTOR_NAME, projectPackageManifest} from './app-descriptor.mjs';
10
12
  import {APP_CONFIG_NAME, validateAppConfig} from './packager/core.mjs';
11
13
  import {createEventQueue} from './event-queue.mjs';
@@ -96,7 +98,7 @@ function parseRequestTarget(rawUrl){
96
98
  let parsed;
97
99
  try{parsed=new URL(raw,'http://127.0.0.1');}
98
100
  catch{return null;}
99
- return {segments,path:decoded,searchParams:parsed.searchParams};
101
+ return {segments,path:decoded,pathname:parsed.pathname,search:parsed.search,searchParams:parsed.searchParams};
100
102
  }
101
103
 
102
104
  function resolveInside(root,segments){
@@ -436,6 +438,7 @@ function sharedPathAllowed(relative,route){
436
438
  });
437
439
  if(excluded)return false;
438
440
  return route.include.some(item=>{
441
+ if(item==='.')return true;
439
442
  const candidate=inventoryKey(item);
440
443
  return comparable===candidate||comparable.startsWith(`${candidate}/`);
441
444
  });
@@ -448,7 +451,8 @@ async function sourceRoutes(workspaceRoot,appId,{
448
451
  }){
449
452
  const resolved=await resolveWorkspace({workspaceRoot,appId});
450
453
  const appMapping={
451
- prefix:['apps',resolved.appId],
454
+ kind:'app',
455
+ prefix:appRelativeRoot(resolved.config,resolved.appId).split('/').filter(Boolean),
452
456
  root:resolved.appRoot,
453
457
  include:resolved.app.manifest.include,
454
458
  allow:relative=>sourcePathAllowed(relative,resolved.app.manifest)
@@ -460,25 +464,39 @@ async function sourceRoutes(workspaceRoot,appId,{
460
464
  resolved.appId,
461
465
  {signal,onEvent}
462
466
  );
467
+ const installedRoutes=resolved.config.browserRuntimeLayout==='installed-v1'
468
+ ?resolved.config.sharedPayloads['browser-runtime']:null;
469
+ const sourceMappings=installedRoutes?sdkSource.mappings.map(
470
+ function selectLiveSourceDestination(mapping,index){
471
+ const route=installedRoutes[[0,2,1][index]];
472
+ return {...mapping,prefix:route.destination.split('/')};
473
+ }
474
+ ):sdkSource.mappings;
463
475
  return {
464
476
  workspaceRoot:resolved.workspaceRoot,
465
477
  workspaceMode:resolved.config.workspaceMode,
466
478
  config:resolved.config,
467
479
  appId:resolved.appId,
468
480
  app:resolved.app.manifest,
469
- startPath:`/apps/${resolved.appId}/${resolved.app.manifest.entry}`,
481
+ startPath:applicationSourcePath(resolved.config,resolved.appId,resolved.app.manifest.entry),
470
482
  runtime:sdkSource.runtime,
471
- mappings:[appMapping,...sdkSource.mappings]
483
+ ...(installedRoutes?{browserRuntimeBase:`/${installedRoutes[1].destination}/`}:{}),
484
+ mappings:[appMapping,...sourceMappings]
472
485
  };
473
486
  }
474
- if(resolved.config.workspaceMode==='integrated'){
487
+ if(resolved.config.workspaceMode==='integrated'
488
+ ||resolved.config.browserRuntimeLayout==='installed-v1'){
489
+ const installed=resolved.config.browserRuntimeLayout==='installed-v1'
490
+ ?await readInstalledSdkLayout(resolved.workspaceRoot,resolved.config)
491
+ :null;
475
492
  return {
476
493
  workspaceRoot:resolved.workspaceRoot,
477
- workspaceMode:'integrated',
494
+ workspaceMode:resolved.config.workspaceMode,
478
495
  config:resolved.config,
496
+ ...(installed?{installed}:{}),
479
497
  appId:resolved.appId,
480
498
  app:resolved.app.manifest,
481
- startPath:`/apps/${resolved.appId}/${resolved.app.manifest.entry}`,
499
+ startPath:applicationSourcePath(resolved.config,resolved.appId,resolved.app.manifest.entry),
482
500
  mappings:[
483
501
  appMapping,
484
502
  ...resolved.config.sharedPayloads['browser-runtime'].map(route=>({
@@ -497,7 +515,7 @@ async function sourceRoutes(workspaceRoot,appId,{
497
515
  config:resolved.config,
498
516
  appId:resolved.appId,
499
517
  app:resolved.app.manifest,
500
- startPath:`/apps/${resolved.appId}/${resolved.app.manifest.entry}`,
518
+ startPath:applicationSourcePath(resolved.config,resolved.appId,resolved.app.manifest.entry),
501
519
  mappings:[
502
520
  appMapping,
503
521
  {
@@ -510,6 +528,11 @@ async function sourceRoutes(workspaceRoot,appId,{
510
528
  };
511
529
  }
512
530
 
531
+ function applicationSourcePath(config,appId,relative='') {
532
+ const root=appRelativeRoot(config,appId);
533
+ return `/${root?`${root}/`:''}${relative}`;
534
+ }
535
+
513
536
  async function packagedRoutes(releaseRoot){
514
537
  if(!is.string(releaseRoot)||!releaseRoot.trim())fail('releaseRoot is required in packaged mode.','ARCANE_USAGE');
515
538
  const requested=path.resolve(releaseRoot);
@@ -571,15 +594,14 @@ async function sourcePwaAssets(routeSet, mappings, signal, resourceUrls, resourc
571
594
  } else if (info.isFile()) {
572
595
  const segments = [...mapping.prefix, ...relative];
573
596
  const url = `/${segments.map(encodeURIComponent).join('/')}`;
574
- const appResource = mapping.prefix[0] === 'apps'
575
- && mapping.prefix[1] === routeSet.appId;
597
+ const appResource = mapping.kind === 'app';
576
598
  const logical = (appResource ? relative : segments).join('/');
577
599
  records.set(logical, url);
578
600
  resources.set(url, {mapping, relative});
579
601
  }
580
602
  }
581
603
  for (const selected of mapping.include ?? ['']) {
582
- await visitSourceResource(selected ? selected.split('/') : []);
604
+ await visitSourceResource(selected && selected!=='.' ? selected.split('/') : []);
583
605
  }
584
606
  }
585
607
  const origin = 'http://arcane.invalid';
@@ -590,7 +612,7 @@ async function sourcePwaAssets(routeSet, mappings, signal, resourceUrls, resourc
590
612
  for (const selected of routeSet.app.include) {
591
613
  if (!/\.html?$/iu.test(selected)) continue;
592
614
  const url = new URL(
593
- `/apps/${routeSet.appId}/${selected.split('/').map(encodeURIComponent).join('/')}`,
615
+ applicationSourcePath(routeSet.config,routeSet.appId,selected.split('/').map(encodeURIComponent).join('/')),
594
616
  origin
595
617
  );
596
618
  pending.push({url, documentUrl: url});
@@ -629,8 +651,8 @@ async function sourcePwaAssets(routeSet, mappings, signal, resourceUrls, resourc
629
651
  const documentUrl = authoredBase ? new URL(authoredBase, current.url) : current.documentUrl;
630
652
  if (!runtimeRootsAdded && pathname === entryUrl.pathname) {
631
653
  runtimeRootsAdded = true;
632
- for (const url of resources.keys()) {
633
- if (/^\/arcane\/.*\.(?:m?js|html?|css)$/iu.test(url)
654
+ for (const [url, resource] of resources) {
655
+ if ((resource.mapping.kind !== 'app' && /\.(?:m?js|html?|css)$/iu.test(url))
634
656
  || path.posix.basename(url) === 'arcane.importmap.json') {
635
657
  pending.push({url: new URL(url, origin), documentUrl});
636
658
  }
@@ -866,7 +888,7 @@ async function startOwnedDevServer({
866
888
  const mappings=deterministicMappings(routeSet.mappings);
867
889
  const versionPath = mode === 'source'
868
890
  ? sdkRuntimeSourceRoot === undefined
869
- ? path.join(routeSet.workspaceRoot, 'arcane.lock.json')
891
+ ? routeSet.installed?.versionPath ?? path.join(routeSet.workspaceRoot, 'arcane.lock.json')
870
892
  : path.join(routeSet.runtime.sourceRoot, 'package.json')
871
893
  : undefined;
872
894
  const [generatorInputs, initialAssetVersion, versionInput] = await Promise.all(
@@ -897,6 +919,9 @@ async function startOwnedDevServer({
897
919
  );
898
920
  async function selectedAssetVersion(){
899
921
  if(mode!=='source')return undefined;
922
+ if(routeSet.installed)return JSON.parse(await readFile(
923
+ routeSet.installed.versionPath,'utf8'
924
+ )).version;
900
925
  return sdkRuntimeSourceRoot===undefined
901
926
  ?readWorkspaceAssetVersion(routeSet.workspaceRoot)
902
927
  :JSON.parse(await readFile(
@@ -956,7 +981,7 @@ async function startOwnedDevServer({
956
981
  let sourceManifestTask;
957
982
  const appMapping = mappings.find(
958
983
  function selectedApplicationMapping(mapping) {
959
- return mapping.prefix[0] === 'apps' && mapping.prefix[1] === routeSet.appId;
984
+ return mapping.kind === 'app';
960
985
  }
961
986
  );
962
987
  async function refreshSourceRoutes() {
@@ -1007,7 +1032,7 @@ async function startOwnedDevServer({
1007
1032
  currentSourceRoutes = {
1008
1033
  ...routeSet,
1009
1034
  app: manifest,
1010
- startPath: `/apps/${routeSet.appId}/${manifest.entry}`,
1035
+ startPath: applicationSourcePath(routeSet.config,routeSet.appId,manifest.entry),
1011
1036
  mappings: mappings.map(
1012
1037
  function currentSourceMapping(mapping) {
1013
1038
  return mapping === appMapping ? currentAppMapping : mapping;
@@ -1021,6 +1046,26 @@ async function startOwnedDevServer({
1021
1046
  let currentPwaState;
1022
1047
  const pwaResourceUrls = new Set();
1023
1048
  function developmentPwaArtifacts(selectedRoutes, assets = [], version = assetVersion) {
1049
+ const rootApp = selectedRoutes.config.appsRoot === '.';
1050
+ const appBase = applicationSourcePath(selectedRoutes.config,selectedRoutes.appId);
1051
+ const navigationAliases = {'/': selectedRoutes.startPath};
1052
+ if (rootApp) {
1053
+ const legacyBase = `/apps/${selectedRoutes.appId}`;
1054
+ navigationAliases[legacyBase] = selectedRoutes.startPath;
1055
+ navigationAliases[`${legacyBase}/`] = selectedRoutes.startPath;
1056
+ for (const asset of [selectedRoutes.startPath,...assets]) {
1057
+ const pathname = new URL(asset,'http://arcane.invalid').pathname;
1058
+ if (!/\.html?$/iu.test(pathname)) continue;
1059
+ const segments = decodeURIComponent(pathname).split('/').filter(Boolean);
1060
+ const mapping = selectedRoutes.mappings.find(function matchingNavigationRoute(route) {
1061
+ return route.prefix.every(function matchingNavigationSegment(segment,index) {
1062
+ return segments[index] === segment;
1063
+ });
1064
+ });
1065
+ if (mapping?.kind === 'app') navigationAliases[`${legacyBase}${pathname}`] = pathname;
1066
+ }
1067
+ navigationAliases[`${legacyBase}/index.html`] = selectedRoutes.startPath;
1068
+ }
1024
1069
  return createPwaArtifacts(
1025
1070
  {
1026
1071
  app: {
@@ -1033,10 +1078,12 @@ async function startOwnedDevServer({
1033
1078
  pwa: selectedRoutes.app.pwa,
1034
1079
  files: [],
1035
1080
  assets,
1036
- navigationAliases: {'/': selectedRoutes.startPath},
1081
+ navigationAliases,
1037
1082
  basePath: '/',
1038
- appBase: `/apps/${routeSet.appId}/`,
1039
- runtimeBase: '/arcane/sdk/',
1083
+ appBase,
1084
+ ...(rootApp ? {installationId:`/apps/${routeSet.appId}/`} : {}),
1085
+ runtimeBase: selectedRoutes.browserRuntimeBase
1086
+ ?? selectedRoutes.installed?.browserRuntimeBase ?? '/arcane/sdk/',
1040
1087
  mode: 'development'
1041
1088
  }
1042
1089
  );
@@ -1128,10 +1175,25 @@ async function startOwnedDevServer({
1128
1175
  const {segments}=target;
1129
1176
  const generatedPwaPath = ['/arcane.webmanifest', '/arcane-offline.json', '/arcane-sw.js', '/arcane-pwa.mjs']
1130
1177
  .includes(target.path);
1131
- const appRequest = segments[0] === 'apps' && segments[1] === routeSet.appId;
1178
+ const legacyAppRequest = mode === 'source' && routeSet.config.appsRoot === '.'
1179
+ && segments[0] === 'apps' && segments[1] === routeSet.appId;
1180
+ const requestedMapping = currentSourceRoutes.mappings.find(function currentRequestMapping(route) {
1181
+ return route.prefix.every(function currentRequestSegment(segment,index) {
1182
+ return segments[index] === segment;
1183
+ });
1184
+ });
1185
+ const appRequest = requestedMapping?.kind === 'app' || legacyAppRequest;
1132
1186
  const selectedRoutes = mode === 'source' && (appRequest || segments.length === 0 || generatedPwaPath)
1133
1187
  ? await refreshSourceRoutes() : currentSourceRoutes;
1134
1188
  const pwaEnabled = mode === 'source' ? selectedRoutes.app?.pwa?.enabled === true : routeSet.pwa;
1189
+ if (legacyAppRequest) {
1190
+ const legacyPath = target.pathname.slice(`/apps/${routeSet.appId}`.length);
1191
+ const location = !legacyPath || legacyPath === '/' || legacyPath === '/index.html'
1192
+ ? selectedRoutes.startPath : legacyPath;
1193
+ response.writeHead(302,{location:`${location}${target.search}`});
1194
+ response.end();
1195
+ return;
1196
+ }
1135
1197
  if (mode === 'source' && pwaEnabled
1136
1198
  && generatedPwaPath) {
1137
1199
  const generated = await sourcePwaArtifact(target.path, selectedRoutes);
@@ -1153,7 +1215,7 @@ async function startOwnedDevServer({
1153
1215
  return;
1154
1216
  }
1155
1217
  if(segments.length===0){
1156
- response.writeHead(302,{location:selectedRoutes.startPath});
1218
+ response.writeHead(302,{location:`${selectedRoutes.startPath}${target.search}`});
1157
1219
  response.end();
1158
1220
  return;
1159
1221
  }
@@ -1185,7 +1247,7 @@ async function startOwnedDevServer({
1185
1247
  const extension=path.extname(opened.candidate).toLowerCase();
1186
1248
  const html=extension==='.html'||extension==='.htm';
1187
1249
  const selectedPwaDocument = mode === 'source' && pwaEnabled && html
1188
- && mapping.prefix[0] === 'apps' && mapping.prefix[1] === routeSet.appId
1250
+ && mapping.kind === 'app'
1189
1251
  && selectedRoutes.app.include.includes(relative.join('/'));
1190
1252
  const selectedDocument = target.path === selectedRoutes.startPath || selectedPwaDocument;
1191
1253
  const managedDocument = !selectedDocument && html && await isManagedDocument(opened);
@@ -1194,7 +1256,7 @@ async function startOwnedDevServer({
1194
1256
  // A live server can span an SDK upgrade. Refresh the small
1195
1257
  // version record on navigation, not on each resource request.
1196
1258
  if(entryDocument||managedMap)rememberAssetVersion(await selectedAssetVersion());
1197
- const runtimeResource=segments[0]==='arcane';
1259
+ const runtimeResource=mapping.kind!=='app';
1198
1260
  const browserResource=['script','style','worker','sharedworker','serviceworker']
1199
1261
  .includes(request.headers['sec-fetch-dest']);
1200
1262
  const rewrite=runtimeResource||entryDocument||browserResource||managedMap
@@ -1,9 +1,11 @@
1
1
  import Is from 'strong-type';
2
+ import {appBaseHref,resolveAppRoot} from './app-layout.mjs';
2
3
  import {lstat,mkdir,readFile as readFileFromDisk,readdir,realpath,writeFile} from 'node:fs/promises';
3
4
  import path from 'node:path';
4
5
  import {pathToFileURL} from 'node:url';
5
6
  import {SDK_VERSION} from './constants.mjs';
6
7
  import {listRuntimeFiles} from './runtime.mjs';
8
+ import {readInstalledSdkLayout,installedRuntimeFiles,installedRuntimeTarget} from './sdk-runtime-layout.mjs';
7
9
 
8
10
  const is = new Is(false);
9
11
 
@@ -19,7 +21,23 @@ const SDK_BROWSER_AI_ENTRY='sdk/ai/browser-wasm.mjs';
19
21
  const SDK_BROWSER_SPEECH_ENTRY='sdk/ai/browser-speech.mjs';
20
22
  const STATIC_RUNTIME_PACKAGE_IMPORTS=new Map([
21
23
  ['arcane-os/preference-store','modules/PreferenceStore.js'],
22
- ['arcane-os/speech-playback','modules/SpeechPlayback.js']
24
+ ['arcane-os/speech-playback','modules/SpeechPlayback.js'],
25
+ ['arcane-os/ai','modules/AI.js'],
26
+ ['arcane-os/ai-preference-tuple','modules/AIPreferenceTuple.js'],
27
+ ['arcane-os/ai-preference-runtime','modules/AIPreferenceRuntime.js'],
28
+ ['arcane-os/ai-provider-runtime','modules/AIProviderRuntime.js'],
29
+ ['arcane-os/ai-runtime-state','modules/AIRuntimeState.js'],
30
+ ['arcane-os/model-definition','modules/ModelDefinition.js'],
31
+ ['arcane-os/conversation-timebox','modules/ConversationTimebox.js'],
32
+ ['arcane-os/conversation-action-items','modules/ConversationActionItems.js'],
33
+ ['arcane-os/conversation-closing-report','modules/ConversationClosingReport.js'],
34
+ ['arcane-os/chat-records','modules/ChatRecords.js'],
35
+ ['arcane-os/app-data-scope','modules/AppDataScope.js'],
36
+ ['arcane-os/core-local-model-catalog','modules/CoreLocalModelCatalog.js'],
37
+ ['arcane-os/dbopfs-document-library','modules/DBOPFSDocumentLibrary.js'],
38
+ ['arcane-os/local-ai-readiness','modules/LocalAIReadiness.js'],
39
+ ['arcane-os/ollama-model-identifier','modules/OllamaModelIdentifier.js'],
40
+ ['arcane-os/mail','modules/MailApi.mjs']
23
41
  ]);
24
42
  const SDK_BROWSER_SELF_IMPORTS=new Map([
25
43
  ['arcane-os/event-manager',SDK_BROWSER_ENTRY],
@@ -1436,16 +1454,33 @@ async function physicalRuntime(workspaceRoot,signal){
1436
1454
  }
1437
1455
 
1438
1456
  async function managedImportMapBuild(resolvedWorkspace,signal,pwaEnabled=false){
1457
+ const installed=await readInstalledSdkLayout(resolvedWorkspace);
1439
1458
  const [runtime,version]=await Promise.all([
1440
- physicalRuntime(resolvedWorkspace,signal),
1441
- pwaEnabled?null:readWorkspaceAssetVersion(resolvedWorkspace)
1459
+ installed?installedRuntimeFiles(resolvedWorkspace,installed,signal):physicalRuntime(resolvedWorkspace,signal),
1460
+ pwaEnabled?null:installed?.version??readWorkspaceAssetVersion(resolvedWorkspace)
1442
1461
  ]);
1443
1462
  const built=await buildImportMap({files:runtime.files,signal,version});
1463
+ if(installed?.direct){
1464
+ const imports={};
1465
+ function installedBrowserUrl(value){
1466
+ return value.startsWith('./')
1467
+ ?`./${installedRuntimeTarget(value.slice(2),installed,{browser:true})}`:value;
1468
+ }
1469
+ for(const [specifier,target] of Object.entries(built.imports)){
1470
+ const selected=installedBrowserUrl(target);
1471
+ imports[specifier]=selected;
1472
+ // Relative module imports and bare names must resolve to the same instance.
1473
+ imports[installedBrowserUrl(specifier)]=selected;
1474
+ }
1475
+ built.imports=imports;
1476
+ }
1444
1477
  const json=`${JSON.stringify({imports:built.imports},null,2).replaceAll('<','\\u003c')}\n`;
1445
1478
  return {built,json,version};
1446
1479
  }
1447
1480
 
1448
1481
  export async function readWorkspaceAssetVersion(workspaceRoot){
1482
+ const installed=await readInstalledSdkLayout(workspaceRoot);
1483
+ if(installed)return installed.version;
1449
1484
  let source;
1450
1485
  try{source=await readFileFromDisk(path.join(workspaceRoot,'arcane.lock.json'),'utf8');}
1451
1486
  catch(error){
@@ -2158,12 +2193,6 @@ export function inspectImportMapHtml(html){
2158
2193
  };
2159
2194
  }
2160
2195
 
2161
- function documentBaseHref(relative){
2162
- const directory=path.posix.dirname(relative);
2163
- const depth=directory==='.'?0:directory.split('/').length;
2164
- return '../'.repeat(depth+2);
2165
- }
2166
-
2167
2196
  function renderManagedHtml(html,json,baseHref='../../'){
2168
2197
  const structure=scanHtmlStructure(html);
2169
2198
  const activeBases=structure.bases.map(base=>({
@@ -2320,11 +2349,18 @@ async function writeGeneratedFiles({root,files,signal,onEvent}){
2320
2349
  return eventError;
2321
2350
  }
2322
2351
 
2323
- function resolvedAppRoot(workspaceRoot,appId,appRoot){
2352
+ async function resolvedAppRoot(workspaceRoot,appId,appRoot){
2324
2353
  if(!is.string(appId)||appId.trim()===''){
2325
2354
  throw new TypeError('Import-map app id must be a nonempty string.');
2326
2355
  }
2327
- const resolved=path.resolve(appRoot??path.join(workspaceRoot,'apps',appId));
2356
+ let selected=appRoot;
2357
+ if(selected===undefined){
2358
+ let config={appsRoot:'apps'};
2359
+ try{config=JSON.parse(await readFileFromDisk(path.join(workspaceRoot,'arcane-packager.json'),'utf8'));}
2360
+ catch(error){if(error?.code!=='ENOENT')throw error;}
2361
+ selected=resolveAppRoot(workspaceRoot,config,appId);
2362
+ }
2363
+ const resolved=path.resolve(selected);
2328
2364
  if(!pathInside(workspaceRoot,resolved))fail('Import-map application root must stay inside the workspace.');
2329
2365
  return resolved;
2330
2366
  }
@@ -2434,10 +2470,17 @@ export async function readApplicationTestImportMapContext({
2434
2470
  ||is.array(document.imports)){
2435
2471
  fail('Application test import-map artifact must contain an imports object.');
2436
2472
  }
2473
+ const installed=await readInstalledSdkLayout(resolvedWorkspaceRoot);
2474
+ const imports=installed?Object.fromEntries(Object.entries(document.imports).map(
2475
+ function installedApplicationTestTarget([specifier,target]){
2476
+ return [specifier,is.string(target)&&target.startsWith('./')
2477
+ ?`./${installedRuntimeTarget(target.slice(2),installed)}`:target];
2478
+ }
2479
+ )):document.imports;
2437
2480
  return createApplicationTestImportMapContext({
2438
2481
  applicationRoot:resolvedWorkspaceRoot,
2439
2482
  boundary:'source',
2440
- imports:document.imports,
2483
+ imports,
2441
2484
  signal
2442
2485
  });
2443
2486
  }
@@ -2762,7 +2805,7 @@ async function generateImportMapUnlocked({
2762
2805
  }
2763
2806
  throwIfAborted(signal);
2764
2807
  const resolvedWorkspace=path.resolve(workspaceRoot);
2765
- const resolvedApp=resolvedAppRoot(resolvedWorkspace,appId,appRoot);
2808
+ const resolvedApp=await resolvedAppRoot(resolvedWorkspace,appId,appRoot);
2766
2809
  await physicalDirectory(resolvedWorkspace,resolvedApp);
2767
2810
  const safeEntry=safeRelativePath(entry,'application entry');
2768
2811
  const safeDocuments=normalizedDocumentPaths(safeEntry,documents);
@@ -2792,7 +2835,7 @@ async function generateImportMapUnlocked({
2792
2835
  const html=await readPhysicalTextFile(resolvedApp,documentPath,label);
2793
2836
  throwIfAborted(signal);
2794
2837
  // Reject malformed application structure before traversing the runtime inventory.
2795
- const baseHref=documentBaseHref(safeDocuments[index]);
2838
+ const baseHref=appBaseHref(resolvedWorkspace,resolvedApp,safeDocuments[index]);
2796
2839
  renderManagedHtml(html,'{"imports":{}}\n',baseHref);
2797
2840
  documentStates.push({filePath:documentPath,html,label,baseHref});
2798
2841
  }
package/src/mail-api.mjs CHANGED
@@ -1,19 +1,2 @@
1
- export {
2
- default,
3
- default as Mail,
4
- resolveMailConfig
5
- } from '../runtime/arcane/modules/Mail.js';
6
- export {
7
- MAIL_OUTBOX_IDEMPOTENCY_WINDOW_MS,
8
- MAIL_OUTBOX_PROTOCOL,
9
- MAIL_OUTBOX_STATES,
10
- MAIL_OUTBOX_TABLE,
11
- MailOutbox,
12
- createMailOutbox
13
- } from '../runtime/arcane/modules/MailOutbox.mjs';
14
- export {
15
- MailTransportError,
16
- normalizeMailEndpoint,
17
- sendMailReport,
18
- serializeMailReport
19
- } from '../runtime/arcane/modules/MailTransport.mjs';
1
+ export {default} from '../runtime/arcane/modules/MailApi.mjs';
2
+ export * from '../runtime/arcane/modules/MailApi.mjs';