flet-box 1.1.2 → 1.1.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.
Files changed (154) hide show
  1. package/AGENTS_FOR_CODE_GEN.md +284 -0
  2. package/CONTRIBUTING.md +238 -0
  3. package/README.md +2 -2
  4. package/bin/cli.js +88 -1
  5. package/bin/commands/auditFix.js +181 -27
  6. package/bin/commands/checkApi.js +14 -3
  7. package/bin/commands/checkStructure.js +368 -0
  8. package/bin/commands/checkSyntax.js +293 -0
  9. package/bin/commands/checkUi.js +666 -14
  10. package/bin/commands/create.js +14 -0
  11. package/bin/commands/createBundle.js +2 -2
  12. package/bin/commands/metrics.js +448 -0
  13. package/bin/commands/release.js +18 -0
  14. package/bin/commands/report.js +849 -0
  15. package/bin/commands/tools.js +76 -0
  16. package/bin/commands/unpack.js +39 -11
  17. package/bin/commands/verify.js +879 -0
  18. package/bin/mcp.js +280 -0
  19. package/bin/utils/contract.js +27 -0
  20. package/bin/utils/dtsProps.js +5 -1
  21. package/bin/utils/migrationGuide.js +146 -0
  22. package/bin/utils/moduleExports.js +9 -5
  23. package/bin/utils/templates.js +225 -18
  24. package/bin/utils/toolkit.js +191 -0
  25. package/docs/README.md +5 -1
  26. package/docs/arquitectura.md +1 -1
  27. package/docs/cli/README.md +168 -2
  28. package/docs/guides/README.md +6 -0
  29. package/docs/guides/accessibility.md +2 -1
  30. package/docs/guides/ai-toolkit.md +166 -0
  31. package/docs/guides/compatibility.md +1 -1
  32. package/docs/guides/migration.md +92 -0
  33. package/docs/guides/minification-and-protection.md +2 -1
  34. package/docs/guides/mobile-and-platforms.md +1 -1
  35. package/docs/guides/online-updates.md +2 -0
  36. package/docs/server/deployment.md +1 -1
  37. package/docs/tools/styling.md +2 -2
  38. package/docs/tools/theme.md +139 -4
  39. package/docs/widget/Accordion.md +74 -8
  40. package/docs/widget/AdMob.md +68 -3
  41. package/docs/widget/AdSense.md +67 -2
  42. package/docs/widget/AlertDialog.md +1 -1
  43. package/docs/widget/AnimatedBox.md +80 -39
  44. package/docs/widget/AnimatedText.md +74 -18
  45. package/docs/widget/Audio.md +73 -14
  46. package/docs/widget/Avatar.md +69 -4
  47. package/docs/widget/Badge.md +68 -3
  48. package/docs/widget/BottomSheet.md +2 -2
  49. package/docs/widget/Button.md +69 -3
  50. package/docs/widget/Card.md +69 -4
  51. package/docs/widget/Carousel.md +67 -2
  52. package/docs/widget/Chart.md +67 -2
  53. package/docs/widget/Checkbox.md +74 -8
  54. package/docs/widget/Chip.md +68 -3
  55. package/docs/widget/CircularBar.md +74 -15
  56. package/docs/widget/CircularChart.md +71 -10
  57. package/docs/widget/CodeViewer.md +67 -2
  58. package/docs/widget/Column.md +68 -3
  59. package/docs/widget/Container.md +68 -3
  60. package/docs/widget/DataTable.md +67 -2
  61. package/docs/widget/Divider.md +67 -2
  62. package/docs/widget/DraggBox.md +67 -2
  63. package/docs/widget/Dropdown.md +76 -10
  64. package/docs/widget/DroppBox.md +67 -2
  65. package/docs/widget/FloatingActionButton.md +68 -3
  66. package/docs/widget/GridView.md +67 -2
  67. package/docs/widget/Icon.md +71 -5
  68. package/docs/widget/Image.md +68 -3
  69. package/docs/widget/Input.md +68 -3
  70. package/docs/widget/Inspector.md +2 -2
  71. package/docs/widget/InstallButton.md +73 -13
  72. package/docs/widget/ListTile.md +69 -3
  73. package/docs/widget/ListView.md +67 -2
  74. package/docs/widget/Maps.md +68 -3
  75. package/docs/widget/Markdown.md +74 -15
  76. package/docs/widget/MatrixRain.md +72 -11
  77. package/docs/widget/Menu.md +209 -0
  78. package/docs/widget/Modal.md +3 -3
  79. package/docs/widget/Pagination.md +68 -3
  80. package/docs/widget/ParallaxBox.md +72 -12
  81. package/docs/widget/ProgressBar.md +69 -4
  82. package/docs/widget/QRCode.md +67 -2
  83. package/docs/widget/README.md +54 -50
  84. package/docs/widget/Radio.md +76 -10
  85. package/docs/widget/Rating.md +70 -6
  86. package/docs/widget/Row.md +68 -3
  87. package/docs/widget/Skeleton.md +68 -3
  88. package/docs/widget/Slider.md +75 -9
  89. package/docs/widget/SnackBar.md +2 -2
  90. package/docs/widget/Stack.md +68 -3
  91. package/docs/widget/Stepper.md +68 -3
  92. package/docs/widget/Switch.md +74 -8
  93. package/docs/widget/Text.md +71 -4
  94. package/docs/widget/ThemePicker.md +197 -0
  95. package/docs/widget/Tooltip.md +72 -7
  96. package/docs/widget/TreeView.md +70 -10
  97. package/docs/widget/Video.md +71 -10
  98. package/package.json +10 -3
  99. package/skills/fletbox-design/SKILL.md +89 -5
  100. package/skills/fletbox-motion/SKILL.md +1 -1
  101. package/skills/fletbox-rules/SKILL.md +56 -4
  102. package/src/core/runApp.js +12 -0
  103. package/src/index.d.ts +374 -101
  104. package/src/index.js +9 -0
  105. package/src/navigations/AdaptiveScaffold.js +2 -2
  106. package/src/navigations/BottomNavigation.js +1 -1
  107. package/src/navigations/Scaffold.js +18 -6
  108. package/src/tools/useState.js +11 -4
  109. package/src/utils/colors.js +275 -0
  110. package/src/utils/errorBoundary.js +288 -0
  111. package/src/utils/index.js +10 -2
  112. package/src/utils/stackPosition.js +39 -24
  113. package/src/utils/syntaxHighlight.js +4 -1
  114. package/src/utils/themes.js +279 -3
  115. package/src/widget-factory/addChildren.js +17 -10
  116. package/src/widget-factory/assignProps.js +18 -6
  117. package/src/widget-factory/createWidget.js +7 -0
  118. package/src/widget-factory/deprecations.js +77 -0
  119. package/src/widget-factory/translateProps.js +11 -1
  120. package/src/widget-factory/warnings.js +1 -27
  121. package/src/widget-factory/widgetFactory.js +59 -17
  122. package/src/widgets/Accordion.js +21 -7
  123. package/src/widgets/Badge.js +11 -2
  124. package/src/widgets/Button.js +15 -3
  125. package/src/widgets/Carousel.js +12 -4
  126. package/src/widgets/Chart.js +7 -2
  127. package/src/widgets/Checkbox.js +1 -2
  128. package/src/widgets/Chip.js +24 -10
  129. package/src/widgets/CircularBar.js +9 -3
  130. package/src/widgets/CircularChart.js +5 -3
  131. package/src/widgets/Container.js +6 -0
  132. package/src/widgets/Divider.js +3 -1
  133. package/src/widgets/DraggBox.js +9 -0
  134. package/src/widgets/Dropdown.js +3 -3
  135. package/src/widgets/DroppBox.js +5 -0
  136. package/src/widgets/FloatingActionButton.js +1 -1
  137. package/src/widgets/Icon.js +5 -1
  138. package/src/widgets/InstallButton.js +4 -2
  139. package/src/widgets/Markdown.js +171 -103
  140. package/src/widgets/Menu.js +340 -0
  141. package/src/widgets/Modal.js +7 -1
  142. package/src/widgets/Pagination.js +10 -5
  143. package/src/widgets/ProgressBar.js +5 -4
  144. package/src/widgets/QRCode.js +4 -0
  145. package/src/widgets/Radio.js +1 -2
  146. package/src/widgets/Rating.js +9 -0
  147. package/src/widgets/Slider.js +4 -1
  148. package/src/widgets/SnackBar.js +2 -2
  149. package/src/widgets/Switch.js +1 -2
  150. package/src/widgets/Text.js +9 -5
  151. package/src/widgets/ThemePicker.js +234 -0
  152. package/src/widgets/TreeView.js +2 -1
  153. package/src/widgets/index.js +2 -0
  154. package/docs/CONTRIBUTING.md +0 -156
