arcane-os 0.11.3 → 0.13.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.
- package/CHANGELOG.md +32 -0
- package/NOTICE +1 -0
- package/README.md +27 -13
- package/browser-runtime/pwa-install.mjs +242 -0
- package/browser-runtime/pwa.mjs +131 -0
- package/docs/architecture.md +28 -12
- package/docs/reference/cli.md +42 -16
- package/docs/reference/inventory/package-api.json +44 -2
- package/docs/reference/inventory/runtime-components.json +29 -1
- package/docs/reference/pwa.md +252 -24
- package/docs/reference/runtime-components.md +64 -0
- package/docs/reference/sdk-api.md +158 -28
- package/package.json +3 -2
- package/runtime/arcane/components/pwa-install.html +216 -0
- package/src/app-descriptor.mjs +43 -1
- package/src/cli/main.mjs +29 -13
- package/src/dev-server.mjs +387 -136
- package/src/pwa-worker.mjs +361 -137
- package/src/pwa.mjs +11 -2
- package/src/targets/index.mjs +5 -1
- package/src/toolchain.mjs +62 -12
|
@@ -39,7 +39,7 @@ for the installed-inventory-derived physical-runtime contract in SDK `0.5.18`.
|
|
|
39
39
|
| `arcane-os/speech-playback` | Portable speech preparation, playback state, and injected media adapters. |
|
|
40
40
|
| `arcane-os/speech-text` | Speech-input formatting cleanup for complete text and streamed chunks. |
|
|
41
41
|
| `arcane-os/browser-device` | Synchronous mobile or desktop identity hints for application-owned settings. |
|
|
42
|
-
| `arcane-os/pwa` | Nonblocking PWA registration,
|
|
42
|
+
| `arcane-os/pwa` | Nonblocking PWA registration, worker updates, native installation state and a dismissible installation component. |
|
|
43
43
|
| `arcane-os/ai/browser-wasm` | Caller-selected browser-local Wllama inference, complete DBOPFS model storage, streaming, cancellation, and structural tool-call results. |
|
|
44
44
|
| `arcane-os/ai/browser-speech` | Caller-selected browser-local Whisper STT and Kokoro TTS provider mechanisms, ordinary upstream assets, materialized/native routing, Workers, and cancellation. |
|
|
45
45
|
| `arcane-os/mail` | Portable Mail runtime, durable outbox, complete transport responses, and provider-neutral acceptance contracts. |
|
|
@@ -129,6 +129,9 @@ browser map are cataloged separately in [Runtime modules](runtime-modules.md).
|
|
|
129
129
|
| `getBrowserDeviceClass()` | function | `arcane-os/browser-device` | Browser device settings | Node and Browser; synchronous identity hint with no model, storage, or GPU operation |
|
|
130
130
|
| `PWA_STATE_EVENT` | constant | `arcane-os/pwa` | Progressive web applications | Browser lifecycle event name; importable without registration |
|
|
131
131
|
| `registerPwa()` | function | `arcane-os/pwa` | Progressive web applications | Browser service workers; synchronous unsupported state when unavailable |
|
|
132
|
+
| `PWA_INSTALL_STATE_EVENT` | constant | `arcane-os/pwa` | Progressive web applications | Browser installation event name; importable without starting observation |
|
|
133
|
+
| `getPwaInstall()` | function | `arcane-os/pwa` | Progressive web applications | Browser native installation events; waiting state while no prompt is available |
|
|
134
|
+
| `mountPwaInstallPrompt()` | function | `arcane-os/pwa` | Progressive web applications | Browser document and managed HTML import; resolves to null without a document |
|
|
132
135
|
| `createDbopfsSpeechArtifactStore()` | function | `arcane-os/ai/browser-speech` | Browser speech providers | Browser with ready DBOPFS, Web Locks, Fetch, File/Blob, and object URLs |
|
|
133
136
|
| `removeBrowserSpeechModelCache()` | function | `arcane-os/ai/browser-speech` | Browser speech providers | Browser CacheStorage; explicit removal of one selected upstream model |
|
|
134
137
|
| `createNativeBuildPlan()` | function | `arcane-os` | Targets, native plans, and providers | Node; selected browser/native target or provider as documented |
|
|
@@ -3459,6 +3462,15 @@ async function useselectApp(...arguments_) {
|
|
|
3459
3462
|
|
|
3460
3463
|
Starts one owned browser development server with exact runtime/app route mappings and a caller-selected bind address.
|
|
3461
3464
|
|
|
3465
|
+
HTTPS serving uses the published `node-http-server` module. The SDK
|
|
3466
|
+
selects source routes and supplies generated representations; the module owns
|
|
3467
|
+
static-file conditional GET/HEAD handling and response delivery. The SDK
|
|
3468
|
+
retains modification dates for its generated representations. Unchanged
|
|
3469
|
+
resources can return `304` with no body while changed resources return their
|
|
3470
|
+
complete current representation. PEM-path listeners use module deployment;
|
|
3471
|
+
explicit raw `tls` options retain native HTTPS transport with the same module
|
|
3472
|
+
response and static-file operations.
|
|
3473
|
+
|
|
3462
3474
|
### Signature, modes, and result
|
|
3463
3475
|
|
|
3464
3476
|
```text
|
|
@@ -3466,53 +3478,64 @@ async startDevServer(options={})
|
|
|
3466
3478
|
```
|
|
3467
3479
|
|
|
3468
3480
|
Import it from `arcane-os`. Source mode accepts
|
|
3469
|
-
`{workspaceRoot=process.cwd(), appId, mode='source', host='127.0.0.1', port=0,
|
|
3470
|
-
|
|
3481
|
+
`{workspaceRoot=process.cwd(), appId, mode='source', host='127.0.0.1', port=0, httpPort=0,
|
|
3482
|
+
certPath, keyPath, tls, signal, onEvent}` and serves one validated
|
|
3471
3483
|
workspace application plus its complete SDK or integrated runtime. Packaged mode uses
|
|
3472
|
-
`{mode:'packaged', releaseRoot, host, port,
|
|
3473
|
-
complete selected release files.
|
|
3484
|
+
`{mode:'packaged', releaseRoot, workspaceRoot, host, port, httpPort, certPath, keyPath, tls,
|
|
3485
|
+
signal, onEvent}` and serves the complete selected release files.
|
|
3486
|
+
`host` defaults to `127.0.0.1` and accepts an
|
|
3474
3487
|
explicit network address or hostname. Use `0.0.0.0` for all IPv4 interfaces or
|
|
3475
|
-
`::` for the platform's IPv6 wildcard
|
|
3476
|
-
|
|
3477
|
-
|
|
3478
|
-
`
|
|
3488
|
+
`::` for the platform's IPv6 wildcard listeners. `port` selects the HTTPS
|
|
3489
|
+
application port, and `httpPort` selects the paired HTTP redirect port. Each
|
|
3490
|
+
defaults to `0`, which asks the operating system for an available port.
|
|
3491
|
+
The SDK request hook returns `308` for HTTP with a `Location` pointing to the
|
|
3492
|
+
actual HTTPS port while preserving the original request path and query. Both listeners
|
|
3493
|
+
use the selected host.
|
|
3494
|
+
|
|
3495
|
+
Every source server and packaged browser preview enforces HTTPS, including
|
|
3496
|
+
localhost. The legacy `https` option is accepted but cannot disable it.
|
|
3497
|
+
Startup reads `.arcane/dev/server-cert.pem` and
|
|
3479
3498
|
`.arcane/dev/server-key.pem` relative to `workspaceRoot` unless explicit
|
|
3480
|
-
`certPath` and `keyPath` are supplied together
|
|
3481
|
-
|
|
3482
|
-
direct `tls` object instead supplies Node HTTPS server options, including
|
|
3499
|
+
`certPath` and `keyPath` are supplied together; relative paths resolve from the
|
|
3500
|
+
workspace. A direct `tls` object instead supplies Node HTTPS server options, including
|
|
3483
3501
|
`cert` and `key`, without reading certificate files. Keep private material
|
|
3484
3502
|
server-side. Missing PEM files and Node certificate/key parse errors reject
|
|
3485
3503
|
startup without falling back to HTTP. Node owns TLS option handling and the
|
|
3486
3504
|
handshake; browser trust and address matching are evaluated when a client
|
|
3487
|
-
connects. The CLI's `--public` selects
|
|
3505
|
+
connects. The CLI's `--public` selects the wildcard bind;
|
|
3488
3506
|
the API's `host` option alone changes only the bind address.
|
|
3489
3507
|
|
|
3490
|
-
The promise settles after
|
|
3508
|
+
The promise settles after both listeners are ready and resolves to
|
|
3491
3509
|
`{server, protocol, mode, workspaceRoot, appId, host, port, origin, cleanUrl, url,
|
|
3492
|
-
networkUrls, close, closed, lifecycle}`. `server` is the raw Node
|
|
3493
|
-
server; `protocol` is `'
|
|
3510
|
+
networkUrls, httpPort, httpOrigin, httpUrl, close, closed, lifecycle}`. `server` is the raw Node HTTPS
|
|
3511
|
+
server; `protocol` is `'https:'`.
|
|
3494
3512
|
`url` and `cleanUrl` are the same application URL. Wildcard listeners use
|
|
3495
3513
|
`localhost` in that local URL; `host` retains the actual bound address.
|
|
3514
|
+
`httpPort` is the actual HTTP listener port, `httpOrigin` is its HTTP origin,
|
|
3515
|
+
and `httpUrl` combines that origin with the application start path. These
|
|
3516
|
+
fields identify the redirect endpoint; `origin`, `url`, `cleanUrl`, and
|
|
3517
|
+
`networkUrls` identify HTTPS application endpoints.
|
|
3496
3518
|
`networkUrls` lists application URLs for applicable non-loopback interface
|
|
3497
3519
|
addresses discovered once at startup. These URLs are connection candidates,
|
|
3498
3520
|
not evidence of reachability from another device. The server adds no session
|
|
3499
3521
|
capability or authentication. In packaged mode, `workspaceRoot` and `appId`
|
|
3500
3522
|
are `null`.
|
|
3501
3523
|
|
|
3502
|
-
|
|
3503
|
-
|
|
3504
|
-
stores. Each client must trust the issuing CA and open an address covered by the
|
|
3524
|
+
PEM-path startup reads the selected pair. The server does not create
|
|
3525
|
+
certificates or modify trust stores. Each client must trust the issuing CA and open an address covered by the
|
|
3505
3526
|
server certificate. Lifecycle events and CLI summaries exclude TLS options and
|
|
3506
3527
|
private key contents. See [development HTTPS setup](cli.md#development-https-setup).
|
|
3507
3528
|
|
|
3508
|
-
Starting the server opens
|
|
3509
|
-
backpressured `server.starting` and `server.started` events.
|
|
3510
|
-
|
|
3529
|
+
Starting the server opens both selected listeners and emits awaited,
|
|
3530
|
+
backpressured `server.starting` and `server.started` events. `server.started`
|
|
3531
|
+
includes `httpPort`, `httpOrigin`, and `httpUrl` alongside the HTTPS endpoint.
|
|
3532
|
+
Request failures emit `server.request.failed`; shutdown emits `server.stopped` after owned
|
|
3511
3533
|
requests and event delivery drain. Call `await result.close()` in a `finally`
|
|
3512
3534
|
block, or abort `signal`; `close()` is idempotent and returns the
|
|
3513
|
-
same settlement represented by both `closed` and `lifecycle`.
|
|
3514
|
-
|
|
3515
|
-
|
|
3535
|
+
same settlement represented by both `closed` and `lifecycle`. Closing the
|
|
3536
|
+
operation closes both listeners. An error from either listener or an
|
|
3537
|
+
event-callback failure closes both and rejects the lifecycle. Invalid
|
|
3538
|
+
mode/host/port/httpPort, malformed workspace or release content, an occupied port,
|
|
3516
3539
|
or an already-aborted signal rejects startup.
|
|
3517
3540
|
|
|
3518
3541
|
### Availability and normalization
|
|
@@ -3907,9 +3930,16 @@ async function usedescribeTargets(...arguments_) {
|
|
|
3907
3930
|
Starts one owned browser development server for the selected application.
|
|
3908
3931
|
|
|
3909
3932
|
`https`, `certPath`, `keyPath`, and `tls` follow the
|
|
3910
|
-
[`startDevServer()` TLS contract](#startdevserver), alongside `host` and
|
|
3911
|
-
|
|
3912
|
-
|
|
3933
|
+
[`startDevServer()` TLS contract](#startdevserver), alongside `host`, `port`, and
|
|
3934
|
+
`httpPort`. `port` selects HTTPS and `httpPort` selects the HTTP `308` redirect
|
|
3935
|
+
listener; each defaults to an available port.
|
|
3936
|
+
The operation refreshes the selected authored descriptor's `arcane-package.json`
|
|
3937
|
+
projection and managed import maps under one development-refresh lock, then
|
|
3938
|
+
releases that lock before opening the HTTPS source listener and its HTTP
|
|
3939
|
+
redirect listener. It returns both endpoints and their shared shutdown
|
|
3940
|
+
lifecycle. Legacy package-only apps remain unchanged. The operation generates
|
|
3941
|
+
no packaged output; enabled PWA manifests
|
|
3942
|
+
are served directly from the selected source resources.
|
|
3913
3943
|
|
|
3914
3944
|
### Signature and result
|
|
3915
3945
|
|
|
@@ -6075,6 +6105,13 @@ registration and returns a synchronous owner with `ready`, `state`, `subscribe`,
|
|
|
6075
6105
|
`update` and `dispose`. It does not block page rendering or load models.
|
|
6076
6106
|
Unsupported environments receive an explicit unsupported state.
|
|
6077
6107
|
|
|
6108
|
+
The generated page bootstrap also starts the SDK's background resource check.
|
|
6109
|
+
It stores one `lastChecked` timestamp per app/cache in DBOPFS, advancing it only
|
|
6110
|
+
after the full resource check succeeds, and revalidates on page load after
|
|
6111
|
+
120 seconds in development or 15 minutes in packaged delivery. Complete
|
|
6112
|
+
responses remain cached without SDK expiration; `304` retains the response
|
|
6113
|
+
and a successful replacement updates it. See the [PWA guide](pwa.md).
|
|
6114
|
+
|
|
6078
6115
|
### Example
|
|
6079
6116
|
|
|
6080
6117
|
```javascript
|
|
@@ -6090,6 +6127,99 @@ current-state replay, disposal, offline resource ownership and update behavior.
|
|
|
6090
6127
|
The generated bootstrap registers automatically; a manual caller owns a separate
|
|
6091
6128
|
entry point and must observe its `ready` rejection.
|
|
6092
6129
|
|
|
6130
|
+
## PWA_INSTALL_STATE_EVENT
|
|
6131
|
+
|
|
6132
|
+
### Overview
|
|
6133
|
+
|
|
6134
|
+
The exact event name `arcane.pwa.install.state` identifies native installation
|
|
6135
|
+
availability and choice state through the existing Arcane event owner. It is
|
|
6136
|
+
separate from service-worker registration state. Importing it starts no
|
|
6137
|
+
observation or installation.
|
|
6138
|
+
|
|
6139
|
+
### Example
|
|
6140
|
+
|
|
6141
|
+
```javascript
|
|
6142
|
+
import {PWA_INSTALL_STATE_EVENT} from 'arcane-os/pwa';
|
|
6143
|
+
console.log(PWA_INSTALL_STATE_EVENT);
|
|
6144
|
+
```
|
|
6145
|
+
|
|
6146
|
+
See the [installation state contract](pwa.md#getpwainstall) for the complete
|
|
6147
|
+
payload and current-state subscription behavior.
|
|
6148
|
+
|
|
6149
|
+
## getPwaInstall()
|
|
6150
|
+
|
|
6151
|
+
### Overview
|
|
6152
|
+
|
|
6153
|
+
`getPwaInstall()` returns one synchronous page owner exposing `state`,
|
|
6154
|
+
`subscribe`, `prompt`, `dismiss` and `dispose`. It captures the browser's
|
|
6155
|
+
`beforeinstallprompt` event and observes `appinstalled` and app display-mode
|
|
6156
|
+
changes. A missing event leaves installation availability unknown and the owner
|
|
6157
|
+
waiting; it does not prove browser incompatibility.
|
|
6158
|
+
|
|
6159
|
+
State contains `status`, `available`, `dismissed`, `outcome` and `error`.
|
|
6160
|
+
Subscriptions replay current state by default. `prompt()` must be called
|
|
6161
|
+
directly within the install click to preserve native user activation. It
|
|
6162
|
+
consumes the event once and returns the browser choice, resolves to `null` when
|
|
6163
|
+
unavailable, or rejects with the actual error. `dismiss()` remembers the
|
|
6164
|
+
session's floating-suggestion dismissal without consuming a retained event.
|
|
6165
|
+
|
|
6166
|
+
### Example
|
|
6167
|
+
|
|
6168
|
+
```javascript
|
|
6169
|
+
import {getPwaInstall} from 'arcane-os/pwa';
|
|
6170
|
+
|
|
6171
|
+
const install = getPwaInstall();
|
|
6172
|
+
const button = document.querySelector('#install');
|
|
6173
|
+
install.subscribe(function updateInstallButton(state) {
|
|
6174
|
+
button.hidden = !state.available;
|
|
6175
|
+
});
|
|
6176
|
+
button.addEventListener('click', function requestInstallation() {
|
|
6177
|
+
install.prompt().catch(function reportInstallFailure(error) {
|
|
6178
|
+
console.error(error);
|
|
6179
|
+
});
|
|
6180
|
+
});
|
|
6181
|
+
```
|
|
6182
|
+
|
|
6183
|
+
The [PWA guide](pwa.md#getpwainstall) defines every status, event lifetime,
|
|
6184
|
+
session dismissal, errors and disposal. Disposing this shared owner removes
|
|
6185
|
+
its page-level observation; components normally unsubscribe only their own
|
|
6186
|
+
listener. Installation state does not establish offline readiness or completed
|
|
6187
|
+
Android WebAPK creation.
|
|
6188
|
+
|
|
6189
|
+
## mountPwaInstallPrompt()
|
|
6190
|
+
|
|
6191
|
+
### Overview
|
|
6192
|
+
|
|
6193
|
+
`mountPwaInstallPrompt({appName = ''} = {})` starts the shared install observer
|
|
6194
|
+
before asynchronously mounting one initially hidden `pwa-install.html`
|
|
6195
|
+
component. Repeated calls return the same mounting promise. It resolves to the
|
|
6196
|
+
ready `html-import` host, or `null` without a document or when the owner is
|
|
6197
|
+
disposed before mounting. Component-load failure rejects; removal or disposal
|
|
6198
|
+
during loading rejects with `AbortError`. A rejected mount permits another
|
|
6199
|
+
explicit attempt. The first call
|
|
6200
|
+
supplies the initial app name.
|
|
6201
|
+
|
|
6202
|
+
The generated PWA bootstrap calls this automatically with the manifest name.
|
|
6203
|
+
Component mounting and worker registration run independently, and application
|
|
6204
|
+
rendering must not await them. The floating component offers Install and a
|
|
6205
|
+
manual close control; an application can also place the same component inline.
|
|
6206
|
+
|
|
6207
|
+
### Example
|
|
6208
|
+
|
|
6209
|
+
```javascript
|
|
6210
|
+
import {mountPwaInstallPrompt} from 'arcane-os/pwa';
|
|
6211
|
+
|
|
6212
|
+
mountPwaInstallPrompt({appName: 'Example Library'}).catch(
|
|
6213
|
+
function reportInstallComponentFailure(error) {
|
|
6214
|
+
console.error(error);
|
|
6215
|
+
}
|
|
6216
|
+
);
|
|
6217
|
+
```
|
|
6218
|
+
|
|
6219
|
+
See the [mounting contract](pwa.md#mountpwainstallprompt) and
|
|
6220
|
+
[component reference](runtime-components.md#pwa-installhtml). The browser owns
|
|
6221
|
+
native installation eligibility and URL-bar promotion.
|
|
6222
|
+
|
|
6093
6223
|
## createBrowserWasmLlmProvider()
|
|
6094
6224
|
|
|
6095
6225
|
### Overview
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "arcane-os",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.0",
|
|
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",
|
|
@@ -67,7 +67,7 @@
|
|
|
67
67
|
"test": "npm run test:unit && npm run test:functional && npm run test:integration && npm run test:regression",
|
|
68
68
|
"test:release": "node ./bin/arcane-test.mjs test/npm-release.test.mjs",
|
|
69
69
|
"test:unit": "node ./bin/arcane-test.mjs test/app-descriptor.test.mjs test/app-schema.test.mjs test/contracts.test.mjs test/doctor.test.mjs test/mail-credentials.test.mjs test/mail-outbox.test.mjs test/mail-public-api.test.mjs test/mail-send.test.mjs test/mail-transport.test.mjs test/targets.test.mjs test/workspace-operation-lock.test.mjs",
|
|
70
|
-
"test:functional": "node ./bin/arcane-test.mjs test/browser-speech-providers.test.mjs test/browser-wasm-gpu-notice.test.mjs test/browser-wasm-download-resume.test.mjs test/cli.test.mjs test/dbopfs-document-library.test.mjs test/dev-server.test.mjs test/dev-pwa.test.mjs test/dom-event-instrumentation.test.mjs test/event-manager.test.mjs test/events.test.mjs test/import-map.test.mjs test/mail-cli.test.mjs test/mail-runtime.test.mjs test/mail-server.test.mjs test/packaging.test.mjs test/pwa-packaging.test.mjs test/pwa-client.test.mjs test/pwa-worker.test.mjs test/persistent-ai-chat-session.test.mjs test/reference-completeness.test.mjs test/runtime-api-behavior.test.mjs test/runtime.test.mjs test/scaffold.test.mjs test/speech-playback.test.mjs test/site.test.mjs test/update-check.test.mjs",
|
|
70
|
+
"test:functional": "node ./bin/arcane-test.mjs test/browser-speech-providers.test.mjs test/browser-wasm-gpu-notice.test.mjs test/browser-wasm-download-resume.test.mjs test/cli.test.mjs test/dbopfs-document-library.test.mjs test/dev-server.test.mjs test/dev-pwa.test.mjs test/dom-event-instrumentation.test.mjs test/event-manager.test.mjs test/events.test.mjs test/import-map.test.mjs test/mail-cli.test.mjs test/mail-runtime.test.mjs test/mail-server.test.mjs test/packaging.test.mjs test/pwa-packaging.test.mjs test/pwa-client.test.mjs test/pwa-install.test.mjs test/pwa-worker.test.mjs test/persistent-ai-chat-session.test.mjs test/reference-completeness.test.mjs test/runtime-api-behavior.test.mjs test/runtime.test.mjs test/scaffold.test.mjs test/speech-playback.test.mjs test/site.test.mjs test/update-check.test.mjs",
|
|
71
71
|
"test:integration": "node ./bin/arcane-test.mjs test/integrated-shared.test.mjs test/integrated-workspace.test.mjs test/mail-browser.test.mjs test/native-plan.test.mjs test/native-provider-loader.test.mjs test/npm-release.test.mjs test/release-bundle.test.mjs test/release-capability-smoke.test.mjs test/shared-payload-batch.test.mjs test/tarball.test.mjs test/browser-wasm-cpu.test.mjs test/wllama-webgpu-runtime.test.mjs",
|
|
72
72
|
"test:regression": "node ./bin/arcane-test.mjs test/channel-workflows.test.mjs test/html-import-registration.test.mjs test/logging-regression.test.mjs test/markdown-speech.test.mjs test/prepared-speech.test.mjs test/native-provider-generation.test.mjs test/speech-queue-regression.test.mjs test/testing.test.mjs test/test-sets.test.mjs",
|
|
73
73
|
"check": "node tools/check-source.mjs && npm test",
|
|
@@ -105,6 +105,7 @@
|
|
|
105
105
|
},
|
|
106
106
|
"dependencies": {
|
|
107
107
|
"event-pubsub": "6.1.0",
|
|
108
|
+
"node-http-server": "9.1.1",
|
|
108
109
|
"strong-type": "2.0.1",
|
|
109
110
|
"vanilla-test": "2.1.3"
|
|
110
111
|
},
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
<link rel="stylesheet" href="./arcane/css/theme.css?v=1">
|
|
2
|
+
<link rel="stylesheet" href="./arcane/css/primitives.css?v=1">
|
|
3
|
+
<style>
|
|
4
|
+
:host{
|
|
5
|
+
display:block;
|
|
6
|
+
inset-block-start:4.5rem;
|
|
7
|
+
inset-inline-end:1rem;
|
|
8
|
+
max-width:calc(100vw - 2rem);
|
|
9
|
+
min-width:0;
|
|
10
|
+
position:fixed;
|
|
11
|
+
width:22rem;
|
|
12
|
+
z-index:110;
|
|
13
|
+
}
|
|
14
|
+
:host([hidden]){display:none}
|
|
15
|
+
:host([data-presentation="inline"]){
|
|
16
|
+
max-width:100%;
|
|
17
|
+
position:static;
|
|
18
|
+
width:fit-content;
|
|
19
|
+
}
|
|
20
|
+
.panel{
|
|
21
|
+
background:var(--arcane-surface-raised);
|
|
22
|
+
border:1px solid var(--arcane-border);
|
|
23
|
+
border-radius:var(--arcane-radius-medium);
|
|
24
|
+
box-shadow:var(--arcane-shadow-medium);
|
|
25
|
+
box-sizing:border-box;
|
|
26
|
+
color:var(--text-color);
|
|
27
|
+
display:grid;
|
|
28
|
+
gap:var(--arcane-space-2);
|
|
29
|
+
max-height:calc(100vh - 5.5rem);
|
|
30
|
+
max-height:calc(100dvh - 5.5rem);
|
|
31
|
+
overflow:auto;
|
|
32
|
+
padding:var(--arcane-space-3);
|
|
33
|
+
}
|
|
34
|
+
.heading{align-items:start;display:flex;gap:var(--arcane-space-2)}
|
|
35
|
+
.copy{flex:1;min-width:0}
|
|
36
|
+
h2,p{margin:0;overflow-wrap:anywhere;white-space:pre-wrap}
|
|
37
|
+
h2{font-size:1rem;line-height:1.4}
|
|
38
|
+
.description{color:var(--arcane-muted);font-size:.9rem;line-height:1.45}
|
|
39
|
+
.close{flex:none}
|
|
40
|
+
.install{justify-self:start;max-width:100%;overflow-wrap:anywhere;white-space:normal}
|
|
41
|
+
.status{font-size:.9rem;line-height:1.45}
|
|
42
|
+
.status[data-error="true"]{color:var(--arcane-danger)}
|
|
43
|
+
:host([data-presentation="inline"]) .panel{box-shadow:none}
|
|
44
|
+
@media(prefers-reduced-motion:reduce){.install{transition:none}}
|
|
45
|
+
@media(forced-colors:active){.panel,.install,.close{border:1px solid CanvasText}}
|
|
46
|
+
</style>
|
|
47
|
+
|
|
48
|
+
<section class="panel" aria-labelledby="installTitle">
|
|
49
|
+
<div class="heading">
|
|
50
|
+
<div class="copy">
|
|
51
|
+
<h2 id="installTitle">Install this app</h2>
|
|
52
|
+
<p id="description" class="description">Open this app directly from your device.</p>
|
|
53
|
+
</div>
|
|
54
|
+
<button id="close" class="close arcane-close-button" type="button" aria-label="Close installation suggestion">✕</button>
|
|
55
|
+
</div>
|
|
56
|
+
<button id="install" class="install arcane-button" type="button">Install</button>
|
|
57
|
+
<p id="status" class="status" role="status" aria-live="polite" hidden></p>
|
|
58
|
+
</section>
|
|
59
|
+
|
|
60
|
+
<script type="module">
|
|
61
|
+
const host=this;
|
|
62
|
+
host.hidden=true;
|
|
63
|
+
const [
|
|
64
|
+
{default:Is},
|
|
65
|
+
{getPwaInstall},
|
|
66
|
+
{createArcaneEventSource,projectArcaneDOMEvent},
|
|
67
|
+
{arcaneLogging}
|
|
68
|
+
]=await Promise.all([
|
|
69
|
+
import('strong-type'),
|
|
70
|
+
import('arcane-os/pwa'),
|
|
71
|
+
import('arcane-os/event-manager'),
|
|
72
|
+
import('arcane-os/logging')
|
|
73
|
+
]);
|
|
74
|
+
const is=new Is(false);
|
|
75
|
+
const root=host.shadowRoot;
|
|
76
|
+
const title=root.querySelector('#installTitle');
|
|
77
|
+
const description=root.querySelector('#description');
|
|
78
|
+
const installButton=root.querySelector('#install');
|
|
79
|
+
const closeButton=root.querySelector('#close');
|
|
80
|
+
const status=root.querySelector('#status');
|
|
81
|
+
const lifecycleController=new AbortController();
|
|
82
|
+
const events=createArcaneEventSource(host,{
|
|
83
|
+
source:'arcane.component.pwa-install',
|
|
84
|
+
eventTypes:['pwa-install-ready','pwa-install-change','pwa-install-dismissed']
|
|
85
|
+
});
|
|
86
|
+
const owner=getPwaInstall();
|
|
87
|
+
let options={
|
|
88
|
+
appName:host.dataset.appName||'this app',
|
|
89
|
+
installLabel:'Install',
|
|
90
|
+
closeLabel:'Close installation suggestion',
|
|
91
|
+
promptingLabel:'Choose an option in the browser installation prompt.',
|
|
92
|
+
description:'Open this app directly from your device.',
|
|
93
|
+
presentation:host.dataset.presentation||'floating'
|
|
94
|
+
};
|
|
95
|
+
let currentState=owner.state;
|
|
96
|
+
let locallyDismissed=false;
|
|
97
|
+
let pending=false;
|
|
98
|
+
let requestError=null;
|
|
99
|
+
let destroyed=false;
|
|
100
|
+
|
|
101
|
+
host.configure=configure;
|
|
102
|
+
host.install=install;
|
|
103
|
+
host.dismiss=dismiss;
|
|
104
|
+
host.destroy=destroy;
|
|
105
|
+
Object.defineProperty(host,'state',{configurable:true,get:getState});
|
|
106
|
+
|
|
107
|
+
function emit(type,detail){
|
|
108
|
+
if(destroyed)return false;
|
|
109
|
+
const publication=events.dispatch(type,detail);
|
|
110
|
+
return projectArcaneDOMEvent(host,publication.occurrence,{bubbles:true,composed:true});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function getState(){
|
|
114
|
+
return {...currentState};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function configure(input={}){
|
|
118
|
+
if(destroyed)return false;
|
|
119
|
+
if(input===null||!is.object(input)||is.array(input)){
|
|
120
|
+
throw new TypeError('PWA install configuration must be an object.');
|
|
121
|
+
}
|
|
122
|
+
const next={...options,...input};
|
|
123
|
+
for(const key of ['appName','installLabel','closeLabel','promptingLabel','description']){
|
|
124
|
+
if(!is.string(next[key]))throw new TypeError(`PWA install ${key} must be a string.`);
|
|
125
|
+
}
|
|
126
|
+
if(next.presentation!=='floating'&&next.presentation!=='inline'){
|
|
127
|
+
throw new TypeError('PWA install presentation must be floating or inline.');
|
|
128
|
+
}
|
|
129
|
+
options=next;
|
|
130
|
+
host.dataset.presentation=options.presentation;
|
|
131
|
+
title.textContent=`Install ${options.appName}`;
|
|
132
|
+
description.textContent=options.description;
|
|
133
|
+
installButton.textContent=options.installLabel;
|
|
134
|
+
closeButton.setAttribute('aria-label',options.closeLabel);
|
|
135
|
+
render();
|
|
136
|
+
return {...options};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function render(){
|
|
140
|
+
if(destroyed)return;
|
|
141
|
+
const terminal=['accepted','installed','running','disposed'].includes(currentState.status);
|
|
142
|
+
if(terminal)requestError=null;
|
|
143
|
+
const dismissed=locallyDismissed
|
|
144
|
+
||(options.presentation==='floating'&¤tState.dismissed);
|
|
145
|
+
host.hidden=terminal||dismissed||(!currentState.available&&!pending&&!requestError);
|
|
146
|
+
installButton.disabled=pending||!currentState.available;
|
|
147
|
+
const message=requestError
|
|
148
|
+
?is.string(requestError.message)?requestError.message:String(requestError)
|
|
149
|
+
:pending?options.promptingLabel:'';
|
|
150
|
+
status.textContent=message;
|
|
151
|
+
status.hidden=!message;
|
|
152
|
+
status.dataset.error=String(Boolean(requestError));
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function observeInstallState(state){
|
|
156
|
+
if(destroyed)return;
|
|
157
|
+
currentState=state;
|
|
158
|
+
render();
|
|
159
|
+
emit('pwa-install-change',{state:getState(),visible:!host.hidden});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function install(){
|
|
163
|
+
if(destroyed||pending||!currentState.available)return Promise.resolve(null);
|
|
164
|
+
locallyDismissed=false;
|
|
165
|
+
requestError=null;
|
|
166
|
+
pending=true;
|
|
167
|
+
render();
|
|
168
|
+
let operation;
|
|
169
|
+
try{
|
|
170
|
+
// Keep the browser prompt inside the initiating click's user activation.
|
|
171
|
+
operation=owner.prompt();
|
|
172
|
+
}catch(error){
|
|
173
|
+
operation=Promise.reject(error);
|
|
174
|
+
}
|
|
175
|
+
return Promise.resolve(operation).catch(function reportPromptError(error){
|
|
176
|
+
if(!destroyed)requestError=error;
|
|
177
|
+
throw error;
|
|
178
|
+
}).finally(function settlePrompt(){
|
|
179
|
+
if(destroyed)return;
|
|
180
|
+
pending=false;
|
|
181
|
+
render();
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function dismiss(){
|
|
186
|
+
if(destroyed)return false;
|
|
187
|
+
locallyDismissed=true;
|
|
188
|
+
requestError=null;
|
|
189
|
+
if(options.presentation==='floating')owner.dismiss();
|
|
190
|
+
render();
|
|
191
|
+
emit('pwa-install-dismissed',{presentation:options.presentation,state:getState()});
|
|
192
|
+
return true;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function destroy(){
|
|
196
|
+
if(destroyed)return false;
|
|
197
|
+
destroyed=true;
|
|
198
|
+
lifecycleController.abort('pwa-install-destroyed');
|
|
199
|
+
events.dispose();
|
|
200
|
+
host.hidden=true;
|
|
201
|
+
host.ready=false;
|
|
202
|
+
return true;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
installButton.addEventListener('click',function requestInstallation(){
|
|
206
|
+
install().catch(function reportInstallationFailure(error){
|
|
207
|
+
arcaneLogging.error('Unable to open the browser installation prompt:',error);
|
|
208
|
+
});
|
|
209
|
+
},{signal:lifecycleController.signal});
|
|
210
|
+
closeButton.addEventListener('click',dismiss,{signal:lifecycleController.signal});
|
|
211
|
+
|
|
212
|
+
configure();
|
|
213
|
+
owner.subscribe(observeInstallState,{signal:lifecycleController.signal});
|
|
214
|
+
host.ready=true;
|
|
215
|
+
emit('pwa-install-ready',{ready:true,state:getState()});
|
|
216
|
+
</script>
|
package/src/app-descriptor.mjs
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import Is from 'strong-type';
|
|
2
2
|
import {isDeepStrictEqual} from 'node:util';
|
|
3
|
-
import {readFile} from 'node:fs/promises';
|
|
3
|
+
import {readFile, writeFile} from 'node:fs/promises';
|
|
4
4
|
import path from 'node:path';
|
|
5
|
+
import {throwIfAborted} from './errors.mjs';
|
|
5
6
|
import {normalizePwaConfig} from './pwa.mjs';
|
|
6
7
|
import {
|
|
7
8
|
ARCANE_MACHINE_BUNDLE_VERSION,
|
|
@@ -424,6 +425,47 @@ async function readJsonFile(filePath,label,{optional=false}={}){
|
|
|
424
425
|
}
|
|
425
426
|
}
|
|
426
427
|
|
|
428
|
+
export async function refreshAppPackageProjection({workspaceRoot, appId, signal, onEvent}) {
|
|
429
|
+
throwIfAborted(signal);
|
|
430
|
+
const appRoot = path.join(workspaceRoot, 'apps', appId);
|
|
431
|
+
const descriptorPath = path.join(appRoot, APP_DESCRIPTOR_NAME);
|
|
432
|
+
const authored = await readJsonFile(
|
|
433
|
+
descriptorPath,
|
|
434
|
+
`apps/${appId}/${APP_DESCRIPTOR_NAME}`,
|
|
435
|
+
{optional: true}
|
|
436
|
+
);
|
|
437
|
+
if (authored === null) return {updated: false};
|
|
438
|
+
|
|
439
|
+
validateAppDescriptor(
|
|
440
|
+
authored,
|
|
441
|
+
{appId}
|
|
442
|
+
);
|
|
443
|
+
const projection = projectPackageManifest(authored);
|
|
444
|
+
const packagePath = path.join(appRoot, 'arcane-package.json');
|
|
445
|
+
const packageManifest = await readJsonFile(
|
|
446
|
+
packagePath,
|
|
447
|
+
`apps/${appId}/arcane-package.json`
|
|
448
|
+
);
|
|
449
|
+
const packageProjection = packageManifest.pwa === undefined ? packageManifest : {
|
|
450
|
+
...packageManifest,
|
|
451
|
+
pwa: normalizePwaConfig(packageManifest.pwa)
|
|
452
|
+
};
|
|
453
|
+
if (isDeepStrictEqual(projection, packageProjection)) return {updated: false};
|
|
454
|
+
|
|
455
|
+
// Development updates the generated projection before strict workspace readers run.
|
|
456
|
+
throwIfAborted(signal);
|
|
457
|
+
await writeFile(packagePath, `${JSON.stringify(projection, null, 2)}\n`, 'utf8');
|
|
458
|
+
await onEvent?.(
|
|
459
|
+
{
|
|
460
|
+
type: 'workspace.application.projection.updated',
|
|
461
|
+
workspaceRoot,
|
|
462
|
+
appId,
|
|
463
|
+
path: packagePath
|
|
464
|
+
}
|
|
465
|
+
);
|
|
466
|
+
return {updated: true};
|
|
467
|
+
}
|
|
468
|
+
|
|
427
469
|
export async function loadAppDescriptor({workspaceRoot,appRoot,appId,packageManifest}){
|
|
428
470
|
const descriptorPath=path.join(appRoot,APP_DESCRIPTOR_NAME);
|
|
429
471
|
const authored=await readJsonFile(descriptorPath,`apps/${appId}/${APP_DESCRIPTOR_NAME}`,{optional:true});
|