Skip to content

Latest commit

 

History

History

plugin-lint

Black Lives Matter! Last commit timestamp Codecov Source license Monthly Downloads NPM version Uses Semantic Release!

@projector-js/plugin-lint

See the usage section for more information.

  • This opinionated CLI tool checks a Node.js project for correctness. It should be run after the project has been built. TypeScript (tsc) is used for type checking, ESLint and Babel for static analysis of JavaScript/TypeScript build output, and Remark and mdast for analysis of Markdown source. Further checks are performed to ensure the project is optimally structured and conforms to best practices, including detecting when running in a monorepo root vs a polyrepo root vs a sub-root.

Install

npm install --save-dev @projector-js/plugin-lint

Usage

Standalone usage:

npx @projector-js/plugin-lint

Or, if configured to work with Projector:

npm run lint

CLI Options

Help text (use --help to get the most up-to-date version):

plugin-lint

Check a project for correctness.

Options:
  --help                  Show help                                    [boolean]
  --version               Show version number                          [boolean]
  --silent                Nothing will be printed to stdout or stderr
                                                      [boolean] [default: false]
  --root-dir              The project root directory containing ESLint and
                          TypeScript configuration files, and that relative
                          paths and globs are resolved against. This must be an
                          absolute path.       [string] [default: process.cwd()]
  --md-path               Absolute paths, relative paths, and/or globs that
                          resolve to one or more markdown files. If a single
                          argument ending in "/" is given, the default glob
                          pattern will be appended to this argument instead.
                             [array] [default: .md files not under node_modules]
  --project               An absolute or relative path to, or file name of, a
                          TypeScript tsconfig.json configuration file. Source
                          paths are determined using this file's "files,"
                          "include," and "exclude" fields with all file
                          extensions recognized by the TypeScript compiler
                          considered.   [string] [default: "tsconfig.lint.json"]
  --pre-push-only         In pre-push mode, a limited subset of checks are
                          performed. Pre-push linting mode is meant to be
                          invoked by the "pre-push" Git hook.
                                                      [boolean] [default: false]
  --link-protection-only  In link protection mode, a limited subset of checks
                          are performed. Link protection linting mode is meant
                          to be invoked after potentially-destructive operations
                          on Markdown files (e.g. via Remark) to check for links
                          that have been accidentally disabled.
                                                      [boolean] [default: false]

API

Importing CLI as a Module

This package can be imported and run directly in source without spawning a child process or calling a CLI. This is useful for, for instance, composing multiple yargs-based CLI tools together or invoking the "link protection" stand-alone checks.

import { configureProgram } from '@projector/plugin-lint';
import { checkForPotentiallyDisabledLinks } from '@projector/plugin-lint/utils';

const { program, parse } = configureProgram();
// `program` is a yargs instance
// `parse` is an async function that will (eventually) call program.parse(...)
await parse(['--help']);

// This package also exposes some of its internal utils for external use
await checkForPotentiallyDisabledLinks();

Examples

Related

Appendix

Further documentation can be found under docs/.

List of Correctness Checks

