@muten/core 0.0.13 → 0.0.14
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 +22 -53
- package/dist/build.js +12 -12
- package/dist/engine/compile/compile.js +8 -9
- package/dist/engine/compile/emit.js +9 -3
- package/dist/engine/compile/logic.js +3 -3
- package/dist/engine/ir/compose.js +1 -1
- package/dist/engine/ir/print.js +16 -16
- package/dist/engine/ir/validate.js +1 -1
- package/dist/engine/lang/grammar.js +2 -2
- package/dist/engine/lang/manifest.js +1 -1
- package/dist/engine/lang/parse.js +1 -1
- package/dist/engine/project/ssr.js +1 -1
- package/dist/engine/shared/vocab.js +1 -1
- package/grammar/muten.gbnf +7 -4
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -37,13 +37,13 @@ action add mutates products, draft <- p {
|
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
Page style(column, gap.md) {
|
|
40
|
-
Form bind
|
|
40
|
+
Form bind(draft) submit(add) "Add product"
|
|
41
41
|
|
|
42
|
-
each products.sortDesc
|
|
42
|
+
each products.sortDesc by price as p { # render the list, sorted
|
|
43
43
|
Text "{p.name} - ${p.price}"
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
Text "Total: ${products.sum
|
|
46
|
+
Text "Total: ${products.sum by price}" # a live aggregate, no JS
|
|
47
47
|
}
|
|
48
48
|
```
|
|
49
49
|
|
|
@@ -99,12 +99,22 @@ that needs the full React ecosystem, and it doesn't pretend to be.
|
|
|
99
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
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
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 |
|
|
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: …)` · **`query x live`** (WebSocket real-time: the server pushes, only changed rows re-render) · a `post`/`put`/`delete` escape for non-REST |
|
|
103
103
|
| **Routing** | real-path URLs · params (`/product/:id` → `param id`) · route guards · a `/404` catch-all |
|
|
104
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
|
|
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 |
|
|
106
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
107
|
|
|
108
|
+
## Reactivity & reconciliation
|
|
109
|
+
|
|
110
|
+
muten is **Solid's fine-grained signals + Svelte's compile-to-direct-DOM**, with **no virtual DOM**:
|
|
111
|
+
|
|
112
|
+
- **Reads subscribe, writes notify.** Each `{count}`, `class(active when x)`, `when`, `each` compiles to its own tiny effect that reads exactly the signals it needs - when one changes, only that spot updates, never a re-render of the tree.
|
|
113
|
+
- **Lists reconcile by `id`** (never by index): `each`/`DataTable` keep a per-row signal, so on new data only the rows whose fields changed touch the DOM (the rest are reused or moved in place), and removed rows dispose their effects - no leaks, no zombies. Focus, scroll and input survive live updates.
|
|
114
|
+
- **Updates batch** into one microtask, the way Solid does: a burst of writes in a tick re-renders each spot **once**, not per write - so a real-time feed (a `live` socket) costs one render per frame, not one per message.
|
|
115
|
+
|
|
116
|
+
No diffing, no virtual-DOM memory, no framework interpreter to ship. That is what keeps muten fast as apps grow; for *huge* lists you still virtualize (render only the visible rows) and send server-side deltas, exactly as you would in any framework.
|
|
117
|
+
|
|
108
118
|
## How muten couples with the rest of the web - three tiers
|
|
109
119
|
|
|
110
120
|
muten the *language* stays tiny on purpose; a muten *app* reaches the whole web platform through **bounded,
|
|
@@ -115,24 +125,20 @@ right tier, and the compiler still checks the seam. Reach for the **lowest tier
|
|
|
115
125
|
|---|---|---|---|
|
|
116
126
|
| **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
127
|
| **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
128
|
|
|
122
|
-
|
|
123
|
-
> and every escape is *bounded*: the oracle still checks the border, so the language never grows into a UI kit.
|
|
129
|
+
Almost every "hard widget" lands at **tier 2**. The language stays small by design; anything beyond pure muten + platform escapes lives in ordinary JS.
|
|
124
130
|
|
|
125
|
-
**Why the escapes stay safe.** The compiler validates the *seam* - the
|
|
126
|
-
crossing into a `Custom
|
|
127
|
-
coupling in chart.js
|
|
131
|
+
**Why the escapes stay safe.** The compiler validates the *seam* - the `state` props and `action` callbacks
|
|
132
|
+
crossing into a `Custom`, and the call site of a `use` function (an undeclared one is a `check` error). So
|
|
133
|
+
coupling in chart.js or zod never costs you the oracle on the muten side.
|
|
128
134
|
|
|
129
|
-
**Deploy - the honest caveat.** A `use` function
|
|
135
|
+
**Deploy - the honest caveat.** A `use` function ships real JS that the static `muten build` does
|
|
130
136
|
**not** bundle:
|
|
131
137
|
|
|
132
138
|
| Your app uses… | Deploy with |
|
|
133
139
|
|---|---|
|
|
134
140
|
| Pure muten, static content | `muten build` (zero-JS HTML) - or `vite build` |
|
|
135
|
-
| `use` JS functions
|
|
141
|
+
| `use` JS functions or shared cross-page state | **`vite build`** (it bundles them; the static build doesn't) |
|
|
136
142
|
|
|
137
143
|
`npm run dev` runs every tier regardless - this only affects the production *build*.
|
|
138
144
|
|
|
@@ -229,54 +235,17 @@ muten imposes no theme. A page lays itself out with `style(…)` tokens (analyza
|
|
|
229
235
|
`theme.muten`) and skins itself via `class("…")` (your CSS / Tailwind / anything). For behavior the
|
|
230
236
|
primitives can't express, drop to a `Custom` component (`src/components/<Name>.js`).
|
|
231
237
|
|
|
232
|
-
## Islands - Svelte & React
|
|
233
|
-
|
|
234
|
-
When a page needs a genuinely interactive widget or a framework UI lib muten can't express, mount a real
|
|
235
|
-
Svelte/React component as an **island**. The `svelte:` / `react:` prefix on `use … from` is the only marker;
|
|
236
|
-
the component file is plain Svelte/React and owns its own tooling.
|
|
237
|
-
|
|
238
|
-
```
|
|
239
|
-
screen home
|
|
240
|
-
|
|
241
|
-
use Counter from "svelte:./Counter.svelte" # a Svelte island
|
|
242
|
-
use Likes from "react:./Likes.jsx" # a React island
|
|
243
|
-
|
|
244
|
-
state { total = 10 : number }
|
|
245
|
-
action setTotal mutates total <- n { total.set(n) }
|
|
246
|
-
|
|
247
|
-
Page style(padding.xl, gap.md) {
|
|
248
|
-
Counter(start: @total, onChange: setTotal) # props ↓ as signals, events ↑ to actions
|
|
249
|
-
Likes(start: @total, onLike: setTotal) client:visible # code-split, hydrated when scrolled into view
|
|
250
|
-
Text "muten state ← islands: {total}"
|
|
251
|
-
}
|
|
252
|
-
```
|
|
253
|
-
|
|
254
|
-
`prop: @state` sends a value **down** (a React island re-renders when the signal changes; Svelte mounts once);
|
|
255
|
-
`onX: action` sends a callback that fires a muten action - that's how an island writes **back** to muten state.
|
|
256
|
-
No `client:` directive = hydrate on load. Add the framework's Vite plugin next to `muten()`:
|
|
257
|
-
|
|
258
|
-
```js
|
|
259
|
-
// vite.config.mjs
|
|
260
|
-
import muten from '@muten/core/vite-plugin-muten.js';
|
|
261
|
-
import { svelte } from '@sveltejs/vite-plugin-svelte';
|
|
262
|
-
import react from '@vitejs/plugin-react';
|
|
263
|
-
export default { plugins: [muten(), svelte(), react()] };
|
|
264
|
-
```
|
|
265
|
-
|
|
266
238
|
## Status & roadmap (honest)
|
|
267
239
|
|
|
268
240
|
**Pre-1.0 - the core is solid, the edges are young.** Build real apps with it; don't bet a critical
|
|
269
241
|
production system on it yet (small ecosystem, one maintainer, not yet battle-tested).
|
|
270
242
|
|
|
271
243
|
**Solid today:** the language + compiler, the `check` / `build` / `map` CLI + oracle, the Vite plugin + dev
|
|
272
|
-
server + HMR, the VS Code extension (live-lint + autocomplete)
|
|
244
|
+
server + HMR, the VS Code extension (live-lint + autocomplete).
|
|
273
245
|
The bounded list toolkit - inline objects, `patch`, `each…where`, aggregates (`sum`/`count`/`avg`/`min`/`max`),
|
|
274
246
|
`sort`/`sortDesc`, and page→store action composition, so a real CRUD/dashboard app is pure muten, no JS escape.
|
|
275
247
|
`Form` fields cover `text` · `number` (coerced) · `email` · `bool` (checkbox) · `enum` (select), with validation.
|
|
276
248
|
|
|
277
|
-
**Experimental:** full island **SSR**: `muten build` server-renders an island's HTML (first paint + SEO),
|
|
278
|
-
but client hydration of that island still needs its framework bundled (pair the SSG HTML with the Vite client build).
|
|
279
|
-
|
|
280
249
|
**Next, toward 1.0:**
|
|
281
250
|
- a `date`/`textarea` `Form` field type; number formatting (`round` / currency) in expressions.
|
|
282
251
|
- keyed `each` (large-list perf); a live `source` (SSE / websocket) for real-time.
|
package/dist/build.js
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
|
-
import{writeFileSync as
|
|
2
|
-
`));const
|
|
3
|
-
${
|
|
4
|
-
return { ${
|
|
1
|
+
import{writeFileSync as O,mkdirSync as N,readFileSync as B,existsSync as G,rmSync as H}from"node:fs";import{join as n,relative as J}from"node:path";import{Nt as U,Fmt as q}from"#engine/shared/vocab.js";import{readRoutes as z,readApi as K}from"#engine/project/routes.js";import{renderSsrBody as L,fetchSources as Q}from"#engine/project/ssr.js";import{routeEntry as V}from"#engine/project/map.js";import{load as W,loadAllParts as X,findStores as Y}from"#engine/project/load.js";import{validateStoresAndGuards as Z}from"#engine/project/check-app.js";import{validate as D}from"#engine/ir/validate.js";import{compile as F,compileStore as R}from"#engine/compile/compile.js";import{formatDiagnostic as k,ParseError as ee}from"#engine/shared/diagnostics.js";async function ue(o,r=n(o,"dist")){const c=e=>J(o,e);H(r,{recursive:!0,force:!0});const d=await X(o);Object.keys(d).length&&console.log(`Parts: ${Object.keys(d).join(", ")}`);const l=Y(n(o,"src")),v=Object.keys(l),u={};for(const[e,t]of Object.entries(l))u[e]=[...Object.keys(t.state||{}),...Object.keys(t.gets||{}),...Object.keys(t.actions||{})];const j={};for(const[e,t]of Object.entries(l))j[e]={state:Object.keys(t.state||{}),gets:Object.keys(t.gets||{}),actions:Object.keys(t.actions||{})};const w=Z(o,l,u);if(w.length)throw new Error(w.map(e=>k(e,e.file)).join(`
|
|
2
|
+
`));const P=Object.entries(l).map(([e,t])=>{const p=R({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
|
+
${p}
|
|
4
|
+
return { ${u[e].join(", ")} };
|
|
5
5
|
})();`}).join(`
|
|
6
|
-
`),
|
|
7
|
-
`);const
|
|
8
|
-
`+
|
|
9
|
-
`+
|
|
10
|
-
`));const
|
|
11
|
-
\u26A0 This app shares a .store across ${
|
|
12
|
-
`);
|
|
6
|
+
`),f=z(o),b=K(o);console.log(`Host app: ${o}`),console.log(`Pages: ${f.map(e=>"/"+e.route).join(", ")}
|
|
7
|
+
`);const g=[],A={app:o.split(/[\\/]/).pop()||"",parts:Object.keys(d),routes:{}};for(const e of f){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 W(e.screenPath,d)}catch(s){if(!(s instanceof ee))throw s;const a={code:s.code,severity:"error",message:s.message,loc:s.loc,suggestion:null};throw new Error(`/${e.route}
|
|
8
|
+
`+k(a,c(e.screenPath)))}const{doc:i,data:p,sources:m,styles:h,partNames:C}=t,x=(i.imports||[]).flatMap(s=>s.names);x.length&&console.log(` \u26A0 /${e.route}: \`use\` function(s) ${x.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:T,diagnostics:_}=D(i,{parts:C,stores:v,storeMembers:u});if(!T)throw new Error(`/${e.route}
|
|
9
|
+
`+_.map(s=>" "+k(s,c(e.screenPath))).join(`
|
|
10
|
+
`));const I=[...new Set(Object.values(i.nodes).filter(s=>s.type===U.Custom).map(s=>s.props?.component))],$={};for(const s of I){if(!s)continue;const a=n(o,"src","components",s+".js");if(!G(a))throw new Error(`/${e.route}: Custom component not found: src/components/${s}.js`);$[s]=B(a,"utf8")}const y=F(i,p,h.css,$,m,{api:b,stores:j,storeCode:P});let E=y,M=!1;if(y.includes('<div id="app"></div>'))try{const s=Object.keys(m).length?{...p,...await Q(m,b)}:p,a=L(F(i,s,h.css,$,m,{format:q.Ssr,api:b,stores:j,storeCode:P}));E=y.replace('<div id="app"></div>',`<div id="app">${a}</div>`),M=!0}catch{}const S=n(r,e.route);N(S,{recursive:!0}),O(n(S,"index.html"),E),console.log(`\u2713 /${e.route} \u2192 ${c(n(S,"index.html"))} (${Object.keys(i.nodes).length} nodes${M?", SSR":y.includes("<script")?", CSR":", static"}${h.from?", + "+h.from:""})`),g.push(e.route),A.routes["/"+e.route]=V(c(e.screenPath),i,m)}if(f.length>1&&v.length&&console.log(`
|
|
11
|
+
\u26A0 This app shares a .store across ${f.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\`.`),N(r,{recursive:!0}),!g.includes("")){const e=g.map(t=>`<li><a href="./${t}/">/${t}</a></li>`).join(`
|
|
12
|
+
`);O(n(r,"index.html"),`<!doctype html><meta charset="utf-8"><title>app</title>
|
|
13
13
|
<h1>Routes</h1>
|
|
14
14
|
<ul>
|
|
15
15
|
${e}
|
|
16
16
|
</ul>
|
|
17
17
|
`),console.log(`
|
|
18
|
-
\u2713 ${
|
|
18
|
+
\u2713 ${c(n(r,"index.html"))} \u2192 route index`)}return O(n(r,"app.map.json"),JSON.stringify(A,null,2)),console.log(`\u2713 ${c(n(r,"app.map.json"))} \u2192 app graph (the root the AI reads)`),{routes:g,outDir:r}}export{ue as buildApp};
|
|
@@ -1,13 +1,12 @@
|
|
|
1
|
-
import{tokenClass as ne,resolveToken as Ne,defaultTheme as ke}from"#engine/style/tokens.js";import{Nt as l,Ek as Ce,Fmt as S,Fk as v}from"#engine/shared/vocab.js";import{customValue as se,CONTAINERS as oe,parseClause as Ee,editableFields as xe}from"#engine/compile/helpers.js";import{emitStore as Oe,emitStatic as Je,emitStaticHtml as je,emitSsr as Ie,emitModule as Le,emitHtml as Te}from"#engine/compile/emit.js";import{Logic as De}from"#engine/compile/logic.js";function Be(m,J={},j="",I={},L={},$={}){return re(m,J,j,I,L,{...$,format:S.Module})}function Me(m={},J={},j={}){const{state:I={},gets:L={},actions:$={},effects:b=[],entities:C={},imports:k=[]}=m;return re({screen:"store",entities:C,state:I,actions:$,gets:L,effects:b,imports:k,consts:{},constraints:{},rootId:void 0,nodes:{}},J,"",{},j,{format:S.Store})}function re(m,J={},j="",I={},L={},$={}){const{nodes:b,rootId:C,state:k,entities:A,screen:ae}=m,H=$.theme||ke,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 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},z=new Set,d=(e,s)=>{for(const o of s.style||[])z.add(o);return[e,...(s.style||[]).map(ne),...(s.class||[]).filter(o=>typeof o=="string")].join(" ")},ie=new Set(Object.keys(k)),U=new Set(Object.entries(k).filter(([,e])=>typeof e.source=="string"&&e.source.startsWith("query:")).map(([e])=>e)),G=new Set,le={state:k,entities:A,actions:m.actions,consts:m.consts||{},gets:m.gets||{},effects:m.effects||[],stateKeys:ie,queryStates:U,stores:$.stores||{},usedStores:G,params:new Set(m.params||[]),format:$.format},p=new De(le),y={locals:new Set},E=(e,s)=>{for(const o of s.class||[])typeof o!="string"&&t.push(`effect(() => el_${e}.classList.toggle(${JSON.stringify(o.name)}, !!(${p.compileExpr(o.cond,y)})));`);for(const[o,n]of Object.entries(s.on||{}))typeof n=="string"&&t.push(`el_${e}.addEventListener(${JSON.stringify(o)}, () => ${p.actionRef(n)}());`)},T=(e,s)=>{for(const o of b[e].children)V(o,s)},Q=e=>e.parts.map(s=>typeof s=="string"?JSON.stringify(s):`String(${p.compileExpr(s,y)} ?? '')`).join(" + ");function B(e,s,o,n,u){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=Q(n),i=n.parts.some(_=>typeof _!="string");t.push(i?`effect(() => { el_${e}.textContent = ${c}; });`:`el_${e}.textContent = ${c};`)}t.push(`${u}.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=Q(o),u=o.parts.some(c=>typeof c!="string");t.push(u?`effect(() => { el_${e}.${s} = ${n}; });`:`el_${e}.${s} = ${n};`)}}function V(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 i="{ "+Object.entries(o.args||{}).map(([h,g])=>typeof g=="string"&&!g.startsWith("@")&&(g in(m.actions||{})||g.includes("."))?`${JSON.stringify(h)}: (...__a) => ${p.actionRef(g)}(...__a)`:`${JSON.stringify(h)}: ${se(g)}`).join(", ")+" }";if($.format===S.Ssr){t.push(`el_${e}.innerHTML = __ssrIsland(${JSON.stringify(c.adapter)}, ${JSON.stringify(c.path)}, ${i});`);return}const _=ce[c.adapter](`el_${e}`,i,c.path);t.push(n.hydrate==="visible"?`__onVisible(el_${e}, () => ${_});`:n.hydrate==="idle"?`__onIdle(() => ${_});`:`${_};`);return}const u=oe[o.type];if(u){const[c,i]=u;t.push(`const el_${e} = document.createElement('${c}');`),t.push(`el_${e}.className = ${JSON.stringify(d(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});`),E(e,n),T(e,`el_${e}`);return}switch(o.type){case l.SearchField:{const c=p.bindSig(n.bind);t.push(`const el_${e} = document.createElement('input');`),t.push(`el_${e}.type = 'search';`),t.push(`el_${e}.className = ${JSON.stringify(d("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=p.bindSig(n.data),i=U.has(c)?`${c}.get().data`:`${c}.get()`,_=n.columns||[],h=(n.where||[]).map(Ee),g=h.filter(a=>!a.dynamic).map(a=>`.filter((row) => ${a.expr})`).join(""),N=h.filter(a=>a.dynamic).map(a=>`.filter((row) => ${a.expr})`).join(""),r=o.children.map(a=>b[a]).filter(a=>a.type===l.RowAction);t.push(`const el_${e} = document.createElement('table');`),t.push(`el_${e}.className = ${JSON.stringify(d("datatable",n))};`),t.push(`const head_${e} = el_${e}.createTHead().insertRow();`);for(const a of _)t.push(`{ const th = document.createElement('th'); th.textContent = ${JSON.stringify(a)}; 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 a of _)t.push(` { const td = document.createElement('td'); effect(() => { td.textContent = (row.get()[${JSON.stringify(a)}] ?? ''); }); tr.appendChild(td); }`);for(const a of r){const f=a.props||{},we=f.arg!==void 0?p.compileExpr(f.arg,{locals:new Set,sigLocals:new Set(["row"])}):"";t.push(` { const td = document.createElement('td'); const b = document.createElement('button'); b.className = ${JSON.stringify(d("row-action",f))}; b.textContent = ${JSON.stringify(f.label)}; b.addEventListener('click', () => ${p.actionRef(f.action)}(${we})); td.appendChild(b); tr.appendChild(td); }`)}t.push(" return tr;"),t.push("}"),t.push(`function base_${e}() { return ${i}${g}; }`),t.push(`const start_${e} = document.createComment('rows');`),t.push(`const anchor_${e} = document.createComment('/rows');`),t.push(`body_${e}.appendChild(start_${e}); body_${e}.appendChild(anchor_${e});`),t.push(`const map_${e} = new Map();`),t.push(`onCleanup(() => { for (const __e of map_${e}.values()) __e.dispose(); map_${e}.clear(); }); // parent unmount \u2192 tear down every row`),t.push("effect(() => {"),t.push(` const __rows = base_${e}()${N};`),t.push(" const __seen = new Set();"),t.push(` let __prev = start_${e};`),t.push(" for (const __row of __rows) {"),t.push(" const __k = __row?.id ?? __row; __seen.add(__k); // key by id (entities) or the value itself (scalars) \u2014 never index"),t.push(` let __e = map_${e}.get(__k);`),t.push(" if (__e) { if (!__eq(__e.data, __row)) { __e.data = __row; __e.sig.set(__row); } }"),t.push(` else { const __sig = signal(__row); const __r = root(() => [renderRow_${e}(__sig)]); __e = { sig: __sig, nodes: __r.value, dispose: __r.dispose, data: __row }; map_${e}.set(__k, __e); }`),t.push(` for (const __n of __e.nodes) { if (__prev.nextSibling !== __n) anchor_${e}.parentNode.insertBefore(__n, __prev.nextSibling); __prev = __n; }`),t.push(" }"),t.push(` for (const [__k, __e] of map_${e}) if (!__seen.has(__k)) { __e.dispose(); for (const __n of __e.nodes) __n.remove(); map_${e}.delete(__k); }`),t.push("});");break}case l.Form:{const c=p.bindSig(n.bind),i=k[c]?.type;if(!i||!A[i])throw new Error(`Form must bind a page-local entity draft, not "${n.bind}"`);const _=xe(A[i]),h=(m.constraints||{})[i]||{};t.push(`const el_${e} = document.createElement('form');`),t.push(`el_${e}.className = ${JSON.stringify(d("form",n))};`),t.push(`{ const t = document.createElement('div'); t.className = 'form-title'; t.textContent = ${JSON.stringify("New "+i)}; el_${e}.appendChild(t); }`);const g=[];for(const r of _){const a=`f_${e}_${r.name}`;if(g.push({...r,var:a,c:h[r.name]}),r.kind===v.Enum){t.push(`const ${a} = document.createElement('select');`),t.push(`${a}.className = 'field';`);for(const f of r.options)t.push(`{ const o = document.createElement('option'); o.value = ${JSON.stringify(f)}; o.textContent = ${JSON.stringify(f)}; ${a}.appendChild(o); }`)}else r.kind===v.Bool?(t.push(`const ${a} = document.createElement('input');`),t.push(`${a}.type = 'checkbox';`),t.push(`${a}.className = 'field-check';`)):(t.push(`const ${a} = document.createElement('input');`),t.push(`${a}.type = ${JSON.stringify(r.kind===v.Email?"email":r.kind===v.Number?"number":"text")};`),t.push(`${a}.className = 'field';`),t.push(`${a}.placeholder = ${JSON.stringify(r.name)};`));if(r.kind===v.Bool)t.push(`${a}.addEventListener('change', (e) => ${c}.set({ ...${c}.get(), ${JSON.stringify(r.name)}: e.target.checked }));`);else{const f=r.kind===v.Number?"(Number(e.target.value) || 0)":"e.target.value";t.push(`${a}.addEventListener('input', (e) => ${c}.set({ ...${c}.get(), ${JSON.stringify(r.name)}: ${f} }));`)}t.push(`el_${e}.appendChild(${a});`),h[r.name]&&t.push(`const err_${a} = document.createElement('small'); err_${a}.className = 'field-error'; el_${e}.appendChild(err_${a});`)}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 r of g){if(!r.c)continue;const a=`err_${r.var}`,f=`String(__d[${JSON.stringify(r.name)}] ?? '')`;N.push(`${a}.textContent = '';`),r.c.required&&N.push(`if (!${f}.trim()) { ${a}.textContent = 'Required'; __ok = false; }`),r.c.min!=null&&N.push(r.kind===v.Number?`if (${f} !== '' && Number(${f}) < ${r.c.min}) { ${a}.textContent = 'Min ${r.c.min}'; __ok = false; }`:`if (${f} && ${f}.length < ${r.c.min}) { ${a}.textContent = 'Min ${r.c.min} characters'; __ok = false; }`),r.c.max!=null&&N.push(r.kind===v.Number?`if (${f} !== '' && Number(${f}) > ${r.c.max}) { ${a}.textContent = 'Max ${r.c.max}'; __ok = false; }`:`if (${f}.length > ${r.c.max}) { ${a}.textContent = 'Max ${r.c.max} characters'; __ok = false; }`)}N.length?t.push(`el_${e}.addEventListener('submit', (e) => { e.preventDefault(); const __d = ${c}.get(); let __ok = true; ${N.join(" ")} if (__ok) ${p.actionRef(n.submit)}(__d); });`):t.push(`el_${e}.addEventListener('submit', (e) => { e.preventDefault(); ${p.actionRef(n.submit)}(${c}.get()); });`),t.push("effect(() => {"),t.push(` const d = ${c}.get();`);for(const r of g){if(r.kind===v.Bool){t.push(` { const v = !!d[${JSON.stringify(r.name)}]; if (${r.var}.checked !== v) ${r.var}.checked = v; }`);continue}const a=r.kind===v.Enum?JSON.stringify(r.options[0]):"''";t.push(` { const v = d[${JSON.stringify(r.name)}] ?? ${a}; if (${r.var}.value !== v) ${r.var}.value = v; }`)}t.push("});"),t.push(`${s}.appendChild(el_${e});`);break}case l.Text:B(e,"p",d("text",n),n.value,s),E(e,n);break;case l.Span:B(e,"span",d("span",n),n.value,s),E(e,n);break;case l.Title:B(e,n.level||"h1",d("title",n),n.value,s),E(e,n);break;case l.Image:{t.push(`const el_${e} = document.createElement('img');`),t.push(`el_${e}.className = ${JSON.stringify(d("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 c=p.compileExpr(n.cond,y),i=K(()=>T(e,"__p"));t.push(`function build_${e}(__p) {`);for(const _ of i)t.push(" "+_);t.push("}"),t.push(`const anchor_${e} = document.createComment('when');`),t.push(`${s}.appendChild(anchor_${e});`),t.push(`let shown_${e} = null; // { value: nodes, dispose } while mounted, else null \u2014 dispose kills the block's effects on unmount (no zombies)`),t.push(`onCleanup(() => { if (shown_${e}) shown_${e}.dispose(); }); // parent unmount \u2192 tear down the mounted block too`),t.push("effect(() => {"),t.push(` if (${c}) {`),t.push(` if (!shown_${e}) { const __r = root(() => { const __f = document.createDocumentFragment(); build_${e}(__f); return [...__f.childNodes]; }); for (const __n of __r.value) anchor_${e}.parentNode.insertBefore(__n, anchor_${e}); shown_${e} = __r; }`),t.push(` } else if (shown_${e}) { shown_${e}.dispose(); for (const __n of shown_${e}.value) __n.remove(); shown_${e} = null; }`),t.push("});");break}case l.Each:{if(!n.list||!n.as)throw new Error("each without a list or item variable");const c=p.compileExpr(n.list,y),i=n.filter?p.compileExpr(n.filter,y):"",_=y.sigLocals;y.sigLocals=new Set([..._||[],n.as]);const h=K(()=>T(e,"__p"));y.sigLocals=_,t.push(`function buildItem_${e}(__p, ${n.as}) {`);for(const g of h)t.push(" "+g);t.push("}"),t.push(`const start_${e} = document.createComment('each');`),t.push(`const anchor_${e} = document.createComment('/each');`),t.push(`${s}.appendChild(start_${e}); ${s}.appendChild(anchor_${e});`),t.push(`const map_${e} = new Map(); // row id \u2192 { sig, nodes, dispose, data }`),t.push(`onCleanup(() => { for (const __e of map_${e}.values()) __e.dispose(); map_${e}.clear(); }); // parent unmount \u2192 tear down every row (no leaked effects)`),t.push("effect(() => {"),t.push(` const __rows = (${c} ?? [])${i?`.filter((${n.as}) => ${i})`:""};`),t.push(" const __seen = new Set();"),t.push(` let __prev = start_${e};`),t.push(" for (const __row of __rows) {"),t.push(" const __k = __row?.id ?? __row; __seen.add(__k); // key by id (entities) or the value itself (scalars) \u2014 never index"),t.push(` let __e = map_${e}.get(__k);`),t.push(" if (__e) { if (!__eq(__e.data, __row)) { __e.data = __row; __e.sig.set(__row); } } // same row, changed data \u2192 granular update"),t.push(` else { const __sig = signal(__row); const __r = root(() => { const __f = document.createDocumentFragment(); buildItem_${e}(__f, __sig); return [...__f.childNodes]; }); __e = { sig: __sig, nodes: __r.value, dispose: __r.dispose, data: __row }; map_${e}.set(__k, __e); } // new row`),t.push(` for (const __n of __e.nodes) { if (__prev.nextSibling !== __n) anchor_${e}.parentNode.insertBefore(__n, __prev.nextSibling); __prev = __n; } // order: move only if out of place`),t.push(" }"),t.push(` for (const [__k, __e] of map_${e}) if (!__seen.has(__k)) { __e.dispose(); for (const __n of __e.nodes) __n.remove(); map_${e}.delete(__k); } // gone \u2192 dispose effects + remove nodes`),t.push("});");break}case l.Custom:{t.push(`const el_${e} = document.createElement('div');`),t.push(`el_${e}.className = ${JSON.stringify(d("custom",n))};`),t.push(`${s}.appendChild(el_${e});`);const c=Object.entries(n.inputs||{}).map(([_,h])=>`${JSON.stringify(_)}: ${se(h)}`).join(", "),i=Object.entries(n.on||{}).map(([_,h])=>`${JSON.stringify(_)}: (...__a) => ${p.actionRef(typeof h=="string"?h:"")}(...__a)`).join(", ");t.push(`if (typeof __custom_${n.component} === 'function') __custom_${n.component}(el_${e}, { ${c} }, { ${i} });`);break}case l.Button:{if(t.push(`const el_${e} = document.createElement('button');`),t.push(`el_${e}.className = ${JSON.stringify(d("button",n))};`),o.children&&o.children.length?T(e,`el_${e}`):n.label!==void 0&&D(e,"textContent",n.label),n.action){const c=n.arg!==void 0?p.compileExpr(n.arg,y):"";t.push(`el_${e}.addEventListener('click', () => ${p.actionRef(n.action)}(${c}));`)}E(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(d("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),E(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,"""),w=e=>typeof e=="string"?e:"";function _e(){if($.format===S.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(b)){const u=b[n],c=u.props||{};if(P[u.type]||e.has(u.type)||s.some(i=>c[i]!==void 0)||(c.class||[]).some(i=>typeof i!="string")||o.some(i=>{const _=c[i];return!!_&&typeof _=="object"&&"kind"in _&&_.kind===Ce.Interp}))return!1}return!0}function X(e){const s=b[e],o=s.props||{},n=()=>(b[e].children||[]).map(X).join(""),u=i=>` class="${q(d(i,o))}"`,c=oe[s.type];if(c){const[i,_]=c;return`<${i}${u(_)}>${n()}</${i}>`}switch(s.type){case l.Text:return`<p${u("text")}>${x(w(o.value))}</p>`;case l.Span:return`<span${u("span")}>${x(w(o.value))}</span>`;case l.Title:{const i=o.level||"h1";return`<${i}${u("title")}>${x(w(o.value))}</${i}>`}case l.Image:return`<img${u("image")} src="${q(w(o.src))}" alt="${q(w(o.alt))}">`;case l.Link:return`<a${u("link")} href="${q(w(o.to)||"/")}">${s.children&&s.children.length?n():x(w(o.label))}</a>`;case l.Button:return`<button${u("button")}>${s.children&&s.children.length?n():x(w(o.label))}</button>`;default:return""}}const Y=$.format===S.Ssr?!1:_e(),pe=(m.params||[]).map(e=>`const ${e} = (__params || {})[${JSON.stringify(e)}] ?? '';`).join(`
|
|
2
|
-
`),
|
|
3
|
-
`)
|
|
4
|
-
`),
|
|
5
|
-
`),
|
|
1
|
+
import{tokenClass as V,resolveToken as ye,defaultTheme as Se}from"#engine/style/tokens.js";import{Nt as l,Ek as we,Fmt as b,Fk as y}from"#engine/shared/vocab.js";import{customValue as ve,CONTAINERS as ee,parseClause as ke,editableFields as Ne}from"#engine/compile/helpers.js";import{emitStore as Ce,emitStatic as xe,emitStaticHtml as Ee,emitSsr as Oe,emitModule as Je,emitHtml as je}from"#engine/compile/emit.js";import{Logic as Le}from"#engine/compile/logic.js";function Pe($,J={},j="",L={},D={},m={}){return te($,J,j,L,D,{...m,format:b.Module})}function qe($={},J={},j={}){const{state:L={},gets:D={},actions:m={},effects:S=[],entities:C={},imports:k=[]}=$;return te({screen:"store",entities:C,state:L,actions:m,gets:D,effects:S,imports:k,consts:{},constraints:{},rootId:void 0,nodes:{}},J,"",{},j,{format:b.Store})}function te($,J={},j="",L={},D={},m={}){const{nodes:S,rootId:C,state:k,entities:q,screen:ne}=$,M=m.theme||Se;let t=[],A=!1;const H=e=>{const s=t;t=[],e();const o=t;return t=s,o},W=new Set,h=(e,s)=>{for(const o of s.style||[])W.add(o);return[e,...(s.style||[]).map(V),...(s.class||[]).filter(o=>typeof o=="string")].join(" ")},se=new Set(Object.keys(k)),K=new Set(Object.entries(k).filter(([,e])=>typeof e.source=="string"&&e.source.startsWith("query:")).map(([e])=>e)),z=new Set,oe={state:k,entities:q,actions:$.actions,consts:$.consts||{},gets:$.gets||{},effects:$.effects||[],stateKeys:se,queryStates:K,stores:m.stores||{},usedStores:z,params:new Set($.params||[]),format:m.format},_=new Le(oe),d={locals:new Set},x=(e,s)=>{for(const o of s.class||[])typeof o!="string"&&t.push(`effect(() => el_${e}.classList.toggle(${JSON.stringify(o.name)}, !!(${_.compileExpr(o.cond,d)})));`);for(const[o,n]of Object.entries(s.on||{}))typeof n=="string"&&t.push(`el_${e}.addEventListener(${JSON.stringify(o)}, () => ${_.actionRef(n)}());`)},I=(e,s)=>{for(const o of S[e].children)G(o,s)},U=e=>e.parts.map(s=>typeof s=="string"?JSON.stringify(s):`String(${_.compileExpr(s,d)} ?? '')`).join(" + ");function B(e,s,o,n,u){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 a=U(n),i=n.parts.some(p=>typeof p!="string");t.push(i?`effect(() => { el_${e}.textContent = ${a}; });`:`el_${e}.textContent = ${a};`)}t.push(`${u}.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=U(o),u=o.parts.some(a=>typeof a!="string");t.push(u?`effect(() => { el_${e}.${s} = ${n}; });`:`el_${e}.${s} = ${n};`)}}function G(e,s){const o=S[e],n=o.props||{},u=ee[o.type];if(u){const[a,i]=u;t.push(`const el_${e} = document.createElement('${a}');`),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});`),x(e,n),I(e,`el_${e}`);return}switch(o.type){case l.SearchField:{const a=_.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(`effect(() => { if (el_${e}.value !== ${a}.get()) el_${e}.value = ${a}.get(); });`),t.push(`el_${e}.addEventListener('input', (e) => ${a}.set(e.target.value));`),t.push(`${s}.appendChild(el_${e});`);break}case l.DataTable:{const a=_.bindSig(n.data),i=K.has(a)?`${a}.get().data`:`${a}.get()`,p=n.columns||[],g=(n.where||[]).map(ke),N=g.filter(c=>!c.dynamic).map(c=>`.filter((row) => ${c.expr})`).join(""),v=g.filter(c=>c.dynamic).map(c=>`.filter((row) => ${c.expr})`).join(""),r=o.children.map(c=>S[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); }`);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 c of p)t.push(` { const td = document.createElement('td'); effect(() => { td.textContent = (row.get()[${JSON.stringify(c)}] ?? ''); }); tr.appendChild(td); }`);for(const c of r){const f=c.props||{},ge={locals:new Set,sigLocals:new Set(["row"])},de=f.arg!==void 0?[f.arg,...f.argRest||[]].map(be=>_.compileExpr(be,ge)).join(", "):"";t.push(` { const td = document.createElement('td'); const b = document.createElement('button'); b.className = ${JSON.stringify(h("row-action",f))}; b.textContent = ${JSON.stringify(f.label)}; b.addEventListener('click', () => ${_.actionRef(f.action)}(${de})); td.appendChild(b); tr.appendChild(td); }`)}t.push(" return tr;"),t.push("}"),t.push(`function base_${e}() { return ${i}${N}; }`),t.push(`const start_${e} = document.createComment('rows');`),t.push(`const anchor_${e} = document.createComment('/rows');`),t.push(`body_${e}.appendChild(start_${e}); body_${e}.appendChild(anchor_${e});`),t.push(`const map_${e} = new Map();`),t.push(`onCleanup(() => { for (const __e of map_${e}.values()) __e.dispose(); map_${e}.clear(); }); // parent unmount \u2192 tear down every row`),t.push("effect(() => {"),t.push(` const __rows = base_${e}()${v};`),t.push(" const __seen = new Set();"),t.push(` let __prev = start_${e};`),t.push(" for (const __row of __rows) {"),t.push(" const __k = __row?.id ?? __row; __seen.add(__k); // key by id (entities) or the value itself (scalars) \u2014 never index"),t.push(` let __e = map_${e}.get(__k);`),t.push(" if (__e) { if (!__eq(__e.data, __row)) { __e.data = __row; __e.sig.set(__row); } }"),t.push(` else { const __sig = signal(__row); const __r = root(() => [renderRow_${e}(__sig)]); __e = { sig: __sig, nodes: __r.value, dispose: __r.dispose, data: __row }; map_${e}.set(__k, __e); }`),t.push(` for (const __n of __e.nodes) { if (__prev.nextSibling !== __n) anchor_${e}.parentNode.insertBefore(__n, __prev.nextSibling); __prev = __n; }`),t.push(" }"),t.push(` for (const [__k, __e] of map_${e}) if (!__seen.has(__k)) { __e.dispose(); for (const __n of __e.nodes) __n.remove(); map_${e}.delete(__k); }`),t.push("});");break}case l.Form:{const a=_.bindSig(n.bind),i=k[a]?.type;if(!i||!q[i])throw new Error(`Form must bind a page-local entity draft, not "${n.bind}"`);const p=Ne(q[i]),g=($.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 N=[];for(const r of p){const c=`f_${e}_${r.name}`;if(N.push({...r,var:c,c:g[r.name]}),r.kind===y.Enum){t.push(`const ${c} = document.createElement('select');`),t.push(`${c}.className = 'field';`);for(const f of r.options)t.push(`{ const o = document.createElement('option'); o.value = ${JSON.stringify(f)}; o.textContent = ${JSON.stringify(f)}; ${c}.appendChild(o); }`)}else r.kind===y.Bool?(t.push(`const ${c} = document.createElement('input');`),t.push(`${c}.type = 'checkbox';`),t.push(`${c}.className = 'field-check';`)):(t.push(`const ${c} = document.createElement('input');`),t.push(`${c}.type = ${JSON.stringify(r.kind===y.Email?"email":r.kind===y.Number?"number":"text")};`),t.push(`${c}.className = 'field';`),t.push(`${c}.placeholder = ${JSON.stringify(r.name)};`));if(r.kind===y.Bool)t.push(`${c}.addEventListener('change', (e) => ${a}.set({ ...${a}.get(), ${JSON.stringify(r.name)}: e.target.checked }));`);else{const f=r.kind===y.Number?"(Number(e.target.value) || 0)":"e.target.value";t.push(`${c}.addEventListener('input', (e) => ${a}.set({ ...${a}.get(), ${JSON.stringify(r.name)}: ${f} }));`)}t.push(`el_${e}.appendChild(${c});`),g[r.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 v=[];for(const r of N){if(!r.c)continue;const c=`err_${r.var}`,f=`String(__d[${JSON.stringify(r.name)}] ?? '')`;v.push(`${c}.textContent = '';`),r.c.required&&v.push(`if (!${f}.trim()) { ${c}.textContent = 'Required'; __ok = false; }`),r.c.min!=null&&v.push(r.kind===y.Number?`if (${f} !== '' && Number(${f}) < ${r.c.min}) { ${c}.textContent = 'Min ${r.c.min}'; __ok = false; }`:`if (${f} && ${f}.length < ${r.c.min}) { ${c}.textContent = 'Min ${r.c.min} characters'; __ok = false; }`),r.c.max!=null&&v.push(r.kind===y.Number?`if (${f} !== '' && Number(${f}) > ${r.c.max}) { ${c}.textContent = 'Max ${r.c.max}'; __ok = false; }`:`if (${f}.length > ${r.c.max}) { ${c}.textContent = 'Max ${r.c.max} characters'; __ok = false; }`)}v.length?t.push(`el_${e}.addEventListener('submit', (e) => { e.preventDefault(); const __d = ${a}.get(); let __ok = true; ${v.join(" ")} if (__ok) ${_.actionRef(n.submit)}(__d); });`):t.push(`el_${e}.addEventListener('submit', (e) => { e.preventDefault(); ${_.actionRef(n.submit)}(${a}.get()); });`),t.push("effect(() => {"),t.push(` const d = ${a}.get();`);for(const r of N){if(r.kind===y.Bool){t.push(` { const v = !!d[${JSON.stringify(r.name)}]; if (${r.var}.checked !== v) ${r.var}.checked = v; }`);continue}const c=r.kind===y.Enum?JSON.stringify(r.options[0]):"''";t.push(` { const v = d[${JSON.stringify(r.name)}] ?? ${c}; if (${r.var}.value !== v) ${r.var}.value = v; }`)}t.push("});"),t.push(`${s}.appendChild(el_${e});`);break}case l.Text:B(e,"p",h("text",n),n.value,s),x(e,n);break;case l.Span:B(e,"span",h("span",n),n.value,s),x(e,n);break;case l.Title:B(e,n.level||"h1",h("title",n),n.value,s),x(e,n);break;case l.Image:{t.push(`const el_${e} = document.createElement('img');`),t.push(`el_${e}.className = ${JSON.stringify(h("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 a=_.compileExpr(n.cond,d),i=H(()=>I(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} = null; // { value: nodes, dispose } while mounted, else null \u2014 dispose kills the block's effects on unmount (no zombies)`),t.push(`onCleanup(() => { if (shown_${e}) shown_${e}.dispose(); }); // parent unmount \u2192 tear down the mounted block too`),t.push("effect(() => {"),t.push(` if (${a}) {`),t.push(` if (!shown_${e}) { const __r = root(() => { const __f = document.createDocumentFragment(); build_${e}(__f); return [...__f.childNodes]; }); for (const __n of __r.value) anchor_${e}.parentNode.insertBefore(__n, anchor_${e}); shown_${e} = __r; }`),t.push(` } else if (shown_${e}) { shown_${e}.dispose(); for (const __n of shown_${e}.value) __n.remove(); shown_${e} = null; }`),t.push("});");break}case l.Each:{if(!n.list||!n.as)throw new Error("each without a list or item variable");const a=_.compileExpr(n.list,d),i=n.filter?_.compileExpr(n.filter,d):"",p=d.sigLocals;d.sigLocals=new Set([...p||[],n.as]);const g=H(()=>I(e,"__p"));d.sigLocals=p,t.push(`function buildItem_${e}(__p, ${n.as}) {`);for(const N of g)t.push(" "+N);t.push("}"),t.push(`const start_${e} = document.createComment('each');`),t.push(`const anchor_${e} = document.createComment('/each');`),t.push(`${s}.appendChild(start_${e}); ${s}.appendChild(anchor_${e});`),t.push(`const map_${e} = new Map(); // row id \u2192 { sig, nodes, dispose, data }`),t.push(`onCleanup(() => { for (const __e of map_${e}.values()) __e.dispose(); map_${e}.clear(); }); // parent unmount \u2192 tear down every row (no leaked effects)`),t.push("effect(() => {"),t.push(` const __rows = (${a} ?? [])${i?`.filter((${n.as}) => ${i})`:""};`),t.push(" const __seen = new Set();"),t.push(` let __prev = start_${e};`),t.push(" for (const __row of __rows) {"),t.push(" const __k = __row?.id ?? __row; __seen.add(__k); // key by id (entities) or the value itself (scalars) \u2014 never index"),t.push(` let __e = map_${e}.get(__k);`),t.push(" if (__e) { if (!__eq(__e.data, __row)) { __e.data = __row; __e.sig.set(__row); } } // same row, changed data \u2192 granular update"),t.push(` else { const __sig = signal(__row); const __r = root(() => { const __f = document.createDocumentFragment(); buildItem_${e}(__f, __sig); return [...__f.childNodes]; }); __e = { sig: __sig, nodes: __r.value, dispose: __r.dispose, data: __row }; map_${e}.set(__k, __e); } // new row`),t.push(` for (const __n of __e.nodes) { if (__prev.nextSibling !== __n) anchor_${e}.parentNode.insertBefore(__n, __prev.nextSibling); __prev = __n; } // order: move only if out of place`),t.push(" }"),t.push(` for (const [__k, __e] of map_${e}) if (!__seen.has(__k)) { __e.dispose(); for (const __n of __e.nodes) __n.remove(); map_${e}.delete(__k); } // gone \u2192 dispose effects + remove nodes`),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 a=Object.entries(n.inputs||{}).map(([p,g])=>`${JSON.stringify(p)}: ${ve(g)}`).join(", "),i=Object.entries(n.on||{}).map(([p,g])=>`${JSON.stringify(p)}: (...__a) => ${_.actionRef(typeof g=="string"?g:"")}(...__a)`).join(", ");t.push(`if (typeof __custom_${n.component} === 'function') __custom_${n.component}(el_${e}, { ${a} }, { ${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?I(e,`el_${e}`):n.label!==void 0&&F(e,"textContent",n.label),n.action){const a=n.arg!==void 0?[n.arg,...n.argRest||[]].map(i=>_.compileExpr(i,d)).join(", "):"";t.push(`el_${e}.addEventListener('click', () => ${_.actionRef(n.action)}(${a}));`)}x(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))};`),F(e,"href",n.to??"/"),o.children&&o.children.length?I(e,`el_${e}`):n.label!==void 0&&F(e,"textContent",n.label),x(e,n),t.push(`${s}.appendChild(el_${e});`);break}case l.Slot:{A=!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 E=e=>String(e??"").replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">"),P=e=>E(e).replace(/"/g,"""),w=e=>typeof e=="string"?e:"";function re(){if(m.format===b.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(S)){const u=S[n],a=u.props||{};if(e.has(u.type)||s.some(i=>a[i]!==void 0)||(a.class||[]).some(i=>typeof i!="string")||o.some(i=>{const p=a[i];return!!p&&typeof p=="object"&&"kind"in p&&p.kind===we.Interp}))return!1}return!0}function Q(e){const s=S[e],o=s.props||{},n=()=>(S[e].children||[]).map(Q).join(""),u=i=>` class="${P(h(i,o))}"`,a=ee[s.type];if(a){const[i,p]=a;return`<${i}${u(p)}>${n()}</${i}>`}switch(s.type){case l.Text:return`<p${u("text")}>${E(w(o.value))}</p>`;case l.Span:return`<span${u("span")}>${E(w(o.value))}</span>`;case l.Title:{const i=o.level||"h1";return`<${i}${u("title")}>${E(w(o.value))}</${i}>`}case l.Image:return`<img${u("image")} src="${P(w(o.src))}" alt="${P(w(o.alt))}">`;case l.Link:return`<a${u("link")} href="${P(w(o.to)||"/")}">${s.children&&s.children.length?n():E(w(o.label))}</a>`;case l.Button:return`<button${u("button")}>${s.children&&s.children.length?n():E(w(o.label))}</button>`;default:return""}}const X=m.format===b.Ssr?!1:re(),ce=($.params||[]).map(e=>`const ${e} = (__params || {})[${JSON.stringify(e)}] ?? '';`).join(`
|
|
2
|
+
`),ae=_.genState(),ie=_.genActions(),le=m.format===b.Store?"export const":"const",pe=Object.entries($.gets||{}).map(([e,s])=>`${le} ${e} = computed(() => ${_.compileExpr(s,d)});`).join(`
|
|
3
|
+
`),_e=_.genEffects();let Y=null;X?Y=C?Q(C):"":m.format!==b.Store&&C&&G(C,"app");const ue=t.join(`
|
|
4
|
+
`),Z={};for(const e of Object.values(k))if(typeof e.source=="string"&&e.source.startsWith("query:")){const s=e.source.slice(6),o=(e.type.match(/^list<(.+)>$/)||[])[1];Z[s]=o?_.uuidFields(o):[]}const fe=[...W].map(e=>{const s=ye(e,M);if(!s)return"";const o=`.${V(e)}{${s}}`,n=e.indexOf(":"),u=n>0&&M.breakpoints[e.slice(0,n)];return u?`@media (min-width:${u}){${o}}`:o}).filter(Boolean).join(`
|
|
5
|
+
`),me=Object.entries(L).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
|
-
`),
|
|
13
|
-
`),F=m.meta||{},R={...F};F.title&&!R["og:title"]&&(R["og:title"]=F.title),F.description&&!R["og:description"]&&(R["og:description"]=F.description);const O={screen:ae,tokenCss:ge,projectCss:j,data:J,sources:L,api:$.api||{},meta:R,queryUuids:ee,stateDecls:ue,paramDecls:pe,actionDecls:fe,getDecls:me,effectDecls:$e,componentDecls:de,storeImports:be,storeDecls:$.storeCode||"",externImports:Se,islandImports:ve,renderBody:he,staticHtml:Z??"",hasSlot:W};return $.format===S.Store?Oe(O):$.format===S.Ssr?Ie(O):Y?$.format===S.Module?Je(O):je(O):$.format===S.Module?Le(O):Te(O)}export{re as compile,Be as compileModule,Me as compileStore};
|
|
10
|
+
`),$e=[...z].map(e=>`import * as __store_${e} from 'virtual:muten/store/${e}';`).join(`
|
|
11
|
+
`),he=($.imports||[]).map(e=>`import { ${e.names.join(", ")} } from ${JSON.stringify(e.from)};`).join(`
|
|
12
|
+
`),T=$.meta||{},R={...T};T.title&&!R["og:title"]&&(R["og:title"]=T.title),T.description&&!R["og:description"]&&(R["og:description"]=T.description);const O={screen:ne,tokenCss:fe,projectCss:j,data:J,sources:D,api:m.api||{},meta:R,queryUuids:Z,stateDecls:ae,paramDecls:ce,actionDecls:ie,getDecls:pe,effectDecls:_e,componentDecls:me,storeImports:$e,storeDecls:m.storeCode||"",externImports:he,renderBody:ue,staticHtml:Y??"",hasSlot:A};return m.format===b.Store?Ce(O):m.format===b.Ssr?Oe(O):X?m.format===b.Module?xe(O):Ee(O):m.format===b.Module?Je(O):je(O)}export{te as compile,Pe as compileModule,qe as compileStore};
|
|
@@ -68,6 +68,8 @@ export function mount(app) { app.innerHTML = ${JSON.stringify(e.staticHtml)}; re
|
|
|
68
68
|
|
|
69
69
|
${e.stateDecls}
|
|
70
70
|
|
|
71
|
+
${e.getDecls}
|
|
72
|
+
|
|
71
73
|
${e.actionDecls}
|
|
72
74
|
|
|
73
75
|
${e.componentDecls}
|
|
@@ -94,7 +96,6 @@ ${e.staticHtml}
|
|
|
94
96
|
`}function h(e){return`import { signal, computed, effect, root, onCleanup, __eq, __id, __has } from 'virtual:muten/runtime';
|
|
95
97
|
${e.storeImports}
|
|
96
98
|
${e.externImports}
|
|
97
|
-
${e.islandImports}
|
|
98
99
|
export const screen = ${JSON.stringify(e.screen)};
|
|
99
100
|
export const css = ${JSON.stringify(`${e.tokenCss}
|
|
100
101
|
${e.projectCss}`)};
|
|
@@ -107,6 +108,8 @@ export function mount(app, __params) {
|
|
|
107
108
|
|
|
108
109
|
${e.stateDecls}
|
|
109
110
|
|
|
111
|
+
${e.getDecls}
|
|
112
|
+
|
|
110
113
|
${e.actionDecls}
|
|
111
114
|
|
|
112
115
|
${e.componentDecls}
|
|
@@ -114,7 +117,7 @@ export function mount(app, __params) {
|
|
|
114
117
|
${e.renderBody}
|
|
115
118
|
return ${e.hasSlot?"__outlet":"app"};
|
|
116
119
|
}
|
|
117
|
-
`}function
|
|
120
|
+
`}function g(e){return`<!doctype html>
|
|
118
121
|
<html lang="en">
|
|
119
122
|
<head>
|
|
120
123
|
<meta charset="utf-8">
|
|
@@ -144,6 +147,9 @@ ${i(e.meta,e.screen)}
|
|
|
144
147
|
// \u2500\u2500 declared state (state from the IR) \u2500\u2500
|
|
145
148
|
${e.stateDecls}
|
|
146
149
|
|
|
150
|
+
// \u2500\u2500 page-level derived values (get \u2192 computed) \u2500\u2500
|
|
151
|
+
${e.getDecls}
|
|
152
|
+
|
|
147
153
|
// \u2500\u2500 actions (actions from the IR) \u2500\u2500
|
|
148
154
|
${e.actionDecls}
|
|
149
155
|
|
|
@@ -157,4 +163,4 @@ ${i(e.meta,e.screen)}
|
|
|
157
163
|
<\/script>
|
|
158
164
|
</body>
|
|
159
165
|
</html>
|
|
160
|
-
`}export{s as RUNTIME,
|
|
166
|
+
`}export{s as RUNTIME,g as emitHtml,h as emitModule,f as emitSsr,_ as emitStatic,m as emitStaticHtml,d as emitStore};
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import{Ek as $,StOp as
|
|
2
|
-
`)}genActions(){const t=this.ctx.format===h.Store?"export ":"",i=[],e=[];for(const[r,s]of Object.entries(this.ctx.actions)){const n
|
|
1
|
+
import{Ek as $,StOp as a,BOp as g,UOp as f,Fmt as h}from"#engine/shared/vocab.js";import{JS_BINOP as d}from"#engine/compile/helpers.js";class S{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)}itemFields(t){const i=this.ctx.state[t.split(".")[0]]?.type||"",e=i.startsWith("list<")?i.slice(5,-1):"",r=this.ctx.entities[e];return new Set(["id",...r?Object.keys(r):[]])}bodyHasWrite(t){return t.some(i=>i.op===a.Create||i.op===a.Update||i.op===a.Delete||i.op===a.Request||i.op===a.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.sigLocals?.has(e))return`${e}.get()${s}`;if(i.locals.has(e))return e+s;if(i.item?.fields.has(e))return`${i.item.var}.${t}`;if(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.gets[e]!==void 0)return`${e}.get()`+s;if(this.ctx.stores[e]){const n=r[0],_=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()${_}`}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,item:{var:"__it",fields:this.itemFields(t.list)}});if(t.op==="sort"||t.op==="sortDesc"){const n=t.op==="sortDesc"?-1:1,_=`((__it) => ${r})`;return`[...${e}].sort((__a, __b) => { const __ka = ${_}(__a), __kb = ${_}(__b); return (__ka < __kb ? -1 : __ka > __kb ? 1 : 0) * ${n}; })`}const s=(n,_)=>`${e}.reduce((__a, __it) => ${_}, ${n})`;return t.op==="count"?`${e}.filter((__it) => ${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===$.Filter){const e=`[...(${this.resolveRef(t.list,i)} ?? [])]`,r=this.compileExpr(t.cond,{...i,item:{var:"__it",fields:this.itemFields(t.list)}});return`${e}.filter((__it) => ${r})`}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=d[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===a.If){s.push(`if (${this.compileExpr(t.cond,i)}) {`);for(const n of t.then||[])for(const _ of this.stmtLines(n,i,e))s.push(" "+_);if(t.else){s.push("} else {");for(const n of t.else)for(const _ of this.stmtLines(n,i,e))s.push(" "+_)}return s.push("}"),s}if(t.op===a.Reset)s.push(`${t.target}.set(${JSON.stringify(r.state[t.target].initial??null)});`);else if(t.op===a.Toggle)s.push(`${t.target}.set(!${t.target}.get());`);else if(t.op===a.Set)s.push(`${t.target}.set(${this.compileExpr(t.arg,i)});`);else if(t.op===a.Push){const n=(r.state[t.target].type.match(/^list<(.+)>$/)||[])[1],_=n&&r.entities[n],o=l=>r.queryStates.has(t.target)?`${t.target}.set({ ...${t.target}.get(), data: [...${t.target}.get().data, ${l}] });`:`${t.target}.set([...${t.target}.get(), ${l}]);`;if(_){s.push(`{ const __it = { ...${this.compileExpr(t.arg,i)} };`);for(const l of this.uuidFields(n))s.push(` if (__it.${l} === null || __it.${l} === undefined) __it.${l} = __id(); // auto uuid`);s.push(` ${o("__it")} }`)}else s.push(`${o(this.compileExpr(t.arg,i))}`)}else if(t.op===a.Remove){const n={...i,item:{var:"__it",fields:this.itemFields(t.target)}},_=this.compileExpr(t.pred,n);s.push(r.queryStates.has(t.target)?`${t.target}.set({ ...${t.target}.get(), data: ${t.target}.get().data.filter((__it) => !(${_})) });`:`${t.target}.set(${t.target}.get().filter((__it) => !(${_})));`)}else if(t.op===a.Patch){const n={...i,item:{var:"__it",fields:this.itemFields(t.target)}},_=this.compileExpr(t.pred,n),o=this.compileExpr(t.patch,n),l=p=>`${p}.map((__it) => (${_}) ? { ...__it, ...${o} } : __it)`;s.push(r.queryStates.has(t.target)?`${t.target}.set({ ...${t.target}.get(), data: ${l(`${t.target}.get().data`)} });`:`${t.target}.set(${l(`${t.target}.get()`)});`)}else if(t.op===a.Create||t.op===a.Update||t.op===a.Delete){const n=r.queryStates.has(t.target),_=n?`${t.target}.get().data`:`${t.target}.get()`,o=u=>n?`${t.target}.set({ ...${t.target}.get(), data: ${u} })`:`${t.target}.set(${u})`,l=n?`.catch((__e) => ${t.target}.set({ ...${t.target}.get(), error: String(__e) }))`:"",p=JSON.stringify(t.target),c=this.compileExpr(t.arg,i);e?t.op===a.Create?s.push(`{ const __i = { ...${c} }; if (__i.id == null) __i.id = __id(); const __prev = ${_}; ${o("[...__prev, __i]")}; try { const __r = await __write(${p}, 'POST', null, __i); ${o(`${_}.map((__x) => __x.id === __i.id ? __r : __x)`)}; } catch (__e) { ${o("__prev")}; throw __e; } }`):t.op===a.Update?s.push(`{ const __i = ${c}; const __prev = ${_}; ${o("__prev.map((__x) => __x.id === __i.id ? __i : __x)")}; try { const __r = await __write(${p}, 'PUT', __i.id, __i); ${o(`${_}.map((__x) => __x.id === __i.id ? __r : __x)`)}; } catch (__e) { ${o("__prev")}; throw __e; } }`):s.push(`{ const __i = ${c}; const __prev = ${_}; ${o("__prev.filter((__x) => __x.id !== __i.id)")}; try { await __write(${p}, 'DELETE', __i.id, null); } catch (__e) { ${o("__prev")}; throw __e; } }`):t.op===a.Create?s.push(`{ const __i = ${c}; __write(${p}, 'POST', null, __i).then((__r) => ${o(`[...${_}, __r]`)})${l}; }`):t.op===a.Update?s.push(`{ const __i = ${c}; __write(${p}, 'PUT', __i.id, __i).then((__r) => ${o(`${_}.map((__x) => __x.id === __i.id ? __r : __x)`)})${l}; }`):s.push(`{ const __i = ${c}; __write(${p}, 'DELETE', __i.id, null).then(() => ${o(`${_}.filter((__x) => __x.id !== __i.id)`)})${l}; }`)}else if(t.op===a.Refetch){const n=Object.entries(t.params).map(([_,o])=>`${JSON.stringify(_)}: ${this.compileExpr(o,i)}`).join(", ");s.push(`__refetch(${JSON.stringify(t.target)}, { ${n} }, ${t.target});`)}else if(t.op===a.Request){const n=typeof t.url=="string"?JSON.stringify(t.url):t.url.parts.map(o=>typeof o=="string"?JSON.stringify(o):`String(${this.compileExpr(o,i)})`).join(" + "),_=t.body?this.compileExpr(t.body,i):"null";s.push(e?`await __send(${n}, ${JSON.stringify(t.method)}, ${_});`:`__send(${n}, ${JSON.stringify(t.method)}, ${_}).catch(() => {});`)}else t.op===a.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))}${r.live?", true":""}); // async: ${e}.loading / .error / .data${r.live?" \u2014 live (websocket)":""}`);else{let s=r.initial??null;const n=r.type.startsWith("list<")?r.type.slice(5,-1):"",_=n?this.uuidFields(n):[];_.length&&Array.isArray(s)&&(s=s.map((o,l)=>{if(typeof o!="object"||o===null||Array.isArray(o))return o;const p={...o};for(const c of _)(p[c]===null||p[c]===void 0)&&(p[c]=`${e}-${l}`);return p})),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=!!s.params?.length,_=!n&&this.ctx.stateKeys.has(s.input),o=n?{locals:new Set(s.params.map(p=>p.name))}:{locals:new Set,input:s.input,inputIsState:_},l=n?s.params.map(p=>p.name).join(", "):_?"":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}(${l}) {`),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}(${l}) {`);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(`
|
|
6
6
|
`)}
|
|
7
7
|
});`).join(`
|
|
8
|
-
`)}}export{
|
|
8
|
+
`)}}export{S as Logic};
|
|
@@ -1 +1 @@
|
|
|
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
|
|
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 P(t,n){const{tree:e,used:r}=h(t.tree,n),u={...t.entities},o={...t.state};for(const g of r){const l=n[g];l&&(Object.assign(u,l.entities),Object.assign(o,l.state))}return{doc:y({...t,entities:u,state:o,tree:e}),used:r}}function a(t,n,e){const r=n[t.type];if(r){e.add(t.type);const o=b(r.tree,t.args||{});return a(o,n,e)}const u={type:t.type};return t.loc&&(u.loc=t.loc),t.args&&(u.args=t.args),t.props&&(u.props=t.props),t.children&&(u.children=t.children.map(o=>a(o,n,e))),u}function b(t,n){const e={type:t.type};return t.props&&(e.props=A(t.props,n)),t.args&&(e.args=s(t.args,n)),t.children&&(e.children=t.children.map(r=>b(r,n))),e}function A(t,n){const e={...t};return t.value!==void 0&&(e.value=c(t.value,n)),t.label!==void 0&&(e.label=c(t.label,n)),t.src!==void 0&&(e.src=c(t.src,n)),t.alt!==void 0&&(e.alt=c(t.alt,n)),t.placeholder!==void 0&&(e.placeholder=c(t.placeholder,n)),t.submitLabel!==void 0&&(e.submitLabel=c(t.submitLabel,n)),t.cond!==void 0&&(e.cond=i(t.cond,n)),t.list!==void 0&&(e.list=i(t.list,n)),t.arg!==void 0&&(e.arg=i(t.arg,n)),t.argRest!==void 0&&(e.argRest=t.argRest.map(r=>i(r,n))),t.action!==void 0&&(e.action=d(t.action,n)),t.submit!==void 0&&(e.submit=d(t.submit,n)),t.bind!==void 0&&(e.bind=d(t.bind,n)),t.to!==void 0&&(e.to=typeof t.to=="string"?t.to:m(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 c(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 m(t,n)}function i(t,n){return t.kind===f.Ref?{kind:f.Ref,name:d(t.name,n)}:t.kind===f.Bin?{...t,left:i(t.left,n),right:i(t.right,n)}:t.kind===f.Un?{...t,operand:i(t.operand,n)}:t.kind===f.Tern?{...t,cond:i(t.cond,n),then:i(t.then,n),else:i(t.else,n)}:t.kind===f.Call?{...t,args:t.args.map(e=>i(e,n))}:t.kind===f.Obj?{...t,fields:t.fields.map(e=>({key:e.key,value:i(e.value,n)}))}:t.kind===f.Agg?{...t,list:d(t.list,n),body:i(t.body,n)}:t.kind===f.Filter?{...t,list:d(t.list,n),cond:i(t.cond,n)}:t}function m(t,n){return{kind:f.Interp,parts:t.parts.map(e=>typeof e=="string"?e:i(e,n))}}function s(t,n){const e={};for(const[r,u]of Object.entries(t))e[r]=I(u,n);return e}function I(t,n){return typeof t=="string"?t.startsWith("$")?d(t,n):t:typeof t=="number"?t:n[t.$param]}function d(t,n){if(!t.startsWith("$"))return t;const e=t.slice(1).split(".")[0],r=t.slice(1+e.length),u=n[e];return(typeof u=="string"?u:e)+r}export{h as compose,P as composeDoc};
|
package/dist/engine/ir/print.js
CHANGED
|
@@ -1,26 +1,26 @@
|
|
|
1
|
-
import{Ek as $,StOp as o,Nt as
|
|
1
|
+
import{Ek as $,StOp as o,Nt as y,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,S=t=>/^[A-Za-z_][A-Za-z0-9_.]*$/.test(t);function i(t){switch(t.kind){case $.Lit:return h(t.value);case $.Ref:return t.name;case $.Un:return`${t.op} ${u(t.operand)}`;case $.Bin:return`${u(t.left)} ${t.op} ${u(t.right)}`;case $.Tern:return`${u(t.cond)} ? ${u(t.then)} : ${u(t.else)}`;case $.Call:return`${t.fn}(${t.args.map(i).join(", ")})`;case $.Obj:return`{ ${t.fields.map(r=>`${r.key}: ${i(r.value)}`).join(", ")} }`;case $.Agg:return`${t.list}.${t.op} ${t.op==="count"?"where":"by"} ${i(t.body)}`;case $.Filter:return`${t.list} where ${i(t.cond)}`}}const u=t=>t.kind===$.Bin||t.kind===$.Tern?`(${i(t)})`:i(t),h=t=>typeof t=="string"?JSON.stringify(t):String(t);function m(t){return t===null?"null":Array.isArray(t)?`[${t.map(m).join(", ")}]`:typeof t=="object"?`{ ${Object.entries(t).map(([r,e])=>`${r}: ${m(e)}`).join(", ")} }`:h(t)}const P=t=>'"'+t.parts.map(r=>typeof r=="string"?r:`{${i(r)}}`).join("")+'"',R=t=>typeof t=="string"?t:t.parts.map(r=>typeof r=="string"?r:`{${i(r)}}`).join("");function k(t){return typeof t=="string"?JSON.stringify(t):I(t)?"$"+t.$param:P(t)}const N=t=>typeof t=="number"?String(t):typeof t=="string"?t:"$"+t.$param,b=t=>Object.entries(t).map(([r,e])=>`${r}: ${N(e)}`).join(", "),w=t=>typeof t=="string"?S(t)?t:JSON.stringify(t):`${S(t.name)?t.name:JSON.stringify(t.name)} when ${i(t.cond)}`;function d(t,r){switch(t.op){case o.Push:return`${t.target}.push(${i(t.arg)})`;case o.Set:return`${t.target}.set(${i(t.arg)})`;case o.Reset:return`${t.target}.reset()`;case o.Toggle:return`${t.target}.toggle()`;case o.Remove:return`${t.target}.remove where ${i(t.pred)}`;case o.Patch:return`${t.target}.patch where ${i(t.pred)} with ${i(t.patch)}`;case o.Create:return`${t.target}.create(${i(t.arg)})`;case o.Update:return`${t.target}.update(${i(t.arg)})`;case o.Delete:return`${t.target}.delete(${i(t.arg)})`;case o.Refetch:return`${t.target}.refetch(${Object.entries(t.params).map(([e,n])=>`${e}: ${i(n)}`).join(", ")})`;case o.Request:return`${t.method.toLowerCase()} ${typeof t.url=="string"?JSON.stringify(t.url):P(t.url)}${t.body?` body ${i(t.body)}`:""}`;case o.Call:return`${t.target}.${t.method}(${t.args.map(i).join(", ")})`;case o.If:{const e=t.then.map(s=>r+a+d(s,r+a)).join(`
|
|
2
2
|
`),n=t.else?` else {
|
|
3
|
-
${t.else.map(
|
|
3
|
+
${t.else.map(s=>r+a+d(s,r+a)).join(`
|
|
4
4
|
`)}
|
|
5
|
-
${r}}`:"";return`if ${
|
|
5
|
+
${r}}`:"";return`if ${i(t.cond)} {
|
|
6
6
|
${e}
|
|
7
|
-
${r}}${n}`}}}function
|
|
8
|
-
${t.children.map(
|
|
7
|
+
${r}}${n}`}}}function j(t,r){if(t.args)return`${r}${t.type}(${b(t.args)})`;if(t.type===y.Slot)return`${r}slot`;const e=t.props||{};let n;if(t.type===y.When)n=`when ${i(e.cond)}`;else if(t.type===y.Each)n=`each ${i(e.list)} as ${e.as}`;else{n=t.type;const s=e.value??e.label??e.src??e.placeholder??e.submitLabel;s!==void 0&&(n+=` ${k(s)}`),e.to!==void 0?n+=` -> ${R(e.to)}`:e.action&&(n+=` -> ${e.action}${e.arg!==void 0?`(${[e.arg,...e.argRest||[]].map(i).join(", ")})`:""}`),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(w).join(", ")})`),e.alt!==void 0&&(n+=` ${c.Alt} ${k(e.alt)}`),e.inputs&&(n+=` ${c.Inputs}(${b(e.inputs)})`),e.on&&(n+=` ${c.On}(${b(e.on)})`),e.submit&&(n+=` ${c.Submit} ${e.submit}`)}return t.children&&t.children.length?`${r}${n} {
|
|
8
|
+
${t.children.map(s=>j(s,r+a)).join(`
|
|
9
9
|
`)}
|
|
10
|
-
${r}}`:`${r}${n}`}const
|
|
11
|
-
${r.body.map(
|
|
10
|
+
${r}}`:`${r}${n}`}const C=(t,r,e)=>{const n=Object.entries(r).map(([s,f])=>{let l=`${s} ${f}`;const g=e?.[s];return g?.required&&(l+=" required"),g?.min!==void 0&&(l+=` min:${g.min}`),g?.max!==void 0&&(l+=` max:${g.max}`),l});return`entity ${t} { ${n.join(" ")} }`},A=(t,r)=>{const e=r.source?.startsWith("query:")?`query ${r.source.slice(6)}`:m(r.initial??null);return`${t} = ${e} : ${r.type}`},E=(t,r)=>{const e=r.params?.length?`(${r.params.map(f=>`${f.name}: ${f.type}`).join(", ")})`:"",n=r.params?.length?"":r.input?` <- ${r.input}`:"";return`${`action ${t}${e}${r.mutates.length?` mutates ${r.mutates.join(", ")}`:""}${n}`} {
|
|
11
|
+
${r.body.map(f=>a+d(f,a)).join(`
|
|
12
12
|
`)}
|
|
13
|
-
}
|
|
14
|
-
${
|
|
13
|
+
}`},V=t=>`${t.url} -> ${t.page}${t.guard?` guard ${t.guardNeg?"not ":""}${t.guard}${t.redirect?` else ${t.redirect}`:""}`:""}`,x=(t,r)=>`part ${t}(${r.params.map(e=>`${e.name}: ${e.type}`).join(", ")}) {
|
|
14
|
+
${j(r.tree,a)}
|
|
15
15
|
}`,p=(t,r)=>`${t} {
|
|
16
16
|
${r}
|
|
17
|
-
}`,
|
|
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(
|
|
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(
|
|
17
|
+
}`,O=(t,r,e)=>p(t,r.map(([n,s])=>`${a}${n}${e} ${m(s)}`).join(`
|
|
18
|
+
`));function M(t){const r=[],e=n=>{n&&r.push(n)};if(t.consts)for(const[n,s]of Object.entries(t.consts))e(`const ${n} = ${h(s)}`);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,s])=>`${a}${n} ${JSON.stringify(s)}`).join(`
|
|
19
|
+
`))),t.params)for(const n of t.params)e(`param ${n}`);if(t.api&&e(O("api",Object.entries(t.api),":")),t.entities)for(const[n,s]of Object.entries(t.entities))e(C(n,s,t.constraints?.[n]));if(t.state&&Object.keys(t.state).length&&e(p("state",Object.entries(t.state).map(([n,s])=>a+A(n,s)).join(`
|
|
20
|
+
`))),t.store&&Object.keys(t.store).length&&e(p("store",Object.entries(t.store).map(([n,s])=>a+A(n,s)).join(`
|
|
21
|
+
`))),t.gets)for(const[n,s]of Object.entries(t.gets))e(`get ${n} = ${i(s)}`);if(t.sources&&e(O("sources",Object.entries(t.sources),":")),t.mock&&e(O("mock",Object.entries(t.mock),":")),t.actions)for(const[n,s]of Object.entries(t.actions))e(E(n,s));if(t.effects)for(const n of t.effects)e(p("effect",n.map(s=>a+d(s,a)).join(`
|
|
22
|
+
`)));if(t.parts)for(const[n,s]of Object.entries(t.parts))e(x(n,s));return t.shell&&e(p("shell",(t.shell.children||[]).map(n=>j(n,a)).join(`
|
|
23
|
+
`))),t.tree&&e(j(t.tree,"")),t.routes&&e(p("routes",t.routes.map(n=>a+V(n)).join(`
|
|
24
24
|
`))),r.join(`
|
|
25
25
|
|
|
26
26
|
`)+`
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{resolveToken as J,SUGGESTED as _,defaultTheme as Z,isKnownTokenShape as tt}from"#engine/style/tokens.js";import{diag as u,closest as y}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 p,StOp as k,BOp as $}from"#engine/shared/vocab.js";const G=new Set([...et,S.Shell]),it=["bind","data"],K=new Set(nt),ot=new Set([k.Create,k.Update,k.Delete,k.Refetch]),W=["text","number","bool","uuid","email","string"];function O(a,d=[]){if(a.kind===p.Ref)d.push(a.name);else if(a.kind===p.Un)O(a.operand,d);else if(a.kind===p.Bin)O(a.left,d),O(a.right,d);else if(a.kind===p.Tern)O(a.cond,d),O(a.then,d),O(a.else,d);else if(a.kind===p.Call)for(const l of a.args)O(l,d);else if(a.kind===p.Obj)for(const l of a.fields)O(l.value,d);else a.kind===p.Agg&&d.push(a.list);return d}function v(a,d=[]){if(a.kind===p.Call){d.push(a.fn);for(const l of a.args)v(l,d)}else if(a.kind===p.Un)v(a.operand,d);else if(a.kind===p.Bin)v(a.left,d),v(a.right,d);else if(a.kind===p.Tern)v(a.cond,d),v(a.then,d),v(a.else,d);else if(a.kind===p.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=e=>/^(svelte|react):/.test(e),N=new Set((a.imports||[]).filter(e=>!C(e.from)).flatMap(e=>e.names)),D=new Set((a.imports||[]).filter(e=>C(e.from)).flatMap(e=>e.names)),Y=a.nodes||{},z=(e,s)=>{for(const r of v(e))N.has(r)||l.push(u("unknown-function",`"${r}" is not a use'd function`,{loc:s,suggestion:y(r,[...N]),from:r}))},I=new Map;for(const[e,s]of Object.entries(a.state||{})){if(!s.source?.startsWith("query:"))continue;const r=new Set(["loading","error","data"]),n=a.entities?.[s.type];if(n){r.add("id");for(const i of Object.keys(n))r.add(i)}I.set(e,r)}const R=new Map;for(const[e,s]of Object.entries(d.storeMembers||{}))R.set(e,new Set(s));const L=(e,s,r)=>{const n=I.get(e);if(n){n.has(s)||l.push(u("unknown-member",`"${s}" is not a member of query "${e}"`,{loc:r,suggestion:y(s,[...n]),from:s}));return}const i=R.get(e);i&&!i.has(s)&&l.push(u("unknown-member",`"${s}" is not a member of store "${e}"`,{loc:r,suggestion:y(s,[...i]),from:s}))},F=Object.keys(a.entities||{});for(const[e,s]of Object.entries(a.state||{})){const r=s.type;if(s.source?.startsWith("query:")&&r!=="list"&&!r.startsWith("list<")&&l.push(u("query-not-list",`state "${e}" 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:s.loc,suggestion:`list<${r}>`})),r==="list")l.push(u("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(r.startsWith("list<")){const n=r.slice(5,-1);!W.includes(n)&&!F.includes(n)&&l.push(u("unknown-type",`list element "${n}" is not a known entity or scalar type`,{loc:s.loc,suggestion:y(n,[...F,...W]),from:n}))}else if(s.initial!==void 0&&s.initial!==null){const n=r==="number"?"number":r==="bool"?"boolean":["text","string","email","uuid"].includes(r)?"string":"";n&&typeof s.initial!==n&&l.push(u("type-mismatch",`state "${e}" is typed "${r}" but its initial value is a ${typeof s.initial}`,{loc:s.loc}))}}const P=e=>{const s=a.entities?.[e];return s?new Set(["id",...Object.keys(s)]):null},H=e=>{if(!e)return"";const s=e.kind===p.Ref?e.name:e.kind===p.Agg&&(e.op==="sort"||e.op==="sortDesc")?e.list:"";if(!s)return"";const r=a.state?.[s.split(".")[0]]?.type||"";return r.startsWith("list<")?r.slice(5,-1):""},U=(e,s)=>{if(typeof e=="string"&&e.startsWith("@")){const r=e.slice(1).split(".")[0];if(!M.has(r)&&!j.has(r)){const n=y(r,[...M]);l.push(u("unknown-ref",`"@${r}" is not a declared state`,{loc:s.loc,suggestion:n?"@"+n:null,from:"@"+r,related:n?a.state?.[n]?.loc??null:null}))}}},q=(e,s)=>{if(!(!e||e.startsWith("$"))){if(e.includes(".")){const r=e.indexOf(".");L(e.slice(0,r),e.slice(r+1).split(".")[0],s.loc??null);return}x.has(e)||l.push(u("unknown-action",`"${e}" is not a declared action`,{loc:s.loc,suggestion:y(e,[...x]),from:e}))}},A=(e,s)=>{if(e.kind===p.Lit)return typeof e.value=="number"?"number":typeof e.value=="boolean"?"bool":"text";if(e.kind===p.Ref){const[r,...n]=e.name.split("."),i=s.has(r)?s.get(r)||"":a.state?.[r]?.type||"";return n.length?n.length===1&&a.entities?.[i]?.[n[0]]||"":i}return e.kind===p.Agg?e.op==="sort"||e.op==="sortDesc"?"":"number":""},w=(e,s,r)=>{if(e.kind===p.Bin){if(e.op===$.Sub||e.op===$.Mul||e.op===$.Div)for(const n of[e.left,e.right]){const i=A(n,r);i&&i!=="number"&&l.push(u("arith-type",`arithmetic \`${e.op}\` needs numbers, but an operand is "${i}" \u2014 declare it \`: number\`.`,{loc:s}))}if([$.Eq,$.Neq,$.Lt,$.Gt,$.Lte,$.Gte].includes(e.op)&&!(e.left.kind===p.Lit&&e.left.value===null)&&!(e.right.kind===p.Lit&&e.right.value===null)){const n=t=>t==="number"||t==="bool"?t:t.startsWith("enum:")||["text","string","email","uuid"].includes(t)?"text":t,i=A(e.left,r),h=A(e.right,r);i&&h&&n(i)!==n(h)&&l.push(u("compare-type",`comparing a ${i} to a ${h} \u2014 they never match (always ${e.op===$.Neq?"true":"false"}). Likely a quoted number (\`== "1"\` vs \`== 1\`) or a type mismatch.`,{loc:s}))}w(e.left,s,r),w(e.right,s,r)}else if(e.kind===p.Un)w(e.operand,s,r);else if(e.kind===p.Tern)w(e.cond,s,r),w(e.then,s,r),w(e.else,s,r);else if(e.kind===p.Obj)for(const n of e.fields)w(n.value,s,r);else if(e.kind===p.Call)for(const n of e.args)w(n,s,r)},b=(e,s,r)=>{w(e,s,r);const n=i=>{if(i.kind===p.Agg){const h=i.list.split(".")[0],t=r.has(h)?r.get(h)||"":a.state?.[h]?.type||"",o=t.startsWith("list<")?t.slice(5,-1):"";t&&!t.startsWith("list<")&&!j.has(h)&&l.push(u("agg-not-list",`\`${i.op}(\u2026)\` needs a list, but "${i.list}" is "${t}".`,{loc:s}));const f=new Map([...r,[i.param,o]]);if(i.op!=="count"&&i.op!=="sort"&&i.op!=="sortDesc"){const c=A(i.body,f);c&&c!=="number"&&l.push(u("agg-type",`\`${i.op}(\u2026)\` reduces a NUMBER, but the body is "${c}". Use a number projection (count uses a true/false condition).`,{loc:s}))}b(i.body,s,f);return}if(i.kind===p.Bin)n(i.left),n(i.right);else if(i.kind===p.Un)n(i.operand);else if(i.kind===p.Tern)n(i.cond),n(i.then),n(i.else);else if(i.kind===p.Obj)for(const h of i.fields)n(h.value);else if(i.kind===p.Call)for(const h of i.args)n(h)};n(e),z(e,s);for(const i of O(e)){const h=i.indexOf("."),t=h===-1?i:i.slice(0,h);if(!(r.has(t)||M.has(t)||j.has(t)||X.has(t)||Q.has(t)||x.has(t))){const g=y(t,[...M,...r.keys()]);l.push(u("unknown-ref",`"${t}" is not a known state or item variable here${g?"":` \u2014 if it's a text/enum value, quote it: "${t}"`}`,{loc:s,suggestion:g,from:t}));continue}if(h===-1)continue;const o=i.slice(h+1).split(".")[0],f=r.has(t)?r.get(t)||"":a.state?.[t]?.type||"",c=P(f);if(c)if(!c.has(o))l.push(u("unknown-member",`"${o}" is not a field of ${f} (${r.has(t)?"item":"state"} "${t}")`,{loc:s,suggestion:y(o,[...c]),from:o}));else{const g=i.slice(h+1).split(".")[1],m=a.entities?.[f]?.[o]||"";g&&!a.entities?.[m]&&l.push(u("unknown-member",`"${o}" is ${m.startsWith("enum:")?"an enum":`a ${m}`} \u2014 it has no field "${g}"`,{loc:s,from:g}))}else if(W.includes(f))l.push(u("unknown-member",`"${t}" is a ${f} \u2014 it has no field "${o}"`,{loc:s}));else if(x.has(t)&&!M.has(t)){const g=new Set(["pending","error"]);g.has(o)||l.push(u("unknown-member",`action "${t}" exposes only .pending / .error, not "${o}"`,{loc:s,suggestion:y(o,[...g]),from:o}))}else if(f.startsWith("list<")){const g=!!a.state?.[t]?.source,m=g?new Set(["length","data","loading","error"]):new Set(["length"]);m.has(o)||l.push(u("unknown-member",`"${t}" is a list \u2014 no member "${o}" (lists expose ${g?".length / .data / .loading / .error":"only .length"}; use \`each ${t} as item\` to read an element)`,{loc:s,suggestion:y(o,[...m]),from:o}))}else L(t,o,s)}},V=new Set,B=(e,s,r=!1)=>{const n=Y[e];if(!n){l.push(u("missing-node",`node ${e} does not exist`));return}if(V.has(e)){l.push(u("dup-node",`${e} is referenced twice`,{loc:n.loc}));return}if(V.add(e),n.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:n.loc})),!D.has(n.type))if(!G.has(n.type))n.args?l.push(u("unknown-part",`"${n.type}" is not a known part`,{loc:n.loc,suggestion:y(n.type,[...d.parts||[],...D]),from:n.type})):l.push(u("unknown-type",`"${n.type}" is not a known primitive`,{loc:n.loc,suggestion:y(n.type,[...G]),from:n.type}));else{const o=st[n.type],f=o?o.props:{};for(const[c,g]of Object.entries(f))!g.endsWith("?")&&!(c in(n.props||{}))&&l.push(u("missing-prop",`${n.type} is missing the required "${c}"`,{loc:n.loc}))}const i=n.props||{};for(const o of it)o in i&&U(i[o],n);if(i.action&&(q(i.action,n),typeof i.action=="string"&&!i.action.includes(".")&&a.actions?.[i.action]?.input&&i.arg===void 0&&l.push(u("action-arity",`action "${i.action}" takes an argument (it reads "${a.actions[i.action].input}") \u2014 pass it, e.g. \`-> ${i.action}(row)\``,{loc:n.loc}))),i.submit&&q(i.submit,n),Array.isArray(i.class))for(const o of i.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:n.loc,from:o}));if(Array.isArray(i.where))for(const o of i.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:n.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:n.loc,from:o.trim()})));if(n.type==="Form"){const o=String(i.bind??"").replace(/^@/,""),f=o.split(".")[0],c=f?a.state?.[f]?.type:void 0;c===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:n.loc})):a.entities?.[c]||l.push(u("form-bind",`Form must bind a state typed as an entity (a draft): "${f}" is "${c}". Declare \`entity X { \u2026 }\` + \`${f} = {} : X\`, or use SearchField for a single text input.`,{loc:n.loc}))}if(n.type==="DataTable"){const o=String(i.data??"").replace(/^@/,""),f=a.state?.[o]?.type||"",c=f.startsWith("list<")?f.slice(5,-1):"",g=P(c);if(g){for(const m of i.columns||[])typeof m=="string"&&!g.has(m)&&l.push(u("unknown-column",`column "${m}" is not a field of ${c}`,{loc:n.loc,suggestion:y(m,[...g]),from:m}));for(const m of i.where||[])if(typeof m=="string"){const E=m.trim().split(/\s+/)[0];E&&!g.has(E)&&l.push(u("unknown-where-field",`where "${m.trim()}": "${E}" is not a field of ${c}`,{loc:n.loc,suggestion:y(E,[...g]),from:E}));for(const T of m.matchAll(/@(\w+)/g))M.has(T[1])||l.push(u("unknown-ref",`where "${m.trim()}": "@${T[1]}" is not a declared state`,{loc:n.loc,from:"@"+T[1]}))}}}if(n.type==="SearchField"){const o=String(i.bind??"").replace(/^@/,"").split(".")[0],f=o?a.state?.[o]?.type:void 0;f!==void 0&&!["text","string","email"].includes(f)&&l.push(u("bind-type",`SearchField binds a single text value, but "${o}" is "${f}" \u2014 bind a text state (e.g. \`q = "" : text\`).`,{loc:n.loc}))}if(n.type==="Custom"){for(const o of Object.values(i.inputs||{}))typeof o=="string"&&U(o,n);for(const o of Object.values(i.on||{}))typeof o=="string"&&q(o,n)}if(Array.isArray(i.style)){const o=d.theme||Z,f=Object.keys(o.space||{}).length>0;for(const c of i.style)tt(c)?f&&J(c,o)===null&&l.push(u("unknown-token",`"${c}": that step isn't in your theme scale`,{loc:n.loc,suggestion:y(c,_),from:c})):l.push(u("unknown-token",`"${c}" is not an accepted style token`,{loc:n.loc,suggestion:y(c,_),from:c}))}n.type===S.When&&i.cond&&b(i.cond,n.loc??null,s),n.type===S.Each&&i.list&&b(i.list,n.loc??null,s),i.arg&&typeof i.arg=="object"&&"kind"in i.arg&&b(i.arg,n.loc??null,s);const h=[];(n.type===S.Text||n.type===S.Title||n.type===S.Span)&&i.value&&h.push(i.value),n.type===S.Image&&(i.src&&h.push(i.src),i.alt&&h.push(i.alt)),n.type===S.Link&&i.to&&h.push(i.to),i.label&&h.push(i.label);for(const o of h)if(typeof o=="object"&&"kind"in o&&o.kind===p.Interp)for(const f of o.parts)typeof f!="string"&&b(f,n.loc??null,s);let t=s;if(n.type===S.Each&&i.as)t=new Map([...s,[i.as,H(i.list)]]),i.filter&&b(i.filter,n.loc??null,t);else if(n.type==="DataTable"){const o=String(i.data||"").replace(/^@/,""),f=a.state?.[o]?.type||"";t=new Map([...s,["row",f.startsWith("list<")?f.slice(5,-1):""]])}for(const o of n.children||[])B(o,t,n.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 e of Object.keys(a.gets||{}))l.push(u("store-only",`\`get ${e}\` 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 s of Object.values(a.gets||{}))b(s,null,new Map);const e=s=>{if(s.op===k.If){b(s.cond,null,new Map);for(const r of s.then||[])e(r);for(const r of s.else||[])e(r);return}if("target"in s&&s.target&&!M.has(s.target)&&l.push(u("undeclared-mutation",`effect mutates "${s.target}" \u2014 not a state of this store`,{suggestion:y(s.target,[...M]),from:s.target})),s.op===k.Remove)b(s.pred,null,new Map([[s.param,""]]));else if(s.op===k.Patch){const r=new Map([[s.param,""]]);b(s.pred,null,r),b(s.patch,null,r)}else if(s.op===k.Refetch)for(const r of Object.values(s.params))b(r,null,new Map);else s.op===k.Request?s.body&&b(s.body,null,new Map):"arg"in s&&s.arg&&b(s.arg,null,new Map)};for(const s of a.effects||[])for(const r of s)e(r)}for(const[e,s]of Object.entries(a.actions||{})){const r=new Set(s.mutates||[]),n=new Map(s.input?[[s.input,""]]:[]),i=t=>{if(t.op===k.If){b(t.cond,null,n);for(const o of t.then||[])h(o);for(const o of t.else||[])h(o);return}if(t.op===k.Call){j.has(t.target)?R.get(t.target)?.has(t.method)||l.push(u("unknown-action",`store "${t.target}" has no member "${t.method}".`,{suggestion:y(t.method,[...R.get(t.target)||[]]),from:t.method})):l.push(u("unknown-action",`"${t.target}.${t.method}(\u2026)": "${t.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:y(t.target,[...j]),from:t.target}));for(const o of t.args)b(o,null,n);return}if(K.has(t.op)||l.push(u("unknown-op",`action "${e}" uses unknown op "${t.op}"`,{suggestion:y(t.op,[...K]),from:t.op})),"target"in t&&t.target&&!r.has(t.target)&&l.push(u("undeclared-mutation",`action "${e}" mutates "${t.target}" but only declares mutates(${[...r].join(", ")||"\u2205"})`,{suggestion:y(t.target,[...r]),from:t.target})),ot.has(t.op)&&"target"in t&&t.target){const o=a.state?.[t.target];o&&!o.source&&l.push(u("missing-source",`action "${e}": "${t.target}.${t.op}(\u2026)" needs a query/source-backed list, but "${t.target}" is local (no source). Use \`= query <name>\` + a \`sources\` entry, or local ops (push/set/reset/remove).`,{from:t.target}))}if((t.op===k.Push||t.op===k.Create||t.op===k.Set)&&t.arg){const o=a.state?.[t.target]?.type||"",f=t.op===k.Set?o:o.startsWith("list<")?o.slice(5,-1):"";if(f&&a.entities?.[f]){const c=t.arg;if(c.kind===p.Obj){const g=a.entities[f];for(const m of c.fields)m.key in g||l.push(u("unknown-field",`action "${e}": "${m.key}" is not a field of ${f}`,{suggestion:y(m.key,Object.keys(g)),from:m.key}))}else{const g=c.kind===p.Lit?typeof c.value=="number"?"number":typeof c.value=="boolean"?"bool":"text":c.kind===p.Ref&&!c.name.includes(".")&&a.state?.[c.name]?.type||"";g&&g!==f&&l.push(u(t.op===k.Set?"set-type":"push-type",t.op===k.Set?`action "${e}": setting "${t.target}" (a ${f} draft) to a ${g} \u2014 assign a ${f} (a draft/state of that entity).`:`action "${e}": pushing a ${g} into list<${f}> "${t.target}" \u2014 push a ${f} (a draft/state of that entity).`))}}}if(t.op===k.Remove)b(t.pred,null,new Map([...n,[t.param,""]]));else if(t.op===k.Patch){const o=a.state?.[t.target]?.type||"",f=o.startsWith("list<")?o.slice(5,-1):"",c=new Map([...n,[t.param,f]]);if(b(t.pred,null,c),b(t.patch,null,c),f&&a.entities?.[f]&&t.patch.kind===p.Obj){const g=a.entities[f];for(const m of t.patch.fields)m.key in g||l.push(u("unknown-field",`action "${e}": "${m.key}" is not a field of ${f}`,{suggestion:y(m.key,Object.keys(g)),from:m.key}))}}else if(t.op===k.Refetch)for(const o of Object.values(t.params))b(o,null,n);else t.op===k.Request?t.body&&b(t.body,null,n):"arg"in t&&t.arg&&b(t.arg,null,n)},h=t=>{const o=l.length;i(t);for(let f=o;f<l.length;f++)l[f].loc||(l[f].loc=t.loc??null)};for(const t of s.body||[])h(t)}return{ok:l.length===0,diagnostics:l}}export{ut as validate};
|
|
1
|
+
import{resolveToken as Z,SUGGESTED as _,defaultTheme as tt,isKnownTokenShape as et}from"#engine/style/tokens.js";import{diag as c,closest as b}from"#engine/shared/diagnostics.js";import{PRIMITIVE_NAMES as nt,ACTION_OPS as st,PRIMITIVES as it}from"#engine/lang/manifest.js";import{Nt as v,Ek as h,StOp as k,BOp as w}from"#engine/shared/vocab.js";const G=new Set([...nt,v.Shell]),ot=["bind","data"],K=new Set(st),rt=new Set([k.Create,k.Update,k.Delete,k.Refetch]),N=["text","number","bool","uuid","email","string"];function S(o,m=[]){if(o.kind===h.Ref)m.push(o.name);else if(o.kind===h.Un)S(o.operand,m);else if(o.kind===h.Bin)S(o.left,m),S(o.right,m);else if(o.kind===h.Tern)S(o.cond,m),S(o.then,m),S(o.else,m);else if(o.kind===h.Call)for(const a of o.args)S(a,m);else if(o.kind===h.Obj)for(const a of o.fields)S(a.value,m);else(o.kind===h.Agg||o.kind===h.Filter)&&m.push(o.list);return m}function O(o,m=[]){if(o.kind===h.Call){m.push(o.fn);for(const a of o.args)O(a,m)}else if(o.kind===h.Un)O(o.operand,m);else if(o.kind===h.Bin)O(o.left,m),O(o.right,m);else if(o.kind===h.Tern)O(o.cond,m),O(o.then,m),O(o.else,m);else if(o.kind===h.Obj)for(const a of o.fields)O(a.value,m);else o.kind===h.Filter&&O(o.cond,m);return m}function ct(o,m={}){const a=[],M=new Set(Object.keys(o.state||{})),x=new Set(m.stores||[]),X=new Set(Object.keys(o.consts||{})),Q=new Set(o.params||[]),E=new Set(Object.keys(o.actions||{})),Y=new Set(Object.keys(o.gets||{})),F=new Set((o.imports||[]).flatMap(i=>i.names)),z=o.nodes||{},H=(i,e)=>{for(const r of O(i))F.has(r)||a.push(c("unknown-function",`"${r}" is not a use'd function`,{loc:e,suggestion:b(r,[...F]),from:r}))},L=new Map;for(const[i,e]of Object.entries(o.state||{})){if(!e.source?.startsWith("query:"))continue;const r=new Set(["loading","error","data"]),s=o.entities?.[e.type];if(s){r.add("id");for(const n of Object.keys(s))r.add(n)}L.set(i,r)}const R=new Map;for(const[i,e]of Object.entries(m.storeMembers||{}))R.set(i,new Set(e));const C=(i,e,r)=>{const s=L.get(i);if(s){s.has(e)||a.push(c("unknown-member",`"${e}" is not a member of query "${i}"`,{loc:r,suggestion:b(e,[...s]),from:e}));return}const n=R.get(i);n&&!n.has(e)&&a.push(c("unknown-member",`"${e}" is not a member of store "${i}"`,{loc:r,suggestion:b(e,[...n]),from:e}))},D=Object.keys(o.entities||{});for(const[i,e]of Object.entries(o.state||{})){const r=e.type;if(e.source?.startsWith("query:")&&r!=="list"&&!r.startsWith("list<")&&a.push(c("query-not-list",`state "${i}" 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:e.loc,suggestion:`list<${r}>`})),r==="list")a.push(c("untyped-list",`state "${i}" is an untyped "list" \u2014 declare the element type, e.g. list<uuid> or list<User>`,{loc:e.loc,suggestion:"list<uuid>"}));else if(r.startsWith("list<")){const s=r.slice(5,-1);!N.includes(s)&&!D.includes(s)&&a.push(c("unknown-type",`list element "${s}" is not a known entity or scalar type`,{loc:e.loc,suggestion:b(s,[...D,...N]),from:s}))}else if(e.initial!==void 0&&e.initial!==null){const s=r==="number"?"number":r==="bool"?"boolean":["text","string","email","uuid"].includes(r)?"string":"";s&&typeof e.initial!==s&&a.push(c("type-mismatch",`state "${i}" is typed "${r}" but its initial value is a ${typeof e.initial}`,{loc:e.loc}))}}const I=i=>{const e=o.entities?.[i];return e?new Set(["id",...Object.keys(e)]):null},W=(i,e)=>{const r=o.state?.[e]?.type||"",s=r.startsWith("list<")?r.slice(5,-1):"",n=new Map(i),g=s?o.entities?.[s]:void 0;if(g){n.set("id","uuid");for(const[f,t]of Object.entries(g))n.set(f,t)}return n},P=(i,e,r,s)=>{const n=o.state?.[i]?.type||"",g=n.startsWith("list<")?n.slice(5,-1):"",f=g?o.entities?.[g]:void 0;if(!f)return;const t=new Set(["id",...Object.keys(f)]),l=new Set;for(const u of r)for(const p of S(u)){const d=p.split(".")[0];t.has(d)&&e.has(d)&&!l.has(d)&&(l.add(d),a.push(c("item-shadow",`"${d}" is both a field of ${g} and a param here \u2014 inside \`where\`/\`with\` the item field wins, so the param "${d}" is unreachable. Rename the param (e.g. "${d}Arg").`,{loc:s,from:d})))}},J=i=>{if(!i)return"";const e=i.kind===h.Ref?i.name:i.kind===h.Agg&&(i.op==="sort"||i.op==="sortDesc")||i.kind===h.Filter?i.list:"";if(!e)return"";const r=o.state?.[e.split(".")[0]]?.type||"";return r.startsWith("list<")?r.slice(5,-1):""},U=(i,e)=>{if(typeof i=="string"&&i.startsWith("@")){const r=i.slice(1).split(".")[0];if(!M.has(r)&&!x.has(r)){const s=b(r,[...M]);a.push(c("unknown-ref",`"@${r}" is not a declared state`,{loc:e.loc,suggestion:s?"@"+s:null,from:"@"+r,related:s?o.state?.[s]?.loc??null:null}))}}},q=(i,e)=>{if(!(!i||i.startsWith("$"))){if(i.includes(".")){const r=i.indexOf(".");C(i.slice(0,r),i.slice(r+1).split(".")[0],e.loc??null);return}E.has(i)||a.push(c("unknown-action",`"${i}" is not a declared action`,{loc:e.loc,suggestion:b(i,[...E]),from:i}))}},A=(i,e)=>{if(i.kind===h.Lit)return typeof i.value=="number"?"number":typeof i.value=="boolean"?"bool":"text";if(i.kind===h.Ref){const[r,...s]=i.name.split("."),n=e.has(r)?e.get(r)||"":o.state?.[r]?.type||"";return s.length?s.length===1&&o.entities?.[n]?.[s[0]]||"":n}return i.kind===h.Agg?i.op==="sort"||i.op==="sortDesc"?"":"number":""},j=(i,e,r)=>{if(i.kind===h.Bin){if(i.op===w.Sub||i.op===w.Mul||i.op===w.Div)for(const s of[i.left,i.right]){const n=A(s,r);n&&n!=="number"&&a.push(c("arith-type",`arithmetic \`${i.op}\` needs numbers, but an operand is "${n}" \u2014 declare it \`: number\`.`,{loc:e}))}if([w.Eq,w.Neq,w.Lt,w.Gt,w.Lte,w.Gte].includes(i.op)&&!(i.left.kind===h.Lit&&i.left.value===null)&&!(i.right.kind===h.Lit&&i.right.value===null)){const s=f=>f==="number"||f==="bool"?f:f.startsWith("enum:")||["text","string","email","uuid"].includes(f)?"text":f,n=A(i.left,r),g=A(i.right,r);n&&g&&s(n)!==s(g)&&a.push(c("compare-type",`comparing a ${n} to a ${g} \u2014 they never match (always ${i.op===w.Neq?"true":"false"}). Likely a quoted number (\`== "1"\` vs \`== 1\`) or a type mismatch.`,{loc:e}))}j(i.left,e,r),j(i.right,e,r)}else if(i.kind===h.Un)j(i.operand,e,r);else if(i.kind===h.Tern)j(i.cond,e,r),j(i.then,e,r),j(i.else,e,r);else if(i.kind===h.Obj)for(const s of i.fields)j(s.value,e,r);else if(i.kind===h.Call)for(const s of i.args)j(s,e,r)},y=(i,e,r)=>{j(i,e,r);const s=n=>{if(n.kind===h.Agg){const g=n.list.split(".")[0],f=r.has(g)?r.get(g)||"":o.state?.[g]?.type||"",t=f.startsWith("list<")?f.slice(5,-1):"";f&&!f.startsWith("list<")&&!x.has(g)&&a.push(c("agg-not-list",`\`${n.op} \u2026\` needs a list, but "${n.list}" is "${f}".`,{loc:e}));const l=new Map(r),u=t?o.entities?.[t]:void 0;if(u){l.set("id","uuid");for(const[p,d]of Object.entries(u))l.set(p,d)}if(n.op!=="count"&&n.op!=="sort"&&n.op!=="sortDesc"){const p=A(n.body,l);p&&p!=="number"&&a.push(c("agg-type",`\`${n.op} \u2026\` reduces a NUMBER, but the body is "${p}". Use a number projection (count uses a true/false condition).`,{loc:e}))}y(n.body,e,l);return}if(n.kind===h.Filter){const g=n.list.split(".")[0],f=r.has(g)?r.get(g)||"":o.state?.[g]?.type||"",t=f.startsWith("list<")?f.slice(5,-1):"";f&&!f.startsWith("list<")&&!x.has(g)&&a.push(c("filter-not-list",`\`${n.list} where \u2026\` needs a list, but "${n.list}" is "${f}".`,{loc:e}));const l=t?o.entities?.[t]:void 0,u=new Map(r);if(l){u.set("id","uuid");for(const[p,d]of Object.entries(l))u.set(p,d)}y(n.cond,e,u);return}if(n.kind===h.Bin)s(n.left),s(n.right);else if(n.kind===h.Un)s(n.operand);else if(n.kind===h.Tern)s(n.cond),s(n.then),s(n.else);else if(n.kind===h.Obj)for(const g of n.fields)s(g.value);else if(n.kind===h.Call)for(const g of n.args)s(g)};s(i),H(i,e);for(const n of S(i)){const g=n.indexOf("."),f=g===-1?n:n.slice(0,g);if(!(r.has(f)||M.has(f)||Y.has(f)||x.has(f)||X.has(f)||Q.has(f)||E.has(f))){const p=b(f,[...M,...r.keys()]);a.push(c("unknown-ref",`"${f}" is not a known state or item variable here${p?"":` \u2014 if it's a text/enum value, quote it: "${f}"`}`,{loc:e,suggestion:p,from:f}));continue}if(g===-1)continue;const t=n.slice(g+1).split(".")[0],l=r.has(f)?r.get(f)||"":o.state?.[f]?.type||"",u=I(l);if(u)if(!u.has(t))a.push(c("unknown-member",`"${t}" is not a field of ${l} (${r.has(f)?"item":"state"} "${f}")`,{loc:e,suggestion:b(t,[...u]),from:t}));else{const p=n.slice(g+1).split(".")[1],d=o.entities?.[l]?.[t]||"";p&&!o.entities?.[d]&&a.push(c("unknown-member",`"${t}" is ${d.startsWith("enum:")?"an enum":`a ${d}`} \u2014 it has no field "${p}"`,{loc:e,from:p}))}else if(N.includes(l))a.push(c("unknown-member",`"${f}" is a ${l} \u2014 it has no field "${t}"`,{loc:e}));else if(E.has(f)&&!M.has(f)){const p=new Set(["pending","error"]);p.has(t)||a.push(c("unknown-member",`action "${f}" exposes only .pending / .error, not "${t}"`,{loc:e,suggestion:b(t,[...p]),from:t}))}else if(l.startsWith("list<")){const p=!!o.state?.[f]?.source,d=p?new Set(["length","data","loading","error"]):new Set(["length"]);d.has(t)||a.push(c("unknown-member",`"${f}" is a list \u2014 no member "${t}" (lists expose ${p?".length / .data / .loading / .error":"only .length"}; use \`each ${f} as item\` to read an element)`,{loc:e,suggestion:b(t,[...d]),from:t}))}else C(f,t,e)}},V=new Set,B=(i,e,r=!1)=>{const s=z[i];if(!s){a.push(c("missing-node",`node ${i} does not exist`));return}if(V.has(i)){a.push(c("dup-node",`${i} is referenced twice`,{loc:s.loc}));return}if(V.add(i),s.type==="RowAction"&&!r&&a.push(c("rowaction-context","RowAction only works inside a DataTable (it renders a button per row). Use Button for a standalone action.",{loc:s.loc})),!G.has(s.type))s.args?a.push(c("unknown-part",`"${s.type}" is not a known part`,{loc:s.loc,suggestion:b(s.type,[...m.parts||[]]),from:s.type})):a.push(c("unknown-type",`"${s.type}" is not a known primitive`,{loc:s.loc,suggestion:b(s.type,[...G]),from:s.type}));else{const t=it[s.type],l=t?t.props:{};for(const[u,p]of Object.entries(l))!p.endsWith("?")&&!(u in(s.props||{}))&&a.push(c("missing-prop",`${s.type} is missing the required "${u}"`,{loc:s.loc}))}const n=s.props||{};for(const t of ot)t in n&&U(n[t],s);if(n.action&&(q(n.action,s),typeof n.action=="string"&&!n.action.includes(".")&&(o.actions?.[n.action]?.input||o.actions?.[n.action]?.params?.length)&&n.arg===void 0&&a.push(c("action-arity",`action "${n.action}" takes an argument (it reads "${o.actions[n.action].params?.length?o.actions[n.action].params.map(t=>t.name).join(", "):o.actions[n.action].input}") \u2014 pass it, e.g. \`-> ${n.action}(row)\``,{loc:s.loc}))),n.submit&&q(n.submit,s),Array.isArray(n.class))for(const t of n.class)typeof t=="string"&&t.includes("{")&&a.push(c("class-interp",`class() does not interpolate "{\u2026}": "${t}" would ship the braces literally. For a dynamic class use \`class(name when cond)\` (e.g. \`class(stage-applied when status == "applied")\`).`,{loc:s.loc,from:t}));if(Array.isArray(n.where))for(const t of n.where)typeof t=="string"&&t.trim()&&(/(?:==|\bcontains\b)/.test(t)?/\b(?:and|or)\b/.test(t)&&a.push(c("unsupported-where",`where clause "${t.trim()}" \u2014 combine conditions with a COMMA, not \`and\`/\`or\`: where(role == admin, name contains @q).`,{loc:s.loc,from:t.trim()})):a.push(c("unsupported-where",`where clause "${t.trim()}" \u2014 where() supports only \`==\` and \`contains\` (e.g. where(role == admin, name contains @q)).`,{loc:s.loc,from:t.trim()})));if(s.type==="Form"){const t=String(n.bind??"").replace(/^@/,""),l=t.split(".")[0],u=l?o.state?.[l]?.type:void 0;u===void 0?a.push(c("form-bind",`Form must bind a page-local draft (a state typed as an entity)${t?`, but "${t}" is not a state on this page`:""}${t.includes(".")?" \u2014 a Form cannot bind a store field; declare a local `draft = {} : Entity` and submit the store action":""}.`,{loc:s.loc})):o.entities?.[u]||a.push(c("form-bind",`Form must bind a state typed as an entity (a draft): "${l}" is "${u}". Declare \`entity X { \u2026 }\` + \`${l} = {} : X\`, or use SearchField for a single text input.`,{loc:s.loc}))}if(s.type==="DataTable"){const t=String(n.data??"").replace(/^@/,""),l=o.state?.[t]?.type||"",u=l.startsWith("list<")?l.slice(5,-1):"",p=I(u);if(p){for(const d of n.columns||[])typeof d=="string"&&!p.has(d)&&a.push(c("unknown-column",`column "${d}" is not a field of ${u}`,{loc:s.loc,suggestion:b(d,[...p]),from:d}));for(const d of n.where||[])if(typeof d=="string"){const $=d.trim().split(/\s+/)[0];$&&!p.has($)&&a.push(c("unknown-where-field",`where "${d.trim()}": "${$}" is not a field of ${u}`,{loc:s.loc,suggestion:b($,[...p]),from:$}));for(const T of d.matchAll(/@(\w+)/g))M.has(T[1])||a.push(c("unknown-ref",`where "${d.trim()}": "@${T[1]}" is not a declared state`,{loc:s.loc,from:"@"+T[1]}))}}}if(s.type==="SearchField"){const t=String(n.bind??"").replace(/^@/,"").split(".")[0],l=t?o.state?.[t]?.type:void 0;l!==void 0&&!["text","string","email"].includes(l)&&a.push(c("bind-type",`SearchField binds a single text value, but "${t}" is "${l}" \u2014 bind a text state (e.g. \`q = "" : text\`).`,{loc:s.loc}))}if(s.type==="Custom"){for(const t of Object.values(n.inputs||{}))typeof t=="string"&&U(t,s);for(const t of Object.values(n.on||{}))typeof t=="string"&&q(t,s)}if(Array.isArray(n.style)){const t=m.theme||tt,l=Object.keys(t.space||{}).length>0;for(const u of n.style)et(u)?l&&Z(u,t)===null&&a.push(c("unknown-token",`"${u}": that step isn't in your theme scale`,{loc:s.loc,suggestion:b(u,_),from:u})):a.push(c("unknown-token",`"${u}" is not an accepted style token`,{loc:s.loc,suggestion:b(u,_),from:u}))}if(s.type===v.When&&n.cond&&y(n.cond,s.loc??null,e),s.type===v.Each&&n.list&&y(n.list,s.loc??null,e),n.arg&&typeof n.arg=="object"&&"kind"in n.arg&&y(n.arg,s.loc??null,e),n.argRest)for(const t of n.argRest)y(t,s.loc??null,e);const g=[];(s.type===v.Text||s.type===v.Title||s.type===v.Span)&&n.value&&g.push(n.value),s.type===v.Image&&(n.src&&g.push(n.src),n.alt&&g.push(n.alt)),s.type===v.Link&&n.to&&g.push(n.to),n.label&&g.push(n.label);for(const t of g)if(typeof t=="object"&&"kind"in t&&t.kind===h.Interp)for(const l of t.parts)typeof l!="string"&&y(l,s.loc??null,e);let f=e;if(s.type===v.Each&&n.as)f=new Map([...e,[n.as,J(n.list)]]),n.filter&&y(n.filter,s.loc??null,f);else if(s.type==="DataTable"){const t=String(n.data||"").replace(/^@/,""),l=o.state?.[t]?.type||"";f=new Map([...e,["row",l.startsWith("list<")?l.slice(5,-1):""]])}for(const t of s.children||[])B(t,f,s.type==="DataTable")};if(o.rootId?B(o.rootId,new Map):m.kind!=="store"&&a.push(c("no-root","the doc is missing a rootId")),m.kind!=="store"){for(const i of Object.values(o.gets||{}))y(i,null,new Map);(o.effects||[]).length&&a.push(c("store-only","`effect { }` is only valid in a .store \u2014 a page reacts through `when`/`each`, not effects."))}if(m.kind==="store"){for(const e of Object.values(o.gets||{}))y(e,null,new Map);const i=e=>{if(e.op===k.If){y(e.cond,null,new Map);for(const r of e.then||[])i(r);for(const r of e.else||[])i(r);return}if("target"in e&&e.target&&!M.has(e.target)&&a.push(c("undeclared-mutation",`effect mutates "${e.target}" \u2014 not a state of this store`,{suggestion:b(e.target,[...M]),from:e.target})),e.op===k.Remove)y(e.pred,null,W(new Map,e.target));else if(e.op===k.Patch){const r=W(new Map,e.target);y(e.pred,null,r),y(e.patch,null,r)}else if(e.op===k.Refetch)for(const r of Object.values(e.params))y(r,null,new Map);else e.op===k.Request?e.body&&y(e.body,null,new Map):"arg"in e&&e.arg&&y(e.arg,null,new Map)};for(const e of o.effects||[])for(const r of e)i(r)}for(const[i,e]of Object.entries(o.actions||{})){const r=new Set(e.mutates||[]),s=new Map(e.params?.length?e.params.map(t=>[t.name,t.type]):e.input?[[e.input,""]]:[]),n=new Set((e.params||[]).map(t=>t.name)),g=t=>{if(t.op===k.If){y(t.cond,null,s);for(const l of t.then||[])f(l);for(const l of t.else||[])f(l);return}if(t.op===k.Call){x.has(t.target)?R.get(t.target)?.has(t.method)||a.push(c("unknown-action",`store "${t.target}" has no member "${t.method}".`,{suggestion:b(t.method,[...R.get(t.target)||[]]),from:t.method})):a.push(c("unknown-action",`"${t.target}.${t.method}(\u2026)": "${t.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:b(t.target,[...x]),from:t.target}));for(const l of t.args)y(l,null,s);return}if(K.has(t.op)||a.push(c("unknown-op",`action "${i}" uses unknown op "${t.op}"`,{suggestion:b(t.op,[...K]),from:t.op})),"target"in t&&t.target&&!r.has(t.target)&&a.push(c("undeclared-mutation",`action "${i}" mutates "${t.target}" but only declares mutates(${[...r].join(", ")||"\u2205"})`,{suggestion:b(t.target,[...r]),from:t.target})),rt.has(t.op)&&"target"in t&&t.target){const l=o.state?.[t.target];l&&!l.source&&a.push(c("missing-source",`action "${i}": "${t.target}.${t.op}(\u2026)" needs a query/source-backed list, but "${t.target}" is local (no source). Use \`= query <name>\` + a \`sources\` entry, or local ops (push/set/reset/remove).`,{from:t.target}))}if((t.op===k.Push||t.op===k.Create||t.op===k.Set)&&t.arg){const l=o.state?.[t.target]?.type||"",u=t.op===k.Set?l:l.startsWith("list<")?l.slice(5,-1):"";if(u&&o.entities?.[u]){const p=t.arg;if(p.kind===h.Obj){const d=o.entities[u];for(const $ of p.fields)$.key in d||a.push(c("unknown-field",`action "${i}": "${$.key}" is not a field of ${u}`,{suggestion:b($.key,Object.keys(d)),from:$.key}))}else{const d=p.kind===h.Lit?typeof p.value=="number"?"number":typeof p.value=="boolean"?"bool":"text":p.kind===h.Ref&&!p.name.includes(".")&&o.state?.[p.name]?.type||"";d&&d!==u&&a.push(c(t.op===k.Set?"set-type":"push-type",t.op===k.Set?`action "${i}": setting "${t.target}" (a ${u} draft) to a ${d} \u2014 assign a ${u} (a draft/state of that entity).`:`action "${i}": pushing a ${d} into list<${u}> "${t.target}" \u2014 push a ${u} (a draft/state of that entity).`))}}}if(t.op===k.Remove)y(t.pred,null,W(s,t.target)),P(t.target,n,[t.pred],null);else if(t.op===k.Patch){const l=o.state?.[t.target]?.type||"",u=l.startsWith("list<")?l.slice(5,-1):"",p=W(s,t.target);if(y(t.pred,null,p),y(t.patch,null,p),P(t.target,n,[t.pred,t.patch],null),u&&o.entities?.[u]&&t.patch.kind===h.Obj){const d=o.entities[u];for(const $ of t.patch.fields)$.key in d||a.push(c("unknown-field",`action "${i}": "${$.key}" is not a field of ${u}`,{suggestion:b($.key,Object.keys(d)),from:$.key}))}}else if(t.op===k.Refetch)for(const l of Object.values(t.params))y(l,null,s);else t.op===k.Request?t.body&&y(t.body,null,s):"arg"in t&&t.arg&&y(t.arg,null,s)},f=t=>{const l=a.length;g(t);for(let u=l;u<a.length;u++)a[u].loc||(a[u].loc=t.loc??null)};for(const t of e.body||[])f(t)}return{ok:a.length===0,diagnostics:a}}export{ct 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 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
|
|
1
|
+
import{ParseError as p}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 x,Ek as a,AGG_OPS as P,SORT_OPS as m}from"#engine/shared/vocab.js";const g=[[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]],l=new Map([[o.True,!0],[o.False,!1],[o.Null,null]]);class d{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 p(`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 g)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:x.Not,operand:this.unary()}):this.primary()}primary(){if(this.at(e.Punct,i.ParenL)){this.next();const c=this.ternary();return this.eat(e.Punct,i.ParenR),c}if(this.at(e.Punct,i.BraceL)){this.next();const c=[];for(;!this.at(e.Punct,i.BraceR);){const f=this.eat(e.Ident).v;this.eat(e.Punct,i.Colon),c.push({key:f,value:this.ternary()}),this.at(e.Punct,i.Comma)&&this.next()}return this.eat(e.Punct,i.BraceR),{kind:a.Obj,fields:c}}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=l.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;const r=t.lastIndexOf("."),s=r===-1?"":t.slice(r+1),u=P.has(s)||m.has(s);if(u&&(this.at(e.Ident,o.By)||this.at(e.Ident,o.Where)))return this.next(),{kind:a.Agg,op:s,list:t.slice(0,r),body:this.parseExpr()};if(!u&&this.at(e.Ident,o.Where))return this.next(),{kind:a.Filter,list:t,cond:this.parseExpr()};if(this.at(e.Punct,i.ParenL)){if(u)throw new p(`\`${s}\` takes ${s==="count"?"`where <cond>`":"`by <expr>`"} now, not a \`(x => \u2026)\` lambda \u2014 write \`${t.slice(0,r)}.${s} ${s==="count"?"where <cond>":"by <expr>"}\` (item fields read bare)`,this.locOf(this.peek().pos));this.next();const c=[];for(;!this.at(e.Punct,i.ParenR);)c.push(this.ternary()),this.at(e.Punct,i.Comma)&&this.next();return this.eat(e.Punct,i.ParenR),{kind:a.Call,fn:t,args:c}}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 d(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=l.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{d as Grammar};
|
|
@@ -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",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","every","live","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
|
|
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","every","live","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(uid: text) mutates users { users.remove where id == uid }`.",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`.",every:"Poll a query on a timer: `query orders every 5s` (also `500ms`, `2m`). Silent auto-refetch \u2014 keyed reconciliation updates only the rows that changed (no full re-render, no loading flash).",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","toggle","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). Prefer `patch` to edit in place; `remove where \u2026 ` + `push(\u2026)` only if you truly need to reorder.',remove:"Remove matching items locally (item fields bare): `users.remove where id == userId`. The param must be named differently from any field (the oracle flags `id == id`).",patch:"Edit matching items IN PLACE (position-preserving): `todos.patch where id == todoId with { done: not done }`. Item fields are bare; list ONLY 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()`.",toggle:"Flip a bool state: `open.toggle()` (same as `open.set(not open)`).",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 x}from"#engine/lang/grammar.js";import{Tk as t,Pn as i,Kw as a,Nt as P,Mod as m,StOp as h,Ek as R}from"#engine/shared/vocab.js";const I={};for(const[u,s]of Object.entries(g))s.string&&(I[u]=s.string);const S=new Set(Object.entries(g).filter(([,u])=>u.interp).map(([u])=>u)),y=u=>u==="text"?"string":u,E=u=>/^h[1-6]$/.test(u);class k extends x{modifiers;statements;constructor(s){super(s),this.modifiers=new Map([[m.Bind,e=>{if(this.at(t.Ref)){let n=this.eat(t.Ref).v;for(;this.at(t.Punct,i.Dot);)this.next(),n+="."+this.eat(t.Ident).v;e.bind=n}else e.bind=this.parseDotted()}],[m.Submit,e=>{e.submit=this.parseDotted()}],[m.Where,e=>{e.where=this.parseParenList(()=>this.rebuildClause())}],[m.Columns,e=>{e.columns=this.parseParenList(()=>this.eat(t.Ident).v)}],[m.Style,e=>{e.style=this.parseParenList(()=>this.parseStyleToken())}],[m.Class,e=>{e.class=this.parseParenList(()=>{const n=this.at(t.String)?this.next().v:this.eat(t.Ident).v;return this.at(t.Ident,a.When)?(this.next(),{name:n,cond:this.parseExpr()}):n})}],[m.Alt,e=>{e.alt=this.parseInterpolation(this.eat(t.String).v)}],[m.Inputs,e=>{e.inputs=this.parseArgs()}],[m.On,e=>{e.on=this.parseArgs()}]]),this.statements=new Map([[h.Push,e=>({op:h.Push,target:e,arg:this.parseExpr()})],[h.Set,e=>({op:h.Set,target:e,arg:this.parseExpr()})],[h.Reset,e=>({op:h.Reset,target:e})],[h.Remove,e=>{const n=this.eat(t.Ident).v;return this.eat(t.FatArrow),{op:h.Remove,target:e,param:n,pred:this.parseExpr()}}],[h.Patch,e=>{const n=this.eat(t.Ident).v;this.eat(t.FatArrow);const r=this.parseExpr();return this.eat(t.Punct,i.Comma),{op:h.Patch,target:e,param:n,pred:r,patch:this.parseExpr()}}],[h.Create,e=>({op:h.Create,target:e,arg:this.parseExpr()})],[h.Update,e=>({op:h.Update,target:e,arg:this.parseExpr()})],[h.Delete,e=>({op:h.Delete,target:e,arg:this.parseExpr()})],[h.Refetch,e=>{const n={};for(;!this.at(t.Punct,i.ParenR);){const r=this.eat(t.Ident).v;this.eat(t.Punct,i.Colon),n[r]=this.parseExpr(),this.at(t.Punct,i.Comma)&&this.next()}return{op:h.Refetch,target:e,params:n}}]])}parse(){const s={screen:"",entities:{},state:{},actions:{},tree:null},e=new Map([[a.Screen,()=>{this.next(),s.screen=this.eat(t.Ident).v}],[a.Entity,()=>this.parseEntity(s)],[a.State,()=>this.parseState(a.State,s.state)],[a.Store,()=>{s.store=s.store||{},this.parseState(a.Store,s.store)}],[a.Get,()=>this.parseGet(s)],[a.Effect,()=>{this.next(),(s.effects=s.effects||[]).push(this.parseActionBody())}],[a.Action,()=>this.parseAction(s)],[a.Mock,()=>this.parseMock(s)],[a.Sources,()=>this.parseSources(s)],[a.Api,()=>this.parseApi(s)],[a.Meta,()=>this.parseMeta(s)],[a.Routes,()=>this.parseRoutes(s)],[a.Shell,()=>this.parseShell(s)],[a.Part,()=>this.parsePart(s)],[a.Const,()=>this.parseConst(s)],[a.Theme,()=>this.parseTheme(s)],[a.Param,()=>{this.next(),(s.params=s.params||[]).push(this.eat(t.Ident).v)}],[a.Use,()=>{this.next();const n=[this.eat(t.Ident).v];for(;this.at(t.Punct,i.Comma);)this.next(),n.push(this.eat(t.Ident).v);this.eat(t.Ident,a.From),(s.imports=s.imports||[]).push({names:n,from:this.eat(t.String).v})}]]);for(;!this.at(t.Eof);){const n=this.peek(),r=n.t===t.Ident?e.get(n.v):void 0;r?r():s.tree=this.parseNode()}return s}parseEntity(s){this.eat(t.Ident,a.Entity);const e=this.eat(t.Ident).v;this.eat(t.Punct,i.BraceL);const n={id:"uuid"},r={};for(;!this.at(t.Punct,i.BraceR);){const p=this.eat(t.Ident).v,o=[this.eat(t.Ident).v];for(;this.at(t.Punct,i.Pipe);)this.next(),o.push(this.eat(t.Ident).v);n[p]=o.length>1?"enum:"+o.join("|"):y(o[0]);const d=this.parseConstraints();Object.keys(d).length&&(r[p]=d)}this.eat(t.Punct,i.BraceR),s.entities[e]=n,Object.keys(r).length&&((s.constraints=s.constraints||{})[e]=r)}parseConstraints(){const s={};for(;this.at(t.Ident,a.Required)||this.at(t.Ident,a.Min)||this.at(t.Ident,a.Max);){const e=this.next().v;if(e===a.Required){s.required=!0;continue}this.eat(t.Punct,i.Colon);const n=Number(this.eat(t.Number).v);e===a.Min?s.min=n:s.max=n}return s}parseState(s,e){for(this.eat(t.Ident,s),this.eat(t.Punct,i.BraceL);!this.at(t.Punct,i.BraceR);){const n=this.eat(t.Ident);this.eat(t.Punct,i.Assign);let r,p,o,d,c=!1;this.at(t.Ident,a.Query)?(this.next(),r="query:"+this.eat(t.Ident).v,this.at(t.Ident,a.Live)?(this.next(),o=!0):this.at(t.Ident,a.Every)&&(this.next(),p=this.parseDuration())):this.at(t.Punct,i.BraceL)||this.at(t.Punct,i.BrackL)?(d=this.parseValue(),c=!0):this.at(t.String)?(d=this.next().v,c=!0):this.at(t.Number)?(d=Number(this.next().v),c=!0):this.at(t.Ident,a.True)||this.at(t.Ident,a.False)?(d=this.next().v===a.True,c=!0):this.at(t.Ident,"null")?(this.next(),c=!0):(d=this.next().v,c=!0),this.eat(t.Punct,i.Colon);const l=this.parseType(),v=this.locOf(n.pos);e[n.v]=r?{type:l,source:r,refresh:p,live:o,loc:v}:{type:l,initial:c?d:null,loc:v}}this.eat(t.Punct,i.BraceR)}parseDuration(){const s=this.peek(),e=Number(this.eat(t.Number).v),n=this.eat(t.Ident).v,r=n==="ms"?1:n==="s"?1e3:n==="m"?6e4:0;if(!r||!(e>0))throw new f("`every` expects a positive duration like `5s`, `500ms`, or `2m`",this.locOf(s.pos));return e*r}parseGet(s){this.eat(t.Ident,a.Get);const e=this.eat(t.Ident).v;this.eat(t.Punct,i.Assign),(s.gets=s.gets||{})[e]=this.parseExpr()}parseAction(s){this.eat(t.Ident,a.Action);const e=this.eat(t.Ident).v,n=[];if(this.at(t.Ident,a.Mutates))for(this.next(),n.push(this.eat(t.Ident).v);this.at(t.Punct,i.Comma);)this.next(),n.push(this.eat(t.Ident).v);let r="";this.at(t.LArrow)&&(this.next(),r=this.eat(t.Ident).v),s.actions[e]={mutates:n,input:r,body:this.parseActionBody()}}parseActionBody(){this.eat(t.Punct,i.BraceL);const s=[];for(;!this.at(t.Punct,i.BraceR);)s.push(this.parseStatement());return this.eat(t.Punct,i.BraceR),s}parseIf(){this.eat(t.Ident,a.If);const s=this.parseExpr(),e=this.parseActionBody(),n=this.at(t.Ident,a.Else)?(this.next(),this.parseActionBody()):null;return{op:h.If,cond:s,then:e,else:n}}parseRequest(){const s=this.eat(t.Ident).v.toUpperCase(),e=this.parseInterpolation(this.eat(t.String).v);let n=null;return this.at(t.Ident,a.Body)&&(this.next(),n=this.parseExpr()),{op:h.Request,method:s,url:e,body:n}}parseStatement(){const s=this.peek().pos,e=this.parseStatementInner();return e.loc=this.locOf(s),e}parseStatementInner(){if(this.at(t.Ident,a.If))return this.parseIf();if(this.at(t.Ident,"post")||this.at(t.Ident,"put")||this.at(t.Ident,"delete"))return this.parseRequest();const s=this.eat(t.Ident).v;this.eat(t.Punct,i.Dot);const e=this.eat(t.Ident).v;this.eat(t.Punct,i.ParenL);const n=this.statements.get(e);if(!n){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:h.Call,target:s,method:e,args:p}}const r=n(s);return this.eat(t.Punct,i.ParenR),r}parseMock(s){this.eat(t.Ident,a.Mock);const e=s.mock||{};this.parseEntries(n=>{e[n]=this.parseValue()}),s.mock=e}parseSources(s){this.eat(t.Ident,a.Sources);const e=s.sources||{};this.parseEntries(n=>{e[n]=this.parseValue()}),s.sources=e}parseApi(s){this.eat(t.Ident,a.Api);const e=s.api||{};this.parseEntries(n=>{e[n]=this.parseValue()}),s.api=e}parseMeta(s){this.eat(t.Ident,a.Meta),this.eat(t.Punct,i.BraceL);const e=s.meta||{};for(;!this.at(t.Punct,i.BraceR);){const n=this.eat(t.Ident).v;e[n]=this.eat(t.String).v}this.eat(t.Punct,i.BraceR),s.meta=e}parseRoutes(s){this.eat(t.Ident,a.Routes),this.eat(t.Punct,i.BraceL);const e=s.routes||[];for(;!this.at(t.Punct,i.BraceR);){const n=this.peek(),r=this.locOf(n.pos).line,p=this.pathOnLine(r);this.eat(t.Arrow);const o={url:p,page:this.eat(t.Ident).v,loc:this.locOf(n.pos)};this.at(t.Ident,a.Guard)&&(this.next(),o.guardNeg=this.at(t.Ident,a.Not)?(this.next(),!0):!1,o.guard=this.parseDotted(),this.eat(t.Ident,a.Else),o.redirect=this.pathOnLine(r)),e.push(o)}this.eat(t.Punct,i.BraceR),s.routes=e}parseShell(s){this.eat(t.Ident,a.Shell),s.shell={type:P.Shell,props:{},children:this.parseChildren()}}parseConst(s){this.eat(t.Ident,a.Const);const e=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));(s.consts=s.consts||{})[e]=this.parseScalar()}parseTheme(s){this.eat(t.Ident,a.Theme),this.eat(t.Punct,i.BraceL);const e={};for(;!this.at(t.Punct,i.BraceR);){const n=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),e[n]=r}this.eat(t.Punct,i.BraceR),s.theme=e}parsePart(s){this.eat(t.Ident,a.Part);const e=this.eat(t.Ident).v;this.eat(t.Punct,i.ParenL);const n=[];for(;!this.at(t.Punct,i.ParenR);){const p=this.eat(t.Ident).v;this.eat(t.Punct,i.Colon),n.push({name:p,type:this.parseType()}),this.at(t.Punct,i.Comma)&&this.next()}this.eat(t.Punct,i.ParenR);const r=this.parseChildren();s.parts=s.parts||{},s.parts[e]={params:n,tree:r.length===1?r[0]:{type:P.Stack,props:{},children:r}}}parseWhen(){const s=this.eat(t.Ident,a.When),e=this.parseExpr();return{type:P.When,props:{cond:e},children:this.parseChildren(),loc:this.locOf(s.pos)}}parseEach(){const s=this.eat(t.Ident,a.Each),e=this.parseExpr();this.eat(t.Ident,a.As);const n=this.eat(t.Ident).v,r={list:e,as:n};return this.at(t.Ident,a.Where)&&(this.next(),r.filter=this.parseExpr()),{type:P.Each,props:r,children:this.parseChildren(),loc:this.locOf(s.pos)}}parseNode(){if(this.at(t.Ident,a.When))return this.parseWhen();if(this.at(t.Ident,a.Each))return this.parseEach();const s=this.eat(t.Ident),e=s.v,n=this.locOf(s.pos);if(this.at(t.Punct,i.ParenL)){const c=this.parseArgs();return this.at(t.Ident,a.Client)?(this.next(),this.eat(t.Punct,i.Colon),{type:e,args:c,props:{hydrate:this.eat(t.Ident).v},loc:n}):{type:e,args:c,loc:n}}const r={},p=[];e===P.Custom&&(r.component=this.eat(t.Ident).v);let o=!0;for(;o;){const c=this.peek();switch(c.t){case t.String:{const l=I[e]||"label";r[l]=S.has(e)?this.parseInterpolation(this.next().v):this.next().v;break}case t.Param:{const l=I[e]||"label";r[l]={$param:this.next().v};break}case t.Ref:r.data=this.next().v;break;case t.Arrow:this.parseArrow(e,r);break;case t.Ident:{const l=c.v;if(e===P.Title&&E(l)){this.next(),r.level=l;break}const v=this.modifiers.get(l);if(!v){o=!1;break}this.next(),v(r);break}case t.Punct:if(c.v===i.BraceL){for(this.next();!this.at(t.Punct,i.BraceR);)p.push(this.parseNode());this.eat(t.Punct,i.BraceR)}o=!1;break;default:o=!1}}const d={type:e,props:r,loc:n};return p.length&&(d.children=p),d}parseArrow(s,e){if(this.next(),s===P.Link){e.to=this.parsePath();return}e.action=this.parseDotted(),this.at(t.Punct,i.ParenL)&&(this.next(),this.at(t.Punct,i.ParenR)||(e.arg=this.parseExpr()),this.eat(t.Punct,i.ParenR))}parseChildren(){this.eat(t.Punct,i.BraceL);const s=[];for(;!this.at(t.Punct,i.BraceR);)s.push(this.parseNode());return this.eat(t.Punct,i.BraceR),s}parseType(){let s=this.eat(t.Ident).v;return this.at(t.Punct,i.Lt)&&(this.next(),s+="<"+this.eat(t.Ident).v+">",this.eat(t.Punct,i.Gt)),s}parseStyleToken(){const s=()=>this.at(t.Number)?this.next().v:this.eat(t.Ident).v;let e=s();for(this.at(t.Punct,i.Colon)&&(this.next(),e+=":"+s());this.at(t.Punct,i.Dot);)this.next(),e+="."+s();return e}parseDotted(){let s=this.at(t.Param)?"$"+this.next().v:this.eat(t.Ident).v;for(;this.at(t.Punct,i.Dot);)this.next(),s+="."+this.eat(t.Ident).v;return s}parseParenList(s){this.eat(t.Punct,i.ParenL);const e=[];for(;!this.at(t.Punct,i.ParenR);)e.push(s()),this.at(t.Punct,i.Comma)&&this.next();return this.eat(t.Punct,i.ParenR),e}rebuildClause(){const s=[];for(;!this.at(t.Punct,i.Comma)&&!this.at(t.Punct,i.ParenR);)s.push(this.next().v);return s.join(" ")}parseEntries(s){for(this.eat(t.Punct,i.BraceL);!this.at(t.Punct,i.BraceR);){const e=this.eat(t.Ident).v;this.eat(t.Punct,i.Colon),s(e),this.at(t.Punct,i.Comma)&&this.next()}this.eat(t.Punct,i.BraceR)}parsePath(){const s=[];let e="";for(;this.at(t.Punct,i.Slash);){const n=this.next();e+="/",this.at(t.Punct,i.BraceL)&&this.peek().pos===n.pos+1?(s.push(e),e="",this.next(),s.push(this.parseExpr()),this.eat(t.Punct,i.BraceR)):this.at(t.Ident)&&this.peek().pos===n.pos+1&&(e+=this.eat(t.Ident).v)}return s.length?(e&&s.push(e),{kind:R.Interp,parts:s}):e}pathOnLine(s){let e="";for(;this.at(t.Punct,i.Slash)&&this.locOf(this.peek().pos).line===s;)this.next(),e+="/",this.at(t.Punct,i.Colon)?(this.next(),e+=":"+this.eat(t.Ident).v):this.at(t.Ident)&&(e+=this.eat(t.Ident).v);return e}parseArgs(){this.eat(t.Punct,i.ParenL);const s={};for(;!this.at(t.Punct,i.ParenR);){const e=this.eat(t.Ident).v;this.eat(t.Punct,i.Colon),s[e]=this.parseArgValue(),this.at(t.Punct,i.Comma)&&this.next()}return this.eat(t.Punct,i.ParenR),s}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 b(u){return new k(u).parse()}export{k as Parser,b as parse};
|
|
1
|
+
import{ParseError as I}from"#engine/shared/diagnostics.js";import{PRIMITIVES as g}from"#engine/lang/manifest.js";import{Grammar as x}from"#engine/lang/grammar.js";import{Tk as t,Pn as i,Kw as a,Nt as m,Mod as P,StOp as h,Ek as R}from"#engine/shared/vocab.js";const f={};for(const[c,s]of Object.entries(g))s.string&&(f[c]=s.string);const S=new Set(Object.entries(g).filter(([,c])=>c.interp).map(([c])=>c)),w=c=>c==="text"?"string":c,y=c=>/^h[1-6]$/.test(c);class k extends x{modifiers;statements;constructor(s){super(s),this.modifiers=new Map([[P.Bind,e=>{const n=this.at(t.Punct,i.ParenL);if(n&&this.next(),this.at(t.Ref)){let r=this.eat(t.Ref).v;for(;this.at(t.Punct,i.Dot);)this.next(),r+="."+this.eat(t.Ident).v;e.bind=r}else e.bind=this.parseDotted();n&&this.eat(t.Punct,i.ParenR)}],[P.Submit,e=>{const n=this.at(t.Punct,i.ParenL);n&&this.next(),e.submit=this.parseDotted(),n&&this.eat(t.Punct,i.ParenR)}],[P.Where,e=>{e.where=this.parseParenList(()=>this.rebuildClause())}],[P.Columns,e=>{e.columns=this.parseParenList(()=>this.eat(t.Ident).v)}],[P.Style,e=>{e.style=this.parseParenList(()=>this.parseStyleToken())}],[P.Class,e=>{e.class=this.parseParenList(()=>{const n=this.at(t.String)?this.next().v:this.eat(t.Ident).v;return this.at(t.Ident,a.When)?(this.next(),{name:n,cond:this.parseExpr()}):n})}],[P.Alt,e=>{const n=this.at(t.Punct,i.ParenL);n&&this.next(),e.alt=this.parseInterpolation(this.eat(t.String).v),n&&this.eat(t.Punct,i.ParenR)}],[P.Inputs,e=>{e.inputs=this.parseArgs()}],[P.On,e=>{e.on=this.parseArgs()}]]),this.statements=new Map([[h.Push,e=>({op:h.Push,target:e,arg:this.parseExpr()})],[h.Set,e=>({op:h.Set,target:e,arg:this.parseExpr()})],[h.Reset,e=>({op:h.Reset,target:e})],[h.Toggle,e=>({op:h.Toggle,target:e})],[h.Create,e=>({op:h.Create,target:e,arg:this.parseExpr()})],[h.Update,e=>({op:h.Update,target:e,arg:this.parseExpr()})],[h.Delete,e=>({op:h.Delete,target:e,arg:this.parseExpr()})],[h.Refetch,e=>{const n={};for(;!this.at(t.Punct,i.ParenR);){const r=this.eat(t.Ident).v;this.eat(t.Punct,i.Colon),n[r]=this.parseExpr(),this.at(t.Punct,i.Comma)&&this.next()}return{op:h.Refetch,target:e,params:n}}]])}parse(){const s={screen:"",entities:{},state:{},actions:{},tree:null},e=new Map([[a.Screen,()=>{this.next(),s.screen=this.eat(t.Ident).v}],[a.Entity,()=>this.parseEntity(s)],[a.State,()=>this.parseState(a.State,s.state)],[a.Store,()=>{s.store=s.store||{},this.parseState(a.Store,s.store)}],[a.Get,()=>this.parseGet(s)],[a.Effect,()=>{this.next(),(s.effects=s.effects||[]).push(this.parseActionBody())}],[a.Action,()=>this.parseAction(s)],[a.Mock,()=>this.parseMock(s)],[a.Sources,()=>this.parseSources(s)],[a.Api,()=>this.parseApi(s)],[a.Meta,()=>this.parseMeta(s)],[a.Routes,()=>this.parseRoutes(s)],[a.Shell,()=>this.parseShell(s)],[a.Part,()=>this.parsePart(s)],[a.Const,()=>this.parseConst(s)],[a.Theme,()=>this.parseTheme(s)],[a.Param,()=>{this.next(),(s.params=s.params||[]).push(this.eat(t.Ident).v)}],[a.Use,()=>{this.next();const n=[this.eat(t.Ident).v];for(;this.at(t.Punct,i.Comma);)this.next(),n.push(this.eat(t.Ident).v);this.eat(t.Ident,a.From);const r=this.eat(t.String).v;(s.imports=s.imports||[]).push({names:n,from:r})}]]);for(;!this.at(t.Eof);){const n=this.peek(),r=n.t===t.Ident?e.get(n.v):void 0;r?r():s.tree=this.parseNode()}return s}parseEntity(s){this.eat(t.Ident,a.Entity);const e=this.eat(t.Ident).v;this.eat(t.Punct,i.BraceL);const n={id:"uuid"},r={};for(;!this.at(t.Punct,i.BraceR);){const o=this.eat(t.Ident).v,p=[this.eat(t.Ident).v];for(;this.at(t.Punct,i.Pipe);)this.next(),p.push(this.eat(t.Ident).v);n[o]=p.length>1?"enum:"+p.join("|"):w(p[0]);const d=this.parseConstraints();Object.keys(d).length&&(r[o]=d)}this.eat(t.Punct,i.BraceR),s.entities[e]=n,Object.keys(r).length&&((s.constraints=s.constraints||{})[e]=r)}parseConstraints(){const s={};for(;this.at(t.Ident,a.Required)||this.at(t.Ident,a.Min)||this.at(t.Ident,a.Max);){const e=this.next().v;if(e===a.Required){s.required=!0;continue}this.eat(t.Punct,i.Colon);const n=Number(this.eat(t.Number).v);e===a.Min?s.min=n:s.max=n}return s}parseState(s,e){for(this.eat(t.Ident,s),this.eat(t.Punct,i.BraceL);!this.at(t.Punct,i.BraceR);){const n=this.eat(t.Ident);this.eat(t.Punct,i.Assign);let r,o,p,d,u=!1;this.at(t.Ident,a.Query)?(this.next(),r="query:"+this.eat(t.Ident).v,this.at(t.Ident,a.Live)?(this.next(),p=!0):this.at(t.Ident,a.Every)&&(this.next(),o=this.parseDuration())):this.at(t.Punct,i.BraceL)||this.at(t.Punct,i.BrackL)?(d=this.parseValue(),u=!0):this.at(t.String)?(d=this.next().v,u=!0):this.at(t.Number)?(d=Number(this.next().v),u=!0):this.at(t.Ident,a.True)||this.at(t.Ident,a.False)?(d=this.next().v===a.True,u=!0):this.at(t.Ident,"null")?(this.next(),u=!0):(d=this.next().v,u=!0),this.eat(t.Punct,i.Colon);const l=this.parseType(),v=this.locOf(n.pos);e[n.v]=r?{type:l,source:r,refresh:o,live:p,loc:v}:{type:l,initial:u?d:null,loc:v}}this.eat(t.Punct,i.BraceR)}parseDuration(){const s=this.peek(),e=Number(this.eat(t.Number).v),n=this.eat(t.Ident).v,r=n==="ms"?1:n==="s"?1e3:n==="m"?6e4:0;if(!r||!(e>0))throw new I("`every` expects a positive duration like `5s`, `500ms`, or `2m`",this.locOf(s.pos));return e*r}parseGet(s){this.eat(t.Ident,a.Get);const e=this.eat(t.Ident).v;this.eat(t.Punct,i.Assign),(s.gets=s.gets||{})[e]=this.parseExpr()}parseAction(s){this.eat(t.Ident,a.Action);const e=this.eat(t.Ident).v;let n;this.at(t.Punct,i.ParenL)&&(n=this.parseParenList(()=>{const p=this.eat(t.Ident).v;return this.eat(t.Punct,i.Colon),{name:p,type:this.parseType()}}));const r=[];if(this.at(t.Ident,a.Mutates))for(this.next(),r.push(this.eat(t.Ident).v);this.at(t.Punct,i.Comma);)this.next(),r.push(this.eat(t.Ident).v);let o="";this.at(t.LArrow)&&(this.next(),o=this.eat(t.Ident).v),s.actions[e]={mutates:r,input:o,params:n,body:this.parseActionBody()}}parseActionBody(){this.eat(t.Punct,i.BraceL);const s=[];for(;!this.at(t.Punct,i.BraceR);)s.push(this.parseStatement());return this.eat(t.Punct,i.BraceR),s}parseIf(){this.eat(t.Ident,a.If);const s=this.parseExpr(),e=this.parseActionBody(),n=this.at(t.Ident,a.Else)?(this.next(),this.parseActionBody()):null;return{op:h.If,cond:s,then:e,else:n}}parseRequest(){const s=this.eat(t.Ident).v.toUpperCase(),e=this.parseInterpolation(this.eat(t.String).v);let n=null;return this.at(t.Ident,a.Body)&&(this.next(),n=this.parseExpr()),{op:h.Request,method:s,url:e,body:n}}parseStatement(){const s=this.peek().pos,e=this.parseStatementInner();return e.loc=this.locOf(s),e}parseStatementInner(){if(this.at(t.Ident,a.If))return this.parseIf();if(this.at(t.Ident,"post")||this.at(t.Ident,"put")||this.at(t.Ident,"delete"))return this.parseRequest();const s=this.eat(t.Ident).v;this.eat(t.Punct,i.Dot);const e=this.eat(t.Ident).v;if(e===h.Remove||e===h.Patch){if(!this.at(t.Ident,a.Where))throw new I(`\`${e}\` takes a \`where <cond>\` predicate now, not a \`(x => \u2026)\` lambda \u2014 write \`${s}.${e} where <cond>\`${e===h.Patch?" with { \u2026 }":""} (item fields read bare)`,this.locOf(this.peek().pos));this.next();const o=this.parseExpr();return e===h.Patch?(this.eat(t.Ident,a.With),{op:h.Patch,target:s,pred:o,patch:this.parseExpr()}):{op:h.Remove,target:s,pred:o}}this.eat(t.Punct,i.ParenL);const n=this.statements.get(e);if(!n){const o=[];for(;!this.at(t.Punct,i.ParenR);)o.push(this.parseExpr()),this.at(t.Punct,i.Comma)&&this.next();return this.eat(t.Punct,i.ParenR),{op:h.Call,target:s,method:e,args:o}}const r=n(s);return this.eat(t.Punct,i.ParenR),r}parseMock(s){this.eat(t.Ident,a.Mock);const e=s.mock||{};this.parseEntries(n=>{e[n]=this.parseValue()}),s.mock=e}parseSources(s){this.eat(t.Ident,a.Sources);const e=s.sources||{};this.parseEntries(n=>{e[n]=this.parseValue()}),s.sources=e}parseApi(s){this.eat(t.Ident,a.Api);const e=s.api||{};this.parseEntries(n=>{e[n]=this.parseValue()}),s.api=e}parseMeta(s){this.eat(t.Ident,a.Meta),this.eat(t.Punct,i.BraceL);const e=s.meta||{};for(;!this.at(t.Punct,i.BraceR);){const n=this.eat(t.Ident).v;e[n]=this.eat(t.String).v}this.eat(t.Punct,i.BraceR),s.meta=e}parseRoutes(s){this.eat(t.Ident,a.Routes),this.eat(t.Punct,i.BraceL);const e=s.routes||[];for(;!this.at(t.Punct,i.BraceR);){const n=this.peek(),r=this.locOf(n.pos).line,o=this.pathOnLine(r);this.eat(t.Arrow);const p={url:o,page:this.eat(t.Ident).v,loc:this.locOf(n.pos)};this.at(t.Ident,a.Guard)&&(this.next(),p.guardNeg=this.at(t.Ident,a.Not)?(this.next(),!0):!1,p.guard=this.parseDotted(),this.eat(t.Ident,a.Else),p.redirect=this.pathOnLine(r)),e.push(p)}this.eat(t.Punct,i.BraceR),s.routes=e}parseShell(s){this.eat(t.Ident,a.Shell),s.shell={type:m.Shell,props:{},children:this.parseChildren()}}parseConst(s){this.eat(t.Ident,a.Const);const e=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 I("const holds a single value (string/number/bool) \u2014 use a block like `theme { \u2026 }` for structured data",this.locOf(this.peek().pos));(s.consts=s.consts||{})[e]=this.parseScalar()}parseTheme(s){this.eat(t.Ident,a.Theme),this.eat(t.Punct,i.BraceL);const e={};for(;!this.at(t.Punct,i.BraceR);){const n=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),e[n]=r}this.eat(t.Punct,i.BraceR),s.theme=e}parsePart(s){this.eat(t.Ident,a.Part);const e=this.eat(t.Ident).v;this.eat(t.Punct,i.ParenL);const n=[];for(;!this.at(t.Punct,i.ParenR);){const o=this.eat(t.Ident).v;this.eat(t.Punct,i.Colon),n.push({name:o,type:this.parseType()}),this.at(t.Punct,i.Comma)&&this.next()}this.eat(t.Punct,i.ParenR);const r=this.parseChildren();s.parts=s.parts||{},s.parts[e]={params:n,tree:r.length===1?r[0]:{type:m.Stack,props:{},children:r}}}parseWhen(){const s=this.eat(t.Ident,a.When),e=this.parseExpr();return{type:m.When,props:{cond:e},children:this.parseChildren(),loc:this.locOf(s.pos)}}parseEach(){const s=this.eat(t.Ident,a.Each),e=this.parseExpr();this.eat(t.Ident,a.As);const n=this.eat(t.Ident).v,r={list:e,as:n};return this.at(t.Ident,a.Where)&&(this.next(),r.filter=this.parseExpr()),{type:m.Each,props:r,children:this.parseChildren(),loc:this.locOf(s.pos)}}parseNode(){if(this.at(t.Ident,a.When))return this.parseWhen();if(this.at(t.Ident,a.Each))return this.parseEach();const s=this.eat(t.Ident),e=s.v,n=this.locOf(s.pos);if(this.at(t.Punct,i.ParenL)){const u=this.parseArgs();return{type:e,args:u,loc:n}}const r={},o=[];e===m.Custom&&(r.component=this.eat(t.Ident).v);let p=!0;for(;p;){const u=this.peek();switch(u.t){case t.String:{const l=f[e]||"label";r[l]=S.has(e)?this.parseInterpolation(this.next().v):this.next().v;break}case t.Param:{const l=f[e]||"label";r[l]={$param:this.next().v};break}case t.Ref:r.data=this.next().v;break;case t.Arrow:this.parseArrow(e,r);break;case t.Ident:{const l=u.v;if(e===m.Title&&y(l)){this.next(),r.level=l;break}const v=this.modifiers.get(l);if(!v){p=!1;break}this.next(),v(r);break}case t.Punct:if(u.v===i.BraceL){for(this.next();!this.at(t.Punct,i.BraceR);)o.push(this.parseNode());this.eat(t.Punct,i.BraceR)}p=!1;break;default:p=!1}}const d={type:e,props:r,loc:n};return o.length&&(d.children=o),d}parseArrow(s,e){if(this.next(),s===m.Link){e.to=this.parsePath();return}if(e.action=this.parseDotted(),!!this.at(t.Punct,i.ParenL)){if(this.next(),!this.at(t.Punct,i.ParenR)){e.arg=this.parseExpr();const n=[];for(;this.at(t.Punct,i.Comma);)this.next(),n.push(this.parseExpr());n.length&&(e.argRest=n)}this.eat(t.Punct,i.ParenR)}}parseChildren(){this.eat(t.Punct,i.BraceL);const s=[];for(;!this.at(t.Punct,i.BraceR);)s.push(this.parseNode());return this.eat(t.Punct,i.BraceR),s}parseType(){let s=this.eat(t.Ident).v;return this.at(t.Punct,i.Lt)&&(this.next(),s+="<"+this.eat(t.Ident).v+">",this.eat(t.Punct,i.Gt)),s}parseStyleToken(){const s=()=>this.at(t.Number)?this.next().v:this.eat(t.Ident).v;let e=s();for(this.at(t.Punct,i.Colon)&&(this.next(),e+=":"+s());this.at(t.Punct,i.Dot);)this.next(),e+="."+s();return e}parseDotted(){let s=this.at(t.Param)?"$"+this.next().v:this.eat(t.Ident).v;for(;this.at(t.Punct,i.Dot);)this.next(),s+="."+this.eat(t.Ident).v;return s}parseParenList(s){this.eat(t.Punct,i.ParenL);const e=[];for(;!this.at(t.Punct,i.ParenR);)e.push(s()),this.at(t.Punct,i.Comma)&&this.next();return this.eat(t.Punct,i.ParenR),e}rebuildClause(){const s=[];for(;!this.at(t.Punct,i.Comma)&&!this.at(t.Punct,i.ParenR);)s.push(this.next().v);return s.join(" ")}parseEntries(s){for(this.eat(t.Punct,i.BraceL);!this.at(t.Punct,i.BraceR);){const e=this.eat(t.Ident).v;this.eat(t.Punct,i.Colon),s(e),this.at(t.Punct,i.Comma)&&this.next()}this.eat(t.Punct,i.BraceR)}parsePath(){const s=[];let e="";for(;this.at(t.Punct,i.Slash);){const n=this.next();e+="/",this.at(t.Punct,i.BraceL)&&this.peek().pos===n.pos+1?(s.push(e),e="",this.next(),s.push(this.parseExpr()),this.eat(t.Punct,i.BraceR)):this.at(t.Ident)&&this.peek().pos===n.pos+1&&(e+=this.eat(t.Ident).v)}return s.length?(e&&s.push(e),{kind:R.Interp,parts:s}):e}pathOnLine(s){let e="";for(;this.at(t.Punct,i.Slash)&&this.locOf(this.peek().pos).line===s;)this.next(),e+="/",this.at(t.Punct,i.Colon)?(this.next(),e+=":"+this.eat(t.Ident).v):this.at(t.Ident)&&(e+=this.eat(t.Ident).v);return e}parseArgs(){this.eat(t.Punct,i.ParenL);const s={};for(;!this.at(t.Punct,i.ParenR);){const e=this.eat(t.Ident).v;this.eat(t.Punct,i.Colon),s[e]=this.parseArgValue(),this.at(t.Punct,i.Comma)&&this.next()}return this.eat(t.Punct,i.ParenR),s}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 B(c){return new k(c).parse()}export{k as Parser,B as parse};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{sourceRequest as
|
|
1
|
+
import{sourceRequest as m,sourceRows as x}from"#engine/shared/source.js";const S=new Set(["img","input","br","hr","meta","link","source"]),l=e=>e.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">"),u=e=>l(e).replace(/"/g,""");class h{parentNode=null;remove(){const n=this.parentNode;if(!n)return;const t=n.children.indexOf(this);t>=0&&n.children.splice(t,1),this.parentNode=null}}class f extends h{constructor(t){super();this.text=t}text}class g extends h{constructor(t){super();this.text=t}text}class c extends h{constructor(t){super();this.tag=t}tag;children=[];attrs={};className="";src="";alt="";href="";type="";value="";placeholder="";text="";get childNodes(){return this.children}get textContent(){return this.text}set textContent(t){this.text=t;for(const r of this.children)r.parentNode=null;this.children=[]}appendChild(t){if(t instanceof a){for(const r of[...t.children])this.appendChild(r);return t}return t.remove(),t.parentNode=this,this.children.push(t),this.text="",t}insertBefore(t,r){const o=t instanceof a?[...t.children]:[t];for(const i of o)i.remove();const s=r?this.children.indexOf(r):-1;this.children.splice(s>=0?s:this.children.length,0,...o);for(const i of o)i.parentNode=this;return t instanceof a&&(t.children=[]),t}replaceChildren(...t){for(const r of this.children)r.parentNode=null;this.children=[];for(const r of t)this.appendChild(r)}setAttribute(t,r){this.attrs[t]=r}addEventListener(){}createTHead(){const t=new c("thead");return this.appendChild(t),t}createTBody(){const t=new c("tbody");return this.appendChild(t),t}insertRow(){const t=new c("tr");return this.appendChild(t),t}}class a extends c{constructor(){super("#fragment")}}class N{createElement(n){return new c(n)}createComment(n){return new g(n)}createDocumentFragment(){return new a}createTextNode(n){return new f(n)}}function p(e){if(e instanceof f)return l(e.text);if(e instanceof g||!(e instanceof c))return"";if(e.tag==="#fragment")return e.children.map(p).join("");const n=[];e.className&&n.push(`class="${u(e.className)}"`);const t=(s,i)=>{i&&n.push(`${s}="${u(i)}"`)};t("src",e.src),t("alt",e.alt),t("href",e.href),t("type",e.type),t("value",e.value),t("placeholder",e.placeholder);for(const[s,i]of Object.entries(e.attrs))n.push(`${s}="${u(i)}"`);const r=`<${e.tag}${n.length?" "+n.join(" "):""}>`;if(S.has(e.tag))return r;const o=e.children.length?e.children.map(p).join(""):l(e.textContent);return`${r}${o}</${e.tag}>`}function $(e){const n=new N,t=new c("div");return new Function("document","app","__params",e)(n,t,{}),t.children.map(p).join("")}async function v(e,n={}){const t={};for(const[r,o]of Object.entries(e)){const s=m(o,n);if(s.url){if(s.method!=="GET"){console.log(` \u2022 source "${r}" is ${s.method} \u2014 not fetched at build (runs client-side)`);continue}try{const i=await(await fetch(s.url,{headers:s.headers})).json(),d=x(i,s.at);t[r]=d,console.log(` \u2022 fetched source "${r}" (${d.length} rows) \u2190 ${s.url}`)}catch{}}}return t}export{v as fetchSources,$ as renderSsrBody};
|
|
@@ -1 +1 @@
|
|
|
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.Every="every",e.Live="live",e.Param="param",e.Api="api",e.Body="body",e.Meta="meta",e.Use="use",e.From="from",e.
|
|
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.Every="every",e.Live="live",e.Param="param",e.Api="api",e.Body="body",e.Meta="meta",e.Use="use",e.From="from",e.When="when",e.Each="each",e.As="as",e.Where="where",e.By="by",e.With="with",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||{}),g=(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))(g||{}),f=(u=>(u.Module="module",u.Store="store",u.Html="html",u.Ssr="ssr",u))(f||{}),x=(l=>(l.Text="text",l.Email="email",l.Number="number",l.Bool="bool",l.Enum="enum",l))(x||{}),b=(r=>(r.Or="or",r.And="and",r.Eq="==",r.Neq="!=",r.Lte="<=",r.Gte=">=",r.Lt="<",r.Gt=">",r.Contains="contains",r.Add="+",r.Sub="-",r.Mul="*",r.Div="/",r))(b||{}),d=(i=>(i.Not="not",i))(d||{}),S=(n=>(n.Lit="lit",n.Ref="ref",n.Un="un",n.Bin="bin",n.Tern="tern",n.Interp="interp",n.Call="call",n.Obj="obj",n.Agg="agg",n.Filter="filter",n))(S||{});const y=new Set(["sum","count","avg","min","max"]),C=new Set(["sort","sortDesc"]);var A=(r=>(r.Push="push",r.Set="set",r.Reset="reset",r.Toggle="toggle",r.Remove="remove",r.Patch="patch",r.Create="create",r.Update="update",r.Delete="delete",r.Refetch="refetch",r.Request="request",r.Call="call",r.If="if",r))(A||{}),R=(s=>(s.Bind="bind",s.Submit="submit",s.Where="where",s.Columns="columns",s.Style="style",s.Class="class",s.Alt="alt",s.Inputs="inputs",s.On="on",s))(R||{});export{y as AGG_OPS,b as BOp,S as Ek,x as Fk,f as Fmt,h as Kw,R as Mod,g as Nt,c as Pn,C as SORT_OPS,A as StOp,m as Tk,d as UOp};
|
package/grammar/muten.gbnf
CHANGED
|
@@ -21,7 +21,9 @@ stateinit ::= "query" sp ident | value
|
|
|
21
21
|
type ::= ident ("<" ident ">")?
|
|
22
22
|
get-decl ::= "get" sp ident ws "=" ws expr
|
|
23
23
|
meta-decl ::= "meta" ws "{" ws (ident sp string ws)* "}"
|
|
24
|
-
action-decl ::= "action" sp ident mutates? input? ws actionbody
|
|
24
|
+
action-decl ::= "action" sp ident actionparams? mutates? input? ws actionbody
|
|
25
|
+
actionparams ::= ws "(" ws (actionparam (ws "," ws actionparam)*)? ws ")"
|
|
26
|
+
actionparam ::= ident ws ":" ws type
|
|
25
27
|
mutates ::= sp "mutates" sp ident (ws "," ws ident)*
|
|
26
28
|
input ::= ws "<-" ws ident
|
|
27
29
|
actionbody ::= "{" ws (stmt ws)* "}"
|
|
@@ -29,7 +31,7 @@ stmt ::= ifstmt | requeststmt | callstmt
|
|
|
29
31
|
ifstmt ::= "if" sp expr ws actionbody (ws "else" ws actionbody)?
|
|
30
32
|
requeststmt ::= ("post" | "put" | "delete") sp string (sp "body" sp expr)?
|
|
31
33
|
callstmt ::= ident "." actionop "(" ws callargs ws ")"
|
|
32
|
-
actionop ::= "push" | "remove" | "patch" | "reset" | "set" | "create" | "update" | "delete" | "refetch"
|
|
34
|
+
actionop ::= "push" | "remove" | "patch" | "reset" | "toggle" | "set" | "create" | "update" | "delete" | "refetch"
|
|
33
35
|
callargs ::= refetcharg (ws "," ws refetcharg)* | predarg | expr | ""
|
|
34
36
|
refetcharg ::= ident ws ":" ws expr
|
|
35
37
|
predarg ::= ident ws "=>" ws expr
|
|
@@ -40,7 +42,7 @@ eachnode ::= "each" sp expr sp "as" sp ident ws block
|
|
|
40
42
|
block ::= "{" ws (node ws)* "}"
|
|
41
43
|
linknode ::= "Link" (sp (commonpart | "->" ws path))* (ws block)?
|
|
42
44
|
actionnode ::= ("RowAction" | "Button") (sp (commonpart | actionarrow))* (ws block)?
|
|
43
|
-
actionarrow ::= "->" ws dotted (ws "(" ws expr? ws ")")?
|
|
45
|
+
actionarrow ::= "->" ws dotted (ws "(" ws (expr (ws "," ws expr)*)? ws ")")?
|
|
44
46
|
customnode ::= "Custom" sp ident (sp modifier)*
|
|
45
47
|
plainnode ::= ("Stack" | "Header" | "Nav" | "Sidebar" | "Footer" | "Page" | "Text" | "Title" | "Span" | "Image" | "SearchField" | "DataTable" | "Form" | "slot") (sp commonpart)* (ws block)?
|
|
46
48
|
commonpart ::= string | ref | level | modifier
|
|
@@ -82,7 +84,8 @@ cmpop ::= "==" | "!=" | "<=" | ">=" | "<" | ">" | "contains"
|
|
|
82
84
|
addexpr ::= mulexpr (ws ("+" | "-") ws mulexpr)*
|
|
83
85
|
mulexpr ::= unary (ws ("*" | "/") ws unary)*
|
|
84
86
|
unary ::= ("not" sp)? primary
|
|
85
|
-
primary ::= "(" ws ternary ws ")" | string | number | bool | "null" | callorref
|
|
87
|
+
primary ::= "(" ws ternary ws ")" | string | number | bool | "null" | filterexpr | callorref
|
|
88
|
+
filterexpr ::= dotted sp "where" sp ternary
|
|
86
89
|
callorref ::= dotted (ws "(" ws (ternary (ws "," ws ternary)*)? ws ")")?
|
|
87
90
|
|
|
88
91
|
value ::= array | object | scalar
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@muten/core",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.14",
|
|
4
4
|
"description": "AI-first frontend framework — compiles .muten files to vanilla JS + signals.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
"prepare": "npm run build",
|
|
45
45
|
"build": "tsc && node --experimental-strip-types esbuild.ts && node scripts/gen-gbnf.mjs",
|
|
46
46
|
"gbnf": "node scripts/gen-gbnf.mjs",
|
|
47
|
-
"test": "npm run build && node --experimental-strip-types test/parse.ts && node --experimental-strip-types test/expr.ts && node --experimental-strip-types test/parts.ts && node --experimental-strip-types test/diagnostics.ts && node --experimental-strip-types test/routes.ts && node --experimental-strip-types test/params.ts && node --experimental-strip-types test/ssr.ts && node --experimental-strip-types test/writes.ts && node --experimental-strip-types test/dynamics.ts && node --experimental-strip-types test/runtime.ts && node --experimental-strip-types test/lang.ts && node --experimental-strip-types test/forms.ts && node --experimental-strip-types test/smoke.ts && node --experimental-strip-types test/
|
|
47
|
+
"test": "npm run build && node --experimental-strip-types test/parse.ts && node --experimental-strip-types test/expr.ts && node --experimental-strip-types test/parts.ts && node --experimental-strip-types test/diagnostics.ts && node --experimental-strip-types test/routes.ts && node --experimental-strip-types test/params.ts && node --experimental-strip-types test/ssr.ts && node --experimental-strip-types test/writes.ts && node --experimental-strip-types test/dynamics.ts && node --experimental-strip-types test/runtime.ts && node --experimental-strip-types test/lang.ts && node --experimental-strip-types test/forms.ts && node --experimental-strip-types test/smoke.ts && node --experimental-strip-types test/realapp.ts && node --experimental-strip-types test/print.ts && node --experimental-strip-types test/externs.ts && node test/gbnf.mjs"
|
|
48
48
|
},
|
|
49
49
|
"optionalDependencies": {
|
|
50
50
|
"sass": "^1.101.0"
|