mirror of
https://github.com/ragestudio/vessel.git
synced 2025-06-09 10:34:18 +00:00
init
This commit is contained in:
commit
45a809a1bb
22
.gitignore
vendored
Normal file
22
.gitignore
vendored
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
# Trash
|
||||||
|
/**/**/.crash.log
|
||||||
|
/**/**/.tmp
|
||||||
|
/**/**/.evite
|
||||||
|
/**/**/.cache
|
||||||
|
/**/**/out
|
||||||
|
/**/**/.out
|
||||||
|
/**/**/dist
|
||||||
|
/**/**/node_modules
|
||||||
|
/**/**/corenode_modules
|
||||||
|
/**/**/.DS_Store
|
||||||
|
/**/**/package-lock.json
|
||||||
|
/**/**/yarn.lock
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
/**/**/npm-debug.log*
|
||||||
|
/**/**/yarn-error.log
|
||||||
|
/**/**/dumps.log
|
||||||
|
/**/**/corenode.log
|
||||||
|
|
||||||
|
# Temporal configurations
|
||||||
|
/**/**/.aliaser
|
31
package.json
Normal file
31
package.json
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
{
|
||||||
|
"name": "vessel",
|
||||||
|
"version": "0.18.0",
|
||||||
|
"main": "./src/index.js",
|
||||||
|
"repository": "https://github.com/ragestudio/vessel.git",
|
||||||
|
"author": "SrGooglo <srgooglo@ragestudio.net>",
|
||||||
|
"license": "MIT",
|
||||||
|
"files": [
|
||||||
|
"src"
|
||||||
|
],
|
||||||
|
"scripts": {
|
||||||
|
"link": "yarn link && yarn link 'evite'",
|
||||||
|
"example:react": "cd example && yarn start"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^18.2.0",
|
||||||
|
"react-dom": "^18.2.0",
|
||||||
|
"vite": "^4.2.1"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@loadable/component": "^5.16.4",
|
||||||
|
"@vitejs/plugin-react": "^4.3.3",
|
||||||
|
"history": "^5.3.0",
|
||||||
|
"less": "^4.2.0",
|
||||||
|
"million": "^3.1.11",
|
||||||
|
"object-observer": "^6.0.0",
|
||||||
|
"react": "^18.3.1",
|
||||||
|
"react-dom": "^18.3.1",
|
||||||
|
"react-helmet": "^6.1.0"
|
||||||
|
}
|
||||||
|
}
|
285
src/classes/EventBus/EventEmitter.ts
Normal file
285
src/classes/EventBus/EventEmitter.ts
Normal file
@ -0,0 +1,285 @@
|
|||||||
|
import Listener from "./Listener";
|
||||||
|
import type {
|
||||||
|
EventTemplateT,
|
||||||
|
TemplateEventT,
|
||||||
|
TemplateListenerArgsT,
|
||||||
|
TemplateListenerContextT,
|
||||||
|
TemplateListenerT,
|
||||||
|
} from "./Listener";
|
||||||
|
|
||||||
|
export default class EventEmitter<Template extends EventTemplateT = EventTemplateT> {
|
||||||
|
protected events: EventsT<Template> = {};
|
||||||
|
|
||||||
|
public on = this.addListener;
|
||||||
|
|
||||||
|
public off = this.removeListener;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an array listing the events for which the emitter has registered listeners.
|
||||||
|
*/
|
||||||
|
public eventNames(): TemplateEventT<Template>[] {
|
||||||
|
const events = this.events;
|
||||||
|
|
||||||
|
return Object.keys(events).filter((event) => events[event] !== undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the array of listeners for the specified `event`.
|
||||||
|
*
|
||||||
|
* @param event
|
||||||
|
*/
|
||||||
|
public rawListeners<Event extends TemplateEventT<Template>>(
|
||||||
|
event: Event,
|
||||||
|
): Listener<Template, Event, this>[] {
|
||||||
|
const listeners = this.events[event];
|
||||||
|
|
||||||
|
if (listeners === undefined) return [];
|
||||||
|
|
||||||
|
if (isSingleListener(listeners)) return [listeners];
|
||||||
|
|
||||||
|
return listeners;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a copy of the array of listeners for the specified `event`.
|
||||||
|
*
|
||||||
|
* @param event
|
||||||
|
*/
|
||||||
|
public listeners<Event extends TemplateEventT<Template>>(
|
||||||
|
event: Event,
|
||||||
|
): TemplateListenerT<Template, Event, this>[] {
|
||||||
|
const listeners = this.rawListeners(event);
|
||||||
|
const length = listeners.length;
|
||||||
|
|
||||||
|
if (length === 0) return [];
|
||||||
|
|
||||||
|
const result = new Array(length);
|
||||||
|
|
||||||
|
for (let index = 0; index < length; index++) result[index] = listeners[index].fn;
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the number of listeners listening to the specified `event`.
|
||||||
|
*
|
||||||
|
* @param event
|
||||||
|
*/
|
||||||
|
public listenerCount<Event extends TemplateEventT<Template>>(
|
||||||
|
event: Event,
|
||||||
|
): number {
|
||||||
|
return this.rawListeners(event).length;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Synchronously calls each of the listeners registered for the specified `event`,
|
||||||
|
* in the order they were registered, passing the supplied arguments to each.
|
||||||
|
*
|
||||||
|
* @param event
|
||||||
|
* @param args
|
||||||
|
* @returns - `true` if the `event` had listeners, `false` otherwise.
|
||||||
|
*/
|
||||||
|
public emit<Event extends TemplateEventT<Template>>(
|
||||||
|
event: Event,
|
||||||
|
...args: TemplateListenerArgsT<Template, Event>
|
||||||
|
): boolean {
|
||||||
|
const events = this.events;
|
||||||
|
const listeners = events[event];
|
||||||
|
|
||||||
|
if (listeners === undefined) {
|
||||||
|
if (event === "error") throw args[0];
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isSingleListener(listeners)) {
|
||||||
|
const { fn, context, once } = listeners;
|
||||||
|
|
||||||
|
if (once) events[event] = undefined;
|
||||||
|
|
||||||
|
fn.apply(context, args);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
let length = listeners.length;
|
||||||
|
|
||||||
|
for (let index = 0; index < length; index++) {
|
||||||
|
const { fn, context, once } = listeners[index];
|
||||||
|
|
||||||
|
if (once) {
|
||||||
|
listeners.splice(index--, 1);
|
||||||
|
|
||||||
|
length--;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn.apply(context, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (length === 0) events[event] = undefined;
|
||||||
|
else if (length === 1) events[event] = listeners[0];
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds the `listener` function to the end of the listeners array for the specified `event`.
|
||||||
|
* No checks are made to see if the listener has already been added.
|
||||||
|
* Multiple calls passing the same combination of `event` and `listener` will result in the listener being added,
|
||||||
|
* and called, multiple times.
|
||||||
|
*
|
||||||
|
* @param event
|
||||||
|
* @param listener
|
||||||
|
* @param context - default: `this` (EventEmitter instance)
|
||||||
|
*/
|
||||||
|
public addListener<Event extends TemplateEventT<Template>, Context = TemplateListenerContextT<Template, Event, this>>(
|
||||||
|
event: Event,
|
||||||
|
listener: TemplateListenerT<Template, Event, Context>,
|
||||||
|
context?: Context,
|
||||||
|
): this {
|
||||||
|
return this._addListener(event, listener, context, true, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a one-time `listener` function for the specified `event` to the end of the `listeners` array.
|
||||||
|
* The next time `event` is triggered, this listener is removed, and then invoked.
|
||||||
|
*
|
||||||
|
* @param event
|
||||||
|
* @param listener
|
||||||
|
* @param context - default: `this` (EventEmitter instance)
|
||||||
|
*/
|
||||||
|
public once<Event extends TemplateEventT<Template>, Context = TemplateListenerContextT<Template, Event, this>>(
|
||||||
|
event: Event,
|
||||||
|
listener: TemplateListenerT<Template, Event, Context>,
|
||||||
|
context?: Context,
|
||||||
|
): this {
|
||||||
|
return this._addListener(event, listener, context, true, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds the `listener` function to the beginning of the listeners array for the specified `event`.
|
||||||
|
* No checks are made to see if the listener has already been added.
|
||||||
|
* Multiple calls passing the same combination of `event` and `listener` will result in the listener being added,
|
||||||
|
* and called, multiple times.
|
||||||
|
*
|
||||||
|
* @param event
|
||||||
|
* @param listener
|
||||||
|
* @param context - default: `this` (EventEmitter instance)
|
||||||
|
*/
|
||||||
|
public prependListener<Event extends TemplateEventT<Template>, Context = TemplateListenerContextT<Template, Event, this>>(
|
||||||
|
event: Event,
|
||||||
|
listener: TemplateListenerT<Template, Event, Context>,
|
||||||
|
context?: Context,
|
||||||
|
): this {
|
||||||
|
return this._addListener(event, listener, context, false, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a one-time `listener` function for the specified `event` to the beginning of the `listeners` array.
|
||||||
|
* The next time `event` is triggered, this listener is removed, and then invoked.
|
||||||
|
*
|
||||||
|
* @param event
|
||||||
|
* @param listener
|
||||||
|
* @param context - default: `this` (EventEmitter instance)
|
||||||
|
*/
|
||||||
|
public prependOnceListener<Event extends TemplateEventT<Template>, Context = TemplateListenerContextT<Template, Event, this>>(
|
||||||
|
event: Event,
|
||||||
|
listener: TemplateListenerT<Template, Event, Context>,
|
||||||
|
context?: Context,
|
||||||
|
): this {
|
||||||
|
return this._addListener(event, listener, context, false, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes all `listeners`, or those of the specified `event`.
|
||||||
|
*
|
||||||
|
* @param event
|
||||||
|
*/
|
||||||
|
public removeAllListeners<Event extends TemplateEventT<Template>>(
|
||||||
|
event?: Event,
|
||||||
|
): this {
|
||||||
|
if (event === undefined) {
|
||||||
|
this.events = {};
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.events[event] === undefined) return this;
|
||||||
|
|
||||||
|
this.events[event] = undefined;
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes the specified `listener` from the `listeners` array for the specified `event`.
|
||||||
|
*
|
||||||
|
* @param event
|
||||||
|
* @param listener
|
||||||
|
*/
|
||||||
|
public removeListener<Event extends TemplateEventT<Template>>(
|
||||||
|
event: Event,
|
||||||
|
listener: TemplateListenerT<Template, Event, this>,
|
||||||
|
): this {
|
||||||
|
const listeners = this.events[event];
|
||||||
|
|
||||||
|
if (listeners === undefined) return this;
|
||||||
|
|
||||||
|
if (isSingleListener(listeners)) {
|
||||||
|
if (listeners.fn === listener) this.events[event] = undefined;
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let index = listeners.length - 1; index >= 0; index--) {
|
||||||
|
if (listeners[index].fn === listener) listeners.splice(index, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (listeners.length === 0) this.events[event] = undefined;
|
||||||
|
else if (listeners.length === 1) this.events[event] = listeners[0];
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected _addListener<Event extends TemplateEventT<Template>, Context = TemplateListenerContextT<Template, Event, this>>(
|
||||||
|
event: Event,
|
||||||
|
fn: TemplateListenerT<Template, Event, Context>,
|
||||||
|
context: Context = this as never,
|
||||||
|
append: boolean,
|
||||||
|
once: boolean,
|
||||||
|
): this {
|
||||||
|
const events = this.events;
|
||||||
|
const listeners = events[event];
|
||||||
|
const listener = new Listener<Template, Event, Context>(fn, context, once);
|
||||||
|
|
||||||
|
if (listeners === undefined) {
|
||||||
|
events[event] = listener;
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isSingleListener(listeners)) {
|
||||||
|
events[event] = append ? [listeners, listener] : [listener, listeners];
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (append) {
|
||||||
|
listeners.push(listener);
|
||||||
|
} else {
|
||||||
|
listeners.unshift(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isSingleListener<Type>(value: Type | Type[]): value is Type {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
return (value as any).length === undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type EventsT<Template extends EventTemplateT> = {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
[Event in TemplateEventT<Template>]?: Listener<Template, Event, any> | Listener<Template, Event, any>[];
|
||||||
|
};
|
51
src/classes/EventBus/Listener.ts
Normal file
51
src/classes/EventBus/Listener.ts
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
export default class Listener<Template extends EventTemplateT, Event extends TemplateEventT<Template>, Context> {
|
||||||
|
constructor(
|
||||||
|
public readonly fn: TemplateListenerT<Template, Event, Context>,
|
||||||
|
public readonly context: Context,
|
||||||
|
public readonly once: boolean,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
export type EventT = keyof any;
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
export type ListenerT = (...args: any[]) => void;
|
||||||
|
|
||||||
|
export type EventTemplateT = {
|
||||||
|
[Event in EventT]: ListenerT;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ListenerArgsT<Listener> = [Listener] extends [
|
||||||
|
(...args: infer Args) => void,
|
||||||
|
]
|
||||||
|
? Args
|
||||||
|
: [Listener] extends [void]
|
||||||
|
? []
|
||||||
|
: [Listener];
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
export type ListenerContextT<Listener, DefaultContext> = [Listener] extends [(this: infer Context, ...args: any[]) => void]
|
||||||
|
? unknown extends Context ? DefaultContext : Context
|
||||||
|
: DefaultContext;
|
||||||
|
|
||||||
|
export type TemplateT<Template extends EventTemplateT> = Template & {
|
||||||
|
error: (error: Error) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TemplateEventT<Template extends EventTemplateT> = keyof TemplateT<Template>;
|
||||||
|
|
||||||
|
export type TemplateListenerT<Template extends EventTemplateT,
|
||||||
|
Event extends TemplateEventT<Template>,
|
||||||
|
Context,
|
||||||
|
> = (this: Context, ...args: TemplateListenerArgsT<Template, Event>) => void;
|
||||||
|
|
||||||
|
export type TemplateListenerContextT<Template extends EventTemplateT,
|
||||||
|
Event extends TemplateEventT<Template>,
|
||||||
|
DefaultContext,
|
||||||
|
> = ListenerContextT<TemplateT<Template>[Event], DefaultContext>;
|
||||||
|
|
||||||
|
export type TemplateListenerArgsT<Template extends EventTemplateT,
|
||||||
|
Event extends TemplateEventT<Template>,
|
||||||
|
> = ListenerArgsT<TemplateT<Template>[Event]>;
|
27
src/classes/EventBus/index.js
Normal file
27
src/classes/EventBus/index.js
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import EventEmitter from "./EventEmitter"
|
||||||
|
|
||||||
|
export default class EventBus extends EventEmitter {
|
||||||
|
constructor(params = {}) {
|
||||||
|
super({ ...params, captureRejections: true })
|
||||||
|
|
||||||
|
this.id = params?.id
|
||||||
|
}
|
||||||
|
|
||||||
|
_emit = this.emit
|
||||||
|
|
||||||
|
emit = (event, ...args) => {
|
||||||
|
return this._emit(event, ...args)
|
||||||
|
}
|
||||||
|
|
||||||
|
on = (event, listener, context) => {
|
||||||
|
return this._addListener(event, listener, context, true, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
off = (event, listener) => {
|
||||||
|
return this.removeListener(event, listener)
|
||||||
|
}
|
||||||
|
|
||||||
|
once = (event, listener, context) => {
|
||||||
|
return this.once(event, listener, context)
|
||||||
|
}
|
||||||
|
}
|
114
src/classes/InternalConsole/index.js
Normal file
114
src/classes/InternalConsole/index.js
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
class Echo {
|
||||||
|
constructor(params = {}) {
|
||||||
|
this.bgColor = params.bgColor ?? "dimgray"
|
||||||
|
this.color = params.color ?? "azure"
|
||||||
|
}
|
||||||
|
|
||||||
|
queue = []
|
||||||
|
ECHO_TOKEN = {}
|
||||||
|
RESET_INPUT = "%c "
|
||||||
|
RESET_CSS = ""
|
||||||
|
|
||||||
|
tagFormatting(value) {
|
||||||
|
this.queue.push({
|
||||||
|
value: value,
|
||||||
|
css: `
|
||||||
|
display: inline-block;
|
||||||
|
background-color: ${this.bgColor};
|
||||||
|
color: ${this.color};
|
||||||
|
font-weight: bold;
|
||||||
|
padding: 3px 7px;
|
||||||
|
border-radius: 8px;
|
||||||
|
`
|
||||||
|
})
|
||||||
|
|
||||||
|
return this.ECHO_TOKEN
|
||||||
|
}
|
||||||
|
|
||||||
|
using = (consoleFunction) => {
|
||||||
|
function consoleFunctionProxy() {
|
||||||
|
var inputs = []
|
||||||
|
var modifiers = []
|
||||||
|
|
||||||
|
for (var i = 0; i < arguments.length; i++) {
|
||||||
|
if (arguments[i] === this.ECHO_TOKEN) {
|
||||||
|
var item = this.queue.shift()
|
||||||
|
|
||||||
|
inputs.push(("%c" + item.value), this.RESET_INPUT)
|
||||||
|
modifiers.push(item.css, this.RESET_CSS)
|
||||||
|
} else {
|
||||||
|
var arg = arguments[i]
|
||||||
|
|
||||||
|
if (typeof (arg) === "object" || typeof (arg) === "function") {
|
||||||
|
inputs.push("%o", this.RESET_INPUT)
|
||||||
|
modifiers.push(arg, this.RESET_CSS)
|
||||||
|
} else {
|
||||||
|
inputs.push(("%c" + arg), this.RESET_INPUT)
|
||||||
|
modifiers.push(this.RESET_CSS, this.RESET_CSS)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
consoleFunction(inputs.join(""), ...modifiers)
|
||||||
|
|
||||||
|
this.queue = []
|
||||||
|
}
|
||||||
|
|
||||||
|
return consoleFunctionProxy.bind(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
out = (method, ...args) => {
|
||||||
|
return this.using(console[method])(...args)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class InternalConsole {
|
||||||
|
constructor(params = {}) {
|
||||||
|
this.namespace = String(params.namespace)
|
||||||
|
this.params = params
|
||||||
|
this.echo = new Echo({
|
||||||
|
bgColor: this.params.bgColor,
|
||||||
|
color: this.params.textColor,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
_output(method, ...args) {
|
||||||
|
this.echo.out(
|
||||||
|
method,
|
||||||
|
this.echo.tagFormatting(`[${this.namespace}]`),
|
||||||
|
...args
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
log = (...args) => {
|
||||||
|
this._output("log", ...args)
|
||||||
|
}
|
||||||
|
|
||||||
|
warn = (...args) => {
|
||||||
|
this._output("warn", ...args)
|
||||||
|
}
|
||||||
|
|
||||||
|
error = (...args) => {
|
||||||
|
this._output("error", ...args)
|
||||||
|
}
|
||||||
|
|
||||||
|
debug = (...args) => {
|
||||||
|
this._output("debug", ...args)
|
||||||
|
}
|
||||||
|
|
||||||
|
info = (...args) => {
|
||||||
|
this._output("info", ...args)
|
||||||
|
}
|
||||||
|
|
||||||
|
trace = (...args) => {
|
||||||
|
this._output("trace", ...args)
|
||||||
|
}
|
||||||
|
|
||||||
|
time = (...args) => {
|
||||||
|
this._output("time", ...args)
|
||||||
|
}
|
||||||
|
|
||||||
|
timeEnd = (...args) => {
|
||||||
|
this._output("timeEnd", ...args)
|
||||||
|
}
|
||||||
|
}
|
47
src/classes/IsolatedContext/index.js
Normal file
47
src/classes/IsolatedContext/index.js
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
export default class IsolatedContext {
|
||||||
|
constructor(context = {}) {
|
||||||
|
this.isolatedKeys = Object.keys(context)
|
||||||
|
this.listeners = {
|
||||||
|
set: [],
|
||||||
|
get: [],
|
||||||
|
}
|
||||||
|
|
||||||
|
this.proxy = new Proxy(context, this.handler)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
subscribe = (event, listener) => {
|
||||||
|
this.listeners[event].push(listener)
|
||||||
|
}
|
||||||
|
|
||||||
|
getProxy = () => {
|
||||||
|
return this.proxy
|
||||||
|
}
|
||||||
|
|
||||||
|
handler = {
|
||||||
|
get: (target, name) => {
|
||||||
|
this.listeners["get"].forEach(listener => {
|
||||||
|
if (typeof listener === "function") {
|
||||||
|
listener(target, name)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return target[name]
|
||||||
|
},
|
||||||
|
set: (target, name, value) => {
|
||||||
|
if (this.isolatedKeys.includes(name)) {
|
||||||
|
console.error("Cannot assign an value to an isolated property", name, value)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
const assignation = Object.assign(target, { [name]: value })
|
||||||
|
|
||||||
|
this.listeners["set"].forEach(listener => {
|
||||||
|
if (typeof listener === "function") {
|
||||||
|
listener(target, name, value)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return assignation
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
102
src/core/index.jsx
Normal file
102
src/core/index.jsx
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
import InternalConsole from "../classes/InternalConsole"
|
||||||
|
import EventBus from "../classes/EventBus"
|
||||||
|
|
||||||
|
export default class Core {
|
||||||
|
constructor(ctx, params) {
|
||||||
|
this.ctx = ctx
|
||||||
|
this.params = params
|
||||||
|
|
||||||
|
this.console = new InternalConsole({
|
||||||
|
namespace: this.constructor.namespace ?? this.constructor.name,
|
||||||
|
bgColor: this.constructor.bgColor,
|
||||||
|
textColor: this.constructor.textColor,
|
||||||
|
})
|
||||||
|
|
||||||
|
this.eventBus = new EventBus({
|
||||||
|
id: this.constructor.namespace ?? this.constructor.name,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async _init() {
|
||||||
|
const namespace = this.constructor.namespace ?? this.constructor.name
|
||||||
|
|
||||||
|
let init_result = {
|
||||||
|
namespace,
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof this.onInitialize === "function") {
|
||||||
|
await this.onInitialize()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.public) {
|
||||||
|
init_result.public_context = this.bindableReadOnlyProxy(this.public)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof this.onEvents === "object") {
|
||||||
|
Object.entries(this.onEvents).forEach(([event, handler]) => {
|
||||||
|
this.eventBus.on(event, handler)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof this.onRuntimeEvents === "object") {
|
||||||
|
Object.entries(this.onRuntimeEvents).forEach(([event, handler]) => {
|
||||||
|
this.ctx.eventBus.on(event, handler)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof this.afterInitialize === "function") {
|
||||||
|
this.ctx.appendToInitializer(this.afterInitialize.bind(this))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof this.constructor.awaitEvents === "object") {
|
||||||
|
let awaitEvents = []
|
||||||
|
|
||||||
|
if (typeof this.constructor.awaitEvents === "string") {
|
||||||
|
awaitEvents = [this.constructor.awaitEvents]
|
||||||
|
} else if (Array.isArray(this.constructor.awaitEvents)) {
|
||||||
|
awaitEvents = this.constructor.awaitEvents
|
||||||
|
}
|
||||||
|
|
||||||
|
// await to events before initialize
|
||||||
|
await Promise.all(awaitEvents.map(([event, handler]) => {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
this.ctx.eventBus.once(event, (data) => {
|
||||||
|
handler(data)
|
||||||
|
resolve()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
return init_result
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bindable read only proxy for public methods of core.
|
||||||
|
*
|
||||||
|
* Bindable means that when a method is accessed, it will be bound to the
|
||||||
|
* core instance. This is useful when you want to pass a public method to
|
||||||
|
* an event or something, so when it is called, it will be called with
|
||||||
|
* the correct context.
|
||||||
|
*
|
||||||
|
* Read only means that the properties of the core cannot be modified
|
||||||
|
* through this proxy.
|
||||||
|
*
|
||||||
|
* @param {Object} obj
|
||||||
|
* @returns {Object}
|
||||||
|
*/
|
||||||
|
bindableReadOnlyProxy(obj) {
|
||||||
|
return new Proxy(obj, {
|
||||||
|
get: (target, prop) => {
|
||||||
|
if (typeof target[prop] === "function") {
|
||||||
|
return target[prop].bind(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
return target[prop]
|
||||||
|
},
|
||||||
|
set: (target, prop, value) => {
|
||||||
|
throw new Error("Cannot set property of public core method")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
34
src/extension/index.js
Normal file
34
src/extension/index.js
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import { Observable } from "object-observer"
|
||||||
|
|
||||||
|
export default class Extension {
|
||||||
|
constructor(appContext, mainContext) {
|
||||||
|
this.appContext = appContext
|
||||||
|
this.mainContext = mainContext
|
||||||
|
|
||||||
|
this.promises = []
|
||||||
|
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
__initializer() {
|
||||||
|
return new Promise(async (resolve, reject) => {
|
||||||
|
if (Array.isArray(this.depends)) {
|
||||||
|
this.depends.forEach((dependency) => {
|
||||||
|
const dependencyPromise = new Promise((resolve, reject) => {
|
||||||
|
Observable.observe(this.mainContext.ATTACHED_EXTENSIONS, (changes) => {
|
||||||
|
changes.forEach((change) => {
|
||||||
|
Array.from(change.object).includes(dependency) && resolve()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
this.promises.push(dependencyPromise)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
await Promise.all(this.promises)
|
||||||
|
|
||||||
|
return resolve()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
13
src/index.js
Normal file
13
src/index.js
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import Runtime from "./runtime"
|
||||||
|
|
||||||
|
import Core from "./core"
|
||||||
|
import Extension from "./extension"
|
||||||
|
|
||||||
|
import EventBus from "./classes/EventBus"
|
||||||
|
|
||||||
|
export {
|
||||||
|
Runtime,
|
||||||
|
Core,
|
||||||
|
Extension,
|
||||||
|
EventBus,
|
||||||
|
}
|
33
src/internals/debug/index.jsx
Normal file
33
src/internals/debug/index.jsx
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import React from "react"
|
||||||
|
|
||||||
|
import "./index.less"
|
||||||
|
|
||||||
|
export class DebugWindow extends React.Component {
|
||||||
|
renderObj = (obj) => {
|
||||||
|
return Object.entries(obj).map(([key, value]) => {
|
||||||
|
return <div className="entry" key={key}>
|
||||||
|
<div className="key">
|
||||||
|
<span>{key}</span>
|
||||||
|
</div>
|
||||||
|
<div className="value">
|
||||||
|
<span>{JSON.stringify(value)}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return <>
|
||||||
|
<div className="section" key="state" >
|
||||||
|
<div className="label">
|
||||||
|
<h4>State</h4>
|
||||||
|
</div>
|
||||||
|
<div className="content">
|
||||||
|
{
|
||||||
|
this.renderObj(this.props.ctx.STATES)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
}
|
61
src/internals/debug/index.less
Normal file
61
src/internals/debug/index.less
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
.__debugWindow {
|
||||||
|
font-family: "DM Mono", monospace;
|
||||||
|
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
|
||||||
|
background-color: rgba(100%, 100%, 100%, 0.3);
|
||||||
|
color: #000;
|
||||||
|
|
||||||
|
width: 100vw;
|
||||||
|
height: fit-content;
|
||||||
|
|
||||||
|
padding: 10px;
|
||||||
|
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
h3,
|
||||||
|
h4,
|
||||||
|
h5,
|
||||||
|
h6,
|
||||||
|
span,
|
||||||
|
p {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section {
|
||||||
|
padding: 0 10px;
|
||||||
|
|
||||||
|
.label {
|
||||||
|
font-weight: bold;
|
||||||
|
letter-spacing: 4px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
font-size: 14px;
|
||||||
|
margin-left: 10px;
|
||||||
|
|
||||||
|
.entry {
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
.key {
|
||||||
|
flex: 1;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value {
|
||||||
|
word-break: break-all;
|
||||||
|
flex: 2;
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
18
src/internals/renders/Crash/index.jsx
Normal file
18
src/internals/renders/Crash/index.jsx
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import React from "react"
|
||||||
|
|
||||||
|
export default (props) => {
|
||||||
|
return <div className="__eviteCrash">
|
||||||
|
<h1>Oops!</h1>
|
||||||
|
<p>Something went wrong, the application has a fatal crash.</p>
|
||||||
|
|
||||||
|
<pre>
|
||||||
|
{props.crash.message}
|
||||||
|
</pre>
|
||||||
|
|
||||||
|
<div className="__eviteCrash, description">
|
||||||
|
<code>
|
||||||
|
{props.crash.details}
|
||||||
|
</code>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
7
src/internals/renders/Initialization/index.jsx
Normal file
7
src/internals/renders/Initialization/index.jsx
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import React from "react"
|
||||||
|
|
||||||
|
export default (props) => {
|
||||||
|
<div className="_eviteAppInitialization">
|
||||||
|
Loading...
|
||||||
|
</div>
|
||||||
|
}
|
7
src/internals/renders/NotFound/index.jsx
Normal file
7
src/internals/renders/NotFound/index.jsx
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import React from "react"
|
||||||
|
|
||||||
|
export default () => {
|
||||||
|
return <div>
|
||||||
|
Not Found
|
||||||
|
</div>
|
||||||
|
}
|
3
src/internals/renders/index.js
Normal file
3
src/internals/renders/index.js
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export { default as Crash } from "./Crash"
|
||||||
|
export { default as Initialization } from "./Initialization"
|
||||||
|
export { default as NotFound } from "./NotFound"
|
25
src/internals/setToWindowContext/index.js
Normal file
25
src/internals/setToWindowContext/index.js
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
export default (params = {}, value, ...args) => {
|
||||||
|
let opts = {
|
||||||
|
key: params.key,
|
||||||
|
locked: params.locked ?? false,
|
||||||
|
enumerable: params.enumerable ?? true,
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof params === "string") {
|
||||||
|
opts.key = params
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof opts.key === "undefined") {
|
||||||
|
throw new Error("key is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (args.length > 0) {
|
||||||
|
value = value(...args)
|
||||||
|
}
|
||||||
|
|
||||||
|
return Object.defineProperty(window.app, opts.key, {
|
||||||
|
value,
|
||||||
|
enumerable: opts.enumerable,
|
||||||
|
configurable: opts.locked
|
||||||
|
})
|
||||||
|
}
|
46
src/internals/style/index.css
Normal file
46
src/internals/style/index.css
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
@import url('https://fonts.googleapis.com/css2?family=DM+Mono:ital,wght@0,300;0,400;0,500;1,300;1,400;1,500&display=swap');
|
||||||
|
|
||||||
|
.__eviteCrash {
|
||||||
|
position: absolute;
|
||||||
|
|
||||||
|
display : flex;
|
||||||
|
flex-direction : column;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
top : 0;
|
||||||
|
left: 0;
|
||||||
|
|
||||||
|
width : 80vw;
|
||||||
|
height: fit-content;
|
||||||
|
|
||||||
|
margin : 10px;
|
||||||
|
padding: 20px;
|
||||||
|
|
||||||
|
border-radius: 8px;
|
||||||
|
|
||||||
|
font-family : "DM Mono", monospace;
|
||||||
|
background-color: white;
|
||||||
|
color : #2d2d2d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.__eviteCrash h1 {
|
||||||
|
margin : 0;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.__eviteCrash p {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.__eviteCrash .description {
|
||||||
|
margin: 40px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.__eviteCrash .description code {
|
||||||
|
background-color: #2d2d2d;
|
||||||
|
color : white;
|
||||||
|
|
||||||
|
padding: 30px 10px;
|
||||||
|
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
532
src/runtime.jsx
Normal file
532
src/runtime.jsx
Normal file
@ -0,0 +1,532 @@
|
|||||||
|
import pkgJson from "../package.json"
|
||||||
|
|
||||||
|
import React from "react"
|
||||||
|
import ReactDOM from "react-dom"
|
||||||
|
import { createRoot } from "react-dom/client"
|
||||||
|
|
||||||
|
import { createBrowserHistory } from "history"
|
||||||
|
import { Observable } from "object-observer"
|
||||||
|
|
||||||
|
import Extension from "./extension"
|
||||||
|
|
||||||
|
import EventBus from "./classes/EventBus"
|
||||||
|
import InternalConsole from "./classes/InternalConsole"
|
||||||
|
|
||||||
|
import isMobile from "./utils/isMobile"
|
||||||
|
|
||||||
|
import * as StaticRenders from "./internals/renders"
|
||||||
|
import "./internals/style/index.css"
|
||||||
|
|
||||||
|
export default class EviteRuntime {
|
||||||
|
Flags = window.flags = Observable.from({
|
||||||
|
debug: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
INTERNAL_CONSOLE = new InternalConsole({
|
||||||
|
namespace: "Runtime",
|
||||||
|
bgColor: "bgMagenta",
|
||||||
|
})
|
||||||
|
|
||||||
|
EXTENSIONS = Object()
|
||||||
|
CORES = Object()
|
||||||
|
|
||||||
|
PublicContext = window.app = Object()
|
||||||
|
|
||||||
|
ExtensionsPublicContext = Object()
|
||||||
|
CoresPublicContext = Object()
|
||||||
|
|
||||||
|
STATES = Observable.from({
|
||||||
|
LOAD_STATE: "early",
|
||||||
|
|
||||||
|
INITIALIZER_TASKS: [],
|
||||||
|
|
||||||
|
LOADED_CORES: [],
|
||||||
|
|
||||||
|
ATTACHED_EXTENSIONS: [],
|
||||||
|
REJECTED_EXTENSIONS: [],
|
||||||
|
|
||||||
|
INITIALIZATION_START: null,
|
||||||
|
INITIALIZATION_STOP: null,
|
||||||
|
INITIALIZATION_TOOKS: null,
|
||||||
|
})
|
||||||
|
|
||||||
|
APP_RENDERER = null
|
||||||
|
SPLASH_RENDERER = null
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
App,
|
||||||
|
Params = {
|
||||||
|
renderMount: "root",
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
this.INTERNAL_CONSOLE.log(`Using React ${React.version}`)
|
||||||
|
|
||||||
|
this.AppComponent = App
|
||||||
|
this.Params = Params
|
||||||
|
|
||||||
|
// toogle splash
|
||||||
|
this.attachSplashScreen()
|
||||||
|
|
||||||
|
// controllers
|
||||||
|
this.root = createRoot(document.getElementById(this.Params.renderMount ?? "root"))
|
||||||
|
this.history = this.registerPublicMethod({ key: "history", locked: true }, createBrowserHistory())
|
||||||
|
this.eventBus = this.registerPublicMethod({ key: "eventBus", locked: true }, new EventBus())
|
||||||
|
|
||||||
|
window.app.cores = new Proxy(this.CoresPublicContext, {
|
||||||
|
get: (target, key) => {
|
||||||
|
return target[key]
|
||||||
|
},
|
||||||
|
set: (target, key, value) => {
|
||||||
|
throw new Error("You can't set a core value")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
window.app.extensions = new Proxy(this.ExtensionsPublicContext, {
|
||||||
|
get: (target, key) => {
|
||||||
|
return target[key]
|
||||||
|
},
|
||||||
|
set: (target, key, value) => {
|
||||||
|
throw new Error("You can't set a extension value")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
this.registerPublicMethod({ key: "isMobile", locked: true }, isMobile())
|
||||||
|
this.registerPublicMethod({ key: "__version", locked: true }, pkgJson.version)
|
||||||
|
|
||||||
|
if (typeof this.AppComponent.splashAwaitEvent === "string") {
|
||||||
|
this.eventBus.on(this.AppComponent.splashAwaitEvent, () => {
|
||||||
|
this.detachSplashScreen()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [key, fn] of Object.entries(this.internalEvents)) {
|
||||||
|
this.eventBus.on(key, fn)
|
||||||
|
}
|
||||||
|
|
||||||
|
// emit attached extensions change events
|
||||||
|
Observable.observe(this.STATES.ATTACHED_EXTENSIONS, (changes) => {
|
||||||
|
changes.forEach((change) => {
|
||||||
|
if (change.type === "insert") {
|
||||||
|
this.eventBus.emit(`runtime.extension.attached`, change)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// emit rejected extensions change events
|
||||||
|
Observable.observe(this.STATES.REJECTED_EXTENSIONS, (changes) => {
|
||||||
|
changes.forEach((change) => {
|
||||||
|
if (change.type === "insert") {
|
||||||
|
this.eventBus.emit(`runtime.extension.rejected`, change)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
return this.initialize().catch((error) => {
|
||||||
|
this.eventBus.emit("runtime.initialize.crash", error)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
internalEvents = {
|
||||||
|
"runtime.initialize.start": () => {
|
||||||
|
this.STATES.LOAD_STATE = "initializing"
|
||||||
|
this.STATES.INITIALIZATION_START = performance.now()
|
||||||
|
},
|
||||||
|
"runtime.initialize.finish": () => {
|
||||||
|
const time = performance.now()
|
||||||
|
|
||||||
|
this.STATES.INITIALIZATION_STOP = time
|
||||||
|
|
||||||
|
if (this.STATES.INITIALIZATION_START) {
|
||||||
|
this.STATES.INITIALIZATION_TOOKS = time - this.STATES.INITIALIZATION_START
|
||||||
|
}
|
||||||
|
|
||||||
|
this.STATES.LOAD_STATE = "initialized"
|
||||||
|
},
|
||||||
|
"runtime.initialize.crash": (error) => {
|
||||||
|
this.STATES.LOAD_STATE = "crashed"
|
||||||
|
|
||||||
|
if (this.SPLASH_RENDERER) {
|
||||||
|
this.detachSplashScreen()
|
||||||
|
}
|
||||||
|
|
||||||
|
this.INTERNAL_CONSOLE.error("Runtime crashed on initialization \n", error)
|
||||||
|
|
||||||
|
// render crash
|
||||||
|
this.render(this.AppComponent.staticRenders?.Crash ?? StaticRenders.Crash, {
|
||||||
|
crash: {
|
||||||
|
message: "Runtime crashed on initialization",
|
||||||
|
details: error.toString(),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
"runtime.crash": (crash) => {
|
||||||
|
this.STATES.LOAD_STATE = "crashed"
|
||||||
|
|
||||||
|
if (this.SPLASH_RENDERER) {
|
||||||
|
this.detachSplashScreen()
|
||||||
|
}
|
||||||
|
|
||||||
|
// render crash
|
||||||
|
this.render(this.AppComponent.staticRenders?.Crash ?? StaticRenders.Crash, {
|
||||||
|
crash
|
||||||
|
})
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
bindObjects = async (bind, events, parent) => {
|
||||||
|
let boundEvents = {}
|
||||||
|
|
||||||
|
for await (let [event, handler] of Object.entries(events)) {
|
||||||
|
if (typeof handler === "object") {
|
||||||
|
boundEvents[event] = await this.bindObjects(bind, handler, parent)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof handler === "function") {
|
||||||
|
boundEvents[event] = handler.bind(bind)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return boundEvents
|
||||||
|
}
|
||||||
|
|
||||||
|
async initialize() {
|
||||||
|
this.eventBus.emit("runtime.initialize.start")
|
||||||
|
|
||||||
|
await this.initializeCores()
|
||||||
|
|
||||||
|
await this.performInitializerTasks()
|
||||||
|
|
||||||
|
// call early app initializer
|
||||||
|
if (typeof this.AppComponent.initialize === "function") {
|
||||||
|
await this.AppComponent.initialize.apply(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
// handle app events handlers registration
|
||||||
|
if (typeof this.AppComponent.publicEvents === "object") {
|
||||||
|
for await (let [event, handler] of Object.entries(this.AppComponent.publicEvents)) {
|
||||||
|
this.eventBus.on(event, handler.bind(this))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// handle app public methods registration
|
||||||
|
if (typeof this.AppComponent.publicMethods === "object") {
|
||||||
|
await this.registerPublicMethods(this.AppComponent.publicMethods)
|
||||||
|
}
|
||||||
|
|
||||||
|
// emit initialize finish event
|
||||||
|
this.eventBus.emit("runtime.initialize.finish")
|
||||||
|
|
||||||
|
this.render()
|
||||||
|
|
||||||
|
if (!this.AppComponent.splashAwaitEvent) {
|
||||||
|
this.detachSplashScreen()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
initializeCore = async (core) => {
|
||||||
|
if (!core.constructor) {
|
||||||
|
this.INTERNAL_CONSOLE.error(`Core [${core.name}] is not a class`)
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const namespace = core.namespace ?? core.name
|
||||||
|
|
||||||
|
this.eventBus.emit(`runtime.initialize.core.${namespace}.start`)
|
||||||
|
|
||||||
|
// construct class
|
||||||
|
let coreInstance = new core(this)
|
||||||
|
|
||||||
|
// set core to context
|
||||||
|
this.CORES[namespace] = coreInstance
|
||||||
|
|
||||||
|
const initializationResult = await coreInstance._init()
|
||||||
|
|
||||||
|
if (!initializationResult) {
|
||||||
|
this.INTERNAL_CONSOLE.warn(`[${namespace}] initializes without returning a result.`)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (initializationResult.public_context) {
|
||||||
|
this.CoresPublicContext[initializationResult.namespace] = initializationResult.public_context
|
||||||
|
}
|
||||||
|
|
||||||
|
// emit event
|
||||||
|
this.eventBus.emit(`runtime.initialize.core.${namespace}.finish`)
|
||||||
|
|
||||||
|
// register internal core
|
||||||
|
this.STATES.LOADED_CORES.push(namespace)
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
initializeCores = async () => {
|
||||||
|
try {
|
||||||
|
const coresPaths = {
|
||||||
|
...import.meta.glob("/src/cores/*/*.core.jsx"),
|
||||||
|
...import.meta.glob("/src/cores/*/*.core.js"),
|
||||||
|
...import.meta.glob("/src/cores/*/*.core.ts"),
|
||||||
|
...import.meta.glob("/src/cores/*/*.core.tsx"),
|
||||||
|
}
|
||||||
|
|
||||||
|
const coresKeys = Object.keys(coresPaths)
|
||||||
|
|
||||||
|
if (coresKeys.length === 0) {
|
||||||
|
this.INTERNAL_CONSOLE.warn(`Skipping cores initialization. No cores found.`)
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// import all cores
|
||||||
|
let cores = await Promise.all(coresKeys.map(async (key) => {
|
||||||
|
const core = await coresPaths[key]().catch((err) => {
|
||||||
|
this.INTERNAL_CONSOLE.warn(`Cannot load core from ${key}.`, err)
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
|
||||||
|
return core.default ?? core
|
||||||
|
}))
|
||||||
|
|
||||||
|
cores = cores.filter((core) => {
|
||||||
|
return core.constructor
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!cores) {
|
||||||
|
this.INTERNAL_CONSOLE.warn(`Skipping cores initialization. No valid cores found.`)
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
this.eventBus.emit(`runtime.initialize.cores.start`)
|
||||||
|
|
||||||
|
if (!Array.isArray(cores)) {
|
||||||
|
this.INTERNAL_CONSOLE.error(`Cannot initialize cores, cause it is not an array. Core dependency is not supported yet. You must use an array to define your core load queue.`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// sort cores by dependencies
|
||||||
|
cores.forEach((core) => {
|
||||||
|
if (core.dependencies) {
|
||||||
|
core.dependencies.forEach((dependency) => {
|
||||||
|
// find dependency
|
||||||
|
const dependencyIndex = cores.findIndex((_core) => {
|
||||||
|
return (_core.namespace ?? _core.name) === dependency
|
||||||
|
})
|
||||||
|
|
||||||
|
if (dependencyIndex === -1) {
|
||||||
|
this.INTERNAL_CONSOLE.error(`Cannot find dependency [${dependency}] for core [${core.name}]`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// move dependency to top
|
||||||
|
cores.splice(0, 0, cores.splice(dependencyIndex, 1)[0])
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
for await (let coreClass of cores) {
|
||||||
|
await this.initializeCore(coreClass)
|
||||||
|
}
|
||||||
|
|
||||||
|
// emit event
|
||||||
|
this.eventBus.emit(`runtime.initialize.cores.finish`)
|
||||||
|
} catch (error) {
|
||||||
|
this.eventBus.emit(`runtime.initialize.cores.failed`, error)
|
||||||
|
|
||||||
|
// make sure to throw that, app must crash if core fails to load
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
appendToInitializer = (task) => {
|
||||||
|
let tasks = []
|
||||||
|
|
||||||
|
if (Array.isArray(task)) {
|
||||||
|
tasks = task
|
||||||
|
} else {
|
||||||
|
tasks.push(task)
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.forEach((_task) => {
|
||||||
|
if (typeof _task === "function") {
|
||||||
|
this.STATES.INITIALIZER_TASKS.push(_task)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
performInitializerTasks = async () => {
|
||||||
|
if (this.STATES.INITIALIZER_TASKS.length === 0) {
|
||||||
|
this.INTERNAL_CONSOLE.warn("No initializer tasks found, skipping...")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
for await (let task of this.STATES.INITIALIZER_TASKS) {
|
||||||
|
if (typeof task === "function") {
|
||||||
|
try {
|
||||||
|
await task(this)
|
||||||
|
} catch (error) {
|
||||||
|
this.INTERNAL_CONSOLE.error(`Failed to perform initializer task >`, error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CONTEXT CONTROL
|
||||||
|
registerPublicMethods = async (methods) => {
|
||||||
|
if (typeof methods !== "object") {
|
||||||
|
this.INTERNAL_CONSOLE.error("Methods must be an object")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const boundedPublicMethods = await this.bindObjects(this, methods)
|
||||||
|
|
||||||
|
for await (let [methodName, fn] of Object.entries(boundedPublicMethods)) {
|
||||||
|
this.registerPublicMethod({
|
||||||
|
key: methodName,
|
||||||
|
locked: true,
|
||||||
|
}, fn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
registerPublicMethod = (params = {}, value, ...args) => {
|
||||||
|
let opts = {
|
||||||
|
key: params.key,
|
||||||
|
locked: params.locked ?? false,
|
||||||
|
enumerable: params.enumerable ?? true,
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof params === "string") {
|
||||||
|
opts.key = params
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof opts.key === "undefined") {
|
||||||
|
throw new Error("key is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (args.length > 0) {
|
||||||
|
value = value(...args)
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
Object.defineProperty(this.PublicContext, opts.key, {
|
||||||
|
value,
|
||||||
|
enumerable: opts.enumerable,
|
||||||
|
configurable: opts.locked
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
this.INTERNAL_CONSOLE.error(error)
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.PublicContext[opts.key]
|
||||||
|
}
|
||||||
|
|
||||||
|
// EXTENSIONS CONTROL
|
||||||
|
initializeExtension = (extension) => {
|
||||||
|
return new Promise(async (resolve, reject) => {
|
||||||
|
if (typeof extension !== "function") {
|
||||||
|
return reject({
|
||||||
|
reason: "EXTENSION_NOT_VALID_CLASS",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
extension = new extension(this.ExtensionsContext.getProxy(), this)
|
||||||
|
|
||||||
|
const extensionName = extension.refName ?? extension.constructor.name
|
||||||
|
|
||||||
|
if (extension instanceof Extension) {
|
||||||
|
// good for u
|
||||||
|
} else {
|
||||||
|
this.STATES.REJECTED_EXTENSIONS = [extensionName, ...this.STATES.REJECTED_EXTENSIONS]
|
||||||
|
|
||||||
|
return reject({
|
||||||
|
name: extensionName,
|
||||||
|
reason: "EXTENSION_NOT_VALID_INSTANCE",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// await to extension initializer
|
||||||
|
await extension.__initializer()
|
||||||
|
|
||||||
|
// appends initializers
|
||||||
|
if (typeof extension.initializers !== "undefined") {
|
||||||
|
for await (let initializer of extension.initializers) {
|
||||||
|
await initializer.apply(this.ExtensionsContext.getProxy(), initializer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// set window context
|
||||||
|
if (typeof extension.publicMethods === "object") {
|
||||||
|
Object.keys(extension.publicMethods).forEach((key) => {
|
||||||
|
if (typeof extension.publicMethods[key] === "function") {
|
||||||
|
extension.publicMethods[key].bind(this.ExtensionsContext.getProxy())
|
||||||
|
}
|
||||||
|
|
||||||
|
this.registerPublicMethod({ key }, extension.publicMethods[key])
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// update attached extensions
|
||||||
|
this.STATES.ATTACHED_EXTENSIONS.push(extensionName)
|
||||||
|
|
||||||
|
// set extension context
|
||||||
|
this.EXTENSIONS[extensionName] = extension
|
||||||
|
|
||||||
|
return resolve()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
attachSplashScreen = () => {
|
||||||
|
// create a new div inside the container
|
||||||
|
let elementContainer = document.getElementById("splash-screen")
|
||||||
|
|
||||||
|
if (!elementContainer) {
|
||||||
|
elementContainer = document.createElement("div")
|
||||||
|
|
||||||
|
// set the id of the new div
|
||||||
|
elementContainer.id = "splash-screen"
|
||||||
|
|
||||||
|
document.body.appendChild(elementContainer)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.AppComponent.staticRenders?.Initialization) {
|
||||||
|
this.SPLASH_RENDERER = ReactDOM.render(React.createElement((this.AppComponent.staticRenders?.Initialization), {
|
||||||
|
states: this.STATES,
|
||||||
|
}), elementContainer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
detachSplashScreen = async () => {
|
||||||
|
const container = document.getElementById("splash-screen")
|
||||||
|
|
||||||
|
if (container) {
|
||||||
|
if (this.SPLASH_RENDERER && typeof this.SPLASH_RENDERER.onUnmount) {
|
||||||
|
await this.SPLASH_RENDERER.onUnmount()
|
||||||
|
}
|
||||||
|
|
||||||
|
ReactDOM.unmountComponentAtNode(container)
|
||||||
|
container.remove()
|
||||||
|
|
||||||
|
this.SPLASH_RENDERER = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RENDER METHOD
|
||||||
|
render(component = this.AppComponent, props = {}) {
|
||||||
|
this.APP_RENDERER = React.createElement(
|
||||||
|
component,
|
||||||
|
{
|
||||||
|
runtime: new Proxy(this, {
|
||||||
|
get: (target, prop) => {
|
||||||
|
return target[prop]
|
||||||
|
},
|
||||||
|
set: (target, prop, value) => {
|
||||||
|
throw new Error("Cannot set property of runtime")
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
cores: this.CORES,
|
||||||
|
...props,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
this.root.render(this.APP_RENDERER)
|
||||||
|
}
|
||||||
|
}
|
25
src/utils/bindProps/index.js
Normal file
25
src/utils/bindProps/index.js
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import React from "react"
|
||||||
|
|
||||||
|
export default (props) => {
|
||||||
|
const context = {}
|
||||||
|
|
||||||
|
Object.keys(props).forEach((key) => {
|
||||||
|
if (key === "children") {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof props[key] === "function") {
|
||||||
|
props[key] = props[key]()
|
||||||
|
}
|
||||||
|
|
||||||
|
context[key] = props[key]
|
||||||
|
})
|
||||||
|
|
||||||
|
if (Array.isArray(props.children)) {
|
||||||
|
return props.children.map((children) => {
|
||||||
|
return React.cloneElement(children, { ...context })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return React.cloneElement(props.children, { ...context })
|
||||||
|
}
|
111
src/utils/classAggregation/index.js
Normal file
111
src/utils/classAggregation/index.js
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
/*
|
||||||
|
** Aggregation -- Aggregation of Base Class and Mixin Classes
|
||||||
|
** Copyright (c) 2015-2021 Dr. Ralf S. Engelschall <rse@engelschall.com>
|
||||||
|
**
|
||||||
|
** Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
** a copy of this software and associated documentation files (the
|
||||||
|
** "Software"), to deal in the Software without restriction, including
|
||||||
|
** without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
** distribute, sublicense, and/or sell copies of the Software, and to
|
||||||
|
** permit persons to whom the Software is furnished to do so, subject to
|
||||||
|
** the following conditions:
|
||||||
|
**
|
||||||
|
** The above copyright notice and this permission notice shall be included
|
||||||
|
** in all copies or substantial portions of the Software.
|
||||||
|
**
|
||||||
|
** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
** EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
** MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||||
|
** IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||||
|
** CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||||
|
** TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||||
|
** SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export default (base, ...mixins) => {
|
||||||
|
let aggregate = class __Aggregate extends base {
|
||||||
|
constructor(...args) {
|
||||||
|
super(...args)
|
||||||
|
|
||||||
|
mixins.forEach((mixin) => {
|
||||||
|
if (typeof mixin.prototype.initializer === "function") {
|
||||||
|
mixin.prototype.initializer.apply(this, args)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof mixin.prototype.selfEnqueueFnApply === "function") {
|
||||||
|
if (typeof this.__fnHandlers !== "object") {
|
||||||
|
this.__fnHandlers = {}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlers = mixin.prototype.selfEnqueueFnApply.apply(this, args)
|
||||||
|
|
||||||
|
if (typeof handlers === "object") {
|
||||||
|
const keys = Object.keys(handlers)
|
||||||
|
|
||||||
|
keys.forEach((key) => {
|
||||||
|
this.__fnHandlers[key] = handlers[key]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (typeof base.initialize === "function") {
|
||||||
|
try {
|
||||||
|
base.initialize.apply(this, args)
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof base.windowContext === "function") {
|
||||||
|
try {
|
||||||
|
const returnedValues = base.windowContext.apply(this, args)
|
||||||
|
|
||||||
|
if (typeof returnedValues === "object") {
|
||||||
|
const keys = Object.keys(returnedValues)
|
||||||
|
|
||||||
|
keys.forEach((key) => {
|
||||||
|
this.contexts.window[key] = returnedValues[key]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof base.appContext === "function") {
|
||||||
|
try {
|
||||||
|
const returnedValues = base.appContext.apply(this, args)
|
||||||
|
|
||||||
|
if (typeof returnedValues === "object") {
|
||||||
|
const keys = Object.keys(returnedValues)
|
||||||
|
|
||||||
|
keys.forEach((key) => {
|
||||||
|
this.contexts.app[key] = returnedValues[key]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let copyProps = (target, source) => {
|
||||||
|
Object.getOwnPropertyNames(source)
|
||||||
|
.concat(Object.getOwnPropertySymbols(source))
|
||||||
|
.forEach((prop) => {
|
||||||
|
if (prop.match(/^(?:initializer|constructor|prototype|arguments|caller|name|bind|call|apply|toString|length)$/)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
Object.defineProperty(target, prop, Object.getOwnPropertyDescriptor(source, prop))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
mixins.forEach((mixin) => {
|
||||||
|
copyProps(aggregate.prototype, mixin.prototype)
|
||||||
|
copyProps(aggregate, mixin)
|
||||||
|
})
|
||||||
|
|
||||||
|
return aggregate
|
||||||
|
}
|
22
src/utils/deepInmutableProxyObj/index.js
Normal file
22
src/utils/deepInmutableProxyObj/index.js
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
function deepInmutableProxyObj(obj, binder, deep = 0) {
|
||||||
|
return new Proxy(obj, {
|
||||||
|
get: (target, prop) => {
|
||||||
|
if (typeof target[prop] === "object" && target[prop] !== null) {
|
||||||
|
return deepInmutableProxyObj(target[prop], binder, deep + 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof target[prop] === "function") {
|
||||||
|
return target[prop].bind(target)
|
||||||
|
}
|
||||||
|
|
||||||
|
return target[prop]
|
||||||
|
},
|
||||||
|
set: (target, prop, value) => {
|
||||||
|
console.error("You can't modify a inmutable proxy")
|
||||||
|
|
||||||
|
return target[prop]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export default deepInmutableProxyObj
|
3
src/utils/isMobile/index.js
Normal file
3
src/utils/isMobile/index.js
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export default function isMobile() {
|
||||||
|
return window.navigator.userAgent === "capacitor" || Math.min(window.screen.width, window.screen.height) < 768 || navigator.userAgent.indexOf("Mobi") > -1
|
||||||
|
}
|
51
src/utils/url/index.js
Normal file
51
src/utils/url/index.js
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
const S = "/"
|
||||||
|
|
||||||
|
export function withPrefix(string, prefix) {
|
||||||
|
return string.startsWith(prefix) ? string : prefix + string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function withoutPrefix(string, prefix) {
|
||||||
|
return string.startsWith(prefix) ? string.slice(prefix.length) : string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function withSuffix(string, suffix) {
|
||||||
|
return string.endsWith(suffix) ? string : string + suffix
|
||||||
|
}
|
||||||
|
|
||||||
|
export function withoutSuffix(string, suffix) {
|
||||||
|
return string.endsWith(suffix)
|
||||||
|
? string.slice(0, -1 * suffix.length)
|
||||||
|
: string + suffix
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createUrl(urlLike) {
|
||||||
|
if (urlLike instanceof URL) {
|
||||||
|
return urlLike
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(urlLike || "").includes("://")) {
|
||||||
|
urlLike = "http://e.g" + withPrefix(urlLike, S)
|
||||||
|
}
|
||||||
|
|
||||||
|
return new URL(urlLike)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function joinPaths(...paths) {
|
||||||
|
return paths.reduce((acc, path) => acc + path, "").replace(/\/\//g, S)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getFullPath(url, routeBase) {
|
||||||
|
url = typeof url === "string" ? createUrl(url) : url
|
||||||
|
let fullPath = withoutPrefix(url.href, url.origin)
|
||||||
|
|
||||||
|
if (routeBase) {
|
||||||
|
const parts = fullPath.split(S)
|
||||||
|
if (parts[1] === routeBase.replace(/\//g, "")) {
|
||||||
|
parts.splice(1, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
fullPath = parts.join(S)
|
||||||
|
}
|
||||||
|
|
||||||
|
return fullPath
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user