diff --git a/.eslintignore b/.eslintignore index 40bd83b988..ac9d2b1105 100644 --- a/.eslintignore +++ b/.eslintignore @@ -3,3 +3,4 @@ cli/tests/error_syntax.js std/deno.d.ts std/prettier/vendor std/**/testdata/ +std/**/node_modules/ diff --git a/std/node/require.ts b/std/node/require.ts new file mode 100644 index 0000000000..22393a4184 --- /dev/null +++ b/std/node/require.ts @@ -0,0 +1,1189 @@ +// 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. + +import * as path from "../path/mod.ts"; +import { assert } from "../testing/asserts.ts"; + +const CHAR_FORWARD_SLASH = "/".charCodeAt(0); +const CHAR_BACKWARD_SLASH = "\\".charCodeAt(0); +const CHAR_COLON = ":".charCodeAt(0); + +const isWindows = path.isWindows; + +const relativeResolveCache = Object.create(null); + +let requireDepth = 0; +let statCache = null; + +type StatResult = -1 | 0 | 1; +// Returns 0 if the path refers to +// a file, 1 when it's a directory or < 0 on error. +function stat(filename: string): StatResult { + filename = path.toNamespacedPath(filename); + if (statCache !== null) { + const result = statCache.get(filename); + if (result !== undefined) return result; + } + try { + const info = Deno.statSync(filename); + const result = info.isFile() ? 0 : 1; + if (statCache !== null) statCache.set(filename, result); + return result; + } catch (e) { + return -1; + } +} + +function updateChildren(parent: Module, child: Module, scan: boolean): void { + const children = parent && parent.children; + if (children && !(scan && children.includes(child))) { + children.push(child); + } +} + +class Module { + id: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + exports: any; + parent?: Module; + filename: string; + loaded: boolean; + children: Module[]; + paths: string[]; + path: string; + constructor(id = "", parent?: Module) { + this.id = id; + this.exports = {}; + this.parent = parent; + updateChildren(parent, this, false); + this.filename = null; + this.loaded = false; + this.children = []; + this.paths = []; + this.path = path.dirname(id); + } + // TODO: populate this with polyfills! + static builtinModules: Module[] = []; + static _extensions: { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + [key: string]: (module: Module, filename: string) => any; + } = Object.create(null); + static _cache: { [key: string]: Module } = Object.create(null); + static _pathCache = Object.create(null); + static globalPaths: string[] = []; + // Proxy related code removed. + static wrapper = [ + "(function (exports, require, module, __filename, __dirname) { ", + "\n});" + ]; + + // Loads a module at the given file path. Returns that module's + // `exports` property. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + require(id: string): any { + if (id === "") { + throw new Error(`id '${id}' must be a non-empty string`); + } + requireDepth++; + try { + return Module._load(id, this, /* isMain */ false); + } finally { + requireDepth--; + } + } + + // Given a file name, pass it to the proper extension handler. + load(filename: string): void { + assert(!this.loaded); + this.filename = filename; + this.paths = Module._nodeModulePaths(path.dirname(filename)); + + const extension = findLongestRegisteredExtension(filename); + // Removed ESM code + Module._extensions[extension](this, filename); + this.loaded = true; + // Removed ESM code + } + + // 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. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + _compile(content: string, filename: string): any { + // manifest code removed + const compiledWrapper = wrapSafe(filename, content); + // inspector code remove + const dirname = path.dirname(filename); + const require = makeRequireFunction(this); + const exports = this.exports; + const thisValue = exports; + if (requireDepth === 0) { + statCache = new Map(); + } + const result = compiledWrapper.call( + thisValue, + exports, + require, + this, + filename, + dirname + ); + if (requireDepth === 0) { + statCache = null; + } + return result; + } + + static _resolveLookupPaths( + request: string, + parent: Module | null + ): string[] | null { + // Check for node modules paths. + if ( + request.charAt(0) !== "." || + (request.length > 1 && + request.charAt(1) !== "." && + request.charAt(1) !== "/" && + (!isWindows || request.charAt(1) !== "\\")) + ) { + let paths = modulePaths; + if (parent !== null && parent.paths && parent.paths.length) { + paths = parent.paths.concat(paths); + } + + return paths.length > 0 ? paths : null; + } + + // With --eval, parent.id is not set and 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 with eval there is no filename + const mainPaths = ["."].concat(Module._nodeModulePaths("."), modulePaths); + return mainPaths; + } + + const parentDir = [path.dirname(parent.filename)]; + return parentDir; + } + + static _resolveFilename( + request: string, + parent: Module, + isMain: boolean, + options?: { paths: string[] } + ): string { + // Native module code removed + let paths: string[]; + + if (typeof options === "object" && options !== null) { + if (Array.isArray(options.paths)) { + const isRelative = + request.startsWith("./") || + request.startsWith("../") || + (isWindows && request.startsWith(".\\")) || + request.startsWith("..\\"); + + 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 (!paths.includes(lookupPaths[j])) paths.push(lookupPaths[j]); + } + } + } + } else if (options.paths === undefined) { + paths = Module._resolveLookupPaths(request, parent); + } else { + throw new Error("options.paths is invalid"); + } + } else { + paths = Module._resolveLookupPaths(request, parent); + } + + // Look up the filename first, since that's the cache key. + const filename = Module._findPath(request, paths, isMain); + if (!filename) { + const requireStack = []; + for (let cursor = parent; cursor; cursor = cursor.parent) { + requireStack.push(cursor.filename || cursor.id); + } + let message = `Cannot find module '${request}'`; + if (requireStack.length > 0) { + message = message + "\nRequire stack:\n- " + requireStack.join("\n- "); + } + const err = new Error(message); + // @ts-ignore + err.code = "MODULE_NOT_FOUND"; + // @ts-ignore + err.requireStack = requireStack; + throw err; + } + return filename as string; + } + + static _findPath( + request: string, + paths: string[], + isMain: boolean + ): string | boolean { + const absoluteRequest = path.isAbsolute(request); + if (absoluteRequest) { + paths = [""]; + } else if (!paths || paths.length === 0) { + return false; + } + + const cacheKey = + request + "\x00" + (paths.length === 1 ? paths[0] : paths.join("\x00")); + const entry = Module._pathCache[cacheKey]; + if (entry) { + return entry; + } + + let exts; + let trailingSlash = + request.length > 0 && + request.charCodeAt(request.length - 1) === CHAR_FORWARD_SLASH; + if (!trailingSlash) { + trailingSlash = /(?:^|\/)\.?\.$/.test(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; + const basePath = resolveExports(curPath, request, absoluteRequest); + let filename; + + const rc = stat(basePath); + if (!trailingSlash) { + if (rc === 0) { + // File. + // preserveSymlinks removed + filename = toRealPath(basePath); + } + + if (!filename) { + // Try it with each of the extensions + if (exts === undefined) exts = Object.keys(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 = Object.keys(Module._extensions); + filename = tryPackage(basePath, exts, isMain, request); + } + + if (filename) { + Module._pathCache[cacheKey] = filename; + return filename; + } + } + // trySelf removed. + + return false; + } + + // 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. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + static _load(request: string, parent: Module, isMain: boolean): any { + let relResolveCacheIdentifier; + if (parent) { + // 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); + + const cachedModule = Module._cache[filename]; + if (cachedModule !== undefined) { + updateChildren(parent, cachedModule, true); + if (!cachedModule.loaded) + return getExportsForCircularRequire(cachedModule); + return cachedModule.exports; + } + + // Native module NOT supported. + + // Don't call updateChildren(), Module constructor already does. + const module = new Module(filename, parent); + + if (isMain) { + // TODO: set process info + // process.mainModule = module; + module.id = "."; + } + + Module._cache[filename] = module; + if (parent !== undefined) { + relativeResolveCache[relResolveCacheIdentifier] = filename; + } + + let threw = true; + try { + // Source map code removed + module.load(filename); + threw = false; + } finally { + if (threw) { + delete Module._cache[filename]; + if (parent !== undefined) { + delete relativeResolveCache[relResolveCacheIdentifier]; + } + } else if ( + module.exports && + Object.getPrototypeOf(module.exports) === + CircularRequirePrototypeWarningProxy + ) { + Object.setPrototypeOf(module.exports, PublicObjectPrototype); + } + } + + return module.exports; + } + + static wrap(script: string): string { + return `${Module.wrapper[0]}${script}${Module.wrapper[1]}`; + } + + static _nodeModulePaths(from: string): string[] { + if (isWindows) { + // 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 ( + from.charCodeAt(from.length - 1) === CHAR_BACKWARD_SLASH && + from.charCodeAt(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 = from.charCodeAt(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) paths.push(from.slice(0, last) + "\\node_modules"); + last = i; + p = 0; + } else if (p !== -1) { + if (nmChars[p] === code) { + ++p; + } else { + p = -1; + } + } + } + + return paths; + } else { + // posix + // 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 = from.charCodeAt(i); + if (code === CHAR_FORWARD_SLASH) { + if (p !== nmLen) paths.push(from.slice(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. + paths.push("/node_modules"); + + return paths; + } + } + + static createRequire(filename: string | URL): RequireFunction { + let filepath: string; + if ( + filename instanceof URL || + (typeof filename === "string" && !path.isAbsolute(filename)) + ) { + filepath = fileURLToPath(filename); + } else if (typeof filename !== "string") { + throw new Error("filename should be a string"); + } else { + filepath = filename; + } + return createRequireFromPath(filepath); + } + + static _initPaths(): void { + const homeDir = Deno.env("HOME"); + const nodePath = Deno.env("NODE_PATH"); + + // Removed $PREFIX/bin/node case + + let paths = []; + + if (homeDir) { + paths.unshift(path.resolve(homeDir, ".node_libraries")); + paths.unshift(path.resolve(homeDir, ".node_modules")); + } + + if (nodePath) { + paths = nodePath + .split(path.delimiter) + .filter(function pathsFilterCB(path) { + return !!path; + }) + .concat(paths); + } + + modulePaths = paths; + + // Clone as a shallow copy, for introspection. + Module.globalPaths = modulePaths.slice(0); + } + + static _preloadModules(requests: string[]): void { + if (!Array.isArray(requests)) { + return; + } + + // 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(Deno.cwd()); + } catch (e) { + if (e.code !== "ENOENT") { + throw e; + } + } + for (let n = 0; n < requests.length; n++) { + parent.require(requests[n]); + } + } +} + +let modulePaths = []; + +// 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. + +const packageJsonCache = new Map(); + +interface PackageInfo { + name?: string; + main?: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + exports?: any; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + type?: any; +} + +function readPackage(requestPath: string): PackageInfo | null { + const jsonPath = path.resolve(requestPath, "package.json"); + + const existing = packageJsonCache.get(jsonPath); + if (existing !== undefined) { + return existing; + } + + let json: string | undefined; + try { + json = new TextDecoder().decode( + Deno.readFileSync(path.toNamespacedPath(jsonPath)) + ); + } catch {} + + if (json === undefined) { + packageJsonCache.set(jsonPath, null); + return null; + } + + try { + const parsed = JSON.parse(json); + const filtered = { + name: parsed.name, + main: parsed.main, + exports: parsed.exports, + type: parsed.type + }; + packageJsonCache.set(jsonPath, filtered); + return filtered; + } catch (e) { + e.path = jsonPath; + e.message = "Error parsing " + jsonPath + ": " + e.message; + throw e; + } +} + +function readPackageScope( + checkPath +): { path: string; data: PackageInfo } | false { + const rootSeparatorIndex = checkPath.indexOf(path.sep); + let separatorIndex; + while ( + (separatorIndex = checkPath.lastIndexOf(path.sep)) > rootSeparatorIndex + ) { + checkPath = checkPath.slice(0, separatorIndex); + if (checkPath.endsWith(path.sep + "node_modules")) return false; + const pjson = readPackage(checkPath); + if (pjson) + return { + path: checkPath, + data: pjson + }; + } + return false; +} + +function readPackageMain(requestPath: string): string | undefined { + const pkg = readPackage(requestPath); + return pkg ? pkg.main : undefined; +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function readPackageExports(requestPath: string): any | undefined { + const pkg = readPackage(requestPath); + return pkg ? pkg.exports : undefined; +} + +function tryPackage( + requestPath: string, + exts: string[], + isMain: boolean, + _originalPath: string +): string | false { + const pkg = readPackageMain(requestPath); + + if (!pkg) { + return tryExtensions(path.resolve(requestPath, "index"), exts, isMain); + } + + const filename = path.resolve(requestPath, pkg); + let actual = + 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' + ); + // @ts-ignore + err.code = "MODULE_NOT_FOUND"; + throw err; + } + } + return actual; +} + +// 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: string, _isMain: boolean): string | false { + const rc = stat(requestPath); + return rc === 0 && toRealPath(requestPath); +} + +function toRealPath(requestPath: string): string { + // Deno does not have realpath implemented yet. + let fullPath = requestPath; + while (true) { + try { + fullPath = Deno.readlinkSync(fullPath); + } catch { + break; + } + } + return path.resolve(requestPath); +} + +// Given a path, check if the file exists with any of the set extensions +function tryExtensions( + p: string, + exts: string[], + isMain: boolean +): string | false { + for (let i = 0; i < exts.length; i++) { + const filename = tryFile(p + exts[i], isMain); + + if (filename) { + return filename; + } + } + return false; +} + +// Find the longest (possibly multi-dot) extension registered in +// Module._extensions +function findLongestRegisteredExtension(filename: string): string { + const name = path.basename(filename); + let currentExtension; + let index; + let startIndex = 0; + while ((index = name.indexOf(".", startIndex)) !== -1) { + startIndex = index + 1; + if (index === 0) continue; // Skip dotfiles like .gitignore + currentExtension = name.slice(index); + if (Module._extensions[currentExtension]) return currentExtension; + } + return ".js"; +} + +// --experimental-resolve-self trySelf() support removed. + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function isConditionalDotExportSugar(exports: any, _basePath: string): boolean { + if (typeof exports === "string") return true; + if (Array.isArray(exports)) return true; + if (typeof exports !== "object") return false; + let isConditional = false; + let firstCheck = true; + for (const key of Object.keys(exports)) { + const curIsConditional = key[0] !== "."; + if (firstCheck) { + firstCheck = false; + isConditional = curIsConditional; + } else if (isConditional !== curIsConditional) { + throw new Error( + '"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 isConditional; +} + +function applyExports(basePath: string, expansion: string): string { + const mappingKey = `.${expansion}`; + + let pkgExports = readPackageExports(basePath); + if (pkgExports === undefined || pkgExports === null) + return path.resolve(basePath, mappingKey); + + if (isConditionalDotExportSugar(pkgExports, basePath)) + pkgExports = { ".": pkgExports }; + + if (typeof pkgExports === "object") { + if (pkgExports.hasOwnProperty(mappingKey)) { + const mapping = pkgExports[mappingKey]; + return resolveExportsTarget( + pathToFileURL(basePath + "/"), + mapping, + "", + basePath, + mappingKey + ); + } + + // Fallback to CJS main lookup when no main export is defined + if (mappingKey === ".") return basePath; + + let dirMatch = ""; + for (const candidateKey of Object.keys(pkgExports)) { + if (candidateKey[candidateKey.length - 1] !== "/") continue; + if ( + candidateKey.length > dirMatch.length && + mappingKey.startsWith(candidateKey) + ) { + dirMatch = candidateKey; + } + } + + if (dirMatch !== "") { + const mapping = pkgExports[dirMatch]; + const subpath = mappingKey.slice(dirMatch.length); + return resolveExportsTarget( + pathToFileURL(basePath + "/"), + mapping, + subpath, + basePath, + mappingKey + ); + } + } + // Fallback to CJS main lookup when no main export is defined + if (mappingKey === ".") return basePath; + + const e = new Error( + `Package exports for '${basePath}' do not define ` + + `a '${mappingKey}' subpath` + ); + // @ts-ignore + e.code = "MODULE_NOT_FOUND"; + throw e; +} + +// This only applies to requests of a specific form: +// 1. name/.* +// 2. @scope/name/.* +const EXPORTS_PATTERN = /^((?:@[^/\\%]+\/)?[^./\\%][^/\\%]*)(\/.*)?$/; +function resolveExports( + nmPath: string, + request: string, + absoluteRequest: boolean +): string { + // The implementation's behavior is meant to mirror resolution in ESM. + if (!absoluteRequest) { + const [, name, expansion = ""] = request.match(EXPORTS_PATTERN) || []; + if (!name) { + return path.resolve(nmPath, request); + } + + const basePath = path.resolve(nmPath, name); + return applyExports(basePath, expansion); + } + + return path.resolve(nmPath, request); +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function resolveExportsTarget( + pkgPath: URL, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + target: any, + subpath: string, + basePath: string, + mappingKey: string +): string { + if (typeof target === "string") { + if ( + target.startsWith("./") && + (subpath.length === 0 || target.endsWith("/")) + ) { + const resolvedTarget = new URL(target, pkgPath); + const pkgPathPath = pkgPath.pathname; + const resolvedTargetPath = resolvedTarget.pathname; + if ( + resolvedTargetPath.startsWith(pkgPathPath) && + resolvedTargetPath.indexOf("/node_modules/", pkgPathPath.length - 1) === + -1 + ) { + const resolved = new URL(subpath, resolvedTarget); + const resolvedPath = resolved.pathname; + if ( + resolvedPath.startsWith(resolvedTargetPath) && + resolvedPath.indexOf("/node_modules/", pkgPathPath.length - 1) === -1 + ) { + return fileURLToPath(resolved); + } + } + } + } else if (Array.isArray(target)) { + for (const targetValue of target) { + if (Array.isArray(targetValue)) continue; + try { + return resolveExportsTarget( + pkgPath, + targetValue, + subpath, + basePath, + mappingKey + ); + } catch (e) { + if (e.code !== "MODULE_NOT_FOUND") throw e; + } + } + } else if (typeof target === "object" && target !== null) { + // removed experimentalConditionalExports + if (target.hasOwnProperty("default")) { + try { + return resolveExportsTarget( + pkgPath, + target.default, + subpath, + basePath, + mappingKey + ); + } catch (e) { + if (e.code !== "MODULE_NOT_FOUND") throw e; + } + } + } + let e: Error; + if (mappingKey !== ".") { + e = new Error( + `Package exports for '${basePath}' do not define a ` + + `valid '${mappingKey}' target${subpath ? " for " + subpath : ""}` + ); + } else { + e = new Error(`No valid exports main found for '${basePath}'`); + } + // @ts-ignore + e.code = "MODULE_NOT_FOUND"; + throw e; +} + +// 'node_modules' character codes reversed +const nmChars = [115, 101, 108, 117, 100, 111, 109, 95, 101, 100, 111, 110]; +const nmLen = nmChars.length; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function emitCircularRequireWarning(prop: any): void { + console.error( + `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-existend properties are accessed. +const CircularRequirePrototypeWarningProxy = new Proxy( + {}, + { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + get(target, prop): any { + if (prop in target) return target[prop]; + emitCircularRequireWarning(prop); + return undefined; + }, + + getOwnPropertyDescriptor(target, prop): PropertyDescriptor | undefined { + if (target.hasOwnProperty(prop)) + return Object.getOwnPropertyDescriptor(target, prop); + emitCircularRequireWarning(prop); + return undefined; + } + } +); + +// Object.prototype and ObjectProtoype refer to our 'primordials' versions +// and are not identical to the versions on the global object. +const PublicObjectPrototype = window.Object.prototype; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function getExportsForCircularRequire(module: Module): any { + if ( + module.exports && + Object.getPrototypeOf(module.exports) === PublicObjectPrototype && + // 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. + Object.setPrototypeOf(module.exports, CircularRequirePrototypeWarningProxy); + } + + return module.exports; +} + +type RequireWrapper = ( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + exports: any, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + require: any, + module: Module, + __filename: string, + __dirname: string +) => void; + +function wrapSafe(filename_: string, content: string): RequireWrapper { + // TODO: fix this + const wrapper = Module.wrap(content); + // @ts-ignore + const [f, err] = Deno.core.evalContext(wrapper); + if (err) { + throw err; + } + return f; + // ESM code removed. +} + +// Native extension for .js +Module._extensions[".js"] = (module: Module, filename: string): void => { + if (filename.endsWith(".js")) { + const pkg = readPackageScope(filename); + if (pkg !== false && pkg.data && pkg.data.type === "module") { + throw new Error("Importing ESM module"); + } + } + const content = new TextDecoder().decode(Deno.readFileSync(filename)); + module._compile(content, filename); +}; + +// Native extension for .json +Module._extensions[".json"] = (module: Module, filename: string): void => { + const content = new TextDecoder().decode(Deno.readFileSync(filename)); + // manifest code removed + try { + module.exports = JSON.parse(stripBOM(content)); + } catch (err) { + err.message = filename + ": " + err.message; + throw err; + } +}; + +// .node extension is not supported + +function createRequireFromPath(filename: string): RequireFunction { + // Allow a directory to be passed as the filename + const trailingSlash = + filename.endsWith("/") || (isWindows && filename.endsWith("\\")); + + 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); +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type Require = (id: string) => any; +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type RequireResolve = (request: string, options: any) => string; +interface RequireResolveFunction extends RequireResolve { + paths: (request: string) => string[] | null; +} + +interface RequireFunction extends Require { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + resolve: RequireResolveFunction; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + extensions: { [key: string]: (module: Module, filename: string) => any }; + cache: { [key: string]: Module }; +} + +function makeRequireFunction(mod: Module): RequireFunction { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const require = function require(path: string): any { + return mod.require(path); + }; + + function resolve(request: string, options?: { paths: string[] }): string { + return Module._resolveFilename(request, mod, false, options); + } + + require.resolve = resolve; + + function paths(request: string): string[] | null { + return Module._resolveLookupPaths(request, mod); + } + + resolve.paths = paths; + // TODO: set main + // 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: string): string { + if (content.charCodeAt(0) === 0xfeff) { + content = content.slice(1); + } + return content; +} + +const forwardSlashRegEx = /\//g; +const CHAR_LOWERCASE_A = "a".charCodeAt(0); +const CHAR_LOWERCASE_Z = "z".charCodeAt(0); + +function getPathFromURLWin32(url: URL): string { + // const hostname = url.hostname; + let pathname = url.pathname; + for (let n = 0; n < pathname.length; n++) { + if (pathname[n] === "%") { + const third = pathname.codePointAt(n + 2) | 0x20; + if ( + (pathname[n + 1] === "2" && third === 102) || // 2f 2F / + (pathname[n + 1] === "5" && third === 99) + ) { + // 5c 5C \ + throw new Error( + "Invalid file url path: must not include encoded \\ or / characters" + ); + } + } + } + pathname = pathname.replace(forwardSlashRegEx, "\\"); + pathname = decodeURIComponent(pathname); + // TODO: handle windows hostname case (needs bindings) + const letter = pathname.codePointAt(1) | 0x20; + const sep = pathname[2]; + if ( + letter < CHAR_LOWERCASE_A || + letter > CHAR_LOWERCASE_Z || // a..z A..Z + sep !== ":" + ) { + throw new Error("Invalid file URL path: must be absolute"); + } + return pathname.slice(1); +} + +function getPathFromURLPosix(url: URL): string { + if (url.hostname !== "") { + throw new Error("Invalid file URL host"); + } + const pathname = url.pathname; + for (let n = 0; n < pathname.length; n++) { + if (pathname[n] === "%") { + const third = pathname.codePointAt(n + 2) | 0x20; + if (pathname[n + 1] === "2" && third === 102) { + throw new Error( + "Invalid file URL path: must not include encoded / characters" + ); + } + } + } + return decodeURIComponent(pathname); +} + +function fileURLToPath(path: string | URL): string { + if (typeof path === "string") { + path = new URL(path); + } + if (path.protocol !== "file:") { + throw new Error("Protocol has to be file://"); + } + return isWindows ? getPathFromURLWin32(path) : getPathFromURLPosix(path); +} + +const percentRegEx = /%/g; +const backslashRegEx = /\\/g; +const newlineRegEx = /\n/g; +const carriageReturnRegEx = /\r/g; +const tabRegEx = /\t/g; +function pathToFileURL(filepath: string): URL { + let resolved = path.resolve(filepath); + // path.resolve strips trailing slashes so we must add them back + const filePathLast = filepath.charCodeAt(filepath.length - 1); + if ( + (filePathLast === CHAR_FORWARD_SLASH || + (isWindows && filePathLast === CHAR_BACKWARD_SLASH)) && + resolved[resolved.length - 1] !== path.sep + ) + resolved += "/"; + const outURL = new URL("file://"); + if (resolved.includes("%")) resolved = resolved.replace(percentRegEx, "%25"); + // In posix, "/" is a valid character in paths + if (!isWindows && resolved.includes("\\")) + resolved = resolved.replace(backslashRegEx, "%5C"); + if (resolved.includes("\n")) resolved = resolved.replace(newlineRegEx, "%0A"); + if (resolved.includes("\r")) + resolved = resolved.replace(carriageReturnRegEx, "%0D"); + if (resolved.includes("\t")) resolved = resolved.replace(tabRegEx, "%09"); + outURL.pathname = resolved; + return outURL; +} + +/** + * Create a `require` function that can be used to import CJS modules + * @param path path of this module + */ +function makeRequire(filePath: string): RequireFunction { + let mod: Module; + const fullPath = path.resolve(filePath); + if (fullPath in Module._cache) { + mod = Module._cache[fullPath]; + } else { + mod = new Module(fullPath); + } + return makeRequireFunction(mod); +} + +export { makeRequire }; diff --git a/std/node/require_test.ts b/std/node/require_test.ts new file mode 100644 index 0000000000..7cac6d6b2b --- /dev/null +++ b/std/node/require_test.ts @@ -0,0 +1,27 @@ +import { test } from "../testing/mod.ts"; +import { assertEquals, assert } from "../testing/asserts.ts"; +import { makeRequire } from "./require.ts"; + +const selfPath = window.unescape(import.meta.url.substring(7)); +// TS compiler would try to resolve if function named "require" +// Thus suffixing it with require_ to fix this... +const require_ = makeRequire(selfPath); + +test(function requireSuccess() { + const result = require_("./node/tests/cjs/cjs_a.js"); + assert("helloA" in result); + assert("helloB" in result); + assert("C" in result); + assert("leftPad" in result); + assertEquals(result.helloA(), "A"); + assertEquals(result.helloB(), "B"); + assertEquals(result.C, "C"); + assertEquals(result.leftPad("pad", 4), " pad"); +}); + +test(function requireCycle() { + const resultA = require_("./node/tests/cjs/cjs_cycle_a"); + const resultB = require_("./node/tests/cjs/cjs_cycle_b"); + assert(resultA); + assert(resultB); +}); diff --git a/std/node/tests/cjs/cjs_a.js b/std/node/tests/cjs/cjs_a.js new file mode 100644 index 0000000000..b8f69c8575 --- /dev/null +++ b/std/node/tests/cjs/cjs_a.js @@ -0,0 +1,10 @@ +/* eslint-disable */ +const { helloB } = require("./cjs_b.js"); +const C = require("./subdir/cjs_c"); +const leftPad = require("left-pad"); + +function helloA() { + return "A"; +} + +module.exports = { helloA, helloB, C, leftPad }; diff --git a/std/node/tests/cjs/cjs_b.js b/std/node/tests/cjs/cjs_b.js new file mode 100644 index 0000000000..17499012c3 --- /dev/null +++ b/std/node/tests/cjs/cjs_b.js @@ -0,0 +1,5 @@ +function helloB() { + return "B"; +} + +module.exports = { helloB }; diff --git a/std/node/tests/cjs/cjs_cycle_a.js b/std/node/tests/cjs/cjs_cycle_a.js new file mode 100644 index 0000000000..7a4e5a5f64 --- /dev/null +++ b/std/node/tests/cjs/cjs_cycle_a.js @@ -0,0 +1,3 @@ +module.exports = false; +require("./cjs_cycle_a"); +module.exports = true; diff --git a/std/node/tests/cjs/cjs_cycle_b.js b/std/node/tests/cjs/cjs_cycle_b.js new file mode 100644 index 0000000000..d85a1fc84f --- /dev/null +++ b/std/node/tests/cjs/cjs_cycle_b.js @@ -0,0 +1,3 @@ +module.exports = false; +require("./cjs_cycle_b"); +module.exports = true; diff --git a/std/node/tests/cjs/subdir/cjs_c.js b/std/node/tests/cjs/subdir/cjs_c.js new file mode 100644 index 0000000000..4e7d7ebe5e --- /dev/null +++ b/std/node/tests/cjs/subdir/cjs_c.js @@ -0,0 +1 @@ +module.exports = "C"; diff --git a/std/node/tests/node_modules/left-pad/README.md b/std/node/tests/node_modules/left-pad/README.md new file mode 100644 index 0000000000..e86ca7cc59 --- /dev/null +++ b/std/node/tests/node_modules/left-pad/README.md @@ -0,0 +1,36 @@ +## left-pad + +String left pad + +[![Build Status][travis-image]][travis-url] + +## Install + +```bash +$ npm install left-pad +``` + +## Usage + +```js +const leftPad = require('left-pad') + +leftPad('foo', 5) +// => " foo" + +leftPad('foobar', 6) +// => "foobar" + +leftPad(1, 2, '0') +// => "01" + +leftPad(17, 5, 0) +// => "00017" +``` + +**NOTE:** The third argument should be a single `char`. However the module doesn't throw an error if you supply more than one `char`s. See [#28](https://github.com/stevemao/left-pad/pull/28). + +**NOTE:** Characters having code points outside of [BMP plan](https://en.wikipedia.org/wiki/Plane_(Unicode)#Basic_Multilingual_Plane) are considered a two distinct characters. See [#58](https://github.com/stevemao/left-pad/issues/58). + +[travis-image]: https://travis-ci.org/stevemao/left-pad.svg?branch=master +[travis-url]: https://travis-ci.org/stevemao/left-pad diff --git a/std/node/tests/node_modules/left-pad/index.js b/std/node/tests/node_modules/left-pad/index.js new file mode 100644 index 0000000000..e90aec35d9 --- /dev/null +++ b/std/node/tests/node_modules/left-pad/index.js @@ -0,0 +1,52 @@ +/* This program is free software. It comes without any warranty, to + * the extent permitted by applicable law. You can redistribute it + * and/or modify it under the terms of the Do What The Fuck You Want + * To Public License, Version 2, as published by Sam Hocevar. See + * http://www.wtfpl.net/ for more details. */ +'use strict'; +module.exports = leftPad; + +var cache = [ + '', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ' +]; + +function leftPad (str, len, ch) { + // convert `str` to a `string` + str = str + ''; + // `len` is the `pad`'s length now + len = len - str.length; + // doesn't need to pad + if (len <= 0) return str; + // `ch` defaults to `' '` + if (!ch && ch !== 0) ch = ' '; + // convert `ch` to a `string` cuz it could be a number + ch = ch + ''; + // cache common use cases + if (ch === ' ' && len < 10) return cache[len] + str; + // `pad` starts with an empty string + var pad = ''; + // loop + while (true) { + // add `ch` to `pad` if `len` is odd + if (len & 1) pad += ch; + // divide `len` by 2, ditch the remainder + len >>= 1; + // "double" the `ch` so this operation count grows logarithmically on `len` + // each time `ch` is "doubled", the `len` would need to be "doubled" too + // similar to finding a value in binary search tree, hence O(log(n)) + if (len) ch += ch; + // `len` is 0, exit the loop + else break; + } + // pad `str`! + return pad + str; +} diff --git a/std/node/tests/node_modules/left-pad/package.json b/std/node/tests/node_modules/left-pad/package.json new file mode 100644 index 0000000000..57be042717 --- /dev/null +++ b/std/node/tests/node_modules/left-pad/package.json @@ -0,0 +1,68 @@ +{ + "_from": "left-pad", + "_id": "left-pad@1.3.0", + "_inBundle": false, + "_integrity": "sha512-XI5MPzVNApjAyhQzphX8BkmKsKUxD4LdyK24iZeQGinBN9yTQT3bFlCBy/aVx2HrNcqQGsdot8ghrjyrvMCoEA==", + "_location": "/left-pad", + "_phantomChildren": {}, + "_requested": { + "type": "tag", + "registry": true, + "raw": "left-pad", + "name": "left-pad", + "escapedName": "left-pad", + "rawSpec": "", + "saveSpec": null, + "fetchSpec": "latest" + }, + "_requiredBy": [ + "#USER", + "/" + ], + "_resolved": "https://registry.npmjs.org/left-pad/-/left-pad-1.3.0.tgz", + "_shasum": "5b8a3a7765dfe001261dde915589e782f8c94d1e", + "_spec": "left-pad", + "_where": "/Users/kun/Projects/Deno/deno/std/node/tests", + "author": { + "name": "azer" + }, + "bugs": { + "url": "https://github.com/stevemao/left-pad/issues" + }, + "bundleDependencies": false, + "deprecated": "use String.prototype.padStart()", + "description": "String left pad", + "devDependencies": { + "benchmark": "^2.1.0", + "fast-check": "0.0.8", + "tape": "*" + }, + "homepage": "https://github.com/stevemao/left-pad#readme", + "keywords": [ + "leftpad", + "left", + "pad", + "padding", + "string", + "repeat" + ], + "license": "WTFPL", + "main": "index.js", + "maintainers": [ + { + "name": "Cameron Westland", + "email": "camwest@gmail.com" + } + ], + "name": "left-pad", + "repository": { + "url": "git+ssh://git@github.com/stevemao/left-pad.git", + "type": "git" + }, + "scripts": { + "bench": "node perf/perf.js", + "test": "node test" + }, + "types": "index.d.ts", + "version": "1.3.0" +} diff --git a/std/node/tests/package.json b/std/node/tests/package.json new file mode 100644 index 0000000000..08c54d5884 --- /dev/null +++ b/std/node/tests/package.json @@ -0,0 +1,15 @@ +{ + "name": "deno_std_node", + "version": "0.0.1", + "description": "", + "main": "index.js", + "dependencies": { + "left-pad": "^1.3.0" + }, + "devDependencies": {}, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "", + "license": "ISC" +} diff --git a/tools/lint.py b/tools/lint.py index f387bc96e1..cbef32ad5e 100755 --- a/tools/lint.py +++ b/tools/lint.py @@ -41,7 +41,7 @@ def eslint(): # Find all *directories* in the main repo that contain .ts/.js files. source_files = git_ls_files(root_path, [ "*.js", "*.ts", ":!:std/prettier/vendor/*", ":!:std/**/testdata/*", - ":!:cli/compilers/*" + ":!:std/**/node_modules/*", ":!:cli/compilers/*" ]) source_dirs = set([os.path.dirname(f) for f in source_files]) # Within the source dirs, eslint does its own globbing, taking into account