@serverkgg/bridge 0.0.10 → 0.0.12

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.
@@ -3,7 +3,8 @@ import { join, resolve } from "node:path";
3
3
  import { pathToFileURL } from "node:url";
4
4
  import { parseArgs } from "node:util";
5
5
  import { z } from "zod";
6
- import { type GuidePack, readGuidePack, readGuidePackage } from "../guides";
6
+ import { type ArticlePack, compileArticles } from "../blog";
7
+ import { GuideOpenTab, type GuideOpenTarget, type GuidePack, readGuidePack, readGuidePackage } from "../guides";
7
8
  import {
8
9
  declaredFiles,
9
10
  type GuidePackage,
@@ -13,8 +14,8 @@ import {
13
14
  readAssets,
14
15
  readPackageDocument,
15
16
  } from "../manifest";
16
- import type { BridgeDriver } from "../modules";
17
- import { isBridgeEventName } from "../protocol";
17
+ import type { BridgeDriver, BridgeSetupModule } from "../modules";
18
+ import { type BridgeSetupStep, BridgeSetupStepKind, isBridgeEventName } from "../protocol";
18
19
  import { validateDriver, validatePortActivation } from "../runtime/bridgeDriver";
19
20
  import { directorySource } from "./directorySource";
20
21
 
@@ -131,6 +132,10 @@ const reportGuides = (pack: GuidePack | null) => {
131
132
  }
132
133
  };
133
134
 
135
+ const reportArticles = (pack: ArticlePack) => {
136
+ console.log(` articles: ${pack.drafts.length} article(s), ${pack.images.length} image(s)`);
137
+ };
138
+
134
139
  const validateGuidePackage = (source: PackageSource, document: GuidePackage) => {
135
140
  const guideFiles = document.guides.flatMap((guide) => [
136
141
  guide.bodyPath.ar,
@@ -144,9 +149,11 @@ const validateGuidePackage = (source: PackageSource, document: GuidePackage) =>
144
149
  }
145
150
 
146
151
  const pack = readGuidePackage(source, document);
152
+ const articles = compileArticles(source);
147
153
 
148
154
  console.log(`${document.metadata.id} is valid`);
149
155
  reportGuides(pack);
156
+ reportArticles(articles);
150
157
  };
151
158
 
152
159
  const ACTION_TARGET_BUDGET = 80;
@@ -186,6 +193,49 @@ const reportActionTargets = (driver: BridgeDriver) => {
186
193
  }
187
194
  };
188
195
 
