@jspm/generator
Version:
Package Import Map Generation Tool
512 lines (510 loc) • 28 kB
JavaScript
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 { getFlattenedResolution, getResolution, resolveScopeGroup } 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 {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 }, mode, pkgScope, parentUrl) {
var // Otherwise we install latest and make an attempt to upgrade any existing
// locks that are compatible to the latest version:
_this_log, _this;
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);
if (JSON.stringify(pkgTarget) !== JSON.stringify(resolutionTarget.pkgTarget)) return this.installTarget(pkgName, resolutionTarget, mode, pkgScope, parentUrl);
}
// URL targets are installed as locks directly, as we have no versioning
// information to work with:
if (pkgTarget instanceof URL) {
var _this_log1, _this1;
const installHref = pkgTarget.href;
const installUrl = installHref + (installHref.endsWith('/') ? '' : '/');
(_this_log1 = (_this1 = this).log) === null || _this_log1 === void 0 ? void 0 : _this_log1.call(_this1, 'installer/installTarget', `${pkgName} ${pkgScope} -> ${installHref} (URL)`);
this.newInstalls = this.setResolution(pkgName, installUrl, pkgScope);
return {
installUrl
};
}
const provider = this.getProvider(pkgTarget);
// For secondary installs, prefer the primary resolution if it satisfies
// our range. This prevents unnecessary version splits where a stale
// higher version from getBestExistingMatch would override the primary:
if (pkgScope !== null && this.installs.primary[pkgName]) {
const primaryUrl = this.installs.primary[pkgName].installUrl;
if (await this.inRange(primaryUrl, pkgTarget)) {
var _this_log2, _this2;
(_this_log2 = (_this2 = this).log) === null || _this_log2 === void 0 ? void 0 : _this_log2.call(_this2, 'installer/installTarget', `${pkgName} ${pkgScope} -> ${primaryUrl} (primary consolidation)`);
this.newInstalls = this.setResolution(pkgName, primaryUrl, pkgScope);
this.setConstraint(pkgName, pkgTarget, pkgScope);
return {
installUrl: primaryUrl
};
}
}
// 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) {
var _this_log3, _this3;
(_this_log3 = (_this3 = this).log) === null || _this_log3 === void 0 ? void 0 : _this_log3.call(_this3, 'installer/installTarget', `${pkgName} ${pkgScope} -> ${JSON.stringify(pkg)} (existing match)`);
const installUrl = await this.resolver.pm.pkgToUrl(pkg, provider.provider, provider.layer);
this.newInstalls = this.setResolution(pkgName, installUrl, pkgScope);
this.setConstraint(pkgName, pkgTarget, pkgScope);
return {
installUrl
};
}
}
const latestPkg = await this.resolver.pm.resolveLatestTarget(pkgTarget, provider, parentUrl, this.resolver);
const pkgUrl = await this.resolver.pm.pkgToUrl(latestPkg, provider.provider, provider.layer);
const installed = this.getConstraintFor(latestPkg.name, latestPkg.registry);
// If there's an existing primary at a different version, check whether
// the primary can be upgraded to latest. If it can, great - use latest.
// If it can't, consolidate with the primary if our range allows it.
// Otherwise we have to split (fall through to install latest):
if (this.installs.primary[pkgName]) {
const primaryUrl = this.installs.primary[pkgName].installUrl;
if (primaryUrl !== pkgUrl) {
// Can all existing installs be upgraded to latest?
if (mode !== 'freeze' && this.tryUpgradeAllTo(latestPkg, pkgUrl, installed)) {
// Yes - fall through to install latest
} else {
// No - can we consolidate with the primary instead?
if (await this.inRange(primaryUrl, pkgTarget)) {
var _this_log4, _this4;
(_this_log4 = (_this4 = this).log) === null || _this_log4 === void 0 ? void 0 : _this_log4.call(_this4, 'installer/installTarget', `${pkgName} ${pkgScope} -> ${primaryUrl} (primary consolidation)`);
this.newInstalls = this.setResolution(pkgName, primaryUrl, pkgScope);
this.setConstraint(pkgName, pkgTarget, pkgScope);
return {
installUrl: primaryUrl
};
}
// No - for secondary installs, try best existing match before splitting:
if (!isTopLevel) {
const pkg = await this.getBestExistingMatch(pkgTarget);
if (pkg) {
var _this_log5, _this5;
(_this_log5 = (_this5 = this).log) === null || _this_log5 === void 0 ? void 0 : _this_log5.call(_this5, 'installer/installTarget', `${pkgName} ${pkgScope} -> ${JSON.stringify(latestPkg)} (existing match not latest)`);
const installUrl = await this.resolver.pm.pkgToUrl(pkg, provider.provider, provider.layer);
this.newInstalls = this.setResolution(pkgName, installUrl, pkgScope);
this.setConstraint(pkgName, pkgTarget, pkgScope);
return {
installUrl
};
}
}
// Split - fall through to install latest
}
}
} else if (mode !== 'freeze' && !useLatest && !isTopLevel && latestPkg && !this.tryUpgradeAllTo(latestPkg, pkgUrl, installed)) {
// Secondary install with no primary: try best existing match
const pkg = await this.getBestExistingMatch(pkgTarget);
if (pkg) {
var _this_log6, _this6;
(_this_log6 = (_this6 = this).log) === null || _this_log6 === void 0 ? void 0 : _this_log6.call(_this6, 'installer/installTarget', `${pkgName} ${pkgScope} -> ${JSON.stringify(latestPkg)} (existing match not latest)`);
const installUrl = await this.resolver.pm.pkgToUrl(pkg, provider.provider, provider.layer);
this.newInstalls = this.setResolution(pkgName, installUrl, pkgScope);
this.setConstraint(pkgName, pkgTarget, pkgScope);
return {
installUrl
};
}
}
(_this_log = (_this = this).log) === null || _this_log === void 0 ? void 0 : _this_log.call(_this, 'installer/installTarget', `${pkgName} ${pkgScope} -> ${pkgUrl} (latest)`);
this.newInstalls = this.setResolution(pkgName, pkgUrl, pkgScope);
this.setConstraint(pkgName, pkgTarget, pkgScope);
if (mode !== 'freeze') this.upgradeSupportedTo(latestPkg, pkgUrl, installed);
return {
installUrl: pkgUrl
};
}
async installBuiltin(install, specifier, mode, parentUrl) {
var _this_log, _this;
(_this_log = (_this = this).log) === null || _this_log === void 0 ? void 0 : _this_log.call(_this, 'installer/install', `installing builtin ${specifier} from ${parentUrl}`);
if (this.resolutions[install.name]) return this.installTarget(install.name, newPackageTarget(this.resolutions[install.name], this.opts.baseUrl, this.defaultRegistry, install.name), mode, null, parentUrl);
const useLatestPjsonTarget = mode.includes('latest');
// 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, install.name, null);
if (!useLatestPjsonTarget && existingResolution) {
var _this_log1, _this1;
(_this_log1 = (_this1 = this).log) === null || _this_log1 === void 0 ? void 0 : _this_log1.call(_this1, 'installer/install', `existing lock for ${install.name} from ${parentUrl} is ${JSON.stringify(existingResolution)}`);
return existingResolution;
}
// existing primary version fallback
if (this.installs.primary[install.name]) {
const { installUrl } = getResolution(this.installs, install.name, null);
return {
installUrl
};
}
// global install fallback
const definitelyPkgScope = await this.installBaseUrl;
const target = newPackageTarget('*', new URL(definitelyPkgScope), this.defaultRegistry, install.name);
const { installUrl } = await this.installTarget(install.name, target, mode, null, parentUrl);
return {
installUrl
};
}
/**
* 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) {
var _this_log, _this, _pcfg_dependencies, _pcfg_peerDependencies, _pcfg_optionalDependencies, _pcfg_devDependencies;
(_this_log = (_this = this).log) === null || _this_log === void 0 ? void 0 : _this_log.call(_this, '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;
const versionScope = !isTopLevel && pkgScope ? resolveScopeGroup(pkgScope, this.opts.linkedScopes) : pkgScope;
if (this.resolutions[pkgName]) return this.installTarget(pkgName, newPackageTarget(this.resolutions[pkgName], this.opts.baseUrl, this.defaultRegistry, pkgName), mode, isTopLevel ? null : versionScope, parentUrl);
// Fetch the version scope's pjson for dependency ranges:
const definitelyVersionScope = versionScope || await this.resolver.getPackageBase(parentUrl);
const pcfg = await this.resolver.getPackageConfig(definitelyVersionScope) || {};
// 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(definitelyVersionScope), this.defaultRegistry, pkgName);
const useLatestPjsonTarget = !!pjsonTarget && (isTopLevel && mode.includes('latest') || !isTopLevel && mode === 'latest-all');
// Find any existing locks in the version scope, making sure
// locks are always in-range for their parent scope pjsons:
const existingResolution = getResolution(this.installs, pkgName, isTopLevel ? null : versionScope);
if (!useLatestPjsonTarget && existingResolution && (isTopLevel || mode === 'freeze' || await this.inRange(existingResolution.installUrl, pjsonTarget.pkgTarget))) {
var _this_log1, _this1;
(_this_log1 = (_this1 = this).log) === null || _this_log1 === void 0 ? void 0 : _this_log1.call(_this1, '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 && this.opts.inputMapFallbacks !== false) {
const semverCompatible = this.opts.inputMapFallbacks === 'semver-compatible';
const flattenedResolution = await getFlattenedResolution(this.installs, pkgName, versionScope, traceSubpath, semverCompatible, semverCompatible && pjsonTarget ? async (installUrl)=>this.inRange(installUrl, pjsonTarget.pkgTarget) : null);
if (!useLatestPjsonTarget && flattenedResolution && (mode === 'freeze' && !semverCompatible || pjsonTarget && await this.inRange(flattenedResolution.installUrl, pjsonTarget.pkgTarget))) {
this.newInstalls = this.setResolution(pkgName, flattenedResolution.installUrl, versionScope);
return flattenedResolution;
}
}
// Use the pjson target if it exists:
if (pjsonTarget) {
return this.installTarget(pkgName, pjsonTarget, mode, isTopLevel ? null : versionScope, parentUrl);
}
// existing primary version fallback
if (this.installs.primary[pkgName]) {
const { installUrl } = getResolution(this.installs, pkgName, null);
return {
installUrl
};
}
// global install fallback
const target = newPackageTarget('*', new URL(definitelyVersionScope), this.defaultRegistry, pkgName);
const { installUrl } = await this.installTarget(pkgName, target, mode, isTopLevel ? null : versionScope, parentUrl);
return {
installUrl
};
}
// Cached set of package URLs - built once then maintained incrementally
get pkgUrls() {
if (this._pkgUrls) return this._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);
}
}
this._pkgUrls = pkgUrls;
return pkgUrls;
}
// Set resolution and maintain caches incrementally
setResolution(name, installUrl, pkgScope = null) {
let changed;
if (pkgScope === null) {
const existing = this.installs.primary[name];
if (existing && existing.installUrl === installUrl) {
changed = false;
} else {
this.installs.primary[name] = {
installUrl
};
changed = true;
}
} else {
this.installs.secondary[pkgScope] = this.installs.secondary[pkgScope] || {};
const existing = this.installs.secondary[pkgScope][name];
if (existing && existing.installUrl === installUrl) {
changed = false;
} else {
this.installs.secondary[pkgScope][name] = {
installUrl
};
changed = true;
}
}
// Maintain the pkgUrls cache incrementally
if (this._pkgUrls) {
this._pkgUrls.add(installUrl);
}
// Maintain the pkgsByKey index incrementally
if (this._pkgsByKey && changed) {
const parsed = this.resolver.pm.parseUrlPkg(installUrl);
if (parsed) {
const key = `${parsed.pkg.registry}:${parsed.pkg.name}`;
const list = this._pkgsByKey.get(key);
if (list) {
// Check if this exact version is already in the list
if (!list.some((p)=>p.version === parsed.pkg.version)) {
list.push(parsed.pkg);
}
} else {
this._pkgsByKey.set(key, [
parsed.pkg
]);
}
}
}
return changed;
}
// Set constraint and maintain the index incrementally
setConstraint(name, target, pkgScope = null) {
if (pkgScope === null) {
// Don't overwrite a specific primary constraint with a wildcard one.
// This prevents subpath installs (e.g. react/jsx-runtime → react@*)
// from diluting an explicit version constraint (e.g. react@18.3.1):
const existing = this.constraints.primary[name];
if (existing && !(existing instanceof URL) && !(target instanceof URL) && !existing.range.isWildcard && target.range.isWildcard) return;
this.constraints.primary[name] = target;
} else {
(this.constraints.secondary[pkgScope] = this.constraints.secondary[pkgScope] || Object.create(null))[name] = target;
}
// Invalidate the index - it will be rebuilt lazily on next access
// This is simpler and safer than trying to maintain it incrementally
// since setConstraint can update existing constraints
this._constraintsByKey = null;
}
// Get constraints for a package, building index lazily
getConstraintFor(name, registry) {
if (!this._constraintsByKey) {
// Build the index
const index = new Map();
for (const [alias, target] of Object.entries(this.constraints.primary)){
if (target instanceof URL) continue;
const key = `${target.registry}:${target.name}`;
const constraint = {
alias,
pkgScope: null,
range: target.range
};
const list = index.get(key);
if (list) list.push(constraint);
else index.set(key, [
constraint
]);
}
for (const [pkgScope, scope] of Object.entries(this.constraints.secondary)){
for (const [alias, target] of Object.entries(scope)){
if (target instanceof URL) continue;
const key = `${target.registry}:${target.name}`;
const constraint = {
alias,
pkgScope: pkgScope,
range: target.range
};
const list = index.get(key);
if (list) list.push(constraint);
else index.set(key, [
constraint
]);
}
}
this._constraintsByKey = index;
}
return this._constraintsByKey.get(`${registry}:${name}`) || [];
}
// Build the package index lazily
buildPkgIndex() {
if (this._pkgsByKey) return this._pkgsByKey;
const index = new Map();
for (const pkgUrl of this.pkgUrls){
const parsed = this.resolver.pm.parseUrlPkg(pkgUrl);
if (parsed) {
const key = `${parsed.pkg.registry}:${parsed.pkg.name}`;
const list = index.get(key);
if (list) {
if (!list.some((p)=>p.version === parsed.pkg.version)) {
list.push(parsed.pkg);
}
} else {
index.set(key, [
parsed.pkg
]);
}
}
}
this._pkgsByKey = index;
return index;
}
async getBestExistingMatch(matchPkg) {
const index = this.buildPkgIndex();
const key = `${matchPkg.registry}:${matchPkg.name}`;
const candidates = index.get(key);
if (!candidates) return null;
let bestMatch = null;
for (const pkg of candidates){
if (matchPkg.range.has(pkg.version, true)) {
if (bestMatch) {
bestMatch = Semver.compare(new Semver(bestMatch.version), pkg.version) === -1 ? pkg : bestMatch;
} else {
bestMatch = 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.pm.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.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 { range } of installed){
if (!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;
this.newInstalls = this.setResolution(alias, pkgUrl, pkgScope);
}
return true;
}
// upgrade some exsiting packages to the new install
upgradeSupportedTo(pkg, pkgUrl, installed) {
const pkgVersion = new Semver(pkg.version);
for (const { alias, pkgScope, range } of installed){
const resolution = getResolution(this.installs, alias, pkgScope);
if (!resolution) continue;
if (!range.has(pkgVersion, true)) continue;
this.newInstalls = this.setResolution(alias, pkgUrl, pkgScope);
}
}
constructor(baseUrl, opts, log, resolver){
var _this_providers, _npm, _;
var _opts_defaultProvider;
_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);
// Cached set of package URLs - maintained incrementally for performance
_define_property(this, "_pkgUrls", null);
// Index of packages by registry:name for fast lookup in getBestExistingMatch
_define_property(this, "_pkgsByKey", null);
// Index of constraints by registry:name for fast lookup
_define_property(this, "_constraintsByKey", null);
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);
// 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 = opts.defaultProvider) === null || _opts_defaultProvider === void 0 ? void 0 : _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