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.
Files changed (93) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/README.md +143 -25
  3. package/WHATSNEW.md +29 -12
  4. package/apprun-book.jpg +0 -0
  5. package/apprun.d.ts +136 -46
  6. package/cli/app.js +29 -0
  7. package/cli/index.html +13 -0
  8. package/{apprun-cli.js → cli/index.js} +8 -14
  9. package/dist/apprun-code.js +2 -0
  10. package/dist/apprun-code.js.map +1 -0
  11. package/dist/apprun-dev-tools.js +1 -2
  12. package/dist/apprun-dev-tools.js.map +1 -1
  13. package/dist/apprun-html.esm.js +7 -7
  14. package/dist/apprun-html.esm.js.map +1 -1
  15. package/dist/apprun-html.js +1 -1
  16. package/dist/apprun-html.js.map +1 -1
  17. package/dist/apprun-play-html.esm.js +7 -7
  18. package/dist/apprun-play-html.esm.js.map +1 -1
  19. package/dist/apprun-play.js +1 -1
  20. package/dist/apprun-play.js.map +1 -1
  21. package/dist/apprun.esm.js +1 -1
  22. package/dist/apprun.esm.js.map +1 -1
  23. package/dist/apprun.js +1 -1
  24. package/dist/apprun.js.map +1 -1
  25. package/dist/createState.js +2 -0
  26. package/dist/createState.js.map +1 -0
  27. package/esm/add-components.js +90 -0
  28. package/esm/add-components.js.map +1 -0
  29. package/esm/app.js +15 -8
  30. package/esm/app.js.map +1 -1
  31. package/esm/apprun-code.js +51 -16
  32. package/esm/apprun-code.js.map +1 -1
  33. package/esm/apprun-dev-tools.js +32 -32
  34. package/esm/apprun-dev-tools.js.map +1 -1
  35. package/esm/apprun-play.js +50 -17
  36. package/esm/apprun-play.js.map +1 -1
  37. package/esm/apprun.js +42 -11
  38. package/esm/apprun.js.map +1 -1
  39. package/esm/component.js +17 -20
  40. package/esm/component.js.map +1 -1
  41. package/esm/createState.js +9 -0
  42. package/esm/createState.js.map +1 -0
  43. package/esm/decorator.js.map +1 -1
  44. package/esm/directive.js +1 -1
  45. package/esm/directive.js.map +1 -1
  46. package/esm/router.js +241 -18
  47. package/esm/router.js.map +1 -1
  48. package/esm/shadow.js +1 -1
  49. package/esm/shadow.js.map +1 -1
  50. package/esm/type-utils.js +2 -3
  51. package/esm/type-utils.js.map +1 -1
  52. package/esm/vdom-my-new.js +327 -0
  53. package/esm/vdom-my-new.js.map +1 -0
  54. package/esm/vdom-my-prop-attr.js +227 -0
  55. package/esm/vdom-my-prop-attr.js.map +1 -0
  56. package/esm/vdom-my.js +114 -150
  57. package/esm/vdom-my.js.map +1 -1
  58. package/esm/vdom-patch.js +1 -1
  59. package/esm/vdom-patch.js.map +1 -1
  60. package/esm/version.js +1 -1
  61. package/esm/web-component.js +3 -4
  62. package/esm/web-component.js.map +1 -1
  63. package/index.html +5 -2
  64. package/jest.config.js +2 -2
  65. package/jest.setup.js +29 -3
  66. package/jsx-runtime.js +1 -1
  67. package/jsx-runtime.js.map +1 -1
  68. package/package.json +14 -14
  69. package/src/add-components.ts +103 -0
  70. package/src/app.ts +10 -19
  71. package/src/apprun-code.tsx +48 -10
  72. package/src/apprun-dev-tools.tsx +23 -27
  73. package/src/apprun-play.tsx +46 -9
  74. package/src/apprun.ts +49 -40
  75. package/src/component.ts +15 -8
  76. package/src/createState.ts +11 -0
  77. package/src/decorator.ts +2 -1
  78. package/src/router.ts +261 -17
  79. package/src/shadow.tsx +1 -1
  80. package/src/tsconfig.json +2 -2
  81. package/src/types.ts +62 -2
  82. package/src/vdom-my-new.ts +311 -0
  83. package/src/vdom-my-prop-attr.ts +241 -0
  84. package/src/vdom-my.ts +109 -134
  85. package/src/version.ts +1 -1
  86. package/src/web-component.ts +4 -10
  87. package/tsconfig.jest.json +15 -6
  88. package/tsconfig.json +3 -3
  89. package/webpack.config.cjs +3 -1
  90. package/cli/export.js +0 -92
  91. package/cli/import.js +0 -68
  92. package/plan/plan-apprun-bugfixes.md +0 -207
  93. package/src/types/apprun.d.ts +0 -56
