@cratis/scene.react 4.11.0 → 4.13.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.
Files changed (108) hide show
  1. package/dist/cjs/core/coreComponents.js +1 -0
  2. package/dist/cjs/core/coreComponents.js.map +1 -1
  3. package/dist/cjs/core/coreDescriptors.js +1 -0
  4. package/dist/cjs/core/coreDescriptors.js.map +1 -1
  5. package/dist/cjs/index.js +17 -0
  6. package/dist/cjs/index.js.map +1 -1
  7. package/dist/cjs/navigation/diagnoseRenderedNavigation.js +17 -0
  8. package/dist/cjs/navigation/diagnoseRenderedNavigation.js.map +1 -0
  9. package/dist/cjs/navigation/identityRoute.js +19 -0
  10. package/dist/cjs/navigation/identityRoute.js.map +1 -0
  11. package/dist/cjs/navigation/navigationRouteKey.js +19 -0
  12. package/dist/cjs/navigation/navigationRouteKey.js.map +1 -0
  13. package/dist/cjs/navigation/resolveDestination.js +2 -10
  14. package/dist/cjs/navigation/resolveDestination.js.map +1 -1
  15. package/dist/cjs/packages/DesignTimeContractVersion.js +15 -0
  16. package/dist/cjs/packages/DesignTimeContractVersion.js.map +1 -0
  17. package/dist/cjs/packages/DesignTimeExtensionPoint.js +26 -0
  18. package/dist/cjs/packages/DesignTimeExtensionPoint.js.map +1 -0
  19. package/dist/cjs/packages/PackageHostConfiguration.js +1 -0
  20. package/dist/cjs/packages/PackageHostConfiguration.js.map +1 -1
  21. package/dist/cjs/packages/loadDesignTimeContributions.js +80 -0
  22. package/dist/cjs/packages/loadDesignTimeContributions.js.map +1 -0
  23. package/dist/cjs/packages/resolveDesignTimeHost.js +114 -0
  24. package/dist/cjs/packages/resolveDesignTimeHost.js.map +1 -0
  25. package/dist/cjs/packages/runDesignTimeAction.js +50 -0
  26. package/dist/cjs/packages/runDesignTimeAction.js.map +1 -0
  27. package/dist/esm/core/coreComponents.js +1 -0
  28. package/dist/esm/core/coreComponents.js.map +1 -1
  29. package/dist/esm/core/coreDescriptors.js +1 -0
  30. package/dist/esm/core/coreDescriptors.js.map +1 -1
  31. package/dist/esm/index.js +8 -0
  32. package/dist/esm/index.js.map +1 -1
  33. package/dist/esm/navigation/diagnoseRenderedNavigation.d.ts +3 -0
  34. package/dist/esm/navigation/diagnoseRenderedNavigation.d.ts.map +1 -0
  35. package/dist/esm/navigation/diagnoseRenderedNavigation.js +15 -0
  36. package/dist/esm/navigation/diagnoseRenderedNavigation.js.map +1 -0
  37. package/dist/esm/navigation/identityRoute.d.ts +3 -0
  38. package/dist/esm/navigation/identityRoute.d.ts.map +1 -0
  39. package/dist/esm/navigation/identityRoute.js +17 -0
  40. package/dist/esm/navigation/identityRoute.js.map +1 -0
  41. package/dist/esm/navigation/index.d.ts +3 -0
  42. package/dist/esm/navigation/index.d.ts.map +1 -1
  43. package/dist/esm/navigation/index.js +3 -0
  44. package/dist/esm/navigation/index.js.map +1 -1
  45. package/dist/esm/navigation/navigationRouteKey.d.ts +3 -0
  46. package/dist/esm/navigation/navigationRouteKey.d.ts.map +1 -0
  47. package/dist/esm/navigation/navigationRouteKey.js +17 -0
  48. package/dist/esm/navigation/navigationRouteKey.js.map +1 -0
  49. package/dist/esm/navigation/resolveDestination.d.ts.map +1 -1
  50. package/dist/esm/navigation/resolveDestination.js +1 -9
  51. package/dist/esm/navigation/resolveDestination.js.map +1 -1
  52. package/dist/esm/packages/DesignTimeActionOutcome.d.ts +7 -0
  53. package/dist/esm/packages/DesignTimeActionOutcome.d.ts.map +1 -0
  54. package/dist/esm/packages/DesignTimeActionOutcome.js +2 -0
  55. package/dist/esm/packages/DesignTimeActionOutcome.js.map +1 -0
  56. package/dist/esm/packages/DesignTimeActionState.d.ts +9 -0
  57. package/dist/esm/packages/DesignTimeActionState.d.ts.map +1 -0
  58. package/dist/esm/packages/DesignTimeActionState.js +2 -0
  59. package/dist/esm/packages/DesignTimeActionState.js.map +1 -0
  60. package/dist/esm/packages/DesignTimeContractVersion.d.ts +2 -0
  61. package/dist/esm/packages/DesignTimeContractVersion.d.ts.map +1 -0
  62. package/dist/esm/packages/DesignTimeContractVersion.js +13 -0
  63. package/dist/esm/packages/DesignTimeContractVersion.js.map +1 -0
  64. package/dist/esm/packages/DesignTimeExtensionPoint.d.ts +9 -0
  65. package/dist/esm/packages/DesignTimeExtensionPoint.d.ts.map +1 -0
  66. package/dist/esm/packages/DesignTimeExtensionPoint.js +23 -0
  67. package/dist/esm/packages/DesignTimeExtensionPoint.js.map +1 -0
  68. package/dist/esm/packages/DesignTimeResolution.d.ts +7 -0
  69. package/dist/esm/packages/DesignTimeResolution.d.ts.map +1 -0
  70. package/dist/esm/packages/DesignTimeResolution.js +2 -0
  71. package/dist/esm/packages/DesignTimeResolution.js.map +1 -0
  72. package/dist/esm/packages/PackageHostConfiguration.d.ts +3 -0
  73. package/dist/esm/packages/PackageHostConfiguration.d.ts.map +1 -1
  74. package/dist/esm/packages/PackageHostConfiguration.js +2 -1
  75. package/dist/esm/packages/PackageHostConfiguration.js.map +1 -1
  76. package/dist/esm/packages/index.d.ts +8 -0
  77. package/dist/esm/packages/index.d.ts.map +1 -1
  78. package/dist/esm/packages/index.js +8 -0
  79. package/dist/esm/packages/index.js.map +1 -1
  80. package/dist/esm/packages/loadDesignTimeContributions.d.ts +11 -0
  81. package/dist/esm/packages/loadDesignTimeContributions.d.ts.map +1 -0
  82. package/dist/esm/packages/loadDesignTimeContributions.js +78 -0
  83. package/dist/esm/packages/loadDesignTimeContributions.js.map +1 -0
  84. package/dist/esm/packages/resolveDesignTimeHost.d.ts +26 -0
  85. package/dist/esm/packages/resolveDesignTimeHost.d.ts.map +1 -0
  86. package/dist/esm/packages/resolveDesignTimeHost.js +112 -0
  87. package/dist/esm/packages/resolveDesignTimeHost.js.map +1 -0
  88. package/dist/esm/packages/runDesignTimeAction.d.ts +4 -0
  89. package/dist/esm/packages/runDesignTimeAction.d.ts.map +1 -0
  90. package/dist/esm/packages/runDesignTimeAction.js +48 -0
  91. package/dist/esm/packages/runDesignTimeAction.js.map +1 -0
  92. package/dist/esm/tsconfig.tsbuildinfo +1 -1
  93. package/navigation/diagnoseRenderedNavigation.ts +14 -0
  94. package/navigation/identityRoute.ts +14 -0
  95. package/navigation/index.ts +3 -0
  96. package/navigation/navigationRouteKey.ts +22 -0
  97. package/navigation/resolveDestination.ts +1 -6
  98. package/package.json +5 -5
  99. package/packages/DesignTimeActionOutcome.ts +18 -0
  100. package/packages/DesignTimeActionState.ts +21 -0
  101. package/packages/DesignTimeContractVersion.ts +12 -0
  102. package/packages/DesignTimeExtensionPoint.ts +23 -0
  103. package/packages/DesignTimeResolution.ts +23 -0
  104. package/packages/PackageHostConfiguration.ts +11 -1
  105. package/packages/index.ts +8 -0
  106. package/packages/loadDesignTimeContributions.ts +98 -0
  107. package/packages/resolveDesignTimeHost.ts +133 -0
  108. package/packages/runDesignTimeAction.ts +36 -0
