arcane-os 0.51.1 → 0.52.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 +26 -0
- package/README.md +81 -44
- package/browser-runtime/ai/twin-cloud.mjs +4 -0
- package/docs/architecture.md +33 -17
- package/docs/platform-targets.md +15 -2
- package/docs/reference/README.md +31 -26
- package/docs/reference/ai/browser-speech.md +106 -31
- package/docs/reference/ai/browser-wasm.md +27 -24
- package/docs/reference/ai/twin-cloud.md +30 -11
- package/docs/reference/asset-versioning.md +34 -37
- package/docs/reference/availability-and-normalization.md +31 -20
- package/docs/reference/cli.md +45 -30
- package/docs/reference/inventory/package-api.json +34 -6
- package/docs/reference/inventory/runtime-components.json +25 -11
- package/docs/reference/inventory/runtime-entities.json +6 -3
- package/docs/reference/inventory/runtime-modules.json +63 -8
- package/docs/reference/protocols.md +110 -66
- package/docs/reference/pwa.md +49 -19
- package/docs/reference/runtime-components.md +9 -2
- package/docs/reference/runtime-entities.md +14 -2
- package/docs/reference/runtime-modules.md +59 -2
- package/docs/reference/sdk-api.md +247 -72
- package/docs/roadmap.md +32 -26
- package/package.json +1 -1
- package/runtime/arcane/modules/AI.js +20 -6
- package/schemas/arcane-app.schema.json +5 -0
- package/schemas/arcane-package.schema.json +10 -0
- package/src/app-descriptor.mjs +6 -1
- package/src/dev-server.mjs +140 -22
- package/src/packager/core.mjs +43 -6
- package/src/templates/workspace-template.mjs +14 -4
- package/src/toolchain.mjs +1 -0
package/docs/roadmap.md
CHANGED
|
@@ -1,22 +1,29 @@
|
|
|
1
1
|
# Development roadmap
|
|
2
2
|
|
|
3
|
-
The SDK
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
The SDK already provides root application scaffolding, direct npm browser
|
|
4
|
+
routes, managed import maps, live source serving, browser/PWA packaging and
|
|
5
|
+
explicit native provider pairing. This roadmap distinguishes those implemented
|
|
6
|
+
contracts from remaining development work. Future items do not select an
|
|
7
|
+
implementation, dependency, publication or deployment.
|
|
7
8
|
|
|
8
|
-
##
|
|
9
|
+
## Current delivery baseline
|
|
9
10
|
|
|
10
|
-
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
11
|
+
- SDK publication uses the existing selected-package workflow. `dev` and
|
|
12
|
+
`latest` are npm channels; `main` remains the canonical source branch. See
|
|
13
|
+
[publishing](publishing.md) for the selected-artifact procedure.
|
|
14
|
+
- License terms and commercial licensing information are already documented in
|
|
15
|
+
[LICENSE](../LICENSE), [COMMERCIAL-LICENSE.md](../COMMERCIAL-LICENSE.md) and
|
|
16
|
+
[NOTICE](../NOTICE).
|
|
17
|
+
- Standalone apps use their repository root and installed npm resources.
|
|
18
|
+
Explicit multi-app and integrated workspaces retain their selected layout.
|
|
19
|
+
- The browser path includes PWA installation, offline resource selection,
|
|
20
|
+
conditional refresh and the shared update prompt. Development and packaged
|
|
21
|
+
preview serving share the published `node-http-server` MIME map.
|
|
22
|
+
- The reusable application release workflow remains unavailable until its
|
|
23
|
+
checked-in implementation matches the documented package-and-upload contract.
|
|
24
|
+
This is separate from SDK npm publication.
|
|
18
25
|
|
|
19
|
-
##
|
|
26
|
+
## Native provider continuity
|
|
20
27
|
|
|
21
28
|
- Keep the implemented process-local `arcane-native-build-plan/1` and
|
|
22
29
|
`arcane-native-builder/1` lifecycle as the single provider seam for the CLI,
|
|
@@ -31,16 +38,11 @@ one unbounded build.
|
|
|
31
38
|
- Preserve the implemented portable, Windows x64, Linux x64, Linux ARM64, and
|
|
32
39
|
Android ARM64 providers as one selected app plus its exact declared dependency
|
|
33
40
|
closure. Add new providers without weakening that release-reader boundary.
|
|
34
|
-
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
- Preserve
|
|
38
|
-
|
|
39
|
-
manifest file as a reusable receipt.
|
|
40
|
-
- Add a manifest-declared origin policy for native enforcement and an optional
|
|
41
|
-
policy-faithful development mode. The current capability-gated loopback host
|
|
42
|
-
deliberately uses a broad development-only policy so the shared runtime can
|
|
43
|
-
exercise remote providers, media, WebSockets, and embeds.
|
|
41
|
+
- Keep native build and launch within the paired provider's process lifecycle.
|
|
42
|
+
Persistent sessions or restartable host integration require a separately
|
|
43
|
+
selected capability and its owning host implementation.
|
|
44
|
+
- Preserve complete selected app releases through planning, build and run.
|
|
45
|
+
Explicit verification remains separate from the ordinary build/run lifecycle.
|
|
44
46
|
|
|
45
47
|
## Platform adapters
|
|
46
48
|
|
|
@@ -62,6 +64,9 @@ one unbounded build.
|
|
|
62
64
|
reproducible. The APK contains no native ABI and is architecture-neutral. Add
|
|
63
65
|
AAB, release signing, store publishing, and update continuity only as explicit
|
|
64
66
|
later promotion work.
|
|
67
|
+
- macOS: the current registry provides browser delivery and no native package
|
|
68
|
+
adapter. A native adapter remains additional platform work; shared portable
|
|
69
|
+
SDK behavior must keep its declared macOS support.
|
|
65
70
|
|
|
66
71
|
## Developer experience
|
|
67
72
|
|
|
@@ -70,8 +75,9 @@ one unbounded build.
|
|
|
70
75
|
development check, never app discovery or an arbitrary package-script loop.
|
|
71
76
|
- Add the Arcane Developer control panel as a client of the same operation API;
|
|
72
77
|
it stores local repository paths per user and never becomes a second builder.
|
|
73
|
-
- Add
|
|
74
|
-
|
|
78
|
+
- Add an Arcane-owned source-development native wrapper around the same live
|
|
79
|
+
browser surface when that capability is selected. The current native `run`
|
|
80
|
+
command packages and builds its selected app; it is a different lifecycle.
|
|
75
81
|
- Preserve the implemented integrated app-scoped native path: `--workspace` and
|
|
76
82
|
`--arcane-root` identify the same checkout, one app and one target are
|
|
77
83
|
selected, and `--output-root` remains outside the checkout. Future GUI clients
|
package/package.json
CHANGED
|
@@ -4448,7 +4448,8 @@ class AI {
|
|
|
4448
4448
|
signal,
|
|
4449
4449
|
payload.reasoningEffort,
|
|
4450
4450
|
onRetry,
|
|
4451
|
-
payload.model
|
|
4451
|
+
payload.model,
|
|
4452
|
+
payload.temperature
|
|
4452
4453
|
);
|
|
4453
4454
|
}
|
|
4454
4455
|
|
|
@@ -4480,7 +4481,8 @@ class AI {
|
|
|
4480
4481
|
payload.reasoningEffort,
|
|
4481
4482
|
observeToolText,
|
|
4482
4483
|
onRetry,
|
|
4483
|
-
payload.model
|
|
4484
|
+
payload.model,
|
|
4485
|
+
payload.temperature
|
|
4484
4486
|
);
|
|
4485
4487
|
}
|
|
4486
4488
|
|
|
@@ -4810,7 +4812,8 @@ class AI {
|
|
|
4810
4812
|
normalizedReasoningEffort,
|
|
4811
4813
|
observeToolText,
|
|
4812
4814
|
onRetry,
|
|
4813
|
-
model
|
|
4815
|
+
model,
|
|
4816
|
+
temperature
|
|
4814
4817
|
);
|
|
4815
4818
|
const structuralToolCalls=normalizeAICompletionToolCalls(
|
|
4816
4819
|
completion
|
|
@@ -4920,7 +4923,8 @@ class AI {
|
|
|
4920
4923
|
reasoningEffort,
|
|
4921
4924
|
observeToolText=null,
|
|
4922
4925
|
onRetry = null,
|
|
4923
|
-
model=this.model
|
|
4926
|
+
model=this.model,
|
|
4927
|
+
temperature
|
|
4924
4928
|
){
|
|
4925
4929
|
let speechTurnCompleted=false;
|
|
4926
4930
|
|
|
@@ -4949,6 +4953,10 @@ class AI {
|
|
|
4949
4953
|
stream:true
|
|
4950
4954
|
}
|
|
4951
4955
|
|
|
4956
|
+
if(this.llmService==='TWIN' && temperature!==undefined){
|
|
4957
|
+
request.temperature=temperature;
|
|
4958
|
+
}
|
|
4959
|
+
|
|
4952
4960
|
if(structuredOutputFormat){
|
|
4953
4961
|
request.response_format=openAIResponseFormat(
|
|
4954
4962
|
structuredOutputFormat
|
|
@@ -5740,7 +5748,8 @@ class AI {
|
|
|
5740
5748
|
signal,
|
|
5741
5749
|
normalizedReasoningEffort,
|
|
5742
5750
|
onRetry,
|
|
5743
|
-
model
|
|
5751
|
+
model,
|
|
5752
|
+
temperature
|
|
5744
5753
|
);
|
|
5745
5754
|
}
|
|
5746
5755
|
|
|
@@ -5795,7 +5804,8 @@ class AI {
|
|
|
5795
5804
|
signal=null,
|
|
5796
5805
|
reasoningEffort,
|
|
5797
5806
|
onRetry = null,
|
|
5798
|
-
model=this.model
|
|
5807
|
+
model=this.model,
|
|
5808
|
+
temperature
|
|
5799
5809
|
){
|
|
5800
5810
|
validateAIStructuralRequest(messages,tools,parallel_tool_calls);
|
|
5801
5811
|
this.#assertServiceConfigured(this.llmService);
|
|
@@ -5819,6 +5829,10 @@ class AI {
|
|
|
5819
5829
|
stream:false
|
|
5820
5830
|
}
|
|
5821
5831
|
|
|
5832
|
+
if(this.llmService==='TWIN' && temperature!==undefined){
|
|
5833
|
+
request.temperature=temperature;
|
|
5834
|
+
}
|
|
5835
|
+
|
|
5822
5836
|
if(structuredOutputFormat){
|
|
5823
5837
|
request.response_format=openAIResponseFormat(
|
|
5824
5838
|
structuredOutputFormat
|
|
@@ -38,6 +38,11 @@
|
|
|
38
38
|
"required": ["entry", "strategy", "include", "exclude", "shared"],
|
|
39
39
|
"properties": {
|
|
40
40
|
"entry": { "$ref": "#/$defs/relativePath" },
|
|
41
|
+
"documents": {
|
|
42
|
+
"type": "array",
|
|
43
|
+
"description": "Application-relative HTML/HTM pages managed by import-map and PWA processing. Entry is always first; an empty list selects entry only. Omission preserves automatic discovery. File inclusion and serving remain controlled by include/exclude.",
|
|
44
|
+
"items": { "allOf": [{ "$ref": "#/$defs/relativePath" }, { "pattern": "\\.[Hh][Tt][Mm][Ll]?$" }] }
|
|
45
|
+
},
|
|
41
46
|
"strategy": { "enum": ["static", "adapter"] },
|
|
42
47
|
"localAIModelPolicy": { "$ref": "#/$defs/localAIModelPolicy" },
|
|
43
48
|
"pwa": { "$ref": "#/$defs/pwa" },
|
|
@@ -34,6 +34,16 @@
|
|
|
34
34
|
"entry": {
|
|
35
35
|
"$ref": "#/$defs/relativePath"
|
|
36
36
|
},
|
|
37
|
+
"documents": {
|
|
38
|
+
"type": "array",
|
|
39
|
+
"description": "Application-relative HTML/HTM pages managed by import-map and PWA processing. Entry is always first; an empty list selects entry only. Omission preserves automatic discovery. File inclusion and serving remain controlled by include/exclude.",
|
|
40
|
+
"items": {
|
|
41
|
+
"allOf": [
|
|
42
|
+
{ "$ref": "#/$defs/relativePath" },
|
|
43
|
+
{ "pattern": "\\.[Hh][Tt][Mm][Ll]?$" }
|
|
44
|
+
]
|
|
45
|
+
}
|
|
46
|
+
},
|
|
37
47
|
"outputDirectory": {
|
|
38
48
|
"$ref": "#/$defs/relativePath",
|
|
39
49
|
"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."
|
package/src/app-descriptor.mjs
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
} from './constants.mjs';
|
|
13
13
|
import {
|
|
14
14
|
normalizeRelativePath,
|
|
15
|
+
normalizeAppDocuments,
|
|
15
16
|
parseSemver,
|
|
16
17
|
validateAppConfig as validateAppPackageConfig
|
|
17
18
|
} from './packager/core.mjs';
|
|
@@ -110,9 +111,10 @@ function validateLocalAIModelPolicy(value,label){
|
|
|
110
111
|
|
|
111
112
|
function validatePackage(value,appId){
|
|
112
113
|
assertOnlyKeys(value,new Set([
|
|
113
|
-
'entry','strategy','include','exclude','shared','adapter','localAIModelPolicy','pwa'
|
|
114
|
+
'entry','strategy','include','exclude','shared','adapter','localAIModelPolicy','pwa','documents'
|
|
114
115
|
]),'descriptor.package');
|
|
115
116
|
const entry=normalizeRelativePath(value.entry,'descriptor.package.entry');
|
|
117
|
+
const documents=normalizeAppDocuments(value.documents,'descriptor.package.documents');
|
|
116
118
|
if(!['static','adapter'].includes(value.strategy))fail('descriptor.package.strategy must be static or adapter.');
|
|
117
119
|
const include=relativePaths(value.include,'descriptor.package.include',{required:true});
|
|
118
120
|
const exclude=relativePaths(value.exclude??[],'descriptor.package.exclude');
|
|
@@ -134,6 +136,7 @@ function validatePackage(value,appId){
|
|
|
134
136
|
}
|
|
135
137
|
return completeValue({
|
|
136
138
|
entry,
|
|
139
|
+
...(documents===undefined?{}:{documents}),
|
|
137
140
|
strategy:value.strategy,
|
|
138
141
|
include,
|
|
139
142
|
exclude,
|
|
@@ -317,6 +320,7 @@ export function projectPackageManifest(descriptor){
|
|
|
317
320
|
displayName:value.displayName,
|
|
318
321
|
version:value.version,
|
|
319
322
|
entry:value.package.entry,
|
|
323
|
+
...(value.package.documents===undefined?{}:{documents:[...value.package.documents]}),
|
|
320
324
|
strategy:value.package.strategy,
|
|
321
325
|
...(hasAuthoredSecurity?{security:{
|
|
322
326
|
connectOrigins:[...value.security.connectOrigins],
|
|
@@ -381,6 +385,7 @@ function synthesizedDescriptor(packageManifest,nativeDescriptor){
|
|
|
381
385
|
publisher:{id:'publisher-undeclared',name:'Publisher not declared'},
|
|
382
386
|
package:{
|
|
383
387
|
entry:packageManifest.entry,
|
|
388
|
+
...(packageManifest.documents===undefined?{}:{documents:[...packageManifest.documents]}),
|
|
384
389
|
strategy:packageManifest.strategy,
|
|
385
390
|
...(packageManifest.localAIModelPolicy?{localAIModelPolicy:packageManifest.localAIModelPolicy}:{}),
|
|
386
391
|
...(packageManifest.pwa===undefined?{}:{pwa:packageManifest.pwa}),
|
package/src/dev-server.mjs
CHANGED
|
@@ -324,7 +324,8 @@ async function serveGeneratedRepresentation(fileServer, request, response, {
|
|
|
324
324
|
}
|
|
325
325
|
|
|
326
326
|
async function serveSourceFile(fileServer, request, response, opened, {
|
|
327
|
-
assetVersion, revalidate = false, onReference, pwaEntry = false, lastModified
|
|
327
|
+
assetVersion, revalidate = false, onReference, pwaEntry = false, lastModified,
|
|
328
|
+
representationTracked = false
|
|
328
329
|
} = {}) {
|
|
329
330
|
const extension = path.extname(opened.candidate).toLowerCase();
|
|
330
331
|
const rewrite = assetVersion !== undefined && /\.(?:m?js|html?|css|json)$/iu.test(extension);
|
|
@@ -336,9 +337,27 @@ async function serveSourceFile(fileServer, request, response, opened, {
|
|
|
336
337
|
response.writeHead = function writeDevelopmentHeaders(...arguments_) {
|
|
337
338
|
this.removeHeader('ETag');
|
|
338
339
|
this.removeHeader('X-Content-Type-Options');
|
|
340
|
+
if (representationTracked) {
|
|
341
|
+
if (lastModified) this.setHeader('Last-Modified', lastModified.toUTCString());
|
|
342
|
+
else this.removeHeader('Last-Modified');
|
|
343
|
+
}
|
|
339
344
|
return writeHead.apply(this, arguments_);
|
|
340
345
|
};
|
|
341
|
-
|
|
346
|
+
const modifiedSince = request.headers['if-modified-since'];
|
|
347
|
+
const range = request.headers.range;
|
|
348
|
+
if (representationTracked) {
|
|
349
|
+
const currentTime = lastModified ? Date.parse(lastModified.toUTCString()) : NaN;
|
|
350
|
+
if (!(Date.parse(modifiedSince) >= currentTime)) delete request.headers['if-modified-since'];
|
|
351
|
+
if (request.headers['if-range'] && !(Date.parse(request.headers['if-range']) >= currentTime)) {
|
|
352
|
+
delete request.headers.range;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
try {
|
|
356
|
+
await fileServer.serveFile(opened.candidate, request, response);
|
|
357
|
+
} finally {
|
|
358
|
+
if (modifiedSince !== undefined) request.headers['if-modified-since'] = modifiedSince;
|
|
359
|
+
if (range !== undefined) request.headers.range = range;
|
|
360
|
+
}
|
|
342
361
|
return;
|
|
343
362
|
}
|
|
344
363
|
await serveGeneratedRepresentation(
|
|
@@ -514,6 +533,10 @@ function applicationSourcePath(config,appId,relative='') {
|
|
|
514
533
|
return `/${root?`${root}/`:''}${relative}`;
|
|
515
534
|
}
|
|
516
535
|
|
|
536
|
+
function selectedApplicationDocument(app,relative){
|
|
537
|
+
return app.documents===undefined||relative===app.entry||app.documents.includes(relative);
|
|
538
|
+
}
|
|
539
|
+
|
|
517
540
|
async function packagedRoutes(releaseRoot){
|
|
518
541
|
if(!is.string(releaseRoot)||!releaseRoot.trim())fail('releaseRoot is required in packaged mode.','ARCANE_USAGE');
|
|
519
542
|
const requested=path.resolve(releaseRoot);
|
|
@@ -548,7 +571,7 @@ async function packagedRoutes(releaseRoot){
|
|
|
548
571
|
};
|
|
549
572
|
}
|
|
550
573
|
|
|
551
|
-
async function sourcePwaAssets(routeSet, mappings, signal, resourceUrls, resourcePaths) {
|
|
574
|
+
async function sourcePwaAssets(routeSet, mappings, signal, resourceUrls, resourcePaths, runtimeDocuments = new Set()) {
|
|
552
575
|
const records = new Map();
|
|
553
576
|
const resources = new Map();
|
|
554
577
|
for (const mapping of mappings) {
|
|
@@ -590,7 +613,7 @@ async function sourcePwaAssets(routeSet, mappings, signal, resourceUrls, resourc
|
|
|
590
613
|
const pending = [{url: entryUrl, documentUrl: entryUrl}];
|
|
591
614
|
const visited = new Set();
|
|
592
615
|
const referencesByPath = new Map();
|
|
593
|
-
for (const selected of routeSet.app.include) {
|
|
616
|
+
for (const selected of routeSet.app.documents ?? routeSet.app.include) {
|
|
594
617
|
if (!/\.html?$/iu.test(selected)) continue;
|
|
595
618
|
const url = new URL(
|
|
596
619
|
applicationSourcePath(routeSet.config,routeSet.appId,selected.split('/').map(encodeURIComponent).join('/')),
|
|
@@ -609,6 +632,9 @@ async function sourcePwaAssets(routeSet, mappings, signal, resourceUrls, resourc
|
|
|
609
632
|
if (!/\.(?:m?js|html?|css)$/iu.test(pathname) && !managedMap) continue;
|
|
610
633
|
visited.add(context);
|
|
611
634
|
resourcePaths.add(decodeURIComponent(pathname));
|
|
635
|
+
if (record.mapping.kind !== 'app' && /\.html?$/iu.test(pathname)) {
|
|
636
|
+
runtimeDocuments.add(decodeURIComponent(pathname));
|
|
637
|
+
}
|
|
612
638
|
let references = referencesByPath.get(pathname);
|
|
613
639
|
if (!references) {
|
|
614
640
|
const opened = await openSafeFile(record.mapping.root, record.relative);
|
|
@@ -654,6 +680,9 @@ async function sourcePwaAssets(routeSet, mappings, signal, resourceUrls, resourc
|
|
|
654
680
|
}
|
|
655
681
|
if (target.origin !== origin || !resources.has(target.pathname)) continue;
|
|
656
682
|
resourceUrls.add(`${target.pathname}${target.search}`);
|
|
683
|
+
const targetRecord = resources.get(target.pathname);
|
|
684
|
+
if (kind === 'document' && targetRecord.mapping.kind === 'app'
|
|
685
|
+
&& !selectedApplicationDocument(routeSet.app, targetRecord.relative.join('/'))) continue;
|
|
657
686
|
const traversable = kind !== 'fetch'
|
|
658
687
|
&& (kind !== 'asset' || /\.css(?:[?#]|$)/iu.test(url));
|
|
659
688
|
if (traversable) {
|
|
@@ -942,21 +971,33 @@ async function startOwnedDevServer({
|
|
|
942
971
|
);
|
|
943
972
|
return managed;
|
|
944
973
|
}
|
|
945
|
-
function
|
|
974
|
+
function sourceRepresentation(opened, version, pwaEntry, rewritten) {
|
|
946
975
|
const modifiedAt = opened.modifiedAt.getTime();
|
|
947
976
|
const previous = representationMetadata.get(opened.candidate);
|
|
948
977
|
if (previous?.modifiedAt === modifiedAt
|
|
949
|
-
&& previous.version === version && previous.pwaEntry === pwaEntry
|
|
950
|
-
|
|
978
|
+
&& previous.version === version && previous.pwaEntry === pwaEntry
|
|
979
|
+
&& previous.rewritten === rewritten) {
|
|
980
|
+
if (!previous.lastModified && Date.now() >= previous.ambiguousThrough + 1000) {
|
|
981
|
+
previous.lastModified = new Date();
|
|
982
|
+
}
|
|
983
|
+
return previous;
|
|
951
984
|
}
|
|
952
985
|
const lastModified = new Date(
|
|
953
986
|
Math.max(modifiedAt, generatorModifiedAt, assetVersionModifiedAt, previous ? Date.now() : 0)
|
|
954
987
|
);
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
988
|
+
const previousTime = previous?.lastModified
|
|
989
|
+
? Date.parse(previous.lastModified.toUTCString()) : previous?.ambiguousThrough;
|
|
990
|
+
// HTTP dates cannot distinguish two representations within one second.
|
|
991
|
+
// Omit that ambiguous validator until a later request can date it honestly.
|
|
992
|
+
const ambiguous = previousTime !== undefined
|
|
993
|
+
&& Date.parse(lastModified.toUTCString()) <= previousTime;
|
|
994
|
+
const record = {
|
|
995
|
+
modifiedAt, version, pwaEntry, rewritten,
|
|
996
|
+
lastModified: ambiguous ? undefined : lastModified,
|
|
997
|
+
ambiguousThrough: ambiguous ? previousTime : undefined
|
|
998
|
+
};
|
|
999
|
+
representationMetadata.set(opened.candidate, record);
|
|
1000
|
+
return record;
|
|
960
1001
|
}
|
|
961
1002
|
for(const mapping of mappings){
|
|
962
1003
|
const info=await lstat(mapping.root);
|
|
@@ -1089,7 +1130,7 @@ async function startOwnedDevServer({
|
|
|
1089
1130
|
const generatedAt = new Date();
|
|
1090
1131
|
state.inventoryTask = Promise.all(
|
|
1091
1132
|
[
|
|
1092
|
-
sourcePwaAssets(selectedRoutes, selectedRoutes.mappings, signal, pwaResourceUrls, resourcePaths),
|
|
1133
|
+
sourcePwaAssets(selectedRoutes, selectedRoutes.mappings, signal, pwaResourceUrls, resourcePaths, runtimeDocuments),
|
|
1093
1134
|
selectedAssetVersion().then(
|
|
1094
1135
|
function rememberCurrentInventoryVersion(version) {
|
|
1095
1136
|
if (selectedRoutes === currentSourceRoutes) rememberAssetVersion(version);
|
|
@@ -1124,6 +1165,66 @@ async function startOwnedDevServer({
|
|
|
1124
1165
|
// Remember actual resource edges as their owners are served, not every
|
|
1125
1166
|
// HTML/JS/CSS file in an application's document or attachment corpus.
|
|
1126
1167
|
const resourcePaths=new Set([routeSet.startPath]);
|
|
1168
|
+
const runtimeDocuments=new Set();
|
|
1169
|
+
const componentReferences=new Map();
|
|
1170
|
+
async function isCurrentComponent(targetPath, selectedRoutes) {
|
|
1171
|
+
// Retain parsed edges, not lifetime classifications. Only current page
|
|
1172
|
+
// roots and observed runtime documents can make an original a component.
|
|
1173
|
+
const pending = [selectedRoutes.startPath, ...runtimeDocuments];
|
|
1174
|
+
for (const document of selectedRoutes.app.documents) {
|
|
1175
|
+
pending.push(applicationSourcePath(selectedRoutes.config, selectedRoutes.appId, document));
|
|
1176
|
+
}
|
|
1177
|
+
const visited = new Set();
|
|
1178
|
+
for (const ownerPath of pending) {
|
|
1179
|
+
throwIfAborted(signal);
|
|
1180
|
+
if (visited.has(ownerPath)) continue;
|
|
1181
|
+
visited.add(ownerPath);
|
|
1182
|
+
const target = parseRequestTarget(ownerPath.split('/').map(encodeURIComponent).join('/'));
|
|
1183
|
+
if (!target) continue;
|
|
1184
|
+
const mapping = selectedRoutes.mappings.find(function componentOwnerMapping(route) {
|
|
1185
|
+
return route.prefix.every(function componentOwnerSegment(segment, index) {
|
|
1186
|
+
return target.segments[index] === segment;
|
|
1187
|
+
});
|
|
1188
|
+
});
|
|
1189
|
+
if (!mapping) continue;
|
|
1190
|
+
const relative = target.segments.slice(mapping.prefix.length);
|
|
1191
|
+
if (!relative.length || (mapping.allow && !mapping.allow(relative))) continue;
|
|
1192
|
+
if (!/\.html?$/iu.test(ownerPath)) continue;
|
|
1193
|
+
const opened = await openSafeFile(mapping.root, relative, {readContent: false});
|
|
1194
|
+
if (!opened) continue;
|
|
1195
|
+
const modifiedAt = opened.modifiedAt.getTime();
|
|
1196
|
+
let record = componentReferences.get(opened.candidate);
|
|
1197
|
+
if (record?.modifiedAt !== modifiedAt) {
|
|
1198
|
+
const references = [];
|
|
1199
|
+
rewriteAssetReferences(await readFile(opened.candidate, 'utf8'), {
|
|
1200
|
+
filePath: opened.candidate,
|
|
1201
|
+
version: null,
|
|
1202
|
+
onReference: function currentComponentReference(reference) {
|
|
1203
|
+
if (reference.kind === 'component') references.push(reference);
|
|
1204
|
+
}
|
|
1205
|
+
});
|
|
1206
|
+
record = {modifiedAt, references};
|
|
1207
|
+
componentReferences.set(opened.candidate, record);
|
|
1208
|
+
}
|
|
1209
|
+
const ownerUrl = new URL(target.pathname, 'http://arcane.invalid');
|
|
1210
|
+
for (const reference of record.references) {
|
|
1211
|
+
let component;
|
|
1212
|
+
try {
|
|
1213
|
+
component = resolveAssetReference(reference, {ownerUrl});
|
|
1214
|
+
} catch {
|
|
1215
|
+
// Non-URL values retain their authored behavior.
|
|
1216
|
+
continue;
|
|
1217
|
+
}
|
|
1218
|
+
if (component.origin !== ownerUrl.origin) continue;
|
|
1219
|
+
const componentTarget = parseRequestTarget(component.pathname);
|
|
1220
|
+
if (!componentTarget) continue;
|
|
1221
|
+
const componentPath = componentTarget.path;
|
|
1222
|
+
if (componentPath === targetPath) return true;
|
|
1223
|
+
pending.push(componentPath);
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
return false;
|
|
1227
|
+
}
|
|
1127
1228
|
const requestTasks=new Set();
|
|
1128
1229
|
const runFileWork=createFileWorkLimiter();
|
|
1129
1230
|
async function serveDevelopmentRequest(request, response) {
|
|
@@ -1208,21 +1309,31 @@ async function startOwnedDevServer({
|
|
|
1208
1309
|
}
|
|
1209
1310
|
const extension=path.extname(opened.candidate).toLowerCase();
|
|
1210
1311
|
const html=extension==='.html'||extension==='.htm';
|
|
1312
|
+
const explicitDocuments = mode === 'source' && mapping.kind === 'app'
|
|
1313
|
+
&& selectedRoutes.app.documents !== undefined;
|
|
1314
|
+
const explicitDocument = explicitDocuments && html
|
|
1315
|
+
&& selectedApplicationDocument(selectedRoutes.app, relative.join('/'));
|
|
1211
1316
|
const selectedPwaDocument = mode === 'source' && pwaEnabled && html
|
|
1212
1317
|
&& mapping.kind === 'app'
|
|
1213
|
-
&& selectedRoutes.app.include.includes(relative.join('/'));
|
|
1214
|
-
const selectedDocument = target.path === selectedRoutes.startPath || selectedPwaDocument;
|
|
1215
|
-
const managedDocument = !selectedDocument && html && await isManagedDocument(opened);
|
|
1318
|
+
&& !explicitDocuments && selectedRoutes.app.include.includes(relative.join('/'));
|
|
1319
|
+
const selectedDocument = target.path === selectedRoutes.startPath || explicitDocument || selectedPwaDocument;
|
|
1320
|
+
const managedDocument = !selectedDocument && !explicitDocuments && html && await isManagedDocument(opened);
|
|
1216
1321
|
const entryDocument = selectedDocument || managedDocument;
|
|
1217
1322
|
const managedMap=path.basename(opened.candidate)==='arcane.importmap.json';
|
|
1218
1323
|
// A live server can span an SDK upgrade. Refresh the small
|
|
1219
1324
|
// version record on navigation, not on each resource request.
|
|
1220
1325
|
if(entryDocument||managedMap)rememberAssetVersion(await selectedAssetVersion());
|
|
1221
1326
|
const runtimeResource=mapping.kind!=='app';
|
|
1327
|
+
if (runtimeResource && html) runtimeDocuments.add(target.path);
|
|
1222
1328
|
const browserResource=['script','style','worker','sharedworker','serviceworker']
|
|
1223
1329
|
.includes(request.headers['sec-fetch-dest']);
|
|
1224
|
-
|
|
1225
|
-
|
|
1330
|
+
// Selecting application pages does not turn retained originals
|
|
1331
|
+
// into resources. HTML components remain owned by html-import.
|
|
1332
|
+
const componentDocument=explicitDocuments&&html&&!selectedDocument
|
|
1333
|
+
&&await isCurrentComponent(target.path, selectedRoutes);
|
|
1334
|
+
const retainedDocument=explicitDocuments&&html&&!selectedDocument&&!componentDocument;
|
|
1335
|
+
const rewrite=!retainedDocument&&(runtimeResource||entryDocument||browserResource||managedMap
|
|
1336
|
+
||componentDocument||resourcePaths.has(target.path));
|
|
1226
1337
|
const onReference = function observeServedResource(reference) {
|
|
1227
1338
|
const {url,kind,baseKind}=reference;
|
|
1228
1339
|
if (pwaEnabled && kind !== 'fetch'
|
|
@@ -1247,7 +1358,13 @@ async function startOwnedDevServer({
|
|
|
1247
1358
|
const resource=resolveAssetReference(reference,{ownerUrl:documentUrl});
|
|
1248
1359
|
if(resource.origin===documentUrl.origin
|
|
1249
1360
|
&&/\.(?:m?js|html?|css)$/iu.test(resource.pathname)){
|
|
1250
|
-
|
|
1361
|
+
const resourcePath=decodeURIComponent(resource.pathname);
|
|
1362
|
+
const appPrefix=applicationSourcePath(selectedRoutes.config,selectedRoutes.appId);
|
|
1363
|
+
const appRelative=resourcePath.startsWith(appPrefix)?resourcePath.slice(appPrefix.length):null;
|
|
1364
|
+
if(kind==='document'&&mode==='source'&&appRelative!==null
|
|
1365
|
+
&&sourcePathAllowed(appRelative.split('/'),selectedRoutes.app)
|
|
1366
|
+
&&!selectedApplicationDocument(selectedRoutes.app,appRelative))return;
|
|
1367
|
+
resourcePaths.add(resourcePath);
|
|
1251
1368
|
}
|
|
1252
1369
|
}catch{ /* Non-URL values remain under their existing owner. */ }
|
|
1253
1370
|
};
|
|
@@ -1255,6 +1372,8 @@ async function startOwnedDevServer({
|
|
|
1255
1372
|
const pwaEntry = mode === 'source' && pwaEnabled && entryDocument;
|
|
1256
1373
|
const transformed = pwaEntry || (selectedVersion !== undefined
|
|
1257
1374
|
&& /\.(?:m?js|html?|css|json)$/iu.test(extension));
|
|
1375
|
+
const representation = transformed || retainedDocument
|
|
1376
|
+
? sourceRepresentation(opened, assetVersion, pwaEntry, transformed) : undefined;
|
|
1258
1377
|
await serveSourceFile(
|
|
1259
1378
|
fileServer,
|
|
1260
1379
|
request,
|
|
@@ -1265,9 +1384,8 @@ async function startOwnedDevServer({
|
|
|
1265
1384
|
revalidate: pwaEnabled || entryDocument || managedMap,
|
|
1266
1385
|
pwaEntry,
|
|
1267
1386
|
onReference,
|
|
1268
|
-
lastModified:
|
|
1269
|
-
|
|
1270
|
-
: undefined
|
|
1387
|
+
lastModified: representation?.lastModified,
|
|
1388
|
+
representationTracked: representation !== undefined
|
|
1271
1389
|
}
|
|
1272
1390
|
);
|
|
1273
1391
|
});
|
package/src/packager/core.mjs
CHANGED
|
@@ -166,6 +166,16 @@ function validatePathList(value,label,{required=false,allowRoot=false}={}){
|
|
|
166
166
|
return normalized;
|
|
167
167
|
}
|
|
168
168
|
|
|
169
|
+
export function normalizeAppDocuments(value,label='documents'){
|
|
170
|
+
if(value===undefined)return undefined;
|
|
171
|
+
if(!is.array(value))fail(`${label} must be an array of application-relative HTML paths.`);
|
|
172
|
+
return value.map(function normalizeDocumentPath(entry,index){
|
|
173
|
+
const document=normalizeRelativePath(entry,`${label}[${index}]`);
|
|
174
|
+
if(!/\.html?$/iu.test(document))fail(`${label}[${index}] must name an HTML or HTM file.`);
|
|
175
|
+
return document;
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
169
179
|
function isAlwaysForbidden(relative){
|
|
170
180
|
return relative.split('/').some(segment=>{
|
|
171
181
|
const key=pathKey(segment);
|
|
@@ -308,7 +318,7 @@ function normalizeOptionalRecord(value,label){
|
|
|
308
318
|
export function validateAppConfig(value,appId,rootConfig,configPath=path.posix.join(appRelativeRoot(rootConfig,appId),APP_CONFIG_NAME)){
|
|
309
319
|
assertOnlyKeys(value,new Set([
|
|
310
320
|
'schemaVersion','id','displayName','version','entry','strategy','security',
|
|
311
|
-
'localAIModelPolicy','include','exclude','shared','adapter','pwa','outputDirectory'
|
|
321
|
+
'localAIModelPolicy','include','exclude','shared','adapter','pwa','outputDirectory','documents'
|
|
312
322
|
]),`${appId}/${APP_CONFIG_NAME}`);
|
|
313
323
|
if(value.schemaVersion!==1)fail(`${appId}/${APP_CONFIG_NAME}.schemaVersion must be 1.`);
|
|
314
324
|
if(!is.string(value.id)||value.id!==appId||!APP_ID_PATTERN.test(value.id)){
|
|
@@ -317,6 +327,7 @@ export function validateAppConfig(value,appId,rootConfig,configPath=path.posix.j
|
|
|
317
327
|
const displayName=assertPresentationText(value.displayName,`${appId}/${APP_CONFIG_NAME}.displayName`);
|
|
318
328
|
parseSemver(value.version);
|
|
319
329
|
const entry=normalizeRelativePath(value.entry,`${appId}/${APP_CONFIG_NAME}.entry`);
|
|
330
|
+
const documents=normalizeAppDocuments(value.documents,`${appId}/${APP_CONFIG_NAME}.documents`);
|
|
320
331
|
const outputDirectory=value.outputDirectory===undefined?undefined:normalizeRelativePath(
|
|
321
332
|
value.outputDirectory,`${appId}/${APP_CONFIG_NAME}.outputDirectory`
|
|
322
333
|
);
|
|
@@ -355,6 +366,7 @@ export function validateAppConfig(value,appId,rootConfig,configPath=path.posix.j
|
|
|
355
366
|
displayName,
|
|
356
367
|
version:value.version,
|
|
357
368
|
entry,
|
|
369
|
+
...(documents===undefined?{}:{documents}),
|
|
358
370
|
...(outputDirectory===undefined?{}:{outputDirectory}),
|
|
359
371
|
strategy:value.strategy,
|
|
360
372
|
...(value.pwa===undefined?{}:{pwa:normalizePwaConfig(value.pwa)}),
|
|
@@ -603,7 +615,23 @@ async function collectPackageRecords(context,{signal}={}){
|
|
|
603
615
|
return records;
|
|
604
616
|
}
|
|
605
617
|
|
|
606
|
-
async function browserDocuments(records,entry){
|
|
618
|
+
async function browserDocuments(records,entry,selected){
|
|
619
|
+
if(selected!==undefined){
|
|
620
|
+
const appRecords=new Map(records.filter(function applicationRecord(record){
|
|
621
|
+
return record.appRelativePath!==undefined;
|
|
622
|
+
}).map(function applicationRecordPath(record){
|
|
623
|
+
return [record.appRelativePath,record];
|
|
624
|
+
}));
|
|
625
|
+
const documentPaths=[entry,...[...new Set(selected)].filter(function secondaryDocument(document){
|
|
626
|
+
return document!==entry;
|
|
627
|
+
}).sort(compareText)];
|
|
628
|
+
return Promise.all(documentPaths.map(async function inspectSelectedDocument(documentPath){
|
|
629
|
+
const record=appRecords.get(documentPath);
|
|
630
|
+
if(!record)fail(`Application document is missing from the selected files: ${documentPath}.`);
|
|
631
|
+
const inspected=inspectImportMapHtml(await readFile(record.source,'utf8'),{documentPath});
|
|
632
|
+
return {path:documentPath,packagePath:record.destination,...copyJson(inspected)};
|
|
633
|
+
}));
|
|
634
|
+
}
|
|
607
635
|
let entryDocument=null;
|
|
608
636
|
const documents=[];
|
|
609
637
|
for(const record of records){
|
|
@@ -638,12 +666,13 @@ async function optionalDescriptor(context){
|
|
|
638
666
|
|
|
639
667
|
async function inspectContext(context,{signal,records}={}){
|
|
640
668
|
const selectedRecords=records??await collectPackageRecords(context,{signal});
|
|
641
|
-
const documents=await browserDocuments(selectedRecords,context.config.entry);
|
|
669
|
+
const documents=await browserDocuments(selectedRecords,context.config.entry,context.config.documents);
|
|
642
670
|
return {
|
|
643
671
|
appId:context.appId,
|
|
644
672
|
displayName:context.config.displayName,
|
|
645
673
|
version:context.config.version,
|
|
646
674
|
entry:context.config.entry,
|
|
675
|
+
...(context.config.documents===undefined?{}:{documents:[...context.config.documents]}),
|
|
647
676
|
...(context.config.outputDirectory===undefined?{}:{outputDirectory:context.config.outputDirectory}),
|
|
648
677
|
strategy:context.config.strategy,
|
|
649
678
|
...(context.config.pwa===undefined?{}:{pwa:copyJson(context.config.pwa)}),
|
|
@@ -888,9 +917,12 @@ async function packageWithContext(context,options={}){
|
|
|
888
917
|
const offlineReferences=new Set();
|
|
889
918
|
const entryUrl=new URL(packageResourceUrl(entryPath),'http://arcane.invalid/');
|
|
890
919
|
const pwaDocumentSources=new Map();
|
|
920
|
+
const explicitDocumentPaths=context.config.documents===undefined?null:new Set(
|
|
921
|
+
inspected.browserDocuments.map(function selectedDocumentPath(document){return document.packagePath;})
|
|
922
|
+
);
|
|
891
923
|
if(pwaEnabled){
|
|
892
|
-
const documentPaths=new Set([entryPath]);
|
|
893
|
-
for(const selected of context.config.include){
|
|
924
|
+
const documentPaths=new Set(explicitDocumentPaths??[entryPath]);
|
|
925
|
+
for(const selected of explicitDocumentPaths===null?context.config.include:[]){
|
|
894
926
|
const selectedPath=appPackagePath(context,selected);
|
|
895
927
|
if(/\.html?$/iu.test(selected)&&inventory.has(selectedPath))documentPaths.add(selectedPath);
|
|
896
928
|
}
|
|
@@ -925,6 +957,9 @@ async function packageWithContext(context,options={}){
|
|
|
925
957
|
?new URL(entryDocument.bases[0].href,entryUrl):entryUrl;
|
|
926
958
|
const pending=[{file:entryPath,documentUrl}];
|
|
927
959
|
const sharedFiles=new Set(records.filter(record=>record.appRelativePath===undefined).map(record=>record.destination));
|
|
960
|
+
const applicationFiles=new Set(records.filter(function applicationRecord(record){
|
|
961
|
+
return record.appRelativePath!==undefined;
|
|
962
|
+
}).map(function applicationDestination(record){return record.destination;}));
|
|
928
963
|
for(const file of files){
|
|
929
964
|
if((sharedFiles.has(file)||/^arcane\/(?:modules|entities|components|css|sdk|dependencies)\//u.test(file))
|
|
930
965
|
&&/\.(?:m?js|html?|css)$/iu.test(file))pending.push({file,documentUrl});
|
|
@@ -985,7 +1020,9 @@ async function packageWithContext(context,options={}){
|
|
|
985
1020
|
if(pwaEnabled&&offlineInventory.has(file)){
|
|
986
1021
|
offlineReferences.add(versionAssetUrl(`.${target.pathname}${target.search}`,null));
|
|
987
1022
|
}
|
|
988
|
-
|
|
1023
|
+
const selectedDocument=kind!=='document'||!applicationFiles.has(file)||explicitDocumentPaths===null
|
|
1024
|
+
||explicitDocumentPaths.has(file);
|
|
1025
|
+
if(traversable&&selectedDocument)pending.push({
|
|
989
1026
|
file,
|
|
990
1027
|
documentUrl:kind==='document'?target
|
|
991
1028
|
:baseHref?new URL(baseHref,ownerUrl):current.documentUrl
|