arcane-os 0.25.0 → 0.27.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 +31 -0
- package/README.md +16 -2
- package/browser-runtime/pwa-install.mjs +3 -3
- package/docs/architecture.md +33 -12
- package/docs/reference/cli.md +39 -11
- package/docs/reference/protocols.md +94 -4
- package/docs/reference/pwa.md +21 -0
- package/docs/reference/sdk-api.md +53 -13
- package/package.json +17 -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/runtime/arcane/modules/MailApi.mjs +19 -0
- 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 +86 -24
- package/src/import-map.mjs +57 -14
- package/src/mail-api.mjs +2 -19
- package/src/packager/core.mjs +84 -29
- package/src/pwa-worker.mjs +18 -4
- package/src/pwa.mjs +2 -1
- package/src/scaffold.mjs +43 -7
- package/src/sdk-runtime-layout.mjs +112 -0
- package/src/templates/workspace-template.mjs +44 -36
- package/src/toolchain.mjs +75 -3
- package/src/workspace.mjs +54 -38
|
@@ -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,75 @@ 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
|
+
runtimeBase:installed.browserRuntimeBase,
|
|
365
|
+
mode:'development',
|
|
366
|
+
navigationAliases
|
|
367
|
+
}):null;
|
|
368
|
+
for(const file of [...navigation,...(pwa?.files??[])]){
|
|
369
|
+
throwIfAborted(signal);
|
|
370
|
+
const filePath=path.join(workspaceRoot,...file.path.split('/'));
|
|
371
|
+
await mkdir(path.dirname(filePath),{recursive:true});
|
|
372
|
+
await writeFile(filePath,file.content,'utf8');
|
|
373
|
+
}
|
|
374
|
+
if(pwa){
|
|
375
|
+
for(const document of inspected.browserDocuments){
|
|
376
|
+
throwIfAborted(signal);
|
|
377
|
+
const filePath=path.join(prepared.appRoot,...document.path.split('/'));
|
|
378
|
+
const content=await readFile(filePath,'utf8');
|
|
379
|
+
await writeFile(filePath,applyPwaEntryReferences(content,{
|
|
380
|
+
manifestUrl:`/${pwa.entryAssets.manifest}`,
|
|
381
|
+
bootstrapUrl:`/${pwa.entryAssets.bootstrap}`
|
|
382
|
+
}),'utf8');
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
await emit(onEvent,{
|
|
386
|
+
type:'import-map.root-files.completed',appId,
|
|
387
|
+
navigation:navigation.map(redirect=>redirect.path),
|
|
388
|
+
pwa:pwa?.files.map(file=>file.path)??[]
|
|
389
|
+
});
|
|
318
390
|
}
|
|
319
391
|
|
|
320
392
|
async function importMapApplication(options={}){
|
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 {
|
|
@@ -9,6 +10,7 @@ import {
|
|
|
9
10
|
import {loadAppDescriptor} from './app-descriptor.mjs';
|
|
10
11
|
import {SDK_NAME as EXPECTED_SDK_NAME} from './constants.mjs';
|
|
11
12
|
import {inspectImportMapHtml} from './import-map.mjs';
|
|
13
|
+
import {installedSdkPackageSource} from './sdk-runtime-layout.mjs';
|
|
12
14
|
|
|
13
15
|
const is = new Is(false);
|
|
14
16
|
|
|
@@ -159,6 +161,13 @@ function classifyRootConfig(config){
|
|
|
159
161
|
const validated=validatePackagerRootConfig(config,ROOT_CONFIG_NAME);
|
|
160
162
|
const routes=validated.sharedPayloads['browser-runtime'];
|
|
161
163
|
if(!is.array(routes))fail(`${ROOT_CONFIG_NAME} must define browser-runtime routes.`);
|
|
164
|
+
const installedPackageSource=installedSdkPackageSource(validated);
|
|
165
|
+
if(installedPackageSource!==null){
|
|
166
|
+
return {
|
|
167
|
+
...validated,workspaceMode:'external',browserRuntimeLayout:'installed-v1',
|
|
168
|
+
sdkPackageSource:installedPackageSource
|
|
169
|
+
};
|
|
170
|
+
}
|
|
162
171
|
const externalPackageSource=routes.length===2
|
|
163
172
|
&&dependencyNameForSdkPackageSource(routes[1]?.source)!==null
|
|
164
173
|
?routes[1].source
|
|
@@ -220,6 +229,25 @@ async function discoverAppsInRoot(root,config,appId){
|
|
|
220
229
|
if(appId!==undefined&&(!is.string(appId)||!APP_ID_PATTERN.test(appId))){
|
|
221
230
|
fail(`Invalid app id: ${String(appId)}.`,'ARCANE_USAGE');
|
|
222
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
|
+
}
|
|
223
251
|
const appsRoot=path.join(root,'apps');
|
|
224
252
|
await assertRealDirectory(appsRoot,'Workspace apps root');
|
|
225
253
|
const entries=await readdir(appsRoot,{withFileTypes:true});
|
|
@@ -232,26 +260,7 @@ async function discoverAppsInRoot(root,config,appId){
|
|
|
232
260
|
if(!entry.isDirectory())continue;
|
|
233
261
|
const appRoot=path.join(appsRoot,entry.name);
|
|
234
262
|
try{
|
|
235
|
-
|
|
236
|
-
const manifest=await readJson(configPath,`apps/${entry.name}/${APP_CONFIG_NAME}`);
|
|
237
|
-
const validatedManifest=validatePackagerAppConfig(manifest,entry.name,config,configPath);
|
|
238
|
-
if(!validatedManifest.shared.includes('browser-runtime')){
|
|
239
|
-
fail(`apps/${entry.name}/${APP_CONFIG_NAME} must include the browser-runtime shared payload.`);
|
|
240
|
-
}
|
|
241
|
-
const descriptor=await loadAppDescriptor({
|
|
242
|
-
workspaceRoot:root,
|
|
243
|
-
appRoot,
|
|
244
|
-
appId:entry.name,
|
|
245
|
-
packageManifest:manifest
|
|
246
|
-
});
|
|
247
|
-
apps.push(completeValue({
|
|
248
|
-
appId:entry.name,
|
|
249
|
-
appRoot,
|
|
250
|
-
manifest:validatedManifest,
|
|
251
|
-
descriptor:descriptor.descriptor,
|
|
252
|
-
descriptorSource:descriptor.source,
|
|
253
|
-
descriptorPath:descriptor.descriptorPath
|
|
254
|
-
}));
|
|
263
|
+
apps.push(await readApp(appRoot,entry.name));
|
|
255
264
|
}catch(error){
|
|
256
265
|
if(!String(error?.message).includes('does not exist'))throw error;
|
|
257
266
|
}
|
|
@@ -286,7 +295,7 @@ export async function selectApp(workspaceRoot=process.cwd(),appId){
|
|
|
286
295
|
if(!selected)fail(`Unknown app "${appId}".`);
|
|
287
296
|
return selected;
|
|
288
297
|
}
|
|
289
|
-
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.');
|
|
290
299
|
if(apps.length>1){
|
|
291
300
|
fail(`This workspace contains multiple apps; select one explicitly: ${apps.map(app=>app.appId).join(', ')}.`,'ARCANE_USAGE');
|
|
292
301
|
}
|
|
@@ -305,7 +314,7 @@ export async function resolveWorkspace({workspaceRoot=process.cwd(),appId}={}){
|
|
|
305
314
|
}else if(apps.length===1){
|
|
306
315
|
[app]=apps;
|
|
307
316
|
}else if(apps.length===0){
|
|
308
|
-
fail('No Arcane applications were found
|
|
317
|
+
fail('No Arcane applications were found at the configured appsRoot.');
|
|
309
318
|
}else{
|
|
310
319
|
fail(`This workspace contains multiple apps; select one explicitly: ${apps.map(item=>item.appId).join(', ')}.`,'ARCANE_USAGE');
|
|
311
320
|
}
|
|
@@ -355,39 +364,43 @@ export async function resolveInstalledSdkInstallation(workspaceRoot,declaration)
|
|
|
355
364
|
|
|
356
365
|
function assertHtmlContract(source,appId,{
|
|
357
366
|
entry='index.html',
|
|
367
|
+
appPath=`apps/${appId}`,
|
|
368
|
+
baseHref='../../',
|
|
369
|
+
runtimePath='arcane',
|
|
358
370
|
strictStyles=true,
|
|
359
371
|
allowMissingManagedImportMap=false
|
|
360
372
|
}={}){
|
|
361
|
-
const entryLabel
|
|
373
|
+
const entryLabel=[appPath,entry].filter(Boolean).join('/');
|
|
362
374
|
const htmlContract=inspectImportMapHtml(source);
|
|
363
375
|
const appIdMetadata=htmlContract.metas.filter(meta=>meta.name==='arcane-app-id');
|
|
364
376
|
if(appIdMetadata.length!==1||appIdMetadata[0].content!==appId){
|
|
365
377
|
fail(`${entryLabel} must declare exactly one active matching arcane-app-id metadata element.`);
|
|
366
378
|
}
|
|
367
|
-
if(htmlContract.bases.length!==1||htmlContract.bases[0].href!==
|
|
368
|
-
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}">.`);
|
|
369
381
|
}
|
|
370
|
-
const resourcePath=value=>value.split(/[?#]/u,1)[0];
|
|
382
|
+
const resourcePath=value=>value.split(/[?#]/u,1)[0].replace(/^\.\//u,'/');
|
|
371
383
|
const styles=htmlContract.links.filter(link=>link.rel
|
|
372
384
|
.split(/[\t\n\f\r ]+/u).includes('stylesheet'));
|
|
373
385
|
const positionOfStyle=expected=>styles.find(link=>resourcePath(link.href)===expected)?.start??-1;
|
|
374
|
-
const theme=positionOfStyle(
|
|
375
|
-
const primitives=positionOfStyle(
|
|
376
|
-
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,'\\$&');
|
|
377
390
|
const appStyle=styles.find(link=>new RegExp(
|
|
378
|
-
`^(?:\\./|/)
|
|
391
|
+
`^(?:\\./|/)${escapedAppPrefix}[^/]+\\.css$`,
|
|
379
392
|
'u'
|
|
380
393
|
).test(resourcePath(link.href)))?.start??-1;
|
|
381
394
|
const modules=htmlContract.scripts.filter(script=>script.type==='module'&&script.src);
|
|
382
395
|
const bootstrap=modules.find(script=>resourcePath(script.src)
|
|
383
|
-
|
|
396
|
+
===`/${runtimePath}/modules/ThemeBootstrap.js`)?.start??-1;
|
|
384
397
|
if(htmlContract.managedMaps.length>1){
|
|
385
398
|
fail(`${entryLabel} must contain at most one active managed Arcane import map.`);
|
|
386
399
|
}
|
|
387
400
|
const managedImportMap=htmlContract.managedMaps[0]?.start??-1;
|
|
388
401
|
const firstModule=htmlContract.firstModulePosition;
|
|
389
402
|
const appModule=modules.find(script=>new RegExp(
|
|
390
|
-
`^(?:\\./|/)
|
|
403
|
+
`^(?:\\./|/)${escapedAppPrefix}(?!arcane/|node_modules/).+\\.(?:js|mjs)$`,
|
|
391
404
|
'u'
|
|
392
405
|
).test(resourcePath(script.src)))?.start??-1;
|
|
393
406
|
if(theme<0){
|
|
@@ -436,12 +449,6 @@ export async function validateDiscoveredApplication({
|
|
|
436
449
|
fail('A discovered Arcane application is required for focused validation.');
|
|
437
450
|
}
|
|
438
451
|
const canonicalWorkspaceRoot=await realpath(workspaceRoot);
|
|
439
|
-
await assertRealDirectory(path.join(canonicalWorkspaceRoot,'apps'),'Workspace apps root');
|
|
440
|
-
const expectedAppRoot=path.join(canonicalWorkspaceRoot,'apps',app.appId);
|
|
441
|
-
if(!sameDirectoryPath(app.appRoot,expectedAppRoot)){
|
|
442
|
-
fail(`Discovered app ${app.appId} does not belong to the selected workspace.`);
|
|
443
|
-
}
|
|
444
|
-
const canonicalAppRoot=await realpath(app.appRoot);
|
|
445
452
|
let config=workspaceConfig;
|
|
446
453
|
if(!config){
|
|
447
454
|
const profile=await inspectWorkspaceProfile(canonicalWorkspaceRoot);
|
|
@@ -453,6 +460,12 @@ export async function validateDiscoveredApplication({
|
|
|
453
460
|
if(!isObject(config?.sharedPayloads)){
|
|
454
461
|
fail('The selected Arcane workspace configuration is unavailable for focused validation.');
|
|
455
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);
|
|
456
469
|
const configPath=path.join(canonicalAppRoot,APP_CONFIG_NAME);
|
|
457
470
|
const rawManifest=await readJson(
|
|
458
471
|
configPath,
|
|
@@ -484,6 +497,9 @@ export async function validateDiscoveredApplication({
|
|
|
484
497
|
}
|
|
485
498
|
assertHtmlContract(await readFile(entryPath,'utf8'),app.appId,{
|
|
486
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,
|
|
487
503
|
strictStyles:workspaceMode==='external',
|
|
488
504
|
allowMissingManagedImportMap
|
|
489
505
|
});
|