Skip to content

Latest commit

 

History

History
215 lines (138 loc) · 14.5 KB

README.md

File metadata and controls

215 lines (138 loc) · 14.5 KB

✈️ nodejs-esm-starter

Starter project for NodeJs esm packages, with rollup, typescript, mocha, chai, eslint, istanbul/nyc, gulp, i18next

Commitizen friendly js-semistandard-style typescript

👑 This starter was created from the information gleaned from the excellent suite of articles written by 'Gil Tayar': Using ES Modules (ESM) in Node.js: A Practical Guide (Part 1), which I would highly recommend to anyone wishing to get a full understanding of ESM modules with NodeJS and provides the full picture lacking in other offical documentation sources/blogs. The following description contains links into the relevant parts of Gil Tayar's blog series.

🎁 package.json features

💎 ESM module

  "type": "module",

🎓 See: Using the .js extension for ESM

This entry makes the packaage an esm module and means that we don't have to use the .mjs extension to indicate a module is esm; doing so causes problems with some tooling.

💎 The 'exports' field

  "exports": {
    ".": "./src/main.js"
  },

🎓 See: The 'exports' field

The correct way to define a package's entry point in esm is to specify the exports field and it must start with a '.' as illustrated.

Using the exports field prevents deep linking into the package; we're are restricted to using the entry points defined in exports only.

✨ Self referencing

  "exports": {
    ".": "./src/main.js",
    "./package.json": "./package.json"
  },

🎓 See: Self referencing

This means we can use the name of the package on an import instead of a relative path, so a unit test could import like so:

import {banner} from 'nodejs-esm-starter'

However, there is still an issue with self referencing like this. typescript will appear not be able to resolve that the package name, but in reality there is no problem. Therefore, we need to disable the resultant error. This is achieved at the import site with a typescript directive as illustarted below:

// @ts-ignore
import {banner} from 'nodejs-esm-starter'

But that now throws up another issue. What we find now is that when we go to lint the project (just run npm run lint), we'll simply be served up an error message of the form:

4:1 error Do not use "@ts-ignore" because it alters compilation errors @typescript-eslint/ban-ts-comment

It is safe to disable this and we do so by turning off the ban-ts-comment rule in the .eslintrc.json config file inside the "rules" entry:

"@typescript-eslint/ban-ts-comment": "off",

✨ Multiple exports

This starter does not come with multiple exports; it would be up to the client package to define as required, but would look something like:

  "exports": {
    ".": "./src/main.js",
    "./red": "./src/main-red.js",
    "./blue": "./src/main-blue.js",
    "./package.json": "./package.json"
  },

🎓 See: Multiple exports

✨ Dual-mode libraries

This allows the module to be required synchronously by other commonjs packages or imported asynchronously by esm packages. This requires transpilation which we achieve by using rollup.

