diff --git a/deps.ts b/deps.ts index 850dbd6..e93008f 100644 --- a/deps.ts +++ b/deps.ts @@ -1,2 +1,9 @@ export * as path from "https://deno.land/std@0.75.0/path/mod.ts"; export { v4 } from "https://deno.land/std@0.67.0/uuid/mod.ts"; +export * as fs from "https://deno.land/std@0.67.0/fs/mod.ts"; +export * as colors from "https://deno.land/std@0.61.0/fmt/colors.ts"; +export { + assertEquals, +} from "https://deno.land/std@0.76.0/testing/asserts.ts"; +export { serve } from "https://deno.land/std@0.76.0/http/server.ts"; +export { parse, print } from "https://x.nest.land/swc@0.0.6/mod.ts"; \ No newline at end of file diff --git a/doc/PATTERN_COMPONENT.md b/doc/PATTERN_COMPONENT.md new file mode 100644 index 0000000..b03138b --- /dev/null +++ b/doc/PATTERN_COMPONENT.md @@ -0,0 +1,97 @@ +# Eon Component Declaration's pattern +Eon's reactivity would be quiet inspired by Svelte's Reactivity + +the idea is to use vars (from the template provided by the end user), and to assign value in two time. +for this we would use two functions `component_ctx` and `init`. +the first one contains the second one. + +```js +function component_ctx() { + // element's vars declarations here like: let tmp, n; + // ... + function init() { + // element assignment here + } + return { + init, + }; +} +customElement.define('[component-uuid]', class extends HTMLElement { + connectedCallback() { + super(); + const ctx = component_ctx(); + const template = ctx.init(); + } +}) +``` +# TODO: more documentations here + +```js +import { VMC } from '../[path_to_component].ts'; + +function component_ctx() { + // tmp is a template element + // n3 is a div element + // t4 is a boundtext textnode + let tmp1, + n2, + n3, + t4, + /* boundtextnode with end user's input */ + t4_data_update; + t4_data_prev, + t4_data_next + /* c5 is a component */ + c5, + c5_props_update, + /* and then the component */ + component = new VMC(); + + /* will assign all the nodes inside vars*/ + function init() { + tmp1 = document.createElement('template'); + n3 = document.createElement('div'); + c5 = document.createElement('data-[uuid_sub_component]'); + t4 = new Text(' '); + t4_data_update = () => this.message; + /* using the component's props attribute value */ + c5_props_update = () => ({ + message: this.message + }); + // append childs + tmp1.append(n3); + n3.append(t4); + n3.append(c5); + // TODO attributes and bound attributes + // return the template + return tmp1; + } + /* general updates */ + function update() { + t4_data_next = t4_data_update(component); + if (t4_data2_prev !== t4_data_next) { + t4.data = t4_data_next; + t4_data2_prev = t4_data_next; + } + /* using the component's props attribute value */ + c5.props(c5_props_update(component)); + } + return { + component, + init: init.bind(component), + update: update.bind(component) + } +} +customElement.define('data-[uuid_component]', class extends HTMLElement { + constructor() { + super(); + const { init, update, component } = component_ctx(); + let template = init(); + let templateContent = template.content; + this.component = component; + + const shadowRoot = this.attachShadow({mode: 'open'}) + .appendChild(templateContent.cloneNode(true)); + } +}); +``` \ No newline at end of file diff --git a/doc/PATTERN_FOR_STATEMENT.md b/doc/PATTERN_FOR_STATEMENT.md new file mode 100644 index 0000000..d4162f2 --- /dev/null +++ b/doc/PATTERN_FOR_STATEMENT.md @@ -0,0 +1,45 @@ +```ts +function component_ctx() { + let wrapper1; // wrapper of all the elements rendered + function init() { + // template creation + wrapper1 = document.createElement('eon-list'); + // a section has no style applied + render_iteration1(); + tmp1.append(wrapper1); + return tmp1; + } + function render_iteration1() { + // start iterations rendering + // usign pattern 'for_directive.ts' + // @ts-nocheck + let arr_uuid_element = this.array, i = 0; + for (const number of arr_uuid_element) { + i = arr_uuid_element.indexOf(number); + // add missing elements + if (i > wrapper1.childNodes.length) { + let n1; + wrapper1.append(n1); + n1.setAttribute('test', 'true'); + "{{ childs_add_event_listener }}" + // update elements + "{{ childs_update }}" + } else { + // need to get the corresponding element + "{{ childs_reassignment }}" + // update elements + "{{ childs_update }}" + } + } + // remove extra elements + if (i < wrapper1.childNodes.length) { + for (let i_remove = wrapper1.childNodes.length; i < i_remove; i_remove--) { + wrapper1.childNodes[i_remove].remove(); + } + } + } + function update() { + render_iteration1(); + } +} +``` \ No newline at end of file diff --git a/examples/hello-app/AnotherComponent.tsx b/examples/hello-app/AnotherComponent.tsx new file mode 100644 index 0000000..e95d076 --- /dev/null +++ b/examples/hello-app/AnotherComponent.tsx @@ -0,0 +1,20 @@ +// @ts-ignore +export type AnProps = EonProps<{ test: string; }>; +export default function (this: VMC, props: AnProps) { + return () +} +export class VMC { + public message = 'Hello World overwritten'; + public test: string = 'test'; + static props(this: VMC, props: AnProps) { + this.test = props.test as string; + } + static updated(this: VMC) { + this.message = 'Im updated'; + } +} \ No newline at end of file diff --git a/examples/hello-app/HelloApp.tsx b/examples/hello-app/HelloApp.tsx index a71a610..b28e690 100644 --- a/examples/hello-app/HelloApp.tsx +++ b/examples/hello-app/HelloApp.tsx @@ -1,23 +1,45 @@ -import * as HelloApp2 from './HelloApp2.jsx'; -import * as HelloApp3 from './HelloApp2.jsx'; -import * as HelloApp4 from './HelloApp2.jsx'; - -export const name = "AppHello"; -export default function(this: ViewModel): JSX.Element { - // what to do about all the things referenced here - // maybe it's a helper zone for SSR - // but what about SPA - // keep in mind, this function is just to do the dom tree - return (<> - - - - ) -} -export class ViewModel { - public message = "Hello World"; +import AnotherComponent, { VMC as AnVMC } from "./AnotherComponent.tsx"; +export default function (this: VMC) { + return () } + + +export class VMC extends AnVMC { + public message: string = "Hello World"; + public array: number[] = [0]; + public newData = { + test: { + message: 'string' + } + }; + + static connected(this: VMC) { + let i = 0; + setInterval(() => { + this.newData.test.message = `${i} test deep reactivity`; + i++; + }, 50); + } + + public switchText() { + this.message = 'test'; + } +} \ No newline at end of file diff --git a/examples/hello-app/HelloApp2.jsx b/examples/hello-app/HelloApp2.jsx deleted file mode 100644 index 94d946c..0000000 --- a/examples/hello-app/HelloApp2.jsx +++ /dev/null @@ -1,19 +0,0 @@ -export const name = "AppHello"; -export default function() { - return (<> - - - ) -} -export class ViewModel { - message = "Hello World"; - array = [1, 3]; - static props(props) { - return props - } -} diff --git a/examples/hello-app/ListForeach.tsx b/examples/hello-app/ListForeach.tsx deleted file mode 100644 index ae47679..0000000 --- a/examples/hello-app/ListForeach.tsx +++ /dev/null @@ -1,20 +0,0 @@ -export const name = "AppHello"; -export default function(this: ViewModel, Directive: any) { - return (<> - - - ) -} -export class ViewModel { - public message = "Hello World"; - public array :number[] = [1, 3]; - static props(props: { name: string }) { - return props - } -} diff --git a/examples/hello-app/mod.ts b/examples/hello-app/mod.ts new file mode 100644 index 0000000..9e970e9 --- /dev/null +++ b/examples/hello-app/mod.ts @@ -0,0 +1,3 @@ +import { EonApplication } from '../../mod.ts'; + +await EonApplication.dev('./examples/hello-app/HelloApp.tsx'); \ No newline at end of file diff --git a/examples/ideas/design-prototype.md b/examples/ideas/design-prototype.md new file mode 100644 index 0000000..6026f10 --- /dev/null +++ b/examples/ideas/design-prototype.md @@ -0,0 +1,32 @@ +```tsx +import TodoListRow, { Todo } from '../todo-list/TodoListRow.tsx'; + +export interface DesignPrototypeProps { message: string } +export interface DesignPrototype { + list: Todo[]; + props: DesignPrototypeProps +} + +export function connected(this: DesignPrototype) { + this.list.push({ + active: false, + value: 'test from design-prototype', + issues: [], + }); +} + +export default function(this: DesignPrototype, props: DesignPrototypeProps) { + this.list = []; + this.props = props; + return () +} +``` \ No newline at end of file diff --git a/examples/todo-list/ThemeTodoList.ts b/examples/todo-list/ThemeTodoList.ts new file mode 100644 index 0000000..510ecd9 --- /dev/null +++ b/examples/todo-list/ThemeTodoList.ts @@ -0,0 +1,7 @@ +export default { + grey: '#333', + lightGrey: '#f8f8ff', + border: '#d0d0d0', + green: '#d0fdd0', + inactive: '#fdd0d0', +}; \ No newline at end of file diff --git a/examples/todo-list/TodoListApp.tsx b/examples/todo-list/TodoListApp.tsx new file mode 100644 index 0000000..3e8fe62 --- /dev/null +++ b/examples/todo-list/TodoListApp.tsx @@ -0,0 +1,84 @@ +import TodoListForm from './TodoListForm.tsx'; +import TodoListRow from './TodoListRow.tsx'; +import type { Todo } from './TodoListRow.tsx'; +import ThemeTodoList from './ThemeTodoList.ts'; + +export default function(this: VMC) { + return () +} +export class VMC { + ThemeTodoList = ThemeTodoList; + list: Todo[] = [ + { + value: 'test', + active: true, + issues: [1, 2, 3] + }, + { + value: 'test2', + active: false, + issues: [0], + }, + ]; + static connected(this: VMC) { + setInterval(() => { + this.ThemeTodoList.grey = 'red'; + this.list.push({ + active: Math.random() > 0.5, + value: 'test3', + issues: [], + }) + // setTimeout(() => this.list.splice(0), 200); + }, 2000); + } +} \ No newline at end of file diff --git a/examples/todo-list/TodoListForm.tsx b/examples/todo-list/TodoListForm.tsx new file mode 100644 index 0000000..75d2d73 --- /dev/null +++ b/examples/todo-list/TodoListForm.tsx @@ -0,0 +1,14 @@ +export default function(this: VMC) { + return () +} +export class VMC { + value = ''; + static connected(this: VMC) { + this.value = 'placeholder'; + } +} \ No newline at end of file diff --git a/examples/todo-list/TodoListRow.tsx b/examples/todo-list/TodoListRow.tsx new file mode 100644 index 0000000..0e24e24 --- /dev/null +++ b/examples/todo-list/TodoListRow.tsx @@ -0,0 +1,46 @@ +import ThemeTodoList from './ThemeTodoList.ts'; + +export interface Todo { + value: string; + active: boolean; + issues: number[]; +} +export type Props = EonProps<{ todo: Todo }>; +export default function(this: VMC, props: Props) { + return () +} +export class VMC { + ThemeTodoList = ThemeTodoList; + todo: Todo = { + value: 'default', + active: true, + issues: [], + }; + static props(this: VMC, props: Props) { + this.todo.value = (props.todo as Todo).value; + this.todo.active = (props.todo as Todo).active; + } +} \ No newline at end of file diff --git a/examples/todo-list/mod.ts b/examples/todo-list/mod.ts new file mode 100644 index 0000000..8d28859 --- /dev/null +++ b/examples/todo-list/mod.ts @@ -0,0 +1,3 @@ +import { EonApplication } from '../../mod.ts'; + +await EonApplication.dev('./examples/todo-list/TodoListApp.tsx'); \ No newline at end of file diff --git a/mod.ts b/mod.ts index 7868d99..7c1e6a6 100644 --- a/mod.ts +++ b/mod.ts @@ -1,6 +1,44 @@ -import { ModuleGetter } from './src/classes/ModuleGetter.ts'; +import ModuleResolver from './src/classes/ModuleResolver.ts'; +import './src/functions/jsxFactory.ts'; +import type { ModuleGetterOptions } from './src/classes/ModuleGetterOptions.ts'; +import EonComponent from './src/classes/EonComponent.ts'; +import DevServer from './src/classes/DevServer.ts'; +import EonSandBox from './src/classes/EonSandBox/EonSandBox.ts'; +import { path } from './deps.ts'; -const component = await ModuleGetter.buildModule({ - entrypoint: './examples/hello-app/HelloApp.tsx', -}); -console.warn(component); +export class EonApplication { + static async getComponents(opts: ModuleGetterOptions): Promise { + await EonSandBox.startSession(); + const documents = await EonSandBox.renderSession(); + const rootComponentPath = path.join(Deno.cwd(), opts.entrypoint); + const components: EonComponent[] = []; + for (let document of documents) { + const component = await ModuleResolver.resolve(document.module, opts, document.sourcePath === rootComponentPath); + component.sourcePath = document.sourcePath; + component.file = document.importable; + component.sandBoxPath = document.sandBoxPath; + components.push(component); + } + return components; + } + static async mount(component: EonComponent): Promise { + const isSaved: boolean = await ModuleResolver.setComponentTemplate(component); + return isSaved; + } + /** + * start development of the application + * @param root {string} path to the root component + * @param registry {string} all the components used in the application + */ + static async dev(root: string): Promise { + const components = await EonApplication.getComponents({ + entrypoint: root, + }); + // EonApplication.mount will set the template of the component + for await (const component of components) { + await EonApplication.mount(component); + } + await DevServer.serveSPA(); + return components; + } +} \ No newline at end of file diff --git a/src/classes/DOMElement/DOMElement.ts b/src/classes/DOMElement/DOMElement.ts new file mode 100644 index 0000000..2e9dd65 --- /dev/null +++ b/src/classes/DOMElement/DOMElement.ts @@ -0,0 +1,19 @@ +import DOMElementObject, { DOMElementInterface } from './DOMElementObject.ts'; +import DOMElementSPA from "./DOMElementSPA.ts"; +import DOMElementRegistry from '../DOMElementRegistry.ts'; + +/** + * class that participate to the DOM Tree description + */ +export default class DOMElement extends DOMElementSPA { + constructor(opts: DOMElementInterface) { + super(opts); + DOMElementRegistry.subscribe(this.uuid, this); + } + setParent(parent: DOMElement) { + this.parent = parent; + } + setChild(child: DOMElement) { + this.children.push(child); + } +} \ No newline at end of file diff --git a/src/classes/DOMElement/DOMElementObject.ts b/src/classes/DOMElement/DOMElementObject.ts new file mode 100644 index 0000000..dcc87e0 --- /dev/null +++ b/src/classes/DOMElement/DOMElementObject.ts @@ -0,0 +1,190 @@ +import Utils from '../Utils.ts'; +import EonComponent from '../EonComponent.ts'; +import { increment } from '../../functions/increment.ts'; +import type { DOMElementDescription } from '../DOMElementDescriber.ts'; +import DOMElement from "./DOMElement.ts"; +/** + * base class of DOMElement/DOMElementSPA + */ + +export type DOMTreeElement = DOMElementObject; +export interface DOMElementInterface { + /** the parent element of the element, undefined if the element is on top */ + parent?: DOMTreeElement; + /** the children of the element */ + children: DOMElement[]; + /** the name of the element */ + name?: string; + /** the value of the element, defined if it's a textnode */ + value?: unknown; + /** the type of the element + * 1 for all elements including the fragments + * 2 for attributes + * 3 for textnodes + * 11 for fragments + */ + nodeType?: 1 | 2 | 3 | 11; + /** + * the element is a template and on top of the dom + * or direct child of the top fragment + */ + isTemplate?: boolean; + /** + * the element is a style element and on top of the dom + * or direct child of the top fragment + */ + isStyle?: boolean; + /** the element is on top and it's a fragment element */ + isFragment?: boolean; + /** the attributes of the element */ + attributes?: { [k: string]: unknown }; + /** related component */ + component?: EonComponent; + /** whenever the end user uses an arrow function with three parameters + * and the last one has an assignment. + * this is only parsed when the arrow function is an element's child + */ + isArrowIterationFunction?: DOMElementDescription; + /** + * to sort the element, we need to assign them a date, using Date.now() + */ + date?: number; +} +export default class DOMElementObject extends Utils implements DOMElementInterface { + parent: DOMElementInterface['parent']; + children: DOMElementInterface['children']; + name: DOMElementInterface['name']; + nodeType: DOMElementInterface['nodeType']; + value: DOMElementInterface['value']; + attributes: DOMElementInterface['attributes']; + component: DOMElementInterface['component']; + isArrowIterationFunction: DOMElementInterface['isArrowIterationFunction']; + date: DOMElementInterface['date']; + id?: number; + constructor(opts: DOMElementInterface) { + super(); + const { + nodeType, + parent, + name, + children, + value, + attributes, + component, + isArrowIterationFunction, + date = 0, + } = opts; + this.nodeType = nodeType; + this.parent = parent; + this.name = name; + this.children = children; + this.value = value; + this.attributes = attributes; + this.component = component; + this.id = increment(); + this.isArrowIterationFunction = isArrowIterationFunction; + this.date = date || performance.now(); + } + get uuid(): string { + const idType = this.isBoundTextnode ? 'bt' + : this.isTemplate ? 'tmp' + : this.isComponent ? 'c' + : this.isArrowIterationFunction ? 'lp' + : this.nodeType === 3 ? 't' + : this.nodeType === 2 ? 'a' + : 'n'; + return `${idType}${this.id}`; + } + get isTextnode(): boolean { + return this.nodeType === 3 && !this.isBoundTextnode; + } + get isBoundTextnode(): boolean { + return this.nodeType === 3 && typeof this.value === 'function'; + } + get isBoundAttribute(): boolean { + return this.nodeType === 2 && typeof this.value === 'function'; + } + get isTemplate(): boolean { + return this.nodeType === 1 && this.name === 'template' && (!this.parent || this.parent.isFragment); + } + get isStyle(): boolean { + return this.nodeType === 1 && this.name === 'style' && (!this.parent || this.parent.isFragment); + } + get isFragment(): boolean { + return this.nodeType === 11 && this.name === undefined && !this.parent; + } + get isComponent(): boolean { + return this.nodeType === 1 && !!this.component && !this.isTemplate; + } + get isInSVG(): boolean { + let result = this.name === 'svg'; + let parent = this.parent; + while (parent && !result) { + if (parent && parent.name === 'svg' || this.name === 'svg') { + result = true; + break; + } + parent = parent?.parent; + } + return result; + } + /** + * if the element is used inside a iteration, + * the rendering should be delayed, + * true if one of the ancestors element is a directive + */ + get isInArrowIteration(): boolean { + let result = false; + let parent = this.parent; + while (parent) { + if (parent && parent.isArrowIterationFunction) { + result = true; + break; + } + parent = parent?.parent; + } + return result; + } + /** returns the component that is using this element */ + get parentComponent(): EonComponent | undefined { + let parent = this.parent; + while (parent) { + if (parent?.parent) { + parent = parent?.parent; + } else { + break; + } + } + return (parent || this).component; + } + /** + * returns all the descendants domelement + */ + get descendants(): DOMElementObject[] { + const descendants: DOMElementObject[] = []; + function recursive_children(children: DOMElementObject[]) { + children.forEach((d) => { + descendants.push(d); + recursive_children(d.children); + }); + } + recursive_children(this.children); + return descendants; + } + /** + * returns all the descendants domelement until a new context is reached (ctx like a for directive) + */ + get descendantsUntilNewContext(): DOMElementObject[] { + const descendants: DOMElementObject[] = []; + function recursive_children(children: DOMElementObject[]) { + children.forEach((d) => { + if (!d.isArrowIterationFunction) { + descendants.push(d); + recursive_children(d.children); + } + }); + } + recursive_children(this.children); + return descendants; + } +} \ No newline at end of file diff --git a/src/classes/DOMElement/DOMElementRenderer.ts b/src/classes/DOMElement/DOMElementRenderer.ts new file mode 100644 index 0000000..e36e06b --- /dev/null +++ b/src/classes/DOMElement/DOMElementRenderer.ts @@ -0,0 +1,88 @@ +import EonComponent from '../EonComponent.ts'; +import DOMElement from './DOMElement.ts'; +/** + * class to get all rendering of elements + * should differ if it's for SPA/SSR/SSG + */ +export default class DOMElementRenderer { + protected static registry: Map = new Map(); + static get collection(): DOMElement[] { + return Array.from(this.registry.entries()) + .map(([key, domelement]) => domelement) + /** + * need to sort by the assigned date because some domelements are instantiated before the precedent domelement + */ + .sort((a: DOMElement, b: DOMElement) => a.date !== undefined && b.date !== undefined && a.date - b.date || 1); + } + public static getVarsSPA(component: EonComponent) { + const collection = DOMElementRenderer.getElementsOfComponent(component.uuid as string); + const vars = collection + .filter((domelement) => domelement + && !domelement.isInArrowIteration + && domelement.declarationSPA) + .map((domelement) => domelement.declarationSPA); + return `let ${vars.join(',\n')};`; + } + public static getAssignementsSPA(component: EonComponent) { + const collection = DOMElementRenderer.getElementsOfComponent(component.uuid as string); + const assignements = collection + .filter((domelement) => domelement + && !domelement.isInArrowIteration + && domelement.assignementSPA) + .map((domelement) => domelement.assignementSPA); + return assignements.join('\n'); + } + public static getAppendChildsSPA(component: EonComponent) { + const collection = DOMElementRenderer.getElementsOfComponent(component.uuid as string); + const appends = collection + .filter((domelement) => domelement + && !domelement.isInArrowIteration + && domelement.appendChildSPA) + .map((domelement) => domelement.appendChildSPA); + return appends.join('\n'); + } + public static getUpdatesSPA(component: EonComponent) { + const collection = DOMElementRenderer.getElementsOfComponent(component.uuid as string); + const updates = collection + .filter((domelement) => domelement + && !domelement.isInArrowIteration + && domelement.updateSPA) + .map((domelement) => domelement.updateSPA); + return updates.join('\n'); + } + public static getReturnTemplateSPA(component: EonComponent) { + const collection = DOMElementRenderer.getElementsOfComponent(component.uuid as string); + const result = collection + .filter((domelement) => domelement + && !domelement.isInArrowIteration + && domelement.returnTemplateStatementSPA) + .map((domelement) => domelement.returnTemplateStatementSPA); + return result.join('\n'); + } + public static getIterationsDeclarationsSPA(component: EonComponent) { + const collection = DOMElementRenderer.getElementsOfComponent(component.uuid as string); + const result = collection + .filter((domelement) => domelement + && !domelement.isInArrowIteration + && domelement.isArrowIterationFunction + && domelement.iterationDeclaration) + .map((domelement) => domelement.iterationDeclaration); + return result.join('\n'); + } + public static getIterationsCallSPA(component: EonComponent) { + const collection = DOMElementRenderer.getElementsOfComponent(component.uuid as string); + const result = collection + .filter((domelement) => domelement + && !domelement.isInArrowIteration + && domelement.isArrowIterationFunction + && domelement.iterationCall) + .map((domelement) => domelement.iterationCall); + return result.join('\n'); + } + static getElementsByNodeType(nodeType: number) { + return this.collection.filter((domelement) => domelement && domelement.nodeType === nodeType); + } + static getElementsOfComponent(uuid: string): DOMElement[] { + return this.collection.filter((domelement) => domelement && domelement.parentComponent && domelement.parentComponent.uuid === uuid); + } +} \ No newline at end of file diff --git a/src/classes/DOMElement/DOMElementSPA.ts b/src/classes/DOMElement/DOMElementSPA.ts new file mode 100644 index 0000000..2212be4 --- /dev/null +++ b/src/classes/DOMElement/DOMElementSPA.ts @@ -0,0 +1,238 @@ +import type { DOMElementDescription } from '../DOMElementDescriber.ts'; +import Patterns from "../Patterns.ts"; +import Utils from '../Utils.ts'; +import DOMElementObject, { DOMElementInterface } from './DOMElementObject.ts'; + +/** + * class to get all Single Page Application's utils + * related to the DOMElementObject + */ +export default class DOMElementSPA extends DOMElementObject { + constructor(opts: DOMElementInterface) { + super(opts); + } + get declarationSPA(): string | undefined { + if (this.isBoundAttribute) { + /** + * the DOMElementObject is a dynamic attribute + */ + return `${this.uuid}, ${this.uuid}_prev, ${this.uuid}_next` + } + if (this.isBoundTextnode) { + /** + * if the element is a bound textnode + * it should use as vars + * one for the textnode element: new Text(' ') + * one for the previous value + * one for the next value + * one for the update function + */ + return `${this.uuid}, ${this.uuid}_prev, ${this.uuid}_update, ${this.uuid}_next`; + } + if (this.isComponent) { + /** + * if the element is a component + * it should use as vars + * one for the component element: document.createElement('my-component') + * one for props function + */ + return `${this.uuid}, ${this.uuid}_props`; + } + if (this.nodeType && [11, 2].includes(this.nodeType)) { + return undefined; + } + return this.uuid; + } + get assignementSPA(): string | undefined { + if (this.isBoundAttribute) { + /** + * set the function for the attribute + */ + return `${this.uuid} = (${(this.value as Function).toString()});` + } + if (this.isBoundTextnode) { + /** + * if the element is a bound textnode + * it should use as vars + * one for the textnode element: new Text(' ') + * one for the previous value + * one for the next value + * one for the update function + */ + return `${this.uuid} = new Text(' '); + ${this.uuid}_update = (${this.value}); + ${this.uuid}.data = ${this.uuid}_update();`; + } + if (this.isTextnode) { + return `${this.uuid} = \`${this.value}\``; + } + if (this.isComponent && this.component) { + /** + * this allows the props to be evaluated when the initialization is done + */ + const directEvaluatedProps = this.children + .filter((d) => d.isBoundAttribute) + .map((domelement) => `${domelement.name}: ${domelement.uuid}(component),`) + .join('\n') + /** + * if the element is a component + * it should use as vars + * one for the component element: document.createElement('my-component') + * one for props function + */ + return `${this.uuid} = crt('${this.component.dataUuidForSPA}'); + ${this.uuid}_props = { + ${directEvaluatedProps} + }; + `; + } + if (this.isArrowIterationFunction) { + const { + wrapperName = 'eon-list' + } = this.isArrowIterationFunction; + /** + * the eon-list element will wrap the list + * this allows a better list management + */ + return `${this.uuid} = crt('${wrapperName}', ${this.isInSVG});`; + } + if (this.nodeType && [11, 2].includes(this.nodeType)) { + return undefined; + } + return `${this.uuid} = crt('${this.name}', ${this.isInSVG});`; + } + get appendChildSPA(): string | undefined { + if (this.nodeType && [11].includes(this.nodeType)) { + return undefined; + } + if (this.isBoundAttribute && !this.parent?.isComponent) { + /** + * set the attribute at the init using the function to get the value + */ + return `${this.uuid}_prev = ${this.uuid}(component); att(${(this.parent as DOMElementObject).uuid}, '${this.name}', ${this.uuid}_prev);`; + } + if (this.isBoundAttribute && this.parent?.isComponent) { + const parentUuid = this.parent.uuid; + /** + * set the attribute at the init using the function to get the value + */ + return `${this.uuid}_prev = ${this.uuid}(component); + if (${parentUuid}_props) { + ${parentUuid}_props['${this.name}'] = ${this.uuid}_prev; + ${parentUuid} && ${parentUuid}.props && ${parentUuid}.props(${parentUuid}_props); + }`; + } + if (this.nodeType === 2 && this.parent && !this.parent.isTemplate) { + return `att(${this.parent.uuid}, '${this.name}', '${this.value}');` + } + if (this.parent && this.parent.parent || this.parent && this.parent.isTemplate) { + return `app(${this.parent.uuid}, ${this.uuid});`; + } + } + get updateSPA(): string | undefined { + if (this.isBoundAttribute && !this.parent?.isComponent) { + /** + * set the attribute at the init using the function to get the value + */ + return ` + ${this.uuid}_next = ${this.uuid}(component); + if (${this.uuid}_prev !== ${this.uuid}_next) { + att(${(this.parent as DOMElementObject).uuid}, '${this.name}', ${this.uuid}_next); + ${this.uuid}_prev = ${this.uuid}_next; + }` + } + if (this.isBoundAttribute && this.parent?.isComponent) { + const parentUuid = this.parent.uuid; + /** + * set the new value + * compare it to the previous value + */ + return ` + ${this.uuid}_next = ${this.uuid}(component); + if (${this.uuid}_prev !== ${this.uuid}_next) { + ${parentUuid}_props['${this.name}'] = ${this.uuid}_next; + ${this.uuid}_prev = ${this.uuid}_next; + }` + } + if (this.isBoundTextnode) { + /** + * the element is dynamic textnode + * it should check the new value before updating + */ + return `${this.uuid}_next = ${this.uuid}_update(component); + if (${this.uuid}_prev !== ${this.uuid}_next) { + ${this.uuid}.data = ${this.uuid}_next; + ${this.uuid}_prev = ${this.uuid}_next; + }`; + } + if (this.isComponent) { + return `${this.uuid} && ${this.uuid}.props && ${this.uuid}.props(${this.uuid}_props);` + } + } + get returnTemplateStatementSPA() { + if (this.isTemplate) { + return `return ${this.uuid};`; + } + } + get renderIterationFunctionNameSPA() { + return `render_iteration_${this.uuid}`; + } + get renderIterationSubscriberSPA() { + return `render_iteration_subscriber_${this.uuid}`; + } + get iterationCall() { + return `${this.renderIterationFunctionNameSPA}();`; + } + get iterationBodySPA() { + const infos = this.isArrowIterationFunction; + const descendants = this.descendantsUntilNewContext as DOMElementSPA[]; + if (!infos || !descendants.length) return ''; + let childs_declarations = `let ${descendants + .filter((domelement) => domelement.declarationSPA) + .map((domelement) => domelement.declarationSPA) + .join(',\n')};`; + let childs_appends = descendants + .filter((domelement) => domelement.appendChildSPA) + .map((domelement) => domelement.appendChildSPA) + .join('\n'); + let childs_assignments = descendants.slice() + .sort((b, a) => a && b && a.date && b.date && a.date - b.date || -1) + .map((domelement) => domelement.assignementSPA) + .join('\n'); + let childs_update = descendants.slice() + .sort((a, b) => a && b && a.date && b.date && a.date - b.date || -1) + .map((domelement) => domelement.updateSPA) + .join('\n') + const body = Utils.renderPattern(Patterns.forDirectivePattern, { + data: { + array_value: infos.arrayValue, + element_index: infos.index, + element_name: infos.currentValue, + element_array_name: infos.array, + element_wrapper: this.uuid, + removal_index: `${this.uuid}_rm`, + wrapper_update_subscriber: this.renderIterationSubscriberSPA, + childs_declarations, + childs_appends, + childs_assignments, + childs_update, + childs_set_attributes: '', + childs_add_event_listener: '', + } + }); + return body; + } + getReassignmentFromArraySPA(infos: DOMElementDescription): string { + if (this.nodeType === 1) { + return `${this.uuid} = ${infos.array}[${infos.index}];` + } + return ''; + } + get iterationDeclaration() { + return ` + const ${this.renderIterationSubscriberSPA} = []; + const ${this.renderIterationFunctionNameSPA} = (function() { + ${this.iterationBodySPA} + }).bind(component)`; + } +} \ No newline at end of file diff --git a/src/classes/DOMElement/README.md b/src/classes/DOMElement/README.md new file mode 100644 index 0000000..423dd4e --- /dev/null +++ b/src/classes/DOMElement/README.md @@ -0,0 +1,16 @@ +# Architecture + +```ts +class DOMElement extends DOMElementSPA extends DOMElementObject +``` + +DOMElementRenderer uses DOMelement + +# DOMElement +main class to describe the Document Object Model used inside the component by the end user. + +# DOMElementSPA +this class contains all the methods, properties used to render the Single Page Application. + +# DOMElementObject +this is the base class extended by DOMElement/DOMElementSPA \ No newline at end of file diff --git a/src/classes/DOMElement/TODO.md b/src/classes/DOMElement/TODO.md new file mode 100644 index 0000000..ca75d76 --- /dev/null +++ b/src/classes/DOMElement/TODO.md @@ -0,0 +1,4 @@ +- [ ] add DOMElementSSR +- [ ] add DOMElementPSSR +- [ ] add DOMElementPCR +- [ ] add getInnerHTML/ getOuterHTML \ No newline at end of file diff --git a/src/classes/DOMElementDescriber.ts b/src/classes/DOMElementDescriber.ts new file mode 100644 index 0000000..672f7a2 --- /dev/null +++ b/src/classes/DOMElementDescriber.ts @@ -0,0 +1,89 @@ +import Utils from './Utils.ts'; +import { parse, print } from '../../deps.ts'; +/** + * result of `DOMElementDescriber.getArrowFunctionDescription` + * the function is recognized as an arrow function with three parameters + * and the last one has a default value + */ +export interface DOMElementDescription { + /** + * the index used for the iteration + */ + index: string; + /** + * the iteration's currentValue name + */ + currentValue: string; + /** + * the name of the array + */ + array: string; + /** + * the value assigned to the array by the end user + */ + arrayValue: string; + /** + * tagname of the wrapper + */ + wrapperName?: string; +} +/** + * a class to parse special directives + * this class uses deno_swc parse function + * + */ +export default class DOMElementDescriber extends Utils { + static swcOptions: Parameters[1] = { + syntax: "typescript" + }; + /** + * only uses arrow functions, + * need to know if the function is an iteration, + * typically this happens when the function has three parameters, + * first is the currentValue, + * second is the index, + * third is an array with an assignment, + * example: `(number, i, array = [0, 1]) =>
{number}
` + * @param value {() => JSX.Element} + */ + static getArrowFunctionDescription(value: (currentValue: unknown, index: number, array: unknown[], ...rest: any[]) => unknown): DOMElementDescription | null { + if (!value) return null; + const func = value.toString().trim(); + // only accept arrow functions + if (!func.trim().startsWith('(')) return null; + const ast = parse(func, DOMElementDescriber.swcOptions); + if (ast + && ast.body + && ast.body.length + && ast.body[0] + // @ts-ignore + && ast.body[0].expression + // @ts-ignore + && ast.body[0].expression.type === "ArrowFunctionExpression") { + // @ts-ignore + const { params } = ast.body[0].expression + // @ts-ignore + const [currentValueInfos, indexInfos, arrayInfos] = params; + if (currentValueInfos + && indexInfos + && arrayInfos + && indexInfos.type === "Identifier" + && arrayInfos.type === "AssignmentPattern") { + let elementName = func.slice( + currentValueInfos.span.start - ast.span.start, + currentValueInfos.span.end - ast.span.start, + ) + return { + index: indexInfos.value, + currentValue: elementName, + array: arrayInfos.left.value, + arrayValue: func.slice( + arrayInfos.right.span.start - ast.span.start, + arrayInfos.right.span.end - ast.span.start, + ), + }; + } + } + return null; + } +} \ No newline at end of file diff --git a/src/classes/DOMElementRegistry.ts b/src/classes/DOMElementRegistry.ts new file mode 100644 index 0000000..f8c296c --- /dev/null +++ b/src/classes/DOMElementRegistry.ts @@ -0,0 +1,12 @@ +import DOMElement from './DOMElement/DOMElement.ts'; +import DOMElementRenderer from './DOMElement/DOMElementRenderer.ts'; +/** + * class to save all the element used + */ +export default class DOMElementRegistry extends DOMElementRenderer { + public static subscribe(uuid: string, domelement: DOMElement) { + if (!this.registry.has(uuid)) { + this.registry.set(uuid, domelement); + } + } +} \ No newline at end of file diff --git a/src/classes/DevBundler.ts b/src/classes/DevBundler.ts new file mode 100644 index 0000000..eb89808 --- /dev/null +++ b/src/classes/DevBundler.ts @@ -0,0 +1,138 @@ +import Utils from './Utils.ts'; +import EonComponentRegistry from './EonComponentRegistry.ts'; +import EonComponent from './EonComponent.ts'; +import { ModuleErrors } from './ModuleErrors.ts'; +import DOMElementRegistry from './DOMElementRegistry.ts'; +import Patterns from './Patterns.ts'; +import { v4 } from '../../deps.ts'; +import EonSandBox from './EonSandBox/EonSandBox.ts'; + + +type EonApplication = { + /** + * script part of the page, + * outerHTML of a script element, + * containing the main script of the application + */ + script: string; + /** + * global style of the application, + * outerHTML of a style element + */ + style: string; + /** + * only contains the tag of the root-component + */ + body: string; + /** + * the string sent to the client, + * contains the page of the application + */ + dom: string; +}; + +export default class DevBundler extends Utils { + /** + * start building the application + */ + protected static async buildApplicationSPA(): Promise { + const rootComponent: EonComponent | undefined = EonComponentRegistry.getRootComponent(); + if (!rootComponent) { + ModuleErrors.error('root component not found'); + return null; + } + const emit = await this.buildScriptSPA(); + const script = ``; + const style = ''; + const body = `<${rootComponent.dataUuidForSPA}>`; + const dom = ` + + + ${style} + + + ${body} + ${script} + + `; + return { + script, + style, + body, + dom + }; + } + protected static async buildScriptSPA(): Promise { + let app: string = `/** Eon application compiled */ + import { reactive, crt, app, att, add } from '${new URL('../functions/runtime.ts', import.meta.url).pathname}'; + `; + const files: string[] = []; + // first get all available components + const components: EonComponent[] = EonComponentRegistry.collection.map(([key, component]) => component); + const appPath = `${ v4.generate()}.ts`; + // create a string for each components + // the string should return a Eon Component Declaration Pattern + // file://doc/PATTERN_COMPONENT.md + components + .slice() + /** + * this avoid rootComponent to be rendered before depencies + * without reverse nested components will be the last components + */ + .reverse() + .forEach((component: EonComponent, i: number) => { + if (component.file && component.isImported) { + const newPath = `${component.uuid}.ts`; + const vmcName = `VMC${i}______${i}`; + // save the new string into the files used by Deno.bundle + const file = this.createMirrorEsmFile(component, vmcName); + const esmSandBoxPath = EonSandBox.addFile(newPath, file); + files.push(esmSandBoxPath); + if (component.VMC) { + app += `\n/** Eon harmony import */\nimport { VMC as ${vmcName} } from '${component.sourcePath}';`; + } + if (file) { + app += `\n${file}`; + } + } + }); + const appSandBoxPath = EonSandBox.addFile(appPath, app); + const [, emit] = await Deno.bundle(appSandBoxPath, undefined, { + jsx: "react", + jsxFactory: "h", + // @ts-ignore + jsxFragmentFactory: "hf", + sourceMap: false, + }); + files.push(appSandBoxPath); + files.forEach((file) => { + Deno.removeSync(file); + }); + return emit; + } + /** + * creates mirror esm files + * for each component + * uses a pattern to render the file + */ + private static createMirrorEsmFile(component: EonComponent, vmc_name: string): string { + const componentDeclartionPattern = Patterns.componentDeclaration; + return DevBundler.renderPattern(componentDeclartionPattern.replace(/\bcomponent_ctx\b/gi, `component_ctx_${vmc_name}`), { + data: { + vmc_name, + vmc_instantiate: `new ${vmc_name}()`, + uuid_component: component.dataUuidForSPA, + element_vars: DOMElementRegistry.getVarsSPA(component), + element_assignments: DOMElementRegistry.getAssignementsSPA(component), + element_parent_append_childs: DOMElementRegistry.getAppendChildsSPA(component), + return_root_template: DOMElementRegistry.getReturnTemplateSPA(component), + element_destructions: '', // DOMElementRegistry.getDestructionsSPA(component), + bound_textnodes_updates: DOMElementRegistry.getUpdatesSPA(component), + bound_attributes_updates: '', + props_updates: '', + iterations_declarations: DOMElementRegistry.getIterationsDeclarationsSPA(component), + iterations_call: DOMElementRegistry.getIterationsCallSPA(component), + } + }); + } +} \ No newline at end of file diff --git a/src/classes/DevServer.ts b/src/classes/DevServer.ts new file mode 100644 index 0000000..c770552 --- /dev/null +++ b/src/classes/DevServer.ts @@ -0,0 +1,33 @@ +import { serve } from '../../deps.ts'; +import DevBundler from './DevBundler.ts'; +import EonSandBox from "./EonSandBox/EonSandBox.ts"; +/** + * a class to serve SPA/SSR/SSG + * in development environment + */ +function random(min: number, max: number): number { + return Math.round(Math.random() * (max - min)) + min; +} +export default class DevServer extends DevBundler { + private static port: number = 3041; + private static HMRport: number = DevServer.port; + private static hostname: string = 'localhost'; + /** + * start development services for Single Page Applications + * TCP server + */ + static async serveSPA(): Promise { + const application = await DevServer.buildApplicationSPA(); + if (!application) { + return; + } + const server = serve({ hostname: this.hostname, port: this.port }); + DevServer.message(`Listening on http://${this.hostname}:${this.port}`); + setTimeout(() => { + EonSandBox.typecheckSession(); + }, 0); + for await (const req of server) { + req.respond({ body: application.dom }); + } + } +} \ No newline at end of file diff --git a/src/classes/EonComponent.ts b/src/classes/EonComponent.ts new file mode 100644 index 0000000..65215b5 --- /dev/null +++ b/src/classes/EonComponent.ts @@ -0,0 +1,79 @@ +import DOMElement from './DOMElement/DOMElement.ts'; +import type { EonModule } from './EonModule.ts'; +import EonComponentRegistry from './EonComponentRegistry.ts'; +import ModuleResolver from "./ModuleResolver.ts"; + +export interface EonComponentInterface { + /** uuid */ + uuid?: string; + /** name */ + name?: string; + /** path to the sandbox file of the component */ + file?: string; + /** path to the sandbox file of the component */ + sandBoxPath?: string; + /** path to the end user's component */ + sourcePath?: string; + /** the DOM tree of the component */ + template?: DOMElement; + /** component's VMC */ + VMC?: EonModule['VMC']; + /** returns the DOM tree of the component */ + templateFactory: EonModule['default']; + /** if the component is the first component */ + isRootComponent?: boolean; + /** if the component is imported into the application */ + isImported?: boolean; + /** + * all used components inside the current component + */ + imports?: EonComponent[]; +} +export default class EonComponent implements EonComponentInterface { + uuid: EonComponentInterface['uuid']; + name: EonComponentInterface['name']; + file: EonComponentInterface['file']; + sourcePath: EonComponentInterface['sourcePath']; + sandBoxPath: EonComponentInterface['sandBoxPath']; + template: EonComponentInterface['template']; + VMC: EonComponentInterface['VMC']; + isRootComponent: EonComponentInterface['isRootComponent'] = false; + isImported: EonComponentInterface['isImported'] = false; + templateFactory: EonComponentInterface['templateFactory']; + imports: EonComponentInterface['imports']; + constructor(opts: EonComponentInterface) { + const { + file, + uuid, + template, + VMC, + templateFactory, + name, + imports, + } = opts; + this.file = file; + this.uuid = uuid; + this.template = template; + this.VMC = VMC; + this.name = name; + this.templateFactory = templateFactory; + if (this.uuid) { + EonComponentRegistry.subscribe(this.uuid, this); + } + this.imports = imports || []; + if (ModuleResolver.currentComponent && ModuleResolver.currentComponent.imports) { + ModuleResolver.currentComponent.imports.push(this); + } + } + /** + * instead of using the name as component identifier (ex: component-name) + * we will use a pseudo uuid (ex: data-a32dsfpi1) + */ + get dataUuidForSPA(): string { + if (this.uuid) { + return `data-${this.uuid.split('-')[0]}`.toLowerCase(); + } else { + return 'no-uuid'; + } + } +} \ No newline at end of file diff --git a/src/classes/EonComponentRegistry.ts b/src/classes/EonComponentRegistry.ts new file mode 100644 index 0000000..6d89d19 --- /dev/null +++ b/src/classes/EonComponentRegistry.ts @@ -0,0 +1,45 @@ +import EonComponent from './EonComponent.ts'; + +export default abstract class EonComponentRegistry { + private static readonly registry: Map = new Map(); + static subscribe(uuid: string, component: EonComponent): boolean { + this.registry.set(uuid, component); + return true; + } + static getComponent(uuid: string): EonComponent | undefined { + return this.registry.get(uuid); + } + /** + * the module and the component are using the same function, + * so we just need to compare the value + * these are equal + */ + static getItemByTemplate(template: Function): EonComponent | undefined { + const entries = Array.from(this.registry.entries()); + const found = entries.find(([key, component]) => { + return component.templateFactory === template; + }); + if (found) { + const [, component] = found; + return component; + } + } + static getItemByUrl(url: string): EonComponent | undefined { + const entries = Array.from(this.registry.entries()); + const found = entries.find(([key, component]) => component.sourcePath === url); + if (found) { + const [, component] = found; + return component; + } + } + static get collection() { + return Array.from(this.registry.entries()) + } + static getRootComponent(): EonComponent | undefined { + const isComponent = this.collection.find(([, component]: [string, EonComponent]) => component.isRootComponent) + if (isComponent) { + const found = isComponent[1]; + return found; + } + } +} \ No newline at end of file diff --git a/src/classes/EonModule.ts b/src/classes/EonModule.ts new file mode 100644 index 0000000..30bf80a --- /dev/null +++ b/src/classes/EonModule.ts @@ -0,0 +1,18 @@ +import DOMElement from './DOMElement/DOMElement.ts'; +export interface EonModule { + /** + * when the end user provide + * `export const name = ...` + */ + name: string; + /** + * the export default is used to get the DOM of a component, + * all component should have a template element that provide the ImportMeta of the module + */ + default(props?: unknown, vm?: T): DOMElement; + /** + * ViewModelController + */ + VMC: unknown; + [k: string]: unknown; +} \ No newline at end of file diff --git a/src/classes/EonSandBox/EonSandBox.ts b/src/classes/EonSandBox/EonSandBox.ts new file mode 100644 index 0000000..1c0ea2c --- /dev/null +++ b/src/classes/EonSandBox/EonSandBox.ts @@ -0,0 +1,70 @@ +import { fs } from "../../../deps.ts"; +import EonSandBoxFileSystem from './EonSandBoxFileSystem.ts'; +import { EonSandBoxDocument } from './EonSandBoxFileSystem.ts'; + +/** + * class to build parallel folder + * the goal is to render all the modules in one time + * using transpileOnly to transform the module before fetching it + */ + +export default class EonSandBox extends EonSandBoxFileSystem { + /** + * creates a new folder eon/ at the same level of Deno.cwd() + */ + static async startSession(): Promise { + const location = this.sandBoxLocation; + if (!fs.existsSync(location)) { + Deno.mkdirSync(location); + } else { + Deno.removeSync(location, { recursive: true }); + } + const paths = fs.walkSync(this.currentLocation, { + includeDirs: true, + includeFiles: true, + skip: [/(.+?)(\b\.git|\b\.gitignore|\b\.vscode|\btsconfig\.json|\bnode_modules)(?:\/|$)/i] + }); + for (let document of paths) { + try { + const { source: sandBoxPath, importable } = this.getSandBoxMirrorPath(document.path); + // save all paths + // this is to save the files that eon is creating in the sandbox + // should be removed after the session is closed + this.paths.push(sandBoxPath); + this.paths.push(importable); + if (document.isFile) { + if (this.isJSXFile(document.path)) { + await this.saveSandBoxedFile({ + path: document.path, + sandBoxPath, + importable, + }) + } else { + // copy the file in the sandbox + Deno.copyFileSync(document.path, sandBoxPath); + } + } else if (document.isDirectory && !fs.existsSync(sandBoxPath)) { + // creates a directory in the sandbox + Deno.mkdirSync(sandBoxPath); + } + } catch (err) { + throw err; + } + } + this.addTsConfig(); + } + /** + /** + * run the session by using Deno.run + * this should be used after the eon sandbox is created + */ + static async renderSession(): Promise { + const entries = this.mapFiles.entries(); + const a = Array.from(entries); + for await (let [filePath, item] of a) { + const module = await this.getSandBoxMirrorModule(filePath); + item.module = module; + } + return a.map(([filePath, item]) => item); + } +} \ No newline at end of file diff --git a/src/classes/EonSandBox/EonSandBoxFileSystem.ts b/src/classes/EonSandBox/EonSandBoxFileSystem.ts new file mode 100644 index 0000000..bde8434 --- /dev/null +++ b/src/classes/EonSandBox/EonSandBoxFileSystem.ts @@ -0,0 +1,187 @@ +import { path, fs, colors } from "../../../deps.ts"; +import { ModuleErrors } from "../ModuleErrors.ts"; +import Utils from "../Utils.ts"; +import EonComponentRegistry from '../EonComponentRegistry.ts'; + + +export interface EonSandBoxDocument { + /** + * the content of the document in the original folder + */ + content: string; + /** + * the transpiled content of the jsx/tsx file + * this is to speed up the check step + */ + transpiled: string; + /** + * the rendered module, fetched via a dynamic import + */ + module?: any; + /** + * the path of the document in the sandBox + */ + sandBoxPath: string; + /** + * the js path of the document in the sandBox + */ + importable: string; + /** + * the original path of the document + */ + sourcePath: string; +}; +/** + * class to build parallel folder + * the goal is to render all the modules in one time + * using transpileOnly to transform the module before fetching it + */ + +export default class EonSandBoxFileSystem extends Utils { + protected static paths: string[] = []; + protected static readonly mapFiles: Map = new Map(); + protected static readonly currentLocation: string = Deno.cwd(); + protected static readonly removeOptions = { recursive: true }; + public static readonly sandBoxLocation: string = path.join(Deno.cwd(), `../.eon`); + static async saveSandBoxedFile(opts: { path: string; sandBoxPath: string; importable: string }): Promise { + const { path: pathToFile, sandBoxPath, importable } = opts; + const content = Deno.readTextFileSync(pathToFile); + const jsxFileContent = await this.getTranspiledFile({ + sandBoxPath, + content, + }); + // save the new file in the sandbox + Deno.writeTextFileSync(sandBoxPath, jsxFileContent); + this.mapFiles.set(pathToFile, { + sandBoxPath, + content, + transpiled: jsxFileContent, + importable, + sourcePath: pathToFile, + module: undefined, + }); + // save it's equivalent js file + Deno.writeTextFileSync(importable, ` + // @ts-nocheck + export * from '${sandBoxPath}'; + import Component from '${sandBoxPath}'; + export default Component; + `); + } + /** + * add a tsconfig file to the session + */ + protected static addTsConfig() { + this.addFile('/tsconfig.json', JSON.stringify({ + compilerOptions: { + jsxFactory: "h", + jsxFragmentFactory: "hf", + jsx: "react" + }, + }, null, 2)) + } + /** + * add a file to the session + */ + static addFile(p: string, text: string): string { + try { + const sandBoxPath = path.join(this.sandBoxLocation, p); + Deno.writeTextFileSync(sandBoxPath, text); + this.paths.push(sandBoxPath); + return sandBoxPath; + } catch (err) { + throw err; + } + } + static isJSXFile(p: string) { + return /\.(tsx|jsx)$/.test(p); + } + /** + * erase the Deno.cwd() from a path + * and returns the matching location in the sandBox with it's equivalent js file + */ + static getSandBoxMirrorPath(p: string): { importable: string; source: string } { + const sourcePath = p.replace(this.currentLocation, ''); + const sandBoxPath = path.join(this.sandBoxLocation, sourcePath); + return { + importable: sandBoxPath.match(/\.(tsx|jsx)$/) ? `${sandBoxPath}.js` : sandBoxPath, + source: sandBoxPath, + }; + } + /** + * returns the matching module from the sandbox + */ + static async getSandBoxMirrorModule(p: string) { + const { importable } = this.getSandBoxMirrorPath(p); + const module = await import(importable); + return module; + } + /** + * takes a source path and a sandBox Path as argument, + * injects h and hf functions as dependencies for jsxFactory + */ + static async getTranspiledFile(opts: { + content: string; + sandBoxPath: string + }): Promise { + const { sandBoxPath, content } = opts; + const hPath = new URL('../../functions/jsxFactory.ts', import.meta.url).toString(); + const newJSX = await Deno.transpileOnly({ + [sandBoxPath]: `// @ts-nocheck + import { h, hf } from '${hPath}'; + ${content}` + }, { + jsxFactory: 'h', + /** @ts-ignore */ + jsxFragmentFactory: 'hf', + jsx: 'react', + sourceMap: false, + }); + return newJSX[sandBoxPath].source; + } + /** + * type checking step for all components + */ + static async typecheckSession(): Promise { + const { gray, green, white, red } = colors; + let diagnostics: unknown[] = [] + let documents = Array.from(this.mapFiles.entries()) + .map(([, document]) => document) + .filter((document) => { + /** + * typecheck only the used component + */ + const component = EonComponentRegistry.getItemByUrl(document.sourcePath); + return component && component.isImported; + }); + // log type checking + this.message(gray('Type checking the current working directory')); + for await( let document of documents) { + if (document) { + this.message(`${gray('Check')} ${green(document.sourcePath)}`); + const [diags] = await Deno.compile(document.sourcePath, undefined, { + jsxFactory: 'h', + /** @ts-ignore */ + jsxFragmentFactory: 'hf', + jsx: 'react', + types: [new URL('./../../../types.d.ts', import.meta.url).pathname], + sourceMap: false, + }); + // TODO typecheck props usages + if (diags) { + diagnostics = [ + ...diagnostics, + ...diags + ]; + this.message(`${gray('\t\t-')} ${red(`error found into ${document.sourcePath}`)}`); + } else { + this.message(`${gray('\t\t-')} ${white('no error found')}`); + } + } + } + // start reporting type errors + // throws if defined + ModuleErrors.checkDiagnostics(diagnostics as unknown[]); + this.message(`${green('Success')} ${white('type checking passed with no errors')}`); + } +} diff --git a/src/classes/EonSandBox/README.md b/src/classes/EonSandBox/README.md new file mode 100644 index 0000000..d16c56e --- /dev/null +++ b/src/classes/EonSandBox/README.md @@ -0,0 +1,6 @@ +# Eon SandBox +Eon SandBox creates a folder at the same level of the current working directory, +so basically if Deno is running in `/home/app`, Eon SandBox will create `/home/.eon` + +all the files, excepted `tsconfig.json | .git/* | .vscode/* | node_modules/*` are copied into this sandbox. +this sandbox allows a better resolution of the imported components. \ No newline at end of file diff --git a/src/classes/EonSandBox/TODO.md b/src/classes/EonSandBox/TODO.md new file mode 100644 index 0000000..2d9db93 --- /dev/null +++ b/src/classes/EonSandBox/TODO.md @@ -0,0 +1,2 @@ +- [ ] support `--watch` argument, all the restarts should be handled by the sandbox +- [ ] watch changes in tsx/jsx files \ No newline at end of file diff --git a/src/classes/ModuleErrors.ts b/src/classes/ModuleErrors.ts index 9d22bc8..1e9c2f5 100644 --- a/src/classes/ModuleErrors.ts +++ b/src/classes/ModuleErrors.ts @@ -1,13 +1,75 @@ +import { colors } from "../../deps.ts"; +import Utils from "./Utils.ts"; /** * a class to display the errors inside the module */ -export abstract class ModuleErrors { - static checkDiagnostics(diagnostics: any[]) { - if (diagnostics) { - // TODO expose to the end user diagnostics here - console.warn(diagnostics) +interface ModuleErrorsDiagnostic { + start?: { + character: number; + line: number; + }; + end?: { + character: number; + line: number; + }; + sourceLine?: string; + messageText?: string; + messageChain?: { + messageText: string; + category: number; + code: number; + next: Pick[]; + } + fileName?: string; + category: number; + code: number; +} +export abstract class ModuleErrors extends Utils { + static checkDiagnostics(diagnostics: unknown[]) { + const { blue, red, gray, } = colors; + function renderChainedDiags(chainedDiags: typeof diagnostics): string{ + let result = ``; + const { red } = colors; + if (chainedDiags && chainedDiags.length) { + for (const d of chainedDiags) { + const diag = d as (ModuleErrorsDiagnostic); + result += red(`TS${diag.code} [ERROR] `); + result += `${diag && diag.messageText}\n` + } + } + return result; + } + if (diagnostics && diagnostics.length) { + let errors = ''; + for (const d of diagnostics.filter(d => (d as ModuleErrorsDiagnostic).start)) { + const diag = d as (ModuleErrorsDiagnostic); + const start = diag.start && diag.start.character || 0; + const end = diag.end && diag.end.character || 0; + const underline = red(`${' '.repeat(start)}^${'~'.repeat(end - start - 1)}`) + let sourceline = diag && diag.sourceLine || ''; + sourceline = gray(sourceline.substring(0, start)) + red(sourceline.substring(start, end)) + gray(sourceline.substring(end)); + // add the error + errors += ` + ${red(`TS${diag && diag.code} [ERROR]`)} ${blue(diag && diag.messageChain && diag.messageChain.messageText || diag && diag.messageText || '')} + ${blue(renderChainedDiags(diag && diag.messageChain && diag.messageChain.next || []))} + ${sourceline} + ${underline} + at ${blue(diag && diag.fileName || '')}:${diag.start && diag.start.line + 1 || ''}:${diag.start && diag.start.character || ''}`; + } + this.error( + errors, + ); + Deno.exit(1); } else { return; } } + static error(message: string, opts?: { [k: string]: unknown }): void { + const { bgRed, red, bold, yellow } = colors; + const m: string = this.message( + `${bgRed(" ERROR ")} ${red(message)}`, + { returns: true }, + ) as string; + console.error(m); + } } diff --git a/src/classes/ModuleGetter.ts b/src/classes/ModuleGetter.ts deleted file mode 100644 index 1657a76..0000000 --- a/src/classes/ModuleGetter.ts +++ /dev/null @@ -1,57 +0,0 @@ -// @deno-types="../../types.d.ts" - -import { ModuleErrors } from './ModuleErrors.ts'; -import { path } from './../../deps.ts'; -import { v4 } from "https://deno.land/std@0.67.0/uuid/mod.ts"; - -export interface ModuleGetterOptions { - entrypoint: string; -} -export interface EonModule { - name: string; - default?: (vm?: FunctionConstructor) => JSX.Element; - template?: (vm?: FunctionConstructor) => JSX.Element; - ViewModel: FunctionConstructor; - [k: string]: any; -} -export abstract class ModuleGetter { - private static async getModule(transpiled: string, opts: ModuleGetterOptions): Promise { - const { entrypoint } = opts; - const newPath = path.join(Deno.cwd(), `${entrypoint}.out.eon.${v4.generate()}.js`); - // TODO import h and hf from a file - Deno.writeTextFileSync(newPath, ` - function h() { return true } - function hf() {return false } - ${transpiled} - `); - const module = import(newPath) as unknown as EonModule; - module.then(() => { - Deno.removeSync(newPath); - }) - .catch((err: Error) => Deno.removeSync(newPath)) - return module; - } - static async buildModule(opts: ModuleGetterOptions): Promise { - const { entrypoint } = opts; - const transpiled = await ModuleGetter.getTranspiledFile(opts); - const module = await ModuleGetter.getModule(transpiled, opts); - return module; - } - static async getTranspiledFile(opts: ModuleGetterOptions): Promise { - const { entrypoint } = opts; - const [diagnostics, mod] = await Deno.compile(entrypoint, undefined, { - jsxFactory: "h", - /** @ts-ignore */ - jsxFragmentFactory: "hf", - types: ['./types.d.ts'], - sourceMap: false, - lib: ['dom'], - }); - // start reporting type errors - // throws if defined - ModuleErrors.checkDiagnostics(diagnostics as any); - // only need the values - const [ transpiled ] = Object.values(mod); - return transpiled; - } -} \ No newline at end of file diff --git a/src/classes/ModuleGetterOptions.ts b/src/classes/ModuleGetterOptions.ts new file mode 100644 index 0000000..8aa0a39 --- /dev/null +++ b/src/classes/ModuleGetterOptions.ts @@ -0,0 +1,5 @@ +// @deno-types="../../types.d.ts" + +export interface ModuleGetterOptions { + entrypoint: string; +} diff --git a/src/classes/ModuleResolver.ts b/src/classes/ModuleResolver.ts new file mode 100644 index 0000000..13b053f --- /dev/null +++ b/src/classes/ModuleResolver.ts @@ -0,0 +1,57 @@ +import type { EonModule } from './EonModule.ts'; +import EonComponent from './EonComponent.ts'; +import type { ModuleGetterOptions } from './ModuleGetterOptions.ts'; +import { v4 } from '../../deps.ts'; +import { ModuleErrors } from './ModuleErrors.ts'; + +export default abstract class ModuleResolver { + static currentComponent: EonComponent | null = null; + static async resolve(module: EonModule, opts: ModuleGetterOptions, isRootComponent: boolean = false): Promise { + const { entrypoint } = opts; + // get the default DOM Graph + // for this we use the default export or the export named template + // we are waiting for a function + const { VMC } = module; + if (!module.default || module.default && !(module.default instanceof Function)) { + throw ModuleErrors.error(`${entrypoint}\n\t Export default is required for all component as a function`) + } + const component = new EonComponent({ + file: '', + uuid: v4.generate(), + templateFactory: module.default, + VMC, + }); + component.isRootComponent = isRootComponent; + component.isImported = isRootComponent; + return component; + } + /** + * set the template of the component + */ + static setComponentTemplate(component: EonComponent): boolean { + const { VMC } = component; + ModuleResolver.currentComponent = component; + const vm = VMC ? new (VMC as FunctionConstructor)() : undefined; + const availableTemplate = component.templateFactory; + const defaultTemplate = + availableTemplate ? + availableTemplate.bind ? + availableTemplate.bind(vm) : + availableTemplate : null; + // start by using the templtate + switch (true) { + // default/template is a function + case !!defaultTemplate && typeof defaultTemplate === 'function': + if (defaultTemplate) { + component.template = defaultTemplate({}, vm); + if (component.template) { + // set the component of the template + // this allows all element to identify the component + component.template.component = component; + } + return true; + } + } + return false; + } +} \ No newline at end of file diff --git a/src/classes/Patterns.ts b/src/classes/Patterns.ts new file mode 100644 index 0000000..332f924 --- /dev/null +++ b/src/classes/Patterns.ts @@ -0,0 +1,15 @@ +/** + * this class holds all the patterns used into Eon + */ +export default abstract class Patterns { + /** + * this pattern allows the duplication of the custom element definition + * with some differencies + */ + static componentDeclaration = Deno.readTextFileSync(new URL('../patterns/component_declaration.ts', import.meta.url).pathname); + /** + * this pattern allows the duplication of the for directive for each elements + * the rendered nodes are wrapped into one element + */ + static forDirectivePattern = Deno.readTextFileSync(new URL('../patterns/for_directive.ts', import.meta.url).pathname); +} \ No newline at end of file diff --git a/src/classes/Utils.ts b/src/classes/Utils.ts new file mode 100644 index 0000000..e340235 --- /dev/null +++ b/src/classes/Utils.ts @@ -0,0 +1,45 @@ +import { colors } from '../../deps.ts'; +interface PatternOptions { + data: { [k: string]: string }; + open?: string; + close?: string; +} +/** + * a class to extend + * delivers some utils methods + */ +export default abstract class Utils { + static build = Deno.build; + static message(message: string, opts?: { [k: string]: unknown }): void | string { + const { cyan, bold, white } = colors; + const name = bold(cyan(' [Eon] ')); + if (opts && opts.returns) { + return `${name} ${message}`; + } else { + console.log(name, message); + return; + } + } + protected static renderPattern(pattern: string, options: PatternOptions): string { + let result = pattern; + const open = '"{{', close = '}}"'; + const { data } = options; + const fn = new Function( + '__value', + ...Object.keys(data), + `try { return eval(__value ); } catch(err) { throw err; }`, + ); + const values = Object.values(data); + while ( + result.indexOf(open) > -1 && result.indexOf(close) > -1 + ) { + const start = result.indexOf(open); + const end = result.indexOf(close) + 3; + const substrContent = result.substring(start + 3, end - 3).trim(); + const partStart = result.substring(0, start); + const partEnd = result.substring(end); + result = partStart + fn(substrContent, ...values) + partEnd; + } + return result; + } +} diff --git a/src/functions/increment.ts b/src/functions/increment.ts new file mode 100644 index 0000000..3cc9642 --- /dev/null +++ b/src/functions/increment.ts @@ -0,0 +1,5 @@ + +let i = 0; +export function increment() { + return i++; +} diff --git a/src/functions/jsxFactory.ts b/src/functions/jsxFactory.ts new file mode 100644 index 0000000..2031505 --- /dev/null +++ b/src/functions/jsxFactory.ts @@ -0,0 +1,143 @@ +import type { JSXFactory, JSXFragmentFactory, Attributes } from '../../types.d.ts'; +import { colors } from '../../deps.ts'; +import DOMElement from '../classes/DOMElement/DOMElement.ts'; +import EonComponentRegistry from '../classes/EonComponentRegistry.ts'; +import DOMElementDescriber from '../classes/DOMElementDescriber.ts'; +import type { DOMElementDescription } from '../classes/DOMElementDescriber.ts'; +import DOMElementRegistry from '../classes/DOMElementRegistry.ts'; +import ModuleResolver from '../classes/ModuleResolver.ts'; + +function setAttributes(element: DOMElement, attributes: Attributes) { + // TODO directives inside attributes + const entries = Object.entries(attributes); + entries.forEach(([key, value]) => { + // if the attribute is a function + // save it as a child of the element + // this will allow to bind the attribute + if (typeof value === 'function') { + element.setChild(new DOMElement({ + value, + name: key, + nodeType: 2, + parent: element, + children: [] + })); + if (element.attributes) { + delete element.attributes[key]; + } + return; + } + element.setChild(new DOMElement({ + value, + name: key, + nodeType: 2, + parent: element, + children: [] + })); + }); +} +/** + * jsxFactory + */ +export function h(...args: JSXFactory) { + const [tag, attributes, ...children] = args; + const component = EonComponentRegistry.getItemByTemplate(tag); + if (component) { + /** + * if the component exists we can render it by setting isImported to true + */ + component.isImported = true; + } + const element = new DOMElement({ + name: tag && tag.name ? tag.name : tag.toString(), + nodeType: 1, + children: [], + component, + attributes, + date: performance.now(), + }); + if (attributes) { + setAttributes(element, attributes); + } + if (tag === hf) { + element.nodeType = 11; + element.name = undefined; + return hf(...children); + } + // assign to the children the parent element + // assign the nodeType to the children + if (children.length) { + children.flat().forEach((child: unknown, i: number, arr: unknown[]) => { + let domelement: DOMElement; + if (child instanceof DOMElement) { + child.setParent(element); + element.setChild(child); + child.date = performance.now(); + } else { + domelement = new DOMElement({ + value: child, + children: [], + date: performance.now(), + }) + const isArrowIterationFunction: DOMElementDescription | null = DOMElementDescriber.getArrowFunctionDescription(child as any); + if (isArrowIterationFunction) { + // save the arrow iteration informations + domelement.isArrowIterationFunction = isArrowIterationFunction; + // need to use the arrow function, to get the child domelement + try { + // + const newChild = (child as (el: unknown, i: number, arr: unknown[]) => DOMElement)(void 0, 0, []) as (DOMElement); + // set child and set parent + domelement.setChild(newChild); + newChild.setParent(domelement); + } catch (err) { + const component = ModuleResolver.currentComponent; + const { red, white, gray } = colors + console.error(red(`[Eon] error in component: ${component?.sourcePath}\n\nContext error, please note that all values are undefined during context analyze.\n${gray(err.stack)}`)); + Deno.exit(1); + } + } else if (child && typeof child === 'string' + || child === null + || typeof child === 'boolean' + || typeof child === 'number' + || child instanceof Function) { + // get the nodetype + domelement.nodeType = 3; + } + // TODO define what to do about objects + // maybe we can think about a template switch with the objects + // save the domelement + domelement.setParent(element); + element.setChild(domelement); + } + }); + } + return element; +} +/** + * jsxFragmentFactory + */ +export function hf(...children: JSXFragmentFactory): DOMElement[] { + const controls: DOMElement[] = [] + children.flat().forEach((child: unknown, i: number, arr: any[]) => { + let domelement: DOMElement; + const isArrowIterationFunction: DOMElementDescription | null = DOMElementDescriber.getArrowFunctionDescription(child as any); + if (isArrowIterationFunction) { + domelement = new DOMElement({ + value: child, + children: [], + date: performance.now(), + }); + isArrowIterationFunction.wrapperName = typeof arr[i -1] === 'string' && arr[i -1] ? arr[i -1].trim() : undefined; + // save the arrow iteration informations + domelement.isArrowIterationFunction = isArrowIterationFunction; + // need to use the arrow function, to get the child domelement + const newChild = (child as () => DOMElement)() as (DOMElement); + // set child and set parent + domelement.setChild(newChild); + newChild.setParent(domelement); + controls.push(domelement); + } + }); + return controls; +} \ No newline at end of file diff --git a/src/functions/runtime.ts b/src/functions/runtime.ts new file mode 100644 index 0000000..d0fbbe4 --- /dev/null +++ b/src/functions/runtime.ts @@ -0,0 +1,75 @@ +/// +/** + * this files should export all the functions + * that can be used in the runtime + */ +/** + * should return a deep reactive proxy + * @param target the proxy target object + * @param updateFunction + * @param parentKey an index to save the proxy + */ +export function reactive(target: Object, updateFunction: Function, parentKey: string = ''): Object { + const proxies: { [k: string]: Object } = {}; + return new Proxy(target, { + get(obj: { [k: string]: unknown }, key: string, ...args: unknown[]) { + let v; + const id = `${parentKey}.${key.toString()}`; + if (key === 'prototype') { + v = Reflect.get(obj, key, ...args) + } else if (obj[key] instanceof Object && !proxies[id]) { + v = reactive(obj[key] as Object, updateFunction, id); + proxies[id] = v; + } else if (proxies[id]) { + return proxies[id]; + } else { + v = Reflect.get(obj, key, ...args); + } + return v; + }, + set(obj: { [k: string]: unknown }, key: string, value: unknown, ...args: unknown[]) { + if (obj[key] === value) return true; + const id = `${parentKey}.${key.toString()}`; + const v = Reflect.set(obj, key, value, ...args); + updateFunction(id); + return v; + }, + deleteProperty(obj, key) { + const id = `${parentKey}.${key.toString()}`; + const v = Reflect.deleteProperty(obj, key) + delete proxies[id]; + updateFunction(id); + return v; + } + }); +} +/** + * createElement function + */ +export function crt(n: string, ns = false) { + return ns ? document.createElementNS("http://www.w3.org/2000/svg", n) : document.createElement(n); +} +/** + * append function + */ +export function app(p: HTMLElement, n: HTMLElement) { + p && n && p.append(n); +} +/** + * append function + */ +export function rm(n: HTMLElement) { + n.remove(); +} +/** + * setAttribute function + */ +export function att(n: HTMLElement, k: string, v: string) { + n && k && n.setAttribute(k, v || ''); +} +/** + * addEventListener function + */ +export function add(n: HTMLElement, k: string, f: EventListenerOrEventListenerObject) { + n && k && f && n.addEventListener(k, f); +} \ No newline at end of file diff --git a/src/patterns/README.md b/src/patterns/README.md new file mode 100644 index 0000000..fbf2956 --- /dev/null +++ b/src/patterns/README.md @@ -0,0 +1,5 @@ +# Patterns Folder + +this folder contains all patterns that will be used for rendering +first choice was made by SRNV to use quote double curly braces `"{{ var }}"` +if you're not ok with this please open an issue and propose another way to declare vars \ No newline at end of file diff --git a/src/patterns/component_declaration.ts b/src/patterns/component_declaration.ts new file mode 100644 index 0000000..41e8e4b --- /dev/null +++ b/src/patterns/component_declaration.ts @@ -0,0 +1,97 @@ +// @ts-nocheck +function component_ctx() { + "{{ element_vars /** let n1, n2; */ }}" + // reactive function will be imported + let component = reactive("{{ vmc_instantiate }}", update); + /** + * all lifeCycle of the component + */ + const connected = "{{ vmc_name }}".connected && "{{ vmc_name }}".connected.bind(component); + const beforeUpdate = "{{ vmc_name }}".beforeUpdate && "{{ vmc_name }}".beforeUpdate.bind(component); + const updated = "{{ vmc_name }}".updated && "{{ vmc_name }}".updated.bind(component); + const beforeDestroy = "{{ vmc_name }}".beforedDstroy && "{{ vmc_name }}".beforedDstroy.bind(component); + const destroyed = "{{ vmc_name }}".destroyed && "{{ vmc_name }}".destroyed.bind(component); + // all render iteration functions + "{{ iterations_declarations }}" + /* will assign all the nodes inside vars*/ + function init() { + "{{ element_assignments }}" + // append childs - attributes will use set attribute + "{{ element_parent_append_childs /** parent.append(...childs) */ }}" + // call the functions that render the first iterations + // first iterations has no iteration ancestors + "{{ iterations_call }}" + // should return the root template + "{{ return_root_template }}" + } + /* general updates */ + function update() { + if (beforeUpdate) { + beforeUpdate(component); + } + // all update of bound textnodes + "{{ bound_textnodes_updates }}" + // all update of bound attributes + "{{ bound_attributes_updates }}" + // all component should update their props + "{{ props_updates }}" + // call the functions that render the first iterations + // first iterations has no iteration ancestors + "{{ iterations_call }}" + if (updated) { + updated(component); + } + } + /** when the component is destroyed */ + function destroy() { + // before we destroy all the elements + if (beforeDestroy) { + beforeDestroy(component); + } + // all elements destructions + // element.remove(); + "{{ element_destructions }}" + // finally destroyed component + if (destroyed) { + destroyed(component); + } + } + function connectedFn() { + if (connected) { + connected(component); + } + } + return { + component, + init: init.bind(component), + update: update.bind(component), + destroy: destroy.bind(component), + connected: connectedFn.bind(component), + } +} +customElements.define('"{{ uuid_component }}"', class extends HTMLElement { + static VMC = "{{ vmc_name }}"; + constructor() { + super(); + const { init, update, destroy, component, connected } = component_ctx(); + let template = init(); + // @ts-ignore + this.component = component; + /** + * set all the lifecycle + */ + this.connected = connected; + this.update = update; + this.destroy = destroy; + if ("{{ vmc_name }}".props) { + this.props = "{{ vmc_name }}".props.bind(component); + } + this.attachShadow({ mode: 'open' }); + this.shadowRoot.append(...template.childNodes); + } + + connectedCallback() { + this.connected && this.connected(); + this.update && this.update(); + } +}); \ No newline at end of file diff --git a/src/patterns/for_directive.ts b/src/patterns/for_directive.ts new file mode 100644 index 0000000..06ccc8a --- /dev/null +++ b/src/patterns/for_directive.ts @@ -0,0 +1,30 @@ +// @ts-nocheck +let "{{ element_array_name }}" = "{{ array_value }}"; +let "{{ element_index }}"_dup = 0; +for (const "{{ element_name }}" of "{{ element_array_name }}") { + // let n1, etc...; + "{{ childs_declarations }}" + let "{{ element_index }}" = "{{ element_array_name }}".indexOf("{{ element_name }}"); + "{{ element_index }}"_dup = "{{ element_index }}"; + // add missing elements + if ("{{ element_index }}" > "{{ element_wrapper }}".children.length -1) { + "{{ childs_assignments }}" + "{{ childs_appends }}" + "{{ childs_set_attributes }}" + "{{ childs_add_event_listener }}" + "{{ wrapper_update_subscriber }}"["{{ element_index }}"] = () => { + // update elements + "{{ childs_update }}" + } + } else { + // call a subscribed function + "{{ wrapper_update_subscriber }}"["{{ element_index }}"] && "{{ wrapper_update_subscriber }}"["{{ element_index }}"](); + } +} +// remove extra elements +if ("{{ element_index }}"_dup < "{{ element_wrapper }}".children.length -1) { + for (let "{{ removal_index }}" = "{{ element_wrapper }}".children.length -1; "{{ element_index }}"_dup < "{{ removal_index }}"; "{{ removal_index }}"--) { + "{{ element_wrapper }}".children["{{ removal_index }}"].remove(); + delete "{{ wrapper_update_subscriber }}"["{{ removal_index }}"]; + } +} \ No newline at end of file diff --git a/tests/DOMDescriber.test.ts b/tests/DOMDescriber.test.ts new file mode 100644 index 0000000..6089dd7 --- /dev/null +++ b/tests/DOMDescriber.test.ts @@ -0,0 +1,30 @@ +import DOMElementDescriber from '../src/classes/DOMElementDescriber.ts'; +import { assertEquals } from '../deps.ts'; + +Deno.test('DOMDescriber can parse arrow functions', () => { + const test = DOMElementDescriber.getArrowFunctionDescription((number, i, array = [1, 0]) => 10); + assertEquals(test, { + index: "i", + currentValue: "number", + array: "array", + arrayValue: "[1, 0]" + }); +}); + +Deno.test('DOMDescriber doesn\'t parse normal functions', () => { + const test = DOMElementDescriber.getArrowFunctionDescription(function(number, i, array = [1, 0]) { + return 10 + }); + assertEquals(test, null); +}); + +Deno.test('DOMDescriber only accept three argument', () => { + const test0 = DOMElementDescriber.getArrowFunctionDescription(() => 0); + const test1 = DOMElementDescriber.getArrowFunctionDescription((n: unknown) => 0); + const test2 = DOMElementDescriber.getArrowFunctionDescription((n: unknown, i: number) => 0); + const test4 = DOMElementDescriber.getArrowFunctionDescription((n: unknown, i: number, a: unknown[], x: null) => 0); + assertEquals(test0, null); + assertEquals(test1, null); + assertEquals(test2, null); + assertEquals(test4, null); +}); \ No newline at end of file diff --git a/tests/DOMElement.test.ts b/tests/DOMElement.test.ts new file mode 100644 index 0000000..0541e36 --- /dev/null +++ b/tests/DOMElement.test.ts @@ -0,0 +1,126 @@ +import DOMElement from '../src/classes/DOMElement.ts'; +import EonComponent from '../src/classes/EonComponent.ts'; +import { assertEquals, v4 } from '../deps.ts'; + +const fragment = new DOMElement({ + children: [], + name: undefined, + nodeType: 11, +}); + +const component = new EonComponent({ + file: `${Deno.cwd()}/tests.tsx`, + uuid: v4.generate(), + name: 'test-component', + templateFactory: () => fragment, +}); +fragment.component = component; + +const fragmentTemplate = new DOMElement({ + children: [], + name: 'template', + nodeType: 1, + parent: fragment +}); +fragment.children.push(fragmentTemplate); +const template = new DOMElement({ + children: [], + name: 'template', + nodeType: 1, + value: '', +}); +const textnode = new DOMElement({ + children: [], + name: undefined, + nodeType: 3, + parent: template, + value: 'Hello', +}); +const boundTextnode = new DOMElement({ + children: [], + name: undefined, + nodeType: 3, + parent: template, + value: () => 'sdgf', +}); +const node = new DOMElement({ + children: [], + name: 'div', + nodeType: 1, + parent: template, + value: '', +}); + +Deno.test('basic: nodeType 1 is an element', () => { + const domelement = new DOMElement({ + nodeType: 1, + children: [], + name: 'div' + }); + assertEquals('div', domelement.name); + assertEquals(false, domelement.isComponent); + assertEquals(false, domelement.isTemplate); + assertEquals(false, domelement.isStyle); + assertEquals(false, domelement.isBoundTextnode); + assertEquals(false, domelement.isFragment); + assertEquals(1, domelement.nodeType); + assertEquals(undefined, domelement.parentComponent); + const child = new DOMElement({ + nodeType: 1, + children: [], + name: 'div' + }); + domelement.setChild(child); + assertEquals(true, domelement.children.includes(child)); + assertEquals(true, domelement.uuid.startsWith('n')); +}); + +Deno.test('first letter of DOMElement\'s uuid sould depend on it\'s nodeType', () => { + assertEquals(true, template.uuid.startsWith('tmp')); + assertEquals(true, node.uuid.startsWith('n')); + assertEquals(true, template.uuid.startsWith('t')); +}); + +Deno.test('type validators of DOMElement are correct', () => { + assertEquals(false, fragment.isTemplate); + assertEquals(false, fragment.isStyle); + assertEquals(true, fragment.isFragment); + assertEquals(false, fragment.isComponent); + assertEquals(false, fragment.isBoundTextnode); + + assertEquals(true, template.isTemplate); + assertEquals(false, template.isStyle); + assertEquals(false, template.isFragment); + assertEquals(false, template.isComponent); + assertEquals(false, template.isBoundTextnode); + + assertEquals(true, fragmentTemplate.isTemplate); + assertEquals(false, fragmentTemplate.isStyle); + assertEquals(false, fragmentTemplate.isFragment); + assertEquals(false, fragmentTemplate.isComponent); + assertEquals(false, fragmentTemplate.isBoundTextnode); + + assertEquals(false, textnode.isTemplate); + assertEquals(false, textnode.isStyle); + assertEquals(false, textnode.isFragment); + assertEquals(false, textnode.isComponent); + assertEquals(false, textnode.isBoundTextnode); + + assertEquals(false, boundTextnode.isTemplate); + assertEquals(false, boundTextnode.isStyle); + assertEquals(false, boundTextnode.isFragment); + assertEquals(false, boundTextnode.isComponent); + assertEquals(true, boundTextnode.isBoundTextnode); + + assertEquals(false, node.isTemplate); + assertEquals(false, node.isStyle); + assertEquals(false, node.isFragment); + assertEquals(false, node.isComponent); + assertEquals(false, node.isBoundTextnode); +}); + +Deno.test('elements can access to the component', () => { + assertEquals(true, fragment.component === component); + assertEquals(true, fragmentTemplate.parentComponent === component); + assertEquals('test-component', fragmentTemplate.parentComponent && fragmentTemplate.parentComponent.name); +}); \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index 85640fe..e7e2a1a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,6 +2,8 @@ "compilerOptions": { "jsxFactory": "h", "jsxFragmentFactory": "hf", - "jsx": "react" - } + "jsx": "react", + "lib": ["esnext"], + "types": ["./types.d.ts"] + }, } \ No newline at end of file diff --git a/types.d.ts b/types.d.ts index 0a5ce44..2071730 100644 --- a/types.d.ts +++ b/types.d.ts @@ -1,27 +1,122 @@ /** bind this part of the graph */ -declare type BindedValue = (() => string) +declare type BoundValue = (() => string) + | (() => JSX.IntrinsicElements[]) + | (() => JSX.Element[]) + | ((currentValue: any, index: any, array?: any[]) => JSX.Element) | string | number | boolean - | any[] + | unknown[] | null; + +// deno-lint-ignore no-namespace declare namespace JSX { export interface IntrinsicElements { style: JSX.StyleElement; + template: JSX.TemplateElement; [k: string]: JSX.Element; } interface ElementChildrenAttribute { - children: any; + children: unknown; } - export interface Element { - children: Element | BindedValue; - [k: string]: any; + export interface Element extends DOMEventsLVL2 { + children?: Element | BoundValue; + [k: string]: unknown; } /** style elements should only accept strings as chlidren */ export interface StyleElement extends Element { - children: string; + children: string | (() => string); } + export interface ElementAttributesProperty { + props: Object; // specify the property name to use + } + export interface TemplateElement extends Element { + useVMC?: {} + meta?: ImportMeta; + name?: string; + } +} +type Attributes = { [k: string]: unknown } | DOMEventsLVL2; +declare function h(tagName: string, attributes: Attributes | null, ...children: unknown[]): JSX.Element; +declare function hf(...children: unknown[]): JSX.Element; +type JSXFactory = Parameters; +type JSXFragmentFactory = Parameters; +interface DOMEventsLVL2 { + onabort?: (...args: unknown[]) => unknown; + onanimationcancel?: (...args: unknown[]) => unknown; + onanimationend?: (...args: unknown[]) => unknown; + onanimationiteration?: (...args: unknown[]) => unknown; + onauxclick?: (...args: unknown[]) => unknown; + onblur?: (...args: unknown[]) => unknown; + oncancel?: (...args: unknown[]) => unknown; + oncanplay?: (...args: unknown[]) => unknown; + oncanplaythrough?: (...args: unknown[]) => unknown; + onchange?: (...args: unknown[]) => unknown; + onclick?: (...args: unknown[]) => unknown; + onclose?: (...args: unknown[]) => unknown; + oncontextmenu?: (...args: unknown[]) => unknown; + oncuechange?: (...args: unknown[]) => unknown; + ondblclick?: (...args: unknown[]) => unknown; + ondurationchange?: (...args: unknown[]) => unknown; + onended?: (...args: unknown[]) => unknown; + onerror?: (...args: unknown[]) => unknown; + onfocus?: (...args: unknown[]) => unknown; + onformdata?: (...args: unknown[]) => unknown; + ongotpointercapture?: (...args: unknown[]) => unknown; + oninput?: (...args: unknown[]) => unknown; + oninvalid?: (...args: unknown[]) => unknown; + onkeydown?: (...args: unknown[]) => unknown; + onkeypress?: (...args: unknown[]) => unknown; + onkeyup?: (...args: unknown[]) => unknown; + onload?: (...args: unknown[]) => unknown; + onloadeddata?: (...args: unknown[]) => unknown; + onloadedmetadata?: (...args: unknown[]) => unknown; + onloadend?: (...args: unknown[]) => unknown; + onloadstart?: (...args: unknown[]) => unknown; + onlostpointercapture?: (...args: unknown[]) => unknown; + onmousedown?: (...args: unknown[]) => unknown; + onmouseenter?: (...args: unknown[]) => unknown; + onmouseleave?: (...args: unknown[]) => unknown; + onmousemove?: (...args: unknown[]) => unknown; + onmouseout?: (...args: unknown[]) => unknown; + onmouseover?: (...args: unknown[]) => unknown; + onmouseup?: (...args: unknown[]) => unknown; + onpause?: (...args: unknown[]) => unknown; + onplay?: (...args: unknown[]) => unknown; + onplaying?: (...args: unknown[]) => unknown; + onpointercancel?: (...args: unknown[]) => unknown; + onpointerdown?: (...args: unknown[]) => unknown; + onpointerenter?: (...args: unknown[]) => unknown; + onpointerleave?: (...args: unknown[]) => unknown; + onpointermove?: (...args: unknown[]) => unknown; + onpointerout?: (...args: unknown[]) => unknown; + onpointerover?: (...args: unknown[]) => unknown; + onpointerup?: (...args: unknown[]) => unknown; + onreset?: (...args: unknown[]) => unknown; + onresize?: (...args: unknown[]) => unknown; + onscroll?: (...args: unknown[]) => unknown; + onselect?: (...args: unknown[]) => unknown; + onselectionchange?: (...args: unknown[]) => unknown; + onselectstart?: (...args: unknown[]) => unknown; + onsubmit?: (...args: unknown[]) => unknown; + ontouchcancel?: (...args: unknown[]) => unknown; + ontouchstart?: (...args: unknown[]) => unknown; + ontransitioncancel?: (...args: unknown[]) => unknown; + ontransitionend?: (...args: unknown[]) => unknown; + onwheel?: (...args: unknown[]) => unknown; +} +/** + * Utils Types from Eon + */ +/** + * both reactive and passive props are allowed + */ +declare type EonProps = { children?: any } & { + [P in keyof T]: ((handler: (value: T[P]) => void) => T[P]) | T[P]; } -type Attributes = { [k: string]: any }; -declare function h(tagName: string, attributes: Attributes | null, ...children: any[]): any; -declare function hf(attributes: Attributes | null, ...children: any[]): any; \ No newline at end of file +/** + * no reactions are allowed for those props + */ +declare type EonPassiveProps = { children?: any } & { + [P in keyof T]: T[P]; +} \ No newline at end of file