@heshammesalam/heshjs 0.2.0 → 0.5.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 (61) hide show
  1. package/README.md +17 -12
  2. package/cli.js +31 -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 +18 -0
  7. package/dist/architecture/html.js +150 -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 +428 -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 +186 -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/heshjs/index.d.ts +2 -0
  24. package/dist/heshjs/index.js +1 -0
  25. package/dist/heshjs/index.js.map +1 -1
  26. package/dist/hmvc/index.d.ts +2 -0
  27. package/dist/hmvc/index.js +11 -0
  28. package/dist/hmvc/index.js.map +1 -1
  29. package/dist/parser/index.d.ts +15 -1
  30. package/dist/parser/index.js +73 -3
  31. package/dist/parser/index.js.map +1 -1
  32. package/dist/presentation/index.d.ts +12 -0
  33. package/dist/presentation/index.js +39 -0
  34. package/dist/presentation/index.js.map +1 -0
  35. package/dist/units/index.d.ts +13 -0
  36. package/dist/units/index.js +16 -0
  37. package/dist/units/index.js.map +1 -0
  38. package/dist/vite/index.d.ts +4 -1
  39. package/dist/vite/index.js +36 -2
  40. package/dist/vite/index.js.map +1 -1
  41. package/docs/contracts.md +67 -0
  42. package/docs/domain-graph.md +205 -0
  43. package/docs/grammar.md +4 -0
  44. package/docs/units.md +71 -0
  45. package/package.json +10 -4
  46. package/scaffold/assets/hesh-icon.png +0 -0
  47. package/scaffold/create.js +78 -0
  48. package/scaffold/template.js +188 -0
  49. package/src/architecture/diagnostics.ts +11 -0
  50. package/src/architecture/html.ts +101 -0
  51. package/src/architecture/index.ts +291 -0
  52. package/src/compiler/index.ts +1 -0
  53. package/src/destinations/index.ts +147 -0
  54. package/src/dom/index.ts +6 -1
  55. package/src/domains/index.ts +97 -0
  56. package/src/heshjs/index.ts +2 -0
  57. package/src/hmvc/index.ts +7 -0
  58. package/src/parser/index.ts +55 -4
  59. package/src/presentation/index.ts +33 -0
  60. package/src/units/index.ts +23 -0
  61. package/src/vite/index.ts +24 -2
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/vite/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,MAAM,sBAAsB,CAAC;AAC/C,MAAM,UAAU,IAAI;IAClB,IAAI,UAAU,GAAG,KAAK,CAAC;IACvB,OAAO;QACL,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,KAAK;QAC9B,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"]}
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,67 @@
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
+ [ "parts", "{", { Identifier, ";" }, "}" ], "}" ;
20
+ destination = "destination", Identifier, "{",
21
+ "url", ":", String, ";",
22
+ "input", "{", { Identifier, ":", ("string" | "number" | "boolean"), ";" }, "}",
23
+ "presentation", ":", Identifier, ";", [ "access", ":", String, ";" ], "}" ;
24
+ application = "application", Identifier, "{",
25
+ "domains", "{", { Identifier, ";" }, "}",
26
+ "destinations", "{", { Identifier, ";" }, "}", "}" ;
27
+ Path = Identifier, { ".", Identifier } ;
28
+ ```
29
+
30
+ Fields/blocks can appear in any order, once each. Unknown/duplicate fields are
31
+ errors. Strings use ordinary JS string escaping. Identifiers cannot be prototype
32
+ mutation names. Locations are retained for diagnostics. Semicolons are required.
33
+
34
+ `implementation` and `logic` have form `./module.js#exportName` (or .ts/.mjs).
35
+ A domain factory takes a dependencies object and returns its actual API. Every
36
+ exposed dotted operation must exist and be callable according to the TS checker;
37
+ every consumed path is Domain.operation and must be publicly exposed. An access
38
+ entry must name an exposed operation. `owns` is ownership metadata, not a generated
39
+ entity/validation/database implementation. The runtime passes only declared
40
+ consumed operations and exposes only the validated public facade.
41
+
42
+ A presentation factory receives `{ domains, input, location, signal }` and returns
43
+ `{ bindings, actions }`. bindings values may be signals/readers/ordinary values;
44
+ actions are callable. Both declared names and nested template paths are checked
45
+ against its inferred JS/TS return shape. HTML text `{product.name}` is escaped via
46
+ text nodes. `h-bind="product.name"` binds textContent (cannot have child content),
47
+ `h-action="reserve"` listens to click (submit on a form), `h-value="quantity"`
48
+ binds a writable signal to an input/textarea/select, and
49
+ `h-destination="ProductDetails" h-input-id="product.id"` binds a native anchor to
50
+ a validated destination. Other normal HTML attributes/semantics are preserved.
51
+ No arbitrary JS expressions, raw HTML, inline event attributes, script elements,
52
+ h-each, h-key or unknown h-* directives are accepted in this first milestone.
53
+ Use normal CSS files; template scoping and keyed template lists remain deferred.
54
+
55
+ Standalone presentations also compile to reusable HTML screens. `h-outlet` on an
56
+ empty container creates the persistent shell's destination outlet (not permitted
57
+ inside destination screens). `h-part="Badge"` mounts a declared nested Hesh Unit
58
+ from the factory's `parts.Badge`. Parts inherit the parent context and disposal
59
+ scope. A container may have only one of h-bind, h-outlet or h-part and no children.
60
+
61
+ Destination URL params must exactly match input names. Values are encoded/decoded
62
+ once; number inputs must be finite and booleans serialize as true/false. Duplicate
63
+ or structurally conflicting URL destinations are errors. Generated declarations
64
+ provide TS input types and JS IntelliSense. Runtime validation also rejects missing,
65
+ extra or invalid fields in JavaScript. Destination policy names are client metadata;
66
+ they cannot enforce server-side authorization. A policy without an authorize
67
+ 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.
package/docs/grammar.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # Milestone 1 language (v0.1)
2
2
 
