UNPKG

one

Version:

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

243 lines (242 loc) 9.52 kB
import { spawn } from "node:child_process"; import { existsSync } from "node:fs"; import path from "node:path"; import { parse } from "oxc-parser"; import { normalizePath } from "vite"; const DOM_TAGS = new Set("a abbr address area article aside audio b base bdi bdo blockquote body br button canvas caption cite code col colgroup data datalist dd del details dfn dialog div dl dt em embed fieldset figcaption figure footer form h1 h2 h3 h4 h5 h6 head header hgroup hr html i iframe img input ins kbd label legend li link main map mark menu meta meter nav noscript object ol optgroup option output p picture pre progress q rp rt ruby s samp script search section select slot small source span strong style sub summary sup table tbody td template textarea tfoot th thead time title tr track u ul var video wbr svg animate animateMotion animateTransform circle clipPath defs desc ellipse feBlend feColorMatrix feComponentTransfer feComposite feConvolveMatrix feDiffuseLighting feDisplacementMap feDistantLight feDropShadow feFlood feFuncA feFuncB feFuncG feFuncR feGaussianBlur feImage feMerge feMergeNode feMorphology feOffset fePointLight feSpecularLighting feSpotLight feTile feTurbulence filter foreignObject g image line linearGradient marker mask metadata mpath path pattern polygon polyline radialGradient rect set stop switch symbol text textPath tspan use view".split(" ")); function isHostTag(tagName) { const first = tagName.charCodeAt(0); return first >= 97 && first <= 122 && !tagName.includes("."); } 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") && (!isHostTag(tagName) || DOM_TAGS.has(tagName))) { 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); } function getSourceInspectorPath(filePath, cwd = process.cwd()) { const normalizedFilePath = normalizePath(filePath); const normalizedCwd = normalizePath(cwd); if (normalizedFilePath === normalizedCwd) { return "/"; } if (normalizedFilePath.startsWith(`${normalizedCwd}/`)) { return normalizedFilePath.slice(normalizedCwd.length); } return normalizedFilePath; } function resolveEditorFilePath(filePath, cwd = process.cwd(), fileExists = existsSync) { const projectPath = normalizePath(path.join(cwd, filePath)); return fileExists(projectPath) ? projectPath : filePath; } async function injectSourceToJsx(code, id) { const [filePath] = id.split("?"); if (!filePath) return; const location = getSourceInspectorPath(filePath); 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 = resolveEditorFilePath(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 { getSourceInspectorPath, injectSourceToJsx, resolveEditorFilePath, sourceInspectorPlugin }; //# sourceMappingURL=sourceInspectorPlugin.mjs.map