@madojs/mado 0.5.1 → 0.6.0
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/AGENTS.md +26 -0
- package/CHANGELOG.md +153 -0
- package/MADO_V1_PLAN.md +179 -0
- package/README.md +31 -13
- package/ROADMAP.md +28 -7
- package/TODO.md +72 -0
- package/dist/src/forms.d.ts +37 -4
- package/dist/src/forms.js +331 -57
- package/dist/src/forms.js.map +1 -1
- package/dist/src/html/bindings.d.ts +41 -0
- package/dist/src/html/bindings.js +163 -6
- package/dist/src/html/bindings.js.map +1 -1
- package/dist/src/html.d.ts +2 -0
- package/dist/src/html.js +1 -0
- package/dist/src/html.js.map +1 -1
- package/dist/src/index.d.ts +6 -6
- package/dist/src/index.js +2 -2
- package/dist/src/index.js.map +1 -1
- package/dist/src/page.d.ts +56 -0
- package/dist/src/page.js +17 -0
- package/dist/src/page.js.map +1 -1
- package/dist/src/router/manifest.d.ts +16 -1
- package/dist/src/router/manifest.js +181 -38
- package/dist/src/router/manifest.js.map +1 -1
- package/dist/src/router/match.d.ts +7 -2
- package/dist/src/router/match.js +14 -4
- package/dist/src/router/match.js.map +1 -1
- package/dist/src/router/navigation.d.ts +10 -0
- package/dist/src/router/navigation.js +71 -3
- package/dist/src/router/navigation.js.map +1 -1
- package/dist/src/signal.d.ts +15 -1
- package/dist/src/signal.js +112 -16
- package/dist/src/signal.js.map +1 -1
- package/docs/en/02-project-layout.md +99 -40
- package/docs/en/10-app-architecture.md +141 -0
- package/docs/en/11-layouts.md +115 -0
- package/docs/en/12-auth-and-api.md +217 -0
- package/docs/en/13-deployment.md +192 -0
- package/docs/en/14-testing.md +82 -0
- package/docs/en/15-error-handling.md +100 -0
- package/docs/en/16-bake-cookbook.md +93 -0
- package/docs/en/README.md +7 -0
- package/docs/fr/10-app-architecture.md +61 -0
- package/docs/fr/11-layouts.md +35 -0
- package/docs/fr/12-auth-and-api.md +35 -0
- package/docs/fr/13-deployment.md +39 -0
- package/docs/fr/14-testing.md +41 -0
- package/docs/fr/15-error-handling.md +50 -0
- package/docs/fr/16-bake-cookbook.md +35 -0
- package/docs/fr/README.md +7 -0
- package/docs/ru/10-app-architecture.md +100 -0
- package/docs/ru/11-layouts.md +47 -0
- package/docs/ru/12-auth-and-api.md +53 -0
- package/docs/ru/13-deployment.md +60 -0
- package/docs/ru/14-testing.md +50 -0
- package/docs/ru/15-error-handling.md +56 -0
- package/docs/ru/16-bake-cookbook.md +55 -0
- package/docs/ru/README.md +7 -0
- package/docs/uk/10-app-architecture.md +56 -0
- package/docs/uk/11-layouts.md +34 -0
- package/docs/uk/12-auth-and-api.md +34 -0
- package/docs/uk/13-deployment.md +39 -0
- package/docs/uk/14-testing.md +34 -0
- package/docs/uk/15-error-handling.md +32 -0
- package/docs/uk/16-bake-cookbook.md +36 -0
- package/docs/uk/README.md +7 -0
- package/llms.txt +9 -1
- package/package.json +3 -1
- package/scripts/_config.mjs +224 -0
- package/scripts/bake.mjs +217 -120
- package/scripts/bundle.mjs +110 -67
- package/scripts/cli.mjs +119 -15
- package/scripts/preview.mjs +22 -12
- package/server/serve.mjs +82 -4
- package/starters/admin/README.md +63 -0
- package/starters/admin/index.html +21 -0
- package/starters/admin/mado.config.json +22 -0
- package/starters/admin/package.json +22 -0
- package/starters/admin/public/favicon.svg +4 -0
- package/starters/admin/src/components/x-button.ts +55 -0
- package/starters/admin/src/components/x-input.ts +74 -0
- package/starters/admin/src/layouts/app.ts +101 -0
- package/starters/admin/src/layouts/auth.ts +41 -0
- package/starters/admin/src/lib/api.ts +133 -0
- package/starters/admin/src/lib/auth.ts +83 -0
- package/starters/admin/src/main.ts +15 -0
- package/starters/admin/src/pages/admin/dashboard.ts +48 -0
- package/starters/admin/src/pages/admin/order-detail.ts +78 -0
- package/starters/admin/src/pages/admin/orders.ts +117 -0
- package/starters/admin/src/pages/home.ts +25 -0
- package/starters/admin/src/pages/login.ts +70 -0
- package/starters/admin/src/pages/not-found.ts +12 -0
- package/starters/admin/src/routes.ts +40 -0
- package/starters/admin/src/styles/global.ts +86 -0
- package/starters/admin/tsconfig.json +15 -0
- package/starters/crud/mado.config.json +20 -0
- package/starters/crud/package.json +8 -4
- package/starters/crud/src/routes.ts +4 -2
- package/starters/minimal/mado.config.json +20 -0
- package/starters/minimal/package.json +7 -3
- package/starters/minimal/src/routes.ts +4 -2
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# Bake cookbook
|
|
2
|
+
|
|
3
|
+
`mado bake` рендерит выбранные роуты в статический HTML. Это для SEO и быстрого
|
|
4
|
+
первого ответа, не SSR с hydration.
|
|
5
|
+
|
|
6
|
+
## Минимальная страница
|
|
7
|
+
|
|
8
|
+
```ts
|
|
9
|
+
export default page({
|
|
10
|
+
head: () => ({ title: "Products", description: "Catalog" }),
|
|
11
|
+
view: ({ data }) => html`
|
|
12
|
+
<main>
|
|
13
|
+
<h1>Products</h1>
|
|
14
|
+
${data.products.map((p) => html`<article><h2>${p.name}</h2></article>`)}
|
|
15
|
+
</main>
|
|
16
|
+
`,
|
|
17
|
+
bake: {
|
|
18
|
+
paths: () => [{}],
|
|
19
|
+
data: async () => ({ products: await api.products() }),
|
|
20
|
+
revalidate: 3600,
|
|
21
|
+
},
|
|
22
|
+
});
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
В baked views лучше использовать обычные массивы (`items.map(...)`). Runtime
|
|
26
|
+
директивы вроде `each()` нужны браузеру.
|
|
27
|
+
|
|
28
|
+
## Dynamic routes
|
|
29
|
+
|
|
30
|
+
```ts
|
|
31
|
+
export default page<{ slug: string }>({
|
|
32
|
+
head: ({ slug }, data) => ({ title: data.title, canonical: `/blog/${slug}` }),
|
|
33
|
+
view: ({ data }) => html`<article>${unsafeHTML(data.html)}</article>`,
|
|
34
|
+
bake: {
|
|
35
|
+
paths: async () => (await api.posts()).map((p) => ({ slug: p.slug })),
|
|
36
|
+
data: ({ slug }) => api.post(slug),
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
`unsafeHTML()` используй только для доверенного или заранее очищенного HTML.
|
|
42
|
+
|
|
43
|
+
## Manifest и output
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
export const manifest = {
|
|
47
|
+
"/": () => import("./pages/home.js"),
|
|
48
|
+
"/blog/:slug": () => import("./pages/blog-post.js"),
|
|
49
|
+
};
|
|
50
|
+
export default routes(manifest);
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
`mado release` создает deploy artifact в `out/`. Если bake ругается на
|
|
54
|
+
unsupported value, значит в статическую страницу попало runtime-only значение:
|
|
55
|
+
замени `each()` на `items.map(...)` или вынеси интерактивный кусок в клиент.
|
package/docs/ru/README.md
CHANGED
|
@@ -12,3 +12,10 @@
|
|
|
12
12
|
| LLM pitfalls | [07-llm-pitfalls.md](./07-llm-pitfalls.md) |
|
|
13
13
|
| LLM zero-history test | [08-llm-zero-history-test.md](./08-llm-zero-history-test.md) |
|
|
14
14
|
| Shadow DOM vs Light DOM | [09-shadow-vs-light-dom.md](./09-shadow-vs-light-dom.md) |
|
|
15
|
+
| Архитектура приложения | [10-app-architecture.md](./10-app-architecture.md) |
|
|
16
|
+
| Layouts | [11-layouts.md](./11-layouts.md) |
|
|
17
|
+
| Auth and API | [12-auth-and-api.md](./12-auth-and-api.md) |
|
|
18
|
+
| Deployment | [13-deployment.md](./13-deployment.md) |
|
|
19
|
+
| Тестирование | [14-testing.md](./14-testing.md) |
|
|
20
|
+
| Обработка ошибок | [15-error-handling.md](./15-error-handling.md) |
|
|
21
|
+
| Bake cookbook | [16-bake-cookbook.md](./16-bake-cookbook.md) |
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# Архітектура застосунку
|
|
2
|
+
|
|
3
|
+
Production-застосунок на Mado має бути простим: один manifest маршрутів, один
|
|
4
|
+
shell, один API-клієнт, один auth-модуль і сторінки, які імпортують власні
|
|
5
|
+
компоненти.
|
|
6
|
+
|
|
7
|
+
```txt
|
|
8
|
+
src/
|
|
9
|
+
├── main.ts
|
|
10
|
+
├── routes.ts
|
|
11
|
+
├── layouts/
|
|
12
|
+
├── pages/
|
|
13
|
+
├── components/
|
|
14
|
+
├── lib/
|
|
15
|
+
└── styles/
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
`lib/` — бізнес-логіка, `layouts/` — обгортки груп маршрутів, `components/` —
|
|
19
|
+
повторні UI-теги, `pages/` — один файл на сторінку.
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
import { html, render } from "@madojs/mado";
|
|
23
|
+
import "./styles/global.js";
|
|
24
|
+
import routesApi from "./routes.js";
|
|
25
|
+
|
|
26
|
+
render(html`${routesApi.view}`, document.getElementById("app")!);
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Feature-компоненти імпортує сторінка, яка їх рендерить.
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
import { layout, routes } from "@madojs/mado";
|
|
33
|
+
import { requireAuth } from "./lib/auth.js";
|
|
34
|
+
|
|
35
|
+
export const manifest = {
|
|
36
|
+
"/": () => import("./pages/home.js"),
|
|
37
|
+
"/admin": layout({
|
|
38
|
+
layout: () => import("./layouts/app.js"),
|
|
39
|
+
guard: requireAuth,
|
|
40
|
+
routes: { "/": () => import("./pages/admin/dashboard.js") },
|
|
41
|
+
}),
|
|
42
|
+
"*": () => import("./pages/not-found.js"),
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export default routes(manifest);
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
`manifest` потрібен для `mado bake`. Для читання використовуй `resource()`, для
|
|
49
|
+
запису `mutation(..., { invalidates })`, для форм `useForm()`.
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
mado dev
|
|
53
|
+
mado release
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Деплоїться `out/`, а не `dist/`.
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Layouts
|
|
2
|
+
|
|
3
|
+
Рекомендований спосіб layout у Mado — вкладена група маршрутів у `routes.ts`.
|
|
4
|
+
|
|
5
|
+
```ts
|
|
6
|
+
import { layout, routes } from "@madojs/mado";
|
|
7
|
+
import { requireAuth } from "./lib/auth.js";
|
|
8
|
+
|
|
9
|
+
export const manifest = {
|
|
10
|
+
"/": () => import("./pages/home.js"),
|
|
11
|
+
"/login": layout({
|
|
12
|
+
layout: () => import("./layouts/auth.js"),
|
|
13
|
+
routes: { "/": () => import("./pages/login.js") },
|
|
14
|
+
}),
|
|
15
|
+
"/admin": layout({
|
|
16
|
+
layout: () => import("./layouts/app.js"),
|
|
17
|
+
guard: requireAuth,
|
|
18
|
+
routes: { "/": () => import("./pages/admin/dashboard.js") },
|
|
19
|
+
}),
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export default routes(manifest);
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Layout — це `page({ view })`, яка рендерить `child`:
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
export default page({
|
|
29
|
+
view: ({ child }) => html`<x-app-shell>${child}</x-app-shell>`,
|
|
30
|
+
});
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Один shell на групу, не на кожну сторінку. Guard на групі захищає все
|
|
34
|
+
піддерево.
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Auth та API
|
|
2
|
+
|
|
3
|
+
Starter `admin` містить рекомендований рецепт:
|
|
4
|
+
|
|
5
|
+
- `src/lib/api.ts` — один HTTP-клієнт, `ApiError`, refresh після 401;
|
|
6
|
+
- `src/lib/auth.ts` — `accessToken`, `restoreSession()`, `login()`,
|
|
7
|
+
`logout()`, `requireAuth`.
|
|
8
|
+
|
|
9
|
+
Модель:
|
|
10
|
+
|
|
11
|
+
- access token у пам'яті через `signal`, не в `localStorage`;
|
|
12
|
+
- HttpOnly refresh cookie відновлює сесію;
|
|
13
|
+
- усі запити проходять через API-клієнт;
|
|
14
|
+
- захищені routes використовують group guard.
|
|
15
|
+
|
|
16
|
+
```ts
|
|
17
|
+
export const requireAuth: Guard = async ({ path }) => {
|
|
18
|
+
if (accessToken()) return;
|
|
19
|
+
if (await restoreSession()) return;
|
|
20
|
+
return { redirect: `/login?return=${encodeURIComponent(path)}`, replace: true };
|
|
21
|
+
};
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Dev proxy:
|
|
25
|
+
|
|
26
|
+
```jsonc
|
|
27
|
+
{
|
|
28
|
+
"dev": {
|
|
29
|
+
"proxy": { "/api": "http://localhost:3000" }
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Якщо backend має іншу auth-схему, змінюй `api.ts`/`auth.ts`, а не сторінки.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# Deployment
|
|
2
|
+
|
|
3
|
+
Одна команда, один artifact:
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
mado release
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
Результат:
|
|
10
|
+
|
|
11
|
+
```txt
|
|
12
|
+
out/
|
|
13
|
+
├── index.html
|
|
14
|
+
├── assets/
|
|
15
|
+
├── baked/
|
|
16
|
+
├── _redirects
|
|
17
|
+
└── _headers
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
`out/` можна деплоїти на nginx, Cloudflare Pages, Netlify, S3/CloudFront або
|
|
21
|
+
GitHub Pages.
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
mado release
|
|
25
|
+
mado preview
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
`mado preview` показує `out/` як статичний хост: baked HTML має пріоритет,
|
|
29
|
+
потім SPA fallback.
|
|
30
|
+
|
|
31
|
+
Для VPS + nginx:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
mado release
|
|
35
|
+
rsync -avz --delete out/ user@server:/var/www/myapp/
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Наданий `nginx.conf` налаштовує immutable cache для hash bundles, no-cache для
|
|
39
|
+
HTML та fallback для deep links.
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Тестування
|
|
2
|
+
|
|
3
|
+
Mado — це TypeScript і browser APIs. У репозиторії фреймворка тести йдуть через
|
|
4
|
+
Node test runner та `linkedom`.
|
|
5
|
+
|
|
6
|
+
```bash
|
|
7
|
+
npm run typecheck
|
|
8
|
+
npm run build
|
|
9
|
+
npm test
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
DOM-тест:
|
|
13
|
+
|
|
14
|
+
```js
|
|
15
|
+
import test from "node:test";
|
|
16
|
+
import assert from "node:assert/strict";
|
|
17
|
+
|
|
18
|
+
const { parseHTML } = await import("linkedom");
|
|
19
|
+
const { window } = parseHTML("<!doctype html><html><body></body></html>");
|
|
20
|
+
globalThis.window = window;
|
|
21
|
+
globalThis.document = window.document;
|
|
22
|
+
|
|
23
|
+
const { html, render } = await import("../dist/src/html.js");
|
|
24
|
+
|
|
25
|
+
test("renders", () => {
|
|
26
|
+
const root = document.createElement("div");
|
|
27
|
+
render(html`<p>${"hello"}</p>`, root);
|
|
28
|
+
assert.equal(root.querySelector("p").textContent, "hello");
|
|
29
|
+
});
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Покривай signals/computed/effect, HTML bindings, guards/redirects, scroll/focus,
|
|
33
|
+
error boundaries, forms, resources/mutations і CLI-команди `mado release`,
|
|
34
|
+
`mado bake`, `mado preview`.
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Обробка помилок
|
|
2
|
+
|
|
3
|
+
Обробляй помилки там, де користувач може відновитися: маршрути, дані, дії
|
|
4
|
+
користувача.
|
|
5
|
+
|
|
6
|
+
```ts
|
|
7
|
+
export default routes(manifest, {
|
|
8
|
+
errorPage: (err) => html`
|
|
9
|
+
<main>
|
|
10
|
+
<h1>Щось пішло не так</h1>
|
|
11
|
+
<pre>${err.message}</pre>
|
|
12
|
+
<a data-link href="/">На головну</a>
|
|
13
|
+
</main>
|
|
14
|
+
`,
|
|
15
|
+
});
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
`page({ errorView })` має пріоритет над глобальною boundary.
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
const users = resource(() => "/api/users", jsonFetcher<User[]>());
|
|
22
|
+
|
|
23
|
+
html`
|
|
24
|
+
${() => users.error()
|
|
25
|
+
? html`<p role="alert">${users.error()!.message}</p>
|
|
26
|
+
<button @click=${users.refresh}>Повторити</button>`
|
|
27
|
+
: null}
|
|
28
|
+
`;
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Валідація належить `useForm()`, помилки запису — поруч із submit-кнопкою.
|
|
32
|
+
Зовнішні browser subscriptions очищай через `ctx.onDispose()`.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Bake cookbook
|
|
2
|
+
|
|
3
|
+
`mado bake` рендерить вибрані маршрути у статичний HTML. Це для SEO та швидкого
|
|
4
|
+
першого рендеру, не SSR з hydration.
|
|
5
|
+
|
|
6
|
+
```ts
|
|
7
|
+
export default page({
|
|
8
|
+
head: () => ({ title: "Products", description: "Catalog" }),
|
|
9
|
+
view: ({ data }) => html`
|
|
10
|
+
<main>
|
|
11
|
+
<h1>Products</h1>
|
|
12
|
+
${data.products.map((p) => html`<article><h2>${p.name}</h2></article>`)}
|
|
13
|
+
</main>
|
|
14
|
+
`,
|
|
15
|
+
bake: {
|
|
16
|
+
paths: () => [{}],
|
|
17
|
+
data: async () => ({ products: await api.products() }),
|
|
18
|
+
revalidate: 3600,
|
|
19
|
+
},
|
|
20
|
+
});
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
У baked views краще використовувати звичайні масиви (`items.map(...)`).
|
|
24
|
+
Runtime-директиви на кшталт `each()` потрібні браузеру.
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
export const manifest = {
|
|
28
|
+
"/": () => import("./pages/home.js"),
|
|
29
|
+
"/blog/:slug": () => import("./pages/blog-post.js"),
|
|
30
|
+
};
|
|
31
|
+
export default routes(manifest);
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
`mado release` створює deploy artifact у `out/`. Якщо bake повідомляє про
|
|
35
|
+
unsupported value, винеси runtime-only частину в клієнт або заміни її на
|
|
36
|
+
серіалізований HTML.
|
package/docs/uk/README.md
CHANGED
|
@@ -14,3 +14,10 @@
|
|
|
14
14
|
| Типові помилки LLM | [07-llm-pitfalls.md](./07-llm-pitfalls.md) |
|
|
15
15
|
| LLM zero-history тест | [08-llm-zero-history-test.md](./08-llm-zero-history-test.md) |
|
|
16
16
|
| Shadow DOM vs Light DOM | [09-shadow-vs-light-dom.md](./09-shadow-vs-light-dom.md) |
|
|
17
|
+
| Архітектура застосунку | [10-app-architecture.md](./10-app-architecture.md) |
|
|
18
|
+
| Layouts | [11-layouts.md](./11-layouts.md) |
|
|
19
|
+
| Auth та API | [12-auth-and-api.md](./12-auth-and-api.md) |
|
|
20
|
+
| Deployment | [13-deployment.md](./13-deployment.md) |
|
|
21
|
+
| Тестування | [14-testing.md](./14-testing.md) |
|
|
22
|
+
| Обробка помилок | [15-error-handling.md](./15-error-handling.md) |
|
|
23
|
+
| Bake cookbook | [16-bake-cookbook.md](./16-bake-cookbook.md) |
|
package/llms.txt
CHANGED
|
@@ -143,6 +143,13 @@ export default page({
|
|
|
143
143
|
- docs/en/05-why-mado.md — honest comparison with Lit / Solid / Svelte / htmx / Alpine / React
|
|
144
144
|
- docs/en/06-for-backenders.md — mental model in 10 minutes for Go/Rust/.NET/Java developers
|
|
145
145
|
- docs/en/07-llm-pitfalls.md — common mistakes when generating Mado code
|
|
146
|
+
- docs/en/10-app-architecture.md — canonical app file layout, routes, auth/API and release shape
|
|
147
|
+
- docs/en/11-layouts.md — blessed layout recipe
|
|
148
|
+
- docs/en/12-auth-and-api.md — blessed auth/API client recipe
|
|
149
|
+
- docs/en/13-deployment.md — deployment recipes and cache rules
|
|
150
|
+
- docs/en/14-testing.md — testing strategy and commands
|
|
151
|
+
- docs/en/15-error-handling.md — route/data/action error boundaries
|
|
152
|
+
- docs/en/16-bake-cookbook.md — static bake recipes and failure modes
|
|
146
153
|
- examples/basic/ — minimal API tour
|
|
147
154
|
- examples/tickets/ — LLM zero-history CRUD validation
|
|
148
155
|
- examples/showcase/ — flagship CRM pressure app (auth, nested routes, forms, mutations)
|
|
@@ -163,7 +170,8 @@ export default page({
|
|
|
163
170
|
|
|
164
171
|
## Version
|
|
165
172
|
|
|
166
|
-
`0.
|
|
173
|
+
`0.6.0` — pre-1.0 product-surface release. API may still change before 1.0.
|
|
174
|
+
Semver is not guaranteed on minor versions before 1.0.
|
|
167
175
|
|
|
168
176
|
## License
|
|
169
177
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@madojs/mado",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Mado — a small native-web SPA framework with Web Components, signals, tagged-template html, router, resources, and forms. TypeScript-only build, zero runtime dependencies.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -48,7 +48,9 @@
|
|
|
48
48
|
"README.md",
|
|
49
49
|
"LICENSE",
|
|
50
50
|
"CHANGELOG.md",
|
|
51
|
+
"MADO_V1_PLAN.md",
|
|
51
52
|
"ROADMAP.md",
|
|
53
|
+
"TODO.md",
|
|
52
54
|
"AGENTS.md",
|
|
53
55
|
"llms.txt",
|
|
54
56
|
"docs/**/*.md"
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
// Mado configuration loader.
|
|
2
|
+
//
|
|
3
|
+
// Single source of project configuration for `mado dev`, `mado build`,
|
|
4
|
+
// `mado bundle`, `mado bake`, `mado preview`, `mado release`.
|
|
5
|
+
//
|
|
6
|
+
// Lookup order (first hit wins):
|
|
7
|
+
// 1. `mado.config.json` in PROJECT_ROOT (recommended)
|
|
8
|
+
// 2. built-in defaults
|
|
9
|
+
//
|
|
10
|
+
// CLI flags always override file values, file values override defaults.
|
|
11
|
+
//
|
|
12
|
+
// Context detection:
|
|
13
|
+
// - "app" : a user project that depends on `@madojs/mado`
|
|
14
|
+
// - "repo" : the framework repository itself (has src/index.ts and examples/)
|
|
15
|
+
//
|
|
16
|
+
// In app-mode, defaults assume the canonical layout from MADO_V1_PLAN.md:
|
|
17
|
+
// src/routes.ts index.html public/ dist/ out/
|
|
18
|
+
|
|
19
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
20
|
+
import { dirname, join, resolve } from "node:path";
|
|
21
|
+
import { fileURLToPath } from "node:url";
|
|
22
|
+
|
|
23
|
+
const PACKAGE_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @typedef {Object} MadoDevConfig
|
|
27
|
+
* @property {number} [port]
|
|
28
|
+
* @property {Record<string,string>} [proxy] // path → upstream base URL
|
|
29
|
+
*
|
|
30
|
+
* @typedef {Object} MadoBuildConfig
|
|
31
|
+
* @property {string} [out] // deploy artifact dir (default: "out")
|
|
32
|
+
* @property {string} [dist] // tsc output dir (default: "dist")
|
|
33
|
+
* @property {string} [publicDir] // static assets dir (default: "public")
|
|
34
|
+
*
|
|
35
|
+
* @typedef {Object} MadoBakeConfig
|
|
36
|
+
* @property {string} [entry] // routes module (default: "src/routes.ts")
|
|
37
|
+
* @property {string} [template] // SPA shell html (default: "index.html")
|
|
38
|
+
* @property {string} [baseUrl] // canonical/sitemap base
|
|
39
|
+
* @property {string} [outDir] // override (default: build.out + "/baked")
|
|
40
|
+
*
|
|
41
|
+
* @typedef {Object} MadoBundleConfig
|
|
42
|
+
* @property {boolean} [splitting]
|
|
43
|
+
* @property {Array<"gz"|"br">} [compress]
|
|
44
|
+
*
|
|
45
|
+
* @typedef {Object} MadoConfig
|
|
46
|
+
* @property {"app"|"repo"} context
|
|
47
|
+
* @property {string} projectRoot
|
|
48
|
+
* @property {MadoDevConfig} dev
|
|
49
|
+
* @property {MadoBuildConfig} build
|
|
50
|
+
* @property {MadoBakeConfig} bake
|
|
51
|
+
* @property {MadoBundleConfig} bundle
|
|
52
|
+
*/
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Detect whether we are inside the framework repository or inside a user app.
|
|
56
|
+
*
|
|
57
|
+
* Heuristic: the framework repo has both `src/index.ts` and an `examples/`
|
|
58
|
+
* directory at PROJECT_ROOT. Anything else is treated as an app.
|
|
59
|
+
*
|
|
60
|
+
* @param {string} projectRoot
|
|
61
|
+
* @returns {"app"|"repo"}
|
|
62
|
+
*/
|
|
63
|
+
export function detectContext(projectRoot) {
|
|
64
|
+
const looksLikeRepo =
|
|
65
|
+
existsSync(join(projectRoot, "src/index.ts")) &&
|
|
66
|
+
existsSync(join(projectRoot, "examples")) &&
|
|
67
|
+
existsSync(join(projectRoot, "package.json")) &&
|
|
68
|
+
safeReadJson(join(projectRoot, "package.json"))?.name === "@madojs/mado";
|
|
69
|
+
return looksLikeRepo ? "repo" : "app";
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Built-in defaults per context.
|
|
74
|
+
*
|
|
75
|
+
* @param {"app"|"repo"} context
|
|
76
|
+
* @returns {MadoConfig}
|
|
77
|
+
*/
|
|
78
|
+
function defaults(context) {
|
|
79
|
+
if (context === "repo") {
|
|
80
|
+
return {
|
|
81
|
+
context,
|
|
82
|
+
projectRoot: "",
|
|
83
|
+
dev: { port: 5173, proxy: {} },
|
|
84
|
+
build: { out: "out", dist: "dist", publicDir: "public" },
|
|
85
|
+
bake: {
|
|
86
|
+
entry: "examples/basic/routes.ts",
|
|
87
|
+
template: "examples/index.html",
|
|
88
|
+
baseUrl: "https://example.com",
|
|
89
|
+
},
|
|
90
|
+
bundle: { splitting: true, compress: ["gz", "br"] },
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
return {
|
|
94
|
+
context,
|
|
95
|
+
projectRoot: "",
|
|
96
|
+
dev: { port: 5173, proxy: {} },
|
|
97
|
+
build: { out: "out", dist: "dist", publicDir: "public" },
|
|
98
|
+
bake: {
|
|
99
|
+
entry: "src/routes.ts",
|
|
100
|
+
template: "index.html",
|
|
101
|
+
baseUrl: "https://example.com",
|
|
102
|
+
},
|
|
103
|
+
bundle: { splitting: true, compress: ["gz", "br"] },
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Load and merge configuration for the given project root.
|
|
109
|
+
*
|
|
110
|
+
* Precedence (low → high): defaults, mado.config.json, CLI overrides.
|
|
111
|
+
*
|
|
112
|
+
* @param {Object} [opts]
|
|
113
|
+
* @param {string} [opts.projectRoot=process.cwd()]
|
|
114
|
+
* @param {Partial<MadoConfig>} [opts.overrides]
|
|
115
|
+
* @returns {MadoConfig}
|
|
116
|
+
*/
|
|
117
|
+
export function loadConfig(opts = {}) {
|
|
118
|
+
const projectRoot = resolve(opts.projectRoot ?? process.cwd());
|
|
119
|
+
const context = detectContext(projectRoot);
|
|
120
|
+
const base = defaults(context);
|
|
121
|
+
base.projectRoot = projectRoot;
|
|
122
|
+
|
|
123
|
+
const file = join(projectRoot, "mado.config.json");
|
|
124
|
+
const fromFile = existsSync(file) ? safeReadJson(file) ?? {} : {};
|
|
125
|
+
|
|
126
|
+
const merged = deepMerge(base, fromFile);
|
|
127
|
+
if (opts.overrides) deepMerge(merged, opts.overrides);
|
|
128
|
+
|
|
129
|
+
merged.context = context;
|
|
130
|
+
merged.projectRoot = projectRoot;
|
|
131
|
+
return merged;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Resolve a project-relative path against `projectRoot`. Absolute paths are
|
|
136
|
+
* returned unchanged.
|
|
137
|
+
*
|
|
138
|
+
* @param {MadoConfig} cfg
|
|
139
|
+
* @param {string} p
|
|
140
|
+
* @returns {string}
|
|
141
|
+
*/
|
|
142
|
+
export function resolveProjectPath(cfg, p) {
|
|
143
|
+
if (!p) return cfg.projectRoot;
|
|
144
|
+
return resolve(cfg.projectRoot, p);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* @param {MadoConfig} cfg
|
|
149
|
+
* @returns {string}
|
|
150
|
+
*/
|
|
151
|
+
export function getPackageRoot() {
|
|
152
|
+
return PACKAGE_ROOT;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// ---------- helpers ----------
|
|
156
|
+
|
|
157
|
+
function safeReadJson(path) {
|
|
158
|
+
try {
|
|
159
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
160
|
+
} catch (err) {
|
|
161
|
+
console.warn(`[mado] failed to parse ${path}: ${err.message}`);
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Shallow-deep merge: objects are merged recursively, arrays and primitives
|
|
168
|
+
* are replaced. Mutates `target` and returns it.
|
|
169
|
+
*/
|
|
170
|
+
function deepMerge(target, source) {
|
|
171
|
+
if (!source || typeof source !== "object") return target;
|
|
172
|
+
for (const [k, v] of Object.entries(source)) {
|
|
173
|
+
if (v === undefined) continue;
|
|
174
|
+
if (
|
|
175
|
+
v !== null &&
|
|
176
|
+
typeof v === "object" &&
|
|
177
|
+
!Array.isArray(v) &&
|
|
178
|
+
target[k] &&
|
|
179
|
+
typeof target[k] === "object" &&
|
|
180
|
+
!Array.isArray(target[k])
|
|
181
|
+
) {
|
|
182
|
+
deepMerge(target[k], v);
|
|
183
|
+
} else {
|
|
184
|
+
target[k] = v;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return target;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Tiny argv parser shared by CLI subcommands.
|
|
192
|
+
*
|
|
193
|
+
* Recognises `--key=value`, `--key value`, and `--flag` (boolean).
|
|
194
|
+
* Unknown leading positionals are returned in `positional`.
|
|
195
|
+
*
|
|
196
|
+
* @param {string[]} argv
|
|
197
|
+
* @returns {{ flags: Record<string, string|boolean>, positional: string[] }}
|
|
198
|
+
*/
|
|
199
|
+
export function parseFlags(argv) {
|
|
200
|
+
const flags = {};
|
|
201
|
+
const positional = [];
|
|
202
|
+
for (let i = 0; i < argv.length; i++) {
|
|
203
|
+
const a = argv[i];
|
|
204
|
+
if (a === "--") continue;
|
|
205
|
+
if (a.startsWith("--")) {
|
|
206
|
+
const eq = a.indexOf("=");
|
|
207
|
+
if (eq > -1) {
|
|
208
|
+
flags[a.slice(2, eq)] = a.slice(eq + 1);
|
|
209
|
+
} else {
|
|
210
|
+
const name = a.slice(2);
|
|
211
|
+
const next = argv[i + 1];
|
|
212
|
+
if (next !== undefined && !next.startsWith("-")) {
|
|
213
|
+
flags[name] = next;
|
|
214
|
+
i++;
|
|
215
|
+
} else {
|
|
216
|
+
flags[name] = true;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
} else {
|
|
220
|
+
positional.push(a);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
return { flags, positional };
|
|
224
|
+
}
|