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/pwa.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import Is from 'strong-type';
|
|
2
2
|
import {randomUUID} from 'node:crypto';
|
|
3
|
+
import path from 'node:path';
|
|
3
4
|
import {createPwaWorkerScript} from './pwa-worker.mjs';
|
|
4
5
|
import {versionAssetUrl} from './import-map.mjs';
|
|
5
6
|
|
|
@@ -202,6 +203,8 @@ export function createPwaArtifacts(
|
|
|
202
203
|
mode = 'release',
|
|
203
204
|
runtimeBase = './arcane/sdk/',
|
|
204
205
|
appBase,
|
|
206
|
+
installationId,
|
|
207
|
+
legacyAppPath,
|
|
205
208
|
appPath = '',
|
|
206
209
|
navigationAliases,
|
|
207
210
|
revision
|
|
@@ -218,7 +221,7 @@ export function createPwaArtifacts(
|
|
|
218
221
|
// Relocating app files must not change an existing installed app's default identity.
|
|
219
222
|
const installationBase = appBase ?? (mode === 'development' ? applicationBase : './');
|
|
220
223
|
const manifest = {
|
|
221
|
-
id: installationBase,
|
|
224
|
+
id: installationId ?? installationBase,
|
|
222
225
|
name: app.displayName,
|
|
223
226
|
short_name: app.displayName,
|
|
224
227
|
start_url: manifestUrl(app.entry, appPath ? basePath : applicationBase),
|
|
@@ -304,18 +307,60 @@ controller.ready.catch(
|
|
|
304
307
|
const entryAssets = {manifest: PWA_MANIFEST_NAME, bootstrap: PWA_BOOTSTRAP_NAME};
|
|
305
308
|
const manifestHref = resourceUrl(basePath, entryAssets.manifest);
|
|
306
309
|
const bootstrapHref = resourceUrl(basePath, entryAssets.bootstrap);
|
|
310
|
+
const generatedFiles = [
|
|
311
|
+
{path: PWA_MANIFEST_NAME, content: json(manifest)},
|
|
312
|
+
{path: PWA_OFFLINE_MANIFEST_NAME, content: json(offlineManifest)},
|
|
313
|
+
{
|
|
314
|
+
path: PWA_WORKER_NAME,
|
|
315
|
+
content: createPwaWorkerScript(offlineManifest, `${runtimeBase}pwa.mjs`)
|
|
316
|
+
},
|
|
317
|
+
{path: PWA_BOOTSTRAP_NAME, content: bootstrap}
|
|
318
|
+
];
|
|
319
|
+
if (legacyAppPath) {
|
|
320
|
+
const directory = legacyAppPath.endsWith('/') ? legacyAppPath : `${legacyAppPath}/`;
|
|
321
|
+
const priorDirectory = new URL(resourceUrl('./', directory), 'https://arcane.invalid/').pathname;
|
|
322
|
+
// Existing registrations continue updating their own script and inventory URLs.
|
|
323
|
+
function priorScopeUrl(value) {
|
|
324
|
+
if (value.startsWith('/') || /^[A-Za-z][A-Za-z0-9+.-]*:/u.test(value)) return value;
|
|
325
|
+
const resolved = new URL(value, 'https://arcane.invalid/');
|
|
326
|
+
let relative = path.posix.relative(priorDirectory, resolved.pathname);
|
|
327
|
+
if (!relative) {
|
|
328
|
+
relative = resolved.pathname.endsWith('/')
|
|
329
|
+
? './'
|
|
330
|
+
: `../${path.posix.basename(resolved.pathname)}`;
|
|
331
|
+
}
|
|
332
|
+
else {
|
|
333
|
+
if (!relative.startsWith('.')) relative = `./${relative}`;
|
|
334
|
+
if (resolved.pathname.endsWith('/') && !relative.endsWith('/')) relative += '/';
|
|
335
|
+
}
|
|
336
|
+
return `${relative}${resolved.search}${resolved.hash}`;
|
|
337
|
+
}
|
|
338
|
+
const legacyOfflineManifest = {
|
|
339
|
+
...offlineManifest,
|
|
340
|
+
assets: [...new Set([
|
|
341
|
+
...offlineManifest.assets.map(priorScopeUrl),
|
|
342
|
+
`./${PWA_OFFLINE_MANIFEST_NAME}`
|
|
343
|
+
])],
|
|
344
|
+
navigationAliases: Object.fromEntries(
|
|
345
|
+
Object.entries(offlineManifest.navigationAliases).map(
|
|
346
|
+
function priorScopeNavigation([alias, target]) {
|
|
347
|
+
return [priorScopeUrl(alias), priorScopeUrl(target)];
|
|
348
|
+
}
|
|
349
|
+
)
|
|
350
|
+
)
|
|
351
|
+
};
|
|
352
|
+
generatedFiles.push(
|
|
353
|
+
{
|
|
354
|
+
path: `${directory}${PWA_WORKER_NAME}`,
|
|
355
|
+
content: createPwaWorkerScript(legacyOfflineManifest, priorScopeUrl(`${runtimeBase}pwa.mjs`))
|
|
356
|
+
},
|
|
357
|
+
{path: `${directory}${PWA_OFFLINE_MANIFEST_NAME}`, content: json(legacyOfflineManifest)}
|
|
358
|
+
);
|
|
359
|
+
}
|
|
307
360
|
return {
|
|
308
361
|
manifest,
|
|
309
362
|
offlineManifest,
|
|
310
|
-
files:
|
|
311
|
-
{path: PWA_MANIFEST_NAME, content: json(manifest)},
|
|
312
|
-
{path: PWA_OFFLINE_MANIFEST_NAME, content: json(offlineManifest)},
|
|
313
|
-
{
|
|
314
|
-
path: PWA_WORKER_NAME,
|
|
315
|
-
content: createPwaWorkerScript(offlineManifest, `${runtimeBase}pwa.mjs`)
|
|
316
|
-
},
|
|
317
|
-
{path: PWA_BOOTSTRAP_NAME, content: bootstrap}
|
|
318
|
-
],
|
|
363
|
+
files: generatedFiles,
|
|
319
364
|
entryAssets,
|
|
320
365
|
entryMarkup: `<link rel="manifest" href="${htmlAttribute(manifestHref)}">\n`
|
|
321
366
|
+ `<script type="module" async data-arcane-pwa src="${htmlAttribute(bootstrapHref)}"></script>\n`
|
package/src/scaffold.mjs
CHANGED
|
@@ -8,6 +8,7 @@ import {withWorkspaceOperationLock} from './workspace-operation-lock.mjs';
|
|
|
8
8
|
import {SDK_NAME,SDK_VERSION,workspaceTemplate} from './templates/workspace-template.mjs';
|
|
9
9
|
import {inspectWorkspaceProfile,resolveSdkPackageDeclaration} from './workspace.mjs';
|
|
10
10
|
import {parseSemver} from './packager/core.mjs';
|
|
11
|
+
import {resolveAppRoot} from './app-layout.mjs';
|
|
11
12
|
|
|
12
13
|
const is = new Is(false);
|
|
13
14
|
|
|
@@ -310,6 +311,7 @@ async function runGitInit(workspaceRoot,signal,onEvent){
|
|
|
310
311
|
export async function createWorkspace({
|
|
311
312
|
targetPath,
|
|
312
313
|
appId,
|
|
314
|
+
appsRoot='apps',
|
|
313
315
|
displayName,
|
|
314
316
|
target='browser',
|
|
315
317
|
initializeGit=false,
|
|
@@ -318,6 +320,7 @@ export async function createWorkspace({
|
|
|
318
320
|
}){
|
|
319
321
|
validateInputs(appId,displayName);
|
|
320
322
|
validateScaffoldTarget(target);
|
|
323
|
+
if(!['apps','.'].includes(appsRoot))fail('appsRoot must be apps or .','ARCANE_USAGE');
|
|
321
324
|
if(!is.string(targetPath)||!targetPath.trim())fail('targetPath is required.','ARCANE_USAGE');
|
|
322
325
|
if(!is.boolean(initializeGit))fail('initializeGit must be a boolean.','ARCANE_USAGE');
|
|
323
326
|
throwIfAborted(signal);
|
|
@@ -332,20 +335,21 @@ export async function createWorkspace({
|
|
|
332
335
|
},async workspaceOperationLease=>{
|
|
333
336
|
const template=workspaceTemplate({
|
|
334
337
|
appId,
|
|
338
|
+
appsRoot,
|
|
335
339
|
displayName,
|
|
336
340
|
target,
|
|
337
341
|
appIcon:await scaffoldIcon(target)
|
|
338
342
|
});
|
|
339
343
|
const writtenFiles=await writeMissingFiles(workspaceRoot,template.files,{signal,onEvent});
|
|
340
|
-
const workspaceRuntime=await materializeWorkspaceRuntimeContent({
|
|
344
|
+
const workspaceRuntime=appsRoot==='.'?null:await materializeWorkspaceRuntimeContent({
|
|
341
345
|
workspaceRoot,
|
|
342
346
|
signal,
|
|
343
347
|
onEvent
|
|
344
348
|
});
|
|
345
|
-
const importMap=await generateImportMap({
|
|
349
|
+
const importMap=appsRoot==='.'?{pending:true,reason:'sdk-install-required'}:await generateImportMap({
|
|
346
350
|
workspaceRoot,
|
|
347
351
|
appId,
|
|
348
|
-
appRoot:
|
|
352
|
+
appRoot:resolveAppRoot(workspaceRoot,{appsRoot},appId),
|
|
349
353
|
workspaceOperationLease,
|
|
350
354
|
signal,
|
|
351
355
|
onEvent
|
|
@@ -354,6 +358,7 @@ export async function createWorkspace({
|
|
|
354
358
|
const result={
|
|
355
359
|
workspaceRoot,
|
|
356
360
|
appId,
|
|
361
|
+
appsRoot,
|
|
357
362
|
displayName:template.name,
|
|
358
363
|
target,
|
|
359
364
|
...writtenFiles,
|
|
@@ -370,12 +375,13 @@ export async function createWorkspace({
|
|
|
370
375
|
export async function initWorkspace({
|
|
371
376
|
workspaceRoot=process.cwd(),
|
|
372
377
|
appId,
|
|
378
|
+
appsRoot,
|
|
373
379
|
displayName,
|
|
374
380
|
target='browser',
|
|
375
381
|
signal,
|
|
376
382
|
onEvent
|
|
377
383
|
}){
|
|
378
|
-
validateInputs(appId,displayName);
|
|
384
|
+
if(appId!==undefined)validateInputs(appId,displayName);
|
|
379
385
|
validateScaffoldTarget(target);
|
|
380
386
|
throwIfAborted(signal);
|
|
381
387
|
const resolvedRoot=path.resolve(workspaceRoot);
|
|
@@ -389,6 +395,28 @@ export async function initWorkspace({
|
|
|
389
395
|
},async workspaceOperationLease=>{
|
|
390
396
|
const profile=await existingWorkspaceProfile(resolvedRoot);
|
|
391
397
|
const workspaceMode=profile?.workspaceMode??'external';
|
|
398
|
+
const selectedAppsRoot=appsRoot??profile?.config.appsRoot??'apps';
|
|
399
|
+
if(!['apps','.'].includes(selectedAppsRoot))fail('appsRoot must be apps or .','ARCANE_USAGE');
|
|
400
|
+
if(profile&&selectedAppsRoot!==profile.config.appsRoot){
|
|
401
|
+
fail('appsRoot must match the existing workspace layout; init does not relocate an application.','ARCANE_USAGE');
|
|
402
|
+
}
|
|
403
|
+
if(workspaceMode==='integrated'&&selectedAppsRoot==='.'){
|
|
404
|
+
fail('The integrated workspace keeps applications under apps; root scaffolding selects a standalone workspace.','ARCANE_USAGE');
|
|
405
|
+
}
|
|
406
|
+
if(selectedAppsRoot==='.'){
|
|
407
|
+
let existingId;
|
|
408
|
+
try{existingId=JSON.parse(await readFile(path.join(resolvedRoot,'arcane-package.json'),'utf8')).id;}
|
|
409
|
+
catch(error){if(error?.code!=='ENOENT')throw error;}
|
|
410
|
+
if(existingId!==undefined){
|
|
411
|
+
if(appId!==undefined&&appId!==existingId){
|
|
412
|
+
fail(`The root application id is ${existingId}; init does not replace its identity.`,'ARCANE_USAGE');
|
|
413
|
+
}
|
|
414
|
+
appId=existingId;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
appId??=path.basename(resolvedRoot).normalize('NFKD').toLowerCase()
|
|
418
|
+
.replace(/[^a-z0-9]+/gu,'-').replace(/^-+|-+$/gu,'')||'arcane-app';
|
|
419
|
+
validateInputs(appId,displayName);
|
|
392
420
|
const existingPackage=workspaceMode==='external'
|
|
393
421
|
?await readExistingPackage(resolvedRoot)
|
|
394
422
|
:null;
|
|
@@ -415,6 +443,7 @@ export async function initWorkspace({
|
|
|
415
443
|
})
|
|
416
444
|
:workspaceTemplate({
|
|
417
445
|
appId,
|
|
446
|
+
appsRoot:selectedAppsRoot,
|
|
418
447
|
displayName,
|
|
419
448
|
sdkDependencyName:sdkDeclaration.dependencyName,
|
|
420
449
|
sdkDependencySpecifier:sdkDeclaration.specifier,
|
|
@@ -451,17 +480,23 @@ export async function initWorkspace({
|
|
|
451
480
|
await emit(onEvent,{type:'scaffold.file.replaced',path:'arcane.lock.json'});
|
|
452
481
|
}
|
|
453
482
|
const packageUpdated=await applyPackageMerge(resolvedRoot,packagePlan,{signal,onEvent});
|
|
454
|
-
const
|
|
483
|
+
const directRuntime=workspaceMode==='external'&&selectedAppsRoot==='.';
|
|
484
|
+
const workspaceRuntime=workspaceMode==='external'&&!directRuntime
|
|
455
485
|
?await materializeWorkspaceRuntimeContent({
|
|
456
486
|
workspaceRoot:resolvedRoot,
|
|
457
487
|
signal,
|
|
458
488
|
onEvent
|
|
459
489
|
})
|
|
460
490
|
:null;
|
|
461
|
-
|
|
491
|
+
let sdkInstalled=true;
|
|
492
|
+
if(directRuntime){
|
|
493
|
+
try{await lstat(path.join(resolvedRoot,sdkDeclaration.packageSource,'package.json'));}
|
|
494
|
+
catch(error){if(error?.code==='ENOENT')sdkInstalled=false;else throw error;}
|
|
495
|
+
}
|
|
496
|
+
const importMap=!sdkInstalled?{pending:true,reason:'sdk-install-required'}:await generateImportMap({
|
|
462
497
|
workspaceRoot:resolvedRoot,
|
|
463
498
|
appId,
|
|
464
|
-
appRoot:
|
|
499
|
+
appRoot:resolveAppRoot(resolvedRoot,{appsRoot:selectedAppsRoot},appId),
|
|
465
500
|
workspaceOperationLease,
|
|
466
501
|
signal,
|
|
467
502
|
onEvent
|
|
@@ -470,6 +505,7 @@ export async function initWorkspace({
|
|
|
470
505
|
workspaceRoot:resolvedRoot,
|
|
471
506
|
workspaceMode,
|
|
472
507
|
appId,
|
|
508
|
+
appsRoot:selectedAppsRoot,
|
|
473
509
|
displayName:template.name,
|
|
474
510
|
target,
|
|
475
511
|
...writtenFiles,
|
|
@@ -4,9 +4,9 @@ import path from 'node:path';
|
|
|
4
4
|
|
|
5
5
|
const is=new Is(false);
|
|
6
6
|
|
|
7
|
-
//
|
|
8
|
-
export function installedSdkRoutes(packageSource,{security=false}={}){
|
|
9
|
-
|
|
7
|
+
// Direct routes use the installed npm paths in both the browser and selected output.
|
|
8
|
+
export function installedSdkRoutes(packageSource,{security=false,direct=false}={}){
|
|
9
|
+
const routes=[
|
|
10
10
|
{
|
|
11
11
|
source:`${packageSource}/runtime/arcane`,destination:'arcane',
|
|
12
12
|
include:['components','css','entities','img','modules',...(security?['security']:[])],exclude:[]
|
|
@@ -24,6 +24,7 @@ export function installedSdkRoutes(packageSource,{security=false}={}){
|
|
|
24
24
|
include:['LICENSE','COMMERCIAL-LICENSE.md','NOTICE'],exclude:[]
|
|
25
25
|
}
|
|
26
26
|
];
|
|
27
|
+
return direct?routes.map(route=>({...route,destination:route.source})):routes;
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
export function installedSdkPackageSource(config){
|
|
@@ -31,7 +32,10 @@ export function installedSdkPackageSource(config){
|
|
|
31
32
|
if(!is.array(routes)||routes.length!==4)return null;
|
|
32
33
|
const source=routes[3]?.source;
|
|
33
34
|
if(!is.string(source)||!/^node_modules\/(?:@[a-z0-9._-]+\/)?[a-z0-9][a-z0-9._-]*$/u.test(source))return null;
|
|
34
|
-
const expected=installedSdkRoutes(source,{
|
|
35
|
+
const expected=installedSdkRoutes(source,{
|
|
36
|
+
security:routes[0]?.include?.at(-1)==='security',
|
|
37
|
+
direct:routes[0]?.destination===routes[0]?.source
|
|
38
|
+
});
|
|
35
39
|
return routes.every(function matchesInstalledRoute(route,index){
|
|
36
40
|
const wanted=expected[index];
|
|
37
41
|
return route?.source===wanted.source&&route?.destination===wanted.destination
|
|
@@ -56,7 +60,13 @@ export async function readInstalledSdkLayout(workspaceRoot,config){
|
|
|
56
60
|
error.code='ARCANE_WORKSPACE_INVALID';
|
|
57
61
|
throw error;
|
|
58
62
|
}
|
|
59
|
-
|
|
63
|
+
const routes=config.sharedPayloads['browser-runtime'];
|
|
64
|
+
return {
|
|
65
|
+
packageSource,packageRoot,versionPath,version:manifest.version,routes,
|
|
66
|
+
direct:routes[0].destination===routes[0].source,
|
|
67
|
+
runtimeBase:`/${routes[0].destination}/`,
|
|
68
|
+
browserRuntimeBase:`/${routes[1].destination}/`
|
|
69
|
+
};
|
|
60
70
|
}
|
|
61
71
|
|
|
62
72
|
export async function installedRuntimeFiles(workspaceRoot,layout,signal){
|
|
@@ -71,9 +81,9 @@ export async function installedRuntimeFiles(workspaceRoot,layout,signal){
|
|
|
71
81
|
else if(entry.isFile())files.push(relative);
|
|
72
82
|
}
|
|
73
83
|
}
|
|
74
|
-
for(const route of layout.routes){
|
|
75
|
-
if(
|
|
76
|
-
const prefix=
|
|
84
|
+
for(const [index,route] of layout.routes.entries()){
|
|
85
|
+
if(index===3)continue;
|
|
86
|
+
const prefix=['','sdk','dependencies/strong-type'][index];
|
|
77
87
|
for(const selected of route.include){
|
|
78
88
|
const suffix=selected==='.'?'':selected;
|
|
79
89
|
await visit(
|
|
@@ -85,15 +95,18 @@ export async function installedRuntimeFiles(workspaceRoot,layout,signal){
|
|
|
85
95
|
return {files:files.sort()};
|
|
86
96
|
}
|
|
87
97
|
|
|
88
|
-
export function installedRuntimeTarget(relative,layout){
|
|
89
|
-
|
|
90
|
-
|
|
98
|
+
export function installedRuntimeTarget(relative,layout,{browser=false}={}){
|
|
99
|
+
const logicalRoots=['arcane','arcane/sdk','arcane/dependencies/strong-type','licenses/arcane-os'];
|
|
100
|
+
const routes=layout.routes.map((route,index)=>({...route,logical:logicalRoots[index]}));
|
|
101
|
+
for(const route of routes.sort(function longestDestinationFirst(left,right){
|
|
102
|
+
return right.logical.length-left.logical.length;
|
|
91
103
|
})){
|
|
92
|
-
if(!relative.startsWith(`${route.
|
|
93
|
-
const suffix=relative.slice(route.
|
|
104
|
+
if(!relative.startsWith(`${route.logical}/`))continue;
|
|
105
|
+
const suffix=relative.slice(route.logical.length+1);
|
|
94
106
|
if(route.include.some(function includesRuntimeTarget(selected){
|
|
95
|
-
|
|
96
|
-
|
|
107
|
+
const pathname=suffix.split(/[?#]/u)[0];
|
|
108
|
+
return selected==='.'||pathname===selected||pathname.startsWith(`${selected}/`);
|
|
109
|
+
}))return `${browser?route.destination:route.source}/${suffix}`;
|
|
97
110
|
}
|
|
98
111
|
return relative;
|
|
99
112
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import Is from 'strong-type';
|
|
2
|
+
import {installedSdkRoutes} from '../sdk-runtime-layout.mjs';
|
|
2
3
|
import {
|
|
3
4
|
ARCANE_PROTOCOL,
|
|
4
5
|
CLI_EVENT_PROTOCOL,
|
|
@@ -59,6 +60,7 @@ export function createWorkspaceLockDocument({
|
|
|
59
60
|
|
|
60
61
|
export function workspaceTemplate({
|
|
61
62
|
appId,
|
|
63
|
+
appsRoot='apps',
|
|
62
64
|
displayName,
|
|
63
65
|
sdkDependencyName=SDK_NAME,
|
|
64
66
|
sdkDependencySpecifier=SDK_VERSION,
|
|
@@ -86,6 +88,12 @@ export function workspaceTemplate({
|
|
|
86
88
|
if(!supportedTargets.includes(target)){
|
|
87
89
|
throw new Error(`Unsupported scaffold target: ${String(target)}.`);
|
|
88
90
|
}
|
|
91
|
+
if(!['apps','.'].includes(appsRoot))throw new Error('appsRoot must be apps or .');
|
|
92
|
+
if(appOnly&&appsRoot==='.')throw new Error('Root scaffolding selects a standalone workspace.');
|
|
93
|
+
const appPrefix=appsRoot==='.'?'':`apps/${appId}/`;
|
|
94
|
+
const directRuntime=appsRoot==='.'&&!appOnly;
|
|
95
|
+
const runtimePrefix=directRuntime?`./${sdkPackageSource}/runtime/arcane`:'./arcane';
|
|
96
|
+
const baseHref=appsRoot==='.'?'./':'../../';
|
|
89
97
|
const native=target!=='browser';
|
|
90
98
|
const buildTarget=native?target:'browser';
|
|
91
99
|
const runTarget=native&&target!=='portable'?target:'browser';
|
|
@@ -125,7 +133,7 @@ appropriate.
|
|
|
125
133
|
</script>
|
|
126
134
|
`:'';
|
|
127
135
|
const bootstrapMarkup=namedImports?'':
|
|
128
|
-
|
|
136
|
+
` <script type="module" src="${runtimePrefix}/modules/ThemeBootstrap.js?v=1"></script>\n`;
|
|
129
137
|
const files=new Map();
|
|
130
138
|
files.set('.gitignore',[
|
|
131
139
|
'node_modules/',
|
|
@@ -150,8 +158,8 @@ appropriate.
|
|
|
150
158
|
files.set('AGENTS.md',`# ${name} development instructions
|
|
151
159
|
|
|
152
160
|
- Use plain JavaScript, HTML, and CSS; do not introduce TypeScript or TSX.
|
|
153
|
-
- Keep reusable portable mechanisms in the Arcane SDK and app-specific behavior under
|
|
154
|
-
- Keep
|
|
161
|
+
- Keep reusable portable mechanisms in the Arcane SDK and app-specific behavior under \`${appPrefix||'./'}\`.
|
|
162
|
+
- Keep \`${runtimePrefix}/css/theme.css\` before app styles and import \`arcane/ThemeBootstrap\` before app code runs.
|
|
155
163
|
- Use \`rgb(...)\` or \`rgba(...)\` for new CSS colors.
|
|
156
164
|
- Build one named app and one explicit target at a time. Native targets may be unavailable until their adapters are installed.
|
|
157
165
|
- Preserve complete application, model, document, message, log, diagnostic, process, and tool content. Do not truncate, clip, tail, elide, or silently discard it.
|
|
@@ -167,10 +175,11 @@ This repository contains the portable Arcane application \`${appId}\`. It includ
|
|
|
167
175
|
|
|
168
176
|
\`\`\`sh
|
|
169
177
|
npm install
|
|
178
|
+
npm run import-map
|
|
170
179
|
npm run dev
|
|
171
180
|
\`\`\`
|
|
172
181
|
|
|
173
|
-
Open the loopback URL printed by the development server.
|
|
182
|
+
Open the loopback URL printed by the development server. ${directRuntime?'This root app reads SDK files directly from its installed npm package; an ordinary static host uses the same resource paths.':'This app uses the existing physical arcane/ runtime layout.'} The SDK server does not expose an Ollama HTTP endpoint.
|
|
174
183
|
|
|
175
184
|
Commit the generated \`package-lock.json\` after dependency installation. CI intentionally uses \`npm ci\` and therefore requires that lock. Before the SDK is published, install a locally packed \`${SDK_NAME}\` \`.tgz\` with \`npm install --save-dev --save-exact <path-to-tarball>\`; keep that tarball at the lock file's relative path for repeatable local \`npm ci\` runs.
|
|
176
185
|
|
|
@@ -185,13 +194,12 @@ npm run run
|
|
|
185
194
|
\`\`\`
|
|
186
195
|
|
|
187
196
|
The explicit \`import-map\` command refreshes
|
|
188
|
-
|
|
197
|
+
\`${appPrefix}modules/arcane.importmap.json\` and the managed inline browser
|
|
189
198
|
import map in every directly navigable descriptor-admitted \`.html\`/\`.htm\`
|
|
190
199
|
document. HTML component fragments remain package files but do not receive a
|
|
191
200
|
document-level base or managed import map.
|
|
192
201
|
Development, package, and build refresh that shared inventory when the selected operation needs it.
|
|
193
|
-
Named \`arcane/*\` imports resolve
|
|
194
|
-
tree. Packaging copies the complete selected application, runtime, and specifier
|
|
202
|
+
Named \`arcane/*\` imports resolve through the managed map to the selected SDK files. Packaging copies the complete selected application, runtime, and specifier
|
|
195
203
|
map to \`dist/${appId}\` without running application tests. Run \`verify\` only when
|
|
196
204
|
the user explicitly selects verification or a release artifact that requires it;
|
|
197
205
|
\`bundle\` creates the distributable archive and \`run\` launches the selected
|
|
@@ -201,7 +209,7 @@ Native targets are provider-supplied and must be scaffolded and selected
|
|
|
201
209
|
explicitly; this browser workflow does not imply a standalone native executable.
|
|
202
210
|
${nativeGuide}
|
|
203
211
|
|
|
204
|
-
Every browser release also carries Arcane OS licensing material under
|
|
212
|
+
Every browser release also carries Arcane OS licensing material under \`${directRuntime?sdkPackageSource:'licenses/arcane-os'}\`. Review those terms before distribution.
|
|
205
213
|
`);
|
|
206
214
|
files.set('package.json',json({
|
|
207
215
|
name:packageName,
|
|
@@ -230,10 +238,10 @@ Every browser release also carries Arcane OS licensing material under \`licenses
|
|
|
230
238
|
}));
|
|
231
239
|
files.set('arcane-packager.json',json({
|
|
232
240
|
schemaVersion:1,
|
|
233
|
-
appsRoot
|
|
241
|
+
appsRoot,
|
|
234
242
|
distRoot:'dist',
|
|
235
243
|
sharedPayloads:{
|
|
236
|
-
'browser-runtime':[
|
|
244
|
+
'browser-runtime':directRuntime?installedSdkRoutes(sdkPackageSource,{direct:true}):[
|
|
237
245
|
{
|
|
238
246
|
source:'arcane',
|
|
239
247
|
destination:'arcane',
|
|
@@ -249,11 +257,11 @@ Every browser release also carries Arcane OS licensing material under \`licenses
|
|
|
249
257
|
]
|
|
250
258
|
}
|
|
251
259
|
}));
|
|
252
|
-
files.set('arcane.lock.json',json(createWorkspaceLockDocument({
|
|
260
|
+
if(!directRuntime)files.set('arcane.lock.json',json(createWorkspaceLockDocument({
|
|
253
261
|
dependencyName:sdkDependencyName,
|
|
254
262
|
packageSource:sdkPackageSource
|
|
255
263
|
})));
|
|
256
|
-
files.set(
|
|
264
|
+
files.set(`${appPrefix}arcane-app.json`,json({
|
|
257
265
|
schemaVersion:2,
|
|
258
266
|
id:appId,
|
|
259
267
|
displayName:name,
|
|
@@ -283,7 +291,7 @@ Every browser release also carries Arcane OS licensing material under \`licenses
|
|
|
283
291
|
},
|
|
284
292
|
targets:native?['browser',target].sort():['browser']
|
|
285
293
|
}));
|
|
286
|
-
files.set(
|
|
294
|
+
files.set(`${appPrefix}arcane-package.json`,json({
|
|
287
295
|
schemaVersion:1,
|
|
288
296
|
id:appId,
|
|
289
297
|
displayName:name,
|
|
@@ -294,7 +302,7 @@ Every browser release also carries Arcane OS licensing material under \`licenses
|
|
|
294
302
|
exclude:[],
|
|
295
303
|
shared:['browser-runtime']
|
|
296
304
|
}));
|
|
297
|
-
files.set(
|
|
305
|
+
files.set(`${appPrefix}manifest.json`,json({
|
|
298
306
|
name,
|
|
299
307
|
short_name:titleCase(appId),
|
|
300
308
|
start_url:'./index.html',
|
|
@@ -303,19 +311,19 @@ Every browser release also carries Arcane OS licensing material under \`licenses
|
|
|
303
311
|
theme_color:'rgb(23, 34, 56)',
|
|
304
312
|
icons:[]
|
|
305
313
|
}));
|
|
306
|
-
files.set(
|
|
314
|
+
files.set(`${appPrefix}index.html`,`<!doctype html>
|
|
307
315
|
<html lang="en">
|
|
308
316
|
<head>
|
|
309
317
|
<meta charset="utf-8">
|
|
310
318
|
<meta name="arcane-app-id" content="${html(appId)}">
|
|
311
|
-
<base href="
|
|
319
|
+
<base href="${baseHref}">
|
|
312
320
|
${importMapMarkup} <meta name="viewport" content="width=device-width, initial-scale=1">
|
|
313
321
|
<meta name="theme-color" content="rgb(23, 34, 56)">
|
|
314
322
|
<title>${html(name)}</title>
|
|
315
|
-
<link rel="manifest" href="
|
|
316
|
-
<link rel="stylesheet" href="
|
|
317
|
-
<link rel="stylesheet" href="
|
|
318
|
-
<link rel="stylesheet" href="
|
|
323
|
+
<link rel="manifest" href="./${html(appPrefix)}manifest.json">
|
|
324
|
+
<link rel="stylesheet" href="${runtimePrefix}/css/theme.css?v=1">
|
|
325
|
+
<link rel="stylesheet" href="${runtimePrefix}/css/primitives.css?v=1">
|
|
326
|
+
<link rel="stylesheet" href="./${html(appPrefix)}${html(appId)}.css?v=1">
|
|
319
327
|
${bootstrapMarkup}</head>
|
|
320
328
|
<body>
|
|
321
329
|
<main class="app-shell">
|
|
@@ -332,11 +340,11 @@ ${bootstrapMarkup}</head>
|
|
|
332
340
|
</div>
|
|
333
341
|
</section>
|
|
334
342
|
</main>
|
|
335
|
-
<script type="module" src="
|
|
343
|
+
<script type="module" src="./${html(appPrefix)}modules/App.js?v=1"></script>
|
|
336
344
|
</body>
|
|
337
345
|
</html>
|
|
338
346
|
`);
|
|
339
|
-
files.set(
|
|
347
|
+
files.set(`${appPrefix}${appId}.css`,`body {
|
|
340
348
|
margin: 0;
|
|
341
349
|
min-height: 100vh;
|
|
342
350
|
background: var(--background, rgb(13, 18, 32));
|
|
@@ -358,15 +366,15 @@ ${bootstrapMarkup}</head>
|
|
|
358
366
|
}
|
|
359
367
|
`);
|
|
360
368
|
if(namedImports){
|
|
361
|
-
files.set(
|
|
369
|
+
files.set(`${appPrefix}modules/arcane.importmap.json`,json({imports:{}}));
|
|
362
370
|
}
|
|
363
371
|
const themeSpecifier=namedImports
|
|
364
|
-
?'arcane/ThemeBootstrap':'../../../
|
|
372
|
+
?'arcane/ThemeBootstrap':`${appsRoot==='.'?'../':'../../../'}${runtimePrefix.slice(2)}/modules/ThemeBootstrap.js`;
|
|
365
373
|
const appDataSpecifier=namedImports
|
|
366
|
-
?'arcane/AppDataScope':'../../../
|
|
374
|
+
?'arcane/AppDataScope':`${appsRoot==='.'?'../':'../../../'}${runtimePrefix.slice(2)}/modules/AppDataScope.js`;
|
|
367
375
|
const strongTypeSpecifier=namedImports
|
|
368
|
-
?'strong-type':'../../../arcane/dependencies/strong-type/index.js';
|
|
369
|
-
files.set(
|
|
376
|
+
?'strong-type':directRuntime?`../${sdkPackageSource}/runtime/strong-type/index.js`:'../../../arcane/dependencies/strong-type/index.js';
|
|
377
|
+
files.set(`${appPrefix}modules/App.js`,`import Is from '${strongTypeSpecifier}';
|
|
370
378
|
import arcaneThemeReady from '${themeSpecifier}';
|
|
371
379
|
import {
|
|
372
380
|
resolveApplicationId,
|
|
@@ -407,9 +415,9 @@ action?.addEventListener('click',()=>{
|
|
|
407
415
|
});
|
|
408
416
|
`);
|
|
409
417
|
if(native){
|
|
410
|
-
files.set(
|
|
418
|
+
files.set(`${appPrefix}img/icon.png`,Buffer.from(appIcon));
|
|
411
419
|
}
|
|
412
|
-
files.set(
|
|
420
|
+
files.set(`${appPrefix}test/app.test.mjs`,`import assert from 'node:assert/strict';
|
|
413
421
|
import {readFile} from 'node:fs/promises';
|
|
414
422
|
import test from '${SDK_NAME}/testing';
|
|
415
423
|
|
|
@@ -420,13 +428,13 @@ test('application shell uses the shared Arcane theme in order',async()=>{
|
|
|
420
428
|
readFile(new URL('index.html',appRoot),'utf8'),
|
|
421
429
|
readFile(new URL('modules/App.js',appRoot),'utf8')
|
|
422
430
|
]);
|
|
423
|
-
const theme=source.indexOf('
|
|
424
|
-
const primitives=source.indexOf('
|
|
425
|
-
const appStyle=source.indexOf('
|
|
426
|
-
const importMap=source.indexOf('${namedImports?'data-arcane-import-map'
|
|
427
|
-
const appModule=source.indexOf('
|
|
431
|
+
const theme=source.indexOf('${runtimePrefix}/css/theme.css');
|
|
432
|
+
const primitives=source.indexOf('${runtimePrefix}/css/primitives.css');
|
|
433
|
+
const appStyle=source.indexOf('./${appPrefix}${appId}.css');
|
|
434
|
+
const importMap=source.indexOf('${namedImports?'data-arcane-import-map':`${runtimePrefix}/modules/ThemeBootstrap.js`}');
|
|
435
|
+
const appModule=source.indexOf('./${appPrefix}modules/App.js');
|
|
428
436
|
|
|
429
|
-
assert.
|
|
437
|
+
assert.ok(source.includes('<base href="${baseHref}">'));
|
|
430
438
|
assert.match(source,/<meta name="arcane-app-id" content="${appId}">/);
|
|
431
439
|
assert.ok(theme>=0&&primitives>theme&&appStyle>primitives);
|
|
432
440
|
assert.ok(importMap>=0&&appModule>importMap);
|
|
@@ -445,7 +453,7 @@ test('application package identity matches its directory',async()=>{
|
|
|
445
453
|
if(appOnly){
|
|
446
454
|
return {
|
|
447
455
|
name,
|
|
448
|
-
files:new Map([...files].filter(([relative])=>relative.startsWith(
|
|
456
|
+
files:new Map([...files].filter(([relative])=>relative.startsWith(`${appPrefix}`)))
|
|
449
457
|
};
|
|
450
458
|
}
|
|
451
459
|
return {name,files};
|
package/src/toolchain.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import Is from 'strong-type';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import {readdir,lstat,realpath} from 'node:fs/promises';
|
|
3
|
+
import {readdir,lstat,realpath,mkdir,readFile,writeFile} from 'node:fs/promises';
|
|
4
4
|
import {createWorkspace,initWorkspace} from './scaffold.mjs';
|
|
5
5
|
import {
|
|
6
6
|
discoverApps as discoverWorkspaceApps,
|
|
@@ -11,7 +11,10 @@ import {
|
|
|
11
11
|
} from './workspace.mjs';
|
|
12
12
|
import {loadArcaneIntegratedProvider} from './integrated-provider-loader.mjs';
|
|
13
13
|
import {startDevServer} from './dev-server.mjs';
|
|
14
|
-
import {generateImportMap,readApplicationTestImportMapContext} from './import-map.mjs';
|
|
14
|
+
import {applyPwaEntryReferences,generateImportMap,readApplicationTestImportMapContext} from './import-map.mjs';
|
|
15
|
+
import {rootAppNavigation} from './app-layout.mjs';
|
|
16
|
+
import {createPwaArtifacts} from './pwa.mjs';
|
|
17
|
+
import {readInstalledSdkLayout} from './sdk-runtime-layout.mjs';
|
|
15
18
|
import {withWorkspaceOperationLock} from './workspace-operation-lock.mjs';
|
|
16
19
|
import {refreshAppPackageProjection} from './app-descriptor.mjs';
|
|
17
20
|
import {
|
|
@@ -303,7 +306,7 @@ async function refreshPreparedImportMap(prepared,{signal,onEvent,workspaceOperat
|
|
|
303
306
|
'The selected application package descriptor changed before import-map refresh.'
|
|
304
307
|
);
|
|
305
308
|
}
|
|
306
|
-
|
|
309
|
+
const importMap=await generateImportMap({
|
|
307
310
|
workspaceRoot:prepared.workspaceRoot,
|
|
308
311
|
appId:prepared.appId,
|
|
309
312
|
appRoot:prepared.appRoot,
|
|
@@ -315,6 +318,76 @@ async function refreshPreparedImportMap(prepared,{signal,onEvent,workspaceOperat
|
|
|
315
318
|
signal,
|
|
316
319
|
onEvent
|
|
317
320
|
});
|
|
321
|
+
if(prepared.validation.config.appsRoot==='.'){
|
|
322
|
+
await refreshRootApplicationFiles(prepared,inspected,importMap,{signal,onEvent});
|
|
323
|
+
}
|
|
324
|
+
return importMap;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
async function refreshRootApplicationFiles(prepared,inspected,importMap,{signal,onEvent}){
|
|
328
|
+
const {workspaceRoot,appId}=prepared;
|
|
329
|
+
const manifest=prepared.validation.app.manifest;
|
|
330
|
+
const navigation=rootAppNavigation(appId,manifest.entry,inspected.browserDocuments.map(document=>document.path));
|
|
331
|
+
// These aliases belong to the SDK only after generation; retained app files stay authored.
|
|
332
|
+
for(const redirect of navigation){
|
|
333
|
+
throwIfAborted(signal);
|
|
334
|
+
try{
|
|
335
|
+
const current=await readFile(path.join(workspaceRoot,...redirect.path.split('/')),'utf8');
|
|
336
|
+
if(!current.includes('<!-- Arcane root application navigation -->')){
|
|
337
|
+
throw new ArcaneError(ERROR_CODES.workspaceInvalid,
|
|
338
|
+
`Root application navigation would replace authored content: ${redirect.path}.`);
|
|
339
|
+
}
|
|
340
|
+
}catch(error){if(error.code!=='ENOENT')throw error;}
|
|
341
|
+
}
|
|
342
|
+
const installed=await readInstalledSdkLayout(workspaceRoot,prepared.validation.config);
|
|
343
|
+
const entry=`/${manifest.entry.split('/').map(encodeURIComponent).join('/')}`;
|
|
344
|
+
const navigationAliases={
|
|
345
|
+
'/':entry,
|
|
346
|
+
[`/apps/${appId}`]:entry,
|
|
347
|
+
[`/apps/${appId}/`]:entry,
|
|
348
|
+
...Object.fromEntries(navigation.map(redirect=>[`/${redirect.path}`,redirect.target]))
|
|
349
|
+
};
|
|
350
|
+
// The source host serves the installed files in place. There is no runtime projection.
|
|
351
|
+
const files=[...new Set([
|
|
352
|
+
...inspected.files.filter(file=>file!=='index.html'||manifest.include.includes('index.html')),
|
|
353
|
+
importMap.artifactRelativePath,
|
|
354
|
+
...navigation.map(redirect=>redirect.path)
|
|
355
|
+
])];
|
|
356
|
+
const pwa=installed?.direct&&manifest.pwa?.enabled?createPwaArtifacts({
|
|
357
|
+
app:{id:appId,displayName:manifest.displayName,version:manifest.version,entry},
|
|
358
|
+
sdkVersion:installed.version,
|
|
359
|
+
pwa:manifest.pwa,
|
|
360
|
+
files,
|
|
361
|
+
basePath:'/',
|
|
362
|
+
appBase:'/',
|
|
363
|
+
installationId:`/apps/${appId}/`,
|
|
364
|
+
legacyAppPath:`apps/${appId}`,
|
|
365
|
+
runtimeBase:installed.browserRuntimeBase,
|
|
366
|
+
mode:'development',
|
|
367
|
+
navigationAliases
|
|
368
|
+
}):null;
|
|
369
|
+
for(const file of [...navigation,...(pwa?.files??[])]){
|
|
370
|
+
throwIfAborted(signal);
|
|
371
|
+
const filePath=path.join(workspaceRoot,...file.path.split('/'));
|
|
372
|
+
await mkdir(path.dirname(filePath),{recursive:true});
|
|
373
|
+
await writeFile(filePath,file.content,'utf8');
|
|
374
|
+
}
|
|
375
|
+
if(pwa){
|
|
376
|
+
for(const document of inspected.browserDocuments){
|
|
377
|
+
throwIfAborted(signal);
|
|
378
|
+
const filePath=path.join(prepared.appRoot,...document.path.split('/'));
|
|
379
|
+
const content=await readFile(filePath,'utf8');
|
|
380
|
+
await writeFile(filePath,applyPwaEntryReferences(content,{
|
|
381
|
+
manifestUrl:`/${pwa.entryAssets.manifest}`,
|
|
382
|
+
bootstrapUrl:`/${pwa.entryAssets.bootstrap}`
|
|
383
|
+
}),'utf8');
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
await emit(onEvent,{
|
|
387
|
+
type:'import-map.root-files.completed',appId,
|
|
388
|
+
navigation:navigation.map(redirect=>redirect.path),
|
|
389
|
+
pwa:pwa?.files.map(file=>file.path)??[]
|
|
390
|
+
});
|
|
318
391
|
}
|
|
319
392
|
|
|
320
393
|
async function importMapApplication(options={}){
|