From 87f80ff6be5e2a132b583a9c380fff5db3cb2b07 Mon Sep 17 00:00:00 2001 From: David Sherret Date: Sat, 20 Aug 2022 11:31:33 -0400 Subject: feat(unstable): initial support for npm specifiers (#15484) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bartek IwaƄczuk --- ext/node/01_node.js | 111 ++++++ ext/node/01_require.js | 824 ------------------------------------------- ext/node/02_require.js | 886 +++++++++++++++++++++++++++++++++++++++++++++++ ext/node/Cargo.toml | 2 + ext/node/errors.rs | 122 +++++++ ext/node/lib.rs | 380 +++++++++++++++----- ext/node/package_json.rs | 159 +++++++++ ext/node/resolution.rs | 696 +++++++++++++++++++++++++++++++++++++ 8 files changed, 2274 insertions(+), 906 deletions(-) create mode 100644 ext/node/01_node.js delete mode 100644 ext/node/01_require.js create mode 100644 ext/node/02_require.js create mode 100644 ext/node/errors.rs create mode 100644 ext/node/package_json.rs create mode 100644 ext/node/resolution.rs (limited to 'ext') diff --git a/ext/node/01_node.js b/ext/node/01_node.js new file mode 100644 index 000000000..80fccf843 --- /dev/null +++ b/ext/node/01_node.js @@ -0,0 +1,111 @@ +// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license. + +// deno-lint-ignore-file + +"use strict"; + +((window) => { + const { + ArrayPrototypePush, + ObjectEntries, + ObjectCreate, + } = window.__bootstrap.primordials; + + function assert(cond) { + if (!cond) { + throw Error("assert"); + } + } + + let initialized = false; + const nodeGlobals = {}; + const nodeGlobalThis = new Proxy(globalThis, { + get(_target, prop, _receiver) { + if (prop in nodeGlobals) { + return nodeGlobals[prop]; + } else { + return globalThis[prop]; + } + }, + set(_target, prop, value) { + if (prop in nodeGlobals) { + nodeGlobals[prop] = value; + } else { + globalThis[prop] = value; + } + return true; + }, + deleteProperty(_target, prop) { + let success = false; + if (prop in nodeGlobals) { + delete nodeGlobals[prop]; + success = true; + } + if (prop in globalThis) { + delete globalThis[prop]; + success = true; + } + return success; + }, + ownKeys(_target) { + const globalThisKeys = Reflect.ownKeys(globalThis); + const nodeGlobalsKeys = Reflect.ownKeys(nodeGlobals); + const nodeGlobalsKeySet = new Set(nodeGlobalsKeys); + return [ + ...ArrayPrototypeFilter( + globalThisKeys, + (k) => !nodeGlobalsKeySet.has(k), + ), + ...nodeGlobalsKeys, + ]; + }, + defineProperty(_target, prop, desc) { + if (prop in nodeGlobals) { + return Reflect.defineProperty(nodeGlobals, prop, desc); + } else { + return Reflect.defineProperty(globalThis, prop, desc); + } + }, + getOwnPropertyDescriptor(_target, prop) { + if (prop in nodeGlobals) { + return Reflect.getOwnPropertyDescriptor(nodeGlobals, prop); + } else { + return Reflect.getOwnPropertyDescriptor(globalThis, prop); + } + }, + has(_target, prop) { + return prop in nodeGlobals || prop in globalThis; + }, + }); + + const nativeModuleExports = ObjectCreate(null); + const builtinModules = []; + + function initialize(nodeModules) { + assert(!initialized); + initialized = true; + for (const [name, exports] of ObjectEntries(nodeModules)) { + nativeModuleExports[name] = exports; + ArrayPrototypePush(builtinModules, name); + } + nodeGlobals.Buffer = nativeModuleExports["buffer"].Buffer; + nodeGlobals.clearImmediate = nativeModuleExports["timers"].clearImmediate; + nodeGlobals.clearInterval = nativeModuleExports["timers"].clearInterval; + nodeGlobals.clearTimeout = nativeModuleExports["timers"].clearTimeout; + nodeGlobals.global = nodeGlobals; + nodeGlobals.process = nativeModuleExports["process"]; + nodeGlobals.setImmediate = nativeModuleExports["timers"].setImmediate; + nodeGlobals.setInterval = nativeModuleExports["timers"].setInterval; + nodeGlobals.setTimeout = nativeModuleExports["timers"].setTimeout; + } + + window.__bootstrap.internals = { + ...window.__bootstrap.internals ?? {}, + node: { + globalThis: nodeGlobalThis, + initialize, + nativeModuleExports, + builtinModules, + }, + }; +})(globalThis); diff --git a/ext/node/01_require.js b/ext/node/01_require.js deleted file mode 100644 index 27b71580d..000000000 --- a/ext/node/01_require.js +++ /dev/null @@ -1,824 +0,0 @@ -// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license. - -// deno-lint-ignore-file - -"use strict"; - -((window) => { - const { - ArrayIsArray, - ArrayPrototypeIncludes, - ArrayPrototypeIndexOf, - ArrayPrototypeJoin, - ArrayPrototypePush, - ArrayPrototypeSlice, - ArrayPrototypeSplice, - ObjectGetOwnPropertyDescriptor, - ObjectGetPrototypeOf, - ObjectEntries, - ObjectPrototypeHasOwnProperty, - ObjectSetPrototypeOf, - ObjectKeys, - ObjectPrototype, - ObjectCreate, - SafeMap, - SafeWeakMap, - JSONParse, - StringPrototypeEndsWith, - StringPrototypeIndexOf, - StringPrototypeSlice, - StringPrototypeStartsWith, - StringPrototypeCharCodeAt, - RegExpPrototypeTest, - } = window.__bootstrap.primordials; - const core = window.Deno.core; - const ops = core.ops; - - // Map used to store CJS parsing data. - const cjsParseCache = new SafeWeakMap(); - - function pathDirname(filepath) { - return ops.op_require_path_dirname(filepath); - } - - function pathResolve(...args) { - return ops.op_require_path_resolve(args); - } - - function assert(cond) { - if (!cond) { - throw Error("assert"); - } - } - - // TODO(bartlomieju): verify in other parts of this file that - // we have initialized the system before making APIs work - let cjsInitialized = false; - let processGlobal = null; - const nativeModulePolyfill = new SafeMap(); - const nativeModuleExports = ObjectCreate(null); - - const relativeResolveCache = ObjectCreate(null); - let requireDepth = 0; - let statCache = null; - let isPreloading = false; - let mainModule = null; - - function stat(filename) { - // TODO: required only on windows - // filename = path.toNamespacedPath(filename); - if (statCache !== null) { - const result = statCache.get(filename); - if (result !== undefined) { - return result; - } - } - const result = ops.op_require_stat(filename); - if (statCache !== null && result >= 0) { - statCache.set(filename, result); - } - - return result; - } - - function updateChildren(parent, child, scan) { - if (!parent) { - return; - } - - const children = parent.children; - if (children && !(scan && ArrayPrototypeIncludes(children, child))) { - ArrayPrototypePush(children, child); - } - } - - function tryFile(requestPath, _isMain) { - const rc = stat(requestPath); - if (rc !== 0) return; - return toRealPath(requestPath); - } - - function tryPackage(requestPath, exts, isMain, originalPath) { - // const pkg = readPackage(requestPath)?.main; - let pkg = false; - - if (!pkg) { - return tryExtensions( - pathResolve(requestPath, "index"), - exts, - isMain, - ); - } - - const filename = path.resolve(requestPath, pkg); - let actual = tryFile(filename, isMain) || - tryExtensions(filename, exts, isMain) || - tryExtensions( - pathResolve(filename, "index"), - exts, - isMain, - ); - if (actual === false) { - actual = tryExtensions( - pathResolve(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 = pathResolve( - requestPath, - "package.json", - ); - err.requestPath = originalPath; - throw err; - } else { - const jsonPath = pathResolve( - 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; - } - - const realpathCache = new SafeMap(); - function toRealPath(requestPath) { - const maybeCached = realpathCache.get(requestPath); - if (maybeCached) { - return maybeCached; - } - const rp = ops.op_require_real_path(requestPath); - realpathCache.set(requestPath, rp); - return rp; - } - - 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; - } - - // Find the longest (possibly multi-dot) extension registered in - // Module._extensions - function findLongestRegisteredExtension(filename) { - const name = ops.op_require_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 getExportsForCircularRequire(module) { - if ( - 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; - } - - function emitCircularRequireWarning(prop) { - processGlobal.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; - }, - }); - - const moduleParentCache = new SafeWeakMap(); - function Module(id = "", parent) { - this.id = id; - this.path = pathDirname(id); - this.exports = {}; - moduleParentCache.set(this, parent); - updateChildren(parent, this, false); - this.filename = null; - this.loaded = false; - this.children = []; - } - - const builtinModules = []; - Module.builtinModules = builtinModules; - - Module._extensions = Object.create(null); - Module._cache = Object.create(null); - Module._pathCache = Object.create(null); - let modulePaths = []; - Module.globalPaths = modulePaths; - - const CHAR_FORWARD_SLASH = 47; - const TRAILING_SLASH_REGEX = /(?:^|\/)\.?\.$/; - Module._findPath = function (request, paths, isMain) { - const absoluteRequest = ops.op_require_path_is_absolute(request); - if (absoluteRequest) { - paths = [""]; - } else if (!paths || paths.length === 0) { - return false; - } - - const cacheKey = request + "\x00" + ArrayPrototypeJoin(paths, "\x00"); - const entry = Module._pathCache[cacheKey]; - if (entry) { - return entry; - } - - let exts; - let trailingSlash = request.length > 0 && - StringPrototypeCharCodeAt(request, request.length - 1) === - CHAR_FORWARD_SLASH; - if (!trailingSlash) { - trailingSlash = RegExpPrototypeTest(TRAILING_SLASH_REGEX, 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 = pathResolve(curPath, request); - let filename; - - const rc = stat(basePath); - if (!trailingSlash) { - if (rc === 0) { // File. - if (!isMain) { - filename = toRealPath(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[cacheKey] = filename; - return filename; - } - } - - return false; - }; - - Module._nodeModulePaths = function (from) { - return ops.op_require_node_module_paths(from); - }; - - Module._resolveLookupPaths = function (request, parent) { - return ops.op_require_resolve_lookup_paths( - request, - parent?.paths, - parent?.filename ?? "", - ); - }; - - Module._load = function (request, parent, isMain) { - 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); - if (StringPrototypeStartsWith(filename, "node:")) { - // Slice 'node:' prefix - const id = StringPrototypeSlice(filename, 5); - - const module = loadNativeModule(id, id); - if (!module) { - // TODO: - // throw new ERR_UNKNOWN_BUILTIN_MODULE(filename); - throw new Error("Unknown built-in module"); - } - - 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 - ) { - return mod.exports; - } - - // Don't call updateChildren(), Module constructor already does. - const module = cachedModule || new Module(filename, parent); - - if (isMain) { - processGlobal.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 && - ObjectGetPrototypeOf(module.exports) === - CircularRequirePrototypeWarningProxy - ) { - ObjectSetPrototypeOf(module.exports, ObjectPrototype); - } - } - - return module.exports; - }; - - Module._resolveFilename = function ( - request, - parent, - isMain, - options, - ) { - if ( - StringPrototypeStartsWith(request, "node:") || - nativeModuleCanBeRequiredByUsers(request) - ) { - return request; - } - - let paths; - - if (typeof options === "object" && options !== null) { - if (ArrayIsArray(options.paths)) { - const isRelative = ops.op_require_is_request_relative( - 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 { - // TODO: - // throw new ERR_INVALID_ARG_VALUE("options.paths", options.paths); - throw new Error("Invalid arg value options.paths", options.path); - } - } else { - paths = Module._resolveLookupPaths(request, parent); - } - - if (parent?.filename) { - if (request[0] === "#") { - console.log("TODO: Module._resolveFilename with #specifier"); - // const pkg = readPackageScope(parent.filename) || {}; - // if (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 - // TODO(bartlomieju): make into a single op - const parentPath = ops.op_require_try_self_parent_path( - !!parent, - parent?.filename, - parent?.id, - ); - // const selfResolved = ops.op_require_try_self(parentPath, request); - const selfResolved = false; - 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; - }; - - Module.prototype.load = function (filename) { - assert(!this.loaded); - this.filename = filename; - this.paths = Module._nodeModulePaths( - pathDirname(filename), - ); - const extension = findLongestRegisteredExtension(filename); - // allow .mjs to be overriden - if ( - StringPrototypeEndsWith(filename, ".mjs") && !Module._extensions[".mjs"] - ) { - // TODO: use proper error class - throw new Error("require ESM", filename); - } - - Module._extensions[extension](this, filename); - this.loaded = true; - - // TODO: do caching - }; - - // Loads a module at the given file path. Returns that module's - // `exports` property. - Module.prototype.require = function (id) { - if (typeof id !== "string") { - // TODO(bartlomieju): it should use different error type - // ("ERR_INVALID_ARG_VALUE") - throw new TypeError("Invalid argument type"); - } - - if (id === "") { - // TODO(bartlomieju): it should use different error type - // ("ERR_INVALID_ARG_VALUE") - throw new TypeError("id must be non empty"); - } - requireDepth++; - try { - return Module._load(id, this, /* isMain */ false); - } finally { - requireDepth--; - } - }; - - Module.wrapper = [ - // TODO: - // We provide non standard timer APIs in the CommonJS wrapper - // to avoid exposing them in global namespace. - "(function (exports, require, module, __filename, __dirname, setTimeout, clearTimeout, setInterval, clearInterval) { (function (exports, require, module, __filename, __dirname) {", - "\n}).call(this, exports, require, module, __filename, __dirname); })", - ]; - Module.wrap = function (script) { - script = script.replace(/^#!.*?\n/, ""); - return `${Module.wrapper[0]}${script}${Module.wrapper[1]}`; - }; - - function enrichCJSError(error) { - if (error instanceof SyntaxError) { - if ( - error.message.includes( - "Cannot use import statement outside a module", - ) || - error.message.includes("Unexpected token 'export'") - ) { - console.error( - 'To load an ES module, set "type": "module" in the package.json or use ' + - "the .mjs extension.", - ); - } - } - } - - function wrapSafe( - filename, - content, - cjsModuleInstance, - ) { - const wrapper = Module.wrap(content); - const [f, err] = core.evalContext(wrapper, filename); - if (err) { - if (processGlobal.mainModule === cjsModuleInstance) { - enrichCJSError(err.thrown); - } - throw err.thrown; - } - return f; - } - - Module.prototype._compile = function (content, filename) { - const compiledWrapper = wrapSafe(filename, content, this); - - const dirname = pathDirname(filename); - const require = makeRequireFunction(this); - const exports = this.exports; - const thisValue = exports; - const module = this; - if (requireDepth === 0) { - statCache = new SafeMap(); - } - const result = compiledWrapper.call( - thisValue, - exports, - require, - this, - filename, - dirname, - ); - if (requireDepth === 0) { - statCache = null; - } - return result; - }; - - Module._extensions[".js"] = function (module, filename) { - const content = ops.op_require_read_file(filename); - - console.log(`TODO: Module._extensions[".js"] is ESM`); - - module._compile(content, filename); - }; - - function stripBOM(content) { - if (content.charCodeAt(0) === 0xfeff) { - content = content.slice(1); - } - return content; - } - - // Native extension for .json - Module._extensions[".json"] = function (module, filename) { - const content = ops.op_require_read_file(filename); - - 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) { - throw new Error("not implemented loading .node files"); - }; - - function createRequireFromPath(filename) { - const proxyPath = ops.op_require_proxy_path(filename); - const mod = new Module(proxyPath); - mod.filename = proxyPath; - mod.paths = Module._nodeModulePaths(mod.path); - return makeRequireFunction(mod); - } - - function makeRequireFunction(mod) { - const require = function require(path) { - return mod.require(path); - }; - - function resolve(request, options) { - return Module._resolveFilename(request, mod, false, options); - } - - require.resolve = resolve; - - function paths(request) { - return Module._resolveLookupPaths(request, mod); - } - - resolve.paths = paths; - require.main = mainModule; - // Enable support to add extra extension types. - require.extensions = Module._extensions; - require.cache = Module._cache; - - return require; - } - - function createRequire(filename) { - // FIXME: handle URLs and validation - return createRequireFromPath(filename); - } - - Module.createRequire = createRequire; - - Module._initPaths = function () { - const paths = ops.op_require_init_paths(); - modulePaths = paths; - Module.globalPaths = ArrayPrototypeSlice(modulePaths); - }; - - Module.syncBuiltinESMExports = function syncBuiltinESMExports() { - throw new Error("not implemented"); - }; - - Module.Module = Module; - - const m = { - _cache: Module._cache, - _extensions: Module._extensions, - _findPath: Module._findPath, - _initPaths: Module._initPaths, - _load: Module._load, - _nodeModulePaths: Module._nodeModulePaths, - _pathCache: Module._pathCache, - _preloadModules: Module._preloadModules, - _resolveFilename: Module._resolveFilename, - _resolveLookupPaths: Module._resolveLookupPaths, - builtinModules: Module.builtinModules, - createRequire: Module.createRequire, - globalPaths: Module.globalPaths, - Module, - wrap: Module.wrap, - }; - - nativeModuleExports.module = m; - - function loadNativeModule(_id, request) { - if (nativeModulePolyfill.has(request)) { - return nativeModulePolyfill.get(request); - } - const modExports = nativeModuleExports[request]; - if (modExports) { - const nodeMod = new Module(request); - nodeMod.exports = modExports; - nodeMod.loaded = true; - nativeModulePolyfill.set(request, nodeMod); - return nodeMod; - } - return undefined; - } - - function nativeModuleCanBeRequiredByUsers(request) { - return !!nativeModuleExports[request]; - } - - function initializeCommonJs(nodeModules, process) { - assert(!cjsInitialized); - cjsInitialized = true; - for (const [name, exports] of ObjectEntries(nodeModules)) { - nativeModuleExports[name] = exports; - ArrayPrototypePush(Module.builtinModules, name); - } - processGlobal = process; - } - - function readPackageScope() { - throw new Error("not implemented"); - } - - window.__bootstrap.internals = { - ...window.__bootstrap.internals ?? {}, - require: { - Module, - wrapSafe, - toRealPath, - cjsParseCache, - readPackageScope, - initializeCommonJs, - }, - }; -})(globalThis); diff --git a/ext/node/02_require.js b/ext/node/02_require.js new file mode 100644 index 000000000..1fcd0167c --- /dev/null +++ b/ext/node/02_require.js @@ -0,0 +1,886 @@ +// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license. + +// deno-lint-ignore-file + +"use strict"; + +((window) => { + const { + ArrayIsArray, + ArrayPrototypeIncludes, + ArrayPrototypeIndexOf, + ArrayPrototypeJoin, + ArrayPrototypePush, + ArrayPrototypeSlice, + ArrayPrototypeSplice, + FunctionPrototypeBind, + ObjectGetOwnPropertyDescriptor, + ObjectGetPrototypeOf, + ObjectPrototypeHasOwnProperty, + ObjectSetPrototypeOf, + ObjectKeys, + ObjectPrototype, + ObjectCreate, + SafeMap, + SafeWeakMap, + JSONParse, + StringPrototypeEndsWith, + StringPrototypeIndexOf, + StringPrototypeMatch, + StringPrototypeSlice, + StringPrototypeStartsWith, + StringPrototypeCharCodeAt, + RegExpPrototypeTest, + } = window.__bootstrap.primordials; + const core = window.Deno.core; + const ops = core.ops; + const { node } = window.__bootstrap.internals; + + // Map used to store CJS parsing data. + const cjsParseCache = new SafeWeakMap(); + + function pathDirname(filepath) { + return ops.op_require_path_dirname(filepath); + } + + function pathResolve(...args) { + return ops.op_require_path_resolve(args); + } + + function assert(cond) { + if (!cond) { + throw Error("assert"); + } + } + + const nativeModulePolyfill = new SafeMap(); + + const relativeResolveCache = ObjectCreate(null); + let requireDepth = 0; + let statCache = null; + let isPreloading = false; + let mainModule = null; + + function stat(filename) { + // TODO: required only on windows + // filename = path.toNamespacedPath(filename); + if (statCache !== null) { + const result = statCache.get(filename); + if (result !== undefined) { + return result; + } + } + const result = ops.op_require_stat(filename); + if (statCache !== null && result >= 0) { + statCache.set(filename, result); + } + + return result; + } + + function updateChildren(parent, child, scan) { + if (!parent) { + return; + } + + const children = parent.children; + if (children && !(scan && ArrayPrototypeIncludes(children, child))) { + ArrayPrototypePush(children, child); + } + } + + function tryFile(requestPath, _isMain) { + const rc = stat(requestPath); + if (rc !== 0) return; + return toRealPath(requestPath); + } + + function tryPackage(requestPath, exts, isMain, originalPath) { + const pkg = core.ops.op_require_read_package_scope(requestPath).main; + if (!pkg) { + return tryExtensions( + pathResolve(requestPath, "index"), + exts, + isMain, + ); + } + + const filename = pathResolve(requestPath, pkg); + let actual = tryFile(filename, isMain) || + tryExtensions(filename, exts, isMain) || + tryExtensions( + pathResolve(filename, "index"), + exts, + isMain, + ); + if (actual === false) { + actual = tryExtensions( + pathResolve(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 = pathResolve( + requestPath, + "package.json", + ); + err.requestPath = originalPath; + throw err; + } else { + const jsonPath = pathResolve( + requestPath, + "package.json", + ); + node.globalThis.process.emitWarning( + `Invalid 'main' field in '${jsonPath}' of '${pkg}'. ` + + "Please either fix that or report it to the module author", + "DeprecationWarning", + "DEP0128", + ); + } + } + return actual; + } + + const realpathCache = new SafeMap(); + function toRealPath(requestPath) { + const maybeCached = realpathCache.get(requestPath); + if (maybeCached) { + return maybeCached; + } + const rp = ops.op_require_real_path(requestPath); + realpathCache.set(requestPath, rp); + return rp; + } + + 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; + } + + // Find the longest (possibly multi-dot) extension registered in + // Module._extensions + function findLongestRegisteredExtension(filename) { + const name = ops.op_require_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 getExportsForCircularRequire(module) { + if ( + 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; + } + + function emitCircularRequireWarning(prop) { + node.globalThis.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; + }, + }); + + const moduleParentCache = new SafeWeakMap(); + function Module(id = "", parent) { + this.id = id; + this.path = pathDirname(id); + this.exports = {}; + moduleParentCache.set(this, parent); + updateChildren(parent, this, false); + this.filename = null; + this.loaded = false; + this.children = []; + } + + Module.builtinModules = node.builtinModules; + + Module._extensions = Object.create(null); + Module._cache = Object.create(null); + Module._pathCache = Object.create(null); + let modulePaths = []; + Module.globalPaths = modulePaths; + + const CHAR_FORWARD_SLASH = 47; + const TRAILING_SLASH_REGEX = /(?:^|\/)\.?\.$/; + const encodedSepRegEx = /%2F|%2C/i; + + 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 filename = resolved; + const actual = tryFile(filename, false); + if (actual) { + return actual; + } + throw new ERR_MODULE_NOT_FOUND( + filename, + path.resolve(pkgPath, "package.json"), + ); + } + + // This only applies to requests of a specific form: + // 1. name/.* + // 2. @scope/name/.* + const EXPORTS_PATTERN = /^((?:@[^/\\%]+\/)?[^./\\%][^/\\%]*)(\/.*)?$/; + function resolveExports(modulesPath, request, parentPath) { + // The implementation's behavior is meant to mirror resolution in ESM. + const [, name, expansion = ""] = + StringPrototypeMatch(request, EXPORTS_PATTERN) || []; + if (!name) { + return; + } + + return core.ops.op_require_resolve_exports( + modulesPath, + request, + name, + expansion, + parentPath, + ) ?? false; + } + + Module._findPath = function (request, paths, isMain, parentPath) { + const absoluteRequest = ops.op_require_path_is_absolute(request); + if (absoluteRequest) { + paths = [""]; + } else if (!paths || paths.length === 0) { + return false; + } + + const cacheKey = request + "\x00" + ArrayPrototypeJoin(paths, "\x00"); + const entry = Module._pathCache[cacheKey]; + if (entry) { + return entry; + } + + let exts; + let trailingSlash = request.length > 0 && + StringPrototypeCharCodeAt(request, request.length - 1) === + CHAR_FORWARD_SLASH; + if (!trailingSlash) { + trailingSlash = RegExpPrototypeTest(TRAILING_SLASH_REGEX, 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, parentPath); + if (exportsResolved) { + return exportsResolved; + } + } + + const isDenoDirPackage = Deno.core.opSync( + "op_require_is_deno_dir_package", + curPath, + ); + const isRelative = ops.op_require_is_request_relative( + request, + ); + // TODO(bartlomieju): could be a single op + const basePath = (isDenoDirPackage && !isRelative) + ? pathResolve(curPath, packageSpecifierSubPath(request)) + : pathResolve(curPath, request); + let filename; + + const rc = stat(basePath); + if (!trailingSlash) { + if (rc === 0) { // File. + 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[cacheKey] = filename; + return filename; + } + } + + return false; + }; + + Module._nodeModulePaths = function (from) { + return ops.op_require_node_module_paths(from); + }; + + Module._resolveLookupPaths = function (request, parent) { + const paths = []; + if (parent?.filename && parent.filename.length > 0) { + const denoDirPath = core.opSync( + "op_require_resolve_deno_dir", + request, + parent.filename, + ); + if (denoDirPath) { + paths.push(denoDirPath); + } + } + paths.push(...ops.op_require_resolve_lookup_paths( + request, + parent?.paths, + parent?.filename ?? "", + )); + return paths; + }; + + Module._load = function (request, parent, isMain) { + 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); + if (StringPrototypeStartsWith(filename, "node:")) { + // Slice 'node:' prefix + const id = StringPrototypeSlice(filename, 5); + + const module = loadNativeModule(id, id); + if (!module) { + // TODO: + // throw new ERR_UNKNOWN_BUILTIN_MODULE(filename); + throw new Error("Unknown built-in module"); + } + + return module.exports; + } + + const cachedModule = Module._cache[filename]; + if (cachedModule !== undefined) { + updateChildren(parent, cachedModule, true); + if (!cachedModule.loaded) { + return getExportsForCircularRequire(cachedModule); + } + return cachedModule.exports; + } + + const mod = loadNativeModule(filename, request); + if ( + mod + ) { + return mod.exports; + } + // Don't call updateChildren(), Module constructor already does. + const module = cachedModule || new Module(filename, parent); + + if (isMain) { + node.globalThis.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 && + ObjectGetPrototypeOf(module.exports) === + CircularRequirePrototypeWarningProxy + ) { + ObjectSetPrototypeOf(module.exports, ObjectPrototype); + } + } + + return module.exports; + }; + + Module._resolveFilename = function ( + request, + parent, + isMain, + options, + ) { + if ( + StringPrototypeStartsWith(request, "node:") || + nativeModuleCanBeRequiredByUsers(request) + ) { + return request; + } + + let paths; + + if (typeof options === "object" && options !== null) { + if (ArrayIsArray(options.paths)) { + const isRelative = ops.op_require_is_request_relative( + 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 { + // TODO: + // throw new ERR_INVALID_ARG_VALUE("options.paths", options.paths); + throw new Error("Invalid arg value options.paths", options.path); + } + } else { + paths = Module._resolveLookupPaths(request, parent); + } + + if (parent?.filename) { + if (request[0] === "#") { + const maybeResolved = core.ops.op_require_package_imports_resolve( + parent.filename, + request, + ); + if (maybeResolved) { + return maybeResolved; + } + } + } + + // Try module self resolution first + const parentPath = ops.op_require_try_self_parent_path( + !!parent, + parent?.filename, + parent?.id, + ); + const selfResolved = ops.op_require_try_self(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, + parentPath, + ); + 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; + }; + + Module.prototype.load = function (filename) { + assert(!this.loaded); + this.filename = filename; + this.paths = Module._nodeModulePaths( + pathDirname(filename), + ); + const extension = findLongestRegisteredExtension(filename); + // allow .mjs to be overriden + if ( + StringPrototypeEndsWith(filename, ".mjs") && !Module._extensions[".mjs"] + ) { + // TODO: use proper error class + throw new Error("require ESM", filename); + } + + Module._extensions[extension](this, filename); + this.loaded = true; + + // TODO: do caching + }; + + // Loads a module at the given file path. Returns that module's + // `exports` property. + Module.prototype.require = function (id) { + if (typeof id !== "string") { + // TODO(bartlomieju): it should use different error type + // ("ERR_INVALID_ARG_VALUE") + throw new TypeError("Invalid argument type"); + } + + if (id === "") { + // TODO(bartlomieju): it should use different error type + // ("ERR_INVALID_ARG_VALUE") + throw new TypeError("id must be non empty"); + } + requireDepth++; + try { + return Module._load(id, this, /* isMain */ false); + } finally { + requireDepth--; + } + }; + + Module.wrapper = [ + // TODO: + // We provide non standard timer APIs in the CommonJS wrapper + // to avoid exposing them in global namespace. + "(function (exports, require, module, __filename, __dirname, globalThis) { (function (exports, require, module, __filename, __dirname, globalThis, Buffer, clearImmediate, clearInterval, clearTimeout, global, process, setImmediate, setInterval, setTimeout) {", + "\n}).call(this, exports, require, module, __filename, __dirname, globalThis, globalThis.Buffer, globalThis.clearImmediate, globalThis.clearInterval, globalThis.clearTimeout, globalThis.global, globalThis.process, globalThis.setImmediate, globalThis.setInterval, globalThis.setTimeout); })", + ]; + Module.wrap = function (script) { + script = script.replace(/^#!.*?\n/, ""); + return `${Module.wrapper[0]}${script}${Module.wrapper[1]}`; + }; + + function enrichCJSError(error) { + if (error instanceof SyntaxError) { + if ( + error.message.includes( + "Cannot use import statement outside a module", + ) || + error.message.includes("Unexpected token 'export'") + ) { + console.error( + 'To load an ES module, set "type": "module" in the package.json or use ' + + "the .mjs extension.", + ); + } + } + } + + function wrapSafe( + filename, + content, + cjsModuleInstance, + ) { + const wrapper = Module.wrap(content); + const [f, err] = core.evalContext(wrapper, filename); + if (err) { + if (node.globalThis.process.mainModule === cjsModuleInstance) { + enrichCJSError(err.thrown); + } + throw err.thrown; + } + return f; + } + + Module.prototype._compile = function (content, filename) { + const compiledWrapper = wrapSafe(filename, content, this); + + const dirname = pathDirname(filename); + const require = makeRequireFunction(this); + const exports = this.exports; + const thisValue = exports; + const module = this; + if (requireDepth === 0) { + statCache = new SafeMap(); + } + const result = compiledWrapper.call( + thisValue, + exports, + require, + this, + filename, + dirname, + node.globalThis, + ); + if (requireDepth === 0) { + statCache = null; + } + return result; + }; + + Module._extensions[".js"] = function (module, filename) { + const content = ops.op_require_read_file(filename); + + if (StringPrototypeEndsWith(filename, ".js")) { + const pkg = core.ops.op_require_read_package_scope(filename); + if (pkg && pkg.exists && pkg.typ == "module") { + throw new Error( + `Import ESM module: ${filename} from ${module.parent.filename}`, + ); + } + } + + module._compile(content, filename); + }; + + function stripBOM(content) { + if (content.charCodeAt(0) === 0xfeff) { + content = content.slice(1); + } + return content; + } + + // Native extension for .json + Module._extensions[".json"] = function (module, filename) { + const content = ops.op_require_read_file(filename); + + 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) { + throw new Error("not implemented loading .node files"); + }; + + function createRequireFromPath(filename) { + const proxyPath = ops.op_require_proxy_path(filename); + const mod = new Module(proxyPath); + mod.filename = proxyPath; + mod.paths = Module._nodeModulePaths(mod.path); + return makeRequireFunction(mod); + } + + function makeRequireFunction(mod) { + const require = function require(path) { + return mod.require(path); + }; + + function resolve(request, options) { + return Module._resolveFilename(request, mod, false, options); + } + + require.resolve = resolve; + + function paths(request) { + return Module._resolveLookupPaths(request, mod); + } + + resolve.paths = paths; + require.main = mainModule; + // Enable support to add extra extension types. + require.extensions = Module._extensions; + require.cache = Module._cache; + + return require; + } + + function createRequire(filenameOrUrl) { + // FIXME: handle URLs and validation + const filename = core.opSync("op_require_as_file_path", filenameOrUrl); + return createRequireFromPath(filename); + } + + Module.createRequire = createRequire; + + Module._initPaths = function () { + const paths = ops.op_require_init_paths(); + modulePaths = paths; + Module.globalPaths = ArrayPrototypeSlice(modulePaths); + }; + + Module.syncBuiltinESMExports = function syncBuiltinESMExports() { + throw new Error("not implemented"); + }; + + Module.Module = Module; + + const m = { + _cache: Module._cache, + _extensions: Module._extensions, + _findPath: Module._findPath, + _initPaths: Module._initPaths, + _load: Module._load, + _nodeModulePaths: Module._nodeModulePaths, + _pathCache: Module._pathCache, + _preloadModules: Module._preloadModules, + _resolveFilename: Module._resolveFilename, + _resolveLookupPaths: Module._resolveLookupPaths, + builtinModules: Module.builtinModules, + createRequire: Module.createRequire, + globalPaths: Module.globalPaths, + Module, + wrap: Module.wrap, + }; + + node.nativeModuleExports.module = m; + + function loadNativeModule(_id, request) { + if (nativeModulePolyfill.has(request)) { + return nativeModulePolyfill.get(request); + } + const modExports = node.nativeModuleExports[request]; + if (modExports) { + const nodeMod = new Module(request); + nodeMod.exports = modExports; + nodeMod.loaded = true; + nativeModulePolyfill.set(request, nodeMod); + return nodeMod; + } + return undefined; + } + + function nativeModuleCanBeRequiredByUsers(request) { + return !!node.nativeModuleExports[request]; + } + + function readPackageScope() { + throw new Error("not implemented"); + } + + function bindExport(value, mod) { + // ensure exported functions are bound to their module object + if (typeof value === "function") { + return FunctionPrototypeBind(value, mod); + } else { + return value; + } + } + + /** @param specifier {string} */ + function packageSpecifierSubPath(specifier) { + let parts = specifier.split("/"); + if (parts[0].startsWith("@")) { + parts = parts.slice(2); + } else { + parts = parts.slice(1); + } + return parts.join("/"); + } + + window.__bootstrap.internals = { + ...window.__bootstrap.internals ?? {}, + require: { + Module, + wrapSafe, + toRealPath, + cjsParseCache, + readPackageScope, + bindExport, + }, + }; +})(globalThis); diff --git a/ext/node/Cargo.toml b/ext/node/Cargo.toml index c6975ee15..026b1fef4 100644 --- a/ext/node/Cargo.toml +++ b/ext/node/Cargo.toml @@ -15,3 +15,5 @@ path = "lib.rs" [dependencies] deno_core = { version = "0.147.0", path = "../../core" } +regex = "1" +serde = "1.0.136" diff --git a/ext/node/errors.rs b/ext/node/errors.rs new file mode 100644 index 000000000..8d1822f7b --- /dev/null +++ b/ext/node/errors.rs @@ -0,0 +1,122 @@ +// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license. + +use deno_core::error::generic_error; +use deno_core::error::type_error; +use deno_core::error::AnyError; + +pub fn err_invalid_module_specifier( + request: &str, + reason: &str, + maybe_base: Option, +) -> AnyError { + let mut msg = format!( + "[ERR_INVALID_MODULE_SPECIFIER] Invalid module \"{}\" {}", + request, reason + ); + + if let Some(base) = maybe_base { + msg = format!("{} imported from {}", msg, base); + } + + type_error(msg) +} + +#[allow(unused)] +pub fn err_invalid_package_config( + path: &str, + maybe_base: Option, + maybe_message: Option, +) -> AnyError { + let mut msg = format!( + "[ERR_INVALID_PACKAGE_CONFIG] Invalid package config {}", + path + ); + + if let Some(base) = maybe_base { + msg = format!("{} while importing {}", msg, base); + } + + if let Some(message) = maybe_message { + msg = format!("{}. {}", msg, message); + } + + generic_error(msg) +} + +#[allow(unused)] +pub fn err_module_not_found(path: &str, base: &str, typ: &str) -> AnyError { + generic_error(format!( + "[ERR_MODULE_NOT_FOUND] Cannot find {} \"{}\" imported from \"{}\"", + typ, path, base + )) +} + +pub fn err_invalid_package_target( + pkg_path: String, + key: String, + target: String, + is_import: bool, + maybe_base: Option, +) -> AnyError { + let rel_error = !is_import && !target.is_empty() && !target.starts_with("./"); + let mut msg = "[ERR_INVALID_PACKAGE_TARGET]".to_string(); + + if key == "." { + assert!(!is_import); + msg = format!("{} Invalid \"exports\" main target {} defined in the package config {}package.json", msg, target, pkg_path) + } else { + let ie = if is_import { "imports" } else { "exports" }; + msg = format!("{} Invalid \"{}\" target {} defined for '{}' in the package config {}package.json", msg, ie, target, key, pkg_path) + }; + + if let Some(base) = maybe_base { + msg = format!("{} imported from {}", msg, base); + }; + if rel_error { + msg = format!("{}; target must start with \"./\"", msg); + } + + generic_error(msg) +} + +pub fn err_package_path_not_exported( + pkg_path: String, + subpath: String, + maybe_base: Option, +) -> AnyError { + let mut msg = "[ERR_PACKAGE_PATH_NOT_EXPORTED]".to_string(); + + if subpath == "." { + msg = format!( + "{} No \"exports\" main defined in {}package.json", + msg, pkg_path + ); + } else { + msg = format!("{} Package subpath \'{}\' is not defined by \"exports\" in {}package.json", msg, subpath, pkg_path); + }; + + if let Some(base) = maybe_base { + msg = format!("{} imported from {}", msg, base); + } + + generic_error(msg) +} + +pub fn err_package_import_not_defined( + specifier: &str, + package_path: Option, + base: &str, +) -> AnyError { + let mut msg = format!( + "[ERR_PACKAGE_IMPORT_NOT_DEFINED] Package import specifier \"{}\" is not defined in", + specifier + ); + + if let Some(package_path) = package_path { + msg = format!("{} in package {}package.json", msg, package_path); + } + + msg = format!("{} imported from {}", msg, base); + + type_error(msg) +} diff --git a/ext/node/lib.rs b/ext/node/lib.rs index c61e27079..6be376e6d 100644 --- a/ext/node/lib.rs +++ b/ext/node/lib.rs @@ -4,22 +4,60 @@ use deno_core::error::AnyError; use deno_core::include_js_files; use deno_core::normalize_path; use deno_core::op; +use deno_core::url::Url; use deno_core::Extension; use deno_core::OpState; +use std::path::Path; use std::path::PathBuf; +use std::rc::Rc; + +pub use package_json::PackageJson; +pub use resolution::get_package_scope_config; +pub use resolution::legacy_main_resolve; +pub use resolution::package_exports_resolve; +pub use resolution::package_imports_resolve; +pub use resolution::package_resolve; +pub use resolution::DEFAULT_CONDITIONS; + +pub trait DenoDirNpmResolver { + fn resolve_package_folder_from_package( + &self, + specifier: &str, + referrer: &Path, + ) -> Result; + + fn resolve_package_folder_from_path( + &self, + path: &Path, + ) -> Result; + + fn in_npm_package(&self, path: &Path) -> bool; + + fn ensure_read_permission(&self, path: &Path) -> Result<(), AnyError>; +} + +mod errors; +mod package_json; +mod resolution; -pub struct Unstable(pub bool); +struct Unstable(pub bool); -pub fn init(unstable: bool) -> Extension { +pub fn init( + unstable: bool, + maybe_npm_resolver: Option>, +) -> Extension { Extension::builder() .js(include_js_files!( prefix "deno:ext/node", - "01_require.js", + "01_node.js", + "02_require.js", )) .ops(vec![ op_require_init_paths::decl(), op_require_node_module_paths::decl(), op_require_proxy_path::decl(), + op_require_is_deno_dir_package::decl(), + op_require_resolve_deno_dir::decl(), op_require_is_request_relative::decl(), op_require_resolve_lookup_paths::decl(), op_require_try_self_parent_path::decl(), @@ -31,9 +69,16 @@ pub fn init(unstable: bool) -> Extension { op_require_path_resolve::decl(), op_require_path_basename::decl(), op_require_read_file::decl(), + op_require_as_file_path::decl(), + op_require_resolve_exports::decl(), + op_require_read_package_scope::decl(), + op_require_package_imports_resolve::decl(), ]) .state(move |state| { state.put(Unstable(unstable)); + if let Some(npm_resolver) = maybe_npm_resolver.clone() { + state.put(npm_resolver); + } Ok(()) }) .build() @@ -48,60 +93,74 @@ fn check_unstable(state: &OpState) { } } +fn ensure_read_permission( + state: &mut OpState, + file_path: &Path, +) -> Result<(), AnyError> { + let resolver = { + let resolver = state.borrow::>(); + resolver.clone() + }; + resolver.ensure_read_permission(file_path) +} + #[op] pub fn op_require_init_paths(state: &mut OpState) -> Vec { check_unstable(state); - let (home_dir, node_path) = if cfg!(windows) { - ( - std::env::var("USERPROFILE").unwrap_or_else(|_| "".into()), - std::env::var("NODE_PATH").unwrap_or_else(|_| "".into()), - ) - } else { - ( - std::env::var("HOME").unwrap_or_else(|_| "".into()), - std::env::var("NODE_PATH").unwrap_or_else(|_| "".into()), - ) - }; - - let mut prefix_dir = std::env::current_exe().unwrap(); - if cfg!(windows) { - prefix_dir = prefix_dir.join("..").join("..") - } else { - prefix_dir = prefix_dir.join("..") - } + // todo(dsherret): this code is node compat mode specific and + // we probably don't want it for small mammal, so ignore it for now + + // let (home_dir, node_path) = if cfg!(windows) { + // ( + // std::env::var("USERPROFILE").unwrap_or_else(|_| "".into()), + // std::env::var("NODE_PATH").unwrap_or_else(|_| "".into()), + // ) + // } else { + // ( + // std::env::var("HOME").unwrap_or_else(|_| "".into()), + // std::env::var("NODE_PATH").unwrap_or_else(|_| "".into()), + // ) + // }; + + // let mut prefix_dir = std::env::current_exe().unwrap(); + // if cfg!(windows) { + // prefix_dir = prefix_dir.join("..").join("..") + // } else { + // prefix_dir = prefix_dir.join("..") + // } - let mut paths = vec![prefix_dir.join("lib").join("node")]; + // let mut paths = vec![prefix_dir.join("lib").join("node")]; - if !home_dir.is_empty() { - paths.insert(0, PathBuf::from(&home_dir).join(".node_libraries")); - paths.insert(0, PathBuf::from(&home_dir).join(".nod_modules")); - } + // if !home_dir.is_empty() { + // paths.insert(0, PathBuf::from(&home_dir).join(".node_libraries")); + // paths.insert(0, PathBuf::from(&home_dir).join(".nod_modules")); + // } - let mut paths = paths - .into_iter() - .map(|p| p.to_string_lossy().to_string()) - .collect(); - - if !node_path.is_empty() { - let delimiter = if cfg!(windows) { ";" } else { ":" }; - let mut node_paths: Vec = node_path - .split(delimiter) - .filter(|e| !e.is_empty()) - .map(|s| s.to_string()) - .collect(); - node_paths.append(&mut paths); - paths = node_paths; - } + // let mut paths = paths + // .into_iter() + // .map(|p| p.to_string_lossy().to_string()) + // .collect(); + + // if !node_path.is_empty() { + // let delimiter = if cfg!(windows) { ";" } else { ":" }; + // let mut node_paths: Vec = node_path + // .split(delimiter) + // .filter(|e| !e.is_empty()) + // .map(|s| s.to_string()) + // .collect(); + // node_paths.append(&mut paths); + // paths = node_paths; + // } - paths + vec![] } #[op] pub fn op_require_node_module_paths( state: &mut OpState, from: String, -) -> Vec { +) -> Result, AnyError> { check_unstable(state); // Guarantee that "from" is absolute. let from = deno_core::resolve_path(&from) @@ -109,6 +168,8 @@ pub fn op_require_node_module_paths( .to_file_path() .unwrap(); + ensure_read_permission(state, &from)?; + if cfg!(windows) { // return root node_modules when path is 'D:\\'. let from_str = from.to_str().unwrap(); @@ -117,14 +178,14 @@ pub fn op_require_node_module_paths( if bytes[from_str.len() - 1] == b'\\' && bytes[from_str.len() - 2] == b':' { let p = from_str.to_owned() + "node_modules"; - return vec![p]; + return Ok(vec![p]); } } } else { // 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.to_string_lossy() == "/" { - return vec!["/node_modules".to_string()]; + return Ok(vec!["/node_modules".to_string()]); } } @@ -144,7 +205,7 @@ pub fn op_require_node_module_paths( paths.push("/node_modules".to_string()); } - paths + Ok(paths) } #[op] @@ -171,11 +232,8 @@ fn op_require_is_request_relative( request: String, ) -> bool { check_unstable(state); - if request.starts_with("./") { - return true; - } - - if request.starts_with("../") { + if request.starts_with("./") || request.starts_with("../") || request == ".." + { return true; } @@ -192,6 +250,30 @@ fn op_require_is_request_relative( false } +#[op] +fn op_require_resolve_deno_dir( + state: &mut OpState, + request: String, + parent_filename: String, +) -> Option { + check_unstable(state); + let resolver = state.borrow::>(); + resolver + .resolve_package_folder_from_package( + &request, + &PathBuf::from(parent_filename), + ) + .ok() + .map(|p| p.to_string_lossy().to_string()) +} + +#[op] +fn op_require_is_deno_dir_package(state: &mut OpState, path: String) -> bool { + check_unstable(state); + let resolver = state.borrow::>(); + resolver.in_npm_package(&PathBuf::from(path)) +} + #[op] fn op_require_resolve_lookup_paths( state: &mut OpState, @@ -242,17 +324,19 @@ fn op_require_path_is_absolute(state: &mut OpState, p: String) -> bool { } #[op] -fn op_require_stat(state: &mut OpState, filename: String) -> i32 { +fn op_require_stat(state: &mut OpState, path: String) -> Result { check_unstable(state); - if let Ok(metadata) = std::fs::metadata(&filename) { + let path = PathBuf::from(path); + ensure_read_permission(state, &path)?; + if let Ok(metadata) = std::fs::metadata(&path) { if metadata.is_file() { - return 0; + return Ok(0); } else { - return 1; + return Ok(1); } } - -1 + Ok(-1) } #[op] @@ -261,7 +345,9 @@ fn op_require_real_path( request: String, ) -> Result { check_unstable(state); - let mut canonicalized_path = PathBuf::from(request).canonicalize()?; + let path = PathBuf::from(request); + ensure_read_permission(state, &path)?; + let mut canonicalized_path = path.canonicalize()?; if cfg!(windows) { canonicalized_path = PathBuf::from( canonicalized_path @@ -273,9 +359,7 @@ fn op_require_real_path( Ok(canonicalized_path.to_string_lossy().to_string()) } -#[op] -fn op_require_path_resolve(state: &mut OpState, parts: Vec) -> String { - check_unstable(state); +fn path_resolve(parts: Vec) -> String { assert!(!parts.is_empty()); let mut p = PathBuf::from(&parts[0]); if parts.len() > 1 { @@ -286,6 +370,12 @@ fn op_require_path_resolve(state: &mut OpState, parts: Vec) -> String { normalize_path(p).to_string_lossy().to_string() } +#[op] +fn op_require_path_resolve(state: &mut OpState, parts: Vec) -> String { + check_unstable(state); + path_resolve(parts) +} + #[op] fn op_require_path_dirname(state: &mut OpState, request: String) -> String { check_unstable(state); @@ -306,57 +396,183 @@ fn op_require_try_self_parent_path( has_parent: bool, maybe_parent_filename: Option, maybe_parent_id: Option, -) -> Option { +) -> Result, AnyError> { check_unstable(state); if !has_parent { - return None; + return Ok(None); } if let Some(parent_filename) = maybe_parent_filename { - return Some(parent_filename); + return Ok(Some(parent_filename)); } if let Some(parent_id) = maybe_parent_id { if parent_id == "" || parent_id == "internal/preload" { if let Ok(cwd) = std::env::current_dir() { - return Some(cwd.to_string_lossy().to_string()); + ensure_read_permission(state, &cwd)?; + return Ok(Some(cwd.to_string_lossy().to_string())); } } } - None + Ok(None) } #[op] fn op_require_try_self( state: &mut OpState, - has_parent: bool, - maybe_parent_filename: Option, - maybe_parent_id: Option, -) -> Option { + parent_path: Option, + request: String, +) -> Result, AnyError> { check_unstable(state); - if !has_parent { - return None; + if parent_path.is_none() { + return Ok(None); } - if let Some(parent_filename) = maybe_parent_filename { - return Some(parent_filename); + let resolver = state.borrow::>().clone(); + let pkg = resolution::get_package_scope_config( + &Url::from_file_path(parent_path.unwrap()).unwrap(), + &*resolver, + ) + .ok(); + if pkg.is_none() { + return Ok(None); } - if let Some(parent_id) = maybe_parent_id { - if parent_id == "" || parent_id == "internal/preload" { - if let Ok(cwd) = std::env::current_dir() { - return Some(cwd.to_string_lossy().to_string()); - } - } + let pkg = pkg.unwrap(); + if pkg.exports.is_none() { + return Ok(None); + } + if pkg.name.is_none() { + return Ok(None); + } + + let pkg_name = pkg.name.as_ref().unwrap().to_string(); + let mut expansion = ".".to_string(); + + if request == pkg_name { + // pass + } else if request.starts_with(&format!("{}/", pkg_name)) { + expansion += &request[pkg_name.len()..]; + } else { + return Ok(None); + } + + let base = deno_core::url::Url::from_file_path(PathBuf::from("/")).unwrap(); + if let Some(exports) = &pkg.exports { + resolution::package_exports_resolve( + deno_core::url::Url::from_file_path(&pkg.path).unwrap(), + expansion, + exports, + &base, + resolution::REQUIRE_CONDITIONS, + &*resolver, + ) + .map(|r| Some(r.as_str().to_string())) + } else { + Ok(None) } - None } #[op] fn op_require_read_file( state: &mut OpState, - _filename: String, + file_path: String, ) -> Result { check_unstable(state); - todo!("not implemented"); + let file_path = PathBuf::from(file_path); + ensure_read_permission(state, &file_path)?; + Ok(std::fs::read_to_string(file_path)?) +} + +#[op] +pub fn op_require_as_file_path( + state: &mut OpState, + file_or_url: String, +) -> String { + check_unstable(state); + match Url::parse(&file_or_url) { + Ok(url) => url.to_file_path().unwrap().to_string_lossy().to_string(), + Err(_) => file_or_url, + } +} + +#[op] +fn op_require_resolve_exports( + state: &mut OpState, + modules_path: String, + _request: String, + name: String, + expansion: String, + parent_path: String, +) -> Result, AnyError> { + check_unstable(state); + let resolver = state.borrow::>().clone(); + + let pkg_path = if resolver.in_npm_package(&PathBuf::from(&modules_path)) { + modules_path + } else { + path_resolve(vec![modules_path, name]) + }; + let pkg = PackageJson::load( + &*resolver, + PathBuf::from(&pkg_path).join("package.json"), + )?; + + if let Some(exports) = &pkg.exports { + let base = Url::from_file_path(parent_path).unwrap(); + resolution::package_exports_resolve( + deno_core::url::Url::from_directory_path(pkg_path).unwrap(), + format!(".{}", expansion), + exports, + &base, + resolution::REQUIRE_CONDITIONS, + &*resolver, + ) + .map(|r| Some(r.to_file_path().unwrap().to_string_lossy().to_string())) + } else { + Ok(None) + } +} + +#[op] +fn op_require_read_package_scope( + state: &mut OpState, + filename: String, +) -> Option { + check_unstable(state); + let resolver = state.borrow::>().clone(); + resolution::get_package_scope_config( + &Url::from_file_path(filename).unwrap(), + &*resolver, + ) + .ok() +} + +#[op] +fn op_require_package_imports_resolve( + state: &mut OpState, + parent_filename: String, + request: String, +) -> Result, AnyError> { + check_unstable(state); + let parent_path = PathBuf::from(&parent_filename); + ensure_read_permission(state, &parent_path)?; + let resolver = state.borrow::>().clone(); + let pkg = PackageJson::load(&*resolver, parent_path.join("package.json"))?; + + if pkg.imports.is_some() { + let referrer = + deno_core::url::Url::from_file_path(&parent_filename).unwrap(); + let r = resolution::package_imports_resolve( + &request, + &referrer, + resolution::REQUIRE_CONDITIONS, + &*resolver, + ) + .map(|r| Some(r.as_str().to_string())); + state.put(resolver); + r + } else { + Ok(None) + } } diff --git a/ext/node/package_json.rs b/ext/node/package_json.rs new file mode 100644 index 000000000..19a79da96 --- /dev/null +++ b/ext/node/package_json.rs @@ -0,0 +1,159 @@ +// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license. + +use super::DenoDirNpmResolver; +use deno_core::anyhow; +use deno_core::anyhow::bail; +use deno_core::error::AnyError; +use deno_core::serde_json; +use deno_core::serde_json::Map; +use deno_core::serde_json::Value; +use serde::Serialize; +use std::io::ErrorKind; +use std::path::PathBuf; + +// TODO(bartlomieju): deduplicate with cli/compat/esm_resolver.rs +#[derive(Clone, Debug, Serialize)] +pub struct PackageJson { + pub exists: bool, + pub exports: Option>, + pub imports: Option>, + pub bin: Option, + pub main: Option, + pub name: Option, + pub path: PathBuf, + pub typ: String, + pub types: Option, +} + +impl PackageJson { + pub fn empty(path: PathBuf) -> PackageJson { + PackageJson { + exists: false, + exports: None, + imports: None, + bin: None, + main: None, + name: None, + path, + typ: "none".to_string(), + types: None, + } + } + + pub fn load( + resolver: &dyn DenoDirNpmResolver, + path: PathBuf, + ) -> Result { + resolver.ensure_read_permission(&path)?; + let source = match std::fs::read_to_string(&path) { + Ok(source) => source, + Err(err) if err.kind() == ErrorKind::NotFound => { + return Ok(PackageJson::empty(path)); + } + Err(err) => bail!( + "Error loading package.json at {}. {:#}", + path.display(), + err + ), + }; + + if source.trim().is_empty() { + return Ok(PackageJson::empty(path)); + } + + let package_json: Value = serde_json::from_str(&source) + .map_err(|err| anyhow::anyhow!("malformed package.json {}", err))?; + + let imports_val = package_json.get("imports"); + let main_val = package_json.get("main"); + let name_val = package_json.get("name"); + let type_val = package_json.get("type"); + let bin = package_json.get("bin").map(ToOwned::to_owned); + let exports = package_json.get("exports").map(|exports| { + if is_conditional_exports_main_sugar(exports) { + let mut map = Map::new(); + map.insert(".".to_string(), exports.to_owned()); + map + } else { + exports.as_object().unwrap().to_owned() + } + }); + + let imports = if let Some(imp) = imports_val { + imp.as_object().map(|imp| imp.to_owned()) + } else { + None + }; + let main = if let Some(m) = main_val { + m.as_str().map(|m| m.to_string()) + } else { + None + }; + let name = if let Some(n) = name_val { + n.as_str().map(|n| n.to_string()) + } else { + None + }; + + // Ignore unknown types for forwards compatibility + let typ = if let Some(t) = type_val { + if let Some(t) = t.as_str() { + if t != "module" && t != "commonjs" { + "none".to_string() + } else { + t.to_string() + } + } else { + "none".to_string() + } + } else { + "none".to_string() + }; + + // for typescript, it looks for "typings" first, then "types" + let types = package_json + .get("typings") + .or_else(|| package_json.get("types")) + .and_then(|t| t.as_str().map(|s| s.to_string())); + + let package_json = PackageJson { + exists: true, + path, + main, + name, + typ, + types, + exports, + imports, + bin, + }; + Ok(package_json) + } +} + +fn is_conditional_exports_main_sugar(exports: &Value) -> bool { + if exports.is_string() || exports.is_array() { + return true; + } + + if exports.is_null() || !exports.is_object() { + return false; + } + + let exports_obj = exports.as_object().unwrap(); + let mut is_conditional_sugar = false; + let mut i = 0; + for key in exports_obj.keys() { + let cur_is_conditional_sugar = key.is_empty() || !key.starts_with('.'); + if i == 0 { + is_conditional_sugar = cur_is_conditional_sugar; + i += 1; + } else if is_conditional_sugar != cur_is_conditional_sugar { + panic!("\"exports\" cannot contains 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.") + } + } + + is_conditional_sugar +} diff --git a/ext/node/resolution.rs b/ext/node/resolution.rs new file mode 100644 index 000000000..9d71fba49 --- /dev/null +++ b/ext/node/resolution.rs @@ -0,0 +1,696 @@ +// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license. + +use std::path::PathBuf; + +use deno_core::error::generic_error; +use deno_core::error::AnyError; +use deno_core::serde_json::Map; +use deno_core::serde_json::Value; +use deno_core::url::Url; +use deno_core::ModuleSpecifier; +use regex::Regex; + +use crate::errors; +use crate::package_json::PackageJson; +use crate::DenoDirNpmResolver; + +pub static DEFAULT_CONDITIONS: &[&str] = &["deno", "node", "import"]; +pub static REQUIRE_CONDITIONS: &[&str] = &["require", "node"]; + +fn to_file_path(url: &ModuleSpecifier) -> PathBuf { + url + .to_file_path() + .unwrap_or_else(|_| panic!("Provided URL was not file:// URL: {}", url)) +} + +fn to_file_path_string(url: &ModuleSpecifier) -> String { + to_file_path(url).display().to_string() +} + +fn throw_import_not_defined( + specifier: &str, + package_json_url: Option, + base: &ModuleSpecifier, +) -> AnyError { + errors::err_package_import_not_defined( + specifier, + package_json_url.map(|u| to_file_path_string(&u.join(".").unwrap())), + &to_file_path_string(base), + ) +} + +fn pattern_key_compare(a: &str, b: &str) -> i32 { + let a_pattern_index = a.find('*'); + let b_pattern_index = b.find('*'); + + let base_len_a = if let Some(index) = a_pattern_index { + index + 1 + } else { + a.len() + }; + let base_len_b = if let Some(index) = b_pattern_index { + index + 1 + } else { + b.len() + }; + + if base_len_a > base_len_b { + return -1; + } + + if base_len_b > base_len_a { + return 1; + } + + if a_pattern_index.is_none() { + return 1; + } + + if b_pattern_index.is_none() { + return -1; + } + + if a.len() > b.len() { + return -1; + } + + if b.len() > a.len() { + return 1; + } + + 0 +} + +pub fn package_imports_resolve( + name: &str, + referrer: &ModuleSpecifier, + conditions: &[&str], + npm_resolver: &dyn DenoDirNpmResolver, +) -> Result { + if name == "#" || name.starts_with("#/") || name.ends_with('/') { + let reason = "is not a valid internal imports specifier name"; + return Err(errors::err_invalid_module_specifier( + name, + reason, + Some(to_file_path_string(referrer)), + )); + } + + let package_config = get_package_scope_config(referrer, npm_resolver)?; + let mut package_json_url = None; + if package_config.exists { + package_json_url = Some(Url::from_file_path(package_config.path).unwrap()); + if let Some(imports) = &package_config.imports { + if imports.contains_key(name) && !name.contains('*') { + let maybe_resolved = resolve_package_target( + package_json_url.clone().unwrap(), + imports.get(name).unwrap().to_owned(), + "".to_string(), + name.to_string(), + referrer, + false, + true, + conditions, + npm_resolver, + )?; + if let Some(resolved) = maybe_resolved { + return Ok(resolved); + } + } else { + let mut best_match = ""; + let mut best_match_subpath = None; + for key in imports.keys() { + let pattern_index = key.find('*'); + if let Some(pattern_index) = pattern_index { + let key_sub = &key[0..=pattern_index]; + if name.starts_with(key_sub) { + let pattern_trailer = &key[pattern_index + 1..]; + if name.len() > key.len() + && name.ends_with(&pattern_trailer) + && pattern_key_compare(best_match, key) == 1 + && key.rfind('*') == Some(pattern_index) + { + best_match = key; + best_match_subpath = Some( + name[pattern_index..=(name.len() - pattern_trailer.len())] + .to_string(), + ); + } + } + } + } + + if !best_match.is_empty() { + let target = imports.get(best_match).unwrap().to_owned(); + let maybe_resolved = resolve_package_target( + package_json_url.clone().unwrap(), + target, + best_match_subpath.unwrap(), + best_match.to_string(), + referrer, + true, + true, + conditions, + npm_resolver, + )?; + if let Some(resolved) = maybe_resolved { + return Ok(resolved); + } + } + } + } + } + + Err(throw_import_not_defined(name, package_json_url, referrer)) +} + +fn throw_invalid_package_target( + subpath: String, + target: String, + package_json_url: &ModuleSpecifier, + internal: bool, + base: &ModuleSpecifier, +) -> AnyError { + errors::err_invalid_package_target( + to_file_path_string(&package_json_url.join(".").unwrap()), + subpath, + target, + internal, + Some(base.as_str().to_string()), + ) +} + +fn throw_invalid_subpath( + subpath: String, + package_json_url: &ModuleSpecifier, + internal: bool, + base: &ModuleSpecifier, +) -> AnyError { + let ie = if internal { "imports" } else { "exports" }; + let reason = format!( + "request is not a valid subpath for the \"{}\" resolution of {}", + ie, + to_file_path_string(package_json_url) + ); + errors::err_invalid_module_specifier( + &subpath, + &reason, + Some(to_file_path_string(base)), + ) +} + +#[allow(clippy::too_many_arguments)] +fn resolve_package_target_string( + target: String, + subpath: String, + match_: String, + package_json_url: ModuleSpecifier, + base: &ModuleSpecifier, + pattern: bool, + internal: bool, + conditions: &[&str], + npm_resolver: &dyn DenoDirNpmResolver, +) -> Result { + if !subpath.is_empty() && !pattern && !target.ends_with('/') { + return Err(throw_invalid_package_target( + match_, + target, + &package_json_url, + internal, + base, + )); + } + let invalid_segment_re = + Regex::new(r"(^|\|/)(..?|node_modules)(\|/|$)").expect("bad regex"); + let pattern_re = Regex::new(r"\*").expect("bad regex"); + if !target.starts_with("./") { + if internal && !target.starts_with("../") && !target.starts_with('/') { + let is_url = Url::parse(&target).is_ok(); + if !is_url { + let export_target = if pattern { + pattern_re + .replace(&target, |_caps: ®ex::Captures| subpath.clone()) + .to_string() + } else { + format!("{}{}", target, subpath) + }; + return package_resolve( + &export_target, + &package_json_url, + conditions, + npm_resolver, + ); + } + } + return Err(throw_invalid_package_target( + match_, + target, + &package_json_url, + internal, + base, + )); + } + if invalid_segment_re.is_match(&target[2..]) { + return Err(throw_invalid_package_target( + match_, + target, + &package_json_url, + internal, + base, + )); + } + let resolved = package_json_url.join(&target)?; + let resolved_path = resolved.path(); + let package_url = package_json_url.join(".").unwrap(); + let package_path = package_url.path(); + if !resolved_path.starts_with(package_path) { + return Err(throw_invalid_package_target( + match_, + target, + &package_json_url, + internal, + base, + )); + } + if subpath.is_empty() { + return Ok(resolved); + } + if invalid_segment_re.is_match(&subpath) { + let request = if pattern { + match_.replace('*', &subpath) + } else { + format!("{}{}", match_, subpath) + }; + return Err(throw_invalid_subpath( + request, + &package_json_url, + internal, + base, + )); + } + if pattern { + let replaced = pattern_re + .replace(resolved.as_str(), |_caps: ®ex::Captures| subpath.clone()); + let url = Url::parse(&replaced)?; + return Ok(url); + } + Ok(resolved.join(&subpath)?) +} + +#[allow(clippy::too_many_arguments)] +fn resolve_package_target( + package_json_url: ModuleSpecifier, + target: Value, + subpath: String, + package_subpath: String, + base: &ModuleSpecifier, + pattern: bool, + internal: bool, + conditions: &[&str], + npm_resolver: &dyn DenoDirNpmResolver, +) -> Result, AnyError> { + if let Some(target) = target.as_str() { + return Ok(Some(resolve_package_target_string( + target.to_string(), + subpath, + package_subpath, + package_json_url, + base, + pattern, + internal, + conditions, + npm_resolver, + )?)); + } else if let Some(target_arr) = target.as_array() { + if target_arr.is_empty() { + return Ok(None); + } + + let mut last_error = None; + for target_item in target_arr { + let resolved_result = resolve_package_target( + package_json_url.clone(), + target_item.to_owned(), + subpath.clone(), + package_subpath.clone(), + base, + pattern, + internal, + conditions, + npm_resolver, + ); + + if let Err(e) = resolved_result { + let err_string = e.to_string(); + last_error = Some(e); + if err_string.starts_with("[ERR_INVALID_PACKAGE_TARGET]") { + continue; + } + return Err(last_error.unwrap()); + } + let resolved = resolved_result.unwrap(); + if resolved.is_none() { + last_error = None; + continue; + } + return Ok(resolved); + } + if last_error.is_none() { + return Ok(None); + } + return Err(last_error.unwrap()); + } else if let Some(target_obj) = target.as_object() { + for key in target_obj.keys() { + // TODO(bartlomieju): verify that keys are not numeric + // return Err(errors::err_invalid_package_config( + // to_file_path_string(package_json_url), + // Some(base.as_str().to_string()), + // Some("\"exports\" cannot contain numeric property keys.".to_string()), + // )); + + if key == "default" || conditions.contains(&key.as_str()) { + let condition_target = target_obj.get(key).unwrap().to_owned(); + let resolved = resolve_package_target( + package_json_url.clone(), + condition_target, + subpath.clone(), + package_subpath.clone(), + base, + pattern, + internal, + conditions, + npm_resolver, + )?; + if resolved.is_none() { + continue; + } + return Ok(resolved); + } + } + } else if target.is_null() { + return Ok(None); + } + + Err(throw_invalid_package_target( + package_subpath, + target.to_string(), + &package_json_url, + internal, + base, + )) +} + +fn throw_exports_not_found( + subpath: String, + package_json_url: &ModuleSpecifier, + base: &ModuleSpecifier, +) -> AnyError { + errors::err_package_path_not_exported( + to_file_path_string(&package_json_url.join(".").unwrap()), + subpath, + Some(to_file_path_string(base)), + ) +} + +pub fn package_exports_resolve( + package_json_url: ModuleSpecifier, + package_subpath: String, + package_exports: &Map, + base: &ModuleSpecifier, + conditions: &[&str], + npm_resolver: &dyn DenoDirNpmResolver, +) -> Result { + if package_exports.contains_key(&package_subpath) + && package_subpath.find('*').is_none() + && !package_subpath.ends_with('/') + { + let target = package_exports.get(&package_subpath).unwrap().to_owned(); + let resolved = resolve_package_target( + package_json_url.clone(), + target, + "".to_string(), + package_subpath.to_string(), + base, + false, + false, + conditions, + npm_resolver, + )?; + if resolved.is_none() { + return Err(throw_exports_not_found( + package_subpath, + &package_json_url, + base, + )); + } + return Ok(resolved.unwrap()); + } + + let mut best_match = ""; + let mut best_match_subpath = None; + for key in package_exports.keys() { + let pattern_index = key.find('*'); + if let Some(pattern_index) = pattern_index { + let key_sub = &key[0..=pattern_index]; + if package_subpath.starts_with(key_sub) { + // When this reaches EOL, this can throw at the top of the whole function: + // + // if (StringPrototypeEndsWith(packageSubpath, '/')) + // throwInvalidSubpath(packageSubpath) + // + // To match "imports" and the spec. + if package_subpath.ends_with('/') { + // TODO(bartlomieju): + // emitTrailingSlashPatternDeprecation(); + } + let pattern_trailer = &key[pattern_index + 1..]; + if package_subpath.len() > key.len() + && package_subpath.ends_with(&pattern_trailer) + && pattern_key_compare(best_match, key) == 1 + && key.rfind('*') == Some(pattern_index) + { + best_match = key; + best_match_subpath = Some( + package_subpath + [pattern_index..=(package_subpath.len() - pattern_trailer.len())] + .to_string(), + ); + } + } + } + } + + if !best_match.is_empty() { + let target = package_exports.get(best_match).unwrap().to_owned(); + let maybe_resolved = resolve_package_target( + package_json_url.clone(), + target, + best_match_subpath.unwrap(), + best_match.to_string(), + base, + true, + false, + conditions, + npm_resolver, + )?; + if let Some(resolved) = maybe_resolved { + return Ok(resolved); + } else { + return Err(throw_exports_not_found( + package_subpath, + &package_json_url, + base, + )); + } + } + + Err(throw_exports_not_found( + package_subpath, + &package_json_url, + base, + )) +} + +fn parse_package_name( + specifier: &str, + base: &ModuleSpecifier, +) -> Result<(String, String, bool), AnyError> { + let mut separator_index = specifier.find('/'); + let mut valid_package_name = true; + let mut is_scoped = false; + if specifier.is_empty() { + valid_package_name = false; + } else if specifier.starts_with('@') { + is_scoped = true; + if let Some(index) = separator_index { + separator_index = specifier[index + 1..].find('/'); + } else { + valid_package_name = false; + } + } + + let package_name = if let Some(index) = separator_index { + specifier[0..index].to_string() + } else { + specifier.to_string() + }; + + // Package name cannot have leading . and cannot have percent-encoding or separators. + for ch in package_name.chars() { + if ch == '%' || ch == '\\' { + valid_package_name = false; + break; + } + } + + if !valid_package_name { + return Err(errors::err_invalid_module_specifier( + specifier, + "is not a valid package name", + Some(to_file_path_string(base)), + )); + } + + let package_subpath = if let Some(index) = separator_index { + format!(".{}", specifier.chars().skip(index).collect::()) + } else { + ".".to_string() + }; + + Ok((package_name, package_subpath, is_scoped)) +} + +pub fn package_resolve( + specifier: &str, + referrer: &ModuleSpecifier, + conditions: &[&str], + npm_resolver: &dyn DenoDirNpmResolver, +) -> Result { + let (package_name, package_subpath, _is_scoped) = + parse_package_name(specifier, referrer)?; + + // ResolveSelf + let package_config = get_package_scope_config(referrer, npm_resolver)?; + if package_config.exists { + let package_json_url = Url::from_file_path(&package_config.path).unwrap(); + if package_config.name.as_ref() == Some(&package_name) { + if let Some(exports) = &package_config.exports { + return package_exports_resolve( + package_json_url, + package_subpath, + exports, + referrer, + conditions, + npm_resolver, + ); + } + } + } + + let package_dir_path = npm_resolver + .resolve_package_folder_from_package( + &package_name, + &referrer.to_file_path().unwrap(), + ) + .unwrap(); + let package_json_path = package_dir_path.join("package.json"); + let package_json_url = + ModuleSpecifier::from_file_path(&package_json_path).unwrap(); + + // todo: error with this instead when can't find package + // Err(errors::err_module_not_found( + // &package_json_url + // .join(".") + // .unwrap() + // .to_file_path() + // .unwrap() + // .display() + // .to_string(), + // &to_file_path_string(referrer), + // "package", + // )) + + // Package match. + let package_json = PackageJson::load(npm_resolver, package_json_path)?; + if let Some(exports) = &package_json.exports { + return package_exports_resolve( + package_json_url, + package_subpath, + exports, + referrer, + conditions, + npm_resolver, + ); + } + if package_subpath == "." { + return legacy_main_resolve(&package_json_url, &package_json, referrer); + } + + package_json_url + .join(&package_subpath) + .map_err(AnyError::from) +} + +pub fn get_package_scope_config( + referrer: &ModuleSpecifier, + npm_resolver: &dyn DenoDirNpmResolver, +) -> Result { + let root_folder = npm_resolver + .resolve_package_folder_from_path(&referrer.to_file_path().unwrap())?; + let package_json_path = root_folder.join("./package.json"); + PackageJson::load(npm_resolver, package_json_path) +} + +fn file_exists(path_url: &ModuleSpecifier) -> bool { + if let Ok(stats) = std::fs::metadata(to_file_path(path_url)) { + stats.is_file() + } else { + false + } +} + +pub fn legacy_main_resolve( + package_json_url: &ModuleSpecifier, + package_json: &PackageJson, + _base: &ModuleSpecifier, +) -> Result { + let mut guess; + + if let Some(main) = &package_json.main { + guess = package_json_url.join(&format!("./{}", main))?; + if file_exists(&guess) { + return Ok(guess); + } + + let mut found = false; + for ext in [ + ".js", + ".json", + ".node", + "/index.js", + "/index.json", + "/index.node", + ] { + guess = package_json_url.join(&format!("./{}{}", main, ext))?; + if file_exists(&guess) { + found = true; + break; + } + } + + if found { + // TODO(bartlomieju): emitLegacyIndexDeprecation() + return Ok(guess); + } + } + + for p in ["./index.js", "./index.json", "./index.node"] { + guess = package_json_url.join(p)?; + if file_exists(&guess) { + // TODO(bartlomieju): emitLegacyIndexDeprecation() + return Ok(guess); + } + } + + Err(generic_error("not found")) +} -- cgit v1.2.3