hunk.code_window:         "@npm//@types/node",
           "@npm//ajv",
           "@npm//ajv-formats",
           "@npm//jsonc-parser",
           "@npm//rxjs",
           "@npm//source-map",  # @external
           # @node_module: typescript:es2015.proxy
           # @node_module: typescript:es2015.reflect
           # @node_module: typescript:es2015.symbol.wellknown
           # @typings: source_map
       ],
  
hunk.labels: keep keep keep keep keep replace keep keep keep keep keep
hunk.after_edit:         "@npm//source-map",
  
hunk.file_path: packages/angular_devkit/core/BUILD.bazel
hunk.type: replace
file: /**
   * @license
   * Copyright Google LLC All Rights Reserved.
   *
   * Use of this source code is governed by an MIT-style license that can be
   * found in the LICENSE file at https://angular.io/license
   */
  
  import * as cacache from 'cacache';
  import * as fs from 'fs';
  import * as https from 'https';
  import proxyAgent from 'https-proxy-agent';
  import { join } from 'path';
  import { URL } from 'url';
  import { NormalizedCachedOptions } from '../normalize-cache';
  import { VERSION } from '../package-version';
  import { htmlRewritingStream } from './html-rewriting-stream';
  
  interface FontProviderDetails {
    preconnectUrl: string;
  }
  
  export interface InlineFontsOptions {
    minify?: boolean;
    cache?: NormalizedCachedOptions;
  }
  
  const SUPPORTED_PROVIDERS: Record<string, FontProviderDetails> = {
    'fonts.googleapis.com': {
      preconnectUrl: 'https://fonts.gstatic.com',
    },
    'use.typekit.net': {
      preconnectUrl: 'https://use.typekit.net',
    },
  };
  
  export class InlineFontsProcessor {
    private readonly cachePath: string | undefined;
    constructor(private options: InlineFontsOptions) {
      const { path: cacheDirectory, enabled } = this.options.cache || {};
      if (cacheDirectory && enabled) {
        this.cachePath = join(cacheDirectory, 'angular-build-fonts');
      }
    }
  
    async process(content: string): Promise<string> {
      const hrefList: string[] = [];
      const existingPreconnect = new Set<string>();
  
      // Collector link tags with href
      const { rewriter: collectorStream } = await htmlRewritingStream(content);
  
      collectorStream.on('startTag', (tag) => {
        const { tagName, attrs } = tag;
  
        if (tagName !== 'link') {
          return;
        }
  
        let hrefValue: string | undefined;
        let relValue: string | undefined;
        for (const { name, value } of attrs) {
          switch (name) {
            case 'rel':
              relValue = value;
              break;
  
            case 'href':
              hrefValue = value;
              break;
          }
  
          if (hrefValue && relValue) {
            switch (relValue) {
              case 'stylesheet':
                // <link rel="stylesheet" href="https://example.com/main.css">
                hrefList.push(hrefValue);
                break;
  
              case 'preconnect':
                // <link rel="preconnect" href="https://example.com">
                existingPreconnect.add(hrefValue.replace(/\/$/, ''));
                break;
            }
  
            return;
          }
        }
      });
  
      await new Promise((resolve) => collectorStream.on('finish', resolve));
  
      // Download stylesheets
      const hrefsContent = new Map<string, string>();
      const newPreconnectUrls = new Set<string>();
  
      for (const hrefItem of hrefList) {
        const url = this.createNormalizedUrl(hrefItem);
        if (!url) {
          continue;
        }
  
        const content = await this.processHref(url);
        if (content === undefined) {
          continue;
        }
  
        hrefsContent.set(hrefItem, content);
  
        // Add preconnect
        const preconnectUrl = this.getFontProviderDetails(url)?.preconnectUrl;
        if (preconnectUrl && !existingPreconnect.has(preconnectUrl)) {
          newPreconnectUrls.add(preconnectUrl);
        }
      }
  
      if (hrefsContent.size === 0) {
        return content;
      }
  
      // Replace link with style tag.
      const { rewriter, transformedContent } = await htmlRewritingStream(content);
      rewriter.on('startTag', (tag) => {
        const { tagName, attrs } = tag;
  
        switch (tagName) {
          case 'head':
            rewriter.emitStartTag(tag);
            for (const url of newPreconnectUrls) {
              rewriter.emitRaw(`<link rel="preconnect" href="${url}" crossorigin>`);
            }
            break;
  
          case 'link':
            const hrefAttr =
              attrs.some(({ name, value }) => name === 'rel' && value === 'stylesheet') &&
              attrs.find(({ name, value }) => name === 'href' && hrefsContent.has(value));
            if (hrefAttr) {
              const href = hrefAttr.value;
              const cssContent = hrefsContent.get(href);
              rewriter.emitRaw(`<style type="text/css">${cssContent}</style>`);
            } else {
              rewriter.emitStartTag(tag);
            }
            break;
  
          default:
            rewriter.emitStartTag(tag);
  
            break;
        }
      });
  
      return transformedContent;
    }
  
    private async getResponse(url: URL): Promise<string> {
      const key = `${VERSION}|${url}`;
  
      if (this.cachePath) {
        const entry = await cacache.get.info(this.cachePath, key);
        if (entry) {
          return fs.promises.readFile(entry.path, 'utf8');
        }
      }
  
      let agent: proxyAgent.HttpsProxyAgent | undefined;
      const httpsProxy = process.env.HTTPS_PROXY ?? process.env.https_proxy;
  
      if (httpsProxy) {
        agent = proxyAgent(httpsProxy);
      }
  
      const data = await new Promise<string>((resolve, reject) => {
        let rawResponse = '';
        https
          .get(
            url,
            {
              agent,
              rejectUnauthorized: false,
              headers: {
                'user-agent':
                  'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.121 Safari/537.36',
              },
            },
            (res) => {
              if (res.statusCode !== 200) {
                reject(
                  new Error(
                    `Inlining of fonts failed. ${url} returned status code: ${res.statusCode}.`,
                  ),
                );
  
                return;
              }
  
              res.on('data', (chunk) => (rawResponse += chunk)).on('end', () => resolve(rawResponse));
            },
          )
          .on('error', (e) =>
            reject(
              new Error(
                `Inlining of fonts failed. An error has occurred while retrieving ${url} over the internet.\n` +
                  e.message,
              ),
            ),
          );
      });
  
      if (this.cachePath) {
        await cacache.put(this.cachePath, key, data);
      }
  
      return data;
    }
  
    private async processHref(url: URL): Promise<string | undefined> {
      const provider = this.getFontProviderDetails(url);
      if (!provider) {
        return undefined;
      }
  
      let cssContent = await this.getResponse(url);
  
      if (this.options.minify) {
        cssContent = cssContent
          // Comments.
          .replace(/\/\*([\s\S]*?)\*\//g, '')
          // New lines.
          .replace(/\n/g, '')
          // Safe spaces.
          .replace(/\s?[{:;]\s+/g, (s) => s.trim());
      }
  
      return cssContent;
    }
  
    private getFontProviderDetails(url: URL): FontProviderDetails | undefined {
      return SUPPORTED_PROVIDERS[url.hostname];
    }
  
    private createNormalizedUrl(value: string): URL | undefined {
      // Need to convert '//' to 'https://' because the URL parser will fail with '//'.
      const normalizedHref = value.startsWith('//') ? `https:${value}` : value;
      if (!normalizedHref.startsWith('http')) {
        // Non valid URL.
        // Example: relative path styles.css.
        return undefined;
      }
  
      const url = new URL(normalizedHref);
      // Force HTTPS protocol
      url.protocol = 'https:';
  
      return url;
    }
  }
  