196
+ const openTargetOf = (target: GuideOpenTarget) => {
197
+ if (target.tab === GuideOpenTab.Panel) {
198
+ return `panel tab "${target.tabId}"`;
199
+ }
200
+
201
+ if (target.tab === GuideOpenTab.Files) {
202
+ return target.path ? `files at "${target.path}"` : "files";
203
+ }
204
+
205
+ return target.tab;
206
+ };
207
+
208
+ const setupTargetOf = (step: BridgeSetupStep) => {
209
+ if (step.kind === BridgeSetupStepKind.Form) {
210
+ const fields = step.fields ?? [];
211
+
212
+ return `→ ${step.tab}/${step.section}${fields.length === 0 ? "" : ` [${fields.join(", ")}]`}`;
213
+ }
214
+
215
+ if (step.kind === BridgeSetupStepKind.Open) {
216
+ return `→ ${openTargetOf(step.target)}`;
217
+ }
218
+
219
+ return step.requiresRunning ? "→ driver (needs the game running)" : "→ driver";
220
+ };
221
+
222
+ export const setupReportLines = (setup: BridgeSetupModule | undefined): string[] => {
223
+ if (!setup) {
224
+ return [
225
+ " setup: none",
226
+ ];
227
+ }
228
+
229
+ return [
230
+ ` setup: ${setup.steps.length} step(s)`,
231
+ ...setup.steps.map((step) => {
232
+ const target = setupTargetOf(step);
233
+
234
+ return ` ${step.id} — ${step.kind}, ${step.required === false ? "optional" : "required"}${target === "" ? "" : ` ${target}`}`;
235
+ }),
236
+ ];
237
+ };
238
+
189
239
  const validateGamePackage = async (source: PackageSource, manifest: Manifest, dir: string, driverArg?: string) => {
190
240
  const assets = readAssets(source, manifest);
191
241
  const guideFiles = manifest.guides.flatMap((guide) => [
@@ -230,7 +280,10 @@ const validateGamePackage = async (source: PackageSource, manifest: Manifest, di
230
280
  ` driver: ${Object.keys(driver.modules ?? {}).length} panel module(s), ${driver.panel?.tabs.length ?? 0} tab(s)`,
231
281
  );
232
282
  console.log(` pending: ${driver.pending ? `declared (${driver.pending.extensions.length} extension(s))` : "none"}`);
233
- console.log(` setup: ${driver.setup ? `${driver.setup.steps.length} step(s)` : "none"}`);
283
+ for (const line of setupReportLines(driver.setup)) {
284
+ console.log(line);
285
+ }
286
+
234
287
  reportEvents(driver);
235
288
  reportActionTargets(driver);
236
289
  };
@@ -91,8 +91,28 @@ export interface BridgeA2sInfo {
91
91
  latencyMs: number;
92
92
  }
93
93
 
94
+ export interface BridgeMinecraftPingStatus {
95
+ players: BridgePingPlayers;
96
+ version: string | null;
97
+ description: string | null;
98
+ latencyMs: number;
99
+ }
100
+
101
+ export interface BridgeRaknetPingStatus {
102
+ players: BridgePingPlayers;
103
+ edition: string | null;
104
+ motd: string | null;
105
+ subMotd: string | null;
106
+ version: string | null;
107
+ protocol: number | null;
108
+ gamemode: string | null;
109
+ latencyMs: number;
110
+ }
111
+
94
112
  export interface BridgeProbes {
95
113
  a2s(port: number): Promise<BridgeA2sInfo>;
114
+ minecraftPing(port: number): Promise<BridgeMinecraftPingStatus>;
115
+ raknetPing(port: number): Promise<BridgeRaknetPingStatus>;
96
116
  }
97
117
 
98
118
  export interface BridgeCommandOptions {
@@ -52,49 +52,65 @@ const compileTags = (guideTags: GuideTag[], tags: string[]): CompiledGuideTag[]
52
52
  });
53
53
  };
54
54
 
