@muten/core 0.0.7 → 0.0.9

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 CHANGED
@@ -1,6 +1,7 @@
1
1
 
2
2
  <img width="157" height="157" alt="Group 21" src="https://github.com/user-attachments/assets/fe9a02e6-483d-4788-9286-142c1ddb7057" />
3
3
  <br/>
4
+
4
5
  An **AI-first** frontend framework. You write `.muten` files; muten compiles them to vanilla JS
5
6
  with fine-grained signals — **no virtual DOM, no framework runtime to ship**. The language is small,
6
7
  semantic and analyzable on purpose: an AI (or a person) can **locate and mutate** an app cheaply.
@@ -10,19 +11,23 @@ npm create muten@latest my-app # scaffold a new app (cross-platform: Windows +
10
11
  cd my-app && npm install && npm run dev
11
12
  ```
12
13
 
13
- ## Why muten (the moat, measured)
14
+ ## Why muten
14
15
 
15
- For an AI the cost of code is **context + mistakes + edit-radius** muten is built to cut all three. The
16
- reproducible benchmark in [`playground/`](../playground) (same app in muten / React / Vue / Svelte) shows:
16
+ For an AI the cost of working on a codebase is **context + mistakes + edit-radius**. muten is built to cut
17
+ all three *by construction* these are properties of how it compiles, not marketing:
17
18
 
18
- - **Smaller to read & ship** — ~447 code tokens vs ~562 (Svelte), and a **~2.6 KB gzip** runtime — **7–28×
19
- less JS** than the React/Vue/Svelte builds (a static page ships *zero*).
20
- - **A deterministic oracle** — `muten check --json` catches the typical mistakes (unknown state/action/part,
21
- bad style token, illegal mutation) at compile time, in ms, no browser. The *bounded* surface is why it can.
22
- - **The whole app in ~80 tokens** — `app.map.json` is the index an agent reads first, instead of grepping a tree.
23
- - **Tiny edit radius** adding a feature touches a few lines in one file, not a component graph.
19
+ - **Almost nothing to ship** — no virtual DOM, no framework runtime. The *same* todo app, scaffolded by each
20
+ framework's official CLI and built, ships **~2.8 KB gzip** of JS in muten vs **~14 KB (Svelte) · ~24 KB (Vue)
21
+ · ~59 KB (React)** — **5–21× less** (a static page ships *zero*). Source is the most compact too (445 B), on
22
+ par with Svelte. *(Reproducible: the `bench/` folder + `node bench.mjs` in the source repo.)*
23
+ - **A deterministic oracle** — `muten check --json` validates every page at compile time (unknown
24
+ state/action/part, bad style token, illegal mutation) in milliseconds, no browser a feedback loop the
25
+ others don't have. A *bounded* language is what makes that possible.
26
+ - **The whole app as data** — `app.map.json` is a compact index of routes + structure an agent reads first,
27
+ instead of grepping a component tree.
28
+ - **Small edit radius** — the UI is declarative, so a change is usually a few lines in one file.
24
29
 
25
- That's the trade: a small, analyzable language an AI can hold in its head — not a general-purpose one it can't.
30
+ The trade is deliberate: a small, analyzable language an AI can hold in its head — not a general-purpose one it can't.
26
31
 
27
32
  ## Capabilities
28
33
 
@@ -3,7 +3,7 @@ import{tokenClass as ne,resolveToken as Ee,defaultTheme as Oe}from"#engine/style
3
3
  `),he=u.genEffects();let Z=null;Y?Z=C?X(C):"":$.format!==b.Store&&C&&Q(C,"app");const _e=t.join(`
4
4
  `),ee={};for(const e of Object.values(I))if(typeof e.source=="string"&&e.source.startsWith("query:")){const s=e.source.slice(6),o=(e.type.match(/^list<(.+)>$/)||[])[1];ee[s]=o?u.uuidFields(o):[]}const ge=[...U].map(e=>{const s=Ee(e,M);if(!s)return"";const o=`.${ne(e)}{${s}}`,n=e.indexOf(":"),f=n>0&&M.breakpoints[e.slice(0,n)];return f?`@media (min-width:${f}){${o}}`:o}).filter(Boolean).join(`
5
5
  `),de=Object.entries(J).map(([e,s])=>`const __custom_${e} = (function () {
6
- ${s}
6
+ ${s.replace(/^[ \t]*export[ \t]+(default[ \t]+)?/gm,"")}
7
7
  return mount;
8
8
  })();`).join(`
9
9
 
@@ -1 +1 @@
1
- import{resolveToken as K,SUGGESTED as P,defaultTheme as L,isKnownTokenShape as U}from"#engine/style/tokens.js";import{diag as f,closest as p}from"#engine/shared/diagnostics.js";import{PRIMITIVE_NAMES as B,ACTION_OPS as G,PRIMITIVES as Y}from"#engine/lang/manifest.js";import{Nt as d,Ek as u,StOp as z}from"#engine/shared/vocab.js";const V=new Set([...B,d.Shell]),H=["bind","data"],A=new Set(G),C=["text","number","bool","uuid","email","string"];function h(s,r=[]){if(s.kind===u.Ref)r.push(s.name);else if(s.kind===u.Un)h(s.operand,r);else if(s.kind===u.Bin)h(s.left,r),h(s.right,r);else if(s.kind===u.Tern)h(s.cond,r),h(s.then,r),h(s.else,r);else if(s.kind===u.Call)for(const a of s.args)h(a,r);return r}function m(s,r=[]){if(s.kind===u.Call){r.push(s.fn);for(const a of s.args)m(a,r)}else s.kind===u.Un?m(s.operand,r):s.kind===u.Bin?(m(s.left,r),m(s.right,r)):s.kind===u.Tern&&(m(s.cond,r),m(s.then,r),m(s.else,r));return r}function tt(s,r={}){const a=[],k=new Set(Object.keys(s.state||{})),W=new Set(r.stores||[]),D=new Set(Object.keys(s.consts||{})),F=new Set(s.params||[]),S=new Set(Object.keys(s.actions||{})),$=n=>/^(svelte|react):/.test(n),O=new Set((s.imports||[]).filter(n=>!$(n.from)).flatMap(n=>n.names)),N=new Set((s.imports||[]).filter(n=>$(n.from)).flatMap(n=>n.names)),q=s.nodes||{},w=(n,o)=>{for(const t of m(n))O.has(t)||a.push(f("unknown-function",`"${t}" is not a use'd function`,{loc:o,suggestion:p(t,[...O])}))},E=new Map;for(const[n,o]of Object.entries(s.state||{})){if(!o.source?.startsWith("query:"))continue;const t=new Set(["loading","error","data"]),e=s.entities?.[o.type];if(e){t.add("id");for(const i of Object.keys(e))t.add(i)}E.set(n,t)}const j=new Map;for(const[n,o]of Object.entries(r.storeMembers||{}))j.set(n,new Set(o));const I=(n,o,t)=>{const e=E.get(n);if(e){e.has(o)||a.push(f("unknown-member",`"${o}" is not a member of query "${n}"`,{loc:t.loc,suggestion:p(o,[...e])}));return}const i=j.get(n);i&&!i.has(o)&&a.push(f("unknown-member",`"${o}" is not a member of store "${n}"`,{loc:t.loc,suggestion:p(o,[...i])}))},M=Object.keys(s.entities||{});for(const[n,o]of Object.entries(s.state||{})){const t=o.type;if(t==="list")a.push(f("untyped-list",`state "${n}" is an untyped "list" \u2014 declare the element type, e.g. list<uuid> or list<User>`,{loc:o.loc,suggestion:"list<uuid>"}));else if(t.startsWith("list<")){const e=t.slice(5,-1);!C.includes(e)&&!M.includes(e)&&a.push(f("unknown-type",`list element "${e}" is not a known entity or scalar type`,{loc:o.loc,suggestion:p(e,[...M,...C])}))}}const _=(n,o)=>{if(typeof n=="string"&&n.startsWith("@")){const t=n.slice(1).split(".")[0];if(!k.has(t)){const e=p(t,[...k]);a.push(f("unknown-ref",`"@${t}" is not a declared state`,{loc:o.loc,suggestion:e?"@"+e:null}))}}},T=(n,o)=>{if(!(!n||n.startsWith("$"))){if(n.includes(".")){const t=n.indexOf(".");I(n.slice(0,t),n.slice(t+1).split(".")[0],o);return}S.has(n)||a.push(f("unknown-action",`"${n}" is not a declared action`,{loc:o.loc,suggestion:p(n,[...S])}))}},b=(n,o,t)=>{w(n,o.loc);for(const e of h(n)){const i=e.indexOf("."),l=i===-1?e:e.slice(0,i);if(!(t.has(l)||k.has(l)||W.has(l)||D.has(l)||F.has(l)||S.has(l))){a.push(f("unknown-ref",`"${l}" is not a known state or item variable here`,{loc:o.loc,suggestion:p(l,[...k,...t])}));continue}i!==-1&&I(l,e.slice(i+1).split(".")[0],o)}},x=new Set,R=(n,o)=>{const t=q[n];if(!t){a.push(f("missing-node",`node ${n} does not exist`));return}if(x.has(n)){a.push(f("dup-node",`${n} is referenced twice`,{loc:t.loc}));return}if(x.add(n),!N.has(t.type))if(!V.has(t.type))t.args?a.push(f("unknown-part",`"${t.type}" is not a known part`,{loc:t.loc,suggestion:p(t.type,[...r.parts||[],...N])})):a.push(f("unknown-type",`"${t.type}" is not a known primitive`,{loc:t.loc,suggestion:p(t.type,[...V])}));else{const c=Y[t.type],y=c?c.props:{};for(const[g,v]of Object.entries(y))!v.endsWith("?")&&!(g in(t.props||{}))&&a.push(f("missing-prop",`${t.type} is missing the required "${g}"`,{loc:t.loc}))}const e=t.props||{};for(const c of H)c in e&&_(e[c],t);if(e.action&&T(e.action,t),e.submit&&T(e.submit,t),Array.isArray(e.style)){const c=r.theme||L,y=Object.keys(c.space||{}).length>0;for(const g of e.style)U(g)?y&&K(g,c)===null&&a.push(f("unknown-token",`"${g}": that step isn't in your theme scale`,{loc:t.loc,suggestion:p(g,P)})):a.push(f("unknown-token",`"${g}" is not an accepted style token`,{loc:t.loc,suggestion:p(g,P)}))}t.type===d.When&&e.cond&&b(e.cond,t,o),t.type===d.Each&&e.list&&b(e.list,t,o);const i=[];(t.type===d.Text||t.type===d.Title||t.type===d.Span)&&e.value&&i.push(e.value),t.type===d.Image&&(e.src&&i.push(e.src),e.alt&&i.push(e.alt)),t.type===d.Link&&e.to&&i.push(e.to),e.label&&i.push(e.label);for(const c of i)if(typeof c=="object"&&"kind"in c&&c.kind===u.Interp)for(const y of c.parts)typeof y!="string"&&b(y,t,o);const l=t.type===d.Each&&e.as?new Set([...o,e.as]):o;for(const c of t.children||[])R(c,l)};if(s.rootId?R(s.rootId,new Set):r.kind!=="store"&&a.push(f("no-root","the doc is missing a rootId")),r.kind==="store")for(const[n,o]of Object.entries(s.gets||{})){w(o);for(const t of h(o)){const e=t.split(".")[0];k.has(e)||a.push(f("unknown-ref",`get "${n}": "${e}" is not a state of this store`,{suggestion:p(e,[...k])}))}}for(const[n,o]of Object.entries(s.actions||{})){const t=new Set(o.mutates||[]),e=i=>{if(i.op===z.If){w(i.cond);for(const l of i.then||[])e(l);for(const l of i.else||[])e(l);return}A.has(i.op)||a.push(f("unknown-op",`action "${n}" uses unknown op "${i.op}"`,{suggestion:p(i.op,[...A])})),"target"in i&&i.target&&!t.has(i.target)&&a.push(f("undeclared-mutation",`action "${n}" mutates "${i.target}" but only declares mutates(${[...t].join(", ")||"\u2205"})`,{suggestion:p(i.target,[...t])})),"arg"in i&&i.arg&&w(i.arg)};for(const i of o.body||[])e(i)}return{ok:a.length===0,diagnostics:a}}export{tt as validate};
1
+ import{resolveToken as U,SUGGESTED as W,defaultTheme as B,isKnownTokenShape as G}from"#engine/style/tokens.js";import{diag as c,closest as p}from"#engine/shared/diagnostics.js";import{PRIMITIVE_NAMES as Y,ACTION_OPS as z,PRIMITIVES as H}from"#engine/lang/manifest.js";import{Nt as d,Ek as h,StOp as J}from"#engine/shared/vocab.js";const A=new Set([...Y,d.Shell]),Q=["bind","data"],C=new Set(z),O=["text","number","bool","uuid","email","string"];function m(o,a=[]){if(o.kind===h.Ref)a.push(o.name);else if(o.kind===h.Un)m(o.operand,a);else if(o.kind===h.Bin)m(o.left,a),m(o.right,a);else if(o.kind===h.Tern)m(o.cond,a),m(o.then,a),m(o.else,a);else if(o.kind===h.Call)for(const r of o.args)m(r,a);return a}function y(o,a=[]){if(o.kind===h.Call){a.push(o.fn);for(const r of o.args)y(r,a)}else o.kind===h.Un?y(o.operand,a):o.kind===h.Bin?(y(o.left,a),y(o.right,a)):o.kind===h.Tern&&(y(o.cond,a),y(o.then,a),y(o.else,a));return a}function et(o,a={}){const r=[],k=new Set(Object.keys(o.state||{})),F=new Set(a.stores||[]),D=new Set(Object.keys(o.consts||{})),q=new Set(o.params||[]),w=new Set(Object.keys(o.actions||{})),E=e=>/^(svelte|react):/.test(e),N=new Set((o.imports||[]).filter(e=>!E(e.from)).flatMap(e=>e.names)),j=new Set((o.imports||[]).filter(e=>E(e.from)).flatMap(e=>e.names)),_=o.nodes||{},S=(e,s)=>{for(const t of y(e))N.has(t)||r.push(c("unknown-function",`"${t}" is not a use'd function`,{loc:s,suggestion:p(t,[...N]),from:t}))},M=new Map;for(const[e,s]of Object.entries(o.state||{})){if(!s.source?.startsWith("query:"))continue;const t=new Set(["loading","error","data"]),n=o.entities?.[s.type];if(n){t.add("id");for(const i of Object.keys(n))t.add(i)}M.set(e,t)}const I=new Map;for(const[e,s]of Object.entries(a.storeMembers||{}))I.set(e,new Set(s));const x=(e,s,t)=>{const n=M.get(e);if(n){n.has(s)||r.push(c("unknown-member",`"${s}" is not a member of query "${e}"`,{loc:t.loc,suggestion:p(s,[...n]),from:s}));return}const i=I.get(e);i&&!i.has(s)&&r.push(c("unknown-member",`"${s}" is not a member of store "${e}"`,{loc:t.loc,suggestion:p(s,[...i]),from:s}))},T=Object.keys(o.entities||{});for(const[e,s]of Object.entries(o.state||{})){const t=s.type;if(t==="list")r.push(c("untyped-list",`state "${e}" is an untyped "list" \u2014 declare the element type, e.g. list<uuid> or list<User>`,{loc:s.loc,suggestion:"list<uuid>"}));else if(t.startsWith("list<")){const n=t.slice(5,-1);!O.includes(n)&&!T.includes(n)&&r.push(c("unknown-type",`list element "${n}" is not a known entity or scalar type`,{loc:s.loc,suggestion:p(n,[...T,...O]),from:n}))}else if(s.initial!==void 0&&s.initial!==null){const n=t==="number"?"number":t==="bool"?"boolean":["text","string","email","uuid"].includes(t)?"string":"";n&&typeof s.initial!==n&&r.push(c("type-mismatch",`state "${e}" is typed "${t}" but its initial value is a ${typeof s.initial}`,{loc:s.loc}))}}const v=e=>{const s=o.entities?.[e];return s?new Set(["id",...Object.keys(s)]):null},K=e=>{if(!e||e.kind!==h.Ref)return"";const s=o.state?.[e.name.split(".")[0]]?.type||"";return s.startsWith("list<")?s.slice(5,-1):""},L=(e,s)=>{if(typeof e=="string"&&e.startsWith("@")){const t=e.slice(1).split(".")[0];if(!k.has(t)){const n=p(t,[...k]);r.push(c("unknown-ref",`"@${t}" is not a declared state`,{loc:s.loc,suggestion:n?"@"+n:null,from:"@"+t,related:n?o.state?.[n]?.loc??null:null}))}}},R=(e,s)=>{if(!(!e||e.startsWith("$"))){if(e.includes(".")){const t=e.indexOf(".");x(e.slice(0,t),e.slice(t+1).split(".")[0],s);return}w.has(e)||r.push(c("unknown-action",`"${e}" is not a declared action`,{loc:s.loc,suggestion:p(e,[...w]),from:e}))}},b=(e,s,t)=>{S(e,s.loc);for(const n of m(e)){const i=n.indexOf("."),f=i===-1?n:n.slice(0,i);if(!(t.has(f)||k.has(f)||F.has(f)||D.has(f)||q.has(f)||w.has(f))){r.push(c("unknown-ref",`"${f}" is not a known state or item variable here`,{loc:s.loc,suggestion:p(f,[...k,...t.keys()]),from:f}));continue}if(i===-1)continue;const l=n.slice(i+1).split(".")[0],g=t.has(f)?t.get(f)||"":o.state?.[f]?.type||"",u=v(g);if(u)u.has(l)||r.push(c("unknown-member",`"${l}" is not a field of ${g} (${t.has(f)?"item":"state"} "${f}")`,{loc:s.loc,suggestion:p(l,[...u]),from:l}));else if(O.includes(g))r.push(c("unknown-member",`"${f}" is a ${g} \u2014 it has no field "${l}"`,{loc:s.loc}));else if(w.has(f)&&!k.has(f)){const $=new Set(["pending","error"]);$.has(l)||r.push(c("unknown-member",`action "${f}" exposes only .pending / .error, not "${l}"`,{loc:s.loc,suggestion:p(l,[...$]),from:l}))}else x(f,l,s)}},P=new Set,V=(e,s)=>{const t=_[e];if(!t){r.push(c("missing-node",`node ${e} does not exist`));return}if(P.has(e)){r.push(c("dup-node",`${e} is referenced twice`,{loc:t.loc}));return}if(P.add(e),!j.has(t.type))if(!A.has(t.type))t.args?r.push(c("unknown-part",`"${t.type}" is not a known part`,{loc:t.loc,suggestion:p(t.type,[...a.parts||[],...j]),from:t.type})):r.push(c("unknown-type",`"${t.type}" is not a known primitive`,{loc:t.loc,suggestion:p(t.type,[...A]),from:t.type}));else{const l=H[t.type],g=l?l.props:{};for(const[u,$]of Object.entries(g))!$.endsWith("?")&&!(u in(t.props||{}))&&r.push(c("missing-prop",`${t.type} is missing the required "${u}"`,{loc:t.loc}))}const n=t.props||{};for(const l of Q)l in n&&L(n[l],t);if(n.action&&R(n.action,t),n.submit&&R(n.submit,t),Array.isArray(n.style)){const l=a.theme||B,g=Object.keys(l.space||{}).length>0;for(const u of n.style)G(u)?g&&U(u,l)===null&&r.push(c("unknown-token",`"${u}": that step isn't in your theme scale`,{loc:t.loc,suggestion:p(u,W),from:u})):r.push(c("unknown-token",`"${u}" is not an accepted style token`,{loc:t.loc,suggestion:p(u,W),from:u}))}t.type===d.When&&n.cond&&b(n.cond,t,s),t.type===d.Each&&n.list&&b(n.list,t,s);const i=[];(t.type===d.Text||t.type===d.Title||t.type===d.Span)&&n.value&&i.push(n.value),t.type===d.Image&&(n.src&&i.push(n.src),n.alt&&i.push(n.alt)),t.type===d.Link&&n.to&&i.push(n.to),n.label&&i.push(n.label);for(const l of i)if(typeof l=="object"&&"kind"in l&&l.kind===h.Interp)for(const g of l.parts)typeof g!="string"&&b(g,t,s);const f=t.type===d.Each&&n.as?new Map([...s,[n.as,K(n.list)]]):s;for(const l of t.children||[])V(l,f)};if(o.rootId?V(o.rootId,new Map):a.kind!=="store"&&r.push(c("no-root","the doc is missing a rootId")),a.kind==="store")for(const[e,s]of Object.entries(o.gets||{})){S(s);for(const t of m(s)){const n=t.split(".")[0];k.has(n)||r.push(c("unknown-ref",`get "${e}": "${n}" is not a state of this store`,{suggestion:p(n,[...k]),from:n}))}}for(const[e,s]of Object.entries(o.actions||{})){const t=new Set(s.mutates||[]),n=i=>{if(i.op===J.If){S(i.cond);for(const f of i.then||[])n(f);for(const f of i.else||[])n(f);return}C.has(i.op)||r.push(c("unknown-op",`action "${e}" uses unknown op "${i.op}"`,{suggestion:p(i.op,[...C]),from:i.op})),"target"in i&&i.target&&!t.has(i.target)&&r.push(c("undeclared-mutation",`action "${e}" mutates "${i.target}" but only declares mutates(${[...t].join(", ")||"\u2205"})`,{suggestion:p(i.target,[...t]),from:i.target})),"arg"in i&&i.arg&&S(i.arg)};for(const i of s.body||[])n(i)}return{ok:r.length===0,diagnostics:r}}export{et as validate};
@@ -1 +1 @@
1
- class c extends Error{code;loc;constructor(r,n){super(r),this.name="ParseError",this.code="syntax",this.loc=n||null}}function g(t,r,n={}){const{loc:e=null,suggestion:o=null,severity:s="error"}=n;return{code:t,severity:s,message:r,loc:e,suggestion:o}}function u(t,r,n=3){let e=null,o=1/0;for(const s of r){const i=l(t,s);i<o&&(o=i,e=s)}return o<=n?e:null}function l(t,r){const n=Array.from({length:r.length+1},(e,o)=>o);for(let e=1;e<=t.length;e++){let o=n[0];n[0]=e;for(let s=1;s<=r.length;s++){const i=n[s];n[s]=t[e-1]===r[s-1]?o:1+Math.min(o,n[s],n[s-1]),o=i}}return n[r.length]}function a(t,r){const n=t.loc?`${r}:${t.loc.line}:${t.loc.col}`:r,e=t.suggestion?` \u2192 did you mean "${t.suggestion}"?`:"";return`${n} ${t.severity} [${t.code}] ${t.message}${e}`}export{c as ParseError,u as closest,g as diag,a as formatDiagnostic};
1
+ class g extends Error{code;loc;constructor(r,n){super(r),this.name="ParseError",this.code="syntax",this.loc=n||null}}function u(t,r,n={}){const{loc:o=null,suggestion:e=null,severity:s="error",from:l=null,related:i=null}=n;return{code:t,severity:s,message:r,loc:o,suggestion:e,fix:l&&e?{from:l,to:e}:null,related:i}}function a(t,r,n=3){let o=null,e=1/0;for(const s of r){const l=c(t,s);l<e&&(e=l,o=s)}return e<=n?o:null}function c(t,r){const n=Array.from({length:r.length+1},(o,e)=>e);for(let o=1;o<=t.length;o++){let e=n[0];n[0]=o;for(let s=1;s<=r.length;s++){const l=n[s];n[s]=t[o-1]===r[s-1]?e:1+Math.min(e,n[s],n[s-1]),e=l}}return n[r.length]}function f(t,r){const n=t.loc?`${r}:${t.loc.line}:${t.loc.col}`:r,o=t.suggestion?` \u2192 did you mean "${t.suggestion}"?`:"";return`${n} ${t.severity} [${t.code}] ${t.message}${o}`}export{g as ParseError,a as closest,u as diag,f as formatDiagnostic};
@@ -1,15 +1,15 @@
1
- import{readFileSync as d,existsSync as g}from"node:fs";import{fileURLToPath as P}from"node:url";import{dirname as N,join as m}from"node:path";import{parse as W}from"#engine/lang/parse.js";import{toDoc as C}from"#engine/ir/flatten.js";import{load as D,loadAllParts as E,findStores as T}from"#engine/project/load.js";import{validate as H}from"#engine/ir/validate.js";import{compileModule as b,compileStore as F}from"#engine/compile/compile.js";import{mergeTheme as j}from"#engine/style/tokens.js";import{Nt as $}from"#engine/shared/vocab.js";const v="virtual:muten/runtime",h="virtual:muten/store/",w="virtual:muten/shell",M=N(P(import.meta.url)),J=d(m(M,"runtime.js"),"utf8");function L(y={}){const k=y.store!==!1;let p=j(y.theme),c=process.cwd(),O={},S={};const l={};let u,f=null;const I=()=>{const t=new Set,o=(u?.routes||[]).map(s=>{const n=JSON.stringify("/"+s.url.replace(/^\//,"")),i=`() => import(${JSON.stringify("/src/pages/"+s.page+"/"+s.page+".muten")})`;if(s.guard){const[a,r]=s.guard.split(".");return t.add(a),` ${n}: { load: ${i}, guard: () => ${s.guardNeg?"!":""}__store_${a}.${r}.get(), redirect: ${JSON.stringify(s.redirect)} },`}return` ${n}: { load: ${i} },`}).join(`
2
- `),e=[...t].map(s=>`import * as __store_${s} from '${h}${s}';`).join(`
3
- `);return`import * as __shell from '${w}';
4
- import { route, injectCss } from '${v}';
5
- ${f?`import ${JSON.stringify(f)};
6
- `:""}${e}
1
+ import{readFileSync as f,existsSync as g}from"node:fs";import{fileURLToPath as P}from"node:url";import{dirname as C,join as l}from"node:path";import{parse as b}from"#engine/lang/parse.js";import{toDoc as D}from"#engine/ir/flatten.js";import{load as M,loadAllParts as E,findStores as H}from"#engine/project/load.js";import{validate as F}from"#engine/ir/validate.js";import{compileModule as k,compileStore as J}from"#engine/compile/compile.js";import{mergeTheme as j}from"#engine/style/tokens.js";import{Nt as $}from"#engine/shared/vocab.js";const O="virtual:muten/runtime",h="virtual:muten/store/",R="virtual:muten/shell",L=C(P(import.meta.url)),U=f(l(L,"runtime.js"),"utf8");function V(y={}){const I=y.store!==!1;let p=j(y.theme),i=process.cwd(),w={},S={};const c={};let m,d=null;const N=()=>{const e=new Set,t=(m?.routes||[]).map(o=>{const r=JSON.stringify("/"+o.url.replace(/^\//,"")),a=`() => import(${JSON.stringify("/src/pages/"+o.page+"/"+o.page+".muten")})`;if(o.guard){const[u,n]=o.guard.split(".");return e.add(u),` ${r}: { load: ${a}, guard: () => ${o.guardNeg?"!":""}__store_${u}.${n}.get(), redirect: ${JSON.stringify(o.redirect)} },`}return` ${r}: { load: ${a} },`}).join(`
2
+ `),s=[...e].map(o=>`import * as __store_${o} from '${h}${o}';`).join(`
3
+ `);return`import * as __shell from '${R}';
4
+ import { route, injectCss } from '${O}';
5
+ ${d?`import ${JSON.stringify(d)};
6
+ `:""}${s}
7
7
  const routes = {
8
- ${o}
8
+ ${t}
9
9
  };
10
10
  const root = document.getElementById('app');
11
11
  if (root) {
12
12
  injectCss(__shell.css);
13
13
  const outlet = __shell.mount(root);
14
14
  route(outlet, routes);
15
- }`},R=async()=>{O=await E(c);for(const e of Object.keys(l))delete l[e];if(k){S=T(m(c,"src"));for(const[e,s]of Object.entries(S))l[e]={state:Object.keys(s.state||{}),gets:Object.keys(s.gets||{}),actions:Object.keys(s.actions||{})}}const t=m(c,"src","app.muten");u=g(t)?W(d(t,"utf8")):void 0;const o=m(c,"theme.muten");p=g(o)?j(W(d(o,"utf8")).theme||{}):j(y.theme),f=null;for(const e of["styles.css","styles.scss"])if(g(m(c,"src",e))){f="/src/"+e;break}};return{name:"vite-plugin-muten",enforce:"pre",async configResolved(t){c=t.root,await R()},resolveId(t){if(t===v||t===w||t.startsWith(h))return"\0"+t},load(t){if(t==="\0"+v)return J;if(t.startsWith("\0"+h)){const o=S[t.slice(("\0"+h).length)];if(o)return F({state:o.state||{},gets:o.gets||{},actions:o.actions||{},effects:o.effects||[],entities:o.entities||{}},o.mock||{},o.sources||{})}if(t==="\0"+w){const o=u?.shell||{type:$.Shell,props:{},children:[{type:$.Slot,props:{}}]},e=C({...u||{},screen:"shell",entities:{},state:{},actions:{},tree:o});return b(e,{},"",{},{},{stores:l,theme:p})}},async transform(t,o){if(!o.endsWith(".muten"))return null;if(o.replace(/\\/g,"/").endsWith("/src/app.muten"))return{code:I(),map:null};const e=await D(o,O),{ok:s,diagnostics:n}=H(e.doc,{parts:e.partNames,stores:Object.keys(l),theme:p});if(!s)throw new Error("muten: "+n.map(r=>r.message).join(" \xB7 "));const i=[...new Set(Object.values(e.doc.nodes).filter(r=>r.type===$.Custom).map(r=>r.props?.component))],a={};for(const r of i){if(!r)continue;const _=m(c,"src","components",r+".js");g(_)&&(a[r]=d(_,"utf8"))}return{code:b(e.doc,e.data,e.styles.css,a,e.sources,{stores:l,theme:p,api:u?.api||{}}),map:null}},handleHotUpdate(t){(t.file.endsWith(".muten")||t.file.endsWith(".store"))&&t.server.ws.send({type:"full-reload"})},configureServer(t){const o=s=>{const n=s.replace(/\\/g,"/");return n.includes("/parts/")&&n.endsWith(".muten")||n.endsWith(".store")||n.endsWith("/app.muten")||n.endsWith("/theme.muten")||n.endsWith("/styles.css")||n.endsWith("/styles.scss")},e=s=>{o(s)&&R().then(()=>t.ws.send({type:"full-reload"}))};t.watcher.on("add",e),t.watcher.on("change",e),t.watcher.on("unlink",e),t.middlewares.use((s,n,i)=>{if((s.url||"").split("?")[0]!=="/src/app.muten"){i();return}t.transformRequest("/src/app.muten").then(a=>{if(!a){i();return}n.setHeader("Content-Type","text/javascript"),n.end(a.code)},i)})}}}export{L as default};
15
+ }`},T=async()=>{w=await E(i);for(const s of Object.keys(c))delete c[s];if(I){S=H(l(i,"src"));for(const[s,o]of Object.entries(S))c[s]={state:Object.keys(o.state||{}),gets:Object.keys(o.gets||{}),actions:Object.keys(o.actions||{})}}const e=l(i,"src","app.muten");m=g(e)?b(f(e,"utf8")):void 0;const t=l(i,"theme.muten");p=g(t)?j(b(f(t,"utf8")).theme||{}):j(y.theme),d=null;for(const s of["styles.css","styles.scss"])if(g(l(i,"src",s))){d="/src/"+s;break}};let v=null;const _=e=>{v&&clearTimeout(v),v=setTimeout(()=>{T().then(()=>{for(const t of e.moduleGraph.idToModuleMap.values()){const s=t.id||"";(s.endsWith(".muten")||s.includes("virtual:muten/shell")||s.includes("virtual:muten/store/"))&&e.moduleGraph.invalidateModule(t)}e.ws.send({type:"full-reload"})})},30)};return{name:"vite-plugin-muten",enforce:"pre",async configResolved(e){i=e.root,await T()},resolveId(e){if(e===O||e===R||e.startsWith(h))return"\0"+e},load(e){if(e==="\0"+O)return U;if(e.startsWith("\0"+h)){const t=S[e.slice(("\0"+h).length)];if(t)return J({state:t.state||{},gets:t.gets||{},actions:t.actions||{},effects:t.effects||[],entities:t.entities||{}},t.mock||{},t.sources||{})}if(e==="\0"+R){const t=m?.shell||{type:$.Shell,props:{},children:[{type:$.Slot,props:{}}]},s=D({...m||{},screen:"shell",entities:{},state:{},actions:{},tree:t});return k(s,{},"",{},{},{stores:c,theme:p})}},async transform(e,t){if(!t.endsWith(".muten"))return null;if(t.replace(/\\/g,"/").endsWith("/src/app.muten"))return{code:N(),map:null};const s=await M(t,w),{ok:o,diagnostics:r}=F(s.doc,{parts:s.partNames,stores:Object.keys(c),theme:p});if(!o)throw new Error("muten: "+r.map(n=>n.message).join(" \xB7 "));const a=[...new Set(Object.values(s.doc.nodes).filter(n=>n.type===$.Custom).map(n=>n.props?.component))],u={};for(const n of a){if(!n)continue;const W=l(i,"src","components",n+".js");g(W)&&(u[n]=f(W,"utf8"))}return{code:k(s.doc,s.data,s.styles.css,u,s.sources,{stores:c,theme:p,api:m?.api||{}}),map:null}},handleHotUpdate(e){if(e.file.endsWith(".muten")||e.file.endsWith(".store"))return _(e.server),[]},configureServer(e){const t=s=>{const o=s.replace(/\\/g,"/");(o.endsWith(".muten")||o.endsWith(".store")||o.endsWith("/styles.css")||o.endsWith("/styles.scss")||o.includes("/components/")&&o.endsWith(".js"))&&_(e)};e.watcher.on("add",t),e.watcher.on("change",t),e.watcher.on("unlink",t),e.middlewares.use((s,o,r)=>{if((s.url||"").split("?")[0]!=="/src/app.muten"){r();return}e.transformRequest("/src/app.muten").then(a=>{if(!a){r();return}o.setHeader("Content-Type","text/javascript"),o.end(a.code)},r)})}}}export{V as default};
@@ -0,0 +1,105 @@
1
+ # muten.gbnf — GENERATED from @muten/core by scripts/gen-gbnf.mjs. DO NOT EDIT BY HAND.
2
+ # A constrained-decoding grammar for ONE screen/page. Covers screen/state/store/const/get/entity/
3
+ # meta/use/param/action + the node tree + expressions. (app.muten: routes/shell/api/sources/theme/part
4
+ # are a separate file — out of scope for v1.)
5
+
6
+ root ::= ws "screen" sp ident ws decls node ws
7
+
8
+ decls ::= (decl ws)*
9
+ decl ::= use-decl | param-decl | const-decl | entity-decl | state-decl | store-decl | get-decl | meta-decl | action-decl
10
+
11
+ use-decl ::= "use" sp ident (ws "," ws ident)* sp "from" sp string
12
+ param-decl ::= "param" sp ident
13
+ const-decl ::= "const" sp ident ws "=" ws scalar
14
+ entity-decl ::= "entity" sp ident ws "{" ws (field ws)* "}"
15
+ field ::= ident sp ident (ws "|" ws ident)* (sp constraint)*
16
+ constraint ::= "required" | ("min" | "max") ws ":" ws number
17
+ state-decl ::= "state" ws "{" ws (statevar ws)* "}"
18
+ store-decl ::= "store" ws "{" ws (statevar ws)* "}"
19
+ statevar ::= ident ws "=" ws stateinit ws ":" ws type
20
+ stateinit ::= "query" sp ident | value
21
+ type ::= ident ("<" ident ">")?
22
+ get-decl ::= "get" sp ident ws "=" ws expr
23
+ meta-decl ::= "meta" ws "{" ws (ident sp string ws)* "}"
24
+ action-decl ::= "action" sp ident mutates? input? ws actionbody
25
+ mutates ::= sp "mutates" sp ident (ws "," ws ident)*
26
+ input ::= ws "<-" ws ident
27
+ actionbody ::= "{" ws (stmt ws)* "}"
28
+ stmt ::= ifstmt | requeststmt | callstmt
29
+ ifstmt ::= "if" sp expr ws actionbody (ws "else" ws actionbody)?
30
+ requeststmt ::= ("post" | "put" | "delete") sp string (sp "body" sp expr)?
31
+ callstmt ::= ident "." actionop "(" ws callargs ws ")"
32
+ actionop ::= "push" | "remove" | "reset" | "set" | "create" | "update" | "delete" | "refetch"
33
+ callargs ::= refetcharg (ws "," ws refetcharg)* | predarg | expr | ""
34
+ refetcharg ::= ident ws ":" ws expr
35
+ predarg ::= ident ws "=>" ws expr
36
+
37
+ node ::= whennode | eachnode | linknode | actionnode | customnode | plainnode
38
+ whennode ::= "when" sp expr ws block
39
+ eachnode ::= "each" sp expr sp "as" sp ident ws block
40
+ block ::= "{" ws (node ws)* "}"
41
+ linknode ::= "Link" (sp (commonpart | "->" ws path))* (ws block)?
42
+ actionnode ::= ("RowAction" | "Button") (sp (commonpart | actionarrow))* (ws block)?
43
+ actionarrow ::= "->" ws dotted (ws "(" ws expr? ws ")")?
44
+ customnode ::= "Custom" sp ident (sp modifier)*
45
+ plainnode ::= ("Stack" | "Header" | "Nav" | "Sidebar" | "Footer" | "Page" | "Text" | "Title" | "Span" | "Image" | "SearchField" | "DataTable" | "Form" | "slot") (sp commonpart)* (ws block)?
46
+ commonpart ::= string | ref | level | modifier
47
+ level ::= "h" [1-6]
48
+
49
+ modifier ::= stylemod | classmod | bindmod | submitmod | wheremod | columnsmod | altmod | inputsmod | onmod
50
+ stylemod ::= "style" ws "(" ws styletoken (ws "," ws styletoken)* ws ")"
51
+ classmod ::= "class" ws "(" ws classitem (ws "," ws classitem)* ws ")"
52
+ classitem ::= (string | ident) (sp "when" sp expr)?
53
+ bindmod ::= "bind" sp (ref | dotted)
54
+ submitmod ::= "submit" sp dotted
55
+ wheremod ::= "where" ws "(" ws clause (ws "," ws clause)* ws ")"
56
+ clause ::= dotted ws cmpop ws (ref | value)
57
+ columnsmod ::= "columns" ws "(" ws ident (ws "," ws ident)* ws ")"
58
+ altmod ::= "alt" sp string
59
+ inputsmod ::= "inputs" ws "(" ws argpairs ws ")"
60
+ onmod ::= "on" ws "(" ws argpairs ws ")"
61
+ argpairs ::= argpair (ws "," ws argpair)*
62
+ argpair ::= ident ws ":" ws argval
63
+ argval ::= string | number | ref | dotted
64
+
65
+ styletoken ::= (breakpoint ":")? (atom | family "." tokenmod)
66
+ breakpoint ::= "sm" | "md" | "lg" | "xl"
67
+ atom ::= "row" | "column" | "wrap" | "grid" | "grow" | "center" | "between" | "bold" | "italic"
68
+ family ::= "gap" | "padding" | "margin" | "cols" | "rows" | "text" | "weight" | "leading" | "align" | "justify" | "items" | "width" | "height"
69
+ tokenmod ::= ("x." | "y.")? scaleseg
70
+ scaleseg ::= ident | number
71
+
72
+ path ::= ("/" pathseg?)+
73
+ pathseg ::= ident | "{" ws expr ws "}"
74
+
75
+ # expressions — the parser's precedence ladder (no left recursion: iterate, don't self-reference first)
76
+ expr ::= ternary
77
+ ternary ::= orexpr (ws "?" ws ternary ws ":" ws ternary)?
78
+ orexpr ::= andexpr (sp "or" sp andexpr)*
79
+ andexpr ::= cmpexpr (sp "and" sp cmpexpr)*
80
+ cmpexpr ::= addexpr (ws cmpop ws addexpr)*
81
+ cmpop ::= "==" | "!=" | "<=" | ">=" | "<" | ">" | "contains"
82
+ addexpr ::= mulexpr (ws ("+" | "-") ws mulexpr)*
83
+ mulexpr ::= unary (ws ("*" | "/") ws unary)*
84
+ unary ::= ("not" sp)? primary
85
+ primary ::= "(" ws ternary ws ")" | string | number | bool | "null" | callorref
86
+ callorref ::= dotted (ws "(" ws (ternary (ws "," ws ternary)*)? ws ")")?
87
+
88
+ value ::= array | object | scalar
89
+ array ::= "[" ws (value (ws "," ws value)*)? ws "]"
90
+ object ::= "{" ws (objpair (ws "," ws objpair)*)? ws "}"
91
+ objpair ::= (ident | string) ws ":" ws value
92
+ scalar ::= string | number | bool | "null" | ident
93
+
94
+ bool ::= "true" | "false"
95
+ ref ::= "@" ident ("." ident)*
96
+ dotted ::= "$"? ident ("." ident)*
97
+ ident ::= [a-zA-Z_] [a-zA-Z0-9_]*
98
+ number ::= [0-9]+ ("." [0-9]+)?
99
+ string ::= "\"" [^"]* "\""
100
+
101
+ # whitespace — NO comments in the generation grammar on purpose: a weak model degenerates into
102
+ # comment spam (free whitespace) and never closes the page. Humans add comments by hand after.
103
+ ws ::= wschar*
104
+ sp ::= wschar+
105
+ wschar ::= [ \t\r\n]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@muten/core",
3
- "version": "0.0.7",
3
+ "version": "0.0.9",
4
4
  "description": "AI-first frontend framework — compiles .muten files to vanilla JS + signals.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -24,6 +24,7 @@
24
24
  "files": [
25
25
  "dist",
26
26
  "spec",
27
+ "grammar",
27
28
  "README.md",
28
29
  "LICENSE"
29
30
  ],
@@ -41,8 +42,9 @@
41
42
  "license": "MIT",
42
43
  "scripts": {
43
44
  "prepare": "npm run build",
44
- "build": "tsc && node --experimental-strip-types esbuild.ts",
45
- "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/print.ts && node --experimental-strip-types test/externs.ts && node --experimental-strip-types test/islands.ts"
45
+ "build": "tsc && node --experimental-strip-types esbuild.ts && node scripts/gen-gbnf.mjs",
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/print.ts && node --experimental-strip-types test/externs.ts && node --experimental-strip-types test/islands.ts && node test/gbnf.mjs"
46
48
  },
47
49
  "optionalDependencies": {
48
50
  "sass": "^1.101.0"
@@ -50,6 +52,7 @@
50
52
  "devDependencies": {
51
53
  "@types/node": "^26.0.0",
52
54
  "esbuild": "^0.28.1",
55
+ "gbnf": "^0.1.41",
53
56
  "typescript": "^6.0.3",
54
57
  "vite": "^8.0.16"
55
58
  }