flet-box 1.0.4 → 1.0.5

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 (147) hide show
  1. package/README.md +10 -0
  2. package/bin/cli.js +15 -0
  3. package/bin/commands/checkApi.js +761 -0
  4. package/bin/commands/create.js +77 -5
  5. package/bin/utils/dtsProps.js +191 -0
  6. package/bin/utils/moduleExports.js +165 -0
  7. package/bin/utils/neutralise.js +64 -0
  8. package/bin/utils/templates.js +366 -27
  9. package/docs/CONTRIBUTING.md +156 -0
  10. package/docs/README.md +68 -0
  11. package/docs/arquitectura.md +164 -0
  12. package/docs/cli/README.md +542 -0
  13. package/docs/guides/README.md +33 -0
  14. package/docs/guides/app-templates.md +197 -0
  15. package/docs/guides/compatibility.md +95 -0
  16. package/docs/guides/embedding.md +284 -0
  17. package/docs/guides/first-app.md +208 -0
  18. package/docs/guides/frontend-services.md +171 -0
  19. package/docs/guides/minification-and-protection.md +260 -0
  20. package/docs/guides/mobile-and-platforms.md +439 -0
  21. package/docs/guides/online-updates.md +196 -0
  22. package/docs/guides/router.md +282 -0
  23. package/docs/guides/state.md +283 -0
  24. package/docs/guides/syntax-philosophy.md +150 -0
  25. package/docs/guides/utilities.md +241 -0
  26. package/docs/guides/widget-structure.md +163 -0
  27. package/docs/server/README.md +68 -0
  28. package/docs/server/START_HERE.md +121 -0
  29. package/docs/server/api.md +225 -0
  30. package/docs/server/data-services.md +300 -0
  31. package/docs/server/deployment.md +113 -0
  32. package/docs/server/security.md +196 -0
  33. package/docs/services/README.md +79 -0
  34. package/docs/services/RamStore.md +84 -0
  35. package/docs/services/Session.md +84 -0
  36. package/docs/services/Storage.md +88 -0
  37. package/docs/services/http.md +86 -0
  38. package/docs/tools/README.md +138 -0
  39. package/docs/tools/animation.md +87 -0
  40. package/docs/tools/async-ids-log.md +80 -0
  41. package/docs/tools/device.md +132 -0
  42. package/docs/tools/introspection.md +67 -0
  43. package/docs/tools/lists.md +96 -0
  44. package/docs/tools/markdown.md +79 -0
  45. package/docs/tools/reactivity.md +288 -0
  46. package/docs/tools/state.md +103 -0
  47. package/docs/tools/styling.md +92 -0
  48. package/docs/tools/text-and-time.md +89 -0
  49. package/docs/tools/theme.md +102 -0
  50. package/docs/tools/validation.md +75 -0
  51. package/docs/tools/visual-effects.md +84 -0
  52. package/docs/widget/Accordion.md +144 -0
  53. package/docs/widget/AdMob.md +144 -0
  54. package/docs/widget/AdSense.md +133 -0
  55. package/docs/widget/AdaptiveScaffold.md +105 -0
  56. package/docs/widget/AlertDialog.md +103 -0
  57. package/docs/widget/AnimatedBox.md +130 -0
  58. package/docs/widget/AnimatedText.md +123 -0
  59. package/docs/widget/AppBar.md +118 -0
  60. package/docs/widget/Audio.md +135 -0
  61. package/docs/widget/Avatar.md +119 -0
  62. package/docs/widget/Badge.md +128 -0
  63. package/docs/widget/BottomNavigation.md +116 -0
  64. package/docs/widget/BottomSheet.md +125 -0
  65. package/docs/widget/Button.md +137 -0
  66. package/docs/widget/COMMON_PROPS.md +150 -0
  67. package/docs/widget/Card.md +125 -0
  68. package/docs/widget/Carousel.md +151 -0
  69. package/docs/widget/Chart.md +151 -0
  70. package/docs/widget/Checkbox.md +118 -0
  71. package/docs/widget/Chip.md +126 -0
  72. package/docs/widget/CircularBar.md +168 -0
  73. package/docs/widget/CircularChart.md +164 -0
  74. package/docs/widget/CodeViewer.md +130 -0
  75. package/docs/widget/CollapsibleSideBar.md +112 -0
  76. package/docs/widget/Column.md +129 -0
  77. package/docs/widget/Container.md +136 -0
  78. package/docs/widget/DataTable.md +150 -0
  79. package/docs/widget/Divider.md +109 -0
  80. package/docs/widget/DraggBox.md +129 -0
  81. package/docs/widget/Drawer.md +135 -0
  82. package/docs/widget/DrawerItem.md +98 -0
  83. package/docs/widget/Dropdown.md +139 -0
  84. package/docs/widget/DroppBox.md +152 -0
  85. package/docs/widget/FloatingActionButton.md +120 -0
  86. package/docs/widget/GridView.md +140 -0
  87. package/docs/widget/Icon.md +117 -0
  88. package/docs/widget/Image.md +125 -0
  89. package/docs/widget/Input.md +151 -0
  90. package/docs/widget/Inspector.md +92 -0
  91. package/docs/widget/InstallButton.md +128 -0
  92. package/docs/widget/ListTile.md +132 -0
  93. package/docs/widget/ListView.md +138 -0
  94. package/docs/widget/Markdown.md +175 -0
  95. package/docs/widget/MatrixRain.md +122 -0
  96. package/docs/widget/Modal.md +129 -0
  97. package/docs/widget/Pagination.md +135 -0
  98. package/docs/widget/ParallaxBox.md +128 -0
  99. package/docs/widget/ProgressBar.md +130 -0
  100. package/docs/widget/QRCode.md +117 -0
  101. package/docs/widget/README.md +188 -0
  102. package/docs/widget/Radio.md +123 -0
  103. package/docs/widget/Rating.md +131 -0
  104. package/docs/widget/Row.md +131 -0
  105. package/docs/widget/START_HERE.md +146 -0
  106. package/docs/widget/Scaffold.md +152 -0
  107. package/docs/widget/Skeleton.md +130 -0
  108. package/docs/widget/Slider.md +146 -0
  109. package/docs/widget/SnackBar.md +102 -0
  110. package/docs/widget/Stack.md +128 -0
  111. package/docs/widget/Stepper.md +145 -0
  112. package/docs/widget/Switch.md +118 -0
  113. package/docs/widget/Tabs.md +124 -0
  114. package/docs/widget/Text.md +133 -0
  115. package/docs/widget/Tooltip.md +145 -0
  116. package/docs/widget/TreeView.md +176 -0
  117. package/docs/widget/Video.md +127 -0
  118. package/docs/widget/_template.md +61 -0
  119. package/package.json +3 -2
  120. package/src/animations/animate.js +180 -70
  121. package/src/animations/index.js +1 -0
  122. package/src/create-index.sh +1 -1
  123. package/src/index.d.ts +45 -22
  124. package/src/index.js +1 -0
  125. package/src/navigations/AdaptiveScaffold.js +11 -2
  126. package/src/navigations/BottomNavigation.js +10 -3
  127. package/src/navigations/Tabs.js +6 -2
  128. package/src/tools/useState.js +54 -63
  129. package/src/widget-factory/assignProps.js +23 -1
  130. package/src/widget-factory/processProps.js +10 -3
  131. package/src/widget-factory/reactiveValue.js +43 -18
  132. package/src/widget-factory/translateProps.js +777 -901
  133. package/src/widgets/AlertDialog.js +7 -5
  134. package/src/widgets/Checkbox.js +9 -7
  135. package/src/widgets/CircularBar.js +12 -1
  136. package/src/widgets/FloatingActionButton.js +4 -7
  137. package/src/widgets/Image.js +5 -7
  138. package/src/widgets/Input.js +92 -0
  139. package/src/widgets/ListView.js +73 -29
  140. package/src/widgets/Pagination.js +6 -3
  141. package/src/widgets/ProgressBar.js +16 -13
  142. package/src/widgets/Radio.js +6 -7
  143. package/src/widgets/Slider.js +23 -4
  144. package/src/widgets/Stack.js +36 -3
  145. package/src/widgets/Stepper.js +4 -3
  146. package/src/widgets/Switch.js +3 -4
  147. package/src/widgets/TreeView.js +0 -1
