flet-box 1.0.7 → 1.0.9
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/README.md +3 -0
- package/bin/cli.js +13 -0
- package/bin/commands/checkApi.js +13 -1
- package/bin/commands/checkUi.js +1369 -0
- package/bin/commands/create.js +49 -5
- package/bin/commands/createBundle.js +21 -0
- package/bin/commands/runServer.js +11 -2
- package/bin/utils/pwaShell.js +137 -0
- package/bin/utils/templates.js +1398 -443
- package/docs/README.md +4 -0
- package/docs/cli/README.md +63 -2
- package/docs/guides/README.md +2 -0
- package/docs/guides/clean-ui.md +211 -0
- package/docs/guides/minification-and-protection.md +1 -1
- package/docs/guides/mobile-and-platforms.md +31 -0
- package/docs/guides/online-updates.md +48 -0
- package/docs/tools/README.md +1 -1
- package/docs/tools/animation.md +1 -1
- package/docs/tools/markdown.md +3 -3
- package/docs/tools/theme.md +42 -1
- package/docs/widget/Accordion.md +3 -0
- package/docs/widget/AdMob.md +4 -1
- package/docs/widget/AdSense.md +3 -0
- package/docs/widget/AnimatedBox.md +6 -3
- package/docs/widget/AnimatedText.md +3 -0
- package/docs/widget/Audio.md +3 -0
- package/docs/widget/Avatar.md +3 -0
- package/docs/widget/Badge.md +3 -0
- package/docs/widget/Button.md +3 -0
- package/docs/widget/COMMON_PROPS.md +52 -0
- package/docs/widget/Card.md +3 -0
- package/docs/widget/Carousel.md +3 -0
- package/docs/widget/Chart.md +3 -0
- package/docs/widget/Checkbox.md +3 -0
- package/docs/widget/Chip.md +3 -0
- package/docs/widget/CircularBar.md +3 -0
- package/docs/widget/CircularChart.md +3 -0
- package/docs/widget/CodeViewer.md +3 -0
- package/docs/widget/Column.md +3 -0
- package/docs/widget/Container.md +3 -0
- package/docs/widget/DataTable.md +3 -0
- package/docs/widget/Divider.md +3 -0
- package/docs/widget/DraggBox.md +3 -0
- package/docs/widget/Dropdown.md +3 -0
- package/docs/widget/DroppBox.md +3 -0
- package/docs/widget/FloatingActionButton.md +3 -0
- package/docs/widget/GridView.md +3 -0
- package/docs/widget/Icon.md +3 -0
- package/docs/widget/Image.md +3 -0
- package/docs/widget/Input.md +3 -0
- package/docs/widget/Inspector.md +2 -2
- package/docs/widget/InstallButton.md +3 -0
- package/docs/widget/ListTile.md +3 -0
- package/docs/widget/ListView.md +3 -0
- package/docs/widget/Maps.md +3 -0
- package/docs/widget/Markdown.md +3 -0
- package/docs/widget/MatrixRain.md +3 -0
- package/docs/widget/Pagination.md +3 -0
- package/docs/widget/ParallaxBox.md +3 -0
- package/docs/widget/ProgressBar.md +3 -0
- package/docs/widget/QRCode.md +3 -0
- package/docs/widget/README.md +2 -0
- package/docs/widget/Radio.md +4 -1
- package/docs/widget/Rating.md +3 -0
- package/docs/widget/Row.md +3 -0
- package/docs/widget/Skeleton.md +3 -0
- package/docs/widget/Slider.md +3 -0
- package/docs/widget/Stack.md +3 -0
- package/docs/widget/Stepper.md +3 -0
- package/docs/widget/Switch.md +3 -0
- package/docs/widget/Tabs.md +5 -5
- package/docs/widget/Text.md +3 -0
- package/docs/widget/Tooltip.md +3 -0
- package/docs/widget/TreeView.md +3 -0
- package/docs/widget/Video.md +3 -0
- package/package.json +2 -2
- package/src/animations/AnimatedBox.js +4 -1
- package/src/animations/MatrixRain.js +3 -3
- package/src/index.d.ts +64 -8
- package/src/index.js +1 -0
- package/src/navigations/AppBar.js +7 -12
- package/src/navigations/BottomNavigation.js +8 -13
- package/src/navigations/CollapsibleSideBar.js +30 -8
- package/src/navigations/Drawer.js +16 -14
- package/src/navigations/DrawerItem.js +3 -1
- package/src/navigations/Scaffold.js +26 -12
- package/src/navigations/Tabs.js +104 -59
- package/src/tools/color.js +54 -0
- package/src/tools/index.js +9 -1
- package/src/tools/scale.js +41 -0
- package/src/tools/shadow.js +20 -0
- package/src/utils/index.js +1 -0
- package/src/utils/readableInk.js +19 -0
- package/src/utils/syntaxHighlight.js +78 -10
- package/src/utils/themes.js +265 -58
- package/src/utils/units.js +33 -11
- package/src/widget-factory/effects.js +56 -5
- package/src/widget-factory/processProps.js +10 -1
- package/src/widget-factory/remTool.js +15 -29
- package/src/widget-factory/stackPosition.js +18 -3
- package/src/widget-factory/translateProps.js +28 -0
- package/src/widgets/Accordion.js +20 -23
- package/src/widgets/AdMob.js +7 -2
- package/src/widgets/Avatar.js +18 -12
- package/src/widgets/Badge.js +3 -2
- package/src/widgets/BottomSheet.js +14 -13
- package/src/widgets/Button.js +69 -30
- package/src/widgets/Card.js +6 -2
- package/src/widgets/Carousel.js +12 -10
- package/src/widgets/Chart.js +28 -5
- package/src/widgets/Checkbox.js +76 -18
- package/src/widgets/Chip.js +31 -10
- package/src/widgets/CircularBar.js +7 -7
- package/src/widgets/CircularChart.js +26 -8
- package/src/widgets/CodeViewer.js +93 -20
- package/src/widgets/DataTable.js +17 -11
- package/src/widgets/DraggBox.js +2 -1
- package/src/widgets/Dropdown.js +26 -13
- package/src/widgets/DroppBox.js +4 -2
- package/src/widgets/FloatingActionButton.js +13 -7
- package/src/widgets/Icon.js +3 -1
- package/src/widgets/Input.js +30 -23
- package/src/widgets/Inspector.js +54 -1
- package/src/widgets/InstallButton.js +6 -4
- package/src/widgets/ListTile.js +4 -3
- package/src/widgets/ListView.js +30 -12
- package/src/widgets/Markdown.js +37 -26
- package/src/widgets/Modal.js +14 -11
- package/src/widgets/Pagination.js +16 -11
- package/src/widgets/ProgressBar.js +6 -4
- package/src/widgets/Radio.js +73 -15
- package/src/widgets/Rating.js +24 -6
- package/src/widgets/Skeleton.js +31 -31
- package/src/widgets/Slider.js +29 -18
- package/src/widgets/SnackBar.js +35 -39
- package/src/widgets/Stepper.js +13 -9
- package/src/widgets/Switch.js +59 -18
- package/src/widgets/Tooltip.js +12 -10
- package/src/widgets/TreeView.js +27 -15
package/bin/commands/create.js
CHANGED
|
@@ -108,6 +108,21 @@ const askRepo = () =>
|
|
|
108
108
|
);
|
|
109
109
|
});
|
|
110
110
|
|
|
111
|
+
/**
|
|
112
|
+
* The GitHub Pages address a repo value implies, or null when the value is not
|
|
113
|
+
* a GitHub repo. Kept apart from `resolveOtaUrl` because the workflow header
|
|
114
|
+
* names the *Pages* site: a custom-domain OTA URL is not an address this file
|
|
115
|
+
* can publish to, so it must not be printed as one.
|
|
116
|
+
*
|
|
117
|
+
* @param {string|null} input - "user/repo" or a github.com URL.
|
|
118
|
+
* @returns {string|null} Base URL ending in "/", or null.
|
|
119
|
+
*/
|
|
120
|
+
const pagesSiteUrl = (input) => {
|
|
121
|
+
if (!input) return null;
|
|
122
|
+
const gh = parseGithubRepo(input.trim());
|
|
123
|
+
return gh ? `https://${gh.user}.github.io/${gh.repo}/` : null;
|
|
124
|
+
};
|
|
125
|
+
|
|
111
126
|
/**
|
|
112
127
|
* Normalizes the user's OTA target into a base URL ending in "/".
|
|
113
128
|
* - "user/repo" (or any GitHub URL shape) → GitHub Pages URL.
|
|
@@ -123,8 +138,7 @@ const resolveOtaUrl = (input) => {
|
|
|
123
138
|
if (/^https?:\/\//i.test(value)) {
|
|
124
139
|
return value.endsWith("/") ? value : `${value}/`;
|
|
125
140
|
}
|
|
126
|
-
|
|
127
|
-
return gh ? `https://${gh.user}.github.io/${gh.repo}/` : null;
|
|
141
|
+
return pagesSiteUrl(value);
|
|
128
142
|
};
|
|
129
143
|
|
|
130
144
|
/**
|
|
@@ -167,6 +181,10 @@ export const createProject = async (projectName, rawArgs = []) => {
|
|
|
167
181
|
repoInput = await askRepo();
|
|
168
182
|
}
|
|
169
183
|
const otaUrl = resolveOtaUrl(repoInput);
|
|
184
|
+
// The Pages address the workflow file prints as its own header. Derived from
|
|
185
|
+
// the repo alone: `flet-box create myapp --repo me/my-website` says so in
|
|
186
|
+
// build.yml, and a missing or unparsable repo leaves the placeholder to edit.
|
|
187
|
+
const siteUrl = pagesSiteUrl(repoInput);
|
|
170
188
|
if (repoInput && !otaUrl) {
|
|
171
189
|
console.warn(
|
|
172
190
|
c(
|
|
@@ -198,6 +216,8 @@ export const createProject = async (projectName, rawArgs = []) => {
|
|
|
198
216
|
".fletbox/api",
|
|
199
217
|
".fletbox/memory/notes",
|
|
200
218
|
".claude",
|
|
219
|
+
".claude/skills/fletbox-clean-ui",
|
|
220
|
+
".qoder/skills/fletbox-clean-ui",
|
|
201
221
|
".cursor/rules",
|
|
202
222
|
];
|
|
203
223
|
const extraDirs =
|
|
@@ -228,7 +248,7 @@ export const createProject = async (projectName, rawArgs = []) => {
|
|
|
228
248
|
createFile(path.join(projectPath, ".gitignore"), templates.gitignore());
|
|
229
249
|
createFile(
|
|
230
250
|
path.join(projectPath, ".github/workflows/build.yml"),
|
|
231
|
-
templates.githubWorkflow(finalName),
|
|
251
|
+
templates.githubWorkflow(finalName, siteUrl),
|
|
232
252
|
);
|
|
233
253
|
createFile(
|
|
234
254
|
path.join(projectPath, ".github/workflows/build-native.yml"),
|
|
@@ -416,6 +436,29 @@ export const createProject = async (projectName, rawArgs = []) => {
|
|
|
416
436
|
path.join(projectPath, ".cursor/rules/fletbox.mdc"),
|
|
417
437
|
templates.cursorRules(),
|
|
418
438
|
);
|
|
439
|
+
|
|
440
|
+
// The clean-UI skill. Claude Code loads `.claude/skills/<name>/SKILL.md` by
|
|
441
|
+
// description, so the agent picks it up whenever it touches a screen without
|
|
442
|
+
// being told. `.qoder/skills/` holds a byte-identical copy because Qoder reads
|
|
443
|
+
// the same shape from a different root — one text, two loaders, never two edits.
|
|
444
|
+
const cleanUiSkill = templates.cleanUiSkill();
|
|
445
|
+
createFile(
|
|
446
|
+
path.join(projectPath, ".claude/skills/fletbox-clean-ui/SKILL.md"),
|
|
447
|
+
cleanUiSkill,
|
|
448
|
+
);
|
|
449
|
+
createFile(
|
|
450
|
+
path.join(projectPath, ".qoder/skills/fletbox-clean-ui/SKILL.md"),
|
|
451
|
+
cleanUiSkill,
|
|
452
|
+
);
|
|
453
|
+
createFile(
|
|
454
|
+
path.join(projectPath, ".cursor/rules/fletbox-clean-ui.mdc"),
|
|
455
|
+
templates.cleanUiCursorRule(),
|
|
456
|
+
);
|
|
457
|
+
createFile(
|
|
458
|
+
path.join(projectPath, "scripts/check-ui.mjs"),
|
|
459
|
+
templates.checkUiScript(finalName),
|
|
460
|
+
);
|
|
461
|
+
|
|
419
462
|
createFile(
|
|
420
463
|
path.join(projectPath, ".fletbox/memory/MEMORY.md"),
|
|
421
464
|
templates.memoryIndexMd(finalName),
|
|
@@ -494,8 +537,9 @@ export const createProject = async (projectName, rawArgs = []) => {
|
|
|
494
537
|
c(
|
|
495
538
|
"gray",
|
|
496
539
|
"CI: .github/workflows/build.yml publishes the web to GitHub Pages on every push.\n" +
|
|
497
|
-
|
|
498
|
-
|
|
540
|
+
` It goes live at: ${siteUrl || `https://TU-USUARIO.github.io/${finalName}/`}\n` +
|
|
541
|
+
" Enable it once: Settings → Pages → Source: GitHub Actions. The job prints the URL.\n" +
|
|
542
|
+
" Android and iOS are manual: .github/workflows/build-native.yml, Run workflow.\n",
|
|
499
543
|
),
|
|
500
544
|
);
|
|
501
545
|
console.log(
|
|
@@ -18,6 +18,7 @@ import path from "path";
|
|
|
18
18
|
import { fileURLToPath } from "url";
|
|
19
19
|
import { c, banner, gradient, section } from "../utils/colors.js";
|
|
20
20
|
import { buildBundleConfig, collectAssets, getGitRemoteUrl, resolveAssetPatterns, absoluteShareTags } from "../utils/bundleConfig.js";
|
|
21
|
+
import { normaliseManifest, writeSpaFallback } from "../utils/pwaShell.js";
|
|
21
22
|
import { INITIAL_VERSION, incrementVersion, isValidVersion } from "../utils/version.js";
|
|
22
23
|
|
|
23
24
|
const __filename = fileURLToPath(import.meta.url);
|
|
@@ -57,6 +58,7 @@ const copyFolderSync = (src, dest) => {
|
|
|
57
58
|
* ```
|
|
58
59
|
* <target>/
|
|
59
60
|
* index.html
|
|
61
|
+
* 404.html ← copy of the shell, so a refresh on a deep link serves the app
|
|
60
62
|
* manifest.json
|
|
61
63
|
* service-worker.js
|
|
62
64
|
* run.sh
|
|
@@ -408,6 +410,25 @@ export const createBundle = async (targetArg) => {
|
|
|
408
410
|
}
|
|
409
411
|
}
|
|
410
412
|
|
|
413
|
+
// ---- Installable, deep-linkable output ----------------------------------
|
|
414
|
+
// Both corrections belong to the deployable folder, not to the project: the
|
|
415
|
+
// app's own manifest.json stays host-agnostic and this bundle is what a static
|
|
416
|
+
// host serves. See bin/utils/pwaShell.js for the two failure modes.
|
|
417
|
+
const manifestFixes = normaliseManifest(targetDir);
|
|
418
|
+
if (manifestFixes.length) {
|
|
419
|
+
console.log(c("green", `✅ manifest.json → ${manifestFixes.length} correction(s)`));
|
|
420
|
+
for (const fix of manifestFixes) console.log(c("gray", ` ${fix}`));
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
if (writeSpaFallback(targetDir)) {
|
|
424
|
+
console.log(
|
|
425
|
+
c(
|
|
426
|
+
"green",
|
|
427
|
+
"✅ 404.html → copy of the shell (hosts without SPA rewrites now serve the app on a refresh)",
|
|
428
|
+
),
|
|
429
|
+
);
|
|
430
|
+
}
|
|
431
|
+
|
|
411
432
|
// Preserve the app source tree so the in-app "View code" feature can read it.
|
|
412
433
|
for (const dir of ["modules", "screens", "database", "components"]) {
|
|
413
434
|
const src = path.join(srcDir, dir);
|
|
@@ -154,7 +154,11 @@ export const runDevServer = async (options = {}) => {
|
|
|
154
154
|
url = req.url;
|
|
155
155
|
}
|
|
156
156
|
|
|
157
|
-
|
|
157
|
+
// The query is part of the request, not of the file name: `?theme=dark`
|
|
158
|
+
// and the CSS hot-swap's `?t=` cache-buster were being resolved on disk
|
|
159
|
+
// and answered with a 404.
|
|
160
|
+
const pathname = url.split("?")[0].split("#")[0];
|
|
161
|
+
const safePath = path.normalize(pathname).replace(/^(\.\.[\/\\])+/, "");
|
|
158
162
|
let filePath = path.join(
|
|
159
163
|
projectRoot,
|
|
160
164
|
safePath === "/" ? "index.html" : safePath,
|
|
@@ -186,7 +190,12 @@ export const runDevServer = async (options = {}) => {
|
|
|
186
190
|
// still resolves `./loader.js` against `/demo/forms/`, so the page came
|
|
187
191
|
// up blank on every shared link. Anchoring the page at the served root
|
|
188
192
|
// keeps the app's relative URLs working whatever the route is.
|
|
189
|
-
|
|
193
|
+
//
|
|
194
|
+
// The root document needs the same anchor: a client router pushes
|
|
195
|
+
// `/widget/Button` without a reload, and from then on the browser
|
|
196
|
+
// re-resolves a relative `<link rel="icon">` against that path. The
|
|
197
|
+
// favicon then 404s on every in-app navigation.
|
|
198
|
+
if (spaMode && ext === ".html" && (isFallback || safePath === "/")) {
|
|
190
199
|
let html = content.toString("utf-8");
|
|
191
200
|
if (!/<base [^>]*href=/i.test(html)) {
|
|
192
201
|
html = html.replace(/<head(\s[^>]*)?>/i, (m) => `${m}\n<base href="/">`);
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file bin/utils/pwaShell.js
|
|
3
|
+
* @description Two build-time corrections that make a bundle deployable as a PWA
|
|
4
|
+
* on a static host, which is where every generated app actually ends up
|
|
5
|
+
* (GitHub Pages, S3, any bucket under `/repo/`).
|
|
6
|
+
*
|
|
7
|
+
* Neither can be left to the app author to remember, because both fail silently
|
|
8
|
+
* on the developer's own machine and only show up once deployed:
|
|
9
|
+
*
|
|
10
|
+
* 1. `writeSpaFallback` — a single-page app has routes the host does not know.
|
|
11
|
+
* GitHub Pages has no rewrite rule, so a refresh on `/repo/menu` is answered
|
|
12
|
+
* with Pages' own "Page not found": the app looks broken exactly on the
|
|
13
|
+
* entry point a shared link or a reload produces. `404.html` is the portable
|
|
14
|
+
* remedy: every static host serves that file for a missing path, so copying
|
|
15
|
+
* the built shell over it hands the deep link to the router instead.
|
|
16
|
+
*
|
|
17
|
+
* 2. `normaliseManifest` — a generated `manifest.json` says `"start_url": "/"`
|
|
18
|
+
* and `"scope": "/"`. Resolved against the manifest's own URL that is the
|
|
19
|
+
* host root, i.e. `https://user.github.io/`, which is outside the app's
|
|
20
|
+
* directory and outside its service worker's scope. Chrome then refuses to
|
|
21
|
+
* offer the install at all: the site renders, works offline, and never shows
|
|
22
|
+
* "Add to Home Screen". Icons have the quieter version of the same bug — a
|
|
23
|
+
* copied logo declared as `192x192` that is really 512 px wide.
|
|
24
|
+
*
|
|
25
|
+
* Both only touch the bundle output. The project's own `manifest.json` and
|
|
26
|
+
* `index.html` stay host-agnostic so the same folder still runs on `:8000`.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import fs from "fs";
|
|
30
|
+
import path from "path";
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Read the intrinsic size of a PNG from its IHDR chunk.
|
|
34
|
+
*
|
|
35
|
+
* @param {string} filePath - Absolute path to the candidate icon.
|
|
36
|
+
* @returns {{width: number, height: number}|null} Nothing for a non-PNG file.
|
|
37
|
+
*/
|
|
38
|
+
export const pngDimensions = (filePath) => {
|
|
39
|
+
const header = Buffer.alloc(24);
|
|
40
|
+
let handle;
|
|
41
|
+
try {
|
|
42
|
+
handle = fs.openSync(filePath, "r");
|
|
43
|
+
if (fs.readSync(handle, header, 0, 24, 0) !== 24) return null;
|
|
44
|
+
} catch {
|
|
45
|
+
return null;
|
|
46
|
+
} finally {
|
|
47
|
+
if (handle !== undefined) fs.closeSync(handle);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const isPng =
|
|
51
|
+
header[0] === 0x89 &&
|
|
52
|
+
header.subarray(12, 16).toString("ascii") === "IHDR";
|
|
53
|
+
if (!isPng) return null;
|
|
54
|
+
return { width: header.readUInt32BE(16), height: header.readUInt32BE(20) };
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Make the bundled `manifest.json` launch and stay inside the app it ships with.
|
|
59
|
+
*
|
|
60
|
+
* `start_url` and `scope` are resolved against the manifest URL, so `"/"` means
|
|
61
|
+
* the host root. Rewriting only that value (and an empty string) to `"./"` keeps
|
|
62
|
+
* a root-hosted app identical to what it was while pulling a sub-path app back
|
|
63
|
+
* inside its own directory; any other value is a deliberate base path and is left
|
|
64
|
+
* alone. Each icon's declared `sizes` is corrected to the pixels the file really
|
|
65
|
+
* has, which is also what makes the 192 px requirement provable to the browser.
|
|
66
|
+
*
|
|
67
|
+
* @param {string} targetDir - Absolute path of the bundle directory.
|
|
68
|
+
* @returns {string[]} Human-readable description of each correction made.
|
|
69
|
+
*/
|
|
70
|
+
export const normaliseManifest = (targetDir) => {
|
|
71
|
+
const manifestPath = path.join(targetDir, "manifest.json");
|
|
72
|
+
if (!fs.existsSync(manifestPath)) return [];
|
|
73
|
+
|
|
74
|
+
let raw;
|
|
75
|
+
try {
|
|
76
|
+
raw = fs.readFileSync(manifestPath, "utf8");
|
|
77
|
+
} catch {
|
|
78
|
+
return [];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
let manifest;
|
|
82
|
+
try {
|
|
83
|
+
manifest = JSON.parse(raw);
|
|
84
|
+
} catch {
|
|
85
|
+
return [];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const changes = [];
|
|
89
|
+
|
|
90
|
+
// "/" escapes the app on a sub-path host; "" is the same mistake written
|
|
91
|
+
// differently. Anything else was typed on purpose.
|
|
92
|
+
for (const field of ["start_url", "scope"]) {
|
|
93
|
+
if (manifest[field] === "/" || manifest[field] === "") {
|
|
94
|
+
manifest[field] = "./";
|
|
95
|
+
changes.push(`${field}: "/" → "./"`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (Array.isArray(manifest.icons)) {
|
|
100
|
+
for (const icon of manifest.icons) {
|
|
101
|
+
if (!icon || typeof icon.src !== "string") continue;
|
|
102
|
+
// Strip any query/hash and the leading "./" before hitting the disk; the
|
|
103
|
+
// bundle keeps icons relative to its own directory.
|
|
104
|
+
const clean = icon.src.split(/[?#]/)[0].replace(/^\.\//, "");
|
|
105
|
+
const size = pngDimensions(path.join(targetDir, clean));
|
|
106
|
+
if (!size) continue;
|
|
107
|
+
const actual = `${size.width}x${size.height}`;
|
|
108
|
+
if (icon.sizes !== actual) {
|
|
109
|
+
const declared = icon.sizes;
|
|
110
|
+
icon.sizes = actual;
|
|
111
|
+
changes.push(`${clean}: sizes now ${actual} (was ${declared})`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (changes.length) {
|
|
117
|
+
fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + "\n");
|
|
118
|
+
}
|
|
119
|
+
return changes;
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Copy the finished shell to `404.html` so unknown paths serve the app.
|
|
124
|
+
*
|
|
125
|
+
* Called last, after the OTA loader is wired and the share tags are absolute, so
|
|
126
|
+
* the fallback document is byte-identical to `index.html` and boots the same
|
|
127
|
+
* bundle. Idempotent: rebuilding the project always rewrites it.
|
|
128
|
+
*
|
|
129
|
+
* @param {string} targetDir - Absolute path of the bundle directory.
|
|
130
|
+
* @returns {boolean} False when there is no `index.html` to copy.
|
|
131
|
+
*/
|
|
132
|
+
export const writeSpaFallback = (targetDir) => {
|
|
133
|
+
const index = path.join(targetDir, "index.html");
|
|
134
|
+
if (!fs.existsSync(index)) return false;
|
|
135
|
+
fs.copyFileSync(index, path.join(targetDir, "404.html"));
|
|
136
|
+
return true;
|
|
137
|
+
};
|