From d8f5677ff4fe81df6e5f5e7d349030532acca3c9 Mon Sep 17 00:00:00 2001 From: D-Pow Date: Tue, 11 Jan 2022 19:44:01 -0700 Subject: [PATCH] Add the ability to run TypeScript files from CLI This allows `ts-node` to be used to run TypeScript files without having to compile them with `tsc` first. It also adds the necessary configs, including `tsconfig-paths` for `paths` import-alias resolution. Unfortunately, this means we have to remove the `--experimental-module-resolution=node` since `ts-node` uses its own loader and thus form of resolving modules. See: Setup * https://medium.com/@jimcraft123hd/setting-up-path-alias-in-typescript-and-tsc-build-without-error-9f1dbc0bccd2 Issues with `ts-node`, ESM, and aliases * https://github.com/TypeStrong/ts-node/issues/1007 - https://github.com/TypeStrong/ts-node/pull/476 - https://github.com/dividab/tsconfig-paths/issues/122#issuecomment-917075470 - https://github.com/TypeStrong/ts-node/discussions/1450#discussion-3563207 * https://github.com/TypeStrong/ts-node/issues/1414 * https://github.com/TypeStrong/ts-node/issues/995 - https://github.com/TypeStrong/ts-node/issues/639 Node issues with ESM * https://nodejs.org/api/packages.html#determining-module-system * https://github.com/nodejs/node/pull/37468 --- .npmrc | 48 +++++++++++++-- config/jest/jest.config.mjs | 2 +- config/jest/jestAssetTransformer.mjs | 2 +- config/utils/ESM/index.mjs | 2 +- config/webpack.config.mjs | 8 +-- package-lock.json | 90 +++++++--------------------- package.json | 2 + tsconfig.json | 55 ++++++++++++++++- 8 files changed, 126 insertions(+), 83 deletions(-) diff --git a/.npmrc b/.npmrc index cbd4f34b..5507c27c 100644 --- a/.npmrc +++ b/.npmrc @@ -51,11 +51,49 @@ engine-strict=true # Options only include those that are valid `NODE_OPTIONS`. # See: https://nodejs.org/api/cli.html#cli_node_options_options # -# Allow top-level `await` calls so that webpack.config.mjs can await CJS imports. -# Allow JSON files to be imported in .mjs files. -# Allow automatic extension resolution as well as importing index.js from directories like source code can (e.g. `import file from './file'` instead of './file.js', and `import Utils from './utils'` instead of './utils/index.js'). -# Allow the use of `import.meta.resolve` to use file paths to generate import-safe URLs (different from browser-safe URLs). -node-options='--experimental-top-level-await --experimental-json-modules --experimental-specifier-resolution=node --experimental-import-meta-resolve' +# Notable ones include: +# +# * --experimental-top-level-await +# Allow top-level `await` calls so that webpack.config.mjs can await CJS imports. +# +# +# * --experimental-json-modules +# Allow JSON files to be imported in .mjs files. +# +# +# * --experimental-import-meta-resolve +# Allow the use of `import.meta.resolve` to use file paths to generate import-safe URLs (different from browser-safe URLs). +# +# +# * --experimental-specifier-resolution=node +# Allow automatic extension resolution as well as importing index.js from directories like source code can +# e.g. `import file from './file'` instead of './file.js', and `import Utils from './utils'` instead of './utils/index.js'. +# Note: This will cause some `npx` commands to fail if the executables don't have a file extension on them (e.g. `npx tsc`). +# Relatedly, if using a different script runner/node module loader, e.g. `ts-node` to run TypeScript files, then they will +# sometimes fail as well b/c they'll either be shell scripts, will need their custom module loader (which won't work b/c we +# overrode it with this flag), or their loaders depending on files being translated to CommonJS before being executed (likely in RAM +# by their custom loader and/or NodeJS itself). +# This can be fixed in your own code by making it executable and adding a shebang, e.g. +# #!/usr/bin/env -S node +# #!/usr/bin/env -S npx +# #!/usr/bin/env -S npx ts-node +# For example, to use `ts-node` (for running TypeScript files from the CLI without having to compile them into JavaScript first), +# then they recommend running via either: +# 1. Using ts-node directly: `ts-node file.ts` +# 2. Using normal node command: ESM: `node --loader ts-node/esm file.ts` - CJS: `node -r ts-node/register file.ts` +# 3. Just run the file directly: Use a shebang like above (See: https://github.com/TypeStrong/ts-node/issues/639) +# 4. Add `--loader ts-node/(register|esm)` to `node-options` here (forces all node commands to run with `ts-node`, even `npx`) +# n. TL;DR - Check out this issue for a full analysis of way to run `ts-node`: https://github.com/TypeStrong/ts-node/issues/995 +# But since `ts-node` uses its own module loader, we cannot set `--experimental-specifier-resolution=node` in `node-options` +# b/c it requires its own loader. +# Finally, for `ts-node`, if package.json uses `type: module`, then `ts-node` must use `module: ESNext` as well. +# +# +# * --no-warnings / --redirect-warnings= +# Removes warnings from STDERR or redirects them to . +# If you want to use ts-node by default for all files, then this will be necessary to reduce console noise: +# --loader=ts-node/register --no-warnings +node-options='--experimental-top-level-await --experimental-json-modules --experimental-import-meta-resolve' # For some reason, `npm run` doesn't use the default shell, diff --git a/config/jest/jest.config.mjs b/config/jest/jest.config.mjs index b06d1e3f..33a33be3 100644 --- a/config/jest/jest.config.mjs +++ b/config/jest/jest.config.mjs @@ -2,7 +2,7 @@ import fs from 'fs'; import { defaults } from 'jest-config'; -import { Paths, FileTypeRegexes, ImportAliases } from '../utils'; +import { Paths, FileTypeRegexes, ImportAliases } from '../utils/index.js'; /* * Note: Add the `--no-cache` CLI option during development of jest transformers diff --git a/config/jest/jestAssetTransformer.mjs b/config/jest/jestAssetTransformer.mjs index e27a2ec8..363610af 100644 --- a/config/jest/jestAssetTransformer.mjs +++ b/config/jest/jestAssetTransformer.mjs @@ -2,7 +2,7 @@ import path from 'path'; import JestCssModulesTransformer from 'jest-css-modules-transform'; -import { FileTypeRegexes } from '../utils'; +import { FileTypeRegexes } from '../utils/index.js'; /** @type {import('@jest/core/node_modules/@jest/transform/build/types').SyncTransformer} */ diff --git a/config/utils/ESM/index.mjs b/config/utils/ESM/index.mjs index b67444ba..d155ccdd 100644 --- a/config/utils/ESM/index.mjs +++ b/config/utils/ESM/index.mjs @@ -1 +1 @@ -export * from './Imports'; +export * from './Imports.mjs'; diff --git a/config/webpack.config.mjs b/config/webpack.config.mjs index 6886edcb..76fdc1e5 100644 --- a/config/webpack.config.mjs +++ b/config/webpack.config.mjs @@ -5,17 +5,17 @@ import CopyWebpackPlugin from 'copy-webpack-plugin'; import TerserJSPlugin from 'terser-webpack-plugin'; import MiniCssExtractPlugin from 'mini-css-extract-plugin'; import CssMinimizerPlugin from 'css-minimizer-webpack-plugin'; -import MockRequestsWebpackPlugin from 'mock-requests/bin/MockRequestsWebpackPlugin'; +import MockRequestsWebpackPlugin from 'mock-requests/bin/MockRequestsWebpackPlugin.js'; -import AlterFilePostBuildPlugin from './AlterFilePostBuildPlugin'; +import AlterFilePostBuildPlugin from './AlterFilePostBuildPlugin.mjs'; import { Paths, FileTypeRegexes, getOutputFileName, ImportAliases, LocalLanHostIpAddresses, -} from './utils'; -import babelConfig from './babel.config'; +} from './utils/index.js'; +import babelConfig from './babel.config.js'; import packageJson from '../package.json'; import manifestJson from '../src/manifest.json'; diff --git a/package-lock.json b/package-lock.json index 267d423f..a3c36d17 100644 --- a/package-lock.json +++ b/package-lock.json @@ -70,6 +70,8 @@ "shelljs": "^0.8.4", "terser-webpack-plugin": "^4.2.3", "ts-loader": "^9.2.6", + "ts-node": "^10.4.0", + "tsconfig-paths": "^3.12.0", "typescript": "^4.5.4", "webpack": "^5.61.0", "webpack-cli": "^4.9.1", @@ -1996,8 +1998,6 @@ "resolved": "https://registry.npmjs.org/@cspotcode/source-map-consumer/-/source-map-consumer-0.8.0.tgz", "integrity": "sha512-41qniHzTU8yAGbCp04ohlmSrZf8bkf/iJsl3V0dRGsQN/5GFfx+LbCSsCpp2gqrqjTVg/K6O8ycoV35JIwAzAg==", "dev": true, - "optional": true, - "peer": true, "engines": { "node": ">= 12" } @@ -2007,8 +2007,6 @@ "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.7.0.tgz", "integrity": "sha512-X4xqRHqN8ACt2aHVe51OxeA2HjbcL4MqFqXkrmQszJ1NOUuUu5u6Vqx/0lZSVNku7velL5FC/s5uEAj1lsBMhA==", "dev": true, - "optional": true, - "peer": true, "dependencies": { "@cspotcode/source-map-consumer": "0.8.0" }, @@ -3364,33 +3362,25 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.8.tgz", "integrity": "sha512-6XFfSQmMgq0CFLY1MslA/CPUfhIL919M1rMsa5lP2P097N2Wd1sSX0tx1u4olM16fLNhtHZpRhedZJphNJqmZg==", - "dev": true, - "optional": true, - "peer": true + "dev": true }, "node_modules/@tsconfig/node12": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.9.tgz", "integrity": "sha512-/yBMcem+fbvhSREH+s14YJi18sp7J9jpuhYByADT2rypfajMZZN4WQ6zBGgBKp53NKmqI36wFYDb3yaMPurITw==", - "dev": true, - "optional": true, - "peer": true + "dev": true }, "node_modules/@tsconfig/node14": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.1.tgz", "integrity": "sha512-509r2+yARFfHHE7T6Puu2jjkoycftovhXRqW328PDXTVGKihlb1P8Z9mMZH04ebyajfRY7dedfGynlrFHJUQCg==", - "dev": true, - "optional": true, - "peer": true + "dev": true }, "node_modules/@tsconfig/node16": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.2.tgz", "integrity": "sha512-eZxlbI8GZscaGS7kkc/trHTT5xgrjH3/1n2JDwusC9iahPKWMRvRjJSAN5mCXviuTGQ/lHnhvv8Q1YTpnfz9gA==", - "dev": true, - "optional": true, - "peer": true + "dev": true }, "node_modules/@types/aria-query": { "version": "4.2.2", @@ -4454,9 +4444,7 @@ "version": "4.1.3", "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", - "dev": true, - "optional": true, - "peer": true + "dev": true }, "node_modules/argparse": { "version": "1.0.10", @@ -5696,9 +5684,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", - "dev": true, - "optional": true, - "peer": true + "dev": true }, "node_modules/cross-env": { "version": "5.2.1", @@ -6461,8 +6447,6 @@ "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", "dev": true, - "optional": true, - "peer": true, "engines": { "node": ">=0.3.1" } @@ -11718,9 +11702,7 @@ "version": "1.3.6", "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", - "dev": true, - "optional": true, - "peer": true + "dev": true }, "node_modules/makeerror": { "version": "1.0.11", @@ -16932,8 +16914,6 @@ "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.4.0.tgz", "integrity": "sha512-g0FlPvvCXSIO1JDF6S232P5jPYqBkRL9qly81ZgAOSU7rwI0stphCgd2kLiCrU9DjQCrJMWEqcNSjQL02s6d8A==", "dev": true, - "optional": true, - "peer": true, "dependencies": { "@cspotcode/source-map-support": "0.7.0", "@tsconfig/node10": "^1.0.7", @@ -16975,8 +16955,6 @@ "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", "dev": true, - "optional": true, - "peer": true, "engines": { "node": ">=0.4.0" } @@ -18047,8 +18025,6 @@ "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", "dev": true, - "optional": true, - "peer": true, "engines": { "node": ">=6" } @@ -19406,17 +19382,13 @@ "version": "0.8.0", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-consumer/-/source-map-consumer-0.8.0.tgz", "integrity": "sha512-41qniHzTU8yAGbCp04ohlmSrZf8bkf/iJsl3V0dRGsQN/5GFfx+LbCSsCpp2gqrqjTVg/K6O8ycoV35JIwAzAg==", - "dev": true, - "optional": true, - "peer": true + "dev": true }, "@cspotcode/source-map-support": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.7.0.tgz", "integrity": "sha512-X4xqRHqN8ACt2aHVe51OxeA2HjbcL4MqFqXkrmQszJ1NOUuUu5u6Vqx/0lZSVNku7velL5FC/s5uEAj1lsBMhA==", "dev": true, - "optional": true, - "peer": true, "requires": { "@cspotcode/source-map-consumer": "0.8.0" } @@ -20398,33 +20370,25 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.8.tgz", "integrity": "sha512-6XFfSQmMgq0CFLY1MslA/CPUfhIL919M1rMsa5lP2P097N2Wd1sSX0tx1u4olM16fLNhtHZpRhedZJphNJqmZg==", - "dev": true, - "optional": true, - "peer": true + "dev": true }, "@tsconfig/node12": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.9.tgz", "integrity": "sha512-/yBMcem+fbvhSREH+s14YJi18sp7J9jpuhYByADT2rypfajMZZN4WQ6zBGgBKp53NKmqI36wFYDb3yaMPurITw==", - "dev": true, - "optional": true, - "peer": true + "dev": true }, "@tsconfig/node14": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.1.tgz", "integrity": "sha512-509r2+yARFfHHE7T6Puu2jjkoycftovhXRqW328PDXTVGKihlb1P8Z9mMZH04ebyajfRY7dedfGynlrFHJUQCg==", - "dev": true, - "optional": true, - "peer": true + "dev": true }, "@tsconfig/node16": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.2.tgz", "integrity": "sha512-eZxlbI8GZscaGS7kkc/trHTT5xgrjH3/1n2JDwusC9iahPKWMRvRjJSAN5mCXviuTGQ/lHnhvv8Q1YTpnfz9gA==", - "dev": true, - "optional": true, - "peer": true + "dev": true }, "@types/aria-query": { "version": "4.2.2", @@ -21310,9 +21274,7 @@ "version": "4.1.3", "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", - "dev": true, - "optional": true, - "peer": true + "dev": true }, "argparse": { "version": "1.0.10", @@ -22298,9 +22260,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", - "dev": true, - "optional": true, - "peer": true + "dev": true }, "cross-env": { "version": "5.2.1", @@ -22846,9 +22806,7 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", - "dev": true, - "optional": true, - "peer": true + "dev": true }, "diff-sequences": { "version": "27.0.6", @@ -26830,9 +26788,7 @@ "version": "1.3.6", "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", - "dev": true, - "optional": true, - "peer": true + "dev": true }, "makeerror": { "version": "1.0.11", @@ -30738,8 +30694,6 @@ "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.4.0.tgz", "integrity": "sha512-g0FlPvvCXSIO1JDF6S232P5jPYqBkRL9qly81ZgAOSU7rwI0stphCgd2kLiCrU9DjQCrJMWEqcNSjQL02s6d8A==", "dev": true, - "optional": true, - "peer": true, "requires": { "@cspotcode/source-map-support": "0.7.0", "@tsconfig/node10": "^1.0.7", @@ -30759,9 +30713,7 @@ "version": "8.2.0", "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", - "dev": true, - "optional": true, - "peer": true + "dev": true } } }, @@ -31565,9 +31517,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", - "dev": true, - "optional": true, - "peer": true + "dev": true }, "yocto-queue": { "version": "0.1.0", diff --git a/package.json b/package.json index d409d65e..10cb0ec1 100644 --- a/package.json +++ b/package.json @@ -109,6 +109,8 @@ "shelljs": "^0.8.4", "terser-webpack-plugin": "^4.2.3", "ts-loader": "^9.2.6", + "ts-node": "^10.4.0", + "tsconfig-paths": "^3.12.0", "typescript": "^4.5.4", "webpack": "^5.61.0", "webpack-cli": "^4.9.1", diff --git a/tsconfig.json b/tsconfig.json index a4404e65..b41433f6 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -55,5 +55,58 @@ ], "exclude": [ "./node_modules" - ] + ], + + /** + * Configuring `ts-node` for running TypeScript files + * -------------------------------------------------- + * - Website: https://typestrong.org/ts-node/docs/configuration + * - Usage: https://github.com/TypeStrong/ts-node#api + * - Options: https://github.com/TypeStrong/ts-node#options + * + * Most of the description is in the .npmrc, but the gist is that this allows running TypeScript files without + * having to compile them to JavaScript first, i.e. no more `tsc && node output.js`! + * + * It does so by using a custom NodeJS module loader, so we can't use `--experimental-specifier-resolution=node`. + * Since the project doesn't use `type: module` but does use `.mjs` files, we must override the base tsconfig's + * `module` field - which is set to `ESNext` for source code - to be CJS. This can be reverted when package.json + * is changed to `type: module`. See the .npmrc for more details. + * + * + * Regarding `paths` import aliases + * -------------------------------- + * Other than avoiding unnecessary disk writes to run TypeScript files, ts-node is particularly + * helpful because without it, `paths` import aliases aren't respected in the local environment, even if + * they're defined in tsconfig. `tsconfig-paths` fixes this without having to duplicate the definitions in + * package.json like you would if you used `module-alias` (https://www.npmjs.com/package/module-alias). + * To use it, we must require `tsconfig-paths/register` so it's loaded before the `node` process starts + * running ts-node. This also helps with the lack of alias translation when building with `tsc` (especially + * if `target` is ES >= 2019 where aliases are supported). + * This is needed for ts-node regardless of how it's called (see .npmrc for a list of ways to call ts-node). + * Alternatively, we could add `-r tsconfig-paths/register` in the command line, but putting it here + * ensures it works all the time, making npm scripts more DRY. + */ + "ts-node": { + "require": [ + "tsconfig-paths/register" + ], + "preferTsExts": true, + "experimentalReplAwait": true, + /** + * Makes ts-node run faster by skipping type checking (see: https://github.com/TypeStrong/ts-node/discussions/1276). + * Ironically, in multiple places (website, GitHub issues, etc.), the ts-node team recommends disabling + * type chceking (even though it's active by default) because type checking, building, and other "formal" + * app operations should use `tsc` instead. + */ + "transpileModule": false, +// "moduleTypes": { // Activate this if you change package.json to `type: module` +// "*.ts": "esm", +// "*.json": "esm" +// }, + "compilerOptions": { + "module": "CommonJS", + // Remove isolatedModules since ts-node scripts aren't being parsed by Babel, Webpack, or another parser + "isolatedModules": false + } + } }