3
+ This remains the legacy component grammar. The opt-in architecture descriptor
4
+ extension is specified separately in [contracts.md](contracts.md); it does not
5
+ replace existing .hesh views or add template directives to the brace UI syntax.
6
+
3
7
  The grammar is deliberately smaller than the product roadmap. Source is UTF-8.
4
8
  Whitespace and JavaScript comments separate tokens. Identifiers follow JavaScript
5
9
  identifier rules. Strings and numbers follow JavaScript lexical rules.
package/docs/units.md ADDED
@@ -0,0 +1,71 @@
1
+ # Unified HeshJS 0.5
2
+
3
+ Use Node 22.12 or later:
4
+
5
+ ```sh
6
+ npx @heshammesalam/heshjs@latest create
7
+ ```
8
+
9
+ Choose your folder and JavaScript or TypeScript. JavaScript is the default.
10
+ For automation: `npx @heshammesalam/heshjs@latest create my-site --ts`.
11
+ Run `cd my-site`, `npm install`, then `npm run dev`.
12
+
13
+ Only `@heshammesalam/heshjs` is needed for Hesh runtime/compiler/CLI features.
14
+ Vite is the build tool; TypeScript is included in typed project development dependencies.
15
+ The former `create-heshjs` package is a legacy entry point. Do not use `npm create`:
16
+ that convention resolves a separate creator package. Use the unified npx command.
17
+
18
+ ```text
19
+ src/
20
+ app.hesh
21
+ hesh-units/
22
+ identity/
23
+ unit.hesh
24
+ state.js # .ts in typed projects
25
+ actions.js
26
+ parts/login/
27
+ state.js
28
+ actions.js
29
+ screen.html
30
+ workspace/
31
+ unit.hesh
32
+ state.js
33
+ actions.js
34
+ parts/home/...
35
+ parts/about/...
36
+ parts/counter/parts/detail/...
37
+ foundation/shell/
38
+ unit.hesh
39
+ unit.js
40
+ state.js
41
+ actions.js
42
+ screen.html
43
+ parts/badge/
44
+ unit.js
45
+ state.js
46
+ actions.js
47
+ screen.html
48
+ ```
49
+
50
+ Hesh Units preserve hierarchical MVC ownership with Hesh-specific names. State
51
+ corresponds to the model, actions to the controller, and screen to the view.
52
+ `defineUnit` composes these using the same scope/parent/lifetime machinery as
53
+ `defineModule`. `h-part="Badge"` mounts an explicitly declared child unit and
54
+ disposes its state effects, DOM and listeners with its parent. Instances have
55
+ independent state. `h-outlet` mounts the SPA destination outlet inside a persistent
56
+ shell; it is not permitted inside asynchronous destination screens.
57
+
58
+ Every starter UI screen is ordinary HTML, including shell and badge. `.hesh`
59
+ files declare Domain Graph contracts; generated `.hesh-generated` JS and type
60
+ declarations are compiler output. Existing brace views and `/hmvc` APIs remain
61
+ available for compatibility. Domain Graph controls business dependencies and
62
+ typed destinations; HTML units control hierarchy and ownership.
63
+
64
+ Use `npm run check` for architecture checks (plus strict tsc in typed projects),
65
+ `npm run graph` for dependencies, and `npm run build` for production. The supplied
66
+ logo is bundled locally and used as favicon. The session is an in-memory demo;
67
+ production authorization needs server enforcement. Configure the production host
68
+ to serve index.html for app URLs while serving real static assets normally.
69
+
70
+ Unsupported features remain unsupported: SSR, Journeys, deep reactivity and
71
+ keyed HTML template lists. Native HTML bindings accept data paths, not arbitrary JS.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@heshammesalam/heshjs",
3
- "version": "0.2.0",
4
- "description": "An experimental independent reactive UI framework with brace-based .hesh components",
3
+ "version": "0.5.0",
4
+ "description": "Independent reactive UI framework with HTML screens, hierarchical Hesh Units, Domain Graph and an integrated project CLI",
5
5
  "author": "Hesham",