package/src/decorator.ts CHANGED
@@ -1,4 +1,5 @@
1
- import webComponent, { CustomElementOptions } from './web-component';
1
+ import { CustomElementOptions } from './types';
2
+ import webComponent from './web-component';
2
3
 
3
4
  /**
4
5
  * TypeScript Decorators for AppRun Components
package/src/router.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * AppRun Router Implementation
2
+ * AppRun Router Implementation with Hierarchical Matching
3
3
  *
4
4
  * This file provides URL routing capabilities:
5
5
  * 1. Hash-based routing (#/path)
@@ -22,6 +22,7 @@
22
22
  * - Route parameter parsing and injection into events
23
23
  * - URL pattern matching with parameter support
24
24
  * - Global and component-level route handling
25
+ * - **NEW: Hierarchical route matching** - progressively tries parent routes
25
26
  *
26
27
  * Type Safety Improvements (v3.35.1):
27
28
  * - Enhanced route type definitions
@@ -30,7 +31,7 @@
30
31
  *
31
32
  * Usage:
32
33
  * ```ts
33
- * // Handle routes
34
+ * // Basic routing
34
35
  * app.on('#/home', () => // Show home page);
35
36
  * app.on('#/users/:id', (id) => // Show user profile);
36
37
  * app.on('/api/*', (path) => // Handle API routes);
@@ -38,30 +39,273 @@
38
39
  * // Navigate programmatically
39
40
  * app.run('route', '#/home');
40
41
  * route('/users/123'); // Direct routing
42
+ *
43
+ * // Hierarchical matching examples
44
+ * app.on('/api', (operation, id) => // Handle /api/users/123);
45
+ * app.on('#users', (id, action) => // Handle #users/123/edit);
46
+ *
47
+ * // Hierarchical Route Matching (NEW):
48
+ * // For URL: /api/v1/users/123
49
+ * // Router tries: /api/v1/users/123 → /api/v1/users → /api/v1 → /api → 404
50
+ * // If handler found at /api, it receives: ('v1', 'users', '123')
51
+ *
52
+ * // Base Path Support (NEW):
53
+ * app.basePath = '/myapp'; // For sub-directory deployments
54
+ * // Links: <a href="/users/123"> (relative paths)
55
+ * // Navigation: /myapp/users/123 (full path)
56
+ * // Routing: /users/123 (base path stripped)
57
+ *
58
+ * // Empty Path Handling (NEW):
59
+ * // For URL: "" (empty)
60
+ * // Router tries: # → / → #/ → 404 (in priority order)
61
+ *
62
+ * // 404 Behavior (ENHANCED):
63
+ * // Fires only at minimal levels: /a, #a, #/a, a
64
+ * // Never tries root handlers: /, #, #/
41
65
  * ```
42
66
  */
43
67
 
44
68
  import app from './app';
45
69
 