@@ -0,0 +1,14 @@
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 { NavigationDiagnostic, NavigationGraph, diagnoseNavigation } from '@cratis/scene.engine';
5
+ import { navigationRouteKey } from './navigationRouteKey';
6
+
7
+ /**
8
+ * Diagnoses navigation the way this renderer routes it: the engine's checks, with duplicate routes compared
9
+ * by the URLs `resolveDestination` actually builds - identity-derived routes included, parameter spellings
10
+ * treated as equal.
11
+ */
12
+ export function diagnoseRenderedNavigation(graph: NavigationGraph): NavigationDiagnostic[] {
13
+ return diagnoseNavigation(graph, navigationRouteKey);
14
+ }
@@ -0,0 +1,14 @@
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 { DestinationReference } from '@cratis/scene.model';
5
+
6
+ /**
7
+ * The route a destination gets when no route override was authored: its screen name, or its stable
8
+ * module/feature/slice identity as a path.
9
+ */
10
+ export function identityRoute(destination: DestinationReference): string | undefined {
11
+ if (destination.screen) return destination.screen;
12
+ const parts = [destination.module, destination.feature, destination.slice].filter((part): part is string => typeof part === 'string' && part.length > 0);
13
+ return parts.length ? parts.join('/') : undefined;
14
+ }
@@ -5,3 +5,6 @@ export * from './DestinationResolution';
5
5
  export * from './SceneNavigationContext';
