@jspm/generator
Version:
Package Import Map Generation Tool
448 lines (446 loc) • 20.8 kB
JavaScript
import { throwInternalError } from "../common/err.js";
import { isPlain, isURL, resolveUrl } from "../common/url.js";
import { JspmError } from "../common/err.js";
import { newPackageTarget, parsePkg } from "./package.js";
// @ts-ignore
import sver from "sver";
import { getPackageConfig } from "../generator.js";
import { decodeBase64 } from "../common/b64.js";
const { Semver, SemverRange } = sver;
function enumerateParentScopes(url) {
const parentScopes = [];
let separatorIndex = url.lastIndexOf("/");
const protocolIndex = url.indexOf("://") + 1;
while((separatorIndex = url.lastIndexOf("/", separatorIndex - 1)) !== protocolIndex){
parentScopes.push(url.slice(0, separatorIndex + 1));
}
return parentScopes;
}
export function getResolution(resolutions, name, pkgScope) {
if (pkgScope && !pkgScope.endsWith("/")) throwInternalError(pkgScope);
if (!pkgScope) return resolutions.primary[name];
const scope = resolutions.secondary[pkgScope];
var _scope_name;
return (_scope_name = scope === null || scope === void 0 ? void 0 : scope[name]) !== null && _scope_name !== void 0 ? _scope_name : null;
}
export function getFlattenedResolution(resolutions, name, pkgScope, flattenedSubpath) {
// no current scope -> check the flattened scopes
const parentScopes = enumerateParentScopes(pkgScope);
for (const scopeUrl of parentScopes){
if (!resolutions.flattened[scopeUrl]) continue;
const flatResolutions = resolutions.flattened[scopeUrl][name];
if (!flatResolutions) continue;
for (const flatResolution of flatResolutions){
if (flatResolution.export === flattenedSubpath || flatResolution.export.endsWith("/") && flattenedSubpath.startsWith(flatResolution.export)) {
return flatResolution.resolution;
}
}
}
return null;
}
export function setConstraint(constraints, name, target, pkgScope = null) {
if (pkgScope === null) constraints.primary[name] = target;
else (constraints.secondary[pkgScope] = constraints.secondary[pkgScope] || Object.create(null))[name] = target;
}
export function setResolution(resolutions, name, installUrl, pkgScope = null, installSubpath = null) {
if (pkgScope && !pkgScope.endsWith("/")) throwInternalError(pkgScope);
if (pkgScope === null) {
const existing = resolutions.primary[name];
if (existing && existing.installUrl === installUrl && existing.installSubpath === installSubpath) return false;
resolutions.primary[name] = {
installUrl,
installSubpath
};
return true;
} else {
resolutions.secondary[pkgScope] = resolutions.secondary[pkgScope] || {};
const existing = resolutions.secondary[pkgScope][name];
if (existing && existing.installUrl === installUrl && existing.installSubpath === installSubpath) return false;
resolutions.secondary[pkgScope][name] = {
installUrl,
installSubpath
};
return true;
}
}
export function mergeLocks(resolutions, newResolutions) {
for (const pkg of Object.keys(newResolutions.primary)){
resolutions.primary[pkg] = newResolutions.primary[pkg];
}
for (const pkgUrl of Object.keys(newResolutions.secondary)){
if (resolutions[pkgUrl]) Object.assign(resolutions[pkgUrl] = Object.create(null), newResolutions[pkgUrl]);
else resolutions.secondary[pkgUrl] = newResolutions.secondary[pkgUrl];
}
for (const scopeUrl of Object.keys(newResolutions.flattened)){
if (resolutions[scopeUrl]) Object.assign(resolutions[scopeUrl], newResolutions[scopeUrl]);
else resolutions.flattened[scopeUrl] = newResolutions.flattened[scopeUrl];
}
}
export function mergeConstraints(constraints, newConstraints) {
for (const pkg of Object.keys(newConstraints.primary)){
constraints.primary[pkg] = newConstraints.primary[pkg];
}
for (const pkgUrl of Object.keys(newConstraints.secondary)){
if (constraints[pkgUrl]) Object.assign(constraints[pkgUrl] = Object.create(null), newConstraints[pkgUrl]);
else constraints.secondary[pkgUrl] = newConstraints.secondary[pkgUrl];
}
}
function toPackageTargetMap(pcfg, pkgUrl, defaultRegistry = "npm", includeDev = false) {
const constraints = Object.create(null);
if (pcfg.dependencies) for (const name of Object.keys(pcfg.dependencies)){
constraints[name] = newPackageTarget(pcfg.dependencies[name], pkgUrl, defaultRegistry, name).pkgTarget;
}
if (pcfg.peerDependencies) for (const name of Object.keys(pcfg.peerDependencies)){
if (name in constraints) continue;
constraints[name] = newPackageTarget(pcfg.peerDependencies[name], pkgUrl, defaultRegistry, name).pkgTarget;
}
if (pcfg.optionalDependencies) for (const name of Object.keys(pcfg.optionalDependencies)){
if (name in constraints) continue;
constraints[name] = newPackageTarget(pcfg.optionalDependencies[name], pkgUrl, defaultRegistry, name).pkgTarget;
}
if (includeDev && pcfg.devDependencies) for (const name of Object.keys(pcfg.devDependencies)){
if (name in constraints) continue;
constraints[name] = newPackageTarget(pcfg.devDependencies[name], pkgUrl, defaultRegistry, name).pkgTarget;
}
return constraints;
}
async function packageTargetFromExact(pkg, resolver, permitDowngrades = false) {
let registry, name, version;
if (pkg.registry === "node_modules") {
// The node_modules versions are always URLs to npm-installed packages:
const pkgUrl = decodeBase64(pkg.version);
const pcfg = await resolver.getPackageConfig(pkgUrl);
if (!pcfg) throw new JspmError(`Package ${pkgUrl} has no package config, cannot create package target.`);
if (!pcfg.name || !pcfg.version) throw new JspmError(`Package ${pkgUrl} has no name or version, cannot create package target.`);
name = pcfg.name;
version = pcfg.version;
registry = "npm";
} else {
// The other registries all use semver ranges:
({ registry, name, version } = pkg);
}
const v = new Semver(version);
if (v.tag) return {
registry,
name,
ranges: [
new SemverRange(version)
],
unstable: false
};
if (permitDowngrades) {
if (v.major !== 0) return {
registry,
name,
ranges: [
new SemverRange(v.major)
],
unstable: false
};
if (v.minor !== 0) return {
registry,
name,
ranges: [
new SemverRange(v.major + "." + v.minor)
],
unstable: false
};
return {
registry,
name,
ranges: [
new SemverRange(version)
],
unstable: false
};
} else {
return {
registry,
name,
ranges: [
new SemverRange("^" + version)
],
unstable: false
};
}
}
export function getConstraintFor(name, registry, constraints) {
const installs = [];
for (const [alias, target] of Object.entries(constraints.primary)){
if (!(target instanceof URL) && target.registry === registry && target.name === name) installs.push({
alias,
pkgScope: null,
ranges: target.ranges
});
}
for (const [pkgScope, scope] of Object.entries(constraints.secondary)){
for (const alias of Object.keys(scope)){
const target = scope[alias];
if (!(target instanceof URL) && target.registry === registry && target.name === name) installs.push({
alias,
pkgScope: pkgScope,
ranges: target.ranges
});
}
}
return installs;
}
export async function extractLockConstraintsAndMap(map, preloadUrls, mapUrl, rootUrl, defaultRegistry, resolver, // TODO: we should pass the whole providers namespace here so that we can
// enforce the user's URL-specific constraints on which provider to use:
provider) {
const locks = {
primary: Object.create(null),
secondary: Object.create(null),
flattened: Object.create(null)
};
const maps = {
imports: Object.create(null),
scopes: Object.create(null)
};
// Primary version constraints taken from the map configuration base (if found)
const primaryBase = await resolver.getPackageBase(mapUrl.href);
const primaryPcfg = await resolver.getPackageConfig(primaryBase);
const constraints = {
primary: primaryPcfg ? toPackageTargetMap(primaryPcfg, new URL(primaryBase), defaultRegistry, true) : Object.create(null),
secondary: Object.create(null)
};
const pkgUrls = new Set();
for (const key of Object.keys(map.imports || {})){
if (isPlain(key)) {
// Get the package name and subpath in package specifier space.
const parsedKey = parsePkg(key);
// Get the target package details in URL space:
let { parsedTarget, pkgUrl, subpath } = await resolveTargetPkg(map.imports[key], mapUrl, rootUrl, primaryBase, resolver, provider);
const exportSubpath = parsedTarget && await resolver.getExportResolution(pkgUrl, subpath, key);
pkgUrls.add(pkgUrl);
// If the plain specifier resolves to a package on some provider's CDN,
// and there's a corresponding import/export map entry in that package,
// then the resolution is standard and we can lock it:
if (exportSubpath) {
// Package "imports" resolutions don't constrain versions.
if (key[0] === "#") continue;
// Otherwise we treat top-level package versions as a constraint.
if (!constraints.primary[parsedKey.pkgName]) {
constraints.primary[parsedKey.pkgName] = await packageTargetFromExact(parsedTarget.pkg, resolver);
}
// In the case of subpaths having diverging versions, we force convergence on one version
// Only scopes permit unpacking
let installSubpath = null;
if (parsedKey.subpath !== exportSubpath) {
if (parsedKey.subpath === ".") {
installSubpath = exportSubpath;
} else if (exportSubpath === ".") {
installSubpath = false;
} else if (exportSubpath.endsWith(parsedKey.subpath.slice(1))) {
installSubpath = exportSubpath.slice(0, parsedKey.subpath.length);
}
}
if (installSubpath !== false) {
setResolution(locks, parsedKey.pkgName, pkgUrl, null, installSubpath);
continue;
}
}
// Another possibility is that the bare specifier is a remapping for the
// primary package's own-name, in which case we should check whether
// there's a corresponding export in the primary pjson:
if (primaryPcfg && primaryPcfg.name === parsedKey.pkgName) {
const exportSubpath = await resolver.getExportResolution(primaryBase, subpath, key);
// If the export subpath matches the key's subpath, then this is a
// standard resolution:
if (parsedKey.subpath === exportSubpath) continue;
}
}
// Fallback - this resolution is non-standard, so we need to record it as
// a custom import override:
maps.imports[isPlain(key) ? key : resolveUrl(key, mapUrl, rootUrl)] = resolveUrl(map.imports[key], mapUrl, rootUrl);
}
for (const scopeUrl of Object.keys(map.scopes || {})){
var _resolveUrl;
const resolvedScopeUrl = (_resolveUrl = resolveUrl(scopeUrl, mapUrl, rootUrl)) !== null && _resolveUrl !== void 0 ? _resolveUrl : scopeUrl;
const scopePkgUrl = await resolver.getPackageBase(resolvedScopeUrl);
const flattenedScope = new URL(scopePkgUrl).pathname === "/";
pkgUrls.add(scopePkgUrl);
const scope = map.scopes[scopeUrl];
for (const key of Object.keys(scope)){
if (isPlain(key)) {
// Get the package name and subpath in package specifier space.
const parsedKey = parsePkg(key);
// Get the target package details in URL space:
let { parsedTarget, pkgUrl, subpath } = await resolveTargetPkg(scope[key], mapUrl, rootUrl, scopePkgUrl, resolver, provider);
pkgUrls.add(pkgUrl);
const exportSubpath = parsedTarget && await resolver.getExportResolution(pkgUrl, subpath, key);
// TODO: we don't handle trailing-slash mappings here at all, which
// leads to them sticking around in the import map as custom
// resolutions forever.
if (exportSubpath) {
// Imports resolutions that resolve as expected can be skipped
if (key[0] === "#") continue;
// If there is no constraint, we just make one as the semver major on the current version
if (!constraints.primary[parsedKey.pkgName]) constraints.primary[parsedKey.pkgName] = parsedTarget ? await packageTargetFromExact(parsedTarget.pkg, resolver) : new URL(pkgUrl);
// In the case of subpaths having diverging versions, we force convergence on one version
// Only scopes permit unpacking
let installSubpath = null;
if (parsedKey.subpath !== exportSubpath) {
if (parsedKey.subpath === ".") {
installSubpath = exportSubpath;
} else if (exportSubpath === ".") {
installSubpath = false;
} else {
if (exportSubpath.endsWith(parsedKey.subpath.slice(1))) installSubpath = exportSubpath.slice(0, parsedKey.subpath.length);
}
}
if (installSubpath !== false) {
if (flattenedScope) {
const flattened = locks.flattened[scopePkgUrl] = locks.flattened[scopePkgUrl] || {};
flattened[parsedKey.pkgName] = flattened[parsedKey.pkgName] || [];
flattened[parsedKey.pkgName].push({
export: parsedKey.subpath,
resolution: {
installUrl: pkgUrl,
installSubpath
}
});
} else {
setResolution(locks, parsedKey.pkgName, pkgUrl, scopePkgUrl, installSubpath);
}
continue;
}
}
}
// Fallback -> Custom import with normalization
(maps.scopes[resolvedScopeUrl] = maps.scopes[resolvedScopeUrl] || Object.create(null))[isPlain(key) ? key : resolveUrl(key, mapUrl, rootUrl)] = resolveUrl(scope[key], mapUrl, rootUrl);
}
}
// for every package we resolved, add their package constraints into the list of constraints
await Promise.all([
...pkgUrls
].map(async (pkgUrl)=>{
if (!isURL(pkgUrl)) return;
const pcfg = await getPackageConfig(pkgUrl);
if (pcfg) constraints.secondary[pkgUrl] = toPackageTargetMap(pcfg, new URL(pkgUrl), defaultRegistry, false);
}));
// TODO: allow preloads to inform used versions somehow
// for (const url of preloadUrls) {
// const resolved = resolveUrl(url, mapUrl, rootUrl).href;
// const providerPkg = resolver.parseUrlPkg(resolved);
// if (providerPkg) {
// const pkgUrl = await resolver.getPackageBase(mapUrl.href);
// }
// }
return {
maps,
constraints,
locks: await enforceProviderConstraints(locks, provider, resolver, primaryBase)
};
}
/**
* Enforces the user's provider constraints, which map subsets of URL-space to
* the provider that should be used to resolve them. Constraints are enforced
* by re-resolving every input map lock and constraint against the provider
* for their parent package URL.
* TODO: actually handle provider constraints
*/ async function enforceProviderConstraints(locks, provider, resolver, basePkgUrl) {
const res = {
primary: {},
secondary: {},
flattened: {}
};
for (const [pkgName, lock] of Object.entries(locks.primary)){
const { installUrl, installSubpath } = await translateLock(lock, provider, resolver, basePkgUrl);
setResolution(res, pkgName, installUrl, null, installSubpath);
}
for (const [pkgUrl, pkgLocks] of Object.entries(locks.secondary)){
for (const [pkgName, lock] of Object.entries(pkgLocks)){
const { installUrl, installSubpath } = await translateLock(lock, provider, resolver, pkgUrl);
setResolution(res, pkgName, installUrl, pkgUrl, installSubpath);
}
}
for (const [scopeUrl, pkgLocks] of Object.entries(locks.flattened)){
res.flattened[scopeUrl] = {};
for (const [pkgName, locks] of Object.entries(pkgLocks)){
res.flattened[scopeUrl][pkgName] = [];
for (const lock of locks){
const newLock = await translateLock(lock.resolution, provider, resolver, scopeUrl);
res.flattened[scopeUrl][pkgName].push({
export: lock.export,
resolution: newLock
});
}
}
}
return res;
}
async function translateLock(lock, provider, resolver, parentUrl) {
const mdl = await resolver.parseUrlPkg(lock.installUrl);
if (!mdl) return lock; // no provider owns it, nothing to translate
const parentPkgUrl = await resolver.getPackageBase(parentUrl);
const newMdl = await translateProvider(mdl, provider, resolver, parentPkgUrl);
if (!newMdl) {
// TODO: we should throw here once parent scoping is implemented
// throw new JspmError(
// `Failed to translate ${lock.installUrl} to provider ${provider.provider}.`
// );
return lock;
}
return {
installUrl: await resolver.pkgToUrl(newMdl.pkg, provider),
installSubpath: lock.installSubpath
};
}
export async function translateProvider(mdl, { provider, layer }, resolver, parentUrl) {
const pkg = mdl.pkg;
if ((pkg.registry === "deno" || pkg.registry === "denoland") && provider === "deno") {
return mdl; // nothing to do if translating deno-to-deno
} else if (pkg.registry === "deno" || pkg.registry === "denoland" || provider === "deno") {
// TODO: we should throw here once parent scoping is implemented
// throw new JspmError(
// "Cannot translate packages between the 'deno' provider and other providers."
// );
return null;
}
const fromNodeModules = pkg.registry === "node_modules";
const toNodeModules = provider === "nodemodules";
if (fromNodeModules === toNodeModules) {
return {
...mdl,
source: {
provider,
layer
}
};
}
const target = await packageTargetFromExact(pkg, resolver);
let latestPkg;
try {
latestPkg = await resolver.resolveLatestTarget(target, {
provider,
layer
}, parentUrl);
} catch (err) {
// TODO: we should throw here once parent scoping is implemented
// throw new JspmError(
// `Failed to translate package ${pkg.name}@${pkg.version} to provider ${provider}.`
// );
return null;
}
return {
pkg: latestPkg,
source: {
provider,
layer
},
subpath: mdl.subpath
};
}
async function resolveTargetPkg(moduleUrl, mapUrl, rootUrl, parentUrl, resolver, provider) {
let targetUrl = resolveUrl(moduleUrl, mapUrl, rootUrl);
let parsedTarget = await resolver.parseUrlPkg(targetUrl);
let pkgUrl = parsedTarget ? await resolver.pkgToUrl(parsedTarget.pkg, parsedTarget.source) : await resolver.getPackageBase(targetUrl);
const subpath = "." + targetUrl.slice(pkgUrl.length - 1);
return {
parsedTarget,
pkgUrl,
subpath
};
}
//# sourceMappingURL=lock.js.map