Skip to content

Commit b52ca45

Browse files
authored
Allow composing register(create()); refactor tests (#1474)
* WIP add ability to compose register(create()) * wip * Fix environmental reset in tests * fix * fix * Fix * fix * fix * fix * fix
1 parent 8ad5292 commit b52ca45

File tree

7 files changed

+257
-122
lines changed

7 files changed

+257
-122
lines changed

.vscode/launch.json

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"configurations": [
3+
{
4+
"name": "Debug AVA test file",
5+
"type": "node",
6+
"request": "launch",
7+
"preLaunchTask": "npm: pre-debug",
8+
"runtimeExecutable": "${workspaceFolder}/node_modules/.bin/ava",
9+
"program": "${file}",
10+
"outputCapture": "std",
11+
"skipFiles": [
12+
"<node_internals>/**/*.js"
13+
],
14+
}
15+
],
16+
}

.vscode/tasks.json

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"tasks": [
3+
{
4+
"type": "npm",
5+
"script": "pre-debug",
6+
"problemMatcher": [
7+
"$tsc"
8+
],
9+
"label": "npm: pre-debug",
10+
"detail": "npm run build-tsc && npm run build-pack"
11+
}
12+
]
13+
}

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@
6767
"test-cov": "nyc ava",
6868
"test": "npm run build && npm run lint && npm run test-cov --",
6969
"test-local": "npm run lint-fix && npm run build-tsc && npm run build-pack && npm run test-spec --",
70+
"pre-debug": "npm run build-tsc && npm run build-pack",
7071
"coverage-report": "nyc report --reporter=lcov",
7172
"prepare": "npm run clean && npm run build-nopack",
7273
"api-extractor": "api-extractor run --local --verbose"

src/index.ts

+57-33
Original file line numberDiff line numberDiff line change
@@ -427,10 +427,14 @@ export class TSError extends BaseError {
427427
}
428428
}
429429

