@serverkgg/bridge 0.0.11 → 0.0.13

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.
Files changed (84) hide show
  1. package/package.json +11 -1
  2. package/src/blog/articleFrontmatter.test.ts +95 -0
  3. package/src/blog/articleFrontmatter.ts +99 -0
  4. package/src/blog/compile.test.ts +148 -0
  5. package/src/blog/compile.ts +172 -0
  6. package/src/blog/compiled.ts +22 -0
  7. package/src/blog/index.ts +3 -0
  8. package/src/catalogs/curseforge.test.ts +580 -0
  9. package/src/catalogs/curseforge.ts +461 -0
  10. package/src/catalogs/index.ts +2 -0
  11. package/src/catalogs/thunderstore.test.ts +94 -0
  12. package/src/catalogs/thunderstore.ts +239 -0
  13. package/src/cli/validate.ts +7 -0
  14. package/src/codecs/index.ts +3 -0
  15. package/src/codecs/ini.test.ts +152 -0
  16. package/src/codecs/ini.ts +158 -0
  17. package/src/codecs/sourceCfg.test.ts +100 -0
  18. package/src/codecs/sourceCfg.ts +111 -0
  19. package/src/codecs/xmlProperties.test.ts +104 -0
  20. package/src/codecs/xmlProperties.ts +133 -0
  21. package/src/context/bridgeContext.ts +40 -0
  22. package/src/core/bridgeRconError.ts +19 -0
  23. package/src/core/index.ts +1 -0
  24. package/src/guides/compile.ts +70 -54
  25. package/src/install/index.ts +1 -0
  26. package/src/install/installStamp.test.ts +125 -0
  27. package/src/install/installStamp.ts +40 -0
  28. package/src/main.ts +1 -0
  29. package/src/manifest/index.ts +1 -0
  30. package/src/manifest/manifest.test.ts +460 -2
  31. package/src/manifest/manifest.ts +141 -13
  32. package/src/manifest/primitives.ts +6 -0
  33. package/src/manifest/runtime.ts +89 -0
  34. package/src/net/index.ts +2 -0
  35. package/src/net/loopback.test.ts +152 -0
  36. package/src/net/loopback.ts +109 -0
  37. package/src/net/loopbackMethod.ts +7 -0
  38. package/src/presence/index.ts +1 -0
  39. package/src/presence/rosterSync.test.ts +179 -0
  40. package/src/presence/rosterSync.ts +57 -0
  41. package/src/probes/index.ts +9 -0
  42. package/src/probes/minecraftPing.test.ts +344 -0
  43. package/src/probes/minecraftPing.ts +283 -0
  44. package/src/probes/raknetPing.test.ts +197 -0
  45. package/src/probes/raknetPing.ts +228 -0
  46. package/src/protocol/bridgeEvents.ts +4 -0
  47. package/src/rcon/index.ts +3 -0
  48. package/src/rcon/sourceRcon.test.ts +308 -0
  49. package/src/rcon/sourceRcon.ts +276 -0
  50. package/src/rcon/sourceRconFrame.test.ts +152 -0
  51. package/src/rcon/sourceRconFrame.ts +99 -0
  52. package/src/runtime/bridgeCodec.ts +45 -2
  53. package/src/runtime/bridgeContext.ts +13 -6
  54. package/src/runtime/bridgeDriver.test.ts +1 -0
  55. package/src/runtime/bridgeLiveConfig.test.ts +149 -0
  56. package/src/runtime/bridgeLiveConfig.ts +63 -0
  57. package/src/runtime/bridgeProbe.ts +13 -1
  58. package/src/runtime/bridgeRcon.ts +15 -0
  59. package/src/runtime/bridgeRegistry.ts +1 -0
  60. package/src/steam/index.ts +7 -0
  61. package/src/steam/steamApp.test.ts +78 -0
  62. package/src/steam/steamApp.ts +35 -0
  63. package/src/steam/steamcmd.test.ts +157 -0
  64. package/src/steam/steamcmd.ts +273 -0
  65. package/src/steam/steamcmdLogin.ts +34 -0
  66. package/src/steam/steamcmdNarrator.test.ts +90 -0
  67. package/src/steam/steamcmdNarrator.ts +43 -0
  68. package/src/steam/steamcmdPlatform.ts +4 -0
  69. package/src/steam/webApi.ts +70 -0
  70. package/src/steam/workshop.test.ts +45 -0
  71. package/src/steam/workshop.ts +148 -0
  72. package/src/utils/execDetail.ts +16 -0
  73. package/src/utils/index.ts +2 -0
  74. package/src/utils/token.test.ts +50 -0
  75. package/src/utils/token.ts +35 -0
  76. package/src/wine/index.ts +3 -0
  77. package/src/wine/protonCommand.test.ts +67 -0
  78. package/src/wine/protonCommand.ts +49 -0
  79. package/src/wine/wineCommand.test.ts +92 -0
  80. package/src/wine/wineCommand.ts +54 -0
  81. package/src/wine/winePrefix.test.ts +111 -0
  82. package/src/wine/winePrefix.ts +88 -0
  83. package/src/runtime/bridgeSecrets.test.ts +0 -121
  84. package/src/runtime/bridgeSecrets.ts +0 -43
