Skip to content

Commit

Permalink
Allow composing register(create()); refactor tests (#1474)
Browse files Browse the repository at this point in the history
* WIP add ability to compose register(create())

* wip

* Fix environmental reset in tests

* fix

* fix

* Fix

* fix

* fix

* fix

* fix
  • Loading branch information
cspotcode committed Oct 10, 2021
1 parent 8ad5292 commit b52ca45
Show file tree
Hide file tree
Showing 7 changed files with 257 additions and 122 deletions.
16 changes: 16 additions & 0 deletions .vscode/launch.json
@@ -0,0 +1,16 @@
{
"configurations": [
{
"name": "Debug AVA test file",
"type": "node",
"request": "launch",
"preLaunchTask": "npm: pre-debug",
"runtimeExecutable": "${workspaceFolder}/node_modules/.bin/ava",
"program": "${file}",
"outputCapture": "std",
"skipFiles": [
"<node_internals>/**/*.js"
],
}
],
}
13 changes: 13 additions & 0 deletions .vscode/tasks.json
@@ -0,0 +1,13 @@
{
"tasks": [
{
"type": "npm",
"script": "pre-debug",
"problemMatcher": [
"$tsc"
],
"label": "npm: pre-debug",
"detail": "npm run build-tsc && npm run build-pack"
}
]
}
1 change: 1 addition & 0 deletions package.json
Expand Up @@ -67,6 +67,7 @@
"test-cov": "nyc ava",
"test": "npm run build && npm run lint && npm run test-cov --",
"test-local": "npm run lint-fix && npm run build-tsc && npm run build-pack && npm run test-spec --",
"pre-debug": "npm run build-tsc && npm run build-pack",
"coverage-report": "nyc report --reporter=lcov",
"prepare": "npm run clean && npm run build-nopack",
"api-extractor": "api-extractor run --local --verbose"
Expand Down
90 changes: 57 additions & 33 deletions src/index.ts
Expand Up @@ -427,10 +427,14 @@ export class TSError extends BaseError {
}
}

const TS_NODE_SERVICE_BRAND = Symbol('TS_NODE_SERVICE_BRAND');

