@heshammesalam/heshjs 0.0.0-stage → 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 +51 -2
- package/dist/compiler/index.d.ts +37 -0
- package/dist/compiler/index.js +294 -0
- package/dist/compiler/index.js.map +1 -0
- package/dist/dom/index.d.ts +12 -0
- package/dist/dom/index.js +69 -0
- package/dist/dom/index.js.map +1 -0
- package/dist/heshjs/index.d.ts +4 -0
- package/dist/heshjs/index.js +3 -0
- package/dist/heshjs/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 +47 -0
- package/dist/parser/index.js +218 -0
- package/dist/parser/index.js.map +1 -0
- package/dist/reactivity/index.d.ts +15 -0
- package/dist/reactivity/index.js +162 -0
- package/dist/reactivity/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 +2 -0
- package/dist/vite/index.js +22 -0
- package/dist/vite/index.js.map +1 -0
- package/docs/grammar.md +65 -0
- package/docs/hmvc.md +134 -0
- package/package.json +26 -4
- package/src/compiler/index.ts +231 -0
- package/src/dom/index.ts +67 -0
- package/src/heshjs/index.ts +4 -0
- package/src/hmvc/index.ts +71 -0
- package/src/parser/index.ts +149 -0
- package/src/reactivity/index.ts +115 -0
- package/src/router/index.ts +171 -0
- package/src/vite/index.ts +18 -0
|
@@ -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"]}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { compile } from '../compiler/index.js';
|
|
2
|
+
export function hesh() {
|
|
3
|
+
let production = false;
|
|
4
|
+
return {
|
|
5
|
+
name: 'heshjs', enforce: 'pre',
|
|
6
|
+
config() { return { appType: 'spa' }; },
|
|
7
|
+
configResolved(config) { production = config.command === 'build'; },
|
|
8
|
+
transform(source, id) {
|
|
9
|
+
if (!id.endsWith('.hesh'))
|
|
10
|
+
return;
|
|
11
|
+
const result = compile(source, { filename: id, development: !production });
|
|
12
|
+
return { code: result.code, map: null };
|
|
13
|
+
},
|
|
14
|
+
handleHotUpdate(context) {
|
|
15
|
+
if (context.file.endsWith('.hesh')) {
|
|
16
|
+
context.server.ws.send({ type: 'full-reload' });
|
|
17
|
+
return [];
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +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,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/grammar.md
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# Milestone 1 language (v0.1)
|
|
2
|
+
|
|
3
|
+
The grammar is deliberately smaller than the product roadmap. Source is UTF-8.
|
|
4
|
+
Whitespace and JavaScript comments separate tokens. Identifiers follow JavaScript
|
|
5
|
+
identifier rules. Strings and numbers follow JavaScript lexical rules.
|
|
6
|
+
|
|
7
|
+
```ebnf
|
|
8
|
+
module = { importDecl | view } ;
|
|
9
|
+
view = [ "export" ], [ "default" ], "view", Identifier, "{", { statement }, "}" ;
|
|
10
|
+
statement = state | constant | element ;
|
|
11
|
+
state = Identifier, "=", expression, ";" ;
|
|
12
|
+
constant = "const", Identifier, "=", expression, ";" ;
|
|
13
|
+
element = Identifier, "(", [ expression, [ ",", [ "async" ], handler ] ], ")",
|
|
14
|
+
( ";" | "{", { element }, "}", [ ";" ] ) ;
|
|
15
|
+
handler = "{", { javascriptStatement }, "}" ;
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
`importDecl` is an ESM import. Expressions and event bodies are parsed with
|
|
19
|
+
TypeScript's JavaScript parser, never evaluated by the compiler. Type annotations
|
|
20
|
+
inside `.hesh` are rejected; use `.ts` modules for typed logic. Imports must precede
|
|
21
|
+
views. Each file may export multiple named views and at most one default view.
|
|
22
|
+
|
|
23
|
+
`count = 0;` declares component-local state; a second declaration is an error.
|
|
24
|
+
State is a shallow signal: assignment and `++`, `--`, compound assignments trigger
|
|
25
|
+
updates. `items.push(x)` and `object.field = x` do **not** notify; assign a new array
|
|
26
|
+
or object instead. Constants are immutable bindings, not deeply frozen objects.
|
|
27
|
+
Initializers execute once per mount in declaration order. There is no derived
|
|
28
|
+
declaration yet. State references in text expressions become tracked reads.
|
|
29
|
+
|
|
30
|
+
`button("+", { count++; });` has an explicit click block inside its argument list.
|
|
31
|
+
A block after `)` contains children, eliminating event/child ambiguity. Event code
|
|
32
|
+
is ordinary JavaScript, with `event` available, lexical local variables, functions,
|
|
33
|
+
and optional `async`. Synchronous and asynchronous errors go to mount's `onError`
|
|
34
|
+
callback (default: `console.error`). Cleanup removes listeners. Async continuations
|
|
35
|
+
are not canceled on unmount; applications must cancel their own async work.
|
|
36
|
+
|
|
37
|
+
Double-quoted strings can interpolate `{expression}`; `"Count: {count}"` becomes
|
|
38
|
+
a sequence of text parts. Braces inside strings use `\{` and `\}`. Interpolation
|
|
39
|
+
expressions may contain nested braces, strings and comments. Single quotes and
|
|
40
|
+
backtick literals use standard JavaScript semantics, without Hesh interpolation.
|
|
41
|
+
The initial lexical envelope does not support backtick `${...}` substitutions or
|
|
42
|
+
regular-expression literals; move these into imported JS/TS
|
|
43
|
+
helpers. Double-quoted Hesh strings support ordinary single-character escapes;
|
|
44
|
+
use single quotes for Unicode/hex escape sequences. Destructuring assignment and
|
|
45
|
+
logical compound assignment to component bindings are rejected. For `for…of` or
|
|
46
|
+
`for…in`, declare a local iteration variable and assign state inside the body.
|
|
47
|
+
|
|
48
|
+
Lowercase names identify native HTML elements. `text` maps to `span`, `card`,
|
|
49
|
+
`row`, `stack`, `grid` to `div`, `image` to `img`, and `link` to `a`. Use a native
|
|
50
|
+
element name for anything else; custom elements use `element("x-widget")` only in
|
|
51
|
+
the public DOM API, not yet in `.hesh`. Unknown uppercase references are errors;
|
|
52
|
+
uppercase view calls mount a locally declared or imported component, with no props
|
|
53
|
+
in this milestone. Views have no parameter syntax yet. Children of a component
|
|
54
|
+
call and click blocks on components are rejected rather than silently discarded.
|
|
55
|
+
|
|
56
|
+
`input(name);` binds a writable state identifier bidirectionally using `input`
|
|
57
|
+
events and its string value. `image(url);` binds a safe `src`. Other values become
|
|
58
|
+
text via `String(value ?? "")`, never HTML. Generic attributes are available in the
|
|
59
|
+
DOM API; declarative attribute syntax is deferred. Event closures may shadow state
|
|
60
|
+
identifiers; lexical resolution preserves those local bindings. Globals and imports
|
|
61
|
+
retain ordinary JavaScript behavior. Unknown unbound names are compile errors.
|
|
62
|
+
|
|
63
|
+
No conditionals, keyed lists, styles, props, TypeScript syntax, slots, or top-level
|
|
64
|
+
JavaScript statements are accepted by this grammar. Future syntax requires a
|
|
65
|
+
versioned grammar change and tests. Unsupported input produces source diagnostics.
|
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,28 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@heshammesalam/heshjs",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "An experimental independent reactive UI framework with brace-based .hesh components",
|
|
5
|
+
"author": "Hesham",
|
|
6
|
+
"license": "UNLICENSED",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"main": "./dist/heshjs/index.js",
|
|
9
|
+
"types": "./dist/heshjs/index.d.ts",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": { "types": "./dist/heshjs/index.d.ts", "import": "./dist/heshjs/index.js" },
|
|
12
|
+
"./reactivity": { "types": "./dist/reactivity/index.d.ts", "import": "./dist/reactivity/index.js" },
|
|
13
|
+
"./dom": { "types": "./dist/dom/index.d.ts", "import": "./dist/dom/index.js" },
|
|
14
|
+
"./parser": { "types": "./dist/parser/index.d.ts", "import": "./dist/parser/index.js" },
|
|
15
|
+
"./compiler": { "types": "./dist/compiler/index.d.ts", "import": "./dist/compiler/index.js" },
|
|
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" },
|
|
19
|
+
"./package.json": "./package.json"
|
|
20
|
+
},
|
|
21
|
+
"files": ["dist", "src", "docs", "README.md"],
|
|
22
|
+
"engines": { "node": ">=22.12.0" },
|
|
23
|
+
"dependencies": { "typescript": "~5.9.3" },
|
|
24
|
+
"peerDependencies": { "vite": "^7.2.2" },
|
|
25
|
+
"peerDependenciesMeta": { "vite": { "optional": true } },
|
|
26
|
+
"publishConfig": { "access": "public", "registry": "https://registry.npmjs.org/" }
|
|
27
|
+
}
|
|
28
|
+
|