6
6
  export * from './SceneNavigationHost';
7
7
  export * from './resolveDestination';
8
+ export * from './diagnoseRenderedNavigation';
9
+ export * from './identityRoute';
10
+ export * from './navigationRouteKey';
@@ -0,0 +1,22 @@
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 { NavigationRouteKey } from '@cratis/scene.engine';
5
+ import { identityRoute } from './identityRoute';
6
+
7
+ /**
8
+ * The route a destination occupies in this renderer: the authored override or the identity-derived route -
9
+ * the same base `resolveDestination` builds a URL from - with surrounding slashes removed and every parameter
10
+ * placeholder (`{id}` or `:id`) reduced to one marker, so `/invoices/{id}` and `invoices/:invoiceId` are the
11
+ * same URL.
12
+ */
13
+ export const navigationRouteKey: NavigationRouteKey = destination => {
14
+ const route = destination.route ?? identityRoute(destination);
15
+ if (!route?.trim()) return undefined;
16
+ return route
17
+ .trim()
18
+ .split('/')
19
+ .filter(segment => segment.length > 0)
20
+ .map(segment => (/^\{[^}]*\}$/.test(segment) || /^:.+$/.test(segment) ? '{}' : segment))
21
+ .join('/');
22
+ };
@@ -4,6 +4,7 @@
4
4
  import { BindingScope, resolveBindingExpression } from '@cratis/scene.engine';
5
5
  import { DestinationKind, DestinationReference } from '@cratis/scene.model';
6
6
  import { DestinationResolution } from './DestinationResolution';
7
+ import { identityRoute } from './identityRoute';
7
8
 
