/
githubmirror
/
meteor
Обзор
Документация
Войти
/
githubmirror
/
meteor
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
devel
tools/isobuild/resolver.ts
434 строки
14 KB
Dirk Stolle
fix a few typos in tools directory
03 ноя 2022, 19:42
03 ноя 2022, 19:42
088e52f
Код
Авторство
О чём код?
import { isString, isObject, has, } from "underscore"; import { matches as archMatches, isLegacyArch } from "../utils/archinfo"; import { pathJoin, pathNormalize, pathDirname, pathBasename, convertToOSPath, convertToPosixPath, containsPath, } from "../fs/files"; import { Stats, BigIntStats } from "fs"; import { wrap } from "optimism"; import { optimisticStatOrNull, optimisticReadJsonOrNull, } from "../fs/optimistic"; const nativeModulesMap: Record<string, string> = Object.create(null); const nativeNames = require('module').builtinModules; nativeNames.forEach((id: string) => { // When a native Node module is imported, we register a dependency on a // meteor-node-stubs/deps/* module of the same name, so that the // necessary stub modules will be included in the bundle. This alternate // identifier will not be imported at runtime, but the modules it // depends on are necessary for the original import to succeed. nativeModulesMap[id] = "meteor-node-stubs/deps/" + id; nativeModulesMap[`node:${id}`] = "meteor-node-stubs/deps/" + id; }); export type ResolverOptions = { sourceRoot: string; targetArch: string; extensions: string[]; nodeModulesPaths: string[]; caller?: string; } export type Resolution = { stat: Stats | BigIntStats; path: string; packageJsonMap?: Record<string, Record<string, any>>; id?: string; } | "missing" | null export default class Resolver { static getOrCreate = wrap(function (options: ResolverOptions) { return new Resolver(options); }, { makeCacheKey(options) { return JSON.stringify(options); } }); private sourceRoot: string; private targetArch: string; private extensions: string[]; private nodeModulesPaths: string[]; private mainFields: string[]; public statOrNull = optimisticStatOrNull as (path: string) => Stats | BigIntStats | null | undefined; constructor({ sourceRoot, targetArch, extensions = [".js", ".json"], nodeModulesPaths = [], }: ResolverOptions) { this.sourceRoot = sourceRoot; this.extensions = extensions; this.targetArch = targetArch; this.nodeModulesPaths = nodeModulesPaths; this.statOrNull = optimisticStatOrNull; const { resolve } = this; this.resolve = wrap((id, absParentPath) => { return resolve.call(this, id, absParentPath); }, { makeCacheKey(id, absParentPath) { // Only the directory of the absParentPath matters for caching. return JSON.stringify([id, pathDirname(absParentPath)]); } }); const { findPkgJsonSubsetForPath, getPkgJsonSubsetForDir, } = this; this.findPkgJsonSubsetForPath = wrap( path => findPkgJsonSubsetForPath.call(this, path)); this.getPkgJsonSubsetForDir = wrap( path => getPkgJsonSubsetForDir.call(this, path)); if (archMatches(this.targetArch, "web")) { if (isLegacyArch(this.targetArch)) { // The legacy bundle prefers the "main" field over the "module" // field, since many npm packages ship modern syntax other than // import/export in their "module" dependency trees. this.mainFields = ["browser", "main", "module"]; } else { this.mainFields = ["browser", "module", "main"]; } } else { this.mainFields = ["main"]; } } static isTopLevel(id: string) { return "./".indexOf(id.charAt(0)) < 0; } static isNative(id: string): boolean { return has(nativeModulesMap, id); } static getNativeStubId(id: string) { return nativeModulesMap[id] || null; } // Resolve the given module identifier to an object { path, stat } or // null, relative to an absolute parent path. The _seenDirPaths // parameter is for internal use only and should be omitted. public resolve( id: string, absParentPath: string, _seenDirPaths?: Set<string>, ): Resolution { let resolved = this.resolveAbsolute(id, absParentPath) || this.resolveRelative(id, absParentPath) || this.resolveNodeModule(id, absParentPath); if (resolved === "missing") { // The _resolveNodeModule method can return "missing" to indicate // that the ImportScanner should look elsewhere for this module, // such as in the app node_modules directory. return resolved; } let packageJsonMap = null; while (resolved && resolved.stat && resolved.stat.isDirectory()) { let dirPath = resolved.path; _seenDirPaths = _seenDirPaths || new Set; // If the "main" field of a package.json file resolves to a // directory we've already considered, then we should not attempt to // read the same package.json file again. if (! _seenDirPaths.has(dirPath)) { _seenDirPaths.add(dirPath); const found = this.getPkgJsonSubsetForDir(dirPath); const foundPkgJsonMain = found && this.mainFields.some(name => { const value = found.pkg[name]; if (isString(value)) { // The "main" field of package.json does not have to begin with ./ // to be considered relative, so first we try simply appending it // to the directory path before falling back to a full resolve, // which might return a package from a node_modules directory. resolved = this.joinAndStat(dirPath, value) || this.resolve(value, found.path, _seenDirPaths); return resolved && typeof resolved === "object"; } return false; }); if (foundPkgJsonMain && found) { if (! resolved.packageJsonMap) { resolved.packageJsonMap = Object.create(null); } resolved.packageJsonMap![found.path] = found.pkg; // The resolution above may have returned a directory, so we // merge resolved.packageJsonMap into packageJsonMap so that we // don't forget the package.json we just resolved, then continue // the loop to make sure we fully resolve the "main" module // identifier to a non-directory. Technically this could // involve even more package.json files, but in practice the // "main" property will almost always name a directory // containing an index.js file. Object.assign( packageJsonMap || (packageJsonMap = Object.create(null)), resolved.packageJsonMap, ); continue; } // Include the package.json stub in the bundle even if it was not // used to resolve the "main" entry point, per this comment: // https://github.com/meteor/meteor/issues/9235#issuecomment-340562285 if (found) { packageJsonMap = packageJsonMap || Object.create(null); packageJsonMap[found.path] = found.pkg; } } // If we didn't find a `package.json` file, or it didn't have a // resolvable `.main` property, the only possibility left to // consider is that this directory contains an `index.js` module. // This assignment almost always terminates the while loop, because // there's very little chance an `index.js` file will be a // directory. However, in principle it is remotely possible that a // file called `index.js` could be a directory instead of a file. resolved = this.joinAndStat(dirPath, "index"); } if (resolved) { if (packageJsonMap) { resolved.packageJsonMap = packageJsonMap; } // If the package.json file that governs resolved.path has a // "browser" field, include it in resolved.packageJsonMap so that // the ImportScanner can register the appropriate browser aliases. const pkgJsonInfo = this.findPkgJsonSubsetForPath(resolved.path); if (pkgJsonInfo && isObject(pkgJsonInfo.pkg.browser)) { if (! resolved.packageJsonMap) { resolved.packageJsonMap = Object.create(null); } resolved.packageJsonMap![pkgJsonInfo.path] = pkgJsonInfo.pkg; } resolved.id = convertToPosixPath( convertToOSPath(resolved.path), true ); } return resolved; } public joinAndStat(...joinArgs: string[]) { const joined: string = pathJoin(...joinArgs); const path = pathNormalize(joined); const exactStat = this.statOrNull(path); const exactResult = exactStat && { path, stat: exactStat }; let result: Resolution = null; if (exactResult && exactStat && exactStat.isFile()) { result = exactResult; } else { // No point in trying alternate file extensions if the parent // directory does not exist. const parentDirStat = this.statOrNull(pathDirname(path)); if (parentDirStat && parentDirStat.isDirectory()) { this.extensions.some(ext => { const pathWithExt = path + ext; const stat = this.statOrNull(pathWithExt); if (stat && ! stat.isDirectory()) { return result = { path: pathWithExt, stat }; } }); } } if (! result && exactResult && exactStat && exactStat.isDirectory()) { // After trying all available file extensions, fall back to the // original result if it was a directory. result = exactResult; } return result; } private resolveAbsolute(id: string, _absParentPath: string): Resolution { return id.charAt(0) === "/" && this.joinAndStat(this.sourceRoot, id.slice(1)) || null; } private resolveRelative(id: string, absParentPath: string): Resolution { if (id.charAt(0) === ".") { return this.joinAndStat(absParentPath, "..", id); } return null; } private resolveNodeModule(id: string, absParentPath: string): Resolution { if (! Resolver.isTopLevel(id)) { return null; } if (Resolver.isNative(id) && archMatches(this.targetArch, "os")) { // Forbid installing any server module with the same name as a // native Node module. return null; } let sourceRoot: string | undefined; if (containsPath(this.sourceRoot, absParentPath)) { // If the file is contained by this.sourceRoot, then it's safe to // use this.sourceRoot as the limiting ancestor directory in the // while loop below, but we're still going to check whether the file // resides in an external node_modules directory, since "external" // .npm/package/node_modules directories are technically contained // within the root directory of their packages. sourceRoot = this.sourceRoot; } this.nodeModulesPaths.some(path => { if (containsPath(path, absParentPath)) { // If the file is inside an external node_modules directory, // consider the rootDir to be the parent directory of that // node_modules directory, rather than this.sourceRoot. return sourceRoot = pathDirname(path); } }); let resolved = null; if (sourceRoot) { let dir = absParentPath; // It's ok for absParentPath to be a directory! let dirStat = this.statOrNull(dir); if (! (dirStat && dirStat.isDirectory())) { dir = pathDirname(dir); } while (! (resolved = this.joinAndStat(dir, "node_modules", id))) { if (dir === sourceRoot) { break; } const parentDir = pathDirname(dir); if (dir === parentDir) { // We've reached the root of the file system?? break; } dir = parentDir; } } if (! resolved) { // After checking any local node_modules directories, fall back to // the package NPM directory, if one was specified. this.nodeModulesPaths.some(path => { return resolved = this.joinAndStat(path, id); }); } // If the dependency is still not resolved, it might be handled by the // fallback function defined in meteor/packages/modules-runtime/[modern|legacy|server].js, or // it might be imported in code that will never run on this platform, // so there is always the possibility that its absence is not actually // a problem. As much as we might like to issue warnings about missing // dependencies here, we just don't have enough information to make // that determination until the code actually runs. return resolved || "missing"; } private getPkgJsonSubsetForDir(dirPath: string) { const pkgJsonPath = pathJoin(dirPath, "package.json"); const pkg = optimisticReadJsonOrNull(pkgJsonPath); if (! pkg) { return null; } // Output a JS module that exports just the "name", "version", "main", // and "browser" properties (if defined) from the package.json file. const pkgSubset: Partial<typeof pkg> = {}; if (has(pkg, "name")) { pkgSubset.name = pkg.name; } if (has(pkg, "version")) { pkgSubset.version = pkg.version; } this.mainFields.forEach(name => { const value = pkg[name]; if (isString(value) || isObject(value)) { pkgSubset[name] = value; } }); return { path: pkgJsonPath, pkg: pkgSubset, }; } private findPkgJsonSubsetForPath( path: string, ): ReturnType<Resolver["getPkgJsonSubsetForDir"]> { const stat = this.statOrNull(path); if (stat && stat.isDirectory()) { const found = this.getPkgJsonSubsetForDir(path); if (found) { return found; } if (path === this.sourceRoot) { return null; } } const parentDir = pathDirname(path); if (parentDir === path) { return null; } if (pathBasename(parentDir) === "node_modules") { return null; } return this.findPkgJsonSubsetForPath(parentDir); } }; import { Profile } from "../tool-env/profile"; const Rp = Resolver.prototype as any; Object.keys(Rp).forEach(key => { if (key === "constructor") return; Rp[key] = Profile(`Resolver#${key}`, Rp[key]); });