@muten/core 0.0.7 → 0.0.8
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
|
|
14
|
+
## Why muten
|
|
14
15
|
|
|
15
|
-
For an AI the cost of
|
|
16
|
-
|
|
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
|
-
- **
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
- **
|
|
23
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
2
|
-
`),
|
|
3
|
-
`);return`import * as __shell from '${
|
|
4
|
-
import { route, injectCss } from '${
|
|
5
|
-
${
|
|
6
|
-
`:""}${
|
|
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
|
-
${
|
|
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
|
-
}`},
|
|
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};
|