@jspm/generator
Version:
Package Import Map Generation Tool
842 lines (840 loc) • 37.4 kB
JavaScript
/**
* Copyright 2020-2023 Guy Bedford
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/ /**
* The main entry point into the @jspm/generator package.
* @module generator.ts
*/ 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 { baseUrl as _baseUrl, relativeUrl, resolveUrl } from "./common/url.js";
import { parseTarget, validatePkgName } from "./install/package.js";
import TraceMap from "./trace/tracemap.js";
// @ts-ignore
import { clearCache as clearFetchCache, fetch as _fetch, setFetch } from "./common/fetch.js";
import { ImportMap } from "@jspm/import-map";
import process from "process";
import { SemverRange } from "sver";
import { JspmError } from "./common/err.js";
import { getIntegrity } from "./common/integrity.js";
import { createLogger } from "./common/log.js";
import { Replacer } from "./common/str.js";
import { analyzeHtml } from "./html/analyze.js";
import { configureProviders, getDefaultProviderStrings } from "./providers/index.js";
import * as nodemodules from "./providers/nodemodules.js";
import { Resolver } from "./trace/resolver.js";
import { getMaybeWrapperUrl } from "./common/wrapper.js";
import { setRetryCount } from "./common/fetch-common.js";
export { // utility export
analyzeHtml, // hook export
setFetch };
/**
* Supports clearing the global fetch cache in Node.js.
*
* Example:
*
* ```js
* import { clearCache } from '@jspm/generator';
* clearCache();
* ```
*/ export function clearCache() {
clearFetchCache();
}
/**
* Generator.
*/ export class Generator {
/**
* Add new custom mappings and lock resolutions to the input map
* of the generator, which are then applied in subsequent installs.
*
* @param jsonOrHtml The mappings are parsed as a JSON data object or string, falling back to reading an inline import map from an HTML file.
* @param mapUrl An optional URL for the map to handle relative resolutions, defaults to generator mapUrl.
* @param rootUrl An optional root URL for the map to handle root resolutions, defaults to generator rootUrl.
* @returns The list of modules pinned by this import map or HTML.
*/ async addMappings(jsonOrHtml, mapUrl = this.mapUrl, rootUrl = this.rootUrl, preloads) {
if (typeof mapUrl === "string") mapUrl = new URL(mapUrl, this.baseUrl);
if (typeof rootUrl === "string") rootUrl = new URL(rootUrl, this.baseUrl);
let htmlModules;
if (typeof jsonOrHtml === "string") {
try {
jsonOrHtml = JSON.parse(jsonOrHtml);
} catch {
const analysis = analyzeHtml(jsonOrHtml, mapUrl);
jsonOrHtml = analysis.map.json || {};
preloads = (preloads || []).concat(analysis.preloads.map((preload)=>{
var _preload_attrs_href;
return (_preload_attrs_href = preload.attrs.href) === null || _preload_attrs_href === void 0 ? void 0 : _preload_attrs_href.value;
}).filter((x)=>x));
htmlModules = [
...new Set([
...analysis.staticImports,
...analysis.dynamicImports
])
];
}
}
await this.traceMap.addInputMap(jsonOrHtml, mapUrl, rootUrl, preloads);
return htmlModules || [
...this.traceMap.pins
];
}
/**
* Retrieve the lockfile data from the installer
*/ getLock() {
return JSON.parse(JSON.stringify(this.traceMap.installer.installs));
}
/**
* Link a module, installing all dependencies necessary into the map
* to support its execution including static and dynamic module imports.
*
* @param specifier Module or list of modules to link
* @param parentUrl Optional parent URL
*/ async link(specifier, parentUrl) {
if (typeof specifier === "string") specifier = [
specifier
];
let error = false;
await this.traceMap.processInputMap;
specifier = specifier.map((specifier)=>specifier.replace(/\\/g, "/"));
try {
await Promise.all(specifier.map((specifier)=>this.traceMap.visit(specifier, {
installMode: "freeze",
toplevel: true
}, parentUrl || this.baseUrl.href)));
for (const s of specifier){
if (!this.traceMap.pins.includes(s)) this.traceMap.pins.push(s);
}
} catch (e) {
error = true;
throw e;
} finally{
const { map, staticDeps, dynamicDeps } = await this.traceMap.extractMap(this.traceMap.pins, this.integrity);
this.map = map;
if (!error) return {
staticDeps,
dynamicDeps
};
}
}
/**
* Links every imported module in the given HTML file, installing all
* dependencies necessary to support its execution.
*
* @param html HTML to link
* @param htmlUrl URL of the given HTML
*/ async linkHtml(html, htmlUrl) {
if (Array.isArray(html)) {
const impts = await Promise.all(html.map((h)=>this.linkHtml(h, htmlUrl)));
return [
...new Set(impts)
].reduce((a, b)=>a.concat(b), []);
}
let resolvedUrl;
if (htmlUrl) {
if (typeof htmlUrl === "string") {
resolvedUrl = new URL(resolveUrl(htmlUrl, this.mapUrl, this.rootUrl));
} else {
resolvedUrl = htmlUrl;
}
}
const analysis = analyzeHtml(html, resolvedUrl);
const impts = [
...new Set([
...analysis.staticImports,
...analysis.dynamicImports
])
];
await Promise.all(impts.map((impt)=>this.link(impt, resolvedUrl === null || resolvedUrl === void 0 ? void 0 : resolvedUrl.href)));
return impts;
}
/**
* Inject the import map into the provided HTML source
*
* @param html HTML source to inject into
* @param opts Injection options
* @returns HTML source with import map injection
*/ async htmlInject(html, { trace = false, pins = !trace, htmlUrl = this.mapUrl, rootUrl = this.rootUrl, preload = false, integrity = false, whitespace = true, esModuleShims = true, comment = true } = {}) {
if (comment === true) comment = " Generated by @jspm/generator - https://github.com/jspm/generator ";
if (typeof htmlUrl === "string") htmlUrl = new URL(htmlUrl);
const analysis = analyzeHtml(html, htmlUrl);
let modules = pins === true ? this.traceMap.pins : Array.isArray(pins) ? pins : [];
if (trace) {
const impts = await this.linkHtml(html, htmlUrl);
modules = [
...new Set([
...modules,
...impts
])
];
}
try {
var { map, staticDeps, dynamicDeps } = await this.extractMap(modules, htmlUrl, rootUrl, integrity);
} catch (err) {
// Most likely cause of a generation failure:
err.message += "\n\nIf you are linking locally against your node_modules folder, make sure that you have all the necessary dependencies installed.";
}
const preloadDeps = preload === "all" ? [
...new Set([
...staticDeps,
...dynamicDeps
])
] : staticDeps;
const newlineTab = !whitespace ? analysis.newlineTab : analysis.newlineTab.includes("\n") ? analysis.newlineTab : "\n" + analysis.newlineTab;
const replacer = new Replacer(html);
let esms = "";
if (esModuleShims) {
let esmsPkg;
try {
esmsPkg = await this.traceMap.resolver.resolveLatestTarget({
name: "es-module-shims",
registry: "npm",
ranges: [
new SemverRange("*")
],
unstable: false
}, this.traceMap.installer.defaultProvider, this.baseUrl.href);
} catch (err) {
// This usually happens because the user is trying to use their
// node_modules as the provider but has not installed the shim:
let errMsg = `Unable to resolve "es-module-shims@*" under current provider "${this.traceMap.installer.defaultProvider.provider}".`;
if (this.traceMap.installer.defaultProvider.provider === "nodemodules") {
errMsg += `\n\nJspm automatically injects a shim so that the import map in your HTML file will be usable by older browsers.\nYou may need to run "npm install es-module-shims" to install the shim if you want to link against your local node_modules folder.`;
}
errMsg += `\nTo disable the import maps polyfill injection, set esModuleShims: false.`;
throw new JspmError(errMsg);
}
let esmsUrl = await this.traceMap.resolver.pkgToUrl(esmsPkg, this.traceMap.installer.defaultProvider) + "dist/es-module-shims.js";
// detect esmsUrl as a wrapper URL
esmsUrl = await getMaybeWrapperUrl(esmsUrl, this.traceMap.resolver.fetchOpts);
if (htmlUrl || rootUrl) esmsUrl = relativeUrl(new URL(esmsUrl), new URL(rootUrl !== null && rootUrl !== void 0 ? rootUrl : htmlUrl), !!rootUrl);
esms = `<script async src="${esmsUrl}" crossorigin="anonymous"${integrity ? ` integrity="${await getIntegrity(new Uint8Array(await (await fetch(esmsUrl, this.traceMap.resolver.fetchOpts)).arrayBuffer()))}"` : ""}></script>${newlineTab}`;
if (analysis.esModuleShims) replacer.remove(analysis.esModuleShims.start, analysis.esModuleShims.end, true);
}
for (const preload of analysis.preloads){
replacer.remove(preload.start, preload.end, true);
}
let preloads = "";
if (preload && preloadDeps.length) {
let first = true;
for (let dep of preloadDeps.sort()){
if (first || whitespace) preloads += newlineTab;
if (first) first = false;
const url = rootUrl || htmlUrl ? relativeUrl(new URL(dep), new URL(rootUrl || htmlUrl), !!rootUrl) : dep;
preloads += `<link rel="modulepreload" href="${url}"${integrity ? ` integrity="${await getIntegrity(new Uint8Array(await (await fetch(dep, this.traceMap.resolver.fetchOpts)).arrayBuffer()))}"` : ""} />`;
}
}
if (comment) {
const existingComment = analysis.comments.find((c)=>replacer.source.slice(replacer.idx(c.start), replacer.idx(c.end)).includes(comment));
if (existingComment) {
replacer.remove(existingComment.start, existingComment.end, true);
}
}
replacer.replace(analysis.map.start, analysis.map.end, (comment ? "<!--" + comment + "-->" + newlineTab : "") + esms + '<script type="importmap">' + (whitespace ? newlineTab : "") + JSON.stringify(map, null, whitespace ? 2 : 0).replace(/\n/g, newlineTab) + (whitespace ? newlineTab : "") + "</script>" + preloads + (analysis.map.newScript ? newlineTab : ""));
return replacer.source;
}
/**
* Install a package target into the import map, including all its dependency resolutions via tracing.
*
* @param install Package or list of packages to install into the import map.
*
* @example
* ```js
* // Install a new package into the import map
* await generator.install('react-dom');
*
* // Install a package version and subpath into the import map (installs lit/decorators.js)
* await generator.install('lit@2/decorators.js');
*
* // Install a package version to a custom alias
* await generator.install({ alias: 'react16', target: 'react@16' });
*
* // Install a specific subpath of a package
* await generator.install({ target: 'lit@2', subpath: './html.js' });
*
* // Install an export from a locally located package folder into the map
* // The package.json is used to determine the exports and dependencies.
* await generator.install({ alias: 'mypkg', target: './packages/local-pkg', subpath: './feature' });
* ```
*/ async install(install) {
return this._install(install);
}
async _install(install, mode) {
// If there are no arguments, then we reinstall all the top-level locks:
if (install === null || install === undefined) {
await this.traceMap.processInputMap;
// To match the behaviour of an argumentless `npm install`, we use
// existing resolutions for everything unless it's out-of-range:
mode !== null && mode !== void 0 ? mode : mode = "default";
return this._install(Object.entries(this.traceMap.installer.installs.primary).map(([alias, target])=>{
const pkgTarget = this.traceMap.installer.constraints.primary[alias];
// Try to reinstall lock against constraints if possible, otherwise
// reinstall it as a URL directly (which has the downside that it
// won't have NPM versioning semantics):
let newTarget = target.installUrl;
if (pkgTarget) {
if (pkgTarget instanceof URL) {
newTarget = pkgTarget.href;
} else {
newTarget = `${pkgTarget.registry}:${pkgTarget.name}`;
}
}
var _target_installSubpath;
return {
alias,
target: newTarget,
subpath: (_target_installSubpath = target.installSubpath) !== null && _target_installSubpath !== void 0 ? _target_installSubpath : undefined
};
}), mode);
}
// Split the case of multiple install subpaths into multiple installs
// TODO: flatten all subpath installs here
if (!Array.isArray(install) && typeof install !== "string" && install.subpaths !== undefined) {
install.subpaths.every((subpath)=>{
if (typeof subpath !== "string" || subpath !== "." && !subpath.startsWith("./")) throw new Error(`Install subpath "${subpath}" must be equal to "." or start with "./".`);
});
return this._install(install.subpaths.map((subpath)=>({
target: install.target,
alias: install.alias,
subpath
})));
}
if (!Array.isArray(install)) install = [
install
];
// Handle case of multiple install targets with at most one subpath:
await this.traceMap.processInputMap; // don't race input processing
const imports = await Promise.all(install.map(async (install)=>{
// Resolve input information to a target package:
let alias, target, subpath;
if (typeof install === "string" || typeof install.target === "string") {
({ alias, target, subpath } = await installToTarget.call(this, install, this.traceMap.installer.defaultRegistry));
} else {
({ alias, target, subpath } = install);
validatePkgName(alias);
}
this.log("generator/install", `Adding primary constraint for ${alias}: ${JSON.stringify(target)}`);
// By default, an install takes the latest compatible version for primary
// dependencies, and existing in-range versions for secondaries:
mode !== null && mode !== void 0 ? mode : mode = "latest-primaries";
await this.traceMap.add(alias, target, mode);
return alias + (subpath ? subpath.slice(1) : "");
}));
await Promise.all(imports.map(async (impt)=>{
await this.traceMap.visit(impt, {
installMode: mode,
toplevel: true
}, this.mapUrl.href);
// Add the target import as a top-level pin
// we do this after the trace, so failed installs don't pollute the map
if (!this.traceMap.pins.includes(impt)) this.traceMap.pins.push(impt);
}));
const { map, staticDeps, dynamicDeps } = await this.traceMap.extractMap(this.traceMap.pins, this.integrity);
this.map = map;
return {
staticDeps,
dynamicDeps
};
}
/**
* Locking install, retraces all top-level pins but does not change the
* versions of anything (similar to "npm ci").
*/ async reinstall() {
await this.traceMap.processInputMap;
const { map, staticDeps, dynamicDeps } = await this.traceMap.extractMap(this.traceMap.pins, this.integrity);
this.map = map;
return {
staticDeps,
dynamicDeps
};
}
/**
* Updates the versions of the given packages to the latest versions
* compatible with their parent's package.json ranges. If no packages are
* given then all the top-level packages in the "imports" field of the
* initial import map are updated.
*
* @param {string | string[]} pkgNames Package name or list of package names to update.
*/ async update(pkgNames) {
if (typeof pkgNames === "string") pkgNames = [
pkgNames
];
await this.traceMap.processInputMap;
const primaryResolutions = this.traceMap.installer.installs.primary;
const primaryConstraints = this.traceMap.installer.constraints.primary;
// Matching the behaviour of "npm update":
let mode = "latest-primaries";
if (!pkgNames) {
pkgNames = Object.keys(primaryResolutions);
mode = "latest-all";
}
const installs = [];
for (const name of pkgNames){
const resolution = primaryResolutions[name];
if (!resolution) {
throw new JspmError(`No "imports" package entry for "${name}" to update. Note update takes package names not package specifiers.`);
}
const { installUrl, installSubpath } = resolution;
const subpaths = this.traceMap.pins.filter((pin)=>pin === name || pin.startsWith(name) && pin[name.length] === "/").map((pin)=>`.${pin.slice(name.length)}`);
// use package.json range if present
if (primaryConstraints[name]) {
installs.push({
alias: name,
subpaths,
target: {
pkgTarget: primaryConstraints[name],
installSubpath
}
});
} else {
const pkg = await this.traceMap.resolver.parseUrlPkg(installUrl);
if (!pkg) throw new Error(`Unable to determine a package version lookup for ${name}. Make sure it is supported as a provider package.`);
const target = {
pkgTarget: {
registry: pkg.pkg.registry,
name: pkg.pkg.name,
ranges: [
new SemverRange("^" + pkg.pkg.version)
],
unstable: false
},
installSubpath
};
installs.push({
alias: name,
subpaths,
target
});
}
}
await this._install(installs, mode);
const { map, staticDeps, dynamicDeps } = await this.traceMap.extractMap(this.traceMap.pins, this.integrity);
this.map = map;
return {
staticDeps,
dynamicDeps
};
}
async uninstall(names) {
if (typeof names === "string") names = [
names
];
await this.traceMap.processInputMap;
let pins = this.traceMap.pins;
const unusedNames = new Set([
...names
]);
for(let i = 0; i < pins.length; i++){
const pin = pins[i];
const pinNames = names.filter((name)=>name === pin || name.endsWith("/") && pin.startsWith(name));
if (pinNames.length) {
pins.splice(i--, 1);
for (const name of pinNames)unusedNames.delete(name);
}
}
if (unusedNames.size) {
throw new JspmError(`No "imports" entry for "${[
...unusedNames
][0]}" to uninstall.`);
}
this.traceMap.pins = pins;
const { staticDeps, dynamicDeps, map } = await this.traceMap.extractMap(this.traceMap.pins, this.integrity);
this.map = map;
return {
staticDeps,
dynamicDeps
};
}
async extractMap(pins, mapUrl, rootUrl, integrity) {
if (typeof mapUrl === "string") mapUrl = new URL(mapUrl, this.baseUrl);
if (typeof rootUrl === "string") rootUrl = new URL(rootUrl, this.baseUrl);
if (!Array.isArray(pins)) pins = [
pins
];
if (typeof integrity !== "boolean") integrity = this.integrity;
await this.traceMap.processInputMap;
const { map, staticDeps, dynamicDeps } = await this.traceMap.extractMap(pins, integrity);
map.rebase(mapUrl, rootUrl);
map.flatten();
map.sort();
map.combineSubpaths();
return {
map: map.toJSON(),
staticDeps,
dynamicDeps
};
}
/**
* Resolve a specifier using the import map.
*
* @param specifier Module to resolve
* @param parentUrl ParentURL of module to resolve
* @returns Resolved URL string
*/ resolve(specifier, parentUrl = this.baseUrl) {
if (typeof parentUrl === "string") parentUrl = new URL(parentUrl, this.baseUrl);
const resolved = this.map.resolve(specifier, parentUrl);
if (resolved === null) throw new JspmError(`Unable to resolve "${specifier}" from ${parentUrl.href}`, "MODULE_NOT_FOUND");
return resolved;
}
get importMap() {
return this.map;
}
getAnalysis(url) {
if (typeof url !== "string") url = url.href;
const trace = this.traceMap.resolver.getAnalysis(url);
if (!trace) throw new Error(`The URL ${url} has not been traced by this generator instance.`);
return {
format: trace.format,
staticDeps: trace.deps,
dynamicDeps: trace.dynamicDeps,
cjsLazyDeps: trace.cjsLazyDeps || []
};
}
getMap(mapUrl, rootUrl) {
const map = this.map.clone();
map.rebase(mapUrl, rootUrl);
map.flatten();
map.sort();
map.combineSubpaths();
return map.toJSON();
}
/**
* Constructs a new Generator instance.
*
* For example:
*
* ```js
* const generator = new Generator({
* mapUrl: import.meta.url,
* inputMap: {
* "imports": {
* "react": "https://cdn.skypack.dev/react"
* }
* },
* defaultProvider: 'jspm',
* defaultRegistry: 'npm',
* providers: {
* '@orgscope': 'nodemodules'
* },
* customProviders: {},
* env: ['production', 'browser'],
* cache: false,
* });
* ```
* @param {GeneratorOptions} opts Configuration for the new generator instance.
*/ constructor({ baseUrl, mapUrl, rootUrl = undefined, inputMap = undefined, env = [
"browser",
"development",
"module",
"import"
], defaultProvider, defaultRegistry = "npm", customProviders = undefined, providers, resolutions = {}, cache = true, fetchOptions = {}, ignore = [], commonJS = false, typeScript = false, system = false, integrity = false, fetchRetries, providerConfig = {}, preserveSymlinks } = {}){
var _process_env, _process_versions;
_define_property(this, "traceMap", void 0);
_define_property(this, "baseUrl", void 0);
_define_property(this, "mapUrl", void 0);
_define_property(this, "rootUrl", void 0);
_define_property(this, "map", void 0);
_define_property(this, "logStream", void 0);
_define_property(this, "log", void 0);
_define_property(this, "integrity", void 0);
// Initialise the debug logger:
const { log, logStream } = createLogger();
this.log = log;
this.logStream = logStream;
if (process === null || process === void 0 ? void 0 : (_process_env = process.env) === null || _process_env === void 0 ? void 0 : _process_env.JSPM_GENERATOR_LOG) {
(async ()=>{
for await (const { type, message } of this.logStream()){
console.log(`\x1b[1m${type}:\x1b[0m ${message}`);
}
})();
}
if (typeof preserveSymlinks !== "boolean") preserveSymlinks = typeof (process === null || process === void 0 ? void 0 : (_process_versions = process.versions) === null || _process_versions === void 0 ? void 0 : _process_versions.node) === "string";
// Initialise the resource fetcher:
let fetchOpts = {
retry: 1,
timeout: 10000,
...fetchOptions,
headers: {
"Accept-Encoding": "gzip, br"
}
};
if (cache === "offline") fetchOpts.cache = "force-cache";
else if (!cache) fetchOpts.cache = "no-store";
// Default logic for the mapUrl, baseUrl and rootUrl:
if (mapUrl && !baseUrl) {
mapUrl = typeof mapUrl === "string" ? new URL(mapUrl, _baseUrl) : mapUrl;
try {
baseUrl = new URL("./", mapUrl);
} catch {
baseUrl = new URL(mapUrl + "/");
}
} else if (baseUrl && !mapUrl) {
mapUrl = baseUrl;
} else if (!mapUrl && !baseUrl) {
baseUrl = mapUrl = _baseUrl;
}
this.baseUrl = typeof baseUrl === "string" ? new URL(baseUrl, _baseUrl) : baseUrl;
if (!this.baseUrl.pathname.endsWith("/")) {
this.baseUrl = new URL(this.baseUrl.href);
this.baseUrl.pathname += "/";
}
this.mapUrl = typeof mapUrl === "string" ? new URL(mapUrl, this.baseUrl) : mapUrl;
this.rootUrl = typeof rootUrl === "string" ? new URL(rootUrl, this.baseUrl) : rootUrl || null;
if (this.rootUrl && !this.rootUrl.pathname.endsWith("/")) this.rootUrl.pathname += "/";
if (!this.mapUrl.pathname.endsWith("/")) {
try {
this.mapUrl = new URL("./", this.mapUrl);
} catch {
this.mapUrl = new URL(this.mapUrl.href + "/");
}
}
this.integrity = integrity;
// Initialise the resolver:
const resolver = new Resolver({
env,
log,
fetchOpts,
preserveSymlinks,
traceCjs: commonJS,
traceTs: typeScript,
traceSystem: system
});
if (customProviders) {
for (const provider of Object.keys(customProviders)){
resolver.addCustomProvider(provider, customProviders[provider]);
}
}
// The node_modules provider is special, because it needs to be rooted to
// perform resolutions against the local node_modules directory:
const nmProvider = nodemodules.createProvider(this.baseUrl.href, defaultProvider === "nodemodules");
resolver.addCustomProvider("nodemodules", nmProvider);
// We make an attempt to auto-detect the default provider from the input
// map, by picking the provider with the most owned URLs:
defaultProvider = detectDefaultProvider(defaultProvider, inputMap, resolver);
// Initialise the tracer:
this.traceMap = new TraceMap({
mapUrl: this.mapUrl,
rootUrl: this.rootUrl,
baseUrl: this.baseUrl,
defaultProvider,
defaultRegistry,
providers,
ignore,
resolutions,
commonJS
}, log, resolver);
// Reconstruct constraints and locks from the input map:
this.map = new ImportMap({
mapUrl: this.mapUrl,
rootUrl: this.rootUrl
});
if (!integrity) this.map.integrity = {};
if (inputMap) this.addMappings(inputMap);
// Set the fetch retry count
if (typeof fetchRetries === "number") setRetryCount(fetchRetries);
configureProviders(providerConfig, resolver.providers);
}
}
/**
* _Use the internal fetch implementation, useful for hooking into the same shared local fetch cache._
*
* ```js
* import { fetch } from '@jspm/generator';
*
* const res = await fetch(url);
* console.log(await res.text());
* ```
*
* Use the `{ cache: 'no-store' }` option to disable the cache, and the `{ cache: 'force-cache' }` option to enforce the offline cache.
*/ export async function fetch(url, opts = {}) {
// @ts-ignore
return _fetch(url, opts);
}
/**
* Get the lookup resolution information for a specific install.
*
* @param install The install object
* @param lookupOptions Provider and cache defaults for lookup
* @returns The resolved install and exact package \{ install, resolved \}
*/ export async function lookup(install, { provider, cache } = {}) {
const generator = new Generator({
cache: !cache,
defaultProvider: provider
});
const { target, subpath, alias } = await installToTarget.call(generator, install, generator.traceMap.installer.defaultRegistry);
if (typeof target === "string") throw new Error(`Resolved install "${install}" to package specifier ${target}, but expected a fully qualified install target.`);
const { pkgTarget, installSubpath } = target;
if (pkgTarget instanceof URL) throw new Error("URL lookups not supported");
const resolved = await generator.traceMap.resolver.resolveLatestTarget(pkgTarget, generator.traceMap.installer.getProvider(pkgTarget), generator.baseUrl.href);
return {
install: {
target: {
registry: pkgTarget.registry,
name: pkgTarget.name,
range: pkgTarget.ranges.map((range)=>range.toString()).join(" || ")
},
installSubpath,
subpath,
alias
},
resolved: resolved
};
}
/**
* Get the package.json configuration for a specific URL or package.
*
* @param pkg Package to lookup configuration for
* @param lookupOptions Optional provider and cache defaults for lookup
* @returns Package JSON configuration
*
* Example:
* ```js
* import { getPackageConfig } from '@jspm/generator';
*
* // Supports a resolved package
* {
* const packageJson = await getPackageConfig({ registry: 'npm', name: 'lit-element', version: '2.5.1' });
* }
*
* // Or alternatively provide any URL
* {
* const packageJson = await getPackageConfig('https://ga.jspm.io/npm:lit-element@2.5.1/lit-element.js');
* }
* ```
*/ export async function getPackageConfig(pkg, { provider, cache } = {}) {
const generator = new Generator({
cache: !cache,
defaultProvider: provider
});
if (typeof pkg === "object" && "name" in pkg) pkg = await generator.traceMap.resolver.pkgToUrl(pkg, generator.traceMap.installer.defaultProvider);
else if (typeof pkg === "string") pkg = new URL(pkg).href;
else pkg = pkg.href;
return generator.traceMap.resolver.getPackageConfig(pkg);
}
/**
* Get the package base URL for the given module URL.
*
* @param url module URL
* @param lookupOptions Optional provider and cache defaults for lookup
* @returns Base package URL
*
* Modules can be remote CDN URLs or local file:/// URLs.
*
* All modules in JSPM are resolved as within a package boundary, which is the
* parent path of the package containing a package.json file.
*
* For JSPM CDN this will always be the base of the package as defined by the
* JSPM CDN provider. For non-provider-defined origins it is always determined
* by trying to fetch the package.json in each parent path until the root is reached
* or one is found. On file:/// URLs this exactly matches the Node.js resolution
* algorithm boundary lookup.
*
* This package.json file controls the package name, imports resolution, dependency
* resolutions and other package information.
*
* getPackageBase will return the folder containing the package.json,
* with a trailing '/'.
*
* This URL will either be the root URL of the origin, or it will be a
* path "pkgBase" such that fetch(`${pkgBase}package.json`) is an existing
* package.json file.
*
* @example
* ```js
* import { getPackageBase } from '@jspm/generator';
* const pkgUrl = await getPackageBase('https://ga.jspm.io/npm:lit-element@2.5.1/lit-element.js');
* // Returns: https://ga.jspm.io/npm:lit-element@2.5.1/
* ```
*/ export async function getPackageBase(url, { provider, cache } = {}) {
const generator = new Generator({
cache: !cache,
defaultProvider: provider
});
return generator.traceMap.resolver.getPackageBase(typeof url === "string" ? url : url.href);
}
/**
* Get the package metadata for the given module or package URL.
*
* @param url URL of a module or package for a configured provider.
* @param lookupOptions Optional provider and cache defaults for lookup.
* @returns Package metadata for the given URL if one of the configured
* providers owns it, else null.
*
* The returned metadata will always contain the package name, version and
* registry, along with the provider name and layer that handles resolution
* for the given URL.
*/ export async function parseUrlPkg(url, { provider, cache } = {}) {
const generator = new Generator({
cache: !cache,
defaultProvider: provider
});
return generator.traceMap.resolver.parseUrlPkg(typeof url === "string" ? url : url.href);
}
/**
* Returns a list of providers that are supported by default.
*
* @returns List of valid provider strings supported by default.
*
* To use one of these providers, pass the string to either the "defaultProvider"
* option or the "providers" mapping when constructing a Generator.
*/ export function getDefaultProviders() {
return getDefaultProviderStrings();
}
async function installToTarget(install, defaultRegistry) {
if (typeof install === "string") install = {
target: install
};
if (typeof install.target !== "string") throw new Error('All installs require a "target" string.');
if (install.subpath !== undefined && (typeof install.subpath !== "string" || install.subpath !== "." && !install.subpath.startsWith("./"))) throw new Error(`Install subpath "${install.subpath}" must be a string equal to "." or starting with "./".${typeof install.subpath === "string" ? `\nTry setting the subpath to "./${install.subpath}"` : ""}`);
const { alias, target, subpath } = await parseTarget(this.traceMap.resolver, install.target, this.baseUrl, defaultRegistry);
return {
target,
alias: install.alias || alias,
subpath: install.subpath || subpath
};
}
function detectDefaultProvider(defaultProvider, inputMap, resolver) {
// We only use top-level install information to detect the provider:
const counts = {};
for (const url of Object.values((inputMap === null || inputMap === void 0 ? void 0 : inputMap.imports) || {})){
const name = resolver.providerNameForUrl(url);
if (name) {
counts[name] = (counts[name] || 0) + 1;
}
}
let winner;
let winnerCount = 0;
for (const [name, count] of Object.entries(counts)){
if (count > winnerCount) {
winner = name;
winnerCount = count;
}
}
// TODO: this should be the behaviour once we support full 'providers' opt
// The leading provider in the input map takes precedence as the provider of
// the root package. Failing that, the user-provided default is used. The
// 'providers' field can be used for hard-overriding this:
// return winner || defaultProvider || "jspm.io";
return defaultProvider || winner || "jspm.io";
}
//# sourceMappingURL=generator.js.map