@heshammesalam/heshjs 0.1.0 → 0.2.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/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # HeshJS
2
2
 
3
3
  An independent JavaScript/TypeScript UI framework created by Hesham. Experimental
4
- Milestone 1 release; APIs and grammar may change.
4
+ 0.2 release with History API routing and hierarchical MVC modules.
5
5
 
6
6
  Requires Node.js 22.12 or later.
7
7
 
@@ -13,6 +13,9 @@ npm run dev
13
13
  ```
14
14
 
15
15
  For TypeScript, use `npm create @heshammesalam/heshjs@latest my-app -- --ts`.
16
+ The starter is a working HMVC SPA with nested modules, Home/About/detail/not-found
17
+ routes and a persistent shell. Each module contains model, controller, View.hesh
18
+ and module registration. See `docs/hmvc.md` for APIs, lifecycle and deployment.
16
19
 
17
20
  ```hesh
18
21
  export default view Counter {
@@ -28,16 +31,22 @@ to your Vite plugins. Import a default `.hesh` component, then call
28
31
  `mount(Component, document.querySelector('#app'))` from `@heshammesalam/heshjs`.
29
32
 
30
33
  ESM entrypoints: `@heshammesalam/heshjs`, `@heshammesalam/heshjs/reactivity`, `@heshammesalam/heshjs/dom`, `@heshammesalam/heshjs/parser`,
31
- `@heshammesalam/heshjs/compiler`, `@heshammesalam/heshjs/vite`. JavaScript and declarations are included.
34
+ `@heshammesalam/heshjs/compiler`, `@heshammesalam/heshjs/vite`,
35
+ `@heshammesalam/heshjs/router`, `@heshammesalam/heshjs/hmvc`. JavaScript and declarations are included.
32
36
  The root entry contains runtime APIs only; compiler/TypeScript code stays out of
33
37
  the browser unless explicitly imported. Vite is an optional peer dependency.
34
38
 
35
39
  State is shallow: replace arrays/objects to notify. Component instances own DOM,
36
40
  effects and listener cleanup. Async handlers report errors but are not automatically
37
41
  canceled on unmount. Hesh edits reload the page. See `docs/grammar.md` for supported
38
- syntax. Props, conditional rendering, keyed lists, themes, routing, SSR and full
42
+ syntax. Props, conditional rendering, keyed lists, themes, SSR and full
39
43
  type checking inside `.hesh` are not yet implemented. TypeScript helpers and
40
44
  consumer APIs support strict type checking.
41
45
 
46
+ Router navigation intercepts eligible same-origin anchors and supports nested
47
+ outlets, :params, query/hash updates, back/forward and not-found handling. Parents
48
+ remain mounted across child navigation. Refreshing a deep link requires the host
49
+ to serve index.html for application paths; the scaffold documents that setup.
50
+
42
51
  This package is published without granting an open-source license (`UNLICENSED`).
43
52
 
@@ -0,0 +1,35 @@
1
+ import { type Component, type MountOptions } from '../dom/index.js';
2
+ import { type Signal, type Disposer } from '../reactivity/index.js';
3
+ export interface RouteLocation {
4
+ readonly url: URL;
5
+ readonly pathname: string;
6
+ readonly params: Readonly<Record<string, string>>;
7
+ readonly query: URLSearchParams;
8
+ readonly hash: string;
9
+ }
10
+ export interface ModuleContext<M = unknown, C = unknown> {
11
+ readonly route: Pick<Signal<RouteLocation>, 'get'>;
12
+ readonly parent?: ModuleContext;
13
+ readonly model: M;
14
+ readonly controller: C;
15
+ }
16
+ export interface ModuleDefinition<M, C> {
17
+ model: (context: Pick<ModuleContext, 'route' | 'parent'>) => M;
18
+ controller: (model: M, context: Pick<ModuleContext, 'route' | 'parent'>) => C;
19
+ view: Component;
20
+ }
21
+ export interface MountedModule {
22
+ readonly context: ModuleContext;
23
+ readonly outlet?: Node;
24
+ readonly dispose: Disposer;
25
+ update(location: RouteLocation): void;
26
+ }
27
+ export interface AppModule {
28
+ attach(parent: Node, location: RouteLocation, parentContext?: ModuleContext, options?: MountOptions): MountedModule;
29
+ }
30
+ export declare function useModule<M = unknown, C = unknown>(): ModuleContext<M, C>;
31
+ export declare function useController<C = unknown>(): C;
32
+ export declare function useModel<M = unknown>(): M;
33
+ /** Child route outlet. Import this component in a .hesh layout and call Outlet(). */
34
+ export declare function Outlet(parent: Node): void;
35
+ export declare function defineModule<M, C>(definition: ModuleDefinition<M, C>): AppModule;
@@ -0,0 +1,49 @@
1
+ import { mount, element } from '../dom/index.js';
2
+ import { signal } from '../reactivity/index.js';
3
+ let current;
4
+ export function useModule() {
5
+ if (!current)
6
+ throw new Error('Module context is only available during a module view mount');
7
+ return current.context;
8
+ }
9
+ export function useController() { return useModule().controller; }
10
+ export function useModel() { return useModule().model; }
11
+ /** Child route outlet. Import this component in a .hesh layout and call Outlet(). */
12
+ export function Outlet(parent) {
13
+ if (!current)
14
+ throw new Error('Outlet requires a module view');
15
+ if (current.outlet)
16
+ throw new Error('A module may render only one Outlet');
17
+ current.outlet = element(parent, 'div');
18
+ current.outlet.setAttribute('data-hesh-outlet', '');
19
+ }
20
+ export function defineModule(definition) {
21
+ return {
22
+ attach(parent, location, parentContext, options) {
23
+ const route = signal(location);
24
+ let state;
25
+ const dispose = mount(() => {
26
+ const context = { route: { get: route.get }, parent: parentContext };
27
+ const model = definition.model(context);
28
+ const controller = definition.controller(model, context);
29
+ for (const value of [model, controller]) {
30
+ if (value && typeof value.then === 'function')
31
+ throw new Error('HMVC model/controller factories must be synchronous');
32
+ }
33
+ state = { context: { ...context, model, controller } };
34
+ const previous = current;
35
+ current = state;
36
+ try {
37
+ const result = definition.view(parent);
38
+ if (result && typeof result.then === 'function')
39
+ throw new Error('HMVC views must mount synchronously');
40
+ }
41
+ finally {
42
+ current = previous;
43
+ }
44
+ }, parent, options);
45
+ return { context: state.context, outlet: state.outlet, dispose, update: next => { route.set(next); } };
46
+ }
47
+ };
48
+ }
49
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/hmvc/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,OAAO,EAAqC,MAAM,iBAAiB,CAAC;AACpF,OAAO,EAAE,MAAM,EAA8B,MAAM,wBAAwB,CAAC;AA6B5E,IAAI,OAA8D,CAAC;AAEnE,MAAM,UAAU,SAAS;IACvB,IAAI,CAAC,OAAO;QAAE,MAAM,IAAI,KAAK,CAAC,6DAA6D,CAAC,CAAC;IAC7F,OAAO,OAAO,CAAC,OAA8B,CAAC;AAChD,CAAC;AACD,MAAM,UAAU,aAAa,KAAqB,OAAO,SAAS,EAAc,CAAC,UAAU,CAAC,CAAC,CAAC;AAC9F,MAAM,UAAU,QAAQ,KAAqB,OAAO,SAAS,EAAK,CAAC,KAAK,CAAC,CAAC,CAAC;AAE3E,qFAAqF;AACrF,MAAM,UAAU,MAAM,CAAC,MAAY;IACjC,IAAI,CAAC,OAAO;QAAE,MAAM,IAAI,KAAK,CAAC,+BAA+B,CAAC,CAAC;IAC/D,IAAI,OAAO,CAAC,MAAM;QAAE,MAAM,IAAI,KAAK,CAAC,qCAAqC,CAAC,CAAC;IAC3E,OAAO,CAAC,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;IACvC,OAAO,CAAC,MAAsB,CAAC,YAAY,CAAC,kBAAkB,EAAE,EAAE,CAAC,CAAC;AACvE,CAAC;AAED,MAAM,UAAU,YAAY,CAAO,UAAkC;IACnE,OAAO;QACL,MAAM,CAAC,MAAM,EAAE,QAAQ,EAAE,aAAa,EAAE,OAAO;YAC7C,MAAM,KAAK,GAAG,MAAM,CAAC,QAAQ,CAAC,CAAC;YAC/B,IAAI,KAA4D,CAAC;YACjE,MAAM,OAAO,GAAG,KAAK,CAAC,GAAG,EAAE;gBACzB,MAAM,OAAO,GAAG,EAAE,KAAK,EAAE,EAAE,GAAG,EAAE,KAAK,CAAC,GAAG,EAAE,EAAE,MAAM,EAAE,aAAa,EAAE,CAAC;gBACrE,MAAM,KAAK,GAAG,UAAU,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;gBACxC,MAAM,UAAU,GAAG,UAAU,CAAC,UAAU,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC;gBACzD,KAAK,MAAM,KAAK,IAAI,CAAC,KAAK,EAAE,UAAU,CAAC,EAAE,CAAC;oBACxC,IAAI,KAAK,IAAI,OAAQ,KAA4B,CAAC,IAAI,KAAK,UAAU;wBAAE,MAAM,IAAI,KAAK,CAAC,qDAAqD,CAAC,CAAC;gBAChJ,CAAC;gBACD,KAAK,GAAG,EAAE,OAAO,EAAE,EAAE,GAAG,OAAO,EAAE,KAAK,EAAE,UAAU,EAAE,EAAE,CAAC;gBACvD,MAAM,QAAQ,GAAG,OAAO,CAAC;gBACzB,OAAO,GAAG,KAAK,CAAC;gBAChB,IAAI,CAAC;oBACH,MAAM,MAAM,GAAY,UAAU,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;oBAChD,IAAI,MAAM,IAAI,OAAQ,MAA6B,CAAC,IAAI,KAAK,UAAU;wBAAE,MAAM,IAAI,KAAK,CAAC,qCAAqC,CAAC,CAAC;gBAClI,CAAC;wBAAS,CAAC;oBAAC,OAAO,GAAG,QAAQ,CAAC;gBAAC,CAAC;YACnC,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,CAAC;YACpB,OAAO,EAAE,OAAO,EAAE,KAAM,CAAC,OAAO,EAAE,MAAM,EAAE,KAAM,CAAC,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,GAAG,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;QAC3G,CAAC;KACF,CAAC;AACJ,CAAC","sourcesContent":["import { mount, element, type Component, type MountOptions } from '../dom/index.js';\nimport { signal, type Signal, type Disposer } from '../reactivity/index.js';\n\nexport interface RouteLocation {\n readonly url: URL;\n readonly pathname: string;\n readonly params: Readonly<Record<string, string>>;\n readonly query: URLSearchParams;\n readonly hash: string;\n}\nexport interface ModuleContext<M = unknown, C = unknown> {\n readonly route: Pick<Signal<RouteLocation>, 'get'>;\n readonly parent?: ModuleContext;\n readonly model: M;\n readonly controller: C;\n}\nexport interface ModuleDefinition<M, C> {\n model: (context: Pick<ModuleContext, 'route' | 'parent'>) => M;\n controller: (model: M, context: Pick<ModuleContext, 'route' | 'parent'>) => C;\n view: Component;\n}\nexport interface MountedModule {\n readonly context: ModuleContext;\n readonly outlet?: Node;\n readonly dispose: Disposer;\n update(location: RouteLocation): void;\n}\nexport interface AppModule {\n attach(parent: Node, location: RouteLocation, parentContext?: ModuleContext, options?: MountOptions): MountedModule;\n}\nlet current: { context: ModuleContext; outlet?: Node } | undefined;\n\nexport function useModule<M = unknown, C = unknown>(): ModuleContext<M, C> {\n if (!current) throw new Error('Module context is only available during a module view mount');\n return current.context as ModuleContext<M, C>;\n}\nexport function useController<C = unknown>(): C { return useModule<unknown, C>().controller; }\nexport function useModel<M = unknown>(): M { return useModule<M>().model; }\n\n/** Child route outlet. Import this component in a .hesh layout and call Outlet(). */\nexport function Outlet(parent: Node): void {\n if (!current) throw new Error('Outlet requires a module view');\n if (current.outlet) throw new Error('A module may render only one Outlet');\n current.outlet = element(parent, 'div');\n (current.outlet as HTMLElement).setAttribute('data-hesh-outlet', '');\n}\n\nexport function defineModule<M, C>(definition: ModuleDefinition<M, C>): AppModule {\n return {\n attach(parent, location, parentContext, options) {\n const route = signal(location);\n let state: { context: ModuleContext; outlet?: Node } | undefined;\n const dispose = mount(() => {\n const context = { route: { get: route.get }, parent: parentContext };\n const model = definition.model(context);\n const controller = definition.controller(model, context);\n for (const value of [model, controller]) {\n if (value && typeof (value as { then?: unknown }).then === 'function') throw new Error('HMVC model/controller factories must be synchronous');\n }\n state = { context: { ...context, model, controller } };\n const previous = current;\n current = state;\n try {\n const result: unknown = definition.view(parent);\n if (result && typeof (result as { then?: unknown }).then === 'function') throw new Error('HMVC views must mount synchronously');\n } finally { current = previous; }\n }, parent, options);\n return { context: state!.context, outlet: state!.outlet, dispose, update: next => { route.set(next); } };\n }\n };\n}\n"]}
@@ -0,0 +1,24 @@
1
+ import { type Disposer } from '../reactivity/index.js';
2
+ import type { AppModule, RouteLocation } from '../hmvc/index.js';
3
+ export type { RouteLocation } from '../hmvc/index.js';
4
+ export interface RouteRecord {
5
+ path: string;
6
+ module: AppModule;
7
+ children?: readonly RouteRecord[];
8
+ }
9
+ export interface RouterOptions {
10
+ routes: readonly RouteRecord[];
11
+ window?: Window;
12
+ notFound?: AppModule;
13
+ onError?: (error: unknown) => void;
14
+ }
15
+ export declare function createRouter(options: RouterOptions): {
16
+ location: {
17
+ get: () => RouteLocation;
18
+ };
19
+ navigate: (to: string, navigation?: {
20
+ replace?: boolean;
21
+ }) => void;
22
+ stop: () => void;
23
+ start(parent: Node): Disposer;
24
+ };
@@ -0,0 +1,258 @@
1
+ import { signal, batch } from '../reactivity/index.js';
2
+ function segments(path) { return path.split('/').filter(Boolean); }
3
+ function validate(routes, ancestors = new Set()) {
4
+ const siblings = new Set();
5
+ for (const route of routes) {
6
+ if (/[?#]/.test(route.path))
7
+ throw new Error(`Route paths cannot contain query/hash: ${route.path}`);
8
+ const parts = segments(route.path);
9
+ const signature = parts.map(p => p.startsWith(':') ? ':' : p).join('/');
10
+ if (siblings.has(signature))
11
+ throw new Error(`Duplicate sibling route: ${route.path}`);
12
+ siblings.add(signature);
13
+ const names = new Set(ancestors);
14
+ parts.forEach((p, i) => {
15
+ if (p === '*' && (i !== parts.length - 1 || route.children?.length))
16
+ throw new Error('Wildcard must end a leaf route');
17
+ if (p.startsWith(':')) {
18
+ const name = p.slice(1);
19
+ if (!/^[a-zA-Z_][a-zA-Z\d_]*$/.test(name) || names.has(name) || ['__proto__', 'prototype', 'constructor'].includes(name))
20
+ throw new Error(`Invalid or duplicate route param: ${name}`);
21
+ names.add(name);
22
+ }
23
+ });
24
+ validate(route.children ?? [], names);
25
+ }
26
+ }
27
+ function find(routes, parts, at = 0, params = {}) {
28
+ const ordered = [...routes].sort((a, b) => {
29
+ const aa = segments(a.path), bb = segments(b.path);
30
+ for (let i = 0; i < Math.max(aa.length, bb.length); i++) {
31
+ const rank = (p) => p === '*' ? 0 : p?.startsWith(':') ? 1 : p ? 2 : 0.5;
32
+ const diff = rank(bb[i]) - rank(aa[i]);
33
+ if (diff)
34
+ return diff;
35
+ }
36
+ return 0;
37
+ });
38
+ for (const record of ordered) {
39
+ const own = { ...params };
40
+ let cursor = at, matched = true;
41
+ for (const part of segments(record.path)) {
42
+ if (part === '*') {
43
+ own['wildcard'] = parts.slice(cursor).join('/');
44
+ cursor = parts.length;
45
+ break;
46
+ }
47
+ const actual = parts[cursor++];
48
+ if (actual === undefined) {
49
+ matched = false;
50
+ break;
51
+ }
52
+ if (part.startsWith(':'))
53
+ own[part.slice(1)] = actual;
54
+ else if (part !== actual) {
55
+ matched = false;
56
+ break;
57
+ }
58
+ }
59
+ if (!matched)
60
+ continue;
61
+ const entry = { record, params: own };
62
+ const children = find(record.children ?? [], parts, cursor, own);
63
+ if (children)
64
+ return [entry, ...children];
65
+ if (cursor === parts.length)
66
+ return [entry];
67
+ }
68
+ return undefined;
69
+ }
70
+ export function createRouter(options) {
71
+ validate(options.routes);
72
+ const browser = options.window ?? (typeof window !== 'undefined' ? window : undefined);
73
+ if (!browser)
74
+ throw new Error('createRouter requires a browser Window');
75
+ const report = options.onError ?? ((error) => console.error(error));
76
+ const location = signal(makeLocation(new URL(browser.location.href), {}));
77
+ let root, active = [], fallback;
78
+ let transitioning = false;
79
+ let generation = 0;
80
+ const notFoundRecord = options.notFound ? { path: '*', module: options.notFound } : undefined;
81
+ function makeLocation(url, params) {
82
+ return Object.freeze({ url: new URL(url), pathname: url.pathname, params: Object.freeze({ ...params }), query: new URLSearchParams(url.search), hash: url.hash });
83
+ }
84
+ function render(url) {
85
+ if (!root)
86
+ throw new Error('Start the router before navigating');
87
+ if (transitioning)
88
+ throw new Error('Navigation during module mounting is not supported');
89
+ transitioning = true;
90
+ try {
91
+ let entries;
92
+ try {
93
+ entries = find(options.routes, segments(url.pathname).map(decodeURIComponent));
94
+ }
95
+ catch (error) {
96
+ if (!(error instanceof URIError))
97
+ throw error;
98
+ }
99
+ if (!entries && notFoundRecord)
100
+ entries = [{ record: notFoundRecord, params: {} }];
101
+ entries ??= [];
102
+ let keep = 0;
103
+ while (keep < active.length && keep < entries.length && active[keep].record === entries[keep].record && JSON.stringify(active[keep].params) === JSON.stringify(entries[keep].params))
104
+ keep++;
105
+ const removed = active.splice(keep);
106
+ const errors = [];
107
+ for (const item of removed.reverse()) {
108
+ try {
109
+ item.instance.dispose();
110
+ }
111
+ catch (error) {
112
+ errors.push(error);
113
+ }
114
+ }
115
+ fallback?.remove();
116
+ fallback = undefined;
117
+ if (errors.length)
118
+ throw new AggregateError(errors, 'Route cleanup failed');
119
+ const added = [];
120
+ try {
121
+ for (let i = keep; i < entries.length; i++) {
122
+ const entry = entries[i];
123
+ const parent = active.at(-1);
124
+ const outlet = parent ? parent.instance.outlet : root;
125
+ if (!outlet)
126
+ throw new Error(`Parent module ${parent.record.path} must render Outlet()`);
127
+ const instance = entry.record.module.attach(outlet, makeLocation(url, entry.params), parent?.instance.context, { onError: report });
128
+ const item = { ...entry, instance };
129
+ active.push(item);
130
+ added.push(item);
131
+ }
132
+ }
133
+ catch (error) {
134
+ active.splice(keep);
135
+ for (const item of added.reverse()) {
136
+ try {
137
+ item.instance.dispose();
138
+ }
139
+ catch (cleanupError) {
140
+ report(cleanupError);
141
+ }
142
+ }
143
+ throw error;
144
+ }
145
+ if (!entries.length) {
146
+ fallback = root.ownerDocument.createElement('p');
147
+ fallback.setAttribute('role', 'status');
148
+ fallback.textContent = 'Page not found';
149
+ root.appendChild(fallback);
150
+ }
151
+ batch(() => {
152
+ for (const item of active)
153
+ item.instance.update(makeLocation(url, item.params));
154
+ location.set(makeLocation(url, entries.at(-1)?.params ?? {}));
155
+ });
156
+ }
157
+ finally {
158
+ transitioning = false;
159
+ }
160
+ }
161
+ function navigate(to, navigation = {}) {
162
+ if (!root)
163
+ throw new Error('Start the router before navigating');
164
+ const url = new URL(to, browser.location.href);
165
+ if (!['http:', 'https:'].includes(url.protocol) || url.origin !== browser.location.origin)
166
+ throw new Error('Router navigation requires a same-origin HTTP(S) URL');
167
+ if (url.href === browser.location.href)
168
+ return;
169
+ render(url);
170
+ browser.history[navigation.replace ? 'replaceState' : 'pushState'](null, '', url.href);
171
+ }
172
+ const popstate = () => {
173
+ try {
174
+ if (location.get().url.href !== browser.location.href)
175
+ render(new URL(browser.location.href));
176
+ }
177
+ catch (error) {
178
+ report(error);
179
+ }
180
+ };
181
+ const click = (event) => {
182
+ if (event.defaultPrevented || event.button !== 0 || event.metaKey || event.ctrlKey || event.altKey || event.shiftKey)
183
+ return;
184
+ const target = event.target;
185
+ const anchor = target?.closest?.('a[href]');
186
+ if (!anchor || anchor.hasAttribute('download') || anchor.hasAttribute('data-hesh-reload') || (anchor.getAttribute('target') && anchor.getAttribute('target').toLowerCase() !== '_self'))
187
+ return;
188
+ const href = anchor.getAttribute('href');
189
+ if (href.startsWith('#'))
190
+ return;
191
+ let url;
192
+ try {
193
+ url = new URL(href, browser.document.baseURI);
194
+ }
195
+ catch {
196
+ return;
197
+ }
198
+ if (!['http:', 'https:'].includes(url.protocol) || url.origin !== browser.location.origin)
199
+ return;
200
+ if (url.pathname === browser.location.pathname && url.search === browser.location.search && url.hash)
201
+ return;
202
+ event.preventDefault();
203
+ try {
204
+ navigate(url.href);
205
+ }
206
+ catch (error) {
207
+ report(error);
208
+ }
209
+ };
210
+ function stop() {
211
+ if (transitioning)
212
+ throw new Error('Cannot stop the router during module mounting');
213
+ browser.document.removeEventListener('click', click);
214
+ browser.removeEventListener('popstate', popstate);
215
+ browser.removeEventListener('hashchange', popstate);
216
+ const old = active;
217
+ active = [];
218
+ root = undefined;
219
+ generation++;
220
+ fallback?.remove();
221
+ fallback = undefined;
222
+ const errors = [];
223
+ for (const item of old.reverse()) {
224
+ try {
225
+ item.instance.dispose();
226
+ }
227
+ catch (error) {
228
+ errors.push(error);
229
+ }
230
+ }
231
+ if (errors.length)
232
+ throw new AggregateError(errors, 'Router cleanup failed');
233
+ }
234
+ return {
235
+ location: { get: location.get }, navigate, stop,
236
+ start(parent) {
237
+ if (root)
238
+ throw new Error('Router is already started');
239
+ if (parent.ownerDocument !== browser.document)
240
+ throw new Error('Router root must belong to its Window');
241
+ const started = ++generation;
242
+ root = parent;
243
+ try {
244
+ render(new URL(browser.location.href));
245
+ }
246
+ catch (error) {
247
+ stop();
248
+ throw error;
249
+ }
250
+ browser.document.addEventListener('click', click);
251
+ browser.addEventListener('popstate', popstate);
252
+ browser.addEventListener('hashchange', popstate);
253
+ return () => { if (generation === started)
254
+ stop(); };
255
+ }
256
+ };
257
+ }
258
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/router/index.ts"],"names":[],"mappings":"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","sourcesContent":["import { signal, batch, type Disposer } from '../reactivity/index.js';\nimport type { AppModule, MountedModule, RouteLocation } from '../hmvc/index.js';\nexport type { RouteLocation } from '../hmvc/index.js';\nexport interface RouteRecord { path: string; module: AppModule; children?: readonly RouteRecord[] }\nexport interface RouterOptions {\n routes: readonly RouteRecord[];\n window?: Window;\n notFound?: AppModule;\n onError?: (error: unknown) => void;\n}\ninterface Entry { record: RouteRecord; params: Record<string, string> }\ninterface Active extends Entry { instance: MountedModule }\n\nfunction segments(path: string): string[] { return path.split('/').filter(Boolean); }\nfunction validate(routes: readonly RouteRecord[], ancestors = new Set<string>()): void {\n const siblings = new Set<string>();\n for (const route of routes) {\n if (/[?#]/.test(route.path)) throw new Error(`Route paths cannot contain query/hash: ${route.path}`);\n const parts = segments(route.path);\n const signature = parts.map(p => p.startsWith(':') ? ':' : p).join('/');\n if (siblings.has(signature)) throw new Error(`Duplicate sibling route: ${route.path}`);\n siblings.add(signature);\n const names = new Set(ancestors);\n parts.forEach((p, i) => {\n if (p === '*' && (i !== parts.length - 1 || route.children?.length)) throw new Error('Wildcard must end a leaf route');\n if (p.startsWith(':')) {\n const name = p.slice(1);\n if (!/^[a-zA-Z_][a-zA-Z\\d_]*$/.test(name) || names.has(name) || ['__proto__', 'prototype', 'constructor'].includes(name)) throw new Error(`Invalid or duplicate route param: ${name}`);\n names.add(name);\n }\n });\n validate(route.children ?? [], names);\n }\n}\nfunction find(routes: readonly RouteRecord[], parts: string[], at = 0, params: Record<string, string> = {}): Entry[] | undefined {\n const ordered = [...routes].sort((a, b) => {\n const aa = segments(a.path), bb = segments(b.path);\n for (let i = 0; i < Math.max(aa.length, bb.length); i++) {\n const rank = (p?: string) => p === '*' ? 0 : p?.startsWith(':') ? 1 : p ? 2 : 0.5;\n const diff = rank(bb[i]) - rank(aa[i]); if (diff) return diff;\n }\n return 0;\n });\n for (const record of ordered) {\n const own = { ...params }; let cursor = at, matched = true;\n for (const part of segments(record.path)) {\n if (part === '*') { own['wildcard'] = parts.slice(cursor).join('/'); cursor = parts.length; break; }\n const actual = parts[cursor++];\n if (actual === undefined) { matched = false; break; }\n if (part.startsWith(':')) own[part.slice(1)] = actual;\n else if (part !== actual) { matched = false; break; }\n }\n if (!matched) continue;\n const entry = { record, params: own };\n const children = find(record.children ?? [], parts, cursor, own);\n if (children) return [entry, ...children];\n if (cursor === parts.length) return [entry];\n }\n return undefined;\n}\n\nexport function createRouter(options: RouterOptions) {\n validate(options.routes);\n const browser = options.window ?? (typeof window !== 'undefined' ? window : undefined);\n if (!browser) throw new Error('createRouter requires a browser Window');\n const report = options.onError ?? ((error: unknown) => console.error(error));\n const location = signal<RouteLocation>(makeLocation(new URL(browser.location.href), {}));\n let root: Node | undefined, active: Active[] = [], fallback: HTMLElement | undefined;\n let transitioning = false;\n let generation = 0;\n const notFoundRecord: RouteRecord | undefined = options.notFound ? { path: '*', module: options.notFound } : undefined;\n function makeLocation(url: URL, params: Record<string, string>): RouteLocation {\n return Object.freeze({ url: new URL(url), pathname: url.pathname, params: Object.freeze({ ...params }), query: new URLSearchParams(url.search), hash: url.hash });\n }\n function render(url: URL): void {\n if (!root) throw new Error('Start the router before navigating');\n if (transitioning) throw new Error('Navigation during module mounting is not supported');\n transitioning = true;\n try {\n let entries: Entry[] | undefined;\n try { entries = find(options.routes, segments(url.pathname).map(decodeURIComponent)); }\n catch (error) { if (!(error instanceof URIError)) throw error; }\n if (!entries && notFoundRecord) entries = [{ record: notFoundRecord, params: {} }];\n entries ??= [];\n let keep = 0;\n while (keep < active.length && keep < entries.length && active[keep]!.record === entries[keep]!.record && JSON.stringify(active[keep]!.params) === JSON.stringify(entries[keep]!.params)) keep++;\n const removed = active.splice(keep);\n const errors: unknown[] = [];\n for (const item of removed.reverse()) { try { item.instance.dispose(); } catch (error) { errors.push(error); } }\n fallback?.remove(); fallback = undefined;\n if (errors.length) throw new AggregateError(errors, 'Route cleanup failed');\n const added: Active[] = [];\n try {\n for (let i = keep; i < entries.length; i++) {\n const entry = entries[i]!;\n const parent = active.at(-1);\n const outlet = parent ? parent.instance.outlet : root;\n if (!outlet) throw new Error(`Parent module ${parent!.record.path} must render Outlet()`);\n const instance = entry.record.module.attach(outlet, makeLocation(url, entry.params), parent?.instance.context, { onError: report });\n const item = { ...entry, instance };\n active.push(item); added.push(item);\n }\n } catch (error) {\n active.splice(keep);\n for (const item of added.reverse()) { try { item.instance.dispose(); } catch (cleanupError) { report(cleanupError); } }\n throw error;\n }\n if (!entries.length) {\n fallback = root.ownerDocument!.createElement('p');\n fallback.setAttribute('role', 'status'); fallback.textContent = 'Page not found'; root.appendChild(fallback);\n }\n batch(() => {\n for (const item of active) item.instance.update(makeLocation(url, item.params));\n location.set(makeLocation(url, entries.at(-1)?.params ?? {}));\n });\n } finally { transitioning = false; }\n }\n function navigate(to: string, navigation: { replace?: boolean } = {}): void {\n if (!root) throw new Error('Start the router before navigating');\n const url = new URL(to, browser!.location.href);\n if (!['http:', 'https:'].includes(url.protocol) || url.origin !== browser!.location.origin) throw new Error('Router navigation requires a same-origin HTTP(S) URL');\n if (url.href === browser!.location.href) return;\n render(url);\n browser!.history[navigation.replace ? 'replaceState' : 'pushState'](null, '', url.href);\n }\n const popstate = () => {\n try { if (location.get().url.href !== browser!.location.href) render(new URL(browser!.location.href)); }\n catch (error) { report(error); }\n };\n const click = (event: MouseEvent) => {\n if (event.defaultPrevented || event.button !== 0 || event.metaKey || event.ctrlKey || event.altKey || event.shiftKey) return;\n const target = event.target as Element | null;\n const anchor = target?.closest?.('a[href]');\n if (!anchor || anchor.hasAttribute('download') || anchor.hasAttribute('data-hesh-reload') || (anchor.getAttribute('target') && anchor.getAttribute('target')!.toLowerCase() !== '_self')) return;\n const href = anchor.getAttribute('href')!;\n if (href.startsWith('#')) return;\n let url: URL;\n try { url = new URL(href, browser!.document.baseURI); } catch { return; }\n if (!['http:', 'https:'].includes(url.protocol) || url.origin !== browser!.location.origin) return;\n if (url.pathname === browser!.location.pathname && url.search === browser!.location.search && url.hash) return;\n event.preventDefault();\n try { navigate(url.href); } catch (error) { report(error); }\n };\n function stop(): void {\n if (transitioning) throw new Error('Cannot stop the router during module mounting');\n browser!.document.removeEventListener('click', click);\n browser!.removeEventListener('popstate', popstate);\n browser!.removeEventListener('hashchange', popstate);\n const old = active; active = []; root = undefined;\n generation++;\n fallback?.remove(); fallback = undefined;\n const errors: unknown[] = [];\n for (const item of old.reverse()) { try { item.instance.dispose(); } catch (error) { errors.push(error); } }\n if (errors.length) throw new AggregateError(errors, 'Router cleanup failed');\n }\n return {\n location: { get: location.get }, navigate, stop,\n start(parent: Node): Disposer {\n if (root) throw new Error('Router is already started');\n if (parent.ownerDocument !== browser!.document) throw new Error('Router root must belong to its Window');\n const started = ++generation;\n root = parent;\n try { render(new URL(browser!.location.href)); }\n catch (error) { stop(); throw error; }\n browser!.document.addEventListener('click', click);\n browser!.addEventListener('popstate', popstate);\n browser!.addEventListener('hashchange', popstate);\n return () => { if (generation === started) stop(); };\n }\n };\n}\n"]}
@@ -3,6 +3,7 @@ export function hesh() {
3
3
  let production = false;
4
4
  return {
5
5
  name: 'heshjs', enforce: 'pre',
6
+ config() { return { appType: 'spa' }; },
6
7
  configResolved(config) { production = config.command === 'build'; },
7
8
  transform(source, id) {
8
9
  if (!id.endsWith('.hesh'))
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/vite/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,MAAM,sBAAsB,CAAC;AAC/C,MAAM,UAAU,IAAI;IAClB,IAAI,UAAU,GAAG,KAAK,CAAC;IACvB,OAAO;QACL,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,KAAK;QAC9B,cAAc,CAAC,MAAM,IAAI,UAAU,GAAG,MAAM,CAAC,OAAO,KAAK,OAAO,CAAC,CAAC,CAAC;QACnE,SAAS,CAAC,MAAM,EAAE,EAAE;YAClB,IAAI,CAAC,EAAE,CAAC,QAAQ,CAAC,OAAO,CAAC;gBAAE,OAAO;YAClC,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,EAAE,QAAQ,EAAE,EAAE,EAAE,WAAW,EAAE,CAAC,UAAU,EAAE,CAAC,CAAC;YAC3E,OAAO,EAAE,IAAI,EAAE,MAAM,CAAC,IAAI,EAAE,GAAG,EAAE,IAAI,EAAE,CAAC;QAC1C,CAAC;QACD,eAAe,CAAC,OAAO;YACrB,IAAI,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC;gBAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,aAAa,EAAE,CAAC,CAAC;gBAAC,OAAO,EAAE,CAAC;YAAC,CAAC;QACrG,CAAC;KACF,CAAC;AACJ,CAAC","sourcesContent":["import type { Plugin } from 'vite';\nimport { compile } from '../compiler/index.js';\nexport function hesh(): Plugin {\n let production = false;\n return {\n name: 'heshjs', enforce: 'pre',\n configResolved(config) { production = config.command === 'build'; },\n transform(source, id) {\n if (!id.endsWith('.hesh')) return;\n const result = compile(source, { filename: id, development: !production });\n return { code: result.code, map: null };\n },\n handleHotUpdate(context) {\n if (context.file.endsWith('.hesh')) { context.server.ws.send({ type: 'full-reload' }); return []; }\n }\n };\n}\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/vite/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,MAAM,sBAAsB,CAAC;AAC/C,MAAM,UAAU,IAAI;IAClB,IAAI,UAAU,GAAG,KAAK,CAAC;IACvB,OAAO;QACL,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,KAAK;QAC9B,MAAM,KAAK,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC;QACvC,cAAc,CAAC,MAAM,IAAI,UAAU,GAAG,MAAM,CAAC,OAAO,KAAK,OAAO,CAAC,CAAC,CAAC;QACnE,SAAS,CAAC,MAAM,EAAE,EAAE;YAClB,IAAI,CAAC,EAAE,CAAC,QAAQ,CAAC,OAAO,CAAC;gBAAE,OAAO;YAClC,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,EAAE,QAAQ,EAAE,EAAE,EAAE,WAAW,EAAE,CAAC,UAAU,EAAE,CAAC,CAAC;YAC3E,OAAO,EAAE,IAAI,EAAE,MAAM,CAAC,IAAI,EAAE,GAAG,EAAE,IAAI,EAAE,CAAC;QAC1C,CAAC;QACD,eAAe,CAAC,OAAO;YACrB,IAAI,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC;gBAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,aAAa,EAAE,CAAC,CAAC;gBAAC,OAAO,EAAE,CAAC;YAAC,CAAC;QACrG,CAAC;KACF,CAAC;AACJ,CAAC","sourcesContent":["import type { Plugin } from 'vite';\nimport { compile } from '../compiler/index.js';\nexport function hesh(): Plugin {\n let production = false;\n return {\n name: 'heshjs', enforce: 'pre',\n config() { return { appType: 'spa' }; },\n configResolved(config) { production = config.command === 'build'; },\n transform(source, id) {\n if (!id.endsWith('.hesh')) return;\n const result = compile(source, { filename: id, development: !production });\n return { code: result.code, map: null };\n },\n handleHotUpdate(context) {\n if (context.file.endsWith('.hesh')) { context.server.ws.send({ type: 'full-reload' }); return []; }\n }\n };\n}\n"]}
package/docs/hmvc.md ADDED
@@ -0,0 +1,134 @@
1
+ # Building an HMVC single-page application
2
+
3
+ Requires HeshJS 0.2.0. New JavaScript/TypeScript projects use this structure:
4
+
5
+ ```text
6
+ src/
7
+ main.js # starts the router (.ts for TypeScript)
8
+ app.js # explicit route tree
9
+ modules/
10
+ shell/
11
+ model.js
12
+ controller.js
13
+ View.hesh
14
+ Navigation.js
15
+ module.js
16
+ home/
17
+ model.js
18
+ controller.js
19
+ View.hesh
20
+ module.js
21
+ modules/
22
+ detail/ # independent nested MVC triad
23
+ model.js
24
+ controller.js
25
+ View.hesh
26
+ module.js
27
+ about/ # same MVC structure
28
+ notFound/ # same MVC structure
29
+ ```
30
+
31
+ This is an application architecture. Compiler, parser and runtime packages stay
32
+ separate infrastructure packages; they are not model/view/controller folders.
33
+
34
+ ## Define a module
35
+
36
+ ```js
37
+ // model.js
38
+ import { signal } from '@heshammesalam/heshjs';
39
+ export const createModel = () => ({ count: signal(0) });
40
+
41
+ // controller.js
42
+ export const createController = (model, context) => ({
43
+ count: model.count,
44
+ increment: () => model.count.set(model.count.get() + 1),
45
+ id: () => context.route.get().params.id
46
+ });
47
+ ```
48
+
49
+ ```hesh
50
+ // View.hesh
51
+ import { useController, Outlet } from '@heshammesalam/heshjs/hmvc';
52
+ export default view Counter {
53
+ const controller = useController();
54
+ h1("Count: {controller.count.get()}");
55
+ button("+", { controller.increment(); });
56
+ Outlet();
57
+ }
58
+ ```
59
+
60
+ ```js
61
+ // module.js
62
+ import { defineModule } from '@heshammesalam/heshjs/hmvc';
63
+ import { createModel } from './model.js';
64
+ import { createController } from './controller.js';
65
+ import View from './View.hesh';
66
+ export const counterModule = defineModule({ model: createModel, controller: createController, view: View });
67
+ ```
68
+
69
+ Factories run once per mounted instance. Use `onCleanup` from HeshJS in factories
70
+ to dispose your subscriptions/resources. Do not call factories at import time if
71
+ their state should be instance-local. `useController`, `useModel` and `useModule`
72
+ are synchronous mount-time accessors. Capture the result in your view; events use
73
+ the captured controller. A controller receives `context.parent` for explicit access
74
+ to the parent module's model/controller. No global store or dependency injection
75
+ container is required. Generic `useController<MyController>()` is available in TS
76
+ view/helper modules, while `.hesh` remains JavaScript without full type checking.
77
+
78
+ ## Declare the route hierarchy
79
+
80
+ ```js
81
+ import { createRouter } from '@heshammesalam/heshjs/router';
82
+ // Import the modules declared by your application.
83
+ export const router = createRouter({ routes: [
84
+ { path: '/', module: shellModule, children: [
85
+ { path: '', module: homeModule, children: [
86
+ { path: 'counter/:id', module: detailModule }
87
+ ] },
88
+ { path: 'about', module: aboutModule },
89
+ { path: '*', module: notFoundModule }
90
+ ] }
91
+ ] });
92
+
93
+ const dispose = router.start(document.querySelector('#app'));
94
+ router.navigate('/counter/42?from=home');
95
+ router.navigate('/about', { replace: true });
96
+ // dispose() or router.stop() releases all modules and router listeners.
97
+ ```
98
+
99
+ Parents with child routes must render one `Outlet()`. Layout state and DOM stay
100
+ mounted when navigating between their children. Leaving a parent resets its state
101
+ on the next visit. Changes to that module's path parameters recreate the module
102
+ and descendants. Query/hash changes update its reactive `context.route.get()`
103
+ without recreating matching instances. A parent's params contain only parameters
104
+ matched up to that parent; `router.location.get()` contains the deepest match.
105
+
106
+ Ordinary same-origin anchor clicks update browser history without document reload.
107
+ Use DOM APIs for anchor attributes for now; declarative attribute syntax is not yet
108
+ part of Hesh grammar. The starter's Navigation component is an example. External,
109
+ download, modifier-key and non-self-target links retain browser behavior. Set
110
+ `data-hesh-reload` on an anchor to opt out. Back/forward and initial deep URLs work.
111
+ There is no base-path option yet; deploy at the origin root or define prefixed routes.
112
+
113
+ Unmatched URLs use `notFound`, a `*` route, or a safe built-in text message.
114
+ `onError` receives navigation/event errors. Programmatic navigation throws errors
115
+ to its caller. Setup must be synchronous. On setup failure new instances are
116
+ disposed, but previously removed children are not restored. Guards, lazy loading,
117
+ async controllers, server authorization and SSR are not implemented.
118
+
119
+ ## Serve deep links in production
120
+
121
+ Vite dev/preview use SPA fallback. Production hosts must return index.html for
122
+ application paths. The client router cannot fix an HTTP 404 served before JS loads.
123
+
124
+ For nginx, inside your site's configuration:
125
+
126
+ ```nginx
127
+ location / {
128
+ try_files $uri $uri/ /index.html;
129
+ }
130
+ ```
131
+
132
+ The scaffold includes `public/_redirects` containing `/* /index.html 200` for hosts
133
+ that support that convention. Configure asset/API routes separately on your host.
134
+ This project does not modify your deployment server automatically.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@heshammesalam/heshjs",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "An experimental independent reactive UI framework with brace-based .hesh components",
5
5
  "author": "Hesham",
6
6
  "license": "UNLICENSED",
@@ -14,6 +14,8 @@
14
14
  "./parser": { "types": "./dist/parser/index.d.ts", "import": "./dist/parser/index.js" },
15
15
  "./compiler": { "types": "./dist/compiler/index.d.ts", "import": "./dist/compiler/index.js" },
16
16
  "./vite": { "types": "./dist/vite/index.d.ts", "import": "./dist/vite/index.js" },
17
+ "./router": { "types": "./dist/router/index.d.ts", "import": "./dist/router/index.js" },
18
+ "./hmvc": { "types": "./dist/hmvc/index.d.ts", "import": "./dist/hmvc/index.js" },
17
19
  "./package.json": "./package.json"
18
20
  },
19
21
  "files": ["dist", "src", "docs", "README.md"],
@@ -0,0 +1,71 @@
1
+ import { mount, element, type Component, type MountOptions } from '../dom/index.js';
2
+ import { signal, type Signal, type Disposer } from '../reactivity/index.js';
3
+
4
+ export interface RouteLocation {
5
+ readonly url: URL;
6
+ readonly pathname: string;
7
+ readonly params: Readonly<Record<string, string>>;
8
+ readonly query: URLSearchParams;
9
+ readonly hash: string;
10
+ }
11
+ export interface ModuleContext<M = unknown, C = unknown> {
12
+ readonly route: Pick<Signal<RouteLocation>, 'get'>;
13
+ readonly parent?: ModuleContext;
14
+ readonly model: M;
15
+ readonly controller: C;
16
+ }
17
+ export interface ModuleDefinition<M, C> {
18
+ model: (context: Pick<ModuleContext, 'route' | 'parent'>) => M;
19
+ controller: (model: M, context: Pick<ModuleContext, 'route' | 'parent'>) => C;
20
+ view: Component;
21
+ }
22
+ export interface MountedModule {
23
+ readonly context: ModuleContext;
24
+ readonly outlet?: Node;
25
+ readonly dispose: Disposer;
26
+ update(location: RouteLocation): void;
27
+ }
28
+ export interface AppModule {
29
+ attach(parent: Node, location: RouteLocation, parentContext?: ModuleContext, options?: MountOptions): MountedModule;
30
+ }
31
+ let current: { context: ModuleContext; outlet?: Node } | undefined;
32
+
33
+ export function useModule<M = unknown, C = unknown>(): ModuleContext<M, C> {
34
+ if (!current) throw new Error('Module context is only available during a module view mount');
35
+ return current.context as ModuleContext<M, C>;
36
+ }
37
+ export function useController<C = unknown>(): C { return useModule<unknown, C>().controller; }
38
+ export function useModel<M = unknown>(): M { return useModule<M>().model; }
39
+
40
+ /** Child route outlet. Import this component in a .hesh layout and call Outlet(). */
41
+ export function Outlet(parent: Node): void {
42
+ if (!current) throw new Error('Outlet requires a module view');
43
+ if (current.outlet) throw new Error('A module may render only one Outlet');
44
+ current.outlet = element(parent, 'div');
45
+ (current.outlet as HTMLElement).setAttribute('data-hesh-outlet', '');
46
+ }
47
+
48
+ export function defineModule<M, C>(definition: ModuleDefinition<M, C>): AppModule {
49
+ return {
50
+ attach(parent, location, parentContext, options) {
51
+ const route = signal(location);
52
+ let state: { context: ModuleContext; outlet?: Node } | undefined;
53
+ const dispose = mount(() => {
54
+ const context = { route: { get: route.get }, parent: parentContext };
55
+ const model = definition.model(context);
56
+ const controller = definition.controller(model, context);
57
+ for (const value of [model, controller]) {
58
+ if (value && typeof (value as { then?: unknown }).then === 'function') throw new Error('HMVC model/controller factories must be synchronous');
59
+ }
60
+ state = { context: { ...context, model, controller } };
61
+ const previous = current;
62
+ current = state;
63
+ try {
64
+ const result: unknown = definition.view(parent);
65
+ if (result && typeof (result as { then?: unknown }).then === 'function') throw new Error('HMVC views must mount synchronously');
66
+ } finally { current = previous; }
67
+ }, parent, options);
68
+ return { context: state!.context, outlet: state!.outlet, dispose, update: next => { route.set(next); } };
69
+ }
70
+ };
71
+ }
@@ -0,0 +1,171 @@
1
+ import { signal, batch, type Disposer } from '../reactivity/index.js';
2
+ import type { AppModule, MountedModule, RouteLocation } from '../hmvc/index.js';
3
+ export type { RouteLocation } from '../hmvc/index.js';
4
+ export interface RouteRecord { path: string; module: AppModule; children?: readonly RouteRecord[] }
5
+ export interface RouterOptions {
6
+ routes: readonly RouteRecord[];
7
+ window?: Window;
8
+ notFound?: AppModule;
9
+ onError?: (error: unknown) => void;
10
+ }
11
+ interface Entry { record: RouteRecord; params: Record<string, string> }
12
+ interface Active extends Entry { instance: MountedModule }
13
+
14
+ function segments(path: string): string[] { return path.split('/').filter(Boolean); }
15
+ function validate(routes: readonly RouteRecord[], ancestors = new Set<string>()): void {
16
+ const siblings = new Set<string>();
17
+ for (const route of routes) {
18
+ if (/[?#]/.test(route.path)) throw new Error(`Route paths cannot contain query/hash: ${route.path}`);
19
+ const parts = segments(route.path);
20
+ const signature = parts.map(p => p.startsWith(':') ? ':' : p).join('/');
21
+ if (siblings.has(signature)) throw new Error(`Duplicate sibling route: ${route.path}`);
22
+ siblings.add(signature);
23
+ const names = new Set(ancestors);
24
+ parts.forEach((p, i) => {
25
+ if (p === '*' && (i !== parts.length - 1 || route.children?.length)) throw new Error('Wildcard must end a leaf route');
26
+ if (p.startsWith(':')) {
27
+ const name = p.slice(1);
28
+ if (!/^[a-zA-Z_][a-zA-Z\d_]*$/.test(name) || names.has(name) || ['__proto__', 'prototype', 'constructor'].includes(name)) throw new Error(`Invalid or duplicate route param: ${name}`);
29
+ names.add(name);
30
+ }
31
+ });
32
+ validate(route.children ?? [], names);
33
+ }
34
+ }
35
+ function find(routes: readonly RouteRecord[], parts: string[], at = 0, params: Record<string, string> = {}): Entry[] | undefined {
36
+ const ordered = [...routes].sort((a, b) => {
37
+ const aa = segments(a.path), bb = segments(b.path);
38
+ for (let i = 0; i < Math.max(aa.length, bb.length); i++) {
39
+ const rank = (p?: string) => p === '*' ? 0 : p?.startsWith(':') ? 1 : p ? 2 : 0.5;
40
+ const diff = rank(bb[i]) - rank(aa[i]); if (diff) return diff;
41
+ }
42
+ return 0;
43
+ });
44
+ for (const record of ordered) {
45
+ const own = { ...params }; let cursor = at, matched = true;
46
+ for (const part of segments(record.path)) {
47
+ if (part === '*') { own['wildcard'] = parts.slice(cursor).join('/'); cursor = parts.length; break; }
48
+ const actual = parts[cursor++];
49
+ if (actual === undefined) { matched = false; break; }
50
+ if (part.startsWith(':')) own[part.slice(1)] = actual;
51
+ else if (part !== actual) { matched = false; break; }
52
+ }
53
+ if (!matched) continue;
54
+ const entry = { record, params: own };
55
+ const children = find(record.children ?? [], parts, cursor, own);
56
+ if (children) return [entry, ...children];
57
+ if (cursor === parts.length) return [entry];
58
+ }
59
+ return undefined;
60
+ }
61
+
62
+ export function createRouter(options: RouterOptions) {
63
+ validate(options.routes);
64
+ const browser = options.window ?? (typeof window !== 'undefined' ? window : undefined);
65
+ if (!browser) throw new Error('createRouter requires a browser Window');
66
+ const report = options.onError ?? ((error: unknown) => console.error(error));
67
+ const location = signal<RouteLocation>(makeLocation(new URL(browser.location.href), {}));
68
+ let root: Node | undefined, active: Active[] = [], fallback: HTMLElement | undefined;
69
+ let transitioning = false;
70
+ let generation = 0;
71
+ const notFoundRecord: RouteRecord | undefined = options.notFound ? { path: '*', module: options.notFound } : undefined;
72
+ function makeLocation(url: URL, params: Record<string, string>): RouteLocation {
73
+ return Object.freeze({ url: new URL(url), pathname: url.pathname, params: Object.freeze({ ...params }), query: new URLSearchParams(url.search), hash: url.hash });
74
+ }
75
+ function render(url: URL): void {
76
+ if (!root) throw new Error('Start the router before navigating');
77
+ if (transitioning) throw new Error('Navigation during module mounting is not supported');
78
+ transitioning = true;
79
+ try {
80
+ let entries: Entry[] | undefined;
81
+ try { entries = find(options.routes, segments(url.pathname).map(decodeURIComponent)); }
82
+ catch (error) { if (!(error instanceof URIError)) throw error; }
83
+ if (!entries && notFoundRecord) entries = [{ record: notFoundRecord, params: {} }];
84
+ entries ??= [];
85
+ let keep = 0;
86
+ while (keep < active.length && keep < entries.length && active[keep]!.record === entries[keep]!.record && JSON.stringify(active[keep]!.params) === JSON.stringify(entries[keep]!.params)) keep++;
87
+ const removed = active.splice(keep);
88
+ const errors: unknown[] = [];
89
+ for (const item of removed.reverse()) { try { item.instance.dispose(); } catch (error) { errors.push(error); } }
90
+ fallback?.remove(); fallback = undefined;
91
+ if (errors.length) throw new AggregateError(errors, 'Route cleanup failed');
92
+ const added: Active[] = [];
93
+ try {
94
+ for (let i = keep; i < entries.length; i++) {
95
+ const entry = entries[i]!;
96
+ const parent = active.at(-1);
97
+ const outlet = parent ? parent.instance.outlet : root;
98
+ if (!outlet) throw new Error(`Parent module ${parent!.record.path} must render Outlet()`);
99
+ const instance = entry.record.module.attach(outlet, makeLocation(url, entry.params), parent?.instance.context, { onError: report });
100
+ const item = { ...entry, instance };
101
+ active.push(item); added.push(item);
102
+ }
103
+ } catch (error) {
104
+ active.splice(keep);
105
+ for (const item of added.reverse()) { try { item.instance.dispose(); } catch (cleanupError) { report(cleanupError); } }
106
+ throw error;
107
+ }
108
+ if (!entries.length) {
109
+ fallback = root.ownerDocument!.createElement('p');
110
+ fallback.setAttribute('role', 'status'); fallback.textContent = 'Page not found'; root.appendChild(fallback);
111
+ }
112
+ batch(() => {
113
+ for (const item of active) item.instance.update(makeLocation(url, item.params));
114
+ location.set(makeLocation(url, entries.at(-1)?.params ?? {}));
115
+ });
116
+ } finally { transitioning = false; }
117
+ }
118
+ function navigate(to: string, navigation: { replace?: boolean } = {}): void {
119
+ if (!root) throw new Error('Start the router before navigating');
120
+ const url = new URL(to, browser!.location.href);
121
+ if (!['http:', 'https:'].includes(url.protocol) || url.origin !== browser!.location.origin) throw new Error('Router navigation requires a same-origin HTTP(S) URL');
122
+ if (url.href === browser!.location.href) return;
123
+ render(url);
124
+ browser!.history[navigation.replace ? 'replaceState' : 'pushState'](null, '', url.href);
125
+ }
126
+ const popstate = () => {
127
+ try { if (location.get().url.href !== browser!.location.href) render(new URL(browser!.location.href)); }
128
+ catch (error) { report(error); }
129
+ };
130
+ const click = (event: MouseEvent) => {
131
+ if (event.defaultPrevented || event.button !== 0 || event.metaKey || event.ctrlKey || event.altKey || event.shiftKey) return;
132
+ const target = event.target as Element | null;
133
+ const anchor = target?.closest?.('a[href]');
134
+ if (!anchor || anchor.hasAttribute('download') || anchor.hasAttribute('data-hesh-reload') || (anchor.getAttribute('target') && anchor.getAttribute('target')!.toLowerCase() !== '_self')) return;
135
+ const href = anchor.getAttribute('href')!;
136
+ if (href.startsWith('#')) return;
137
+ let url: URL;
138
+ try { url = new URL(href, browser!.document.baseURI); } catch { return; }
139
+ if (!['http:', 'https:'].includes(url.protocol) || url.origin !== browser!.location.origin) return;
140
+ if (url.pathname === browser!.location.pathname && url.search === browser!.location.search && url.hash) return;
141
+ event.preventDefault();
142
+ try { navigate(url.href); } catch (error) { report(error); }
143
+ };
144
+ function stop(): void {
145
+ if (transitioning) throw new Error('Cannot stop the router during module mounting');
146
+ browser!.document.removeEventListener('click', click);
147
+ browser!.removeEventListener('popstate', popstate);
148
+ browser!.removeEventListener('hashchange', popstate);
149
+ const old = active; active = []; root = undefined;
150
+ generation++;
151
+ fallback?.remove(); fallback = undefined;
152
+ const errors: unknown[] = [];
153
+ for (const item of old.reverse()) { try { item.instance.dispose(); } catch (error) { errors.push(error); } }
154
+ if (errors.length) throw new AggregateError(errors, 'Router cleanup failed');
155
+ }
156
+ return {
157
+ location: { get: location.get }, navigate, stop,
158
+ start(parent: Node): Disposer {
159
+ if (root) throw new Error('Router is already started');
160
+ if (parent.ownerDocument !== browser!.document) throw new Error('Router root must belong to its Window');
161
+ const started = ++generation;
162
+ root = parent;
163
+ try { render(new URL(browser!.location.href)); }
164
+ catch (error) { stop(); throw error; }
165
+ browser!.document.addEventListener('click', click);
166
+ browser!.addEventListener('popstate', popstate);
167
+ browser!.addEventListener('hashchange', popstate);
168
+ return () => { if (generation === started) stop(); };
169
+ }
170
+ };
171
+ }
package/src/vite/index.ts CHANGED
@@ -4,6 +4,7 @@ export function hesh(): Plugin {
4
4
  let production = false;
5
5
  return {
6
6
  name: 'heshjs', enforce: 'pre',
7
+ config() { return { appType: 'spa' }; },
7
8
  configResolved(config) { production = config.command === 'build'; },
8
9
  transform(source, id) {
9
10
  if (!id.endsWith('.hesh')) return;