123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475 |
- import fs from 'fs';
- import path from 'path';
- import { fileURLToPath } from 'url';
- import manualOverrides from '../doc/contributors/extensions/manual_overrides.json.js';
- // Get the directory name in ES modules
- const __filename = fileURLToPath(import.meta.url);
- const __dirname = path.dirname(__filename);
- // Create a map of manual overrides for quick lookup
- const manualOverridesMap = new Map();
- manualOverrides.forEach(override => {
- manualOverridesMap.set(override.id, override);
- });
- // Array to collect all warnings
- const warnings = [];
- // Add a function to detect and collect duplicate events
- function checkForDuplicateEvent(eventId, filePath, seenEvents) {
- if (seenEvents.has(eventId)) {
- const existing = seenEvents.get(eventId);
- if (existing.fromManualOverride) {
- warnings.push(`Event ${eventId} found in ${filePath} but already defined in manual overrides. Using manual override.`);
- } else {
- warnings.push(`Duplicate event ${eventId} found in ${filePath}. First seen in ${existing.filename}.`);
- }
- return true;
- }
- return false;
- }
- function extractEventsFromFile(filePath, seenEvents, debugMode) {
- const content = fs.readFileSync(filePath, 'utf-8');
-
- // Use a more general regex to capture all event emissions
- // This captures the event name and whatever is passed as the second argument
- const regex = /svc_event\.emit\(['"]([^'"]+)['"]\s*,\s*([^)]+)\)/g;
- let match;
-
- while ((match = regex.exec(content)) !== null) {
- const eventName = match[1];
- const eventId = `core.${eventName}`;
- const eventArg = match[2].trim();
-
- // Check if this file contains code that might affect event.allow
- const hasAllowEffect = content.includes('event.allow') ||
- content.includes('.allow =') ||
- content.includes('.allow=');
-
- // Check for duplicate events and collect warnings
- if (checkForDuplicateEvent(eventId, filePath, seenEvents)) {
- continue; // Skip this event if it's a duplicate
- }
-
- // Check if this event has a manual override
- if (manualOverridesMap.has(eventId)) {
- // Use the manual override instead of generating a new definition
- const override = manualOverridesMap.get(eventId);
- // Mark this as coming from manual override for later reference
- override.fromManualOverride = true;
- seenEvents.set(eventId, override);
- continue;
- }
-
- // Generate description based on event name
- let description = generateDescription(eventName);
- let propertyDetails = {};
-
- // Case 1: Inline object - extract properties directly
- if (eventArg.startsWith('{')) {
- // Extract properties from inline object
- const propertiesMatch = eventArg.match(/{([^}]*)}/);
- if (propertiesMatch) {
- const propertiesText = propertiesMatch[1];
- extractProperties(propertiesText, propertyDetails, hasAllowEffect, eventName);
- }
- }
- // Case 2: Variable reference - find variable definition
- else {
- const varName = eventArg.trim();
- // Look for variable definition patterns like: const event = { prop1: value1 };
- const varDefRegex = new RegExp(`(?:const|let|var)\\s+${varName}\\s*=\\s*{([^}]*)}`, 'g');
- let varMatch;
-
- if ((varMatch = varDefRegex.exec(content)) !== null) {
- const propertiesText = varMatch[1];
- extractProperties(propertiesText, propertyDetails, hasAllowEffect, eventName);
- }
- }
-
- // Add the event to our collection
- seenEvents.set(eventId, {
- id: eventId,
- event: eventName,
- filename: path.basename(filePath),
- description: description,
- properties: propertyDetails,
- fromManualOverride: false
- });
- }
- }
- // Helper function to extract properties from a properties text string
- function extractProperties(propertiesText, propertyDetails, hasAllowEffect, eventName) {
- const properties = propertiesText
- .split(/\s*,\s*/)
- .map(prop => prop.split(':')[0].trim())
- .filter(prop => prop);
-
- // Generate property details
- properties.forEach(prop => {
- propertyDetails[prop] = {
- type: guessType(prop),
- mutability: hasAllowEffect ? 'effect' : 'no-effect',
- summary: guessSummary(prop, eventName)
- };
- });
- }
- function generateDescription(eventName) {
- const parts = eventName.split('.');
-
- if (parts.length >= 2) {
- const system = parts[0];
- const action = parts.slice(1).join('.');
-
- if (action.includes('create')) {
- return `This event is emitted when a ${parts[parts.length - 1]} is created.`;
- } else if (action.includes('update') || action.includes('write')) {
- return `This event is emitted when a ${parts[parts.length - 1]} is updated.`;
- } else if (action.includes('delete') || action.includes('remove')) {
- return `This event is emitted when a ${parts[parts.length - 1]} is deleted.`;
- } else if (action.includes('progress')) {
- return `This event reports progress of a ${parts[parts.length - 1]} operation.`;
- } else if (action.includes('validate')) {
- 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.`;
- } else {
- return `This event is emitted for ${system} ${action.replace(/[-\.]/g, ' ')} operations.`;
- }
- }
-
- return `This event is emitted for ${eventName} operations.`;
- }
- function guessType(propertyName) {
- // Guess the type based on property name
- if (propertyName === 'node') return 'FSNodeContext';
- if (propertyName === 'context') return 'Context';
- if (propertyName === 'user') return 'User';
- if (propertyName.includes('path')) return 'string';
- if (propertyName.includes('id')) return 'string';
- if (propertyName.includes('name')) return 'string';
- if (propertyName.includes('progress')) return 'number';
- if (propertyName.includes('tracker')) return 'ProgressTracker';
- if (propertyName.includes('meta')) return 'object';
- if (propertyName.includes('policy')) return 'Policy';
- if (propertyName.includes('allow')) return 'boolean';
-
- return 'any';
- }
- function guessSummary(propertyName, eventName) {
- // Generate summary based on property name and event context
- if (propertyName === 'node') {
- const entityType = eventName.split('.').pop();
- return `the ${entityType} that was affected`;
- }
- if (propertyName === 'context') return 'current context';
- if (propertyName === 'user') return 'user associated with the operation';
- if (propertyName.includes('path')) return 'path to the affected resource';
- if (propertyName.includes('tracker')) return 'tracks progress of the operation';
- if (propertyName.includes('meta')) return 'additional metadata for the operation';
- if (propertyName.includes('policy')) return 'policy information for the operation';
- if (propertyName.includes('allow')) return 'whether the operation is allowed';
-
- // Default summary based on property name
- return propertyName.replace(/_/g, ' ');
- }
- function scanDirectory(directory, seenEvents, debugMode) {
- const files = fs.readdirSync(directory);
-
- for (const file of files) {
- const filePath = path.join(directory, file);
- const stat = fs.statSync(filePath);
-
- if (stat.isDirectory()) {
- scanDirectory(filePath, seenEvents, debugMode);
- } else if (file.endsWith('.js')) {
- try {
- extractEventsFromFile(filePath, seenEvents, debugMode);
- } catch (error) {
- warnings.push(`Error processing file ${filePath}: ${error.message}`);
- }
- }
- }
- }
- function generateTestExtension(events) {
- let code = `// Test extension for event listeners\n\n`;
-
- events.forEach(event => {
- const eventId = event.id;
- const eventName = event.event ? event.event.toUpperCase() : eventId.split('.').slice(1).join('.').toUpperCase();
-
- code += `extension.on('${eventId}', event => {\n`;
- code += ` console.log('GOT ${eventName} EVENT', event);\n`;
- code += `});\n\n`;
- });
-
- return code;
- }
- function main() {
- const args = process.argv.slice(2);
- if (args.length < 1) {
- console.error('Usage: node doc_helper.js <directory> [output_file] [--generate-test] [--test-dir=<directory>] [--debug]');
- process.exit(1);
- }
-
- // Resolve directory path relative to project root
- const directory = path.resolve(path.join(path.dirname(__dirname), args[0]));
- let outputFile = null;
- let generateTest = false;
- let testOutputDir = "./extensions/";
- let debugMode = false;
-
- // Parse arguments
- for (let i = 1; i < args.length; i++) {
- if (args[i] === '--generate-test') {
- generateTest = true;
- } else if (args[i].startsWith('--test-dir=')) {
- testOutputDir = args[i].substring('--test-dir='.length);
- } else if (args[i] === '--debug') {
- debugMode = true;
- } else if (!args[i].startsWith('--')) {
- // Only treat non-flag arguments as output file
- outputFile = path.resolve(path.join(path.dirname(__dirname), args[i]));
- }
- }
-
- // Resolve test output directory relative to project root if it's not an absolute path
- if (!path.isAbsolute(testOutputDir)) {
- testOutputDir = path.resolve(path.join(path.dirname(__dirname), testOutputDir));
- }
-
- const seenEvents = new Map();
-
- // First, add all manual overrides to the seenEvents map
- manualOverrides.forEach(override => {
- // Mark this as coming from manual override for later reference
- override.fromManualOverride = true;
- seenEvents.set(override.id, override);
- });
-
- // Then scan the directory for additional events
- scanDirectory(directory, seenEvents, debugMode);
-
- // Check for any manual overrides that weren't used
- manualOverrides.forEach(override => {
- const event = seenEvents.get(override.id);
- if (!event || !event.fromManualOverride) {
- warnings.push(`Manual override for ${override.id} exists but no matching event was found in the codebase.`);
- }
- });
-
- const result = Array.from(seenEvents.values());
-
- // Sort events alphabetically by ID
- result.sort((a, b) => a.id.localeCompare(b.id));
-
- // Format the output to match events.json.js
- const formattedOutput = formatEventsOutput(result);
-
- // Output the result
- if (outputFile) {
- fs.writeFileSync(outputFile, formattedOutput);
- console.log(`Event metadata written to ${outputFile}`);
- } else {
- console.log(formattedOutput);
- }
-
- // Generate test extension file if requested
- if (generateTest) {
- const testCode = generateTestExtension(result);
-
- // Ensure the output directory exists
- if (!fs.existsSync(testOutputDir)) {
- fs.mkdirSync(testOutputDir, { recursive: true });
- }
-
- const testFilePath = path.join(testOutputDir, 'testex.js');
- fs.writeFileSync(testFilePath, testCode);
- console.log(`Test extension file generated: ${testFilePath}`);
- }
-
- // Print warnings in the requested format
- if (warnings.length > 0) {
- // Collect duplicate events
- const duplicateEvents = new Set();
- const overrideEvents = new Set();
- const otherWarnings = [];
-
- warnings.forEach(warning => {
- if (warning.includes("Duplicate event")) {
- // Extract event ID from the warning message
- const match = warning.match(/Duplicate event (core\.[^ ]+)/);
- if (match && match[1]) {
- duplicateEvents.add(match[1]);
- }
- } else if (warning.includes("already defined in manual overrides")) {
- // Extract event ID from the warning message
- const match = warning.match(/Event (core\.[^ ]+) found/);
- if (match && match[1]) {
- overrideEvents.add(match[1]);
- }
- } else {
- otherWarnings.push(warning);
- }
- });
-
- // Output in the requested format
- console.log(`\nduplicate events: ${Array.from(duplicateEvents).join(', ')}`);
- console.log(`Override events: ${Array.from(overrideEvents).join(', ')}`);
-
- // If there are any other warnings, print them too
- if (otherWarnings.length > 0) {
- console.log("\nOther warnings:");
- otherWarnings.forEach(warning => {
- console.log(`- ${warning}`);
- });
- }
- }
- }
- /**
- * Format the events data to match the events.json.js format
- */
- function formatEventsOutput(events) {
- let output = 'export default [\n';
-
- events.forEach((event, index) => {
- // Check if this is a manual override
- if (event.fromManualOverride) {
- // This is a manual override, output it exactly as defined
- output += ' {\n';
- output += ` id: '${event.id}',\n`;
- output += ` description: \``;
-
- // Format the description with proper indentation, preserving original formatting
- // Don't add extra newlines before or after the description
- output += event.description;
-
- output += `\`,\n`;
-
- // Add properties if they exist, preserving exact format
- if (event.properties && Object.keys(event.properties).length > 0) {
- output += ' properties: {\n';
-
- Object.entries(event.properties).forEach(([propName, propDetails], propIndex) => {
- output += ` ${propName}: {\n`;
- output += ` type: '${propDetails.type}',\n`;
- output += ` mutability: '${propDetails.mutability}',\n`;
- output += ` summary: '${propDetails.summary}'`;
-
- // Add notes array if it exists
- if (propDetails.notes && propDetails.notes.length > 0) {
- output += `,\n notes: [\n`;
- propDetails.notes.forEach((note, noteIndex) => {
- output += ` '${note}'`;
- if (noteIndex < propDetails.notes.length - 1) {
- output += ',';
- }
- output += '\n';
- });
- output += ` ]`;
- }
-
- output += '\n }';
-
- // Add comma if not the last property
- if (propIndex < Object.keys(event.properties).length - 1) {
- output += ',';
- }
-
- output += '\n';
- });
-
- output += ' },\n';
- }
-
- // Add example if it exists
- if (event.example) {
- output += ' example: {\n';
- output += ` language: '${event.example.language}',\n`;
- output += ` code: /*${event.example.language}*/\``;
-
- // Preserve the exact formatting of the example code
- // Don't add extra newlines and preserve escape sequences exactly as they are
- output += event.example.code;
-
- output += `\`\n`;
- output += ' },\n';
- }
-
- output += ' }';
- } else {
- // This is an auto-generated event
- output += ' {\n';
- output += ` id: '${event.id}',\n`;
- output += ` description: \`\n`;
-
- // Format the description with proper indentation
- const descriptionLines = event.description.split('\n');
- descriptionLines.forEach(line => {
- output += ` ${line}\n`;
- });
-
- output += ` \`,\n`;
-
- // Add properties if they exist
- if (Object.keys(event.properties).length > 0) {
- output += ' properties: {\n';
-
- Object.entries(event.properties).forEach(([propName, propDetails], propIndex) => {
- output += ` ${propName}: {\n`;
- output += ` type: '${propDetails.type}',\n`;
- output += ` mutability: '${propDetails.mutability === 'effect' ? 'mutable' : 'no-effect'}',\n`;
- output += ` summary: '${propDetails.summary}',\n`;
-
- // Add notes array with appropriate content
- if (propName === 'allow' && event.event.includes('validate')) {
- output += ` notes: [\n`;
- output += ` 'If set to false, the ${event.event.split('.')[0]} will be considered invalid.',\n`;
- output += ` ],\n`;
- } else if (propName === 'email' && event.event.includes('validate')) {
- output += ` notes: [\n`;
- output += ` 'The email may have already been cleaned.',\n`;
- output += ` ],\n`;
- } else {
- output += ` notes: [],\n`;
- }
-
- output += ' }';
-
- // Add comma if not the last property
- if (propIndex < Object.keys(event.properties).length - 1) {
- output += ',';
- }
-
- output += '\n';
- });
-
- output += ' },\n';
- }
-
- output += ' }';
- }
-
- // Add comma if not the last event
- if (index < events.length - 1) {
- output += ',';
- }
-
- output += '\n';
- });
-
- output += '];\n';
-
- return output;
- }
- main();
- // Updated Sun Mar 9 23:52:51 EDT 2025
|