diff --git a/docs/manual/01 Getting Started.md b/docs/manual/01 Getting Started.md index a16c49e36..00bdec139 100644 --- a/docs/manual/01 Getting Started.md +++ b/docs/manual/01 Getting Started.md @@ -36,8 +36,7 @@ This is why UIX ships with integrated features such as: To install uix, you need to install [Deno](https://docs.deno.com/runtime/manual/getting_started/installation) first. > [!WARNING] -> UIX is currently only supported for Deno versions <= 1.39.4. We are actively working on a -> release that is compatible with newer Deno versions. +> UIX is only supported for Deno versions > 1.40.0 #### Linux / MacOS diff --git a/run.ts b/run.ts index 5c956d904..3d426d531 100644 --- a/run.ts +++ b/run.ts @@ -4,7 +4,6 @@ import { Datex, datex } from "datex-core-legacy/no_init.ts"; // required by getAppConfig import type { Datex as _Datex } from "datex-core-legacy"; // required by getAppConfig - import { getAppOptions } from "./src/app/config-files.ts"; import { getExistingFile } from "./src/utils/file-utils.ts"; import { clear, command_line_options, enableTLS, login, init, rootPath, stage, watch, watch_backend, live } from "./src/app/args.ts"; @@ -22,10 +21,13 @@ import { getDXConfigData } from "./src/app/dx-config-parser.ts"; import { Path } from "./src/utils/path.ts"; import { handleAutoUpdate, updateCache } from "./auto-update.ts"; +import "./src/base/uix-datex-module.ts" + import { enableErrorReporting } from "datex-core-legacy/utils/error-reporting.ts"; import { getErrorReportingPreference, saveErrorReportingPreference, shouldAskForErrorReportingPreference } from "./src/utils/error-reporting-preference.ts"; import { isCIRunner } from "./src/utils/check-ci.ts"; import { logger, runParams } from "./src/runners/runner.ts"; +import { applyPlugins } from "./src/app/config-files.ts"; // login flow if (login) await triggerLogin(); @@ -144,30 +146,16 @@ async function loadPlugins() { return plugins; } -/** - * Mock #public.uix - */ -async function mockUIX() { - await datex` - #public.uix = { - stage: function (options) ( - options.${stage} default @@local - ) - } - ` -} -await mockUIX(); // find importmap (from app.dx or deno.json) to start the actual deno process with valid imports const plugins = await loadPlugins(); const runners = [new LocalDockerRunner()]; -const [options, new_base_url] = await normalizeAppOptions(await getAppOptions(rootPath, plugins), rootPath); +const [options, new_base_url] = await normalizeAppOptions(await getAppOptions(rootPath), rootPath); if (!options.import_map) throw new Error("Could not find importmap"); - options.import_map = await createProxyImports(options, new_base_url, params.deno_config_path!); -// make sure UIX mock is not overridden -await mockUIX(); +await applyPlugins(plugins, rootPath, options) + await runBackends(options); diff --git a/src/app/app-plugin.ts b/src/app/app-plugin.ts index 677a0d832..b53150c38 100644 --- a/src/app/app-plugin.ts +++ b/src/app/app-plugin.ts @@ -1,4 +1,7 @@ +import { Path } from "datex-core-legacy/utils/path.ts"; +import type { normalizedAppOptions } from "./options.ts"; + export interface AppPlugin { name: string - apply(data:Data): Promise|void + apply(data:Data, rootPath:Path.File, appOptions:normalizedAppOptions): Promise|void } diff --git a/src/app/config-files.ts b/src/app/config-files.ts index e750c704b..e3ac124bf 100644 --- a/src/app/config-files.ts +++ b/src/app/config-files.ts @@ -1,10 +1,11 @@ // no explicit imports, should also work without import maps... import {getExistingFile} from "../utils/file-utils.ts"; import { command_line_options } from "../app/args.ts"; -import { Path } from "../utils/path.ts"; +import { Path } from "datex-core-legacy/utils/path.ts"; import { Datex } from "datex-core-legacy/mod.ts"; import type { AppPlugin } from "./app-plugin.ts"; import { logger } from "../utils/global-values.ts"; +import { normalizedAppOptions } from "./options.ts"; const default_importmap = "https://dev.cdn.unyt.org/importmap.json"; const arg_import_map_string = command_line_options.option("import-map", {type:"string", description: "Import map path"}); @@ -15,10 +16,27 @@ const arg_import_map = (arg_import_map_string?.startsWith("http://")||arg_import undefined ) +export async function applyPlugins(plugins: AppPlugin[], rootPath:Path, appOptions: normalizedAppOptions) { + const config_path = getExistingFile(rootPath, './app.dx', './app.json'); + + if (!config_path) throw "Could not find an app.dx or app.json config file in " + new Path(rootPath).normal_pathname + + // handle plugins (only if in dev environment, not on host, TODO: better solution) + if (plugins?.length && !Deno.env.get("UIX_HOST_ENDPOINT")) { + const pluginData = await datex.get>(config_path, undefined, undefined, plugins.map(p=>p.name)); + for (const plugin of plugins) { + if (pluginData[plugin.name]) { + logger.debug(`using plugin "${plugin.name}"`); + await plugin.apply(pluginData[plugin.name], rootPath, appOptions) + } + } + } +} + /** * get combined config of app.dx and deno.json and command line args */ -export async function getAppOptions(root_path:URL, plugins?: AppPlugin[]) { +export async function getAppOptions(root_path:URL) { const config_path = getExistingFile(root_path, './app.dx', './app.json'); let config:Record = {} @@ -29,18 +47,6 @@ export async function getAppOptions(root_path:URL, plugins?: AppPlugin[]) { throw "Invalid config file" } config = Object.fromEntries(Datex.DatexObject.entries(>raw_config)); - - // handle plugins (only if in dev environment, not on host, TODO: better solution) - if (plugins?.length && !Deno.env.has("UIX_HOST_ENDPOINT")) { - const pluginData = await datex.get>(config_path, undefined, undefined, plugins.map(p=>p.name)); - for (const plugin of plugins) { - if (pluginData[plugin.name]) { - logger.debug(`using plugin "${plugin.name}"`); - await plugin.apply(pluginData[plugin.name]) - } - } - } - } else throw "Could not find an app.dx or app.json config file in " + new Path(root_path).normal_pathname @@ -70,7 +76,6 @@ export async function getAppOptions(root_path:URL, plugins?: AppPlugin[]) { } catch {} } - if (!config.import_map && !config.import_map_path) config.import_map_path = default_importmap; // if (config.import_map) throw "embeded import maps are not yet supported for uix apps"; diff --git a/src/app/frontend-manager.ts b/src/app/frontend-manager.ts index 6b015f953..2598951af 100644 --- a/src/app/frontend-manager.ts +++ b/src/app/frontend-manager.ts @@ -4,7 +4,7 @@ import { TypescriptImportResolver } from "../server/ts-import-resolver.ts"; import { $$, Datex } from "datex-core-legacy"; import { Server, requestMetadata } from "../server/server.ts"; import { ALLOWED_ENTRYPOINT_FILE_NAMES, app } from "./app.ts"; -import { Path } from "../utils/path.ts"; +import { Path } from "datex-core-legacy/utils/path.ts"; import { BackendManager } from "./backend-manager.ts"; import { getExistingFile, getExistingFileExclusive } from "../utils/file-utils.ts"; import { logger } from "../utils/global-values.ts"; @@ -87,11 +87,12 @@ export class FrontendManager extends HTMLProvider { initFrontendDir(){ this.transpiler = new Transpiler(this.scope, { - sourceMap: app.stage == "dev", + sourceMaps: this.app_options.source_maps ?? app.stage == "dev", watch: this.#watch, minifyJS: this.app_options.minify_js, import_resolver: this.import_resolver, - on_file_update: this.#watch ? ()=>this.handleFrontendReload() : undefined + on_file_update: this.#watch ? ()=>this.handleFrontendReload() : undefined, + basePath: this.#base_path }); } @@ -104,7 +105,7 @@ export class FrontendManager extends HTMLProvider { intCommonDirs() { for (const common_dir of this.app_options.common) { const transpiler = new Transpiler(new Path(common_dir), { - sourceMap: app.stage == "dev", + sourceMaps: this.app_options.source_maps ?? app.stage == "dev", dist_parent_dir: this.transpiler.tmp_dir, watch: this.#watch, minifyJS: this.app_options.minify_js, @@ -118,7 +119,8 @@ export class FrontendManager extends HTMLProvider { this.#backend?.handleUpdate("common"); } this.handleFrontendReload(); - } : undefined + } : undefined, + basePath: this.#base_path }) this.#common_transpilers.set(common_dir.toString(), [transpiler, this.srcPrefix + new Path(common_dir).name + '/']) @@ -190,7 +192,7 @@ export class FrontendManager extends HTMLProvider { #backend?: BackendManager - #base_path!:Path + #base_path!:Path.File #web_path!: Path #entrypoint?: Path @@ -827,7 +829,13 @@ if (!window.location.origin.endsWith(".unyt.app")) { if (content instanceof Blob || content instanceof Response) return [content, RenderMethod.RAW_CONTENT, status_code, openGraphData, headers]; // Markdown - if (content instanceof Datex.Markdown) return [getOuterHTML( content.getHTML(false), {includeShadowRoots:true, injectStandaloneJS:render_method!=RenderMethod.STATIC&&render_method!=RenderMethod.HYBRID, injectStandaloneComponents:render_method!=RenderMethod.STATIC&&render_method!=RenderMethod.HYBRID/*TODO: should also work with HYBRID, but cannot create standalone component class and new class later*/, allowIgnoreDatexFunctions:(render_method==RenderMethod.HYBRID||render_method==RenderMethod.PREVIEW), lang}), render_method, status_code, openGraphData, headers]; + if (content instanceof Datex.Markdown) return [getOuterHTML( content.getHTML(false), { + includeShadowRoots:true, + injectStandaloneJS:render_method!=RenderMethod.STATIC, + injectStandaloneComponents:render_method!=RenderMethod.STATIC, + allowIgnoreDatexFunctions:(render_method==RenderMethod.HYBRID||render_method==RenderMethod.PREVIEW), + lang + }), render_method, status_code, openGraphData, headers]; // convert content to valid HTML string if (content instanceof Element || content instanceof DocumentFragment) { @@ -836,8 +844,8 @@ if (!window.location.origin.endsWith(".unyt.app")) { content as Element, { includeShadowRoots:true, - injectStandaloneJS:render_method!=RenderMethod.STATIC&&render_method!=RenderMethod.HYBRID, - injectStandaloneComponents:render_method!=RenderMethod.STATIC&&render_method!=RenderMethod.HYBRID, + injectStandaloneJS:render_method!=RenderMethod.STATIC, + injectStandaloneComponents:render_method!=RenderMethod.STATIC, allowIgnoreDatexFunctions:(render_method==RenderMethod.HYBRID||render_method==RenderMethod.PREVIEW), lang, requiredPointers diff --git a/src/app/options.ts b/src/app/options.ts index 3cec12cb3..38679a377 100644 --- a/src/app/options.ts +++ b/src/app/options.ts @@ -1,6 +1,6 @@ import type { Tuple } from "datex-core-legacy/types/tuple.ts"; import { ImportMap } from "../utils/importmap.ts"; -import { Path } from "../utils/path.ts"; +import { Path } from "datex-core-legacy/utils/path.ts"; declare const Datex: any; // cannot import Datex here, circular dependency problems @@ -28,6 +28,7 @@ export type appOptions = { debug_mode?: boolean // enable debug interfaces available on /@debug/... minify_js?: boolean // minify transpiled javascript modules, default: true preload_dependencies?: boolean // automatically preload all ts module dependencies, default: true + source_maps?: boolean // generate source maps for transpiled javascript modules, default: false, true for dev stage } export interface normalizedAppOptions extends appOptions { @@ -43,14 +44,14 @@ export interface normalizedAppOptions extends appOptions { experimentalFeatures: string[] } -export async function normalizeAppOptions(options:appOptions = {}, base_url?:string|URL): Promise<[normalizedAppOptions, URL]> { +export async function normalizeAppOptions(options:appOptions = {}, baseURL?:string|URL): Promise<[normalizedAppOptions, Path.File]> { const n_options = {}; // determine base url - if (typeof base_url == "string" && !base_url.startsWith("file://")) base_url = 'file://' + base_url; - base_url ??= new Error().stack?.trim()?.match(/((?:https?|file)\:\/\/.*?)(?::\d+)*(?:$|\nevaluate@)/)?.[1]; - if (!base_url) throw new Error("Could not determine the app base url (this should not happen)"); - base_url = new URL(base_url.toString()); + if (typeof baseURL == "string" && !baseURL.startsWith("file://")) baseURL = 'file://' + baseURL; + baseURL ??= new Error().stack?.trim()?.match(/((?:https?|file)\:\/\/.*?)(?::\d+)*(?:$|\nevaluate@)/)?.[1]; + if (!baseURL) throw new Error("Could not determine the app base url (this should not happen)"); + const basePath = Path.File(baseURL.toString()); n_options.name = options.name; n_options.description = options.description; @@ -67,6 +68,7 @@ export async function normalizeAppOptions(options:appOptions = {}, base_url?:str n_options.debug_mode = options.debug_mode ?? false; n_options.minify_js = options.minify_js ?? true; n_options.preload_dependencies = options.preload_dependencies ?? true; + n_options.source_maps = options.source_maps; // import map or import map path if (options.import_map) n_options.import_map = new ImportMap(options.import_map); @@ -76,9 +78,9 @@ export async function normalizeAppOptions(options:appOptions = {}, base_url?:str else throw new Error("No importmap found or set in the app configuration") // should not happen // default frontend, backend, common - if (options.frontend==undefined && new Path('./frontend/', base_url).fs_exists) options.frontend = [new Path('./frontend/', base_url)] - if (options.backend==undefined && new Path('./backend/', base_url).fs_exists) options.backend = [new Path('./backend/', base_url)] - if (options.common==undefined && new Path('./common/', base_url).fs_exists) options.common = [new Path('./common/', base_url)] + if (options.frontend==undefined && new Path('./frontend/', basePath).fs_exists) options.frontend = [new Path('./frontend/', basePath)] + if (options.backend==undefined && new Path('./backend/', basePath).fs_exists) options.backend = [new Path('./backend/', basePath)] + if (options.common==undefined && new Path('./common/', basePath).fs_exists) options.common = [new Path('./common/', basePath)] // convert to arrays const frontends = options.frontend instanceof Array ? options.frontend : options.frontend instanceof Datex.Tuple ? (options.frontend as unknown as Tuple).toArray() : [options.frontend] @@ -86,14 +88,14 @@ export async function normalizeAppOptions(options:appOptions = {}, base_url?:str const commons = options.common instanceof Array ? options.common : options.common instanceof Datex.Tuple ? (options.common as unknown as Tuple).toArray() : [options.common] // convert to absolute paths - n_options.frontend = frontends.filter(p=>!!p).map(p=>new Path(p,base_url).asDir().fsCreateIfNotExists()); - n_options.backend = backends.filter(p=>!!p).map(p=>new Path(p,base_url).asDir().fsCreateIfNotExists()); - n_options.common = commons.filter(p=>!!p).map(p=>new Path(p,base_url).asDir().fsCreateIfNotExists()); + n_options.frontend = frontends.filter(p=>!!p).map(p=>new Path(p,basePath).asDir().fsCreateIfNotExists()); + n_options.backend = backends.filter(p=>!!p).map(p=>new Path(p,basePath).asDir().fsCreateIfNotExists()); + n_options.common = commons.filter(p=>!!p).map(p=>new Path(p,basePath).asDir().fsCreateIfNotExists()); // pages dir or default pages dir - if (options.pages) n_options.pages = new Path(options.pages,base_url).asDir() + if (options.pages) n_options.pages = new Path(options.pages,basePath).asDir() else { - const defaultPagesDir = new Path('./pages/', base_url); + const defaultPagesDir = new Path('./pages/', basePath); if (defaultPagesDir.fs_exists) n_options.pages = defaultPagesDir; } @@ -104,7 +106,7 @@ export async function normalizeAppOptions(options:appOptions = {}, base_url?:str if (!n_options.frontend.length) { // try to find the frontend dir - const frontend_dir = new Path("./frontend/",base_url); + const frontend_dir = new Path("./frontend/",basePath); try { if (!Deno.statSync(frontend_dir).isFile) n_options.frontend.push(frontend_dir) } @@ -113,7 +115,7 @@ export async function normalizeAppOptions(options:appOptions = {}, base_url?:str if (!n_options.backend.length) { // try to find the backend dir - const backend_dir = new Path("./backend/",base_url); + const backend_dir = new Path("./backend/",basePath); try { if (!Deno.statSync(backend_dir).isFile) n_options.backend.push(backend_dir) } @@ -122,12 +124,29 @@ export async function normalizeAppOptions(options:appOptions = {}, base_url?:str if (!n_options.common.length) { // try to find the common dir - const common_dir = new Path("./common/",base_url); + const common_dir = new Path("./common/",basePath); try { if (!Deno.statSync(common_dir).isFile) n_options.common.push(common_dir) } catch {} } - return [n_options, base_url] + return [n_options, basePath] +} + + +export function getInferredRunPaths(importMap: ImportMap, rootPath: Path.File): {importMapPath: string|null, uixRunPath: string|null} { + const importMapPath = importMap.originalPath ? importMap.originalPath.getAsRelativeFrom(rootPath) : null; + const inferredAbsoluteRunPath = importMap.imports["uix"]?.replace(/\/uix\.ts$/, '/run.ts') ?? null as string|null; + const uixRunPath = inferredAbsoluteRunPath ? + ( + Path.pathIsURL(inferredAbsoluteRunPath) ? + inferredAbsoluteRunPath : + new Path(inferredAbsoluteRunPath, importMap.path).getAsRelativeFrom(rootPath)) : + null; + + return { + importMapPath, + uixRunPath + } } \ No newline at end of file diff --git a/src/app/start-app.ts b/src/app/start-app.ts index bbcb6ac60..f61573c58 100644 --- a/src/app/start-app.ts +++ b/src/app/start-app.ts @@ -11,6 +11,8 @@ import { getDirType } from "./utils.ts"; import { WebSocketServerInterface } from "datex-core-legacy/network/communication-interfaces/websocket-server-interface.ts" import { HTTPServerInterface } from "datex-core-legacy/network/communication-interfaces/http-server-interface.ts" import { communicationHub } from "datex-core-legacy/network/communication-hub.ts"; +import { resolveDependencies } from "../html/dependency-resolver.ts"; +import { resolve } from "https://deno.land/std@0.172.0/path/win32.ts"; const logger = new Datex.Logger("UIX App"); @@ -67,7 +69,9 @@ export async function startApp(app: {domains:string[], hostDomains: string[], op // also override endpoint default if (backend_with_default_export) { Datex.Runtime.endpoint_entrypoint = backend_with_default_export.entrypointProxy; - backend_with_default_export.content_provider[Datex.DX_SOURCE] = Datex.Runtime.endpoint.toString(); // use @@local::#entrypoint as dx source + const content_provider = backend_with_default_export.content_provider; + if ((content_provider && typeof content_provider == "object") || typeof content_provider == "function") + (content_provider as any)[Datex.DX_SOURCE] = Datex.Runtime.endpoint.toString(); // use @@local::#entrypoint as dx source } let server:Server|undefined @@ -166,6 +170,9 @@ export async function startApp(app: {domains:string[], hostDomains: string[], op const {HTTP} = await import("./http-over-datex.ts") HTTP.setServer(server); } + + // preload dependencies + resolveDependencies(import.meta.resolve("datex-core-legacy")) return { defaultServer: server, diff --git a/src/base/decorators.ts b/src/base/decorators.ts index 4882d03b4..f33bda886 100644 --- a/src/base/decorators.ts +++ b/src/base/decorators.ts @@ -1,53 +1,51 @@ -import { Datex } from "datex-core-legacy"; -import { context_kind, context_meta_getter, context_meta_setter, context_name, handleDecoratorArgs, METADATA } from "datex-core-legacy/datex_all.ts"; -import { logger } from "../utils/global-values.ts"; +import { Datex, handleClassDecoratorWithOptionalArgs } from "datex-core-legacy/mod.ts"; +import { handleClassDecoratorWithArgs, handleClassFieldDecoratorWithOptionalArgs } from "datex-core-legacy/js_adapter/decorators.ts"; import { getCloneKeys, Component } from "../components/Component.ts"; import { getCallerFile } from "datex-core-legacy/utils/caller_metadata.ts"; import { domContext, domUtils } from "../app/dom-context.ts"; import { getTransformWrapper } from "../uix-dom/datex-bindings/transform-wrapper.ts"; import { client_type } from "datex-core-legacy/utils/constants.ts"; - - +import { Class, Decorators } from "datex-core-legacy/datex_all.ts"; /** * @defaultOptions to define component default options */ -export function defaultOptions (default_options:Partial>):any -export function defaultOptions():any -export function defaultOptions(target: Function & { prototype: C }):any -export function defaultOptions(...args:any[]):any { +export function defaultOptions>(defaultOptions:Partial>): (value: typeof Component, context: ClassDecoratorContext)=>void { const url = getCallerFile(); // TODO: called even if _init_module set - return handleDecoratorArgs(args, (...args)=>_defaultOptions(url, ...args)); + return handleClassDecoratorWithArgs([defaultOptions], ([defaultOptions], value, context) => { + console.log("@defaultOptions",defaultOptions, value,context) + return initDefaultOptions(url, value as unknown as ComponentClass, defaultOptions) + }) } +type ComponentClass = + typeof Component & + (new (...args: unknown[]) => unknown) & // fix abstract class + {_init_module: string, _module: string, _use_resources: boolean} // access protected properties + const transformWrapper = getTransformWrapper(domUtils, domContext) -function _defaultOptions(url:string, component_class:typeof HTMLElement, name:context_name, kind:context_kind, is_static:boolean, is_private:boolean, setMetadata:context_meta_setter, getMetadata:context_meta_getter, params:[any] = []) { - url = Object.hasOwn(component_class, "_init_module") ? - component_class._init_module : +export function initDefaultOptions>(url:string, componentClass: ComponentClass, defaultOptions?:Partial>) { + url = Object.hasOwn(componentClass, "_init_module") ? + componentClass._init_module : url; if (!url) { console.log(new Error().stack) - throw new Error("Could not get the location of the UIX component '"+component_class.name+"'. This should not happen"); + throw new Error("Could not get the location of the UIX component '"+componentClass.name+"'. This should not happen"); } - // deprecated message - // if (component_class.prototype instanceof Components.Base) { - // logger.warn("UIX.Components.Base is deprecated - please use Component") - // } - - if (component_class.prototype instanceof Component) { + if (componentClass.prototype instanceof Component) { // set auto module (url from stack trace), not if module === null => resources was disabled with @NoResources - if (url && component_class._module !== null) component_class._module = url; + if (url && componentClass._module !== null) componentClass._module = url; // default value of _use_resources is true (independent of parent class), if it was not overriden for this Component with @NoResources - if (!Object.hasOwn(component_class, '_use_resources')) component_class._use_resources = true; + if (!Object.hasOwn(componentClass, '_use_resources')) componentClass._use_resources = true; // preload css files - component_class.preloadStylesheets?.(); + componentClass.preloadStylesheets?.(); - const name = String(component_class.name).replace(/1$/,'').split(/([A-Z][a-z]+)/).filter(t=>!!t).map(t=>t.toLowerCase()).join("-"); // convert from CamelCase to snake-case + const name = String(componentClass.name).replace(/1$/,'').split(/([A-Z][a-z]+)/).filter(t=>!!t).map(t=>t.toLowerCase()).join("-"); // convert from CamelCase to snake-case const datex_type = Datex.Type.get("std", "uix", name); const options_datex_type = Datex.Type.get("uixopt", name); @@ -58,9 +56,11 @@ function _defaultOptions(url:string, component_class:typeof HTMLElement, name:co } // create template class for component - const new_class = Datex.createTemplateClass(component_class, datex_type, true); + const new_class = Datex.createTemplateClass(componentClass, datex_type, true) as ComponentClass; + + // Object.defineProperty(componentClass, Datex.DX_TYPE, {set:(v)=>{console.log("set",v,new Error().stack)}, get:()=>datex_type}); - component_class[Datex.DX_TYPE] = datex_type; + componentClass[Datex.DX_TYPE] = datex_type; const html_interface = Datex.Type.get('html').interface_config!; datex_type.interface_config.cast_no_tuple = html_interface.cast_no_tuple; // handle casts from object @@ -70,13 +70,14 @@ function _defaultOptions(url:string, component_class:typeof HTMLElement, name:co datex_type.interface_config.serialize = (value) => { // serialize html part (style, attr, content) - const html_serialized = > html_interface.serialize!(value); + const html_serialized = > html_interface.serialize!(value); // add additional properties (same as in Datex.Runtime.serializeValue) const pointer = Datex.Pointer.getByValue(value) for (const key of datex_type.visible_children){ if (!html_serialized.p) html_serialized.p = {}; - html_serialized.p[key] = pointer?.shadow_object ? pointer.shadow_object[key]/*keep references*/ : value[key]; + + html_serialized.p[key] = pointer?.shadow_object ? pointer.shadow_object[key]/*keep references*/ : value[key]; } return html_serialized; @@ -85,22 +86,15 @@ function _defaultOptions(url:string, component_class:typeof HTMLElement, name:co // component default options new_class.DEFAULT_OPTIONS = Object.create(Object.getPrototypeOf(new_class).DEFAULT_OPTIONS ?? {}); - if (!params[0]) params[0] = {}; + if (!defaultOptions) defaultOptions = {}; // set default options + title // ! title is overriden, even if a parent class has specified another default title // if (!(params[0]).title)(params[0]).title = component_class.name; - Object.assign(new_class.DEFAULT_OPTIONS, params[0]) + Object.assign(new_class.DEFAULT_OPTIONS, defaultOptions) // find non-primitive values in default options (must be copied) new_class.CLONE_OPTION_KEYS = getCloneKeys(new_class.DEFAULT_OPTIONS); - // initial constraints - if (Object.getPrototypeOf(new_class).INITIAL_CONSTRAINTS) new_class.INITIAL_CONSTRAINTS = {...Object.getPrototypeOf(new_class).INITIAL_CONSTRAINTS}; - if (params[1]) { - if (!new_class.INITIAL_CONSTRAINTS) new_class.INITIAL_CONSTRAINTS = {} - Object.assign(new_class.INITIAL_CONSTRAINTS, params[1]); - } - // create DATEX type for options (with prototype) options_datex_type.setJSInterface({ prototype: new_class.DEFAULT_OPTIONS, @@ -109,30 +103,24 @@ function _defaultOptions(url:string, component_class:typeof HTMLElement, name:co is_normal_object: true, }) // define custom DOM element after everything is initialized - domContext.customElements.define("uix-" + name, component_class) - return new_class //element_class + domContext.customElements.define("uix-" + name, componentClass as typeof HTMLElement) + return new_class } - else throw new Error("Invalid @defaultOptions - class must extend or Component") + else throw new Error("Invalid @defaultOptions - class must extend Component") } /** * @NoResources: disable external resource loading (.css, .dx), must be located below the @defaultOptions decorator */ -export function NoResources(target: Function & { prototype: C }):any -export function NoResources(...args:any[]):any { - return handleDecoratorArgs(args, _NoResources); -} - -function _NoResources(component_class:typeof HTMLElement, name:context_name, kind:context_kind, is_static:boolean, is_private:boolean, setMetadata:context_meta_setter, getMetadata:context_meta_getter) { +export function NoResources(value: typeof Component, context: ClassDecoratorContext) { // was called after @Component - if (Object.hasOwn(component_class, '_module')) { - throw new Error("Please put the @NoResources decorator for the component '"+name+"' below the @defaultOptions decorator"); + if (Object.hasOwn(value, '_module')) { + throw new Error("Please put the @NoResources decorator for the component '"+context.name+"' below the @defaultOptions decorator"); } - component_class._use_resources = false; + (value as ComponentClass)._use_resources = false; } - export const ID_PROPS: unique symbol = Symbol("ID_PROPS"); export const CONTENT_PROPS: unique symbol = Symbol("CONTENT_PROPS"); export const CHILD_PROPS: unique symbol = Symbol("CHILD_PROPS"); @@ -141,125 +129,106 @@ export const IMPORT_PROPS: unique symbol = Symbol("IMPORT_PROPS"); export const STANDALONE_PROPS: unique symbol = Symbol("STANDALONE_PROPS"); export const ORIGIN_PROPS: unique symbol = Symbol("ORIGIN_PROPS"); -/** @id to automatically assign a element id to a component property */ -export function id(id?:string):any -export function id(target: any, name?: string, method?:any):any -export function id(...args:any[]) { - return handleDecoratorArgs(args, _id); +/** \@id to automatically assign a element id to a component property */ +export function id(id?:string): (value: undefined, context: ClassFieldDecoratorContext) => void +export function id(value: undefined, context: ClassFieldDecoratorContext): void +export function id(id:string|undefined, context?: ClassFieldDecoratorContext) { + return handleClassFieldDecoratorWithOptionalArgs([id], context, ([id], context) => { + Decorators.setMetadata(context, ID_PROPS, id ?? context.name); + }) } -function _id(element_class:HTMLElement, name:context_name, kind:context_kind, is_static:boolean, is_private:boolean, setMetadata:context_meta_setter, getMetadata:context_meta_getter, params:[string?] = []) { - if (kind != "field") { - logger.error("@UIX.id has to be used on a field"); - return; - } - - setMetadata(ID_PROPS, params[0]??name); -} -/** @content to automatically assign a element id to a component property and add element to component content (#content) */ -export function content(id?:string):any -export function content(target: any, name?: string, method?:any):any -export function content(...args:any[]) { - return handleDecoratorArgs(args, _content); +/** \@content to automatically assign a element id to a component property and add element to component content (#content) */ +export function content(id?:string): (value: undefined, context: ClassFieldDecoratorContext) => void +export function content(value: undefined, context: ClassFieldDecoratorContext): void +export function content(id:string|undefined, context?: ClassFieldDecoratorContext) { + return handleClassFieldDecoratorWithOptionalArgs([id], context, ([id], context) => { + Decorators.setMetadata(context, CONTENT_PROPS, id ?? context.name); + }) } -function _content(element_class:typeof HTMLElement, name:context_name, kind:context_kind, is_static:boolean, is_private:boolean, setMetadata:context_meta_setter, getMetadata:context_meta_getter, params:[string?] = []) { - if (kind != "field") { - logger.error("@UIX.content has to be used on a field"); - return; - } - - setMetadata(CONTENT_PROPS, params[0]??name); -} /** @layout to automatically assign a element id to a component property and add element to component content container layout (#layout) */ -export function layout(id?:string):any -export function layout(target: any, name?: string, method?:any):any -export function layout(...args:any[]) { - return handleDecoratorArgs(args, _layout); -} - -function _layout(element_class:typeof HTMLElement, name:context_name, kind:context_kind, is_static:boolean, is_private:boolean, setMetadata:context_meta_setter, getMetadata:context_meta_getter, params:[string?] = []) { - if (kind != "field") { - logger.error("@UIX.layout has to be used on a field"); - return; - } - - setMetadata(LAYOUT_PROPS, params[0]??name); -} - -/** @child to automatically assign a element id to a component property and add element as a component child */ -export function child(id?:string):any -export function child(target: any, name?: string, method?:any):any -export function child(...args:any[]) { - return handleDecoratorArgs(args, _child); -} - -function _child(element_class:typeof HTMLElement, name:context_name, kind:context_kind, is_static:boolean, is_private:boolean, setMetadata:context_meta_setter, getMetadata:context_meta_getter, params:[string?] = []) { - if (kind != "field") { - logger.error("@UIX.child has to be used on a field"); - return; - } - - setMetadata(CHILD_PROPS, params[0]??name); +export function layout(id?:string): (value: undefined, context: ClassFieldDecoratorContext) => void +export function layout(value: undefined, context: ClassFieldDecoratorContext): void +export function layout(id:string|undefined, context?: ClassFieldDecoratorContext) { + return handleClassFieldDecoratorWithOptionalArgs([id], context, ([id], context) => { + Decorators.setMetadata(context, LAYOUT_PROPS, id ?? context.name); + }) +} + + +/** \@child to automatically assign a element id to a component property and add element as a component child */ +export function child(id?:string): (value: undefined, context: ClassFieldDecoratorContext) => void +export function child(value: undefined, context: ClassFieldDecoratorContext): void +export function child(id:string|undefined, context?: ClassFieldDecoratorContext) { + return handleClassFieldDecoratorWithOptionalArgs([id], context, ([id], context) => { + Decorators.setMetadata(context, CHILD_PROPS, id ?? context.name); + }) +} + +/** \@include to bind static properties */ +export function include(resource?:string, export_name?:string): (value: undefined, context: ClassFieldDecoratorContext) => void +export function include(value: undefined, context: ClassFieldDecoratorContext): void +export function include(value: undefined|string, context?: ClassFieldDecoratorContext|string) { + return handleClassFieldDecoratorWithOptionalArgs([value, context as string], context as ClassFieldDecoratorContext, + ([resource, export_name], context) => { + Decorators.setMetadata(context, IMPORT_PROPS, [resource, export_name??context.name]); + } + ) } - -/** @UIX.use to bind static properties */ -export function include(resource?:string, export_name?:string):any -export function include(target: any, name?: string, method?:any):any -export function include(...args:any[]) { - return handleDecoratorArgs(args, _include); +type frontendClassDecoratorOptions = { + inheritedFields?: string[] } -/** - * @depreacted use @include +/** + * \@frontend + * Exposes all methods of the class to the frontend context. */ -export const use = include; - -function _include(element_class:typeof HTMLElement, name:context_name, kind:context_kind, is_static:boolean, is_private:boolean, setMetadata:context_meta_setter, getMetadata:context_meta_getter, params:[string?, string?] = []) { - - if (kind != "field" && kind != "method") { - logger.error("@include has to be used on a field or method"); - return; +export function frontend(value: Class, context: ClassDecoratorContext): void +/** + * \@frontend + * Exposes all methods of the class to the frontend context. + * Optionally inherited properties and methods that should be exposed to the frontend can be specified. + */ +export function frontend(options: frontendClassDecoratorOptions): (value: Class, context: ClassDecoratorContext) => void +/** + * \@frontend + * Exposes the property or method to the frontend context. + */ +export function frontend(_value: undefined|((...args:any[])=>any), context: ClassFieldDecoratorContext|ClassMethodDecoratorContext): void +export function frontend(_value: undefined|Class|((...args:any[])=>any)|frontendClassDecoratorOptions, context?: ClassDecoratorContext|ClassFieldDecoratorContext|ClassMethodDecoratorContext): any { + // class decorator + if (!context || context.kind == "class") { + return handleClassDecoratorWithOptionalArgs( + [_value as frontendClassDecoratorOptions], + _value as Class, + context as ClassDecoratorContext, + ([options], value, context) => { + for (const prop of [...Object.getOwnPropertyNames(value.prototype), ...options.inheritedFields??[]]) { + if (prop == "constructor") continue; + Decorators.setMetadata({...(context as unknown as ClassFieldDecoratorContext), kind:"field",name:prop}, STANDALONE_PROPS, prop); + } + } + ) } - - setMetadata(IMPORT_PROPS, [params[0], params[1]??name]); -} - -/** \@frontend decorator to declare methods that always run on the frontend */ -export function frontend(target: any, name?: string, method?:any):any -export function frontend(...args:any[]) { - return handleDecoratorArgs(args, _frontend); -} - - - -function _frontend(element_class:typeof HTMLElement, name:context_name, kind:context_kind, is_static:boolean, is_private:boolean, setMetadata:context_meta_setter, getMetadata:context_meta_getter) { - if (is_static) { - logger.error("@frontend cannot be used on static class fields"); - return; + // field/method decorator + else { + Decorators.setMetadata(context!, STANDALONE_PROPS, context!.name); } - - setMetadata(STANDALONE_PROPS, name); } /** @bindOrigin to declare methods that work in a standlone context, but are executed in the original context */ -export function bindOrigin(options:{datex:boolean}):any -export function bindOrigin(target: any, propertyKey: string, descriptor: PropertyDescriptor):any -export function bindOrigin(_invalid_param_0_: HTMLElement, _invalid_param_1_?: string, _invalid_param_2_?: PropertyDescriptor):any - -export function bindOrigin(...args:any[]) { - return handleDecoratorArgs(args, _bindOrigin); -} - -function _bindOrigin(val:(...args:any)=>any, name:context_name, kind:context_kind, is_static:boolean, is_private:boolean, setMetadata:context_meta_setter, getMetadata:context_meta_getter, params:[{datex:boolean}?] = []) { - if (is_static) { - logger.error("@UIX.bindOrigin cannot be used on static class fields"); - return; - } - setMetadata(STANDALONE_PROPS, name); - setMetadata(ORIGIN_PROPS, params[0]??{datex:false}); +export function bindOrigin(options:{datex:boolean}): (value: undefined, context: ClassFieldDecoratorContext|ClassMethodDecoratorContext) => void +export function bindOrigin(value: undefined, context: ClassFieldDecoratorContext|ClassMethodDecoratorContext): void +export function bindOrigin(options:{datex:boolean}|undefined, context?: ClassFieldDecoratorContext|ClassMethodDecoratorContext) { + return handleClassFieldDecoratorWithOptionalArgs([options], context as ClassFieldDecoratorContext, + ([options], context:ClassFieldDecoratorContext|ClassMethodDecoratorContext) => { + Decorators.setMetadata(context, STANDALONE_PROPS, context.name); + Decorators.setMetadata(context, ORIGIN_PROPS, options ?? {datex:false}); + } + ) } \ No newline at end of file diff --git a/src/base/uix-datex-module.ts b/src/base/uix-datex-module.ts index dd1016754..7d79f1b47 100644 --- a/src/base/uix-datex-module.ts +++ b/src/base/uix-datex-module.ts @@ -1,18 +1,16 @@ -import { scope, expose } from "datex-core-legacy"; import { client_type } from "datex-core-legacy/utils/constants.ts"; +import { datex } from "datex-core-legacy/mod.ts"; const stage = client_type == "deno" ? (await import("../app/args.ts#lazy")).stage : "TODO!"; -// uix.stage -const stageTransformFunction = await datex` - function (options) ( - use currentStage from #public.uix; - always options.(currentStage) default @@local - ); -` - -@scope("uix") class UIXDatexModule { - @expose static LANG = "en"; - @expose static stage = stageTransformFunction - @expose static currentStage = stage -} \ No newline at end of file +// Can't use @endpoint class here, because endpoint is only initialized after #public.uix is required in .dx config +await datex` + #public.uix = { + LANG: "en", + currentStage: ${stage}, + stage: function (options) ( + use currentStage from #public.uix; + always options.(currentStage) default @@local + ) + }; +` \ No newline at end of file diff --git a/src/components/Component.ts b/src/components/Component.ts index 9d9cc38fa..5528418c6 100644 --- a/src/components/Component.ts +++ b/src/components/Component.ts @@ -1,5 +1,5 @@ // deno-lint-ignore-file no-async-promise-executor -import { constructor, Datex, property, replicator, template, get} from "datex-core-legacy" +import { Datex, property, get} from "datex-core-legacy" import { logger } from "../utils/global-values.ts" import { Class, Logger, METADATA, ValueError } from "datex-core-legacy/datex_all.ts" import { CHILD_PROPS, CONTENT_PROPS, ID_PROPS, IMPORT_PROPS, LAYOUT_PROPS, ORIGIN_PROPS, STANDALONE_PROPS } from "../base/decorators.ts"; @@ -17,27 +17,24 @@ import { BOUND_TO_ORIGIN, bindToOrigin, getValueInitializer } from "../app/datex import type { DynamicCSSStyleSheet } from "../utils/css-template-strings.ts"; import { addCSSScopeSelector } from "../utils/css-scoping.ts" import { jsxInputGenerator } from "../html/template.ts"; -import { bindObserver, domContext, domUtils } from "../app/dom-context.ts"; +import { domContext, domUtils } from "../app/dom-context.ts"; import { UIX } from "../../uix.ts"; import { convertToWebPath } from "../app/convert-to-web-path.ts"; import { client_type } from "datex-core-legacy/utils/constants.ts"; import { app } from "../app/app.ts"; import { fileExists } from "../utils/files.ts"; +import { DISPOSE_BOUND_PROTOTYPE } from "../standalone/get_prototype_properties.ts"; export type propInit = {datex?:boolean}; export type standaloneContentPropertyData = {type:'id'|'content'|'layout'|'child',id:string}; export type standalonePropertyData = {type:'prop'} export type standaloneProperties = Record; -// deno-lint-ignore no-namespace -export namespace Component { - export interface Options { - title?: string - } -} +// deno-lint-ignore no-empty-interface +interface Options {} // @template("uix:component") -export abstract class Component extends domContext.HTMLElement implements RouteManager { +export abstract class Component extends domContext.HTMLElement implements RouteManager { /************************************ STATIC ***************************************/ @@ -49,7 +46,7 @@ export abstract class Component // list of all default option keys that need to be cloned when options are initialized (non-primitive options) // guessing module stylesheets, get added to normal stylesheets array after successful fetch @@ -112,9 +109,10 @@ export abstract class Component = {} + private static async loadDatexImports(target:Component|typeof Component, valid_dx_files:string[], dx_file_values:Map]>){ const allowed_imports:Record = target[METADATA]?.[IMPORT_PROPS]?.public - + // try to resolve imports for (const [prop, [location, exprt]] of Object.entries(allowed_imports??{})) { - // try to get from module dx files if (location == undefined) { let found = false; @@ -149,13 +149,13 @@ export abstract class Componenttarget)[prop] = file_val; + this.virtualDatexPrototype[prop] = file_val; found = true; file_data[1].add(exprt); // remember that export was used logger.debug(`using DATEX export '${exprt}' ${exprt!=prop?`as '${prop}' `:''}in '${this.name}'`); } else if (Datex.DatexObject.has(file_val, exprt)) { - (target)[prop] = Datex.DatexObject.get(file_val, exprt); + this.virtualDatexPrototype[prop] = Datex.DatexObject.get(file_val, exprt); found = true; file_data[1].add(exprt); // remember that export was used logger.debug(`using DATEX export '${exprt}' ${exprt!=prop?`as '${prop}' `:''}in '${this.name}'`); @@ -197,17 +197,15 @@ export abstract class Component(); private static loadStandaloneProps() { - const scope = this.prototype; - if (this.standalone_loaded.has(this)) return; this.standalone_loaded.add(this); this.standaloneMethods = {}; this.standaloneProperties = {}; - const parentProps = (Object.getPrototypeOf(this).prototype)?.[METADATA]?.[STANDALONE_PROPS]?.public; - const props:Record = scope[METADATA]?.[STANDALONE_PROPS]?.public; - const originProps:Record = scope[METADATA]?.[ORIGIN_PROPS]?.public; + const parentProps = (Object.getPrototypeOf(this))?.[METADATA]?.[STANDALONE_PROPS]?.public; + const props:Record = this[METADATA]?.[STANDALONE_PROPS]?.public; + const originProps:Record = this[METADATA]?.[ORIGIN_PROPS]?.public; if (!props) return; // workaround: [STANDALONE_PROPS] from parent isn't overriden, just ignore @@ -215,14 +213,13 @@ export abstract class Component>>", originProps?.[name], typeof originProps?.[name], name, "<--") - if (scope[name]) { + if ((this.prototype as any)[name]) { // also bound to origin if (originProps?.[name]) { this.addStandaloneProperty(name, originProps?.[name]); - } else this.addStandaloneMethod(name, scope[name]); + } else this.addStandaloneMethod(name, (this.prototype as any)[name]); } - // otherwise, instace property + // otherwise, instance property else this.addStandaloneProperty(name, originProps?.[name]); } } @@ -246,8 +243,8 @@ export abstract class ComponentclassType).construct(this, [], true, true); } @@ -628,12 +628,12 @@ export abstract class Component = Object.getPrototypeOf(this)[METADATA]?.[ID_PROPS]?.public; - const content_props:Record = Object.getPrototypeOf(this)[METADATA]?.[CONTENT_PROPS]?.public; - const layout_props:Record = Object.getPrototypeOf(this)[METADATA]?.[LAYOUT_PROPS]?.public; + const id_props:Record = (this.constructor as any)[METADATA]?.[ID_PROPS]?.public; + const content_props:Record = (this.constructor as any)[METADATA]?.[CONTENT_PROPS]?.public; + const layout_props:Record = (this.constructor as any)[METADATA]?.[LAYOUT_PROPS]?.public; // only add children when constructing component, otherwise they are added twice - const child_props:Record = constructed ? Object.getPrototypeOf(this)[METADATA]?.[CHILD_PROPS]?.public : undefined; - bindContentProperties(this, id_props, content_props, layout_props, child_props); + const child_props:Record = constructed ? (this.constructor as any)[METADATA]?.[CHILD_PROPS]?.public : undefined; + bindContentProperties(this, id_props, content_props, layout_props, child_props); } @@ -680,7 +680,7 @@ export abstract class Component): Promise { + async construct(options?:Datex.DatexObjectInit): Promise { // options already handled in constructor // handle default component options (class, ...) @@ -694,8 +694,11 @@ export abstract class Componentthis.constructor).init(); + this.inheritDatexProperties(); + if (!this.reconstructed_from_dom) await this.loadTemplate(); else this.logger.debug("Reconstructed from DOM, not creating new template content") this.loadDefaultStyle() @@ -706,11 +709,12 @@ export abstract class Componentthis.constructor).init(); - // this.loadTemplate(); + this.inheritDatexProperties(); + this.loadDefaultStyle() await this.init(); this.#datex_lifecycle_ready_resolve?.(); // onCreate can be called (required because of async) @@ -775,7 +779,7 @@ export abstract class Componentname] = > this.attributes[i].value; + options[name] = > this.attributes[i].value; } } } @@ -784,6 +788,13 @@ export abstract class Componentthis.constructor).virtualDatexPrototype); + } + // init for base element (and every element) protected async init(constructed = false) { @@ -794,27 +805,31 @@ export abstract class Componentthis.constructor).stylesheets??[]) loaders.push(this.addStyleSheet(url)); + let standaloneOnDisplayWasTriggered = false + if ((this as any)[DISPOSE_BOUND_PROTOTYPE]) { + standaloneOnDisplayWasTriggered = (this as any)[DISPOSE_BOUND_PROTOTYPE](); + } + + this.onCreateLayout?.(); // custom layout extensions + + // @id, @content, @layout + this.handleIdProps(constructed); + + // @standlone props only relevant for backend + if (UIX.context == "backend") this.loadStandaloneProps(); + Datex.Pointer.onPointerForValueCreated(this, () => { const pointer = Datex.Pointer.getByValue(this)! if (!this.hasAttribute("uix-ptr")) this.setAttribute("uix-ptr", pointer.id); if (this.is_skeleton && UIX.context == "frontend") { this.logger.debug("hybrid initialization") - this.onDisplay?.(); + if (!standaloneOnDisplayWasTriggered) this.onDisplay?.(); } // TODO: required? should probably not be called per default // bindObserver(this) }) - - this.onCreateLayout?.(); // custom layout extensions - - // @id, @content, @layout - this.handleIdProps(constructed); - - // @standlone props only relevant for backend - if (UIX.context == "backend") this.loadStandaloneProps(); - if (constructed) await this.onConstruct?.(); // this.bindOriginMethods(); @@ -866,20 +881,19 @@ export abstract class Component = scope[METADATA]?.[ORIGIN_PROPS]?.public; + const scope = this.constructor as any; + const originProps:Record|undefined = scope[METADATA]?.[ORIGIN_PROPS]?.public; // init props with current values for (const [name, data] of Object.entries((this.constructor as typeof Component).standaloneProperties)) { // check if prop is method if (typeof this[name] === "function") { - if (originProps[name] && !(this[name] as any)[BOUND_TO_ORIGIN]) { + if (originProps?.[name] && !(this[name] as any)[BOUND_TO_ORIGIN]) { // @ts-ignore $ - this[name] = bindToOrigin(this[name], this, null, originProps[name].datex); + this[name] = bindToOrigin(this[name], this, null, originProps[name].datex); } js_code += `self["${name}"] = ${Component.getStandaloneMethodContentWithMappedImports(this[name] as Function)};\n`; } @@ -1083,7 +1097,7 @@ export abstract class Component>delegate).onRoute?.(route.route[0]??"", initial_route); + const child = await (>delegate).onRoute?.(route.route[0]??"", initial_route); if (child == false) return []; // route not valid else if (typeof (child)?.focus == "function") { @@ -1135,10 +1149,10 @@ export abstract class Component void) { + public observeOption(key:keyof O, handler: (value: unknown, key?: unknown, type?: Datex.Ref.UPDATE_TYPE) => void) { Datex.Ref.observeAndInit(this.options.$$[key as keyof typeof this.options.$$], handler, this); } - public observeOptions(keys:(keyof O & Component.Options)[], handler: (value: unknown, key?: unknown, type?: Datex.Ref.UPDATE_TYPE) => void) { + public observeOptions(keys:(keyof O)[], handler: (value: unknown, key?: unknown, type?: Datex.Ref.UPDATE_TYPE) => void) { for (const key of keys) this.observeOption(key, handler); } diff --git a/src/html/html-provider.ts b/src/html/html-provider.ts index 7b1d98b59..da9e408a3 100644 --- a/src/html/html-provider.ts +++ b/src/html/html-provider.ts @@ -41,7 +41,7 @@ export class HTMLProvider { resolveForBackend(path:string|URL):string { path = path.toString(); if (path.startsWith("uix://")) path = new Path("." + path.replace("uix:///@uix/src",""), this.base_path).toString(); - return import.meta.resolve(path) + return Path.pathIsURL(path) ? path.toString() : import.meta.resolve(path) } getRelativeImportMap() { diff --git a/src/html/render.ts b/src/html/render.ts index fc192c698..0222ee8f5 100644 --- a/src/html/render.ts +++ b/src/html/render.ts @@ -254,7 +254,7 @@ function _getOuterHTML(el:Node, opts?:_renderOptions, collectedStylesheets?:stri else attrs.push("uix-static"); // inject event listeners - if (dataPtr && opts?._injectedJsData && ((el)[DOMUtils.EVENT_LISTENERS] || (el)[DOMUtils.PSEUDO_ATTR_BINDINGS])) { + if (dataPtr && opts?._injectedJsData && ((el)[DOMUtils.EVENT_LISTENERS] || (el)[DOMUtils.ATTR_BINDINGS])) { let context: HTMLElement|undefined; let parent: Element|null = el; let hasScriptContent = false; // indicates whether the generated script actually contains relevant content, not just skeleton code @@ -315,7 +315,7 @@ function _getOuterHTML(el:Node, opts?:_renderOptions, collectedStylesheets?:stri throw new Error(`Invalid datex-update="onsubmit", no form found`) } - for (const [attr, ptr] of (el)[DOMUtils.PSEUDO_ATTR_BINDINGS] ?? []) { + for (const [attr, ptr] of (el)[DOMUtils.ATTR_BINDINGS] ?? []) { opts?.requiredPointers?.add(ptr); @@ -484,7 +484,7 @@ const isNormalFunction = (fnSrc:string) => { function isLiveNode(node: Node) { // hybrid components are always live (TODO: not for backend-only components) if (node instanceof Component) return true; - if (node[DOMUtils.PSEUDO_ATTR_BINDINGS]?.size) return true; + if (node[DOMUtils.ATTR_BINDINGS]?.size) return true; if (node[DOMUtils.EVENT_LISTENERS]?.size) return true; // uix-placeholder are always live if (node instanceof domContext.Element && node.tagName?.toLowerCase() == "uix-placeholder") return true; diff --git a/src/html/style.ts b/src/html/style.ts index 5ab238fd6..6dccd030a 100644 --- a/src/html/style.ts +++ b/src/html/style.ts @@ -20,7 +20,13 @@ import type { HTMLElement } from "../uix-dom/dom/mod.ts"; * ``` * @param styleGenerator */ -export function style = {}, Children = JSX.childrenOrChildrenPromise|JSX.childrenOrChildrenPromise[], Context = unknown>(styleGenerator:jsxInputGenerator):jsxInputGenerator&((cl:typeof HTMLElement)=>any) +export function style< + Options extends Record = {}, + Children = JSX.childrenOrChildrenPromise|JSX.childrenOrChildrenPromise[], + Context extends typeof HTMLElement = typeof HTMLElement +>( + styleGenerator:jsxInputGenerator> +): jsxInputGenerator&((cl: Context, context: ClassDecoratorContext)=>any) /** * \@style decorator @@ -37,7 +43,7 @@ export function style = {}, Children = JSX.c * ``` * @param styleGenerator */ -export function style(style:CSSStyleSheet):((cl:typeof HTMLElement)=>any) +export function style(style:CSSStyleSheet):((cl:typeof HTMLElement, context: ClassDecoratorContext)=>any) /** * \@style decorator @@ -50,7 +56,7 @@ export function style(style:CSSStyleSheet):((cl:typeof HTMLElement)=>any) * ``` * @param styleGenerator */ -export function style(file:string|URL):((cl:typeof HTMLElement)=>any) +export function style(file:string|URL):((cl:typeof HTMLElement, context: ClassDecoratorContext)=>any) export function style(templateOrGenerator:string|URL|CSSStyleSheet|jsxInputGenerator) { diff --git a/src/html/template.ts b/src/html/template.ts index 5b7382dde..79f824f21 100644 --- a/src/html/template.ts +++ b/src/html/template.ts @@ -5,7 +5,9 @@ import { Component } from "../components/Component.ts"; import { DOMUtils } from "../uix-dom/datex-bindings/dom-utils.ts"; import { domContext, domUtils } from "../app/dom-context.ts"; import type { Element, Node, HTMLElement, HTMLTemplateElement } from "../uix-dom/dom/mod.ts"; -import { defaultOptions } from "../base/decorators.ts"; +import { defaultOptions, initDefaultOptions } from "../base/decorators.ts"; +import type { Class } from "datex-core-legacy/utils/global_types.ts"; +import { METADATA } from "datex-core-legacy/js_adapter/js_class_adapter.ts"; /** * cloneNode(true), but also clones shadow roots. @@ -72,15 +74,29 @@ type Props, Children, handleAllProps = tr ) : unknown ) + type ObjectWithCollapsedValues> = { [K in keyof O]: O[K] extends Datex.RefOrValue ? T : O[K] } -export type jsxInputGenerator, Children, handleAllProps = true, optionalChildren = true, Context = unknown> = +export type jsxInputGenerator< + Return, + Options extends Record, + Children, + handleAllProps = true, + optionalChildren = true, + Context extends HTMLElement = HTMLElement +> = ( this: Context, - props: Props, - propsValues: ObjectWithCollapsedValues> + props: Props< + // inferred options: + Context extends {options:unknown} ? Omit<(Context)['options'], '$'|'$$'> : Options + , Children, handleAllProps, optionalChildren>, + propsValues: ObjectWithCollapsedValues : Options + , Children, handleAllProps, optionalChildren>> ) => Return; @@ -90,7 +106,7 @@ export type jsxInputGenerator, Ch * Custom Attributes can be handled in the generator * @example * ```tsx - * const CustomComponent = UIX.template<{color:string}>(({color}) =>
) + * const CustomComponent = template<{color:string}>(({color}) =>
) * // create: * const comp = * ``` @@ -99,7 +115,7 @@ export type jsxInputGenerator, Ch * Children are appended to the element inside the root: * @example * ```tsx - * const CustomComponent2 = UIX.template<{color:string}>(({color}) => + * const CustomComponent2 = template<{color:string}>(({color}) => *
* Custom content before children * @@ -115,19 +131,27 @@ export type jsxInputGenerator, Ch * ``` * @param elementGenerator */ -export function template = {}, Children = JSX.childrenOrChildrenPromise|JSX.childrenOrChildrenPromise[], Context = unknown>(elementGenerator:jsxInputGenerator, Options, never, false, false, Context>):jsxInputGenerator, Options, Children>&((cl:typeof HTMLElement)=>any) +export function template< + Options extends Record = Record, + Children = JSX.childrenOrChildrenPromise|JSX.childrenOrChildrenPromise[], + Context extends typeof HTMLElement = typeof HTMLElement +> ( + elementGenerator: jsxInputGenerator, Options, never, false, false, InstanceType> +): + jsxInputGenerator, Options, Children>&((cl: Context, context: ClassDecoratorContext)=>any) + /** * Define an HTML template that can be used as an anonymous JSX component. * Default HTML Attributes defined in JSX are also set for the root element. * @example * ```tsx - * const CustomComponent = UIX.template(
) + * const CustomComponent = template(
) * // create: * const comp = * ``` * @param elementGenerator */ -export function template = {}, Children = JSX.childrenOrChildrenPromise|JSX.childrenOrChildrenPromise[]>(element:JSX.Element):jsxInputGenerator&((cl:typeof HTMLElement)=>any) +export function template = {}, Children = JSX.childrenOrChildrenPromise|JSX.childrenOrChildrenPromise[]>(element:JSX.Element):jsxInputGenerator&((cl: Class, context: ClassDecoratorContext)=>any) /** * Empty template for component @@ -138,7 +162,7 @@ export function template = {}, Children = JS * ``` * @param elementGenerator */ -export function template():jsxInputGenerator, never>&((cl:typeof HTMLElement)=>any) +export function template():jsxInputGenerator, never>&((cl: Class, context: ClassDecoratorContext)=>any) export function template(templateOrGenerator?:JSX.Element|jsxInputGenerator, any, any, any>) { @@ -148,7 +172,9 @@ export function template(templateOrGenerator?:JSX.Element|jsxInputGenerator(({color, style, id, children}) =>

