@kubohiroya/turbowarp-webrtc-qrcode-pairing 0.2.3 → 0.2.4
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 +12 -1
- package/dist/webrtc-qrcode-pairing.js +191 -2
- package/docs/index.html +52 -1
- package/package.json +18 -10
- package/schemas/block-editor-fixed-inputs.schema.json +48 -0
- package/src/extension.ts +7 -1
- package/src/fixed-input-editors.ts +214 -0
- package/schemas/extension-manifest.schema.json +0 -49
- package/src/extension-manifest.ts +0 -149
package/README.md
CHANGED
|
@@ -116,7 +116,7 @@ pnpm run dev
|
|
|
116
116
|
|
|
117
117
|
Build output: `dist/webrtc-qrcode-pairing.js` and `dist/extension-manifest.json`. Load the JavaScript file as a custom TurboWarp extension. The manifest always records every declared block, including blocks the feature flag hides at runtime: it is the build-time contract, not the runtime state.
|
|
118
118
|
|
|
119
|
-
Package name and version: `@kubohiroya/turbowarp-webrtc-qrcode-pairing@0.2.
|
|
119
|
+
Package name and version: `@kubohiroya/turbowarp-webrtc-qrcode-pairing@0.2.4`.
|
|
120
120
|
|
|
121
121
|
## Block reference
|
|
122
122
|
|
|
@@ -449,3 +449,14 @@ Track implementation in this repository's GitHub Issues. Design documents live i
|
|
|
449
449
|
## License
|
|
450
450
|
|
|
451
451
|
SPDX-License-Identifier: MPL-2.0
|
|
452
|
+
|
|
453
|
+
## Fixed input editing
|
|
454
|
+
|
|
455
|
+
With a compatible TurboWarp GUI, enable fixed-input forms to edit only standard literal fields. Numbers use complete finite JSON number tokens; menu fields use existing choices. Connected reporters and unavailable fields stay read-only and are never evaluated. Saving an unchanged form preserves exact existing raw values, including legacy spellings and unknown choices. Invalid drafts stay in the same editing session; cancel, project/target replacement, or block removal discards them. Disabling the GUI feature flag restores native field editing. This form does not run blocks, open network/audio/key/QR sessions, resolve names, or edit Schema arguments. QR index is a positive integer without a session-derived candidate limit. Timeout remains 1–3600 seconds.
|
|
456
|
+
|
|
457
|
+
Canonical contracts: `schemas/block-editor-fixed-inputs.schema.json`; codecs and optional registration: `src/fixed-input-editors.ts`. These contracts describe new fixed-field candidates; they do not redefine all runtime data. The GUI integration is optional and existing standalone/composition APIs stay available.
|
|
458
|
+
|
|
459
|
+
- `showPairingQrPart`: INDEX — [block documentation](https://kubohiroya.github.io/turbowarp-webrtc-qrcode-pairing/#block-showPairingQrPart).
|
|
460
|
+
- `pairingQrPartSvg`: INDEX — [block documentation](https://kubohiroya.github.io/turbowarp-webrtc-qrcode-pairing/#block-pairingQrPartSvg).
|
|
461
|
+
- `pairingQrPartDataUri`: INDEX — [block documentation](https://kubohiroya.github.io/turbowarp-webrtc-qrcode-pairing/#block-pairingQrPartDataUri).
|
|
462
|
+
- `setPairingTimeout`: SECONDS — [block documentation](https://kubohiroya.github.io/turbowarp-webrtc-qrcode-pairing/#block-setPairingTimeout).
|
|
@@ -19,6 +19,192 @@
|
|
|
19
19
|
docsURI: "https://kubohiroya.github.io/turbowarp-webrtc-qrcode-pairing/",
|
|
20
20
|
blockIconURI: "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA0OCA0OCI+PHJlY3QgeD0iNCIgeT0iOCIgd2lkdGg9IjE4IiBoZWlnaHQ9IjE0IiByeD0iMyIgZmlsbD0iIzRDOTdGRiIvPjxyZWN0IHg9IjI2IiB5PSI4IiB3aWR0aD0iMTgiIGhlaWdodD0iMTQiIHJ4PSIzIiBmaWxsPSIjNTlDMDU5Ii8+PHJlY3QgeD0iMTUiIHk9IjI2IiB3aWR0aD0iMTgiIGhlaWdodD0iMTQiIHJ4PSIzIiBmaWxsPSIjRkZBQjE5Ii8+PC9zdmc+"
|
|
21
21
|
};
|
|
22
|
+
var block_editor_fixed_inputs_schema_default = {
|
|
23
|
+
$schema: "http://json-schema.org/draft-07/schema#",
|
|
24
|
+
$id: "https://kubohiroya.github.io/turbowarp-webrtc-qrcode-pairing/schemas/block-editor-fixed-inputs.schema.json",
|
|
25
|
+
title: "turbowarp-webrtc-qrcode-pairing fixed editing contracts",
|
|
26
|
+
$defs: {
|
|
27
|
+
"showPairingQrPart": {
|
|
28
|
+
"type": "object",
|
|
29
|
+
"properties": { "INDEX": {
|
|
30
|
+
"type": "integer",
|
|
31
|
+
"minimum": 1
|
|
32
|
+
} },
|
|
33
|
+
"additionalProperties": false
|
|
34
|
+
},
|
|
35
|
+
"pairingQrPartSvg": {
|
|
36
|
+
"type": "object",
|
|
37
|
+
"properties": { "INDEX": {
|
|
38
|
+
"type": "integer",
|
|
39
|
+
"minimum": 1
|
|
40
|
+
} },
|
|
41
|
+
"additionalProperties": false
|
|
42
|
+
},
|
|
43
|
+
"pairingQrPartDataUri": {
|
|
44
|
+
"type": "object",
|
|
45
|
+
"properties": { "INDEX": {
|
|
46
|
+
"type": "integer",
|
|
47
|
+
"minimum": 1
|
|
48
|
+
} },
|
|
49
|
+
"additionalProperties": false
|
|
50
|
+
},
|
|
51
|
+
"setPairingTimeout": {
|
|
52
|
+
"type": "object",
|
|
53
|
+
"properties": { "SECONDS": {
|
|
54
|
+
"type": "number",
|
|
55
|
+
"minimum": 1,
|
|
56
|
+
"maximum": 3600
|
|
57
|
+
} },
|
|
58
|
+
"additionalProperties": false
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
//#endregion
|
|
63
|
+
//#region src/fixed-input-editors.ts
|
|
64
|
+
var contracts = block_editor_fixed_inputs_schema_default.$defs;
|
|
65
|
+
var repository = "turbowarp-webrtc-qrcode-pairing";
|
|
66
|
+
var extensionId = "kubohiroyawebrtcqrcodepairing";
|
|
67
|
+
var token = /^-?(?:0|[1-9][0-9]*)(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?$/u;
|
|
68
|
+
function readNumber(value) {
|
|
69
|
+
if (typeof value !== "string" && typeof value !== "number") throw new TypeError("Invalid numeric projection.");
|
|
70
|
+
if (typeof value === "string" && value.trim() === "") throw new TypeError("Existing blank field is readonly.");
|
|
71
|
+
const number = Number(value);
|
|
72
|
+
if (!Number.isFinite(number)) throw new TypeError("Existing nonfinite field is readonly.");
|
|
73
|
+
return number;
|
|
74
|
+
}
|
|
75
|
+
function newNumberToken(value) {
|
|
76
|
+
if (typeof value === "number") {
|
|
77
|
+
if (!Number.isFinite(value)) throw new TypeError("Invalid new number.");
|
|
78
|
+
return Object.is(value, -0) ? "-0" : String(value);
|
|
79
|
+
}
|
|
80
|
+
if (typeof value !== "string" || !token.test(value.trim()) || !Number.isFinite(Number(value))) throw new TypeError("Invalid new number.");
|
|
81
|
+
return value.trim();
|
|
82
|
+
}
|
|
83
|
+
function existingChoice(_opcode, value) {
|
|
84
|
+
if (typeof value !== "string") throw new TypeError("Invalid string projection.");
|
|
85
|
+
return value;
|
|
86
|
+
}
|
|
87
|
+
function candidateValid(_opcode, _argument, schema, value) {
|
|
88
|
+
if (schema.type === "number" || schema.type === "integer") {
|
|
89
|
+
let number;
|
|
90
|
+
try {
|
|
91
|
+
number = readNumber(value);
|
|
92
|
+
} catch {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
if (schema.type === "integer" && !Number.isInteger(number)) return false;
|
|
96
|
+
if (schema.minimum !== void 0 && number < schema.minimum) return false;
|
|
97
|
+
if (schema.maximum !== void 0 && number > schema.maximum) return false;
|
|
98
|
+
if (schema.exclusiveMinimum !== void 0 && number <= schema.exclusiveMinimum) return false;
|
|
99
|
+
if (schema.enum && !schema.enum.includes(number)) return false;
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
return typeof value === "string" && schema.enum.includes(value);
|
|
103
|
+
}
|
|
104
|
+
function fixedInputEditorOptions() {
|
|
105
|
+
return Object.entries(contracts).map(([opcode, contract]) => ({
|
|
106
|
+
apiVersion: 1,
|
|
107
|
+
extensionId,
|
|
108
|
+
editorId: `${extensionId}.fixed.${opcode}`,
|
|
109
|
+
opcodes: [`${extensionId}_${opcode}`],
|
|
110
|
+
contractId: `${repository}.${opcode}.fixed`,
|
|
111
|
+
contractRevision: "fixed-inputs-v1",
|
|
112
|
+
validationContext: () => ({
|
|
113
|
+
consumerId: repository,
|
|
114
|
+
nowMs: 0,
|
|
115
|
+
settings: null
|
|
116
|
+
}),
|
|
117
|
+
bindings: Object.entries(contract.properties).map(([argument, schema]) => {
|
|
118
|
+
const numeric = schema.type === "number" || schema.type === "integer";
|
|
119
|
+
const source = {
|
|
120
|
+
repository,
|
|
121
|
+
path: "schemas/block-editor-fixed-inputs.schema.json",
|
|
122
|
+
symbol: `/$defs/${opcode}/properties/${argument}`
|
|
123
|
+
};
|
|
124
|
+
return {
|
|
125
|
+
argument,
|
|
126
|
+
schemaBundle: {
|
|
127
|
+
identity: {
|
|
128
|
+
ownerId: repository,
|
|
129
|
+
contractId: `${opcode}.${argument}`,
|
|
130
|
+
revision: "fixed-inputs-v1",
|
|
131
|
+
variantId: "fixed"
|
|
132
|
+
},
|
|
133
|
+
dialect: "draft-07",
|
|
134
|
+
root: {
|
|
135
|
+
retrievalUri: `${block_editor_fixed_inputs_schema_default.$id}?binding=${opcode}.${argument}`,
|
|
136
|
+
document: schema,
|
|
137
|
+
artifact: {
|
|
138
|
+
role: "canonical-schema",
|
|
139
|
+
source
|
|
140
|
+
}
|
|
141
|
+
},
|
|
142
|
+
resources: [],
|
|
143
|
+
provenance: {
|
|
144
|
+
canonicalSources: [
|
|
145
|
+
source,
|
|
146
|
+
{
|
|
147
|
+
repository,
|
|
148
|
+
path: "src/block-definitions.json"
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
repository,
|
|
152
|
+
path: "src/extension.ts",
|
|
153
|
+
symbol: opcode
|
|
154
|
+
}
|
|
155
|
+
],
|
|
156
|
+
generatedFiles: [],
|
|
157
|
+
referenceSchemas: [],
|
|
158
|
+
generationSources: []
|
|
159
|
+
}
|
|
160
|
+
},
|
|
161
|
+
control: numeric ? { kind: "number" } : {
|
|
162
|
+
kind: "enum",
|
|
163
|
+
choices: schema.enum.map((value) => ({
|
|
164
|
+
value,
|
|
165
|
+
label: String(value)
|
|
166
|
+
}))
|
|
167
|
+
},
|
|
168
|
+
read: (value) => numeric ? readNumber(value) : existingChoice(opcode, value),
|
|
169
|
+
encode: (value) => {
|
|
170
|
+
if (numeric) return newNumberToken(value);
|
|
171
|
+
if (typeof value !== "string" || !schema.enum.includes(value)) throw new TypeError("Invalid new enum value.");
|
|
172
|
+
return value;
|
|
173
|
+
},
|
|
174
|
+
validateCandidate: (value) => candidateValid(opcode, argument, schema, value) ? [] : [{
|
|
175
|
+
path: argument,
|
|
176
|
+
message: "Check this value before saving."
|
|
177
|
+
}]
|
|
178
|
+
};
|
|
179
|
+
})
|
|
180
|
+
}));
|
|
181
|
+
}
|
|
182
|
+
/** Register only visible static contracts; ordinary composition consumers load no editor runtime. */
|
|
183
|
+
function registerFixedInputEditors(scratch, runtime, visibleBlocks) {
|
|
184
|
+
const api = scratch.gui?.blockEditors;
|
|
185
|
+
if (api?.apiVersion !== 1 || typeof api.registerFixedInputEditor !== "function") return void 0;
|
|
186
|
+
const visibleOpcodes = new Set(visibleBlocks.flatMap((block) => {
|
|
187
|
+
if (typeof block !== "object" || block === null || !("opcode" in block)) return [];
|
|
188
|
+
return typeof block.opcode === "string" ? [block.opcode] : [];
|
|
189
|
+
}));
|
|
190
|
+
const events = runtime;
|
|
191
|
+
const removers = [];
|
|
192
|
+
let active = true;
|
|
193
|
+
const dispose = () => {
|
|
194
|
+
if (!active) return;
|
|
195
|
+
active = false;
|
|
196
|
+
events?.off?.("RUNTIME_DISPOSED", dispose);
|
|
197
|
+
for (const remove of removers.splice(0).reverse()) remove();
|
|
198
|
+
};
|
|
199
|
+
try {
|
|
200
|
+
for (const options of fixedInputEditorOptions()) if (options.opcodes.some((opcode) => visibleOpcodes.has(opcode.slice(30)))) removers.push(api.registerFixedInputEditor.call(api, options));
|
|
201
|
+
events?.on?.("RUNTIME_DISPOSED", dispose);
|
|
202
|
+
} catch (error) {
|
|
203
|
+
dispose();
|
|
204
|
+
throw error;
|
|
205
|
+
}
|
|
206
|
+
return dispose;
|
|
207
|
+
}
|
|
22
208
|
var block_definitions_default = {
|
|
23
209
|
extensionName: "TurboWarp-WebRTC-QRCode-Pairing",
|
|
24
210
|
blocks: [
|
|
@@ -509,7 +695,7 @@
|
|
|
509
695
|
_checkPrivateRedeclaration(e, a), a.add(e);
|
|
510
696
|
}
|
|
511
697
|
//#endregion
|
|
512
|
-
//#region node_modules/.pnpm/@kubohiroya+qrcode-structured-append@0.1.
|
|
698
|
+
//#region node_modules/.pnpm/@kubohiroya+qrcode-structured-append@0.1.1/node_modules/@kubohiroya/qrcode-structured-append/dist/index.js
|
|
513
699
|
var _Charset;
|
|
514
700
|
var _label;
|
|
515
701
|
var _values;
|
|
@@ -4129,13 +4315,15 @@
|
|
|
4129
4315
|
this.runtime.on?.("targetWasRemoved", this.targetRemovedListener);
|
|
4130
4316
|
}
|
|
4131
4317
|
getInfo() {
|
|
4132
|
-
|
|
4318
|
+
const info = {
|
|
4133
4319
|
id: extensionConfig.id,
|
|
4134
4320
|
name: Scratch.translate(block_definitions_default.extensionName),
|
|
4135
4321
|
docsURI: extensionConfig.docsURI,
|
|
4136
4322
|
blockIconURI: extensionConfig.blockIconURI,
|
|
4137
4323
|
blocks: this.enabled ? blockDefinitions.map((block) => this.toScratchBlock(block)) : []
|
|
4138
4324
|
};
|
|
4325
|
+
this.fixedEditorsDispose ?? (this.fixedEditorsDispose = registerFixedInputEditors(Scratch, this.runtime, info.blocks));
|
|
4326
|
+
return info;
|
|
4139
4327
|
}
|
|
4140
4328
|
async startOfferPairing(args) {
|
|
4141
4329
|
await this.pairing.startOfferPairing({
|
|
@@ -4239,6 +4427,7 @@
|
|
|
4239
4427
|
this.pairing.setTimeoutSeconds(Scratch.Cast.toString(args.SESSION), Scratch.Cast.toNumber(args.SECONDS));
|
|
4240
4428
|
}
|
|
4241
4429
|
dispose() {
|
|
4430
|
+
this.fixedEditorsDispose?.();
|
|
4242
4431
|
this.pairing.dispose();
|
|
4243
4432
|
this.runtime.off?.("PROJECT_RUN_STOP", this.stopListener);
|
|
4244
4433
|
this.runtime.off?.("PROJECT_STOP_ALL", this.stopListener);
|
package/docs/index.html
CHANGED
|
@@ -4,8 +4,17 @@
|
|
|
4
4
|
<meta charset="utf-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
6
6
|
<title>TurboWarp-WebRTC-QRCode-Pairing</title>
|
|
7
|
+
<style id="block-reference-style">
|
|
8
|
+
#extension-block-reference{max-width:72rem;margin:2rem auto;padding:1rem;line-height:1.6;overflow-wrap:anywhere}
|
|
9
|
+
#extension-block-reference .block-reference-entry{display:block;scroll-margin-top:6rem;margin:2rem 0;padding:1rem;border:1px solid currentColor;border-radius:.5rem}
|
|
10
|
+
#extension-block-reference .block-reference-table{overflow-x:auto}
|
|
11
|
+
#extension-block-reference table{border-collapse:collapse;width:100%}
|
|
12
|
+
#extension-block-reference th,#extension-block-reference td{text-align:left;vertical-align:top;border:1px solid currentColor;padding:.5rem}
|
|
13
|
+
#extension-block-reference code{white-space:pre-wrap}
|
|
14
|
+
#extension-block-reference a:focus-visible{outline:3px solid currentColor;outline-offset:3px}
|
|
15
|
+
</style>
|
|
7
16
|
</head>
|
|
8
|
-
<body>
|
|
17
|
+
<body><p lang="en" class="complete-block-reference-link"><a href="#extension-block-reference">Complete block reference (31 blocks)</a></p>
|
|
9
18
|
<h1>TurboWarp-WebRTC-QRCode-Pairing</h1>
|
|
10
19
|
<p>A TurboWarp extension that pairs WebRTC peers by carrying the offer and the answer optically, as QR codes.</p>
|
|
11
20
|
<h2>How the workflow runs</h2>
|
|
@@ -30,5 +39,47 @@
|
|
|
30
39
|
<li><a href="architecture.md">Architecture</a> (<a href="architecture.ja.md">日本語</a>) — runtime structure, build outputs, extension API manifest</li>
|
|
31
40
|
<li><a href="https://github.com/kubohiroya/turbowarp-webrtc-qrcode-pairing#readme">Repository documentation and block reference</a></li>
|
|
32
41
|
</ul>
|
|
42
|
+
<!-- BEGIN COMPLETE BLOCK REFERENCE -->
|
|
43
|
+
<section lang="en" id="extension-block-reference" aria-labelledby="block-reference-title">
|
|
44
|
+
<h2 id="block-reference-title">TurboWarp-WebRTC-QRCode-Pairing: complete block reference</h2>
|
|
45
|
+
<p>All 31 blocks are documented below, including compatibility blocks and blocks available through optional features. Select a block to read its behavior, arguments, default values, and menu choices. Each block has a permanent link.</p>
|
|
46
|
+
<nav aria-label="Block reference"><ul><li><a href="#block-startOfferPairing"><code>start offer pairing [SESSION] as [LOCAL_PEER] to [REMOTE_PEER]</code></a></li>
|
|
47
|
+
<li><a href="#block-startAnswerPairing"><code>start answer pairing [SESSION] as [LOCAL_PEER]</code></a></li>
|
|
48
|
+
<li><a href="#block-ingestPairingQrText"><code>receive pairing message [TEXT] for [SESSION]</code></a></li>
|
|
49
|
+
<li><a href="#block-scanPairingQrFromCamera"><code>scan pairing QR for [SESSION] from camera [CAMERA_ID]</code></a></li>
|
|
50
|
+
<li><a href="#block-pairingReceivedParts"><code>received parts of [SESSION]</code></a></li>
|
|
51
|
+
<li><a href="#block-pairingRequiredParts"><code>required parts of [SESSION]</code></a></li>
|
|
52
|
+
<li><a href="#block-pairingMissingParts"><code>missing parts of [SESSION]</code></a></li>
|
|
53
|
+
<li><a href="#block-pairingReadCount"><code>pairing QR reads of [SESSION]</code></a></li>
|
|
54
|
+
<li><a href="#block-pairingLastRead"><code>last pairing QR read of [SESSION]</code></a></li>
|
|
55
|
+
<li><a href="#block-pairingLastReadDetail"><code>last pairing QR read detail of [SESSION]</code></a></li>
|
|
56
|
+
<li><a href="#block-showPairingQrPart"><code>show pairing QR part [INDEX] of [SESSION] on this sprite</code></a></li>
|
|
57
|
+
<li><a href="#block-showNextPairingQrPart"><code>show next pairing QR part of [SESSION] on this sprite</code></a></li>
|
|
58
|
+
<li><a href="#block-pairingQrPartCount"><code>pairing QR part count of [SESSION]</code></a></li>
|
|
59
|
+
<li><a href="#block-pairingQrCurrentPart"><code>current pairing QR part of [SESSION]</code></a></li>
|
|
60
|
+
<li><a href="#block-pairingQrPartSvg"><code>pairing QR part [INDEX] of [SESSION] as SVG</code></a></li>
|
|
61
|
+
<li><a href="#block-pairingQrPartDataUri"><code>pairing QR part [INDEX] of [SESSION] as data URI</code></a></li>
|
|
62
|
+
<li><a href="#block-endPairingQrDisplay"><code>end pairing QR display of [SESSION]</code></a></li>
|
|
63
|
+
<li><a href="#block-pairingPhase"><code>pairing phase of [SESSION]</code></a></li>
|
|
64
|
+
<li><a href="#block-pairingConnectionState"><code>pairing connection state of [SESSION]</code></a></li>
|
|
65
|
+
<li><a href="#block-isPairingConnected"><code>pairing [SESSION] connected?</code></a></li>
|
|
66
|
+
<li><a href="#block-waitUntilPairingConnected"><code>wait until pairing [SESSION] is connected</code></a></li>
|
|
67
|
+
<li><a href="#block-pairingError"><code>pairing error code of [SESSION]</code></a></li>
|
|
68
|
+
<li><a href="#block-pairingErrorMessage"><code>pairing error message of [SESSION]</code></a></li>
|
|
69
|
+
<li><a href="#block-pairingLocalPeer"><code>local peer of [SESSION]</code></a></li>
|
|
70
|
+
<li><a href="#block-pairingRemotePeer"><code>remote peer of [SESSION]</code></a></li>
|
|
71
|
+
<li><a href="#block-pairingExchangeId"><code>exchange id of [SESSION]</code></a></li>
|
|
72
|
+
<li><a href="#block-pairingRemainingSeconds"><code>remaining seconds of [SESSION]</code></a></li>
|
|
73
|
+
<li><a href="#block-pairingSessions"><code>pairing sessions</code></a></li>
|
|
74
|
+
<li><a href="#block-cancelPairing"><code>cancel pairing [SESSION]</code></a></li>
|
|
75
|
+
<li><a href="#block-retryPairing"><code>retry pairing [SESSION]</code></a></li>
|
|
76
|
+
<li><a href="#block-setPairingTimeout"><code>set pairing timeout of [SESSION] to [SECONDS] seconds</code></a></li>
|
|
77
|
+
</ul></nav>
|
|
78
|
+
<article id="block-startOfferPairing" class="block-reference-entry"><h3><code>start offer pairing [SESSION] as [LOCAL_PEER] to [REMOTE_PEER]</code></h3><p>Creates a WebRTC offer for the integration machine and prepares it as a Structured Append sequence of QR codes.</p><p>Block type: COMMAND. Opcode: <code>startOfferPairing</code>. <a href="#block-startOfferPairing">Permanent link</a></p><div class="block-reference-table"><table><thead><tr><th>Argument</th><th>Type</th><th>Default value</th><th>Menu choices</th></tr></thead><tbody><tr><th scope="row"><code>SESSION</code></th><td>STRING</td><td><code>pairing-1</code></td><td></td></tr><tr><th scope="row"><code>LOCAL_PEER</code></th><td>STRING</td><td><code>hub</code></td><td></td></tr><tr><th scope="row"><code>REMOTE_PEER</code></th><td>STRING</td><td><code>camera-1</code></td><td></td></tr></tbody></table></div></article><article id="block-startAnswerPairing" class="block-reference-entry"><h3><code>start answer pairing [SESSION] as [LOCAL_PEER]</code></h3><p>Waits for an offer on the camera machine. Leave the name empty to adopt the name the offer assigns.</p><p>Block type: COMMAND. Opcode: <code>startAnswerPairing</code>. <a href="#block-startAnswerPairing">Permanent link</a></p><div class="block-reference-table"><table><thead><tr><th>Argument</th><th>Type</th><th>Default value</th><th>Menu choices</th></tr></thead><tbody><tr><th scope="row"><code>SESSION</code></th><td>STRING</td><td><code>pairing-1</code></td><td></td></tr><tr><th scope="row"><code>LOCAL_PEER</code></th><td>STRING</td><td><code></code></td><td></td></tr></tbody></table></div></article><article id="block-ingestPairingQrText" class="block-reference-entry"><h3><code>receive pairing message [TEXT] for [SESSION]</code></h3><p>Accepts a whole pairing message as text, such as one pasted or carried some other way. Camera scanning reads the QR codes of a sequence itself.</p><p>Block type: COMMAND. Opcode: <code>ingestPairingQrText</code>. <a href="#block-ingestPairingQrText">Permanent link</a></p><div class="block-reference-table"><table><thead><tr><th>Argument</th><th>Type</th><th>Default value</th><th>Menu choices</th></tr></thead><tbody><tr><th scope="row"><code>TEXT</code></th><td>STRING</td><td><code></code></td><td></td></tr><tr><th scope="row"><code>SESSION</code></th><td>STRING</td><td><code>pairing-1</code></td><td></td></tr></tbody></table></div></article><article id="block-scanPairingQrFromCamera" class="block-reference-entry"><h3><code>scan pairing QR for [SESSION] from camera [CAMERA_ID]</code></h3><p>Reads QR codes from the named camera, in any order, until the exchange has every code of the sequence. Codes of other sequences are reported and ignored.</p><p>Block type: COMMAND. Opcode: <code>scanPairingQrFromCamera</code>. <a href="#block-scanPairingQrFromCamera">Permanent link</a></p><div class="block-reference-table"><table><thead><tr><th>Argument</th><th>Type</th><th>Default value</th><th>Menu choices</th></tr></thead><tbody><tr><th scope="row"><code>SESSION</code></th><td>STRING</td><td><code>pairing-1</code></td><td></td></tr><tr><th scope="row"><code>CAMERA_ID</code></th><td>STRING</td><td><code>default</code></td><td></td></tr></tbody></table></div></article><article id="block-pairingReceivedParts" class="block-reference-entry"><h3><code>received parts of [SESSION]</code></h3><p>Returns how many distinct codes of the incoming sequence have been read.</p><p>Block type: REPORTER. Opcode: <code>pairingReceivedParts</code>. <a href="#block-pairingReceivedParts">Permanent link</a></p><div class="block-reference-table"><table><thead><tr><th>Argument</th><th>Type</th><th>Default value</th><th>Menu choices</th></tr></thead><tbody><tr><th scope="row"><code>SESSION</code></th><td>STRING</td><td><code>pairing-1</code></td><td></td></tr></tbody></table></div></article><article id="block-pairingRequiredParts" class="block-reference-entry"><h3><code>required parts of [SESSION]</code></h3><p>Returns how many codes the incoming sequence has, or zero before the first code arrives.</p><p>Block type: REPORTER. Opcode: <code>pairingRequiredParts</code>. <a href="#block-pairingRequiredParts">Permanent link</a></p><div class="block-reference-table"><table><thead><tr><th>Argument</th><th>Type</th><th>Default value</th><th>Menu choices</th></tr></thead><tbody><tr><th scope="row"><code>SESSION</code></th><td>STRING</td><td><code>pairing-1</code></td><td></td></tr></tbody></table></div></article><article id="block-pairingMissingParts" class="block-reference-entry"><h3><code>missing parts of [SESSION]</code></h3><p>Returns the one-based code numbers that have not been read yet, separated by commas.</p><p>Block type: REPORTER. Opcode: <code>pairingMissingParts</code>. <a href="#block-pairingMissingParts">Permanent link</a></p><div class="block-reference-table"><table><thead><tr><th>Argument</th><th>Type</th><th>Default value</th><th>Menu choices</th></tr></thead><tbody><tr><th scope="row"><code>SESSION</code></th><td>STRING</td><td><code>pairing-1</code></td><td></td></tr></tbody></table></div></article><article id="block-pairingReadCount" class="block-reference-entry"><h3><code>pairing QR reads of [SESSION]</code></h3><p>Returns how many pairing QR codes this session has read, of any result. It rises by one per read, so a change means there is a new result to show.</p><p>Block type: REPORTER. Opcode: <code>pairingReadCount</code>. <a href="#block-pairingReadCount">Permanent link</a></p><div class="block-reference-table"><table><thead><tr><th>Argument</th><th>Type</th><th>Default value</th><th>Menu choices</th></tr></thead><tbody><tr><th scope="row"><code>SESSION</code></th><td>STRING</td><td><code>pairing-1</code></td><td></td></tr></tbody></table></div></article><article id="block-pairingLastRead" class="block-reference-entry"><h3><code>last pairing QR read of [SESSION]</code></h3><p>Returns what the latest pairing QR code was: accepted (a new code of the sequence), duplicate (a code already read), foreign (a code this exchange cannot use, which is ignored), or an empty string before the first read. QR codes that are not pairing codes are not reported.</p><p>Block type: REPORTER. Opcode: <code>pairingLastRead</code>. <a href="#block-pairingLastRead">Permanent link</a></p><div class="block-reference-table"><table><thead><tr><th>Argument</th><th>Type</th><th>Default value</th><th>Menu choices</th></tr></thead><tbody><tr><th scope="row"><code>SESSION</code></th><td>STRING</td><td><code>pairing-1</code></td><td></td></tr></tbody></table></div></article><article id="block-pairingLastReadDetail" class="block-reference-entry"><h3><code>last pairing QR read detail of [SESSION]</code></h3><p>Returns the code as "2 / 4" for accepted and duplicate reads, or why a foreign code was ignored as an error code such as message-mismatch (another sequence), stale-exchange or peer-mismatch.</p><p>Block type: REPORTER. Opcode: <code>pairingLastReadDetail</code>. <a href="#block-pairingLastReadDetail">Permanent link</a></p><div class="block-reference-table"><table><thead><tr><th>Argument</th><th>Type</th><th>Default value</th><th>Menu choices</th></tr></thead><tbody><tr><th scope="row"><code>SESSION</code></th><td>STRING</td><td><code>pairing-1</code></td><td></td></tr></tbody></table></div></article><article id="block-showPairingQrPart" class="block-reference-entry"><p class="fixed-input-help-link">Fixed literal fields: <code>INDEX</code>. <a href="#fixed-input-editing">Editing UI and preservation rules</a>.</p><h3><code>show pairing QR part [INDEX] of [SESSION] on this sprite</code></h3><p>Shows the selected one-based code of the sequence using a temporary sprite skin.</p><p>Block type: COMMAND. Opcode: <code>showPairingQrPart</code>. <a href="#block-showPairingQrPart">Permanent link</a></p><div class="block-reference-table"><table><thead><tr><th>Argument</th><th>Type</th><th>Default value</th><th>Menu choices</th></tr></thead><tbody><tr><th scope="row"><code>INDEX</code></th><td>NUMBER</td><td><code>1</code></td><td></td></tr><tr><th scope="row"><code>SESSION</code></th><td>STRING</td><td><code>pairing-1</code></td><td></td></tr></tbody></table></div></article><article id="block-showNextPairingQrPart" class="block-reference-entry"><h3><code>show next pairing QR part of [SESSION] on this sprite</code></h3><p>Shows the next code and wraps from the last back to the first, so a loop with a short wait cycles the whole sequence.</p><p>Block type: COMMAND. Opcode: <code>showNextPairingQrPart</code>. <a href="#block-showNextPairingQrPart">Permanent link</a></p><div class="block-reference-table"><table><thead><tr><th>Argument</th><th>Type</th><th>Default value</th><th>Menu choices</th></tr></thead><tbody><tr><th scope="row"><code>SESSION</code></th><td>STRING</td><td><code>pairing-1</code></td><td></td></tr></tbody></table></div></article><article id="block-pairingQrPartCount" class="block-reference-entry"><h3><code>pairing QR part count of [SESSION]</code></h3><p>Returns how many codes are prepared for display, or zero when none are.</p><p>Block type: REPORTER. Opcode: <code>pairingQrPartCount</code>. <a href="#block-pairingQrPartCount">Permanent link</a></p><div class="block-reference-table"><table><thead><tr><th>Argument</th><th>Type</th><th>Default value</th><th>Menu choices</th></tr></thead><tbody><tr><th scope="row"><code>SESSION</code></th><td>STRING</td><td><code>pairing-1</code></td><td></td></tr></tbody></table></div></article><article id="block-pairingQrCurrentPart" class="block-reference-entry"><h3><code>current pairing QR part of [SESSION]</code></h3><p>Returns the one-based code selected for display, or zero when none is selected.</p><p>Block type: REPORTER. Opcode: <code>pairingQrCurrentPart</code>. <a href="#block-pairingQrCurrentPart">Permanent link</a></p><div class="block-reference-table"><table><thead><tr><th>Argument</th><th>Type</th><th>Default value</th><th>Menu choices</th></tr></thead><tbody><tr><th scope="row"><code>SESSION</code></th><td>STRING</td><td><code>pairing-1</code></td><td></td></tr></tbody></table></div></article><article id="block-pairingQrPartSvg" class="block-reference-entry"><p class="fixed-input-help-link">Fixed literal fields: <code>INDEX</code>. <a href="#fixed-input-editing">Editing UI and preservation rules</a>.</p><h3><code>pairing QR part [INDEX] of [SESSION] as SVG</code></h3><p>Returns the code as SVG markup so a project can display it its own way.</p><p>Block type: REPORTER. Opcode: <code>pairingQrPartSvg</code>. <a href="#block-pairingQrPartSvg">Permanent link</a></p><div class="block-reference-table"><table><thead><tr><th>Argument</th><th>Type</th><th>Default value</th><th>Menu choices</th></tr></thead><tbody><tr><th scope="row"><code>INDEX</code></th><td>NUMBER</td><td><code>1</code></td><td></td></tr><tr><th scope="row"><code>SESSION</code></th><td>STRING</td><td><code>pairing-1</code></td><td></td></tr></tbody></table></div></article><article id="block-pairingQrPartDataUri" class="block-reference-entry"><p class="fixed-input-help-link">Fixed literal fields: <code>INDEX</code>. <a href="#fixed-input-editing">Editing UI and preservation rules</a>.</p><h3><code>pairing QR part [INDEX] of [SESSION] as data URI</code></h3><p>Returns the code as a base64 SVG data URI for costumes and HTML images.</p><p>Block type: REPORTER. Opcode: <code>pairingQrPartDataUri</code>. <a href="#block-pairingQrPartDataUri">Permanent link</a></p><div class="block-reference-table"><table><thead><tr><th>Argument</th><th>Type</th><th>Default value</th><th>Menu choices</th></tr></thead><tbody><tr><th scope="row"><code>INDEX</code></th><td>NUMBER</td><td><code>1</code></td><td></td></tr><tr><th scope="row"><code>SESSION</code></th><td>STRING</td><td><code>pairing-1</code></td><td></td></tr></tbody></table></div></article><article id="block-endPairingQrDisplay" class="block-reference-entry"><h3><code>end pairing QR display of [SESSION]</code></h3><p>Restores the sprites this session changed. The session itself stays open.</p><p>Block type: COMMAND. Opcode: <code>endPairingQrDisplay</code>. <a href="#block-endPairingQrDisplay">Permanent link</a></p><div class="block-reference-table"><table><thead><tr><th>Argument</th><th>Type</th><th>Default value</th><th>Menu choices</th></tr></thead><tbody><tr><th scope="row"><code>SESSION</code></th><td>STRING</td><td><code>pairing-1</code></td><td></td></tr></tbody></table></div></article><article id="block-pairingPhase" class="block-reference-entry"><h3><code>pairing phase of [SESSION]</code></h3><p>Returns the session phase, such as offer-ready, receiving, answer-ready, connecting, or connected.</p><p>Block type: REPORTER. Opcode: <code>pairingPhase</code>. <a href="#block-pairingPhase">Permanent link</a></p><div class="block-reference-table"><table><thead><tr><th>Argument</th><th>Type</th><th>Default value</th><th>Menu choices</th></tr></thead><tbody><tr><th scope="row"><code>SESSION</code></th><td>STRING</td><td><code>pairing-1</code></td><td></td></tr></tbody></table></div></article><article id="block-pairingConnectionState" class="block-reference-entry"><h3><code>pairing connection state of [SESSION]</code></h3><p>Returns the WebRTC connection state reported for this session's peer.</p><p>Block type: REPORTER. Opcode: <code>pairingConnectionState</code>. <a href="#block-pairingConnectionState">Permanent link</a></p><div class="block-reference-table"><table><thead><tr><th>Argument</th><th>Type</th><th>Default value</th><th>Menu choices</th></tr></thead><tbody><tr><th scope="row"><code>SESSION</code></th><td>STRING</td><td><code>pairing-1</code></td><td></td></tr></tbody></table></div></article><article id="block-isPairingConnected" class="block-reference-entry"><h3><code>pairing [SESSION] connected?</code></h3><p>Reports whether WebRTC has established the connection for this session.</p><p>Block type: BOOLEAN. Opcode: <code>isPairingConnected</code>. <a href="#block-isPairingConnected">Permanent link</a></p><div class="block-reference-table"><table><thead><tr><th>Argument</th><th>Type</th><th>Default value</th><th>Menu choices</th></tr></thead><tbody><tr><th scope="row"><code>SESSION</code></th><td>STRING</td><td><code>pairing-1</code></td><td></td></tr></tbody></table></div></article><article id="block-waitUntilPairingConnected" class="block-reference-entry"><h3><code>wait until pairing [SESSION] is connected</code></h3><p>Waits for the connection, or fails when the session is cancelled, times out, or fails.</p><p>Block type: COMMAND. Opcode: <code>waitUntilPairingConnected</code>. <a href="#block-waitUntilPairingConnected">Permanent link</a></p><div class="block-reference-table"><table><thead><tr><th>Argument</th><th>Type</th><th>Default value</th><th>Menu choices</th></tr></thead><tbody><tr><th scope="row"><code>SESSION</code></th><td>STRING</td><td><code>pairing-1</code></td><td></td></tr></tbody></table></div></article><article id="block-pairingError" class="block-reference-entry"><h3><code>pairing error code of [SESSION]</code></h3><p>Returns the latest error code, such as hash-mismatch or peer-mismatch, or an empty string.</p><p>Block type: REPORTER. Opcode: <code>pairingError</code>. <a href="#block-pairingError">Permanent link</a></p><div class="block-reference-table"><table><thead><tr><th>Argument</th><th>Type</th><th>Default value</th><th>Menu choices</th></tr></thead><tbody><tr><th scope="row"><code>SESSION</code></th><td>STRING</td><td><code>pairing-1</code></td><td></td></tr></tbody></table></div></article><article id="block-pairingErrorMessage" class="block-reference-entry"><h3><code>pairing error message of [SESSION]</code></h3><p>Returns the latest error message. Pairing codes and QR payloads are never included.</p><p>Block type: REPORTER. Opcode: <code>pairingErrorMessage</code>. <a href="#block-pairingErrorMessage">Permanent link</a></p><div class="block-reference-table"><table><thead><tr><th>Argument</th><th>Type</th><th>Default value</th><th>Menu choices</th></tr></thead><tbody><tr><th scope="row"><code>SESSION</code></th><td>STRING</td><td><code>pairing-1</code></td><td></td></tr></tbody></table></div></article><article id="block-pairingLocalPeer" class="block-reference-entry"><h3><code>local peer of [SESSION]</code></h3><p>Returns the name this machine uses for itself in this exchange.</p><p>Block type: REPORTER. Opcode: <code>pairingLocalPeer</code>. <a href="#block-pairingLocalPeer">Permanent link</a></p><div class="block-reference-table"><table><thead><tr><th>Argument</th><th>Type</th><th>Default value</th><th>Menu choices</th></tr></thead><tbody><tr><th scope="row"><code>SESSION</code></th><td>STRING</td><td><code>pairing-1</code></td><td></td></tr></tbody></table></div></article><article id="block-pairingRemotePeer" class="block-reference-entry"><h3><code>remote peer of [SESSION]</code></h3><p>Returns the name this machine uses for the other machine, which is also its WebRTC peer name.</p><p>Block type: REPORTER. Opcode: <code>pairingRemotePeer</code>. <a href="#block-pairingRemotePeer">Permanent link</a></p><div class="block-reference-table"><table><thead><tr><th>Argument</th><th>Type</th><th>Default value</th><th>Menu choices</th></tr></thead><tbody><tr><th scope="row"><code>SESSION</code></th><td>STRING</td><td><code>pairing-1</code></td><td></td></tr></tbody></table></div></article><article id="block-pairingExchangeId" class="block-reference-entry"><h3><code>exchange id of [SESSION]</code></h3><p>Returns the identifier of the current offer and answer exchange. A retry allocates a new one.</p><p>Block type: REPORTER. Opcode: <code>pairingExchangeId</code>. <a href="#block-pairingExchangeId">Permanent link</a></p><div class="block-reference-table"><table><thead><tr><th>Argument</th><th>Type</th><th>Default value</th><th>Menu choices</th></tr></thead><tbody><tr><th scope="row"><code>SESSION</code></th><td>STRING</td><td><code>pairing-1</code></td><td></td></tr></tbody></table></div></article><article id="block-pairingRemainingSeconds" class="block-reference-entry"><h3><code>remaining seconds of [SESSION]</code></h3><p>Returns the seconds left before the exchange times out.</p><p>Block type: REPORTER. Opcode: <code>pairingRemainingSeconds</code>. <a href="#block-pairingRemainingSeconds">Permanent link</a></p><div class="block-reference-table"><table><thead><tr><th>Argument</th><th>Type</th><th>Default value</th><th>Menu choices</th></tr></thead><tbody><tr><th scope="row"><code>SESSION</code></th><td>STRING</td><td><code>pairing-1</code></td><td></td></tr></tbody></table></div></article><article id="block-pairingSessions" class="block-reference-entry"><h3><code>pairing sessions</code></h3><p>Returns the open session names, separated by commas.</p><p>Block type: REPORTER. Opcode: <code>pairingSessions</code>. <a href="#block-pairingSessions">Permanent link</a></p><p>This block has no arguments.</p></article><article id="block-cancelPairing" class="block-reference-entry"><h3><code>cancel pairing [SESSION]</code></h3><p>Cancels the exchange and releases displays, buffers, and unconnected peers. Established connections stay up.</p><p>Block type: COMMAND. Opcode: <code>cancelPairing</code>. <a href="#block-cancelPairing">Permanent link</a></p><div class="block-reference-table"><table><thead><tr><th>Argument</th><th>Type</th><th>Default value</th><th>Menu choices</th></tr></thead><tbody><tr><th scope="row"><code>SESSION</code></th><td>STRING</td><td><code>pairing-1</code></td><td></td></tr></tbody></table></div></article><article id="block-retryPairing" class="block-reference-entry"><h3><code>retry pairing [SESSION]</code></h3><p>Starts a new exchange for the same session. QR codes from the previous exchange stop being accepted.</p><p>Block type: COMMAND. Opcode: <code>retryPairing</code>. <a href="#block-retryPairing">Permanent link</a></p><div class="block-reference-table"><table><thead><tr><th>Argument</th><th>Type</th><th>Default value</th><th>Menu choices</th></tr></thead><tbody><tr><th scope="row"><code>SESSION</code></th><td>STRING</td><td><code>pairing-1</code></td><td></td></tr></tbody></table></div></article><article id="block-setPairingTimeout" class="block-reference-entry"><p class="fixed-input-help-link">Fixed literal fields: <code>SECONDS</code>. <a href="#fixed-input-editing">Editing UI and preservation rules</a>.</p><h3><code>set pairing timeout of [SESSION] to [SECONDS] seconds</code></h3><p>Sets how long the exchange waits, measured on this machine's own clock.</p><p>Block type: COMMAND. Opcode: <code>setPairingTimeout</code>. <a href="#block-setPairingTimeout">Permanent link</a></p><div class="block-reference-table"><table><thead><tr><th>Argument</th><th>Type</th><th>Default value</th><th>Menu choices</th></tr></thead><tbody><tr><th scope="row"><code>SESSION</code></th><td>STRING</td><td><code>pairing-1</code></td><td></td></tr><tr><th scope="row"><code>SECONDS</code></th><td>NUMBER</td><td><code>600</code></td><td></td></tr></tbody></table></div></article>
|
|
79
|
+
<p><a href="https://github.com/kubohiroya/turbowarp-webrtc-qrcode-pairing/blob/main/README.md">Repository guide and examples</a></p>
|
|
80
|
+
</section>
|
|
81
|
+
<!-- END COMPLETE BLOCK REFERENCE -->
|
|
82
|
+
|
|
83
|
+
<section id="fixed-input-editing"><h2>Fixed input editing</h2><p>With a compatible TurboWarp GUI, enable fixed-input forms to edit only standard literal fields. Numbers use complete finite JSON number tokens; menu fields use existing choices. Connected reporters and unavailable fields stay read-only and are never evaluated. Saving an unchanged form preserves exact existing raw values, including legacy spellings and unknown choices. Invalid drafts stay in the same editing session; cancel, project/target replacement, or block removal discards them. Disabling the GUI feature flag restores native field editing. This form does not run blocks, open network/audio/key/QR sessions, resolve names, or edit Schema arguments. QR index is a positive integer without a session-derived candidate limit. Timeout remains 1–3600 seconds.</p><ul><li><a href="#block-showPairingQrPart"><code>showPairingQrPart</code></a>: INDEX</li><li><a href="#block-pairingQrPartSvg"><code>pairingQrPartSvg</code></a>: INDEX</li><li><a href="#block-pairingQrPartDataUri"><code>pairingQrPartDataUri</code></a>: INDEX</li><li><a href="#block-setPairingTimeout"><code>setPairingTimeout</code></a>: SECONDS</li></ul></section>
|
|
33
84
|
</body>
|
|
34
85
|
</html>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kubohiroya/turbowarp-webrtc-qrcode-pairing",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.4",
|
|
4
4
|
"description": "TurboWarp extension that pairs WebRTC peers by carrying offers and answers as QR codes.",
|
|
5
5
|
"author": "Hiroya Kubo <hiroya@cuc.ac.jp>",
|
|
6
6
|
"license": "MPL-2.0",
|
|
@@ -25,12 +25,12 @@
|
|
|
25
25
|
"files": [
|
|
26
26
|
"dist/",
|
|
27
27
|
"docs/",
|
|
28
|
-
"schemas/",
|
|
29
28
|
"src/",
|
|
30
29
|
"templates/",
|
|
31
30
|
"README.md",
|
|
32
31
|
"README.ja.md",
|
|
33
|
-
"LICENSE"
|
|
32
|
+
"LICENSE",
|
|
33
|
+
"schemas"
|
|
34
34
|
],
|
|
35
35
|
"publishConfig": {
|
|
36
36
|
"access": "public",
|
|
@@ -57,20 +57,22 @@
|
|
|
57
57
|
},
|
|
58
58
|
"devDependencies": {
|
|
59
59
|
"@eslint/js": "^9.39.5",
|
|
60
|
-
"@kubohiroya/turbowarp-
|
|
61
|
-
"@kubohiroya/
|
|
60
|
+
"@kubohiroya/turbowarp-extension-manifest": "0.3.2",
|
|
61
|
+
"@kubohiroya/turbowarp-jsqr": "0.4.1",
|
|
62
|
+
"@kubohiroya/vite-plugin-turbowarp-extension": "0.6.1",
|
|
62
63
|
"@types/node": "^24.13.3",
|
|
63
64
|
"eslint": "^9.39.5",
|
|
64
65
|
"typescript": "^5.9.3",
|
|
65
66
|
"typescript-eslint": "^8.66.0",
|
|
66
67
|
"vite": "^8.2.2",
|
|
67
68
|
"vitest": "^5.0.0",
|
|
68
|
-
"zxing-wasm": "3.1.4"
|
|
69
|
+
"zxing-wasm": "3.1.4",
|
|
70
|
+
"@kubohiroya/turbowarp-extension-data-editor": "0.2.1"
|
|
69
71
|
},
|
|
70
72
|
"peerDependencies": {
|
|
71
|
-
"@kubohiroya/turbowarp-camera-source": "0.
|
|
72
|
-
"@kubohiroya/turbowarp-jsqr": "
|
|
73
|
-
"@kubohiroya/turbowarp-webrtc": "
|
|
73
|
+
"@kubohiroya/turbowarp-camera-source": "0.17.1",
|
|
74
|
+
"@kubohiroya/turbowarp-jsqr": "0.4.1",
|
|
75
|
+
"@kubohiroya/turbowarp-webrtc": "0.4.1"
|
|
74
76
|
},
|
|
75
77
|
"peerDependenciesMeta": {
|
|
76
78
|
"@kubohiroya/turbowarp-webrtc": {
|
|
@@ -84,6 +86,12 @@
|
|
|
84
86
|
}
|
|
85
87
|
},
|
|
86
88
|
"dependencies": {
|
|
87
|
-
"@kubohiroya/qrcode-structured-append": "0.1.
|
|
89
|
+
"@kubohiroya/qrcode-structured-append": "0.1.1"
|
|
90
|
+
},
|
|
91
|
+
"gitHead": "f085ffbbb8417b058d102cc9c0679160f5904d4f",
|
|
92
|
+
"x-turbowarp-source": {
|
|
93
|
+
"revision": "f085ffbbb8417b058d102cc9c0679160f5904d4f",
|
|
94
|
+
"revisionRole": "base-commit",
|
|
95
|
+
"sourceState": "local-release-overlay"
|
|
88
96
|
}
|
|
89
97
|
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"$id": "https://kubohiroya.github.io/turbowarp-webrtc-qrcode-pairing/schemas/block-editor-fixed-inputs.schema.json",
|
|
4
|
+
"title": "turbowarp-webrtc-qrcode-pairing fixed editing contracts",
|
|
5
|
+
"$defs": {
|
|
6
|
+
"showPairingQrPart": {
|
|
7
|
+
"type": "object",
|
|
8
|
+
"properties": {
|
|
9
|
+
"INDEX": {
|
|
10
|
+
"type": "integer",
|
|
11
|
+
"minimum": 1
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"additionalProperties": false
|
|
15
|
+
},
|
|
16
|
+
"pairingQrPartSvg": {
|
|
17
|
+
"type": "object",
|
|
18
|
+
"properties": {
|
|
19
|
+
"INDEX": {
|
|
20
|
+
"type": "integer",
|
|
21
|
+
"minimum": 1
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"additionalProperties": false
|
|
25
|
+
},
|
|
26
|
+
"pairingQrPartDataUri": {
|
|
27
|
+
"type": "object",
|
|
28
|
+
"properties": {
|
|
29
|
+
"INDEX": {
|
|
30
|
+
"type": "integer",
|
|
31
|
+
"minimum": 1
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
"additionalProperties": false
|
|
35
|
+
},
|
|
36
|
+
"setPairingTimeout": {
|
|
37
|
+
"type": "object",
|
|
38
|
+
"properties": {
|
|
39
|
+
"SECONDS": {
|
|
40
|
+
"type": "number",
|
|
41
|
+
"minimum": 1,
|
|
42
|
+
"maximum": 3600
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
"additionalProperties": false
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
package/src/extension.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import {registerFixedInputEditors} from './fixed-input-editors.js';
|
|
1
2
|
import definitions from './block-definitions.json';
|
|
2
3
|
import {extensionConfig} from './config';
|
|
3
4
|
import {featureFlags} from './config/feature-flags.js';
|
|
@@ -33,6 +34,8 @@ export interface WebRtcQrCodePairingExtensionOptions extends PairingControllerOp
|
|
|
33
34
|
* a runtime.
|
|
34
35
|
*/
|
|
35
36
|
export class WebRtcQrCodePairingExtension implements TurboWarpExtension {
|
|
37
|
+
private fixedEditorsDispose: (() => void) | undefined;
|
|
38
|
+
|
|
36
39
|
private readonly enabled: boolean;
|
|
37
40
|
private readonly runtime: TurboWarpRuntime;
|
|
38
41
|
private readonly pairing: PairingController;
|
|
@@ -68,13 +71,15 @@ export class WebRtcQrCodePairingExtension implements TurboWarpExtension {
|
|
|
68
71
|
}
|
|
69
72
|
|
|
70
73
|
public getInfo(): Record<string, unknown> {
|
|
71
|
-
|
|
74
|
+
const info = {
|
|
72
75
|
id: extensionConfig.id,
|
|
73
76
|
name: Scratch.translate(definitions.extensionName),
|
|
74
77
|
docsURI: extensionConfig.docsURI,
|
|
75
78
|
blockIconURI: extensionConfig.blockIconURI,
|
|
76
79
|
blocks: this.enabled ? blockDefinitions.map((block) => this.toScratchBlock(block)) : []
|
|
77
80
|
};
|
|
81
|
+
this.fixedEditorsDispose ??= registerFixedInputEditors(Scratch, this.runtime, info.blocks);
|
|
82
|
+
return info;
|
|
78
83
|
}
|
|
79
84
|
|
|
80
85
|
// --- Starting an exchange ------------------------------------------------
|
|
@@ -246,6 +251,7 @@ export class WebRtcQrCodePairingExtension implements TurboWarpExtension {
|
|
|
246
251
|
}
|
|
247
252
|
|
|
248
253
|
public dispose(): void {
|
|
254
|
+
this.fixedEditorsDispose?.();
|
|
249
255
|
this.pairing.dispose();
|
|
250
256
|
this.runtime.off?.('PROJECT_RUN_STOP', this.stopListener);
|
|
251
257
|
this.runtime.off?.('PROJECT_STOP_ALL', this.stopListener);
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
FixedInputBinding,
|
|
3
|
+
FixedInputEditorOptions,
|
|
4
|
+
JsonValue,
|
|
5
|
+
LiteralValue,
|
|
6
|
+
} from "@kubohiroya/turbowarp-extension-data-editor";
|
|
7
|
+
import type { SchemaBundle } from "@kubohiroya/turbowarp-extension-data-editor/schema";
|
|
8
|
+
import schemas from "../schemas/block-editor-fixed-inputs.schema.json" with { type: "json" };
|
|
9
|
+
interface InputSchema {
|
|
10
|
+
readonly type: string;
|
|
11
|
+
readonly enum?: readonly (string | number)[];
|
|
12
|
+
readonly minimum?: number;
|
|
13
|
+
readonly maximum?: number;
|
|
14
|
+
readonly exclusiveMinimum?: number;
|
|
15
|
+
}
|
|
16
|
+
const contracts = schemas.$defs as Readonly<
|
|
17
|
+
Record<string, { readonly properties: Readonly<Record<string, InputSchema>> }>
|
|
18
|
+
>;
|
|
19
|
+
const repository = "turbowarp-webrtc-qrcode-pairing";
|
|
20
|
+
const extensionId = "kubohiroyawebrtcqrcodepairing";
|
|
21
|
+
const token = /^-?(?:0|[1-9][0-9]*)(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?$/u;
|
|
22
|
+
export function readNumber(value: LiteralValue): number {
|
|
23
|
+
if (typeof value !== "string" && typeof value !== "number")
|
|
24
|
+
throw new TypeError("Invalid numeric projection.");
|
|
25
|
+
if (typeof value === "string" && value.trim() === "")
|
|
26
|
+
throw new TypeError("Existing blank field is readonly.");
|
|
27
|
+
const number = Number(value);
|
|
28
|
+
if (!Number.isFinite(number))
|
|
29
|
+
throw new TypeError("Existing nonfinite field is readonly.");
|
|
30
|
+
return number;
|
|
31
|
+
}
|
|
32
|
+
export function newNumberToken(value: JsonValue): string {
|
|
33
|
+
if (typeof value === "number") {
|
|
34
|
+
if (!Number.isFinite(value)) throw new TypeError("Invalid new number.");
|
|
35
|
+
return Object.is(value, -0) ? "-0" : String(value);
|
|
36
|
+
}
|
|
37
|
+
if (
|
|
38
|
+
typeof value !== "string" ||
|
|
39
|
+
!token.test(value.trim()) ||
|
|
40
|
+
!Number.isFinite(Number(value))
|
|
41
|
+
)
|
|
42
|
+
throw new TypeError("Invalid new number.");
|
|
43
|
+
return value.trim();
|
|
44
|
+
}
|
|
45
|
+
function existingChoice(_opcode: string, value: LiteralValue): string {
|
|
46
|
+
if (typeof value !== "string")
|
|
47
|
+
throw new TypeError("Invalid string projection.");
|
|
48
|
+
return value;
|
|
49
|
+
}
|
|
50
|
+
export function candidateValid(
|
|
51
|
+
_opcode: string,
|
|
52
|
+
_argument: string,
|
|
53
|
+
schema: InputSchema,
|
|
54
|
+
value: LiteralValue,
|
|
55
|
+
): boolean {
|
|
56
|
+
if (schema.type === "number" || schema.type === "integer") {
|
|
57
|
+
let number;
|
|
58
|
+
try {
|
|
59
|
+
number = readNumber(value);
|
|
60
|
+
} catch {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
if (schema.type === "integer" && !Number.isInteger(number)) return false;
|
|
64
|
+
if (schema.minimum !== undefined && number < schema.minimum) return false;
|
|
65
|
+
if (schema.maximum !== undefined && number > schema.maximum) return false;
|
|
66
|
+
if (
|
|
67
|
+
schema.exclusiveMinimum !== undefined &&
|
|
68
|
+
number <= schema.exclusiveMinimum
|
|
69
|
+
)
|
|
70
|
+
return false;
|
|
71
|
+
if (schema.enum && !schema.enum!.includes(number)) return false;
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
return typeof value === "string" && schema.enum!.includes(value);
|
|
75
|
+
}
|
|
76
|
+
export function fixedInputEditorOptions(): readonly FixedInputEditorOptions[] {
|
|
77
|
+
return Object.entries(contracts).map(([opcode, contract]) => ({
|
|
78
|
+
apiVersion: 1,
|
|
79
|
+
extensionId,
|
|
80
|
+
editorId: `${extensionId}.fixed.${opcode}`,
|
|
81
|
+
opcodes: [`${extensionId}_${opcode}`],
|
|
82
|
+
contractId: `${repository}.${opcode}.fixed`,
|
|
83
|
+
contractRevision: "fixed-inputs-v1",
|
|
84
|
+
validationContext: () => ({
|
|
85
|
+
consumerId: repository,
|
|
86
|
+
nowMs: 0,
|
|
87
|
+
settings: null,
|
|
88
|
+
}),
|
|
89
|
+
bindings: Object.entries(contract.properties).map(
|
|
90
|
+
([argument, schema]): FixedInputBinding => {
|
|
91
|
+
const numeric = schema.type === "number" || schema.type === "integer";
|
|
92
|
+
const source = {
|
|
93
|
+
repository,
|
|
94
|
+
path: "schemas/block-editor-fixed-inputs.schema.json",
|
|
95
|
+
symbol: `/$defs/${opcode}/properties/${argument}`,
|
|
96
|
+
};
|
|
97
|
+
return {
|
|
98
|
+
argument,
|
|
99
|
+
schemaBundle: {
|
|
100
|
+
identity: {
|
|
101
|
+
ownerId: repository,
|
|
102
|
+
contractId: `${opcode}.${argument}`,
|
|
103
|
+
revision: "fixed-inputs-v1",
|
|
104
|
+
variantId: "fixed",
|
|
105
|
+
},
|
|
106
|
+
dialect: "draft-07",
|
|
107
|
+
root: {
|
|
108
|
+
retrievalUri: `${schemas.$id}?binding=${opcode}.${argument}`,
|
|
109
|
+
document: schema as unknown as SchemaBundle["root"]["document"],
|
|
110
|
+
artifact: { role: "canonical-schema", source },
|
|
111
|
+
},
|
|
112
|
+
resources: [],
|
|
113
|
+
provenance: {
|
|
114
|
+
canonicalSources: [
|
|
115
|
+
source,
|
|
116
|
+
{ repository, path: "src/block-definitions.json" },
|
|
117
|
+
{ repository, path: "src/extension.ts", symbol: opcode },
|
|
118
|
+
],
|
|
119
|
+
generatedFiles: [],
|
|
120
|
+
referenceSchemas: [],
|
|
121
|
+
generationSources: [],
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
control: numeric
|
|
125
|
+
? { kind: "number" }
|
|
126
|
+
: {
|
|
127
|
+
kind: "enum",
|
|
128
|
+
choices: schema.enum!.map((value) => ({
|
|
129
|
+
value,
|
|
130
|
+
label: String(value),
|
|
131
|
+
})),
|
|
132
|
+
},
|
|
133
|
+
read: (value) =>
|
|
134
|
+
numeric ? readNumber(value) : existingChoice(opcode, value),
|
|
135
|
+
encode: (value) => {
|
|
136
|
+
if (numeric) return newNumberToken(value);
|
|
137
|
+
if (typeof value !== "string" || !schema.enum!.includes(value))
|
|
138
|
+
throw new TypeError("Invalid new enum value.");
|
|
139
|
+
return value;
|
|
140
|
+
},
|
|
141
|
+
validateCandidate: (value) =>
|
|
142
|
+
candidateValid(opcode, argument, schema, value)
|
|
143
|
+
? []
|
|
144
|
+
: [
|
|
145
|
+
{
|
|
146
|
+
path: argument,
|
|
147
|
+
message: "Check this value before saving.",
|
|
148
|
+
},
|
|
149
|
+
],
|
|
150
|
+
};
|
|
151
|
+
},
|
|
152
|
+
),
|
|
153
|
+
}));
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
interface RegistrationRuntime {
|
|
157
|
+
on?(event: string, listener: () => void): void;
|
|
158
|
+
off?(event: string, listener: () => void): void;
|
|
159
|
+
}
|
|
160
|
+
/** Register only visible static contracts; ordinary composition consumers load no editor runtime. */
|
|
161
|
+
export function registerFixedInputEditors(
|
|
162
|
+
scratch: unknown,
|
|
163
|
+
runtime: unknown,
|
|
164
|
+
visibleBlocks: readonly unknown[],
|
|
165
|
+
): (() => void) | undefined {
|
|
166
|
+
const api = (
|
|
167
|
+
scratch as {
|
|
168
|
+
gui?: {
|
|
169
|
+
blockEditors?: {
|
|
170
|
+
apiVersion?: number;
|
|
171
|
+
registerFixedInputEditor?: (
|
|
172
|
+
options: FixedInputEditorOptions,
|
|
173
|
+
) => () => void;
|
|
174
|
+
};
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
).gui?.blockEditors;
|
|
178
|
+
if (
|
|
179
|
+
api?.apiVersion !== 1 ||
|
|
180
|
+
typeof api.registerFixedInputEditor !== "function"
|
|
181
|
+
)
|
|
182
|
+
return undefined;
|
|
183
|
+
const visibleOpcodes = new Set(
|
|
184
|
+
visibleBlocks.flatMap((block) => {
|
|
185
|
+
if (typeof block !== "object" || block === null || !("opcode" in block))
|
|
186
|
+
return [];
|
|
187
|
+
return typeof block.opcode === "string" ? [block.opcode] : [];
|
|
188
|
+
}),
|
|
189
|
+
);
|
|
190
|
+
const events = runtime as RegistrationRuntime | undefined;
|
|
191
|
+
const removers: (() => void)[] = [];
|
|
192
|
+
let active = true;
|
|
193
|
+
const dispose = () => {
|
|
194
|
+
if (!active) return;
|
|
195
|
+
active = false;
|
|
196
|
+
events?.off?.("RUNTIME_DISPOSED", dispose);
|
|
197
|
+
for (const remove of removers.splice(0).reverse()) remove();
|
|
198
|
+
};
|
|
199
|
+
try {
|
|
200
|
+
for (const options of fixedInputEditorOptions()) {
|
|
201
|
+
if (
|
|
202
|
+
options.opcodes.some((opcode) =>
|
|
203
|
+
visibleOpcodes.has(opcode.slice(extensionId.length + 1)),
|
|
204
|
+
)
|
|
205
|
+
)
|
|
206
|
+
removers.push(api.registerFixedInputEditor.call(api, options));
|
|
207
|
+
}
|
|
208
|
+
events?.on?.("RUNTIME_DISPOSED", dispose);
|
|
209
|
+
} catch (error) {
|
|
210
|
+
dispose();
|
|
211
|
+
throw error;
|
|
212
|
+
}
|
|
213
|
+
return dispose;
|
|
214
|
+
}
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
-
"$id": "https://raw.githubusercontent.com/kubohiroya/turbowarp-extension-template/main/schemas/extension-manifest.schema.json",
|
|
4
|
-
"title": "TurboWarp Extension API Manifest",
|
|
5
|
-
"type": "object",
|
|
6
|
-
"additionalProperties": false,
|
|
7
|
-
"required": ["formatVersion", "id", "blocks", "menus"],
|
|
8
|
-
"properties": {
|
|
9
|
-
"formatVersion": {"const": 1},
|
|
10
|
-
"id": {"type": "string", "pattern": "^[a-z0-9]+$"},
|
|
11
|
-
"blocks": {
|
|
12
|
-
"type": "array",
|
|
13
|
-
"items": {
|
|
14
|
-
"type": "object",
|
|
15
|
-
"additionalProperties": false,
|
|
16
|
-
"required": ["opcode", "blockType", "arguments"],
|
|
17
|
-
"properties": {
|
|
18
|
-
"opcode": {"type": "string", "minLength": 1},
|
|
19
|
-
"blockType": {"type": "string", "minLength": 1},
|
|
20
|
-
"arguments": {
|
|
21
|
-
"type": "array",
|
|
22
|
-
"items": {
|
|
23
|
-
"type": "object",
|
|
24
|
-
"additionalProperties": false,
|
|
25
|
-
"required": ["id", "type"],
|
|
26
|
-
"properties": {
|
|
27
|
-
"id": {"type": "string", "minLength": 1},
|
|
28
|
-
"type": {"type": "string", "minLength": 1},
|
|
29
|
-
"menu": {"type": "string", "minLength": 1}
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
},
|
|
36
|
-
"menus": {
|
|
37
|
-
"type": "array",
|
|
38
|
-
"items": {
|
|
39
|
-
"type": "object",
|
|
40
|
-
"additionalProperties": false,
|
|
41
|
-
"required": ["id", "acceptReporters"],
|
|
42
|
-
"properties": {
|
|
43
|
-
"id": {"type": "string", "minLength": 1},
|
|
44
|
-
"acceptReporters": {"type": "boolean"}
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
}
|
|
@@ -1,149 +0,0 @@
|
|
|
1
|
-
import type {Plugin} from 'vite';
|
|
2
|
-
|
|
3
|
-
export const EXTENSION_MANIFEST_FORMAT_VERSION = 1 as const;
|
|
4
|
-
|
|
5
|
-
export interface ExtensionManifestArgument {
|
|
6
|
-
id: string;
|
|
7
|
-
type: string;
|
|
8
|
-
menu?: string;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export interface ExtensionManifestBlock {
|
|
12
|
-
opcode: string;
|
|
13
|
-
blockType: string;
|
|
14
|
-
arguments: ExtensionManifestArgument[];
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export interface ExtensionManifestMenu {
|
|
18
|
-
id: string;
|
|
19
|
-
acceptReporters: boolean;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export interface ExtensionManifest {
|
|
23
|
-
formatVersion: typeof EXTENSION_MANIFEST_FORMAT_VERSION;
|
|
24
|
-
id: string;
|
|
25
|
-
blocks: ExtensionManifestBlock[];
|
|
26
|
-
menus: ExtensionManifestMenu[];
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export interface ExtensionManifestPluginOptions {
|
|
30
|
-
id: string;
|
|
31
|
-
definitions: unknown;
|
|
32
|
-
fileName?: string;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export function createExtensionManifest(id: string, definitions: unknown): ExtensionManifest {
|
|
36
|
-
if (!/^[a-z0-9]+$/.test(id)) {
|
|
37
|
-
throw new TypeError('Extension manifest ID must contain only lowercase letters and numbers.');
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
const source = requireRecord(definitions, 'Block definitions');
|
|
41
|
-
const sourceBlocks = source.blocks;
|
|
42
|
-
if (!Array.isArray(sourceBlocks)) {
|
|
43
|
-
throw new TypeError('Block definitions must contain a blocks array.');
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
const menus = normalizeMenus(source.menus);
|
|
47
|
-
const menuIds = new Set(menus.map((menu) => menu.id));
|
|
48
|
-
const seenOpcodes = new Set<string>();
|
|
49
|
-
const blocks = sourceBlocks.map((block, index) => {
|
|
50
|
-
const normalized = normalizeBlock(block, index, menuIds);
|
|
51
|
-
if (seenOpcodes.has(normalized.opcode)) {
|
|
52
|
-
throw new TypeError(`Duplicate block opcode: ${normalized.opcode}`);
|
|
53
|
-
}
|
|
54
|
-
seenOpcodes.add(normalized.opcode);
|
|
55
|
-
return normalized;
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
return {
|
|
59
|
-
formatVersion: EXTENSION_MANIFEST_FORMAT_VERSION,
|
|
60
|
-
id,
|
|
61
|
-
blocks: blocks.sort((left, right) => compareIds(left.opcode, right.opcode)),
|
|
62
|
-
menus
|
|
63
|
-
};
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
export function serializeExtensionManifest(id: string, definitions: unknown): string {
|
|
67
|
-
return `${JSON.stringify(createExtensionManifest(id, definitions), null, 2)}\n`;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
export function extensionManifestPlugin(options: ExtensionManifestPluginOptions): Plugin {
|
|
71
|
-
return {
|
|
72
|
-
name: 'extension-api-manifest',
|
|
73
|
-
apply: 'build',
|
|
74
|
-
enforce: 'post',
|
|
75
|
-
generateBundle() {
|
|
76
|
-
this.emitFile({
|
|
77
|
-
type: 'asset',
|
|
78
|
-
fileName: options.fileName ?? 'extension-manifest.json',
|
|
79
|
-
source: serializeExtensionManifest(options.id, options.definitions)
|
|
80
|
-
});
|
|
81
|
-
}
|
|
82
|
-
};
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
function normalizeBlock(
|
|
86
|
-
value: unknown,
|
|
87
|
-
index: number,
|
|
88
|
-
menuIds: ReadonlySet<string>
|
|
89
|
-
): ExtensionManifestBlock {
|
|
90
|
-
const block = requireRecord(value, `Block at index ${index}`);
|
|
91
|
-
const opcode = requireNonEmptyString(block.opcode, `Block at index ${index} opcode`);
|
|
92
|
-
const blockType = requireNonEmptyString(block.blockType, `Block ${opcode} blockType`);
|
|
93
|
-
const sourceArguments = block.arguments ?? {};
|
|
94
|
-
const argumentRecord = requireRecord(sourceArguments, `Block ${opcode} arguments`);
|
|
95
|
-
const argumentsList = Object.entries(argumentRecord).map(([argumentId, argument]) => {
|
|
96
|
-
requireNonEmptyString(argumentId, `Block ${opcode} argument ID`);
|
|
97
|
-
const definition = requireRecord(argument, `Block ${opcode} argument ${argumentId}`);
|
|
98
|
-
const type = requireNonEmptyString(
|
|
99
|
-
definition.type,
|
|
100
|
-
`Block ${opcode} argument ${argumentId} type`
|
|
101
|
-
);
|
|
102
|
-
const menu = definition.menu;
|
|
103
|
-
if (menu !== undefined && (typeof menu !== 'string' || !menuIds.has(menu))) {
|
|
104
|
-
throw new TypeError(`Block ${opcode} argument ${argumentId} references unknown menu: ${menu}`);
|
|
105
|
-
}
|
|
106
|
-
return menu === undefined ? {id: argumentId, type} : {id: argumentId, type, menu};
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
return {
|
|
110
|
-
opcode,
|
|
111
|
-
blockType,
|
|
112
|
-
arguments: argumentsList.sort((left, right) => compareIds(left.id, right.id))
|
|
113
|
-
};
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
function normalizeMenus(value: unknown): ExtensionManifestMenu[] {
|
|
117
|
-
const menuRecord = requireRecord(value ?? {}, 'Block definition menus');
|
|
118
|
-
return Object.entries(menuRecord)
|
|
119
|
-
.map(([id, menu]) => {
|
|
120
|
-
requireNonEmptyString(id, 'Menu ID');
|
|
121
|
-
const definition = requireRecord(menu, `Menu ${id}`);
|
|
122
|
-
const acceptReporters = definition.acceptReporters ?? false;
|
|
123
|
-
if (typeof acceptReporters !== 'boolean') {
|
|
124
|
-
throw new TypeError(`Menu ${id} acceptReporters must be a boolean.`);
|
|
125
|
-
}
|
|
126
|
-
return {id, acceptReporters};
|
|
127
|
-
})
|
|
128
|
-
.sort((left, right) => compareIds(left.id, right.id));
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
function requireRecord(value: unknown, label: string): Record<string, unknown> {
|
|
132
|
-
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
133
|
-
throw new TypeError(`${label} must be an object.`);
|
|
134
|
-
}
|
|
135
|
-
return value as Record<string, unknown>;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
function requireNonEmptyString(value: unknown, label: string): string {
|
|
139
|
-
if (typeof value !== 'string' || value.length === 0) {
|
|
140
|
-
throw new TypeError(`${label} must be a non-empty string.`);
|
|
141
|
-
}
|
|
142
|
-
return value;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
function compareIds(left: string, right: string): number {
|
|
146
|
-
if (left < right) return -1;
|
|
147
|
-
if (left > right) return 1;
|
|
148
|
-
return 0;
|
|
149
|
-
}
|