55
- export const compileGuides = (source: PackageSource, guideTags: GuideTag[], entries: GuideEntry[]): GuidePack => {
56
- const drafts: DraftGuide[] = [];
55
+ export interface MarkdownBodyReader {
56
+ files: string[];
57
+ commands: string[];
58
+ images: Map<string, GuideImageAsset>;
59
+ read(path: string, text?: string): ParsedBlock[];
60
+ }
61
+
62
+ export const markdownBodyReader = (source: PackageSource): MarkdownBodyReader => {
57
63
  const files: string[] = [];
58
64
  const commands: string[] = [];
59
- const imageByPath = new Map<string, GuideImageAsset>();
65
+ const images = new Map<string, GuideImageAsset>();
60
66
 
61
- const readBody = (path: string) => {
62
- const body = source.read(path);
67
+ return {
68
+ files,
69
+ commands,
70
+ images,
71
+ read(path, text) {
72
+ const body = source.read(path);
63
73
 
64
- if (!body) {
65
- throw new Error(`missing guide body ${path}`);
66
- }
74
+ if (!body) {
75
+ throw new Error(`missing markdown body ${path}`);
76
+ }
67
77
 
68
- files.push(path);
78
+ files.push(path);
69
79
 
70
- const parsed = parseGuideMarkdown(path, new TextDecoder().decode(body));
80
+ const parsed = parseGuideMarkdown(path, text ?? new TextDecoder().decode(body));
71
81
 
72
- commands.push(...parsed.commands);
82
+ commands.push(...parsed.commands);
73
83
 
74
- for (const imagePath of parsed.images) {
75
- if (imageByPath.has(imagePath)) {
76
- continue;
77
- }
84
+ for (const imagePath of parsed.images) {
85
+ if (images.has(imagePath)) {
86
+ continue;
87
+ }
78
88
 
79
- const imageBytes = source.read(imagePath);
89
+ const imageBytes = source.read(imagePath);
80
90
 
81
- if (!imageBytes) {
82
- throw new Error(`missing guide image ${imagePath}`);
83
- }
91
+ if (!imageBytes) {
92
+ throw new Error(`missing guide image ${imagePath}`);
93
+ }
84
94
 
85
- const verified = verifyAsset(imagePath, imageBytes);
95
+ const verified = verifyAsset(imagePath, imageBytes);
86
96
 
87
- imageByPath.set(imagePath, {
88
- path: imagePath,
89
- bytes: imageBytes,
90
- mediaType: verified.mediaType,
91
- width: verified.dimensions?.width ?? null,
92
- height: verified.dimensions?.height ?? null,
93
- });
94
- }
97
+ images.set(imagePath, {
98
+ path: imagePath,
99
+ bytes: imageBytes,
100
+ mediaType: verified.mediaType,
101
+ width: verified.dimensions?.width ?? null,
102
+ height: verified.dimensions?.height ?? null,
103
+ });
104
+ }
95
105
 
96
- return parsed.blocks;
106
+ return parsed.blocks;
107
+ },
97
108
  };
109
+ };
110
+
111
+ export const compileGuides = (source: PackageSource, guideTags: GuideTag[], entries: GuideEntry[]): GuidePack => {
112
+ const drafts: DraftGuide[] = [];
113
+ const reader = markdownBodyReader(source);
98
114
 
99
115
  for (const entry of entries) {
100
116
  drafts.push({
@@ -105,8 +121,8 @@ export const compileGuides = (source: PackageSource, guideTags: GuideTag[], entr
105
121
  surfaces: entry.surfaces,
106
122
  videoId: entry.video ?? null,
107
123
  body: {
108
- ar: readBody(entry.bodyPath.ar),
109
- en: readBody(entry.bodyPath.en),
124
+ ar: reader.read(entry.bodyPath.ar),
125
+ en: reader.read(entry.bodyPath.en),
110
126
  },
111
127
  });
112
128
  }
@@ -114,13 +130,13 @@ export const compileGuides = (source: PackageSource, guideTags: GuideTag[], entr
114
130
  return {
115
131
  drafts,
116
132
  files: [
117
- ...files,
118
- ...imageByPath.keys(),
133
+ ...reader.files,
134
+ ...reader.images.keys(),
119
135
  ],
120
136
  images: [
121
- ...imageByPath.values(),
137
+ ...reader.images.values(),
122
138
  ],
123
- commands,
139
+ commands: reader.commands,
124
140
  };
125
141
  };
126
142
 
@@ -142,10 +158,7 @@ export interface ResolvedGuideImage {
142
158
  height: number | null;
143
159
  }
144
160
 
145
- export const resolveGuideImages = (
146
- drafts: DraftGuide[],
147
- resolved: Map<string, ResolvedGuideImage>,
148
- ): CompiledGuide[] => {
161
+ export const resolveGuideBlocks = (blocks: ParsedBlock[], resolved: Map<string, ResolvedGuideImage>): GuideBlock[] => {
149
162
  const resolveLeaf = (block: ParsedLeafBlock): GuideLeafBlock => {
150
163
  if (block.type !== "image") {
151
164
  return block;
@@ -166,27 +179,30 @@ export const resolveGuideImages = (
166
179
  };
167
180
  };
168
181
 
169
- const resolveBlocks = (blocks: ParsedBlock[]): GuideBlock[] => {
170
- return blocks.map((block) => {
171
- if (block.type !== "conditional") {
172
- return resolveLeaf(block);
173
- }
182
+ return blocks.map((block) => {
183
+ if (block.type !== "conditional") {
184
+ return resolveLeaf(block);
185
+ }
174
186
 
175
- return {
176
- type: "conditional",
177
- key: block.key,
178
- when: block.when.map(resolveLeaf),
179
- otherwise: block.otherwise.map(resolveLeaf),
180
- };
181
- });
182
- };
187
+ return {
188
+ type: "conditional",
189
+ key: block.key,
190
+ when: block.when.map(resolveLeaf),
191
+ otherwise: block.otherwise.map(resolveLeaf),
192
+ };
193
+ });
194
+ };
183
195
 
196
+ export const resolveGuideImages = (
197
+ drafts: DraftGuide[],
198
+ resolved: Map<string, ResolvedGuideImage>,
199
+ ): CompiledGuide[] => {
184
200
  return drafts.map((draft) => {
185
201
  return compiledGuideSchema.parse({
186
202
  ...draft,
187
203
  body: {
188
- ar: resolveBlocks(draft.body.ar),
189
- en: resolveBlocks(draft.body.en),
204
+ ar: resolveGuideBlocks(draft.body.ar, resolved),
205
+ en: resolveGuideBlocks(draft.body.en, resolved),
190
206
  },
191
207
  });
192
208
  });
@@ -16,6 +16,7 @@ export enum GuideSurface {
16
16
  Files = "files",
17
17
  Backups = "backups",
18
18
  Panel = "panel",
19
+ Setup = "setup",
19
20
  }
20
21
 
21
22
  export const guideTagSchema = z.object({
@@ -10,6 +10,26 @@ const text = (value: string) => ({
10
10
  en: value,
11
11
  });
12
12
 
13
+ const guide = (id: string, extra: Record<string, unknown> = {}) => ({
14
+ id,
15
+ title: text(id),
16
+ summary: text(id),
17
+ bodyPath: {
18
+ ar: `guides/${id}/ar.md`,
19
+ en: `guides/${id}/en.md`,
20
+ },
21
+ ...extra,
22
+ });
23
+
24
+ const guidePackage = (guides: unknown[]) => ({
25
+ schemaVersion: MANIFEST_SCHEMA_VERSION,
26
+ kind: "GuidePackage",
27
+ metadata: {
28
+ id: "platform",
29
+ },
30
+ guides,
31
+ });
32
+
13
33
  const base = () => ({
14
34
  schemaVersion: MANIFEST_SCHEMA_VERSION,
15
35
  kind: "GamePackage",
@@ -586,6 +606,19 @@ describe("packageSchema", () => {
586
606
  expect(result.success).toBe(false);
587
607
  expect(result.error && z.prettifyError(result.error)).toContain('tag "start" is not declared in guideTags');
588
608
  });
609
+
610
+ test("carries up to 64 guides", () => {
611
+ const many = (count: number) =>
612
+ Array.from(
613
+ {
614
+ length: count,
615
+ },
616
+ (_, index) => guide(`guide-${index}`),
617
+ );
618
+
619
+ expect(guidePackageSchema.safeParse(guidePackage(many(64))).success).toBe(true);
620
+ expect(guidePackageSchema.safeParse(guidePackage(many(65))).success).toBe(false);
621
+ });
589
622
  });
590
623
 
591
624
  describe("presence", () => {
@@ -1,6 +1,7 @@
1
1
  export enum BridgeControl {
2
2
  Boolean = "boolean",
3
3
  Number = "number",
4
+ Secret = "secret",
4
5
  Select = "select",
5
6
  Slider = "slider",
6
7
  Text = "text",
@@ -11,6 +11,7 @@ export enum BridgeDetailFormat {
11
11
  Bytes = "bytes",
12
12
  Date = "date",
13
13
  Number = "number",
14
+ Secret = "secret",
14
15
  Text = "text",
15
16
  }
16
17
 
@@ -32,6 +32,8 @@ export interface BridgeField {
32
32
  max?: number;
33
33
  step?: number;
34
34
  maxLength?: number;
35
+ pattern?: string;
36
+ patternHint?: LocalizedText;
35
37
  options?: BridgeOption[] | BridgeOptionsRef;
36
38
  visibleWhen?: BridgeVisibleWhen;
37
39
  }
@@ -1 +1,10 @@
1
+ export type {
2
+ BridgeA2sInfo,
3
+ BridgeMinecraftPingStatus,
4
+ BridgePingPlayers,
5
+ BridgeRaknetPingStatus,
6
+ } from "../context";
7
+ export { BridgeProbeCode, BridgeProbeError } from "../core";
1
8
  export * from "./a2s";
9
+ export * from "./minecraftPing";
10
+ export * from "./raknetPing";