'use strict';

const path = require('node:path');
const node_buffer = require('node:buffer');
const promises = require('node:fs/promises');
const process = require('node:process');
const findUp = require('find-up');
const vite = require('vite');
const MagicString = require('magic-string');
const compilerDom = require('@vue/compiler-dom');
const core = require('@babel/core');
const vueJsxPlugin = require('@vue/babel-plugin-jsx');
const typescriptPlugin = require('@babel/plugin-transform-typescript');
const importMeta = require('@babel/plugin-syntax-import-meta');
const decoratorsPlugin = require('@babel/plugin-proposal-decorators');
const importAttributesPlugin = require('@babel/plugin-syntax-import-attributes');

function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e.default : e; }

function _interopNamespaceCompat(e) {
  if (e && typeof e === 'object' && 'default' in e) return e;
  const n = Object.create(null);
  if (e) {
    for (const k in e) {
      n[k] = e[k];
    }
  }
  n.default = e;
  return n;
}

const path__default = /*#__PURE__*/_interopDefaultCompat(path);
const process__namespace = /*#__PURE__*/_interopNamespaceCompat(process);
const MagicString__default = /*#__PURE__*/_interopDefaultCompat(MagicString);
const vueJsxPlugin__default = /*#__PURE__*/_interopDefaultCompat(vueJsxPlugin);
const typescriptPlugin__default = /*#__PURE__*/_interopDefaultCompat(typescriptPlugin);
const importMeta__default = /*#__PURE__*/_interopDefaultCompat(importMeta);
const decoratorsPlugin__default = /*#__PURE__*/_interopDefaultCompat(decoratorsPlugin);
const importAttributesPlugin__default = /*#__PURE__*/_interopDefaultCompat(importAttributesPlugin);

const PROFILE_PATH = "node_modules/@guiiai/core/.cache/profiles";
async function exists(path) {
  try {
    await promises.stat(path);
    return true;
  } catch (error) {
    if (!(error instanceof Error)) {
      throw error;
    }
    if (!("code" in error)) {
      throw error;
    }
    if (error.code !== "ENOENT") {
      throw error;
    }
    return false;
  }
}
async function readUserConfig(config) {
  const profilesDir = vite.normalizePath(path.join(config.root, PROFILE_PATH));
  const profileExist = await exists(profilesDir).catch(() => false);
  if (!profileExist) {
    return {
      isSignedIn: false
    };
  }
  const profile = await promises.readdir(profilesDir);
  const profileId = profile[0];
  const profileFile = vite.normalizePath(path.join(profilesDir, profileId, "profile.json"));
  const profileContent = await promises.readFile(profileFile, "utf-8");
  const profileData = JSON.parse(profileContent);
  return {
    isSignedIn: true,
    profile: profileData
  };
}
async function writeUserConfig(config, profile) {
  await clearUserConfig(config);
  const profilePath = vite.normalizePath(path.join(config.root, PROFILE_PATH, profile.id));
  await promises.mkdir(profilePath, { recursive: true });
  const profileFile = vite.normalizePath(path.join(profilePath, "profile.json"));
  await promises.writeFile(profileFile, JSON.stringify(profile, null, 2));
}
async function clearUserConfig(config) {
  const profilePath = vite.normalizePath(path.join(config.root, PROFILE_PATH));
  await promises.rm(profilePath, { recursive: true, force: true });
}

const EL_DATA_KEY_CODEMAP = "data-guii-codemap";
const EL_PROPS_KEY_CODEMAP = "__guii_codemap";
const CLIENT_VIRTUAL_MODULE_ID = "virtual:vite-plugin-guii-devtool";
const EL_ID_DEVTOOLS_HOLDER = "guii-devtools";
const CLIENT_CALLBACK_TO_URL = "__guii/devtools/auth/callback";

