Skip to content

Commit b284080

Browse files
authored
feat: upgrade sync script and run it (#336)
* chore: add syncCommandsEnum and syncResponseParsing to sync script * chore: add syncCommandsIndex and generateCommandFiles to script * chore: add mock commands generation * fix: run sync with new script * fix: mock interface * chore: use START and END sentinels * fix: remove all non-synced generated commands * fix: remove the duplicate script block * fix: use the sentinels in mock, better sentinel matching in imports and exports * chore: refactor the script for DRY * fix: sort commands alphabetically * fix: run 'npm run sync' with new script * fix: keep getInstanceConnectedParticipants for backwards-compatibility * chore: checkout old files from main to get the comments cause why not * fix: do not diff the commands, responses, or import/exports
1 parent aa0d033 commit b284080

File tree

10 files changed

+316
-53
lines changed

10 files changed

+316
-53
lines changed

scripts/syncRPCSchema.mjs

Lines changed: 242 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,101 @@ const defaultBranch = 'main';
99
const branch = argv.branch ?? defaultBranch;
1010
let jsonSchemaPath = argv.path;
1111
if (jsonSchemaPath == null) {
12-
throw new Error('Expected -- --path argument.\nThis should point to the generated JSON Schema file.\nExample command below:\nnpm run sync -- --path path/to/monorepo/discord_common/js/packages/rpc-schema/generated/schema.json');
12+
throw new Error(
13+
'Expected -- --path argument.\nThis should point to the generated JSON Schema file.\nExample command below:\nnpm run sync -- --path path/to/monorepo/discord_common/js/packages/rpc-schema/generated/schema.json',
14+
);
1315
}
1416
// Resolve absolute path
1517
jsonSchemaPath = path.resolve(jsonSchemaPath);
1618
const genDir = path.join(__dirname, '..', 'src', 'generated');
1719
const schemaFilePath = path.join(genDir, 'schema.json');
1820

21+
// Constants for generated sections
22+
const GENERATED_SECTION_START = '// START-GENERATED-SECTION';
23+
const GENERATED_SECTION_END = '// END-GENERATED-SECTION';
24+
const SENTINEL_REGEX = /(\/\/ START-GENERATED-SECTION\n)([\s\S]*?)(\/\/ END-GENERATED-SECTION)/g;
25+
const SENTINEL_REGEX_SINGLE = /(\/\/ START-GENERATED-SECTION\n)([\s\S]*?)(\/\/ END-GENERATED-SECTION)/;
26+
27+
// File paths
28+
const PATHS = {
29+
common: path.join(__dirname, '..', 'src', 'schema', 'common.ts'),
30+
responses: path.join(__dirname, '..', 'src', 'schema', 'responses.ts'),
31+
index: path.join(__dirname, '..', 'src', 'commands', 'index.ts'),
32+
mock: path.join(__dirname, '..', 'src', 'mock.ts'),
33+
commandsDir: path.join(__dirname, '..', 'src', 'commands'),
34+
};
35+
36+
// Templates
37+
const COMMAND_FILE_TEMPLATE = (cmdName, cmd) => `import {Command} from '../generated/schemas';
38+
import {schemaCommandFactory} from '../utils/commandFactory';
39+
40+
export const ${cmdName} = schemaCommandFactory(Command.${cmd});
41+
`;
42+
43+
// Helper Functions
44+
/**
45+
* @param {string} filePath - Path to write the file
46+
* @param {string} content - File content to format and write
47+
*/
48+
async function formatAndWriteFile(filePath, content) {
49+
const prettierOpts = await prettier.resolveConfig(__dirname);
50+
prettierOpts.parser = 'typescript';
51+
const formattedContent = await prettier.format(content, prettierOpts);
52+
await fs.writeFile(filePath, formattedContent);
53+
}
54+
55+
/**
56+
* @param {string} content - File content to search
57+
* @param {string} filePath - File path for error messages
58+
* @param {number} expectedCount - Expected number of sentinel pairs
59+
* @returns {RegExpMatchArray | RegExpMatchArray[]} Single match or array of matches
60+
*/
61+
function findSentinelSections(content, filePath, expectedCount = 1) {
62+
const matches = [...content.matchAll(SENTINEL_REGEX)];
63+
if (matches.length !== expectedCount) {
64+
throw createSentinelError(filePath, expectedCount, matches.length);
65+
}
66+
return expectedCount === 1 ? matches[0] : matches;
67+
}
68+
69+
/**
70+
* @param {string} filePath - File path for error message
71+
* @param {number} expected - Expected number of sentinels
72+
* @param {number} found - Actual number found
73+
* @returns {Error} Descriptive error with guidance
74+
*/
75+
function createSentinelError(filePath, expected, found) {
76+
return new Error(
77+
`Expected exactly ${expected} ${GENERATED_SECTION_START}/${GENERATED_SECTION_END} pair(s) in ${filePath}, but found ${found}. ` +
78+
'Please add these comments around the generated sections.',
79+
);
80+
}
81+
82+
/**
83+
* @param {string} cmd - Command name to convert
84+
* @returns {{camelCase: string, original: string}} Command names in different formats
85+
*/
86+
function getCommandNames(cmd) {
87+
return {
88+
camelCase: camelCase(cmd),
89+
original: cmd,
90+
};
91+
}
92+
93+
/**
94+
* @param {string} content - Content to search in
95+
* @param {RegExp} regex - Regex pattern to match
96+
* @returns {Set<string>} Set of extracted items
97+
*/
98+
function extractExistingItems(content, regex) {
99+
const existing = new Set();
100+
const matches = content.matchAll(regex);
101+
for (const match of matches) {
102+
existing.add(match[1]);
103+
}
104+
return existing;
105+
}
106+
19107
main().catch((err) => {
20108
throw err;
21109
});
@@ -116,14 +204,167 @@ async function main() {
116204
prettierOpts.parser = 'typescript';
117205
const formattedCode = await prettier.format(output, prettierOpts);
118206
await fs.writeFile(path.join(genDir, 'schemas.ts'), formattedCode);
207+
208+
// Auto-sync Commands enum and response parsing
209+
console.log('> Auto-syncing Commands enum and response parsing');
210+
await syncCommandsEnum(schemas);
211+
await syncResponseParsing(schemas);
212+
await syncCommandsIndex(schemas);
213+
await generateCommandFiles(schemas);
214+
await syncMockCommands(schemas);
119215
}
120216

217+
/**
218+
* @param {string} name - Token name to format
219+
* @returns {string} Formatted class name
220+
*/
121221
function formatToken(name) {
122222
let className = camelCase(name);
123223
className = className.charAt(0).toUpperCase() + className.slice(1);
124224
return className;
125225
}
126226

227+
/**
228+
* @param {Record<string, any>} schemas - Schema definitions from JSON
229+
*/
230+
async function syncCommandsEnum(schemas) {
231+
const content = await fs.readFile(PATHS.common, 'utf-8');
232+
233+
// Find the Commands enum using sentinel comments
234+
const enumRegex =
235+
/(export enum Commands \{[\s\S]*?\/\/ START-GENERATED-SECTION\n)([\s\S]*?)(\/\/ END-GENERATED-SECTION)/;
236+
const enumMatch = content.match(enumRegex);
237+
if (!enumMatch) {
238+
throw new Error(
239+
`Could not find Commands enum with ${GENERATED_SECTION_START}/${GENERATED_SECTION_END} sentinels in ${PATHS.common}`,
240+
);
241+
}
242+
243+
const [fullMatch, beforeSection, generatedSection, afterSection] = enumMatch;
244+
245+
// Generate ALL schema commands (sorted alphabetically)
246+
const allCommandEntries = Object.keys(schemas).sort().map((cmd) => ` ${cmd} = '${cmd}',`);
247+
248+
console.log(`> Syncing ${allCommandEntries.length} commands in Commands enum`);
249+
250+
// Replace entire generated section
251+
const updatedContent = beforeSection + allCommandEntries.join('\n') + '\n ' + afterSection;
252+
const updatedFile = content.replace(fullMatch, updatedContent);
253+
254+
await formatAndWriteFile(PATHS.common, updatedFile);
255+
}
256+
257+
/**
258+
* @param {Record<string, any>} schemas - Schema definitions from JSON
259+
*/
260+
async function syncResponseParsing(schemas) {
261+
const content = await fs.readFile(PATHS.responses, 'utf-8');
262+
263+
const [fullMatch, beforeSection, generatedSection, afterSection] = findSentinelSections(content, PATHS.responses);
264+
265+
// Generate ALL schema case statements (sorted alphabetically)
266+
const allCaseStatements = Object.keys(schemas).sort().map((cmd) => ` case Commands.${cmd}:`);
267+
268+
console.log(`> Syncing ${allCaseStatements.length} commands in response parsing`);
269+
270+
// Replace entire generated section
271+
const updatedContent = beforeSection + allCaseStatements.join('\n') + '\n ' + afterSection;
272+
const updatedFile = content.replace(fullMatch, updatedContent);
273+
274+
await formatAndWriteFile(PATHS.responses, updatedFile);
275+
}
276+
277+
/**
278+
* @param {Record<string, any>} schemas - Schema definitions from JSON
279+
*/
280+
async function syncCommandsIndex(schemas) {
281+
let content = await fs.readFile(PATHS.index, 'utf-8');
282+
283+
const [importsMatch, exportsMatch] = findSentinelSections(content, PATHS.index, 2);
284+
285+
// Generate ALL schema imports and exports (sorted alphabetically)
286+
const allCommands = Object.keys(schemas).sort().map(getCommandNames);
287+
288+
const allImports = allCommands.map(({camelCase}) => `import {${camelCase}} from './${camelCase}';`);
289+
const allExports = allCommands.map(({camelCase}) => ` ${camelCase}: ${camelCase}(sendCommand),`);
290+
291+
console.log(`> Syncing ${allCommands.length} commands in index.ts`);
292+
293+
// Replace entire imports section
294+
const updatedImports = importsMatch[1] + allImports.join('\n') + '\n' + importsMatch[3];
295+
content = content.replace(importsMatch[0], updatedImports);
296+
297+
// Replace entire exports section
298+
const updatedExports = exportsMatch[1] + allExports.join('\n') + '\n ' + exportsMatch[3];
299+
content = content.replace(exportsMatch[0], updatedExports);
300+
301+
await formatAndWriteFile(PATHS.index, content);
302+
}
303+
304+
/**
305+
* @param {Record<string, any>} schemas - Schema definitions from JSON
306+
*/
307+
async function generateCommandFiles(schemas) {
308+
const commandsToGenerate = [];
309+
310+
for (const cmd of Object.keys(schemas).sort()) {
311+
const {camelCase: cmdName} = getCommandNames(cmd);
312+
const filePath = path.join(PATHS.commandsDir, `${cmdName}.ts`);
313+
314+
const exists = await fs.pathExists(filePath);
315+
if (!exists) {
316+
commandsToGenerate.push({cmd, cmdName, filePath});
317+
}
318+
}
319+
320+
if (commandsToGenerate.length === 0) return;
321+
322+
console.log(
323+
`> Generating ${commandsToGenerate.length} command files:`,
324+
commandsToGenerate.map((c) => c.cmdName),
325+
);
326+
327+
// Generate all files
328+
await Promise.all(
329+
commandsToGenerate.map(({cmd, cmdName, filePath}) => fs.writeFile(filePath, COMMAND_FILE_TEMPLATE(cmdName, cmd))),
330+
);
331+
}
332+
333+
/**
334+
* @param {Record<string, any>} schemas - Schema definitions from JSON
335+
*/
336+
async function syncMockCommands(schemas) {
337+
const content = await fs.readFile(PATHS.mock, 'utf-8');
338+
339+
const [fullMatch, beforeSection, generatedSection, afterSection] = findSentinelSections(content, PATHS.mock);
340+
341+
// Extract existing mock commands from generated section
342+
const existingMocks = extractExistingItems(generatedSection, /(\w+):\s*\(\)/g);
343+
344+
// Find missing commands (sorted alphabetically)
345+
const missingCommands = Object.keys(schemas)
346+
.sort()
347+
.map(getCommandNames)
348+
.filter(({camelCase}) => !existingMocks.has(camelCase));
349+
if (missingCommands.length === 0) return;
350+
351+
console.log(`> Adding ${missingCommands.length} new mock commands:`, missingCommands.map(c => c.camelCase));
352+
353+
// Generate basic mock functions for missing commands only
354+
const newMockFunctions = missingCommands.map(({camelCase}) => ` ${camelCase}: () => Promise.resolve(null),`);
355+
356+
const currentContent = generatedSection.trim();
357+
const newContent = currentContent ? currentContent + '\n' + newMockFunctions.join('\n') : newMockFunctions.join('\n');
358+
const updatedContent = beforeSection + newContent + '\n ' + afterSection;
359+
360+
const updatedFile = content.replace(fullMatch, updatedContent);
361+
await formatAndWriteFile(PATHS.mock, updatedFile);
362+
}
363+
364+
/**
365+
* @param {string} code - JSON schema code to parse
366+
* @returns {string} Converted Zod schema code
367+
*/
127368
function parseZodSchema(code) {
128369
return (
129370
parseSchema(code)
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
11
import {Command} from '../generated/schemas';
22
import {schemaCommandFactory} from '../utils/commandFactory';
33

4-
/**
5-
* Gets all participants connected to the instance
6-
*/
7-
export const getInstanceConnectedParticipants = schemaCommandFactory(
4+
export const getActivityInstanceConnectedParticipants = schemaCommandFactory(
85
Command.GET_ACTIVITY_INSTANCE_CONNECTED_PARTICIPANTS,
96
);

src/commands/getRelationships.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {schemaCommandFactory} from '../utils/commandFactory';
21
import {Command} from '../generated/schemas';
2+
import {schemaCommandFactory} from '../utils/commandFactory';
33

44
export const getRelationships = schemaCommandFactory(Command.GET_RELATIONSHIPS);

src/commands/getUser.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {schemaCommandFactory} from '../utils/commandFactory';
21
import {Command} from '../generated/schemas';
2+
import {schemaCommandFactory} from '../utils/commandFactory';
33

44
export const getUser = schemaCommandFactory(Command.GET_USER);

src/commands/index.ts

Lines changed: 23 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,39 @@
11
import {Commands} from '../schema/common';
22
import {TSendCommand} from '../schema/types';
33

4-
import {authenticate} from './authenticate';
54
import {authorize} from './authorize';
65
import {captureLog} from './captureLog';
76
import {encourageHardwareAcceleration} from './encourageHardwareAcceleration';
7+
import {getChannel} from './getChannel';
88
import {getEntitlements} from './getEntitlements';
99
import {getSkus} from './getSkus';
1010
import {getChannelPermissions} from './getChannelPermissions';
1111
import {getPlatformBehaviors} from './getPlatformBehaviors';
1212
import {openExternalLink} from './openExternalLink';
1313
import {openInviteDialog} from './openInviteDialog';
14-
import {openShareMomentDialog} from './openShareMomentDialog';
1514
import {setActivity, SetActivity} from './setActivity';
1615
import {setConfig} from './setConfig';
1716
import {setOrientationLockState} from './setOrientationLockState';
18-
import {shareLink} from './shareLink';
1917
import {startPurchase} from './startPurchase';
2018
import {userSettingsGetLocale} from './userSettingsGetLocale';
21-
import {initiateImageUpload} from './initiateImageUpload';
22-
import {getChannel} from './getChannel';
23-
import {getInstanceConnectedParticipants} from './getInstanceConnectedParticipants';
19+
// START-GENERATED-SECTION
20+
import {authenticate} from './authenticate';
21+
import {getActivityInstanceConnectedParticipants} from './getActivityInstanceConnectedParticipants';
22+
import {getQuestEnrollmentStatus} from './getQuestEnrollmentStatus';
2423
import {getRelationships} from './getRelationships';
25-
import {inviteUserEmbedded} from './inviteUserEmbedded';
2624
import {getUser} from './getUser';
27-
import {getQuestEnrollmentStatus} from './getQuestEnrollmentStatus';
25+
import {initiateImageUpload} from './initiateImageUpload';
26+
import {inviteUserEmbedded} from './inviteUserEmbedded';
27+
import {openShareMomentDialog} from './openShareMomentDialog';
2828
import {questStartTimer} from './questStartTimer';
29+
import {shareInteraction} from './shareInteraction';
30+
import {shareLink} from './shareLink';
31+
// END-GENERATED-SECTION
2932

3033
export {Commands, SetActivity};
3134

3235
function commands(sendCommand: TSendCommand) {
3336
return {
34-
authenticate: authenticate(sendCommand),
3537
authorize: authorize(sendCommand),
3638
captureLog: captureLog(sendCommand),
3739
encourageHardwareAcceleration: encourageHardwareAcceleration(sendCommand),
@@ -42,20 +44,26 @@ function commands(sendCommand: TSendCommand) {
4244
getSkus: getSkus(sendCommand),
4345
openExternalLink: openExternalLink(sendCommand),
4446
openInviteDialog: openInviteDialog(sendCommand),
45-
openShareMomentDialog: openShareMomentDialog(sendCommand),
4647
setActivity: setActivity(sendCommand),
4748
setConfig: setConfig(sendCommand),
4849
setOrientationLockState: setOrientationLockState(sendCommand),
49-
shareLink: shareLink(sendCommand),
5050
startPurchase: startPurchase(sendCommand),
5151
userSettingsGetLocale: userSettingsGetLocale(sendCommand),
52-
initiateImageUpload: initiateImageUpload(sendCommand),
53-
getInstanceConnectedParticipants: getInstanceConnectedParticipants(sendCommand),
52+
// Backward compatibility - getInstanceConnectedParticipants is an alias for getActivityInstanceConnectedParticipants
53+
getInstanceConnectedParticipants: getActivityInstanceConnectedParticipants(sendCommand),
54+
// START-GENERATED-SECTION
55+
authenticate: authenticate(sendCommand),
56+
getActivityInstanceConnectedParticipants: getActivityInstanceConnectedParticipants(sendCommand),
57+
getQuestEnrollmentStatus: getQuestEnrollmentStatus(sendCommand),
5458
getRelationships: getRelationships(sendCommand),
55-
inviteUserEmbedded: inviteUserEmbedded(sendCommand),
5659
getUser: getUser(sendCommand),
57-
getQuestEnrollmentStatus: getQuestEnrollmentStatus(sendCommand),
60+
initiateImageUpload: initiateImageUpload(sendCommand),
61+
inviteUserEmbedded: inviteUserEmbedded(sendCommand),
62+
openShareMomentDialog: openShareMomentDialog(sendCommand),
5863
questStartTimer: questStartTimer(sendCommand),
64+
shareInteraction: shareInteraction(sendCommand),
65+
shareLink: shareLink(sendCommand),
66+
// END-GENERATED-SECTION
5967
};
6068
}
6169

src/commands/inviteUserEmbedded.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {schemaCommandFactory} from '../utils/commandFactory';
21
import {Command} from '../generated/schemas';
2+
import {schemaCommandFactory} from '../utils/commandFactory';
33

44
export const inviteUserEmbedded = schemaCommandFactory(Command.INVITE_USER_EMBEDDED);

src/commands/shareInteraction.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import {Command} from '../generated/schemas';
2+
import {schemaCommandFactory} from '../utils/commandFactory';
3+
4+
export const shareInteraction = schemaCommandFactory(Command.SHARE_INTERACTION);

0 commit comments

Comments
 (0)