@@ -0,0 +1,111 @@
1
+ import type { BridgeValue, BridgeValues } from "../core";
2
+
3
+ const ENTRY = /^(?<key>[A-Za-z_][A-Za-z0-9_.]*)(?:[ \t]+(?<value>.*))?$/;
4
+
5
+ const NUMERIC = /^-?(?:0|[1-9]\d*)(?:\.\d+)?$/;
6
+
7
+ const NEWLINES = /[\r\n]+/g;
8
+
9
+ export interface SourceCfgEntry {
10
+ key: string;
11
+ value: string;
12
+ }
13
+
14
+ export const readSourceCfgEntry = (line: string): SourceCfgEntry | null => {
15
+ const trimmed = line.trim();
16
+
17
+ if (trimmed.length === 0 || trimmed.startsWith("//") || trimmed.startsWith("#")) {
18
+ return null;
19
+ }
20
+
21
+ const match = trimmed.match(ENTRY);
22
+
23
+ if (!match?.groups?.key) {
24
+ return null;
25
+ }
26
+
27
+ return {
28
+ key: match.groups.key,
29
+ value: (match.groups.value ?? "").trim(),
30
+ };
31
+ };
32
+
33
+ export const decodeSourceCfgValue = (raw: string): BridgeValue => {
34
+ if (raw.length >= 2 && raw.startsWith('"') && raw.endsWith('"')) {
35
+ return raw.slice(1, -1).replaceAll('\\"', '"').replaceAll("\\\\", "\\");
36
+ }
37
+
38
+ if (raw === "true" || raw === "false") {
39
+ return raw === "true";
40
+ }
41
+
42
+ if (NUMERIC.test(raw)) {
43
+ return Number(raw);
44
+ }
45
+
46
+ return raw;
47
+ };
48
+
49
+ export const encodeSourceCfgValue = (value: BridgeValue): string => {
50
+ if (typeof value === "boolean") {
51
+ return value ? "true" : "false";
52
+ }
53
+
54
+ if (typeof value === "number") {
55
+ return Number.isFinite(value) ? String(value) : '""';
56
+ }
57
+
58
+ const text = (value ?? "").replaceAll("\\", "\\\\").replaceAll('"', '\\"').replaceAll(NEWLINES, " ");
59
+
60
+ return `"${text}"`;
61
+ };
62
+
63
+ export const parseSourceCfg = (contents: string): BridgeValues => {
64
+ const values: BridgeValues = {};
65
+
66
+ for (const line of contents.split("\n")) {
67
+ const entry = readSourceCfgEntry(line);
68
+
69
+ if (entry) {
70
+ values[entry.key] = decodeSourceCfgValue(entry.value);
71
+ }
72
+ }
73
+
74
+ return values;
75
+ };
76
+
77
+ export const mergeSourceCfg = (contents: string, values: BridgeValues): string => {
78
+ const pending = new Map(Object.entries(values));
79
+ const written = new Set<string>();
80
+ const lines: string[] = [];
81
+
82
+ for (const line of contents.split("\n")) {
83
+ const entry = readSourceCfgEntry(line);
84
+ const key = entry?.key ?? "";
85
+
86
+ if (!entry || !pending.has(key)) {
87
+ lines.push(line);
88
+
89
+ continue;
90
+ }
91
+
92
+ if (written.has(key)) {
93
+ continue;
94
+ }
95
+
96
+ written.add(key);
97
+ lines.push(`${key} ${encodeSourceCfgValue(pending.get(key) ?? null)}`);
98
+ }
99
+
100
+ while (lines.length > 0 && (lines.at(-1) ?? "").trim().length === 0) {
101
+ lines.pop();
102
+ }
103
+
104
+ for (const [key, value] of pending) {
105
+ if (!written.has(key)) {
106
+ lines.push(`${key} ${encodeSourceCfgValue(value)}`);
107
+ }
108
+ }
109
+
110
+ return `${lines.join("\n")}\n`;
111
+ };
@@ -0,0 +1,104 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { mergeXmlProperties, parseXmlProperties } from "./xmlProperties";
3
+
4
+ const SAMPLE = [
5
+ '<?xml version="1.0"?>',
6
+ "<ServerSettings>",
7
+ '\t<property name="ServerName" value="serverk"/>',
8
+ "\t<!-- a note -->",
9
+ '\t<property name="ServerPort" value="26900"/>',
10
+ '\t<property value="8" name="ServerMaxPlayerCount"/>',
11
+ "</ServerSettings>",
12
+ "",
13
+ ].join("\n");
14
+
15
+ describe("parseXmlProperties", () => {
16
+ test("reads every property whichever way the attributes are ordered", () => {
17
+ expect(parseXmlProperties(SAMPLE)).toEqual({
18
+ ServerName: "serverk",
19
+ ServerPort: "26900",
20
+ ServerMaxPlayerCount: "8",
21
+ });
22
+ });
23
+
24
+ test("decodes entities", () => {
25
+ expect(parseXmlProperties('<property name="ServerName" value="a &amp; b &lt;c&gt;"/>')).toEqual({
26
+ ServerName: "a & b <c>",
27
+ });
28
+ });
29
+
30
+ test("accepts single quoted attributes and an empty value", () => {
31
+ expect(parseXmlProperties("<property name='ServerPassword' value=''/>")).toEqual({
32
+ ServerPassword: "",
33
+ });
34
+ });
35
+
36
+ test("ignores everything that is not a property", () => {
37
+ expect(parseXmlProperties('<ServerSettings>\n<other name="x" value="1"/>\n</ServerSettings>')).toEqual({});
38
+ });
39
+ });
40
+
41
+ describe("mergeXmlProperties", () => {
42
+ test("rewrites a value in place and keeps order, comments and the header", () => {
43
+ expect(
44
+ mergeXmlProperties(SAMPLE, {
45
+ ServerPort: "26950",
46
+ }),
47
+ ).toBe(
48
+ [
49
+ '<?xml version="1.0"?>',
50
+ "<ServerSettings>",
51
+ '\t<property name="ServerName" value="serverk"/>',
52
+ "\t<!-- a note -->",
53
+ '\t<property name="ServerPort" value="26950"/>',
54
+ '\t<property value="8" name="ServerMaxPlayerCount"/>',
55
+ "</ServerSettings>",
56
+ "",
57
+ ].join("\n"),
58
+ );
59
+ });
60
+
61
+ test("rewrites a property whose value attribute comes first", () => {
62
+ expect(
63
+ mergeXmlProperties(SAMPLE, {
64
+ ServerMaxPlayerCount: "16",
65
+ }),
66
+ ).toContain('<property value="16" name="ServerMaxPlayerCount"/>');
67
+ });
68
+
69
+ test("adds an unknown property before the closing tag with the same indent", () => {
70
+ expect(
71
+ mergeXmlProperties(SAMPLE, {
72
+ ServerPassword: "1234",
73
+ }),
74
+ ).toBe(
75
+ [
76
+ '<?xml version="1.0"?>',
77
+ "<ServerSettings>",
78
+ '\t<property name="ServerName" value="serverk"/>',
79
+ "\t<!-- a note -->",
80
+ '\t<property name="ServerPort" value="26900"/>',
81
+ '\t<property value="8" name="ServerMaxPlayerCount"/>',
82
+ '\t<property name="ServerPassword" value="1234"/>',
83
+ "</ServerSettings>",
84
+ "",
85
+ ].join("\n"),
86
+ );
87
+ });
88
+
89
+ test("escapes a value it writes", () => {
90
+ expect(
91
+ mergeXmlProperties(SAMPLE, {
92
+ ServerName: 'a & b "c"',
93
+ }),
94
+ ).toContain('<property name="ServerName" value="a &amp; b &quot;c&quot;"/>');
95
+ });
96
+
97
+ test("adds a value attribute to a property that has none", () => {
98
+ expect(
99
+ mergeXmlProperties('<property name="ServerName"/>', {
100
+ ServerName: "x",
101
+ }),
102
+ ).toBe('<property name="ServerName" value="x"/>');
103
+ });
104
+ });
@@ -0,0 +1,133 @@
1
+ const PROPERTY_TAG = /<property\b[^>]*>/gi;
2
+
3
+ const PROPERTY_LINE = /<property\b/i;
4
+
5
+ const CLOSING_TAG = /<\/[A-Za-z_][\w.:-]*\s*>/g;
6
+
7
+ const INDENT = /^[ \t]*/;
8
+
9
+ const DEFAULT_INDENT = "\t";
10
+
11
+ const ENTITIES: Record<string, string> = {
12
+ amp: "&",
13
+ apos: "'",
14
+ gt: ">",
15
+ lt: "<",
16
+ quot: '"',
17
+ };
18
+
19
+ const ENTITY = /&(amp|apos|gt|lt|quot);/g;
20
+
21
+ const attributePattern = (name: string) => {
22
+ return new RegExp(`\\b${name}\\s*=\\s*(?:"([^"]*)"|'([^']*)')`, "i");
23
+ };
24
+
25
+ const NAME_ATTRIBUTE = attributePattern("name");
26
+
27
+ const VALUE_ATTRIBUTE = attributePattern("value");
28
+
29
+ const decodeXml = (raw: string) => {
30
+ return raw.replace(ENTITY, (_match, entity: string) => ENTITIES[entity] ?? _match);
31
+ };
32
+
33
+ const encodeXml = (raw: string) => {
34
+ return raw
35
+ .replaceAll("&", "&amp;")
36
+ .replaceAll("<", "&lt;")
37
+ .replaceAll(">", "&gt;")
38
+ .replaceAll('"', "&quot;")
39
+ .replaceAll("'", "&apos;");
40
+ };
41
+
42
+ const attributeOf = (tag: string, pattern: RegExp) => {
43
+ const match = tag.match(pattern);
44
+
45
+ if (!match) {
46
+ return null;
47
+ }
48
+
49
+ return decodeXml(match[1] ?? match[2] ?? "");
50
+ };
51
+
52
+ const propertyLine = (indent: string, key: string, value: string) => {
53
+ return `${indent}<property name="${encodeXml(key)}" value="${encodeXml(value)}"/>`;
54
+ };
55
+
56
+ const existingIndent = (contents: string) => {
57
+ for (const line of contents.split("\n")) {
58
+ if (PROPERTY_LINE.test(line)) {
59
+ return line.match(INDENT)?.at(0) ?? DEFAULT_INDENT;
60
+ }
61
+ }
62
+
63
+ return DEFAULT_INDENT;
64
+ };
65
+
66
+ export const parseXmlProperties = (contents: string) => {
67
+ const values: Record<string, string> = {};
68
+
69
+ for (const [tag] of contents.matchAll(PROPERTY_TAG)) {
70
+ const key = attributeOf(tag, NAME_ATTRIBUTE);
71
+
72
+ if (key !== null && key.length > 0) {
73
+ values[key] = attributeOf(tag, VALUE_ATTRIBUTE) ?? "";
74
+ }
75
+ }
76
+
77
+ return values;
78
+ };
79
+
80
+ export const mergeXmlProperties = (contents: string, values: Record<string, string>) => {
81
+ const pending = new Map(Object.entries(values));
82
+ const indent = existingIndent(contents);
83
+
84
+ const rewritten = contents.replace(PROPERTY_TAG, (tag) => {
85
+ const key = attributeOf(tag, NAME_ATTRIBUTE);
86
+
87
+ if (key === null || !pending.has(key)) {
88
+ return tag;
89
+ }
90
+
91
+ const value = encodeXml(pending.get(key) ?? "");
92
+
93
+ pending.delete(key);
94
+
95
+ if (VALUE_ATTRIBUTE.test(tag)) {
96
+ return tag.replace(VALUE_ATTRIBUTE, `value="${value}"`);
97
+ }
98
+
99
+ return tag.replace(/\s*\/?>$/, ` value="${value}"/>`);
100
+ });
101
+
102
+ if (pending.size === 0) {
103
+ return rewritten;
104
+ }
105
+
106
+ const added = [
107
+ ...pending,
108
+ ].map(([key, value]) => propertyLine(indent, key, value));
109
+
110
+ const closings = [
111
+ ...rewritten.matchAll(CLOSING_TAG),
112
+ ];
113
+ const last = closings.at(-1);
114
+
115
+ if (last?.index === undefined) {
116
+ const lines = rewritten.split("\n");
117
+
118
+ while (lines.length > 0 && (lines.at(-1) ?? "").trim().length === 0) {
119
+ lines.pop();
120
+ }
121
+
122
+ return `${[
123
+ ...lines,
124
+ ...added,
125
+ ].join("\n")}\n`;
126
+ }
127
+
128
+ const head = rewritten.slice(0, last.index);
129
+ const tail = rewritten.slice(last.index);
130
+ const lineStart = head.lastIndexOf("\n") + 1;
131
+
132
+ return `${head.slice(0, lineStart)}${added.join("\n")}\n${head.slice(lineStart)}${tail}`;
133
+ };
@@ -59,9 +59,12 @@ export interface BridgeCodec {
59
59
  }
