apprun 3.36.0 → 3.37.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +28 -0
- package/README.md +143 -25
- package/WHATSNEW.md +29 -12
- package/apprun-book.jpg +0 -0
- package/apprun.d.ts +136 -46
- package/cli/app.js +29 -0
- package/cli/index.html +13 -0
- package/{apprun-cli.js → cli/index.js} +8 -14
- package/dist/apprun-code.js +2 -0
- package/dist/apprun-code.js.map +1 -0
- package/dist/apprun-dev-tools.js +1 -2
- package/dist/apprun-dev-tools.js.map +1 -1
- package/dist/apprun-html.esm.js +7 -7
- package/dist/apprun-html.esm.js.map +1 -1
- package/dist/apprun-html.js +1 -1
- package/dist/apprun-html.js.map +1 -1
- package/dist/apprun-play-html.esm.js +7 -7
- package/dist/apprun-play-html.esm.js.map +1 -1
- package/dist/apprun-play.js +1 -1
- package/dist/apprun-play.js.map +1 -1
- package/dist/apprun.esm.js +1 -1
- package/dist/apprun.esm.js.map +1 -1
- package/dist/apprun.js +1 -1
- package/dist/apprun.js.map +1 -1
- package/dist/createState.js +2 -0
- package/dist/createState.js.map +1 -0
- package/esm/add-components.js +90 -0
- package/esm/add-components.js.map +1 -0
- package/esm/app.js +15 -8
- package/esm/app.js.map +1 -1
- package/esm/apprun-code.js +51 -16
- package/esm/apprun-code.js.map +1 -1
- package/esm/apprun-dev-tools.js +32 -32
- package/esm/apprun-dev-tools.js.map +1 -1
- package/esm/apprun-play.js +50 -17
- package/esm/apprun-play.js.map +1 -1
- package/esm/apprun.js +42 -11
- package/esm/apprun.js.map +1 -1
- package/esm/component.js +17 -20
- package/esm/component.js.map +1 -1
- package/esm/createState.js +9 -0
- package/esm/createState.js.map +1 -0
- package/esm/decorator.js.map +1 -1
- package/esm/directive.js +1 -1
- package/esm/directive.js.map +1 -1
- package/esm/router.js +241 -18
- package/esm/router.js.map +1 -1
- package/esm/shadow.js +1 -1
- package/esm/shadow.js.map +1 -1
- package/esm/type-utils.js +2 -3
- package/esm/type-utils.js.map +1 -1
- package/esm/vdom-my-new.js +327 -0
- package/esm/vdom-my-new.js.map +1 -0
- package/esm/vdom-my-prop-attr.js +227 -0
- package/esm/vdom-my-prop-attr.js.map +1 -0
- package/esm/vdom-my.js +114 -150
- package/esm/vdom-my.js.map +1 -1
- package/esm/vdom-patch.js +1 -1
- package/esm/vdom-patch.js.map +1 -1
- package/esm/version.js +1 -1
- package/esm/web-component.js +3 -4
- package/esm/web-component.js.map +1 -1
- package/index.html +5 -2
- package/jest.config.js +2 -2
- package/jest.setup.js +29 -3
- package/jsx-runtime.js +1 -1
- package/jsx-runtime.js.map +1 -1
- package/package.json +14 -14
- package/src/add-components.ts +103 -0
- package/src/app.ts +10 -19
- package/src/apprun-code.tsx +48 -10
- package/src/apprun-dev-tools.tsx +23 -27
- package/src/apprun-play.tsx +46 -9
- package/src/apprun.ts +49 -40
- package/src/component.ts +15 -8
- package/src/createState.ts +11 -0
- package/src/decorator.ts +2 -1
- package/src/router.ts +261 -17
- package/src/shadow.tsx +1 -1
- package/src/tsconfig.json +2 -2
- package/src/types.ts +62 -2
- package/src/vdom-my-new.ts +311 -0
- package/src/vdom-my-prop-attr.ts +241 -0
- package/src/vdom-my.ts +109 -134
- package/src/version.ts +1 -1
- package/src/web-component.ts +4 -10
- package/tsconfig.jest.json +15 -6
- package/tsconfig.json +3 -3
- package/webpack.config.cjs +3 -1
- package/cli/export.js +0 -92
- package/cli/import.js +0 -68
- package/plan/plan-apprun-bugfixes.md +0 -207
- package/src/types/apprun.d.ts +0 -56
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { ComponentRoute, IApp } from './types';
|
|
2
|
+
import app from './app'; // ADD: Global app instance access
|
|
3
|
+
|
|
4
|
+
// Type guard functions using the enhanced type system
|
|
5
|
+
function isComponentInstance(obj: any): boolean {
|
|
6
|
+
return obj && typeof obj === 'object' && typeof obj.mount === 'function';
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function isComponentConstructor(fn: any): boolean {
|
|
10
|
+
return typeof fn === 'function' &&
|
|
11
|
+
fn.prototype &&
|
|
12
|
+
fn.prototype.constructor === fn &&
|
|
13
|
+
(fn.prototype.mount !== undefined ||
|
|
14
|
+
fn.prototype.state !== undefined ||
|
|
15
|
+
fn.prototype.view !== undefined);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function isFactoryFunction(fn: any): boolean {
|
|
19
|
+
return typeof fn === 'function' && !isComponentConstructor(fn);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Recursive function resolution with enhanced type checking
|
|
23
|
+
async function resolveComponent(component: any, maxDepth = 3): Promise<any> {
|
|
24
|
+
let resolved = component;
|
|
25
|
+
let depth = 0;
|
|
26
|
+
|
|
27
|
+
while (isFactoryFunction(resolved) && depth < maxDepth) {
|
|
28
|
+
try {
|
|
29
|
+
const result = await resolved();
|
|
30
|
+
if (result === resolved) break; // Prevent infinite loops
|
|
31
|
+
resolved = result;
|
|
32
|
+
depth++;
|
|
33
|
+
} catch (error) {
|
|
34
|
+
console.error(`Error executing component function: ${error}`);
|
|
35
|
+
break;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return resolved;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export default async (element: HTMLElement | string, components: ComponentRoute) => {
|
|
43
|
+
for (const [route, component] of Object.entries(components)) {
|
|
44
|
+
if (!component || !route) {
|
|
45
|
+
console.error(`Invalid component configuration: component=${component}, route=${route}`);
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Check if it's a direct component instance
|
|
50
|
+
if (isComponentInstance(component)) {
|
|
51
|
+
const options = { route };
|
|
52
|
+
(component as any).mount(element, options);
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Check if it's a component class constructor
|
|
57
|
+
if (isComponentConstructor(component)) {
|
|
58
|
+
const instance = new (component as any)();
|
|
59
|
+
const options = { route };
|
|
60
|
+
instance.mount(element, options);
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// At this point it must be a function - resolve it
|
|
65
|
+
if (isFactoryFunction(component)) {
|
|
66
|
+
// Resolve the function to see what it returns
|
|
67
|
+
let resolved = await resolveComponent(component);
|
|
68
|
+
|
|
69
|
+
// Check if resolved result is a component instance
|
|
70
|
+
if (isComponentInstance(resolved)) {
|
|
71
|
+
const options = { route };
|
|
72
|
+
resolved.mount(element, options);
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Check if resolved result is a component constructor
|
|
77
|
+
if (isComponentConstructor(resolved)) {
|
|
78
|
+
const instance = new resolved();
|
|
79
|
+
const options = { route };
|
|
80
|
+
instance.mount(element, options);
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// If resolved result is still a function or anything else, treat original as event handler with render wrapper
|
|
85
|
+
app.on(route, (...args: any[]) => {
|
|
86
|
+
const result = (component as any)(...args);
|
|
87
|
+
if (typeof element === 'string') {
|
|
88
|
+
element = document.querySelector(element) as HTMLElement;
|
|
89
|
+
if (!element) {
|
|
90
|
+
console.error(`Element not found: ${element}`);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return (app as unknown as IApp).render(element, result);
|
|
95
|
+
});
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// If we get here, it's an invalid component type
|
|
100
|
+
console.error(`Invalid component: component must be a class, instance, or function that returns a class/instance`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
package/src/app.ts
CHANGED
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
* - on(): Subscribe to events with options (once, delay, global)
|
|
7
7
|
* - off(): Unsubscribe from events with proper cleanup
|
|
8
8
|
* - run(): Publish events synchronously with error handling
|
|
9
|
-
* - runAsync(): Publish events asynchronously with Promise support
|
|
10
|
-
* - query():
|
|
9
|
+
* - runAsync(): Publish events asynchronously with Promise support, returns handler values
|
|
10
|
+
* - query(): Deprecated alias for runAsync() - use runAsync() instead
|
|
11
11
|
*
|
|
12
12
|
* 2. Default app singleton - Global event bus instance
|
|
13
13
|
* - Created once and reused across the application
|
|
@@ -35,12 +35,12 @@
|
|
|
35
35
|
* // Handle event
|
|
36
36
|
* });
|
|
37
37
|
*
|
|
38
|
-
* // Publish events
|
|
38
|
+
* // Publish events (fire-and-forget)
|
|
39
39
|
* app.run('event-name', ...args);
|
|
40
40
|
*
|
|
41
|
-
* //
|
|
42
|
-
* app.runAsync('
|
|
43
|
-
* // Handle results
|
|
41
|
+
* // Get return values from event handlers
|
|
42
|
+
* app.runAsync('event-name', data).then(results => {
|
|
43
|
+
* // Handle results array
|
|
44
44
|
* });
|
|
45
45
|
* ```
|
|
46
46
|
*/
|
|
@@ -52,19 +52,6 @@ export class App {
|
|
|
52
52
|
|
|
53
53
|
_events: { [key: string]: Array<{ fn: (...args: any[]) => any, options: EventOptions }> };
|
|
54
54
|
|
|
55
|
-
public start: any;
|
|
56
|
-
public h: any;
|
|
57
|
-
public createElement: any;
|
|
58
|
-
public render: any;
|
|
59
|
-
public Fragment: any;
|
|
60
|
-
public webComponent: any;
|
|
61
|
-
public safeHTML: any;
|
|
62
|
-
public use_render: any;
|
|
63
|
-
public use_react: any;
|
|
64
|
-
public route: any;
|
|
65
|
-
|
|
66
|
-
public version: string;
|
|
67
|
-
|
|
68
55
|
constructor() {
|
|
69
56
|
this._events = {} as { [key: string]: Array<{ fn: (...args: any[]) => any, options: EventOptions }> };
|
|
70
57
|
}
|
|
@@ -143,7 +130,11 @@ export class App {
|
|
|
143
130
|
return Promise.all(promises);
|
|
144
131
|
}
|
|
145
132
|
|
|
133
|
+
/**
|
|
134
|
+
* @deprecated Use runAsync() instead. app.query() will be removed in a future version.
|
|
135
|
+
*/
|
|
146
136
|
query(name: string, ...args: any[]): Promise<any[]> {
|
|
137
|
+
console.warn('app.query() is deprecated. Use app.runAsync() instead.');
|
|
147
138
|
return this.runAsync(name, ...args);
|
|
148
139
|
}
|
|
149
140
|
|
package/src/apprun-code.tsx
CHANGED
|
@@ -11,6 +11,7 @@ const styles = `
|
|
|
11
11
|
.apprun-play {
|
|
12
12
|
height: 100%;
|
|
13
13
|
display: flex;
|
|
14
|
+
font-size: 1.1rem;
|
|
14
15
|
}
|
|
15
16
|
|
|
16
17
|
.apprun-play .col {
|
|
@@ -23,13 +24,20 @@ const styles = `
|
|
|
23
24
|
}
|
|
24
25
|
`;
|
|
25
26
|
|
|
27
|
+
const encodeHTML = code => {
|
|
28
|
+
return code.replace(/&/g, '&')
|
|
29
|
+
.replace(/</g, '<')
|
|
30
|
+
.replace(/>/g, '>')
|
|
31
|
+
.replace(/"/g, '"')
|
|
32
|
+
.replace(/'/g, ''');
|
|
33
|
+
}
|
|
34
|
+
|
|
26
35
|
const code_html = code => `<!DOCTYPE html>
|
|
27
36
|
<html lang="en">
|
|
28
37
|
<head>
|
|
29
38
|
<meta charset="UTF-8">
|
|
30
39
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
31
40
|
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
|
32
|
-
<script src="https://cdnjs.cloudflare.com/ajax/libs/custom-elements/1.1.2/custom-elements.min.js"></script>
|
|
33
41
|
<title>AppRun Playground</title>
|
|
34
42
|
<style>
|
|
35
43
|
body {
|
|
@@ -37,17 +45,47 @@ const code_html = code => `<!DOCTYPE html>
|
|
|
37
45
|
margin: 2em;
|
|
38
46
|
}
|
|
39
47
|
</style>
|
|
40
|
-
<script src="https://
|
|
41
|
-
<script src="
|
|
48
|
+
<script src="https://cdn.jsdelivr.net/npm/typescript@latest"></script>
|
|
49
|
+
<script src="dist/apprun-html.js"></script>
|
|
42
50
|
</head>
|
|
43
51
|
<body>
|
|
44
|
-
<
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
52
|
+
<pre id="code" style="display:none">${encodeHTML(code)}</pre>
|
|
53
|
+
<script type="module">
|
|
54
|
+
const code = document.getElementById('code').innerText;
|
|
55
|
+
const compiled = ts.transpileModule(code, {
|
|
56
|
+
compilerOptions: {
|
|
57
|
+
"jsx": "react",
|
|
58
|
+
"jsxFactory": "app.h",
|
|
59
|
+
"jsxFragmentFactory": "app.Fragment",
|
|
60
|
+
"target": "es2020",
|
|
61
|
+
"module": "esnext",
|
|
62
|
+
},
|
|
63
|
+
reportDiagnostics: true,
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
if (compiled.diagnostics && compiled.diagnostics.length) {
|
|
67
|
+
const pre = document.createElement('pre');
|
|
68
|
+
pre.style = 'font-size: 10px;';
|
|
69
|
+
pre.innerText = compiled.diagnostics.map(d => {
|
|
70
|
+
const start = d.start;
|
|
71
|
+
const end = d.start + d.length;
|
|
72
|
+
const line = code.substring(0, end).split('\\n').length;
|
|
73
|
+
const column = code.substring(0, end).split('\\n').pop().length;
|
|
74
|
+
return \`Line: \${line}, Column: \${column}, \${d.messageText}\`;
|
|
75
|
+
}).join('\\n');
|
|
76
|
+
document.body.appendChild(pre);
|
|
77
|
+
} else {
|
|
78
|
+
window.onerror = function () {
|
|
79
|
+
const pre = document.createElement('pre');
|
|
80
|
+
pre.style = 'font-size: 10px;';
|
|
81
|
+
pre.innerText = compiled.outputText;;
|
|
82
|
+
document.body.appendChild(pre);
|
|
83
|
+
};
|
|
84
|
+
const script = document.createElement('script');
|
|
85
|
+
script.type = 'module';
|
|
86
|
+
script.text = compiled.outputText;
|
|
87
|
+
document.body.appendChild(script);
|
|
88
|
+
}
|
|
51
89
|
</script>
|
|
52
90
|
</body>
|
|
53
91
|
</html>`;
|
package/src/apprun-dev-tools.tsx
CHANGED
|
@@ -1,12 +1,6 @@
|
|
|
1
1
|
import app from './app';
|
|
2
2
|
import toHTML from './vdom-to-html';
|
|
3
3
|
import { _createEventTests, _createStateTests } from './apprun-dev-tools-tests';
|
|
4
|
-
import yaml from 'js-yaml';
|
|
5
|
-
|
|
6
|
-
function replacer(key, value) {
|
|
7
|
-
if (typeof value === 'function') return value.toString(); // value.toString();
|
|
8
|
-
return ['', null].includes(value) || (typeof value === 'object' && (value.length === 0 || Object.keys(value).length === 0)) ? undefined : value;
|
|
9
|
-
}
|
|
10
4
|
|
|
11
5
|
function createProxy(obj) {
|
|
12
6
|
const handler = {
|
|
@@ -53,7 +47,17 @@ function getVDOM(component) {
|
|
|
53
47
|
return view;
|
|
54
48
|
}
|
|
55
49
|
|
|
50
|
+
const componentCache = new Map();
|
|
56
51
|
app['debug'] = true;
|
|
52
|
+
app.on('debug-create-component', component => {
|
|
53
|
+
const element = component.element;
|
|
54
|
+
if (!element) {
|
|
55
|
+
console.warn('Component created without an element:', component);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
if (componentCache.get(element)) { componentCache.get(element).push(component) }
|
|
59
|
+
else { componentCache.set(element, [component]) }
|
|
60
|
+
});
|
|
57
61
|
|
|
58
62
|
window['_apprun-help'] = ['', () => {
|
|
59
63
|
Object.keys(window).forEach(cmd => {
|
|
@@ -81,12 +85,6 @@ function newWin(html) {
|
|
|
81
85
|
win.document.close();
|
|
82
86
|
}
|
|
83
87
|
|
|
84
|
-
const get_components = () => {
|
|
85
|
-
const o = { components: {} };
|
|
86
|
-
app.run('get-components', o);
|
|
87
|
-
const { components } = o;
|
|
88
|
-
return components;
|
|
89
|
-
}
|
|
90
88
|
const viewElement = element => <div>
|
|
91
89
|
{element.tagName.toLowerCase()}{element.id ? '#' + element.id : ''}
|
|
92
90
|
{' '}
|
|
@@ -109,7 +107,7 @@ const viewComponents = state => {
|
|
|
109
107
|
|
|
110
108
|
return <li>
|
|
111
109
|
<div>{component.constructor.name}</div>
|
|
112
|
-
<div><pre>{htmlEncode(
|
|
110
|
+
<div><pre>{htmlEncode(JSON.stringify(component_def, null, 2))}</pre></div>
|
|
113
111
|
<br />
|
|
114
112
|
</li>;
|
|
115
113
|
|
|
@@ -152,7 +150,7 @@ const viewEvents = state => {
|
|
|
152
150
|
const _events = (print?) => {
|
|
153
151
|
const global_events = app['_events']
|
|
154
152
|
const events = {};
|
|
155
|
-
const cache =
|
|
153
|
+
const cache = componentCache;
|
|
156
154
|
|
|
157
155
|
const add_component = component => component['_actions'].forEach(event => {
|
|
158
156
|
events[event.name] = events[event.name] || [];
|
|
@@ -163,10 +161,6 @@ const _events = (print?) => {
|
|
|
163
161
|
for (let [key, comps] of cache) {
|
|
164
162
|
comps.forEach(add_component);
|
|
165
163
|
}
|
|
166
|
-
} else {
|
|
167
|
-
Object.keys(cache).forEach(el =>
|
|
168
|
-
cache[el].forEach(add_component)
|
|
169
|
-
);
|
|
170
164
|
}
|
|
171
165
|
const data = [];
|
|
172
166
|
Object.keys(events).forEach(event => {
|
|
@@ -189,7 +183,7 @@ const _events = (print?) => {
|
|
|
189
183
|
}
|
|
190
184
|
|
|
191
185
|
const _components = (print?) => {
|
|
192
|
-
const components =
|
|
186
|
+
const components = componentCache;
|
|
193
187
|
const data = [];
|
|
194
188
|
|
|
195
189
|
if (components instanceof Map) {
|
|
@@ -197,11 +191,6 @@ const _components = (print?) => {
|
|
|
197
191
|
const element = typeof key === 'string' ? document.getElementById(key) || document.querySelector(key) : key;
|
|
198
192
|
data.push({ element, comps });
|
|
199
193
|
}
|
|
200
|
-
} else {
|
|
201
|
-
Object.keys(components).forEach(el => {
|
|
202
|
-
const element = typeof el === 'string' ? document.getElementById(el) || document.querySelector(el) : el;
|
|
203
|
-
data.push({ element, comps: components[el] });
|
|
204
|
-
});
|
|
205
194
|
}
|
|
206
195
|
if (print) {
|
|
207
196
|
const vdom = viewComponents(data);
|
|
@@ -217,8 +206,15 @@ app.on('debug', p => {
|
|
|
217
206
|
if (debugging & 2 && p.vdom) console.log(p);
|
|
218
207
|
});
|
|
219
208
|
|
|
220
|
-
window['_apprun-components'] = ['components [print]', (p) => {
|
|
221
|
-
|
|
209
|
+
window['_apprun-components'] = ['components [print|clear]', (p) => {
|
|
210
|
+
if (p === 'print') {
|
|
211
|
+
_components(true);
|
|
212
|
+
} else if (p === 'clear') {
|
|
213
|
+
componentCache.clear();
|
|
214
|
+
console.log('Component cache cleared');
|
|
215
|
+
} else {
|
|
216
|
+
_components(false);
|
|
217
|
+
}
|
|
222
218
|
}]
|
|
223
219
|
|
|
224
220
|
window['_apprun-events'] = ['events [print]', (p) => {
|
|
@@ -262,7 +258,7 @@ window['_apprun'] = (strings) => {
|
|
|
262
258
|
else window['_apprun-help'][1]();
|
|
263
259
|
}
|
|
264
260
|
|
|
265
|
-
console.info('AppRun DevTools
|
|
261
|
+
console.info('AppRun DevTools 3.36: type "_apprun `help`" to list all available commands.');
|
|
266
262
|
|
|
267
263
|
const reduxExt = window['__REDUX_DEVTOOLS_EXTENSION__'];
|
|
268
264
|
if (reduxExt) {
|
package/src/apprun-play.tsx
CHANGED
|
@@ -92,13 +92,20 @@ a.button:hover {
|
|
|
92
92
|
</div>
|
|
93
93
|
</div>`;
|
|
94
94
|
|
|
95
|
+
const encodeHTML = code => {
|
|
96
|
+
return code.replace(/&/g, '&')
|
|
97
|
+
.replace(/</g, '<')
|
|
98
|
+
.replace(/>/g, '>')
|
|
99
|
+
.replace(/"/g, '"')
|
|
100
|
+
.replace(/'/g, ''');
|
|
101
|
+
}
|
|
102
|
+
|
|
95
103
|
const code_html = code => `<!DOCTYPE html>
|
|
96
104
|
<html lang="en">
|
|
97
105
|
<head>
|
|
98
106
|
<meta charset="UTF-8">
|
|
99
107
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
100
108
|
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
|
101
|
-
<script src="https://cdnjs.cloudflare.com/ajax/libs/custom-elements/1.1.2/custom-elements.min.js"></script>
|
|
102
109
|
<title>AppRun Playground</title>
|
|
103
110
|
<style>
|
|
104
111
|
body {
|
|
@@ -106,17 +113,47 @@ const code_html = code => `<!DOCTYPE html>
|
|
|
106
113
|
margin: 2em;
|
|
107
114
|
}
|
|
108
115
|
</style>
|
|
109
|
-
<script src="https://
|
|
116
|
+
<script src="https://cdn.jsdelivr.net/npm/typescript@latest"></script>
|
|
110
117
|
<script src="https://unpkg.com/apprun/dist/apprun-html.js"></script>
|
|
111
118
|
</head>
|
|
112
119
|
<body>
|
|
113
|
-
<
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
+
<pre id="code" style="display:none">${encodeHTML(code)}</pre>
|
|
121
|
+
<script type="module">
|
|
122
|
+
const code = document.getElementById('code').innerText;
|
|
123
|
+
const compiled = ts.transpileModule(code, {
|
|
124
|
+
compilerOptions: {
|
|
125
|
+
"jsx": "react",
|
|
126
|
+
"jsxFactory": "app.h",
|
|
127
|
+
"jsxFragmentFactory": "app.Fragment",
|
|
128
|
+
"target": "es2020",
|
|
129
|
+
"module": "esnext",
|
|
130
|
+
},
|
|
131
|
+
reportDiagnostics: true,
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
if (compiled.diagnostics && compiled.diagnostics.length) {
|
|
135
|
+
const pre = document.createElement('pre');
|
|
136
|
+
pre.style = 'font-size: 10px;';
|
|
137
|
+
pre.innerText = compiled.diagnostics.map(d => {
|
|
138
|
+
const start = d.start;
|
|
139
|
+
const end = d.start + d.length;
|
|
140
|
+
const line = code.substring(0, end).split('\\n').length;
|
|
141
|
+
const column = code.substring(0, end).split('\\n').pop().length;
|
|
142
|
+
return \`Line: \${line}, Column: \${column}, \${d.messageText}\`;
|
|
143
|
+
}).join('\\n');
|
|
144
|
+
document.body.appendChild(pre);
|
|
145
|
+
} else {
|
|
146
|
+
window.onerror = function () {
|
|
147
|
+
const pre = document.createElement('pre');
|
|
148
|
+
pre.style = 'font-size: 10px;';
|
|
149
|
+
pre.innerText = compiled.outputText;;
|
|
150
|
+
document.body.appendChild(pre);
|
|
151
|
+
};
|
|
152
|
+
const script = document.createElement('script');
|
|
153
|
+
script.type = 'module';
|
|
154
|
+
script.text = compiled.outputText;
|
|
155
|
+
document.body.appendChild(script);
|
|
156
|
+
}
|
|
120
157
|
</script>
|
|
121
158
|
</body>
|
|
122
159
|
</html>`;
|
package/src/apprun.ts
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* - Router with improved null safety
|
|
11
11
|
* - Web component support
|
|
12
12
|
* - Type-safe React integration
|
|
13
|
+
* - Component batch mounting system
|
|
13
14
|
*
|
|
14
15
|
* Key exports:
|
|
15
16
|
* - app: Global event system instance
|
|
@@ -26,6 +27,7 @@
|
|
|
26
27
|
* - Web Components integration
|
|
27
28
|
* - React compatibility layer
|
|
28
29
|
* - TypeScript support with strong typing
|
|
30
|
+
* - Batch component mounting with addComponents(element, components)
|
|
29
31
|
*
|
|
30
32
|
* Type Safety Improvements (v3.35.1):
|
|
31
33
|
* - Added null checks for DOM event targets
|
|
@@ -34,6 +36,10 @@
|
|
|
34
36
|
* - Better error handling for invalid event handlers
|
|
35
37
|
* - Safer element access with proper type assertions
|
|
36
38
|
*
|
|
39
|
+
* Recent Changes:
|
|
40
|
+
* - Modified addComponents to accept (element, components) where components is a key-value object with routes as keys and components as values
|
|
41
|
+
* - Simplified component mounting API for better usability
|
|
42
|
+
*
|
|
37
43
|
* Usage:
|
|
38
44
|
* ```ts
|
|
39
45
|
* import { app, Component } from 'apprun';
|
|
@@ -46,16 +52,23 @@
|
|
|
46
52
|
* 'event': (state, ...args) => // Handle events
|
|
47
53
|
* }
|
|
48
54
|
* }
|
|
55
|
+
*
|
|
56
|
+
* // Mount multiple components
|
|
57
|
+
* app.addComponents(document.body, {
|
|
58
|
+
* '/home': MyComponent,
|
|
59
|
+
* '/about': AnotherComponent
|
|
60
|
+
* });
|
|
49
61
|
* ```
|
|
50
62
|
*/
|
|
51
63
|
|
|
52
|
-
import
|
|
64
|
+
import _app, { App } from './app';
|
|
53
65
|
import { createElement, render, Fragment, safeHTML } from './vdom';
|
|
54
66
|
import { Component } from './component';
|
|
55
|
-
import { VNode, View, Action, Update, EventOptions, ActionOptions, MountOptions, AppStartOptions } from './types';
|
|
67
|
+
import { IApp, VNode, View, Action, Update, EventOptions, ActionOptions, MountOptions, AppStartOptions, CustomElementOptions } from './types';
|
|
56
68
|
import { on, update, customElement } from './decorator';
|
|
57
|
-
import
|
|
58
|
-
import
|
|
69
|
+
import { route, ROUTER_EVENT, ROUTER_404_EVENT } from './router';
|
|
70
|
+
import webComponent from './web-component';
|
|
71
|
+
import addComponents from './add-components';
|
|
59
72
|
import { APPRUN_VERSION } from './version';
|
|
60
73
|
|
|
61
74
|
export type StatelessComponent<T = {}> = (args: T) => string | VNode | void;
|
|
@@ -64,6 +77,9 @@ type OnDecorator = {
|
|
|
64
77
|
<E = string>(events?: E, options?: any): (target: any, key: string) => void;
|
|
65
78
|
};
|
|
66
79
|
|
|
80
|
+
const app: IApp = _app as unknown as IApp;
|
|
81
|
+
export default app as IApp;
|
|
82
|
+
|
|
67
83
|
export {
|
|
68
84
|
App,
|
|
69
85
|
app,
|
|
@@ -82,29 +98,6 @@ export {
|
|
|
82
98
|
export { update as event };
|
|
83
99
|
export { ROUTER_EVENT, ROUTER_404_EVENT };
|
|
84
100
|
export { customElement, CustomElementOptions, AppStartOptions };
|
|
85
|
-
export default app as IApp;
|
|
86
|
-
|
|
87
|
-
export interface IApp {
|
|
88
|
-
start<T, E = any>(element?: Element | string, model?: T, view?: View<T>, update?: Update<T, E>,
|
|
89
|
-
options?: AppStartOptions<T>): Component<T, E>;
|
|
90
|
-
on(name: string, fn: (...args: any[]) => void, options?: any): void;
|
|
91
|
-
once(name: string, fn: (...args: any[]) => void, options?: any): void;
|
|
92
|
-
off(name: string, fn: (...args: any[]) => void): void;
|
|
93
|
-
run(name: string, ...args: any[]): number;
|
|
94
|
-
find(name: string): any | any[];
|
|
95
|
-
query(name: string, ...args: any[]): Promise<any[]>;
|
|
96
|
-
runAsync(name: string, ...args: any[]): Promise<any[]>;
|
|
97
|
-
h(tag: string | Function, props, ...children): VNode | VNode[];
|
|
98
|
-
createElement(tag: string | Function, props, ...children): VNode | VNode[];
|
|
99
|
-
render(element: Element | string, node: VNode): void;
|
|
100
|
-
Fragment(props, ...children): any[];
|
|
101
|
-
route?: Route;
|
|
102
|
-
webComponent(name: string, componentClass, options?: CustomElementOptions): void;
|
|
103
|
-
safeHTML(html: string): any[];
|
|
104
|
-
use_render(render, mode?: 0 | 1);
|
|
105
|
-
use_react(React, ReactDOM);
|
|
106
|
-
version: string;
|
|
107
|
-
}
|
|
108
101
|
|
|
109
102
|
if (!app.start) {
|
|
110
103
|
|
|
@@ -126,14 +119,11 @@ if (!app.start) {
|
|
|
126
119
|
return component;
|
|
127
120
|
};
|
|
128
121
|
|
|
129
|
-
|
|
130
|
-
app.on(name, fn, { ...options, once: true });
|
|
131
|
-
});
|
|
132
|
-
|
|
122
|
+
// Deprecated: app.query is deprecated in favor of app.runAsync
|
|
133
123
|
app.query = app.query || app.runAsync;
|
|
134
124
|
|
|
135
125
|
const NOOP = _ => {/* Intentionally empty */ }
|
|
136
|
-
|
|
126
|
+
app.on('/', NOOP);
|
|
137
127
|
app.on('debug', _ => NOOP);
|
|
138
128
|
app.on(ROUTER_EVENT, NOOP);
|
|
139
129
|
app.on(ROUTER_404_EVENT, NOOP);
|
|
@@ -141,6 +131,11 @@ if (!app.start) {
|
|
|
141
131
|
app.on('route', url => app['route'] && app['route'](url));
|
|
142
132
|
|
|
143
133
|
if (typeof document === 'object') {
|
|
134
|
+
let basePath = location.pathname;
|
|
135
|
+
if (basePath.endsWith('/')) {
|
|
136
|
+
basePath = basePath.slice(0, -1);
|
|
137
|
+
}
|
|
138
|
+
app.basePath = basePath;
|
|
144
139
|
document.addEventListener("DOMContentLoaded", () => {
|
|
145
140
|
const no_init_route = document.body.hasAttribute('apprun-no-init') || app['no-init-route'] || false;
|
|
146
141
|
const use_hash = app.find('#') || app.find('#/') || false;
|
|
@@ -152,7 +147,18 @@ if (!app.start) {
|
|
|
152
147
|
if (use_hash) {
|
|
153
148
|
!no_init_route && route(location.hash);
|
|
154
149
|
} else {
|
|
155
|
-
!no_init_route &&
|
|
150
|
+
!no_init_route && (() => {
|
|
151
|
+
const basePath = app.basePath || '';
|
|
152
|
+
let initialPath = location.pathname;
|
|
153
|
+
|
|
154
|
+
// Strip base path if present
|
|
155
|
+
if (basePath && initialPath.startsWith(basePath)) {
|
|
156
|
+
initialPath = initialPath.substring(basePath.length);
|
|
157
|
+
if (!initialPath.startsWith('/')) initialPath = '/' + initialPath;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
route(initialPath);
|
|
161
|
+
})();
|
|
156
162
|
document.body.addEventListener('click', e => {
|
|
157
163
|
const element = e.target as HTMLElement;
|
|
158
164
|
if (!element) return;
|
|
@@ -162,21 +168,22 @@ if (!app.start) {
|
|
|
162
168
|
menu.origin === location.origin &&
|
|
163
169
|
menu.pathname) {
|
|
164
170
|
e.preventDefault();
|
|
165
|
-
|
|
166
|
-
|
|
171
|
+
|
|
172
|
+
// Handle base path for navigation
|
|
173
|
+
const basePath = app.basePath || '';
|
|
174
|
+
const fullPath = basePath + menu.pathname;
|
|
175
|
+
|
|
176
|
+
history.pushState(null, '', fullPath);
|
|
177
|
+
route(menu.pathname); // Route with relative path (without base path)
|
|
167
178
|
}
|
|
168
179
|
});
|
|
169
180
|
}
|
|
170
181
|
});
|
|
171
182
|
}
|
|
172
183
|
|
|
173
|
-
type ComponentType = typeof Component & {
|
|
174
|
-
<T = any>(options?: any): (constructor: Function) => void;
|
|
175
|
-
};
|
|
176
|
-
|
|
177
184
|
if (typeof window === 'object') {
|
|
178
185
|
const globalWindow = window as any;
|
|
179
|
-
globalWindow['Component'] = Component
|
|
186
|
+
globalWindow['Component'] = Component;
|
|
180
187
|
globalWindow['_React'] = globalWindow['React'];
|
|
181
188
|
globalWindow['React'] = app;
|
|
182
189
|
globalWindow['on'] = on as OnDecorator;
|
|
@@ -231,4 +238,6 @@ if (!app.start) {
|
|
|
231
238
|
app.render = (el, vdom) => ReactDOM.render(vdom, el);
|
|
232
239
|
}
|
|
233
240
|
}
|
|
241
|
+
|
|
242
|
+
app.addComponents = addComponents;
|
|
234
243
|
}
|
package/src/component.ts
CHANGED
|
@@ -51,16 +51,20 @@
|
|
|
51
51
|
* ```
|
|
52
52
|
*/
|
|
53
53
|
|
|
54
|
-
import
|
|
54
|
+
import _app, { App } from './app';
|
|
55
|
+
|
|
56
|
+
|
|
55
57
|
import { Reflect } from './decorator'
|
|
56
|
-
import { View, Update, ActionDef, ActionOptions, MountOptions, EventOptions } from './types';
|
|
58
|
+
import { View, Update, ActionDef, ActionOptions, MountOptions, EventOptions, IApp } from './types';
|
|
57
59
|
import directive from './directive';
|
|
58
60
|
import { safeQuerySelector, safeGetElementById } from './type-utils';
|
|
59
61
|
|
|
60
|
-
const componentCache = new Map();
|
|
61
|
-
if (!app.find('get-components')) app.on('get-components', o => o.components = componentCache);
|
|
62
|
+
// const componentCache = new Map();
|
|
63
|
+
// if (!app.find('get-components')) app.on('get-components', o => o.components = componentCache);
|
|
64
|
+
|
|
65
|
+
export const REFRESH = state => state;
|
|
62
66
|
|
|
63
|
-
const
|
|
67
|
+
const app = _app as unknown as IApp;
|
|
64
68
|
|
|
65
69
|
export class Component<T = any, E = any> {
|
|
66
70
|
static __isAppRunComponent = true;
|
|
@@ -244,9 +248,8 @@ export class Component<T = any, E = any> {
|
|
|
244
248
|
|
|
245
249
|
this.setState(this.state, { render: !!options.render, history: true });
|
|
246
250
|
|
|
247
|
-
if (app['debug']) {
|
|
248
|
-
|
|
249
|
-
else { componentCache.set(element, [this]) }
|
|
251
|
+
if (app['debug'] && app.find('debug-create-component')?.length) {
|
|
252
|
+
app.run('debug-create-component', this);
|
|
250
253
|
}
|
|
251
254
|
return this;
|
|
252
255
|
}
|
|
@@ -367,7 +370,11 @@ export class Component<T = any, E = any> {
|
|
|
367
370
|
}
|
|
368
371
|
|
|
369
372
|
// obsolete
|
|
373
|
+
/**
|
|
374
|
+
* @deprecated Use runAsync() instead. query() will be removed in a future version.
|
|
375
|
+
*/
|
|
370
376
|
public query(event: E, ...args) {
|
|
377
|
+
console.warn('component.query() is deprecated. Use component.runAsync() instead.');
|
|
371
378
|
return this.runAsync(event, ...args);
|
|
372
379
|
}
|
|
373
380
|
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { produce, Draft } from 'immer';
|
|
2
|
+
|
|
3
|
+
export function createState<T = any>(
|
|
4
|
+
fn: (draft: Draft<T>, ...args: any[]) => void
|
|
5
|
+
): (state: T, ...args: any[]) => T {
|
|
6
|
+
return (state: T, ...args: any[]): T => {
|
|
7
|
+
return produce(state, (draft) => {
|
|
8
|
+
fn(draft, ...args);
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
}
|