UNPKG

iiif-processor

Version:

IIIF 2.1 & 3.0 Image API modules for NodeJS

906 lines (892 loc) 27.6 kB
var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/index.ts var index_exports = {}; __export(index_exports, { IIIFError: () => IIIFError, Processor: () => Processor, Versions: () => Versions }); module.exports = __toCommonJS(index_exports); // src/error.ts var IIIFError = class extends Error { constructor(message, opts = {}) { super(message); this.statusCode = opts.statusCode; } }; // src/processor.ts var import_debug3 = __toESM(require("debug")); var import_mime_types = __toESM(require("mime-types")); var import_path = __toESM(require("path")); var import_sharp2 = __toESM(require("sharp")); // src/transform.ts var import_sharp = __toESM(require("sharp")); var import_debug2 = __toESM(require("debug")); // src/v2/index.ts var v2_exports = {}; __export(v2_exports, { Base: () => Base, Calculator: () => Calculator, Formats: () => Formats, Qualities: () => Qualities, infoDoc: () => infoDoc, profileLink: () => profileLink }); // src/calculator/base.ts var import_debug = __toESM(require("debug")); var debug = (0, import_debug.default)("iiif-processor:calculator"); var IR = "\\d+"; var FR = "\\d+(?:\\.\\d+)?"; var PCTR = /^pct:(?<val>[\d.,]+)/; var Formats = ["gif", "jpg", "tif", "png", "webp"]; var Qualities = ["color", "gray", "bitonal", "default"]; var extraFormats = ["jpeg", "tiff"]; var Validators = { quality: Qualities, format: [...Formats, ...extraFormats], region: ["full", "square", `pct:${FR},${FR},${FR},${FR}`, `${IR},${IR},${IR},${IR}`], size: ["full", "max", `pct:${FR}`, `${IR},`, `,${IR}`, `\\!?${IR},${IR}`], rotation: [`\\!?${FR}`], density: [] }; function validateDensity(v) { debug("validating density %s", v); if (v === null) return true; if (v === void 0) return true; if (typeof v !== "number" || v < 0) { throw new IIIFError(`Invalid density value: ${v}`); } return true; } var Base = class _Base { static _matchers() { return Validators; } static _validator(type) { const result = this._matchers()[type].join("|"); return `(?<${type}>${result})`; } static parsePath(path2) { path2 = decodeURIComponent(path2); debug("parsing IIIF path: %s", path2); const idOnlyRe = new RegExp("^/?(?<id>.+)/?$"); const infoJsonRe = new RegExp("^/?(?<id>.+)/(?<info>info.json)$"); const transformRe = new RegExp( "^/?(?<id>.+)/(?<region>.+)/(?<size>.+)/(?<rotation>.+)/(?<quality>.+)\\.(?<format>.+)$" ); let result = infoJsonRe.exec(path2)?.groups; debug("info.json match result: %j", result); if (result) return result; result = transformRe.exec(path2)?.groups; debug("transform match result: %j", result); if (result) { for (const component of [ "region", "size", "rotation", "quality", "format" ]) { const validator = new RegExp(this._validator(component)); if (!validator.test(result[component])) { throw new IIIFError(`Invalid ${component} in IIIF path: ${path2}`, { statusCode: 400 }); } } return result; } result = idOnlyRe.exec(path2)?.groups; debug("ID only match result: %j", result); if (result) return result; throw new IIIFError(`Not a valid IIIF path: ${path2}`, { statusCode: 400 }); } constructor(dims, opts = {}) { this.dims = { ...dims }; this.opts = { ...opts }; this._sourceDims = { ...dims }; this._canonicalInfo = { region: "full", size: "full", rotation: "0", quality: "default", format: "jpg" }; this._parsedInfo = { region: { left: 0, top: 0, ...dims }, size: { width: dims.width, height: dims.height, fit: "fill" }, rotation: { flop: false, degree: 0 }, quality: "default", format: { type: "jpg" }, upscale: true }; } _validate(type, v) { if (type === "density") return validateDensity(v); const re = new RegExp(`^${_Base._validator(type)}$`); debug("validating %s %s against %s", type, v, re); if (!re.test(String(v))) { throw new IIIFError(`Invalid ${type}: ${v}`, { statusCode: 400 }); } return true; } region(v) { this._validate("region", v); const pct = PCTR.exec(v); let isFull = false; if (v === "full") { this._parsedInfo.region = { left: 0, top: 0, ...this.dims }; isFull = true; } else if (v === "square") { this._parsedInfo.region = regionSquare(this.dims); } else if (pct) { this._parsedInfo.region = regionPct(pct.groups?.val, this.dims); } else { this._parsedInfo.region = regionXYWH(v); } this._canonicalInfo.region = isFull ? "full" : this._parsedInfo.region; this._constrainRegion(); return this; } size(v) { this._validate("size", v); const pct = PCTR.exec(v); let isMax = false; if (["full", "max"].includes(v)) { this._setSize(this._parsedInfo.region); isMax = true; } else if (pct) { this._setSize( sizePct(pct.groups?.val, this._parsedInfo.region) ); } else { this._setSize(sizeWH(v)); } this._canonicalInfo.size = isMax ? v : this._canonicalSize(); return this; } rotation(v) { this._validate("rotation", v); this._canonicalInfo.rotation = v; this._parsedInfo.rotation = { flop: v[0] === "!", degree: Number(v.replace(/^!/, "")) }; return this; } quality(v) { this._validate("quality", v); this._canonicalInfo.quality = v; this._parsedInfo.quality = v; return this; } format(v, density) { this._validate("format", v); this._validate("density", density); this._canonicalInfo.format = v; this._parsedInfo.format = { type: v, density }; return this; } info() { return { ...this._parsedInfo, fullSize: fullSize(this._sourceDims, this._parsedInfo) }; } canonicalPath() { const { region, size, rotation, quality, format } = this._canonicalInfo; return `${region}/${size}/${rotation}/${quality}.${format}`; } _setSize(v) { const max = { ...this.opts?.max || {} }; max.height = max.height || max.width; this._parsedInfo.size = "left" in v ? { width: v.width, height: v.height, fit: "fill" } : { ...v }; this._constrainSize(max); return this; } _constrainSize(constraints) { const full = fullSize(this._sourceDims, this._parsedInfo); const constraint = minNum( constraints.width / full.width, constraints.height / full.height, constraints.area / (full.width * full.height) ); if (constraint < 1) { if (this._parsedInfo.size.width) { this._parsedInfo.size.width = Math.floor( this._parsedInfo.size.width * constraint ); } if (this._parsedInfo.size.height) { this._parsedInfo.size.height = Math.floor( this._parsedInfo.size.height * constraint ); } } } _canonicalSize() { const { width, height } = this._parsedInfo.size; const result = `${width},${height}`; return this._parsedInfo.size.fit === "inside" ? `!${result}` : result; } _constrainRegion() { let { left, top, width, height } = this._parsedInfo.region; left = Math.max(left, 0); top = Math.max(top, 0); if (left > this.dims.width || top > this.dims.height) { throw new IIIFError("Region is out of bounds", { statusCode: 400 }); } width = Math.min(width, this.dims.width - left); height = Math.min(height, this.dims.height - top); this._parsedInfo.region = { left, top, width, height }; } }; function minNum(...args) { const nums = args.filter((arg) => typeof arg === "number" && !isNaN(arg)); return Math.min(...nums); } function fillMissingDimension(size, aspect) { if (!size.width && size.height != null) size.width = Math.floor(size.height * aspect); if (!size.height && size.width != null) size.height = Math.floor(size.width / aspect); } function fullSize(dims, { region, size }) { const regionAspect = region.width / region.height; fillMissingDimension(size, regionAspect); const scaleFactor = size.width / region.width; const result = { width: Math.floor(dims.width * scaleFactor), height: Math.floor(dims.height * scaleFactor) }; debug( "Region %j at size %j yields full size %j, a scale factor of %f", region, size, result, scaleFactor ); return result; } function regionSquare(dims) { let result = { left: 0, top: 0, width: dims.width, height: dims.height }; if (dims.width !== dims.height) { const side = Math.min(dims.width, dims.height); result = { ...result, width: side, height: side }; const offset = Math.abs(Math.floor((dims.width - dims.height) / 2)); if (dims.width > dims.height) { result.left = offset; result.top = 0; } else { result.left = 0; result.top = offset; } } return result; } function regionPct(v, dims) { let x, y, w, h; [x, y, w, h] = v.split(/\s*,\s*/).map((pct) => Number(pct) / 100); [x, w] = [x, w].map((val) => Math.floor(dims.width * val)); [y, h] = [y, h].map((val) => Math.floor(dims.height * val)); return regionXYWH([x, y, w, h]); } function regionXYWH(v) { const parts = typeof v === "string" ? v.split(/\s*,\s*/).map((val) => Number(val)) : v; const result = { left: parts[0], top: parts[1], width: parts[2], height: parts[3] }; if (result.width === 0 || result.height === 0) { throw new IIIFError("Region width and height must both be > 0", { statusCode: 400 }); } return result; } function sizePct(v, dims) { const pct = Number(v); if (isNaN(pct) || pct <= 0) { throw new IIIFError(`Invalid resize %: ${v}`, { statusCode: 400 }); } const width = Math.floor(dims.width * (pct / 100)); return sizeWH(`${width},`); } function sizeWH(v) { const result = { fit: "fill" }; if (v[0] === "!") { result.fit = "inside"; v = v.slice(1); } const parts = v.split(/\s*,\s*/).map((val) => val === "" ? null : Number(val)); [result.width, result.height] = parts; if (result.width === 0 || result.height === 0) { throw new IIIFError("Resize width and height must both be > 0", { statusCode: 400 }); } return result; } // src/calculator/v2.ts var Calculator = class extends Base { }; // src/v2/info.ts var profileLink = "http://iiif.io/api/image/2/level2.json"; var IIIFProfile = { formats: new Set(Formats), qualities: new Set(Qualities), supports: /* @__PURE__ */ new Set([ "baseUriRedirect", "canonicalLinkHeader", "cors", "jsonldMediaType", "mirroring", "profileLinkHeader", "regionByPct", "regionByPx", "regionSquare", "rotationArbitrary", "rotationBy90s", "sizeAboveFull", "sizeByConfinedWh", "sizeByDistortedWh", "sizeByForcedWh", "sizeByH", "sizeByPct", "sizeByW", "sizeByWh", "sizeByWhListed" ]) }; function infoDoc({ id, width, height, sizes, max }) { const maxAttrs = { maxWidth: max?.width, maxHeight: max?.height, maxArea: max?.area }; return { "@context": "http://iiif.io/api/image/2/context.json", "@id": id, protocol: "http://iiif.io/api/image", width, height, sizes, tiles: [ { width: 512, height: 512, scaleFactors: sizes.map((_v, i) => 2 ** i) } ], profile: [profileLink, { ...IIIFProfile, ...maxAttrs }] }; } // src/v3/index.ts var v3_exports = {}; __export(v3_exports, { Base: () => Base, Calculator: () => Calculator2, Formats: () => Formats, Qualities: () => Qualities, infoDoc: () => infoDoc2, profileLink: () => profileLink2 }); // src/calculator/v3.ts var Calculator2 = class extends Base { static _matchers() { const result = { ...super._matchers() }; result.size = [...result.size].reduce( (sizes, pattern) => { if (pattern !== "full") sizes.push(`\\^?${pattern}`); return sizes; }, [] ); return result; } constructor(dims, opts = {}) { super(dims, opts); this._canonicalInfo.size = "max"; this._parsedInfo.upscale = false; } size(v) { if (v[0] === "^") { this._parsedInfo.upscale = true; v = v.slice(1, v.length); } super.size(v); const { region, size, upscale } = this._parsedInfo; if (!upscale) { if (size.width > region.width || size.height > region.height) { throw new IIIFError("Requested size requires upscaling", { statusCode: 400 }); } } return this; } }; // src/v3/info.ts var profileLink2 = "https://iiif.io/api/image/3/level2.json"; var defaultFormats = /* @__PURE__ */ new Set(["jpg", "png"]); var defaultQualities = /* @__PURE__ */ new Set(["default"]); var IIIFExtras = { extraFeatures: [ "canonicalLinkHeader", "mirroring", "profileLinkHeader", "rotationArbitrary", "sizeByDistortedWh", "sizeByForcedWh", "sizeByWhListed", "sizeUpscaling" ], extraFormats: new Set(Formats.filter((f) => !defaultFormats.has(f))), extraQualities: new Set(Qualities.filter((q) => !defaultQualities.has(q))) }; function infoDoc2({ id, width, height, sizes, max }) { const maxAttrs = { maxWidth: max?.width, maxHeight: max?.height, maxArea: max?.area }; return { "@context": "http://iiif.io/api/image/3/context.json", id, type: "ImageService3", protocol: "http://iiif.io/api/image", profile: "level2", width, height, sizes, tiles: [ { width: 512, height: 512, scaleFactors: sizes.map((_v, i) => 2 ** i) } ], ...IIIFExtras, ...maxAttrs }; } // src/versions.ts var Versions = { 2: v2_exports, 3: v3_exports }; var versions_default = Versions; // src/transform.ts var debug2 = (0, import_debug2.default)("iiif-processor:transform"); var DEFAULT_PAGE_THRESHOLD = 1; var SCALE_PRECISION = 1e7; var Operations = class { constructor(version, dims, opts) { const { sharp: sharp2, pageThreshold, ...rest } = { ...opts }; const Implementation = Versions[version]; this.calculator = new Implementation.Calculator(dims[0], rest); this.pageThreshold = typeof pageThreshold === "number" ? pageThreshold : DEFAULT_PAGE_THRESHOLD; this.pages = dims.map((dim, page) => ({ ...dim, page })).sort((a, b) => b.width * b.height - a.width * a.height); this.sharpOptions = sharp2; } region(v) { this.calculator.region(v); return this; } size(v) { this.calculator.size(v); return this; } rotation(v) { this.calculator.rotation(v); return this; } quality(v) { this.calculator.quality(v); return this; } format(v, density) { this.calculator.format(v, density); return this; } info() { return this.calculator.info(); } canonicalPath() { return this.calculator.canonicalPath(); } withMetadata(v) { this.keepMetadata = v; return this; } computePage() { const { fullSize: fullSize2 } = this.info(); const { page } = this.pages.find((_candidate, index) => { const next = this.pages[index + 1]; debug2("comparing candidate %j to target %j with a %d-pixel buffer", next, fullSize2, this.pageThreshold); return !next || next.width + this.pageThreshold < fullSize2.width && next.height + this.pageThreshold < fullSize2.height; }); const resolution = this.pages[page]; const scale = page === 0 ? 1 : Math.round(resolution.width / this.pages[0].width * SCALE_PRECISION) / SCALE_PRECISION; debug2("Using page %d (%j) as source and scaling by %f", page, resolution, scale); return { page, scale }; } pipeline() { const pipeline = (0, import_sharp.default)({ limitInputPixels: false, ...{ ...this.sharpOptions } }); const { page, scale } = this.computePage(); pipeline.options.input.page = page; const { format, quality, region, rotation: { flop, degree }, size } = this.info(); scaleRegion(region, scale, this.pages[page]); pipeline.autoOrient().extract(region).resize(size); if (flop) pipeline.flop(); pipeline.rotate(degree); if (quality === "gray") pipeline.grayscale(); if (quality === "bitonal") pipeline.threshold(); setFormat(pipeline, format); if (this.keepMetadata) pipeline.keepMetadata(); debug2("Pipeline: %j", { page, region, size, rotation: { flop, degree }, quality, format }); return pipeline; } }; function setFormat(pipeline, format) { let pipelineFormat; const pipelineOptions = {}; switch (format.type) { case "jpeg": pipelineFormat = "jpg"; break; case "tif": pipelineFormat = "tiff"; if (format.density) { pipelineOptions.xres = format.density / 25.4; pipelineOptions.yres = format.density / 25.4; } break; default: pipelineFormat = format.type; } pipeline.toFormat(pipelineFormat, pipelineOptions); if (format.density) { pipeline.withMetadata({ density: format.density }); } } function scaleRegion(region, scale, page) { region.left = Math.floor(region.left * scale); region.top = Math.floor(region.top * scale); region.width = Math.floor(region.width * scale); region.height = Math.floor(region.height * scale); region.left = Math.max(region.left, 0); region.top = Math.max(region.top, 0); region.width = Math.min(region.width, page.width); region.height = Math.min(region.height, page.height); return region; } // src/processor.ts var debug3 = (0, import_debug3.default)("iiif-processor:main"); var debugv = (0, import_debug3.default)("verbose:iiif-processor"); var defaultpathPrefix = "/iiif/{{version}}/"; function getIiifVersion(url, template) { const { origin, pathname } = new URL(url); const templateMatcher = template.replace( /\{\{version\}\}/, "(?<iiifVersion>\\d+)" ); const pathMatcher = `^(?<prefix>${templateMatcher})(?<request>.+)$`; const re = new RegExp(pathMatcher); const parsed = re.exec(pathname); if (parsed) { parsed.groups.prefix = origin + parsed.groups.prefix; return { ...parsed.groups }; } else { throw new IIIFError("Invalid IIIF path"); } } var Processor = class { constructor(url, streamResolver, opts = {}) { this.errorClass = IIIFError; this.includeMetadata = false; this.debugBorder = false; const { prefix, iiifVersion, request } = getIiifVersion( url, opts.pathPrefix || defaultpathPrefix ); if (typeof streamResolver !== "function") { throw new IIIFError("streamResolver option must be specified"); } if (opts.max?.height && !opts.max?.width) { throw new IIIFError("maxHeight cannot be specified without maxWidth"); } const defaults = { dimensionFunction: this.defaultDimensionFunction.bind(this), density: null }; this.setOpts({ ...defaults, iiifVersion, ...opts, prefix, request }).initialize(streamResolver); } setOpts(opts) { this.dimensionFunction = opts.dimensionFunction; this.max = { ...opts.max }; this.includeMetadata = !!opts.includeMetadata; this.density = opts.density; this.baseUrl = opts.prefix; this.debugBorder = !!opts.debugBorder; this.pageThreshold = opts.pageThreshold; this.sharpOptions = { ...opts.sharpOptions }; this.version = Number(opts.iiifVersion); this.request = opts.request; return this; } initialize(streamResolver) { this.Implementation = versions_default[this.version]; if (!this.Implementation) { throw new IIIFError( `No implementation found for IIIF Image API v${this.version}` ); } const params = this.Implementation.Calculator.parsePath(this.request); debug3("Parsed URL: %j", params); Object.assign(this, params); this.streamResolver = streamResolver; if (this.quality && this.format) { this.filename = [this.quality, this.format].join("."); } else if (this.info) { this.filename = "info.json"; } return this; } async withStream({ id, baseUrl }, callback) { debug3("Requesting stream for %s", id); if (this.streamResolver.length === 2) { return await this.streamResolver( { id, baseUrl }, callback ); } else { const stream = await this.streamResolver({ id, baseUrl }); return await callback(stream); } } async defaultDimensionFunction({ id, baseUrl }) { const result = []; let page = 0; const target = (0, import_sharp2.default)({ limitInputPixels: false, page }); return await this.withStream({ id, baseUrl }, async (stream) => { stream.pipe(target); const { autoOrient, ...metadata } = await target.metadata(); const { width, height, pages } = { ...metadata, ...autoOrient }; if (!width || !height) return result; result.push({ width, height }); if (!isNaN(pages)) { for (page += 1; page < pages; page++) { const scale = 1 / 2 ** page; result.push({ width: Math.floor(width * scale), height: Math.floor(height * scale) }); } } return result; }); } async dimensions() { const fallback = this.dimensionFunction !== this.defaultDimensionFunction.bind(this); if (!this.sizeInfo) { debug3( "Attempting to use dimensionFunction to retrieve dimensions for %j", this.id ); const params = { id: this.id, baseUrl: this.baseUrl }; let dims = await this.dimensionFunction(params); if (fallback && !dims) { const warning = "Unable to get dimensions for %s using custom function. Falling back to sharp.metadata()."; debug3(warning, this.id); console.warn(warning, this.id); dims = await this.defaultDimensionFunction(params); } if (!Array.isArray(dims)) dims = [dims]; this.sizeInfo = dims; } return this.sizeInfo; } async infoJson() { const [dim] = await this.dimensions(); const sizes = []; for (let size = [dim.width, dim.height]; size.every((x) => x >= 64); size = size.map((x) => Math.floor(x / 2))) { sizes.push({ width: size[0], height: size[1] }); } const uri = new URL(this.baseUrl); uri.pathname = import_path.default.join(uri.pathname, this.id); const id = uri.toString(); const doc = this.Implementation.infoDoc({ id, ...dim, sizes, max: this.max }); for (const prop in doc) { if (doc[prop] === null || doc[prop] === void 0) delete doc[prop]; } const body = JSON.stringify( doc, (_key, value) => value?.constructor === Set ? [...value] : value ); return { type: "content", contentType: "application/ld+json", body }; } operations(dim) { const sharpOpt = this.sharpOptions; const { max, pageThreshold } = this; debug3("pageThreshold: %d", pageThreshold); return new Operations(this.version, dim, { sharp: sharpOpt, max, pageThreshold }).region(this.region).size(this.size).rotation(this.rotation).quality(this.quality).format(this.format, this.density ?? void 0).withMetadata(this.includeMetadata); } async applyBorder(transformed) { const buf = await transformed.toBuffer(); const borderPipe = (0, import_sharp2.default)(buf, { limitInputPixels: false }); const { width, height } = await borderPipe.metadata(); const background = { r: 255, g: 0, b: 0, alpha: 1 }; const topBorder = { create: { width, height: 1, channels: 4, background } }; const bottomBorder = { create: { width, height: 1, channels: 4, background } }; const leftBorder = { create: { width: 1, height, channels: 4, background } }; const rightBorder = { create: { width: 1, height, channels: 4, background } }; return borderPipe.composite([ { input: topBorder, left: 0, top: 0 }, { input: bottomBorder, left: 0, top: height - 1 }, { input: leftBorder, left: 0, top: 0 }, { input: rightBorder, left: width - 1, top: 0 } ]); } async iiifImage() { debugv("Request %s", this.request); const dim = await this.dimensions(); const operations = this.operations(dim); debugv("Operations: %j", operations); const pipeline = await operations.pipeline(); const result = await this.withStream( { id: this.id, baseUrl: this.baseUrl }, async (stream) => { debug3("piping stream to pipeline"); let transformed = await stream.pipe(pipeline); if (this.debugBorder) { transformed = await this.applyBorder(transformed); } debug3("converting to buffer"); return await transformed.toBuffer(); } ); debug3("returning %d bytes", result.length); debug3("baseUrl", this.baseUrl); const canonicalUrl = new URL( import_path.default.join(this.id, operations.canonicalPath()), this.baseUrl ); return { type: "content", canonicalLink: canonicalUrl.toString(), profileLink: this.Implementation.profileLink, contentType: import_mime_types.default.lookup(this.format), body: result }; } async execute() { try { if (this.format === void 0 && this.info === void 0) { debug3("No format or info.json requested; redirecting to info.json"); return { location: new URL( import_path.default.join(this.id, "info.json"), this.baseUrl ).toString(), type: "redirect" }; } if (this.filename === "info.json") { return await this.infoJson(); } return await this.iiifImage(); } catch (err) { if (err instanceof IIIFError) { debug3("IIIFError caught: %j", err); return { type: "error", message: err.message, statusCode: err.statusCode || 500 }; } else { throw err; } } } }; // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { IIIFError, Processor, Versions }); //# sourceMappingURL=index.js.map