@muten/core 0.0.9 → 0.0.11
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 +135 -68
- package/dist/build.js +13 -7
- package/dist/engine/compile/compile.js +9 -9
- package/dist/engine/compile/emit.js +12 -6
- package/dist/engine/compile/helpers.js +1 -1
- package/dist/engine/compile/logic.js +2 -2
- package/dist/engine/ir/compose.js +1 -1
- package/dist/engine/ir/flatten.js +1 -1
- package/dist/engine/ir/print.js +15 -15
- package/dist/engine/ir/validate.js +1 -1
- package/dist/engine/lang/grammar.js +2 -2
- package/dist/engine/lang/lexer.js +4 -4
- package/dist/engine/lang/manifest.js +1 -1
- package/dist/engine/lang/parse.js +1 -1
- package/dist/engine/project/analyze.js +1 -1
- package/dist/engine/project/check-app.js +1 -0
- package/dist/engine/shared/vocab.js +1 -1
- package/dist/lint.js +2 -2
- package/dist/vite-plugin-muten.js +1 -1
- package/grammar/muten.gbnf +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,8 +2,11 @@
|
|
|
2
2
|
<img width="157" height="157" alt="Group 21" src="https://github.com/user-attachments/assets/fe9a02e6-483d-4788-9286-142c1ddb7057" />
|
|
3
3
|
<br/>
|
|
4
4
|
|
|
5
|
+
## ALPHA - STILL ON DEVELOPMENT
|
|
6
|
+
Muten is still under active development. We are currently in the alpha stage and are working on training models with Muten. Please keep in mind that improvements are being made gradually, and version 1.0 has not been released yet.
|
|
7
|
+
|
|
5
8
|
An **AI-first** frontend framework. You write `.muten` files; muten compiles them to vanilla JS
|
|
6
|
-
with fine-grained signals
|
|
9
|
+
with fine-grained signals - **no virtual DOM, no framework runtime to ship**. The language is small,
|
|
7
10
|
semantic and analyzable on purpose: an AI (or a person) can **locate and mutate** an app cheaply.
|
|
8
11
|
|
|
9
12
|
```sh
|
|
@@ -11,67 +14,127 @@ npm create muten@latest my-app # scaffold a new app (cross-platform: Windows +
|
|
|
11
14
|
cd my-app && npm install && npm run dev
|
|
12
15
|
```
|
|
13
16
|
|
|
17
|
+
## What a page looks like
|
|
18
|
+
|
|
19
|
+
One file, declarative, no imports or boilerplate. This is the whole thing:
|
|
20
|
+
|
|
21
|
+
```
|
|
22
|
+
screen home
|
|
23
|
+
|
|
24
|
+
entity Product {
|
|
25
|
+
name text
|
|
26
|
+
price number
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
state {
|
|
30
|
+
products = [] : list<Product>
|
|
31
|
+
draft = {} : Product
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
action add mutates products, draft <- p {
|
|
35
|
+
products.push({ name: p.name, price: p.price }) # build a record inline
|
|
36
|
+
draft.reset() # then clear the form
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
Page style(column, gap.md) {
|
|
40
|
+
Form bind @draft submit add "Add product"
|
|
41
|
+
|
|
42
|
+
each products.sortDesc(p => p.price) as p { # render the list, sorted
|
|
43
|
+
Text "{p.name} - ${p.price}"
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
Text "Total: ${products.sum(p => p.price)}" # a live aggregate, no JS
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
No `useState`, no component tree, no build wiring, and `muten check` validates every reference and type before
|
|
51
|
+
it ever runs in a browser.
|
|
52
|
+
|
|
14
53
|
## Why muten
|
|
15
54
|
|
|
16
55
|
For an AI the cost of working on a codebase is **context + mistakes + edit-radius**. muten is built to cut
|
|
17
|
-
all three *by construction*
|
|
18
|
-
|
|
19
|
-
- **Almost nothing to ship
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
state/action/part, bad style token, illegal mutation) in milliseconds, no browser — a feedback loop the
|
|
56
|
+
all three *by construction* - these are properties of how it compiles, not marketing:
|
|
57
|
+
|
|
58
|
+
- **Almost nothing to ship**: no virtual DOM, no framework runtime. The same todo app ships a small fraction of
|
|
59
|
+
the JS the big frameworks do, and a static page ships *zero*. (See the size table in *muten vs React / Vue /
|
|
60
|
+
Svelte* below.)
|
|
61
|
+
- **A deterministic oracle**: `muten check --json` validates every page at compile time (unknown
|
|
62
|
+
state/action/part, bad style token, illegal mutation) in milliseconds, no browser - a feedback loop the
|
|
25
63
|
others don't have. A *bounded* language is what makes that possible.
|
|
26
|
-
- **The whole app as data
|
|
64
|
+
- **The whole app as data**: `app.map.json` is a compact index of routes + structure an agent reads first,
|
|
27
65
|
instead of grepping a component tree.
|
|
28
|
-
- **Small edit radius
|
|
66
|
+
- **Small edit radius**: the UI is declarative, so a change is usually a few lines in one file.
|
|
67
|
+
|
|
68
|
+
The trade is deliberate: a small, analyzable language an AI can hold in its head, not a general-purpose one it can't.
|
|
69
|
+
|
|
70
|
+
## muten vs React / Vue / Svelte - the honest version
|
|
71
|
+
|
|
72
|
+
They are general-purpose: they build *anything*, with mature ecosystems and a deep talent pool. **For a human team
|
|
73
|
+
building a big, bespoke product, that is usually the right call.** muten makes the opposite trade on purpose, and it
|
|
74
|
+
only wins on its own terms:
|
|
75
|
+
|
|
76
|
+
| | muten | React / Vue / Svelte |
|
|
77
|
+
|---|---|---|
|
|
78
|
+
| **Best for** | an **AI** builds & maintains it; the declarative 80% (CRUD, dashboards, content, internal tools) | human teams, large bespoke UIs |
|
|
79
|
+
| **Language surface** | small - the whole thing fits in an AI's context | large (hooks, lifecycle, reactivity rules) |
|
|
80
|
+
| **Catches mistakes** | `muten check` - at **compile time**, in milliseconds, no browser | at runtime / in tests |
|
|
81
|
+
| **A typical change** | a few lines in one file | ripples across components |
|
|
82
|
+
| **Ships to the browser** | ~2.8 KB gzip for a todo app - a static page ships **zero** | 14-59 KB of runtime + your app |
|
|
83
|
+
| **Ecosystem / maturity** | young · one maintainer · **pre-1.0** | mature · huge |
|
|
29
84
|
|
|
30
|
-
The
|
|
85
|
+
The single biggest reason AI-written muten works on the *first* try more often is that middle row: **a compiler that
|
|
86
|
+
answers before the browser does.**
|
|
87
|
+
|
|
88
|
+
**The honest cost:** muten is small by design, so it can't express everything. It shines when an **AI builds and
|
|
89
|
+
maintains the app** and the app is the declarative 80%. It is **not** the tool for a hand-crafted, highly-custom UI
|
|
90
|
+
that needs the full React ecosystem, and it doesn't pretend to be.
|
|
91
|
+
|
|
92
|
+
> **Rule of thumb:** let muten do the structure and the data; couple in other tech for the rest (next section).
|
|
31
93
|
|
|
32
94
|
## Capabilities
|
|
33
95
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
muten the
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
**
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
96
|
+
| Area | What you get |
|
|
97
|
+
|---|---|
|
|
98
|
+
| **UI** | declarative primitives (layout, text, forms, tables, links) · `when`/`each` control flow · `style()` layout tokens + `class()` look (reactive: `class(active when isOpen)`) · `on(event: action)` on any element |
|
|
99
|
+
| **State** | local `state` · app-global `store` · derived `get` · `action`s with `if/else` · fine-grained signals. A page action can **call a store action** (`cart.add(d) draft.reset()`): store + local work in one handler |
|
|
100
|
+
| **Lists** | bounded ops, no raw `map`/`reduce`: inline objects (`push({…})`) · in-place `patch` · filtered `each…where` · aggregates `sum`/`count`/`avg`/`min`/`max` · `sort`/`sortDesc` |
|
|
101
|
+
| **Forms** | a `Form` auto-built from an entity, one input per field: `text` · `number` (coerced) · `email` · `bool` (checkbox) · `enum` (select), with built-in validation |
|
|
102
|
+
| **Data** | `query` states over `sources` (full HTTP: method, headers, body, nested `at`) · one `api` block (named multi-backend clients) · optimistic CRUD (`create`/`update`/`delete` + `.pending`/`.error`) · `refetch(q: …, page: …)` · a `post`/`put`/`delete` escape for non-REST |
|
|
103
|
+
| **Routing** | real-path URLs · params (`/product/:id` → `param id`) · route guards · a `/404` catch-all |
|
|
104
|
+
| **SEO / SSR** | `muten build` pre-renders every route to real HTML (static pages ship zero JS; data pages fetched at build) · per-page `meta { title … description … }` with `og:*` auto-derived |
|
|
105
|
+
| **Interop** (lowest tier first) | `class()` for native HTML + CSS libs · `Custom` for vanilla-JS libs (charts, maps, pickers) · `use fmt from "./lib.ts"` for JS logic · Svelte/React **islands** for a real framework component |
|
|
106
|
+
| **AI-native** | `lint == build` · one source of truth per concept · the full language reference ships in every scaffolded app under `.claude/` (an AGENTS guide + a Claude skill) |
|
|
107
|
+
|
|
108
|
+
## How muten couples with the rest of the web - three tiers
|
|
109
|
+
|
|
110
|
+
muten the *language* stays tiny on purpose; a muten *app* reaches the whole web platform through **bounded,
|
|
111
|
+
analyzable escapes**. The point: you never *fight* the language to do something it doesn't have, you drop to the
|
|
112
|
+
right tier, and the compiler still checks the seam. Reach for the **lowest tier that works**:
|
|
113
|
+
|
|
114
|
+
| Tier | What it reaches | Typical examples | The trade |
|
|
115
|
+
|---|---|---|---|
|
|
116
|
+
| **1 · Pure muten** | the declarative 80% - pages, routing (params, guards, `/404`), `state`/`store`/`get`, the **list toolkit**, `Form` (+validation), `query` over REST, SSG + SEO | a whole **CRUD / SaaS / catalog / dashboard / content** app | **zero extra deps** |
|
|
117
|
+
| **2 · muten + the platform** | native HTML + CSS libs via `class()`, **vanilla JS via `Custom`**, JS logic via `use … from "./lib.ts"` | `<dialog>`, Tailwind/DaisyUI, chart.js, Leaflet, flatpickr, Quill, zod, date-fns | a JS dep - **no framework runtime** |
|
|
118
|
+
| **3 · Svelte / React island** | a real framework component used as a node - props ↓, events ↑ | **shadcn/ui**, a React-only lib (no native/vanilla equivalent) | ships that framework's runtime (lazy, code-split) |
|
|
119
|
+
|
|
120
|
+
Almost every "hard widget" lands at **tier 2, without React**; tier 3 is the narrow last resort, not the default.
|
|
121
|
+
|
|
122
|
+
> "Not expressible in pure muten" usually means **tier 2 (platform)**, rarely **tier 3 (a framework component)** -
|
|
123
|
+
> and every escape is *bounded*: the oracle still checks the border, so the language never grows into a UI kit.
|
|
124
|
+
|
|
125
|
+
**Why the escapes stay safe.** The compiler validates the *seam* - the `@state` props and `action` callbacks
|
|
126
|
+
crossing into a `Custom`/island, and the call site of a `use` function (an undeclared one is a `check` error). So
|
|
127
|
+
coupling in chart.js, zod, or a shadcn island never costs you the oracle on the muten side.
|
|
128
|
+
|
|
129
|
+
**Deploy - the honest caveat.** A `use` function or an island ships real JS that the static `muten build` does
|
|
130
|
+
**not** bundle:
|
|
131
|
+
|
|
132
|
+
| Your app uses… | Deploy with |
|
|
133
|
+
|---|---|
|
|
134
|
+
| Pure muten, static content | `muten build` (zero-JS HTML) - or `vite build` |
|
|
135
|
+
| `use` JS functions, islands, or shared cross-page state | **`vite build`** (it bundles them; the static build doesn't) |
|
|
136
|
+
|
|
137
|
+
`npm run dev` runs every tier regardless - this only affects the production *build*.
|
|
75
138
|
|
|
76
139
|
## The app, by convention
|
|
77
140
|
|
|
@@ -99,9 +162,9 @@ routes {
|
|
|
99
162
|
|
|
100
163
|
```sh
|
|
101
164
|
muten build [dir] # compile → ./dist/<route>/index.html (+ app.map.json)
|
|
102
|
-
muten check [dir] [--json] # parse + validate every page, no compile
|
|
165
|
+
muten check [dir] [--json] # parse + validate every page, no compile - the deterministic ORACLE
|
|
103
166
|
# --json → structured diagnostics (code + loc + "did you mean…?") in ms, no browser
|
|
104
|
-
muten map [dir] [--json] # emit app.map.json COLD (no build)
|
|
167
|
+
muten map [dir] [--json] # emit app.map.json COLD (no build) - the app graph an AI reads FIRST
|
|
105
168
|
```
|
|
106
169
|
|
|
107
170
|
`check` and `map` are the AI-first feedback loop: an agent asks the compiler "is this valid, and what
|
|
@@ -140,7 +203,7 @@ The compiler is a straight pipeline of small, single-purpose stages:
|
|
|
140
203
|
.muten ─[lang]→ IR ─[ir: compose]→ tree ─[ir: flatten]→ Doc ─[ir: validate]→ ✓ ─[compile]→ JS
|
|
141
204
|
```
|
|
142
205
|
|
|
143
|
-
The source is TypeScript under `src/`, organized by **domain
|
|
206
|
+
The source is TypeScript under `src/`, organized by **domain**: each has its own README:
|
|
144
207
|
|
|
145
208
|
| Domain | Role |
|
|
146
209
|
|---|---|
|
|
@@ -158,7 +221,7 @@ file-level conventions (≤500 lines, honest types, data-table dispatch, no magi
|
|
|
158
221
|
## Build
|
|
159
222
|
|
|
160
223
|
`npm run build` = `tsc` (strict type-check) + `esbuild` → `dist/**/*.js`, **minified, per-file**
|
|
161
|
-
(modules preserved, so nothing bundles into a heavy monolith). `dist/` is generated
|
|
224
|
+
(modules preserved, so nothing bundles into a heavy monolith). `dist/` is generated - edit `src/`.
|
|
162
225
|
|
|
163
226
|
## Styling & escape hatch
|
|
164
227
|
|
|
@@ -166,7 +229,7 @@ muten imposes no theme. A page lays itself out with `style(…)` tokens (analyza
|
|
|
166
229
|
`theme.muten`) and skins itself via `class("…")` (your CSS / Tailwind / anything). For behavior the
|
|
167
230
|
primitives can't express, drop to a `Custom` component (`src/components/<Name>.js`).
|
|
168
231
|
|
|
169
|
-
## Islands
|
|
232
|
+
## Islands - Svelte & React
|
|
170
233
|
|
|
171
234
|
When a page needs a genuinely interactive widget or a framework UI lib muten can't express, mount a real
|
|
172
235
|
Svelte/React component as an **island**. The `svelte:` / `react:` prefix on `use … from` is the only marker;
|
|
@@ -189,7 +252,7 @@ Page style(padding.xl, gap.md) {
|
|
|
189
252
|
```
|
|
190
253
|
|
|
191
254
|
`prop: @state` sends a value **down** (a React island re-renders when the signal changes; Svelte mounts once);
|
|
192
|
-
`onX: action` sends a callback that fires a muten action
|
|
255
|
+
`onX: action` sends a callback that fires a muten action - that's how an island writes **back** to muten state.
|
|
193
256
|
No `client:` directive = hydrate on load. Add the framework's Vite plugin next to `muten()`:
|
|
194
257
|
|
|
195
258
|
```js
|
|
@@ -202,21 +265,25 @@ export default { plugins: [muten(), svelte(), react()] };
|
|
|
202
265
|
|
|
203
266
|
## Status & roadmap (honest)
|
|
204
267
|
|
|
205
|
-
**Pre-1.0
|
|
268
|
+
**Pre-1.0 - the core is solid, the edges are young.** Build real apps with it; don't bet a critical
|
|
206
269
|
production system on it yet (small ecosystem, one maintainer, not yet battle-tested).
|
|
207
270
|
|
|
208
271
|
**Solid today:** the language + compiler, the `check` / `build` / `map` CLI + oracle, the Vite plugin + dev
|
|
209
|
-
server + HMR, the VS Code extension (live-lint + autocomplete), Svelte & React islands
|
|
272
|
+
server + HMR, the VS Code extension (live-lint + autocomplete), Svelte & React islands.
|
|
273
|
+
The bounded list toolkit - inline objects, `patch`, `each…where`, aggregates (`sum`/`count`/`avg`/`min`/`max`),
|
|
274
|
+
`sort`/`sortDesc`, and page→store action composition, so a real CRUD/dashboard app is pure muten, no JS escape.
|
|
275
|
+
`Form` fields cover `text` · `number` (coerced) · `email` · `bool` (checkbox) · `enum` (select), with validation.
|
|
210
276
|
|
|
211
|
-
**Experimental:** full island **SSR
|
|
277
|
+
**Experimental:** full island **SSR**: `muten build` server-renders an island's HTML (first paint + SEO),
|
|
212
278
|
but client hydration of that island still needs its framework bundled (pair the SSG HTML with the Vite client build).
|
|
213
279
|
|
|
214
280
|
**Next, toward 1.0:**
|
|
215
|
-
-
|
|
216
|
-
entity→form model, so more is pure-muten with *no* library.
|
|
217
|
-
- bounded aggregates in expressions (`sum` · `count`) — e.g. a cart total without an escape.
|
|
281
|
+
- a `date`/`textarea` `Form` field type; number formatting (`round` / currency) in expressions.
|
|
218
282
|
- keyed `each` (large-list perf); a live `source` (SSE / websocket) for real-time.
|
|
283
|
+
- richer SSG for stateful multi-page apps (today a shared `.store` across pages deploys via `vite build`, not the
|
|
284
|
+
static `muten build`).
|
|
219
285
|
|
|
220
|
-
**By design (the moat, not a bug):** muten is declarative + bounded
|
|
221
|
-
|
|
222
|
-
|
|
286
|
+
**By design (the moat, not a bug):** muten is declarative + bounded. The list toolkit (`patch` · `sort` · the
|
|
287
|
+
aggregates · `each…where`) gives the *common* list jobs without exposing raw `map`/`reduce` - anything past that
|
|
288
|
+
(an arbitrary transform) is a `use` JS function, and a real framework widget is a tier 2/3 escape. The ceiling is
|
|
289
|
+
what keeps it small and analyzable; closing it would just make another general-purpose framework.
|
package/dist/build.js
CHANGED
|
@@ -1,12 +1,18 @@
|
|
|
1
|
-
import{writeFileSync as
|
|
2
|
-
`);const
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
import{writeFileSync as A,mkdirSync as U,readFileSync as K,existsSync as Q,rmSync as R}from"node:fs";import{join as i,relative as W,resolve as X,dirname as Y}from"node:path";import{createRequire as Z}from"node:module";import{pathToFileURL as M}from"node:url";import{Nt as ee,Fmt as te}from"#engine/shared/vocab.js";import{readRoutes as se,readApi as re}from"#engine/project/routes.js";import{renderSsrBody as oe,fetchSources as ne}from"#engine/project/ssr.js";import{routeEntry as ie}from"#engine/project/map.js";import{load as ae,loadAllParts as ce,findStores as le}from"#engine/project/load.js";import{validateStoresAndGuards as pe}from"#engine/project/check-app.js";import{validate as me}from"#engine/ir/validate.js";import{compile as q,compileStore as de}from"#engine/compile/compile.js";import{formatDiagnostic as T,ParseError as ue}from"#engine/shared/diagnostics.js";async function xe(o,l=i(o,"dist")){const m=e=>W(o,e);R(l,{recursive:!0,force:!0});const g=await ce(o);Object.keys(g).length&&console.log(`Parts: ${Object.keys(g).join(", ")}`);const d=le(i(o,"src")),F=Object.keys(d),h={};for(const[e,t]of Object.entries(d))h[e]=[...Object.keys(t.state||{}),...Object.keys(t.gets||{}),...Object.keys(t.actions||{})];const w={};for(const[e,t]of Object.entries(d))w[e]={state:Object.keys(t.state||{}),gets:Object.keys(t.gets||{}),actions:Object.keys(t.actions||{})};const I=pe(o,d,h);if(I.length)throw new Error(I.map(e=>T(e,e.file)).join(`
|
|
2
|
+
`));const N=Object.entries(d).map(([e,t])=>{const n=de({state:t.state||{},gets:t.gets||{},actions:t.actions||{},effects:t.effects||[],entities:t.entities||{},imports:t.imports||[]},t.mock||{},t.sources||{}).replace(/^[ \t]*import .*$/gm,"").replace(/^([ \t]*)export /gm,"$1");return`const __store_${e} = (function () {
|
|
3
|
+
${n}
|
|
4
|
+
return { ${h[e].join(", ")} };
|
|
5
|
+
})();`}).join(`
|
|
6
|
+
`),v=se(o),S=re(o);console.log(`Host app: ${o}`),console.log(`Pages: ${v.map(e=>"/"+e.route).join(", ")}
|
|
7
|
+
`);const y=[],C={app:o.split(/[\\/]/).pop()||"",parts:Object.keys(g),routes:{}},u={vite:null},b=Z(i(o,"package.json")),L=e=>import(M(b.resolve(e)).href),z=async()=>{if(u.vite)return u.vite;const{createServer:e}=await import("vite"),t=[];try{const n=await L("@sveltejs/vite-plugin-svelte");t.push(n.svelte({compilerOptions:{dev:!1}}))}catch{}try{const n=await L("@vitejs/plugin-react");t.push(n.default())}catch{}const r=await e({configFile:!1,root:o,mode:"production",appType:"custom",server:{middlewareMode:!0},logLevel:"silent",plugins:t,resolve:{dedupe:["svelte"]},optimizeDeps:{include:["react","react-dom","react/jsx-runtime","react/jsx-dev-runtime"]}});return u.vite=r,r},B=async(e,t)=>{const r=await z(),n=(await r.ssrLoadModule(X(t,e.path))).default;if(e.adapter==="svelte")return(await r.ssrLoadModule("svelte/server")).render(n,{props:e.props}).body;if(e.adapter==="react"){const a=await import(M(b.resolve("react-dom/server")).href),p=await import(M(b.resolve("react")).href),O=a.renderToString||a.default.renderToString,j=p.createElement||p.default.createElement;return O(j(n,e.props))}return""};for(const e of v){if(e.route.includes(":")){console.log(`\u2022 /${e.route} \u2014 skipped (route params run in the SPA runtime, not the static build)`);continue}let t;try{t=await ae(e.screenPath,g)}catch(s){if(!(s instanceof ue))throw s;const c={code:s.code,severity:"error",message:s.message,loc:s.loc,suggestion:null};throw new Error(`/${e.route}
|
|
8
|
+
`+T(c,m(e.screenPath)))}const{doc:r,data:n,sources:a,styles:p,partNames:O}=t,j=(r.imports||[]).filter(s=>!/^(svelte|react):/.test(s.from)).flatMap(s=>s.names);j.length&&console.log(` \u26A0 /${e.route}: \`use\` function(s) ${j.join(", ")} are NOT inlined into the standalone build \u2014 they'll throw at runtime. Use \`vite build\` for a bundle that includes them.`);const{ok:G,diagnostics:H}=me(r,{parts:O,stores:F,storeMembers:h});if(!G)throw new Error(`/${e.route}
|
|
9
|
+
`+H.map(s=>" "+T(s,m(e.screenPath))).join(`
|
|
10
|
+
`));const J=[...new Set(Object.values(r.nodes).filter(s=>s.type===ee.Custom).map(s=>s.props?.component))],k={};for(const s of J){if(!s)continue;const c=i(o,"src","components",s+".js");if(!Q(c))throw new Error(`/${e.route}: Custom component not found: src/components/${s}.js`);k[s]=K(c,"utf8")}const $=q(r,n,p.css,k,a,{api:S,stores:w,storeCode:N});let V=$,D=!1;if($.includes('<div id="app"></div>'))try{const s=Object.keys(a).length?{...n,...await ne(a,S)}:n,c=[];let x=oe(q(r,s,p.css,k,a,{format:te.Ssr,api:S,stores:w,storeCode:N}),c);for(let f=0;f<c.length;f++){let _="";try{_=await B(c[f],Y(e.screenPath))}catch(E){console.log(` \u2022 island ${c[f].path} \u2192 client-only (${E instanceof Error?E.message:E})`)}x=x.replace(`<!--mi:${f}-->`,_)}V=$.replace('<div id="app"></div>',`<div id="app">${x}</div>`),D=!0}catch{}const P=i(l,e.route);U(P,{recursive:!0}),A(i(P,"index.html"),V),console.log(`\u2713 /${e.route} \u2192 ${m(i(P,"index.html"))} (${Object.keys(r.nodes).length} nodes${D?", SSR":$.includes("<script")?", CSR":", static"}${p.from?", + "+p.from:""})`),y.push(e.route),C.routes["/"+e.route]=ie(m(e.screenPath),r,a)}if(u.vite&&await u.vite.close(),v.length>1&&F.length&&console.log(`
|
|
11
|
+
\u26A0 This app shares a .store across ${v.length} pages. The static build renders each page standalone, so store state does NOT persist across page navigations (and route guards / a persistent shell aren't wired). For a stateful multi-page SPA, deploy with \`vite build\`.`),U(l,{recursive:!0}),!y.includes("")){const e=y.map(t=>`<li><a href="./${t}/">/${t}</a></li>`).join(`
|
|
12
|
+
`);A(i(l,"index.html"),`<!doctype html><meta charset="utf-8"><title>app</title>
|
|
7
13
|
<h1>Routes</h1>
|
|
8
14
|
<ul>
|
|
9
15
|
${e}
|
|
10
16
|
</ul>
|
|
11
17
|
`),console.log(`
|
|
12
|
-
\u2713 ${m(i(l,"index.html"))} \u2192 route index`)}return
|
|
18
|
+
\u2713 ${m(i(l,"index.html"))} \u2192 route index`)}return A(i(l,"app.map.json"),JSON.stringify(C,null,2)),console.log(`\u2713 ${m(i(l,"app.map.json"))} \u2192 app graph (the root the AI reads)`),{routes:y,outDir:l}}export{xe as buildApp};
|
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
import{tokenClass as ne,resolveToken as Ee,defaultTheme as Oe}from"#engine/style/tokens.js";import{Nt as l,Ek as ve,Fmt as b,Fk as B}from"#engine/shared/vocab.js";import{customValue as se,CONTAINERS as oe,parseClause as xe,editableFields as ke}from"#engine/compile/helpers.js";import{emitStore as we,emitStatic as Je,emitStaticHtml as je,emitSsr as Ie,emitModule as Te,emitHtml as De}from"#engine/compile/emit.js";import{Logic as Re}from"#engine/compile/logic.js";function Be(m,k={},w="",J={},j={},$={}){return re(m,k,w,J,j,{...$,format:b.Module})}function He(m={},k={},w={}){const{state:J={},gets:j={},actions:$={},effects:y=[],entities:C={}}=m;return re({screen:"store",entities:C,state:J,actions:$,gets:j,effects:y,consts:{},constraints:{},rootId:void 0,nodes:{}},k,"",{},w,{format:b.Store})}function re(m,k={},w="",J={},j={},$={}){const{nodes:y,rootId:C,state:I,entities:H,screen:ce}=m,M=$.theme||Oe,ie={svelte:(e,s,o)=>`import('svelte').then((__s) => import(${JSON.stringify(o)}).then((__c) => (${e}.hasChildNodes() ? __s.hydrate : __s.mount)(__c.default, { target: ${e}, props: ${s} })))`,react:(e,s,o)=>`Promise.all([import('react-dom/client'), import('react'), import(${JSON.stringify(o)})]).then(([__d, __r, __c]) => { const __root = ${e}.hasChildNodes() ? __d.hydrateRoot(${e}, __r.createElement(__c.default, ${s})) : __d.createRoot(${e}); effect(() => __root.render(__r.createElement(__c.default, ${s}))); })`},P={};for(const e of m.imports||[]){const s=/^(svelte|react):(.+)$/.exec(e.from);if(s)for(const o of e.names)P[o]={adapter:s[1],path:s[2]}}let t=[],W=!1;const K=e=>{const s=t;t=[],e();const o=t;return t=s,o},U=new Set,h=(e,s)=>{for(const o of s.style||[])U.add(o);return[e,...(s.style||[]).map(ne),...(s.class||[]).filter(o=>typeof o=="string")].join(" ")},ae=new Set(Object.keys(I)),V=new Set(Object.entries(I).filter(([,e])=>typeof e.source=="string"&&e.source.startsWith("query:")).map(([e])=>e)),z=new Set,le={state:I,entities:H,actions:m.actions,consts:m.consts||{},gets:m.gets||{},effects:m.effects||[],stateKeys:ae,queryStates:V,stores:$.stores||{},usedStores:z,params:new Set(m.params||[]),format:$.format},u=new Re(le),E={locals:new Set},O=(e,s)=>{for(const o of s.class||[])typeof o!="string"&&t.push(`effect(() => el_${e}.classList.toggle(${JSON.stringify(o.name)}, !!(${u.compileExpr(o.cond,E)})));`);for(const[o,n]of Object.entries(s.on||{}))typeof n=="string"&&t.push(`el_${e}.addEventListener(${JSON.stringify(o)}, () => ${u.actionRef(n)}());`)},T=(e,s)=>{for(const o of y[e].children)Q(o,s)},G=e=>e.parts.map(s=>typeof s=="string"?JSON.stringify(s):`String(${u.compileExpr(s,E)} ?? '')`).join(" + ");function q(e,s,o,n,f){if(t.push(`const el_${e} = document.createElement('${s}');`),t.push(`el_${e}.className = ${JSON.stringify(o)};`),typeof n=="string")t.push(`el_${e}.textContent = ${JSON.stringify(n)};`);else if(n&&"kind"in n){const r=G(n),i=n.parts.some(p=>typeof p!="string");t.push(i?`effect(() => { el_${e}.textContent = ${r}; });`:`el_${e}.textContent = ${r};`)}t.push(`${f}.appendChild(el_${e});`)}function D(e,s,o){if(typeof o=="string")t.push(`el_${e}.${s} = ${JSON.stringify(o)};`);else if(o&&"kind"in o){const n=G(o),f=o.parts.some(r=>typeof r!="string");t.push(f?`effect(() => { el_${e}.${s} = ${n}; });`:`el_${e}.${s} = ${n};`)}}function Q(e,s){const o=y[e],n=o.props||{};if(P[o.type]){const r=P[o.type];t.push(`const el_${e} = document.createElement('div');`),t.push(`${s}.appendChild(el_${e});`);const i="{ "+Object.entries(o.args||{}).map(([_,d])=>typeof d=="string"&&!d.startsWith("@")&&(d in(m.actions||{})||d.includes("."))?`${JSON.stringify(_)}: (...__a) => ${u.actionRef(d)}(...__a)`:`${JSON.stringify(_)}: ${se(d)}`).join(", ")+" }";if($.format===b.Ssr){t.push(`el_${e}.innerHTML = __ssrIsland(${JSON.stringify(r.adapter)}, ${JSON.stringify(r.path)}, ${i});`);return}const p=ie[r.adapter](`el_${e}`,i,r.path);t.push(n.hydrate==="visible"?`__onVisible(el_${e}, () => ${p});`:n.hydrate==="idle"?`__onIdle(() => ${p});`:`${p};`);return}const f=oe[o.type];if(f){const[r,i]=f;t.push(`const el_${e} = document.createElement('${r}');`),t.push(`el_${e}.className = ${JSON.stringify(h(i,n))};`),o.type===l.Nav&&typeof n.label=="string"&&t.push(`el_${e}.setAttribute('aria-label', ${JSON.stringify(n.label)});`),t.push(`${s}.appendChild(el_${e});`),O(e,n),T(e,`el_${e}`);return}switch(o.type){case l.SearchField:{const r=u.bindSig(n.bind);t.push(`const el_${e} = document.createElement('input');`),t.push(`el_${e}.type = 'search';`),t.push(`el_${e}.className = ${JSON.stringify(h("search",n))};`),typeof n.placeholder=="string"&&t.push(`el_${e}.placeholder = ${JSON.stringify(n.placeholder)};`),t.push(`el_${e}.value = ${r}.get();`),t.push(`el_${e}.addEventListener('input', (e) => ${r}.set(e.target.value));`),t.push(`${s}.appendChild(el_${e});`);break}case l.DataTable:{const r=u.bindSig(n.data),i=V.has(r)?`${r}.get().data`:`${r}.get()`,p=n.columns||[],_=(n.where||[]).map(xe),d=_.filter(c=>!c.dynamic).map(c=>`.filter((row) => ${c.expr})`).join(""),N=_.filter(c=>c.dynamic).map(c=>`.filter((row) => ${c.expr})`).join(""),a=o.children.map(c=>y[c]).filter(c=>c.type===l.RowAction);t.push(`const el_${e} = document.createElement('table');`),t.push(`el_${e}.className = ${JSON.stringify(h("datatable",n))};`),t.push(`const head_${e} = el_${e}.createTHead().insertRow();`);for(const c of p)t.push(`{ const th = document.createElement('th'); th.textContent = ${JSON.stringify(c)}; head_${e}.appendChild(th); }`);a.length&&t.push(`head_${e}.appendChild(document.createElement('th'));`),t.push(`const body_${e} = el_${e}.createTBody();`),t.push(`${s}.appendChild(el_${e});`),t.push(`function renderRow_${e}(row) {`),t.push(" const tr = document.createElement('tr');");for(const c of p)t.push(` { const td = document.createElement('td'); td.textContent = row[${JSON.stringify(c)}] ?? ''; tr.appendChild(td); }`);for(const c of a){const g=c.props||{},Ce=g.arg!==void 0?u.compileExpr(g.arg,{locals:new Set(["row"])}):"";t.push(` { const td = document.createElement('td'); const b = document.createElement('button'); b.className = ${JSON.stringify(h("row-action",g))}; b.textContent = ${JSON.stringify(g.label)}; b.addEventListener('click', () => ${u.actionRef(g.action)}(${Ce})); td.appendChild(b); tr.appendChild(td); }`)}t.push(" return tr;"),t.push("}"),t.push(`function base_${e}() { return ${i}${d}; }`),t.push("effect(() => {"),t.push(` const rows = base_${e}()${N};`),t.push(` body_${e}.replaceChildren(...rows.map(renderRow_${e}));`),t.push("});");break}case l.Form:{const r=u.bindSig(n.bind),i=I[r].type,p=ke(H[i]),_=(m.constraints||{})[i]||{};t.push(`const el_${e} = document.createElement('form');`),t.push(`el_${e}.className = ${JSON.stringify(h("form",n))};`),t.push(`{ const t = document.createElement('div'); t.className = 'form-title'; t.textContent = ${JSON.stringify("New "+i)}; el_${e}.appendChild(t); }`);const d=[];for(const a of p){const c=`f_${e}_${a.name}`;if(d.push({...a,var:c,c:_[a.name]}),a.kind===B.Enum){t.push(`const ${c} = document.createElement('select');`),t.push(`${c}.className = 'field';`);for(const g of a.options)t.push(`{ const o = document.createElement('option'); o.value = ${JSON.stringify(g)}; o.textContent = ${JSON.stringify(g)}; ${c}.appendChild(o); }`)}else t.push(`const ${c} = document.createElement('input');`),t.push(`${c}.type = ${JSON.stringify(a.kind===B.Email?"email":"text")};`),t.push(`${c}.className = 'field';`),t.push(`${c}.placeholder = ${JSON.stringify(a.name)};`);t.push(`${c}.addEventListener('input', (e) => ${r}.set({ ...${r}.get(), ${JSON.stringify(a.name)}: e.target.value }));`),t.push(`el_${e}.appendChild(${c});`),_[a.name]&&t.push(`const err_${c} = document.createElement('small'); err_${c}.className = 'field-error'; el_${e}.appendChild(err_${c});`)}t.push(`{ const sb = document.createElement('button'); sb.type = 'submit'; sb.className = 'submit'; sb.textContent = ${JSON.stringify(typeof n.submitLabel=="string"?n.submitLabel:"Submit")}; el_${e}.appendChild(sb); }`);const N=[];for(const a of d){if(!a.c)continue;const c=`err_${a.var}`,g=`String(__d[${JSON.stringify(a.name)}] ?? '')`;N.push(`${c}.textContent = '';`),a.c.required&&N.push(`if (!${g}.trim()) { ${c}.textContent = 'Required'; __ok = false; }`),a.c.min!=null&&N.push(`if (${g} && ${g}.length < ${a.c.min}) { ${c}.textContent = 'Min ${a.c.min} characters'; __ok = false; }`),a.c.max!=null&&N.push(`if (${g}.length > ${a.c.max}) { ${c}.textContent = 'Max ${a.c.max} characters'; __ok = false; }`)}N.length?t.push(`el_${e}.addEventListener('submit', (e) => { e.preventDefault(); const __d = ${r}.get(); let __ok = true; ${N.join(" ")} if (__ok) ${u.actionRef(n.submit)}(); });`):t.push(`el_${e}.addEventListener('submit', (e) => { e.preventDefault(); ${u.actionRef(n.submit)}(); });`),t.push("effect(() => {"),t.push(` const d = ${r}.get();`);for(const a of d){const c=a.kind===B.Enum?JSON.stringify(a.options[0]):"''";t.push(` { const v = d[${JSON.stringify(a.name)}] ?? ${c}; if (${a.var}.value !== v) ${a.var}.value = v; }`)}t.push("});"),t.push(`${s}.appendChild(el_${e});`);break}case l.Text:q(e,"p",h("text",n),n.value,s),O(e,n);break;case l.Span:q(e,"span",h("span",n),n.value,s),O(e,n);break;case l.Title:q(e,n.level||"h1",h("title",n),n.value,s),O(e,n);break;case l.Image:{t.push(`const el_${e} = document.createElement('img');`),t.push(`el_${e}.className = ${JSON.stringify(h("image",n))};`),D(e,"src",n.src),D(e,"alt",n.alt??""),t.push(`${s}.appendChild(el_${e});`);break}case l.When:{if(!n.cond)throw new Error("when without a condition");const r=u.compileExpr(n.cond,E),i=K(()=>T(e,"__p"));t.push(`function build_${e}(__p) {`);for(const p of i)t.push(" "+p);t.push("}"),t.push(`const anchor_${e} = document.createComment('when');`),t.push(`${s}.appendChild(anchor_${e});`),t.push(`let shown_${e} = [];`),t.push("effect(() => {"),t.push(` if (${r}) {`),t.push(` if (!shown_${e}.length) { const __f = document.createDocumentFragment(); build_${e}(__f); shown_${e} = [...__f.childNodes]; anchor_${e}.parentNode.insertBefore(__f, anchor_${e}); }`),t.push(` } else if (shown_${e}.length) { for (const __n of shown_${e}) __n.remove(); shown_${e} = []; }`),t.push("});");break}case l.Each:{if(!n.list||!n.as)throw new Error("each without a list or item variable");const r=u.compileExpr(n.list,E),i=K(()=>T(e,"__p"));t.push(`function buildItem_${e}(__p, ${n.as}) {`);for(const p of i)t.push(" "+p);t.push("}"),t.push(`const anchor_${e} = document.createComment('each');`),t.push(`${s}.appendChild(anchor_${e});`),t.push(`let items_${e} = [];`),t.push("effect(() => {"),t.push(` for (const __n of items_${e}) __n.remove();`),t.push(" const __f = document.createDocumentFragment();"),t.push(` for (const ${n.as} of (${r} ?? [])) buildItem_${e}(__f, ${n.as});`),t.push(` items_${e} = [...__f.childNodes];`),t.push(` anchor_${e}.parentNode.insertBefore(__f, anchor_${e});`),t.push("});");break}case l.Custom:{t.push(`const el_${e} = document.createElement('div');`),t.push(`el_${e}.className = ${JSON.stringify(h("custom",n))};`),t.push(`${s}.appendChild(el_${e});`);const r=Object.entries(n.inputs||{}).map(([p,_])=>`${JSON.stringify(p)}: ${se(_)}`).join(", "),i=Object.entries(n.on||{}).map(([p,_])=>`${JSON.stringify(p)}: (...__a) => ${u.actionRef(typeof _=="string"?_:"")}(...__a)`).join(", ");t.push(`if (typeof __custom_${n.component} === 'function') __custom_${n.component}(el_${e}, { ${r} }, { ${i} });`);break}case l.Button:{if(t.push(`const el_${e} = document.createElement('button');`),t.push(`el_${e}.className = ${JSON.stringify(h("button",n))};`),o.children&&o.children.length?T(e,`el_${e}`):n.label!==void 0&&D(e,"textContent",n.label),n.action){const r=n.arg!==void 0?u.compileExpr(n.arg,E):"";t.push(`el_${e}.addEventListener('click', () => ${u.actionRef(n.action)}(${r}));`)}O(e,n),t.push(`${s}.appendChild(el_${e});`);break}case l.Link:{t.push(`const el_${e} = document.createElement('a');`),t.push(`el_${e}.className = ${JSON.stringify(h("link",n))};`),D(e,"href",n.to??"/"),o.children&&o.children.length?T(e,`el_${e}`):n.label!==void 0&&D(e,"textContent",n.label),O(e,n),t.push(`${s}.appendChild(el_${e});`);break}case l.Slot:{W=!0,t.push("const __outlet = document.createElement('div');"),t.push("__outlet.className = 'muten-outlet';"),t.push(`${s}.appendChild(__outlet);`);break}default:throw new Error("unsupported primitive: "+o.type)}}const v=e=>String(e??"").replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">"),L=e=>v(e).replace(/"/g,"""),S=e=>typeof e=="string"?e:"";function pe(){if($.format===b.Store||(m.params||[]).length)return!1;const e=new Set([l.When,l.Each,l.Custom,l.Form,l.SearchField,l.DataTable,l.Slot]),s=["action","bind","submit","on","inputs","data"],o=["value","src","alt","label","to"];for(const n of Object.keys(y)){const f=y[n],r=f.props||{};if(P[f.type]||e.has(f.type)||s.some(i=>r[i]!==void 0)||(r.class||[]).some(i=>typeof i!="string")||o.some(i=>{const p=r[i];return!!p&&typeof p=="object"&&"kind"in p&&p.kind===ve.Interp}))return!1}return!0}function X(e){const s=y[e],o=s.props||{},n=()=>(y[e].children||[]).map(X).join(""),f=i=>` class="${L(h(i,o))}"`,r=oe[s.type];if(r){const[i,p]=r;return`<${i}${f(p)}>${n()}</${i}>`}switch(s.type){case l.Text:return`<p${f("text")}>${v(S(o.value))}</p>`;case l.Span:return`<span${f("span")}>${v(S(o.value))}</span>`;case l.Title:{const i=o.level||"h1";return`<${i}${f("title")}>${v(S(o.value))}</${i}>`}case l.Image:return`<img${f("image")} src="${L(S(o.src))}" alt="${L(S(o.alt))}">`;case l.Link:return`<a${f("link")} href="${L(S(o.to)||"/")}">${s.children&&s.children.length?n():v(S(o.label))}</a>`;case l.Button:return`<button${f("button")}>${s.children&&s.children.length?n():v(S(o.label))}</button>`;default:return""}}const Y=$.format===b.Ssr?!1:pe(),ue=(m.params||[]).map(e=>`const ${e} = (__params || {})[${JSON.stringify(e)}] ?? '';`).join(`
|
|
2
|
-
`),fe=u.genState(),me=u.genActions(),$e=Object.entries(
|
|
3
|
-
`),he=u.genEffects();let Z=null;Y?Z=
|
|
4
|
-
`),ee={};for(const e of Object.values(
|
|
5
|
-
`),de=Object.entries(
|
|
1
|
+
import{tokenClass as ne,resolveToken as Ce,defaultTheme as Ee}from"#engine/style/tokens.js";import{Nt as l,Ek as Oe,Fmt as y,Fk as S}from"#engine/shared/vocab.js";import{customValue as se,CONTAINERS as oe,parseClause as ke,editableFields as xe}from"#engine/compile/helpers.js";import{emitStore as we,emitStatic as Je,emitStaticHtml as je,emitSsr as Ie,emitModule as Te,emitHtml as De}from"#engine/compile/emit.js";import{Logic as Fe}from"#engine/compile/logic.js";function Be($,J={},j="",I={},T={},h={}){return re($,J,j,I,T,{...h,format:y.Module})}function Me($={},J={},j={}){const{state:I={},gets:T={},actions:h={},effects:b=[],entities:O={},imports:C=[]}=$;return re({screen:"store",entities:O,state:I,actions:h,gets:T,effects:b,imports:C,consts:{},constraints:{},rootId:void 0,nodes:{}},J,"",{},j,{format:y.Store})}function re($,J={},j="",I={},T={},h={}){const{nodes:b,rootId:O,state:C,entities:A,screen:ie}=$,H=h.theme||Ee,ce={svelte:(e,s,o)=>`import('svelte').then((__s) => import(${JSON.stringify(o)}).then((__c) => (${e}.hasChildNodes() ? __s.hydrate : __s.mount)(__c.default, { target: ${e}, props: ${s} })))`,react:(e,s,o)=>`Promise.all([import('react-dom/client'), import('react'), import(${JSON.stringify(o)})]).then(([__d, __r, __c]) => { const __root = ${e}.hasChildNodes() ? __d.hydrateRoot(${e}, __r.createElement(__c.default, ${s})) : __d.createRoot(${e}); effect(() => __root.render(__r.createElement(__c.default, ${s}))); })`},P={};for(const e of $.imports||[]){const s=/^(svelte|react):(.+)$/.exec(e.from);if(s)for(const o of e.names)P[o]={adapter:s[1],path:s[2]}}let t=[],W=!1;const K=e=>{const s=t;t=[],e();const o=t;return t=s,o},U=new Set,g=(e,s)=>{for(const o of s.style||[])U.add(o);return[e,...(s.style||[]).map(ne),...(s.class||[]).filter(o=>typeof o=="string")].join(" ")},ae=new Set(Object.keys(C)),V=new Set(Object.entries(C).filter(([,e])=>typeof e.source=="string"&&e.source.startsWith("query:")).map(([e])=>e)),z=new Set,le={state:C,entities:A,actions:$.actions,consts:$.consts||{},gets:$.gets||{},effects:$.effects||[],stateKeys:ae,queryStates:V,stores:h.stores||{},usedStores:z,params:new Set($.params||[]),format:h.format},u=new Fe(le),E={locals:new Set},k=(e,s)=>{for(const o of s.class||[])typeof o!="string"&&t.push(`effect(() => el_${e}.classList.toggle(${JSON.stringify(o.name)}, !!(${u.compileExpr(o.cond,E)})));`);for(const[o,n]of Object.entries(s.on||{}))typeof n=="string"&&t.push(`el_${e}.addEventListener(${JSON.stringify(o)}, () => ${u.actionRef(n)}());`)},D=(e,s)=>{for(const o of b[e].children)Q(o,s)},G=e=>e.parts.map(s=>typeof s=="string"?JSON.stringify(s):`String(${u.compileExpr(s,E)} ?? '')`).join(" + ");function B(e,s,o,n,f){if(t.push(`const el_${e} = document.createElement('${s}');`),t.push(`el_${e}.className = ${JSON.stringify(o)};`),typeof n=="string")t.push(`el_${e}.textContent = ${JSON.stringify(n)};`);else if(n&&"kind"in n){const c=G(n),a=n.parts.some(p=>typeof p!="string");t.push(a?`effect(() => { el_${e}.textContent = ${c}; });`:`el_${e}.textContent = ${c};`)}t.push(`${f}.appendChild(el_${e});`)}function F(e,s,o){if(typeof o=="string")t.push(`el_${e}.${s} = ${JSON.stringify(o)};`);else if(o&&"kind"in o){const n=G(o),f=o.parts.some(c=>typeof c!="string");t.push(f?`effect(() => { el_${e}.${s} = ${n}; });`:`el_${e}.${s} = ${n};`)}}function Q(e,s){const o=b[e],n=o.props||{};if(P[o.type]){const c=P[o.type];t.push(`const el_${e} = document.createElement('div');`),t.push(`${s}.appendChild(el_${e});`);const a="{ "+Object.entries(o.args||{}).map(([_,d])=>typeof d=="string"&&!d.startsWith("@")&&(d in($.actions||{})||d.includes("."))?`${JSON.stringify(_)}: (...__a) => ${u.actionRef(d)}(...__a)`:`${JSON.stringify(_)}: ${se(d)}`).join(", ")+" }";if(h.format===y.Ssr){t.push(`el_${e}.innerHTML = __ssrIsland(${JSON.stringify(c.adapter)}, ${JSON.stringify(c.path)}, ${a});`);return}const p=ce[c.adapter](`el_${e}`,a,c.path);t.push(n.hydrate==="visible"?`__onVisible(el_${e}, () => ${p});`:n.hydrate==="idle"?`__onIdle(() => ${p});`:`${p};`);return}const f=oe[o.type];if(f){const[c,a]=f;t.push(`const el_${e} = document.createElement('${c}');`),t.push(`el_${e}.className = ${JSON.stringify(g(a,n))};`),o.type===l.Nav&&typeof n.label=="string"&&t.push(`el_${e}.setAttribute('aria-label', ${JSON.stringify(n.label)});`),t.push(`${s}.appendChild(el_${e});`),k(e,n),D(e,`el_${e}`);return}switch(o.type){case l.SearchField:{const c=u.bindSig(n.bind);t.push(`const el_${e} = document.createElement('input');`),t.push(`el_${e}.type = 'search';`),t.push(`el_${e}.className = ${JSON.stringify(g("search",n))};`),typeof n.placeholder=="string"&&t.push(`el_${e}.placeholder = ${JSON.stringify(n.placeholder)};`),t.push(`effect(() => { if (el_${e}.value !== ${c}.get()) el_${e}.value = ${c}.get(); });`),t.push(`el_${e}.addEventListener('input', (e) => ${c}.set(e.target.value));`),t.push(`${s}.appendChild(el_${e});`);break}case l.DataTable:{const c=u.bindSig(n.data),a=V.has(c)?`${c}.get().data`:`${c}.get()`,p=n.columns||[],_=(n.where||[]).map(ke),d=_.filter(i=>!i.dynamic).map(i=>`.filter((row) => ${i.expr})`).join(""),v=_.filter(i=>i.dynamic).map(i=>`.filter((row) => ${i.expr})`).join(""),r=o.children.map(i=>b[i]).filter(i=>i.type===l.RowAction);t.push(`const el_${e} = document.createElement('table');`),t.push(`el_${e}.className = ${JSON.stringify(g("datatable",n))};`),t.push(`const head_${e} = el_${e}.createTHead().insertRow();`);for(const i of p)t.push(`{ const th = document.createElement('th'); th.textContent = ${JSON.stringify(i)}; head_${e}.appendChild(th); }`);r.length&&t.push(`head_${e}.appendChild(document.createElement('th'));`),t.push(`const body_${e} = el_${e}.createTBody();`),t.push(`${s}.appendChild(el_${e});`),t.push(`function renderRow_${e}(row) {`),t.push(" const tr = document.createElement('tr');");for(const i of p)t.push(` { const td = document.createElement('td'); td.textContent = row[${JSON.stringify(i)}] ?? ''; tr.appendChild(td); }`);for(const i of r){const m=i.props||{},ve=m.arg!==void 0?u.compileExpr(m.arg,{locals:new Set(["row"])}):"";t.push(` { const td = document.createElement('td'); const b = document.createElement('button'); b.className = ${JSON.stringify(g("row-action",m))}; b.textContent = ${JSON.stringify(m.label)}; b.addEventListener('click', () => ${u.actionRef(m.action)}(${ve})); td.appendChild(b); tr.appendChild(td); }`)}t.push(" return tr;"),t.push("}"),t.push(`function base_${e}() { return ${a}${d}; }`),t.push("effect(() => {"),t.push(` const rows = base_${e}()${v};`),t.push(` body_${e}.replaceChildren(...rows.map(renderRow_${e}));`),t.push("});");break}case l.Form:{const c=u.bindSig(n.bind),a=C[c]?.type;if(!a||!A[a])throw new Error(`Form must bind a page-local entity draft, not "${n.bind}"`);const p=xe(A[a]),_=($.constraints||{})[a]||{};t.push(`const el_${e} = document.createElement('form');`),t.push(`el_${e}.className = ${JSON.stringify(g("form",n))};`),t.push(`{ const t = document.createElement('div'); t.className = 'form-title'; t.textContent = ${JSON.stringify("New "+a)}; el_${e}.appendChild(t); }`);const d=[];for(const r of p){const i=`f_${e}_${r.name}`;if(d.push({...r,var:i,c:_[r.name]}),r.kind===S.Enum){t.push(`const ${i} = document.createElement('select');`),t.push(`${i}.className = 'field';`);for(const m of r.options)t.push(`{ const o = document.createElement('option'); o.value = ${JSON.stringify(m)}; o.textContent = ${JSON.stringify(m)}; ${i}.appendChild(o); }`)}else r.kind===S.Bool?(t.push(`const ${i} = document.createElement('input');`),t.push(`${i}.type = 'checkbox';`),t.push(`${i}.className = 'field-check';`)):(t.push(`const ${i} = document.createElement('input');`),t.push(`${i}.type = ${JSON.stringify(r.kind===S.Email?"email":r.kind===S.Number?"number":"text")};`),t.push(`${i}.className = 'field';`),t.push(`${i}.placeholder = ${JSON.stringify(r.name)};`));if(r.kind===S.Bool)t.push(`${i}.addEventListener('change', (e) => ${c}.set({ ...${c}.get(), ${JSON.stringify(r.name)}: e.target.checked }));`);else{const m=r.kind===S.Number?"(Number(e.target.value) || 0)":"e.target.value";t.push(`${i}.addEventListener('input', (e) => ${c}.set({ ...${c}.get(), ${JSON.stringify(r.name)}: ${m} }));`)}t.push(`el_${e}.appendChild(${i});`),_[r.name]&&t.push(`const err_${i} = document.createElement('small'); err_${i}.className = 'field-error'; el_${e}.appendChild(err_${i});`)}t.push(`{ const sb = document.createElement('button'); sb.type = 'submit'; sb.className = 'submit'; sb.textContent = ${JSON.stringify(typeof n.submitLabel=="string"?n.submitLabel:"Submit")}; el_${e}.appendChild(sb); }`);const v=[];for(const r of d){if(!r.c)continue;const i=`err_${r.var}`,m=`String(__d[${JSON.stringify(r.name)}] ?? '')`;v.push(`${i}.textContent = '';`),r.c.required&&v.push(`if (!${m}.trim()) { ${i}.textContent = 'Required'; __ok = false; }`),r.c.min!=null&&v.push(r.kind===S.Number?`if (${m} !== '' && Number(${m}) < ${r.c.min}) { ${i}.textContent = 'Min ${r.c.min}'; __ok = false; }`:`if (${m} && ${m}.length < ${r.c.min}) { ${i}.textContent = 'Min ${r.c.min} characters'; __ok = false; }`),r.c.max!=null&&v.push(r.kind===S.Number?`if (${m} !== '' && Number(${m}) > ${r.c.max}) { ${i}.textContent = 'Max ${r.c.max}'; __ok = false; }`:`if (${m}.length > ${r.c.max}) { ${i}.textContent = 'Max ${r.c.max} characters'; __ok = false; }`)}v.length?t.push(`el_${e}.addEventListener('submit', (e) => { e.preventDefault(); const __d = ${c}.get(); let __ok = true; ${v.join(" ")} if (__ok) ${u.actionRef(n.submit)}(__d); });`):t.push(`el_${e}.addEventListener('submit', (e) => { e.preventDefault(); ${u.actionRef(n.submit)}(${c}.get()); });`),t.push("effect(() => {"),t.push(` const d = ${c}.get();`);for(const r of d){if(r.kind===S.Bool){t.push(` { const v = !!d[${JSON.stringify(r.name)}]; if (${r.var}.checked !== v) ${r.var}.checked = v; }`);continue}const i=r.kind===S.Enum?JSON.stringify(r.options[0]):"''";t.push(` { const v = d[${JSON.stringify(r.name)}] ?? ${i}; if (${r.var}.value !== v) ${r.var}.value = v; }`)}t.push("});"),t.push(`${s}.appendChild(el_${e});`);break}case l.Text:B(e,"p",g("text",n),n.value,s),k(e,n);break;case l.Span:B(e,"span",g("span",n),n.value,s),k(e,n);break;case l.Title:B(e,n.level||"h1",g("title",n),n.value,s),k(e,n);break;case l.Image:{t.push(`const el_${e} = document.createElement('img');`),t.push(`el_${e}.className = ${JSON.stringify(g("image",n))};`),F(e,"src",n.src),F(e,"alt",n.alt??""),t.push(`${s}.appendChild(el_${e});`);break}case l.When:{if(!n.cond)throw new Error("when without a condition");const c=u.compileExpr(n.cond,E),a=K(()=>D(e,"__p"));t.push(`function build_${e}(__p) {`);for(const p of a)t.push(" "+p);t.push("}"),t.push(`const anchor_${e} = document.createComment('when');`),t.push(`${s}.appendChild(anchor_${e});`),t.push(`let shown_${e} = [];`),t.push("effect(() => {"),t.push(` if (${c}) {`),t.push(` if (!shown_${e}.length) { const __f = document.createDocumentFragment(); build_${e}(__f); shown_${e} = [...__f.childNodes]; anchor_${e}.parentNode.insertBefore(__f, anchor_${e}); }`),t.push(` } else if (shown_${e}.length) { for (const __n of shown_${e}) __n.remove(); shown_${e} = []; }`),t.push("});");break}case l.Each:{if(!n.list||!n.as)throw new Error("each without a list or item variable");const c=u.compileExpr(n.list,E),a=n.filter?u.compileExpr(n.filter,E):"",p=K(()=>D(e,"__p"));t.push(`function buildItem_${e}(__p, ${n.as}) {`);for(const _ of p)t.push(" "+_);t.push("}"),t.push(`const anchor_${e} = document.createComment('each');`),t.push(`${s}.appendChild(anchor_${e});`),t.push(`let items_${e} = [];`),t.push("effect(() => {"),t.push(` for (const __n of items_${e}) __n.remove();`),t.push(" const __f = document.createDocumentFragment();"),t.push(` for (const ${n.as} of (${c} ?? [])) ${a?`if (${a}) `:""}buildItem_${e}(__f, ${n.as});`),t.push(` items_${e} = [...__f.childNodes];`),t.push(` anchor_${e}.parentNode.insertBefore(__f, anchor_${e});`),t.push("});");break}case l.Custom:{t.push(`const el_${e} = document.createElement('div');`),t.push(`el_${e}.className = ${JSON.stringify(g("custom",n))};`),t.push(`${s}.appendChild(el_${e});`);const c=Object.entries(n.inputs||{}).map(([p,_])=>`${JSON.stringify(p)}: ${se(_)}`).join(", "),a=Object.entries(n.on||{}).map(([p,_])=>`${JSON.stringify(p)}: (...__a) => ${u.actionRef(typeof _=="string"?_:"")}(...__a)`).join(", ");t.push(`if (typeof __custom_${n.component} === 'function') __custom_${n.component}(el_${e}, { ${c} }, { ${a} });`);break}case l.Button:{if(t.push(`const el_${e} = document.createElement('button');`),t.push(`el_${e}.className = ${JSON.stringify(g("button",n))};`),o.children&&o.children.length?D(e,`el_${e}`):n.label!==void 0&&F(e,"textContent",n.label),n.action){const c=n.arg!==void 0?u.compileExpr(n.arg,E):"";t.push(`el_${e}.addEventListener('click', () => ${u.actionRef(n.action)}(${c}));`)}k(e,n),t.push(`${s}.appendChild(el_${e});`);break}case l.Link:{t.push(`const el_${e} = document.createElement('a');`),t.push(`el_${e}.className = ${JSON.stringify(g("link",n))};`),F(e,"href",n.to??"/"),o.children&&o.children.length?D(e,`el_${e}`):n.label!==void 0&&F(e,"textContent",n.label),k(e,n),t.push(`${s}.appendChild(el_${e});`);break}case l.Slot:{W=!0,t.push("const __outlet = document.createElement('div');"),t.push("__outlet.className = 'muten-outlet';"),t.push(`${s}.appendChild(__outlet);`);break}default:throw new Error("unsupported primitive: "+o.type)}}const x=e=>String(e??"").replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">"),q=e=>x(e).replace(/"/g,"""),N=e=>typeof e=="string"?e:"";function pe(){if(h.format===y.Store||($.params||[]).length)return!1;const e=new Set([l.When,l.Each,l.Custom,l.Form,l.SearchField,l.DataTable,l.Slot]),s=["action","bind","submit","on","inputs","data"],o=["value","src","alt","label","to"];for(const n of Object.keys(b)){const f=b[n],c=f.props||{};if(P[f.type]||e.has(f.type)||s.some(a=>c[a]!==void 0)||(c.class||[]).some(a=>typeof a!="string")||o.some(a=>{const p=c[a];return!!p&&typeof p=="object"&&"kind"in p&&p.kind===Oe.Interp}))return!1}return!0}function X(e){const s=b[e],o=s.props||{},n=()=>(b[e].children||[]).map(X).join(""),f=a=>` class="${q(g(a,o))}"`,c=oe[s.type];if(c){const[a,p]=c;return`<${a}${f(p)}>${n()}</${a}>`}switch(s.type){case l.Text:return`<p${f("text")}>${x(N(o.value))}</p>`;case l.Span:return`<span${f("span")}>${x(N(o.value))}</span>`;case l.Title:{const a=o.level||"h1";return`<${a}${f("title")}>${x(N(o.value))}</${a}>`}case l.Image:return`<img${f("image")} src="${q(N(o.src))}" alt="${q(N(o.alt))}">`;case l.Link:return`<a${f("link")} href="${q(N(o.to)||"/")}">${s.children&&s.children.length?n():x(N(o.label))}</a>`;case l.Button:return`<button${f("button")}>${s.children&&s.children.length?n():x(N(o.label))}</button>`;default:return""}}const Y=h.format===y.Ssr?!1:pe(),ue=($.params||[]).map(e=>`const ${e} = (__params || {})[${JSON.stringify(e)}] ?? '';`).join(`
|
|
2
|
+
`),fe=u.genState(),me=u.genActions(),$e=Object.entries($.gets||{}).map(([e,s])=>`export const ${e} = computed(() => ${u.compileExpr(s,E)});`).join(`
|
|
3
|
+
`),he=u.genEffects();let Z=null;Y?Z=O?X(O):"":h.format!==y.Store&&O&&Q(O,"app");const _e=t.join(`
|
|
4
|
+
`),ee={};for(const e of Object.values(C))if(typeof e.source=="string"&&e.source.startsWith("query:")){const s=e.source.slice(6),o=(e.type.match(/^list<(.+)>$/)||[])[1];ee[s]=o?u.uuidFields(o):[]}const ge=[...U].map(e=>{const s=Ce(e,H);if(!s)return"";const o=`.${ne(e)}{${s}}`,n=e.indexOf(":"),f=n>0&&H.breakpoints[e.slice(0,n)];return f?`@media (min-width:${f}){${o}}`:o}).filter(Boolean).join(`
|
|
5
|
+
`),de=Object.entries(I).map(([e,s])=>`const __custom_${e} = (function () {
|
|
6
6
|
${s.replace(/^[ \t]*export[ \t]+(default[ \t]+)?/gm,"")}
|
|
7
7
|
return mount;
|
|
8
8
|
})();`).join(`
|
|
9
9
|
|
|
10
|
-
`),
|
|
11
|
-
`),
|
|
12
|
-
`),te=new Set(Object.values(
|
|
13
|
-
`),R
|
|
10
|
+
`),be=[...z].map(e=>`import * as __store_${e} from 'virtual:muten/store/${e}';`).join(`
|
|
11
|
+
`),ye=e=>/^(svelte|react):/.test(e),Se=($.imports||[]).filter(e=>!ye(e.from)).map(e=>`import { ${e.names.join(", ")} } from ${JSON.stringify(e.from)};`).join(`
|
|
12
|
+
`),te=new Set(Object.values(b).map(e=>e.props?.hydrate).filter(Boolean)),M=[];te.has("visible")&&M.push("const __onVisible = (el, cb) => { const o = new IntersectionObserver((es) => { if (es[0].isIntersecting) { o.disconnect(); cb(); } }); o.observe(el); };"),te.has("idle")&&M.push("const __onIdle = (cb) => (typeof requestIdleCallback === 'function' ? requestIdleCallback : (f) => setTimeout(f, 1))(cb);");const Ne=M.join(`
|
|
13
|
+
`),R=$.meta||{},L={...R};R.title&&!L["og:title"]&&(L["og:title"]=R.title),R.description&&!L["og:description"]&&(L["og:description"]=R.description);const w={screen:ie,tokenCss:ge,projectCss:j,data:J,sources:T,api:h.api||{},meta:L,queryUuids:ee,stateDecls:fe,paramDecls:ue,actionDecls:me,getDecls:$e,effectDecls:he,componentDecls:de,storeImports:be,storeDecls:h.storeCode||"",externImports:Se,islandImports:Ne,renderBody:_e,staticHtml:Z??"",hasSlot:W};return h.format===y.Store?we(w):h.format===y.Ssr?Ie(w):Y?h.format===y.Module?Je(w):je(w):h.format===y.Module?Te(w):De(w)}export{re as compile,Be as compileModule,Me as compileStore};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{sourceRequest as
|
|
1
|
+
import{sourceRequest as l,sourceRows as a}from"#engine/shared/source.js";const i=`// \u2500\u2500 fine-grained signals runtime (~18 lines, no dependencies) \u2500\u2500
|
|
2
2
|
let __current = null;
|
|
3
3
|
function signal(value) {
|
|
4
4
|
const subs = new Set();
|
|
@@ -11,13 +11,14 @@ import{sourceRequest as a,sourceRows as l}from"#engine/shared/source.js";const i
|
|
|
11
11
|
const run = () => { const prev = __current; __current = run; try { fn(); } finally { __current = prev; } };
|
|
12
12
|
run();
|
|
13
13
|
}
|
|
14
|
+
function computed(fn) { const s = signal(fn()); effect(() => s.set(fn())); return s; } // derived signal (store \`get\`)
|
|
14
15
|
function __has(a, b) { return Array.isArray(a) ? a.includes(b) : String(a ?? '').toLowerCase().includes(String(b ?? '').toLowerCase()); }`;function n(e){return`const __DATA = ${JSON.stringify(e.data)};
|
|
15
16
|
const __SOURCES = ${JSON.stringify(e.sources)};
|
|
16
17
|
const __API = ${JSON.stringify(e.api)};
|
|
17
18
|
const __UUIDS = ${JSON.stringify(e.queryUuids)};
|
|
18
19
|
const __DELAY = 450;
|
|
19
|
-
const __req = ${
|
|
20
|
-
const __rows = ${
|
|
20
|
+
const __req = ${l.toString()};
|
|
21
|
+
const __rows = ${a.toString()};
|
|
21
22
|
const __fill = (name, rows) => { const ids = __UUIDS[name] || []; return rows.map((r) => { const o = { ...r }; for (const f of ids) if (o[f] === null || o[f] === undefined) o[f] = __id(); return o; }); };
|
|
22
23
|
function __fetch(name) { const s = __SOURCES[name]; if (s) { const q = __req(s, __API); const init = { method: q.method, headers: { ...q.headers } }; if (q.body != null) { init.body = q.body; if (!init.headers['content-type'] && !init.headers['Content-Type']) init.headers['content-type'] = 'application/json'; } return fetch(q.url, init).then((r) => r.json()).then((j) => __fill(name, __rows(j, q.at))); } return new Promise((res) => setTimeout(() => res(__fill(name, __DATA[name] ?? [])), __DELAY)); }
|
|
23
24
|
function __write(name, method, id, body) { const s = __SOURCES[name]; const q = __req(s, __API); let url = q.url; if (id != null) { url = (url.charAt(url.length - 1) === '/' ? url.slice(0, -1) : url) + '/' + encodeURIComponent(id); } const init = { method: method, headers: { ...q.headers } }; if (body != null) { init.body = JSON.stringify(body); if (!init.headers['content-type'] && !init.headers['Content-Type']) init.headers['content-type'] = 'application/json'; } return fetch(url, init).then((r) => { if (!r.ok) throw new Error('HTTP ' + r.status); return method === 'DELETE' ? null : r.json(); }); }
|
|
@@ -40,7 +41,7 @@ export const css = ${JSON.stringify(`${e.tokenCss}
|
|
|
40
41
|
${e.projectCss}`)};
|
|
41
42
|
export const meta = ${JSON.stringify(e.meta)};
|
|
42
43
|
export function mount(app) { app.innerHTML = ${JSON.stringify(e.staticHtml)}; return app; }
|
|
43
|
-
`}function
|
|
44
|
+
`}function f(e){return`${i}
|
|
44
45
|
let __seq = 0;
|
|
45
46
|
function __id() { return 'id-' + (++__seq); }
|
|
46
47
|
const __DATA = ${JSON.stringify(e.data)};
|
|
@@ -48,6 +49,8 @@ export function mount(app) { app.innerHTML = ${JSON.stringify(e.staticHtml)}; re
|
|
|
48
49
|
const __fill = (name, rows) => { const ids = __UUIDS[name] || []; return rows.map((r) => { const o = { ...r }; for (const f of ids) if (o[f] === null || o[f] === undefined) o[f] = __id(); return o; }); };
|
|
49
50
|
function query(name) { return signal({ data: __fill(name, __DATA[name] ?? []), loading: false, error: null }); }
|
|
50
51
|
|
|
52
|
+
${e.storeDecls}
|
|
53
|
+
|
|
51
54
|
${e.paramDecls}
|
|
52
55
|
|
|
53
56
|
${e.stateDecls}
|
|
@@ -58,7 +61,7 @@ export function mount(app) { app.innerHTML = ${JSON.stringify(e.staticHtml)}; re
|
|
|
58
61
|
|
|
59
62
|
${e.renderBody}
|
|
60
63
|
return app;`}function s(e,c){const r=t=>t.replace(/&/g,"&").replace(/</g,"<").replace(/"/g,"""),o=[`<title>${r(e.title||c)}</title>`];for(const t in e)t==="title"||!e[t]||o.push(`<meta ${t.indexOf("og:")===0?"property":"name"}="${t}" content="${r(e[t])}">`);return o.join(`
|
|
61
|
-
`)}function
|
|
64
|
+
`)}function _(e){return`<!doctype html>
|
|
62
65
|
<html lang="en">
|
|
63
66
|
<head>
|
|
64
67
|
<meta charset="utf-8">
|
|
@@ -122,6 +125,9 @@ ${s(e.meta,e.screen)}
|
|
|
122
125
|
|
|
123
126
|
${n(e)}
|
|
124
127
|
|
|
128
|
+
// \u2500\u2500 app-global stores, inlined (the CLI build has no virtual modules) \u2500\u2500
|
|
129
|
+
${e.storeDecls}
|
|
130
|
+
|
|
125
131
|
// \u2500\u2500 declared state (state from the IR) \u2500\u2500
|
|
126
132
|
${e.stateDecls}
|
|
127
133
|
|
|
@@ -138,4 +144,4 @@ ${s(e.meta,e.screen)}
|
|
|
138
144
|
<\/script>
|
|
139
145
|
</body>
|
|
140
146
|
</html>
|
|
141
|
-
`}export{i as RUNTIME,g as emitHtml,h as emitModule,
|
|
147
|
+
`}export{i as RUNTIME,g as emitHtml,h as emitModule,f as emitSsr,m as emitStatic,_ as emitStaticHtml,d as emitStore};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{Nt as
|
|
1
|
+
import{Nt as o,BOp as i,Fk as l}from"#engine/shared/vocab.js";const f=e=>typeof e=="string"&&e.startsWith("@")?`${e.slice(1)}.get()`:JSON.stringify(e),m={[o.Shell]:["div","shell"],[o.Header]:["header","header"],[o.Nav]:["nav","nav"],[o.Sidebar]:["aside","sidebar"],[o.Footer]:["footer","footer"],[o.Page]:["main","page"],[o.Stack]:["div","stack"]},h={[i.Eq]:"===",[i.Neq]:"!==",[i.Lt]:"<",[i.Gt]:">",[i.Lte]:"<=",[i.Gte]:">=",[i.And]:"&&",[i.Or]:"||",[i.Add]:"+",[i.Sub]:"-",[i.Mul]:"*",[i.Div]:"/"};function b(e){let n,s,t;if(e.includes(" contains "))n="contains",[s,t]=e.split(" contains ").map(r=>r.trim());else if(e.includes("=="))n="eq",[s,t]=e.split("==").map(r=>r.trim());else throw new Error("unsupported where clause: "+e);const a=t.startsWith("@"),d=a?`${t.slice(1)}.get()`:(r=>/^-?\d+(?:\.\d+)?$/.test(r)||r==="true"||r==="false"?r:JSON.stringify(r))(t),p=JSON.stringify(s),u=n==="eq"?`row[${p}] === ${d}`:`String(row[${p}] ?? '').toLowerCase().includes(String(${d}).toLowerCase())`;return{dynamic:a,expr:u}}function y(e){const n=[];for(const[s,t]of Object.entries(e))t!=="uuid"&&(t.startsWith("enum:")?n.push({name:s,kind:l.Enum,options:t.slice(5).split("|")}):t==="email"?n.push({name:s,kind:l.Email}):t==="number"?n.push({name:s,kind:l.Number}):t==="bool"?n.push({name:s,kind:l.Bool}):n.push({name:s,kind:l.Text}));return n}export{m as CONTAINERS,h as JS_BINOP,f as customValue,y as editableFields,b as parseClause};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import{Ek as
|
|
2
|
-
`)}genActions(){const t=this.ctx.format
|
|
1
|
+
import{Ek as $,StOp as _,BOp as g,UOp as f,Fmt as h}from"#engine/shared/vocab.js";import{JS_BINOP as m}from"#engine/compile/helpers.js";class y{constructor(t){this.ctx=t}ctx;inStore(t,i,e){const r=this.ctx.stores[t];return!!r&&(r[e]||[]).includes(i)}actionRef(t){if(!t)return"";const[i,e]=t.split(".");return e&&this.inStore(i,e,"actions")?(this.ctx.usedStores.add(i),`__store_${i}.${e}`):t}bindSig(t){if(typeof t!="string")return"";if(t.startsWith("@"))return t.slice(1);const[i,e]=t.split(".");return e&&this.ctx.stores[i]?(this.ctx.usedStores.add(i),`__store_${i}.${e}`):t}uuidFields(t){const i=this.ctx.entities[t]||{};return Object.entries(i).filter(([,e])=>e==="uuid").map(([e])=>e)}bodyHasWrite(t){return t.some(i=>i.op===_.Create||i.op===_.Update||i.op===_.Delete||i.op===_.Request||i.op===_.If&&(this.bodyHasWrite(i.then||[])||this.bodyHasWrite(i.else||[])))}writeActionsSet=null;writeActions(){return this.writeActionsSet||(this.writeActionsSet=new Set(Object.entries(this.ctx.actions).filter(([,t])=>this.bodyHasWrite(t.body||[])).map(([t])=>t))),this.writeActionsSet}resolveRef(t,i){const[e,...r]=t.split("."),s=r.length?"."+r.join("."):"";if(i.locals.has(e)||this.ctx.params.has(e))return e+s;if(this.ctx.queryStates.has(e))return r[0]==="loading"||r[0]==="error"||r[0]==="data"?`${e}.get()${s}`:`${e}.get().data${s}`;if(this.ctx.stateKeys.has(e))return`${e}.get()`+s;if(this.ctx.stores[e]){const n=r[0],o=r.length>1?"."+r.slice(1).join("."):"";if(this.inStore(e,n,"state")||this.inStore(e,n,"gets"))return this.ctx.usedStores.add(e),`__store_${e}.${n}.get()${o}`}return this.ctx.consts[e]!==void 0?JSON.stringify(r.length?null:this.ctx.consts[e]):this.writeActions().has(e)&&(r[0]==="pending"||r[0]==="error")?`${r[0]==="pending"?"__pending_":"__error_"}${e}.get()`:e+s}compileExpr(t,i){if(t.kind===$.Lit)return JSON.stringify(t.value);if(t.kind===$.Ref)return this.resolveRef(t.name,i);if(t.kind===$.Call)return`${t.fn}(${t.args.map(e=>this.compileExpr(e,i)).join(", ")})`;if(t.kind===$.Obj)return`{ ${t.fields.map(e=>`${JSON.stringify(e.key)}: ${this.compileExpr(e.value,i)}`).join(", ")} }`;if(t.kind===$.Agg){const e=`(${this.resolveRef(t.list,i)} ?? [])`,r=this.compileExpr(t.body,{...i,locals:new Set([...i.locals,t.param])});if(t.op==="sort"||t.op==="sortDesc"){const n=t.op==="sortDesc"?-1:1,o=`((${t.param}) => ${r})`;return`[...${e}].sort((__a, __b) => { const __ka = ${o}(__a), __kb = ${o}(__b); return (__ka < __kb ? -1 : __ka > __kb ? 1 : 0) * ${n}; })`}const s=(n,o)=>`${e}.reduce((__a, ${t.param}) => ${o}, ${n})`;return t.op==="count"?`${e}.filter((${t.param}) => ${r}).length`:t.op==="sum"?s("0",`__a + (${r})`):t.op==="avg"?`(${s("0",`__a + (${r})`)} / (${e}.length || 1))`:t.op==="min"?`(${e}.length ? ${s("Infinity",`Math.min(__a, ${r})`)} : 0)`:`(${e}.length ? ${s("-Infinity",`Math.max(__a, ${r})`)} : 0)`}if(t.kind===$.Tern)return`(${this.compileExpr(t.cond,i)} ? ${this.compileExpr(t.then,i)} : ${this.compileExpr(t.else,i)})`;if(t.kind===$.Un){if(t.op===f.Not)return`!(${this.compileExpr(t.operand,i)})`;throw new Error("unsupported unary operator")}if(t.kind===$.Bin){const e=this.compileExpr(t.left,i),r=this.compileExpr(t.right,i);if(t.op===g.Contains)return`__has(${e}, ${r})`;const s=m[t.op];if(s)return`(${e} ${s} ${r})`;throw new Error("unsupported operator: "+t.op)}throw new Error("unsupported expression")}stmtLines(t,i,e=!1){const r=this.ctx,s=[];if(t.op===_.If){s.push(`if (${this.compileExpr(t.cond,i)}) {`);for(const n of t.then||[])for(const o of this.stmtLines(n,i,e))s.push(" "+o);if(t.else){s.push("} else {");for(const n of t.else)for(const o of this.stmtLines(n,i,e))s.push(" "+o)}return s.push("}"),s}if(t.op===_.Reset)s.push(`${t.target}.set(${JSON.stringify(r.state[t.target].initial??null)});`);else if(t.op===_.Set)s.push(`${t.target}.set(${this.compileExpr(t.arg,i)});`);else if(t.op===_.Push){const n=(r.state[t.target].type.match(/^list<(.+)>$/)||[])[1],o=n&&r.entities[n],a=p=>r.queryStates.has(t.target)?`${t.target}.set({ ...${t.target}.get(), data: [...${t.target}.get().data, ${p}] });`:`${t.target}.set([...${t.target}.get(), ${p}]);`;if(o){s.push(`{ const __it = { ...${this.compileExpr(t.arg,i)} };`);for(const p of this.uuidFields(n))s.push(` if (__it.${p} === null || __it.${p} === undefined) __it.${p} = __id(); // auto uuid`);s.push(` ${a("__it")} }`)}else s.push(`${a(this.compileExpr(t.arg,i))}`)}else if(t.op===_.Remove){const n={...i,locals:new Set([...i.locals,t.param])},o=this.compileExpr(t.pred,n);s.push(r.queryStates.has(t.target)?`${t.target}.set({ ...${t.target}.get(), data: ${t.target}.get().data.filter((${t.param}) => !(${o})) });`:`${t.target}.set(${t.target}.get().filter((${t.param}) => !(${o})));`)}else if(t.op===_.Patch){const n={...i,locals:new Set([...i.locals,t.param])},o=this.compileExpr(t.pred,n),a=this.compileExpr(t.patch,n),p=c=>`${c}.map((${t.param}) => (${o}) ? { ...${t.param}, ...${a} } : ${t.param})`;s.push(r.queryStates.has(t.target)?`${t.target}.set({ ...${t.target}.get(), data: ${p(`${t.target}.get().data`)} });`:`${t.target}.set(${p(`${t.target}.get()`)});`)}else if(t.op===_.Create||t.op===_.Update||t.op===_.Delete){const n=r.queryStates.has(t.target),o=n?`${t.target}.get().data`:`${t.target}.get()`,a=u=>n?`${t.target}.set({ ...${t.target}.get(), data: ${u} })`:`${t.target}.set(${u})`,p=n?`.catch((__e) => ${t.target}.set({ ...${t.target}.get(), error: String(__e) }))`:"",c=JSON.stringify(t.target),l=this.compileExpr(t.arg,i);e?t.op===_.Create?s.push(`{ const __i = { ...${l} }; if (__i.id == null) __i.id = __id(); const __prev = ${o}; ${a("[...__prev, __i]")}; try { const __r = await __write(${c}, 'POST', null, __i); ${a(`${o}.map((__x) => __x.id === __i.id ? __r : __x)`)}; } catch (__e) { ${a("__prev")}; throw __e; } }`):t.op===_.Update?s.push(`{ const __i = ${l}; const __prev = ${o}; ${a("__prev.map((__x) => __x.id === __i.id ? __i : __x)")}; try { const __r = await __write(${c}, 'PUT', __i.id, __i); ${a(`${o}.map((__x) => __x.id === __i.id ? __r : __x)`)}; } catch (__e) { ${a("__prev")}; throw __e; } }`):s.push(`{ const __i = ${l}; const __prev = ${o}; ${a("__prev.filter((__x) => __x.id !== __i.id)")}; try { await __write(${c}, 'DELETE', __i.id, null); } catch (__e) { ${a("__prev")}; throw __e; } }`):t.op===_.Create?s.push(`{ const __i = ${l}; __write(${c}, 'POST', null, __i).then((__r) => ${a(`[...${o}, __r]`)})${p}; }`):t.op===_.Update?s.push(`{ const __i = ${l}; __write(${c}, 'PUT', __i.id, __i).then((__r) => ${a(`${o}.map((__x) => __x.id === __i.id ? __r : __x)`)})${p}; }`):s.push(`{ const __i = ${l}; __write(${c}, 'DELETE', __i.id, null).then(() => ${a(`${o}.filter((__x) => __x.id !== __i.id)`)})${p}; }`)}else if(t.op===_.Refetch){const n=Object.entries(t.params).map(([o,a])=>`${JSON.stringify(o)}: ${this.compileExpr(a,i)}`).join(", ");s.push(`__refetch(${JSON.stringify(t.target)}, { ${n} }, ${t.target});`)}else if(t.op===_.Request){const n=typeof t.url=="string"?JSON.stringify(t.url):t.url.parts.map(a=>typeof a=="string"?JSON.stringify(a):`String(${this.compileExpr(a,i)})`).join(" + "),o=t.body?this.compileExpr(t.body,i):"null";s.push(e?`await __send(${n}, ${JSON.stringify(t.method)}, ${o});`:`__send(${n}, ${JSON.stringify(t.method)}, ${o}).catch(() => {});`)}else t.op===_.Call&&(this.ctx.usedStores.add(t.target),s.push(`__store_${t.target}.${t.method}(${t.args.map(n=>this.compileExpr(n,i)).join(", ")});`));return s}genState(){const t=this.ctx.format===h.Store?"export ":"",i=[];for(const[e,r]of Object.entries(this.ctx.state))if(typeof r.source=="string"&&r.source.startsWith("query:"))i.push(`${t}const ${e} = query(${JSON.stringify(r.source.slice(6))}); // async: ${e}.loading / .error / .data`);else{let s=r.initial??null;const n=r.type.startsWith("list<")?r.type.slice(5,-1):"",o=n?this.uuidFields(n):[];o.length&&Array.isArray(s)&&(s=s.map((a,p)=>{if(typeof a!="object"||a===null||Array.isArray(a))return a;const c={...a};for(const l of o)(c[l]===null||c[l]===void 0)&&(c[l]=`${e}-${p}`);return c})),i.push(`${t}const ${e} = signal(${JSON.stringify(s)});`)}return i.join(`
|
|
2
|
+
`)}genActions(){const t=this.ctx.format===h.Store?"export ":"",i=[],e=[];for(const[r,s]of Object.entries(this.ctx.actions)){const n=this.ctx.stateKeys.has(s.input),o={locals:new Set,input:s.input,inputIsState:n},a=n?"":s.input;if(this.writeActions().has(r)){i.push(`${t}const __pending_${r} = signal(false);`,`${t}const __error_${r} = signal(null);`),e.push(`${t}async function ${r}(${a}) {`),e.push(` __pending_${r}.set(true); __error_${r}.set(null);`),e.push(" try {");for(const p of s.body||[])for(const c of this.stmtLines(p,o,!0))e.push(" "+c);e.push(` } catch (__e) { __error_${r}.set(String(__e)); }`),e.push(` __pending_${r}.set(false);`),e.push("}")}else{e.push(`${t}function ${r}(${a}) {`);for(const p of s.body||[])for(const c of this.stmtLines(p,o))e.push(" "+c);e.push("}")}}return[...i,...e].join(`
|
|
3
3
|
`)}genEffects(){const t={locals:new Set};return this.ctx.effects.map(i=>`effect(() => {
|
|
4
4
|
${i.map(e=>this.stmtLines(e,t).map(r=>" "+r).join(`
|
|
5
5
|
`)).join(`
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{Ek as f}from"#engine/shared/vocab.js";import{toDoc as
|
|
1
|
+
import{Ek as f}from"#engine/shared/vocab.js";import{toDoc as y}from"#engine/ir/flatten.js";function h(t,n){const e=new Set;return{tree:t&&a(t,n,e),used:[...e]}}function N(t,n){const{tree:e,used:u}=h(t.tree,n),i={...t.entities},o={...t.state};for(const m of u){const l=n[m];l&&(Object.assign(i,l.entities),Object.assign(o,l.state))}return{doc:y({...t,entities:i,state:o,tree:e}),used:u}}function a(t,n,e){const u=n[t.type];if(u){e.add(t.type);const o=b(u.tree,t.args||{});return a(o,n,e)}const i={type:t.type};return t.loc&&(i.loc=t.loc),t.args&&(i.args=t.args),t.props&&(i.props=t.props),t.children&&(i.children=t.children.map(o=>a(o,n,e))),i}function b(t,n){const e={type:t.type};return t.props&&(e.props=p(t.props,n)),t.args&&(e.args=s(t.args,n)),t.children&&(e.children=t.children.map(u=>b(u,n))),e}function p(t,n){const e={...t};return t.value!==void 0&&(e.value=d(t.value,n)),t.label!==void 0&&(e.label=d(t.label,n)),t.src!==void 0&&(e.src=d(t.src,n)),t.alt!==void 0&&(e.alt=d(t.alt,n)),t.placeholder!==void 0&&(e.placeholder=d(t.placeholder,n)),t.submitLabel!==void 0&&(e.submitLabel=d(t.submitLabel,n)),t.cond!==void 0&&(e.cond=r(t.cond,n)),t.list!==void 0&&(e.list=r(t.list,n)),t.arg!==void 0&&(e.arg=r(t.arg,n)),t.action!==void 0&&(e.action=c(t.action,n)),t.submit!==void 0&&(e.submit=c(t.submit,n)),t.bind!==void 0&&(e.bind=c(t.bind,n)),t.to!==void 0&&(e.to=typeof t.to=="string"?t.to:g(t.to,n)),t.inputs!==void 0&&(e.inputs=s(t.inputs,n)),t.on!==void 0&&(e.on=s(t.on,n)),e}function d(t,n){if(typeof t=="string")return t;if("$param"in t){const e=n[t.$param];return typeof e=="number"?String(e):e}return g(t,n)}function r(t,n){return t.kind===f.Ref?{kind:f.Ref,name:c(t.name,n)}:t.kind===f.Bin?{...t,left:r(t.left,n),right:r(t.right,n)}:t.kind===f.Un?{...t,operand:r(t.operand,n)}:t.kind===f.Tern?{...t,cond:r(t.cond,n),then:r(t.then,n),else:r(t.else,n)}:t.kind===f.Call?{...t,args:t.args.map(e=>r(e,n))}:t.kind===f.Obj?{...t,fields:t.fields.map(e=>({key:e.key,value:r(e.value,n)}))}:t.kind===f.Agg?{...t,list:c(t.list,n),body:r(t.body,n)}:t}function g(t,n){return{kind:f.Interp,parts:t.parts.map(e=>typeof e=="string"?e:r(e,n))}}function s(t,n){const e={};for(const[u,i]of Object.entries(t))e[u]=A(i,n);return e}function A(t,n){return typeof t=="string"?t.startsWith("$")?c(t,n):t:typeof t=="number"?t:n[t.$param]}function c(t,n){if(!t.startsWith("$"))return t;const e=t.slice(1).split(".")[0],u=t.slice(1+e.length),i=n[e];return(typeof i=="string"?i:e)+u}export{h as compose,N as composeDoc};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
function a(t){const s={};let n=0;const
|
|
1
|
+
function a(t){const s={};let n=0;const r=e=>{const c="n"+ ++n,o={id:c,type:e.type,props:e.props||{},children:[]};return e.loc&&(o.loc=e.loc),e.args&&(o.args=e.args),s[c]=o,o.children=(e.children||[]).map(r),c};return{rootId:r(t),nodes:s}}function i(t){const{rootId:s,nodes:n}=t.tree?a(t.tree):{rootId:void 0,nodes:{}};return{screen:t.screen,entities:t.entities,state:t.state,actions:t.actions,gets:t.gets||{},effects:t.effects||[],consts:t.consts||{},constraints:t.constraints||{},params:t.params,meta:t.meta,imports:t.imports,rootId:s,nodes:n}}export{i as toDoc};
|
package/dist/engine/ir/print.js
CHANGED
|
@@ -1,26 +1,26 @@
|
|
|
1
|
-
import{Ek as $,StOp as
|
|
1
|
+
import{Ek as $,StOp as o,Nt as j,Mod as c}from"#engine/shared/vocab.js";const a=" ",B=t=>!!t&&typeof t=="object"&&"kind"in t&&t.kind===$.Interp,I=t=>typeof t=="object"&&"$param"in t,O=t=>/^[A-Za-z_][A-Za-z0-9_.]*$/.test(t);function s(t){switch(t.kind){case $.Lit:return y(t.value);case $.Ref:return t.name;case $.Un:return`${t.op} ${f(t.operand)}`;case $.Bin:return`${f(t.left)} ${t.op} ${f(t.right)}`;case $.Tern:return`${f(t.cond)} ? ${f(t.then)} : ${f(t.else)}`;case $.Call:return`${t.fn}(${t.args.map(s).join(", ")})`;case $.Obj:return`{ ${t.fields.map(r=>`${r.key}: ${s(r.value)}`).join(", ")} }`;case $.Agg:return`${t.list}.${t.op}(${t.param} => ${s(t.body)})`}}const f=t=>t.kind===$.Bin||t.kind===$.Tern?`(${s(t)})`:s(t),y=t=>typeof t=="string"?JSON.stringify(t):String(t);function l(t){return t===null?"null":Array.isArray(t)?`[${t.map(l).join(", ")}]`:typeof t=="object"?`{ ${Object.entries(t).map(([r,e])=>`${r}: ${l(e)}`).join(", ")} }`:y(t)}const S=t=>'"'+t.parts.map(r=>typeof r=="string"?r:`{${s(r)}}`).join("")+'"',N=t=>typeof t=="string"?t:t.parts.map(r=>typeof r=="string"?r:`{${s(r)}}`).join("");function P(t){return typeof t=="string"?JSON.stringify(t):I(t)?"$"+t.$param:S(t)}const R=t=>typeof t=="number"?String(t):typeof t=="string"?t:"$"+t.$param,h=t=>Object.entries(t).map(([r,e])=>`${r}: ${R(e)}`).join(", "),C=t=>typeof t=="string"?O(t)?t:JSON.stringify(t):`${O(t.name)?t.name:JSON.stringify(t.name)} when ${s(t.cond)}`;function m(t,r){switch(t.op){case o.Push:return`${t.target}.push(${s(t.arg)})`;case o.Set:return`${t.target}.set(${s(t.arg)})`;case o.Reset:return`${t.target}.reset()`;case o.Remove:return`${t.target}.remove(${t.param} => ${s(t.pred)})`;case o.Patch:return`${t.target}.patch(${t.param} => ${s(t.pred)}, ${s(t.patch)})`;case o.Create:return`${t.target}.create(${s(t.arg)})`;case o.Update:return`${t.target}.update(${s(t.arg)})`;case o.Delete:return`${t.target}.delete(${s(t.arg)})`;case o.Refetch:return`${t.target}.refetch(${Object.entries(t.params).map(([e,n])=>`${e}: ${s(n)}`).join(", ")})`;case o.Request:return`${t.method.toLowerCase()} ${typeof t.url=="string"?JSON.stringify(t.url):S(t.url)}${t.body?` body ${s(t.body)}`:""}`;case o.Call:return`${t.target}.${t.method}(${t.args.map(s).join(", ")})`;case o.If:{const e=t.then.map(i=>r+a+m(i,r+a)).join(`
|
|
2
2
|
`),n=t.else?` else {
|
|
3
|
-
${t.else.map(
|
|
3
|
+
${t.else.map(i=>r+a+m(i,r+a)).join(`
|
|
4
4
|
`)}
|
|
5
|
-
${r}}`:"";return`if ${
|
|
5
|
+
${r}}`:"";return`if ${s(t.cond)} {
|
|
6
6
|
${e}
|
|
7
|
-
${r}}${n}`}}}function d(t,r){if(t.args)return`${r}${t.type}(${h(t.args)})`;if(t.type===j.Slot)return`${r}slot`;const e=t.props||{};let n;if(t.type===j.When)n=`when ${
|
|
8
|
-
${t.children.map(
|
|
7
|
+
${r}}${n}`}}}function d(t,r){if(t.args)return`${r}${t.type}(${h(t.args)})`;if(t.type===j.Slot)return`${r}slot`;const e=t.props||{};let n;if(t.type===j.When)n=`when ${s(e.cond)}`;else if(t.type===j.Each)n=`each ${s(e.list)} as ${e.as}`;else{n=t.type;const i=e.value??e.label??e.src??e.placeholder??e.submitLabel;i!==void 0&&(n+=` ${P(i)}`),e.to!==void 0?n+=` -> ${N(e.to)}`:e.action&&(n+=` -> ${e.action}${e.arg!==void 0?`(${s(e.arg)})`:""}`),e.data&&(n+=` @${e.data}`),e.bind&&(n+=` bind ${e.bind.includes(".")?e.bind:"@"+e.bind}`),e.where&&(n+=` ${c.Where}(${e.where.join(", ")})`),e.columns&&(n+=` ${c.Columns}(${e.columns.join(", ")})`),e.style&&(n+=` ${c.Style}(${e.style.join(", ")})`),e.class&&(n+=` ${c.Class}(${e.class.map(C).join(", ")})`),e.alt!==void 0&&(n+=` ${c.Alt} ${P(e.alt)}`),e.inputs&&(n+=` ${c.Inputs}(${h(e.inputs)})`),e.on&&(n+=` ${c.On}(${h(e.on)})`),e.submit&&(n+=` ${c.Submit} ${e.submit}`)}return t.children&&t.children.length?`${r}${n} {
|
|
8
|
+
${t.children.map(i=>d(i,r+a)).join(`
|
|
9
9
|
`)}
|
|
10
|
-
${r}}`:`${r}${n}`}const
|
|
11
|
-
${r.body.map(n=>
|
|
10
|
+
${r}}`:`${r}${n}`}const E=(t,r,e)=>{const n=Object.entries(r).map(([i,A])=>{let g=`${i} ${A}`;const u=e?.[i];return u?.required&&(g+=" required"),u?.min!==void 0&&(g+=` min:${u.min}`),u?.max!==void 0&&(g+=` max:${u.max}`),g});return`entity ${t} { ${n.join(" ")} }`},k=(t,r)=>{const e=r.source?.startsWith("query:")?`query ${r.source.slice(6)}`:l(r.initial??null);return`${t} = ${e} : ${r.type}`},V=(t,r)=>`${`action ${t}${r.mutates.length?` mutates ${r.mutates.join(", ")}`:""}${r.input?` <- ${r.input}`:""}`} {
|
|
11
|
+
${r.body.map(n=>a+m(n,a)).join(`
|
|
12
12
|
`)}
|
|
13
13
|
}`,x=t=>`${t.url} -> ${t.page}${t.guard?` guard ${t.guardNeg?"not ":""}${t.guard}${t.redirect?` else ${t.redirect}`:""}`:""}`,w=(t,r)=>`part ${t}(${r.params.map(e=>`${e.name}: ${e.type}`).join(", ")}) {
|
|
14
|
-
${d(r.tree,
|
|
14
|
+
${d(r.tree,a)}
|
|
15
15
|
}`,p=(t,r)=>`${t} {
|
|
16
16
|
${r}
|
|
17
|
-
}`,b=(t,r,e)=>p(t,r.map(([n,
|
|
18
|
-
`));function M(t){const r=[],e=n=>{n&&r.push(n)};if(t.consts)for(const[n,
|
|
19
|
-
`))),t.params)for(const n of t.params)e(`param ${n}`);if(t.api&&e(b("api",Object.entries(t.api),":")),t.entities)for(const[n,
|
|
20
|
-
`))),t.store&&Object.keys(t.store).length&&e(p("store",Object.entries(t.store).map(([n,
|
|
21
|
-
`))),t.gets)for(const[n,
|
|
22
|
-
`)));if(t.parts)for(const[n,
|
|
23
|
-
`))),t.tree&&e(d(t.tree,"")),t.routes&&e(p("routes",t.routes.map(n=>
|
|
17
|
+
}`,b=(t,r,e)=>p(t,r.map(([n,i])=>`${a}${n}${e} ${l(i)}`).join(`
|
|
18
|
+
`));function M(t){const r=[],e=n=>{n&&r.push(n)};if(t.consts)for(const[n,i]of Object.entries(t.consts))e(`const ${n} = ${y(i)}`);if(e(t.screen?`screen ${t.screen}`:void 0),t.imports)for(const n of t.imports)e(`use ${n.names.join(", ")} from ${JSON.stringify(n.from)}`);if(t.meta&&e(p("meta",Object.entries(t.meta).map(([n,i])=>`${a}${n} ${JSON.stringify(i)}`).join(`
|
|
19
|
+
`))),t.params)for(const n of t.params)e(`param ${n}`);if(t.api&&e(b("api",Object.entries(t.api),":")),t.entities)for(const[n,i]of Object.entries(t.entities))e(E(n,i,t.constraints?.[n]));if(t.state&&Object.keys(t.state).length&&e(p("state",Object.entries(t.state).map(([n,i])=>a+k(n,i)).join(`
|
|
20
|
+
`))),t.store&&Object.keys(t.store).length&&e(p("store",Object.entries(t.store).map(([n,i])=>a+k(n,i)).join(`
|
|
21
|
+
`))),t.gets)for(const[n,i]of Object.entries(t.gets))e(`get ${n} = ${s(i)}`);if(t.sources&&e(b("sources",Object.entries(t.sources),":")),t.mock&&e(b("mock",Object.entries(t.mock),":")),t.actions)for(const[n,i]of Object.entries(t.actions))e(V(n,i));if(t.effects)for(const n of t.effects)e(p("effect",n.map(i=>a+m(i,a)).join(`
|
|
22
|
+
`)));if(t.parts)for(const[n,i]of Object.entries(t.parts))e(w(n,i));return t.shell&&e(p("shell",(t.shell.children||[]).map(n=>d(n,a)).join(`
|
|
23
|
+
`))),t.tree&&e(d(t.tree,"")),t.routes&&e(p("routes",t.routes.map(n=>a+x(n)).join(`
|
|
24
24
|
`))),r.join(`
|
|
25
25
|
|
|
26
26
|
`)+`
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{resolveToken as U,SUGGESTED as W,defaultTheme as B,isKnownTokenShape as G}from"#engine/style/tokens.js";import{diag as c,closest as p}from"#engine/shared/diagnostics.js";import{PRIMITIVE_NAMES as Y,ACTION_OPS as z,PRIMITIVES as H}from"#engine/lang/manifest.js";import{Nt as d,Ek as h,StOp as J}from"#engine/shared/vocab.js";const A=new Set([...Y,d.Shell]),Q=["bind","data"],C=new Set(z),O=["text","number","bool","uuid","email","string"];function m(o,a=[]){if(o.kind===h.Ref)a.push(o.name);else if(o.kind===h.Un)m(o.operand,a);else if(o.kind===h.Bin)m(o.left,a),m(o.right,a);else if(o.kind===h.Tern)m(o.cond,a),m(o.then,a),m(o.else,a);else if(o.kind===h.Call)for(const r of o.args)m(r,a);return a}function y(o,a=[]){if(o.kind===h.Call){a.push(o.fn);for(const r of o.args)y(r,a)}else o.kind===h.Un?y(o.operand,a):o.kind===h.Bin?(y(o.left,a),y(o.right,a)):o.kind===h.Tern&&(y(o.cond,a),y(o.then,a),y(o.else,a));return a}function et(o,a={}){const r=[],k=new Set(Object.keys(o.state||{})),F=new Set(a.stores||[]),D=new Set(Object.keys(o.consts||{})),q=new Set(o.params||[]),w=new Set(Object.keys(o.actions||{})),E=e=>/^(svelte|react):/.test(e),N=new Set((o.imports||[]).filter(e=>!E(e.from)).flatMap(e=>e.names)),j=new Set((o.imports||[]).filter(e=>E(e.from)).flatMap(e=>e.names)),_=o.nodes||{},S=(e,s)=>{for(const t of y(e))N.has(t)||r.push(c("unknown-function",`"${t}" is not a use'd function`,{loc:s,suggestion:p(t,[...N]),from:t}))},M=new Map;for(const[e,s]of Object.entries(o.state||{})){if(!s.source?.startsWith("query:"))continue;const t=new Set(["loading","error","data"]),n=o.entities?.[s.type];if(n){t.add("id");for(const i of Object.keys(n))t.add(i)}M.set(e,t)}const I=new Map;for(const[e,s]of Object.entries(a.storeMembers||{}))I.set(e,new Set(s));const x=(e,s,t)=>{const n=M.get(e);if(n){n.has(s)||r.push(c("unknown-member",`"${s}" is not a member of query "${e}"`,{loc:t.loc,suggestion:p(s,[...n]),from:s}));return}const i=I.get(e);i&&!i.has(s)&&r.push(c("unknown-member",`"${s}" is not a member of store "${e}"`,{loc:t.loc,suggestion:p(s,[...i]),from:s}))},T=Object.keys(o.entities||{});for(const[e,s]of Object.entries(o.state||{})){const t=s.type;if(t==="list")r.push(c("untyped-list",`state "${e}" is an untyped "list" \u2014 declare the element type, e.g. list<uuid> or list<User>`,{loc:s.loc,suggestion:"list<uuid>"}));else if(t.startsWith("list<")){const n=t.slice(5,-1);!O.includes(n)&&!T.includes(n)&&r.push(c("unknown-type",`list element "${n}" is not a known entity or scalar type`,{loc:s.loc,suggestion:p(n,[...T,...O]),from:n}))}else if(s.initial!==void 0&&s.initial!==null){const n=t==="number"?"number":t==="bool"?"boolean":["text","string","email","uuid"].includes(t)?"string":"";n&&typeof s.initial!==n&&r.push(c("type-mismatch",`state "${e}" is typed "${t}" but its initial value is a ${typeof s.initial}`,{loc:s.loc}))}}const v=e=>{const s=o.entities?.[e];return s?new Set(["id",...Object.keys(s)]):null},K=e=>{if(!e||e.kind!==h.Ref)return"";const s=o.state?.[e.name.split(".")[0]]?.type||"";return s.startsWith("list<")?s.slice(5,-1):""},L=(e,s)=>{if(typeof e=="string"&&e.startsWith("@")){const t=e.slice(1).split(".")[0];if(!k.has(t)){const n=p(t,[...k]);r.push(c("unknown-ref",`"@${t}" is not a declared state`,{loc:s.loc,suggestion:n?"@"+n:null,from:"@"+t,related:n?o.state?.[n]?.loc??null:null}))}}},R=(e,s)=>{if(!(!e||e.startsWith("$"))){if(e.includes(".")){const t=e.indexOf(".");x(e.slice(0,t),e.slice(t+1).split(".")[0],s);return}w.has(e)||r.push(c("unknown-action",`"${e}" is not a declared action`,{loc:s.loc,suggestion:p(e,[...w]),from:e}))}},b=(e,s,t)=>{S(e,s.loc);for(const n of m(e)){const i=n.indexOf("."),f=i===-1?n:n.slice(0,i);if(!(t.has(f)||k.has(f)||F.has(f)||D.has(f)||q.has(f)||w.has(f))){r.push(c("unknown-ref",`"${f}" is not a known state or item variable here`,{loc:s.loc,suggestion:p(f,[...k,...t.keys()]),from:f}));continue}if(i===-1)continue;const l=n.slice(i+1).split(".")[0],g=t.has(f)?t.get(f)||"":o.state?.[f]?.type||"",u=v(g);if(u)u.has(l)||r.push(c("unknown-member",`"${l}" is not a field of ${g} (${t.has(f)?"item":"state"} "${f}")`,{loc:s.loc,suggestion:p(l,[...u]),from:l}));else if(O.includes(g))r.push(c("unknown-member",`"${f}" is a ${g} \u2014 it has no field "${l}"`,{loc:s.loc}));else if(w.has(f)&&!k.has(f)){const $=new Set(["pending","error"]);$.has(l)||r.push(c("unknown-member",`action "${f}" exposes only .pending / .error, not "${l}"`,{loc:s.loc,suggestion:p(l,[...$]),from:l}))}else x(f,l,s)}},P=new Set,V=(e,s)=>{const t=_[e];if(!t){r.push(c("missing-node",`node ${e} does not exist`));return}if(P.has(e)){r.push(c("dup-node",`${e} is referenced twice`,{loc:t.loc}));return}if(P.add(e),!j.has(t.type))if(!A.has(t.type))t.args?r.push(c("unknown-part",`"${t.type}" is not a known part`,{loc:t.loc,suggestion:p(t.type,[...a.parts||[],...j]),from:t.type})):r.push(c("unknown-type",`"${t.type}" is not a known primitive`,{loc:t.loc,suggestion:p(t.type,[...A]),from:t.type}));else{const l=H[t.type],g=l?l.props:{};for(const[u,$]of Object.entries(g))!$.endsWith("?")&&!(u in(t.props||{}))&&r.push(c("missing-prop",`${t.type} is missing the required "${u}"`,{loc:t.loc}))}const n=t.props||{};for(const l of Q)l in n&&L(n[l],t);if(n.action&&R(n.action,t),n.submit&&R(n.submit,t),Array.isArray(n.style)){const l=a.theme||B,g=Object.keys(l.space||{}).length>0;for(const u of n.style)G(u)?g&&U(u,l)===null&&r.push(c("unknown-token",`"${u}": that step isn't in your theme scale`,{loc:t.loc,suggestion:p(u,W),from:u})):r.push(c("unknown-token",`"${u}" is not an accepted style token`,{loc:t.loc,suggestion:p(u,W),from:u}))}t.type===d.When&&n.cond&&b(n.cond,t,s),t.type===d.Each&&n.list&&b(n.list,t,s);const i=[];(t.type===d.Text||t.type===d.Title||t.type===d.Span)&&n.value&&i.push(n.value),t.type===d.Image&&(n.src&&i.push(n.src),n.alt&&i.push(n.alt)),t.type===d.Link&&n.to&&i.push(n.to),n.label&&i.push(n.label);for(const l of i)if(typeof l=="object"&&"kind"in l&&l.kind===h.Interp)for(const g of l.parts)typeof g!="string"&&b(g,t,s);const f=t.type===d.Each&&n.as?new Map([...s,[n.as,K(n.list)]]):s;for(const l of t.children||[])V(l,f)};if(o.rootId?V(o.rootId,new Map):a.kind!=="store"&&r.push(c("no-root","the doc is missing a rootId")),a.kind==="store")for(const[e,s]of Object.entries(o.gets||{})){S(s);for(const t of m(s)){const n=t.split(".")[0];k.has(n)||r.push(c("unknown-ref",`get "${e}": "${n}" is not a state of this store`,{suggestion:p(n,[...k]),from:n}))}}for(const[e,s]of Object.entries(o.actions||{})){const t=new Set(s.mutates||[]),n=i=>{if(i.op===J.If){S(i.cond);for(const f of i.then||[])n(f);for(const f of i.else||[])n(f);return}C.has(i.op)||r.push(c("unknown-op",`action "${e}" uses unknown op "${i.op}"`,{suggestion:p(i.op,[...C]),from:i.op})),"target"in i&&i.target&&!t.has(i.target)&&r.push(c("undeclared-mutation",`action "${e}" mutates "${i.target}" but only declares mutates(${[...t].join(", ")||"\u2205"})`,{suggestion:p(i.target,[...t]),from:i.target})),"arg"in i&&i.arg&&S(i.arg)};for(const i of s.body||[])n(i)}return{ok:r.length===0,diagnostics:r}}export{et as validate};
|
|
1
|
+
import{resolveToken as J,SUGGESTED as _,defaultTheme as Z,isKnownTokenShape as tt}from"#engine/style/tokens.js";import{diag as u,closest as m}from"#engine/shared/diagnostics.js";import{PRIMITIVE_NAMES as et,ACTION_OPS as nt,PRIMITIVES as st}from"#engine/lang/manifest.js";import{Nt as S,Ek as g,StOp as b,BOp as $}from"#engine/shared/vocab.js";const G=new Set([...et,S.Shell]),it=["bind","data"],K=new Set(nt),ot=new Set([b.Create,b.Update,b.Delete,b.Refetch]),W=["text","number","bool","uuid","email","string"];function O(a,d=[]){if(a.kind===g.Ref)d.push(a.name);else if(a.kind===g.Un)O(a.operand,d);else if(a.kind===g.Bin)O(a.left,d),O(a.right,d);else if(a.kind===g.Tern)O(a.cond,d),O(a.then,d),O(a.else,d);else if(a.kind===g.Call)for(const l of a.args)O(l,d);else if(a.kind===g.Obj)for(const l of a.fields)O(l.value,d);else a.kind===g.Agg&&d.push(a.list);return d}function v(a,d=[]){if(a.kind===g.Call){d.push(a.fn);for(const l of a.args)v(l,d)}else if(a.kind===g.Un)v(a.operand,d);else if(a.kind===g.Bin)v(a.left,d),v(a.right,d);else if(a.kind===g.Tern)v(a.cond,d),v(a.then,d),v(a.else,d);else if(a.kind===g.Obj)for(const l of a.fields)v(l.value,d);return d}function ut(a,d={}){const l=[],M=new Set(Object.keys(a.state||{})),j=new Set(d.stores||[]),X=new Set(Object.keys(a.consts||{})),Q=new Set(a.params||[]),x=new Set(Object.keys(a.actions||{})),C=t=>/^(svelte|react):/.test(t),N=new Set((a.imports||[]).filter(t=>!C(t.from)).flatMap(t=>t.names)),D=new Set((a.imports||[]).filter(t=>C(t.from)).flatMap(t=>t.names)),Y=a.nodes||{},z=(t,n)=>{for(const r of v(t))N.has(r)||l.push(u("unknown-function",`"${r}" is not a use'd function`,{loc:n,suggestion:m(r,[...N]),from:r}))},L=new Map;for(const[t,n]of Object.entries(a.state||{})){if(!n.source?.startsWith("query:"))continue;const r=new Set(["loading","error","data"]),e=a.entities?.[n.type];if(e){r.add("id");for(const s of Object.keys(e))r.add(s)}L.set(t,r)}const R=new Map;for(const[t,n]of Object.entries(d.storeMembers||{}))R.set(t,new Set(n));const I=(t,n,r)=>{const e=L.get(t);if(e){e.has(n)||l.push(u("unknown-member",`"${n}" is not a member of query "${t}"`,{loc:r,suggestion:m(n,[...e]),from:n}));return}const s=R.get(t);s&&!s.has(n)&&l.push(u("unknown-member",`"${n}" is not a member of store "${t}"`,{loc:r,suggestion:m(n,[...s]),from:n}))},F=Object.keys(a.entities||{});for(const[t,n]of Object.entries(a.state||{})){const r=n.type;if(n.source?.startsWith("query:")&&r!=="list"&&!r.startsWith("list<")&&l.push(u("query-not-list",`state "${t}" is a query but typed "${r}" \u2014 a query returns a LIST (the data is an array). Use \`list<${r}>\` and read it with \`each\`. (Single-record fetch isn't supported in muten.)`,{loc:n.loc,suggestion:`list<${r}>`})),r==="list")l.push(u("untyped-list",`state "${t}" is an untyped "list" \u2014 declare the element type, e.g. list<uuid> or list<User>`,{loc:n.loc,suggestion:"list<uuid>"}));else if(r.startsWith("list<")){const e=r.slice(5,-1);!W.includes(e)&&!F.includes(e)&&l.push(u("unknown-type",`list element "${e}" is not a known entity or scalar type`,{loc:n.loc,suggestion:m(e,[...F,...W]),from:e}))}else if(n.initial!==void 0&&n.initial!==null){const e=r==="number"?"number":r==="bool"?"boolean":["text","string","email","uuid"].includes(r)?"string":"";e&&typeof n.initial!==e&&l.push(u("type-mismatch",`state "${t}" is typed "${r}" but its initial value is a ${typeof n.initial}`,{loc:n.loc}))}}const P=t=>{const n=a.entities?.[t];return n?new Set(["id",...Object.keys(n)]):null},H=t=>{if(!t)return"";const n=t.kind===g.Ref?t.name:t.kind===g.Agg&&(t.op==="sort"||t.op==="sortDesc")?t.list:"";if(!n)return"";const r=a.state?.[n.split(".")[0]]?.type||"";return r.startsWith("list<")?r.slice(5,-1):""},U=(t,n)=>{if(typeof t=="string"&&t.startsWith("@")){const r=t.slice(1).split(".")[0];if(!M.has(r)&&!j.has(r)){const e=m(r,[...M]);l.push(u("unknown-ref",`"@${r}" is not a declared state`,{loc:n.loc,suggestion:e?"@"+e:null,from:"@"+r,related:e?a.state?.[e]?.loc??null:null}))}}},q=(t,n)=>{if(!(!t||t.startsWith("$"))){if(t.includes(".")){const r=t.indexOf(".");I(t.slice(0,r),t.slice(r+1).split(".")[0],n.loc??null);return}x.has(t)||l.push(u("unknown-action",`"${t}" is not a declared action`,{loc:n.loc,suggestion:m(t,[...x]),from:t}))}},A=(t,n)=>{if(t.kind===g.Lit)return typeof t.value=="number"?"number":typeof t.value=="boolean"?"bool":"text";if(t.kind===g.Ref){const[r,...e]=t.name.split("."),s=n.has(r)?n.get(r)||"":a.state?.[r]?.type||"";return e.length?e.length===1&&a.entities?.[s]?.[e[0]]||"":s}return t.kind===g.Agg?t.op==="sort"||t.op==="sortDesc"?"":"number":""},w=(t,n,r)=>{if(t.kind===g.Bin){if(t.op===$.Sub||t.op===$.Mul||t.op===$.Div)for(const e of[t.left,t.right]){const s=A(e,r);s&&s!=="number"&&l.push(u("arith-type",`arithmetic \`${t.op}\` needs numbers, but an operand is "${s}" \u2014 declare it \`: number\`.`,{loc:n}))}if([$.Eq,$.Neq,$.Lt,$.Gt,$.Lte,$.Gte].includes(t.op)&&!(t.left.kind===g.Lit&&t.left.value===null)&&!(t.right.kind===g.Lit&&t.right.value===null)){const e=f=>f==="number"||f==="bool"?f:f.startsWith("enum:")||["text","string","email","uuid"].includes(f)?"text":f,s=A(t.left,r),i=A(t.right,r);s&&i&&e(s)!==e(i)&&l.push(u("compare-type",`comparing a ${s} to a ${i} \u2014 they never match (always ${t.op===$.Neq?"true":"false"}). Likely a quoted number (\`== "1"\` vs \`== 1\`) or a type mismatch.`,{loc:n}))}w(t.left,n,r),w(t.right,n,r)}else if(t.kind===g.Un)w(t.operand,n,r);else if(t.kind===g.Tern)w(t.cond,n,r),w(t.then,n,r),w(t.else,n,r);else if(t.kind===g.Obj)for(const e of t.fields)w(e.value,n,r);else if(t.kind===g.Call)for(const e of t.args)w(e,n,r)},y=(t,n,r)=>{w(t,n,r);const e=s=>{if(s.kind===g.Agg){const i=s.list.split(".")[0],f=r.has(i)?r.get(i)||"":a.state?.[i]?.type||"",o=f.startsWith("list<")?f.slice(5,-1):"";f&&!f.startsWith("list<")&&!j.has(i)&&l.push(u("agg-not-list",`\`${s.op}(\u2026)\` needs a list, but "${s.list}" is "${f}".`,{loc:n}));const c=new Map([...r,[s.param,o]]);if(s.op!=="count"&&s.op!=="sort"&&s.op!=="sortDesc"){const p=A(s.body,c);p&&p!=="number"&&l.push(u("agg-type",`\`${s.op}(\u2026)\` reduces a NUMBER, but the body is "${p}". Use a number projection (count uses a true/false condition).`,{loc:n}))}y(s.body,n,c);return}if(s.kind===g.Bin)e(s.left),e(s.right);else if(s.kind===g.Un)e(s.operand);else if(s.kind===g.Tern)e(s.cond),e(s.then),e(s.else);else if(s.kind===g.Obj)for(const i of s.fields)e(i.value);else if(s.kind===g.Call)for(const i of s.args)e(i)};e(t),z(t,n);for(const s of O(t)){const i=s.indexOf("."),f=i===-1?s:s.slice(0,i);if(!(r.has(f)||M.has(f)||j.has(f)||X.has(f)||Q.has(f)||x.has(f))){const h=m(f,[...M,...r.keys()]);l.push(u("unknown-ref",`"${f}" is not a known state or item variable here${h?"":` \u2014 if it's a text/enum value, quote it: "${f}"`}`,{loc:n,suggestion:h,from:f}));continue}if(i===-1)continue;const o=s.slice(i+1).split(".")[0],c=r.has(f)?r.get(f)||"":a.state?.[f]?.type||"",p=P(c);if(p)if(!p.has(o))l.push(u("unknown-member",`"${o}" is not a field of ${c} (${r.has(f)?"item":"state"} "${f}")`,{loc:n,suggestion:m(o,[...p]),from:o}));else{const h=s.slice(i+1).split(".")[1],k=a.entities?.[c]?.[o]||"";h&&!a.entities?.[k]&&l.push(u("unknown-member",`"${o}" is ${k.startsWith("enum:")?"an enum":`a ${k}`} \u2014 it has no field "${h}"`,{loc:n,from:h}))}else if(W.includes(c))l.push(u("unknown-member",`"${f}" is a ${c} \u2014 it has no field "${o}"`,{loc:n}));else if(x.has(f)&&!M.has(f)){const h=new Set(["pending","error"]);h.has(o)||l.push(u("unknown-member",`action "${f}" exposes only .pending / .error, not "${o}"`,{loc:n,suggestion:m(o,[...h]),from:o}))}else if(c.startsWith("list<")){const h=!!a.state?.[f]?.source,k=h?new Set(["length","data","loading","error"]):new Set(["length"]);k.has(o)||l.push(u("unknown-member",`"${f}" is a list \u2014 no member "${o}" (lists expose ${h?".length / .data / .loading / .error":"only .length"}; use \`each ${f} as item\` to read an element)`,{loc:n,suggestion:m(o,[...k]),from:o}))}else I(f,o,n)}},V=new Set,B=(t,n,r=!1)=>{const e=Y[t];if(!e){l.push(u("missing-node",`node ${t} does not exist`));return}if(V.has(t)){l.push(u("dup-node",`${t} is referenced twice`,{loc:e.loc}));return}if(V.add(t),e.type==="RowAction"&&!r&&l.push(u("rowaction-context","RowAction only works inside a DataTable (it renders a button per row). Use Button for a standalone action.",{loc:e.loc})),!D.has(e.type))if(!G.has(e.type))e.args?l.push(u("unknown-part",`"${e.type}" is not a known part`,{loc:e.loc,suggestion:m(e.type,[...d.parts||[],...D]),from:e.type})):l.push(u("unknown-type",`"${e.type}" is not a known primitive`,{loc:e.loc,suggestion:m(e.type,[...G]),from:e.type}));else{const o=st[e.type],c=o?o.props:{};for(const[p,h]of Object.entries(c))!h.endsWith("?")&&!(p in(e.props||{}))&&l.push(u("missing-prop",`${e.type} is missing the required "${p}"`,{loc:e.loc}))}const s=e.props||{};for(const o of it)o in s&&U(s[o],e);if(s.action&&(q(s.action,e),typeof s.action=="string"&&!s.action.includes(".")&&a.actions?.[s.action]?.input&&s.arg===void 0&&l.push(u("action-arity",`action "${s.action}" takes an argument (it reads "${a.actions[s.action].input}") \u2014 pass it, e.g. \`-> ${s.action}(row)\``,{loc:e.loc}))),s.submit&&q(s.submit,e),Array.isArray(s.class))for(const o of s.class)typeof o=="string"&&o.includes("{")&&l.push(u("class-interp",`class() does not interpolate "{\u2026}": "${o}" would ship the braces literally. For a dynamic class use \`class(name when cond)\` (e.g. \`class(stage-applied when status == "applied")\`).`,{loc:e.loc,from:o}));if(Array.isArray(s.where))for(const o of s.where)typeof o=="string"&&o.trim()&&(/(?:==|\bcontains\b)/.test(o)?/\b(?:and|or)\b/.test(o)&&l.push(u("unsupported-where",`where clause "${o.trim()}" \u2014 combine conditions with a COMMA, not \`and\`/\`or\`: where(role == admin, name contains @q).`,{loc:e.loc,from:o.trim()})):l.push(u("unsupported-where",`where clause "${o.trim()}" \u2014 where() supports only \`==\` and \`contains\` (e.g. where(role == admin, name contains @q)).`,{loc:e.loc,from:o.trim()})));if(e.type==="Form"){const o=String(s.bind??"").replace(/^@/,""),c=o.split(".")[0],p=c?a.state?.[c]?.type:void 0;p===void 0?l.push(u("form-bind",`Form must bind a page-local draft (a state typed as an entity)${o?`, but "${o}" is not a state on this page`:""}${o.includes(".")?" \u2014 a Form cannot bind a store field; declare a local `draft = {} : Entity` and submit the store action":""}.`,{loc:e.loc})):a.entities?.[p]||l.push(u("form-bind",`Form must bind a state typed as an entity (a draft): "${c}" is "${p}". Declare \`entity X { \u2026 }\` + \`${c} = {} : X\`, or use SearchField for a single text input.`,{loc:e.loc}))}if(e.type==="DataTable"){const o=String(s.data??"").replace(/^@/,""),c=a.state?.[o]?.type||"",p=c.startsWith("list<")?c.slice(5,-1):"",h=P(p);if(h){for(const k of s.columns||[])typeof k=="string"&&!h.has(k)&&l.push(u("unknown-column",`column "${k}" is not a field of ${p}`,{loc:e.loc,suggestion:m(k,[...h]),from:k}));for(const k of s.where||[])if(typeof k=="string"){const E=k.trim().split(/\s+/)[0];E&&!h.has(E)&&l.push(u("unknown-where-field",`where "${k.trim()}": "${E}" is not a field of ${p}`,{loc:e.loc,suggestion:m(E,[...h]),from:E}));for(const T of k.matchAll(/@(\w+)/g))M.has(T[1])||l.push(u("unknown-ref",`where "${k.trim()}": "@${T[1]}" is not a declared state`,{loc:e.loc,from:"@"+T[1]}))}}}if(e.type==="SearchField"){const o=String(s.bind??"").replace(/^@/,"").split(".")[0],c=o?a.state?.[o]?.type:void 0;c!==void 0&&!["text","string","email"].includes(c)&&l.push(u("bind-type",`SearchField binds a single text value, but "${o}" is "${c}" \u2014 bind a text state (e.g. \`q = "" : text\`).`,{loc:e.loc}))}if(e.type==="Custom"){for(const o of Object.values(s.inputs||{}))typeof o=="string"&&U(o,e);for(const o of Object.values(s.on||{}))typeof o=="string"&&q(o,e)}if(Array.isArray(s.style)){const o=d.theme||Z,c=Object.keys(o.space||{}).length>0;for(const p of s.style)tt(p)?c&&J(p,o)===null&&l.push(u("unknown-token",`"${p}": that step isn't in your theme scale`,{loc:e.loc,suggestion:m(p,_),from:p})):l.push(u("unknown-token",`"${p}" is not an accepted style token`,{loc:e.loc,suggestion:m(p,_),from:p}))}e.type===S.When&&s.cond&&y(s.cond,e.loc??null,n),e.type===S.Each&&s.list&&y(s.list,e.loc??null,n),s.arg&&typeof s.arg=="object"&&"kind"in s.arg&&y(s.arg,e.loc??null,n);const i=[];(e.type===S.Text||e.type===S.Title||e.type===S.Span)&&s.value&&i.push(s.value),e.type===S.Image&&(s.src&&i.push(s.src),s.alt&&i.push(s.alt)),e.type===S.Link&&s.to&&i.push(s.to),s.label&&i.push(s.label);for(const o of i)if(typeof o=="object"&&"kind"in o&&o.kind===g.Interp)for(const c of o.parts)typeof c!="string"&&y(c,e.loc??null,n);let f=n;if(e.type===S.Each&&s.as)f=new Map([...n,[s.as,H(s.list)]]),s.filter&&y(s.filter,e.loc??null,f);else if(e.type==="DataTable"){const o=String(s.data||"").replace(/^@/,""),c=a.state?.[o]?.type||"";f=new Map([...n,["row",c.startsWith("list<")?c.slice(5,-1):""]])}for(const o of e.children||[])B(o,f,e.type==="DataTable")};if(a.rootId?B(a.rootId,new Map):d.kind!=="store"&&l.push(u("no-root","the doc is missing a rootId")),d.kind!=="store"){for(const t of Object.keys(a.gets||{}))l.push(u("store-only",`\`get ${t}\` is only valid in a .store \u2014 a page has no derived values. Compute it inline (\`{\u2026}\`) or keep a state cell.`));(a.effects||[]).length&&l.push(u("store-only","`effect { }` is only valid in a .store \u2014 a page reacts through `when`/`each`, not effects."))}if(d.kind==="store"){for(const n of Object.values(a.gets||{}))y(n,null,new Map);const t=n=>{if(n.op===b.If){y(n.cond,null,new Map);for(const r of n.then||[])t(r);for(const r of n.else||[])t(r);return}if("target"in n&&n.target&&!M.has(n.target)&&l.push(u("undeclared-mutation",`effect mutates "${n.target}" \u2014 not a state of this store`,{suggestion:m(n.target,[...M]),from:n.target})),n.op===b.Remove)y(n.pred,null,new Map([[n.param,""]]));else if(n.op===b.Patch){const r=new Map([[n.param,""]]);y(n.pred,null,r),y(n.patch,null,r)}else if(n.op===b.Refetch)for(const r of Object.values(n.params))y(r,null,new Map);else n.op===b.Request?n.body&&y(n.body,null,new Map):"arg"in n&&n.arg&&y(n.arg,null,new Map)};for(const n of a.effects||[])for(const r of n)t(r)}for(const[t,n]of Object.entries(a.actions||{})){const r=new Set(n.mutates||[]),e=new Map(n.input?[[n.input,""]]:[]),s=i=>{if(i.op===b.If){y(i.cond,null,e);for(const f of i.then||[])s(f);for(const f of i.else||[])s(f);return}if(i.op===b.Call){j.has(i.target)?R.get(i.target)?.has(i.method)||l.push(u("unknown-action",`store "${i.target}" has no member "${i.method}".`,{suggestion:m(i.method,[...R.get(i.target)||[]]),from:i.method})):l.push(u("unknown-action",`"${i.target}.${i.method}(\u2026)": "${i.target}" is not a store. A page action mutates LOCAL state with push/set/patch/\u2026; only a STORE action can be called like this.`,{suggestion:m(i.target,[...j]),from:i.target}));for(const f of i.args)y(f,null,e);return}if(K.has(i.op)||l.push(u("unknown-op",`action "${t}" uses unknown op "${i.op}"`,{suggestion:m(i.op,[...K]),from:i.op})),"target"in i&&i.target&&!r.has(i.target)&&l.push(u("undeclared-mutation",`action "${t}" mutates "${i.target}" but only declares mutates(${[...r].join(", ")||"\u2205"})`,{suggestion:m(i.target,[...r]),from:i.target})),ot.has(i.op)&&"target"in i&&i.target){const f=a.state?.[i.target];f&&!f.source&&l.push(u("missing-source",`action "${t}": "${i.target}.${i.op}(\u2026)" needs a query/source-backed list, but "${i.target}" is local (no source). Use \`= query <name>\` + a \`sources\` entry, or local ops (push/set/reset/remove).`,{from:i.target}))}if((i.op===b.Push||i.op===b.Create||i.op===b.Set)&&i.arg){const f=a.state?.[i.target]?.type||"",o=i.op===b.Set?f:f.startsWith("list<")?f.slice(5,-1):"";if(o&&a.entities?.[o]){const c=i.arg;if(c.kind===g.Obj){const p=a.entities[o];for(const h of c.fields)h.key in p||l.push(u("unknown-field",`action "${t}": "${h.key}" is not a field of ${o}`,{suggestion:m(h.key,Object.keys(p)),from:h.key}))}else{const p=c.kind===g.Lit?typeof c.value=="number"?"number":typeof c.value=="boolean"?"bool":"text":c.kind===g.Ref&&!c.name.includes(".")&&a.state?.[c.name]?.type||"";p&&p!==o&&l.push(u(i.op===b.Set?"set-type":"push-type",i.op===b.Set?`action "${t}": setting "${i.target}" (a ${o} draft) to a ${p} \u2014 assign a ${o} (a draft/state of that entity).`:`action "${t}": pushing a ${p} into list<${o}> "${i.target}" \u2014 push a ${o} (a draft/state of that entity).`))}}}if(i.op===b.Remove)y(i.pred,null,new Map([...e,[i.param,""]]));else if(i.op===b.Patch){const f=a.state?.[i.target]?.type||"",o=f.startsWith("list<")?f.slice(5,-1):"",c=new Map([...e,[i.param,o]]);if(y(i.pred,null,c),y(i.patch,null,c),o&&a.entities?.[o]&&i.patch.kind===g.Obj){const p=a.entities[o];for(const h of i.patch.fields)h.key in p||l.push(u("unknown-field",`action "${t}": "${h.key}" is not a field of ${o}`,{suggestion:m(h.key,Object.keys(p)),from:h.key}))}}else if(i.op===b.Refetch)for(const f of Object.values(i.params))y(f,null,e);else i.op===b.Request?i.body&&y(i.body,null,e):"arg"in i&&i.arg&&y(i.arg,null,e)};for(const i of n.body||[])s(i)}return{ok:l.length===0,diagnostics:l}}export{ut as validate};
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{ParseError as
|
|
2
|
-
`&&this.lineStarts.push(n+1)}peek(){return this.toks[this.pos]}at(t,n){const
|
|
1
|
+
import{ParseError as f}from"#engine/shared/diagnostics.js";import{tokenize as k}from"#engine/lang/lexer.js";import{Tk as e,Pn as i,Kw as o,BOp as h,UOp as P,Ek as a,AGG_OPS as x,SORT_OPS as m}from"#engine/shared/vocab.js";const v=[[e.Eq,void 0,h.Eq],[e.Neq,void 0,h.Neq],[e.Lte,void 0,h.Lte],[e.Gte,void 0,h.Gte],[e.Punct,i.Lt,h.Lt],[e.Punct,i.Gt,h.Gt],[e.Ident,o.Contains,h.Contains]],c=new Map([[o.True,!0],[o.False,!1],[o.Null,null]]);class p{toks;pos=0;lineStarts=[0];constructor(t){this.toks=k(t);for(let n=0;n<t.length;n++)t[n]===`
|
|
2
|
+
`&&this.lineStarts.push(n+1)}peek(){return this.toks[this.pos]}at(t,n){const r=this.peek();return r.t===t&&(n===void 0||r.v===n)}next(){return this.toks[this.pos++]}eat(t,n){if(!this.at(t,n)){const r=this.peek();throw new f(`expected ${t}${n?' "'+n+'"':""}, got ${r.t} "${r.v}"`,this.locOf(r.pos))}return this.next()}locOf(t){let n=0,r=this.lineStarts.length-1;for(;n<r;){const s=n+r+1>>1;this.lineStarts[s]<=t?n=s:r=s-1}return{line:n+1,col:t-this.lineStarts[n]+1}}parseExpr(){return this.ternary()}ternary(){const t=this.or();if(!this.at(e.Punct,i.Question))return t;this.next();const n=this.ternary();return this.eat(e.Punct,i.Colon),{kind:a.Tern,cond:t,then:n,else:this.ternary()}}or(){let t=this.and();for(;this.at(e.Ident,o.Or);)this.next(),t={kind:a.Bin,op:h.Or,left:t,right:this.and()};return t}and(){let t=this.cmp();for(;this.at(e.Ident,o.And);)this.next(),t={kind:a.Bin,op:h.And,left:t,right:this.cmp()};return t}comparison(){for(const[t,n,r]of v)if(this.at(t,n))return r;return null}cmp(){let t=this.add();for(let n=this.comparison();n;n=this.comparison())this.next(),t={kind:a.Bin,op:n,left:t,right:this.add()};return t}add(){let t=this.mul();for(;this.at(e.Punct,i.Plus)||this.at(e.Punct,i.Dash);){const n=this.peek().v===i.Plus?h.Add:h.Sub;this.next(),t={kind:a.Bin,op:n,left:t,right:this.mul()}}return t}mul(){let t=this.unary();for(;this.at(e.Punct,i.Star)||this.at(e.Punct,i.Slash);){const n=this.peek().v===i.Star?h.Mul:h.Div;this.next(),t={kind:a.Bin,op:n,left:t,right:this.unary()}}return t}unary(){return this.at(e.Ident,o.Not)?(this.next(),{kind:a.Un,op:P.Not,operand:this.unary()}):this.primary()}primary(){if(this.at(e.Punct,i.ParenL)){this.next();const r=this.ternary();return this.eat(e.Punct,i.ParenR),r}if(this.at(e.Punct,i.BraceL)){this.next();const r=[];for(;!this.at(e.Punct,i.BraceR);){const s=this.eat(e.Ident).v;this.eat(e.Punct,i.Colon),r.push({key:s,value:this.ternary()}),this.at(e.Punct,i.Comma)&&this.next()}return this.eat(e.Punct,i.BraceR),{kind:a.Obj,fields:r}}if(this.at(e.String))return{kind:a.Lit,value:this.next().v};if(this.at(e.Number))return{kind:a.Lit,value:Number(this.next().v)};let t=this.at(e.Param)?"$"+this.next().v:this.eat(e.Ident).v;const n=c.get(t);if(n!==void 0)return{kind:a.Lit,value:n};for(;this.at(e.Punct,i.Dot);)this.next(),t+="."+this.eat(e.Ident).v;if(this.at(e.Punct,i.ParenL)){const r=t.lastIndexOf("."),s=r===-1?"":t.slice(r+1);if(x.has(s)||m.has(s)){this.next();const l=this.eat(e.Ident).v;this.eat(e.FatArrow);const d=this.ternary();return this.eat(e.Punct,i.ParenR),{kind:a.Agg,op:s,list:t.slice(0,r),param:l,body:d}}this.next();const u=[];for(;!this.at(e.Punct,i.ParenR);)u.push(this.ternary()),this.at(e.Punct,i.Comma)&&this.next();return this.eat(e.Punct,i.ParenR),{kind:a.Call,fn:t,args:u}}return{kind:a.Ref,name:t}}parseInterpolation(t){if(!t.includes("{"))return t;const n=[];let r=0;for(;r<t.length;){const s=t.indexOf("{",r);if(s<0){n.push(t.slice(r));break}s>r&&n.push(t.slice(r,s));const u=t.indexOf("}",s);if(u<0){n.push(t.slice(s));break}n.push(new p(t.slice(s+1,u)).parseExpr()),r=u+1}return{kind:a.Interp,parts:n}}parseScalar(){if(this.at(e.String))return this.next().v;if(this.at(e.Number))return Number(this.next().v);const t=this.eat(e.Ident).v,n=c.get(t);return n!==void 0?n:t}parseValue(){return this.at(e.Punct,i.BrackL)?this.parseArray():this.at(e.Punct,i.BraceL)?this.parseObject():this.parseScalar()}parseArray(){this.eat(e.Punct,i.BrackL);const t=[];for(;!this.at(e.Punct,i.BrackR);)t.push(this.parseValue()),this.at(e.Punct,i.Comma)&&this.next();return this.eat(e.Punct,i.BrackR),t}parseObject(){this.eat(e.Punct,i.BraceL);const t={};for(;!this.at(e.Punct,i.BraceR);){const n=this.at(e.String)?this.next().v:this.eat(e.Ident).v;this.eat(e.Punct,i.Colon),t[n]=this.parseValue(),this.at(e.Punct,i.Comma)&&this.next()}return this.eat(e.Punct,i.BraceR),t}}export{p as Grammar};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{Tk as s,Pn as c}from"#engine/shared/vocab.js";import{ParseError as a}from"#engine/shared/diagnostics.js";const p=Object.values(c).join(""),x=[["->",s.Arrow],["<-",s.LArrow],["==",s.Eq],["=>",s.FatArrow],["!=",s.Neq],["<=",s.Lte],[">=",s.Gte]],g=
|
|
2
|
-
`,h=
|
|
3
|
-
`?(
|
|
4
|
-
`;)this.index++}scanString(n){const{source:
|
|
1
|
+
import{Tk as s,Pn as c}from"#engine/shared/vocab.js";import{ParseError as a}from"#engine/shared/diagnostics.js";const p=Object.values(c).join(""),x=[["->",s.Arrow],["<-",s.LArrow],["==",s.Eq],["=>",s.FatArrow],["!=",s.Neq],["<=",s.Lte],[">=",s.Gte]],g=t=>t===" "||t===" "||t==="\r"||t===`
|
|
2
|
+
`,h=t=>t>="0"&&t<="9",d=t=>t>="a"&&t<="z"||t>="A"&&t<="Z"||t==="_",l=t=>d(t)||h(t);function f(t,n){let e=1,i=1;for(let o=0;o<n&&o<t.length;o++)t[o]===`
|
|
3
|
+
`?(e++,i=1):i++;return{line:e,col:i}}class m{constructor(n){this.source=n}source;index=0;tokens=[];tokenize(){const{source:n}=this;for(;this.index<n.length;){const e=this.index,i=n[this.index];if(g(i)){this.index++;continue}if(i==="#"){this.skipComment();continue}if(i==='"'){this.scanString(e);continue}if(i==="@"){this.scanSigil(e,s.Ref,"@");continue}if(i==="$"){this.scanSigil(e,s.Param,"");continue}if(!this.scanOperator(e)){if(h(i)||i===c.Dash&&h(n[this.index+1])){this.scanNumber(e);continue}if(p.includes(i)){this.push(s.Punct,i,e),this.index++;continue}if(d(i)){this.scanWord(e);continue}throw new a(`unexpected character ${JSON.stringify(i)}`,f(n,this.index))}}return this.push(s.Eof,"",this.index),this.tokens}push(n,e,i){this.tokens.push({t:n,v:e,pos:i})}skipComment(){for(;this.index<this.source.length&&this.source[this.index]!==`
|
|
4
|
+
`;)this.index++}scanString(n){const{source:e}=this;let i=this.index+1,o="",r=0;for(;i<e.length&&(e[i]!=='"'||r>0);)e[i]==="{"?r++:e[i]==="}"&&r--,o+=e[i],i++;this.push(s.String,o,n),this.index=i+1}scanSigil(n,e,i){const{source:o}=this;let r=this.index+1,u="";for(;r<o.length&&l(o[r]);)u+=o[r],r++;this.push(e,i+u,n),this.index=r}scanOperator(n){const e=this.source.slice(this.index,this.index+2),i=x.find(([o])=>o===e);return i?(this.push(i[1],e,n),this.index+=2,!0):!1}scanNumber(n){const{source:e}=this;let i=this.index+(e[this.index]===c.Dash?1:0);for(;i<e.length&&h(e[i]);)i++;if(e[i]===c.Dot)for(i++;i<e.length&&h(e[i]);)i++;this.push(s.Number,e.slice(this.index,i),n),this.index=i}scanWord(n){const{source:e}=this;let i=this.index;for(;i<e.length&&l(e[i]);)i++;this.push(s.Ident,e.slice(this.index,i),n),this.index=i}}function k(t){return new m(t).tokenize()}export{k as tokenize};
|
|
@@ -12,4 +12,4 @@ import{SUGGESTED as e,resolveToken as t}from"#engine/style/tokens.js";const a={S
|
|
|
12
12
|
$0
|
|
13
13
|
}`},Text:{string:"value",props:{value:"text",style:"tokens?"},children:!1,interp:!0,doc:'Paragraph text (<p>). Interpolates state reactively: `Text "Hi, {user.name}"`.',snippet:'Text "$1"'},Title:{string:"value",props:{value:"text",style:"tokens?"},children:!1,interp:!0,doc:'Heading. Level via keyword: `Title "Hi" h2` \u2192 <h2> (h1\u2026h6; default h1). Prefer one h1 per page. Interpolates state.',snippet:'Title "$1"'},Span:{string:"value",props:{value:"text",style:"tokens?"},children:!1,interp:!0,doc:'Inline text (<span>). Interpolates state: `Span "{cart.total}"`.',snippet:'Span "$1"'},Image:{string:"src",props:{src:"text",alt:"text",style:"tokens?"},children:!1,interp:!0,doc:'Image (<img>). `alt` is required (a11y/SEO): `Image "{p.image}" alt "{p.title}"`. Use alt "" for decorative images.',snippet:'Image "{${1:item.image}}" alt "${2:description}"'},SearchField:{string:"placeholder",props:{bind:"state",placeholder:"text?"},children:!1,doc:"Search input two-way bound to a text state.",snippet:'SearchField bind @${1:search} "${2:Search by name}"'},DataTable:{props:{data:"state",where:"clauses?",columns:"fields",style:"tokens?"},children:!0,doc:"Reactive table over a list/query. Static `where` filters are pushed to the query; dynamic ones stay reactive.",snippet:"DataTable @${1:items}\n columns(${2:name})"},RowAction:{string:"label",props:{label:"text",action:"action",arg:"expr?"},children:!1,doc:'A button rendered in each DataTable row: `RowAction "Delete" -> deleteItem(row.id)`.',snippet:'RowAction "${1:Delete}" -> ${2:action}(row.id)'},Button:{string:"label",props:{label:"text?",action:"action?",arg:"expr?",style:"tokens?"},children:!0,interp:!0,doc:'Clickable button \u2192 runs an action. Label interpolates (`Button "{x}"`) OR use `{ }` children for a clickable card. `-> action(arg)`; arg may be a ref or a literal.',snippet:'Button "${1:label}" -> ${2:action}($3)'},Form:{string:"submitLabel",props:{bind:"state",submit:"action",submitLabel:"text?"},children:!1,doc:"Auto-form: one field per entity field, two-way bound to a draft state.",snippet:'Form bind @${1:draft} submit ${2:createItem} "${3:Save}"'},Link:{string:"label",props:{label:"text?",to:"route",style:"tokens?"},children:!0,interp:!0,doc:'Navigation link: `Link "Catalog" -> /catalog`. Label interpolates, OR use `{ }` children for a clickable card that navigates. Client-side (no full reload).',snippet:'Link "${1:label}" -> /${2:route}'},slot:{props:{},children:!1,doc:"The outlet in a `shell { }` where the active route\u2019s page mounts.",snippet:"slot"},When:{props:{cond:"expr"},children:!0,control:!0,doc:"Conditional render: `when <expr> { ... }`. Mounts/unmounts reactively.",snippet:`when \${1:cond} {
|
|
14
14
|
$0
|
|
15
|
-
}`},Each:{props:{list:"expr",as:"ident"},children:!0,control:!0,doc:"List render: `each <list> as <item> { ... }`. The item is a scope variable in the template.",snippet:"each ${1:items} as ${2:item} {\n $0\n}"},Custom:{props:{component:"name",inputs:"map?",on:"map?"},children:!1,doc:"Escape hatch (\xA77): mount a host component from `src/components/<Name>.js`. Opaque to the IR; connected via inputs/on.",snippet:"Custom ${1:Name} inputs(${2:prop}: ${3}) on(${4:event}: ${5:action})"}},r=["bind","submit","where","columns","style","class","alt","inputs","on"],o={bind:"Two-way bind to a @state, e.g. `bind @search`.",submit:"Action to run on form submit, e.g. `submit createUser`.",where:"Filter clauses: `where(role == admin, name contains @q)`.",columns:"Columns to show: `columns(name, email, role)`.",style:"Layout & typography tokens (Muten builds, doesn\u2019t skin): `style(row, gap.md, text.lg)`.",class:'Raw CSS class(es) for LOOK \u2014 your CSS or a third-party like Tailwind: `class(card)` or `class("flex gap-4")`. Muten stays agnostic about appearance.',alt:'Required accessible/SEO text for an Image: `alt "{p.title}"`. Use "" for decorative images.',inputs:"Custom component inputs: `inputs(data: @sales)`.",on:"Custom component events wired to actions: `on(select: pick)`."},n=["screen","entity","state","store","const","theme","get","effect","action","mutates","mock","sources","api","meta","routes","shell","guard","else","part","param","query","post","put","delete","body","if","when","each","as","and","or","not","contains","use"],i={screen:"Declares the screen name: `screen users_dashboard`.",entity:"Declares a data shape + validation: `entity User { name text required email email required password text min:8 }` (implicit uuid id). Constraints: `required`, `min:N`, `max:N`.",state:'Declares reactive state: `state { search = "" : text users = query listUsers : list<User> }`.',store:"App-GLOBAL reactive state (shared across pages, no prop drilling): `store { cart = [] : list<number> }`. Referenced by name like local state.",const:"A compile-time IMMUTABLE scalar, inlined (never reactive): `const TAX = 0.21`. Scalars only \u2014 structured config uses a block (e.g. theme).",theme:'The project theme block (theme.muten): `theme { space { md "16px" } breakpoints { md "768px" } }`. Supplies the token SCALE; the engine owns only the vocabulary. The reset/base CSS lives in your stylesheet.',get:"A `.store` derived/memoized value (getter): `get total = items.length`. Read as `domain.total`, recomputes when deps change.",effect:"A `.store` reactive side-effect (Angular-style): `effect { ... }`. Re-runs automatically when the store state it reads changes.",action:"Declares a mutation: `action delete mutates users <- id { users.remove(u => u.id == id) }`.",mutates:"Lists the state an action may mutate \u2014 the linter enforces it.",mock:'Inline mock data for queries: `mock { listUsers: [ { name: "Ana", role: admin } ] }`.',sources:'Real data sources for queries: `sources { listChars: { url: "https://api...", at: "results" } }`.',api:'App-wide backend config in app.muten: `api { base: "https://\u2026" headers: { \u2026 } }`. A relative `sources` url is joined to `base`; headers merge (the source wins).',meta:'Page <head> metadata: `meta { title "\u2026" description "\u2026" }` \u2192 `<title>` + `<meta>` tags (og:* auto-derived). Applied on navigation.',post:'Explicit non-REST request in an action: `post "shop:/orders" body item` (escape hatch when CRUD ops do not fit).',put:'Explicit non-REST request in an action: `put "shop:/orders/{id}" body item`.',body:'The JSON body of an explicit `post`/`put` request: `post "shop:/x" body item`.',routes:"App root (app.muten): maps URLs to pages, `routes { /url -> page }`. The single source of truth the AI reads.",shell:"Persistent app chrome in app.muten: `shell { Header { \u2026 } slot Footer { \u2026 } }`. Wraps every route; `slot` is where the active Page (<main>) mounts.",guard:"Route guard in app.muten: `routes { /cart -> cart guard auth.loggedIn else /login }`. If the store boolean is false on navigation, redirect. Guest-only page: `guard not auth.loggedIn else /catalog`.",else:"The redirect target of a route `guard`: `guard auth.loggedIn else /login`.",part:"Reusable composition: `part Card(item: Item, onPick: action) { ... }`. Pass OBJECTS (`$item.field`) and ACTION callbacks (`-> $onPick(...)`). Inlined at build time.",param:"Declares a route param read from the URL: `param id` for a route `/x/:id`. Usable in interpolation/`when`/expressions like a read-only string.",query:"An async data source. The state exposes `.loading`, `.error` and `.data`.",if:"Conditional INSIDE an action body: `if <expr> { \u2026 } else { \u2026 }` \u2014 the only branching in actions (toggles, validation, add-or-remove).",when:"Conditional render: `when <expr> { ... }`.",each:"List render: `each <list> as <item> { ... }`.",as:"Names the item variable in an `each`.",and:"Logical AND.",or:"Logical OR.",not:"Logical NOT, e.g. `when not (cart.isEmpty)`.",contains:"Case-insensitive substring match: `name contains @q`."},l=["push","remove","reset","set","create","update","delete","refetch"],
|
|
15
|
+
}`},Each:{props:{list:"expr",as:"ident",filter:"expr?"},children:!0,control:!0,doc:"List render: `each <list> as <item> { ... }`. Filter with `where`: `each posts as p where p.published { ... }` renders only matching items (no leak). The item is a scope variable in the template.",snippet:"each ${1:items} as ${2:item} {\n $0\n}"},Custom:{props:{component:"name",inputs:"map?",on:"map?"},children:!1,doc:"Escape hatch (\xA77): mount a host component from `src/components/<Name>.js`. Opaque to the IR; connected via inputs/on.",snippet:"Custom ${1:Name} inputs(${2:prop}: ${3}) on(${4:event}: ${5:action})"}},r=["bind","submit","where","columns","style","class","alt","inputs","on"],o={bind:"Two-way bind to a @state, e.g. `bind @search`.",submit:"Action to run on form submit, e.g. `submit createUser`.",where:"Filter clauses: `where(role == admin, name contains @q)`.",columns:"Columns to show: `columns(name, email, role)`.",style:"Layout & typography tokens (Muten builds, doesn\u2019t skin): `style(row, gap.md, text.lg)`.",class:'Raw CSS class(es) for LOOK \u2014 your CSS or a third-party like Tailwind: `class(card)` or `class("flex gap-4")`. Muten stays agnostic about appearance.',alt:'Required accessible/SEO text for an Image: `alt "{p.title}"`. Use "" for decorative images.',inputs:"Custom component inputs: `inputs(data: @sales)`.",on:"Custom component events wired to actions: `on(select: pick)`."},n=["screen","entity","state","store","const","theme","get","effect","action","mutates","mock","sources","api","meta","routes","shell","guard","else","part","param","query","post","put","delete","body","if","when","each","as","where","and","or","not","contains","use"],i={screen:"Declares the screen name: `screen users_dashboard`.",entity:"Declares a data shape + validation: `entity User { name text required email email required password text min:8 }` (implicit uuid id). Constraints: `required`, `min:N`, `max:N`.",state:'Declares reactive state: `state { search = "" : text users = query listUsers : list<User> }`.',store:"App-GLOBAL reactive state (shared across pages, no prop drilling): `store { cart = [] : list<number> }`. Referenced by name like local state.",const:"A compile-time IMMUTABLE scalar, inlined (never reactive): `const TAX = 0.21`. Scalars only \u2014 structured config uses a block (e.g. theme).",theme:'The project theme block (theme.muten): `theme { space { md "16px" } breakpoints { md "768px" } }`. Supplies the token SCALE; the engine owns only the vocabulary. The reset/base CSS lives in your stylesheet.',get:"A `.store` derived/memoized value (getter): `get total = items.length`. Read as `domain.total`, recomputes when deps change.",effect:"A `.store` reactive side-effect (Angular-style): `effect { ... }`. Re-runs automatically when the store state it reads changes.",action:"Declares a mutation: `action delete mutates users <- id { users.remove(u => u.id == id) }`.",mutates:"Lists the state an action may mutate \u2014 the linter enforces it.",mock:'Inline mock data for queries: `mock { listUsers: [ { name: "Ana", role: admin } ] }`.',sources:'Real data sources for queries: `sources { listChars: { url: "https://api...", at: "results" } }`.',api:'App-wide backend config in app.muten: `api { base: "https://\u2026" headers: { \u2026 } }`. A relative `sources` url is joined to `base`; headers merge (the source wins).',meta:'Page <head> metadata: `meta { title "\u2026" description "\u2026" }` \u2192 `<title>` + `<meta>` tags (og:* auto-derived). Applied on navigation.',post:'Explicit non-REST request in an action: `post "shop:/orders" body item` (escape hatch when CRUD ops do not fit).',put:'Explicit non-REST request in an action: `put "shop:/orders/{id}" body item`.',body:'The JSON body of an explicit `post`/`put` request: `post "shop:/x" body item`.',routes:"App root (app.muten): maps URLs to pages, `routes { /url -> page }`. The single source of truth the AI reads.",shell:"Persistent app chrome in app.muten: `shell { Header { \u2026 } slot Footer { \u2026 } }`. Wraps every route; `slot` is where the active Page (<main>) mounts.",guard:"Route guard in app.muten: `routes { /cart -> cart guard auth.loggedIn else /login }`. If the store boolean is false on navigation, redirect. Guest-only page: `guard not auth.loggedIn else /catalog`.",else:"The redirect target of a route `guard`: `guard auth.loggedIn else /login`.",part:"Reusable composition: `part Card(item: Item, onPick: action) { ... }`. Pass OBJECTS (`$item.field`) and ACTION callbacks (`-> $onPick(...)`). Inlined at build time.",param:"Declares a route param read from the URL: `param id` for a route `/x/:id`. Usable in interpolation/`when`/expressions like a read-only string.",query:"An async data source. The state exposes `.loading`, `.error` and `.data`.",if:"Conditional INSIDE an action body: `if <expr> { \u2026 } else { \u2026 }` \u2014 the only branching in actions (toggles, validation, add-or-remove).",when:"Conditional render: `when <expr> { ... }`.",each:"List render: `each <list> as <item> { ... }`. Optional `where`: `each posts as p where p.published { ... }` renders only matching items.",as:"Names the item variable in an `each`.",where:"Filters an `each` by a per-item condition: `each posts as p where p.published`. (Also the DataTable `where(...)` modifier.)",and:"Logical AND.",or:"Logical OR.",not:"Logical NOT, e.g. `when not (cart.isEmpty)`.",contains:"Case-insensitive substring match: `name contains @q`."},l=["push","remove","patch","reset","set","create","update","delete","refetch"],p={push:'Append to a list state: `users.push(draft)` or an inline object `users.push({ name: draft.name, role: "admin" })` (auto-fills uuid fields). Move/edit an item in place with `remove(x => x.id == c.id)` + `push({ id: c.id, \u2026, field: newVal })`.',remove:"Remove matching items locally: `users.remove(u => u.id == id)`.",patch:"Edit matching items IN PLACE (position-preserving): `todos.patch(t => t.id == id, { done: not t.done })`. Only list the fields that change. Use this to toggle/move/update an item instead of remove+push.",reset:"Reset a state to its declared initial: `draft.reset()`.",set:"Set a state value: `rating.set(v)` or an entity draft to an inline object: `draft.set({ name: c.name })`.",create:"POST an item to a source-backed list, then append the result: `orders.create(draft)`.",update:"PUT an item (by id) to a source-backed list, then replace it: `orders.update(order)`.",delete:"DELETE an item (by id) from a source-backed list, then drop it: `orders.delete(order)`.",refetch:"Re-run a query with N query-string params (paginate / search / filter): `products.refetch(q: term, page: n)`."},c=Object.keys(a),d=e;export{l as ACTION_OPS,p as ACTION_OP_DOCS,n as KEYWORDS,i as KEYWORD_DOCS,r as MODIFIERS,o as MODIFIER_DOCS,a as PRIMITIVES,c as PRIMITIVE_NAMES,d as TOKEN_NAMES,t as resolveToken};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{ParseError as f}from"#engine/shared/diagnostics.js";import{PRIMITIVES as g}from"#engine/lang/manifest.js";import{Grammar as R}from"#engine/lang/grammar.js";import{Tk as t,Pn as i,Kw as n,Nt as m,Mod as d,StOp as o,Ek as x}from"#engine/shared/vocab.js";const v={};for(const[c,e]of Object.entries(g))e.string&&(v[c]=e.string);const S=new Set(Object.entries(g).filter(([,c])=>c.interp).map(([c])=>c)),y=c=>c==="text"?"string":c,k=c=>/^h[1-6]$/.test(c);class w extends R{modifiers;statements;constructor(e){super(e),this.modifiers=new Map([[d.Bind,s=>{s.bind=this.at(t.Ref)?this.eat(t.Ref).v:this.parseDotted()}],[d.Submit,s=>{s.submit=this.parseDotted()}],[d.Where,s=>{s.where=this.parseParenList(()=>this.rebuildClause())}],[d.Columns,s=>{s.columns=this.parseParenList(()=>this.eat(t.Ident).v)}],[d.Style,s=>{s.style=this.parseParenList(()=>this.parseStyleToken())}],[d.Class,s=>{s.class=this.parseParenList(()=>{const a=this.at(t.String)?this.next().v:this.eat(t.Ident).v;return this.at(t.Ident,n.When)?(this.next(),{name:a,cond:this.parseExpr()}):a})}],[d.Alt,s=>{s.alt=this.parseInterpolation(this.eat(t.String).v)}],[d.Inputs,s=>{s.inputs=this.parseArgs()}],[d.On,s=>{s.on=this.parseArgs()}]]),this.statements=new Map([[o.Push,s=>({op:o.Push,target:s,arg:this.parseExpr()})],[o.Set,s=>({op:o.Set,target:s,arg:this.parseExpr()})],[o.Reset,s=>({op:o.Reset,target:s})],[o.Remove,s=>{const a=this.eat(t.Ident).v;return this.eat(t.FatArrow),{op:o.Remove,target:s,param:a,pred:this.parseExpr()}}],[o.Create,s=>({op:o.Create,target:s,arg:this.parseExpr()})],[o.Update,s=>({op:o.Update,target:s,arg:this.parseExpr()})],[o.Delete,s=>({op:o.Delete,target:s,arg:this.parseExpr()})],[o.Refetch,s=>{const a={};for(;!this.at(t.Punct,i.ParenR);){const r=this.eat(t.Ident).v;this.eat(t.Punct,i.Colon),a[r]=this.parseExpr(),this.at(t.Punct,i.Comma)&&this.next()}return{op:o.Refetch,target:s,params:a}}]])}parse(){const e={screen:"",entities:{},state:{},actions:{},tree:null},s=new Map([[n.Screen,()=>{this.next(),e.screen=this.eat(t.Ident).v}],[n.Entity,()=>this.parseEntity(e)],[n.State,()=>this.parseState(n.State,e.state)],[n.Store,()=>{e.store=e.store||{},this.parseState(n.Store,e.store)}],[n.Get,()=>this.parseGet(e)],[n.Effect,()=>{this.next(),(e.effects=e.effects||[]).push(this.parseActionBody())}],[n.Action,()=>this.parseAction(e)],[n.Mock,()=>this.parseMock(e)],[n.Sources,()=>this.parseSources(e)],[n.Api,()=>this.parseApi(e)],[n.Meta,()=>this.parseMeta(e)],[n.Routes,()=>this.parseRoutes(e)],[n.Shell,()=>this.parseShell(e)],[n.Part,()=>this.parsePart(e)],[n.Const,()=>this.parseConst(e)],[n.Theme,()=>this.parseTheme(e)],[n.Param,()=>{this.next(),(e.params=e.params||[]).push(this.eat(t.Ident).v)}],[n.Use,()=>{this.next();const a=[this.eat(t.Ident).v];for(;this.at(t.Punct,i.Comma);)this.next(),a.push(this.eat(t.Ident).v);this.eat(t.Ident,n.From),(e.imports=e.imports||[]).push({names:a,from:this.eat(t.String).v})}]]);for(;!this.at(t.Eof);){const a=this.peek(),r=a.t===t.Ident?s.get(a.v):void 0;r?r():e.tree=this.parseNode()}return e}parseEntity(e){this.eat(t.Ident,n.Entity);const s=this.eat(t.Ident).v;this.eat(t.Punct,i.BraceL);const a={id:"uuid"},r={};for(;!this.at(t.Punct,i.BraceR);){const p=this.eat(t.Ident).v,h=[this.eat(t.Ident).v];for(;this.at(t.Punct,i.Pipe);)this.next(),h.push(this.eat(t.Ident).v);a[p]=h.length>1?"enum:"+h.join("|"):y(h[0]);const l=this.parseConstraints();Object.keys(l).length&&(r[p]=l)}this.eat(t.Punct,i.BraceR),e.entities[s]=a,Object.keys(r).length&&((e.constraints=e.constraints||{})[s]=r)}parseConstraints(){const e={};for(;this.at(t.Ident,n.Required)||this.at(t.Ident,n.Min)||this.at(t.Ident,n.Max);){const s=this.next().v;if(s===n.Required){e.required=!0;continue}this.eat(t.Punct,i.Colon);const a=Number(this.eat(t.Number).v);s===n.Min?e.min=a:e.max=a}return e}parseState(e,s){for(this.eat(t.Ident,e),this.eat(t.Punct,i.BraceL);!this.at(t.Punct,i.BraceR);){const a=this.eat(t.Ident);this.eat(t.Punct,i.Assign);let r,p,h=!1;this.at(t.Ident,n.Query)?(this.next(),r="query:"+this.eat(t.Ident).v):this.at(t.Punct,i.BraceL)||this.at(t.Punct,i.BrackL)?(p=this.parseValue(),h=!0):this.at(t.String)?(p=this.next().v,h=!0):this.at(t.Number)?(p=Number(this.next().v),h=!0):this.at(t.Ident,n.True)||this.at(t.Ident,n.False)?(p=this.next().v===n.True,h=!0):(p=this.next().v,h=!0),this.eat(t.Punct,i.Colon);const l=this.parseType(),u=this.locOf(a.pos);s[a.v]=r?{type:l,source:r,loc:u}:{type:l,initial:h?p:null,loc:u}}this.eat(t.Punct,i.BraceR)}parseGet(e){this.eat(t.Ident,n.Get);const s=this.eat(t.Ident).v;this.eat(t.Punct,i.Assign),(e.gets=e.gets||{})[s]=this.parseExpr()}parseAction(e){this.eat(t.Ident,n.Action);const s=this.eat(t.Ident).v,a=[];if(this.at(t.Ident,n.Mutates))for(this.next(),a.push(this.eat(t.Ident).v);this.at(t.Punct,i.Comma);)this.next(),a.push(this.eat(t.Ident).v);let r="";this.at(t.LArrow)&&(this.next(),r=this.eat(t.Ident).v),e.actions[s]={mutates:a,input:r,body:this.parseActionBody()}}parseActionBody(){this.eat(t.Punct,i.BraceL);const e=[];for(;!this.at(t.Punct,i.BraceR);)e.push(this.parseStatement());return this.eat(t.Punct,i.BraceR),e}parseIf(){this.eat(t.Ident,n.If);const e=this.parseExpr(),s=this.parseActionBody(),a=this.at(t.Ident,n.Else)?(this.next(),this.parseActionBody()):null;return{op:o.If,cond:e,then:s,else:a}}parseRequest(){const e=this.eat(t.Ident).v.toUpperCase(),s=this.parseInterpolation(this.eat(t.String).v);let a=null;return this.at(t.Ident,n.Body)&&(this.next(),a=this.parseExpr()),{op:o.Request,method:e,url:s,body:a}}parseStatement(){if(this.at(t.Ident,n.If))return this.parseIf();if(this.at(t.Ident,"post")||this.at(t.Ident,"put")||this.at(t.Ident,"delete"))return this.parseRequest();const e=this.eat(t.Ident).v;this.eat(t.Punct,i.Dot);const s=this.eat(t.Ident).v;this.eat(t.Punct,i.ParenL);const a=this.statements.get(s);if(!a)throw new f(`unknown action method "${s}" on "${e}"`,this.locOf(this.peek().pos));const r=a(e);return this.eat(t.Punct,i.ParenR),r}parseMock(e){this.eat(t.Ident,n.Mock);const s=e.mock||{};this.parseEntries(a=>{s[a]=this.parseValue()}),e.mock=s}parseSources(e){this.eat(t.Ident,n.Sources);const s=e.sources||{};this.parseEntries(a=>{s[a]=this.parseValue()}),e.sources=s}parseApi(e){this.eat(t.Ident,n.Api);const s=e.api||{};this.parseEntries(a=>{s[a]=this.parseValue()}),e.api=s}parseMeta(e){this.eat(t.Ident,n.Meta),this.eat(t.Punct,i.BraceL);const s=e.meta||{};for(;!this.at(t.Punct,i.BraceR);){const a=this.eat(t.Ident).v;s[a]=this.eat(t.String).v}this.eat(t.Punct,i.BraceR),e.meta=s}parseRoutes(e){this.eat(t.Ident,n.Routes),this.eat(t.Punct,i.BraceL);const s=e.routes||[];for(;!this.at(t.Punct,i.BraceR);){const a=this.peek(),r=this.locOf(a.pos).line,p=this.pathOnLine(r);this.eat(t.Arrow);const h={url:p,page:this.eat(t.Ident).v,loc:this.locOf(a.pos)};this.at(t.Ident,n.Guard)&&(this.next(),h.guardNeg=this.at(t.Ident,n.Not)?(this.next(),!0):!1,h.guard=this.parseDotted(),this.eat(t.Ident,n.Else),h.redirect=this.pathOnLine(r)),s.push(h)}this.eat(t.Punct,i.BraceR),e.routes=s}parseShell(e){this.eat(t.Ident,n.Shell),e.shell={type:m.Shell,props:{},children:this.parseChildren()}}parseConst(e){this.eat(t.Ident,n.Const);const s=this.eat(t.Ident).v;if(this.eat(t.Punct,i.Assign),this.at(t.Punct,i.BraceL)||this.at(t.Punct,i.BrackL))throw new f("const holds a single value (string/number/bool) \u2014 use a block like `theme { \u2026 }` for structured data",this.locOf(this.peek().pos));(e.consts=e.consts||{})[s]=this.parseScalar()}parseTheme(e){this.eat(t.Ident,n.Theme),this.eat(t.Punct,i.BraceL);const s={};for(;!this.at(t.Punct,i.BraceR);){const a=this.eat(t.Ident).v;this.eat(t.Punct,i.BraceL);const r={};for(;!this.at(t.Punct,i.BraceR);)r[this.eat(t.Ident).v]=this.eat(t.String).v;this.eat(t.Punct,i.BraceR),s[a]=r}this.eat(t.Punct,i.BraceR),e.theme=s}parsePart(e){this.eat(t.Ident,n.Part);const s=this.eat(t.Ident).v;this.eat(t.Punct,i.ParenL);const a=[];for(;!this.at(t.Punct,i.ParenR);){const p=this.eat(t.Ident).v;this.eat(t.Punct,i.Colon),a.push({name:p,type:this.parseType()}),this.at(t.Punct,i.Comma)&&this.next()}this.eat(t.Punct,i.ParenR);const r=this.parseChildren();e.parts=e.parts||{},e.parts[s]={params:a,tree:r.length===1?r[0]:{type:m.Stack,props:{},children:r}}}parseWhen(){const e=this.eat(t.Ident,n.When),s=this.parseExpr();return{type:m.When,props:{cond:s},children:this.parseChildren(),loc:this.locOf(e.pos)}}parseEach(){const e=this.eat(t.Ident,n.Each),s=this.parseExpr();this.eat(t.Ident,n.As);const a=this.eat(t.Ident).v;return{type:m.Each,props:{list:s,as:a},children:this.parseChildren(),loc:this.locOf(e.pos)}}parseNode(){if(this.at(t.Ident,n.When))return this.parseWhen();if(this.at(t.Ident,n.Each))return this.parseEach();const e=this.eat(t.Ident),s=e.v,a=this.locOf(e.pos);if(this.at(t.Punct,i.ParenL)){const u=this.parseArgs();return this.at(t.Ident,n.Client)?(this.next(),this.eat(t.Punct,i.Colon),{type:s,args:u,props:{hydrate:this.eat(t.Ident).v},loc:a}):{type:s,args:u,loc:a}}const r={},p=[];s===m.Custom&&(r.component=this.eat(t.Ident).v);let h=!0;for(;h;){const u=this.peek();switch(u.t){case t.String:{const P=v[s]||"label";r[P]=S.has(s)?this.parseInterpolation(this.next().v):this.next().v;break}case t.Param:{const P=v[s]||"label";r[P]={$param:this.next().v};break}case t.Ref:r.data=this.next().v;break;case t.Arrow:this.parseArrow(s,r);break;case t.Ident:{const P=u.v;if(s===m.Title&&k(P)){this.next(),r.level=P;break}const I=this.modifiers.get(P);if(!I){h=!1;break}this.next(),I(r);break}case t.Punct:if(u.v===i.BraceL){for(this.next();!this.at(t.Punct,i.BraceR);)p.push(this.parseNode());this.eat(t.Punct,i.BraceR)}h=!1;break;default:h=!1}}const l={type:s,props:r,loc:a};return p.length&&(l.children=p),l}parseArrow(e,s){if(this.next(),e===m.Link){s.to=this.parsePath();return}s.action=this.parseDotted(),this.at(t.Punct,i.ParenL)&&(this.next(),this.at(t.Punct,i.ParenR)||(s.arg=this.parseExpr()),this.eat(t.Punct,i.ParenR))}parseChildren(){this.eat(t.Punct,i.BraceL);const e=[];for(;!this.at(t.Punct,i.BraceR);)e.push(this.parseNode());return this.eat(t.Punct,i.BraceR),e}parseType(){let e=this.eat(t.Ident).v;return this.at(t.Punct,i.Lt)&&(this.next(),e+="<"+this.eat(t.Ident).v+">",this.eat(t.Punct,i.Gt)),e}parseStyleToken(){const e=()=>this.at(t.Number)?this.next().v:this.eat(t.Ident).v;let s=e();for(this.at(t.Punct,i.Colon)&&(this.next(),s+=":"+e());this.at(t.Punct,i.Dot);)this.next(),s+="."+e();return s}parseDotted(){let e=this.at(t.Param)?"$"+this.next().v:this.eat(t.Ident).v;for(;this.at(t.Punct,i.Dot);)this.next(),e+="."+this.eat(t.Ident).v;return e}parseParenList(e){this.eat(t.Punct,i.ParenL);const s=[];for(;!this.at(t.Punct,i.ParenR);)s.push(e()),this.at(t.Punct,i.Comma)&&this.next();return this.eat(t.Punct,i.ParenR),s}rebuildClause(){const e=[];for(;!this.at(t.Punct,i.Comma)&&!this.at(t.Punct,i.ParenR);)e.push(this.next().v);return e.join(" ")}parseEntries(e){for(this.eat(t.Punct,i.BraceL);!this.at(t.Punct,i.BraceR);){const s=this.eat(t.Ident).v;this.eat(t.Punct,i.Colon),e(s),this.at(t.Punct,i.Comma)&&this.next()}this.eat(t.Punct,i.BraceR)}parsePath(){const e=[];let s="";for(;this.at(t.Punct,i.Slash);){const a=this.next();s+="/",this.at(t.Punct,i.BraceL)&&this.peek().pos===a.pos+1?(e.push(s),s="",this.next(),e.push(this.parseExpr()),this.eat(t.Punct,i.BraceR)):this.at(t.Ident)&&this.peek().pos===a.pos+1&&(s+=this.eat(t.Ident).v)}return e.length?(s&&e.push(s),{kind:x.Interp,parts:e}):s}pathOnLine(e){let s="";for(;this.at(t.Punct,i.Slash)&&this.locOf(this.peek().pos).line===e;)this.next(),s+="/",this.at(t.Punct,i.Colon)?(this.next(),s+=":"+this.eat(t.Ident).v):this.at(t.Ident)&&(s+=this.eat(t.Ident).v);return s}parseArgs(){this.eat(t.Punct,i.ParenL);const e={};for(;!this.at(t.Punct,i.ParenR);){const s=this.eat(t.Ident).v;this.eat(t.Punct,i.Colon),e[s]=this.parseArgValue(),this.at(t.Punct,i.Comma)&&this.next()}return this.eat(t.Punct,i.ParenR),e}parseArgValue(){return this.at(t.String)?this.next().v:this.at(t.Number)?Number(this.next().v):this.at(t.Ref)?this.next().v:this.at(t.Param)?{$param:this.next().v}:this.parseDotted()}}function A(c){return new w(c).parse()}export{w as Parser,A as parse};
|
|
1
|
+
import{ParseError as g}from"#engine/shared/diagnostics.js";import{PRIMITIVES as f}from"#engine/lang/manifest.js";import{Grammar as x}from"#engine/lang/grammar.js";import{Tk as t,Pn as i,Kw as n,Nt as m,Mod as d,StOp as o,Ek as R}from"#engine/shared/vocab.js";const v={};for(const[c,e]of Object.entries(f))e.string&&(v[c]=e.string);const S=new Set(Object.entries(f).filter(([,c])=>c.interp).map(([c])=>c)),y=c=>c==="text"?"string":c,E=c=>/^h[1-6]$/.test(c);class k extends x{modifiers;statements;constructor(e){super(e),this.modifiers=new Map([[d.Bind,s=>{if(this.at(t.Ref)){let a=this.eat(t.Ref).v;for(;this.at(t.Punct,i.Dot);)this.next(),a+="."+this.eat(t.Ident).v;s.bind=a}else s.bind=this.parseDotted()}],[d.Submit,s=>{s.submit=this.parseDotted()}],[d.Where,s=>{s.where=this.parseParenList(()=>this.rebuildClause())}],[d.Columns,s=>{s.columns=this.parseParenList(()=>this.eat(t.Ident).v)}],[d.Style,s=>{s.style=this.parseParenList(()=>this.parseStyleToken())}],[d.Class,s=>{s.class=this.parseParenList(()=>{const a=this.at(t.String)?this.next().v:this.eat(t.Ident).v;return this.at(t.Ident,n.When)?(this.next(),{name:a,cond:this.parseExpr()}):a})}],[d.Alt,s=>{s.alt=this.parseInterpolation(this.eat(t.String).v)}],[d.Inputs,s=>{s.inputs=this.parseArgs()}],[d.On,s=>{s.on=this.parseArgs()}]]),this.statements=new Map([[o.Push,s=>({op:o.Push,target:s,arg:this.parseExpr()})],[o.Set,s=>({op:o.Set,target:s,arg:this.parseExpr()})],[o.Reset,s=>({op:o.Reset,target:s})],[o.Remove,s=>{const a=this.eat(t.Ident).v;return this.eat(t.FatArrow),{op:o.Remove,target:s,param:a,pred:this.parseExpr()}}],[o.Patch,s=>{const a=this.eat(t.Ident).v;this.eat(t.FatArrow);const r=this.parseExpr();return this.eat(t.Punct,i.Comma),{op:o.Patch,target:s,param:a,pred:r,patch:this.parseExpr()}}],[o.Create,s=>({op:o.Create,target:s,arg:this.parseExpr()})],[o.Update,s=>({op:o.Update,target:s,arg:this.parseExpr()})],[o.Delete,s=>({op:o.Delete,target:s,arg:this.parseExpr()})],[o.Refetch,s=>{const a={};for(;!this.at(t.Punct,i.ParenR);){const r=this.eat(t.Ident).v;this.eat(t.Punct,i.Colon),a[r]=this.parseExpr(),this.at(t.Punct,i.Comma)&&this.next()}return{op:o.Refetch,target:s,params:a}}]])}parse(){const e={screen:"",entities:{},state:{},actions:{},tree:null},s=new Map([[n.Screen,()=>{this.next(),e.screen=this.eat(t.Ident).v}],[n.Entity,()=>this.parseEntity(e)],[n.State,()=>this.parseState(n.State,e.state)],[n.Store,()=>{e.store=e.store||{},this.parseState(n.Store,e.store)}],[n.Get,()=>this.parseGet(e)],[n.Effect,()=>{this.next(),(e.effects=e.effects||[]).push(this.parseActionBody())}],[n.Action,()=>this.parseAction(e)],[n.Mock,()=>this.parseMock(e)],[n.Sources,()=>this.parseSources(e)],[n.Api,()=>this.parseApi(e)],[n.Meta,()=>this.parseMeta(e)],[n.Routes,()=>this.parseRoutes(e)],[n.Shell,()=>this.parseShell(e)],[n.Part,()=>this.parsePart(e)],[n.Const,()=>this.parseConst(e)],[n.Theme,()=>this.parseTheme(e)],[n.Param,()=>{this.next(),(e.params=e.params||[]).push(this.eat(t.Ident).v)}],[n.Use,()=>{this.next();const a=[this.eat(t.Ident).v];for(;this.at(t.Punct,i.Comma);)this.next(),a.push(this.eat(t.Ident).v);this.eat(t.Ident,n.From),(e.imports=e.imports||[]).push({names:a,from:this.eat(t.String).v})}]]);for(;!this.at(t.Eof);){const a=this.peek(),r=a.t===t.Ident?s.get(a.v):void 0;r?r():e.tree=this.parseNode()}return e}parseEntity(e){this.eat(t.Ident,n.Entity);const s=this.eat(t.Ident).v;this.eat(t.Punct,i.BraceL);const a={id:"uuid"},r={};for(;!this.at(t.Punct,i.BraceR);){const p=this.eat(t.Ident).v,h=[this.eat(t.Ident).v];for(;this.at(t.Punct,i.Pipe);)this.next(),h.push(this.eat(t.Ident).v);a[p]=h.length>1?"enum:"+h.join("|"):y(h[0]);const l=this.parseConstraints();Object.keys(l).length&&(r[p]=l)}this.eat(t.Punct,i.BraceR),e.entities[s]=a,Object.keys(r).length&&((e.constraints=e.constraints||{})[s]=r)}parseConstraints(){const e={};for(;this.at(t.Ident,n.Required)||this.at(t.Ident,n.Min)||this.at(t.Ident,n.Max);){const s=this.next().v;if(s===n.Required){e.required=!0;continue}this.eat(t.Punct,i.Colon);const a=Number(this.eat(t.Number).v);s===n.Min?e.min=a:e.max=a}return e}parseState(e,s){for(this.eat(t.Ident,e),this.eat(t.Punct,i.BraceL);!this.at(t.Punct,i.BraceR);){const a=this.eat(t.Ident);this.eat(t.Punct,i.Assign);let r,p,h=!1;this.at(t.Ident,n.Query)?(this.next(),r="query:"+this.eat(t.Ident).v):this.at(t.Punct,i.BraceL)||this.at(t.Punct,i.BrackL)?(p=this.parseValue(),h=!0):this.at(t.String)?(p=this.next().v,h=!0):this.at(t.Number)?(p=Number(this.next().v),h=!0):this.at(t.Ident,n.True)||this.at(t.Ident,n.False)?(p=this.next().v===n.True,h=!0):this.at(t.Ident,"null")?(this.next(),h=!0):(p=this.next().v,h=!0),this.eat(t.Punct,i.Colon);const l=this.parseType(),u=this.locOf(a.pos);s[a.v]=r?{type:l,source:r,loc:u}:{type:l,initial:h?p:null,loc:u}}this.eat(t.Punct,i.BraceR)}parseGet(e){this.eat(t.Ident,n.Get);const s=this.eat(t.Ident).v;this.eat(t.Punct,i.Assign),(e.gets=e.gets||{})[s]=this.parseExpr()}parseAction(e){this.eat(t.Ident,n.Action);const s=this.eat(t.Ident).v,a=[];if(this.at(t.Ident,n.Mutates))for(this.next(),a.push(this.eat(t.Ident).v);this.at(t.Punct,i.Comma);)this.next(),a.push(this.eat(t.Ident).v);let r="";this.at(t.LArrow)&&(this.next(),r=this.eat(t.Ident).v),e.actions[s]={mutates:a,input:r,body:this.parseActionBody()}}parseActionBody(){this.eat(t.Punct,i.BraceL);const e=[];for(;!this.at(t.Punct,i.BraceR);)e.push(this.parseStatement());return this.eat(t.Punct,i.BraceR),e}parseIf(){this.eat(t.Ident,n.If);const e=this.parseExpr(),s=this.parseActionBody(),a=this.at(t.Ident,n.Else)?(this.next(),this.parseActionBody()):null;return{op:o.If,cond:e,then:s,else:a}}parseRequest(){const e=this.eat(t.Ident).v.toUpperCase(),s=this.parseInterpolation(this.eat(t.String).v);let a=null;return this.at(t.Ident,n.Body)&&(this.next(),a=this.parseExpr()),{op:o.Request,method:e,url:s,body:a}}parseStatement(){if(this.at(t.Ident,n.If))return this.parseIf();if(this.at(t.Ident,"post")||this.at(t.Ident,"put")||this.at(t.Ident,"delete"))return this.parseRequest();const e=this.eat(t.Ident).v;this.eat(t.Punct,i.Dot);const s=this.eat(t.Ident).v;this.eat(t.Punct,i.ParenL);const a=this.statements.get(s);if(!a){const p=[];for(;!this.at(t.Punct,i.ParenR);)p.push(this.parseExpr()),this.at(t.Punct,i.Comma)&&this.next();return this.eat(t.Punct,i.ParenR),{op:o.Call,target:e,method:s,args:p}}const r=a(e);return this.eat(t.Punct,i.ParenR),r}parseMock(e){this.eat(t.Ident,n.Mock);const s=e.mock||{};this.parseEntries(a=>{s[a]=this.parseValue()}),e.mock=s}parseSources(e){this.eat(t.Ident,n.Sources);const s=e.sources||{};this.parseEntries(a=>{s[a]=this.parseValue()}),e.sources=s}parseApi(e){this.eat(t.Ident,n.Api);const s=e.api||{};this.parseEntries(a=>{s[a]=this.parseValue()}),e.api=s}parseMeta(e){this.eat(t.Ident,n.Meta),this.eat(t.Punct,i.BraceL);const s=e.meta||{};for(;!this.at(t.Punct,i.BraceR);){const a=this.eat(t.Ident).v;s[a]=this.eat(t.String).v}this.eat(t.Punct,i.BraceR),e.meta=s}parseRoutes(e){this.eat(t.Ident,n.Routes),this.eat(t.Punct,i.BraceL);const s=e.routes||[];for(;!this.at(t.Punct,i.BraceR);){const a=this.peek(),r=this.locOf(a.pos).line,p=this.pathOnLine(r);this.eat(t.Arrow);const h={url:p,page:this.eat(t.Ident).v,loc:this.locOf(a.pos)};this.at(t.Ident,n.Guard)&&(this.next(),h.guardNeg=this.at(t.Ident,n.Not)?(this.next(),!0):!1,h.guard=this.parseDotted(),this.eat(t.Ident,n.Else),h.redirect=this.pathOnLine(r)),s.push(h)}this.eat(t.Punct,i.BraceR),e.routes=s}parseShell(e){this.eat(t.Ident,n.Shell),e.shell={type:m.Shell,props:{},children:this.parseChildren()}}parseConst(e){this.eat(t.Ident,n.Const);const s=this.eat(t.Ident).v;if(this.eat(t.Punct,i.Assign),this.at(t.Punct,i.BraceL)||this.at(t.Punct,i.BrackL))throw new g("const holds a single value (string/number/bool) \u2014 use a block like `theme { \u2026 }` for structured data",this.locOf(this.peek().pos));(e.consts=e.consts||{})[s]=this.parseScalar()}parseTheme(e){this.eat(t.Ident,n.Theme),this.eat(t.Punct,i.BraceL);const s={};for(;!this.at(t.Punct,i.BraceR);){const a=this.eat(t.Ident).v;this.eat(t.Punct,i.BraceL);const r={};for(;!this.at(t.Punct,i.BraceR);)r[this.eat(t.Ident).v]=this.eat(t.String).v;this.eat(t.Punct,i.BraceR),s[a]=r}this.eat(t.Punct,i.BraceR),e.theme=s}parsePart(e){this.eat(t.Ident,n.Part);const s=this.eat(t.Ident).v;this.eat(t.Punct,i.ParenL);const a=[];for(;!this.at(t.Punct,i.ParenR);){const p=this.eat(t.Ident).v;this.eat(t.Punct,i.Colon),a.push({name:p,type:this.parseType()}),this.at(t.Punct,i.Comma)&&this.next()}this.eat(t.Punct,i.ParenR);const r=this.parseChildren();e.parts=e.parts||{},e.parts[s]={params:a,tree:r.length===1?r[0]:{type:m.Stack,props:{},children:r}}}parseWhen(){const e=this.eat(t.Ident,n.When),s=this.parseExpr();return{type:m.When,props:{cond:s},children:this.parseChildren(),loc:this.locOf(e.pos)}}parseEach(){const e=this.eat(t.Ident,n.Each),s=this.parseExpr();this.eat(t.Ident,n.As);const a=this.eat(t.Ident).v,r={list:s,as:a};return this.at(t.Ident,n.Where)&&(this.next(),r.filter=this.parseExpr()),{type:m.Each,props:r,children:this.parseChildren(),loc:this.locOf(e.pos)}}parseNode(){if(this.at(t.Ident,n.When))return this.parseWhen();if(this.at(t.Ident,n.Each))return this.parseEach();const e=this.eat(t.Ident),s=e.v,a=this.locOf(e.pos);if(this.at(t.Punct,i.ParenL)){const u=this.parseArgs();return this.at(t.Ident,n.Client)?(this.next(),this.eat(t.Punct,i.Colon),{type:s,args:u,props:{hydrate:this.eat(t.Ident).v},loc:a}):{type:s,args:u,loc:a}}const r={},p=[];s===m.Custom&&(r.component=this.eat(t.Ident).v);let h=!0;for(;h;){const u=this.peek();switch(u.t){case t.String:{const P=v[s]||"label";r[P]=S.has(s)?this.parseInterpolation(this.next().v):this.next().v;break}case t.Param:{const P=v[s]||"label";r[P]={$param:this.next().v};break}case t.Ref:r.data=this.next().v;break;case t.Arrow:this.parseArrow(s,r);break;case t.Ident:{const P=u.v;if(s===m.Title&&E(P)){this.next(),r.level=P;break}const I=this.modifiers.get(P);if(!I){h=!1;break}this.next(),I(r);break}case t.Punct:if(u.v===i.BraceL){for(this.next();!this.at(t.Punct,i.BraceR);)p.push(this.parseNode());this.eat(t.Punct,i.BraceR)}h=!1;break;default:h=!1}}const l={type:s,props:r,loc:a};return p.length&&(l.children=p),l}parseArrow(e,s){if(this.next(),e===m.Link){s.to=this.parsePath();return}s.action=this.parseDotted(),this.at(t.Punct,i.ParenL)&&(this.next(),this.at(t.Punct,i.ParenR)||(s.arg=this.parseExpr()),this.eat(t.Punct,i.ParenR))}parseChildren(){this.eat(t.Punct,i.BraceL);const e=[];for(;!this.at(t.Punct,i.BraceR);)e.push(this.parseNode());return this.eat(t.Punct,i.BraceR),e}parseType(){let e=this.eat(t.Ident).v;return this.at(t.Punct,i.Lt)&&(this.next(),e+="<"+this.eat(t.Ident).v+">",this.eat(t.Punct,i.Gt)),e}parseStyleToken(){const e=()=>this.at(t.Number)?this.next().v:this.eat(t.Ident).v;let s=e();for(this.at(t.Punct,i.Colon)&&(this.next(),s+=":"+e());this.at(t.Punct,i.Dot);)this.next(),s+="."+e();return s}parseDotted(){let e=this.at(t.Param)?"$"+this.next().v:this.eat(t.Ident).v;for(;this.at(t.Punct,i.Dot);)this.next(),e+="."+this.eat(t.Ident).v;return e}parseParenList(e){this.eat(t.Punct,i.ParenL);const s=[];for(;!this.at(t.Punct,i.ParenR);)s.push(e()),this.at(t.Punct,i.Comma)&&this.next();return this.eat(t.Punct,i.ParenR),s}rebuildClause(){const e=[];for(;!this.at(t.Punct,i.Comma)&&!this.at(t.Punct,i.ParenR);)e.push(this.next().v);return e.join(" ")}parseEntries(e){for(this.eat(t.Punct,i.BraceL);!this.at(t.Punct,i.BraceR);){const s=this.eat(t.Ident).v;this.eat(t.Punct,i.Colon),e(s),this.at(t.Punct,i.Comma)&&this.next()}this.eat(t.Punct,i.BraceR)}parsePath(){const e=[];let s="";for(;this.at(t.Punct,i.Slash);){const a=this.next();s+="/",this.at(t.Punct,i.BraceL)&&this.peek().pos===a.pos+1?(e.push(s),s="",this.next(),e.push(this.parseExpr()),this.eat(t.Punct,i.BraceR)):this.at(t.Ident)&&this.peek().pos===a.pos+1&&(s+=this.eat(t.Ident).v)}return e.length?(s&&e.push(s),{kind:R.Interp,parts:e}):s}pathOnLine(e){let s="";for(;this.at(t.Punct,i.Slash)&&this.locOf(this.peek().pos).line===e;)this.next(),s+="/",this.at(t.Punct,i.Colon)?(this.next(),s+=":"+this.eat(t.Ident).v):this.at(t.Ident)&&(s+=this.eat(t.Ident).v);return s}parseArgs(){this.eat(t.Punct,i.ParenL);const e={};for(;!this.at(t.Punct,i.ParenR);){const s=this.eat(t.Ident).v;this.eat(t.Punct,i.Colon),e[s]=this.parseArgValue(),this.at(t.Punct,i.Comma)&&this.next()}return this.eat(t.Punct,i.ParenR),e}parseArgValue(){return this.at(t.String)?this.next().v:this.at(t.Number)?Number(this.next().v):this.at(t.Ref)?this.next().v:this.at(t.Param)?{$param:this.next().v}:this.parseDotted()}}function A(c){return new k(c).parse()}export{k as Parser,A as parse};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import p from"node:fs";import{dirname as m,join as u}from"node:path";import{parse as f}from"#engine/lang/parse.js";import{composeDoc as D}from"#engine/ir/compose.js";import{validate as y}from"#engine/ir/validate.js";import{closest as k,diag as g,ParseError as b}from"#engine/shared/diagnostics.js";import{PRIMITIVE_NAMES as O}from"#engine/lang/manifest.js";import{mergeTheme as d}from"#engine/style/tokens.js";function h(r){const e=l(r);if(!e)return d({});try{return d(f(p.readFileSync(u(e,"theme.muten"),"utf8")).theme||{})}catch{return d({})}}function S(r){const e={};let t;try{t=p.readdirSync(r)}catch{return e}for(const s of t){if(!s.endsWith(".muten"))continue;let i;try{i=f(p.readFileSync(u(r,s),"utf8"))}catch{continue}for(const[o,c]of Object.entries(i.parts||{}))e[o]={...c,state:i.state||{},entities:i.entities||{}}}return e}function l(r){let e=m(r);for(let t=0;t<30;t++){if(p.existsSync(u(e,"src","pages")))return e;const s=m(e);if(s===e)break;e=s}return null}function T(r){const e=[],t=s=>{let i;try{i=p.readdirSync(s,{withFileTypes:!0})}catch{return}for(const o of i){if(!o.isDirectory())continue;const c=u(s,o.name);o.name==="parts"?e.push(c):t(c)}};return t(u(r,"src")),e}function w(r){const e=l(r);if(!e)return[];const t=[],s=i=>{let o;try{o=p.readdirSync(i,{withFileTypes:!0})}catch{return}for(const c of o){const n=u(i,c.name);c.isDirectory()?s(n):c.name.endsWith(".store")&&t.push(c.name.slice(0,-6))}};return s(u(e,"src")),t}function R(r){const e=l(r);if(!e)return S(u(m(r),"parts"));const t={};for(const s of T(e))Object.assign(t,S(s));return t}function x(r,e){const t=l(r),s=t?u(t,"src","pages"):null;let i=[];if(s)try{i=p.readdirSync(s,{withFileTypes:!0}).filter(n=>n.isDirectory()).map(n=>n.name)}catch{}const o=[],c=new Set;for(const n of e)c.has(n.url)&&o.push(g("dup-route",`duplicate route "${n.url}"`,{loc:n.loc})),c.add(n.url),s&&!i.includes(n.page)&&o.push(g("unknown-page",`route "${n.url}" \u2192 page "${n.page}" not found in src/pages/`,{loc:n.loc,suggestion:k(n.page,i)}));return{ok:o.length===0,diagnostics:o}}function L(r,e){let t;try{t=f(e)}catch(o){return o instanceof b&&o.loc?{ok:!1,diagnostics:[g("syntax",o.message,{loc:o.loc})]}:{ok:!0,diagnostics:[]}}if(t.routes)return x(r,t.routes);if(t.theme)return{ok:!0,diagnostics:[]};if(r.endsWith(".store"))return y({screen:"store",state:t.state||{},actions:t.actions||{},entities:t.entities||{},gets:t.gets||{},consts:{},constraints:{},rootId:void 0,nodes:{}},{kind:"store"});const s=R(r),{doc:i}=D(t,s);return y(i,{parts:Object.keys(s),stores:w(r),theme:h(r)})}function $(r,e){let t=null;try{t=f(e)}catch{}const s=R(r),i=Object.entries(s).map(([n,a])=>({name:n,params:a.params||[]})),o=[],c=(n,a)=>{o.push({name:n,type:a.type||"",query:typeof a.source=="string"&&a.source.startsWith("query:")})};for(const[n,a]of Object.entries(t?.state||{}))c(n,a);for(const n of Object.values(s))for(const[a,j]of Object.entries(n.state||{}))c(a,j);return{parts:i,state:o,actions:Object.keys(t?.actions||{}),primitives:O,theme:h(r)}}export{L as analyze,$ as completion,R as projectParts,w as projectStores,h as projectTheme};
|
|
1
|
+
import p from"node:fs";import{dirname as m,join as u}from"node:path";import{parse as f}from"#engine/lang/parse.js";import{composeDoc as D}from"#engine/ir/compose.js";import{validate as y}from"#engine/ir/validate.js";import{closest as k,diag as g,ParseError as b}from"#engine/shared/diagnostics.js";import{PRIMITIVE_NAMES as O}from"#engine/lang/manifest.js";import{mergeTheme as d}from"#engine/style/tokens.js";function h(r){const e=l(r);if(!e)return d({});try{return d(f(p.readFileSync(u(e,"theme.muten"),"utf8")).theme||{})}catch{return d({})}}function S(r){const e={};let t;try{t=p.readdirSync(r)}catch{return e}for(const s of t){if(!s.endsWith(".muten"))continue;let i;try{i=f(p.readFileSync(u(r,s),"utf8"))}catch{continue}for(const[o,c]of Object.entries(i.parts||{}))e[o]={...c,state:i.state||{},entities:i.entities||{}}}return e}function l(r){let e=m(r);for(let t=0;t<30;t++){if(p.existsSync(u(e,"src","pages")))return e;const s=m(e);if(s===e)break;e=s}return null}function T(r){const e=[],t=s=>{let i;try{i=p.readdirSync(s,{withFileTypes:!0})}catch{return}for(const o of i){if(!o.isDirectory())continue;const c=u(s,o.name);o.name==="parts"?e.push(c):t(c)}};return t(u(r,"src")),e}function w(r){const e=l(r);if(!e)return[];const t=[],s=i=>{let o;try{o=p.readdirSync(i,{withFileTypes:!0})}catch{return}for(const c of o){const n=u(i,c.name);c.isDirectory()?s(n):c.name.endsWith(".store")&&t.push(c.name.slice(0,-6))}};return s(u(e,"src")),t}function R(r){const e=l(r);if(!e)return S(u(m(r),"parts"));const t={};for(const s of T(e))Object.assign(t,S(s));return t}function x(r,e){const t=l(r),s=t?u(t,"src","pages"):null;let i=[];if(s)try{i=p.readdirSync(s,{withFileTypes:!0}).filter(n=>n.isDirectory()).map(n=>n.name)}catch{}const o=[],c=new Set;for(const n of e)c.has(n.url)&&o.push(g("dup-route",`duplicate route "${n.url}"`,{loc:n.loc})),c.add(n.url),s&&!i.includes(n.page)&&o.push(g("unknown-page",`route "${n.url}" \u2192 page "${n.page}" not found in src/pages/`,{loc:n.loc,suggestion:k(n.page,i)}));return{ok:o.length===0,diagnostics:o}}function L(r,e){let t;try{t=f(e)}catch(o){return o instanceof b&&o.loc?{ok:!1,diagnostics:[g("syntax",o.message,{loc:o.loc})]}:{ok:!0,diagnostics:[]}}if(t.routes)return x(r,t.routes);if(t.theme)return{ok:!0,diagnostics:[]};if(r.endsWith(".store"))return y({screen:"store",state:t.state||{},actions:t.actions||{},entities:t.entities||{},gets:t.gets||{},effects:t.effects||[],consts:{},constraints:{},rootId:void 0,nodes:{}},{kind:"store"});const s=R(r),{doc:i}=D(t,s);return y(i,{parts:Object.keys(s),stores:w(r),theme:h(r)})}function $(r,e){let t=null;try{t=f(e)}catch{}const s=R(r),i=Object.entries(s).map(([n,a])=>({name:n,params:a.params||[]})),o=[],c=(n,a)=>{o.push({name:n,type:a.type||"",query:typeof a.source=="string"&&a.source.startsWith("query:")})};for(const[n,a]of Object.entries(t?.state||{}))c(n,a);for(const n of Object.values(s))for(const[a,j]of Object.entries(n.state||{}))c(a,j);return{parts:i,state:o,actions:Object.keys(t?.actions||{}),primitives:O,theme:h(r)}}export{L as analyze,$ as completion,R as projectParts,w as projectStores,h as projectTheme};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{join as m,relative as y}from"node:path";import{existsSync as h,readFileSync as $}from"node:fs";import{parse as v}from"#engine/lang/parse.js";import{toDoc as j}from"#engine/ir/flatten.js";import{validate as D}from"#engine/ir/validate.js";function O(i,l,s){const f=t=>y(i,t),p=Object.keys(l),n=[];for(const[t,c]of Object.entries(l)){const d=f(m(i,"src",t+".store"));try{for(const r of D(j(c),{stores:p,storeMembers:s,kind:"store"}).diagnostics)n.push({file:d,...r})}catch{}}const a=m(i,"src","app.muten");if(h(a))try{const t=v($(a,"utf8")),c=new Set((t.routes||[]).map(r=>r.url)),d=f(a);for(const r of t.routes||[]){const u=o=>{n.push({file:d,code:"guard-error",severity:"error",message:o,loc:r.loc??null,suggestion:null})};if(r.guard){const o=r.guard.indexOf("."),e=o===-1?r.guard:r.guard.slice(0,o),g=o===-1?"":r.guard.slice(o+1).split(".")[0];s[e]?g&&!s[e].includes(g)&&u(`route guard "${r.guard}": "${g}" is not a member of store "${e}"`):u(`route guard "${r.guard}": "${e}" is not a store \u2014 a guard reads a store boolean (e.g. \`guard auth.loggedIn else /login\`)`)}r.redirect&&!c.has(r.redirect)&&u(`route guard redirect "${r.redirect}" is not a declared route`)}}catch{}return n}export{O as validateStoresAndGuards};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
var
|
|
1
|
+
var m=(o=>(o.Ident="ident",o.String="string",o.Number="number",o.Ref="ref",o.Param="param",o.Punct="punct",o.Arrow="arrow",o.LArrow="larrow",o.Eq="eq",o.Neq="neq",o.Lte="lte",o.Gte="gte",o.FatArrow="fatarrow",o.Eof="eof",o))(m||{}),c=(a=>(a.BraceL="{",a.BraceR="}",a.ParenL="(",a.ParenR=")",a.BrackL="[",a.BrackR="]",a.Comma=",",a.Pipe="|",a.Colon=":",a.Assign="=",a.Lt="<",a.Gt=">",a.Dot=".",a.Slash="/",a.Plus="+",a.Star="*",a.Question="?",a.Dash="-",a))(c||{}),h=(e=>(e.Screen="screen",e.Entity="entity",e.State="state",e.Store="store",e.Get="get",e.Effect="effect",e.Action="action",e.Mutates="mutates",e.Mock="mock",e.Sources="sources",e.Routes="routes",e.Shell="shell",e.Part="part",e.Const="const",e.Theme="theme",e.Query="query",e.Param="param",e.Api="api",e.Body="body",e.Meta="meta",e.Use="use",e.From="from",e.Client="client",e.When="when",e.Each="each",e.As="as",e.Where="where",e.If="if",e.Else="else",e.Guard="guard",e.Not="not",e.And="and",e.Or="or",e.Contains="contains",e.Required="required",e.Min="min",e.Max="max",e.True="true",e.False="false",e.Null="null",e))(h||{}),x=(t=>(t.Shell="Shell",t.Header="Header",t.Nav="Nav",t.Sidebar="Sidebar",t.Footer="Footer",t.Page="Page",t.Stack="Stack",t.Text="Text",t.Title="Title",t.Span="Span",t.Image="Image",t.Link="Link",t.Button="Button",t.Form="Form",t.SearchField="SearchField",t.DataTable="DataTable",t.RowAction="RowAction",t.Custom="Custom",t.When="When",t.Each="Each",t.Slot="slot",t))(x||{}),f=(l=>(l.Module="module",l.Store="store",l.Html="html",l.Ssr="ssr",l))(f||{}),d=(u=>(u.Text="text",u.Email="email",u.Number="number",u.Bool="bool",u.Enum="enum",u))(d||{}),S=(n=>(n.Or="or",n.And="and",n.Eq="==",n.Neq="!=",n.Lte="<=",n.Gte=">=",n.Lt="<",n.Gt=">",n.Contains="contains",n.Add="+",n.Sub="-",n.Mul="*",n.Div="/",n))(S||{}),b=(i=>(i.Not="not",i))(b||{}),g=(r=>(r.Lit="lit",r.Ref="ref",r.Un="un",r.Bin="bin",r.Tern="tern",r.Interp="interp",r.Call="call",r.Obj="obj",r.Agg="agg",r))(g||{});const C=new Set(["sum","count","avg","min","max"]),q=new Set(["sort","sortDesc"]);var A=(s=>(s.Push="push",s.Set="set",s.Reset="reset",s.Remove="remove",s.Patch="patch",s.Create="create",s.Update="update",s.Delete="delete",s.Refetch="refetch",s.Request="request",s.Call="call",s.If="if",s))(A||{}),R=(r=>(r.Bind="bind",r.Submit="submit",r.Where="where",r.Columns="columns",r.Style="style",r.Class="class",r.Alt="alt",r.Inputs="inputs",r.On="on",r))(R||{});export{C as AGG_OPS,S as BOp,g as Ek,d as Fk,f as Fmt,h as Kw,R as Mod,x as Nt,c as Pn,q as SORT_OPS,A as StOp,m as Tk,b as UOp};
|
package/dist/lint.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{join as l,relative as b}from"node:path";import{readFileSync as P,existsSync as O}from"node:fs";import{readRoutes as
|
|
2
|
-
\u2716 ${o.length} problem(s)`:"\u2713 no problems"),o.length}export{
|
|
1
|
+
import{join as l,relative as b}from"node:path";import{readFileSync as P,existsSync as O}from"node:fs";import{readRoutes as D}from"#engine/project/routes.js";import{load as S,loadParts as j,findStores as k}from"#engine/project/load.js";import{validateStoresAndGuards as v}from"#engine/project/check-app.js";import{parse as w}from"#engine/lang/parse.js";import{toDoc as A}from"#engine/ir/flatten.js";import{validate as d}from"#engine/ir/validate.js";import{formatDiagnostic as m,ParseError as p}from"#engine/shared/diagnostics.js";async function q(r,i=!1){const n=s=>b(r,s),h=await j(l(r,"src","parts")),f=k(l(r,"src")),g=Object.keys(f),c={};for(const[s,e]of Object.entries(f))c[s]=[...Object.keys(e.state||{}),...Object.keys(e.gets||{}),...Object.keys(e.actions||{})];const u=D(r),o=[];for(const s of u){let e=[];try{const{doc:t,partNames:y}=await S(s.screenPath,h);e=d(t,{parts:y,stores:g,storeMembers:c}).diagnostics}catch(t){if(!(t instanceof p))throw t;e=[{code:t.code,severity:"error",message:t.message,loc:t.loc,suggestion:null}]}for(const t of e)i||console.log(m(t,n(s.screenPath))),o.push({file:n(s.screenPath),...t})}const a=l(r,"src","app.muten");if(O(a))try{const s=w(P(a,"utf8"));if(s.shell)for(const e of d(A({...s,tree:s.shell}),{stores:g,storeMembers:c}).diagnostics)i||console.log(m(e,n(a))),o.push({file:n(a),...e})}catch(s){if(!(s instanceof p))throw s}for(const s of v(r,f,c))i||console.log(m(s,s.file)),o.push(s);return console.log(i?JSON.stringify(o,null,2):o.length?`
|
|
2
|
+
\u2716 ${o.length} problem(s)`:"\u2713 no problems"),o.length}export{q as lintApp};
|
|
@@ -12,4 +12,4 @@ if (root) {
|
|
|
12
12
|
injectCss(__shell.css);
|
|
13
13
|
const outlet = __shell.mount(root);
|
|
14
14
|
route(outlet, routes);
|
|
15
|
-
}`},T=async()=>{w=await E(i);for(const s of Object.keys(c))delete c[s];if(I){S=H(l(i,"src"));for(const[s,o]of Object.entries(S))c[s]={state:Object.keys(o.state||{}),gets:Object.keys(o.gets||{}),actions:Object.keys(o.actions||{})}}const e=l(i,"src","app.muten");m=g(e)?b(f(e,"utf8")):void 0;const t=l(i,"theme.muten");p=g(t)?j(b(f(t,"utf8")).theme||{}):j(y.theme),d=null;for(const s of["styles.css","styles.scss"])if(g(l(i,"src",s))){d="/src/"+s;break}};let v=null;const _=e=>{v&&clearTimeout(v),v=setTimeout(()=>{T().then(()=>{for(const t of e.moduleGraph.idToModuleMap.values()){const s=t.id||"";(s.endsWith(".muten")||s.includes("virtual:muten/shell")||s.includes("virtual:muten/store/"))&&e.moduleGraph.invalidateModule(t)}e.ws.send({type:"full-reload"})})},30)};return{name:"vite-plugin-muten",enforce:"pre",async configResolved(e){i=e.root,await T()},resolveId(e){if(e===O||e===R||e.startsWith(h))return"\0"+e},load(e){if(e==="\0"+O)return U;if(e.startsWith("\0"+h)){const t=S[e.slice(("\0"+h).length)];if(t)return J({state:t.state||{},gets:t.gets||{},actions:t.actions||{},effects:t.effects||[],entities:t.entities||{}},t.mock||{},t.sources||{})}if(e==="\0"+R){const t=m?.shell||{type:$.Shell,props:{},children:[{type:$.Slot,props:{}}]},s=D({...m||{},screen:"shell",entities:{},state:{},actions:{},tree:t});return k(s,{},"",{},{},{stores:c,theme:p})}},async transform(e,t){if(!t.endsWith(".muten"))return null;if(t.replace(/\\/g,"/").endsWith("/src/app.muten"))return{code:N(),map:null};const s=await M(t,w),{ok:o,diagnostics:r}=F(s.doc,{parts:s.partNames,stores:Object.keys(c),theme:p});if(!o)throw new Error("muten: "+r.map(n=>n.message).join(" \xB7 "));const a=[...new Set(Object.values(s.doc.nodes).filter(n=>n.type===$.Custom).map(n=>n.props?.component))],u={};for(const n of a){if(!n)continue;const W=l(i,"src","components",n+".js");g(W)&&(u[n]=f(W,"utf8"))}return{code:k(s.doc,s.data,s.styles.css,u,s.sources,{stores:c,theme:p,api:m?.api||{}}),map:null}},handleHotUpdate(e){if(e.file.endsWith(".muten")||e.file.endsWith(".store"))return _(e.server),[]},configureServer(e){const t=s=>{const o=s.replace(/\\/g,"/");(o.endsWith(".muten")||o.endsWith(".store")||o.endsWith("/styles.css")||o.endsWith("/styles.scss")||o.includes("/components/")&&o.endsWith(".js"))&&_(e)};e.watcher.on("add",t),e.watcher.on("change",t),e.watcher.on("unlink",t),e.middlewares.use((s,o,r)=>{if((s.url||"").split("?")[0]!=="/src/app.muten"){r();return}e.transformRequest("/src/app.muten").then(a=>{if(!a){r();return}o.setHeader("Content-Type","text/javascript"),o.end(a.code)},r)})}}}export{V as default};
|
|
15
|
+
}`},T=async()=>{w=await E(i);for(const s of Object.keys(c))delete c[s];if(I){S=H(l(i,"src"));for(const[s,o]of Object.entries(S))c[s]={state:Object.keys(o.state||{}),gets:Object.keys(o.gets||{}),actions:Object.keys(o.actions||{})}}const e=l(i,"src","app.muten");m=g(e)?b(f(e,"utf8")):void 0;const t=l(i,"theme.muten");p=g(t)?j(b(f(t,"utf8")).theme||{}):j(y.theme),d=null;for(const s of["styles.css","styles.scss"])if(g(l(i,"src",s))){d="/src/"+s;break}};let v=null;const _=e=>{v&&clearTimeout(v),v=setTimeout(()=>{T().then(()=>{for(const t of e.moduleGraph.idToModuleMap.values()){const s=t.id||"";(s.endsWith(".muten")||s.includes("virtual:muten/shell")||s.includes("virtual:muten/store/"))&&e.moduleGraph.invalidateModule(t)}e.ws.send({type:"full-reload"})})},30)};return{name:"vite-plugin-muten",enforce:"pre",async configResolved(e){i=e.root,await T()},resolveId(e){if(e===O||e===R||e.startsWith(h))return"\0"+e},load(e){if(e==="\0"+O)return U;if(e.startsWith("\0"+h)){const t=S[e.slice(("\0"+h).length)];if(t)return J({state:t.state||{},gets:t.gets||{},actions:t.actions||{},effects:t.effects||[],entities:t.entities||{},imports:t.imports||[]},t.mock||{},t.sources||{})}if(e==="\0"+R){const t=m?.shell||{type:$.Shell,props:{},children:[{type:$.Slot,props:{}}]},s=D({...m||{},screen:"shell",entities:{},state:{},actions:{},tree:t});return k(s,{},"",{},{},{stores:c,theme:p})}},async transform(e,t){if(!t.endsWith(".muten"))return null;if(t.replace(/\\/g,"/").endsWith("/src/app.muten"))return{code:N(),map:null};const s=await M(t,w),{ok:o,diagnostics:r}=F(s.doc,{parts:s.partNames,stores:Object.keys(c),theme:p});if(!o)throw new Error("muten: "+r.map(n=>n.message).join(" \xB7 "));const a=[...new Set(Object.values(s.doc.nodes).filter(n=>n.type===$.Custom).map(n=>n.props?.component))],u={};for(const n of a){if(!n)continue;const W=l(i,"src","components",n+".js");g(W)&&(u[n]=f(W,"utf8"))}return{code:k(s.doc,s.data,s.styles.css,u,s.sources,{stores:c,theme:p,api:m?.api||{}}),map:null}},handleHotUpdate(e){if(e.file.endsWith(".muten")||e.file.endsWith(".store"))return _(e.server),[]},configureServer(e){const t=s=>{const o=s.replace(/\\/g,"/");(o.endsWith(".muten")||o.endsWith(".store")||o.endsWith("/styles.css")||o.endsWith("/styles.scss")||o.includes("/components/")&&o.endsWith(".js"))&&_(e)};e.watcher.on("add",t),e.watcher.on("change",t),e.watcher.on("unlink",t),e.middlewares.use((s,o,r)=>{if((s.url||"").split("?")[0]!=="/src/app.muten"){r();return}e.transformRequest("/src/app.muten").then(a=>{if(!a){r();return}o.setHeader("Content-Type","text/javascript"),o.end(a.code)},r)})}}}export{V as default};
|
package/grammar/muten.gbnf
CHANGED
|
@@ -29,7 +29,7 @@ stmt ::= ifstmt | requeststmt | callstmt
|
|
|
29
29
|
ifstmt ::= "if" sp expr ws actionbody (ws "else" ws actionbody)?
|
|
30
30
|
requeststmt ::= ("post" | "put" | "delete") sp string (sp "body" sp expr)?
|
|
31
31
|
callstmt ::= ident "." actionop "(" ws callargs ws ")"
|
|
32
|
-
actionop ::= "push" | "remove" | "reset" | "set" | "create" | "update" | "delete" | "refetch"
|
|
32
|
+
actionop ::= "push" | "remove" | "patch" | "reset" | "set" | "create" | "update" | "delete" | "refetch"
|
|
33
33
|
callargs ::= refetcharg (ws "," ws refetcharg)* | predarg | expr | ""
|
|
34
34
|
refetcharg ::= ident ws ":" ws expr
|
|
35
35
|
predarg ::= ident ws "=>" ws expr
|