arcane-os 0.1.0-dev.5 → 0.1.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/browser-runtime/ARCANE_SDK_BROWSER_RELEASE.json +86 -0
- package/browser-runtime/dependencies/event-pubsub/index.js +141 -0
- package/browser-runtime/dependencies/event-pubsub/licence +21 -0
- package/browser-runtime/dependencies/event-pubsub/package.json +59 -0
- package/browser-runtime/dependencies/strong-type/index.js +1151 -0
- package/browser-runtime/dependencies/strong-type/licence +21 -0
- package/browser-runtime/dependencies/strong-type/package.json +61 -0
- package/browser-runtime/dom-event-instrumentation.mjs +594 -0
- package/browser-runtime/event-manager.mjs +1342 -0
- package/docs/publishing.md +52 -59
- package/docs/reference/event-manager.md +5 -5
- package/docs/work-amplification.md +4 -3
- package/package.json +11 -7
- package/runtime/ARCANE_RUNTIME_RELEASE.json +1 -1
- package/schemas/arcane-lock.schema.json +86 -1
- package/src/cli/main.mjs +5 -0
- package/src/dev-server.mjs +78 -34
- package/src/doctor.mjs +77 -3
- package/src/import-map.mjs +2328 -0
- package/src/packager/core.mjs +607 -29
- package/src/scaffold.mjs +122 -5
- package/src/sdk-browser-runtime.mjs +585 -0
- package/src/targets/index.mjs +31 -4
- package/src/templates/workspace-template.mjs +135 -23
- package/src/toolchain.mjs +288 -55
- package/src/workspace-operation-lock.mjs +716 -0
- package/src/workspace-runtime.mjs +841 -0
- package/src/workspace.mjs +286 -37
package/src/workspace.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import {createHash} from 'node:crypto';
|
|
1
2
|
import {lstat,readFile,readdir,realpath} from 'node:fs/promises';
|
|
2
3
|
import path from 'node:path';
|
|
3
4
|
import {
|
|
@@ -9,6 +10,11 @@ import {
|
|
|
9
10
|
SDK_NAME as EXPECTED_SDK_NAME,
|
|
10
11
|
SDK_VERSION as EXPECTED_SDK_VERSION
|
|
11
12
|
} from './constants.mjs';
|
|
13
|
+
import {inspectImportMapHtml} from './import-map.mjs';
|
|
14
|
+
import {
|
|
15
|
+
SDK_BROWSER_RUNTIME_CONTENT_SHA256,
|
|
16
|
+
SDK_BROWSER_RUNTIME_MANIFEST_SHA256
|
|
17
|
+
} from './sdk-browser-runtime.mjs';
|
|
12
18
|
|
|
13
19
|
const APP_ID_PATTERN=/^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
|
|
14
20
|
const SHA256_PATTERN=/^[a-f0-9]{64}$/;
|
|
@@ -65,20 +71,89 @@ async function assertRealDirectory(directory,label){
|
|
|
65
71
|
if(info.isSymbolicLink()||!info.isDirectory())fail(`${label} must be a real directory: ${directory}.`);
|
|
66
72
|
}
|
|
67
73
|
|
|
74
|
+
function sameDirectoryIdentity(left,right){
|
|
75
|
+
return left.device===right.device&&left.inode===right.inode;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function sameDirectoryPath(left,right){
|
|
79
|
+
const normalize=value=>{
|
|
80
|
+
const resolved=path.resolve(value);
|
|
81
|
+
return process.platform==='win32'?resolved.toLocaleLowerCase('en-US'):resolved;
|
|
82
|
+
};
|
|
83
|
+
return normalize(left)===normalize(right);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function captureRealDirectoryIdentity(directory,label){
|
|
87
|
+
const requested=path.resolve(directory);
|
|
88
|
+
let requestedInfo;
|
|
89
|
+
try{requestedInfo=await lstat(requested,{bigint:true});}
|
|
90
|
+
catch(error){
|
|
91
|
+
if(error?.code==='ENOENT')fail(`${label} does not exist: ${requested}.`);
|
|
92
|
+
throw error;
|
|
93
|
+
}
|
|
94
|
+
if(requestedInfo.isSymbolicLink()||!requestedInfo.isDirectory()){
|
|
95
|
+
fail(`${label} must be a real directory: ${requested}.`);
|
|
96
|
+
}
|
|
97
|
+
const canonical=await realpath(requested);
|
|
98
|
+
const canonicalInfo=await lstat(canonical,{bigint:true});
|
|
99
|
+
const requestedIdentity=Object.freeze({
|
|
100
|
+
device:requestedInfo.dev,
|
|
101
|
+
inode:requestedInfo.ino
|
|
102
|
+
});
|
|
103
|
+
const canonicalIdentity=Object.freeze({
|
|
104
|
+
device:canonicalInfo.dev,
|
|
105
|
+
inode:canonicalInfo.ino
|
|
106
|
+
});
|
|
107
|
+
if(canonicalInfo.isSymbolicLink()||!canonicalInfo.isDirectory()
|
|
108
|
+
||!sameDirectoryIdentity(requestedIdentity,canonicalIdentity)){
|
|
109
|
+
fail(`${label} must resolve to one physical directory: ${requested}.`);
|
|
110
|
+
}
|
|
111
|
+
return Object.freeze({
|
|
112
|
+
requested,
|
|
113
|
+
requestedIdentity,
|
|
114
|
+
canonical,
|
|
115
|
+
identity:canonicalIdentity
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async function assertRealDirectoryIdentity(captured,label){
|
|
120
|
+
let canonical;
|
|
121
|
+
let requestedInfo;
|
|
122
|
+
let canonicalInfo;
|
|
123
|
+
try{
|
|
124
|
+
[canonical,requestedInfo,canonicalInfo]=await Promise.all([
|
|
125
|
+
realpath(captured.requested),
|
|
126
|
+
lstat(captured.requested,{bigint:true}),
|
|
127
|
+
lstat(captured.canonical,{bigint:true})
|
|
128
|
+
]);
|
|
129
|
+
}catch(error){
|
|
130
|
+
if(error?.code==='ENOENT'){
|
|
131
|
+
fail(`${label} changed while focused validation was active.`,
|
|
132
|
+
'ARCANE_INTEGRITY_FAILED');
|
|
133
|
+
}
|
|
134
|
+
throw error;
|
|
135
|
+
}
|
|
136
|
+
const requestedIdentity={device:requestedInfo.dev,inode:requestedInfo.ino};
|
|
137
|
+
const canonicalIdentity={device:canonicalInfo.dev,inode:canonicalInfo.ino};
|
|
138
|
+
if(!sameDirectoryPath(canonical,captured.canonical)
|
|
139
|
+
||requestedInfo.isSymbolicLink()||!requestedInfo.isDirectory()
|
|
140
|
+
||canonicalInfo.isSymbolicLink()||!canonicalInfo.isDirectory()
|
|
141
|
+
||!sameDirectoryIdentity(requestedIdentity,captured.requestedIdentity)
|
|
142
|
+
||!sameDirectoryIdentity(requestedIdentity,captured.identity)
|
|
143
|
+
||!sameDirectoryIdentity(canonicalIdentity,captured.identity)){
|
|
144
|
+
fail(`${label} changed while focused validation was active.`,'ARCANE_INTEGRITY_FAILED');
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
68
148
|
function classifyRootConfig(config){
|
|
69
149
|
const validated=validatePackagerRootConfig(config,ROOT_CONFIG_NAME);
|
|
70
150
|
const routes=validated.sharedPayloads['browser-runtime'];
|
|
71
151
|
if(!Array.isArray(routes))fail(`${ROOT_CONFIG_NAME} must define browser-runtime routes.`);
|
|
72
152
|
const external=[
|
|
73
153
|
{
|
|
74
|
-
source:'
|
|
154
|
+
source:'arcane',
|
|
75
155
|
destination:'arcane',
|
|
76
|
-
include:['components','css','entities','img','modules','security']
|
|
77
|
-
},
|
|
78
|
-
{
|
|
79
|
-
source:'node_modules/arcane-os/runtime/strong-type',
|
|
80
|
-
destination:'node_modules/strong-type',
|
|
81
|
-
include:['index.js','licence','package.json']
|
|
156
|
+
include:['components','css','dependencies','entities','img','modules','sdk','security']
|
|
82
157
|
},
|
|
83
158
|
{
|
|
84
159
|
source:'node_modules/arcane-os',
|
|
@@ -87,6 +162,13 @@ function classifyRootConfig(config){
|
|
|
87
162
|
}
|
|
88
163
|
];
|
|
89
164
|
const integrated=[
|
|
165
|
+
{
|
|
166
|
+
source:'arcane',
|
|
167
|
+
destination:'arcane',
|
|
168
|
+
include:['components','css','dependencies','entities','img','modules','sdk','security']
|
|
169
|
+
}
|
|
170
|
+
];
|
|
171
|
+
const integratedLegacy=[
|
|
90
172
|
{
|
|
91
173
|
source:'arcane',
|
|
92
174
|
destination:'arcane',
|
|
@@ -106,14 +188,20 @@ function classifyRootConfig(config){
|
|
|
106
188
|
&&Array.isArray(route.exclude)&&route.exclude.length===0;
|
|
107
189
|
});
|
|
108
190
|
let workspaceMode;
|
|
191
|
+
let browserRuntimeLayout;
|
|
109
192
|
if(matches(external)){
|
|
110
193
|
workspaceMode='external';
|
|
194
|
+
browserRuntimeLayout='physical-v1';
|
|
111
195
|
}else if(matches(integrated)){
|
|
112
196
|
workspaceMode='integrated';
|
|
197
|
+
browserRuntimeLayout='physical-v1';
|
|
198
|
+
}else if(matches(integratedLegacy)){
|
|
199
|
+
workspaceMode='integrated';
|
|
200
|
+
browserRuntimeLayout='integrated-legacy';
|
|
113
201
|
}else{
|
|
114
202
|
fail(`${ROOT_CONFIG_NAME} browser-runtime routes must match the external SDK or integrated Arcane workspace contract.`);
|
|
115
203
|
}
|
|
116
|
-
return Object.freeze({...validated,workspaceMode});
|
|
204
|
+
return Object.freeze({...validated,workspaceMode,browserRuntimeLayout});
|
|
117
205
|
}
|
|
118
206
|
|
|
119
207
|
async function discoverAppsInRoot(root,config){
|
|
@@ -220,12 +308,57 @@ export async function resolveWorkspace({workspaceRoot=process.cwd(),appId}={}){
|
|
|
220
308
|
}
|
|
221
309
|
|
|
222
310
|
function validateLock(lock){
|
|
223
|
-
|
|
311
|
+
const browser=lock?.sdkBrowserRuntime;
|
|
312
|
+
const browserSource=browser?.source;
|
|
313
|
+
const dependencies=browserSource?.dependencies;
|
|
314
|
+
const exactKeys=(value,keys)=>isObject(value)
|
|
315
|
+
&&Object.keys(value).sort().join('\0')===[...keys].sort().join('\0');
|
|
316
|
+
const expectedDependencies=[
|
|
317
|
+
{
|
|
318
|
+
name:'event-pubsub',
|
|
319
|
+
version:'6.1.0',
|
|
320
|
+
resolved:'https://registry.npmjs.org/event-pubsub/-/event-pubsub-6.1.0.tgz',
|
|
321
|
+
integrity:'sha512-FEMlhTxwqGM0hztTixG6FhVFXqp7Eq1ltk5mSreK6Mhy3xWWpLAzEUR6OMvMdNqT3jgSxA8JDhnhyAG3X4Xy7Q=='
|
|
322
|
+
},
|
|
323
|
+
{
|
|
324
|
+
name:'strong-type',
|
|
325
|
+
version:'2.0.0',
|
|
326
|
+
resolved:'https://registry.npmjs.org/strong-type/-/strong-type-2.0.0.tgz',
|
|
327
|
+
integrity:'sha512-HHrY9qYC7yn+5mlewiI3k9RQM9gZqGQsqbomZcd10Ks0h4RlX01nnkWbCe4AsVPCI6KaFvpkWm1nHMD+Ykup6g=='
|
|
328
|
+
}
|
|
329
|
+
];
|
|
330
|
+
const dependenciesMatch=Array.isArray(dependencies)&&dependencies.length===2
|
|
331
|
+
&&dependencies.every((actual,index)=>{
|
|
332
|
+
const expected=expectedDependencies[index];
|
|
333
|
+
return exactKeys(actual,['name','version','resolved','integrity'])
|
|
334
|
+
&&actual.name===expected.name&&actual.version===expected.version
|
|
335
|
+
&&actual.resolved===expected.resolved&&actual.integrity===expected.integrity;
|
|
336
|
+
});
|
|
337
|
+
if(!exactKeys(lock,['schemaVersion','sdk','runtime','sdkBrowserRuntime','protocols'])
|
|
338
|
+
||lock.schemaVersion!==1
|
|
339
|
+
||!exactKeys(lock.sdk,['name','version'])
|
|
224
340
|
||lock.sdk.name!==EXPECTED_SDK_NAME||lock.sdk.version!==EXPECTED_SDK_VERSION
|
|
225
|
-
||!
|
|
341
|
+
||!exactKeys(lock.runtime,['manifest','contentSha256','upstreamCommit'])
|
|
342
|
+
||!SHA256_PATTERN.test(lock.runtime.contentSha256)
|
|
226
343
|
||!/^([a-f0-9]{40})$/.test(lock.runtime.upstreamCommit)
|
|
227
344
|
||lock.runtime.manifest!=='node_modules/arcane-os/runtime/ARCANE_RUNTIME_RELEASE.json'
|
|
228
|
-
||!
|
|
345
|
+
||!exactKeys(browser,[
|
|
346
|
+
'manifest','manifestSha256','contentSha256','builder','sdkVersion','source'
|
|
347
|
+
])
|
|
348
|
+
||browser.manifest!=='node_modules/arcane-os/browser-runtime/ARCANE_SDK_BROWSER_RELEASE.json'
|
|
349
|
+
||browser.manifestSha256!==SDK_BROWSER_RUNTIME_MANIFEST_SHA256
|
|
350
|
+
||browser.contentSha256!==SDK_BROWSER_RUNTIME_CONTENT_SHA256
|
|
351
|
+
||browser.builder!=='arcane-sdk-browser-runtime-v1'
|
|
352
|
+
||browser.sdkVersion!==EXPECTED_SDK_VERSION
|
|
353
|
+
||!exactKeys(browserSource,[
|
|
354
|
+
'authority','repository','protocol','browserEntry','dependencies'
|
|
355
|
+
])||browserSource.authority!=='arcane-os-sdk'
|
|
356
|
+
||browserSource.repository!=='https://github.com/TheWizardNexus/arcane-os-sdk.git'
|
|
357
|
+
||browserSource.protocol!=='arcane-sdk-browser-runtime/1'
|
|
358
|
+
||browserSource.browserEntry!=='arcane-os/event-manager'
|
|
359
|
+
||!dependenciesMatch
|
|
360
|
+
||!exactKeys(lock.protocols,['arcane','cliEvents','targetAdapter'])
|
|
361
|
+
||lock.protocols.arcane!=='arcane/1'
|
|
229
362
|
||lock.protocols.cliEvents!=='arcane-cli-events/1'
|
|
230
363
|
||lock.protocols.targetAdapter!=='arcane-target-adapter/1'){
|
|
231
364
|
fail('arcane.lock.json is incompatible with this SDK. Run arcane init only after reviewing missing files; existing locks are never overwritten.');
|
|
@@ -233,33 +366,94 @@ function validateLock(lock){
|
|
|
233
366
|
return lock;
|
|
234
367
|
}
|
|
235
368
|
|
|
236
|
-
function
|
|
369
|
+
function sameBrowserRuntimeSource(actual,pinned){
|
|
370
|
+
const exactKeys=(value,keys)=>isObject(value)
|
|
371
|
+
&&Object.keys(value).sort().join('\0')===[...keys].sort().join('\0');
|
|
372
|
+
const keys=['authority','repository','protocol','browserEntry','dependencies'];
|
|
373
|
+
if(!exactKeys(actual,keys)||!exactKeys(pinned,keys)
|
|
374
|
+
||actual.authority!==pinned.authority
|
|
375
|
+
||actual.repository!==pinned.repository
|
|
376
|
+
||actual.protocol!==pinned.protocol
|
|
377
|
+
||actual.browserEntry!==pinned.browserEntry
|
|
378
|
+
||!Array.isArray(actual.dependencies)||!Array.isArray(pinned.dependencies)
|
|
379
|
+
||actual.dependencies.length!==pinned.dependencies.length){
|
|
380
|
+
return false;
|
|
381
|
+
}
|
|
382
|
+
const dependencyKeys=['name','version','resolved','integrity'];
|
|
383
|
+
return actual.dependencies.every((dependency,index)=>{
|
|
384
|
+
const expected=pinned.dependencies[index];
|
|
385
|
+
return exactKeys(dependency,dependencyKeys)&&exactKeys(expected,dependencyKeys)
|
|
386
|
+
&&dependency.name===expected.name&&dependency.version===expected.version
|
|
387
|
+
&&dependency.resolved===expected.resolved
|
|
388
|
+
&&dependency.integrity===expected.integrity;
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function assertHtmlContract(source,appId,{
|
|
393
|
+
entry='index.html',
|
|
394
|
+
strictStyles=true,
|
|
395
|
+
allowMissingManagedImportMap=false
|
|
396
|
+
}={}){
|
|
237
397
|
const entryLabel=`apps/${appId}/${entry}`;
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
const
|
|
247
|
-
const
|
|
248
|
-
|
|
249
|
-
const
|
|
250
|
-
const
|
|
398
|
+
const htmlContract=inspectImportMapHtml(source);
|
|
399
|
+
const appIdMetadata=htmlContract.metas.filter(meta=>meta.name==='arcane-app-id');
|
|
400
|
+
if(appIdMetadata.length!==1||appIdMetadata[0].content!==appId){
|
|
401
|
+
fail(`${entryLabel} must declare exactly one active matching arcane-app-id metadata element.`);
|
|
402
|
+
}
|
|
403
|
+
if(htmlContract.bases.length!==1||htmlContract.bases[0].href!=='../../'){
|
|
404
|
+
fail(`${entryLabel} must declare exactly one active <base href="../../">.`);
|
|
405
|
+
}
|
|
406
|
+
const resourcePath=value=>value.split(/[?#]/u,1)[0];
|
|
407
|
+
const styles=htmlContract.links.filter(link=>link.rel
|
|
408
|
+
.split(/[\t\n\f\r ]+/u).includes('stylesheet'));
|
|
409
|
+
const positionOfStyle=expected=>styles.find(link=>resourcePath(link.href)===expected)?.start??-1;
|
|
410
|
+
const theme=positionOfStyle('./arcane/css/theme.css');
|
|
411
|
+
const primitives=positionOfStyle('./arcane/css/primitives.css');
|
|
412
|
+
const escapedAppId=appId.replace(/[.*+?^${}()|[\]\\]/gu,'\\$&');
|
|
413
|
+
const appStyle=styles.find(link=>new RegExp(
|
|
414
|
+
`^(?:\\./|/)apps/${escapedAppId}/[^/]+\\.css$`,
|
|
415
|
+
'u'
|
|
416
|
+
).test(resourcePath(link.href)))?.start??-1;
|
|
417
|
+
const modules=htmlContract.scripts.filter(script=>script.type==='module'&&script.src);
|
|
418
|
+
const bootstrap=modules.find(script=>resourcePath(script.src)
|
|
419
|
+
==='./arcane/modules/ThemeBootstrap.js')?.start??-1;
|
|
420
|
+
if(htmlContract.managedMaps.length>1){
|
|
421
|
+
fail(`${entryLabel} must contain at most one active managed Arcane import map.`);
|
|
422
|
+
}
|
|
423
|
+
const managedImportMap=htmlContract.managedMaps[0]?.start??-1;
|
|
424
|
+
const firstModule=htmlContract.firstModulePosition;
|
|
425
|
+
const appModule=modules.find(script=>new RegExp(
|
|
426
|
+
`^(?:\\./|/)apps/${escapedAppId}/.+\\.(?:js|mjs)$`,
|
|
427
|
+
'u'
|
|
428
|
+
).test(resourcePath(script.src)))?.start??-1;
|
|
251
429
|
if(theme<0){
|
|
252
430
|
fail(`${entryLabel} must load the shared Arcane theme.css.`);
|
|
253
431
|
}
|
|
432
|
+
if(appModule<0){
|
|
433
|
+
fail(`${entryLabel} must load an active app-local module script.`);
|
|
434
|
+
}
|
|
254
435
|
if(strictStyles&&(primitives<=theme||appStyle<=primitives)){
|
|
255
436
|
fail(`${entryLabel} must load theme.css, primitives.css, and app CSS in that order.`);
|
|
256
437
|
}
|
|
257
438
|
if(!strictStyles&&((primitives>=0&&primitives<=theme)||(appStyle>=0&&appStyle<=theme))){
|
|
258
439
|
fail(`${entryLabel} must load shared and app CSS after theme.css.`);
|
|
259
440
|
}
|
|
260
|
-
if(bootstrap
|
|
441
|
+
if(bootstrap>=0&&appModule>=0&&appModule<=bootstrap){
|
|
261
442
|
fail(`${entryLabel} must load ThemeBootstrap.js before app-local module scripts.`);
|
|
262
443
|
}
|
|
444
|
+
if((managedImportMap>=0&&htmlContract.bases[0].end>managedImportMap)
|
|
445
|
+
||(firstModule>=0&&htmlContract.bases[0].end>firstModule)){
|
|
446
|
+
fail(`${entryLabel} must place its base element before import maps and module loads.`);
|
|
447
|
+
}
|
|
448
|
+
if(bootstrap<0&&(
|
|
449
|
+
(managedImportMap<0&&!allowMissingManagedImportMap)
|
|
450
|
+
||(managedImportMap>=0&&appModule>=0&&appModule<=managedImportMap)
|
|
451
|
+
)){
|
|
452
|
+
fail(
|
|
453
|
+
`${entryLabel} must install its managed Arcane import map before app-local `
|
|
454
|
+
+'module scripts when ThemeBootstrap.js is imported by name.'
|
|
455
|
+
);
|
|
456
|
+
}
|
|
263
457
|
}
|
|
264
458
|
|
|
265
459
|
export async function validateDiscoveredApplication({
|
|
@@ -267,19 +461,31 @@ export async function validateDiscoveredApplication({
|
|
|
267
461
|
workspaceMode,
|
|
268
462
|
workspaceConfig,
|
|
269
463
|
app,
|
|
464
|
+
allowMissingManagedImportMap=false,
|
|
270
465
|
signal,
|
|
271
466
|
onEvent
|
|
272
467
|
}={}){
|
|
273
468
|
throwIfAborted(signal);
|
|
274
|
-
if(!app||typeof app.appId!=='string'
|
|
469
|
+
if(!app||typeof app.appId!=='string'||!APP_ID_PATTERN.test(app.appId)
|
|
470
|
+
||typeof app.appRoot!=='string'
|
|
275
471
|
||!app.manifest||!app.descriptor){
|
|
276
472
|
fail('A discovered Arcane application is required for focused validation.');
|
|
277
473
|
}
|
|
278
|
-
const
|
|
474
|
+
const capturedWorkspace=await captureRealDirectoryIdentity(workspaceRoot,'Workspace');
|
|
475
|
+
const canonicalWorkspaceRoot=capturedWorkspace.canonical;
|
|
476
|
+
const capturedAppsRoot=await captureRealDirectoryIdentity(
|
|
477
|
+
path.join(canonicalWorkspaceRoot,'apps'),
|
|
478
|
+
'Workspace apps root'
|
|
479
|
+
);
|
|
279
480
|
const expectedAppRoot=path.join(canonicalWorkspaceRoot,'apps',app.appId);
|
|
280
|
-
|
|
481
|
+
const [capturedExpectedApp,capturedDiscoveredApp]=await Promise.all([
|
|
482
|
+
captureRealDirectoryIdentity(expectedAppRoot,`apps/${app.appId}`),
|
|
483
|
+
captureRealDirectoryIdentity(app.appRoot,`Discovered app ${app.appId}`)
|
|
484
|
+
]);
|
|
485
|
+
if(!sameDirectoryIdentity(capturedExpectedApp.identity,capturedDiscoveredApp.identity)){
|
|
281
486
|
fail(`Discovered app ${app.appId} does not belong to the selected workspace.`);
|
|
282
487
|
}
|
|
488
|
+
const canonicalAppRoot=capturedExpectedApp.canonical;
|
|
283
489
|
let config=workspaceConfig;
|
|
284
490
|
if(!config){
|
|
285
491
|
const profile=await inspectWorkspaceProfile(canonicalWorkspaceRoot);
|
|
@@ -291,7 +497,7 @@ export async function validateDiscoveredApplication({
|
|
|
291
497
|
if(!isObject(config?.sharedPayloads)){
|
|
292
498
|
fail('The selected Arcane workspace configuration is unavailable for focused validation.');
|
|
293
499
|
}
|
|
294
|
-
const configPath=path.join(
|
|
500
|
+
const configPath=path.join(canonicalAppRoot,APP_CONFIG_NAME);
|
|
295
501
|
const rawManifest=await readJson(
|
|
296
502
|
configPath,
|
|
297
503
|
`apps/${app.appId}/${APP_CONFIG_NAME}`
|
|
@@ -302,7 +508,7 @@ export async function validateDiscoveredApplication({
|
|
|
302
508
|
}
|
|
303
509
|
const loadedDescriptor=await loadAppDescriptor({
|
|
304
510
|
workspaceRoot:canonicalWorkspaceRoot,
|
|
305
|
-
appRoot:
|
|
511
|
+
appRoot:canonicalAppRoot,
|
|
306
512
|
appId:app.appId,
|
|
307
513
|
packageManifest:rawManifest
|
|
308
514
|
});
|
|
@@ -315,27 +521,35 @@ export async function validateDiscoveredApplication({
|
|
|
315
521
|
}
|
|
316
522
|
const freshApp=Object.freeze({
|
|
317
523
|
appId:app.appId,
|
|
318
|
-
appRoot:
|
|
524
|
+
appRoot:canonicalAppRoot,
|
|
319
525
|
manifest,
|
|
320
526
|
descriptor,
|
|
321
527
|
descriptorSource:loadedDescriptor.source,
|
|
322
528
|
descriptorPath:loadedDescriptor.descriptorPath
|
|
323
529
|
});
|
|
324
|
-
const entryPath=path.join(
|
|
530
|
+
const entryPath=path.join(canonicalAppRoot,manifest.entry);
|
|
325
531
|
const info=await lstat(entryPath);
|
|
326
532
|
if(info.isSymbolicLink()||!info.isFile()){
|
|
327
533
|
fail(`apps/${app.appId}/${manifest.entry} must be a real file.`);
|
|
328
534
|
}
|
|
329
535
|
assertHtmlContract(await readFile(entryPath,'utf8'),app.appId,{
|
|
330
536
|
entry:manifest.entry,
|
|
331
|
-
strictStyles:workspaceMode==='external'
|
|
537
|
+
strictStyles:workspaceMode==='external',
|
|
538
|
+
allowMissingManagedImportMap
|
|
332
539
|
});
|
|
540
|
+
const assertCapturedDirectories=()=>Promise.all([
|
|
541
|
+
assertRealDirectoryIdentity(capturedWorkspace,'Workspace'),
|
|
542
|
+
assertRealDirectoryIdentity(capturedAppsRoot,'Workspace apps root'),
|
|
543
|
+
assertRealDirectoryIdentity(capturedExpectedApp,`apps/${app.appId}`),
|
|
544
|
+
assertRealDirectoryIdentity(capturedDiscoveredApp,`Discovered app ${app.appId}`)
|
|
545
|
+
]);
|
|
546
|
+
await assertCapturedDirectories();
|
|
333
547
|
const receipt=Object.freeze({
|
|
334
548
|
valid:true,
|
|
335
549
|
workspaceRoot:canonicalWorkspaceRoot,
|
|
336
550
|
workspaceMode,
|
|
337
551
|
appId:app.appId,
|
|
338
|
-
appRoot:
|
|
552
|
+
appRoot:canonicalAppRoot,
|
|
339
553
|
app:freshApp
|
|
340
554
|
});
|
|
341
555
|
await emit(onEvent,{
|
|
@@ -343,10 +557,17 @@ export async function validateDiscoveredApplication({
|
|
|
343
557
|
workspaceRoot:canonicalWorkspaceRoot,
|
|
344
558
|
appId:app.appId
|
|
345
559
|
});
|
|
560
|
+
await assertCapturedDirectories();
|
|
346
561
|
return receipt;
|
|
347
562
|
}
|
|
348
563
|
|
|
349
|
-
export async function validateWorkspace({
|
|
564
|
+
export async function validateWorkspace({
|
|
565
|
+
workspaceRoot=process.cwd(),
|
|
566
|
+
appId,
|
|
567
|
+
allowMissingManagedImportMap=false,
|
|
568
|
+
signal,
|
|
569
|
+
onEvent
|
|
570
|
+
}={}){
|
|
350
571
|
throwIfAborted(signal);
|
|
351
572
|
const resolved=await resolveWorkspace({workspaceRoot,appId});
|
|
352
573
|
await emit(onEvent,{type:'workspace.validate.started',workspaceRoot:resolved.workspaceRoot,appId:resolved.appId});
|
|
@@ -409,12 +630,39 @@ export async function validateWorkspace({workspaceRoot=process.cwd(),appId,signa
|
|
|
409
630
|
||installed.source?.commit!==lock.runtime.upstreamCommit){
|
|
410
631
|
fail('Installed SDK runtime does not match arcane.lock.json.');
|
|
411
632
|
}
|
|
633
|
+
const browserManifestPath=path.join(
|
|
634
|
+
resolved.workspaceRoot,
|
|
635
|
+
'node_modules',
|
|
636
|
+
'arcane-os',
|
|
637
|
+
'browser-runtime',
|
|
638
|
+
'ARCANE_SDK_BROWSER_RELEASE.json'
|
|
639
|
+
);
|
|
640
|
+
const browserBytes=await readFile(browserManifestPath);
|
|
641
|
+
let installedBrowser;
|
|
642
|
+
try{installedBrowser=JSON.parse(browserBytes.toString('utf8'));}
|
|
643
|
+
catch(error){
|
|
644
|
+
fail(`Installed SDK browser runtime manifest is not valid JSON: ${error.message}`);
|
|
645
|
+
}
|
|
646
|
+
if(createHash('sha256').update(browserBytes).digest('hex')
|
|
647
|
+
!==lock.sdkBrowserRuntime.manifestSha256
|
|
648
|
+
||installedBrowser.contentSha256!==lock.sdkBrowserRuntime.contentSha256
|
|
649
|
+
||installedBrowser.builder!==lock.sdkBrowserRuntime.builder
|
|
650
|
+
||installedBrowser.sdkVersion!==lock.sdkBrowserRuntime.sdkVersion
|
|
651
|
+
||!sameBrowserRuntimeSource(
|
|
652
|
+
installedBrowser.source,
|
|
653
|
+
lock.sdkBrowserRuntime.source
|
|
654
|
+
)){
|
|
655
|
+
fail('Installed SDK browser runtime does not match arcane.lock.json.');
|
|
656
|
+
}
|
|
412
657
|
});
|
|
413
658
|
}else{
|
|
414
659
|
await add('workspace-runtime',async()=>{
|
|
660
|
+
const strongType=resolved.config.browserRuntimeLayout==='integrated-legacy'
|
|
661
|
+
?path.join('node_modules','strong-type')
|
|
662
|
+
:path.join('arcane','dependencies','strong-type');
|
|
415
663
|
for(const [relative,label] of [
|
|
416
664
|
['arcane','Integrated Arcane runtime'],
|
|
417
|
-
[
|
|
665
|
+
[strongType,'Integrated strong-type runtime']
|
|
418
666
|
]){
|
|
419
667
|
const info=await lstat(path.join(resolved.workspaceRoot,relative));
|
|
420
668
|
if(info.isSymbolicLink()||!info.isDirectory()){
|
|
@@ -429,6 +677,7 @@ export async function validateWorkspace({workspaceRoot=process.cwd(),appId,signa
|
|
|
429
677
|
workspaceMode,
|
|
430
678
|
workspaceConfig:resolved.config,
|
|
431
679
|
app:resolved.app,
|
|
680
|
+
allowMissingManagedImportMap,
|
|
432
681
|
signal,
|
|
433
682
|
onEvent
|
|
434
683
|
});
|