@jspm/generator
Version:
Package Import Map Generation Tool
317 lines (315 loc) • 16.7 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 { importedFrom, isFetchProtocol, isPlain, isURL, resolveUrl } from "../common/url.js";
import { Installer } from "../install/installer.js";
import { JspmError, throwInternalError } from "../common/err.js";
import { parsePkg } from "../install/package.js";
// @ts-ignore
import { ImportMap, getMapMatch, getScopeMatches } from "@jspm/import-map";
import { isBuiltinScheme, isMappableScheme } from "./resolver.js";
import { mergeConstraints, mergeLocks, extractLockConstraintsAndMap } from "../install/lock.js";
function combineSubpaths(installSubpath, traceSubpath) {
if (traceSubpath.endsWith("/")) throw new Error("Trailing slash subpaths unsupported");
return installSubpath === null || installSubpath === "." || traceSubpath === "." ? installSubpath || traceSubpath : `${installSubpath}${traceSubpath.slice(1)}`;
}
class TraceMap {
async addInputMap(map, mapUrl = this.mapUrl, rootUrl = this.rootUrl, preloads) {
// Note: integrity is currently ignored for inputMaps, instead integrity
// is always trusted at generation time.
return this.processInputMap = this.processInputMap.then(async ()=>{
const inMap = new ImportMap({
map,
mapUrl,
rootUrl
}).rebase(this.mapUrl, this.rootUrl);
const pins = Object.keys(inMap.imports || []);
for (const pin of pins){
if (!this.pins.includes(pin)) this.pins.push(pin);
}
const { maps, locks, constraints } = await extractLockConstraintsAndMap(inMap, preloads, mapUrl, rootUrl, this.installer.defaultRegistry, this.resolver, this.installer.defaultProvider);
this.inputMap.extend(maps);
mergeLocks(this.installer.installs, locks);
mergeConstraints(this.installer.constraints, constraints);
});
}
/**
* Resolves, analyses and recursively visits the given module specifier and all of its dependencies.
*
* @param {string} specifier Module specifier to visit.
* @param {VisitOpts} opts Visitor configuration.
* @param {} parentUrl URL of the parent context for the specifier.
* @param {} seen Cache for optimisation.
*/ async visit(specifier, opts, parentUrl = this.baseUrl.href, seen = new Set()) {
var _this_opts_ignore;
if (!parentUrl) throw new Error("Internal error: expected parentUrl");
if ((_this_opts_ignore = this.opts.ignore) === null || _this_opts_ignore === void 0 ? void 0 : _this_opts_ignore.includes(specifier)) return;
if (seen.has(`${specifier}##${parentUrl}`)) return;
seen.add(`${specifier}##${parentUrl}`);
this.log("tracemap/visit", `Attempting to resolve ${specifier} to a module from ${parentUrl}, toplevel=${opts.toplevel}, mode=${opts.installMode}`);
const resolved = await this.resolve(specifier, parentUrl, opts.installMode, opts.toplevel);
// We support analysis of CommonJS modules for local workflows, where it's
// very likely that the user has some CommonJS dependencies, but this is
// something that the user has to explicitly enable:
if (isBuiltinScheme(resolved)) return null;
if (resolved.endsWith("/")) throw new JspmError(`Trailing "/" installs not supported installing ${resolved} for ${parentUrl}`);
try {
var entry = await this.resolver.analyze(resolved);
} catch (e) {
if (e instanceof JspmError) throw new Error(`Unable to analyze ${resolved} imported from ${parentUrl}: ${e.message}`);
else throw new Error(`Unable to analyze ${resolved} imported from ${parentUrl}`, {
cause: e
});
}
if (entry === null) {
throw new Error(`Module not found ${resolved} imported from ${parentUrl}`);
}
if ((entry === null || entry === void 0 ? void 0 : entry.format) === "commonjs" && entry.usesCjs && !this.opts.commonJS) {
throw new JspmError(`Unable to trace ${resolved}, as it is a CommonJS module. Either enable CommonJS tracing explicitly by setting "GeneratorOptions.commonJS" to true, or use a provider that performs ESM transpiling like jspm.io via defaultProvider: 'jspm.io'.`);
}
if (opts.visitor) {
const stop = await opts.visitor(specifier, parentUrl, resolved, opts.toplevel, entry);
if (stop) return;
}
if (!entry) return;
let allDeps = [
...entry.deps
];
if (entry.dynamicDeps.length && !opts.static) {
for (const dep of entry.dynamicDeps){
if (!allDeps.includes(dep)) allDeps.push(dep);
}
}
if (entry.cjsLazyDeps && !opts.static) {
for (const dep of entry.cjsLazyDeps){
if (!allDeps.includes(dep)) allDeps.push(dep);
}
}
if (opts.toplevel && (isMappableScheme(specifier) || isPlain(specifier))) {
opts = {
...opts,
toplevel: false
};
}
await Promise.all(allDeps.map(async (dep)=>{
// Special wildcard tracing syntax for dynamic imports, todo
if (dep.indexOf("\x10") !== -1) {
this.log("todo", "Handle wildcard trace " + dep + " in " + resolved);
return;
}
await this.visit(dep, opts, resolved, seen);
}));
}
async extractMap(modules, integrity) {
const map = new ImportMap({
mapUrl: this.mapUrl,
rootUrl: this.rootUrl
});
// note this plucks custom top-level custom imports
// we may want better control over this
map.extend(this.inputMap);
// visit to build the mappings
const staticList = new Set();
const dynamicList = new Set();
const dynamics = [];
let list = staticList;
const visitor = async (specifier, parentUrl, resolved, toplevel, entry)=>{
if (!list.has(resolved)) list.add(resolved);
// no entry applies to builtins
if (entry) {
if (integrity) map.setIntegrity(resolved, entry.integrity);
for (const dep of entry.dynamicDeps){
dynamics.push([
dep,
resolved
]);
}
}
if (toplevel) {
if (isPlain(specifier) || isMappableScheme(specifier)) {
var _this_resolver_getAnalysis;
const existing = map.imports[specifier];
if (!existing || existing !== resolved && ((_this_resolver_getAnalysis = this.resolver.getAnalysis(parentUrl)) === null || _this_resolver_getAnalysis === void 0 ? void 0 : _this_resolver_getAnalysis.wasCjs)) {
map.set(specifier, resolved);
}
}
} else {
if (isPlain(specifier) || isMappableScheme(specifier)) {
var _map_scopes_scopeUrl, _map_scopes_parentUrl;
const scopeUrl = await this.resolver.getPackageBase(parentUrl);
const existing = (_map_scopes_scopeUrl = map.scopes[scopeUrl]) === null || _map_scopes_scopeUrl === void 0 ? void 0 : _map_scopes_scopeUrl[specifier];
if (!existing) {
map.set(specifier, resolved, scopeUrl);
} else if (existing !== resolved && ((_map_scopes_parentUrl = map.scopes[parentUrl]) === null || _map_scopes_parentUrl === void 0 ? void 0 : _map_scopes_parentUrl[specifier]) !== resolved) {
map.set(specifier, resolved, parentUrl);
}
}
}
};
const seen = new Set();
await Promise.all(modules.map(async (module)=>{
await this.visit(module, {
static: true,
visitor,
installMode: "freeze",
toplevel: true
}, this.baseUrl.href, seen);
}));
list = dynamicList;
await Promise.all(dynamics.map(async ([specifier, parent])=>{
await this.visit(specifier, {
visitor,
installMode: "freeze",
toplevel: false
}, parent, seen);
}));
if (this.installer.newInstalls) {
// Disabled as it obscures valid errors
// console.warn('Unexpected resolution divergence.');
}
return {
map,
staticDeps: [
...staticList
],
dynamicDeps: [
...dynamicList
]
};
}
async add(name, target, opts) {
await this.installer.installTarget(name, target, null, opts, null, this.mapUrl.href);
}
/**
* @returns `resolved` - either a URL `string` pointing to the module or `null` if the specifier should be ignored.
*/ async resolve(specifier, parentUrl, installOpts, toplevel) {
const parentAnalysis = this.resolver.getAnalysis(parentUrl);
const cjsEnv = parentAnalysis === null || parentAnalysis === void 0 ? void 0 : parentAnalysis.wasCjs;
const parentPkgUrl = await this.resolver.getPackageBase(parentUrl);
if (!parentPkgUrl) throwInternalError();
const parentIsCjs = (parentAnalysis === null || parentAnalysis === void 0 ? void 0 : parentAnalysis.format) === "commonjs";
if ((!isPlain(specifier) || specifier === "..") && !isMappableScheme(specifier)) {
let resolvedUrl = new URL(specifier, parentUrl);
if (!isFetchProtocol(resolvedUrl.protocol)) throw new JspmError(`Found unexpected protocol ${resolvedUrl.protocol}${importedFrom(parentUrl)}`);
const resolvedHref = resolvedUrl.href;
let finalized = await this.resolver.realPath(await this.resolver.finalizeResolve(resolvedHref, parentIsCjs, false, parentPkgUrl));
// handle URL mappings
const urlResolved = this.inputMap.resolve(finalized, parentUrl);
// TODO: avoid this hack - perhaps solved by conditional maps
if (urlResolved !== finalized && !urlResolved.startsWith("node:") && !urlResolved.startsWith("deno:")) {
finalized = urlResolved;
}
if (finalized !== resolvedHref) {
this.inputMap.set(resolvedHref.endsWith("/") ? resolvedHref.slice(0, -1) : resolvedHref, finalized);
resolvedUrl = new URL(finalized);
}
this.log("tracemap/resolve", `${specifier} ${parentUrl} -> ${resolvedUrl} (URL resolution)`);
return resolvedUrl.href;
}
// Subscope override
const scopeMatches = getScopeMatches(parentUrl, this.inputMap.scopes, this.inputMap.mapUrl);
const pkgSubscopes = scopeMatches.filter(([, url])=>url.startsWith(parentPkgUrl));
if (pkgSubscopes.length) {
for (const [scope] of pkgSubscopes){
const mapMatch = getMapMatch(specifier, this.inputMap.scopes[scope]);
if (mapMatch) {
const resolved = await this.resolver.realPath(resolveUrl(this.inputMap.scopes[scope][mapMatch] + specifier.slice(mapMatch.length), this.inputMap.mapUrl, this.inputMap.rootUrl));
this.log("tracemap/resolve", `${specifier} ${parentUrl} -> ${resolved} (subscope resolution)`);
return resolved;
}
}
}
// Scope override
// TODO: isn't this subsumed by previous check?
const userScopeMatch = scopeMatches.find(([, url])=>url === parentPkgUrl);
if (userScopeMatch) {
const imports = this.inputMap.scopes[userScopeMatch[0]];
const userImportsMatch = getMapMatch(specifier, imports);
const userImportsResolved = userImportsMatch ? await this.resolver.realPath(resolveUrl(imports[userImportsMatch] + specifier.slice(userImportsMatch.length), this.inputMap.mapUrl, this.inputMap.rootUrl)) : null;
if (userImportsResolved) {
this.log("tracemap/resolve", `${specifier} ${parentUrl} -> ${userImportsResolved} (scope resolution)`);
return userImportsResolved;
}
}
// User import overrides
const userImportsMatch = getMapMatch(specifier, this.inputMap.imports);
const userImportsResolved = userImportsMatch ? await this.resolver.realPath(resolveUrl(this.inputMap.imports[userImportsMatch] + specifier.slice(userImportsMatch.length), this.inputMap.mapUrl, this.inputMap.rootUrl)) : null;
if (userImportsResolved) {
this.log("tracemap/resolve", `${specifier} ${parentUrl} -> ${userImportsResolved} (imports resolution)`);
return userImportsResolved;
}
const parsed = parsePkg(specifier);
if (!parsed) throw new JspmError(`Invalid package name ${specifier}`);
const { pkgName, subpath } = parsed;
// Own name import
const pcfg = await this.resolver.getPackageConfig(parentPkgUrl) || {};
if (pcfg.exports && pcfg.name === pkgName) {
const resolved = await this.resolver.realPath(await this.resolver.resolveExport(parentPkgUrl, subpath, cjsEnv, parentIsCjs, specifier, parentUrl));
this.log("tracemap/resolve", `${specifier} ${parentUrl} -> ${resolved} (package own-name resolution)`);
return resolved;
}
// Imports
if (pcfg.imports && pkgName[0] === "#") {
const match = getMapMatch(specifier, pcfg.imports);
if (!match) throw new JspmError(`No '${specifier}' import defined in ${parentPkgUrl}${importedFrom(parentUrl)}.`);
const target = this.resolver.resolvePackageTarget(pcfg.imports[match], parentPkgUrl, cjsEnv, specifier.slice(match.length), true);
if (!isURL(target)) {
return this.resolve(target, parentUrl, installOpts, toplevel);
}
const resolved = await this.resolver.realPath(target);
this.log("tracemap/resolve", `${specifier} ${parentUrl} -> ${resolved} (package imports resolution)`);
return resolved;
}
// @ts-ignore
const installed = await this.installer.install(pkgName, installOpts, toplevel ? null : parentPkgUrl, subpath, parentUrl);
if (typeof installed === "string") {
return installed;
} else if (installed) {
const { installUrl, installSubpath } = installed;
const resolved = await this.resolver.realPath(await this.resolver.resolveExport(installUrl, combineSubpaths(installSubpath, parentIsCjs && subpath.endsWith("/") ? subpath.slice(0, -1) : subpath), cjsEnv, parentIsCjs, specifier, parentUrl));
this.log("tracemap/resolve", `${specifier} ${parentUrl} -> ${resolved} (installation resolution)`);
return resolved;
}
throw new JspmError(`No resolution in map for ${specifier}${importedFrom(parentUrl)}`);
}
constructor(opts, log, resolver){
_define_property(this, "installer", void 0);
_define_property(this, "opts", void 0);
_define_property(this, "inputMap", void 0 // custom imports
);
_define_property(this, "mapUrl", void 0);
_define_property(this, "baseUrl", void 0);
_define_property(this, "rootUrl", void 0);
_define_property(this, "pins", []);
_define_property(this, "log", void 0);
_define_property(this, "resolver", void 0);
/**
* Lock to ensure no races against input map processing.
* @type {Promise<void>}
*/ _define_property(this, "processInputMap", Promise.resolve());
this.log = log;
this.resolver = resolver;
this.mapUrl = opts.mapUrl;
this.baseUrl = opts.baseUrl;
this.rootUrl = opts.rootUrl || null;
this.opts = opts;
this.inputMap = new ImportMap({
mapUrl: this.mapUrl,
rootUrl: this.rootUrl
});
this.installer = new Installer(this.mapUrl.pathname.endsWith("/") ? this.mapUrl.href : `${this.mapUrl.href}/`, this.opts, this.log, this.resolver);
}
}
// The tracemap fully drives the installer
export { TraceMap as default };
//# sourceMappingURL=tracemap.js.map