diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index dc4f034ab..3c824b554 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -54,7 +54,6 @@ jobs: flavor: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13] include: # Node 12.15 - # TODO Add comments about why we test 12.15; I think git blame says it's because of an ESM behavioral change that happened at 12.16 - flavor: 1 node: 12.15 nodeFlag: 12_15 @@ -122,10 +121,10 @@ jobs: typescript: next typescriptFlag: next downgradeNpm: true - # Node 17 + # Node 18 - flavor: 12 - node: 17 - nodeFlag: 17 + node: 18 + nodeFlag: 18 typescript: latest typescriptFlag: latest downgradeNpm: true diff --git a/.gitignore b/.gitignore index 5b0df0a80..388cf2460 100644 --- a/.gitignore +++ b/.gitignore @@ -1,14 +1,15 @@ /node_modules/ /tests/node_modules/ -.nyc_output/ -coverage/ +/.nyc_output/ +/coverage/ .DS_Store npm-debug.log /dist/ -tsconfig.schema.json -tsconfig.schemastore-schema.json -.idea/ -/.vscode/ +/tsconfig.schema.json +/tsconfig.schemastore-schema.json +/.idea/ +/.vscode/* +!/.vscode/launch.json /website/static/api /tsconfig.tsbuildinfo /temp diff --git a/.vscode/launch.json b/.vscode/launch.json index e9111194b..6a7bf35c8 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -12,6 +12,16 @@ "/**/*.js" ], }, + { + "name": "Debug resolver tests (example)", + "type": "pwa-node", + "request": "launch", + "cwd": "${workspaceFolder}/tests/tmp/resolver-0015-preferSrc-typeModule-allowJs-experimentalSpecifierResolutionNode", + "runtimeArgs": [ + "--loader", "../../../esm.mjs" + ], + "program": "./src/entrypoint-0054-src-to-src.mjs" + }, { "name": "Debug Example: running a test fixture against local ts-node/esm loader", "type": "pwa-node", @@ -24,5 +34,5 @@ "/**/*.js" ], } - ], + ] } diff --git a/NOTES.md b/NOTES.md new file mode 100644 index 000000000..0f0cbf15d --- /dev/null +++ b/NOTES.md @@ -0,0 +1,61 @@ +*Delete this file before merging this PR* + +## PnP interop + +Asked about it here: +https://discord.com/channels/226791405589233664/654372321225605128/957301175609344070 + +PnP API checks if import specifiers are for dependencies: non-relative, non-absolute + libfoo + @scope/libfoo + +When they are, it does `resolveToUnqualified` to map to an unqualified path. +This path points to the module's location on disk (in a zip, perhaps) but does +not handle file extension resolution or stuff like that. + +To interop with PnP, we need PnP to *only* `resolveToUnqualified`. +We do everything else. + +```typescript +import { Module } from 'module'; +import fs from 'fs'; + +const pathRegExp = /^(?![a-zA-Z]:[\\/]|\\\\|\.{0,2}(?:\/|$))((?:@[^/]+\/)?[^/]+)\/*(.*|)$/; + +const originalModuleResolveFilename = Module._resolveFilename; +Module._resolveFilename = function ( + request: string, + parent: typeof Module | null | undefined, + isMain: boolean, + options?: { [key: string]: any } +) { + const dependencyNameMatch = request.match(pathRegExp); + if (dependencyNameMatch !== null) { + + const [, dependencyName, subPath] = dependencyNameMatch; + + const unqualified = pnpapi.resolveToUnqualified(....); + + // Do your modified resolution on the unqualified path here + + } else { + + // Do your modified resolution here; no need for PnP + + } + +}; +``` + +PnP can be installed at runtime. + +To conditionally check if PnP is available at the start of *every* resolution: + +```typescript +// Get the pnpapi of either the issuer or the specifier. +// The latter is required when the specifier is an absolute path to a +// zip file and the issuer doesn't belong to a pnpapi +const {findPnPApi} = Module; +const pnpapi = findPnPApi ? (findPnpApi(issuer) ?? (url ? findPnpApi(specifier) : null)) : null; +if (pnpapi) {...} +``` diff --git a/TODO.md b/TODO.md new file mode 100644 index 000000000..69f07097d --- /dev/null +++ b/TODO.md @@ -0,0 +1,14 @@ +*Delete this file before merging this PR* + +## TODOs + +Copy any relevant changes from `add-cjs-loader-resolve` + +I forgot exactly where I was in `add-cjs-loader-resolve` +Re-do the renaming and moving that I did in that branch. +Then diff to see that I did it correctly. +Avoid introducing any accidental behavioral changes. + +Make list of changes from vanilla node in dist-raw/node-internal-modules-cjs-loader-old.js +Apply those changes to dist-raw/node-internal-modules-cjs-loader.js + diff --git a/ava.config.cjs b/ava.config.cjs index b3a5904b7..f6dc6951c 100644 --- a/ava.config.cjs +++ b/ava.config.cjs @@ -1,4 +1,5 @@ const expect = require('expect'); +const semver = require('semver'); const { createRequire } = require('module'); module.exports = { @@ -14,6 +15,9 @@ module.exports = { NODE_PATH: '' }, require: ['./src/test/remove-env-var-force-color.js'], + nodeArguments: semver.gte(process.version, '14.0.0') + ? ['--loader', './src/test/test-loader.mjs', '--no-warnings'] + : [], timeout: '300s', concurrency: 1, }; diff --git a/child-loader.mjs b/child-loader.mjs index 3a96eeea4..6ef592ac5 100644 --- a/child-loader.mjs +++ b/child-loader.mjs @@ -2,6 +2,7 @@ import { fileURLToPath } from 'url'; import { createRequire } from 'module'; const require = createRequire(fileURLToPath(import.meta.url)); +// TODO why use require() here? I think we can just `import` /** @type {import('./dist/child-loader')} */ const childLoader = require('./dist/child/child-loader'); export const { resolve, load, getFormat, transformSource } = childLoader; diff --git a/dist-raw/node-errors.js b/dist-raw/node-errors.js deleted file mode 100644 index e327734a8..000000000 --- a/dist-raw/node-errors.js +++ /dev/null @@ -1,32 +0,0 @@ -// TODO Sync this with node-internal-errors? - -exports.codes = { - ERR_INPUT_TYPE_NOT_ALLOWED: createErrorCtor(joinArgs('ERR_INPUT_TYPE_NOT_ALLOWED')), - ERR_INVALID_ARG_VALUE: createErrorCtor(joinArgs('ERR_INVALID_ARG_VALUE')), - ERR_INVALID_MODULE_SPECIFIER: createErrorCtor(joinArgs('ERR_INVALID_MODULE_SPECIFIER')), - ERR_INVALID_PACKAGE_CONFIG: createErrorCtor(joinArgs('ERR_INVALID_PACKAGE_CONFIG')), - ERR_INVALID_PACKAGE_TARGET: createErrorCtor(joinArgs('ERR_INVALID_PACKAGE_TARGET')), - ERR_MANIFEST_DEPENDENCY_MISSING: createErrorCtor(joinArgs('ERR_MANIFEST_DEPENDENCY_MISSING')), - ERR_MODULE_NOT_FOUND: createErrorCtor((path, base, type = 'package') => { - return `Cannot find ${type} '${path}' imported from ${base}` - }), - ERR_PACKAGE_IMPORT_NOT_DEFINED: createErrorCtor(joinArgs('ERR_PACKAGE_IMPORT_NOT_DEFINED')), - ERR_PACKAGE_PATH_NOT_EXPORTED: createErrorCtor(joinArgs('ERR_PACKAGE_PATH_NOT_EXPORTED')), - ERR_UNSUPPORTED_DIR_IMPORT: createErrorCtor(joinArgs('ERR_UNSUPPORTED_DIR_IMPORT')), - ERR_UNSUPPORTED_ESM_URL_SCHEME: createErrorCtor(joinArgs('ERR_UNSUPPORTED_ESM_URL_SCHEME')), - ERR_UNKNOWN_FILE_EXTENSION: createErrorCtor(joinArgs('ERR_UNKNOWN_FILE_EXTENSION')), -} - -function joinArgs(name) { - return (...args) => { - return [name, ...args].join(' ') - } -} - -function createErrorCtor(errorMessageCreator) { - return class CustomError extends Error { - constructor(...args) { - super(errorMessageCreator(...args)) - } - } -} diff --git a/dist-raw/node-internal-constants.js b/dist-raw/node-internal-constants.js new file mode 100644 index 000000000..b4aa7aadb --- /dev/null +++ b/dist-raw/node-internal-constants.js @@ -0,0 +1,4 @@ +// Copied from https://github.com/nodejs/node/blob/master/lib/internal/constants.js +module.exports = { + CHAR_FORWARD_SLASH: 47, /* / */ +}; diff --git a/dist-raw/node-internal-errors.js b/dist-raw/node-internal-errors.js index 244d32505..ddcd66178 100644 --- a/dist-raw/node-internal-errors.js +++ b/dist-raw/node-internal-errors.js @@ -2,9 +2,37 @@ const path = require('path'); -module.exports = { - createErrRequireEsm -}; +exports.codes = { + ERR_INPUT_TYPE_NOT_ALLOWED: createErrorCtor(joinArgs('ERR_INPUT_TYPE_NOT_ALLOWED')), + ERR_INVALID_ARG_VALUE: createErrorCtor(joinArgs('ERR_INVALID_ARG_VALUE')), + ERR_INVALID_MODULE_SPECIFIER: createErrorCtor(joinArgs('ERR_INVALID_MODULE_SPECIFIER')), + ERR_INVALID_PACKAGE_CONFIG: createErrorCtor(joinArgs('ERR_INVALID_PACKAGE_CONFIG')), + ERR_INVALID_PACKAGE_TARGET: createErrorCtor(joinArgs('ERR_INVALID_PACKAGE_TARGET')), + ERR_MANIFEST_DEPENDENCY_MISSING: createErrorCtor(joinArgs('ERR_MANIFEST_DEPENDENCY_MISSING')), + ERR_MODULE_NOT_FOUND: createErrorCtor((path, base, type = 'package') => { + return `Cannot find ${type} '${path}' imported from ${base}` + }), + ERR_PACKAGE_IMPORT_NOT_DEFINED: createErrorCtor(joinArgs('ERR_PACKAGE_IMPORT_NOT_DEFINED')), + ERR_PACKAGE_PATH_NOT_EXPORTED: createErrorCtor(joinArgs('ERR_PACKAGE_PATH_NOT_EXPORTED')), + ERR_UNSUPPORTED_DIR_IMPORT: createErrorCtor(joinArgs('ERR_UNSUPPORTED_DIR_IMPORT')), + ERR_UNSUPPORTED_ESM_URL_SCHEME: createErrorCtor(joinArgs('ERR_UNSUPPORTED_ESM_URL_SCHEME')), + ERR_UNKNOWN_FILE_EXTENSION: createErrorCtor(joinArgs('ERR_UNKNOWN_FILE_EXTENSION')), +} + +function joinArgs(name) { + return (...args) => { + return [name, ...args].join(' ') + } +} + +function createErrorCtor(errorMessageCreator) { + return class CustomError extends Error { + constructor(...args) { + super(errorMessageCreator(...args)) + } + } +} +exports.createErrRequireEsm = createErrRequireEsm; // Native ERR_REQUIRE_ESM Error is declared here: // https://github.com/nodejs/node/blob/2d5d77306f6dff9110c1f77fefab25f973415770/lib/internal/errors.js#L1294-L1313 diff --git a/dist-raw/node-internal-modules-cjs-helpers.d.ts b/dist-raw/node-internal-modules-cjs-helpers.d.ts deleted file mode 100644 index a57c2f831..000000000 --- a/dist-raw/node-internal-modules-cjs-helpers.d.ts +++ /dev/null @@ -1 +0,0 @@ -export function addBuiltinLibsToObject(object: any): void; diff --git a/dist-raw/node-internal-modules-cjs-helpers.js b/dist-raw/node-internal-modules-cjs-helpers.js index 20bfb44f8..bd4f70204 100644 --- a/dist-raw/node-internal-modules-cjs-helpers.js +++ b/dist-raw/node-internal-modules-cjs-helpers.js @@ -1,11 +1,45 @@ -const {ArrayPrototypeForEach, StringPrototypeStartsWith, ObjectPrototypeHasOwnProperty, StringPrototypeIncludes, ObjectDefineProperty} = require('./node-primordials'); +// Copied from https://github.com/nodejs/node/blob/v17.0.1/lib/internal/modules/cjs/helpers.js -exports.addBuiltinLibsToObject = addBuiltinLibsToObject; +'use strict'; + +const { + ArrayPrototypeForEach, + ObjectDefineProperty, + ObjectPrototypeHasOwnProperty, + SafeSet, + StringPrototypeIncludes, + StringPrototypeStartsWith, +} = require('./node-primordials'); + +const { getOptionValue } = require('./node-options'); +const userConditions = getOptionValue('--conditions'); + +const noAddons = getOptionValue('--no-addons'); +const addonConditions = noAddons ? [] : ['node-addons']; + +// TODO: Use this set when resolving pkg#exports conditions in loader.js. +const cjsConditions = new SafeSet([ + 'require', + 'node', + ...addonConditions, + ...userConditions, +]); -// Copied from https://github.com/nodejs/node/blob/21f5a56914a3b24ad77535ef369b93c6b1c11d18/lib/internal/modules/cjs/helpers.js#L133-L178 -function addBuiltinLibsToObject(object) { +/** + * @param {any} object + * @param {string} [dummyModuleName] + * @return {void} + */ +function addBuiltinLibsToObject(object, dummyModuleName) { // Make built-in modules available directly (loaded lazily). - const { builtinModules } = require('module').Module; + const Module = require('module').Module; + const { builtinModules } = Module; + + // To require built-in modules in user-land and ignore modules whose + // `canBeRequiredByUsers` is false. So we create a dummy module object and not + // use `require()` directly. + const dummyModule = new Module(dummyModuleName); + ArrayPrototypeForEach(builtinModules, (name) => { // Neither add underscored modules, nor ones that contain slashes (e.g., // 'fs/promises') or ones that are already defined. @@ -29,7 +63,8 @@ function addBuiltinLibsToObject(object) { ObjectDefineProperty(object, name, { get: () => { - const lib = require(name); + // Node 12 hack; remove when we drop node12 support + const lib = (dummyModule.require || require)(name); // Disable the current getter/setter and set up a new // non-enumerable property. @@ -49,3 +84,6 @@ function addBuiltinLibsToObject(object) { }); }); } + +exports.addBuiltinLibsToObject = addBuiltinLibsToObject; +exports.cjsConditions = cjsConditions; diff --git a/dist-raw/node-internal-modules-cjs-loader.js b/dist-raw/node-internal-modules-cjs-loader.js index e51a0eceb..ae17939d6 100644 --- a/dist-raw/node-internal-modules-cjs-loader.js +++ b/dist-raw/node-internal-modules-cjs-loader.js @@ -2,26 +2,86 @@ // https://github.com/nodejs/node/blob/2d5d77306f6dff9110c1f77fefab25f973415770/lib/internal/modules/cjs/loader.js // Each function and variable below must have a comment linking to the source in node's github repo. +'use strict'; + const { + ArrayPrototypeJoin, JSONParse, + ObjectKeys, + RegExpPrototypeTest, SafeMap, + SafeWeakMap, + StringPrototypeCharCodeAt, StringPrototypeEndsWith, StringPrototypeLastIndexOf, StringPrototypeIndexOf, + StringPrototypeMatch, StringPrototypeSlice, + StringPrototypeStartsWith, } = require('./node-primordials'); +const { NativeModule } = require('./node-nativemodule'); +const { pathToFileURL, fileURLToPath } = require('url'); +const fs = require('fs'); const path = require('path'); const { sep } = path; +const { internalModuleStat } = require('./node-internalBinding-fs'); const packageJsonReader = require('./node-internal-modules-package_json_reader'); +const { + cjsConditions, +} = require('./node-internal-modules-cjs-helpers'); +const { getOptionValue } = require('./node-options'); +const preserveSymlinks = getOptionValue('--preserve-symlinks'); +const preserveSymlinksMain = getOptionValue('--preserve-symlinks-main'); const {normalizeSlashes} = require('../dist/util'); const {createErrRequireEsm} = require('./node-internal-errors'); +const { + codes: { + ERR_INVALID_MODULE_SPECIFIER, + }, +} = require('./node-internal-errors'); + +const { + CHAR_FORWARD_SLASH, +} = require('./node-internal-constants'); + +const Module = require('module'); + +let statCache = null; +function stat(filename) { + filename = path.toNamespacedPath(filename); + if (statCache !== null) { + const result = statCache.get(filename); + if (result !== undefined) return result; + } + const result = internalModuleStat(filename); + if (statCache !== null && result >= 0) { + // Only set cache when `internalModuleStat(filename)` succeeds. + statCache.set(filename, result); + } + return result; +} + +// Note: +// we cannot get access to node's internal cache, which is populated from +// within node's Module constructor. So the cache here will always be empty. +// It's possible we could approximate our own cache by building it up with +// hacky workarounds, but it's not worth the complexity and flakiness. +const moduleParentCache = new SafeWeakMap(); +// Given a module name, and a list of paths to test, returns the first +// matching file in the following precedence. +// +// require("a.") +// -> a. +// +// require("a") +// -> a +// -> a. +// -> a/index. -// Copied from https://github.com/nodejs/node/blob/2d5d77306f6dff9110c1f77fefab25f973415770/lib/internal/modules/cjs/loader.js#L249 const packageJsonCache = new SafeMap(); -// Copied from https://github.com/nodejs/node/blob/v15.3.0/lib/internal/modules/cjs/loader.js#L275-L304 function readPackage(requestPath) { const jsonPath = path.resolve(requestPath, 'package.json'); @@ -53,25 +113,446 @@ function readPackage(requestPath) { } } -// Copied from https://github.com/nodejs/node/blob/2d5d77306f6dff9110c1f77fefab25f973415770/lib/internal/modules/cjs/loader.js#L285-L301 function readPackageScope(checkPath) { const rootSeparatorIndex = StringPrototypeIndexOf(checkPath, sep); let separatorIndex; - while ( - (separatorIndex = StringPrototypeLastIndexOf(checkPath, sep)) > rootSeparatorIndex - ) { + do { + separatorIndex = StringPrototypeLastIndexOf(checkPath, sep); checkPath = StringPrototypeSlice(checkPath, 0, separatorIndex); if (StringPrototypeEndsWith(checkPath, sep + 'node_modules')) return false; - const pjson = readPackage(checkPath); + const pjson = readPackage(checkPath + sep); if (pjson) return { + data: pjson, path: checkPath, - data: pjson }; + } while (separatorIndex > rootSeparatorIndex); + return false; +} + +/** + * @param {{ + * nodeEsmResolver: ReturnType, + * compiledExtensions: string[], + * preferTsExts + * }} opts + */ +function createCjsLoader(opts) { + const {nodeEsmResolver, compiledExtensions, preferTsExts} = opts; +const { + encodedSepRegEx, + packageExportsResolve, + packageImportsResolve +} = nodeEsmResolver; + +function tryPackage(requestPath, exts, isMain, originalPath) { + // const pkg = readPackage(requestPath)?.main; + const tmp = readPackage(requestPath) + const pkg = tmp != null ? tmp.main : undefined; + + if (!pkg) { + return tryExtensions(path.resolve(requestPath, 'index'), exts, isMain); + } + + const filename = path.resolve(requestPath, pkg); + let actual = tryReplacementExtensions(filename, isMain) || + tryFile(filename, isMain) || + tryExtensions(filename, exts, isMain) || + tryExtensions(path.resolve(filename, 'index'), exts, isMain); + if (actual === false) { + actual = tryExtensions(path.resolve(requestPath, 'index'), exts, isMain); + if (!actual) { + // eslint-disable-next-line no-restricted-syntax + const err = new Error( + `Cannot find module '${filename}'. ` + + 'Please verify that the package.json has a valid "main" entry' + ); + err.code = 'MODULE_NOT_FOUND'; + err.path = path.resolve(requestPath, 'package.json'); + err.requestPath = originalPath; + // TODO(BridgeAR): Add the requireStack as well. + throw err; + } else { + const jsonPath = path.resolve(requestPath, 'package.json'); + process.emitWarning( + `Invalid 'main' field in '${jsonPath}' of '${pkg}'. ` + + 'Please either fix that or report it to the module author', + 'DeprecationWarning', + 'DEP0128' + ); + } + } + return actual; +} + +// In order to minimize unnecessary lstat() calls, +// this cache is a list of known-real paths. +// Set to an empty Map to reset. +const realpathCache = new SafeMap(); + +// Check if the file exists and is not a directory +// if using --preserve-symlinks and isMain is false, +// keep symlinks intact, otherwise resolve to the +// absolute realpath. +function tryFile(requestPath, isMain) { + const rc = stat(requestPath); + if (rc !== 0) return; + if (preserveSymlinks && !isMain) { + return path.resolve(requestPath); + } + return toRealPath(requestPath); +} + +function toRealPath(requestPath) { + return fs.realpathSync(requestPath, { + // [internalFS.realpathCacheKey]: realpathCache + }); +} + +/** + * TS's resolver can resolve foo.js to foo.ts, by replacing .js extension with several source extensions. + * IMPORTANT: preserve ordering according to preferTsExts; this affects resolution behavior! + */ +const extensions = Array.from(new Set([ + ...(preferTsExts ? compiledExtensions : []), + '.js', '.json', '.node', '.mjs', '.cjs', + ...compiledExtensions +])); +const replacementExtensions = { + '.js': extensions.filter(ext => ['.js', '.jsx', '.ts', '.tsx'].includes(ext)), + '.cjs': extensions.filter(ext => ['.cjs', '.cts'].includes(ext)), + '.mjs': extensions.filter(ext => ['.mjs', '.mts'].includes(ext)), +}; + +const replacableExtensionRe = /(\.(?:js|cjs|mjs))$/; + +function statReplacementExtensions(p) { + const match = p.match(replacableExtensionRe); + if (match) { + const replacementExts = replacementExtensions[match[1]]; + const pathnameWithoutExtension = p.slice(0, -match[1].length); + for (let i = 0; i < replacementExts.length; i++) { + const filename = pathnameWithoutExtension + replacementExts[i]; + const rc = stat(filename); + if (rc === 0) { + return [rc, filename]; + } + } + } + return [stat(p), p]; +} +function tryReplacementExtensions(p, isMain) { + const match = p.match(replacableExtensionRe); + if (match) { + const replacementExts = replacementExtensions[match[1]]; + const pathnameWithoutExtension = p.slice(0, -match[1].length); + for (let i = 0; i < replacementExts.length; i++) { + const filename = tryFile(pathnameWithoutExtension + replacementExts[i], isMain); + if (filename) { + return filename; + } + } } return false; } +// Given a path, check if the file exists with any of the set extensions +function tryExtensions(p, exts, isMain) { + for (let i = 0; i < exts.length; i++) { + const filename = tryFile(p + exts[i], isMain); + + if (filename) { + return filename; + } + } + return false; +} + +function trySelfParentPath(parent) { + if (!parent) return false; + + if (parent.filename) { + return parent.filename; + } else if (parent.id === '' || parent.id === 'internal/preload') { + try { + return process.cwd() + path.sep; + } catch { + return false; + } + } +} + +function trySelf(parentPath, request) { + if (!parentPath) return false; + + const { data: pkg, path: pkgPath } = readPackageScope(parentPath) || {}; + if (!pkg || pkg.exports === undefined) return false; + if (typeof pkg.name !== 'string') return false; + + let expansion; + if (request === pkg.name) { + expansion = '.'; + } else if (StringPrototypeStartsWith(request, `${pkg.name}/`)) { + expansion = '.' + StringPrototypeSlice(request, pkg.name.length); + } else { + return false; + } + + try { + return finalizeEsmResolution(packageExportsResolve( + pathToFileURL(pkgPath + '/package.json'), expansion, pkg, + pathToFileURL(parentPath), cjsConditions).resolved, parentPath, pkgPath); + } catch (e) { + if (e.code === 'ERR_MODULE_NOT_FOUND') + throw createEsmNotFoundErr(request, pkgPath + '/package.json'); + throw e; + } +} + +// This only applies to requests of a specific form: +// 1. name/.* +// 2. @scope/name/.* +const EXPORTS_PATTERN = /^((?:@[^/\\%]+\/)?[^./\\%][^/\\%]*)(\/.*)?$/; +function resolveExports(nmPath, request) { + // The implementation's behavior is meant to mirror resolution in ESM. + const { 1: name, 2: expansion = '' } = + StringPrototypeMatch(request, EXPORTS_PATTERN) || []; + if (!name) + return; + const pkgPath = path.resolve(nmPath, name); + const pkg = readPackage(pkgPath); + // if (pkg?.exports != null) { + if (pkg != null && pkg.exports != null) { + try { + return finalizeEsmResolution(packageExportsResolve( + pathToFileURL(pkgPath + '/package.json'), '.' + expansion, pkg, null, + cjsConditions).resolved, null, pkgPath); + } catch (e) { + if (e.code === 'ERR_MODULE_NOT_FOUND') + throw createEsmNotFoundErr(request, pkgPath + '/package.json'); + throw e; + } + } +} + +// Backwards compat for old node versions +const hasModulePathCache = !!require('module')._pathCache; +const Module_pathCache = Object.create(null); +const Module_pathCache_get = hasModulePathCache ? (cacheKey) => Module._pathCache[cacheKey] : (cacheKey) => Module_pathCache[cacheKey]; +const Module_pathCache_set = hasModulePathCache ? (cacheKey, value) => (Module._pathCache[cacheKey] = value) : (cacheKey) => (Module_pathCache[cacheKey] = value); + +const trailingSlashRegex = /(?:^|\/)\.?\.$/; +const Module_findPath = function _findPath(request, paths, isMain) { + const absoluteRequest = path.isAbsolute(request); + if (absoluteRequest) { + paths = ['']; + } else if (!paths || paths.length === 0) { + return false; + } + + const cacheKey = request + '\x00' + ArrayPrototypeJoin(paths, '\x00'); + const entry = Module_pathCache_get(cacheKey); + if (entry) + return entry; + + let exts; + let trailingSlash = request.length > 0 && + StringPrototypeCharCodeAt(request, request.length - 1) === + CHAR_FORWARD_SLASH; + if (!trailingSlash) { + trailingSlash = RegExpPrototypeTest(trailingSlashRegex, request); + } + + // For each path + for (let i = 0; i < paths.length; i++) { + // Don't search further if path doesn't exist + const curPath = paths[i]; + if (curPath && stat(curPath) < 1) continue; + + if (!absoluteRequest) { + const exportsResolved = resolveExports(curPath, request); + if (exportsResolved) + return exportsResolved; + } + + const _basePath = path.resolve(curPath, request); + let filename; + + const [rc, basePath] = statReplacementExtensions(_basePath); + if (!trailingSlash) { + if (rc === 0) { // File. + if (!isMain) { + if (preserveSymlinks) { + filename = path.resolve(basePath); + } else { + filename = toRealPath(basePath); + } + } else if (preserveSymlinksMain) { + // For the main module, we use the preserveSymlinksMain flag instead + // mainly for backward compatibility, as the preserveSymlinks flag + // historically has not applied to the main module. Most likely this + // was intended to keep .bin/ binaries working, as following those + // symlinks is usually required for the imports in the corresponding + // files to resolve; that said, in some use cases following symlinks + // causes bigger problems which is why the preserveSymlinksMain option + // is needed. + filename = path.resolve(basePath); + } else { + filename = toRealPath(basePath); + } + } + + if (!filename) { + // Try it with each of the extensions + if (exts === undefined) + exts = ObjectKeys(Module._extensions); + filename = tryExtensions(basePath, exts, isMain); + } + } + + if (!filename && rc === 1) { // Directory. + // try it with each of the extensions at "index" + if (exts === undefined) + exts = ObjectKeys(Module._extensions); + filename = tryPackage(basePath, exts, isMain, request); + } + + if (filename) { + Module_pathCache_set(cacheKey, filename); + return filename; + } + } + + return false; +}; + +const Module_resolveFilename = function _resolveFilename(request, parent, isMain, options) { + if (StringPrototypeStartsWith(request, 'node:') || + NativeModule.canBeRequiredByUsers(request)) { + return request; + } + + let paths; + + if (typeof options === 'object' && options !== null) { + if (ArrayIsArray(options.paths)) { + const isRelative = StringPrototypeStartsWith(request, './') || + StringPrototypeStartsWith(request, '../') || + ((isWindows && StringPrototypeStartsWith(request, '.\\')) || + StringPrototypeStartsWith(request, '..\\')); + + if (isRelative) { + paths = options.paths; + } else { + const fakeParent = new Module('', null); + + paths = []; + + for (let i = 0; i < options.paths.length; i++) { + const path = options.paths[i]; + fakeParent.paths = Module._nodeModulePaths(path); + const lookupPaths = Module._resolveLookupPaths(request, fakeParent); + + for (let j = 0; j < lookupPaths.length; j++) { + if (!ArrayPrototypeIncludes(paths, lookupPaths[j])) + ArrayPrototypePush(paths, lookupPaths[j]); + } + } + } + } else if (options.paths === undefined) { + paths = Module._resolveLookupPaths(request, parent); + } else { + throw new ERR_INVALID_ARG_VALUE('options.paths', options.paths); + } + } else { + paths = Module._resolveLookupPaths(request, parent); + } + + // if (parent?.filename) { + // node 12 hack + if (parent != null && parent.filename) { + if (request[0] === '#') { + const pkg = readPackageScope(parent.filename) || {}; + + // if (pkg.data?.imports != null) { + // node 12 hack + if (pkg.data != null && pkg.data.imports != null) { + try { + return finalizeEsmResolution( + packageImportsResolve(request, pathToFileURL(parent.filename), + cjsConditions), parent.filename, + pkg.path); + } catch (e) { + if (e.code === 'ERR_MODULE_NOT_FOUND') + throw createEsmNotFoundErr(request); + throw e; + } + } + } + } + + // Try module self resolution first + const parentPath = trySelfParentPath(parent); + const selfResolved = trySelf(parentPath, request); + if (selfResolved) { + const cacheKey = request + '\x00' + + (paths.length === 1 ? paths[0] : ArrayPrototypeJoin(paths, '\x00')); + Module._pathCache[cacheKey] = selfResolved; + return selfResolved; + } + + // Look up the filename first, since that's the cache key. + const filename = Module._findPath(request, paths, isMain, false); + if (filename) return filename; + const requireStack = []; + for (let cursor = parent; + cursor; + cursor = moduleParentCache.get(cursor)) { + ArrayPrototypePush(requireStack, cursor.filename || cursor.id); + } + let message = `Cannot find module '${request}'`; + if (requireStack.length > 0) { + message = message + '\nRequire stack:\n- ' + + ArrayPrototypeJoin(requireStack, '\n- '); + } + // eslint-disable-next-line no-restricted-syntax + const err = new Error(message); + err.code = 'MODULE_NOT_FOUND'; + err.requireStack = requireStack; + throw err; +}; + +function finalizeEsmResolution(resolved, parentPath, pkgPath) { + if (RegExpPrototypeTest(encodedSepRegEx, resolved)) + throw new ERR_INVALID_MODULE_SPECIFIER( + resolved, 'must not include encoded "/" or "\\" characters', parentPath); + const filename = fileURLToPath(resolved); + const actual = tryReplacementExtensions(filename) || tryFile(filename); + if (actual) + return actual; + const err = createEsmNotFoundErr(filename, + path.resolve(pkgPath, 'package.json')); + throw err; +} + +function createEsmNotFoundErr(request, path) { + // eslint-disable-next-line no-restricted-syntax + const err = new Error(`Cannot find module '${request}'`); + err.code = 'MODULE_NOT_FOUND'; + if (path) + err.path = path; + // TODO(BridgeAR): Add the requireStack as well. + return err; +} + + +return { + Module_findPath, + Module_resolveFilename +} + +} + /** * copied from Module._extensions['.js'] * https://github.com/nodejs/node/blob/v15.3.0/lib/internal/modules/cjs/loader.js#L1113-L1120 @@ -94,6 +575,8 @@ function assertScriptCanLoadAsCJSImpl(service, module, filename) { } } + module.exports = { + createCjsLoader, assertScriptCanLoadAsCJSImpl }; diff --git a/dist-raw/node-internal-modules-esm-get_format.js b/dist-raw/node-internal-modules-esm-get_format.js index 02bb65670..1f3586adf 100644 --- a/dist-raw/node-internal-modules-esm-get_format.js +++ b/dist-raw/node-internal-modules-esm-get_format.js @@ -13,12 +13,9 @@ const experimentalJsonModules = nodeMajor > 17 || (nodeMajor === 17 && nodeMinor >= 5) || getOptionValue('--experimental-json-modules'); -const experimentalSpeciferResolution = - getOptionValue('--experimental-specifier-resolution'); const experimentalWasmModules = getOptionValue('--experimental-wasm-modules'); -const { getPackageType } = require('./node-internal-modules-esm-resolve').createResolve({tsExtensions: [], jsExtensions: []}); const { URL, fileURLToPath } = require('url'); -const { ERR_UNKNOWN_FILE_EXTENSION } = require('./node-errors').codes; +const { ERR_UNKNOWN_FILE_EXTENSION } = require('./node-internal-errors').codes; const extensionFormatMap = { '__proto__': null, @@ -42,6 +39,24 @@ if (experimentalWasmModules) if (experimentalJsonModules) extensionFormatMap['.json'] = legacyExtensionFormatMap['.json'] = 'json'; +/** + * + * @param {'node' | 'explicit'} [tsNodeExperimentalSpecifierResolution] + * @param {ReturnType< + * typeof import('../dist-raw/node-internal-modules-esm-resolve').createResolve + * >} nodeEsmResolver + */ +function createGetFormat(tsNodeExperimentalSpecifierResolution, nodeEsmResolver) { +// const experimentalSpeciferResolution = tsNodeExperimentalSpecifierResolution ?? getOptionValue('--experimental-specifier-resolution'); +let experimentalSpeciferResolution = tsNodeExperimentalSpecifierResolution != null ? tsNodeExperimentalSpecifierResolution : getOptionValue('--experimental-specifier-resolution'); +const { getPackageType } = nodeEsmResolver; + +/** + * @param {string} url + * @param {{}} context + * @param {any} defaultGetFormatUnused + * @returns {ReturnType} + */ function defaultGetFormat(url, context, defaultGetFormatUnused) { if (StringPrototypeStartsWith(url, 'node:')) { return { format: 'builtin' }; @@ -81,4 +96,10 @@ function defaultGetFormat(url, context, defaultGetFormatUnused) { } return { format: null }; } -exports.defaultGetFormat = defaultGetFormat; + +return {defaultGetFormat}; +} + +module.exports = { + createGetFormat +}; diff --git a/dist-raw/node-internal-modules-esm-resolve.js b/dist-raw/node-internal-modules-esm-resolve.js index c5d5b38b9..b32f8aab1 100644 --- a/dist-raw/node-internal-modules-esm-resolve.js +++ b/dist-raw/node-internal-modules-esm-resolve.js @@ -1,4 +1,5 @@ // Copied from https://raw.githubusercontent.com/nodejs/node/v15.3.0/lib/internal/modules/esm/resolve.js + 'use strict'; const [nodeMajor, nodeMinor, nodePatch] = process.versions.node.split('.').map(s => parseInt(s, 10)) @@ -28,11 +29,9 @@ const { ObjectFreeze, ObjectGetOwnPropertyNames, ObjectPrototypeHasOwnProperty, - // RegExp, RegExpPrototypeTest, SafeMap, SafeSet, - // String, StringPrototypeEndsWith, StringPrototypeIndexOf, StringPrototypeLastIndexOf, @@ -44,13 +43,8 @@ const { } = require('./node-primordials'); // const internalFS = require('internal/fs/utils'); -// const { NativeModule } = require('internal/bootstrap/loaders'); -const Module = require('module') -const NativeModule = { - canBeRequiredByUsers(specifier) { - return Module.builtinModules.includes(specifier) - } -} +const Module = require('module'); +const { NativeModule } = require('./node-nativemodule'); const { realpathSync, statSync, @@ -83,7 +77,7 @@ const { ERR_UNSUPPORTED_DIR_IMPORT, ERR_UNSUPPORTED_ESM_URL_SCHEME, // } = require('internal/errors').codes; -} = require('./node-errors').codes; +} = require('./node-internal-errors').codes; // const { Module: CJSModule } = require('internal/modules/cjs/loader'); const CJSModule = Module; @@ -98,7 +92,9 @@ const pendingDeprecation = getOptionValue('--pending-deprecation'); function createResolve(opts) { // TODO receive cached fs implementations here -const {tsExtensions, jsExtensions, preferTsExts} = opts; +const {compiledExtensions, preferTsExts, tsNodeExperimentalSpecifierResolution} = opts; +// const experimentalSpecifierResolution = tsNodeExperimentalSpecifierResolution ?? getOptionValue('--experimental-specifier-resolution'); +const experimentalSpecifierResolution = tsNodeExperimentalSpecifierResolution != null ? tsNodeExperimentalSpecifierResolution : getOptionValue('--experimental-specifier-resolution'); const emittedPackageWarnings = new SafeSet(); function emitFolderMapDeprecation(match, pjsonUrl, isExports, base) { @@ -251,66 +247,52 @@ function legacyMainResolve(packageJSONUrl, packageConfig, base) { let guess; if (packageConfig.main !== undefined) { // Note: fs check redundances will be handled by Descriptor cache here. - if (fileExists(guess = new URL(`./${packageConfig.main}`, - packageJSONUrl))) { - return guess; - } - if (fileExists(guess = new URL(`./${packageConfig.main}.js`, - packageJSONUrl))) { + if(guess = resolveReplacementExtensions(new URL(`./${packageConfig.main}`, packageJSONUrl))) { return guess; } - if (fileExists(guess = new URL(`./${packageConfig.main}.json`, - packageJSONUrl))) { - return guess; - } - if (fileExists(guess = new URL(`./${packageConfig.main}.node`, - packageJSONUrl))) { - return guess; - } - if (fileExists(guess = new URL(`./${packageConfig.main}/index.js`, + if (fileExists(guess = new URL(`./${packageConfig.main}`, packageJSONUrl))) { return guess; } - if (fileExists(guess = new URL(`./${packageConfig.main}/index.json`, - packageJSONUrl))) { - return guess; + for(const extension of extensions) { + if (fileExists(guess = new URL(`./${packageConfig.main}${extension}`, + packageJSONUrl))) { + return guess; + } } - if (fileExists(guess = new URL(`./${packageConfig.main}/index.node`, - packageJSONUrl))) { - return guess; + for(const extension of extensions) { + if (fileExists(guess = new URL(`./${packageConfig.main}/index${extension}`, + packageJSONUrl))) { + return guess; + } } // Fallthrough. } - if (fileExists(guess = new URL('./index.js', packageJSONUrl))) { - return guess; - } - // So fs. - if (fileExists(guess = new URL('./index.json', packageJSONUrl))) { - return guess; - } - if (fileExists(guess = new URL('./index.node', packageJSONUrl))) { - return guess; + for(const extension of extensions) { + if (fileExists(guess = new URL(`./index${extension}`, packageJSONUrl))) { + return guess; + } } // Not found. throw new ERR_MODULE_NOT_FOUND( fileURLToPath(new URL('.', packageJSONUrl)), fileURLToPath(base)); } +/** attempts replacement extensions, then tries exact name, then attempts appending extensions */ function resolveExtensionsWithTryExactName(search) { - if (fileExists(search)) return search; const resolvedReplacementExtension = resolveReplacementExtensions(search); if(resolvedReplacementExtension) return resolvedReplacementExtension; + if (fileExists(search)) return search; return resolveExtensions(search); } const extensions = Array.from(new Set([ - ...(preferTsExts ? tsExtensions : []), - '.js', - ...jsExtensions, - '.json', '.node', '.mjs', - ...tsExtensions + ...(preferTsExts ? compiledExtensions : []), + '.js', '.json', '.node', '.mjs', + ...compiledExtensions ])); +// This appends missing extensions function resolveExtensions(search) { for (let i = 0; i < extensions.length; i++) { const extension = extensions[i]; @@ -324,14 +306,22 @@ function resolveExtensions(search) { * TS's resolver can resolve foo.js to foo.ts, by replacing .js extension with several source extensions. * IMPORTANT: preserve ordering according to preferTsExts; this affects resolution behavior! */ -const replacementExtensions = extensions.filter(ext => ['.js', '.jsx', '.ts', '.tsx'].includes(ext)); +const replacementExtensions = { + '.js': extensions.filter(ext => ['.js', '.jsx', '.ts', '.tsx'].includes(ext)), + '.cjs': extensions.filter(ext => ['.cjs', '.cts'].includes(ext)), + '.mjs': extensions.filter(ext => ['.mjs', '.mts'].includes(ext)), +}; +const replacableExtensionRe = /(\.(?:js|cjs|mjs))$/; +/** This replaces JS with TS extensions */ function resolveReplacementExtensions(search) { - if (search.pathname.match(/\.js$/)) { - const pathnameWithoutExtension = search.pathname.slice(0, search.pathname.length - 3); - for (let i = 0; i < replacementExtensions.length; i++) { - const extension = replacementExtensions[i]; - const guess = new URL(search.toString()); + const match = search.pathname.match(replacableExtensionRe); + if (match) { + const replacementExts = replacementExtensions[match[1]]; + const pathnameWithoutExtension = search.pathname.slice(0, -match[1].length); + const guess = new URL(search.toString()); + for (let i = 0; i < replacementExts.length; i++) { + const extension = replacementExts[i]; guess.pathname = `${pathnameWithoutExtension}${extension}`; if (fileExists(guess)) return guess; } @@ -350,7 +340,7 @@ function finalizeResolution(resolved, base) { resolved.pathname, 'must not include encoded "/" or "\\" characters', fileURLToPath(base)); - if (getOptionValue('--experimental-specifier-resolution') === 'node') { + if (experimentalSpecifierResolution === 'node') { const path = fileURLToPath(resolved); let file = resolveExtensionsWithTryExactName(resolved); if (file !== undefined) return file; @@ -563,7 +553,7 @@ function isConditionalExportsMainSugar(exports, packageJSONUrl, base) { * @param {object} packageConfig * @param {string} base * @param {Set} conditions - * @returns {URL} + * @returns {{resolved: URL, exact: boolean}} */ function packageExportsResolve( packageJSONUrl, packageSubpath, packageConfig, base, conditions) { diff --git a/dist-raw/node-internal-modules-package_json_reader.js b/dist-raw/node-internal-modules-package_json_reader.js index e9f82c6f4..9266bc1c4 100644 --- a/dist-raw/node-internal-modules-package_json_reader.js +++ b/dist-raw/node-internal-modules-package_json_reader.js @@ -2,7 +2,7 @@ 'use strict'; const { SafeMap } = require('./node-primordials'); -const { internalModuleReadJSON } = require('./node-internal-fs'); +const { internalModuleReadJSON } = require('./node-internalBinding-fs'); const { pathToFileURL } = require('url'); const { toNamespacedPath } = require('path'); // const { getOptionValue } = require('./node-options'); @@ -13,7 +13,7 @@ let manifest; /** * @param {string} jsonPath - * @return {[string, boolean]} + * @return {{string: string, containsKeys: boolean}} */ function read(jsonPath) { if (cache.has(jsonPath)) { diff --git a/dist-raw/node-internal-fs.js b/dist-raw/node-internalBinding-fs.js similarity index 53% rename from dist-raw/node-internal-fs.js rename to dist-raw/node-internalBinding-fs.js index d9a2528dd..20a42c36c 100644 --- a/dist-raw/node-internal-fs.js +++ b/dist-raw/node-internalBinding-fs.js @@ -2,6 +2,10 @@ const fs = require('fs'); // In node's core, this is implemented in C // https://github.com/nodejs/node/blob/v15.3.0/src/node_file.cc#L891-L985 +/** + * @param {string} path + * @returns {[] | [string, boolean]} + */ function internalModuleReadJSON(path) { let string try { @@ -17,6 +21,23 @@ function internalModuleReadJSON(path) { return [string, containsKeys] } +// In node's core, this is implemented in C +// https://github.com/nodejs/node/blob/63e7dc1e5c71b70c80ed9eda230991edb00811e2/src/node_file.cc#L987-L1005 +/** + * @param {string} path + * @returns {number} 0 = file, 1 = dir, negative = error + */ +function internalModuleStat(path) { + try { + const stat = fs.statSync(path); + if(stat.isFile()) return 0; + if(stat.isDirectory()) return 1; + } catch(e) { + return -e.errno || -1; + } +} + module.exports = { - internalModuleReadJSON + internalModuleReadJSON, + internalModuleStat }; diff --git a/dist-raw/node-nativemodule.js b/dist-raw/node-nativemodule.js new file mode 100644 index 000000000..ea54fcd49 --- /dev/null +++ b/dist-raw/node-nativemodule.js @@ -0,0 +1,9 @@ + +// Node imports this from 'internal/bootstrap/loaders' +const Module = require('module'); +const NativeModule = { + canBeRequiredByUsers(specifier) { + return Module.builtinModules.includes(specifier) + } +}; +exports.NativeModule = NativeModule; diff --git a/dist-raw/node-primordials.js b/dist-raw/node-primordials.js index 21d8cfd19..a7c1574af 100644 --- a/dist-raw/node-primordials.js +++ b/dist-raw/node-primordials.js @@ -20,6 +20,7 @@ module.exports = { RegExpPrototypeSymbolReplace: (obj, ...rest) => RegExp.prototype[Symbol.replace].apply(obj, rest), SafeMap: Map, SafeSet: Set, + SafeWeakMap: WeakMap, StringPrototypeEndsWith: (str, ...rest) => String.prototype.endsWith.apply(str, rest), StringPrototypeIncludes: (str, ...rest) => String.prototype.includes.apply(str, rest), StringPrototypeLastIndexOf: (str, ...rest) => String.prototype.lastIndexOf.apply(str, rest), @@ -30,5 +31,7 @@ module.exports = { StringPrototypeSplit: (str, ...rest) => String.prototype.split.apply(str, rest), StringPrototypeStartsWith: (str, ...rest) => String.prototype.startsWith.apply(str, rest), StringPrototypeSubstr: (str, ...rest) => String.prototype.substr.apply(str, rest), + StringPrototypeCharCodeAt: (str, ...rest) => String.prototype.charCodeAt.apply(str, rest), + StringPrototypeMatch: (str, ...rest) => String.prototype.match.apply(str, rest), SyntaxError: SyntaxError }; diff --git a/dprint.json b/dprint.json index afa5d0ea9..3fcf217d5 100644 --- a/dprint.json +++ b/dprint.json @@ -28,7 +28,8 @@ "tests/throw error react tsx.tsx", "tests/esm/throw error.ts", "tests/legacy-source-map-support-interop/index.ts", - "tests/main-realpath/symlink/symlink.tsx" + "tests/main-realpath/symlink/symlink.tsx", + "tests/tmp" ], "plugins": [ "https://plugins.dprint.dev/prettier-0.6.2.exe-plugin@36dd4f8b9710ab323c471017ecd00a20cf1dca2728c12242c7dabb8dfacad0e2" diff --git a/justfile b/justfile index 08b4612e6..e6b8b1a90 100644 --- a/justfile +++ b/justfile @@ -31,7 +31,7 @@ lint *ARGS: lint-fix *ARGS: dprint fmt "$@" clean *ARGS: - rimraf dist tsconfig.schema.json tsconfig.schemastore-schema.json tsconfig.tsbuildinfo tests/ts-node-packed.tgz "$@" + rimraf temp dist tsconfig.schema.json tsconfig.schemastore-schema.json tsconfig.tsbuildinfo tests/ts-node-packed.tgz tests/tmp "$@" rebuild *ARGS: just clean && just build "$@" build *ARGS: @@ -39,7 +39,7 @@ build *ARGS: build-nopack *ARGS: just build-tsc && just build-configSchema "$@" build-tsc *ARGS: - tsc "$@" + tsc -b ./tsconfig.build-dist.json "$@" build-configSchema *ARGS: typescript-json-schema --topRef --refs --validationKeywords allOf --out tsconfig.schema.json tsconfig.build-schema.json TsConfigSchema && node --require ./register ./scripts/create-merged-schema "$@" build-pack *ARGS: diff --git a/notes1.md b/notes1.md new file mode 100644 index 000000000..ee561f48d --- /dev/null +++ b/notes1.md @@ -0,0 +1,9 @@ +const patterns = getRegularExpressionsForWildcards(specs, basePath, usage); +const pattern = patterns.map(pattern => `(${pattern})`).join("|"); +// If excluding, match "foo/bar/baz...", but if including, only allow "foo". +const terminator = usage === "exclude" ? "($|/)" : "$"; +return `^(${pattern})${terminator}`; + + +const pattern = spec && getSubPatternFromSpec(spec, basePath, usage, wildcardMatchers[usage]); +return pattern && `^(${pattern})${usage === "exclude" ? "($|/)" : "$"}`; diff --git a/notes2.md b/notes2.md new file mode 100644 index 000000000..93e6bac65 --- /dev/null +++ b/notes2.md @@ -0,0 +1,32 @@ +When /dist and /src are understood to be overlaid because of src -> dist compiling +/dist/ +/src/ + +Loop over require.extensions +/src/foo.js +/src/foo.mjs +/src/foo.cjs +/src/foo.ts +/src/foo.mts +/src/foo.cts +/src/foo/index.js +/src/foo/index.mjs +/src/foo/index.ts +// Where do we check package.json main?? + + +/dist/foo.js +/dist/foo.ts + + +_resolveLookupPaths +_findPath +_resolveFilename + +_findPath calls resolveExports calls packageExportsResolve, which is in the ESM loader + +Is anything within packageExportsResolve hooked/modified by us? File extension swapping? + + +When resolver calls statSync('./dist/foo.js') and we intercept and discover './src/foo.ts' +How to redirect? We need to rewrite whatever local variable is storing `./dist/foo.js` diff --git a/package-lock.json b/package-lock.json index 8dba42678..61a6d0fea 100644 --- a/package-lock.json +++ b/package-lock.json @@ -878,6 +878,12 @@ "integrity": "sha512-f5j5b/Gf71L+dbqxIpQ4Z2WlmI/mPJ0fOkGGmFgtb6sAu97EPczzbS3/tJKxmcYDj55OX6ssqwDAWOHIYDRDGA==", "dev": true }, + "@types/prop-types": { + "version": "15.7.5", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz", + "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==", + "dev": true + }, "@types/proper-lockfile": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/@types/proper-lockfile/-/proper-lockfile-4.1.2.tgz", @@ -894,10 +900,15 @@ "dev": true }, "@types/react": { - "version": "16.0.31", - "resolved": "https://registry.npmjs.org/@types/react/-/react-16.0.31.tgz", - "integrity": "sha512-ft7OuDGUo39e+9LGwUewf2RyEaNBOjWbHUmD5bzjNuSuDabccE/1IuO7iR0dkzLjVUKxTMq69E+FmKfbgBcfbQ==", - "dev": true + "version": "16.14.26", + "resolved": "https://registry.npmjs.org/@types/react/-/react-16.14.26.tgz", + "integrity": "sha512-c/5CYyciOO4XdFcNhZW1O2woVx86k4T+DO2RorHZL7EhitkNQgSD/SgpdZJAUJa/qjVgOmTM44gHkAdZSXeQuQ==", + "dev": true, + "requires": { + "@types/prop-types": "*", + "@types/scheduler": "*", + "csstype": "^3.0.2" + } }, "@types/retry": { "version": "0.12.1", @@ -915,6 +926,12 @@ "@types/node": "*" } }, + "@types/scheduler": { + "version": "0.16.2", + "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz", + "integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==", + "dev": true + }, "@types/semver": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.1.0.tgz", @@ -1907,6 +1924,12 @@ "integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==", "dev": true }, + "csstype": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.11.tgz", + "integrity": "sha512-sa6P2wJ+CAbgyy4KFssIb/JNMLxFvKF1pCYCSXS8ZMuqZnMsrxqI2E5sPyoTpxoPU/gVZMzr2zjOfg8GIZOMsw==", + "dev": true + }, "currently-unhandled": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz", @@ -4647,9 +4670,9 @@ } }, "typescript": { - "version": "4.6.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.6.3.tgz", - "integrity": "sha512-yNIatDa5iaofVozS/uQJEl3JRWLKKGJKh6Yaiv0GLGSuhpFJe7P3SbHZ8/yjAHRQwKRoA6YZqlfjXWmVzoVSMw==", + "version": "4.6.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.6.4.tgz", + "integrity": "sha512-9ia/jWHIEbo49HfjrLGfKbZSuWo9iTMwXO+Ca3pRsSpbsMbc7/IU8NKdCZVRRBafVPGnoJeFL76ZOAA84I9fEg==", "dev": true }, "typescript-json-schema": { diff --git a/package.json b/package.json index 4483c3859..0bd18bf59 100644 --- a/package.json +++ b/package.json @@ -63,11 +63,11 @@ "scripts": { "lint": "dprint check", "lint-fix": "dprint fmt", - "clean": "rimraf dist tsconfig.schema.json tsconfig.schemastore-schema.json tsconfig.tsbuildinfo tests/ts-node-packed.tgz", + "clean": "rimraf temp dist tsconfig.schema.json tsconfig.schemastore-schema.json tsconfig.tsbuildinfo tests/ts-node-packed.tgz tests/node_modules tests/tmp", "rebuild": "npm run clean && npm run build", "build": "npm run build-nopack && npm run build-pack", "build-nopack": "npm run build-tsc && npm run build-configSchema", - "build-tsc": "tsc", + "build-tsc": "tsc -b ./tsconfig.build-dist.json", "build-configSchema": "typescript-json-schema --topRef --refs --validationKeywords allOf --out tsconfig.schema.json tsconfig.build-schema.json TsConfigSchema && node --require ./register ./scripts/create-merged-schema", "build-pack": "node ./scripts/build-pack.js", "test-spec": "ava", @@ -119,7 +119,7 @@ "@types/node": "13.13.5", "@types/proper-lockfile": "^4.1.2", "@types/proxyquire": "^1.3.28", - "@types/react": "^16.0.2", + "@types/react": "^16.14.19", "@types/rimraf": "^3.0.0", "@types/semver": "^7.1.0", "@yarnpkg/fslib": "^2.4.0", @@ -138,7 +138,7 @@ "semver": "^7.1.3", "throat": "^6.0.1", "typedoc": "^0.22.10", - "typescript": "4.6.3", + "typescript": "4.6.4", "typescript-json-schema": "^0.53.0", "util.promisify": "^1.0.1" }, @@ -175,7 +175,7 @@ "singleQuote": true }, "volta": { - "node": "17.5.0", + "node": "18.1.0", "npm": "6.14.15" } } diff --git a/raw/download-and-compare.sh b/raw/download-and-compare.sh index 62c3b7685..fdfc9c287 100755 --- a/raw/download-and-compare.sh +++ b/raw/download-and-compare.sh @@ -40,6 +40,14 @@ download #### +path=lib/internal/modules/cjs/helpers +local=node-internal-modules-cjs-helpers +version=v17.0.1 +download +# compare + +#### + path=lib/internal/modules/esm/resolve local=node-internal-modules-esm-resolve version=v13.12.0 @@ -97,5 +105,7 @@ set -x assertStrippedIsOnlyDeletions node-internal-modules-cjs-loader-v15.3.0 assertStrippedIsOnlyDeletions node-internal-modules-cjs-loader-v17.0.1 +assertStrippedIsOnlyDeletions node-internal-modules-cjs-helpers-v17.0.1 +assertStrippedIsOnlyDeletions node-internal-modules-esm-resolve-v15.3.0 assertStrippedIsOnlyDeletions node-internal-errors-2d5d77306f6dff9110c1f77fefab25f973415770 assertStrippedIsOnlyDeletions node-internal-errors-b533fb3508009e5f567cc776daba8fbf665386a6 diff --git a/raw/node-internal-modules-cjs-helpers-v17.0.1-stripped.js b/raw/node-internal-modules-cjs-helpers-v17.0.1-stripped.js new file mode 100644 index 000000000..746acaafb --- /dev/null +++ b/raw/node-internal-modules-cjs-helpers-v17.0.1-stripped.js @@ -0,0 +1,81 @@ +// Copied from https://github.com/nodejs/node/blob/v17.0.1/lib/internal/modules/cjs/helpers.js + +'use strict'; + +const { + ArrayPrototypeForEach, + ObjectDefineProperty, + ObjectPrototypeHasOwnProperty, + SafeSet, + StringPrototypeIncludes, + StringPrototypeSlice, + StringPrototypeStartsWith, +} = primordials; + +const { getOptionValue } = require('internal/options'); +const userConditions = getOptionValue('--conditions'); + +const noAddons = getOptionValue('--no-addons'); +const addonConditions = noAddons ? [] : ['node-addons']; + +// TODO: Use this set when resolving pkg#exports conditions in loader.js. +const cjsConditions = new SafeSet([ + 'require', + 'node', + ...addonConditions, + ...userConditions, +]); + +function addBuiltinLibsToObject(object, dummyModuleName) { + // Make built-in modules available directly (loaded lazily). + const Module = require('internal/modules/cjs/loader').Module; + const { builtinModules } = Module; + + // To require built-in modules in user-land and ignore modules whose + // `canBeRequiredByUsers` is false. So we create a dummy module object and not + // use `require()` directly. + const dummyModule = new Module(dummyModuleName); + + ArrayPrototypeForEach(builtinModules, (name) => { + // Neither add underscored modules, nor ones that contain slashes (e.g., + // 'fs/promises') or ones that are already defined. + if (StringPrototypeStartsWith(name, '_') || + StringPrototypeIncludes(name, '/') || + ObjectPrototypeHasOwnProperty(object, name)) { + return; + } + // Goals of this mechanism are: + // - Lazy loading of built-in modules + // - Having all built-in modules available as non-enumerable properties + // - Allowing the user to re-assign these variables as if there were no + // pre-existing globals with the same name. + + const setReal = (val) => { + // Deleting the property before re-assigning it disables the + // getter/setter mechanism. + delete object[name]; + object[name] = val; + }; + + ObjectDefineProperty(object, name, { + get: () => { + const lib = dummyModule.require(name); + + // Disable the current getter/setter and set up a new + // non-enumerable property. + delete object[name]; + ObjectDefineProperty(object, name, { + get: () => lib, + set: setReal, + configurable: true, + enumerable: false + }); + + return lib; + }, + set: setReal, + configurable: true, + enumerable: false + }); + }); +} diff --git a/raw/node-internal-modules-cjs-helpers-v17.0.1.js b/raw/node-internal-modules-cjs-helpers-v17.0.1.js new file mode 100644 index 000000000..3dffdd340 --- /dev/null +++ b/raw/node-internal-modules-cjs-helpers-v17.0.1.js @@ -0,0 +1,231 @@ +// Copied from https://github.com/nodejs/node/blob/v17.0.1/lib/internal/modules/cjs/helpers.js + +'use strict'; + +const { + ArrayPrototypeForEach, + ArrayPrototypeJoin, + ArrayPrototypeSome, + ObjectDefineProperty, + ObjectPrototypeHasOwnProperty, + SafeMap, + SafeSet, + StringPrototypeCharCodeAt, + StringPrototypeIncludes, + StringPrototypeSlice, + StringPrototypeStartsWith, +} = primordials; +const { + ERR_MANIFEST_DEPENDENCY_MISSING, + ERR_UNKNOWN_BUILTIN_MODULE +} = require('internal/errors').codes; +const { NativeModule } = require('internal/bootstrap/loaders'); + +const { validateString } = require('internal/validators'); +const path = require('path'); +const { pathToFileURL, fileURLToPath, URL } = require('internal/url'); + +const { getOptionValue } = require('internal/options'); +const userConditions = getOptionValue('--conditions'); + +let debug = require('internal/util/debuglog').debuglog('module', (fn) => { + debug = fn; +}); + +const noAddons = getOptionValue('--no-addons'); +const addonConditions = noAddons ? [] : ['node-addons']; + +// TODO: Use this set when resolving pkg#exports conditions in loader.js. +const cjsConditions = new SafeSet([ + 'require', + 'node', + ...addonConditions, + ...userConditions, +]); + +function loadNativeModule(filename, request) { + const mod = NativeModule.map.get(filename); + if (mod?.canBeRequiredByUsers) { + debug('load native module %s', request); + // compileForPublicLoader() throws if mod.canBeRequiredByUsers is false: + mod.compileForPublicLoader(); + return mod; + } +} + +// Invoke with makeRequireFunction(module) where |module| is the Module object +// to use as the context for the require() function. +// Use redirects to set up a mapping from a policy and restrict dependencies +const urlToFileCache = new SafeMap(); +function makeRequireFunction(mod, redirects) { + const Module = mod.constructor; + + let require; + if (redirects) { + const id = mod.filename || mod.id; + const conditions = cjsConditions; + const { resolve, reaction } = redirects; + require = function require(specifier) { + let missing = true; + const destination = resolve(specifier, conditions); + if (destination === true) { + missing = false; + } else if (destination) { + const href = destination.href; + if (destination.protocol === 'node:') { + const specifier = destination.pathname; + const mod = loadNativeModule(specifier, href); + if (mod && mod.canBeRequiredByUsers) { + return mod.exports; + } + throw new ERR_UNKNOWN_BUILTIN_MODULE(specifier); + } else if (destination.protocol === 'file:') { + let filepath; + if (urlToFileCache.has(href)) { + filepath = urlToFileCache.get(href); + } else { + filepath = fileURLToPath(destination); + urlToFileCache.set(href, filepath); + } + return mod.require(filepath); + } + } + if (missing) { + reaction(new ERR_MANIFEST_DEPENDENCY_MISSING( + id, + specifier, + ArrayPrototypeJoin([...conditions], ', ') + )); + } + return mod.require(specifier); + }; + } else { + require = function require(path) { + return mod.require(path); + }; + } + + function resolve(request, options) { + validateString(request, 'request'); + return Module._resolveFilename(request, mod, false, options); + } + + require.resolve = resolve; + + function paths(request) { + validateString(request, 'request'); + return Module._resolveLookupPaths(request, mod); + } + + resolve.paths = paths; + + require.main = process.mainModule; + + // Enable support to add extra extension types. + require.extensions = Module._extensions; + + require.cache = Module._cache; + + return require; +} + +/** + * Remove byte order marker. This catches EF BB BF (the UTF-8 BOM) + * because the buffer-to-string conversion in `fs.readFileSync()` + * translates it to FEFF, the UTF-16 BOM. + */ +function stripBOM(content) { + if (StringPrototypeCharCodeAt(content) === 0xFEFF) { + content = StringPrototypeSlice(content, 1); + } + return content; +} + +function addBuiltinLibsToObject(object, dummyModuleName) { + // Make built-in modules available directly (loaded lazily). + const Module = require('internal/modules/cjs/loader').Module; + const { builtinModules } = Module; + + // To require built-in modules in user-land and ignore modules whose + // `canBeRequiredByUsers` is false. So we create a dummy module object and not + // use `require()` directly. + const dummyModule = new Module(dummyModuleName); + + ArrayPrototypeForEach(builtinModules, (name) => { + // Neither add underscored modules, nor ones that contain slashes (e.g., + // 'fs/promises') or ones that are already defined. + if (StringPrototypeStartsWith(name, '_') || + StringPrototypeIncludes(name, '/') || + ObjectPrototypeHasOwnProperty(object, name)) { + return; + } + // Goals of this mechanism are: + // - Lazy loading of built-in modules + // - Having all built-in modules available as non-enumerable properties + // - Allowing the user to re-assign these variables as if there were no + // pre-existing globals with the same name. + + const setReal = (val) => { + // Deleting the property before re-assigning it disables the + // getter/setter mechanism. + delete object[name]; + object[name] = val; + }; + + ObjectDefineProperty(object, name, { + get: () => { + const lib = dummyModule.require(name); + + // Disable the current getter/setter and set up a new + // non-enumerable property. + delete object[name]; + ObjectDefineProperty(object, name, { + get: () => lib, + set: setReal, + configurable: true, + enumerable: false + }); + + return lib; + }, + set: setReal, + configurable: true, + enumerable: false + }); + }); +} + +function normalizeReferrerURL(referrer) { + if (typeof referrer === 'string' && path.isAbsolute(referrer)) { + return pathToFileURL(referrer).href; + } + return new URL(referrer).href; +} + +// For error messages only - used to check if ESM syntax is in use. +function hasEsmSyntax(code) { + debug('Checking for ESM syntax'); + const parser = require('internal/deps/acorn/acorn/dist/acorn').Parser; + let root; + try { + root = parser.parse(code, { sourceType: 'module', ecmaVersion: 'latest' }); + } catch { + return false; + } + + return ArrayPrototypeSome(root.body, (stmt) => + stmt.type === 'ExportDefaultDeclaration' || + stmt.type === 'ExportNamedDeclaration' || + stmt.type === 'ImportDeclaration' || + stmt.type === 'ExportAllDeclaration'); +} + +module.exports = { + addBuiltinLibsToObject, + cjsConditions, + hasEsmSyntax, + loadNativeModule, + makeRequireFunction, + normalizeReferrerURL, + stripBOM, +}; diff --git a/raw/node-internal-modules-cjs-loader-v17.0.1-stripped.js b/raw/node-internal-modules-cjs-loader-v17.0.1-stripped.js index 0ea7182d2..5788979df 100644 --- a/raw/node-internal-modules-cjs-loader-v17.0.1-stripped.js +++ b/raw/node-internal-modules-cjs-loader-v17.0.1-stripped.js @@ -1,26 +1,5 @@ // Copied from https://github.com/nodejs/node/blob/v17.0.1/lib/internal/modules/cjs/loader.js -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - 'use strict'; const { @@ -67,37 +46,17 @@ const { StringPrototypeStartsWith, } = primordials; -// Map used to store CJS parsing data. -const cjsParseCache = new SafeWeakMap(); - -// Set first due to cycle with ESM loader functions. -module.exports = { - wrapSafe, Module, toRealPath, readPackageScope, cjsParseCache, - get hasLoadedAnyUserCJSModule() { return hasLoadedAnyUserCJSModule; } -}; - const { NativeModule } = require('internal/bootstrap/loaders'); -const { - maybeCacheSourceMap, -} = require('internal/source_map/source_map_cache'); const { pathToFileURL, fileURLToPath, isURLInstance } = require('internal/url'); -const { deprecate } = require('internal/util'); -const vm = require('vm'); -const assert = require('internal/assert'); const fs = require('fs'); const internalFS = require('internal/fs/utils'); const path = require('path'); const { sep } = path; const { internalModuleStat } = internalBinding('fs'); const packageJsonReader = require('internal/modules/package_json_reader'); -const { safeGetenv } = internalBinding('credentials'); const { cjsConditions, hasEsmSyntax, - loadNativeModule, - makeRequireFunction, - normalizeReferrerURL, - stripBOM, } = require('internal/modules/cjs/helpers'); const { getOptionValue } = require('internal/options'); const preserveSymlinks = getOptionValue('--preserve-symlinks'); @@ -107,35 +66,17 @@ const policy = getOptionValue('--experimental-policy') ? require('internal/process/policy') : null; -// Whether any user-provided CJS modules had been loaded (executed). -// Used for internal assertions. -let hasLoadedAnyUserCJSModule = false; - const { codes: { - ERR_INVALID_ARG_VALUE, ERR_INVALID_MODULE_SPECIFIER, ERR_REQUIRE_ESM, - ERR_UNKNOWN_BUILTIN_MODULE, }, - setArrowMessage, } = require('internal/errors'); -const { validateString } = require('internal/validators'); -const pendingDeprecation = getOptionValue('--pending-deprecation'); const { CHAR_FORWARD_SLASH, - CHAR_BACKWARD_SLASH, - CHAR_COLON } = require('internal/constants'); -const { - isProxy -} = require('internal/util/types'); - -const asyncESM = require('internal/process/esm_loader'); -const { enrichCJSError } = require('internal/modules/esm/translators'); -const { kEvaluated } = internalBinding('module_wrap'); const { encodedSepRegEx, packageExportsResolve, @@ -164,30 +105,8 @@ function stat(filename) { return result; } -function updateChildren(parent, child, scan) { - const children = parent?.children; - if (children && !(scan && ArrayPrototypeIncludes(children, child))) - ArrayPrototypePush(children, child); -} - const moduleParentCache = new SafeWeakMap(); -let modulePaths = []; - -let patched = false; - -function getModuleParent() { - return moduleParentCache.get(this); -} - -function setModuleParent(value) { - moduleParentCache.set(this, value); -} - -let debug = require('internal/util/debuglog').debuglog('module', (fn) => { - debug = fn; -}); - // Given a module name, and a list of paths to test, returns the first // matching file in the following precedence. // @@ -322,22 +241,6 @@ function tryExtensions(p, exts, isMain) { return false; } -// Find the longest (possibly multi-dot) extension registered in -// Module._extensions -function findLongestRegisteredExtension(filename) { - const name = path.basename(filename); - let currentExtension; - let index; - let startIndex = 0; - while ((index = StringPrototypeIndexOf(name, '.', startIndex)) !== -1) { - startIndex = index + 1; - if (index === 0) continue; // Skip dotfiles like .gitignore - currentExtension = StringPrototypeSlice(name, index); - if (Module._extensions[currentExtension]) return currentExtension; - } - return '.js'; -} - function trySelfParentPath(parent) { if (!parent) return false; @@ -489,276 +392,6 @@ Module._findPath = function(request, paths, isMain) { return false; }; -// 'node_modules' character codes reversed -const nmChars = [ 115, 101, 108, 117, 100, 111, 109, 95, 101, 100, 111, 110 ]; -const nmLen = nmChars.length; -if (isWindows) { - // 'from' is the __dirname of the module. - Module._nodeModulePaths = function(from) { - // Guarantee that 'from' is absolute. - from = path.resolve(from); - - // note: this approach *only* works when the path is guaranteed - // to be absolute. Doing a fully-edge-case-correct path.split - // that works on both Windows and Posix is non-trivial. - - // return root node_modules when path is 'D:\\'. - // path.resolve will make sure from.length >=3 in Windows. - if (StringPrototypeCharCodeAt(from, from.length - 1) === - CHAR_BACKWARD_SLASH && - StringPrototypeCharCodeAt(from, from.length - 2) === CHAR_COLON) - return [from + 'node_modules']; - - const paths = []; - for (let i = from.length - 1, p = 0, last = from.length; i >= 0; --i) { - const code = StringPrototypeCharCodeAt(from, i); - // The path segment separator check ('\' and '/') was used to get - // node_modules path for every path segment. - // Use colon as an extra condition since we can get node_modules - // path for drive root like 'C:\node_modules' and don't need to - // parse drive name. - if (code === CHAR_BACKWARD_SLASH || - code === CHAR_FORWARD_SLASH || - code === CHAR_COLON) { - if (p !== nmLen) - ArrayPrototypePush( - paths, - StringPrototypeSlice(from, 0, last) + '\\node_modules' - ); - last = i; - p = 0; - } else if (p !== -1) { - if (nmChars[p] === code) { - ++p; - } else { - p = -1; - } - } - } - - return paths; - }; -} else { // posix - // 'from' is the __dirname of the module. - Module._nodeModulePaths = function(from) { - // Guarantee that 'from' is absolute. - from = path.resolve(from); - // Return early not only to avoid unnecessary work, but to *avoid* returning - // an array of two items for a root: [ '//node_modules', '/node_modules' ] - if (from === '/') - return ['/node_modules']; - - // note: this approach *only* works when the path is guaranteed - // to be absolute. Doing a fully-edge-case-correct path.split - // that works on both Windows and Posix is non-trivial. - const paths = []; - for (let i = from.length - 1, p = 0, last = from.length; i >= 0; --i) { - const code = StringPrototypeCharCodeAt(from, i); - if (code === CHAR_FORWARD_SLASH) { - if (p !== nmLen) - ArrayPrototypePush( - paths, - StringPrototypeSlice(from, 0, last) + '/node_modules' - ); - last = i; - p = 0; - } else if (p !== -1) { - if (nmChars[p] === code) { - ++p; - } else { - p = -1; - } - } - } - - // Append /node_modules to handle root paths. - ArrayPrototypePush(paths, '/node_modules'); - - return paths; - }; -} - -Module._resolveLookupPaths = function(request, parent) { - if (NativeModule.canBeRequiredByUsers(request)) { - debug('looking for %j in []', request); - return null; - } - - // Check for node modules paths. - if (StringPrototypeCharAt(request, 0) !== '.' || - (request.length > 1 && - StringPrototypeCharAt(request, 1) !== '.' && - StringPrototypeCharAt(request, 1) !== '/' && - (!isWindows || StringPrototypeCharAt(request, 1) !== '\\'))) { - - let paths = modulePaths; - if (parent?.paths?.length) { - paths = ArrayPrototypeConcat(parent.paths, paths); - } - - debug('looking for %j in %j', request, paths); - return paths.length > 0 ? paths : null; - } - - // In REPL, parent.filename is null. - if (!parent || !parent.id || !parent.filename) { - // Make require('./path/to/foo') work - normally the path is taken - // from realpath(__filename) but in REPL there is no filename - const mainPaths = ['.']; - - debug('looking for %j in %j', request, mainPaths); - return mainPaths; - } - - debug('RELATIVE: requested: %s from parent.id %s', request, parent.id); - - const parentDir = [path.dirname(parent.filename)]; - debug('looking for %j', parentDir); - return parentDir; -}; - -function emitCircularRequireWarning(prop) { - process.emitWarning( - `Accessing non-existent property '${String(prop)}' of module exports ` + - 'inside circular dependency' - ); -} - -// A Proxy that can be used as the prototype of a module.exports object and -// warns when non-existent properties are accessed. -const CircularRequirePrototypeWarningProxy = new Proxy({}, { - get(target, prop) { - // Allow __esModule access in any case because it is used in the output - // of transpiled code to determine whether something comes from an - // ES module, and is not used as a regular key of `module.exports`. - if (prop in target || prop === '__esModule') return target[prop]; - emitCircularRequireWarning(prop); - return undefined; - }, - - getOwnPropertyDescriptor(target, prop) { - if (ObjectPrototypeHasOwnProperty(target, prop) || prop === '__esModule') - return ObjectGetOwnPropertyDescriptor(target, prop); - emitCircularRequireWarning(prop); - return undefined; - } -}); - -function getExportsForCircularRequire(module) { - if (module.exports && - !isProxy(module.exports) && - ObjectGetPrototypeOf(module.exports) === ObjectPrototype && - // Exclude transpiled ES6 modules / TypeScript code because those may - // employ unusual patterns for accessing 'module.exports'. That should - // be okay because ES6 modules have a different approach to circular - // dependencies anyway. - !module.exports.__esModule) { - // This is later unset once the module is done loading. - ObjectSetPrototypeOf( - module.exports, CircularRequirePrototypeWarningProxy); - } - - return module.exports; -} - -// Check the cache for the requested file. -// 1. If a module already exists in the cache: return its exports object. -// 2. If the module is native: call -// `NativeModule.prototype.compileForPublicLoader()` and return the exports. -// 3. Otherwise, create a new module for the file and save it to the cache. -// Then have it load the file contents before returning its exports -// object. -Module._load = function(request, parent, isMain) { - let relResolveCacheIdentifier; - if (parent) { - debug('Module._load REQUEST %s parent: %s', request, parent.id); - // Fast path for (lazy loaded) modules in the same directory. The indirect - // caching is required to allow cache invalidation without changing the old - // cache key names. - relResolveCacheIdentifier = `${parent.path}\x00${request}`; - const filename = relativeResolveCache[relResolveCacheIdentifier]; - if (filename !== undefined) { - const cachedModule = Module._cache[filename]; - if (cachedModule !== undefined) { - updateChildren(parent, cachedModule, true); - if (!cachedModule.loaded) - return getExportsForCircularRequire(cachedModule); - return cachedModule.exports; - } - delete relativeResolveCache[relResolveCacheIdentifier]; - } - } - - const filename = Module._resolveFilename(request, parent, isMain); - if (StringPrototypeStartsWith(filename, 'node:')) { - // Slice 'node:' prefix - const id = StringPrototypeSlice(filename, 5); - - const module = loadNativeModule(id, request); - if (!module?.canBeRequiredByUsers) { - throw new ERR_UNKNOWN_BUILTIN_MODULE(filename); - } - - return module.exports; - } - - const cachedModule = Module._cache[filename]; - if (cachedModule !== undefined) { - updateChildren(parent, cachedModule, true); - if (!cachedModule.loaded) { - const parseCachedModule = cjsParseCache.get(cachedModule); - if (!parseCachedModule || parseCachedModule.loaded) - return getExportsForCircularRequire(cachedModule); - parseCachedModule.loaded = true; - } else { - return cachedModule.exports; - } - } - - const mod = loadNativeModule(filename, request); - if (mod?.canBeRequiredByUsers) return mod.exports; - - // Don't call updateChildren(), Module constructor already does. - const module = cachedModule || new Module(filename, parent); - - if (isMain) { - process.mainModule = module; - module.id = '.'; - } - - Module._cache[filename] = module; - if (parent !== undefined) { - relativeResolveCache[relResolveCacheIdentifier] = filename; - } - - let threw = true; - try { - module.load(filename); - threw = false; - } finally { - if (threw) { - delete Module._cache[filename]; - if (parent !== undefined) { - delete relativeResolveCache[relResolveCacheIdentifier]; - const children = parent?.children; - if (ArrayIsArray(children)) { - const index = ArrayPrototypeIndexOf(children, module); - if (index !== -1) { - ArrayPrototypeSplice(children, index, 1); - } - } - } - } else if (module.exports && - !isProxy(module.exports) && - ObjectGetPrototypeOf(module.exports) === - CircularRequirePrototypeWarningProxy) { - ObjectSetPrototypeOf(module.exports, ObjectPrototype); - } - } - - return module.exports; -}; - Module._resolveFilename = function(request, parent, isMain, options) { if (StringPrototypeStartsWith(request, 'node:') || NativeModule.canBeRequiredByUsers(request)) { @@ -873,147 +506,6 @@ function createEsmNotFoundErr(request, path) { return err; } -// Given a file name, pass it to the proper extension handler. -Module.prototype.load = function(filename) { - debug('load %j for module %j', filename, this.id); - - assert(!this.loaded); - this.filename = filename; - this.paths = Module._nodeModulePaths(path.dirname(filename)); - - const extension = findLongestRegisteredExtension(filename); - // allow .mjs to be overridden - if (StringPrototypeEndsWith(filename, '.mjs') && !Module._extensions['.mjs']) - throw new ERR_REQUIRE_ESM(filename, true); - - Module._extensions[extension](this, filename); - this.loaded = true; - - const esmLoader = asyncESM.esmLoader; - // Create module entry at load time to snapshot exports correctly - const exports = this.exports; - // Preemptively cache - if ((module?.module === undefined || - module.module.getStatus() < kEvaluated) && - !esmLoader.cjsCache.has(this)) - esmLoader.cjsCache.set(this, exports); -}; - - -// Loads a module at the given file path. Returns that module's -// `exports` property. -Module.prototype.require = function(id) { - validateString(id, 'id'); - if (id === '') { - throw new ERR_INVALID_ARG_VALUE('id', id, - 'must be a non-empty string'); - } - requireDepth++; - try { - return Module._load(id, this, /* isMain */ false); - } finally { - requireDepth--; - } -}; - - -// Resolved path to process.argv[1] will be lazily placed here -// (needed for setting breakpoint when called with --inspect-brk) -let resolvedArgv; -let hasPausedEntry = false; - -function wrapSafe(filename, content, cjsModuleInstance) { - if (patched) { - const wrapper = Module.wrap(content); - return vm.runInThisContext(wrapper, { - filename, - lineOffset: 0, - displayErrors: true, - importModuleDynamically: async (specifier) => { - const loader = asyncESM.esmLoader; - return loader.import(specifier, normalizeReferrerURL(filename)); - }, - }); - } - try { - return vm.compileFunction(content, [ - 'exports', - 'require', - 'module', - '__filename', - '__dirname', - ], { - filename, - importModuleDynamically(specifier) { - const loader = asyncESM.esmLoader; - return loader.import(specifier, normalizeReferrerURL(filename)); - }, - }); - } catch (err) { - if (process.mainModule === cjsModuleInstance) - enrichCJSError(err, content); - throw err; - } -} - -// Run the file contents in the correct scope or sandbox. Expose -// the correct helper variables (require, module, exports) to -// the file. -// Returns exception, if any. -Module.prototype._compile = function(content, filename) { - let moduleURL; - let redirects; - if (policy?.manifest) { - moduleURL = pathToFileURL(filename); - redirects = policy.manifest.getDependencyMapper(moduleURL); - policy.manifest.assertIntegrity(moduleURL, content); - } - - maybeCacheSourceMap(filename, content, this); - const compiledWrapper = wrapSafe(filename, content, this); - - let inspectorWrapper = null; - if (getOptionValue('--inspect-brk') && process._eval == null) { - if (!resolvedArgv) { - // We enter the repl if we're not given a filename argument. - if (process.argv[1]) { - try { - resolvedArgv = Module._resolveFilename(process.argv[1], null, false); - } catch { - // We only expect this codepath to be reached in the case of a - // preloaded module (it will fail earlier with the main entry) - assert(ArrayIsArray(getOptionValue('--require'))); - } - } else { - resolvedArgv = 'repl'; - } - } - - // Set breakpoint on module start - if (resolvedArgv && !hasPausedEntry && filename === resolvedArgv) { - hasPausedEntry = true; - inspectorWrapper = internalBinding('inspector').callAndPauseOnStart; - } - } - const dirname = path.dirname(filename); - const require = makeRequireFunction(this, redirects); - let result; - const exports = this.exports; - const thisValue = exports; - const module = this; - if (requireDepth === 0) statCache = new SafeMap(); - if (inspectorWrapper) { - result = inspectorWrapper(compiledWrapper, thisValue, exports, - require, module, filename, dirname); - } else { - result = ReflectApply(compiledWrapper, thisValue, - [exports, require, module, filename, dirname]); - } - hasLoadedAnyUserCJSModule = true; - if (requireDepth === 0) statCache = null; - return result; -}; - // Native extension for .js Module._extensions['.js'] = function(module, filename) { // If already analyzed the source, then it will be cached. @@ -1060,138 +552,3 @@ Module._extensions['.js'] = function(module, filename) { } module._compile(content, filename); }; - - -// Native extension for .json -Module._extensions['.json'] = function(module, filename) { - const content = fs.readFileSync(filename, 'utf8'); - - if (policy?.manifest) { - const moduleURL = pathToFileURL(filename); - policy.manifest.assertIntegrity(moduleURL, content); - } - - try { - module.exports = JSONParse(stripBOM(content)); - } catch (err) { - err.message = filename + ': ' + err.message; - throw err; - } -}; - - -// Native extension for .node -Module._extensions['.node'] = function(module, filename) { - if (policy?.manifest) { - const content = fs.readFileSync(filename); - const moduleURL = pathToFileURL(filename); - policy.manifest.assertIntegrity(moduleURL, content); - } - // Be aware this doesn't use `content` - return process.dlopen(module, path.toNamespacedPath(filename)); -}; - -function createRequireFromPath(filename) { - // Allow a directory to be passed as the filename - const trailingSlash = - StringPrototypeEndsWith(filename, '/') || - (isWindows && StringPrototypeEndsWith(filename, '\\')); - - const proxyPath = trailingSlash ? - path.join(filename, 'noop.js') : - filename; - - const m = new Module(proxyPath); - m.filename = proxyPath; - - m.paths = Module._nodeModulePaths(m.path); - return makeRequireFunction(m, null); -} - -const createRequireError = 'must be a file URL object, file URL string, or ' + - 'absolute path string'; - -function createRequire(filename) { - let filepath; - - if (isURLInstance(filename) || - (typeof filename === 'string' && !path.isAbsolute(filename))) { - try { - filepath = fileURLToPath(filename); - } catch { - throw new ERR_INVALID_ARG_VALUE('filename', filename, - createRequireError); - } - } else if (typeof filename !== 'string') { - throw new ERR_INVALID_ARG_VALUE('filename', filename, createRequireError); - } else { - filepath = filename; - } - return createRequireFromPath(filepath); -} - -Module.createRequire = createRequire; - -Module._initPaths = function() { - const homeDir = isWindows ? process.env.USERPROFILE : safeGetenv('HOME'); - const nodePath = isWindows ? process.env.NODE_PATH : safeGetenv('NODE_PATH'); - - // process.execPath is $PREFIX/bin/node except on Windows where it is - // $PREFIX\node.exe where $PREFIX is the root of the Node.js installation. - const prefixDir = isWindows ? - path.resolve(process.execPath, '..') : - path.resolve(process.execPath, '..', '..'); - - const paths = [path.resolve(prefixDir, 'lib', 'node')]; - - if (homeDir) { - ArrayPrototypeUnshift(paths, path.resolve(homeDir, '.node_libraries')); - ArrayPrototypeUnshift(paths, path.resolve(homeDir, '.node_modules')); - } - - if (nodePath) { - ArrayPrototypeUnshiftApply(paths, ArrayPrototypeFilter( - StringPrototypeSplit(nodePath, path.delimiter), - Boolean - )); - } - - modulePaths = paths; - - // Clone as a shallow copy, for introspection. - Module.globalPaths = ArrayPrototypeSlice(modulePaths); -}; - -Module._preloadModules = function(requests) { - if (!ArrayIsArray(requests)) - return; - - isPreloading = true; - - // Preloaded modules have a dummy parent module which is deemed to exist - // in the current working directory. This seeds the search path for - // preloaded modules. - const parent = new Module('internal/preload', null); - try { - parent.paths = Module._nodeModulePaths(process.cwd()); - } catch (e) { - if (e.code !== 'ENOENT') { - isPreloading = false; - throw e; - } - } - for (let n = 0; n < requests.length; n++) - parent.require(requests[n]); - isPreloading = false; -}; - -Module.syncBuiltinESMExports = function syncBuiltinESMExports() { - for (const mod of NativeModule.map.values()) { - if (mod.canBeRequiredByUsers) { - mod.syncExports(); - } - } -}; - -// Backwards compatibility -Module.Module = Module; diff --git a/raw/node-internal-modules-esm-resolve-v15.3.0-stripped.js b/raw/node-internal-modules-esm-resolve-v15.3.0-stripped.js new file mode 100644 index 000000000..53599c9ed --- /dev/null +++ b/raw/node-internal-modules-esm-resolve-v15.3.0-stripped.js @@ -0,0 +1,899 @@ +// Copied from https://github.com/nodejs/node/blob/v15.3.0/lib/internal/modules/esm/resolve.js + +'use strict'; + +const { + ArrayIsArray, + ArrayPrototypeJoin, + ArrayPrototypeShift, + JSONParse, + JSONStringify, + ObjectFreeze, + ObjectGetOwnPropertyNames, + ObjectPrototypeHasOwnProperty, + RegExpPrototypeTest, + SafeMap, + SafeSet, + StringPrototypeEndsWith, + StringPrototypeIndexOf, + StringPrototypeLastIndexOf, + StringPrototypeReplace, + StringPrototypeSlice, + StringPrototypeSplit, + StringPrototypeStartsWith, + StringPrototypeSubstr, +} = primordials; +const internalFS = require('internal/fs/utils'); +const { NativeModule } = require('internal/bootstrap/loaders'); +const { + realpathSync, + statSync, + Stats, +} = require('fs'); +const { getOptionValue } = require('internal/options'); +// Do not eagerly grab .manifest, it may be in TDZ +const policy = getOptionValue('--experimental-policy') ? + require('internal/process/policy') : + null; +const { sep, relative } = require('path'); +const preserveSymlinks = getOptionValue('--preserve-symlinks'); +const preserveSymlinksMain = getOptionValue('--preserve-symlinks-main'); +const typeFlag = getOptionValue('--input-type'); +const { URL, pathToFileURL, fileURLToPath } = require('internal/url'); +const { + ERR_INPUT_TYPE_NOT_ALLOWED, + ERR_INVALID_ARG_VALUE, + ERR_INVALID_MODULE_SPECIFIER, + ERR_INVALID_PACKAGE_CONFIG, + ERR_INVALID_PACKAGE_TARGET, + ERR_MANIFEST_DEPENDENCY_MISSING, + ERR_MODULE_NOT_FOUND, + ERR_PACKAGE_IMPORT_NOT_DEFINED, + ERR_PACKAGE_PATH_NOT_EXPORTED, + ERR_UNSUPPORTED_DIR_IMPORT, + ERR_UNSUPPORTED_ESM_URL_SCHEME, +} = require('internal/errors').codes; +const { Module: CJSModule } = require('internal/modules/cjs/loader'); + +const packageJsonReader = require('internal/modules/package_json_reader'); +const userConditions = getOptionValue('--conditions'); +const DEFAULT_CONDITIONS = ObjectFreeze(['node', 'import', ...userConditions]); +const DEFAULT_CONDITIONS_SET = new SafeSet(DEFAULT_CONDITIONS); + +const pendingDeprecation = getOptionValue('--pending-deprecation'); +const emittedPackageWarnings = new SafeSet(); +function emitFolderMapDeprecation(match, pjsonUrl, isExports, base) { + const pjsonPath = fileURLToPath(pjsonUrl); + if (!pendingDeprecation) { + const nodeModulesIndex = StringPrototypeLastIndexOf(pjsonPath, + '/node_modules/'); + if (nodeModulesIndex !== -1) { + const afterNodeModulesPath = StringPrototypeSlice(pjsonPath, + nodeModulesIndex + 14, + -13); + try { + const { packageSubpath } = parsePackageName(afterNodeModulesPath); + if (packageSubpath === '.') + return; + } catch {} + } + } + if (emittedPackageWarnings.has(pjsonPath + '|' + match)) + return; + emittedPackageWarnings.add(pjsonPath + '|' + match); + process.emitWarning( + `Use of deprecated folder mapping "${match}" in the ${isExports ? + '"exports"' : '"imports"'} field module resolution of the package at ${ + pjsonPath}${base ? ` imported from ${fileURLToPath(base)}` : ''}.\n` + + `Update this package.json to use a subpath pattern like "${match}*".`, + 'DeprecationWarning', + 'DEP0148' + ); +} + +function getConditionsSet(conditions) { + if (conditions !== undefined && conditions !== DEFAULT_CONDITIONS) { + if (!ArrayIsArray(conditions)) { + throw new ERR_INVALID_ARG_VALUE('conditions', conditions, + 'expected an array'); + } + return new SafeSet(conditions); + } + return DEFAULT_CONDITIONS_SET; +} + +const realpathCache = new SafeMap(); +const packageJSONCache = new SafeMap(); /* string -> PackageConfig */ + +function tryStatSync(path) { + try { + return statSync(path); + } catch { + return new Stats(); + } +} + +function getPackageConfig(path, specifier, base) { + const existing = packageJSONCache.get(path); + if (existing !== undefined) { + return existing; + } + const source = packageJsonReader.read(path).string; + if (source === undefined) { + const packageConfig = { + pjsonPath: path, + exists: false, + main: undefined, + name: undefined, + type: 'none', + exports: undefined, + imports: undefined, + }; + packageJSONCache.set(path, packageConfig); + return packageConfig; + } + + let packageJSON; + try { + packageJSON = JSONParse(source); + } catch (error) { + throw new ERR_INVALID_PACKAGE_CONFIG( + path, + (base ? `"${specifier}" from ` : '') + fileURLToPath(base || specifier), + error.message + ); + } + + let { imports, main, name, type } = packageJSON; + const { exports } = packageJSON; + if (typeof imports !== 'object' || imports === null) imports = undefined; + if (typeof main !== 'string') main = undefined; + if (typeof name !== 'string') name = undefined; + // Ignore unknown types for forwards compatibility + if (type !== 'module' && type !== 'commonjs') type = 'none'; + + const packageConfig = { + pjsonPath: path, + exists: true, + main, + name, + type, + exports, + imports, + }; + packageJSONCache.set(path, packageConfig); + return packageConfig; +} + +function getPackageScopeConfig(resolved) { + let packageJSONUrl = new URL('./package.json', resolved); + while (true) { + const packageJSONPath = packageJSONUrl.pathname; + if (StringPrototypeEndsWith(packageJSONPath, 'node_modules/package.json')) + break; + const packageConfig = getPackageConfig(fileURLToPath(packageJSONUrl), + resolved); + if (packageConfig.exists) return packageConfig; + + const lastPackageJSONUrl = packageJSONUrl; + packageJSONUrl = new URL('../package.json', packageJSONUrl); + + // Terminates at root where ../package.json equals ../../package.json + // (can't just check "/package.json" for Windows support). + if (packageJSONUrl.pathname === lastPackageJSONUrl.pathname) break; + } + const packageJSONPath = fileURLToPath(packageJSONUrl); + const packageConfig = { + pjsonPath: packageJSONPath, + exists: false, + main: undefined, + name: undefined, + type: 'none', + exports: undefined, + imports: undefined, + }; + packageJSONCache.set(packageJSONPath, packageConfig); + return packageConfig; +} + +/* + * Legacy CommonJS main resolution: + * 1. let M = pkg_url + (json main field) + * 2. TRY(M, M.js, M.json, M.node) + * 3. TRY(M/index.js, M/index.json, M/index.node) + * 4. TRY(pkg_url/index.js, pkg_url/index.json, pkg_url/index.node) + * 5. NOT_FOUND + */ +function fileExists(url) { + return tryStatSync(fileURLToPath(url)).isFile(); +} + +function legacyMainResolve(packageJSONUrl, packageConfig, base) { + let guess; + if (packageConfig.main !== undefined) { + // Note: fs check redundances will be handled by Descriptor cache here. + if (fileExists(guess = new URL(`./${packageConfig.main}`, + packageJSONUrl))) { + return guess; + } + if (fileExists(guess = new URL(`./${packageConfig.main}.js`, + packageJSONUrl))) { + return guess; + } + if (fileExists(guess = new URL(`./${packageConfig.main}.json`, + packageJSONUrl))) { + return guess; + } + if (fileExists(guess = new URL(`./${packageConfig.main}.node`, + packageJSONUrl))) { + return guess; + } + if (fileExists(guess = new URL(`./${packageConfig.main}/index.js`, + packageJSONUrl))) { + return guess; + } + if (fileExists(guess = new URL(`./${packageConfig.main}/index.json`, + packageJSONUrl))) { + return guess; + } + if (fileExists(guess = new URL(`./${packageConfig.main}/index.node`, + packageJSONUrl))) { + return guess; + } + // Fallthrough. + } + if (fileExists(guess = new URL('./index.js', packageJSONUrl))) { + return guess; + } + // So fs. + if (fileExists(guess = new URL('./index.json', packageJSONUrl))) { + return guess; + } + if (fileExists(guess = new URL('./index.node', packageJSONUrl))) { + return guess; + } + // Not found. + throw new ERR_MODULE_NOT_FOUND( + fileURLToPath(new URL('.', packageJSONUrl)), fileURLToPath(base)); +} + +function resolveExtensionsWithTryExactName(search) { + if (fileExists(search)) return search; + return resolveExtensions(search); +} + +const extensions = ['.js', '.json', '.node', '.mjs']; +function resolveExtensions(search) { + for (let i = 0; i < extensions.length; i++) { + const extension = extensions[i]; + const guess = new URL(`${search.pathname}${extension}`, search); + if (fileExists(guess)) return guess; + } + return undefined; +} + +function resolveIndex(search) { + return resolveExtensions(new URL('index', search)); +} + +const encodedSepRegEx = /%2F|%2C/i; +function finalizeResolution(resolved, base) { + if (RegExpPrototypeTest(encodedSepRegEx, resolved.pathname)) + throw new ERR_INVALID_MODULE_SPECIFIER( + resolved.pathname, 'must not include encoded "/" or "\\" characters', + fileURLToPath(base)); + + const path = fileURLToPath(resolved); + if (getOptionValue('--experimental-specifier-resolution') === 'node') { + let file = resolveExtensionsWithTryExactName(resolved); + if (file !== undefined) return file; + if (!StringPrototypeEndsWith(path, '/')) { + file = resolveIndex(new URL(`${resolved}/`)); + if (file !== undefined) return file; + } else { + return resolveIndex(resolved) || resolved; + } + throw new ERR_MODULE_NOT_FOUND( + resolved.pathname, fileURLToPath(base), 'module'); + } + + const stats = tryStatSync(StringPrototypeEndsWith(path, '/') ? + StringPrototypeSlice(path, -1) : path); + if (stats.isDirectory()) { + const err = new ERR_UNSUPPORTED_DIR_IMPORT(path, fileURLToPath(base)); + err.url = String(resolved); + throw err; + } else if (!stats.isFile()) { + throw new ERR_MODULE_NOT_FOUND( + path || resolved.pathname, base && fileURLToPath(base), 'module'); + } + + return resolved; +} + +function throwImportNotDefined(specifier, packageJSONUrl, base) { + throw new ERR_PACKAGE_IMPORT_NOT_DEFINED( + specifier, packageJSONUrl && fileURLToPath(new URL('.', packageJSONUrl)), + fileURLToPath(base)); +} + +function throwExportsNotFound(subpath, packageJSONUrl, base) { + throw new ERR_PACKAGE_PATH_NOT_EXPORTED( + fileURLToPath(new URL('.', packageJSONUrl)), subpath, + base && fileURLToPath(base)); +} + +function throwInvalidSubpath(subpath, packageJSONUrl, internal, base) { + const reason = `request is not a valid subpath for the "${internal ? + 'imports' : 'exports'}" resolution of ${fileURLToPath(packageJSONUrl)}`; + throw new ERR_INVALID_MODULE_SPECIFIER(subpath, reason, + base && fileURLToPath(base)); +} + +function throwInvalidPackageTarget( + subpath, target, packageJSONUrl, internal, base) { + if (typeof target === 'object' && target !== null) { + target = JSONStringify(target, null, ''); + } else { + target = `${target}`; + } + throw new ERR_INVALID_PACKAGE_TARGET( + fileURLToPath(new URL('.', packageJSONUrl)), subpath, target, + internal, base && fileURLToPath(base)); +} + +const invalidSegmentRegEx = /(^|\\|\/)(\.\.?|node_modules)(\\|\/|$)/; +const patternRegEx = /\*/g; + +function resolvePackageTargetString( + target, subpath, match, packageJSONUrl, base, pattern, internal, conditions) { + if (subpath !== '' && !pattern && target[target.length - 1] !== '/') + throwInvalidPackageTarget(match, target, packageJSONUrl, internal, base); + + if (!StringPrototypeStartsWith(target, './')) { + if (internal && !StringPrototypeStartsWith(target, '../') && + !StringPrototypeStartsWith(target, '/')) { + let isURL = false; + try { + new URL(target); + isURL = true; + } catch {} + if (!isURL) { + const exportTarget = pattern ? + StringPrototypeReplace(target, patternRegEx, subpath) : + target + subpath; + return packageResolve(exportTarget, packageJSONUrl, conditions); + } + } + throwInvalidPackageTarget(match, target, packageJSONUrl, internal, base); + } + + if (RegExpPrototypeTest(invalidSegmentRegEx, StringPrototypeSlice(target, 2))) + throwInvalidPackageTarget(match, target, packageJSONUrl, internal, base); + + const resolved = new URL(target, packageJSONUrl); + const resolvedPath = resolved.pathname; + const packagePath = new URL('.', packageJSONUrl).pathname; + + if (!StringPrototypeStartsWith(resolvedPath, packagePath)) + throwInvalidPackageTarget(match, target, packageJSONUrl, internal, base); + + if (subpath === '') return resolved; + + if (RegExpPrototypeTest(invalidSegmentRegEx, subpath)) + throwInvalidSubpath(match + subpath, packageJSONUrl, internal, base); + + if (pattern) + return new URL(StringPrototypeReplace(resolved.href, patternRegEx, + subpath)); + return new URL(subpath, resolved); +} + +/** + * @param {string} key + * @returns {boolean} + */ +function isArrayIndex(key) { + const keyNum = +key; + if (`${keyNum}` !== key) return false; + return keyNum >= 0 && keyNum < 0xFFFF_FFFF; +} + +function resolvePackageTarget(packageJSONUrl, target, subpath, packageSubpath, + base, pattern, internal, conditions) { + if (typeof target === 'string') { + return resolvePackageTargetString( + target, subpath, packageSubpath, packageJSONUrl, base, pattern, internal, + conditions); + } else if (ArrayIsArray(target)) { + if (target.length === 0) + return null; + + let lastException; + for (let i = 0; i < target.length; i++) { + const targetItem = target[i]; + let resolved; + try { + resolved = resolvePackageTarget( + packageJSONUrl, targetItem, subpath, packageSubpath, base, pattern, + internal, conditions); + } catch (e) { + lastException = e; + if (e.code === 'ERR_INVALID_PACKAGE_TARGET') + continue; + throw e; + } + if (resolved === undefined) + continue; + if (resolved === null) { + lastException = null; + continue; + } + return resolved; + } + if (lastException === undefined || lastException === null) + return lastException; + throw lastException; + } else if (typeof target === 'object' && target !== null) { + const keys = ObjectGetOwnPropertyNames(target); + for (let i = 0; i < keys.length; i++) { + const key = keys[i]; + if (isArrayIndex(key)) { + throw new ERR_INVALID_PACKAGE_CONFIG( + fileURLToPath(packageJSONUrl), base, + '"exports" cannot contain numeric property keys.'); + } + } + for (let i = 0; i < keys.length; i++) { + const key = keys[i]; + if (key === 'default' || conditions.has(key)) { + const conditionalTarget = target[key]; + const resolved = resolvePackageTarget( + packageJSONUrl, conditionalTarget, subpath, packageSubpath, base, + pattern, internal, conditions); + if (resolved === undefined) + continue; + return resolved; + } + } + return undefined; + } else if (target === null) { + return null; + } + throwInvalidPackageTarget(packageSubpath, target, packageJSONUrl, internal, + base); +} + +function isConditionalExportsMainSugar(exports, packageJSONUrl, base) { + if (typeof exports === 'string' || ArrayIsArray(exports)) return true; + if (typeof exports !== 'object' || exports === null) return false; + + const keys = ObjectGetOwnPropertyNames(exports); + let isConditionalSugar = false; + let i = 0; + for (let j = 0; j < keys.length; j++) { + const key = keys[j]; + const curIsConditionalSugar = key === '' || key[0] !== '.'; + if (i++ === 0) { + isConditionalSugar = curIsConditionalSugar; + } else if (isConditionalSugar !== curIsConditionalSugar) { + throw new ERR_INVALID_PACKAGE_CONFIG( + fileURLToPath(packageJSONUrl), base, + '"exports" cannot contain some keys starting with \'.\' and some not.' + + ' The exports object must either be an object of package subpath keys' + + ' or an object of main entry condition name keys only.'); + } + } + return isConditionalSugar; +} + +/** + * @param {URL} packageJSONUrl + * @param {string} packageSubpath + * @param {object} packageConfig + * @param {string} base + * @param {Set} conditions + * @returns {URL} + */ +function packageExportsResolve( + packageJSONUrl, packageSubpath, packageConfig, base, conditions) { + let exports = packageConfig.exports; + if (isConditionalExportsMainSugar(exports, packageJSONUrl, base)) + exports = { '.': exports }; + + if (ObjectPrototypeHasOwnProperty(exports, packageSubpath)) { + const target = exports[packageSubpath]; + const resolved = resolvePackageTarget( + packageJSONUrl, target, '', packageSubpath, base, false, false, conditions + ); + if (resolved === null || resolved === undefined) + throwExportsNotFound(packageSubpath, packageJSONUrl, base); + return { resolved, exact: true }; + } + + let bestMatch = ''; + const keys = ObjectGetOwnPropertyNames(exports); + for (let i = 0; i < keys.length; i++) { + const key = keys[i]; + if (key[key.length - 1] === '*' && + StringPrototypeStartsWith(packageSubpath, + StringPrototypeSlice(key, 0, -1)) && + packageSubpath.length >= key.length && + key.length > bestMatch.length) { + bestMatch = key; + } else if (key[key.length - 1] === '/' && + StringPrototypeStartsWith(packageSubpath, key) && + key.length > bestMatch.length) { + bestMatch = key; + } + } + + if (bestMatch) { + const target = exports[bestMatch]; + const pattern = bestMatch[bestMatch.length - 1] === '*'; + const subpath = StringPrototypeSubstr(packageSubpath, bestMatch.length - + (pattern ? 1 : 0)); + const resolved = resolvePackageTarget(packageJSONUrl, target, subpath, + bestMatch, base, pattern, false, + conditions); + if (resolved === null || resolved === undefined) + throwExportsNotFound(packageSubpath, packageJSONUrl, base); + if (!pattern) + emitFolderMapDeprecation(bestMatch, packageJSONUrl, true, base); + return { resolved, exact: pattern }; + } + + throwExportsNotFound(packageSubpath, packageJSONUrl, base); +} + +function packageImportsResolve(name, base, conditions) { + if (name === '#' || StringPrototypeStartsWith(name, '#/')) { + const reason = 'is not a valid internal imports specifier name'; + throw new ERR_INVALID_MODULE_SPECIFIER(name, reason, fileURLToPath(base)); + } + let packageJSONUrl; + const packageConfig = getPackageScopeConfig(base); + if (packageConfig.exists) { + packageJSONUrl = pathToFileURL(packageConfig.pjsonPath); + const imports = packageConfig.imports; + if (imports) { + if (ObjectPrototypeHasOwnProperty(imports, name)) { + const resolved = resolvePackageTarget( + packageJSONUrl, imports[name], '', name, base, false, true, conditions + ); + if (resolved !== null) + return { resolved, exact: true }; + } else { + let bestMatch = ''; + const keys = ObjectGetOwnPropertyNames(imports); + for (let i = 0; i < keys.length; i++) { + const key = keys[i]; + if (key[key.length - 1] === '*' && + StringPrototypeStartsWith(name, + StringPrototypeSlice(key, 0, -1)) && + name.length >= key.length && + key.length > bestMatch.length) { + bestMatch = key; + } else if (key[key.length - 1] === '/' && + StringPrototypeStartsWith(name, key) && + key.length > bestMatch.length) { + bestMatch = key; + } + } + + if (bestMatch) { + const target = imports[bestMatch]; + const pattern = bestMatch[bestMatch.length - 1] === '*'; + const subpath = StringPrototypeSubstr(name, bestMatch.length - + (pattern ? 1 : 0)); + const resolved = resolvePackageTarget( + packageJSONUrl, target, subpath, bestMatch, base, pattern, true, + conditions); + if (resolved !== null) { + if (!pattern) + emitFolderMapDeprecation(bestMatch, packageJSONUrl, false, base); + return { resolved, exact: pattern }; + } + } + } + } + } + throwImportNotDefined(name, packageJSONUrl, base); +} + +function getPackageType(url) { + const packageConfig = getPackageScopeConfig(url); + return packageConfig.type; +} + +function parsePackageName(specifier, base) { + let separatorIndex = StringPrototypeIndexOf(specifier, '/'); + let validPackageName = true; + let isScoped = false; + if (specifier[0] === '@') { + isScoped = true; + if (separatorIndex === -1 || specifier.length === 0) { + validPackageName = false; + } else { + separatorIndex = StringPrototypeIndexOf( + specifier, '/', separatorIndex + 1); + } + } + + const packageName = separatorIndex === -1 ? + specifier : StringPrototypeSlice(specifier, 0, separatorIndex); + + // Package name cannot have leading . and cannot have percent-encoding or + // separators. + for (let i = 0; i < packageName.length; i++) { + if (packageName[i] === '%' || packageName[i] === '\\') { + validPackageName = false; + break; + } + } + + if (!validPackageName) { + throw new ERR_INVALID_MODULE_SPECIFIER( + specifier, 'is not a valid package name', fileURLToPath(base)); + } + + const packageSubpath = '.' + (separatorIndex === -1 ? '' : + StringPrototypeSlice(specifier, separatorIndex)); + + return { packageName, packageSubpath, isScoped }; +} + +/** + * @param {string} specifier + * @param {URL} base + * @param {Set} conditions + * @returns {URL} + */ +function packageResolve(specifier, base, conditions) { + const { packageName, packageSubpath, isScoped } = + parsePackageName(specifier, base); + + // ResolveSelf + const packageConfig = getPackageScopeConfig(base); + if (packageConfig.exists) { + const packageJSONUrl = pathToFileURL(packageConfig.pjsonPath); + if (packageConfig.name === packageName && + packageConfig.exports !== undefined && packageConfig.exports !== null) { + return packageExportsResolve( + packageJSONUrl, packageSubpath, packageConfig, base, conditions + ).resolved; + } + } + + let packageJSONUrl = + new URL('./node_modules/' + packageName + '/package.json', base); + let packageJSONPath = fileURLToPath(packageJSONUrl); + let lastPath; + do { + const stat = tryStatSync(StringPrototypeSlice(packageJSONPath, 0, + packageJSONPath.length - 13)); + if (!stat.isDirectory()) { + lastPath = packageJSONPath; + packageJSONUrl = new URL((isScoped ? + '../../../../node_modules/' : '../../../node_modules/') + + packageName + '/package.json', packageJSONUrl); + packageJSONPath = fileURLToPath(packageJSONUrl); + continue; + } + + // Package match. + const packageConfig = getPackageConfig(packageJSONPath, specifier, base); + if (packageConfig.exports !== undefined && packageConfig.exports !== null) + return packageExportsResolve( + packageJSONUrl, packageSubpath, packageConfig, base, conditions + ).resolved; + if (packageSubpath === '.') + return legacyMainResolve(packageJSONUrl, packageConfig, base); + return new URL(packageSubpath, packageJSONUrl); + // Cross-platform root check. + } while (packageJSONPath.length !== lastPath.length); + + // eslint can't handle the above code. + // eslint-disable-next-line no-unreachable + throw new ERR_MODULE_NOT_FOUND(packageName, fileURLToPath(base)); +} + +function isBareSpecifier(specifier) { + return specifier[0] && specifier[0] !== '/' && specifier[0] !== '.'; +} + +function isRelativeSpecifier(specifier) { + if (specifier[0] === '.') { + if (specifier.length === 1 || specifier[1] === '/') return true; + if (specifier[1] === '.') { + if (specifier.length === 2 || specifier[2] === '/') return true; + } + } + return false; +} + +function shouldBeTreatedAsRelativeOrAbsolutePath(specifier) { + if (specifier === '') return false; + if (specifier[0] === '/') return true; + return isRelativeSpecifier(specifier); +} + +/** + * @param {string} specifier + * @param {URL} base + * @param {Set} conditions + * @returns {URL} + */ +function moduleResolve(specifier, base, conditions) { + // Order swapped from spec for minor perf gain. + // Ok since relative URLs cannot parse as URLs. + let resolved; + if (shouldBeTreatedAsRelativeOrAbsolutePath(specifier)) { + resolved = new URL(specifier, base); + } else if (specifier[0] === '#') { + ({ resolved } = packageImportsResolve(specifier, base, conditions)); + } else { + try { + resolved = new URL(specifier); + } catch { + resolved = packageResolve(specifier, base, conditions); + } + } + return finalizeResolution(resolved, base); +} + +/** + * Try to resolve an import as a CommonJS module + * @param {string} specifier + * @param {string} parentURL + * @returns {boolean|string} + */ +function resolveAsCommonJS(specifier, parentURL) { + try { + const parent = fileURLToPath(parentURL); + const tmpModule = new CJSModule(parent, null); + tmpModule.paths = CJSModule._nodeModulePaths(parent); + + let found = CJSModule._resolveFilename(specifier, tmpModule, false); + + // If it is a relative specifier return the relative path + // to the parent + if (isRelativeSpecifier(specifier)) { + found = relative(parent, found); + // Add '.separator if the path does not start with '..separator' + // This should be a safe assumption because when loading + // esm modules there should be always a file specified so + // there should not be a specifier like '..' or '.' + if (!StringPrototypeStartsWith(found, `..${sep}`)) { + found = `.${sep}${found}`; + } + } else if (isBareSpecifier(specifier)) { + // If it is a bare specifier return the relative path within the + // module + const pkg = StringPrototypeSplit(specifier, '/')[0]; + const index = StringPrototypeIndexOf(found, pkg); + if (index !== -1) { + found = StringPrototypeSlice(found, index); + } + } + // Normalize the path separator to give a valid suggestion + // on Windows + if (process.platform === 'win32') { + found = StringPrototypeReplace(found, new RegExp(`\\${sep}`, 'g'), '/'); + } + return found; + } catch { + return false; + } +} + +function defaultResolve(specifier, context = {}, defaultResolveUnused) { + let { parentURL, conditions } = context; + if (parentURL && policy?.manifest) { + const redirects = policy.manifest.getDependencyMapper(parentURL); + if (redirects) { + const { resolve, reaction } = redirects; + const destination = resolve(specifier, new SafeSet(conditions)); + let missing = true; + if (destination === true) { + missing = false; + } else if (destination) { + const href = destination.href; + return { url: href }; + } + if (missing) { + reaction(new ERR_MANIFEST_DEPENDENCY_MISSING( + parentURL, + specifier, + ArrayPrototypeJoin([...conditions], ', ')) + ); + } + } + } + let parsed; + try { + parsed = new URL(specifier); + if (parsed.protocol === 'data:') { + return { + url: specifier + }; + } + } catch {} + if (parsed && parsed.protocol === 'node:') + return { url: specifier }; + if (parsed && parsed.protocol !== 'file:' && parsed.protocol !== 'data:') + throw new ERR_UNSUPPORTED_ESM_URL_SCHEME(parsed); + if (NativeModule.canBeRequiredByUsers(specifier)) { + return { + url: 'node:' + specifier + }; + } + if (parentURL && StringPrototypeStartsWith(parentURL, 'data:')) { + // This is gonna blow up, we want the error + new URL(specifier, parentURL); + } + + const isMain = parentURL === undefined; + if (isMain) { + parentURL = pathToFileURL(`${process.cwd()}/`).href; + + // This is the initial entry point to the program, and --input-type has + // been passed as an option; but --input-type can only be used with + // --eval, --print or STDIN string input. It is not allowed with file + // input, to avoid user confusion over how expansive the effect of the + // flag should be (i.e. entry point only, package scope surrounding the + // entry point, etc.). + if (typeFlag) + throw new ERR_INPUT_TYPE_NOT_ALLOWED(); + } + + conditions = getConditionsSet(conditions); + let url; + try { + url = moduleResolve(specifier, parentURL, conditions); + } catch (error) { + // Try to give the user a hint of what would have been the + // resolved CommonJS module + if (error.code === 'ERR_MODULE_NOT_FOUND' || + error.code === 'ERR_UNSUPPORTED_DIR_IMPORT') { + if (StringPrototypeStartsWith(specifier, 'file://')) { + specifier = fileURLToPath(specifier); + } + const found = resolveAsCommonJS(specifier, parentURL); + if (found) { + // Modify the stack and message string to include the hint + const lines = StringPrototypeSplit(error.stack, '\n'); + const hint = `Did you mean to import ${found}?`; + error.stack = + ArrayPrototypeShift(lines) + '\n' + + hint + '\n' + + ArrayPrototypeJoin(lines, '\n'); + error.message += `\n${hint}`; + } + } + throw error; + } + + if (isMain ? !preserveSymlinksMain : !preserveSymlinks) { + const urlPath = fileURLToPath(url); + const real = realpathSync(urlPath, { + [internalFS.realpathCacheKey]: realpathCache + }); + const old = url; + url = pathToFileURL( + real + (StringPrototypeEndsWith(urlPath, sep) ? '/' : '')); + url.search = old.search; + url.hash = old.hash; + } + + return { url: `${url}` }; +} + +module.exports = { + DEFAULT_CONDITIONS, + defaultResolve, + encodedSepRegEx, + getPackageType, + packageExportsResolve, + packageImportsResolve +}; diff --git a/scripts/build-pack.js b/scripts/build-pack.js index ac5e19768..5ee399b58 100644 --- a/scripts/build-pack.js +++ b/scripts/build-pack.js @@ -23,7 +23,10 @@ exec( console.error(err); process.exit(1); } - const tempTarballPath = join(tempDir, readdirSync(tempDir)[0]); + const tempTarballPath = join( + tempDir, + readdirSync(tempDir).find((name) => name.endsWith('.tgz')) + ); writeFileSync(tarballPath, readFileSync(tempTarballPath)); unlinkSync(tempTarballPath); rmdirSync(tempDir); diff --git a/src/child/child-loader.ts b/src/child/child-loader.ts index 0ac018132..e65ffe2fe 100644 --- a/src/child/child-loader.ts +++ b/src/child/child-loader.ts @@ -1,7 +1,3 @@ -// TODO same version check as ESM loader, but export stubs -// Also export a binder function that allows re-binding where the stubs -// delegate. - import type { NodeLoaderHooksAPI1, NodeLoaderHooksAPI2 } from '..'; import { filterHooksByAPIVersion } from '../esm'; diff --git a/src/cjs-resolve-filename-hook.ts b/src/cjs-resolve-filename-hook.ts deleted file mode 100644 index 9c6f66b02..000000000 --- a/src/cjs-resolve-filename-hook.ts +++ /dev/null @@ -1,59 +0,0 @@ -import type Module = require('module'); -import type { Service } from '.'; - -/** @internal */ -export type ModuleConstructorWithInternals = typeof Module & { - _resolveFilename( - request: string, - parent?: Module, - isMain?: boolean, - options?: ModuleResolveFilenameOptions, - ...rest: any[] - ): string; - _preloadModules(requests?: string[]): void; -}; - -interface ModuleResolveFilenameOptions { - paths?: Array; -} - -/** - * @internal - */ -export function installCommonjsResolveHookIfNecessary(tsNodeService: Service) { - const Module = require('module') as ModuleConstructorWithInternals; - const originalResolveFilename = Module._resolveFilename; - const shouldInstallHook = tsNodeService.options.experimentalResolverFeatures; - if (shouldInstallHook) { - Module._resolveFilename = _resolveFilename; - } - function _resolveFilename( - this: any, - request: string, - parent?: Module, - isMain?: boolean, - options?: ModuleResolveFilenameOptions, - ...rest: any[] - ): string { - if (!tsNodeService.enabled()) - return originalResolveFilename.call( - this, - request, - parent, - isMain, - options, - ...rest - ); - - // This is a stub to support other pull requests that will be merged in the near future - // Right now, it does nothing. - return originalResolveFilename.call( - this, - request, - parent, - isMain, - options, - ...rest - ); - } -} diff --git a/src/cjs-resolve-hooks.ts b/src/cjs-resolve-hooks.ts new file mode 100644 index 000000000..9300d8f77 --- /dev/null +++ b/src/cjs-resolve-hooks.ts @@ -0,0 +1,67 @@ +import type Module = require('module'); +import type { Service } from '.'; + +/** @internal */ +export type ModuleConstructorWithInternals = typeof Module & { + _resolveFilename( + request: string, + parent?: Module, + isMain?: boolean, + options?: ModuleResolveFilenameOptions, + ...rest: any[] + ): string; + _preloadModules(requests?: string[]): void; + _findPath(request: string, paths: string[], isMain: boolean): string; +}; + +interface ModuleResolveFilenameOptions { + paths?: Array; +} + +/** + * @internal + */ +export function installCommonjsResolveHooksIfNecessary(tsNodeService: Service) { + const Module = require('module') as ModuleConstructorWithInternals; + const originalResolveFilename = Module._resolveFilename; + const originalFindPath = Module._findPath; + const shouldInstallHook = tsNodeService.options.experimentalResolver; + if (shouldInstallHook) { + const { Module_findPath, Module_resolveFilename } = + tsNodeService.getNodeCjsLoader(); + Module._resolveFilename = _resolveFilename; + Module._findPath = _findPath; + function _resolveFilename( + this: any, + request: string, + parent?: Module, + isMain?: boolean, + options?: ModuleResolveFilenameOptions, + ...rest: [] + ): string { + if (!tsNodeService.enabled()) + return originalResolveFilename.call( + this, + request, + parent, + isMain, + options, + ...rest + ); + + return Module_resolveFilename.call( + this, + request, + parent, + isMain, + options, + ...rest + ); + } + function _findPath(this: any): string { + if (!tsNodeService.enabled()) + return originalFindPath.apply(this, arguments as any); + return Module_findPath.apply(this, arguments as any); + } + } +} diff --git a/src/configuration.ts b/src/configuration.ts index 13b7ad28f..5142a3584 100644 --- a/src/configuration.ts +++ b/src/configuration.ts @@ -271,10 +271,6 @@ export function readConfig( // Remove resolution of "files". const files = rawApiOptions.files ?? tsNodeOptionsFromTsconfig.files ?? DEFAULTS.files; - if (!files) { - config.files = []; - config.include = []; - } // Only if a config file is *not* loaded, load an implicit configuration from @tsconfig/bases const skipDefaultCompilerOptions = configFilePath != null; @@ -309,7 +305,9 @@ export function readConfig( { fileExists, readFile, - readDirectory: ts.sys.readDirectory, + // Only used for globbing "files", "include", "exclude" + // When `files` option disabled, we want to avoid the fs calls + readDirectory: files ? ts.sys.readDirectory : () => [], useCaseSensitiveFileNames: ts.sys.useCaseSensitiveFileNames, }, basePath, @@ -382,8 +380,9 @@ function filterRecognizedTsConfigTsNodeOptions(jsonObject: any): { moduleTypes, experimentalReplAwait, swc, - experimentalResolverFeatures, + experimentalResolver, esm, + experimentalSpecifierResolution, ...unrecognized } = jsonObject as TsConfigOptions; const filteredTsConfigOptions = { @@ -407,8 +406,9 @@ function filterRecognizedTsConfigTsNodeOptions(jsonObject: any): { scopeDir, moduleTypes, swc, - experimentalResolverFeatures, + experimentalResolver, esm, + experimentalSpecifierResolution, }; // Use the typechecker to make sure this implementation has the correct set of properties const catchExtraneousProps: keyof TsConfigOptions = @@ -417,3 +417,40 @@ function filterRecognizedTsConfigTsNodeOptions(jsonObject: any): { null as any as keyof TsConfigOptions; return { recognized: filteredTsConfigOptions, unrecognized }; } + +/** @internal */ +export const ComputeAsCommonRootOfFiles = Symbol(); + +/** + * Some TS compiler options have defaults which are not provided by TS's config parsing functions. + * This function centralizes the logic for computing those defaults. + * @internal + */ +export function getTsConfigDefaults( + config: _ts.ParsedCommandLine, + basePath: string, + _files: string[] | undefined, + _include: string[] | undefined, + _exclude: string[] | undefined +) { + const { composite = false } = config.options; + let rootDir: string | typeof ComputeAsCommonRootOfFiles = + config.options.rootDir!; + if (rootDir == null) { + if (composite) rootDir = basePath; + // Return this symbol to avoid computing from `files`, which would require fs calls + else rootDir = ComputeAsCommonRootOfFiles; + } + const { outDir = rootDir } = config.options; + // Docs are wrong: https://www.typescriptlang.org/tsconfig#include + // Docs say **, but it's actually **/*; compiler throws error for ** + const include = _files ? [] : ['**/*']; + const files = _files ?? []; + // Docs are misleading: https://www.typescriptlang.org/tsconfig#exclude + // Docs say it excludes node_modules, bower_components, jspm_packages, but actually those are excluded via behavior of "include" + const exclude = _exclude ?? [outDir]; // TODO technically, outDir is absolute path, but exclude should be relative glob pattern? + + // TODO compute baseUrl + + return { rootDir, outDir, include, files, exclude, composite }; +} diff --git a/src/esm.ts b/src/esm.ts index a92210bb9..17bbc16a4 100644 --- a/src/esm.ts +++ b/src/esm.ts @@ -16,12 +16,6 @@ import { extname } from 'path'; import * as assert from 'assert'; import { normalizeSlashes } from './util'; import { createRequire } from 'module'; -const { - createResolve, -} = require('../dist-raw/node-internal-modules-esm-resolve'); -const { - defaultGetFormat, -} = require('../dist-raw/node-internal-modules-esm-get_format'); // Note: On Windows, URLs look like this: file:///D:/dev/@TypeStrong/ts-node-examples/foo.ts @@ -108,12 +102,7 @@ export interface NodeImportAssertions { } // The hooks API changed in node version X so we need to check for backwards compatibility. -// TODO: When the new API is backported to v12, v14, update these version checks accordingly. -const newHooksAPI = - versionGteLt(process.versions.node, '17.0.0') || - versionGteLt(process.versions.node, '16.12.0', '17.0.0') || - versionGteLt(process.versions.node, '14.999.999', '15.0.0') || - versionGteLt(process.versions.node, '12.999.999', '13.0.0'); +const newHooksAPI = versionGteLt(process.versions.node, '16.12.0'); /** @internal */ export function filterHooksByAPIVersion( @@ -139,10 +128,9 @@ export function createEsmHooks(tsNodeService: Service) { tsNodeService.enableExperimentalEsmLoaderInterop(); // Custom implementation that considers additional file extensions and automatically adds file extensions - const nodeResolveImplementation = createResolve({ - ...getExtensions(tsNodeService.config), - preferTsExts: tsNodeService.options.preferTsExts, - }); + const nodeResolveImplementation = tsNodeService.getNodeEsmResolver(); + const nodeGetFormatImplementation = tsNodeService.getNodeEsmGetFormat(); + const extensions = tsNodeService.extensions; const hooksAPI = filterHooksByAPIVersion({ resolve, @@ -180,7 +168,7 @@ export function createEsmHooks(tsNodeService: Service) { // See: https://github.com/nodejs/node/discussions/41711 // nodejs will likely implement a similar fallback. Till then, we can do our users a favor and fallback today. async function entrypointFallback( - cb: () => ReturnType + cb: () => ReturnType | Awaited> ): ReturnType { try { const resolution = await cb(); @@ -259,7 +247,13 @@ export function createEsmHooks(tsNodeService: Service) { // otherwise call the old getFormat() hook using node's old built-in defaultGetFormat() that ships with ts-node const format = context.format ?? - (await getFormat(url, context, defaultGetFormat)).format; + ( + await getFormat( + url, + context, + nodeGetFormatImplementation.defaultGetFormat + ) + ).format; let source = undefined; if (format !== 'builtin' && format !== 'commonjs') { @@ -337,12 +331,27 @@ export function createEsmHooks(tsNodeService: Service) { // If file has .ts, .tsx, or .jsx extension, then ask node how it would treat this file if it were .js const ext = extname(nativePath); let nodeSays: { format: NodeLoaderHooksFormat }; - if (ext !== '.js' && !tsNodeService.ignored(nativePath)) { + const nodeDoesNotUnderstandExt = + extensions.extensionsNodeDoesNotUnderstand.includes(ext); + const tsNodeIgnored = tsNodeService.ignored(nativePath); + if (nodeDoesNotUnderstandExt && !tsNodeIgnored) { nodeSays = await entrypointFallback(() => defer(formatUrl(pathToFileURL(nativePath + '.js'))) ); } else { - nodeSays = await entrypointFallback(defer); + try { + nodeSays = await entrypointFallback(defer); + } catch (e) { + if (e instanceof Error && tsNodeIgnored && nodeDoesNotUnderstandExt) { + e.message += + `\n\n` + + `Hint:\n` + + `ts-node is configured to ignore this file.\n` + + `If you want ts-node to handle this file, consider enabling the "skipIgnore" option or adjusting your "ignore" patterns.\n` + + `https://typestrong.org/ts-node/docs/scope\n`; + } + throw e; + } } // For files compiled by ts-node that node believes are either CJS or ESM, check if we should override that classification if ( diff --git a/src/index.ts b/src/index.ts index ff2186ba3..d38213fbd 100644 --- a/src/index.ts +++ b/src/index.ts @@ -12,6 +12,7 @@ import { cachedLookup, createProjectLocalResolveHelper, normalizeSlashes, + once, parse, ProjectLocalResolveHelper, split, @@ -26,9 +27,12 @@ import { import { createResolverFunctions } from './resolver-functions'; import type { createEsmHooks as createEsmHooksFn } from './esm'; import { - installCommonjsResolveHookIfNecessary, + installCommonjsResolveHooksIfNecessary, ModuleConstructorWithInternals, -} from './cjs-resolve-filename-hook'; +} from './cjs-resolve-hooks'; +import type * as _nodeInternalModulesEsmResolve from '../dist-raw/node-internal-modules-esm-resolve'; +import type * as _nodeInternalModulesEsmGetFormat from '../dist-raw/node-internal-modules-esm-get_format'; +import type * as _nodeInternalModulesCjsLoader from '../dist-raw/node-internal-modules-cjs-loader'; export { TSCommon }; export { @@ -96,8 +100,9 @@ let assertScriptCanLoadAsCJS: ( module: NodeJS.Module, filename: string ) => void = engineSupportsPackageTypeField - ? require('../dist-raw/node-internal-modules-cjs-loader') - .assertScriptCanLoadAsCJSImpl + ? ( + require('../dist-raw/node-internal-modules-cjs-loader') as typeof _nodeInternalModulesCjsLoader + ).assertScriptCanLoadAsCJSImpl : () => { /* noop */ }; @@ -408,7 +413,10 @@ export interface RegisterOptions extends CreateOptions { * * For details, see https://github.com/TypeStrong/ts-node/issues/1514 */ - experimentalResolverFeatures?: boolean; + experimentalResolver?: boolean; + + /** @internal */ + experimentalSpecifierResolution?: 'node' | 'explicit'; } /** @@ -532,6 +540,20 @@ export interface Service { transpileOnly: boolean; /** @internal */ projectLocalResolveHelper: ProjectLocalResolveHelper; + /** @internal */ + getNodeEsmResolver: () => ReturnType< + typeof import('../dist-raw/node-internal-modules-esm-resolve').createResolve + >; + /** @internal */ + getNodeEsmGetFormat: () => ReturnType< + typeof import('../dist-raw/node-internal-modules-esm-get_format').createGetFormat + >; + /** @internal */ + getNodeCjsLoader: () => ReturnType< + typeof import('../dist-raw/node-internal-modules-cjs-loader').createCjsLoader + >; + /** @internal */ + extensions: Extensions; } /** @@ -551,16 +573,57 @@ export interface DiagnosticFilter { diagnosticsIgnored: number[]; } -/** @internal */ -export function getExtensions(config: _ts.ParsedCommandLine) { - const tsExtensions = ['.ts']; - const jsExtensions = []; +/** + * Centralized specification of how we deal with file extensions based on + * project options: + * which ones we do/don't support, in what situations, etc. These rules drive + * logic elsewhere. + * @internal + * */ +export type Extensions = ReturnType; + +/** + * @internal + */ +export function getExtensions( + config: _ts.ParsedCommandLine, + options: RegisterOptions +) { + const compiledExtensions: string[] = []; + const extensionsNodeDoesNotUnderstand = [ + '.ts', + '.tsx', + '.jsx', + '.cts', + '.mts', + ]; + + // .js, .cjs, .mjs take precedence if preferTsExts is off + if (!options.preferTsExts && config.options.allowJs) + compiledExtensions.push('.js'); + + compiledExtensions.push('.ts'); // Enable additional extensions when JSX or `allowJs` is enabled. - if (config.options.jsx) tsExtensions.push('.tsx'); - if (config.options.allowJs) jsExtensions.push('.js'); - if (config.options.jsx && config.options.allowJs) jsExtensions.push('.jsx'); - return { tsExtensions, jsExtensions }; + if (config.options.jsx) compiledExtensions.push('.tsx'); + if (config.options.jsx && config.options.allowJs) + compiledExtensions.push('.jsx'); + if (config.options.preferTsExt && config.options.allowJs) + compiledExtensions.push('.js'); + + const compiledExtensionsNodeDoesNotUnderstand = + extensionsNodeDoesNotUnderstand.filter((ext) => + compiledExtensions.includes(ext) + ); + + return { + /** All file extensions we transform, ordered by resolution preference according to preferTsExts */ + compiledExtensions, + /** Resolved extensions that vanilla node will not understand; we should handle them */ + extensionsNodeDoesNotUnderstand, + /** Like the above, but only the ones we're compiling */ + compiledExtensionsNodeDoesNotUnderstand, + }; } /** @@ -586,8 +649,7 @@ export function register( } const originalJsHandler = require.extensions['.js']; - const { tsExtensions, jsExtensions } = getExtensions(service.config); - const extensions = [...tsExtensions, ...jsExtensions]; + const { compiledExtensions } = getExtensions(service.config, service.options); // Expose registered instance globally. process[REGISTER_INSTANCE] = service; @@ -595,12 +657,12 @@ export function register( // Register the extensions. registerExtensions( service.options.preferTsExts, - extensions, + compiledExtensions, service, originalJsHandler ); - installCommonjsResolveHookIfNecessary(service); + installCommonjsResolveHooksIfNecessary(service); // Require specified modules before start-up. (Module as ModuleConstructorWithInternals)._preloadModules( @@ -891,7 +953,7 @@ export function createFromPreloadedConfig( const rootFileNames = new Set(config.fileNames); const cachedReadFile = cachedLookup(debugFn('readFile', readFile)); - // Use language services by default (TODO: invert next major version). + // Use language services by default if (!options.compilerHost) { let projectVersion = 1; const fileVersions = new Map( @@ -1364,14 +1426,11 @@ export function createFromPreloadedConfig( let active = true; const enabled = (enabled?: boolean) => enabled === undefined ? active : (active = !!enabled); - const extensions = getExtensions(config); + const extensions = getExtensions(config, options); const ignored = (fileName: string) => { if (!active) return true; const ext = extname(fileName); - if ( - extensions.tsExtensions.includes(ext) || - extensions.jsExtensions.includes(ext) - ) { + if (extensions.compiledExtensions.includes(ext)) { return !isScoped(fileName) || shouldIgnore(fileName); } return true; @@ -1386,6 +1445,34 @@ export function createFromPreloadedConfig( }); } + const getNodeEsmResolver = once(() => + ( + require('../dist-raw/node-internal-modules-esm-resolve') as typeof _nodeInternalModulesEsmResolve + ).createResolve({ + ...extensions, + preferTsExts: options.preferTsExts, + tsNodeExperimentalSpecifierResolution: + options.experimentalSpecifierResolution, + }) + ); + const getNodeEsmGetFormat = once(() => + ( + require('../dist-raw/node-internal-modules-esm-get_format') as typeof _nodeInternalModulesEsmGetFormat + ).createGetFormat( + options.experimentalSpecifierResolution, + getNodeEsmResolver() + ) + ); + const getNodeCjsLoader = once(() => + ( + require('../dist-raw/node-internal-modules-cjs-loader') as typeof _nodeInternalModulesCjsLoader + ).createCjsLoader({ + ...extensions, + preferTsExts: options.preferTsExts, + nodeEsmResolver: getNodeEsmResolver(), + }) + ); + return { [TS_NODE_SERVICE_BRAND]: true, ts, @@ -1404,6 +1491,10 @@ export function createFromPreloadedConfig( enableExperimentalEsmLoaderInterop, transpileOnly, projectLocalResolveHelper, + getNodeEsmResolver, + getNodeEsmGetFormat, + getNodeCjsLoader, + extensions, }; } diff --git a/src/node-resolver-functions.ts.disabled b/src/node-resolver-functions.ts.disabled new file mode 100644 index 000000000..3d56b028e --- /dev/null +++ b/src/node-resolver-functions.ts.disabled @@ -0,0 +1,389 @@ +import { cachedLookup, normalizeSlashes } from './util'; +import type * as _ts from 'typescript'; +import { dirname, resolve } from 'path'; +import { getPatternFromSpec } from './ts-internals'; +import type { TSInternal } from './ts-compiler-types'; +import { getDefaultTsconfigJsonForNodeVersion } from './tsconfigs'; +import { + getTsConfigDefaults, + ComputeAsCommonRootOfFiles, +} from './configuration'; + +interface SrcOutPair { + preferSrc: boolean; + root: string; + out: string; +} +// interface RootDirsSet { +// rootDirs: string[]; +// } + +function contains(parentDirWithTrailingSlash: string, childDir: string) { + return childDir.startsWith(parentDirWithTrailingSlash); +} + +class SolutionResolver {} + +class ProjectResolver { + files: string[]; + includeRe: RegExp; + excludeRe: Regexp; + + constuctor( + ts: TSInternal, + tsconfig: _ts.ParsedCommandLine, + configFilePathOrCwd: string, + basePath: string, + files: string[] | undefined, + include: string[] | undefined, + exclude: string[] | undefined + ) { + // const configBaseDir = normalizeSlashes(dirname(configFilePathOrCwd)); + const { + rootDir, + include: includeSpecs, + files: filesArray, + exclude: excludeSpecs, + } = getTsConfigDefaults(tsconfig, basePath, files, include, exclude); + if (rootDir === ComputeAsCommonRootOfFiles) { + throw new Error( + 'Cannot determine rootDir if composite is not set. Either enable composite or set explicit rootDir' + ); + } + + this.files = filesArray.map((f) => normalizeSlashes(resolve(basePath, f))); + const reString = ts.getRegularExpressionForWildcard( + includeSpecs, + basePath, + 'files' + ); + this.includeRe = new RegExp(reString ?? '$^'); + const reString2 = ts.getRegularExpressionForWildcard( + excludeSpecs as string[], + basePath, + 'exclude' + ); + this.excludeRe = new RegExp(reString2 ?? '$^'); + } +} + +function createNodeResolverFunctions(opts: { + allowJs: boolean; + jsx: boolean; + cjsMjs: boolean; +}) { + const { allowJs, cjsMjs, jsx } = opts; + + const rootOutPairs: SrcOutPair[] = []; + // const rootDirsSets: RootDirsSet[] = []; + + /** + * Must be passed normalized slashes! + * Assumes root and out are different! + */ + function addRootOutPair(root: string, out: string, preferSrc = true) { + root = ensureTrailingSlash(root); + out = ensureTrailingSlash(out); + rootOutPairs.push({ root, out, preferSrc }); + } + + function ensureTrailingSlash(str: string) { + if (str.includes('\\')) + throw new Error('String must have normalized slashes'); + if (!str.endsWith('/')) str += '/'; + return str; + } + + // function mapFromOutToRoot(directory: string) { + // directory = ensureTrailingSlash(directory); + // for(const {out, root} of rootOutPairs) { + // if(directory.startsWith(out)) { + // return root + directory.slice(out.length); + // } + // } + // return directory; + // } + function mapFromRootToOut(directory: string) { + directory = ensureTrailingSlash(directory); + for (const { out, root } of rootOutPairs) { + if (directory.startsWith(root)) { + // TODO how to exclude node_modules from this mapping?? + // Check regexp patterns to see if this file is included? + return out + directory.slice(root.length); + } + } + return directory; + } + + // /** Must be passed normalized slashes! */ + // function addRootDirs(rootDirs: string[]) { + // rootDirsSets.push({ + // rootDirs: rootDirs.map(rootDir => rootDir.endsWith('/') ? rootDir : rootDir + '/') + // }); + // } + + const getAlternativeDirectoriesCached = cachedLookup( + getAlternativeDirectories + ); + + /** Get array of alternative directories to check because they're overlaid over each other */ + function* getAlternativeDirectories(directory: string) { + directory = ensureTrailingSlash(directory); + for (const { out, preferSrc, root } of rootOutPairs) { + if (contains(root, directory)) { + if (preferSrc) { + yield directory; // directory is in src; preferred + yield out + directory.slice(root.length); + } else { + yield out + directory.slice(root.length); + yield directory; + } + } else if (contains(out, directory)) { + if (preferSrc) { + yield root + directory.slice(out.length); + yield directory; // directory is in out + } else { + yield directory; + yield root + directory.slice(out.length); + } + } else { + yield directory; + } + } + // for(const rootDirsSet of rootDirsSets) { + // const alternatives2: string[] = []; + // for(const alternative of alternatives!) { + // ForRootDirsInSingleSet: + // for(const rootDir of rootDirsSet.rootDirs) { + // if(contains(rootDir, alternative)) { + // // alternative matches; replace it with each rootDir in the set + // for(const rootDir2 of rootDirsSet.rootDirs) { + // alternatives2.push(rootDir2 + alternative.slice(rootDir.length)); + // } + // break ForRootDirsInSingleSet; + // } + // } + // // alternative does not match; passthrough + // alternatives2.push(alternative); + // } + // alternatives = alternatives2; + // } + } + + // If extension is omitted and we are expected to add one, try these + const extensionlessExtensions = [ + 'js', + 'cjs', + 'mjs', + jsx && 'jsx', + 'ts', + 'cts', + 'mts', + jsx && 'tsx', + ]; + // If extension already specified, and is recognized, attempt these replacements + const jsExtensions = ['js', jsx && 'jsx', 'ts', jsx && 'tsx'].filter( + (v) => v + ) as string[]; + const cjsExtensions = ['cjs', 'cts']; + const mjsExtensions = ['mjs', 'mts']; + + /** + * Get alternative filenames to check because they're equivalent. + * + * Alternatives should only be attempted in: + * -- rootDir, if was able to map root<==>out + * -- otherwise attempt in dir, whatever it is. + */ + function* getAlternativeFilenames( + filename: string, + allowOmitFileExtension: boolean + ) { + // TODO be sure to avoid .d.ts, .d.mts, and .d.cts + const lastDotIndex = filename.lastIndexOf('.'); + let emittedReplacements = false; + if (lastDotIndex > 0) { + const endsInDts = + filename.endsWith('.d.ts') || + filename.endsWith('.d.cts') || + filename.endsWith('.d.mts'); + if (!endsInDts) { + const name = filename.slice(0, lastDotIndex); + const extension = filename.slice(lastDotIndex + 1); + const replacements = + extension === 'js' + ? jsExtensions + : extension === 'cjs' + ? cjsExtensions + : extension === 'mjs' + ? mjsExtensions + : undefined; + if (replacements) { + emittedReplacements = true; + for (const replacement of replacements) { + yield name + '.' + replacement; + } + } + } + } + if (!emittedReplacements) yield filename; + if (allowOmitFileExtension) { + for (const replacement of extensionlessExtensions) { + yield filename + '.' + replacement; + } + } + } + + return { + addRootOutPair, + getAlternativeDirectories, + getAlternativeDirectoriesCached, + getAlternativeFilenames, + }; +} + +/* +. +dist + +If rootDir matches any rootDirs entry: +- generate list of alternative rootDir +- map each to outDir + +foo +bar +baz + +For path foo/hello: + +foo/hello +bar/hello +baz/hello +dist/foo/hello +dist/bar/hello +dist/baz/hello + +For path node_modules/lodash +node_modules/lodash +dist/node_modules/lodash + +If directory is outside of common root of all mappings, skip + +If parent mappings were computed, how can they be augmented? +For each directory, a given mapping is either APPLIED, IRRELEVANT, or NOTYET +- src <-> out +- if any rootDirs are child of src or dist +*/ + +/* + +src/foo +dist/foo + +src/foo +src/bar +src/baz +dist/foo +dist/bar +dist/baz + +foo/src/lib +dist + +foo/src/lib +bar/src/lib +baz/src/lib +dist/lib + +outDir mapping: src/foo->dist +rootDirs mappings: ./foo, ./bar, ./baz +src/foo +src/bar +src/baz +dist + +outDir mapping: src->dist +rootDirs mappings: ./foo, ./bar, ./baz +src/foo +src/bar +src/baz +dist/foo +dist/bar +dist/baz + +expand src by rootDirs +then expand each by root->out mappings +*/ + +/* +For now, think about *only* rootDir<->outDir mappings +Sort all rootDir by length descending +Sort all outDir by length descending +Attempt mapping for each. +As soon as mapping succeeds for *any single entry*, stop attempting others. +*/ + +/* +rootDirs +src/foo +src/bar +src/baz + +preprocess to include rootDir<->outDir +src/foo +src/bar +src/baz +dist/foo +dist/bar +dist/baz +*/ + +/* +First must map importer from src to dist if possible. + +Then attempt import relative to the dist location, +checking dist<-src whenever possible. + +*/ + +// const ts: typeof _ts; + +// ts.createLanguageServiceSourceFile +// ts.createSourceFile +// ts.createUnparsedSourceFile +// ts.create + +/* +To use experimentalResolver: +- must set explicit rootDir, imply it with composite, or imply it with references + +When resolving an import + +##Step one: convert importer path to outDir if appropriate + +If importer is within rootDir & matched by include/files/exclude, map it to outDir +- Abandon mapping if `outDir` and `rootDir` identical +- Abandon mapping if import already within outDir +- sort by rootDir specificity so that workspaces are matched prior to ancestors +- mapping logic can be cached per __dirname&tsconfig pair, but must still consider if filename matches regexps + - allows supporting multiple tsconfigs with same rootDir +- allows respecting `path` mappings when we know which tsconfig governs the importer, because we have a single tsconfig to use for mappings + +##Step two: convert (many) target paths to rootDir if appropriate + +While resolving, we check many directories. +If resolution target is within `outDir`, attempt mapping to `rootDir` + - Abandon mapping if `outDir` and `rootDir` identical + - Abandon mapping if target already within `rootDir` + - Abandon mapping if is not matched by include/files/exclude + - HOW TO CHECK THIS BEFORE WE HAVE A FILE EXTENSION? For each include/files/exclude, generate a version that ignores file extensions? Test the directory with it? + - `TsconfigFilesMatcher.directoryChildMightMatch(dirname)` + - `TsconfigFilesMatcher.directoryAncestorMightMatch(dirname)` + + - OPTIMIZATION + - If none of the include / files patterns contain `node_modules`, and if target directory after basedir contains `node_modules`, then we *know* the entire + directory tree is excluded + - This optimization should apply to almost all projects + + - OPTIMIZATION detect when all file extensions are treated identically?? +*/ diff --git a/src/repl.ts b/src/repl.ts index 69d02425c..eed95a0d7 100644 --- a/src/repl.ts +++ b/src/repl.ts @@ -124,7 +124,6 @@ export interface CreateReplOptions { stderr?: NodeJS.WritableStream; /** @internal */ composeWithEvalAwarePartialHost?: EvalAwarePartialHost; - // TODO collapse both of the following two flags into a single `isInteractive` or `isLineByLine` flag. /** * @internal * Ignore diagnostics that are annoying when interactively entering input line-by-line. diff --git a/src/test/exec-helpers.ts b/src/test/exec-helpers.ts index bf0766475..c98c4c7ba 100644 --- a/src/test/exec-helpers.ts +++ b/src/test/exec-helpers.ts @@ -100,6 +100,7 @@ export function createSpawn>( } const defaultExec = createExec(); +export { defaultExec as exec }; export interface ExecTesterOptions { cmd: string; diff --git a/src/test/fs-helpers.ts b/src/test/fs-helpers.ts new file mode 100644 index 000000000..3f609a34d --- /dev/null +++ b/src/test/fs-helpers.ts @@ -0,0 +1,102 @@ +import { TEST_DIR } from './helpers'; +import * as fs from 'fs'; +import * as Path from 'path'; + +// Helpers to describe a bunch of files in a project programmatically, +// then write them to disk in a temp directory. + +export interface File { + path: string; + content: string; +} +export interface JsonFile extends File { + obj: T; +} +export interface DirectoryApi { + add(file: File): File; + addFile(...args: Parameters): File; + addJsonFile(...args: Parameters): JsonFile; + dir(dirPath: string, cb?: (dir: DirectoryApi) => void): DirectoryApi; +} + +export type ProjectAPI = ReturnType; + +export function file(path: string, content = '') { + return { path, content }; +} +export function jsonFile(path: string, obj: T) { + const file: JsonFile = { + path, + obj, + get content() { + return JSON.stringify(obj, null, 2); + }, + }; + return file; +} + +export function tempdirProject(name = '') { + const rootTmpDir = `${TEST_DIR}/tmp/`; + fs.mkdirSync(rootTmpDir, { recursive: true }); + const tmpdir = fs.mkdtempSync(`${TEST_DIR}/tmp/${name}`); + return projectInternal(tmpdir); +} +export type Project = ReturnType; +export function project(name: string) { + return projectInternal(`${TEST_DIR}/tmp/${name}`); +} + +function projectInternal(cwd: string) { + const files: File[] = []; + function write() { + for (const file of files) { + fs.mkdirSync(Path.dirname(file.path), { recursive: true }); + fs.writeFileSync(file.path, file.content); + } + } + function rm() { + try { + fs.rmdirSync(cwd, { recursive: true }); + } catch (err) { + if (fs.existsSync(cwd)) throw err; + } + } + const { add, addFile, addJsonFile, dir } = createDirectory(cwd); + function createDirectory( + dirPath: string, + cb?: (dir: DirectoryApi) => void + ): DirectoryApi { + function add(file: File) { + file.path = Path.join(dirPath, file.path); + files.push(file); + return file; + } + function addFile(...args: Parameters) { + return add(file(...args)); + } + function addJsonFile(...args: Parameters) { + return add(jsonFile(...args)) as JsonFile; + } + function dir(path: string, cb?: (dir: DirectoryApi) => void) { + return createDirectory(Path.join(dirPath, path), cb); + } + const _dir: DirectoryApi = { + add, + addFile, + addJsonFile, + dir, + }; + cb?.(_dir); + return _dir; + } + return { + cwd, + files: [], + dir, + add, + addFile, + addJsonFile, + write, + rm, + }; +} diff --git a/src/test/helpers.ts b/src/test/helpers.ts index 5856959f4..23b05d8e6 100644 --- a/src/test/helpers.ts +++ b/src/test/helpers.ts @@ -12,7 +12,7 @@ import type { Readable } from 'stream'; */ import type * as tsNodeTypes from '../index'; import type _createRequire from 'create-require'; -import { has, mapValues, once } from 'lodash'; +import { has, mapValues, once, sortBy } from 'lodash'; import semver = require('semver'); import type { ExecutionContext } from './testlib'; const createRequire: typeof _createRequire = require('create-require'); @@ -201,6 +201,10 @@ export function getStream(stream: Readable, waitForPattern?: string | RegExp) { //#region Reset node environment +// Delete any added by nyc that aren't in vanilla nodejs +for (const ext of Object.keys(require.extensions)) { + if (!['.js', '.json', '.node'].includes(ext)) delete require.extensions[ext]; +} const defaultRequireExtensions = captureObjectState(require.extensions); // Avoid node deprecation warning for accessing _channel const defaultProcess = captureObjectState(process, ['_channel']); @@ -223,7 +227,14 @@ export function resetNodeEnvironment() { sms.resetRetrieveHandlers(); // Modified by ts-node hooks - resetObject(require.extensions, defaultRequireExtensions); + resetObject( + require.extensions, + defaultRequireExtensions, + undefined, + undefined, + undefined, + true + ); // ts-node attaches a property when it registers an instance // source-map-support monkey-patches the emit function @@ -233,12 +244,15 @@ export function resetNodeEnvironment() { // 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 + // _resolveFilename et.al. are modified by ts-node, tsconfig-paths, source-map-support, yarn, maybe other things? resetObject(require('module'), defaultModule); // May be modified by REPL tests, since the REPL sets globals. // Avoid deleting nyc's coverage data. resetObject(global, defaultGlobal, ['__coverage__']); + + // Reset our ESM hooks + process.__test_setloader__?.(undefined); } function captureObjectState(object: any, avoidGetters: string[] = []) { @@ -258,7 +272,8 @@ function resetObject( state: ReturnType, doNotDeleteTheseKeys: string[] = [], doNotSetTheseKeys: string[] = [], - avoidSetterIfUnchanged: string[] = [] + avoidSetterIfUnchanged: string[] = [], + reorderProperties = false ) { const currentDescriptors = Object.getOwnPropertyDescriptors(object); for (const key of Object.keys(currentDescriptors)) { @@ -277,6 +292,19 @@ function resetObject( } // Reset descriptors Object.defineProperties(object, state.descriptors); + + if (reorderProperties) { + // Delete and re-define each property so that they are in original order + const originalOrder = Object.keys(state.descriptors); + const properties = Object.getOwnPropertyDescriptors(object); + const sortedKeys = sortBy(Object.keys(properties), (name) => + originalOrder.includes(name) ? originalOrder.indexOf(name) : 999 + ); + for (const key of sortedKeys) { + delete object[key]; + Object.defineProperty(object, key, properties[key]); + } + } } //#endregion diff --git a/src/test/index.spec.ts b/src/test/index.spec.ts index 707993853..5571a2480 100644 --- a/src/test/index.spec.ts +++ b/src/test/index.spec.ts @@ -69,11 +69,13 @@ test.suite('ts-node', (test) => { testsDirRequire.resolve('ts-node/register/transpile-only'); testsDirRequire.resolve('ts-node/register/type-check'); - // `node --loader ts-node/esm` - testsDirRequire.resolve('ts-node/esm'); - testsDirRequire.resolve('ts-node/esm.mjs'); - testsDirRequire.resolve('ts-node/esm/transpile-only'); - testsDirRequire.resolve('ts-node/esm/transpile-only.mjs'); + if (semver.gte(process.version, '12.17.0')) { + // `node --loader ts-node/esm` + testsDirRequire.resolve('ts-node/esm'); + testsDirRequire.resolve('ts-node/esm.mjs'); + testsDirRequire.resolve('ts-node/esm/transpile-only'); + testsDirRequire.resolve('ts-node/esm/transpile-only.mjs'); + } testsDirRequire.resolve('ts-node/transpilers/swc'); testsDirRequire.resolve('ts-node/transpilers/swc-experimental'); diff --git a/src/test/resolver.spec.ts b/src/test/resolver.spec.ts new file mode 100755 index 000000000..4dc5905cd --- /dev/null +++ b/src/test/resolver.spec.ts @@ -0,0 +1,688 @@ +import { context, ExecutionContext, TestInterface } from './testlib'; +import { ctxTsNode, resetNodeEnvironment, ts } from './helpers'; +import { project as fsProject, Project as FsProject } from './fs-helpers'; +import { join } from 'path'; +import * as semver from 'semver'; +import { padStart } from 'lodash'; +import _ = require('lodash'); +import { pathToFileURL } from 'url'; + +/* + * Each test case is a separate TS project, with a different permutation of + * project options. The project is written to disc, then ts-node is installed, + * then several entrypoint-* files are imported to test our resolver. + * + * High-level structure of these tests: + * package.json, tsconfig.json, src/, and out/ + * entrypoint-* files are the entrypoints + * they import a bunch of target files / directories / node_modules + * + * The heart of this test is every time an entrypoint imports a target. + * We are testing if the resolver figures out the correct target file to import. + * + * To better understand the emitted projects, run the tests, then look in `tests/tmp/resolver-*` + * + * Whenever a test fails, the error will log a command you can paste into your terminal to re-run + * that project *outside* of this test suite. This may be helpful in understanding and debugging + * these tests. + */ + +// Test a bunch of permutations of: + +// import permutations: + +// - [x] Relative import of file +// - [x] Relative import of index +// - [x] rootless library import of main +// - [x] rootless library import of index +// - [x] rootless library import of exports sub-path +// - [x] rootless self-import of main +// - [x] rootless self-import of index +// - [x] rootless self-import of exports sub-path + +// - [x] Require with extension +// - [x] Require without extension + +// - Require from dist to dist +// - Require from dist to src +// - Require from src to dist +// - [x] Require from src to src + +// lib permutations: + +// - [x] module exists in both src and dist (precompilation ran) +// - [x] module exists in only dist (came from elsewhere) +// - [x] module exists only in src (did not precompile) + +// - .ts / .js extension +// - .tsx / .js extension +// - .cts / .cjs extension +// - .mts / .mjs extension +// - .js / .js extension +// - .jsx / .js extension +// - .cjs / .cjs extension +// - .mjs / .mjs extension + +// Side-step compiler transformation of import() into require() +const dynamicImport = new Function('specifier', 'return import(specifier)'); + +const test = context(ctxTsNode); +type Test = TestInterface; +type T = ExecutionContext; + +const projectSeq = seqGenerator(); +const entrypointSeq = seqGenerator(); +const targetSeq = seqGenerator(); + +interface Project { + identifier: string; + allowJs: boolean; + preferSrc: boolean; + typeModule: boolean; + experimentalSpecifierResolutionNode: boolean; + skipIgnore: boolean; +} +type Entrypoint = string; +interface Target { + /** If true, is an index.* file within a directory */ + isIndex: boolean; + targetIdentifier: string; + outName: string; + srcName: string; + srcExt: string; + outExt: string; + inSrc: boolean; + inOut: boolean; + /** If true, should be imported as an npm package, not relative import */ + isPackage: boolean; + packageFlavor: ExternalPackageFlavor; + typeModule: boolean; +} +test.suite('Resolver hooks', (test) => { + test.runSerially(); + test.runIf( + semver.gte(process.version, '14.0.0') && + !semver.satisfies(ts.version, '2.7.x') + ); + + // + // Generate all permutations of projects + // + for (const allowJs of [false, true]) { + for (const preferSrc of [false, true]) { + for (const typeModule of [false, true]) { + for (const experimentalSpecifierResolutionNode of [false, true]) { + // TODO test against skipIgnore: false, where imports of third-party deps in `node_modules` should not get our mapping behaviors + for (const skipIgnore of [/*false, */ true]) { + const project: Project = { + identifier: `resolver-${projectSeq()}-${ + preferSrc ? 'preferSrc' : 'preferOut' + }-${typeModule ? 'typeModule' : 'typeCommonjs'}${ + allowJs ? '-allowJs' : '' + }${skipIgnore ? '-skipIgnore' : ''}${ + experimentalSpecifierResolutionNode + ? '-experimentalSpecifierResolutionNode' + : '' + }`, + allowJs, + preferSrc, + typeModule, + experimentalSpecifierResolutionNode, + skipIgnore, + }; + declareProject(test, project); + } + } + } + } + } +}); + +function declareProject(test: Test, project: Project) { + const { + allowJs, + experimentalSpecifierResolutionNode, + preferSrc, + typeModule, + skipIgnore, + } = project; + + test(`${project.identifier}`, async (t) => { + t.teardown(() => { + resetNodeEnvironment(); + }); + + const p = fsProject(project.identifier); + p.rm(); + + p.addJsonFile('package.json', { + type: project.typeModule ? 'module' : undefined, + }); + p.addJsonFile('tsconfig.json', { + 'ts-node': { + experimentalResolver: true, + preferTsExts: preferSrc, + transpileOnly: true, + experimentalSpecifierResolution: experimentalSpecifierResolutionNode + ? 'node' + : undefined, + skipIgnore, + }, + compilerOptions: { + allowJs, + skipLibCheck: true, + // TODO add nodenext permutation + module: typeModule ? 'esnext' : 'commonjs', + jsx: 'react', + }, + }); + + const targets = generateTargets(project, p); + const entrypoints = generateEntrypoints(project, p, targets); + p.write(); + await execute(t, p, entrypoints); + }); +} +type ExternalPackageFlavor = typeof externalPackageFlavors[number]; +const externalPackageFlavors = [ + false, + // test that the package contains /index.* + 'empty-manifest', + // "main": "src/target." + 'main-src-with-extension', + // "main": "src/target." + 'main-src-with-out-extension', + // "main": "out/target." + 'main-out-with-extension', + // "main": "src/target" + 'main-src-extensionless', + // "main": "out/target" + 'main-out-extensionless', + // "exports": {".": "src/target."} + 'exports-src-with-extension', + // "exports": {".": "src/target."} + 'exports-src-with-out-extension', + // "exports": {".": "out/target."} + 'exports-out-with-extension', +] as const; +function generateTargets(project: Project, p: FsProject) { + // + // Generate all target-* files + // + + /** Array of metadata about target files to be imported */ + const targets: Array = []; + // TODO does allowJs matter? + for (const inOut of [false, true]) { + for (const inSrc of [false, true]) { + for (const srcExt of [ + 'ts', + 'tsx', + 'cts', + 'mts', + 'jsx', + 'js', + 'cjs', + 'mjs', + ]) { + for (const externalPackageFlavor of externalPackageFlavors) { + const targetPackageTypeModulePermutations = externalPackageFlavor + ? [true, false] + : [project.typeModule]; + for (const targetPackageTypeModule of targetPackageTypeModulePermutations) { + const isIndexPermutations = externalPackageFlavor + ? [false] + : [true, false]; + // TODO test main pointing to a directory containing an `index.` file? + for (const isIndex of isIndexPermutations) { + //#region SKIPPING + if (!inSrc && !inOut) continue; + + // Don't bother with jsx if we don't have allowJs enabled + // TODO Get rid of this? "Just work" in this case? + if (srcExt === 'jsx' && !project.allowJs) continue; + // Don't bother with src-only extensions when only emitting to `out` + if (!inSrc && ['ts', 'tsx', 'cts', 'mts', 'jsx'].includes(srcExt)) + continue; + + // TODO re-enable with src <-> out mapping + if ( + !inOut && + isOneOf(externalPackageFlavor, [ + 'main-out-with-extension', + 'main-out-extensionless', + 'exports-out-with-extension', + ]) + ) + continue; + if ( + !inSrc && + isOneOf(externalPackageFlavor, [ + 'main-src-with-extension', + 'main-src-extensionless', + 'exports-src-with-extension', + ]) + ) + continue; + if ( + isOneOf(externalPackageFlavor, [ + 'main-out-with-extension', + 'main-out-extensionless', + 'exports-out-with-extension', + ]) + ) + continue; + //#endregion + + const outExt = srcExt.replace('ts', 'js').replace('x', ''); + let targetIdentifier = `target-${targetSeq()}-${ + inOut && inSrc ? 'inboth' : inOut ? 'onlyout' : 'onlysrc' + }-${srcExt}`; + + if (externalPackageFlavor) + targetIdentifier = `${targetIdentifier}-${externalPackageFlavor}-${ + targetPackageTypeModule ? 'module' : 'commonjs' + }`; + let prefix = externalPackageFlavor + ? `node_modules/${targetIdentifier}/` + : ''; + let suffix = + externalPackageFlavor === 'empty-manifest' + ? 'index' + : externalPackageFlavor + ? 'target' + : targetIdentifier; + if (isIndex) suffix += '-dir/index'; + const srcDirInfix = + externalPackageFlavor === 'empty-manifest' ? '' : 'src/'; + const outDirInfix = + externalPackageFlavor === 'empty-manifest' ? '' : 'out/'; + const srcName = `${prefix}${srcDirInfix}${suffix}.${srcExt}`; + const srcDirOutExtName = `${prefix}${srcDirInfix}${suffix}.${outExt}`; + const outName = `${prefix}${outDirInfix}${suffix}.${outExt}`; + const selfImporterCjsName = `${prefix}self-import-cjs.cjs`; + const selfImporterMjsName = `${prefix}self-import-mjs.mjs`; + const target: Target = { + srcName, + outName, + srcExt, + outExt, + inSrc, + inOut, + isIndex, + targetIdentifier, + isPackage: !!externalPackageFlavor, + packageFlavor: externalPackageFlavor, + typeModule: targetPackageTypeModule, + }; + targets.push(target); + const { isMjs: targetIsMjs } = fileInfo( + '.' + srcExt, + targetPackageTypeModule, + project.allowJs + ); + function targetContent(loc: string) { + let content = ''; + if (targetIsMjs) { + content += String.raw` + const {fileURLToPath} = await import('url'); + const filenameNative = fileURLToPath(import.meta.url); + export const directory = filenameNative.replace(/.*[\\\/](.*?)[\\\/]/, '$1'); + export const filename = filenameNative.replace(/.*[\\\/]/, ''); + export const targetIdentifier = '${targetIdentifier}'; + export const ext = filenameNative.replace(/.*\./, ''); + export const loc = '${loc}'; + `; + } else { + content += String.raw` + const filenameNative = __filename; + exports.filename = filenameNative.replace(/.*[\\\/]/, ''); + exports.directory = filenameNative.replace(/.*[\\\/](.*?)[\\\/].*/, '$1'); + exports.targetIdentifier = '${targetIdentifier}'; + exports.ext = filenameNative.replace(/.*\./, ''); + exports.loc = '${loc}'; + `; + } + return content; + } + if (inOut) { + p.addFile(outName, targetContent('out')); + // TODO so we can test multiple file extensions in a single directory, preferTsExt + p.addFile(srcDirOutExtName, targetContent('out')); + } + if (inSrc) { + p.addFile(srcName, targetContent('src')); + } + if (externalPackageFlavor) { + p.addFile( + selfImporterCjsName, + ` + module.exports = require("${targetIdentifier}"); + ` + ); + p.addFile( + selfImporterMjsName, + ` + export * from "${targetIdentifier}"; + ` + ); + function writePackageJson(obj: any) { + p.addJsonFile(`${prefix}/package.json`, { + name: targetIdentifier, + type: targetPackageTypeModule ? 'module' : undefined, + ...obj, + }); + } + switch (externalPackageFlavor) { + case 'empty-manifest': + writePackageJson({}); + break; + case 'exports-src-with-extension': + writePackageJson({ + exports: { + '.': `./src/${suffix}.${srcExt}`, + }, + }); + break; + case 'exports-src-with-out-extension': + writePackageJson({ + exports: { + '.': `./src/${suffix}.${outExt}`, + }, + }); + break; + case 'exports-out-with-extension': + writePackageJson({ + exports: { + '.': `./out/${suffix}.${outExt}`, + }, + }); + break; + case 'main-src-extensionless': + writePackageJson({ + main: `src/${suffix}`, + }); + break; + case 'main-out-extensionless': + writePackageJson({ + main: `out/${suffix}`, + }); + break; + case 'main-src-with-extension': + writePackageJson({ + main: `src/${suffix}.${srcExt}`, + }); + break; + case 'main-src-with-out-extension': + writePackageJson({ + main: `src/${suffix}.${outExt}`, + }); + break; + case 'main-out-with-extension': + writePackageJson({ + main: `src/${suffix}.${outExt}`, + }); + break; + default: + const _assert: never = externalPackageFlavor; + } + } + } + } + } + } + } + } + return targets; +} + +/** + * Generate all entrypoint-* files + */ +function generateEntrypoints( + project: Project, + p: FsProject, + targets: Target[] +) { + /** Array of entrypoint files to be imported during the test */ + let entrypoints: string[] = []; + for (const entrypointExt of ['cjs', 'mjs'] as const) { + const withExtPermutations = + entrypointExt == 'mjs' && + project.experimentalSpecifierResolutionNode === false + ? [true] + : [false, true]; + for (const withExt of withExtPermutations) { + // Location of the entrypoint + for (const entrypointLocation of ['src', 'out'] as const) { + // Target of the entrypoint's import statements + for (const entrypointTargetting of ['src', 'out'] as const) { + // TODO + if (entrypointLocation !== 'src') continue; + if (entrypointTargetting !== 'src') continue; + + const entrypointFilename = `entrypoint-${entrypointSeq()}-${entrypointLocation}-to-${entrypointTargetting}${ + withExt ? '-withext' : '' + }.${entrypointExt}`; + const { isMjs: entrypointIsMjs } = fileInfo( + entrypointFilename, + project.typeModule, + project.allowJs + ); + let entrypointContent = 'let mod;\n'; + if (entrypointIsMjs) { + entrypointContent += `import assert from 'assert';\n`; + } else { + entrypointContent += `const assert = require('assert');\n`; + } + + entrypoints.push(entrypointLocation + '/' + entrypointFilename); + for (const target of targets) { + // TODO re-enable these when we have outDir <-> rootDir mapping + if ( + target.srcName.includes('onlyout') && + entrypointTargetting === 'src' + ) + continue; + if ( + target.srcName.includes('onlysrc') && + //@ts-expect-error + entrypointTargetting === 'out' + ) + continue; + + const { + ext: targetSrcExt, + isMjs: targetIsMjs, + isCompiled: targetIsCompiled, + } = fileInfo(target.srcName, target.typeModule, project.allowJs); + const { ext: targetOutExt } = fileInfo( + target.outName, + project.typeModule, + project.allowJs + ); + + let targetExtPermutations = ['']; + if (!target.isPackage) { + if ( + // @ts-expect-error + entrypointTargetting === 'out' && + target.outExt !== target.srcExt + ) { + // TODO re-enable when we have out <-> src mapping + targetExtPermutations = [target.outExt]; + } else if (target.srcExt !== target.outExt) { + targetExtPermutations = [target.srcExt, target.outExt]; + } else { + targetExtPermutations = [target.srcExt]; + } + } + const externalPackageSelfImportPermutations = target.isPackage + ? [false, true] + : [false]; + for (const targetExt of targetExtPermutations) { + for (const externalPackageSelfImport of externalPackageSelfImportPermutations) { + entrypointContent += `\n// ${target.targetIdentifier}`; + if (target.isPackage) { + entrypointContent += ` node_modules package`; + if (externalPackageSelfImport) { + entrypointContent += ` self-import`; + } + } else { + entrypointContent += `.${targetExt}`; + } + entrypointContent += '\n'; + + // should specifier be relative or absolute? + let specifier: string; + if (externalPackageSelfImport) { + specifier = `../node_modules/${target.targetIdentifier}/self-import-${entrypointExt}.${entrypointExt}`; + } else if (target.isPackage) { + specifier = target.targetIdentifier; + } else { + if (entrypointTargetting === entrypointLocation) + specifier = './'; + else specifier = `../${entrypointTargetting}/`; + specifier += target.targetIdentifier; + if (target.isIndex) specifier += '-dir'; + if (!target.isIndex && withExt) specifier += '.' + targetExt; + } + + // Do not try to import mjs from cjs + if (targetIsMjs && entrypointExt === 'cjs') { + entrypointContent += `// skipping ${specifier} because we cannot import mjs from cjs\n`; + continue; + } + + // Do not try to import mjs or cjs without extension; node always requires these extensions, even in CommonJS. + if ( + !withExt && + (targetSrcExt === 'cjs' || targetSrcExt === 'mjs') + ) { + entrypointContent += `// skipping ${specifier} because we cannot omit extension from cjs or mjs files; node always requires them\n`; + continue; + } + + // Do not try to import a transpiled file if compiler options disagree with node's extension-based classification + if (targetIsCompiled && targetIsMjs && !project.typeModule) { + entrypointContent += `// skipping ${specifier} because it is compiled and compiler options disagree with node's module classification: extension=${targetSrcExt}, tsconfig module=commonjs\n`; + continue; + } + if (targetIsCompiled && !targetIsMjs && project.typeModule) { + entrypointContent += `// skipping ${specifier} because it is compiled and compiler options disagree with node's module classification: extension=${targetSrcExt}, tsconfig module=esnext\n`; + continue; + } + // Do not try to import cjs/mjs/cts/mts extensions because they are being added by a different pull request + if (['cts', 'mts', 'cjs', 'mjs'].includes(targetSrcExt)) { + entrypointContent += `// skipping ${specifier} because it uses a file extension that requires us to merge the relevant pull request\n`; + continue; + } + + // Do not try to import index from a directory if is forbidden by node's ESM resolver + if ( + entrypointIsMjs && + target.isIndex && + !project.experimentalSpecifierResolutionNode + ) { + entrypointContent += `// skipping ${specifier} because it relies on node automatically resolving a directory to index.*, but experimental-specifier-resolution is not enabled\n`; + continue; + } + + // NOTE: if you try to explicitly import foo.ts, we will load foo.ts, EVEN IF you have `preferTsExts` off + const assertIsSrcOrOut = !target.inSrc + ? 'out' + : !target.inOut + ? 'src' + : project.preferSrc || + (!target.isIndex && + targetExt === target.srcExt && + withExt) || + target.srcExt === target.outExt || // <-- TODO re-enable when we have src <-> out mapping + (target.isPackage && + isOneOf(target.packageFlavor, [ + 'main-src-with-extension', + 'exports-src-with-extension', + ])) + ? 'src' + : 'out'; + const assertHasExt = + assertIsSrcOrOut === 'src' ? target.srcExt : target.outExt; + + entrypointContent += + entrypointExt === 'cjs' + ? `mod = require('${specifier}');\n` + : `mod = await import('${specifier}');\n`; + entrypointContent += `assert.equal(mod.loc, '${assertIsSrcOrOut}');\n`; + entrypointContent += `assert.equal(mod.targetIdentifier, '${target.targetIdentifier}');\n`; + entrypointContent += `assert.equal(mod.ext, '${assertHasExt}');\n`; + } + } + } + function writeAssertions(specifier: string) {} + p.dir(entrypointLocation).addFile( + entrypointFilename, + entrypointContent + ); + } + } + } + } + return entrypoints; +} + +/** + * Assertions happen here + */ +async function execute(t: T, p: FsProject, entrypoints: Entrypoint[]) { + // + // Install ts-node and try to import all the index-* files + // + + const service = t.context.tsNodeUnderTest.register({ + projectSearchDir: p.cwd, + }); + process.__test_setloader__(t.context.tsNodeUnderTest.createEsmHooks(service)); + + for (const entrypoint of entrypoints) { + try { + await dynamicImport(pathToFileURL(join(p.cwd, entrypoint))); + } catch (e) { + throw new Error( + [ + (e as Error).message, + (e as Error).stack, + '', + 'This is an error in a resolver test. It might be easier to investigate by running outside of the test suite.', + 'To do that, try pasting this into your bash shell (windows invocation will be similar but maybe not identical):', + `( cd ${p.cwd} ; node --loader ../../../esm.mjs ./${entrypoint} )`, + ].join('\n') + ); + } + } +} + +function fileInfo(filename: string, typeModule: boolean, allowJs: boolean) { + const ext = filename.match(/\.(.*)$/)?.[1] ?? filename; + // ['ts', 'tsx', 'cts', 'mts', 'js', 'jsx', 'cjs', 'mjs'] + return { + ext, + isMjs: ['mts', 'mjs'].includes(ext) + ? true + : ['cts', 'cjs'].includes(ext) + ? false + : typeModule, + isCompiled: allowJs || ['ts', 'tsx', 'jsx', 'mts', 'cts'].includes(ext), + }; +} + +function seqGenerator() { + let next = 0; + return function () { + return padStart('' + next++, 4, '0'); + }; +} + +/** Essentially Array:includes, but with tweaked types for checks on enums */ +function isOneOf(value: V, arrayOfPossibilities: ReadonlyArray) { + return arrayOfPossibilities.includes(value as any); +} diff --git a/src/test/test-loader.d.ts b/src/test/test-loader.d.ts new file mode 100644 index 000000000..1d83a9976 --- /dev/null +++ b/src/test/test-loader.d.ts @@ -0,0 +1,8 @@ +export {}; +declare global { + namespace NodeJS { + interface Process { + __test_setloader__(hooks: any): void; + } + } +} diff --git a/src/test/test-loader.mjs b/src/test/test-loader.mjs new file mode 100644 index 000000000..b7fd7bbc8 --- /dev/null +++ b/src/test/test-loader.mjs @@ -0,0 +1,21 @@ +// Grant ourselves the ability to install ESM loader behaviors in-process during tests +import semver from 'semver'; + +const newHooksAPI = semver.gte(process.versions.node, '16.12.0'); + +let hooks = undefined; +process.__test_setloader__ = function (_hooks) { + hooks = _hooks; +}; +function createHook(name) { + return function (a, b, c) { + const target = (hooks && hooks[name]) || c; + return target(...arguments); + }; +} +export const resolve = createHook('resolve'); +export const load = newHooksAPI ? createHook('load') : null; +export const getFormat = !newHooksAPI ? createHook('getFormat') : null; +export const transformSource = !newHooksAPI + ? createHook('transformSource') + : null; diff --git a/src/ts-compiler-types.ts b/src/ts-compiler-types.ts index 0abaaec21..9c9d180d4 100644 --- a/src/ts-compiler-types.ts +++ b/src/ts-compiler-types.ts @@ -112,6 +112,17 @@ export interface TSInternal { ref: _ts.FileReference | string, containingFileMode: _ts.SourceFile['impliedNodeFormat'] ) => _ts.SourceFile['impliedNodeFormat']; + // TODO do we need these? Which TS version adds them? + getPatternFromSpec( + spec: string, + basePath: string, + usage: 'files' | 'directories' | 'exclude' + ): string | undefined; + getRegularExpressionForWildcard( + specs: readonly string[] | undefined, + basePath: string, + usage: 'files' | 'directories' | 'exclude' + ): string | undefined; } /** @internal */ export namespace TSInternal { diff --git a/src/util.ts b/src/util.ts index adc5eaca9..b8454ca26 100644 --- a/src/util.ts +++ b/src/util.ts @@ -156,3 +156,16 @@ export function getBasePathForProjectLocalDependencyResolution( // and we attempt to resolve relative specifiers. By the time we resolve relative specifiers, // should have configFilePath, so not reach this codepath. } + +/** @internal */ +export function once any>(fn: Fn) { + let value: ReturnType; + let ran = false; + function onceFn(...args: Parameters): ReturnType { + if (ran) return value; + value = fn(...args); + ran = true; + return value; + } + return onceFn; +} diff --git a/tests/.gitignore b/tests/.gitignore index 1f16711ae..33ac13eac 100644 --- a/tests/.gitignore +++ b/tests/.gitignore @@ -1,3 +1,4 @@ !from-node-modules/node_modules/ package-lock.json ts-node-packed.tgz +/tmp diff --git a/tests/package.json b/tests/package.json index 371b90084..1db06d541 100644 --- a/tests/package.json +++ b/tests/package.json @@ -3,5 +3,8 @@ "@swc/core": "latest", "ts-node": "file:ts-node-packed.tgz", "tslog": "3.2.2" + }, + "volta": { + "extends": "../package.json" } } diff --git a/tests/resolver/README.md b/tests/resolver/README.md new file mode 100644 index 000000000..50ec74246 --- /dev/null +++ b/tests/resolver/README.md @@ -0,0 +1,46 @@ +TODO must require explicit rootDir; do not allow to be inferred. +TODO resolve JSON if resolveJsonModules?? + +Test a bunch of permutations of: + +config permutations: + +- allowJs +- not allowJs + +- preferSrc +- not preferSrc + +import permutations: + +- Relative import of file +- Relative import of index +- rootless library import of main +- rootless library import of index +- rootless library import of exports sub-path +- rootless self-import of main +- rootless self-import of index +- rootless self-import of exports sub-path + + - Require with extension + - Require without extension + + - Require from dist to dist + - Require from dist to src + - Require from src to dist + - Require from src to src + +lib permutations: + +- module exists in both src and dist (precompilation ran) +- module exists in only dist (came from elsewhere) +- module exists only in src (did not precompile) + +- .ts / .js extension +- .tsx / .js extension +- .cts / .cjs extension +- .mts / .mjs extension +- .js / .js extension +- .jsx / .js extension +- .cjs / .cjs extension +- .mjs / .mjs extension diff --git a/tests/resolver/package.json b/tests/resolver/package.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/tests/resolver/package.json @@ -0,0 +1 @@ +{} diff --git a/tests/resolver/test.sh b/tests/resolver/test.sh new file mode 100644 index 000000000..a30080e33 --- /dev/null +++ b/tests/resolver/test.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env bash +set -euo pipefail + +cd project-preferSrc-typeModule diff --git a/tsconfig.build-dist-raw.json b/tsconfig.build-dist-raw.json new file mode 100644 index 000000000..82ee4b184 --- /dev/null +++ b/tsconfig.build-dist-raw.json @@ -0,0 +1,18 @@ +// This tsconfig.json is referenced by tsconfig.build-dist.json, a little trick to keep tsc happy. +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "composite": true, + "allowJs": true, + "checkJs": false, + "noResolve": true, + "declaration": true, + "noEmit": false, + "emitDeclarationOnly": true, + "rootDir": "dist-raw", + "outDir": "temp/dist-raw" + }, + "include": [ + "dist-raw/**/*" + ] +} diff --git a/tsconfig.build-dist.json b/tsconfig.build-dist.json new file mode 100644 index 000000000..8889d3d91 --- /dev/null +++ b/tsconfig.build-dist.json @@ -0,0 +1,18 @@ +// This tsconfig.json drives compiling the src/ directory into the dist/ directory +{ + "extends": "./tsconfig.json", + "references": [{ + "path": "./tsconfig.build-dist-raw.json" + }], + "compilerOptions": { + "noEmit": false, + "rootDir": "src", + "outDir": "dist", + "tsBuildInfoFile": "temp/tsconfig.build-dist.tsbuildinfo", + "allowJs": false, + "rootDirs": ["temp", "."] + }, + "include": [ + "src/**/*" + ] +} diff --git a/tsconfig.build-schema.json b/tsconfig.build-schema.json index adf48b4c9..f3f762f5a 100644 --- a/tsconfig.build-schema.json +++ b/tsconfig.build-schema.json @@ -1,6 +1,11 @@ +// This tsconfig.json is used when we generate the tsconfig JSON schema { "extends": "./tsconfig.json", "compilerOptions": { - "incremental": false + "incremental": false, + "tsBuildInfoFile": null, + "noEmit": false, + "allowJs": true, + "checkJs": false, } } diff --git a/tsconfig.json b/tsconfig.json index 2b2231bf2..0b879a942 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,11 +1,14 @@ +// This tsconfig.json drives Intellisense { "$schema": "./tsconfig.schemastore-schema.json", "compilerOptions": { // `target` and `lib` match @tsconfig/bases for node12, since that's the oldest node LTS, so it's the oldest node we support "target": "es2019", "lib": ["es2019", "es2020.promise", "es2020.bigint", "es2020.string", "dom"], - "rootDir": "src", - "outDir": "dist", + "rootDir": ".", + "outDir": "temp", + "tsBuildInfoFile": "temp/tsconfig.tsbuildinfo", + "noEmit": true, "module": "commonjs", "moduleResolution": "node", "strict": true, @@ -16,7 +19,12 @@ "stripInternal": true, "incremental": true, "skipLibCheck": true, - "importsNotUsedAsValues": "error" + "importsNotUsedAsValues": "error", + "allowJs": true, + + // Enable to assist in sanity-checking your changes to JS files, but note you will see many unrelated type errors! + // "checkJs": true, + // "noImplicitAny": false }, "include": [ "src/**/*" diff --git a/website/docs/options.md b/website/docs/options.md index 726741ad1..bcfce5563 100644 --- a/website/docs/options.md +++ b/website/docs/options.md @@ -367,7 +367,7 @@ Disable top-level await in REPL. Equivalent to node's [`--no-experimental-repl- *Default:* Enabled if TypeScript version is 3.8 or higher and target is ES2018 or higher.
*Environment:* `TS_NODE_EXPERIMENTAL_REPL_AWAIT` set `false` to disable -### experimentalResolverFeatures +### experimentalResolver Enable experimental features that re-map imports and require calls to support: `baseUrl`, `paths`, `rootDirs`, `.js` to `.ts` file extension mappings, `outDir` to `rootDir` mappings for composite projects and monorepos. For details, see [#1514](https://github.com/TypeStrong/ts-node/issues/1514)