@cratis/scene.engine 4.12.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 (133) hide show
  1. package/dist/cjs/index.js +20 -0
  2. package/dist/cjs/index.js.map +1 -1
  3. package/dist/cjs/navigation/NavigationDiagnosticCode.js +19 -0
  4. package/dist/cjs/navigation/NavigationDiagnosticCode.js.map +1 -0
  5. package/dist/cjs/navigation/collectOutlets.js +45 -0
  6. package/dist/cjs/navigation/collectOutlets.js.map +1 -0
  7. package/dist/cjs/navigation/diagnoseNavigation.js +116 -0
  8. package/dist/cjs/navigation/diagnoseNavigation.js.map +1 -0
  9. package/dist/cjs/navigation/findHostingCycles.js +43 -0
  10. package/dist/cjs/navigation/findHostingCycles.js.map +1 -0
  11. package/dist/cjs/navigation/navigationEntriesFrom.js +19 -0
  12. package/dist/cjs/navigation/navigationEntriesFrom.js.map +1 -0
  13. package/dist/cjs/navigation/navigationRoute.js +25 -0
  14. package/dist/cjs/navigation/navigationRoute.js.map +1 -0
  15. package/dist/cjs/packageVersionRange.js +16 -0
  16. package/dist/cjs/packageVersionRange.js.map +1 -1
  17. package/dist/cjs/templates/TemplateCatalogKind.js +15 -0
  18. package/dist/cjs/templates/TemplateCatalogKind.js.map +1 -0
  19. package/dist/cjs/templates/describeTemplateCatalog.js +89 -0
  20. package/dist/cjs/templates/describeTemplateCatalog.js.map +1 -0
  21. package/dist/cjs/templates/templateMetadataRules.js +32 -0
  22. package/dist/cjs/templates/templateMetadataRules.js.map +1 -0
  23. package/dist/cjs/templates/validateTemplateMetadata.js +50 -0
  24. package/dist/cjs/templates/validateTemplateMetadata.js.map +1 -0
  25. package/dist/cjs/validateNavigationDestinations.js +12 -38
  26. package/dist/cjs/validateNavigationDestinations.js.map +1 -1
  27. package/dist/esm/index.d.ts +2 -0
  28. package/dist/esm/index.d.ts.map +1 -1
  29. package/dist/esm/index.js +9 -1
  30. package/dist/esm/index.js.map +1 -1
  31. package/dist/esm/navigation/NavigationDiagnostic.d.ts +7 -0
  32. package/dist/esm/navigation/NavigationDiagnostic.d.ts.map +1 -0
  33. package/dist/esm/navigation/NavigationDiagnostic.js +2 -0
  34. package/dist/esm/navigation/NavigationDiagnostic.js.map +1 -0
  35. package/dist/esm/navigation/NavigationDiagnosticCode.d.ts +9 -0
  36. package/dist/esm/navigation/NavigationDiagnosticCode.d.ts.map +1 -0
  37. package/dist/esm/navigation/NavigationDiagnosticCode.js +17 -0
  38. package/dist/esm/navigation/NavigationDiagnosticCode.js.map +1 -0
  39. package/dist/esm/navigation/NavigationEntry.d.ts +6 -0
  40. package/dist/esm/navigation/NavigationEntry.d.ts.map +1 -0
  41. package/dist/esm/navigation/NavigationEntry.js +2 -0
  42. package/dist/esm/navigation/NavigationEntry.js.map +1 -0
  43. package/dist/esm/navigation/NavigationGraph.d.ts +10 -0
  44. package/dist/esm/navigation/NavigationGraph.d.ts.map +1 -0
  45. package/dist/esm/navigation/NavigationGraph.js +2 -0
  46. package/dist/esm/navigation/NavigationGraph.js.map +1 -0
  47. package/dist/esm/navigation/OutletOwner.d.ts +7 -0
  48. package/dist/esm/navigation/OutletOwner.d.ts.map +1 -0
  49. package/dist/esm/navigation/OutletOwner.js +2 -0
  50. package/dist/esm/navigation/OutletOwner.js.map +1 -0
  51. package/dist/esm/navigation/collectOutlets.d.ts +5 -0
  52. package/dist/esm/navigation/collectOutlets.d.ts.map +1 -0
  53. package/dist/esm/navigation/collectOutlets.js +43 -0
  54. package/dist/esm/navigation/collectOutlets.js.map +1 -0
  55. package/dist/esm/navigation/diagnoseNavigation.d.ts +5 -0
  56. package/dist/esm/navigation/diagnoseNavigation.d.ts.map +1 -0
  57. package/dist/esm/navigation/diagnoseNavigation.js +114 -0
  58. package/dist/esm/navigation/diagnoseNavigation.js.map +1 -0
  59. package/dist/esm/navigation/findHostingCycles.d.ts +2 -0
  60. package/dist/esm/navigation/findHostingCycles.d.ts.map +1 -0
  61. package/dist/esm/navigation/findHostingCycles.js +41 -0
  62. package/dist/esm/navigation/findHostingCycles.js.map +1 -0
  63. package/dist/esm/navigation/index.d.ts +8 -0
  64. package/dist/esm/navigation/index.d.ts.map +1 -0
  65. package/dist/esm/navigation/index.js +8 -0
  66. package/dist/esm/navigation/index.js.map +1 -0
  67. package/dist/esm/navigation/navigationEntriesFrom.d.ts +4 -0
  68. package/dist/esm/navigation/navigationEntriesFrom.d.ts.map +1 -0
  69. package/dist/esm/navigation/navigationEntriesFrom.js +17 -0
  70. package/dist/esm/navigation/navigationEntriesFrom.js.map +1 -0
  71. package/dist/esm/navigation/navigationRoute.d.ts +5 -0
  72. package/dist/esm/navigation/navigationRoute.d.ts.map +1 -0
  73. package/dist/esm/navigation/navigationRoute.js +22 -0
  74. package/dist/esm/navigation/navigationRoute.js.map +1 -0
  75. package/dist/esm/packageVersionRange.d.ts +2 -0
  76. package/dist/esm/packageVersionRange.d.ts.map +1 -1
  77. package/dist/esm/packageVersionRange.js +15 -1
  78. package/dist/esm/packageVersionRange.js.map +1 -1
  79. package/dist/esm/templates/TemplateCatalogEntry.d.ts +20 -0
  80. package/dist/esm/templates/TemplateCatalogEntry.d.ts.map +1 -0
  81. package/dist/esm/templates/TemplateCatalogEntry.js +2 -0
  82. package/dist/esm/templates/TemplateCatalogEntry.js.map +1 -0
  83. package/dist/esm/templates/TemplateCatalogKind.d.ts +6 -0
  84. package/dist/esm/templates/TemplateCatalogKind.d.ts.map +1 -0
  85. package/dist/esm/templates/TemplateCatalogKind.js +13 -0
  86. package/dist/esm/templates/TemplateCatalogKind.js.map +1 -0
  87. package/dist/esm/templates/TemplateSource.d.ts +8 -0
  88. package/dist/esm/templates/TemplateSource.d.ts.map +1 -0
  89. package/dist/esm/templates/TemplateSource.js +2 -0
  90. package/dist/esm/templates/TemplateSource.js.map +1 -0
  91. package/dist/esm/templates/describeTemplateCatalog.d.ts +4 -0
  92. package/dist/esm/templates/describeTemplateCatalog.d.ts.map +1 -0
  93. package/dist/esm/templates/describeTemplateCatalog.js +87 -0
  94. package/dist/esm/templates/describeTemplateCatalog.js.map +1 -0
  95. package/dist/esm/templates/index.d.ts +7 -0
  96. package/dist/esm/templates/index.d.ts.map +1 -0
  97. package/dist/esm/templates/index.js +7 -0
  98. package/dist/esm/templates/index.js.map +1 -0
  99. package/dist/esm/templates/templateMetadataRules.d.ts +3 -0
  100. package/dist/esm/templates/templateMetadataRules.d.ts.map +1 -0
  101. package/dist/esm/templates/templateMetadataRules.js +29 -0
  102. package/dist/esm/templates/templateMetadataRules.js.map +1 -0
  103. package/dist/esm/templates/validateTemplateMetadata.d.ts +3 -0
  104. package/dist/esm/templates/validateTemplateMetadata.d.ts.map +1 -0
  105. package/dist/esm/templates/validateTemplateMetadata.js +48 -0
  106. package/dist/esm/templates/validateTemplateMetadata.js.map +1 -0
  107. package/dist/esm/tsconfig.tsbuildinfo +1 -1
  108. package/dist/esm/validateNavigationDestinations.d.ts +5 -5
  109. package/dist/esm/validateNavigationDestinations.d.ts.map +1 -1
  110. package/dist/esm/validateNavigationDestinations.js +12 -38
  111. package/dist/esm/validateNavigationDestinations.js.map +1 -1
  112. package/index.ts +2 -0
  113. package/navigation/NavigationDiagnostic.ts +18 -0
  114. package/navigation/NavigationDiagnosticCode.ts +26 -0
  115. package/navigation/NavigationEntry.ts +16 -0
  116. package/navigation/NavigationGraph.ts +26 -0
  117. package/navigation/OutletOwner.ts +13 -0
  118. package/navigation/collectOutlets.ts +36 -0
  119. package/navigation/diagnoseNavigation.ts +136 -0
  120. package/navigation/findHostingCycles.ts +41 -0
  121. package/navigation/index.ts +10 -0
  122. package/navigation/navigationEntriesFrom.ts +16 -0
  123. package/navigation/navigationRoute.ts +25 -0
  124. package/package.json +3 -3
  125. package/packageVersionRange.ts +18 -0
  126. package/templates/TemplateCatalogEntry.ts +35 -0
  127. package/templates/TemplateCatalogKind.ts +11 -0
  128. package/templates/TemplateSource.ts +15 -0
  129. package/templates/describeTemplateCatalog.ts +92 -0
  130. package/templates/index.ts +9 -0
  131. package/templates/templateMetadataRules.ts +25 -0
  132. package/templates/validateTemplateMetadata.ts +55 -0
  133. package/validateNavigationDestinations.ts +12 -32