const ResolvedVirtualModuleId = `\0${CLIENT_VIRTUAL_MODULE_ID}`;
async function detectRootPackageInfo() {
  const startingPackageJsonPath = await findUp.findUp("package.json");
  if (!startingPackageJsonPath) {
    throw new Error("No package.json found in the starting directory");
  }
  let currentDir = path.dirname(startingPackageJsonPath);
  while (currentDir) {
    const parentPackageJsonPath = await findUp.findUp("package.json", { cwd: path.resolve(currentDir, "..") });
    if (!parentPackageJsonPath) {
      break;
    }
    currentDir = path.dirname(parentPackageJsonPath);
  }
  const packageJSONContent = await promises.readFile(startingPackageJsonPath, "utf8");
  const rootPackageJson = JSON.parse(packageJSONContent.toString());
  const rootPackageName = rootPackageJson.name;
  const rootDir = path.dirname(startingPackageJsonPath);
  return { rootPackageName, rootDir };
}
function createClientPlugin(pluginOptions = {}) {
  if (!pluginOptions)
    pluginOptions = {};
  let config;
  let projectTitle;
  return [
    {
      name: "@guiiai/vite-devtools-client",
      apply(_, { command }) {
        return command === "serve" && process.env.NODE_ENV !== "test";
      },
      async configResolved(_config) {
        config = _config;
        try {
          const { rootPackageName } = await detectRootPackageInfo();
          projectTitle = rootPackageName;
        } catch (err) {
          console.error(`[@guiiai/vite-devtools-previewer] failed to detect root package info: ${err}`);
          projectTitle = path.basename(_config.root);
        }
      },
      resolveId(id) {
        if (id === CLIENT_VIRTUAL_MODULE_ID)
          return ResolvedVirtualModuleId;
      },
      load(id) {
        if (id !== ResolvedVirtualModuleId)
          return null;
        return `
          /* begin injected by @guiiai/vite-devtools-client */
          import { createDevToolsInstance, DevTools } from '@guiiai/core/client'
          import '@guiiai/core/client/style.css'

          const devToolInstance = createDevToolsInstance()

          let isMounted = false
          function mount() {
            if (isMounted) {
              console.warn('Guii.AI DevTools is already mounted. the mount function has been called repeatedly. skipping...')
              return
            }
            devToolInstance.mount('#${EL_ID_DEVTOOLS_HOLDER}')
          }

          ${pluginOptions.immediatelyMount ? "mount()" : "globalThis.mountGuiiDevtools = mount"}
          /* end injected by @guiiai/vite-devtools-client */
        `;
      },
      transform(code, id, transformOptions) {
        if (transformOptions?.ssr)
          return;
        const { appendTo } = pluginOptions;
        const [filename] = id.split("?", 2);
        if (appendTo && (typeof appendTo === "string" && filename.endsWith(appendTo) || appendTo instanceof RegExp && appendTo.test(filename))) {
          const injectImport = `import '${CLIENT_VIRTUAL_MODULE_ID}';`;
          if (!code.includes(injectImport.trim())) {
            code = `${injectImport}
${code}`;
          }
        }
        return code;
      },
      transformIndexHtml(html) {
        const injection = `
        <div id="${EL_ID_DEVTOOLS_HOLDER}"></div>
        `;
        html = html.replace("<body>", `<body>${injection}`);
        const tags = [];
        if (!pluginOptions.appendTo) {
          tags.push({
            tag: "script",
            injectTo: "head-prepend",
            attrs: {
              type: "module",
              scr: `${config.base || "/"}@id/${CLIENT_VIRTUAL_MODULE_ID}`
            }
          });
        }
        return {
          html,
          tags
        };
      },
      configureServer(server) {
        server.middlewares.use(async (req, res, next) => {
          if (!req.url) {
            return next();
          }
          const parsedUrl = new URL(req.url, `http://${req.headers.host}`);
          if (!parsedUrl.pathname.endsWith(`/${CLIENT_CALLBACK_TO_URL}`)) {
            return next();
          }
          const token = parsedUrl.searchParams.get("code");
          if (!token) {
            return next();
          }
          const id = parsedUrl.searchParams.get("id");
          if (!id) {
            return next();
          }
          const body = "<!DOCTYPE html><html><body><script>window.close()<\/script></body></html>";
          res.writeHead(200, {
            "content-length": node_buffer.Buffer.byteLength(body),
            "content-type": "text/html"
          }).end(body);
          await writeUserConfig(config, {
            id,
            token
          });
          server.ws.send({
            type: "custom",
            event: "@guiiai/vite-devtools-client:get-config:response",
            data: {
              isSignedIn: true,
              profile: {
                id,
                token,
                projectTitle
              }
            }
          });
        });
        server.ws.on("@guiiai/vite-devtools-client:get-config", async () => {
          const result = await readUserConfig(config);
          server.ws.send({
            type: "custom",
            event: "@guiiai/vite-devtools-client:get-config:response",
            data: result
          });
        });
        server.ws.on("@guiiai/vite-devtools-client:get-project", async () => {
          server.ws.send({
            type: "custom",
            event: "@guiiai/vite-devtools-client:get-project:response",
            data: {
              project: {
                title: projectTitle
              }
            }
          });
        });
        server.ws.on("@guiiai/vite-devtools-client:save-config", async (data) => {
          await writeUserConfig(config, data);
          server.ws.send({
            type: "custom",
            event: "@guiiai/vite-devtools-client:save-config:response",
            data: true
          });
        });
        server.ws.on("@guiiai/vite-devtools-client:clear-config", async () => {
          await clearUserConfig(config);
          server.ws.send({
            type: "custom",
            event: "@guiiai/vite-devtools-client:clear-config:response",
            data: true
          });
        });
      }
    }
  ];
}

