import path from "path";
import { fileURLToPath } from "url";
import { promisify } from "util";
import connect from "connect";
import mime from "mime";
import { performance } from "perf_hooks";
import stripAnsi from "strip-ansi";
import vite from "../vite.js";
import { defaultLogOptions, error, info } from "../logger.js";
import { ssr } from "../ssr/index.js";
import { STYLE_EXTENSIONS } from "../ssr/css.js";
import { collectResources } from "../ssr/html.js";
import { createRouteManifest, matchRoute } from "../ssr/routing.js";
import { createVite } from "../create-vite.js";
import * as msg from "./messages.js";
import notFoundTemplate, { subpathNotUsedTemplate } from "./template/4xx.js";
import serverErrorTemplate from "./template/5xx.js";
import { viteifyURL } from "../util.js";
async function dev(config, options = { logging: defaultLogOptions }) {
  const server = new AstroDevServer(config, options);
  await server.start();
  process.on("SIGTERM", () => server.stop());
  return {
    hostname: server.hostname,
    port: server.port,
    server: server.app,
    stop: () => server.stop()
  };
}
class AstroDevServer {
  constructor(config, options) {
    this.app = connect();
    this.routeCache = {};
    this.config = config;
    this.hostname = config.devOptions.hostname || "localhost";
    this.logging = options.logging;
    this.port = config.devOptions.port;
    this.origin = `http://localhost:${this.port}`;
    this.site = config.buildOptions.site ? new URL(config.buildOptions.site) : void 0;
    this.devRoot = this.site ? this.site.pathname : "/";
    this.url = new URL(this.devRoot, this.origin);
    this.manifest = createRouteManifest({ config });
  }
  async start() {
    const devStart = performance.now();
    this.viteServer = await this.createViteServer();
    this.app.use(this.viteServer.middlewares);
    this.app.use((req, res, next) => this.handleRequest(req, res, next));
    this.app.use((req, res, next) => this.renderError(req, res, next));
    await this.listen(devStart);
  }
  async stop() {
    if (this.viteServer) {
      await this.viteServer.close();
    }
    if (this.httpServer) {
      await promisify(this.httpServer.close.bind(this.httpServer))();
    }
  }
  async handleHotUpdate({ file, modules }) {
    var _a;
    const { viteServer } = this;
    if (!viteServer)
      throw new Error(`AstroDevServer.start() not called`);
    for (const module of modules) {
      viteServer.moduleGraph.invalidateModule(module);
    }
    const route = this.mostRecentRoute;
    const pathname = (_a = route == null ? void 0 : route.pathname) != null ? _a : "/";
    if (!route) {
      viteServer.ws.send({
        type: "full-reload"
      });
      return [];
    }
    try {
      const filePath = new URL(`./${route.component}`, this.config.projectRoot);
      const html = await ssr({
        astroConfig: this.config,
        filePath,
        logging: this.logging,
        mode: "development",
        origin: this.origin,
        pathname,
        route,
        routeCache: this.routeCache,
        viteServer
      });
      let invalidatedModules = [];
      await Promise.all(collectResources(html).filter(({ href }) => {
        if (!href)
          return false;
        const ext = path.extname(href);
        return STYLE_EXTENSIONS.has(ext);
      }).map(async ({ href }) => {
        const viteModule = viteServer.moduleGraph.getModuleById(`${href}?direct`) || await viteServer.moduleGraph.getModuleByUrl(`${href}?direct`) || viteServer.moduleGraph.getModuleById(href) || await viteServer.moduleGraph.getModuleByUrl(href);
        if (viteModule) {
          invalidatedModules.push(viteModule);
          viteServer.moduleGraph.invalidateModule(viteModule);
        }
      }));
      viteServer.ws.send({
        type: "custom",
        event: "astro:reload",
        data: { html }
      });
      for (const viteModule of invalidatedModules) {
        setTimeout(() => {
          viteServer.ws.send({
            type: "update",
            updates: [
              {
                type: viteModule.type === "js" ? "js-update" : "css-update",
                path: viteModule.id || viteModule.file || viteModule.url,
                acceptedPath: viteModule.url,
                timestamp: Date.now()
              }
            ]
          });
        }, 150);
      }
      return [];
    } catch (e) {
      const err = e;
      console.error(err.stack);
      viteServer.ws.send({
        type: "full-reload"
      });
      return [];
    }
  }
  listen(devStart) {
    let showedPortTakenMsg = false;
    return new Promise((resolve, reject) => {
      const listen = () => {
        var _a;
        this.httpServer = this.app.listen(this.port, this.hostname, () => {
          info(this.logging, "astro", msg.devStart({ startupTime: performance.now() - devStart }));
          info(this.logging, "astro", msg.devHost({ host: `http://${this.hostname}:${this.port}${this.devRoot}` }));
          resolve();
        });
        (_a = this.httpServer) == null ? void 0 : _a.on("error", onError);
      };
      const onError = (err) => {
        var _a;
        if (err.code && err.code === "EADDRINUSE") {
          if (!showedPortTakenMsg) {
            info(this.logging, "astro", msg.portInUse({ port: this.port }));
            showedPortTakenMsg = true;
          }
          this.port++;
          return listen();
        } else {
          error(this.logging, "astro", err.stack);
          (_a = this.httpServer) == null ? void 0 : _a.removeListener("error", onError);
          reject(err);
        }
      };
      listen();
    });
  }
  async createViteServer() {
    const viteConfig = await createVite(vite.mergeConfig({
      mode: "development",
      server: {
        middlewareMode: "ssr",
        host: this.hostname
      }
    }, this.config.vite || {}), { astroConfig: this.config, logging: this.logging, devServer: this });
    const viteServer = await vite.createServer(viteConfig);
    const pagesDirectory = fileURLToPath(this.config.pages);
    viteServer.watcher.on("add", (file) => {
      if (!file.startsWith(pagesDirectory)) {
        return;
      }
      this.routeCache = {};
      this.manifest = createRouteManifest({ config: this.config });
    });
    viteServer.watcher.on("unlink", (file) => {
      if (!file.startsWith(pagesDirectory)) {
        return;
      }
      this.routeCache = {};
      this.manifest = createRouteManifest({ config: this.config });
    });
    viteServer.watcher.on("change", () => {
      this.routeCache = {};
    });
    return viteServer;
  }
  async handleRequest(req, res, next) {
    if (!this.viteServer)
      throw new Error(`AstroDevServer.start() not called`);
    let pathname = req.url || "/";
    const reqStart = performance.now();
    let filePath;
    try {
      let routePathname = pathname;
      if (this.devRoot !== "/") {
        if (pathname.startsWith(this.devRoot)) {
          routePathname = pathname.substr(this.devRoot.length) || "";
          if (!routePathname.startsWith("/")) {
            routePathname = "/" + routePathname;
          }
        } else {
          next();
          return;
        }
      }
      const route = matchRoute(routePathname, this.manifest);
      if (!route) {
        const newPathname = routePathname.startsWith("/") ? routePathname : "/" + routePathname;
        req.url = newPathname;
        next();
        return;
      }
      filePath = new URL(`./${route.component}`, this.config.projectRoot);
      const html = await ssr({
        astroConfig: this.config,
        filePath,
        logging: this.logging,
        mode: "development",
        origin: this.origin,
        pathname,
        route,
        routeCache: this.routeCache,
        viteServer: this.viteServer
      });
      this.mostRecentRoute = route;
      info(this.logging, "astro", msg.req({ url: pathname, statusCode: 200, reqTime: performance.now() - reqStart }));
      res.writeHead(200, {
        "Content-Type": mime.getType(".html"),
        "Content-Length": Buffer.byteLength(html, "utf8")
      });
      res.write(html);
      res.end();
    } catch (err) {
      const statusCode = 500;
      const mod = filePath && this.viteServer.moduleGraph.getModuleById(viteifyURL(filePath));
      if (mod) {
        for (const m of [mod, ...mod.importedModules]) {
          this.viteServer.moduleGraph.invalidateModule(m);
        }
      } else {
        this.viteServer.moduleGraph.invalidateAll();
      }
      this.viteServer.ws.send({ type: "error", err });
      let html = serverErrorTemplate({
        statusCode,
        title: "Internal Error",
        tabTitle: "500: Error",
        message: stripAnsi(err.message),
        url: err.url || void 0,
        stack: stripAnsi(err.stack)
      });
      html = await this.viteServer.transformIndexHtml(pathname, html, pathname);
      info(this.logging, "astro", msg.req({ url: pathname, statusCode: 500, reqTime: performance.now() - reqStart }));
      res.writeHead(statusCode, {
        "Content-Type": mime.getType(".html"),
        "Content-Length": Buffer.byteLength(html, "utf8")
      });
      res.write(html);
      res.end();
    }
  }
  async renderError(req, res, next) {
    if (!this.viteServer)
      throw new Error(`AstroDevServer.start() not called`);
    const pathname = req.url || "/";
    const reqStart = performance.now();
    let html = "";
    const statusCode = 404;
    const relPages = this.config.pages.href.replace(this.config.projectRoot.href, "");
    const userDefined404 = this.manifest.routes.find((route) => route.component === relPages + "404.astro");
    if (userDefined404) {
      html = await ssr({
        astroConfig: this.config,
        filePath: new URL(`./${userDefined404.component}`, this.config.projectRoot),
        logging: this.logging,
        mode: "development",
        pathname: `/${userDefined404.component}`,
        origin: this.origin,
        routeCache: this.routeCache,
        viteServer: this.viteServer
      });
    } else {
      if (pathname === "/" && !pathname.startsWith(this.devRoot)) {
        html = subpathNotUsedTemplate(this.devRoot, pathname);
      } else {
        html = notFoundTemplate({ statusCode, title: "Not found", tabTitle: "404: Not Found", pathname });
      }
    }
    info(this.logging, "astro", msg.req({ url: pathname, statusCode, reqTime: performance.now() - reqStart }));
    res.writeHead(statusCode, {
      "Content-Type": mime.getType(".html"),
      "Content-Length": Buffer.byteLength(html, "utf8")
    });
    res.write(html);
    res.end();
  }
}
export {
  AstroDevServer,
  dev as default
};
//# sourceMappingURL=data:application/json;base64, 
