@condorinst/mobile-bridge 0.8.0
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/CHANGELOG.md +350 -0
- package/LICENSE +14 -0
- package/README.md +396 -0
- package/dist/cli.cjs +194 -0
- package/dist/cli.cjs.map +1 -0
- package/dist/cli.js +193 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.cjs +1045 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +624 -0
- package/dist/index.d.ts +624 -0
- package/dist/index.js +1005 -0
- package/dist/index.js.map +1 -0
- package/package.json +65 -0
package/README.md
ADDED
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
# @condorinst/mobile-bridge
|
|
2
|
+
|
|
3
|
+
Canonical TypeScript SDK for the JS↔Native bridge of the Condor white-label
|
|
4
|
+
mobile app.
|
|
5
|
+
|
|
6
|
+
> **Purpose:** give the JavaScript code that runs inside the white-label app's
|
|
7
|
+
> WebView a typed, runtime-validated, testable API to talk to the native
|
|
8
|
+
> Flutter side — versioned handshake, observation of keys emitted by the
|
|
9
|
+
> background, commands to the foreground service, and hooks into native
|
|
10
|
+
> screens (Settings).
|
|
11
|
+
|
|
12
|
+
> **New here?** Start with the [Client Guide](./docs/client-guide.md) —
|
|
13
|
+
> _"Build your White Label Home"_ — and the runnable
|
|
14
|
+
> [`examples/minimal-home`](./examples/minimal-home).
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm install @condorinst/mobile-bridge
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
The published package ships a prebuilt `dist/` (ESM + CJS + type
|
|
23
|
+
declarations) — **no build toolchain is required on your side**.
|
|
24
|
+
|
|
25
|
+
> **Versioning during the pilot.** The package is on `0.x` (beta). Pin
|
|
26
|
+
> `"@condorinst/mobile-bridge": "^0.8.0"`. Under semver, while on `0.x` a
|
|
27
|
+
> caret range auto-updates **patches only** (`0.8.x`); moving to `0.9.0` is
|
|
28
|
+
> an intentional opt-in. `1.0.0` is reserved for production.
|
|
29
|
+
|
|
30
|
+
## Quickstart
|
|
31
|
+
|
|
32
|
+
```typescript
|
|
33
|
+
import { Bridge, BRIDGE_KEYS } from '@condorinst/mobile-bridge';
|
|
34
|
+
|
|
35
|
+
// Versioned handshake with the native side (can be called at any time;
|
|
36
|
+
// recommended on the client's DOMContentLoaded)
|
|
37
|
+
const ack = await Bridge.handshake({ clientVersion: '1.0.0' });
|
|
38
|
+
console.log('Native bridge:', ack.nativeVersion, 'accepted:', ack.accepted);
|
|
39
|
+
|
|
40
|
+
// Subscribe to an observable key coming from the background
|
|
41
|
+
const unsubscribe = Bridge.on(BRIDGE_KEYS.deviceConnection, (payload) => {
|
|
42
|
+
console.log('Device:', payload);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// Command: enable real-time
|
|
46
|
+
Bridge.send('realTime', { enabled: true });
|
|
47
|
+
|
|
48
|
+
// Native hook: open the app's native Settings screen
|
|
49
|
+
Bridge.openSettings();
|
|
50
|
+
|
|
51
|
+
// Cleanup
|
|
52
|
+
unsubscribe();
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
> The recommended model for white-label clients is **`BridgeClient`** (sandbox
|
|
56
|
+
> iframe), not the legacy `Bridge`. See below.
|
|
57
|
+
|
|
58
|
+
## Sandbox iframe — `BridgeClient` + `BridgeShell`
|
|
59
|
+
|
|
60
|
+
The recommended model isolates the external client code inside an
|
|
61
|
+
`<iframe sandbox="allow-scripts">`, communicating over `window.postMessage`
|
|
62
|
+
with schema-validated messages and a default-deny policy.
|
|
63
|
+
|
|
64
|
+
### For the external client — `BridgeClient`
|
|
65
|
+
|
|
66
|
+
API **identical** to the legacy `Bridge`, but over a postMessage transport.
|
|
67
|
+
The client imports it and never notices it is sandboxed:
|
|
68
|
+
|
|
69
|
+
```typescript
|
|
70
|
+
import { BridgeClient, BRIDGE_KEYS } from '@condorinst/mobile-bridge';
|
|
71
|
+
|
|
72
|
+
const ack = await BridgeClient.handshake({ clientVersion: '1.0.0' });
|
|
73
|
+
console.log('Shell:', ack.shellVersion, 'accepted:', ack.accepted);
|
|
74
|
+
|
|
75
|
+
const unsubscribe = BridgeClient.on(BRIDGE_KEYS.userDevice, (payload) => {
|
|
76
|
+
console.log('Devices:', payload);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
BridgeClient.send('realTime', { serialNo: 1234, value: true });
|
|
80
|
+
BridgeClient.openSettings();
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
**Differences vs `Bridge`:**
|
|
84
|
+
- Does not expose `mockEmit`, `installDevPanel`, `isMock`, `getMockState` —
|
|
85
|
+
dev/mock APIs live only on the shell.
|
|
86
|
+
- `handshake` resolves with `{ shellVersion, accepted }` (not `nativeVersion`).
|
|
87
|
+
- Commands not allowed by the `ShellPolicy` are silently dropped (with a
|
|
88
|
+
`console.warn`). The external client has no visibility into which commands
|
|
89
|
+
do or do not exist.
|
|
90
|
+
|
|
91
|
+
### For the Condor-controlled shell — `BridgeShell`
|
|
92
|
+
|
|
93
|
+
```typescript
|
|
94
|
+
import { BridgeShell, BRIDGE_KEYS, type ShellPolicy } from '@condorinst/mobile-bridge';
|
|
95
|
+
|
|
96
|
+
const iframe = document.getElementById('client-frame') as HTMLIFrameElement;
|
|
97
|
+
|
|
98
|
+
const policy: ShellPolicy = {
|
|
99
|
+
readOnlyKeys: [
|
|
100
|
+
BRIDGE_KEYS.userDevice,
|
|
101
|
+
BRIDGE_KEYS.deviceConnection,
|
|
102
|
+
BRIDGE_KEYS.deviceLightSensors,
|
|
103
|
+
BRIDGE_KEYS.realTime,
|
|
104
|
+
],
|
|
105
|
+
allowedCommands: ['userDevice', 'realTime', 'openSettings'],
|
|
106
|
+
requiresConfirmation: ['syncTime', 'deleteMemory'],
|
|
107
|
+
deniedCommands: ['dfu'],
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const shell = new BridgeShell({
|
|
111
|
+
iframe,
|
|
112
|
+
policy,
|
|
113
|
+
onConfirmationRequest: async ({ command, args }) => {
|
|
114
|
+
return await showConfirmationModal(command, args); // shell UI
|
|
115
|
+
},
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// Layer 1 — bootstrap with an override prefix.
|
|
119
|
+
//
|
|
120
|
+
// Use bootstrapClientIframe(clientPath) when HTML comes over HTTP:
|
|
121
|
+
await shell.bootstrapClientIframe('https://api.condor.example/whitelabel/bundle.html');
|
|
122
|
+
//
|
|
123
|
+
// Use bootstrapClientIframeWithHtml(htmlString) when the HTML is already in
|
|
124
|
+
// memory — required for an asset bundled at file:// (Android WebView cannot
|
|
125
|
+
// fetch over file://), a build-time inlined bundle, etc:
|
|
126
|
+
declare const __CLIENT_HTML__: string; // e.g. via esbuild.define
|
|
127
|
+
await shell.bootstrapClientIframeWithHtml(__CLIENT_HTML__);
|
|
128
|
+
|
|
129
|
+
// Layer 2 — iframe attestation.
|
|
130
|
+
const verify = await shell.verifySandbox();
|
|
131
|
+
if (!verify.ok) {
|
|
132
|
+
// Abort the boot — Layer 1 failed.
|
|
133
|
+
throw new Error('Sandbox compromised: bridges still reachable from iframe.');
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// From here on, BridgeShell routes postMessage ↔ native Bridge
|
|
137
|
+
// automatically. Nothing else to do at boot.
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### Layered defense (propagation of `window.flutter_inappwebview`)
|
|
141
|
+
|
|
142
|
+
| Layer | Where | What it does |
|
|
143
|
+
|---|---|---|
|
|
144
|
+
| **1** | `BridgeShell.bootstrapClientIframe` | `srcdoc` with a prefix that deletes `window.flutter_inappwebview` + `window.CondorBridge` before any client script runs. |
|
|
145
|
+
| **2** | `BridgeShell.verifySandbox` + `BridgeClient` handler | Round-trip `postMessage` to attest the iframe state. |
|
|
146
|
+
| **3** | (Native side) | Shell nonce validated natively in the Flutter bridge. |
|
|
147
|
+
| **4** | (Fallback if 1+2 fail) | Dual `InAppWebView` — shell and client in separate WebViews. |
|
|
148
|
+
|
|
149
|
+
## Build CLI — `condor-mobile-bridge build`
|
|
150
|
+
|
|
151
|
+
Generates the **self-contained `.zip` bundle** that the white-label app
|
|
152
|
+
consumes. The app injects the client HTML via `iframe.srcdoc` (origin `null`,
|
|
153
|
+
no base URL) → **relative references do not resolve** and the CSP blocks
|
|
154
|
+
external fetches; therefore the `.zip` must contain **a single `index.html`**
|
|
155
|
+
with inline JS/CSS and media as `data:` URIs.
|
|
156
|
+
|
|
157
|
+
The CLI does this from your build's `dist/`: it reads `index.html`, **inlines**
|
|
158
|
+
`<script src>` (escaping `</script>`) and `<link rel=stylesheet>`, resolves CSS
|
|
159
|
+
`url()` and local `<img>`/icons to `data:` URIs, and packs a `.zip` with
|
|
160
|
+
`index.html` at the root. External refs (`https://…`) are **preserved** (and
|
|
161
|
+
blocked by the app's CSP at runtime).
|
|
162
|
+
|
|
163
|
+
```bash
|
|
164
|
+
npx condor-mobile-bridge build -i dist/index.html -o dist/bundle.zip
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
| Option | Description |
|
|
168
|
+
|---|---|
|
|
169
|
+
| `-i, --html <path>` | Input HTML (e.g. `dist/index.html`). Required. |
|
|
170
|
+
| `-o, --out <path>` | Output `.zip` (default: `dist/whitelabel-bundle.zip`). |
|
|
171
|
+
| `--max-bytes <n>` | **Warning** threshold in bytes (default: `2097152` = 2 MB). |
|
|
172
|
+
|
|
173
|
+
Output (for registering the bundle with the backend — `bundleSize`/`bundleHash`):
|
|
174
|
+
|
|
175
|
+
```
|
|
176
|
+
Built dist/bundle.zip
|
|
177
|
+
entry: index.html (123.4 KB self-contained)
|
|
178
|
+
bundleSize (zip): 45678 bytes
|
|
179
|
+
bundleHash (zip): sha256-<hex>
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
The hash is **deterministic** (fixed mtime on the zip entry) — the same input
|
|
183
|
+
produces the same `bundleHash`. Above the threshold, the CLI **warns** (does
|
|
184
|
+
not block): remember that **the app rejects** a `.zip` larger than 2 MB and
|
|
185
|
+
falls back to the mock.
|
|
186
|
+
|
|
187
|
+
> ⚠️ **Constraint: single-chunk build.** The inliner is post-build and assumes
|
|
188
|
+
> a `dist` with **one JS + one CSS + assets**. Builds with **code-splitting /
|
|
189
|
+
> ES modules** that `import` across chunks **break** in `srcdoc` (the imports
|
|
190
|
+
> do not resolve). Configure your build to emit a single file:
|
|
191
|
+
> - **Vite:** `vite-plugin-singlefile` (or `build.rollupOptions.output.inlineDynamicImports: true` + `manualChunks: undefined`).
|
|
192
|
+
> - **CRA:** build without code-splitting (no `React.lazy`/dynamic `import()` in routes), single bundle.
|
|
193
|
+
>
|
|
194
|
+
> Inlining inflates `data:` media by ~33% (base64) — size against the 2 MB limit.
|
|
195
|
+
|
|
196
|
+
## Key catalog (strictly typed)
|
|
197
|
+
|
|
198
|
+
Mirrors the JS-facing side of the native foreground com keys. The strings here
|
|
199
|
+
are what the JS receives via `window.CondorBridge.emit(key, payload)`.
|
|
200
|
+
|
|
201
|
+
| `BRIDGE_KEYS.*` | Wire (JS) | Typed payload | Typical direction |
|
|
202
|
+
|---|---|---|---|
|
|
203
|
+
| `pingPong` | `'pingPong'` | `PingPongPayload = { data: 'ping' \| 'pong' }` | bidirectional |
|
|
204
|
+
| `userDevice` | `'userDevice'` | `UserDevicePayload = { lastUpdate, devicesConfig[], devicesTelemetry[], devicesUserConfig[] }` | BG → JS |
|
|
205
|
+
| `deviceLightSensors` | `'deviceLightSensors'` | `DeviceLightSensorsPayload = { serialNo, type, data }` | BG → JS |
|
|
206
|
+
| `syncTime` | `'syncTime'` | `SyncTimeAckPayload = { serialNo, success }` | JS → BG (cmd) / BG → JS (ack) |
|
|
207
|
+
| `realTime` | `'realTime'` | `RealTimeAckPayload = { serialNo, success, value }` | JS → BG (cmd) / BG → JS (state) |
|
|
208
|
+
| `deleteMemory` | `'deleteMemory'` | `DeleteMemoryAckPayload = { serialNo, success }` | JS → BG (cmd) / BG → JS (ack) |
|
|
209
|
+
| `deviceNotification` | `'deviceNotification'` | `DeviceNotificationPayload = { serialNo, isConnect, message, messageCode, isError }` | BG → JS |
|
|
210
|
+
| `deviceConnection` | `'deviceConnection'` | `DeviceConnectionPayload = { serialNo, isConnect }` | BG → JS (status); JS → BG (`{type, serialNo}`, permissive) |
|
|
211
|
+
| `sensorLog` | `'sensorLog'` | `SensorLogPayload` | BG → JS |
|
|
212
|
+
| `dfu` | `'dfu'` | `Record<string, unknown>` (fallback) | BG → JS |
|
|
213
|
+
|
|
214
|
+
**Automatic inference on `Bridge.on`:**
|
|
215
|
+
|
|
216
|
+
```typescript
|
|
217
|
+
Bridge.on(BRIDGE_KEYS.realTime, (payload) => {
|
|
218
|
+
// payload: RealTimeAckPayload — type-safe!
|
|
219
|
+
console.log(payload.serialNo, payload.success, payload.value);
|
|
220
|
+
});
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
**`Bridge.send` commands** remain permissive (heterogeneous shapes per type).
|
|
224
|
+
The client passes `Record<string, unknown>`.
|
|
225
|
+
|
|
226
|
+
## Versioning policy (semver)
|
|
227
|
+
|
|
228
|
+
- **PATCH** (`0.8.x`): bugfix with no API change.
|
|
229
|
+
- **MINOR** (`0.x.0`): new observable key, new command, backward-compatible API.
|
|
230
|
+
> While on `0.x`, a minor bump may carry breaking changes (semver treats
|
|
231
|
+
> pre-1.0 minors as breaking). Pin `^0.8.0` to stay on safe patches.
|
|
232
|
+
- **MAJOR** (`x.0.0`): incompatible schema change (key rename, payload shape
|
|
233
|
+
change, public API change). `1.0.0` marks the production-stable contract.
|
|
234
|
+
|
|
235
|
+
The native side declares `nativeVersion` in the `handshake`; the client
|
|
236
|
+
declares `clientVersion`. This release does **not** enforce hard semver
|
|
237
|
+
validation — the native side accepts any `clientVersion`. Real validation will
|
|
238
|
+
land once there are 2+ versions in the field.
|
|
239
|
+
|
|
240
|
+
## Host detection
|
|
241
|
+
|
|
242
|
+
The package detects automatically:
|
|
243
|
+
|
|
244
|
+
- **Android / iOS** — `window.flutter_inappwebview` (via the `flutter_inappwebview` package on the Flutter side).
|
|
245
|
+
- **Browser outside the app** (dev/test): **mock mode** activates automatically.
|
|
246
|
+
|
|
247
|
+
## Mock mode
|
|
248
|
+
|
|
249
|
+
When the package runs outside the Flutter WebView (regular browser, Vite dev
|
|
250
|
+
server, Storybook), the adapter activates mock mode automatically. You can
|
|
251
|
+
develop in the browser without building the APK.
|
|
252
|
+
|
|
253
|
+
```typescript
|
|
254
|
+
import { Bridge, BRIDGE_KEYS } from '@condorinst/mobile-bridge';
|
|
255
|
+
|
|
256
|
+
if (Bridge.isMock) {
|
|
257
|
+
// Subscribe to a listener as usual
|
|
258
|
+
Bridge.on(BRIDGE_KEYS.deviceConnection, (payload) => {
|
|
259
|
+
console.log('Device:', payload);
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
// Simulate the native side sending an event
|
|
263
|
+
Bridge.mockEmit(BRIDGE_KEYS.deviceConnection, {
|
|
264
|
+
serialNo: 1234,
|
|
265
|
+
connected: true,
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
### Handshake in mock is EXPLICIT
|
|
271
|
+
|
|
272
|
+
```typescript
|
|
273
|
+
// In mock, handshake does NOT resolve by itself — the client/test responds.
|
|
274
|
+
const ackPromise = Bridge.handshake({ clientVersion: '1.0.0' });
|
|
275
|
+
|
|
276
|
+
if (Bridge.isMock) {
|
|
277
|
+
Bridge.mockEmit('handshake-ack', {
|
|
278
|
+
nativeVersion: '0.1.0',
|
|
279
|
+
accepted: true,
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const ack = await ackPromise;
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
Design decision: it forces tests to be intentional about the native state. In
|
|
287
|
+
real mode (inside the Flutter WebView), the ack arrives naturally from native.
|
|
288
|
+
|
|
289
|
+
### `Bridge.send` in mock
|
|
290
|
+
|
|
291
|
+
Messages sent via `Bridge.send` (or helpers like `openSettings`) are recorded
|
|
292
|
+
in an internal FIFO buffer (cap 100) + `console.debug`. Inspect via
|
|
293
|
+
`Bridge.getMockState()`:
|
|
294
|
+
|
|
295
|
+
```typescript
|
|
296
|
+
Bridge.send('realTime', { enabled: true });
|
|
297
|
+
|
|
298
|
+
const state = Bridge.getMockState();
|
|
299
|
+
console.log(state.messages);
|
|
300
|
+
// → [{ handlerName: 'CondorBridge', payload: {...}, timestamp: ... }]
|
|
301
|
+
console.log(state.subscriberKeys);
|
|
302
|
+
// → ['deviceConnection']
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
## Dev panel
|
|
306
|
+
|
|
307
|
+
An opt-in HTML panel to inspect the bridge in the browser during development.
|
|
308
|
+
|
|
309
|
+
```typescript
|
|
310
|
+
import { Bridge } from '@condorinst/mobile-bridge';
|
|
311
|
+
|
|
312
|
+
if (Bridge.isMock) {
|
|
313
|
+
Bridge.installDevPanel({
|
|
314
|
+
position: 'bottom-right', // 'bottom-right' | 'bottom-left' | 'top-*'
|
|
315
|
+
maxMessages: 20, // last N shown
|
|
316
|
+
theme: 'dark', // 'dark' | 'light'
|
|
317
|
+
startCollapsed: false,
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
Contents:
|
|
323
|
+
|
|
324
|
+
- Header with a "MOCK" badge, sent counter, minimize button.
|
|
325
|
+
- Sent log — last N messages (timestamp, type, truncated payload).
|
|
326
|
+
- Trigger emit — dropdown of `BRIDGE_KEYS` + JSON textarea + "Emit" button
|
|
327
|
+
(fires `Bridge.mockEmit`) + "Clear sent" button.
|
|
328
|
+
|
|
329
|
+
Idempotent — calling `installDevPanel` again replaces the previous panel.
|
|
330
|
+
Throws if called in real mode (`!Bridge.isMock`).
|
|
331
|
+
|
|
332
|
+
### Auto-respond toggle
|
|
333
|
+
|
|
334
|
+
To close the full loop in the browser (client clicks a button → native
|
|
335
|
+
responds → subscriber fires), pass a **policy** that maps `send` to `emit`.
|
|
336
|
+
The package provides the **mechanism** (checkbox UI + hook); you provide the
|
|
337
|
+
**policy** (app-specific mappings).
|
|
338
|
+
|
|
339
|
+
```typescript
|
|
340
|
+
import { Bridge, BRIDGE_KEYS } from '@condorinst/mobile-bridge';
|
|
341
|
+
|
|
342
|
+
if (Bridge.isMock) {
|
|
343
|
+
Bridge.installDevPanel({
|
|
344
|
+
startAutoRespondEnabled: true, // default false
|
|
345
|
+
autoRespondDelayMs: 50, // default 50 (simulates IPC)
|
|
346
|
+
onSendForAutoRespond(msg) {
|
|
347
|
+
const payload = msg.payload as { type?: string };
|
|
348
|
+
switch (payload?.type) {
|
|
349
|
+
case 'handshake':
|
|
350
|
+
return {
|
|
351
|
+
key: 'handshake-ack',
|
|
352
|
+
payload: { nativeVersion: '0.1.0 (mock)', accepted: true },
|
|
353
|
+
};
|
|
354
|
+
case 'ping':
|
|
355
|
+
return {
|
|
356
|
+
key: BRIDGE_KEYS.pingPong,
|
|
357
|
+
payload: { data: 'pong' },
|
|
358
|
+
};
|
|
359
|
+
default:
|
|
360
|
+
return null; // do not auto-respond to this send
|
|
361
|
+
}
|
|
362
|
+
},
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
Behavior:
|
|
368
|
+
|
|
369
|
+
- The toggle appears in the panel **only when** `onSendForAutoRespond` is
|
|
370
|
+
passed — without a policy, the panel keeps the manual-emit UX.
|
|
371
|
+
- The toggle state persists in memory for the panel's lifetime. Re-install
|
|
372
|
+
resets to `startAutoRespondEnabled`.
|
|
373
|
+
- Errors thrown by the policy or the internal `triggerEmit` are caught and
|
|
374
|
+
logged (`console.error`) — they do not destabilize the bridge.
|
|
375
|
+
- Use `BRIDGE_KEYS` to avoid string-magic in the mappings.
|
|
376
|
+
|
|
377
|
+
## Local development (SDK contributors)
|
|
378
|
+
|
|
379
|
+
```bash
|
|
380
|
+
npm install # installs deps + runs prepare (build)
|
|
381
|
+
npm run typecheck # tsc --noEmit
|
|
382
|
+
npm run lint # biome check
|
|
383
|
+
npm run format # biome format --write
|
|
384
|
+
npm run test # vitest run
|
|
385
|
+
npm run build # tsup (ESM + CJS + types in dist/)
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
## Further reading
|
|
389
|
+
|
|
390
|
+
- [Client Guide — Build your White Label Home](./docs/client-guide.md)
|
|
391
|
+
- [`examples/minimal-home`](./examples/minimal-home) — runnable minimal client
|
|
392
|
+
- [CHANGELOG](./CHANGELOG.md)
|
|
393
|
+
|
|
394
|
+
## License
|
|
395
|
+
|
|
396
|
+
Proprietary — Condor Instruments. See [LICENSE](./LICENSE).
|
package/dist/cli.cjs
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
// src/cli.ts
|
|
5
|
+
var import_node_fs2 = require("fs");
|
|
6
|
+
var import_node_path2 = require("path");
|
|
7
|
+
|
|
8
|
+
// src/cli-core.ts
|
|
9
|
+
var import_node_crypto = require("crypto");
|
|
10
|
+
var import_node_fs = require("fs");
|
|
11
|
+
var import_node_path = require("path");
|
|
12
|
+
var import_fflate = require("fflate");
|
|
13
|
+
var import_node_html_parser = require("node-html-parser");
|
|
14
|
+
var DEFAULT_MAX_BYTES = 2 * 1024 * 1024;
|
|
15
|
+
var FIXED_MTIME = /* @__PURE__ */ new Date("2000-01-01T00:00:00Z");
|
|
16
|
+
var MIME_TYPES = {
|
|
17
|
+
".png": "image/png",
|
|
18
|
+
".jpg": "image/jpeg",
|
|
19
|
+
".jpeg": "image/jpeg",
|
|
20
|
+
".gif": "image/gif",
|
|
21
|
+
".webp": "image/webp",
|
|
22
|
+
".avif": "image/avif",
|
|
23
|
+
".svg": "image/svg+xml",
|
|
24
|
+
".ico": "image/x-icon",
|
|
25
|
+
".bmp": "image/bmp",
|
|
26
|
+
".woff": "font/woff",
|
|
27
|
+
".woff2": "font/woff2",
|
|
28
|
+
".ttf": "font/ttf",
|
|
29
|
+
".otf": "font/otf",
|
|
30
|
+
".eot": "application/vnd.ms-fontobject",
|
|
31
|
+
".mp3": "audio/mpeg",
|
|
32
|
+
".mp4": "video/mp4",
|
|
33
|
+
".webm": "video/webm",
|
|
34
|
+
".json": "application/json"
|
|
35
|
+
};
|
|
36
|
+
function isExternalOrInline(url) {
|
|
37
|
+
return /^(https?:)?\/\//i.test(url) || /^data:/i.test(url) || /^(mailto:|tel:|#)/i.test(url);
|
|
38
|
+
}
|
|
39
|
+
function stripQueryHash(url) {
|
|
40
|
+
return url.split(/[?#]/)[0] ?? url;
|
|
41
|
+
}
|
|
42
|
+
function resolveLocal(htmlRoot, resourceDir, url) {
|
|
43
|
+
const clean = stripQueryHash(url);
|
|
44
|
+
return clean.startsWith("/") ? (0, import_node_path.resolve)(htmlRoot, `.${clean}`) : (0, import_node_path.resolve)(resourceDir, clean);
|
|
45
|
+
}
|
|
46
|
+
function toDataUri(filePath) {
|
|
47
|
+
const bytes = (0, import_node_fs.readFileSync)(filePath);
|
|
48
|
+
const mime = MIME_TYPES[(0, import_node_path.extname)(filePath).toLowerCase()] ?? "application/octet-stream";
|
|
49
|
+
return `data:${mime};base64,${bytes.toString("base64")}`;
|
|
50
|
+
}
|
|
51
|
+
function inlineCssUrls(css, htmlRoot, cssDir) {
|
|
52
|
+
return css.replace(/url\(\s*(['"]?)([^'")]+)\1\s*\)/gi, (match, _quote, rawUrl) => {
|
|
53
|
+
if (isExternalOrInline(rawUrl)) return match;
|
|
54
|
+
const filePath = resolveLocal(htmlRoot, cssDir, rawUrl);
|
|
55
|
+
return `url(${toDataUri(filePath)})`;
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
function inlineScripts(root, htmlRoot) {
|
|
59
|
+
for (const el of root.querySelectorAll("script[src]")) {
|
|
60
|
+
const src = el.getAttribute("src");
|
|
61
|
+
if (!src || isExternalOrInline(src)) continue;
|
|
62
|
+
const js = (0, import_node_fs.readFileSync)(resolveLocal(htmlRoot, htmlRoot, src), "utf-8").replace(
|
|
63
|
+
/<\/script>/gi,
|
|
64
|
+
"<\\/script>"
|
|
65
|
+
);
|
|
66
|
+
el.removeAttribute("src");
|
|
67
|
+
el.set_content(js);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
function inlineStylesheets(root, htmlRoot) {
|
|
71
|
+
for (const el of root.querySelectorAll('link[rel="stylesheet"]')) {
|
|
72
|
+
const href = el.getAttribute("href");
|
|
73
|
+
if (!href || isExternalOrInline(href)) continue;
|
|
74
|
+
const cssPath = resolveLocal(htmlRoot, htmlRoot, href);
|
|
75
|
+
const css = inlineCssUrls((0, import_node_fs.readFileSync)(cssPath, "utf-8"), htmlRoot, (0, import_node_path.dirname)(cssPath));
|
|
76
|
+
el.replaceWith(...(0, import_node_html_parser.parse)(`<style>${css}</style>`).childNodes);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
function inlineExistingStyleBlocks(root, htmlRoot) {
|
|
80
|
+
for (const el of root.querySelectorAll("style")) {
|
|
81
|
+
const css = el.innerHTML;
|
|
82
|
+
if (!css.includes("url(")) continue;
|
|
83
|
+
el.set_content(inlineCssUrls(css, htmlRoot, htmlRoot));
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
function inlineMediaAttributes(root, htmlRoot) {
|
|
87
|
+
const targets = [
|
|
88
|
+
["img[src]", "src"],
|
|
89
|
+
["source[src]", "src"],
|
|
90
|
+
["audio[src]", "src"],
|
|
91
|
+
["video[src]", "src"],
|
|
92
|
+
['link[rel="icon"]', "href"],
|
|
93
|
+
['link[rel="shortcut icon"]', "href"],
|
|
94
|
+
['link[rel="apple-touch-icon"]', "href"],
|
|
95
|
+
['link[rel="preload"]', "href"]
|
|
96
|
+
];
|
|
97
|
+
for (const [selector, attr] of targets) {
|
|
98
|
+
for (const el of root.querySelectorAll(selector)) {
|
|
99
|
+
const ref = el.getAttribute(attr);
|
|
100
|
+
if (!ref || isExternalOrInline(ref)) continue;
|
|
101
|
+
el.setAttribute(attr, toDataUri(resolveLocal(htmlRoot, htmlRoot, ref)));
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
function inlineHtml(htmlPath) {
|
|
106
|
+
const htmlRoot = (0, import_node_path.dirname)((0, import_node_path.resolve)(htmlPath));
|
|
107
|
+
const root = (0, import_node_html_parser.parse)((0, import_node_fs.readFileSync)(htmlPath, "utf-8"), {
|
|
108
|
+
comment: true,
|
|
109
|
+
voidTag: { closingSlash: true }
|
|
110
|
+
});
|
|
111
|
+
inlineScripts(root, htmlRoot);
|
|
112
|
+
inlineStylesheets(root, htmlRoot);
|
|
113
|
+
inlineExistingStyleBlocks(root, htmlRoot);
|
|
114
|
+
inlineMediaAttributes(root, htmlRoot);
|
|
115
|
+
return root.toString();
|
|
116
|
+
}
|
|
117
|
+
function buildBundle(htmlPath, maxBytes = DEFAULT_MAX_BYTES) {
|
|
118
|
+
const html = inlineHtml(htmlPath);
|
|
119
|
+
const zipBytes = (0, import_fflate.zipSync)({ "index.html": [(0, import_fflate.strToU8)(html), { mtime: FIXED_MTIME }] }, { level: 6 });
|
|
120
|
+
const size = zipBytes.length;
|
|
121
|
+
const hash = `sha256-${(0, import_node_crypto.createHash)("sha256").update(zipBytes).digest("hex")}`;
|
|
122
|
+
return { html, zipBytes, size, hash, overLimit: size > maxBytes };
|
|
123
|
+
}
|
|
124
|
+
var DEFAULT_OUT = "dist/whitelabel-bundle.zip";
|
|
125
|
+
function parseArgs(argv) {
|
|
126
|
+
const args = {
|
|
127
|
+
htmlPath: "",
|
|
128
|
+
outPath: DEFAULT_OUT,
|
|
129
|
+
maxBytes: DEFAULT_MAX_BYTES,
|
|
130
|
+
help: false
|
|
131
|
+
};
|
|
132
|
+
for (let i = 0; i < argv.length; i++) {
|
|
133
|
+
const token = argv[i];
|
|
134
|
+
switch (token) {
|
|
135
|
+
case "--help":
|
|
136
|
+
case "-h":
|
|
137
|
+
args.help = true;
|
|
138
|
+
break;
|
|
139
|
+
case "--html":
|
|
140
|
+
case "-i":
|
|
141
|
+
args.htmlPath = argv[++i] ?? "";
|
|
142
|
+
break;
|
|
143
|
+
case "--out":
|
|
144
|
+
case "-o":
|
|
145
|
+
args.outPath = argv[++i] ?? DEFAULT_OUT;
|
|
146
|
+
break;
|
|
147
|
+
case "--max-bytes":
|
|
148
|
+
args.maxBytes = Number.parseInt(argv[++i] ?? "", 10) || DEFAULT_MAX_BYTES;
|
|
149
|
+
break;
|
|
150
|
+
case "build":
|
|
151
|
+
break;
|
|
152
|
+
default:
|
|
153
|
+
if (token && !token.startsWith("-") && !args.htmlPath) args.htmlPath = token;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return args;
|
|
157
|
+
}
|
|
158
|
+
var USAGE = `condor-mobile-bridge build \u2014 gera o .zip white-label self-contained.
|
|
159
|
+
|
|
160
|
+
Uso:
|
|
161
|
+
npx condor-mobile-bridge build -i <html> [-o <zip>]
|
|
162
|
+
|
|
163
|
+
Op\xE7\xF5es:
|
|
164
|
+
-i, --html <path> HTML de entrada (ex.: dist/index.html). Obrigat\xF3rio.
|
|
165
|
+
-o, --out <path> .zip de sa\xEDda (default: ${DEFAULT_OUT}).
|
|
166
|
+
--max-bytes <n> Limite de aviso em bytes (default: ${DEFAULT_MAX_BYTES} = 2 MB).
|
|
167
|
+
-h, --help Mostra esta ajuda.`;
|
|
168
|
+
|
|
169
|
+
// src/cli.ts
|
|
170
|
+
function main() {
|
|
171
|
+
const args = parseArgs(process.argv.slice(2));
|
|
172
|
+
if (args.help || !args.htmlPath) {
|
|
173
|
+
console.log(USAGE);
|
|
174
|
+
process.exit(args.help ? 0 : 1);
|
|
175
|
+
}
|
|
176
|
+
const result = buildBundle(args.htmlPath, args.maxBytes);
|
|
177
|
+
const outPath = (0, import_node_path2.resolve)(args.outPath);
|
|
178
|
+
(0, import_node_fs2.mkdirSync)((0, import_node_path2.dirname)(outPath), { recursive: true });
|
|
179
|
+
(0, import_node_fs2.writeFileSync)(outPath, result.zipBytes);
|
|
180
|
+
const kb = (Buffer.byteLength(result.html) / 1024).toFixed(1);
|
|
181
|
+
console.log(`Built ${args.outPath}`);
|
|
182
|
+
console.log(` entry: index.html (${kb} KB self-contained)`);
|
|
183
|
+
console.log(` bundleSize (zip): ${result.size} bytes`);
|
|
184
|
+
console.log(` bundleHash (zip): ${result.hash}`);
|
|
185
|
+
if (result.overLimit) {
|
|
186
|
+
const limitMb = (args.maxBytes / (1024 * 1024)).toFixed(1);
|
|
187
|
+
console.warn(
|
|
188
|
+
`
|
|
189
|
+
\u26A0\uFE0F AVISO: o .zip (${result.size} bytes) excede o limite de ${limitMb} MB \u2014 o app vai REJEITAR este bundle (cai no mock). Reduza assets/m\xEDdias inline.`
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
main();
|
|
194
|
+
//# sourceMappingURL=cli.cjs.map
|
package/dist/cli.cjs.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/cli.ts","../src/cli-core.ts"],"sourcesContent":["/**\n * cli.ts — Entry `bin` do `condor-mobile-bridge build`.\n *\n * Fino: parse de argv → `buildBundle` → escreve o `.zip` (mkdir recursivo) →\n * imprime tamanho + SHA-256 (formato que espelha `web-mock-src/build-demo.mjs`)\n * e avisa se passar do limite. Toda a lógica testável vive em `cli-core.ts`.\n */\nimport { mkdirSync, writeFileSync } from 'node:fs';\nimport { dirname, resolve } from 'node:path';\nimport { buildBundle, parseArgs, USAGE } from './cli-core';\n\nfunction main(): void {\n const args = parseArgs(process.argv.slice(2));\n\n if (args.help || !args.htmlPath) {\n console.log(USAGE);\n process.exit(args.help ? 0 : 1);\n }\n\n const result = buildBundle(args.htmlPath, args.maxBytes);\n\n const outPath = resolve(args.outPath);\n mkdirSync(dirname(outPath), { recursive: true });\n writeFileSync(outPath, result.zipBytes);\n\n const kb = (Buffer.byteLength(result.html) / 1024).toFixed(1);\n console.log(`Built ${args.outPath}`);\n console.log(` entry: index.html (${kb} KB self-contained)`);\n console.log(` bundleSize (zip): ${result.size} bytes`);\n console.log(` bundleHash (zip): ${result.hash}`);\n\n if (result.overLimit) {\n const limitMb = (args.maxBytes / (1024 * 1024)).toFixed(1);\n console.warn(\n `\\n⚠️ AVISO: o .zip (${result.size} bytes) excede o limite de ${limitMb} MB — ` +\n 'o app vai REJEITAR este bundle (cai no mock). Reduza assets/mídias inline.',\n );\n }\n}\n\nmain();\n","/**\n * cli-core.ts — Núcleo (puro/testável) do CLI `condor-mobile-bridge build`.\n *\n * Materializa a Frente 1 da Opção A do bundle white-label: pega um\n * `dist/index.html` (build React/Vite do cliente), inlina JS em `<script>`,\n * CSS em `<style>` (resolvendo `url()` em `data:` URI), converte mídias\n * locais em `data:` URI e empacota num `.zip` com um único `index.html` na\n * raiz. Não toca o filesystem de saída — quem escreve o `.zip` é `cli.ts`.\n *\n * Modelo de render do app: `iframe.srcdoc` (origem `null`, sem base URL) só\n * resolve conteúdo inline + `data:` URI — por isso o bundle precisa ser\n * self-contained. Ver `condor_wiki/decisions/2026-06-22_whitelabel-bundle-format.md`.\n *\n * LIMITAÇÃO conhecida: o inliner pós-build assume `dist` single-chunk\n * (um JS + um CSS + assets). Builds com code-split / ES modules que fazem\n * `import` entre chunks quebram em `srcdoc` — o cliente deve buildar\n * single-file (Vite: `vite-plugin-singlefile`; CRA: sem split). Ver README.\n */\nimport { createHash } from 'node:crypto';\nimport { readFileSync } from 'node:fs';\nimport { dirname, extname, resolve } from 'node:path';\nimport { strToU8, zipSync } from 'fflate';\nimport { type HTMLElement, parse } from 'node-html-parser';\n\n/** Default do tamanho máximo do `.zip` (espelha `WhitelabelBundleLimits.maxZipBytes` no app). */\nexport const DEFAULT_MAX_BYTES = 2 * 1024 * 1024;\n\n/** Mtime fixo (>=1980, limite do formato ZIP) → SHA-256 reprodutível entre builds. */\nconst FIXED_MTIME = new Date('2000-01-01T00:00:00Z');\n\n/** Mapa de extensão → MIME para montar `data:` URIs de mídias embutidas. */\nexport const MIME_TYPES: Record<string, string> = {\n '.png': 'image/png',\n '.jpg': 'image/jpeg',\n '.jpeg': 'image/jpeg',\n '.gif': 'image/gif',\n '.webp': 'image/webp',\n '.avif': 'image/avif',\n '.svg': 'image/svg+xml',\n '.ico': 'image/x-icon',\n '.bmp': 'image/bmp',\n '.woff': 'font/woff',\n '.woff2': 'font/woff2',\n '.ttf': 'font/ttf',\n '.otf': 'font/otf',\n '.eot': 'application/vnd.ms-fontobject',\n '.mp3': 'audio/mpeg',\n '.mp4': 'video/mp4',\n '.webm': 'video/webm',\n '.json': 'application/json',\n};\n\n/** Refs que NÃO devem ser inlinadas: absolutas externas, protocol-relative, data: já-inline. */\nfunction isExternalOrInline(url: string): boolean {\n return /^(https?:)?\\/\\//i.test(url) || /^data:/i.test(url) || /^(mailto:|tel:|#)/i.test(url);\n}\n\n/** Remove query/hash de uma ref local (`main.js?v=abc#x` → `main.js`). */\nfunction stripQueryHash(url: string): string {\n return url.split(/[?#]/)[0] ?? url;\n}\n\n/**\n * Resolve uma ref local para caminho absoluto no filesystem.\n * `htmlRoot` é a raiz do site (dir do `index.html`); `resourceDir` é o dir do\n * recurso que contém a ref (p/ `url()` no CSS, é o dir do `.css`). Refs que\n * começam com `/` são relativas à raiz do site.\n */\nfunction resolveLocal(htmlRoot: string, resourceDir: string, url: string): string {\n const clean = stripQueryHash(url);\n return clean.startsWith('/') ? resolve(htmlRoot, `.${clean}`) : resolve(resourceDir, clean);\n}\n\n/** Lê um arquivo local e devolve uma `data:` URI base64. */\nexport function toDataUri(filePath: string): string {\n const bytes = readFileSync(filePath);\n const mime = MIME_TYPES[extname(filePath).toLowerCase()] ?? 'application/octet-stream';\n return `data:${mime};base64,${bytes.toString('base64')}`;\n}\n\n/**\n * Resolve `url(...)` dentro de um bloco CSS, trocando refs locais por `data:`\n * URIs. `cssDir` é o dir do arquivo CSS (ou do HTML, p/ `<style>` inline).\n */\nexport function inlineCssUrls(css: string, htmlRoot: string, cssDir: string): string {\n return css.replace(/url\\(\\s*(['\"]?)([^'\")]+)\\1\\s*\\)/gi, (match, _quote, rawUrl: string) => {\n if (isExternalOrInline(rawUrl)) return match;\n const filePath = resolveLocal(htmlRoot, cssDir, rawUrl);\n return `url(${toDataUri(filePath)})`;\n });\n}\n\n/** Inlina `<script src=\"local\">` → `<script>…</script>` (escapando `</script>`). */\nfunction inlineScripts(root: HTMLElement, htmlRoot: string): void {\n for (const el of root.querySelectorAll('script[src]')) {\n const src = el.getAttribute('src');\n if (!src || isExternalOrInline(src)) continue;\n const js = readFileSync(resolveLocal(htmlRoot, htmlRoot, src), 'utf-8').replace(\n /<\\/script>/gi,\n '<\\\\/script>',\n );\n el.removeAttribute('src');\n el.set_content(js);\n }\n}\n\n/** Inlina `<link rel=\"stylesheet\" href=\"local\">` → `<style>…</style>` (com `url()` resolvido). */\nfunction inlineStylesheets(root: HTMLElement, htmlRoot: string): void {\n for (const el of root.querySelectorAll('link[rel=\"stylesheet\"]')) {\n const href = el.getAttribute('href');\n if (!href || isExternalOrInline(href)) continue;\n const cssPath = resolveLocal(htmlRoot, htmlRoot, href);\n const css = inlineCssUrls(readFileSync(cssPath, 'utf-8'), htmlRoot, dirname(cssPath));\n el.replaceWith(...parse(`<style>${css}</style>`).childNodes);\n }\n}\n\n/** Resolve `url()` em blocos `<style>` já existentes no HTML. */\nfunction inlineExistingStyleBlocks(root: HTMLElement, htmlRoot: string): void {\n for (const el of root.querySelectorAll('style')) {\n const css = el.innerHTML;\n if (!css.includes('url(')) continue;\n el.set_content(inlineCssUrls(css, htmlRoot, htmlRoot));\n }\n}\n\n/** Converte mídias locais referenciadas por atributo (`src`/`href`) em `data:` URIs. */\nfunction inlineMediaAttributes(root: HTMLElement, htmlRoot: string): void {\n const targets: Array<[selector: string, attr: string]> = [\n ['img[src]', 'src'],\n ['source[src]', 'src'],\n ['audio[src]', 'src'],\n ['video[src]', 'src'],\n ['link[rel=\"icon\"]', 'href'],\n ['link[rel=\"shortcut icon\"]', 'href'],\n ['link[rel=\"apple-touch-icon\"]', 'href'],\n ['link[rel=\"preload\"]', 'href'],\n ];\n for (const [selector, attr] of targets) {\n for (const el of root.querySelectorAll(selector)) {\n const ref = el.getAttribute(attr);\n if (!ref || isExternalOrInline(ref)) continue;\n el.setAttribute(attr, toDataUri(resolveLocal(htmlRoot, htmlRoot, ref)));\n }\n }\n}\n\n/**\n * Lê o HTML de entrada e devolve o HTML consolidado self-contained\n * (JS/CSS inline, mídias em `data:` URI). Não escreve nada em disco.\n */\nexport function inlineHtml(htmlPath: string): string {\n const htmlRoot = dirname(resolve(htmlPath));\n const root = parse(readFileSync(htmlPath, 'utf-8'), {\n comment: true,\n voidTag: { closingSlash: true },\n });\n inlineScripts(root, htmlRoot);\n inlineStylesheets(root, htmlRoot);\n inlineExistingStyleBlocks(root, htmlRoot);\n inlineMediaAttributes(root, htmlRoot);\n return root.toString();\n}\n\nexport interface BundleResult {\n /** HTML consolidado self-contained. */\n html: string;\n /** Bytes do `.zip` (entry único `index.html` na raiz). */\n zipBytes: Uint8Array;\n /** Tamanho do `.zip` em bytes. */\n size: number;\n /** SHA-256 dos bytes do `.zip`, no formato `sha256-<hex>`. */\n hash: string;\n /** `true` se `size` excede `maxBytes`. */\n overLimit: boolean;\n}\n\n/**\n * Constrói o bundle a partir do `index.html` de entrada: inlina, zipa\n * (entry `index.html` na raiz, mtime fixo p/ hash determinístico) e calcula\n * tamanho + SHA-256. Puro — não escreve o `.zip` (responsabilidade do caller).\n */\nexport function buildBundle(htmlPath: string, maxBytes: number = DEFAULT_MAX_BYTES): BundleResult {\n const html = inlineHtml(htmlPath);\n const zipBytes = zipSync({ 'index.html': [strToU8(html), { mtime: FIXED_MTIME }] }, { level: 6 });\n const size = zipBytes.length;\n const hash = `sha256-${createHash('sha256').update(zipBytes).digest('hex')}`;\n return { html, zipBytes, size, hash, overLimit: size > maxBytes };\n}\n\nexport interface CliArgs {\n htmlPath: string;\n outPath: string;\n maxBytes: number;\n help: boolean;\n}\n\nconst DEFAULT_OUT = 'dist/whitelabel-bundle.zip';\n\n/** Parse mínimo de argv (sem deps): `--html/-i`, `--out/-o`, `--max-bytes`, `--help/-h`. */\nexport function parseArgs(argv: string[]): CliArgs {\n const args: CliArgs = {\n htmlPath: '',\n outPath: DEFAULT_OUT,\n maxBytes: DEFAULT_MAX_BYTES,\n help: false,\n };\n for (let i = 0; i < argv.length; i++) {\n const token = argv[i];\n switch (token) {\n case '--help':\n case '-h':\n args.help = true;\n break;\n case '--html':\n case '-i':\n args.htmlPath = argv[++i] ?? '';\n break;\n case '--out':\n case '-o':\n args.outPath = argv[++i] ?? DEFAULT_OUT;\n break;\n case '--max-bytes':\n args.maxBytes = Number.parseInt(argv[++i] ?? '', 10) || DEFAULT_MAX_BYTES;\n break;\n case 'build':\n // Subcomando (`condor-mobile-bridge build …`) — ignorado aqui.\n break;\n default:\n // Primeiro posicional vira o html de entrada (ergonomia: `build dist/index.html`).\n if (token && !token.startsWith('-') && !args.htmlPath) args.htmlPath = token;\n }\n }\n return args;\n}\n\nexport const USAGE = `condor-mobile-bridge build — gera o .zip white-label self-contained.\n\nUso:\n npx condor-mobile-bridge build -i <html> [-o <zip>]\n\nOpções:\n -i, --html <path> HTML de entrada (ex.: dist/index.html). Obrigatório.\n -o, --out <path> .zip de saída (default: ${DEFAULT_OUT}).\n --max-bytes <n> Limite de aviso em bytes (default: ${DEFAULT_MAX_BYTES} = 2 MB).\n -h, --help Mostra esta ajuda.`;\n"],"mappings":"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","names":["import_node_fs","import_node_path"]}
|