@serverkgg/bridge 0.0.9 → 0.0.10
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/package.json +1 -1
- package/src/cli/validate.ts +1 -0
- package/src/context/bridgeContext.ts +8 -1
- package/src/core/bridgeKind.ts +1 -0
- package/src/core/bridgeProbeError.ts +16 -0
- package/src/core/bridgeSetup.ts +20 -0
- package/src/core/bridgeValue.test.ts +29 -0
- package/src/core/bridgeValue.ts +12 -0
- package/src/core/index.ts +2 -0
- package/src/guides/blocks.ts +8 -0
- package/src/guides/markdown.test.ts +18 -0
- package/src/guides/markdown.ts +7 -1
- package/src/manifest/manifest.test.ts +49 -2
- package/src/manifest/presence.ts +9 -1
- package/src/modules/bridge.ts +20 -0
- package/src/modules/bridgeDriver.ts +2 -0
- package/src/modules/bridgeModule.ts +7 -0
- package/src/panel/bridgeDetail.ts +5 -5
- package/src/probes/a2s.test.ts +71 -0
- package/src/probes/a2s.ts +38 -11
- package/src/protocol/bridgeFrame.ts +5 -0
- package/src/protocol/bridgeLimits.ts +2 -0
- package/src/protocol/bridgeManifest.ts +2 -0
- package/src/protocol/bridgeSetup.ts +97 -0
- package/src/protocol/index.ts +1 -0
- package/src/runtime/bridgeConfigReader.ts +12 -8
- package/src/runtime/bridgeContext.ts +28 -2
- package/src/runtime/bridgeDriver.test.ts +272 -2
- package/src/runtime/bridgeDriver.ts +82 -1
- package/src/runtime/bridgeEvents.test.ts +96 -0
- package/src/runtime/bridgeEvents.ts +4 -1
- package/src/runtime/bridgeRegistry.test.ts +38 -1
- package/src/runtime/bridgeRegistry.ts +14 -1
- package/src/runtime/bridgeSecrets.test.ts +121 -0
- package/src/runtime/bridgeSecrets.ts +43 -0
- package/src/runtime/bridgeServer.test.ts +262 -2
- package/src/runtime/bridgeServer.ts +17 -0
- package/src/runtime/bridgeSetup.test.ts +189 -0
- package/src/runtime/bridgeSetup.ts +63 -0
- package/src/runtime/bridgeSignal.test.ts +114 -0
- package/src/runtime/bridgeSignal.ts +31 -0
- package/src/runtime/bridgeSupervisor.test.ts +111 -0
- package/src/runtime/bridgeSupervisor.ts +37 -5
- package/src/runtime/serve.ts +52 -1
- package/src/utils/index.ts +1 -0
- package/src/utils/stripAnsi.ts +12 -0
- package/src/wire/panel.test.ts +93 -0
- package/src/wire/panel.ts +40 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { BridgeKind, BridgeStream } from "../core";
|
|
2
2
|
import type { BridgePanel } from "../panel";
|
|
3
3
|
import type { BridgeTerminalCommand, BridgeTerminalLevel } from "../terminal";
|
|
4
|
+
import type { BridgeSetupManifest } from "./bridgeSetup";
|
|
4
5
|
|
|
5
6
|
export interface BridgeTerminalRuleManifest {
|
|
6
7
|
match: string;
|
|
@@ -28,6 +29,7 @@ export interface BridgeManifest {
|
|
|
28
29
|
version: string;
|
|
29
30
|
panel: BridgePanel | null;
|
|
30
31
|
terminal: BridgeTerminalManifest | null;
|
|
32
|
+
setup: BridgeSetupManifest | null;
|
|
31
33
|
modules: BridgeModuleManifest[];
|
|
32
34
|
events: string[];
|
|
33
35
|
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import type { BridgeSetupPromptKind, BridgeSetupStepKind, BridgeSetupStepState, LocalizedText } from "../core";
|
|
2
|
+
import type { GuideOpenTarget } from "../guides";
|
|
3
|
+
|
|
4
|
+
interface BridgeSetupStepBase {
|
|
5
|
+
id: string;
|
|
6
|
+
title: LocalizedText;
|
|
7
|
+
help?: LocalizedText;
|
|
8
|
+
required?: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface BridgeSetupDriverStep extends BridgeSetupStepBase {
|
|
12
|
+
kind: BridgeSetupStepKind.Driver;
|
|
13
|
+
requiresRunning?: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface BridgeSetupFormStep extends BridgeSetupStepBase {
|
|
17
|
+
kind: BridgeSetupStepKind.Form;
|
|
18
|
+
tab: string;
|
|
19
|
+
section: string;
|
|
20
|
+
fields?: string[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface BridgeSetupOpenStep extends BridgeSetupStepBase {
|
|
24
|
+
kind: BridgeSetupStepKind.Open;
|
|
25
|
+
target: GuideOpenTarget;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export type BridgeSetupStep = BridgeSetupDriverStep | BridgeSetupFormStep | BridgeSetupOpenStep;
|
|
29
|
+
|
|
30
|
+
export interface BridgeSetupManifest {
|
|
31
|
+
steps: BridgeSetupStep[];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface BridgeSetupAction {
|
|
35
|
+
id: string;
|
|
36
|
+
label: LocalizedText;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface BridgeSetupWaitPrompt {
|
|
40
|
+
kind: BridgeSetupPromptKind.Wait;
|
|
41
|
+
message: LocalizedText;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface BridgeSetupProgressPrompt {
|
|
45
|
+
kind: BridgeSetupPromptKind.Progress;
|
|
46
|
+
message: LocalizedText;
|
|
47
|
+
percent: number | null;
|
|
48
|
+
detail: string | null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface BridgeSetupLinkPrompt {
|
|
52
|
+
kind: BridgeSetupPromptKind.Link;
|
|
53
|
+
message: LocalizedText;
|
|
54
|
+
url: string;
|
|
55
|
+
code: string | null;
|
|
56
|
+
expiresAt: string | null;
|
|
57
|
+
actions: BridgeSetupAction[];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface BridgeSetupActionPrompt {
|
|
61
|
+
kind: BridgeSetupPromptKind.Action;
|
|
62
|
+
message: LocalizedText;
|
|
63
|
+
actions: BridgeSetupAction[];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface BridgeSetupFailedPrompt {
|
|
67
|
+
kind: BridgeSetupPromptKind.Failed;
|
|
68
|
+
message: LocalizedText;
|
|
69
|
+
actions: BridgeSetupAction[];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export type BridgeSetupPrompt =
|
|
73
|
+
| BridgeSetupActionPrompt
|
|
74
|
+
| BridgeSetupFailedPrompt
|
|
75
|
+
| BridgeSetupLinkPrompt
|
|
76
|
+
| BridgeSetupProgressPrompt
|
|
77
|
+
| BridgeSetupWaitPrompt;
|
|
78
|
+
|
|
79
|
+
export interface BridgeSetupStepStatus {
|
|
80
|
+
id: string;
|
|
81
|
+
state: BridgeSetupStepState;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export interface BridgeSetupRuntime {
|
|
85
|
+
steps: BridgeSetupStepStatus[];
|
|
86
|
+
prompt: BridgeSetupPrompt | null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export interface BridgeSetupSummary {
|
|
90
|
+
step: string | null;
|
|
91
|
+
attention: boolean;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export interface BridgeSetupResult {
|
|
95
|
+
supported: boolean;
|
|
96
|
+
runtime: BridgeSetupRuntime | null;
|
|
97
|
+
}
|
package/src/protocol/index.ts
CHANGED
|
@@ -82,14 +82,8 @@ export const statePath = (config: BridgeConfig) => {
|
|
|
82
82
|
return `${config.run}/${BRIDGE_STATE_NAME}`;
|
|
83
83
|
};
|
|
84
84
|
|
|
85
|
-
export const
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
if (!(await file.exists())) {
|
|
89
|
-
throw new BridgeConfigError(`no bridge config at "${path}"`);
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
const parsed: unknown = JSON.parse(await file.text());
|
|
85
|
+
export const parseConfig = (text: string): BridgeConfig => {
|
|
86
|
+
const parsed: unknown = JSON.parse(text);
|
|
93
87
|
|
|
94
88
|
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
95
89
|
throw new BridgeConfigError("bridge config must be an object");
|
|
@@ -117,3 +111,13 @@ export const readConfig = async (path: string): Promise<BridgeConfig> => {
|
|
|
117
111
|
|
|
118
112
|
return config;
|
|
119
113
|
};
|
|
114
|
+
|
|
115
|
+
export const readConfig = async (path: string): Promise<BridgeConfig> => {
|
|
116
|
+
const file = Bun.file(path);
|
|
117
|
+
|
|
118
|
+
if (!(await file.exists())) {
|
|
119
|
+
throw new BridgeConfigError(`no bridge config at "${path}"`);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return parseConfig(await file.text());
|
|
123
|
+
};
|
|
@@ -1,18 +1,22 @@
|
|
|
1
1
|
import type { BridgeContext } from "../context";
|
|
2
2
|
import type { BridgeValues } from "../core";
|
|
3
|
-
import type { BridgeConfig } from "../protocol";
|
|
3
|
+
import type { BridgeConfig, BridgeSetupRuntime } from "../protocol";
|
|
4
|
+
import { stripAnsi } from "../utils";
|
|
4
5
|
import { createBridgeCodecs } from "./bridgeCodec";
|
|
6
|
+
import { configPath } from "./bridgeConfigReader";
|
|
5
7
|
import { createBridgeEgress } from "./bridgeEgress";
|
|
6
8
|
import { createBridgeExec } from "./bridgeExec";
|
|
7
9
|
import { createBridgeFiles } from "./bridgeFiles";
|
|
8
10
|
import type { BridgeLeveledLogger } from "./bridgeLog";
|
|
9
11
|
import { createBridgeNet } from "./bridgeNet";
|
|
10
12
|
import { createBridgeProbes } from "./bridgeProbe";
|
|
13
|
+
import { createBridgeSecrets } from "./bridgeSecrets";
|
|
11
14
|
import type { BridgeSupervisor } from "./bridgeSupervisor";
|
|
12
15
|
|
|
13
16
|
export interface ContextHooks {
|
|
14
17
|
onLog: BridgeLeveledLogger;
|
|
15
18
|
onEmit: (event: string, payload?: BridgeValues) => void;
|
|
19
|
+
onSetup: (runtime: BridgeSetupRuntime | null) => void;
|
|
16
20
|
}
|
|
17
21
|
|
|
18
22
|
export const createContext = (
|
|
@@ -22,6 +26,8 @@ export const createContext = (
|
|
|
22
26
|
): BridgeContext => {
|
|
23
27
|
const egress = createBridgeEgress(config);
|
|
24
28
|
|
|
29
|
+
const secrets = createBridgeSecrets(configPath(), config);
|
|
30
|
+
|
|
25
31
|
const files = createBridgeFiles({
|
|
26
32
|
root: config.cwd,
|
|
27
33
|
uid: config.gameUid,
|
|
@@ -45,7 +51,7 @@ export const createContext = (
|
|
|
45
51
|
},
|
|
46
52
|
|
|
47
53
|
secret(key) {
|
|
48
|
-
return
|
|
54
|
+
return secrets.read(key);
|
|
49
55
|
},
|
|
50
56
|
|
|
51
57
|
port(key) {
|
|
@@ -84,6 +90,26 @@ export const createContext = (
|
|
|
84
90
|
async watch(pattern, timeoutMs) {
|
|
85
91
|
return await supervisor.watch(pattern, timeoutMs);
|
|
86
92
|
},
|
|
93
|
+
|
|
94
|
+
follow(pattern, onMatch) {
|
|
95
|
+
return supervisor.subscribe((entry) => {
|
|
96
|
+
const match = stripAnsi(entry.line).match(pattern);
|
|
97
|
+
|
|
98
|
+
if (match) {
|
|
99
|
+
onMatch(match);
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
|
|
105
|
+
setup: {
|
|
106
|
+
report(runtime) {
|
|
107
|
+
hooks.onSetup(runtime);
|
|
108
|
+
},
|
|
109
|
+
|
|
110
|
+
clear() {
|
|
111
|
+
hooks.onSetup(null);
|
|
112
|
+
},
|
|
87
113
|
},
|
|
88
114
|
|
|
89
115
|
emit(event, payload) {
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
|
-
import { BridgeKind, PortJoin, PortProtocol } from "../core";
|
|
2
|
+
import { BridgeKind, BridgeSetupStepKind, PortJoin, PortProtocol } from "../core";
|
|
3
|
+
import { GuideOpenTab } from "../guides";
|
|
3
4
|
import type { ManifestPort, ManifestPortActivation } from "../manifest";
|
|
4
|
-
import type { BridgeDriver, BridgePendingModule } from "../modules";
|
|
5
|
+
import type { BridgeDriver, BridgePendingModule, BridgeSetupModule } from "../modules";
|
|
5
6
|
import {
|
|
6
7
|
BridgeConfirm,
|
|
7
8
|
BridgeControl,
|
|
@@ -13,6 +14,7 @@ import {
|
|
|
13
14
|
BridgeUploadMode,
|
|
14
15
|
type BridgeVisibleWhen,
|
|
15
16
|
} from "../panel";
|
|
17
|
+
import type { BridgeSetupStep } from "../protocol";
|
|
16
18
|
import { validateDriver, validatePortActivation } from "./bridgeDriver";
|
|
17
19
|
|
|
18
20
|
const text = (value: string) => ({
|
|
@@ -1125,3 +1127,271 @@ describe("the pending module", () => {
|
|
|
1125
1127
|
).toThrow('module id "pending" is reserved');
|
|
1126
1128
|
});
|
|
1127
1129
|
});
|
|
1130
|
+
|
|
1131
|
+
const driverWithSetup = (setup: BridgeSetupModule): BridgeDriver => ({
|
|
1132
|
+
install: {
|
|
1133
|
+
kind: BridgeKind.Install,
|
|
1134
|
+
run: async () => {
|
|
1135
|
+
return;
|
|
1136
|
+
},
|
|
1137
|
+
},
|
|
1138
|
+
lifecycle: {
|
|
1139
|
+
kind: BridgeKind.Lifecycle,
|
|
1140
|
+
command: async () => [
|
|
1141
|
+
"run",
|
|
1142
|
+
],
|
|
1143
|
+
},
|
|
1144
|
+
setup,
|
|
1145
|
+
panel: {
|
|
1146
|
+
tabs: [
|
|
1147
|
+
{
|
|
1148
|
+
id: "settings",
|
|
1149
|
+
title: text("Settings"),
|
|
1150
|
+
sections: [
|
|
1151
|
+
{
|
|
1152
|
+
layout: BridgeLayout.Form,
|
|
1153
|
+
id: "server",
|
|
1154
|
+
target: BridgeFormTarget.Variables,
|
|
1155
|
+
fields: [
|
|
1156
|
+
field("SERVER_NAME"),
|
|
1157
|
+
field("MOTD"),
|
|
1158
|
+
],
|
|
1159
|
+
},
|
|
1160
|
+
],
|
|
1161
|
+
},
|
|
1162
|
+
],
|
|
1163
|
+
},
|
|
1164
|
+
});
|
|
1165
|
+
|
|
1166
|
+
const driverStep = (id: string) => ({
|
|
1167
|
+
kind: BridgeSetupStepKind.Driver as const,
|
|
1168
|
+
id,
|
|
1169
|
+
title: text(id),
|
|
1170
|
+
});
|
|
1171
|
+
|
|
1172
|
+
const setupWith = (steps: BridgeSetupStep[]): BridgeSetupModule => ({
|
|
1173
|
+
kind: BridgeKind.Setup,
|
|
1174
|
+
steps,
|
|
1175
|
+
});
|
|
1176
|
+
|
|
1177
|
+
describe("the setup singleton", () => {
|
|
1178
|
+
test("takes a driver step with both locales", () => {
|
|
1179
|
+
expect(() =>
|
|
1180
|
+
validateDriver(
|
|
1181
|
+
driverWithSetup(
|
|
1182
|
+
setupWith([
|
|
1183
|
+
driverStep("sign-in"),
|
|
1184
|
+
]),
|
|
1185
|
+
),
|
|
1186
|
+
),
|
|
1187
|
+
).not.toThrow();
|
|
1188
|
+
});
|
|
1189
|
+
|
|
1190
|
+
test("rejects a setup that declares no steps", () => {
|
|
1191
|
+
expect(() => validateDriver(driverWithSetup(setupWith([])))).toThrow("the setup module declares no steps");
|
|
1192
|
+
});
|
|
1193
|
+
|
|
1194
|
+
test("rejects more steps than the platform takes", () => {
|
|
1195
|
+
expect(() =>
|
|
1196
|
+
validateDriver(
|
|
1197
|
+
driverWithSetup(
|
|
1198
|
+
setupWith(
|
|
1199
|
+
Array.from({
|
|
1200
|
+
length: 9,
|
|
1201
|
+
}).map((_entry, index) => driverStep(`step-${index}`)),
|
|
1202
|
+
),
|
|
1203
|
+
),
|
|
1204
|
+
),
|
|
1205
|
+
).toThrow("the setup module declares 9 steps — the platform takes at most 8");
|
|
1206
|
+
});
|
|
1207
|
+
|
|
1208
|
+
test("rejects a step id that is not kebab-case", () => {
|
|
1209
|
+
expect(() =>
|
|
1210
|
+
validateDriver(
|
|
1211
|
+
driverWithSetup(
|
|
1212
|
+
setupWith([
|
|
1213
|
+
driverStep("signIn"),
|
|
1214
|
+
]),
|
|
1215
|
+
),
|
|
1216
|
+
),
|
|
1217
|
+
).toThrow('the setup module step id "signIn" must be kebab-case');
|
|
1218
|
+
});
|
|
1219
|
+
|
|
1220
|
+
test("rejects the same step id twice", () => {
|
|
1221
|
+
expect(() =>
|
|
1222
|
+
validateDriver(
|
|
1223
|
+
driverWithSetup(
|
|
1224
|
+
setupWith([
|
|
1225
|
+
driverStep("sign-in"),
|
|
1226
|
+
driverStep("sign-in"),
|
|
1227
|
+
]),
|
|
1228
|
+
),
|
|
1229
|
+
),
|
|
1230
|
+
).toThrow('the setup module declares step "sign-in" more than once');
|
|
1231
|
+
});
|
|
1232
|
+
|
|
1233
|
+
test("rejects a title that is missing a locale", () => {
|
|
1234
|
+
expect(() =>
|
|
1235
|
+
validateDriver(
|
|
1236
|
+
driverWithSetup(
|
|
1237
|
+
setupWith([
|
|
1238
|
+
{
|
|
1239
|
+
kind: BridgeSetupStepKind.Driver,
|
|
1240
|
+
id: "sign-in",
|
|
1241
|
+
title: {
|
|
1242
|
+
ar: "",
|
|
1243
|
+
en: "Sign in",
|
|
1244
|
+
},
|
|
1245
|
+
},
|
|
1246
|
+
]),
|
|
1247
|
+
),
|
|
1248
|
+
),
|
|
1249
|
+
).toThrow('the setup module step "sign-in" title needs both arabic and english text');
|
|
1250
|
+
});
|
|
1251
|
+
|
|
1252
|
+
test("takes a form step that names an existing form section and its own fields", () => {
|
|
1253
|
+
expect(() =>
|
|
1254
|
+
validateDriver(
|
|
1255
|
+
driverWithSetup(
|
|
1256
|
+
setupWith([
|
|
1257
|
+
{
|
|
1258
|
+
kind: BridgeSetupStepKind.Form,
|
|
1259
|
+
id: "name",
|
|
1260
|
+
title: text("Name"),
|
|
1261
|
+
tab: "settings",
|
|
1262
|
+
section: "server",
|
|
1263
|
+
fields: [
|
|
1264
|
+
"SERVER_NAME",
|
|
1265
|
+
],
|
|
1266
|
+
},
|
|
1267
|
+
]),
|
|
1268
|
+
),
|
|
1269
|
+
),
|
|
1270
|
+
).not.toThrow();
|
|
1271
|
+
});
|
|
1272
|
+
|
|
1273
|
+
test("rejects a form step that names a section the panel does not declare", () => {
|
|
1274
|
+
expect(() =>
|
|
1275
|
+
validateDriver(
|
|
1276
|
+
driverWithSetup(
|
|
1277
|
+
setupWith([
|
|
1278
|
+
{
|
|
1279
|
+
kind: BridgeSetupStepKind.Form,
|
|
1280
|
+
id: "name",
|
|
1281
|
+
title: text("Name"),
|
|
1282
|
+
tab: "settings",
|
|
1283
|
+
section: "missing",
|
|
1284
|
+
},
|
|
1285
|
+
]),
|
|
1286
|
+
),
|
|
1287
|
+
),
|
|
1288
|
+
).toThrow(
|
|
1289
|
+
'the setup module step "name" names tab "settings" section "missing", which is not a form section of this panel',
|
|
1290
|
+
);
|
|
1291
|
+
});
|
|
1292
|
+
|
|
1293
|
+
test("rejects a form step that names a field the section does not declare", () => {
|
|
1294
|
+
expect(() =>
|
|
1295
|
+
validateDriver(
|
|
1296
|
+
driverWithSetup(
|
|
1297
|
+
setupWith([
|
|
1298
|
+
{
|
|
1299
|
+
kind: BridgeSetupStepKind.Form,
|
|
1300
|
+
id: "name",
|
|
1301
|
+
title: text("Name"),
|
|
1302
|
+
tab: "settings",
|
|
1303
|
+
section: "server",
|
|
1304
|
+
fields: [
|
|
1305
|
+
"PASSWORD",
|
|
1306
|
+
],
|
|
1307
|
+
},
|
|
1308
|
+
]),
|
|
1309
|
+
),
|
|
1310
|
+
),
|
|
1311
|
+
).toThrow('the setup module step "name" names field "PASSWORD", which section "server" does not declare');
|
|
1312
|
+
});
|
|
1313
|
+
|
|
1314
|
+
test("takes an open step that names an existing panel tab", () => {
|
|
1315
|
+
expect(() =>
|
|
1316
|
+
validateDriver(
|
|
1317
|
+
driverWithSetup(
|
|
1318
|
+
setupWith([
|
|
1319
|
+
{
|
|
1320
|
+
kind: BridgeSetupStepKind.Open,
|
|
1321
|
+
id: "tune",
|
|
1322
|
+
title: text("Tune"),
|
|
1323
|
+
target: {
|
|
1324
|
+
tab: GuideOpenTab.Panel,
|
|
1325
|
+
tabId: "settings",
|
|
1326
|
+
},
|
|
1327
|
+
},
|
|
1328
|
+
]),
|
|
1329
|
+
),
|
|
1330
|
+
),
|
|
1331
|
+
).not.toThrow();
|
|
1332
|
+
});
|
|
1333
|
+
|
|
1334
|
+
test("rejects an open step that names a panel tab the panel does not declare", () => {
|
|
1335
|
+
expect(() =>
|
|
1336
|
+
validateDriver(
|
|
1337
|
+
driverWithSetup(
|
|
1338
|
+
setupWith([
|
|
1339
|
+
{
|
|
1340
|
+
kind: BridgeSetupStepKind.Open,
|
|
1341
|
+
id: "tune",
|
|
1342
|
+
title: text("Tune"),
|
|
1343
|
+
target: {
|
|
1344
|
+
tab: GuideOpenTab.Panel,
|
|
1345
|
+
tabId: "world",
|
|
1346
|
+
},
|
|
1347
|
+
},
|
|
1348
|
+
]),
|
|
1349
|
+
),
|
|
1350
|
+
),
|
|
1351
|
+
).toThrow('the setup module step "tune" opens panel tab "world", which the panel does not declare');
|
|
1352
|
+
});
|
|
1353
|
+
|
|
1354
|
+
test("takes an open step that leaves the panel altogether", () => {
|
|
1355
|
+
expect(() =>
|
|
1356
|
+
validateDriver(
|
|
1357
|
+
driverWithSetup(
|
|
1358
|
+
setupWith([
|
|
1359
|
+
{
|
|
1360
|
+
kind: BridgeSetupStepKind.Open,
|
|
1361
|
+
id: "invite",
|
|
1362
|
+
title: text("Invite"),
|
|
1363
|
+
target: {
|
|
1364
|
+
tab: GuideOpenTab.Access,
|
|
1365
|
+
},
|
|
1366
|
+
},
|
|
1367
|
+
]),
|
|
1368
|
+
),
|
|
1369
|
+
),
|
|
1370
|
+
).not.toThrow();
|
|
1371
|
+
});
|
|
1372
|
+
|
|
1373
|
+
test("reserves the setup module id for the singleton", () => {
|
|
1374
|
+
expect(() =>
|
|
1375
|
+
validateDriver({
|
|
1376
|
+
install: {
|
|
1377
|
+
kind: BridgeKind.Install,
|
|
1378
|
+
run: async () => {
|
|
1379
|
+
return;
|
|
1380
|
+
},
|
|
1381
|
+
},
|
|
1382
|
+
lifecycle: {
|
|
1383
|
+
kind: BridgeKind.Lifecycle,
|
|
1384
|
+
command: async () => [
|
|
1385
|
+
"run",
|
|
1386
|
+
],
|
|
1387
|
+
},
|
|
1388
|
+
modules: {
|
|
1389
|
+
setup: {
|
|
1390
|
+
kind: BridgeKind.Collection,
|
|
1391
|
+
list: async () => [],
|
|
1392
|
+
},
|
|
1393
|
+
},
|
|
1394
|
+
}),
|
|
1395
|
+
).toThrow('module id "setup" is reserved');
|
|
1396
|
+
});
|
|
1397
|
+
});
|
|
@@ -1,4 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
BRIDGE_ID_PATTERN,
|
|
3
|
+
BRIDGE_MODULE_ID_PATTERN,
|
|
4
|
+
BridgeKind,
|
|
5
|
+
BridgeSetupStepKind,
|
|
6
|
+
type LocalizedText,
|
|
7
|
+
} from "../core";
|
|
8
|
+
import { GuideOpenTab } from "../guides";
|
|
2
9
|
import type { ManifestPort } from "../manifest";
|
|
3
10
|
import type { BridgeDriver, BridgePanelModule } from "../modules";
|
|
4
11
|
import {
|
|
@@ -28,6 +35,7 @@ import {
|
|
|
28
35
|
BRIDGE_PLACEHOLDER_LENGTH_LIMIT,
|
|
29
36
|
BRIDGE_ROW_ACTIONS_LIMIT,
|
|
30
37
|
BRIDGE_SECTION_ACTIONS_LIMIT,
|
|
38
|
+
BRIDGE_SETUP_STEPS_LIMIT,
|
|
31
39
|
BRIDGE_STAGING_LENGTH_LIMIT,
|
|
32
40
|
BRIDGE_TAB_SECTIONS_LIMIT,
|
|
33
41
|
BRIDGE_TABLE_COLUMNS_LIMIT,
|
|
@@ -47,6 +55,7 @@ const RESERVED_MODULE_IDS = new Set([
|
|
|
47
55
|
"lifecycle",
|
|
48
56
|
"pending",
|
|
49
57
|
"query",
|
|
58
|
+
"setup",
|
|
50
59
|
]);
|
|
51
60
|
|
|
52
61
|
export class BridgeDriverError extends Error {
|
|
@@ -437,6 +446,77 @@ const validatePending = (driver: BridgeDriver) => {
|
|
|
437
446
|
}
|
|
438
447
|
};
|
|
439
448
|
|
|
449
|
+
const formSectionOf = (driver: BridgeDriver, tabId: string, sectionId: string) => {
|
|
450
|
+
const tab = (driver.panel?.tabs ?? []).find((entry) => entry.id === tabId);
|
|
451
|
+
const section = (tab?.sections ?? []).find((entry) => entry.id === sectionId);
|
|
452
|
+
|
|
453
|
+
if (!section || section.layout !== BridgeLayout.Form) {
|
|
454
|
+
return null;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
return section;
|
|
458
|
+
};
|
|
459
|
+
|
|
460
|
+
const validateSetup = (driver: BridgeDriver) => {
|
|
461
|
+
const setup = driver.setup;
|
|
462
|
+
|
|
463
|
+
if (!setup) {
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
const where = "the setup module";
|
|
468
|
+
|
|
469
|
+
if (setup.kind !== BridgeKind.Setup) {
|
|
470
|
+
fail(`${where} declares kind "${setup.kind}", which is not "${BridgeKind.Setup}"`);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
if (setup.steps.length === 0) {
|
|
474
|
+
fail(`${where} declares no steps`);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
countIn(setup.steps.length, BRIDGE_SETUP_STEPS_LIMIT, "steps", where);
|
|
478
|
+
|
|
479
|
+
const ids = new Set<string>();
|
|
480
|
+
|
|
481
|
+
for (const step of setup.steps) {
|
|
482
|
+
if (ids.has(step.id)) {
|
|
483
|
+
fail(`${where} declares step "${step.id}" more than once`);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
ids.add(step.id);
|
|
487
|
+
|
|
488
|
+
const at = `${where} step "${step.id}"`;
|
|
489
|
+
|
|
490
|
+
validateId(step.id, `${where} step`);
|
|
491
|
+
validateText(step.title, "title", at);
|
|
492
|
+
validateText(step.help, "help", at);
|
|
493
|
+
|
|
494
|
+
if (step.kind === BridgeSetupStepKind.Form) {
|
|
495
|
+
const section = formSectionOf(driver, step.tab, step.section);
|
|
496
|
+
|
|
497
|
+
if (!section) {
|
|
498
|
+
fail(`${at} names tab "${step.tab}" section "${step.section}", which is not a form section of this panel`);
|
|
499
|
+
|
|
500
|
+
continue;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
for (const key of step.fields ?? []) {
|
|
504
|
+
if (!section.fields.some((field) => field.key === key)) {
|
|
505
|
+
fail(`${at} names field "${key}", which section "${step.section}" does not declare`);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
if (step.kind === BridgeSetupStepKind.Open) {
|
|
511
|
+
const target = step.target;
|
|
512
|
+
|
|
513
|
+
if (target.tab === GuideOpenTab.Panel && !(driver.panel?.tabs ?? []).some((tab) => tab.id === target.tabId)) {
|
|
514
|
+
fail(`${at} opens panel tab "${target.tabId}", which the panel does not declare`);
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
};
|
|
519
|
+
|
|
440
520
|
const validateTab = (modules: Record<string, BridgePanelModule>, tab: BridgeTab) => {
|
|
441
521
|
const sections = new Set<string>();
|
|
442
522
|
|
|
@@ -474,6 +554,7 @@ export const validateDriver = (driver: BridgeDriver) => {
|
|
|
474
554
|
countIn(emitted.size, BRIDGE_EVENTS_LIMIT, "event names", "the driver");
|
|
475
555
|
|
|
476
556
|
validatePending(driver);
|
|
557
|
+
validateSetup(driver);
|
|
477
558
|
|
|
478
559
|
for (const [id, module] of Object.entries(modules)) {
|
|
479
560
|
validateModuleId(id, "module");
|