/**
* Primary ts-node service, which wraps the TypeScript API and can compile TypeScript to JavaScript
*/
export interface Service {
/** @internal */
[TS_NODE_SERVICE_BRAND]: true;
ts: TSCommon;
config: _ts.ParsedCommandLine;
options: RegisterOptions;
Expand All @@ -446,6 +450,8 @@ export interface Service {
readonly shouldReplAwait: boolean;
/** @internal */
addDiagnosticFilter(filter: DiagnosticFilter): void;
/** @internal */
installSourceMapSupport(): void;
}

/**
Expand Down Expand Up @@ -477,12 +483,25 @@ export function getExtensions(config: _ts.ParsedCommandLine) {
return { tsExtensions, jsExtensions };
}

/**
* Create a new TypeScript compiler instance and register it onto node.js
*/
export function register(opts?: RegisterOptions): Service;
/**
* Register TypeScript compiler instance onto node.js
*/
export function register(opts: RegisterOptions = {}): Service {
export function register(service: Service): Service;
export function register(
serviceOrOpts: Service | RegisterOptions | undefined
): Service {
// Is this a Service or a RegisterOptions?
let service = serviceOrOpts as Service;
if (!(serviceOrOpts as Service)?.[TS_NODE_SERVICE_BRAND]) {
// Not a service; is options
service = create((serviceOrOpts ?? {}) as RegisterOptions);
}

const originalJsHandler = require.extensions['.js'];
const service = create(opts);
const { tsExtensions, jsExtensions } = getExtensions(service.config);
const extensions = [...tsExtensions, ...jsExtensions];

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

// Install source map support and read from memory cache.
sourceMapSupport.install({
environment: 'node',
retrieveFile(pathOrUrl: string) {
let path = pathOrUrl;
// If it's a file URL, convert to local path
// Note: fileURLToPath does not exist on early node v10
// I could not find a way to handle non-URLs except to swallow an error
if (options.experimentalEsmLoader && path.startsWith('file://')) {
try {
path = fileURLToPath(path);
} catch (e) {
/* swallow error */
installSourceMapSupport();
function installSourceMapSupport() {
sourceMapSupport.install({
environment: 'node',
retrieveFile(pathOrUrl: string) {
let path = pathOrUrl;
// If it's a file URL, convert to local path
// Note: fileURLToPath does not exist on early node v10
// I could not find a way to handle non-URLs except to swallow an error
if (options.experimentalEsmLoader && path.startsWith('file://')) {
try {
path = fileURLToPath(path);
} catch (e) {
/* swallow error */
}
}
}
path = normalizeSlashes(path);
return outputCache.get(path)?.content || '';
},
redirectConflictingLibrary: true,
onConflictingLibraryRedirect(
request,
parent,
isMain,
options,
redirectedRequest
) {
debug(
`Redirected an attempt to require source-map-support to instead receive @cspotcode/source-map-support. "${
(parent as NodeJS.Module).filename
}" attempted to require or resolve "${request}" and was redirected to "${redirectedRequest}".`
);
},
});
path = normalizeSlashes(path);
return outputCache.get(path)?.content || '';
},
redirectConflictingLibrary: true,
onConflictingLibraryRedirect(
request,
parent,
isMain,
options,
redirectedRequest
) {
debug(
`Redirected an attempt to require source-map-support to instead receive @cspotcode/source-map-support. "${
(parent as NodeJS.Module).filename
}" attempted to require or resolve "${request}" and was redirected to "${redirectedRequest}".`
);
},
});
}

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

return {
[TS_NODE_SERVICE_BRAND]: true,
ts,
config,
compile,
Expand All @@ -1250,6 +1273,7 @@ export function create(rawOptions: CreateOptions = {}): Service {
moduleTypeClassifier,
shouldReplAwait,
addDiagnosticFilter,
installSourceMapSupport,
};
}

Expand Down
66 changes: 63 additions & 3 deletions src/test/helpers.ts
Expand Up @@ -12,9 +12,9 @@ import type { Readable } from 'stream';
*/
import type * as tsNodeTypes from '../index';
import type _createRequire from 'create-require';
import { once } from 'lodash';
import { has, once } from 'lodash';
import semver = require('semver');
import { isConstructSignatureDeclaration } from 'typescript';
import * as expect from 'expect';
const createRequire: typeof _createRequire = require('create-require');
export { tsNodeTypes };

Expand Down Expand Up @@ -45,7 +45,7 @@ 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');
const tsNodeUnderTest: typeof tsNodeTypes = testsDirRequire('ts-node');
return {
tsNodeUnderTest,
};
Expand Down Expand Up @@ -155,3 +155,63 @@ export function getStream(stream: Readable, waitForPattern?: string | RegExp) {
combinedString = combinedBuffer.toString('utf8');
}
}

const defaultRequireExtensions = captureObjectState(require.extensions);
const defaultProcess = captureObjectState(process);
const defaultModule = captureObjectState(require('module'));
const defaultError = captureObjectState(Error);
const defaultGlobal = captureObjectState(global);

/**
* Undo all of ts-node & co's installed hooks, resetting the node environment to default
* so we can run multiple test cases which `.register()` ts-node.
*
* Must also play nice with `nyc`'s environmental mutations.
*/
export function resetNodeEnvironment() {
// We must uninstall so that it resets its internal state; otherwise it won't know it needs to reinstall in the next test.
require('@cspotcode/source-map-support').uninstall();

// Modified by ts-node hooks
resetObject(require.extensions, defaultRequireExtensions);

// ts-node attaches a property when it registers an instance
// source-map-support monkey-patches the emit function
resetObject(process, defaultProcess);

// source-map-support swaps out the prepareStackTrace function
resetObject(Error, defaultError);

// _resolveFilename is modified by tsconfig-paths, future versions of source-map-support, and maybe future versions of ts-node
resetObject(require('module'), defaultModule);

// May be modified by REPL tests, since the REPL sets globals.
resetObject(global, defaultGlobal);
}

function captureObjectState(object: any) {
return {
descriptors: Object.getOwnPropertyDescriptors(object),
values: { ...object },
};
}
// Redefine all property descriptors and delete any new properties
function resetObject(
object: any,
state: ReturnType<typeof captureObjectState>
) {
const currentDescriptors = Object.getOwnPropertyDescriptors(object);
for (const key of Object.keys(currentDescriptors)) {
if (!has(state.descriptors, key)) {
delete object[key];
}
}
// Trigger nyc's setter functions
for (const [key, value] of Object.entries(state.values)) {
try {
object[key] = value;
} catch {}
}
// Reset descriptors
Object.defineProperties(object, state.descriptors);
}

0 comments on commit b52ca45

Please sign in to comment.