const hasOwnProperty = Object.prototype.hasOwnProperty;
function hasOwn(val, key) {
  return hasOwnProperty.call(val, key);
}

const EXCLUDE_TAG = ["template", "script", "style"];
async function compileSFCTemplate({ code, id, type }) {
  const s = new MagicString__default(code);
  const relativePath = vite.normalizePath(path__default.relative(process__namespace.cwd(), id));
  const result = await new Promise((resolve) => {
    switch (type) {
      case "template": {
        const ast = compilerDom.parse(code, { comments: true });
        compilerDom.transform(ast, {
          nodeTransforms: [
            (node) => {
              if (node.type === 1) {
                if (node.tagType === 0 && !EXCLUDE_TAG.includes(node.tag)) {
                  if (node.loc.source.includes(EL_DATA_KEY_CODEMAP))
                    return;
                  const insertPosition = node.props.length ? Math.max(...node.props.map((i) => i.loc.end.offset)) : node.loc.start.offset + node.tag.length + 1;
                  const { line, column } = node.loc.start;
                  const content = ` ${EL_DATA_KEY_CODEMAP}="${relativePath}:${line}:${column}"`;
                  s.prependLeft(
                    insertPosition,
                    content
                  );
                }
              }
            }
          ]
        });
        break;
      }
      case "jsx": {
        const ast = core.parse(code, {
          babelrc: false,
          configFile: false,
          comments: true,
          plugins: [
            importMeta__default,
            [vueJsxPlugin__default, {}],
            [
              typescriptPlugin__default,
              { isTSX: true, allowExtensions: true }
            ],
            [
              decoratorsPlugin__default,
              { legacy: true }
            ],
            [
              importAttributesPlugin__default,
              { deprecatedAssertSyntax: true }
            ]
          ]
        });
        core.traverse(ast, {
          enter({ node }) {
            if (node.type === "JSXElement") {
              if (node.openingElement.attributes.some(
                (attr) => attr.type !== "JSXSpreadAttribute" && attr.name.name === EL_DATA_KEY_CODEMAP
              )) {
                return;
              }
              const insertPosition = node.openingElement.end - (node.openingElement.selfClosing ? 2 : 1);
              const { line, column } = node.loc.start;
              const content = ` ${EL_DATA_KEY_CODEMAP}="${relativePath}:${line}:${column}"`;
              s.prependLeft(
                insertPosition,
                content
              );
            }
          }
        });
        break;
      }
    }
    resolve(s.toString());
  });
  return result;
}
function parseVueRequest(id) {
  const [filename] = id.split("?", 2);
  const url = new URL(id, "http://domain.inspector");
  const query = Object.fromEntries(url.searchParams.entries());
  if (query.vue != null)
    query.vue = true;
  if (query.src != null)
    query.src = true;
  if (query.index != null)
    query.index = Number(query.index);
  if (query.raw != null)
    query.raw = true;
  if (hasOwn(query, "lang.tsx") || hasOwn(query, "lang.jsx"))
    query.isJsx = true;
  return {
    filename,
    query
  };
}