46
- export type Route = (url: string, ...args: any[]) => any;
70
+ // Helper functions for hierarchical routing
71
+
72
+ /**
73
+ * Extract clean path segments from URL
74
+ * @param url - The URL to parse
75
+ * @returns Array of path segments
76
+ */
77
+ function parsePathSegments(url: string): string[] {
78
+ if (!url) return [];
79
+
80
+ // Handle different URL types
81
+ if (url.startsWith('#/')) {
82
+ return url.substring(2).split('/');
83
+ } else if (url.startsWith('#')) {
84
+ return url.substring(1).split('/');
85
+ } else if (url.startsWith('/')) {
86
+ return url.substring(1).split('/');
87
+ } else {
88
+ return url.split('/');
89
+ }
90
+ }
91
+
92
+ /**
93
+ * Normalize trailing slash - convert /a/ to /a
94
+ * @param url - The URL to normalize
95
+ * @returns Normalized URL
96
+ */
97
+ function normalizeTrailingSlash(url: string): string {
98
+ if (!url || url === '/' || url === '#' || url === '#/') return url;
99
+ if (url.endsWith('/')) return url.slice(0, -1);
100
+ return url;
101
+ }
102
+
103
+ /**
104
+ * Validate hierarchy depth and warn if too deep
105
+ * @param segments - Path segments to validate
106
+ */
107
+ function validateHierarchyDepth(segments: string[]): void {
108
+ // Count non-empty segments for depth validation
109
+ const nonEmptySegments = segments.filter(Boolean);
110
+ if (nonEmptySegments.length > 11) {
111
+ console.warn(`Deep route hierarchy detected: ${nonEmptySegments.join('/')} (${nonEmptySegments.length} levels)`);
112
+ }
113
+ }
114
+
115
+ /**
116
+ * Strip base path from URL
117
+ * @param url - The URL to process
118
+ * @param basePath - The base path to remove
119
+ * @returns URL with base path removed
120
+ */
121
+ function stripBasePath(url: string, basePath: string): string {
122
+ if (!basePath || basePath === '/' || basePath === '') return url;
123
+
124
+ // Normalize base path
125
+ const normalizedBasePath = basePath.startsWith('/') ? basePath : '/' + basePath;
126
+
127
+ if (url.startsWith(normalizedBasePath)) {
128
+ const stripped = url.substring(normalizedBasePath.length);
129
+ return stripped.startsWith('/') ? stripped : '/' + stripped;
130
+ }
131
+
132
+ return url;
133
+ }
134
+
135
+ /**
136
+ * Generate hierarchy of routes to try (stops at minimal level)
137
+ * @param segments - Array of path segments
138
+ * @param routeType - Type of route (path, hash, hash-slash, non-prefixed)
139
+ * @returns Array of route names to try in order
140
+ */
141
+ function generateRouteHierarchy(segments: string[], routeType: 'path' | 'hash' | 'hash-slash' | 'non-prefixed'): string[] {
142
+ const hierarchy: string[] = [];
143
+
144
+ // Build hierarchy from most specific to least specific
145
+ for (let i = segments.length; i > 0; i--) {
146
+ const currentSegments = segments.slice(0, i);
147
+ let routeName = '';
148
+
149
+ switch (routeType) {
150
+ case 'path':
151
+ routeName = '/' + currentSegments.join('/');
152
+ break;
153
+ case 'hash':
154
+ routeName = '#' + currentSegments.join('/');
155
+ break;
156
+ case 'hash-slash':
157
+ routeName = '#/' + currentSegments.join('/');
158
+ break;
159
+ case 'non-prefixed':
160
+ routeName = currentSegments.join('/');
161
+ break;
162
+ }
163
+
164
+ hierarchy.push(routeName);
165
+ }
166
+
167
+ return hierarchy;
168
+ }
169
+
170
+ /**
171
+ * Find handler in hierarchy and return handler info
172
+ * @param hierarchy - Array of route names to try
173
+ * @param originalSegments - Original path segments
174
+ * @returns Handler info if found, null otherwise
175
+ */
176
+ function findHandlerInHierarchy(hierarchy: string[], originalSegments: string[]): { eventName: string; parameters: string[] } | null {
177
+ for (let i = 0; i < hierarchy.length; i++) {
178
+ const routeName = hierarchy[i];
179
+ const subscribers = app.find(routeName);
180
+
181
+ if (subscribers && subscribers.length > 0) {
182
+ // Found handler - calculate remaining parameters
183
+ const handlerDepth = hierarchy.length - i;
184
+ const parameters = originalSegments.slice(handlerDepth);
185
+
186
+ return {
187
+ eventName: routeName,
188
+ parameters: parameters
189
+ };
190
+ }
191
+ }
192
+
193
+ return null;
194
+ }
195
+
196
+ /**
197
+ * Handle empty path with priority order: # → / → #/ → 404
198
+ */
199
+ function handleEmptyPath(): void {
200
+ // Try # first
201
+ const hashSubscribers = app.find('#');
202
+ if (hashSubscribers && hashSubscribers.length > 0) {
203
+ app.run('#');
204
+ app.run(ROUTER_EVENT, '#');
205
+ return;
206
+ }
207
+
208
+ // Try / second
209
+ const pathSubscribers = app.find('/');
210
+ if (pathSubscribers && pathSubscribers.length > 0) {
211
+ app.run('/');
212
+ app.run(ROUTER_EVENT, '/');
213
+ return;
214
+ }
215
+
216
+ // Try #/ third
217
+ const hashSlashSubscribers = app.find('#/');
218
+ if (hashSlashSubscribers && hashSlashSubscribers.length > 0) {
219
+ app.run('#/');
220
+ app.run(ROUTER_EVENT, '#/');
221
+ return;
222
+ }
223
+
224
+ // Fire 404 if no handlers found
225
+ console.warn('No subscribers for event: ');
226
+ app.run(ROUTER_404_EVENT, '');
227
+ app.run(ROUTER_EVENT, '');
228
+ }
229
+
230
+ /**
231
+ * Main hierarchical routing logic
232
+ * @param url - The URL to route
233
+ */
234
+ function routeWithHierarchy(url: string): void {
235
+ // Handle empty path
236
+ if (!url) {
237
+ handleEmptyPath();
238
+ return;
239
+ }
240
+
241
+ // Normalize trailing slash
242
+ url = normalizeTrailingSlash(url);
243
+
244
+ // Strip base path if configured
245
+ const basePath = app['basePath'];
246
+ if (basePath) {
247
+ url = stripBasePath(url, basePath);
248
+ }
249
+
250
+ // Parse segments and validate depth
251
+ const segments = parsePathSegments(url);
252
+ validateHierarchyDepth(segments);
253
+
254
+ // Determine route type
255
+ let routeType: 'path' | 'hash' | 'hash-slash' | 'non-prefixed';
256
+ if (url.startsWith('#/')) {
257
+ routeType = 'hash-slash';
258
+ } else if (url.startsWith('#')) {
259
+ routeType = 'hash';
260
+ } else if (url.startsWith('/')) {
261
+ routeType = 'path';
262
+ } else {
263
+ routeType = 'non-prefixed';
264
+ }
265
+
266
+ // Generate hierarchy
267
+ const hierarchy = generateRouteHierarchy(segments, routeType);
268
+
269
+ // Find handler in hierarchy
270
+ const handlerInfo = findHandlerInHierarchy(hierarchy, segments);
271
+
272
+ if (handlerInfo) {
273
+ // Found handler - publish route with parameters
274
+ publishRoute(handlerInfo.eventName, ...handlerInfo.parameters);
275
+ } else {
276
+ // No handler found - fire 404 with original URL
277
+ if (hierarchy.length > 0) {
278
+ const minimalRoute = hierarchy[hierarchy.length - 1];
279
+ console.warn(`No subscribers for event: ${minimalRoute}`);
280
+ app.run(ROUTER_404_EVENT, url);
281
+ app.run(ROUTER_EVENT, url);
282
+ } else {
283
+ handleEmptyPath();
284
+ }
285
+ }
286
+ }
287
+
288
+ const publishRoute = (name: string, ...args: any[]) => {
289
+ if (!name || name === ROUTER_EVENT || name === ROUTER_404_EVENT) return;
290
+ const subscribers = app.find(name);
291
+ if (!subscribers || subscribers.length === 0) {
292
+ console.warn(`No subscribers for event: ${name}`);
293
+ app.run(ROUTER_404_EVENT, name, ...args);
294
+ } else {
295
+ app.run(name, ...args);
296
+ }
297
+ app.run(ROUTER_EVENT, name, ...args);
298
+ }
299
+
300
+ import { Router } from './types';
47
301
 
48
302
  export const ROUTER_EVENT: string = '//';
49
303
  export const ROUTER_404_EVENT: string = '///';
50
- export const route: Route = (url: string) => {
304
+ export const route: Router = (url: string) => {
51
305
  if (app['lastUrl'] === url) return; // Prevent duplicate routing
52
306
  app['lastUrl'] = url;
53
- if (!url) url = '#';
54
- if (url.startsWith('#')) {
55
- const [name, ...rest] = url.split('/');
56
- app.run(name, ...rest) || app.run(ROUTER_404_EVENT, name, ...rest);
57
- app.run(ROUTER_EVENT, name, ...rest);
58
- } else if (url.startsWith('/')) {
59
- const [_, name, ...rest] = url.split('/');
60
- app.run('/' + name, ...rest) || app.run(ROUTER_404_EVENT, '/' + name, ...rest);
61
- app.run(ROUTER_EVENT, '/' + name, ...rest);
62
- } else {
63
- app.run(url) || app.run(ROUTER_404_EVENT, url);
64
- app.run(ROUTER_EVENT, url);
65
- }
307
+
308
+ // Use hierarchical routing logic
309
+ routeWithHierarchy(url);
66
310
  }
67
311
  export default route;
package/src/shadow.tsx CHANGED
@@ -1,4 +1,4 @@
1
- import app from './app';
1
+ import app from './apprun';
2
2
 
3
3
  export default (_, children) => {
4
4
  const el = document.createElement('section');
package/src/tsconfig.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "compilerOptions": {
3
- "target": "es2017",
4
- "module": "es2015",
3
+ "target": "es2020",
4
+ "module": "esnext",
5
5
  "moduleResolution": "node",
6
6
  "jsx": "react",
7
7
  "jsxFactory": "app.h",
package/src/types.ts CHANGED
@@ -59,10 +59,10 @@ export type VDOM = false | string | VNode | Array<VNode | string> | TemplateResu
59
59
  export type View<T> = (state: T) => VDOM | void;
60
60
  export type Action<T> = (state: T, ...p: any[]) => T | Promise<T> | void;
61
61
  export type ActionDef<T, E> = (readonly [E, Action<T>, {}?]);
62
- export type Update<T, E = string> = ActionDef<T, E>[] | { [name: string]: Action<T> | {}[] } | (E | Action<T> | {})[];
62
+ export type Update<T, E = any> = ActionDef<T, E>[] | { [name: string]: Action<T> | {}[] } | (E | Action<T> | {})[];
63
63
  export type ActionOptions = {
64
64
  render?: boolean, history?, global?: boolean;
65
- callback?: (state) => void;
65
+ callback?: (state: any) => void;
66
66
  };
67
67
  export type EventOptions = {
68
68
  once?: boolean;
@@ -82,3 +82,63 @@ export type AppStartOptions<T> = {
82
82
  rendered?: (state: T) => void
83
83
  mounted?: (props: any, children: any, state: T) => T
84
84
  };
85
+ export type Router = (url: string, ...args: any[]) => any;
86
+
87
+ export type CustomElementOptions = {
88
+ render?: boolean;
89
+ shadow?: boolean;
90
+ history?: boolean;
91
+ global_event?: boolean;
92
+ observedAttributes?: string[];
93
+ };
94
+
95
+ export interface IApp {
96
+ // Event system methods
97
+ on(name: string, fn: (...args: any[]) => any, options?: EventOptions): void;
98
+ off(name: string, fn: (...args: any[]) => any): void;
99
+ run(name: string, ...args: any[]): number;
100
+ runAsync(name: string, ...args: any[]): Promise<any[]>;
101
+ once(name: string, fn: (...args: any[]) => any, options?: EventOptions): void;
102
+ find(name: string): any;
103
+
104
+ /** @deprecated Use runAsync() instead */
105
+ query(name: string, ...args: any[]): Promise<any[]>;
106
+
107
+ start<T, E = any>(element?: Element | string, model?: T, view?: View<T>, update?: Update<T, E>,
108
+ options?: AppStartOptions<T>): any;
109
+
110
+ h(tag: string | Function, props?: any, ...children: any[]): VNode | VNode[];
111
+ createElement(tag: string | Function, props?: any, ...children: any[]): VNode | VNode[];
112
+ render(element: Element | ShadowRoot, node: VNode, component?: {}): void;
113
+ Fragment(props: any, ...children: any[]): any[];
114
+ route: Router;
115
+ webComponent(name: string, componentClass: any, options?: CustomElementOptions): void;
116
+ safeHTML(html: string): any[];
117
+ use_render(render: any, mode?: 0 | 1): void;
118
+ use_react(React: any, ReactDOM: any): void;
119
+ version: string;
120
+ basePath?: string;
121
+ addComponents: (element: Element | string, components: ComponentRoute) => void;
122
+ }
123
+
124
+ // Define what constitutes a mountable component
125
+ interface ComponentLike {
126
+ mount(element?: Element | string, options?: any): any;
127
+ }
128
+
129
+ // Define component constructor type
130
+ type ComponentConstructor<T = any> = new (
131
+ state?: T,
132
+ view?: any,
133
+ update?: any,
134
+ options?: any
135
+ ) => ComponentLike;
136
+
137
+ // Enhanced ComponentRoute type with clear distinctions
138
+ export type ComponentRoute = {
139
+ [route: string]:
140
+ | ComponentLike // Component instance
141
+ | ComponentConstructor // Component class constructor
142
+ | (() => ComponentLike | ComponentConstructor | Promise<ComponentLike | ComponentConstructor>) // Factory function
143
+ | ((...args: any[]) => any) // Event handler function
144
+ };