'use strict';

const api = require('@previewjs/api');
const typeAnalyzer = require('@previewjs/type-analyzer');
const cookieParser = require('cookie-parser');
const express = require('express');
const fs$1 = require('fs-extra');
const getPort = require('get-port');
const path = require('path');
const fs = require('fs');
const globby = require('globby');
const envPaths = require('env-paths');
const config = require('@previewjs/config');
const vfs = require('@previewjs/vfs');
const assertNever = require('assert-never');
const httpTerminator = require('http-terminator');
const recrawl = require('recrawl');
const fakeExportedTypesPlugin = require('rollup-plugin-friendly-type-imports');
const tsconfigLoader_js = require('tsconfig-paths/lib/tsconfig-loader.js');
const vite = require('vite');
const viteTsconfigPaths = require('vite-tsconfig-paths');
const url = require('url');
const acorn = require('acorn');

function _interopNamespaceDefault(e) {
  const n = Object.create(null);
  if (e) {
    for (const k in e) {
      n[k] = e[k];
    }
  }
  n.default = e;
  return n;
}

const vite__namespace = /*#__PURE__*/_interopNamespaceDefault(vite);

function getCacheDir(rootDirPath) {
  try {
    const { version } = JSON.parse(
      fs.readFileSync(path.resolve(__dirname, "..", "package.json"), "utf8")
    );
    return path.resolve(
      rootDirPath,
      "node_modules",
      ".previewjs",
      `v${version}`
    );
  } catch (e) {
    throw new Error(`Unable to detect @previewjs/core version.`);
  }
}

async function findFiles(rootDirPath, pattern) {
  const files = await globby(pattern, {
    gitignore: true,
    ignore: ["**/node_modules/**"],
    cwd: rootDirPath,
    absolute: true,
    followSymbolicLinks: false
  });
  return files.filter(
    (f) => f.startsWith(rootDirPath.replace(/\\/g, "/") + "/")
  );
}

async function analyzeProject(workspace, options = {}) {
  const cacheFilePath = path.join(
    getCacheDir(workspace.rootDirPath),
    "components.json"
  );
  const absoluteFilePaths = await findFiles(
    workspace.rootDirPath,
    "**/*.@(js|jsx|ts|tsx|svelte|vue)"
  );
  const filePathsSet = new Set(
    absoluteFilePaths.map(
      (absoluteFilePath) => path.relative(workspace.rootDirPath, absoluteFilePath)
    )
  );
  const existingCacheLastModified = !options.forceRefresh && fs$1.existsSync(cacheFilePath) ? fs$1.statSync(cacheFilePath).mtimeMs : 0;
  const existingCache = existingCacheLastModified ? JSON.parse(fs$1.readFileSync(cacheFilePath, "utf8")) : {};
  const changedAbsoluteFilePaths = absoluteFilePaths.filter(
    (absoluteFilePath) => fs$1.statSync(absoluteFilePath).mtimeMs > existingCacheLastModified
  );
  const recycledComponents = Object.fromEntries(
    Object.entries(existingCache).filter(
      ([filePath]) => filePathsSet.has(filePath)
    )
  );
  const refreshedComponents = await analyzeProjectCore(
    workspace,
    changedAbsoluteFilePaths
  );
  const allComponents = {
    ...recycledComponents,
    ...refreshedComponents
  };
  const components = Object.keys(allComponents).sort().reduce((ordered, key) => {
    ordered[key] = allComponents[key];
    return ordered;
  }, {});
  await fs$1.mkdirp(path.dirname(cacheFilePath));
  await fs$1.writeFile(cacheFilePath, JSON.stringify(components));
  return { components };
}
async function analyzeProjectCore(workspace, changedAbsoluteFilePaths) {
  const components = {};
  if (changedAbsoluteFilePaths.length === 0) {
    return components;
  }
  const found = await workspace.frameworkPlugin.detectComponents(
    workspace.typeAnalyzer,
    changedAbsoluteFilePaths
  );
  for (const component of found) {
    const filePath = path.relative(
      workspace.rootDirPath,
      component.absoluteFilePath
    );
    const fileComponents = components[filePath] || (components[filePath] = []);
    fileComponents.push({
      name: component.name,
      info: component.info.kind === "component" ? {
        kind: "component",
        exported: component.info.exported
      } : {
        kind: "story",
        associatedComponent: component.info.associatedComponent ? {
          filePath: path.relative(
            workspace.rootDirPath,
            component.info.associatedComponent.absoluteFilePath
          ),
          name: component.info.associatedComponent.name
        } : null
      }
    });
  }
  return components;
}

var __accessCheck = (obj, member, msg) => {
  if (!member.has(obj))
    throw TypeError("Cannot " + msg);
};
var __privateAdd = (obj, member, value) => {
  if (member.has(obj))
    throw TypeError("Cannot add the same private member more than once");
  member instanceof WeakSet ? member.add(obj) : member.set(obj, value);
};
var __privateMethod = (obj, member, method) => {
  __accessCheck(obj, member, "access private method");
  return method;
};
var _read, read_fn, _update, update_fn;
class LocalFilePersistedStateManager {
  constructor(absoluteFilePath = path.join(
    envPaths("previewjs").config,
    "state.json"
  )) {
    this.absoluteFilePath = absoluteFilePath;
    __privateAdd(this, _read);
    __privateAdd(this, _update);
    this.get = async () => {
      const state = await __privateMethod(this, _read, read_fn).call(this);
      return state || __privateMethod(this, _update, update_fn).call(this, {});
    };
    this.update = async (partialState) => {
      return __privateMethod(this, _update, update_fn).call(this, partialState);
    };
  }
}
_read = new WeakSet();
read_fn = async function() {
  try {
    const content = await fs$1.readFile(this.absoluteFilePath, "utf8");
    return JSON.parse(content);
  } catch {
    return null;
  }
};
_update = new WeakSet();
update_fn = async function(partialState) {
  const state = {
    ...await (__privateMethod(this, _read, read_fn).call(this) || {}),
    ...partialState
  };
  await fs$1.ensureDir(path.dirname(this.absoluteFilePath));
  await fs$1.writeFile(
    this.absoluteFilePath,
    JSON.stringify(state, null, 2),
    "utf8"
  );
  return state;
};

