/*
MIT License http://www.opensource.org/licenses/mit-license.php
Author Natsu @xiaoxiaojx
*/
"use strict";
const path = require("path");
const memoize = require("./memoize");
// data URL scheme: "data:text/javascript;charset=utf-8;base64,some-string"
// http://www.ietf.org/rfc/rfc2397.txt
const URIRegEx = /^data:([^;,]+)?((?:;[^;,]+)*?)(?:;(base64)?)?,(.*)$/i;
/**
* Decodes the provided uri.
* @param {string} uri data URI
* @returns {Buffer | null} decoded data
*/
const decodeDataURI = (uri) => {
const match = URIRegEx.exec(uri);
if (!match) return null;
const isBase64 = match[3];
const body = match[4];
if (isBase64) {
return Buffer.from(body, "base64");
}
// CSS allows to use `data:image/svg+xml;utf8, `
// so we return original body if we can't `decodeURIComponent`
try {
return Buffer.from(decodeURIComponent(body), "utf8");
} catch (_) {
return Buffer.from(body, "utf8");
}
};
/**
* A `data:` URI split into the parts a caller has to put back together.
* `payload` is the text after the comma, still in the form it was written.
* @typedef {{ mediaType: string, base64: boolean, payload: string }} ParsedDataURI
*/
// `URIRegEx`'s `.` stops at a newline, which a raw `` payload carries, and
// its `base64` group is optional in a way that reads `;charset=utf8` as one.
// This one is the strict split a rewriter needs; that one stays as the matcher
// module requests are recognized by.
const STRICT_URI_REGEXP = /^data:([^,;]*)((?:;[^,;]*)*),([\s\S]*)$/i;
const JSON_TYPE = "json";
const SVG_TYPE = "svg";
const CSS_TYPE = "css";
const HTML_TYPE = "html";
const JAVASCRIPT_TYPE = "javascript";
/**
* Every language `languageOfMediaType` can name, for a consumer that has to say
* up front which of them it handles. Stated here rather than at each such
* consumer: adding one below adds it to every ability that names this.
* @type {string[]}
*/
const EMBEDDED_LANGUAGES = [
SVG_TYPE,
CSS_TYPE,
HTML_TYPE,
JSON_TYPE,
JAVASCRIPT_TYPE
];
/**
* The language name a media type carries, for a consumer that dispatches on it
* — a `renderEmbeddedSource` renderer today. Not a grammar table read out of a
* dataset: it is the handful of essences webpack itself has a notion of, so an
* unknown one declines rather than guessing. The `+json` structured suffix is
* read off the subtype.
* @param {string} mediaType a media type, e.g. `image/svg+xml`
* @returns {string | undefined} the language, or undefined when it names none
*/
const languageOfMediaType = (mediaType) => {
const essence = mediaType.toLowerCase().trim();
if (essence === "") return undefined;
if (essence === "image/svg+xml") return SVG_TYPE;
if (essence === "text/css") return CSS_TYPE;
if (essence === "text/html") return HTML_TYPE;
if (essence === "application/json" || essence.endsWith("+json")) {
return JSON_TYPE;
}
if (/^(?:text|application)\/(?:x-)?(?:ecma|java)script$/.test(essence)) {
return JAVASCRIPT_TYPE;
}
return undefined;
};
const getMimeTypes = memoize(() => require("./mimeTypes"));
/**
* The language a file's name says it holds, for a module that embeds the file's
* text rather than a `data:` URI — the same question `languageOfMediaType`
* answers, read off the extension through webpack's own mime lookup.
* @param {string | null} filename the file's name, or null when it has none
* @returns {string | undefined} the language, or undefined when it names none
*/
const languageOfFilename = (filename) => {
if (!filename) return undefined;
const mediaType = getMimeTypes().lookup(path.extname(filename));
return typeof mediaType === "string"
? languageOfMediaType(mediaType)
: undefined;
};
/**
* Split a `data:` URI, or `null` when it is not one.
* @param {string} uri the URI, unquoted and unescaped
* @returns {ParsedDataURI | null} its parts
*/
const parseDataURI = (uri) => {
const match = STRICT_URI_REGEXP.exec(uri);
if (match === null) return null;
return {
mediaType: match[1],
base64: /;base64$/i.test(match[2]),
payload: match[3]
};
};
/**
* The payload as text, or `null` when reading it would not round-trip. A
* percent-escaped payload is declined rather than decoded: how much of it the
* author escaped is not recorded anywhere in the URI, so re-escaping would
* rewrite bytes nothing asked to change.
* @param {ParsedDataURI} parsed the split URI
* @returns {string | null} the payload as text
*/
const decodeDataURIPayload = (parsed) => {
if (!parsed.base64) {
return parsed.payload.includes("%") ? null : parsed.payload;
}
// Decoding is lenient rather than throwing, so garbage decodes to something
// that does not round-trip, which is the same answer: leave it alone.
const text = Buffer.from(parsed.payload, "base64").toString("utf8");
return Buffer.from(text, "utf8").toString("base64") === parsed.payload
? text
: null;
};
/**
* Rebuild a `data:` URI around a new payload, in the form it was written in.
* @param {ParsedDataURI} parsed the split URI
* @param {string} text the new payload as text
* @returns {string} the rebuilt URI
*/
const buildDataURI = (parsed, text) => {
const head = `data:${parsed.mediaType}${parsed.base64 ? ";base64" : ""},`;
if (parsed.base64) {
return head + Buffer.from(text, "utf8").toString("base64");
}
// Only the two that would change what the URI means: `%` starts an escape and
// `#` starts a fragment. Everything else stays as the renderer wrote it, and
// the url token's own quoting is the caller's to apply.
return head + text.replace(/%/g, "%25").replace(/#/g, "%23");
};
/**
* The minified text a renderer answered with, for one that may answer with a
* whole result instead. `undefined` is a renderer that declined.
* @param {string | { code?: string } | undefined} answer what it answered
* @returns {string | undefined} the text, or undefined
*/
const embeddedText = (answer) =>
answer === undefined || typeof answer === "string" ? answer : answer.code;
/**
* Everything a run's renderers reported, in the shape a minifier returns —
* an empty list is left off, so a run nothing was reported over is exactly
* what it always was.
* @param {{ warnings?: (Error | string)[], errors?: (Error | string)[] }[]} reported what each answered with
* @returns {{ warnings?: (Error | string)[], errors?: (Error | string)[] }} the collected diagnostics
*/
const collectEmbeddedDiagnostics = (reported) => {
/** @type {(Error | string)[]} */
const warnings = [];
/** @type {(Error | string)[]} */
const errors = [];
for (const entry of reported) {
if (entry.warnings !== undefined) warnings.push(...entry.warnings);
if (entry.errors !== undefined) errors.push(...entry.errors);
}
/** @type {{ warnings?: (Error | string)[], errors?: (Error | string)[] }} */
const out = {};
if (warnings.length !== 0) out.warnings = warnings;
if (errors.length !== 0) out.errors = errors;
return out;
};
/**
* One embedded body a print offered but could not wait for, and the text to
* print around the answer once it is in: {@link import("./SourceProcessor").DeferredWrite},
* plus what the grammar has to say about what it offered. `as` names which of
* that language's productions the body is, where it has more than one — an HTML
* `style=""` is `"block-contents"` rather than a whole stylesheet.
* @typedef {import("./SourceProcessor").DeferredWrite & { type: string, hostType: string, as?: string }} DeferredEmbeddedSource
*/
/**
* What a renderer made of one embedded body: the minified text, and anything it
* has to report about it. A bare string is the text alone, `undefined` a
* renderer that declined.
* @typedef {{ code?: string, warnings?: (Error | string)[], errors?: (Error | string)[] }} EmbeddedSourceResult
*/
/**
* Ask a renderer for one embedded body and keep what it reported. Anything it
* throws is a renderer that did not answer rather than an asset that fails to
* serialize: the rest of the print stands, the body is spelled as an untapped
* run spells it, and why it was is not lost.
* @param {(source: string, info: { type: string, hostType: string, as?: string }) => Promise | string | EmbeddedSourceResult | undefined} render the caller's renderer
* @param {{ source: string, type: string, hostType: string, as?: string }} hole the body to offer
* @param {EmbeddedSourceResult[]} reported collects what each says about them
* @returns {Promise} what it answered, or undefined where it declined or threw
*/
const askEmbeddedRenderer = async (render, hole, reported) => {
const { source, type, hostType, as } = hole;
try {
const answer = await render(source, {
type,
hostType,
...(as === undefined ? undefined : { as })
});
if (
answer !== undefined &&
typeof answer !== "string" &&
(answer.warnings !== undefined || answer.errors !== undefined)
) {
reported.push(answer);
}
return answer;
} catch (error) {
reported.push({ errors: [/** @type {Error} */ (error)] });
return undefined;
}
};
module.exports = {
EMBEDDED_LANGUAGES,
URIRegEx,
askEmbeddedRenderer,
buildDataURI,
collectEmbeddedDiagnostics,
decodeDataURI,
decodeDataURIPayload,
embeddedText,
languageOfFilename,
languageOfMediaType,
parseDataURI
};