@cratis/scene.engine 4.38.0 → 4.39.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 (59) hide show
  1. package/blueprints/BlueprintCatalogEntry.ts +58 -0
  2. package/blueprints/BlueprintSource.ts +24 -0
  3. package/blueprints/blueprintRules.ts +23 -0
  4. package/blueprints/describeBlueprintCatalog.ts +95 -0
  5. package/blueprints/evaluateBlueprintPin.ts +71 -0
  6. package/blueprints/index.ts +9 -0
  7. package/blueprints/verifyBlueprintPackage.ts +192 -0
  8. package/dist/cjs/blueprints/blueprintRules.js +32 -0
  9. package/dist/cjs/blueprints/blueprintRules.js.map +1 -0
  10. package/dist/cjs/blueprints/describeBlueprintCatalog.js +134 -0
  11. package/dist/cjs/blueprints/describeBlueprintCatalog.js.map +1 -0
  12. package/dist/cjs/blueprints/evaluateBlueprintPin.js +75 -0
  13. package/dist/cjs/blueprints/evaluateBlueprintPin.js.map +1 -0
  14. package/dist/cjs/blueprints/verifyBlueprintPackage.js +194 -0
  15. package/dist/cjs/blueprints/verifyBlueprintPackage.js.map +1 -0
  16. package/dist/cjs/index.js +17 -0
  17. package/dist/cjs/index.js.map +1 -1
  18. package/dist/cjs/packageVersionRange.js +11 -0
  19. package/dist/cjs/packageVersionRange.js.map +1 -1
  20. package/dist/esm/blueprints/BlueprintCatalogEntry.d.ts +46 -0
  21. package/dist/esm/blueprints/BlueprintCatalogEntry.d.ts.map +1 -0
  22. package/dist/esm/blueprints/BlueprintCatalogEntry.js +2 -0
  23. package/dist/esm/blueprints/BlueprintCatalogEntry.js.map +1 -0
  24. package/dist/esm/blueprints/BlueprintSource.d.ts +8 -0
  25. package/dist/esm/blueprints/BlueprintSource.d.ts.map +1 -0
  26. package/dist/esm/blueprints/BlueprintSource.js +2 -0
  27. package/dist/esm/blueprints/BlueprintSource.js.map +1 -0
  28. package/dist/esm/blueprints/blueprintRules.d.ts +7 -0
  29. package/dist/esm/blueprints/blueprintRules.d.ts.map +1 -0
  30. package/dist/esm/blueprints/blueprintRules.js +25 -0
  31. package/dist/esm/blueprints/blueprintRules.js.map +1 -0
  32. package/dist/esm/blueprints/describeBlueprintCatalog.d.ts +5 -0
  33. package/dist/esm/blueprints/describeBlueprintCatalog.d.ts.map +1 -0
  34. package/dist/esm/blueprints/describeBlueprintCatalog.js +131 -0
  35. package/dist/esm/blueprints/describeBlueprintCatalog.js.map +1 -0
  36. package/dist/esm/blueprints/evaluateBlueprintPin.d.ts +15 -0
  37. package/dist/esm/blueprints/evaluateBlueprintPin.d.ts.map +1 -0
  38. package/dist/esm/blueprints/evaluateBlueprintPin.js +71 -0
  39. package/dist/esm/blueprints/evaluateBlueprintPin.js.map +1 -0
  40. package/dist/esm/blueprints/index.d.ts +7 -0
  41. package/dist/esm/blueprints/index.d.ts.map +1 -0
  42. package/dist/esm/blueprints/index.js +7 -0
  43. package/dist/esm/blueprints/index.js.map +1 -0
  44. package/dist/esm/blueprints/verifyBlueprintPackage.d.ts +8 -0
  45. package/dist/esm/blueprints/verifyBlueprintPackage.d.ts.map +1 -0
  46. package/dist/esm/blueprints/verifyBlueprintPackage.js +192 -0
  47. package/dist/esm/blueprints/verifyBlueprintPackage.js.map +1 -0
  48. package/dist/esm/index.d.ts +1 -0
  49. package/dist/esm/index.d.ts.map +1 -1
  50. package/dist/esm/index.js +5 -1
  51. package/dist/esm/index.js.map +1 -1
  52. package/dist/esm/packageVersionRange.d.ts +1 -0
  53. package/dist/esm/packageVersionRange.d.ts.map +1 -1
  54. package/dist/esm/packageVersionRange.js +11 -1
  55. package/dist/esm/packageVersionRange.js.map +1 -1
  56. package/dist/esm/tsconfig.tsbuildinfo +1 -1
  57. package/index.ts +1 -0
  58. package/package.json +3 -3
  59. package/packageVersionRange.ts +12 -0
