@cratis/scene.react 4.18.1 → 4.20.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/dist/cjs/index.js +3 -0
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/packages/loadApprovedPackages.js +122 -0
- package/dist/cjs/packages/loadApprovedPackages.js.map +1 -0
- package/dist/esm/index.js +1 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/packages/ApprovedPackageSet.d.ts +16 -0
- package/dist/esm/packages/ApprovedPackageSet.d.ts.map +1 -0
- package/dist/esm/packages/ApprovedPackageSet.js +2 -0
- package/dist/esm/packages/ApprovedPackageSet.js.map +1 -0
- package/dist/esm/packages/index.d.ts +2 -0
- package/dist/esm/packages/index.d.ts.map +1 -1
- package/dist/esm/packages/index.js +2 -0
- package/dist/esm/packages/index.js.map +1 -1
- package/dist/esm/packages/loadApprovedPackages.d.ts +18 -0
- package/dist/esm/packages/loadApprovedPackages.d.ts.map +1 -0
- package/dist/esm/packages/loadApprovedPackages.js +119 -0
- package/dist/esm/packages/loadApprovedPackages.js.map +1 -0
- package/dist/esm/tsconfig.tsbuildinfo +1 -1
- package/package.json +5 -5
- package/packages/ApprovedPackageSet.ts +37 -0
- package/packages/index.ts +2 -0
- package/packages/loadApprovedPackages.ts +143 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cratis/scene.react",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.20.0",
|
|
4
4
|
"description": "The React renderer implementing Scene.Engine's Renderer contract against real React/DOM.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cratis",
|
|
@@ -51,12 +51,12 @@
|
|
|
51
51
|
"build-storybook": "storybook build"
|
|
52
52
|
},
|
|
53
53
|
"dependencies": {
|
|
54
|
-
"@cratis/scene.engine": "4.
|
|
55
|
-
"@cratis/scene.model": "4.
|
|
54
|
+
"@cratis/scene.engine": "4.20.0",
|
|
55
|
+
"@cratis/scene.model": "4.20.0"
|
|
56
56
|
},
|
|
57
57
|
"devDependencies": {
|
|
58
|
-
"@cratis/scene.engine": "4.
|
|
59
|
-
"@cratis/scene.model": "4.
|
|
58
|
+
"@cratis/scene.engine": "4.20.0",
|
|
59
|
+
"@cratis/scene.model": "4.20.0",
|
|
60
60
|
"@storybook/addon-links": "^10.6.1",
|
|
61
61
|
"@storybook/react": "^10.6.1",
|
|
62
62
|
"@storybook/react-vite": "^10.6.1",
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// Copyright (c) Cratis. All rights reserved.
|
|
2
|
+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
3
|
+
|
|
4
|
+
import { ScenePackage } from '@cratis/scene.model';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Where the runtime of an approved package comes from: the npm package the frontend build installs, and the
|
|
8
|
+
* export of that package which is its {@link ScenePackageBundle}.
|
|
9
|
+
*/
|
|
10
|
+
export interface PackageModuleReference {
|
|
11
|
+
/** The npm package name the build installs and imports, for example `@acme/scene.charts`. */
|
|
12
|
+
npm: string;
|
|
13
|
+
|
|
14
|
+
/** The npm version or range written into the application's `package.json`. */
|
|
15
|
+
version: string;
|
|
16
|
+
|
|
17
|
+
/** The named export of the npm package that is the package's runtime bundle. */
|
|
18
|
+
export: string;
|
|
19
|
+
|
|
20
|
+
/** Package-relative style sheets the application imports for the package. Never URLs. */
|
|
21
|
+
styles?: string[];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** One package a host approved: the declaration planning resolves a profile against, and where its runtime comes from. */
|
|
25
|
+
export interface ApprovedPackage {
|
|
26
|
+
manifest: ScenePackage;
|
|
27
|
+
module: PackageModuleReference;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* The custom runtime packages a host approved on top of the ones it ships. The document the CLI, Stage and a
|
|
32
|
+
* React host share; `Cratis.Scene.Engine.Packages.ApprovedPackageSetJson` reads and writes the same JSON in .NET.
|
|
33
|
+
*/
|
|
34
|
+
export interface ApprovedPackageSet {
|
|
35
|
+
version: 1;
|
|
36
|
+
packages: ApprovedPackage[];
|
|
37
|
+
}
|
package/packages/index.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Copyright (c) Cratis. All rights reserved.
|
|
2
2
|
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
3
3
|
|
|
4
|
+
export * from './ApprovedPackageSet';
|
|
4
5
|
export * from './DesignTimeAction';
|
|
5
6
|
export * from './DesignTimeActionOutcome';
|
|
6
7
|
export * from './DesignTimeActionState';
|
|
@@ -15,6 +16,7 @@ export * from './DesignTimeProvider';
|
|
|
15
16
|
export * from './DesignTimeResolution';
|
|
16
17
|
export * from '../navigation';
|
|
17
18
|
export * from './EmbeddedPackageHost';
|
|
19
|
+
export * from './loadApprovedPackages';
|
|
18
20
|
export * from './loadDesignTimeContributions';
|
|
19
21
|
export * from './isNetworkAsset';
|
|
20
22
|
export * from './PackageHostAssets';
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
// Copyright (c) Cratis. All rights reserved.
|
|
2
|
+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
3
|
+
|
|
4
|
+
import { ApprovedPackageSet } from './ApprovedPackageSet';
|
|
5
|
+
import { PackageHostConfiguration, ResolvedPackageHost, resolvePackageHost } from './PackageHostConfiguration';
|
|
6
|
+
import { ScenePackageBundle } from './ScenePackageBundle';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* How a host loads the modules of an approved package set: one loader per npm package name the set may import.
|
|
10
|
+
*
|
|
11
|
+
* A bundler resolves imports at build time, so the host lists them as literal `import()` calls
|
|
12
|
+
* (`{ '@acme/scene.charts': () => import('@acme/scene.charts') }`). A set naming a module the host's build does
|
|
13
|
+
* not include is a diagnostic, never an attempt to load an arbitrary specifier.
|
|
14
|
+
*/
|
|
15
|
+
export type ApprovedPackageImporters = Record<string, () => Promise<Record<string, unknown>>>;
|
|
16
|
+
|
|
17
|
+
/** The outcome of loading an approved package set. */
|
|
18
|
+
export interface LoadedApprovedPackages {
|
|
19
|
+
/** The bundles that loaded and match what the set declared, in ordinal package-name order. */
|
|
20
|
+
bundles: ScenePackageBundle[];
|
|
21
|
+
|
|
22
|
+
/** Every reason a package was not loaded or does not match its declaration. */
|
|
23
|
+
diagnostics: string[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Loads the runtime bundles of an approved package set through the host's importers.
|
|
28
|
+
*
|
|
29
|
+
* Nothing executes unless the host's policy allows executable imports. A package whose module cannot be imported,
|
|
30
|
+
* lacks its declared export, or whose bundle manifest differs from the declaration (name, version, components,
|
|
31
|
+
* layouts, templates, themes) contributes nothing and is reported - the declaration planning used and the code
|
|
32
|
+
* running must be the same package. Packages load in ordinal name order, so the result does not depend on the
|
|
33
|
+
* order the set was written in.
|
|
34
|
+
*/
|
|
35
|
+
export async function loadApprovedPackages(
|
|
36
|
+
set: ApprovedPackageSet,
|
|
37
|
+
importers: ApprovedPackageImporters,
|
|
38
|
+
policy: { allowExecutableImports: boolean },
|
|
39
|
+
): Promise<LoadedApprovedPackages> {
|
|
40
|
+
const bundles: ScenePackageBundle[] = [];
|
|
41
|
+
const diagnostics: string[] = [];
|
|
42
|
+
const ordered = [...set.packages].sort((a, b) => (a.manifest.name < b.manifest.name ? -1 : a.manifest.name > b.manifest.name ? 1 : 0));
|
|
43
|
+
|
|
44
|
+
for (const { manifest, module } of ordered) {
|
|
45
|
+
const label = `Package '${manifest.name}'`;
|
|
46
|
+
if (!policy.allowExecutableImports) {
|
|
47
|
+
diagnostics.push(`${label} brings executable code from '${module.npm}', but host policy forbids executable imports`);
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const importer = Object.prototype.hasOwnProperty.call(importers, module.npm) ? importers[module.npm] : undefined;
|
|
52
|
+
if (!importer) {
|
|
53
|
+
diagnostics.push(`${label} is imported from '${module.npm}', which this host's build does not include`);
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
let loaded: Record<string, unknown>;
|
|
58
|
+
try {
|
|
59
|
+
loaded = await importer();
|
|
60
|
+
} catch (error) {
|
|
61
|
+
diagnostics.push(`${label} could not be loaded from '${module.npm}': ${error instanceof Error ? error.message : String(error)}`);
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const bundle = loaded[module.export] as ScenePackageBundle | undefined;
|
|
66
|
+
if (!bundle || typeof bundle !== 'object' || !bundle.manifest || !bundle.components) {
|
|
67
|
+
diagnostics.push(`${label} expects '${module.npm}' to export a package bundle named '${module.export}', which it does not`);
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const mismatches = declarationMismatches(manifest, bundle);
|
|
72
|
+
if (mismatches.length > 0) {
|
|
73
|
+
diagnostics.push(...mismatches.map(mismatch => `${label} does not match the bundle '${module.export}' of '${module.npm}': ${mismatch}`));
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
bundles.push(bundle);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return { bundles, diagnostics };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function declarationMismatches(declared: ApprovedPackageSet['packages'][number]['manifest'], bundle: ScenePackageBundle): string[] {
|
|
84
|
+
const actual = bundle.manifest;
|
|
85
|
+
const mismatches: string[] = [];
|
|
86
|
+
if (actual.name !== declared.name) mismatches.push(`the bundle is named '${actual.name}'`);
|
|
87
|
+
if (actual.version !== declared.version) mismatches.push(`the set declares version ${declared.version} but the bundle is ${actual.version}`);
|
|
88
|
+
if (actual.kind !== declared.kind) mismatches.push(`the set declares kind ${declared.kind} but the bundle is ${actual.kind}`);
|
|
89
|
+
|
|
90
|
+
const lists = ['components', 'layouts', 'screenTemplates', 'dialogTemplates', 'themes'] as const;
|
|
91
|
+
for (const list of lists) {
|
|
92
|
+
const expected = [...(declared[list] ?? [])].sort();
|
|
93
|
+
const found = [...(actual[list] ?? [])].sort();
|
|
94
|
+
if (expected.join('\u0000') !== found.join('\u0000')) {
|
|
95
|
+
mismatches.push(`the set declares ${list} [${expected.join(', ')}] but the bundle declares [${found.join(', ')}]`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return mismatches;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** An approved package set and the importers that load it, resolved together with the built-in bundles. */
|
|
103
|
+
export interface ApprovedPackageHostConfiguration extends Omit<PackageHostConfiguration, 'bundles'> {
|
|
104
|
+
/** The bundles the host ships. */
|
|
105
|
+
bundles: ScenePackageBundle[];
|
|
106
|
+
|
|
107
|
+
/** The custom packages the host approved. */
|
|
108
|
+
packageSet: ApprovedPackageSet;
|
|
109
|
+
|
|
110
|
+
/** The importers for the set's modules. */
|
|
111
|
+
importers: ApprovedPackageImporters;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Loads the approved package set and resolves the effective profile over the built-in and approved bundles in one
|
|
116
|
+
* step. Packages that failed to load are reported in the result's `diagnostics` and block the host, so a profile
|
|
117
|
+
* is never rendered with a silently missing package.
|
|
118
|
+
*/
|
|
119
|
+
export async function resolveApprovedPackageHost(configuration: ApprovedPackageHostConfiguration): Promise<ResolvedPackageHost> {
|
|
120
|
+
const { packageSet, importers, ...rest } = configuration;
|
|
121
|
+
const loaded = await loadApprovedPackages(packageSet, importers, configuration.policy);
|
|
122
|
+
const host = resolvePackageHost({ ...rest, bundles: [...configuration.bundles, ...loaded.bundles] });
|
|
123
|
+
if (loaded.diagnostics.length === 0) {
|
|
124
|
+
return host;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return {
|
|
128
|
+
...host,
|
|
129
|
+
bundles: [],
|
|
130
|
+
components: {},
|
|
131
|
+
layouts: [],
|
|
132
|
+
screenTemplates: [],
|
|
133
|
+
dialogTemplates: [],
|
|
134
|
+
themes: [],
|
|
135
|
+
layout: undefined,
|
|
136
|
+
theme: undefined,
|
|
137
|
+
templates: [],
|
|
138
|
+
assets: [],
|
|
139
|
+
fonts: [],
|
|
140
|
+
blocked: true,
|
|
141
|
+
diagnostics: [...loaded.diagnostics, ...host.diagnostics],
|
|
142
|
+
};
|
|
143
|
+
}
|