Skip to content

Latest commit

 

History

History
42 lines (34 loc) · 11.6 KB

creating_a_new_npm_package.md

File metadata and controls

42 lines (34 loc) · 11.6 KB

Creating a new NPM package

⚠️ Creating a new package can be hazardous to your health, and to the health of your teammates. ☠️

Please read the entirety of this document very carefully.

Before creating a new NPM package, please consider the costs:

  • Creating a new NPM package involves a lot of initial work which is described in the checklist further down, but in brief, it may include:
    • Creating a new repository, or a new package within an existing monorepo.
    • Setting up documentation (README, CONTRIBUTING, LICENSE etc). In the case of an independent project, this may even include making a marketing site, with all that that entails (working with GitHub pages or other platforms, registering domain names, coordinating with legal and IT departments to make arrangements and vet for possible trademark issues etc).
    • Setting up workflows for testing, linting, formatting, releasing, and possibly more.
    • Configuring appropriate access controls for publishing.
  • After the initial creation, there is an ongoing maintenance obligation:
    • If the package is part of a dependency graph, every update to the package (or its own transitive dependencies) may require cascading updates to other packages. For example: a bug-fix to liferay-js-toolkit-core, requires a release of that package, an update and release of liferay-npm-bundler (and other dependents), which in turn requires an update and release of liferay-npm-scripts in order to deliver the initial bug-fix to liferay-portal; when updates span separate repos as is the case in this example, the level of overhead is particularly high.
    • In order to maintain consistent build processes, testing procedures, CI integration, formatting conventions, linting, and so on across projects, we must update these everywhere whenever they change; the cost here is proportional to the number of packages and projects that we have.
    • Likewise, the burden of keeping up with security issues reported by yarn audit is roughly proportional to the number of packages we maintain.

In our experience, the benefits of modularity are far outweighed by the overhead costs. Worse still, the purported benefits of modularity (reuse, isolation, thoughtfully designed interfaces and contracts) tend not to materialize in practice: "reusable" components are often used in exactly one place, and we wind up with a accidental monolith of tightly coupled abstractions that would be easier to operate on if it were all stored in one place instead of artificially distributed across packages and repos.

In short, splitting something off into a separate package should be a last resort. There are some rare legitimate use cases (actual reuse in multiple contexts, or because you have to deliver software as a package in order to integrate with some third-party system that requires it, such as Babel or ESLint that implement extensibility via plug-ins), but your initial default approach should always be to start by colocating code inside some existing artefact. It is generally easier to extract something down the road, than it is to "put the genie back in the bottle" after it has been prematurely extracted.

With all that in mind, the following checklist contains things to verify when creating a new NPM package. It may seem that a lot of this could be made cheaper by creating tools and templates to make it easier to spin off a new package, but we don't actually want to make it too easy to create new packages. In other words, the friction is a feature, not a bug.

Checklist

  1. The proposal for the package has been vetted and approved: Before even starting, talk over the need for a new package with colleagues and submit an issue to the liferay-frontend-projects repo. Requiring a second (or third) opinion in support of the proposal will help us avoid a high-maintenance future in which we create packages which should never really have been packages.
  2. The package is in a discoverable location: That means under the the "liferay" GitHub organization. Historically, we released some packages under other organizations (eg. metal, but maintaining multiple organizations is another source of undesirable overhead. Don't host packages under your personal GitHub account or in forks. Generally speaking, most Liferay packages should start with the "liferay" prefix, unless some other convention dictates otherwise (eg. a Babel plug-in should start with "babel-plugin").
  3. The package contains a yarn.lock: This lockfile gets created by the Yarn package manager. We currently use Yarn v1.x.
  4. The package.json has correct metadata: A basic version can be created with yarn init, but for full details see the NPM reference.
  5. The package has an appropriate LICENSE.md file, a LICENSES directory, and SPDX headers in all source files: As an example, see the LICENSE.md in the liferay-npm-tools project, and note how a copy is stored in the top-level LICENSES directory as well, named according to the corresponding SPDX license identifier. We apply a standard SPDX header via ESLint. The copyright.js template is special because it has a trailing blank line, and it should be ignored by both ESlint and by Prettier.
  6. The package uses the Semantic Pull Requests Bot: This config ensures that all PR titles and commit messages conform to the Conventional Commits specification.
  7. The package has a lint script: Specifically, a script which runs ESLint using our standard eslint-config-liferay, and a corresponding lint:fix script that can be used to apply autofixes.
  8. The package has a format script: This script should format all source files with Prettier, and be accompanied by a format:check script that uses Prettier to report problems. In general, we use Prettier for all the file formats that it supports (CSS, JavaScript, JSON, Markdown, SCSS, TypeScript), and a typical config looks like this (ie. no bracket spacing, use single quotes, use tabs, tab width of 4).
  9. The package has a test script: This script should run Jest, which is our preferred test runner.
  10. The package has a ci script: This script should run any applicable checks and be run via GitHub actions (example); note that some projects with a legacy configuration still use Travis (example), but no new projects should use Travis.
  11. The package has a preversion script: This script should call the ci script (example).
  12. The package has README and CONTRIBUTING files: The primary audience of these Markdown files is usually (but not always) Liferay internal developers, so write them accordingly. The CONTRIBUTING.md especially should be targeted towards internal developers because, in practice, external contributions are rare and the primary utility of the document is to ensure that our team members know how to cut a release of the package (example).
  13. The package has a CHANGELOG: We use liferay-changelog-generator to produce changelogs based on merge commits between tags; note that to avoid churn caused by version updates, you should run it with npx liferay-changelog-generator rather than adding it as an explicit dependency to the project.
  14. The package has liferay ownership: After the first publication, use npm owner add liferay to ensure that other team members can take over maintenance if necessary. As a second step here, somebody with access to the liferay user should add the package to the liferay-frontend organization's developers team, at which point you should see all of the team members listed under "Collaborators" on the package's NPM page (example).
  15. The package uses liferay-js-publish for publishing: liferay-js-publish is a simple script invoked from a package.json postversion script (example), which means that we can cut releases by running a command like yarn version --minor.
  16. The package has a .yarnrc file: The information in this file is used by Yarn and by liferay-changelog-generator during releases. In a single-package repository it should look something like this, with a tag prefix of v. In a monorepo where packages are released independently, each package has its own .yarnrc; as seen in this example the tag prefix includes the package name followed by /v (eg. liferay-changelog-generator/v). In repos that do all-at-once releases, no .yarnrc need be provided but you may still want one in order to do things like set the NPM tag on maintenance branches (example).