6
6
  "license": "UNLICENSED",
7
7
  "type": "module",
@@ -16,11 +16,17 @@
16
16
  "./vite": { "types": "./dist/vite/index.d.ts", "import": "./dist/vite/index.js" },
17
17
  "./router": { "types": "./dist/router/index.d.ts", "import": "./dist/router/index.js" },
18
18
  "./hmvc": { "types": "./dist/hmvc/index.d.ts", "import": "./dist/hmvc/index.js" },
19
+ "./units": { "types": "./dist/units/index.d.ts", "import": "./dist/units/index.js" },
20
+ "./domains": { "types": "./dist/domains/index.d.ts", "import": "./dist/domains/index.js" },
21
+ "./destinations": { "types": "./dist/destinations/index.d.ts", "import": "./dist/destinations/index.js" },
22
+ "./presentation": { "types": "./dist/presentation/index.d.ts", "import": "./dist/presentation/index.js" },
23
+ "./architecture": { "types": "./dist/architecture/index.d.ts", "import": "./dist/architecture/index.js" },
19
24
  "./package.json": "./package.json"
20
25
  },
21
- "files": ["dist", "src", "docs", "README.md"],
26
+ "files": ["dist", "src", "docs", "README.md", "cli.js", "scaffold"],
27
+ "bin": { "hesh": "./cli.js" },
22
28
  "engines": { "node": ">=22.12.0" },
23
- "dependencies": { "typescript": "~5.9.3" },
29
+ "dependencies": { "typescript": "~5.9.3", "parse5": "8.1.0" },
24
30
  "peerDependencies": { "vite": "^7.2.2" },
25
31
  "peerDependenciesMeta": { "vite": { "optional": true } },
26
32
  "publishConfig": { "access": "public", "registry": "https://registry.npmjs.org/" }
Binary file
@@ -0,0 +1,78 @@
1
+ #!/usr/bin/env node
2
+ import { mkdir, readdir, writeFile, readFile } from 'node:fs/promises';
3
+ import { resolve, basename, join, dirname } from 'node:path';
4
+ import { createInterface } from 'node:readline';
5
+ import { stdin, stdout } from 'node:process';
6
+ import { applicationFiles } from './template.js';
7
+
8
+ export async function createProject(args = process.argv.slice(2)) {
9
+ if (args.includes('--help') || args.includes('-h')) {
10
+ console.log('Usage: hesh create [directory] [--ts | --js]\nOmit directory/language to choose interactively.');
11
+ return;
12
+ }
13
+ const flags = args.filter(arg => arg.startsWith('--'));
14
+ if (flags.some(flag => !['--ts', '--js'].includes(flag)) || (flags.includes('--ts') && flags.includes('--js'))) throw new Error('Choose --ts or --js. Use --help for usage.');
15
+ const directories = args.filter(arg => !arg.startsWith('--'));
16
+ if (directories.length > 1) throw new Error('Provide at most one project directory. Use --help for usage.');
17
+ let terminal, lines, interrupted = false;
18
+ const ask = async (question, validate) => {
19
+ if (!terminal) {
20
+ terminal = createInterface({ input: stdin, output: stdout, terminal: Boolean(stdin.isTTY && stdout.isTTY) });
21
+ lines = terminal[Symbol.asyncIterator]();
22
+ terminal.on('SIGINT', () => { interrupted = true; terminal.close(); });
23
+ }
24
+ while (true) {
25
+ stdout.write(question);
26
+ const answer = await lines.next();
27
+ if (answer.done) {
28
+ const error = new Error(interrupted ? 'Project creation canceled.' : 'Input ended. Provide a directory and --js or --ts for non-interactive use.');
29
+ error.exitCode = interrupted ? 130 : 1;
30
+ throw error;
31
+ }
32
+ const value = validate(answer.value.trim());
33
+ if (value !== undefined) return value;
34
+ }
35
+ };
36
+ let directory, typed, target;
37
+ try {
38
+ console.log('\nHeshJS — Create your application\n');
39
+ directory = directories[0] ?? await ask('Project folder: ', answer => {
40
+ if (answer && !answer.includes('\0')) return answer;
41
+ console.log('Enter the folder name or path you want to create.');
42
+ });
43
+ target = resolve(directory);
44
+ let existing;
45
+ try { existing = await readdir(target); } catch (error) { if (error.code !== 'ENOENT') throw error; }
46
+ if (existing?.length) throw new Error(`Directory is not empty: ${target}`);
47
+ typed = flags.includes('--ts') ? true : flags.includes('--js') ? false : await ask('Choose language:\n 1. JavaScript\n 2. TypeScript\nLanguage [1]: ', answer => {
48
+ const choice = answer.toLowerCase();
49
+ if (['', '1', 'js', 'javascript'].includes(choice)) return false;
50
+ if (['2', 'ts', 'typescript'].includes(choice)) return true;
51
+ console.log('Enter 1 for JavaScript or 2 for TypeScript.');
52
+ });
53
+ } finally { terminal?.close(); }
54
+
55
+ // Read the bundled asset before creating any project files.
56
+ const icon = await readFile(new URL('./assets/hesh-icon.png', import.meta.url));
57
+ const name = basename(target).toLowerCase().replace(/[^a-z0-9-]/g, '-') || 'hesh-app';
58
+ const files = {
59
+ 'package.json': JSON.stringify({ name, private: true, version: '0.0.0', type: 'module', scripts: { dev: 'vite', check: 'hesh check src --write' + (typed ? ' && tsc --noEmit' : ''), graph: 'hesh graph src', build: 'hesh check src --write && ' + (typed ? 'tsc --noEmit && ' : '') + 'vite build', preview: 'vite preview' }, dependencies: { '@heshammesalam/heshjs': '0.5.0' }, devDependencies: { vite: '^7.2.2', ...(typed ? { typescript: '~5.9.3' } : {}) } }, null, 2) + '\n',
60
+ '.gitignore': 'node_modules/\ndist/\n',
61
+ 'index.html': `<!doctype html>\n<html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><link rel="icon" type="image/png" href="/brand/hesh-icon.png"><link rel="apple-touch-icon" href="/brand/hesh-icon.png"><title>HeshJS</title></head><body><div id="app"></div><script type="module" src="/src/main.${typed ? 'ts' : 'js'}"></script></body></html>\n`,
62
+ 'vite.config.js': "import { hesh } from '@heshammesalam/heshjs/vite';\nexport default { plugins: [hesh({ architecture: { root: 'src'" + (typed ? ", tsconfig: '../tsconfig.json'" : '') + " } })] };\n",
63
+ 'src/style.css': ':root { font: 17px/1.6 system-ui; background: #f3f6fb; color: #17243a; } #app { max-width: 900px; margin: 5vh auto; padding: 32px; background: white; border-radius: 16px; } .hesh-brand { display: flex; align-items: center; gap: 16px; margin-bottom: 24px; } .hesh-brand img { display: block; width: 80px; height: 80px; object-fit: contain; flex: 0 0 80px; } .hesh-brand h1 { margin: 0; font-size: clamp(1.3rem, 4vw, 2rem); } nav { display: flex; flex-wrap: wrap; gap: 20px; } button,input { font: inherit; margin-right: 12px; padding: 8px 16px; } button { cursor: pointer; } button:focus-visible, a:focus-visible, input:focus-visible { outline: 3px solid #2563eb; } main { padding: 20px 0; } footer { border-top: 1px solid #dbe3ef; padding-top: 16px; } .architecture-note { padding: 12px; background: #f0f4ff; border-radius: 8px; } @media(max-width:600px) { #app { margin: 0; padding: 20px; border-radius: 0; } }\n',
64
+ 'public/brand/hesh-icon.png': icon,
65
+ ...applicationFiles(typed)
66
+ };
67
+ if (typed) {
68
+ files['tsconfig.json'] = JSON.stringify({ compilerOptions: { target: 'ES2022', module: 'ESNext', moduleResolution: 'Bundler', strict: true, noEmit: true, allowImportingTsExtensions: true, skipLibCheck: true, types: ['vite/client'] }, include: ['src'] }, null, 2) + '\n';
69
+ files['src/hesh.d.ts'] = "declare module '*.hesh' {\n import type { Component } from '@heshammesalam/heshjs';\n const component: Component;\n export default component;\n}\n";
70
+ }
71
+ await mkdir(target, { recursive: true });
72
+ for (const [path, content] of Object.entries(files)) {
73
+ await mkdir(dirname(join(target, path)), { recursive: true });
74
+ await writeFile(join(target, path), content, { flag: 'wx' });
75
+ }
76
+ console.log(`Created ${typed ? 'TypeScript' : 'JavaScript'} HeshJS project in ${target}\n\nNext:\n cd ${JSON.stringify(directory)}\n npm install\n npm run dev`);
77
+ }
78
+