8
9
  /**
9
10
  * Resolves a destination into a renderer-neutral navigation action, deep link and target outlet/dialog.
@@ -27,12 +28,6 @@ export function resolveDestination(destination: DestinationReference, scope: Bin
27
28
  };
28
29
  }
29
30
 
30
- function identityRoute(destination: DestinationReference): string | undefined {
31
- if (destination.screen) return destination.screen;
32
- const parts = [destination.module, destination.feature, destination.slice].filter((part): part is string => typeof part === 'string' && part.length > 0);
33
- return parts.length ? parts.join('/') : undefined;
34
- }
35
-
36
31
  function appendParameters(route: string, parameters: readonly (readonly [string, string])[]): string {
37
32
  if (!parameters.length) return route;
38
33
  let replaced = route;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cratis/scene.react",
3
- "version": "4.11.0",
3
+ "version": "4.13.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.11.0",
55
- "@cratis/scene.model": "4.11.0"
54
+ "@cratis/scene.engine": "4.13.0",
55
+ "@cratis/scene.model": "4.13.0"
56
56
  },
57
57
  "devDependencies": {
58
- "@cratis/scene.engine": "4.11.0",
59
- "@cratis/scene.model": "4.11.0",
58
+ "@cratis/scene.engine": "4.13.0",
59
+ "@cratis/scene.model": "4.13.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,18 @@
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 { SceneEdit } from '@cratis/scene.model';
5
+
6
+ /**
7
+ * What running a design-time action did. A batch is submitted to the host whole or not at all.
8
+ */
9
+ export interface DesignTimeActionOutcome {
10
+ /** Whether the edit batch was submitted to the host through `submitAction`. */
11
+ submitted: boolean;
12
+
13
+ /** The canonical edits the action produced. */
14
+ edits: SceneEdit[];
15
+
16
+ /** The action's own diagnostics, and any reason the batch was not submitted. */
17
+ diagnostics: string[];
18
+ }
@@ -0,0 +1,21 @@
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 { DesignTimeActionDescriptor } from '@cratis/scene.model';
5
+
6
+ /**
7
+ * One design-time action as a designer presents it, with the visibility and enablement its owning package
8
+ * decided. An action without a loaded handler is shown disabled, with the reason.
9
+ */
10
+ export interface DesignTimeActionState {
11
+ descriptor: DesignTimeActionDescriptor;
12
+
13
+ /** The package that owns the handler. */
14
+ package: string;
15
+
16
+ visible: boolean;
17
+ enabled: boolean;
18
+
19
+ /** Why the action is unavailable, when it is. */
20
+ diagnostic?: string;
21
+ }
@@ -0,0 +1,12 @@
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
+ /**
5
+ * The design-time extension contract this host implements, as `major.minor`.
6
+ *
7
+ * The contract is the set of extension points a package's design-time bundle fills - previews, designers,
8
+ * property editors, property displays and actions - and the props and context each receives. A minor
9
+ * version adds optional members; a major version changes or removes them. A host loads a package's
10
+ * contributions only when the package's declared major version matches this one.
11
+ */
12
+ export const DesignTimeContractVersion = '1.0';
@@ -0,0 +1,23 @@
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
+ /**
5
+ * The versioned design-time extension points a package can fill. The values are the keys shared by
6
+ * `PackageDesignTimeMetadata` (what a manifest declares) and `DesignTimeBundle` (what a bundle provides).
7
+ */
8
+ export enum DesignTimeExtensionPoint {
9
+ Preview = 'previews',
10
+ Designer = 'designers',
11
+ PropertyEditor = 'propertyEditors',
12
+ PropertyDisplay = 'propertyDisplays',
13
+ Action = 'actions',
14
+ }
15
+
16
+ /** How a diagnostic names an extension point. */
17
+ export const designTimeExtensionPointLabels: Record<DesignTimeExtensionPoint, string> = {
18
+ [DesignTimeExtensionPoint.Preview]: 'preview',
19
+ [DesignTimeExtensionPoint.Designer]: 'designer',
20
+ [DesignTimeExtensionPoint.PropertyEditor]: 'property editor',
21
+ [DesignTimeExtensionPoint.PropertyDisplay]: 'property display',
22
+ [DesignTimeExtensionPoint.Action]: 'action',
23
+ };
@@ -0,0 +1,23 @@
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
+ /**
5
+ * What a design-time host found for one extension point request.
6
+ *
7
+ * When `contribution` is absent the host renders its generic fallback - the default preview, the
8
+ * descriptor-driven inspector, the default editor for the property's value type - and `diagnostic` says
9
+ * why, unless nothing was requested in the first place.
10
+ */
11
+ export interface DesignTimeResolution<T> {
12
+ /** The package-provided contribution, when one was found and loaded. */
13
+ contribution?: T;
14
+
15
+ /** The package that provided it. */
16
+ package?: string;
17
+
18
+ /** The host-provided editor kind used instead, for property editors the host implements itself. */
19
+ hostKind?: string;
20
+
21
+ /** Why the generic fallback is used. Absent when the request was satisfied or nothing was requested. */
22
+ diagnostic?: string;
23
+ }
@@ -2,7 +2,7 @@
2
2
  // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
3
 
4
4
  import { DialogTemplate, Layout, ScreenTemplate, Theme, UiProfile } from '@cratis/scene.model';
5
- import { resolvePackageDependencies } from '@cratis/scene.engine';
5
+ import { TemplateCatalogEntry, describeTemplateCatalog, resolvePackageDependencies } from '@cratis/scene.engine';
6
6
  import { IconAdapterRegistry, createIconAdapterRegistry } from '../icons/IconAdapterRegistry';
7
7
  import { ScenePackageBundle, mergePackageRegistries, validatePackageBundle } from './ScenePackageBundle';
8
8
 
@@ -18,6 +18,9 @@ export interface PackageHostConfiguration {
18
18
  bundles: ScenePackageBundle[];
19
19
  profile: UiProfile;
20
20
  policy: PackageHostPolicy;
21
+
22
+ /** The Scene version the host runs, checked against each template's compatibility range when given. */
23
+ sceneVersion?: string;
21
24
  }
22
25
 
