From ccad2eb6315da87f50b02ce58fc5dbe54625e648 Mon Sep 17 00:00:00 2001 From: Gumptious Date: Wed, 24 Jul 2019 11:41:52 -0700 Subject: [PATCH] feat(compiler): add support for plain TypeScript classes without decorators (#215) - Deprecate `extends Component` and `@property` semantics required for component authoring. Dangling references remain in `@diez/engine` for legacy projects created before the next release, but they are no longer functional. - Allow design token components to be built with plain TypeScript classes, punching `public`-scoped instance properties through to transpiled SDKs and leaving everything else out. We are now equipped to easily add support for typed hashmaps, but that should come in a subsequent PR. - Provide a new, more strongly typed generic factory for prefab base classes. It would have been lovely to do this with just an abstract base class and not a factory, but sadly, TypeScript has a few limitations with regard to typing classes whose constructors mutate the class type to conform to a generic interface whose type is not known statically at compile time. The new syntax is: ``` import {prefab} from '@diez/engine'; interface FooData { bar: string; } class Foo extends prefab() { defaults = { bar: 'bar', }; } ``` With the power of abstract classes, generics, and ES6 proxies, `new Foo()` and `new Foo({bar: string})` satisfy `Prefab & FooData` automatically and without the need to carefully register data properties matching the types of the state shape. - Migrate example projects and documentation to use/refer to this new syntax. --- .travis.yml | 1 - examples/lorem-ipsum/src/DesignSystem.ts | 75 ++--- examples/lorem-ipsum/src/components/Margin.ts | 18 +- examples/poodle-surf/src/DesignSystem.ts | 17 +- examples/poodle-surf/src/ModelMocks.ts | 5 +- .../poodle-surf/src/designs/LoadingDesign.ts | 7 +- .../src/designs/NavigationTitleDesign.ts | 13 +- .../src/designs/PoodleSurf.sketch.ts | 16 +- .../poodle-surf/src/designs/ReportDesign.ts | 148 ++++---- .../src/designs/components/EdgeInsets.ts | 16 +- examples/poodle-surf/src/designs/constants.ts | 35 +- .../poodle-surf/src/mocks/ReportModelMock.ts | 72 ++-- .../web/docs/getting-started/css-sass.md | 14 +- .../web/docs/getting-started/figma.md | 4 +- .../web/docs/getting-started/javascript.md | 12 +- .../web/docs/getting-started/kotlin.md | 14 +- .../web/docs/getting-started/swift.md | 14 +- .../web/docs/getting-started/the-basics.md | 20 +- examples/site/src/DesignSystem.ts | 13 +- examples/site/src/constants.ts | 99 +++--- packages/compiler/README.md | 7 +- packages/compiler/src/api.ts | 18 +- packages/compiler/src/compiler.ts | 73 ++-- packages/compiler/src/server/hot-component.ts | 21 +- packages/compiler/src/utils.ts | 9 +- .../test/fixtures/Bindings/Bindings.ts | 12 +- .../test/fixtures/Filtered/Filtered.ts | 23 +- .../compiler/test/fixtures/Valid/Valid.ts | 54 +-- packages/compiler/test/target.test.ts | 6 - .../templates/project/src/DesignSystem.ts | 5 +- packages/engine/README.md | 5 +- packages/engine/src/api.ts | 135 +------- packages/engine/src/component.ts | 308 ----------------- packages/engine/src/decorators.ts | 85 ----- packages/engine/src/expression.ts | 103 ------ packages/engine/src/index.ts | 6 +- packages/engine/src/legacy.ts | 19 ++ packages/engine/src/prefab.ts | 94 ++++++ packages/engine/src/serialization.ts | 35 +- packages/engine/src/transitions.ts | 32 -- packages/engine/test/component.test.ts | 317 ------------------ packages/engine/test/prefab.test.ts | 50 +++ packages/engine/test/serialization.test.ts | 14 +- packages/engine/test/transitions.test.ts | 27 -- packages/engine/test/tweens.test.ts | 129 ------- packages/generation/src/linear-gradient.ts | 8 +- packages/generation/src/utils.ts | 19 -- .../test/goldens/codegennable/src/index.ts | 18 +- packages/prefabs/src/color.ts | 55 ++- packages/prefabs/src/file.ts | 27 +- packages/prefabs/src/image.ts | 49 ++- packages/prefabs/src/linear-gradient.ts | 84 ++--- packages/prefabs/src/lottie.ts | 17 +- packages/prefabs/src/point2d.ts | 29 +- packages/prefabs/src/typography.ts | 71 ++-- packages/prefabs/test/linear-gradient.test.ts | 4 +- packages/prefabs/test/lottie.test.ts | 1 - packages/prefabs/test/typography.test.ts | 27 +- packages/targets/src/asset-binders/file.ts | 3 +- packages/targets/src/targets/android.api.ts | 4 +- packages/targets/src/targets/ios.api.ts | 4 +- packages/targets/src/targets/web.api.ts | 4 +- packages/targets/src/utils.ts | 4 +- .../test/fixtures/Bindings/Bindings.ts | 13 +- .../test/fixtures/Primitives/Primitives.ts | 34 +- packages/targets/test/helpers.ts | 6 +- .../web-sdk-common/src/css-linear-gradient.ts | 26 +- utils/diez-webpack-plugin/src/index.ts | 2 +- 68 files changed, 872 insertions(+), 1837 deletions(-) delete mode 100644 packages/engine/src/component.ts delete mode 100644 packages/engine/src/decorators.ts delete mode 100644 packages/engine/src/expression.ts create mode 100644 packages/engine/src/legacy.ts create mode 100644 packages/engine/src/prefab.ts delete mode 100644 packages/engine/src/transitions.ts delete mode 100644 packages/engine/test/component.test.ts create mode 100644 packages/engine/test/prefab.test.ts delete mode 100644 packages/engine/test/transitions.test.ts delete mode 100644 packages/engine/test/tweens.test.ts diff --git a/.travis.yml b/.travis.yml index 4043a9020..4c0ff9066 100644 --- a/.travis.yml +++ b/.travis.yml @@ -44,7 +44,6 @@ before_install: - nvm install 10.15.3 - nvm alias default 10.15.3 - curl -o- -L https://yarnpkg.com/install.sh | bash -s -- --version 1.16.0 -- nvm alias default 10.15.3 - nvm use 10.15.3 - export PATH="$HOME/.yarn/bin:$PATH" - gem install cocoapods -v '1.7.4' diff --git a/examples/lorem-ipsum/src/DesignSystem.ts b/examples/lorem-ipsum/src/DesignSystem.ts index bcb6ba575..5e68e45fb 100644 --- a/examples/lorem-ipsum/src/DesignSystem.ts +++ b/examples/lorem-ipsum/src/DesignSystem.ts @@ -1,20 +1,19 @@ import {Color, Image, Lottie, Toward, Typograph, Font, LinearGradient} from '@diez/prefabs'; -import {Component, property} from '@diez/engine'; import {Margin} from './components/Margin'; /** - * You can collect anything inside a Diez component. Design tokens labeled with - * @property will be made available in the SDKs transpiled with Diez. Everything + * You can collect anything inside a Diez component. Design tokens specified as public + * properties will be made available in the SDKs transpiled with Diez. Everything * else is purely internal. */ -class Colors extends Component { +class Colors { private lightener = 0.2; - @property white = Color.hex('#FFFFFF'); - @property black = Color.hex('#000010'); - @property purple = Color.rgb(86, 35, 238); - @property darkPurple = Color.rgb(22, 11, 54); - @property lightPurple = this.purple.lighten(this.lightener); + white = Color.hex('#FFFFFF'); + black = Color.hex('#000010'); + purple = Color.rgb(86, 35, 238); + darkPurple = Color.rgb(22, 11, 54); + lightPurple = this.purple.lighten(this.lightener); } /** @@ -29,12 +28,12 @@ const colors = new Colors(); /** * You can reference properties from other components. */ -class Palette extends Component { - @property background = colors.black; - @property contentBackground = colors.white; - @property text = colors.black; - @property caption = colors.purple; - @property headerBackground = LinearGradient.make(Toward.Bottom, colors.darkPurple, colors.black); +class Palette { + background = colors.black; + contentBackground = colors.white; + text = colors.black; + caption = colors.purple; + headerBackground = LinearGradient.make(Toward.Bottom, colors.darkPurple, colors.black); } const palette = new Palette(); @@ -53,20 +52,20 @@ const Fonts = { * Typographs encapsulate type styles with support for a specific font, font size, * and color. More typograph properties are coming soon. */ -class Typography extends Component { - @property heading1 = new Typograph({ +class Typography { + heading1 = new Typograph({ font: Fonts.SourceSansPro.Regular, fontSize: 24, color: palette.text, }); - @property body = new Typograph({ + body = new Typograph({ font: Fonts.SourceSansPro.Regular, fontSize: 18, color: palette.text, }); - @property caption = new Typograph({ + caption = new Typograph({ font: Fonts.SourceSansPro.Regular, fontSize: 14, color: palette.caption, @@ -78,19 +77,19 @@ class Typography extends Component { * design system primitives in components as well — such as images, icons & * animations. */ -class Images extends Component { - @property logo = Image.responsive('assets/logo.png', 52, 48); - @property masthead = Image.responsive('assets/masthead.png', 208, 88); +class Images { + logo = Image.responsive('assets/logo.png', 52, 48); + masthead = Image.responsive('assets/masthead.png', 208, 88); } /** * You can even collect your own custom components. */ -class LayoutValues extends Component { - @property spacingSmall = 5; - @property spacingMedium = 25; - @property spacingLarge = 40; - @property contentMargin = new Margin({ +class LayoutValues { + spacingSmall = 5; + spacingMedium = 25; + spacingLarge = 40; + contentMargin = new Margin({ top: this.spacingLarge, left: this.spacingMedium, right: this.spacingMedium, @@ -101,10 +100,10 @@ class LayoutValues extends Component { /** * You can also define strings. */ -class Strings extends Component { - @property title = 'Diez'; - @property caption = 'Keep your designs in sync with code'; - @property helper = 'Modify the contents of “src/DesignSystem.ts” (relative to the root of the Diez project) to see changes to the design system in real time.'; +class Strings { + title = 'Diez'; + caption = 'Keep your designs in sync with code'; + helper = 'Modify the contents of “src/DesignSystem.ts” (relative to the root of the Diez project) to see changes to the design system in real time.'; } /** @@ -122,11 +121,11 @@ class Strings extends Component { * Look for `MainActivity.kt` inside `examples/android` to see how you can * use Diez in an Android codebase. */ -export class DesignSystem extends Component { - @property palette = palette; - @property typography = new Typography(); - @property images = new Images(); - @property layoutValues = new LayoutValues(); - @property strings = new Strings(); - @property loadingAnimation = Lottie.fromJson('assets/loadingAnimation.json', false); +export class DesignSystem { + palette = palette; + typography = new Typography(); + images = new Images(); + layoutValues = new LayoutValues(); + strings = new Strings(); + loadingAnimation = Lottie.fromJson('assets/loadingAnimation.json', false); } diff --git a/examples/lorem-ipsum/src/components/Margin.ts b/examples/lorem-ipsum/src/components/Margin.ts index 5fd783604..9afacfc64 100644 --- a/examples/lorem-ipsum/src/components/Margin.ts +++ b/examples/lorem-ipsum/src/components/Margin.ts @@ -1,10 +1,10 @@ -import {Component, property} from '@diez/engine'; +import {prefab} from '@diez/engine'; /** - * Defining the interface of your component's state enables you to instantiate your own + * Defining the interface of your component's data enables you to instantiate your own * reusable components. */ -interface MarginState { +interface MarginData { top: number; bottom: number; left: number; @@ -14,11 +14,13 @@ interface MarginState { /** * Here we create a custom reusable component for describing layout margins. */ -export class Margin extends Component { - @property top = 0; - @property bottom = 0; - @property left = 0; - @property right = 0; +export class Margin extends prefab() { + defaults = { + top: 0, + bottom: 0, + left: 0, + right: 0, + }; /** * Let's add in a helper method for defining margins (inspired by CSS shorthand). diff --git a/examples/poodle-surf/src/DesignSystem.ts b/examples/poodle-surf/src/DesignSystem.ts index fcfe9bae8..a10f1446b 100644 --- a/examples/poodle-surf/src/DesignSystem.ts +++ b/examples/poodle-surf/src/DesignSystem.ts @@ -1,17 +1,16 @@ -import {Component, property} from '@diez/engine'; import {LoadingDesign, NavigationTitleDesign, palette, ReportDesign, typographs} from './designs'; -class Designs extends Component { - @property report = new ReportDesign(); - @property loading = new LoadingDesign(); - @property navigationTitle = new NavigationTitleDesign(); +class Designs { + report = new ReportDesign(); + loading = new LoadingDesign(); + navigationTitle = new NavigationTitleDesign(); } /** * The design system for Poodle Surf. */ -export class DesignSystem extends Component { - @property palette = palette; - @property typographs = typographs; - @property designs = new Designs(); +export class DesignSystem { + palette = palette; + typographs = typographs; + designs = new Designs(); } diff --git a/examples/poodle-surf/src/ModelMocks.ts b/examples/poodle-surf/src/ModelMocks.ts index 129f6a620..ef7ce486a 100644 --- a/examples/poodle-surf/src/ModelMocks.ts +++ b/examples/poodle-surf/src/ModelMocks.ts @@ -1,9 +1,8 @@ -import {Component, property} from '@diez/engine'; import {ReportModelMock} from './mocks'; /** * The model mocks for Poodle Surf. */ -export class ModelMocks extends Component { - @property report = new ReportModelMock(); +export class ModelMocks { + report = new ReportModelMock(); } diff --git a/examples/poodle-surf/src/designs/LoadingDesign.ts b/examples/poodle-surf/src/designs/LoadingDesign.ts index 49f267efa..b85cdeb10 100644 --- a/examples/poodle-surf/src/designs/LoadingDesign.ts +++ b/examples/poodle-surf/src/designs/LoadingDesign.ts @@ -1,5 +1,4 @@ import {Lottie} from '@diez/prefabs'; -import {Component, property} from '@diez/engine'; import {palette} from './constants'; enum LottieJsons { @@ -11,7 +10,7 @@ enum LottieJsons { /** * The loading design. */ -export class LoadingDesign extends Component { - @property backgroundColor = palette.loadingBackground; - @property animation = Lottie.fromJson(LottieJsons.PoodleSurf); +export class LoadingDesign { + backgroundColor = palette.loadingBackground; + animation = Lottie.fromJson(LottieJsons.PoodleSurf); } diff --git a/examples/poodle-surf/src/designs/NavigationTitleDesign.ts b/examples/poodle-surf/src/designs/NavigationTitleDesign.ts index 8ca273ec0..dccc77ed9 100644 --- a/examples/poodle-surf/src/designs/NavigationTitleDesign.ts +++ b/examples/poodle-surf/src/designs/NavigationTitleDesign.ts @@ -1,14 +1,13 @@ import {PoodleSurfSlices} from './PoodleSurf.sketch'; -import {Component, property} from '@diez/engine'; import {LayoutValues, palette, typographs} from './constants'; /** * The navigation title design. */ -export class NavigationTitleDesign extends Component { - @property barTintColor = palette.background; - @property icon = PoodleSurfSlices.Icon; - @property title = 'P o o d l e S u r f'; - @property typograph = typographs.headerTitle; - @property iconToTitleSpacing = LayoutValues.DefaultSpacing; +export class NavigationTitleDesign { + barTintColor = palette.background; + icon = PoodleSurfSlices.Icon; + title = 'P o o d l e S u r f'; + typograph = typographs.headerTitle; + iconToTitleSpacing = LayoutValues.DefaultSpacing; } diff --git a/examples/poodle-surf/src/designs/PoodleSurf.sketch.ts b/examples/poodle-surf/src/designs/PoodleSurf.sketch.ts index 4630b0aa5..6b57ea277 100644 --- a/examples/poodle-surf/src/designs/PoodleSurf.sketch.ts +++ b/examples/poodle-surf/src/designs/PoodleSurf.sketch.ts @@ -1,23 +1,15 @@ import { Color, File, GradientStop, Image, LinearGradient, Point2D } from "@diez/prefabs"; -import { Component, property } from "@diez/engine"; -class PoodleSurfColors extends Component { - @property +class PoodleSurfColors { pink = Color.rgba(255, 63, 112, 1); - @property orange = Color.rgba(255, 154, 58, 1); - @property blue = Color.rgba(120, 207, 253, 1); - @property white = Color.rgba(255, 255, 255, 1); - @property whiteA40 = Color.rgba(255, 255, 255, 0.4); - @property black = Color.rgba(0, 0, 0, 1); } -class PoodleSurfGradients extends Component { - @property +class PoodleSurfGradients { gradient = new LinearGradient({stops: [GradientStop.make(0.000000, Color.rgba(255, 63, 112, 1)), GradientStop.make(1.000000, Color.rgba(255, 154, 58, 1))], start: Point2D.make(0.256905, -0.052988), end: Point2D.make(0.912005, 1.039424)}); } @@ -72,10 +64,8 @@ export class PoodleSurfSlices { static Icon = Image.responsive("assets/PoodleSurf.sketch.contents/slices/Icon.png", 29, 26); } -export class PoodleSurfTokens extends Component { - @property +export class PoodleSurfTokens { colors = new PoodleSurfColors(); - @property gradients = new PoodleSurfGradients(); } diff --git a/examples/poodle-surf/src/designs/ReportDesign.ts b/examples/poodle-surf/src/designs/ReportDesign.ts index b74712f96..ecfc8b8d2 100644 --- a/examples/poodle-surf/src/designs/ReportDesign.ts +++ b/examples/poodle-surf/src/designs/ReportDesign.ts @@ -1,30 +1,30 @@ -import {Typograph, LinearGradient} from '@diez/prefabs'; -import {Component, property} from '@diez/engine'; +import {Typograph, LinearGradient, Color} from '@diez/prefabs'; +import {prefab} from '@diez/engine'; import {PoodleSurfSlices} from './PoodleSurf.sketch'; import {EdgeInsets} from './components/EdgeInsets'; import {LayoutValues, palette, typographs} from './constants'; -class LocationImageDesign extends Component { - @property strokeWidth = 3; - @property strokeGradient = palette.contentBackground; - @property widthAndHeight = 106; +class LocationImageDesign { + strokeWidth = 3; + strokeGradient = palette.contentBackground; + widthAndHeight = 106; } -class HeaderDesign extends Component { - @property regionLabel = typographs.headerTitle; - @property placeLabel = typographs.headerCaption; - @property mapPinIcon = PoodleSurfSlices.MapPin; - @property locationImage = new LocationImageDesign(); - @property bannerHeight = 149; - @property labelsLayoutMargin = EdgeInsets.simple( +class HeaderDesign { + regionLabel = typographs.headerTitle; + placeLabel = typographs.headerCaption; + mapPinIcon = PoodleSurfSlices.MapPin; + locationImage = new LocationImageDesign(); + bannerHeight = 149; + labelsLayoutMargin = EdgeInsets.simple( LayoutValues.CompactMargin, LayoutValues.DefaultMargin, ); - @property pinIconToLabelSpacing = LayoutValues.DefaultSpacing; - @property labelsSpacing = LayoutValues.CompactSpacing; + pinIconToLabelSpacing = LayoutValues.DefaultSpacing; + labelsSpacing = LayoutValues.CompactSpacing; } -interface SharedCardDesignState { +interface SharedCardDesignData { title: string; titleTypograph: Typograph; titleContentSpacing: number; @@ -33,88 +33,92 @@ interface SharedCardDesignState { cornerRadius: number; } -class SharedCardDesign extends Component { - @property title = ''; - @property titleTypograph = typographs.cardTitle; - @property titleContentSpacing = LayoutValues.DefaultMargin; - @property gradient = palette.contentBackground; - @property layoutMargins = new EdgeInsets({ - top: LayoutValues.DefaultMargin, - bottom: LayoutValues.LooseMargin, - left: LayoutValues.DefaultMargin, - right: LayoutValues.DefaultMargin, - }); - @property cornerRadius = 5; +class SharedCardDesign extends prefab() { + defaults = { + title: '', + titleTypograph: typographs.cardTitle, + titleContentSpacing: LayoutValues.DefaultMargin, + gradient: palette.contentBackground, + layoutMargins: new EdgeInsets({ + top: LayoutValues.DefaultMargin, + bottom: LayoutValues.LooseMargin, + left: LayoutValues.DefaultMargin, + right: LayoutValues.DefaultMargin, + }), + cornerRadius: 5, + }; } -class TemperatureDesign extends Component { - @property typograph = typographs.value; - @property icon = PoodleSurfSlices.Thermometer; - @property iconSpacing = LayoutValues.DefaultSpacing; +class TemperatureDesign { + typograph = typographs.value; + icon = PoodleSurfSlices.Thermometer; + iconSpacing = LayoutValues.DefaultSpacing; } -class WetsuitDesign extends Component { - @property headerText = 'Recommended'; - @property headerTypograph = typographs.captionHeader; - @property valueTypograph = typographs.caption; - @property labelSpacing = LayoutValues.CompactSpacing; - @property iconSpacing = LayoutValues.DefaultSpacing; - @property icon = PoodleSurfSlices.Gear; +class WetsuitDesign { + headerText = 'Recommended'; + headerTypograph = typographs.captionHeader; + valueTypograph = typographs.caption; + labelSpacing = LayoutValues.CompactSpacing; + iconSpacing = LayoutValues.DefaultSpacing; + icon = PoodleSurfSlices.Gear; } -class WaterTemperatureCardDesign extends Component { - @property shared = new SharedCardDesign({ +class WaterTemperatureCardDesign { + shared = new SharedCardDesign({ title: 'Water temperature', }); - @property horizontalSpacing = LayoutValues.DefaultMargin; - @property temperature = new TemperatureDesign(); - @property wetsuit = new WetsuitDesign(); + horizontalSpacing = LayoutValues.DefaultMargin; + temperature = new TemperatureDesign(); + wetsuit = new WetsuitDesign(); } const DayPartIconSize = 78; -class DayPartDesign extends Component { - @property valueTypograph = typographs.value; - @property unitTypograph = typographs.unit; - @property timeTypograph = typographs.caption; - @property valueUnitSpacing = LayoutValues.CompactSpacing; - @property layoutMargins = new EdgeInsets(); - @property iconWidth = DayPartIconSize; - @property iconHeight = DayPartIconSize; +class DayPartDesign { + valueTypograph = typographs.value; + unitTypograph = typographs.unit; + timeTypograph = typographs.caption; + valueUnitSpacing = LayoutValues.CompactSpacing; + layoutMargins = new EdgeInsets(); + iconWidth = DayPartIconSize; + iconHeight = DayPartIconSize; } -interface ForecastCardDesignState { +interface ForecastCardDesignData { shared: SharedCardDesign; dayPart: DayPartDesign; unit: string; dayPartsHorizontalSpacing: number; dayPartVerticalSpacing: number; separatorWidth: number; - separatorColor: number; + separatorColor: Color; valueUnitMargins: EdgeInsets; } -class ForecastCardDesign extends Component { - @property shared = new SharedCardDesign(); - @property unit = ''; - @property dayPart = new DayPartDesign(); - @property dayPartsHorizontalSpacing = LayoutValues.DefaultMargin; - @property dayPartVerticalSpacing = LayoutValues.LooseMargin; - @property separatorWidth = 1; - @property separatorColor = palette.separator; - @property valueUnitMargins = new EdgeInsets(); +class ForecastCardDesign extends prefab() { + defaults = { + shared: new SharedCardDesign(), + dayPart: new DayPartDesign(), + unit: '', + dayPartsHorizontalSpacing: LayoutValues.DefaultMargin, + dayPartVerticalSpacing: LayoutValues.LooseMargin, + separatorWidth: 1, + separatorColor: palette.separator, + valueUnitMargins: new EdgeInsets(), + }; } /** * The report design. */ -export class ReportDesign extends Component { - @property backgroundColor = palette.background; - @property contentLayoutMargins = EdgeInsets.simple(LayoutValues.DefaultMargin); - @property contentSpacing = LayoutValues.DefaultMargin; - @property header = new HeaderDesign(); - @property waterTemperature = new WaterTemperatureCardDesign(); - @property wind = new ForecastCardDesign({ +export class ReportDesign { + backgroundColor = palette.background; + contentLayoutMargins = EdgeInsets.simple(LayoutValues.DefaultMargin); + contentSpacing = LayoutValues.DefaultMargin; + header = new HeaderDesign(); + waterTemperature = new WaterTemperatureCardDesign(); + wind = new ForecastCardDesign({ shared: new SharedCardDesign({ title: 'Wind', }), @@ -124,13 +128,13 @@ export class ReportDesign extends Component { top: LayoutValues.DefaultMargin, }), }); - @property swell = new ForecastCardDesign({ + swell = new ForecastCardDesign({ shared: new SharedCardDesign({ title: 'Swell', }), unit: 'ft', }); - @property tide = new ForecastCardDesign({ + tide = new ForecastCardDesign({ shared: new SharedCardDesign({ title: 'Tide', }), diff --git a/examples/poodle-surf/src/designs/components/EdgeInsets.ts b/examples/poodle-surf/src/designs/components/EdgeInsets.ts index a25d2e5d1..236196ca4 100644 --- a/examples/poodle-surf/src/designs/components/EdgeInsets.ts +++ b/examples/poodle-surf/src/designs/components/EdgeInsets.ts @@ -1,6 +1,6 @@ -import {Component, property} from '@diez/engine'; +import {prefab} from '@diez/engine'; -interface EdgeInsetsState { +interface EdgeInsetsData { top: number; bottom: number; left: number; @@ -10,11 +10,13 @@ interface EdgeInsetsState { /** * Provides inset to be used for layout margins, etc. */ -export class EdgeInsets extends Component { - @property top = 0; - @property bottom = 0; - @property left = 0; - @property right = 0; +export class EdgeInsets extends prefab() { + defaults = { + top: 0, + bottom: 0, + left: 0, + right: 0, + }; /** * A helper method for defining edge insets inspired by CSS shorthand. diff --git a/examples/poodle-surf/src/designs/constants.ts b/examples/poodle-surf/src/designs/constants.ts index 6b513068b..957f5e539 100644 --- a/examples/poodle-surf/src/designs/constants.ts +++ b/examples/poodle-surf/src/designs/constants.ts @@ -1,16 +1,15 @@ import {Font, LinearGradient, Toward, Typograph} from '@diez/prefabs'; -import {Component, property} from '@diez/engine'; import {poodleSurfTokens} from './PoodleSurf.sketch'; -class Palette extends Component { - @property foreground = poodleSurfTokens.colors.black; - @property background = poodleSurfTokens.colors.white; - @property loadingBackground = poodleSurfTokens.colors.blue; - @property primary = poodleSurfTokens.colors.pink; - @property secondary = poodleSurfTokens.colors.orange; - @property separator = poodleSurfTokens.colors.whiteA40; - @property contentForeground = poodleSurfTokens.colors.white; - @property contentBackground = LinearGradient.make(Toward.BottomRight, this.primary, this.secondary); +class Palette { + foreground = poodleSurfTokens.colors.black; + background = poodleSurfTokens.colors.white; + loadingBackground = poodleSurfTokens.colors.blue; + primary = poodleSurfTokens.colors.pink; + secondary = poodleSurfTokens.colors.orange; + separator = poodleSurfTokens.colors.whiteA40; + contentForeground = poodleSurfTokens.colors.white; + contentBackground = LinearGradient.make(Toward.BottomRight, this.primary, this.secondary); } /** @@ -42,38 +41,38 @@ enum FontSizes { Unit = 16, } -class Typographs extends Component { - @property headerTitle = new Typograph({ +class Typographs { + headerTitle = new Typograph({ font: Fonts.Nunito.Bold, fontSize: FontSizes.Title, color: palette.foreground, }); - @property headerCaption = new Typograph({ + headerCaption = new Typograph({ font: Fonts.Nunito.Regular, fontSize: FontSizes.Caption, color: palette.foreground, }); - @property cardTitle = new Typograph({ + cardTitle = new Typograph({ font: Fonts.Nunito.Regular, fontSize: FontSizes.CardTitle, color: palette.contentForeground, }); - @property value = new Typograph({ + value = new Typograph({ font: Fonts.Nunito.Regular, fontSize: FontSizes.Value, color: palette.contentForeground, }); - @property unit = new Typograph({ + unit = new Typograph({ font: Fonts.Nunito.Regular, fontSize: FontSizes.Unit, color: palette.contentForeground, }); - @property caption = new Typograph({ + caption = new Typograph({ font: Fonts.Nunito.Regular, fontSize: FontSizes.Caption, color: palette.contentForeground, }); - @property captionHeader = new Typograph({ + captionHeader = new Typograph({ font: Fonts.Nunito.Bold, fontSize: FontSizes.Caption, color: palette.contentForeground, diff --git a/examples/poodle-surf/src/mocks/ReportModelMock.ts b/examples/poodle-surf/src/mocks/ReportModelMock.ts index efb0cb3b4..647a7f8b9 100644 --- a/examples/poodle-surf/src/mocks/ReportModelMock.ts +++ b/examples/poodle-surf/src/mocks/ReportModelMock.ts @@ -1,80 +1,86 @@ import {File} from '@diez/prefabs'; -import {Component, property} from '@diez/engine'; +import {prefab} from '@diez/engine'; import {DayPartTimes} from './constants'; import {PoodleSurfSlicesFiles} from '../designs/PoodleSurf.sketch'; -class LocationMock extends Component { - @property region = 'Santa Cruz, CA'; - @property place = 'Natural Bridges State Park'; - @property mapImage = PoodleSurfSlicesFiles.SantaCruzMap3x; - @property bannerImage = PoodleSurfSlicesFiles.SantaCruzBanner3x; +class LocationMock { + region = 'Santa Cruz, CA'; + place = 'Natural Bridges State Park'; + mapImage = PoodleSurfSlicesFiles.SantaCruzMap3x; + bannerImage = PoodleSurfSlicesFiles.SantaCruzBanner3x; } -class TemperatureMock extends Component { - @property value = '55°F'; - @property gear = '4mm Wetsuit'; +class TemperatureMock { + value = '55°F'; + gear = '4mm Wetsuit'; } -interface WindDayPartMockState { +interface WindDayPartMockData { direction: File; value: string; dayPart: string; } -class WindDayPartMock extends Component { - @property direction = PoodleSurfSlicesFiles.DirectionNorthEast3x; - @property value = ''; - @property dayPart = ''; +class WindDayPartMock extends prefab() { + defaults = { + direction: PoodleSurfSlicesFiles.DirectionNorthEast3x, + value: '', + dayPart: '', + }; } -class WindMock extends Component { - @property early = new WindDayPartMock({ +class WindMock { + early = new WindDayPartMock({ direction: PoodleSurfSlicesFiles.DirectionSouthWest3x, value: '4', dayPart: DayPartTimes.Early, }); - @property middle = new WindDayPartMock({ + middle = new WindDayPartMock({ direction: PoodleSurfSlicesFiles.DirectionSouth3x, value: '12', dayPart: DayPartTimes.Middle, }); - @property late = new WindDayPartMock({ + late = new WindDayPartMock({ direction: PoodleSurfSlicesFiles.DirectionNorthEast3x, value: '17', dayPart: DayPartTimes.Late, }); } -interface ForecastDayPartMockState { +interface ForecastDayPartMockData { value: string; dayPart: string; } -class ForecastDayPartMock extends Component { - @property value = ''; - @property dayPart = ''; +class ForecastDayPartMock extends prefab() { + defaults = { + value: '', + dayPart: '', + }; } -interface ForecastMockState { +interface ForecastMockData { early: ForecastDayPartMock; middle: ForecastDayPartMock; late: ForecastDayPartMock; } -class ForecastMock extends Component { - @property early = new ForecastDayPartMock(); - @property middle = new ForecastDayPartMock(); - @property late = new ForecastDayPartMock(); +class ForecastMock extends prefab() { + defaults = { + early: new ForecastDayPartMock(), + middle: new ForecastDayPartMock(), + late: new ForecastDayPartMock(), + }; } /** * A mock API report object. */ -export class ReportModelMock extends Component { - @property location = new LocationMock(); - @property temperature = new TemperatureMock(); - @property wind = new WindMock(); - @property swell = new ForecastMock({ +export class ReportModelMock { + location = new LocationMock(); + temperature = new TemperatureMock(); + wind = new WindMock(); + swell = new ForecastMock({ early: new ForecastDayPartMock({ value: '6.3', dayPart: DayPartTimes.Early, @@ -88,7 +94,7 @@ export class ReportModelMock extends Component { dayPart: DayPartTimes.Late, }), }); - @property tide = new ForecastMock({ + tide = new ForecastMock({ early: new ForecastDayPartMock({ value: '5', dayPart: DayPartTimes.Early, diff --git a/examples/site/examples/web/docs/getting-started/css-sass.md b/examples/site/examples/web/docs/getting-started/css-sass.md index 50d7cda2d..ff9e22bff 100644 --- a/examples/site/examples/web/docs/getting-started/css-sass.md +++ b/examples/site/examples/web/docs/getting-started/css-sass.md @@ -45,11 +45,11 @@ For example, you can change the background color of the web app by modifying you First, open `src/DesignSystem.ts` in an editor of your choice. Look for the following block of code: ```typescript -class Colors extends Component { - @property lightBackground = palette.white; - @property darkBackground = palette.black; - @property text = palette.black; - @property caption = palette.purple; +class Colors { + lightBackground = palette.white; + darkBackground = palette.black; + text = palette.black; + caption = palette.purple; } ``` @@ -58,8 +58,8 @@ In this example, the `Colors`component maps semantic names to the `Palette` comp Change `lightBackground` to `palette.lightPurple` like so: ```Diff -- @property lightBackground = palette.white; -+ @property lightBackground = palette.lightPurple; +- lightBackground = palette.white; ++ lightBackground = palette.lightPurple; ``` Go back to your browser and see the web app hot update! You can update and hot reload **any** value defined in your design system: strings, colors, images, fonts, etc. diff --git a/examples/site/examples/web/docs/getting-started/figma.md b/examples/site/examples/web/docs/getting-started/figma.md index 7c6caabb8..89692c691 100644 --- a/examples/site/examples/web/docs/getting-started/figma.md +++ b/examples/site/examples/web/docs/getting-started/figma.md @@ -74,8 +74,8 @@ import { yourFigmaProjNameTokens } from './designs/YourFigmaProjName.figma'; Then use it as you see fit. As shown here, we've used the Color Style from Figma named `fuss` and set it as the 'lightBackground' color of our design system. ```typescript -class Palette extends Component { - @property lightBackground = yourFigmaProjNameTokens.palette.fuss +class Palette { + lightBackground = yourFigmaProjNameTokens.palette.fuss } ``` diff --git a/examples/site/examples/web/docs/getting-started/javascript.md b/examples/site/examples/web/docs/getting-started/javascript.md index ce54fec06..1af4728bd 100644 --- a/examples/site/examples/web/docs/getting-started/javascript.md +++ b/examples/site/examples/web/docs/getting-started/javascript.md @@ -42,10 +42,10 @@ For example, you can change the background color of the web app by modifying you First, open `src/DesignSystem.ts` in an editor of your choice. Look for the following block of code: ```typescript -class Strings extends Component { - @property title = 'Diez'; - @property caption = 'Keep your designs in sync with code'; - @property helper = 'Modify the contents of “src/DesignSystem.ts” (relative to the root of the Diez project) to see changes to the design system in real time.'; +class Strings { + title = 'Diez'; + caption = 'Keep your designs in sync with code'; + helper = 'Modify the contents of “src/DesignSystem.ts” (relative to the root of the Diez project) to see changes to the design system in real time.'; } ``` @@ -54,8 +54,8 @@ In this example, the `String`component maps semantic names to strings that are d Change the contents of `title` to something of your choice, for example: ```Diff -- @property title = 'Diez'; -+ @property title = 'I <3 Diez!'; +- title = 'Diez'; ++ title = 'I <3 Diez!'; ``` Go back to your browser and see the web app hot update! You can update and hot reload **any** value defined in your design system: strings, colors, images, fonts, etc. diff --git a/examples/site/examples/web/docs/getting-started/kotlin.md b/examples/site/examples/web/docs/getting-started/kotlin.md index 2d51ee977..f428391b7 100644 --- a/examples/site/examples/web/docs/getting-started/kotlin.md +++ b/examples/site/examples/web/docs/getting-started/kotlin.md @@ -28,11 +28,11 @@ Let's change the background color of our application by modifying our design sys Open `src/DesignSystem.ts`, in an editor of your choice, and look for the following block of code: ```typescript -class Colors extends Component { - @property lightBackground = palette.white; - @property darkBackground = palette.black; - @property text = palette.black; - @property caption = palette.purple; +class Colors { + lightBackground = palette.white; + darkBackground = palette.black; + text = palette.black; + caption = palette.purple; } ``` @@ -41,8 +41,8 @@ In this example, the `Colors` component maps semantic names to the `Palette` com Let's change `lightBackground` to `palette.lightPurple` like so: ```Diff -- @property lightBackground = palette.white; -+ @property lightBackground = palette.lightPurple; +- lightBackground = palette.white; ++ lightBackground = palette.lightPurple; ``` Save your changes to see the background color update in real time! Feel free to experiment with changing other values to see Diez in action. diff --git a/examples/site/examples/web/docs/getting-started/swift.md b/examples/site/examples/web/docs/getting-started/swift.md index 342a7a959..68a956819 100644 --- a/examples/site/examples/web/docs/getting-started/swift.md +++ b/examples/site/examples/web/docs/getting-started/swift.md @@ -30,11 +30,11 @@ Let's change the background color of our application by modifying our design sys Open `src/DesignSystem.ts`, in an editor of your choice, and look for the following block of code: ```typescript -class Colors extends Component { - @property lightBackground = palette.white; - @property darkBackground = palette.black; - @property text = palette.black; - @property caption = palette.purple; +class Colors { + lightBackground = palette.white; + darkBackground = palette.black; + text = palette.black; + caption = palette.purple; } ``` @@ -43,8 +43,8 @@ In this example, the `Colors` component maps semantic names to the `Palette` com Let's change `lightBackground` to `palette.lightPurple` like so: ```Diff -- @property lightBackground = palette.white; -+ @property lightBackground = palette.lightPurple; +- lightBackground = palette.white; ++ lightBackground = palette.lightPurple; ``` Save your changes to see the background color update in real time! Feel free to experiment with changing other values to see Diez in action. diff --git a/examples/site/examples/web/docs/getting-started/the-basics.md b/examples/site/examples/web/docs/getting-started/the-basics.md index 4eda0c8e8..1e0ba5c47 100644 --- a/examples/site/examples/web/docs/getting-started/the-basics.md +++ b/examples/site/examples/web/docs/getting-started/the-basics.md @@ -27,12 +27,12 @@ In general, you define `Component`(s) composed of `Property`(ies) and compose th ```typescript import {Component, property} from '@diez/engine'; -class LayoutValues extends Component { - @property spacingSmall = 5; +class LayoutValues { + spacingSmall = 5; } -export class DesignSystem extends Component { - @property layoutValues = new LayoutValues(); +export class DesignSystem { + layoutValues = new LayoutValues(); } ``` @@ -49,8 +49,8 @@ Use the `Color` prefab to create color palettes. ```typescript import {Color} from '@diez/prefabs'; -class MyColors extends Component { - @property purple = Color.rgb(86, 35, 238); +class MyColors { + purple = Color.rgb(86, 35, 238); } ``` @@ -61,8 +61,8 @@ View the full `Color` API [here](/docs/latest/classes/color.image.html). ```typescript import {Image} from '@diez/prefabs'; -class Images extends Component { - @property logo = Image.responsive('assets/logo.png'); +class Images { + logo = Image.responsive('assets/logo.png'); } ``` @@ -75,8 +75,8 @@ Typography is a bit more complicated. You'll need to _compose_ two prefabs (`Fon ```typescript import {Font, Typograph} from '@diez/prefabs'; -class TextStyles extends Component { - @property heading1 = new Typograph({ +class TextStyles { + heading1 = new Typograph({ font: Font.fromFile('assets/SourceSansPro-Regular.ttf'), fontSize: 24, color: colors.text, diff --git a/examples/site/src/DesignSystem.ts b/examples/site/src/DesignSystem.ts index a00a129a1..a4789d27a 100644 --- a/examples/site/src/DesignSystem.ts +++ b/examples/site/src/DesignSystem.ts @@ -1,10 +1,9 @@ -import {Component, property} from '@diez/engine'; import {palette, spacing, sizing, borderRadius, typography} from './constants'; -export class DesignSystem extends Component { - @property palette = palette; - @property spacing = spacing; - @property sizing = sizing; - @property borderRadius = borderRadius; - @property typography = typography; +export class DesignSystem { + palette = palette; + spacing = spacing; + sizing = sizing; + borderRadius = borderRadius; + typography = typography; } diff --git a/examples/site/src/constants.ts b/examples/site/src/constants.ts index 410e22344..2e9b09cac 100644 --- a/examples/site/src/constants.ts +++ b/examples/site/src/constants.ts @@ -1,4 +1,3 @@ -import {Component, property} from '@diez/engine'; import {Color, Font, Typograph, FontStyle} from '@diez/prefabs'; const baseColors = { @@ -12,48 +11,48 @@ const baseColors = { black: Color.hex('#000010'), } -class Palette extends Component { - @property purple = baseColors.purple; - @property mauve = baseColors.mauve; - @property mauve700 = baseColors.mauve.darken(0.20); - @property white = baseColors.white; - @property gray100 = baseColors.gray100; - @property gray400 = baseColors.gray400; - @property gray700 = baseColors.gray700; - @property gray900 = baseColors.gray900; - @property black = baseColors.black; - @property primary = baseColors.purple; - @property secondary = baseColors.mauve; - @property cardInsetShadow = baseColors.gray100; - @property cardColor = baseColors.gray400; - @property cardShadow = baseColors.gray700; +class Palette { + purple = baseColors.purple; + mauve = baseColors.mauve; + mauve700 = baseColors.mauve.darken(0.20); + white = baseColors.white; + gray100 = baseColors.gray100; + gray400 = baseColors.gray400; + gray700 = baseColors.gray700; + gray900 = baseColors.gray900; + black = baseColors.black; + primary = baseColors.purple; + secondary = baseColors.mauve; + cardInsetShadow = baseColors.gray100; + cardColor = baseColors.gray400; + cardShadow = baseColors.gray700; } -class Spacing extends Component { - @property xxs = 2; - @property xs = 4; - @property sm = 8; - @property md = 12; - @property lg = 18; - @property xl = 24; - @property xxl = 32; - @property xxxl = 44; +class Spacing { + xxs = 2; + xs = 4; + sm = 8; + md = 12; + lg = 18; + xl = 24; + xxl = 32; + xxxl = 44; } -class Sizing extends Component { - @property xxs = 60; - @property xs = 100; - @property sm = 200; - @property md = 300; - @property lg = 500; - @property xl = 640; - @property xxl = 860; - @property xxxl = 1300; +class Sizing { + xxs = 60; + xs = 100; + sm = 200; + md = 300; + lg = 500; + xl = 640; + xxl = 860; + xxxl = 1300; } -class BorderRadius extends Component { - @property card = 7; - @property button = 4; +class BorderRadius { + card = 7; + button = 4; } /** @@ -71,61 +70,61 @@ const Fonts = { } }; -class Typography extends Component { - @property headingOne = new Typograph({ +class Typography { + headingOne = new Typograph({ font: Fonts.SourceSansPro.Black, fontSize: 64, color: palette.black, }); - @property headingTwo = new Typograph({ + headingTwo = new Typograph({ font: Fonts.SourceSansPro.Black, fontSize: 48, color: palette.black, }); - @property headingThree = new Typograph({ + headingThree = new Typograph({ font: Fonts.SourceSansPro.Regular, fontSize: 32, color: palette.black, }); - @property headingFour = new Typograph({ + headingFour = new Typograph({ font: Fonts.SourceSansPro.Black, fontSize: 23, color: palette.black, }); - @property copy = new Typograph({ + copy = new Typograph({ font: Fonts.SourceSansPro.Regular, fontSize: 16, color: palette.black, }); - @property nav = new Typograph({ + nav = new Typograph({ font: Fonts.SourceSansPro.Regular, fontSize: 20, color: palette.black, }); - @property link = new Typograph({ + link = new Typograph({ font: Fonts.SourceSansPro.Black, fontSize: 20, color: palette.primary, }); - @property button = new Typograph({ + button = new Typograph({ font: Fonts.SourceCodePro.Black, fontSize: 20, color: palette.primary, }); - @property logo = new Typograph({ + logo = new Typograph({ font: Fonts.SourceCodePro.Black, fontSize: 30, color: palette.black, }); - @property code = new Typograph({ + code = new Typograph({ font: Fonts.SourceCodePro.Regular, fontSize: 16, }); - @property codeLarge = new Typograph({ + codeLarge = new Typograph({ font: Fonts.SourceCodePro.Regular, fontSize: 18, }); - @property codeSmall = new Typograph({ + codeSmall = new Typograph({ font: Fonts.SourceCodePro.Regular, fontSize: 15, }); diff --git a/packages/compiler/README.md b/packages/compiler/README.md index f3f7c3e01..941ee0dad 100644 --- a/packages/compiler/README.md +++ b/packages/compiler/README.md @@ -17,15 +17,14 @@ Additionally, the compiler expects the TypeScript configuration is set up to com At compile time, the compiler first parses the TypeScript AST of every component exported in `src/index.ts`, and recursively parses its component properties. For example, given these contents: ``` -import {Component, property} from '@diez/engine'; import {Color} from '@diez/prefabs'; -class Palette extends Component { - @property red = Color.rgb(255, 0, 0); +class Palette { + red = Color.rgb(255, 0, 0); } export class DesignSystem { - @property palette = new Palette(); + palette = new Palette(); } ``` diff --git a/packages/compiler/src/api.ts b/packages/compiler/src/api.ts index be52577ee..83b23b6e1 100644 --- a/packages/compiler/src/api.ts +++ b/packages/compiler/src/api.ts @@ -1,4 +1,4 @@ -import {Component, ConcreteComponent, ConcreteComponentType, Target} from '@diez/engine'; +import {Prefab, Target} from '@diez/engine'; import {EventEmitter} from 'events'; import {Type} from 'ts-morph'; @@ -144,7 +144,7 @@ export interface CompilerTargetProvider { * @ignore */ export interface ComponentModule { - [key: string]: ConcreteComponentType; + [key: string]: object | Constructor; } /** @@ -219,7 +219,7 @@ export interface AssetBinding { * Provides 0 or more bindings from a component instance. */ export type AssetBinder< - ComponentType extends Component, + ComponentType extends object, OutputType = TargetOutput, > = ( instance: ComponentType, @@ -241,7 +241,7 @@ export enum CompilerEvent { /** * Sent by the compiler when it encounters an error. */ - Error = 'error', + Error = 'compilerError', } /** @@ -270,7 +270,7 @@ export interface TargetComponentSpec { */ export interface TargetSpecLedger { spec: Spec; - instances: Set; + instances: Set; binding?: Binding; } @@ -298,9 +298,15 @@ export interface TargetOutput< * Provides a base binding interfaces for target compilers can extend as needed. */ export interface TargetBinding< - T extends Component = Component, + T extends Prefab = Prefab, OutputType = TargetOutput, > { sources: string[]; assetsBinder?: AssetBinder; } + +/** + * A general type for an object that supports construction. + * @ignore + */ +export type Constructor = new () => object; diff --git a/packages/compiler/src/compiler.ts b/packages/compiler/src/compiler.ts index 84fccc51b..1c720fc56 100644 --- a/packages/compiler/src/compiler.ts +++ b/packages/compiler/src/compiler.ts @@ -1,16 +1,16 @@ -/* tslint:disable:max-line-length */ +/* tslint:disable:max-line-length ban-types */ import {exitTrap, Log} from '@diez/cli-core'; -import {ConcreteComponent} from '@diez/engine'; +import {serialize} from '@diez/engine'; import {noCase} from 'change-case'; import {EventEmitter} from 'events'; import {copySync, ensureDirSync, existsSync, outputFileSync, removeSync, writeFileSync} from 'fs-extra'; import {dirname, join, relative} from 'path'; -import {ClassDeclaration, EnumDeclaration, Project, PropertyDeclaration, SourceFile, Type, TypeChecker} from 'ts-morph'; -import {createAbstractBuilder, createWatchCompilerHost, createWatchProgram, Diagnostic, FileWatcher, FormatDiagnosticsHost, formatDiagnosticsWithColorAndContext, isClassDeclaration, Program as TypescriptProgram, sys} from 'typescript'; +import {ClassDeclaration, EnumDeclaration, Project, PropertyDeclaration, Scope, SourceFile, Symbol, Type, TypeChecker} from 'ts-morph'; +import {createAbstractBuilder, createWatchCompilerHost, createWatchProgram, Diagnostic, FileWatcher, FormatDiagnosticsHost, formatDiagnosticsWithColorAndContext, isClassDeclaration, Program as TypescriptProgram, SymbolFlags, sys} from 'typescript'; import {v4} from 'uuid'; import {CompilerEvent, CompilerOptions, CompilerProgram, MaybeNestedArray, NamedComponentMap, PrimitiveType, PrimitiveTypes, PropertyType, TargetBinding, TargetComponent, TargetComponentProperty, TargetComponentSpec, TargetOutput, TargetProperty} from './api'; import {serveHot} from './server'; -import {getBinding, getHotPort, getProject, loadComponentModule, purgeRequireCache} from './utils'; +import {getBinding, getHotPort, getProject, isConstructible, loadComponentModule, purgeRequireCache} from './utils'; /** * A class implementing the requirements of Diez compilation. @@ -36,7 +36,7 @@ export class Program extends EventEmitter implements CompilerProgram { /** * The component declaration, which we can use to determine component-ness using the typechecker. */ - private readonly componentDeclaration: ClassDeclaration; + private readonly prefabDeclaration: ClassDeclaration; /** * The active TypeScript program. @@ -142,12 +142,31 @@ export class Program extends EventEmitter implements CompilerProgram { return false; } - const typeValue = typeSymbol.getValueDeclaration() as ClassDeclaration; - if (!isClassDeclaration(typeValue.compilerNode)) { - // FIXME: we are catching methods in this net as well, but should not. + const typeValue = typeSymbol.getValueDeclaration() as ClassDeclaration | undefined; + + if (typeValue === undefined || !isClassDeclaration(typeValue.compilerNode)) { + // FIXME: we should allow non-class declarations as long as they use explicit types. return false; } + const children: Symbol[] = []; + if (typeValue.getBaseClass() === this.prefabDeclaration) { + for (const symbol of type.getProperties()) { + if (symbol.getFlags() !== SymbolFlags.Property) { + continue; + } + children.push(symbol); + } + } else { + for (const property of typeValue.getProperties()) { + if (property.getScope() !== Scope.Public) { + continue; + } + + children.push(typeSymbol.getMemberOrThrow(property.getName())); + } + } + const componentName = typeValue.getName(); if (!componentName) { // FIXME: we should be able to handle this by automatically generating anonymous componenet names. @@ -167,10 +186,6 @@ export class Program extends EventEmitter implements CompilerProgram { return true; } - if (typeValue.getBaseClass() !== this.componentDeclaration) { - return false; - } - const newTarget: TargetComponent = { type, properties: [], @@ -180,10 +195,10 @@ export class Program extends EventEmitter implements CompilerProgram { source: sourceMap.get(typeValue.getSourceFile().getFilePath()) || '.', }; - for (const typeMember of typeSymbol.getMembers()) { + for (const typeMember of children) { const valueDeclaration = typeMember.getValueDeclaration() as PropertyDeclaration; if (!valueDeclaration) { - // We will skip e.g. @typeparams here. + // This should never happen? continue; } const propertyName = valueDeclaration.getName(); @@ -401,13 +416,13 @@ export class Program extends EventEmitter implements CompilerProgram { // Create a stub type file for typing the Component class and number primitives. const stubTypeFile = this.project.createSourceFile( join('src', '__stub.ts'), - 'import {Component, Integer, Float} from \'@diez/engine\';', + 'import {Prefab, Integer, Float} from \'@diez/engine\';', {overwrite: true}, ); - const [componentImport, intImport, floatImport] = stubTypeFile.getImportDeclarationOrThrow('@diez/engine').getNamedImports(); + const [prefabImport, intImport, floatImport] = stubTypeFile.getImportDeclarationOrThrow('@diez/engine').getNamedImports(); this.targetComponents = new Map(); - this.componentDeclaration = this.checker.getTypeAtLocation(componentImport).getSymbolOrThrow().getValueDeclarationOrThrow() as ClassDeclaration; + this.prefabDeclaration = this.checker.getTypeAtLocation(prefabImport).getSymbolOrThrow().getValueDeclarationOrThrow() as ClassDeclaration; this.types = { [PrimitiveType.Int]: intImport.getSymbolOrThrow().getDeclaredType(), [PrimitiveType.Float]: floatImport.getSymbolOrThrow().getDeclaredType(), @@ -589,7 +604,7 @@ export abstract class TargetCompiler< /** * Recursively processes a component instance and all its properties. */ - protected async processComponentInstance (instance: ConcreteComponent, name: PropertyType) { + protected async processComponentInstance (instance: any, name: PropertyType) { const targetComponent = this.program.targetComponents.get(name); if (!targetComponent) { Log.warning(`Unable to find component definition for ${name}!`); @@ -597,18 +612,24 @@ export abstract class TargetCompiler< } const spec = this.createSpec(name); + const serializedData = serialize(instance); for (const property of targetComponent.properties) { - const propertyOptions = instance.boundStates.get(property.name); - if (!propertyOptions || (propertyOptions.targets && !propertyOptions.targets.includes(this.program.options.target))) { + // TODO: move this check upstream of the target compiler, into the compiler metadata stream where it belongs. + if ( + instance.options && + instance.options[property.name] && + Array.isArray(instance.options[property.name].targets) && + !instance.options[property.name].targets.includes(this.program.options.target) + ) { // We are looking at a property that is either not a state or explicitly excluded by the host. continue; } const propertySpec = await this.processComponentProperty( property, - instance.get(property.name), - instance.serialize()[property.name], + instance[property.name], + serializedData[property.name], targetComponent, ); @@ -637,13 +658,13 @@ export abstract class TargetCompiler< purgeRequireCache(require.resolve(this.program.emitRoot)); const componentModule = await loadComponentModule(this.program.emitRoot); for (const componentName of this.program.localComponentNames) { - const constructor = componentModule[componentName]; - if (!constructor) { + const maybeConstructor = componentModule[componentName]; + if (!maybeConstructor) { Log.warning(`Unable to resolve component instance from ${this.program.projectRoot}: ${componentName}.`); continue; } - const componentInstance = new constructor(); + const componentInstance = isConstructible(maybeConstructor) ? new maybeConstructor() : maybeConstructor; await this.processComponentInstance(componentInstance, componentName); } } diff --git a/packages/compiler/src/server/hot-component.ts b/packages/compiler/src/server/hot-component.ts index 711b88893..9ef6ae459 100644 --- a/packages/compiler/src/server/hot-component.ts +++ b/packages/compiler/src/server/hot-component.ts @@ -1,7 +1,7 @@ declare global { interface Window { componentName: string; - component: ConcreteComponent; + component: object; __resourceQuery: string; } } @@ -9,22 +9,30 @@ declare global { // Shim in a resource query indicating a shorter timeout before reconnecting. window.__resourceQuery = '?timeout=1000'; -import {ConcreteComponent, ConcreteComponentType, Patcher} from '@diez/engine'; +import {Patcher, serialize} from '@diez/engine'; import {subscribe} from 'webpack-hot-middleware/client'; + +// Mirrored in ../api.ts, but required at runtime in the web. +type Constructor = new () => object; + +// Mirrored in ../utils.ts, but required at runtime in the web. +const isConstructible = (maybeConstructible: any): maybeConstructible is Constructor => + maybeConstructible.prototype !== undefined && maybeConstructible.prototype.constructor instanceof Function; + subscribe((message) => { if (message.reload) { window.location.reload(true); } }); -const getComponentDefinition = async (): Promise => { +const getComponentDefinition = async (): Promise => { const componentFile = await import(`${'@'}`) as any; return componentFile[window.componentName]; }; const loadComponent = async () => { - const constructor = await getComponentDefinition(); - return window.component = new constructor(); + const maybeConstructor = await getComponentDefinition(); + return window.component = (isConstructible(maybeConstructor) ? new maybeConstructor() : maybeConstructor); }; /** @@ -32,6 +40,5 @@ const loadComponent = async () => { */ export const activate = async (patcher: Patcher) => { const component = await loadComponent(); - component.dirty(); - component.tick(Date.now(), patcher); + patcher(serialize(component)); }; diff --git a/packages/compiler/src/utils.ts b/packages/compiler/src/utils.ts index 02e5465a8..5b62847d5 100644 --- a/packages/compiler/src/utils.ts +++ b/packages/compiler/src/utils.ts @@ -3,7 +3,14 @@ import {Target} from '@diez/engine'; import {dirname, join} from 'path'; import {Project} from 'ts-morph'; import {findConfigFile, sys} from 'typescript'; -import {CompilerTargetProvider, ComponentModule, NamedComponentMap, PropertyType} from './api'; +import {CompilerTargetProvider, ComponentModule, Constructor, NamedComponentMap, PropertyType} from './api'; + +/** + * A type guard for identifying a [[Constructor]] vs. a plain object. + * @ignore + */ +export const isConstructible = (maybeConstructible: any): maybeConstructible is Constructor => + maybeConstructible.prototype !== undefined && maybeConstructible.prototype.constructor instanceof Function; /** * Shared singleton for retrieving Projects. diff --git a/packages/compiler/test/fixtures/Bindings/Bindings.ts b/packages/compiler/test/fixtures/Bindings/Bindings.ts index fae93fa3d..2613d9a06 100644 --- a/packages/compiler/test/fixtures/Bindings/Bindings.ts +++ b/packages/compiler/test/fixtures/Bindings/Bindings.ts @@ -1,8 +1,10 @@ -import {Component, property} from '@diez/engine'; +import {prefab} from '@diez/engine'; -export class BoundComponent extends Component {} +export class BoundComponent extends prefab<{}>() { + defaults = {}; +} -export class Bindings extends Component { - @property bound = new BoundComponent(); - @property ambiguous: any = '12'; +export class Bindings { + bound = new BoundComponent(); + ambiguous: any = '12'; } diff --git a/packages/compiler/test/fixtures/Filtered/Filtered.ts b/packages/compiler/test/fixtures/Filtered/Filtered.ts index f47eb7437..dafa99bb2 100644 --- a/packages/compiler/test/fixtures/Filtered/Filtered.ts +++ b/packages/compiler/test/fixtures/Filtered/Filtered.ts @@ -1,7 +1,20 @@ -import {Component, property, Target} from '@diez/engine'; +import {prefab, Target} from '@diez/engine'; -export class Filtered extends Component { - @property({targets: ['not-test' as Target]}) excludeMe = false; - @property includeMe = true; - @property({targets: ['test' as Target]}) includeUs = [true, true, true]; +interface FilteredData { + excludeMe: boolean; + includeMe: boolean; + includeUs: boolean[]; +} + +export class Filtered extends prefab() { + defaults = { + excludeMe: false, + includeMe: true, + includeUs: [true, true, true], + }; + + options = { + excludeMe: {targets: ['not-test' as Target]}, + includeUs: {targets: ['test' as Target]}, + }; } diff --git a/packages/compiler/test/fixtures/Valid/Valid.ts b/packages/compiler/test/fixtures/Valid/Valid.ts index e8f445e15..5be71fc9b 100644 --- a/packages/compiler/test/fixtures/Valid/Valid.ts +++ b/packages/compiler/test/fixtures/Valid/Valid.ts @@ -1,4 +1,4 @@ -import {Component, Float, Integer, property} from '@diez/engine'; +import {Float, Integer} from '@diez/engine'; enum StringEnum { Foo = 'Foo', @@ -16,50 +16,52 @@ enum HeterogeneousEnum { Baz = 'Bat', } -class GrandchildComponent extends Component { - @property diez = 'diez'; +class GrandchildComponent { + diez = 'diez'; } -class ChildComponent extends Component { - @property grandchild = new GrandchildComponent(); +class ChildComponent { + grandchild = new GrandchildComponent(); } -class NotAComponent { - ignoreMe = true; -} +export class Valid { + // Private and protected members should be ignored. + private five = 5; + protected cinco = 5.0; -export class Valid extends Component { // Primitive types. - @property int: Integer = 10; - @property number = 10; - @property float: Float = 10.0; - @property string = 'ten'; - @property boolean = !!10; + int: Integer = this.five + this.cinco; + number = 10; + float: Float = 10.0; + string = 'ten'; + boolean = !!10; // Homogenous enums should compile with the correct type. - @property stringEnum = StringEnum.Foo; - @property numberEnum = NumberEnum.Bar; + stringEnum = StringEnum.Foo; + numberEnum = NumberEnum.Bar; // Child components should be processed on the parse. - @property child = new ChildComponent(); + child = new ChildComponent(); // This noncomponent should be safely skipped. - @property badChild = new NotAComponent(); + badChild = { + ignoreMe: true, + }; // Heterogenous enums are incompatible with the type system, and are banned, as are other properties with ambiguous // types. - @property invalidEnum = HeterogeneousEnum.Baz; - @property unknown: unknown = 10; - @property any: any = 10; - @property union: number | string = 10; + invalidEnum = HeterogeneousEnum.Baz; + unknown: unknown = 10; + any: any = 10; + union: number | string = 10; // These valid list types have uniform depth of a supported type. - @property validListDepth1 = [10, 10, 10, 10]; - @property validListDepth2 = [['10', '10'], ['10', '10']]; + validListDepth1 = [10, 10, 10, 10]; + validListDepth2 = [['10', '10'], ['10', '10']]; // This list type has uniform depth, but of an underlying union. - @property invalidListUniformDepth = [10, '10']; + invalidListUniformDepth = [10, '10']; // This list type has uniform types, but inconsistent depth. - @property invalidListUniformType = [10, [10]]; + invalidListUniformType = [10, [10]]; } diff --git a/packages/compiler/test/target.test.ts b/packages/compiler/test/target.test.ts index 9095bf434..6ddbc891b 100644 --- a/packages/compiler/test/target.test.ts +++ b/packages/compiler/test/target.test.ts @@ -15,12 +15,6 @@ describe('target compiler', () => { expect(ledger.spec.componentName).toEqual('Filtered'); // The ledger does not have `excludeMe` even though it was defined on the component. expect(ledger.instances.size).toBe(1); - const instance = ledger.instances.values().next().value; - expect(instance.boundStates.has('excludeMe')); - expect(instance.boundStates.has('includeMe')); - expect(instance.boundStates.has('includeUs')); - - expect(instance.boundStates.get('excludeMe')).toEqual({targets: ['not-test']}); // The excluded property is not included in the ledger spec. expect(ledger.spec.properties).toEqual({ diff --git a/packages/createproject/templates/project/src/DesignSystem.ts b/packages/createproject/templates/project/src/DesignSystem.ts index 78b0a16b5..0d10cb97b 100644 --- a/packages/createproject/templates/project/src/DesignSystem.ts +++ b/packages/createproject/templates/project/src/DesignSystem.ts @@ -1,4 +1,3 @@ -import {Component, property} from '@diez/engine'; import {Color} from '@diez/prefabs'; /** @@ -7,6 +6,6 @@ import {Color} from '@diez/prefabs'; * * Check out https://beta.diez.org/getting-started to learn more. */ -export class DesignSystem extends Component { - @property red = Color.hex('#f00'); +export class DesignSystem { + red = Color.hex('#f00'); } diff --git a/packages/engine/README.md b/packages/engine/README.md index b90c14523..4648d5257 100644 --- a/packages/engine/README.md +++ b/packages/engine/README.md @@ -2,8 +2,7 @@ The Diez engine provides the basic building block of Diez packages: - - A Diez `Component` class which can be extended to define cross-platform components. - - Decorators like `@property` which can endow components with properties. - - A thin runtime engine which is capable of observing and patching component hosts based on state changes. + - A Diez `prefab` class factory which can be used to define cross-platform prefabs. + - A thin serialization engine which obeys simple serialization instructions for prefabs. Diez components generally should be composed with nested components and primitive values in order to produce semantic and readable component hierarchies. See [here](https://github.com/diez/diez/blob/master/examples/poodle-surf/src/DesignSystem.ts) for some advanced examples of how components can be composed to build a design system. diff --git a/packages/engine/src/api.ts b/packages/engine/src/api.ts index 9fda7cd24..c20f08e4b 100644 --- a/packages/engine/src/api.ts +++ b/packages/engine/src/api.ts @@ -1,110 +1,9 @@ -/* tslint:disable:no-empty-interface */ - -/** - * Primitive types. These can always be serialized over the wire without intervention. - */ -export type Primitive = ( - string | number | boolean | null | - string[] | number[] | boolean[] | null[] -); - -/** - * Anything serializable is either primitive or provides its own serialization instructions. - */ -export type AnySerializable = Primitive | Serializable | {[property: string]: AnySerializable}; - /** * A serializable interface for providing bespoke serialization instructions. Can return anything * recursively serializable. */ -export interface Serializable { - serialize (): any; -} - -/** - * Alias for any indexable type. Stateful and Tweenable interfaces use a generic type extending this. - */ -export interface Indexable { - [key: string]: any; -} - -/** - * Stateful interfaces can be updated through partial state definitions. - * - * @typeparam T - The shape of the state the stateful component is expected to receive. - */ -export interface Stateful { - get (key: K): any; - set (state: Partial): void; - has (key: keyof T): boolean; -} - -/** - * A curve is a mapping from [0, 1] to a number representing a normalized value progression over time. - * - * It is expected that a curve is always 0 at t = 0 and always 1 at t = 1. - * @ignore - * @experimental - */ -export type Curve = (t: number) => number; - -/** - * A tween specification should specify a duration and an optional curve. - * @ignore - * @experimental - */ -export interface TweenSpecification { - duration: number; - curve?: Curve; -} - -/** - * A tween provides complete instructions for changing a numeric value over time. - * @ignore - * @experimental - */ -export interface Tween { - startValue: number; - endValue: number; - startTime: number; - endTime: number; - curve: Curve; -} - -/** - * Tweenable interfaces can be updated through partial state definitions with tween specifications. - * @ignore - * @experimental - */ -export interface Tweenable { - tween (state: Partial, spec: TweenSpecification): Promise; -} - -/** - * A listener is any function of a data payload. Its return value is ignored; it should rely on - * side effects to mutate application state. - * @ignore - * @experimental - */ -export type Listener = (data?: D) => void; - -/** - * A hashmap of Listeners. - * @ignore - * @experimental - */ -export interface Listeners { - [name: string]: Listener; -} - -/** - * Listening interfaces can trigger a listener by name with a payload of a generically specified - * type. - * @ignore - * @experimental - */ -export interface Listening { - trigger (name: string, payload: D): void; +export interface Serializable { + serialize (): T; } /** @@ -113,38 +12,12 @@ export interface Listening { export type Patcher = (payload: any) => void; /** - * Tickable interfaces can tick an internal clock with a specified time. - * @ignore - * @experimental + * A typed hashmap mapping string keys to objects of a specific type. */ -export interface Tickable { - tick (time: number, onPatch?: Patcher): void; -} - -/** - * A special type of state for components, which only can contain values of a specific type. - */ -export interface HashMap { +export interface HashMap { [key: string]: T; } -/** - * A formula receives variously typed args and returns a specific type. - * @ignore - * @experimental - */ -export type Formula = (...args: any[]) => T; - -/** - * An expression resolver can be any object that supports indexing. We use this to specify - * what expression variables should be resolved from at runtime. - * @ignore - * @experimental - */ -export interface ExpressionResolver { - [key: string]: any; -} - /** * @internal * diff --git a/packages/engine/src/component.ts b/packages/engine/src/component.ts deleted file mode 100644 index 4bbb35f17..000000000 --- a/packages/engine/src/component.ts +++ /dev/null @@ -1,308 +0,0 @@ -import { - Indexable, - Listeners, - Listening, - Patcher, - PropertyOptions, - Serializable, - Stateful, - Tickable, - Tween, - Tweenable, - TweenSpecification, -} from './api'; -import {Serializer} from './serialization'; -import {interpolateNumbers} from './transitions'; - -/** - * Private type verifier for component-ness. - */ -const isComponent = (maybeComponent: any): maybeComponent is Component => { - return maybeComponent && maybeComponent.constructor && maybeComponent.constructor.isComponent; -}; - -/** - * Private type verifier for number-ness. - */ -const isNumeric = (n: any): n is number => !isNaN(parseFloat(n)) && isFinite(n); - -/** - * A default tween to use when no other tween is available. - */ -const linearTween = (t: number) => t; - -interface EndtimeResolver { - resolve (): void; - endTime: number; - keys: Set; -} - -/** - * The abstract Component class is responsible for serializing its readonly state via patches. - * @typeparam T - An [[Indexable]] interface the component state must adhere to. This parameter can be elided unless the - * component is instantiated with overridden state. - */ -export abstract class Component - implements Stateful, Tweenable, Listening, Tickable, Serializable { - /** - * Important: this flag instructs the hosting system we are a component instance. - * @ignore - */ - static isComponent = true; - - /** - * A registry of hosted child components. - */ - private readonly children = new Map>(); - - /** - * A registry of tweens. - * @ignore - */ - tweens = new Map(); - - /** - * A humble, readonly state container. - */ - protected readonly state!: T; - - /** - * The (nullable) component that may be hosting us. - */ - host?: Component; - - /** - * Initialized via decorators. - * @ignore - */ - listeners?: Listeners; - - /** - * Responsible for serializing against our mutable state container. - */ - protected readonly serializer: Serializer; - - /** - * Responsible for tracking tween end resolutions. - * @ignore - */ - private readonly endtimeResolvers = new Set>(); - - /** - * The internal timekeeping mechanism for this component. - * @ignore - */ - time = 0; - - /** - * Internal tracker for whether our component instance should patch on the next tick. - */ - private doPatch = false; - - /** - * A map of bound state names to associated property options. Created with the [[property]] decorator. - * @ignore - */ - readonly boundStates = new Map>(); - - constructor ( - state: Partial = {}, - ) { - // Note how we explicitly case state as T. Our engine relies on runtime initialization with defaults in concrete - // implementations. - this.state = {} as T; - this.set(state); - this.serializer = new Serializer(this.state); - } - - private patch (patcher: Patcher) { - if (this.doPatch) { - this.doPatch = false; - patcher(this.serializer.payload); - } - } - - private eachChild (handler: (child: Component) => void) { - this.children.forEach(handler); - } - - /** - * Serializable interface. - * - * Generic serialization instructions. These can be overridden as needed. - */ - serialize () { - return this.serializer.payload; - } - - /** - * Notes that state has been mutated and serialized payload should be re-flushed. - */ - dirty () { - if (this.host) { - this.host.dirty(); - } else { - this.doPatch = true; - } - } - - /** - * Tickable interface. Ticks the clock at the provided time. - * @ignore - */ - tick (time: number, onPatch?: Patcher) { - this.time = time; - this.eachChild((child) => child.tick(time)); - this.executeTweens(); - - // Check our endtime resolvers for any dangling promises, and resolve them if we are past the end time. - for (const endtimeResolver of this.endtimeResolvers) { - if (endtimeResolver.endTime <= time) { - endtimeResolver.resolve(); - this.endtimeResolvers.delete(endtimeResolver); - } - } - if (!this.host && onPatch) { - this.patch(onPatch); - } - } - - /** - * Stateful interface. Retrieves a state value. - */ - get (key: K): any { - return this.state[key as keyof T]; - } - - /** - * Stateful interface. Retrieves a state value. - */ - has (key: keyof T) { - return this.state.hasOwnProperty(key); - } - - /** - * Stateful interface. Updates states and patch. - */ - set (state: Partial): void { - let dirty = false; - Object.entries(state).forEach(([key, child]) => { - if (this.state[key] === child || child === undefined) { - return; - } - - dirty = true; - if (isComponent(child)) { - this.children.set(key, child); - child.host = this; - } - this.state[key as keyof T] = child; - }); - if (dirty) { - this.dirty(); - } - } - - /** - * Tweenable interface. Schedule state updates for the duration of the tween. - * @ignore - */ - tween (state: Partial, spec: TweenSpecification): Promise { - if (spec.duration === 0) { - this.set(state); - return new Promise((resolve) => resolve()); - } - - const endTime = this.time + spec.duration; - - if ( - !spec.curve || - !(spec.curve instanceof Function) - || spec.curve(0) !== 0 - || spec.curve(1) !== 1 - ) { - spec.curve = linearTween; - } - - for (const k in state) { - if (isNumeric(this.state[k]) && isNumeric(state[k])) { - // Do not bother setting trivial tweens. - if (this.state[k] === state[k]) { - continue; - } - - this.tweens.set(k, { - endTime, - startValue: this.state[k], - endValue: state[k]!, - startTime: this.time, - curve: spec.curve, - }); - } else if (isComponent(this.state[k]) && isComponent(state[k])) { - this.state[k].tween(state[k].state, spec); - } - } - - // Check for any transitions in progress that were interrupted. Resolve and dequeue them. - const keys = Object.keys(state); - for (const endtimeResolver of this.endtimeResolvers) { - if (keys.filter((key) => endtimeResolver.keys.has(key)).length > 0) { - endtimeResolver.resolve(); - this.endtimeResolvers.delete(endtimeResolver); - } - } - - return new Promise((resolve) => { - this.endtimeResolvers.add({endTime, resolve, keys: new Set(keys)}); - }); - } - - /** - * Ticks all active tweens, trims completed tweens, and fires onComplete callbacks. - */ - private executeTweens () { - const tweenPatch: Partial = {}; - for (const [key, tween] of this.tweens) { - // FIXME: in the case of a bulk tween, we are significantly overcalculating here. - // The tween function can be wrapped to store its tween values at time for better - // computational efficiency. - tweenPatch[key] = interpolateNumbers( - tween.startValue, - tween.endValue, - tween.startTime, - tween.endTime, - this.time, - tween.curve, - ) as T[keyof T]; - - if (tween.endTime <= this.time) { - this.tweens.delete(key); - } - } - this.set(tweenPatch); - } - - /** - * Listening interface. Fires the event listener for a given event name and payload type. - * @ignore - */ - trigger (name: string, payload?: D) { - if (!this.listeners || !this.listeners[name]) { - return; - } - - this.listeners[name].call(this, payload); - } -} - -/** - * A concrete component, used for typing. - * @ignore - */ -export class ConcreteComponent extends Component {} - -/** - * A concrete component type, used for typing. - * @ignore - */ -export type ConcreteComponentType = typeof ConcreteComponent; diff --git a/packages/engine/src/decorators.ts b/packages/engine/src/decorators.ts deleted file mode 100644 index d87082b07..000000000 --- a/packages/engine/src/decorators.ts +++ /dev/null @@ -1,85 +0,0 @@ -import {PropertyOptions} from './api'; -import {Component} from './component'; - -/** - * `@shared` decorator. Enables one-way data binding from a host component to its children, suitable for use in - * expressions. - * @ignore - */ -export const shared = (target: Component, key: string) => { - Object.defineProperty(target, key, { - get () { - // Yield the value from our host if it is provided. - if (this.host && this.host.has(key)) { - return this.host.get(key); - } - - throw new Error(`Shared property ${key} is not present on host.`); - }, - set () { - throw new Error('Do not set @shared values directly.'); - }, - } as ThisType); -}; - -/** - * `@method` decorator. Late-binds methods to a class prototype by name key. - * @ignore - */ -export const method = (target: any, key: string, descriptor: any) => { - if (!target.listeners) { - target.listeners = {}; - } - target.listeners[key] = descriptor.value; -}; - -/** - * @internal - */ -const actualProperty = (target: Component, key: string, options: Partial = {}) => { - Object.defineProperty(target, key, { - get () { - return this.get(key); - }, - - set (data: any) { - // Important: only set the state if it is undefined at bind-time or has already been bound. - // In the event that it is not undefined and unbound, it must have been overridden with a - // construction-time value. - if (this.get(key) === undefined || this.boundStates.get(key)) { - this.set({[key]: data}); - } - - this.boundStates.set(key, options); - - // Important: if we detect that a data value is actually an expression, make sure it knows - // to auto-resolve state names against itself unless otherwise specified. - if (data && typeof data === 'object' && data.constructor.isExpression) { - data.autoResolve(this); - } - }, - } as ThisType); -}; - -export function property (target: Component, key: string): void; -export function property (options: Partial): (target: Component, key: string) => void; - -/** - * `@property` decorator. Inspired by the VueJS compiler, this decorator replaces explicitly assigned properties - * with getters/setters that delegate down to the state container. - * - * This decorator can be invoked with additional options for enhanced functionality. - * - * ``` - * class DesignSystem extends Component { - * @property({key: 'value'}) foo = 'bar'; - * } - * ``` - */ -export function property (targetOrOptions: Partial | Component, maybeKey?: string) { - if (maybeKey) { - return actualProperty(targetOrOptions as Component, maybeKey); - } - - return (target: Component, key: string) => actualProperty(target, key, targetOrOptions as Partial); -} diff --git a/packages/engine/src/expression.ts b/packages/engine/src/expression.ts deleted file mode 100644 index 95db94e85..000000000 --- a/packages/engine/src/expression.ts +++ /dev/null @@ -1,103 +0,0 @@ -import {AnySerializable, ExpressionResolver, Formula, Serializable} from './api'; - -/** - * Given a function specification, retrieve its args for inspection. - */ -const getArgs = (formula: Formula): string[] => { - return formula - .toString() - .match(/\(([^)]*)\)/)![1] - .split(',') - .map((arg) => arg.replace(/\/\*.*\*\//, '').trim()) - .filter((arg) => !!arg); -}; - -/** - * An internal class that is always proxied to its return value. - */ -class Expression implements Serializable { - private readonly argList: string[]; - private resolver: ExpressionResolver = {}; - - /** - * Important: this flag instructs the `@property` decorator we are dealing with an expression - * instance. - */ - static isExpression = true; - - constructor ( - private readonly formula: Formula, - ) { - // TODO: support a compile-time alternative passing in explicit prop names as strings, and prefer - // this when it is provided, in the manner of `ngInject`. - this.argList = getArgs(formula); - } - - /** - * Binds a default resolver for `this.argList`. - * {@see {@link decorators.ts}} - */ - autoResolve (resolver: ExpressionResolver) { - this.resolver = resolver; - } - - /** - * Serializable interface. - * - * By implementing formula evaluation here, we can cleanly sub our proxied instance in at patch - * time and receive back the expected serialized payload. - */ - serialize (): T { - // TODO: implement dirty arg watching (only update expression if one of its args has changed). - const serialized = this.formula.apply( - this.resolver, - this.argList.map( - (name: string) => this.resolver[name] || null, - ), - ); - - return serialized; - } -} - -/** - * Generic expression constructor that hides the Expression type in a `Proxy` designed to unwrap - * to the "derived" `T` instance for typing purposes. - * - * See `component.test.ts` for examples. - * @ignore - */ -export const expression = ( - formula: Formula, -): T => { - const instance = new Expression(formula); - return new Proxy( - instance, - { - get (self: any, property: string) { - const serialized = self.serialize(); - if (property === 'serialize' && serialized.serialize) { - return serialized.serialize; - } - - // If this property is defined on ourself (think `serialize` or `autoResolve`), return it - // directly. - if ( - // Nit: Object.prototype provides `toString()`, but we don't really want it here. - property !== 'toString' && - (self[property] || self[property] === null || !self.serialize) - ) { - return self[property]; - } - - const proxied = serialized[property]; - if (proxied instanceof Function) { - // Important: make sure to bind to our serialized self if we have a proxied function. - // Without this, method calls on expression-wrapped components and primitives won't work. - return proxied.bind(serialized); - } - - return proxied; - }, - }); -}; diff --git a/packages/engine/src/index.ts b/packages/engine/src/index.ts index 2f766df72..2f1f8775f 100644 --- a/packages/engine/src/index.ts +++ b/packages/engine/src/index.ts @@ -5,6 +5,6 @@ */ export * from './api'; -export * from './component'; -export * from './decorators'; -export * from './expression'; +export * from './prefab'; +export * from './serialization'; +export * from './legacy'; diff --git a/packages/engine/src/legacy.ts b/packages/engine/src/legacy.ts new file mode 100644 index 000000000..a33cd8aca --- /dev/null +++ b/packages/engine/src/legacy.ts @@ -0,0 +1,19 @@ +/** + * It is no longer necessary for Diez components to extend a base `Component` class. + * + * This empty class is provided here for backward compatibility. + * @deprecated since version 10.0.0-beta.3 + * @ignore + */ +export class Component {} + +/** + * It is no longer necessary for Diez components to declare properties using the `@property` decorator. + * + * This null decorator is provided here for backward compatibility. + * @deprecated since version 10.0.0-beta.3 + * @ignore + */ +export const property = () => { + console.warn('It is no longer necessary for Diez components to declare properties using the `@property` decorator.'); +}; diff --git a/packages/engine/src/prefab.ts b/packages/engine/src/prefab.ts new file mode 100644 index 000000000..4b60bf703 --- /dev/null +++ b/packages/engine/src/prefab.ts @@ -0,0 +1,94 @@ +import {PropertyOptions, Serializable} from './api'; +import {serialize} from './serialization'; + +/** + * The abstract Prefab class provides a harness for reusable, instantiable design token prefabs. + * + * IMPORTANT: never extend this class directly. Due to the type semantics of Prefab instances--which should both store + * and implement the interface of their generic type parameters--we provide a factory ensuring intuitive typing. + * + * See [[prefab]] for details. + * @typeparam T - The interface the prefab data, as well as the prefab itself, must adhere to. + */ +export abstract class Prefab implements Serializable { + /** + * The component that is hosting us. + */ + host?: Prefab; + + /** + * Every concrete extension must implement exhaustive defaults conforming to the data interface. + */ + readonly abstract defaults: Readonly; + + /** + * If necessary, options may be defined. + */ + readonly options: Partial<{[K in keyof T]: Partial}> = {}; + + constructor (private readonly overrides: Partial = {}) { + // Build a proxy through which we can implement T, which is not statically known at compile time. + const proxy = new Proxy(this, this); + + // Pluck the defined overrides from the constructor argument. + this.overrides = {}; + for (const key of Object.keys(overrides) as (keyof T)[]) { + if (overrides[key] !== undefined) { + if (typeof overrides[key] === 'object') { + (overrides[key] as any).host = proxy; + } + this.overrides[key] = overrides[key]; + } + } + + return proxy; + } + + /** + * Proxy method, by which we subtly implement the data interface in the class itself. + */ + get (instance: any, key: string, receiver: any) { + if (instance[key] instanceof Function) { + return instance[key].bind(receiver); + } + + if (instance[key] !== undefined) { + return instance[key]; + } + + if (instance.overrides.hasOwnProperty(key)) { + return instance.overrides[key]; + } + + return instance.defaults[key]; + } + + /** + * A local data sanitizer, which can be used for pinning scalar values and any other data normalization needs. + */ + protected sanitize (data: T): T { + return data; + } + + /** + * Serializable interface. + * + * Generic serialization instructions. These can be overridden as needed. + */ + serialize () { + return serialize(this.sanitize(Object.assign(this.defaults, this.overrides))); + } +} + +/** + * A typing which acknowledges the Proxy by which Prefab actually implements T. + */ +type PrefabConstructor = new (overrides?: Partial) => Prefab & T; + +/** + * A factory for prefab base classes. All prefabs should be implemented as concrete classes extending + * the result of calling `prefab()` for some specific state shape `T`. + * @typeparam T - The interface the prefab data, as well as the prefab itself, must adhere to. + */ +export const prefab = (): PrefabConstructor => + Prefab.prototype.constructor as PrefabConstructor; diff --git a/packages/engine/src/serialization.ts b/packages/engine/src/serialization.ts index e5c965e69..57ec28d5f 100644 --- a/packages/engine/src/serialization.ts +++ b/packages/engine/src/serialization.ts @@ -1,14 +1,14 @@ -import {AnySerializable, Primitive, Serializable} from './api'; +import {Serializable} from './api'; -const isSerializable = (value: object | AnySerializable): value is Serializable => { - return value !== null && (value as Serializable).serialize instanceof Function; -}; +const isSerializable = (value: any): value is Serializable => value && value.serialize instanceof Function; -const isPrimitive = (value: object | AnySerializable): value is Primitive => { - return value === null || typeof value !== 'object'; -}; +const isPrimitive = (value: any) => value === null || typeof value !== 'object'; -const serialize = (value: any): AnySerializable => { +/** + * An agnostic serializer for design token components, producing a stable and noncircular + * representation of the data held in components. + */ +export const serialize = (value: T): any => { if (isSerializable(value)) { // Important! We must recursively serialize any subcomponents below. return serialize(value.serialize()); @@ -19,7 +19,7 @@ const serialize = (value: any): AnySerializable => { } if (Array.isArray(value)) { - return value.map((arrayValue) => serialize(arrayValue)) as AnySerializable; + return value.map(serialize); } const serialized: any = {}; @@ -28,20 +28,3 @@ const serialize = (value: any): AnySerializable => { } return serialized; }; - -/** - * Generically typed serializer for a given state shape. - * - * @typeparam T - The type of state we are expected to serialize. - */ -export class Serializer { - constructor (private readonly state: T) {} - - /** - * @ignore - * @todo - track dirty state and use an internal cache so we don't have to reserialize values that haven't changed. - */ - get payload (): {[property: string]: AnySerializable} { - return serialize(this.state) as {[property: string]: AnySerializable}; - } -} diff --git a/packages/engine/src/transitions.ts b/packages/engine/src/transitions.ts deleted file mode 100644 index cef231dcb..000000000 --- a/packages/engine/src/transitions.ts +++ /dev/null @@ -1,32 +0,0 @@ -import {Curve} from './api'; - -/** - * Given a time interval [t0, t1], returns the normalized value of t in [0, 1]. - */ -const normalizedProgress = (t0: number, t1: number, t: number) => { - if (t0 === t1) { - return 1; - } - - return (t - t0) / (t1 - t0); -}; - -/** - * Given a numeric value progression from v0 to v1, returns an intermediate value after [0, 1]-normalized progress. - * For example, at progress 0 the value is v0, at progress 1 the value is v1, and at progress 0.5 the value is the - * average of v0 and v1. - */ -const progression = (v0: number, v1: number, progress: number) => { - if (v0 === v1) { - return v1; - } - - return v0 + (v1 - v0) * progress; -}; - -/** - * Simple interpolator between numbers. - * @ignore - */ -export const interpolateNumbers = (v0: number, v1: number, t0: number, t1: number, t: number, curve: Curve): number => - progression(v0, v1, curve(normalizedProgress(t0, t1, t))); diff --git a/packages/engine/test/component.test.ts b/packages/engine/test/component.test.ts deleted file mode 100644 index 4b2d85227..000000000 --- a/packages/engine/test/component.test.ts +++ /dev/null @@ -1,317 +0,0 @@ -import {Serializable} from '../src/api'; -import {Component} from '../src/component'; -import {method, property, shared} from '../src/decorators'; -import {expression} from '../src/expression'; - -class FooString implements Serializable { - constructor (private readonly input: string) {} - - serialize () { - return `foo${this.input}`; - } -} - -interface FooState { - justbar: string; - foobar: FooString; -} - -class FooComponent extends Component {} - -describe('component', () => { - test('ticks on state changes', () => { - const patcher = jest.fn(); - const component = new FooComponent( - { - justbar: '', - foobar: new FooString('bar'), - }, - ); - - component.tick(0, patcher); - expect(patcher).toBeCalledTimes(1); - component.set({justbar: 'bar'}); - expect(patcher).toBeCalledTimes(1); - component.tick(1, patcher); - expect(patcher).toBeCalledTimes(2); - expect(patcher).toHaveBeenNthCalledWith(1, { - justbar: '', - foobar: 'foobar', - }); - expect(patcher).toHaveBeenNthCalledWith(2, { - justbar: 'bar', - foobar: 'foobar', - }); - }); - - test('serializes nested components', () => { - interface FooStateWrapper { - foo: Component; - } - - class FooWrapperComponent extends Component {} - - const component = new FooWrapperComponent({ - foo: new FooComponent({ - justbar: 'bar', - foobar: new FooString('bar'), - }), - }); - expect(component.serialize()).toEqual({ - foo: { - justbar: 'bar', - foobar: 'foobar', - }, - }); - }); - - test('can provide defaults via state decorators', () => { - class Foo extends Component { - @property justbar = 'barbar'; - - @property foobar = new FooString('barbaz'); - } - - interface FooStateWrapper { - foo: Foo; - } - - class FooWrapper extends Component { - @property foo = new Foo(); - } - - const component = new FooWrapper(); - - expect(component.serialize()).toEqual({ - foo: { - justbar: 'barbar', - foobar: 'foobarbaz', - }, - }); - }); - - test('child components dirty parents', () => { - class Foo extends Component { - @property foobar = new FooString('bar'); - } - - interface FooStateWrapper { - foo: Foo; - } - - class FooWrapper extends Component { - @property foo = new Foo(); - } - - const patcher = jest.fn(); - const component = new FooWrapper({}); - component.dirty(); - component.tick(0, patcher); - expect(patcher).toBeCalledTimes(1); - expect(patcher).toHaveBeenNthCalledWith(1, { - foo: { - foobar: 'foobar', - }, - }); - component.foo.set({justbar: 'bar'}); - component.tick(1, patcher); - expect(patcher).toBeCalledTimes(2); - expect(patcher).toHaveBeenNthCalledWith(2, { - foo: { - justbar: 'bar', - foobar: 'foobar', - }, - }); - }); - - test('can provide global listeners via listener decorators', () => { - const helloFn = jest.fn(); - class Foo extends Component { - @method hello (payload: string) { - helloFn(payload); - } - } - - const component = new Foo(); - component.trigger('hello', 'hello'); - - expect(helloFn).toHaveBeenCalledTimes(1); - expect(helloFn).toHaveBeenCalledWith('hello'); - component.trigger('goodbye', 'noop'); - expect(helloFn).toHaveBeenCalledTimes(1); - component.trigger('hello', 'hello again!'); - expect(helloFn).toHaveBeenCalledTimes(2); - expect(helloFn).toHaveBeenCalledWith('hello again!'); - }); - - test('can provide expressions that proxy to their returned values', () => { - interface DerivableState { - f: string; - fo: string; - foo: string; - foobar: string; - } - - class Derivable extends Component { - @property f: string = 'f'; - - @property fo: string = expression((f: string) => `${f}o`); - - @property foo: string = expression((fo: string) => `${fo}o`); - - @property foobar: string = expression((foo: string) => `${foo}bar`); - } - - const component = new Derivable(); - expect(component.serialize()).toEqual({ - f: 'f', - fo: 'fo', - foo: 'foo', - foobar: 'foobar', - }); - - component.f += 'oobarf'; - expect(component.serialize()).toEqual({ - f: 'foobarf', - fo: 'foobarfo', - foo: 'foobarfoo', - foobar: 'foobarfoobar', - }); - - // Note how we can use `component.foobar` like a string, even though it's an expression! - expect(component.foobar.substr(0, 6)).toBe('foobar'); - expect(component.foobar.substr(0, 6).split('')).toEqual(['f', 'o', 'o', 'b', 'a', 'r']); - }); - - test('expression component serialization', () => { - class Child extends Component { - @property a = 'a'; - } - - class Parent extends Component { - @property child = new Child(); - @property alsoChild = expression((child: Child) => child); - } - - const component = new Parent(); - expect(component.serialize()).toEqual({ - child: {a: 'a'}, - alsoChild: {a: 'a'}, - }); - }); - - test('can use shared bindings to evaluate expressions from child components', () => { - interface AdderState { - sum: number; - } - - class Adder extends Component { - @shared a!: number; - @shared b!: number; - @shared c!: number; - - @property sum = expression( - (a: number, b: number, c: number) => a + b + c, - ); - } - - interface MultiplierState { - product: number; - } - - class Multiplier extends Component { - @shared a!: number; - @shared b!: number; - @shared c!: number; - - @property product = expression( - (a: number, b: number, c: number) => a * b * c, - ); - } - - interface NumberBagState { - a: number; - b: number; - c: number; - - adder: Adder; - multiplier: Multiplier; - } - - class NumberBag extends Component { - @property a = 1; - @property b = 3; - @property c = 5; - @property adder = new Adder(); - @property multiplier = new Multiplier(); - } - - const component = new NumberBag(); - expect(component.serialize()).toEqual({ - a: 1, - b: 3, - c: 5, - adder: { - sum: 9, - }, - multiplier: { - product: 15, - }, - }); - - component.a = 2; - expect(component.serialize()).toEqual({ - a: 2, - b: 3, - c: 5, - adder: { - sum: 10, - }, - multiplier: { - product: 30, - }, - }); - - component.b *= 2; - expect(component.serialize()).toEqual({ - a: 2, - b: 6, - c: 5, - adder: { - sum: 13, - }, - multiplier: { - product: 60, - }, - }); - - // Note how we can use `sum` like a number, even though it's an expression! - expect(component.adder.sum.toFixed(3)).toBe('13.000'); - }); - - test('invalid shared usage', () => { - class InvalidAssignment extends Component { - @shared a = 12; - } - - expect(() => new InvalidAssignment()).toThrow(); - - class InvalidReference extends Component { - @shared a!: number; - - @property b = expression((a: number) => a); - } - - expect(() => (new InvalidReference()).serialize()).toThrow(); - }); - - test('property options', () => { - class Options extends Component { - // @ts-ignore - @property({foo: 'bar'}) foo = 'bar'; - } - - const options = new Options(); - expect(options.boundStates.get('foo')).toEqual({foo: 'bar'}); - }); -}); diff --git a/packages/engine/test/prefab.test.ts b/packages/engine/test/prefab.test.ts new file mode 100644 index 000000000..3a71906ff --- /dev/null +++ b/packages/engine/test/prefab.test.ts @@ -0,0 +1,50 @@ +import {prefab} from '../src/prefab'; + +interface FooStringData { + value: string; +} + +class FooString extends prefab() { + defaults = { + value: '', + }; + + sanitize ({value}: FooStringData) { + return {value: `foo${value}`}; + } +} + +interface FooData { + justbar: string[]; + foobar: FooString; +} + +class FooPrefab extends prefab() { + defaults = { + justbar: ['bar'], + foobar: new FooString(), + }; +} + +describe('prefab', () => { + test('serializes through sanitized inputs and nested subcomponents', () => { + const plain = new FooPrefab(); + expect(plain.serialize()).toEqual({ + justbar: ['bar'], + foobar: {value: 'foo'}, + }); + + const someFooString = new FooString({value: 'bar'}); + const overridden = new FooPrefab({ + justbar: ['bar', 'bar'], + foobar: someFooString, + }); + expect(overridden.serialize()).toEqual({ + justbar: ['bar', 'bar'], + foobar: {value: 'foobar'}, + }); + + // Confirm that we can access component data like regular properties. + expect(overridden.foobar).toBe(someFooString); + }); +}); diff --git a/packages/engine/test/serialization.test.ts b/packages/engine/test/serialization.test.ts index f2299fe98..022ce009d 100644 --- a/packages/engine/test/serialization.test.ts +++ b/packages/engine/test/serialization.test.ts @@ -1,7 +1,7 @@ import {Serializable} from '../src/api'; -import {Serializer} from '../src/serialization'; +import {serialize} from '../src/serialization'; -class FooString implements Serializable { +class FooString implements Serializable { constructor (private readonly input: string) {} serialize () { @@ -11,18 +11,12 @@ class FooString implements Serializable { describe('serialization', () => { test('payload', () => { - interface SerializableState { - justbar: string; - foobar: FooString[]; - } - - const state: SerializableState = { + const values = { justbar: 'bar', foobar: [new FooString('bar')], }; - const serializer = new Serializer(state); - expect(serializer.payload).toEqual({ + expect(serialize(values)).toEqual({ justbar: 'bar', foobar: ['foobar'], }); diff --git a/packages/engine/test/transitions.test.ts b/packages/engine/test/transitions.test.ts deleted file mode 100644 index cb0f14b33..000000000 --- a/packages/engine/test/transitions.test.ts +++ /dev/null @@ -1,27 +0,0 @@ -import {interpolateNumbers} from '../src/transitions'; - -describe('transitions', () => { - test('interpolateNumbers', () => { - const linearCurve = (t: number) => t; - const sineCurve = (t: number) => Math.sin(Math.PI / 2 * t); - - expect(interpolateNumbers(0, 0, 0, 0, 0, linearCurve)).toBe(0); - expect(interpolateNumbers(0, 0, 0, 100, 0, linearCurve)).toBe(0); - - expect(interpolateNumbers(0, 100, 0, 100, 0, linearCurve)).toBe(0); - expect(interpolateNumbers(0, 100, 0, 100, 50, linearCurve)).toBe(50); - expect(interpolateNumbers(0, 100, 0, 100, 100, linearCurve)).toBe(100); - - expect(interpolateNumbers(100, 200, 100, 200, 100, linearCurve)).toBe(100); - expect(interpolateNumbers(100, 200, 100, 200, 150, linearCurve)).toBe(150); - expect(interpolateNumbers(100, 200, 100, 200, 200, linearCurve)).toBe(200); - - expect(interpolateNumbers(0, 1000, 0, 100, 0, linearCurve)).toBe(0); - expect(interpolateNumbers(0, 1000, 0, 100, 50, linearCurve)).toBe(500); - expect(interpolateNumbers(0, 1000, 0, 100, 100, linearCurve)).toBe(1000); - - expect(interpolateNumbers(0, 100, 0, 100, 0, sineCurve)).toBe(0); - expect(interpolateNumbers(0, 100, 0, 100, 50, sineCurve)).toBe(100 * Math.sin(Math.PI / 4)); - expect(interpolateNumbers(0, 100, 0, 100, 100, sineCurve)).toBe(100); - }); -}); diff --git a/packages/engine/test/tweens.test.ts b/packages/engine/test/tweens.test.ts deleted file mode 100644 index ddee9eea2..000000000 --- a/packages/engine/test/tweens.test.ts +++ /dev/null @@ -1,129 +0,0 @@ -import {Component} from '../src/component'; -import {method, property} from '../src/decorators'; - -interface FooState { - aNumber: number; - anotherNumber: number; -} - -class Foo extends Component { - @property aNumber = 1; - @property anotherNumber = 2; -} - -interface BarState { - myNumber: number; - foo: Foo; -} - -class Bar extends Component { - @property myNumber = 3; - - @property foo = new Foo(); - - @method async doSomething () { - await this.tween({myNumber: 4}, {duration: 1000}); - await this.foo.tween( - {aNumber: 2, anotherNumber: 4}, - {duration: 1000}, - ); - await this.tween( - {foo: new Foo({aNumber: 3, anotherNumber: 4})}, - {duration: 1000}, - ); - } - - @method async doSomethingTrivial () { - await this.tween({myNumber: 3}, {duration: 0}); - } -} - -const component = new Bar(); -const originalFoo = component.foo; - -describe('tweens', () => { - // The tests below mimic the runtime mechanics of ticking periodically from an external event loop. - // To allow the tween promises to resolve before the next tick, each subsequent transition is tested - // in a subsequent test. - test('simple numeric tween', () => { - component.tick(0); // Start the clock at 0. - component.doSomethingTrivial(); - expect(component.serialize()).toEqual({myNumber: 3, foo: {aNumber: 1, anotherNumber: 2}}); - // Tick 1000 units, but without any tweens firing. Expected: unchanged. - component.tick(1000); - expect(component.serialize()).toEqual({myNumber: 3, foo: {aNumber: 1, anotherNumber: 2}}); - component.doSomething(); - component.tick(1000); - // Expected: unchanged (the time hasn't progressed from the last tick). - expect(component.serialize()).toEqual({myNumber: 3, foo: {aNumber: 1, anotherNumber: 2}}); - component.tick(1500); - // Expected: 50% through myNumber transition from 3 to 4. - expect(component.serialize()).toEqual({myNumber: 3.5, foo: {aNumber: 1, anotherNumber: 2}}); - component.tick(2000); - // Expected: 100% through myNumber transition from 3 to 4. - expect(component.serialize()).toEqual({myNumber: 4, foo: {aNumber: 1, anotherNumber: 2}}); - }); - - test('double-valued numeric tween', () => { - // At this point, a new double-valued Foo-transition frpm {1, 2} to {2, 4} has begun. - component.tick(2500); - expect(component.foo.serialize()).toEqual({aNumber: 1.5, anotherNumber: 3}); - component.tick(3000); - expect(component.foo.serialize()).toEqual({aNumber: 2, anotherNumber: 4}); - }); - - test('full component tween', () => { - // At this point, a new full-component transition from {2, 4} to {3, 4} has begun. - component.tick(3500); - expect(component.foo.serialize()).toEqual({aNumber: 2.5, anotherNumber: 4}); - component.tick(4000); - expect(component.foo.serialize()).toEqual({aNumber: 3, anotherNumber: 4}); - // Important: we shouldn't have actually reassigned foo. - expect(component.foo).toBe(originalFoo); - }); - - const interruptedComponent = new Bar(); - test('interruption', () => { - interruptedComponent.tick(0); // Start the clock at 0. - expect(interruptedComponent.serialize()).toEqual({myNumber: 3, foo: {aNumber: 1, anotherNumber: 2}}); - // Tick 1000 units, but without any tweens firing. Expected: unchanged. - interruptedComponent.tick(1000); - expect(interruptedComponent.myNumber).toBe(3); - interruptedComponent.doSomething(); - interruptedComponent.tick(1000); - // Expected: unchanged (the time hasn't progressed from the last tick). - expect(interruptedComponent.myNumber).toBe(3); - interruptedComponent.tick(1500); - // Expected: 50% through myNumber transition from 3 to 4. - expect(interruptedComponent.myNumber).toBe(3.5); - - // INTERRUPT! - interruptedComponent.doSomething(); - // Expected: unchanged. - expect(interruptedComponent.myNumber).toBe(3.5); - interruptedComponent.tick(2000); - // Expected: 50% through myNumber transition from 3.5 to 4. - expect(interruptedComponent.myNumber).toBe(3.75); - - // INTERRUPT! - interruptedComponent.doSomething(); - // Expected: unchanged. - expect(interruptedComponent.myNumber).toBe(3.75); - interruptedComponent.tick(2500); - // Expected: 50% through myNumber transition from 3.75 to 4. - expect(interruptedComponent.myNumber).toBe(3.875); - - // INTERRUPT! - interruptedComponent.doSomething(); - // Expected: unchanged. - expect(interruptedComponent.myNumber).toBe(3.875); - interruptedComponent.tick(3000); - // Expected: 50% through myNumber transition from 3.875 to 4. - expect(interruptedComponent.myNumber).toBe(3.9375); - interruptedComponent.tick(3500); - // Expected: 100% through myNumber transition from 3.875 to 4. - expect(interruptedComponent.myNumber).toBe(4); - // Important: none of the this.foo.tween(...) calls should have fired. - expect(interruptedComponent.serialize()).toEqual({myNumber: 4, foo: {aNumber: 1, anotherNumber: 2}}); - }); -}); diff --git a/packages/generation/src/linear-gradient.ts b/packages/generation/src/linear-gradient.ts index 650fdcb6c..1600d7a92 100644 --- a/packages/generation/src/linear-gradient.ts +++ b/packages/generation/src/linear-gradient.ts @@ -1,23 +1,23 @@ -interface Point2D { +interface GeneratedPoint2D { x: number; y: number; } -interface GradientStop { +interface GeneratedGradientStop { position: number; colorInitializer: string; } const FloatPrecision = 6; -const getPoint2DInitializer = (point: Point2D) => +const getPoint2DInitializer = (point: GeneratedPoint2D) => `Point2D.make(${point.x.toFixed(FloatPrecision)}, ${point.y.toFixed(FloatPrecision)})`; /** * Returns a linear gradient initializer. * @ignore */ -export const getLinearGradientInitializer = (stops: GradientStop[], start: Point2D, end: Point2D) => { +export const getLinearGradientInitializer = (stops: GeneratedGradientStop[], start: GeneratedPoint2D, end: GeneratedPoint2D) => { const colorStopInitializers = stops.map((stop) => { return `GradientStop.make(${stop.position.toFixed(FloatPrecision)}, ${stop.colorInitializer})`; }); diff --git a/packages/generation/src/utils.ts b/packages/generation/src/utils.ts index c08cb8122..867420faa 100644 --- a/packages/generation/src/utils.ts +++ b/packages/generation/src/utils.ts @@ -154,7 +154,6 @@ export const codegenDesignSystem = async (spec: CodegenDesignSystem) => { const project = getProject(projectRoot); const sourceFile = project.createSourceFile(spec.filename, '', {overwrite: true}); - const engineImports = new Set(['Component']); const designSystemImports = new Set(); const colorsName = localResolver.getComponentName(`${designSystemName} Colors`); const gradientsName = localResolver.getComponentName(`${designSystemName} Gradients`); @@ -165,49 +164,41 @@ export const codegenDesignSystem = async (spec: CodegenDesignSystem) => { const hasTypographs = spec.typographs.length > 0; if (hasColors) { - engineImports.add('property'); designSystemImports.add('Color'); sourceFile.addClass({ name: colorsName, - extends: 'Component', properties: spec.colors.map(({name, initializer}) => { const colorName = localResolver.getPropertyName(name || 'Untitled Color', colorsName); return { initializer, name: colorName, - decorators: [{name: 'property'}], }; }), }); } if (hasGradients) { - engineImports.add('property'); designSystemImports.add('LinearGradient'); designSystemImports.add('Color'); designSystemImports.add('GradientStop'); designSystemImports.add('Point2D'); sourceFile.addClass({ name: gradientsName, - extends: 'Component', properties: spec.gradients.map(({name, initializer}) => { const gradientName = localResolver.getPropertyName(name || 'Untitled Linear Gradient', gradientsName); return { initializer, name: gradientName, - decorators: [{name: 'property'}], }; }), }); } if (hasTypographs) { - engineImports.add('property'); designSystemImports.add('Color'); designSystemImports.add('Typograph'); sourceFile.addClass({ name: typographsName, - extends: 'Component', properties: spec.typographs.map(({name, initializer}) => { const typographName = localResolver.getPropertyName( name || 'Untitled Typograph', @@ -216,7 +207,6 @@ export const codegenDesignSystem = async (spec: CodegenDesignSystem) => { return { initializer, name: typographName, - decorators: [{name: 'property'}], }; }), }); @@ -298,13 +288,11 @@ export const codegenDesignSystem = async (spec: CodegenDesignSystem) => { const exportedClassDeclaration = sourceFile.addClass({ isExported: true, name: componentName, - extends: 'Component', }); if (hasColors) { exportedClassDeclaration.addProperty({ name: 'colors', - decorators: [{name: 'property'}], initializer: `new ${colorsName}()`, }); } @@ -312,7 +300,6 @@ export const codegenDesignSystem = async (spec: CodegenDesignSystem) => { if (hasGradients) { exportedClassDeclaration.addProperty({ name: 'gradients', - decorators: [{name: 'property'}], initializer: `new ${gradientsName}()`, }); } @@ -321,7 +308,6 @@ export const codegenDesignSystem = async (spec: CodegenDesignSystem) => { designSystemImports.add('Font'); exportedClassDeclaration.addProperty({ name: 'typographs', - decorators: [{name: 'property'}], initializer: `new ${typographsName}()`, }); } @@ -342,10 +328,5 @@ export const codegenDesignSystem = async (spec: CodegenDesignSystem) => { }); } - sourceFile.addImportDeclaration({ - moduleSpecifier: '@diez/engine', - namedImports: Array.from(engineImports).sort().map((name) => ({name})), - }); - return sourceFile.save(); }; diff --git a/packages/generation/test/goldens/codegennable/src/index.ts b/packages/generation/test/goldens/codegennable/src/index.ts index b9d551b8a..8f0ecb188 100644 --- a/packages/generation/test/goldens/codegennable/src/index.ts +++ b/packages/generation/test/goldens/codegennable/src/index.ts @@ -1,24 +1,17 @@ import { Color, File, Font, GradientStop, Image, LinearGradient, Point2D, Typograph } from "@diez/prefabs"; -import { Component, property } from "@diez/engine"; -class MyDesignSystemColors extends Component { - @property +class MyDesignSystemColors { untitledColor = 2; - @property someColor = 3; } -class MyDesignSystemGradients extends Component { - @property +class MyDesignSystemGradients { untitledLinearGradient = 4; - @property someGradient = 5; } -class MyDesignSystemTypographs extends Component { - @property +class MyDesignSystemTypographs { untitledTypograph = 0; - @property someTypograph = 1; } @@ -45,12 +38,9 @@ export const MyDesignSystemFonts = { } }; -export class MyDesignSystemTokens extends Component { - @property +export class MyDesignSystemTokens { colors = new MyDesignSystemColors(); - @property gradients = new MyDesignSystemGradients(); - @property typographs = new MyDesignSystemTypographs(); } diff --git a/packages/prefabs/src/color.ts b/packages/prefabs/src/color.ts index 6305b9d47..bdb6c96b8 100644 --- a/packages/prefabs/src/color.ts +++ b/packages/prefabs/src/color.ts @@ -1,10 +1,9 @@ -import {Component, HashMap, property} from '@diez/engine'; +import {HashMap, prefab} from '@diez/engine'; /** - * Provides simple hue-saturation-lightness-alpha color state. - * @ignore + * Provides simple hue-saturation-lightness-alpha color data. */ -export interface ColorState { +export interface ColorData { h: number; s: number; l: number; @@ -55,11 +54,18 @@ const hexRgb = (r: number, g: number, b: number, a: number = 255) => Color.rgba( * * @noinheritdoc */ -export class Color extends Component { +export class Color extends prefab() { + defaults = { + h: 0, + s: 0, + l: 0, + a: 1, + }; + /** * Creates an RGBA color. * - * `@property red = Color.rgba(255, 0, 0, 1);` + * `red = Color.rgba(255, 0, 0, 1);` */ static rgba (rIn: number, gIn: number, bIn: number, a: number) { const r = rIn / 255; @@ -76,7 +82,7 @@ export class Color extends Component { /** * Creates an RGB color. * - * `@property red = Color.rgb(255, 0, 0);` + * `red = Color.rgb(255, 0, 0);` */ static rgb (r: number, g: number, b: number) { return Color.rgba(r, g, b, 1); @@ -85,7 +91,7 @@ export class Color extends Component { /** * Creates an HSLA color. * - * `@property red = Color.hsla(0, 1, 0.5, 1);` + * `red = Color.hsla(0, 1, 0.5, 1);` */ static hsla (h: number, s: number, l: number, a: number) { return new Color({h, s, l, a}); @@ -94,7 +100,7 @@ export class Color extends Component { /** * Creates an HSL color. * - * `@property red = Color.hsl(0, 1, 0.5);` + * `red = Color.hsl(0, 1, 0.5);` */ static hsl (h: number, s: number, l: number) { return Color.hsla(h, s, l, 1); @@ -103,7 +109,7 @@ export class Color extends Component { /** * Creates a color from a hex code * - * `@property red = Color.hex('#ff0');` + * `red = Color.hex('#ff0');` * * 3, 4, 6, and 8 character hex specifications are supported. `#ff0`, `#ff0f`, `#ffff00`, and `#ffff00ff` should * all work. @@ -126,31 +132,22 @@ export class Color extends Component { return new Color(); } - @property h = 0; - - @property s = 0; - - @property l = 0; - - @property a = 1; - /** - * @ignore + * Ensures all values are normalized in [0, 1] before serialization. */ - serialize () { - // Important: ensure all values are normalized in [0, 1] before serializing. + sanitize (data: ColorData) { return { - h: normalizeUnit(this.h), - s: normalizeUnit(this.s), - l: normalizeUnit(this.l), - a: normalizeUnit(this.a), + h: normalizeUnit(data.h), + s: normalizeUnit(data.s), + l: normalizeUnit(data.l), + a: normalizeUnit(data.a), }; } /** * Lightens a color by the specified amount. * - * `@property pink = this.red.lighten(0.5);` + * `pink = this.red.lighten(0.5);` * * @returns A new Color instance after applying the specified lightener. */ @@ -161,7 +158,7 @@ export class Color extends Component { /** * Darkens a color by the specified amount. * - * `@property maroon = this.red.darken(0.25);` + * `maroon = this.red.darken(0.25);` * * @returns A new Color instance after applying the specified darkener. */ @@ -172,7 +169,7 @@ export class Color extends Component { /** * Saturates a color by the specified amount. * - * `@property bloodRed = this.mediumRed.saturate(0.25);` + * `bloodRed = this.mediumRed.saturate(0.25);` * * @returns A new Color instance after applying the specified saturater. */ @@ -183,7 +180,7 @@ export class Color extends Component { /** * Desaturates a color by the specified amount * - * `@property grayRed = this.mediumRed.desaturate(0.25);` + * `grayRed = this.mediumRed.desaturate(0.25);` * * @returns A new Color instance after applying the specified desaturater. */ diff --git a/packages/prefabs/src/file.ts b/packages/prefabs/src/file.ts index 97b34a9af..bde68b76c 100644 --- a/packages/prefabs/src/file.ts +++ b/packages/prefabs/src/file.ts @@ -1,4 +1,4 @@ -import {Component, property} from '@diez/engine'; +import {prefab} from '@diez/engine'; /** * The type of a file resource. @@ -10,10 +10,9 @@ export enum FileType { } /** - * File state. - * @ignore + * File data. */ -export interface FileState { +export interface FileData { src: string; type: FileType; } @@ -24,22 +23,20 @@ export interface FileState { * * The compiler may enforce certain restrictions on the `type` of a `File` instance. * - * Usage: `@property file = new File({src: 'assets/images/file.jpg', type: FileType.Image});`. + * Usage: `file = new File({src: 'assets/images/file.jpg', type: FileType.Image});`. * * @noinheritdoc */ -export class File extends Component { - @property src = ''; +export class File extends prefab() { + defaults = { + src: '', + type: FileType.Raw, + }; - @property type = FileType.Raw; - - /** - * @ignore - */ - serialize () { + protected sanitize (data: FileData) { return { - src: encodeURI(this.src), - type: this.type, + src: encodeURI(data.src), + type: data.type, }; } } diff --git a/packages/prefabs/src/image.ts b/packages/prefabs/src/image.ts index 15de200fd..2b20f1cc0 100644 --- a/packages/prefabs/src/image.ts +++ b/packages/prefabs/src/image.ts @@ -1,17 +1,16 @@ -import {Component, Integer, property, Target} from '@diez/engine'; +import {Integer, prefab, Target} from '@diez/engine'; import {File, FileType} from './file'; /** - * Responsive image state. - * @ignore + * Responsive image data. */ -export interface ImageState { +export interface ImageData { file: File; file2x: File; file3x: File; file4x: File; - width: number; - height: number; + width: Integer; + height: Integer; } /** @@ -22,7 +21,7 @@ export interface ImageState { * * @noinheritdoc */ -export class Image extends Component { +export class Image extends prefab() { /** * Yields a raster image according to the convention that files should be located in the same directory using the * same filename prefix. For example: @@ -37,7 +36,7 @@ export class Image extends Component { * * can be specified with: * - * `@property image = Image.responsive('assets/filename.png', 640, 480);` + * `image = Image.responsive('assets/filename.png', 640, 480);` */ static responsive (src: string, width: number = 0, height: number = 0) { const pathComponents = src.split('/'); @@ -56,26 +55,26 @@ export class Image extends Component { }); } - @property file = new File({type: FileType.Image}); + defaults = { + file: new File({type: FileType.Image}), + file2x: new File({type: FileType.Image}), + file3x: new File({type: FileType.Image}), + file4x: new File({type: FileType.Image}), + width: 0, + height: 0, + }; - @property file2x = new File({type: FileType.Image}); + options = { + file4x: { + targets: [Target.Android], + }, + }; - @property file3x = new File({type: FileType.Image}); - - @property({targets: [Target.Android]}) file4x = new File({type: FileType.Image}); - - @property width: Integer = 0; - - @property height: Integer = 0; - - /** - * @ignore - */ - serialize () { + protected sanitize (data: ImageData) { return { - ...super.serialize(), - width: Math.round(this.width), - height: Math.round(this.height), + ...data, + width: Math.round(data.width), + height: Math.round(data.height), }; } } diff --git a/packages/prefabs/src/linear-gradient.ts b/packages/prefabs/src/linear-gradient.ts index a8e3eb906..95f5e46c9 100644 --- a/packages/prefabs/src/linear-gradient.ts +++ b/packages/prefabs/src/linear-gradient.ts @@ -1,13 +1,19 @@ -import {Component, property} from '@diez/engine'; +import {prefab} from '@diez/engine'; import {Color} from './color'; -import {Point2D, Point2DState} from './point2d'; +import {Point2D, Point2DData} from './point2d'; /** - * GradientStop state. - * @ignore + * GradientStop data. */ -export interface GradientStopState { +export interface GradientStopData { + /** + * The position of this color within a gradient as percentage value where 1.0 is 100%. The stop position can be less + * than 0 or greater than 1. + */ position: number; + /** + * The color at this stop position within a gradient. + */ color: Color; } @@ -16,16 +22,11 @@ export interface GradientStopState { * * @noinheritdoc */ -export class GradientStop extends Component { - /** - * The position of this color within a gradient as percentage value where 1.0 is 100%. The stop position can be less - * than 0 or greater than 1. - */ - @property position = 0; - /** - * The color at this stop position within a gradient. - */ - @property color = Color.rgb(0, 0, 0); +export class GradientStop extends prefab() { + defaults = { + position: 0, + color: Color.rgb(0, 0, 0), + }; /** * Creates an gradient stop. @@ -37,20 +38,10 @@ export class GradientStop extends Component { } } -/** - * LinearGradient state. - * @ignore - */ -export interface LinearGradientState { - stops: GradientStop[]; - start: Point2D; - end: Point2D; -} - /** * The direction of a linear gradient relative to the containing view's edges. */ -export enum Toward { +export const enum Toward { Top = 0, TopRight = 45, Right = 90, @@ -64,7 +55,7 @@ export enum Toward { /** * Gets the length of a CSS linear gradient line. * - * See https://drafts.csswg.org/css-images-3/#funcdef-linear-gradient + * @see {@link https://drafts.csswg.org/css-images-3/#funcdef-linear-gradient} */ export const cssLinearGradientLength = (angle: number) => Math.abs(Math.sin(angle)) + Math.abs(Math.cos(angle)); @@ -79,7 +70,7 @@ export const cssLinearGradientLength = (angle: number) => * @returns The `start` and `end` points of a line in a coordinate space where positive x is to the right and positive * y is downward. */ -export const linearGradientStartAndEndPoints = (angle: number, lineLength: number, center: Point2DState) => { +export const linearGradientStartAndEndPoints = (angle: number, lineLength: number, center: Point2DData) => { const differenceVector = { x: Math.sin(angle) * lineLength / 2, y: Math.cos(angle) * lineLength / 2, @@ -100,7 +91,7 @@ const FloatPrecision = 6; const roundFloat = (value: number) => parseFloat(value.toFixed(FloatPrecision)); -const roundPoint = (point: Point2DState) => { +const roundPoint = (point: Point2DData) => { return { x: roundFloat(point.x), y: roundFloat(point.y), @@ -124,29 +115,40 @@ const stopsFromColors = (...colors: Color[]) => { }; /** - * Provides a linear gradient. - * - * @noinheritdoc + * LinearGradient data. */ -export class LinearGradient extends Component { +export interface LinearGradientData { /** * The color stops within the gradient. * * The position of a stop is represented as a percentage value where 1.0 is 100%. The stop position can be less than * 0 or greater than 1. */ - @property stops = [ - GradientStop.make(0, Color.rgb(0, 0, 0)), - GradientStop.make(1, Color.rgb(255, 255, 255)), - ]; + stops: GradientStop[]; /** * The start position of the gradient in a coordinate space where (0, 0) is top left and (1, 1) is bottom right. */ - @property start = Point2D.make(0, 0); + start: Point2D; /** * The end position of the gradient in a coordinate space where (0, 0) is top left and (1, 1) is bottom right. */ - @property end = Point2D.make(1, 1); + end: Point2D; +} + +/** + * Provides a linear gradient. + * + * @noinheritdoc + */ +export class LinearGradient extends prefab() { + defaults = { + stops: [ + GradientStop.make(0, Color.rgb(0, 0, 0)), + GradientStop.make(1, Color.rgb(255, 255, 255)), + ], + start: Point2D.make(0, 0), + end: Point2D.make(1, 1), + }; /** * Constructs a linear gradient using an angle in degrees, or a [[Toward]] value, that specifies the direction of the @@ -156,7 +158,7 @@ export class LinearGradient extends Component { * [[Toward]] value (e.g. `Toward.TopRight`). * @param colors: The colors that make up the gradient. * - * `@property gradient = LinearGradient.make(Toward.TopRight, Color.rgb(255, 0, 0), Color.rgb(0, 0, 255));` + * `gradient = LinearGradient.make(Toward.TopRight, Color.rgb(255, 0, 0), Color.rgb(0, 0, 255));` */ static make (angle: Toward | number, ...colors: Color[]) { const {start, end} = pointsFromAngle(angle); @@ -174,7 +176,7 @@ export class LinearGradient extends Component { * (0, 0) represents the top left. * (1, 1) represents the bottom right. * - * `@property gradient = LinearGradient.makeWithPoints(0, 0, 1, 1, Color.rgb(255, 0, 0), Color.rgb(0, 0, 255));` + * `gradient = LinearGradient.makeWithPoints(0, 0, 1, 1, Color.rgb(255, 0, 0), Color.rgb(0, 0, 255));` */ static makeWithPoints (x1: number, y1: number, x2: number, y2: number, ...colors: Color[]) { const stops = stopsFromColors(...colors); diff --git a/packages/prefabs/src/lottie.ts b/packages/prefabs/src/lottie.ts index 431d61959..40c60107d 100644 --- a/packages/prefabs/src/lottie.ts +++ b/packages/prefabs/src/lottie.ts @@ -1,11 +1,10 @@ -import {Component, property} from '@diez/engine'; +import {prefab} from '@diez/engine'; import {File} from './file'; /** - * Lottie state. - * @ignore + * Lottie data. */ -export interface LottieState { +export interface LottieData { file: File; loop: boolean; autoplay: boolean; @@ -16,10 +15,12 @@ export interface LottieState { * * @noinheritdoc */ -export class Lottie extends Component { - @property file: File = new File(); - @property loop = true; - @property autoplay = true; +export class Lottie extends prefab() { + defaults = { + file: new File(), + loop: true, + autoplay: true, + }; /** * Creates a Lottie component from a source file, e.g. diff --git a/packages/prefabs/src/point2d.ts b/packages/prefabs/src/point2d.ts index 437af303b..c37e2c917 100644 --- a/packages/prefabs/src/point2d.ts +++ b/packages/prefabs/src/point2d.ts @@ -1,11 +1,16 @@ -import {Component, property} from '@diez/engine'; +import {prefab} from '@diez/engine'; /** - * Point state. - * @ignore + * Point data. */ -export interface Point2DState { +export interface Point2DData { + /** + * The x value of the point. + */ x: number; + /** + * The y value of the point. + */ y: number; } @@ -16,19 +21,15 @@ export interface Point2DState { * context of other prefabs like [[LinearGradient]], points typically should use the standard two dimensional graphics * space, often normalized in the unit square, where x increases from left to right and y increases from top to bottom. * - * Usage: `@property point = Point2D.make(0.5, 0.5);`. + * Usage: `point = Point2D.make(0.5, 0.5);`. * * @noinheritdoc */ -export class Point2D extends Component { - /** - * The x value of the point. - */ - @property x = 0; - /** - * The y value of the point. - */ - @property y = 0; +export class Point2D extends prefab() { + defaults = { + x: 0, + y: 0, + }; /** * Creates a two dimensional point. diff --git a/packages/prefabs/src/typography.ts b/packages/prefabs/src/typography.ts index 273f88a8e..d913440bd 100644 --- a/packages/prefabs/src/typography.ts +++ b/packages/prefabs/src/typography.ts @@ -1,19 +1,7 @@ -import {Component, property, Target} from '@diez/engine'; +import {prefab, Target} from '@diez/engine'; import {Color} from './color'; import {File, FileType} from './file'; -/** - * Font state. - * @ignore - */ -export interface FontState { - file: File; - name: string; - fallbacks: string[]; - weight: number; - style: FontStyle; -} - /** * Valid face forms for `@font-face` declarations in web. */ @@ -22,9 +10,6 @@ export enum FontStyle { Italic = 'italic', } -/** - * @internal - */ const inferNameFromPath = (src: string) => { const pathComponents = src.split('/'); const filename = pathComponents.pop() || ''; @@ -32,35 +17,50 @@ const inferNameFromPath = (src: string) => { }; /** - * A representation of a font resource, with a reference to a [[File]] containing a TTF or OTF font file. - * @noinheritdoc + * Font data. */ -export class Font extends Component { +export interface FontData { /** * The font file containing the font's definition. Due to target limitations, the file _must_ be a TrueType file * with a `.ttf` extension or an OpenType file with an `.otf` extension. */ - @property file = new File({type: FileType.Font}); - + file: File; /** * The exact, correct PostScript name of the font. */ - @property name = ''; - + name: string; /** * An array of fallback fonts (web only). */ - @property({targets: [Target.Web]}) fallbacks = ['sans-serif']; - + fallbacks: string[]; /** * The weight or boldness of the font (web only). */ - @property({targets: [Target.Web]}) weight = 400; - + weight: number; /** * The font style (web only). */ - @property({targets: [Target.Web]}) style = FontStyle.Normal; + style: FontStyle; +} + +/** + * A representation of a font resource, with a reference to a [[File]] containing a TTF or OTF font file. + * @noinheritdoc + */ +export class Font extends prefab() { + defaults = { + file: new File({type: FileType.Font}), + name: '', + fallbacks: ['sans-serif'], + weight: 400, + style: FontStyle.Normal, + }; + + options = { + fallbacks: {targets: [Target.Web]}, + weight: {targets: [Target.Web]}, + style: {targets: [Target.Web]}, + }; /** * Creates a Font component from a source file and its PostScript name. @@ -77,10 +77,9 @@ export class Font extends Component { } /** - * Typograph state. - * @ignore + * Typograph data. */ -export interface TypographState { +export interface TypographData { font: Font; fontSize: number; color: Color; @@ -92,10 +91,12 @@ export interface TypographState { * * @noinheritdoc */ -export class Typograph extends Component { - @property font = new Font(); - @property fontSize = 12; - @property color = Color.hsla(0, 0, 0, 1); +export class Typograph extends prefab() { + defaults = { + font: new Font(), + fontSize: 12, + color: Color.hsla(0, 0, 0, 1), + }; } /** diff --git a/packages/prefabs/test/linear-gradient.test.ts b/packages/prefabs/test/linear-gradient.test.ts index 68d07c7d8..f84d4beb3 100644 --- a/packages/prefabs/test/linear-gradient.test.ts +++ b/packages/prefabs/test/linear-gradient.test.ts @@ -95,9 +95,9 @@ describe('linear-gradient', () => { const colors = [ Color.hsl(0.25, 1, 0.5), ]; - const start = Point2D.make(0.0, 0); + const start = Point2D.make(0, 0); const end = Point2D.make(1, 1); - const gradient = LinearGradient.makeWithPoints(start.x, start.y, end.x, end.y, ...colors); + const gradient = LinearGradient.makeWithPoints(0, 0, 1, 1, ...colors); expect(gradient.serialize()).toEqual({ start: start.serialize(), end: end.serialize(), diff --git a/packages/prefabs/test/lottie.test.ts b/packages/prefabs/test/lottie.test.ts index 99dd78adc..918b34b79 100644 --- a/packages/prefabs/test/lottie.test.ts +++ b/packages/prefabs/test/lottie.test.ts @@ -4,7 +4,6 @@ describe('lottie', () => { test('basic functionality', () => { const src = 'lottie.json'; const image = Lottie.fromJson(src); - expect(image.file.src).toBe(src); expect(image.serialize()).toEqual({file: {src, type: 'raw'}, loop: true, autoplay: true}); }); }); diff --git a/packages/prefabs/test/typography.test.ts b/packages/prefabs/test/typography.test.ts index 719bc24ff..8ef08f43a 100644 --- a/packages/prefabs/test/typography.test.ts +++ b/packages/prefabs/test/typography.test.ts @@ -1,17 +1,10 @@ import {Color} from '../src/color'; import {Font, Typograph} from '../src/typography'; -describe('font', () => { - test('basic functionality', () => { - expect(Font.fromFile('Whatever.ttf', 'Bloop-MediumItalic').name).toBe('Bloop-MediumItalic'); - expect(Font.fromFile('Whatever.ttf').name).toBe('Whatever'); - }); -}); - describe('typograph', () => { test('basic functionality', () => { const typograph = new Typograph({ - font: Font.fromFile('Bloop-MediumItalic.ttf', 'Bloop-MediumItalic'), + font: Font.fromFile('Bloop-MediumItalic.ttf'), fontSize: 50, color: Color.hsla(0, 0, 0, 0.5), }); @@ -27,5 +20,23 @@ describe('typograph', () => { fontSize: 50, color: {h: 0, s: 0, l: 0, a: 0.5}, }); + + const typographWithSpecificName = new Typograph({ + font: Font.fromFile('Bloop-MediumItalic.ttf', 'SomethingElse'), + fontSize: 50, + color: Color.hsla(0, 0, 0, 0.5), + }); + + expect(typographWithSpecificName.serialize()).toEqual({ + font: { + file: {src: 'Bloop-MediumItalic.ttf', type: 'font'}, + name: 'SomethingElse', + style: 'normal', + weight: 400, + fallbacks: ['sans-serif'], + }, + fontSize: 50, + color: {h: 0, s: 0, l: 0, a: 0.5}, + }); }); }); diff --git a/packages/targets/src/asset-binders/file.ts b/packages/targets/src/asset-binders/file.ts index d5a5908c3..1d0a7a651 100644 --- a/packages/targets/src/asset-binders/file.ts +++ b/packages/targets/src/asset-binders/file.ts @@ -1,12 +1,11 @@ import {AssetBinder} from '@diez/compiler'; -import {ConcreteComponentType} from '@diez/engine'; import {File, FileType, Font, Image} from '@diez/prefabs'; // Note: we are careful to import the full module so we can monkey-patch it in our test harness. import fontkit from 'fontkit'; import {stat} from 'fs-extra'; import {extname, join} from 'path'; -const requireFileTypeForChild = (instance: File, fileType: FileType, type: ConcreteComponentType) => { +const requireFileTypeForChild = (instance: File, fileType: FileType, type: any) => { if (instance.host && instance.host instanceof type && instance.type !== fileType) { throw new Error(`${instance.host.constructor.name} file ${instance.src} does not specify file type ${fileType}.`); } diff --git a/packages/targets/src/targets/android.api.ts b/packages/targets/src/targets/android.api.ts index 070c3aa57..a803368b4 100644 --- a/packages/targets/src/targets/android.api.ts +++ b/packages/targets/src/targets/android.api.ts @@ -1,5 +1,5 @@ import {AssetBinding, TargetBinding, TargetOutput} from '@diez/compiler'; -import {Component} from '@diez/engine'; +import {Prefab} from '@diez/engine'; /** * Describes an Android third party dependency. @@ -15,7 +15,7 @@ export interface AndroidDependency { /** * Describes an Android binding. */ -export interface AndroidBinding extends TargetBinding { +export interface AndroidBinding = Prefab<{}>> extends TargetBinding { dependencies?: AndroidDependency[]; } diff --git a/packages/targets/src/targets/ios.api.ts b/packages/targets/src/targets/ios.api.ts index f25aa7b9e..e6b2ec92e 100644 --- a/packages/targets/src/targets/ios.api.ts +++ b/packages/targets/src/targets/ios.api.ts @@ -1,5 +1,5 @@ import {TargetBinding, TargetOutput} from '@diez/compiler'; -import {Component} from '@diez/engine'; +import {Prefab} from '@diez/engine'; declare module '@diez/compiler/types/api' { /** @@ -29,7 +29,7 @@ export interface IosDependency { /** * Describes an iOS binding. */ -export interface IosBinding extends TargetBinding { +export interface IosBinding = Prefab<{}>> extends TargetBinding { dependencies?: IosDependency[]; } diff --git a/packages/targets/src/targets/web.api.ts b/packages/targets/src/targets/web.api.ts index 4b5aea195..89b4dd631 100644 --- a/packages/targets/src/targets/web.api.ts +++ b/packages/targets/src/targets/web.api.ts @@ -1,5 +1,5 @@ import {TargetBinding, TargetOutput} from '@diez/compiler'; -import {Component} from '@diez/engine'; +import {Prefab} from '@diez/engine'; /** * Describes an Web third party dependency. @@ -14,7 +14,7 @@ export interface WebDependency { /** * Describes a Web binding. */ -export interface WebBinding extends TargetBinding { +export interface WebBinding = Prefab<{}>> extends TargetBinding { declarations?: string[]; dependencies?: WebDependency[]; } diff --git a/packages/targets/src/utils.ts b/packages/targets/src/utils.ts index dafc84d4b..13361fbae 100644 --- a/packages/targets/src/utils.ts +++ b/packages/targets/src/utils.ts @@ -1,5 +1,5 @@ import {CompilerOptions, TargetComponentProperty} from '@diez/compiler'; -import {Color} from '@diez/prefabs'; +import {ColorData} from '@diez/prefabs'; import {kebabCase} from 'change-case'; import {resolve} from 'path'; @@ -22,7 +22,7 @@ export const onlyTarget = (option: keyof CompilerOptions, options: CompilerOptio * Returns a string with a valid CSS value from a Color prefab instance. * @ignore */ -export const colorToCss = ({h, s, l, a}: Color) => `hsla(${h * 360}, ${s * 100}%, ${l * 100}%, ${a})`; +export const colorToCss = ({h, s, l, a}: ColorData) => `hsla(${h * 360}, ${s * 100}%, ${l * 100}%, ${a})`; /** * Casts to `string` and joins all arguments in kebab-case. diff --git a/packages/targets/test/fixtures/Bindings/Bindings.ts b/packages/targets/test/fixtures/Bindings/Bindings.ts index eda929c76..51918728b 100644 --- a/packages/targets/test/fixtures/Bindings/Bindings.ts +++ b/packages/targets/test/fixtures/Bindings/Bindings.ts @@ -1,12 +1,11 @@ -import {Component, property} from '@diez/engine'; import {Color, File, FileType, Font, Image, LinearGradient, Lottie, Point2D, Toward, Typograph} from '@diez/prefabs'; -export class Bindings extends Component { - @property image = Image.responsive('assets/image with spaces.jpg', 246, 246); +export class Bindings { + image = Image.responsive('assets/image with spaces.jpg', 246, 246); - @property lottie = Lottie.fromJson('assets/lottie.json'); + lottie = Lottie.fromJson('assets/lottie.json'); - @property typograph = new Typograph({ + typograph = new Typograph({ font: new Font({ name: 'SomeFont', file: new File({src: 'assets/SomeFont.ttf', type: FileType.Font}), @@ -17,7 +16,7 @@ export class Bindings extends Component { color: Color.hex('#ff0'), }); - @property linearGradient = LinearGradient.make(Toward.Right, Color.rgb(255, 0, 0), Color.rgb(0, 0, 255)); + linearGradient = LinearGradient.make(Toward.Right, Color.rgb(255, 0, 0), Color.rgb(0, 0, 255)); - @property point = Point2D.make(0.5, 0.5); + point = Point2D.make(0.5, 0.5); } diff --git a/packages/targets/test/fixtures/Primitives/Primitives.ts b/packages/targets/test/fixtures/Primitives/Primitives.ts index db53a6ecb..eb734b30a 100644 --- a/packages/targets/test/fixtures/Primitives/Primitives.ts +++ b/packages/targets/test/fixtures/Primitives/Primitives.ts @@ -1,31 +1,33 @@ -import {Component, Float, Integer, property} from '@diez/engine'; +import {Float, Integer, prefab} from '@diez/engine'; -interface ChildComponentState { +interface ChildComponentData { diez: number; } -class ChildComponent extends Component { - @property diez = 0; +class ChildComponent extends prefab() { + defaults = { + diez: 0, + }; } -class EmptyComponent extends Component {} +class EmptyComponent {} -export class Primitives extends Component { - @property number = 10; - @property integer: Integer = 10; - @property float: Float = 10.0; - @property string = 'ten'; - @property boolean = !!10; +export class Primitives { + number = 10; + integer: Integer = 10; + float: Float = 10.0; + string = 'ten'; + boolean = !!10; // Lists of consistent depth and typing should carry through without issue. - @property integers = [[1, 2], [3, 4], [5]]; - @property strings = [[['6'], ['7']], [['8'], ['9']], [['10']]]; + integers = [[1, 2], [3, 4], [5]]; + strings = [[['6'], ['7']], [['8'], ['9']], [['10']]]; // This child component should override the default value. - @property child = new ChildComponent({diez: 10}); + child = new ChildComponent({diez: 10}); // Lists of components should also succeed. - @property childs = [[new ChildComponent({diez: 10})]]; + childs = [[new ChildComponent({diez: 10})]]; - @property emptyChild = new EmptyComponent(); + emptyChild = new EmptyComponent(); } diff --git a/packages/targets/test/helpers.ts b/packages/targets/test/helpers.ts index ab4565628..5abd0b864 100644 --- a/packages/targets/test/helpers.ts +++ b/packages/targets/test/helpers.ts @@ -1,5 +1,5 @@ -import {CompilerOptions, Program, projectCache} from '@diez/compiler'; -import {ConcreteComponentType, Target} from '@diez/engine'; +import {CompilerOptions, Constructor, Program, projectCache} from '@diez/compiler'; +import {Target} from '@diez/engine'; import {copySync, existsSync, readdirSync, readFileSync, removeSync, writeFileSync} from 'fs-extra'; import {join} from 'path'; import {AndroidCompiler} from '../src/targets/android.handler'; @@ -34,7 +34,7 @@ export const getFixtures = () => readdirSync(fixturesRoot); */ export const getFixtureComponentDeclaration = async (fixture: string) => { const {[fixture]: constructor} = await import(join(fixturesRoot, fixture, fixture)); - return constructor as ConcreteComponentType; + return constructor as Constructor; }; /** diff --git a/packages/web-sdk-common/src/css-linear-gradient.ts b/packages/web-sdk-common/src/css-linear-gradient.ts index 9543af5a1..afb1bb0be 100644 --- a/packages/web-sdk-common/src/css-linear-gradient.ts +++ b/packages/web-sdk-common/src/css-linear-gradient.ts @@ -1,14 +1,14 @@ -import {cssLinearGradientLength, linearGradientStartAndEndPoints, LinearGradientState, Point2DState} from '@diez/prefabs'; +import {cssLinearGradientLength, LinearGradientData, linearGradientStartAndEndPoints, Point2DData} from '@diez/prefabs'; /** * @returns The hypotenuse of the provided point. */ -const hypot = (point: Point2DState) => Math.sqrt(point.x * point.x + point.y * point.y); +const hypot = (point: Point2DData) => Math.sqrt(point.x * point.x + point.y * point.y); /** * @returns A normalized copy of the provided point. */ -const normalizePoint = (point: Point2DState) => { +const normalizePoint = (point: Point2DData) => { const length = hypot(point); return { x: point.x / length, @@ -19,7 +19,7 @@ const normalizePoint = (point: Point2DState) => { /** * @returns A Point2D where `x = pointA.x - pointB.x` and `y = pointA.y - pointB.y`. */ -const subtractPoints = (pointA: Point2DState, pointB: Point2DState) => { +const subtractPoints = (pointA: Point2DData, pointB: Point2DData) => { return { x: pointA.x - pointB.x, y: pointA.y - pointB.y, @@ -29,13 +29,13 @@ const subtractPoints = (pointA: Point2DState, pointB: Point2DState) => { /** * @returns The dot product of the two provided points. */ -const dotProduct = (pointA: Point2DState, pointB: Point2DState) => +const dotProduct = (pointA: Point2DData, pointB: Point2DData) => pointA.x * pointB.x + pointA.y * pointB.y; /** * @returns The cross product of the two provided points. */ -const crossProduct = (pointA: Point2DState, pointB: Point2DState) => +const crossProduct = (pointA: Point2DData, pointB: Point2DData) => pointA.x * pointB.y - pointA.y * pointB.x; /** @@ -45,7 +45,7 @@ const crossProduct = (pointA: Point2DState, pointB: Point2DState) => * @param lineVector A normalized vector representing the direction of the line. * @param point The point to compare with. */ -const nearestPointOnLine = (linePoint: Point2DState, lineVector: Point2DState, point: Point2DState) => { +const nearestPointOnLine = (linePoint: Point2DData, lineVector: Point2DData, point: Point2DData) => { const linePointToPoint = subtractPoints(point, linePoint); const t = dotProduct(linePointToPoint, lineVector); return { @@ -66,7 +66,7 @@ const nearestPointOnLine = (linePoint: Point2DState, lineVector: Point2DState, p * start * ``` */ -const angleBetween = (start: Point2DState, endA: Point2DState, endB: Point2DState) => { +const angleBetween = (start: Point2DData, endA: Point2DData, endB: Point2DData) => { const lineA = subtractPoints(start, endA); const lineB = subtractPoints(start, endB); @@ -103,7 +103,7 @@ const angleBetween = (start: Point2DState, endA: Point2DState, endB: Point2DStat * point * ``` */ -const isPointInDirection = (lineStart: Point2DState, lineEnd: Point2DState, point: Point2DState) => { +const isPointInDirection = (lineStart: Point2DData, lineEnd: Point2DData, point: Point2DData) => { const angle = angleBetween(lineStart, lineEnd, point); return Math.abs(angle) < Math.PI / 2; }; @@ -118,7 +118,7 @@ const convertToCSSLinearGradientAngle = (angle: number) => /** * @returns A normalized direction vector for the provided start and end points. */ -const normalizedDirectionFromPoints = (start: Point2DState, end: Point2DState) => { +const normalizedDirectionFromPoints = (start: Point2DData, end: Point2DData) => { const direction = subtractPoints(end, start); return normalizePoint(direction); }; @@ -153,7 +153,7 @@ const normalizedDirectionFromPoints = (start: Point2DState, end: Point2DState) = * @returns The corresponding stop position of the provided point where 1.0 is 100%. This value can be less than 0 or * greater than 1.0. */ -const stopPositionForPoint = (angle: number, point: Point2DState) => { +const stopPositionForPoint = (angle: number, point: Point2DData) => { const length = cssLinearGradientLength(angle); const center = {x: 0.5, y: 0.5}; const points = linearGradientStartAndEndPoints(angle, length, center); @@ -177,7 +177,7 @@ const stopPositionForPoint = (angle: number, point: Point2DState) => { * - (0, 0) is top left and (1, 1) is bottom right, * - (0, 0) is bottom left and (1, 1) is top right. */ -export const convertPoint = (point: Point2DState) => { +export const convertPoint = (point: Point2DData) => { return { x: point.x, y: 1 - point.y, @@ -189,7 +189,7 @@ export const convertPoint = (point: Point2DState) => { * * See https://drafts.csswg.org/css-images-3/#funcdef-linear-gradient */ -export const linearGradientToCss = (gradient: LinearGradientState) => { +export const linearGradientToCss = (gradient: LinearGradientData) => { if (gradient.stops.length === 0) { return 'linear-gradient(none)'; } diff --git a/utils/diez-webpack-plugin/src/index.ts b/utils/diez-webpack-plugin/src/index.ts index c258a4b7e..b11b41fd6 100644 --- a/utils/diez-webpack-plugin/src/index.ts +++ b/utils/diez-webpack-plugin/src/index.ts @@ -37,7 +37,7 @@ class DiezWebpackPlugin { if (!existsSync(this.options.projectPath)) { // tslint:disable-next-line:max-line-length - console.warn("DiezWebpackPlugin: unable to determine the location of your Diez project. Note that hot mode will not work unless you provide an explicit path via 'projectPath'."); + console.warn('DiezWebpackPlugin: unable to determine the location of your Diez project. Note that hot mode will not work unless you provide an explicit path via \'projectPath\'.'); } }