question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Improve README documentation for TypeScript usage

See original GitHub issue

Expected Behavior

The ‘TypeScript’ section of the README file would provide accurate, complete, and up to date information of how to use zx with TypeScript.

Actual Behavior

The current ‘how to use TypeScript’ documentation appears inadequate:

https://github.com/google/zx/blob/854e4eca9e42f056fa9d1de9f5e7abd1e7461cbd/README.md?plain=1#L454-L468

Crawling through the release history it seems like support for TypeScript via the zx binary was dropped in v5.0.0:

This release drops build of CommonJS version and support for .ts extension by zx bin.

TypeScript is still supported, for example, via ts-node:

node --loader ts-node/esm script.ts

Also, a new Node version requirement is >= 16.0.0.

Looking at the README at this commit seemed to have better usage information for how to use this with ts-node: https://github.com/google/zx/blob/7977cb5adb9545082b8edb8bfe647dedfcb98b42/README.md?plain=1#L398-L421

Which seems to have been removed ~17 days ago in this commit: https://github.com/google/zx/commit/db0e65163d31e37cc6d71ae2e20e2ada4186efa6#diff-b335630551682c19a781afebcf4d07bf978fb1f8ac04c6bf87428ed5106870f5L542-R458


Following the current README as written, I tried making a foo.mjs test file:

#!/usr/bin/env zx

import 'zx/globals'

void async function () {
  const foo: string = "foo";
  await $`ls -la`
}()

Running this with ./foo.mjs (zx bin in the shebang) results in the following error, presumably because .mjs doesn’t specify it as a TypeScript file:

SyntaxError: Missing initializer in const declaration
    at ESMLoader.moduleStrategy (node:internal/modules/esm/translators:117:18)
    at ESMLoader.moduleProvider (node:internal/modules/esm/loader:337:14)
    at async link (node:internal/modules/esm/module_job:70:21)

I also tried renaming it to foo.ts, which resulted in this error:

TypeError [ERR_UNKNOWN_FILE_EXTENSION]: Unknown file extension ".ts" for /Users/devalias/dev/0xdevalias/minimal-zx-ts/foo.ts
    at new NodeError (node:internal/errors:372:5)
    at Object.getFileProtocolModuleFormat [as file:] (node:internal/modules/esm/get_format:76:11)
    at defaultGetFormat (node:internal/modules/esm/get_format:118:38)
    at defaultLoad (node:internal/modules/esm/load:21:20)
    at ESMLoader.load (node:internal/modules/esm/loader:407:26)
    at ESMLoader.moduleProvider (node:internal/modules/esm/loader:326:22)
    at new ModuleJob (node:internal/modules/esm/module_job:66:26)
    at ESMLoader.#createModuleJob (node:internal/modules/esm/loader:345:17)
    at ESMLoader.getModuleJob (node:internal/modules/esm/loader:304:34)
    at async Promise.all (index 0) {
  code: 'ERR_UNKNOWN_FILE_EXTENSION'
}

Looking at the TypeScript docs for ‘ECMAScript Modules in Node.js’ it talks about various other file extensions that can be used:

The type field in package.json is nice because it allows us to continue using the .ts and .js file extensions which can be convenient; however, you will occasionally need to write a file that differs from what type specifies. You might also just prefer to always be explicit.

Node.js supports two extensions to help with this: .mjs and .cjs. .mjs files are always ES modules, and .cjs files are always CommonJS modules, and there’s no way to override these.

In turn, TypeScript supports two new source file extensions: .mts and .cts. When TypeScript emits these to JavaScript files, it will emit them to .mjs and .cjs respectively.

Based on this, I figured that maybe the .mts extension would work, as it should be the ‘TypeScript flavoured’ version of the recommended .mjs; but renaming to foo.mts and running it again resulted in this error:

TypeError [ERR_UNKNOWN_FILE_EXTENSION]: Unknown file extension ".mts" for /Users/devalias/dev/0xdevalias/minimal-zx-ts/foo.mts
    at new NodeError (node:internal/errors:372:5)
    at Object.getFileProtocolModuleFormat [as file:] (node:internal/modules/esm/get_format:76:11)
    at defaultGetFormat (node:internal/modules/esm/get_format:118:38)
    at defaultLoad (node:internal/modules/esm/load:21:20)
    at ESMLoader.load (node:internal/modules/esm/loader:407:26)
    at ESMLoader.moduleProvider (node:internal/modules/esm/loader:326:22)
    at new ModuleJob (node:internal/modules/esm/module_job:66:26)
    at ESMLoader.#createModuleJob (node:internal/modules/esm/loader:345:17)
    at ESMLoader.getModuleJob (node:internal/modules/esm/loader:304:34)
    at async Promise.all (index 0) {
  code: 'ERR_UNKNOWN_FILE_EXTENSION'
}

Based on the old README example of using ts-node I decided to try that method, and followed the latest instructions/details from:

I can seemingly run the script with ts-node-esm directly as:

npm exec ts-node-esm -- ./foo.mts

And by modifying the shebang to call ts-node-esm:

#!/usr/bin/env npx --package=ts-node -- ts-node-esm

import 'zx/globals'

void async function () {
  const foo: string = "foo";
  await $`ls -la`
}()

We can run the file with a .ts extension (when "type": "module", is set in package.json).

If we try to use a .js or .mjs extension we get the same error as earlier (as we would expect):

SyntaxError: Missing initializer in const declaration
    at ESMLoader.moduleStrategy (node:internal/modules/esm/translators:117:18)
    at ESMLoader.moduleProvider (node:internal/modules/esm/loader:337:14)
    at async link (node:internal/modules/esm/module_job:70:21)

If we use the .mts extension however, we can remove "type": "module", from package.json and it will still work (as .mts are files are always ES modules)

We can make this run even faster if we use ts-node’s swc suport:

  • https://typestrong.org/ts-node/docs/transpilers/
    • ts-node supports third-party transpilers as plugins. Transpilers such as swc can transform TypeScript into JavaScript much faster than the TypeScript compiler. You will still benefit from ts-node’s automatic tsconfig.json discovery, sourcemap support, and global ts-node CLI.

  • https://typestrong.org/ts-node/docs/swc
    • SWC support is built-in via the --swc flag or "swc": true tsconfig option.

    • SWC is a TypeScript-compatible transpiler implemented in Rust. This makes it an order of magnitude faster than vanilla transpileOnly.

    • To use it, first install @swc/core or @swc/wasm. If using importHelpers, also install @swc/helpers. If target is less than "es2015" and using async/await or generator functions, also install regenerator-runtime.

  • https://github.com/swc-project/swc
  • https://swc.rs/

Based on this we could install the @swc/core lib, and modify our script’s shebang to add the --swc arg as follows:

npm i --save-dev @swc/core
#!/usr/bin/env npx --package=ts-node -- ts-node-esm --swc

import 'zx/globals'

void async function () {
  const foo: string = "foo";
  await $`ls -la`
}()

Super basic testing on my laptop with time shows the following speed improvements when using --swc with ts-node:

// time ./foo.mts

// Without --swc
./foo.mts  3.45s user 0.55s system 126% cpu 3.151 total
./foo.mts  3.29s user 0.34s system 153% cpu 2.374 total
./foo.mts  3.16s user 0.34s system 156% cpu 2.243 total

// With --swc
./foo.mts  1.14s user 0.46s system 69% cpu 2.299 total
./foo.mts  1.14s user 0.27s system 103% cpu 1.358 total
./foo.mts  1.13s user 0.27s system 103% cpu 1.356 total

Contrasting this against a plain .mjs file using the zx bin directly, we can see that the .mjs is still faster, but for the dev efficiency improvements of using TypeScript, I feel like the tradeoff isn’t too bad when using tas-node with --swc:

#!/usr/bin/env zx

import 'zx/globals'

const foo = "foo";
await $`ls -la`
console.log(foo);
./foo.mjs  0.46s user 0.25s system 72% cpu 0.977 total
./foo.mjs  0.45s user 0.11s system 118% cpu 0.481 total
./foo.mjs  0.44s user 0.11s system 119% cpu 0.461 total

Trying to remove the wrapping async function results in the following error:

/Users/devalias/.npm/_npx/1bf7c3c15bf47d04/node_modules/ts-node/src/index.ts:843
    return new TSError(diagnosticText, diagnosticCodes, diagnostics);
           ^
TSError: ⨯ Unable to compile TypeScript:
foo.mts:7:3 - error TS1378: Top-level 'await' expressions are only allowed when the 'module' option is set to 'es2022', 'esnext', 'system', 'node16', or 'nodenext', and the 'target' option is set to 'es2017' or higher.

7   await $`ls -la`
    ~~~~~

    at createTSError (/Users/devalias/.npm/_npx/1bf7c3c15bf47d04/node_modules/ts-node/src/index.ts:843:12)
    at reportTSError (/Users/devalias/.npm/_npx/1bf7c3c15bf47d04/node_modules/ts-node/src/index.ts:847:19)
    at getOutput (/Users/devalias/.npm/_npx/1bf7c3c15bf47d04/node_modules/ts-node/src/index.ts:1057:36)
    at Object.compile (/Users/devalias/.npm/_npx/1bf7c3c15bf47d04/node_modules/ts-node/src/index.ts:1411:41)
    at transformSource (/Users/devalias/.npm/_npx/1bf7c3c15bf47d04/node_modules/ts-node/src/esm.ts:400:37)
    at /Users/devalias/.npm/_npx/1bf7c3c15bf47d04/node_modules/ts-node/src/esm.ts:278:53
    at async addShortCircuitFlag (/Users/devalias/.npm/_npx/1bf7c3c15bf47d04/node_modules/ts-node/src/esm.ts:409:15)
    at async ESMLoader.load (node:internal/modules/esm/loader:407:20)
    at async ESMLoader.moduleProvider (node:internal/modules/esm/loader:326:11)
    at async link (node:internal/modules/esm/module_job:70:21) {
  diagnosticCodes: [ 1378 ]
}

Which we can fix by setting our tsconfig.json to something like this:

{
  "compilerOptions": {
    "target": "ES2021",
    "module": "node16",
  },
}

Which then allows us to simplify the basic code to:

#!/usr/bin/env npx --package=ts-node -- ts-node-esm

import 'zx/globals'

const foo: string = "foo";
await $`ls -la`

We could also use something like the following (from https://github.com/tsconfig/bases#node-16--esm--strictest-tsconfigjson) rather than handcrafting our own compatible tsconfig.json:

npm i @tsconfig/node16-strictest-esm
// tsconfig.json

{
  "extends": "@tsconfig/node16-strictest-esm/tsconfig.json"
}

An alternative to using ts-node could be to use tsm (which wraps esbuild), but I didn’t look too deeply into how to make it work:

Steps to Reproduce the Problem

  1. Be a newbie to using zx
  2. Wonder how to use it effectively with TypeScript
  3. Get confused

Specifications

  • Version: 7.0.5
  • Platform: node v16.15.1, macOS 12.3.1

Other older TypeScript related issues that I looked through first

Issue Analytics

  • State:open
  • Created a year ago
  • Reactions:54
  • Comments:16 (1 by maintainers)

github_iconTop GitHub Comments

4reactions
ambiguous48commented, Oct 8, 2022

I’ve found success using the .mts extension and adding the following shebang:

#!/usr/bin/env -S npx tsx

import 'zx/globals';

await $`ls -la`;

console.log(chalk.green('Hello, World!'));

Run like so:

chmod +x ./myfile.mts
./myfile.mts
4reactions
antonmedvcommented, Jul 4, 2022

But will be cool if zx also supports global ts scripts.

Read more comments on GitHub >

github_iconTop Results From Across the Web

Documenting Your TypeScript Projects: There Are Options
The first and easiest way of providing some kind of documentation is by writing proper comments on your code. And by proper, I...
Read more >
TypeScript + JSDoc = better-docs - Wojciech Krysiak - Medium
Documentation generated by better-docs looks very well (screenshots below) · It is based on JSDoc — a stable tool created a long time...
Read more >
Develop and refine documentation - AWS Prescriptive Guidance
Supporting documentation about the code can be README files and external documents. ... You can use TypeDoc to read your TypeScript source files, ......
Read more >
better-docs/README.md - UNPKG
better -docs has a plugin which allows you to generate documentation from your TypeScript codebase. 67. 68, ## Usage.
Read more >
Documentation Style Guide - GitLab Docs
This document defines the standards for GitLab documentation, including grammar, formatting, word use, and more. For style questions, mention @tw-style in an ...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found