class Server {
  constructor(options) {
    this.serverTerminator = null;
    this.server = null;
    const app = express();
    app.use((_req, res, next) => {
      res.setHeader("Cache-Control", "max-age=0, must-revalidate");
      next();
    });
    for (const middleware of options.middlewares) {
      app.use(middleware);
    }
    this.app = app;
  }
  async start(port) {
    return new Promise((resolve) => {
      const server = this.server = this.app.listen(port, () => {
        console.log(`Preview.js Server running at http://localhost:${port}.`);
        resolve(server);
      });
      this.serverTerminator = httpTerminator.createHttpTerminator({
        server: this.server,
        gracefulTerminationTimeout: 0
      });
    });
  }
  async stop() {
    if (this.serverTerminator) {
      await this.serverTerminator.terminate();
      this.serverTerminator = null;
    }
    if (this.server) {
      const server = this.server;
      await new Promise((resolve) => server.close(resolve));
      this.server = null;
    }
    console.log(`Preview.js Server stopped.`);
  }
}

const COMPONENT_LOADER_MODULE = "/@component-loader.js";
function componentLoaderPlugin(options) {
  return {
    name: "previewjs:component-loader",
    resolveId: async function(id) {
      if (id.startsWith(COMPONENT_LOADER_MODULE)) {
        return id;
      }
      return null;
    },
    load: async function(id) {
      if (!id.startsWith(COMPONENT_LOADER_MODULE)) {
        return null;
      }
      const params = new url.URLSearchParams(id.split("?")[1] || "");
      return generateComponentLoaderModule(params, options);
    }
  };
}
function generateComponentLoaderModule(urlParams, {
  rootDirPath,
  detectedGlobalCssFilePaths,
  config: { wrapper }
}) {
  const filePath = urlParams.get("p");
  const componentName = urlParams.get("c");
  if (filePath === null || componentName === null) {
    throw new Error(`Invalid use of ${COMPONENT_LOADER_MODULE} module`);
  }
  const componentModuleId = `/${filePath.replace(/\\/g, "/")}`;
  return `import { updateComponent } from '/__previewjs_internal__/update-component';
import { load } from '/__previewjs_internal__/renderer/index';

let counter = 0;
export async function refresh() {
  const currentCounter = ++counter;
  let loadingError = null;
  ${wrapper && fs$1.pathExistsSync(path.join(rootDirPath, wrapper.path)) ? `
  let wrapperModulePromise;
  if (import.meta.hot.data.preloadedWrapperModule) {
    wrapperModulePromise = Promise.resolve(import.meta.hot.data.preloadedWrapperModule);
  } else {
    wrapperModulePromise = import("/${wrapper.path}");
  }
  const wrapperModule = await wrapperModulePromise.catch(e => {
    console.error(e);
    loadingError = e.stack || e.message || null;
    return null;
  });
  ` : `
  ${detectedGlobalCssFilePaths.map(
    (filePath2) => `import("/${filePath2.replace(/\\/g, "/")}").catch(console.warn);`
  ).join("\n")}
  const wrapperModule = null;
  `}
  let componentModulePromise;
  if (import.meta.hot.data.preloadedComponentModule) {
    componentModulePromise = Promise.resolve(import.meta.hot.data.preloadedComponentModule);
  } else {
    componentModulePromise = import("${componentModuleId}");
  }
  const componentModule = await componentModulePromise.catch(e => {
    console.error(e);
    loadingError = e.stack || e.message || null;
    return null;
  });
  if (currentCounter !== counter) {
    // Abort to avoid double rendering.
    return;
  }
  await updateComponent({
    wrapperModule,
    wrapperName: ${JSON.stringify(wrapper?.componentName || null)},
    componentModule,
    componentFilePath: ${JSON.stringify(filePath)},
    componentName: ${JSON.stringify(componentName)},
    updateId: ${JSON.stringify(filePath)} + "-" + ${JSON.stringify(
    componentName
  )} + counter,
    loadingError,
    load,
  })
}

import.meta.hot.accept();

${wrapper ? `
import.meta.hot.accept(["${wrapper.path}"], ([wrapperModule]) => {
  if (wrapperModule) {
    import.meta.hot.data.preloadedWrapperModule = wrapperModule;
    refresh();
  }
});
` : ``}

import.meta.hot.accept(["${componentModuleId}"], ([componentModule]) => {
  if (componentModule) {
    import.meta.hot.data.preloadedComponentModule = componentModule;
    refresh();
  }
});
`;
}

