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 +19 -0
- package/README.md +1 -1
- package/docs/architecture.md +26 -3
- package/docs/reference/cli.md +30 -2
- package/docs/reference/protocols.md +23 -4
- package/docs/reference/pwa.md +44 -2
- package/docs/reference/sdk-api.md +8 -1
- package/package.json +1 -1
- package/src/dev-server.mjs +68 -25
- package/src/packager/core.mjs +10 -5
- package/src/pwa-worker.mjs +4 -2
- package/src/toolchain.mjs +23 -6
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.
|
|
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
|
|
package/docs/architecture.md
CHANGED
|
@@ -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
|
-
|
|
257
|
-
|
|
258
|
-
|
|
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
|
package/docs/reference/cli.md
CHANGED
|
@@ -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>/`.
|
|
554
|
-
`index.html
|
|
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
|
-
|
|
255
|
-
depth back to the workspace. Managed bare imports remain the
|
|
256
|
-
their targets follow the selected npm routes. `arcane import-map`
|
|
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
|
package/docs/reference/pwa.md
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
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
package/src/dev-server.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
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=
|
|
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 =
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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/packager/core.mjs
CHANGED
|
@@ -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 (!
|
|
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`,
|
package/src/pwa-worker.mjs
CHANGED
|
@@ -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
|
|
500
|
-
|
|
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
|
|
334
|
-
|
|
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(
|
|
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=
|
|
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=
|
|
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,
|