@madojs/mado 0.10.1 → 0.11.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.
Files changed (217) hide show
  1. package/AGENTS.md +24 -26
  2. package/CHANGELOG.md +68 -0
  3. package/README.md +18 -45
  4. package/TODO.md +52 -48
  5. package/dist/src/component.d.ts +2 -1
  6. package/dist/src/component.js +5 -2
  7. package/dist/src/component.js.map +1 -1
  8. package/dist/src/each.d.ts +1 -1
  9. package/dist/src/each.js +1 -1
  10. package/dist/src/each.js.map +1 -1
  11. package/dist/src/index.d.ts +11 -6
  12. package/dist/src/index.js +5 -3
  13. package/dist/src/index.js.map +1 -1
  14. package/dist/src/lazy.d.ts +1 -1
  15. package/dist/src/lazy.js +1 -1
  16. package/dist/src/lazy.js.map +1 -1
  17. package/dist/src/page.d.ts +17 -21
  18. package/dist/src/page.js +7 -12
  19. package/dist/src/page.js.map +1 -1
  20. package/dist/src/router/manifest.d.ts +1 -1
  21. package/dist/src/router/manifest.js +21 -13
  22. package/dist/src/router/manifest.js.map +1 -1
  23. package/dist/src/router/match.d.ts +2 -2
  24. package/dist/src/router/match.js +3 -3
  25. package/dist/src/router/match.js.map +1 -1
  26. package/dist/src/router/navigation.js +1 -1
  27. package/dist/src/router/navigation.js.map +1 -1
  28. package/dist/src/vite/index.d.ts +10 -0
  29. package/dist/src/vite/index.js +33 -0
  30. package/dist/src/vite/index.js.map +1 -0
  31. package/docs/en/00-the-mado-way.md +25 -12
  32. package/docs/en/01-routing.md +90 -142
  33. package/docs/en/02-project-layout.md +59 -53
  34. package/docs/en/03-static-bake.md +5 -6
  35. package/docs/en/05-why-mado.md +6 -6
  36. package/docs/en/06-for-backenders.md +18 -22
  37. package/docs/en/08-llm-zero-history-test.md +9 -14
  38. package/docs/en/09-shadow-vs-light-dom.md +28 -36
  39. package/docs/en/10-app-architecture.md +158 -96
  40. package/docs/en/11-layouts.md +22 -24
  41. package/docs/en/12-auth-and-api.md +89 -182
  42. package/docs/en/13-deployment.md +18 -22
  43. package/docs/en/14-testing.md +4 -4
  44. package/docs/en/16-bake-cookbook.md +11 -12
  45. package/docs/en/18-api-freeze-map.md +6 -4
  46. package/docs/en/20-v1-stability.md +1 -1
  47. package/docs/fr/00-the-mado-way.md +55 -90
  48. package/docs/fr/01-routing.md +70 -152
  49. package/docs/fr/02-project-layout.md +61 -42
  50. package/docs/fr/03-static-bake.md +1 -1
  51. package/docs/fr/05-why-mado.md +6 -6
  52. package/docs/fr/06-for-backenders.md +7 -7
  53. package/docs/fr/08-llm-zero-history-test.md +21 -48
  54. package/docs/fr/09-shadow-vs-light-dom.md +43 -162
  55. package/docs/fr/10-app-architecture.md +110 -33
  56. package/docs/fr/11-layouts.md +24 -12
  57. package/docs/fr/12-auth-and-api.md +63 -22
  58. package/docs/fr/13-deployment.md +7 -10
  59. package/docs/fr/14-testing.md +1 -1
  60. package/docs/fr/16-bake-cookbook.md +2 -2
  61. package/docs/fr/18-api-freeze-map.md +1 -1
  62. package/docs/fr/20-v1-stability.md +1 -1
  63. package/docs/recipes/nginx/README.md +13 -0
  64. package/docs/ru/00-the-mado-way.md +53 -75
  65. package/docs/ru/01-routing.md +68 -143
  66. package/docs/ru/02-project-layout.md +61 -41
  67. package/docs/ru/03-static-bake.md +2 -2
  68. package/docs/ru/05-why-mado.md +6 -6
  69. package/docs/ru/06-for-backenders.md +7 -7
  70. package/docs/ru/08-llm-zero-history-test.md +9 -14
  71. package/docs/ru/09-shadow-vs-light-dom.md +43 -178
  72. package/docs/ru/10-app-architecture.md +115 -63
  73. package/docs/ru/11-layouts.md +24 -24
  74. package/docs/ru/12-auth-and-api.md +57 -35
  75. package/docs/ru/13-deployment.md +7 -11
  76. package/docs/ru/14-testing.md +1 -1
  77. package/docs/ru/16-bake-cookbook.md +12 -6
  78. package/docs/ru/18-api-freeze-map.md +5 -3
  79. package/docs/ru/20-v1-stability.md +1 -1
  80. package/docs/uk/00-the-mado-way.md +70 -44
  81. package/docs/uk/01-routing.md +41 -47
  82. package/docs/uk/02-project-layout.md +68 -41
  83. package/docs/uk/03-static-bake.md +1 -2
  84. package/docs/uk/06-for-backenders.md +3 -3
  85. package/docs/uk/08-llm-zero-history-test.md +22 -24
  86. package/docs/uk/09-shadow-vs-light-dom.md +37 -86
  87. package/docs/uk/10-app-architecture.md +72 -31
  88. package/docs/uk/11-layouts.md +25 -12
  89. package/docs/uk/12-auth-and-api.md +58 -22
  90. package/docs/uk/13-deployment.md +4 -3
  91. package/docs/uk/14-testing.md +1 -1
  92. package/docs/uk/18-api-freeze-map.md +1 -1
  93. package/docs/uk/20-v1-stability.md +1 -1
  94. package/llms.txt +14 -15
  95. package/package.json +18 -11
  96. package/scripts/_config.mjs +15 -161
  97. package/scripts/bake.mjs +67 -57
  98. package/scripts/cli/generate.mjs +348 -0
  99. package/scripts/cli/help.mjs +27 -0
  100. package/scripts/cli/index.mjs +79 -0
  101. package/scripts/cli/init.mjs +153 -0
  102. package/scripts/cli/release.mjs +152 -0
  103. package/scripts/cli/run.mjs +96 -0
  104. package/scripts/cli.mjs +2 -621
  105. package/scripts/package-smoke.mjs +4 -1
  106. package/scripts/preview.mjs +13 -37
  107. package/scripts/size-budget.mjs +5 -2
  108. package/scripts/vite.default.mjs +11 -0
  109. package/starters/default/.editorconfig +12 -0
  110. package/starters/default/README.md +74 -0
  111. package/starters/default/eslint.config.mjs +256 -0
  112. package/starters/default/index.html +13 -0
  113. package/starters/default/package.json +30 -0
  114. package/starters/default/public/favicon.svg +4 -0
  115. package/starters/default/src/app.routes.ts +39 -0
  116. package/starters/default/src/layouts/app-shell.layout.ts +35 -0
  117. package/starters/default/src/layouts/auth-shell.layout.ts +17 -0
  118. package/starters/default/src/main.ts +16 -0
  119. package/starters/default/src/modules/auth/_contracts/auth-api.types.ts +17 -0
  120. package/starters/default/src/modules/auth/auth.connector.ts +45 -0
  121. package/starters/default/src/modules/auth/auth.guard.ts +22 -0
  122. package/starters/default/src/modules/auth/auth.public.ts +9 -0
  123. package/starters/default/src/modules/auth/auth.routes.ts +8 -0
  124. package/starters/default/src/modules/auth/auth.service.ts +71 -0
  125. package/starters/default/src/modules/auth/auth.types.ts +15 -0
  126. package/starters/default/src/modules/auth/login.page.ts +62 -0
  127. package/starters/default/src/modules/billing/_contracts/stripe.types.ts +17 -0
  128. package/starters/default/src/modules/billing/api/stripe.connector.ts +71 -0
  129. package/starters/default/src/modules/billing/billing.public.ts +5 -0
  130. package/starters/default/src/modules/billing/billing.routes.ts +9 -0
  131. package/starters/default/src/modules/billing/billing.types.ts +15 -0
  132. package/starters/default/src/modules/billing/components/invoice-status-badge.component.ts +43 -0
  133. package/starters/default/src/modules/billing/data/invoices.resource.ts +35 -0
  134. package/starters/default/src/modules/billing/pages/invoice-detail.page.ts +70 -0
  135. package/starters/default/src/modules/billing/pages/invoices-list.page.ts +73 -0
  136. package/starters/default/src/modules/home/home.page.ts +34 -0
  137. package/starters/default/src/modules/home/not-found.page.ts +11 -0
  138. package/starters/default/src/shared/http/http-client.ts +86 -0
  139. package/starters/default/src/shared/http/http-error.ts +37 -0
  140. package/starters/default/src/shared/http/interceptors.ts +59 -0
  141. package/starters/default/src/shared/lib/format-date.ts +19 -0
  142. package/starters/default/src/shared/styles/content.css +70 -0
  143. package/starters/default/src/shared/styles/reset.css +32 -0
  144. package/starters/default/src/shared/styles/shell.css +57 -0
  145. package/starters/default/src/shared/styles/tokens.css +44 -0
  146. package/starters/default/src/shared/ui/x-button.component.ts +49 -0
  147. package/starters/default/src/shared/ui/x-spinner.component.ts +22 -0
  148. package/starters/default/src/styles.d.ts +1 -0
  149. package/starters/default/src/vite-env.d.ts +1 -0
  150. package/starters/default/tsconfig.json +24 -0
  151. package/starters/default/vite.config.ts +9 -0
  152. package/MADO_V1_PLAN.md +0 -179
  153. package/ROADMAP.md +0 -178
  154. package/dist/src/html.d.ts +0 -18
  155. package/dist/src/html.js +0 -17
  156. package/dist/src/html.js.map +0 -1
  157. package/dist/src/router.d.ts +0 -13
  158. package/dist/src/router.js +0 -13
  159. package/dist/src/router.js.map +0 -1
  160. package/scripts/bundle.mjs +0 -212
  161. package/scripts/llm-zero-history-smoke.mjs +0 -93
  162. package/scripts/new.mjs +0 -80
  163. package/scripts/showcase-regression.mjs +0 -392
  164. package/server/serve.mjs +0 -455
  165. package/starters/admin/README.md +0 -63
  166. package/starters/admin/index.html +0 -28
  167. package/starters/admin/mado.config.json +0 -22
  168. package/starters/admin/package.json +0 -24
  169. package/starters/admin/public/favicon.svg +0 -4
  170. package/starters/admin/src/components/x-button.ts +0 -82
  171. package/starters/admin/src/components/x-input.ts +0 -105
  172. package/starters/admin/src/layouts/app.ts +0 -101
  173. package/starters/admin/src/layouts/auth.ts +0 -41
  174. package/starters/admin/src/lib/api.ts +0 -184
  175. package/starters/admin/src/lib/auth.ts +0 -83
  176. package/starters/admin/src/main.ts +0 -15
  177. package/starters/admin/src/pages/admin/dashboard.ts +0 -48
  178. package/starters/admin/src/pages/admin/order-detail.ts +0 -80
  179. package/starters/admin/src/pages/admin/orders.ts +0 -117
  180. package/starters/admin/src/pages/home.ts +0 -34
  181. package/starters/admin/src/pages/login.ts +0 -70
  182. package/starters/admin/src/pages/not-found.ts +0 -12
  183. package/starters/admin/src/routes.ts +0 -40
  184. package/starters/admin/src/styles/global.ts +0 -86
  185. package/starters/admin/tsconfig.json +0 -15
  186. package/starters/crud/README.md +0 -33
  187. package/starters/crud/index.html +0 -28
  188. package/starters/crud/mado.config.json +0 -20
  189. package/starters/crud/package.json +0 -24
  190. package/starters/crud/src/components/app-shell.ts +0 -56
  191. package/starters/crud/src/components/ticket-detail.ts +0 -33
  192. package/starters/crud/src/components/ticket-form.ts +0 -69
  193. package/starters/crud/src/components/ticket-list.ts +0 -66
  194. package/starters/crud/src/lib/api.ts +0 -76
  195. package/starters/crud/src/main.ts +0 -9
  196. package/starters/crud/src/pages/home.ts +0 -34
  197. package/starters/crud/src/pages/not-found.ts +0 -12
  198. package/starters/crud/src/pages/ticket-detail.ts +0 -7
  199. package/starters/crud/src/pages/ticket-new.ts +0 -7
  200. package/starters/crud/src/pages/tickets.ts +0 -7
  201. package/starters/crud/src/routes.ts +0 -11
  202. package/starters/crud/src/styles/global.ts +0 -155
  203. package/starters/crud/tsconfig.json +0 -15
  204. package/starters/minimal/README.md +0 -21
  205. package/starters/minimal/index.html +0 -28
  206. package/starters/minimal/mado.config.json +0 -20
  207. package/starters/minimal/package.json +0 -24
  208. package/starters/minimal/src/components/app-counter.ts +0 -31
  209. package/starters/minimal/src/main.ts +0 -9
  210. package/starters/minimal/src/pages/home.ts +0 -35
  211. package/starters/minimal/src/pages/not-found.ts +0 -14
  212. package/starters/minimal/src/routes.ts +0 -8
  213. package/starters/minimal/src/styles/global.ts +0 -60
  214. package/starters/minimal/tsconfig.json +0 -15
  215. package/templates/page-detail.ts +0 -63
  216. package/templates/page-form.ts +0 -94
  217. package/templates/page-list.ts +0 -79