The '.' entry inside exports is what gives us this dual mode capability:

  "exports": {
    ".": {
      "require": "./lib/main.cjs",
      "import": "./src/main.js"
    },

🎓 See: Dual-mode libraries

NB: we write our rollup config in a .mjs file because rollup assumes .js is commonjs, so we are forced to use .mjs, regardless of the fact that our package has been marked as esm via the package.json type property.

📃 The 'files' entry
  "files": [
    "src",
    "lib"
  ],

🎓 See: Transpiling with Rollup

Required for dual-mode package.

📂 Boilerplate project structure

All rollup related funcitonality is contained within the rollup folder. Currently, there is a separate file for development and production. The main difference between the production and development rollup configs is that for the former, we use the terser plugin to mangle the generated javascript bundle.

📚 options/production/development

The setup is structured to keep the gulp config encapulated away from the rollup config. This means that the user can discard gulp if they so wish to without it affecting the rollup. The flow of data goes from the root, that being rollup/options.mjs, to either rollup.development.mjs or rollup.production.mjs dependending on the current mode which is then finally imported into the gulp file gulpfile.esm.mjs.

It is intended that the user should specify all generic settings in the options.mjs file and export them from there. This way, we can ensure that any properties are defined in a single place only and inherited as required. Clearly, production specific settings should go in the production file and like-wise for development.

🍻 gulp file (gulpfile.esm.mjs)

In order to simplify usage of gulp in the presence of the alternative gulfile name being gulpfile.esm.mjs (as opposed to the default of simply being gulpfile.mjs), a symbolic link has been defined from gulpfile.mjs to gulpfile.esm.mjs. This means that the user can run gulp commands without having to explicitly define the gulp file gulpfile.esm.mjs.

➕ Copying resources

The gulp file, contains an array definition resourceSpecs. By default it contains a single dummy entry that illustrates how to define resource(s) to be copied into the output folder. Each entry in the array should be an object eg:

  {
    name: "copy text file",
    source: "./src/text.txt",
    destination: `./${outDir}`
  }

A copyTask is defined composed from a series of tasks defined by resourceSpecs. If no resources are to be copied, then just remove this default entry and leave the array to be empty.

🚀 Using this template

After the client project has been created from this template, a number of changes need to be made and are listed as follows:

  • Update the name property inside package.json. Initially it will be set to nodejs-esm-starter. The user should perform a global search and replace inside package.js as there are other entries derived from this name. Ideally, there would be a way in json to be able to cross reference fields, but alas, this is not currently possible. The dummy unit tests also import the template project name, so these will have to be updated to use the real package name other wise the tests will fail due to an incorrect import.
  • add a .env file to the root of the project. This will be used to store secrets when the time comes for performing a release. Initially, the user can simply set the contents to:

GH_TOKEN=ADD-KEY-HERE

This can be taken literally, ie if you don't yet have a personal access token, then set it here to a dummy value

  • define resources to copy, if any.
  • remove the dummy tests and source dode.
  • ... and then of course, customise the configs as required.

🌐 i18next Translation ready

This template comes complete with the initial boilerplate required for integration with i18next. It has been set up with English UK (en) set as the default alongside English US (en-US). If so required, this setup can easily be changed and more languages added as appropriate.

If translation is not required, then it can be removed (dependencies: i18next and i18next-fs-backend) but it is highly recommended to leave it in. i18next can help in writing cleaner code. The biggest issue for users just starting with i18next is getting used to the idea that string literals should now never be used (see exceptions documented for the eslint-plugin-i18next plugin) and this will be made evident by the linting process; in particular, the user is likely to see violations of the i18next/no-literal-string rule.

The lint gulp task will flag up translation violations and another gulp task i18next has been implemented using i18next-parser, which helps with the process of maintaining translations as the code base evoles.

The i18next/no-literal-string should really only be applied to user facing text content. For this reason, the project has been setup to only apply the rule to typescript files inside the "src" directory and not to unit tests, which would have become too onerous to manage.

🤖 Automated releases

Releases have been automated using gulp's Automate Releases recipe. However, this is just an initial setup. The user should become accustomed with the following concepts:

To run the full release, just run npm run release. Two methods have been defined for completing an automated release, see the following:

📌 Gulp: this recipe recipe generates and publishes releases (including version number bumping, change log generation and tagging) to gihub. In it's current form, it does not publish to the npm registry, so the user will have to add this to the release chain. The gulp release has been defined as a script named "_gulp:rel"

📌 standard version: this is an alternative to what has been defined in the release gulp task and has been defined in package.json denoted by a script entry named "_standard:rel".

By default, release has been set to use "standard", but this can be switched to use the "gulp" version instead.

It should also be noted that there is a third way (not implemented mentioned here for reference), which is to use semantic release.

🚧 Required dev depenencies of note

  • 🔨 dual mode package rollup (npm)
  • 🔨 platform independent copy of non js assets cpr (npm)
  • 🔨 merge json objects (used to derive test rollup config from the source config) deepmerge (npm)
  • 🔨 type definitions for file system backend (npm) @types/i18next-fs-backend
  • 🔨 eslint plugin (npm)
  • 🔨 i18next-parser (npm)

⚠️ A note about 'vulnerablities' in dev dependencies

An issue was raised to try and resolve the problem of npm audit reporting so called vulnerabilities (mostly relating to gulp dependencies). However, after a lot of head scratching and many failed attempts to resolve, it was discovered that there is a design flaw with npm audit. This is a widely known issue and very well documented at a blog post npm audit: Broken by Design. It is for the reasons documented here, that there is no need to attempt to resolve these issues. A custom audit package.json script entry has been defined that specifies the --production flag, (just run npm run audit).

🏁 Other external resources

Here's a list of other links that were consulted duration the creation of this starter template

📺 Youtube: