cefn / zx-tsnode-repro

MIT License
0 stars 1 forks source link

ZX Removed CommonJS support, broke ts-node. #1

Open cefn opened 2 years ago

cefn commented 2 years ago

With the aim of modernising, but at the cost of breaking existing usages, zx no longer publishes a CommonJS bundle, only an ESM bundle.

This can be observed in the package.json of zx which only references .mjs files and no .cjs...

  "main": "src/index.mjs",
  "types": "src/index.d.ts",
  "exports": {
    ".": "./src/index.mjs",
    "./globals": "./src/globals.mjs",
    "./experimental": "./src/experimental.mjs",
    "./cli": "./zx.mjs",
    "./package.json": "./package.json"
  },

Point of view of the zx maintainers is that this is hard cheese, so it's up to users of zx to find a way to fix it.

Initial Error

My reference zx-node-repro project has nothing but the default npm and typescript scaffolding that comes from npm init and tsc --init followed by npm install --save-dev zx ts-node.

Running an example script breaks with the following error whenever you try to use ts-node to run a file which includes e.g. import { $ } from 'zx';.

zx-tsnode-repro % npx ts-node src/example.ts                                                              
Error [ERR_PACKAGE_PATH_NOT_EXPORTED]: No "exports" main defined in /Users/cefn/Documents/cefn/github/zx-tsnode-repro/node_modules/zx/package.json
    at new NodeError (node:internal/errors:371:5)
    at throwExportsNotFound (node:internal/modules/esm/resolve:453:9)
    at packageExportsResolve (node:internal/modules/esm/resolve:671:7)
    at resolveExports (node:internal/modules/cjs/loader:482:36)
    at Function.Module._findPath (node:internal/modules/cjs/loader:522:31)
    at Function.Module._resolveFilename (node:internal/modules/cjs/loader:919:27)
    at Function.Module._resolveFilename.sharedData.moduleResolveFilenameHook.installedValue [as _resolveFilename] (/Users/cefn/Documents/cefn/github/zx-tsnode-repro/node_modules/@cspotcode/source-map-support/source-map-support.js:679:30)
    at Function.Module._load (node:internal/modules/cjs/loader:778:27)
    at Module.require (node:internal/modules/cjs/loader:1005:19)
    at require (node:internal/modules/cjs/helpers:102:18) {

Note the stack trace originates from node:internal/modules/cjs/loader which embodies Node's CommonJS loading strategy. The EcmaScript Modules (ESM) strategy has not been triggered since we haven't declared our project as using ESM.

We might try to run it as esm by telling ts-node, but that doesn't change how it attempts to load its dependencies, giving us this error, which is maybe a bug in how zx is packaged? It's hard for anyone to know what is actually going on, and we'd rather just be able to run our scripts but we still get ...

npx ts-node --esm src/example.ts 
Error [ERR_PACKAGE_PATH_NOT_EXPORTED]: No "exports" main defined in /Users/cefn/Documents/cefn/github/zx-tsnode-repro/node_modules/zx/package.json

Step 1: Move importing package to ESM

So let's declare the project as being an ESM project, by adding this property to the top level of our package.json...

"type":"module",

However, this doesn't fix the issue straight away. Running npx ts-node src/example.ts now gets this even more unhelpful error...

TypeError [ERR_UNKNOWN_FILE_EXTENSION]: Unknown file extension ".ts" for /Users/cefn/Documents/cefn/github/zx-tsnode-repro/src/example.ts
    at new NodeError (node:internal/errors:371:5)
    at Object.getFileProtocolModuleFormat [as file:] (node:internal/modules/esm/get_format:87:11)
    at defaultGetFormat (node:internal/modules/esm/get_format:102:38)
    at defaultLoad (node:internal/modules/esm/load:21:14)
    at ESMLoader.load (node:internal/modules/esm/loader:359:26)
    at ESMLoader.moduleProvider (node:internal/modules/esm/loader:280:58)
    at new ModuleJob (node:internal/modules/esm/module_job:66:26)
    at ESMLoader.#createModuleJob (node:internal/modules/esm/loader:297:17)
    at ESMLoader.getModuleJob (node:internal/modules/esm/loader:261:34)
    at async Promise.all (index 0) {
  code: 'ERR_UNKNOWN_FILE_EXTENSION'
}

Step 2: Instruct ts-node it's ESM

Because this isn't enough, yet, we might try instructing ts-node that our example.ts should be interpreted as an ESM module like...

npx ts-node --esm src/example.ts

Which gets us this error instead...

ReferenceError: exports is not defined in ES module scope
    at file:///Users/cefn/Documents/cefn/github/zx-tsnode-repro/src/example.ts:11:23
    at ModuleJob.run (node:internal/modules/esm/module_job:197:25)
    at async Promise.all (index 0)
    at async ESMLoader.import (node:internal/modules/esm/loader:337:24)
    at async loadESM (node:internal/process/esm_loader:88:5)
    at async handleMainPromise (node:internal/modules/run_main:61:12)

Step 3: Change our Typescript Project type

So now we might accept, to run this one script, we need to change our package AND our typescript config from CommonJS to ES Module. This change also requires that we declare a moduleResolution strategy.

// tsconfig.json
"module": "esnext",                                /* Specify what module code is generated. */
"moduleResolution": "node",                       /* Specify how TypeScript looks up a file from a given module specifier. */

If this is impossible given you might be writing some other Typescript code you can have a separate tsconfig.example.json from the default, and pass it with --project tsconfig.example.json to ts-node if you need to.

However, even with all the package and typescript changes in place, ts-node can't make sense of our example file...

npx ts-node src/example.ts      
TypeError [ERR_UNKNOWN_FILE_EXTENSION]: Unknown file extension ".ts" for /Users/cefn/Documents/cefn/github/zx-tsnode-repro/src/example.ts
    at new NodeError (node:internal/errors:371:5)
    at Object.getFileProtocolModuleFormat [as file:] (node:internal/modules/esm/get_format:87:11)
    at defaultGetFormat (node:internal/modules/esm/get_format:102:38)
    at defaultLoad (node:internal/modules/esm/load:21:14)
    at ESMLoader.load (node:internal/modules/esm/loader:359:26)
    at ESMLoader.moduleProvider (node:internal/modules/esm/loader:280:58)
    at new ModuleJob (node:internal/modules/esm/module_job:66:26)
    at ESMLoader.#createModuleJob (node:internal/modules/esm/loader:297:17)
    at ESMLoader.getModuleJob (node:internal/modules/esm/loader:261:34)
    at async Promise.all (index 0) {
  code: 'ERR_UNKNOWN_FILE_EXTENSION'

Finally Executing a script

We can get the script to run but ONLY with the --esm flag...

npx ts-node --esm src/example.ts

> zx-tsnode-repro@1.0.0 test
> ts-node --esm src/example.ts

$ git diff-index --quiet HEAD .
You have uncommitted files in your git worktree. Halting

Reference configuration

You can see the reference configuration in this PR and the test script shows an example execution of an example tooling script I wrote with zx that checks if your git worktree is clean as part of other routines. In that PR branch you can run it like ...

npm run test

Epilogue

If we had missed adding "type":"module" above, passing --esm would raise the really helpful error below that tells us to declare the package type. If only ALL maintainers in the javascript ecosystem wrote meaningful errors. Props to whoever wrote Object.compileFunction you are the hero we need...

npx ts-node --esm src/example.ts
(node:24314) Warning: To load an ES module, set "type": "module" in the package.json or use the .mjs extension.
(Use `node --trace-warnings ...` to show where the warning was created)
/Users/cefn/Documents/cefn/github/zx-tsnode-repro/src/example.ts:10
import { exit } from 'process';
^^^^^^
SyntaxError: Cannot use import statement outside a module
    at Object.compileFunction (node:vm:352:18)
cefn commented 2 years ago

Thanks to the solution in https://github.com/google/zx/issues/125#issuecomment-850392517 for helpful summary of changes to fix the issue.

The more detailed issue above documents that you would experience if you DIDN'T get each of the steps right, as that's probably what will lead people to finding a solution.

An alternative might be to use @cspotcode/zx which pins zx to 5 and has some defaults which enable things to mostly work. See https://github.com/cefn/zx-tsnode-repro/pull/3 as an alternative solution.