Pacharapol Withayasakpunt Pacharapol Withayasakpunt
Tue 7 January 2020

Creating an NPM repo with browser version, and also with testing and linting

This can be done by setting up,

  • Multiple tsconfig.json
  • /browser.ts compiled by Parcel.js to /umd/index.min.js
  • ESLint, with eslint --init
  • Testing, with ts-mocha

So, the project is basically like this,

.
├── .eslintrc.js
├── browser.ts
├── package.json
├── src
│   ├── index.ts
│   └── tsconfig.json
├── tests
│   ├── index.spec.ts
│   ├── index.spec.yaml
│   └── tsconfig.json
└── tsconfig.json

The contents of the files are the following,

// /package.json
{
  "files": [
    "dist",
    "umd"
  ],
  "main": "dist/index.js",
  "types": "dist/index.d.ts",
  "scripts": {
    "prebuild": "yarn test",
    "build": "tsc -p src/tsconfig.json",
    "browserify": "parcel build -d umd -o index.min.js ./browser.ts",
    "test": "ts-mocha --paths -p tests/tsconfig.json tests/**/*.spec.ts",
    "prepack": "yarn build && yarn browserify"
  },
  "devDependencies": {
    "@types/expect": "^24.3.0",
    "@types/js-yaml": "^3.12.1",
    "@types/mocha": "^5.2.7",
    "js-yaml": "^3.13.1",
    "mocha": "^6.0.0",
    "parcel-bundler": "^1.12.4",
    "ts-mocha": "^6.0.0",
    "typescript": "^3.7.4"
  }
}

Note that prepack means prepublish, so every time you publish to NPM, there will be both JS and UMD versions.

// /tsconfig.json
{
  "compilerOptions": {
    "strict": true,
    "skipLibCheck": true,
    "sourceMap": true,
    "declaration": true,
    "declarationMap": true,
    "esModuleInterop": true,
    "allowSyntheticDefaultImports": true,
    "resolveJsonModule": true
  }
}

Not sure if I should also specify target: "esnext" and module: "esnext" as well?

// /src/tsconfig.json
{
  "extends": "../tsconfig.json",
  "compilerOptions": {
    "outDir": "../dist"
  }
}
// /tests/tsconfig.json
{
  "extends": "../tsconfig.json",
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": [
        "../src/*"
      ]
    }
  },
  "include": [
    "../src",
    "../tests"
  ]
}
// /browser.ts
import { a, b, c } from './src'
Object.assign(window, { a, b, c })

Now, when you push to NPM, you can access the browser version via unpkg, with the globals, a, b and c.

unpkg.com/:[email protected]:version/umd/index.min.js

What is missing here, is I haven't add pre-commit hooks to test and lint before committing, probably via husky. Also, the CI, e.g. Travis CI

Real project

In my real repo, it's actually a monorepo (mostly powered by Yarn workspaces), and the following folder structure.

.
├── data/**/*.*
├── .eslintignore
├── .eslintrc.js
├── lerna.json
├── package.json
├── tsconfig.json
└── packages
    ├── eqdict
    │   ├── browser.ts
    │   ├── package.json
    │   ├── src
    │   │   ├── index.ts
    │   │   └── tsconfig.json
    │   ├── tests
    │   │   ├── index.spec.ts
    │   │   ├── index.spec.yaml
    │   │   └── tsconfig.json
    │   └── tsconfig.json
    ├── hyperpug
    │   ├── browser.ts
    │   ├── package.json
    │   ├── src
    │   │   ├── index.ts
    │   │   └── tsconfig.json
    │   ├── tests
    │   │   ├── index.spec.ts
    │   │   ├── index.spec.yaml
    │   │   ├── sample.html
    │   │   └── tsconfig.json
    │   └── tsconfig.json
    ├── indent-utils
    │   ├── browser.ts
    │   ├── package.json
    │   ├── src
    │   │   ├── index.ts
    │   │   └── tsconfig.json
    │   ├── tests
    │   │   ├── index.spec.ts
    │   │   ├── index.spec.yaml
    │   │   └── tsconfig.json
    │   └── tsconfig.json
    ├── make-html
    │   ├── package.json
    │   ├── src
    │   │   ├── index.ts
    │   │   └── tsconfig.json
    │   ├── tests
    │   │   ├── index.spec.ts
    │   │   └── tsconfig.json
    │   └── tsconfig.json
    └── web

In this case, the repo is tested in /packages/make-html and /packages/web, while querying the data from /data.

I also add .eslintignore with the following

!**/.eslintrc*
node_modules
dist
umd
*.svg
*.ico
*.json
*.md
*.log
*.lock

And, my ESLint config, that can properly control my TypeScript.

module.exports = {
  env: {
    browser: true,
    es6: true,
    node: true,
  },
  extends: [
    'standard',
    'plugin:vue/essential',
    '@vue/standard',
    '@vue/typescript',
  ],
  globals: {
    Atomics: 'readonly',
    SharedArrayBuffer: 'readonly',
  },
  parser: 'vue-eslint-parser',
  parserOptions: {
    ecmaVersion: 2018,
    sourceType: 'module',
    parser: '@typescript-eslint/parser',
  },
  plugins: [
    '@typescript-eslint',
  ],
  rules: {
    'no-unused-vars': 0,
    'no-useless-constructor': 0,
    'no-cond-assign': 0,
    'no-undef': 0,
    'no-new': 0,
    'arrow-parens': ['error', 'always'],
    'quote-props': ['error', 'as-needed'],
    'comma-dangle': ['error', 'always-multiline'],
    semi: 'off',
    '@typescript-eslint/semi': ['error', 'never'],
    '@typescript-eslint/member-delimiter-style': ['error', {
      multiline: {
        delimiter: 'none',
      },
    }],
  },
}