flet-box 1.0.4 → 1.0.6

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 +23 -7
  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
@@ -17,6 +17,7 @@ import { fileURLToPath } from "url";
17
17
  import { createFile, copyFile, makeExecutable } from "../utils/helpers.js";
18
18
  import * as templates from "../utils/templates.js";
19
19
  import { parseGithubRepo } from "../utils/bundleConfig.js";
20
+ import { checkApi } from "../commands/checkApi.js";
20
21
  import { c, banner, gradient } from "../utils/colors.js";
21
22
 
22
23
  const __filename = fileURLToPath(import.meta.url);
@@ -179,6 +180,11 @@ export const createProject = async (projectName, rawArgs = []) => {
179
180
  "src",
180
181
  "src/assets/fonts",
181
182
  "src/database",
183
+ // AI agent contract: rules, memory and the generated API cheatsheet.
184
+ ".fletbox/api",
185
+ ".fletbox/memory/notes",
186
+ ".claude",
187
+ ".cursor/rules",
182
188
  ];
183
189
  const extraDirs =
184
190
  template === "blank" ? [] : ["src/screens", "src/components"];
@@ -291,16 +297,16 @@ export const createProject = async (projectName, rawArgs = []) => {
291
297
  path.join(projectPath, "src/screens/AboutScreen.js"),
292
298
  templates.aboutScreenJs(),
293
299
  );
300
+ // Barrel export so app.js and screens can import all screens from one place.
301
+ createFile(
302
+ path.join(projectPath, "src/screens/index.js"),
303
+ templates.screensIndexJs(template),
304
+ );
294
305
  if (template === "adaptive") {
295
306
  createFile(
296
307
  path.join(projectPath, "src/screens/ProfileScreen.js"),
297
308
  templates.profileScreenJs(),
298
309
  );
299
- // Barrel export so app.js can import all screens from one place.
300
- createFile(
301
- path.join(projectPath, "src/screens/index.js"),
302
- templates.screensIndexJs(),
303
- );
304
310
  }
305
311
  // Layout components (skipped for blank).
306
312
  if (template !== "blank") {
@@ -365,11 +371,70 @@ export const createProject = async (projectName, rawArgs = []) => {
365
371
  }
366
372
  }
367
373
 
374
+ // ---------------------------------------------------------------------------
375
+ // AI agent contract (AGENTS.md + pointers + memory + checker)
376
+ // ---------------------------------------------------------------------------
377
+ // Written into every app so an AI editing it looks APIs up instead of inventing
378
+ // them, and has a machine gate that proves it right or wrong offline.
379
+ const pkgVersion = (() => {
380
+ try {
381
+ return JSON.parse(
382
+ fs.readFileSync(path.join(sourcePackageDir, "package.json"), "utf8"),
383
+ ).version;
384
+ } catch {
385
+ return "unknown";
386
+ }
387
+ })();
388
+
389
+ createFile(
390
+ path.join(projectPath, "AGENTS.md"),
391
+ templates.agentsMd(finalName, template, pkgVersion),
392
+ );
393
+ createFile(
394
+ path.join(projectPath, ".claude/CLAUDE.md"),
395
+ templates.claudeMd(),
396
+ );
397
+ createFile(
398
+ path.join(projectPath, ".cursor/rules/fletbox.mdc"),
399
+ templates.cursorRules(),
400
+ );
401
+ createFile(
402
+ path.join(projectPath, ".fletbox/memory/MEMORY.md"),
403
+ templates.memoryIndexMd(finalName),
404
+ );
405
+ createFile(
406
+ path.join(projectPath, ".fletbox/memory/notes/baseline.md"),
407
+ templates.memoryBaselineMd(finalName, template, pkgVersion),
408
+ );
409
+ createFile(
410
+ path.join(projectPath, "scripts/check-app.mjs"),
411
+ templates.checkAppScript(finalName),
412
+ );
413
+
414
+ // Generate the API cheatsheet from the framework the CLI was installed with.
415
+ // `npm install` is not required here: check-api resolves the package sources
416
+ // from its own copy, so a fresh app can be validated fully offline.
417
+ await checkApi(["--emit-api", "--root", projectPath], {
418
+ print: (line) => console.log(line),
419
+ });
420
+
368
421
  makeExecutable(path.join(projectPath, "run.sh"));
