@@ -9,13 +9,101 @@ const defaultBranch = 'main';
99const branch = argv . branch ?? defaultBranch ;
1010let jsonSchemaPath = argv . path ;
1111if ( 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
1517jsonSchemaPath = path . resolve ( jsonSchemaPath ) ;
1618const genDir = path . join ( __dirname , '..' , 'src' , 'generated' ) ;
1719const 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 = / ( \/ \/ S T A R T - G E N E R A T E D - S E C T I O N \n ) ( [ \s \S ] * ?) ( \/ \/ E N D - G E N E R A T E D - S E C T I O N ) / g;
25+ const SENTINEL_REGEX_SINGLE = / ( \/ \/ S T A R T - G E N E R A T E D - S E C T I O N \n ) ( [ \s \S ] * ?) ( \/ \/ E N D - G E N E R A T E D - S E C T I O N ) / ;
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+
19107main ( ) . 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+ */
121221function 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+ / ( e x p o r t e n u m C o m m a n d s \{ [ \s \S ] * ?\/ \/ S T A R T - G E N E R A T E D - S E C T I O N \n ) ( [ \s \S ] * ?) ( \/ \/ E N D - G E N E R A T E D - S E C T I O N ) / ;
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+ */
127368function parseZodSchema ( code ) {
128369 return (
129370 parseSchema ( code )
0 commit comments