UNPKG

one

Version:

One is a new React Framework that makes Vite serve both native and web.

222 lines (221 loc) 7.46 kB
import { spawn } from "node:child_process"; import path from "node:path"; import { parse } from "oxc-parser"; import { normalizePath } from "vite"; async function findJsxElements(code, filename) { const result = await parse(filename, code); if (result.errors.length > 0) { return []; } const locations = []; function getJsxName(node) { if (!node) return null; if (node.type === "JSXIdentifier") return node.name; if (node.type === "JSXMemberExpression") { const obj = getJsxName(node.object); return obj ? `${obj}.${node.property?.name}` : null; } return null; } function getLocation(offset) { const before = code.slice(0, offset); const lines = before.split("\n"); return { line: lines.length, column: lines[lines.length - 1].length + 1 }; } function walk(node) { if (!node || typeof node !== "object") return; if (node.type === "JSXOpeningElement" && node.name) { const tagName = getJsxName(node.name); if (tagName && tagName !== "Fragment" && !tagName.endsWith(".Fragment")) { const hasSourceAttr = node.attributes?.some(attr => attr.type === "JSXAttribute" && attr.name?.name === "data-one-source"); if (!hasSourceAttr) { const nameEnd = node.name.end; const loc = getLocation(node.start); locations.push({ insertOffset: nameEnd, line: loc.line, column: loc.column }); } } } for (const key of Object.keys(node)) { if (key === "parent") continue; const value = node[key]; if (Array.isArray(value)) { for (const child of value) { walk(child); } } else if (value && typeof value === "object") { walk(value); } } } walk(result.program); return locations.sort((a, b) => b.insertOffset - a.insertOffset); } async function injectSourceToJsx(code, id) { const [filePath] = id.split("?"); if (!filePath) return; const location = filePath.replace(normalizePath(process.cwd()), ""); if (!code.includes("<") || !code.includes(">")) { return; } const jsxLocations = await findJsxElements(code, filePath); if (jsxLocations.length === 0) { return; } let result = code; for (const jsx of jsxLocations) { const sourceAttr = ` data-one-source="${location}:${jsx.line}:${jsx.column}"`; result = result.slice(0, jsx.insertOffset) + sourceAttr + result.slice(jsx.insertOffset); } return { code: result, map: null }; } let editorWarned = false; const editorArgs = { code: (f, l, c) => ["-g", `${f}:${l}:${c}`], cursor: (f, l, c) => ["-g", `${f}:${l}:${c}`], codium: (f, l, c) => ["-g", `${f}:${l}:${c}`], vscodium: (f, l, c) => ["-g", `${f}:${l}:${c}`], zed: (f, l, c) => [`${f}:${l}:${c}`], subl: (f, l, c) => [`${f}:${l}:${c}`], webstorm: (f, l, c) => ["--line", l, "--column", c, f], idea: (f, l, c) => ["--line", l, "--column", c, f], vim: (f, l) => [`+${l}`, f], nvim: (f, l) => [`+${l}`, f], emacs: (f, l, c) => [`+${l}:${c}`, f] }; function openInEditor(editor, filePath, line, column) { const resolved = editor || process.env.LAUNCH_EDITOR || process.env.EDITOR; if (!resolved) { if (!editorWarned) { editorWarned = true; console.warn(`[one] Set devtools.editor in your one config or LAUNCH_EDITOR env var to open files from the inspector (e.g. 'cursor', 'code', 'zed')`); } return; } const fullPath = path.join(process.cwd(), filePath); const l = line || "1"; const c = column || "1"; const buildArgs = editorArgs[resolved]; const args = buildArgs ? buildArgs(fullPath, l, c) : [fullPath]; const child = spawn(resolved, args, { stdio: "ignore", detached: true }); child.unref(); child.on("error", err => { console.warn(`[one:source-inspector] Failed to open editor '${resolved}': ${err.message}`); }); } const vscodeClients = /* @__PURE__ */new Set(); function sourceInspectorPlugin(opts) { const cache = /* @__PURE__ */new Map(); return [ // Transform plugin - injects data-one-source attributes { name: "one:source-inspector-transform", enforce: "pre", apply: "serve", transform: { // must run before clientTreeShakePlugin which also uses order: 'pre' // (within same order, plugin array position determines precedence) order: "pre", async handler(code, id) { const envName = this.environment?.name; if (envName === "ios" || envName === "android") return; if (id.includes("node_modules") || id.includes("?raw") || id.includes("dist") || id.includes("build")) { return; } if (!id.endsWith(".jsx") && !id.endsWith(".tsx")) return; if (cache.has(code)) { return cache.get(code); } const out = await injectSourceToJsx(code, id); cache.set(code, out); if (cache.size > 100) { cache.clear(); } return out; } } }, // Note: Inspector UI script is now injected via DevHead.tsx for SSR compatibility // Server plugin - handles open-source requests and cursor WebSocket { name: "one:source-inspector-server", apply: "serve", configureServer(server) { let wss = null; import("ws").then(({ WebSocketServer }) => { wss = new WebSocketServer({ noServer: true }); server.httpServer?.on("upgrade", (req, socket, head) => { if (req.url !== "/__one/cursor") return; wss.handleUpgrade(req, socket, head, ws => { vscodeClients.add(ws); ws.on("message", data => { try { const message = JSON.parse(data.toString()); if (message.type === "cursor-position") { server.hot.send("one:cursor-highlight", { file: message.file, line: message.line, column: message.column }); } else if (message.type === "cursor-clear") { server.hot.send("one:cursor-highlight", { clear: true }); } } catch {} }); ws.on("close", () => { vscodeClients.delete(ws); server.hot.send("one:cursor-highlight", { clear: true }); }); }); }); }); server.middlewares.use(async (req, res, next) => { if (!req.url?.startsWith("/__one/open-source")) { return next(); } try { const url = new URL(req.url, "http://localhost"); const source = url.searchParams.get("source"); if (!source) { res.statusCode = 400; res.end("Missing source parameter"); return; } const parts = source.split(":"); const column = parts.pop(); const line = parts.pop(); const filePath = parts.join(":"); openInEditor(opts?.editor, filePath, line, column); res.statusCode = 200; res.end("OK"); } catch (err) { console.error("[one:source-inspector] Error:", err); res.statusCode = 500; res.end("Internal server error"); } }); } }]; } export { sourceInspectorPlugin }; //# sourceMappingURL=sourceInspectorPlugin.mjs.map