@@ -0,0 +1,348 @@
1
+ import { access, mkdir, writeFile } from "node:fs/promises";
2
+ import { dirname, join, relative } from "node:path";
3
+
4
+ const KINDS = [
5
+ "module",
6
+ "page",
7
+ "connector",
8
+ "resource",
9
+ "service",
10
+ "form",
11
+ "component",
12
+ "guard",
13
+ "layout",
14
+ ];
15
+
16
+ export async function runNew(ctx, args) {
17
+ const [kind, target] = args;
18
+ if (!kind || !target) {
19
+ printUsage();
20
+ process.exit(1);
21
+ }
22
+
23
+ const generators = {
24
+ module: scaffoldModule,
25
+ page: scaffoldPage,
26
+ connector: scaffoldConnector,
27
+ resource: scaffoldResource,
28
+ service: scaffoldService,
29
+ form: scaffoldForm,
30
+ component: scaffoldComponent,
31
+ guard: scaffoldGuard,
32
+ layout: scaffoldLayout,
33
+ };
34
+
35
+ const fn = generators[kind];
36
+ if (!fn) {
37
+ console.error(`[mado] unknown generator: ${kind}`);
38
+ console.error(`[mado] available generators: ${KINDS.join(", ")}`);
39
+ process.exit(1);
40
+ }
41
+
42
+ await fn(ctx, normalizeTarget(target));
43
+ }
44
+
45
+ function printUsage() {
46
+ console.error("usage: mado new <module|page|connector|resource|service|form|component|guard|layout> <path>");
47
+ console.error("");
48
+ console.error("examples:");
49
+ console.error(" mado new module billing");
50
+ console.error(" mado new page billing/pages/invoices-list");
51
+ console.error(" mado new connector billing/api/stripe");
52
+ console.error(" mado new resource billing/data/invoices");
53
+ console.error(" mado new service billing/cart");
54
+ console.error(" mado new form billing/invoice");
55
+ console.error(" mado new component billing/components/invoice-status-badge");
56
+ console.error(" mado new guard billing/billing");
57
+ console.error(" mado new layout app-shell");
58
+ }
59
+
60
+ async function scaffoldModule(ctx, name) {
61
+ assertSingleSegment("Module", name);
62
+ const dir = join(srcDir(ctx), "modules", name);
63
+ const camel = kebabToCamel(name);
64
+ const Pascal = kebabToPascal(name);
65
+
66
+ await writeOnce(
67
+ join(dir, `${name}.types.ts`),
68
+ `// Domain types of the ${name} module. Public via ${name}.public.ts.\n\nexport interface ${Pascal} {\n id: string;\n}\n`,
69
+ );
70
+ await writeOnce(
71
+ join(dir, `${name}.routes.ts`),
72
+ `// Path prefix is applied in src/app.routes.ts.\n\nexport const ${camel}Routes = {\n // "/": () => import("./${name}.page"),\n};\n`,
73
+ );
74
+ await writeOnce(
75
+ join(dir, `${name}.public.ts`),
76
+ `// Public surface of the ${name} module.\n// Anything not re-exported here is private to the module.\n\nexport type { ${Pascal} } from "./${name}.types";\n`,
77
+ );
78
+
79
+ console.log(
80
+ `\nNext step: wire the module in src/app.routes.ts:\n\n` +
81
+ ` import { ${camel}Routes } from "./modules/${name}/${name}.routes";\n\n` +
82
+ ` "/${name}": layout({\n` +
83
+ ` layout: () => import("./layouts/app-shell.layout"),\n` +
84
+ ` routes: ${camel}Routes,\n` +
85
+ ` }),\n`,
86
+ );
87
+ }
88
+
89
+ async function scaffoldPage(ctx, target) {
90
+ const file = moduleFile(ctx, target, ".page.ts");
91
+ const name = leafName(target);
92
+ await writeOnce(
93
+ file,
94
+ `import { html, page } from "@madojs/mado";
95
+
96
+ export default page({
97
+ title: "${kebabToPascal(name)}",
98
+ view: () => {
99
+ // 1. LOCAL STATE
100
+ // 2. DATA
101
+ // 3. ACTIONS
102
+
103
+ // 4. VIEW
104
+ return html\`
105
+ <section>
106
+ <h1>${kebabToPascal(name)}</h1>
107
+ </section>
108
+ \`;
109
+ },
110
+ });
111
+ `,
112
+ );
113
+ }
114
+
115
+ async function scaffoldConnector(ctx, target) {
116
+ const file = moduleFile(ctx, target, ".connector.ts");
117
+ const name = leafName(target);
118
+ const api = `${kebabToCamel(name)}Api`;
119
+ const err = `${kebabToPascal(name)}Error`;
120
+ const up = relative(dirname(file), join(srcDir(ctx), "shared")).split("\\").join("/");
121
+
122
+ await writeOnce(
123
+ file,
124
+ `// ${kebabToPascal(name)} connector. One file per external API system.
125
+ // Shape: CONFIG -> MAPPERS -> ENDPOINTS -> ERRORS.
126
+ // Never import signals, resources, html, components, pages, or services here.
127
+
128
+ import { httpClient } from "${up}/http/http-client";
129
+ import { HttpError } from "${up}/http/http-error";
130
+
131
+ // 1. CONFIG
132
+ const base = "/api/${name}";
133
+
134
+ // 2. MAPPERS
135
+ // Map provider DTOs from _contracts/ to domain types from <module>.types.ts.
136
+
137
+ // 3. ENDPOINTS
138
+ export const ${api} = {
139
+ list: () => httpClient.get<unknown[]>(base),
140
+ };
141
+
142
+ // 4. ERRORS
143
+ export class ${err} extends HttpError {
144
+ override readonly name = "${err}";
145
+ }
146
+ `,
147
+ );
148
+ }
149
+
150
+ async function scaffoldResource(ctx, target) {
151
+ const file = moduleFile(ctx, target, ".resource.ts");
152
+ const name = leafName(target);
153
+ const Pascal = kebabToPascal(name);
154
+ await writeOnce(
155
+ file,
156
+ `import { mutation, resource } from "@madojs/mado";
157
+
158
+ // Resource keys are URL-shaped so mutation invalidation stays predictable.
159
+ const key = "/api/${name}";
160
+
161
+ export const use${Pascal} = () =>
162
+ resource(
163
+ () => key,
164
+ async () => {
165
+ throw new Error("TODO: connect use${Pascal}() to a module connector");
166
+ },
167
+ { staleTime: 30_000 },
168
+ );
169
+
170
+ export const save${Pascal} = mutation(
171
+ async (_input: unknown) => {
172
+ throw new Error("TODO: connect save${Pascal}() to a module connector");
173
+ },
174
+ { invalidates: ["/api/${name}*"] },
175
+ );
176
+ `,
177
+ );
178
+ }
179
+
180
+ async function scaffoldService(ctx, target) {
181
+ const file = moduleFile(ctx, target, ".service.ts");
182
+ const name = leafName(target);
183
+ const camel = kebabToCamel(name);
184
+ const Pascal = kebabToPascal(name);
185
+ await writeOnce(
186
+ file,
187
+ `import { computed, signal } from "@madojs/mado";
188
+
189
+ export interface ${Pascal} {
190
+ id: string;
191
+ }
192
+
193
+ // 1. PRIVATE STATE
194
+ const _${camel} = signal<${Pascal} | null>(null);
195
+
196
+ // 2. PUBLIC READS
197
+ export const ${camel} = (): ${Pascal} | null => _${camel}();
198
+ export const has${Pascal} = computed(() => _${camel}() !== null);
199
+
200
+ // 3. ACTIONS
201
+ export function set${Pascal}(next: ${Pascal} | null): void {
202
+ _${camel}.set(next);
203
+ }
204
+
205
+ // 4. INIT
206
+ export function init${Pascal}(): void {}
207
+ `,
208
+ );
209
+ }
210
+
211
+ async function scaffoldForm(ctx, target) {
212
+ const file = moduleFile(ctx, target, ".form.ts");
213
+ const name = leafName(target);
214
+ const useName = `use${kebabToPascal(name)}Form`;
215
+ await writeOnce(
216
+ file,
217
+ `import { useForm } from "@madojs/mado";
218
+
219
+ // Call inside a page view: const form = ${useName}();
220
+ export const ${useName} = () =>
221
+ useForm({
222
+ // name: { required: true },
223
+ });
224
+ `,
225
+ );
226
+ }
227
+
228
+ async function scaffoldComponent(ctx, target) {
229
+ const file = moduleFile(ctx, target, ".component.ts");
230
+ const name = leafName(target);
231
+ const tag = name.includes("-") ? name : `x-${name}`;
232
+ await writeOnce(
233
+ file,
234
+ `import { component, css, html } from "@madojs/mado";
235
+
236
+ component(
237
+ "${tag}",
238
+ () => () => html\`<span><slot></slot></span>\`,
239
+ {
240
+ styles: css\`
241
+ :host {
242
+ display: inline-block;
243
+ }
244
+ \`,
245
+ },
246
+ );
247
+ `,
248
+ );
249
+ }
250
+
251
+ async function scaffoldGuard(ctx, target) {
252
+ const file = moduleFile(ctx, target, ".guard.ts");
253
+ const name = leafName(target);
254
+ const Pascal = kebabToPascal(name);
255
+ await writeOnce(
256
+ file,
257
+ `// Return true to allow, a path string to redirect, or false to deny.
258
+
259
+ export function require${Pascal}(): boolean | string {
260
+ return true;
261
+ }
262
+ `,
263
+ );
264
+ }
265
+
266
+ async function scaffoldLayout(ctx, name) {
267
+ assertSingleSegment("Layout", name);
268
+ const file = join(srcDir(ctx), "layouts", `${name}.layout.ts`);
269
+ await writeOnce(
270
+ file,
271
+ `import { html, page } from "@madojs/mado";
272
+
273
+ export default page({
274
+ title: "${kebabToPascal(name)}",
275
+ view: ({ child }) => html\`
276
+ <div class="layout layout--${name}">
277
+ <main>\${child}</main>
278
+ </div>
279
+ \`,
280
+ });
281
+ `,
282
+ );
283
+
284
+ console.log(
285
+ `\nNext step: wrap routes with this layout in src/app.routes.ts:\n\n` +
286
+ ` "/<prefix>": layout({\n` +
287
+ ` layout: () => import("./layouts/${name}.layout"),\n` +
288
+ ` routes: <moduleRoutes>,\n` +
289
+ ` }),\n`,
290
+ );
291
+ }
292
+
293
+ function srcDir(ctx) {
294
+ return join(ctx.projectRoot, "src");
295
+ }
296
+
297
+ function moduleFile(ctx, target, suffix) {
298
+ return join(srcDir(ctx), "modules", `${target}${suffix}`);
299
+ }
300
+
301
+ async function writeOnce(path, content) {
302
+ if (await exists(path)) {
303
+ console.error(`[mado] file already exists: ${path}`);
304
+ process.exit(2);
305
+ }
306
+ await mkdir(dirname(path), { recursive: true });
307
+ await writeFile(path, content, "utf8");
308
+ console.log(`created ${path}`);
309
+ }
310
+
311
+ async function exists(path) {
312
+ try {
313
+ await access(path);
314
+ return true;
315
+ } catch {
316
+ return false;
317
+ }
318
+ }
319
+
320
+ function normalizeTarget(target) {
321
+ return target
322
+ .split("\\").join("/")
323
+ .replace(/^src\/modules\//, "")
324
+ .replace(/^modules\//, "")
325
+ .replace(/^src\/layouts\//, "")
326
+ .replace(/^layouts\//, "")
327
+ .replace(/^\/+|\/+$/g, "");
328
+ }
329
+
330
+ function leafName(target) {
331
+ return target.split("/").filter(Boolean).at(-1) ?? target;
332
+ }
333
+
334
+ function assertSingleSegment(label, name) {
335
+ if (name.includes("/")) {
336
+ console.error(`[mado] ${label.toLowerCase()} name must be a single path segment`);
337
+ process.exit(1);
338
+ }
339
+ }
340
+
341
+ function kebabToCamel(s) {
342
+ return s.replace(/-([a-z0-9])/g, (_, c) => c.toUpperCase());
343
+ }
344
+
345
+ function kebabToPascal(s) {
346
+ const c = kebabToCamel(s);
347
+ return c.charAt(0).toUpperCase() + c.slice(1);
348
+ }
@@ -0,0 +1,27 @@
1
+ export function printHelp(ctx) {
2
+ const mode = ctx.isRepo ? "repo-mode (framework repository)" : "app-mode";
3
+ console.log(`mado commands (${mode}):
4
+
5
+ Project lifecycle:
6
+ mado init <name> [--starter default] [--force]
7
+ scaffold a new app
8
+ mado dev Vite dev server
9
+ mado build tsc package compile (writes internal dist/)
10
+ mado typecheck tsc --noEmit
11
+ mado test run unit tests
12
+
13
+ Production:
14
+ mado bake [--entry <file>] [--template <html>] [--out <dir>] [--base-url <url>]
15
+ prerender baked routes -> out/
16
+ mado release typecheck + vite build + bake -> out/
17
+ mado preview serve exactly out/ locally
18
+
19
+ Generators:
20
+ mado new <module|page|connector|resource|service|form|component|guard|layout> <path>
21
+
22
+ Configuration:
23
+ Put app/dev/build settings in vite.config.ts.
24
+ Mado CLI flags are explicit per command.
25
+
26
+ Docs: README.md and docs/en/README.md.`);
27
+ }
@@ -0,0 +1,79 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { dirname, join, resolve } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+
5
+ import { detectContext } from "../_config.mjs";
6
+ import { runInit } from "./init.mjs";
7
+ import { runNew } from "./generate.mjs";
8
+ import { printHelp } from "./help.mjs";
9
+ import { runRelease } from "./release.mjs";
10
+ import { hasFlag, listTestFiles, run, runNodeBin, runNodeScript, runVite } from "./run.mjs";
11
+
12
+ const PACKAGE_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "../..");
13
+
14
+ export async function main(argv) {
15
+ const projectRoot = resolve(process.cwd());
16
+ const context = detectContext(projectRoot);
17
+ const packageJson = JSON.parse(readFileSync(join(PACKAGE_ROOT, "package.json"), "utf8"));
18
+ const ctx = {
19
+ packageRoot: PACKAGE_ROOT,
20
+ projectRoot,
21
+ packageJson,
22
+ context,
23
+ isRepo: context === "repo",
24
+ };
25
+
26
+ const [rawCommand, ...args] = argv;
27
+ const command = rawCommand ?? "help";
28
+
29
+ switch (command) {
30
+ case "init":
31
+ await runInit(ctx, args);
32
+ break;
33
+ case "build":
34
+ await runNodeBin(ctx, "typescript/bin/tsc", args);
35
+ break;
36
+ case "watch":
37
+ await runNodeBin(ctx, "typescript/bin/tsc", ["-w", ...args]);
38
+ break;
39
+ case "typecheck":
40
+ await runNodeBin(ctx, "typescript/bin/tsc", ["--noEmit", ...args]);
41
+ break;
42
+ case "test": {
43
+ await runNodeBin(ctx, "typescript/bin/tsc", []);
44
+ const files = await listTestFiles(projectRoot);
45
+ await run(process.execPath, ["--test", "--test-timeout=30000", ...files, ...args], {
46
+ cwd: projectRoot,
47
+ });
48
+ break;
49
+ }
50
+ case "dev":
51
+ await runVite(
52
+ ctx,
53
+ [...(hasFlag(args, "--host") ? [] : ["--host", "localhost"]), ...args],
54
+ { defaultConfig: true },
55
+ );
56
+ break;
57
+ case "bake":
58
+ await runNodeScript(ctx, "scripts/bake.mjs", args);
59
+ break;
60
+ case "preview":
61
+ await runNodeScript(ctx, "scripts/preview.mjs", args);
62
+ break;
63
+ case "release":
64
+ await runRelease(ctx, args);
65
+ break;
66
+ case "new":
67
+ await runNew(ctx, args);
68
+ break;
69
+ case "help":
70
+ case "--help":
71
+ case "-h":
72
+ printHelp(ctx);
73
+ break;
74
+ default:
75
+ console.error(`[mado] unknown command: ${command}`);
76
+ printHelp(ctx);
77
+ process.exit(1);
78
+ }
79
+ }
@@ -0,0 +1,153 @@
1
+ import { existsSync, readdirSync, statSync } from "node:fs";
2
+ import { copyFile, cp, mkdir, readFile, writeFile } from "node:fs/promises";
3
+ import { join, resolve } from "node:path";
4
+
5
+ import { parseFlags } from "../_config.mjs";
6
+
7
+ const STARTERS = ["default"];
8
+
9
+ export async function runInit(ctx, rawArgs) {
10
+ const { flags, positional } = parseFlags(rawArgs);
11
+ const targetArg = positional[0];
12
+ if (!targetArg) {
13
+ console.error("[mado] usage: mado init <name> [--starter default] [--force]");
14
+ process.exit(1);
15
+ }
16
+
17
+ const starter = String(flags.starter ?? "default");
18
+ if (!STARTERS.includes(starter)) {
19
+ console.error(`[mado] unknown starter: ${starter}`);
20
+ console.error(`[mado] available starters: ${STARTERS.join(", ")}`);
21
+ process.exit(1);
22
+ }
23
+
24
+ const target = resolve(ctx.projectRoot, targetArg);
25
+ const source = join(ctx.packageRoot, "starters", starter);
26
+ if (!existsSync(source)) {
27
+ console.error(`[mado] missing starter template: ${starter}`);
28
+ process.exit(1);
29
+ }
30
+ if (existsSync(target) && statSync(target).isFile()) {
31
+ console.error(`[mado] target exists and is a file: ${target}`);
32
+ process.exit(1);
33
+ }
34
+ if (existsSync(target) && readdirSync(target).length > 0 && !flags.force) {
35
+ console.error(`[mado] target directory is not empty: ${target}`);
36
+ console.error("[mado] use --force to write into it");
37
+ process.exit(1);
38
+ }
39
+
40
+ await mkdir(target, { recursive: true });
41
+ await cp(source, target, { recursive: true, force: true });
42
+ await copyCanonicalLLMFiles(ctx, target);
43
+ await ensureStarterGitignore(target);
44
+ await ensureStarterPackageJson(ctx, target);
45
+
46
+ const packageName = packageNameFromDir(target);
47
+ if (!isValidPackageName(packageName)) {
48
+ console.error(`[mado] invalid package name derived from target: ${packageName}`);
49
+ process.exit(1);
50
+ }
51
+
52
+ const replacements = {
53
+ __APP_NAME__: packageName,
54
+ __PACKAGE_NAME__: packageName,
55
+ __MADOJS_VERSION__:
56
+ process.env.MADO_PACKAGE_SPEC ||
57
+ process.env.MADOJS_PACKAGE_SPEC ||
58
+ `^${ctx.packageJson.version}`,
59
+ __MADO_VERSION__: ctx.packageJson.version,
60
+ };
61
+
62
+ for (const file of await walkFiles(target)) {
63
+ const text = await readFile(file, "utf8").catch(() => null);
64
+ if (text === null) continue;
65
+ let next = text;
66
+ for (const [key, value] of Object.entries(replacements)) {
67
+ next = next.split(key).join(value);
68
+ }
69
+ if (next !== text) await writeFile(file, next);
70
+ }
71
+
72
+ console.log("");
73
+ console.log(`Created ${packageName} with the ${starter} starter.`);
74
+ console.log("");
75
+ console.log("Next steps:");
76
+ console.log(` cd ${relativePath(ctx.projectRoot, target)}`);
77
+ console.log(" npm install");
78
+ console.log(" npm run dev");
79
+ console.log("");
80
+ }
81
+
82
+ async function copyCanonicalLLMFiles(ctx, target) {
83
+ for (const file of ["llms.txt"]) {
84
+ const source = join(ctx.packageRoot, file);
85
+ const dest = join(target, file);
86
+ if (existsSync(source) && !existsSync(dest)) await copyFile(source, dest);
87
+ }
88
+ }
89
+
90
+ async function ensureStarterGitignore(target) {
91
+ const file = join(target, ".gitignore");
92
+ if (existsSync(file)) return;
93
+ await writeFile(file, "node_modules\nout\n.DS_Store\n*.log\n");
94
+ }
95
+
96
+ async function ensureStarterPackageJson(ctx, target) {
97
+ const file = join(target, "package.json");
98
+ if (!existsSync(file)) return;
99
+
100
+ const pkg = JSON.parse(await readFile(file, "utf8"));
101
+ pkg.dependencies = {
102
+ ...(pkg.dependencies ?? {}),
103
+ "@madojs/mado":
104
+ process.env.MADO_PACKAGE_SPEC ||
105
+ process.env.MADOJS_PACKAGE_SPEC ||
106
+ `^${ctx.packageJson.version}`,
107
+ };
108
+ const rootDev = ctx.packageJson.devDependencies ?? {};
109
+ pkg.devDependencies = {
110
+ ...(pkg.devDependencies ?? {}),
111
+ typescript: rootDev.typescript ?? "^6.0.3",
112
+ linkedom: rootDev.linkedom ?? "^0.18.12",
113
+ lightningcss: rootDev.lightningcss ?? "^1.32.0",
114
+ vite: rootDev.vite ?? "^8.0.16",
115
+ };
116
+
117
+ await writeFile(file, `${JSON.stringify(pkg, null, 2)}\n`);
118
+ }
119
+
120
+ function packageNameFromDir(target) {
121
+ return target
122
+ .split(/[\\/]/)
123
+ .filter(Boolean)
124
+ .at(-1)
125
+ ?.toLowerCase()
126
+ .replace(/[^a-z0-9._-]+/g, "-")
127
+ .replace(/^-+|-+$/g, "") || "mado-app";
128
+ }
129
+
130
+ function isValidPackageName(name) {
131
+ return /^(?:@[a-z0-9._-]+\/)?[a-z0-9][a-z0-9._-]*$/.test(name)
132
+ && !name.includes("..")
133
+ && !name.startsWith(".")
134
+ && name.length <= 214;
135
+ }
136
+
137
+ async function walkFiles(dir) {
138
+ const out = [];
139
+ for (const entry of readdirSync(dir)) {
140
+ if (["node_modules", "dist", ".git"].includes(entry)) continue;
141
+ if (entry === "package-lock.json") continue;
142
+ const file = join(dir, entry);
143
+ const stat = statSync(file);
144
+ if (stat.isDirectory()) out.push(...await walkFiles(file));
145
+ else out.push(file);
146
+ }
147
+ return out;
148
+ }
149
+
150
+ function relativePath(from, to) {
151
+ const rel = to.startsWith(from) ? to.slice(from.length).replace(/^[/\\]/, "") : to;
152
+ return rel || ".";
153
+ }