function createCodeMapPlugin(options) {
  const {
    vue,
    cleanHtml = vue === 3
    // Only enabled for Vue 3 by default
  } = options;
  return [
    // Pre: transform source map
    {
      name: "@guiiai/vite-devtools-codemap",
      enforce: "pre",
      apply(_, { command }) {
        return command === "serve" && process.env.NODE_ENV !== "test";
      },
      transform(code, id) {
        const { filename, query } = parseVueRequest(id);
        const isJsx = filename.endsWith(".jsx") || filename.endsWith(".tsx") || filename.endsWith(".vue") && query.isJsx;
        const isTpl = filename.endsWith(".vue") && query.type !== "style" && !query.raw;
        if (isJsx || isTpl)
          return compileSFCTemplate({ code, id: filename, type: isJsx ? "jsx" : "template" });
      }
    },
    // Post: Hidden data-v tag in Elements
    {
      name: "GuiiJS:CodeMap:Post",
      enforce: "post",
      apply(_, { command }) {
        return cleanHtml && vue === 3 && command === "serve" && process.env.NODE_ENV !== "test";
      },
      transform(code) {
        if (code.includes("_interopVNode"))
          return;
        if (!code.includes(EL_DATA_KEY_CODEMAP))
          return;
        const fn = /* @__PURE__ */ new Set();
        const s = new MagicString__default(code);
        s.replace(/(createElementVNode|createVNode|createElementBlock) as _\1,?/g, (_, name) => {
          fn.add(name);
          return "";
        });
        if (!fn.size)
          return;
        s.appendLeft(0, `/* Injection by vite-plugin-guii-devtools Start */
import { ${Array.from(fn.values()).map((i) => `${i} as __${i}`).join(",")} } from 'vue'
function _interopVNode(vnode) {
  if (vnode && vnode.props && '${EL_DATA_KEY_CODEMAP}' in vnode.props) {
    const data = vnode.props['${EL_DATA_KEY_CODEMAP}']
    delete vnode.props['${EL_DATA_KEY_CODEMAP}']
    Object.defineProperty(vnode.props, '${EL_PROPS_KEY_CODEMAP}', { value: data, enumerable: false })
  }
  return vnode
}
${Array.from(fn.values()).map((i) => `function _${i}(...args) { return _interopVNode(__${i}(...args)) }`).join("\n")}
/* Injection by vite-plugin-guii-devtools End */
`);
        return {
          code: s.toString(),
          map: s.generateMap({ hires: "boundary" })
        };
      }
    }
  ];
}

