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.
- package/AGENTS_FOR_CODE_GEN.md +284 -0
- package/CONTRIBUTING.md +238 -0
- package/README.md +2 -2
- package/bin/cli.js +88 -1
- package/bin/commands/auditFix.js +181 -27
- package/bin/commands/checkApi.js +14 -3
- package/bin/commands/checkStructure.js +368 -0
- package/bin/commands/checkSyntax.js +293 -0
- package/bin/commands/checkUi.js +666 -14
- package/bin/commands/create.js +14 -0
- package/bin/commands/createBundle.js +2 -2
- package/bin/commands/metrics.js +448 -0
- package/bin/commands/release.js +18 -0
- package/bin/commands/report.js +849 -0
- package/bin/commands/tools.js +76 -0
- package/bin/commands/unpack.js +39 -11
- package/bin/commands/verify.js +879 -0
- package/bin/mcp.js +280 -0
- package/bin/utils/contract.js +27 -0
- package/bin/utils/dtsProps.js +5 -1
- package/bin/utils/migrationGuide.js +146 -0
- package/bin/utils/moduleExports.js +9 -5
- package/bin/utils/templates.js +225 -18
- package/bin/utils/toolkit.js +191 -0
- package/docs/README.md +5 -1
- package/docs/arquitectura.md +1 -1
- package/docs/cli/README.md +168 -2
- package/docs/guides/README.md +6 -0
- package/docs/guides/accessibility.md +2 -1
- package/docs/guides/ai-toolkit.md +166 -0
- package/docs/guides/compatibility.md +1 -1
- package/docs/guides/migration.md +92 -0
- package/docs/guides/minification-and-protection.md +2 -1
- package/docs/guides/mobile-and-platforms.md +1 -1
- package/docs/guides/online-updates.md +2 -0
- package/docs/server/deployment.md +1 -1
- package/docs/tools/styling.md +2 -2
- package/docs/tools/theme.md +139 -4
- package/docs/widget/Accordion.md +74 -8
- package/docs/widget/AdMob.md +68 -3
- package/docs/widget/AdSense.md +67 -2
- package/docs/widget/AlertDialog.md +1 -1
- package/docs/widget/AnimatedBox.md +80 -39
- package/docs/widget/AnimatedText.md +74 -18
- package/docs/widget/Audio.md +73 -14
- package/docs/widget/Avatar.md +69 -4
- package/docs/widget/Badge.md +68 -3
- package/docs/widget/BottomSheet.md +2 -2
- package/docs/widget/Button.md +69 -3
- package/docs/widget/Card.md +69 -4
- package/docs/widget/Carousel.md +67 -2
- package/docs/widget/Chart.md +67 -2
- package/docs/widget/Checkbox.md +74 -8
- package/docs/widget/Chip.md +68 -3
- package/docs/widget/CircularBar.md +74 -15
- package/docs/widget/CircularChart.md +71 -10
- package/docs/widget/CodeViewer.md +67 -2
- package/docs/widget/Column.md +68 -3
- package/docs/widget/Container.md +68 -3
- package/docs/widget/DataTable.md +67 -2
- package/docs/widget/Divider.md +67 -2
- package/docs/widget/DraggBox.md +67 -2
- package/docs/widget/Dropdown.md +76 -10
- package/docs/widget/DroppBox.md +67 -2
- package/docs/widget/FloatingActionButton.md +68 -3
- package/docs/widget/GridView.md +67 -2
- package/docs/widget/Icon.md +71 -5
- package/docs/widget/Image.md +68 -3
- package/docs/widget/Input.md +68 -3
- package/docs/widget/Inspector.md +2 -2
- package/docs/widget/InstallButton.md +73 -13
- package/docs/widget/ListTile.md +69 -3
- package/docs/widget/ListView.md +67 -2
- package/docs/widget/Maps.md +68 -3
- package/docs/widget/Markdown.md +74 -15
- package/docs/widget/MatrixRain.md +72 -11
- package/docs/widget/Menu.md +209 -0
- package/docs/widget/Modal.md +3 -3
- package/docs/widget/Pagination.md +68 -3
- package/docs/widget/ParallaxBox.md +72 -12
- package/docs/widget/ProgressBar.md +69 -4
- package/docs/widget/QRCode.md +67 -2
- package/docs/widget/README.md +54 -50
- package/docs/widget/Radio.md +76 -10
- package/docs/widget/Rating.md +70 -6
- package/docs/widget/Row.md +68 -3
- package/docs/widget/Skeleton.md +68 -3
- package/docs/widget/Slider.md +75 -9
- package/docs/widget/SnackBar.md +2 -2
- package/docs/widget/Stack.md +68 -3
- package/docs/widget/Stepper.md +68 -3
- package/docs/widget/Switch.md +74 -8
- package/docs/widget/Text.md +71 -4
- package/docs/widget/ThemePicker.md +197 -0
- package/docs/widget/Tooltip.md +72 -7
- package/docs/widget/TreeView.md +70 -10
- package/docs/widget/Video.md +71 -10
- package/package.json +10 -3
- package/skills/fletbox-design/SKILL.md +89 -5
- package/skills/fletbox-motion/SKILL.md +1 -1
- package/skills/fletbox-rules/SKILL.md +56 -4
- package/src/core/runApp.js +12 -0
- package/src/index.d.ts +374 -101
- package/src/index.js +9 -0
- package/src/navigations/AdaptiveScaffold.js +2 -2
- package/src/navigations/BottomNavigation.js +1 -1
- package/src/navigations/Scaffold.js +18 -6
- package/src/tools/useState.js +11 -4
- package/src/utils/colors.js +275 -0
- package/src/utils/errorBoundary.js +288 -0
- package/src/utils/index.js +10 -2
- package/src/utils/stackPosition.js +39 -24
- package/src/utils/syntaxHighlight.js +4 -1
- package/src/utils/themes.js +279 -3
- package/src/widget-factory/addChildren.js +17 -10
- package/src/widget-factory/assignProps.js +18 -6
- package/src/widget-factory/createWidget.js +7 -0
- package/src/widget-factory/deprecations.js +77 -0
- package/src/widget-factory/translateProps.js +11 -1
- package/src/widget-factory/warnings.js +1 -27
- package/src/widget-factory/widgetFactory.js +59 -17
- package/src/widgets/Accordion.js +21 -7
- package/src/widgets/Badge.js +11 -2
- package/src/widgets/Button.js +15 -3
- package/src/widgets/Carousel.js +12 -4
- package/src/widgets/Chart.js +7 -2
- package/src/widgets/Checkbox.js +1 -2
- package/src/widgets/Chip.js +24 -10
- package/src/widgets/CircularBar.js +9 -3
- package/src/widgets/CircularChart.js +5 -3
- package/src/widgets/Container.js +6 -0
- package/src/widgets/Divider.js +3 -1
- package/src/widgets/DraggBox.js +9 -0
- package/src/widgets/Dropdown.js +3 -3
- package/src/widgets/DroppBox.js +5 -0
- package/src/widgets/FloatingActionButton.js +1 -1
- package/src/widgets/Icon.js +5 -1
- package/src/widgets/InstallButton.js +4 -2
- package/src/widgets/Markdown.js +171 -103
- package/src/widgets/Menu.js +340 -0
- package/src/widgets/Modal.js +7 -1
- package/src/widgets/Pagination.js +10 -5
- package/src/widgets/ProgressBar.js +5 -4
- package/src/widgets/QRCode.js +4 -0
- package/src/widgets/Radio.js +1 -2
- package/src/widgets/Rating.js +9 -0
- package/src/widgets/Slider.js +4 -1
- package/src/widgets/SnackBar.js +2 -2
- package/src/widgets/Switch.js +1 -2
- package/src/widgets/Text.js +9 -5
- package/src/widgets/ThemePicker.js +234 -0
- package/src/widgets/TreeView.js +2 -1
- package/src/widgets/index.js +2 -0
- 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");
|
package/bin/utils/dtsProps.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
"
|
|
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*=`, "
|
|
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})
|
|
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
|
-
"
|
|
147
|
+
"gu",
|
|
144
148
|
);
|
|
145
149
|
let star;
|
|
146
150
|
while ((star = starRe.exec(code))) {
|