Header

{...children}
) + * const CustomComponent = blankTemplate<{color:string}>(({color, style, id, children}) =>

Header

{...children}
) * // create: * const comp = ( * @@ -223,7 +253,13 @@ export function template(templateOrGenerator?:JSX.Element|jsxInputGenerator, Children = JSX.childrenOrChildrenPromise|JSX.childrenOrChildrenPromise[]>(elementGenerator:jsxInputGenerator, Options, Children extends any[] ? Children : Children[], true, false>):jsxInputGenerator, Options, Children>&((cl:typeof HTMLElement)=>any) { +export function blankTemplate< + Options extends Record, + Children = JSX.childrenOrChildrenPromise|JSX.childrenOrChildrenPromise[], + Context extends typeof HTMLElement = typeof HTMLElement +> ( + elementGenerator: jsxInputGenerator, Options, Children extends any[] ? Children : Children[], true, false, InstanceType> +): jsxInputGenerator, Options, Children>&((cl: Context, context: ClassDecoratorContext)=>any) { const module = getCallerFile(); function generator(propsOrClass:any) { @@ -231,7 +267,7 @@ export function blankTemplate, Children = JS // decorator if (Component.isPrototypeOf(propsOrClass)) { propsOrClass._init_module = module; - const decoratedClass = defaultOptions(propsOrClass) + const decoratedClass = initDefaultOptions(module, propsOrClass) decoratedClass.template = generator decoratedClass[SET_DEFAULT_CHILDREN] = false; return decoratedClass diff --git a/src/hydration/partial-hydration.ts b/src/hydration/partial-hydration.ts index 2e573c30b..65c897c06 100644 --- a/src/hydration/partial-hydration.ts +++ b/src/hydration/partial-hydration.ts @@ -1,11 +1,9 @@ -import { constructor } from "datex-core-legacy/js_adapter/legacy_decorators.ts"; import type { Element, Node } from "../uix-dom/dom/mod.ts"; @sync("uix:PartialHydration") export class PartialHydration { @property nodes!: Node[] - constructor(root:Element){} - @constructor construct(root: Element) { + construct(root: Element) { // this.nodes = getLiveNodes(root); } diff --git a/src/hydration/partial.ts b/src/hydration/partial.ts index 3312d1ddb..837185a97 100644 --- a/src/hydration/partial.ts +++ b/src/hydration/partial.ts @@ -29,7 +29,7 @@ export function getLiveNodes(treeRoot: Element, includeEventListeners = true, _l } if (!isLive) { - if (treeRoot[DOMUtils.PSEUDO_ATTR_BINDINGS]?.size) isLive = true + if (treeRoot[DOMUtils.ATTR_BINDINGS]?.size) isLive = true } if (isLive) _list.push(treeRoot); diff --git a/src/plugins/git-deploy.ts b/src/plugins/git-deploy.ts index 2648baacb..2566d4137 100644 --- a/src/plugins/git-deploy.ts +++ b/src/plugins/git-deploy.ts @@ -4,13 +4,17 @@ import { GitRepo } from "../utils/git.ts"; import { json2yaml } from "https://deno.land/x/json2yaml@v1.0.1/mod.ts"; import { Datex } from "datex-core-legacy/mod.ts"; import { isCIRunner } from "../utils/check-ci.ts"; +import { normalizedAppOptions } from "../app/options.ts"; +import { app } from "../app/app.ts"; +import { Path } from "datex-core-legacy/utils/path.ts"; +import { getInferredRunPaths } from "../app/options.ts"; const logger = new Logger("Git Deploy Plugin"); export default class GitDeployPlugin implements AppPlugin { name = "git_deploy" - async apply(data: Record) { + async apply(data: Record, rootPath: Path.File, appOptions: normalizedAppOptions) { // don't update CI workflows from inside a CI runner if (isCIRunner()) { @@ -28,7 +32,7 @@ export default class GitDeployPlugin implements AppPlugin { const workflowDir = await gitRepo.initWorkflowDirectory(); // TODO: also support gitlab - const workflows = this.generateGithubWorkflows(data); + const workflows = this.generateGithubWorkflows(data, rootPath, appOptions); // first delete all old uix-deploy.yml files for await (const entry of Deno.readDir(workflowDir.normal_pathname)) { @@ -43,9 +47,11 @@ export default class GitDeployPlugin implements AppPlugin { } - generateGithubWorkflows(data: Record) { + generateGithubWorkflows(data: Record, rootPath: Path.File, appOptions: normalizedAppOptions) { const workflows: Record = {} + const {importMapPath, uixRunPath} = getInferredRunPaths(appOptions.import_map, rootPath) + for (let [stage, config] of Object.entries(data)) { config = Object.fromEntries(Datex.DatexObject.entries(config)); @@ -55,10 +61,10 @@ export default class GitDeployPlugin implements AppPlugin { const tests = config.tests ?? true; const useDevCDN = config.useDevCDN; - const importmapPath = useDevCDN ? "https://dev.cdn.unyt.org/importmap.json" : "https://cdn.unyt.org/importmap.json" - const importmapPathUIX = useDevCDN ? "https://dev.cdn.unyt.org/uix1/importmap.dev.json" : "https://cdn.unyt.org/uix/importmap.json" + const importmapPath = useDevCDN ? "https://dev.cdn.unyt.org/importmap.json" : (importMapPath??"https://cdn.unyt.org/importmap.json") + const importmapPathUIX = useDevCDN ? "https://dev.cdn.unyt.org/uix1/importmap.dev.json" : (importMapPath??"https://cdn.unyt.org/importmap.json") const testRunPath = useDevCDN ? "https://dev.cdn.unyt.org/unyt_tests/run.ts" : "https://cdn.unyt.org/unyt-tests/run.ts" - const uixRunPath = useDevCDN ? "https://dev.cdn.unyt.org/uix1/run.ts" : "https://cdn.unyt.org/uix/run.ts" + const uixRunnerPath = useDevCDN ? "https://dev.cdn.unyt.org/uix1/run.ts" : (uixRunPath??"https://cdn.unyt.org/uix/run.ts") if (branch && branch !== "*") { on = { @@ -90,10 +96,7 @@ export default class GitDeployPlugin implements AppPlugin { }, { name: 'Setup Deno', - uses: 'denoland/setup-deno@v1', - with: { - 'deno-version': '1.39.4' - } + uses: 'denoland/setup-deno@v1' }, { name: 'Run Tests', @@ -116,18 +119,18 @@ export default class GitDeployPlugin implements AppPlugin { steps: [ { name: 'Checkout Repo', - uses: 'actions/checkout@v3' + uses: 'actions/checkout@v3', + with: { + submodules: 'recursive' + } }, { name: 'Setup Deno', - uses: 'denoland/setup-deno@v1', - with: { - 'deno-version': '1.39.4' - } + uses: 'denoland/setup-deno@v1' }, { name: 'Deploy UIX App', - run: `deno run --importmap ${importmapPathUIX} -Aqr ${uixRunPath} --stage ${stage} --detach` + (args ? ' ' + args.join(" ") : '') + (env_strings ? ' ' + env_strings.join(" ") : '') + run: `deno run --importmap ${importmapPathUIX} -Aqr ${uixRunnerPath} --stage ${stage} --detach` + (args ? ' ' + args.join(" ") : '') + (env_strings ? ' ' + env_strings.join(" ") : '') } ] } diff --git a/src/routing/frontend-routing.ts b/src/routing/frontend-routing.ts index 158103ac5..21490aff9 100644 --- a/src/routing/frontend-routing.ts +++ b/src/routing/frontend-routing.ts @@ -88,11 +88,11 @@ export namespace Routing { if (el instanceof HTMLElement && el.hasAttribute("slot")) { const name = el.getAttribute("slot")! slot = querySelector(`frontend-slot[name="${domUtils.escapeHtml(name)}"]`) as HTMLElement; - if (!slot) logger.error(`Could not find a matching for frontend entrypoint route`); + if (!slot) logger.error(`Could not find a matching for content provided from frontend entrypoint. Make sure your backend and frontend routes are not unintentionally colliding.`); } else { slot = querySelector("frontend-slot") as HTMLElement; - if (!slot) logger.error("Could not find a matching for frontend entrypoint route"); + if (!slot) logger.error("Could not find a matching for content provided from frontend entrypoint. Make sure your backend and frontend routes are not unintentionally colliding."); } if (slot) { diff --git a/src/runners/run-local-docker.ts b/src/runners/run-local-docker.ts index d89e4045d..26477c4cb 100644 --- a/src/runners/run-local-docker.ts +++ b/src/runners/run-local-docker.ts @@ -113,7 +113,7 @@ export default class LocalDockerRunner implements UIXRunner { services: { "uix-app": { container_name: `${name}`, - image: "denoland/deno:1.37.2", // max 1.39.4, currently 1.37.2 because of websocket issues + image: "denoland/deno", expose: ["80"], ports, diff --git a/src/runners/run-remote.ts b/src/runners/run-remote.ts index 2c1ed4c50..9bf63aa40 100644 --- a/src/runners/run-remote.ts +++ b/src/runners/run-remote.ts @@ -1,8 +1,8 @@ -import { normalizedAppOptions } from "../app/options.ts"; +import { getInferredRunPaths, normalizedAppOptions } from "../app/options.ts"; import { stage, env, watch, clear } from "../app/args.ts"; import { ESCAPE_SEQUENCES, verboseArg } from "datex-core-legacy/utils/logger.ts"; import { GitRepo } from "../utils/git.ts"; -import { Path } from "../utils/path.ts"; +import { Path } from "datex-core-legacy/utils/path.ts"; import { runParams } from "./runner.ts"; import { logger } from "../utils/global-values.ts"; import { gitToken } from "../app/args.ts"; @@ -30,7 +30,7 @@ function onlyDenoFileChanges(fileOutput: string) { * Run UIX app on a remote host * Currently using git for file sync with remote */ -export async function runRemote(params: runParams, root_path: URL, options: normalizedAppOptions, backend: URL, requiredLocation: Datex.Endpoint, stageEndpoint: Datex.Endpoint, customDomains: Record = {}, volumes:URL[] = []) { +export async function runRemote(params: runParams, root_path: Path.File, options: normalizedAppOptions, backend: URL, requiredLocation: Datex.Endpoint, stageEndpoint: Datex.Endpoint, customDomains: Record = {}, volumes:URL[] = []) { const logger = new Datex.Logger(); const repo = await GitRepo.get(); @@ -103,13 +103,14 @@ export async function runRemote(params: runParams, root_path: URL, options: norm const relativeVolumePath = new Path(volume).getAsRelativeFrom(repoRoot) normalizedVolumes.push(relativeVolumePath) } - + + const {importMapPath, uixRunPath} = getInferredRunPaths(options.import_map, root_path) + // tell docker host to use uix v.0.1 env.push(`UIX_VERSION=0.1`) const container = await datex ` - use ContainerManager from ${requiredLocation}; - ContainerManager.createUIXAppContainer( + ${requiredLocation}.ContainerManager.createUIXAppContainer( ${repo.origin}, ${repo.branch}, ${stageEndpoint}, @@ -118,12 +119,11 @@ export async function runRemote(params: runParams, root_path: URL, options: norm ${env}, ${args}, ${normalizedVolumes}, - ${gitToken ?? Deno.env.get("GITHUB_TOKEN")} + ${gitToken ?? Deno.env.get("GITHUB_TOKEN")}, + ${{importMapPath, uixRunPath}} ) ` // console.log(""); - // logger.error(container) - // observe container status and exit const handler = async (status: ContainerStatus) => { @@ -155,6 +155,7 @@ export async function runRemote(params: runParams, root_path: URL, options: norm } catch (e) { + console.error(e) logFailure(stageEndpoint, requiredLocation, customDomains, e.message) } diff --git a/src/runners/runner.ts b/src/runners/runner.ts index 53ba07939..def464b8f 100644 --- a/src/runners/runner.ts +++ b/src/runners/runner.ts @@ -13,7 +13,7 @@ export type runParams = { inspect: string | undefined; unstable: boolean | undefined; detach: boolean | undefined; - deno_config_path: string | URL | null; + deno_config_path: URL | null; } export type runOptions = { diff --git a/src/server/network-interface.ts b/src/server/network-interface.ts index 73e2a2a35..09db1b549 100644 --- a/src/server/network-interface.ts +++ b/src/server/network-interface.ts @@ -1,10 +1,10 @@ /** Custom ROUDINI stuff */ -import { Datex, expose, scope } from "datex-core-legacy/mod.ts"; +import { Datex } from "datex-core-legacy/mod.ts"; -@scope("network") abstract class network { +@endpoint export abstract class network { /** get sign and encryption keys for an alias */ - @expose static async get_keys(endpoint: Datex.Endpoint) { + @property static async get_keys(endpoint: Datex.Endpoint) { // console.log("GET keys for " +endpoint) const keys = await Datex.Crypto.getExportedKeysForEndpoint(endpoint); return keys; diff --git a/src/server/standalone-file-server.ts b/src/server/standalone-file-server.ts index 36407be59..657cc07c8 100644 --- a/src/server/standalone-file-server.ts +++ b/src/server/standalone-file-server.ts @@ -4,8 +4,8 @@ await Deno.run({ "run", "-Aq", "--import-map", - "https://dev.cdn.unyt.org/importmap.json", - "https://dev.cdn.unyt.org/uix1/src/server/file-server-runner.ts", + "https://cdn.unyt.org/uix/importmap.json", + "https://cdn.unyt.org/uix/src/server/file-server-runner.ts", ...Deno.args ] }).status() \ No newline at end of file diff --git a/src/server/transpiler.ts b/src/server/transpiler.ts index 59eba2e74..7585608dc 100644 --- a/src/server/transpiler.ts +++ b/src/server/transpiler.ts @@ -1,4 +1,4 @@ -import { Path } from "../utils/path.ts"; +import { Path } from "datex-core-legacy/utils/path.ts"; import { Datex } from "datex-core-legacy"; import { TypescriptImportResolver } from "./ts-import-resolver.ts"; import { getCallerDir } from "datex-core-legacy/utils/caller_metadata.ts"; @@ -33,8 +33,9 @@ export type transpiler_options = { import_resolver?: TypescriptImportResolver, dist_parent_dir?: Path.File, // parent dir for dist dirs dist_dir?: Path.File, // use different path for dist (default: generated tmp dir) - sourceMap?: boolean // generate inline source maps when transpiling ts, + sourceMaps?: boolean // generate inline source maps when transpiling ts, minifyJS?: boolean // minify js files after transpiling + basePath?: Path.File } type transpiler_options_all = Required; @@ -88,7 +89,7 @@ export class Transpiler { // returns true if the file has to be transpiled isTranspiledFile(path:Path) { - return path.hasFileExtension(...this.#transpile_exts) && !path.hasFileExtension('d.ts') + return (path.hasFileExtension(...this.#transpile_exts, 'map')) && !path.hasFileExtension('d.ts') } // returns true if the tranpiled file has the same name as the src file (e.g. x.css -> x.css) @@ -351,7 +352,7 @@ export class Transpiler { } protected async transpileTS(dist_path:Path.File, src_path:Path.File) { - const js_dist_path = await this.transpileToJS(dist_path) + const js_dist_path = await this.transpileToJS(dist_path, src_path) if (this.import_resolver) { await this.import_resolver.resolveImports(dist_path, src_path, true); // resolve imports in ts, no side effects (don't update referenced module files) await this.import_resolver.resolveImports(js_dist_path, src_path) @@ -525,7 +526,7 @@ export class Transpiler { if (dist_path_js && await dist_path_js.fsExists()) await Deno.remove(dist_path_js) } - private transpileToJS(ts_dist_path: Path.File) { + private transpileToJS(ts_dist_path: Path.File, src_path: Path.File) { // check if corresponding ts file exists @@ -545,9 +546,8 @@ export class Transpiler { if (!valid) throw new Error("the typescript file cannot be transpiled - not a valid file extension"); - return app.options?.experimentalFeatures.includes('embedded-reactivity') ? - this.transpileToJSSWC(ts_dist_path, true): - this.transpileToJSSWC(ts_dist_path, false) + // return this.transpileToJSDenoEmit(ts_dist_path) + return this.transpileToJSSWC(ts_dist_path, src_path, app.options?.experimentalFeatures.includes('embedded-reactivity')); } private async transpileToJSDenoEmit(ts_dist_path:Path.File) { @@ -561,11 +561,15 @@ export class Transpiler { } as const : null; // TODO: remove jsxAutomatic:true, currently only because of caching problems const transpiled = await transpile(await Deno.readTextFile(ts_dist_path.normal_pathname), { - inlineSourceMap: !!this.#options.sourceMap, - inlineSources: !!this.#options.sourceMap, + inlineSourceMap: !!this.#options.sourceMaps, + inlineSources: !!this.#options.sourceMaps, ...jsxOptions }); - if (transpiled != undefined) await Deno.writeTextFile(js_dist_path.normal_pathname, transpiled); + if (transpiled != undefined) await Deno.writeTextFile(js_dist_path.normal_pathname, + this.#options.minifyJS ? + await this.minifyJS(transpiled) : + transpiled + ); else throw "unknown error" } catch (e) { @@ -575,8 +579,8 @@ export class Transpiler { return js_dist_path; } - private async transpileToJSSWC(ts_dist_path: Path.File, useJusix = false) { - const {transformSync} = await import("npm:@swc/core"); + private async transpileToJSSWC(ts_dist_path: Path.File, src_path: Path.File, useJusix = false) { + const {transform} = await import("npm:@swc/core@^1.4.2"); const experimentalPlugins = useJusix ? { plugins: [ @@ -586,7 +590,15 @@ export class Transpiler { const js_dist_path = this.getFileWithMappedExtension(ts_dist_path); try { - const transpiled = transformSync!(await Deno.readTextFile(ts_dist_path.normal_pathname), { + + // workaround: select decorators based on uix/datex version + let decoratorVersion = "2022-03"; + const pathname = ts_dist_path.normal_pathname; + if (pathname.match(/\/uix-0\.(0|1)\.\d+\//)||pathname.match(/\/datex-core-js-legacy-0\.0\.\d+\//)) decoratorVersion = "2021-12"; + + const file = await Deno.readTextFile(ts_dist_path.normal_pathname) + let {code: transpiled, map} = await transform(file, { + sourceMaps: !!this.#options.sourceMaps, jsc: { parser: { tsx: !!ts_dist_path.hasFileExtension("tsx"), @@ -596,25 +608,42 @@ export class Transpiler { }, transform: { - legacyDecorator: true, - decoratorMetadata: true, + decoratorVersion, react: { runtime: "automatic", importSource: "uix", throwIfNamespace: false } }, - target: "es2022", + target: "esnext", keepClassNames: true, externalHelpers: false, - experimental: experimentalPlugins + experimental: experimentalPlugins, + minify: (this.#options.minifyJS && this.#options.sourceMaps) ? + { + module: true, + keep_classnames: true + }: + undefined } - }).code + }); + if (map) { + transpiled = transpiled + `\n//# sourceMappingURL=./${ts_dist_path.filename}.map`; + const jsonMap = JSON.parse(map); + if (this.#options.basePath) jsonMap.sourceRoot = src_path.parent_dir.getAsRelativeFrom(this.#options.basePath).replace(/^\.\//, '/').replace(/\/[^/]+\/@uix\/src/,''); + jsonMap.file = src_path.filename; + jsonMap.sources[0] = src_path.filename; + await Deno.writeTextFile( + ts_dist_path.normal_pathname + ".map", + JSON.stringify(jsonMap) + ); + } + if (transpiled != undefined) { - await Deno.writeTextFile(js_dist_path.normal_pathname, - this.#options.minifyJS ? - await this.minifyJS(transpiled) : - transpiled + const minifyOptimized = this.#options.minifyJS && !this.#options.sourceMaps + await Deno.writeTextFile( + js_dist_path.normal_pathname, + minifyOptimized ? await this.minifyJS(transpiled) : transpiled ); } else throw "unknown error" diff --git a/src/server/ts-import-resolver.ts b/src/server/ts-import-resolver.ts index 367498dcf..2ea3f6161 100644 --- a/src/server/ts-import-resolver.ts +++ b/src/server/ts-import-resolver.ts @@ -139,8 +139,9 @@ export class TypescriptImportResolver { const rel_import_path = is_prefix ? null : this.resolveImportSpecifier(specifier) const abs_import_path = is_prefix ? null : new Path(rel_import_path!, reference_path); - // workaround: ignore 'node:x' paths - if (abs_import_path?.toString().startsWith("node:")) return match; + // workaround: ignore 'node:x'/'npm:x' paths + const pathString = abs_import_path?.toString() + if (pathString?.startsWith("node:") || pathString?.startsWith("npm:")) return match; // already resolved web path if (abs_import_path?.is_web) return match; diff --git a/src/standalone/get_prototype_properties.ts b/src/standalone/get_prototype_properties.ts index c3be43496..af46b2bf6 100644 --- a/src/standalone/get_prototype_properties.ts +++ b/src/standalone/get_prototype_properties.ts @@ -21,13 +21,27 @@ export function* getPrototypeProperties(clss: Class) { return; } + +export const DISPOSE_BOUND_PROTOTYPE = Symbol("DISPOSE_BOUND_PROTOTYPE"); + /** * Bind class prototoype to a value to act if the value was extending the class * @param value object * @param clss class with prototype */ export function bindPrototype(value:Record, clss:Class) { + const keys = new Set() + value[DISPOSE_BOUND_PROTOTYPE] = () => { + const hasOnDisplay = !!keys.has("onDisplay"); + for (const k of keys) delete value[k]; + delete value[DISPOSE_BOUND_PROTOTYPE]; + return hasOnDisplay; + } + for (const [n,v] of getPrototypeProperties(clss)) { + if (n == "constructor") continue; value[n] = v.bind(value); + keys.add(n) } + } \ No newline at end of file diff --git a/src/uix-dom b/src/uix-dom index c3809434c..18d50ba77 160000 --- a/src/uix-dom +++ b/src/uix-dom @@ -1 +1 @@ -Subproject commit c3809434cf401743033e15f40d972363bbeab33f +Subproject commit 18d50ba776d80c649d17f47c9eaeb80dfd0671f2 diff --git a/src/utils/importmap.ts b/src/utils/importmap.ts index 568d7fd8a..141657cf2 100644 --- a/src/utils/importmap.ts +++ b/src/utils/importmap.ts @@ -1,4 +1,4 @@ -import { Path } from "../utils/path.ts"; +import { Path } from "datex-core-legacy/utils/path.ts"; import { Logger } from "datex-core-legacy/utils/logger.ts"; const logger = new Logger("UIX Import Map"); @@ -46,21 +46,24 @@ export class ImportMap { #readonly = true; #json: {imports:Record, scopes?:Record>}; #path?: Path; + #originalPath?: Path #temporary_imports = new Set; get readonly() {return this.#readonly} get path() {return this.#path} + get originalPath() {return this.#originalPath??this.#path} static async fromPath(path:string|URL) { const map = JSON.parse(await new Path(path).getTextContent()); return new ImportMap(map, path); } - constructor(map:{imports:Record}, path?:string|URL, resetOnUnload = false) { + constructor(map:{imports:Record}, path?:string|URL, resetOnUnload = false, originalPath?:Path) { this.#json = map; this.#path = path ? new Path(path) : undefined; + this.#originalPath = originalPath; this.#readonly = !this.#path || this.#path.is_web; this.clearEntriesForExtension(".dx.d.ts"); // remove temporarily created dx.d.ts entries from previous sessions @@ -208,7 +211,7 @@ export class ImportMap { } mappedImports[key] = value; } - return new ImportMap({imports:mappedImports}, newImportMapLocation, resetOnUnload); + return new ImportMap({imports:mappedImports}, newImportMapLocation, resetOnUnload, this.originalPath); } } diff --git a/uix-short.ts b/uix-short.ts index b6d6a6b4f..19eff7e08 100644 --- a/uix-short.ts +++ b/uix-short.ts @@ -21,6 +21,7 @@ declare global { const ref: typeof _content; const id: typeof _id; const layout: typeof _layout; + const content: typeof _content; const value: typeof _child; const NoResources: typeof _NoResources; const frontend: typeof _frontend;