23
26
  export interface ResolvedPackageHost {
@@ -27,6 +30,12 @@ export interface ResolvedPackageHost {
27
30
  screenTemplates: ScreenTemplate[];
28
31
  dialogTemplates: DialogTemplate[];
29
32
  themes: Theme[];
33
+
34
+ /**
35
+ * Every template the approved packages provide, with compatibility, attribution and license metadata and
36
+ * whether it can be used with this package set. Template browsers read this rather than the raw lists.
37
+ */
38
+ templates: TemplateCatalogEntry[];
30
39
  icons: IconAdapterRegistry;
31
40
  assets: string[];
32
41
  fonts: string[];
@@ -100,6 +109,7 @@ export function resolvePackageHost(configuration: PackageHostConfiguration): Res
100
109
  screenTemplates: approved.flatMap(bundle => bundle.screenTemplates ?? []),
101
110
  dialogTemplates: approved.flatMap(bundle => bundle.dialogTemplates ?? []),
102
111
  themes: approved.flatMap(bundle => bundle.themes ?? []),
112
+ templates: describeTemplateCatalog(approved, configuration.sceneVersion),
103
113
  icons: createIconAdapterRegistry(approved.map(bundle => bundle.iconLibrary?.adapter).filter(adapter => adapter !== undefined)),
104
114
  assets: approvedAssets,
105
115
  fonts: approvedAssets.filter(isFontAsset),
package/packages/index.ts CHANGED
@@ -2,18 +2,26 @@
2
2
  // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
3
 
4
4
  export * from './DesignTimeAction';
5
+ export * from './DesignTimeActionOutcome';
6
+ export * from './DesignTimeActionState';
5
7
  export * from './DesignTimeBundle';
6
8
  export * from './DesignTimeComponentProps';
7
9
  export * from './DesignTimeContext';
10
+ export * from './DesignTimeContractVersion';
11
+ export * from './DesignTimeExtensionPoint';
8
12
  export * from './DesignTimePropertyDisplayProps';
9
13
  export * from './DesignTimePropertyEditorProps';
10
14
  export * from './DesignTimeProvider';
15
+ export * from './DesignTimeResolution';
11
16
  export * from '../navigation';
12
17
  export * from './EmbeddedPackageHost';
18
+ export * from './loadDesignTimeContributions';
13
19
  export * from './PackageHostAssets';
14
20
  export * from './PackageHostConfiguration';
15
21
  export * from './PackageHostRenderMode';
16
22
  export * from './PackageHostView';
23
+ export * from './resolveDesignTimeHost';
24
+ export * from './runDesignTimeAction';
17
25
  export * from './ScenePackageBundle';
18
26
  export * from './StandalonePackageHost';
19
27
  export * from './WebViewPackageHost';
@@ -0,0 +1,98 @@
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 { DesignTimeBundle } from './DesignTimeBundle';
5
+ import { DesignTimeContractVersion } from './DesignTimeContractVersion';
6
+ import { DesignTimeExtensionPoint, designTimeExtensionPointLabels } from './DesignTimeExtensionPoint';
7
+ import { ScenePackageBundle } from './ScenePackageBundle';
8
+
9
+ /**
10
+ * A package's design-time contributions as a host loaded them: only what the manifest declares, or nothing
11
+ * with the reason when the package cannot be loaded at all.
12
+ */
13
+ export interface LoadedDesignTimeContributions {
14
+ contributions: Partial<Record<DesignTimeExtensionPoint, Record<string, unknown>>>;
15
+ unavailableReason?: string;
16
+ }
17
+
18
+ /**
19
+ * Loads the design-time contributions of approved bundles, checking each against the contract version this
20
+ * host implements and against what its manifest declares. Nothing is partially trusted: a package with an
21
+ * incompatible or unreadable contract version, or contributions its manifest does not declare at all,
22
+ * contributes nothing; a single undeclared contribution is left out; a declared one the bundle lacks is
23
+ * reported and falls back.
24
+ */
25
+ export function loadDesignTimeContributions(
26
+ bundles: ScenePackageBundle[],
27
+ loadDesignTime: boolean,
28
+ ): { packages: Map<string, LoadedDesignTimeContributions>; diagnostics: string[] } {
29
+ const packages = new Map<string, LoadedDesignTimeContributions>();
30
+ const diagnostics: string[] = [];
31
+ const hostMajor = DesignTimeContractVersion.split('.')[0];
32
+
33
+ for (const { manifest, designTime } of bundles) {
34
+ const declared = manifest.designTime;
35
+ const unavailable = (reason: string) => {
36
+ diagnostics.push(reason);
37
+ packages.set(manifest.name, { contributions: {}, unavailableReason: reason });
38
+ };
39
+
40
+ if (!declared && !designTime) continue;
41
+ if (!loadDesignTime) {
42
+ packages.set(manifest.name, { contributions: {}, unavailableReason: `This host does not load design-time contributions, so package '${manifest.name}' uses generic editors` });
43
+ continue;
44
+ }
45
+
46
+ if (!declared) {
47
+ unavailable(`Package '${manifest.name}' provides design-time contributions its manifest does not declare; none are loaded`);
48
+ continue;
49
+ }
50
+
51
+ const version = declared.contractVersion ?? '1.0';
52
+ if (!/^\d+\.\d+$/.test(version)) {
53
+ unavailable(`Package '${manifest.name}' declares design-time contract version '${version}', which is not a major.minor version; none of its contributions are loaded`);
54
+ continue;
55
+ }
56
+
57
+ if (version.split('.')[0] !== hostMajor) {
58
+ unavailable(`Package '${manifest.name}' targets design-time contract ${version}, but this host implements ${DesignTimeContractVersion}; none of its contributions are loaded`);
59
+ continue;
60
+ }
61
+
62
+ packages.set(manifest.name, { contributions: declaredContributions(manifest.name, declared, designTime, diagnostics) });
63
+ }
64
+
65
+ return { packages, diagnostics };
66
+ }
67
+
68
+ function declaredContributions(
69
+ packageName: string,
70
+ declared: Record<DesignTimeExtensionPoint, string[]>,
71
+ provided: DesignTimeBundle | undefined,
72
+ diagnostics: string[],
73
+ ): LoadedDesignTimeContributions['contributions'] {
74
+ const contributions: LoadedDesignTimeContributions['contributions'] = {};
75
+
76
+ for (const point of Object.values(DesignTimeExtensionPoint)) {
77
+ const label = designTimeExtensionPointLabels[point];
78
+ const names = new Set(declared[point] ?? []);
79
+ const available = (provided?.[point] ?? {}) as Record<string, unknown>;
80
+ const loaded: Record<string, unknown> = {};
81
+
82
+ for (const name of names) {
83
+ if (name in available) {
84
+ loaded[name] = available[name];
85
+ } else {
86
+ diagnostics.push(`Package '${packageName}' declares the ${label} '${name}', but its design-time bundle does not provide it`);
87
+ }
88
+ }
89
+
90
+ for (const name of Object.keys(available).filter(name => !names.has(name))) {
91
+ diagnostics.push(`Package '${packageName}' provides the ${label} '${name}', which its manifest does not declare; it is not loaded`);
92
+ }
93
+
94
+ contributions[point] = loaded;
95
+ }
96
+
97
+ return contributions;
98
+ }
@@ -0,0 +1,133 @@
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 { ComponentType } from 'react';
5
+ import { ComponentDescriptor, PropertyDescriptor } from '@cratis/scene.model';
6
+ import { DesignTimeAction, DesignTimeActionContext } from './DesignTimeAction';
7
+ import { DesignTimeActionOutcome } from './DesignTimeActionOutcome';
8
+ import { DesignTimeActionState } from './DesignTimeActionState';
9
+ import { DesignTimeComponentProps } from './DesignTimeComponentProps';
10
+ import { DesignTimeExtensionPoint, designTimeExtensionPointLabels } from './DesignTimeExtensionPoint';
11
+ import { DesignTimePropertyDisplayProps } from './DesignTimePropertyDisplayProps';
12
+ import { DesignTimePropertyEditorProps } from './DesignTimePropertyEditorProps';
13
+ import { DesignTimeResolution } from './DesignTimeResolution';
14
+ import { LoadedDesignTimeContributions, loadDesignTimeContributions } from './loadDesignTimeContributions';
15
+ import { PackageHostConfiguration, ResolvedPackageHost, resolvePackageHost } from './PackageHostConfiguration';
16
+ import { runDesignTimeAction } from './runDesignTimeAction';
17
+
18
+ /**
19
+ * The generic design-time host contract: every package - first or third party - is reached through these
20
+ * lookups, so a designer never switches on package or component names.
21
+ */
22
+ export interface DesignTimeHost {
23
+ /** The runtime package host the design-time contributions were loaded alongside. */
24
+ host: ResolvedPackageHost;
25
+
26
+ /** Package-level problems: the host's own, contract version mismatches and declared/provided disagreements. */
27
+ diagnostics: string[];
28
+
29
+ preview(descriptor: ComponentDescriptor): DesignTimeResolution<ComponentType<DesignTimeComponentProps>>;
30
+ designer(descriptor: ComponentDescriptor): DesignTimeResolution<ComponentType<DesignTimeComponentProps>>;
31
+ propertyEditor(descriptor: ComponentDescriptor, property: PropertyDescriptor): DesignTimeResolution<ComponentType<DesignTimePropertyEditorProps>>;
32
+ propertyDisplay(descriptor: ComponentDescriptor): DesignTimeResolution<ComponentType<DesignTimePropertyDisplayProps>>;
33
+
34
+ /** The component's actions, with the visibility and enablement their owning package decides. */
35
+ actions(descriptor: ComponentDescriptor, context: DesignTimeActionContext): DesignTimeActionState[];
36
+
37
+ /** Runs an action and submits its canonical edit batch to the host through `context.submitAction`. */
38
+ runAction(descriptor: ComponentDescriptor, actionId: string, context: DesignTimeActionContext): DesignTimeActionOutcome;
39
+
40
+ /** Every unknown or unavailable extension the approved packages' descriptors ask for. */
41
+ diagnoseDescriptors(): string[];
42
+ }
43
+
44
+ /** Options for {@link resolveDesignTimeHost}. */
45
+ export interface DesignTimeHostOptions {
46
+ /** Property editor kinds the host implements itself, such as an icon picker or a query binder. */
47
+ hostEditorKinds?: string[];
48
+ }
49
+
50
+ /**
51
+ * Resolves a package host and the design-time contributions of its approved packages. Design-time code is
52
+ * only loaded when the host policy sets `loadDesignTime`; a runtime host gets generic fallbacks everywhere.
53
+ */
54
+ export function resolveDesignTimeHost(configuration: PackageHostConfiguration, options: DesignTimeHostOptions = {}): DesignTimeHost {
55
+ const host = resolvePackageHost(configuration);
56
+ const loaded = loadDesignTimeContributions(host.bundles, configuration.policy.loadDesignTime === true);
57
+ const hostEditorKinds = new Set(options.hostEditorKinds ?? []);
58
+ const approved = new Set(host.bundles.map(bundle => bundle.manifest.name));
59
+
60
+ const lookup = <T>(descriptor: ComponentDescriptor, point: DesignTimeExtensionPoint, kind: string | undefined): DesignTimeResolution<T> => {
61
+ if (!kind) return {};
62
+ const label = designTimeExtensionPointLabels[point];
63
+ const separator = kind.indexOf(':');
64
+ const owner = separator > 0 ? kind.substring(0, separator) : ownerOf(descriptor);
65
+ const name = separator > 0 ? kind.substring(separator + 1) : kind;
66
+ const outcome = point === DesignTimeExtensionPoint.Action ? 'the host shows it disabled' : `the host uses its generic ${label}`;
67
+ const fallback = (reason: string) => ({ diagnostic: `'${descriptor.component}' asks for the ${label} '${kind}', ${reason}; ${outcome}` });
68
+
69
+ if (!approved.has(owner)) return fallback(`but package '${owner}' is not approved by this host`);
70
+ const contributions: LoadedDesignTimeContributions | undefined = loaded.packages.get(owner);
71
+ if (contributions?.unavailableReason) return fallback(`but design-time contributions from package '${owner}' are not loaded`);
72
+ const contribution = contributions?.contributions[point]?.[name] as T | undefined;
73
+ if (contribution !== undefined) return { contribution, package: owner };
74
+ if (point === DesignTimeExtensionPoint.PropertyEditor && separator < 0 && hostEditorKinds.has(kind)) return { hostKind: kind };
75
+ return fallback(`which package '${owner}' does not provide`);
76
+ };
77
+
78
+ const actionOf = (descriptor: ComponentDescriptor, actionId: string): DesignTimeAction | undefined =>
79
+ loaded.packages.get(ownerOf(descriptor))?.contributions[DesignTimeExtensionPoint.Action]?.[actionId] as DesignTimeAction | undefined;
80
+
81
+ const actions = (descriptor: ComponentDescriptor, context: DesignTimeActionContext): DesignTimeActionState[] =>
82
+ (descriptor.actions ?? []).map(action => {
83
+ const state = { descriptor: action, package: ownerOf(descriptor) };
84
+ const handler = actionOf(descriptor, action.id);
85
+ if (!handler) {
86
+ const resolution = lookup<DesignTimeAction>(descriptor, DesignTimeExtensionPoint.Action, action.id);
87
+ return { ...state, visible: true, enabled: false, diagnostic: resolution.diagnostic };
88
+ }
89
+
90
+ try {
91
+ return { ...state, visible: handler.isVisible(context), enabled: handler.isEnabled(context) };
92
+ } catch (error) {
93
+ return { ...state, visible: true, enabled: false, diagnostic: `Action '${action.id}' could not decide its availability: ${String(error)}` };
94
+ }
95
+ });
96
+
97
+ const descriptors = host.bundles.flatMap(bundle => bundle.descriptors ?? []);
98
+
99
+ return {
100
+ host,
101
+ diagnostics: [...host.diagnostics, ...loaded.diagnostics],
102
+ preview: descriptor => lookup(descriptor, DesignTimeExtensionPoint.Preview, descriptor.previewKind),
103
+ designer: descriptor => lookup(descriptor, DesignTimeExtensionPoint.Designer, descriptor.editorKind),
104
+ propertyEditor: (descriptor, property) => lookup(descriptor, DesignTimeExtensionPoint.PropertyEditor, property.editorKind),
105
+ propertyDisplay: descriptor => lookup(descriptor, DesignTimeExtensionPoint.PropertyDisplay, descriptor.propertyDisplayKind),
106
+ actions,
107
+ runAction: (descriptor, actionId, context) => {
108
+ const state = actions(descriptor, context).find(action => action.descriptor.id === actionId);
109
+ const handler = actionOf(descriptor, actionId);
110
+ if (!state || !handler || !state.visible || !state.enabled) {
111
+ const reason = state?.diagnostic ?? (state ? `Action '${actionId}' is not available here` : `'${descriptor.component}' has no action '${actionId}'`);
112
+ return { submitted: false, edits: [], diagnostics: [reason] };
113
+ }
114
+
115
+ return runDesignTimeAction(handler, context);
116
+ },
117
+ diagnoseDescriptors: () =>
118
+ descriptors.flatMap(descriptor => [
119
+ lookup(descriptor, DesignTimeExtensionPoint.Preview, descriptor.previewKind),
120
+ lookup(descriptor, DesignTimeExtensionPoint.Designer, descriptor.editorKind),
121
+ lookup(descriptor, DesignTimeExtensionPoint.PropertyDisplay, descriptor.propertyDisplayKind),
122
+ ...descriptor.properties.map(property => lookup(descriptor, DesignTimeExtensionPoint.PropertyEditor, property.editorKind)),
123
+ ...(descriptor.actions ?? []).map(action => lookup(descriptor, DesignTimeExtensionPoint.Action, action.id)),
124
+ ])
125
+ .map(resolution => resolution.diagnostic)
126
+ .filter((diagnostic): diagnostic is string => diagnostic !== undefined),
127
+ };
128
+ }
129
+
130
+ function ownerOf(descriptor: ComponentDescriptor): string {
131
+ const separator = descriptor.component.indexOf(':');
132
+ return separator > 0 ? descriptor.component.substring(0, separator) : descriptor.component;
133
+ }
@@ -0,0 +1,36 @@
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 { SceneEdit, SceneEditKind } from '@cratis/scene.model';
5
+ import { DesignTimeAction, DesignTimeActionContext } from './DesignTimeAction';
6
+ import { DesignTimeActionOutcome } from './DesignTimeActionOutcome';
7
+
8
+ const editKinds = new Set<string>(Object.values(SceneEditKind));
9
+
10
+ /**
11
+ * Runs one design-time action and hands its edit batch to the host as a single canonical batch, so the host's
12
+ * own validation and undo/redo stay authoritative. A batch containing anything that is not a canonical Scene
13
+ * edit is rejected whole - a partially applied action is never reported as success.
14
+ */
15
+ export function runDesignTimeAction(action: DesignTimeAction, context: DesignTimeActionContext): DesignTimeActionOutcome {
16
+ let result: { edits: SceneEdit[]; diagnostics: string[] };
17
+ try {
18
+ result = action.execute(context);
19
+ } catch (error) {
20
+ return { submitted: false, edits: [], diagnostics: [`Action '${action.descriptor.id}' failed: ${String(error)}`] };
21
+ }
22
+
23
+ const invalid = result.edits.filter(edit => !editKinds.has((edit as { kind?: string }).kind ?? ''));
24
+ if (invalid.length > 0) {
25
+ return {
26
+ submitted: false,
27
+ edits: result.edits,
28
+ diagnostics: [...result.diagnostics, `Action '${action.descriptor.id}' produced ${invalid.length} edit(s) that are not canonical Scene edits; nothing was submitted`],
29
+ };
30
+ }
31
+
32
+ if (result.edits.length === 0) return { submitted: false, edits: [], diagnostics: result.diagnostics };
33
+
34
+ context.submitAction(action.descriptor.id, result.edits);
35
+ return { submitted: true, edits: result.edits, diagnostics: result.diagnostics };
36
+ }