UNPKG

@jspm/generator

Version:

Package Import Map Generation Tool

304 lines (302 loc) 17 kB
function _define_property(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } import { Semver } from "sver"; import { registryProviders } from "../providers/index.js"; import { getConstraintFor, getFlattenedResolution, getResolution, setConstraint, setResolution } from "./lock.js"; import { newPackageTarget } from "./package.js"; export class Installer { visitInstalls(visitor) { if (visitor(this.installs.primary, null)) return; for (const scopeUrl of Object.keys(this.installs.secondary)){ if (visitor(this.installs.secondary[scopeUrl], scopeUrl)) return; } } getProvider(target) { let provider = this.defaultProvider; for (const name of Object.keys(this.providers)){ if (name.endsWith(":") && target.registry === name.slice(0, -1) || target.name.startsWith(name) && (target.name.length === name.length || target.name[name.length] === "/")) { provider = parseProviderStr(this.providers[name]); break; } } return provider; } /** * Locks a package against the given target. * * @param {string} pkgName Name of the package being installed. * @param {InstallTarget} target The installation target being installed. * @param {`./${string}` | '.'} traceSubpath * @param {InstallMode} mode Specifies how to interact with existing installs. * @param {`${string}/` | null} pkgScope URL of the package scope in which this install is occurring, null if it's a top-level install. * @param {string} parentUrl URL of the parent for this install. * @returns {Promise<InstalledResolution>} */ async installTarget(pkgName, { pkgTarget, installSubpath }, traceSubpath, mode, pkgScope, parentUrl) { const isTopLevel = pkgScope === null; const useLatest = isTopLevel && mode.includes("latest") || !isTopLevel && mode === "latest-all"; // Resolutions are always authoritative, and override the existing target: if (this.resolutions[pkgName]) { const resolutionTarget = newPackageTarget(this.resolutions[pkgName], this.opts.baseUrl, this.defaultRegistry, pkgName); resolutionTarget.installSubpath = installSubpath; if (JSON.stringify(pkgTarget) !== JSON.stringify(resolutionTarget.pkgTarget)) return this.installTarget(pkgName, resolutionTarget, traceSubpath, mode, pkgScope, parentUrl); } // URL targets are installed as locks directly, as we have no versioning // information to work with: if (pkgTarget instanceof URL) { const installHref = pkgTarget.href; const installUrl = installHref + (installHref.endsWith("/") ? "" : "/"); this.log("installer/installTarget", `${pkgName} ${pkgScope} -> ${installHref} (URL)`); this.newInstalls = setResolution(this.installs, pkgName, installUrl, pkgScope, installSubpath); return { installUrl, installSubpath }; } const provider = this.getProvider(pkgTarget); // Look for an existing lock for this package if we're in an install mode // that supports them: if (mode === "default" || mode === "freeze" || !useLatest) { const pkg = await this.getBestExistingMatch(pkgTarget); if (pkg) { this.log("installer/installTarget", `${pkgName} ${pkgScope} -> ${JSON.stringify(pkg)} (existing match)`); const installUrl = await this.resolver.pkgToUrl(pkg, provider); this.newInstalls = setResolution(this.installs, pkgName, installUrl, pkgScope, installSubpath); setConstraint(this.constraints, pkgName, pkgTarget, pkgScope); return { installUrl, installSubpath }; } } const latestPkg = await this.resolver.resolveLatestTarget(pkgTarget, provider, parentUrl); const pkgUrl = await this.resolver.pkgToUrl(latestPkg, provider); const installed = getConstraintFor(latestPkg.name, latestPkg.registry, this.constraints); // If this is a secondary install, then we ideally want to upgrade all // existing locks on this package to latest and use that. If there's a // constraint and we can't, then we fallback to the best existing lock: if (mode !== "freeze" && !useLatest && !isTopLevel && latestPkg && !this.tryUpgradeAllTo(latestPkg, pkgUrl, installed)) { const pkg = await this.getBestExistingMatch(pkgTarget); // cannot upgrade to latest -> stick with existing resolution (if compatible) if (pkg) { this.log("installer/installTarget", `${pkgName} ${pkgScope} -> ${JSON.stringify(latestPkg)} (existing match not latest)`); const installUrl = await this.resolver.pkgToUrl(pkg, provider); this.newInstalls = setResolution(this.installs, pkgName, installUrl, pkgScope, installSubpath); setConstraint(this.constraints, pkgName, pkgTarget, pkgScope); return { installUrl, installSubpath }; } } // Otherwise we install latest and make an attempt to upgrade any existing // locks that are compatible to the latest version: this.log("installer/installTarget", `${pkgName} ${pkgScope} -> ${pkgUrl} ${installSubpath ? installSubpath : "<no-subpath>"} (latest)`); this.newInstalls = setResolution(this.installs, pkgName, pkgUrl, pkgScope, installSubpath); setConstraint(this.constraints, pkgName, pkgTarget, pkgScope); if (mode !== "freeze") this.upgradeSupportedTo(latestPkg, pkgUrl, installed); return { installUrl: pkgUrl, installSubpath }; } /** * Installs the given package specifier. * * @param {string} pkgName The package specifier being installed. * @param {InstallMode} mode Specifies how to interact with existing installs. * @param {`${string}/` | null} pkgScope URL of the package scope in which this install is occurring, null if it's a top-level install. * @param {`./${string}` | '.'} traceSubpath * @param {string} parentUrl URL of the parent for this install. * @returns {Promise<string | InstalledResolution>} */ async install(pkgName, mode, pkgScope = null, traceSubpath, parentUrl = this.installBaseUrl) { var _pcfg_dependencies, _pcfg_peerDependencies, _pcfg_optionalDependencies, _pcfg_devDependencies; this.log("installer/install", `installing ${pkgName} from ${parentUrl} in scope ${pkgScope}`); // Anything installed in the scope of the installer's base URL is treated // as top-level, and hits the primary locks. Anything else is treated as // a secondary dependency: // TODO: wire this concept through the whole codebase. const isTopLevel = !pkgScope || pkgScope == this.installBaseUrl; if (this.resolutions[pkgName]) return this.installTarget(pkgName, newPackageTarget(this.resolutions[pkgName], this.opts.baseUrl, this.defaultRegistry, pkgName), traceSubpath, mode, isTopLevel ? null : pkgScope, parentUrl); // Fetch the current scope's pjson: const definitelyPkgScope = pkgScope || await this.resolver.getPackageBase(parentUrl); const pcfg = await this.resolver.getPackageConfig(definitelyPkgScope) || {}; // By default, we take an install target from the current scope's pjson: const pjsonTargetStr = ((_pcfg_dependencies = pcfg.dependencies) === null || _pcfg_dependencies === void 0 ? void 0 : _pcfg_dependencies[pkgName]) || ((_pcfg_peerDependencies = pcfg.peerDependencies) === null || _pcfg_peerDependencies === void 0 ? void 0 : _pcfg_peerDependencies[pkgName]) || ((_pcfg_optionalDependencies = pcfg.optionalDependencies) === null || _pcfg_optionalDependencies === void 0 ? void 0 : _pcfg_optionalDependencies[pkgName]) || isTopLevel && ((_pcfg_devDependencies = pcfg.devDependencies) === null || _pcfg_devDependencies === void 0 ? void 0 : _pcfg_devDependencies[pkgName]); const pjsonTarget = pjsonTargetStr && newPackageTarget(pjsonTargetStr, new URL(definitelyPkgScope), this.defaultRegistry, pkgName); const useLatestPjsonTarget = !!pjsonTarget && (isTopLevel && mode.includes("latest") || !isTopLevel && mode === "latest-all"); // Find any existing locks in the current package scope, making sure // locks are always in-range for their parent scope pjsons: const existingResolution = getResolution(this.installs, pkgName, isTopLevel ? null : pkgScope); if (!useLatestPjsonTarget && existingResolution && (isTopLevel || mode === "freeze" || await this.inRange(existingResolution.installUrl, pjsonTarget.pkgTarget))) { this.log("installer/install", `existing lock for ${pkgName} from ${parentUrl} in scope ${pkgScope} is ${JSON.stringify(existingResolution)}`); return existingResolution; } // Pick up resolutions from flattened scopes like 'https://ga.jspm.io/" // for secondary installs, if they're in range for the current pjson, or // if we're in a freeze install: if (!isTopLevel) { const flattenedResolution = getFlattenedResolution(this.installs, pkgName, pkgScope, traceSubpath); if (!useLatestPjsonTarget && flattenedResolution && (mode === "freeze" || await this.inRange(flattenedResolution.installUrl, pjsonTarget.pkgTarget))) { this.newInstalls = setResolution(this.installs, pkgName, flattenedResolution.installUrl, pkgScope, flattenedResolution.installSubpath); return flattenedResolution; } } // Use the pjson target if it exists: if (pjsonTarget) { return this.installTarget(pkgName, pjsonTarget, traceSubpath, mode, isTopLevel ? null : pkgScope, parentUrl); } // Try resolve the package as a built-in: const specifier = pkgName + (traceSubpath ? traceSubpath.slice(1) : ""); const builtin = this.resolver.resolveBuiltin(specifier); if (builtin) { if (typeof builtin === "string") return builtin; return this.installTarget(specifier, // TODO: either change the types so resolveBuiltin always returns a // fully qualified InstallTarget, or support string targets here. builtin.target, traceSubpath, mode, isTopLevel ? null : pkgScope, parentUrl); } // existing primary version fallback if (this.installs.primary[pkgName]) { const { installUrl } = getResolution(this.installs, pkgName, null); return { installUrl, installSubpath: null }; } // global install fallback const target = newPackageTarget("*", new URL(definitelyPkgScope), this.defaultRegistry, pkgName); const { installUrl } = await this.installTarget(pkgName, target, null, mode, isTopLevel ? null : pkgScope, parentUrl); return { installUrl, installSubpath: null }; } // Note: maintain this live instead of recomputing get pkgUrls() { const pkgUrls = new Set(); for (const pkgUrl of Object.values(this.installs.primary)){ pkgUrls.add(pkgUrl.installUrl); } for (const scope of Object.keys(this.installs.secondary)){ for (const { installUrl } of Object.values(this.installs.secondary[scope])){ pkgUrls.add(installUrl); } } for (const flatScope of Object.keys(this.installs.flattened)){ for (const { resolution: { installUrl } } of Object.values(this.installs.flattened[flatScope]).flat()){ pkgUrls.add(installUrl); } } return pkgUrls; } async getBestExistingMatch(matchPkg) { let bestMatch = null; for (const pkgUrl of this.pkgUrls){ const pkg = await this.resolver.parseUrlPkg(pkgUrl); if (pkg && await this.inRange(pkg.pkg, matchPkg)) { if (bestMatch) bestMatch = Semver.compare(new Semver(bestMatch.version), pkg.pkg.version) === -1 ? pkg.pkg : bestMatch; else bestMatch = pkg.pkg; } } return bestMatch; } async inRange(pkg, target) { var _this; // URL|null targets don't have ranges, so nothing is in-range for them: if (!target || target instanceof URL) return false; const pkgExact = typeof pkg === "string" ? (_this = await this.resolver.parseUrlPkg(pkg)) === null || _this === void 0 ? void 0 : _this.pkg : pkg; if (!pkgExact) return false; return pkgExact.registry === target.registry && pkgExact.name === target.name && target.ranges.some((range)=>range.has(pkgExact.version, true)); } // upgrade all existing packages to this package if possible tryUpgradeAllTo(pkg, pkgUrl, installed) { const pkgVersion = new Semver(pkg.version); let allCompatible = true; for (const { ranges } of installed){ if (ranges.every((range)=>!range.has(pkgVersion))) allCompatible = false; } if (!allCompatible) return false; // if every installed version can support this new version, update them all for (const { alias, pkgScope } of installed){ const resolution = getResolution(this.installs, alias, pkgScope); if (!resolution) continue; const { installSubpath } = resolution; this.newInstalls = setResolution(this.installs, alias, pkgUrl, pkgScope, installSubpath); } return true; } // upgrade some exsiting packages to the new install upgradeSupportedTo(pkg, pkgUrl, installed) { const pkgVersion = new Semver(pkg.version); for (const { alias, pkgScope, ranges } of installed){ const resolution = getResolution(this.installs, alias, pkgScope); if (!resolution) continue; if (!ranges.some((range)=>range.has(pkgVersion, true))) continue; const { installSubpath } = resolution; this.newInstalls = setResolution(this.installs, alias, pkgUrl, pkgScope, installSubpath); } } constructor(baseUrl, opts, log, resolver){ var _this_providers, _npm; _define_property(this, "opts", void 0); _define_property(this, "installs", void 0); _define_property(this, "constraints", void 0); _define_property(this, "newInstalls", false); // @ts-ignore _define_property(this, "installBaseUrl", void 0); _define_property(this, "hasLock", false); _define_property(this, "defaultProvider", { provider: "jspm.io", layer: "default" }); _define_property(this, "defaultRegistry", "npm"); _define_property(this, "providers", void 0); _define_property(this, "resolutions", void 0); _define_property(this, "log", void 0); _define_property(this, "resolver", void 0); this.log = log; this.resolver = resolver; this.resolver.installer = this; this.resolutions = opts.resolutions || {}; this.installBaseUrl = baseUrl; this.opts = opts; this.hasLock = !!opts.lock; this.installs = opts.lock || { primary: Object.create(null), secondary: Object.create(null), flattened: Object.create(null) }; this.constraints = { primary: Object.create(null), secondary: Object.create(null) }; if (opts.defaultRegistry) this.defaultRegistry = opts.defaultRegistry; if (opts.defaultProvider) this.defaultProvider = parseProviderStr(opts.defaultProvider); this.providers = Object.assign({}, registryProviders); var _; // TODO: this is a hack, as we currently don't have proper support for // providers owning particular registries. The proper way to do this would // be to have each provider declare what registries it supports, and // construct a providers mapping at init when we detect default provider: if (opts.defaultProvider.includes("deno")) (_ = (_this_providers = this.providers)[_npm = "npm:"]) !== null && _ !== void 0 ? _ : _this_providers[_npm] = "jspm.io"; if (opts.providers) Object.assign(this.providers, opts.providers); } } function parseProviderStr(provider) { const split = provider.split("#"); return { provider: split[0], layer: split[1] || "default" }; } //# sourceMappingURL=installer.js.map