flet-box 0.0.0-stage ā 1.0.2
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/LICENSE +21 -0
- package/README.md +190 -2
- package/bin/cli.js +394 -0
- package/bin/commands/buildAndroid.js +200 -0
- package/bin/commands/buildIOS.js +237 -0
- package/bin/commands/create.js +406 -0
- package/bin/commands/createBundle.js +375 -0
- package/bin/commands/createComponent.js +115 -0
- package/bin/commands/createScreen.js +161 -0
- package/bin/commands/killServer.js +107 -0
- package/bin/commands/package.js +131 -0
- package/bin/commands/release.js +189 -0
- package/bin/commands/runBundle.js +62 -0
- package/bin/commands/runCloudflare.js +89 -0
- package/bin/commands/runNative.js +109 -0
- package/bin/commands/runServer.js +390 -0
- package/bin/ota/loader.js +361 -0
- package/bin/utils/bundleConfig.js +178 -0
- package/bin/utils/capacitor.js +137 -0
- package/bin/utils/colors.js +215 -0
- package/bin/utils/helpers.js +80 -0
- package/bin/utils/spinner.js +96 -0
- package/bin/utils/templates.js +1824 -0
- package/bin/utils/version.js +108 -0
- package/package.json +58 -4
- package/src/animations/AnimatedBox.js +607 -0
- package/src/animations/AnimatedText.js +105 -0
- package/src/animations/MatrixRain.js +220 -0
- package/src/animations/ParallaxBox.js +369 -0
- package/src/animations/animate.js +332 -0
- package/src/animations/index.js +25 -0
- package/src/app.js +41 -0
- package/src/assets/icon-1024-maskable.png +0 -0
- package/src/assets/icon-192-maskable.png +0 -0
- package/src/assets/icon-192.png +0 -0
- package/src/assets/icon-512-maskable.png +0 -0
- package/src/assets/icon-512.png +0 -0
- package/src/assets/logo.png +0 -0
- package/src/core/App.js +44 -0
- package/src/core/hmr-client.js +142 -0
- package/src/core/index.js +34 -0
- package/src/core/pwa.js +100 -0
- package/src/core/runApp.js +430 -0
- package/src/create-index.sh +237 -0
- package/src/fonts/MaterialIcons-Regular.woff2 +0 -0
- package/src/fonts/icons.css +23 -0
- package/src/index.d.ts +1930 -0
- package/src/index.js +288 -0
- package/src/navigations/AdaptiveScaffold.js +149 -0
- package/src/navigations/AppBar.js +331 -0
- package/src/navigations/BottomNavigation.js +298 -0
- package/src/navigations/CollapsibleSideBar.js +139 -0
- package/src/navigations/Drawer.js +333 -0
- package/src/navigations/DrawerItem.js +260 -0
- package/src/navigations/Router.js +475 -0
- package/src/navigations/Scaffold.js +537 -0
- package/src/navigations/Tabs.js +384 -0
- package/src/navigations/index.js +57 -0
- package/src/services/RamStore.js +213 -0
- package/src/services/Session.js +310 -0
- package/src/services/Storage.js +309 -0
- package/src/services/http.js +350 -0
- package/src/services/index.js +57 -0
- package/src/tools/animation.js +42 -0
- package/src/tools/array.js +95 -0
- package/src/tools/border.js +28 -0
- package/src/tools/clipboard.js +104 -0
- package/src/tools/color.js +329 -0
- package/src/tools/createList.js +215 -0
- package/src/tools/delay.js +55 -0
- package/src/tools/device.js +138 -0
- package/src/tools/dict.js +297 -0
- package/src/tools/dimensions.js +118 -0
- package/src/tools/filter.js +41 -0
- package/src/tools/flex.js +26 -0
- package/src/tools/gradient.js +34 -0
- package/src/tools/grid.js +57 -0
- package/src/tools/index.js +38 -0
- package/src/tools/mapList.js +107 -0
- package/src/tools/margin.js +93 -0
- package/src/tools/memo.js +60 -0
- package/src/tools/os.js +104 -0
- package/src/tools/padding.js +93 -0
- package/src/tools/print.js +69 -0
- package/src/tools/random.js +299 -0
- package/src/tools/ref.js +70 -0
- package/src/tools/rgba.js +4 -0
- package/src/tools/shadow.js +23 -0
- package/src/tools/string.js +85 -0
- package/src/tools/time.js +674 -0
- package/src/tools/transform.js +68 -0
- package/src/tools/transition.js +48 -0
- package/src/tools/useState.js +268 -0
- package/src/tools/uuid.js +42 -0
- package/src/utils/TextInputValidator.js +256 -0
- package/src/utils/composeUpdate.js +65 -0
- package/src/utils/getWidgetProps.js +35 -0
- package/src/utils/index.js +76 -0
- package/src/utils/markdownParser.js +368 -0
- package/src/utils/mediaTime.js +79 -0
- package/src/utils/navigation.js +263 -0
- package/src/utils/scriptInjector.js +73 -0
- package/src/utils/stackPosition.js +31 -0
- package/src/utils/stopWebRefresh.js +77 -0
- package/src/utils/styleInjector.js +38 -0
- package/src/utils/syntaxHighlight.js +339 -0
- package/src/utils/themes.js +347 -0
- package/src/utils/units.js +87 -0
- package/src/utils/visualEffects.js +169 -0
- package/src/widget-factory/addChildren.js +114 -0
- package/src/widget-factory/assignProps.js +256 -0
- package/src/widget-factory/createWidget.js +28 -0
- package/src/widget-factory/effects.js +132 -0
- package/src/widget-factory/index.js +7 -0
- package/src/widget-factory/lifecycle.js +210 -0
- package/src/widget-factory/makeParentable.js +89 -0
- package/src/widget-factory/pixTool.js +40 -0
- package/src/widget-factory/processProps.js +122 -0
- package/src/widget-factory/reactivity.js +98 -0
- package/src/widget-factory/remTool.js +34 -0
- package/src/widget-factory/stackPosition.js +87 -0
- package/src/widget-factory/stringTool.js +26 -0
- package/src/widget-factory/tools.js +249 -0
- package/src/widget-factory/translateProps.js +1049 -0
- package/src/widget-factory/warnings.js +284 -0
- package/src/widget-factory/widgetFactory.js +200 -0
- package/src/widgets/Accordion.js +562 -0
- package/src/widgets/AdMob.js +315 -0
- package/src/widgets/AdSense.js +268 -0
- package/src/widgets/AlertDialog.js +162 -0
- package/src/widgets/Audio.js +311 -0
- package/src/widgets/Avatar.js +210 -0
- package/src/widgets/Badge.js +189 -0
- package/src/widgets/BottomSheet.js +287 -0
- package/src/widgets/Button.js +247 -0
- package/src/widgets/Card.js +33 -0
- package/src/widgets/Carousel.js +301 -0
- package/src/widgets/Chart.js +632 -0
- package/src/widgets/Checkbox.js +106 -0
- package/src/widgets/Chip.js +153 -0
- package/src/widgets/CircularBar.js +352 -0
- package/src/widgets/CircularChart.js +549 -0
- package/src/widgets/CodeViewer.js +236 -0
- package/src/widgets/Column.js +29 -0
- package/src/widgets/Container.js +31 -0
- package/src/widgets/DataTable.js +257 -0
- package/src/widgets/Divider.js +93 -0
- package/src/widgets/DraggBox.js +115 -0
- package/src/widgets/Dropdown.js +505 -0
- package/src/widgets/DroppBox.js +180 -0
- package/src/widgets/FloatingActionButton.js +91 -0
- package/src/widgets/GridView.js +20 -0
- package/src/widgets/Icon.js +33 -0
- package/src/widgets/Image.js +15 -0
- package/src/widgets/Input.js +565 -0
- package/src/widgets/Inspector.js +254 -0
- package/src/widgets/InstallButton.js +110 -0
- package/src/widgets/ListTile.js +224 -0
- package/src/widgets/ListView.js +503 -0
- package/src/widgets/Markdown.js +1236 -0
- package/src/widgets/Modal.js +496 -0
- package/src/widgets/Pagination.js +195 -0
- package/src/widgets/ProgressBar.js +178 -0
- package/src/widgets/QRCode.js +467 -0
- package/src/widgets/Radio.js +132 -0
- package/src/widgets/Rating.js +261 -0
- package/src/widgets/Row.js +29 -0
- package/src/widgets/Skeleton.js +243 -0
- package/src/widgets/Slider.js +527 -0
- package/src/widgets/SnackBar.js +227 -0
- package/src/widgets/Stack.js +29 -0
- package/src/widgets/Stepper.js +296 -0
- package/src/widgets/Switch.js +102 -0
- package/src/widgets/Text.js +98 -0
- package/src/widgets/Tooltip.js +267 -0
- package/src/widgets/TreeView.js +370 -0
- package/src/widgets/Video.js +92 -0
- package/src/widgets/index.js +52 -0
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FletBox OTA loader ā runs BEFORE the app inside a production bundle (www/).
|
|
3
|
+
*
|
|
4
|
+
* createBundle copies this file verbatim to www/loader.js and rewrites
|
|
5
|
+
* www/index.html so the module entry is ./loader.js instead of ./src/app.js.
|
|
6
|
+
* It is a real, self-contained ES module (no build step, no imports) so it can
|
|
7
|
+
* be syntax-checked with `node --check` and shipped as-is.
|
|
8
|
+
*
|
|
9
|
+
* Boot sequence:
|
|
10
|
+
* 1. Read the SHIPPED ./config.json (describes the bundle baked into this
|
|
11
|
+
* build + where updates live: `update.url`).
|
|
12
|
+
* 2. Work out which bundle is currently active ā a previously installed OTA
|
|
13
|
+
* bundle (tracked in localStorage + Cache Storage) or the shipped one ā
|
|
14
|
+
* and remember its hash.
|
|
15
|
+
* 3. Fetch the REMOTE config.json from `update.url` (no-store, short timeout).
|
|
16
|
+
* If its hash differs from the active one, an update is available.
|
|
17
|
+
* 4. Depending on policy, either download silently (mode:"auto"/mandatory) or
|
|
18
|
+
* show an Accept/Cancel dialog. On accept: download the bundle, verify its
|
|
19
|
+
* sha256, store it in Cache Storage, and run it.
|
|
20
|
+
* 5. Whatever happens, an app always launches ā network failures, offline,
|
|
21
|
+
* hash mismatches, or a missing update URL simply fall back to the current
|
|
22
|
+
* bundle. OTA never bricks the app.
|
|
23
|
+
*
|
|
24
|
+
* SECURITY: the downloaded bundle is only run if its sha256 matches the hash in
|
|
25
|
+
* the remote config.json. If Web Crypto is unavailable (non-secure context) OTA
|
|
26
|
+
* is skipped rather than running unverified code.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
const SHIPPED_BUNDLE = "./src/app.js";
|
|
30
|
+
const OTA_CACHE = "fletbox-ota";
|
|
31
|
+
const LS_KEY = "fletbox.ota.installed";
|
|
32
|
+
const FETCH_TIMEOUT_MS = 6000;
|
|
33
|
+
|
|
34
|
+
const log = (...a) => console.log("[fletbox-ota]", ...a);
|
|
35
|
+
const warn = (...a) => console.warn("[fletbox-ota]", ...a);
|
|
36
|
+
|
|
37
|
+
const readInstalled = () => {
|
|
38
|
+
try {
|
|
39
|
+
return JSON.parse(localStorage.getItem(LS_KEY)) || null;
|
|
40
|
+
} catch {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const writeInstalled = (value) => {
|
|
46
|
+
try {
|
|
47
|
+
if (value) localStorage.setItem(LS_KEY, JSON.stringify(value));
|
|
48
|
+
else localStorage.removeItem(LS_KEY);
|
|
49
|
+
} catch {
|
|
50
|
+
/* storage disabled (private mode) ā OTA just won't persist */
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
/** sha256 hex digest of a string, or null when Web Crypto is unavailable. */
|
|
55
|
+
async function sha256(text) {
|
|
56
|
+
if (!(globalThis.crypto && crypto.subtle)) return null;
|
|
57
|
+
const buf = new TextEncoder().encode(text);
|
|
58
|
+
const digest = await crypto.subtle.digest("SHA-256", buf);
|
|
59
|
+
return [...new Uint8Array(digest)]
|
|
60
|
+
.map((b) => b.toString(16).padStart(2, "0"))
|
|
61
|
+
.join("");
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** fetch JSON with a timeout + no-store so a stale cache never masks an update. */
|
|
65
|
+
async function fetchJson(url) {
|
|
66
|
+
const ctrl = new AbortController();
|
|
67
|
+
const timer = setTimeout(() => ctrl.abort(), FETCH_TIMEOUT_MS);
|
|
68
|
+
try {
|
|
69
|
+
const res = await fetch(url, { cache: "no-store", signal: ctrl.signal });
|
|
70
|
+
if (!res.ok) throw new Error("HTTP " + res.status);
|
|
71
|
+
return await res.json();
|
|
72
|
+
} finally {
|
|
73
|
+
clearTimeout(timer);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Executes an ES module from source text via a Blob URL. */
|
|
78
|
+
function runModuleSource(text) {
|
|
79
|
+
const blobUrl = URL.createObjectURL(
|
|
80
|
+
new Blob([text], { type: "text/javascript" }),
|
|
81
|
+
);
|
|
82
|
+
return import(blobUrl);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Runs the installed OTA bundle if present, else the shipped one. */
|
|
86
|
+
async function runInstalledOrShipped() {
|
|
87
|
+
const inst = readInstalled();
|
|
88
|
+
if (inst && inst.url && "caches" in globalThis) {
|
|
89
|
+
try {
|
|
90
|
+
const cache = await caches.open(OTA_CACHE);
|
|
91
|
+
const res = await cache.match(inst.url);
|
|
92
|
+
if (res) {
|
|
93
|
+
log("running installed bundle", inst.version || "");
|
|
94
|
+
return runModuleSource(await res.text());
|
|
95
|
+
}
|
|
96
|
+
writeInstalled(null); // cache entry vanished ā fall back to shipped
|
|
97
|
+
} catch (e) {
|
|
98
|
+
warn("could not read installed bundle, using shipped:", e.message);
|
|
99
|
+
writeInstalled(null);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return import(SHIPPED_BUNDLE);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Plain-DOM "update available" dialog. Built by hand (no flet-box) because it
|
|
107
|
+
* must render BEFORE the framework itself is loaded. Resolves true on Accept,
|
|
108
|
+
* false on Cancel. `mandatory` hides the Cancel option.
|
|
109
|
+
*/
|
|
110
|
+
function showPrompt({ from, to, notes, sizeBytes, mandatory }) {
|
|
111
|
+
return new Promise((resolve) => {
|
|
112
|
+
const kb = sizeBytes ? (sizeBytes / 1024).toFixed(0) + " KB" : "";
|
|
113
|
+
const overlay = document.createElement("div");
|
|
114
|
+
overlay.setAttribute("role", "dialog");
|
|
115
|
+
overlay.setAttribute("aria-modal", "true");
|
|
116
|
+
overlay.style.cssText =
|
|
117
|
+
"position:fixed;inset:0;z-index:2147483647;display:flex;align-items:center;" +
|
|
118
|
+
"justify-content:center;background:rgba(10,10,20,.6);backdrop-filter:blur(4px);" +
|
|
119
|
+
"font-family:system-ui,-apple-system,sans-serif;padding:20px;box-sizing:border-box;";
|
|
120
|
+
|
|
121
|
+
const card = document.createElement("div");
|
|
122
|
+
card.style.cssText =
|
|
123
|
+
"width:100%;max-width:380px;background:#16162a;color:#eaeaf2;border-radius:18px;" +
|
|
124
|
+
"padding:24px;box-shadow:0 20px 60px rgba(0,0,0,.5);box-sizing:border-box;";
|
|
125
|
+
|
|
126
|
+
const title = document.createElement("div");
|
|
127
|
+
title.textContent = "Update available";
|
|
128
|
+
title.style.cssText = "font-size:19px;font-weight:700;margin-bottom:6px;";
|
|
129
|
+
|
|
130
|
+
const ver = document.createElement("div");
|
|
131
|
+
ver.textContent = from && to ? from + " ā " + to : "A new version is ready.";
|
|
132
|
+
ver.style.cssText = "font-size:13px;color:#9a9ac0;margin-bottom:14px;";
|
|
133
|
+
|
|
134
|
+
card.appendChild(title);
|
|
135
|
+
card.appendChild(ver);
|
|
136
|
+
|
|
137
|
+
if (notes) {
|
|
138
|
+
const n = document.createElement("div");
|
|
139
|
+
n.textContent = notes;
|
|
140
|
+
n.style.cssText =
|
|
141
|
+
"font-size:14px;line-height:1.5;color:#c7c7e0;margin-bottom:14px;" +
|
|
142
|
+
"white-space:pre-wrap;max-height:160px;overflow:auto;";
|
|
143
|
+
card.appendChild(n);
|
|
144
|
+
}
|
|
145
|
+
if (kb) {
|
|
146
|
+
const s = document.createElement("div");
|
|
147
|
+
s.textContent = "Download size: " + kb;
|
|
148
|
+
s.style.cssText = "font-size:12px;color:#7a7aa0;margin-bottom:18px;";
|
|
149
|
+
card.appendChild(s);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const row = document.createElement("div");
|
|
153
|
+
row.style.cssText = "display:flex;gap:10px;justify-content:flex-end;";
|
|
154
|
+
|
|
155
|
+
const btnBase =
|
|
156
|
+
"border:0;border-radius:10px;padding:11px 18px;font-size:14px;font-weight:600;" +
|
|
157
|
+
"cursor:pointer;font-family:inherit;";
|
|
158
|
+
|
|
159
|
+
if (!mandatory) {
|
|
160
|
+
const later = document.createElement("button");
|
|
161
|
+
later.textContent = "Later";
|
|
162
|
+
later.style.cssText = btnBase + "background:#2a2a44;color:#c7c7e0;";
|
|
163
|
+
later.onclick = () => {
|
|
164
|
+
overlay.remove();
|
|
165
|
+
resolve(false);
|
|
166
|
+
};
|
|
167
|
+
row.appendChild(later);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const update = document.createElement("button");
|
|
171
|
+
update.textContent = mandatory ? "Update" : "Update now";
|
|
172
|
+
update.style.cssText = btnBase + "background:#6366f1;color:#fff;";
|
|
173
|
+
update.onclick = () => {
|
|
174
|
+
update.disabled = true;
|
|
175
|
+
update.textContent = "Downloadingā¦";
|
|
176
|
+
overlay.dataset.accepted = "1";
|
|
177
|
+
resolve(true);
|
|
178
|
+
};
|
|
179
|
+
row.appendChild(update);
|
|
180
|
+
|
|
181
|
+
card.appendChild(row);
|
|
182
|
+
overlay.appendChild(card);
|
|
183
|
+
document.body.appendChild(overlay);
|
|
184
|
+
|
|
185
|
+
// Expose the overlay so the caller can update/close it during download.
|
|
186
|
+
showPrompt._overlay = overlay;
|
|
187
|
+
showPrompt._btn = update;
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const closePrompt = () => {
|
|
192
|
+
if (showPrompt._overlay) {
|
|
193
|
+
showPrompt._overlay.remove();
|
|
194
|
+
showPrompt._overlay = null;
|
|
195
|
+
showPrompt._btn = null;
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
const setPromptStatus = (text) => {
|
|
200
|
+
if (showPrompt._btn) showPrompt._btn.textContent = text;
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Numeric version compare for the minNativeVersion gate. Returns true when
|
|
205
|
+
* `have` >= `want`. Missing parts are treated as 0. On web there is no native
|
|
206
|
+
* version, so callers pass null and the gate is skipped.
|
|
207
|
+
*/
|
|
208
|
+
function versionAtLeast(have, want) {
|
|
209
|
+
if (!want || want === "0.0.0") return true;
|
|
210
|
+
if (!have) return true; // unknown native version (web) ā don't block
|
|
211
|
+
const a = String(have).split(".").map((n) => parseInt(n, 10) || 0);
|
|
212
|
+
const b = String(want).split(".").map((n) => parseInt(n, 10) || 0);
|
|
213
|
+
for (let i = 0; i < Math.max(a.length, b.length); i++) {
|
|
214
|
+
const x = a[i] || 0;
|
|
215
|
+
const y = b[i] || 0;
|
|
216
|
+
if (x !== y) return x >= y;
|
|
217
|
+
}
|
|
218
|
+
return true;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Checks the remote config and, if an update is accepted, downloads + verifies +
|
|
223
|
+
* stores + runs it. Returns true when a NEW bundle was launched, false when the
|
|
224
|
+
* caller should run the current one.
|
|
225
|
+
*/
|
|
226
|
+
async function checkAndUpdate(shippedConfig, activeHash) {
|
|
227
|
+
const update = shippedConfig && shippedConfig.update;
|
|
228
|
+
if (!update || !update.url) return false; // OTA disabled
|
|
229
|
+
|
|
230
|
+
let remote;
|
|
231
|
+
try {
|
|
232
|
+
remote = await fetchJson(update.url + "config.json");
|
|
233
|
+
} catch (e) {
|
|
234
|
+
log("no remote config (offline or not deployed yet):", e.message);
|
|
235
|
+
return false;
|
|
236
|
+
}
|
|
237
|
+
if (!remote || typeof remote.hash !== "string") return false;
|
|
238
|
+
if (remote.hash === activeHash) {
|
|
239
|
+
log("up to date (" + (remote.version || activeHash.slice(0, 8)) + ")");
|
|
240
|
+
return false;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Native-shell compatibility gate (Phase 2 injects the real native version).
|
|
244
|
+
const nativeVersion = globalThis.__FLETBOX_NATIVE_VERSION__ || null;
|
|
245
|
+
if (!versionAtLeast(nativeVersion, remote.minNativeVersion)) {
|
|
246
|
+
warn(
|
|
247
|
+
"update requires native " +
|
|
248
|
+
remote.minNativeVersion +
|
|
249
|
+
" but shell is " +
|
|
250
|
+
(nativeVersion || "unknown") +
|
|
251
|
+
" ā update the app from the store instead.",
|
|
252
|
+
);
|
|
253
|
+
return false;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const mandatory = !!(remote.update && remote.update.mandatory);
|
|
257
|
+
const auto = update.mode === "auto";
|
|
258
|
+
|
|
259
|
+
// mode:"auto" downloads silently. Otherwise show the dialog; a mandatory
|
|
260
|
+
// update still shows it but without a "Later" button, so it can't be skipped.
|
|
261
|
+
if (!auto) {
|
|
262
|
+
const ok = await showPrompt({
|
|
263
|
+
from: (readInstalled() || shippedConfig || {}).version,
|
|
264
|
+
to: remote.version,
|
|
265
|
+
notes: (remote.update && remote.update.notes) || "",
|
|
266
|
+
sizeBytes: remote.size,
|
|
267
|
+
mandatory,
|
|
268
|
+
});
|
|
269
|
+
if (!ok) {
|
|
270
|
+
closePrompt();
|
|
271
|
+
return false;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Download the bundle.
|
|
276
|
+
const bundleUrl = update.url + (remote.bundle || "src/app.js");
|
|
277
|
+
let text;
|
|
278
|
+
try {
|
|
279
|
+
if (!auto) setPromptStatus("Downloadingā¦");
|
|
280
|
+
const ctrl = new AbortController();
|
|
281
|
+
const timer = setTimeout(() => ctrl.abort(), 30000);
|
|
282
|
+
const res = await fetch(bundleUrl, { cache: "no-store", signal: ctrl.signal });
|
|
283
|
+
clearTimeout(timer);
|
|
284
|
+
if (!res.ok) throw new Error("HTTP " + res.status);
|
|
285
|
+
text = await res.text();
|
|
286
|
+
} catch (e) {
|
|
287
|
+
warn("download failed:", e.message);
|
|
288
|
+
closePrompt();
|
|
289
|
+
return false;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Verify integrity before running anything.
|
|
293
|
+
const digest = await sha256(text);
|
|
294
|
+
if (digest === null) {
|
|
295
|
+
warn("Web Crypto unavailable ā refusing to run an unverified bundle.");
|
|
296
|
+
closePrompt();
|
|
297
|
+
return false;
|
|
298
|
+
}
|
|
299
|
+
if (digest !== remote.hash) {
|
|
300
|
+
warn("hash mismatch (expected " + remote.hash.slice(0, 12) + ", got " + digest.slice(0, 12) + ") ā aborting.");
|
|
301
|
+
closePrompt();
|
|
302
|
+
return false;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Persist into Cache Storage so the next launch runs it offline.
|
|
306
|
+
if ("caches" in globalThis) {
|
|
307
|
+
try {
|
|
308
|
+
const cache = await caches.open(OTA_CACHE);
|
|
309
|
+
await cache.put(
|
|
310
|
+
bundleUrl,
|
|
311
|
+
new Response(text, {
|
|
312
|
+
headers: { "Content-Type": "text/javascript" },
|
|
313
|
+
}),
|
|
314
|
+
);
|
|
315
|
+
writeInstalled({
|
|
316
|
+
url: bundleUrl,
|
|
317
|
+
hash: remote.hash,
|
|
318
|
+
version: remote.version,
|
|
319
|
+
});
|
|
320
|
+
} catch (e) {
|
|
321
|
+
warn("could not cache bundle (running it once):", e.message);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
log("applying update ā " + (remote.version || remote.hash.slice(0, 12)));
|
|
326
|
+
await runModuleSource(text);
|
|
327
|
+
closePrompt();
|
|
328
|
+
return true;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
(async function main() {
|
|
332
|
+
let shippedConfig = null;
|
|
333
|
+
try {
|
|
334
|
+
shippedConfig = await fetchJson("./config.json");
|
|
335
|
+
} catch (e) {
|
|
336
|
+
warn("no shipped config.json:", e.message);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const installed = readInstalled();
|
|
340
|
+
const activeHash = installed
|
|
341
|
+
? installed.hash
|
|
342
|
+
: shippedConfig && shippedConfig.hash;
|
|
343
|
+
|
|
344
|
+
let updated = false;
|
|
345
|
+
try {
|
|
346
|
+
updated = await checkAndUpdate(shippedConfig, activeHash);
|
|
347
|
+
} catch (e) {
|
|
348
|
+
warn("update check failed:", e.message);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
if (!updated) {
|
|
352
|
+
try {
|
|
353
|
+
await runInstalledOrShipped();
|
|
354
|
+
} catch (e) {
|
|
355
|
+
// Last resort: an installed bundle may be corrupt ā clear it and ship.
|
|
356
|
+
warn("could not run active bundle, falling back to shipped:", e.message);
|
|
357
|
+
writeInstalled(null);
|
|
358
|
+
await import(SHIPPED_BUNDLE);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
})();
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file bin/utils/bundleConfig.js
|
|
3
|
+
* @description Builds the OTA `config.json` manifest that ships inside a
|
|
4
|
+
* FletBox bundle (`www/config.json`).
|
|
5
|
+
*
|
|
6
|
+
* The manifest is what a running app fetches to decide whether an update is
|
|
7
|
+
* available: it carries the app version, a sha256 of the bundle, the build
|
|
8
|
+
* timestamp, and update policy (channel / mode / mandatory / notes). The
|
|
9
|
+
* comparison that drives the OTA is the `hash` ā the version is a human label.
|
|
10
|
+
*
|
|
11
|
+
* Update source URL resolution order:
|
|
12
|
+
* 1. `update.url` in `fletbox.config.json` (explicit wins)
|
|
13
|
+
* 2. derived from the `repository` field of the project's package.json,
|
|
14
|
+
* mapped to its GitHub Pages URL: https://<user>.github.io/<repo>/
|
|
15
|
+
* 3. derived from the CURRENT PROJECT's git remote `origin` (when it is a
|
|
16
|
+
* GitHub URL), so a bundle always points at the project itself ā never at
|
|
17
|
+
* the flet-box package that happens to be installed
|
|
18
|
+
* 4. null (OTA disabled ā the bundle just runs what shipped in it)
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import crypto from "crypto";
|
|
22
|
+
import { execSync } from "child_process";
|
|
23
|
+
import fs from "fs";
|
|
24
|
+
import path from "path";
|
|
25
|
+
import { INITIAL_VERSION, isValidVersion } from "./version.js";
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Computes the sha256 hex digest of a string or buffer.
|
|
29
|
+
*
|
|
30
|
+
* @param {string|Buffer} content
|
|
31
|
+
* @returns {string} `"<hex>"`
|
|
32
|
+
*/
|
|
33
|
+
export const sha256 = (content) =>
|
|
34
|
+
crypto.createHash("sha256").update(content).digest("hex");
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Reads and JSON-parses a file, returning `fallback` when it is missing or
|
|
38
|
+
* cannot be parsed. Never throws.
|
|
39
|
+
*
|
|
40
|
+
* @param {string} file - Absolute path.
|
|
41
|
+
* @param {*} fallback - Value returned on any failure.
|
|
42
|
+
* @returns {*}
|
|
43
|
+
*/
|
|
44
|
+
const readJsonSafe = (file, fallback) => {
|
|
45
|
+
try {
|
|
46
|
+
return JSON.parse(fs.readFileSync(file, "utf8"));
|
|
47
|
+
} catch {
|
|
48
|
+
return fallback;
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Extracts `user` and `repo` from a GitHub repository reference.
|
|
54
|
+
* Accepts the shapes npm produces/accepts:
|
|
55
|
+
* - "git+https://github.com/user/repo.git"
|
|
56
|
+
* - "https://github.com/user/repo"
|
|
57
|
+
* - "git@github.com:user/repo.git"
|
|
58
|
+
* - "user/repo"
|
|
59
|
+
*
|
|
60
|
+
* @param {string} repository - The `repository` value (string form).
|
|
61
|
+
* @returns {{ user: string, repo: string } | null}
|
|
62
|
+
*/
|
|
63
|
+
export const parseGithubRepo = (repository) => {
|
|
64
|
+
if (!repository || typeof repository !== "string") return null;
|
|
65
|
+
const match = repository.match(/github\.com[:/]([^/]+)\/([^/.]+)/);
|
|
66
|
+
if (match) return { user: match[1], repo: match[2] };
|
|
67
|
+
const short = repository.match(/^([\w.-]+)\/([\w.-]+)$/);
|
|
68
|
+
if (short) return { user: short[1], repo: short[2] };
|
|
69
|
+
return null;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Resolves the OTA update URL for a project.
|
|
74
|
+
*
|
|
75
|
+
* @param {string} projectRoot - Absolute path to the project root.
|
|
76
|
+
* @returns {string|null} A base URL ending in "/", or null when it cannot be
|
|
77
|
+
* determined (OTA disabled).
|
|
78
|
+
*/
|
|
79
|
+
export const resolveUpdateUrl = (projectRoot) => {
|
|
80
|
+
const fletboxConfig = readJsonSafe(
|
|
81
|
+
path.join(projectRoot, "fletbox.config.json"),
|
|
82
|
+
{},
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
// 1. Explicit configuration always wins.
|
|
86
|
+
const explicit = fletboxConfig?.update?.url;
|
|
87
|
+
if (typeof explicit === "string" && explicit.trim()) {
|
|
88
|
+
return explicit.endsWith("/") ? explicit : `${explicit}/`;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// 2. Derive from package.json repository ā GitHub Pages URL.
|
|
92
|
+
const pkg = readJsonSafe(path.join(projectRoot, "package.json"), {});
|
|
93
|
+
const repository =
|
|
94
|
+
typeof pkg.repository === "string"
|
|
95
|
+
? pkg.repository
|
|
96
|
+
: pkg.repository?.url || null;
|
|
97
|
+
const gh = parseGithubRepo(repository);
|
|
98
|
+
if (gh) return `https://${gh.user}.github.io/${gh.repo}/`;
|
|
99
|
+
|
|
100
|
+
// 3. Derive from the current project's git remote. This guarantees the
|
|
101
|
+
// bundle targets the project it was built in, not the flet-box framework
|
|
102
|
+
// that may be present as a dependency elsewhere.
|
|
103
|
+
const remote = getGitRemoteUrl(projectRoot);
|
|
104
|
+
const ghRemote = parseGithubRepo(remote);
|
|
105
|
+
if (ghRemote) return `https://${ghRemote.user}.github.io/${ghRemote.repo}/`;
|
|
106
|
+
|
|
107
|
+
return null;
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Returns the `origin` remote URL of the git repo at `projectRoot`, or null.
|
|
112
|
+
*
|
|
113
|
+
* @param {string} projectRoot - Absolute path to the project root.
|
|
114
|
+
* @returns {string|null}
|
|
115
|
+
*/
|
|
116
|
+
export const getGitRemoteUrl = (projectRoot) => {
|
|
117
|
+
try {
|
|
118
|
+
const out = execSync("git remote get-url origin", {
|
|
119
|
+
cwd: projectRoot,
|
|
120
|
+
encoding: "utf8",
|
|
121
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
122
|
+
timeout: 5000,
|
|
123
|
+
});
|
|
124
|
+
return out.trim() || null;
|
|
125
|
+
} catch {
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Builds the OTA config manifest object for a freshly written bundle.
|
|
132
|
+
*
|
|
133
|
+
* @param {object} opts
|
|
134
|
+
* @param {string} opts.projectRoot - Absolute path to the project root.
|
|
135
|
+
* @param {string|Buffer} opts.bundleContent - Exact bytes of the shipped
|
|
136
|
+
* `app.js` (after the version header is stamped), used for the hash + size.
|
|
137
|
+
* @param {string} [opts.bundlePath="src/app.js"] - Bundle path, relative to
|
|
138
|
+
* the bundle root, as the loader should request it.
|
|
139
|
+
* @returns {object} The config.json manifest.
|
|
140
|
+
*/
|
|
141
|
+
export const buildBundleConfig = ({
|
|
142
|
+
projectRoot,
|
|
143
|
+
bundleContent,
|
|
144
|
+
bundlePath = "src/app.js",
|
|
145
|
+
}) => {
|
|
146
|
+
const pkg = readJsonSafe(path.join(projectRoot, "package.json"), {});
|
|
147
|
+
const fletboxConfig = readJsonSafe(
|
|
148
|
+
path.join(projectRoot, "fletbox.config.json"),
|
|
149
|
+
{},
|
|
150
|
+
);
|
|
151
|
+
const updateCfg = fletboxConfig.update || {};
|
|
152
|
+
|
|
153
|
+
const version = isValidVersion(pkg.version) ? pkg.version : INITIAL_VERSION;
|
|
154
|
+
const content = Buffer.from(bundleContent);
|
|
155
|
+
|
|
156
|
+
return {
|
|
157
|
+
version,
|
|
158
|
+
hash: sha256(content),
|
|
159
|
+
builtAt: new Date().toISOString(),
|
|
160
|
+
bundle: bundlePath,
|
|
161
|
+
size: content.length,
|
|
162
|
+
// Minimum native (APK/IPA) version this bundle is compatible with. When the
|
|
163
|
+
// installed native shell is older, the loader must fall back to a store
|
|
164
|
+
// update instead of applying the OTA. Defaults to "0.0.0" (always compatible).
|
|
165
|
+
minNativeVersion:
|
|
166
|
+
typeof updateCfg.minNativeVersion === "string"
|
|
167
|
+
? updateCfg.minNativeVersion
|
|
168
|
+
: "0.0.0",
|
|
169
|
+
update: {
|
|
170
|
+
// Where the app fetches config.json + bundle from. null disables OTA.
|
|
171
|
+
url: resolveUpdateUrl(projectRoot),
|
|
172
|
+
mode: updateCfg.mode === "auto" ? "auto" : "prompt",
|
|
173
|
+
channel: typeof updateCfg.channel === "string" ? updateCfg.channel : "stable",
|
|
174
|
+
mandatory: updateCfg.mandatory === true,
|
|
175
|
+
notes: typeof updateCfg.notes === "string" ? updateCfg.notes : "",
|
|
176
|
+
},
|
|
177
|
+
};
|
|
178
|
+
};
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file bin/utils/capacitor.js
|
|
3
|
+
* @description Shared plumbing for the Capacitor build commands (`build android`,
|
|
4
|
+
* `build ios`). Both commands do the same three things before they touch a
|
|
5
|
+
* native toolchain: validate the project, make sure the generated npm
|
|
6
|
+
* dependencies exist, and run child processes with readable failures.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { spawnSync } from "node:child_process";
|
|
10
|
+
import fs from "node:fs";
|
|
11
|
+
import path from "node:path";
|
|
12
|
+
import { c } from "./colors.js";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Runs a command synchronously, inheriting stdio so the user watches Gradle,
|
|
16
|
+
* CocoaPods or Xcode do their work.
|
|
17
|
+
*
|
|
18
|
+
* @param {string} command - Executable to run.
|
|
19
|
+
* @param {string[]} args - Arguments for the executable.
|
|
20
|
+
* @param {string} cwd - Working directory.
|
|
21
|
+
* @param {string} label - Human readable name used in error messages.
|
|
22
|
+
* @param {NodeJS.ProcessEnv} [env] - Environment for the child process.
|
|
23
|
+
* @returns {void}
|
|
24
|
+
* @throws {Error} When the command cannot start or exits with a non-zero code.
|
|
25
|
+
*/
|
|
26
|
+
export const run = (command, args, cwd, label, env = process.env) => {
|
|
27
|
+
const result = spawnSync(command, args, {
|
|
28
|
+
cwd,
|
|
29
|
+
stdio: "inherit",
|
|
30
|
+
shell: process.platform === "win32",
|
|
31
|
+
env,
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
if (result.error) {
|
|
35
|
+
throw new Error(`${label} could not start: ${result.error.message}`);
|
|
36
|
+
}
|
|
37
|
+
if (result.status !== 0) {
|
|
38
|
+
throw new Error(
|
|
39
|
+
`${label} failed with exit code ${result.status ?? "unknown"}`,
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Checks that a command exists and can report a version.
|
|
46
|
+
*
|
|
47
|
+
* @param {string} command - Executable name, resolved through PATH.
|
|
48
|
+
* @param {string[]} [args] - Arguments used as the probe (defaults to `--version`).
|
|
49
|
+
* @returns {boolean} `true` when the probe exits successfully.
|
|
50
|
+
*/
|
|
51
|
+
export const commandExists = (command, args = ["--version"]) => {
|
|
52
|
+
const result = spawnSync(command, args, { stdio: "ignore" });
|
|
53
|
+
return !result.error && result.status === 0;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const hasPackage = (projectRoot, packageName) =>
|
|
57
|
+
fs.existsSync(
|
|
58
|
+
path.join(projectRoot, "node_modules", ...packageName.split("/"), "package.json"),
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Installs the project npm dependencies when any of the packages the native
|
|
63
|
+
* build needs are missing. Projects created by `flet-box create` already declare
|
|
64
|
+
* them, this only repairs projects whose `node_modules` was never installed.
|
|
65
|
+
*
|
|
66
|
+
* @param {string} projectRoot - Absolute path of the project.
|
|
67
|
+
* @param {string[]} requiredPackages - Packages that must be resolvable.
|
|
68
|
+
* @returns {void}
|
|
69
|
+
*/
|
|
70
|
+
export const ensureNpmDependencies = (projectRoot, requiredPackages) => {
|
|
71
|
+
const missingPackages = requiredPackages.filter(
|
|
72
|
+
(packageName) => !hasPackage(projectRoot, packageName),
|
|
73
|
+
);
|
|
74
|
+
if (missingPackages.length === 0) return;
|
|
75
|
+
|
|
76
|
+
console.log(
|
|
77
|
+
c(
|
|
78
|
+
"yellow",
|
|
79
|
+
`\nš¦ Installing project dependencies: ${missingPackages.join(", ")}`,
|
|
80
|
+
),
|
|
81
|
+
);
|
|
82
|
+
run("npm", ["install"], projectRoot, "npm dependency installation");
|
|
83
|
+
|
|
84
|
+
const stillMissing = requiredPackages.filter(
|
|
85
|
+
(packageName) => !hasPackage(projectRoot, packageName),
|
|
86
|
+
);
|
|
87
|
+
if (stillMissing.length > 0) {
|
|
88
|
+
throw new Error(
|
|
89
|
+
`Dependencies are still missing after npm install: ${stillMissing.join(", ")}.`,
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Validates that the current directory is a FletBox project scaffolded for
|
|
96
|
+
* Capacitor, and returns its parsed `package.json`.
|
|
97
|
+
*
|
|
98
|
+
* @param {string[]} requiredScripts - npm scripts the caller needs, for example `["ios:init"]`.
|
|
99
|
+
* @returns {{ projectRoot: string, projectPackage: object }} Project root and manifest.
|
|
100
|
+
* @throws {Error} When the folder is not a Capacitor-enabled FletBox project.
|
|
101
|
+
*/
|
|
102
|
+
export const assertCapacitorProject = (requiredScripts) => {
|
|
103
|
+
const projectRoot = process.cwd();
|
|
104
|
+
const packagePath = path.join(projectRoot, "package.json");
|
|
105
|
+
const capacitorConfig = [
|
|
106
|
+
"capacitor.config.json",
|
|
107
|
+
"capacitor.config.ts",
|
|
108
|
+
"capacitor.config.js",
|
|
109
|
+
].some((file) => fs.existsSync(path.join(projectRoot, file)));
|
|
110
|
+
|
|
111
|
+
if (!fs.existsSync(path.join(projectRoot, "src", "app.js"))) {
|
|
112
|
+
throw new Error("Run this inside a Flet-Box project with src/app.js.");
|
|
113
|
+
}
|
|
114
|
+
if (!fs.existsSync(packagePath) || !capacitorConfig) {
|
|
115
|
+
throw new Error(
|
|
116
|
+
"This project is not configured for Capacitor. Create it with a recent version of Flet-Box.",
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
let projectPackage;
|
|
121
|
+
try {
|
|
122
|
+
projectPackage = JSON.parse(fs.readFileSync(packagePath, "utf8"));
|
|
123
|
+
} catch (error) {
|
|
124
|
+
throw new Error(`Could not read package.json: ${error.message}`);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const missingScripts = requiredScripts.filter(
|
|
128
|
+
(script) => !projectPackage?.scripts?.[script],
|
|
129
|
+
);
|
|
130
|
+
if (missingScripts.length > 0) {
|
|
131
|
+
throw new Error(
|
|
132
|
+
`Missing npm scripts: ${missingScripts.join(", ")}. Recreate the project with a recent version of Flet-Box.`,
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return { projectRoot, projectPackage };
|
|
137
|
+
};
|