diff --git a/deps.ts b/deps.ts
index 850dbd6..5d7fb44 100644
--- a/deps.ts
+++ b/deps.ts
@@ -1,2 +1,6 @@
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 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";
\ No newline at end of file
diff --git a/examples/hello-app/HelloApp2.jsx b/examples/hello-app/AnotherComponent.tsx
similarity index 57%
rename from examples/hello-app/HelloApp2.jsx
rename to examples/hello-app/AnotherComponent.tsx
index 94d946c..e055d03 100644
--- a/examples/hello-app/HelloApp2.jsx
+++ b/examples/hello-app/AnotherComponent.tsx
@@ -1,19 +1,19 @@
-export const name = "AppHello";
-export default function() {
+export const name = "component-a";
+export default function(this: VMC) {
return (<>
{() => this.message}
{() => this.message}
{() => this.array[1]}
- { number }
}/>
>
)
}
-export class ViewModel {
+export class VMC {
message = "Hello World";
array = [1, 3];
- static props(props) {
+ static props(this: VMC, props: { message: string }) {
+ this.message = props.message;
return props
}
}
diff --git a/examples/hello-app/HelloApp.tsx b/examples/hello-app/HelloApp.tsx
index a71a610..65e8170 100644
--- a/examples/hello-app/HelloApp.tsx
+++ b/examples/hello-app/HelloApp.tsx
@@ -1,23 +1,30 @@
-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 {
+export const name = "hello-app";
+export default function(this: VMC) {
// 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 (<>
-
- {() => this.message}
+ this.message}>
+
+
+
+
+ {() => this.message}
+
-
>
)
}
-export class ViewModel {
+export class VMC {
public message = "Hello World";
+ public switch() {
+ this.message = 'test';
+ }
+ static props(this: VMC, props: { message: string }): boolean {
+ this.message = props.message;
+ return true;
+ }
}
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 (<>
-
- {
- new Directive.for(() => this.array.forEach((number: number) =>
- {number}
-
))
- }
-
- >
- )
-}
-export class ViewModel {
- public message = "Hello World";
- public array :number[] = [1, 3];
- static props(props: { name: string }) {
- return props
- }
-}
diff --git a/mod.ts b/mod.ts
index 7868d99..feba679 100644
--- a/mod.ts
+++ b/mod.ts
@@ -1,6 +1,49 @@
import { ModuleGetter } from './src/classes/ModuleGetter.ts';
+import ModuleResolver from './src/classes/ModuleResolver.ts';
+import './src/functions/jsxFactory.ts';
+import { ModuleGetterOptions } from './src/classes/ModuleGetter.ts';
+import EonComponent from './src/classes/EonComponent.ts';
+import DOMElementRegistry from './src/classes/DOMElementRegistry.ts';
-const component = await ModuleGetter.buildModule({
- entrypoint: './examples/hello-app/HelloApp.tsx',
-});
-console.warn(component);
+export class Eon {
+ static async define(opts: ModuleGetterOptions): Promise {
+ const module = await ModuleGetter.buildModule(opts);
+ const component = await ModuleResolver.resolve(module, opts);
+ return component
+ }
+ 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, registry: string[]): Promise {
+ async function render(path: string) {
+ const component = await Eon.define({
+ entrypoint: path,
+ });
+ // assign the root position to the root component
+ component.isRootComponent = root === path;
+ return component;
+ }
+ const rootComponent = await render(root);
+ const components = [rootComponent];
+ for await (const componentPath of registry) {
+ const component = await render(componentPath);
+ components.push(component);
+ }
+ // Eon.mount will set the template of the component
+ for await (const component of components) {
+ await Eon.mount(component);
+ }
+ ModuleGetter.typeCheckComponents();
+ return components;
+ }
+}
+
+const components = await Eon.dev('./examples/hello-app/HelloApp.tsx', [
+ './examples/hello-app/AnotherComponent.tsx'
+])
diff --git a/src/classes/DOMElement.ts b/src/classes/DOMElement.ts
new file mode 100644
index 0000000..a3681c3
--- /dev/null
+++ b/src/classes/DOMElement.ts
@@ -0,0 +1,107 @@
+import EonComponent from './EonComponent.ts';
+import { increment } from '../functions/increment.ts';
+import DOMElementRegistry from './DOMElementRegistry.ts';
+/**
+ * class that participate to the DOM Tree description
+ */
+export type DOMTreeElement = DOMElement;
+interface DOMElementInterface {
+ /** the parent element of the element, undefined if the element is on top */
+ parent?: DOMTreeElement;
+ /** the children of the element */
+ children: DOMTreeElement[];
+ /** 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;
+}
+export default class DOMElement implements DOMElementInterface {
+ parent: DOMElementInterface['parent'];
+ children: DOMElementInterface['children'];
+ name: DOMElementInterface['name'];
+ nodeType: DOMElementInterface['nodeType'];
+ value: DOMElementInterface['value'];
+ attributes: DOMElementInterface['attributes'];
+ component: DOMElementInterface['component'];
+ id?: number;
+ constructor(opts: DOMElementInterface) {
+ const {
+ nodeType,
+ parent,
+ name,
+ children,
+ value,
+ attributes,
+ component,
+ } = 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();
+ DOMElementRegistry.subscribe(this.uuid, this);
+ }
+ get uuid(): string {
+ const idType = this.isBoundTextnode ? 't'
+ : this.isTemplate ? 'tmp'
+ : this.isComponent ? 'c'
+ : 'n';
+ return `${idType}${this.id}`;
+ }
+ get isBoundTextnode(): boolean {
+ return this.nodeType === 3 && 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;
+ }
+ /** returns the component that is using this element */
+ get parentComponent(): EonComponent | undefined {
+ let parent = this.parent, domelement: DOMElement | undefined;
+ while (parent) {
+ domelement = this.parent;
+ parent = this.parent?.parent;
+ }
+ return (domelement || this).component;
+ }
+ setParent(parent: DOMTreeElement) {
+ this.parent = parent;
+ }
+ setChild(child: DOMTreeElement) {
+ this.children.push(child);
+ }
+}
\ No newline at end of file
diff --git a/src/classes/DOMElementRegistry.ts b/src/classes/DOMElementRegistry.ts
new file mode 100644
index 0000000..2ff2f1a
--- /dev/null
+++ b/src/classes/DOMElementRegistry.ts
@@ -0,0 +1,21 @@
+import DOMElement from './DOMElement.ts';
+/**
+ * class to save all the element used
+ */
+export default class DOMElementRegistry {
+ private static registry: Map = new Map();
+ public static subscribe(uuid: string, domelement: DOMElement) {
+ if (!this.registry.has(uuid)) {
+ this.registry.set(uuid, domelement);
+ }
+ }
+ static get collection(): DOMElement[] {
+ return Array.from(this.registry.entries()).map(([key, domelement]) => domelement);
+ }
+ 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/EonComponent.ts b/src/classes/EonComponent.ts
new file mode 100644
index 0000000..9ef2c2a
--- /dev/null
+++ b/src/classes/EonComponent.ts
@@ -0,0 +1,48 @@
+import DOMElement from './DOMElement.ts';
+import type { EonModule } from './ModuleGetter.ts';
+import EonComponentRegistry from './EonComponentRegistry.ts';
+
+export interface EonComponentInterface {
+ /** uuid */
+ uuid?: string;
+ /** name */
+ name?: string;
+ /** path of the component */
+ file?: 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;
+}
+export default class EonComponent implements EonComponentInterface {
+ uuid: EonComponentInterface['uuid'];
+ name: EonComponentInterface['name'];
+ file: EonComponentInterface['file'];
+ template: EonComponentInterface['template'];
+ VMC: EonComponentInterface['VMC'];
+ isRootComponent: EonComponentInterface['isRootComponent'] = false;
+ templateFactory: EonComponentInterface['templateFactory'];
+ constructor(opts: EonComponentInterface) {
+ const {
+ file,
+ uuid,
+ template,
+ VMC,
+ templateFactory,
+ name
+ } = 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);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/classes/EonComponentRegistry.ts b/src/classes/EonComponentRegistry.ts
new file mode 100644
index 0000000..c2d53f5
--- /dev/null
+++ b/src/classes/EonComponentRegistry.ts
@@ -0,0 +1,23 @@
+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);
+ }
+ static getItemByName(name: string): EonComponent | undefined {
+ const entries = Array.from(this.registry.entries());
+ const found = entries.find(([key, component]) => component.name === name);
+ if (found) {
+ const [, component] = found;
+ return component;
+ }
+ }
+ static get collection() {
+ return Array.from(this.registry.entries())
+ }
+}
\ No newline at end of file
diff --git a/src/classes/ModuleErrors.ts b/src/classes/ModuleErrors.ts
index 9d22bc8..346048f 100644
--- a/src/classes/ModuleErrors.ts
+++ b/src/classes/ModuleErrors.ts
@@ -1,12 +1,56 @@
+import { colors, path } from "../../deps.ts";
/**
* a class to display the errors inside the module
*/
+interface ModuleErrorsDiagnostic {
+ start?: {
+ character: number
+ };
+ end?: {
+ character: number
+ };
+ sourceLine?: string;
+ messageText?: string;
+ fileName?: string;
+}
export abstract class ModuleErrors {
- static checkDiagnostics(diagnostics: any[]) {
- if (diagnostics) {
- // TODO expose to the end user diagnostics here
- console.warn(diagnostics)
+ static checkDiagnostics(diagnostics: unknown[]) {
+ const { blue, red, white, gray, bgRed } = colors;
+ 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)) + bgRed(white(sourceline.substring(start, end))) + gray(sourceline.substring(end));
+ // add the error
+ errors +=+ `\n\t${blue(diag && diag.messageText || '')}\n\t${sourceline}\n\t${underline}\n\tat ${blue(diag && diag.fileName || '')}`;
+ }
+ 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;
+ throw new Error(m);
+ }
+ 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;
}
}
diff --git a/src/classes/ModuleGetter.ts b/src/classes/ModuleGetter.ts
index 1657a76..cd43fbc 100644
--- a/src/classes/ModuleGetter.ts
+++ b/src/classes/ModuleGetter.ts
@@ -1,30 +1,39 @@
// @deno-types="../../types.d.ts"
+import DOMElement from './DOMElement.ts';
import { ModuleErrors } from './ModuleErrors.ts';
-import { path } from './../../deps.ts';
-import { v4 } from "https://deno.land/std@0.67.0/uuid/mod.ts";
+import { path, v4 } from './../../deps.ts';
+import EonComponentRegistry from './EonComponentRegistry.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;
+ default(vm?: T): DOMElement;
+ template?: (vm?: T) => DOMElement;
+ VMC: unknown;
+ [k: string]: unknown;
}
+const sessionUuid = v4.generate();
export abstract class ModuleGetter {
+ /**
+ * creates a new ts file, with the transpiled JSX
+ * and imports this file with the dynamic import,
+ * this generate the ES Module with all the exports (named and default)
+ */
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
+ const newPath = path.join(Deno.cwd(), `${entrypoint}.${sessionUuid}.js`);
Deno.writeTextFileSync(newPath, `
- function h() { return true }
- function hf() {return false }
+ // @ts-nocheck
+ import { h, hf } from '${path.join(import.meta.url, '../../functions/jsxFactory.ts')}';
${transpiled}
`);
const module = import(newPath) as unknown as EonModule;
+ // TODO fix this for deno lint and deno run
+ // deno-lint-ignore ban-ts-comment
+ // @ts-ignore
module.then(() => {
Deno.removeSync(newPath);
})
@@ -32,26 +41,55 @@ export abstract class ModuleGetter {
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;
}
+ /**
+ *
+ * the transpilation is needed
+ * because we need to set the jsxFactory and the jsxFragmentFactory
+ * for the jsx transpilation
+ */
static async getTranspiledFile(opts: ModuleGetterOptions): Promise {
const { entrypoint } = opts;
- const [diagnostics, mod] = await Deno.compile(entrypoint, undefined, {
+ const result = await Deno.transpileOnly({
+ [entrypoint]: Deno.readTextFileSync(entrypoint),
+ }, {
jsxFactory: "h",
/** @ts-ignore */
jsxFragmentFactory: "hf",
- types: ['./types.d.ts'],
sourceMap: false,
+ jsx: "react",
lib: ['dom'],
});
+ return result[entrypoint].source;
+ }
+ static async typeCheckComponents(): Promise {
+ let diagnostics: unknown[] = []
+ for await (const [key, component] of EonComponentRegistry.collection) {
+ if (component.file) {
+ const [diags, mod] = await Deno.compile(component.file, undefined, {
+ jsxFactory: "h",
+ /** @ts-ignore */
+ jsxFragmentFactory: "hf",
+ jsx: "react",
+ types: ['./types.d.ts'],
+ sourceMap: false,
+ lib: ['dom'],
+ });
+ // TODO typecheck props usages
+ if (diags) {
+ diagnostics = [
+ ...diagnostics,
+ ...diags
+ ];
+ }
+ }
+ }
+
// start reporting type errors
// throws if defined
- ModuleErrors.checkDiagnostics(diagnostics as any);
- // only need the values
- const [ transpiled ] = Object.values(mod);
- return transpiled;
+ ModuleErrors.checkDiagnostics(diagnostics as unknown[]);
}
}
\ No newline at end of file
diff --git a/src/classes/ModuleResolver.ts b/src/classes/ModuleResolver.ts
new file mode 100644
index 0000000..2b419df
--- /dev/null
+++ b/src/classes/ModuleResolver.ts
@@ -0,0 +1,66 @@
+import type { EonModule } from './ModuleGetter.ts';
+import EonComponent from './EonComponent.ts';
+import type { ModuleGetterOptions } from './ModuleGetter.ts';
+import { v4 } from '../../deps.ts';
+import { ModuleErrors } from './ModuleErrors.ts';
+
+export default abstract class ModuleResolver {
+ static async resolve(module: EonModule, opts: ModuleGetterOptions) {
+ 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
+ // TODO define what to do if default/template is a string
+ const { template, VMC, name } = module;
+ const component = new EonComponent({
+ file: entrypoint,
+ uuid: v4.generate(),
+ templateFactory: module.default || template,
+ name,
+ VMC,
+ });
+ this.checkNameValidity(name, opts);
+ component.name = name;
+ return component;
+ }
+ /**
+ * throws if the name is undefined or
+ * doesn't follow the DOMString pattern
+ */
+ static checkNameValidity(name: string | undefined, opts: ModuleGetterOptions): void {
+ if (!name) {
+ ModuleErrors.error(`\n\t${opts.entrypoint}\n\tall components should export a name`);
+ }
+ if (name && !/^[a-zA-Z]\w+(\-\w+)+$/i.test(name)) {
+ ModuleErrors.error(`\n\t${opts.entrypoint}\n\tCannot use ${name} as component's name\n\tplease follow the DOMString pattern: /^[a-zA-Z]\w+(\-\w+)+$/i\n\tfor example: component-name`);
+ }
+ }
+ /**
+ * set the template of the component
+ */
+ static setComponentTemplate(component: EonComponent): boolean {
+ const { VMC } = 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/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..e1229f8
--- /dev/null
+++ b/src/functions/jsxFactory.ts
@@ -0,0 +1,81 @@
+import type { JSXFactory, JSXFragmentFactory, Attributes } from '../../types.d.ts';
+import DOMElement from '../classes/DOMElement.ts';
+import EonComponentRegistry from '../classes/EonComponentRegistry.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];
+ }
+ }
+ });
+}
+/**
+ * jsxFactory
+ */
+export function h(...args: JSXFactory) {
+ const [tag, attributes, ...children] = args;
+ const element = new DOMElement({
+ name: tag && tag.name ? tag.name : tag.toString(),
+ nodeType: 1,
+ children: [],
+ component: EonComponentRegistry.getItemByName(tag),
+ attributes,
+ });
+ if (attributes) {
+ setAttributes(element, attributes);
+ }
+ // assign to the children the parent element
+ // assign the nodeType to the children
+ if (children.length) {
+ children.forEach((child: unknown) => {
+ let domelement: DOMElement;
+ if (child instanceof DOMElement) {
+ child.setParent(element);
+ element.setChild(child);
+ } else {
+ domelement = new DOMElement({
+ value: child,
+ children: [],
+ })
+ domelement.setParent(element);
+ // get the nodetype
+ if (child && typeof child === 'string'
+ || child === null
+ || typeof child === 'boolean'
+ || typeof child === 'number'
+ || child instanceof Function) {
+ domelement.nodeType = 3;
+ }
+ // TODO define what to do about objects
+ // maybe we can think about a template switch with the objects
+ // save the domelement
+ element.setChild(domelement);
+ }
+ });
+ }
+ if (typeof tag === 'function' && tag.name === 'hf') {
+ element.nodeType = 11;
+ element.name = undefined;
+ }
+ return element;
+}
+/**
+ * jsxFragmentFactory
+ */
+export function hf(...args: JSXFragmentFactory) {
+ return args;
+}
\ 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..6961472 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -2,6 +2,5 @@
"compilerOptions": {
"jsxFactory": "h",
"jsxFragmentFactory": "hf",
- "jsx": "react"
}
}
\ No newline at end of file
diff --git a/types.d.ts b/types.d.ts
index 0a5ce44..b3a42dd 100644
--- a/types.d.ts
+++ b/types.d.ts
@@ -1,27 +1,96 @@
/** bind this part of the graph */
-declare type BindedValue = (() => string)
+declare type BoundValue = (() => string)
+ | (() => JSX.IntrinsicElements[])
| string
| number
| boolean
- | any[]
+ | unknown[]
| null;
+
+// deno-lint-ignore no-namespace
declare namespace JSX {
export interface IntrinsicElements {
style: JSX.StyleElement;
[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;
}
}
-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
+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;
+}
\ No newline at end of file