doc_helper.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475
  1. import fs from 'fs';
  2. import path from 'path';
  3. import { fileURLToPath } from 'url';
  4. import manualOverrides from '../doc/contributors/extensions/manual_overrides.json.js';
  5. // Get the directory name in ES modules
  6. const __filename = fileURLToPath(import.meta.url);
  7. const __dirname = path.dirname(__filename);
  8. // Create a map of manual overrides for quick lookup
  9. const manualOverridesMap = new Map();
  10. manualOverrides.forEach(override => {
  11. manualOverridesMap.set(override.id, override);
  12. });
  13. // Array to collect all warnings
  14. const warnings = [];
  15. // Add a function to detect and collect duplicate events
  16. function checkForDuplicateEvent(eventId, filePath, seenEvents) {
  17. if (seenEvents.has(eventId)) {
  18. const existing = seenEvents.get(eventId);
  19. if (existing.fromManualOverride) {
  20. warnings.push(`Event ${eventId} found in ${filePath} but already defined in manual overrides. Using manual override.`);
  21. } else {
  22. warnings.push(`Duplicate event ${eventId} found in ${filePath}. First seen in ${existing.filename}.`);
  23. }
  24. return true;
  25. }
  26. return false;
  27. }
  28. function extractEventsFromFile(filePath, seenEvents, debugMode) {
  29. const content = fs.readFileSync(filePath, 'utf-8');
  30. // Use a more general regex to capture all event emissions
  31. // This captures the event name and whatever is passed as the second argument
  32. const regex = /svc_event\.emit\(['"]([^'"]+)['"]\s*,\s*([^)]+)\)/g;
  33. let match;
  34. while ((match = regex.exec(content)) !== null) {
  35. const eventName = match[1];
  36. const eventId = `core.${eventName}`;
  37. const eventArg = match[2].trim();
  38. // Check if this file contains code that might affect event.allow
  39. const hasAllowEffect = content.includes('event.allow') ||
  40. content.includes('.allow =') ||
  41. content.includes('.allow=');
  42. // Check for duplicate events and collect warnings
  43. if (checkForDuplicateEvent(eventId, filePath, seenEvents)) {
  44. continue; // Skip this event if it's a duplicate
  45. }
  46. // Check if this event has a manual override
  47. if (manualOverridesMap.has(eventId)) {
  48. // Use the manual override instead of generating a new definition
  49. const override = manualOverridesMap.get(eventId);
  50. // Mark this as coming from manual override for later reference
  51. override.fromManualOverride = true;
  52. seenEvents.set(eventId, override);
  53. continue;
  54. }
  55. // Generate description based on event name
  56. let description = generateDescription(eventName);
  57. let propertyDetails = {};
  58. // Case 1: Inline object - extract properties directly
  59. if (eventArg.startsWith('{')) {
  60. // Extract properties from inline object
  61. const propertiesMatch = eventArg.match(/{([^}]*)}/);
  62. if (propertiesMatch) {
  63. const propertiesText = propertiesMatch[1];
  64. extractProperties(propertiesText, propertyDetails, hasAllowEffect, eventName);
  65. }
  66. }
  67. // Case 2: Variable reference - find variable definition
  68. else {
  69. const varName = eventArg.trim();
  70. // Look for variable definition patterns like: const event = { prop1: value1 };
  71. const varDefRegex = new RegExp(`(?:const|let|var)\\s+${varName}\\s*=\\s*{([^}]*)}`, 'g');
  72. let varMatch;
  73. if ((varMatch = varDefRegex.exec(content)) !== null) {
  74. const propertiesText = varMatch[1];
  75. extractProperties(propertiesText, propertyDetails, hasAllowEffect, eventName);
  76. }
  77. }
  78. // Add the event to our collection
  79. seenEvents.set(eventId, {
  80. id: eventId,
  81. event: eventName,
  82. filename: path.basename(filePath),
  83. description: description,
  84. properties: propertyDetails,
  85. fromManualOverride: false
  86. });
  87. }
  88. }
  89. // Helper function to extract properties from a properties text string
  90. function extractProperties(propertiesText, propertyDetails, hasAllowEffect, eventName) {
  91. const properties = propertiesText
  92. .split(/\s*,\s*/)
  93. .map(prop => prop.split(':')[0].trim())
  94. .filter(prop => prop);
  95. // Generate property details
  96. properties.forEach(prop => {
  97. propertyDetails[prop] = {
  98. type: guessType(prop),
  99. mutability: hasAllowEffect ? 'effect' : 'no-effect',
  100. summary: guessSummary(prop, eventName)
  101. };
  102. });
  103. }
  104. function generateDescription(eventName) {
  105. const parts = eventName.split('.');
  106. if (parts.length >= 2) {
  107. const system = parts[0];
  108. const action = parts.slice(1).join('.');
  109. if (action.includes('create')) {
  110. return `This event is emitted when a ${parts[parts.length - 1]} is created.`;
  111. } else if (action.includes('update') || action.includes('write')) {
  112. return `This event is emitted when a ${parts[parts.length - 1]} is updated.`;
  113. } else if (action.includes('delete') || action.includes('remove')) {
  114. return `This event is emitted when a ${parts[parts.length - 1]} is deleted.`;
  115. } else if (action.includes('progress')) {
  116. return `This event reports progress of a ${parts[parts.length - 1]} operation.`;
  117. } else if (action.includes('validate')) {
  118. return `This event is emitted when a ${parts[parts.length - 1]} is being validated.\nThe event can be used to block certain ${parts[parts.length - 1]}s from being validated.`;
  119. } else {
  120. return `This event is emitted for ${system} ${action.replace(/[-\.]/g, ' ')} operations.`;
  121. }
  122. }
  123. return `This event is emitted for ${eventName} operations.`;
  124. }
  125. function guessType(propertyName) {
  126. // Guess the type based on property name
  127. if (propertyName === 'node') return 'FSNodeContext';
  128. if (propertyName === 'context') return 'Context';
  129. if (propertyName === 'user') return 'User';
  130. if (propertyName.includes('path')) return 'string';
  131. if (propertyName.includes('id')) return 'string';
  132. if (propertyName.includes('name')) return 'string';
  133. if (propertyName.includes('progress')) return 'number';
  134. if (propertyName.includes('tracker')) return 'ProgressTracker';
  135. if (propertyName.includes('meta')) return 'object';
  136. if (propertyName.includes('policy')) return 'Policy';
  137. if (propertyName.includes('allow')) return 'boolean';
  138. return 'any';
  139. }
  140. function guessSummary(propertyName, eventName) {
  141. // Generate summary based on property name and event context
  142. if (propertyName === 'node') {
  143. const entityType = eventName.split('.').pop();
  144. return `the ${entityType} that was affected`;
  145. }
  146. if (propertyName === 'context') return 'current context';
  147. if (propertyName === 'user') return 'user associated with the operation';
  148. if (propertyName.includes('path')) return 'path to the affected resource';
  149. if (propertyName.includes('tracker')) return 'tracks progress of the operation';
  150. if (propertyName.includes('meta')) return 'additional metadata for the operation';
  151. if (propertyName.includes('policy')) return 'policy information for the operation';
  152. if (propertyName.includes('allow')) return 'whether the operation is allowed';
  153. // Default summary based on property name
  154. return propertyName.replace(/_/g, ' ');
  155. }
  156. function scanDirectory(directory, seenEvents, debugMode) {
  157. const files = fs.readdirSync(directory);
  158. for (const file of files) {
  159. const filePath = path.join(directory, file);
  160. const stat = fs.statSync(filePath);
  161. if (stat.isDirectory()) {
  162. scanDirectory(filePath, seenEvents, debugMode);
  163. } else if (file.endsWith('.js')) {
  164. try {
  165. extractEventsFromFile(filePath, seenEvents, debugMode);
  166. } catch (error) {
  167. warnings.push(`Error processing file ${filePath}: ${error.message}`);
  168. }
  169. }
  170. }
  171. }
  172. function generateTestExtension(events) {
  173. let code = `// Test extension for event listeners\n\n`;
  174. events.forEach(event => {
  175. const eventId = event.id;
  176. const eventName = event.event ? event.event.toUpperCase() : eventId.split('.').slice(1).join('.').toUpperCase();
  177. code += `extension.on('${eventId}', event => {\n`;
  178. code += ` console.log('GOT ${eventName} EVENT', event);\n`;
  179. code += `});\n\n`;
  180. });
  181. return code;
  182. }
  183. function main() {
  184. const args = process.argv.slice(2);
  185. if (args.length < 1) {
  186. console.error('Usage: node doc_helper.js <directory> [output_file] [--generate-test] [--test-dir=<directory>] [--debug]');
  187. process.exit(1);
  188. }
  189. // Resolve directory path relative to project root
  190. const directory = path.resolve(path.join(path.dirname(__dirname), args[0]));
  191. let outputFile = null;
  192. let generateTest = false;
  193. let testOutputDir = "./extensions/";
  194. let debugMode = false;
  195. // Parse arguments
  196. for (let i = 1; i < args.length; i++) {
  197. if (args[i] === '--generate-test') {
  198. generateTest = true;
  199. } else if (args[i].startsWith('--test-dir=')) {
  200. testOutputDir = args[i].substring('--test-dir='.length);
  201. } else if (args[i] === '--debug') {
  202. debugMode = true;
  203. } else if (!args[i].startsWith('--')) {
  204. // Only treat non-flag arguments as output file
  205. outputFile = path.resolve(path.join(path.dirname(__dirname), args[i]));
  206. }
  207. }
  208. // Resolve test output directory relative to project root if it's not an absolute path
  209. if (!path.isAbsolute(testOutputDir)) {
  210. testOutputDir = path.resolve(path.join(path.dirname(__dirname), testOutputDir));
  211. }
  212. const seenEvents = new Map();
  213. // First, add all manual overrides to the seenEvents map
  214. manualOverrides.forEach(override => {
  215. // Mark this as coming from manual override for later reference
  216. override.fromManualOverride = true;
  217. seenEvents.set(override.id, override);
  218. });
  219. // Then scan the directory for additional events
  220. scanDirectory(directory, seenEvents, debugMode);
  221. // Check for any manual overrides that weren't used
  222. manualOverrides.forEach(override => {
  223. const event = seenEvents.get(override.id);
  224. if (!event || !event.fromManualOverride) {
  225. warnings.push(`Manual override for ${override.id} exists but no matching event was found in the codebase.`);
  226. }
  227. });
  228. const result = Array.from(seenEvents.values());
  229. // Sort events alphabetically by ID
  230. result.sort((a, b) => a.id.localeCompare(b.id));
  231. // Format the output to match events.json.js
  232. const formattedOutput = formatEventsOutput(result);
  233. // Output the result
  234. if (outputFile) {
  235. fs.writeFileSync(outputFile, formattedOutput);
  236. console.log(`Event metadata written to ${outputFile}`);
  237. } else {
  238. console.log(formattedOutput);
  239. }
  240. // Generate test extension file if requested
  241. if (generateTest) {
  242. const testCode = generateTestExtension(result);
  243. // Ensure the output directory exists
  244. if (!fs.existsSync(testOutputDir)) {
  245. fs.mkdirSync(testOutputDir, { recursive: true });
  246. }
  247. const testFilePath = path.join(testOutputDir, 'testex.js');
  248. fs.writeFileSync(testFilePath, testCode);
  249. console.log(`Test extension file generated: ${testFilePath}`);
  250. }
  251. // Print warnings in the requested format
  252. if (warnings.length > 0) {
  253. // Collect duplicate events
  254. const duplicateEvents = new Set();
  255. const overrideEvents = new Set();
  256. const otherWarnings = [];
  257. warnings.forEach(warning => {
  258. if (warning.includes("Duplicate event")) {
  259. // Extract event ID from the warning message
  260. const match = warning.match(/Duplicate event (core\.[^ ]+)/);
  261. if (match && match[1]) {
  262. duplicateEvents.add(match[1]);
  263. }
  264. } else if (warning.includes("already defined in manual overrides")) {
  265. // Extract event ID from the warning message
  266. const match = warning.match(/Event (core\.[^ ]+) found/);
  267. if (match && match[1]) {
  268. overrideEvents.add(match[1]);
  269. }
  270. } else {
  271. otherWarnings.push(warning);
  272. }
  273. });
  274. // Output in the requested format
  275. console.log(`\nduplicate events: ${Array.from(duplicateEvents).join(', ')}`);
  276. console.log(`Override events: ${Array.from(overrideEvents).join(', ')}`);
  277. // If there are any other warnings, print them too
  278. if (otherWarnings.length > 0) {
  279. console.log("\nOther warnings:");
  280. otherWarnings.forEach(warning => {
  281. console.log(`- ${warning}`);
  282. });
  283. }
  284. }
  285. }
  286. /**
  287. * Format the events data to match the events.json.js format
  288. */
  289. function formatEventsOutput(events) {
  290. let output = 'export default [\n';
  291. events.forEach((event, index) => {
  292. // Check if this is a manual override
  293. if (event.fromManualOverride) {
  294. // This is a manual override, output it exactly as defined
  295. output += ' {\n';
  296. output += ` id: '${event.id}',\n`;
  297. output += ` description: \``;
  298. // Format the description with proper indentation, preserving original formatting
  299. // Don't add extra newlines before or after the description
  300. output += event.description;
  301. output += `\`,\n`;
  302. // Add properties if they exist, preserving exact format
  303. if (event.properties && Object.keys(event.properties).length > 0) {
  304. output += ' properties: {\n';
  305. Object.entries(event.properties).forEach(([propName, propDetails], propIndex) => {
  306. output += ` ${propName}: {\n`;
  307. output += ` type: '${propDetails.type}',\n`;
  308. output += ` mutability: '${propDetails.mutability}',\n`;
  309. output += ` summary: '${propDetails.summary}'`;
  310. // Add notes array if it exists
  311. if (propDetails.notes && propDetails.notes.length > 0) {
  312. output += `,\n notes: [\n`;
  313. propDetails.notes.forEach((note, noteIndex) => {
  314. output += ` '${note}'`;
  315. if (noteIndex < propDetails.notes.length - 1) {
  316. output += ',';
  317. }
  318. output += '\n';
  319. });
  320. output += ` ]`;
  321. }
  322. output += '\n }';
  323. // Add comma if not the last property
  324. if (propIndex < Object.keys(event.properties).length - 1) {
  325. output += ',';
  326. }
  327. output += '\n';
  328. });
  329. output += ' },\n';
  330. }
  331. // Add example if it exists
  332. if (event.example) {
  333. output += ' example: {\n';
  334. output += ` language: '${event.example.language}',\n`;
  335. output += ` code: /*${event.example.language}*/\``;
  336. // Preserve the exact formatting of the example code
  337. // Don't add extra newlines and preserve escape sequences exactly as they are
  338. output += event.example.code;
  339. output += `\`\n`;
  340. output += ' },\n';
  341. }
  342. output += ' }';
  343. } else {
  344. // This is an auto-generated event
  345. output += ' {\n';
  346. output += ` id: '${event.id}',\n`;
  347. output += ` description: \`\n`;
  348. // Format the description with proper indentation
  349. const descriptionLines = event.description.split('\n');
  350. descriptionLines.forEach(line => {
  351. output += ` ${line}\n`;
  352. });
  353. output += ` \`,\n`;
  354. // Add properties if they exist
  355. if (Object.keys(event.properties).length > 0) {
  356. output += ' properties: {\n';
  357. Object.entries(event.properties).forEach(([propName, propDetails], propIndex) => {
  358. output += ` ${propName}: {\n`;
  359. output += ` type: '${propDetails.type}',\n`;
  360. output += ` mutability: '${propDetails.mutability === 'effect' ? 'mutable' : 'no-effect'}',\n`;
  361. output += ` summary: '${propDetails.summary}',\n`;
  362. // Add notes array with appropriate content
  363. if (propName === 'allow' && event.event.includes('validate')) {
  364. output += ` notes: [\n`;
  365. output += ` 'If set to false, the ${event.event.split('.')[0]} will be considered invalid.',\n`;
  366. output += ` ],\n`;
  367. } else if (propName === 'email' && event.event.includes('validate')) {
  368. output += ` notes: [\n`;
  369. output += ` 'The email may have already been cleaned.',\n`;
  370. output += ` ],\n`;
  371. } else {
  372. output += ` notes: [],\n`;
  373. }
  374. output += ' }';
  375. // Add comma if not the last property
  376. if (propIndex < Object.keys(event.properties).length - 1) {
  377. output += ',';
  378. }
  379. output += '\n';
  380. });
  381. output += ' },\n';
  382. }
  383. output += ' }';
  384. }
  385. // Add comma if not the last event
  386. if (index < events.length - 1) {
  387. output += ',';
  388. }
  389. output += '\n';
  390. });
  391. output += '];\n';
  392. return output;
  393. }
  394. main();
  395. // Updated Sun Mar 9 23:52:51 EDT 2025