airbnb/caravel

View on GitHub
superset-frontend/webpack.proxy-config.js

Summary

Maintainability
A
2 hrs
Test Coverage
/**
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
const zlib = require('zlib');
const { ZSTDDecompress } = require('simple-zstd');

const yargs = require('yargs');
// eslint-disable-next-line import/no-extraneous-dependencies
const parsedArgs = yargs.argv;

const parsedEnvArg = () => {
  if (parsedArgs.env) {
    return yargs(parsedArgs.env).argv;
  }
  return {};
};

const { supersetPort = 8088, superset: supersetUrl = null } = parsedEnvArg();
const backend = (supersetUrl || `http://localhost:${supersetPort}`).replace(
  '//+$/',
  '',
); // strip ending backslash

let manifest;
function isHTML(res) {
  const CONTENT_TYPE_HEADER = 'content-type';
  const contentType = res.getHeader
    ? res.getHeader(CONTENT_TYPE_HEADER)
    : res.headers[CONTENT_TYPE_HEADER];
  return contentType.includes('text/html');
}

function toDevHTML(originalHtml) {
  let html = originalHtml.replace(
    /(<head>\s*<title>)([\s\S]*)(<\/title>)/i,
    '$1[DEV] $2 $3',
  );
  if (manifest) {
    const loaded = new Set();
    // replace bundled asset files, HTML comment tags generated by Jinja macros
    // in superset/templates/superset/partials/asset_bundle.html
    html = html.replace(
      /<!-- Bundle (css|js) (.*?) START -->[\s\S]*?<!-- Bundle \1 \2 END -->/gi,
      (match, assetType, bundleName) => {
        if (bundleName in manifest.entrypoints) {
          return `<!-- DEV bundle: ${bundleName} ${assetType} START -->\n  ${(
            manifest.entrypoints[bundleName][assetType] || []
          )
            .filter(chunkFilePath => {
              if (loaded.has(chunkFilePath)) {
                return false;
              }
              loaded.add(chunkFilePath);
              return true;
            })
            .map(chunkFilePath =>
              assetType === 'css'
                ? `<link rel="stylesheet" type="text/css" href="${chunkFilePath}" />`
                : `<script src="${chunkFilePath}"></script>`,
            )
            .join(
              '\n  ',
            )}\n  <!-- DEV bundle: ${bundleName} ${assetType} END -->`;
        }
        return match;
      },
    );
  }
  return html;
}

function copyHeaders(originalResponse, response) {
  response.statusCode = originalResponse.statusCode;
  response.statusMessage = originalResponse.statusMessage;
  if (response.setHeader) {
    let keys = Object.keys(originalResponse.headers);
    if (isHTML(originalResponse)) {
      keys = keys.filter(
        key => key !== 'content-encoding' && key !== 'content-length',
      );
    }
    keys.forEach(key => {
      let value = originalResponse.headers[key];
      if (key === 'set-cookie') {
        // remove cookie domain
        value = Array.isArray(value) ? value : [value];
        value = value.map(x => x.replace(/Domain=[^;]+?/i, ''));
      } else if (key === 'location') {
        // set redirects to use local URL
        value = (value || '').replace(backend, '');
      }
      response.setHeader(key, value);
    });
  } else {
    response.headers = originalResponse.headers;
  }
}

/**
 * Manipulate HTML server response to replace asset files with
 * local webpack-dev-server build.
 */
function processHTML(proxyResponse, response) {
  let body = Buffer.from([]);
  let originalResponse = proxyResponse;
  let uncompress;
  const responseEncoding = originalResponse.headers['content-encoding'];

  // decode GZIP response
  if (responseEncoding === 'gzip') {
    uncompress = zlib.createGunzip();
  } else if (responseEncoding === 'br') {
    uncompress = zlib.createBrotliDecompress();
  } else if (responseEncoding === 'deflate') {
    uncompress = zlib.createInflate();
  } else if (responseEncoding === 'zstd') {
    uncompress = ZSTDDecompress();
  }
  if (uncompress) {
    originalResponse.pipe(uncompress);
    originalResponse = uncompress;
  }

  originalResponse
    .on('data', data => {
      body = Buffer.concat([body, data]);
    })
    .on('error', error => {
      // eslint-disable-next-line no-console
      console.error(error);
      response.end(`Error fetching proxied request: ${error.message}`);
    })
    .on('end', () => {
      response.end(toDevHTML(body.toString()));
    });
}

module.exports = newManifest => {
  manifest = newManifest;
  return {
    context: '/',
    target: backend,
    hostRewrite: true,
    changeOrigin: true,
    cookieDomainRewrite: '', // remove cookie domain
    selfHandleResponse: true, // so that the onProxyRes takes care of sending the response
    onProxyRes(proxyResponse, request, response) {
      try {
        copyHeaders(proxyResponse, response);
        if (isHTML(response)) {
          processHTML(proxyResponse, response);
        } else {
          proxyResponse.pipe(response);
        }
        response.flushHeaders();
      } catch (e) {
        response.setHeader('content-type', 'text/plain');
        response.write(`Error requesting ${request.path} from proxy:\n\n`);
        response.end(e.stack);
      }
    },
  };
};