arcane-os 0.52.0 → 0.52.1

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
@@ -1,5 +1,24 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.52.1
4
+
5
+ - Preserve an authored host `index.html` at `/` independently of application
6
+ entry and offline selection. Root import-map/PWA generation no longer aliases
7
+ that public homepage to the app entry. Existing app-only roots retain their
8
+ launcher behavior; excluded homepage content and assets stay outside the
9
+ application's managed documents and offline inventory.
10
+ - Source development follows root-file creation/removal without a descriptor
11
+ edit, serves an unselected host root unchanged, and avoids stale conditional
12
+ responses when generated aliases change within one HTTP second. Ordinary
13
+ selected routes, startup entry, manifest identity and worker lifecycle remain
14
+ unchanged.
15
+ - Packaged authored roots carry optional generated `app.rootDocument` metadata
16
+ so preview serves them at `/`; older and app-only packages retain their
17
+ existing redirects and queries. Worker directory navigation uses only indexes
18
+ already selected in its current offline inventory, with exact-query priority.
19
+ No dependency, configuration option, saved-data migration or cache reset is
20
+ introduced. Consumers regenerate their normal public SDK outputs after update.
21
+
3
22
  ## 0.52.0
4
23
 
5
24
  - Add optional `package.documents` to select managed application HTML separately
package/README.md CHANGED
@@ -20,7 +20,7 @@ selected SDK projection or an explicit live SDK source mount. Both profiles
20
20
  share the theme, packaging, event, cancellation,
21
21
  and browser run contracts while retaining their selected application layout.
22
22
 
23
- This checkout defines the `0.52.0` SDK contract. Applications pin one exact npm
23
+ This checkout defines the `0.52.1` SDK contract. Applications pin one exact npm
24
24
  version and lockfile; registry state is deliberately not baked into application
25
25
  artifacts.
26
26
 
@@ -145,6 +145,19 @@ one metadata task; unchanged files reuse their parsed selection. Shared runtime
145
145
  requests do not wait for that refresh. Full offline inventories are enumerated
146
146
  only for worker/offline-manifest requests, with separate state for each selected
147
147
  app snapshot so an older inventory cannot overwrite newer generated metadata.
148
+
149
+ Authored host-root `index.html` presence is refreshed independently of descriptor
150
+ edits. Development serves that host index at `/` instead of redirecting to the
151
+ app entry and suppresses the root PWA alias. An index outside the selected app
152
+ mapping uses an unchanged exact-root response; `/index.html` and other resource
153
+ URLs retain their existing selection rules. A selected index keeps its existing
154
+ document classification. This broadens no app or offline inventory and adds no
155
+ managed maps or bootstrap. With no host index, the app-entry redirect remains.
156
+ A worker serves a directory's cached index body only when that index is already
157
+ in its current offline selection; otherwise the host owns the unchanged request.
158
+ The selected app launch and installation identity remain independent of this
159
+ host-root navigation.
160
+
148
161
  Changed resource requests return the complete current saved source without
149
162
  packaging, copying files into `dist`, or restarting the server. Conditional
150
163
  requests for unchanged resources return `304`. Enabled PWAs check on page load