@@ -0,0 +1,761 @@
1
+ // bin/commands/checkApi.js
2
+ // ============================================================
3
+ // FLETBOX API VALIDATOR — THE ANTI-HALLUCINATION GATE
4
+ // ============================================================
5
+ // An AI assistant editing an app invents APIs: `Card({ shadow: 3 })`,
6
+ // `import { Button } from 'react'`, `ListView({ items: [] })`. None of that
7
+ // throws — the factory drops unknown keys with a console warning, so the app
8
+ // silently renders the wrong thing and the user burns tokens re-reading files.
9
+ //
10
+ // This command compares the app against the framework that is actually
11
+ // installed, resolved from `node_modules/flet-box` (never from the network):
12
+ //
13
+ // • the export list — from the runtime barrel, imported in Node
14
+ // • the engine props — from `widget-factory/translateProps.js` `ALL_PROPS`
15
+ // • the declared API — from the shipped `index.d.ts` interfaces
16
+ //
17
+ // and statically scans the app's own sources for calls of those imports.
18
+ // Everything it reports is a call site that cannot work as written.
19
+ //
20
+ // Usage:
21
+ // flet-box check-api validate this project (exit 1 on errors)
22
+ // flet-box check-api --props Button print one widget's real contract
23
+ // flet-box check-api --list list every widget and utility
24
+ // flet-box check-api --emit-api write .fletbox/api/cheatsheet.md
25
+ // flet-box check-api --json machine-readable report
26
+ // ============================================================
27
+
28
+ import fs from "node:fs";
29
+ import path from "node:path";
30
+ import { createRequire } from "node:module";
31
+ import { fileURLToPath, pathToFileURL } from "node:url";
32
+ import {
33
+ parseInterfaces,
34
+ resolveInterfaceProps,
35
+ } from "../utils/dtsProps.js";
36
+ import { c, banner, section } from "../utils/colors.js";
37
+ import { neutralise } from "../utils/neutralise.js";
38
+ import { resolveLocalModule, readModuleExports } from "../utils/moduleExports.js";
39
+
40
+ const require = createRequire(import.meta.url);
41
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
42
+ const CLI_PACKAGE_DIR = path.join(__dirname, "..", "..");
43
+
44
+ /** Keys the factory itself reads, so they are never "invented props". */
45
+ const STRUCTURAL_PROPS = new Set([
46
+ "child",
47
+ "children",
48
+ "tag",
49
+ "widgetName",
50
+ "style",
51
+ "update",
52
+ "getProps",
53
+ "onMount",
54
+ "onUnmount",
55
+ "triggerMount",
56
+ "_cleanup",
57
+ "key",
58
+ ]);
59
+
60
+ /** Stacks that must never appear in a FletBox app — the point is zero deps. */
61
+ const FOREIGN_PACKAGES = [
62
+ "react",
63
+ "react-dom",
64
+ "next",
65
+ "vue",
66
+ "nuxt",
67
+ "svelte",
68
+ "solid-js",
69
+ "preact",
70
+ "angular",
71
+ "@angular/core",
72
+ "jquery",
73
+ "lit",
74
+ "lit-html",
75
+ "alpinejs",
76
+ "bootstrap",
77
+ "tailwindcss",
78
+ ];
79
+
80
+ const FOREIGN_EXTENSIONS = [".jsx", ".tsx", ".vue", ".svelte", ".angular"];
81
+
82
+ /**
83
+ * Locate the installed `flet-box` package the app should be validated against.
84
+ *
85
+ * @param {string} projectRoot
86
+ * @returns {{ dir: string, pkg: object }}
87
+ */
88
+ const resolvePackage = (projectRoot) => {
89
+ const candidates = [];
90
+ const same = (a, b) => {
91
+ try {
92
+ return fs.realpathSync(a) === fs.realpathSync(b);
93
+ } catch {
94
+ return false;
95
+ }
96
+ };
97
+ // Inside the framework repository the CLI package IS the framework, so its
98
+ // sources are the truth; a stale `node_modules/flet-box` copy (build residue)
99
+ // would otherwise validate the app against an older version.
100
+ const isFrameworkRepo =
101
+ fs.existsSync(path.join(CLI_PACKAGE_DIR, "src", "widget-factory", "translateProps.js")) &&
102
+ same(projectRoot, CLI_PACKAGE_DIR);
103
+ if (isFrameworkRepo) candidates.push(CLI_PACKAGE_DIR);
104
+ candidates.push(path.join(projectRoot, "node_modules", "flet-box"));
105
+ try {
106
+ candidates.push(path.dirname(require.resolve("flet-box/package.json")));
107
+ } catch {
108
+ /* not installed as a dependency — fall through */
109
+ }
110
+ if (!isFrameworkRepo) candidates.push(CLI_PACKAGE_DIR);
111
+
112
+ for (const dir of candidates) {
113
+ const ready =
114
+ fs.existsSync(path.join(dir, "package.json")) &&
115
+ fs.existsSync(path.join(dir, "src", "index.d.ts")) &&
116
+ fs.existsSync(path.join(dir, "src", "widget-factory", "translateProps.js"));
117
+ if (ready) {
118
+ return { dir, pkg: JSON.parse(fs.readFileSync(path.join(dir, "package.json"), "utf8")) };
119
+ }
120
+ }
121
+ throw new Error(
122
+ "Could not find an installed flet-box package. Run `npm install` in the project.",
123
+ );
124
+ };
125
+
126
+ /**
127
+ * Read the three surfaces of the installed framework.
128
+ *
129
+ * @param {string} projectRoot
130
+ * @returns {Promise<object>}
131
+ */
132
+ const loadApi = async (projectRoot) => {
133
+ const { dir, pkg } = resolvePackage(projectRoot);
134
+ const barrel = await import(pathToFileURL(path.join(dir, "src", "index.js")).href);
135
+ const registry = await import(
136
+ pathToFileURL(path.join(dir, "src", "widget-factory", "translateProps.js")).href
137
+ );
138
+ const dts = fs.readFileSync(path.join(dir, "src", "index.d.ts"), "utf8");
139
+ return {
140
+ dir,
141
+ pkg,
142
+ exports: Object.keys(barrel).filter((name) => name !== "default"),
143
+ PROP_MAP: registry.PROP_MAP,
144
+ ALL_PROPS: registry.ALL_PROPS,
145
+ interfaces: parseInterfaces(dts),
146
+ };
147
+ };
148
+
149
+ /**
150
+ * Edit distance, used only to answer "did you mean …?" cheaply.
151
+ *
152
+ * @param {string} a
153
+ * @param {string} b
154
+ * @returns {number}
155
+ */
156
+ const distance = (a, b) => {
157
+ if (a === b) return 0;
158
+ const m = a.length;
159
+ const n = b.length;
160
+ if (Math.abs(m - n) > 3) return 99;
161
+ let prev = Array.from({ length: n + 1 }, (_, i) => i);
162
+ for (let i = 1; i <= m; i++) {
163
+ const row = [i];
164
+ for (let j = 1; j <= n; j++) {
165
+ row[j] = Math.min(
166
+ prev[j] + 1,
167
+ row[j - 1] + 1,
168
+ prev[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1),
169
+ );
170
+ }
171
+ prev = row;
172
+ }
173
+ return prev[n];
174
+ };
175
+
176
+ /**
177
+ * Nearest candidate for a mistyped name.
178
+ *
179
+ * @param {string} value
180
+ * @param {Iterable<string>} candidates
181
+ * @returns {string | null}
182
+ */
183
+ const nearest = (value, candidates) => {
184
+ let best = null;
185
+ let bestScore = 4; // never suggest for names that are simply unrelated
186
+ const lower = value.toLowerCase();
187
+ for (const candidate of candidates) {
188
+ const score = distance(lower, candidate.toLowerCase());
189
+ if (score < bestScore) {
190
+ bestScore = score;
191
+ best = candidate;
192
+ }
193
+ }
194
+ return best;
195
+ };
196
+
197
+ /**
198
+ * Collect the source files an app is made of.
199
+ *
200
+ * @param {string} projectRoot
201
+ * @returns {string[]}
202
+ */
203
+ const collectSources = (projectRoot) => {
204
+ const files = [];
205
+ const walk = (dir) => {
206
+ let entries;
207
+ try {
208
+ entries = fs.readdirSync(dir, { withFileTypes: true });
209
+ } catch {
210
+ return;
211
+ }
212
+ for (const entry of entries) {
213
+ const full = path.join(dir, entry.name);
214
+ if (entry.isDirectory()) {
215
+ if (entry.name === "node_modules" || entry.name.startsWith(".")) continue;
216
+ walk(full);
217
+ } else if (/\.(m?js)$/.test(entry.name)) {
218
+ files.push(full);
219
+ } else if (FOREIGN_EXTENSIONS.some((ext) => entry.name.endsWith(ext))) {
220
+ files.push(full);
221
+ }
222
+ }
223
+ };
224
+ walk(path.join(projectRoot, "src"));
225
+ for (const rootFile of ["app.js", "main.js"]) {
226
+ const full = path.join(projectRoot, rootFile);
227
+ if (fs.existsSync(full)) files.push(full);
228
+ }
229
+ return files;
230
+ };
231
+
232
+ /**
233
+ * Read the keys of an object literal starting at its opening brace.
234
+ *
235
+ * @param {string} code Comment/string-free source.
236
+ * @param {number} open Index of `{`.
237
+ * @returns {{ keys: string[], opaque: boolean }} `opaque` when a top-level
238
+ * spread makes the real key set unknowable, so no missing prop is claimed.
239
+ */
240
+ const literalKeys = (code, open) => {
241
+ const keys = [];
242
+ let depth = 0;
243
+ let current = "";
244
+ let opaque = false;
245
+ const flush = () => {
246
+ const text = current.trim();
247
+ current = "";
248
+ if (!text) return;
249
+ if (text.startsWith("...")) {
250
+ opaque = true;
251
+ return;
252
+ }
253
+ const key = /^(?:async\s+)?['"]?([A-Za-z_$][\w$]*)['"]?\s*:/.exec(text);
254
+ if (key) keys.push(key[1]);
255
+ else if (/^[A-Za-z_$][\w$]*$/.test(text)) keys.push(text); // shorthand
256
+ };
257
+ for (let i = open; i < code.length; i++) {
258
+ const ch = code[i];
259
+ if (ch === "{") {
260
+ depth++;
261
+ if (depth === 1) continue;
262
+ } else if (ch === "}") {
263
+ depth--;
264
+ if (depth === 0) {
265
+ flush();
266
+ return { keys, opaque };
267
+ }
268
+ } else if (depth === 1 && ch === ",") {
269
+ flush();
270
+ continue;
271
+ }
272
+ if (depth >= 1) current += ch;
273
+ }
274
+ flush();
275
+ return { keys, opaque };
276
+ };
277
+
278
+ const lineOf = (source, index) =>
279
+ source.slice(0, index).split("\n").length;
280
+
281
+ /**
282
+ * Analyse one app file against the installed API.
283
+ *
284
+ * @param {string} file
285
+ * @param {object} api
286
+ * @returns {Array<object>} findings
287
+ */
288
+ const checkFile = (file, api) => {
289
+ const findings = [];
290
+ const raw = fs.readFileSync(file, "utf8");
291
+ const code = neutralise(raw);
292
+ // Module specifiers live in strings, so the import pass keeps them intact.
293
+ const importCode = neutralise(raw, { keepStrings: true });
294
+ const rel = path.relative(process.cwd(), file);
295
+
296
+ const add = (index, code_, message, suggestion) =>
297
+ findings.push({
298
+ file: rel,
299
+ line: lineOf(raw, index),
300
+ code: code_,
301
+ message,
302
+ suggestion: suggestion || null,
303
+ });
304
+
305
+ if (FOREIGN_EXTENSIONS.some((ext) => file.endsWith(ext))) {
306
+ add(0, "WRONG_STACK", "a React/Svelte/Vue-style file cannot run in FletBox");
307
+ return findings;
308
+ }
309
+
310
+ // ---- imports -------------------------------------------------------------
311
+ const localToExported = new Map();
312
+ const importRe =
313
+ /import\s+([\s\S]*?)\s*from\s*['"]([^'"]+)['"]/g;
314
+ let match;
315
+ while ((match = importRe.exec(importCode))) {
316
+ const clause = match[1];
317
+ const source = match[2];
318
+ const index = match.index;
319
+
320
+ const root = source.split("/")[0].startsWith("@")
321
+ ? source.split("/").slice(0, 2).join("/")
322
+ : source.split("/")[0];
323
+ if (FOREIGN_PACKAGES.includes(root)) {
324
+ add(
325
+ index,
326
+ "WRONG_STACK",
327
+ `imports "${source}" — FletBox apps are dependency-free vanilla JS`,
328
+ );
329
+ continue;
330
+ }
331
+ if (source === "flet-box") {
332
+ // handled below
333
+ } else if (source.startsWith("flet-box/")) {
334
+ add(
335
+ index,
336
+ "DEEP_IMPORT",
337
+ `deep import "${source}" is not a public entry point`,
338
+ "import from 'flet-box' instead",
339
+ );
340
+ continue;
341
+ } else if (source.startsWith(".") || source.startsWith("/")) {
342
+ // The app's own wiring. A name that is not exported here is `undefined`
343
+ // at runtime, which renders a blank screen and throws no error the checker
344
+ // could catch later, so it is verified against the target file's exports.
345
+ const target = resolveLocalModule(file, source);
346
+ if (!target) {
347
+ add(
348
+ index,
349
+ "MODULE_NOT_FOUND",
350
+ `"${source}" does not resolve to a file`,
351
+ "fix the path, or create the module",
352
+ );
353
+ continue;
354
+ }
355
+ const surface = readModuleExports(target, api.moduleCache);
356
+ const braces = /\{([^}]*)\}/.exec(clause);
357
+ const head = clause.replace(/\{[^}]*\}/g, "").replace(/\*[\s\S]*$/, "").trim();
358
+ const defaultName = /^([A-Za-z_$][\w$]*)\s*,?\s*$/.exec(head);
359
+ if (defaultName && !surface.hasDefault && !surface.opaque) {
360
+ const first = surface.named.values().next().value;
361
+ add(
362
+ index,
363
+ "MISSING_EXPORT",
364
+ `"${source}" has no default export, so ${defaultName[1]} is undefined at runtime`,
365
+ first ? `import { ${first} } from '${source}'` : null,
366
+ );
367
+ }
368
+ if (braces) {
369
+ for (const part of braces[1].split(",")) {
370
+ const token = part.trim();
371
+ if (!token) continue;
372
+ const aliased = /^(\S+)\s+as\s+(\S+)$/.exec(token);
373
+ const wanted = aliased ? aliased[1] : token;
374
+ if (surface.opaque || surface.named.has(wanted)) continue;
375
+ add(
376
+ index,
377
+ "MISSING_EXPORT",
378
+ `"${wanted}" is not exported by ${source}`,
379
+ nearest(wanted, [...surface.named]),
380
+ );
381
+ }
382
+ }
383
+ continue;
384
+ } else {
385
+ continue;
386
+ }
387
+
388
+ const braces = /\{([^}]*)\}/.exec(clause);
389
+ const names = [];
390
+ if (braces) {
391
+ for (const part of braces[1].split(",")) {
392
+ const token = part.trim();
393
+ if (!token) continue;
394
+ const aliased = /^(\S+)\s+as\s+(\S+)$/.exec(token);
395
+ names.push(aliased ? { exported: aliased[1], local: aliased[2] } : { exported: token, local: token });
396
+ }
397
+ }
398
+ const star = /\*\s+as\s+([A-Za-z_$][\w$]*)/.exec(clause);
399
+ if (star) {
400
+ // Namespace import: every `fb.Widget(...)` call is checked below.
401
+ names.forEach((n) => localToExported.set(`${star[1]}.${n.exported}`, n.exported));
402
+ }
403
+ for (const n of names) {
404
+ localToExported.set(n.local, n.exported);
405
+ if (!api.exports.includes(n.exported)) {
406
+ add(
407
+ index,
408
+ "UNKNOWN_EXPORT",
409
+ `"${n.exported}" is not exported by flet-box@${api.pkg.version}`,
410
+ nearest(n.exported, api.exports),
411
+ );
412
+ }
413
+ }
414
+ }
415
+
416
+ // ---- local re-exports ----------------------------------------------------
417
+ // `export { NewScreen } from './screens/NewScreen.js'` in a barrel is how an
418
+ // agent adds a screen; a typo there is invisible until the route renders blank.
419
+ const reExportRe = /export\s*\{([^}]*)\}\s*from\s*['"]([^'"]+)['"]/g;
420
+ let reExport;
421
+ while ((reExport = reExportRe.exec(importCode))) {
422
+ const source = reExport[2];
423
+ if (!source.startsWith(".") && !source.startsWith("/")) continue;
424
+ const target = resolveLocalModule(file, source);
425
+ if (!target) {
426
+ add(
427
+ reExport.index,
428
+ "MODULE_NOT_FOUND",
429
+ `"${source}" does not resolve to a file`,
430
+ "fix the path, or create the module",
431
+ );
432
+ continue;
433
+ }
434
+ const surface = readModuleExports(target, api.moduleCache);
435
+ for (const part of reExport[1].split(",")) {
436
+ const token = part.trim();
437
+ if (!token) continue;
438
+ const aliased = /^(\S+)\s+as\s+(\S+)$/.exec(token);
439
+ const wanted = aliased ? aliased[1] : token;
440
+ if (wanted === "default" || surface.opaque || surface.named.has(wanted)) continue;
441
+ add(
442
+ reExport.index,
443
+ "MISSING_EXPORT",
444
+ `cannot re-export "${wanted}" — ${source} does not export it`,
445
+ nearest(wanted, [...surface.named]),
446
+ );
447
+ }
448
+ }
449
+
450
+ // ---- call sites ----------------------------------------------------------
451
+ const widgetNames = new Set(
452
+ api.exports.filter((name) => /^[A-Z]/.test(name)),
453
+ );
454
+ for (const [local, exported] of localToExported) {
455
+ if (local.includes(".")) continue;
456
+ if (!widgetNames.has(exported)) continue;
457
+ const iface = api.interfaces.get(`${exported}Props`);
458
+ const callRe = new RegExp(`\\b${local.replace(/\$/g, "\\$")}\\s*\\(`, "g");
459
+ let call;
460
+ while ((call = callRe.exec(code))) {
461
+ const open = call.index + call[0].length;
462
+ const nextChar = code.slice(open).match(/^\s*(.)/);
463
+ if (!nextChar) continue;
464
+ if (nextChar[1] === "'" || nextChar[1] === '"' || nextChar[1] === "`") {
465
+ add(
466
+ call.index,
467
+ "POSITIONAL_ARG",
468
+ `${local}("…") — FletBox widgets take a props object`,
469
+ `${local}({ … })`,
470
+ );
471
+ continue;
472
+ }
473
+ if (nextChar[1] !== "{") continue;
474
+ if (!iface) continue; // no declared prop list: nothing to verify
475
+
476
+ const declared = resolveInterfaceProps(api.interfaces, `${exported}Props`);
477
+ const { keys, opaque } = literalKeys(code, open);
478
+ // A top-level `...spread` can carry props the caller built elsewhere, so
479
+ // claiming a key is missing would be a false positive.
480
+ if (opaque) continue;
481
+ for (const key of keys) {
482
+ if (declared.has(key) || STRUCTURAL_PROPS.has(key)) continue;
483
+ if (api.PROP_MAP[key]) {
484
+ const def = api.PROP_MAP[key];
485
+ // `style`/`attribute`/`event` keys are the universal CSS and DOM
486
+ // surface — every widget accepts them, so their absence from a
487
+ // per-widget interface is not a defect. `special`/`text` keys are
488
+ // widget-specific behaviour: a widget that never reads one silently
489
+ // drops it, which is the hallucination worth warning about.
490
+ if (def.type !== "special" && def.type !== "text") continue;
491
+ add(
492
+ call.index,
493
+ "UNDOCUMENTED_PROP",
494
+ `${exported}({ ${key} }) is a widget-specific engine prop but is not in ${exported}Props — check the widget reads it`,
495
+ nearest(key, [...declared.keys()]),
496
+ );
497
+ continue;
498
+ }
499
+ const elsewhere = [...api.interfaces.entries()]
500
+ .filter(([name, value]) => name.endsWith("Props") && value.props.has(key))
501
+ .map(([name]) => name.replace(/Props$/, ""));
502
+ add(
503
+ call.index,
504
+ "UNKNOWN_PROP",
505
+ `${exported}({ ${key} }) — the factory drops unknown keys, so nothing happens`,
506
+ elsewhere.length
507
+ ? `it is a prop of ${elsewhere.slice(0, 3).join(", ")}`
508
+ : nearest(key, [...declared.keys()]),
509
+ );
510
+ }
511
+ void opaque;
512
+ }
513
+ }
514
+ return findings;
515
+ };
516
+
517
+ /** Severity gate: these codes fail `npm run check`. */
518
+ const ERROR_CODES = new Set([
519
+ "WRONG_STACK",
520
+ "UNKNOWN_EXPORT",
521
+ "UNKNOWN_PROP",
522
+ "POSITIONAL_ARG",
523
+ "MODULE_NOT_FOUND",
524
+ "MISSING_EXPORT",
525
+ ]);
526
+
527
+ const SEVERITY = {
528
+ WRONG_STACK: "error",
529
+ UNKNOWN_EXPORT: "error",
530
+ UNKNOWN_PROP: "error",
531
+ POSITIONAL_ARG: "error",
532
+ MODULE_NOT_FOUND: "error",
533
+ MISSING_EXPORT: "error",
534
+ UNDOCUMENTED_PROP: "warn",
535
+ DEEP_IMPORT: "warn",
536
+ };
537
+
538
+ /**
539
+ * Print one widget's real contract, from the installed package.
540
+ *
541
+ * @param {object} api
542
+ * @param {string} name
543
+ * @returns {string}
544
+ */
545
+ const printProps = (api, name) => {
546
+ const ifaceName = `${name}Props`;
547
+ if (!api.interfaces.has(ifaceName)) {
548
+ const guess = nearest(name, api.exports.filter((n) => /^[A-Z]/.test(n)));
549
+ return `${c("red", `✗ ${name} is not a FletBox widget`)}${
550
+ guess ? `\n did you mean ${c("brightCyan", guess)}?` : ""
551
+ }\n list them all with: flet-box check-api --list`;
552
+ }
553
+ const own = api.interfaces.get(ifaceName).props;
554
+ const all = resolveInterfaceProps(api.interfaces, ifaceName);
555
+ const lines = [];
556
+ lines.push(`${c("bold", gradientSafe(name))} — ${own.size} own props, ${all.size} accepted\n`);
557
+ lines.push(`${c("gray", " own:")}`);
558
+ for (const [prop, info] of own) lines.push(` ${entry(prop, info, api)}`);
559
+ const inherited = [...all].filter(([prop]) => !own.has(prop));
560
+ if (inherited.length) {
561
+ lines.push(`${c("gray", ` inherited (${inherited.length}):`)}`);
562
+ for (const [prop, info] of inherited) lines.push(` ${entry(prop, info, api)}`);
563
+ }
564
+ const widget = path.join(api.dir, "docs", "widget", `${name}.md`);
565
+ if (fs.existsSync(widget)) {
566
+ lines.push(`\n ${c("gray", `semantics + examples: ${widget}`)}`);
567
+ }
568
+ return lines.join("\n");
569
+ };
570
+
571
+ const gradientSafe = (text) => text;
572
+
573
+ const entry = (prop, info, api) => {
574
+ const def = api.PROP_MAP[prop];
575
+ const detail = def
576
+ ? `${def.type}${def.unit && def.unit !== "none" ? `:${def.unit}` : ""}`
577
+ : "widget-read";
578
+ const type = info.type.length > 58 ? `${info.type.slice(0, 55)}…` : info.type;
579
+ return `${c("brightCyan", prop.padEnd(18))}${c("gray", detail.padEnd(14))}${type}`;
580
+ };
581
+
582
+ /**
583
+ * Generate the condensed, always-accurate API sheet the in-app agent reads once.
584
+ *
585
+ * @param {object} api
586
+ * @param {string} projectRoot
587
+ * @returns {string} written path
588
+ */
589
+ const emitCheatsheet = (api, projectRoot) => {
590
+ const widgetNames = api.exports
591
+ .filter((name) => /^[A-Z]/.test(name) && api.interfaces.has(`${name}Props`))
592
+ .sort();
593
+ const otherNames = api.exports.filter((name) => !/^[A-Z]/.test(name)).sort();
594
+ const common = api.interfaces.get("CommonProps");
595
+ const out = [];
596
+ out.push(`# FletBox API — generated, do not edit`);
597
+ out.push(``);
598
+ out.push(
599
+ `Source of truth: \`flet-box@${api.pkg.version}\` in \`node_modules/flet-box\`. ` +
600
+ `Regenerate with \`flet-box check-api --emit-api\`.`,
601
+ );
602
+ out.push(`Widgets: ${widgetNames.length} · engine props: ${api.ALL_PROPS.length}`);
603
+ out.push(``);
604
+ out.push(`## Common props (every widget)`);
605
+ out.push(
606
+ [...common.props.keys()]
607
+ .map((p) => `${p}${common.props.get(p).optional ? "?" : ""}`)
608
+ .join(" · "),
609
+ );
610
+ out.push(``);
611
+ for (const name of widgetNames) {
612
+ const own = api.interfaces.get(`${name}Props`).props;
613
+ out.push(
614
+ `## ${name} (${own.size})`,
615
+ [...own]
616
+ .map(([prop, info]) => {
617
+ const type = info.type.length > 46 ? `${info.type.slice(0, 43)}…` : info.type;
618
+ return `${prop}${info.optional ? "?" : ""}: ${type}`;
619
+ })
620
+ .join(" · "),
621
+ );
622
+ }
623
+ out.push(`## Utilities and services`);
624
+ out.push(otherNames.join(" · "));
625
+ out.push(``);
626
+
627
+ const dir = path.join(projectRoot, ".fletbox", "api");
628
+ fs.mkdirSync(dir, { recursive: true });
629
+ const target = path.join(dir, "cheatsheet.md");
630
+ fs.writeFileSync(target, out.join("\n"), "utf8");
631
+ return target;
632
+ };
633
+
634
+ /**
635
+ * `flet-box check-api` entry point.
636
+ *
637
+ * @param {string[]} argv Flags after the command name.
638
+ * @param {object} [io] Injection point for tests.
639
+ * @returns {Promise<{ findings: object[], exitCode: number }>}
640
+ */
641
+ export const checkApi = async (argv = [], io = {}) => {
642
+ const print = io.print ?? ((line) => console.log(line));
643
+ const root = (() => {
644
+ const i = argv.indexOf("--root");
645
+ return i >= 0 && argv[i + 1] ? path.resolve(argv[i + 1]) : process.cwd();
646
+ })();
647
+ const api = await loadApi(root);
648
+ // Shared across files so a barrel imported by ten screens is parsed once, and
649
+ // so a module cycle terminates on the cached (possibly partial) surface.
650
+ api.moduleCache = new Map();
651
+ const wanted = (flag) => {
652
+ const i = argv.indexOf(flag);
653
+ return i >= 0 ? argv[i + 1] : null;
654
+ };
655
+
656
+ if (argv.includes("--props")) {
657
+ const name = wanted("--props");
658
+ if (!name) throw new Error("Usage: flet-box check-api --props <Widget>");
659
+ print(printProps(api, name));
660
+ return { findings: [], exitCode: 0 };
661
+ }
662
+ if (argv.includes("--emit-api")) {
663
+ const target = emitCheatsheet(api, root);
664
+ print(`${c("green", "✅")} Wrote ${path.relative(root, target)}`);
665
+ return { findings: [], exitCode: 0 };
666
+ }
667
+ if (argv.includes("--list")) {
668
+ const widgets = api.exports.filter((n) => /^[A-Z]/.test(n)).sort();
669
+ print(
670
+ `${c("bold", `flet-box@${api.pkg.version}`)}\n` +
671
+ `${c("brightCyan", "widgets:")} ${widgets.join(", ")}\n` +
672
+ `${c("gray", "engine props:")} ${api.ALL_PROPS.length} · ` +
673
+ `declared interfaces: ${api.interfaces.size}`,
674
+ );
675
+ return { findings: [], exitCode: 0 };
676
+ }
677
+
678
+ const files = collectSources(root);
679
+ const findings = files.flatMap((file) => checkFile(file, api));
680
+ const errors = findings.filter((f) => ERROR_CODES.has(f.code));
681
+ const warns = findings.filter((f) => !ERROR_CODES.has(f.code));
682
+
683
+ if (argv.includes("--json")) {
684
+ print(
685
+ JSON.stringify({ package: api.pkg.version, scanned: files.length, findings }, null, 2),
686
+ );
687
+ return { findings, exitCode: errors.length ? 1 : 0 };
688
+ }
689
+
690
+ print(
691
+ banner(
692
+ "🔎 FletBox API check",
693
+ `flet-box@${api.pkg.version} · ${files.length} file(s) scanned`,
694
+ ),
695
+ );
696
+ if (!findings.length) {
697
+ print(
698
+ `\n${c("green", "✅ No invented APIs.")} ${widgetSummary(api)}\n`,
699
+ );
700
+ return { findings, exitCode: 0 };
701
+ }
702
+ const showFull = argv.includes("--full");
703
+ const byCode = new Map();
704
+ for (const finding of findings) {
705
+ if (!byCode.has(finding.code)) byCode.set(finding.code, []);
706
+ byCode.get(finding.code).push(finding);
707
+ }
708
+ for (const [code, list] of byCode) {
709
+ const isWarning = SEVERITY[code] !== "error";
710
+ const color = isWarning ? "yellow" : "red";
711
+ print(`\n${section("!", `${code} (${list.length}) — ${SEVERITY[code]}`)}`);
712
+ // Warnings repeat across call sites of the same widget; an agent only needs
713
+ // the distinct ones plus where to look, so errors stay itemised and
714
+ // warnings collapse by message.
715
+ const groups = new Map();
716
+ if (isWarning && !showFull) {
717
+ for (const f of list) {
718
+ if (!groups.has(f.message)) groups.set(f.message, []);
719
+ groups.get(f.message).push(f);
720
+ }
721
+ for (const [message, hits] of groups) {
722
+ const where = hits
723
+ .slice(0, 2)
724
+ .map((f) => `${f.file}:${f.line}`)
725
+ .join(", ");
726
+ print(
727
+ ` ${c(color, "·")} ${message} ${c("gray", `(${hits.length}×: ${where}${hits.length > 2 ? ", …" : ""})`)}`,
728
+ );
729
+ }
730
+ continue;
731
+ }
732
+ for (const f of list) {
733
+ print(
734
+ ` ${c(color, "·")} ${c("brightCyan", `${f.file}:${f.line}`)} ${f.message}` +
735
+ (f.suggestion ? ` ${c("gray", `→ ${f.suggestion}`)}` : ""),
736
+ );
737
+ }
738
+ }
739
+ const collapsed = warns.length && !showFull;
740
+ print(
741
+ `\n${c("gray", `${errors.length} error(s), ${warns.length} warning(s)`)}` +
742
+ (collapsed ? c("gray", " — add --full to list every warning site") : "") +
743
+ (errors.length
744
+ ? c(
745
+ "gray",
746
+ " — look up the real API with: flet-box check-api --props <Widget>",
747
+ )
748
+ : "") +
749
+ "\n",
750
+ );
751
+ return { findings, exitCode: errors.length ? 1 : 0 };
752
+ };
753
+
754
+ const widgetSummary = (api) =>
755
+ c(
756
+ "gray",
757
+ `${api.exports.filter((n) => /^[A-Z]/.test(n)).length} widgets, ` +
758
+ `${api.ALL_PROPS.length} engine props available offline.`,
759
+ );
760
+
761
+ export default checkApi;