arcane-os 0.26.0 → 0.27.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 +27 -0
- package/README.md +9 -4
- package/browser-runtime/pwa-install.mjs +3 -3
- package/docs/architecture.md +19 -8
- package/docs/reference/cli.md +18 -7
- package/docs/reference/protocols.md +30 -8
- package/docs/reference/pwa.md +24 -0
- package/package.json +2 -2
- package/runtime/arcane/components/dashboard-config.html +2 -1
- package/runtime/arcane/components/data-view.html +2 -1
- package/runtime/arcane/components/file-manager.html +7 -6
- package/src/app-descriptor.mjs +3 -1
- package/src/app-layout.mjs +32 -0
- package/src/application-tests.mjs +3 -1
- package/src/cli/main.mjs +9 -12
- package/src/dev-server.mjs +82 -24
- package/src/import-map.mjs +26 -10
- package/src/packager/core.mjs +79 -21
- package/src/pwa-worker.mjs +18 -4
- package/src/pwa.mjs +55 -10
- package/src/scaffold.mjs +43 -7
- package/src/sdk-runtime-layout.mjs +28 -15
- package/src/templates/workspace-template.mjs +44 -36
- package/src/toolchain.mjs +76 -3
- package/src/workspace.mjs +46 -38
package/src/workspace.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import Is from 'strong-type';
|
|
2
|
+
import {appRelativeRoot, resolveAppRoot, appBaseHref} from './app-layout.mjs';
|
|
2
3
|
import {readFile,readdir,realpath,stat} from 'node:fs/promises';
|
|
3
4
|
import path from 'node:path';
|
|
4
5
|
import {
|
|
@@ -228,6 +229,25 @@ async function discoverAppsInRoot(root,config,appId){
|
|
|
228
229
|
if(appId!==undefined&&(!is.string(appId)||!APP_ID_PATTERN.test(appId))){
|
|
229
230
|
fail(`Invalid app id: ${String(appId)}.`,'ARCANE_USAGE');
|
|
230
231
|
}
|
|
232
|
+
async function readApp(appRoot,id,manifest){
|
|
233
|
+
const configPath=path.join(appRoot,APP_CONFIG_NAME);
|
|
234
|
+
manifest??=await readJson(configPath,`${appRelativeRoot(config,id) || '.'}/${APP_CONFIG_NAME}`);
|
|
235
|
+
const validatedManifest=validatePackagerAppConfig(manifest,id,config,configPath);
|
|
236
|
+
if(!validatedManifest.shared.includes('browser-runtime')){
|
|
237
|
+
fail(`${configPath} must include the browser-runtime shared payload.`);
|
|
238
|
+
}
|
|
239
|
+
const descriptor=await loadAppDescriptor({workspaceRoot:root,appRoot,appId:id,packageManifest:manifest});
|
|
240
|
+
return completeValue({
|
|
241
|
+
appId:id,appRoot,manifest:validatedManifest,
|
|
242
|
+
descriptor:descriptor.descriptor,descriptorSource:descriptor.source,
|
|
243
|
+
descriptorPath:descriptor.descriptorPath
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
if(config.appsRoot==='.'){
|
|
247
|
+
const manifest=await readJson(path.join(root,APP_CONFIG_NAME),APP_CONFIG_NAME);
|
|
248
|
+
if(appId!==undefined&&appId!==manifest.id)return [];
|
|
249
|
+
return [await readApp(root,manifest.id,manifest)];
|
|
250
|
+
}
|
|
231
251
|
const appsRoot=path.join(root,'apps');
|
|
232
252
|
await assertRealDirectory(appsRoot,'Workspace apps root');
|
|
233
253
|
const entries=await readdir(appsRoot,{withFileTypes:true});
|
|
@@ -240,26 +260,7 @@ async function discoverAppsInRoot(root,config,appId){
|
|
|
240
260
|
if(!entry.isDirectory())continue;
|
|
241
261
|
const appRoot=path.join(appsRoot,entry.name);
|
|
242
262
|
try{
|
|
243
|
-
|
|
244
|
-
const manifest=await readJson(configPath,`apps/${entry.name}/${APP_CONFIG_NAME}`);
|
|
245
|
-
const validatedManifest=validatePackagerAppConfig(manifest,entry.name,config,configPath);
|
|
246
|
-
if(!validatedManifest.shared.includes('browser-runtime')){
|
|
247
|
-
fail(`apps/${entry.name}/${APP_CONFIG_NAME} must include the browser-runtime shared payload.`);
|
|
248
|
-
}
|
|
249
|
-
const descriptor=await loadAppDescriptor({
|
|
250
|
-
workspaceRoot:root,
|
|
251
|
-
appRoot,
|
|
252
|
-
appId:entry.name,
|
|
253
|
-
packageManifest:manifest
|
|
254
|
-
});
|
|
255
|
-
apps.push(completeValue({
|
|
256
|
-
appId:entry.name,
|
|
257
|
-
appRoot,
|
|
258
|
-
manifest:validatedManifest,
|
|
259
|
-
descriptor:descriptor.descriptor,
|
|
260
|
-
descriptorSource:descriptor.source,
|
|
261
|
-
descriptorPath:descriptor.descriptorPath
|
|
262
|
-
}));
|
|
263
|
+
apps.push(await readApp(appRoot,entry.name));
|
|
263
264
|
}catch(error){
|
|
264
265
|
if(!String(error?.message).includes('does not exist'))throw error;
|
|
265
266
|
}
|
|
@@ -294,7 +295,7 @@ export async function selectApp(workspaceRoot=process.cwd(),appId){
|
|
|
294
295
|
if(!selected)fail(`Unknown app "${appId}".`);
|
|
295
296
|
return selected;
|
|
296
297
|
}
|
|
297
|
-
if(apps.length===0)fail('No Arcane applications were found
|
|
298
|
+
if(apps.length===0)fail('No Arcane applications were found at the configured appsRoot.');
|
|
298
299
|
if(apps.length>1){
|
|
299
300
|
fail(`This workspace contains multiple apps; select one explicitly: ${apps.map(app=>app.appId).join(', ')}.`,'ARCANE_USAGE');
|
|
300
301
|
}
|
|
@@ -313,7 +314,7 @@ export async function resolveWorkspace({workspaceRoot=process.cwd(),appId}={}){
|
|
|
313
314
|
}else if(apps.length===1){
|
|
314
315
|
[app]=apps;
|
|
315
316
|
}else if(apps.length===0){
|
|
316
|
-
fail('No Arcane applications were found
|
|
317
|
+
fail('No Arcane applications were found at the configured appsRoot.');
|
|
317
318
|
}else{
|
|
318
319
|
fail(`This workspace contains multiple apps; select one explicitly: ${apps.map(item=>item.appId).join(', ')}.`,'ARCANE_USAGE');
|
|
319
320
|
}
|
|
@@ -363,39 +364,43 @@ export async function resolveInstalledSdkInstallation(workspaceRoot,declaration)
|
|
|
363
364
|
|
|
364
365
|
function assertHtmlContract(source,appId,{
|
|
365
366
|
entry='index.html',
|
|
367
|
+
appPath=`apps/${appId}`,
|
|
368
|
+
baseHref='../../',
|
|
369
|
+
runtimePath='arcane',
|
|
366
370
|
strictStyles=true,
|
|
367
371
|
allowMissingManagedImportMap=false
|
|
368
372
|
}={}){
|
|
369
|
-
const entryLabel
|
|
373
|
+
const entryLabel=[appPath,entry].filter(Boolean).join('/');
|
|
370
374
|
const htmlContract=inspectImportMapHtml(source);
|
|
371
375
|
const appIdMetadata=htmlContract.metas.filter(meta=>meta.name==='arcane-app-id');
|
|
372
376
|
if(appIdMetadata.length!==1||appIdMetadata[0].content!==appId){
|
|
373
377
|
fail(`${entryLabel} must declare exactly one active matching arcane-app-id metadata element.`);
|
|
374
378
|
}
|
|
375
|
-
if(htmlContract.bases.length!==1||htmlContract.bases[0].href!==
|
|
376
|
-
fail(`${entryLabel} must declare exactly one active <base href="
|
|
379
|
+
if(htmlContract.bases.length!==1||htmlContract.bases[0].href!==baseHref){
|
|
380
|
+
fail(`${entryLabel} must declare exactly one active <base href="${baseHref}">.`);
|
|
377
381
|
}
|
|
378
|
-
const resourcePath=value=>value.split(/[?#]/u,1)[0];
|
|
382
|
+
const resourcePath=value=>value.split(/[?#]/u,1)[0].replace(/^\.\//u,'/');
|
|
379
383
|
const styles=htmlContract.links.filter(link=>link.rel
|
|
380
384
|
.split(/[\t\n\f\r ]+/u).includes('stylesheet'));
|
|
381
385
|
const positionOfStyle=expected=>styles.find(link=>resourcePath(link.href)===expected)?.start??-1;
|
|
382
|
-
const theme=positionOfStyle(
|
|
383
|
-
const primitives=positionOfStyle(
|
|
384
|
-
const
|
|
386
|
+
const theme=positionOfStyle(`/${runtimePath}/css/theme.css`);
|
|
387
|
+
const primitives=positionOfStyle(`/${runtimePath}/css/primitives.css`);
|
|
388
|
+
const appPrefix=appPath?`${appPath}/`:'';
|
|
389
|
+
const escapedAppPrefix=appPrefix.replace(/[.*+?^${}()|[\]\\]/gu,'\\$&');
|
|
385
390
|
const appStyle=styles.find(link=>new RegExp(
|
|
386
|
-
`^(?:\\./|/)
|
|
391
|
+
`^(?:\\./|/)${escapedAppPrefix}[^/]+\\.css$`,
|
|
387
392
|
'u'
|
|
388
393
|
).test(resourcePath(link.href)))?.start??-1;
|
|
389
394
|
const modules=htmlContract.scripts.filter(script=>script.type==='module'&&script.src);
|
|
390
395
|
const bootstrap=modules.find(script=>resourcePath(script.src)
|
|
391
|
-
|
|
396
|
+
===`/${runtimePath}/modules/ThemeBootstrap.js`)?.start??-1;
|
|
392
397
|
if(htmlContract.managedMaps.length>1){
|
|
393
398
|
fail(`${entryLabel} must contain at most one active managed Arcane import map.`);
|
|
394
399
|
}
|
|
395
400
|
const managedImportMap=htmlContract.managedMaps[0]?.start??-1;
|
|
396
401
|
const firstModule=htmlContract.firstModulePosition;
|
|
397
402
|
const appModule=modules.find(script=>new RegExp(
|
|
398
|
-
`^(?:\\./|/)
|
|
403
|
+
`^(?:\\./|/)${escapedAppPrefix}(?!arcane/|node_modules/).+\\.(?:js|mjs)$`,
|
|
399
404
|
'u'
|
|
400
405
|
).test(resourcePath(script.src)))?.start??-1;
|
|
401
406
|
if(theme<0){
|
|
@@ -444,12 +449,6 @@ export async function validateDiscoveredApplication({
|
|
|
444
449
|
fail('A discovered Arcane application is required for focused validation.');
|
|
445
450
|
}
|
|
446
451
|
const canonicalWorkspaceRoot=await realpath(workspaceRoot);
|
|
447
|
-
await assertRealDirectory(path.join(canonicalWorkspaceRoot,'apps'),'Workspace apps root');
|
|
448
|
-
const expectedAppRoot=path.join(canonicalWorkspaceRoot,'apps',app.appId);
|
|
449
|
-
if(!sameDirectoryPath(app.appRoot,expectedAppRoot)){
|
|
450
|
-
fail(`Discovered app ${app.appId} does not belong to the selected workspace.`);
|
|
451
|
-
}
|
|
452
|
-
const canonicalAppRoot=await realpath(app.appRoot);
|
|
453
452
|
let config=workspaceConfig;
|
|
454
453
|
if(!config){
|
|
455
454
|
const profile=await inspectWorkspaceProfile(canonicalWorkspaceRoot);
|
|
@@ -461,6 +460,12 @@ export async function validateDiscoveredApplication({
|
|
|
461
460
|
if(!isObject(config?.sharedPayloads)){
|
|
462
461
|
fail('The selected Arcane workspace configuration is unavailable for focused validation.');
|
|
463
462
|
}
|
|
463
|
+
const expectedAppRoot=resolveAppRoot(canonicalWorkspaceRoot,config,app.appId);
|
|
464
|
+
await assertRealDirectory(expectedAppRoot,'Selected application root');
|
|
465
|
+
if(!sameDirectoryPath(app.appRoot,expectedAppRoot)){
|
|
466
|
+
fail(`Discovered app ${app.appId} does not belong to the selected workspace.`);
|
|
467
|
+
}
|
|
468
|
+
const canonicalAppRoot=await realpath(app.appRoot);
|
|
464
469
|
const configPath=path.join(canonicalAppRoot,APP_CONFIG_NAME);
|
|
465
470
|
const rawManifest=await readJson(
|
|
466
471
|
configPath,
|
|
@@ -492,6 +497,9 @@ export async function validateDiscoveredApplication({
|
|
|
492
497
|
}
|
|
493
498
|
assertHtmlContract(await readFile(entryPath,'utf8'),app.appId,{
|
|
494
499
|
entry:manifest.entry,
|
|
500
|
+
appPath:appRelativeRoot(config,app.appId),
|
|
501
|
+
baseHref:appBaseHref(canonicalWorkspaceRoot,canonicalAppRoot,manifest.entry),
|
|
502
|
+
runtimePath:config.sharedPayloads['browser-runtime'][0].destination,
|
|
495
503
|
strictStyles:workspaceMode==='external',
|
|
496
504
|
allowMissingManagedImportMap
|
|
497
505
|
});
|