flet-box 1.1.1 → 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 +17 -13
- 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 +18 -1
- package/bin/commands/createBundle.js +20 -8
- 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/bundleConfig.js +47 -3
- 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 +248 -30
- package/bin/utils/toolkit.js +191 -0
- package/docs/README.md +26 -1
- package/docs/arquitectura.md +1 -1
- package/docs/cli/README.md +201 -11
- 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/first-app.md +6 -7
- 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 +20 -6
- 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/cli.js
CHANGED
|
@@ -15,6 +15,10 @@ import path from "path";
|
|
|
15
15
|
import { createProject } from "./commands/create.js";
|
|
16
16
|
import { checkApi } from "./commands/checkApi.js";
|
|
17
17
|
import { checkUi } from "./commands/checkUi.js";
|
|
18
|
+
import { verify, watchVerify, formatReport } from "./commands/verify.js";
|
|
19
|
+
import { metrics } from "./commands/metrics.js";
|
|
20
|
+
import { report } from "./commands/report.js";
|
|
21
|
+
import { tools } from "./commands/tools.js";
|
|
18
22
|
import { auditFix } from "./commands/auditFix.js";
|
|
19
23
|
import { toPng } from "./commands/toPng.js";
|
|
20
24
|
import { images } from "./commands/images.js";
|
|
@@ -91,8 +95,22 @@ ${cmd("check-ui", "Clean-UI gate: tokens, 8-based scale, tap targets, WCAG contr
|
|
|
91
95
|
${cmd(" └─ --tokens", "print the measured contrast of every color token")}
|
|
92
96
|
${cmd(" └─ --strict", "fail on contrast warnings too")}
|
|
93
97
|
${cmd(" └─ alias", "ui | checkUi")}
|
|
98
|
+
${cmd("verify", "Every gate at once: one report, one exit code")}
|
|
99
|
+
${cmd(" └─ --only <gate>", "syntax | api | types | ui | structure | surface | tests")}
|
|
100
|
+
${cmd(" └─ --no-tests", "the static gates only; the project's suites run alone")}
|
|
101
|
+
${cmd(" └─ --fix", "the mechanical repair first, then the gates")}
|
|
102
|
+
${cmd(" └─ --json", "the report as data")}
|
|
103
|
+
${cmd(" └─ alias", "check-all")}
|
|
104
|
+
${cmd("report", "Read-only diagnosis: a verdict per file and what to do next")}
|
|
105
|
+
${cmd(" └─ --json | --md", "the same facts as data or Markdown")}
|
|
106
|
+
${cmd(" └─ --html [--out <file>]", "one self-contained page (.fletbox/reports/report.html)")}
|
|
107
|
+
${cmd(" └─ --tests", "also run the project's own suites")}
|
|
108
|
+
${cmd("tools [--emit]", "The read-only toolkit: one question, one command")}
|
|
109
|
+
${cmd("mcp", "Serve that toolkit over MCP stdio (read-only)")}
|
|
110
|
+
${cmd("metrics", "The north metric: code that passed with no correction")}
|
|
111
|
+
${cmd(" └─ --log <file>", "read another project's log")}
|
|
94
112
|
${cmd("audit-fix", "Rewrite broken imports, missing imports and misspelled props")}
|
|
95
|
-
${cmd(" └─ --only imports|props", "run one repair instead of
|
|
113
|
+
${cmd(" └─ --only imports|props|migrations", "run one repair instead of the others")}
|
|
96
114
|
${cmd(" └─ alias", "fix-imports | fix-path-imports | auditFix")}
|
|
97
115
|
${cmd("to-png [dir]", "Convert JPEG/BMP/GIF images to PNG, no dependencies")}
|
|
98
116
|
${cmd(" └─ --replace", "remove each source after its PNG was written and read back")}
|
|
@@ -124,6 +142,7 @@ ${cmd("--version | --help", "Version and this help")}
|
|
|
124
142
|
${section("⚙️", "OPTIONS")}
|
|
125
143
|
${opt("--port <n>, -p <n>", "Port (default 8000) — asked if not provided")}
|
|
126
144
|
${opt("--hot", "Hot reload with run")}
|
|
145
|
+
${opt("--verify", "Run the gates on every edit while the server runs")}
|
|
127
146
|
${opt("--quiet", "No request logging")}
|
|
128
147
|
${opt("--dry-run | --list", "kill-server: just list, do not kill")}
|
|
129
148
|
|
|
@@ -167,6 +186,7 @@ const parseFlags = () => {
|
|
|
167
186
|
logRequests: true,
|
|
168
187
|
hot: false,
|
|
169
188
|
dryRun: false,
|
|
189
|
+
verify: false,
|
|
170
190
|
};
|
|
171
191
|
|
|
172
192
|
for (let i = 0; i < args.length; i++) {
|
|
@@ -187,6 +207,8 @@ const parseFlags = () => {
|
|
|
187
207
|
flags.logRequests = false;
|
|
188
208
|
} else if (arg === "--hot" || arg === "--hot-reload") {
|
|
189
209
|
flags.hot = true;
|
|
210
|
+
} else if (arg === "--verify") {
|
|
211
|
+
flags.verify = true;
|
|
190
212
|
} else if (arg === "--dry-run" || arg === "--list" || arg === "--check") {
|
|
191
213
|
flags.dryRun = true;
|
|
192
214
|
}
|
|
@@ -242,6 +264,24 @@ async function main() {
|
|
|
242
264
|
switch (command) {
|
|
243
265
|
case "create":
|
|
244
266
|
case "new":
|
|
267
|
+
// `flet-box create --help` must print the help, not scaffold a folder
|
|
268
|
+
// named `--help`: that stray clone lands inside the project and the
|
|
269
|
+
// structure gate starts reporting SOURCE_OUTSIDE_ROOT for every module
|
|
270
|
+
// in it.
|
|
271
|
+
if (projectName && projectName.startsWith("-")) {
|
|
272
|
+
if (projectName === "--help" || projectName === "-h") {
|
|
273
|
+
showHelp();
|
|
274
|
+
break;
|
|
275
|
+
}
|
|
276
|
+
console.error(c("red", `❌ Unknown option: ${projectName}`));
|
|
277
|
+
console.log(
|
|
278
|
+
c(
|
|
279
|
+
"gray",
|
|
280
|
+
"Usage: flet-box create <project-name> [--adaptive|--blank|--full|--sidebar|--template <name>|--repo user/repo]",
|
|
281
|
+
),
|
|
282
|
+
);
|
|
283
|
+
process.exit(1);
|
|
284
|
+
}
|
|
245
285
|
if (!projectName) {
|
|
246
286
|
console.error(c("red", "❌ Error: Project name required"));
|
|
247
287
|
console.log(
|
|
@@ -328,6 +368,40 @@ async function main() {
|
|
|
328
368
|
break;
|
|
329
369
|
}
|
|
330
370
|
|
|
371
|
+
case "verify":
|
|
372
|
+
case "check-all": {
|
|
373
|
+
const { exitCode } = await verify(args.slice(1));
|
|
374
|
+
if (exitCode) process.exit(exitCode);
|
|
375
|
+
break;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
case "metrics": {
|
|
379
|
+
const { exitCode } = await metrics(args.slice(1));
|
|
380
|
+
if (exitCode) process.exit(exitCode);
|
|
381
|
+
break;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
case "report": {
|
|
385
|
+
const { exitCode } = await report(args.slice(1));
|
|
386
|
+
if (exitCode) process.exit(exitCode);
|
|
387
|
+
break;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
case "tools":
|
|
391
|
+
case "toolkit": {
|
|
392
|
+
const { exitCode } = await tools(args.slice(1));
|
|
393
|
+
if (exitCode) process.exit(exitCode);
|
|
394
|
+
break;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
case "mcp": {
|
|
398
|
+
// Lazy: the stdio server takes over the process, and nothing else in
|
|
399
|
+
// the CLI should pay for loading it.
|
|
400
|
+
const { serveStdio } = await import("./mcp.js");
|
|
401
|
+
serveStdio();
|
|
402
|
+
break;
|
|
403
|
+
}
|
|
404
|
+
|
|
331
405
|
case "audit-fix":
|
|
332
406
|
case "auditFix":
|
|
333
407
|
case "fix-imports":
|
|
@@ -410,6 +484,19 @@ async function main() {
|
|
|
410
484
|
// which are devDependencies: a consumer installing `flet-box` from npm
|
|
411
485
|
// has neither, so loading them at module scope would break every CLI
|
|
412
486
|
// command for them. Import them when a server is actually started.
|
|
487
|
+
if (flags.verify) {
|
|
488
|
+
console.log(
|
|
489
|
+
c("dim", "🔎 verify is watching src/ — every edit is checked against the gates"),
|
|
490
|
+
);
|
|
491
|
+
watchVerify({
|
|
492
|
+
root: process.cwd(),
|
|
493
|
+
onReport: (report) => {
|
|
494
|
+
console.log("");
|
|
495
|
+
for (const line of formatReport(report)) console.log(line);
|
|
496
|
+
console.log("");
|
|
497
|
+
},
|
|
498
|
+
});
|
|
499
|
+
}
|
|
413
500
|
await (await import("./commands/runServer.js")).runDevServer({
|
|
414
501
|
spaMode: false,
|
|
415
502
|
hotReload: flags.hot,
|
package/bin/commands/auditFix.js
CHANGED
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `flet-box audit-fix` — write what `check-api` only reports.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Three repairs, all anchored to spans the gate already knows how to find:
|
|
5
5
|
*
|
|
6
|
-
* imports
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* props
|
|
11
|
-
*
|
|
12
|
-
*
|
|
6
|
+
* imports a relative path that does not resolve, a name the target module
|
|
7
|
+
* never exports, a default import from a module without a default,
|
|
8
|
+
* and identifiers used with no import at all (a flet-box export, or
|
|
9
|
+
* a Node builtin used as `path.join(…)`).
|
|
10
|
+
* props a key inside `Widget({ … })` that neither the widget's declared
|
|
11
|
+
* props, the structural props, nor the engine registry knows —
|
|
12
|
+
* `chilldren` → `children`, `postion` → `position`.
|
|
13
|
+
* migrations a key inside `Widget({ … })` that the widget still honours but no
|
|
14
|
+
* longer recommends — `Radio({ onSelect })` → `Radio({ onPress })`.
|
|
13
15
|
*
|
|
14
16
|
* A rewrite only happens when the intended name is unambiguous: one candidate,
|
|
15
17
|
* edit distance small enough that no real name could collide with it. Anything
|
|
@@ -23,6 +25,7 @@ import { c, banner, section } from "../utils/colors.js";
|
|
|
23
25
|
import { neutralise } from "../utils/neutralise.js";
|
|
24
26
|
import { resolveLocalModule, readModuleExports } from "../utils/moduleExports.js";
|
|
25
27
|
import { resolveInterfaceProps } from "../utils/dtsProps.js";
|
|
28
|
+
import { deprecationFor } from "../../src/widget-factory/deprecations.js";
|
|
26
29
|
import {
|
|
27
30
|
STRUCTURAL_PROPS,
|
|
28
31
|
collectSources,
|
|
@@ -252,9 +255,13 @@ const strictMatch = (wanted, candidates) => {
|
|
|
252
255
|
/**
|
|
253
256
|
* Top-level keys of an object literal, with their spans.
|
|
254
257
|
*
|
|
258
|
+
* `entryEnd` is where the whole entry ends — the key *and* its value — which
|
|
259
|
+
* is what a removal has to take, since deleting a key without its value would
|
|
260
|
+
* leave the value behind.
|
|
261
|
+
*
|
|
255
262
|
* @param {string} code comment/string-free source
|
|
256
263
|
* @param {number} open index of `{`
|
|
257
|
-
* @returns {{ keys: Array<{ name: string, start: number, end: number }>, opaque: boolean }}
|
|
264
|
+
* @returns {{ keys: Array<{ name: string, start: number, end: number, entryEnd: number }>, opaque: boolean }}
|
|
258
265
|
*/
|
|
259
266
|
const literalKeySpans = (code, open) => {
|
|
260
267
|
const keys = [];
|
|
@@ -268,6 +275,7 @@ const literalKeySpans = (code, open) => {
|
|
|
268
275
|
const at = entryStart + (text ? lead : 0);
|
|
269
276
|
current = "";
|
|
270
277
|
if (!text) return;
|
|
278
|
+
const entryEnd = at + text.length;
|
|
271
279
|
if (text.startsWith("...")) {
|
|
272
280
|
opaque = true;
|
|
273
281
|
return;
|
|
@@ -275,9 +283,9 @@ const literalKeySpans = (code, open) => {
|
|
|
275
283
|
const key = /^(?:async\s+)?['"]?([A-Za-z_$][\w$]*)['"]?\s*:/.exec(text);
|
|
276
284
|
if (key) {
|
|
277
285
|
const start = at + text.indexOf(key[1]);
|
|
278
|
-
keys.push({ name: key[1], start, end: start + key[1].length });
|
|
286
|
+
keys.push({ name: key[1], start, end: start + key[1].length, entryEnd });
|
|
279
287
|
} else if (/^[A-Za-z_$][\w$]*$/.test(text)) {
|
|
280
|
-
keys.push({ name: text, start: at, end: at + text.length });
|
|
288
|
+
keys.push({ name: text, start: at, end: at + text.length, entryEnd });
|
|
281
289
|
}
|
|
282
290
|
};
|
|
283
291
|
for (let i = open; i < code.length; i++) {
|
|
@@ -518,20 +526,15 @@ const planImports = (file, code, api, allSources) => {
|
|
|
518
526
|
};
|
|
519
527
|
|
|
520
528
|
/**
|
|
521
|
-
*
|
|
529
|
+
* The widgets an app imported from flet-box, as `local name → exported name`.
|
|
522
530
|
*
|
|
523
|
-
*
|
|
524
|
-
*
|
|
525
|
-
*
|
|
526
|
-
* @
|
|
531
|
+
* Only capitalised exports are widgets; the rest of the barrel is functions
|
|
532
|
+
* and tools, and a call to one of those never carries a props object.
|
|
533
|
+
*
|
|
534
|
+
* @param {string} withStrings Neutralised source, strings kept.
|
|
535
|
+
* @returns {Map<string, string>}
|
|
527
536
|
*/
|
|
528
|
-
const
|
|
529
|
-
const edits = [];
|
|
530
|
-
const skipped = [];
|
|
531
|
-
const rel = path.relative(api.root, file);
|
|
532
|
-
const withStrings = neutralise(code, { keepStrings: true });
|
|
533
|
-
const body = neutralise(code);
|
|
534
|
-
|
|
537
|
+
const fletWidgetImports = (withStrings) => {
|
|
535
538
|
const localToWidget = new Map();
|
|
536
539
|
for (const match of withStrings.matchAll(IMPORT_RE)) {
|
|
537
540
|
if (match[2] !== "flet-box") continue;
|
|
@@ -546,6 +549,54 @@ const planProps = (file, code, api) => {
|
|
|
546
549
|
if (/^[A-Z]/.test(exported)) localToWidget.set(local, exported);
|
|
547
550
|
}
|
|
548
551
|
}
|
|
552
|
+
return localToWidget;
|
|
553
|
+
};
|
|
554
|
+
|
|
555
|
+
/**
|
|
556
|
+
* The value written after a key of an object literal, or null.
|
|
557
|
+
*
|
|
558
|
+
* @param {string} code Source with the strings kept — a neutralised string
|
|
559
|
+
* keeps its quotes but loses its content, and the content is the value.
|
|
560
|
+
* @param {{ end: number }} key The key's span.
|
|
561
|
+
* @returns {string|null}
|
|
562
|
+
*/
|
|
563
|
+
const keyValue = (code, key) => {
|
|
564
|
+
const match = /^\s*:\s*['"]?([^'"\s,}]+)/.exec(code.slice(key.end, key.end + 80));
|
|
565
|
+
return match ? match[1] : null;
|
|
566
|
+
};
|
|
567
|
+
|
|
568
|
+
/**
|
|
569
|
+
* The span that removes one entry of an object literal, comma included.
|
|
570
|
+
*
|
|
571
|
+
* @param {Array<{ start: number, end: number, entryEnd: number }>} keys
|
|
572
|
+
* @param {number} index The entry to remove.
|
|
573
|
+
* @returns {{ start: number, end: number }}
|
|
574
|
+
*/
|
|
575
|
+
const removalSpan = (keys, index) => {
|
|
576
|
+
const key = keys[index];
|
|
577
|
+
const next = keys[index + 1];
|
|
578
|
+
if (next) return { start: key.start, end: next.start };
|
|
579
|
+
const prev = keys[index - 1];
|
|
580
|
+
if (prev) return { start: prev.entryEnd, end: key.entryEnd };
|
|
581
|
+
return { start: key.start, end: key.entryEnd };
|
|
582
|
+
};
|
|
583
|
+
|
|
584
|
+
/**
|
|
585
|
+
* Plan the prop-key rewrites for one file.
|
|
586
|
+
*
|
|
587
|
+
* @param {string} file
|
|
588
|
+
* @param {string} code
|
|
589
|
+
* @param {object} api
|
|
590
|
+
* @returns {{ edits: Edit[], skipped: string[] }}
|
|
591
|
+
*/
|
|
592
|
+
const planProps = (file, code, api) => {
|
|
593
|
+
const edits = [];
|
|
594
|
+
const skipped = [];
|
|
595
|
+
const rel = path.relative(api.root, file);
|
|
596
|
+
const withStrings = neutralise(code, { keepStrings: true });
|
|
597
|
+
const body = neutralise(code);
|
|
598
|
+
|
|
599
|
+
const localToWidget = fletWidgetImports(withStrings);
|
|
549
600
|
|
|
550
601
|
for (const [local, exported] of localToWidget) {
|
|
551
602
|
const iface = `${exported}Props`;
|
|
@@ -579,6 +630,103 @@ const planProps = (file, code, api) => {
|
|
|
579
630
|
return { edits, skipped };
|
|
580
631
|
};
|
|
581
632
|
|
|
633
|
+
/**
|
|
634
|
+
* Plan the renames an app can make without touching a line by hand.
|
|
635
|
+
*
|
|
636
|
+
* A prop the framework still honours but no longer recommends is not broken
|
|
637
|
+
* code, so `check-api` has nothing to say about it — but every screen that
|
|
638
|
+
* keeps the old name keeps an old spelling forever. The table in
|
|
639
|
+
* `src/widget-factory/deprecations.js` is the list, and it is per widget
|
|
640
|
+
* because the same name means different things in different widgets.
|
|
641
|
+
*
|
|
642
|
+
* The rewrite only happens when it is provably the same call: the widget has to
|
|
643
|
+
* declare the old name, it has to declare the new one, and the call must not
|
|
644
|
+
* already carry the new one (two callbacks for one event is a decision, not a
|
|
645
|
+
* rename).
|
|
646
|
+
*
|
|
647
|
+
* A row with no target (`to: null`) is a prop nobody reads any more: the key
|
|
648
|
+
* is removed, together with the comma that held it.
|
|
649
|
+
*
|
|
650
|
+
* @param {string} file
|
|
651
|
+
* @param {string} code
|
|
652
|
+
* @param {object} api
|
|
653
|
+
* @returns {{ edits: Edit[], skipped: string[] }}
|
|
654
|
+
*/
|
|
655
|
+
const planMigrations = (file, code, api) => {
|
|
656
|
+
const edits = [];
|
|
657
|
+
const skipped = [];
|
|
658
|
+
const rel = path.relative(api.root, file);
|
|
659
|
+
const withStrings = neutralise(code, { keepStrings: true });
|
|
660
|
+
const body = neutralise(code);
|
|
661
|
+
|
|
662
|
+
for (const [local, exported] of fletWidgetImports(withStrings)) {
|
|
663
|
+
const iface = `${exported}Props`;
|
|
664
|
+
if (!api.interfaces.has(iface)) continue;
|
|
665
|
+
const declared = resolveInterfaceProps(api.interfaces, iface);
|
|
666
|
+
const callRe = new RegExp(`\\b${escapeRe(local)}\\s*\\(\\s*\\{`, "g");
|
|
667
|
+
let call;
|
|
668
|
+
while ((call = callRe.exec(body))) {
|
|
669
|
+
const open = call.index + call[0].length - 1;
|
|
670
|
+
const { keys, opaque } = literalKeySpans(body, open);
|
|
671
|
+
if (opaque) continue;
|
|
672
|
+
const byName = new Map(keys.map((entry) => [entry.name, entry]));
|
|
673
|
+
|
|
674
|
+
for (const key of keys) {
|
|
675
|
+
const rule = deprecationFor(exported, key.name);
|
|
676
|
+
if (!rule || !rule.to) continue;
|
|
677
|
+
if (!declared.has(rule.from) || !declared.has(rule.to)) {
|
|
678
|
+
skipped.push(`${rel}: ${exported}({ ${key.name} }) — the widget does not declare the pair`);
|
|
679
|
+
continue;
|
|
680
|
+
}
|
|
681
|
+
const inert = (rule.guard || []).find((name) => byName.has(name));
|
|
682
|
+
if (inert) {
|
|
683
|
+
skipped.push(
|
|
684
|
+
`${rel}: ${exported}({ ${key.name} }) — ${inert} is already there, so the old name does nothing`,
|
|
685
|
+
);
|
|
686
|
+
continue;
|
|
687
|
+
}
|
|
688
|
+
let target = rule.to;
|
|
689
|
+
if (rule.alternate) {
|
|
690
|
+
const side = byName.get(rule.alternate.prop);
|
|
691
|
+
if (side && keyValue(withStrings, side) === rule.alternate.value) {
|
|
692
|
+
target = rule.alternate.to;
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
if (byName.has(target)) {
|
|
696
|
+
skipped.push(
|
|
697
|
+
`${rel}: ${exported}({ ${key.name} }) — ${target} is already there; two callbacks for one event is your call`,
|
|
698
|
+
);
|
|
699
|
+
continue;
|
|
700
|
+
}
|
|
701
|
+
edits.push({
|
|
702
|
+
start: key.start,
|
|
703
|
+
end: key.end,
|
|
704
|
+
text: target,
|
|
705
|
+
note: `${exported}({ ${c("brightRed", key.name)} }) → ${c("brightGreen", target)}`,
|
|
706
|
+
kind: "migration",
|
|
707
|
+
});
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
// The removals run after the renames: a prop that only meant something
|
|
711
|
+
// next to another one goes away with it.
|
|
712
|
+
for (let index = 0; index < keys.length; index++) {
|
|
713
|
+
const key = keys[index];
|
|
714
|
+
const rule = deprecationFor(exported, key.name);
|
|
715
|
+
if (!rule || rule.to) continue;
|
|
716
|
+
if (!declared.has(rule.from)) continue;
|
|
717
|
+
edits.push({
|
|
718
|
+
...removalSpan(keys, index),
|
|
719
|
+
text: "",
|
|
720
|
+
note: `${exported}({ ${c("brightRed", key.name)} }) → ${c("brightGreen", "removed")}`,
|
|
721
|
+
kind: "migration",
|
|
722
|
+
});
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
return { edits, skipped };
|
|
728
|
+
};
|
|
729
|
+
|
|
582
730
|
/**
|
|
583
731
|
* Apply planned edits to one file.
|
|
584
732
|
*
|
|
@@ -615,13 +763,14 @@ export const auditFix = async (argv = [], io = {}) => {
|
|
|
615
763
|
return i >= 0 && argv[i + 1] ? path.resolve(argv[i + 1]) : process.cwd();
|
|
616
764
|
})();
|
|
617
765
|
const onlyFlag = argv.includes("--only") ? argv[argv.indexOf("--only") + 1] : null;
|
|
618
|
-
if (onlyFlag && !["imports", "props"].includes(onlyFlag)) {
|
|
619
|
-
print(c("red", `--only expects "imports" or "
|
|
766
|
+
if (onlyFlag && !["imports", "props", "migrations"].includes(onlyFlag)) {
|
|
767
|
+
print(c("red", `--only expects "imports", "props" or "migrations", got "${onlyFlag}"`));
|
|
620
768
|
return { exitCode: 1, changed: [], untouched: [] };
|
|
621
769
|
}
|
|
622
770
|
const only = {
|
|
623
771
|
imports: !onlyFlag || onlyFlag === "imports",
|
|
624
772
|
props: !onlyFlag || onlyFlag === "props",
|
|
773
|
+
migrations: !onlyFlag || onlyFlag === "migrations",
|
|
625
774
|
};
|
|
626
775
|
|
|
627
776
|
let api;
|
|
@@ -637,7 +786,11 @@ export const auditFix = async (argv = [], io = {}) => {
|
|
|
637
786
|
const files = collectSources(root);
|
|
638
787
|
const changed = [];
|
|
639
788
|
const untouched = [];
|
|
640
|
-
const passes = [
|
|
789
|
+
const passes = [
|
|
790
|
+
only.imports && "imports",
|
|
791
|
+
only.props && "props",
|
|
792
|
+
only.migrations && "migrations",
|
|
793
|
+
].filter(Boolean);
|
|
641
794
|
|
|
642
795
|
print(banner("🔧 FletBox audit fix", `${files.length} file(s) · ${passes.join(" + ")}`));
|
|
643
796
|
|
|
@@ -651,6 +804,7 @@ export const auditFix = async (argv = [], io = {}) => {
|
|
|
651
804
|
const runs = [];
|
|
652
805
|
if (only.imports) runs.push(() => planImports(file, text, api, files));
|
|
653
806
|
if (only.props) runs.push(() => planProps(file, text, api));
|
|
807
|
+
if (only.migrations) runs.push(() => planMigrations(file, text, api));
|
|
654
808
|
for (const run of runs) {
|
|
655
809
|
const plan = run();
|
|
656
810
|
untouched.push(...plan.skipped);
|
|
@@ -673,7 +827,7 @@ export const auditFix = async (argv = [], io = {}) => {
|
|
|
673
827
|
|
|
674
828
|
print(
|
|
675
829
|
changed.length
|
|
676
|
-
? `\n${c("brightGreen", `${changed.length} file(s) rewritten`)} ${c("gray", "— run npm run
|
|
830
|
+
? `\n${c("brightGreen", `${changed.length} file(s) rewritten`)} ${c("gray", "— run npm run verify to confirm, and git diff to review.")}`
|
|
677
831
|
: c("gray", "\nNothing to rewrite — every import resolves and every prop is known."),
|
|
678
832
|
);
|
|
679
833
|
return { exitCode: 0, changed, untouched };
|
package/bin/commands/checkApi.js
CHANGED
|
@@ -37,6 +37,7 @@ import {
|
|
|
37
37
|
import { c, banner, section } from "../utils/colors.js";
|
|
38
38
|
import { neutralise } from "../utils/neutralise.js";
|
|
39
39
|
import { resolveLocalModule, readModuleExports } from "../utils/moduleExports.js";
|
|
40
|
+
import { deprecationFor } from "../../src/widget-factory/deprecations.js";
|
|
40
41
|
|
|
41
42
|
const require = createRequire(import.meta.url);
|
|
42
43
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
@@ -538,7 +539,7 @@ const ERROR_CODES = new Set([
|
|
|
538
539
|
"MISSING_EXPORT",
|
|
539
540
|
]);
|
|
540
541
|
|
|
541
|
-
const SEVERITY = {
|
|
542
|
+
export const SEVERITY = {
|
|
542
543
|
WRONG_STACK: "error",
|
|
543
544
|
UNKNOWN_EXPORT: "error",
|
|
544
545
|
UNKNOWN_PROP: "error",
|
|
@@ -600,7 +601,7 @@ const entry = (prop, info, api) => {
|
|
|
600
601
|
* @param {string} projectRoot
|
|
601
602
|
* @returns {string} written path
|
|
602
603
|
*/
|
|
603
|
-
const emitCheatsheet = (api, projectRoot) => {
|
|
604
|
+
export const emitCheatsheet = (api, projectRoot) => {
|
|
604
605
|
const widgetNames = api.exports
|
|
605
606
|
.filter((name) => /^[A-Z]/.test(name) && api.interfaces.has(`${name}Props`))
|
|
606
607
|
.sort();
|
|
@@ -614,6 +615,10 @@ const emitCheatsheet = (api, projectRoot) => {
|
|
|
614
615
|
`Regenerate with \`flet-box check-api --emit-api\`.`,
|
|
615
616
|
);
|
|
616
617
|
out.push(`Widgets: ${widgetNames.length} · engine props: ${api.ALL_PROPS.length}`);
|
|
618
|
+
out.push(
|
|
619
|
+
`A prop marked \`(deprecated → x)\` still works and is not the name to write; ` +
|
|
620
|
+
`\`npm run fix\` renames the call it can prove.`,
|
|
621
|
+
);
|
|
617
622
|
out.push(``);
|
|
618
623
|
out.push(`## Common props (every widget)`);
|
|
619
624
|
out.push(
|
|
@@ -629,7 +634,13 @@ const emitCheatsheet = (api, projectRoot) => {
|
|
|
629
634
|
[...own]
|
|
630
635
|
.map(([prop, info]) => {
|
|
631
636
|
const type = info.type.length > 46 ? `${info.type.slice(0, 43)}…` : info.type;
|
|
632
|
-
|
|
637
|
+
// The cheatsheet is the first file an agent reads, so a renamed prop has
|
|
638
|
+
// to say so here. Without the marker `onPress` and `onToggle` look like
|
|
639
|
+
// two equally good names, and half the calls built from this file carry
|
|
640
|
+
// the one the framework is trying to retire.
|
|
641
|
+
const rule = deprecationFor(name, prop);
|
|
642
|
+
const mark = rule ? ` (deprecated → ${rule.to ?? "removed"})` : "";
|
|
643
|
+
return `${prop}${info.optional ? "?" : ""}: ${type}${mark}`;
|
|
633
644
|
})
|
|
634
645
|
.join(" · "),
|
|
635
646
|
);
|