arcane-os 0.39.1 → 0.40.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 +33 -0
- package/README.md +1 -1
- package/browser-runtime/ai/digitalocean-speech.mjs +59 -19
- package/browser-runtime/ai/speech-worker-runtime.mjs +2 -3
- package/docs/architecture.md +14 -2
- package/docs/reference/ai/browser-speech.md +15 -2
- package/docs/reference/cli.md +19 -4
- package/docs/reference/inventory/package-api.json +4 -4
- package/docs/reference/protocols.md +40 -1
- package/docs/reference/pwa.md +28 -1
- package/docs/reference/runtime-components.md +14 -4
- package/docs/reference/sdk-api.md +39 -0
- package/package.json +1 -1
- package/runtime/arcane/components/speech.html +17 -18
- package/schemas/arcane-package.schema.json +4 -0
- package/src/app-descriptor.mjs +8 -2
- package/src/app-layout.mjs +4 -0
- package/src/dev-server.mjs +19 -11
- package/src/import-map.mjs +20 -1
- package/src/packager/core.mjs +98 -20
- package/src/pwa-worker.mjs +28 -2
- package/src/targets/index.mjs +1 -2
- package/src/toolchain.mjs +29 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,38 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.40.1
|
|
4
|
+
|
|
5
|
+
- Include complete selected local resource URL variants in generated static
|
|
6
|
+
PWA inventories. Reuse the source resource graph, resolve shared component
|
|
7
|
+
styles and modules against their runtime root, and retain speech worker mode
|
|
8
|
+
queries. Application selection, source content and output locations remain
|
|
9
|
+
unchanged; regenerate through the public import-map workflow to receive the
|
|
10
|
+
corrected offline inventory.
|
|
11
|
+
- Space DigitalOcean FAL inference request starts by one second while retaining
|
|
12
|
+
four concurrent jobs and ordered playback. Retry a readable rate-limit
|
|
13
|
+
rejection once, honoring longer readable Retry-After values, and retry a
|
|
14
|
+
same-job status network failure once. An ambiguous submission network failure
|
|
15
|
+
is surfaced without submitting another potentially paid job.
|
|
16
|
+
- Preserve the user's enabled speech intent after a synthesis, playback or
|
|
17
|
+
provider lifecycle failure. A failed job leaves unrelated queued jobs intact;
|
|
18
|
+
explicit Retry, Load, Mute and Cancel remain user-controlled.
|
|
19
|
+
|
|
20
|
+
## 0.40.0
|
|
21
|
+
|
|
22
|
+
- Add optional `arcane-package.json` `outputDirectory` to select the final
|
|
23
|
+
workspace-relative package directory without an appended application ID.
|
|
24
|
+
For example, `"ai"` selects `<workspace>/ai`; omission preserves `dist/<id>`.
|
|
25
|
+
Descriptor refresh retains this package-local setting, and preview,
|
|
26
|
+
verification, bundles and native browser-content inputs use the selected
|
|
27
|
+
root. Source layout, app identity and internal release URLs are unchanged.
|
|
28
|
+
Packaging replaces only the selected destination through its existing staged
|
|
29
|
+
swap, preserving old destinations and siblings when that selection changes.
|
|
30
|
+
- Serve selected cached static HTML documents for navigation query variants
|
|
31
|
+
without redirecting or losing the requested URL. Exact selected query
|
|
32
|
+
variants retain precedence; a missing cached document fetches the original
|
|
33
|
+
request without replacing the canonical cached body. Assets, API requests,
|
|
34
|
+
non-navigation requests and update/refresh lifecycle remain unchanged.
|
|
35
|
+
|
|
3
36
|
## 0.39.1
|
|
4
37
|
|
|
5
38
|
- Preserve the Markdown editor's current source viewport and selection during
|
package/README.md
CHANGED
|
@@ -19,7 +19,7 @@ version-locked SDK runtime, while an integrated Arcane checkout uses its live
|
|
|
19
19
|
`arcane/` runtime. Both profiles share the theme, packaging, event, cancellation,
|
|
20
20
|
and browser run contracts while retaining their selected application layout.
|
|
21
21
|
|
|
22
|
-
This checkout defines the `0.
|
|
22
|
+
This checkout defines the `0.40.1` SDK contract. Applications pin one exact npm
|
|
23
23
|
version and lockfile; registry state is deliberately not baked into application
|
|
24
24
|
artifacts.
|
|
25
25
|
|
|
@@ -113,6 +113,7 @@ export function createDigitalOceanFalTTSProvider({
|
|
|
113
113
|
let generation = 0;
|
|
114
114
|
let unloadOperation = null;
|
|
115
115
|
let disposeOperation = null;
|
|
116
|
+
let nextInferenceStart = 0;
|
|
116
117
|
|
|
117
118
|
function authority() {
|
|
118
119
|
return {
|
|
@@ -214,12 +215,62 @@ export function createDigitalOceanFalTTSProvider({
|
|
|
214
215
|
};
|
|
215
216
|
}
|
|
216
217
|
|
|
218
|
+
async function fetchInference(url, options, operation) {
|
|
219
|
+
const signal = options.signal;
|
|
220
|
+
let retryError = null;
|
|
221
|
+
while (true) {
|
|
222
|
+
// Space request starts across jobs, not their completion. Remote jobs
|
|
223
|
+
// still run concurrently and the runtime retains ordered playback.
|
|
224
|
+
while (true) {
|
|
225
|
+
assertNotAborted(signal);
|
|
226
|
+
const delay = nextInferenceStart - Date.now();
|
|
227
|
+
if (delay <= 0) break;
|
|
228
|
+
await waitForStatus(delay, signal);
|
|
229
|
+
}
|
|
230
|
+
nextInferenceStart = Date.now() + POLL_INTERVAL_MS;
|
|
231
|
+
let response;
|
|
232
|
+
try {
|
|
233
|
+
response = await fetchImpl(url, options);
|
|
234
|
+
} catch (error) {
|
|
235
|
+
assertNotAborted(signal);
|
|
236
|
+
// A lost POST response may represent an accepted paid job. Only
|
|
237
|
+
// an identified job's status read can retry a network failure.
|
|
238
|
+
if (!retryError && options.method === 'GET' && error instanceof TypeError) {
|
|
239
|
+
nextInferenceStart = Math.max(nextInferenceStart, Date.now() + POLL_INTERVAL_MS);
|
|
240
|
+
retryError = error;
|
|
241
|
+
console.warn('Retrying the cloud speech status read once.', error);
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
throw error;
|
|
245
|
+
}
|
|
246
|
+
assertNotAborted(signal);
|
|
247
|
+
if (response.status === 429) {
|
|
248
|
+
// A readable rejection is safe to retry. Its cooldown also
|
|
249
|
+
// applies to waiting siblings, including when this retry ends.
|
|
250
|
+
const delay = Math.max(POLL_INTERVAL_MS, retryAfterDelay(response) ?? POLL_INTERVAL_MS);
|
|
251
|
+
nextInferenceStart = Math.max(nextInferenceStart, Date.now() + delay);
|
|
252
|
+
}
|
|
253
|
+
try {
|
|
254
|
+
await requireSuccessfulResponse(response, signal, operation);
|
|
255
|
+
} catch (error) {
|
|
256
|
+
assertNotAborted(signal);
|
|
257
|
+
if (!retryError && response.status === 429) {
|
|
258
|
+
retryError = error;
|
|
259
|
+
console.warn(`Retrying the cloud speech ${operation} once.`, error);
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
throw error;
|
|
263
|
+
}
|
|
264
|
+
return response;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
217
268
|
async function synthesize(payload, controller, requestGeneration) {
|
|
218
269
|
const signal = controller.signal;
|
|
219
270
|
const apiKey = await readApiKey(signal);
|
|
220
271
|
assertNotAborted(signal);
|
|
221
272
|
const headers = {Authorization: `Bearer ${apiKey}`};
|
|
222
|
-
const response = await
|
|
273
|
+
const response = await fetchInference(
|
|
223
274
|
INVOKE_URL,
|
|
224
275
|
{
|
|
225
276
|
method: 'POST',
|
|
@@ -235,9 +286,9 @@ export function createDigitalOceanFalTTSProvider({
|
|
|
235
286
|
}
|
|
236
287
|
),
|
|
237
288
|
signal
|
|
238
|
-
}
|
|
289
|
+
},
|
|
290
|
+
'submission'
|
|
239
291
|
);
|
|
240
|
-
await requireSuccessfulResponse(response, signal, 'submission');
|
|
241
292
|
let result = await response.json();
|
|
242
293
|
assertNotAborted(signal);
|
|
243
294
|
const requestId = result?.request_id;
|
|
@@ -249,22 +300,11 @@ export function createDigitalOceanFalTTSProvider({
|
|
|
249
300
|
);
|
|
250
301
|
}
|
|
251
302
|
await waitForStatus(POLL_INTERVAL_MS, signal);
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
);
|
|
258
|
-
assertNotAborted(signal);
|
|
259
|
-
const retryDelay = statusResponse.status === 429
|
|
260
|
-
? retryAfterDelay(statusResponse)
|
|
261
|
-
: null;
|
|
262
|
-
if (retryDelay === null) break;
|
|
263
|
-
// Only status reads may be retried; submitting twice can create two paid jobs.
|
|
264
|
-
await statusResponse.text();
|
|
265
|
-
await waitForStatus(retryDelay, signal);
|
|
266
|
-
} while (true);
|
|
267
|
-
await requireSuccessfulResponse(statusResponse, signal, 'status read');
|
|
303
|
+
const statusResponse = await fetchInference(
|
|
304
|
+
`${INVOKE_URL}/${encodeURIComponent(requestId)}`,
|
|
305
|
+
{method: 'GET', headers, signal},
|
|
306
|
+
'status read'
|
|
307
|
+
);
|
|
268
308
|
result = await statusResponse.json();
|
|
269
309
|
assertNotAborted(signal);
|
|
270
310
|
}
|
|
@@ -844,11 +844,10 @@ function validateConfiguration(configuration, role) {
|
|
|
844
844
|
function nestedWorkerUrl(role) {
|
|
845
845
|
const url = new URL(
|
|
846
846
|
role === "stt"
|
|
847
|
-
? "./browser-whisper-worker.mjs"
|
|
848
|
-
: "./browser-kokoro-worker.mjs",
|
|
847
|
+
? "./browser-whisper-worker.mjs?arcaneSpeechWorkerMode=artifact-module-worker"
|
|
848
|
+
: "./browser-kokoro-worker.mjs?arcaneSpeechWorkerMode=artifact-module-worker",
|
|
849
849
|
import.meta.url,
|
|
850
850
|
);
|
|
851
|
-
url.searchParams.set("arcaneSpeechWorkerMode", "artifact-module-worker");
|
|
852
851
|
return url;
|
|
853
852
|
}
|
|
854
853
|
|
package/docs/architecture.md
CHANGED
|
@@ -200,12 +200,13 @@ available until this wrapper and its explicit capability boundary are actually
|
|
|
200
200
|
implemented.
|
|
201
201
|
|
|
202
202
|
Packaging and release verification are separate explicit operations. Run
|
|
203
|
-
`arcane package` to generate `
|
|
203
|
+
`arcane package` to generate the selected app's `outputDirectory`, defaulting
|
|
204
|
+
to `dist/<id>`, then use
|
|
204
205
|
`arcane run --target browser` to serve that selected release. Packaging does
|
|
205
206
|
not automatically run tests or checks; those run only when the user expressly
|
|
206
207
|
requests them or when required for a separately selected release output. The browser
|
|
207
208
|
run command does not substitute source files. If source changes after
|
|
208
|
-
packaging, the prior
|
|
209
|
+
packaging, the prior packaged output remains intentionally unchanged until the next
|
|
209
210
|
explicit package operation. Never use packaged `dist` as the everyday
|
|
210
211
|
development tree, and never treat source-serving behavior as evidence for the
|
|
211
212
|
release artifact.
|
|
@@ -224,6 +225,17 @@ dist/<id>/apps/<id>/index.html
|
|
|
224
225
|
dist/<id>/ARCANE_APP_RELEASE.json
|
|
225
226
|
```
|
|
226
227
|
|
|
228
|
+
These release paths show the default output location. An app's optional
|
|
229
|
+
schema-1 `arcane-package.json` field `outputDirectory` selects the final
|
|
230
|
+
workspace-relative directory instead: `"ai"` means `<workspace>/ai`, with no
|
|
231
|
+
app-ID suffix. The selected directory is wholly replaced through the existing
|
|
232
|
+
staged package swap; a changed destination preserves the old output and sibling
|
|
233
|
+
directories. Input/source conflicts fail before output writes. This changes
|
|
234
|
+
only package placement, not source layout, app identity, OPFS scope, or internal
|
|
235
|
+
release URLs. Preview, verification, bundling, and native browser-content input
|
|
236
|
+
consume that same selected release root; native executable output remains
|
|
237
|
+
separate. See the [complete output contract](reference/protocols.md#app-selected-package-output).
|
|
238
|
+
|
|
227
239
|
Packaging places each selected app file beneath `apps/<id>/` in the release
|
|
228
240
|
and preserves every shared route destination. When no selected shared payload
|
|
229
241
|
supplies a root `index.html`, the SDK generates one that opens the selected app
|
|
@@ -56,8 +56,21 @@ punctuation boundary should submit promptly. This is incremental playback of
|
|
|
56
56
|
completed synthesis jobs, not a live audio stream from DigitalOcean.
|
|
57
57
|
|
|
58
58
|
The transport submits `POST https://inference.do-ai.run/v1/async-invoke`, then
|
|
59
|
-
reads that job until its completed `output.audio.url` is available.
|
|
60
|
-
|
|
59
|
+
reads that job until its completed `output.audio.url` is available. One provider
|
|
60
|
+
spaces inference request starts at least one second apart across its concurrent
|
|
61
|
+
jobs; it does not wait for one remote job to finish before starting another.
|
|
62
|
+
A readable `429` rejection permits one retry of the same submission or status
|
|
63
|
+
request after at least one second. A longer readable `Retry-After` postpones
|
|
64
|
+
all pending inference starts for that provider. A failed status Fetch may also
|
|
65
|
+
retry once for the same accepted job. Repeated failures surface their complete
|
|
66
|
+
diagnostics without an endless retry loop; a failed job does not cancel siblings.
|
|
67
|
+
Retry diagnostics remain in the developer console, outside conversation history.
|
|
68
|
+
|
|
69
|
+
An ambiguous submission network failure is not automatically resubmitted: a
|
|
70
|
+
browser cannot distinguish a rejected preflight from an accepted paid job whose
|
|
71
|
+
response was lost. This contract has no submission idempotency mechanism, and
|
|
72
|
+
unreadable response headers cannot supply a server cooldown. Pacing reduces local
|
|
73
|
+
request bursts; it does not claim to free upstream concurrency slots or fix CORS.
|
|
61
74
|
The audio download never receives the inference Authorization header.
|
|
62
75
|
The selected multilingual-v2 service supports speed `0.7` through `1.2`;
|
|
63
76
|
unsupported speeds reject rather than being silently clamped. Complete
|
package/docs/reference/cli.md
CHANGED
|
@@ -529,8 +529,9 @@ npm exec -- arcane check --app hello-world
|
|
|
529
529
|
|
|
530
530
|
### Overview
|
|
531
531
|
|
|
532
|
-
Creates one complete browser release
|
|
533
|
-
|
|
532
|
+
Creates one complete browser release in the app's selected output directory,
|
|
533
|
+
defaulting to `dist/<id>/`, and preserves the prior output until the replacement
|
|
534
|
+
is complete. It consumes saved source and managed
|
|
534
535
|
import maps, keeps standalone app files at the output root, and retains the
|
|
535
536
|
configured shared route destinations. Explicit multi-app workspaces retain
|
|
536
537
|
their selected app beneath `apps/<id>/`. When selected shared content supplies no root
|
|
@@ -538,6 +539,14 @@ their selected app beneath `apps/<id>/`. When selected shared content supplies n
|
|
|
538
539
|
Source document bases and resource URLs therefore retain their development
|
|
539
540
|
layout. Packaging does not run tests or checks automatically.
|
|
540
541
|
|
|
542
|
+
Set `"outputDirectory": "ai"` in the selected app's `arcane-package.json`
|
|
543
|
+
to emit directly to `<workspace>/ai`; the SDK appends no app ID. Omit the field
|
|
544
|
+
to retain the default. There is no new CLI flag. The selected directory is
|
|
545
|
+
wholly replaced, not merged; choose a directory dedicated to the release.
|
|
546
|
+
Changing this setting preserves the old output and its siblings. Source/input
|
|
547
|
+
overlap fails before output writes. App identity, source layout, and saved
|
|
548
|
+
data remain unchanged. See the [output contract](protocols.md#app-selected-package-output).
|
|
549
|
+
|
|
541
550
|
For `installed-v1`, the same four configured source routes supply the complete
|
|
542
551
|
selected SDK content directly from `node_modules`. Only the portable output
|
|
543
552
|
receives copies; no workspace `arcane/` projection is required. Its runtime URLs,
|
|
@@ -603,7 +612,10 @@ arcane bundle [--app <id>] [--artifact <file>.arcane-app.tar.gz] [--overwrite]
|
|
|
603
612
|
|
|
604
613
|
### Replacement behavior
|
|
605
614
|
|
|
606
|
-
The default
|
|
615
|
+
The default archive is `<id>-<version>.arcane-app.tar.gz` beside the app's
|
|
616
|
+
selected package directory. With the default package location, this remains
|
|
617
|
+
`dist/<id>-<version>.arcane-app.tar.gz`. Bundling reads the same selected release
|
|
618
|
+
root used by packaging and preview. An existing path
|
|
607
619
|
is refused unless `--overwrite` is explicit. Even then, the prior artifact is
|
|
608
620
|
retained until the replacement is complete. A conflicting or uncertain path is
|
|
609
621
|
preserved rather than overwritten.
|
|
@@ -697,6 +709,8 @@ The command selects one workspace, app, target, architecture, format, signing
|
|
|
697
709
|
profile, and output root. Current providers emit a portable directory,
|
|
698
710
|
Windows x64 EXE bundle, Linux x64/ARM64 DEB, or development-signed Android APK.
|
|
699
711
|
The output remains target-specific inside the common plan contract.
|
|
712
|
+
The app's `outputDirectory` selects its packaged browser-content input;
|
|
713
|
+
native `--output-root` independently selects native build output.
|
|
700
714
|
`--dry-run` is implemented for the browser build path. Native builds reject it
|
|
701
715
|
rather than returning a fictional native artifact plan.
|
|
702
716
|
|
|
@@ -713,7 +727,8 @@ npm exec -- arcane build \
|
|
|
713
727
|
|
|
714
728
|
### Overview
|
|
715
729
|
|
|
716
|
-
For `--target browser`, starts the existing
|
|
730
|
+
For `--target browser`, starts the existing release in the app's configured
|
|
731
|
+
`outputDirectory`, or `dist/<app>` when omitted; it
|
|
717
732
|
does not package, rebuild, test, check, or verify that release automatically.
|
|
718
733
|
It opens the release manifest's `app.start` URL. Older flat releases without
|
|
719
734
|
that field continue to open their `app.entry` path.
|
|
@@ -902,7 +902,7 @@
|
|
|
902
902
|
],
|
|
903
903
|
"primaryImport": "arcane-os",
|
|
904
904
|
"group": "Packaging and release bundles",
|
|
905
|
-
"summary": "Loads one validated packager app plan, including the shared descriptor-admitted browser document inventory.",
|
|
905
|
+
"summary": "Loads one validated packager app plan, including the shared descriptor-admitted browser document inventory and final workspace-relative output selected by app outputDirectory or default dist/appId.",
|
|
906
906
|
"availability": "Node",
|
|
907
907
|
"protocol": "SDK packager and deterministic bundle contract",
|
|
908
908
|
"normalization": "Normalized SDK validation with complete canonical archive and release content"
|
|
@@ -1197,7 +1197,7 @@
|
|
|
1197
1197
|
],
|
|
1198
1198
|
"primaryImport": "arcane-os",
|
|
1199
1199
|
"group": "Packaging and release bundles",
|
|
1200
|
-
"summary": "Builds one selected browser application release through
|
|
1200
|
+
"summary": "Builds one selected browser application release into app outputDirectory or default dist/appId through staged whole-directory replacement, preserving source and unrelated prior destinations; it never runs tests or checks automatically.",
|
|
1201
1201
|
"availability": "Node",
|
|
1202
1202
|
"protocol": "SDK packager and deterministic bundle contract",
|
|
1203
1203
|
"normalization": "Normalized SDK validation with complete canonical archive and release content"
|
|
@@ -1214,7 +1214,7 @@
|
|
|
1214
1214
|
],
|
|
1215
1215
|
"primaryImport": "arcane-os",
|
|
1216
1216
|
"group": "Headless toolchain operations",
|
|
1217
|
-
"summary": "Runs the high-level package operation for one selected application.",
|
|
1217
|
+
"summary": "Runs the high-level package operation for one selected application using the same app-selected final outputDirectory, defaulting to dist/appId, with no separate destination override.",
|
|
1218
1218
|
"availability": "Node; selected operation may produce browser or native output",
|
|
1219
1219
|
"protocol": "Shared headless operation API + arcane-cli-events/1",
|
|
1220
1220
|
"normalization": "SDK-normalized inputs, errors, events, and documented result"
|
|
@@ -2031,7 +2031,7 @@
|
|
|
2031
2031
|
],
|
|
2032
2032
|
"primaryImport": "arcane-os/packager",
|
|
2033
2033
|
"group": "Packaging and release bundles",
|
|
2034
|
-
"summary": "Validates one schema-1 packager app configuration and its relationship to the root config.",
|
|
2034
|
+
"summary": "Validates one schema-1 packager app configuration and its relationship to the root config, including optional workspace-relative final outputDirectory without changing source layout or app identity.",
|
|
2035
2035
|
"availability": "Node",
|
|
2036
2036
|
"protocol": "SDK packager and deterministic bundle contract",
|
|
2037
2037
|
"normalization": "Normalized SDK validation with complete canonical archive and release content"
|
|
@@ -96,6 +96,44 @@ materializes the complete required SDK runtime,
|
|
|
96
96
|
browser-runtime, and managed-import-map closure inside that app's own artifact.
|
|
97
97
|
No application polls for SDK changes.
|
|
98
98
|
|
|
99
|
+
## App-selected package output
|
|
100
|
+
|
|
101
|
+
An app may set `"outputDirectory": "ai"` in its schema-1
|
|
102
|
+
`arcane-package.json`. This selects the final directory relative to the
|
|
103
|
+
workspace: `<workspace>/ai`, not `<workspace>/ai/<app-id>`. Omitting the
|
|
104
|
+
field retains `dist/<app-id>`. The root `arcane-packager.json` configuration
|
|
105
|
+
and simple scaffold defaults remain unchanged.
|
|
106
|
+
The destination is local to `arcane-package.json`, including apps with a
|
|
107
|
+
schema-2 `arcane-app.json`. Descriptor refresh preserves it; no matching
|
|
108
|
+
descriptor property or identity change is required.
|
|
109
|
+
|
|
110
|
+
This field controls the destination of the complete packaged release. It does
|
|
111
|
+
not relocate application source, rename the app, change its identity or OPFS
|
|
112
|
+
scope, or alter the package's internal file paths. Source development continues
|
|
113
|
+
to serve the same source tree. There is no migration of existing source,
|
|
114
|
+
saved data, or previously generated output.
|
|
115
|
+
|
|
116
|
+
The selected directory is wholly replaced through the packager's staged
|
|
117
|
+
replacement operation, so it must be dedicated to that release. The prior
|
|
118
|
+
contents remain until the replacement is ready; this is not an in-place merge.
|
|
119
|
+
Changing the configured destination leaves the former output directory and
|
|
120
|
+
unrelated siblings untouched. Output that would replace the app's source root,
|
|
121
|
+
selected inputs, or required control files fails with `ARCANE_PACKAGE_INVALID`
|
|
122
|
+
before output writes rather than overwriting those inputs.
|
|
123
|
+
If an included source directory contains the output directory, use the existing
|
|
124
|
+
`exclude` selection to keep that generated subtree outside the package inputs.
|
|
125
|
+
|
|
126
|
+
Inspection and dry runs report this selected destination. Packaging,
|
|
127
|
+
verification, and `arcane run --target browser` use the same release root.
|
|
128
|
+
The normal bundle destination is beside that release directory as
|
|
129
|
+
`<app-id>-<version>.arcane-app.tar.gz`; an explicit artifact path retains its
|
|
130
|
+
existing meaning. Native packaging supplies the selected release root to the
|
|
131
|
+
native provider, while native executable `outputRoot` remains a separate
|
|
132
|
+
target-output setting. Selected app package directories and native artifact
|
|
133
|
+
output must remain separate; a conflicting native assembly fails before it
|
|
134
|
+
packages any app. App launch paths, resource URLs, and enabled PWA
|
|
135
|
+
representations retain their package-relative layout.
|
|
136
|
+
|
|
99
137
|
## Installed-package browser routes
|
|
100
138
|
|
|
101
139
|
An external application can serve and package its installed SDK directly,
|
|
@@ -340,7 +378,8 @@ catch-all `arcane/` prefix. Classic scripts, workers, stylesheets, and other
|
|
|
340
378
|
non-ESM assets use their documented URL or host loading contract rather than
|
|
341
379
|
invented package bindings. Development serves the selected app plus the
|
|
342
380
|
complete selected tree.
|
|
343
|
-
Packaging copies the same map, app entry, and physical content into
|
|
381
|
+
Packaging copies the same map, app entry, and physical content into the
|
|
382
|
+
[selected package output](#app-selected-package-output), defaulting to `dist/<id>`;
|
|
344
383
|
targets never resolve through the consumer workspace's root `node_modules/`.
|
|
345
384
|
The two lowercase static runtime package specifiers above and the shared
|
|
346
385
|
speech-text helper are exact npm package exports, so Node and managed-browser
|
package/docs/reference/pwa.md
CHANGED
|
@@ -93,6 +93,16 @@ The application entry and generated PWA shell records are retained. Select the
|
|
|
93
93
|
resources needed by every offline page, including its shared modules and styles.
|
|
94
94
|
The worker script itself is never an application cache entry.
|
|
95
95
|
|
|
96
|
+
For direct installed-package root applications, public `arcane import-map`
|
|
97
|
+
generation follows the same source resource graph as `arcane dev` before writing
|
|
98
|
+
the static offline inventory. Imports from unvisited pages, shared runtime
|
|
99
|
+
modules, component styles, and statically declared Worker URLs retain their
|
|
100
|
+
authored query values. SDK component `./arcane/` resource attributes resolve from
|
|
101
|
+
that component's actual runtime root, including installed package aliases and
|
|
102
|
+
nested component directories; ordinary application document bases keep their
|
|
103
|
+
normal meaning. The graph selects resources already present in the configured
|
|
104
|
+
routes. It does not invent resource paths or enumerate user-specific page URLs.
|
|
105
|
+
|
|
96
106
|
These settings describe published application resources. They do not select
|
|
97
107
|
user uploads, provider responses, live API requests, saved conversations,
|
|
98
108
|
preferences, IndexedDB, OPFS, or a model owner's download/cache lifecycle.
|
|
@@ -180,7 +190,7 @@ without loading certificates; see [explicit HTTP development](cli.md#explicit-ht
|
|
|
180
190
|
`--public` selects the IPv4 wildcard bind address; it does not enable PWA
|
|
181
191
|
configuration, change manifest metadata, or determine browser installability.
|
|
182
192
|
|
|
183
|
-
|
|
193
|
+
Live source inventory work begins when the browser requests the worker or current
|
|
184
194
|
offline manifest, after the page can start. It traverses the selected route
|
|
185
195
|
inventory once for that request, follows page and runtime resource references
|
|
186
196
|
to retain selected query variants, and shares an in-flight traversal with
|
|
@@ -189,6 +199,11 @@ document corpus bodies remain under their existing owner. It does not rebuild
|
|
|
189
199
|
the application. Installation metadata and bootstrap requests reuse the current
|
|
190
200
|
generated bundle.
|
|
191
201
|
|
|
202
|
+
An explicit root import-map refresh also performs that traversal to produce the
|
|
203
|
+
committed static PWA files. It opens no server or listener. Its generated
|
|
204
|
+
inventory is available to ordinary static hosting without first visiting each
|
|
205
|
+
page through the development server.
|
|
206
|
+
|
|
192
207
|
The SDK owns version information in `arcane-offline.json`:
|
|
193
208
|
|
|
194
209
|
| Field | Owner and update rule |
|
|
@@ -262,6 +277,18 @@ consistent deployment of the selected output.
|
|
|
262
277
|
An offline navigation alias redirects to its selected entry page, preserving the
|
|
263
278
|
document URL used to resolve relative modules and styles.
|
|
264
279
|
|
|
280
|
+
For a same-origin navigation to a selected static `.html` or `.htm` document,
|
|
281
|
+
the worker can reuse that document's cached body while leaving the complete
|
|
282
|
+
query and fragment in the browser's navigation URL. Pages such as
|
|
283
|
+
`dashboard.html?id=person` therefore retain their application-owned parameters
|
|
284
|
+
without enumerating people or saving a cache entry for every query. An exact
|
|
285
|
+
query-specific resource already selected in the inventory takes precedence.
|
|
286
|
+
When the plain document has no retained cached body, the original navigation
|
|
287
|
+
request goes to the network unchanged; its response is not saved over the plain
|
|
288
|
+
document. This does not change query matching for assets, API calls, ordinary
|
|
289
|
+
fetch requests, unselected documents or other origins. Cache refresh and the
|
|
290
|
+
Update prompt keep their existing lifecycle.
|
|
291
|
+
|
|
265
292
|
Browser storage eviction can remove an offline cache. A missing release cache
|
|
266
293
|
entry falls back to the network, so offline availability still depends on the
|
|
267
294
|
browser retaining the selected resources.
|
|
@@ -1566,8 +1566,10 @@ boundary applies to STT activation, transcription, TTS lifecycle, synthesis,
|
|
|
1566
1566
|
and playback failures.
|
|
1567
1567
|
|
|
1568
1568
|
`reportTTSError()` recognizes `synthesis`, `decode`, `playback-start`, and
|
|
1569
|
-
`playback-resume`. It
|
|
1570
|
-
|
|
1569
|
+
`playback-resume`. It preserves the user's mute choice, sticky provider
|
|
1570
|
+
readiness, and accepted sibling speech jobs. The AI runtime owns settlement of
|
|
1571
|
+
the failed job; reporting an error neither stops the queue nor retries it. The
|
|
1572
|
+
component emits `speech-synthesis-error` with the exact boundary,
|
|
1571
1573
|
stable reason/code, generic user-safe status, and complete Error in the local
|
|
1572
1574
|
event detail.
|
|
1573
1575
|
|
|
@@ -1600,8 +1602,16 @@ same capture operation id rather than inventing a second correlation boundary.
|
|
|
1600
1602
|
User Unmute calls the shared `AI.setSpeechMuted(false)` lifecycle owner before
|
|
1601
1603
|
or with publishing the TTS load intent, so the runtime can legally load TTS.
|
|
1602
1604
|
Configured `initialMuted:false` records that same unmute intent even when the
|
|
1603
|
-
TTS route is still unselected or loading
|
|
1604
|
-
|
|
1605
|
+
TTS route is still unselected or loading. Public `muted` expresses the user's
|
|
1606
|
+
choice; role availability and the AI runtime still prevent playback until the
|
|
1607
|
+
selected role reaches `ready`, when the waiting intent is applied. A failed
|
|
1608
|
+
load ends its activation attempt without changing that choice or starting
|
|
1609
|
+
another attempt. Retry voice explicitly starts a new attempt, including when
|
|
1610
|
+
the preserved choice is unmuted; an unloaded selected voice offers Load voice.
|
|
1611
|
+
When a role returns to ready, permitted queued playback resumes through the
|
|
1612
|
+
existing AI owner without repeating activation or replaying failed jobs.
|
|
1613
|
+
Synthesis, decode, and playback failures also
|
|
1614
|
+
preserve that choice and do not automatically replay failed speech.
|
|
1605
1615
|
Mute calls `AI.setSpeechMuted(true)`, stops playback, cancels active TTS work,
|
|
1606
1616
|
and unloads the selected TTS role. Lifecycle failures remain visible through
|
|
1607
1617
|
`speech-tts-lifecycle-error` and sticky role state.
|
|
@@ -779,6 +779,10 @@ such as `apps/hello-world/index.html`. The complete `files` inventory uses
|
|
|
779
779
|
package-relative paths and includes the root `index.html`. Shared
|
|
780
780
|
files retain their configured route destinations.
|
|
781
781
|
|
|
782
|
+
`output` is the selected workspace-relative final package directory. It uses
|
|
783
|
+
the app's optional `arcane-package.json` `outputDirectory` directly, or
|
|
784
|
+
`dist/<app-id>` when omitted; the SDK appends no app ID to an explicit value.
|
|
785
|
+
|
|
782
786
|
### Availability and normalization
|
|
783
787
|
|
|
784
788
|
**Node.** Normalized SDK validation with complete canonical archive and release content. Deep protocol: [SDK packager and deterministic bundle contract](protocols.md).
|
|
@@ -839,6 +843,15 @@ The package preserves the complete selected content, applying the documented
|
|
|
839
843
|
asset-version and enabled browser-PWA transformations to resource references.
|
|
840
844
|
Malformed configuration or descriptors fail while preserving the prior output.
|
|
841
845
|
|
|
846
|
+
The app's optional schema-1 `outputDirectory` selects the final
|
|
847
|
+
workspace-relative destination. For example, `"ai"` emits directly into
|
|
848
|
+
`<workspace>/ai`; omission retains `dist/<app-id>`. The selected directory is
|
|
849
|
+
wholly replaced through the existing staged swap, not merged. Changing the
|
|
850
|
+
destination preserves the former output and siblings. Conflicting source,
|
|
851
|
+
selected input, or required control paths fail before output writes. Source
|
|
852
|
+
layout, app identity, OPFS scope, and internal package paths remain unchanged.
|
|
853
|
+
See [app-selected package output](protocols.md#app-selected-package-output).
|
|
854
|
+
|
|
842
855
|
Selected app files are emitted beneath `apps/<id>/`; shared files retain their
|
|
843
856
|
configured route destinations. When selected shared content supplies no root
|
|
844
857
|
`index.html`, the SDK generates one that opens the selected app page. The result
|
|
@@ -1030,6 +1043,13 @@ console.log(validateAppBundlePath('payload/apps/example/index.html'));
|
|
|
1030
1043
|
|
|
1031
1044
|
Validates one schema-1 packager app configuration and its relationship to the root config.
|
|
1032
1045
|
|
|
1046
|
+
Optional `outputDirectory` is the final workspace-relative package directory;
|
|
1047
|
+
omission preserves `dist/<app-id>`. It is an app package field, not a change to
|
|
1048
|
+
the root configuration's `distRoot` or the schema-2 app identity. Packaging
|
|
1049
|
+
reports conflicting output/input selections with `ARCANE_PACKAGE_INVALID`
|
|
1050
|
+
before output writes. Authored-descriptor comparison treats the destination
|
|
1051
|
+
as package-local, and descriptor refresh preserves the existing setting.
|
|
1052
|
+
|
|
1033
1053
|
### Signature and result
|
|
1034
1054
|
|
|
1035
1055
|
```text
|
|
@@ -2650,6 +2670,11 @@ async function useresolveNativeBuildOutputRoot(...arguments_) {
|
|
|
2650
2670
|
|
|
2651
2671
|
Invokes one target adapter run method and fails honestly when the artifact kind is not runnable.
|
|
2652
2672
|
|
|
2673
|
+
For the browser adapter, optional `releaseRoot` selects an existing packaged
|
|
2674
|
+
directory directly. Omission keeps the direct adapter's `dist/<app-id>`
|
|
2675
|
+
default, including callers with only a release workspace. The high-level
|
|
2676
|
+
`runApplication()` supplies the app-configured destination automatically.
|
|
2677
|
+
|
|
2653
2678
|
### Signature and result
|
|
2654
2679
|
|
|
2655
2680
|
```text
|
|
@@ -4024,6 +4049,10 @@ async function usebuildApplication(...arguments_) {
|
|
|
4024
4049
|
|
|
4025
4050
|
Creates one deterministic external application bundle from one authenticated packaged release.
|
|
4026
4051
|
|
|
4052
|
+
The high-level operation reads the app's selected package directory. Its
|
|
4053
|
+
default archive is placed beside that directory; an explicit artifact path
|
|
4054
|
+
retains the existing output-path contract.
|
|
4055
|
+
|
|
4027
4056
|
### Signature and result
|
|
4028
4057
|
|
|
4029
4058
|
```text
|
|
@@ -4280,6 +4309,12 @@ import maps through `packageApp()`. Map generation remains the explicit
|
|
|
4280
4309
|
Selected app files retain their `apps/<id>/` paths alongside the shared routes
|
|
4281
4310
|
and root launcher described by `packageApp()`.
|
|
4282
4311
|
|
|
4312
|
+
The same low-level operation resolves the app's optional `outputDirectory`;
|
|
4313
|
+
there is no separate high-level destination override. `release.outputRoot`
|
|
4314
|
+
names the resulting complete release directory, with omitted configuration
|
|
4315
|
+
retaining `dist/<app-id>`. Browser preview, bundle creation, and native
|
|
4316
|
+
browser-content input use that selected root.
|
|
4317
|
+
|
|
4283
4318
|
Packaging does not automatically run tests or checks. Verification occurs only
|
|
4284
4319
|
when explicitly requested or when required for the selected release output.
|
|
4285
4320
|
A failure leaves the prior distribution untouched. Success returns the selected
|
|
@@ -4375,6 +4410,10 @@ async function userepositoryApplication(...arguments_) {
|
|
|
4375
4410
|
|
|
4376
4411
|
Runs a browser app or performs the selected native build/launch lifecycle for one target. It does not run tests or checks automatically; selected release-output verification remains explicit to that release operation.
|
|
4377
4412
|
|
|
4413
|
+
Browser runs resolve the app's current `outputDirectory`, defaulting to
|
|
4414
|
+
`dist/<app-id>`, and preview that existing release without repackaging it.
|
|
4415
|
+
Native artifact output remains separately selected by its target contract.
|
|
4416
|
+
|
|
4378
4417
|
### Signature and result
|
|
4379
4418
|
|
|
4380
4419
|
```text
|
package/package.json
CHANGED
|
@@ -518,7 +518,7 @@
|
|
|
518
518
|
requestUserMute();
|
|
519
519
|
} else {
|
|
520
520
|
pendingUnmute = true;
|
|
521
|
-
host.muted =
|
|
521
|
+
host.muted = false;
|
|
522
522
|
if (!settleSuccessfulUnmute()) {
|
|
523
523
|
synchronizeAIMutedState();
|
|
524
524
|
continuePendingUnmute();
|
|
@@ -567,21 +567,24 @@
|
|
|
567
567
|
if (sttBecameUnready) {
|
|
568
568
|
cancelSTTOperation('runtime-unready');
|
|
569
569
|
}
|
|
570
|
-
if (ttsBecameUnready) {
|
|
570
|
+
if (ttsBecameUnready && ttsRole.state !== 'error') {
|
|
571
571
|
pendingUnmute = false;
|
|
572
|
-
host.muted = true;
|
|
573
572
|
stopTTSPlayback();
|
|
574
573
|
}
|
|
575
|
-
if (ttsRole.state === 'error'
|
|
574
|
+
if (ttsRole.state === 'error') {
|
|
576
575
|
pendingUnmute = false;
|
|
577
|
-
host.muted = true;
|
|
578
|
-
stopTTSPlayback();
|
|
579
576
|
}
|
|
580
577
|
|
|
581
|
-
settleSuccessfulUnmute();
|
|
578
|
+
const unmuteSettled = settleSuccessfulUnmute();
|
|
582
579
|
continuePendingUnmute();
|
|
583
580
|
|
|
584
581
|
synchronizeAIMutedState();
|
|
582
|
+
if (!unmuteSettled
|
|
583
|
+
&& previousTTSRole.state !== 'ready'
|
|
584
|
+
&& ttsRole.state === 'ready'
|
|
585
|
+
&& !host.muted) {
|
|
586
|
+
resumeTTSPlayback();
|
|
587
|
+
}
|
|
585
588
|
renderControls();
|
|
586
589
|
renderStatus();
|
|
587
590
|
}
|
|
@@ -720,6 +723,9 @@
|
|
|
720
723
|
if (pendingTTSIntent === 'load') {
|
|
721
724
|
label = 'Cancel voice load';
|
|
722
725
|
description = 'Cancel loading the selected voice.';
|
|
726
|
+
} else if (!host.muted) {
|
|
727
|
+
label = 'Load voice';
|
|
728
|
+
description = 'Load the selected voice.';
|
|
723
729
|
}
|
|
724
730
|
} else if (ttsRole.state === 'error') {
|
|
725
731
|
disabled = !providerSelected;
|
|
@@ -960,7 +966,7 @@
|
|
|
960
966
|
return;
|
|
961
967
|
}
|
|
962
968
|
|
|
963
|
-
if (host.muted) {
|
|
969
|
+
if (host.muted || ['unloaded', 'error'].includes(ttsRole.state)) {
|
|
964
970
|
void requestUserUnmute();
|
|
965
971
|
} else {
|
|
966
972
|
requestUserMute();
|
|
@@ -976,7 +982,7 @@
|
|
|
976
982
|
}
|
|
977
983
|
const wasMuted = host.muted;
|
|
978
984
|
pendingUnmute = false;
|
|
979
|
-
const shouldUnload = !wasMuted
|
|
985
|
+
const shouldUnload = (!wasMuted && is.function(globalThis.ai?.setSpeechMuted))
|
|
980
986
|
|| pendingTTSIntent === 'load'
|
|
981
987
|
|| ['loading', 'ready', 'error'].includes(ttsRole.state);
|
|
982
988
|
host.muted = true;
|
|
@@ -1004,9 +1010,9 @@
|
|
|
1004
1010
|
) {
|
|
1005
1011
|
return false;
|
|
1006
1012
|
}
|
|
1013
|
+
host.muted = false;
|
|
1007
1014
|
if (!is.function(globalThis.ai?.setSpeechMuted)) {
|
|
1008
1015
|
pendingUnmute = true;
|
|
1009
|
-
host.muted = true;
|
|
1010
1016
|
renderControls();
|
|
1011
1017
|
renderStatus();
|
|
1012
1018
|
return false;
|
|
@@ -1021,8 +1027,7 @@
|
|
|
1021
1027
|
&& ['unloaded', 'loading', 'error'].includes(ttsRole.state)
|
|
1022
1028
|
);
|
|
1023
1029
|
if (!canRequestLoad) {
|
|
1024
|
-
|
|
1025
|
-
stopTTSPlayback();
|
|
1030
|
+
pendingUnmute = false;
|
|
1026
1031
|
reportTTSLifecycleError(
|
|
1027
1032
|
new Error('No selected speech provider is available to load.'),
|
|
1028
1033
|
'load'
|
|
@@ -1038,8 +1043,6 @@
|
|
|
1038
1043
|
}
|
|
1039
1044
|
if (!requested) {
|
|
1040
1045
|
pendingUnmute = false;
|
|
1041
|
-
host.muted = true;
|
|
1042
|
-
stopTTSPlayback();
|
|
1043
1046
|
renderControls();
|
|
1044
1047
|
renderStatus();
|
|
1045
1048
|
return false;
|
|
@@ -1117,8 +1120,6 @@
|
|
|
1117
1120
|
pendingTTSIntent = null;
|
|
1118
1121
|
if (action === 'load') {
|
|
1119
1122
|
pendingUnmute = false;
|
|
1120
|
-
host.muted = true;
|
|
1121
|
-
stopTTSPlayback();
|
|
1122
1123
|
}
|
|
1123
1124
|
reportTTSLifecycleError(error, action, operationId);
|
|
1124
1125
|
return false;
|
|
@@ -1211,8 +1212,6 @@
|
|
|
1211
1212
|
: 'synthesis';
|
|
1212
1213
|
const failure = failures[normalizedBoundary];
|
|
1213
1214
|
const message = visibleErrorMessage(error, failure.message);
|
|
1214
|
-
host.muted = true;
|
|
1215
|
-
stopTTSPlayback();
|
|
1216
1215
|
localStatus = {
|
|
1217
1216
|
message,
|
|
1218
1217
|
tone: 'error'
|
|
@@ -34,6 +34,10 @@
|
|
|
34
34
|
"entry": {
|
|
35
35
|
"$ref": "#/$defs/relativePath"
|
|
36
36
|
},
|
|
37
|
+
"outputDirectory": {
|
|
38
|
+
"$ref": "#/$defs/relativePath",
|
|
39
|
+
"description": "Final package directory relative to the workspace, with no appended app ID. Omission preserves dist/<id>. This directory is replaced when packaging succeeds; source layout and app identity remain unchanged."
|
|
40
|
+
},
|
|
37
41
|
"strategy": {
|
|
38
42
|
"enum": [
|
|
39
43
|
"static",
|
package/src/app-descriptor.mjs
CHANGED
|
@@ -448,6 +448,11 @@ export async function refreshAppPackageProjection({workspaceRoot, appId, signal,
|
|
|
448
448
|
packagePath,
|
|
449
449
|
`apps/${appId}/arcane-package.json`
|
|
450
450
|
);
|
|
451
|
+
// The output destination belongs to this workspace's package configuration,
|
|
452
|
+
// not the portable application descriptor or its identity.
|
|
453
|
+
if (packageManifest.outputDirectory !== undefined) {
|
|
454
|
+
projection.outputDirectory = packageManifest.outputDirectory;
|
|
455
|
+
}
|
|
451
456
|
const packageProjection = packageManifest.pwa === undefined ? packageManifest : {
|
|
452
457
|
...packageManifest,
|
|
453
458
|
pwa: normalizePwaConfig(packageManifest.pwa)
|
|
@@ -474,10 +479,11 @@ export async function loadAppDescriptor({workspaceRoot,appRoot,appId,packageMani
|
|
|
474
479
|
if(authored){
|
|
475
480
|
const descriptor=validateAppDescriptor(authored,{appId});
|
|
476
481
|
const projection=projectPackageManifest(authored);
|
|
477
|
-
const packageProjection=
|
|
482
|
+
const packageProjection={
|
|
478
483
|
...packageManifest,
|
|
479
|
-
pwa:normalizePwaConfig(packageManifest.pwa)
|
|
484
|
+
...(packageManifest.pwa===undefined?{}:{pwa:normalizePwaConfig(packageManifest.pwa)})
|
|
480
485
|
};
|
|
486
|
+
delete packageProjection.outputDirectory;
|
|
481
487
|
if(!isDeepStrictEqual(projection,packageProjection)){
|
|
482
488
|
fail(`${APP_DESCRIPTOR_NAME} does not project exactly to arcane-package.json.`);
|
|
483
489
|
}
|
package/src/app-layout.mjs
CHANGED
|
@@ -9,6 +9,10 @@ export function resolveAppRoot(workspaceRoot, config, appId) {
|
|
|
9
9
|
return path.resolve(workspaceRoot, appRelativeRoot(config, appId));
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
+
export function resolvePackageOutputRoot(workspaceRoot, config, app) {
|
|
13
|
+
return path.resolve(workspaceRoot, app.outputDirectory ?? `${config.distRoot}/${app.id}`);
|
|
14
|
+
}
|
|
15
|
+
|
|
12
16
|
export function appBaseHref(workspaceRoot, appRoot, document = 'index.html') {
|
|
13
17
|
const directory = path.dirname(path.resolve(appRoot, document));
|
|
14
18
|
const relative = path.relative(directory, workspaceRoot).split(path.sep).join('/');
|
package/src/dev-server.mjs
CHANGED
|
@@ -13,7 +13,7 @@ import {APP_CONFIG_NAME, validateAppConfig} from './packager/core.mjs';
|
|
|
13
13
|
import {createEventQueue} from './event-queue.mjs';
|
|
14
14
|
import {
|
|
15
15
|
applyPwaEntryReferences,inspectImportMapHtml,MANAGED_IMPORT_MAP_ATTRIBUTE,
|
|
16
|
-
readWorkspaceAssetVersion,rewriteAssetReferences,versionAssetUrl
|
|
16
|
+
readWorkspaceAssetVersion,resolveAssetReference,rewriteAssetReferences,versionAssetUrl
|
|
17
17
|
} from './import-map.mjs';
|
|
18
18
|
import {createPwaArtifacts,selectPwaFiles} from './pwa.mjs';
|
|
19
19
|
|
|
@@ -658,14 +658,15 @@ async function sourcePwaAssets(routeSet, mappings, signal, resourceUrls, resourc
|
|
|
658
658
|
}
|
|
659
659
|
}
|
|
660
660
|
}
|
|
661
|
-
for (const
|
|
661
|
+
for (const reference of references) {
|
|
662
|
+
const {url, kind} = reference;
|
|
662
663
|
if (kind === 'import' && !/^(?:\.{1,2}\/|\/)/u.test(url)) continue;
|
|
663
664
|
let target;
|
|
664
|
-
let base;
|
|
665
665
|
try {
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
666
|
+
target = resolveAssetReference(
|
|
667
|
+
{...reference, url: versionAssetUrl(url, null)},
|
|
668
|
+
{ownerUrl: current.url, documentUrl, managedMap}
|
|
669
|
+
);
|
|
669
670
|
} catch {
|
|
670
671
|
// Non-URL source values retain their authored behavior.
|
|
671
672
|
continue;
|
|
@@ -695,6 +696,11 @@ async function sourcePwaAssets(routeSet, mappings, signal, resourceUrls, resourc
|
|
|
695
696
|
return [...selectedUrls].sort();
|
|
696
697
|
}
|
|
697
698
|
|
|
699
|
+
export async function collectSourcePwaAssets({workspaceRoot,appId,signal,onEvent}={}){
|
|
700
|
+
const routes=await sourceRoutes(workspaceRoot,appId,{signal,onEvent});
|
|
701
|
+
return sourcePwaAssets(routes,routes.mappings,signal,new Set(),new Set());
|
|
702
|
+
}
|
|
703
|
+
|
|
698
704
|
function closeDevelopmentListeners(fileServer, tlsServer) {
|
|
699
705
|
const closeTls = tlsServer ? new Promise(
|
|
700
706
|
function closeRawTlsListener(resolve, reject) {
|
|
@@ -1236,13 +1242,16 @@ async function startOwnedDevServer({
|
|
|
1236
1242
|
.includes(request.headers['sec-fetch-dest']);
|
|
1237
1243
|
const rewrite=runtimeResource||entryDocument||browserResource||managedMap
|
|
1238
1244
|
||resourcePaths.has(target.path);
|
|
1239
|
-
const onReference = function observeServedResource(
|
|
1245
|
+
const onReference = function observeServedResource(reference) {
|
|
1246
|
+
const {url,kind,baseKind}=reference;
|
|
1240
1247
|
if (pwaEnabled && kind !== 'fetch'
|
|
1241
1248
|
&& !(kind === 'import' && !/^(?:\.{1,2}\/|\/)/u.test(url))) {
|
|
1242
1249
|
try {
|
|
1243
1250
|
const documentUrl = new URL(target.path, 'http://arcane.invalid');
|
|
1244
|
-
const
|
|
1245
|
-
|
|
1251
|
+
const resource = resolveAssetReference(
|
|
1252
|
+
{...reference,url:versionAssetUrl(url,null)},
|
|
1253
|
+
{ownerUrl:documentUrl}
|
|
1254
|
+
);
|
|
1246
1255
|
if (resource.origin === documentUrl.origin) {
|
|
1247
1256
|
pwaResourceUrls.add(`${resource.pathname}${resource.search}`);
|
|
1248
1257
|
}
|
|
@@ -1254,8 +1263,7 @@ async function startOwnedDevServer({
|
|
|
1254
1263
|
||(kind==='import'&&!/^(?:\.{1,2}\/|\/)/u.test(url)))return;
|
|
1255
1264
|
try{
|
|
1256
1265
|
const documentUrl=new URL(target.path,'http://arcane.invalid');
|
|
1257
|
-
const
|
|
1258
|
-
const resource=new URL(url,base);
|
|
1266
|
+
const resource=resolveAssetReference(reference,{ownerUrl:documentUrl});
|
|
1259
1267
|
if(resource.origin===documentUrl.origin
|
|
1260
1268
|
&&/\.(?:m?js|html?|css)$/iu.test(resource.pathname)){
|
|
1261
1269
|
resourcePaths.add(decodeURIComponent(resource.pathname));
|
package/src/import-map.mjs
CHANGED
|
@@ -1281,6 +1281,23 @@ export function rewriteAssetReferences(source,{filePath,version=SDK_VERSION,onRe
|
|
|
1281
1281
|
return source;
|
|
1282
1282
|
}
|
|
1283
1283
|
|
|
1284
|
+
export function resolveAssetReference({url,baseHref,baseKind},{ownerUrl,documentUrl=ownerUrl,managedMap=false}){
|
|
1285
|
+
if(baseKind==='component-runtime'&&url.startsWith('./arcane/')){
|
|
1286
|
+
const owner=new URL(ownerUrl);
|
|
1287
|
+
const marker='/arcane/components/';
|
|
1288
|
+
const componentIndex=owner.pathname.lastIndexOf(marker);
|
|
1289
|
+
if(componentIndex>=0){
|
|
1290
|
+
// HTMLImport resolves these component attributes against their
|
|
1291
|
+
// installed runtime, independently of the host document's base.
|
|
1292
|
+
const runtimeRoot=new URL(owner.pathname.slice(0,componentIndex+'/arcane/'.length),owner.origin);
|
|
1293
|
+
return new URL(url.slice('./arcane/'.length),runtimeRoot);
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
const base=baseHref?new URL(baseHref,ownerUrl)
|
|
1297
|
+
:managedMap||baseKind==='document'?documentUrl:ownerUrl;
|
|
1298
|
+
return new URL(url,base);
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1284
1301
|
function registerSpecifier(registry,specifier,target){
|
|
1285
1302
|
registry.set(specifier,{specifier,target});
|
|
1286
1303
|
}
|
|
@@ -2071,7 +2088,9 @@ function htmlReferenceEdits(source,version,onReference){
|
|
|
2071
2088
|
:element.tag==='link'&&relationships.includes('preload')
|
|
2072
2089
|
&&['script','worker','style'].includes(destination)?destination==='style'?'style':'script':'asset';
|
|
2073
2090
|
const view=htmlAttributeView(original);
|
|
2074
|
-
if(view)reportAssetReference(reportHtmlReference,view.decoded,kind
|
|
2091
|
+
if(view)reportAssetReference(reportHtmlReference,view.decoded,kind,
|
|
2092
|
+
(name==='href'||name==='src')&&view.decoded.startsWith('./arcane/')
|
|
2093
|
+
?'component-runtime':undefined);
|
|
2075
2094
|
const value=versionHtmlAttribute(original,version);
|
|
2076
2095
|
if(value===original)continue;
|
|
2077
2096
|
const position=attributes.positions.get(name);
|
package/src/packager/core.mjs
CHANGED
|
@@ -12,13 +12,14 @@ import {
|
|
|
12
12
|
} from 'node:fs/promises';
|
|
13
13
|
import path from 'node:path';
|
|
14
14
|
import {pathToFileURL} from 'node:url';
|
|
15
|
-
import {appRelativeRoot,resolveAppRoot} from '../app-layout.mjs';
|
|
15
|
+
import {appRelativeRoot,resolveAppRoot,resolvePackageOutputRoot} from '../app-layout.mjs';
|
|
16
16
|
import {readInstalledSdkLayout} from '../sdk-runtime-layout.mjs';
|
|
17
17
|
import {withWorkspaceOperationLock} from '../workspace-operation-lock.mjs';
|
|
18
18
|
import {
|
|
19
19
|
applyPwaEntryReferences,
|
|
20
20
|
inspectImportMapHtml,
|
|
21
21
|
readWorkspaceAssetVersion,
|
|
22
|
+
resolveAssetReference,
|
|
22
23
|
rewriteAssetReferences,
|
|
23
24
|
versionAssetUrl
|
|
24
25
|
} from '../import-map.mjs';
|
|
@@ -307,7 +308,7 @@ function normalizeOptionalRecord(value,label){
|
|
|
307
308
|
export function validateAppConfig(value,appId,rootConfig,configPath=path.posix.join(appRelativeRoot(rootConfig,appId),APP_CONFIG_NAME)){
|
|
308
309
|
assertOnlyKeys(value,new Set([
|
|
309
310
|
'schemaVersion','id','displayName','version','entry','strategy','security',
|
|
310
|
-
'localAIModelPolicy','include','exclude','shared','adapter','pwa'
|
|
311
|
+
'localAIModelPolicy','include','exclude','shared','adapter','pwa','outputDirectory'
|
|
311
312
|
]),`${appId}/${APP_CONFIG_NAME}`);
|
|
312
313
|
if(value.schemaVersion!==1)fail(`${appId}/${APP_CONFIG_NAME}.schemaVersion must be 1.`);
|
|
313
314
|
if(!is.string(value.id)||value.id!==appId||!APP_ID_PATTERN.test(value.id)){
|
|
@@ -316,6 +317,9 @@ export function validateAppConfig(value,appId,rootConfig,configPath=path.posix.j
|
|
|
316
317
|
const displayName=assertPresentationText(value.displayName,`${appId}/${APP_CONFIG_NAME}.displayName`);
|
|
317
318
|
parseSemver(value.version);
|
|
318
319
|
const entry=normalizeRelativePath(value.entry,`${appId}/${APP_CONFIG_NAME}.entry`);
|
|
320
|
+
const outputDirectory=value.outputDirectory===undefined?undefined:normalizeRelativePath(
|
|
321
|
+
value.outputDirectory,`${appId}/${APP_CONFIG_NAME}.outputDirectory`
|
|
322
|
+
);
|
|
319
323
|
const include=validatePathList(value.include,`${appId}/${APP_CONFIG_NAME}.include`,{required:true});
|
|
320
324
|
const exclude=validatePathList(value.exclude??[],`${appId}/${APP_CONFIG_NAME}.exclude`);
|
|
321
325
|
if(include.some(allowed=>sameOrDescendant(APP_CONFIG_NAME,allowed))){
|
|
@@ -351,6 +355,7 @@ export function validateAppConfig(value,appId,rootConfig,configPath=path.posix.j
|
|
|
351
355
|
displayName,
|
|
352
356
|
version:value.version,
|
|
353
357
|
entry,
|
|
358
|
+
...(outputDirectory===undefined?{}:{outputDirectory}),
|
|
354
359
|
strategy:value.strategy,
|
|
355
360
|
...(value.pwa===undefined?{}:{pwa:normalizePwaConfig(value.pwa)}),
|
|
356
361
|
...(value.security===undefined?{}:{security:normalizeOptionalRecord(
|
|
@@ -421,11 +426,67 @@ async function loadContext(requestedWorkspaceRoot,appId){
|
|
|
421
426
|
appRoot,
|
|
422
427
|
appId,
|
|
423
428
|
config,
|
|
424
|
-
|
|
425
|
-
outputRoot:path.join(workspaceRoot,rootConfig.distRoot,appId)
|
|
429
|
+
outputRoot:resolvePackageOutputRoot(workspaceRoot,rootConfig,config)
|
|
426
430
|
};
|
|
427
431
|
}
|
|
428
432
|
|
|
433
|
+
async function assertPackageOutputLocation(context){
|
|
434
|
+
const output=context.outputRoot.split(path.sep).join('/');
|
|
435
|
+
const absolute=location=>path.resolve(location).split(path.sep).join('/');
|
|
436
|
+
const controls=[
|
|
437
|
+
context.appRoot,
|
|
438
|
+
path.join(context.workspaceRoot,ROOT_CONFIG_NAME),
|
|
439
|
+
path.join(context.appRoot,APP_CONFIG_NAME),
|
|
440
|
+
path.join(context.appRoot,APP_DESCRIPTOR_NAME),
|
|
441
|
+
...(context.config.adapter?[path.join(context.appRoot,context.config.adapter)]:[])
|
|
442
|
+
];
|
|
443
|
+
for(const control of controls){
|
|
444
|
+
if(sameOrDescendant(absolute(control),output)){
|
|
445
|
+
fail(`Package output would replace application source or configuration: ${control}.`);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
for(const name of ['.git','.arcane','.agents','.codex','node_modules']){
|
|
449
|
+
const control=absolute(path.join(context.workspaceRoot,name));
|
|
450
|
+
if(sameOrDescendant(output,control)||sameOrDescendant(control,output)){
|
|
451
|
+
fail(`Package output overlaps the workspace control directory: ${name}.`);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
if(context.rootConfig.appsRoot!=='.'){
|
|
455
|
+
const relative=path.relative(context.appsRoot,context.outputRoot);
|
|
456
|
+
if(relative&&!relative.startsWith(`..${path.sep}`)&&relative!=='..'&&!path.isAbsolute(relative)){
|
|
457
|
+
const [otherAppId]=relative.split(path.sep);
|
|
458
|
+
if(pathKey(otherAppId)!==pathKey(context.appId)){
|
|
459
|
+
const otherAppRoot=path.join(context.appsRoot,otherAppId);
|
|
460
|
+
for(const manifest of [APP_CONFIG_NAME,APP_DESCRIPTOR_NAME]){
|
|
461
|
+
try{await lstat(path.join(otherAppRoot,manifest));}
|
|
462
|
+
catch(error){if(error?.code==='ENOENT')continue;throw error;}
|
|
463
|
+
fail(`Package output overlaps another application's source directory: ${otherAppRoot}.`);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
function assertSelectedInput(sourceRoot,selected,excludes,label){
|
|
469
|
+
if(isExcluded(selected,excludes))return;
|
|
470
|
+
const source=absolute(path.resolve(sourceRoot,selected));
|
|
471
|
+
const relativeOutput=path.relative(sourceRoot,context.outputRoot).split(path.sep).join('/');
|
|
472
|
+
if(sameOrDescendant(source,output)
|
|
473
|
+
||(sameOrDescendant(output,source)&&!isExcluded(relativeOutput,excludes))){
|
|
474
|
+
fail(`Package output overlaps selected ${label} content: ${selected}.`);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
for(const selected of context.config.include){
|
|
478
|
+
assertSelectedInput(context.appRoot,selected,context.config.exclude,context.appId);
|
|
479
|
+
}
|
|
480
|
+
for(const sharedId of context.config.shared){
|
|
481
|
+
for(const route of context.rootConfig.sharedPayloads[sharedId]){
|
|
482
|
+
const sourceRoot=path.resolve(context.workspaceRoot,route.source);
|
|
483
|
+
for(const selected of route.include){
|
|
484
|
+
assertSelectedInput(sourceRoot,selected,route.exclude,`sharedPayloads.${sharedId}`);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
429
490
|
function destinationJoin(root,relative){
|
|
430
491
|
return relative==='.'?root:root==='.'?relative:`${root}/${relative}`;
|
|
431
492
|
}
|
|
@@ -495,6 +556,7 @@ async function collectSelectedPath({
|
|
|
495
556
|
}
|
|
496
557
|
|
|
497
558
|
async function collectPackageRecords(context,{signal}={}){
|
|
559
|
+
await assertPackageOutputLocation(context);
|
|
498
560
|
const records=[];
|
|
499
561
|
const destinations=new Set();
|
|
500
562
|
for(const selected of context.config.include){
|
|
@@ -574,14 +636,15 @@ async function optionalDescriptor(context){
|
|
|
574
636
|
}
|
|
575
637
|
}
|
|
576
638
|
|
|
577
|
-
async function inspectContext(context,{signal}={}){
|
|
578
|
-
const records
|
|
579
|
-
const documents=await browserDocuments(
|
|
639
|
+
async function inspectContext(context,{signal,records}={}){
|
|
640
|
+
const selectedRecords=records??await collectPackageRecords(context,{signal});
|
|
641
|
+
const documents=await browserDocuments(selectedRecords,context.config.entry);
|
|
580
642
|
return {
|
|
581
643
|
appId:context.appId,
|
|
582
644
|
displayName:context.config.displayName,
|
|
583
645
|
version:context.config.version,
|
|
584
646
|
entry:context.config.entry,
|
|
647
|
+
...(context.config.outputDirectory===undefined?{}:{outputDirectory:context.config.outputDirectory}),
|
|
585
648
|
strategy:context.config.strategy,
|
|
586
649
|
...(context.config.pwa===undefined?{}:{pwa:copyJson(context.config.pwa)}),
|
|
587
650
|
include:[...context.config.include],
|
|
@@ -594,7 +657,7 @@ async function inspectContext(context,{signal}={}){
|
|
|
594
657
|
...(context.config.adapter===undefined?{}:{adapter:context.config.adapter}),
|
|
595
658
|
descriptor:await optionalDescriptor(context),
|
|
596
659
|
browserDocuments:documents,
|
|
597
|
-
files:[...new Set(['index.html',...
|
|
660
|
+
files:[...new Set(['index.html',...selectedRecords.map(record=>record.destination)])].sort(compareText),
|
|
598
661
|
output:path.relative(context.workspaceRoot,context.outputRoot).split(path.sep).join('/')
|
|
599
662
|
};
|
|
600
663
|
}
|
|
@@ -752,7 +815,8 @@ async function packageWithContext(context,options={}){
|
|
|
752
815
|
const pwaEnabled=browserPwa&&context.config.pwa?.enabled===true;
|
|
753
816
|
const appPath=appRelativeRoot(context.rootConfig,context.appId);
|
|
754
817
|
const entryPath=appPackagePath(context,context.config.entry);
|
|
755
|
-
const
|
|
818
|
+
const records=await collectPackageRecords(context,{signal});
|
|
819
|
+
const inspected=await inspectContext(context,{signal,records});
|
|
756
820
|
if(options.dryRun){
|
|
757
821
|
return {
|
|
758
822
|
appId:context.appId,
|
|
@@ -770,17 +834,29 @@ async function packageWithContext(context,options={}){
|
|
|
770
834
|
].sort(compareText)
|
|
771
835
|
};
|
|
772
836
|
}
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
837
|
+
// Directory replacement must stay on the requested physical path before
|
|
838
|
+
// mkdir or rename can affect an existing source tree through an ancestor.
|
|
839
|
+
let current=context.workspaceRoot;
|
|
840
|
+
for(const segment of path.relative(context.workspaceRoot,context.outputRoot).split(path.sep)){
|
|
841
|
+
current=path.join(current,segment);
|
|
842
|
+
let info;
|
|
843
|
+
try{info=await lstat(current);}
|
|
844
|
+
catch(error){if(error?.code==='ENOENT')break;throw error;}
|
|
845
|
+
if(info.isSymbolicLink()||!info.isDirectory()){
|
|
846
|
+
fail(`Package output must use real directories: ${current}.`);
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
const outputParent=path.dirname(context.outputRoot);
|
|
850
|
+
await mkdir(outputParent,{recursive:true});
|
|
851
|
+
const parentInfo=await lstat(outputParent);
|
|
852
|
+
if(parentInfo.isSymbolicLink()||!parentInfo.isDirectory())fail('Package output parent must be a real directory.');
|
|
776
853
|
const stagingRoot=path.join(
|
|
777
|
-
|
|
854
|
+
outputParent,
|
|
778
855
|
`.${context.appId}-staging-${process.pid}-${Date.now()}`
|
|
779
856
|
);
|
|
780
857
|
await mkdir(stagingRoot);
|
|
781
858
|
let promoted=false;
|
|
782
859
|
try{
|
|
783
|
-
const records=await collectPackageRecords(context,{signal});
|
|
784
860
|
async function copyBase() {
|
|
785
861
|
await copyRecords(records,stagingRoot,{signal,onEvent});
|
|
786
862
|
if (!records.some(function selectedRootIndex(record) {
|
|
@@ -892,17 +968,19 @@ async function packageWithContext(context,options={}){
|
|
|
892
968
|
resource={references,...(pwaDocumentSources.has(relative)?{content}:{})};
|
|
893
969
|
resources.set(relative,resource);
|
|
894
970
|
}
|
|
895
|
-
for(const
|
|
971
|
+
for(const reference of resource.references){
|
|
972
|
+
const {url,kind,baseHref}=reference;
|
|
896
973
|
const traversable=kind!=='fetch'&&(kind!=='asset'||/\.css(?:[?#]|$)/iu.test(url))
|
|
897
974
|
&&(kind!=='import'||/^(?:\.{1,2}\/|\/)/u.test(url));
|
|
898
975
|
if(!pwaEnabled&&!traversable)continue;
|
|
899
976
|
if(kind==='import'&&!/^(?:\.{1,2}\/|\/)/u.test(url))continue;
|
|
900
977
|
try{
|
|
901
978
|
const ownerUrl=new URL(packageResourceUrl(relative),entryUrl.origin);
|
|
902
|
-
const
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
979
|
+
const target=resolveAssetReference(reference,{
|
|
980
|
+
ownerUrl,
|
|
981
|
+
documentUrl:current.documentUrl,
|
|
982
|
+
managedMap:path.posix.basename(relative)==='arcane.importmap.json'
|
|
983
|
+
});
|
|
906
984
|
if(target.origin===entryUrl.origin){
|
|
907
985
|
const file=decodeURIComponent(target.pathname).replace(/^\//u,'');
|
|
908
986
|
if(pwaEnabled&&offlineInventory.has(file)){
|
|
@@ -1009,7 +1087,7 @@ export async function packageApp(options={}){
|
|
|
1009
1087
|
export async function verifyApp({workspaceRoot,appId,signal,onEvent}={}){
|
|
1010
1088
|
throwIfAborted(signal);
|
|
1011
1089
|
const context=await loadContext(workspaceRoot,appId);
|
|
1012
|
-
const outputRoot=await realDirectory(context.outputRoot
|
|
1090
|
+
const outputRoot=await realDirectory(context.outputRoot,'Package output');
|
|
1013
1091
|
const manifest=await readJson(path.join(outputRoot,RELEASE_MANIFEST_NAME),RELEASE_MANIFEST_NAME);
|
|
1014
1092
|
if(!isPlainObject(manifest)||manifest.schemaVersion!==1||manifest.kind!=='arcane-app-release'
|
|
1015
1093
|
||manifest.packagerVersion!==PACKAGER_VERSION||manifest.app?.id!==appId
|
package/src/pwa-worker.mjs
CHANGED
|
@@ -494,13 +494,39 @@ function installPwaWorker(manifest, clientUrl) {
|
|
|
494
494
|
return {location: destination.href, resource: cacheUrl(selected)};
|
|
495
495
|
}
|
|
496
496
|
|
|
497
|
+
function staticNavigationResource(url) {
|
|
498
|
+
const document = new URL(url);
|
|
499
|
+
if (document.origin !== scopeOrigin || !document.search
|
|
500
|
+
|| !/\.html?$/iu.test(document.pathname)) {
|
|
501
|
+
return null;
|
|
502
|
+
}
|
|
503
|
+
document.search = '';
|
|
504
|
+
return ownedUrls.has(document.href) ? document.href : null;
|
|
505
|
+
}
|
|
506
|
+
|
|
497
507
|
async function requestedResource(request, url) {
|
|
498
508
|
await restored;
|
|
499
|
-
const
|
|
509
|
+
const selectedQuery = ownedUrls.has(url) && new URL(url).search;
|
|
510
|
+
const redirect = request.mode === 'navigate' && !selectedQuery ? navigationRedirect(url) : null;
|
|
500
511
|
if (redirect && cacheUrl(redirect.location) !== url && ownedUrls.has(redirect.resource)) {
|
|
501
512
|
return {response: Response.redirect(redirect.location, 302), done: Promise.resolve(null)};
|
|
502
513
|
}
|
|
503
514
|
if (!ownedUrls.has(url)) {
|
|
515
|
+
const document = request.mode === 'navigate' ? staticNavigationResource(url) : null;
|
|
516
|
+
if (document) {
|
|
517
|
+
// Reuse only the selected static document body. Keep the navigation URL
|
|
518
|
+
// intact; a query-specific response must never replace this shared entry.
|
|
519
|
+
const cached = await findCachedResource(document);
|
|
520
|
+
if (cached.response) {
|
|
521
|
+
return {
|
|
522
|
+
response: cached.response,
|
|
523
|
+
done: cached.current ? Promise.resolve(null)
|
|
524
|
+
: cached.cache.put(document, cached.response.clone()).then(function retainedDocument() {
|
|
525
|
+
return null;
|
|
526
|
+
})
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
}
|
|
504
530
|
return {response: await fetch(request), done: Promise.resolve(null)};
|
|
505
531
|
}
|
|
506
532
|
const cache = await caches.open(cacheName);
|
|
@@ -522,7 +548,7 @@ function installPwaWorker(manifest, clientUrl) {
|
|
|
522
548
|
return;
|
|
523
549
|
}
|
|
524
550
|
if (manifestRestored && !ownedUrls.has(url)
|
|
525
|
-
&& !(request.mode === 'navigate' && navigationRedirect(url))) {
|
|
551
|
+
&& !(request.mode === 'navigate' && (navigationRedirect(url) || staticNavigationResource(url)))) {
|
|
526
552
|
return;
|
|
527
553
|
}
|
|
528
554
|
const resource = requestedResource(request, url);
|
package/src/targets/index.mjs
CHANGED
|
@@ -148,9 +148,8 @@ const browserAdapter={
|
|
|
148
148
|
throwIfAborted(signal);
|
|
149
149
|
return {target:'browser',release:await verifyApp({workspaceRoot,appId,signal,onEvent})};
|
|
150
150
|
},
|
|
151
|
-
async run({workspaceRoot,appId,host='127.0.0.1',port=0,httpPort=0,tls,certPath,keyPath,signal,onEvent}={}){
|
|
151
|
+
async run({workspaceRoot,appId,releaseRoot=path.join(workspaceRoot,'dist',appId),host='127.0.0.1',port=0,httpPort=0,tls,certPath,keyPath,signal,onEvent}={}){
|
|
152
152
|
throwIfAborted(signal);
|
|
153
|
-
const releaseRoot=path.join(workspaceRoot,'dist',appId);
|
|
154
153
|
const server=await startDevServer({
|
|
155
154
|
workspaceRoot,
|
|
156
155
|
appId,
|
package/src/toolchain.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import Is from 'strong-type';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
+
import {resolvePackageOutputRoot} from './app-layout.mjs';
|
|
3
4
|
import {readdir,lstat,realpath,mkdir,readFile,writeFile} from 'node:fs/promises';
|
|
4
5
|
import {createWorkspace,initWorkspace} from './scaffold.mjs';
|
|
5
6
|
import {
|
|
@@ -10,7 +11,7 @@ import {
|
|
|
10
11
|
validateWorkspace
|
|
11
12
|
} from './workspace.mjs';
|
|
12
13
|
import {loadArcaneIntegratedProvider} from './integrated-provider-loader.mjs';
|
|
13
|
-
import {startDevServer} from './dev-server.mjs';
|
|
14
|
+
import {collectSourcePwaAssets,startDevServer} from './dev-server.mjs';
|
|
14
15
|
import {applyPwaEntryReferences,generateImportMap,readApplicationTestImportMapContext} from './import-map.mjs';
|
|
15
16
|
import {createPwaArtifacts} from './pwa.mjs';
|
|
16
17
|
import {readInstalledSdkLayout} from './sdk-runtime-layout.mjs';
|
|
@@ -336,11 +337,21 @@ async function refreshRootApplicationFiles(prepared,inspected,importMap,{signal,
|
|
|
336
337
|
...inspected.files.filter(file=>file!=='index.html'||manifest.include.includes('index.html')),
|
|
337
338
|
importMap.artifactRelativePath
|
|
338
339
|
])];
|
|
340
|
+
const sourceOrigin='http://arcane.invalid';
|
|
341
|
+
const selectedPaths=new Set(files.map(function selectedStaticPath(file){
|
|
342
|
+
return new URL(`/${file.split('/').map(encodeURIComponent).join('/')}`,sourceOrigin).pathname;
|
|
343
|
+
}));
|
|
344
|
+
const assets=installed?.direct&&manifest.pwa?.enabled
|
|
345
|
+
?(await collectSourcePwaAssets({workspaceRoot,appId,signal,onEvent})).filter(function selectedStaticVariant(value){
|
|
346
|
+
const url=new URL(value,sourceOrigin);
|
|
347
|
+
return url.origin===sourceOrigin&&selectedPaths.has(url.pathname);
|
|
348
|
+
}):[];
|
|
339
349
|
const pwa=installed?.direct&&manifest.pwa?.enabled?createPwaArtifacts({
|
|
340
350
|
app:{id:appId,displayName:manifest.displayName,version:manifest.version,entry},
|
|
341
351
|
sdkVersion:installed.version,
|
|
342
352
|
pwa:manifest.pwa,
|
|
343
353
|
files,
|
|
354
|
+
assets,
|
|
344
355
|
basePath:'/',
|
|
345
356
|
appBase:'/',
|
|
346
357
|
installationId:`/apps/${appId}/`,
|
|
@@ -1022,6 +1033,20 @@ async function executePairedNativeBuild(options,adapter,{
|
|
|
1022
1033
|
}
|
|
1023
1034
|
const dependencyApps=await nativeDependencyClosure(initialPrepared,{target,signal,onEvent});
|
|
1024
1035
|
const prepared=initialPrepared;
|
|
1036
|
+
const packageOutputs=[];
|
|
1037
|
+
for(const app of [prepared.validation.app,...dependencyApps]){
|
|
1038
|
+
const releaseRoot=resolvePackageOutputRoot(
|
|
1039
|
+
prepared.workspaceRoot,prepared.validation.config,app.manifest
|
|
1040
|
+
);
|
|
1041
|
+
if(pathsOverlap(releaseRoot,outputRoot)
|
|
1042
|
+
||packageOutputs.some(existing=>pathsOverlap(existing,releaseRoot))){
|
|
1043
|
+
throw new ArcaneError(
|
|
1044
|
+
'ARCANE_PACKAGE_INVALID',
|
|
1045
|
+
`Package output for ${app.appId} overlaps another selected package or native output directory.`
|
|
1046
|
+
);
|
|
1047
|
+
}
|
|
1048
|
+
packageOutputs.push(releaseRoot);
|
|
1049
|
+
}
|
|
1025
1050
|
const selectedRelease=await packageNativeRelease(prepared,{
|
|
1026
1051
|
appId:prepared.appId,
|
|
1027
1052
|
appRoot:prepared.appRoot,
|
|
@@ -1180,6 +1205,9 @@ export async function runApplication(options={}){
|
|
|
1180
1205
|
return runTarget({
|
|
1181
1206
|
...selectedOptions,
|
|
1182
1207
|
...prepared,
|
|
1208
|
+
releaseRoot:resolvePackageOutputRoot(
|
|
1209
|
+
prepared.workspaceRoot,prepared.validation.config,prepared.validation.app.manifest
|
|
1210
|
+
),
|
|
1183
1211
|
target
|
|
1184
1212
|
});
|
|
1185
1213
|
}
|