From 6e6bf63445dbf481a11e577922ca2e1ed319bd88 Mon Sep 17 00:00:00 2001 From: Andrew Bradley Date: Sun, 26 Sep 2021 18:58:22 -0400 Subject: [PATCH] Refactor tests and bump TS version (#1475) * initial commit, WIP * Refactoring tests into multiple files; moving away from macros * More test refactor & fix * revert ts-node API changes so that this branch is fully dedicated to test refactor * upgrade deps as needed * try improving flaky test * lint-fix * Replacing more chai with expect; WIP fixing timing issues in REPL tests * Attempted fix * Fix? * fix test flakiness --- ava.config.js | 8 + package-lock.json | 473 +++---- package.json | 15 +- src/test/exec-helpers.ts | 97 ++ src/test/helpers.ts | 157 +++ src/test/index.spec.ts | 1618 +++++------------------- src/test/macros.ts | 108 -- src/test/register.spec.ts | 187 +++ src/test/repl/helpers.ts | 103 ++ src/test/{ => repl}/node-repl-tla.ts | 12 +- src/test/repl/repl-environment.spec.ts | 472 +++++++ src/test/repl/repl.spec.ts | 290 +++++ src/test/testlib.ts | 8 +- 13 files changed, 1851 insertions(+), 1697 deletions(-) create mode 100644 ava.config.js create mode 100644 src/test/exec-helpers.ts create mode 100644 src/test/helpers.ts delete mode 100644 src/test/macros.ts create mode 100644 src/test/register.spec.ts create mode 100644 src/test/repl/helpers.ts rename src/test/{ => repl}/node-repl-tla.ts (97%) create mode 100644 src/test/repl/repl-environment.spec.ts create mode 100644 src/test/repl/repl.spec.ts diff --git a/ava.config.js b/ava.config.js new file mode 100644 index 000000000..9cf877b83 --- /dev/null +++ b/ava.config.js @@ -0,0 +1,8 @@ +export default { + files: ['dist/test/**/*.spec.js'], + failWithoutAssertions: false, + environmentVariables: { + ts_node_install_lock: `id-${Math.floor(Math.random() * 10e9)}`, + }, + timeout: '300s', +}; diff --git a/package-lock.json b/package-lock.json index bddb6e80f..3f8ca56be 100644 --- a/package-lock.json +++ b/package-lock.json @@ -896,9 +896,9 @@ } }, "@types/json-schema": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.4.tgz", - "integrity": "sha1-OP1z3f2bVaux4bLtV4y1W9e30zk=", + "version": "7.0.9", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.9.tgz", + "integrity": "sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ==", "dev": true }, "@types/lodash": { @@ -925,6 +925,15 @@ "integrity": "sha512-f5j5b/Gf71L+dbqxIpQ4Z2WlmI/mPJ0fOkGGmFgtb6sAu97EPczzbS3/tJKxmcYDj55OX6ssqwDAWOHIYDRDGA==", "dev": true }, + "@types/proper-lockfile": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@types/proper-lockfile/-/proper-lockfile-4.1.2.tgz", + "integrity": "sha512-kd4LMvcnpYkspDcp7rmXKedn8iJSCoa331zRRamUp5oanKt/CefbEGPQP7G89enz7sKD4bvsr8mHSsC8j5WOvA==", + "dev": true, + "requires": { + "@types/retry": "*" + } + }, "@types/proxyquire": { "version": "1.3.28", "resolved": "https://registry.npmjs.org/@types/proxyquire/-/proxyquire-1.3.28.tgz", @@ -937,6 +946,12 @@ "integrity": "sha512-ft7OuDGUo39e+9LGwUewf2RyEaNBOjWbHUmD5bzjNuSuDabccE/1IuO7iR0dkzLjVUKxTMq69E+FmKfbgBcfbQ==", "dev": true }, + "@types/retry": { + "version": "0.12.1", + "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.1.tgz", + "integrity": "sha512-xoDlM2S4ortawSWORYqsdU+2rxdh4LRW9ytc3zmT37RIKQh6IHyKwwtKhKis9ah8ol07DCkZxPt8BBvPjC6v4g==", + "dev": true + }, "@types/rimraf": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@types/rimraf/-/rimraf-3.0.0.tgz", @@ -1156,12 +1171,6 @@ "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", "dev": true }, - "at-least-node": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", - "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", - "dev": true - }, "ava": { "version": "3.15.0", "resolved": "https://registry.npmjs.org/ava/-/ava-3.15.0.tgz", @@ -1787,42 +1796,14 @@ } }, "cliui": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", - "integrity": "sha1-3u/P2y6AB4SqNPRvoI4GhRx7u8U=", + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", "dev": true, "requires": { - "string-width": "^3.1.0", - "strip-ansi": "^5.2.0", - "wrap-ansi": "^5.1.0" - }, - "dependencies": { - "ansi-regex": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", - "integrity": "sha1-i5+PCM8ay4Q3Vqg5yox+MWjFGZc=", - "dev": true - }, - "string-width": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", - "integrity": "sha1-InZ74htirxCBV0MG9prFG2IgOWE=", - "dev": true, - "requires": { - "emoji-regex": "^7.0.1", - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^5.1.0" - } - }, - "strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha1-jJpTb+tq/JYr36WxBKUJHBrZwK4=", - "dev": true, - "requires": { - "ansi-regex": "^4.1.0" - } - } + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" } }, "clone": { @@ -1864,12 +1845,6 @@ "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", "dev": true }, - "colors": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", - "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==", - "dev": true - }, "commander": { "version": "2.20.3", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", @@ -2386,18 +2361,6 @@ "integrity": "sha512-33X7H/wdfO99GdRLLgkjUrD4geAFdq/Uv0kl3HD4da6HDixd2GUg8Mw7dahLCV9r/EARkmtYBB6Tch4EEokFTQ==", "dev": true }, - "fs-extra": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", - "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", - "dev": true, - "requires": { - "at-least-node": "^1.0.0", - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - } - }, "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -2529,19 +2492,6 @@ "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==", "dev": true }, - "handlebars": { - "version": "4.7.7", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.7.tgz", - "integrity": "sha512-aAcXm5OAfE/8IXkcZvCepKU3VzW1/39Fb5ZuqMtgI/hT8X2YgoMvBY5dLhq/cpOvw7Lk1nK/UF71aLG/ZnVYRA==", - "dev": true, - "requires": { - "minimist": "^1.2.5", - "neo-async": "^2.6.0", - "source-map": "^0.6.1", - "uglify-js": "^3.1.4", - "wordwrap": "^1.0.0" - } - }, "has": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", @@ -2673,12 +2623,6 @@ "integrity": "sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==", "dev": true }, - "interpret": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.4.0.tgz", - "integrity": "sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==", - "dev": true - }, "irregular-plurals": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/irregular-plurals/-/irregular-plurals-3.2.0.tgz", @@ -3075,15 +3019,11 @@ "minimist": "^1.2.5" } }, - "jsonfile": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", - "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", - "dev": true, - "requires": { - "graceful-fs": "^4.1.6", - "universalify": "^2.0.0" - } + "jsonc-parser": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.0.0.tgz", + "integrity": "sha512-fQzRfAbIBnR0IQvftw9FJveWiHp72Fg20giDrHz6TdfB12UH/uue0D3hm57UB5KgAVuniLMCaS8P1IMj9NR7cA==", + "dev": true }, "jsonify": { "version": "0.0.0", @@ -3238,9 +3178,9 @@ } }, "marked": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/marked/-/marked-2.0.0.tgz", - "integrity": "sha512-NqRSh2+LlN2NInpqTQnS614Y/3NkVMFFU6sJlRFEpxJ/LHuK/qJECH7/fXZjk4VZstPW/Pevjil/VtSONsLc7Q==", + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/marked/-/marked-3.0.4.tgz", + "integrity": "sha512-jBo8AOayNaEcvBhNobg6/BLhdsK3NvnKWJg33MAAPbvTWiG4QBn9gpW1+7RssrKu4K1dKlN+0goVQwV41xEfOA==", "dev": true }, "matcher": { @@ -3348,12 +3288,6 @@ "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", "dev": true }, - "neo-async": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", - "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", - "dev": true - }, "node-preload": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/node-preload/-/node-preload-0.2.1.tgz", @@ -3991,12 +3925,6 @@ "fromentries": "^1.2.0" } }, - "progress": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", - "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", - "dev": true - }, "prop-types": { "version": "15.7.2", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz", @@ -4008,6 +3936,17 @@ "react-is": "^16.8.1" } }, + "proper-lockfile": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz", + "integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==", + "dev": true, + "requires": { + "graceful-fs": "^4.2.4", + "retry": "^0.12.0", + "signal-exit": "^3.0.2" + } + }, "proxyquire": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/proxyquire/-/proxyquire-2.0.0.tgz", @@ -4139,15 +4078,6 @@ "picomatch": "^2.2.1" } }, - "rechoir": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz", - "integrity": "sha1-hSBLVNuoLVdC4oyWdW70OvUOM4Q=", - "dev": true, - "requires": { - "resolve": "^1.1.6" - } - }, "registry-auth-token": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-4.2.1.tgz", @@ -4227,6 +4157,12 @@ "signal-exit": "^3.0.2" } }, + "retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha1-G0KmJmoh8HQh0bC1S33BZ7AcATs=", + "dev": true + }, "reusify": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", @@ -4334,25 +4270,15 @@ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "dev": true }, - "shelljs": { - "version": "0.8.4", - "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.8.4.tgz", - "integrity": "sha512-7gk3UZ9kOfPLIAbslLzyWeGiEqx9e3rxwZM0KE6EL8GlGwjym9Mrlx5/p33bWTu9YG6vcS4MBxYZDHYr5lr8BQ==", - "dev": true, - "requires": { - "glob": "^7.0.0", - "interpret": "^1.0.0", - "rechoir": "^0.6.2" - } - }, "shiki": { - "version": "0.9.2", - "resolved": "https://registry.npmjs.org/shiki/-/shiki-0.9.2.tgz", - "integrity": "sha512-BjUCxVbxMnvjs8jC4b+BQ808vwjJ9Q8NtLqPwXShZ307HdXiDFYP968ORSVfaTNNSWYDBYdMnVKJ0fYNsoZUBA==", + "version": "0.9.11", + "resolved": "https://registry.npmjs.org/shiki/-/shiki-0.9.11.tgz", + "integrity": "sha512-tjruNTLFhU0hruCPoJP0y+B9LKOmcqUhTpxn7pcJB3fa+04gFChuEmxmrUfOJ7ZO6Jd+HwMnDHgY3lv3Tqonuw==", "dev": true, "requires": { + "jsonc-parser": "^3.0.0", "onigasm": "^2.2.5", - "vscode-textmate": "^5.2.0" + "vscode-textmate": "5.2.0" } }, "signal-exit": { @@ -4512,6 +4438,31 @@ "integrity": "sha512-a1uQGz7IyVy9YwhqjZIZu1c8JO8dNIe20xBmSS6qu9kv++k3JGzCVmprbNN5Kn+BgzD5E7YYwg1CcjuJMRNsvg==", "dev": true }, + "string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "dependencies": { + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true + } + } + }, "string.prototype.trimleft": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/string.prototype.trimleft/-/string.prototype.trimleft-2.1.1.tgz", @@ -4549,6 +4500,23 @@ } } }, + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.1" + }, + "dependencies": { + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true + } + } + }, "strip-bom": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", @@ -4682,6 +4650,26 @@ "integrity": "sha1-n5up2e+odkw4dpi8v+sshI8RrbM=", "dev": true }, + "ts-node": { + "version": "10.2.1", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.2.1.tgz", + "integrity": "sha512-hCnyOyuGmD5wHleOQX6NIjJtYVIO8bPP8F2acWkB4W06wdlkgyvJtubO/I9NkI88hCFECbsEgoLc0VNkYmcSfw==", + "dev": true, + "requires": { + "@cspotcode/source-map-support": "0.6.1", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "yn": "3.1.1" + } + }, "tslib": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", @@ -4710,67 +4698,65 @@ } }, "typedoc": { - "version": "0.20.28", - "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.20.28.tgz", - "integrity": "sha512-8j0T8u9FuyDkoe+M/3cyoaGJSVgXCY9KwVoo7TLUnmQuzXwqH+wkScY530ZEdK6G39UZ2LFTYPIrL5eykWjx6A==", + "version": "0.22.4", + "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.22.4.tgz", + "integrity": "sha512-M/a8NnPxq3/iZNNVjzFCK5gu4m//HTJIPbSS0JQVbkHJPP9wyepR12agylWTSqeVZe0xsbidVtO26+PP7iD/jw==", "dev": true, "requires": { - "colors": "^1.4.0", - "fs-extra": "^9.1.0", - "handlebars": "^4.7.7", - "lodash": "^4.17.21", + "glob": "^7.1.7", "lunr": "^2.3.9", - "marked": "^2.0.0", - "minimatch": "^3.0.0", - "progress": "^2.0.3", - "shelljs": "^0.8.4", - "shiki": "^0.9.2", - "typedoc-default-themes": "^0.12.7" + "marked": "^3.0.4", + "minimatch": "^3.0.4", + "shiki": "^0.9.11" }, "dependencies": { - "lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true + "glob": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } } } }, - "typedoc-default-themes": { - "version": "0.12.7", - "resolved": "https://registry.npmjs.org/typedoc-default-themes/-/typedoc-default-themes-0.12.7.tgz", - "integrity": "sha512-0XAuGEqID+gon1+fhi4LycOEFM+5Mvm2PjwaiVZNAzU7pn3G2DEpsoXnFOPlLDnHY6ZW0BY0nO7ur9fHOFkBLQ==", - "dev": true - }, "typescript": { - "version": "4.3.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.3.5.tgz", - "integrity": "sha512-DqQgihaQ9cUrskJo9kIyW/+g0Vxsk8cDtZ52a3NGh0YNTfpUSArXSohyUGnvbPazEPLu398C0UxmKSOrPumUzA==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.4.3.tgz", + "integrity": "sha512-4xfscpisVgqqDfPaJo5vkd+Qd/ItkoagnHpufr+i2QCHBsNYp+G7UAoyFl8aPtx879u38wPV65rZ8qbGZijalA==", "dev": true }, "typescript-json-schema": { - "version": "0.42.0", - "resolved": "https://registry.npmjs.org/typescript-json-schema/-/typescript-json-schema-0.42.0.tgz", - "integrity": "sha1-aV8hKnLZHUfAYFNx3Gl1l7eBfBs=", + "version": "0.51.0", + "resolved": "https://registry.npmjs.org/typescript-json-schema/-/typescript-json-schema-0.51.0.tgz", + "integrity": "sha512-POhWbUNs2oaBti1W9k/JwS+uDsaZD9J/KQiZ/iXRQEOD0lTn9VmshIls9tn+A9X6O+smPjeEz5NEy6WTkCCzrQ==", "dev": true, "requires": { - "@types/json-schema": "^7.0.3", - "glob": "~7.1.4", + "@types/json-schema": "^7.0.9", + "@types/node": "^16.9.2", + "glob": "^7.1.7", "json-stable-stringify": "^1.0.1", - "typescript": "^3.5.3", - "yargs": "^14.0.0" + "ts-node": "^10.2.1", + "typescript": "~4.2.3", + "yargs": "^17.1.1" }, "dependencies": { - "ansi-regex": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", - "integrity": "sha1-i5+PCM8ay4Q3Vqg5yox+MWjFGZc=", + "@types/node": { + "version": "16.10.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.10.1.tgz", + "integrity": "sha512-4/Z9DMPKFexZj/Gn3LylFgamNKHm4K3QDi0gz9B26Uk0c8izYf97B5fxfpspMNkWlFupblKM/nV8+NA9Ffvr+w==", "dev": true }, "glob": { - "version": "7.1.6", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", - "integrity": "sha1-FB8zuBp8JJLhJVlDB0gMRmeSeKY=", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", "dev": true, "requires": { "fs.realpath": "^1.0.0", @@ -4781,70 +4767,14 @@ "path-is-absolute": "^1.0.0" } }, - "string-width": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", - "integrity": "sha1-InZ74htirxCBV0MG9prFG2IgOWE=", - "dev": true, - "requires": { - "emoji-regex": "^7.0.1", - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^5.1.0" - } - }, - "strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha1-jJpTb+tq/JYr36WxBKUJHBrZwK4=", - "dev": true, - "requires": { - "ansi-regex": "^4.1.0" - } - }, "typescript": { - "version": "3.9.7", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.7.tgz", - "integrity": "sha512-BLbiRkiBzAwsjut4x/dsibSTB6yWpwT5qWmC2OfuCg3GgVQCSgMs4vEctYPhsaGtd0AeuuHMkjZ2h2WG8MSzRw==", + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.2.4.tgz", + "integrity": "sha512-V+evlYHZnQkaz8TRBuxTA92yZBPotr5H+WhQ7bD3hZUndx5tGOa1fuCgeSjxAzM1RiN5IzvadIXTVefuuwZCRg==", "dev": true - }, - "yargs": { - "version": "14.2.3", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-14.2.3.tgz", - "integrity": "sha1-Ghw+3O0a+yov6jNgS8bR2NaIpBQ=", - "dev": true, - "requires": { - "cliui": "^5.0.0", - "decamelize": "^1.2.0", - "find-up": "^3.0.0", - "get-caller-file": "^2.0.1", - "require-directory": "^2.1.1", - "require-main-filename": "^2.0.0", - "set-blocking": "^2.0.0", - "string-width": "^3.0.0", - "which-module": "^2.0.0", - "y18n": "^4.0.0", - "yargs-parser": "^15.0.1" - } - }, - "yargs-parser": { - "version": "15.0.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-15.0.1.tgz", - "integrity": "sha1-VHhq9AuCDcsvuAJbEbTWWddjI7M=", - "dev": true, - "requires": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - } } } }, - "uglify-js": { - "version": "3.12.8", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.12.8.tgz", - "integrity": "sha512-fvBeuXOsvqjecUtF/l1dwsrrf5y2BCUk9AOJGzGcm6tE7vegku5u/YvqjyDaAGr422PLoLnrxg3EnRvTqsdC1w==", - "dev": true, - "optional": true - }, "unique-string": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz", @@ -4854,12 +4784,6 @@ "crypto-random-string": "^2.0.0" } }, - "universalify": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", - "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", - "dev": true - }, "update-notifier": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/update-notifier/-/update-notifier-5.1.0.tgz", @@ -5036,48 +4960,40 @@ } } }, - "wordwrap": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", - "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=", - "dev": true - }, "wrap-ansi": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", - "integrity": "sha1-H9H2cjXVttD+54EFYAG/tpTAOwk=", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", "dev": true, "requires": { - "ansi-styles": "^3.2.0", - "string-width": "^3.0.0", - "strip-ansi": "^5.0.0" + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" }, "dependencies": { - "ansi-regex": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", - "integrity": "sha1-i5+PCM8ay4Q3Vqg5yox+MWjFGZc=", - "dev": true - }, - "string-width": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", - "integrity": "sha1-InZ74htirxCBV0MG9prFG2IgOWE=", + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, "requires": { - "emoji-regex": "^7.0.1", - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^5.1.0" + "color-convert": "^2.0.1" } }, - "strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha1-jJpTb+tq/JYr36WxBKUJHBrZwK4=", + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, "requires": { - "ansi-regex": "^4.1.0" + "color-name": "~1.1.4" } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true } } }, @@ -5117,6 +5033,35 @@ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", "dev": true }, + "yargs": { + "version": "17.2.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.2.1.tgz", + "integrity": "sha512-XfR8du6ua4K6uLGm5S6fA+FIJom/MdJcFNVY8geLlp2v8GYbOXD4EB1tPNZsRn4vBzKGMgb5DRZMeWuFc2GO8Q==", + "dev": true, + "requires": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "dependencies": { + "y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true + } + } + }, + "yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true + }, "yn": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", diff --git a/package.json b/package.json index d30cff213..e440ef3b0 100644 --- a/package.json +++ b/package.json @@ -103,13 +103,6 @@ "url": "https://github.com/TypeStrong/ts-node/issues" }, "homepage": "https://typestrong.org/ts-node", - "ava": { - "files": [ - "dist/test/*.spec.js" - ], - "failWithoutAssertions": false, - "timeout": "300s" - }, "devDependencies": { "@microsoft/api-extractor": "^7.15.2", "@swc/core": ">=1.2.50", @@ -118,6 +111,7 @@ "@types/diff": "^4.0.2", "@types/lodash": "^4.14.151", "@types/node": "13.13.5", + "@types/proper-lockfile": "^4.1.2", "@types/proxyquire": "^1.3.28", "@types/react": "^16.0.2", "@types/rimraf": "^3.0.0", @@ -132,14 +126,15 @@ "ntypescript": "^1.201507091536.1", "nyc": "^15.0.1", "prettier": "^2.2.1", + "proper-lockfile": "^4.1.2", "proxyquire": "^2.0.0", "react": "^16.14.0", "rimraf": "^3.0.0", "semver": "^7.1.3", "throat": "^6.0.1", - "typedoc": "^0.20.28", - "typescript": "4.3.5", - "typescript-json-schema": "^0.42.0", + "typedoc": "^0.22.4", + "typescript": "4.4.3", + "typescript-json-schema": "^0.51.0", "util.promisify": "^1.0.1" }, "peerDependencies": { diff --git a/src/test/exec-helpers.ts b/src/test/exec-helpers.ts new file mode 100644 index 000000000..87a797874 --- /dev/null +++ b/src/test/exec-helpers.ts @@ -0,0 +1,97 @@ +import type { ChildProcess, ExecException, ExecOptions } from 'child_process'; +import { exec as childProcessExec } from 'child_process'; +import * as expect from 'expect'; + +export type ExecReturn = Promise & { child: ChildProcess }; +export interface ExecResult { + stdout: string; + stderr: string; + err: null | ExecException; + child: ChildProcess; +} + +export function createExec>( + preBoundOptions?: T +) { + /** + * Helper to exec a child process. + * Returns a Promise and a reference to the child process to suite multiple situations. + * Promise resolves with the process's stdout, stderr, and error. + */ + return function exec( + cmd: string, + opts?: Pick> & + Partial> + ): ExecReturn { + let child!: ChildProcess; + return Object.assign( + new Promise((resolve, reject) => { + child = childProcessExec( + cmd, + { + ...preBoundOptions, + ...opts, + }, + (err, stdout, stderr) => { + resolve({ err, stdout, stderr, child }); + } + ); + }), + { + child, + } + ); + }; +} + +const defaultExec = createExec(); + +export interface ExecTesterOptions { + cmd: string; + flags?: string; + env?: Record; + stdin?: string; + expectError?: boolean; + exec?: typeof defaultExec; +} + +/** + * Create a function that launches a CLI command, optionally pipes stdin, optionally sets env vars, + * optionally runs a couple baked-in assertions, and returns the results for additional assertions. + */ +export function createExecTester>( + preBoundOptions: T +) { + return async function ( + options: Pick< + ExecTesterOptions, + Exclude + > & + Partial> + ) { + const { + cmd, + flags = '', + stdin, + expectError = false, + env, + exec = defaultExec, + } = { + ...preBoundOptions, + ...options, + }; + const execPromise = exec(`${cmd} ${flags}`, { + env: { ...process.env, ...env }, + }); + if (stdin !== undefined) { + execPromise.child.stdin!.end(stdin); + } + const { err, stdout, stderr } = await execPromise; + if (expectError) { + expect(err).toBeDefined(); + } else { + expect(err).toBeNull(); + } + return { stdout, stderr, err }; + }; +} diff --git a/src/test/helpers.ts b/src/test/helpers.ts new file mode 100644 index 000000000..6683b0558 --- /dev/null +++ b/src/test/helpers.ts @@ -0,0 +1,157 @@ +import { NodeFS } from '@yarnpkg/fslib'; +import { exec as childProcessExec } from 'child_process'; +import * as promisify from 'util.promisify'; +import { sync as rimrafSync } from 'rimraf'; +import { existsSync, readFileSync, unlinkSync, writeFileSync } from 'fs'; +import { join, resolve } from 'path'; +import * as fs from 'fs'; +import { lock } from 'proper-lockfile'; +import type { Readable } from 'stream'; +/** + * types from ts-node under test + */ +import type * as tsNodeTypes from '../index'; +import type _createRequire from 'create-require'; +import { once } from 'lodash'; +import semver = require('semver'); +import { isConstructSignatureDeclaration } from 'typescript'; +const createRequire: typeof _createRequire = require('create-require'); +export { tsNodeTypes }; + +export const ROOT_DIR = resolve(__dirname, '../..'); +export const DIST_DIR = resolve(__dirname, '..'); +export const TEST_DIR = join(__dirname, '../../tests'); +export const PROJECT = join(TEST_DIR, 'tsconfig.json'); +export const BIN_PATH = join(TEST_DIR, 'node_modules/.bin/ts-node'); +export const BIN_SCRIPT_PATH = join( + TEST_DIR, + 'node_modules/.bin/ts-node-script' +); +export const BIN_CWD_PATH = join(TEST_DIR, 'node_modules/.bin/ts-node-cwd'); +/** Default `ts-node --project` invocation */ +export const CMD_TS_NODE_WITH_PROJECT_FLAG = `"${BIN_PATH}" --project "${PROJECT}"`; +/** Default `ts-node` invocation without `--project` */ +export const CMD_TS_NODE_WITHOUT_PROJECT_FLAG = `"${BIN_PATH}"`; +export const EXPERIMENTAL_MODULES_FLAG = semver.gte(process.version, '12.17.0') + ? '' + : '--experimental-modules'; +export const CMD_ESM_LOADER_WITHOUT_PROJECT = `node ${EXPERIMENTAL_MODULES_FLAG} --loader ts-node/esm`; + +// `createRequire` does not exist on older node versions +export const testsDirRequire = createRequire(join(TEST_DIR, 'index.js')); + +export const xfs = new NodeFS(fs); + +/** Pass to `test.context()` to get access to the ts-node API under test */ +export const contextTsNodeUnderTest = once(async () => { + await installTsNode(); + const tsNodeUnderTest = testsDirRequire('ts-node'); + return { + tsNodeUnderTest, + }; +}); + +const ts_node_install_lock = process.env.ts_node_install_lock as string; +const lockPath = join(__dirname, ts_node_install_lock); + +interface InstallationResult { + error: string | null; +} + +/** + * Pack and install ts-node locally, necessary to test package "exports" + * FS locking b/c tests run in separate processes + */ +export async function installTsNode() { + await lockedMemoizedOperation(lockPath, async () => { + const totalTries = process.platform === 'win32' ? 5 : 1; + let tries = 0; + while (true) { + try { + rimrafSync(join(TEST_DIR, 'node_modules')); + await promisify(childProcessExec)(`npm install`, { cwd: TEST_DIR }); + const packageLockPath = join(TEST_DIR, 'package-lock.json'); + existsSync(packageLockPath) && unlinkSync(packageLockPath); + break; + } catch (e) { + tries++; + if (tries >= totalTries) throw e; + } + } + }); +} + +/** + * Attempt an operation once across multiple processes, using filesystem locking. + * If it was executed already by another process, and it errored, throw the same error message. + */ +async function lockedMemoizedOperation( + lockPath: string, + operation: () => Promise +) { + const releaseLock = await lock(lockPath, { + realpath: false, + stale: 120e3, + retries: { + retries: 120, + maxTimeout: 1000, + }, + }); + try { + const operationHappened = existsSync(lockPath); + if (operationHappened) { + const result: InstallationResult = JSON.parse( + readFileSync(lockPath, 'utf8') + ); + if (result.error) throw result.error; + } else { + const result: InstallationResult = { error: null }; + try { + await operation(); + } catch (e) { + result.error = `${e}`; + throw e; + } finally { + writeFileSync(lockPath, JSON.stringify(result)); + } + } + } finally { + releaseLock(); + } +} + +/** + * Get a stream into a string. + * Will resolve early if + */ +export function getStream(stream: Readable, waitForPattern?: string | RegExp) { + let resolve: (value: string) => void; + const promise = new Promise((res) => { + resolve = res; + }); + const received: Buffer[] = []; + let combinedBuffer: Buffer = Buffer.concat([]); + let combinedString: string = ''; + + stream.on('data', (data) => { + received.push(data); + combine(); + if ( + (typeof waitForPattern === 'string' && + combinedString.indexOf(waitForPattern) >= 0) || + (waitForPattern instanceof RegExp && combinedString.match(waitForPattern)) + ) + resolve(combinedString); + combinedBuffer = Buffer.concat(received); + }); + stream.on('end', () => { + resolve(combinedString); + }); + + return promise; + + function combine() { + combinedBuffer = Buffer.concat(received); + combinedString = combinedBuffer.toString('utf8'); + } +} diff --git a/src/test/index.spec.ts b/src/test/index.spec.ts index 6b3c337eb..3ab8ad950 100644 --- a/src/test/index.spec.ts +++ b/src/test/index.spec.ts @@ -1,156 +1,42 @@ -import { test } from './testlib'; -import { expect } from 'chai'; -import * as exp from 'expect'; -import { - ChildProcess, - exec as childProcessExec, - ExecException, - ExecOptions, -} from 'child_process'; -import { dirname, join, resolve, sep as pathSep } from 'path'; -import { homedir, tmpdir } from 'os'; +import { _test } from './testlib'; +import * as expect from 'expect'; +import { join, resolve, sep as pathSep } from 'path'; +import { tmpdir } from 'os'; import semver = require('semver'); import ts = require('typescript'); -import proxyquire = require('proxyquire'); -import type * as tsNodeTypes from '../index'; -import * as fs from 'fs'; -import { unlinkSync, existsSync, lstatSync, mkdtempSync } from 'fs'; -import { NodeFS, npath } from '@yarnpkg/fslib'; -import * as promisify from 'util.promisify'; -import { sync as rimrafSync } from 'rimraf'; +import { lstatSync, mkdtempSync } from 'fs'; +import { npath } from '@yarnpkg/fslib'; import type _createRequire from 'create-require'; -const createRequire: typeof _createRequire = require('create-require'); import { pathToFileURL } from 'url'; -import type * as Module from 'module'; -import { PassThrough } from 'stream'; -import * as getStream from 'get-stream'; -import { once } from 'lodash'; -import { upstreamTopLevelAwaitTests } from './node-repl-tla'; -import { createMacrosAndHelpers, ExecMacroAssertionCallback } from './macros'; - -const xfs = new NodeFS(fs); - -async function settled(fn: () => Promise | T) { - try { - return { - status: 'fulfilled', - value: await fn(), - }; - } catch (reason) { - return { - status: 'rejected', - reason, - }; - } -} - -interface CreateReplViaApiOptions { - createReplOpts?: Partial; - createServiceOpts?: Partial; -} -function createReplViaApi({ - createReplOpts, - createServiceOpts, -}: CreateReplViaApiOptions = {}) { - const stdin = new PassThrough(); - const stdout = new PassThrough(); - const stderr = new PassThrough(); - const replService = createRepl({ - stdin, - stdout, - stderr, - ...createReplOpts, - }); - const service = create({ - ...replService.evalAwarePartialHost, - project: `${TEST_DIR}/tsconfig.json`, - ...createServiceOpts, - }); - replService.setService(service); - return { stdin, stdout, stderr, replService, service }; -} - -// Todo combine with replApiMacro -async function executeInRepl( - input: string, - { - waitMs = 1e3, - startOptions, - ...rest - }: CreateReplViaApiOptions & { - waitMs?: number; - startOptions?: Parameters[0]; - } = {} -) { - const { stdin, stdout, stderr, replService } = createReplViaApi(rest); - - replService.startInternal(startOptions); - - stdin.write(input); - stdin.end(); - await promisify(setTimeout)(waitMs); - stdout.end(); - stderr.end(); - - return { - stdin, - stdout: await getStream(stdout), - stderr: await getStream(stderr), - }; -} - -const ROOT_DIR = resolve(__dirname, '../..'); -const DIST_DIR = resolve(__dirname, '..'); -const TEST_DIR = join(__dirname, '../../tests'); -const PROJECT = join(TEST_DIR, 'tsconfig.json'); -const BIN_PATH = join(TEST_DIR, 'node_modules/.bin/ts-node'); -const BIN_SCRIPT_PATH = join(TEST_DIR, 'node_modules/.bin/ts-node-script'); -const BIN_CWD_PATH = join(TEST_DIR, 'node_modules/.bin/ts-node-cwd'); - -const SOURCE_MAP_REGEXP = /\/\/# sourceMappingURL=data:application\/json;charset=utf\-8;base64,[\w\+]+=*$/; - -// `createRequire` does not exist on older node versions -const testsDirRequire = createRequire(join(TEST_DIR, 'index.js')); - -// Set after ts-node is installed locally -let { register, create, VERSION, createRepl }: typeof tsNodeTypes = {} as any; - -const { exec, createExecMacro } = createMacrosAndHelpers({ - test, - defaultCwd: TEST_DIR, +import { createExec } from './exec-helpers'; +import { + BIN_CWD_PATH, + BIN_PATH, + BIN_SCRIPT_PATH, + DIST_DIR, + ROOT_DIR, + TEST_DIR, + testsDirRequire, + tsNodeTypes, + xfs, + contextTsNodeUnderTest, + CMD_TS_NODE_WITH_PROJECT_FLAG, + CMD_TS_NODE_WITHOUT_PROJECT_FLAG, + CMD_ESM_LOADER_WITHOUT_PROJECT, + EXPERIMENTAL_MODULES_FLAG, +} from './helpers'; + +const exec = createExec({ + cwd: TEST_DIR, }); -// Pack and install ts-node locally, necessary to test package "exports" -test.beforeAll(async () => { - const totalTries = process.platform === 'win32' ? 5 : 1; - let tries = 0; - while (true) { - try { - rimrafSync(join(TEST_DIR, 'node_modules')); - await promisify(childProcessExec)(`npm install`, { cwd: TEST_DIR }); - const packageLockPath = join(TEST_DIR, 'package-lock.json'); - existsSync(packageLockPath) && unlinkSync(packageLockPath); - break; - } catch (e) { - tries++; - if (tries >= totalTries) throw e; - } - } - ({ register, create, VERSION, createRepl } = testsDirRequire('ts-node')); -}); +const test = _test.context(contextTsNodeUnderTest); test.suite('ts-node', (test) => { - /** Default `ts-node --project` invocation */ - const cmd = `"${BIN_PATH}" --project "${PROJECT}"`; - /** Default `ts-node` invocation without `--project` */ - const cmdNoProject = `"${BIN_PATH}"`; - const experimentalModulesFlag = semver.gte(process.version, '12.17.0') - ? '' - : '--experimental-modules'; - const cmdEsmLoaderNoProject = `node ${experimentalModulesFlag} --loader ts-node/esm`; - - test('should export the correct version', () => { - expect(VERSION).to.equal(require('../../package.json').version); + test('should export the correct version', (t) => { + expect(t.context.tsNodeUnderTest.VERSION).toBe( + require('../../package.json').version + ); }); test('should export all CJS entrypoints', () => { // Ensure our package.json "exports" declaration allows `require()`ing all our entrypoints @@ -194,27 +80,35 @@ test.suite('ts-node', (test) => { test.suite('cli', (test) => { test('should execute cli', async () => { - const { err, stdout } = await exec(`${cmd} hello-world`); - expect(err).to.equal(null); - expect(stdout).to.equal('Hello, world!\n'); + const { err, stdout } = await exec( + `${CMD_TS_NODE_WITH_PROJECT_FLAG} hello-world` + ); + expect(err).toBe(null); + expect(stdout).toBe('Hello, world!\n'); }); test('shows usage via --help', async () => { - const { err, stdout } = await exec(`${cmdNoProject} --help`); - expect(err).to.equal(null); - expect(stdout).to.match(/Usage: ts-node /); + const { err, stdout } = await exec( + `${CMD_TS_NODE_WITHOUT_PROJECT_FLAG} --help` + ); + expect(err).toBe(null); + expect(stdout).toMatch(/Usage: ts-node /); }); test('shows version via -v', async () => { - const { err, stdout } = await exec(`${cmdNoProject} -v`); - expect(err).to.equal(null); - expect(stdout.trim()).to.equal( + const { err, stdout } = await exec( + `${CMD_TS_NODE_WITHOUT_PROJECT_FLAG} -v` + ); + expect(err).toBe(null); + expect(stdout.trim()).toBe( 'v' + testsDirRequire('ts-node/package').version ); }); test('shows version of compiler via -vv', async () => { - const { err, stdout } = await exec(`${cmdNoProject} -vv`); - expect(err).to.equal(null); - expect(stdout.trim()).to.equal( + const { err, stdout } = await exec( + `${CMD_TS_NODE_WITHOUT_PROJECT_FLAG} -vv` + ); + expect(err).toBe(null); + expect(stdout.trim()).toBe( `ts-node v${testsDirRequire('ts-node/package').version}\n` + `node ${process.version}\n` + `compiler v${testsDirRequire('typescript/package').version}` @@ -228,89 +122,93 @@ test.suite('ts-node', (test) => { cwd: TEST_DIR, } ); - expect(err).to.equal(null); - expect(stdout).to.equal('Hello, world!\n'); + expect(err).toBe(null); + expect(stdout).toBe('Hello, world!\n'); }); test('should execute cli with absolute path', async () => { const { err, stdout } = await exec( - `${cmd} "${join(TEST_DIR, 'hello-world')}"` + `${CMD_TS_NODE_WITH_PROJECT_FLAG} "${join(TEST_DIR, 'hello-world')}"` ); - expect(err).to.equal(null); - expect(stdout).to.equal('Hello, world!\n'); + expect(err).toBe(null); + expect(stdout).toBe('Hello, world!\n'); }); test('should print scripts', async () => { const { err, stdout } = await exec( - `${cmd} -pe "import { example } from './complex/index';example()"` + `${CMD_TS_NODE_WITH_PROJECT_FLAG} -pe "import { example } from './complex/index';example()"` ); - expect(err).to.equal(null); - expect(stdout).to.equal('example\n'); + expect(err).toBe(null); + expect(stdout).toBe('example\n'); }); test('should provide registered information globally', async () => { - const { err, stdout } = await exec(`${cmd} env`); - expect(err).to.equal(null); - expect(stdout).to.equal('object\n'); + const { err, stdout } = await exec( + `${CMD_TS_NODE_WITH_PROJECT_FLAG} env` + ); + expect(err).toBe(null); + expect(stdout).toBe('object\n'); }); test('should provide registered information on register', async () => { const { err, stdout } = await exec(`node -r ts-node/register env.ts`, { cwd: TEST_DIR, }); - expect(err).to.equal(null); - expect(stdout).to.equal('object\n'); + expect(err).toBe(null); + expect(stdout).toBe('object\n'); }); if (semver.gte(ts.version, '1.8.0')) { test('should allow js', async () => { const { err, stdout } = await exec( [ - cmd, + CMD_TS_NODE_WITH_PROJECT_FLAG, '-O "{\\"allowJs\\":true}"', '-pe "import { main } from \'./allow-js/run\';main()"', ].join(' ') ); - expect(err).to.equal(null); - expect(stdout).to.equal('hello world\n'); + expect(err).toBe(null); + expect(stdout).toBe('hello world\n'); }); test('should include jsx when `allow-js` true', async () => { const { err, stdout } = await exec( [ - cmd, + CMD_TS_NODE_WITH_PROJECT_FLAG, '-O "{\\"allowJs\\":true}"', '-pe "import { Foo2 } from \'./allow-js/with-jsx\'; Foo2.sayHi()"', ].join(' ') ); - expect(err).to.equal(null); - expect(stdout).to.equal('hello world\n'); + expect(err).toBe(null); + expect(stdout).toBe('hello world\n'); }); } test('should eval code', async () => { const { err, stdout } = await exec( - `${cmd} -e "import * as m from './module';console.log(m.example('test'))"` + `${CMD_TS_NODE_WITH_PROJECT_FLAG} -e "import * as m from './module';console.log(m.example('test'))"` ); - expect(err).to.equal(null); - expect(stdout).to.equal('TEST\n'); + expect(err).toBe(null); + expect(stdout).toBe('TEST\n'); }); test('should import empty files', async () => { - const { err, stdout } = await exec(`${cmd} -e "import './empty'"`); - expect(err).to.equal(null); - expect(stdout).to.equal(''); + const { err, stdout } = await exec( + `${CMD_TS_NODE_WITH_PROJECT_FLAG} -e "import './empty'"` + ); + expect(err).toBe(null); + expect(stdout).toBe(''); }); test('should throw errors', async () => { const { err } = await exec( - `${cmd} -e "import * as m from './module';console.log(m.example(123))"` + `${CMD_TS_NODE_WITH_PROJECT_FLAG} -e "import * as m from './module';console.log(m.example(123))"` ); if (err === null) { throw new Error('Command was expected to fail, but it succeeded.'); } - expect(err.message).to.match( + expect(err.message).toMatch( new RegExp( "TS2345: Argument of type '(?:number|123)' " + "is not assignable to parameter of type 'string'\\." @@ -320,24 +218,26 @@ test.suite('ts-node', (test) => { test('should be able to ignore diagnostic', async () => { const { err } = await exec( - `${cmd} --ignore-diagnostics 2345 -e "import * as m from './module';console.log(m.example(123))"` + `${CMD_TS_NODE_WITH_PROJECT_FLAG} --ignore-diagnostics 2345 -e "import * as m from './module';console.log(m.example(123))"` ); if (err === null) { throw new Error('Command was expected to fail, but it succeeded.'); } - expect(err.message).to.match( + expect(err.message).toMatch( /TypeError: (?:(?:undefined|foo\.toUpperCase) is not a function|.*has no method \'toUpperCase\')/ ); }); test('should work with source maps', async () => { - const { err } = await exec(`${cmd} "throw error"`); + const { err } = await exec( + `${CMD_TS_NODE_WITH_PROJECT_FLAG} "throw error"` + ); if (err === null) { throw new Error('Command was expected to fail, but it succeeded.'); } - expect(err.message).to.contain( + expect(err.message).toMatch( [ `${join(TEST_DIR, 'throw error.ts')}:100`, " bar() { throw new Error('this is a demo'); }", @@ -348,12 +248,14 @@ test.suite('ts-node', (test) => { }); test('should work with source maps in --transpile-only mode', async () => { - const { err } = await exec(`${cmd} --transpile-only "throw error"`); + const { err } = await exec( + `${CMD_TS_NODE_WITH_PROJECT_FLAG} --transpile-only "throw error"` + ); if (err === null) { throw new Error('Command was expected to fail, but it succeeded.'); } - expect(err.message).to.contain( + expect(err.message).toMatch( [ `${join(TEST_DIR, 'throw error.ts')}:100`, " bar() { throw new Error('this is a demo'); }", @@ -364,12 +266,14 @@ test.suite('ts-node', (test) => { }); test('eval should work with source maps', async () => { - const { err } = await exec(`${cmd} -pe "import './throw error'"`); + const { err } = await exec( + `${CMD_TS_NODE_WITH_PROJECT_FLAG} -pe "import './throw error'"` + ); if (err === null) { throw new Error('Command was expected to fail, but it succeeded.'); } - expect(err.message).to.contain( + expect(err.message).toMatch( [ `${join(TEST_DIR, 'throw error.ts')}:100`, " bar() { throw new Error('this is a demo'); }", @@ -379,721 +283,104 @@ test.suite('ts-node', (test) => { }); test('should support transpile only mode', async () => { - const { err } = await exec(`${cmd} --transpile-only -pe "x"`); + const { err } = await exec( + `${CMD_TS_NODE_WITH_PROJECT_FLAG} --transpile-only -pe "x"` + ); if (err === null) { throw new Error('Command was expected to fail, but it succeeded.'); } - expect(err.message).to.contain('ReferenceError: x is not defined'); + expect(err.message).toMatch('ReferenceError: x is not defined'); }); test('should throw error even in transpileOnly mode', async () => { - const { err } = await exec(`${cmd} --transpile-only -pe "console."`); + const { err } = await exec( + `${CMD_TS_NODE_WITH_PROJECT_FLAG} --transpile-only -pe "console."` + ); if (err === null) { throw new Error('Command was expected to fail, but it succeeded.'); } - expect(err.message).to.contain('error TS1003: Identifier expected'); + expect(err.message).toMatch('error TS1003: Identifier expected'); }); test('should support third-party transpilers via --transpiler', async () => { const { err, stdout } = await exec( - `${cmdNoProject} --transpiler ts-node/transpilers/swc-experimental transpile-only-swc` + `${CMD_TS_NODE_WITHOUT_PROJECT_FLAG} --transpiler ts-node/transpilers/swc-experimental transpile-only-swc` ); - expect(err).to.equal(null); - expect(stdout).to.contain('Hello World!'); + expect(err).toBe(null); + expect(stdout).toMatch('Hello World!'); }); test('should support third-party transpilers via tsconfig', async () => { const { err, stdout } = await exec( - `${cmdNoProject} transpile-only-swc-via-tsconfig` + `${CMD_TS_NODE_WITHOUT_PROJECT_FLAG} transpile-only-swc-via-tsconfig` ); - expect(err).to.equal(null); - expect(stdout).to.contain('Hello World!'); + expect(err).toBe(null); + expect(stdout).toMatch('Hello World!'); }); if (semver.gte(process.version, '12.16.0')) { test('swc transpiler supports native ESM emit', async () => { const { err, stdout } = await exec( - `${cmdEsmLoaderNoProject} ./index.ts`, + `${CMD_ESM_LOADER_WITHOUT_PROJECT} ./index.ts`, { cwd: resolve(TEST_DIR, 'transpile-only-swc-native-esm'), } ); - expect(err).to.equal(null); - expect(stdout).to.contain('Hello file://'); + expect(err).toBe(null); + expect(stdout).toMatch('Hello file://'); }); } test('should pipe into `ts-node` and evaluate', async () => { - const execPromise = exec(cmd); + const execPromise = exec(CMD_TS_NODE_WITH_PROJECT_FLAG); execPromise.child.stdin!.end("console.log('hello')"); const { err, stdout } = await execPromise; - expect(err).to.equal(null); - expect(stdout).to.equal('hello\n'); + expect(err).toBe(null); + expect(stdout).toBe('hello\n'); }); test('should pipe into `ts-node`', async () => { - const execPromise = exec(`${cmd} -p`); + const execPromise = exec(`${CMD_TS_NODE_WITH_PROJECT_FLAG} -p`); execPromise.child.stdin!.end('true'); const { err, stdout } = await execPromise; - expect(err).to.equal(null); - expect(stdout).to.equal('true\n'); + expect(err).toBe(null); + expect(stdout).toBe('true\n'); }); test('should pipe into an eval script', async () => { const execPromise = exec( - `${cmd} --transpile-only -pe "process.stdin.isTTY"` + `${CMD_TS_NODE_WITH_PROJECT_FLAG} --transpile-only -pe "process.stdin.isTTY"` ); execPromise.child.stdin!.end('true'); const { err, stdout } = await execPromise; - expect(err).to.equal(null); - expect(stdout).to.equal('undefined\n'); - }); - - test('should run REPL when --interactive passed and stdin is not a TTY', async () => { - const execPromise = exec(`${cmd} --interactive`); - execPromise.child.stdin!.end('console.log("123")\n'); - const { err, stdout } = await execPromise; - expect(err).to.equal(null); - expect(stdout).to.equal('> 123\n' + 'undefined\n' + '> '); - }); - - test('REPL has command to get type information', async () => { - const execPromise = exec(`${cmd} --interactive`); - execPromise.child.stdin!.end('\nconst a = 123\n.type a'); - const { err, stdout } = await execPromise; - expect(err).to.equal(null); - expect(stdout).to.equal( - '> undefined\n' + '> undefined\n' + '> const a: 123\n' + '> ' - ); - }); - - const execMacro = createExecMacro({ - cmd, - cwd: TEST_DIR, - }); - - type ReplApiMacroAssertions = ( - stdout: string, - stderr: string - ) => Promise; - - const replApiMacro = test.macro( - (opts: { input: string }, assertions: ReplApiMacroAssertions) => async ( - t - ) => { - const { input } = opts; - const { stdin, stdout, stderr, replService } = createReplViaApi(); - replService.start(); - stdin.write(input); - stdin.end(); - await promisify(setTimeout)(1e3); - stdout.end(); - stderr.end(); - const stderrString = await getStream(stderr); - const stdoutString = await getStream(stdout); - await assertions(stdoutString, stderrString); - } - ); - - // Serial because it's timing-sensitive - test.serial( - 'REPL can be created via API', - replApiMacro, - { - input: '\nconst a = 123\n.type a\n', - }, - async (stdout, stderr) => { - expect(stderr).to.equal(''); - expect(stdout).to.equal( - '> undefined\n' + '> undefined\n' + '> const a: 123\n' + '> ' - ); - } - ); - - // Serial because it's timing-sensitive - test.serial('REPL can be configured on `start`', async () => { - const prompt = '#> '; - - const { stdout, stderr } = await executeInRepl('const x = 3', { - startOptions: { - prompt, - ignoreUndefined: true, - }, - }); - - expect(stderr).to.equal(''); - expect(stdout).to.equal(`${prompt}${prompt}`); + expect(err).toBe(null); + expect(stdout).toBe('undefined\n'); }); - // Serial because it's timing-sensitive - test.serial( - 'REPL uses a different context when `useGlobal` is false', - async () => { - const { stdout, stderr } = await executeInRepl( - // No error when re-declaring x - 'const x = 3\n' + - // console.log ouput will end up in the stream and not in test output - 'console.log(1)\n', - { - startOptions: { - useGlobal: false, - }, - } - ); - - expect(stderr).to.equal(''); - expect(stdout).to.equal(`> undefined\n> 1\nundefined\n> `); - } - ); - - test.suite( - '[eval], , and [stdin] execute with correct globals', - (test) => { - interface GlobalInRepl extends NodeJS.Global { - testReport: any; - replReport: any; - stdinReport: any; - evalReport: any; - module: any; - exports: any; - fs: any; - __filename: any; - __dirname: any; - } - const globalInRepl = global as GlobalInRepl; - const programmaticTest = test.macro( - ( - { - evalCodeBefore, - stdinCode, - }: { - evalCodeBefore: string | null; - stdinCode: string; - }, - assertions: (stdout: string) => Promise | void - ) => async (t) => { - delete globalInRepl.testReport; - delete globalInRepl.replReport; - delete globalInRepl.stdinReport; - delete globalInRepl.evalReport; - delete globalInRepl.module; - delete globalInRepl.exports; - delete globalInRepl.fs; - delete globalInRepl.__filename; - delete globalInRepl.__dirname; - const { stdin, stderr, stdout, replService } = createReplViaApi(); - if (typeof evalCodeBefore === 'string') { - replService.evalCode(evalCodeBefore); - } - replService.start(); - stdin.write(stdinCode); - stdin.end(); - await promisify(setTimeout)(1e3); - stdout.end(); - stderr.end(); - expect(await getStream(stderr)).to.equal(''); - await assertions(await getStream(stdout)); - } - ); - - const declareGlobals = `declare var replReport: any, stdinReport: any, evalReport: any, restReport: any, global: any, __filename: any, __dirname: any, module: any, exports: any, fs: any;`; - function setReportGlobal(type: 'repl' | 'stdin' | 'eval') { - return ` - ${declareGlobals} - global.${type}Report = { - __filename: typeof __filename !== 'undefined' && __filename, - __dirname: typeof __dirname !== 'undefined' && __dirname, - moduleId: typeof module !== 'undefined' && module.id, - modulePath: typeof module !== 'undefined' && module.path, - moduleFilename: typeof module !== 'undefined' && module.filename, - modulePaths: typeof module !== 'undefined' && [...module.paths], - exportsTest: typeof exports !== 'undefined' ? module.exports === exports : null, - stackTest: new Error().stack!.split('\\n')[1], - moduleAccessorsTest: typeof fs === 'undefined' ? null : fs === require('fs'), - argv: [...process.argv] - }; - `.replace(/\n/g, ''); - } - const reportsObject = ` - { - stdinReport: typeof stdinReport !== 'undefined' && stdinReport, - evalReport: typeof evalReport !== 'undefined' && evalReport, - replReport: typeof replReport !== 'undefined' && replReport - } - `; - const printReports = ` - ${declareGlobals} - console.log(JSON.stringify(${reportsObject})); - `.replace(/\n/g, ''); - const saveReportsAsGlobal = ` - ${declareGlobals} - global.testReport = ${reportsObject}; - `.replace(/\n/g, ''); - - function parseStdoutStripReplPrompt(stdout: string) { - // Strip node's welcome header, only uncomment if running these tests manually against vanilla node - // stdout = stdout.replace(/^Welcome to.*\nType "\.help" .*\n/, ''); - expect(stdout.slice(0, 2)).to.equal('> '); - expect(stdout.slice(-12)).to.equal('undefined\n> '); - return parseStdout(stdout.slice(2, -12)); - } - function parseStdout(stdout: string) { - return JSON.parse(stdout); - } - - /** Every possible ./node_modules directory ascending upwards starting with ./tests/node_modules */ - const modulePaths = createModulePaths(TEST_DIR); - const rootModulePaths = createModulePaths(ROOT_DIR); - function createModulePaths(dir: string) { - const modulePaths: string[] = []; - for (let path = dir; ; path = dirname(path)) { - modulePaths.push(join(path, 'node_modules')); - if (dirname(path) === path) break; - } - return modulePaths; - } - - // Executable is `ts-node` on Posix, `bin.js` on Windows due to Windows shimming limitations (this is determined by package manager) - const tsNodeExe = exp.stringMatching(/\b(ts-node|bin.js)$/); - - test( - 'stdin', - execMacro, - { - stdin: `${setReportGlobal('stdin')};${printReports}`, - flags: '', - }, - (stdout) => { - const report = parseStdout(stdout); - exp(report).toMatchObject({ - stdinReport: { - __filename: '[stdin]', - __dirname: '.', - moduleId: '[stdin]', - modulePath: '.', - // Note: vanilla node does does not have file extension - moduleFilename: join(TEST_DIR, `[stdin].ts`), - modulePaths, - exportsTest: true, - // Note: vanilla node uses different name. See #1360 - stackTest: exp.stringContaining( - ` at ${join(TEST_DIR, `[stdin].ts`)}:1:` - ), - moduleAccessorsTest: null, - argv: [tsNodeExe], - }, - evalReport: false, - replReport: false, - }); - } - ); - test( - 'repl', - execMacro, - { - stdin: `${setReportGlobal('repl')};${printReports}`, - flags: '-i', - }, - (stdout) => { - const report = parseStdoutStripReplPrompt(stdout); - exp(report).toMatchObject({ - stdinReport: false, - evalReport: false, - replReport: { - __filename: false, - __dirname: false, - moduleId: '', - modulePath: '.', - moduleFilename: null, - modulePaths: exp.objectContaining({ - ...[join(TEST_DIR, `repl/node_modules`), ...modulePaths], - }), - // Note: vanilla node REPL does not set exports - exportsTest: true, - // Note: vanilla node uses different name. See #1360 - stackTest: exp.stringContaining( - ` at ${join(TEST_DIR, '.ts')}:2:` - ), - moduleAccessorsTest: true, - argv: [tsNodeExe], - }, - }); - // Prior to these, nyc adds another entry on Windows; we need to ignore it - exp(report.replReport.modulePaths.slice(-3)).toMatchObject([ - join(homedir(), `.node_modules`), - join(homedir(), `.node_libraries`), - // additional entry goes to node's install path - exp.any(String), - ]); - } - ); - - // Should ignore -i and run the entrypoint - test( - '-i w/entrypoint ignores -i', - execMacro, - { - stdin: `${setReportGlobal('repl')};${printReports}`, - flags: '-i ./repl/script.js', - }, - (stdout) => { - const report = parseStdout(stdout); - exp(report).toMatchObject({ - stdinReport: false, - evalReport: false, - replReport: false, - }); - } - ); - - // Should not execute stdin - // Should not interpret positional arg as an entrypoint script - test( - '-e', - execMacro, - { - stdin: `throw new Error()`, - flags: `-e "${setReportGlobal('eval')};${printReports}"`, - }, - (stdout) => { - const report = parseStdout(stdout); - exp(report).toMatchObject({ - stdinReport: false, - evalReport: { - __filename: '[eval]', - __dirname: '.', - moduleId: '[eval]', - modulePath: '.', - // Note: vanilla node does does not have file extension - moduleFilename: join(TEST_DIR, `[eval].ts`), - modulePaths: [...modulePaths], - exportsTest: true, - // Note: vanilla node uses different name. See #1360 - stackTest: exp.stringContaining( - ` at ${join(TEST_DIR, `[eval].ts`)}:1:` - ), - moduleAccessorsTest: true, - argv: [tsNodeExe], - }, - replReport: false, - }); - } - ); - test( - '-e w/entrypoint arg does not execute entrypoint', - execMacro, - { - stdin: `throw new Error()`, - flags: `-e "${setReportGlobal( - 'eval' - )};${printReports}" ./repl/script.js`, - }, - (stdout) => { - const report = parseStdout(stdout); - exp(report).toMatchObject({ - stdinReport: false, - evalReport: { - __filename: '[eval]', - __dirname: '.', - moduleId: '[eval]', - modulePath: '.', - // Note: vanilla node does does not have file extension - moduleFilename: join(TEST_DIR, `[eval].ts`), - modulePaths, - exportsTest: true, - // Note: vanilla node uses different name. See #1360 - stackTest: exp.stringContaining( - ` at ${join(TEST_DIR, `[eval].ts`)}:1:` - ), - moduleAccessorsTest: true, - argv: [tsNodeExe, './repl/script.js'], - }, - replReport: false, - }); - } - ); - test( - '-e w/non-path arg', - execMacro, - { - stdin: `throw new Error()`, - flags: `-e "${setReportGlobal( - 'eval' - )};${printReports}" ./does-not-exist.js`, - }, - (stdout) => { - const report = parseStdout(stdout); - exp(report).toMatchObject({ - stdinReport: false, - evalReport: { - __filename: '[eval]', - __dirname: '.', - moduleId: '[eval]', - modulePath: '.', - // Note: vanilla node does does not have file extension - moduleFilename: join(TEST_DIR, `[eval].ts`), - modulePaths, - exportsTest: true, - // Note: vanilla node uses different name. See #1360 - stackTest: exp.stringContaining( - ` at ${join(TEST_DIR, `[eval].ts`)}:1:` - ), - moduleAccessorsTest: true, - argv: [tsNodeExe, './does-not-exist.js'], - }, - replReport: false, - }); - } - ); - test( - '-e -i', - execMacro, - { - stdin: `${setReportGlobal('repl')};${printReports}`, - flags: `-e "${setReportGlobal('eval')}" -i`, - }, - (stdout) => { - const report = parseStdoutStripReplPrompt(stdout); - exp(report).toMatchObject({ - stdinReport: false, - evalReport: { - __filename: '[eval]', - __dirname: '.', - moduleId: '[eval]', - modulePath: '.', - // Note: vanilla node does does not have file extension - moduleFilename: join(TEST_DIR, `[eval].ts`), - modulePaths, - exportsTest: true, - // Note: vanilla node uses different name. See #1360 - stackTest: exp.stringContaining( - ` at ${join(TEST_DIR, `[eval].ts`)}:1:` - ), - moduleAccessorsTest: true, - argv: [tsNodeExe], - }, - replReport: { - __filename: '[eval]', - __dirname: '.', - moduleId: '', - modulePath: '.', - moduleFilename: null, - modulePaths: exp.objectContaining({ - ...[join(TEST_DIR, `repl/node_modules`), ...modulePaths], - }), - // Note: vanilla node REPL does not set exports, so this would be false - exportsTest: true, - // Note: vanilla node uses different name. See #1360 - stackTest: exp.stringContaining( - ` at ${join(TEST_DIR, '.ts')}:2:` - ), - moduleAccessorsTest: true, - argv: [tsNodeExe], - }, - }); - // Prior to these, nyc adds another entry on Windows; we need to ignore it - exp(report.replReport.modulePaths.slice(-3)).toMatchObject([ - join(homedir(), `.node_modules`), - join(homedir(), `.node_libraries`), - // additional entry goes to node's install path - exp.any(String), - ]); - } - ); - - test( - '-e -i w/entrypoint ignores -e and -i, runs entrypoint', - execMacro, - { - stdin: `throw new Error()`, - flags: '-e "throw new Error()" -i ./repl/script.js', - }, - (stdout) => { - const report = parseStdout(stdout); - exp(report).toMatchObject({ - stdinReport: false, - evalReport: false, - replReport: false, - }); - } - ); - - test( - '-e -i when -e throws error, -i does not run', - execMacro, - { - stdin: `console.log('hello')`, - flags: `-e "throw new Error('error from -e')" -i`, - expectError: true, - }, - (stdout, stderr, err) => { - exp(err).toBeDefined(); - exp(stdout).toBe(''); - exp(stderr).toContain('error from -e'); - } - ); - - // Serial because it's timing-sensitive - test.serial( - 'programmatically, eval-ing before starting REPL', - programmaticTest, - { - evalCodeBefore: `${setReportGlobal('repl')};${saveReportsAsGlobal}`, - stdinCode: '', - }, - (stdout) => { - exp(globalInRepl.testReport).toMatchObject({ - stdinReport: false, - evalReport: false, - replReport: { - __filename: false, - __dirname: false, - - // Due to limitations in node's REPL API, we can't really expose - // the `module` prior to calling repl.start() which also sends - // output to stdout. - // For now, leaving this as unsupported / undefined behavior. - - // moduleId: '', - // modulePath: '.', - // moduleFilename: null, - // modulePaths: [ - // join(ROOT_DIR, `repl/node_modules`), - // ...rootModulePaths, - // join(homedir(), `.node_modules`), - // join(homedir(), `.node_libraries`), - // // additional entry goes to node's install path - // exp.any(String), - // ], - // // Note: vanilla node REPL does not set exports - // exportsTest: true, - // moduleAccessorsTest: true, - - // Note: vanilla node uses different name. See #1360 - stackTest: exp.stringContaining( - ` at ${join(ROOT_DIR, '.ts')}:1:` - ), - }, - }); - } - ); - test.serial( - 'programmatically, passing code to stdin after starting REPL', - programmaticTest, - { - evalCodeBefore: null, - stdinCode: `${setReportGlobal('repl')};${saveReportsAsGlobal}`, - }, - (stdout) => { - exp(globalInRepl.testReport).toMatchObject({ - stdinReport: false, - evalReport: false, - replReport: { - __filename: false, - __dirname: false, - moduleId: '', - modulePath: '.', - moduleFilename: null, - modulePaths: exp.objectContaining({ - ...[join(ROOT_DIR, `repl/node_modules`), ...rootModulePaths], - }), - // Note: vanilla node REPL does not set exports - exportsTest: true, - // Note: vanilla node uses different name. See #1360 - stackTest: exp.stringContaining( - ` at ${join(ROOT_DIR, '.ts')}:1:` - ), - moduleAccessorsTest: true, - }, - }); - // Prior to these, nyc adds another entry on Windows; we need to ignore it - exp( - globalInRepl.testReport.replReport.modulePaths.slice(-3) - ).toMatchObject([ - join(homedir(), `.node_modules`), - join(homedir(), `.node_libraries`), - // additional entry goes to node's install path - exp.any(String), - ]); - } - ); - } - ); - - test.suite( - 'REPL ignores diagnostics that are annoying in interactive sessions', - (test) => { - const code = `function foo() {};\nfunction foo() {return 123};\nconsole.log(foo());\n`; - const diagnosticMessage = `Duplicate function implementation`; - test( - 'interactive repl should ignore them', - execMacro, - { - flags: '-i', - stdin: code, - }, - async (stdout, stderr) => { - exp(stdout).not.toContain(diagnosticMessage); - } - ); - test( - 'interactive repl should not ignore them if they occur in other files', - execMacro, - { - flags: '-i', - stdin: `import './repl-ignored-diagnostics/index.ts';\n`, - }, - async (stdout, stderr) => { - exp(stderr).toContain(diagnosticMessage); - } - ); - test( - '[stdin] should not ignore them', - execMacro, - { - stdin: code, - expectError: true, - }, - async (stdout, stderr) => { - exp(stderr).toContain(diagnosticMessage); - } - ); - test( - '[eval] should not ignore them', - execMacro, - { - flags: `-e "${code.replace(/\n/g, '')}"`, - expectError: true, - }, - async (stdout, stderr) => { - exp(stderr).toContain(diagnosticMessage); - } - ); - } - ); - test('should support require flags', async () => { const { err, stdout } = await exec( - `${cmd} -r ./hello-world -pe "console.log('success')"` + `${CMD_TS_NODE_WITH_PROJECT_FLAG} -r ./hello-world -pe "console.log('success')"` ); - expect(err).to.equal(null); - expect(stdout).to.equal('Hello, world!\nsuccess\nundefined\n'); + expect(err).toBe(null); + expect(stdout).toBe('Hello, world!\nsuccess\nundefined\n'); }); test('should support require from node modules', async () => { const { err, stdout } = await exec( - `${cmd} -r typescript -e "console.log('success')"` + `${CMD_TS_NODE_WITH_PROJECT_FLAG} -r typescript -e "console.log('success')"` ); - expect(err).to.equal(null); - expect(stdout).to.equal('success\n'); + expect(err).toBe(null); + expect(stdout).toBe('success\n'); }); test('should use source maps with react tsx', async () => { - const { err, stdout } = await exec(`${cmd} "throw error react tsx.tsx"`); - expect(err).not.to.equal(null); - expect(err!.message).to.contain( + const { err, stdout } = await exec( + `${CMD_TS_NODE_WITH_PROJECT_FLAG} "throw error react tsx.tsx"` + ); + expect(err).not.toBe(null); + expect(err!.message).toMatch( [ `${join(TEST_DIR, './throw error react tsx.tsx')}:100`, " bar() { throw new Error('this is a demo'); }", @@ -1105,10 +392,10 @@ test.suite('ts-node', (test) => { test('should use source maps with react tsx in --transpile-only mode', async () => { const { err, stdout } = await exec( - `${cmd} --transpile-only "throw error react tsx.tsx"` + `${CMD_TS_NODE_WITH_PROJECT_FLAG} --transpile-only "throw error react tsx.tsx"` ); - expect(err).not.to.equal(null); - expect(err!.message).to.contain( + expect(err).not.toBe(null); + expect(err!.message).toMatch( [ `${join(TEST_DIR, './throw error react tsx.tsx')}:100`, " bar() { throw new Error('this is a demo'); }", @@ -1119,20 +406,29 @@ test.suite('ts-node', (test) => { }); test('should allow custom typings', async () => { - const { err, stdout } = await exec(`${cmd} custom-types`); - expect(err).to.match(/Error: Cannot find module 'does-not-exist'/); + const { err, stdout } = await exec( + `${CMD_TS_NODE_WITH_PROJECT_FLAG} custom-types` + ); + // This error comes from *node*, meaning TypeScript respected the custom types (good) but *node* could not find the non-existent module (expected) + expect(err?.message).toMatch( + /Error: Cannot find module 'does-not-exist'/ + ); }); test('should preserve `ts-node` context with child process', async () => { - const { err, stdout } = await exec(`${cmd} child-process`); - expect(err).to.equal(null); - expect(stdout).to.equal('Hello, world!\n'); + const { err, stdout } = await exec( + `${CMD_TS_NODE_WITH_PROJECT_FLAG} child-process` + ); + expect(err).toBe(null); + expect(stdout).toBe('Hello, world!\n'); }); test('should import js before ts by default', async () => { - const { err, stdout } = await exec(`${cmd} import-order/compiled`); - expect(err).to.equal(null); - expect(stdout).to.equal('Hello, JavaScript!\n'); + const { err, stdout } = await exec( + `${CMD_TS_NODE_WITH_PROJECT_FLAG} import-order/compiled` + ); + expect(err).toBe(null); + expect(stdout).toBe('Hello, JavaScript!\n'); }); const preferTsExtsEntrypoint = semver.gte(process.version, '12.0.0') @@ -1140,24 +436,29 @@ test.suite('ts-node', (test) => { : 'import-order/require-compiled'; test('should import ts before js when --prefer-ts-exts flag is present', async () => { const { err, stdout } = await exec( - `${cmd} --prefer-ts-exts ${preferTsExtsEntrypoint}` + `${CMD_TS_NODE_WITH_PROJECT_FLAG} --prefer-ts-exts ${preferTsExtsEntrypoint}` ); - expect(err).to.equal(null); - expect(stdout).to.equal('Hello, TypeScript!\n'); + expect(err).toBe(null); + expect(stdout).toBe('Hello, TypeScript!\n'); }); test('should import ts before js when TS_NODE_PREFER_TS_EXTS env is present', async () => { - const { err, stdout } = await exec(`${cmd} ${preferTsExtsEntrypoint}`, { - env: { ...process.env, TS_NODE_PREFER_TS_EXTS: 'true' }, - }); - expect(err).to.equal(null); - expect(stdout).to.equal('Hello, TypeScript!\n'); + const { err, stdout } = await exec( + `${CMD_TS_NODE_WITH_PROJECT_FLAG} ${preferTsExtsEntrypoint}`, + { + env: { ...process.env, TS_NODE_PREFER_TS_EXTS: 'true' }, + } + ); + expect(err).toBe(null); + expect(stdout).toBe('Hello, TypeScript!\n'); }); test('should ignore .d.ts files', async () => { - const { err, stdout } = await exec(`${cmd} import-order/importer`); - expect(err).to.equal(null); - expect(stdout).to.equal('Hello, World!\n'); + const { err, stdout } = await exec( + `${CMD_TS_NODE_WITH_PROJECT_FLAG} import-order/importer` + ); + expect(err).toBe(null); + expect(stdout).toBe('Hello, World!\n'); }); test.suite('issue #884', (test) => { @@ -1173,8 +474,8 @@ test.suite('ts-node', (test) => { const { err, stdout } = await exec( `"${BIN_PATH}" --project issue-884/tsconfig.json issue-884` ); - expect(err).to.equal(null); - expect(stdout).to.equal(''); + expect(err).toBe(null); + expect(stdout).toBe(''); } }); }); @@ -1184,18 +485,18 @@ test.suite('ts-node', (test) => { const { err, stdout, stderr } = await exec( `"${BIN_PATH}" --project issue-986/tsconfig.json issue-986` ); - expect(err).not.to.equal(null); - expect(stderr).to.contain("Cannot find name 'TEST'"); // TypeScript error. - expect(stdout).to.equal(''); + expect(err).not.toBe(null); + expect(stderr).toMatch("Cannot find name 'TEST'"); // TypeScript error. + expect(stdout).toBe(''); }); test('should compile with `--files`', async () => { const { err, stdout, stderr } = await exec( `"${BIN_PATH}" --files --project issue-986/tsconfig.json issue-986` ); - expect(err).not.to.equal(null); - expect(stderr).to.contain('ReferenceError: TEST is not defined'); // Runtime error. - expect(stdout).to.equal(''); + expect(err).not.toBe(null); + expect(stderr).toMatch('ReferenceError: TEST is not defined'); // Runtime error. + expect(stdout).toBe(''); }); }); @@ -1204,15 +505,15 @@ test.suite('ts-node', (test) => { const { err, stdout } = await exec(`${BIN_PATH} ../a/index`, { cwd: join(TEST_DIR, 'cwd-and-script-mode/b'), }); - expect(err).to.equal(null); - expect(stdout).to.match(/plugin-a/); + expect(err).toBe(null); + expect(stdout).toMatch(/plugin-a/); }); test('should locate tsconfig relative to entry-point via ts-node-script', async () => { const { err, stdout } = await exec(`${BIN_SCRIPT_PATH} ../a/index`, { cwd: join(TEST_DIR, 'cwd-and-script-mode/b'), }); - expect(err).to.equal(null); - expect(stdout).to.match(/plugin-a/); + expect(err).toBe(null); + expect(stdout).toMatch(/plugin-a/); }); test('should locate tsconfig relative to entry-point with --script-mode', async () => { const { err, stdout } = await exec( @@ -1221,23 +522,23 @@ test.suite('ts-node', (test) => { cwd: join(TEST_DIR, 'cwd-and-script-mode/b'), } ); - expect(err).to.equal(null); - expect(stdout).to.match(/plugin-a/); + expect(err).toBe(null); + expect(stdout).toMatch(/plugin-a/); }); test('should locate tsconfig relative to cwd via ts-node-cwd', async () => { const { err, stdout } = await exec(`${BIN_CWD_PATH} ../a/index`, { cwd: join(TEST_DIR, 'cwd-and-script-mode/b'), }); - expect(err).to.equal(null); - expect(stdout).to.match(/plugin-b/); + expect(err).toBe(null); + expect(stdout).toMatch(/plugin-b/); }); test('should locate tsconfig relative to cwd in --cwd-mode', async () => { const { err, stdout } = await exec( `${BIN_PATH} --cwd-mode ../a/index`, { cwd: join(TEST_DIR, 'cwd-and-script-mode/b') } ); - expect(err).to.equal(null); - expect(stdout).to.match(/plugin-b/); + expect(err).toBe(null); + expect(stdout).toMatch(/plugin-b/); }); test('should locate tsconfig relative to realpath, not symlink, when entrypoint is a symlink', async (t) => { if ( @@ -1248,8 +549,8 @@ test.suite('ts-node', (test) => { const { err, stdout } = await exec( `${BIN_PATH} main-realpath/symlink/symlink.tsx` ); - expect(err).to.equal(null); - expect(stdout).to.equal(''); + expect(err).toBe(null); + expect(stdout).toBe(''); } else { t.log('Skipping'); return; @@ -1270,9 +571,9 @@ test.suite('ts-node', (test) => { }, } ); - expect(err).to.equal(null); + expect(err).toBe(null); const { config } = JSON.parse(stdout); - expect(config.options.typeRoots).to.deep.equal([ + expect(config.options.typeRoots).toEqual([ join(TEST_DIR, './tsconfig-options/env-typeroots').replace( /\\/g, '/' @@ -1284,19 +585,19 @@ test.suite('ts-node', (test) => { const { err, stdout } = await exec( `${BIN_EXEC} tsconfig-options/log-options1.js` ); - expect(err).to.equal(null); + expect(err).toBe(null); const { options, config } = JSON.parse(stdout); - expect(config.options.typeRoots).to.deep.equal([ + expect(config.options.typeRoots).toEqual([ join(TEST_DIR, './tsconfig-options/tsconfig-typeroots').replace( /\\/g, '/' ), ]); - expect(config.options.types).to.deep.equal(['tsconfig-tsnode-types']); - expect(options.pretty).to.equal(undefined); - expect(options.skipIgnore).to.equal(false); - expect(options.transpileOnly).to.equal(true); - expect(options.require).to.deep.equal([ + expect(config.options.types).toEqual(['tsconfig-tsnode-types']); + expect(options.pretty).toBe(undefined); + expect(options.skipIgnore).toBe(false); + expect(options.transpileOnly).toBe(true); + expect(options.require).toEqual([ join(TEST_DIR, './tsconfig-options/required1.js'), ]); }); @@ -1305,19 +606,19 @@ test.suite('ts-node', (test) => { const { err, stdout } = await exec( `${BIN_EXEC} --skip-ignore --compiler-options "{\\"types\\":[\\"flags-types\\"]}" --require ./tsconfig-options/required2.js tsconfig-options/log-options2.js` ); - expect(err).to.equal(null); + expect(err).toBe(null); const { options, config } = JSON.parse(stdout); - expect(config.options.typeRoots).to.deep.equal([ + expect(config.options.typeRoots).toEqual([ join(TEST_DIR, './tsconfig-options/tsconfig-typeroots').replace( /\\/g, '/' ), ]); - expect(config.options.types).to.deep.equal(['flags-types']); - expect(options.pretty).to.equal(undefined); - expect(options.skipIgnore).to.equal(true); - expect(options.transpileOnly).to.equal(true); - expect(options.require).to.deep.equal([ + expect(config.options.types).toEqual(['flags-types']); + expect(options.pretty).toBe(undefined); + expect(options.skipIgnore).toBe(true); + expect(options.transpileOnly).toBe(true); + expect(options.require).toEqual([ join(TEST_DIR, './tsconfig-options/required1.js'), './tsconfig-options/required2.js', ]); @@ -1334,19 +635,19 @@ test.suite('ts-node', (test) => { }, } ); - expect(err).to.equal(null); + expect(err).toBe(null); const { options, config } = JSON.parse(stdout); - expect(config.options.typeRoots).to.deep.equal([ + expect(config.options.typeRoots).toEqual([ join(TEST_DIR, './tsconfig-options/tsconfig-typeroots').replace( /\\/g, '/' ), ]); - expect(config.options.types).to.deep.equal(['tsconfig-tsnode-types']); - expect(options.pretty).to.equal(true); - expect(options.skipIgnore).to.equal(false); - expect(options.transpileOnly).to.equal(true); - expect(options.require).to.deep.equal([ + expect(config.options.types).toEqual(['tsconfig-tsnode-types']); + expect(options.pretty).toBe(true); + expect(options.skipIgnore).toBe(false); + expect(options.transpileOnly).toBe(true); + expect(options.require).toEqual([ join(TEST_DIR, './tsconfig-options/required1.js'), ]); }); @@ -1356,15 +657,15 @@ test.suite('ts-node', (test) => { const { err, stdout } = await exec( `${BIN_PATH} --show-config --project ./tsconfig-extends/tsconfig.json` ); - expect(err).to.equal(null); + expect(err).toBe(null); const config = JSON.parse(stdout); - expect(config['ts-node'].require).to.deep.equal([ + expect(config['ts-node'].require).toEqual([ resolve(TEST_DIR, 'tsconfig-extends/other/require-hook.js'), ]); - expect(config['ts-node'].scopeDir).to.equal( + expect(config['ts-node'].scopeDir).toBe( resolve(TEST_DIR, 'tsconfig-extends/other/scopedir') ); - expect(config['ts-node'].preferTsExts).to.equal(true); + expect(config['ts-node'].preferTsExts).toBe(true); }); } }); @@ -1392,7 +693,7 @@ test.suite('ts-node', (test) => { stdout: stdout1, stderr: stderr1, } = await exec(`${BIN_PATH} --showConfig`, { cwd: tempDir }); - expect(err1).to.equal(null); + expect(err1).toBe(null); t.like(JSON.parse(stdout1), { compilerOptions: { target: libAndTarget, @@ -1404,8 +705,8 @@ test.suite('ts-node', (test) => { stdout: stdout2, stderr: stderr2, } = await exec(`${BIN_PATH} -pe 10n`, { cwd: tempDir }); - expect(err2).to.equal(null); - expect(stdout2).to.equal('10n\n'); + expect(err2).toBe(null); + expect(stdout2).toBe('10n\n'); }); } else { test('implicitly uses @tsconfig/* lower than node14 (node12) when either TS or node versions do not support @tsconfig/node14', async ({ @@ -1414,8 +715,8 @@ test.suite('ts-node', (test) => { const { err, stdout, stderr } = await exec(`${BIN_PATH} -pe 10n`, { cwd: tempDir, }); - expect(err).to.not.equal(null); - expect(stderr).to.match( + expect(err).not.toBe(null); + expect(stderr).toMatch( /BigInt literals are not available when targeting lower than|error TS2304: Cannot find name 'n'/ ); }); @@ -1430,8 +731,8 @@ test.suite('ts-node', (test) => { env: { ...process.env, foo: 'hello world' }, } ); - expect(err).to.equal(null); - expect(stdout).to.equal('hello world\n'); + expect(err).toBe(null); + expect(stdout).toBe('hello world\n'); }); test('implicitly loads local @types/node', async ({ context: { tempDir }, @@ -1447,8 +748,8 @@ test.suite('ts-node', (test) => { env: { ...process.env, foo: 'hello world' }, } ); - expect(err).to.not.equal(null); - expect(stderr).to.contain( + expect(err).not.toBe(null); + expect(stderr).toMatch( "Property 'env' does not exist on type 'LocalNodeTypes_Process'" ); }); @@ -1467,7 +768,7 @@ test.suite('ts-node', (test) => { cwd: join(TEST_DIR, 'tsconfig-bases', nodeVersion), } ); - expect(err).to.equal(null); + expect(err).toBe(null); t.like(JSON.parse(stdout), { compilerOptions: { target: config.compilerOptions.target, @@ -1486,22 +787,22 @@ test.suite('ts-node', (test) => { test.suite('compiler host', (test) => { test('should execute cli', async () => { const { err, stdout } = await exec( - `${cmd} --compiler-host hello-world` + `${CMD_TS_NODE_WITH_PROJECT_FLAG} --compiler-host hello-world` ); - expect(err).to.equal(null); - expect(stdout).to.equal('Hello, world!\n'); + expect(err).toBe(null); + expect(stdout).toBe('Hello, world!\n'); }); }); test('should transpile files inside a node_modules directory when not ignored', async () => { const { err, stdout, stderr } = await exec( - `${cmdNoProject} from-node-modules/from-node-modules` + `${CMD_TS_NODE_WITHOUT_PROJECT_FLAG} from-node-modules/from-node-modules` ); if (err) throw new Error( `Unexpected error: ${err}\nstdout:\n${stdout}\nstderr:\n${stderr}` ); - expect(JSON.parse(stdout)).to.deep.equal({ + expect(JSON.parse(stdout)).toEqual({ external: { tsmri: { name: 'typescript-module-required-internally' }, jsmri: { name: 'javascript-module-required-internally' }, @@ -1518,10 +819,10 @@ test.suite('ts-node', (test) => { test.suite('should respect maxNodeModulesJsDepth', (test) => { test('for unscoped modules', async () => { const { err, stdout, stderr } = await exec( - `${cmdNoProject} maxnodemodulesjsdepth` + `${CMD_TS_NODE_WITHOUT_PROJECT_FLAG} maxnodemodulesjsdepth` ); - expect(err).to.not.equal(null); - expect(stderr.replace(/\r\n/g, '\n')).to.contain( + expect(err).not.toBe(null); + expect(stderr.replace(/\r\n/g, '\n')).toMatch( 'TSError: тип Unable to compile TypeScript:\n' + "maxnodemodulesjsdepth/other.ts(4,7): error TS2322: Type 'string' is not assignable to type 'boolean'.\n" + '\n' @@ -1530,10 +831,10 @@ test.suite('ts-node', (test) => { test('for @scoped modules', async () => { const { err, stdout, stderr } = await exec( - `${cmdNoProject} maxnodemodulesjsdepth-scoped` + `${CMD_TS_NODE_WITHOUT_PROJECT_FLAG} maxnodemodulesjsdepth-scoped` ); - expect(err).to.not.equal(null); - expect(stderr.replace(/\r\n/g, '\n')).to.contain( + expect(err).not.toBe(null); + expect(stderr.replace(/\r\n/g, '\n')).toMatch( 'TSError: тип Unable to compile TypeScript:\n' + "maxnodemodulesjsdepth-scoped/other.ts(7,7): error TS2322: Type 'string' is not assignable to type 'boolean'.\n" + '\n' @@ -1549,8 +850,10 @@ test.suite('ts-node', (test) => { function posix(path: string) { return path.replace(/\/|\\/g, '/'); } - const { err, stdout } = await exec(`${cmd} --showConfig`); - expect(err).to.equal(null); + const { err, stdout } = await exec( + `${CMD_TS_NODE_WITH_PROJECT_FLAG} --showConfig` + ); + expect(err).toBe(null); t.is( stdout, JSON.stringify( @@ -1585,190 +888,27 @@ test.suite('ts-node', (test) => { }); } else { test('--show-config should log error message when used with old typescript versions', async (t) => { - const { err, stderr } = await exec(`${cmd} --showConfig`); - expect(err).to.not.equal(null); - expect(stderr).to.contain('Error: --show-config requires'); + const { err, stderr } = await exec( + `${CMD_TS_NODE_WITH_PROJECT_FLAG} --showConfig` + ); + expect(err).not.toBe(null); + expect(stderr).toMatch('Error: --show-config requires'); }); } test('should support compiler scope specified via tsconfig.json', async (t) => { const { err, stderr, stdout } = await exec( - `${cmdNoProject} --project ./scope/c/config/tsconfig.json ./scope/c/index.js` + `${CMD_TS_NODE_WITHOUT_PROJECT_FLAG} --project ./scope/c/config/tsconfig.json ./scope/c/index.js` ); - expect(err).to.equal(null); - expect(stdout).to.equal(`value\nFailures: 0\n`); - }); - }); - - test.suite('register', (_test) => { - const test = _test.context( - once(async () => { - return { - registered: register({ - project: PROJECT, - compilerOptions: { - jsx: 'preserve', - }, - }), - moduleTestPath: require.resolve('../../tests/module'), - }; - }) - ); - test.beforeEach(async ({ context: { registered } }) => { - // Re-enable project for every test. - registered.enabled(true); - }); - test.runSerially(); - - test('should be able to require typescript', ({ - context: { moduleTestPath }, - }) => { - const m = require(moduleTestPath); - - expect(m.example('foo')).to.equal('FOO'); - }); - - test('should support dynamically disabling', ({ - context: { registered, moduleTestPath }, - }) => { - delete require.cache[moduleTestPath]; - - expect(registered.enabled(false)).to.equal(false); - expect(() => require(moduleTestPath)).to.throw(/Unexpected token/); - - delete require.cache[moduleTestPath]; - - expect(registered.enabled()).to.equal(false); - expect(() => require(moduleTestPath)).to.throw(/Unexpected token/); - - delete require.cache[moduleTestPath]; - - expect(registered.enabled(true)).to.equal(true); - expect(() => require(moduleTestPath)).to.not.throw(); - - delete require.cache[moduleTestPath]; - - expect(registered.enabled()).to.equal(true); - expect(() => require(moduleTestPath)).to.not.throw(); - }); - - test('should support compiler scopes', ({ - context: { registered, moduleTestPath }, - }) => { - const calls: string[] = []; - - registered.enabled(false); - - const compilers = [ - register({ - projectSearchDir: join(TEST_DIR, 'scope/a'), - scopeDir: join(TEST_DIR, 'scope/a'), - scope: true, - }), - register({ - projectSearchDir: join(TEST_DIR, 'scope/a'), - scopeDir: join(TEST_DIR, 'scope/b'), - scope: true, - }), - ]; - - compilers.forEach((c) => { - const old = c.compile; - c.compile = (code, fileName, lineOffset) => { - calls.push(fileName); - - return old(code, fileName, lineOffset); - }; - }); - - try { - expect(require('../../tests/scope/a').ext).to.equal('.ts'); - expect(require('../../tests/scope/b').ext).to.equal('.ts'); - } finally { - compilers.forEach((c) => c.enabled(false)); - } - - expect(calls).to.deep.equal([ - join(TEST_DIR, 'scope/a/index.ts'), - join(TEST_DIR, 'scope/b/index.ts'), - ]); - - delete require.cache[moduleTestPath]; - - expect(() => require(moduleTestPath)).to.throw(); - }); - - test('should compile through js and ts', () => { - const m = require('../../tests/complex'); - - expect(m.example()).to.equal('example'); - }); - - test('should work with proxyquire', () => { - const m = proxyquire('../../tests/complex', { - './example': 'hello', - }); - - expect(m.example()).to.equal('hello'); - }); - - test('should work with `require.cache`', () => { - const { example1, example2 } = require('../../tests/require-cache'); - - expect(example1).to.not.equal(example2); - }); - - test('should use source maps', async () => { - try { - require('../../tests/throw error'); - } catch (error) { - expect(error.stack).to.contain( - [ - 'Error: this is a demo', - ` at Foo.bar (${join(TEST_DIR, './throw error.ts')}:100:17)`, - ].join('\n') - ); - } - }); - - test.suite('JSX preserve', (test) => { - let old: (m: Module, filename: string) => any; - let compiled: string; - - test.runSerially(); - test.beforeAll(async () => { - old = require.extensions['.tsx']!; - require.extensions['.tsx'] = (m: any, fileName) => { - const _compile = m._compile; - - m._compile = function (code: string, fileName: string) { - compiled = code; - return _compile.call(this, code, fileName); - }; - - return old(m, fileName); - }; - }); - - test('should use source maps', async (t) => { - t.teardown(() => { - require.extensions['.tsx'] = old; - }); - try { - require('../../tests/with-jsx.tsx'); - } catch (error) { - expect(error.stack).to.contain('SyntaxError: Unexpected token'); - } - - expect(compiled).to.match(SOURCE_MAP_REGEXP); - }); + expect(err).toBe(null); + expect(stdout).toBe(`value\nFailures: 0\n`); }); }); test.suite('create', (_test) => { const test = _test.context(async (t) => { return { - service: create({ + service: t.context.tsNodeUnderTest.create({ compilerOptions: { target: 'es5' }, skipProject: true, }), @@ -1779,14 +919,14 @@ test.suite('ts-node', (test) => { context: { service }, }) => { const output = service.compile('const x = 10', 'test.ts'); - expect(output).to.contain('var x = 10;'); + expect(output).toMatch('var x = 10;'); }); test.suite('should get type information', (test) => { test('given position of identifier', ({ context: { service } }) => { expect( service.getTypeInfo('/**jsdoc here*/const x = 10', 'test.ts', 21) - ).to.deep.equal({ + ).toEqual({ comment: 'jsdoc here', name: 'const x: 10', }); @@ -1796,7 +936,7 @@ test.suite('ts-node', (test) => { }) => { expect( service.getTypeInfo('/**jsdoc here*/const x = 10', 'test.ts', 0) - ).to.deep.equal({ + ).toEqual({ comment: '', name: '', }); @@ -1811,29 +951,28 @@ test.suite('ts-node', (test) => { disallowed: string[] ) { for (const ext of allowed) { - expect(ignored(join(DIST_DIR, `index${ext}`))).equal( - false, - `should accept ${ext} files` - ); + // should accept ${ext} files + expect(ignored(join(DIST_DIR, `index${ext}`))).toBe(false); } for (const ext of disallowed) { - expect(ignored(join(DIST_DIR, `index${ext}`))).equal( - true, - `should ignore ${ext} files` - ); + // should ignore ${ext} files + expect(ignored(join(DIST_DIR, `index${ext}`))).toBe(true); } } - test('correctly filters file extensions from the compiler when allowJs=false and jsx=false', () => { - const { ignored } = create({ compilerOptions: {}, skipProject: true }); + test('correctly filters file extensions from the compiler when allowJs=false and jsx=false', (t) => { + const { ignored } = t.context.tsNodeUnderTest.create({ + compilerOptions: {}, + skipProject: true, + }); testIgnored( ignored, ['.ts', '.d.ts'], ['.js', '.tsx', '.jsx', '.mjs', '.cjs', '.xyz', ''] ); }); - test('correctly filters file extensions from the compiler when allowJs=true and jsx=false', () => { - const { ignored } = create({ + test('correctly filters file extensions from the compiler when allowJs=true and jsx=false', (t) => { + const { ignored } = t.context.tsNodeUnderTest.create({ compilerOptions: { allowJs: true }, skipProject: true, }); @@ -1843,8 +982,8 @@ test.suite('ts-node', (test) => { ['.tsx', '.jsx', '.mjs', '.cjs', '.xyz', ''] ); }); - test('correctly filters file extensions from the compiler when allowJs=false and jsx=true', () => { - const { ignored } = create({ + test('correctly filters file extensions from the compiler when allowJs=false and jsx=true', (t) => { + const { ignored } = t.context.tsNodeUnderTest.create({ compilerOptions: { allowJs: false, jsx: 'preserve' }, skipProject: true, }); @@ -1854,8 +993,8 @@ test.suite('ts-node', (test) => { ['.js', '.jsx', '.mjs', '.cjs', '.xyz', ''] ); }); - test('correctly filters file extensions from the compiler when allowJs=true and jsx=true', () => { - const { ignored } = create({ + test('correctly filters file extensions from the compiler when allowJs=true and jsx=true', (t) => { + const { ignored } = t.context.tsNodeUnderTest.create({ compilerOptions: { allowJs: true, jsx: 'preserve' }, skipProject: true, }); @@ -1871,23 +1010,23 @@ test.suite('ts-node', (test) => { if (semver.gte(process.version, '12.16.0')) { test('should compile and execute as ESM', async () => { const { err, stdout } = await exec( - `${cmdEsmLoaderNoProject} index.ts`, + `${CMD_ESM_LOADER_WITHOUT_PROJECT} index.ts`, { cwd: join(TEST_DIR, './esm'), } ); - expect(err).to.equal(null); - expect(stdout).to.equal('foo bar baz biff libfoo\n'); + expect(err).toBe(null); + expect(stdout).toBe('foo bar baz biff libfoo\n'); }); test('should use source maps', async () => { const { err, stdout } = await exec( - `${cmdEsmLoaderNoProject} "throw error.ts"`, + `${CMD_ESM_LOADER_WITHOUT_PROJECT} "throw error.ts"`, { cwd: join(TEST_DIR, './esm'), } ); - expect(err).not.to.equal(null); - expect(err!.message).to.contain( + expect(err).not.toBe(null); + expect(err!.message).toMatch( [ `${pathToFileURL(join(TEST_DIR, './esm/throw error.ts')) .toString() @@ -1905,90 +1044,90 @@ test.suite('ts-node', (test) => { err, stdout, } = await exec( - `${cmdEsmLoaderNoProject} --experimental-specifier-resolution=node index.ts`, + `${CMD_ESM_LOADER_WITHOUT_PROJECT} --experimental-specifier-resolution=node index.ts`, { cwd: join(TEST_DIR, './esm-node-resolver') } ); - expect(err).to.equal(null); - expect(stdout).to.equal('foo bar baz biff libfoo\n'); + expect(err).toBe(null); + expect(stdout).toBe('foo bar baz biff libfoo\n'); }); test('via --es-module-specifier-resolution alias', async () => { const { err, stdout, } = await exec( - `${cmdEsmLoaderNoProject} --experimental-modules --es-module-specifier-resolution=node index.ts`, + `${CMD_ESM_LOADER_WITHOUT_PROJECT} ${EXPERIMENTAL_MODULES_FLAG} --es-module-specifier-resolution=node index.ts`, { cwd: join(TEST_DIR, './esm-node-resolver') } ); - expect(err).to.equal(null); - expect(stdout).to.equal('foo bar baz biff libfoo\n'); + expect(err).toBe(null); + expect(stdout).toBe('foo bar baz biff libfoo\n'); }); test('via NODE_OPTIONS', async () => { const { err, stdout } = await exec( - `${cmdEsmLoaderNoProject} index.ts`, + `${CMD_ESM_LOADER_WITHOUT_PROJECT} index.ts`, { cwd: join(TEST_DIR, './esm-node-resolver'), env: { ...process.env, - NODE_OPTIONS: `${experimentalModulesFlag} --experimental-specifier-resolution=node`, + NODE_OPTIONS: `${EXPERIMENTAL_MODULES_FLAG} --experimental-specifier-resolution=node`, }, } ); - expect(err).to.equal(null); - expect(stdout).to.equal('foo bar baz biff libfoo\n'); + expect(err).toBe(null); + expect(stdout).toBe('foo bar baz biff libfoo\n'); }); }); test('throws ERR_REQUIRE_ESM when attempting to require() an ESM script when ESM loader is enabled', async () => { const { err, stderr } = await exec( - `${cmdEsmLoaderNoProject} ./index.js`, + `${CMD_ESM_LOADER_WITHOUT_PROJECT} ./index.js`, { cwd: join(TEST_DIR, './esm-err-require-esm'), } ); - expect(err).to.not.equal(null); - expect(stderr).to.contain( + expect(err).not.toBe(null); + expect(stderr).toMatch( 'Error [ERR_REQUIRE_ESM]: Must use import to load ES Module:' ); }); test('defers to fallback loaders when URL should not be handled by ts-node', async () => { const { err, stdout, stderr } = await exec( - `${cmdEsmLoaderNoProject} index.mjs`, + `${CMD_ESM_LOADER_WITHOUT_PROJECT} index.mjs`, { cwd: join(TEST_DIR, './esm-import-http-url'), } ); - expect(err).to.not.equal(null); + expect(err).not.toBe(null); // expect error from node's default resolver - expect(stderr).to.match( + expect(stderr).toMatch( /Error \[ERR_UNSUPPORTED_ESM_URL_SCHEME\]:.*(?:\n.*){0,1}\n *at defaultResolve/ ); }); test('should bypass import cache when changing search params', async () => { const { err, stdout } = await exec( - `${cmdEsmLoaderNoProject} index.ts`, + `${CMD_ESM_LOADER_WITHOUT_PROJECT} index.ts`, { cwd: join(TEST_DIR, './esm-import-cache'), } ); - expect(err).to.equal(null); - expect(stdout).to.equal('log1\nlog2\nlog2\n'); + expect(err).toBe(null); + expect(stdout).toBe('log1\nlog2\nlog2\n'); }); test('should support transpile only mode via dedicated loader entrypoint', async () => { const { err, stdout } = await exec( - `${cmdEsmLoaderNoProject}/transpile-only index.ts`, + `${CMD_ESM_LOADER_WITHOUT_PROJECT}/transpile-only index.ts`, { cwd: join(TEST_DIR, './esm-transpile-only'), } ); - expect(err).to.equal(null); - expect(stdout).to.equal(''); + expect(err).toBe(null); + expect(stdout).toBe(''); }); test('should throw type errors without transpile-only enabled', async () => { const { err, stdout } = await exec( - `${cmdEsmLoaderNoProject} index.ts`, + `${CMD_ESM_LOADER_WITHOUT_PROJECT} index.ts`, { cwd: join(TEST_DIR, './esm-transpile-only'), } @@ -1997,23 +1136,23 @@ test.suite('ts-node', (test) => { throw new Error('Command was expected to fail, but it succeeded.'); } - expect(err.message).to.contain('Unable to compile TypeScript'); - expect(err.message).to.match( + expect(err.message).toMatch('Unable to compile TypeScript'); + expect(err.message).toMatch( new RegExp( "TS2345: Argument of type '(?:number|1101)' is not assignable to parameter of type 'string'\\." ) ); - expect(err.message).to.match( + expect(err.message).toMatch( new RegExp( "TS2322: Type '(?:\"hello world\"|string)' is not assignable to type 'number'\\." ) ); - expect(stdout).to.equal(''); + expect(stdout).toBe(''); }); async function runModuleTypeTest(project: string, ext: string) { const { err, stderr, stdout } = await exec( - `${cmdEsmLoaderNoProject} ./module-types/${project}/test.${ext}`, + `${CMD_ESM_LOADER_WITHOUT_PROJECT} ./module-types/${project}/test.${ext}`, { env: { ...process.env, @@ -2021,18 +1160,18 @@ test.suite('ts-node', (test) => { }, } ); - expect(err).to.equal(null); - expect(stdout).to.equal(`Failures: 0\n`); + expect(err).toBe(null); + expect(stdout).toBe(`Failures: 0\n`); } test('moduleTypes should allow importing CJS in an otherwise ESM project', async (t) => { // A notable case where you can use ts-node's CommonJS loader, not the ESM loader, in an ESM project: // when loading a webpack.config.ts or similar config const { err, stderr, stdout } = await exec( - `${cmdNoProject} --project ./module-types/override-to-cjs/tsconfig.json ./module-types/override-to-cjs/test-webpack-config.cjs` + `${CMD_TS_NODE_WITHOUT_PROJECT_FLAG} --project ./module-types/override-to-cjs/tsconfig.json ./module-types/override-to-cjs/test-webpack-config.cjs` ); - expect(err).to.equal(null); - expect(stdout).to.equal(``); + expect(err).toBe(null); + expect(stdout).toBe(``); await runModuleTypeTest('override-to-cjs', 'cjs'); if (semver.gte(process.version, '14.13.1')) @@ -2053,8 +1192,8 @@ test.suite('ts-node', (test) => { const { err, stderr } = await exec(`${BIN_PATH} ./index.js`, { cwd: join(TEST_DIR, './esm-err-require-esm'), }); - expect(err).to.not.equal(null); - expect(stderr).to.contain( + expect(err).not.toBe(null); + expect(stderr).toMatch( 'Error [ERR_REQUIRE_ESM]: Must use import to load ES Module:' ); }); @@ -2064,143 +1203,8 @@ test.suite('ts-node', (test) => { const { err, stdout } = await exec(`${BIN_PATH} ./index.js`, { cwd: join(TEST_DIR, './esm-err-require-esm'), }); - expect(err).to.equal(null); - expect(stdout).to.contain('CommonJS'); - }); - } - }); - - test.suite('top level await', (test) => { - const compilerOptions = { - target: 'es2018', - }; - function executeInTlaRepl(input: string, waitMs = 1000) { - return executeInRepl( - input - .split('\n') - .map((line) => line.trim()) - // Restore newline once https://github.com/nodejs/node/pull/39392 is merged - .join(''), - { - waitMs, - createServiceOpts: { - experimentalReplAwait: true, - compilerOptions, - }, - startOptions: { useGlobal: false }, - } - ); - } - - if (semver.gte(ts.version, '3.8.0')) { - // Serial because it's timing-sensitive - test.serial('should allow evaluating top level await', async () => { - const script = ` - const x: number = await new Promise((r) => r(1)); - for await (const x of [1,2,3]) { console.log(x) }; - for (const x of ['a', 'b']) { await x; console.log(x) }; - class Foo {}; await 1; - function Bar() {}; await 2; - const {y} = await ({y: 2}); - const [z] = await [3]; - x + y + z; - `; - - const { stdout, stderr } = await executeInTlaRepl(script); - expect(stderr).to.equal(''); - expect(stdout).to.equal('> 1\n2\n3\na\nb\n6\n> '); - }); - - // Serial because it's timing-sensitive - test.serial( - 'should wait until promise is settled when awaiting at top level', - async () => { - const awaitMs = 500; - const script = ` - const startTime = new Date().getTime(); - await new Promise((r) => setTimeout(() => r(1), ${awaitMs})); - const endTime = new Date().getTime(); - endTime - startTime; - `; - const { stdout, stderr } = await executeInTlaRepl(script, 6000); - - expect(stderr).to.equal(''); - - const elapsedTime = Number( - stdout.split('\n')[0].replace('> ', '').trim() - ); - expect(elapsedTime).to.be.gte(awaitMs - 50); - expect(elapsedTime).to.be.lte(awaitMs + 100); - } - ); - - // Serial because it's timing-sensitive - test.serial( - 'should not wait until promise is settled when not using await at top level', - async () => { - const script = ` - const startTime = new Date().getTime(); - (async () => await new Promise((r) => setTimeout(() => r(1), ${1000})))(); - const endTime = new Date().getTime(); - endTime - startTime; - `; - const { stdout, stderr } = await executeInTlaRepl(script); - - expect(stderr).to.equal(''); - - const ellapsedTime = Number( - stdout.split('\n')[0].replace('> ', '').trim() - ); - expect(ellapsedTime).to.be.gte(0); - expect(ellapsedTime).to.be.lte(10); - } - ); - - // Serial because it's timing-sensitive - test.serial( - 'should error with typing information when awaited result has type mismatch', - async () => { - const { stdout, stderr } = await executeInTlaRepl( - 'const x: string = await 1' - ); - - expect(stdout).to.equal('> > '); - expect(stderr.replace(/\r\n/g, '\n')).to.equal( - '.ts(2,7): error TS2322: ' + - (semver.gte(ts.version, '4.0.0') - ? `Type 'number' is not assignable to type 'string'.\n` - : `Type '1' is not assignable to type 'string'.\n`) + - '\n' - ); - } - ); - - // Serial because it's timing-sensitive - test.serial( - 'should error with typing information when importing a file with type errors', - async () => { - const { stdout, stderr } = await executeInTlaRepl( - `const {foo} = await import('./tests/repl/tla-import');` - ); - - expect(stdout).to.equal('> > '); - expect(stderr.replace(/\r\n/g, '\n')).to.equal( - 'tests/repl/tla-import.ts(1,14): error TS2322: ' + - (semver.gte(ts.version, '4.0.0') - ? `Type 'number' is not assignable to type 'string'.\n` - : `Type '1' is not assignable to type 'string'.\n`) + - '\n' - ); - } - ); - - test('should pass upstream test cases', async () => - upstreamTopLevelAwaitTests({ TEST_DIR, create, createRepl })); - } else { - test('should throw error when attempting to use top level await on TS < 3.8', async () => { - exp(executeInTlaRepl('', 1000)).rejects.toThrow( - 'Experimental REPL await is not compatible with TypeScript versions older than 3.8' - ); + expect(err).toBe(null); + expect(stdout).toMatch('CommonJS'); }); } }); diff --git a/src/test/macros.ts b/src/test/macros.ts deleted file mode 100644 index 4e076ae30..000000000 --- a/src/test/macros.ts +++ /dev/null @@ -1,108 +0,0 @@ -import type { ChildProcess, ExecException, ExecOptions } from 'child_process'; -import { exec as childProcessExec } from 'child_process'; -import type { TestInterface } from './testlib'; -import { expect } from 'chai'; -import * as exp from 'expect'; - -export type ExecReturn = Promise & { child: ChildProcess }; -export interface ExecResult { - stdout: string; - stderr: string; - err: null | ExecException; - child: ChildProcess; -} - -export interface ExecMacroOptions { - titlePrefix?: string; - cmd: string; - flags?: string; - cwd?: string; - env?: Record; - stdin?: string; - expectError?: boolean; -} -export type ExecMacroAssertionCallback = ( - stdout: string, - stderr: string, - err: ExecException | null -) => Promise | void; - -export interface createMacrosAndHelpersOptions { - test: TestInterface; - defaultCwd: string; -} -export function createMacrosAndHelpers(opts: createMacrosAndHelpersOptions) { - const { test, defaultCwd } = opts; - - /** - * Helper to exec a child process. - * Returns a Promise and a reference to the child process to suite multiple situations. - * Promise resolves with the process's stdout, stderr, and error. - */ - function exec(cmd: string, opts: ExecOptions = {}): ExecReturn { - let child!: ChildProcess; - return Object.assign( - new Promise((resolve, reject) => { - child = childProcessExec( - cmd, - { - cwd: defaultCwd, - ...opts, - }, - (err, stdout, stderr) => { - resolve({ err, stdout, stderr, child }); - } - ); - }), - { - child, - } - ); - } - - /** - * Create a macro that launches a CLI command, optionally pipes stdin, optionally sets env vars, - * and allows assertions against the output. - */ - function createExecMacro>( - preBoundOptions: T - ) { - return test.macro( - ( - options: Pick< - ExecMacroOptions, - Exclude - > & - Partial>, - assertions: ExecMacroAssertionCallback - ) => [ - (title) => `${options.titlePrefix ?? ''}${title}`, - async (t) => { - const { cmd, flags = '', stdin, expectError = false, cwd, env } = { - ...preBoundOptions, - ...options, - }; - const execPromise = exec(`${cmd} ${flags}`, { - cwd, - env: { ...process.env, ...env }, - }); - if (stdin !== undefined) { - execPromise.child.stdin!.end(stdin); - } - const { err, stdout, stderr } = await execPromise; - if (expectError) { - exp(err).toBeDefined(); - } else { - exp(err).toBeNull(); - } - await assertions(stdout, stderr, err); - }, - ] - ); - } - - return { - exec, - createExecMacro, - }; -} diff --git a/src/test/register.spec.ts b/src/test/register.spec.ts new file mode 100644 index 000000000..5708b9c37 --- /dev/null +++ b/src/test/register.spec.ts @@ -0,0 +1,187 @@ +import { once } from 'lodash'; +import { + installTsNode, + PROJECT, + testsDirRequire, + TEST_DIR, + tsNodeTypes, +} from './helpers'; +import { test } from './testlib'; +import { expect } from 'chai'; +import { join } from 'path'; +import proxyquire = require('proxyquire'); +import type * as Module from 'module'; + +const SOURCE_MAP_REGEXP = /\/\/# sourceMappingURL=data:application\/json;charset=utf\-8;base64,[\w\+]+=*$/; + +// Set after ts-node is installed locally +let { register }: typeof tsNodeTypes = {} as any; +test.beforeAll(async () => { + await installTsNode(); + ({ register } = testsDirRequire('ts-node')); +}); + +test.suite('register', (_test) => { + const test = _test.context( + once(async () => { + return { + registered: register({ + project: PROJECT, + compilerOptions: { + jsx: 'preserve', + }, + }), + moduleTestPath: require.resolve('../../tests/module'), + }; + }) + ); + test.beforeEach(async ({ context: { registered } }) => { + // Re-enable project for every test. + registered.enabled(true); + }); + test.runSerially(); + + test('should be able to require typescript', ({ + context: { moduleTestPath }, + }) => { + const m = require(moduleTestPath); + + expect(m.example('foo')).to.equal('FOO'); + }); + + test('should support dynamically disabling', ({ + context: { registered, moduleTestPath }, + }) => { + delete require.cache[moduleTestPath]; + + expect(registered.enabled(false)).to.equal(false); + expect(() => require(moduleTestPath)).to.throw(/Unexpected token/); + + delete require.cache[moduleTestPath]; + + expect(registered.enabled()).to.equal(false); + expect(() => require(moduleTestPath)).to.throw(/Unexpected token/); + + delete require.cache[moduleTestPath]; + + expect(registered.enabled(true)).to.equal(true); + expect(() => require(moduleTestPath)).to.not.throw(); + + delete require.cache[moduleTestPath]; + + expect(registered.enabled()).to.equal(true); + expect(() => require(moduleTestPath)).to.not.throw(); + }); + + test('should support compiler scopes', ({ + context: { registered, moduleTestPath }, + }) => { + const calls: string[] = []; + + registered.enabled(false); + + const compilers = [ + register({ + projectSearchDir: join(TEST_DIR, 'scope/a'), + scopeDir: join(TEST_DIR, 'scope/a'), + scope: true, + }), + register({ + projectSearchDir: join(TEST_DIR, 'scope/a'), + scopeDir: join(TEST_DIR, 'scope/b'), + scope: true, + }), + ]; + + compilers.forEach((c) => { + const old = c.compile; + c.compile = (code, fileName, lineOffset) => { + calls.push(fileName); + + return old(code, fileName, lineOffset); + }; + }); + + try { + expect(require('../../tests/scope/a').ext).to.equal('.ts'); + expect(require('../../tests/scope/b').ext).to.equal('.ts'); + } finally { + compilers.forEach((c) => c.enabled(false)); + } + + expect(calls).to.deep.equal([ + join(TEST_DIR, 'scope/a/index.ts'), + join(TEST_DIR, 'scope/b/index.ts'), + ]); + + delete require.cache[moduleTestPath]; + + expect(() => require(moduleTestPath)).to.throw(); + }); + + test('should compile through js and ts', () => { + const m = require('../../tests/complex'); + + expect(m.example()).to.equal('example'); + }); + + test('should work with proxyquire', () => { + const m = proxyquire('../../tests/complex', { + './example': 'hello', + }); + + expect(m.example()).to.equal('hello'); + }); + + test('should work with `require.cache`', () => { + const { example1, example2 } = require('../../tests/require-cache'); + + expect(example1).to.not.equal(example2); + }); + + test('should use source maps', async () => { + try { + require('../../tests/throw error'); + } catch (error: any) { + expect(error.stack).to.contain( + [ + 'Error: this is a demo', + ` at Foo.bar (${join(TEST_DIR, './throw error.ts')}:100:17)`, + ].join('\n') + ); + } + }); + + test.suite('JSX preserve', (test) => { + let old: (m: Module, filename: string) => any; + let compiled: string; + + test.runSerially(); + test.beforeAll(async () => { + old = require.extensions['.tsx']!; + require.extensions['.tsx'] = (m: any, fileName) => { + const _compile = m._compile; + + m._compile = function (code: string, fileName: string) { + compiled = code; + return _compile.call(this, code, fileName); + }; + + return old(m, fileName); + }; + }); + + test('should use source maps', async (t) => { + t.teardown(() => { + require.extensions['.tsx'] = old; + }); + try { + require('../../tests/with-jsx.tsx'); + } catch (error: any) { + expect(error.stack).to.contain('SyntaxError: Unexpected token'); + } + + expect(compiled).to.match(SOURCE_MAP_REGEXP); + }); + }); +}); diff --git a/src/test/repl/helpers.ts b/src/test/repl/helpers.ts new file mode 100644 index 000000000..cc4edfcf7 --- /dev/null +++ b/src/test/repl/helpers.ts @@ -0,0 +1,103 @@ +import * as promisify from 'util.promisify'; +import { PassThrough } from 'stream'; +import { getStream, TEST_DIR, tsNodeTypes } from '../helpers'; +import type { ExecutionContext } from 'ava'; + +export interface ContextWithTsNodeUnderTest { + tsNodeUnderTest: Pick< + typeof tsNodeTypes, + 'create' | 'register' | 'createRepl' + >; +} + +export interface CreateReplViaApiOptions { + registerHooks: true; + createReplOpts?: Partial; + createServiceOpts?: Partial; +} + +/** + * pass to test.context() to get REPL testing helper functions + */ +export async function contextReplHelpers( + t: ExecutionContext +) { + const { tsNodeUnderTest } = t.context; + return { createReplViaApi, executeInRepl }; + + function createReplViaApi({ + registerHooks, + createReplOpts, + createServiceOpts, + }: CreateReplViaApiOptions) { + const stdin = new PassThrough(); + const stdout = new PassThrough(); + const stderr = new PassThrough(); + const replService = tsNodeUnderTest.createRepl({ + stdin, + stdout, + stderr, + ...createReplOpts, + }); + const service = (registerHooks + ? tsNodeUnderTest.register + : tsNodeUnderTest.create)({ + ...replService.evalAwarePartialHost, + project: `${TEST_DIR}/tsconfig.json`, + ...createServiceOpts, + }); + replService.setService(service); + t.teardown(async () => { + service.enabled(false); + }); + + return { stdin, stdout, stderr, replService, service }; + } + + // Todo combine with replApiMacro + async function executeInRepl( + input: string, + options: CreateReplViaApiOptions & { + waitMs?: number; + waitPattern?: string | RegExp; + /** When specified, calls `startInternal` instead of `start` and passes options */ + startInternalOptions?: Parameters< + tsNodeTypes.ReplService['startInternal'] + >[0]; + } + ) { + const { + waitPattern, + // Wait longer if there's a signal to end it early + waitMs = waitPattern != null ? 20e3 : 1e3, + startInternalOptions, + ...rest + } = options; + const { stdin, stdout, stderr, replService } = createReplViaApi(rest); + + if (startInternalOptions) { + replService.startInternal(startInternalOptions); + } else { + replService.start(); + } + + stdin.write(input); + stdin.end(); + const stdoutPromise = getStream(stdout, waitPattern); + const stderrPromise = getStream(stderr, waitPattern); + // Wait for expected output pattern or timeout, whichever comes first + await Promise.race([ + promisify(setTimeout)(waitMs), + stdoutPromise, + stderrPromise, + ]); + stdout.end(); + stderr.end(); + + return { + stdin, + stdout: await stdoutPromise, + stderr: await stderrPromise, + }; + } +} diff --git a/src/test/node-repl-tla.ts b/src/test/repl/node-repl-tla.ts similarity index 97% rename from src/test/node-repl-tla.ts rename to src/test/repl/node-repl-tla.ts index c981892d1..210d22ec0 100644 --- a/src/test/node-repl-tla.ts +++ b/src/test/repl/node-repl-tla.ts @@ -1,25 +1,23 @@ import { expect } from 'chai'; import type { Key } from 'readline'; import { Stream } from 'stream'; -import type * as tsNodeTypes from '../index'; import semver = require('semver'); import ts = require('typescript'); +import type { ContextWithTsNodeUnderTest } from './helpers'; -interface SharedObjects - extends Pick { +interface SharedObjects extends ContextWithTsNodeUnderTest { TEST_DIR: string; } // Based on https://github.com/nodejs/node/blob/88799930794045795e8abac874730f9eba7e2300/test/parallel/test-repl-top-level-await.js export async function upstreamTopLevelAwaitTests({ TEST_DIR, - create, - createRepl, + tsNodeUnderTest, }: SharedObjects) { const PROMPT = 'await repl > '; const putIn = new REPLStream(); - const replService = createRepl({ + const replService = tsNodeUnderTest.createRepl({ // @ts-ignore stdin: putIn, // @ts-ignore @@ -27,7 +25,7 @@ export async function upstreamTopLevelAwaitTests({ // @ts-ignore stderr: putIn, }); - const service = create({ + const service = tsNodeUnderTest.create({ ...replService.evalAwarePartialHost, project: `${TEST_DIR}/tsconfig.json`, experimentalReplAwait: true, diff --git a/src/test/repl/repl-environment.spec.ts b/src/test/repl/repl-environment.spec.ts new file mode 100644 index 000000000..3ce36441c --- /dev/null +++ b/src/test/repl/repl-environment.spec.ts @@ -0,0 +1,472 @@ +/* + * Tests that the REPL environment is setup correctly: + * globals, __filename, builtin module accessors. + */ + +import { test as _test } from '../testlib'; +import * as expect from 'expect'; +import * as promisify from 'util.promisify'; +import * as getStream from 'get-stream'; +import { + CMD_TS_NODE_WITH_PROJECT_FLAG, + contextTsNodeUnderTest, + ROOT_DIR, + TEST_DIR, +} from '../helpers'; +import { dirname, join } from 'path'; +import { createExec, createExecTester } from '../exec-helpers'; +import { homedir } from 'os'; +import { contextReplHelpers } from './helpers'; + +const test = _test.context(contextTsNodeUnderTest).context(contextReplHelpers); + +const exec = createExec({ + cwd: TEST_DIR, +}); +const execTester = createExecTester({ + cmd: CMD_TS_NODE_WITH_PROJECT_FLAG, + exec, +}); + +test.suite( + '[eval], , and [stdin] execute with correct globals', + (test) => { + interface GlobalInRepl extends NodeJS.Global { + testReport: any; + replReport: any; + stdinReport: any; + evalReport: any; + module: any; + exports: any; + fs: any; + __filename: any; + __dirname: any; + } + const globalInRepl = global as GlobalInRepl; + const programmaticTest = test.macro( + ( + { + evalCodeBefore, + stdinCode, + }: { + evalCodeBefore: string | null; + stdinCode: string; + }, + assertions: (stdout: string) => Promise | void + ) => async (t) => { + delete globalInRepl.testReport; + delete globalInRepl.replReport; + delete globalInRepl.stdinReport; + delete globalInRepl.evalReport; + delete globalInRepl.module; + delete globalInRepl.exports; + delete globalInRepl.fs; + delete globalInRepl.__filename; + delete globalInRepl.__dirname; + const { + stdin, + stderr, + stdout, + replService, + } = t.context.createReplViaApi({ registerHooks: true }); + if (typeof evalCodeBefore === 'string') { + replService.evalCode(evalCodeBefore); + } + replService.start(); + stdin.write(stdinCode); + stdin.end(); + await promisify(setTimeout)(1e3); + stdout.end(); + stderr.end(); + expect(await getStream(stderr)).toBe(''); + await assertions(await getStream(stdout)); + } + ); + + const declareGlobals = `declare var replReport: any, stdinReport: any, evalReport: any, restReport: any, global: any, __filename: any, __dirname: any, module: any, exports: any, fs: any;`; + function setReportGlobal(type: 'repl' | 'stdin' | 'eval') { + return ` + ${declareGlobals} + global.${type}Report = { + __filename: typeof __filename !== 'undefined' && __filename, + __dirname: typeof __dirname !== 'undefined' && __dirname, + moduleId: typeof module !== 'undefined' && module.id, + modulePath: typeof module !== 'undefined' && module.path, + moduleFilename: typeof module !== 'undefined' && module.filename, + modulePaths: typeof module !== 'undefined' && [...module.paths], + exportsTest: typeof exports !== 'undefined' ? module.exports === exports : null, + stackTest: new Error().stack!.split('\\n')[1], + moduleAccessorsTest: typeof fs === 'undefined' ? null : fs === require('fs'), + argv: [...process.argv] + }; + `.replace(/\n/g, ''); + } + const reportsObject = ` + { + stdinReport: typeof stdinReport !== 'undefined' && stdinReport, + evalReport: typeof evalReport !== 'undefined' && evalReport, + replReport: typeof replReport !== 'undefined' && replReport + } + `; + const printReports = ` + ${declareGlobals} + console.log(JSON.stringify(${reportsObject})); + `.replace(/\n/g, ''); + const saveReportsAsGlobal = ` + ${declareGlobals} + global.testReport = ${reportsObject}; + `.replace(/\n/g, ''); + + function parseStdoutStripReplPrompt(stdout: string) { + // Strip node's welcome header, only uncomment if running these tests manually against vanilla node + // stdout = stdout.replace(/^Welcome to.*\nType "\.help" .*\n/, ''); + expect(stdout.slice(0, 2)).toBe('> '); + expect(stdout.slice(-12)).toBe('undefined\n> '); + return parseStdout(stdout.slice(2, -12)); + } + function parseStdout(stdout: string) { + return JSON.parse(stdout); + } + + /** Every possible ./node_modules directory ascending upwards starting with ./tests/node_modules */ + const modulePaths = createModulePaths(TEST_DIR); + const rootModulePaths = createModulePaths(ROOT_DIR); + function createModulePaths(dir: string) { + const modulePaths: string[] = []; + for (let path = dir; ; path = dirname(path)) { + modulePaths.push(join(path, 'node_modules')); + if (dirname(path) === path) break; + } + return modulePaths; + } + + // Executable is `ts-node` on Posix, `bin.js` on Windows due to Windows shimming limitations (this is determined by package manager) + const tsNodeExe = expect.stringMatching(/\b(ts-node|bin.js)$/); + + test('stdin', async (t) => { + const { stdout } = await execTester({ + stdin: `${setReportGlobal('stdin')};${printReports}`, + flags: '', + }); + const report = parseStdout(stdout); + expect(report).toMatchObject({ + stdinReport: { + __filename: '[stdin]', + __dirname: '.', + moduleId: '[stdin]', + modulePath: '.', + // Note: vanilla node does does not have file extension + moduleFilename: join(TEST_DIR, `[stdin].ts`), + modulePaths, + exportsTest: true, + // Note: vanilla node uses different name. See #1360 + stackTest: expect.stringContaining( + ` at ${join(TEST_DIR, `[stdin].ts`)}:1:` + ), + moduleAccessorsTest: null, + argv: [tsNodeExe], + }, + evalReport: false, + replReport: false, + }); + }); + test('repl', async (t) => { + const { stdout } = await execTester({ + stdin: `${setReportGlobal('repl')};${printReports}`, + flags: '-i', + }); + const report = parseStdoutStripReplPrompt(stdout); + expect(report).toMatchObject({ + stdinReport: false, + evalReport: false, + replReport: { + __filename: false, + __dirname: false, + moduleId: '', + modulePath: '.', + moduleFilename: null, + modulePaths: expect.objectContaining({ + ...[join(TEST_DIR, `repl/node_modules`), ...modulePaths], + }), + // Note: vanilla node REPL does not set exports + exportsTest: true, + // Note: vanilla node uses different name. See #1360 + stackTest: expect.stringContaining( + ` at ${join(TEST_DIR, '.ts')}:2:` + ), + moduleAccessorsTest: true, + argv: [tsNodeExe], + }, + }); + // Prior to these, nyc adds another entry on Windows; we need to ignore it + expect(report.replReport.modulePaths.slice(-3)).toMatchObject([ + join(homedir(), `.node_modules`), + join(homedir(), `.node_libraries`), + // additional entry goes to node's install path + expect.any(String), + ]); + }); + + // Should ignore -i and run the entrypoint + test('-i w/entrypoint ignores -i', async (t) => { + const { stdout } = await execTester({ + stdin: `${setReportGlobal('repl')};${printReports}`, + flags: '-i ./repl/script.js', + }); + const report = parseStdout(stdout); + expect(report).toMatchObject({ + stdinReport: false, + evalReport: false, + replReport: false, + }); + }); + + // Should not execute stdin + // Should not interpret positional arg as an entrypoint script + test('-e', async (t) => { + const { stdout } = await execTester({ + stdin: `throw new Error()`, + flags: `-e "${setReportGlobal('eval')};${printReports}"`, + }); + const report = parseStdout(stdout); + expect(report).toMatchObject({ + stdinReport: false, + evalReport: { + __filename: '[eval]', + __dirname: '.', + moduleId: '[eval]', + modulePath: '.', + // Note: vanilla node does does not have file extension + moduleFilename: join(TEST_DIR, `[eval].ts`), + modulePaths: [...modulePaths], + exportsTest: true, + // Note: vanilla node uses different name. See #1360 + stackTest: expect.stringContaining( + ` at ${join(TEST_DIR, `[eval].ts`)}:1:` + ), + moduleAccessorsTest: true, + argv: [tsNodeExe], + }, + replReport: false, + }); + }); + test('-e w/entrypoint arg does not execute entrypoint', async (t) => { + const { stdout } = await execTester({ + stdin: `throw new Error()`, + flags: `-e "${setReportGlobal( + 'eval' + )};${printReports}" ./repl/script.js`, + }); + const report = parseStdout(stdout); + expect(report).toMatchObject({ + stdinReport: false, + evalReport: { + __filename: '[eval]', + __dirname: '.', + moduleId: '[eval]', + modulePath: '.', + // Note: vanilla node does does not have file extension + moduleFilename: join(TEST_DIR, `[eval].ts`), + modulePaths, + exportsTest: true, + // Note: vanilla node uses different name. See #1360 + stackTest: expect.stringContaining( + ` at ${join(TEST_DIR, `[eval].ts`)}:1:` + ), + moduleAccessorsTest: true, + argv: [tsNodeExe, './repl/script.js'], + }, + replReport: false, + }); + }); + test('-e w/non-path arg', async (t) => { + const { stdout } = await execTester({ + stdin: `throw new Error()`, + flags: `-e "${setReportGlobal( + 'eval' + )};${printReports}" ./does-not-exist.js`, + }); + const report = parseStdout(stdout); + expect(report).toMatchObject({ + stdinReport: false, + evalReport: { + __filename: '[eval]', + __dirname: '.', + moduleId: '[eval]', + modulePath: '.', + // Note: vanilla node does does not have file extension + moduleFilename: join(TEST_DIR, `[eval].ts`), + modulePaths, + exportsTest: true, + // Note: vanilla node uses different name. See #1360 + stackTest: expect.stringContaining( + ` at ${join(TEST_DIR, `[eval].ts`)}:1:` + ), + moduleAccessorsTest: true, + argv: [tsNodeExe, './does-not-exist.js'], + }, + replReport: false, + }); + }); + test('-e -i', async (t) => { + const { stdout } = await execTester({ + stdin: `${setReportGlobal('repl')};${printReports}`, + flags: `-e "${setReportGlobal('eval')}" -i`, + }); + const report = parseStdoutStripReplPrompt(stdout); + expect(report).toMatchObject({ + stdinReport: false, + evalReport: { + __filename: '[eval]', + __dirname: '.', + moduleId: '[eval]', + modulePath: '.', + // Note: vanilla node does does not have file extension + moduleFilename: join(TEST_DIR, `[eval].ts`), + modulePaths, + exportsTest: true, + // Note: vanilla node uses different name. See #1360 + stackTest: expect.stringContaining( + ` at ${join(TEST_DIR, `[eval].ts`)}:1:` + ), + moduleAccessorsTest: true, + argv: [tsNodeExe], + }, + replReport: { + __filename: '[eval]', + __dirname: '.', + moduleId: '', + modulePath: '.', + moduleFilename: null, + modulePaths: expect.objectContaining({ + ...[join(TEST_DIR, `repl/node_modules`), ...modulePaths], + }), + // Note: vanilla node REPL does not set exports, so this would be false + exportsTest: true, + // Note: vanilla node uses different name. See #1360 + stackTest: expect.stringContaining( + ` at ${join(TEST_DIR, '.ts')}:2:` + ), + moduleAccessorsTest: true, + argv: [tsNodeExe], + }, + }); + // Prior to these, nyc adds another entry on Windows; we need to ignore it + expect(report.replReport.modulePaths.slice(-3)).toMatchObject([ + join(homedir(), `.node_modules`), + join(homedir(), `.node_libraries`), + // additional entry goes to node's install path + expect.any(String), + ]); + }); + + test('-e -i w/entrypoint ignores -e and -i, runs entrypoint', async (t) => { + const { stdout } = await execTester({ + stdin: `throw new Error()`, + flags: '-e "throw new Error()" -i ./repl/script.js', + }); + const report = parseStdout(stdout); + expect(report).toMatchObject({ + stdinReport: false, + evalReport: false, + replReport: false, + }); + }); + + test('-e -i when -e throws error, -i does not run', async (t) => { + const { stdout, stderr, err } = await execTester({ + stdin: `console.log('hello')`, + flags: `-e "throw new Error('error from -e')" -i`, + expectError: true, + }); + expect(err).toBeDefined(); + expect(stdout).toBe(''); + expect(stderr).toContain('error from -e'); + }); + + // Serial because it's timing-sensitive + test.serial( + 'programmatically, eval-ing before starting REPL', + programmaticTest, + { + evalCodeBefore: `${setReportGlobal('repl')};${saveReportsAsGlobal}`, + stdinCode: '', + }, + (stdout) => { + expect(globalInRepl.testReport).toMatchObject({ + stdinReport: false, + evalReport: false, + replReport: { + __filename: false, + __dirname: false, + + // Due to limitations in node's REPL API, we can't really expose + // the `module` prior to calling repl.start() which also sends + // output to stdout. + // For now, leaving this as unsupported / undefined behavior. + + // moduleId: '', + // modulePath: '.', + // moduleFilename: null, + // modulePaths: [ + // join(ROOT_DIR, `repl/node_modules`), + // ...rootModulePaths, + // join(homedir(), `.node_modules`), + // join(homedir(), `.node_libraries`), + // // additional entry goes to node's install path + // exp.any(String), + // ], + // // Note: vanilla node REPL does not set exports + // exportsTest: true, + // moduleAccessorsTest: true, + + // Note: vanilla node uses different name. See #1360 + stackTest: expect.stringContaining( + ` at ${join(ROOT_DIR, '.ts')}:1:` + ), + }, + }); + } + ); + test.serial( + 'programmatically, passing code to stdin after starting REPL', + programmaticTest, + { + evalCodeBefore: null, + stdinCode: `${setReportGlobal('repl')};${saveReportsAsGlobal}`, + }, + (stdout) => { + expect(globalInRepl.testReport).toMatchObject({ + stdinReport: false, + evalReport: false, + replReport: { + __filename: false, + __dirname: false, + moduleId: '', + modulePath: '.', + moduleFilename: null, + modulePaths: expect.objectContaining({ + ...[join(ROOT_DIR, `repl/node_modules`), ...rootModulePaths], + }), + // Note: vanilla node REPL does not set exports + exportsTest: true, + // Note: vanilla node uses different name. See #1360 + stackTest: expect.stringContaining( + ` at ${join(ROOT_DIR, '.ts')}:1:` + ), + moduleAccessorsTest: true, + }, + }); + // Prior to these, nyc adds another entry on Windows; we need to ignore it + expect( + globalInRepl.testReport.replReport.modulePaths.slice(-3) + ).toMatchObject([ + join(homedir(), `.node_modules`), + join(homedir(), `.node_libraries`), + // additional entry goes to node's install path + expect.any(String), + ]); + } + ); + } +); diff --git a/src/test/repl/repl.spec.ts b/src/test/repl/repl.spec.ts new file mode 100644 index 000000000..6ec33f434 --- /dev/null +++ b/src/test/repl/repl.spec.ts @@ -0,0 +1,290 @@ +import ts = require('typescript'); +import semver = require('semver'); +import * as expect from 'expect'; +import { + CMD_TS_NODE_WITH_PROJECT_FLAG, + contextTsNodeUnderTest, + TEST_DIR, +} from '../helpers'; +import { createExec, createExecTester } from '../exec-helpers'; +import { upstreamTopLevelAwaitTests } from './node-repl-tla'; +import { _test } from '../testlib'; +import { contextReplHelpers } from './helpers'; + +const test = _test.context(contextTsNodeUnderTest).context(contextReplHelpers); + +const exec = createExec({ + cwd: TEST_DIR, +}); + +const execTester = createExecTester({ + cmd: CMD_TS_NODE_WITH_PROJECT_FLAG, + exec, +}); + +test('should run REPL when --interactive passed and stdin is not a TTY', async () => { + const execPromise = exec(`${CMD_TS_NODE_WITH_PROJECT_FLAG} --interactive`); + execPromise.child.stdin!.end('console.log("123")\n'); + const { err, stdout } = await execPromise; + expect(err).toBe(null); + expect(stdout).toBe('> 123\n' + 'undefined\n' + '> '); +}); + +test('REPL has command to get type information', async () => { + const execPromise = exec(`${CMD_TS_NODE_WITH_PROJECT_FLAG} --interactive`); + execPromise.child.stdin!.end('\nconst a = 123\n.type a'); + const { err, stdout } = await execPromise; + expect(err).toBe(null); + expect(stdout).toBe( + '> undefined\n' + '> undefined\n' + '> const a: 123\n' + '> ' + ); +}); + +// Serial because it's timing-sensitive +test.serial('REPL can be configured on `start`', async (t) => { + const prompt = '#> '; + + const { stdout, stderr } = await t.context.executeInRepl('const x = 3', { + registerHooks: true, + startInternalOptions: { + prompt, + ignoreUndefined: true, + }, + }); + + expect(stderr).toBe(''); + expect(stdout).toBe(`${prompt}${prompt}`); +}); + +// Serial because it's timing-sensitive +test.serial( + 'REPL uses a different context when `useGlobal` is false', + async (t) => { + const { stdout, stderr } = await t.context.executeInRepl( + // No error when re-declaring x + 'const x = 3\n' + + // console.log ouput will end up in the stream and not in test output + 'console.log(1)\n', + { + registerHooks: true, + waitPattern: `> undefined\n> 1\nundefined\n> `, + startInternalOptions: { + useGlobal: false, + }, + } + ); + + expect(stderr).toBe(''); + expect(stdout).toBe(`> undefined\n> 1\nundefined\n> `); + } +); + +// Serial because it's timing-sensitive +test.serial('REPL can be created via API', async (t) => { + const { stdout, stderr } = await t.context.executeInRepl( + `\nconst a = 123\n.type a\n`, + { + registerHooks: true, + waitPattern: '123\n> ', + } + ); + expect(stderr).toBe(''); + expect(stdout).toBe( + '> undefined\n' + '> undefined\n' + '> const a: 123\n' + '> ' + ); +}); + +test.suite('top level await', (_test) => { + const compilerOptions = { + target: 'es2018', + }; + const test = _test.context(async (t) => { + return { executeInTlaRepl }; + + function executeInTlaRepl(input: string, waitPattern?: string | RegExp) { + return t.context.executeInRepl( + input + .split('\n') + .map((line) => line.trim()) + // Restore newline once https://github.com/nodejs/node/pull/39392 is merged + .join(''), + { + registerHooks: true, + waitPattern, + createServiceOpts: { + experimentalReplAwait: true, + compilerOptions, + }, + startInternalOptions: { useGlobal: false }, + } + ); + } + }); + + if (semver.gte(ts.version, '3.8.0')) { + // Serial because it's timing-sensitive + test.serial('should allow evaluating top level await', async (t) => { + const script = ` + const x: number = await new Promise((r) => r(1)); + for await (const x of [1,2,3]) { console.log(x) }; + for (const x of ['a', 'b']) { await x; console.log(x) }; + class Foo {}; await 1; + function Bar() {}; await 2; + const {y} = await ({y: 2}); + const [z] = await [3]; + x + y + z; + `; + + const { stdout, stderr } = await t.context.executeInTlaRepl( + script, + '6\n> ' + ); + expect(stderr).toBe(''); + expect(stdout).toBe('> 1\n2\n3\na\nb\n6\n> '); + }); + + // Serial because it's timing-sensitive + test.serial( + 'should wait until promise is settled when awaiting at top level', + async (t) => { + const awaitMs = 500; + const script = ` + const startTime = new Date().getTime(); + await new Promise((r) => setTimeout(() => r(1), ${awaitMs})); + const endTime = new Date().getTime(); + endTime - startTime; + `; + const { stdout, stderr } = await t.context.executeInTlaRepl( + script, + /\d+\n/ + ); + + expect(stderr).toBe(''); + + const elapsedTimeString = stdout + .split('\n')[0] + .replace('> ', '') + .trim(); + expect(elapsedTimeString).toMatch(/^\d+$/); + const elapsedTime = Number(elapsedTimeString); + expect(elapsedTime).toBeGreaterThanOrEqual(awaitMs - 50); + // When CI is taxed, the time may be *much* greater than expected. + // I can't think of a case where the time being *too high* is a bug + // that this test can catch. So I've made this check very loose. + expect(elapsedTime).toBeLessThanOrEqual(awaitMs + 10e3); + } + ); + + // Serial because it's timing-sensitive + test.serial( + 'should not wait until promise is settled when not using await at top level', + async (t) => { + const script = ` + const startTime = new Date().getTime(); + (async () => await new Promise((r) => setTimeout(() => r(1), ${1000})))(); + const endTime = new Date().getTime(); + endTime - startTime; + `; + const { stdout, stderr } = await t.context.executeInTlaRepl( + script, + /\d+\n/ + ); + + expect(stderr).toBe(''); + + const ellapsedTime = Number( + stdout.split('\n')[0].replace('> ', '').trim() + ); + expect(ellapsedTime).toBeGreaterThanOrEqual(0); + expect(ellapsedTime).toBeLessThanOrEqual(10); + } + ); + + // Serial because it's timing-sensitive + test.serial( + 'should error with typing information when awaited result has type mismatch', + async (t) => { + const { stdout, stderr } = await t.context.executeInTlaRepl( + 'const x: string = await 1', + 'error' + ); + + expect(stdout).toBe('> > '); + expect(stderr.replace(/\r\n/g, '\n')).toBe( + '.ts(2,7): error TS2322: ' + + (semver.gte(ts.version, '4.0.0') + ? `Type 'number' is not assignable to type 'string'.\n` + : `Type '1' is not assignable to type 'string'.\n`) + + '\n' + ); + } + ); + + // Serial because it's timing-sensitive + test.serial( + 'should error with typing information when importing a file with type errors', + async (t) => { + const { stdout, stderr } = await t.context.executeInTlaRepl( + `const {foo} = await import('./tests/repl/tla-import');`, + 'error' + ); + + expect(stdout).toBe('> > '); + expect(stderr.replace(/\r\n/g, '\n')).toBe( + 'tests/repl/tla-import.ts(1,14): error TS2322: ' + + (semver.gte(ts.version, '4.0.0') + ? `Type 'number' is not assignable to type 'string'.\n` + : `Type '1' is not assignable to type 'string'.\n`) + + '\n' + ); + } + ); + + test('should pass upstream test cases', async (t) => { + const { tsNodeUnderTest } = t.context; + upstreamTopLevelAwaitTests({ TEST_DIR, tsNodeUnderTest }); + }); + } else { + test('should throw error when attempting to use top level await on TS < 3.8', async (t) => { + expect(t.context.executeInTlaRepl('')).rejects.toThrow( + 'Experimental REPL await is not compatible with TypeScript versions older than 3.8' + ); + }); + } +}); + +test.suite( + 'REPL ignores diagnostics that are annoying in interactive sessions', + (test) => { + const code = `function foo() {};\nfunction foo() {return 123};\nconsole.log(foo());\n`; + const diagnosticMessage = `Duplicate function implementation`; + test('interactive repl should ignore them', async (t) => { + const { stdout, stderr } = await execTester({ + flags: '-i', + stdin: code, + }); + expect(stdout).not.toContain(diagnosticMessage); + }); + test('interactive repl should not ignore them if they occur in other files', async (t) => { + const { stdout, stderr } = await execTester({ + flags: '-i', + stdin: `import './repl-ignored-diagnostics/index.ts';\n`, + }); + expect(stderr).toContain(diagnosticMessage); + }); + test('[stdin] should not ignore them', async (t) => { + const { stdout, stderr } = await execTester({ + stdin: code, + expectError: true, + }); + expect(stderr).toContain(diagnosticMessage); + }); + test('[eval] should not ignore them', async (t) => { + const { stdout, stderr } = await execTester({ + flags: `-e "${code.replace(/\n/g, '')}"`, + expectError: true, + }); + expect(stderr).toContain(diagnosticMessage); + }); + } +); diff --git a/src/test/testlib.ts b/src/test/testlib.ts index d48af9ddb..5d090d7d7 100644 --- a/src/test/testlib.ts +++ b/src/test/testlib.ts @@ -11,8 +11,11 @@ import avaTest, { } from 'ava'; import * as assert from 'assert'; import throat from 'throat'; +export { ExecutionContext }; -const concurrencyLimiter = throat(8); +// NOTE: this limits concurrency within a single process, but AVA launches +// each .spec file in its own process, so actual concurrency is higher. +const concurrencyLimiter = throat(16); function once(func: T): T { let run = false; @@ -32,6 +35,9 @@ export const test = createTestInterface({ separator: ' > ', titlePrefix: undefined, }); +// In case someone wants to `const test = _test.context()` +export { test as _test }; + export interface TestInterface< Context > /*extends Omit, 'before' | 'beforeEach' | 'after' | 'afterEach' | 'failing' | 'serial'>*/ {