369
422
 
370
423
  console.log(
371
424
  `\n${gradient(` ✅ Project "${finalName}" created successfully!`, "#34d399", "#22d3ee")}\n`,
372
425
  );
426
+
427
+ // Prove the starter code passes the gate the agent will be held to.
428
+ const { exitCode } = await checkApi(["--root", projectPath]);
429
+ if (exitCode !== 0) {
430
+ console.warn(
431
+ c(
432
+ "yellow",
433
+ `⚠️ Starter code reported issues — check-api exited ${exitCode}. Fix them before building on top.`,
434
+ ),
435
+ );
436
+ }
437
+
373
438
  console.log(` ${c("brightCyan", "cd")} ${finalName}`);
374
439
  console.log(` ${c("brightCyan", "npm install")}`);
375
440
  console.log(` ${c("brightCyan", "npm run dev")}\n`);
@@ -403,4 +468,11 @@ export const createProject = async (projectName, rawArgs = []) => {
403
468
  " Enable GitHub Pages (Settings → Pages → Source: GitHub Actions) for the web preview.\n",
404
469
  ),
405
470
  );
471
+ console.log(
472
+ c(
473
+ "gray",
474
+ "AI: AGENTS.md is the contract for any assistant editing this app.\n" +
475
+ " npm run check verifies every widget/prop against the installed package.\n",
476
+ ),
477
+ );
406
478
  };
