diff --git a/Directory.Build.props b/Directory.Build.props index e73a76e6..90dcc8e0 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -5,7 +5,7 @@ 2.0.0.0 2.0.0.0 - 5.120.1 + 5.123.0 OutSystems ReactView Copyright © OutSystems 2023 @@ -14,6 +14,9 @@ .npm-install-stamp + + + true diff --git a/ReactViewResources/AMDLoader/AMDLoader.ts b/ReactViewResources/AMDLoader/AMDLoader.ts index ffc074d9..ffffd8b3 100644 --- a/ReactViewResources/AMDLoader/AMDLoader.ts +++ b/ReactViewResources/AMDLoader/AMDLoader.ts @@ -11,6 +11,7 @@ namespace AMDLoader { export let timeout = 5000; export function getOrCreateDependencyPromise(name: string): Promise { + console.log("AMDLoader :: getOrCreateDependencyPromise: ", name); name = name.replace(/^.\//, "").toLowerCase(); if (!promises[name]) { promises[name] = new Promise((resolve, reject) => { @@ -29,12 +30,14 @@ namespace AMDLoader { } export function resolve(name: string, value: any): void { + console.log("AMDLoader :: resolve: ", name);1 getOrCreateDependencyPromise(name); // create promise if necessary resolves[name](value); defines[name] = true; } export function require(deps: string[], definition: Function): void { + console.log("AMDLoader :: require: ", deps); if (!deps || deps.length === 0) { definition.apply(null, []); return; @@ -49,6 +52,7 @@ namespace AMDLoader { } const define = function (name: string, deps: string[], definition: Function): void { + console.log("AMDLoader :: define: ", name); if (typeof name !== "string") { throw new Error("Unnamed modules are not supported"); } diff --git a/ReactViewResources/Loader/Internal/ComponentsRenderCache.ts b/ReactViewResources/Loader/Internal/ComponentsRenderCache.ts index 19dc2c35..516e349f 100644 --- a/ReactViewResources/Loader/Internal/ComponentsRenderCache.ts +++ b/ReactViewResources/Loader/Internal/ComponentsRenderCache.ts @@ -7,27 +7,29 @@ export interface IRenderCacheEntry { } export async function renderCachedView(view: ViewMetadata, componentSource: string, componentPropertiesHash: string): Promise { - if (!view.isMain) { - // disable render from cache for inner views, since react does not currently support portals hydration - return null; - } - - const componentCacheKey = componentSource + "|" + componentPropertiesHash; - - const cachedComponentHtml = localStorage.getItem(componentCacheKey); - if (cachedComponentHtml) { - // render cached component html to reduce time to first render - view.root!.innerHTML = cachedComponentHtml; - await waitForNextPaint(); - - // already on cache, skip storing on cache - return null; - } - - return { - cacheKey: componentCacheKey, - componentSource: componentSource - }; + return null; + // + // if (!view.isMain) { + // // disable render from cache for inner views, since react does not currently support portals hydration + // return null; + // } + // + // const componentCacheKey = componentSource + "|" + componentPropertiesHash; + // + // const cachedComponentHtml = localStorage.getItem(componentCacheKey); + // if (cachedComponentHtml) { + // // render cached component html to reduce time to first render + // view.root!.innerHTML = cachedComponentHtml; + // await waitForNextPaint(); + // + // // already on cache, skip storing on cache + // return null; + // } + // + // return { + // cacheKey: componentCacheKey, + // componentSource: componentSource + // }; } export function storeViewRenderInCache(view: ViewMetadata, cacheEntry: IRenderCacheEntry, maxPreRenderedCacheEntries: number): Promise { diff --git a/ReactViewResources/Loader/Internal/Loader.View.tsx b/ReactViewResources/Loader/Internal/Loader.View.tsx index 0c7f425e..e944358f 100644 --- a/ReactViewResources/Loader/Internal/Loader.View.tsx +++ b/ReactViewResources/Loader/Internal/Loader.View.tsx @@ -9,19 +9,36 @@ import { ViewMetadata } from "./ViewMetadata"; import { ViewPortalsCollection } from "./ViewPortalsCollection"; import { addView, deleteView } from "./ViewsCollection"; -export function createView(componentClass: any, properties: {}, view: ViewMetadata, componentName: string) { - componentClass.contextType = PluginsContext; +export function createView(componentClass: any, properties: {}, view: ViewMetadata, componentName: string, componentNativeObject: any, componentNativeObjectName: string) { + return ; +} +interface IViewProps { + componentClass: any; + properties: {}; + view: ViewMetadata; + componentName: string + componentNativeObject: any; + componentNativeObjectName: string +} + +const View = ({ componentClass, properties, view, componentName, componentNativeObject, componentNativeObjectName }: IViewProps) => { + componentClass.contextType = PluginsContext; const makeResourceUrl = (resourceKey: string, ...params: string[]) => formatUrl(view.name, resourceKey, ...params); + const pluginsContext = React.useRef(new PluginsContextHolder(Array.from(view.modules.values()))); + + React.useEffect(() => { + return () => { + pluginsContext.current.dispose(); + pluginsContext.current = null!; + } + }, []); + return ( - + - {React.createElement(componentClass, { ref: e => view.modules.set(componentName, e), ...properties })} diff --git a/ReactViewResources/Loader/Internal/NativeAPI.ts b/ReactViewResources/Loader/Internal/NativeAPI.ts index 2d0c8d5a..8a390087 100644 --- a/ReactViewResources/Loader/Internal/NativeAPI.ts +++ b/ReactViewResources/Loader/Internal/NativeAPI.ts @@ -33,5 +33,7 @@ export function notifyViewLoaded(viewName: string, id: string): void { } export function notifyViewDestroyed(viewName: string): void { - withAPI(api => api.notifyViewDestroyed(viewName)); -} \ No newline at end of file + withAPI(api => { + api.notifyViewDestroyed(viewName) + }); +} diff --git a/ReactViewResources/Loader/Internal/ResourcesLoader.ts b/ReactViewResources/Loader/Internal/ResourcesLoader.ts index a97e4de1..59e89bc7 100644 --- a/ReactViewResources/Loader/Internal/ResourcesLoader.ts +++ b/ReactViewResources/Loader/Internal/ResourcesLoader.ts @@ -4,21 +4,47 @@ import { Task } from "./Task"; import { ViewMetadata } from "./ViewMetadata"; import { getView } from "./ViewsCollection"; +console.log("Loading resources loader file..."); +const LoadedScriptsKey = "LOADED_SCRIPTS_KEY"; +const ScriptLoadTasksKey = "SCRIPT_LOAD_TASKS_KEY"; +let loadedScripts: Set = window[LoadedScriptsKey]; +let scriptLoadTasks: Map> = window[ScriptLoadTasksKey]; + +if (!loadedScripts) { + console.log("Create new 'loadedScripts' SET !!"); + loadedScripts = new Set(); + window[LoadedScriptsKey] = loadedScripts; +} + +if (!scriptLoadTasks) { + console.log("Create new 'scriptLoadTasks' MAP !!"); + scriptLoadTasks = new Map>(); + window[LoadedScriptsKey] = loadedScripts; +} + export function loadScript(scriptSrc: string, view: ViewMetadata): Promise { return new Promise(async (resolve) => { - const frameScripts = view.scriptsLoadTasks; - // check if script was already added, fallback to main frame - const scriptLoadTask = frameScripts.get(scriptSrc) || !view.isMain ? getView(mainFrameName).scriptsLoadTasks.get(scriptSrc) : null; + const scriptLoadTask = scriptLoadTasks.get(scriptSrc) || null; if (scriptLoadTask) { + console.log("Wait loading TASK for: " + scriptSrc); // wait for script to be loaded await scriptLoadTask.promise; + scriptLoadTasks.delete(scriptSrc); + resolve(); + return; + } + + if(loadedScripts.has(scriptSrc)) { + console.log("Load skipped for: " + scriptSrc); resolve(); return; } + console.log("Load script Task: " + scriptSrc); + loadedScripts.add(scriptSrc); const loadTask = new Task(); - frameScripts.set(scriptSrc, loadTask); + scriptLoadTasks.set(scriptSrc, loadTask); const script = document.createElement("script"); script.src = scriptSrc; @@ -54,18 +80,51 @@ export function loadStyleSheet(stylesheet: string, containerElement: Element, ma } function waitForLoad(element: T, url: string, timeout: number): Promise { - return new Promise((resolve) => { - const timeoutHandle = setTimeout( - () => { - if (isDebugModeEnabled) { - showWarningMessage(`Timeout loading resouce: '${url}'. If you paused the application to debug, you may disregard this message.`); - } - }, - timeout); - - element.addEventListener("load", () => { - clearTimeout(timeoutHandle); + return new Promise((resolve, reject) => { + // const timeoutHandle = setTimeout( + // () => { + // if (isDebugModeEnabled) { + // showWarningMessage(`Timeout loading resouce: '${url}'. If you paused the application to debug, you may disregard this message.`); + // } + // }, + // timeout); + + // element.addEventListener("load", () => { + // clearTimeout(timeoutHandle); + // resolve(element); + // }); + + let timeoutHandle: number | undefined; + + // Define handlers ahead of time + const loadHandler = () => { + cleanup(); resolve(element); - }); + }; + + const errorHandler = () => { + cleanup(); + reject(new Error(`Failed to load resource: '${url}'`)); + }; + + // This central function is key to disposing of everything + const cleanup = () => { + clearTimeout(timeoutHandle); + element.removeEventListener("load", loadHandler); + element.removeEventListener("error", errorHandler); + }; + + // Set the timeout to reject the promise and clean up + timeoutHandle = setTimeout(() => { + cleanup(); + const message = `Timeout loading resource: '${url}'.`; + if (isDebugModeEnabled) { + showWarningMessage(`${message} If you paused the application to debug, you may disregard this message.`); + } + reject(new Error(message)); + }, timeout); + + element.addEventListener("load", loadHandler); + element.addEventListener("error", errorHandler); }); } \ No newline at end of file diff --git a/ReactViewResources/Loader/Internal/ViewMetadata.ts b/ReactViewResources/Loader/Internal/ViewMetadata.ts index 53426d56..68c37596 100644 --- a/ReactViewResources/Loader/Internal/ViewMetadata.ts +++ b/ReactViewResources/Loader/Internal/ViewMetadata.ts @@ -9,7 +9,6 @@ export type ViewMetadata = { placeholder: Element; // element were the view is mounted (where the shadow root is mounted in case of child views) root?: Element; // view root element head?: Element; // view head element - scriptsLoadTasks: Map>; // maps scripts urls to load tasks pluginsLoadTask: Task; // plugins load task viewLoadTask: Task; // resolved when view is loaded modules: Map; // maps module name to module instance @@ -33,7 +32,6 @@ export function newView(id: number, name: string, isMain: boolean, placeholder: nativeObjectNames: [], pluginsLoadTask: new Task(), viewLoadTask: new Task(), - scriptsLoadTasks: new Map>(), childViews: new ObservableListCollection(), context: null, parentView: null! diff --git a/ReactViewResources/Loader/Internal/ViewPortal.tsx b/ReactViewResources/Loader/Internal/ViewPortal.tsx index 239e7728..e8841e6d 100644 --- a/ReactViewResources/Loader/Internal/ViewPortal.tsx +++ b/ReactViewResources/Loader/Internal/ViewPortal.tsx @@ -28,12 +28,13 @@ interface IViewPortalState { * */ export class ViewPortal extends React.Component { - private head: Element; - private shadowRoot: HTMLElement; + private head: Element | null = null; + private shadowRoot: HTMLElement | null = null; constructor(props: IViewPortalProps, context: any) { super(props, context); + debugger; this.state = { component: null! }; this.shadowRoot = props.view.placeholder.attachShadow({ mode: "open" }).getRootNode() as HTMLElement; @@ -42,6 +43,7 @@ export class ViewPortal extends React.Component {component} @@ -56,21 +58,24 @@ export class ViewPortal extends React.Component s.dataset.sticky === "true"); - stylesheets.forEach(s => this.head.appendChild(document.importNode(s, true))); + stylesheets.forEach(s => this.head!.appendChild(document.importNode(s, true))); this.props.viewMounted(this.props.view); } public componentWillUnmount() { + this.head = null; + this.shadowRoot = null; this.props.viewUnmounted(this.props.view); } @@ -90,6 +95,6 @@ export class ViewPortal extends React.Component , - this.shadowRoot); + this.shadowRoot!); } } \ No newline at end of file diff --git a/ReactViewResources/Loader/Internal/ViewPropertiesProxy.ts b/ReactViewResources/Loader/Internal/ViewPropertiesProxy.ts index e5a60ae7..8ba295bb 100644 --- a/ReactViewResources/Loader/Internal/ViewPropertiesProxy.ts +++ b/ReactViewResources/Loader/Internal/ViewPropertiesProxy.ts @@ -10,7 +10,6 @@ export function createPropertiesProxy(rootElement: Element, objProperties: {}, n } else { proxy[key] = async function () { const nativeObject = window[nativeObjName] || await bindNativeObject(nativeObjName); - const result = nativeObject[key].apply(window, arguments); if (componentRenderedWaitTask) { diff --git a/ReactViewResources/Loader/Internal/ViewsCollection.ts b/ReactViewResources/Loader/Internal/ViewsCollection.ts index e54f97b0..32719013 100644 --- a/ReactViewResources/Loader/Internal/ViewsCollection.ts +++ b/ReactViewResources/Loader/Internal/ViewsCollection.ts @@ -3,6 +3,8 @@ import { modulesFunctionName } from "./Environment"; const views = new Map(); +window["my-views"] = views; // for debugging + export function addView(name: string, view: ViewMetadata): void { views.set(name, view); } @@ -23,6 +25,7 @@ export function getView(viewName: string): ViewMetadata { return view; } +console.log("modulesFunctionName", modulesFunctionName); window[modulesFunctionName] = function getModule(viewName: string, id: string, moduleName: string) { const view = views.get(viewName); if (view && view.id.toString() === id) { @@ -35,7 +38,7 @@ window[modulesFunctionName] = function getModule(viewName: string, id: string, m return new Proxy({}, { get: function () { - // return a dummy function, call will be ingored, but no exception thrown + // return a dummy function, call will be ignored, but no exception thrown return new Function(); } }); diff --git a/ReactViewResources/Loader/Loader.ts b/ReactViewResources/Loader/Loader.ts index 0e4ff86d..da52813b 100644 --- a/ReactViewResources/Loader/Loader.ts +++ b/ReactViewResources/Loader/Loader.ts @@ -115,6 +115,8 @@ export function loadPlugins(plugins: any[][], frameName: string): void { innerLoad(); } +let createView; + export function loadComponent( componentName: string, componentNativeObjectName: string, @@ -135,7 +137,7 @@ export function loadComponent( // wait for the stylesheet to load before first render await defaultStylesheetLoadTask.promise; } - + view = tryGetView(frameName)!; if (!view) { return; // view was probably unloaded @@ -174,20 +176,23 @@ export function loadComponent( throw new Error(`Component ${componentName} is not defined or does not have a default class`); } - const { createView } = await import("./Internal/Loader.View"); + if (!createView) { + console.log("LOAD 'Internal/Loader.View' !!") + const loader = await import("./Internal/Loader.View"); + createView = loader.createView; + } - const viewElement = createView(componentClass, properties, view, componentName); + const viewElement = createView(componentClass, properties, view, componentName, componentNativeObject, componentNativeObjectName); const render = view.renderHandler; if (!render) { throw new Error(`View ${view.name} render handler is not set`); } - + await render(viewElement); - await waitForNextPaint(); if (cacheEntry) { - storeViewRenderInCache(view, cacheEntry, maxPreRenderedCacheEntries); // dont need to await + //storeViewRenderInCache(view, cacheEntry, maxPreRenderedCacheEntries); // dont need to await } // queue view loaded notification to run after all other pending promise notifications (ensure order) diff --git a/ReactViewResources/Loader/Public/PluginsContext.ts b/ReactViewResources/Loader/Public/PluginsContext.ts index bac77f2c..4a89e684 100644 --- a/ReactViewResources/Loader/Public/PluginsContext.ts +++ b/ReactViewResources/Loader/Public/PluginsContext.ts @@ -14,6 +14,11 @@ export class PluginsContextHolder implements IPluginsContext { public getPluginInstance(_class: Type) { return this.pluginInstances.get(_class.name); } + + public dispose(): void { + this.pluginInstances.clear(); + this.pluginInstances = null!; + } } export const PluginsContext = React.createContext(null!); diff --git a/ReactViewResources/Loader/Public/ViewFrame.tsx b/ReactViewResources/Loader/Public/ViewFrame.tsx index a2ed29be..ab111eaa 100644 --- a/ReactViewResources/Loader/Public/ViewFrame.tsx +++ b/ReactViewResources/Loader/Public/ViewFrame.tsx @@ -1,29 +1,59 @@ -import * as React from "react"; -import { IViewFrameProps } from "ViewFrame"; +import * as React from "react"; +import * as ReactDOM from "react-dom"; +// import { IViewFrameProps } from "ViewFrame"; import { newView, ViewMetadata } from "../Internal/ViewMetadata"; import { ViewMetadataContext } from "../Internal/ViewMetadataContext"; import { ViewSharedContext } from "./ViewSharedContext"; +import { getStylesheets } from "../Internal/Common"; +import {addView, deleteView} from "../Internal/ViewsCollection"; +import {notifyViewDestroyed, notifyViewInitialized} from "../Internal/NativeAPI"; +import {handleError} from "../Internal/ErrorHandler"; +import {webViewRootId} from "../Internal/Environment"; + +function onChildViewAdded(childView: ViewMetadata) { + addView(childView.name, childView); + notifyViewInitialized(childView.name); +} + +function onChildViewRemoved(childView: ViewMetadata) { + deleteView(childView.name); + notifyViewDestroyed(childView.name); +} + +function onChildViewErrorRaised(childView: ViewMetadata, error: Error) { + handleError(error, childView); +} + +// --- INTERFACES --- + +// Update the props interface to formally include a 'loaded' callback +// that provides the ViewMetadata, which will contain our render handler. +export interface IViewFrameProps { + name: string | number; + className?: string; + context?: T; + loaded?: () => void; +} interface IInternalViewFrameProps extends IViewFrameProps { viewMetadata: ViewMetadata; context: any; } -/** - * Placeholder were a child view is mounted. - * */ -export class ViewFrame extends React.Component, {}, ViewMetadata> { +interface IInternalViewFrameState { + // This will hold the component passed to the renderHandler + componentToRender: React.ReactElement | null; +} - constructor(props: IViewFrameProps, context: any) { - super(props, context); - } +// --- PUBLIC COMPONENT (Unchanged) --- +export class ViewFrame extends React.Component, {}> { public render(): JSX.Element { return ( {viewMetadata => - {viewcontext => } + {viewContext => } } @@ -31,97 +61,264 @@ export class ViewFrame extends React.Component, {}, ViewMe } } -class InternalViewFrame extends React.Component, {}, ViewMetadata> { +// --- INTERNAL COMPONENT (Refactored for renderHandler) --- +class InternalViewFrame extends React.Component, IInternalViewFrameState> { private static generation = 0; - - private generation: number; - private placeholder: Element; - private replacement: Element; - + private readonly generation: number; + + private placeholder: HTMLDivElement | null = null; + private replacement: Element | null = null; + + private shadowRoot: Element | null = null; + private head: HTMLElement | null; + private root: HTMLElement | null; + constructor(props: IInternalViewFrameProps, context: any) { super(props, context); - if (props.name === "") { - throw new Error("View Frame name must be specified (not empty)"); - } - if (!/^[A-Za-z_][A-Za-z0-9_]*/.test(props.name as string)) { - // must be a valid js symbol name - throw new Error("View Frame name can only contain letters, numbers or _"); - } + this.state = { + componentToRender: null, // Initialize component as null + }; - // keep track of this frame generation, so that we can keep tracking the most recent frame instance - this.generation = ++InternalViewFrame.generation; + if (props.name === "") throw new Error("View Frame name must be specified (not empty)"); + if (!/^[A-Za-z_][A-Za-z0-9_]*/.test(props.name as string)) throw new Error("View Frame name can only contain letters, numbers or _"); - const view = this.getView(); - if (view) { - // update the existing view generation - view.generation = this.generation; - } + this.generation = ++InternalViewFrame.generation; } - private get fullName() { - const parentName = this.parentView.name; + // ... (getters like fullName, parentView, getView remain the same) ... + private get fullName(): string { + const parentName = this.props.viewMetadata.name; // @ts-ignore return (parentName ? (parentName + ".") : "") + this.props.name; } - public shouldComponentUpdate(): boolean { - // prevent component updates - return false; - } - private get parentView(): ViewMetadata { return this.props.viewMetadata; } private getView(): ViewMetadata | undefined { - const fullName = this.fullName; - return this.parentView.childViews.items.find(c => c.name === fullName); + return this.parentView.childViews.items.find(c => c.name === this.fullName); } + /** + * This method will be exposed as the renderHandler. It sets the component + * in state, which causes the portal to render it. + */ + private renderInPortal = (component: React.ReactElement): Promise => { + const view = this.getView(); + if (!view) { + return Promise.reject(new Error("ViewFrame not mounted or already destroyed.")); + } + + const wrappedComponent = ( + + {component} + + ); + + return new Promise(resolve => { + this.setState({ componentToRender: wrappedComponent }, resolve); + }); + }; + public componentDidMount() { + // if (!this.placeholder || !this.root || !this.head || !this.shadowRoot) { + // // Should never happen. consider removing + // return; + // } + // + // const existingView = this.getView(); + // if (existingView) { + // this.replacement = existingView.placeholder; + // this.placeholder.parentElement!.replaceChild(this.replacement, this.placeholder); + // return; + // } + // + // const id = this.generation; + // const childView = newView(id, this.fullName, false, this.placeholder); + // childView.generation = this.generation; + // childView.parentView = this.parentView; + // childView.context = this.props.context; + // + // debugger + // + // // Notify the parent that the frame is ready by calling the 'loaded' prop + // const loadedHandler = this.props.loaded; + // if (loadedHandler) { + // // The handler receives the view object, which now contains our renderHandler + // childView.viewLoadTask.promise.then(() => loadedHandler()); + // } + // + // // view portal + // // **KEY CHANGE**: Attach our render method to the metadata object + // + // + // // const shadowRoot = this.placeholder.attachShadow({ mode: "open" }).getRootNode() as HTMLElement; + // // const head = document.createElement("head"); + // // const body = document.createElement("body"); + // // const portalRootDiv = document.createElement("div"); + // // portalRootDiv.id = webViewRootId; + // + // const styleResets = document.createElement("style"); + // styleResets.innerHTML = ":host { all: initial; display: block; }"; + // this.head.appendChild(styleResets); + // + // // get sticky stylesheets + // const stylesheets = getStylesheets(document.head).filter(s => s.dataset.sticky === "true"); + // stylesheets.forEach(s => this.head?.appendChild(document.importNode(s, true))); + // + // // body.appendChild(portalRootDiv); + // // shadowRoot.appendChild(head); + // // shadowRoot.appendChild(body); + // + // childView.head = this.head; + // childView.root = this.root; + // + // this.parentView.childViews.add(childView); + // + // childView.renderHandler = component => this.renderInPortal(component); + // // Set the mount point to enable the portal, but render no content yet. + // // this.setState({ portalMountPoint: portalRootDiv }); + // onChildViewAdded(childView); + } + + public componentWillUnmount() { + + if (this.replacement) { + this.replacement.parentElement!.replaceChild(this.placeholder!, this.replacement); + } + + const existingView = this.getView(); + if (existingView && this.generation === existingView.generation) { + this.parentView.childViews.remove(existingView); + onChildViewRemoved(existingView); + console.log("unmount internal view frame", this.fullName); + } + + // if (this.placeholder && this.shadowRoot) { + // this.placeholder?.removeChild(this.shadowRoot) + // } + + this.setState({ componentToRender: null }); + + this.placeholder = null; + this.replacement = null; + this.shadowRoot = null; + this.head = null; + this.root = null; + } + + public componentDidCatch(error: Error, errorInfo: React.ErrorInfo) { + const existingView = this.getView(); + if (!existingView) { + // View is not available, log error generically + return; + } + // execute error handling inside promise, to avoid the error handler to rethrow exception inside componentDidCatch + Promise.resolve(null).then(() => onChildViewErrorRaised(existingView, error)); + } + + private setContainer = (element: HTMLDivElement) => { + this.placeholder = element; + if (this.placeholder && !this.shadowRoot) { + // create an open shadow-dom, so that bubbled events expose the inner element + this.shadowRoot = this.placeholder.attachShadow({ mode: "open" }).getRootNode() as Element; + this.forceUpdate(); + } + } + + private setRoot = (element: HTMLDivElement) => { + this.root = element; + + if (!this.placeholder || !this.root || !this.head || !this.shadowRoot) { + return; + } + const existingView = this.getView(); if (existingView) { - // there's a view already rendered, insert in current frame's placeholder this.replacement = existingView.placeholder; this.placeholder.parentElement!.replaceChild(this.replacement, this.placeholder); return; } - const id = this.generation; // for this purpose we can use generation (we just need a unique number) + const id = this.generation; const childView = newView(id, this.fullName, false, this.placeholder); childView.generation = this.generation; childView.parentView = this.parentView; childView.context = this.props.context; + // Notify the parent that the frame is ready by calling the 'loaded' prop const loadedHandler = this.props.loaded; if (loadedHandler) { + // The handler receives the view object, which now contains our renderHandler childView.viewLoadTask.promise.then(() => loadedHandler()); } - this.parentView.childViews.add(childView); - } + // view portal + // **KEY CHANGE**: Attach our render method to the metadata object - public componentWillUnmount() { - if (this.replacement) { - // put back the original container, otherwise react will complain - this.replacement.parentElement!.replaceChild(this.placeholder, this.replacement); - } - const existingView = this.getView(); - if (existingView && this.generation === existingView.generation) { - // this is the most recent frame - meaning it was not replaced by another one - so the view should be removed - this.parentView.childViews.remove(existingView); + // const shadowRoot = this.placeholder.attachShadow({ mode: "open" }).getRootNode() as HTMLElement; + // const head = document.createElement("head"); + // const body = document.createElement("body"); + // const portalRootDiv = document.createElement("div"); + // portalRootDiv.id = webViewRootId; + + const styleResets = document.createElement("style"); + styleResets.innerHTML = ":host { all: initial; display: block; }"; + this.head.appendChild(styleResets); + + // get sticky stylesheets + const stylesheets = getStylesheets(document.head).filter(s => s.dataset.sticky === "true"); + stylesheets.forEach(s => this.head?.appendChild(document.importNode(s, true))); + + // body.appendChild(portalRootDiv); + // shadowRoot.appendChild(head); + // shadowRoot.appendChild(body); + + childView.head = this.head; + childView.root = this.root; + + this.parentView.childViews.add(childView); + + childView.renderHandler = component => this.renderInPortal(component); + // Set the mount point to enable the portal, but render no content yet. + // this.setState({ portalMountPoint: portalRootDiv }); + onChildViewAdded(childView); + } + + private renderPortal() { + if (!this.shadowRoot) { + return null; } + + return ReactDOM.createPortal( + <> + this.head = e!}> + + +
+ {this.state.componentToRender ? this.state.componentToRender : null} +
+ + , + this.shadowRoot); } - + public render() { - return
this.placeholder = e!} className={this.props.className} />; + return ( +
+ {/* The portal now renders the component from the state */} + {/*{portalMountPoint && componentToRender && ReactDOM.createPortal(componentToRender, portalMountPoint)}*/} + {this.renderPortal()} +
+ ); } } window["ViewFrame"] = { ViewFrame: ViewFrame, ViewSharedContext: ViewSharedContext -}; +}; \ No newline at end of file diff --git a/ReactViewResources/Loader/Public/ViewFrameLegacy.tsx b/ReactViewResources/Loader/Public/ViewFrameLegacy.tsx new file mode 100644 index 00000000..bfeb836f --- /dev/null +++ b/ReactViewResources/Loader/Public/ViewFrameLegacy.tsx @@ -0,0 +1,127 @@ +import * as React from "react"; +import { IViewFrameProps } from "ViewFrame"; +import { newView, ViewMetadata } from "../Internal/ViewMetadata"; +import { ViewMetadataContext } from "../Internal/ViewMetadataContext"; +import { ViewSharedContext } from "./ViewSharedContext"; + +interface IInternalViewFrameProps extends IViewFrameProps { + viewMetadata: ViewMetadata; + context: any; +} + +/** + * Placeholder were a child view is mounted. + * */ +export class ViewFrameLegacy extends React.Component, {}, ViewMetadata> { + + constructor(props: IViewFrameProps, context: any) { + super(props, context); + } + + public render(): JSX.Element { + return ( + + {viewMetadata => + + {viewcontext => } + + } + + ); + } +} + +class InternalViewFrame extends React.Component, {}, ViewMetadata> { + + private static generation = 0; + + private generation: number; + private placeholder: Element; + private replacement: Element; + + constructor(props: IInternalViewFrameProps, context: any) { + super(props, context); + if (props.name === "") { + throw new Error("View Frame name must be specified (not empty)"); + } + + if (!/^[A-Za-z_][A-Za-z0-9_]*/.test(props.name as string)) { + // must be a valid js symbol name + throw new Error("View Frame name can only contain letters, numbers or _"); + } + + // keep track of this frame generation, so that we can keep tracking the most recent frame instance + this.generation = ++InternalViewFrame.generation; + + const view = this.getView(); + if (view) { + // update the existing view generation + view.generation = this.generation; + } + } + + private get fullName() { + const parentName = this.parentView.name; + // @ts-ignore + return (parentName ? (parentName + ".") : "") + this.props.name; + } + + public shouldComponentUpdate(): boolean { + // prevent component updates + return false; + } + + private get parentView(): ViewMetadata { + return this.props.viewMetadata; + } + + private getView(): ViewMetadata | undefined { + const fullName = this.fullName; + return this.parentView.childViews.items.find(c => c.name === fullName); + } + + public componentDidMount() { + const existingView = this.getView(); + if (existingView) { + // there's a view already rendered, insert in current frame's placeholder + this.replacement = existingView.placeholder; + this.placeholder.parentElement!.replaceChild(this.replacement, this.placeholder); + return; + } + + const id = this.generation; // for this purpose we can use generation (we just need a unique number) + const childView = newView(id, this.fullName, false, this.placeholder); + childView.generation = this.generation; + childView.parentView = this.parentView; + childView.context = this.props.context; + + const loadedHandler = this.props.loaded; + if (loadedHandler) { + childView.viewLoadTask.promise.then(() => loadedHandler()); + } + + this.parentView.childViews.add(childView); + } + + public componentWillUnmount() { + if (this.replacement) { + // put back the original container, otherwise react will complain + this.replacement.parentElement!.replaceChild(this.placeholder, this.replacement); + } + + const existingView = this.getView(); + if (existingView && this.generation === existingView.generation) { + // this is the most recent frame - meaning it was not replaced by another one - so the view should be removed + this.parentView.childViews.remove(existingView); + } + } + + public render() { + return
this.placeholder = e!} className={this.props.className} />; + } +} + +window["ViewFrameLegacy"] = { + ViewFrame: ViewFrameLegacy, + ViewSharedContext: ViewSharedContext +}; diff --git a/Sample.Avalonia/App.xaml.cs b/Sample.Avalonia/App.xaml.cs index cb360f0f..23ae155a 100644 --- a/Sample.Avalonia/App.xaml.cs +++ b/Sample.Avalonia/App.xaml.cs @@ -3,20 +3,19 @@ using Avalonia.Markup.Xaml; using WebViewControl; -namespace Sample.Avalonia { +namespace Sample.Avalonia; - public class App : Application { +public class App : Application { + public override void Initialize() { + AvaloniaXamlLoader.Load(this); + WebView.Settings.OsrEnabled = false; + } - public override void Initialize() { - AvaloniaXamlLoader.Load(this); - WebView.Settings.OsrEnabled = false; + public override void OnFrameworkInitializationCompleted() { + if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) { + desktop.MainWindow = new MainWindow(); } - public override void OnFrameworkInitializationCompleted() { - if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) { - desktop.MainWindow = new MainWindow(); - } - base.OnFrameworkInitializationCompleted(); - } + base.OnFrameworkInitializationCompleted(); } -} +} \ No newline at end of file diff --git a/Sample.Avalonia/ExtendedReactViewFactory.cs b/Sample.Avalonia/ExtendedReactViewFactory.cs index 79324c0e..6a31a5f7 100644 --- a/Sample.Avalonia/ExtendedReactViewFactory.cs +++ b/Sample.Avalonia/ExtendedReactViewFactory.cs @@ -16,7 +16,7 @@ public override IViewModule[] InitializePlugins() { return new IViewModule[] { viewPlugin }; } - public override bool ShowDeveloperTools => false; + public override bool ShowDeveloperTools => true; public override bool EnableViewPreload => true; diff --git a/Sample.Avalonia/MainView/MainView.tsx b/Sample.Avalonia/MainView/MainView.tsx index efb6092a..2423b2d9 100644 --- a/Sample.Avalonia/MainView/MainView.tsx +++ b/Sample.Avalonia/MainView/MainView.tsx @@ -4,7 +4,8 @@ import ViewPlugin from "./../ViewPlugin/ViewPlugin"; import { IPluginsContext } from "PluginsProvider"; import "./MainView.scss"; // import a stylesheet import TaskListView from "./../TaskListView/TaskListView"; // import another component -import * as BackgroundImage from "./Tasks.png"; // import images +import * as BackgroundImage from "./Tasks.png"; +import UIEditorLike from "../UiEditorLike/UIEditorLike"; // import images export interface ITaskCreationDetails { text: string; @@ -18,8 +19,10 @@ export enum BackgroundKind { // component properties ... the interface name must start with I prefix and end with Properties suffix export interface IMainViewProperties { + getInnerViewName(): string; getTasksCount(): Promise; taskListShown(): void; + innerViewEditorShown(): void; inputChanged(): void; addTaskButtonClicked(taskDetails: ITaskCreationDetails): void; readonly titleMessage: string; @@ -29,10 +32,11 @@ export interface IMainViewProperties { // component methods that can be called on .NET ... the interface name must start with I prefix and end with Behaviors suffix export interface IMainViewBehaviors { refresh(): void; + refreshInnerView(): void; } export interface IChildViews { - ListView: TaskListView; + ListView: TaskListView } enum TaskListShowStatus { @@ -44,6 +48,8 @@ enum TaskListShowStatus { interface MainViewState { tasksCount: number; taskListShowStatus: TaskListShowStatus; + editorViewName: string; + renderUIEditor: boolean; } export default class MainView extends React.Component implements IMainViewBehaviors { @@ -59,12 +65,22 @@ export default class MainView extends React.Component { this.state = { + editorViewName: null, tasksCount: 0, - taskListShowStatus: TaskListShowStatus.Show + taskListShowStatus: TaskListShowStatus.Show, + renderUIEditor: false, }; this.refresh(); } + public async refreshInnerView(): Promise { + console.log("Refreshing inner view..."); + const editorViewName = await this.props.getInnerViewName(); + + console.log("Refreshing inner view: Name: ", editorViewName); + this.setState({ editorViewName }, () => this.props.innerViewEditorShown()); + } + public refresh(): void { (async () => { const tasksCount = await this.props.getTasksCount(); @@ -94,6 +110,10 @@ export default class MainView extends React.Component { + this.setState(prevState => ({renderUIEditor: !prevState.renderUIEditor})); + } + private onAddTaskButtonClicked = () => { const input = this.inputRef.current; this.props.addTaskButtonClicked({ text: input.value }); @@ -118,16 +138,22 @@ export default class MainView extends React.Component; + } + + private renderUIEditor(): JSX.Element { + return <> + + {this.state.renderUIEditor && } + ; + } + public render(): JSX.Element { - return ( -
-
{this.props.titleMessage}
- this.props.inputChanged()} /> - - - {this.renderListView()} -
{this.state.tasksCount} task(s)
-
- ); + return this.renderUIEditor(); } } \ No newline at end of file diff --git a/Sample.Avalonia/MainView/MainViewAdapter.cs b/Sample.Avalonia/MainView/MainViewAdapter.cs new file mode 100644 index 00000000..79630363 --- /dev/null +++ b/Sample.Avalonia/MainView/MainViewAdapter.cs @@ -0,0 +1,24 @@ +using ReactViewControl; + +namespace Sample.Avalonia; + +partial class MainView { + private uint counter; + private string currentEditorViewName; + + public IViewModule ToggleEditorView() { + if (counter == 0) { + GetInnerViewName += () => currentEditorViewName; + } + + var childViewName = "canvas-" + counter++; + currentEditorViewName = childViewName; + + IViewModule view = counter % 2 == 0 + ? MainModule.GetOrAddChildView(childViewName) + : MainModule.GetOrAddChildView(childViewName); + + RefreshInnerView(); + return view; + } +} \ No newline at end of file diff --git a/Sample.Avalonia/MainWindow.xaml b/Sample.Avalonia/MainWindow.xaml index 0968b1b7..55276dd4 100644 --- a/Sample.Avalonia/MainWindow.xaml +++ b/Sample.Avalonia/MainWindow.xaml @@ -30,6 +30,7 @@ + diff --git a/Sample.Avalonia/MainWindow.xaml.cs b/Sample.Avalonia/MainWindow.xaml.cs index c10f7701..8b0bd8e7 100644 --- a/Sample.Avalonia/MainWindow.xaml.cs +++ b/Sample.Avalonia/MainWindow.xaml.cs @@ -72,6 +72,8 @@ public void CreateTab() { private void OnToggleThemeStyleSheetMenuItemClick(object sender, RoutedEventArgs e) => Settings.IsLightTheme = !Settings.IsLightTheme; private void OnShowDevToolsMenuItemClick(object sender, RoutedEventArgs e) => SelectedView.ShowDevTools(); + + private void OnToggleEditorView(object sender, RoutedEventArgs e) => SelectedView.ToggleCustomInnerView(); private void OnToggleIsEnabledMenuItemClick(object sender, RoutedEventArgs e) => SelectedView.ToggleIsEnabled(); diff --git a/Sample.Avalonia/Sample.Avalonia.csproj b/Sample.Avalonia/Sample.Avalonia.csproj index 48a6e45e..11b201a1 100644 --- a/Sample.Avalonia/Sample.Avalonia.csproj +++ b/Sample.Avalonia/Sample.Avalonia.csproj @@ -23,6 +23,8 @@ + + diff --git a/Sample.Avalonia/TabView.cs b/Sample.Avalonia/TabView.cs index 198200a8..c138ff15 100644 --- a/Sample.Avalonia/TabView.cs +++ b/Sample.Avalonia/TabView.cs @@ -30,6 +30,9 @@ public TabView(int id) { mainView.AddTaskButtonClicked += OnMainViewAddTaskButtonClicked; mainView.GetTasksCount += () => taskList.Count; mainView.TaskListShown += () => taskListView.Load(); + mainView.InnerViewEditorShown += () => { + innerView.Load(); + }; mainView.WithPlugin().NotifyViewLoaded += viewName => AppendLog(viewName + " loaded"); taskListView = (TaskListViewModule)mainView.ListView; @@ -41,9 +44,12 @@ public TabView(int id) { taskListView.WithPlugin().NotifyViewLoaded += (viewName) => AppendLog(viewName + " loaded (child)"); taskListView.Load(); + innerView = mainView.ToggleEditorView(); Content = mainView; } + private IViewModule innerView; + private void OnMainViewAddTaskButtonClicked(TaskCreationDetails taskDetails) { taskList.Add(new Task() { id = taskCounter++, @@ -54,6 +60,10 @@ private void OnMainViewAddTaskButtonClicked(TaskCreationDetails taskDetails) { taskListView.Refresh(); // refresh task list AppendLog("Added task: " + taskDetails.text); } + + public void ToggleCustomInnerView() { + innerView = mainView.ToggleEditorView(); + } public void ToggleHideCompletedTasks() => taskListView.ToggleHideCompletedTasks(); diff --git a/Sample.Avalonia/TaskListView/TaskListView.tsx b/Sample.Avalonia/TaskListView/TaskListView.tsx index 341f602c..3f9b3cb8 100644 --- a/Sample.Avalonia/TaskListView/TaskListView.tsx +++ b/Sample.Avalonia/TaskListView/TaskListView.tsx @@ -71,7 +71,7 @@ export default class TaskListView extends React.Component { - const tasks = await this.props.getTasks(); + const tasks = (await this.props.getTasks()) || []; this.setState({ tasks }); })(); } diff --git a/Sample.Avalonia/UiEditorLike/Component/TaskUI.ts b/Sample.Avalonia/UiEditorLike/Component/TaskUI.ts new file mode 100644 index 00000000..7fa0df3f --- /dev/null +++ b/Sample.Avalonia/UiEditorLike/Component/TaskUI.ts @@ -0,0 +1,24 @@ +export class TaskUI { + private taskPromise: Promise; + private resolve: (result: ResultType) => void; + private reject: (error: Error) => void; + + constructor() { + this.taskPromise = new Promise((resolve, reject) => { + this.resolve = resolve; + this.reject = reject; + }); + } + + public setResult(result?: ResultType): void { + this.resolve(result as ResultType); + } + + public setFailure(result?: Error): void { + this.reject(result as Error); + } + + public get promise(): Promise { + return this.taskPromise; + } +} \ No newline at end of file diff --git a/Sample.Avalonia/UiEditorLike/Component/index.html b/Sample.Avalonia/UiEditorLike/Component/index.html new file mode 100644 index 00000000..f03a2643 --- /dev/null +++ b/Sample.Avalonia/UiEditorLike/Component/index.html @@ -0,0 +1,8 @@ + + + + + +

Inner iframe content

+ + diff --git a/Sample.Avalonia/UiEditorLike/UIEditor.scss b/Sample.Avalonia/UiEditorLike/UIEditor.scss new file mode 100644 index 00000000..fa1077b9 --- /dev/null +++ b/Sample.Avalonia/UiEditorLike/UIEditor.scss @@ -0,0 +1,6 @@ + + +.ui-editor-wrapper { + height: 100px; + background: antiquewhite; +} \ No newline at end of file diff --git a/Sample.Avalonia/UiEditorLike/UIEditorLike.tsx b/Sample.Avalonia/UiEditorLike/UIEditorLike.tsx new file mode 100644 index 00000000..b693b451 --- /dev/null +++ b/Sample.Avalonia/UiEditorLike/UIEditorLike.tsx @@ -0,0 +1,84 @@ +import * as React from "react"; +import "./UIEditor.scss"; +import {createRef, useEffect, useRef} from "react"; +import {TaskUI} from "./Component/TaskUI"; + +type Action = () => void; + +export interface IUIEditorProperties { + +} + +const UIEditorLike = ({}: IUIEditorProperties): JSX.Element => { + const editorWrapper = createRef(); + const disposeHandlers = useRef([]); + + useEffect(() => { + if (!editorWrapper.current) return () => {}; + + let cancelled = false; + + const setup = async (localDoc: HTMLDocument, wrapper: HTMLElement, level: number) => { + let iframe: HTMLIFrameElement | null = null; + iframe = localDoc.createElement("iframe"); + iframe.src = "/Sample.Avalonia/sample.html"; + iframe.style.border = "0"; + iframe.style.height = "100%"; + iframe.style.width = "100%"; + + const iframeLoadTask = new TaskUI(); + const onLoad = () => iframeLoadTask.setResult(); + iframe.addEventListener("load", onLoad, { once: true }); + + wrapper!.appendChild(iframe); + + disposeHandlers.current.push(() => { + localDoc.head.innerHTML = ""; + + // 2️⃣ Break contentWindow references BEFORE touching contentDocument + const win = iframe.contentWindow; + if (win && !win.closed) { + try { win.location.replace("about:blank"); } catch {} + } + + iframe.removeEventListener("load", onLoad); + iframe.src = "about:blank"; // ensures unload event fires + const doc = iframe.contentDocument; + if (doc) { + doc.open(); + doc.write(""); + doc.close(); + } + iframe.remove(); // safer modern removal + + // 4️⃣ Close the window context if possible + iframe.contentWindow?.close?.(); + iframe.onload = null; + iframe = null; + }); + + await iframeLoadTask.promise; + if (cancelled || !iframe?.contentDocument) return; + + const doc = iframe.contentDocument; + if(level > 1) setup(doc, doc.body, level - 1); + const body = doc.body as HTMLBodyElement; + body.className = "root-with-iframe"; + }; + + setup(document, editorWrapper.current, 1); + + return () => { + cancelled = true; + disposeHandlers.current.forEach(h => h()); + disposeHandlers.current.length = 0; + console.log("unmounted and cleaned iframe"); + }; + }, []); + + return ( +
+ ); +} + +export default UIEditorLike; \ No newline at end of file diff --git a/Sample.Avalonia/UsersView/UsersView.scss b/Sample.Avalonia/UsersView/UsersView.scss new file mode 100644 index 00000000..734822fe --- /dev/null +++ b/Sample.Avalonia/UsersView/UsersView.scss @@ -0,0 +1,8 @@ +body { + background: none; + background: red; + width: 100px; + height: 100px; +} + + diff --git a/Sample.Avalonia/UsersView/UsersView.tsx b/Sample.Avalonia/UsersView/UsersView.tsx new file mode 100644 index 00000000..53f3239f --- /dev/null +++ b/Sample.Avalonia/UsersView/UsersView.tsx @@ -0,0 +1,25 @@ +import * as React from "react"; +import { IPluginsContext } from "PluginsProvider"; +import "./UsersView.scss"; + +export interface IUsersViewProperties { +} + +export interface IUsersViewBehaviors { + +} + +export default class UsersView extends React.Component implements IUsersViewBehaviors { + + constructor(props: IUsersViewProperties, context: IPluginsContext) { + super(props, context); + } + + public render(): JSX.Element { + return ( +
+ Hello World!! This is the users inner view! +
+ ); + } +} diff --git a/Sample.Avalonia/sample.html b/Sample.Avalonia/sample.html new file mode 100644 index 00000000..32c3d35f --- /dev/null +++ b/Sample.Avalonia/sample.html @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/ViewGeneratorCore/tools/package-lock.json b/ViewGeneratorCore/tools/package-lock.json index d4452c1e..1563976c 100644 --- a/ViewGeneratorCore/tools/package-lock.json +++ b/ViewGeneratorCore/tools/package-lock.json @@ -1,133 +1,170 @@ { + "name": "tools", + "lockfileVersion": 3, "requires": true, - "lockfileVersion": 1, - "dependencies": { - "@outsystems/ts2lang": { + "packages": { + "": { + "license": "ISC", + "dependencies": { + "@types/node": "^12.6.8" + }, + "devDependencies": { + "@outsystems/ts2lang": "1.0.21" + } + }, + "node_modules/@outsystems/ts2lang": { "version": "1.0.21", "resolved": "https://registry.npmjs.org/@outsystems/ts2lang/-/ts2lang-1.0.21.tgz", "integrity": "sha512-K4v/hOvImzm/28ZpLZmiK3CRzdWm0SMJEN3mMbgKbj2rDAzLuAfE53RNE5DLAGoZGMeCrLkq6yuT9wLrAHANog==", "dev": true, - "requires": { + "dependencies": { "@outsystems/ts2lang": "^1.0.12", "commander": "^2.9.0", "glob": "^7.1.2", "is-directory": "^0.3.1", "merge": "^1.2.0", "typescript": "3.4" + }, + "bin": { + "ts2lang": "ts2lang-main.js" } }, - "@types/node": { + "node_modules/@types/node": { "version": "12.20.4", "resolved": "https://registry.npmjs.org/@types/node/-/node-12.20.4.tgz", "integrity": "sha512-xRCgeE0Q4pT5UZ189TJ3SpYuX/QGl6QIAOAIeDSbAVAd2gX1NxSZup4jNVK7cxIeP8KDSbJgcckun495isP1jQ==" }, - "balanced-match": { + "node_modules/balanced-match": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", "dev": true }, - "brace-expansion": { + "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "dev": true, - "requires": { + "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, - "commander": { + "node_modules/commander": { "version": "2.20.3", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", "dev": true }, - "concat-map": { + "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", "dev": true }, - "fs.realpath": { + "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", "dev": true }, - "glob": { + "node_modules/glob": { "version": "7.1.6", "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "deprecated": "Glob versions prior to v9 are no longer supported", "dev": true, - "requires": { + "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.0.4", "once": "^1.3.0", "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "inflight": { + "node_modules/inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", "dev": true, - "requires": { + "dependencies": { "once": "^1.3.0", "wrappy": "1" } }, - "inherits": { + "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "dev": true }, - "is-directory": { + "node_modules/is-directory": { "version": "0.3.1", "resolved": "https://registry.npmjs.org/is-directory/-/is-directory-0.3.1.tgz", "integrity": "sha1-YTObbyR1/Hcv2cnYP1yFddwVSuE=", - "dev": true + "dev": true, + "engines": { + "node": ">=0.10.0" + } }, - "merge": { + "node_modules/merge": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/merge/-/merge-1.2.1.tgz", "integrity": "sha512-VjFo4P5Whtj4vsLzsYBu5ayHhoHJ0UqNm7ibvShmbmoz7tGi0vXaoJbGdB+GmDMLUdg8DpQXEIeVDAe8MaABvQ==", "dev": true }, - "minimatch": { + "node_modules/minimatch": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", "dev": true, - "requires": { + "dependencies": { "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" } }, - "once": { + "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", "dev": true, - "requires": { + "dependencies": { "wrappy": "1" } }, - "path-is-absolute": { + "node_modules/path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", - "dev": true + "dev": true, + "engines": { + "node": ">=0.10.0" + } }, - "typescript": { + "node_modules/typescript": { "version": "3.4.5", "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.4.5.tgz", "integrity": "sha512-YycBxUb49UUhdNMU5aJ7z5Ej2XGmaIBL0x34vZ82fn3hGvD+bgrMrVDpatgz2f7YxUMJxMkbWxJZeAvDxVe7Vw==", - "dev": true + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } }, - "wrappy": { + "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",