scripts/bitrot.js (188 lines of code) (raw):

#!/usr/bin/env node /* * Copyright Elasticsearch B.V. and contributors * SPDX-License-Identifier: Apache-2.0 */ const {execSync} = require('child_process'); const fs = require('fs'); const path = require('path'); const semver = require('semver'); const ETEL_PJ_PATH = path.resolve( __dirname, '..', 'packages', 'opentelemetry-node', 'package.json' ); const SKIP_INSTR_NAMES = [ '@opentelemetry/instrumentation-aws-lambda', // supported versions isn't meaningful '@opentelemetry/instrumentation-redis', // the separate 'instrumentation-redis-4' handles the latest versions ]; const QUIET = true; let gRotCount = 0; // ---- caching const gCachePath = '/tmp/eon-bitrot.cache.json'; let gCache = null; function ensureCacheLoaded(ns) { if (gCache === null) { try { gCache = JSON.parse(fs.readFileSync(gCachePath)); } catch (loadErr) { gCache = {}; } } if (!(ns in gCache)) { gCache[ns] = {}; } return gCache[ns]; } function saveCache() { if (gCache !== null) { fs.writeFileSync(gCachePath, JSON.stringify(gCache, null, 2)); } } // ---- minimal ANSI styling support (from bunyan) // http://en.wikipedia.org/wiki/ANSI_escape_code#graphics // Suggested colors (some are unreadable in common cases): // - Good: cyan, yellow (limited use, poor visibility on white background), // bold, green, magenta, red // - Bad: blue (not visible on cmd.exe), grey (same color as background on // Solarized Dark theme from <https://github.com/altercation/solarized>, see // issue #160) var colors = { bold: [1, 22], italic: [3, 23], underline: [4, 24], inverse: [7, 27], white: [37, 39], grey: [90, 39], black: [30, 39], blue: [34, 39], cyan: [36, 39], green: [32, 39], magenta: [35, 39], red: [31, 39], yellow: [33, 39], }; function stylizeWithColor(str, color) { if (!str) { return ''; } var codes = colors[color]; if (codes) { return '\x1B[' + codes[0] + 'm' + str + '\x1B[' + codes[1] + 'm'; } else { return str; } } let stylize = stylizeWithColor; // ---- support functions function rot(moduleName, s) { gRotCount++; console.log(`${stylize(moduleName, 'bold')} bitrot: ${s}`); } function getNpmInfo(name) { const CACHE_TIMEOUT_MS = 30 * 60 * 1000; // 30 minutes const cache = ensureCacheLoaded('npmInfo'); const cacheEntry = cache[name]; if (cacheEntry) { if (cacheEntry.timestamp + CACHE_TIMEOUT_MS > Date.now()) { return cacheEntry.value; } else { delete cache[name]; } } // Limited security guard on exec'ing given `name`. const PKG_NAME_RE = /^(@[\w_.-]+\/)?([\w_.-]+)$/; if (!PKG_NAME_RE.test(name)) { throw new Error( `${JSON.stringify( name )} does not look like a valid npm package name` ); } const stdout = execSync(`npm info -j "${name}"`).toString('utf8'); const npmInfo = JSON.parse(stdout); cache[name] = { timestamp: Date.now(), value: npmInfo, }; saveCache(); return npmInfo; } function bitrot() { const pj = JSON.parse(fs.readFileSync(ETEL_PJ_PATH, 'utf8')); const instrNames = Object.keys(pj.dependencies).filter((d) => d.startsWith('@opentelemetry/instrumentation-') ); const ainPj = getNpmInfo('@opentelemetry/auto-instrumentations-node'); const ainInstrNames = Object.keys(ainPj.dependencies).filter((d) => d.startsWith('@opentelemetry/instrumentation-') ); for (let instrName of ainInstrNames) { if (SKIP_INSTR_NAMES.includes(instrName)) continue; if (!instrNames.includes(instrName)) { rot(instrName, 'missing instr that auto-instrumentations-node has'); } } for (let instrName of instrNames) { if (SKIP_INSTR_NAMES.includes(instrName)) continue; if (!QUIET) console.log(`${instrName}:`); const mod = require(instrName); const instrClass = Object.keys(mod).filter((n) => n.endsWith('Instrumentation') )[0]; const instr = new mod[instrClass](); const initVal = instr.init(); // grpc is weird here if (initVal === undefined) { if (!QUIET) console.log(` (instr.init() returned undefined!)`); continue; } const instrNodeModuleFiles = Array.isArray(initVal) ? initVal : [initVal]; const supVersFromModName = {}; for (let inmf of instrNodeModuleFiles) { // TODO: warn if supportedVersions range is open-ended. E.g. if it satisfies 9999.9999.9999 or something. if (!QUIET) console.log( ` ${inmf.name}: ${JSON.stringify( inmf.supportedVersions )}` ); // TODO: keep printing these? Do they ever matter? for (let file of inmf.files) { if (!QUIET) console.log( ` ${file.name}: ${JSON.stringify( file.supportedVersions )}` ); } if (!supVersFromModName[inmf.name]) { supVersFromModName[inmf.name] = []; } supVersFromModName[inmf.name].push(inmf.supportedVersions); } for (let modName of Object.keys(supVersFromModName)) { const supVers = supVersFromModName[modName].flat(); if (supVers.toString() === '*') { // This is code for "node core module". continue; } const info = getNpmInfo(modName); const latest = info['dist-tags'].latest; if (!QUIET) console.log(` latest published: ${modName}@${latest}`); let supsLatest = false; for (let range of supVers) { if (semver.satisfies(latest, range)) { supsLatest = true; } } if (!supsLatest) { rot( instrName, `supportedVersions of module "${modName}" (${JSON.stringify( supVers )}) do not support the latest published ${modName}@${latest}` ); } } } } function main(argv) { bitrot(); if (gRotCount > 0) { process.exit(3); } } if (require.main === module) { main(process.argv); }