From eb2838eac430287ddb1a9a9b593319619a18aa7f Mon Sep 17 00:00:00 2001 From: Einar Date: Sat, 15 Aug 2026 10:39:37 +0200 Subject: [PATCH 1/3] Add UiStarter to the Scene model A packaged, versioned UI starting point: a package list, the themes it ships as compatible choices, and a gallery of sample screens - the data shape Cratis/Scene#6's starters and Studio's new-project scaffolding build on. Mirrored in both Cratis.Scene.Model and @cratis/scene.model, with the shared shape manifest updated to keep them in parity. --- Source/DotNET/Model/Starters/UiStarter.cs | 17 +++++++++++++++++ Source/JavaScript/model/index.ts | 1 + Source/JavaScript/model/starters/UiStarter.ts | 18 ++++++++++++++++++ Source/JavaScript/model/starters/index.ts | 4 ++++ scene-model-shape.json | 3 ++- 5 files changed, 42 insertions(+), 1 deletion(-) create mode 100644 Source/DotNET/Model/Starters/UiStarter.cs create mode 100644 Source/JavaScript/model/starters/UiStarter.ts create mode 100644 Source/JavaScript/model/starters/index.ts diff --git a/Source/DotNET/Model/Starters/UiStarter.cs b/Source/DotNET/Model/Starters/UiStarter.cs new file mode 100644 index 0000000..96fbacb --- /dev/null +++ b/Source/DotNET/Model/Starters/UiStarter.cs @@ -0,0 +1,17 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Cratis.Scene.Model.Starters; + +/// +/// A packaged, versioned UI starting point: a package list, the themes it ships compatible with, and a +/// gallery of ordinary screens shipped alongside it - part of Cratis/Scene#6. A starter is data, not a +/// .play language construct; Studio's "new project" flow scaffolds from it, and its gallery boots +/// through the real Scene.Engine + Scene.React inside a sandboxed +/// built from - there is no separate preview pipeline and no mocked screens. +/// +/// The starter's name. +/// The component packages this starter bundles, in the same override-priority order a declares them. +/// The names of the themes this starter ships as compatible choices. +/// The names of the ordinary s shipped as this starter's sample gallery. +public record UiStarter(string Name, IReadOnlyList Packages, IReadOnlyList Themes, IReadOnlyList Gallery); diff --git a/Source/JavaScript/model/index.ts b/Source/JavaScript/model/index.ts index 6dcf92e..82f1a95 100644 --- a/Source/JavaScript/model/index.ts +++ b/Source/JavaScript/model/index.ts @@ -9,3 +9,4 @@ export * from './forms'; export * from './contributionPoints'; export * from './profiles'; export * from './screens'; +export * from './starters'; diff --git a/Source/JavaScript/model/starters/UiStarter.ts b/Source/JavaScript/model/starters/UiStarter.ts new file mode 100644 index 0000000..7094f71 --- /dev/null +++ b/Source/JavaScript/model/starters/UiStarter.ts @@ -0,0 +1,18 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +/** + * A packaged, versioned UI starting point: a package list, the themes it ships compatible with, and a + * gallery of ordinary screens shipped alongside it - part of Cratis/Scene#6. A starter is data, not a + * `.play` language construct; Studio's "new project" flow scaffolds from it, and its gallery boots + * through the real `Scene.Engine` + `Scene.React` inside a sandboxed `UiProfile` built from `packages` - + * there is no separate preview pipeline and no mocked screens. + */ +export interface UiStarter { + name: string; + packages: string[]; + themes: string[]; + gallery: string[]; +} + +export const UiStarterPropertyNames: (keyof UiStarter)[] = ['name', 'packages', 'themes', 'gallery']; diff --git a/Source/JavaScript/model/starters/index.ts b/Source/JavaScript/model/starters/index.ts new file mode 100644 index 0000000..649056d --- /dev/null +++ b/Source/JavaScript/model/starters/index.ts @@ -0,0 +1,4 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +export * from './UiStarter'; diff --git a/scene-model-shape.json b/scene-model-shape.json index ea98d44..9fc6017 100644 --- a/scene-model-shape.json +++ b/scene-model-shape.json @@ -38,7 +38,8 @@ "NavigationItem": ["label", "targetScreen", "routeParameterBindings", "order", "group"], "UiProfile": ["name", "targetPlatform", "packages", "defaultSizeClass"], "Theme": ["name", "compatibleWith"], - "Screen": ["name", "layout", "slotContent", "forms", "contributions"] + "Screen": ["name", "layout", "slotContent", "forms", "contributions"], + "UiStarter": ["name", "packages", "themes", "gallery"] }, "enums": { "WidthSizeClass": ["Compact", "Regular"], From e61ac0c0e5148390059f06e9540ff5100a9b660c Mon Sep 17 00:00:00 2001 From: Einar Date: Sat, 15 Aug 2026 10:39:43 +0200 Subject: [PATCH 2/3] Add starter profile and theme resolution to Cratis.Scene.Engine StarterProfileBuilder builds the sandboxed UiProfile a UiStarter's gallery boots through, scoped to exactly the starter's own package list, so Studio can run the gallery as a real working mini-app rather than a simulated preview. StarterThemeValidation reuses ThemeCompatibility to find which of a starter's declared themes are actually incompatible with its own packages - a starter is versioned per package combination, so this is the same compatibility gap the theme engine already surfaces for a UiProfile. Both are asserted against a shared ui-starter-fixtures.json corpus so the upcoming TypeScript twin cannot drift from this implementation. --- ...cking_against_the_shared_fixture_corpus.cs | 89 +++++++++++++++++++ ...cking_against_the_shared_fixture_corpus.cs | 70 +++++++++++++++ .../Engine/Starters/StarterProfileBuilder.cs | 27 ++++++ .../Engine/Starters/StarterThemeValidation.cs | 33 +++++++ ui-starter-fixtures.json | 46 ++++++++++ 5 files changed, 265 insertions(+) create mode 100644 Source/DotNET/Engine.Specs/for_StarterProfileBuilder/when_checking_against_the_shared_fixture_corpus.cs create mode 100644 Source/DotNET/Engine.Specs/for_StarterThemeValidation/when_checking_against_the_shared_fixture_corpus.cs create mode 100644 Source/DotNET/Engine/Starters/StarterProfileBuilder.cs create mode 100644 Source/DotNET/Engine/Starters/StarterThemeValidation.cs create mode 100644 ui-starter-fixtures.json diff --git a/Source/DotNET/Engine.Specs/for_StarterProfileBuilder/when_checking_against_the_shared_fixture_corpus.cs b/Source/DotNET/Engine.Specs/for_StarterProfileBuilder/when_checking_against_the_shared_fixture_corpus.cs new file mode 100644 index 0000000..f22a4be --- /dev/null +++ b/Source/DotNET/Engine.Specs/for_StarterProfileBuilder/when_checking_against_the_shared_fixture_corpus.cs @@ -0,0 +1,89 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Text.Json; +using Cratis.Scene.Engine.Starters; +using Cratis.Scene.Model.Profiles; +using Cratis.Scene.Model.Starters; + +namespace Cratis.Scene.Engine.for_StarterProfileBuilder; + +public class when_checking_against_the_shared_fixture_corpus : Specification +{ + record FixtureCase(string Name, UiStarter Starter, string TargetPlatform, UiProfile ExpectedProfile); + + List _cases = null!; + List<(FixtureCase Case, UiProfile Actual)> _results = null!; + + void Establish() + { + var manifestPath = Path.Combine(FindRepositoryRoot(), "ui-starter-fixtures.json"); + using var document = JsonDocument.Parse(File.ReadAllText(manifestPath)); + + _cases = document.RootElement.GetProperty("profileCases").EnumerateArray().Select(ToFixtureCase).ToList(); + } + + void Because() => _results = [.. _cases.Select(fixtureCase => (fixtureCase, StarterProfileBuilder.BuildProfile(fixtureCase.Starter, fixtureCase.TargetPlatform)))]; + + [Fact] + void should_match_the_expected_name_for_every_case() + { + foreach (var (fixtureCase, actual) in _results) + { + (fixtureCase.Name, actual.Name).ShouldEqual((fixtureCase.Name, fixtureCase.ExpectedProfile.Name)); + } + } + + [Fact] + void should_match_the_expected_target_platform_for_every_case() + { + foreach (var (fixtureCase, actual) in _results) + { + (fixtureCase.Name, actual.TargetPlatform).ShouldEqual((fixtureCase.Name, fixtureCase.ExpectedProfile.TargetPlatform)); + } + } + + [Fact] + void should_match_the_expected_packages_for_every_case() + { + foreach (var (fixtureCase, actual) in _results) + { + (fixtureCase.Name, Flatten(actual.Packages)).ShouldEqual((fixtureCase.Name, Flatten(fixtureCase.ExpectedProfile.Packages))); + } + } + + static string Flatten(IReadOnlyList packages) => string.Join(',', packages); + + static FixtureCase ToFixtureCase(JsonElement element) + { + var name = element.GetProperty("name").GetString()!; + + var starterElement = element.GetProperty("starter"); + var starter = new UiStarter( + starterElement.GetProperty("name").GetString()!, + starterElement.GetProperty("packages").EnumerateArray().Select(value => value.GetString()!).ToList(), + starterElement.GetProperty("themes").EnumerateArray().Select(value => value.GetString()!).ToList(), + starterElement.GetProperty("gallery").EnumerateArray().Select(value => value.GetString()!).ToList()); + + var targetPlatform = element.GetProperty("targetPlatform").GetString()!; + + var expectedProfileElement = element.GetProperty("expectedProfile"); + var expectedProfile = new UiProfile( + expectedProfileElement.GetProperty("name").GetString()!, + expectedProfileElement.GetProperty("targetPlatform").GetString()!, + expectedProfileElement.GetProperty("packages").EnumerateArray().Select(value => value.GetString()!).ToList()); + + return new(name, starter, targetPlatform, expectedProfile); + } + + static string FindRepositoryRoot() + { + var directory = new DirectoryInfo(AppContext.BaseDirectory); + while (directory is not null && !File.Exists(Path.Combine(directory.FullName, "Scene.slnx"))) + { + directory = directory.Parent; + } + + return directory?.FullName ?? throw new DirectoryNotFoundException("Could not locate the repository root (Scene.slnx) above " + AppContext.BaseDirectory); + } +} diff --git a/Source/DotNET/Engine.Specs/for_StarterThemeValidation/when_checking_against_the_shared_fixture_corpus.cs b/Source/DotNET/Engine.Specs/for_StarterThemeValidation/when_checking_against_the_shared_fixture_corpus.cs new file mode 100644 index 0000000..424152a --- /dev/null +++ b/Source/DotNET/Engine.Specs/for_StarterThemeValidation/when_checking_against_the_shared_fixture_corpus.cs @@ -0,0 +1,70 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Text.Json; +using Cratis.Scene.Engine.Starters; +using Cratis.Scene.Model.Profiles; +using Cratis.Scene.Model.Starters; + +namespace Cratis.Scene.Engine.for_StarterThemeValidation; + +public class when_checking_against_the_shared_fixture_corpus : Specification +{ + record FixtureCase(string Name, UiStarter Starter, Dictionary Themes, IReadOnlyList ExpectedIncompatible); + + List _cases = null!; + List<(FixtureCase Case, IReadOnlyList Incompatible)> _results = null!; + + void Establish() + { + var manifestPath = Path.Combine(FindRepositoryRoot(), "ui-starter-fixtures.json"); + using var document = JsonDocument.Parse(File.ReadAllText(manifestPath)); + + _cases = document.RootElement.GetProperty("themeCases").EnumerateArray().Select(ToFixtureCase).ToList(); + } + + void Because() => _results = [.. _cases.Select(fixtureCase => (fixtureCase, StarterThemeValidation.IncompatibleThemes(fixtureCase.Starter, fixtureCase.Themes)))]; + + [Fact] + void should_match_the_expected_incompatible_themes_for_every_case() + { + foreach (var (fixtureCase, incompatible) in _results) + { + (fixtureCase.Name, Flatten(incompatible)).ShouldEqual((fixtureCase.Name, Flatten(fixtureCase.ExpectedIncompatible))); + } + } + + static string Flatten(IReadOnlyList themes) => string.Join(',', themes); + + static FixtureCase ToFixtureCase(JsonElement element) + { + var name = element.GetProperty("name").GetString()!; + + var starterElement = element.GetProperty("starter"); + var starter = new UiStarter( + starterElement.GetProperty("name").GetString()!, + starterElement.GetProperty("packages").EnumerateArray().Select(value => value.GetString()!).ToList(), + starterElement.GetProperty("themes").EnumerateArray().Select(value => value.GetString()!).ToList(), + starterElement.GetProperty("gallery").EnumerateArray().Select(value => value.GetString()!).ToList()); + + var themes = element.GetProperty("themes").EnumerateObject() + .ToDictionary( + property => property.Name, + property => new Theme(property.Name, property.Value.GetProperty("compatibleWith").EnumerateArray().Select(value => value.GetString()!).ToList())); + + var expectedIncompatible = element.GetProperty("expectedIncompatible").EnumerateArray().Select(value => value.GetString()!).ToList(); + + return new(name, starter, themes, expectedIncompatible); + } + + static string FindRepositoryRoot() + { + var directory = new DirectoryInfo(AppContext.BaseDirectory); + while (directory is not null && !File.Exists(Path.Combine(directory.FullName, "Scene.slnx"))) + { + directory = directory.Parent; + } + + return directory?.FullName ?? throw new DirectoryNotFoundException("Could not locate the repository root (Scene.slnx) above " + AppContext.BaseDirectory); + } +} diff --git a/Source/DotNET/Engine/Starters/StarterProfileBuilder.cs b/Source/DotNET/Engine/Starters/StarterProfileBuilder.cs new file mode 100644 index 0000000..7491d86 --- /dev/null +++ b/Source/DotNET/Engine/Starters/StarterProfileBuilder.cs @@ -0,0 +1,27 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Cratis.Scene.Model.Profiles; +using Cratis.Scene.Model.SizeClasses; +using Cratis.Scene.Model.Starters; + +namespace Cratis.Scene.Engine.Starters; + +/// +/// Builds the sandboxed a 's gallery boots through - part of +/// Cratis/Scene#6. Studio runs a starter's gallery screens as a working mini-app rather than a simulated +/// preview, so the gallery needs a real profile scoped to exactly the starter's own package list, not the +/// consuming project's eventual profile. +/// +public static class StarterProfileBuilder +{ + /// + /// Builds the sandboxed for a 's gallery. + /// + /// The to build the profile for. + /// The platform the gallery runs on (e.g. web). + /// The size class assumed when the renderer cannot otherwise determine one. + /// A named after the starter, scoped to exactly its own . + public static UiProfile BuildProfile(UiStarter starter, string targetPlatform, SizeClass? defaultSizeClass = null) => + new(starter.Name, targetPlatform, starter.Packages, defaultSizeClass); +} diff --git a/Source/DotNET/Engine/Starters/StarterThemeValidation.cs b/Source/DotNET/Engine/Starters/StarterThemeValidation.cs new file mode 100644 index 0000000..b3e878d --- /dev/null +++ b/Source/DotNET/Engine/Starters/StarterThemeValidation.cs @@ -0,0 +1,33 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Cratis.Scene.Engine.Profiles; +using Cratis.Scene.Model.Profiles; +using Cratis.Scene.Model.Starters; + +namespace Cratis.Scene.Engine.Starters; + +/// +/// Checks a 's declared against its own +/// - part of Cratis/Scene#6. A starter is versioned per package +/// combination it targets, so a theme it ships as a choice that turns out incompatible with the starter's +/// own packages is exactly the gap already surfaces for a +/// ; this reuses that rule rather than reimplementing it for starters. +/// +public static class StarterThemeValidation +{ + /// + /// Finds the that are not compatible with the starter's own + /// . + /// + /// The to check. + /// Every known , keyed by name. + /// The names in that are either unknown to or incompatible with . + public static IReadOnlyList IncompatibleThemes(UiStarter starter, IReadOnlyDictionary themes) + { + var profile = new UiProfile(starter.Name, string.Empty, starter.Packages); + return starter.Themes + .Where(name => !themes.TryGetValue(name, out var theme) || !ThemeCompatibility.IsCompatible(theme, profile)) + .ToList(); + } +} diff --git a/ui-starter-fixtures.json b/ui-starter-fixtures.json new file mode 100644 index 0000000..dab12a8 --- /dev/null +++ b/ui-starter-fixtures.json @@ -0,0 +1,46 @@ +{ + "description": "Shared behavior corpus for Cratis.Scene.Engine.Starters (C#) and buildStarterProfile.ts/incompatibleStarterThemes.ts (TypeScript, @cratis/scene.engine) - both sides assert every case here independently, so the two implementations of Cratis/Scene#6's starter-profile and starter-theme rules cannot drift apart.", + "profileCases": [ + { + "name": "a starter's sandboxed profile is scoped to exactly its own packages", + "starter": { "name": "Admin", "packages": ["core", "PrimeReact"], "themes": ["Aurora"], "gallery": ["SampleDashboard"] }, + "targetPlatform": "web", + "expectedProfile": { "name": "Admin", "targetPlatform": "web", "packages": ["core", "PrimeReact"] } + }, + { + "name": "a starter with only core still produces a valid profile", + "starter": { "name": "Minimal", "packages": ["core"], "themes": [], "gallery": [] }, + "targetPlatform": "ios", + "expectedProfile": { "name": "Minimal", "targetPlatform": "ios", "packages": ["core"] } + } + ], + "themeCases": [ + { + "name": "every declared theme compatible with the starter's packages is not incompatible", + "starter": { "name": "Admin", "packages": ["core", "PrimeReact"], "themes": ["Aurora"], "gallery": [] }, + "themes": { "Aurora": { "compatibleWith": ["core", "PrimeReact"] } }, + "expectedIncompatible": [] + }, + { + "name": "a declared theme missing one of the starter's packages is incompatible", + "starter": { "name": "Admin", "packages": ["core", "PrimeReact"], "themes": ["Aurora"], "gallery": [] }, + "themes": { "Aurora": { "compatibleWith": ["core"] } }, + "expectedIncompatible": ["Aurora"] + }, + { + "name": "a starter theme not present in the theme catalog at all is incompatible", + "starter": { "name": "Admin", "packages": ["core"], "themes": ["Midnight"], "gallery": [] }, + "themes": { "Aurora": { "compatibleWith": ["core"] } }, + "expectedIncompatible": ["Midnight"] + }, + { + "name": "a starter with multiple themes reports only the incompatible ones", + "starter": { "name": "Admin", "packages": ["core", "PrimeReact"], "themes": ["Aurora", "Midnight"], "gallery": [] }, + "themes": { + "Aurora": { "compatibleWith": ["core", "PrimeReact"] }, + "Midnight": { "compatibleWith": ["core"] } + }, + "expectedIncompatible": ["Midnight"] + } + ] +} From 64fb0f669d8eb0536fb34da6b4f1c85f78932365 Mon Sep 17 00:00:00 2001 From: Einar Date: Sat, 15 Aug 2026 10:39:47 +0200 Subject: [PATCH 3/3] Add starter profile and theme resolution to @cratis/scene.engine TypeScript twin of the C# starter engine: buildStarterProfile mirrors StarterProfileBuilder, incompatibleStarterThemes mirrors StarterThemeValidation. Both are asserted against the same shared ui-starter-fixtures.json corpus as the C# implementation. --- .../JavaScript/engine/buildStarterProfile.ts | 21 ++++++++++++ ...cking_against_the_shared_fixture_corpus.ts | 32 +++++++++++++++++ ...cking_against_the_shared_fixture_corpus.ts | 34 +++++++++++++++++++ .../engine/incompatibleStarterThemes.ts | 24 +++++++++++++ Source/JavaScript/engine/index.ts | 2 ++ 5 files changed, 113 insertions(+) create mode 100644 Source/JavaScript/engine/buildStarterProfile.ts create mode 100644 Source/JavaScript/engine/for_buildStarterProfile/when_checking_against_the_shared_fixture_corpus.ts create mode 100644 Source/JavaScript/engine/for_incompatibleStarterThemes/when_checking_against_the_shared_fixture_corpus.ts create mode 100644 Source/JavaScript/engine/incompatibleStarterThemes.ts diff --git a/Source/JavaScript/engine/buildStarterProfile.ts b/Source/JavaScript/engine/buildStarterProfile.ts new file mode 100644 index 0000000..9e2d501 --- /dev/null +++ b/Source/JavaScript/engine/buildStarterProfile.ts @@ -0,0 +1,21 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +import { SizeClass, UiProfile, UiStarter } from '@cratis/scene.model'; + +/** + * Builds the sandboxed {@link UiProfile} a {@link UiStarter}'s gallery boots through - part of + * Cratis/Scene#6. Studio runs a starter's gallery screens as a working mini-app rather than a simulated + * preview, so the gallery needs a real profile scoped to exactly the starter's own package list, not the + * consuming project's eventual profile. Studio's design-time tooling and Stage's build-time resolution + * use the C# twin of this function in `Cratis.Scene.Engine`; both sides are asserted against the same + * shared fixture corpus so they cannot drift apart. + * + * @param starter The {@link UiStarter} to build the profile for. + * @param targetPlatform The platform the gallery runs on (e.g. `web`). + * @param defaultSizeClass The size class assumed when the renderer cannot otherwise determine one. + * @returns A {@link UiProfile} named after the starter, scoped to exactly its own `packages`. + */ +export function buildStarterProfile(starter: UiStarter, targetPlatform: string, defaultSizeClass?: SizeClass): UiProfile { + return { name: starter.name, targetPlatform, packages: starter.packages, defaultSizeClass }; +} diff --git a/Source/JavaScript/engine/for_buildStarterProfile/when_checking_against_the_shared_fixture_corpus.ts b/Source/JavaScript/engine/for_buildStarterProfile/when_checking_against_the_shared_fixture_corpus.ts new file mode 100644 index 0000000..9a1484f --- /dev/null +++ b/Source/JavaScript/engine/for_buildStarterProfile/when_checking_against_the_shared_fixture_corpus.ts @@ -0,0 +1,32 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +import { readFileSync } from 'node:fs'; +import { join } from 'node:path'; +import { UiStarter } from '@cratis/scene.model'; +import { buildStarterProfile } from '../index'; + +interface FixtureCase { + name: string; + starter: UiStarter; + targetPlatform: string; + expectedProfile: { name: string; targetPlatform: string; packages: string[] }; +} + +interface FixtureCorpus { + profileCases: FixtureCase[]; +} + +const manifestPath = join(import.meta.dirname, '..', '..', '..', '..', 'ui-starter-fixtures.json'); +const corpus = JSON.parse(readFileSync(manifestPath, 'utf-8')) as FixtureCorpus; + +describe('when checking against the shared fixture corpus', () => { + for (const fixtureCase of corpus.profileCases) { + it(`should build the expected profile for "${fixtureCase.name}"`, () => { + const profile = buildStarterProfile(fixtureCase.starter, fixtureCase.targetPlatform); + profile.name.should.equal(fixtureCase.expectedProfile.name); + profile.targetPlatform.should.equal(fixtureCase.expectedProfile.targetPlatform); + profile.packages.should.deep.equal(fixtureCase.expectedProfile.packages); + }); + } +}); diff --git a/Source/JavaScript/engine/for_incompatibleStarterThemes/when_checking_against_the_shared_fixture_corpus.ts b/Source/JavaScript/engine/for_incompatibleStarterThemes/when_checking_against_the_shared_fixture_corpus.ts new file mode 100644 index 0000000..f4ee1ce --- /dev/null +++ b/Source/JavaScript/engine/for_incompatibleStarterThemes/when_checking_against_the_shared_fixture_corpus.ts @@ -0,0 +1,34 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +import { readFileSync } from 'node:fs'; +import { join } from 'node:path'; +import { Theme, UiStarter } from '@cratis/scene.model'; +import { incompatibleStarterThemes } from '../index'; + +interface FixtureCase { + name: string; + starter: UiStarter; + themes: Record; + expectedIncompatible: string[]; +} + +interface FixtureCorpus { + themeCases: FixtureCase[]; +} + +const manifestPath = join(import.meta.dirname, '..', '..', '..', '..', 'ui-starter-fixtures.json'); +const corpus = JSON.parse(readFileSync(manifestPath, 'utf-8')) as FixtureCorpus; + +describe('when checking against the shared fixture corpus', () => { + for (const fixtureCase of corpus.themeCases) { + const themes: Record = {}; + for (const [themeName, theme] of Object.entries(fixtureCase.themes)) { + themes[themeName] = { name: themeName, compatibleWith: theme.compatibleWith }; + } + + it(`should match the expected incompatible themes for "${fixtureCase.name}"`, () => { + incompatibleStarterThemes(fixtureCase.starter, themes).should.deep.equal(fixtureCase.expectedIncompatible); + }); + } +}); diff --git a/Source/JavaScript/engine/incompatibleStarterThemes.ts b/Source/JavaScript/engine/incompatibleStarterThemes.ts new file mode 100644 index 0000000..4511fae --- /dev/null +++ b/Source/JavaScript/engine/incompatibleStarterThemes.ts @@ -0,0 +1,24 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +import { Theme, UiProfile, UiStarter } from '@cratis/scene.model'; +import { incompatiblePackages } from './themeCompatibility'; + +/** + * Finds the {@link UiStarter.themes} that are not compatible with the starter's own {@link UiStarter.packages} - + * part of Cratis/Scene#6. A starter is versioned per package combination it targets, so a theme it ships + * as a choice that turns out incompatible with the starter's own packages is exactly the gap + * `themeCompatibility` already surfaces for a {@link UiProfile}; this reuses that rule rather than + * reimplementing it for starters. + * + * @param starter The {@link UiStarter} to check. + * @param themes Every known {@link Theme}, keyed by name. + * @returns The names in `starter.themes` that are either unknown to `themes` or incompatible with `starter.packages`. + */ +export function incompatibleStarterThemes(starter: UiStarter, themes: Record): string[] { + const profile: UiProfile = { name: starter.name, targetPlatform: '', packages: starter.packages }; + return starter.themes.filter(name => { + const theme = themes[name]; + return !theme || incompatiblePackages(theme, profile).length > 0; + }); +} diff --git a/Source/JavaScript/engine/index.ts b/Source/JavaScript/engine/index.ts index e22f45d..0add27c 100644 --- a/Source/JavaScript/engine/index.ts +++ b/Source/JavaScript/engine/index.ts @@ -12,3 +12,5 @@ export * from './evaluateFlowArrangement'; export * from './evaluateFreeformArrangement'; export * from './aggregateContributions'; export * from './themeCompatibility'; +export * from './buildStarterProfile'; +export * from './incompatibleStarterThemes';