145 lines
4.7 KiB
JavaScript
145 lines
4.7 KiB
JavaScript
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
|