package/bin/mcp.js ADDED
@@ -0,0 +1,280 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * @file bin/mcp.js
5
+ * @description `flet-box mcp` — the read-only toolkit over the Model Context
6
+ * Protocol, stdio transport.
7
+ *
8
+ * An assistant that can run a shell already has every tool in this package. An
9
+ * assistant that cannot — a chat panel with no terminal, a hosted agent, an
10
+ * editor extension — has none of them, so it guesses about the framework. This
11
+ * server is the same catalogue with a different mouth: the nine read-only
12
+ * entries from `bin/utils/toolkit.js`, each answered by the same function the
13
+ * CLI calls, so the two can never disagree about what the code says.
14
+ *
15
+ * Read-only is not a promise in a comment; it is the shape of the code:
16
+ *
17
+ * • `report` runs `buildReport`, which never writes (the HTML artifact is a
18
+ * CLI flag, not an MCP argument).
19
+ * • the gates run through `runGates`, not `verify` — `verify` appends a
20
+ * metrics row, and a diagnosis must not change what it measures.
21
+ * • `check-api`, `check-ui` and `unpack` are called without `--emit-api`,
22
+ * without `--fix` and without `--all` (the one `unpack` form that writes).
23
+ * • no executor edits, deletes, builds or publishes. `scripts/check-report.mjs`
24
+ * hashes the tree around every tool call to prove it.
25
+ *
26
+ * The transport is newline-delimited JSON-RPC 2.0 (the MCP stdio framing). Logs
27
+ * go to stderr; stdout carries protocol messages only.
28
+ */
29
+
30
+ import fs from "node:fs";
31
+ import path from "node:path";
32
+ import readline from "node:readline";
33
+ import { createRequire } from "node:module";
34
+ import { TOOLS, toolById } from "./utils/toolkit.js";
35
+ import { buildReport, renderText } from "./commands/report.js";
36
+ import { runGates, formatReport } from "./commands/verify.js";
37
+ import { checkApi } from "./commands/checkApi.js";
38
+ import { checkUi } from "./commands/checkUi.js";
39
+ import { metrics } from "./commands/metrics.js";
40
+ import { unpack } from "./commands/unpack.js";
41
+
42
+ const require = createRequire(import.meta.url);
43
+ const { version } = require("../package.json");
44
+
45
+ /** A terminal's colours must not leak into a protocol string. */
46
+ const ANSI = /\x1b\[[0-9;]*m/g;
47
+
48
+ /**
49
+ * Run a CLI function with its output captured.
50
+ *
51
+ * @param {(argv: string[], io: object) => Promise<{exitCode?: number}>} fn
52
+ * @param {string[]} argv
53
+ * @returns {Promise<{text: string, exitCode: number}>}
54
+ */
55
+ const capture = async (fn, argv) => {
56
+ const lines = [];
57
+ const result = (await fn(argv, { print: (line) => lines.push(String(line)) })) || {};
58
+ return {
59
+ text: lines.join("\n").replace(ANSI, "").trim(),
60
+ exitCode: Number(result.exitCode) || 0,
61
+ };
62
+ };
63
+
64
+ const rootArg = (root) => ["--root", root];
65
+
66
+ /**
67
+ * Parse JSON that a command printed, so the protocol can carry it as data too.
68
+ *
69
+ * @param {string} text
70
+ * @returns {object|null}
71
+ */
72
+ const asData = (text) => {
73
+ try {
74
+ return JSON.parse(text);
75
+ } catch {
76
+ return null;
77
+ }
78
+ };
79
+
80
+ /**
81
+ * The executor for every catalogue entry. Each one returns the text a person
82
+ * would read and, where the command has a JSON form, the same facts as data.
83
+ */
84
+ const RUN = {
85
+ report: async ({ root, tests }) => {
86
+ const built = await buildReport({ root, tests: Boolean(tests) });
87
+ return { text: renderText(built).join("\n"), data: built };
88
+ },
89
+
90
+ widget_props: async ({ root, widget }) => {
91
+ const out = await capture(checkApi, ["--props", widget, ...rootArg(root)]);
92
+ const missing = /is not a FletBox widget/.test(out.text);
93
+ return { ...out, error: missing ? `no widget named "${widget}"` : null };
94
+ },
95
+
96
+ widget_contract: async ({ root, widget }) => {
97
+ const out = await capture(unpack, [widget, "--json", ...rootArg(root)]);
98
+ return { ...out, data: asData(out.text) };
99
+ },
100
+
101
+ list_widgets: async ({ root }) => capture(checkApi, ["--list", ...rootArg(root)]),
102
+
103
+ check: async ({ root, full }) => {
104
+ const out = await capture(checkApi, ["--json", ...(full ? ["--full"] : []), ...rootArg(root)]);
105
+ return { ...out, data: asData(out.text) };
106
+ },
107
+
108
+ design: async ({ root, tokens, strict }) => {
109
+ const out = await capture(checkUi, [
110
+ "--json",
111
+ ...(tokens ? ["--tokens"] : []),
112
+ ...(strict ? ["--strict"] : []),
113
+ ...rootArg(root),
114
+ ]);
115
+ return { ...out, data: asData(out.text) };
116
+ },
117
+
118
+ syntax: async ({ root }) => {
119
+ // `runGates`, not `verify`: verify appends a metrics row, and a diagnosis
120
+ // that changes the history it reports on is not read-only.
121
+ const result = await runGates(root, ["syntax"]);
122
+ return { text: formatReport(result).join("\n").replace(ANSI, ""), data: result };
123
+ },
124
+
125
+ types: async ({ root }) => {
126
+ const result = await runGates(root, ["types"]);
127
+ return { text: formatReport(result).join("\n").replace(ANSI, ""), data: result };
128
+ },
129
+
130
+ metrics: async ({ root }) => capture(metrics, rootArg(root)),
131
+ };
132
+
133
+ /**
134
+ * The MCP tool descriptor for one catalogue entry.
135
+ *
136
+ * @param {object} tool
137
+ * @returns {object}
138
+ */
139
+ const descriptor = (tool) => {
140
+ const properties = {};
141
+ const required = [];
142
+ for (const param of tool.params) {
143
+ properties[param.name] = { type: param.type, description: param.description };
144
+ if (param.required) required.push(param.name);
145
+ }
146
+ return {
147
+ name: tool.id,
148
+ title: tool.title,
149
+ description:
150
+ `${tool.question} Read-only: ${tool.writes.length ? `only writes on the CLI (${tool.writes.join("; ")}), never here.` : "writes nothing."}`,
151
+ inputSchema: {
152
+ type: "object",
153
+ properties,
154
+ ...(required.length ? { required } : {}),
155
+ additionalProperties: false,
156
+ },
157
+ };
158
+ };
159
+
160
+ /**
161
+ * Answer one JSON-RPC message.
162
+ *
163
+ * Exported so `scripts/check-report.mjs` can drive the protocol without a
164
+ * child process; the stdio loop below is a thin wrapper over it.
165
+ *
166
+ * @param {object} request
167
+ * @param {{cwd?: string}} [context]
168
+ * @returns {Promise<object|null>} null for a notification (no reply)
169
+ */
170
+ export const handleRequest = async (request, { cwd = process.cwd() } = {}) => {
171
+ const { id = null, method, params = {} } = request || {};
172
+ const reply = (result) => ({ jsonrpc: "2.0", id, result });
173
+ const fail = (code, message) => ({ jsonrpc: "2.0", id, error: { code, message } });
174
+
175
+ if (method === "initialize") {
176
+ return reply({
177
+ protocolVersion:
178
+ typeof params.protocolVersion === "string" ? params.protocolVersion : "2024-11-05",
179
+ capabilities: { tools: {} },
180
+ serverInfo: { name: "flet-box", version },
181
+ instructions:
182
+ "Read-only diagnosis for a FletBox project. Start with `report` when you do not know which file is wrong; `widget_props` before writing a widget call; `metrics` when the same finding keeps returning.",
183
+ });
184
+ }
185
+ if (method === "notifications/initialized" || method === "notifications/cancelled") return null;
186
+ if (method === "ping") return reply({});
187
+ if (method === "tools/list") return reply({ tools: TOOLS.map(descriptor) });
188
+
189
+ if (method !== "tools/call") return fail(-32601, `unknown method: ${method}`);
190
+
191
+ const tool = toolById(params.name);
192
+ if (!tool) return fail(-32602, `unknown tool: ${params.name}`);
193
+ const args = params.arguments && typeof params.arguments === "object" ? params.arguments : {};
194
+
195
+ const missing = tool.params.filter((param) => param.required && !args[param.name]);
196
+ if (missing.length) {
197
+ return reply({
198
+ content: [
199
+ { type: "text", text: `Missing argument(s): ${missing.map((param) => param.name).join(", ")}` },
200
+ ],
201
+ isError: true,
202
+ });
203
+ }
204
+
205
+ let root;
206
+ try {
207
+ root = args.root ? path.resolve(cwd, String(args.root)) : cwd;
208
+ if (!fs.existsSync(root) || !fs.statSync(root).isDirectory()) {
209
+ throw new Error(`no such directory: ${root}`);
210
+ }
211
+ } catch (error) {
212
+ return reply({ content: [{ type: "text", text: error.message }], isError: true });
213
+ }
214
+
215
+ try {
216
+ const result = await RUN[tool.id]({ ...args, root });
217
+ if (result.error) {
218
+ return reply({ content: [{ type: "text", text: result.text || result.error }], isError: true });
219
+ }
220
+ return reply({
221
+ content: [{ type: "text", text: result.text || "(no output)" }],
222
+ ...(result.data ? { structuredContent: result.data } : {}),
223
+ });
224
+ } catch (error) {
225
+ return reply({ content: [{ type: "text", text: `${tool.id} failed: ${error.message}` }], isError: true });
226
+ }
227
+ };
228
+
229
+ /**
230
+ * Serve the toolkit on stdin/stdout until the client closes the pipe.
231
+ *
232
+ * @returns {void}
233
+ */
234
+ export const serveStdio = () => {
235
+ process.stderr.write(
236
+ `flet-box mcp v${version} — ${TOOLS.length} read-only tool(s) over stdio (JSON-RPC 2.0)\n`,
237
+ );
238
+ const input = readline.createInterface({ input: process.stdin, crlfDelay: Infinity });
239
+ const write = (message) => {
240
+ if (message) process.stdout.write(`${JSON.stringify(message)}\n`);
241
+ };
242
+
243
+ // stdin can close while a request is still running — `report` takes about a
244
+ // second — and exiting on "close" would drop the answer the client is waiting
245
+ // for. Count the work in flight and leave only when the queue is empty.
246
+ let pending = 0;
247
+ let closed = false;
248
+ const finish = () => {
249
+ if (closed && pending === 0) process.exit(0);
250
+ };
251
+
252
+ input.on("line", (line) => {
253
+ const trimmed = line.trim();
254
+ if (!trimmed) return;
255
+ let request;
256
+ try {
257
+ request = JSON.parse(trimmed);
258
+ } catch {
259
+ write({ jsonrpc: "2.0", id: null, error: { code: -32700, message: "parse error" } });
260
+ return;
261
+ }
262
+ pending += 1;
263
+ handleRequest(request)
264
+ .then((message) => write(message))
265
+ .catch((error) => {
266
+ write({ jsonrpc: "2.0", id: request?.id ?? null, error: { code: -32603, message: error.message } });
267
+ })
268
+ .finally(() => {
269
+ pending -= 1;
270
+ finish();
271
+ });
272
+ });
273
+
274
+ input.on("close", () => {
275
+ closed = true;
276
+ finish();
277
+ });
278
+ };
279
+
280
+ export default serveStdio;
@@ -0,0 +1,27 @@
1
+ // bin/utils/contract.js
2
+ //
3
+ // `AGENTS_FOR_CODE_GEN.md` at the package root is the ONLY source for the
4
+ // code-generation contract. `flet-box create` copies it into every app it
5
+ // builds, the same way it copies the skills: one copy of the text, so an
6
+ // editor, a diff and the app's own agent all read the same bytes.
7
+ //
8
+ // Node core only: `bin/` may not import a devDependency at module scope.
9
+
10
+ import fs from "node:fs";
11
+ import path from "node:path";
12
+ import { fileURLToPath } from "node:url";
13
+
14
+ const packageDir = path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "..");
15
+
16
+ /** The file name the contract carries inside a generated app. */
17
+ export const CONTRACT_FILE = "AGENTS_FOR_CODE_GEN.md";
18
+
19
+ /** Absolute path of the shipped contract. */
20
+ export const contractPath = () => path.join(packageDir, CONTRACT_FILE);
21
+
22
+ /**
23
+ * The contract text.
24
+ * @throws when the file is missing — a missing contract is a build error, not a
25
+ * silent skip, or the app ships with half the rules and nobody notices.
26
+ */
27
+ export const readContract = () => fs.readFileSync(contractPath(), "utf8");
@@ -107,8 +107,12 @@ export const parseInterfaces = (source) => {
107
107
  // Comments can contain `interface` in prose; strip them first so brace
108
108
  // matching never starts on a documentation example.
109
109
  const code = source.replace(/\/\*[\s\S]*?\*\//g, "").replace(/\/\/[^\n]*/g, "");
110
+ // `[^{;]*`, not `[^{]*`: a non-object alias (`export type FletWidget =
111
+ // WidgetFactoryInstance;`) has no brace of its own, and the looser tail ran on
112
+ // to the next declaration's brace — swallowing the interface that followed it
113
+ // and with it every prop it declared.
110
114
  const header =
111
- /(export\s+)?(?:declare\s+)?(?:interface\s+([A-Za-z_$][\w$]*)|type\s+([A-Za-z_$][\w$]*)\s*=\s*)([^{]*)\{/g;
115
+ /(export\s+)?(?:declare\s+)?(?:interface\s+([A-Za-z_$][\w$]*)|type\s+([A-Za-z_$][\w$]*)\s*=\s*)([^{;]*)\{/g;
112
116
  let match;
113
117
  while ((match = header.exec(code))) {
114
118
  const name = match[2] || match[3];
@@ -0,0 +1,146 @@
1
+ // bin/utils/migrationGuide.js
2
+ //
3
+ // The migration guide is the promise a release makes to the app that used the
4
+ // previous one: "here is what changed, and here is what you write now".
5
+ //
6
+ // Three things read it, and all three read the same bytes:
7
+ // • `flet-box release --major` refuses to publish a major that has no
8
+ // section for it — a major is exactly the release where an app has to
9
+ // change something, so the guide must exist before the release does;
10
+ // • `scripts/check-migrations.mjs` proves the guide, the deprecation table
11
+ // and the codemod still agree with each other;
12
+ // • the person whose app is about to break.
13
+ //
14
+ // Node core only: `bin/` may not import a devDependency at module scope.
15
+
16
+ import fs from "node:fs";
17
+ import path from "node:path";
18
+ import { fileURLToPath } from "node:url";
19
+
20
+ const packageDir = path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "..");
21
+
22
+ /** The guide, relative to the package root. */
23
+ export const GUIDE_RELATIVE = "docs/guides/migration.md";
24
+
25
+ /**
26
+ * Absolute path of the guide inside a checkout.
27
+ *
28
+ * @param {string} [root]
29
+ * @returns {string}
30
+ */
31
+ export const guidePath = (root = packageDir) =>
32
+ path.join(root, ...GUIDE_RELATIVE.split("/"));
33
+
34
+ /**
35
+ * The guide's text, or null when there is none.
36
+ *
37
+ * @param {string} [root]
38
+ * @returns {string|null}
39
+ */
40
+ export const readGuide = (root = packageDir) => {
41
+ try {
42
+ return fs.readFileSync(guidePath(root), "utf8");
43
+ } catch {
44
+ return null;
45
+ }
46
+ };
47
+
48
+ /**
49
+ * The guide's version sections, as `heading → body`.
50
+ *
51
+ * A section is a `## <version>` heading; anything under it belongs to it.
52
+ *
53
+ * @param {string|null} text
54
+ * @returns {Map<string, string>}
55
+ */
56
+ export const sections = (text) => {
57
+ const map = new Map();
58
+ if (!text) return map;
59
+ let current = null;
60
+ let body = [];
61
+ for (const line of text.split(/\r?\n/)) {
62
+ const heading = /^##\s+(.+?)\s*$/.exec(line);
63
+ if (heading) {
64
+ if (current !== null) map.set(current, body.join("\n"));
65
+ current = heading[1];
66
+ body = [];
67
+ continue;
68
+ }
69
+ if (current !== null) body.push(line);
70
+ }
71
+ if (current !== null) map.set(current, body.join("\n"));
72
+ return map;
73
+ };
74
+
75
+ /**
76
+ * The "de X a Y" rows of one section.
77
+ *
78
+ * The table is the contract, so it is parsed rather than guessed: a row is
79
+ * `| Widget | From | To | What changed |`, and the header and the separator
80
+ * are not rows.
81
+ *
82
+ * @param {string|null} text The whole guide.
83
+ * @param {string} version The section heading, e.g. `"1.2.0"`.
84
+ * @returns {Array<{ widget: string, from: string, to: string, note: string }>}
85
+ */
86
+ export const rowsFor = (text, version) => {
87
+ const body = sections(text).get(version);
88
+ if (!body) return [];
89
+ const rows = [];
90
+ for (const line of body.split(/\r?\n/)) {
91
+ const trimmed = line.trim();
92
+ if (!trimmed.startsWith("|")) continue;
93
+ const cells = trimmed
94
+ .split("|")
95
+ .slice(1, -1)
96
+ .map((cell) => cell.trim().replace(/`/g, ""));
97
+ if (cells.length < 3) continue;
98
+ const [widget, from, to] = cells;
99
+ if (!/^[A-Z][A-Za-z0-9]*$/.test(widget)) continue; // the separator row
100
+ if (from === "From") continue; // the header row
101
+ rows.push({ widget, from, to, note: (cells[3] || "").trim() });
102
+ }
103
+ return rows;
104
+ };
105
+
106
+ /**
107
+ * Is this checkout the framework itself, rather than an app that uses it?
108
+ *
109
+ * Only the framework ships the guide and only the framework releases its own
110
+ * major version, so the guard never fires inside somebody's app.
111
+ *
112
+ * @param {string} [root]
113
+ * @returns {boolean}
114
+ */
115
+ export const isFrameworkProject = (root = packageDir) => {
116
+ let pkg;
117
+ try {
118
+ pkg = JSON.parse(fs.readFileSync(path.join(root, "package.json"), "utf8"));
119
+ } catch {
120
+ return false;
121
+ }
122
+ const bin = pkg.bin && (pkg.bin["flet-box"] || pkg.bin.fletBox);
123
+ if (!bin) return false;
124
+ return fs.existsSync(path.join(root, bin));
125
+ };
126
+
127
+ /**
128
+ * What is missing before a version may be released, or null when nothing is.
129
+ *
130
+ * @param {string} version The version about to be published.
131
+ * @param {string} [root]
132
+ * @returns {{ version: string, reason: string }|null}
133
+ */
134
+ export const missingMigration = (version, root = packageDir) => {
135
+ const text = readGuide(root);
136
+ if (!text) {
137
+ return { version, reason: `the migration guide is missing (${GUIDE_RELATIVE})` };
138
+ }
139
+ if (!sections(text).has(version)) {
140
+ return { version, reason: `the guide has no "## ${version}" section` };
141
+ }
142
+ if (!rowsFor(text, version).length) {
143
+ return { version, reason: `the "## ${version}" section of the guide has no rows` };
144
+ }
145
+ return null;
146
+ };
@@ -12,7 +12,11 @@ import fs from "node:fs";
12
12
  import path from "node:path";
13
13
  import { neutralise } from "./neutralise.js";
14
14
 
15
- const IDENT = "[A-Za-z_$][\\w$]*";
15
+ // Unicode identifiers: an app is allowed to export `fijarTamaño`. The ASCII
16
+ // class stopped at `ñ`, so the reader stored `fijarTama` and every import of the
17
+ // real name came back as MISSING_EXPORT — a false positive the app then worked
18
+ // around by renaming its exports.
19
+ const IDENT = "[\\p{ID_Start}$_][\\p{ID_Continue}$]*";
16
20
 
17
21
  /**
18
22
  * Resolve a relative/absolute specifier the way the browser ESM loader would,
@@ -85,7 +89,7 @@ export const readModuleExports = (absFile, cache = new Map(), chain = new Set())
85
89
  // export const|let|var|function|class NAME (incl. multi-declarator and async)
86
90
  const declRe = new RegExp(
87
91
  `export\\s+(?:async\\s+)?(?:const|let|var|function|class)\\s+(${IDENT})`,
88
- "g",
92
+ "gu",
89
93
  );
90
94
  let decl;
91
95
  while ((decl = declRe.exec(code))) {
@@ -93,7 +97,7 @@ export const readModuleExports = (absFile, cache = new Map(), chain = new Set())
93
97
  // `export const a = 1, b = 2;` — only accept a later name when it is
94
98
  // immediately assigned, so type-ish commas cannot invent an export.
95
99
  const tail = code.slice(decl.index + decl[0].length, code.indexOf(";", decl.index));
96
- const extraRe = new RegExp(`,\\s*(${IDENT})\\s*=`, "g");
100
+ const extraRe = new RegExp(`,\\s*(${IDENT})\\s*=`, "gu");
97
101
  let extra;
98
102
  while ((extra = extraRe.exec(tail))) surface.named.add(extra[1]);
99
103
  }
@@ -119,7 +123,7 @@ export const readModuleExports = (absFile, cache = new Map(), chain = new Set())
119
123
  for (const part of braces[1].split(",")) {
120
124
  const token = part.trim();
121
125
  if (!token) continue;
122
- const aliased = new RegExp(`^(${IDENT})\\s+as\\s+(${IDENT})$`).exec(token);
126
+ const aliased = new RegExp(`^(${IDENT})\\s+as\\s+(${IDENT})$`, "u").exec(token);
123
127
  const original = aliased ? aliased[1] : token;
124
128
  const exportedAs = aliased ? aliased[2] : token;
125
129
  if (original === "default") {
@@ -140,7 +144,7 @@ export const readModuleExports = (absFile, cache = new Map(), chain = new Set())
140
144
  // export * [as NS] from './mod.js'
141
145
  const starRe = new RegExp(
142
146
  `export\\s+\\*\\s*(?:as\\s+(${IDENT})\\s+)?from\\s*['"]([^'"]+)['"]`,
143
- "g",
147
+ "gu",
144
148
  );
145
149
  let star;
146
150
  while ((star = starRe.exec(code))) {