Specifically, in addition to type checking and static analysis with tsc and ESLint, the following checks are performed:

  • ⛔ Errors when the project is not a git repository
  • ⛔ Errors when the package.json file is missing or unparsable
  • ⛔ Errors when the dist directory or its subdirectories contain .tsbuildinfo files
  • ⛔ Errors when package.json does not contain description, homepage, repository, license, author, engines, or type fields
    • When linting a monorepo root, the check for description is skipped
  • ⛔ Errors when package.json does not contain name, version, keywords, sideEffects, exports, files, or publishConfig fields
    • When linting a monorepo root, or if the private field exists and is set to true, this check is skipped
  • ⛔ Errors when the same dependency appears under both dependencies and devDependencies fields in package.json
  • ⛔ Errors when package.json contains the files field but its array is missing "/dist", "/LICENSE", "/package.json", or "/README.md" elements
  • ⛔ Errors when package.json is missing the exports["./package"] or exports["./package.json"] fields, or if they point to files that do not exist
  • ⛔ Errors when missing LICENSE or README.md files
  • ⛔ ⹋ Errors when an unpublished git commit has "fixup" or "mergeme" in its subject
    • This is evidence that the commit tree needs to be cleaned up before changes are merged upstream!
  • ⛔ Errors when any exports or typesVersions entry points in package.json point to files that do not exist
  • ⛔ ⹋⹋ Errors when any Markdown file matching the provided --md-path glob(s) contain disabled links
    • This check can be skipped for specific files by setting config['plugin-lint']['link-protection'].ignore = ['relative/path/or/glob'] in package.json
  • ⛔ Errors when depending on a non-pinned pre-release package version (like "^x.y.z-alpha.1", which should instead be "x.y.z-alpha.1")
    • This is dangerous enough to warrant an error instead of a warning since pre-release versions can differ radically from version to version and should therefore be pinned
    • Pinned pre-release package versions will still trigger a "pinned package version" warning (below), as they should
  • ⛔ † Errors when a source file contains an import but does not list the imported package in the package.json dependencies, peerDependencies, or optionalDependencies fields
    • Also checks for unlisted cross-dependencies when linting a monorepo
    • Self-referential imports are excluded from this check
    • Checks of type imports additionally consider the package.json devDependencies field
    • Checks of source files ending in .test.ts (or any other supported extension), source files with /test/ or /__test__/ in their path, or source files located at a package root additionally consider the package.json devDependencies field. These rules can be overwritten by setting config['plugin-lint']['imports'].considerDevDeps = ['relative/path/or/glob'] in package.json
  • ⚠️ Warns when one or more package.json files exist at a location that is not at the project root or, when linting a monorepo, at a sub-root
  • ⚠️ Warns when missing tsconfig.json, tsconfig.docs.json, tsconfig.eslint.json, tsconfig.lint.json, or tsconfig.types.json files
    • When linting a monorepo root, only tsconfig.json, tsconfig.lint.json, and tsconfig.eslint.json are checked for existence
    • When linting a monorepo sub-root, only tsconfig.docs.json, tsconfig.lint.json, and tsconfig.types.json are checked for existence
  • ⚠️ Warns when package.json license field is not "MIT"
  • ⚠️ Warns when package.json contains an experimental version (i.e. <1.0.0)
    • This INCLUDES the obsoleted "placeholder" version 0.0.0-development
  • ⚠️ Warns when package.json contains the outdated main, module, types, or typesVersions fields
    • Use exports instead
  • ⚠️ Warns when package.json contains the engines field but is missing the engines.node field, or if it is not set to the maintained and LTS versions of Node.js
    • For example: { "engines": { "node": "^12.22.0 || ^14.19.0 || ^16.13.0 || >=17.4.0" }} (as of Feb 2022)
  • ⚠️ Warns when package.json is missing the scripts field
  • ⚠️ Warns when package.json scripts field contains a script name equal to or starting with any of the following: test-integration-webpack, test-integration-browser, prepublishOnly, postpublish, repl, preinstall, postinstall, fixup, check-types, or publishGuard
  • ⚠️ Warns when package.json scripts field is missing one of the following script names: build, build-changelog, build-dist, build-docs, build-stats, clean, format, lint, lint-all, list-tasks, prepare, test, test-all, test-integration, test-repeat-all, test-repeat-unit, or test-unit
    • When linting a monorepo root, existence checks for the build, build-changelog, build-dist, build-docs, and build-stats script names are skipped
    • When linting a monorepo sub-root, existence checks for the prepare script name is skipped
    • If a next.config.js file exists at the project root, existence checks for the following script names are additionally performed: dev, start, test-e2e
  • ⚠️ Warns when depending on a pinned package version (like "x.y.z", which should instead be "^x.y.z")
  • ⚠️ Warns when depending on a dist-tag package version (like "next" or "latest") instead of a proper semver (like "~x.y.z")
  • ⚠️ Warns when package.json is missing the config['plugin-build'].docs.entry field, or if it points to a file that does not exist
  • ⚠️ ‡ Warns when README.md does not contain the standard badge topmatter, or when said topmatter is pointing to the wrong package name and/or repo uri
    • When linting a monorepo, what is considered "standard topmatter" changes depending on the current working directory being within the project root versus a sub-root
  • ⚠️ ‡ Warns when standard links in README.md are missing, or are pointing to the wrong package name and/or repo uri
    • When linting a monorepo, what is considered "standard links" changes depending on the current working directory being within the project root vs a sub-root

These additional checks are performed if the current project is a monorepo:

  • ⛔ Errors when a sub-root package.json file is unparsable
  • ⛔ Errors when a package shares the same package.json name field as another package in the monorepo
  • ⛔ Errors when an unnamed package shares the same package-id as another unnamed package in the monorepo

