TypeScript/ECMA

Tools

TypeScript projects use:

  • prettier for formatting
  • eslint for checking code style and issues
  • tsc for type checking and generating d.ts files for libraries
  • vitest for testing
  • vite for bundling library or app
  • typedoc for generating documentation using rustdoc theme

mono-dev packages itself as a node module. In TypeScript/ECMAScript projects, the package needs to be declared in package.json to be managed by the package manager.

Running pnpm up mono-dev will resolve the latest commit and update it.

Template: package.json

  • {
        "devDependencies": {
            "mono-dev": "github:Pistonight/mono-dev#dist"
        },
        "pistonight/mono-dev": {
            "nocheck": [
                "/src/generated"
            ]
        }
    }
    
    • Paths in nocheck will not be processed by eslint or prettier. If the path is in the form of /foo or foo, the foo directory will also not be type-checked.
    • pistonight/mono-dev is the options for mono-dev. See MonoDevOptions

Template: Taskfile.yml

  • version: '3'
    
    includes:
      ecma:
        taskfile: ./node_modules/mono-dev/task/ecma.yaml
        internal: true
        optional: true
    

Template: .gitignore

  • # mono-dev: ecma gitignores
    node_modules
    package-lock.json
    .prettierignore
    .eslintcache
    /eslint.config.js
    /tsconfig*.json
    /mono-dev
    /dist
    /docs
    

Type Checking & Linting

mono-dev automatically generates type checking configs based on directory structure:

  • Each directory is type-checked separately and allow for different env config (for example, src vs scripts)
  • no DOM and no types exist by default. They need to be manually included in env.d.ts in each directory. Only directories with env.d.ts will be checked.
  • If root directory contain any TypeScript stuff, it will be checked as well
  • ESLint only checks the TypeScript projects. If you use ECMAScript, you opt-out of safety anyway

Note

For LSP and compatibility with other tools, ESLint and tsconfig files will be generated at the project root (like how they are for a regular project). For eslint-lsp, you may need to add eslint dependency to downstream in order for the server to find the eslint library.

Import Path remapping

We now use the NodeJS subpath imports to map internal imports to avoid the “relative parent import hell”. This has a few advantages over mapping it in TS:

  • Generated .d.ts files would have the correct imports
  • Bundler tools such as vite or bun doesn’t need extra TS-specific configuration.

Since the bundler would remove these imports when bundling, they are only significant in type declarations generated by TSC (since they keep the original imports). This is why the field is mapped to the source typescript files instead of files in dist.

The mapping is always automatically generated for the src directory and can be manuall disabled with mono-dev option importmap: false.

Example

The following directory structure:

    - src/
      - app/
      - util/
        - image/
          - index.ts
        - data/
          - index.ts
      - lib/
        - foo/
          - index.ts
        - index.ts

generates:

#lib ->  ./src/lib/index.ts
#util/image -> ./src/util/image/index.ts
#util/data -> ./src/util/data/index.ts

Note that in the published package.json, the imports will be replaced with the .d.ts files

Test

mono-dev re-exports vitest for testing. This ensures the version of vitest is managed by the version of mono-dev. Import anything from mono-dev/vitest instead of vitest.

Use ecma:test task to run test once and ecma:test-dev task to run in watch mode.

Library Exports

The tooling supports 3 kinds of exports:

  1. Raw: As-configured in exports field; anything that is not one of the below

  2. Auto-compiled: Any exports key that satisfies *.(c|m)?tsx?

    • In the published package, the export is transformed to an object like below.

      {
          "exports": {
              "./foo": "./src/bar/foo.ts"
          }
      }
      // becomes:
      {
          "exports": {
              "./foo": {
                  "import": "./dist/bar/foo.js",
                  "types": "./dist/_dts_/src/bar/foo.d.ts"
              }
          }
      }
      

      Info

      In this configuration, only the published package contains ESM + Type Declaration. Internal consumers (like other packages in a monorepo) consume the TS source directly for more streamlined dev experience. For example running vite dev server will not require internal packages to be built into ESM first. Editing TS source also does not require rebuilding the packages

    • Use the mono-dev nocompile option to exclude auto-configured compilation, which means the published package will also export the TS file directly.

  3. Manual-compiled: Object exports key of the form:

    {
        "pistonight/mono-dev": {
            "compile": {
                "./foo": "./src/bar/foo.ts"
            }
        },
        "exports": {
            "./foo": {
                "import": "./dist/bar/foo.js",
                "types": "./dist/_dts_/src/bar/foo.d.ts"
            }
        }
    }
    

    Warning

    the compile option must be specified to enable this.

    Info

    This configuration is useful if the compiled ESM of the package needs to be consumed as-is from within the monorepo. For example when testing bundling the package (importing transpiled ESM vs. TS directly would not produce the same output)

Note that src must be where TS files are exported from, and dist must be where output ESM is emitted. These 2 directory names are NOT configurable.

Build Library

Building library from TS source into consumable package without TS supports zero-config. Under the hood, the library mode of vite is used. vite.config.ts will be generated on-the-go when running the ecma:lib-build task.

Currently the library is only packaged as ESM.

The build uses dependencies, devDependencies and peerDependencies to automatically externalize dependencies.

  • dependencies
    • Will be installed by package manager when consumer adds the library
    • Externalized: The code of the dependency will not be in the library
  • peerDependencies:
    • Will NOT be installed by package manager when consumer adds the library; They have to also install it in the downstream.
    • Externalized: The code of the dependency will not be in the library
  • devDependencies:
    • Will NOT be installed by package manager when consumer adds the library.
    • NOT Externalized: The code of the dependency is bundled into the library.

Consideration: Unless the library is meant to be used as a framework, DO NOT add global states. It’s very easy to cause duplicated global states when resolving dependencies.

See the above section for out library exports are configured.

If needed, the lib-types export defines the types for library builds. Put in env.d.ts

/// <reference types="mono-dev/lib-types" />

Vite

mono-dev ships a baseline vite config that adds common plugins and configs to my projects.

vite.config.ts at the root:

import { configure } from "mono-dev/app-build-config";

// config options are referenced from pistonight/mono-dev options
// in package.json

// use configure just like defineConfig
export default configure({
    /** normal vite config here */
});

Plugins are automatically added:

  • YAML loader (always added)
  • React and React Compiler (if react is installed)
  • WASM (if configured)

Define vite types in src/env.d.ts:

/// <reference types="mono-dev/app-types" />
/// <reference types="dom" />

Use the ecma:app-dev and ecma:app-build tasks to execute vite dev server and build.

import.meta.env

Mono-dev options can be used to specify auto import.meta.env values. See MonoDevOptions

Publish

The tool need to zap the package.json before publishing. Therefore, private must be true to prevent accidental publishing of the original package.json. Use the ecma:publish or pnpm exec mono publish to publish the package.

--access public is always specified.