@@ -1,10 +1,10 @@
1
- import { DestinationReference, Layout, ScreenTemplate } from '@cratis/scene.model';
2
- export interface NavigationDestinationDiagnostic {
3
- code: string;
4
- message: string;
5
- }
1
+ import { DestinationReference, DialogTemplate, Layout, Screen, ScreenTemplate } from '@cratis/scene.model';
2
+ import { NavigationDiagnostic } from './navigation';
3
+ export type NavigationDestinationDiagnostic = NavigationDiagnostic;
6
4
  export declare function validateNavigationDestinations(destinations: DestinationReference[], surfaces?: {
7
5
  layouts?: Layout[];
8
6
  screenTemplates?: ScreenTemplate[];
7
+ screens?: Screen[];
8
+ dialogTemplates?: DialogTemplate[];
9
9
  }): NavigationDestinationDiagnostic[];
10
10
  //# sourceMappingURL=validateNavigationDestinations.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"validateNavigationDestinations.d.ts","sourceRoot":"","sources":["../../validateNavigationDestinations.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,oBAAoB,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAEnF,MAAM,WAAW,+BAA+B;IAC5C,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACnB;AAKD,wBAAgB,8BAA8B,CAC1C,YAAY,EAAE,oBAAoB,EAAE,EACpC,QAAQ,GAAE;IAAE,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IAAC,eAAe,CAAC,EAAE,cAAc,EAAE,CAAA;CAAO,GAC1E,+BAA+B,EAAE,CA0BnC"}
1
+ {"version":3,"file":"validateNavigationDestinations.d.ts","sourceRoot":"","sources":["../../validateNavigationDestinations.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,oBAAoB,EAAE,cAAc,EAAE,MAAM,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAC3G,OAAO,EAAE,oBAAoB,EAAsB,MAAM,cAAc,CAAC;AAKxE,MAAM,MAAM,+BAA+B,GAAG,oBAAoB,CAAC;AAQnE,wBAAgB,8BAA8B,CAC1C,YAAY,EAAE,oBAAoB,EAAE,EACpC,QAAQ,GAAE;IAAE,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IAAC,eAAe,CAAC,EAAE,cAAc,EAAE,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IAAC,eAAe,CAAC,EAAE,cAAc,EAAE,CAAA;CAAO,GAClI,+BAA+B,EAAE,CAEnC"}
@@ -1,45 +1,19 @@
1
+ import { diagnoseNavigation } from './navigation/diagnoseNavigation.js';
2
+
1
3
  // Copyright (c) Cratis. All rights reserved.
2
4
  // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
5
  /**
4
- * Validates destinations against the outlet/route declarations owned by layouts and templates.
6
+ * Validates destinations against the outlet/route declarations owned by layouts and templates, and - when
7
+ * the screens and dialog templates are given - against the targets that actually exist.
8
+ *
9
+ * A shorthand for {@link diagnoseNavigation} over destinations without entry identities.
5
10
  */ function validateNavigationDestinations(destinations, surfaces = {}) {
6
- const diagnostics = [];
7
- const outlets = new Set();
8
- const routes = new Set();
9
- for (const surface of [
10
- ...surfaces.layouts ?? [],
11
- ...surfaces.screenTemplates ?? []
12
- ]){
13
- for (const outlet of surface.outlets ?? []){
14
- if (outlets.has(outlet.name)) diagnostics.push({
15
- code: 'duplicateOutlet',
16
- message: `Outlet '${outlet.name}' is declared more than once.`
17
- });
18
- outlets.add(outlet.name);
19
- }
20
- }
21
- for (const destination of destinations){
22
- if (destination.outlet && !outlets.has(destination.outlet)) {
23
- diagnostics.push({
24
- code: 'missingOutlet',
25
- message: `Destination targets missing outlet '${destination.outlet}'.`
26
- });
27
- }
28
- if (destination.route) {
29
- if (routes.has(destination.route)) diagnostics.push({
30
- code: 'duplicateRoute',
31
- message: `Route '${destination.route}' is used by more than one destination.`
32
- });
33
- routes.add(destination.route);
34
- }
35
- if (!destination.screen && !destination.module && !destination.route && !destination.dialog) {
36
- diagnostics.push({
37
- code: 'unavailableTarget',
38
- message: 'Destination must name a screen, stable identity, route or dialog.'
39
- });
40
- }
41
- }
42
- return diagnostics;
11
+ return diagnoseNavigation({
12
+ entries: destinations.map((destination)=>({
13
+ destination
14
+ })),
15
+ ...surfaces
16
+ });
43
17
  }
44
18
 
45
19
  export { validateNavigationDestinations };
@@ -1 +1 @@
1
- {"version":3,"file":"validateNavigationDestinations.js","sources":["../../validateNavigationDestinations.ts"],"sourcesContent":["// Copyright (c) Cratis. All rights reserved.\n// Licensed under the MIT license. See LICENSE file in the project root for full license information.\n\nimport { DestinationReference, Layout, ScreenTemplate } from '@cratis/scene.model';\n\nexport interface NavigationDestinationDiagnostic {\n code: string;\n message: string;\n}\n\n/**\n * Validates destinations against the outlet/route declarations owned by layouts and templates.\n */\nexport function validateNavigationDestinations(\n destinations: DestinationReference[],\n surfaces: { layouts?: Layout[]; screenTemplates?: ScreenTemplate[] } = {},\n): NavigationDestinationDiagnostic[] {\n const diagnostics: NavigationDestinationDiagnostic[] = [];\n const outlets = new Set<string>();\n const routes = new Set<string>();\n\n for (const surface of [...(surfaces.layouts ?? []), ...(surfaces.screenTemplates ?? [])]) {\n for (const outlet of surface.outlets ?? []) {\n if (outlets.has(outlet.name)) diagnostics.push({ code: 'duplicateOutlet', message: `Outlet '${outlet.name}' is declared more than once.` });\n outlets.add(outlet.name);\n }\n }\n\n for (const destination of destinations) {\n if (destination.outlet && !outlets.has(destination.outlet)) {\n diagnostics.push({ code: 'missingOutlet', message: `Destination targets missing outlet '${destination.outlet}'.` });\n }\n if (destination.route) {\n if (routes.has(destination.route)) diagnostics.push({ code: 'duplicateRoute', message: `Route '${destination.route}' is used by more than one destination.` });\n routes.add(destination.route);\n }\n if (!destination.screen && !destination.module && !destination.route && !destination.dialog) {\n diagnostics.push({ code: 'unavailableTarget', message: 'Destination must name a screen, stable identity, route or dialog.' });\n }\n }\n\n return diagnostics;\n}\n"],"names":["validateNavigationDestinations","destinations","surfaces","diagnostics","outlets","Set","routes","surface","layouts","screenTemplates","outlet","has","name","push","code","message","add","destination","route","screen","module","dialog"],"mappings":"AAAA;AACA;AASA;;AAEC,IACM,SAASA,8BAAAA,CACZC,YAAoC,EACpCC,QAAAA,GAAuE,EAAE,EAAA;AAEzE,IAAA,MAAMC,cAAiD,EAAE;AACzD,IAAA,MAAMC,UAAU,IAAIC,GAAAA,EAAAA;AACpB,IAAA,MAAMC,SAAS,IAAID,GAAAA,EAAAA;AAEnB,IAAA,KAAK,MAAME,OAAAA,IAAW;WAAKL,QAAAA,CAASM,OAAO,IAAI,EAAE;WAAON,QAAAA,CAASO,eAAe,IAAI;KAAI,CAAE;AACtF,QAAA,KAAK,MAAMC,MAAAA,IAAUH,OAAAA,CAAQH,OAAO,IAAI,EAAE,CAAE;YACxC,IAAIA,OAAAA,CAAQO,GAAG,CAACD,MAAAA,CAAOE,IAAI,CAAA,EAAGT,WAAAA,CAAYU,IAAI,CAAC;gBAAEC,IAAAA,EAAM,iBAAA;AAAmBC,gBAAAA,OAAAA,EAAS,CAAC,QAAQ,EAAEL,OAAOE,IAAI,CAAC,6BAA6B;AAAE,aAAA,CAAA;YACzIR,OAAAA,CAAQY,GAAG,CAACN,MAAAA,CAAOE,IAAI,CAAA;AAC3B,QAAA;AACJ,IAAA;IAEA,KAAK,MAAMK,eAAehB,YAAAA,CAAc;QACpC,IAAIgB,WAAAA,CAAYP,MAAM,IAAI,CAACN,QAAQO,GAAG,CAACM,WAAAA,CAAYP,MAAM,CAAA,EAAG;AACxDP,YAAAA,WAAAA,CAAYU,IAAI,CAAC;gBAAEC,IAAAA,EAAM,eAAA;AAAiBC,gBAAAA,OAAAA,EAAS,CAAC,oCAAoC,EAAEE,YAAYP,MAAM,CAAC,EAAE;AAAE,aAAA,CAAA;AACrH,QAAA;QACA,IAAIO,WAAAA,CAAYC,KAAK,EAAE;YACnB,IAAIZ,MAAAA,CAAOK,GAAG,CAACM,WAAAA,CAAYC,KAAK,CAAA,EAAGf,WAAAA,CAAYU,IAAI,CAAC;gBAAEC,IAAAA,EAAM,gBAAA;AAAkBC,gBAAAA,OAAAA,EAAS,CAAC,OAAO,EAAEE,YAAYC,KAAK,CAAC,uCAAuC;AAAE,aAAA,CAAA;YAC5JZ,MAAAA,CAAOU,GAAG,CAACC,WAAAA,CAAYC,KAAK,CAAA;AAChC,QAAA;AACA,QAAA,IAAI,CAACD,WAAAA,CAAYE,MAAM,IAAI,CAACF,WAAAA,CAAYG,MAAM,IAAI,CAACH,YAAYC,KAAK,IAAI,CAACD,WAAAA,CAAYI,MAAM,EAAE;AACzFlB,YAAAA,WAAAA,CAAYU,IAAI,CAAC;gBAAEC,IAAAA,EAAM,mBAAA;gBAAqBC,OAAAA,EAAS;AAAoE,aAAA,CAAA;AAC/H,QAAA;AACJ,IAAA;IAEA,OAAOZ,WAAAA;AACX;;"}
1
+ {"version":3,"file":"validateNavigationDestinations.js","sources":["../../validateNavigationDestinations.ts"],"sourcesContent":["// Copyright (c) Cratis. All rights reserved.\n// Licensed under the MIT license. See LICENSE file in the project root for full license information.\n\nimport { DestinationReference, DialogTemplate, Layout, Screen, ScreenTemplate } from '@cratis/scene.model';\nimport { NavigationDiagnostic, diagnoseNavigation } from './navigation';\n\n/**\n * A navigation problem as {@link validateNavigationDestinations} reports it.\n */\nexport type NavigationDestinationDiagnostic = NavigationDiagnostic;\n\n/**\n * Validates destinations against the outlet/route declarations owned by layouts and templates, and - when\n * the screens and dialog templates are given - against the targets that actually exist.\n *\n * A shorthand for {@link diagnoseNavigation} over destinations without entry identities.\n */\nexport function validateNavigationDestinations(\n destinations: DestinationReference[],\n surfaces: { layouts?: Layout[]; screenTemplates?: ScreenTemplate[]; screens?: Screen[]; dialogTemplates?: DialogTemplate[] } = {},\n): NavigationDestinationDiagnostic[] {\n return diagnoseNavigation({ entries: destinations.map(destination => ({ destination })), ...surfaces });\n}\n"],"names":["validateNavigationDestinations","destinations","surfaces","diagnoseNavigation","entries","map","destination"],"mappings":";;AAAA;AACA;AAUA;;;;;AAKC,IACM,SAASA,8BAAAA,CACZC,YAAoC,EACpCC,QAAAA,GAA+H,EAAE,EAAA;AAEjI,IAAA,OAAOC,kBAAAA,CAAmB;AAAEC,QAAAA,OAAAA,EAASH,YAAAA,CAAaI,GAAG,CAACC,CAAAA,eAAgB;AAAEA,gBAAAA;aAAY,CAAA,CAAA;AAAK,QAAA,GAAGJ;AAAS,KAAA,CAAA;AACzG;;"}
package/index.ts CHANGED
@@ -27,11 +27,13 @@ export * from './resolvePackageDependencies';
27
27
  export * from './packageCatalog';
28
28
  export * from './resolveScreenTemplates';
29
29
  export * from './templateApplicability';
30
+ export * from './templates';
30
31
  export * from './buildStarterProfile';
31
32
  export * from './incompatibleStarterThemes';
32
33
  export * from './ActionDispatcher';
33
34
  export * from './runActions';
34
35
  export * from './resolveBehaviors';
35
36
  export * from './resolveStrings';
37
+ export * from './navigation';
36
38
  export * from './validateNavigationDestinations';
37
39
  export * from './editing';
@@ -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 { NavigationDiagnosticCode } from './NavigationDiagnosticCode';
5
+
6
+ /**
7
+ * One problem with how destinations, outlets and routes fit together.
8
+ */
9
+ export interface NavigationDiagnostic {
10
+ /** What kind of problem this is. */
11
+ code: NavigationDiagnosticCode;
12
+
13
+ /** A sentence naming the destinations, outlets or screens involved. */
14
+ message: string;
15
+
16
+ /** The entry the problem was found on - its `id`, or `#<position>` when it has none. Absent for model-wide problems. */
17
+ entry?: string;
18
+ }
@@ -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
+ /**
5
+ * What is wrong with a navigation destination. The values are shared with the C# engine so both report
6
+ * the same code for the same model.
7
+ */
8
+ export enum NavigationDiagnosticCode {
9
+ /** Two destinations that go to different targets share one URL. */
10
+ DuplicateRoute = 'duplicateRoute',
11
+
12
+ /** Two surfaces declare an outlet with the same name, so a destination cannot tell them apart. */
13
+ DuplicateOutlet = 'duplicateOutlet',
14
+
15
+ /** A destination names an outlet that no layout or screen template declares. */
16
+ MissingOutlet = 'missingOutlet',
17
+
18
+ /** A destination names an outlet that cannot host what it opens. */
19
+ IncompatibleOutlet = 'incompatibleOutlet',
20
+
21
+ /** Screens are placed in outlets owned by each other, so composition never terminates. */
22
+ NavigationCycle = 'navigationCycle',
23
+
24
+ /** A destination opens a screen or dialog that does not exist, or names nothing to open. */
25
+ UnavailableTarget = 'unavailableTarget',
26
+ }
@@ -0,0 +1,16 @@
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
+ * One place in the model a user can navigate from - a navigation item, toolbar action or link - reduced to
8
+ * its stable identity and destination.
9
+ */
10
+ export interface NavigationEntry {
11
+ /** Stable identity used to point at the entry in diagnostics. */
12
+ id?: string;
13
+
14
+ /** Where the entry takes the user. */
15
+ destination: DestinationReference;
16
+ }
@@ -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 { DialogTemplate, Layout, Screen, ScreenTemplate } from '@cratis/scene.model';
5
+ import { NavigationEntry } from './NavigationEntry';
6
+
7
+ /**
8
+ * Everything navigation diagnostics look at. The catalogs are optional: an absent catalog means "not known
9
+ * here", so targets in it are not reported as unavailable. An empty catalog means "known to be empty".
10
+ */
11
+ export interface NavigationGraph {
12
+ /** The destinations to check, in authored order. */
13
+ entries: NavigationEntry[];
14
+
15
+ /** The screens destinations may open. */
16
+ screens?: Screen[];
17
+
18
+ /** The layouts that may own outlets. */
19
+ layouts?: Layout[];
20
+
21
+ /** The screen templates that may own outlets, and that give screens their semantic type. */
22
+ screenTemplates?: ScreenTemplate[];
23
+
24
+ /** The dialog templates dialog destinations may open. */
25
+ dialogTemplates?: DialogTemplate[];
26
+ }
@@ -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 { Outlet } from '@cratis/scene.model';
5
+
6
+ /**
7
+ * An outlet together with the layout or screen template that declares it.
8
+ */
9
+ export interface OutletOwner {
10
+ outlet: Outlet;
11
+ owner: string;
12
+ ownerKind: 'Layout' | 'ScreenTemplate';
13
+ }
@@ -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 { NavigationDiagnostic } from './NavigationDiagnostic';
5
+ import { NavigationDiagnosticCode } from './NavigationDiagnosticCode';
6
+ import { NavigationGraph } from './NavigationGraph';
7
+ import { OutletOwner } from './OutletOwner';
8
+
9
+ /**
10
+ * Indexes every declared outlet by name, keeping the first declaration and reporting each later one as a
11
+ * duplicate - a destination names an outlet only by name, so two of them are ambiguous.
12
+ */
13
+ export function collectOutlets(graph: NavigationGraph, diagnostics: NavigationDiagnostic[]): Map<string, OutletOwner> {
14
+ const owners = new Map<string, OutletOwner>();
15
+ const surfaces = [
16
+ ...(graph.layouts ?? []).map(layout => ({ name: layout.name, kind: 'Layout' as const, outlets: layout.outlets ?? [] })),
17
+ ...(graph.screenTemplates ?? []).map(template => ({ name: template.name, kind: 'ScreenTemplate' as const, outlets: template.outlets ?? [] })),
18
+ ];
19
+
20
+ for (const surface of surfaces) {
21
+ for (const outlet of surface.outlets) {
22
+ const existing = owners.get(outlet.name);
23
+ if (existing) {
24
+ diagnostics.push({
25
+ code: NavigationDiagnosticCode.DuplicateOutlet,
26
+ message: `Outlet '${outlet.name}' is declared by both '${existing.owner}' and '${surface.name}'; destinations cannot tell them apart.`,
27
+ });
28
+ continue;
29
+ }
30
+
31
+ owners.set(outlet.name, { outlet, owner: surface.name, ownerKind: surface.kind });
32
+ }
33
+ }
34
+
35
+ return owners;
36
+ }
@@ -0,0 +1,136 @@
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 { DestinationKind, DestinationReference, Screen } from '@cratis/scene.model';
5
+ import { collectOutlets } from './collectOutlets';
6
+ import { findHostingCycles } from './findHostingCycles';
7
+ import { NavigationDiagnostic } from './NavigationDiagnostic';
8
+ import { NavigationDiagnosticCode } from './NavigationDiagnosticCode';
9
+ import { NavigationGraph } from './NavigationGraph';
10
+ import { NavigationRouteKey, authoredRouteKey, navigationTarget } from './navigationRoute';
11
+ import { OutletOwner } from './OutletOwner';
12
+
13
+ /**
14
+ * Diagnoses how destinations, outlets, routes and targets fit together: duplicate routes, duplicate,
15
+ * missing or incompatible outlets, hosting cycles and unavailable targets.
16
+ *
17
+ * Routes are compared by `routeKey` - by default the authored route override, since the engine constructs no
18
+ * URLs. Nothing is rewritten. The C# engine (`NavigationDiagnostics.Diagnose`) reports the same codes and
19
+ * messages in the same order for the same model; both are asserted against `navigation-diagnostic-fixtures.json`.
20
+ */
21
+ export function diagnoseNavigation(graph: NavigationGraph, routeKey: NavigationRouteKey = authoredRouteKey): NavigationDiagnostic[] {
22
+ const diagnostics: NavigationDiagnostic[] = [];
23
+ const outlets = collectOutlets(graph, diagnostics);
24
+ const screens = graph.screens ? new Map(graph.screens.map(screen => [screen.name, screen])) : undefined;
25
+ const routes = new Map<string, { entry: string; target: string }>();
26
+ const hostedIn = new Map<string, string[]>();
27
+
28
+ graph.entries.forEach((navigationEntry, index) => {
29
+ const entry = navigationEntry.id ?? `#${index + 1}`;
30
+ const destination = navigationEntry.destination;
31
+ const kind = destination.kind ?? (destination.dialog ? DestinationKind.Dialog : DestinationKind.Outlet);
32
+ const report = (code: NavigationDiagnosticCode, message: string) => diagnostics.push({ code, message, entry });
33
+
34
+ if (!destination.screen && !destination.module && !destination.route && !destination.dialog) {
35
+ report(NavigationDiagnosticCode.UnavailableTarget, `Destination '${entry}' names no screen, stable identity, route or dialog.`);
36
+ return;
37
+ }
38
+
39
+ diagnoseTarget(graph, screens, destination, kind, entry, report);
40
+ const owner = diagnoseOutlet(graph, outlets, screens, destination, kind, entry, report);
41
+ if (owner?.ownerKind === 'ScreenTemplate' && destination.screen && screens?.has(destination.screen)) {
42
+ const hosts = graph.screens!.filter(screen => screen.screenTemplate === owner.owner).map(screen => screen.name);
43
+ hostedIn.set(destination.screen, [...new Set([...(hostedIn.get(destination.screen) ?? []), ...hosts])]);
44
+ }
45
+
46
+ const route = kind === DestinationKind.External ? undefined : routeKey(destination);
47
+ if (route) {
48
+ const target = navigationTarget(destination, kind);
49
+ const existing = routes.get(route);
50
+ if (!existing) {
51
+ routes.set(route, { entry, target });
52
+ } else if (existing.target !== target) {
53
+ report(NavigationDiagnosticCode.DuplicateRoute, `Route '${destination.route ?? route}' is used by destinations '${existing.entry}' and '${entry}', which open different targets.`);
54
+ }
55
+ }
56
+ });
57
+
58
+ for (const cycle of findHostingCycles(graph.screens?.map(screen => screen.name) ?? [], hostedIn)) {
59
+ diagnostics.push({
60
+ code: NavigationDiagnosticCode.NavigationCycle,
61
+ message: `Navigation cycle ${[...cycle, cycle[0]].join(' -> ')}: each screen is placed in an outlet owned by the next, so composition never terminates.`,
62
+ });
63
+ }
64
+
65
+ return diagnostics;
66
+ }
67
+
68
+ type Report = (code: NavigationDiagnosticCode, message: string) => void;
69
+
70
+ function diagnoseTarget(
71
+ graph: NavigationGraph,
72
+ screens: Map<string, Screen> | undefined,
73
+ destination: DestinationReference,
74
+ kind: DestinationKind,
75
+ entry: string,
76
+ report: Report,
77
+ ) {
78
+ if (kind === DestinationKind.Dialog) {
79
+ if (!destination.dialog) {
80
+ report(NavigationDiagnosticCode.UnavailableTarget, `Dialog destination '${entry}' names no dialog.`);
81
+ } else if (graph.dialogTemplates && !graph.dialogTemplates.some(template => template.name === destination.dialog)) {
82
+ report(NavigationDiagnosticCode.UnavailableTarget, `Destination '${entry}' opens dialog '${destination.dialog}', which no dialog template provides.`);
83
+ }
84
+ return;
85
+ }
86
+
87
+ if (kind === DestinationKind.Outlet && destination.screen && screens && !screens.has(destination.screen)) {
88
+ report(NavigationDiagnosticCode.UnavailableTarget, `Destination '${entry}' opens screen '${destination.screen}', which does not exist.`);
89
+ }
90
+ }
91
+
92
+ function diagnoseOutlet(
93
+ graph: NavigationGraph,
94
+ outlets: Map<string, OutletOwner>,
95
+ screens: Map<string, Screen> | undefined,
96
+ destination: DestinationReference,
97
+ kind: DestinationKind,
98
+ entry: string,
99
+ report: Report,
100
+ ): OutletOwner | undefined {
101
+ if (!destination.outlet) return undefined;
102
+
103
+ if (kind !== DestinationKind.Outlet) {
104
+ const opens = kind === DestinationKind.Dialog ? 'a dialog' : 'an external target';
105
+ report(NavigationDiagnosticCode.IncompatibleOutlet, `Destination '${entry}' opens ${opens} but names outlet '${destination.outlet}'; only outlet destinations render in outlets.`);
106
+ return undefined;
107
+ }
108
+
109
+ const owner = outlets.get(destination.outlet);
110
+ if (!owner) {
111
+ report(NavigationDiagnosticCode.MissingOutlet, `Destination '${entry}' targets missing outlet '${destination.outlet}'.`);
112
+ return undefined;
113
+ }
114
+
115
+ const screen = destination.screen ? screens?.get(destination.screen) : undefined;
116
+ if (!screen) return owner;
117
+
118
+ if (owner.ownerKind === 'Layout' && screen.layout !== owner.owner) {
119
+ report(
120
+ NavigationDiagnosticCode.IncompatibleOutlet,
121
+ `Outlet '${owner.outlet.name}' belongs to layout '${owner.owner}', but destination '${entry}' places '${screen.name}', which renders in layout '${screen.layout}'.`,
122
+ );
123
+ }
124
+
125
+ if (owner.outlet.accepts) {
126
+ const type = graph.screenTemplates?.find(template => template.name === screen.screenTemplate)?.metadata?.type;
127
+ if (!type || !owner.outlet.accepts.includes(type)) {
128
+ report(
129
+ NavigationDiagnosticCode.IncompatibleOutlet,
130
+ `Outlet '${owner.outlet.name}' accepts ${owner.outlet.accepts.join(', ')} screens, but destination '${entry}' places '${screen.name}' of type '${type ?? 'untyped'}'.`,
131
+ );
132
+ }
133
+ }
134
+
135
+ return owner;
136
+ }
@@ -0,0 +1,41 @@
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
+ * Finds cycles in a "renders inside" graph, where an edge from a screen to another means the first is
6
+ * placed in an outlet the second one's template owns. Screens are visited in the given order and edges in
7
+ * insertion order, so the same graph always yields the same cycles in the same order. Each set of screens
8
+ * is reported once, starting from the screen the walk reached it through.
9
+ */
10
+ export function findHostingCycles(screens: string[], edges: Map<string, string[]>): string[][] {
11
+ const state = new Map<string, 'visiting' | 'done'>();
12
+ const stack: string[] = [];
13
+ const reported = new Set<string>();
14
+ const cycles: string[][] = [];
15
+
16
+ const visit = (screen: string) => {
17
+ state.set(screen, 'visiting');
18
+ stack.push(screen);
19
+ for (const host of edges.get(screen) ?? []) {
20
+ const hostState = state.get(host);
21
+ if (hostState === 'visiting') {
22
+ const cycle = stack.slice(stack.indexOf(host));
23
+ const key = [...cycle].sort().join('\u0000');
24
+ if (!reported.has(key)) {
25
+ reported.add(key);
26
+ cycles.push(cycle);
27
+ }
28
+ } else if (hostState === undefined) {
29
+ visit(host);
30
+ }
31
+ }
32
+ stack.pop();
33
+ state.set(screen, 'done');
34
+ };
35
+
36
+ for (const screen of screens) {
37
+ if (!state.has(screen)) visit(screen);
38
+ }
39
+
40
+ return cycles;
41
+ }
@@ -0,0 +1,10 @@
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 './NavigationDiagnostic';
5
+ export * from './NavigationDiagnosticCode';
6
+ export * from './NavigationEntry';
7
+ export * from './NavigationGraph';
8
+ export * from './diagnoseNavigation';
9
+ export * from './navigationEntriesFrom';
10
+ export * from './navigationRoute';
@@ -0,0 +1,16 @@
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 { NavigationItem } from '@cratis/scene.model';
5
+ import { NavigationEntry } from './NavigationEntry';
6
+
7
+ /**
8
+ * Turns navigation contributions into entries for {@link diagnoseNavigation}. A legacy item carrying only
9
+ * `targetScreen` becomes a destination for that screen, so old and new content are checked the same way.
10
+ */
11
+ export function navigationEntriesFrom(items: NavigationItem[]): NavigationEntry[] {
12
+ return items.map(item => ({
13
+ id: item.id,
14
+ destination: item.destination ?? { screen: item.targetScreen, routeParameterBindings: item.routeParameterBindings },
15
+ }));
16
+ }
@@ -0,0 +1,25 @@
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
+ * Decides which route a destination occupies, so destinations occupying the same one can be compared.
8
+ *
9
+ * The engine constructs no URLs (ADR-004), so it only compares what was authored. A renderer that derives
10
+ * routes from stable identities, or treats parameter spellings as equivalent, supplies its own key - the same
11
+ * derivation it uses when it builds the URL - and gets duplicate routes diagnosed by that rule instead.
12
+ */
13
+ export type NavigationRouteKey = (destination: DestinationReference) => string | undefined;
14
+
15
+ /** The default route key: the authored route override, trimmed, or none. */
16
+ export const authoredRouteKey: NavigationRouteKey = destination => destination.route?.trim() || undefined;
17
+
18
+ /**
19
+ * What a destination opens, so two destinations sharing a route are only a conflict when they open
20
+ * different things.
21
+ */
22
+ export function navigationTarget(destination: DestinationReference, kind: string): string {
23
+ const identity = destination.screen ?? [destination.module, destination.feature, destination.slice].filter(part => part).join('.');
24
+ return [kind, identity, destination.dialog ?? '', destination.outlet ?? ''].join('|');
25
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cratis/scene.engine",
3
- "version": "4.12.0",
3
+ "version": "4.13.0",
4
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
5
  "keywords": [
6
6
  "cratis",
@@ -48,9 +48,9 @@
48
48
  "up": "yarn g:up"
49
49
  },
50
50
  "dependencies": {
51
- "@cratis/scene.model": "4.12.0"
51
+ "@cratis/scene.model": "4.13.0"
52
52
  },
53
53
  "devDependencies": {
54
- "@cratis/scene.model": "4.12.0"
54
+ "@cratis/scene.model": "4.13.0"
55
55
  }
56
56
  }
@@ -42,6 +42,24 @@ export function isVersionSatisfiedBy(version: string, range: string | undefined)
42
42
  }
