arcane-os 0.52.0 → 0.53.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 +37 -0
- package/README.md +1 -1
- package/docs/architecture.md +26 -3
- package/docs/reference/cli.md +30 -2
- package/docs/reference/inventory/package-api.json +15 -1
- package/docs/reference/protocols.md +23 -4
- package/docs/reference/pwa.md +44 -2
- package/docs/reference/sdk-api.md +158 -1
- package/package.json +3 -2
- package/src/dev-server.mjs +68 -25
- package/src/index.mjs +1 -0
- package/src/packager/core.mjs +10 -5
- package/src/pwa-worker.mjs +4 -2
- package/src/toolchain.mjs +23 -6
- package/src/websocket.mjs +479 -0
package/src/dev-server.mjs
CHANGED
|
@@ -543,11 +543,13 @@ async function packagedRoutes(releaseRoot){
|
|
|
543
543
|
const canonical=await canonicalRealDirectory(requested,'Packaged release root');
|
|
544
544
|
let pwa = false;
|
|
545
545
|
let startPath = '/index.html';
|
|
546
|
+
let rootDocument = null;
|
|
546
547
|
try {
|
|
547
548
|
const release = JSON.parse(
|
|
548
549
|
await readFile(path.join(canonical, 'ARCANE_APP_RELEASE.json'), 'utf8')
|
|
549
550
|
);
|
|
550
551
|
const start = release.app.start ?? release.app.entry;
|
|
552
|
+
if (release.app.rootDocument === './index.html') rootDocument = '/index.html';
|
|
551
553
|
const startUrl = new URL(start, 'http://arcane.invalid/');
|
|
552
554
|
startPath = `${startUrl.pathname}${startUrl.search}${startUrl.hash}`;
|
|
553
555
|
} catch (error) {
|
|
@@ -564,6 +566,7 @@ async function packagedRoutes(releaseRoot){
|
|
|
564
566
|
appId:null,
|
|
565
567
|
pwa,
|
|
566
568
|
startPath,
|
|
569
|
+
rootDocument,
|
|
567
570
|
mappings:[{
|
|
568
571
|
prefix:[],
|
|
569
572
|
root:canonical
|
|
@@ -1026,19 +1029,30 @@ async function startOwnedDevServer({
|
|
|
1026
1029
|
}
|
|
1027
1030
|
async function readCurrentSourceRoutes() {
|
|
1028
1031
|
throwIfAborted(signal);
|
|
1032
|
+
function optionalSourceMetadata(error) {
|
|
1033
|
+
if (error.code !== 'ENOENT') throw error;
|
|
1034
|
+
return null;
|
|
1035
|
+
}
|
|
1029
1036
|
let manifestPath = path.join(appMapping.root, APP_DESCRIPTOR_NAME);
|
|
1037
|
+
const [rootIndex, descriptorInput] = await Promise.all(
|
|
1038
|
+
[
|
|
1039
|
+
lstat(path.join(routeSet.workspaceRoot, 'index.html')).catch(optionalSourceMetadata),
|
|
1040
|
+
lstat(manifestPath).catch(optionalSourceMetadata)
|
|
1041
|
+
]
|
|
1042
|
+
);
|
|
1043
|
+
const rootDocument = rootIndex?.isFile() ? '/index.html' : null;
|
|
1030
1044
|
let authored = true;
|
|
1031
|
-
let input;
|
|
1032
|
-
|
|
1033
|
-
input = await lstat(manifestPath);
|
|
1034
|
-
} catch (error) {
|
|
1035
|
-
if (error.code !== 'ENOENT') throw error;
|
|
1045
|
+
let input = descriptorInput;
|
|
1046
|
+
if (!input) {
|
|
1036
1047
|
authored = false;
|
|
1037
1048
|
manifestPath = path.join(appMapping.root, APP_CONFIG_NAME);
|
|
1038
1049
|
input = await lstat(manifestPath);
|
|
1039
1050
|
}
|
|
1040
1051
|
if (sourceManifestInput?.path === manifestPath
|
|
1041
1052
|
&& sourceManifestInput.modifiedAt === input.mtimeMs) {
|
|
1053
|
+
if (currentSourceRoutes.rootDocument !== rootDocument) {
|
|
1054
|
+
currentSourceRoutes = {...currentSourceRoutes, rootDocument};
|
|
1055
|
+
}
|
|
1042
1056
|
return currentSourceRoutes;
|
|
1043
1057
|
}
|
|
1044
1058
|
const value = JSON.parse(await readFile(manifestPath, 'utf8'));
|
|
@@ -1060,6 +1074,7 @@ async function startOwnedDevServer({
|
|
|
1060
1074
|
currentSourceRoutes = {
|
|
1061
1075
|
...routeSet,
|
|
1062
1076
|
app: manifest,
|
|
1077
|
+
rootDocument,
|
|
1063
1078
|
startPath: applicationSourcePath(routeSet.config,routeSet.appId,manifest.entry),
|
|
1064
1079
|
mappings: mappings.map(
|
|
1065
1080
|
function currentSourceMapping(mapping) {
|
|
@@ -1076,7 +1091,7 @@ async function startOwnedDevServer({
|
|
|
1076
1091
|
function developmentPwaArtifacts(selectedRoutes, assets = [], version = assetVersion) {
|
|
1077
1092
|
const rootApp = selectedRoutes.config.appsRoot === '.';
|
|
1078
1093
|
const appBase = applicationSourcePath(selectedRoutes.config,selectedRoutes.appId);
|
|
1079
|
-
const navigationAliases = {'/': selectedRoutes.startPath};
|
|
1094
|
+
const navigationAliases = selectedRoutes.rootDocument ? {} : {'/': selectedRoutes.startPath};
|
|
1080
1095
|
return createPwaArtifacts(
|
|
1081
1096
|
{
|
|
1082
1097
|
app: {
|
|
@@ -1104,12 +1119,21 @@ async function startOwnedDevServer({
|
|
|
1104
1119
|
function rememberPwaArtifacts(state, artifacts, generatedAt = new Date()) {
|
|
1105
1120
|
for (const file of artifacts.files) {
|
|
1106
1121
|
const previous = state.metadata.get(file.path);
|
|
1107
|
-
const
|
|
1122
|
+
const previousTime = previous?.lastModified
|
|
1123
|
+
? Date.parse(previous.lastModified.toUTCString()) : previous?.ambiguousThrough;
|
|
1124
|
+
let lastModified = previous?.content === file.content
|
|
1108
1125
|
? previous.lastModified
|
|
1109
1126
|
: generatedAt;
|
|
1127
|
+
if (!lastModified && generatedAt.getTime() >= previousTime + 1000) {
|
|
1128
|
+
lastModified = generatedAt;
|
|
1129
|
+
}
|
|
1130
|
+
// A changed alias must not validate the prior body within one HTTP second.
|
|
1131
|
+
const ambiguous = previous?.content !== file.content && previousTime !== undefined
|
|
1132
|
+
&& Date.parse(generatedAt.toUTCString()) <= previousTime;
|
|
1133
|
+
if (ambiguous) lastModified = undefined;
|
|
1110
1134
|
state.metadata.set(
|
|
1111
1135
|
file.path,
|
|
1112
|
-
{content: file.content, lastModified}
|
|
1136
|
+
{content: file.content, lastModified, ambiguousThrough: lastModified ? undefined : previousTime}
|
|
1113
1137
|
);
|
|
1114
1138
|
}
|
|
1115
1139
|
state.artifacts = artifacts;
|
|
@@ -1277,22 +1301,29 @@ async function startOwnedDevServer({
|
|
|
1277
1301
|
);
|
|
1278
1302
|
return;
|
|
1279
1303
|
}
|
|
1280
|
-
|
|
1304
|
+
const rootRequest = segments.length === 0;
|
|
1305
|
+
if (rootRequest && !selectedRoutes.rootDocument) {
|
|
1281
1306
|
response.writeHead(302,{location:`${selectedRoutes.startPath}${target.search}`});
|
|
1282
1307
|
response.end();
|
|
1283
1308
|
return;
|
|
1284
1309
|
}
|
|
1285
|
-
const
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1310
|
+
const resourceSegments = rootRequest ? ['index.html'] : segments;
|
|
1311
|
+
const hostRoot = rootRequest && mode === 'source'
|
|
1312
|
+
&& (selectedRoutes.config.appsRoot !== '.' || !sourcePathAllowed(resourceSegments, selectedRoutes.app));
|
|
1313
|
+
// Only the host's exact root document is outside app selection.
|
|
1314
|
+
// Its direct URL and every other resource keep the selected routes.
|
|
1315
|
+
const mapping = hostRoot ? {kind: 'host', root: selectedRoutes.workspaceRoot, prefix: []}
|
|
1316
|
+
: selectedRoutes.mappings.find(
|
|
1317
|
+
function requestedSourceMapping(route) {
|
|
1318
|
+
return route.prefix.every(
|
|
1319
|
+
function matchingSourcePrefix(segment, index) {
|
|
1320
|
+
return resourceSegments[index] === segment;
|
|
1321
|
+
}
|
|
1322
|
+
);
|
|
1323
|
+
}
|
|
1324
|
+
);
|
|
1294
1325
|
if(!mapping){deny(response,404,'Not found.');return;}
|
|
1295
|
-
const relative=
|
|
1326
|
+
const relative=resourceSegments.slice(mapping.prefix.length);
|
|
1296
1327
|
if(relative.length===0){deny(response,404,'Not found.');return;}
|
|
1297
1328
|
if(mapping.allow&&!mapping.allow(relative)){deny(response,404,'Not found.');return;}
|
|
1298
1329
|
await runFileWork(async function serveMappedResource() {
|
|
@@ -1307,6 +1338,18 @@ async function startOwnedDevServer({
|
|
|
1307
1338
|
deny(response,404,'Not found.');
|
|
1308
1339
|
return;
|
|
1309
1340
|
}
|
|
1341
|
+
if (hostRoot) {
|
|
1342
|
+
const representation = sourceRepresentation(opened, assetVersion, false, false);
|
|
1343
|
+
await serveSourceFile(
|
|
1344
|
+
fileServer,
|
|
1345
|
+
request,
|
|
1346
|
+
response,
|
|
1347
|
+
opened,
|
|
1348
|
+
{lastModified: representation.lastModified, representationTracked: true}
|
|
1349
|
+
);
|
|
1350
|
+
return;
|
|
1351
|
+
}
|
|
1352
|
+
const resourcePath = rootRequest ? selectedRoutes.rootDocument : target.path;
|
|
1310
1353
|
const extension=path.extname(opened.candidate).toLowerCase();
|
|
1311
1354
|
const html=extension==='.html'||extension==='.htm';
|
|
1312
1355
|
const explicitDocuments = mode === 'source' && mapping.kind === 'app'
|
|
@@ -1316,7 +1359,7 @@ async function startOwnedDevServer({
|
|
|
1316
1359
|
const selectedPwaDocument = mode === 'source' && pwaEnabled && html
|
|
1317
1360
|
&& mapping.kind === 'app'
|
|
1318
1361
|
&& !explicitDocuments && selectedRoutes.app.include.includes(relative.join('/'));
|
|
1319
|
-
const selectedDocument =
|
|
1362
|
+
const selectedDocument = resourcePath === selectedRoutes.startPath || explicitDocument || selectedPwaDocument;
|
|
1320
1363
|
const managedDocument = !selectedDocument && !explicitDocuments && html && await isManagedDocument(opened);
|
|
1321
1364
|
const entryDocument = selectedDocument || managedDocument;
|
|
1322
1365
|
const managedMap=path.basename(opened.candidate)==='arcane.importmap.json';
|
|
@@ -1324,22 +1367,22 @@ async function startOwnedDevServer({
|
|
|
1324
1367
|
// version record on navigation, not on each resource request.
|
|
1325
1368
|
if(entryDocument||managedMap)rememberAssetVersion(await selectedAssetVersion());
|
|
1326
1369
|
const runtimeResource=mapping.kind!=='app';
|
|
1327
|
-
if (runtimeResource && html) runtimeDocuments.add(
|
|
1370
|
+
if (runtimeResource && html) runtimeDocuments.add(resourcePath);
|
|
1328
1371
|
const browserResource=['script','style','worker','sharedworker','serviceworker']
|
|
1329
1372
|
.includes(request.headers['sec-fetch-dest']);
|
|
1330
1373
|
// Selecting application pages does not turn retained originals
|
|
1331
1374
|
// into resources. HTML components remain owned by html-import.
|
|
1332
1375
|
const componentDocument=explicitDocuments&&html&&!selectedDocument
|
|
1333
|
-
&&await isCurrentComponent(
|
|
1376
|
+
&&await isCurrentComponent(resourcePath, selectedRoutes);
|
|
1334
1377
|
const retainedDocument=explicitDocuments&&html&&!selectedDocument&&!componentDocument;
|
|
1335
1378
|
const rewrite=!retainedDocument&&(runtimeResource||entryDocument||browserResource||managedMap
|
|
1336
|
-
||componentDocument||resourcePaths.has(
|
|
1379
|
+
||componentDocument||resourcePaths.has(resourcePath));
|
|
1337
1380
|
const onReference = function observeServedResource(reference) {
|
|
1338
1381
|
const {url,kind,baseKind}=reference;
|
|
1339
1382
|
if (pwaEnabled && kind !== 'fetch'
|
|
1340
1383
|
&& !(kind === 'import' && !/^(?:\.{1,2}\/|\/)/u.test(url))) {
|
|
1341
1384
|
try {
|
|
1342
|
-
const documentUrl = new URL(
|
|
1385
|
+
const documentUrl = new URL(resourcePath, 'http://arcane.invalid');
|
|
1343
1386
|
const resource = resolveAssetReference(
|
|
1344
1387
|
{...reference,url:versionAssetUrl(url,null)},
|
|
1345
1388
|
{ownerUrl:documentUrl}
|
|
@@ -1354,7 +1397,7 @@ async function startOwnedDevServer({
|
|
|
1354
1397
|
if(baseKind==='document'||kind==='fetch'||(kind==='asset'&&!/\.css(?:[?#]|$)/iu.test(url))
|
|
1355
1398
|
||(kind==='import'&&!/^(?:\.{1,2}\/|\/)/u.test(url)))return;
|
|
1356
1399
|
try{
|
|
1357
|
-
const documentUrl=new URL(
|
|
1400
|
+
const documentUrl=new URL(resourcePath,'http://arcane.invalid');
|
|
1358
1401
|
const resource=resolveAssetReference(reference,{ownerUrl:documentUrl});
|
|
1359
1402
|
if(resource.origin===documentUrl.origin
|
|
1360
1403
|
&&/\.(?:m?js|html?|css)$/iu.test(resource.pathname)){
|
package/src/index.mjs
CHANGED
|
@@ -146,6 +146,7 @@ export {
|
|
|
146
146
|
export {materializeInstalledSdkRuntime} from './installed-sdk-runtime.mjs';
|
|
147
147
|
export {startDevServer} from './dev-server.mjs';
|
|
148
148
|
export {startSourceExampleServer} from './source-server.mjs';
|
|
149
|
+
export {acceptWebSocket} from './websocket.mjs';
|
|
149
150
|
export {
|
|
150
151
|
NATIVE_BUILD_PLAN_PROTOCOL,
|
|
151
152
|
NATIVE_BUILDER_PROTOCOL,
|
package/src/packager/core.mjs
CHANGED
|
@@ -788,7 +788,7 @@ async function loadAdapter(context){
|
|
|
788
788
|
return module;
|
|
789
789
|
}
|
|
790
790
|
|
|
791
|
-
function releaseManifest(context,files,pwaArtifacts){
|
|
791
|
+
function releaseManifest(context,files,pwaArtifacts,rootDocument){
|
|
792
792
|
return {
|
|
793
793
|
schemaVersion:1,
|
|
794
794
|
kind:'arcane-app-release',
|
|
@@ -799,6 +799,7 @@ function releaseManifest(context,files,pwaArtifacts){
|
|
|
799
799
|
version:context.config.version,
|
|
800
800
|
entry:context.config.entry,
|
|
801
801
|
start:packageResourceUrl(appPackagePath(context, context.config.entry)),
|
|
802
|
+
...(rootDocument ? {rootDocument} : {}),
|
|
802
803
|
strategy:context.config.strategy,
|
|
803
804
|
shared:[...context.config.shared],
|
|
804
805
|
...(pwaArtifacts?{pwa:{
|
|
@@ -845,6 +846,11 @@ async function packageWithContext(context,options={}){
|
|
|
845
846
|
const appPath=appRelativeRoot(context.rootConfig,context.appId);
|
|
846
847
|
const entryPath=appPackagePath(context,context.config.entry);
|
|
847
848
|
const records=await collectPackageRecords(context,{signal});
|
|
849
|
+
const rootDocument = records.some(
|
|
850
|
+
function selectedRootIndex(record) {
|
|
851
|
+
return record.destination === 'index.html';
|
|
852
|
+
}
|
|
853
|
+
) ? './index.html' : undefined;
|
|
848
854
|
const inspected=await inspectContext(context,{signal,records});
|
|
849
855
|
if(options.dryRun){
|
|
850
856
|
return {
|
|
@@ -888,9 +894,7 @@ async function packageWithContext(context,options={}){
|
|
|
888
894
|
try{
|
|
889
895
|
async function copyBase() {
|
|
890
896
|
await copyRecords(records,stagingRoot,{signal,onEvent});
|
|
891
|
-
if (!
|
|
892
|
-
return record.destination === 'index.html';
|
|
893
|
-
})) {
|
|
897
|
+
if (!rootDocument) {
|
|
894
898
|
await writePackageLauncher(context, stagingRoot);
|
|
895
899
|
}
|
|
896
900
|
}
|
|
@@ -1050,6 +1054,7 @@ async function packageWithContext(context,options={}){
|
|
|
1050
1054
|
sdkVersion:assetVersion,
|
|
1051
1055
|
pwa:context.config.pwa,
|
|
1052
1056
|
files,
|
|
1057
|
+
navigationAliases: rootDocument ? {} : {'./': packageResourceUrl(entryPath)},
|
|
1053
1058
|
assets:[...offlineReferences]
|
|
1054
1059
|
}):null;
|
|
1055
1060
|
if(pwaArtifacts){
|
|
@@ -1080,7 +1085,7 @@ async function packageWithContext(context,options={}){
|
|
|
1080
1085
|
}
|
|
1081
1086
|
files.sort(compareText);
|
|
1082
1087
|
}
|
|
1083
|
-
const manifest=releaseManifest(context,files,pwaArtifacts);
|
|
1088
|
+
const manifest=releaseManifest(context,files,pwaArtifacts,rootDocument);
|
|
1084
1089
|
await writeFile(
|
|
1085
1090
|
path.join(stagingRoot,RELEASE_MANIFEST_NAME),
|
|
1086
1091
|
`${JSON.stringify(manifest,null,2)}\n`,
|
package/src/pwa-worker.mjs
CHANGED
|
@@ -496,8 +496,10 @@ function installPwaWorker(manifest, clientUrl) {
|
|
|
496
496
|
|
|
497
497
|
function staticNavigationResource(url) {
|
|
498
498
|
const document = new URL(url);
|
|
499
|
-
if (document.origin !== scopeOrigin
|
|
500
|
-
|
|
499
|
+
if (document.origin !== scopeOrigin) return null;
|
|
500
|
+
if (document.pathname.endsWith('/')) {
|
|
501
|
+
document.pathname += 'index.html';
|
|
502
|
+
} else if (!document.search || !/\.html?$/iu.test(document.pathname)) {
|
|
501
503
|
return null;
|
|
502
504
|
}
|
|
503
505
|
document.search = '';
|
package/src/toolchain.mjs
CHANGED
|
@@ -330,24 +330,41 @@ async function refreshRootApplicationFiles(prepared,inspected,importMap,{signal,
|
|
|
330
330
|
const manifest=prepared.validation.app.manifest;
|
|
331
331
|
const installed=await readInstalledSdkLayout(workspaceRoot,prepared.validation.config);
|
|
332
332
|
const entry=`/${manifest.entry.split('/').map(encodeURIComponent).join('/')}`;
|
|
333
|
-
const
|
|
334
|
-
|
|
335
|
-
|
|
333
|
+
const pwaEnabled = installed?.direct && manifest.pwa?.enabled;
|
|
334
|
+
let authoredRoot = false;
|
|
335
|
+
if (pwaEnabled) {
|
|
336
|
+
try {
|
|
337
|
+
authoredRoot = (await lstat(path.join(workspaceRoot, 'index.html'))).isFile();
|
|
338
|
+
} catch (error) {
|
|
339
|
+
if (error.code !== 'ENOENT') throw error;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
// A host homepage need not belong to the selected application or its cache.
|
|
343
|
+
const navigationAliases = authoredRoot ? {} : {'/': entry};
|
|
344
|
+
function rootIndexPath(relative) {
|
|
345
|
+
return (process.platform === 'win32' ? relative.toLowerCase() : relative) === 'index.html';
|
|
346
|
+
}
|
|
347
|
+
const selectedRootIndex = authoredRoot && manifest.include.some(rootIndexPath)
|
|
348
|
+
&& !manifest.exclude.some(rootIndexPath);
|
|
336
349
|
// The source host serves the installed files in place. There is no runtime projection.
|
|
337
350
|
const files=[...new Set([
|
|
338
|
-
...inspected.files.filter(
|
|
351
|
+
...inspected.files.filter(
|
|
352
|
+
function selectedSourceFile(file) {
|
|
353
|
+
return file !== 'index.html' || selectedRootIndex;
|
|
354
|
+
}
|
|
355
|
+
),
|
|
339
356
|
importMap.artifactRelativePath
|
|
340
357
|
])];
|
|
341
358
|
const sourceOrigin='http://arcane.invalid';
|
|
342
359
|
const selectedPaths=new Set(files.map(function selectedStaticPath(file){
|
|
343
360
|
return new URL(`/${file.split('/').map(encodeURIComponent).join('/')}`,sourceOrigin).pathname;
|
|
344
361
|
}));
|
|
345
|
-
const assets=
|
|
362
|
+
const assets=pwaEnabled
|
|
346
363
|
?(await collectSourcePwaAssets({workspaceRoot,appId,signal,onEvent})).filter(function selectedStaticVariant(value){
|
|
347
364
|
const url=new URL(value,sourceOrigin);
|
|
348
365
|
return url.origin===sourceOrigin&&selectedPaths.has(url.pathname);
|
|
349
366
|
}):[];
|
|
350
|
-
const pwa=
|
|
367
|
+
const pwa=pwaEnabled?createPwaArtifacts({
|
|
351
368
|
app:{id:appId,displayName:manifest.displayName,version:manifest.version,entry},
|
|
352
369
|
sdkVersion:installed.version,
|
|
353
370
|
pwa:manifest.pwa,
|