UNPKG

@jspm/generator

Version:

Package Import Map Generation Tool

1,115 lines 51.7 kB
/** * Copyright 2020-2025 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, isURL, 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, setRetryCount, setVirtualSourceData } from './common/fetch.js'; import { ImportMap } from '@jspm/import-map'; 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 { getDefaultProviderStrings, ProviderManager } from './providers/index.js'; import * as nodemodules from './providers/nodemodules.js'; import { Resolver } from './trace/resolver.js'; import { getMaybeWrapperUrl } from './common/wrapper.js'; import { expandExportsResolutions } from './common/package.js'; import { isNode } from './common/env.js'; import { minimatch } from 'minimatch'; /** * Supports clearing the global fetch cache in Node.js. * * @example * * ```js * import { clearCache } from '@jspm/generator'; * clearCache(); * ``` */ export async function clearCache() { return clearFetchCache(); } function createFetchOptions(cache = true, fetchOptions = {}) { 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'; return fetchOpts; } /** * 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 * * Link specifiers are module specifiers - they can be bare specifiers resolved through * package resolution, relative URLs, or full URLs, for example: * * @example * ```js * await generator.link(['react', './local.js']); * ``` * * In the above, an import map will be constructed based on the resolution of react, * and tracing all its dependencies in turn, as well as for the local module, and * any dependencies it has in turn as well, installing all dependencies into the import * map as needed. * * In general, using `generator.link(entryPoints)` is recommended over `generator.install()`, * since it represents a real module graph linkage as would be required in a browser. * * By using link, we guarantee that the import map constructed is only for what is truly * needed and loaded. Dynamic imports that are statically analyzable are traced by link. * * If a custom resolver is configured, it will be applied to the provided specifiers * and all their dependencies during the linking process. */ async link(specifier, parentUrl) { if (typeof specifier === 'string') specifier = [ specifier ]; let error = false; await this.traceMap.processInputMap; try { await Promise.all(specifier.map((specifier)=>this.traceMap.visit(specifier, { installMode: 'freeze', toplevel: !this.scopedLink }, 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.scopedLink); 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.pm.resolveLatestTarget({ name: 'es-module-shims', registry: 'npm', ranges: [ new SemverRange('*') ], unstable: false }, this.traceMap.installer.defaultProvider, this.baseUrl.href, this.traceMap.resolver); } 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.pm.pkgToUrl(esmsPkg, this.traceMap.installer.defaultProvider.provider, this.traceMap.installer.defaultProvider.layer) + '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; } async install(install, mode) { if (install === 'default' || install === 'latest-primaries' || install === 'latest-all' || install === 'freeze') { mode = install; install = []; } return this._install(install, mode); } 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); } if (!Array.isArray(install)) install = [ install ]; 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, subpaths; if (typeof install === 'string' || typeof install.target === 'string') { ({ alias, target, subpath } = await installToTarget.call(this, install, this.traceMap.installer.defaultRegistry)); if (install === null || install === void 0 ? void 0 : install.subpaths) subpaths = install.subpaths; } else { ({ alias, target, subpath, subpaths } = 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'; const installed = await this.traceMap.add(alias, target, mode); // expand all package subpaths if (subpaths === true) { const pcfg = await this.traceMap.resolver.getPackageConfig(installed.installUrl); // no entry point case if (!pcfg.exports && !pcfg.main) { return []; } // main only if (!pcfg.exports || !Object.keys(pcfg.exports).every((expt)=>expt[0] === '.')) { return alias; } // If the provider supports it, get a file listing for the package to assist with glob expansions const fileList = await this.traceMap.resolver.pm.getFileList(installed.installUrl); // Expand exports into entry point list const resolutionMap = new Map(); await expandExportsResolutions(pcfg.exports, this.traceMap.resolver.env, fileList, resolutionMap); return [ ...resolutionMap ].map(([subpath, _entry])=>alias + subpath.slice(1)); } else if (subpaths) { subpaths.every((subpath)=>{ if (typeof subpath !== 'string' || subpath !== '.' && !subpath.startsWith('./')) throw new Error(`Install subpath "${subpath}" must be equal to "." or start with "./".`); }); return subpaths.map((subpath)=>alias + subpath.slice(1)); } else { return alias + (subpath ? subpath.slice(1) : ''); } }))).flatMap((i)=>i); 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"). * @deprecated use generator.install('freeze') instead. */ async reinstall() { return await this.install('freeze'); } /** * 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.pm.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 }; } /** * Populate virtual source files into the generator for further linking or install operations, effectively * intercepting network and file system requests to those URLs. * * @param baseUrl base URL under which all file data is located @example `"file:///path/to/package/"` or * `"https://site.com/pkg@1.2.3/)"`. * @param fileData Key value pairs of file data strings or buffers virtualized under the provided * URL base path, * @example * ``` * { * 'package.json': '', * 'dir/file.bin': new Uint8Array([1,2,3]) * } * ``` */ setVirtualSourceData(baseUrl, fileData) { setVirtualSourceData(baseUrl, fileData); } /** * Publish a package to a JSPM provider * * This function creates a tarball from the provided files and uploads it. * * @param options Publish options * @returns Promise that resolves with the package URL, map URL, and * an optional copy-paste code snippet demonstrating usage. * * @example * ```js * import { Generator } from '@jspm/generator'; * * const generator = new Generator({ * inputMap: { ...custom import map... } * }); * const result = await generator.publish({ * package: './pkg', * provider: 'jspm.io', * importMap: true, * link: true, * }); * * // URL to the published package and published import map * console.log(result.packageUrl, result.mapUrl); * // HTML code snippets demonstrating how to run the published code in a browser * console.log(result.codeSnippets); * ``` * JSPM will fully link all dependencies when link: true is provided, and * populate them into the import map of the generator instance provided * to the publish. * * Alternatively, instead of a local package path, package can also be provided * as a record of virtual sources. * */ async publish({ package: pkg, importMap = true, install = importMap === true, version, name, provider = 'jspm.io' }) { if (typeof pkg === 'object') { const virtualUrl = `https://virtual/${name !== null && name !== void 0 ? name : 'publish'}@${version !== null && version !== void 0 ? version : Math.round(Math.random() * 10000)}`; this.setVirtualSourceData(virtualUrl, pkg); pkg = virtualUrl; } if (typeof pkg !== 'string' || !isURL(pkg) || pkg.match(/^\w\:/)) { throw new JspmError(`Package must be a URL string, received "${pkg}"`); } if (!pkg.endsWith('/')) pkg += '/'; // Get the file list from the package and read all the file data const fileList = await this.traceMap.resolver.pm.getFileList(pkg); const fileData = {}; await Promise.all([ ...fileList ].map(async (file)=>{ const res = await fetch(pkg + file, this.traceMap.resolver.fetchOpts); if (!res.ok) { throw new JspmError(`Unable to read file ${file} in ${pkg} - got ${res.statusText || res.status}`); } fileData[file] = await res.arrayBuffer(); })); // Ensure package.json exists and has correct name and version const pkgJson = fileData['package.json'] || '{}'; // Parse package.json if it's a string let pjson; try { if (typeof pkgJson === 'string') { pjson = JSON.parse(pkgJson); } else { // Convert ArrayBuffer to string and parse const decoder = new TextDecoder(); pjson = JSON.parse(decoder.decode(pkgJson)); } } catch (err) { throw new JspmError('Invalid package.json: ' + err.message); } if (pjson.jspm) { const { jspm } = pjson; delete pjson.jspm; Object.assign(pjson, jspm); } const ignore = Array.isArray(pjson.ignore) ? pjson.ignore : []; const files = Array.isArray(pjson.files) ? pjson.files : []; const filteredFileList = []; for (const file of fileList){ for (const ignorePattern of ignore){ if (minimatch(file, ignorePattern)) { continue; } } const parts = file.split('/'); if (parts.includes('node_modules') || parts.some((part)=>part.startsWith('.')) || parts.includes('package-lock.json')) continue; if (files.length) { for (const includePattern of files){ if (minimatch(file, includePattern)) { filteredFileList.push(file); } } } else { filteredFileList.push(file); } } for (const file of Object.keys(fileData)){ if (!filteredFileList.includes(file)) delete fileData[file]; } if (filteredFileList.length === 0 && !importMap) throw new JspmError('At least one file or importMap is required for publishing'); if (!name) { name = pjson.name; if (!name) throw new JspmError(`Package name is required for publishing, either in the package.json or as a publish option.`); if (!name.match(/^[a-zA-Z0-9_\-]+$/)) throw new JspmError(`Invalid package name for publish.`); } if (!version) { version = pjson.version; if (!version) throw new JspmError(`Package version is required for publishing, either in the package.json or as a publish option.`); } const exactPkg = { name, version, registry: 'app' }; const packageUrl = await this.traceMap.resolver.pm.pkgToUrl({ name, version, registry: 'app' }, provider); if (install) { await this.install({ alias: name, target: pkg, subpaths: true }, 'freeze'); // we then substitute the package URL with the final publish URL this.importMap.rebase('about:blank'); this.importMap.replace(pkg, packageUrl); } const map = importMap === true ? this.map.clone() : importMap ? new ImportMap({ map: importMap }) : undefined; if (map) { if (this.flattenScopes) map.flatten(); map.sort(); if (this.combineSubpaths) map.combineSubpaths(); } // If importMap option is set to true, pass a clone of the generator's map return await this.traceMap.resolver.pm.publish(exactPkg, provider, this.traceMap.pins.sort((a, b)=>{ const aIsPublishAlias = a === name || a.startsWith(name) && a[name.length] === '/'; const bIsPublishAlias = b === name || b.startsWith(name) && b[name.length] === '/'; if (aIsPublishAlias && !bIsPublishAlias) return -1; else if (bIsPublishAlias && !aIsPublishAlias) return 1; return a > b ? 1 : -1; }), fileData, map); } /** * Authenticate with a provider to obtain an authentication token. * * @param options Authentication options including provider, username, and verify callback * @returns Promise resolving to the authentication token */ async auth(options = {}) { const providerName = options.provider || 'jspm.io'; return this.traceMap.resolver.pm.auth(providerName, { username: options.username, verify: options.verify }); } /** * Eject a published package by downloading it to the provided local folder, * and stitching its import map into the generator import map. */ async eject({ name, version, registry = 'app', provider = 'jspm.io' }, outDir) { if (!isNode) { throw new JspmError(`Eject functionality is currently only available on a filesystem`); } const pkg = { name, version, registry }; const packageUrl = await this.traceMap.resolver.pm.pkgToUrl({ name, version, registry: 'app' }, provider); const mapUrl = packageUrl + 'importmap.json'; let publishMap = null; try { const res = await fetch(mapUrl); if (res.status !== 404) { if (!res.ok && res.status !== 304) { throw res.statusText || res.status; } publishMap = await res.json(); } } catch (e) { throw new JspmError(`Unable to load import map ${mapUrl}: ${e}`); } const packageFiles = await this.traceMap.resolver.pm.download(pkg, provider); const [{ writeFileSync, mkdirSync }, { resolve, dirname }, { fileURLToPath, pathToFileURL }] = await Promise.all([ import(eval('"node:fs"')), import(eval('"node:path"')), import(eval('"node:url"')) ]); outDir = resolve(fileURLToPath(this.baseUrl), outDir); for (const [path, source] of Object.entries(packageFiles)){ const resolved = resolve(outDir, path); mkdirSync(dirname(resolved), { recursive: true }); writeFileSync(resolved, source); } if (publishMap) { await this.mergeMap(publishMap, 'about:blank'); } this.map.replace(packageUrl, pathToFileURL(outDir).href + '/'); this.map.rebase(this.mapUrl, this.rootUrl); } /** * Merges an import map into this instance's import map. * * Performs a full retrace of the map to be merged, building out its version constraints separately, * and expanding scopes previously flattened by the scope-flattening "flattenScopes" option that occurs * by default for extracted import maps. */ async mergeMap(map, mapUrl) { const mergeGenerator = this.clone(); mergeGenerator.flattenScopes = false; await mergeGenerator.addMappings(map, mapUrl); await mergeGenerator.install('freeze'); await this.addMappings(mergeGenerator.getMap(mergeGenerator.mapUrl, mergeGenerator.rootUrl)); await this.install('freeze'); } /** * Create a clone of this generator instance with the same configuration. * * Does not clone the internal import map or install state. */ clone() { const cloned = new Generator({ baseUrl: this.baseUrl, mapUrl: this.mapUrl, rootUrl: this.rootUrl, env: this.traceMap.resolver.env, defaultProvider: this.traceMap.installer.defaultProvider.provider + '#' + this.traceMap.installer.defaultProvider.layer, defaultRegistry: this.traceMap.installer.defaultRegistry, resolutions: this.traceMap.installer.resolutions, fetchOptions: this.traceMap.resolver.fetchOpts, commonJS: this.traceMap.resolver.traceCjs, typeScript: this.traceMap.resolver.traceTs, system: this.traceMap.resolver.traceSystem, integrity: this.integrity, preserveSymlinks: this.traceMap.resolver.preserveSymlinks, flattenScopes: this.flattenScopes, combineSubpaths: this.combineSubpaths }); cloned.traceMap.resolver.pm.providers = { ...this.traceMap.resolver.pm.providers }; return cloned; } /** * Extracts a smaller import map from a larger import map * * This is for the use case where one large import map is being used to manage * dependencies across multiple entry points in say a multi-page application, * and one pruned import map is desired just for a set of top-level imports which * is smaller than the full set of top-level imports * * These top-level imports can be provided as a list of "pins" to extract, and a * fully pruned map with only the necessary scoped mappings will be traced out * of the larger map while respecting its resolutions. */ 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); if (this.flattenScopes) map.flatten(); map.sort(); if (this.combineSubpaths) 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 || [] }; } /** * Obtain the final generated import map, with flattening and subpaths combined * (unless otherwise disabled via the Generator flattenScopes and combineSubpaths options). * * A mapUrl can be provided typically as a file URL corresponding to the location of the import map on the file * system. Relative paths to other files on the filesystem will then be tracked as map-relative and * output as relative paths, assuming the map retains its relative relation to local modules regardless * of the publish URLs. * * When a root URL is provided pointing to a local file URL, `/` prefixed URLs will be used for all * modules contained within this file URL base as root URL relative instead of map relative URLs like the above. */ getMap(mapUrl, rootUrl) { const map = this.map.clone(); map.rebase(mapUrl, rootUrl); if (this.flattenScopes) map.flatten(); map.sort(); if (this.combineSubpaths) map.combineSubpaths(); return map.toJSON(); } /** * Constructs a new Generator instance. * * @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 = {}, packageConfigs = {}, ignore = [], commonJS = false, typeScript = false, system = false, integrity = false, fetchRetries, providerConfig = {}, preserveSymlinks, customResolver, flattenScopes = true, combineSubpaths = true, scopedLink = false } = {}){ _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); _define_property(this, "flattenScopes", void 0); _define_property(this, "combineSubpaths", void 0); _define_property(this, "scopedLink", void 0); if (typeof preserveSymlinks !== 'boolean') preserveSymlinks = isNode; // 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.scopedLink = scopedLink; this.integrity = integrity; const fetchOpts = createFetchOptions(cache, fetchOptions); const { log, logStream } = createLogger(); this.logStream = logStream; this.log = log; // 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'); const pm = new ProviderManager(log, fetchOpts, providerConfig, { ...customProviders, 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, pm); // Initialise the resolver: const resolver = new Resolver({ env, providerManager: pm, fetchOpts, preserveSymlinks, traceCjs: commonJS, traceTs: typeScript, traceSystem: system, packageConfigs: Object.fromEntries(Object.entries(packageConfigs).map(([key, pcfg])=>{ let resolved = new URL(key, baseUrl).href; if (!resolved.endsWith('/')) resolved += '/'; if (resolved.endsWith('/package.json')) resolved = resolved.slice(0, -12); return [ resolved, pcfg ]; })) }); // Initialise the tracer: this.traceMap = new TraceMap({ mapUrl: this.mapUrl, rootUrl: this.rootUrl, baseUrl: this.baseUrl, defaultProvider, defaultRegistry, providers, ignore, resolutions, commonJS, customResolver }, 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); this.flattenScopes = flattenScopes; this.combineSubpaths = combineSubpaths; // Set the fetch retry count if (typeof fetchRetries === 'number') setRetryCount(fetchRetries); } } /** * _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.pm.resolveLatestTarget(pkgTarget, generator.traceMap.installer.getProvider(pkgTarget), generator.baseUrl.href, generator.traceMap.resolver); 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.pm.pkgToUrl(pkg, generator.traceMap.installer.defaultProvider.provider, generator.traceMap.installer.defaultProvider.layer); 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.pm.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.