flet-box 1.0.6 → 1.0.7

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 (94) hide show
  1. package/README.md +28 -1
  2. package/bin/cli.js +77 -11
  3. package/bin/commands/auditFix.js +682 -0
  4. package/bin/commands/checkApi.js +7 -5
  5. package/bin/commands/create.js +40 -10
  6. package/bin/commands/createBundle.js +54 -1
  7. package/bin/commands/images.js +349 -0
  8. package/bin/commands/pruneAssets.js +170 -0
  9. package/bin/commands/release.js +34 -1
  10. package/bin/commands/runCloudflare.js +47 -9
  11. package/bin/commands/runServer.js +81 -24
  12. package/bin/commands/toPng.js +346 -0
  13. package/bin/commands/unpack.js +1173 -0
  14. package/bin/ota/loader.js +90 -0
  15. package/bin/utils/appSources.js +101 -0
  16. package/bin/utils/bundleConfig.js +166 -0
  17. package/bin/utils/dtsProps.js +147 -0
  18. package/bin/utils/imageTools.js +271 -0
  19. package/bin/utils/images/bmp.js +99 -0
  20. package/bin/utils/images/gif.js +244 -0
  21. package/bin/utils/images/index.js +163 -0
  22. package/bin/utils/images/jpeg.js +643 -0
  23. package/bin/utils/images/png.js +486 -0
  24. package/bin/utils/images/unsupported.js +29 -0
  25. package/bin/utils/neutralise.js +109 -6
  26. package/bin/utils/templates.js +307 -64
  27. package/docs/cli/README.md +82 -0
  28. package/docs/guides/online-updates.md +57 -9
  29. package/docs/widget/Accordion.md +1 -0
  30. package/docs/widget/AdMob.md +1 -0
  31. package/docs/widget/AdSense.md +1 -0
  32. package/docs/widget/AnimatedBox.md +1 -0
  33. package/docs/widget/AnimatedText.md +1 -0
  34. package/docs/widget/Audio.md +2 -1
  35. package/docs/widget/Avatar.md +12 -3
  36. package/docs/widget/Badge.md +1 -0
  37. package/docs/widget/Button.md +1 -0
  38. package/docs/widget/Card.md +1 -0
  39. package/docs/widget/Carousel.md +1 -0
  40. package/docs/widget/Chart.md +1 -0
  41. package/docs/widget/Checkbox.md +1 -0
  42. package/docs/widget/Chip.md +1 -0
  43. package/docs/widget/CircularBar.md +1 -0
  44. package/docs/widget/CircularChart.md +1 -0
  45. package/docs/widget/CodeViewer.md +1 -0
  46. package/docs/widget/Column.md +1 -0
  47. package/docs/widget/Container.md +1 -0
  48. package/docs/widget/DataTable.md +1 -0
  49. package/docs/widget/Divider.md +1 -0
  50. package/docs/widget/DraggBox.md +3 -2
  51. package/docs/widget/Dropdown.md +3 -0
  52. package/docs/widget/DroppBox.md +2 -1
  53. package/docs/widget/FloatingActionButton.md +1 -0
  54. package/docs/widget/GridView.md +1 -0
  55. package/docs/widget/Icon.md +1 -0
  56. package/docs/widget/Image.md +11 -3
  57. package/docs/widget/Input.md +1 -0
  58. package/docs/widget/InstallButton.md +1 -0
  59. package/docs/widget/ListTile.md +1 -0
  60. package/docs/widget/ListView.md +1 -0
  61. package/docs/widget/Maps.md +188 -0
  62. package/docs/widget/Markdown.md +1 -0
  63. package/docs/widget/MatrixRain.md +1 -0
  64. package/docs/widget/Pagination.md +1 -0
  65. package/docs/widget/ParallaxBox.md +1 -0
  66. package/docs/widget/ProgressBar.md +1 -0
  67. package/docs/widget/QRCode.md +1 -0
  68. package/docs/widget/README.md +12 -10
  69. package/docs/widget/Radio.md +1 -0
  70. package/docs/widget/Rating.md +1 -0
  71. package/docs/widget/Row.md +1 -0
  72. package/docs/widget/Skeleton.md +1 -0
  73. package/docs/widget/Slider.md +1 -0
  74. package/docs/widget/Stack.md +1 -0
  75. package/docs/widget/Stepper.md +1 -0
  76. package/docs/widget/Switch.md +1 -0
  77. package/docs/widget/Text.md +1 -0
  78. package/docs/widget/Tooltip.md +11 -13
  79. package/docs/widget/TreeView.md +1 -0
  80. package/docs/widget/Video.md +3 -2
  81. package/package.json +2 -2
  82. package/src/core/hmr-client.js +129 -34
  83. package/src/index.d.ts +132 -0
  84. package/src/index.js +1 -0
  85. package/src/navigations/AppBar.js +15 -4
  86. package/src/navigations/Drawer.js +9 -1
  87. package/src/widget-factory/translateProps.js +32 -1
  88. package/src/widgets/Avatar.js +228 -131
  89. package/src/widgets/Container.js +13 -0
  90. package/src/widgets/Dropdown.js +33 -1
  91. package/src/widgets/Image.js +102 -3
  92. package/src/widgets/Maps.js +1495 -0
  93. package/src/widgets/Tooltip.js +12 -2
  94. package/src/widgets/index.js +1 -0