43
43
  }
44
44
 
45
+ /**
46
+ * Whether a range is one {@link isVersionSatisfiedBy} understands. An empty range or `*` is valid and means
47
+ * "any version"; anything the parser cannot read is invalid, so declarations can be rejected up front
48
+ * instead of surfacing later as a conflict nobody can satisfy.
49
+ */
50
+ export function isValidVersionRange(range: string | undefined): boolean {
51
+ const trimmed = range?.trim();
52
+ if (!trimmed || trimmed === '*') return true;
53
+ return parseVersion(splitRange(trimmed)[1]) !== undefined;
54
+ }
55
+
56
+ /**
57
+ * Whether a value is a `major.minor.patch` version, optionally with a pre-release or build suffix.
58
+ */
59
+ export function isValidVersion(version: string | undefined): boolean {
60
+ return version !== undefined && version.trim().length > 0 && parseVersion(version) !== undefined;
61
+ }
62
+
45
63
  type Version = [number, number, number];
46
64
 
47
65
  function splitRange(range: string): [string, string] {
@@ -0,0 +1,35 @@
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 { TemplateAttribution, TemplateCompatibility, TemplateScope } from '@cratis/scene.model';
5
+ import { TemplateCatalogKind } from './TemplateCatalogKind';
6
+
7
+ /**
8
+ * One template as a template browser reads it: what it is, where it comes from, what it needs and whether the
9
+ * active package set can use it.
10
+ */
11
+ export interface TemplateCatalogEntry {
12
+ /** The package that provides the template. */
13
+ package: string;
14
+
15
+ /** The version of that package. */
16
+ packageVersion: string;
17
+
18
+ kind: TemplateCatalogKind;
19
+ name: string;
20
+ displayName?: string;
21
+ description?: string;
22
+ type?: string;
23
+ category?: string;
24
+ scopes?: TemplateScope[];
25
+ compatibility?: TemplateCompatibility;
26
+ attribution?: TemplateAttribution;
27
+ license?: string;
28
+ licenseUrl?: string;
29
+
30
+ /** Whether the template's metadata is complete and every requirement is met. */
31
+ compatible: boolean;
32
+
33
+ /** Why it is not - missing or invalid metadata, or an unmet Scene or package requirement. */
34
+ problems: string[];
35
+ }
@@ -0,0 +1,11 @@
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
+ * Which kind of template a catalog entry describes.
6
+ */
7
+ export enum TemplateCatalogKind {
8
+ Layout = 'Layout',
9
+ ScreenTemplate = 'ScreenTemplate',
10
+ DialogTemplate = 'DialogTemplate',
11
+ }
@@ -0,0 +1,15 @@
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 { DialogTemplate, Layout, ScenePackage, ScreenTemplate } from '@cratis/scene.model';
5
+
6
+ /**
7
+ * A package and the templates it provides. A React `ScenePackageBundle` already has this shape, so hosts pass
8
+ * their approved bundles straight in.
9
+ */
10
+ export interface TemplateSource {
11
+ manifest: ScenePackage;
12
+ layouts?: Layout[];
13
+ screenTemplates?: ScreenTemplate[];
14
+ dialogTemplates?: DialogTemplate[];
15
+ }