These additional checks are performed except when linting a sub-root:

  • ⚠️ Warns when any of the following files are missing:
    • .codecov.yml
    • .editorconfig
    • .eslintrc.js
    • .gitattributes
    • .gitignore
    • .prettierignore
    • .spellcheckignore
    • babel.config.js
    • commitlint.config.js
    • conventional.config.js
    • jest.config.js
    • lint-staged.config.js
    • webpack.config.js
    • prettier.config.js
    • CONTRIBUTING.md
    • SECURITY.md
    • .github/ISSUE_TEMPLATE/BUG_REPORT.md
    • .github/ISSUE_TEMPLATE/config.yml
    • .github/ISSUE_TEMPLATE/FEATURE_REQUEST.md
    • .github/CODE_OF_CONDUCT.md
    • .github/CODEOWNERS
    • .github/dependabot.yml
    • .github/pipeline.config.js
    • .github/PULL_REQUEST_TEMPLATE.md
    • .github/SUPPORT.md
  • ⚠️ Warns when missing the release.config.js file
    • If the package.json private field exists and is set to true, this check is skipped
  • ⚠️ Warns when missing the .github, .github/ISSUE_TEMPLATE, .github/workflows, .husky, or types directories
  • ⚠️ Warns when the contents of CONTRIBUTING.md, SECURITY.md, .github/SUPPORT.md, .github/CODE_OF_CONDUCT.md, .github/PULL_REQUEST_TEMPLATE.md, .github/ISSUE_TEMPLATE/BUG_REPORT.md, .github/ISSUE_TEMPLATE/FEATURE_REQUEST.md, .github/ISSUE_TEMPLATE/config.yml, or .github/dependabot.yml differ from their latest blueprints
  • ⚠️ ‡ Warns when SECURITY.md or .github/SUPPORT.md topmatter is pointing to the wrong package name and/or repo uri
  • ⚠️ ‡ Warns when standard links in CONTRIBUTING.md, SECURITY.md, or .github/SUPPORT.md are pointing to the wrong package name and/or repo uri

These additional checks are performed only if linting a monorepo root:

  • ⛔ Errors when the package.json workspaces field contains a path that points to a directory without a package.json file
  • ⚠️ Warns when package.json contains dependencies or version fields (0.0.0-monorepo is allowed)
    • Since the typical root package of a monorepo is only encountered in development, any dependencies should always be devDependencies
    • If a next.config.js file exists at the project root, this check is skipped
  • ⚠️ Warns when package.json is missing the private field or if it is not set to true
  • ⚠️ Warns when package.json is missing the name field
  • All valid sub-roots defined in the package.json workspaces field are recursively linted

These additional checks are performed only if linting a sub-root:

  • ⛔ † Errors when a source file contains an import of a package from the same monorepo without using the pkgverse alias
    • Using a pkgverse alias, which is transformed into a cross-dependency import when built for production, allows Jest tests to run using the direct source code rather than the build artifacts when working in monorepos
  • ⛔ † Errors when a pkgverse alias does not have a matching entry point in the package.json exports field
  • ⚠️ Warns when package.json is missing the config['plugin-build'].codecov.flag field
  • ⚠️ Warns when package.json contains devDependencies
    • These should be located in the project root's package.json file instead

† This check is performed using Babel AST static analysis. Dynamic imports and requires are not checked. Files ending in .js (when the package's package.json type field equals "commonjs"), .jsx, .cjs, .cts, and .d.* are excluded from these checks.
‡ This check is performed using mdast-util-from-markdown AST static analysis.
⹋ When in pre-push mode (--pre-push-only), only these checks are performed. All others are skipped.
⹋⹋ When in link protection mode (--link-protection-only), only these checks are performed. All others are skipped.

Published Package Details

This is a CJS2 package with statically-analyzable exports built by Babel for Node14 and above.

Expand details

That means both CJS2 (via require(...)) and ESM (via import { ... } from ... or await import(...)) source will load this package from the same entry points when using Node. This has several benefits, the foremost being: less code shipped/smaller package size, avoiding dual package hazard entirely, distributables are not packed/bundled/uglified, and a less complex build process.

Each entry point (i.e. ENTRY) in package.json's exports[ENTRY] object includes one or more export conditions. These entries may or may not include: an exports[ENTRY].types condition pointing to a type declarations file for TypeScript and IDEs, an exports[ENTRY].module condition pointing to (usually ESM) source for Webpack/Rollup, an exports[ENTRY].node condition pointing to (usually CJS2) source for Node.js require and import, an exports[ENTRY].default condition pointing to source for browsers and other environments, and other conditions not enumerated here. Check the package.json file to see which export conditions are supported.

Though package.json includes { "type": "commonjs" }, note that any ESM-only entry points will be ES module (.mjs) files. Finally, package.json also includes the sideEffects key, which is false for optimal tree shaking.

License

See LICENSE.

Contributing and Support

New issues and pull requests are always welcome and greatly appreciated! 🤩 Just as well, you can star 🌟 this project to let me know you found it useful! ✊🏿 Thank you!

See CONTRIBUTING.md and SUPPORT.md for more information.

Contributors

See the table of contributors.