@cratis/scene.engine 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.
Files changed (221) hide show
  1. package/BindingResolver.ts +11 -0
  2. package/ComponentResolution.ts +22 -0
  3. package/PackageSelection.ts +87 -0
  4. package/README.md +40 -0
  5. package/Renderer.ts +33 -0
  6. package/aggregateContributions.ts +40 -0
  7. package/buildStarterProfile.ts +21 -0
  8. package/computeSizeClass.ts +30 -0
  9. package/dist/cjs/PackageSelection.js +15 -0
  10. package/dist/cjs/PackageSelection.js.map +1 -0
  11. package/dist/cjs/aggregateContributions.js +37 -0
  12. package/dist/cjs/aggregateContributions.js.map +1 -0
  13. package/dist/cjs/buildStarterProfile.js +27 -0
  14. package/dist/cjs/buildStarterProfile.js.map +1 -0
  15. package/dist/cjs/computeSizeClass.js +31 -0
  16. package/dist/cjs/computeSizeClass.js.map +1 -0
  17. package/dist/cjs/elementKind.js +26 -0
  18. package/dist/cjs/elementKind.js.map +1 -0
  19. package/dist/cjs/evaluateFlowArrangement.js +31 -0
  20. package/dist/cjs/evaluateFlowArrangement.js.map +1 -0
  21. package/dist/cjs/evaluateFreeformArrangement.js +16 -0
  22. package/dist/cjs/evaluateFreeformArrangement.js.map +1 -0
  23. package/dist/cjs/evaluateFreeformSlotArrangement.js +18 -0
  24. package/dist/cjs/evaluateFreeformSlotArrangement.js.map +1 -0
  25. package/dist/cjs/flowNodeKind.js +40 -0
  26. package/dist/cjs/flowNodeKind.js.map +1 -0
  27. package/dist/cjs/incompatibleStarterThemes.js +29 -0
  28. package/dist/cjs/incompatibleStarterThemes.js.map +1 -0
  29. package/dist/cjs/index.js +59 -0
  30. package/dist/cjs/index.js.map +1 -0
  31. package/dist/cjs/package.json +3 -0
  32. package/dist/cjs/packageCatalog.js +63 -0
  33. package/dist/cjs/packageCatalog.js.map +1 -0
  34. package/dist/cjs/packageVersionRange.js +113 -0
  35. package/dist/cjs/packageVersionRange.js.map +1 -0
  36. package/dist/cjs/renderElement.js +39 -0
  37. package/dist/cjs/renderElement.js.map +1 -0
  38. package/dist/cjs/resolveComponentName.js +67 -0
  39. package/dist/cjs/resolveComponentName.js.map +1 -0
  40. package/dist/cjs/resolvePackageDependencies.js +149 -0
  41. package/dist/cjs/resolvePackageDependencies.js.map +1 -0
  42. package/dist/cjs/resolveScreenTemplates.js +136 -0
  43. package/dist/cjs/resolveScreenTemplates.js.map +1 -0
  44. package/dist/cjs/themeCompatibility.js +45 -0
  45. package/dist/cjs/themeCompatibility.js.map +1 -0
  46. package/dist/esm/BindingResolver.d.ts +3 -0
  47. package/dist/esm/BindingResolver.d.ts.map +1 -0
  48. package/dist/esm/BindingResolver.js +2 -0
  49. package/dist/esm/BindingResolver.js.map +1 -0
  50. package/dist/esm/ComponentResolution.d.ts +6 -0
  51. package/dist/esm/ComponentResolution.d.ts.map +1 -0
  52. package/dist/esm/ComponentResolution.js +2 -0
  53. package/dist/esm/ComponentResolution.js.map +1 -0
  54. package/dist/esm/PackageSelection.d.ts +19 -0
  55. package/dist/esm/PackageSelection.d.ts.map +1 -0
  56. package/dist/esm/PackageSelection.js +13 -0
  57. package/dist/esm/PackageSelection.js.map +1 -0
  58. package/dist/esm/Renderer.d.ts +8 -0
  59. package/dist/esm/Renderer.d.ts.map +1 -0
  60. package/dist/esm/Renderer.js +2 -0
  61. package/dist/esm/Renderer.js.map +1 -0
  62. package/dist/esm/aggregateContributions.d.ts +3 -0
  63. package/dist/esm/aggregateContributions.d.ts.map +1 -0
  64. package/dist/esm/aggregateContributions.js +35 -0
  65. package/dist/esm/aggregateContributions.js.map +1 -0
  66. package/dist/esm/buildStarterProfile.d.ts +3 -0
  67. package/dist/esm/buildStarterProfile.d.ts.map +1 -0
  68. package/dist/esm/buildStarterProfile.js +25 -0
  69. package/dist/esm/buildStarterProfile.js.map +1 -0
  70. package/dist/esm/computeSizeClass.d.ts +5 -0
  71. package/dist/esm/computeSizeClass.d.ts.map +1 -0
  72. package/dist/esm/computeSizeClass.js +27 -0
  73. package/dist/esm/computeSizeClass.js.map +1 -0
  74. package/dist/esm/elementKind.d.ts +6 -0
  75. package/dist/esm/elementKind.d.ts.map +1 -0
  76. package/dist/esm/elementKind.js +21 -0
  77. package/dist/esm/elementKind.js.map +1 -0
  78. package/dist/esm/evaluateFlowArrangement.d.ts +3 -0
  79. package/dist/esm/evaluateFlowArrangement.d.ts.map +1 -0
  80. package/dist/esm/evaluateFlowArrangement.js +29 -0
  81. package/dist/esm/evaluateFlowArrangement.js.map +1 -0
  82. package/dist/esm/evaluateFreeformArrangement.d.ts +3 -0
  83. package/dist/esm/evaluateFreeformArrangement.d.ts.map +1 -0
  84. package/dist/esm/evaluateFreeformArrangement.js +14 -0
  85. package/dist/esm/evaluateFreeformArrangement.js.map +1 -0
  86. package/dist/esm/evaluateFreeformSlotArrangement.d.ts +3 -0
  87. package/dist/esm/evaluateFreeformSlotArrangement.d.ts.map +1 -0
  88. package/dist/esm/evaluateFreeformSlotArrangement.js +16 -0
  89. package/dist/esm/evaluateFreeformSlotArrangement.js.map +1 -0
  90. package/dist/esm/flowNodeKind.d.ts +8 -0
  91. package/dist/esm/flowNodeKind.d.ts.map +1 -0
  92. package/dist/esm/flowNodeKind.js +33 -0
  93. package/dist/esm/flowNodeKind.js.map +1 -0
  94. package/dist/esm/for_aggregateContributions/when_aggregating_by_contribution_point.d.ts +2 -0
  95. package/dist/esm/for_aggregateContributions/when_aggregating_by_contribution_point.d.ts.map +1 -0
  96. package/dist/esm/for_aggregateContributions/when_aggregating_by_contribution_point.js +23 -0
  97. package/dist/esm/for_aggregateContributions/when_aggregating_by_contribution_point.js.map +1 -0
  98. package/dist/esm/for_buildStarterProfile/when_checking_against_the_shared_fixture_corpus.d.ts +2 -0
  99. package/dist/esm/for_buildStarterProfile/when_checking_against_the_shared_fixture_corpus.d.ts.map +1 -0
  100. package/dist/esm/for_buildStarterProfile/when_checking_against_the_shared_fixture_corpus.js +16 -0
  101. package/dist/esm/for_buildStarterProfile/when_checking_against_the_shared_fixture_corpus.js.map +1 -0
  102. package/dist/esm/for_computeSizeClass/when_computing_the_size_class.d.ts +2 -0
  103. package/dist/esm/for_computeSizeClass/when_computing_the_size_class.d.ts.map +1 -0
  104. package/dist/esm/for_computeSizeClass/when_computing_the_size_class.js +17 -0
  105. package/dist/esm/for_computeSizeClass/when_computing_the_size_class.js.map +1 -0
  106. package/dist/esm/for_evaluateFlowArrangement/when_evaluating_against_the_shared_fixture_corpus.d.ts +2 -0
  107. package/dist/esm/for_evaluateFlowArrangement/when_evaluating_against_the_shared_fixture_corpus.d.ts.map +1 -0
  108. package/dist/esm/for_evaluateFlowArrangement/when_evaluating_against_the_shared_fixture_corpus.js +43 -0
  109. package/dist/esm/for_evaluateFlowArrangement/when_evaluating_against_the_shared_fixture_corpus.js.map +1 -0
  110. package/dist/esm/for_evaluateFreeformArrangement/when_evaluating_against_the_shared_fixture_corpus.d.ts +2 -0
  111. package/dist/esm/for_evaluateFreeformArrangement/when_evaluating_against_the_shared_fixture_corpus.d.ts.map +1 -0
  112. package/dist/esm/for_evaluateFreeformArrangement/when_evaluating_against_the_shared_fixture_corpus.js +46 -0
  113. package/dist/esm/for_evaluateFreeformArrangement/when_evaluating_against_the_shared_fixture_corpus.js.map +1 -0
  114. package/dist/esm/for_evaluateFreeformSlotArrangement/when_evaluating_against_the_shared_fixture_corpus.d.ts +2 -0
  115. package/dist/esm/for_evaluateFreeformSlotArrangement/when_evaluating_against_the_shared_fixture_corpus.d.ts.map +1 -0
  116. package/dist/esm/for_evaluateFreeformSlotArrangement/when_evaluating_against_the_shared_fixture_corpus.js +25 -0
  117. package/dist/esm/for_evaluateFreeformSlotArrangement/when_evaluating_against_the_shared_fixture_corpus.js.map +1 -0
  118. package/dist/esm/for_incompatibleStarterThemes/when_checking_against_the_shared_fixture_corpus.d.ts +2 -0
  119. package/dist/esm/for_incompatibleStarterThemes/when_checking_against_the_shared_fixture_corpus.d.ts.map +1 -0
  120. package/dist/esm/for_incompatibleStarterThemes/when_checking_against_the_shared_fixture_corpus.js +17 -0
  121. package/dist/esm/for_incompatibleStarterThemes/when_checking_against_the_shared_fixture_corpus.js.map +1 -0
  122. package/dist/esm/for_packageCatalog/when_querying_against_the_shared_fixture_corpus.d.ts +2 -0
  123. package/dist/esm/for_packageCatalog/when_querying_against_the_shared_fixture_corpus.d.ts.map +1 -0
  124. package/dist/esm/for_packageCatalog/when_querying_against_the_shared_fixture_corpus.js +22 -0
  125. package/dist/esm/for_packageCatalog/when_querying_against_the_shared_fixture_corpus.js.map +1 -0
  126. package/dist/esm/for_packageDependencies/fixtures.d.ts +38 -0
  127. package/dist/esm/for_packageDependencies/fixtures.d.ts.map +1 -0
  128. package/dist/esm/for_packageDependencies/fixtures.js +5 -0
  129. package/dist/esm/for_packageDependencies/fixtures.js.map +1 -0
  130. package/dist/esm/for_packageDependencies/when_resolving_against_the_shared_fixture_corpus.d.ts +2 -0
  131. package/dist/esm/for_packageDependencies/when_resolving_against_the_shared_fixture_corpus.d.ts.map +1 -0
  132. package/dist/esm/for_packageDependencies/when_resolving_against_the_shared_fixture_corpus.js +29 -0
  133. package/dist/esm/for_packageDependencies/when_resolving_against_the_shared_fixture_corpus.js.map +1 -0
  134. package/dist/esm/for_packageVersionRange/when_checking_against_the_shared_fixture_corpus.d.ts +2 -0
  135. package/dist/esm/for_packageVersionRange/when_checking_against_the_shared_fixture_corpus.d.ts.map +1 -0
  136. package/dist/esm/for_packageVersionRange/when_checking_against_the_shared_fixture_corpus.js +10 -0
  137. package/dist/esm/for_packageVersionRange/when_checking_against_the_shared_fixture_corpus.js.map +1 -0
  138. package/dist/esm/for_renderElement/when_rendering_a_mixed_tree.d.ts +2 -0
  139. package/dist/esm/for_renderElement/when_rendering_a_mixed_tree.d.ts.map +1 -0
  140. package/dist/esm/for_renderElement/when_rendering_a_mixed_tree.js +46 -0
  141. package/dist/esm/for_renderElement/when_rendering_a_mixed_tree.js.map +1 -0
  142. package/dist/esm/for_resolveComponentName/when_resolving_against_the_shared_fixture_corpus.d.ts +2 -0
  143. package/dist/esm/for_resolveComponentName/when_resolving_against_the_shared_fixture_corpus.d.ts.map +1 -0
  144. package/dist/esm/for_resolveComponentName/when_resolving_against_the_shared_fixture_corpus.js +20 -0
  145. package/dist/esm/for_resolveComponentName/when_resolving_against_the_shared_fixture_corpus.js.map +1 -0
  146. package/dist/esm/for_resolveScreenTemplates/when_resolving_against_the_shared_fixture_corpus.d.ts +2 -0
  147. package/dist/esm/for_resolveScreenTemplates/when_resolving_against_the_shared_fixture_corpus.d.ts.map +1 -0
  148. package/dist/esm/for_resolveScreenTemplates/when_resolving_against_the_shared_fixture_corpus.js +30 -0
  149. package/dist/esm/for_resolveScreenTemplates/when_resolving_against_the_shared_fixture_corpus.js.map +1 -0
  150. package/dist/esm/for_themeCompatibility/when_checking_against_the_shared_fixture_corpus.d.ts +2 -0
  151. package/dist/esm/for_themeCompatibility/when_checking_against_the_shared_fixture_corpus.d.ts.map +1 -0
  152. package/dist/esm/for_themeCompatibility/when_checking_against_the_shared_fixture_corpus.js +18 -0
  153. package/dist/esm/for_themeCompatibility/when_checking_against_the_shared_fixture_corpus.js.map +1 -0
  154. package/dist/esm/incompatibleStarterThemes.d.ts +3 -0
  155. package/dist/esm/incompatibleStarterThemes.d.ts.map +1 -0
  156. package/dist/esm/incompatibleStarterThemes.js +27 -0
  157. package/dist/esm/incompatibleStarterThemes.js.map +1 -0
  158. package/dist/esm/index.d.ts +21 -0
  159. package/dist/esm/index.d.ts.map +1 -0
  160. package/dist/esm/index.js +18 -0
  161. package/dist/esm/index.js.map +1 -0
  162. package/dist/esm/package.json +3 -0
  163. package/dist/esm/packageCatalog.d.ts +7 -0
  164. package/dist/esm/packageCatalog.d.ts.map +1 -0
  165. package/dist/esm/packageCatalog.js +57 -0
  166. package/dist/esm/packageCatalog.js.map +1 -0
  167. package/dist/esm/packageVersionRange.d.ts +2 -0
  168. package/dist/esm/packageVersionRange.d.ts.map +1 -0
  169. package/dist/esm/packageVersionRange.js +111 -0
  170. package/dist/esm/packageVersionRange.js.map +1 -0
  171. package/dist/esm/renderElement.d.ts +5 -0
  172. package/dist/esm/renderElement.d.ts.map +1 -0
  173. package/dist/esm/renderElement.js +37 -0
  174. package/dist/esm/renderElement.js.map +1 -0
  175. package/dist/esm/resolveComponentName.d.ts +7 -0
  176. package/dist/esm/resolveComponentName.d.ts.map +1 -0
  177. package/dist/esm/resolveComponentName.js +63 -0
  178. package/dist/esm/resolveComponentName.js.map +1 -0
  179. package/dist/esm/resolvePackageDependencies.d.ts +4 -0
  180. package/dist/esm/resolvePackageDependencies.d.ts.map +1 -0
  181. package/dist/esm/resolvePackageDependencies.js +147 -0
  182. package/dist/esm/resolvePackageDependencies.js.map +1 -0
  183. package/dist/esm/resolveScreenTemplates.d.ts +20 -0
  184. package/dist/esm/resolveScreenTemplates.d.ts.map +1 -0
  185. package/dist/esm/resolveScreenTemplates.js +133 -0
  186. package/dist/esm/resolveScreenTemplates.js.map +1 -0
  187. package/dist/esm/themeCompatibility.d.ts +5 -0
  188. package/dist/esm/themeCompatibility.d.ts.map +1 -0
  189. package/dist/esm/themeCompatibility.js +41 -0
  190. package/dist/esm/themeCompatibility.js.map +1 -0
  191. package/dist/esm/tsconfig.tsbuildinfo +1 -0
  192. package/elementKind.ts +26 -0
  193. package/evaluateFlowArrangement.ts +35 -0
  194. package/evaluateFreeformArrangement.ts +15 -0
  195. package/evaluateFreeformSlotArrangement.ts +17 -0
  196. package/flowNodeKind.ts +38 -0
  197. package/for_aggregateContributions/when_aggregating_by_contribution_point.ts +31 -0
  198. package/for_buildStarterProfile/when_checking_against_the_shared_fixture_corpus.ts +32 -0
  199. package/for_computeSizeClass/when_computing_the_size_class.ts +23 -0
  200. package/for_evaluateFlowArrangement/when_evaluating_against_the_shared_fixture_corpus.ts +67 -0
  201. package/for_evaluateFreeformArrangement/when_evaluating_against_the_shared_fixture_corpus.ts +68 -0
  202. package/for_evaluateFreeformSlotArrangement/when_evaluating_against_the_shared_fixture_corpus.ts +47 -0
  203. package/for_incompatibleStarterThemes/when_checking_against_the_shared_fixture_corpus.ts +34 -0
  204. package/for_packageCatalog/when_querying_against_the_shared_fixture_corpus.ts +26 -0
  205. package/for_packageDependencies/fixtures.ts +44 -0
  206. package/for_packageDependencies/when_resolving_against_the_shared_fixture_corpus.ts +39 -0
  207. package/for_packageVersionRange/when_checking_against_the_shared_fixture_corpus.ts +13 -0
  208. package/for_renderElement/when_rendering_a_mixed_tree.ts +58 -0
  209. package/for_resolveComponentName/when_resolving_against_the_shared_fixture_corpus.ts +36 -0
  210. package/for_resolveScreenTemplates/when_resolving_against_the_shared_fixture_corpus.ts +49 -0
  211. package/for_themeCompatibility/when_checking_against_the_shared_fixture_corpus.ts +37 -0
  212. package/incompatibleStarterThemes.ts +24 -0
  213. package/index.ts +23 -0
  214. package/package.json +45 -0
  215. package/packageCatalog.ts +65 -0
  216. package/packageVersionRange.ts +90 -0
  217. package/renderElement.ts +40 -0
  218. package/resolveComponentName.ts +66 -0
  219. package/resolvePackageDependencies.ts +156 -0
  220. package/resolveScreenTemplates.ts +191 -0
  221. package/themeCompatibility.ts +46 -0
