@napplet/conformance-cli 0.3.3 → 0.4.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/README.md +17 -1
- package/dist/cli.js +46 -9
- package/dist/cli.js.map +1 -1
- package/dist/host-bundle.global.js +126 -18
- package/dist/ui/assets/index-CQX23owf.js +5450 -0
- package/dist/ui/index.html +1 -1
- package/package.json +7 -5
- package/dist/ui/assets/index-CS2zxBsY.js +0 -109
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
var NappletConformanceHostBundle = (() => {
|
|
3
3
|
// ../core/dist/index.js
|
|
4
|
-
var NAP_DOMAINS = ["shell", "relay", "identity", "storage", "inc", "theme", "keys", "media", "notify", "config", "resource", "cvm", "outbox", "upload", "intent", "ble", "webrtc", "link", "count", "lists", "serial", "fs", "common", "dm"];
|
|
4
|
+
var NAP_DOMAINS = ["shell", "relay", "identity", "storage", "inc", "theme", "keys", "media", "notify", "config", "resource", "cvm", "outbox", "upload", "intent", "ble", "webrtc", "link", "count", "lists", "serial", "fs", "common", "dm", "gamepad"];
|
|
5
5
|
function createDispatch() {
|
|
6
6
|
const handlers = /* @__PURE__ */ new Map();
|
|
7
7
|
function registerNap2(domain, handler) {
|
|
@@ -32,6 +32,11 @@ var NappletConformanceHostBundle = (() => {
|
|
|
32
32
|
// ../conformance/dist/index.js
|
|
33
33
|
var ID = { id: "string" };
|
|
34
34
|
var ENVELOPE_SPECS = {
|
|
35
|
+
// NAP-GAMEPAD Wire Protocol: all three messages are fire-and-forget, without ids.
|
|
36
|
+
// https://github.com/napplet/naps/pull/108
|
|
37
|
+
"gamepad.subscribe": { dir: "out", forbiddenFields: ["id"], forbiddenReason: "NAP-GAMEPAD fire-and-forget envelopes do not carry it" },
|
|
38
|
+
"gamepad.unsubscribe": { dir: "out", forbiddenFields: ["id"], forbiddenReason: "NAP-GAMEPAD fire-and-forget envelopes do not carry it" },
|
|
39
|
+
"gamepad.state": { dir: "in", fields: { available: "boolean", focused: "boolean", pads: "array" } },
|
|
35
40
|
// Optional NAP-SHELL environment, independent of other domains.
|
|
36
41
|
"shell.ready": { dir: "out" },
|
|
37
42
|
"shell.init": { dir: "in", fields: { capabilities: "object", services: "array" } },
|
|
@@ -462,7 +467,7 @@ var NappletConformanceHostBundle = (() => {
|
|
|
462
467
|
if (field in record) {
|
|
463
468
|
errors.push({
|
|
464
469
|
code: "forbidden-field",
|
|
465
|
-
message: `Field "${field}"
|
|
470
|
+
message: `Field "${field}" cannot be emitted by a napplet: ${spec.forbiddenReason ?? "it must be runtime-derived"}`,
|
|
466
471
|
field
|
|
467
472
|
});
|
|
468
473
|
}
|
|
@@ -564,6 +569,11 @@ var NappletConformanceHostBundle = (() => {
|
|
|
564
569
|
errors.push({ code: "invalid-metadata", message: `Invalid ${name} tag cardinality for kind ${event.kind}` });
|
|
565
570
|
}
|
|
566
571
|
}
|
|
572
|
+
for (const tag of event.tags.filter((tag2) => tag2[0] === "source")) {
|
|
573
|
+
if (tag.length !== 2 || !isSourceLocation(tag[1])) {
|
|
574
|
+
errors.push({ code: "invalid-metadata", message: "source must contain one HTTP(S), SSH, Git, or SCP repository location" });
|
|
575
|
+
}
|
|
576
|
+
}
|
|
567
577
|
for (const tag of event.tags.filter((tag2) => tag2[0] === "i")) {
|
|
568
578
|
if (!tag[1]?.trim() || tag[1].includes("?")) {
|
|
569
579
|
errors.push({ code: "invalid-metadata", message: "Intent advertisements must be non-empty queryless identities" });
|
|
@@ -574,6 +584,16 @@ var NappletConformanceHostBundle = (() => {
|
|
|
574
584
|
warnings.push({ code: "invalid-icon", message: "Ignore invalid icon metadata and use generic artwork (NIP-5D \xA7Icon)" });
|
|
575
585
|
}
|
|
576
586
|
}
|
|
587
|
+
function isSourceLocation(value) {
|
|
588
|
+
if (!value || /^[a-z]:[\\/]/i.test(value)) return false;
|
|
589
|
+
try {
|
|
590
|
+
const { protocol } = new URL(value);
|
|
591
|
+
if (["http:", "https:", "ssh:", "git:"].includes(protocol)) return true;
|
|
592
|
+
if (protocol === "file:") return false;
|
|
593
|
+
} catch {
|
|
594
|
+
}
|
|
595
|
+
return !value.includes("://") && /^(?:[^@\s/:]+@)?[^:\s/]+:[^\s]+$/.test(value);
|
|
596
|
+
}
|
|
577
597
|
var REFERENCE_PUBKEY = "f".repeat(64);
|
|
578
598
|
var REFERENCE_ENDPOINT = { id: "reference-source-endpoint", catalogId: "reference-source" };
|
|
579
599
|
var REFERENCE_BLOB_URL = "https://reference.invalid/blob";
|
|
@@ -609,6 +629,9 @@ var NappletConformanceHostBundle = (() => {
|
|
|
609
629
|
}
|
|
610
630
|
}
|
|
611
631
|
var RESPONDERS = {
|
|
632
|
+
// This transport-neutral mock has no controller API. Production shells use a host reader.
|
|
633
|
+
"gamepad.subscribe": () => ok({ type: "gamepad.state", available: false, reason: "unavailable", focused: false, pads: [] }),
|
|
634
|
+
"gamepad.unsubscribe": none,
|
|
612
635
|
// relay
|
|
613
636
|
"relay.subscribe": (e) => ok({ type: "relay.eose", subId: e.subId }),
|
|
614
637
|
"relay.close": none,
|
|
@@ -971,18 +994,76 @@ var NappletConformanceHostBundle = (() => {
|
|
|
971
994
|
};
|
|
972
995
|
}
|
|
973
996
|
var INTERNAL_BOOT_ERROR = "__nappletConformance.error";
|
|
997
|
+
var INTERNAL_INSTALLED = "__nappletConformance.installed";
|
|
974
998
|
function sleep(ms) {
|
|
975
999
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
976
1000
|
}
|
|
977
1001
|
function escapeHtmlAttr(value) {
|
|
978
1002
|
return value.replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<").replace(/>/g, ">");
|
|
979
1003
|
}
|
|
980
|
-
|
|
1004
|
+
var GAMEPAD_STATE_TOOLS_SOURCE = `(() => {
|
|
1005
|
+
const record = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
1006
|
+
const numberIn = (value, min, max) => typeof value === 'number' && Number.isFinite(value) && value >= min && value <= max;
|
|
1007
|
+
const isGamepadState = (value) => {
|
|
1008
|
+
if (!record(value) || typeof value.available !== 'boolean' || typeof value.focused !== 'boolean' || !Array.isArray(value.pads)) return false;
|
|
1009
|
+
if (value.available) {
|
|
1010
|
+
if (value.reason !== undefined) return false;
|
|
1011
|
+
} else if (value.focused || (value.reason !== 'blocked' && value.reason !== 'unavailable')) return false;
|
|
1012
|
+
return Array.from(value.pads).every((pad) => {
|
|
1013
|
+
if (pad === null) return true;
|
|
1014
|
+
if (!record(pad) || !Number.isInteger(pad.index) || pad.index < 0 || typeof pad.id !== 'string' || typeof pad.mapping !== 'string' || pad.connected !== true) return false;
|
|
1015
|
+
if (typeof pad.timestamp !== 'number' || !Number.isFinite(pad.timestamp) || !Array.isArray(pad.axes) || !Array.isArray(pad.buttons)) return false;
|
|
1016
|
+
if (!value.focused && pad.timestamp !== 0) return false;
|
|
1017
|
+
if (!Array.from(pad.axes).every((axis) => numberIn(axis, -1, 1) && (value.focused || axis === 0))) return false;
|
|
1018
|
+
return Array.from(pad.buttons).every((button) => record(button) && numberIn(button.value, 0, 1) && typeof button.pressed === 'boolean' && typeof button.touched === 'boolean' && (value.focused || (button.value === 0 && !button.pressed && !button.touched)));
|
|
1019
|
+
});
|
|
1020
|
+
};
|
|
1021
|
+
const copyGamepadState = (state) => {
|
|
1022
|
+
const copy = { available: state.available, focused: state.focused };
|
|
1023
|
+
if (state.reason !== undefined) copy.reason = state.reason;
|
|
1024
|
+
copy.pads = state.pads.map((pad) => pad === null ? null : {
|
|
1025
|
+
index: pad.index, id: pad.id, mapping: pad.mapping, connected: pad.connected,
|
|
1026
|
+
timestamp: pad.timestamp, axes: pad.axes.slice(), buttons: pad.buttons.map((button) => ({ value: button.value, pressed: button.pressed, touched: button.touched })),
|
|
1027
|
+
});
|
|
1028
|
+
return copy;
|
|
1029
|
+
};
|
|
1030
|
+
return { isGamepadState, copyGamepadState };
|
|
1031
|
+
})()`;
|
|
1032
|
+
function errorObserver() {
|
|
1033
|
+
return `(() => {
|
|
1034
|
+
const report = (reason) => {
|
|
1035
|
+
try { window.parent.postMessage({ type: '${INTERNAL_BOOT_ERROR}', reason: String(reason && reason.message || reason) }, '*'); }
|
|
1036
|
+
catch {}
|
|
1037
|
+
};
|
|
1038
|
+
window.addEventListener('error', (event) => report(event.error || event.message));
|
|
1039
|
+
window.addEventListener('unhandledrejection', (event) => report(event.reason));
|
|
1040
|
+
})();`;
|
|
1041
|
+
}
|
|
1042
|
+
function runtimePrelude(domains, runtimePreludeSource) {
|
|
1043
|
+
const installed = `window.parent.postMessage({ type: '${INTERNAL_INSTALLED}', installed: !!window.napplet }, '*');`;
|
|
1044
|
+
if (runtimePreludeSource !== void 0) {
|
|
1045
|
+
return `${runtimePreludeSource}
|
|
1046
|
+
;globalThis.NappletShimPrelude.install(${JSON.stringify({ domains })});
|
|
1047
|
+
${installed}`;
|
|
1048
|
+
}
|
|
981
1049
|
return `
|
|
982
1050
|
(() => {
|
|
983
1051
|
const domains = ${JSON.stringify(domains)};
|
|
984
1052
|
const napplet = {};
|
|
985
|
-
for (const domain of domains)
|
|
1053
|
+
for (const domain of domains) {
|
|
1054
|
+
if (domain === 'resource' || domain === 'gamepad') napplet[domain] = {};
|
|
1055
|
+
}
|
|
1056
|
+
const gamepadTools = ${GAMEPAD_STATE_TOOLS_SOURCE};
|
|
1057
|
+
let gamepadState = { available: true, focused: false, pads: [] };
|
|
1058
|
+
const gamepadHandlers = new Set();
|
|
1059
|
+
// Subscribe on first use, like the nap binding, so napplets that never touch
|
|
1060
|
+
// gamepad emit no gamepad traffic for checks to attribute to them.
|
|
1061
|
+
let gamepadSubscribed = false;
|
|
1062
|
+
const gamepadSubscribe = () => {
|
|
1063
|
+
if (gamepadSubscribed) return;
|
|
1064
|
+
gamepadSubscribed = true;
|
|
1065
|
+
window.parent.postMessage({ type: 'gamepad.subscribe' }, '*');
|
|
1066
|
+
};
|
|
986
1067
|
const pending = new Map();
|
|
987
1068
|
let seq = 0;
|
|
988
1069
|
const nextId = () => {
|
|
@@ -1002,7 +1083,18 @@ var NappletConformanceHostBundle = (() => {
|
|
|
1002
1083
|
window.parent.postMessage({ ...message, id }, '*');
|
|
1003
1084
|
});
|
|
1004
1085
|
window.addEventListener('message', (event) => {
|
|
1086
|
+
if (event.source !== window.parent) return;
|
|
1005
1087
|
const msg = event.data;
|
|
1088
|
+
if (gamepadSubscribed && msg && msg.type === 'gamepad.state' && !('id' in msg) && gamepadTools.isGamepadState(msg)) {
|
|
1089
|
+
gamepadState = gamepadTools.copyGamepadState(msg);
|
|
1090
|
+
const recipients = Array.from(gamepadHandlers);
|
|
1091
|
+
for (const entry of recipients) {
|
|
1092
|
+
if (!gamepadHandlers.has(entry)) continue;
|
|
1093
|
+
try { entry.handler(gamepadTools.copyGamepadState(gamepadState)); }
|
|
1094
|
+
catch (error) { queueMicrotask(() => { throw error; }); }
|
|
1095
|
+
}
|
|
1096
|
+
return;
|
|
1097
|
+
}
|
|
1006
1098
|
if (!msg || typeof msg !== 'object' || typeof msg.id !== 'string') return;
|
|
1007
1099
|
const slot = pending.get(msg.id);
|
|
1008
1100
|
if (!slot) return;
|
|
@@ -1015,6 +1107,19 @@ var NappletConformanceHostBundle = (() => {
|
|
|
1015
1107
|
}
|
|
1016
1108
|
slot.resolve(msg);
|
|
1017
1109
|
});
|
|
1110
|
+
if (napplet.gamepad) {
|
|
1111
|
+
napplet.gamepad = {
|
|
1112
|
+
get available() { gamepadSubscribe(); return gamepadState.available; },
|
|
1113
|
+
get focused() { gamepadSubscribe(); return gamepadState.focused; },
|
|
1114
|
+
getGamepads() { gamepadSubscribe(); return gamepadTools.copyGamepadState(gamepadState).pads; },
|
|
1115
|
+
onChange(handler) {
|
|
1116
|
+
const entry = { handler };
|
|
1117
|
+
gamepadHandlers.add(entry);
|
|
1118
|
+
gamepadSubscribe();
|
|
1119
|
+
return { close() { gamepadHandlers.delete(entry); } };
|
|
1120
|
+
},
|
|
1121
|
+
};
|
|
1122
|
+
}
|
|
1018
1123
|
if (napplet.resource) {
|
|
1019
1124
|
napplet.resource.info = () =>
|
|
1020
1125
|
request({ type: 'resource.info' }, 'resource.info.result', 'resource.info.error')
|
|
@@ -1048,19 +1153,14 @@ var NappletConformanceHostBundle = (() => {
|
|
|
1048
1153
|
};
|
|
1049
1154
|
}
|
|
1050
1155
|
window.napplet = napplet;
|
|
1051
|
-
|
|
1052
|
-
try {
|
|
1053
|
-
window.parent.postMessage({ type: '${INTERNAL_BOOT_ERROR}', reason: String(reason && reason.message || reason) }, '*');
|
|
1054
|
-
} catch {}
|
|
1055
|
-
};
|
|
1056
|
-
window.addEventListener('error', (event) => report(event.error || event.message));
|
|
1057
|
-
window.addEventListener('unhandledrejection', (event) => report(event.reason));
|
|
1156
|
+
${installed}
|
|
1058
1157
|
})();
|
|
1059
1158
|
`;
|
|
1060
1159
|
}
|
|
1061
|
-
function injectRuntime(html, url, domains) {
|
|
1160
|
+
function injectRuntime(html, url, domains, runtimePreludeSource) {
|
|
1062
1161
|
const base = `<base href="${escapeHtmlAttr(url)}">`;
|
|
1063
|
-
const
|
|
1162
|
+
const source = runtimePrelude(domains, runtimePreludeSource).replace(/<\/script/gi, "<\\/script");
|
|
1163
|
+
const script = `<script>${errorObserver()}<\/script><script>${source}<\/script>`;
|
|
1064
1164
|
const headOpen = /<head(?:\s[^>]*)?>/i.exec(html);
|
|
1065
1165
|
if (headOpen) {
|
|
1066
1166
|
const at = headOpen.index + headOpen[0].length;
|
|
@@ -1089,15 +1189,21 @@ var NappletConformanceHostBundle = (() => {
|
|
|
1089
1189
|
const shell = createReferenceShell();
|
|
1090
1190
|
const iframe = doc.createElement("iframe");
|
|
1091
1191
|
iframe.setAttribute("sandbox", opts.allowSameOrigin ? "allow-scripts allow-same-origin" : "allow-scripts");
|
|
1192
|
+
iframe.setAttribute("allow", "gamepad 'none'");
|
|
1092
1193
|
iframe.setAttribute("aria-hidden", "true");
|
|
1093
1194
|
iframe.style.position = "absolute";
|
|
1094
1195
|
iframe.style.width = "1px";
|
|
1095
1196
|
iframe.style.height = "1px";
|
|
1096
1197
|
iframe.style.left = "-9999px";
|
|
1097
1198
|
let bootError = null;
|
|
1199
|
+
let installedGlobal = false;
|
|
1098
1200
|
const listener = (event) => {
|
|
1099
1201
|
if (event.source !== iframe.contentWindow) return;
|
|
1100
1202
|
const data = event.data;
|
|
1203
|
+
if (data && typeof data === "object" && data.type === INTERNAL_INSTALLED) {
|
|
1204
|
+
installedGlobal = data.installed === true;
|
|
1205
|
+
return;
|
|
1206
|
+
}
|
|
1101
1207
|
if (data && typeof data === "object" && data.type === INTERNAL_BOOT_ERROR) {
|
|
1102
1208
|
bootError = `napplet boot error: ${String(data.reason ?? "unknown error")}`;
|
|
1103
1209
|
return;
|
|
@@ -1114,10 +1220,10 @@ var NappletConformanceHostBundle = (() => {
|
|
|
1114
1220
|
}, { once: true });
|
|
1115
1221
|
});
|
|
1116
1222
|
win.addEventListener("message", listener);
|
|
1117
|
-
(doc.body ?? doc.documentElement).appendChild(iframe);
|
|
1118
1223
|
try {
|
|
1119
|
-
const html = await loadHtml(opts.url);
|
|
1120
|
-
iframe.srcdoc = injectRuntime(html, opts.url, domains);
|
|
1224
|
+
const html = opts.html ?? await loadHtml(opts.url);
|
|
1225
|
+
iframe.srcdoc = injectRuntime(html, opts.url, domains, opts.runtimePreludeSource);
|
|
1226
|
+
(doc.body ?? doc.documentElement).appendChild(iframe);
|
|
1121
1227
|
} catch (error) {
|
|
1122
1228
|
bootError = error instanceof Error ? error.message : String(error);
|
|
1123
1229
|
}
|
|
@@ -1135,16 +1241,18 @@ var NappletConformanceHostBundle = (() => {
|
|
|
1135
1241
|
iframe.remove();
|
|
1136
1242
|
} catch {
|
|
1137
1243
|
}
|
|
1138
|
-
return { installedGlobal
|
|
1244
|
+
return { installedGlobal, bootError, emitted: [...shell.records] };
|
|
1139
1245
|
}
|
|
1140
1246
|
async function bootAndCollect(options) {
|
|
1141
1247
|
const doc = options.document ?? document;
|
|
1142
1248
|
const win = options.window ?? window;
|
|
1143
1249
|
const resolved = {
|
|
1144
1250
|
url: options.url,
|
|
1251
|
+
html: options.html,
|
|
1145
1252
|
readyTimeoutMs: options.readyTimeoutMs ?? 5e3,
|
|
1146
1253
|
settleMs: options.settleMs ?? 600,
|
|
1147
|
-
allowSameOrigin: options.allowSameOrigin ?? false
|
|
1254
|
+
allowSameOrigin: options.allowSameOrigin ?? false,
|
|
1255
|
+
runtimePreludeSource: options.runtimePreludeSource
|
|
1148
1256
|
};
|
|
1149
1257
|
const primary = await bootOnce(NAP_DOMAINS, resolved, doc, win);
|
|
1150
1258
|
let degraded = null;
|