@@ -0,0 +1,346 @@
1
+ /**
2
+ * @file bin/commands/toPng.js
3
+ * @description `flet-box to-png` — converts the images of a folder to PNG.
4
+ *
5
+ * JPEG, BMP, GIF and palette PNG files are decoded with the readers in
6
+ * `bin/utils/images` and re-written as PNG by `encodePng`, which chooses the
7
+ * layout (palette, grey, no alpha when none is used) and the row filters that
8
+ * make the smallest file for those exact pixels, using nothing outside Node core.
9
+ * That is what makes the command safe to ship inside a package that has no
10
+ * runtime dependencies, and it is also its honest limit: WebP, AVIF, TIFF, ICO
11
+ * and SVG are reported as declined, because reading them needs a codec or a
12
+ * rasteriser this repo does not have.
13
+ *
14
+ * Nothing is overwritten by default. A `logo.jpg` becomes `logo.png` beside it;
15
+ * if a `logo.png` already exists the file is skipped and said so, because that
16
+ * collision usually means someone already converted it by hand and the two
17
+ * files are not the same picture. `--replace` deletes the source, and only
18
+ * after the PNG it replaces has been read back and checked pixel for pixel.
19
+ * `--optimize` rewrites a PNG that is already there, and only when the new bytes
20
+ * are smaller and hold the same picture.
21
+ */
22
+
23
+ import fs from "fs";
24
+ import path from "path";
25
+ import { c, section, dot } from "../utils/colors.js";
26
+ import { decodeImage, encodePng, sniffFormat, UnsupportedImage } from "../utils/images/index.js";
27
+
28
+ const USAGE = `Usage: flet-box to-png [dir] [options]
29
+
30
+ dir Folder to convert (default: ./assets, or ./public/assets)
31
+
32
+ Options:
33
+ --out <dir> Write the PNGs there instead of beside the source
34
+ --optimize Rewrite files that already are PNG, and only when that makes them smaller
35
+ --replace Delete each source file after its PNG was written and read back
36
+ --force Overwrite an existing PNG with the same name
37
+ --recursive Descend into sub-folders (default: yes)
38
+ --flat Only the top level of the folder
39
+ --dry-run Report what would happen; write nothing
40
+ --quiet Only the summary line`;
41
+
42
+ /** Folders tried when no path is given, in order. */
43
+ const DEFAULT_DIRS = ["assets", "public/assets", "src/assets"];
44
+
45
+ /** Never wandered into, however deep it sits. */
46
+ const SKIPPED_DIRS = new Set(["node_modules", ".git", "dist", "build", ".cache"]);
47
+
48
+ /**
49
+ * Image-ish files to look at — including the formats this tool will decline.
50
+ *
51
+ * A `.webp` in an assets folder is exactly the asset the developer is asking
52
+ * about, so it is opened, sniffed and reported rather than passed over because
53
+ * its extension is not on a short list. The extension only picks what to read;
54
+ * the bytes still decide what it is.
55
+ */
56
+ const IMAGE_FILE = /\.(jpe?g|jfif|png|apng|gif|bmp|webp|avif|heic|heif|tif|tiff|ico|cur|svg)$/i;
57
+
58
+ /**
59
+ * Collects candidate files under `dir`.
60
+ *
61
+ * @param {string} dir
62
+ * @param {boolean} recursive
63
+ * @returns {string[]} Absolute paths, sorted.
64
+ */
65
+ const collect = (dir, recursive) => {
66
+ const found = [];
67
+ const walk = (current) => {
68
+ for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
69
+ const full = path.join(current, entry.name);
70
+ if (entry.isDirectory()) {
71
+ if (recursive && !SKIPPED_DIRS.has(entry.name)) walk(full);
72
+ continue;
73
+ }
74
+ if (entry.isFile() && IMAGE_FILE.test(entry.name)) found.push(full);
75
+ }
76
+ };
77
+ if (fs.existsSync(dir)) walk(dir);
78
+ return found.sort();
79
+ };
80
+
81
+ /**
82
+ * Byte counts as the report prints them.
83
+ *
84
+ * @param {number} bytes
85
+ * @returns {string}
86
+ */
87
+ const size = (bytes) =>
88
+ bytes >= 1024 * 1024 ? `${(bytes / 1024 / 1024).toFixed(1)} MB` : `${Math.max(1, Math.round(bytes / 1024))} KB`;
89
+
90
+ /**
91
+ * Decodes PNG bytes in memory and asks whether they are still this picture.
92
+ *
93
+ * The writer now chooses palettes and drops alpha channels, so "same size" is no
94
+ * longer evidence of anything: the test is that every pixel reads back equal.
95
+ * Doing it before the write is what lets `--optimize` refuse a rewrite instead of
96
+ * replacing a good asset with a plausible one.
97
+ *
98
+ * @param {Buffer} png - File bytes about to be written.
99
+ * @param {{ width: number, height: number, rgba: Buffer }} image
100
+ * @returns {boolean}
101
+ */
102
+ const pngHoldsThePicture = (png, image) => {
103
+ try {
104
+ const decoded = decodeImage(png);
105
+ return (
106
+ decoded.width === image.width &&
107
+ decoded.height === image.height &&
108
+ decoded.rgba.length === image.rgba.length &&
109
+ decoded.rgba.equals(image.rgba)
110
+ );
111
+ } catch {
112
+ return false;
113
+ }
114
+ };
115
+
116
+ /**
117
+ * Reads a file back and confirms it is the PNG that was just written.
118
+ *
119
+ * A truncated write is the failure mode worth guarding: `--replace` would
120
+ * otherwise delete the only copy of the picture.
121
+ *
122
+ * @param {string} file
123
+ * @param {{ width: number, height: number, rgba: Buffer }} expected
124
+ * @returns {boolean}
125
+ */
126
+ const writtenPngMatches = (file, expected) => {
127
+ try {
128
+ const buf = fs.readFileSync(file);
129
+ if (sniffFormat(buf) !== "png") return false;
130
+ return pngHoldsThePicture(buf, expected);
131
+ } catch {
132
+ return false;
133
+ }
134
+ };
135
+
136
+ /**
137
+ * Runs the conversion.
138
+ *
139
+ * @param {string[]} args - CLI arguments after the command name.
140
+ * @returns {Promise<{ exitCode: number }>} Non-zero only when a file failed to decode.
141
+ */
142
+ export const toPng = async (args = []) => {
143
+ const has = (flag) => args.includes(flag);
144
+ const valueOf = (flag) => {
145
+ const index = args.indexOf(flag);
146
+ return index >= 0 ? args[index + 1] : undefined;
147
+ };
148
+
149
+ if (has("--help") || has("-h")) {
150
+ console.log(USAGE);
151
+ return { exitCode: 0 };
152
+ }
153
+
154
+ const quiet = has("--quiet");
155
+ const dryRun = has("--dry-run");
156
+ const replace = has("--replace");
157
+ const force = has("--force");
158
+ const optimize = has("--optimize");
159
+ const recursive = !has("--flat");
160
+ const outDir = valueOf("--out");
161
+
162
+ const positional = args.filter((a, i) => !a.startsWith("--") && args[i - 1] !== "--out");
163
+ let target = positional[0] ? path.resolve(positional[0]) : null;
164
+ if (!target) {
165
+ const candidate = DEFAULT_DIRS.map((dir) => path.resolve(dir)).find((dir) => fs.existsSync(dir));
166
+ if (!candidate) {
167
+ console.log(c("red", `Nothing to convert: none of ${DEFAULT_DIRS.join(", ")} exists here.`));
168
+ console.log(c("gray", " Pass a folder: flet-box to-png <dir>"));
169
+ return { exitCode: 1 };
170
+ }
171
+ target = candidate;
172
+ }
173
+ if (!fs.existsSync(target) || !fs.statSync(target).isDirectory()) {
174
+ console.log(c("red", `Not a folder: ${target}`));
175
+ return { exitCode: 1 };
176
+ }
177
+ if (outDir) fs.mkdirSync(path.resolve(outDir), { recursive: true });
178
+
179
+ const files = collect(target, recursive);
180
+ const lines = [];
181
+ let converted = 0;
182
+ let declined = 0;
183
+ let skipped = 0;
184
+ let clash = 0;
185
+ let failed = 0;
186
+ let optimized = 0;
187
+ let optimal = 0;
188
+ let saved = 0;
189
+ let grew = 0;
190
+
191
+ for (const file of files) {
192
+ const relative = path.relative(process.cwd(), file) || path.basename(file);
193
+ let image;
194
+ let bytes;
195
+ try {
196
+ bytes = fs.readFileSync(file);
197
+ image = decodeImage(bytes);
198
+ } catch (error) {
199
+ if (error instanceof UnsupportedImage) {
200
+ declined++;
201
+ lines.push(` ${dot("#f59e0b")} ${relative} — ${c("yellow", "declined")} — ${error.message}`);
202
+ } else {
203
+ failed++;
204
+ lines.push(` ${dot("#ef4444")} ${relative} — ${c("red", "unreadable")} — ${error.message}`);
205
+ }
206
+ continue;
207
+ }
208
+
209
+ // Already PNG bytes: the only question is whether this writer can make them
210
+ // smaller, and `--optimize` is what asks it. A 16-bit source is left alone
211
+ // either way: the reader narrows it to the high byte, so rewriting would
212
+ // trade someone else's bit depth for a smaller file, and "smaller without
213
+ // losing quality" does not cover that.
214
+ if (image.format === "png" && !force) {
215
+ if (bytes.length > 25 && bytes[24] === 16) {
216
+ skipped++;
217
+ lines.push(` ${dot("#64748b")} ${relative} — 16-bit PNG, left as it is (${image.width}×${image.height})`);
218
+ continue;
219
+ }
220
+ if (!optimize) {
221
+ skipped++;
222
+ lines.push(` ${dot("#64748b")} ${relative} — already a PNG (${image.width}×${image.height})`);
223
+ continue;
224
+ }
225
+
226
+ const before = bytes.length;
227
+ const png = encodePng(image);
228
+ if (png.length >= before) {
229
+ optimal++;
230
+ lines.push(` ${dot("#64748b")} ${relative} — already the smallest this writer makes it (${size(before)})`);
231
+ continue;
232
+ }
233
+ if (!pngHoldsThePicture(png, image)) {
234
+ failed++;
235
+ lines.push(` ${dot("#ef4444")} ${relative} — ${c("red", "failed")} — the rewrite does not read back as the same picture`);
236
+ continue;
237
+ }
238
+ saved += before - png.length;
239
+ if (dryRun) {
240
+ optimized++;
241
+ lines.push(` ${dot("#22d3ee")} ${relative} — ${size(before)} → ${size(png.length)}${c("dim", " [dry run]")}`);
242
+ continue;
243
+ }
244
+ // With no --out the rewrite replaces the file, and it does so by writing
245
+ // beside it and renaming: an in-place rewrite that stops halfways would
246
+ // leave the asset smaller *and* unreadable, with the original already gone.
247
+ const destination = outDir ? path.resolve(outDir, path.basename(file)) : file;
248
+ if (destination !== file) {
249
+ fs.writeFileSync(destination, png);
250
+ } else {
251
+ const temp = `${file}.fletbox-replace.tmp`;
252
+ try {
253
+ fs.writeFileSync(temp, png);
254
+ fs.renameSync(temp, file);
255
+ } catch (error) {
256
+ fs.rmSync(temp, { force: true });
257
+ failed++;
258
+ lines.push(` ${dot("#ef4444")} ${relative} — ${c("red", "failed")} — ${error.message}`);
259
+ continue;
260
+ }
261
+ }
262
+ optimized++;
263
+ lines.push(` ${dot("#34d399")} ${relative} — ${c("dim", `optimized ${size(before)} → ${size(png.length)} (−${size(before - png.length)})`)}`);
264
+ continue;
265
+ }
266
+
267
+ const destination = path.resolve(
268
+ outDir ? path.resolve(outDir) : path.dirname(file),
269
+ path.basename(file, path.extname(file)) + ".png",
270
+ );
271
+
272
+ // A file that is merely *named* .png but holds JPEG bytes would otherwise be
273
+ // overwritten in place, which is the one thing this command never does unless
274
+ // it is told to.
275
+ if (destination === file && !force) {
276
+ skipped++;
277
+ lines.push(` ${dot("#64748b")} ${relative} — already named .png (${image.width}×${image.height})`);
278
+ continue;
279
+ }
280
+ // Checked before the dry-run branch, so a rehearsal reports the files it
281
+ // would refuse instead of counting them as conversions waiting to happen.
282
+ if (destination !== file && fs.existsSync(destination) && !force) {
283
+ clash++;
284
+ lines.push(` ${dot("#f59e0b")} ${relative} — ${c("yellow", "kept")} — ${path.basename(destination)} already exists${dryRun ? c("dim", " [dry run]") : " (--force to replace it)"}`);
285
+ continue;
286
+ }
287
+
288
+ const png = encodePng({ width: image.width, height: image.height, rgba: image.rgba });
289
+ const note = image.note ? c("dim", ` (${image.note})`) : "";
290
+
291
+ // PNG keeps every pixel, so the file this writes for a photograph is several
292
+ // times the JPEG it came from. That is the format, not a bug, and the report
293
+ // says it in numbers instead of letting someone discover it in a deploy.
294
+ const sourceSize = bytes.length;
295
+ if (png.length > sourceSize) grew++;
296
+ const sizeNote = c("dim", png.length > sourceSize
297
+ ? `${size(png.length)} · ${(png.length / sourceSize).toFixed(1)}× the ${image.format} it came from`
298
+ : size(png.length));
299
+
300
+ if (dryRun) {
301
+ converted++;
302
+ lines.push(` ${dot("#22d3ee")} ${relative} → ${path.relative(process.cwd(), destination) || destination} ${image.width}×${image.height} ${sizeNote}${note} ${c("dim", "[dry run]")}`);
303
+ continue;
304
+ }
305
+
306
+ fs.writeFileSync(destination, png);
307
+ if (!writtenPngMatches(destination, image)) {
308
+ failed++;
309
+ lines.push(` ${dot("#ef4444")} ${relative} — ${c("red", "failed")} — the PNG written back does not match`);
310
+ continue;
311
+ }
312
+
313
+ let removed = "";
314
+ if (replace && destination !== file) {
315
+ fs.rmSync(file);
316
+ removed = c("dim", " · source removed");
317
+ }
318
+ converted++;
319
+ lines.push(` ${dot("#34d399")} ${relative} → ${path.relative(process.cwd(), destination) || destination} ${image.width}×${image.height} ${sizeNote}${note}${removed}`);
320
+ }
321
+
322
+ if (!quiet) {
323
+ console.log(section("🖼", `to-png — ${path.relative(process.cwd(), target) || "."}`));
324
+ for (const line of lines) console.log(line);
325
+ }
326
+ if (!files.length) console.log(c("gray", " No image files in that folder."));
327
+
328
+ const parts = [`${converted} converted`, `${declined} declined`, `${skipped} already png`];
329
+ if (optimized) parts.push(`${optimized} optimized`);
330
+ if (optimal) parts.push(`${optimal} already optimal`);
331
+ if (saved) parts.push(c("green", `−${size(saved)}`));
332
+ if (clash) parts.push(`${clash} name clash`);
333
+ if (failed) parts.push(`${failed} failed`);
334
+ console.log(`\n ${parts.join(" · ")}${dryRun ? c("yellow", " · nothing written (--dry-run)") : ""}\n`);
335
+
336
+ // The advice belongs with the count, not with ninety individual lines.
337
+ if (grew) {
338
+ console.log(c("yellow", ` ${grew} of the written PNG${grew === 1 ? " is" : "s are"} bigger than the file it came from.`));
339
+ console.log(c("gray", " A PNG stores every pixel, so a photograph is never small in it: keep the JPEG (or a WebP from a tool with a codec) for that picture and let this convert the flat art, logos and icons."));
340
+ console.log("");
341
+ }
342
+
343
+ return { exitCode: failed ? 1 : 0 };
344
+ };
345
+
346
+ export default toPng;