function isPreviewerProposeData(data) {
  if (!data || typeof data !== "object")
    return false;
  return typeof data.id === "string" && typeof data.code === "string";
}
function isPreviewerRejectData(data) {
  if (!data || typeof data !== "object")
    return false;
  return typeof data.id === "string";
}
function isPreviewerApproveData(data) {
  if (!data || typeof data !== "object")
    return false;
  return typeof data.id === "string";
}
function isPreviewerPrefetchData(data) {
  if (!data || typeof data !== "object")
    return false;
  return typeof data.id === "string";
}
function createPreviewerPlugin(_ = {}) {
  const proposedModules = /* @__PURE__ */ new Map();
  const proposingModules = /* @__PURE__ */ new Set();
  let config;
  return [
    {
      name: "@guiiai/@guiiai/vite-devtools-previewer",
      apply(_2, { command }) {
        return command === "serve" && process.env.NODE_ENV !== "test";
      },
      async configResolved(_config) {
        config = _config;
      },
      async handleHotUpdate(ctx) {
        const path$1 = path.relative(config.root, ctx.file);
        const module = proposedModules.get(path$1);
        if (!module) {
          return;
        }
        if (proposingModules.has(path$1)) {
          console.log(`[@guiiai/vite-devtools-previewer] file changed: ${path$1}, but it's being proposed, skipping`);
          return;
        }
        delete module.pendingUpdate;
        module.source = (await promises.readFile(ctx.file)).toString("utf-8");
        console.log(`[@guiiai/vite-devtools-previewer] file changed: ${path$1}, updated correspondingly`);
      },
      configureServer(server) {
        server.watcher.on("change", async (file) => {
          const path$1 = path.relative(config.root, file);
          const module = proposedModules.get(path$1);
          if (!module) {
            return;
          }
          if (proposingModules.has(path$1)) {
            console.log(`[@guiiai/vite-devtools-previewer] file changed: ${path$1}, but it's being proposed, skipping`);
            return;
          }
          delete module.pendingUpdate;
          module.source = (await promises.readFile(file)).toString("utf-8");
          console.log(`[@guiiai/vite-devtools-previewer] file changed: ${path$1}, updated correspondingly`);
        });
        server.watcher.on("unlink", (file) => {
          const path$1 = path.relative(config.root, file);
          proposedModules.delete(path$1);
          console.log(`[@guiiai/vite-devtools-previewer] file deleted: ${path$1}, removed from history`);
        });
        server.watcher.on("unlinkDir", (dir) => {
          const relativeDir = path.relative(config.root, dir);
          for (const key of proposedModules.keys()) {
            if (key.startsWith(relativeDir)) {
              proposedModules.delete(key);
            }
          }
          console.log(`[@guiiai/vite-devtools-previewer] directory deleted: ${relativeDir}, removed all files from history`);
        });
        server.ws.on("@guiiai/vite-devtools-previewer:prefetch", async (data) => {
          if (!isPreviewerPrefetchData(data)) {
            console.error(`[@guiiai/vite-devtools-previewer] invalid prefetch data: ${JSON.stringify(data)}`);
            return;
          }
          const absoluteId = path.join(config.root, data.id);
          const module = proposedModules.get(absoluteId);
          if (!module) {
            try {
              const content = (await promises.readFile(absoluteId)).toString();
              server.ws.send({
                type: "custom",
                event: "@guiiai/vite-devtools-previewer:prefetch:response",
                data: {
                  id: data.id,
                  code: content
                }
              });
              return;
            } catch (err) {
              server.ws.send({
                type: "custom",
                event: "@guiiai/vite-devtools-previewer:prefetch:response",
                data: {
                  id: data.id,
                  code: null,
                  error: String(err)
                }
              });
              return;
            }
          }
          server.ws.send({
            type: "custom",
            event: "@guiiai/vite-devtools-previewer:prefetch:response",
            data: {
              id: data.id,
              code: module.source
            }
          });
        });
        server.ws.on("@guiiai/vite-devtools-previewer:approve", async (data) => {
          if (!isPreviewerApproveData(data)) {
            console.error(`[@guiiai/vite-devtools-previewer] invalid approve data: ${JSON.stringify(data)}`);
            return;
          }
          const absoluteId = vite.normalizePath(path.join(config.root, data.id));
          const module = proposedModules.get(data.id);
          if (!module) {
            return;
          }
          const latestCode = module.pendingUpdate;
          if (!latestCode) {
            console.error(`[@guiiai/vite-devtools-previewer] no code to approve for ${data.id}, possible bug`);
            return;
          }
          delete module.pendingUpdate;
          proposedModules.delete(data.id);
          proposingModules.delete(data.id);
          await promises.writeFile(absoluteId, latestCode, "utf-8");
          console.log(`[@guiiai/vite-devtools-previewer] approved update for ${data.id}`);
        });
        server.ws.on("@guiiai/vite-devtools-previewer:reject", async (data) => {
          if (!isPreviewerRejectData(data)) {
            console.error(`[@guiiai/vite-devtools-previewer] invalid reject data: ${JSON.stringify(data)}`);
            return;
          }
          const absoluteId = vite.normalizePath(path.join(config.root, data.id));
          const module = proposedModules.get(data.id);
          if (!module) {
            return;
          }
          delete module.pendingUpdate;
          proposingModules.delete(data.id);
          proposedModules.delete(data.id);
          await promises.writeFile(absoluteId, data.code, "utf-8");
          console.log(`[@guiiai/vite-devtools-previewer] rejected update for ${data.id}`);
        });
        server.ws.on("@guiiai/vite-devtools-previewer:propose", async (data) => {
          if (!isPreviewerProposeData(data)) {
            console.error(`[@guiiai/vite-devtools-previewer] invalid propose data: ${JSON.stringify(data)}`);
            return;
          }
          const absoluteId = vite.normalizePath(path.join(config.root, data.id));
          const pendingUpdatedBundlerModule = server.moduleGraph.getModuleById(absoluteId);
          if (!pendingUpdatedBundlerModule) {
            console.error(`[@guiiai/vite-devtools-previewer] requested module not found: ${absoluteId}`);
            return;
          }
          if (!pendingUpdatedBundlerModule.file) {
            console.error(`[@guiiai/vite-devtools-previewer] requested module has no file: ${absoluteId}`);
            return;
          }
          let module = proposedModules.get(data.id);
          const source = (await promises.readFile(pendingUpdatedBundlerModule.file)).toString();
          module = {
            id: data.id,
            source,
            pendingUpdate: data.code
          };
          proposedModules.set(data.id, module);
          proposingModules.add(data.id);
          await promises.writeFile(absoluteId, data.code, "utf-8");
          console.log(`[@guiiai/vite-devtools-previewer] proposed update for ${data.id}`);
        });
      }
    }
  ];
}