const extensions = /* @__PURE__ */ new Set([".js", ".jsx", ".ts", ".tsx"]);
const importRegexp = /import\s+([\S]+)\s+from\s+('|")([\S]+)\.(css|less|sass|scss|styl|stylus|postcss)(\?[\S]*)?&?('|")/g;
function cssModulesWithoutSuffixPlugin() {
  return {
    name: "previewjs:css-modules-without-suffix",
    async transform(code, id) {
      id = id.split("?")[0];
      if (!extensions.has(path.extname(id))) {
        return;
      }
      return code.replace(
        importRegexp,
        'import $1 from "$3.$4?$5&.module.css"'
      );
    }
  };
}

const jsExtensions$1 = /* @__PURE__ */ new Set([".js", ".jsx", ".ts", ".tsx"]);
function exportToplevelPlugin() {
  return {
    name: "previewjs:export-toplevel",
    enforce: "post",
    transform: async function(code, id) {
      if (id.indexOf(`/node_modules/`) !== -1) {
        return null;
      }
      const extension = path.extname(id);
      if (!jsExtensions$1.has(extension)) {
        return null;
      }
      const topLevelEntityNames = findTopLevelEntityNames(id, code);
      return `${code};
          export {
          ${topLevelEntityNames.map((c) => `${c} as __previewjs__${c},`).join("")}
        }`;
    }
  };
}
function findTopLevelEntityNames(filePath, source) {
  let parsed;
  try {
    parsed = acorn.Parser.parse(source, {
      ecmaVersion: "latest",
      sourceType: "module"
    });
  } catch (e) {
    throw new Error(`Unable to parse ${filePath} with Acorn: ${e}`);
  }
  const topLevelEntityNames = [];
  for (const statement of parsed.body || []) {
    if (statement.type === "VariableDeclaration") {
      for (const declaration of statement.declarations) {
        if (declaration.type === "VariableDeclarator") {
          addIfIdentifier(topLevelEntityNames, declaration.id);
        }
      }
    }
    if (statement.type === "FunctionDeclaration") {
      addIfIdentifier(topLevelEntityNames, statement.id);
    }
    if (statement.type === "ClassDeclaration") {
      addIfIdentifier(topLevelEntityNames, statement.id);
    }
    if (statement.type === "ExportDefaultDeclaration") {
      if (statement.declaration?.id) {
        addIfIdentifier(topLevelEntityNames, statement.declaration.id);
      }
    }
    if (statement.type === "ExportNamedDeclaration") {
      if (statement.declaration?.id) {
        addIfIdentifier(topLevelEntityNames, statement.declaration.id);
      }
      if (statement.declaration?.declarations) {
        for (const declarator of statement.declaration.declarations) {
          addIfIdentifier(topLevelEntityNames, declarator.id);
        }
      }
    }
  }
  return topLevelEntityNames;
}
function addIfIdentifier(array, id) {
  if (id && id.type === "Identifier") {
    const name = id.name;
    if (name.endsWith("_default")) {
      return;
    }
    array.push(name);
  }
}

const VIRTUAL_PREFIX = `/@previewjs-virtual:`;
const jsExtensions = /* @__PURE__ */ new Set([".js", ".jsx", ".ts", ".tsx"]);
function virtualPlugin(options) {
  const { reader, rootDirPath } = options;
  return {
    name: "previewjs:virtual-fs",
    resolveId: async function(id, importer) {
      const virtualImporter = importer?.startsWith(VIRTUAL_PREFIX) || false;
      if (id.indexOf(`/node_modules/`) !== -1) {
        return null;
      }
      if (id.startsWith(VIRTUAL_PREFIX)) {
        id = id.slice(VIRTUAL_PREFIX.length);
      }
      const extension = path.extname(id);
      let absoluteId;
      if (path.isAbsolute(id)) {
        absoluteId = id;
      } else {
        if (!importer) {
          return null;
        }
        if (virtualImporter) {
          importer = importer.slice(VIRTUAL_PREFIX.length);
        }
        if (extension && !jsExtensions.has(extension) && extension !== ".svg") {
          return path.join(path.dirname(importer), id);
        }
        absoluteId = path.join(path.dirname(importer), id);
      }
      const resolved = await resolveAbsoluteModuleId(absoluteId);
      if (resolved) {
        const [absoluteFilePath] = resolved;
        if (await fs$1.pathExists(absoluteFilePath) && !virtualImporter) {
          return null;
        }
        if (!absoluteId.endsWith(extension)) {
          absoluteId += extension;
        }
        const resolvedId = VIRTUAL_PREFIX + absoluteFilePath.replace(/\\/g, "/");
        return resolvedId;
      }
      return null;
    },
    load: async function(id) {
      if (id.indexOf(`/node_modules/`) !== -1) {
        return null;
      }
      if (id.startsWith(VIRTUAL_PREFIX)) {
        id = id.slice(VIRTUAL_PREFIX.length);
      }
      const resolved = await resolveAbsoluteModuleId(id);
      if (!resolved) {
        return null;
      }
      const [absoluteFilePath, entry] = resolved;
      let isAllowed = false;
      for (const allowedAbsolutePath of options.allowedAbsolutePaths) {
        const relativePath = path.relative(
          allowedAbsolutePath,
          absoluteFilePath
        );
        if (relativePath && !relativePath.startsWith("..") && !path.isAbsolute(relativePath)) {
          isAllowed = true;
          break;
        }
      }
      if (!isAllowed) {
        console.error(
          `Attempted access to ${absoluteFilePath} which is outside of allowed directories. See https://previewjs.com/docs/config and https://vitejs.dev/config/server-options.html#server-fs-allow for more information.`
        );
        return null;
      }
      if (entry.kind !== "file") {
        console.error(`Unable to read file from ${absoluteFilePath}`);
        return null;
      }
      const source = await entry.read();
      const fileExtension = path.extname(absoluteFilePath);
      if (!jsExtensions.has(fileExtension)) {
        return source;
      }
      const moduleExtension = path.extname(id);
      return {
        code: moduleExtension === "" || moduleExtension === ".js" ? (await vite.transformWithEsbuild(source, absoluteFilePath, {
          loader: "tsx",
          format: "esm",
          target: "es2020",
          sourcefile: path.relative(rootDirPath, absoluteFilePath),
          ...options.esbuildOptions
        })).code : source
      };
    },
    transform: (code, id) => {
      if (!id.startsWith(VIRTUAL_PREFIX)) {
        return null;
      }
      return {
        code,
        map: {
          mappings: "",
          names: [],
          sources: [],
          version: 3
        }
      };
    },
    handleHotUpdate: async function(context) {
      const moduleGraph = options.moduleGraph();
      if (!moduleGraph) {
        return;
      }
      const filePath = path.relative(rootDirPath, context.file);
      const absoluteFilePath = path.join(rootDirPath, filePath);
      const entry = await reader.read(absoluteFilePath);
      if (!entry || entry.kind !== "file") {
        return;
      }
      const virtualModuleId = VIRTUAL_PREFIX + absoluteFilePath.replace(/\\/g, "/");
      const node = moduleGraph.getModuleById(virtualModuleId);
      return node && [node];
    }
  };
  async function resolveAbsoluteModuleId(id) {
    return await resolveBaseFilePath(id) || await resolveBaseFilePath(path.join(rootDirPath, id));
  }
  async function resolveBaseFilePath(baseFilePath) {
    for (const suffix of [
      "",
      ...jsExtensions,
      ...[...jsExtensions].map((extension) => `${path.sep}index${extension}`)
    ]) {
      const absoluteFilePath = `${baseFilePath}${suffix}`;
      const entry = await reader.read(absoluteFilePath);
      if (entry?.kind === "file") {
        return [absoluteFilePath, entry];
      }
    }
    return null;
  }
}

class ViteManager {
  constructor(options) {
    this.options = options;
    this.lastPingTimestamp = 0;
    const router = express.Router();
    router.get("/preview/", async (req, res) => {
      await this.viteStartupPromise;
      const template = await fs$1.readFile(
        this.options.shadowHtmlFilePath,
        "utf-8"
      );
      if (!this.viteServer) {
        res.status(404).end(`Vite is not running.`);
        return;
      }
      res.status(200).set({ "Content-Type": "text/html" }).end(
        await this.viteServer.transformIndexHtml(req.originalUrl, template)
      );
    });
    router.use("/ping", async (req, res) => {
      this.lastPingTimestamp = Date.now();
      res.json(
        JSON.stringify({
          pong: "match!"
        })
      );
    });
    router.use(async (req, res, next) => {
      const waitSeconds = 60;
      const waitUntil = Date.now() + waitSeconds * 1e3;
      while (!this.viteServer) {
        await new Promise((resolve) => setTimeout(resolve, 50));
        if (Date.now() > waitUntil) {
          throw new Error(
            `Vite server is not running after ${waitSeconds} seconds.`
          );
        }
      }
      this.viteServer.middlewares(req, res, next);
    });
    this.middleware = router;
  }
  async start(server, port) {
    const typeScriptConfigFilePaths = await recrawl.recrawl({
      only: ["jsconfig.json", "tsconfig.json"],
      skip: ["node_modules", ".git"]
    })(this.options.rootDirPath);
    const validTypeScriptFilePaths = [];
    for (const configFilePath of typeScriptConfigFilePaths) {
      try {
        tsconfigLoader_js.loadTsconfig(configFilePath);
        validTypeScriptFilePaths.push(configFilePath);
      } catch (e) {
        console.warn(
          `Encountered an invalid config file, ignoring: ${configFilePath}`
        );
      }
    }
    const tsInferredAlias = {};
    const config = tsconfigLoader_js.loadTsconfig(
      typeScriptConfigFilePaths.includes("tsconfig.json") ? "tsconfig.json" : "jsconfig.json"
    );
    if (config?.compilerOptions?.baseUrl && config?.compilerOptions?.paths) {
      const { baseUrl, paths } = config.compilerOptions;
      for (const [match, mapping] of Object.entries(paths)) {
        const firstMapping = mapping[0];
        if (!firstMapping) {
          continue;
        }
        const matchNoWildcard = match.endsWith("/*") ? match.slice(0, match.length - 2) : match;
        const firstMappingNoWildcard = firstMapping.endsWith("/*") ? firstMapping.slice(0, firstMapping.length - 2) : firstMapping;
        tsInferredAlias[matchNoWildcard] = path.join(
          this.options.rootDirPath,
          baseUrl,
          firstMappingNoWildcard
        );
      }
    }
    const existingViteConfig = await vite__namespace.loadConfigFromFile(
      {
        command: "serve",
        mode: "development"
      },
      void 0,
      this.options.rootDirPath
    );
    const alias = {
      ...tsInferredAlias,
      ...existingViteConfig?.config.resolve?.alias,
      ...this.options.config.alias,
      ...this.options.config.vite?.resolve?.alias
    };
    const defaultLogger = vite__namespace.createLogger(this.options.logLevel);
    const frameworkPluginViteConfig = this.options.frameworkPlugin.viteConfig({
      ...this.options.config,
      alias
    });
    const projectVitePlugins = await excludePlugins(
      new Set(this.options.frameworkPlugin.incompatibleVitePlugins),
      [
        ...existingViteConfig?.config.plugins || [],
        ...this.options.config.vite?.plugins || []
      ]
    );
    const frameworkVitePlugins = await excludePlugins(
      await extractPluginNames(projectVitePlugins),
      frameworkPluginViteConfig.plugins || []
    );
    const vitePlugins = [
      viteTsconfigPaths.default({
        root: this.options.rootDirPath,
        projects: validTypeScriptFilePaths
      }),
      virtualPlugin({
        reader: this.options.reader,
        rootDirPath: this.options.rootDirPath,
        allowedAbsolutePaths: this.options.config.vite?.server?.fs?.allow || [
          vite.searchForWorkspaceRoot(this.options.rootDirPath)
        ],
        moduleGraph: () => this.viteServer?.moduleGraph || null,
        esbuildOptions: frameworkPluginViteConfig.esbuild || {}
      }),
      exportToplevelPlugin(),
      fakeExportedTypesPlugin({
        readFile: (absoluteFilePath) => this.options.reader.read(absoluteFilePath).then((entry) => {
          if (entry?.kind !== "file") {
            return null;
          }
          return entry.read();
        })
      }),
      cssModulesWithoutSuffixPlugin(),
      componentLoaderPlugin(this.options),
      projectVitePlugins,
      frameworkVitePlugins
    ];
    const plugins = await Promise.all(
      vitePlugins.flat().map(async (pluginOrPromise) => {
        const plugin = await pluginOrPromise;
        if (!plugin || Array.isArray(plugin) || !plugin.handleHotUpdate) {
          return plugin;
        }
        const handleHotUpdate = typeof plugin.handleHotUpdate === "function" ? plugin.handleHotUpdate : plugin.handleHotUpdate.handler;
        return {
          ...plugin,
          handleHotUpdate: async (ctx) => {
            return handleHotUpdate({
              ...ctx,
              read: async () => {
                const entry = await this.options.reader.read(ctx.file);
                if (entry?.kind !== "file") {
                  return ctx.read();
                }
                return entry.read();
              }
            });
          }
        };
      })
    );
    const viteServerPromise = vite__namespace.createServer({
      ...existingViteConfig?.config,
      ...frameworkPluginViteConfig,
      ...existingViteConfig?.config,
      ...this.options.config.vite,
      configFile: false,
      root: this.options.rootDirPath,
      base: "/preview/",
      server: {
        ...existingViteConfig?.config.server,
        middlewareMode: true,
        hmr: {
          overlay: false,
          server,
          clientPort: port,
          ...typeof this.options.config.vite?.server?.hmr === "object" ? this.options.config.vite?.server?.hmr : {}
        },
        fs: {
          strict: false,
          ...this.options.config.vite?.server?.fs || {}
        },
        ...this.options.config.vite?.server
      },
      customLogger: {
        info: defaultLogger.info,
        warn: defaultLogger.warn,
        error: defaultLogger.error,
        warnOnce: defaultLogger.warnOnce,
        clearScreen: () => {
        },
        hasWarned: defaultLogger.hasWarned,
        hasErrorLogged: defaultLogger.hasErrorLogged
      },
      clearScreen: false,
      cacheDir: this.options.config.vite?.cacheDir || existingViteConfig?.config.cacheDir || this.options.cacheDir,
      publicDir: this.options.config.vite?.publicDir || existingViteConfig?.config.publicDir || frameworkPluginViteConfig.publicDir || this.options.config.publicDir,
      plugins,
      define: {
        ...existingViteConfig?.config.define,
        __filename: void 0,
        __dirname: void 0,
        ...frameworkPluginViteConfig.define,
        ...existingViteConfig?.config.define,
        ...this.options.config.vite?.define
      },
      resolve: {
        ...existingViteConfig?.config.resolve,
        ...this.options.config.vite?.resolve,
        alias: {
          "~": "",
          "@": "",
          ...alias,
          ...existingViteConfig?.config.resolve?.alias,
          ...frameworkPluginViteConfig.resolve?.alias
        }
      }
    });
    this.viteStartupPromise = new Promise((resolve) => {
      viteServerPromise.finally(() => {
        resolve();
        delete this.viteStartupPromise;
      });
    });
    this.viteServer = await viteServerPromise;
    this.viteServer.watcher.addListener("change", (path2) => {
      this.viteServer?.ws.send({
        type: "custom",
        event: "previewjs-file-changed",
        data: {
          path: path2
        }
      });
    });
  }
  async stop() {
    if (this.viteServer) {
      await this.viteServer.close();
      delete this.viteServer;
    }
  }
  getConfig() {
    return this.options.config;
  }
  getLastPingTimestamp() {
    return this.lastPingTimestamp;
  }
  triggerReload(absoluteFilePath) {
    if (!this.viteServer) {
      return;
    }
    for (const onChange of this.viteServer.watcher.listeners("change")) {
      onChange(absoluteFilePath);
    }
  }
  triggerFullReload() {
    this.viteServer?.moduleGraph.invalidateAll();
    this.viteServer?.ws.send({
      type: "full-reload"
    });
  }
}
async function extractPluginNames(pluginOptions) {
  const names = /* @__PURE__ */ new Set();
  for (const pluginOption of await Promise.all(pluginOptions)) {
    if (!pluginOption) {
      continue;
    }
    if (Array.isArray(pluginOption)) {
      for (const name of await extractPluginNames(pluginOption)) {
        names.add(name);
      }
    } else {
      names.add(pluginOption.name);
    }
  }
  return names;
}
async function excludePlugins(excludePluginNames, pluginOptions) {
  const plugins = [];
  for (const pluginOption of await Promise.all(pluginOptions)) {
    if (!pluginOption) {
      continue;
    }
    if (Array.isArray(pluginOption)) {
      plugins.push(...await excludePlugins(excludePluginNames, pluginOption));
    } else if (!excludePluginNames.has(pluginOption.name)) {
      plugins.push(pluginOption);
    }
  }
  return plugins;
}

const POSTCSS_CONFIG_FILE = [
  ".postcssrc",
  ".postcssrc.json",
  ".postcssrc.yml",
  ".postcssrc.js",
  ".postcssrc.mjs",
  ".postcssrc.cjs",
  ".postcssrc.ts",
  "postcss.config.js",
  "postcss.config.mjs",
  "postcss.config.cjs",
  "postcss.config.ts"
];
const GLOBAL_CSS_FILE_NAMES_WITHOUT_EXT = [
  "index",
  "global",
  "globals",
  "style",
  "styles",
  "app"
];
const GLOBAL_CSS_EXTS = ["css", "sass", "scss", "less", "styl", "stylus"];
const GLOBAL_CSS_FILE = GLOBAL_CSS_FILE_NAMES_WITHOUT_EXT.flatMap(
  (fileName) => GLOBAL_CSS_EXTS.map((ext) => `${fileName}.${ext}`)
);
const FILES_REQUIRING_RESTART = /* @__PURE__ */ new Set([
  config.PREVIEW_CONFIG_NAME,
  "jsconfig.json",
  "tsconfig.json",
  "package.json",
  "package-lock.json",
  "pnpm-lock.yaml",
  "yarn.lock",
  ...POSTCSS_CONFIG_FILE,
  ...GLOBAL_CSS_FILE
]);
const SHUTDOWN_CHECK_INTERVAL = 3e3;
const SHUTDOWN_AFTER_INACTIVITY = 1e4;
class Previewer {
  constructor(options) {
    this.options = options;
    this.appServer = null;
    this.viteManager = null;
    this.status = { kind: "stopped" };
    this.shutdownCheckInterval = null;
    this.disposeObserver = null;
    this.config = null;
    this.onFileChangeListener = {
      onChange: (absoluteFilePath, info) => {
        absoluteFilePath = path.resolve(absoluteFilePath);
        if (!info.virtual && this.config?.wrapper && absoluteFilePath === path.resolve(this.options.rootDirPath, this.config.wrapper.path)) {
          this.viteManager?.triggerFullReload();
        }
        if (!info.virtual && FILES_REQUIRING_RESTART.has(path.basename(absoluteFilePath))) {
          if (this.status.kind === "starting" || this.status.kind === "started") {
            const port = this.status.port;
            console.log("New dependencies were detected. Restarting...");
            this.stop().then(async () => {
              await this.start(async () => port);
            }).catch(console.error);
          }
          return;
        }
        if (info.virtual) {
          this.viteManager?.triggerReload(absoluteFilePath);
          this.viteManager?.triggerReload(absoluteFilePath + ".ts");
        } else if (this.options.onFileChanged) {
          this.options.onFileChanged(absoluteFilePath);
        }
      }
    };
    this.transformingReader = vfs.createStackedReader([
      options.reader,
      vfs.createFileSystemReader({
        mapping: {
          from: options.frameworkPlugin.previewDirPath,
          to: path.join(
            options.rootDirPath,
            "__previewjs_internal__",
            "renderer"
          )
        },
        watch: false
      }),
      vfs.createFileSystemReader({
        mapping: {
          from: options.previewDirPath,
          to: options.rootDirPath
        },
        watch: false
      })
    ]);
  }
  async start(allocatePort) {
    let port;
    const statusBeforeStart = this.status;
    switch (statusBeforeStart.kind) {
      case "starting":
        port = statusBeforeStart.port;
        try {
          await statusBeforeStart.promise;
        } catch (e) {
          console.error(e);
          this.status = {
            kind: "stopped"
          };
          await this.startFromStopped(port);
        }
        break;
      case "started":
        if (this.shutdownCheckInterval) {
          clearInterval(this.shutdownCheckInterval);
          this.shutdownCheckInterval = null;
        }
        port = statusBeforeStart.port;
        break;
      case "stopping":
        port = statusBeforeStart.port;
        try {
          await statusBeforeStart.promise;
        } catch (e) {
          console.error(e);
          this.status = {
            kind: "stopped"
          };
        }
        await this.start(async () => port);
        break;
      case "stopped":
        port = await allocatePort();
        await this.startFromStopped(port);
        break;
      default:
        throw assertNever(statusBeforeStart);
    }
    return port;
  }
  async startFromStopped(port) {
    this.status = {
      kind: "starting",
      port,
      promise: (async () => {
        process.chdir(this.options.rootDirPath);
        const config$1 = await config.readConfig(this.options.rootDirPath);
        this.config = {
          ...config$1,
          wrapper: config$1.wrapper || {
            path: this.options.frameworkPlugin.defaultWrapperPath
          }
        };
        const globalCssAbsoluteFilePaths = await findFiles(
          this.options.rootDirPath,
          `**/@(${GLOBAL_CSS_FILE_NAMES_WITHOUT_EXT.join(
            "|"
          )}).@(${GLOBAL_CSS_EXTS.join("|")})`
        );
        this.appServer = new Server({
          middlewares: [
            ...this.options.middlewares || [],
            (req, res, next) => {
              this.viteManager?.middleware(req, res, next);
            }
          ]
        });
        if (this.transformingReader.observe) {
          this.disposeObserver = await this.transformingReader.observe(
            this.options.rootDirPath
          );
        }
        this.transformingReader.listeners.add(this.onFileChangeListener);
        this.viteManager = new ViteManager({
          rootDirPath: this.options.rootDirPath,
          shadowHtmlFilePath: path.join(
            this.options.previewDirPath,
            "index.html"
          ),
          detectedGlobalCssFilePaths: globalCssAbsoluteFilePaths.map(
            (absoluteFilePath) => path.relative(this.options.rootDirPath, absoluteFilePath)
          ),
          reader: this.transformingReader,
          cacheDir: path.join(getCacheDir(this.options.rootDirPath), "vite"),
          config: this.config,
          logLevel: this.options.logLevel,
          frameworkPlugin: this.options.frameworkPlugin
        });
        const server = await this.appServer.start(port);
        await this.viteManager.start(server, port);
        this.status = {
          kind: "started",
          port
        };
      })()
    };
    await this.status.promise;
  }
  async stop(options = {}) {
    if (this.status.kind === "starting") {
      await this.status.promise;
    }
    if (this.status.kind !== "started") {
      return;
    }
    if (options.onceUnused) {
      if (!await this.shutdownCheck()) {
        this.shutdownCheckInterval = setInterval(() => {
          this.shutdownCheck().catch(console.error);
        }, SHUTDOWN_CHECK_INTERVAL);
      }
    } else {
      await this.stopNow();
    }
  }
  async shutdownCheck() {
    const lastPingTimestamp = this.viteManager?.getLastPingTimestamp() || 0;
    if (lastPingTimestamp + SHUTDOWN_AFTER_INACTIVITY > Date.now()) {
      return false;
    }
    if (this.shutdownCheckInterval) {
      clearInterval(this.shutdownCheckInterval);
      this.shutdownCheckInterval = null;
    }
    await this.stopNow();
    return true;
  }
  async stopNow() {
    if (this.status.kind === "starting") {
      await this.status.promise;
    }
    if (this.status.kind !== "started") {
      return;
    }
    this.transformingReader.listeners.remove(this.onFileChangeListener);
    this.status = {
      kind: "stopping",
      port: this.status.port,
      promise: (async () => {
        if (this.disposeObserver) {
          await this.disposeObserver();
          this.disposeObserver = null;
        }
        if (this.viteManager) {
          await this.viteManager.stop();
          this.viteManager = null;
        }
        if (this.appServer) {
          await this.appServer.stop();
          this.appServer = null;
        }
        this.status = {
          kind: "stopped"
        };
      })()
    };
    await this.status.promise;
  }
}

class ApiRouter {
  constructor() {
    this.handlers = /* @__PURE__ */ new Map();
    this.registerEndpoint = (endpoint, handler) => {
      if (this.handlers.has(endpoint.path)) {
        throw new Error(
          `Multiple handlers registered for endpoint '${endpoint.path}'`
        );
      }
      this.handlers.set(endpoint.path, handler);
    };
  }
  async handle(path, request, expressRequest, expressResponse) {
    const handler = this.handlers.get(path);
    if (!handler) {
      return {
        kind: "error",
        message: `No handler is registered for path '${path}'`
      };
    }
    try {
      const response = await handler(request, expressRequest, expressResponse);
      return {
        kind: "success",
        response
      };
    } catch (e) {
      console.error(`Handler ${path} failed`, e);
      return {
        kind: "error",
        message: e.message
      };
    }
  }
}

function generateComponentId(options) {
  return `${options.currentFilePath.replace(/\\/g, "/")}:${options.siblingFileName ? `${options.siblingFileName}/` : ""}${options.name}`;
}

async function loadPreviewEnv({
  rootDirPath,
  setupEnvironment,
  frameworkPluginFactories
}) {
  const dependencies = await extractPackageDependencies(rootDirPath);
  for (const candidate of frameworkPluginFactories || []) {
    if (await candidate.isCompatible(dependencies)) {
      return {
        previewEnv: await setupEnvironment({ rootDirPath }),
        frameworkPlugin: await candidate.create()
      };
    }
  }
  return null;
}
async function extractPackageDependencies(rootDirPath) {
  const packageJsonPath = path.join(rootDirPath, "package.json");
  if (!await fs$1.pathExists(packageJsonPath)) {
    return {};
  }
  const { dependencies, devDependencies, peerDependencies } = JSON.parse(
    await fs$1.readFile(packageJsonPath, "utf8")
  );
  const allDependencies = {
    ...dependencies,
    ...devDependencies,
    ...peerDependencies
  };
  return Object.fromEntries(
    Object.entries(allDependencies).map(
      ([name, version]) => {
        let majorVersion;
        if (typeof version !== "string") {
          majorVersion = 0;
        } else if (version.startsWith("^") || version.startsWith("~")) {
          majorVersion = parseInt(version.slice(1));
        } else {
          majorVersion = parseInt(version);
        }
        const readInstalledVersion = async () => {
          try {
            const moduleEntryPath = require.resolve(name, {
              paths: [rootDirPath]
            });
            let packagePath = moduleEntryPath;
            let packageJsonPath2 = null;
            while (packagePath !== path.dirname(packagePath)) {
              const candidatePackageJsonPath = path.join(
                packagePath,
                "package.json"
              );
              if (fs$1.existsSync(candidatePackageJsonPath)) {
                packageJsonPath2 = candidatePackageJsonPath;
                break;
              }
              packagePath = path.dirname(packagePath);
            }
            if (!packageJsonPath2) {
              throw new Error(
                `No package.json path found from: ${moduleEntryPath}`
              );
            }
            const packageInfo = JSON.parse(
              await fs$1.readFile(packageJsonPath2, "utf8")
            );
            const version2 = packageInfo["version"];
            if (!version2 || typeof version2 !== "string") {
              throw new Error(
                `Invalid version found for package: ${packageJsonPath2}`
              );
            }
            return version2;
          } catch (e) {
            console.error(
              `Unable to read installed version of package: ${name}`,
              e
            );
            return null;
          }
        };
        return [name, { majorVersion, readInstalledVersion }];
      }
    )
  );
}

async function createWorkspace({
  versionCode,
  rootDirPath,
  reader,
  frameworkPlugin,
  logLevel,
  middlewares,
  onReady,
  persistedStateManager = new LocalFilePersistedStateManager()
}) {
  const expectedPluginApiVersion = 3;
  if (!frameworkPlugin.pluginApiVersion || frameworkPlugin.pluginApiVersion < expectedPluginApiVersion) {
    throw new Error(
      `Preview.js framework plugin ${frameworkPlugin.name} is incompatible with this version of Preview.js. Please upgrade it.`
    );
  } else if (frameworkPlugin.pluginApiVersion > expectedPluginApiVersion) {
    throw new Error(
      `Preview.js framework plugin ${frameworkPlugin.name} is too recent. Please upgrade Preview.js or use an older version of ${frameworkPlugin.name}.`
    );
  }
  if (frameworkPlugin.transformReader) {
    reader = frameworkPlugin.transformReader(reader, rootDirPath);
  }
  const collected = {};
  const typeAnalyzer$1 = typeAnalyzer.createTypeAnalyzer({
    reader,
    rootDirPath,
    collected,
    specialTypes: frameworkPlugin.specialTypes,
    tsCompilerOptions: frameworkPlugin.tsCompilerOptions
  });
  const router = new ApiRouter();
  router.registerEndpoint(api.localEndpoints.GetInfo, async () => {
    const separatorPosition = versionCode.indexOf("-");
    if (separatorPosition === -1) {
      throw new Error(`Unsupported version code format: ${versionCode}`);
    }
    const platform = versionCode.substr(0, separatorPosition);
    const version = versionCode.substr(separatorPosition + 1);
    return {
      appInfo: {
        platform,
        version
      }
    };
  });
  router.registerEndpoint(api.localEndpoints.GetState, persistedStateManager.get);
  router.registerEndpoint(
    api.localEndpoints.UpdateState,
    persistedStateManager.update
  );
  router.registerEndpoint(
    api.localEndpoints.ComputeProps,
    async ({ filePath, componentName }) => {
      const component = (await frameworkPlugin.detectComponents(typeAnalyzer$1, [
        path.join(rootDirPath, filePath)
      ])).find((c) => c.name === componentName);
      if (!component) {
        return {
          types: {
            props: typeAnalyzer.UNKNOWN_TYPE,
            all: {}
          }
        };
      }
      if (component.info.kind === "story") {
        return {
          types: {
            props: typeAnalyzer.EMPTY_OBJECT_TYPE,
            all: {}
          }
        };
      }
      const result = await component.info.analyze();
      return {
        types: {
          props: result.propsType,
          all: result.types
        }
      };
    }
  );
  router.registerEndpoint(
    api.localEndpoints.AnalyzeProject,
    (options) => analyzeProject(workspace, options)
  );
  const previewer = new Previewer({
    reader,
    rootDirPath,
    previewDirPath: path.join(
      path.dirname(path.dirname(require.resolve("@previewjs/iframe"))),
      "preview"
    ),
    frameworkPlugin,
    logLevel,
    middlewares: [
      express.json(),
      cookieParser(),
      express.Router().use(
        "/monaco-editor",
        express.static(path.join(__dirname, "monaco-editor"))
      ),
      async (req, res, next) => {
        if (req.path.startsWith("/api/")) {
          res.json(await router.handle(req.path.substr(5), req.body, req, res));
        } else {
          next();
        }
      },
      ...middlewares
    ],
    onFileChanged: (absoluteFilePath) => {
      const filePath = path.relative(rootDirPath, absoluteFilePath);
      for (const name of Object.keys(collected)) {
        if (name.startsWith(`${filePath}:`)) {
          delete collected[name];
        }
      }
    }
  });
  const workspace = {
    rootDirPath,
    reader,
    typeAnalyzer: typeAnalyzer$1,
    frameworkPlugin,
    preview: {
      start: async (allocatePort) => {
        const port = await previewer.start(async () => {
          const port2 = allocatePort ? await allocatePort() : 0;
          return port2 || await getPort({
            port: getPort.makeRange(3140, 4e3)
          });
        });
        return {
          url: () => `http://localhost:${port}`,
          stop: async (options) => {
            await previewer.stop(options);
          }
        };
      }
    },
    dispose: async () => {
      typeAnalyzer$1.dispose();
    }
  };
  if (onReady) {
    await onReady({
      registerEndpoint: (endpoint, handler) => router.registerEndpoint(endpoint, handler),
      workspace
    });
  }
  return workspace;
}
function findWorkspaceRoot(absoluteFilePath) {
  let dirPath = path.resolve(absoluteFilePath);
  while (dirPath !== path.dirname(dirPath)) {
    if (fs$1.existsSync(path.join(dirPath, "package.json")) && fs$1.existsSync(path.join(dirPath, "node_modules"))) {
      return dirPath;
    }
    dirPath = path.dirname(dirPath);
  }
  return null;
}

exports.createWorkspace = createWorkspace;
exports.findWorkspaceRoot = findWorkspaceRoot;
exports.generateComponentId = generateComponentId;
exports.loadPreviewEnv = loadPreviewEnv;
