@heshammesalam/heshjs 0.2.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.
Files changed (45) hide show
  1. package/README.md +8 -2
  2. package/cli.js +26 -0
  3. package/dist/architecture/diagnostics.d.ts +12 -0
  4. package/dist/architecture/diagnostics.js +8 -0
  5. package/dist/architecture/diagnostics.js.map +1 -0
  6. package/dist/architecture/html.d.ts +16 -0
  7. package/dist/architecture/html.js +132 -0
  8. package/dist/architecture/html.js.map +1 -0
  9. package/dist/architecture/index.d.ts +42 -0
  10. package/dist/architecture/index.js +418 -0
  11. package/dist/architecture/index.js.map +1 -0
  12. package/dist/compiler/index.js +2 -0
  13. package/dist/compiler/index.js.map +1 -1
  14. package/dist/destinations/index.d.ts +63 -0
  15. package/dist/destinations/index.js +185 -0
  16. package/dist/destinations/index.js.map +1 -0
  17. package/dist/dom/index.d.ts +2 -0
  18. package/dist/dom/index.js +5 -1
  19. package/dist/dom/index.js.map +1 -1
  20. package/dist/domains/index.d.ts +24 -0
  21. package/dist/domains/index.js +110 -0
  22. package/dist/domains/index.js.map +1 -0
  23. package/dist/parser/index.d.ts +15 -1
  24. package/dist/parser/index.js +73 -3
  25. package/dist/parser/index.js.map +1 -1
  26. package/dist/presentation/index.d.ts +9 -0
  27. package/dist/presentation/index.js +38 -0
  28. package/dist/presentation/index.js.map +1 -0
  29. package/dist/vite/index.d.ts +4 -1
  30. package/dist/vite/index.js +36 -2
  31. package/dist/vite/index.js.map +1 -1
  32. package/docs/contracts.md +60 -0
  33. package/docs/domain-graph.md +205 -0
  34. package/docs/grammar.md +4 -0
  35. package/package.json +8 -3
  36. package/src/architecture/diagnostics.ts +11 -0
  37. package/src/architecture/html.ts +88 -0
  38. package/src/architecture/index.ts +283 -0
  39. package/src/compiler/index.ts +1 -0
  40. package/src/destinations/index.ts +146 -0
  41. package/src/dom/index.ts +6 -1
  42. package/src/domains/index.ts +97 -0
  43. package/src/parser/index.ts +55 -4
  44. package/src/presentation/index.ts +31 -0
  45. package/src/vite/index.ts +24 -2
@@ -4,7 +4,15 @@ export interface Expression extends Location { source: string }
4
4
  export interface State extends Location { kind: 'state' | 'const'; name: string; value: Expression }
5
5
  export interface Element extends Location { kind: 'element'; name: string; value?: Expression; handler?: Expression; async: boolean; children: Element[] }
6
6
  export interface View extends Location { name: string; exported: boolean; default: boolean; body: (State | Element)[] }