430+
const TS_NODE_SERVICE_BRAND = Symbol('TS_NODE_SERVICE_BRAND');
431+
430432
/**
431433
* Primary ts-node service, which wraps the TypeScript API and can compile TypeScript to JavaScript
432434
*/
433435
export interface Service {
436+
/** @internal */
437+
[TS_NODE_SERVICE_BRAND]: true;
434438
ts: TSCommon;
435439
config: _ts.ParsedCommandLine;
436440
options: RegisterOptions;
@@ -446,6 +450,8 @@ export interface Service {
446450
readonly shouldReplAwait: boolean;
447451
/** @internal */
448452
addDiagnosticFilter(filter: DiagnosticFilter): void;
453+
/** @internal */
454+
installSourceMapSupport(): void;
449455
}
450456

451457
/**
@@ -477,12 +483,25 @@ export function getExtensions(config: _ts.ParsedCommandLine) {
477483
return { tsExtensions, jsExtensions };
478484
}
479485

486+
/**
487+
* Create a new TypeScript compiler instance and register it onto node.js
488+
*/
489+
export function register(opts?: RegisterOptions): Service;
480490
/**
481491
* Register TypeScript compiler instance onto node.js
482492
*/
483-
export function register(opts: RegisterOptions = {}): Service {
493+
export function register(service: Service): Service;
494+
export function register(
495+
serviceOrOpts: Service | RegisterOptions | undefined
496+
): Service {
497+
// Is this a Service or a RegisterOptions?
498+
let service = serviceOrOpts as Service;
499+
if (!(serviceOrOpts as Service)?.[TS_NODE_SERVICE_BRAND]) {
500+
// Not a service; is options
501+
service = create((serviceOrOpts ?? {}) as RegisterOptions);
502+
}
503+
484504
const originalJsHandler = require.extensions['.js'];
485-
const service = create(opts);
486505
const { tsExtensions, jsExtensions } = getExtensions(service.config);
487506
const extensions = [...tsExtensions, ...jsExtensions];
488507

@@ -660,38 +679,41 @@ export function create(rawOptions: CreateOptions = {}): Service {
660679
}
661680

662681
// Install source map support and read from memory cache.
663-
sourceMapSupport.install({
664-
environment: 'node',
665-
retrieveFile(pathOrUrl: string) {
666-
let path = pathOrUrl;
667-
// If it's a file URL, convert to local path
668-
// Note: fileURLToPath does not exist on early node v10
669-
// I could not find a way to handle non-URLs except to swallow an error
670-
if (options.experimentalEsmLoader && path.startsWith('file://')) {
671-
try {
672-
path = fileURLToPath(path);
673-
} catch (e) {
674-
/* swallow error */
682+
installSourceMapSupport();
683+
function installSourceMapSupport() {
684+
sourceMapSupport.install({
685+
environment: 'node',
686+
retrieveFile(pathOrUrl: string) {
687+
let path = pathOrUrl;
688+
// If it's a file URL, convert to local path
689+
// Note: fileURLToPath does not exist on early node v10
690+
// I could not find a way to handle non-URLs except to swallow an error
691+
if (options.experimentalEsmLoader && path.startsWith('file://')) {
692+
try {
693+
path = fileURLToPath(path);
694+
} catch (e) {
695+
/* swallow error */
696+
}
675697
}
676-
}
677-
path = normalizeSlashes(path);
678-
return outputCache.get(path)?.content || '';
679-
},
680-
redirectConflictingLibrary: true,
681-
onConflictingLibraryRedirect(
682-
request,
683-
parent,
684-
isMain,
685-
options,
686-
redirectedRequest
687-
) {
688-
debug(
689-
`Redirected an attempt to require source-map-support to instead receive @cspotcode/source-map-support. "${
690-
(parent as NodeJS.Module).filename
691-
}" attempted to require or resolve "${request}" and was redirected to "${redirectedRequest}".`
692-
);
693-
},
694-
});
698+
path = normalizeSlashes(path);
699+
return outputCache.get(path)?.content || '';
700+
},
701+
redirectConflictingLibrary: true,
702+
onConflictingLibraryRedirect(
703+
request,
704+
parent,
705+
isMain,
706+
options,
707+
redirectedRequest
708+
) {
709+
debug(
710+
`Redirected an attempt to require source-map-support to instead receive @cspotcode/source-map-support. "${
711+
(parent as NodeJS.Module).filename
712+
}" attempted to require or resolve "${request}" and was redirected to "${redirectedRequest}".`
713+
);
714+
},
715+
});
716+
}
695717

696718
const shouldHavePrettyErrors =
697719
options.pretty === undefined ? process.stdout.isTTY : options.pretty;
@@ -1239,6 +1261,7 @@ export function create(rawOptions: CreateOptions = {}): Service {
12391261
}
12401262

12411263
return {
1264+
[TS_NODE_SERVICE_BRAND]: true,
12421265
ts,
12431266
config,
12441267
compile,
@@ -1250,6 +1273,7 @@ export function create(rawOptions: CreateOptions = {}): Service {
12501273
moduleTypeClassifier,
12511274
shouldReplAwait,
12521275
addDiagnosticFilter,
1276+
installSourceMapSupport,
12531277
};
12541278
}
12551279

src/test/helpers.ts

+63-3
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,9 @@ import type { Readable } from 'stream';
1212
*/
1313
import type * as tsNodeTypes from '../index';
1414
import type _createRequire from 'create-require';
15-
import { once } from 'lodash';
15+
import { has, once } from 'lodash';
1616
import semver = require('semver');
17-
import { isConstructSignatureDeclaration } from 'typescript';
17+
import * as expect from 'expect';
1818
const createRequire: typeof _createRequire = require('create-require');
1919
export { tsNodeTypes };
2020

@@ -45,7 +45,7 @@ export const xfs = new NodeFS(fs);
4545
/** Pass to `test.context()` to get access to the ts-node API under test */
4646
export const contextTsNodeUnderTest = once(async () => {
4747
await installTsNode();
48-
const tsNodeUnderTest = testsDirRequire('ts-node');
48+
const tsNodeUnderTest: typeof tsNodeTypes = testsDirRequire('ts-node');
4949
return {
5050
tsNodeUnderTest,
5151
};
@@ -155,3 +155,63 @@ export function getStream(stream: Readable, waitForPattern?: string | RegExp) {
155155
combinedString = combinedBuffer.toString('utf8');
156156
}
157157
}
158+
159+
const defaultRequireExtensions = captureObjectState(require.extensions);
160+
const defaultProcess = captureObjectState(process);
161+
const defaultModule = captureObjectState(require('module'));
162+
const defaultError = captureObjectState(Error);
163+
const defaultGlobal = captureObjectState(global);
164+
165+
/**
166+
* Undo all of ts-node & co's installed hooks, resetting the node environment to default
167+
* so we can run multiple test cases which `.register()` ts-node.
168+
*
169+
* Must also play nice with `nyc`'s environmental mutations.
170+
*/
171+
export function resetNodeEnvironment() {
172+
// We must uninstall so that it resets its internal state; otherwise it won't know it needs to reinstall in the next test.
173+
require('@cspotcode/source-map-support').uninstall();
174+
175+
// Modified by ts-node hooks
176+
resetObject(require.extensions, defaultRequireExtensions);
177+
178+
// ts-node attaches a property when it registers an instance
179+
// source-map-support monkey-patches the emit function
180+
resetObject(process, defaultProcess);
181+
182+
// source-map-support swaps out the prepareStackTrace function
183+
resetObject(Error, defaultError);
184+
185+
// _resolveFilename is modified by tsconfig-paths, future versions of source-map-support, and maybe future versions of ts-node
186+
resetObject(require('module'), defaultModule);
187+
188+
// May be modified by REPL tests, since the REPL sets globals.
189+
resetObject(global, defaultGlobal);
190+
}
191+
192+
function captureObjectState(object: any) {
193+
return {
194+
descriptors: Object.getOwnPropertyDescriptors(object),
195+
values: { ...object },
196+
};
197+
}
198+
// Redefine all property descriptors and delete any new properties
199+
function resetObject(
200+
object: any,
201+
state: ReturnType<typeof captureObjectState>
202+
) {
203+
const currentDescriptors = Object.getOwnPropertyDescriptors(object);
204+
for (const key of Object.keys(currentDescriptors)) {
205+
if (!has(state.descriptors, key)) {
206+
delete object[key];
207+
}
208+
}
209+
// Trigger nyc's setter functions
210+
for (const [key, value] of Object.entries(state.values)) {
211+
try {
212+
object[key] = value;
213+
} catch {}
214+
}
215+
// Reset descriptors
216+
Object.defineProperties(object, state.descriptors);
217+
}

0 commit comments

Comments
 (0)