lume / cli

A CLI for managing TypeScript packages.
6 stars 2 forks source link
automation babel builder builder-archetype builders webdev webpack

@lume/cli

A command line tool for building, testing, and publishing JavaScript/TypeScript packages.

Write source code (with tests) and don't worry about the specifics of package management.

@lume/cli is designed with ES Modules (ESM), i.e. JavaScript modules, in mind. CommonJS is not supported.

npm install @lume/cli --global

Current Features

The following is a brief overview of LUME cli's features. For more details, see lume --help.

Future Features

Projects using LUME CLI

lume/element, classy-solid, lume/element-behaviors, lume/lume, trusktr/lowclass, trusktr/perfect

Notice in those projects that they have no dependencies on any build tools directly and no build configurations; they use lume commands for building, testing, formatting, and publishing in a common way.

[!Note] This project initially meets needs for LUME packages, and as such may not be a perfect fit for everyone's needs.

I'd like to make this easy to extend and even more generic to fit any needs, so that only few modifications are needed in order to adopt its use for more specific cases (f.e. adding Babel or @web/test-runner plugins). See TODO.

Requirements

Getting Started

There are two ways to install LUME cli.

Local Install (recommended)

Install the cli as a dev dependency of your project, so you can rely on a specific version of it with confidence.

npm install @lume/cli --save-dev

Then use the npx command (which ships with npm and is used to run local executables) to run the cli and show the help menu:

npx lume --help

Global Install

Install the lume command globally so it is available in any shell:

npm install @lume/cli --global

If the above fails with permissions errors, you may need to run it with sudo in Linux/macOS or Admin priviliges in Windows:

sudo npm install @lume/cli --global

Then run the cli and show the help menu:

lume --help

[!Important] Installing lume globally may work up to a certain point (at least the way the cli currently works, where it does not yet manage internal versioning). If you have multiple projects that depend on different versions of the lume cli with differing and incompatible features, you'll want to install specific versions locally in each project instead. In the future, the LUME cli will have internal version management.

No Install (easiest)

Using npx, we can also skip installing the LUME cli at all. If npx does not detect a locally-installed version of an executable in a project, it will default to downloading the latest version of the executable and running it.

Use the npx command (which ships with npm and is used to run local executables) to run the cli and show the help menu:

npx lume --help

[!Important] This poses a problem similar to the global install option: the latest version of the cli downloaded by npx may not be the version of LUME cli that your project works with. In the future, the LUME cli may have internal version management.

Project setup

File structure

The general structure of a project mananaged with the lume cli is as follows:

src/               # All source files go here, as well as `.test.ts` files.
  index.ts         # The project's entry point.
  index.test.ts    # A co-located test file.
  ...              # Other files imported by entry point, and associated test files.
dist/              # The folder where build output goes (you might ignore this folder in your version control system).
.gitignore         # Things to ignore, like the `dist/` output folder, are listed in here.
package.json       # The project meta file, listing dependencies, scripts, etc.
lume.config.cjs    # Optional config options read by `lume` cli, see below.
tsconfig.json      # Optional, TypeScript configuration overrides. Extend from ./node_modules/@lume/cli/config/ts.config.json.

The lume build command will compile .ts files from the src/ folder, outputting them as .js files along with .js.map source map files into the dist/ folder. It will also output .d.ts and .d.ts.map files for type definitions and mapping from type definitions back to .ts sources files.

Set up files

Let's set up package.json, .gitignore, src/index.ts, and src/index.test.ts.

NOTE, in the near future we'll add command to LUME cli to scaffold these files.

We'll want to have the following things in our package.json (not all scripts are required, but this makes them convenient to call with npm run and documents their existence to anyone looking in package.json to understand available actions).

package.json

{
    "name": "PACKAGE-NAME",
    "version": "0.0.0",
    "license": "MIT",
    "type": "module",
    "main": "dist/index.js",
    "types": "src/index.ts",
    "exports COMMENT:": "This removes 'dist' from import statements, as well as replaces the 'main' field. See https://github.com/nodejs/node/issues/14970#issuecomment-571887546",
    "exports": {
        ".": "./dist/index.js",
        "./": "./dist/"
    },
    "scripts": {
        "clean": "lume clean",
        "build": "lume build",
        "dev": "lume dev",
        "typecheck": "lume typecheck",
        "typecheck:watch": "lume typecheckWatch",
        "test": "lume test",
        "test:debug": "lume testDebug",
        "prettier": "lume prettier",
        "prettier:check": "lume prettierCheck",
        "release:patch": "lume releasePatch",
        "release:minor": "lume releaseMinor",
        "release:major": "lume releaseMajor",
        "version": "lume versionHook",
        "postversion": "lume postVersionHook",
        "prepare": "npm run build"
    }
}

Where PACKAGE-NAME would be the actual name of our package.

We should ignore some things in a .gitignore file.

.gitignore

node_modules/ # project dependencies
dist/ # build output (unless you want to commit output JS files)
*.log # log files in case of errors, etc

Lastly, let's create src/index.ts with some sample code and ensure that it exports the project's version number at the very bottom:

src/index.ts

export function isAwesome(thing: string) {
    return `${thing} is awesome.`
}

