@heshammesalam/heshjs 0.1.0 → 0.3.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 +19 -4
- package/cli.js +26 -0
- package/dist/architecture/diagnostics.d.ts +12 -0
- package/dist/architecture/diagnostics.js +8 -0
- package/dist/architecture/diagnostics.js.map +1 -0
- package/dist/architecture/html.d.ts +16 -0
- package/dist/architecture/html.js +132 -0
- package/dist/architecture/html.js.map +1 -0
- package/dist/architecture/index.d.ts +42 -0
- package/dist/architecture/index.js +418 -0
- package/dist/architecture/index.js.map +1 -0
- package/dist/compiler/index.js +2 -0
- package/dist/compiler/index.js.map +1 -1
- package/dist/destinations/index.d.ts +63 -0
- package/dist/destinations/index.js +185 -0
- package/dist/destinations/index.js.map +1 -0
- package/dist/dom/index.d.ts +2 -0
- package/dist/dom/index.js +5 -1
- package/dist/dom/index.js.map +1 -1
- package/dist/domains/index.d.ts +24 -0
- package/dist/domains/index.js +110 -0
- package/dist/domains/index.js.map +1 -0
- package/dist/hmvc/index.d.ts +35 -0
- package/dist/hmvc/index.js +49 -0
- package/dist/hmvc/index.js.map +1 -0
- package/dist/parser/index.d.ts +15 -1
- package/dist/parser/index.js +73 -3
- package/dist/parser/index.js.map +1 -1
- package/dist/presentation/index.d.ts +9 -0
- package/dist/presentation/index.js +38 -0
- package/dist/presentation/index.js.map +1 -0
- package/dist/router/index.d.ts +24 -0
- package/dist/router/index.js +258 -0
- package/dist/router/index.js.map +1 -0
- package/dist/vite/index.d.ts +4 -1
- package/dist/vite/index.js +37 -2
- package/dist/vite/index.js.map +1 -1
- package/docs/contracts.md +60 -0
- package/docs/domain-graph.md +205 -0
- package/docs/grammar.md +4 -0
- package/docs/hmvc.md +134 -0
- package/package.json +10 -3
- package/src/architecture/diagnostics.ts +11 -0
- package/src/architecture/html.ts +88 -0
- package/src/architecture/index.ts +283 -0
- package/src/compiler/index.ts +1 -0
- package/src/destinations/index.ts +146 -0
- package/src/dom/index.ts +6 -1
- package/src/domains/index.ts +97 -0
- package/src/hmvc/index.ts +71 -0
- package/src/parser/index.ts +55 -4
- package/src/presentation/index.ts +31 -0
- package/src/router/index.ts +171 -0
- package/src/vite/index.ts +25 -2
|
@@ -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"]}
|
package/dist/vite/index.d.ts
CHANGED
package/dist/vite/index.js
CHANGED
|
@@ -1,16 +1,51 @@
|
|
|
1
1
|
import { compile } from '../compiler/index.js';
|
|
2
|
-
|
|
2
|
+
import { createArchitectureCompiler, writeProject, formatDiagnostic } from '../architecture/index.js';
|
|
3
|
+
import { resolve } from 'node:path';
|
|
4
|
+
export function hesh(options = {}) {
|
|
3
5
|
let production = false;
|
|
6
|
+
const architecture = createArchitectureCompiler();
|
|
7
|
+
let project;
|
|
8
|
+
let watched = [];
|
|
9
|
+
let warn;
|
|
10
|
+
const check = () => {
|
|
11
|
+
if (!project)
|
|
12
|
+
return;
|
|
13
|
+
const result = architecture.check(project);
|
|
14
|
+
if (result.diagnostics.some(item => item.severity === 'error'))
|
|
15
|
+
throw new Error(result.diagnostics.map(formatDiagnostic).join('\n'));
|
|
16
|
+
for (const item of result.diagnostics)
|
|
17
|
+
if (item.severity === 'warning')
|
|
18
|
+
warn?.(formatDiagnostic(item));
|
|
19
|
+
writeProject(result);
|
|
20
|
+
watched = result.files;
|
|
21
|
+
};
|
|
4
22
|
return {
|
|
5
23
|
name: 'heshjs', enforce: 'pre',
|
|
6
|
-
|
|
24
|
+
config() { return { appType: 'spa' }; },
|
|
25
|
+
configResolved(config) {
|
|
26
|
+
production = config.command === 'build';
|
|
27
|
+
warn = message => config.logger.warn(message);
|
|
28
|
+
if (options.architecture) {
|
|
29
|
+
project = { ...options.architecture, root: resolve(config.root, options.architecture.root) };
|
|
30
|
+
check();
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
buildStart() { for (const file of watched)
|
|
34
|
+
this.addWatchFile(file); },
|
|
7
35
|
transform(source, id) {
|
|
36
|
+
if (project && /(?:^|\/)server(?:\/|$)|\.server\.[cm]?[jt]s$/.test(id.replace(/\\/g, '/')))
|
|
37
|
+
throw new Error(`HESH-SEC-001 Server-only module cannot enter the browser bundle: ${id}`);
|
|
8
38
|
if (!id.endsWith('.hesh'))
|
|
9
39
|
return;
|
|
10
40
|
const result = compile(source, { filename: id, development: !production });
|
|
11
41
|
return { code: result.code, map: null };
|
|
12
42
|
},
|
|
13
43
|
handleHotUpdate(context) {
|
|
44
|
+
if (project && (watched.includes(context.file.replace(/\\/g, '/').toLowerCase()) || watched.includes(context.file.replace(/\\/g, '/')))) {
|
|
45
|
+
check();
|
|
46
|
+
context.server.ws.send({ type: 'full-reload' });
|
|
47
|
+
return [];
|
|
48
|
+
}
|
|
14
49
|
if (context.file.endsWith('.hesh')) {
|
|
15
50
|
context.server.ws.send({ type: 'full-reload' });
|
|
16
51
|
return [];
|
package/dist/vite/index.js.map
CHANGED
|
@@ -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;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/vite/index.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Plugin } from 'vite';\nimport { compile } from '../compiler/index.js';\nimport { createArchitectureCompiler, writeProject, formatDiagnostic, type ProjectOptions } from '../architecture/index.js';\nimport { resolve } from 'node:path';\nexport function hesh(options: { architecture?: ProjectOptions } = {}): Plugin {\n let production = false;\n const architecture = createArchitectureCompiler();\n let project: ProjectOptions | undefined;\n let watched: string[] = [];\n let warn: ((message: string) => void) | undefined;\n const check = () => {\n if (!project) return;\n const result = architecture.check(project);\n if (result.diagnostics.some(item => item.severity === 'error')) throw new Error(result.diagnostics.map(formatDiagnostic).join('\\n'));\n for (const item of result.diagnostics) if (item.severity === 'warning') warn?.(formatDiagnostic(item));\n writeProject(result); watched = result.files;\n };\n return {\n name: 'heshjs', enforce: 'pre',\n config() { return { appType: 'spa' }; },\n configResolved(config) {\n production = config.command === 'build';\n warn = message => config.logger.warn(message);\n if (options.architecture) { project = { ...options.architecture, root: resolve(config.root, options.architecture.root) }; check(); }\n },\n buildStart() { for (const file of watched) this.addWatchFile(file); },\n transform(source, id) {\n if (project && /(?:^|\\/)server(?:\\/|$)|\\.server\\.[cm]?[jt]s$/.test(id.replace(/\\\\/g, '/'))) throw new Error(`HESH-SEC-001 Server-only module cannot enter the browser bundle: ${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 (project && (watched.includes(context.file.replace(/\\\\/g, '/').toLowerCase()) || watched.includes(context.file.replace(/\\\\/g, '/')))) {\n check(); context.server.ws.send({ type: 'full-reload' }); return [];\n }\n if (context.file.endsWith('.hesh')) { context.server.ws.send({ type: 'full-reload' }); return []; }\n }\n };\n}\n"]}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# Architecture declaration grammar (experimental)
|
|
2
|
+
|
|
3
|
+
These declarations extend the existing Hesh lexer/parser. A file may contain
|
|
4
|
+
views or architecture declarations; project compilation resolves architecture.
|
|
5
|
+
Imports in architecture files reference other .hesh descriptor files explicitly.
|
|
6
|
+
|
|
7
|
+
```ebnf
|
|
8
|
+
domain = "domain", Identifier, "{",
|
|
9
|
+
"implementation", ":", String, ";",
|
|
10
|
+
[ "owns", "{", { Identifier, ";" }, "}" ],
|
|
11
|
+
"exposes", "{", { Path, ";" }, "}",
|
|
12
|
+
[ "consumes", "{", { Path, ";" }, "}" ],
|
|
13
|
+
[ "access", "{", { Path, ":", String, ";" }, "}" ], "}" ;
|
|
14
|
+
presentation = "presentation", Identifier, "{",
|
|
15
|
+
"html", ":", String, ";", "logic", ":", String, ";",
|
|
16
|
+
[ "styles", ":", String, ";" ],
|
|
17
|
+
"bindings", "{", { Identifier, ";" }, "}",
|
|
18
|
+
"actions", "{", { Identifier, ";" }, "}", "}" ;
|
|
19
|
+
destination = "destination", Identifier, "{",
|
|
20
|
+
"url", ":", String, ";",
|
|
21
|
+
"input", "{", { Identifier, ":", ("string" | "number" | "boolean"), ";" }, "}",
|
|
22
|
+
"presentation", ":", Identifier, ";", [ "access", ":", String, ";" ], "}" ;
|
|
23
|
+
application = "application", Identifier, "{",
|
|
24
|
+
"domains", "{", { Identifier, ";" }, "}",
|
|
25
|
+
"destinations", "{", { Identifier, ";" }, "}", "}" ;
|
|
26
|
+
Path = Identifier, { ".", Identifier } ;
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Fields/blocks can appear in any order, once each. Unknown/duplicate fields are
|
|
30
|
+
errors. Strings use ordinary JS string escaping. Identifiers cannot be prototype
|
|
31
|
+
mutation names. Locations are retained for diagnostics. Semicolons are required.
|
|
32
|
+
|
|
33
|
+
`implementation` and `logic` have form `./module.js#exportName` (or .ts/.mjs).
|
|
34
|
+
A domain factory takes a dependencies object and returns its actual API. Every
|
|
35
|
+
exposed dotted operation must exist and be callable according to the TS checker;
|
|
36
|
+
every consumed path is Domain.operation and must be publicly exposed. An access
|
|
37
|
+
entry must name an exposed operation. `owns` is ownership metadata, not a generated
|
|
38
|
+
entity/validation/database implementation. The runtime passes only declared
|
|
39
|
+
consumed operations and exposes only the validated public facade.
|
|
40
|
+
|
|
41
|
+
A presentation factory receives `{ domains, input, location, signal }` and returns
|
|
42
|
+
`{ bindings, actions }`. bindings values may be signals/readers/ordinary values;
|
|
43
|
+
actions are callable. Both declared names and nested template paths are checked
|
|
44
|
+
against its inferred JS/TS return shape. HTML text `{product.name}` is escaped via
|
|
45
|
+
text nodes. `h-bind="product.name"` binds textContent (cannot have child content),
|
|
46
|
+
`h-action="reserve"` listens to click (submit on a form), `h-value="quantity"`
|
|
47
|
+
binds a writable signal to an input/textarea/select, and
|
|
48
|
+
`h-destination="ProductDetails" h-input-id="product.id"` binds a native anchor to
|
|
49
|
+
a validated destination. Other normal HTML attributes/semantics are preserved.
|
|
50
|
+
No arbitrary JS expressions, raw HTML, inline event attributes, script elements,
|
|
51
|
+
h-each, h-key or unknown h-* directives are accepted in this first milestone.
|
|
52
|
+
Use normal CSS files; template scoping and keyed template lists remain deferred.
|
|
53
|
+
|
|
54
|
+
Destination URL params must exactly match input names. Values are encoded/decoded
|
|
55
|
+
once; number inputs must be finite and booleans serialize as true/false. Duplicate
|
|
56
|
+
or structurally conflicting URL destinations are errors. Generated declarations
|
|
57
|
+
provide TS input types and JS IntelliSense. Runtime validation also rejects missing,
|
|
58
|
+
extra or invalid fields in JavaScript. Destination policy names are client metadata;
|
|
59
|
+
they cannot enforce server-side authorization. A policy without an authorize
|
|
60
|
+
callback is denied rather than silently accepted.
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
# Domain Graph first vertical slice (0.3)
|
|
2
|
+
|
|
3
|
+
This opt-in architecture builds on the existing Hesh compiler, shallow reactivity,
|
|
4
|
+
DOM renderer, router and HMVC. Existing components and `createRouter` remain valid.
|
|
5
|
+
This is the first architecture milestone, not an enterprise-ready framework.
|
|
6
|
+
These APIs require 0.3 or later. Install `@heshammesalam/heshjs@0.3.0` in an
|
|
7
|
+
existing Vite application and enable the architecture option described below.
|
|
8
|
+
The npm creator's default template remains HMVC; it does not scaffold domains.
|
|
9
|
+
The following commands run the reference examples from the framework repository:
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
npm install
|
|
13
|
+
npm run check:architecture
|
|
14
|
+
npm run dev:domains # JS Identity/Inventory/Finance demo, localhost:5180
|
|
15
|
+
npm run dev:domains:ts # strict TS alternative, localhost:5181
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
The JS demo uses mock identity, an abortable timer-backed mock product API and a
|
|
19
|
+
mock tax service. No HTTP server, credentials, payments or real authorization are
|
|
20
|
+
implemented. Browser refresh restores destination/path input, not in-memory demo
|
|
21
|
+
login state. Journeys are deliberately not simulated.
|
|
22
|
+
|
|
23
|
+
## Descriptors and real implementations
|
|
24
|
+
|
|
25
|
+
```hesh
|
|
26
|
+
domain Inventory {
|
|
27
|
+
implementation: "./inventory.js#createInventory";
|
|
28
|
+
owns { Product; }
|
|
29
|
+
exposes { products.get; stock.reserve; }
|
|
30
|
+
consumes { Identity.session; }
|
|
31
|
+
access { stock.reserve: "inventory.reserve"; }
|
|
32
|
+
}
|
|
33
|
+
presentation ProductScreen {
|
|
34
|
+
html: "./ui/product.html";
|
|
35
|
+
logic: "./ui/product.js#createProduct";
|
|
36
|
+
styles: "./ui/product.css";
|
|
37
|
+
bindings { title; }
|
|
38
|
+
actions { reserve; }
|
|
39
|
+
}
|
|
40
|
+
destination ProductDetails {
|
|
41
|
+
url: "/products/:id";
|
|
42
|
+
input { id: string; }
|
|
43
|
+
presentation: ProductScreen;
|
|
44
|
+
access: "inventory.read";
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
```js
|
|
49
|
+
// inventory.js: private implementation, no imports from another domain's files.
|
|
50
|
+
export function createInventory(dependencies) {
|
|
51
|
+
return {
|
|
52
|
+
products: { get: id => ({ id, title: 'Mock product' }) },
|
|
53
|
+
stock: { reserve: id => dependencies.Identity.session() + ': ' + id }
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
`owns` is metadata, not a generated entity/database/validation system. Exposes are
|
|
59
|
+
checked against the real factory return type. Consumers receive only the listed
|
|
60
|
+
public operations, not the whole implementation object. Factories execute once
|
|
61
|
+
per application domain graph in dependency order. Cycles and missing contracts
|
|
62
|
+
are errors. Dependencies and public facades are frozen; disposed graphs reject
|
|
63
|
+
further operation calls. Factories may use `onCleanup` for owned resources.
|
|
64
|
+
|
|
65
|
+
The application descriptor imports domain files explicitly and selects names:
|
|
66
|
+
|
|
67
|
+
```hesh
|
|
68
|
+
import "./domains/identity/domain.hesh";
|
|
69
|
+
import "./domains/inventory/domain.hesh";
|
|
70
|
+
application Shop {
|
|
71
|
+
domains { Identity; Inventory; }
|
|
72
|
+
destinations { ProductDetails; }
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Every selected destination/presentation belongs to one selected domain directory.
|
|
77
|
+
Each domain has its own directory boundary. Shared code can be imported by domains;
|
|
78
|
+
shared code cannot re-export a private domain implementation as an escape hatch.
|
|
79
|
+
|
|
80
|
+
## Standard HTML, CSS and explicit bindings
|
|
81
|
+
|
|
82
|
+
```html
|
|
83
|
+
<section aria-labelledby="title">
|
|
84
|
+
<h1 id="title">{title}</h1>
|
|
85
|
+
<button h-action="reserve">Reserve</button>
|
|
86
|
+
</section>
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
```js
|
|
90
|
+
import { signal } from '@heshammesalam/heshjs';
|
|
91
|
+
export function createProduct({ domains, input, location, signal: cancellation }) {
|
|
92
|
+
const title = signal(domains.Inventory.products.get(input.id).title);
|
|
93
|
+
return {
|
|
94
|
+
bindings: { title },
|
|
95
|
+
actions: { reserve() { domains.Inventory.stock.reserve(input.id); } }
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Bindings are explicit declared data paths, not arbitrary evaluated JS strings.
|
|
101
|
+
`{product.name}` and `h-bind="product.name"` use text nodes. `\{`/`\}` produce
|
|
102
|
+
literal braces. A whole attribute binding like `disabled="{disabled}"` preserves
|
|
103
|
+
boolean values; mixed attribute text is a string. Standard native HTML/custom tags
|
|
104
|
+
are preserved without Hesh element aliases. Browser forms, labels, ARIA and native
|
|
105
|
+
validation remain available. `h-action` means click, or submit on a form (preventing
|
|
106
|
+
full submission). Events are passed to actions and async errors use onError.
|
|
107
|
+
`h-value="email"` is two-way binding to a root writable signal on text controls.
|
|
108
|
+
|
|
109
|
+
`h-destination="ProductDetails" h-input-id="productId"` on a native anchor builds
|
|
110
|
+
a real href from the destination contract. Input attributes follow HTML's case
|
|
111
|
+
insensitivity; destination input names must therefore be unique ignoring case.
|
|
112
|
+
Missing/extra/wrongly typed inputs, missing bindings/actions and unsafe directives
|
|
113
|
+
produce located diagnostics. Dynamic any-typed data emits warnings when the shape
|
|
114
|
+
cannot be proven. JS receives inferred checks/JSDoc where present and runtime
|
|
115
|
+
input validation; this does not make arbitrary JavaScript fully type-safe.
|
|
116
|
+
|
|
117
|
+
HTML is parsed with parse5 at build time and emitted as DOM instructions. No
|
|
118
|
+
innerHTML/eval/Function constructor is used. Script/embedding/foreign/template
|
|
119
|
+
elements, inline event attributes and unknown h-* directives are rejected. SVG,
|
|
120
|
+
keyed h-each, raw HTML and scoped CSS are not implemented in this slice. Standard
|
|
121
|
+
CSS is imported unchanged through Vite, including variables, modern selectors,
|
|
122
|
+
media/container queries, animations and global themes. CSS is global in this release.
|
|
123
|
+
|
|
124
|
+
## Generated typed destinations
|
|
125
|
+
|
|
126
|
+
Configure Vite using `hesh({ architecture: { root: 'src' } })`. For a tsconfig
|
|
127
|
+
outside src add `tsconfig: '../tsconfig.json'`. The plugin validates before build
|
|
128
|
+
and generates `src/.hesh-generated/app.js`, app.d.ts and compiled presentation
|
|
129
|
+
modules. Do not hand-edit generated files. Invalid checks block build; last valid
|
|
130
|
+
files are not deleted, so bypassing the check pipeline can use stale output.
|
|
131
|
+
|
|
132
|
+
```js
|
|
133
|
+
import { createApplication } from './.hesh-generated/app.js';
|
|
134
|
+
const app = createApplication({
|
|
135
|
+
authorize: policy => policy === 'inventory.read', // CLIENT ONLY
|
|
136
|
+
onError: error => console.error(error),
|
|
137
|
+
onDiagnostic: event => console.debug(event.type, event.destination, event.reason)
|
|
138
|
+
});
|
|
139
|
+
const dispose = app.start(document.querySelector('#app'));
|
|
140
|
+
app.destinations.ProductDetails.open({ id: '15' });
|
|
141
|
+
app.destinations.ProductDetails.replace({ id: '20' }, { query: { tab: 'stock' } });
|
|
142
|
+
await app.destinations.ProductDetails.prefetch({ id: '30' });
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Generated declarations give each destination its exact required input fields and
|
|
146
|
+
primitive types. Empty-input destinations use `Record<string, never>`. Runtime
|
|
147
|
+
validation also rejects unknown/missing fields, non-finite numbers, empty strings
|
|
148
|
+
and URL dot segments. Inputs are encoded once and decoded once; URL parameter
|
|
149
|
+
names currently use the existing router's ASCII letter/digit/underscore rules.
|
|
150
|
+
Overlapping destination patterns are rejected so an input cannot silently open a
|
|
151
|
+
different static destination. This restriction applies only to the new contracts;
|
|
152
|
+
the existing router retains its static-priority behavior.
|
|
153
|
+
|
|
154
|
+
Page is the only supported presentation mode. Destinations load their own compiled
|
|
155
|
+
presentation and logic resources lazily. Prefetch deduplicates those code loads;
|
|
156
|
+
it does not fetch business data or authorize operations. Navigation disposes the
|
|
157
|
+
previous presentation, aborts its signal and prevents a late loader from mounting
|
|
158
|
+
stale UI. Dynamic module downloads themselves cannot be canceled. Factories are
|
|
159
|
+
synchronous and start their own asynchronous work using the supplied signal.
|
|
160
|
+
Guard against late results when using non-abortable services. Query/hash changes
|
|
161
|
+
create a fresh presentation instance. Successful mounting focuses a heading or
|
|
162
|
+
`data-hesh-focus`; scroll restoration and modal focus trapping are deferred.
|
|
163
|
+
|
|
164
|
+
`app.status.get()` is reactive loading/ready/error state. Optional diagnostic
|
|
165
|
+
events identify loading, ready, blocked, errors, cancellation and disposal. They
|
|
166
|
+
do not infer business intent or explain arbitrary reactive dependency chains.
|
|
167
|
+
Client policies default to deny when declared without an authorize callback;
|
|
168
|
+
server-side enforcement is still necessary for every protected operation.
|
|
169
|
+
|
|
170
|
+
## Architecture validation and graph tooling
|
|
171
|
+
|
|
172
|
+
```sh
|
|
173
|
+
hesh check ./src --write
|
|
174
|
+
hesh check ./src --tsconfig ../tsconfig.json --write
|
|
175
|
+
hesh graph ./src
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
From the repository use `node packages/heshjs/cli.js ...`. Root means the directory
|
|
179
|
+
containing app.hesh, typically src. The TS resolver handles aliases, relative paths,
|
|
180
|
+
type imports and re-exports. Canonical paths account for filesystem aliases and
|
|
181
|
+
symlinks. Physical cross-domain imports of implementations are private, even if
|
|
182
|
+
the target JS module exports a function; use declared injection instead. Node and
|
|
183
|
+
server-only modules are blocked in client code. Computed imports in application
|
|
184
|
+
source are rejected because their target cannot be validated. This is build-time
|
|
185
|
+
architecture enforcement for configured projects, not a sandbox against malicious
|
|
186
|
+
application code, reflection or uninspected third-party packages.
|
|
187
|
+
|
|
188
|
+
Diagnostics include code, severity, path, offset/line/column, explanation and a
|
|
189
|
+
suggestion. HTML locations identify their source node/attribute; entity decoding
|
|
190
|
+
can make the exact inner text offset approximate. Graph JSON contains domain
|
|
191
|
+
dependencies, destination ownership/input schemas and compiled navigation links.
|
|
192
|
+
Descriptor ASTs are reused when unchanged and output writes are content-based.
|
|
193
|
+
Semantic analysis currently rechecks the whole graph; precise dependency-aware
|
|
194
|
+
typecheck invalidation and full HTML/Hesh source maps are deferred. The Vite workflow
|
|
195
|
+
uses full reload rather than state-preserving HMR.
|
|
196
|
+
|
|
197
|
+
## Deliberate limits
|
|
198
|
+
|
|
199
|
+
Journeys, modal/panel/nested destination modes, redirects, scroll restoration,
|
|
200
|
+
file routing, generic async resources/caching, deep collections, scoped CSS,
|
|
201
|
+
server authorization, SSR/hydration, advanced scaffolding commands and a language
|
|
202
|
+
server are not implemented. Existing HMVC nested routes remain usable separately.
|
|
203
|
+
Production hosts still need SPA fallback to index.html for direct URLs. No
|
|
204
|
+
performance superiority, globally unique architecture or enterprise-readiness
|
|
205
|
+
claim is made. See architecture/domain-graph-audit.md for migration order.
|