arcane-os 0.52.0 → 0.53.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.
@@ -543,11 +543,13 @@ async function packagedRoutes(releaseRoot){
543
543
  const canonical=await canonicalRealDirectory(requested,'Packaged release root');
544
544
  let pwa = false;
545
545
  let startPath = '/index.html';
546
+ let rootDocument = null;
546
547
  try {
547
548
  const release = JSON.parse(
548
549
  await readFile(path.join(canonical, 'ARCANE_APP_RELEASE.json'), 'utf8')
549
550
  );
550
551
  const start = release.app.start ?? release.app.entry;
552
+ if (release.app.rootDocument === './index.html') rootDocument = '/index.html';
551
553
  const startUrl = new URL(start, 'http://arcane.invalid/');
552
554
  startPath = `${startUrl.pathname}${startUrl.search}${startUrl.hash}`;
553
555
  } catch (error) {
@@ -564,6 +566,7 @@ async function packagedRoutes(releaseRoot){
564
566
  appId:null,
565
567
  pwa,
566
568
  startPath,
569
+ rootDocument,
567
570
  mappings:[{
568
571
  prefix:[],
569
572
  root:canonical
@@ -1026,19 +1029,30 @@ async function startOwnedDevServer({
1026
1029
  }
1027
1030
  async function readCurrentSourceRoutes() {
1028
1031
  throwIfAborted(signal);
1032
+ function optionalSourceMetadata(error) {
1033
+ if (error.code !== 'ENOENT') throw error;
1034
+ return null;
1035
+ }
1029
1036
  let manifestPath = path.join(appMapping.root, APP_DESCRIPTOR_NAME);
1037
+ const [rootIndex, descriptorInput] = await Promise.all(
1038
+ [
1039
+ lstat(path.join(routeSet.workspaceRoot, 'index.html')).catch(optionalSourceMetadata),
1040
+ lstat(manifestPath).catch(optionalSourceMetadata)
1041
+ ]
1042
+ );
1043
+ const rootDocument = rootIndex?.isFile() ? '/index.html' : null;
1030
1044
  let authored = true;
1031
- let input;
1032
- try {
1033
- input = await lstat(manifestPath);
1034
- } catch (error) {
1035
- if (error.code !== 'ENOENT') throw error;
1045
+ let input = descriptorInput;
1046
+ if (!input) {
1036
1047
  authored = false;
1037
1048
  manifestPath = path.join(appMapping.root, APP_CONFIG_NAME);
1038
1049
  input = await lstat(manifestPath);
1039
1050
  }
1040
1051
  if (sourceManifestInput?.path === manifestPath
1041
1052
  && sourceManifestInput.modifiedAt === input.mtimeMs) {
1053
+ if (currentSourceRoutes.rootDocument !== rootDocument) {
1054
+ currentSourceRoutes = {...currentSourceRoutes, rootDocument};
1055
+ }
1042
1056
  return currentSourceRoutes;
1043
1057
  }
1044
1058
  const value = JSON.parse(await readFile(manifestPath, 'utf8'));
@@ -1060,6 +1074,7 @@ async function startOwnedDevServer({
1060
1074
  currentSourceRoutes = {
1061
1075
  ...routeSet,
1062
1076
  app: manifest,
1077
+ rootDocument,
1063
1078
  startPath: applicationSourcePath(routeSet.config,routeSet.appId,manifest.entry),
1064
1079
  mappings: mappings.map(
1065
1080
  function currentSourceMapping(mapping) {
@@ -1076,7 +1091,7 @@ async function startOwnedDevServer({
1076
1091
  function developmentPwaArtifacts(selectedRoutes, assets = [], version = assetVersion) {
1077
1092
  const rootApp = selectedRoutes.config.appsRoot === '.';
1078
1093
  const appBase = applicationSourcePath(selectedRoutes.config,selectedRoutes.appId);
1079
- const navigationAliases = {'/': selectedRoutes.startPath};
1094
+ const navigationAliases = selectedRoutes.rootDocument ? {} : {'/': selectedRoutes.startPath};
1080
1095
  return createPwaArtifacts(
1081
1096
  {
1082
1097
  app: {
@@ -1104,12 +1119,21 @@ async function startOwnedDevServer({
1104
1119
  function rememberPwaArtifacts(state, artifacts, generatedAt = new Date()) {
1105
1120
  for (const file of artifacts.files) {
1106
1121
  const previous = state.metadata.get(file.path);
1107
- const lastModified = previous?.content === file.content
1122
+ const previousTime = previous?.lastModified
1123
+ ? Date.parse(previous.lastModified.toUTCString()) : previous?.ambiguousThrough;
1124
+ let lastModified = previous?.content === file.content
1108
1125
  ? previous.lastModified
1109
1126
  : generatedAt;
1127
+ if (!lastModified && generatedAt.getTime() >= previousTime + 1000) {
1128
+ lastModified = generatedAt;
1129
+ }
1130
+ // A changed alias must not validate the prior body within one HTTP second.
1131
+ const ambiguous = previous?.content !== file.content && previousTime !== undefined
1132
+ && Date.parse(generatedAt.toUTCString()) <= previousTime;
1133
+ if (ambiguous) lastModified = undefined;
1110
1134
  state.metadata.set(
1111
1135
  file.path,
1112
- {content: file.content, lastModified}
1136
+ {content: file.content, lastModified, ambiguousThrough: lastModified ? undefined : previousTime}
1113
1137
  );
1114
1138
  }
1115
1139
  state.artifacts = artifacts;
@@ -1277,22 +1301,29 @@ async function startOwnedDevServer({
1277
1301
  );
1278
1302
  return;
1279
1303
  }
1280
- if(segments.length===0){
1304
+ const rootRequest = segments.length === 0;
1305
+ if (rootRequest && !selectedRoutes.rootDocument) {
1281
1306
  response.writeHead(302,{location:`${selectedRoutes.startPath}${target.search}`});
1282
1307
  response.end();
1283
1308
  return;
1284
1309
  }
1285
- const mapping = selectedRoutes.mappings.find(
1286
- function requestedSourceMapping(route) {
1287
- return route.prefix.every(
1288
- function matchingSourcePrefix(segment, index) {
1289
- return segments[index] === segment;
1290
- }
1291
- );
1292
- }
1293
- );
1310
+ const resourceSegments = rootRequest ? ['index.html'] : segments;
1311
+ const hostRoot = rootRequest && mode === 'source'
1312
+ && (selectedRoutes.config.appsRoot !== '.' || !sourcePathAllowed(resourceSegments, selectedRoutes.app));
1313
+ // Only the host's exact root document is outside app selection.
1314
+ // Its direct URL and every other resource keep the selected routes.
1315
+ const mapping = hostRoot ? {kind: 'host', root: selectedRoutes.workspaceRoot, prefix: []}
1316
+ : selectedRoutes.mappings.find(
1317
+ function requestedSourceMapping(route) {
1318
+ return route.prefix.every(
1319
+ function matchingSourcePrefix(segment, index) {
1320
+ return resourceSegments[index] === segment;
1321
+ }
1322
+ );
1323
+ }
1324
+ );
1294
1325
  if(!mapping){deny(response,404,'Not found.');return;}
1295
- const relative=segments.slice(mapping.prefix.length);
1326
+ const relative=resourceSegments.slice(mapping.prefix.length);
1296
1327
  if(relative.length===0){deny(response,404,'Not found.');return;}
1297
1328
  if(mapping.allow&&!mapping.allow(relative)){deny(response,404,'Not found.');return;}
1298
1329
  await runFileWork(async function serveMappedResource() {
@@ -1307,6 +1338,18 @@ async function startOwnedDevServer({
1307
1338
  deny(response,404,'Not found.');
1308
1339
  return;
1309
1340
  }
1341
+ if (hostRoot) {
1342
+ const representation = sourceRepresentation(opened, assetVersion, false, false);
1343
+ await serveSourceFile(
1344
+ fileServer,
1345
+ request,
1346
+ response,
1347
+ opened,
1348
+ {lastModified: representation.lastModified, representationTracked: true}
1349
+ );
1350
+ return;
1351
+ }
1352
+ const resourcePath = rootRequest ? selectedRoutes.rootDocument : target.path;
1310
1353
  const extension=path.extname(opened.candidate).toLowerCase();
1311
1354
  const html=extension==='.html'||extension==='.htm';
1312
1355
  const explicitDocuments = mode === 'source' && mapping.kind === 'app'
@@ -1316,7 +1359,7 @@ async function startOwnedDevServer({
1316
1359
  const selectedPwaDocument = mode === 'source' && pwaEnabled && html
1317
1360
  && mapping.kind === 'app'
1318
1361
  && !explicitDocuments && selectedRoutes.app.include.includes(relative.join('/'));
1319
- const selectedDocument = target.path === selectedRoutes.startPath || explicitDocument || selectedPwaDocument;
1362
+ const selectedDocument = resourcePath === selectedRoutes.startPath || explicitDocument || selectedPwaDocument;
1320
1363
  const managedDocument = !selectedDocument && !explicitDocuments && html && await isManagedDocument(opened);
1321
1364
  const entryDocument = selectedDocument || managedDocument;
1322
1365
  const managedMap=path.basename(opened.candidate)==='arcane.importmap.json';
@@ -1324,22 +1367,22 @@ async function startOwnedDevServer({
1324
1367
  // version record on navigation, not on each resource request.
1325
1368
  if(entryDocument||managedMap)rememberAssetVersion(await selectedAssetVersion());
1326
1369
  const runtimeResource=mapping.kind!=='app';
1327
- if (runtimeResource && html) runtimeDocuments.add(target.path);
1370
+ if (runtimeResource && html) runtimeDocuments.add(resourcePath);
1328
1371
  const browserResource=['script','style','worker','sharedworker','serviceworker']
1329
1372
  .includes(request.headers['sec-fetch-dest']);
1330
1373
  // Selecting application pages does not turn retained originals
1331
1374
  // into resources. HTML components remain owned by html-import.
1332
1375
  const componentDocument=explicitDocuments&&html&&!selectedDocument
1333
- &&await isCurrentComponent(target.path, selectedRoutes);
1376
+ &&await isCurrentComponent(resourcePath, selectedRoutes);
1334
1377
  const retainedDocument=explicitDocuments&&html&&!selectedDocument&&!componentDocument;
1335
1378
  const rewrite=!retainedDocument&&(runtimeResource||entryDocument||browserResource||managedMap
1336
- ||componentDocument||resourcePaths.has(target.path));
1379
+ ||componentDocument||resourcePaths.has(resourcePath));
1337
1380
  const onReference = function observeServedResource(reference) {
1338
1381
  const {url,kind,baseKind}=reference;
1339
1382
  if (pwaEnabled && kind !== 'fetch'
1340
1383
  && !(kind === 'import' && !/^(?:\.{1,2}\/|\/)/u.test(url))) {
1341
1384
  try {
1342
- const documentUrl = new URL(target.path, 'http://arcane.invalid');
1385
+ const documentUrl = new URL(resourcePath, 'http://arcane.invalid');
1343
1386
  const resource = resolveAssetReference(
1344
1387
  {...reference,url:versionAssetUrl(url,null)},
1345
1388
  {ownerUrl:documentUrl}
@@ -1354,7 +1397,7 @@ async function startOwnedDevServer({
1354
1397
  if(baseKind==='document'||kind==='fetch'||(kind==='asset'&&!/\.css(?:[?#]|$)/iu.test(url))
1355
1398
  ||(kind==='import'&&!/^(?:\.{1,2}\/|\/)/u.test(url)))return;
1356
1399
  try{
1357
- const documentUrl=new URL(target.path,'http://arcane.invalid');
1400
+ const documentUrl=new URL(resourcePath,'http://arcane.invalid');
1358
1401
  const resource=resolveAssetReference(reference,{ownerUrl:documentUrl});
1359
1402
  if(resource.origin===documentUrl.origin
1360
1403
  &&/\.(?:m?js|html?|css)$/iu.test(resource.pathname)){
package/src/index.mjs CHANGED
@@ -146,6 +146,7 @@ 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';
149
150
  export {
150
151
  NATIVE_BUILD_PLAN_PROTOCOL,
151
152
  NATIVE_BUILDER_PROTOCOL,
@@ -788,7 +788,7 @@ async function loadAdapter(context){
788
788
  return module;
789
789
  }
790
790
 
791
- function releaseManifest(context,files,pwaArtifacts){
791
+ function releaseManifest(context,files,pwaArtifacts,rootDocument){
792
792
  return {
793
793
  schemaVersion:1,
794
794
  kind:'arcane-app-release',
@@ -799,6 +799,7 @@ function releaseManifest(context,files,pwaArtifacts){
799
799
  version:context.config.version,
800
800
  entry:context.config.entry,
801
801
  start:packageResourceUrl(appPackagePath(context, context.config.entry)),
802
+ ...(rootDocument ? {rootDocument} : {}),
802
803
  strategy:context.config.strategy,
803
804
  shared:[...context.config.shared],
804
805
  ...(pwaArtifacts?{pwa:{
@@ -845,6 +846,11 @@ async function packageWithContext(context,options={}){
845
846
  const appPath=appRelativeRoot(context.rootConfig,context.appId);
846
847
  const entryPath=appPackagePath(context,context.config.entry);
847
848
  const records=await collectPackageRecords(context,{signal});
849
+ const rootDocument = records.some(
850
+ function selectedRootIndex(record) {
851
+ return record.destination === 'index.html';
852
+ }
853
+ ) ? './index.html' : undefined;
848
854
  const inspected=await inspectContext(context,{signal,records});
849
855
  if(options.dryRun){
850
856
  return {
@@ -888,9 +894,7 @@ async function packageWithContext(context,options={}){
888
894
  try{
889
895
  async function copyBase() {
890
896
  await copyRecords(records,stagingRoot,{signal,onEvent});
891
- if (!records.some(function selectedRootIndex(record) {
892
- return record.destination === 'index.html';
893
- })) {
897
+ if (!rootDocument) {
894
898
  await writePackageLauncher(context, stagingRoot);
895
899
  }
896
900
  }
@@ -1050,6 +1054,7 @@ async function packageWithContext(context,options={}){
1050
1054
  sdkVersion:assetVersion,
1051
1055
  pwa:context.config.pwa,
1052
1056
  files,
1057
+ navigationAliases: rootDocument ? {} : {'./': packageResourceUrl(entryPath)},
1053
1058
  assets:[...offlineReferences]
1054
1059
  }):null;
1055
1060
  if(pwaArtifacts){
@@ -1080,7 +1085,7 @@ async function packageWithContext(context,options={}){
1080
1085
  }
1081
1086
  files.sort(compareText);
1082
1087
  }
1083
- const manifest=releaseManifest(context,files,pwaArtifacts);
1088
+ const manifest=releaseManifest(context,files,pwaArtifacts,rootDocument);
1084
1089
  await writeFile(
1085
1090
  path.join(stagingRoot,RELEASE_MANIFEST_NAME),
1086
1091
  `${JSON.stringify(manifest,null,2)}\n`,
@@ -496,8 +496,10 @@ function installPwaWorker(manifest, clientUrl) {
496
496
 
497
497
  function staticNavigationResource(url) {
498
498
  const document = new URL(url);
499
- if (document.origin !== scopeOrigin || !document.search
500
- || !/\.html?$/iu.test(document.pathname)) {
499
+ if (document.origin !== scopeOrigin) return null;
500
+ if (document.pathname.endsWith('/')) {
501
+ document.pathname += 'index.html';
502
+ } else if (!document.search || !/\.html?$/iu.test(document.pathname)) {
501
503
  return null;
502
504
  }
503
505
  document.search = '';
package/src/toolchain.mjs CHANGED
@@ -330,24 +330,41 @@ async function refreshRootApplicationFiles(prepared,inspected,importMap,{signal,
330
330
  const manifest=prepared.validation.app.manifest;
331
331
  const installed=await readInstalledSdkLayout(workspaceRoot,prepared.validation.config);
332
332
  const entry=`/${manifest.entry.split('/').map(encodeURIComponent).join('/')}`;
333
- const navigationAliases={
334
- '/':entry
335
- };
333
+ const pwaEnabled = installed?.direct && manifest.pwa?.enabled;
334
+ let authoredRoot = false;
335
+ if (pwaEnabled) {
336
+ try {
337
+ authoredRoot = (await lstat(path.join(workspaceRoot, 'index.html'))).isFile();
338
+ } catch (error) {
339
+ if (error.code !== 'ENOENT') throw error;
340
+ }
341
+ }
342
+ // A host homepage need not belong to the selected application or its cache.
343
+ const navigationAliases = authoredRoot ? {} : {'/': entry};
344
+ function rootIndexPath(relative) {
345
+ return (process.platform === 'win32' ? relative.toLowerCase() : relative) === 'index.html';
346
+ }
347
+ const selectedRootIndex = authoredRoot && manifest.include.some(rootIndexPath)
348
+ && !manifest.exclude.some(rootIndexPath);
336
349
  // The source host serves the installed files in place. There is no runtime projection.
337
350
  const files=[...new Set([
338
- ...inspected.files.filter(file=>file!=='index.html'||manifest.include.includes('index.html')),
351
+ ...inspected.files.filter(
352
+ function selectedSourceFile(file) {
353
+ return file !== 'index.html' || selectedRootIndex;
354
+ }
355
+ ),
339
356
  importMap.artifactRelativePath
340
357
  ])];
341
358
  const sourceOrigin='http://arcane.invalid';
342
359
  const selectedPaths=new Set(files.map(function selectedStaticPath(file){
343
360
  return new URL(`/${file.split('/').map(encodeURIComponent).join('/')}`,sourceOrigin).pathname;
344
361
  }));
345
- const assets=installed?.direct&&manifest.pwa?.enabled
362
+ const assets=pwaEnabled
346
363
  ?(await collectSourcePwaAssets({workspaceRoot,appId,signal,onEvent})).filter(function selectedStaticVariant(value){
347
364
  const url=new URL(value,sourceOrigin);
348
365
  return url.origin===sourceOrigin&&selectedPaths.has(url.pathname);
349
366
  }):[];
350
- const pwa=installed?.direct&&manifest.pwa?.enabled?createPwaArtifacts({
367
+ const pwa=pwaEnabled?createPwaArtifacts({
351
368
  app:{id:appId,displayName:manifest.displayName,version:manifest.version,entry},
352
369
  sdkVersion:installed.version,
353
370
  pwa:manifest.pwa,