export const version = '0.0.0'

The lume release* commands will automatically update both the exported version variable in src/index.ts and the version field in package.json.

NOTE! At the moment the release commands will throw an error if they don't find this line at the bottom of the entrypoint. We'll make this optional in the near future.

Lets write a test file to test our nifty isAwesome function.

src/index.test.ts

import {isAwesome} from './index.js'

describe('isAwesome', () => {
    it('is a function', () => {
        expect(isAwesome).toBeInstanceOf(Function)
    })

    it('says things are awesome', () => {
        expect(isAwesome('Code')).toBe('Code is awesome.')
    })
})

This is enough to get a project bootstrapped. To learn more on how to configure build and test settings with lume.config.cjs and tsconfig.json files, see Configuration below.

Configuration

The lume.config.cjs and tsconfig.json files can be used for configuration.

lume.config.cjs

Various parts of the build/test/publish process can be configured with a lume.config.cjs file at the root of the project. The following example shows the available options with their defaults.

module.exports = {
    // EXPERIMENTAL
    // Whether or not to run the TypeScript compiler in project reference mode
    // (--build) for incremental compilation. This requires some advanced
    // configuration of tsconfig.json.
    //
    // Default: false
    tsProjectReferenceMode: true,

    // The figlet font to use when your project's name is displayed at the
    // beginning of `lume build`. Info: https://www.npmjs.com/package/figlet
    //
    // Example:
    //   _   _      _ _        __        __         _     _ _ _
    //  | | | | ___| | | ___   \ \      / /__  _ __| | __| | | |
    //  | |_| |/ _ \ | |/ _ \   \ \ /\ / / _ \| '__| |/ _` | | |
    //  |  _  |  __/ | | (_) |   \ V  V / (_) | |  | | (_| |_|_|
    //  |_| |_|\___|_|_|\___/     \_/\_/ \___/|_|  |_|\__,_(_|_)
    //
    // Default: 'ICL-1900'
    figletFont: 'Ghost',

    // If true, transpile TypeScript code with Babel instead of TypeScript. The
    // `tsc` command will be used only for type checking, while the actual code
    // transpilation will be done with `babel`.
    //
    // This can be useful, for example, when TypeScript doesn't support a feature
    // Babel does. For example, at the time of writing this, Babel had Stage 3
    // decorators, while TypeScript did not.
    //
    // NOTE! If you do not use this option, the legacy version of decorators will
    // be used. This option must be `true` if you wish to use the current
    // decorator spec.
    //
    // Default: true
    useBabelForTypeScript: false,

    // A path (if not absolute, then relative to the working directory) to a file
    // that has ignore rules in it. This is useful for cases when we want `lume
    // prettier` to use a specific ignore file, but otherwise (for example) want
    // our IDE to use the default .prettierignore file. In the `lume` repo, we
    // want the IDE to format on save in any file we are editing, even in
    // sub-workspaces, but we want `lume prettier` to format only files in the top
    // level workspace.
    //
    // Default: The `.prettierignore` in your project if it exists, otherwise "./node_modules/@lume/cli/.prettierignore".
    prettierIgnorePath: './path/to/.some-other-ignore-file',

    // The test files that will be executed with `@web/test-runner`. It can be a
    // string glob, or array of string globs. Prefix a glob with `!` to negate
    // it.
    //
    // Default: "dist/**/*.test.js"
    testFiles: ['./build/**/tests/*.js'],

    // The import map to use for `@web/test-runner`, which runs tests as native
    // JavaScript modules in a browser. The import map is needed for mapping
    // import specifiers to URLs from which to get those imports. Learn about import maps here:
    // https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script/type/importmap
    //
    // Default: {}
    importMap: {
        imports: {
            // If we `npm install`ed `some-package`, then we tell web test-runner where to get it from:
            'some-package': '/node_modules/some-package/dist/index.js',
        },
    },
}

tsconfig.json

To configure (override) TypeScript compiler options, create a tsconfig.json file at the root of the project that extends from ./node_modules/@lume/cli/config/ts.config.json, and override any settings as needed (to see what LUME cli's default settings are, see ./config/ts.config.ts).

[!Note] If you don't wish to override anything, then tsconfig.json is not necessary, Lume cli will automatically use its own config during lume build. However, IDEs by default look for tsconfig.json for configuration, so if you want your IDE to use the same configuration, you can make an empty config that only extends from Lume's.

See the TypeScript compiler options For TypeScript-specific build and type-checking configuration.

{
    "extends": "./node_modules/@lume/cli/config/ts.config.json",
    "compilerOptions": {
        "target": "es5"
    }
}

Managing a project

Now that we've bootstrapped our project, the following are the basic commands we'll want to run to manage the life cycle of our project. For sake of simplicity, the following examples assume that lume was installed globally as per the "Global Install" option above.

For more commands and details, run lume --help.

Caveats

This uses TypeScript for transpiling all code. To customize build options, you will need to get familiar with TypeScript's compiler options.

If you lower TypeScript's compiler target to ES5 or lower, you may need to enable the downlevelIteration option if you need spec-compliant for..of loops (for example if you depend on in-place modification of iterables like Set while iterating on them, etc).

TODOs