one
Version:
One is a new React Framework that makes Vite serve both native and web.
222 lines (221 loc) • 7.46 kB
JavaScript
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