UNPKG

@jspm/generator

Version:

Package Import Map Generation Tool

604 lines (603 loc) 25.2 kB
/** * 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. */ import { ExactModule, ExactPackage, PackageConfig } from "./install/package.js"; import TraceMap from "./trace/tracemap.js"; import { setFetch } from "./common/fetch.js"; import { IImportMap, ImportMap } from "@jspm/import-map"; import { Log, LogStream } from "./common/log.js"; import { analyzeHtml } from "./html/analyze.js"; import { InstallTarget } from "./install/installer.js"; import { LockResolutions } from "./install/lock.js"; import { type Provider } from "./providers/index.js"; export { analyzeHtml, setFetch, }; export { Provider }; /** * @interface GeneratorOptions. */ export interface GeneratorOptions { /** * The URL to use for resolutions without a parent context. * * Defaults to mapUrl or the process base URL. * * Also determines the default scoping base for the import map when flattening. */ baseUrl?: URL | string; /** * The URL of the import map itself, used to construct relative import map URLs. * * Defaults to the base URL. * * The `mapUrl` is used in order to output relative URLs for modules located on the same * host as the import map. * * E.g. for `mapUrl: 'file:///path/to/project/map.importmap'`, installing local file packages * will be output as relative URLs to their file locations from the map location, since all URLs in an import * map are relative to the URL of the import map. */ mapUrl?: URL | string; /** * The URL to treat as the root of the serving protocol of the * import map, used to construct absolute import map URLs. * * When set, `rootUrl` takes precendence over `mapUrl` and is used to normalize all import map URLs * as absolute paths against this URL. * * E.g. for `rootUrl: 'file:///path/to/project/public'`, any local module `public/local/mod.js` within the `public` folder * will be normalized to `/local/mod.js` in the output map. */ rootUrl?: URL | string | null; /** * An authoritative initial import map. * * An initial import map to start with - can be from a previous * install or to provide custom mappings. */ inputMap?: IImportMap; /** * The provider to use for top-level (i.e. root package) installs if there's no context in the inputMap. This can be used to set the provider for a new import map. To use a specific provider for an install, rather than relying on context, register an override using the 'providers' option. * * Supports: 'jspm.io' | 'jspm.io#system' | 'nodemodules' | 'skypack' | 'jsdelivr' | 'unpkg' | 'esm.sh'; * * Providers are responsible for resolution from abstract package names and version ranges to exact URL locations. * * Providers resolve package names and semver ranges to exact CDN package URL paths using provider hooks. * * These hooks include version resolution and converting package versions into URLs and back again. * * See `src/providers/[name].ts` for how to define a custom provider. * * New providers can be provided via the `customProviders` option. PRs to merge in providers are welcome as well. */ defaultProvider?: string; /** * The default registry to use when no registry is provided to an install. * Defaults to 'npm:'. * * Registries are separated from providers because multiple providers can serve * any public registry. * * Internally, the default providers for registries are handled by the providers object */ defaultRegistry?: string; /** * The conditional environment resolutions to apply. * * The conditions passed to the `env` option are environment conditions, as [supported by Node.js](https://nodejs.org/dist/latest-v16.x/docs/api/packages.html#packages_conditions_definitions) in the package exports field. * * By default the `"default"`, `"require"` and `"import"` conditions are always supported regardless of what `env` conditions are provided. * * In addition the default conditions applied if no `env` option is set are `"browser"`, `"development"` and `"module"`. * * Webpack and RollupJS support a custom `"module"` condition as a bundler-specific solution to the [dual package hazard](https://nodejs.org/dist/latest-v16.x/docs/api/packages.html#packages_dual_package_hazard), which is by default included in the JSPM resolution as well although * can be turned off if needed. * * Note when providing custom conditions like setting `env: ["production"]` that the `"browser"` and `"module"` conditions still need to be * applied as well via `env: ["production", "browser", "module"]`. Ordering does not matter though. * * Any other custom condition strings can also be provided. */ env?: string[]; /** * Whether to use a local FS cache for fetched modules. Set to 'offline' to use the offline cache. * * By default a global fetch cache is maintained between runs on the file system. * * This caching can be disabled by setting `cache: false`. * * When running offline, setting `cache: 'offline'` will only use the local cache and not touch the network at all, * making fully offline workflows possible provided the modules have been seen before. */ cache?: "offline" | boolean; /** * User-provided fetch options for fetching modules, eg check https://github.com/npm/make-fetch-happen#extra-options for Node.js fetch */ fetchOptions?: Record<string, any>; /** * Custom provider definitions. * * When installing from a custom CDN it can be advisable to define a custom provider in order to be able to get version deduping against that CDN. * * Custom provider definitions define a provider name, and the provider instance consisting of three main hooks: * * * `pkgToUrl({ registry: string, name: string, version: string }, layer: string) -> String URL`: Returns the URL for a given exact package registry, name and version to use for this provider. If the provider is using layers, the `layer` string can be used to determine the URL layer (where the `defaultProvider: '[name].[layer]'` form is used to determine the layer, eg minified v unminified etc). It is important that package URLs always end in `/`, because packages must be treated as folders not files. An error will be thrown for package URLs returned not ending in `/`. * * `parsePkgUrl(url: string) -> { { registry: string, name: string, version: string }, layer: string } | undefined`: Defines the converse operation to `pkgToUrl`, converting back from a string URL * into the exact package registry, name and version, as well as the layer. Should always return `undefined` for unknown URLs as the first matching provider is treated as authoritative when dealing with * multi-provider installations. * * `resolveLatestTarget(target: { registry: string, name: string, range: SemverRange }, unstable: boolean, layer: string, parentUrl: string) -> Promise<{ registry: string, name: string, version: string } | null>`: Resolve the latest version to use for a given package target. `unstable` indicates that prerelease versions can be matched. The definition of `SemverRange` is as per the [sver package](https://www.npmjs.com/package/sver#semverrange). Returning `null` corresponds to a package not found error. * * The use of `pkgToUrl` and `parsePkgUrl` is what allows the JSPM Generator to dedupe package versions internally based on their unique internal identifier `[registry]:[name]@[version]` regardless of what CDN location is used. URLs that do not support `parsePkgUrl` can still be installed and used fine, they just do not participate in version deduping operations. * * @example * ```js * const unpkgUrl = 'https://unpkg.com/'; * const exactPkgRegEx = /^((?:@[^/\\%@]+\/)?[^./\\%@][^/\\%@]*)@([^\/]+)(\/.*)?$/; * * const generator = new Generator({ * defaultProvider: 'custom', * customProviders: { * custom: { * pkgToUrl ({ registry, name, version }) { * return `${unpkgUrl}${name}@${version}/`; * }, * parseUrlPkg (url) { * if (url.startsWith(unpkgUrl)) { * const [, name, version] = url.slice(unpkgUrl.length).match(exactPkgRegEx) || []; * return { registry: 'npm', name, version }; * } * }, * resolveLatestTarget ({ registry, name, range }, unstable, layer, parentUrl) { * return { registry, name, version: '3.6.0' }; * } * } * } * }); * * await generator.install('custom:jquery'); * ``` */ customProviders?: Record<string, Provider>; /** * A map of custom scoped providers. * * The provider map allows setting custom providers for specific package names, package scopes or registries. * For example, an organization with private packages with names like `npmpackage` and `@orgscope/...` can define the custom providers to reference these from a custom source: * * ```js * providers: { * 'npmpackage': 'nodemodules', * '@orgscope': 'nodemodules', * 'npm:': 'nodemodules' * } * ``` * * Alternatively a custom provider can be referenced this way for eg private CDN / registry support. */ providers?: Record<string, string>; /** * Custom dependency resolution overrides for all installs. * * The resolutions option allows configuring a specific dependency version to always be used overriding all version resolution * logic for that dependency for all nestings. * * It is a map from package name to package version target just like the package.json "dependencies" map, but that applies and overrides universally. * * @example * ```js * const generator = new Generator({ * resolutions: { * dep: '1.2.3' * } * }); * ``` * * It is also useful for local monorepo patterns where all local packages should be located locally. * When referencing local paths, the baseUrl configuration option is used as the URL parent. * * ```js * const generator = new Generator({ * mapUrl: new URL('./app.html', import.meta.url), * baseUrl: new URL('../', import.meta.url), * resolutions: { * '@company/pkgA': `./pkgA`, * '@company/pkgB': `./pkgB` * '@company/pkgC': `./pkgC` * } * }) * ``` * * All subpath and main resolution logic will follow the package.json definitions of the resolved package, unlike `inputMap` * which only maps specific specifiers. */ resolutions?: Record<string, string>; /** * Allows ignoring certain module specifiers during the tracing process. * It can be useful, for example, when you provide an `inputMap` * that contains a mapping that can't be traced in current context, * but you know it will work in the context where the generated map * is going to be used. * ```js * const generator = new Generator({ * inputMap: { * imports: { * "react": "./my/own/react.js", * } * }, * ignore: ["react"] * }); * * // Even though `@react-three/fiber@7` depends upon `react`, * // `generator` will not try to trace and resolve `react`, * // so the mapping provided in `inputMap` will end up in the resulting import map. * await generator.install("@react-three/fiber@7") * ``` */ ignore?: string[]; /** * Lockfile data to use for resolutions */ lock?: LockResolutions; /** * Support tracing CommonJS dependencies locally. This is necessary if you * are using the "nodemodules" provider and have CommonJS dependencies. * Disabled by default. */ commonJS?: boolean; /** * Support tracing TypeScript dependencies when generating the import map. * Disabled by default. */ typeScript?: boolean; /** * Support tracing SystemJS dependencies when generating the import map. * Disabled by default. */ system?: boolean; /** * Whether to include "integrity" field in the import map */ integrity?: boolean; /** * The number of fetch retries to attempt for request failures. * Defaults to 3. */ fetchRetries?: number; /** * The same as the Node.js `--preserve-symlinks` flag, except it will apply * to both the main and the dependencies. * See https://nodejs.org/api/cli.html#--preserve-symlinks. * This is only supported for file: URLs. * Defaults to false, like Node.js. */ preserveSymlinks?: boolean; /** * Provider configuration options * * @example * ```js * const generator = new Generator({ * mapUrl: import.meta.url, * defaultProvider: "jspm.io", * providerConfig: { * "jspm.io": { * cdnUrl: `https://jspm-mirror.com/` * } * } */ providerConfig?: { [providerName: string]: any; }; } export interface ModuleAnalysis { format: "commonjs" | "esm" | "system" | "json" | "css" | "typescript" | "wasm"; staticDeps: string[]; dynamicDeps: string[]; cjsLazyDeps: string[] | null; } export interface Install { target: string | InstallTarget; alias?: string; subpath?: "." | `./${string}`; subpaths?: ("." | `./${string}`)[]; } /** * Supports clearing the global fetch cache in Node.js. * * Example: * * ```js * import { clearCache } from '@jspm/generator'; * clearCache(); * ``` */ export declare function clearCache(): void; /** * Generator. */ export declare class Generator { traceMap: TraceMap; baseUrl: URL; mapUrl: URL; rootUrl: URL | null; map: ImportMap; logStream: LogStream; log: Log; integrity: boolean; /** * 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, inputMap, env, defaultProvider, defaultRegistry, customProviders, providers, resolutions, cache, fetchOptions, ignore, commonJS, typeScript, system, integrity, fetchRetries, providerConfig, preserveSymlinks, }?: GeneratorOptions); /** * 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. */ addMappings(jsonOrHtml: string | IImportMap, mapUrl?: string | URL, rootUrl?: string | URL, preloads?: string[]): Promise<string[]>; /** * Retrieve the lockfile data from the installer */ getLock(): LockResolutions; /** * 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(specifier: string | string[], parentUrl?: string): Promise<{ staticDeps: string[]; dynamicDeps: string[]; }>; /** * 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 */ linkHtml(html: string | string[], htmlUrl?: string | URL): Promise<string[]>; /** * 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 */ htmlInject(html: string, { trace, pins, htmlUrl, rootUrl, preload, integrity, whitespace, esModuleShims, comment, }?: { pins?: string[] | boolean; trace?: string[] | boolean; htmlUrl?: string | URL; rootUrl?: string | URL | null; preload?: boolean | "all" | "static"; integrity?: boolean; whitespace?: boolean; esModuleShims?: string | boolean; comment?: boolean | string; }): Promise<string>; /** * 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' }); * ``` */ install(install?: string | Install | (string | Install)[]): Promise<void | { staticDeps: string[]; dynamicDeps: string[]; }>; private _install; /** * Locking install, retraces all top-level pins but does not change the * versions of anything (similar to "npm ci"). */ reinstall(): Promise<{ staticDeps: string[]; dynamicDeps: string[]; }>; /** * 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. */ update(pkgNames?: string | string[]): Promise<{ staticDeps: string[]; dynamicDeps: string[]; }>; uninstall(names: string | string[]): Promise<{ staticDeps: string[]; dynamicDeps: string[]; }>; extractMap(pins: string | string[], mapUrl?: URL | string, rootUrl?: URL | string | null, integrity?: boolean): Promise<{ map: IImportMap; staticDeps: string[]; dynamicDeps: string[]; }>; /** * 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: string, parentUrl?: URL | string): string; get importMap(): ImportMap; getAnalysis(url: string | URL): ModuleAnalysis; getMap(mapUrl?: string | URL, rootUrl?: string | URL | null): IImportMap; } export interface LookupOptions { provider?: string; cache?: "offline" | boolean; } /** * _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 declare function fetch(url: string, opts?: any): Promise<Response | import("./common/fetch.js").WrappedResponse>; /** * 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 declare function lookup(install: string | Install, { provider, cache }?: LookupOptions): Promise<{ install: { target: { registry: string; name: string; range: string; }; installSubpath: "." | `./${string}`; subpath: "." | `./${string}`; alias: string; }; resolved: ExactPackage; }>; /** * 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 declare function getPackageConfig(pkg: string | URL | ExactPackage, { provider, cache }?: LookupOptions): Promise<PackageConfig | null>; /** * 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 declare function getPackageBase(url: string | URL, { provider, cache }?: LookupOptions): Promise<string>; /** * 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 declare function parseUrlPkg(url: string | URL, { provider, cache }?: LookupOptions): Promise<ExactModule | null>; /** * 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 declare function getDefaultProviders(): string[];