From 550a92eec18a8ad31a1de240e62be2a3e0a92e89 Mon Sep 17 00:00:00 2001 From: rewin Date: Thu, 7 Mar 2024 21:03:42 +0300 Subject: [PATCH 01/13] Inspect for StandartMaterial (#76) Allow to render StandartMaterial color and change in uniform way as another components ![image](https://github.com/storytold/storyteller-bevy/assets/10954133/bf2fc91a-642e-40b6-99d3-a18c06dc156a) ![image](https://github.com/storytold/storyteller-bevy/assets/10954133/8534b82d-ed55-4714-8888-257db50c5e0f) --- studio/src/ui/inspector.rs | 319 +++++++++++++++++++++++++++++-------- 1 file changed, 255 insertions(+), 64 deletions(-) diff --git a/studio/src/ui/inspector.rs b/studio/src/ui/inspector.rs index 61ed1ff..851a4bf 100644 --- a/studio/src/ui/inspector.rs +++ b/studio/src/ui/inspector.rs @@ -1,6 +1,7 @@ use std::{marker::PhantomData, sync::Arc}; use bevy::{ + ecs::world::unsafe_world_cell::UnsafeWorldCell, prelude::*, reflect::{serde::ReflectSerializer, GetTypeRegistration}, utils::HashMap, @@ -60,6 +61,43 @@ impl Plugin for InspectorPlugin { app.register_inspect::(); app.register_inspect::(); app.register_inspect::(); + + //register standart material + { + app.register_type::() + .add_event::>>() + .add_systems( + Update, + ( + update_inspect_by_select_change::>, + update_inspect_by_remove::>, + update_inspect_by_change::>, + notify_asset_change_system::, + ) + .in_set(InspectorSystem::DetectChanges), + ) + .add_systems( + Update, + serialize_standart_materal.in_set(InspectorSystem::PerComponentCollect), + ); + + app.world.resource_mut::().getters.insert( + StandardMaterial::get_type_registration() + .type_info() + .type_path() + .to_string(), + Arc::new(|entity, path, world, f| unsafe { + let handle = entity.get::>().unwrap(); + let mut assets = world + .get_resource_mut::>() + .unwrap(); + let material = assets.get_mut(handle).unwrap(); + + let reflect_field = material.reflect_path_mut(path).unwrap(); + (f)(reflect_field); + }), + ); + } } } @@ -67,12 +105,17 @@ impl Plugin for InspectorPlugin { struct InspectGetters { getters: HashMap< String, - Arc, + Arc< + dyn Fn(&mut EntityWorldMut, &str, &mut UnsafeWorldCell, &dyn Fn(&mut dyn Reflect)) + + Send + + Sync, + >, >, } trait AppInspectable { fn register_inspect(&mut self); + fn register_asset_inspect(&mut self); } impl AppInspectable for App { @@ -100,7 +143,7 @@ impl AppInspectable for App { .type_info() .type_path() .to_string(), - Arc::new(|entity, path, f| { + Arc::new(|entity, path, _, f| { let mut component = entity.get_mut::().unwrap(); let reflect_field = component.reflect_path_mut(path).unwrap(); (f)(reflect_field); @@ -108,6 +151,25 @@ impl AppInspectable for App { ); } } + + fn register_asset_inspect(&mut self) { + self.register_type::() + .add_event::>>() + .add_systems( + Update, + ( + update_inspect_by_select_change::>, + update_inspect_by_remove::>, + update_inspect_by_change::>, + notify_asset_change_system::, + ) + .in_set(InspectorSystem::DetectChanges), + ) + .add_systems( + Update, + auto_serialize_asset::.in_set(InspectorSystem::PerComponentCollect), + ); + } } #[derive(Default, Resource)] @@ -160,7 +222,7 @@ impl Default for UpdateInspect { fn update_field_by_ui(world: &mut World) { unsafe { - let unsafe_world = world.as_unsafe_world_cell(); + let mut unsafe_world = world.as_unsafe_world_cell(); let Some(msg) = CHANGE_FROM_UI.take_message() else { return; }; @@ -181,68 +243,73 @@ fn update_field_by_ui(world: &mut World) { return; }; - getter.as_ref()(&mut entity_mut, &msg.reflect_path, &|reflect_field| { - if let Some(val) = reflect_field.downcast_mut::() { - let Ok(msg_value) = serde_json::from_value(msg.value.clone()) else { - error!("Failed to parse value: {}", msg.value); + getter.as_ref()( + &mut entity_mut, + &msg.reflect_path, + &mut unsafe_world, + &|reflect_field| { + if let Some(val) = reflect_field.downcast_mut::() { + let Ok(msg_value) = serde_json::from_value(msg.value.clone()) else { + error!("Failed to parse value: {}", msg.value); + return; + }; + *val = msg_value; + } else if let Some(val) = reflect_field.downcast_mut::() { + let Ok(msg_value) = serde_json::from_value(msg.value.clone()) else { + error!("Failed to parse value: {}", msg.value); + return; + }; + *val = msg_value; + } else if let Some(val) = reflect_field.downcast_mut::() { + let Ok(msg_value) = serde_json::from_value(msg.value.clone()) else { + error!("Failed to parse value: {}", msg.value); + return; + }; + *val = msg_value; + } else if let Some(val) = reflect_field.downcast_mut::() { + let Ok(msg_value) = serde_json::from_value(msg.value.clone()) else { + error!("Failed to parse value: {}", msg.value); + return; + }; + *val = msg_value; + } else if let Some(val) = reflect_field.downcast_mut::() { + let Ok(msg_value) = serde_json::from_value(msg.value.clone()) else { + error!("Failed to parse value: {}", msg.value); + return; + }; + *val = msg_value; + } else if let Some(val) = reflect_field.downcast_mut::() { + let Ok(msg_value) = serde_json::from_value(msg.value.clone()) else { + error!("Failed to parse value: {}", msg.value); + return; + }; + *val = msg_value; + } else if let Some(val) = reflect_field.downcast_mut::() { + let Ok(msg_value) = serde_json::from_value(msg.value.clone()) else { + error!("Failed to parse value: {}", msg.value); + return; + }; + *val = msg_value; + } else if let Some(val) = reflect_field.downcast_mut::() { + *val = msg.value.as_str().unwrap().to_string(); + } else if let Some(val) = reflect_field.downcast_mut::() { + let Ok(msg_value) = serde_json::from_value(msg.value.clone()) else { + error!("Failed to parse value: {}", msg.value); + return; + }; + *val = msg_value; + } else if let Some(val) = reflect_field.downcast_mut::() { + let Ok(msg_value) = serde_json::from_value(msg.value.clone()) else { + error!("Failed to parse value: {}", msg.value); + return; + }; + *val = msg_value; + } else { + error!("Unsupported type {}!", &msg.reflect_path); return; - }; - *val = msg_value; - } else if let Some(val) = reflect_field.downcast_mut::() { - let Ok(msg_value) = serde_json::from_value(msg.value.clone()) else { - error!("Failed to parse value: {}", msg.value); - return; - }; - *val = msg_value; - } else if let Some(val) = reflect_field.downcast_mut::() { - let Ok(msg_value) = serde_json::from_value(msg.value.clone()) else { - error!("Failed to parse value: {}", msg.value); - return; - }; - *val = msg_value; - } else if let Some(val) = reflect_field.downcast_mut::() { - let Ok(msg_value) = serde_json::from_value(msg.value.clone()) else { - error!("Failed to parse value: {}", msg.value); - return; - }; - *val = msg_value; - } else if let Some(val) = reflect_field.downcast_mut::() { - let Ok(msg_value) = serde_json::from_value(msg.value.clone()) else { - error!("Failed to parse value: {}", msg.value); - return; - }; - *val = msg_value; - } else if let Some(val) = reflect_field.downcast_mut::() { - let Ok(msg_value) = serde_json::from_value(msg.value.clone()) else { - error!("Failed to parse value: {}", msg.value); - return; - }; - *val = msg_value; - } else if let Some(val) = reflect_field.downcast_mut::() { - let Ok(msg_value) = serde_json::from_value(msg.value.clone()) else { - error!("Failed to parse value: {}", msg.value); - return; - }; - *val = msg_value; - } else if let Some(val) = reflect_field.downcast_mut::() { - *val = msg.value.as_str().unwrap().to_string(); - } else if let Some(val) = reflect_field.downcast_mut::() { - let Ok(msg_value) = serde_json::from_value(msg.value.clone()) else { - error!("Failed to parse value: {}", msg.value); - return; - }; - *val = msg_value; - } else if let Some(val) = reflect_field.downcast_mut::() { - let Ok(msg_value) = serde_json::from_value(msg.value.clone()) else { - error!("Failed to parse value: {}", msg.value); - return; - }; - *val = msg_value; - } else { - error!("Unsupported type {}!", &msg.reflect_path); - return; - } - }); + } + }, + ); } } @@ -316,6 +383,130 @@ fn auto_serialize_system( ev_update_description.send(UpdateEntityDescription); } +fn notify_asset_change_system( + mut ev_inspect: EventWriter>>, + mut asset_changing: EventReader>, + mut selected: Query<&Handle, With>, +) { + for ev in asset_changing.read() { + for handle in selected.iter_mut() { + if ev.is_modified(handle.id()) { + ev_inspect.send(UpdateInspect::>::default()); + } + } + } +} + +fn auto_serialize_asset( + q_t: Query<&Handle, With>, + mut description: ResMut, + registry: Res, + mut ev_inspect: EventReader>>, + mut ev_update_description: EventWriter, + assets: Res>, +) { + if ev_inspect.is_empty() { + return; + } + + ev_inspect.clear(); + + if let Some(handle) = q_t.iter().next() { + if let Some(asset) = assets.get(handle) { + let registry = registry.read(); + info!( + "type: {:?}", + T::get_type_registration() + .type_info() + .type_path() + .to_string() + ); + + let json = serde_json::to_value(ReflectSerializer::new(asset, ®istry)).unwrap(); + info!("json: {:?}", json.to_string()); + description.components.insert( + T::get_type_registration() + .type_info() + .type_path() + .to_string(), + json, + ); + } + } else { + description + .components + .remove(T::get_type_registration().type_info().type_path()); + } + + ev_update_description.send(UpdateEntityDescription); +} + +#[derive(Reflect, Default)] +#[reflect(Default)] +struct ShortMaterial { + pub base_color: Color, +} + +impl ShortMaterial { + pub fn new(material: &StandardMaterial) -> Self { + Self { + base_color: material.base_color, + } + } +} + +fn serialize_standart_materal( + q_m: Query<&Handle>, + mut description: ResMut, + registry: Res, + mut ev_inspect: EventReader>>, + mut ev_update_description: EventWriter, + assets: Res>, +) { + if ev_inspect.is_empty() { + return; + } + + ev_inspect.clear(); + + if let Some(handle) = q_m.iter().next() { + if let Some(material) = assets.get(handle) { + let registry = registry.read(); + let short_material = ShortMaterial::new(material); + let json = + serde_json::to_value(ReflectSerializer::new(&short_material, ®istry)).unwrap(); + let json = json[ShortMaterial::get_type_registration() + .type_info() + .type_path() + .to_string()] + .clone(); + let json = json!( + { + StandardMaterial::get_type_registration() + .type_info() + .type_path() + .to_string(): json + }); + info!("json: {:?}", json.to_string()); + description.components.insert( + StandardMaterial::get_type_registration() + .type_info() + .type_path() + .to_string(), + json, + ); + } + } else { + description.components.remove( + StandardMaterial::get_type_registration() + .type_info() + .type_path(), + ); + } + + ev_update_description.send(UpdateEntityDescription); +} + fn update_description( description: ResMut, mut ev_update_description: EventReader, From fc022b1b3a41f681947e5456e58bd112b1104191 Mon Sep 17 00:00:00 2001 From: Danny McGee Date: Thu, 7 Mar 2024 20:58:00 -0500 Subject: [PATCH 02/13] Catch `DOMException` when accessing parent origin (#80) --- .../lib/iframe-messaging-bridge.element.ts | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/studio-web/src/lib/iframe-messaging-bridge.element.ts b/studio-web/src/lib/iframe-messaging-bridge.element.ts index d065c5b..10a9900 100644 --- a/studio-web/src/lib/iframe-messaging-bridge.element.ts +++ b/studio-web/src/lib/iframe-messaging-bridge.element.ts @@ -28,13 +28,19 @@ export class IframeMessagingBridgeElement extends LitElement { #parentOrigin?: string; override connectedCallback(): void { - if ( - window.parent - && window.parent !== window - && PARENT_ORIGIN_WHITELIST.includes(window.parent.origin) - ) { - this.#parentWindow = window.parent; - this.#parentOrigin = window.parent.origin; + try { + if ( + window.parent + && window.parent !== window + && PARENT_ORIGIN_WHITELIST.includes(window.parent.origin) + ) { + this.#parentWindow = window.parent; + this.#parentOrigin = window.parent.origin; + } + } + catch { + this.#parentWindow = undefined; + this.#parentOrigin = undefined; } super.connectedCallback(); From 30979fec961f3abeb406a78ec966d70ec5cfd627 Mon Sep 17 00:00:00 2001 From: Danny McGee Date: Thu, 7 Mar 2024 21:44:55 -0500 Subject: [PATCH 03/13] Fix issues with `framework` decorators (#79) This PR fixes issues with several of the decorator implementations in `framework` and adds unit tests to specify and validate their functionality. --- framework/src/lib/decorators/on.spec.ts | 14 ++ framework/src/lib/decorators/on.ts | 28 ++- framework/src/lib/di/decorators.ts | 71 +++---- framework/src/lib/di/di.spec.ts | 250 ++++++++++++++++++++++++ 4 files changed, 323 insertions(+), 40 deletions(-) create mode 100644 framework/src/lib/di/di.spec.ts diff --git a/framework/src/lib/decorators/on.spec.ts b/framework/src/lib/decorators/on.spec.ts index 430456a..94866fa 100644 --- a/framework/src/lib/decorators/on.spec.ts +++ b/framework/src/lib/decorators/on.spec.ts @@ -30,4 +30,18 @@ describe("@on(eventName)", () => { expect(counter.count).toBe(3); }); + + it("exhibits correct `this` handling when multiple instances are present", () => { + const counter1 = document.createElement("test-counter"); + document.body.appendChild(counter1); + + const counter2 = document.createElement("test-counter"); + document.body.appendChild(counter2); + + counter1.click(); + counter2.click(); + + expect(counter1.count).toBe(1); + expect(counter2.count).toBe(1); + }); }); diff --git a/framework/src/lib/decorators/on.ts b/framework/src/lib/decorators/on.ts index dbd4f5b..c3a66a1 100644 --- a/framework/src/lib/decorators/on.ts +++ b/framework/src/lib/decorators/on.ts @@ -15,7 +15,7 @@ export function on(eventSelector: string) { ? eventSelector.split(":") : [null, eventSelector]; - let target = match (targetName, { + const target = match (targetName, { "window": () => window as EventTarget, "document": () => document as EventTarget, _: () => null, @@ -23,17 +23,35 @@ export function on(eventSelector: string) { return (proto: T, propName: keyof T, desc: PropertyDescriptor) => { const handler = getMethodDescriptor(proto, propName)?.value ?? (() => {}); + const $handler = Symbol(String(propName)); + + type Decorated = T & { + [$handler]: typeof handler; + } prependRoutine(proto, "connectedCallback", function (this: T) { - target ??= this; - target.addEventListener(eventName, handler.bind(this)); + assertType(this); + this[$handler] ??= handler.bind(this); + + if (target) { + target.addEventListener(eventName, this[$handler]); + } else { + this.addEventListener(eventName, this[$handler]); + } }); prependRoutine(proto, "disconnectedCallback", function (this: T) { - target ??= this; - target.removeEventListener(eventName, handler.bind(this)); + assertType(this); + + if (target) { + target.removeEventListener(eventName, this[$handler]); + } else { + this.removeEventListener(eventName, this[$handler]); + } }); return desc; } } + +function assertType(value: unknown): asserts value is T {} diff --git a/framework/src/lib/di/decorators.ts b/framework/src/lib/di/decorators.ts index 6674c96..c49de19 100644 --- a/framework/src/lib/di/decorators.ts +++ b/framework/src/lib/di/decorators.ts @@ -1,4 +1,4 @@ -import { Ctor, Fn, Method } from "@storyteller/utility"; +import { Ctor, Fn } from "@storyteller/utility"; import { LitElement } from "lit"; import { appendRoutine, prependRoutine } from "../decorators/internal"; @@ -7,6 +7,7 @@ import { Token } from "./types"; const $injector = Symbol("injector"); const $$injector = Symbol("#injector"); +const $fulfillInjectionRequest = Symbol("fulfillInjectionRequest"); export type ProviderOptions = Token @@ -41,6 +42,7 @@ export function provide(options: ProviderOptions) type Decorated = E & { [$$injector]?: Map, any> | undefined; readonly [$injector]: Map, any>; + [$fulfillInjectionRequest](event: InjectionRequest): void; } return (Target: Ctor) => { @@ -48,9 +50,7 @@ export function provide(options: ProviderOptions) assertType(proto); let descriptor = Object.getOwnPropertyDescriptor(proto, $injector); - let needsInit = false; if (!($injector in proto) || !descriptor) { - needsInit = true; Object.defineProperty(proto, $injector, { get(this: Decorated) { return this[$$injector] ??= new Map() }, configurable: false, @@ -73,17 +73,16 @@ export function provide(options: ProviderOptions) this[$injector].set(token, value); this.dispatchEvent(new DependencyProvision(token, value)); - if (needsInit) { - const fulfill = fulfillInjectionRequest.bind(this); - this.addEventListener(DIEvent.InjectionRequest, fulfill as Method); - } + this[$fulfillInjectionRequest] ??= fulfillInjectionRequest.bind(this); + this.addEventListener(DIEvent.InjectionRequest, this[$fulfillInjectionRequest]); }); - appendRoutine(proto, "disconnectedCallback", function (this: Decorated): void { + prependRoutine(proto, "disconnectedCallback", function (this: Decorated): void { const token = "token" in options ? options.token : options; const value = this[$injector].get(token); this.dispatchEvent(new ProviderRemoval(token, value)); + this.removeEventListener(DIEvent.InjectionRequest, this[$fulfillInjectionRequest]); }); } } @@ -140,43 +139,45 @@ export function inject(token: Token) { */ export function queryProviders(token: Token) { return (proto: E, key: K) => { - function onProvided(this: E, event: DependencyProvision): void { - const array = this[key] ?? []; - assertType(array); + const $onProvided = Symbol(`onProvided:${String(key)}`); + const $onRemoved = Symbol(`onRemoved:${String(key)}`); - if (event.detail.token === token) - this[key] = array.concat(event.detail.value) as E[K]; + type Decorated = E & { + [$onProvided](event: DependencyProvision): void; + [$onRemoved](event: ProviderRemoval): void; } - function onRemoved(this: E, event: ProviderRemoval): void { + assertType(proto); + + function onProvided(this: Decorated, event: DependencyProvision): void { const array = this[key] ?? []; - assertType(array); + assertType(array); if (event.detail.token === token) - this[key] = array.filter(el => el !== event.detail.value) as E[K]; + this[key] = array.concat(event.detail.value) as Decorated[K]; } - appendRoutine(proto, "connectedCallback", function (this: E): void { - this[key] = [] as E[K]; - this.addEventListener( - DIEvent.DependencyProvision, - onProvided.bind(this) as Method, - ); - this.addEventListener( - DIEvent.ProviderRemoval, - onRemoved.bind(this) as Method, - ); + function onRemoved(this: Decorated, event: ProviderRemoval): void { + const array = this[key] ?? []; + assertType(array); + + if (event.detail.token === token) + this[key] = array.filter(el => el !== event.detail.value) as Decorated[K]; + } + + appendRoutine(proto, "connectedCallback", function (this: Decorated): void { + this[key] = [] as Decorated[K]; + + this[$onProvided] ??= onProvided.bind(this); + this[$onRemoved] ??= onRemoved.bind(this); + + this.addEventListener(DIEvent.DependencyProvision, this[$onProvided]); + this.addEventListener(DIEvent.ProviderRemoval, this[$onRemoved]); }); - appendRoutine(proto, "disconnectedCallback", function (this: E): void { - this.removeEventListener( - DIEvent.DependencyProvision, - onProvided.bind(this) as Method, - ); - this.removeEventListener( - DIEvent.ProviderRemoval, - onRemoved.bind(this) as Method, - ); + appendRoutine(proto, "disconnectedCallback", function (this: Decorated): void { + this.removeEventListener(DIEvent.DependencyProvision, this[$onProvided]); + this.removeEventListener(DIEvent.ProviderRemoval, this[$onRemoved]); }); } } diff --git a/framework/src/lib/di/di.spec.ts b/framework/src/lib/di/di.spec.ts new file mode 100644 index 0000000..400807a --- /dev/null +++ b/framework/src/lib/di/di.spec.ts @@ -0,0 +1,250 @@ +import { LitElement, html, render } from "lit"; +import { customElement, property } from "lit/decorators.js"; +import { createRef, ref } from "lit/directives/ref.js"; + +import { provide, inject, queryProviders } from "./decorators"; +import { UniqueToken } from "./types"; + +const TEST_TOKEN = UniqueToken.create(); +@customElement("test-token-provider") +@provide(TEST_TOKEN) +class TestTokenProvider extends LitElement { + override render = () => html`` +} + +@customElement("test-concrete-provider") +@provide(TestConcreteProvider) +class TestConcreteProvider extends LitElement { + override render = () => html`` +} + +const TEST_VALUE = UniqueToken.create(); +@customElement("test-value-provider") +@provide({ + token: TEST_VALUE, + provide() { return this.value; } +}) +class TestValueProvider extends LitElement { + @property({ type: Number }) + value = 42; + + override render = () => html`` +} + +@customElement("test-injector") +class TestInjector extends LitElement { + @inject(TEST_TOKEN) + tokenProvider: unknown; + + @inject(TestConcreteProvider) + concreteProvider?: TestConcreteProvider; + + @inject(TEST_VALUE) + valueProvider?: number; + + override render = () => html``; +} + +@customElement("test-queryer") +class TestQueryer extends LitElement { + @queryProviders(TEST_TOKEN) + tokenProviders: unknown[] = []; + + @queryProviders(TestConcreteProvider) + concreteProviders: TestConcreteProvider[] = []; + + @queryProviders(TEST_VALUE) + valueProviders: number[] = []; + + override render = () => html`` +} + +describe("`@provide` and `@inject`", () => { + it("works with abstract tokens", () => { + const providerRef = createRef(); + const injectorRef = createRef(); + + render(html` + + + + `, document.body); + + expect(providerRef.value).toBeInstanceOf(TestTokenProvider); + expect(injectorRef.value).toBeInstanceOf(TestInjector); + expect(injectorRef.value!.tokenProvider).toBe(providerRef.value); + }); + + it("works with concrete tokens", () => { + const providerRef = createRef(); + const injectorRef = createRef(); + + render(html` + + + + `, document.body); + + expect(providerRef.value).toBeInstanceOf(TestConcreteProvider); + expect(injectorRef.value).toBeInstanceOf(TestInjector); + expect(injectorRef.value!.concreteProvider).toBe(providerRef.value); + }); + + it("works with value tokens", () => { + const providerRef = createRef(); + const injectorRef = createRef(); + + render(html` + + + + `, document.body); + + expect(providerRef.value).toBeInstanceOf(TestValueProvider); + expect(injectorRef.value).toBeInstanceOf(TestInjector); + expect(injectorRef.value!.valueProvider).toBe(42); + }); + + it("works with multiple provider/injector instances", () => { + const parentProviderRef = createRef(); + const childProviderRef = createRef(); + const parentInjectorRef = createRef(); + const childInjectorRef = createRef(); + + render(html` + + + + + + + `, document.body); + + expect(parentProviderRef.value).toBeInstanceOf(TestValueProvider); + expect(childProviderRef.value).toBeInstanceOf(TestValueProvider); + expect(parentInjectorRef.value).toBeInstanceOf(TestInjector); + expect(childInjectorRef.value).toBeInstanceOf(TestInjector); + + expect(parentInjectorRef.value!.valueProvider).toBe(420); + expect(childInjectorRef.value!.valueProvider).toBe(69); + }); + + it("works with nested providers", () => { + const tokenProviderRef = createRef(); + const concreteProviderRef = createRef(); + const valueProviderRef = createRef(); + const injectorRef = createRef(); + + render(html` + + + + + + + + `, document.body); + + expect(tokenProviderRef.value).toBeInstanceOf(TestTokenProvider); + expect(concreteProviderRef.value).toBeInstanceOf(TestConcreteProvider); + expect(valueProviderRef.value).toBeInstanceOf(TestValueProvider); + expect(injectorRef.value).toBeInstanceOf(TestInjector); + + expect(injectorRef.value!.tokenProvider).toBe(tokenProviderRef.value); + expect(injectorRef.value!.concreteProvider).toBe(concreteProviderRef.value); + expect(injectorRef.value!.valueProvider).toBe(42); + }); +}); + +describe("`@provide` and `@queryProviders`", () => { + it("works with abstract tokens", () => { + const queryerRef = createRef(); + const providerRef = createRef(); + + render(html` + + + + `, document.body); + + expect(queryerRef.value).toBeInstanceOf(TestQueryer); + expect(providerRef.value).toBeInstanceOf(TestTokenProvider); + + expect(queryerRef.value?.tokenProviders).toEqual([providerRef.value]); + }); + + it("works with concrete tokens", () => { + const queryerRef = createRef(); + const providerRef = createRef(); + + render(html` + + + + `, document.body); + + expect(queryerRef.value).toBeInstanceOf(TestQueryer); + expect(providerRef.value).toBeInstanceOf(TestConcreteProvider); + + expect(queryerRef.value?.concreteProviders).toEqual([providerRef.value]); + }); + + it("works with value tokens", () => { + const queryerRef = createRef(); + const providerRef = createRef(); + + render(html` + + + + `, document.body); + + expect(queryerRef.value).toBeInstanceOf(TestQueryer); + expect(providerRef.value).toBeInstanceOf(TestValueProvider); + + expect(queryerRef.value?.valueProviders).toEqual([42]); + }); + + it("works with multiple providers", () => { + const queryerRef = createRef(); + + render(html` + + + + + + `, document.body); + + expect(queryerRef.value).toBeInstanceOf(TestQueryer); + expect(queryerRef.value!.valueProviders).toEqual([42, 420, 69]); + }); + + it("dynamically updates", () => { + const queryerRef = createRef(); + + render(html` + + + + + + `, document.body); + + expect(queryerRef.value).toBeInstanceOf(TestQueryer); + expect(queryerRef.value!.valueProviders).toEqual([42, 420, 69]); + + const middleElement = queryerRef.value!.children.item(1) as TestValueProvider; + expect(middleElement).toBeInstanceOf(TestValueProvider); + middleElement.disconnectedCallback(); + middleElement.remove(); + + expect(queryerRef.value!.valueProviders).toEqual([42, 69]); + }); +}); From 33250e46277390367616f913ea57f0e6472026e0 Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Fri, 8 Mar 2024 18:34:33 -0500 Subject: [PATCH 04/13] fix CORS for certain hostnames (#81) --- .../src/lib/iframe-messaging-bridge.element.ts | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/studio-web/src/lib/iframe-messaging-bridge.element.ts b/studio-web/src/lib/iframe-messaging-bridge.element.ts index 10a9900..bd9f376 100644 --- a/studio-web/src/lib/iframe-messaging-bridge.element.ts +++ b/studio-web/src/lib/iframe-messaging-bridge.element.ts @@ -10,13 +10,19 @@ const PARENT_ORIGIN_WHITELIST: readonly string[] = [ "https://staging.fakeyou.com", "https://staging.storyteller.ai", "https://feature-mvp--fakeyou.netlify.app", - "https://feature-mvp--storyteller.netlify.app", + "https://feature-mvp--storyteller-ai.netlify.app", "http://dev.fakeyou.com:7000", "http://dev.fakeyou.com:7001", "http://dev.fakeyou.com:7002", - "http://dev.storyteller.com:7000", - "http://dev.storyteller.com:7001", - "http://dev.storyteller.com:7002", + "http://devproxy.fakeyou.com:7000", + "http://devproxy.fakeyou.com:7001", + "http://devproxy.fakeyou.com:7002", + "http://dev.storyteller.ai:7000", + "http://dev.storyteller.ai:7001", + "http://dev.storyteller.ai:7002", + "http://devproxy.storyteller.ai:7000", + "http://devproxy.storyteller.ai:7001", + "http://devproxy.storyteller.ai:7002", "http://localhost:7000", "http://localhost:7001", "http://localhost:7002", From 9dfbcae134ba093246ff0e3f9fa1592e59ba2d4c Mon Sep 17 00:00:00 2001 From: Danny McGee Date: Fri, 8 Mar 2024 19:56:19 -0500 Subject: [PATCH 05/13] Hotfix: Replace calls to `crypto.randomUUID` (#82) `crypto.randomUUID` is only available in secure contexts (HTTPS), which throws an error when trying to run in certain situations/browsers. --- framework/src/lib/di/types.ts | 3 ++- image-generator-demo/src/app/app.element.ts | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/framework/src/lib/di/types.ts b/framework/src/lib/di/types.ts index 112076f..a882de9 100644 --- a/framework/src/lib/di/types.ts +++ b/framework/src/lib/di/types.ts @@ -1,5 +1,6 @@ import { type AbstractCtor, type Ctor, type Fn } from "@storyteller/utility"; import { type PropertyValues } from "lit"; +import { v4 as uuid } from "uuid"; export class UniqueToken { get id() { return this.#id; } @@ -10,7 +11,7 @@ export class UniqueToken { } static create(name?: string): Token { - return new UniqueToken(name ?? crypto.randomUUID()); + return new UniqueToken(name ?? uuid()); } toString(): string { diff --git a/image-generator-demo/src/app/app.element.ts b/image-generator-demo/src/app/app.element.ts index 853f6d6..3ea93a9 100644 --- a/image-generator-demo/src/app/app.element.ts +++ b/image-generator-demo/src/app/app.element.ts @@ -12,6 +12,7 @@ import { customElement, state } from "lit/decorators.js"; import { classMap } from "lit/directives/class-map.js"; import { createRef, ref } from "lit/directives/ref.js"; import { styleMap } from "lit/directives/style-map.js"; +import { v4 as uuid } from "uuid"; import { ImageInfo, LcmClient } from "./lcm-client"; @@ -38,7 +39,7 @@ export class AppElement extends LitElement { @state() inputUrl?: string; @state() outputUrl?: string; - #lcmClient = new LcmClient(crypto.randomUUID()); + #lcmClient = new LcmClient(uuid()); #outputImageRef = createRef(); constructor () { From 26bbec58ecf78648b27b55bedab34979213abd48 Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Sat, 9 Mar 2024 02:34:55 -0500 Subject: [PATCH 06/13] cargo target documentation --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index 2bab7bb..0ec9742 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,11 @@ frontend project, run: npx nx serve studio-frontend ``` +You'll need to install the wasm compiler for Rust: +``` +rustup target add wasm32-unknown-unknown +``` + Projects are located in directories at the workspace root. The list of commands supported by a given project can be found under the `target` property of its `project.json` file, which defines the Nx configuration for that project. From 005b4fdba3a1858dd6611c3a6cdaf2406dad0d54 Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Sat, 9 Mar 2024 02:45:48 -0500 Subject: [PATCH 07/13] debug cors with logging (#83) --- .../src/lib/iframe-messaging-bridge.element.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/studio-web/src/lib/iframe-messaging-bridge.element.ts b/studio-web/src/lib/iframe-messaging-bridge.element.ts index bd9f376..01915cb 100644 --- a/studio-web/src/lib/iframe-messaging-bridge.element.ts +++ b/studio-web/src/lib/iframe-messaging-bridge.element.ts @@ -34,6 +34,9 @@ export class IframeMessagingBridgeElement extends LitElement { #parentOrigin?: string; override connectedCallback(): void { + console.log('window.parent', window.parent); + console.log('window.parent.origin', window.parent.origin); + try { if ( window.parent @@ -54,6 +57,9 @@ export class IframeMessagingBridgeElement extends LitElement { @on("window:message") onParentMessage(event: MessageEvent): void { + console.log('window.parent', window.parent); + console.log('window.parent.origin', window.parent.origin); + if (!this.#parentOrigin || event.origin !== this.#parentOrigin) return; @@ -63,6 +69,9 @@ export class IframeMessagingBridgeElement extends LitElement { @on("window:scene-uploaded") onSceneUploaded(event: CustomEvent): void { + console.log('window.parent', window.parent); + console.log('window.parent.origin', window.parent.origin); + if (!this.#parentWindow || !this.#parentOrigin) return; @@ -71,6 +80,9 @@ export class IframeMessagingBridgeElement extends LitElement { @on("window:scene-state") onSceneStateChange({ detail: state }: SceneStateEvent): void { + console.log('window.parent', window.parent); + console.log('window.parent.origin', window.parent.origin); + if (!this.#parentWindow || !this.#parentOrigin) return; From 676b9f86a85a78c7d31bb7189dd9edf4b2c57526 Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Sat, 9 Mar 2024 03:06:09 -0500 Subject: [PATCH 08/13] add netlify toml for CORS (#84) --- netlify.toml | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 netlify.toml diff --git a/netlify.toml b/netlify.toml new file mode 100644 index 0000000..633f4e9 --- /dev/null +++ b/netlify.toml @@ -0,0 +1,30 @@ +# https://answers.netlify.com/t/access-control-allow-origin-to-multiple-domains/23969 +[[headers]] + for = "/*" + [headers.values] + Access-Control-Allow-Origin = ''' + http://dev.fakeyou.com, + http://dev.fakeyou.com:7000, + http://dev.fakeyou.com:7001, + http://dev.fakeyou.com:7002, + http://dev.storyteller.ai, + http://dev.storyteller.ai:7000, + http://dev.storyteller.ai:7001, + http://dev.storyteller.ai:7002, + http://devproxy.fakeyou.com:7000, + http://devproxy.fakeyou.com:7001, + http://devproxy.fakeyou.com:7002, + http://devproxy.storyteller.ai, + http://devproxy.storyteller.ai:7000, + http://devproxy.storyteller.ai:7001, + http://devproxy.storyteller.ai:7002, + https://api.fakeyou.com, + https://engine.fakeyou.com, + https://fakeyou.com, + https://feature-mvp--fakeyou.netlify.app, + https://feature-mvp--storyteller-ai.netlify.app, + https://staging.fakeyou.com, + https://staging.storyteller.ai, + https://storyteller.ai, + https://example.com''' + From 241c5cc25c57060d5305b43b929fe5660e580033 Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Sat, 9 Mar 2024 03:36:04 -0500 Subject: [PATCH 09/13] toml cors attempt 2 (#85) --- netlify.toml | 28 ++-------------------------- 1 file changed, 2 insertions(+), 26 deletions(-) diff --git a/netlify.toml b/netlify.toml index 633f4e9..eaeac9f 100644 --- a/netlify.toml +++ b/netlify.toml @@ -1,30 +1,6 @@ -# https://answers.netlify.com/t/access-control-allow-origin-to-multiple-domains/23969 [[headers]] for = "/*" [headers.values] - Access-Control-Allow-Origin = ''' - http://dev.fakeyou.com, - http://dev.fakeyou.com:7000, - http://dev.fakeyou.com:7001, - http://dev.fakeyou.com:7002, - http://dev.storyteller.ai, - http://dev.storyteller.ai:7000, - http://dev.storyteller.ai:7001, - http://dev.storyteller.ai:7002, - http://devproxy.fakeyou.com:7000, - http://devproxy.fakeyou.com:7001, - http://devproxy.fakeyou.com:7002, - http://devproxy.storyteller.ai, - http://devproxy.storyteller.ai:7000, - http://devproxy.storyteller.ai:7001, - http://devproxy.storyteller.ai:7002, - https://api.fakeyou.com, - https://engine.fakeyou.com, - https://fakeyou.com, - https://feature-mvp--fakeyou.netlify.app, - https://feature-mvp--storyteller-ai.netlify.app, - https://staging.fakeyou.com, - https://staging.storyteller.ai, - https://storyteller.ai, - https://example.com''' + # NB: This is potentially dangerous, but I don't think we have much other choice: + Access-Control-Allow-Origin = "*" From b36ee00d738e6b7967b66e4d3470393fd61d212e Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Sat, 9 Mar 2024 04:14:32 -0500 Subject: [PATCH 10/13] frame-ancestors --- netlify.toml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/netlify.toml b/netlify.toml index eaeac9f..15bbb6e 100644 --- a/netlify.toml +++ b/netlify.toml @@ -4,3 +4,7 @@ # NB: This is potentially dangerous, but I don't think we have much other choice: Access-Control-Allow-Origin = "*" + # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/frame-ancestors + # NB: The *.netlify.app is dangerous. + Content-Security-Policy = "frame-ancestors 'self' https://fakeyou.com https://*.fakeyou.com https://storyteller.ai https://*.storyteller.ai https://*.netlify.app;" + From a958cc82835f4efc35e0cc2c1957225c5921a7af Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Sat, 9 Mar 2024 13:10:08 -0500 Subject: [PATCH 11/13] remove origin checks - these might be the final CORS blockers --- .../lib/iframe-messaging-bridge.element.ts | 78 ++++++++++--------- 1 file changed, 41 insertions(+), 37 deletions(-) diff --git a/studio-web/src/lib/iframe-messaging-bridge.element.ts b/studio-web/src/lib/iframe-messaging-bridge.element.ts index 01915cb..d0bfa1c 100644 --- a/studio-web/src/lib/iframe-messaging-bridge.element.ts +++ b/studio-web/src/lib/iframe-messaging-bridge.element.ts @@ -4,52 +4,52 @@ import { SceneState, type SceneStateEvent } from "@storyteller/studio"; import { LitElement, nothing } from "lit"; import { customElement } from "lit/decorators.js"; -const PARENT_ORIGIN_WHITELIST: readonly string[] = [ - "https://fakeyou.com", - "https://storyteller.ai", - "https://staging.fakeyou.com", - "https://staging.storyteller.ai", - "https://feature-mvp--fakeyou.netlify.app", - "https://feature-mvp--storyteller-ai.netlify.app", - "http://dev.fakeyou.com:7000", - "http://dev.fakeyou.com:7001", - "http://dev.fakeyou.com:7002", - "http://devproxy.fakeyou.com:7000", - "http://devproxy.fakeyou.com:7001", - "http://devproxy.fakeyou.com:7002", - "http://dev.storyteller.ai:7000", - "http://dev.storyteller.ai:7001", - "http://dev.storyteller.ai:7002", - "http://devproxy.storyteller.ai:7000", - "http://devproxy.storyteller.ai:7001", - "http://devproxy.storyteller.ai:7002", - "http://localhost:7000", - "http://localhost:7001", - "http://localhost:7002", -]; +//const PARENT_ORIGIN_WHITELIST: readonly string[] = [ +// "https://fakeyou.com", +// "https://storyteller.ai", +// "https://staging.fakeyou.com", +// "https://staging.storyteller.ai", +// "https://feature-mvp--fakeyou.netlify.app", +// "https://feature-mvp--storyteller-ai.netlify.app", +// "http://dev.fakeyou.com:7000", +// "http://dev.fakeyou.com:7001", +// "http://dev.fakeyou.com:7002", +// "http://devproxy.fakeyou.com:7000", +// "http://devproxy.fakeyou.com:7001", +// "http://devproxy.fakeyou.com:7002", +// "http://dev.storyteller.ai:7000", +// "http://dev.storyteller.ai:7001", +// "http://dev.storyteller.ai:7002", +// "http://devproxy.storyteller.ai:7000", +// "http://devproxy.storyteller.ai:7001", +// "http://devproxy.storyteller.ai:7002", +// "http://localhost:7000", +// "http://localhost:7001", +// "http://localhost:7002", +//]; @customElement("sts-iframe-messaging-bridge") export class IframeMessagingBridgeElement extends LitElement { #parentWindow?: Window; - #parentOrigin?: string; + //#parentOrigin?: string; override connectedCallback(): void { console.log('window.parent', window.parent); - console.log('window.parent.origin', window.parent.origin); + //console.log('window.parent.origin', window.parent.origin); try { if ( window.parent && window.parent !== window - && PARENT_ORIGIN_WHITELIST.includes(window.parent.origin) + //&& PARENT_ORIGIN_WHITELIST.includes(window.parent.origin) ) { this.#parentWindow = window.parent; - this.#parentOrigin = window.parent.origin; + //this.#parentOrigin = window.parent.origin; } } catch { this.#parentWindow = undefined; - this.#parentOrigin = undefined; + //this.#parentOrigin = undefined; } super.connectedCallback(); @@ -58,10 +58,10 @@ export class IframeMessagingBridgeElement extends LitElement { @on("window:message") onParentMessage(event: MessageEvent): void { console.log('window.parent', window.parent); - console.log('window.parent.origin', window.parent.origin); + //console.log('window.parent.origin', window.parent.origin); - if (!this.#parentOrigin || event.origin !== this.#parentOrigin) - return; + //if (!this.#parentOrigin || event.origin !== this.#parentOrigin) + // return; if (event.data === "save-scene") studio.saveScene(); @@ -70,24 +70,28 @@ export class IframeMessagingBridgeElement extends LitElement { @on("window:scene-uploaded") onSceneUploaded(event: CustomEvent): void { console.log('window.parent', window.parent); - console.log('window.parent.origin', window.parent.origin); + //console.log('window.parent.origin', window.parent.origin); - if (!this.#parentWindow || !this.#parentOrigin) + if (!this.#parentWindow /*|| !this.#parentOrigin */) return; - this.#parentWindow.postMessage(`scene-saved:${event.detail}`, this.#parentOrigin); + // NB: We'll need to receive and validate the targetOrigin by another method since CORS + // removes our ability to inspect this via `window.parent.origin`. + //this.#parentWindow.postMessage(`scene-saved:${event.detail}`, this.#parentOrigin); + this.#parentWindow.postMessage(`scene-saved:${event.detail}`, "*"); } @on("window:scene-state") onSceneStateChange({ detail: state }: SceneStateEvent): void { console.log('window.parent', window.parent); - console.log('window.parent.origin', window.parent.origin); + //console.log('window.parent.origin', window.parent.origin); - if (!this.#parentWindow || !this.#parentOrigin) + if (!this.#parentWindow /* || !this.#parentOrigin */) return; if (state === SceneState.Active) - this.#parentWindow.postMessage("studio-ready", this.#parentOrigin); + //this.#parentWindow.postMessage("studio-ready", this.#parentOrigin); + this.#parentWindow.postMessage("studio-ready", "*"); } protected override render = () => nothing; From 98373637835f4c0c64040d76a861d86e117ec17d Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Sat, 9 Mar 2024 13:26:08 -0500 Subject: [PATCH 12/13] cleanup removal of iframe origin checks --- .../lib/iframe-messaging-bridge.element.ts | 43 +------------------ 1 file changed, 2 insertions(+), 41 deletions(-) diff --git a/studio-web/src/lib/iframe-messaging-bridge.element.ts b/studio-web/src/lib/iframe-messaging-bridge.element.ts index d0bfa1c..deca809 100644 --- a/studio-web/src/lib/iframe-messaging-bridge.element.ts +++ b/studio-web/src/lib/iframe-messaging-bridge.element.ts @@ -4,52 +4,23 @@ import { SceneState, type SceneStateEvent } from "@storyteller/studio"; import { LitElement, nothing } from "lit"; import { customElement } from "lit/decorators.js"; -//const PARENT_ORIGIN_WHITELIST: readonly string[] = [ -// "https://fakeyou.com", -// "https://storyteller.ai", -// "https://staging.fakeyou.com", -// "https://staging.storyteller.ai", -// "https://feature-mvp--fakeyou.netlify.app", -// "https://feature-mvp--storyteller-ai.netlify.app", -// "http://dev.fakeyou.com:7000", -// "http://dev.fakeyou.com:7001", -// "http://dev.fakeyou.com:7002", -// "http://devproxy.fakeyou.com:7000", -// "http://devproxy.fakeyou.com:7001", -// "http://devproxy.fakeyou.com:7002", -// "http://dev.storyteller.ai:7000", -// "http://dev.storyteller.ai:7001", -// "http://dev.storyteller.ai:7002", -// "http://devproxy.storyteller.ai:7000", -// "http://devproxy.storyteller.ai:7001", -// "http://devproxy.storyteller.ai:7002", -// "http://localhost:7000", -// "http://localhost:7001", -// "http://localhost:7002", -//]; - @customElement("sts-iframe-messaging-bridge") export class IframeMessagingBridgeElement extends LitElement { #parentWindow?: Window; - //#parentOrigin?: string; override connectedCallback(): void { console.log('window.parent', window.parent); - //console.log('window.parent.origin', window.parent.origin); try { if ( window.parent && window.parent !== window - //&& PARENT_ORIGIN_WHITELIST.includes(window.parent.origin) ) { this.#parentWindow = window.parent; - //this.#parentOrigin = window.parent.origin; } } catch { this.#parentWindow = undefined; - //this.#parentOrigin = undefined; } super.connectedCallback(); @@ -58,10 +29,6 @@ export class IframeMessagingBridgeElement extends LitElement { @on("window:message") onParentMessage(event: MessageEvent): void { console.log('window.parent', window.parent); - //console.log('window.parent.origin', window.parent.origin); - - //if (!this.#parentOrigin || event.origin !== this.#parentOrigin) - // return; if (event.data === "save-scene") studio.saveScene(); @@ -70,27 +37,21 @@ export class IframeMessagingBridgeElement extends LitElement { @on("window:scene-uploaded") onSceneUploaded(event: CustomEvent): void { console.log('window.parent', window.parent); - //console.log('window.parent.origin', window.parent.origin); - if (!this.#parentWindow /*|| !this.#parentOrigin */) + if (!this.#parentWindow) return; - // NB: We'll need to receive and validate the targetOrigin by another method since CORS - // removes our ability to inspect this via `window.parent.origin`. - //this.#parentWindow.postMessage(`scene-saved:${event.detail}`, this.#parentOrigin); this.#parentWindow.postMessage(`scene-saved:${event.detail}`, "*"); } @on("window:scene-state") onSceneStateChange({ detail: state }: SceneStateEvent): void { console.log('window.parent', window.parent); - //console.log('window.parent.origin', window.parent.origin); - if (!this.#parentWindow /* || !this.#parentOrigin */) + if (!this.#parentWindow) return; if (state === SceneState.Active) - //this.#parentWindow.postMessage("studio-ready", this.#parentOrigin); this.#parentWindow.postMessage("studio-ready", "*"); } From 7d5c4908744bac985f83d09dff4110e3735ae6ab Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Sun, 10 Mar 2024 05:13:48 -0400 Subject: [PATCH 13/13] CORS - add allowed frame ancestors for development --- netlify.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netlify.toml b/netlify.toml index 15bbb6e..e0eccd8 100644 --- a/netlify.toml +++ b/netlify.toml @@ -6,5 +6,5 @@ # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/frame-ancestors # NB: The *.netlify.app is dangerous. - Content-Security-Policy = "frame-ancestors 'self' https://fakeyou.com https://*.fakeyou.com https://storyteller.ai https://*.storyteller.ai https://*.netlify.app;" + Content-Security-Policy = "frame-ancestors 'self' https://fakeyou.com https://*.fakeyou.com https://storyteller.ai https://*.storyteller.ai https://*.netlify.app http://*.fakeyou.com:7000 http://*.fakeyou.com:7001 http://*.fakeyou.com:7002 http://*.storyteller.ai:7000 http://*.storyteller.ai:7001 http://*.storyteller.ai:7002;"