text: // @flow
  
  import type {DevServerOptions, Request, Response} from './types.js.flow';
  import type {
    BundleGraph,
    FilePath,
    PluginOptions,
    NamedBundle,
  } from '@parcel/types';
  import type {Diagnostic} from '@parcel/diagnostic';
  import type {FileSystem} from '@parcel/fs';
  import type {HTTPServer} from '@parcel/utils';
  
  import invariant from 'assert';
  import EventEmitter from 'events';
  import nullthrows from 'nullthrows';
  import path from 'path';
  import url from 'url';
  import {
    ansiHtml,
    createHTTPServer,
    loadConfig,
    prettyDiagnostic,
  } from '@parcel/utils';
  import serverErrors from './serverErrors';
  import fs from 'fs';
  import ejs from 'ejs';
  import connect from 'connect';
  import serveHandler from 'serve-handler';
  import {createProxyMiddleware} from 'http-proxy-middleware';
  import {URL} from 'url';
  
  function setHeaders(res: Response) {
    res.setHeader('Access-Control-Allow-Origin', '*');
    res.setHeader(
      'Access-Control-Allow-Methods',
      'GET, HEAD, PUT, PATCH, POST, DELETE',
    );
    res.setHeader(
      'Access-Control-Allow-Headers',
      'Origin, X-Requested-With, Content-Type, Accept, Content-Type',
    );
  }
  
  const SOURCES_ENDPOINT = '/__parcel_source_root';
  const TEMPLATE_404 = fs.readFileSync(
    path.join(__dirname, 'templates/404.html'),
    'utf8',
  );
  
  const TEMPLATE_500 = fs.readFileSync(
    path.join(__dirname, 'templates/500.html'),
    'utf8',
  );
  type NextFunction = (req: Request, res: Response, next?: (any) => any) => any;
  
  export default class Server extends EventEmitter {
    pending: boolean;
    options: DevServerOptions;
    rootPath: string;
    bundleGraph: BundleGraph<NamedBundle> | null;
    errors: Array<{|
      message: string,
      stack: string,
      hints: Array<string>,
    |}> | null;
    stopServer: ?() => Promise<void>;
  
    constructor(options: DevServerOptions) {
      super();
  
      this.options = options;
      try {
        this.rootPath = new URL(options.publicUrl).pathname;
      } catch (e) {
        this.rootPath = options.publicUrl;
      }
      this.pending = true;
      this.bundleGraph = null;
      this.errors = null;
    }
  
    buildStart() {
      this.pending = true;
    }
  
    buildSuccess(bundleGraph: BundleGraph<NamedBundle>) {
      this.bundleGraph = bundleGraph;
      this.errors = null;
      this.pending = false;
  
      this.emit('bundled');
    }
  
    async buildError(options: PluginOptions, diagnostics: Array<Diagnostic>) {
      this.pending = false;
      this.errors = await Promise.all(
        diagnostics.map(async d => {
          let ansiDiagnostic = await prettyDiagnostic(d, options);
  
          return {
            message: ansiHtml(ansiDiagnostic.message),
            stack: ansiDiagnostic.codeframe
              ? ansiHtml(ansiDiagnostic.codeframe)
              : ansiHtml(ansiDiagnostic.stack),
            hints: ansiDiagnostic.hints.map(hint => ansiHtml(hint)),
          };
        }),
      );
    }
  
    respond(req: Request, res: Response): mixed {
      let {pathname} = url.parse(req.originalUrl || req.url);
  
      if (pathname == null) {
        pathname = '/';
      }
  
      if (this.errors) {
        return this.send500(req, res);
      } else if (path.extname(pathname) === '') {
        // If the URL doesn't start with the public path, or the URL doesn't
        // have a file extension, send the main HTML bundle.
        return this.sendIndex(req, res);
      } else if (pathname.startsWith(SOURCES_ENDPOINT)) {
        req.url = pathname.slice(SOURCES_ENDPOINT.length);
        return this.serve(
          this.options.inputFS,
          this.options.projectRoot,
          req,
          res,
          () => this.send404(req, res),
        );
      } else if (pathname.startsWith(this.rootPath)) {
        // Otherwise, serve the file from the dist folder
        req.url =
          this.rootPath === '/' ? pathname : pathname.slice(this.rootPath.length);
        return this.serveDist(req, res, () => this.sendIndex(req, res));
      } else {
        return this.send404(req, res);
      }
    }
  
    sendIndex(req: Request, res: Response) {
      if (this.bundleGraph) {
        // If the main asset is an HTML file, serve it
        let htmlBundle = this.bundleGraph.traverseBundles(
          (bundle, context, {stop}) => {
            if (bundle.type !== 'html' || !bundle.isEntry) return;
  
            if (!context) {
              context = bundle;
            }
  
            if (
              context &&
              bundle.filePath &&
              bundle.filePath.endsWith('index.html')
            ) {
              stop();
              return bundle;
            }
          },
        );
  
        if (htmlBundle) {
          req.url = `/${path.relative(
            this.options.distDir,
            nullthrows(htmlBundle.filePath),
          )}`;
  
          this.serveDist(req, res, () => this.send404(req, res));
        } else {
          this.send404(req, res);
        }
      } else {
        this.send404(req, res);
      }
    }
  
    serveDist(
      req: Request,
      res: Response,
      next: NextFunction,
    ): Promise<void> | Promise<mixed> {
      return this.serve(
        this.options.outputFS,
        this.options.distDir,
        req,
        res,
        next,
      );
    }
  
    async serve(
      fs: FileSystem,
      root: FilePath,
      req: Request,
      res: Response,
      next: NextFunction,
    ): Promise<mixed> {
      if (req.method !== 'GET' && req.method !== 'HEAD') {
        // method not allowed
        res.statusCode = 405;
        res.setHeader('Allow', 'GET, HEAD');
        res.setHeader('Content-Length', '0');
        res.end();
        return;
      }
  
      try {
        var filePath = url.parse(req.url).pathname || '';
        filePath = decodeURIComponent(filePath);
      } catch (err) {
        return this.sendError(res, 400);
      }
  
      filePath = path.normalize('.' + path.sep + filePath);
  
      // malicious path
      if (filePath.includes(path.sep + '..' + path.sep)) {
        return this.sendError(res, 403);
      }
  
      // join / normalize from the root dir
      if (!path.isAbsolute(filePath)) {
        filePath = path.normalize(path.join(root, filePath));
      }
  
      try {
        var stat = await fs.stat(filePath);
      } catch (err) {
        if (err.code === 'ENOENT') {
          return next(req, res);
        }
  
        return this.sendError(res, 500);
      }
  
      // Fall back to next handler if not a file
      if (!stat || !stat.isFile()) {
        return next(req, res);
      }
  
      if (req.method === 'HEAD') {
        res.end();
        return;
      }
  
      setHeaders(res);
  
      return serveHandler(
        req,
        res,
        {
          public: root,
          cleanUrls: false,
        },
        {
          lstat: path => fs.stat(path),
          realpath: path => fs.realpath(path),
          createReadStream: (path, options) => fs.createReadStream(path, options),
          readdir: path => fs.readdir(path),
        },
      );
    }
  
    sendError(res: Response, statusCode: number) {
      res.statusCode = statusCode;
      setHeaders(res);
      res.end();
    }
  
    send404(req: Request, res: Response) {
      res.statusCode = 404;
      setHeaders(res);
      res.end(TEMPLATE_404);
    }
  
    send500(req: Request, res: Response): void | Response {
      setHeaders(res);
  
      res.setHeader('Content-Type', 'text/html; charset=utf-8');
      res.writeHead(500);
  
      if (this.errors) {
        return res.end(
          ejs.render(TEMPLATE_500, {
            errors: this.errors,
          }),
        );
      }
    }
  
    logAccessIfVerbose(req: Request) {
      this.options.logger.verbose({
        message: `Request: ${req.headers.host}${req.originalUrl || req.url}`,
      });
    }
  
    /**
     * Load proxy table from package.json and apply them.
     */
    async applyProxyTable(app: any): Promise<Server> {
      // avoid skipping project root
      const fileInRoot: string = path.join(this.options.projectRoot, '_');
  
      const pkg = await loadConfig(this.options.inputFS, fileInRoot, [
        '.proxyrc.js',
        '.proxyrc',
        '.proxyrc.json',
      ]);
  
      if (!pkg || !pkg.config || !pkg.files) {
        return this;
      }
  
      const cfg = pkg.config;
      const filename = path.basename(pkg.files[0].filePath);
  
      if (filename === '.proxyrc.js') {
        if (typeof cfg !== 'function') {
          this.options.logger.warn({
            message:
              "Proxy configuration file '.proxyrc.js' should export a function. Skipping...",
          });
          return this;
        }
        cfg(app);
      } else if (filename === '.proxyrc' || filename === '.proxyrc.json') {
        if (typeof cfg !== 'object') {
          this.options.logger.warn({
            message:
              "Proxy table in '.proxyrc' should be of object type. Skipping...",
          });
          return this;
        }
        for (const [context, options] of Object.entries(cfg)) {
          // each key is interpreted as context, and value as middleware options
          app.use(createProxyMiddleware(context, options));
        }
      }
  
      return this;
    }
  
    async start(): Promise<HTTPServer> {
      const finalHandler = (req: Request, res: Response) => {
        this.logAccessIfVerbose(req);
  
        const response = () => this.respond(req, res);
  
        // Wait for the parcelInstance to finish bundling if needed
        if (this.pending) {
          this.once('bundled', response);
        } else {
          response();
        }
      };
  
      const app = connect();
      await this.applyProxyTable(app);
      app.use(finalHandler);
  
      let {server, stop} = await createHTTPServer({
        cacheDir: this.options.cacheDir,
        https: this.options.https,
        inputFS: this.options.inputFS,
        listener: app,
        outputFS: this.options.outputFS,
        host: this.options.host,
      });
      this.stopServer = stop;
  
      server.listen(this.options.port, this.options.host);
      return new Promise((resolve, reject) => {
        server.once('error', err => {
          this.options.logger.error(
            ({
              message: serverErrors(err, this.options.port),
            }: Diagnostic),
          );
          reject(err);
        });
  
        server.once('listening', () => {
          resolve(server);
        });
      });
    }
  
    async stop(): Promise<void> {
      invariant(this.stopServer != null);
      await this.stopServer();
      this.stopServer = null;
    }
  }
  