@@ -0,0 +1,191 @@
1
+ // bin/utils/dtsProps.js
2
+ // ============================================================
3
+ // MINIMAL `.d.ts` INTERFACE READER FOR THE CLI VALIDATOR
4
+ // ============================================================
5
+ // A generated app cannot build TypeScript, so the CLI derives the declared API
6
+ // straight from the shipped `src/index.d.ts` text. This parser understands the
7
+ // subset the file actually uses:
8
+ //
9
+ // export interface ButtonProps extends CommonProps {
10
+ // text?: string;
11
+ // items: Array<{ icon: string; label?: string }>;
12
+ // }
13
+ // export interface GridViewProps extends Omit<ListViewProps, 'wrapItems'> {}
14
+ //
15
+ // It is deliberately forgiving: an unparseable member is skipped instead of
16
+ // throwing, because a partial API list still catches invented props, which is
17
+ // the whole point of `flet-box check-api`.
18
+ // ============================================================
19
+
20
+ /**
21
+ * Split a brace-matched region into top-level interface members.
22
+ *
23
+ * Semicolons and newlines only end a member when every bracket, brace, paren
24
+ * and type-argument list is closed, so `Array<{ a: number; b: number }>` stays
25
+ * one member instead of two.
26
+ *
27
+ * @param {string} body Text between the interface's outer braces.
28
+ * @returns {string[]} Member sources, comments still attached.
29
+ */
30
+ const splitMembers = (body) => {
31
+ const members = [];
32
+ let depth = 0;
33
+ let current = "";
34
+ for (let i = 0; i < body.length; i++) {
35
+ const ch = body[i];
36
+ // `=>` is an arrow, not a closed type-argument list: consuming the `>` here
37
+ // is what made every prop after the first callback type vanish.
38
+ if (ch === "=" && body[i + 1] === ">") {
39
+ current += "=>";
40
+ i++;
41
+ continue;
42
+ }
43
+ if (ch === "{" || ch === "[" || ch === "(" || ch === "<") depth++;
44
+ else if (ch === "}" || ch === "]" || ch === ")" || ch === ">") depth--;
45
+ else if (depth === 0 && (ch === ";" || ch === "\n")) {
46
+ if (current.trim()) members.push(current.trim());
47
+ current = "";
48
+ continue;
49
+ }
50
+ current += ch;
51
+ }
52
+ if (current.trim()) members.push(current.trim());
53
+ return members;
54
+ };
55
+
56
+ /**
57
+ * Parse `extends A, Omit<B, 'x' | 'y'>` into structured parents.
58
+ *
59
+ * @param {string} clause Everything between `extends` and the opening brace.
60
+ * @returns {Array<{ name: string, omit: string[] }>}
61
+ */
62
+ export const parseExtendsClause = (clause) => {
63
+ if (!clause) return [];
64
+ // The captured clause still carries the keyword itself; leaving it in would
65
+ // resolve a parent named `extends` and silently drop all inherited props.
66
+ const text = clause.replace(/^\s*extends\s+/i, "");
67
+ const parents = [];
68
+ let depth = 0;
69
+ let current = "";
70
+ const parts = [];
71
+ for (const ch of text) {
72
+ if (ch === "<") depth++;
73
+ else if (ch === ">") depth--;
74
+ if (ch === "," && depth === 0) {
75
+ parts.push(current);
76
+ current = "";
77
+ continue;
78
+ }
79
+ current += ch;
80
+ }
81
+ parts.push(current);
82
+ for (const raw of parts) {
83
+ const text = raw.trim();
84
+ if (!text) continue;
85
+ const omit = /^Omit\s*<\s*([A-Za-z_$][\w$]*)\s*,\s*([^>]*)>/.exec(text);
86
+ if (omit) {
87
+ parents.push({
88
+ name: omit[1],
89
+ omit: [...omit[2].matchAll(/['"]([^'"]+)['"]/g)].map((m) => m[1]),
90
+ });
91
+ continue;
92
+ }
93
+ const plain = /^([A-Za-z_$][\w$]*)/.exec(text);
94
+ if (plain) parents.push({ name: plain[1], omit: [] });
95
+ }
96
+ return parents;
97
+ };
98
+
99
+ /**
100
+ * Read every `interface`/`type X = { ... }` declaration out of a `.d.ts` source.
101
+ *
102
+ * @param {string} source Full file text.
103
+ * @returns {Map<string, { parents: Array<{ name: string, omit: string[] }>, props: Map<string, { type: string, optional: boolean }> }>}
104
+ */
105
+ export const parseInterfaces = (source) => {
106
+ const interfaces = new Map();
107
+ // Comments can contain `interface` in prose; strip them first so brace
108
+ // matching never starts on a documentation example.
109
+ const code = source.replace(/\/\*[\s\S]*?\*\//g, "").replace(/\/\/[^\n]*/g, "");
110
+ const header =
111
+ /(export\s+)?(?:declare\s+)?(?:interface\s+([A-Za-z_$][\w$]*)|type\s+([A-Za-z_$][\w$]*)\s*=\s*)([^{]*)\{/g;
112
+ let match;
113
+ while ((match = header.exec(code))) {
114
+ const name = match[2] || match[3];
115
+ if (!name) continue;
116
+ const clause = match[4] || "";
117
+ const open = match.index + match[0].length - 1;
118
+ let depth = 0;
119
+ let close = -1;
120
+ for (let i = open; i < code.length; i++) {
121
+ if (code[i] === "{") depth++;
122
+ else if (code[i] === "}") {
123
+ depth--;
124
+ if (depth === 0) {
125
+ close = i;
126
+ break;
127
+ }
128
+ }
129
+ }
130
+ if (close < 0) continue;
131
+ const props = new Map();
132
+ for (const member of splitMembers(code.slice(open + 1, close))) {
133
+ const field = /^['"]?([A-Za-z_$][\w$]*)['"]?\s*(\?)?\s*:\s*([\s\S]+)$/.exec(
134
+ member.replace(/\s+/g, " ").trim(),
135
+ );
136
+ if (!field) continue;
137
+ props.set(field[1], {
138
+ type: field[3].replace(/;$/, "").trim(),
139
+ optional: field[2] === "?",
140
+ });
141
+ }
142
+ interfaces.set(name, { parents: parseExtendsClause(clause), props });
143
+ header.lastIndex = close;
144
+ }
145
+ return interfaces;
146
+ };
147
+
148
+ /**
149
+ * Flatten an interface with its inheritance chain, honouring `Omit<...>`.
150
+ *
151
+ * @param {Map} interfaces Output of `parseInterfaces`.
152
+ * @param {string} name Interface to resolve.
153
+ * @param {Set<string>} [seen] Guard against a cyclic `extends`.
154
+ * @returns {Map<string, { type: string, optional: boolean, declaredBy: string }>}
155
+ */
156
+ export const resolveInterfaceProps = (interfaces, name, seen = new Set()) => {
157
+ const resolved = new Map();
158
+ if (seen.has(name)) return resolved;
159
+ seen.add(name);
160
+ const entry = interfaces.get(name);
161
+ if (!entry) return resolved;
162
+ for (const parent of entry.parents) {
163
+ const inherited = resolveInterfaceProps(interfaces, parent.name, seen);
164
+ for (const [prop, info] of inherited) {
165
+ if (parent.omit.includes(prop)) continue;
166
+ if (!resolved.has(prop)) resolved.set(prop, info);
167
+ }
168
+ }
169
+ for (const [prop, info] of entry.props) {
170
+ resolved.set(prop, { ...info, declaredBy: name });
171
+ }
172
+ return resolved;
173
+ };
174
+
175
+ /**
176
+ * Find the declaration that types a widget's props object.
177
+ *
178
+ * `AppBar` is typed by `AppBarProps`; a few widgets reuse another widget's
179
+ * interface, so the search also accepts the first `<Name>Props` that exists.
180
+ *
181
+ * @param {Map} interfaces
182
+ * @param {string} widgetName Exported widget name.
183
+ * @returns {string | null}
184
+ */
185
+ export const propsInterfaceFor = (interfaces, widgetName) => {
186
+ const direct = `${widgetName}Props`;
187
+ if (interfaces.has(direct)) return direct;
188
+ const alt = `${widgetName}Options`;
189
+ if (interfaces.has(alt)) return alt;
190
+ return null;
191
+ };
@@ -0,0 +1,165 @@
1
+ /**
2
+ * @file bin/utils/moduleExports.js
3
+ * @description Resolve a relative import to the export surface of the file it
4
+ * points at, so `check-api` can catch broken wiring between an app's own
5
+ * modules — the failure class a flet-box-only API check cannot see.
6
+ *
7
+ * This is a static reader, not a module loader: it never executes app code, so
8
+ * it works offline, in CI, and on files that would throw at import time.
9
+ */
10
+
11
+ import fs from "node:fs";
12
+ import path from "node:path";
13
+ import { neutralise } from "./neutralise.js";
14
+
15
+ const IDENT = "[A-Za-z_$][\\w$]*";
16
+
17
+ /**
18
+ * Resolve a relative/absolute specifier the way the browser ESM loader would,
19
+ * for the conventions this project enforces (explicit `.js` extensions).
20
+ *
21
+ * @param {string} fromFile Absolute path of the importing file.
22
+ * @param {string} spec The raw specifier, e.g. `./screens/HomeScreen.js`.
23
+ * @returns {string|null} Absolute path, or null when nothing matches.
24
+ */
25
+ export const resolveLocalModule = (fromFile, spec) => {
26
+ if (!spec.startsWith(".") && !spec.startsWith("/")) return null;
27
+ const base = spec.startsWith("/")
28
+ ? spec.slice(1)
29
+ : path.resolve(path.dirname(fromFile), spec);
30
+ const candidates = /\.(m?js|json)$/.test(base)
31
+ ? [base]
32
+ : [`${base}.js`, `${base}.mjs`, path.join(base, "index.js")];
33
+ for (const candidate of candidates) {
34
+ try {
35
+ if (fs.statSync(candidate).isFile()) return candidate;
36
+ } catch {
37
+ /* keep looking */
38
+ }
39
+ }
40
+ return null;
41
+ };
42
+
43
+ /**
44
+ * @typedef {object} ModuleSurface
45
+ * @property {Set<string>} named Named exports, including names re-exported from
46
+ * another module.
47
+ * @property {boolean} hasDefault Whether `export default` appears.
48
+ * @property {boolean} opaque True when the surface could not be read completely,
49
+ * so callers must not report missing names.
50
+ */
51
+
52
+ const emptySurface = (opaque) => ({
53
+ named: new Set(),
54
+ hasDefault: false,
55
+ opaque,
56
+ });
57
+
58
+ /**
59
+ * Read the export surface of a local module, following re-exports.
60
+ *
61
+ * @param {string} absFile Absolute path of the module.
62
+ * @param {Map<string, ModuleSurface>} [cache] Shared cache for the whole scan.
63
+ * @param {Set<string>} [chain] Modules being resolved right now, for cycles.
64
+ * @returns {ModuleSurface}
65
+ */
66
+ export const readModuleExports = (absFile, cache = new Map(), chain = new Set()) => {
67
+ if (cache.has(absFile)) return cache.get(absFile);
68
+ // index.js -> Screen.js -> index.js must not recurse forever. Returning an
69
+ // opaque throwaway keeps the caller from reporting names the cycle does export.
70
+ if (chain.has(absFile)) return emptySurface(true);
71
+
72
+ const surface = emptySurface(false);
73
+ let raw;
74
+ try {
75
+ raw = fs.readFileSync(absFile, "utf8");
76
+ } catch {
77
+ surface.opaque = true;
78
+ return surface;
79
+ }
80
+ const code = neutralise(raw, { keepStrings: true });
81
+
82
+ // export default / export default function|class NAME
83
+ if (new RegExp(`export\\s+default\\b`).test(code)) surface.hasDefault = true;
84
+
85
+ // export const|let|var|function|class NAME (incl. multi-declarator and async)
86
+ const declRe = new RegExp(
87
+ `export\\s+(?:async\\s+)?(?:const|let|var|function|class)\\s+(${IDENT})`,
88
+ "g",
89
+ );
90
+ let decl;
91
+ while ((decl = declRe.exec(code))) {
92
+ surface.named.add(decl[1]);
93
+ // `export const a = 1, b = 2;` — only accept a later name when it is
94
+ // immediately assigned, so type-ish commas cannot invent an export.
95
+ const tail = code.slice(decl.index + decl[0].length, code.indexOf(";", decl.index));
96
+ const extraRe = new RegExp(`,\\s*(${IDENT})\\s*=`, "g");
97
+ let extra;
98
+ while ((extra = extraRe.exec(tail))) surface.named.add(extra[1]);
99
+ }
100
+
101
+ // export { A, B as C } [from './mod.js']
102
+ const bracesRe = /export\s*\{([^}]*)\}(\s*from\s*['"]([^'"]+)['"])?/g;
103
+ let braces;
104
+ while ((braces = bracesRe.exec(code))) {
105
+ const source = braces[3];
106
+ let fromSurface = null;
107
+ if (source) {
108
+ const target = resolveLocalModule(absFile, source);
109
+ // A specifier that is not local (a package re-export) is taken on trust:
110
+ // guessing here would produce false alarms about the app's own files.
111
+ if (target) {
112
+ chain.add(absFile);
113
+ fromSurface = readModuleExports(target, cache, chain);
114
+ chain.delete(absFile);
115
+ } else if (!source.startsWith(".")) {
116
+ fromSurface = emptySurface(true);
117
+ }
118
+ }
119
+ for (const part of braces[1].split(",")) {
120
+ const token = part.trim();
121
+ if (!token) continue;
122
+ const aliased = new RegExp(`^(${IDENT})\\s+as\\s+(${IDENT})$`).exec(token);
123
+ const original = aliased ? aliased[1] : token;
124
+ const exportedAs = aliased ? aliased[2] : token;
125
+ if (original === "default") {
126
+ if (!source) surface.hasDefault = true;
127
+ else surface.named.add(exportedAs);
128
+ continue;
129
+ }
130
+ if (!source) {
131
+ surface.named.add(exportedAs);
132
+ } else if (fromSurface && !fromSurface.opaque) {
133
+ if (fromSurface.named.has(original)) surface.named.add(exportedAs);
134
+ } else {
135
+ surface.named.add(exportedAs);
136
+ }
137
+ }
138
+ }
139
+
140
+ // export * [as NS] from './mod.js'
141
+ const starRe = new RegExp(
142
+ `export\\s+\\*\\s*(?:as\\s+(${IDENT})\\s+)?from\\s*['"]([^'"]+)['"]`,
143
+ "g",
144
+ );
145
+ let star;
146
+ while ((star = starRe.exec(code))) {
147
+ if (star[1]) {
148
+ surface.named.add(star[1]);
149
+ continue;
150
+ }
151
+ const target = resolveLocalModule(absFile, star[2]);
152
+ if (!target) {
153
+ if (!star[2].startsWith(".")) surface.opaque = true;
154
+ continue;
155
+ }
156
+ chain.add(absFile);
157
+ const fromSurface = readModuleExports(target, cache, chain);
158
+ chain.delete(absFile);
159
+ for (const name of fromSurface.named) surface.named.add(name);
160
+ if (fromSurface.opaque) surface.opaque = true;
161
+ }
162
+
163
+ cache.set(absFile, surface);
164
+ return surface;
165
+ };
@@ -0,0 +1,64 @@
1
+ /**
2
+ * @file bin/utils/neutralise.js
3
+ * @description Comment/string stripper that preserves character offsets.
4
+ */
5
+
6
+ /**
7
+ * Strip comments, and optionally string bodies, keeping character offsets.
8
+ *
9
+ * Two passes are needed: imports must read their module specifier, while the
10
+ * call-site scanner must not see braces inside string literals. String contents
11
+ * are replaced by an equal-length filler so line numbers stay correct.
12
+ *
13
+ * @param {string} code
14
+ * @param {{ keepStrings?: boolean }} [options]
15
+ * @returns {string}
16
+ */
17
+ export const neutralise = (code, { keepStrings = false } = {}) => {
18
+ let out = "";
19
+ let i = 0;
20
+ while (i < code.length) {
21
+ const ch = code[i];
22
+ const next = code[i + 1];
23
+ if (ch === "/" && next === "/") {
24
+ while (i < code.length && code[i] !== "\n") {
25
+ out += " ";
26
+ i++;
27
+ }
28
+ continue;
29
+ }
30
+ if (ch === "/" && next === "*") {
31
+ while (i < code.length && !(code[i] === "*" && code[i + 1] === "/")) {
32
+ out += code[i] === "\n" ? "\n" : " ";
33
+ i++;
34
+ }
35
+ out += " ";
36
+ i += 2;
37
+ continue;
38
+ }
39
+ if (ch === '"' || ch === "'" || ch === "`") {
40
+ const quote = ch;
41
+ out += ch;
42
+ i++;
43
+ while (i < code.length) {
44
+ if (code[i] === "\\") {
45
+ out += keepStrings ? code.slice(i, i + 2) : " ";
46
+ i += 2;
47
+ continue;
48
+ }
49
+ if (code[i] === quote) {
50
+ out += quote;
51
+ i++;
52
+ break;
53
+ }
54
+ const fill = keepStrings ? code[i] : code[i] === "\n" ? "\n" : " ";
55
+ out += fill;
56
+ i++;
57
+ }
58
+ continue;
59
+ }
60
+ out += ch;
61
+ i++;
62
+ }
63
+ return out;
64
+ };