@muten/core 0.0.1 → 0.0.3
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 +97 -97
- package/dist/build.js +9 -9
- package/dist/engine/style/tokens.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,97 +1,97 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
An **AI-first** frontend framework. You write `.muten` files; muten compiles them to vanilla JS
|
|
4
|
-
with fine-grained signals — **no virtual DOM, no framework runtime to ship**. The language is small,
|
|
5
|
-
semantic and analyzable on purpose: an AI (or a person) can **locate and mutate** an app cheaply.
|
|
6
|
-
|
|
7
|
-
```sh
|
|
8
|
-
npm create muten@latest my-app # scaffold a new app (cross-platform: Windows + macOS)
|
|
9
|
-
cd my-app && npm install && npm run dev
|
|
10
|
-
```
|
|
11
|
-
|
|
12
|
-
## The app, by convention
|
|
13
|
-
|
|
14
|
-
```
|
|
15
|
-
my-app/
|
|
16
|
-
├─ src/
|
|
17
|
-
│ ├─ app.muten # the ROOT: routes (+ optional persistent shell)
|
|
18
|
-
│ ├─ pages/
|
|
19
|
-
│ │ └─ home/home.muten # a page; the folder name is its route target
|
|
20
|
-
│ ├─ parts/ # reusable .muten components (object + action params)
|
|
21
|
-
│ └─ components/ # host-written Custom JS (the escape hatch)
|
|
22
|
-
├─ theme.muten # the project's token scale (md=16px, breakpoints, …)
|
|
23
|
-
└─ src/styles.css # the look (muten ships structure + layout; the skin is yours)
|
|
24
|
-
```
|
|
25
|
-
|
|
26
|
-
`src/app.muten` is the single source of truth the AI reads first:
|
|
27
|
-
|
|
28
|
-
```
|
|
29
|
-
routes {
|
|
30
|
-
/ -> home
|
|
31
|
-
}
|
|
32
|
-
```
|
|
33
|
-
|
|
34
|
-
## CLI
|
|
35
|
-
|
|
36
|
-
```sh
|
|
37
|
-
muten build [dir] # compile → ./dist/<route>/index.html (+ dist/app.map.json, the app graph)
|
|
38
|
-
muten lint [dir] # parse + validate every page, no compile
|
|
39
|
-
```
|
|
40
|
-
|
|
41
|
-
`build`/`lint` default to the current directory; pass a path to target another. The `muten` bin ships
|
|
42
|
-
with the app (it's a dependency). To scaffold a *new* app, use `npm create muten@latest` (the separate
|
|
43
|
-
[`create-muten`](https://www.npmjs.com/package/create-muten) scaffolder).
|
|
44
|
-
|
|
45
|
-
## Dev server (Vite)
|
|
46
|
-
|
|
47
|
-
The Vite plugin gives a Muten app a dev server + HMR + client-side routing while authoring stays the
|
|
48
|
-
DSL. `npm create muten` wires it up; `npm run dev` runs it.
|
|
49
|
-
|
|
50
|
-
```js
|
|
51
|
-
// vite.config.mjs
|
|
52
|
-
import muten from '@muten/core/vite-plugin-muten.js';
|
|
53
|
-
export default { plugins: [muten()] }; // theme.muten is auto-loaded
|
|
54
|
-
```
|
|
55
|
-
|
|
56
|
-
## Programmatic API
|
|
57
|
-
|
|
58
|
-
```js
|
|
59
|
-
import { buildApp, compile, parse, validate, toDoc } from '@muten/core';
|
|
60
|
-
|
|
61
|
-
await buildApp('./my-app'); // same as `muten build ./my-app`
|
|
62
|
-
const html = compile(toDoc(parse(src))); // drive the compiler directly (embedding)
|
|
63
|
-
```
|
|
64
|
-
|
|
65
|
-
## Architecture
|
|
66
|
-
|
|
67
|
-
The compiler is a straight pipeline of small, single-purpose stages:
|
|
68
|
-
|
|
69
|
-
```
|
|
70
|
-
.muten ─[lang]→ IR ─[ir: compose]→ tree ─[ir: flatten]→ Doc ─[ir: validate]→ ✓ ─[compile]→ JS
|
|
71
|
-
```
|
|
72
|
-
|
|
73
|
-
The source is TypeScript under `src/`, organized by **domain** — each has its own README:
|
|
74
|
-
|
|
75
|
-
| Domain | Role |
|
|
76
|
-
|---|---|
|
|
77
|
-
| [`src/engine/shared`](src/engine/shared/README.md) | contracts: types, the vocabulary (no magic strings), diagnostics |
|
|
78
|
-
| [`src/engine/lang`](src/engine/lang/README.md) | front-end: `.muten` text → IR (lexer · grammar · parser · manifest) |
|
|
79
|
-
| [`src/engine/ir`](src/engine/ir/README.md) | IR transforms + validation (compose · flatten · validate) |
|
|
80
|
-
| [`src/engine/compile`](src/engine/compile/README.md) | back-end: Doc → runnable JS (DOM + logic + emit + helpers) |
|
|
81
|
-
| [`src/engine/style`](src/engine/style/README.md) | the styling token vocabulary (the engine ships no values) |
|
|
82
|
-
| [`src/engine/project`](src/engine/project/README.md) | filesystem + whole-app awareness (load · analyze · routes · styles) |
|
|
83
|
-
|
|
84
|
-
The runtime (the only thing shipped to the browser), the Vite plugin, the CLI and the build/lint
|
|
85
|
-
orchestration also live in `src/`. See [`src/engine/README.md`](src/engine/README.md) for the
|
|
86
|
-
file-level conventions (≤500 lines, honest types, data-table dispatch, no magic strings).
|
|
87
|
-
|
|
88
|
-
## Build
|
|
89
|
-
|
|
90
|
-
`npm run build` = `tsc` (strict type-check) + `esbuild` → `dist/**/*.js`, **minified, per-file**
|
|
91
|
-
(modules preserved, so nothing bundles into a heavy monolith). `dist/` is generated — edit `src/`.
|
|
92
|
-
|
|
93
|
-
## Styling & escape hatch
|
|
94
|
-
|
|
95
|
-
muten imposes no theme. A page lays itself out with `style(…)` tokens (analyzable, resolved against
|
|
96
|
-
`theme.muten`) and skins itself via `class("…")` (your CSS / Tailwind / anything). For behavior the
|
|
97
|
-
primitives can't express, drop to a `Custom` component (`src/components/<Name>.js`).
|
|
1
|
+
|
|
2
|
+
<img width="157" height="157" alt="Group 21" src="https://github.com/user-attachments/assets/fe9a02e6-483d-4788-9286-142c1ddb7057" /> </br>
|
|
3
|
+
An **AI-first** frontend framework. You write `.muten` files; muten compiles them to vanilla JS
|
|
4
|
+
with fine-grained signals — **no virtual DOM, no framework runtime to ship**. The language is small,
|
|
5
|
+
semantic and analyzable on purpose: an AI (or a person) can **locate and mutate** an app cheaply.
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
npm create muten@latest my-app # scaffold a new app (cross-platform: Windows + macOS)
|
|
9
|
+
cd my-app && npm install && npm run dev
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## The app, by convention
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
my-app/
|
|
16
|
+
├─ src/
|
|
17
|
+
│ ├─ app.muten # the ROOT: routes (+ optional persistent shell)
|
|
18
|
+
│ ├─ pages/
|
|
19
|
+
│ │ └─ home/home.muten # a page; the folder name is its route target
|
|
20
|
+
│ ├─ parts/ # reusable .muten components (object + action params)
|
|
21
|
+
│ └─ components/ # host-written Custom JS (the escape hatch)
|
|
22
|
+
├─ theme.muten # the project's token scale (md=16px, breakpoints, …)
|
|
23
|
+
└─ src/styles.css # the look (muten ships structure + layout; the skin is yours)
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
`src/app.muten` is the single source of truth the AI reads first:
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
routes {
|
|
30
|
+
/ -> home
|
|
31
|
+
}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## CLI
|
|
35
|
+
|
|
36
|
+
```sh
|
|
37
|
+
muten build [dir] # compile → ./dist/<route>/index.html (+ dist/app.map.json, the app graph)
|
|
38
|
+
muten lint [dir] # parse + validate every page, no compile
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
`build`/`lint` default to the current directory; pass a path to target another. The `muten` bin ships
|
|
42
|
+
with the app (it's a dependency). To scaffold a *new* app, use `npm create muten@latest` (the separate
|
|
43
|
+
[`create-muten`](https://www.npmjs.com/package/create-muten) scaffolder).
|
|
44
|
+
|
|
45
|
+
## Dev server (Vite)
|
|
46
|
+
|
|
47
|
+
The Vite plugin gives a Muten app a dev server + HMR + client-side routing while authoring stays the
|
|
48
|
+
DSL. `npm create muten` wires it up; `npm run dev` runs it.
|
|
49
|
+
|
|
50
|
+
```js
|
|
51
|
+
// vite.config.mjs
|
|
52
|
+
import muten from '@muten/core/vite-plugin-muten.js';
|
|
53
|
+
export default { plugins: [muten()] }; // theme.muten is auto-loaded
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Programmatic API
|
|
57
|
+
|
|
58
|
+
```js
|
|
59
|
+
import { buildApp, compile, parse, validate, toDoc } from '@muten/core';
|
|
60
|
+
|
|
61
|
+
await buildApp('./my-app'); // same as `muten build ./my-app`
|
|
62
|
+
const html = compile(toDoc(parse(src))); // drive the compiler directly (embedding)
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Architecture
|
|
66
|
+
|
|
67
|
+
The compiler is a straight pipeline of small, single-purpose stages:
|
|
68
|
+
|
|
69
|
+
```
|
|
70
|
+
.muten ─[lang]→ IR ─[ir: compose]→ tree ─[ir: flatten]→ Doc ─[ir: validate]→ ✓ ─[compile]→ JS
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
The source is TypeScript under `src/`, organized by **domain** — each has its own README:
|
|
74
|
+
|
|
75
|
+
| Domain | Role |
|
|
76
|
+
|---|---|
|
|
77
|
+
| [`src/engine/shared`](src/engine/shared/README.md) | contracts: types, the vocabulary (no magic strings), diagnostics |
|
|
78
|
+
| [`src/engine/lang`](src/engine/lang/README.md) | front-end: `.muten` text → IR (lexer · grammar · parser · manifest) |
|
|
79
|
+
| [`src/engine/ir`](src/engine/ir/README.md) | IR transforms + validation (compose · flatten · validate) |
|
|
80
|
+
| [`src/engine/compile`](src/engine/compile/README.md) | back-end: Doc → runnable JS (DOM + logic + emit + helpers) |
|
|
81
|
+
| [`src/engine/style`](src/engine/style/README.md) | the styling token vocabulary (the engine ships no values) |
|
|
82
|
+
| [`src/engine/project`](src/engine/project/README.md) | filesystem + whole-app awareness (load · analyze · routes · styles) |
|
|
83
|
+
|
|
84
|
+
The runtime (the only thing shipped to the browser), the Vite plugin, the CLI and the build/lint
|
|
85
|
+
orchestration also live in `src/`. See [`src/engine/README.md`](src/engine/README.md) for the
|
|
86
|
+
file-level conventions (≤500 lines, honest types, data-table dispatch, no magic strings).
|
|
87
|
+
|
|
88
|
+
## Build
|
|
89
|
+
|
|
90
|
+
`npm run build` = `tsc` (strict type-check) + `esbuild` → `dist/**/*.js`, **minified, per-file**
|
|
91
|
+
(modules preserved, so nothing bundles into a heavy monolith). `dist/` is generated — edit `src/`.
|
|
92
|
+
|
|
93
|
+
## Styling & escape hatch
|
|
94
|
+
|
|
95
|
+
muten imposes no theme. A page lays itself out with `style(…)` tokens (analyzable, resolved against
|
|
96
|
+
`theme.muten`) and skins itself via `class("…")` (your CSS / Tailwind / anything). For behavior the
|
|
97
|
+
primitives can't express, drop to a `Custom` component (`src/components/<Name>.js`).
|
package/dist/build.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import{writeFileSync as
|
|
2
|
-
`);const
|
|
3
|
-
`+$(o,i(t.screenPath)))}const{doc:c,data:
|
|
4
|
-
`+
|
|
5
|
-
`));const
|
|
6
|
-
`);
|
|
1
|
+
import{writeFileSync as f,mkdirSync as j,readFileSync as x,existsSync as A,rmSync as S}from"node:fs";import{join as s,relative as E}from"node:path";import{Nt as N}from"#engine/shared/vocab.js";import{readRoutes as M}from"#engine/project/routes.js";import{load as C,loadAllParts as F}from"#engine/project/load.js";import{validate as V}from"#engine/ir/validate.js";import{compile as v}from"#engine/compile/compile.js";import{formatDiagnostic as $,ParseError as H}from"#engine/shared/diagnostics.js";const I=r=>typeof r=="string"?r:r&&typeof r=="object"&&!Array.isArray(r)&&typeof r.url=="string"?r.url:"";async function Q(r,n=s(r,"dist")){const i=t=>E(r,t);S(n,{recursive:!0,force:!0});const l=await F(r);Object.keys(l).length&&console.log(`Parts: ${Object.keys(l).join(", ")}`);const g=M(r);console.log(`Host app: ${r}`),console.log(`Pages: ${g.map(t=>"/"+t.route).join(", ")}
|
|
2
|
+
`);const p=[],d={app:r.split(/[\\/]/).pop()||"",parts:Object.keys(l),routes:{}};for(const t of g){let a;try{a=await C(t.screenPath,l)}catch(e){if(!(e instanceof H))throw e;const o={code:e.code,severity:"error",message:e.message,loc:e.loc,suggestion:null};throw new Error(`/${t.route}
|
|
3
|
+
`+$(o,i(t.screenPath)))}const{doc:c,data:b,sources:h,styles:m,partNames:O}=a,{ok:w,diagnostics:P}=V(c,{parts:O});if(!w)throw new Error(`/${t.route}
|
|
4
|
+
`+P.map(e=>" "+$(e,i(t.screenPath))).join(`
|
|
5
|
+
`));const k=[...new Set(Object.values(c.nodes).filter(e=>e.type===N.Custom).map(e=>e.props?.component))],y={};for(const e of k){if(!e)continue;const o=s(r,"src","components",e+".js");if(!A(o))throw new Error(`/${t.route}: Custom component not found: src/components/${e}.js`);y[e]=x(o,"utf8")}const u=s(n,t.route);j(u,{recursive:!0}),f(s(u,"index.html"),v(c,b,m.css,y,h)),console.log(`\u2713 /${t.route} \u2192 ${i(s(u,"index.html"))} (${Object.keys(c.nodes).length} nodes${m.from?", + "+m.from:""})`),p.push(t.route),d.routes["/"+t.route]={file:i(t.screenPath),models:Object.keys(c.entities),state:Object.fromEntries(Object.entries(c.state).map(([e,o])=>[e,typeof o.source=="string"?o.source:o.initial??null])),sources:Object.fromEntries(Object.entries(h).map(([e,o])=>[e,I(o)]))}}if(j(n,{recursive:!0}),!p.includes("")){const t=p.map(a=>`<li><a href="./${a}/">/${a}</a></li>`).join(`
|
|
6
|
+
`);f(s(n,"index.html"),`<!doctype html><meta charset="utf-8"><title>app</title>
|
|
7
7
|
<h1>Routes</h1>
|
|
8
8
|
<ul>
|
|
9
|
-
${
|
|
9
|
+
${t}
|
|
10
10
|
</ul>
|
|
11
|
-
`),
|
|
12
|
-
\u2713 ${i(s(n,"index.html"))} \u2192 route index`),console.log(`\u2713 ${i(s(n,"app.map.json"))} \u2192 app graph (the root the AI reads)`),{routes:
|
|
11
|
+
`),console.log(`
|
|
12
|
+
\u2713 ${i(s(n,"index.html"))} \u2192 route index`)}return f(s(n,"app.map.json"),JSON.stringify(d,null,2)),console.log(`\u2713 ${i(s(n,"app.map.json"))} \u2192 app graph (the root the AI reads)`),{routes:p,outDir:n}}export{Q as buildApp};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
const g=new Map([["start","flex-start"],["center","center"],["end","flex-end"],["between","space-between"],["around","space-around"],["evenly","space-evenly"]]),d=new Map([["start","flex-start"],["center","center"],["end","flex-end"],["stretch","stretch"],["baseline","baseline"]]),r=new Map([["row","display:flex;flex-direction:row"],["column","display:flex;flex-direction:column"],["wrap","flex-wrap:wrap"],["grid","display:grid"],["grow","flex:1"],["center","align-items:center"],["between","justify-content:space-between"],["bold","font-weight:700"],["italic","font-style:italic"]]),
|
|
1
|
+
const g=new Map([["start","flex-start"],["center","center"],["end","flex-end"],["between","space-between"],["around","space-around"],["evenly","space-evenly"]]),d=new Map([["start","flex-start"],["center","center"],["end","flex-end"],["stretch","stretch"],["baseline","baseline"]]),r=new Map([["row","display:flex;flex-direction:row"],["column","display:flex;flex-direction:column"],["wrap","flex-wrap:wrap"],["grid","display:grid"],["grow","flex:1"],["center","align-items:center"],["between","justify-content:space-between"],["bold","font-weight:700"],["italic","font-style:italic"]]),s=(e,t)=>t[e]??(/^\d+$/.test(e)?e+"px":null),o=(e,t,n)=>{if(e.startsWith("x.")){const l=s(e.slice(2),n);return l?`${t}-left:${l};${t}-right:${l}`:null}if(e.startsWith("y.")){const l=s(e.slice(2),n);return l?`${t}-top:${l};${t}-bottom:${l}`:null}const i=s(e,n);return i?`${t}:${i}`:null},c={gap:(e,t)=>{const n=s(e,t.space);return n?`gap:${n}`:null},padding:(e,t)=>o(e,"padding",t.space),margin:(e,t)=>o(e,"margin",t.space),cols:e=>e==="auto"?"grid-template-columns:repeat(auto-fill,minmax(160px,1fr))":/^\d+$/.test(e)?`grid-template-columns:repeat(${e},1fr)`:null,rows:e=>/^\d+$/.test(e)?`grid-template-rows:repeat(${e},1fr)`:null,text:(e,t)=>{const n=s(e,t.font);return n?`font-size:${n}`:null},weight:(e,t)=>{const n=t.weight[e]??(/^\d+$/.test(e)?e:null);return n?`font-weight:${n}`:null},leading:(e,t)=>{const n=t.leading[e]??(/^[\d.]+$/.test(e)?e:null);return n?`line-height:${n}`:null},align:e=>["left","center","right","justify"].includes(e)?`text-align:${e}`:null,justify:e=>{const t=g.get(e);return t?`justify-content:${t}`:null},items:e=>{const t=d.get(e);return t?`align-items:${t}`:null},width:e=>e==="full"?"width:100%":/^\d+$/.test(e)?`width:${e}px`:null,height:e=>e==="full"?"height:100%":/^\d+$/.test(e)?`height:${e}px`:null},u=Object.keys(c),h=[...r.keys()],f=["sm","md","lg","xl"],p={space:{},font:{},weight:{},leading:{},breakpoints:{}};function w(e={}){return{space:{...e.space||{}},font:{...e.font||{}},weight:{...e.weight||{}},leading:{...e.leading||{}},breakpoints:{...e.breakpoints||{}}}}function x(e,t=p){const n=e.indexOf(":");if(n>0&&t.breakpoints[e.slice(0,n)])return x(e.slice(n+1),t);const i=r.get(e);if(i)return i;const l=e.indexOf(".");if(l<0)return null;const a=c[e.slice(0,l)];return a&&a(e.slice(l+1),t)||null}function m(e){const t=e.indexOf(":"),n=t>0&&f.includes(e.slice(0,t))?e.slice(t+1):e;if(r.has(n))return!0;const i=n.indexOf(".");return i>0&&u.includes(n.slice(0,i))}const $=e=>"t-"+e.replace(/[.:]/g,"-"),y=["row","column","wrap","grid","grow","center","between","bold","italic","gap.sm","gap.md","gap.lg","padding.md","padding.lg","padding.x.md","padding.y.md","margin.md","cols.2","cols.3","cols.auto","rows.2","text.sm","text.md","text.lg","text.xl","weight.medium","weight.bold","leading.normal","align.left","align.center","align.right","justify.center","justify.between","items.center","items.start","width.full","height.full","md:row","md:cols.2","md:cols.3","lg:cols.4"];export{h as ATOM_NAMES,f as BREAKPOINT_NAMES,u as FAMILY_NAMES,y as SUGGESTED,p as defaultTheme,m as isKnownTokenShape,w as mergeTheme,x as resolveToken,$ as tokenClass};
|