diff --git a/scripts/.eslintrc.js b/scripts/.eslintrc.js new file mode 100644 index 0000000000..106fa4246a --- /dev/null +++ b/scripts/.eslintrc.js @@ -0,0 +1,6 @@ +module.exports = { + extends: [require.resolve('@backstage/cli/config/eslint')], + rules: { + 'no-console': 0, + }, +}; diff --git a/scripts/check-type-dependencies.js b/scripts/check-type-dependencies.js new file mode 100755 index 0000000000..5b7daa13d2 --- /dev/null +++ b/scripts/check-type-dependencies.js @@ -0,0 +1,194 @@ +#!/usr/bin/env node +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +const fs = require('fs'); +const { resolve: resolvePath } = require('path'); +// Cba polluting root package.json, we'll have this +// eslint-disable-next-line import/no-extraneous-dependencies +const chalk = require('chalk'); + +async function main() { + // This is from lerna, and cba polluting root package.json + // eslint-disable-next-line import/no-extraneous-dependencies + const LernaProject = require('@lerna/project'); + const project = new LernaProject(resolvePath('.')); + const packages = await project.getPackages(); + + let hadErrors = false; + + for (const pkg of packages) { + if (!shouldCheckTypes(pkg)) { + continue; + } + const { errors } = await checkTypes(pkg); + if (errors.length) { + hadErrors = true; + console.error( + `Incorrect type dependencies in ${chalk.yellow(pkg.name)}:`, + ); + for (const error of errors) { + if (error.name === 'WrongDepError') { + console.error( + ` Move from ${chalk.red(error.from)} to ${chalk.green( + error.to, + )}: ${chalk.cyan(error.dep)}`, + ); + } else if (error.name === 'MissingDepError') { + console.error( + ` Missing a type dependency: ${chalk.cyan(error.dep)}`, + ); + } else { + console.error(` Unknown error, ${chalk.red(error)}`); + } + } + } + } + + if (hadErrors) { + console.error(); + console.error( + chalk.red('At least one package had incorrect type dependencies'), + ); + + process.exit(2); + } +} + +function shouldCheckTypes(pkg) { + return !pkg.private && pkg.get('types'); +} + +/** + * Scan index.d.ts for imports and return errors for any dependency that's + * missing or incorrect in package.json + */ +function checkTypes(pkg) { + const typeDecl = fs.readFileSync( + resolvePath(pkg.location, 'dist/index.d.ts'), + 'utf8', + ); + const deps = (typeDecl.match(/from '.*'/g) || []) + .map(match => match.replace(/from '(.*)'/, '$1')) + .filter(n => !n.startsWith('.')); + + const errors = []; + const typeDeps = []; + for (const dep of deps) { + try { + const typeDep = findTypesPackage(dep, pkg); + if (typeDep) { + typeDeps.push(typeDep); + } + } catch (error) { + errors.push(error); + } + } + + errors.push(...findTypeDepErrors(typeDeps, pkg)); + + return { errors }; +} + +/** + * Find the package used for types. This assumes that types are working is a package + * can be resolved, it doesn't do any checking of presence of types inside the dep. + */ +function findTypesPackage(dep, pkg) { + try { + require.resolve(`@types/${dep}/package.json`, { paths: [pkg.location] }); + return `@types/${dep}`; + } catch { + try { + require.resolve(dep, { paths: [pkg.location] }); + return undefined; + } catch { + try { + // Some type-only modules don't have a working main field, so try resolving package.json too + require.resolve(`${dep}/package.json`, { paths: [pkg.location] }); + return undefined; + } catch { + try { + // Finally check if it's just a .d.ts file + require.resolve(`${dep}.d.ts`, { paths: [pkg.location] }); + return undefined; + } catch { + throw mkErr('MissingDepError', `No types for ${dep}`, { dep }); + } + } + } + } +} + +/** + * Figures out what type dependencies are missing, or should be moved between dep types + */ +function findTypeDepErrors(typeDeps, pkg) { + const devDeps = mkTypeDepSet(pkg.get('devDependencies')); + const deps = mkTypeDepSet(pkg.get('dependencies')); + + const errors = []; + for (const typeDep of typeDeps) { + if (!deps.has(typeDep)) { + if (devDeps.has(typeDep)) { + errors.push( + mkErr('WrongDepError', `Should be dep ${typeDep}`, { + dep: typeDep, + from: 'devDependencies', + to: 'dependencies', + }), + ); + } else { + errors.push( + mkErr('MissingDepError', `No types for ${typeDep}`, { + dep: typeDep, + }), + ); + } + } else { + deps.delete(typeDep); + } + } + + for (const dep of deps) { + errors.push( + mkErr('WrongDepError', `Should be dev dep ${dep}`, { + dep, + from: 'dependencies', + to: 'devDependencies', + }), + ); + } + + return errors; +} + +function mkTypeDepSet(deps) { + const typeDeps = Object.keys(deps || {}).filter(n => n.startsWith('@types/')); + return new Set(typeDeps); +} + +function mkErr(name, msg, extra) { + const error = new Error(msg); + error.name = name; + Object.assign(error, extra); + return error; +} + +main().catch(error => { + console.error(error.stack || error); + process.exit(1); +});