60
60
 
61
61
  export interface BridgeCodecs {
62
+ ini: BridgeCodec;
62
63
  json: BridgeCodec;
63
64
  properties: BridgeCodec;
65
+ sourceCfg: BridgeCodec;
64
66
  ueIni: BridgeCodec;
67
+ xmlProperties: BridgeCodec;
65
68
  }
66
69
 
67
70
  export interface BridgeRequestOptions {
@@ -91,8 +94,44 @@ export interface BridgeA2sInfo {
91
94
  latencyMs: number;
92
95
  }
93
96
 
97
+ export interface BridgeMinecraftPingStatus {
98
+ players: BridgePingPlayers;
99
+ version: string | null;
100
+ description: string | null;
101
+ latencyMs: number;
102
+ }
103
+
104
+ export interface BridgeRaknetPingStatus {
105
+ players: BridgePingPlayers;
106
+ edition: string | null;
107
+ motd: string | null;
108
+ subMotd: string | null;
109
+ version: string | null;
110
+ protocol: number | null;
111
+ gamemode: string | null;
112
+ latencyMs: number;
113
+ }
114
+
94
115
  export interface BridgeProbes {
95
116
  a2s(port: number): Promise<BridgeA2sInfo>;
117
+ minecraftPing(port: number): Promise<BridgeMinecraftPingStatus>;
118
+ raknetPing(port: number): Promise<BridgeRaknetPingStatus>;
119
+ }
120
+
121
+ export interface BridgeRconOptions {
122
+ port: number;
123
+ password: string;
124
+ timeoutMs?: number;
125
+ multiPacket?: boolean;
126
+ }
127
+
128
+ export interface BridgeRconClient {
129
+ command(text: string): Promise<string>;
130
+ close(): void;
131
+ }
132
+
133
+ export interface BridgeRcons {
134
+ source(options: BridgeRconOptions): Promise<BridgeRconClient>;
96
135
  }
97
136
 
98
137
  export interface BridgeCommandOptions {
@@ -144,6 +183,7 @@ export interface BridgeContext {
144
183
  codec: BridgeCodecs;
145
184
  net: BridgeNet;
146
185
  probe: BridgeProbes;
186
+ rcon: BridgeRcons;
147
187
  command(input: string, options?: BridgeCommandOptions): Promise<BridgeReply>;
148
188
  exec(command: string[], options?: BridgeExecOptions): Promise<BridgeExecResult>;
149
189
  logs: BridgeLogs;
@@ -0,0 +1,19 @@
1
+ export enum BridgeRconCode {
2
+ AuthFailed = "auth-failed",
3
+ Closed = "closed",
4
+ Malformed = "malformed",
5
+ Oversized = "oversized",
6
+ TimedOut = "timed-out",
7
+ Unreachable = "unreachable",
8
+ }
9
+
10
+ export class BridgeRconError extends Error {
11
+ readonly code: BridgeRconCode;
12
+
13
+ constructor(code: BridgeRconCode, message: string) {
14
+ super(message);
15
+
16
+ this.name = "BridgeRconError";
17
+ this.code = code;
18
+ }
19
+ }
package/src/core/index.ts CHANGED
@@ -7,6 +7,7 @@ export * from "./bridgeKind";
7
7
  export * from "./bridgeNetError";
8
8
  export * from "./bridgePort";
9
9
  export * from "./bridgeProbeError";
10
+ export * from "./bridgeRconError";
10
11
  export * from "./bridgeRuntime";
11
12
  export * from "./bridgeSecretError";
12
13
  export * from "./bridgeSetup";
@@ -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
  });
@@ -0,0 +1 @@
1
+ export * from "./installStamp";