jasmine
Version:
CLI for Jasmine, a simple JavaScript testing framework for browsers and Node
169 lines (152 loc) • 7.27 kB
JavaScript
const path = require('path');
const url = require('url');
class Loader {
constructor(options) {
options = options || {};
this.require_ = options.requireShim || requireShim;
this.import_ = options.importShim || importShim;
this.resolvePath_ = options.resolvePath || path.resolve.bind(path);
this.alwaysImport = true;
}
load(modulePath) {
if ((this.alwaysImport && !modulePath.endsWith('.json')) || modulePath.endsWith('.mjs')) {
const importSpecifier = this.resolveImportSpecifier_(modulePath);
return this.import_(importSpecifier)
.then(
mod => mod.default,
e => {
if (e.code === 'ERR_UNKNOWN_FILE_EXTENSION') {
// Extension isn't supported by import, e.g. .jsx. Fall back to
// require(). This could lead to confusing error messages if someone
// tries to use ES module syntax without transpiling in a file with
// an unsupported extension, but it shouldn't break anything and it
// should work well in the normal case where the file is loadable
// as a CommonJS module, either directly or with the help of a
// loader like `@babel/register`.
return this.require_(modulePath);
} else {
return Promise.reject(fixupImportException(e, modulePath));
}
}
);
} else {
return new Promise(resolve => {
const result = this.require_(modulePath);
resolve(result);
});
}
}
resolveImportSpecifier_(modulePath) {
const isNamespaced = modulePath.startsWith('@');
const isRelative = modulePath.startsWith('.');
const hasExtension = /\.[A-Za-z]+/.test(modulePath);
const resolvedModulePath = hasExtension || isRelative
? this.resolvePath_(modulePath)
: modulePath;
if (isNamespaced || ! hasExtension) {
return resolvedModulePath;
}
// The ES module spec requires import paths to be valid URLs. As of v14,
// Node enforces this on Windows but not on other OSes. On OS X, import
// paths that are URLs must not contain parent directory references.
return url.pathToFileURL(resolvedModulePath).toString();
}
}
function requireShim(modulePath) {
return require(modulePath);
}
function importShim(modulePath) {
return import(modulePath);
}
function fixupImportException(e, importedPath) {
// When an ES module has a syntax error, the resulting exception does not
// include the filename, which the user will need to debug the problem. We
// need to fix those up to include the filename. However, other kinds of load-
// time errors *do* include the filename and usually the line number. We need
// to leave those alone.
//
// Some examples of load-time errors that we need to deal with:
// 1. Syntax error in an ESM spec:
// SyntaxError: missing ) after argument list
// at Loader.moduleStrategy (node:internal/modules/esm/translators:147:18)
// at async link (node:internal/modules/esm/module_job:64:21)
//
// 2. Syntax error in an ES module imported from an ESM spec. This is exactly
// the same as #1: there is no way to tell which file actually has the syntax
// error.
//
// 3. Syntax error in a CommonJS module imported by an ES module:
// /path/to/commonjs_with_syntax_error.js:2
//
//
//
// SyntaxError: Unexpected end of input
// at Object.compileFunction (node:vm:355:18)
// at wrapSafe (node:internal/modules/cjs/loader:1038:15)
// at Module._compile (node:internal/modules/cjs/loader:1072:27)
// at Object.Module._extensions..js (node:internal/modules/cjs/loader:1137:10)
// at Module.load (node:internal/modules/cjs/loader:988:32)
// at Function.Module._load (node:internal/modules/cjs/loader:828:14)
// at ModuleWrap.<anonymous> (node:internal/modules/esm/translators:201:29)
// at ModuleJob.run (node:internal/modules/esm/module_job:175:25)
// at async Loader.import (node:internal/modules/esm/loader:178:24)
// at async file:///path/to/esm_that_imported_cjs.mjs:2:11
//
// Note: For Jasmine's purposes, case 3 only occurs in Node >= 14.8. Older
// versions don't support top-level await, without which it's not possible to
// load a CommonJS module from an ES module at load-time. The entire content
// above, including the file path and the three blank lines, is part of the
// error's `stack` property. There may or may not be any stack trace after the
// SyntaxError line, and if there's a stack trace it may or may not contain
// any useful information.
//
// 4. Any other kind of exception thrown at load time
//
// Error: nope
// at Object.<anonymous> (/path/to/file_throwing_error.js:1:7)
// at Module._compile (node:internal/modules/cjs/loader:1108:14)
// at Object.Module._extensions..js (node:internal/modules/cjs/loader:1137:10)
// at Module.load (node:internal/modules/cjs/loader:988:32)
// at Function.Module._load (node:internal/modules/cjs/loader:828:14)
// at ModuleWrap.<anonymous> (node:internal/modules/esm/translators:201:29)
// at ModuleJob.run (node:internal/modules/esm/module_job:175:25)
// at async Loader.import (node:internal/modules/esm/loader:178:24)
// at async file:///path_to_file_importing_broken_file.mjs:1:1
//
// We need to replace the error with a useful one in cases 1 and 2, but not in
// cases 3 and 4. Distinguishing among them can be tricky. Simple heuristics
// like checking the stack trace for the name of the file we imported fail
// because it often shows up even when the error was elsewhere, e.g. at the
// bottom of the stack traces in the examples for cases 3 and 4 above. To add
// to the fun, file paths in errors on Windows can be either Windows style
// paths (c:\path\to\file.js) or URLs (file:///c:/path/to/file.js).
if (!(e instanceof SyntaxError)) {
return e;
}
const escapedWin = escapeStringForRegexp(importedPath.replace(/\//g, '\\'));
const windowsPathRegex = new RegExp('[a-zA-z]:\\\\([^\\s]+\\\\|)' + escapedWin);
const windowsUrlRegex = new RegExp('file:///[a-zA-z]:\\\\([^\\s]+\\\\|)' + escapedWin);
const anyUnixPathFirstLineRegex = /^\/[^\s:]+:\d/;
const anyWindowsPathFirstLineRegex = /^[a-zA-Z]:(\\[^\s\\:]+)+:/;
if (e.message.indexOf(importedPath) !== -1
|| e.stack.indexOf(importedPath) !== -1
|| e.stack.match(windowsPathRegex) || e.stack.match(windowsUrlRegex)
|| e.stack.match(anyUnixPathFirstLineRegex)
|| e.stack.match(anyWindowsPathFirstLineRegex)) {
return e;
} else {
return new Error(
`While loading ${importedPath}: ${e.constructor.name}: ${e.message}`,
{cause: e}
);
}
}
// Adapted from Sindre Sorhus's escape-string-regexp (MIT license)
function escapeStringForRegexp(string) {
// Escape characters with special meaning either inside or outside character sets.
// Use a simple backslash escape when it’s always valid, and a `\xnn` escape when the simpler form would be disallowed by Unicode patterns’ stricter grammar.
return string
.replace(/[|\\{}()[\]^$+*?.]/g, '\\$&')
.replace(/-/g, '\\x2d');
}
module.exports = Loader;