@jsenv/import-map
Version:
Helpers to implement importmaps.
879 lines (719 loc) • 22.5 kB
JavaScript
Object.defineProperty(exports, '__esModule', { value: true });
var logger = require('@jsenv/logger');
const assertImportMap = value => {
if (value === null) {
throw new TypeError(`an importMap must be an object, got null`);
}
const type = typeof value;
if (type !== "object") {
throw new TypeError(`an importMap must be an object, received ${value}`);
}
if (Array.isArray(value)) {
throw new TypeError(`an importMap must be an object, received array ${value}`);
}
};
const hasScheme = string => {
return /^[a-zA-Z]{2,}:/.test(string);
};
const urlToScheme = urlString => {
const colonIndex = urlString.indexOf(":");
if (colonIndex === -1) return "";
return urlString.slice(0, colonIndex);
};
const urlToPathname = urlString => {
return ressourceToPathname(urlToRessource(urlString));
};
const urlToRessource = urlString => {
const scheme = urlToScheme(urlString);
if (scheme === "file") {
return urlString.slice("file://".length);
}
if (scheme === "https" || scheme === "http") {
// remove origin
const afterProtocol = urlString.slice(scheme.length + "://".length);
const pathnameSlashIndex = afterProtocol.indexOf("/", "://".length);
return afterProtocol.slice(pathnameSlashIndex);
}
return urlString.slice(scheme.length + 1);
};
const ressourceToPathname = ressource => {
const searchSeparatorIndex = ressource.indexOf("?");
return searchSeparatorIndex === -1 ? ressource : ressource.slice(0, searchSeparatorIndex);
};
const urlToOrigin = urlString => {
const scheme = urlToScheme(urlString);
if (scheme === "file") {
return "file://";
}
if (scheme === "http" || scheme === "https") {
const secondProtocolSlashIndex = scheme.length + "://".length;
const pathnameSlashIndex = urlString.indexOf("/", secondProtocolSlashIndex);
if (pathnameSlashIndex === -1) return urlString;
return urlString.slice(0, pathnameSlashIndex);
}
return urlString.slice(0, scheme.length + 1);
};
const pathnameToParentPathname = pathname => {
const slashLastIndex = pathname.lastIndexOf("/");
if (slashLastIndex === -1) {
return "/";
}
return pathname.slice(0, slashLastIndex + 1);
};
// could be useful: https://url.spec.whatwg.org/#url-miscellaneous
const resolveUrl = (specifier, baseUrl) => {
if (baseUrl) {
if (typeof baseUrl !== "string") {
throw new TypeError(writeBaseUrlMustBeAString({
baseUrl,
specifier
}));
}
if (!hasScheme(baseUrl)) {
throw new Error(writeBaseUrlMustBeAbsolute({
baseUrl,
specifier
}));
}
}
if (hasScheme(specifier)) {
return specifier;
}
if (!baseUrl) {
throw new Error(writeBaseUrlRequired({
baseUrl,
specifier
}));
} // scheme relative
if (specifier.slice(0, 2) === "//") {
return `${urlToScheme(baseUrl)}:${specifier}`;
} // origin relative
if (specifier[0] === "/") {
return `${urlToOrigin(baseUrl)}${specifier}`;
}
const baseOrigin = urlToOrigin(baseUrl);
const basePathname = urlToPathname(baseUrl);
if (specifier === ".") {
const baseDirectoryPathname = pathnameToParentPathname(basePathname);
return `${baseOrigin}${baseDirectoryPathname}`;
} // pathname relative inside
if (specifier.slice(0, 2) === "./") {
const baseDirectoryPathname = pathnameToParentPathname(basePathname);
return `${baseOrigin}${baseDirectoryPathname}${specifier.slice(2)}`;
} // pathname relative outside
if (specifier.slice(0, 3) === "../") {
let unresolvedPathname = specifier;
const importerFolders = basePathname.split("/");
importerFolders.pop();
while (unresolvedPathname.slice(0, 3) === "../") {
unresolvedPathname = unresolvedPathname.slice(3); // when there is no folder left to resolved
// we just ignore '../'
if (importerFolders.length) {
importerFolders.pop();
}
}
const resolvedPathname = `${importerFolders.join("/")}/${unresolvedPathname}`;
return `${baseOrigin}${resolvedPathname}`;
} // bare
if (basePathname === "") {
return `${baseOrigin}/${specifier}`;
}
if (basePathname[basePathname.length] === "/") {
return `${baseOrigin}${basePathname}${specifier}`;
}
return `${baseOrigin}${pathnameToParentPathname(basePathname)}${specifier}`;
};
const writeBaseUrlMustBeAString = ({
baseUrl,
specifier
}) => `baseUrl must be a string.
--- base url ---
${baseUrl}
--- specifier ---
${specifier}`;
const writeBaseUrlMustBeAbsolute = ({
baseUrl,
specifier
}) => `baseUrl must be absolute.
--- base url ---
${baseUrl}
--- specifier ---
${specifier}`;
const writeBaseUrlRequired = ({
baseUrl,
specifier
}) => `baseUrl required to resolve relative specifier.
--- base url ---
${baseUrl}
--- specifier ---
${specifier}`;
const tryUrlResolution = (string, url) => {
const result = resolveUrl(string, url);
return hasScheme(result) ? result : null;
};
const resolveSpecifier = (specifier, importer) => {
if (specifier === "." || specifier[0] === "/" || specifier.startsWith("./") || specifier.startsWith("../")) {
return resolveUrl(specifier, importer);
}
if (hasScheme(specifier)) {
return specifier;
}
return null;
};
const applyImportMap = ({
importMap,
specifier,
importer,
createBareSpecifierError = ({
specifier,
importer
}) => {
return new Error(logger.createDetailedMessage(`Unmapped bare specifier.`, {
specifier,
importer
}));
},
onImportMapping = () => {}
}) => {
assertImportMap(importMap);
if (typeof specifier !== "string") {
throw new TypeError(logger.createDetailedMessage("specifier must be a string.", {
specifier,
importer
}));
}
if (importer) {
if (typeof importer !== "string") {
throw new TypeError(logger.createDetailedMessage("importer must be a string.", {
importer,
specifier
}));
}
if (!hasScheme(importer)) {
throw new Error(logger.createDetailedMessage(`importer must be an absolute url.`, {
importer,
specifier
}));
}
}
const specifierUrl = resolveSpecifier(specifier, importer);
const specifierNormalized = specifierUrl || specifier;
const {
scopes
} = importMap;
if (scopes && importer) {
const scopeSpecifierMatching = Object.keys(scopes).find(scopeSpecifier => {
return scopeSpecifier === importer || specifierIsPrefixOf(scopeSpecifier, importer);
});
if (scopeSpecifierMatching) {
const scopeMappings = scopes[scopeSpecifierMatching];
const mappingFromScopes = applyMappings(scopeMappings, specifierNormalized, scopeSpecifierMatching, onImportMapping);
if (mappingFromScopes !== null) {
return mappingFromScopes;
}
}
}
const {
imports
} = importMap;
if (imports) {
const mappingFromImports = applyMappings(imports, specifierNormalized, undefined, onImportMapping);
if (mappingFromImports !== null) {
return mappingFromImports;
}
}
if (specifierUrl) {
return specifierUrl;
}
throw createBareSpecifierError({
specifier,
importer
});
};
const applyMappings = (mappings, specifierNormalized, scope, onImportMapping) => {
const specifierCandidates = Object.keys(mappings);
let i = 0;
while (i < specifierCandidates.length) {
const specifierCandidate = specifierCandidates[i];
i++;
if (specifierCandidate === specifierNormalized) {
const address = mappings[specifierCandidate];
onImportMapping({
scope,
from: specifierCandidate,
to: address,
before: specifierNormalized,
after: address
});
return address;
}
if (specifierIsPrefixOf(specifierCandidate, specifierNormalized)) {
const address = mappings[specifierCandidate];
const afterSpecifier = specifierNormalized.slice(specifierCandidate.length);
const addressFinal = tryUrlResolution(afterSpecifier, address);
onImportMapping({
scope,
from: specifierCandidate,
to: address,
before: specifierNormalized,
after: addressFinal
});
return addressFinal;
}
}
return null;
};
const specifierIsPrefixOf = (specifierHref, href) => {
return specifierHref[specifierHref.length - 1] === "/" && href.startsWith(specifierHref);
};
// https://github.com/systemjs/systemjs/blob/89391f92dfeac33919b0223bbf834a1f4eea5750/src/common.js#L136
const composeTwoImportMaps = (leftImportMap, rightImportMap) => {
assertImportMap(leftImportMap);
assertImportMap(rightImportMap);
const importMap = {};
const leftImports = leftImportMap.imports;
const rightImports = rightImportMap.imports;
const leftHasImports = Boolean(leftImports);
const rightHasImports = Boolean(rightImports);
if (leftHasImports && rightHasImports) {
importMap.imports = composeTwoMappings(leftImports, rightImports);
} else if (leftHasImports) {
importMap.imports = { ...leftImports
};
} else if (rightHasImports) {
importMap.imports = { ...rightImports
};
}
const leftScopes = leftImportMap.scopes;
const rightScopes = rightImportMap.scopes;
const leftHasScopes = Boolean(leftScopes);
const rightHasScopes = Boolean(rightScopes);
if (leftHasScopes && rightHasScopes) {
importMap.scopes = composeTwoScopes(leftScopes, rightScopes, importMap.imports || {});
} else if (leftHasScopes) {
importMap.scopes = { ...leftScopes
};
} else if (rightHasScopes) {
importMap.scopes = { ...rightScopes
};
}
return importMap;
};
const composeTwoMappings = (leftMappings, rightMappings) => {
const mappings = {};
Object.keys(leftMappings).forEach(leftSpecifier => {
if (objectHasKey(rightMappings, leftSpecifier)) {
// will be overidden
return;
}
const leftAddress = leftMappings[leftSpecifier];
const rightSpecifier = Object.keys(rightMappings).find(rightSpecifier => {
return compareAddressAndSpecifier(leftAddress, rightSpecifier);
});
mappings[leftSpecifier] = rightSpecifier ? rightMappings[rightSpecifier] : leftAddress;
});
Object.keys(rightMappings).forEach(rightSpecifier => {
mappings[rightSpecifier] = rightMappings[rightSpecifier];
});
return mappings;
};
const objectHasKey = (object, key) => Object.prototype.hasOwnProperty.call(object, key);
const compareAddressAndSpecifier = (address, specifier) => {
const addressUrl = resolveUrl(address, "file:///");
const specifierUrl = resolveUrl(specifier, "file:///");
return addressUrl === specifierUrl;
};
const composeTwoScopes = (leftScopes, rightScopes, imports) => {
const scopes = {};
Object.keys(leftScopes).forEach(leftScopeKey => {
if (objectHasKey(rightScopes, leftScopeKey)) {
// will be merged
scopes[leftScopeKey] = leftScopes[leftScopeKey];
return;
}
const topLevelSpecifier = Object.keys(imports).find(topLevelSpecifierCandidate => {
return compareAddressAndSpecifier(leftScopeKey, topLevelSpecifierCandidate);
});
if (topLevelSpecifier) {
scopes[imports[topLevelSpecifier]] = leftScopes[leftScopeKey];
} else {
scopes[leftScopeKey] = leftScopes[leftScopeKey];
}
});
Object.keys(rightScopes).forEach(rightScopeKey => {
if (objectHasKey(scopes, rightScopeKey)) {
scopes[rightScopeKey] = composeTwoMappings(scopes[rightScopeKey], rightScopes[rightScopeKey]);
} else {
scopes[rightScopeKey] = { ...rightScopes[rightScopeKey]
};
}
});
return scopes;
};
const getCommonPathname = (pathname, otherPathname) => {
const firstDifferentCharacterIndex = findFirstDifferentCharacterIndex(pathname, otherPathname); // pathname and otherpathname are exactly the same
if (firstDifferentCharacterIndex === -1) {
return pathname;
}
const commonString = pathname.slice(0, firstDifferentCharacterIndex + 1); // the first different char is at firstDifferentCharacterIndex
if (pathname.charAt(firstDifferentCharacterIndex) === "/") {
return commonString;
}
if (otherPathname.charAt(firstDifferentCharacterIndex) === "/") {
return commonString;
}
const firstDifferentSlashIndex = commonString.lastIndexOf("/");
return pathname.slice(0, firstDifferentSlashIndex + 1);
};
const findFirstDifferentCharacterIndex = (string, otherString) => {
const maxCommonLength = Math.min(string.length, otherString.length);
let i = 0;
while (i < maxCommonLength) {
const char = string.charAt(i);
const otherChar = otherString.charAt(i);
if (char !== otherChar) {
return i;
}
i++;
}
if (string.length === otherString.length) {
return -1;
} // they differ at maxCommonLength
return maxCommonLength;
};
const urlToRelativeUrl = (urlArg, baseUrlArg) => {
const url = new URL(urlArg);
const baseUrl = new URL(baseUrlArg);
if (url.protocol !== baseUrl.protocol) {
return urlArg;
}
if (url.username !== baseUrl.username || url.password !== baseUrl.password) {
return urlArg.slice(url.protocol.length);
}
if (url.host !== baseUrl.host) {
return urlArg.slice(url.protocol.length);
}
const {
pathname,
hash,
search
} = url;
if (pathname === "/") {
return baseUrl.pathname.slice(1);
}
const {
pathname: basePathname
} = baseUrl;
const commonPathname = getCommonPathname(pathname, basePathname);
if (!commonPathname) {
return urlArg;
}
const specificPathname = pathname.slice(commonPathname.length);
const baseSpecificPathname = basePathname.slice(commonPathname.length);
if (baseSpecificPathname.includes("/")) {
const baseSpecificParentPathname = pathnameToParentPathname(baseSpecificPathname);
const relativeDirectoriesNotation = baseSpecificParentPathname.replace(/.*?\//g, "../");
return `${relativeDirectoriesNotation}${specificPathname}${search}${hash}`;
}
return `${specificPathname}${search}${hash}`;
};
const moveImportMap = (importMap, fromUrl, toUrl) => {
assertImportMap(importMap);
const makeRelativeTo = (value, type) => {
let url;
if (type === "specifier") {
url = resolveSpecifier(value, fromUrl);
if (!url) {
// bare specifier
return value;
}
} else {
url = resolveUrl(value, fromUrl);
}
const relativeUrl = urlToRelativeUrl(url, toUrl);
if (relativeUrl.startsWith("../")) {
return relativeUrl;
}
if (relativeUrl.startsWith("./")) {
return relativeUrl;
}
if (hasScheme(relativeUrl)) {
return relativeUrl;
}
return `./${relativeUrl}`;
};
const importMapRelative = {};
const {
imports
} = importMap;
if (imports) {
importMapRelative.imports = makeMappingsRelativeTo(imports, makeRelativeTo) || imports;
}
const {
scopes
} = importMap;
if (scopes) {
importMapRelative.scopes = makeScopesRelativeTo(scopes, makeRelativeTo) || scopes;
} // nothing changed
if (importMapRelative.imports === imports && importMapRelative.scopes === scopes) {
return importMap;
}
return importMapRelative;
};
const makeMappingsRelativeTo = (mappings, makeRelativeTo) => {
const mappingsTransformed = {};
const mappingsRemaining = {};
let transformed = false;
Object.keys(mappings).forEach(specifier => {
const address = mappings[specifier];
const specifierRelative = makeRelativeTo(specifier, "specifier");
const addressRelative = makeRelativeTo(address, "address");
if (specifierRelative) {
transformed = true;
mappingsTransformed[specifierRelative] = addressRelative || address;
} else if (addressRelative) {
transformed = true;
mappingsTransformed[specifier] = addressRelative;
} else {
mappingsRemaining[specifier] = address;
}
});
return transformed ? { ...mappingsTransformed,
...mappingsRemaining
} : null;
};
const makeScopesRelativeTo = (scopes, makeRelativeTo) => {
const scopesTransformed = {};
const scopesRemaining = {};
let transformed = false;
Object.keys(scopes).forEach(scopeSpecifier => {
const scopeMappings = scopes[scopeSpecifier];
const scopeSpecifierRelative = makeRelativeTo(scopeSpecifier, "address");
const scopeMappingsRelative = makeMappingsRelativeTo(scopeMappings, makeRelativeTo);
if (scopeSpecifierRelative) {
transformed = true;
scopesTransformed[scopeSpecifierRelative] = scopeMappingsRelative || scopeMappings;
} else if (scopeMappingsRelative) {
transformed = true;
scopesTransformed[scopeSpecifier] = scopeMappingsRelative;
} else {
scopesRemaining[scopeSpecifier] = scopeMappingsRelative;
}
});
return transformed ? { ...scopesTransformed,
...scopesRemaining
} : null;
};
const sortImportMap = importMap => {
assertImportMap(importMap);
const {
imports,
scopes
} = importMap;
return { ...(imports ? {
imports: sortImports(imports)
} : {}),
...(scopes ? {
scopes: sortScopes(scopes)
} : {})
};
};
const sortImports = imports => {
const mappingsSorted = {};
Object.keys(imports).sort(compareLengthOrLocaleCompare).forEach(name => {
mappingsSorted[name] = imports[name];
});
return mappingsSorted;
};
const sortScopes = scopes => {
const scopesSorted = {};
Object.keys(scopes).sort(compareLengthOrLocaleCompare).forEach(scopeSpecifier => {
scopesSorted[scopeSpecifier] = sortImports(scopes[scopeSpecifier]);
});
return scopesSorted;
};
const compareLengthOrLocaleCompare = (a, b) => {
return b.length - a.length || a.localeCompare(b);
};
const normalizeImportMap = (importMap, baseUrl) => {
assertImportMap(importMap);
if (!isStringOrUrl(baseUrl)) {
throw new TypeError(formulateBaseUrlMustBeStringOrUrl({
baseUrl
}));
}
const {
imports,
scopes
} = importMap;
return {
imports: imports ? normalizeMappings(imports, baseUrl) : undefined,
scopes: scopes ? normalizeScopes(scopes, baseUrl) : undefined
};
};
const isStringOrUrl = value => {
if (typeof value === "string") {
return true;
}
if (typeof URL === "function" && value instanceof URL) {
return true;
}
return false;
};
const normalizeMappings = (mappings, baseUrl) => {
const mappingsNormalized = {};
Object.keys(mappings).forEach(specifier => {
const address = mappings[specifier];
if (typeof address !== "string") {
console.warn(formulateAddressMustBeAString({
address,
specifier
}));
return;
}
const specifierResolved = resolveSpecifier(specifier, baseUrl) || specifier;
const addressUrl = tryUrlResolution(address, baseUrl);
if (addressUrl === null) {
console.warn(formulateAdressResolutionFailed({
address,
baseUrl,
specifier
}));
return;
}
if (specifier.endsWith("/") && !addressUrl.endsWith("/")) {
console.warn(formulateAddressUrlRequiresTrailingSlash({
addressUrl,
address,
specifier
}));
return;
}
mappingsNormalized[specifierResolved] = addressUrl;
});
return sortImports(mappingsNormalized);
};
const normalizeScopes = (scopes, baseUrl) => {
const scopesNormalized = {};
Object.keys(scopes).forEach(scopeSpecifier => {
const scopeMappings = scopes[scopeSpecifier];
const scopeUrl = tryUrlResolution(scopeSpecifier, baseUrl);
if (scopeUrl === null) {
console.warn(formulateScopeResolutionFailed({
scope: scopeSpecifier,
baseUrl
}));
return;
}
const scopeValueNormalized = normalizeMappings(scopeMappings, baseUrl);
scopesNormalized[scopeUrl] = scopeValueNormalized;
});
return sortScopes(scopesNormalized);
};
const formulateBaseUrlMustBeStringOrUrl = ({
baseUrl
}) => `baseUrl must be a string or an url.
--- base url ---
${baseUrl}`;
const formulateAddressMustBeAString = ({
specifier,
address
}) => `Address must be a string.
--- address ---
${address}
--- specifier ---
${specifier}`;
const formulateAdressResolutionFailed = ({
address,
baseUrl,
specifier
}) => `Address url resolution failed.
--- address ---
${address}
--- base url ---
${baseUrl}
--- specifier ---
${specifier}`;
const formulateAddressUrlRequiresTrailingSlash = ({
addressURL,
address,
specifier
}) => `Address must end with /.
--- address url ---
${addressURL}
--- address ---
${address}
--- specifier ---
${specifier}`;
const formulateScopeResolutionFailed = ({
scope,
baseUrl
}) => `Scope url resolution failed.
--- scope ---
${scope}
--- base url ---
${baseUrl}`;
const pathnameToExtension = pathname => {
const slashLastIndex = pathname.lastIndexOf("/");
if (slashLastIndex !== -1) {
pathname = pathname.slice(slashLastIndex + 1);
}
const dotLastIndex = pathname.lastIndexOf(".");
if (dotLastIndex === -1) return ""; // if (dotLastIndex === pathname.length - 1) return ""
return pathname.slice(dotLastIndex);
};
const resolveImport = ({
specifier,
importer,
importMap,
defaultExtension = true,
createBareSpecifierError,
onImportMapping = () => {}
}) => {
return applyDefaultExtension({
url: importMap ? applyImportMap({
importMap,
specifier,
importer,
createBareSpecifierError,
onImportMapping
}) : resolveUrl(specifier, importer),
importer,
defaultExtension
});
};
const applyDefaultExtension = ({
url,
importer,
defaultExtension
}) => {
if (urlToPathname(url).endsWith("/")) {
return url;
}
if (typeof defaultExtension === "string") {
const extension = pathnameToExtension(url);
if (extension === "") {
return `${url}${defaultExtension}`;
}
return url;
}
if (defaultExtension === true) {
const extension = pathnameToExtension(url);
if (extension === "" && importer) {
const importerPathname = urlToPathname(importer);
const importerExtension = pathnameToExtension(importerPathname);
return `${url}${importerExtension}`;
}
}
return url;
};
exports.applyImportMap = applyImportMap;
exports.composeTwoImportMaps = composeTwoImportMaps;
exports.moveImportMap = moveImportMap;
exports.normalizeImportMap = normalizeImportMap;
exports.resolveImport = resolveImport;
exports.resolveSpecifier = resolveSpecifier;
exports.resolveUrl = resolveUrl;
exports.sortImportMap = sortImportMap;
//# sourceMappingURL=jsenv_importmap.cjs.map
;