| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144 |
- /*
- MIT License http://www.opensource.org/licenses/mit-license.php
- Author Alexander Akait @alexander-akait
- */
- "use strict";
- const { parse } = require("acorn");
- const Parser = require("../Parser");
- const HtmlSourceDependency = require("../dependencies/HtmlSourceDependency");
- /** @import { Expression, ObjectExpression, Program, Property } from "estree" */
- /** @import { BuildInfo } from "../Module" */
- /** @import { ParserState, PreparsedAst } from "../Parser" */
- // acorn decorates every node with numeric source offsets (absent from the estree types).
- /** @typedef {{ start: number, end: number }} Positioned */
- // A URL carrying its own scheme (`https:`, `data:`, …) is external; it is only
- // bundled when `experiments.buildHttp` is enabled (like an absolute URL in HTML).
- const ABSOLUTE_URL_SCHEME_REGEXP = /^[a-zA-Z][a-zA-Z\d+\-.]*:/;
- // Manifest members whose array items carry an asset `src` (`icons[].src`,
- // `screenshots[].src`, and `shortcuts[].icons[].src` — the last nests under `icons`).
- const ICON_CONTAINER_KEYS = new Set(["icons", "screenshots"]);
- const BOM_BYTE_LENGTH = 3;
- /**
- * Parses a Web App Manifest (`.webmanifest`) so its icon/screenshot `src`
- * URLs are bundled as assets. Reuses acorn (JSON is a valid JS expression
- * literal) to recover the exact source offsets, then emits an
- * `HtmlSourceDependency` per URL so the generator can rewrite it in place.
- */
- class WebManifestParser extends Parser {
- /**
- * Parses the provided source and updates the parser state.
- * @param {string | Buffer | PreparsedAst} source the source to parse
- * @param {ParserState} state the parser state
- * @returns {ParserState} the parser state
- */
- parse(source, state) {
- // The generator replaces over the raw buffer, so the emitted ranges are
- // byte offsets rather than the UTF-16 ones acorn reports.
- const binary = Buffer.isBuffer(source);
- if (binary) {
- source = /** @type {Buffer} */ (source).toString("utf8");
- } else if (typeof source === "object") {
- throw new Error("webpackAst is unexpected for the WebManifestParser");
- }
- let leadingBytes = 0;
- if (source[0] === "") {
- source = source.slice(1);
- leadingBytes = binary ? BOM_BYTE_LENGTH : 1;
- }
- const text = /** @type {string} */ (source);
- const asciiOnly = !binary || Buffer.byteLength(text) === text.length;
- /**
- * @param {number} offset offset in UTF-16 code units into `text`
- * @returns {number} matching offset into the module source
- */
- const toSourceOffset = (offset) =>
- (asciiOnly ? offset : Buffer.byteLength(text.slice(0, offset))) +
- leadingBytes;
- const module = state.module;
- // Without `buildHttp` there is no handler to fetch remote icons, so an
- // absolute URL is left as-is rather than rewritten to an ignored asset.
- const buildHttp = Boolean(state.options.experiments.buildHttp);
- /** @type {Program | undefined} */
- let ast;
- try {
- // Wrap in parens so the top-level object is an expression, not a block.
- ast = /** @type {Program} */ (
- /** @type {unknown} */ (parse(`(${source})`, { ecmaVersion: "latest" }))
- );
- } catch (_err) {
- // Not valid JSON — leave the file untouched (still emitted as-is).
- ast = undefined;
- }
- const statement = ast && ast.body[0];
- if (
- statement &&
- statement.type === "ExpressionStatement" &&
- statement.expression.type === "ObjectExpression"
- ) {
- /**
- * @param {ObjectExpression} obj object expression node
- * @param {string | undefined} parentKey key of the array/object this node sits in
- */
- const walk = (obj, parentKey) => {
- for (const prop of obj.properties) {
- if (prop.type !== "Property") continue;
- const key =
- prop.key.type === "Literal" && typeof prop.key.value === "string"
- ? prop.key.value
- : prop.key.type === "Identifier"
- ? prop.key.name
- : undefined;
- const value = /** @type {Expression} */ (prop.value);
- if (value.type === "ObjectExpression") {
- walk(value, key);
- } else if (value.type === "ArrayExpression") {
- for (const element of value.elements) {
- if (element && element.type === "ObjectExpression") {
- walk(element, key);
- }
- }
- } else if (
- key === "src" &&
- value.type === "Literal" &&
- typeof value.value === "string" &&
- parentKey !== undefined &&
- ICON_CONTAINER_KEYS.has(parentKey)
- ) {
- const url = value.value;
- // Fragment-only refs aren't assets; absolute URLs need `buildHttp`.
- if (!url || url.startsWith("#")) continue;
- if (!buildHttp && ABSOLUTE_URL_SCHEME_REGEXP.test(url)) continue;
- // acorn offsets are into `(${source})`; the leading `(` and the
- // opening quote cancel, so the inner span is [start, end - 2].
- const { start, end } = /** @type {Positioned} */ (
- /** @type {unknown} */ (value)
- );
- const dep = new HtmlSourceDependency(url, [
- toSourceOffset(start),
- toSourceOffset(end - 2)
- ]);
- module.addDependency(dep);
- module.addCodeGenerationDependency(dep);
- }
- }
- };
- walk(statement.expression, undefined);
- }
- /** @type {BuildInfo} */
- (state.module.buildInfo).strict = true;
- return state;
- }
- }
- module.exports = WebManifestParser;
|