@condorinst/mobile-bridge 0.19.2 → 0.21.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/AGENTS.md +100 -42
- package/CHANGELOG.md +106 -0
- package/README.md +64 -14
- package/contract/bridge-contract.json +70 -25
- package/dist/cli.cjs +436 -5
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +433 -3
- package/dist/cli.js.map +1 -1
- package/dist/dev-harness.global.js +2110 -0
- package/dist/dev.cjs +1662 -0
- package/dist/dev.cjs.map +1 -0
- package/dist/dev.d.cts +683 -0
- package/dist/dev.d.ts +683 -0
- package/dist/dev.js +1611 -0
- package/dist/dev.js.map +1 -0
- package/dist/index.cjs +96 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +265 -19
- package/dist/index.d.ts +265 -19
- package/dist/index.js +94 -3
- package/dist/index.js.map +1 -1
- package/docs/agent-guide.md +83 -25
- package/docs/bridge-contract.md +154 -25
- package/docs/client-guide.md +39 -28
- package/docs/device-testing.md +84 -0
- package/docs/pilot-onboarding.md +26 -12
- package/docs/troubleshooting.md +83 -18
- package/examples/minimal-home/AGENTS.md +9 -6
- package/examples/minimal-home/README.md +10 -5
- package/examples/minimal-home/build.mjs +2 -1
- package/examples/minimal-home/index.html +2 -0
- package/examples/minimal-home/package.json +3 -2
- package/examples/minimal-home/src/main.ts +151 -73
- package/llms-full.txt +2041 -0
- package/llms.txt +15 -13
- package/package.json +9 -3
package/dist/cli.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
4
|
import { mkdirSync, writeFileSync } from "fs";
|
|
5
|
-
import { dirname as
|
|
5
|
+
import { dirname as dirname3, resolve as resolve3 } from "path";
|
|
6
6
|
|
|
7
7
|
// src/cli-core.ts
|
|
8
8
|
import { createHash } from "crypto";
|
|
@@ -167,8 +167,16 @@ var BLOCKED_API_RULES = [
|
|
|
167
167
|
message: "connect-src 'self' on a null origin blocks every network call. Request data through the bridge instead (e.g. BridgeClient.fetchSensorLog)."
|
|
168
168
|
}
|
|
169
169
|
];
|
|
170
|
+
var DEV_HARNESS_MARKER = "__condorDevHarness__";
|
|
170
171
|
function lintClientHtml(html) {
|
|
171
172
|
const findings = [];
|
|
173
|
+
if (html.includes(DEV_HARNESS_MARKER)) {
|
|
174
|
+
findings.push({
|
|
175
|
+
severity: "error",
|
|
176
|
+
rule: "dev-harness-bundled",
|
|
177
|
+
message: "The bundle contains the development harness (@condorinst/mobile-bridge/dev). It belongs in the page that hosts the bundle during development, never in the bundle: remove the import."
|
|
178
|
+
});
|
|
179
|
+
}
|
|
172
180
|
for (const { rule, pattern, message } of BLOCKED_API_RULES) {
|
|
173
181
|
if (pattern.test(html)) findings.push({ severity: "warning", rule, message });
|
|
174
182
|
}
|
|
@@ -276,6 +284,7 @@ function validateBundle(inputPath, maxBytes = DEFAULT_MAX_BYTES) {
|
|
|
276
284
|
};
|
|
277
285
|
}
|
|
278
286
|
var DEFAULT_OUT = "dist/whitelabel-bundle.zip";
|
|
287
|
+
var DEFAULT_DEV_PORT = 5173;
|
|
279
288
|
function parseArgs(argv) {
|
|
280
289
|
const args = {
|
|
281
290
|
command: "build",
|
|
@@ -283,6 +292,9 @@ function parseArgs(argv) {
|
|
|
283
292
|
outPath: DEFAULT_OUT,
|
|
284
293
|
maxBytes: DEFAULT_MAX_BYTES,
|
|
285
294
|
strict: false,
|
|
295
|
+
scenario: "default",
|
|
296
|
+
port: DEFAULT_DEV_PORT,
|
|
297
|
+
nativeVersion: void 0,
|
|
286
298
|
help: false
|
|
287
299
|
};
|
|
288
300
|
for (let i = 0; i < argv.length; i++) {
|
|
@@ -313,6 +325,18 @@ function parseArgs(argv) {
|
|
|
313
325
|
case "validate":
|
|
314
326
|
args.command = "validate";
|
|
315
327
|
break;
|
|
328
|
+
case "dev":
|
|
329
|
+
args.command = "dev";
|
|
330
|
+
break;
|
|
331
|
+
case "--scenario":
|
|
332
|
+
args.scenario = argv[++i] ?? "default";
|
|
333
|
+
break;
|
|
334
|
+
case "--native-version":
|
|
335
|
+
args.nativeVersion = argv[++i] ?? "null";
|
|
336
|
+
break;
|
|
337
|
+
case "--port":
|
|
338
|
+
args.port = Number.parseInt(argv[++i] ?? "", 10) || DEFAULT_DEV_PORT;
|
|
339
|
+
break;
|
|
316
340
|
default:
|
|
317
341
|
if (token && !token.startsWith("-") && !args.htmlPath) args.htmlPath = token;
|
|
318
342
|
}
|
|
@@ -324,24 +348,421 @@ var USAGE = `condor-mobile-bridge \u2014 tooling for Condor Canvas client bundle
|
|
|
324
348
|
Usage:
|
|
325
349
|
npx condor-mobile-bridge build -i <html> [-o <zip>]
|
|
326
350
|
npx condor-mobile-bridge validate -i <zip|html> [--strict]
|
|
351
|
+
npx condor-mobile-bridge dev -i <html|zip|dir> [--scenario <name>] [--native-version <v|null>] [--port <n>]
|
|
327
352
|
|
|
328
353
|
Commands:
|
|
329
354
|
build Inline a dist/index.html into a self-contained .zip.
|
|
330
355
|
validate Check a bundle against every rule the Canvas app enforces, plus
|
|
331
356
|
the APIs the sandbox and the CSP block at runtime.
|
|
357
|
+
dev Serve the bundle in a browser harness that behaves like the app:
|
|
358
|
+
same sandbox, shell and policy, and a simulated native side.
|
|
359
|
+
Re-reads the input on every reload. Scenarios: default,
|
|
360
|
+
cold-start, no-devices, shared-serial, offline.
|
|
332
361
|
|
|
333
362
|
Options:
|
|
334
363
|
-i, --html <path> Input. build: dist/index.html. validate: .zip or .html. Required.
|
|
335
364
|
-o, --out <path> build only: output .zip (default: ${DEFAULT_OUT}).
|
|
336
365
|
--max-bytes <n> Size limit in bytes (default: ${DEFAULT_MAX_BYTES} = 2 MB).
|
|
337
366
|
--strict validate only: exit 1 on warnings too, not just errors.
|
|
367
|
+
--scenario <n> dev only: device behaviour to simulate (default: default).
|
|
368
|
+
--port <n> dev only: HTTP port (default: ${DEFAULT_DEV_PORT}).
|
|
369
|
+
--native-version <v|null>
|
|
370
|
+
dev only: nativeVersion the harness reports (default
|
|
371
|
+
0.17.0). 'null' reproduces a shell that could not
|
|
372
|
+
reach the native side.
|
|
338
373
|
-h, --help Show this help.`;
|
|
339
374
|
|
|
375
|
+
// src/cli-dev.ts
|
|
376
|
+
import { existsSync, readFileSync as readFileSync2, statSync } from "fs";
|
|
377
|
+
import { createServer } from "http";
|
|
378
|
+
import { dirname as dirname2, extname as extname2, join, resolve as resolve2 } from "path";
|
|
379
|
+
import { fileURLToPath } from "url";
|
|
380
|
+
import { strFromU8, unzipSync as unzipSync2 } from "fflate";
|
|
381
|
+
|
|
382
|
+
// src/keys.ts
|
|
383
|
+
var BRIDGE_KEYS = {
|
|
384
|
+
pingPong: "pingPong",
|
|
385
|
+
userDevice: "userDevice",
|
|
386
|
+
deviceLightSensors: "deviceLightSensors",
|
|
387
|
+
syncTime: "syncTime",
|
|
388
|
+
realTime: "realTime",
|
|
389
|
+
deleteMemory: "deleteMemory",
|
|
390
|
+
deviceNotification: "deviceNotification",
|
|
391
|
+
deviceConnection: "deviceConnection",
|
|
392
|
+
sensorLog: "sensorLog",
|
|
393
|
+
dfu: "dfu",
|
|
394
|
+
systemStatus: "systemStatus",
|
|
395
|
+
/**
|
|
396
|
+
* Native → client: the OS back button was pressed and the client had
|
|
397
|
+
* reported internal history via `reportNavigation(true)`. Go back exactly
|
|
398
|
+
* ONE internal page. Emitted only when the native side decided to delegate
|
|
399
|
+
* the back gesture. See `BridgeClient.onBack`.
|
|
400
|
+
*/
|
|
401
|
+
navigationBack: "navigationBack",
|
|
402
|
+
/**
|
|
403
|
+
* Native → client: the app's language state. Carries the raw device locale
|
|
404
|
+
* AND the language explicitly chosen in the app Settings, so the client can
|
|
405
|
+
* apply its own precedence rule — including rendering a language the native
|
|
406
|
+
* chrome does not support. Emitted on attach, after `handshake-ack`, and on
|
|
407
|
+
* every change in Settings. See `LanguagePayload`.
|
|
408
|
+
*/
|
|
409
|
+
language: "language"
|
|
410
|
+
};
|
|
411
|
+
|
|
412
|
+
// src/dev/policy.ts
|
|
413
|
+
var CANVAS_POLICY = {
|
|
414
|
+
readOnlyKeys: [
|
|
415
|
+
BRIDGE_KEYS.userDevice,
|
|
416
|
+
BRIDGE_KEYS.deviceConnection,
|
|
417
|
+
BRIDGE_KEYS.deviceLightSensors,
|
|
418
|
+
BRIDGE_KEYS.deviceNotification,
|
|
419
|
+
BRIDGE_KEYS.pingPong,
|
|
420
|
+
BRIDGE_KEYS.realTime,
|
|
421
|
+
BRIDGE_KEYS.syncTime,
|
|
422
|
+
BRIDGE_KEYS.deleteMemory,
|
|
423
|
+
BRIDGE_KEYS.sensorLog,
|
|
424
|
+
BRIDGE_KEYS.systemStatus,
|
|
425
|
+
BRIDGE_KEYS.navigationBack,
|
|
426
|
+
BRIDGE_KEYS.language
|
|
427
|
+
],
|
|
428
|
+
allowedCommands: [
|
|
429
|
+
"userDevice",
|
|
430
|
+
"realTime",
|
|
431
|
+
"openSettings",
|
|
432
|
+
"deviceConnection",
|
|
433
|
+
"ping",
|
|
434
|
+
"jsError",
|
|
435
|
+
"sensorLog",
|
|
436
|
+
"setStatusBarColor",
|
|
437
|
+
"requestPermission",
|
|
438
|
+
"navigationState",
|
|
439
|
+
"language"
|
|
440
|
+
],
|
|
441
|
+
requiresConfirmation: ["syncTime", "deleteMemory", "logout"],
|
|
442
|
+
deniedCommands: ["dfu"]
|
|
443
|
+
};
|
|
444
|
+
var CANVAS_CSP = "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self' data:; connect-src 'self'; frame-src 'self'; object-src 'none'; base-uri 'none'; form-action 'none';";
|
|
445
|
+
|
|
446
|
+
// src/dev/fixtures.ts
|
|
447
|
+
function config(spec) {
|
|
448
|
+
const item = {
|
|
449
|
+
deviceId: spec.deviceId,
|
|
450
|
+
serialNo: spec.serialNo,
|
|
451
|
+
deviceBodyPart: spec.bodyPart,
|
|
452
|
+
bleConnectionPeriod: 5,
|
|
453
|
+
autosync: true,
|
|
454
|
+
eraseDeviceMemory: false,
|
|
455
|
+
autoEraseDeviceMemory: false,
|
|
456
|
+
autoEraseDeviceMemoryThreshold: 80,
|
|
457
|
+
readSensorsMemory: false,
|
|
458
|
+
startDevice: false,
|
|
459
|
+
readFullMemory: false,
|
|
460
|
+
allowFirmwareUpdate: true
|
|
461
|
+
};
|
|
462
|
+
if (spec.modelId) item.modelId = spec.modelId;
|
|
463
|
+
return item;
|
|
464
|
+
}
|
|
465
|
+
function telemetry(spec, now) {
|
|
466
|
+
return {
|
|
467
|
+
serialNo: spec.serialNo,
|
|
468
|
+
deviceId: spec.deviceId,
|
|
469
|
+
updataDateTime: now - spec.syncedMinutesAgo * 6e4,
|
|
470
|
+
deviceName: spec.name,
|
|
471
|
+
deviceVersion: spec.modelId === "actlumus_plus" ? "3.0.2" : "2.1.0",
|
|
472
|
+
hardwareVersion: spec.modelId === "actlumus_plus" ? "B" : "A",
|
|
473
|
+
deviceDateTime: now - spec.syncedMinutesAgo * 6e4,
|
|
474
|
+
memoryDataUsage: 37.5,
|
|
475
|
+
memorySystemUsage: 4.2,
|
|
476
|
+
batteryLevel: spec.battery,
|
|
477
|
+
batteryVoltage: 3.7 + spec.battery / 1e3,
|
|
478
|
+
isCharging: spec.charging,
|
|
479
|
+
isOnDock: spec.charging,
|
|
480
|
+
syncErrors: spec.syncError ? [
|
|
481
|
+
{
|
|
482
|
+
serialNo: spec.serialNo,
|
|
483
|
+
attemptDate: now - 10 * 6e4,
|
|
484
|
+
message: spec.syncError,
|
|
485
|
+
messageCode: "SYNC_FAILED"
|
|
486
|
+
}
|
|
487
|
+
] : []
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
function userConfig(spec) {
|
|
491
|
+
return {
|
|
492
|
+
serialNo: spec.serialNo,
|
|
493
|
+
deviceId: spec.deviceId,
|
|
494
|
+
syncTime: false,
|
|
495
|
+
realTime: false,
|
|
496
|
+
deleteMemory: false,
|
|
497
|
+
readAllMemory: false,
|
|
498
|
+
readAllSystemLog: false
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
function payloadOf(specs, now = Date.now()) {
|
|
502
|
+
return {
|
|
503
|
+
lastUpdate: now,
|
|
504
|
+
devicesConfig: specs.map(config),
|
|
505
|
+
devicesTelemetry: specs.map((s) => telemetry(s, now)),
|
|
506
|
+
devicesUserConfig: specs.map(userConfig)
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
function twoDevices(now = Date.now()) {
|
|
510
|
+
return payloadOf(
|
|
511
|
+
[
|
|
512
|
+
{
|
|
513
|
+
deviceId: 9001,
|
|
514
|
+
serialNo: 4534,
|
|
515
|
+
modelId: "actlumus",
|
|
516
|
+
bodyPart: "wrist",
|
|
517
|
+
name: "ActLumus 4534",
|
|
518
|
+
battery: 82,
|
|
519
|
+
charging: false,
|
|
520
|
+
syncedMinutesAgo: 12
|
|
521
|
+
},
|
|
522
|
+
{
|
|
523
|
+
deviceId: 9002,
|
|
524
|
+
serialNo: 5120,
|
|
525
|
+
modelId: "actlumus_plus",
|
|
526
|
+
bodyPart: "necklace",
|
|
527
|
+
name: "ActLumus Plus 5120",
|
|
528
|
+
battery: 41,
|
|
529
|
+
charging: true,
|
|
530
|
+
syncedMinutesAgo: 190,
|
|
531
|
+
syncError: "Could not read the device memory."
|
|
532
|
+
}
|
|
533
|
+
],
|
|
534
|
+
now
|
|
535
|
+
);
|
|
536
|
+
}
|
|
537
|
+
function sharedSerial(now = Date.now()) {
|
|
538
|
+
return payloadOf(
|
|
539
|
+
[
|
|
540
|
+
{
|
|
541
|
+
deviceId: 9101,
|
|
542
|
+
serialNo: 7047,
|
|
543
|
+
bodyPart: "wrist",
|
|
544
|
+
name: "ActLumus 7047",
|
|
545
|
+
battery: 67,
|
|
546
|
+
charging: false,
|
|
547
|
+
syncedMinutesAgo: 30
|
|
548
|
+
},
|
|
549
|
+
{
|
|
550
|
+
deviceId: 9102,
|
|
551
|
+
serialNo: 7047,
|
|
552
|
+
modelId: "actlumus_plus",
|
|
553
|
+
bodyPart: "necklace",
|
|
554
|
+
name: "ActLumus Plus 7047",
|
|
555
|
+
battery: 93,
|
|
556
|
+
charging: false,
|
|
557
|
+
syncedMinutesAgo: 8
|
|
558
|
+
}
|
|
559
|
+
],
|
|
560
|
+
now
|
|
561
|
+
);
|
|
562
|
+
}
|
|
563
|
+
function systemStatus(overrides = {}) {
|
|
564
|
+
return {
|
|
565
|
+
bluetooth: true,
|
|
566
|
+
internet: true,
|
|
567
|
+
permission: true,
|
|
568
|
+
hasDevices: true,
|
|
569
|
+
missingOptionalPermissions: [],
|
|
570
|
+
missingEssentialPermissions: [],
|
|
571
|
+
deviceSyncError: {},
|
|
572
|
+
deviceSyncErrorById: {},
|
|
573
|
+
...overrides
|
|
574
|
+
};
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
// src/dev/scenarios.ts
|
|
578
|
+
var HARNESS_NATIVE_VERSION = "0.17.0";
|
|
579
|
+
var base = {
|
|
580
|
+
description: "Two devices, background service already running, realistic latency.",
|
|
581
|
+
nativeVersion: HARNESS_NATIVE_VERSION,
|
|
582
|
+
latencyMs: [80, 250],
|
|
583
|
+
serviceStartMs: 0,
|
|
584
|
+
userDeviceSilent: false,
|
|
585
|
+
logPeriodS: 60,
|
|
586
|
+
systemStatus: systemStatus(),
|
|
587
|
+
devices: () => twoDevices()
|
|
588
|
+
};
|
|
589
|
+
var SCENARIOS = {
|
|
590
|
+
default: base,
|
|
591
|
+
"cold-start": {
|
|
592
|
+
...base,
|
|
593
|
+
description: "App launched from cold: the background service takes 4 s to start, and every ping/userDevice sent before that is lost. A bundle that asks once stays on its loading screen forever.",
|
|
594
|
+
serviceStartMs: 4e3
|
|
595
|
+
},
|
|
596
|
+
"no-devices": {
|
|
597
|
+
...base,
|
|
598
|
+
description: "Account with no linked device: userDevice never answers and systemStatus.hasDevices is false. Your bundle must show an empty state.",
|
|
599
|
+
userDeviceSilent: true,
|
|
600
|
+
systemStatus: systemStatus({ hasDevices: false })
|
|
601
|
+
},
|
|
602
|
+
"shared-serial": {
|
|
603
|
+
...base,
|
|
604
|
+
description: "An ActLumus and an ActLumus Plus share serial number 7047. Only deviceId tells them apart; sensorLog cannot address one of them.",
|
|
605
|
+
devices: () => sharedSerial()
|
|
606
|
+
},
|
|
607
|
+
offline: {
|
|
608
|
+
...base,
|
|
609
|
+
description: "No internet. systemStatus.internet is false and sensorLog fails; userDevice answers from the cache.",
|
|
610
|
+
sensorLogError: "Connection failed",
|
|
611
|
+
systemStatus: systemStatus({ internet: false })
|
|
612
|
+
}
|
|
613
|
+
};
|
|
614
|
+
|
|
615
|
+
// src/cli-dev.ts
|
|
616
|
+
function readClientHtml(input) {
|
|
617
|
+
const path = resolve2(input);
|
|
618
|
+
if (existsSync(path) && statSync(path).isDirectory()) {
|
|
619
|
+
return inlineHtml(join(path, "index.html"));
|
|
620
|
+
}
|
|
621
|
+
if (extname2(path).toLowerCase() === ".zip") {
|
|
622
|
+
const files = unzipSync2(new Uint8Array(readFileSync2(path)));
|
|
623
|
+
const entry = files["index.html"];
|
|
624
|
+
if (!entry) throw new Error(`${input}: no index.html at the root of the zip.`);
|
|
625
|
+
return strFromU8(entry);
|
|
626
|
+
}
|
|
627
|
+
return inlineHtml(path);
|
|
628
|
+
}
|
|
629
|
+
var escapeHtml = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
630
|
+
function harnessPage(scenario, nativeVersion) {
|
|
631
|
+
const options = Object.entries(SCENARIOS).map(
|
|
632
|
+
([name, s]) => `<option value="${name}" title="${escapeHtml(s.description)}"${name === scenario ? " selected" : ""}>${name}</option>`
|
|
633
|
+
).join("");
|
|
634
|
+
const description = escapeHtml(SCENARIOS[scenario]?.description ?? "");
|
|
635
|
+
const scenarioJson = JSON.stringify(scenario).replace(/</g, "\\u003c");
|
|
636
|
+
const nativeJson = JSON.stringify(
|
|
637
|
+
nativeVersion === void 0 ? void 0 : nativeVersion === "null" ? null : nativeVersion
|
|
638
|
+
)?.replace(/</g, "\\u003c") ?? "undefined";
|
|
639
|
+
const nativeLabel = escapeHtml(
|
|
640
|
+
nativeVersion === void 0 ? "" : `nativeVersion ${nativeVersion}`
|
|
641
|
+
);
|
|
642
|
+
return `<!doctype html>
|
|
643
|
+
<html lang="en">
|
|
644
|
+
<head>
|
|
645
|
+
<meta charset="utf-8">
|
|
646
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
647
|
+
<meta http-equiv="Content-Security-Policy" content="${CANVAS_CSP}">
|
|
648
|
+
<title>Condor Canvas \u2014 dev harness</title>
|
|
649
|
+
<style>
|
|
650
|
+
body { margin: 0; font: 13px system-ui, sans-serif; background: #e9ebef; color: #1b1f24; }
|
|
651
|
+
header { display: flex; gap: 12px; align-items: center; padding: 8px 16px; background: #1b1f24; color: #fff; flex-wrap: wrap; }
|
|
652
|
+
header p { margin: 0; opacity: .8; flex: 1 1 320px; }
|
|
653
|
+
main { display: flex; justify-content: center; padding: 16px; }
|
|
654
|
+
#device { width: 390px; height: 780px; max-width: 100%; border-radius: 28px; overflow: hidden; box-shadow: 0 10px 40px rgba(0,0,0,.25); background: #fff; }
|
|
655
|
+
</style>
|
|
656
|
+
</head>
|
|
657
|
+
<body>
|
|
658
|
+
<header>
|
|
659
|
+
<strong>Condor Canvas \xB7 dev harness</strong>
|
|
660
|
+
<label>Scenario <select id="scenario">${options}</select></label>
|
|
661
|
+
<button id="back" type="button" title="The OS back button: reaches the bundle only after it reported canGoBack true">\u25C0 OS back</button>
|
|
662
|
+
<span>${nativeLabel}</span>
|
|
663
|
+
<p>${description}</p>
|
|
664
|
+
</header>
|
|
665
|
+
<main><div id="device"></div></main>
|
|
666
|
+
<script src="/__condor/harness.js"></script>
|
|
667
|
+
<script>
|
|
668
|
+
var scenario = ${scenarioJson};
|
|
669
|
+
var nativeVersion = ${nativeJson};
|
|
670
|
+
var harness = null;
|
|
671
|
+
document.getElementById('scenario').addEventListener('change', function (e) {
|
|
672
|
+
var params = new URLSearchParams(location.search);
|
|
673
|
+
params.set('scenario', e.target.value);
|
|
674
|
+
location.search = params.toString();
|
|
675
|
+
});
|
|
676
|
+
document.getElementById('back').addEventListener('click', function () {
|
|
677
|
+
if (harness) harness.pressBack();
|
|
678
|
+
});
|
|
679
|
+
var chosen = CondorDevHarness.SCENARIOS[scenario];
|
|
680
|
+
if (nativeVersion !== undefined) chosen = Object.assign({}, chosen, { nativeVersion: nativeVersion });
|
|
681
|
+
fetch('/__condor/client.html')
|
|
682
|
+
.then(function (r) { if (!r.ok) throw new Error('could not read the bundle: HTTP ' + r.status); return r.text(); })
|
|
683
|
+
.then(function (html) {
|
|
684
|
+
return CondorDevHarness.createDevHarness({ html: html, scenario: chosen, mount: document.getElementById('device') });
|
|
685
|
+
})
|
|
686
|
+
.then(function (h) { harness = h; })
|
|
687
|
+
.catch(function (err) { document.getElementById('device').textContent = String(err); console.error(err); });
|
|
688
|
+
</script>
|
|
689
|
+
</body>
|
|
690
|
+
</html>`;
|
|
691
|
+
}
|
|
692
|
+
function harnessScriptPath() {
|
|
693
|
+
const here = typeof __dirname !== "undefined" ? __dirname : dirname2(fileURLToPath(import.meta.url));
|
|
694
|
+
return join(here, "dev-harness.global.js");
|
|
695
|
+
}
|
|
696
|
+
function startDevServer(opts) {
|
|
697
|
+
if (!(opts.scenario in SCENARIOS)) {
|
|
698
|
+
throw new Error(
|
|
699
|
+
`Unknown scenario "${opts.scenario}". Known: ${Object.keys(SCENARIOS).join(", ")}.`
|
|
700
|
+
);
|
|
701
|
+
}
|
|
702
|
+
readClientHtml(opts.input);
|
|
703
|
+
const server = createServer((req, res) => {
|
|
704
|
+
const url = new URL(req.url ?? "/", "http://localhost");
|
|
705
|
+
try {
|
|
706
|
+
if (url.pathname === "/__condor/client.html") {
|
|
707
|
+
res.writeHead(200, {
|
|
708
|
+
"content-type": "text/html; charset=utf-8",
|
|
709
|
+
"cache-control": "no-store"
|
|
710
|
+
});
|
|
711
|
+
res.end(readClientHtml(opts.input));
|
|
712
|
+
return;
|
|
713
|
+
}
|
|
714
|
+
if (url.pathname === "/__condor/harness.js") {
|
|
715
|
+
res.writeHead(200, {
|
|
716
|
+
"content-type": "text/javascript; charset=utf-8",
|
|
717
|
+
"cache-control": "no-store"
|
|
718
|
+
});
|
|
719
|
+
res.end(readFileSync2(harnessScriptPath()));
|
|
720
|
+
return;
|
|
721
|
+
}
|
|
722
|
+
if (url.pathname === "/") {
|
|
723
|
+
const scenario = url.searchParams.get("scenario") ?? opts.scenario;
|
|
724
|
+
res.writeHead(200, {
|
|
725
|
+
"content-type": "text/html; charset=utf-8",
|
|
726
|
+
"content-security-policy": CANVAS_CSP,
|
|
727
|
+
"cache-control": "no-store"
|
|
728
|
+
});
|
|
729
|
+
const nativeVersion = url.searchParams.get("nativeVersion") ?? opts.nativeVersion;
|
|
730
|
+
res.end(harnessPage(scenario in SCENARIOS ? scenario : opts.scenario, nativeVersion));
|
|
731
|
+
return;
|
|
732
|
+
}
|
|
733
|
+
res.writeHead(404).end();
|
|
734
|
+
} catch (err) {
|
|
735
|
+
res.writeHead(500, { "content-type": "text/plain; charset=utf-8" });
|
|
736
|
+
res.end(String(err instanceof Error ? err.message : err));
|
|
737
|
+
}
|
|
738
|
+
});
|
|
739
|
+
server.listen(opts.port, "127.0.0.1");
|
|
740
|
+
return server;
|
|
741
|
+
}
|
|
742
|
+
|
|
340
743
|
// src/cli.ts
|
|
744
|
+
function runDev(input, scenario, port, nativeVersion) {
|
|
745
|
+
const server = startDevServer({ input, scenario, port, nativeVersion });
|
|
746
|
+
server.on("listening", () => {
|
|
747
|
+
const native = nativeVersion === void 0 ? "" : `&nativeVersion=${nativeVersion}`;
|
|
748
|
+
console.log(
|
|
749
|
+
`Condor Canvas dev harness: http://localhost:${port}/?scenario=${scenario}${native}`
|
|
750
|
+
);
|
|
751
|
+
console.log(` bundle: ${input} (re-read on every reload)`);
|
|
752
|
+
console.log(" sandbox, shell policy and CSP: same as the app");
|
|
753
|
+
console.log(" scenarios: default, cold-start, no-devices, shared-serial, offline");
|
|
754
|
+
console.log(' OS back button: the "\u25C0 OS back" button at the top');
|
|
755
|
+
console.log("\nA bundle is ready for a device only when it handles every scenario.");
|
|
756
|
+
});
|
|
757
|
+
server.on("error", (err) => {
|
|
758
|
+
console.error(`dev: ${err.message}`);
|
|
759
|
+
process.exit(1);
|
|
760
|
+
});
|
|
761
|
+
}
|
|
341
762
|
function runBuild(htmlPath, outPath, maxBytes) {
|
|
342
763
|
const result = buildBundle(htmlPath, maxBytes);
|
|
343
|
-
const resolvedOut =
|
|
344
|
-
mkdirSync(
|
|
764
|
+
const resolvedOut = resolve3(outPath);
|
|
765
|
+
mkdirSync(dirname3(resolvedOut), { recursive: true });
|
|
345
766
|
writeFileSync(resolvedOut, result.zipBytes);
|
|
346
767
|
const kb = (Buffer.byteLength(result.html) / 1024).toFixed(1);
|
|
347
768
|
console.log(`Built ${outPath}`);
|
|
@@ -396,6 +817,15 @@ function main() {
|
|
|
396
817
|
console.log(USAGE);
|
|
397
818
|
process.exit(args.help ? 0 : 1);
|
|
398
819
|
}
|
|
820
|
+
if (args.command === "dev") {
|
|
821
|
+
try {
|
|
822
|
+
runDev(args.htmlPath, args.scenario, args.port, args.nativeVersion);
|
|
823
|
+
} catch (err) {
|
|
824
|
+
console.error(`dev: ${err instanceof Error ? err.message : String(err)}`);
|
|
825
|
+
process.exit(1);
|
|
826
|
+
}
|
|
827
|
+
return;
|
|
828
|
+
}
|
|
399
829
|
if (args.command === "validate") {
|
|
400
830
|
const result = validateBundle(args.htmlPath, args.maxBytes);
|
|
401
831
|
process.exit(reportValidation(args.htmlPath, result, args.strict));
|