7
- export interface Module { source: string; filename: string; imports: Expression[]; views: View[] }
7
+ export interface ContractEntry extends Location { name: string; value?: string }
8
+ export interface Contract extends Location {
9
+ kind: 'domain' | 'presentation' | 'destination' | 'application';
10
+ name: string;
11
+ fields: Record<string, ContractEntry>;
12
+ blocks: Record<string, ContractEntry[]>;
13
+ }
14
+ export interface Module { source: string; filename: string; imports: Expression[]; views: View[]; contracts?: Contract[] }
15
+ export interface ParsedModule extends Module { contracts: Contract[] }
8
16
  export class HeshError extends Error {
9
17
  readonly line: number;
10
18
  readonly column: number;
@@ -61,7 +69,7 @@ export function tokenize(source: string, filename = 'component.hesh'): Token[] {
61
69
  return tokens;
62
70
  }
63
71
 
64
- export function parse(source: string, filename = 'component.hesh'): Module {
72
+ export function parse(source: string, filename = 'component.hesh'): ParsedModule {
65
73
  const tokens = tokenize(source, filename);
66
74
  let at = 0;
67
75
  const peek = () => tokens[at]?.text;
@@ -119,8 +127,51 @@ export function parse(source: string, filename = 'component.hesh'): Module {
119
127
  else take(';');
120
128
  return { kind: 'element', name: name.text, start: name.start, end: tokens[at - 1]!.end, value, handler, async, children };
121
129
  };
122
- const module: Module = { source, filename, imports: [], views: [] };
130
+ const contract = (): Contract => {
131
+ const kind = take(); const name = identifier(); take('{');
132
+ const result: Contract = { kind: kind.text as Contract['kind'], name: name.text, start: kind.start, end: 0, fields: Object.create(null), blocks: Object.create(null) };
133
+ const schemas: Record<Contract['kind'], Record<string, string>> = {
134
+ domain: { implementation: 'string', owns: 'list', exposes: 'list', consumes: 'list', access: 'map' },
135
+ presentation: { html: 'string', logic: 'string', styles: 'string', bindings: 'list', actions: 'list' },
136
+ destination: { url: 'string', input: 'map', presentation: 'identifier', access: 'string' },
137
+ application: { domains: 'list', destinations: 'list' }
138
+ };
139
+ const path = (): ContractEntry => {
140
+ const first = identifier(); let name = first.text;
141
+ while (peek() === '.') { take(); name += '.' + identifier().text; }
142
+ for (const part of name.split('.')) if (['__proto__', 'prototype', 'constructor'].includes(part)) fail('Unsafe contract identifier', first.start);
143
+ return { name, start: first.start, end: tokens[at - 1]!.end };
144
+ };
145
+ const string = () => {
146
+ const token = take();
147
+ if (token.kind !== ts.SyntaxKind.StringLiteral) fail('Expected quoted string', token.start);
148
+ const parsed = ts.createSourceFile('literal.js', token.text, ts.ScriptTarget.Latest, true, ts.ScriptKind.JS);
149
+ return ((parsed.statements[0] as ts.ExpressionStatement).expression as ts.StringLiteral).text;
150
+ };
151
+ while (peek() && peek() !== '}') {
152
+ const key = take(); const type = Object.hasOwn(schemas[result.kind], key.text) ? schemas[result.kind][key.text] : undefined;
153
+ if (!type) fail(`Unknown ${result.kind} field ${key.text}`, key.start);
154
+ if (result.fields[key.text] || result.blocks[key.text]) fail(`Duplicate field ${key.text}`, key.start);
155
+ if (type === 'string' || type === 'identifier') {
156
+ take(':'); const value = type === 'string' ? string() : identifier().text; const end = take(';').end;
157
+ result.fields[key.text] = { name: key.text, value, start: key.start, end };
158
+ } else {
159
+ take('{'); const entries: ContractEntry[] = []; const seen = new Set<string>();
160
+ while (peek() && peek() !== '}') {
161
+ const entry = path();
162
+ if (seen.has(entry.name)) fail(`Duplicate entry ${entry.name}`, entry.start); seen.add(entry.name);
163
+ if (type === 'map') { take(':'); entry.value = key.text === 'input' ? identifier().text : string(); }
164
+ entry.end = take(';').end; entries.push(entry);
165
+ }
166
+ take('}'); result.blocks[key.text] = entries;
167
+ }
168
+ }
169
+ result.end = take('}').end;
170
+ return result;
171
+ };
172
+ const module: ParsedModule = { source, filename, imports: [], views: [], contracts: [] };
123
173
  while (tokens[at]) {
174
+ if (['domain', 'presentation', 'destination', 'application'].includes(peek() ?? '')) { module.contracts.push(contract()); continue; }
124
175
  if (peek() === 'import') {
125
176
  if (module.views.length) fail('Imports must precede views');
126
177
  const start = take().start;
@@ -144,6 +195,6 @@ export function parse(source: string, filename = 'component.hesh'): Module {
144
195
  const end = take('}').end;
145
196
  module.views.push({ name, body, start, end, exported, default: isDefault });
146
197
  }
147
- if (!module.views.length) fail('Expected at least one view');
198
+ if (!module.views.length && !module.contracts.length) fail('Expected at least one view or architecture declaration');
148
199
  return module;
149
200
  }
@@ -0,0 +1,31 @@
1
+ import { effect } from '../reactivity/index.js';
2
+ import { nativeElement as element, text, attribute, listen } from '../dom/index.js';
3
+ export { element, text, attribute, listen };
4
+ export interface PresentationState { bindings: Record<string, unknown>; actions: Record<string, (event: Event) => unknown> }
5
+ export function readBinding(bindings: Record<string, unknown>, path: string): unknown {
6
+ const unwrap = (value: unknown): unknown => {
7
+ if (typeof value === 'function') return value();
8
+ if (value && typeof (value as { get?: unknown }).get === 'function') return (value as { get(): unknown }).get();
9
+ return value;
10
+ };
11
+ let value: unknown = bindings;
12
+ for (const part of path.split('.')) {
13
+ if (['__proto__', 'constructor', 'prototype'].includes(part)) throw new Error('Unsafe binding path');
14
+ value = (unwrap(value) as Record<string, unknown> | null)?.[part];
15
+ }
16
+ return unwrap(value);
17
+ }
18
+ export function bindValue(node: HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement, bindings: Record<string, unknown>, name: string): void {
19
+ const state = bindings[name] as { get?(): unknown; set?(value: unknown): void };
20
+ if (typeof state?.get !== 'function' || typeof state.set !== 'function') throw new Error(`h-value ${name} requires a writable signal`);
21
+ effect(() => { const value = String(state.get!() ?? ''); if (node.value !== value) node.value = value; });
22
+ listen(node, 'input', () => state.set!(node.value));
23
+ }
24
+ export function bindAction(node: HTMLElement, state: PresentationState, name: string): void {
25
+ const handler = state.actions[name];
26
+ if (typeof handler !== 'function') throw new Error(`Missing presentation action ${name}`);
27
+ listen(node, node.tagName === 'FORM' ? 'submit' : 'click', event => {
28
+ if (node.tagName === 'FORM') event.preventDefault();
29
+ return handler.call(state.actions, event);
30
+ });
31
+ }
package/src/vite/index.ts CHANGED
@@ -1,17 +1,39 @@
1
1
  import type { Plugin } from 'vite';
2
2
  import { compile } from '../compiler/index.js';
3
- export function hesh(): Plugin {
3
+ import { createArchitectureCompiler, writeProject, formatDiagnostic, type ProjectOptions } from '../architecture/index.js';
4
+ import { resolve } from 'node:path';
5
+ export function hesh(options: { architecture?: ProjectOptions } = {}): Plugin {
4
6
  let production = false;
7
+ const architecture = createArchitectureCompiler();
8
+ let project: ProjectOptions | undefined;
9
+ let watched: string[] = [];
10
+ let warn: ((message: string) => void) | undefined;
11
+ const check = () => {
12
+ if (!project) return;
13
+ const result = architecture.check(project);
14
+ if (result.diagnostics.some(item => item.severity === 'error')) throw new Error(result.diagnostics.map(formatDiagnostic).join('\n'));
15
+ for (const item of result.diagnostics) if (item.severity === 'warning') warn?.(formatDiagnostic(item));
16
+ writeProject(result); watched = result.files;
17
+ };
5
18
  return {
6
19
  name: 'heshjs', enforce: 'pre',
7
20
  config() { return { appType: 'spa' }; },
8
- configResolved(config) { production = config.command === 'build'; },
21
+ configResolved(config) {
22
+ production = config.command === 'build';
23
+ warn = message => config.logger.warn(message);
24
+ if (options.architecture) { project = { ...options.architecture, root: resolve(config.root, options.architecture.root) }; check(); }
25
+ },
26
+ buildStart() { for (const file of watched) this.addWatchFile(file); },
9
27
  transform(source, id) {
28
+ 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}`);
10
29
  if (!id.endsWith('.hesh')) return;
11
30
  const result = compile(source, { filename: id, development: !production });
12
31
  return { code: result.code, map: null };
13
32
  },
14
33
  handleHotUpdate(context) {
34
+ if (project && (watched.includes(context.file.replace(/\\/g, '/').toLowerCase()) || watched.includes(context.file.replace(/\\/g, '/')))) {
35
+ check(); context.server.ws.send({ type: 'full-reload' }); return [];
36
+ }
15
37
  if (context.file.endsWith('.hesh')) { context.server.ws.send({ type: 'full-reload' }); return []; }
16
38
  }
17
39
  };