import fs from 'node:fs'; import { createRequire } from 'node:module'; import path from 'node:path'; import { fileURLToPath, pathToFileURL } from 'node:url'; import type { EventDef } from '@suite-common/analytics'; import type { EventDoc } from '../src/types'; import { AttributeTypesByEventName, extractAttributeTypesByEventName, findPackageRoot, findUp, } from '../src/utils/extractAttributeTypes'; import { normalizeChangelog } from '../src/utils/normalizeChangelog'; import { normalizeEvents } from '../src/utils/normalizeEvents'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const cjsRequire = createRequire(import.meta.url); const repoRoot = path.resolve(__dirname, '../../..'); const PACKAGES = [ '@suite-common/analytics', '@suite/analytics', '@suite-native/analytics', ] as const; const PACKAGE_TO_PLATFORM: Record<(typeof PACKAGES)[number], string> = { '@suite-common/analytics': 'shared', '@suite/analytics': 'desktop', '@suite-native/analytics': 'mobile', }; /** Paths from repo root for packages we don't depend on (to avoid cyclic deps). */ const PACKAGE_TO_REL_PATH: Partial> = { '@suite/analytics': 'suite/analytics', '@suite-native/analytics': 'suite-native/analytics', }; const getPackageRoot = (packageName: (typeof PACKAGES)[number]): string => { const relPath = PACKAGE_TO_REL_PATH[packageName]; if (relPath) { return path.join(repoRoot, relPath); } return path.dirname(cjsRequire.resolve(`${packageName}/package.json`)); }; const loadEventsFromPackage = async ( packageName: (typeof PACKAGES)[number], ): Promise>> => { const packageRoot = getPackageRoot(packageName); const eventsPath = path.join(packageRoot, 'src', 'events', 'index.ts'); const module = await import(pathToFileURL(eventsPath).href); return Object.values(module) as Array>; }; const loadAllEvents = async (): Promise< Array & { platform: string }> > => { const results = await Promise.all( PACKAGES.map(async packageName => { const events = await loadEventsFromPackage(packageName); const platform = PACKAGE_TO_PLATFORM[packageName]; return events.map(event => ({ ...event, platform })); }), ); return results.flat(); }; const getTsConfigPath = (): string => { const docgenPath = path.resolve(__dirname, '../tsconfig.docgen.json'); if (fs.existsSync(docgenPath)) return docgenPath; const up = findUp('tsconfig.json', path.resolve(__dirname, '..')); return up ?? path.resolve(repoRoot, 'tsconfig.json'); }; const getPackageRoots = (): string[] => { const roots = PACKAGES.map(name => findPackageRoot(path.join(getPackageRoot(name), 'package.json')), ).filter((x): x is string => Boolean(x)); return [...new Set(roots)]; }; const getEventFileGlobs = (packageRoots: string[]): string[] => packageRoots.flatMap(root => [ path.join(root, 'src/**/*.{ts,tsx}'), path.join(root, 'dist/**/*.d.ts'), ]); /** Adds attribute docs for type-only keys (e.g. Record) so runtimeType can be merged. */ const ensureAttributeDocsFromTypes = ( events: Record, attributeTypesByEventName: AttributeTypesByEventName, ): void => { for (const [eventName, eventDoc] of Object.entries(events)) { const eventTypes = attributeTypesByEventName[eventName]; if (!eventTypes) continue; for (const attrName of Object.keys(eventTypes)) { if (attrName in eventDoc.attributes) continue; eventDoc.attributes[attrName] = { changelog: eventDoc.changelog ?? normalizeChangelog(), }; } } }; const mergeRuntimeTypes = ( events: Record, attributeTypesByEventName: AttributeTypesByEventName, ): void => { for (const [eventName, eventDoc] of Object.entries(events)) { const eventTypes = attributeTypesByEventName[eventName]; if (!eventTypes) continue; for (const [attrName, attrDoc] of Object.entries(eventDoc.attributes)) { const runtimeType = eventTypes[attrName]; if (runtimeType) attrDoc.runtimeType = runtimeType; } } }; const writeOutput = ( data: { events: Record; generatedAt: string }, outputPath: string, ): void => { const pretty = process.env.PRETTY_ANALYTICS_JSON === '1'; const json = pretty ? JSON.stringify(data, null, 2) : JSON.stringify(data); fs.writeFileSync(outputPath, json, 'utf-8'); }; const run = async () => { const allEvents = await loadAllEvents(); const normalizedEvents = normalizeEvents(allEvents); const tsConfigPath = getTsConfigPath(); const packageRoots = getPackageRoots(); const eventFileGlobs = getEventFileGlobs(packageRoots); const attributeTypesByEventName = extractAttributeTypesByEventName({ tsConfigFilePath: tsConfigPath, eventFileGlobs, }); ensureAttributeDocsFromTypes(normalizedEvents, attributeTypesByEventName); mergeRuntimeTypes(normalizedEvents, attributeTypesByEventName); const publicDir = path.resolve(__dirname, '../public'); fs.mkdirSync(publicDir, { recursive: true }); const outputPath = path.join(publicDir, 'analytics.json'); const generatedAt = new Date().toISOString(); writeOutput({ events: normalizedEvents, generatedAt }, outputPath); // eslint-disable-next-line no-console console.log( `[analytics-docs] analytics.json generated (${Object.keys(normalizedEvents).length} events)`, ); }; void run();