@@ -0,0 +1,67 @@
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 { readFileSync } from 'node:fs';
5
+ import { join } from 'node:path';
6
+ import { ExternalComponent, FlowArrangement, FlowLeaf, FlowOverride, HeightSizeClass, HorizontalAlignment, SizeClass, VerticalAlignment, Visibility, WidthSizeClass } from '@cratis/scene.model';
7
+ import { evaluateFlowArrangement } from '../index';
8
+
9
+ interface FixtureOverride {
10
+ width: WidthSizeClass | null;
11
+ height: HeightSizeClass | null;
12
+ tag: string;
13
+ }
14
+
15
+ interface FixtureCase {
16
+ name: string;
17
+ rootTag: string;
18
+ overrides: FixtureOverride[];
19
+ sizeClass: SizeClass;
20
+ expectedTag: string;
21
+ }
22
+
23
+ interface FixtureCorpus {
24
+ flowCases: FixtureCase[];
25
+ }
26
+
27
+ const manifestPath = join(import.meta.dirname, '..', '..', '..', '..', 'layout-evaluation-fixtures.json');
28
+ const corpus = JSON.parse(readFileSync(manifestPath, 'utf-8')) as FixtureCorpus;
29
+
30
+ function leaf(tag: string): FlowLeaf {
31
+ const content: ExternalComponent = {
32
+ id: tag,
33
+ name: tag,
34
+ properties: {},
35
+ visibility: Visibility.Visible,
36
+ isEnabled: true,
37
+ opacity: 1,
38
+ size: {},
39
+ zIndex: 0,
40
+ minimumSize: {},
41
+ maximumSize: {},
42
+ margin: { left: 0, top: 0, right: 0, bottom: 0 },
43
+ horizontalAlignment: HorizontalAlignment.Stretch,
44
+ verticalAlignment: VerticalAlignment.Stretch,
45
+ componentName: 'core:text',
46
+ slots: {},
47
+ };
48
+ return { content };
49
+ }
50
+
51
+ describe('when evaluating against the shared fixture corpus', () => {
52
+ for (const fixtureCase of corpus.flowCases) {
53
+ it(`should match the expected tag for "${fixtureCase.name}"`, () => {
54
+ const arrangement: FlowArrangement = {
55
+ root: leaf(fixtureCase.rootTag),
56
+ overrides: fixtureCase.overrides.map((override): FlowOverride => ({
57
+ width: override.width ?? undefined,
58
+ height: override.height ?? undefined,
59
+ root: leaf(override.tag),
60
+ })),
61
+ };
62
+
63
+ const result = evaluateFlowArrangement(arrangement, fixtureCase.sizeClass) as FlowLeaf;
64
+ (result.content as ExternalComponent).id.should.equal(fixtureCase.expectedTag);
65
+ });
66
+ }
67
+ });
@@ -0,0 +1,68 @@
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 { readFileSync } from 'node:fs';
5
+ import { join } from 'node:path';
6
+ import { ElementPlacement, ExternalComponent, FreeformArrangement, HeightSizeClass, HorizontalAlignment, SizeClass, VerticalAlignment, Visibility, WidthSizeClass } from '@cratis/scene.model';
7
+ import { evaluateFreeformArrangement } from '../index';
8
+
9
+ interface FixtureVariant {
10
+ width: WidthSizeClass;
11
+ height: HeightSizeClass;
12
+ tag: string;
13
+ }
14
+
15
+ interface FixtureCase {
16
+ name: string;
17
+ variants: FixtureVariant[];
18
+ sizeClass: SizeClass;
19
+ expectedTag: string | null;
20
+ }
21
+
22
+ interface FixtureCorpus {
23
+ freeformCases: FixtureCase[];
24
+ }
25
+
26
+ const manifestPath = join(import.meta.dirname, '..', '..', '..', '..', 'layout-evaluation-fixtures.json');
27
+ const corpus = JSON.parse(readFileSync(manifestPath, 'utf-8')) as FixtureCorpus;
28
+
29
+ function placement(tag: string): ElementPlacement {
30
+ const element: ExternalComponent = {
31
+ id: tag,
32
+ name: tag,
33
+ properties: {},
34
+ visibility: Visibility.Visible,
35
+ isEnabled: true,
36
+ opacity: 1,
37
+ size: {},
38
+ zIndex: 0,
39
+ minimumSize: {},
40
+ maximumSize: {},
41
+ margin: { left: 0, top: 0, right: 0, bottom: 0 },
42
+ horizontalAlignment: HorizontalAlignment.Stretch,
43
+ verticalAlignment: VerticalAlignment.Stretch,
44
+ componentName: 'core:text',
45
+ slots: {},
46
+ };
47
+ return { element, x: 0, y: 0, width: 0, height: 0 };
48
+ }
49
+
50
+ describe('when evaluating against the shared fixture corpus', () => {
51
+ for (const fixtureCase of corpus.freeformCases) {
52
+ it(`should match the expected tag for "${fixtureCase.name}"`, () => {
53
+ const arrangement: FreeformArrangement = {
54
+ variants: fixtureCase.variants.map(variant => ({
55
+ sizeClass: { width: variant.width, height: variant.height },
56
+ placements: [placement(variant.tag)],
57
+ })),
58
+ };
59
+
60
+ const result = evaluateFreeformArrangement(arrangement, fixtureCase.sizeClass);
61
+ if (fixtureCase.expectedTag === null) {
62
+ (result === undefined).should.be.true;
63
+ } else {
64
+ (result!.placements[0].element as ExternalComponent).id.should.equal(fixtureCase.expectedTag);
65
+ }
66
+ });
67
+ }
68
+ });
@@ -0,0 +1,47 @@
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 { readFileSync } from 'node:fs';
5
+ import { join } from 'node:path';
6
+ import { FreeformSlotArrangement, HeightSizeClass, SizeClass, WidthSizeClass } from '@cratis/scene.model';
7
+ import { evaluateFreeformSlotArrangement } from '../index';
8
+
9
+ interface FixtureVariant {
10
+ width: WidthSizeClass;
11
+ height: HeightSizeClass;
12
+ tag: string;
13
+ }
14
+
15
+ interface FixtureCase {
16
+ name: string;
17
+ variants: FixtureVariant[];
18
+ sizeClass: SizeClass;
19
+ expectedTag: string | null;
20
+ }
21
+
22
+ interface FixtureCorpus {
23
+ freeformCases: FixtureCase[];
24
+ }
25
+
26
+ const manifestPath = join(import.meta.dirname, '..', '..', '..', '..', 'layout-evaluation-fixtures.json');
27
+ const corpus = JSON.parse(readFileSync(manifestPath, 'utf-8')) as FixtureCorpus;
28
+
29
+ describe('when evaluating against the shared fixture corpus', () => {
30
+ for (const fixtureCase of corpus.freeformCases) {
31
+ it(`should match the expected tag for "${fixtureCase.name}"`, () => {
32
+ const arrangement: FreeformSlotArrangement = {
33
+ variants: fixtureCase.variants.map(variant => ({
34
+ sizeClass: { width: variant.width, height: variant.height },
35
+ placements: [{ slotName: variant.tag, x: 0, y: 0, width: 0, height: 0 }],
36
+ })),
37
+ };
38
+
39
+ const result = evaluateFreeformSlotArrangement(arrangement, fixtureCase.sizeClass);
40
+ if (fixtureCase.expectedTag === null) {
41
+ (result === undefined).should.be.true;
42
+ } else {
43
+ result!.placements[0].slotName.should.equal(fixtureCase.expectedTag);
44
+ }
45
+ });
46
+ }
47
+ });
@@ -0,0 +1,34 @@
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 { readFileSync } from 'node:fs';
5
+ import { join } from 'node:path';
6
+ import { Theme, UiStarter } from '@cratis/scene.model';
7
+ import { incompatibleStarterThemes } from '../index';
8
+
9
+ interface FixtureCase {
10
+ name: string;
11
+ starter: UiStarter;
12
+ themes: Record<string, Theme>;
13
+ expectedIncompatible: string[];
14
+ }
15
+
16
+ interface FixtureCorpus {
17
+ themeCases: FixtureCase[];
18
+ }
19
+
20
+ const manifestPath = join(import.meta.dirname, '..', '..', '..', '..', 'ui-starter-fixtures.json');
21
+ const corpus = JSON.parse(readFileSync(manifestPath, 'utf-8')) as FixtureCorpus;
22
+
23
+ describe('when checking against the shared fixture corpus', () => {
24
+ for (const fixtureCase of corpus.themeCases) {
25
+ const themes: Record<string, Theme> = {};
26
+ for (const [themeName, theme] of Object.entries(fixtureCase.themes)) {
27
+ themes[themeName] = { name: themeName, compatibleWith: theme.compatibleWith };
28
+ }
29
+
30
+ it(`should match the expected incompatible themes for "${fixtureCase.name}"`, () => {
31
+ incompatibleStarterThemes(fixtureCase.starter, themes).should.deep.equal(fixtureCase.expectedIncompatible);
32
+ });
33
+ }
34
+ });
@@ -0,0 +1,26 @@
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 { availablePackagesFor, baseComponentLibraries, componentsForPackages } from '../index';
5
+ import { CatalogFixtureCase, corpus } from '../for_packageDependencies/fixtures';
6
+
7
+ function run(fixtureCase: CatalogFixtureCase): string[] {
8
+ switch (fixtureCase.query) {
9
+ case 'baseComponentLibraries':
10
+ return baseComponentLibraries(corpus.catalog).map((scenePackage) => scenePackage.name);
11
+ case 'availableFor':
12
+ return availablePackagesFor(corpus.catalog, fixtureCase.selected ?? []).map((scenePackage) => scenePackage.name);
13
+ case 'componentsFor':
14
+ return componentsForPackages(corpus.catalog, fixtureCase.selected ?? []);
15
+ default:
16
+ throw new Error(`The fixture corpus asks for an unknown query '${fixtureCase.query}'`);
17
+ }
18
+ }
19
+
20
+ describe('when querying against the shared fixture corpus', () => {
21
+ for (const fixtureCase of corpus.catalogCases) {
22
+ it(`should match the expected result for "${fixtureCase.name}"`, () => {
23
+ run(fixtureCase).should.deep.equal(fixtureCase.expected);
24
+ });
25
+ }
26
+ });
@@ -0,0 +1,44 @@
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 { readFileSync } from 'node:fs';
5
+ import { join } from 'node:path';
6
+ import { ScenePackage } from '@cratis/scene.model';
7
+
8
+ export interface ResolutionFixtureCase {
9
+ name: string;
10
+ selected: string[];
11
+ expectedPackages: string[];
12
+ expectedAdded: string[];
13
+ expectedMissing: { package: string; dependsOn: string }[];
14
+ expectedVersionConflicts: { package: string; dependsOn: string; requiredRange: string; actualVersion: string }[];
15
+ expectedCycles: string[][];
16
+ }
17
+
18
+ export interface VersionRangeFixtureCase {
19
+ version: string;
20
+ range: string | null;
21
+ expected: boolean;
22
+ }
23
+
24
+ export interface CatalogFixtureCase {
25
+ name: string;
26
+ query: string;
27
+ selected?: string[];
28
+ expected: string[];
29
+ }
30
+
31
+ interface FixtureCorpus {
32
+ catalog: ScenePackage[];
33
+ resolutionCases: ResolutionFixtureCase[];
34
+ versionRangeCases: VersionRangeFixtureCase[];
35
+ catalogCases: CatalogFixtureCase[];
36
+ }
37
+
38
+ const fixturePath = join(import.meta.dirname, '..', '..', '..', '..', 'package-dependency-fixtures.json');
39
+
40
+ /**
41
+ * The same corpus the C# specs read, so both languages resolve against an identical catalog rather than
42
+ * two hand-written ones that can quietly diverge.
43
+ */
44
+ export const corpus = JSON.parse(readFileSync(fixturePath, 'utf-8')) as FixtureCorpus;
@@ -0,0 +1,39 @@
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 { isPackageSelectionValid, resolvePackageDependencies } from '../index';
5
+ import { corpus } from './fixtures';
6
+
7
+ describe('when resolving against the shared fixture corpus', () => {
8
+ for (const fixtureCase of corpus.resolutionCases) {
9
+ const selection = resolvePackageDependencies(fixtureCase.selected, corpus.catalog);
10
+
11
+ it(`should order the packages as expected for "${fixtureCase.name}"`, () => {
12
+ selection.packages.should.deep.equal(fixtureCase.expectedPackages);
13
+ });
14
+
15
+ it(`should report the expected transitively added packages for "${fixtureCase.name}"`, () => {
16
+ selection.added.should.deep.equal(fixtureCase.expectedAdded);
17
+ });
18
+
19
+ it(`should report the expected missing dependencies for "${fixtureCase.name}"`, () => {
20
+ selection.missing.should.deep.equal(fixtureCase.expectedMissing);
21
+ });
22
+
23
+ it(`should report the expected version conflicts for "${fixtureCase.name}"`, () => {
24
+ selection.versionConflicts.should.deep.equal(fixtureCase.expectedVersionConflicts);
25
+ });
26
+
27
+ it(`should report the expected cycles for "${fixtureCase.name}"`, () => {
28
+ selection.cycles.should.deep.equal(fixtureCase.expectedCycles);
29
+ });
30
+
31
+ it(`should consider "${fixtureCase.name}" valid only when nothing is wrong`, () => {
32
+ const expectedValid =
33
+ fixtureCase.expectedMissing.length === 0 &&
34
+ fixtureCase.expectedVersionConflicts.length === 0 &&
35
+ fixtureCase.expectedCycles.length === 0;
36
+ isPackageSelectionValid(selection).should.equal(expectedValid);
37
+ });
38
+ }
39
+ });
@@ -0,0 +1,13 @@
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 { isVersionSatisfiedBy } from '../index';
5
+ import { corpus } from '../for_packageDependencies/fixtures';
6
+
7
+ describe('when checking against the shared fixture corpus', () => {
8
+ for (const fixtureCase of corpus.versionRangeCases) {
9
+ it(`should decide ${fixtureCase.version} against '${fixtureCase.range ?? '<null>'}' the same way the corpus does`, () => {
10
+ isVersionSatisfiedBy(fixtureCase.version, fixtureCase.range ?? undefined).should.equal(fixtureCase.expected);
11
+ });
12
+ }
13
+ });
@@ -0,0 +1,58 @@
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 { ContentControl, Control, ExternalComponent, HorizontalAlignment, ItemsControl, Panel, VerticalAlignment, Visibility } from '@cratis/scene.model';
5
+ import { Renderer, renderElement } from '../index';
6
+
7
+ function control(id: string): Control {
8
+ return {
9
+ id,
10
+ name: id,
11
+ properties: {},
12
+ visibility: Visibility.Visible,
13
+ isEnabled: true,
14
+ opacity: 1,
15
+ size: {},
16
+ zIndex: 0,
17
+ minimumSize: {},
18
+ maximumSize: {},
19
+ margin: { left: 0, top: 0, right: 0, bottom: 0 },
20
+ horizontalAlignment: HorizontalAlignment.Stretch,
21
+ verticalAlignment: VerticalAlignment.Stretch,
22
+ borderThickness: { left: 0, top: 0, right: 0, bottom: 0 },
23
+ padding: { left: 0, top: 0, right: 0, bottom: 0 },
24
+ tabIndex: 0,
25
+ };
26
+ }
27
+
28
+ class StringRenderer implements Renderer<string> {
29
+ renderContentControl(element: ContentControl, content: string): string {
30
+ return `ContentControl(${element.id})[${content}]`;
31
+ }
32
+
33
+ renderItemsControl(element: ItemsControl, items: string[]): string {
34
+ return `ItemsControl(${element.id})[${items.join(',')}]`;
35
+ }
36
+
37
+ renderExternalComponent(element: ExternalComponent, slots: Record<string, string[]>): string {
38
+ const slotsText = Object.entries(slots).map(([name, children]) => `${name}=${children.join(',')}`).join(';');
39
+ return `ExternalComponent(${element.componentName})[${slotsText}]`;
40
+ }
41
+
42
+ renderPanel(element: Panel, children: string[]): string {
43
+ return `Panel(${element.id})[${children.join(',')}]`;
44
+ }
45
+ }
46
+
47
+ describe('when rendering a mixed tree', () => {
48
+ const badge: ExternalComponent = { ...control('badge'), componentName: 'core:badge', slots: {} };
49
+ const itemTemplate: ContentControl = { ...control('row'), content: badge };
50
+ const items: ItemsControl = { ...control('items'), itemsSource: { path: 'rows' }, itemTemplate };
51
+ const root: Panel = { ...control('root'), children: [items] };
52
+
53
+ const output = renderElement(root, new StringRenderer(), binding => (binding.path === 'rows' ? [{}, {}] : []));
54
+
55
+ it('should render the panel wrapping the items control', () => output.should.equal(
56
+ 'Panel(root)[ItemsControl(items)[ContentControl(row)[ExternalComponent(core:badge)[]],ContentControl(row)[ExternalComponent(core:badge)[]]]]',
57
+ ));
58
+ });
@@ -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 { readFileSync } from 'node:fs';
5
+ import { join } from 'node:path';
6
+ import { UiProfile } from '@cratis/scene.model';
7
+ import { ComponentResolution, PackageCatalog, resolveComponentName } from '../index';
8
+
9
+ interface FixtureCase {
10
+ name: string;
11
+ profile: { packages: string[] };
12
+ catalog: PackageCatalog;
13
+ requestedName: string;
14
+ expected: ComponentResolution | null;
15
+ }
16
+
17
+ interface FixtureCorpus {
18
+ cases: FixtureCase[];
19
+ }
20
+
21
+ const manifestPath = join(import.meta.dirname, '..', '..', '..', '..', 'package-resolution-fixtures.json');
22
+ const corpus = JSON.parse(readFileSync(manifestPath, 'utf-8')) as FixtureCorpus;
23
+
24
+ describe('when resolving against the shared fixture corpus', () => {
25
+ for (const fixtureCase of corpus.cases) {
26
+ it(`should match the expected resolution for "${fixtureCase.name}"`, () => {
27
+ const profile: UiProfile = { name: 'test', targetPlatform: 'web', packages: fixtureCase.profile.packages };
28
+ const actual = resolveComponentName(fixtureCase.requestedName, profile, fixtureCase.catalog);
29
+ if (fixtureCase.expected === null) {
30
+ (actual === undefined).should.be.true;
31
+ } else {
32
+ actual!.should.deep.equal(fixtureCase.expected);
33
+ }
34
+ });
35
+ }
36
+ });
@@ -0,0 +1,49 @@
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 { readFileSync } from 'node:fs';
5
+ import { join } from 'node:path';
6
+ import { Layout, ScreenTemplate } from '@cratis/scene.model';
7
+ import { isScreenTemplateResolutionValid, resolveScreenTemplates } from '../index';
8
+
9
+ interface FixtureCase {
10
+ name: string;
11
+ layout: { name: string; slots: string[] };
12
+ templates: { name: string; fitsSlot: string | null; slots: string[] }[];
13
+ expectedPlacements: { template: string; slot: string; container: string; depth: number }[];
14
+ expectedUnplaced: { template: string; slot: string; candidates: string[] }[];
15
+ expectedCycles: string[][];
16
+ }
17
+
18
+ const fixturePath = join(import.meta.dirname, '..', '..', '..', '..', 'screen-template-fixtures.json');
19
+ const corpus = JSON.parse(readFileSync(fixturePath, 'utf-8')) as { cases: FixtureCase[] };
20
+
21
+ describe('when resolving against the shared fixture corpus', () => {
22
+ for (const fixtureCase of corpus.cases) {
23
+ const layout: Layout = { name: fixtureCase.layout.name, slots: fixtureCase.layout.slots.map((name) => ({ name })) };
24
+ const templates: ScreenTemplate[] = fixtureCase.templates.map((template) => ({
25
+ name: template.name,
26
+ fitsSlot: template.fitsSlot ?? undefined,
27
+ slots: template.slots.map((name) => ({ name })),
28
+ }));
29
+
30
+ const resolution = resolveScreenTemplates(layout, templates);
31
+
32
+ it(`should place every template where the corpus expects for "${fixtureCase.name}"`, () => {
33
+ resolution.placements.should.deep.equal(fixtureCase.expectedPlacements);
34
+ });
35
+
36
+ it(`should report the expected unplaced templates for "${fixtureCase.name}"`, () => {
37
+ resolution.unplaced.should.deep.equal(fixtureCase.expectedUnplaced);
38
+ });
39
+
40
+ it(`should report the expected cycles for "${fixtureCase.name}"`, () => {
41
+ resolution.cycles.should.deep.equal(fixtureCase.expectedCycles);
42
+ });
43
+
44
+ it(`should consider "${fixtureCase.name}" valid only when nothing is wrong`, () => {
45
+ const expectedValid = fixtureCase.expectedUnplaced.length === 0 && fixtureCase.expectedCycles.length === 0;
46
+ isScreenTemplateResolutionValid(resolution).should.equal(expectedValid);
47
+ });
48
+ }
49
+ });
@@ -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 { readFileSync } from 'node:fs';
5
+ import { join } from 'node:path';
6
+ import { Theme, UiProfile } from '@cratis/scene.model';
7
+ import { applicableThemePackages, incompatiblePackages } from '../index';
8
+
9
+ interface FixtureCase {
10
+ name: string;
11
+ theme: { compatibleWith: string[] };
12
+ profile: { packages: string[] };
13
+ expectedIncompatible: string[];
14
+ expectedApplicable: string[];
15
+ }
16
+
17
+ interface FixtureCorpus {
18
+ cases: FixtureCase[];
19
+ }
20
+
21
+ const manifestPath = join(import.meta.dirname, '..', '..', '..', '..', 'theme-compatibility-fixtures.json');
22
+ const corpus = JSON.parse(readFileSync(manifestPath, 'utf-8')) as FixtureCorpus;
23
+
24
+ describe('when checking against the shared fixture corpus', () => {
25
+ for (const fixtureCase of corpus.cases) {
26
+ const theme: Theme = { name: 'test-theme', compatibleWith: fixtureCase.theme.compatibleWith };
27
+ const profile: UiProfile = { name: 'test-profile', targetPlatform: 'web', packages: fixtureCase.profile.packages };
28
+
29
+ it(`should match the expected incompatible packages for "${fixtureCase.name}"`, () => {
30
+ incompatiblePackages(theme, profile).should.deep.equal(fixtureCase.expectedIncompatible);
31
+ });
32
+
33
+ it(`should match the expected applicable packages for "${fixtureCase.name}"`, () => {
34
+ applicableThemePackages(theme, profile).should.deep.equal(fixtureCase.expectedApplicable);
35
+ });
36
+ }
37
+ });
@@ -0,0 +1,24 @@
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 { Theme, UiProfile, UiStarter } from '@cratis/scene.model';
5
+ import { incompatiblePackages } from './themeCompatibility';
6
+
7
+ /**
8
+ * Finds the {@link UiStarter.themes} that are not compatible with the starter's own {@link UiStarter.packages} -
9
+ * part of Cratis/Scene#6. A starter is versioned per package combination it targets, so a theme it ships
10
+ * as a choice that turns out incompatible with the starter's own packages is exactly the gap
11
+ * `themeCompatibility` already surfaces for a {@link UiProfile}; this reuses that rule rather than
12
+ * reimplementing it for starters.
13
+ *
14
+ * @param starter The {@link UiStarter} to check.
15
+ * @param themes Every known {@link Theme}, keyed by name.
16
+ * @returns The names in `starter.themes` that are either unknown to `themes` or incompatible with `starter.packages`.
17
+ */
18
+ export function incompatibleStarterThemes(starter: UiStarter, themes: Record<string, Theme>): string[] {
19
+ const profile: UiProfile = { name: starter.name, targetPlatform: '', packages: starter.packages };
20
+ return starter.themes.filter(name => {
21
+ const theme = themes[name];
22
+ return !theme || incompatiblePackages(theme, profile).length > 0;
23
+ });
24
+ }
package/index.ts ADDED
@@ -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
+ export * from './Renderer';
5
+ export * from './BindingResolver';
6
+ export * from './renderElement';
7
+ export * from './elementKind';
8
+ export * from './flowNodeKind';
9
+ export * from './ComponentResolution';
10
+ export * from './resolveComponentName';
11
+ export * from './computeSizeClass';
12
+ export * from './evaluateFlowArrangement';
13
+ export * from './evaluateFreeformArrangement';
14
+ export * from './evaluateFreeformSlotArrangement';
15
+ export * from './aggregateContributions';
16
+ export * from './themeCompatibility';
17
+ export * from './PackageSelection';
18
+ export * from './packageVersionRange';
19
+ export * from './resolvePackageDependencies';
20
+ export * from './packageCatalog';
21
+ export * from './resolveScreenTemplates';
22
+ export * from './buildStarterProfile';
23
+ export * from './incompatibleStarterThemes';
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "@cratis/scene.engine",
3
+ "version": "0.1.0",
4
+ "description": "The Scene runtime: walks a Scene element tree and drives whichever renderer is plugged in. Does not know React, or any other rendering platform, exists.",
5
+ "author": "Cratis",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/Cratis/Scene.git"
10
+ },
11
+ "publishConfig": {
12
+ "access": "public"
13
+ },
14
+ "files": [
15
+ "dist",
16
+ "**/*.ts"
17
+ ],
18
+ "main": "dist/cjs/index.js",
19
+ "module": "dist/esm/index.js",
20
+ "types": "dist/esm/index.d.ts",
21
+ "exports": {
22
+ "./package.json": "./package.json",
23
+ ".": {
24
+ "types": "./dist/esm/index.d.ts",
25
+ "require": "./dist/cjs/index.js",
26
+ "import": "./dist/esm/index.js"
27
+ }
28
+ },
29
+ "scripts": {
30
+ "prepare": "yarn g:build",
31
+ "clean": "yarn g:clean",
32
+ "build": "yarn g:build",
33
+ "lint": "yarn g:lint",
34
+ "lint:ci": "yarn g:lint:ci",
35
+ "test": "yarn g:test",
36
+ "ci": "yarn g:ci",
37
+ "up": "yarn g:up"
38
+ },
39
+ "dependencies": {
40
+ "@cratis/scene.model": "0.1.0"
41
+ },
42
+ "devDependencies": {
43
+ "@cratis/scene.model": "0.1.0"
44
+ }
45
+ }