@@ -253,9 +266,12 @@ separate. See the [complete output contract](reference/protocols.md#app-selected
253
266
 
254
267
  Packaging keeps standalone app files at the release root and places an explicitly
255
268
  nested app beneath `apps/<id>/`. It preserves every shared route destination.
256
- When selected content supplies no root `index.html`, the SDK generates one that
257
- opens the selected app entry. Authored document bases, app URLs, relative module
258
- imports and shared runtime paths therefore resolve through the same layout in
269
+ A selected authored output-root `index.html` is preserved and suppresses the
270
+ deployment-root PWA navigation alias. When selected content supplies no root
271
+ index, the SDK generates a launcher and retains the selected app-entry alias;
272
+ an excluded source-host index does not alter that package result. Authored
273
+ document bases, app URLs, relative module imports and shared runtime paths
274
+ therefore resolve through the same layout in
259
275
  source development and packaged delivery. Packaging consumes the saved source
260
276
  and managed import maps; use `arcane import-map` to refresh those maps, or the
261
277
  ordinary `arcane dev` startup that performs that refresh.
@@ -266,6 +282,13 @@ entry, or `./apps/hello-world/index.html` for a nested app. Its complete `files`
266
282
  inventory follows the selected layout and includes the root `index.html`.
267
283
  Packaged browser serving opens `app.start`; a previously generated flat release without `app.start` retains
268
284
  its `app.entry` launch path.
285
+ Only selected records supplying an authored output-root index add the optional
286
+ generated `app.rootDocument: "./index.html"` release field. Preview serves that
287
+ document at `/` without inferring ownership from HTML content. When the field is
288
+ absent, older releases and generated-launcher packages retain their existing
289
+ `302` root redirect to `app.start` or the older `app.entry` fallback, preserving
290
+ the request query. This release fact adds no app configuration or API option and
291
+ does not change `app.start`.
269
292
 
270
293
  The authored schema-2 descriptor is canonical for new apps and projects an
271
294
  exact schema-1 `arcane-package.json` for current consumers. Existing Arcane
@@ -245,6 +245,12 @@ files remain unchanged. The same layout applies to `arcane dev` and
245
245
  `arcane package`. Generated and offline app files are committed; GitHub Actions
246
246
  consume those committed files. See [root generated output](pwa.md#root-generated-output).
247
247
 
248
+ An existing authored source-host `index.html` suppresses the generated `/`
249
+ navigation alias even when the page is outside the application or offline
250
+ selection. It is not automatically added to either inventory or given managed
251
+ maps or PWA bootstrap. The selected app entry and installation identity remain
252
+ unchanged; see [authored host roots](pwa.md#authored-host-root-and-app-entry).
253
+
248
254
  The supported physical layout preserves its route count and ordered include
249
255
  list. External and modern integrated physical routes require `components`, `css`,
250
256
  `dependencies`, `entities`, `img`, `modules`, and `sdk`; a physical workspace
@@ -373,6 +379,16 @@ not package the app or produce `dist` output. With PWA enabled, the server
373
379
  generates the installation and offline manifests directly; see
374
380
  [PWA development and versioning](pwa.md#development-and-hosting).
375
381
 
382
+ The startup URL remains the selected app entry. At `/`, the server serves an
383
+ existing authored host-root `index.html` and suppresses the app-entry redirect
384
+ and PWA alias. A root outside the selected app mapping is served unchanged only
385
+ through this exact-root route; `/index.html` and other resource URLs retain their
386
+ existing selection rules. A selected root follows its existing document
387
+ classification. Root presence does not expand either inventory or cause
388
+ bootstrap injection. With no host index, the app-entry redirect remains.
389
+ Root-file creation or removal is reflected in subsequent root-navigation and
390
+ generated-PWA requests without a restart or a descriptor edit.
391
+
376
392
  The command reports acceptance before bind/start work, emits the final URL,
377
393
  owns the server until cancellation, and restores failure to the process exit.
378
394
  The default host is `127.0.0.1`. `--public` selects `0.0.0.0` (all IPv4
@@ -550,8 +566,11 @@ defaulting to `dist/<id>/`, and preserves the prior output until the replacement
550
566
  is complete. It consumes saved source and managed
551
567
  import maps, keeps standalone app files at the output root, and retains the
552
568
  configured shared route destinations. Explicit multi-app workspaces retain
553
- their selected app beneath `apps/<id>/`. When selected shared content supplies no root
554
- `index.html`, the SDK generates one that opens the selected app entry.
569
+ their selected app beneath `apps/<id>/`. A selected authored output-root
570
+ `index.html` is preserved and suppresses the deployment-root PWA navigation
571
+ alias. When selected content supplies no root index, the SDK generates a launcher
572
+ and retains the app-entry alias. An excluded source-host root does not change
573
+ that package selection.
555
574
  Source document bases and resource URLs therefore retain their development
556
575
  layout. Packaging does not run tests or checks automatically.
557
576
 
@@ -584,6 +603,10 @@ The result includes the release root, manifest, and complete selected inventory.
584
603
  the package launch URL in `app.start`: `./index.html` for a root app with that
585
604
  entry, or `./apps/hello-world/index.html` for a nested app. The complete file
586
605
  inventory follows the selected layout and includes the root `index.html`.
606
+ Only selected records supplying an authored output-root index add
607
+ `app.rootDocument: "./index.html"`. This optional generated release fact selects
608
+ root-document serving in preview; it is not an app configuration or CLI option
609
+ and does not change `app.start`.
587
610
  `--dry-run` plans the package without refreshing source, running tests, or
588
611
  replacing output.
589
612
 
@@ -749,6 +772,11 @@ For `--target browser`, starts the existing release in the app's configured
749
772
  does not package, rebuild, test, check, or verify that release automatically.
750
773
  It opens the release manifest's `app.start` URL. Older flat releases without
751
774
  that field continue to open their `app.entry` path.
775
+ Requests to `/` serve the explicit `app.rootDocument` when that generated
776
+ release field is present. With no field, including older releases and app-only
777
+ packages with a generated launcher, `/` retains its `302` redirect to
778
+ `app.start` or the older `app.entry` fallback, preserving the request query.
779
+ Preview does not infer root ownership from HTML content.
752
780
  The preview always uses HTTPS with the workspace certificate pair. Supply
753
781
  `--cert <file> --key <file>` together to use another pair; see
754
782
  [development HTTPS setup](#development-https-setup).
@@ -251,15 +251,34 @@ The declared application ID remains unchanged. `appsRoot: "apps"` continues
251
251
  to discover `apps/<id>` and supports the existing integrated and multi-app
252
252
  layouts. Entries and include/exclude paths remain relative to the application.
253
253
 
254
- Root HTML uses `<base href="./">`; nested navigable documents use their actual
255
- depth back to the workspace. Managed bare imports remain the public interface;
256
- their targets follow the selected npm routes. `arcane import-map` generates
257
- enabled static PWA records at the app root, so normal static hosting needs no
254
+ SDK-managed root HTML uses `<base href="./">`; nested navigable documents use
255
+ their actual depth back to the workspace. Managed bare imports remain the
256
+ public interface; their targets follow the selected npm routes. `arcane import-map`
257
+ generates enabled static PWA records at the app root, so normal static hosting needs no
258
258
  SDK request handler or runtime copy. Managed refresh, development serving,
259
259
  inspection, dry run and packaging add no nested `apps/<id>/` navigation pages,
260
260
  redirects, or duplicate PWA worker/inventory files. There is no output-retention
261
261
  switch for those retired records.
262
262
 
263
+ An authored source-host `index.html` suppresses the root app-entry redirect and
264
+ generated PWA navigation alias even when that file is outside the application's
265
+ include or offline selection. Development serves that host index at `/`. An
266
+ index outside the selected app mapping uses an unchanged exact-root response;
267
+ `/index.html` and other resource URLs retain their existing selection rules.
268
+ A selected index follows the existing document classification. Root presence
269
+ adds no page, assets, managed maps, or PWA bootstrap to the application. With no
270
+ host index, development retains the app-entry redirect. In a package, only a
271
+ selected authored output-root index suppresses the alias; an absent or excluded
272
+ root retains the generated launcher and app-entry alias. Selected records that
273
+ supply an authored output-root index add the optional generated release field
274
+ `app.rootDocument: "./index.html"`. Preview serves that document at `/`; absence
275
+ retains the existing `302` to `app.start` or the older `app.entry` fallback with
276
+ the request query preserved. This is a release fact, not an app setting or HTML
277
+ content inference, and `app.start` remains unchanged. The worker can answer a
278
+ directory navigation from an index only when that index already belongs to its
279
+ current offline selection and has a cached response; otherwise the unchanged
280
+ request remains host-owned. See [authored host roots](pwa.md#authored-host-root-and-app-entry).
281
+
263
282
  Application identity, stored data, the existing default installation ID and an
264
283
  authored `pwa.manifest.id` remain unchanged. Authored resources continue to
265
284
  follow the app's include/exclude selection; generation does not delete existing
@@ -102,6 +102,38 @@ committed files rather than generating them. Actual multi-app workspaces keep
102
102
  their explicit `appsRoot: "apps"` layout. This source/package contract does not
103
103
  establish any particular installed application's browser lifecycle.
104
104
 
105
+ ### Authored host root and app entry
106
+
107
+ An authored `index.html` at the source host root keeps `/` separate from the
108
+ selected application entry. Root import-map generation and development PWA
109
+ generation omit the `/`-to-entry navigation alias whenever that file exists,
110
+ including when it is outside `package.include` or the offline selection.
111
+ Development serving answers `/` with that host index. An index outside the app's
112
+ selected source mapping is served unchanged through this exact-root route;
113
+ `/index.html` and other resource URLs retain their existing selection rules.
114
+ A selected index follows its existing managed-document classification. Root
115
+ presence alone adds no page or assets to the application or offline inventory
116
+ and adds no managed maps or PWA bootstrap. The host owns a root response outside
117
+ the worker's selected offline resources. With no host index, development retains
118
+ its app-entry redirect.
119
+
120
+ Packaging considers the selected output instead of unrelated source-host files.
121
+ A selected authored output-root `index.html` is preserved and suppresses the
122
+ deployment-root navigation alias. When selected content supplies no output-root
123
+ index, the package retains its generated launcher and app-entry alias. An
124
+ authored source root excluded from the package does not replace that launcher.
125
+ Only when selected records supply an authored output-root index does packaging
126
+ write `app.rootDocument: "./index.html"` in `ARCANE_APP_RELEASE.json`.
127
+ Packaged preview uses that generated field to serve the authored document at
128
+ `/`; it does not inspect HTML to infer root ownership. Without the field,
129
+ including older releases and generated-launcher packages, preview retains its
130
+ `302` redirect to `app.start` (or the older `app.entry` fallback), preserving the
131
+ request query. The startup URL remains the selected app launch URL.
132
+
133
+ This behavior requires no new setting. The application entry, default install
134
+ start URL, manifest identity and scope, worker registration scope, cache
135
+ identity, and saved application data keep their existing contracts.
136
+
105
137
  ### Offline resource selection
106
138
 
107
139
  `offline.include` and `offline.exclude` select literal paths or directory
@@ -304,8 +336,10 @@ therefore matters even when an offline cache exists. Updating files in place
304
336
  while installation fetches them is not an atomic release snapshot; the host owns
305
337
  consistent deployment of the selected output.
306
338
 
307
- An offline navigation alias redirects to its selected entry page, preserving the
308
- document URL used to resolve relative modules and styles.
339
+ Where an app-entry navigation alias remains, it redirects to the selected entry
340
+ page, preserving the document URL used to resolve relative modules and styles.
341
+ An authored host root suppresses that alias as described above; merely retaining
342
+ an old cached root resource does not add it to the current offline selection.
309
343
 
310
344
  For a same-origin navigation to a selected static `.html` or `.htm` document,
311
345
  the worker can reuse that document's cached body while leaving the complete
@@ -319,6 +353,14 @@ document. This does not change query matching for assets, API calls, ordinary
319
353
  fetch requests, unselected documents or other origins. Cache refresh and the
320
354
  Update prompt keep their existing lifecycle.
321
355
 
356
+ For a same-origin directory navigation without an app-entry alias, the worker
357
+ can likewise reuse that directory's `index.html` body only when the index is
358
+ already in the current offline selection and has a retained cached response.
359
+ The directory URL and its query remain unchanged, including beneath a deployment
360
+ subpath. An exact query-specific selected resource takes precedence. Otherwise,
361
+ the original request passes to the host unchanged. This adds no page or asset to
362
+ the inventory and does not save a host response over the selected index.
363
+
322
364
  Browser storage eviction can remove an offline cache. A missing release cache
323
365
  entry falls back to the network, so offline availability still depends on the
324
366
  browser retaining the selected resources.
@@ -882,7 +882,14 @@ generates one that opens the selected app page. The result
882
882
  contains `outputRoot`, `manifest`, and the complete `files` inventory.
883
883
  `manifest.app.entry` stays app-relative, while `manifest.app.start`
884
884
  is the package launch URL, such as `./index.html` for a root app or
885
- `./apps/hello-world/index.html` for a nested app. Release
885
+ `./apps/hello-world/index.html` for a nested app. Only when selected records
886
+ supply an authored output-root index does the release add
887
+ `manifest.app.rootDocument: "./index.html"`. Preview serves that explicit
888
+ document at `/`; without the field, older releases and generated-launcher
889
+ packages retain the `302` redirect to `app.start` or the older `app.entry`
890
+ fallback with the request query preserved. This generated release fact adds no
891
+ app configuration or API option, changes no `app.start`, and requires no HTML
892
+ content inference. Release
886
893
  schema `1`, kind `arcane-app-release`, and packager identity
887
894
  `arcane-app-packager-v1` remain unchanged. A dry run returns the planned output
888
895
  and file inventory without writing source or output.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "arcane-os",
3
- "version": "0.52.0",
3
+ "version": "0.52.1",
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",
@@ -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)){
@@ -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,