const DEFAULT_GUII_AI_OPTIONS = {
  vue: 3,
  // enabled: false,
  // toggleComboKey: process.platform === 'darwin' ? 'meta-shift' : 'control-shift',
  // toggleButtonVisibility: 'active',
  // toggleButtonPos: 'top-right',
  appendTo: "",
  // lazyLoad: false,
  // launchEditor: process.env.LAUNCH_EDITOR ?? 'code',
  cleanHtml: true,
  immediatelyMount: true
};
function GuiiAIDevToolsVitePlugin(options = {}) {
  const normalizedOptions = { ...DEFAULT_GUII_AI_OPTIONS, ...options };
  const codeMapPlugin = createCodeMapPlugin(normalizedOptions);
  const clientPlugin = createClientPlugin(normalizedOptions);
  const previewerPlugin = createPreviewerPlugin(normalizedOptions);
  return [
    ...codeMapPlugin,
    ...clientPlugin,
    ...previewerPlugin
  ];
}

exports.DEFAULT_GUII_AI_OPTIONS = DEFAULT_GUII_AI_OPTIONS;
exports.GuiiAIDevToolsVitePlugin = GuiiAIDevToolsVitePlugin;
exports.defaultDevtoolsHolderId = EL_ID_DEVTOOLS_HOLDER;
//# sourceMappingURL=index.cjs.map