@@ -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
+ /** One screen of a blueprint's screen set, as the gallery lists it. */
5
+ export interface BlueprintCatalogScreen {
6
+ name: string;
7
+ title: string;
8
+ description?: string;
9
+ template?: string;
10
+ parent?: string;
11
+ order?: number;
12
+ }
13
+
14
+ /** One template a blueprint provides, as the gallery lists it. */
15
+ export interface BlueprintCatalogTemplate {
16
+ name: string;
17
+ kind: string;
18
+ displayName?: string;
19
+ description?: string;
20
+ type?: string;
21
+ category?: string;
22
+ thumbnail?: string;
23
+ }
24
+
25
+ /** One sample-data pack a blueprint ships, as the gallery's preview-data selector lists it. */
26
+ export interface BlueprintCatalogSampleData {
27
+ id: string;
28
+ title: string;
29
+ locale: string;
30
+ path: string;
31
+ default: boolean;
32
+ }
33
+
34
+ /**
35
+ * One domain blueprint as the gallery and the CLI documentation read it: what it is, how to show it, what it brings and what it needs.
36
+ * This is the shape of an entry of `blueprintCatalog.json`.
37
+ */
38
+ export interface BlueprintCatalogEntry {
39
+ package: string;
40
+ version: string;
41
+ domain: string;
42
+ displayName?: string;
43
+ description: string;
44
+ thumbnail: string;
45
+ license?: string;
46
+ licenseUrl?: string;
47
+ dependencies: { name: string; versionRange?: string }[];
48
+ screens: BlueprintCatalogScreen[];
49
+ templates: BlueprintCatalogTemplate[];
50
+ sampleData: BlueprintCatalogSampleData[];
51
+ }
52
+
53
+ /** The file a blueprint package ships and the gallery reads: every domain blueprint the build knows, in package name order. */
54
+ export interface BlueprintCatalogFile {
55
+ /** The version of this file's shape; a reader refuses a newer one. */
56
+ schema: 1;
57
+ blueprints: BlueprintCatalogEntry[];
58
+ }
@@ -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 { SampleDataPack, Screen } from '@cratis/scene.model';
5
+ import { TemplateSource } from '../templates/TemplateSource';
6
+
7
+ /**
8
+ * A blueprint package as the verification and the catalog read it: its manifest and the layouts and templates it provides (as a
9
+ * {@link TemplateSource}), plus what a manifest only names - the screens of its screen set, the sample-data packs its references point
10
+ * to, and the package's files. A React `ScenePackageBundle` already has most of this, so hosts pass their bundles straight in.
11
+ */
12
+ export interface BlueprintSource extends TemplateSource {
13
+ /** The screens the package provides. When given, every screen of the screen set must be among them. */
14
+ screens?: Screen[];
15
+
16
+ /** The sample-data packs the manifest's references point to, loaded. */
17
+ packs?: SampleDataPack[];
18
+
19
+ /**
20
+ * The package-relative paths of the files the package contains (`thumbnails/crm.svg`, `sampleData/crm.en.json`). When given, the
21
+ * thumbnails and sample-data files the manifest names must be among them.
22
+ */
23
+ files?: string[];
24
+ }
@@ -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
+ /** The name of the package that provides the foundation layouts and templates every domain blueprint builds on. */
5
+ export const foundationPackageName = 'Cratis.Blueprint.Default';
6
+
7
+ /** The template types a domain blueprint may declare (Part B 1.2). */
8
+ export const blueprintTemplateTypes: readonly string[] = ['Shell', 'Workspace', 'Dashboard', 'List', 'Board', 'Detail', 'Form', 'Wizard', 'Dialog', 'Auth', 'State'];
9
+
10
+ /** The prefix every template category of a domain blueprint starts with; the rest is the domain. */
11
+ export const businessCategoryPrefix = 'Business / ';
12
+
13
+ /** The longest a blueprint description may be. */
14
+ export const maximumBlueprintDescriptionLength = 200;
15
+
16
+ const domainPattern = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/;
17
+ const thumbnailPattern = /^thumbnails\/[A-Za-z0-9][A-Za-z0-9._-]*\.(png|webp|jpg|svg)$/;
18
+
19
+ /** Whether a value is a lower-case domain slug: `crm`, `project-tracker`. */
20
+ export const isValidBlueprintDomain = (domain: string | undefined): boolean => domain !== undefined && domainPattern.test(domain);
21
+
22
+ /** Whether a value is a package-relative thumbnail path: `thumbnails/<name>.png`, `.webp`, `.jpg` or `.svg`. */
23
+ export const isValidThumbnailPath = (path: string | undefined): boolean => path !== undefined && thumbnailPattern.test(path);
@@ -0,0 +1,95 @@
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 { PackageKind } from '@cratis/scene.model';
5
+ import { TemplateSource } from '../templates/TemplateSource';
6
+ import { BlueprintCatalogEntry, BlueprintCatalogFile } from './BlueprintCatalogEntry';
7
+
8
+ /**
9
+ * Describes every domain blueprint among the given packages - those of kind Blueprint that declare blueprint metadata - for a gallery:
10
+ * its thumbnail and description, its screen set in navigation order, the templates it provides, and the sample data a preview can use.
11
+ * A package that only provides foundation layouts and templates is not a domain blueprint and is not listed. The result is ordered by
12
+ * package name so the file it is written to is stable. The C# engine (`BlueprintCatalog.Describe`) returns the same entries; both are
13
+ * asserted against `blueprint-verification-fixtures.json`.
14
+ */
15
+ export function describeBlueprintCatalog(sources: readonly TemplateSource[]): BlueprintCatalogEntry[] {
16
+ return sources
17
+ .filter(source => source.manifest.kind === PackageKind.Blueprint && source.manifest.blueprint !== undefined)
18
+ .map((source): BlueprintCatalogEntry => {
19
+ const manifest = source.manifest;
20
+ const blueprint = manifest.blueprint!;
21
+ const defaultId = blueprint.defaultSampleData ?? ((manifest.sampleData ?? []).length === 1 ? manifest.sampleData![0].id : undefined);
22
+ const templates = [
23
+ ...(source.layouts ?? []).map(template => ({ kind: 'Layout', template })),
24
+ ...(source.screenTemplates ?? []).map(template => ({ kind: 'ScreenTemplate', template })),
25
+ ...(source.dialogTemplates ?? []).map(template => ({ kind: 'DialogTemplate', template })),
26
+ ];
27
+
28
+ return {
29
+ package: manifest.name,
30
+ version: manifest.version,
31
+ domain: blueprint.domain,
32
+ ...(manifest.displayName === undefined ? {} : { displayName: manifest.displayName }),
33
+ description: blueprint.description,
34
+ thumbnail: blueprint.thumbnail,
35
+ ...(manifest.license === undefined ? {} : { license: manifest.license }),
36
+ ...(manifest.licenseUrl === undefined ? {} : { licenseUrl: manifest.licenseUrl }),
37
+ dependencies: manifest.dependencies.map(dependency => ({ name: dependency.name, ...(dependency.versionRange === undefined ? {} : { versionRange: dependency.versionRange }) })),
38
+ screens: orderedScreens(blueprint.screenSet ?? []).map(screen => ({
39
+ name: screen.name,
40
+ title: screen.title,
41
+ ...(screen.description === undefined ? {} : { description: screen.description }),
42
+ ...(screen.template === undefined ? {} : { template: screen.template }),
43
+ ...(screen.parent === undefined ? {} : { parent: screen.parent }),
44
+ ...(screen.order === undefined ? {} : { order: screen.order }),
45
+ })),
46
+ templates: templates.map(({ kind, template }) => {
47
+ const described = template as { displayName?: string; description?: string; metadata?: { type?: string; category?: string; thumbnail?: string } };
48
+ return {
49
+ name: template.name,
50
+ kind,
51
+ ...(described.displayName === undefined ? {} : { displayName: described.displayName }),
52
+ ...(described.description === undefined ? {} : { description: described.description }),
53
+ ...(described.metadata?.type === undefined ? {} : { type: described.metadata.type }),
54
+ ...(described.metadata?.category === undefined ? {} : { category: described.metadata.category }),
55
+ ...(described.metadata?.thumbnail === undefined ? {} : { thumbnail: described.metadata.thumbnail }),
56
+ };
57
+ }),
58
+ sampleData: (manifest.sampleData ?? []).map(reference => ({ id: reference.id, title: reference.title, locale: reference.locale, path: reference.path, default: reference.id === defaultId })),
59
+ };
60
+ })
61
+ .sort((left, right) => (left.package < right.package ? -1 : left.package > right.package ? 1 : 0));
62
+ }
63
+
64
+ /** The catalog as the file a blueprint package ships. */
65
+ export const toBlueprintCatalogFile = (entries: BlueprintCatalogEntry[]): BlueprintCatalogFile => ({ schema: 1, blueprints: entries });
66
+
67
+ /**
68
+ * Puts screens in navigation order: siblings by `order` (a screen without one keeps its written place after those with one), each
69
+ * screen directly followed by its children, so a flat list reads top to bottom as the navigation does.
70
+ */
71
+ function orderedScreens<T extends { name: string; parent?: string; order?: number }>(screens: readonly T[]): T[] {
72
+ const names = new Set(screens.map(screen => screen.name));
73
+ const childrenOf = (parent: string | undefined): T[] => {
74
+ const siblings = screens.filter(screen => (parent === undefined ? screen.parent === undefined || !names.has(screen.parent) : screen.parent === parent));
75
+ return siblings
76
+ .map((screen, index) => ({ screen, index }))
77
+ .sort((left, right) => (left.screen.order ?? Number.MAX_SAFE_INTEGER) - (right.screen.order ?? Number.MAX_SAFE_INTEGER) || left.index - right.index)
78
+ .map(entry => entry.screen);
79
+ };
80
+
81
+ const result: T[] = [];
82
+ const seen = new Set<string>();
83
+ const walk = (level: T[]) => {
84
+ for (const screen of level) {
85
+ if (seen.has(screen.name)) continue;
86
+ seen.add(screen.name);
87
+ result.push(screen);
88
+ walk(childrenOf(screen.name));
89
+ }
90
+ };
91
+
92
+ walk(childrenOf(undefined));
93
+ for (const screen of screens) if (!seen.has(screen.name)) result.push(screen);
94
+ return result;
95
+ }
@@ -0,0 +1,71 @@
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 { compareVersions, isValidVersion, isValidVersionRange, isVersionSatisfiedBy } from '../packageVersionRange';
5
+
6
+ /** Where a blueprint reference's pin stands against what is installed and what is available. */
7
+ export enum BlueprintPinStatus {
8
+ /** The installed version satisfies the pin and nothing newer is available. */
9
+ Current = 'Current',
10
+
11
+ /** A newer version the pin allows is available: taking it changes nothing the pin promised to keep. */
12
+ UpdateAvailable = 'UpdateAvailable',
13
+
14
+ /** A newer version is available that the pin does not allow - usually a new major version - so taking it is a deliberate move that rewrites the pin. */
15
+ UpdateBlockedByPin = 'UpdateBlockedByPin',
16
+
17
+ /** The installed version does not satisfy the pin: the reference was chosen from a version that is no longer there. */
18
+ NotSatisfied = 'NotSatisfied',
19
+
20
+ /** The pin or a version is not one the engine understands. */
21
+ Invalid = 'Invalid',
22
+ }
23
+
24
+ /** The outcome of evaluating a pin. */
25
+ export interface BlueprintPinEvaluation {
26
+ status: BlueprintPinStatus;
27
+
28
+ /** The pin that was evaluated: the one given, or the default for the installed version when none was. */
29
+ pin: string;
30
+
31
+ /** Said in a sentence, for a tooltip or a diagnostic. */
32
+ message: string;
33
+ }
34
+
35
+ /**
36
+ * The pin a reference gets when a template is chosen from a blueprint: the caret range of the installed version, so the reference accepts
37
+ * every compatible update and refuses a new major version until someone moves it on purpose.
38
+ */
39
+ export const defaultBlueprintPin = (installedVersion: string): string => `^${installedVersion}`;
40
+
41
+ /**
42
+ * Decides whether "update available" applies to a template reference that pins a blueprint version (`TemplateReference.version`).
43
+ *
44
+ * A reference without a pin is held to {@link defaultBlueprintPin} of the installed version. The pin says which versions the reference
45
+ * is content to run against; the installed version must satisfy it, and a newer available version is an update - an ordinary one when
46
+ * the pin allows it, one the pin blocks (usually a major version) when it does not. Both engines implement this identically (`BlueprintVersionPin.Evaluate`) and are
47
+ * asserted against `blueprint-verification-fixtures.json`.
48
+ *
49
+ * @param pin The reference's version range, or undefined when it has none.
50
+ * @param installedVersion The version of the blueprint that is installed.
51
+ * @param latestVersion The newest version of the blueprint that is available; when undefined only the installed version is known.
52
+ */
53
+ export function evaluateBlueprintPin(pin: string | undefined, installedVersion: string, latestVersion?: string): BlueprintPinEvaluation {
54
+ const effective = pin === undefined || pin.trim() === '' ? defaultBlueprintPin(installedVersion) : pin.trim();
55
+
56
+ if (!isValidVersion(installedVersion)) return { status: BlueprintPinStatus.Invalid, pin: effective, message: `Installed version '${installedVersion}' is not a version` };
57
+ if (!isValidVersionRange(effective)) return { status: BlueprintPinStatus.Invalid, pin: effective, message: `Pin '${effective}' is not a version range` };
58
+ if (latestVersion !== undefined && !isValidVersion(latestVersion)) return { status: BlueprintPinStatus.Invalid, pin: effective, message: `Latest version '${latestVersion}' is not a version` };
59
+
60
+ if (!isVersionSatisfiedBy(installedVersion, effective)) {
61
+ return { status: BlueprintPinStatus.NotSatisfied, pin: effective, message: `Version ${installedVersion} is installed but the reference pins ${effective}` };
62
+ }
63
+
64
+ if (latestVersion === undefined || compareVersions(latestVersion, installedVersion) <= 0) {
65
+ return { status: BlueprintPinStatus.Current, pin: effective, message: `Version ${installedVersion} is the latest` };
66
+ }
67
+
68
+ return isVersionSatisfiedBy(latestVersion, effective)
69
+ ? { status: BlueprintPinStatus.UpdateAvailable, pin: effective, message: `Version ${latestVersion} is available, and ${effective} allows it` }
70
+ : { status: BlueprintPinStatus.UpdateBlockedByPin, pin: effective, message: `Version ${latestVersion} is available but ${effective} does not allow it` };
71
+ }
@@ -0,0 +1,9 @@
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 './BlueprintSource';
5
+ export * from './BlueprintCatalogEntry';
6
+ export * from './blueprintRules';
7
+ export * from './verifyBlueprintPackage';
8
+ export * from './describeBlueprintCatalog';
9
+ export * from './evaluateBlueprintPin';
@@ -0,0 +1,192 @@
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 { PackageKind, ScreenTemplate } from '@cratis/scene.model';
5
+ import { isValidVersion } from '../packageVersionRange';
6
+ import { resolveScreenTemplates } from '../resolveScreenTemplates';
7
+ import { validateSampleDataPackages } from '../sampleData/validateSampleDataPackages';
8
+ import { describeTemplateCatalog } from '../templates/describeTemplateCatalog';
9
+ import { TemplateSource } from '../templates/TemplateSource';
10
+ import { BlueprintSource } from './BlueprintSource';
11
+ import {
12
+ blueprintTemplateTypes, businessCategoryPrefix, foundationPackageName, isValidBlueprintDomain, isValidThumbnailPath, maximumBlueprintDescriptionLength,
13
+ } from './blueprintRules';
14
+
15
+ /** How a blueprint is verified. */
16
+ export interface BlueprintVerificationOptions {
17
+ /** The package that provides the foundation set a domain blueprint must depend on. Defaults to {@link foundationPackageName}. */
18
+ foundationPackage?: string;
19
+
20
+ /** Whether a package without blueprint metadata is a problem. Set for every domain blueprint; unset for a package that only provides foundation layouts and templates. */
21
+ requireBlueprintMetadata?: boolean;
22
+ }
23
+
24
+ /**
25
+ * The definition of done for a blueprint (spec Part B 6), as rules a package can be held to: valid manifest and version; for a domain
26
+ * blueprint its domain, description, thumbnail and screen set; every template with complete metadata, a category, a type from the
27
+ * vocabulary, a description and a thumbnail; every template placed by the slot resolver, with nothing unplaced, ambiguous or nested in
28
+ * itself (ambiguity is only a problem for a domain blueprint: the foundation's generic nesting chain fits slots many templates declare, and is chosen explicitly); sample data present, locale-tagged and valid; and a dependency on the foundation set. The C# engine
29
+ * (`BlueprintVerification.Verify`) reports the same problems in the same order; both are asserted against
30
+ * `blueprint-verification-fixtures.json`.
31
+ *
32
+ * @param source The blueprint to verify.
33
+ * @param library The other packages available - above all the foundation set - so templates can be placed into its layouts and checked against its versions.
34
+ * @param options How strictly to verify.
35
+ * @returns The problems; empty when the blueprint is done.
36
+ */
37
+ export function verifyBlueprintPackage(source: BlueprintSource, library: readonly TemplateSource[] = [], options: BlueprintVerificationOptions = {}): string[] {
38
+ const manifest = source.manifest;
39
+ const name = manifest.name;
40
+ const label = `Blueprint '${name}'`;
41
+ const foundation = options.foundationPackage ?? foundationPackageName;
42
+
43
+ if (manifest.kind !== PackageKind.Blueprint) return [`Package '${name}' is not a Blueprint package`];
44
+
45
+ const problems: string[] = [];
46
+ if (!isValidVersion(manifest.version)) problems.push(`${label} declares an invalid version '${manifest.version ?? ''}'`);
47
+
48
+ const blueprint = manifest.blueprint;
49
+ const domain = blueprint !== undefined;
50
+ const available = new Set([...(source.screenTemplates ?? []), ...library.flatMap(other => other.screenTemplates ?? [])].map(template => template.name));
51
+
52
+ if (!blueprint) {
53
+ if (options.requireBlueprintMetadata) problems.push(`${label} declares no blueprint metadata (domain, description, thumbnail and screen set)`);
54
+ } else {
55
+ if ((blueprint.domain ?? '').trim() === '') problems.push(`${label} declares no domain`);
56
+ else if (!isValidBlueprintDomain(blueprint.domain)) problems.push(`${label} declares an invalid domain '${blueprint.domain}'; use lower-case words separated by '-'`);
57
+
58
+ if ((blueprint.description ?? '').trim() === '') problems.push(`${label} declares no description`);
59
+ else if (blueprint.description.length > maximumBlueprintDescriptionLength) problems.push(`${label} declares a description longer than ${maximumBlueprintDescriptionLength} characters`);
60
+
61
+ problems.push(...thumbnailProblems(label, blueprint.thumbnail, source.files));
62
+ problems.push(...screenSetProblems(source, label, available));
63
+
64
+ if (blueprint.defaultSampleData !== undefined && !(manifest.sampleData ?? []).some(reference => reference.id === blueprint.defaultSampleData)) {
65
+ problems.push(`${label} declares default sample data '${blueprint.defaultSampleData}', which is not one of its sample data packs`);
66
+ }
67
+
68
+ if ((manifest.sampleData ?? []).length === 0) problems.push(`${label} ships no sample data pack`);
69
+ else if ((manifest.sampleData ?? []).length > 1 && blueprint.defaultSampleData === undefined) problems.push(`${label} ships several sample data packs but declares no default`);
70
+
71
+ if (name !== foundation && !manifest.dependencies.some(dependency => dependency.name === foundation)) {
72
+ problems.push(`${label} does not depend on the foundation set '${foundation}'`);
73
+ }
74
+ }
75
+
76
+ problems.push(...validateSampleDataPackages([{ manifest, packs: source.packs }]));
77
+ for (const reference of manifest.sampleData ?? []) {
78
+ if (source.files !== undefined && !source.files.includes(reference.path)) problems.push(`${label} sample data pack '${reference.id}' names '${reference.path}', which is not in the package`);
79
+ }
80
+
81
+ problems.push(...templateProblems(source, library, domain));
82
+ problems.push(...placementProblems(source, library, label, domain));
83
+ return problems;
84
+ }
85
+
86
+ function thumbnailProblems(owner: string, thumbnail: string | undefined, files: string[] | undefined): string[] {
87
+ if ((thumbnail ?? '').trim() === '') return [`${owner} declares no thumbnail`];
88
+ if (!isValidThumbnailPath(thumbnail)) return [`${owner} declares thumbnail '${thumbnail}', which is not 'thumbnails/<name>.png|webp|jpg|svg'`];
89
+ if (files !== undefined && !files.includes(thumbnail!)) return [`${owner} declares thumbnail '${thumbnail}', which is not in the package`];
90
+ return [];
91
+ }
92
+
93
+ function screenSetProblems(source: BlueprintSource, label: string, availableTemplates: Set<string>): string[] {
94
+ const set = source.manifest.blueprint!.screenSet ?? [];
95
+ if (set.length === 0) return [`${label} declares no ready-to-use screen set`];
96
+
97
+ const problems: string[] = [];
98
+ const names = new Set<string>();
99
+ for (const screen of set) {
100
+ if ((screen.name ?? '').trim() === '') {
101
+ problems.push(`${label} declares a screen set entry without a name`);
102
+ continue;
103
+ }
104
+
105
+ if (names.has(screen.name)) problems.push(`${label} declares screen '${screen.name}' more than once`);
106
+ names.add(screen.name);
107
+ }
108
+
109
+ for (const screen of set) {
110
+ if ((screen.name ?? '').trim() === '') continue;
111
+ const owner = `${label} screen '${screen.name}'`;
112
+ if ((screen.title ?? '').trim() === '') problems.push(`${owner} declares no title`);
113
+ if (screen.template !== undefined && !availableTemplates.has(screen.template)) problems.push(`${owner} uses template '${screen.template}', which is not available`);
114
+ if (screen.parent !== undefined && !names.has(screen.parent)) problems.push(`${owner} has parent '${screen.parent}', which is not in the screen set`);
115
+ if (source.screens !== undefined && !source.screens.some(candidate => candidate.name === screen.name)) problems.push(`${owner} is named in the screen set but the package does not provide it`);
116
+ }
117
+
118
+ for (const screen of set) {
119
+ if (screen.parent !== undefined && names.has(screen.parent) && isOwnAncestor(screen.name, set)) problems.push(`${label} screen '${screen.name}' is its own ancestor`);
120
+ }
121
+
122
+ return problems;
123
+ }
124
+
125
+ function isOwnAncestor(name: string, set: { name: string; parent?: string }[]): boolean {
126
+ const visited = new Set<string>();
127
+ for (let current = set.find(screen => screen.name === name)?.parent; current !== undefined; current = set.find(screen => screen.name === current)?.parent) {
128
+ if (current === name) return true;
129
+ if (visited.has(current)) return false;
130
+ visited.add(current);
131
+ }
132
+
133
+ return false;
134
+ }
135
+
136
+ function templateProblems(source: BlueprintSource, library: readonly TemplateSource[], domain: boolean): string[] {
137
+ const problems: string[] = [];
138
+ const described = [...(source.screenTemplates ?? []), ...(source.dialogTemplates ?? [])];
139
+ const everyTemplate: { name: string; displayName?: string; description?: string; metadata?: { type?: string; category?: string; thumbnail?: string }; describable: boolean }[] = [
140
+ ...(source.layouts ?? []).map(layout => ({ name: layout.name, metadata: layout.metadata, describable: false })),
141
+ ...described.map(template => ({ name: template.name, displayName: template.displayName, description: template.description, metadata: template.metadata, describable: true })),
142
+ ];
143
+
144
+ const others = library.filter(other => other.manifest.name !== source.manifest.name);
145
+ for (const entry of describeTemplateCatalog([source, ...others]).filter(candidate => candidate.package === source.manifest.name)) {
146
+ problems.push(...entry.problems);
147
+ }
148
+
149
+ for (const template of everyTemplate) {
150
+ const owner = `Template '${template.name}'`;
151
+ if (template.describable && (template.displayName ?? '').trim() === '') problems.push(`${owner} declares no display name`);
152
+ if (template.describable && (template.description ?? '').trim() === '') problems.push(`${owner} declares no description`);
153
+ if (!domain) continue;
154
+
155
+ const metadata = template.metadata;
156
+ if (metadata?.type === undefined) problems.push(`${owner} declares no type`);
157
+ else if (!blueprintTemplateTypes.includes(metadata.type)) problems.push(`${owner} declares type '${metadata.type}', which is not one of ${blueprintTemplateTypes.join(', ')}`);
158
+
159
+ if (metadata?.category !== undefined && metadata.category.trim() !== '' && !metadata.category.startsWith(businessCategoryPrefix)) {
160
+ problems.push(`${owner} declares category '${metadata.category}', which does not start with '${businessCategoryPrefix}'`);
161
+ }
162
+
163
+ problems.push(...thumbnailProblems(owner, metadata?.thumbnail, source.files));
164
+ }
165
+
166
+ return problems;
167
+ }
168
+
169
+ function placementProblems(source: BlueprintSource, library: readonly TemplateSource[], label: string, domain: boolean): string[] {
170
+ const own: ScreenTemplate[] = (source.screenTemplates ?? []).filter(template => template.fitsSlot !== undefined);
171
+ if (own.length === 0) return [];
172
+
173
+ const layouts = [...(source.layouts ?? []), ...library.flatMap(other => other.layouts ?? [])];
174
+ const everyTemplate = [...(source.screenTemplates ?? []), ...library.flatMap(other => other.screenTemplates ?? [])];
175
+ if (layouts.length === 0) return [`${label} has no layout to place its screen templates in`];
176
+
177
+ const resolutions = layouts.map(layout => ({ layout, resolution: resolveScreenTemplates(layout, everyTemplate) }));
178
+ const problems: string[] = [];
179
+
180
+ for (const template of own) {
181
+ if (resolutions.some(({ resolution }) => resolution.placements.some(placement => placement.template === template.name))) continue;
182
+
183
+ const unplacedEverywhere = resolutions.flatMap(({ resolution }) => resolution.unplaced.filter(candidate => candidate.template === template.name));
184
+ const unplaced = unplacedEverywhere.find(candidate => candidate.candidates.length > 1);
185
+ const cyclic = resolutions.some(({ resolution }) => resolution.cycles.some(cycle => cycle.includes(template.name)));
186
+ if (cyclic) problems.push(`Template '${template.name}' is nested inside itself`);
187
+ else if (unplaced && domain) problems.push(`Template '${template.name}' is ambiguous: slot '${unplaced.slot}' is declared by ${unplaced.candidates.join(', ')}`);
188
+ else if (!unplaced) problems.push(`Template '${template.name}' is unplaced: no layout or template declares slot '${template.fitsSlot}'`);
189
+ }
190
+
191
+ return problems;
192
+ }
@@ -0,0 +1,32 @@
1
+ 'use strict';
2
+
3
+ // Copyright (c) Cratis. All rights reserved.
4
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
5
+ /** The name of the package that provides the foundation layouts and templates every domain blueprint builds on. */ const foundationPackageName = 'Cratis.Blueprint.Default';
6
+ /** The template types a domain blueprint may declare (Part B 1.2). */ const blueprintTemplateTypes = [
7
+ 'Shell',
8
+ 'Workspace',
9
+ 'Dashboard',
10
+ 'List',
11
+ 'Board',
12
+ 'Detail',
13
+ 'Form',
14
+ 'Wizard',
15
+ 'Dialog',
16
+ 'Auth',
17
+ 'State'
18
+ ];
19
+ /** The prefix every template category of a domain blueprint starts with; the rest is the domain. */ const businessCategoryPrefix = 'Business / ';
20
+ /** The longest a blueprint description may be. */ const maximumBlueprintDescriptionLength = 200;
21
+ const domainPattern = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/;
22
+ const thumbnailPattern = /^thumbnails\/[A-Za-z0-9][A-Za-z0-9._-]*\.(png|webp|jpg|svg)$/;
23
+ /** Whether a value is a lower-case domain slug: `crm`, `project-tracker`. */ const isValidBlueprintDomain = (domain)=>domain !== undefined && domainPattern.test(domain);
24
+ /** Whether a value is a package-relative thumbnail path: `thumbnails/<name>.png`, `.webp`, `.jpg` or `.svg`. */ const isValidThumbnailPath = (path)=>path !== undefined && thumbnailPattern.test(path);
25
+
26
+ exports.blueprintTemplateTypes = blueprintTemplateTypes;
27
+ exports.businessCategoryPrefix = businessCategoryPrefix;
28
+ exports.foundationPackageName = foundationPackageName;
29
+ exports.isValidBlueprintDomain = isValidBlueprintDomain;
30
+ exports.isValidThumbnailPath = isValidThumbnailPath;
31
+ exports.maximumBlueprintDescriptionLength = maximumBlueprintDescriptionLength;
32
+ //# sourceMappingURL=blueprintRules.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"blueprintRules.js","sources":["../../../blueprints/blueprintRules.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\n/** The name of the package that provides the foundation layouts and templates every domain blueprint builds on. */\nexport const foundationPackageName = 'Cratis.Blueprint.Default';\n\n/** The template types a domain blueprint may declare (Part B 1.2). */\nexport const blueprintTemplateTypes: readonly string[] = ['Shell', 'Workspace', 'Dashboard', 'List', 'Board', 'Detail', 'Form', 'Wizard', 'Dialog', 'Auth', 'State'];\n\n/** The prefix every template category of a domain blueprint starts with; the rest is the domain. */\nexport const businessCategoryPrefix = 'Business / ';\n\n/** The longest a blueprint description may be. */\nexport const maximumBlueprintDescriptionLength = 200;\n\nconst domainPattern = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/;\nconst thumbnailPattern = /^thumbnails\\/[A-Za-z0-9][A-Za-z0-9._-]*\\.(png|webp|jpg|svg)$/;\n\n/** Whether a value is a lower-case domain slug: `crm`, `project-tracker`. */\nexport const isValidBlueprintDomain = (domain: string | undefined): boolean => domain !== undefined && domainPattern.test(domain);\n\n/** Whether a value is a package-relative thumbnail path: `thumbnails/<name>.png`, `.webp`, `.jpg` or `.svg`. */\nexport const isValidThumbnailPath = (path: string | undefined): boolean => path !== undefined && thumbnailPattern.test(path);\n"],"names":["foundationPackageName","blueprintTemplateTypes","businessCategoryPrefix","maximumBlueprintDescriptionLength","domainPattern","thumbnailPattern","isValidBlueprintDomain","domain","undefined","test","isValidThumbnailPath","path"],"mappings":";;AAAA;AACA;AAEA,oHACO,MAAMA,qBAAAA,GAAwB;AAErC,uEACO,MAAMC,sBAAAA,GAA4C;AAAC,IAAA,OAAA;AAAS,IAAA,WAAA;AAAa,IAAA,WAAA;AAAa,IAAA,MAAA;AAAQ,IAAA,OAAA;AAAS,IAAA,QAAA;AAAU,IAAA,MAAA;AAAQ,IAAA,QAAA;AAAU,IAAA,QAAA;AAAU,IAAA,MAAA;AAAQ,IAAA;;AAE5J,qGACO,MAAMC,sBAAAA,GAAyB;AAEtC,mDACO,MAAMC,iCAAAA,GAAoC;AAEjD,MAAMC,aAAAA,GAAgB,+BAAA;AACtB,MAAMC,gBAAAA,GAAmB,8DAAA;AAEzB,8EACO,MAAMC,sBAAAA,GAAyB,CAACC,MAAAA,GAAwCA,MAAAA,KAAWC,SAAAA,IAAaJ,aAAAA,CAAcK,IAAI,CAACF,MAAAA;AAE1H,iHACO,MAAMG,oBAAAA,GAAuB,CAACC,IAAAA,GAAsCA,IAAAA,KAASH,SAAAA,IAAaH,gBAAAA,CAAiBI,IAAI,CAACE,IAAAA;;;;;;;"}
@@ -0,0 +1,134 @@
1
+ 'use strict';
2
+
3
+ var scene_model = require('@cratis/scene.model');
4
+
5
+ // Copyright (c) Cratis. All rights reserved.
6
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
7
+ /**
8
+ * Describes every domain blueprint among the given packages - those of kind Blueprint that declare blueprint metadata - for a gallery:
9
+ * its thumbnail and description, its screen set in navigation order, the templates it provides, and the sample data a preview can use.
10
+ * A package that only provides foundation layouts and templates is not a domain blueprint and is not listed. The result is ordered by
11
+ * package name so the file it is written to is stable. The C# engine (`BlueprintCatalog.Describe`) returns the same entries; both are
12
+ * asserted against `blueprint-verification-fixtures.json`.
13
+ */ function describeBlueprintCatalog(sources) {
14
+ return sources.filter((source)=>source.manifest.kind === scene_model.PackageKind.Blueprint && source.manifest.blueprint !== undefined).map((source)=>{
15
+ const manifest = source.manifest;
16
+ const blueprint = manifest.blueprint;
17
+ const defaultId = blueprint.defaultSampleData ?? ((manifest.sampleData ?? []).length === 1 ? manifest.sampleData[0].id : undefined);
18
+ const templates = [
19
+ ...(source.layouts ?? []).map((template)=>({
20
+ kind: 'Layout',
21
+ template
22
+ })),
23
+ ...(source.screenTemplates ?? []).map((template)=>({
24
+ kind: 'ScreenTemplate',
25
+ template
26
+ })),
27
+ ...(source.dialogTemplates ?? []).map((template)=>({
28
+ kind: 'DialogTemplate',
29
+ template
30
+ }))
31
+ ];
32
+ return {
33
+ package: manifest.name,
34
+ version: manifest.version,
35
+ domain: blueprint.domain,
36
+ ...manifest.displayName === undefined ? {} : {
37
+ displayName: manifest.displayName
38
+ },
39
+ description: blueprint.description,
40
+ thumbnail: blueprint.thumbnail,
41
+ ...manifest.license === undefined ? {} : {
42
+ license: manifest.license
43
+ },
44
+ ...manifest.licenseUrl === undefined ? {} : {
45
+ licenseUrl: manifest.licenseUrl
46
+ },
47
+ dependencies: manifest.dependencies.map((dependency)=>({
48
+ name: dependency.name,
49
+ ...dependency.versionRange === undefined ? {} : {
50
+ versionRange: dependency.versionRange
51
+ }
52
+ })),
53
+ screens: orderedScreens(blueprint.screenSet ?? []).map((screen)=>({
54
+ name: screen.name,
55
+ title: screen.title,
56
+ ...screen.description === undefined ? {} : {
57
+ description: screen.description
58
+ },
59
+ ...screen.template === undefined ? {} : {
60
+ template: screen.template
61
+ },
62
+ ...screen.parent === undefined ? {} : {
63
+ parent: screen.parent
64
+ },
65
+ ...screen.order === undefined ? {} : {
66
+ order: screen.order
67
+ }
68
+ })),
69
+ templates: templates.map(({ kind, template })=>{
70
+ const described = template;
71
+ return {
72
+ name: template.name,
73
+ kind,
74
+ ...described.displayName === undefined ? {} : {
75
+ displayName: described.displayName
76
+ },
77
+ ...described.description === undefined ? {} : {
78
+ description: described.description
79
+ },
80
+ ...described.metadata?.type === undefined ? {} : {
81
+ type: described.metadata.type
82
+ },
83
+ ...described.metadata?.category === undefined ? {} : {
84
+ category: described.metadata.category
85
+ },
86
+ ...described.metadata?.thumbnail === undefined ? {} : {
87
+ thumbnail: described.metadata.thumbnail
88
+ }
89
+ };
90
+ }),
91
+ sampleData: (manifest.sampleData ?? []).map((reference)=>({
92
+ id: reference.id,
93
+ title: reference.title,
94
+ locale: reference.locale,
95
+ path: reference.path,
96
+ default: reference.id === defaultId
97
+ }))
98
+ };
99
+ }).sort((left, right)=>left.package < right.package ? -1 : left.package > right.package ? 1 : 0);
100
+ }
101
+ /** The catalog as the file a blueprint package ships. */ const toBlueprintCatalogFile = (entries)=>({
102
+ schema: 1,
103
+ blueprints: entries
104
+ });
105
+ /**
106
+ * Puts screens in navigation order: siblings by `order` (a screen without one keeps its written place after those with one), each
107
+ * screen directly followed by its children, so a flat list reads top to bottom as the navigation does.
108
+ */ function orderedScreens(screens) {
109
+ const names = new Set(screens.map((screen)=>screen.name));
110
+ const childrenOf = (parent)=>{
111
+ const siblings = screens.filter((screen)=>parent === undefined ? screen.parent === undefined || !names.has(screen.parent) : screen.parent === parent);
112
+ return siblings.map((screen, index)=>({
113
+ screen,
114
+ index
115
+ })).sort((left, right)=>(left.screen.order ?? Number.MAX_SAFE_INTEGER) - (right.screen.order ?? Number.MAX_SAFE_INTEGER) || left.index - right.index).map((entry)=>entry.screen);
116
+ };
117
+ const result = [];
118
+ const seen = new Set();
119
+ const walk = (level)=>{
120
+ for (const screen of level){
121
+ if (seen.has(screen.name)) continue;
122
+ seen.add(screen.name);
123
+ result.push(screen);
124
+ walk(childrenOf(screen.name));
125
+ }
126
+ };
127
+ walk(childrenOf(undefined));
128
+ for (const screen of screens)if (!seen.has(screen.name)) result.push(screen);
129
+ return result;
130
+ }
131
+
132
+ exports.describeBlueprintCatalog = describeBlueprintCatalog;
133
+ exports.toBlueprintCatalogFile = toBlueprintCatalogFile;
134
+ //# sourceMappingURL=describeBlueprintCatalog.js.map