import fs from 'node:fs' import path from 'node:path' import ts from 'typescript' const root = path.resolve(import.meta.dirname, '..') const files = [] function walk(dir) { for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { if (['.git', 'node_modules'].includes(entry.name)) continue const file = path.join(dir, entry.name) if (entry.isDirectory()) walk(file) else if (/\.(ts|tsx|mjs)$/.test(file)) files.push(file) } } walk(root) const errors = [], routes = [], columns = new Map() function value(node, env) { if (!node) return undefined if (ts.isAsExpression(node) || ts.isParenthesizedExpression(node)) return value(node.expression, env) if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) return node.text if (ts.isIdentifier(node)) return env[node.text] if (ts.isArrayLiteralExpression(node)) return node.elements.map((n) => value(n, env)) if (ts.isBinaryExpression(node) && node.operatorToken.kind === ts.SyntaxKind.PlusToken) { const a = value(node.left, env), b = value(node.right, env) return typeof a === 'string' && typeof b === 'string' ? a + b : undefined } } function normalize(route) { return route.replace(/\{[^}]+\}|:[^/]+/g, ':param') } for (const file of files) { const source = ts.createSourceFile( file, fs.readFileSync(file, 'utf8'), ts.ScriptTarget.Latest, true, ) for (const d of source.parseDiagnostics) errors.push( path.relative(root, file) + ': ' + ts.flattenDiagnosticMessageText(d.messageText, ' '), ) function visit(node, env = {}) { if (ts.isForOfStatement(node) && ts.isVariableDeclarationList(node.initializer)) { const name = node.initializer.declarations[0].name, values = value(node.expression, env) if (ts.isIdentifier(name) && Array.isArray(values)) { for (const item of values) visit(node.statement, { ...env, [name.text]: item }) return } } if ( ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && ts.isIdentifier(node.expression.expression) && node.expression.expression.text === 'app' && ['get', 'post', 'put', 'patch', 'delete'].includes(node.expression.name.text) ) { const route = value(node.arguments[0], env) if (route) routes.push({ method: node.expression.name.text.toUpperCase(), path: route, file: path.relative(root, file), }) } if ( file.endsWith('/packages/database/src/schema.ts') && ts.isCallExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === 'pgTable' && ts.isStringLiteral(node.arguments[0]) && ts.isObjectLiteralExpression(node.arguments[1]) ) columns.set( node.arguments[0].text, node.arguments[1].properties .filter(ts.isPropertyAssignment) .map((p) => p.name.getText(source).replaceAll(/['"]/g, '')), ) ts.forEachChild(node, (child) => visit(child, env)) } visit(source) } const expected = JSON.parse(fs.readFileSync(path.join(root, 'migration/routes.json'), 'utf8')) for (const route of expected) if (!routes.some((r) => r.method === route.method && normalize(r.path) === normalize(route.path))) errors.push('Missing API route ' + route.method + ' ' + route.path) const legacyColumns = JSON.parse( fs.readFileSync(path.join(root, 'migration/database-columns.json'), 'utf8'), ) for (const [table, names] of Object.entries(legacyColumns)) for (const name of names) if (!columns.get(table)?.includes(name)) errors.push('Missing database column ' + table + '.' + name) const sourcePages = [ '/', '/en', '/en/trump', '/en/macro', '/en/kol', '/en/trades', '/en/analytics', '/en/settings', '/en/archive', '/en/posts', '/en/btc', '/en/methodology', '/en/glossary', '/en/case-studies', '/en/privacy', '/en/terms', '/en/contact', ] for (const route of sourcePages) { const relative = route === '/' ? 'index' : route === '/en' ? '[locale]/index' : route.replace('/en/', '[locale]/') if (!fs.existsSync(path.join(root, 'apps/web/pages', relative + '.tsx'))) errors.push('Missing page ' + route) } const report = { sourceFilesParsed: files.length, legacyApiHandlers: expected.length, registeredHttpRoutes: routes.length, pages: sourcePages.length, legacyTables: Object.keys(legacyColumns).length, legacyColumns: Object.values(legacyColumns).reduce((n, a) => n + a.length, 0), errors, applicationExecuted: false, buildExecuted: false, testsExecuted: false, } fs.writeFileSync( path.join(root, 'migration/static-review.json'), JSON.stringify(report, null, 2) + '\n', ) console.log(JSON.stringify(report, null, 2)) if (errors.length) process.exitCode = 1