@tanstack/devtools-vite 0.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 TanStack
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,20 @@
1
+ # @tanstack/devtools-vite
2
+
3
+ This package is still under active development and might have breaking changes in the future. Please use it with caution.
4
+
5
+ ## General Usage
6
+
7
+ The `@tanstack/devtools-vite` package is designed to work with Vite projects.
8
+ Plug it into your plugins array:
9
+
10
+ ```ts
11
+ import { devtools } from '@tanstack/devtools-vite'
12
+
13
+ export default {
14
+ plugins: [
15
+ // Important to include it first!
16
+ devtools(),
17
+ ... //rest of the plugins
18
+ ],
19
+ }
20
+ ```
@@ -0,0 +1,26 @@
1
+ type OpenSourceData = {
2
+ type: 'open-source';
3
+ data: {
4
+ /** The source file to open */
5
+ source?: string;
6
+ /** The react router route ID, usually discovered via the hook useMatches */
7
+ routeID?: string;
8
+ /** The line number in the source file */
9
+ line?: number;
10
+ /** The column number in the source file */
11
+ column?: number;
12
+ };
13
+ };
14
+ export type EditorConfig = {
15
+ /** The name of the editor, used for debugging purposes */
16
+ name: string;
17
+ /** Callback to open a file in the editor */
18
+ open: (path: string, lineNumber: string | undefined, columnNumber?: string) => Promise<void>;
19
+ };
20
+ export declare const DEFAULT_EDITOR_CONFIG: EditorConfig;
21
+ export declare const handleOpenSource: ({ data, openInEditor, appDir, }: {
22
+ data: OpenSourceData;
23
+ appDir: string;
24
+ openInEditor: EditorConfig["open"];
25
+ }) => Promise<void>;
26
+ export {};
@@ -0,0 +1,58 @@
1
+ import { normalizePath } from "vite";
2
+ import { checkPath } from "./utils.js";
3
+ const DEFAULT_EDITOR_CONFIG = {
4
+ name: "VSCode",
5
+ open: async (path, lineNumber, columnNumber) => {
6
+ const { exec } = await import("node:child_process");
7
+ exec(
8
+ `code -g "${normalizePath(path).replaceAll("$", "\\$")}${lineNumber ? `:${lineNumber}` : ""}${columnNumber ? `:${columnNumber}` : ""}"`
9
+ );
10
+ }
11
+ };
12
+ const handleOpenSource = async ({
13
+ data,
14
+ openInEditor,
15
+ appDir
16
+ }) => {
17
+ const { source, line, routeID } = data.data;
18
+ const lineNum = line ? `${line}` : void 0;
19
+ const fs = await import("node:fs");
20
+ const path = await import("node:path");
21
+ if (source) {
22
+ return openInEditor(source, lineNum);
23
+ }
24
+ if (routeID) {
25
+ const routePath = path.join(appDir, routeID);
26
+ const checkedPath = await checkPath(routePath);
27
+ if (!checkedPath) return;
28
+ const { type, validPath } = checkedPath;
29
+ const reactExtensions = ["tsx", "jsx"];
30
+ const allExtensions = ["ts", "js", ...reactExtensions];
31
+ const isRoot = routeID === "root";
32
+ const findFileByExtension = (prefix, filePaths) => {
33
+ const file = filePaths.find(
34
+ (file2) => allExtensions.some((ext) => file2 === `${prefix}.${ext}`)
35
+ );
36
+ return file;
37
+ };
38
+ if (isRoot) {
39
+ if (!fs.existsSync(appDir)) return;
40
+ const filesInReactRouterPath = fs.readdirSync(appDir);
41
+ const rootFile = findFileByExtension("root", filesInReactRouterPath);
42
+ rootFile && openInEditor(path.join(appDir, rootFile), lineNum);
43
+ return;
44
+ }
45
+ if (type === "directory") {
46
+ const filesInFolderRoute = fs.readdirSync(validPath);
47
+ const routeFile = findFileByExtension("route", filesInFolderRoute);
48
+ routeFile && openInEditor(path.join(appDir, routeID, routeFile), lineNum);
49
+ return;
50
+ }
51
+ return openInEditor(validPath, lineNum);
52
+ }
53
+ };
54
+ export {
55
+ DEFAULT_EDITOR_CONFIG,
56
+ handleOpenSource
57
+ };
58
+ //# sourceMappingURL=editor.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"editor.js","sources":["../../src/editor.ts"],"sourcesContent":["import { normalizePath } from 'vite'\nimport { checkPath } from './utils.js'\n\ntype OpenSourceData = {\n type: 'open-source'\n data: {\n /** The source file to open */\n source?: string\n /** The react router route ID, usually discovered via the hook useMatches */\n routeID?: string\n /** The line number in the source file */\n line?: number\n /** The column number in the source file */\n column?: number\n }\n}\n\nexport type EditorConfig = {\n /** The name of the editor, used for debugging purposes */\n name: string\n /** Callback to open a file in the editor */\n open: (\n path: string,\n lineNumber: string | undefined,\n columnNumber?: string,\n ) => Promise<void>\n}\n\nexport const DEFAULT_EDITOR_CONFIG: EditorConfig = {\n name: 'VSCode',\n open: async (path, lineNumber, columnNumber) => {\n const { exec } = await import('node:child_process')\n exec(\n `code -g \"${normalizePath(path).replaceAll('$', '\\\\$')}${lineNumber ? `:${lineNumber}` : ''}${columnNumber ? `:${columnNumber}` : ''}\"`,\n )\n },\n}\n\nexport const handleOpenSource = async ({\n data,\n openInEditor,\n appDir,\n}: {\n data: OpenSourceData\n appDir: string\n openInEditor: EditorConfig['open']\n}) => {\n const { source, line, routeID } = data.data\n const lineNum = line ? `${line}` : undefined\n const fs = await import('node:fs')\n const path = await import('node:path')\n if (source) {\n return openInEditor(source, lineNum)\n }\n\n if (routeID) {\n const routePath = path.join(appDir, routeID)\n const checkedPath = await checkPath(routePath)\n\n if (!checkedPath) return\n const { type, validPath } = checkedPath\n\n const reactExtensions = ['tsx', 'jsx']\n const allExtensions = ['ts', 'js', ...reactExtensions]\n const isRoot = routeID === 'root'\n const findFileByExtension = (prefix: string, filePaths: Array<string>) => {\n const file = filePaths.find((file) =>\n allExtensions.some((ext) => file === `${prefix}.${ext}`),\n )\n return file\n }\n\n if (isRoot) {\n if (!fs.existsSync(appDir)) return\n const filesInReactRouterPath = fs.readdirSync(appDir)\n const rootFile = findFileByExtension('root', filesInReactRouterPath)\n rootFile && openInEditor(path.join(appDir, rootFile), lineNum)\n return\n }\n\n // If its not the root route, then we find the file or folder in the routes folder\n // We know that the route ID is in the form of \"routes/contact\" or \"routes/user.profile\" when is not root\n // so the ID already contains the \"routes\" segment, so we just need to find the file or folder in the routes folder\n if (type === 'directory') {\n const filesInFolderRoute = fs.readdirSync(validPath)\n const routeFile = findFileByExtension('route', filesInFolderRoute)\n routeFile && openInEditor(path.join(appDir, routeID, routeFile), lineNum)\n return\n }\n return openInEditor(validPath, lineNum)\n }\n}\n"],"names":["file"],"mappings":";;AA4BO,MAAM,wBAAsC;AAAA,EACjD,MAAM;AAAA,EACN,MAAM,OAAO,MAAM,YAAY,iBAAiB;AAC9C,UAAM,EAAE,KAAA,IAAS,MAAM,OAAO,oBAAoB;AAClD;AAAA,MACE,YAAY,cAAc,IAAI,EAAE,WAAW,KAAK,KAAK,CAAC,GAAG,aAAa,IAAI,UAAU,KAAK,EAAE,GAAG,eAAe,IAAI,YAAY,KAAK,EAAE;AAAA,IAAA;AAAA,EAExI;AACF;AAEO,MAAM,mBAAmB,OAAO;AAAA,EACrC;AAAA,EACA;AAAA,EACA;AACF,MAIM;AACJ,QAAM,EAAE,QAAQ,MAAM,QAAA,IAAY,KAAK;AACvC,QAAM,UAAU,OAAO,GAAG,IAAI,KAAK;AACnC,QAAM,KAAK,MAAM,OAAO,SAAS;AACjC,QAAM,OAAO,MAAM,OAAO,WAAW;AACrC,MAAI,QAAQ;AACV,WAAO,aAAa,QAAQ,OAAO;AAAA,EACrC;AAEA,MAAI,SAAS;AACX,UAAM,YAAY,KAAK,KAAK,QAAQ,OAAO;AAC3C,UAAM,cAAc,MAAM,UAAU,SAAS;AAE7C,QAAI,CAAC,YAAa;AAClB,UAAM,EAAE,MAAM,UAAA,IAAc;AAE5B,UAAM,kBAAkB,CAAC,OAAO,KAAK;AACrC,UAAM,gBAAgB,CAAC,MAAM,MAAM,GAAG,eAAe;AACrD,UAAM,SAAS,YAAY;AAC3B,UAAM,sBAAsB,CAAC,QAAgB,cAA6B;AACxE,YAAM,OAAO,UAAU;AAAA,QAAK,CAACA,UAC3B,cAAc,KAAK,CAAC,QAAQA,UAAS,GAAG,MAAM,IAAI,GAAG,EAAE;AAAA,MAAA;AAEzD,aAAO;AAAA,IACT;AAEA,QAAI,QAAQ;AACV,UAAI,CAAC,GAAG,WAAW,MAAM,EAAG;AAC5B,YAAM,yBAAyB,GAAG,YAAY,MAAM;AACpD,YAAM,WAAW,oBAAoB,QAAQ,sBAAsB;AACnE,kBAAY,aAAa,KAAK,KAAK,QAAQ,QAAQ,GAAG,OAAO;AAC7D;AAAA,IACF;AAKA,QAAI,SAAS,aAAa;AACxB,YAAM,qBAAqB,GAAG,YAAY,SAAS;AACnD,YAAM,YAAY,oBAAoB,SAAS,kBAAkB;AACjE,mBAAa,aAAa,KAAK,KAAK,QAAQ,SAAS,SAAS,GAAG,OAAO;AACxE;AAAA,IACF;AACA,WAAO,aAAa,WAAW,OAAO;AAAA,EACxC;AACF;"}
@@ -0,0 +1,2 @@
1
+ export { devtools, defineDevtoolsConfig } from './plugin.js';
2
+ export type { TanStackDevtoolsViteConfig } from './plugin.js';
@@ -0,0 +1,6 @@
1
+ import { defineDevtoolsConfig, devtools } from "./plugin.js";
2
+ export {
3
+ defineDevtoolsConfig,
4
+ devtools
5
+ };
6
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";"}
@@ -0,0 +1,27 @@
1
+ import { EditorConfig } from './editor.js';
2
+ import { ServerEventBusConfig } from '@tanstack/devtools-event-bus/server';
3
+ import { Plugin } from 'vite';
4
+ export type TanStackDevtoolsViteConfig = {
5
+ /** The directory where the react router app is located. Defaults to the "./src" relative to where vite.config is being defined. */
6
+ appDir?: string;
7
+ /**
8
+ * Configuration for the editor integration. Defaults to opening in VS code
9
+ */
10
+ editor?: EditorConfig;
11
+ /**
12
+ * The configuration options for the server event bus
13
+ */
14
+ eventBusConfig?: ServerEventBusConfig;
15
+ /**
16
+ * Configuration for enhanced logging.
17
+ */
18
+ enhancedLogs?: {
19
+ /**
20
+ * Whether to enable enhanced logging.
21
+ * @default true
22
+ */
23
+ enabled: boolean;
24
+ };
25
+ };
26
+ export declare const defineDevtoolsConfig: (config: TanStackDevtoolsViteConfig) => TanStackDevtoolsViteConfig;
27
+ export declare const devtools: (args?: TanStackDevtoolsViteConfig) => Array<Plugin>;
@@ -0,0 +1,96 @@
1
+ import { normalizePath } from "vite";
2
+ import chalk from "chalk";
3
+ import { ServerEventBus } from "@tanstack/devtools-event-bus/server";
4
+ import { handleDevToolsViteRequest } from "./utils.js";
5
+ import { DEFAULT_EDITOR_CONFIG, handleOpenSource } from "./editor.js";
6
+ const defineDevtoolsConfig = (config) => config;
7
+ const devtools = (args) => {
8
+ let port = 5173;
9
+ const appDir = args?.appDir || "./src";
10
+ const enhancedLogsConfig = args?.enhancedLogs ?? { enabled: true };
11
+ const bus = new ServerEventBus(args?.eventBusConfig);
12
+ return [
13
+ {
14
+ enforce: "pre",
15
+ name: "@tanstack/devtools:custom-server",
16
+ apply(config) {
17
+ return config.mode === "development";
18
+ },
19
+ configureServer(server) {
20
+ bus.start();
21
+ server.middlewares.use((req, _res, next) => {
22
+ if (req.socket.localPort && req.socket.localPort !== port) {
23
+ port = req.socket.localPort;
24
+ }
25
+ next();
26
+ });
27
+ if (server.config.server.port) {
28
+ port = server.config.server.port;
29
+ }
30
+ server.httpServer?.on("listening", () => {
31
+ port = server.config.server.port;
32
+ });
33
+ const editor = args?.editor ?? DEFAULT_EDITOR_CONFIG;
34
+ const openInEditor = async (path, lineNum) => {
35
+ if (!path) {
36
+ return;
37
+ }
38
+ await editor.open(path, lineNum);
39
+ };
40
+ server.middlewares.use(
41
+ (req, res, next) => handleDevToolsViteRequest(req, res, next, (parsedData) => {
42
+ const { data, routine } = parsedData;
43
+ if (routine === "open-source") {
44
+ return handleOpenSource({
45
+ data: { type: data.type, data },
46
+ openInEditor,
47
+ appDir
48
+ });
49
+ }
50
+ return;
51
+ })
52
+ );
53
+ }
54
+ },
55
+ {
56
+ name: "@tanstack/devtools:better-console-logs",
57
+ enforce: "pre",
58
+ apply(config) {
59
+ return config.mode === "development" && enhancedLogsConfig.enabled;
60
+ },
61
+ transform(code, id) {
62
+ if (id.includes("node_modules") || id.includes("?raw") || id.includes("dist") || id.includes("build"))
63
+ return code;
64
+ if (!code.includes("console.")) {
65
+ return code;
66
+ }
67
+ const lines = code.split("\n");
68
+ return lines.map((line, lineNumber) => {
69
+ if (line.trim().startsWith("//") || line.trim().startsWith("/**") || line.trim().startsWith("*")) {
70
+ return line;
71
+ }
72
+ if (line.replaceAll(" ", "").includes("=>console.") || line.includes("return console.")) {
73
+ return line;
74
+ }
75
+ const column = line.indexOf("console.");
76
+ const location = `${id.replace(normalizePath(process.cwd()), "")}:${lineNumber + 1}:${column + 1}`;
77
+ const logMessage = `'${chalk.magenta("LOG")} ${chalk.blueBright(`${location} - http://localhost:${port}/__tsd/open-source?source=${encodeURIComponent(id.replace(normalizePath(process.cwd()), ""))}&line=${lineNumber + 1}&column=${column + 1}`)}\\n → '`;
78
+ if (line.includes("console.log(")) {
79
+ const newLine = `console.log(${logMessage},`;
80
+ return line.replace("console.log(", newLine);
81
+ }
82
+ if (line.includes("console.error(")) {
83
+ const newLine = `console.error(${logMessage},`;
84
+ return line.replace("console.error(", newLine);
85
+ }
86
+ return line;
87
+ }).join("\n");
88
+ }
89
+ }
90
+ ];
91
+ };
92
+ export {
93
+ defineDevtoolsConfig,
94
+ devtools
95
+ };
96
+ //# sourceMappingURL=plugin.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugin.js","sources":["../../src/plugin.ts"],"sourcesContent":["import { normalizePath } from 'vite'\nimport chalk from 'chalk'\nimport { ServerEventBus } from '@tanstack/devtools-event-bus/server'\nimport { handleDevToolsViteRequest } from './utils'\nimport { DEFAULT_EDITOR_CONFIG, handleOpenSource } from './editor'\nimport type { EditorConfig } from './editor'\nimport type { ServerEventBusConfig } from '@tanstack/devtools-event-bus/server'\nimport type { Plugin } from 'vite'\n\nexport type TanStackDevtoolsViteConfig = {\n /** The directory where the react router app is located. Defaults to the \"./src\" relative to where vite.config is being defined. */\n appDir?: string\n /**\n * Configuration for the editor integration. Defaults to opening in VS code\n */\n editor?: EditorConfig\n /**\n * The configuration options for the server event bus\n */\n eventBusConfig?: ServerEventBusConfig\n /**\n * Configuration for enhanced logging.\n */\n enhancedLogs?: {\n /**\n * Whether to enable enhanced logging.\n * @default true\n */\n enabled: boolean\n }\n}\n\nexport const defineDevtoolsConfig = (config: TanStackDevtoolsViteConfig) =>\n config\n\nexport const devtools = (args?: TanStackDevtoolsViteConfig): Array<Plugin> => {\n let port = 5173\n const appDir = args?.appDir || './src'\n const enhancedLogsConfig = args?.enhancedLogs ?? { enabled: true }\n const bus = new ServerEventBus(args?.eventBusConfig)\n\n return [\n {\n enforce: 'pre',\n name: '@tanstack/devtools:custom-server',\n apply(config) {\n // Custom server is only needed in development for piping events to the client\n return config.mode === 'development'\n },\n configureServer(server) {\n bus.start()\n server.middlewares.use((req, _res, next) => {\n if (req.socket.localPort && req.socket.localPort !== port) {\n port = req.socket.localPort\n }\n next()\n })\n if (server.config.server.port) {\n port = server.config.server.port\n }\n\n server.httpServer?.on('listening', () => {\n port = server.config.server.port\n })\n\n const editor = args?.editor ?? DEFAULT_EDITOR_CONFIG\n const openInEditor = async (\n path: string | undefined,\n lineNum: string | undefined,\n ) => {\n if (!path) {\n return\n }\n await editor.open(path, lineNum)\n }\n server.middlewares.use((req, res, next) =>\n handleDevToolsViteRequest(req, res, next, (parsedData) => {\n const { data, routine } = parsedData\n if (routine === 'open-source') {\n return handleOpenSource({\n data: { type: data.type, data },\n openInEditor,\n appDir,\n })\n }\n return\n }),\n )\n },\n },\n {\n name: '@tanstack/devtools:better-console-logs',\n enforce: 'pre',\n apply(config) {\n return config.mode === 'development' && enhancedLogsConfig.enabled\n },\n transform(code, id) {\n // Ignore anything external\n if (\n id.includes('node_modules') ||\n id.includes('?raw') ||\n id.includes('dist') ||\n id.includes('build')\n )\n return code\n\n if (!code.includes('console.')) {\n return code\n }\n const lines = code.split('\\n')\n return lines\n .map((line, lineNumber) => {\n if (\n line.trim().startsWith('//') ||\n line.trim().startsWith('/**') ||\n line.trim().startsWith('*')\n ) {\n return line\n }\n // Do not add for arrow functions or return statements\n if (\n line.replaceAll(' ', '').includes('=>console.') ||\n line.includes('return console.')\n ) {\n return line\n }\n\n const column = line.indexOf('console.')\n const location = `${id.replace(normalizePath(process.cwd()), '')}:${lineNumber + 1}:${column + 1}`\n const logMessage = `'${chalk.magenta('LOG')} ${chalk.blueBright(`${location} - http://localhost:${port}/__tsd/open-source?source=${encodeURIComponent(id.replace(normalizePath(process.cwd()), ''))}&line=${lineNumber + 1}&column=${column + 1}`)}\\\\n → '`\n if (line.includes('console.log(')) {\n const newLine = `console.log(${logMessage},`\n return line.replace('console.log(', newLine)\n }\n if (line.includes('console.error(')) {\n const newLine = `console.error(${logMessage},`\n return line.replace('console.error(', newLine)\n }\n return line\n })\n .join('\\n')\n },\n },\n ]\n}\n"],"names":[],"mappings":"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"}
@@ -0,0 +1,10 @@
1
+ import { Connect } from 'vite';
2
+ import { IncomingMessage, ServerResponse } from 'node:http';
3
+ export declare const handleDevToolsViteRequest: (req: Connect.IncomingMessage, res: ServerResponse<IncomingMessage>, next: Connect.NextFunction, cb: (data: any) => void) => void;
4
+ export declare function checkPath(routePath: string, extensions?: string[]): Promise<{
5
+ readonly validPath: string;
6
+ readonly type: "directory";
7
+ } | {
8
+ readonly validPath: string;
9
+ readonly type: "file";
10
+ } | null>;
@@ -0,0 +1,56 @@
1
+ import { normalizePath } from "vite";
2
+ const handleDevToolsViteRequest = (req, res, next, cb) => {
3
+ if (req.url?.includes("__tsd/open-source")) {
4
+ const searchParams = new URLSearchParams(req.url.split("?")[1]);
5
+ const source = searchParams.get("source");
6
+ const line = searchParams.get("line");
7
+ const column = searchParams.get("column");
8
+ cb({
9
+ type: "open-source",
10
+ routine: "open-source",
11
+ data: {
12
+ source: source ? normalizePath(`${process.cwd()}/${source}`) : void 0,
13
+ line,
14
+ column
15
+ }
16
+ });
17
+ res.setHeader("Content-Type", "text/html");
18
+ res.write(`<script> window.close(); <\/script>`);
19
+ res.end();
20
+ return;
21
+ }
22
+ if (!req.url?.includes("__tsd")) {
23
+ return next();
24
+ }
25
+ const chunks = [];
26
+ req.on("data", (chunk) => {
27
+ chunks.push(chunk);
28
+ });
29
+ req.on("end", () => {
30
+ const dataToParse = Buffer.concat(chunks);
31
+ try {
32
+ const parsedData = JSON.parse(dataToParse.toString());
33
+ cb(parsedData);
34
+ } catch (e) {
35
+ }
36
+ res.write("OK");
37
+ });
38
+ };
39
+ async function checkPath(routePath, extensions = [".tsx", ".jsx", ".ts", ".js"]) {
40
+ const fs = await import("node:fs");
41
+ if (fs.existsSync(routePath) && fs.lstatSync(routePath).isDirectory()) {
42
+ return { validPath: routePath, type: "directory" };
43
+ }
44
+ for (const ext of extensions) {
45
+ const filePath = `${routePath}${ext}`;
46
+ if (fs.existsSync(filePath) && fs.lstatSync(filePath).isFile()) {
47
+ return { validPath: filePath, type: "file" };
48
+ }
49
+ }
50
+ return null;
51
+ }
52
+ export {
53
+ checkPath,
54
+ handleDevToolsViteRequest
55
+ };
56
+ //# sourceMappingURL=utils.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"utils.js","sources":["../../src/utils.ts"],"sourcesContent":["import { normalizePath } from 'vite'\nimport type { Connect } from 'vite'\nimport type { IncomingMessage, ServerResponse } from 'node:http'\n\nexport const handleDevToolsViteRequest = (\n req: Connect.IncomingMessage,\n res: ServerResponse<IncomingMessage>,\n next: Connect.NextFunction,\n cb: (data: any) => void,\n) => {\n if (req.url?.includes('__tsd/open-source')) {\n const searchParams = new URLSearchParams(req.url.split('?')[1])\n const source = searchParams.get('source')\n const line = searchParams.get('line')\n const column = searchParams.get('column')\n cb({\n type: 'open-source',\n routine: 'open-source',\n data: {\n source: source\n ? normalizePath(`${process.cwd()}/${source}`)\n : undefined,\n line,\n column,\n },\n })\n res.setHeader('Content-Type', 'text/html')\n res.write(`<script> window.close(); </script>`)\n res.end()\n return\n }\n if (!req.url?.includes('__tsd')) {\n return next()\n }\n\n const chunks: Array<any> = []\n req.on('data', (chunk) => {\n chunks.push(chunk)\n })\n req.on('end', () => {\n const dataToParse = Buffer.concat(chunks)\n try {\n const parsedData = JSON.parse(dataToParse.toString())\n cb(parsedData)\n } catch (e) {}\n res.write('OK')\n })\n}\n\nexport async function checkPath(\n routePath: string,\n extensions = ['.tsx', '.jsx', '.ts', '.js'],\n) {\n const fs = await import('node:fs')\n // Check if the path exists as a directory\n if (fs.existsSync(routePath) && fs.lstatSync(routePath).isDirectory()) {\n return { validPath: routePath, type: 'directory' } as const\n }\n\n // Check if the path exists as a file with one of the given extensions\n for (const ext of extensions) {\n const filePath = `${routePath}${ext}`\n if (fs.existsSync(filePath) && fs.lstatSync(filePath).isFile()) {\n return { validPath: filePath, type: 'file' } as const\n }\n }\n\n // If neither a file nor a directory is found\n return null\n}\n"],"names":[],"mappings":";AAIO,MAAM,4BAA4B,CACvC,KACA,KACA,MACA,OACG;AACH,MAAI,IAAI,KAAK,SAAS,mBAAmB,GAAG;AAC1C,UAAM,eAAe,IAAI,gBAAgB,IAAI,IAAI,MAAM,GAAG,EAAE,CAAC,CAAC;AAC9D,UAAM,SAAS,aAAa,IAAI,QAAQ;AACxC,UAAM,OAAO,aAAa,IAAI,MAAM;AACpC,UAAM,SAAS,aAAa,IAAI,QAAQ;AACxC,OAAG;AAAA,MACD,MAAM;AAAA,MACN,SAAS;AAAA,MACT,MAAM;AAAA,QACJ,QAAQ,SACJ,cAAc,GAAG,QAAQ,KAAK,IAAI,MAAM,EAAE,IAC1C;AAAA,QACJ;AAAA,QACA;AAAA,MAAA;AAAA,IACF,CACD;AACD,QAAI,UAAU,gBAAgB,WAAW;AACzC,QAAI,MAAM,qCAAoC;AAC9C,QAAI,IAAA;AACJ;AAAA,EACF;AACA,MAAI,CAAC,IAAI,KAAK,SAAS,OAAO,GAAG;AAC/B,WAAO,KAAA;AAAA,EACT;AAEA,QAAM,SAAqB,CAAA;AAC3B,MAAI,GAAG,QAAQ,CAAC,UAAU;AACxB,WAAO,KAAK,KAAK;AAAA,EACnB,CAAC;AACD,MAAI,GAAG,OAAO,MAAM;AAClB,UAAM,cAAc,OAAO,OAAO,MAAM;AACxC,QAAI;AACF,YAAM,aAAa,KAAK,MAAM,YAAY,UAAU;AACpD,SAAG,UAAU;AAAA,IACf,SAAS,GAAG;AAAA,IAAC;AACb,QAAI,MAAM,IAAI;AAAA,EAChB,CAAC;AACH;AAEA,eAAsB,UACpB,WACA,aAAa,CAAC,QAAQ,QAAQ,OAAO,KAAK,GAC1C;AACA,QAAM,KAAK,MAAM,OAAO,SAAS;AAEjC,MAAI,GAAG,WAAW,SAAS,KAAK,GAAG,UAAU,SAAS,EAAE,eAAe;AACrE,WAAO,EAAE,WAAW,WAAW,MAAM,YAAA;AAAA,EACvC;AAGA,aAAW,OAAO,YAAY;AAC5B,UAAM,WAAW,GAAG,SAAS,GAAG,GAAG;AACnC,QAAI,GAAG,WAAW,QAAQ,KAAK,GAAG,UAAU,QAAQ,EAAE,UAAU;AAC9D,aAAO,EAAE,WAAW,UAAU,MAAM,OAAA;AAAA,IACtC;AAAA,EACF;AAGA,SAAO;AACT;"}
package/package.json ADDED
@@ -0,0 +1,57 @@
1
+ {
2
+ "name": "@tanstack/devtools-vite",
3
+ "version": "0.2.3",
4
+ "description": "TanStack Vite plugin used to enhance the core devtools with additional functionalities",
5
+ "author": "Tanner Linsley",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/TanStack/devtools.git",
10
+ "directory": "packages/devtools"
11
+ },
12
+ "homepage": "https://tanstack.com/devtools",
13
+ "funding": {
14
+ "type": "github",
15
+ "url": "https://github.com/sponsors/tannerlinsley"
16
+ },
17
+ "keywords": [
18
+ "devtools"
19
+ ],
20
+ "type": "module",
21
+ "types": "dist/esm//index.d.ts",
22
+ "module": "dist/esm/index.js",
23
+ "exports": {
24
+ ".": {
25
+ "import": {
26
+ "types": "./dist/esm/index.d.ts",
27
+ "default": "./dist/esm/index.js"
28
+ }
29
+ },
30
+ "./package.json": "./package.json"
31
+ },
32
+ "sideEffects": false,
33
+ "engines": {
34
+ "node": ">=18"
35
+ },
36
+ "files": [
37
+ "dist/",
38
+ "src"
39
+ ],
40
+ "peerDependencies": {
41
+ "vite": "^7.0.0"
42
+ },
43
+ "dependencies": {
44
+ "chalk": "^5.6.0",
45
+ "@tanstack/devtools-event-bus": "0.2.2"
46
+ },
47
+ "scripts": {
48
+ "clean": "premove ./build ./dist",
49
+ "lint:fix": "eslint ./src --fix",
50
+ "test:eslint": "eslint ./src",
51
+ "test:lib": "vitest",
52
+ "test:lib:dev": "pnpm test:lib --watch",
53
+ "test:types": "tsc",
54
+ "test:build": "publint --strict",
55
+ "build": "vite build"
56
+ }
57
+ }
package/src/editor.ts ADDED
@@ -0,0 +1,92 @@
1
+ import { normalizePath } from 'vite'
2
+ import { checkPath } from './utils.js'
3
+
4
+ type OpenSourceData = {
5
+ type: 'open-source'
6
+ data: {
7
+ /** The source file to open */
8
+ source?: string
9
+ /** The react router route ID, usually discovered via the hook useMatches */
10
+ routeID?: string
11
+ /** The line number in the source file */
12
+ line?: number
13
+ /** The column number in the source file */
14
+ column?: number
15
+ }
16
+ }
17
+
18
+ export type EditorConfig = {
19
+ /** The name of the editor, used for debugging purposes */
20
+ name: string
21
+ /** Callback to open a file in the editor */
22
+ open: (
23
+ path: string,
24
+ lineNumber: string | undefined,
25
+ columnNumber?: string,
26
+ ) => Promise<void>
27
+ }
28
+
29
+ export const DEFAULT_EDITOR_CONFIG: EditorConfig = {
30
+ name: 'VSCode',
31
+ open: async (path, lineNumber, columnNumber) => {
32
+ const { exec } = await import('node:child_process')
33
+ exec(
34
+ `code -g "${normalizePath(path).replaceAll('$', '\\$')}${lineNumber ? `:${lineNumber}` : ''}${columnNumber ? `:${columnNumber}` : ''}"`,
35
+ )
36
+ },
37
+ }
38
+
39
+ export const handleOpenSource = async ({
40
+ data,
41
+ openInEditor,
42
+ appDir,
43
+ }: {
44
+ data: OpenSourceData
45
+ appDir: string
46
+ openInEditor: EditorConfig['open']
47
+ }) => {
48
+ const { source, line, routeID } = data.data
49
+ const lineNum = line ? `${line}` : undefined
50
+ const fs = await import('node:fs')
51
+ const path = await import('node:path')
52
+ if (source) {
53
+ return openInEditor(source, lineNum)
54
+ }
55
+
56
+ if (routeID) {
57
+ const routePath = path.join(appDir, routeID)
58
+ const checkedPath = await checkPath(routePath)
59
+
60
+ if (!checkedPath) return
61
+ const { type, validPath } = checkedPath
62
+
63
+ const reactExtensions = ['tsx', 'jsx']
64
+ const allExtensions = ['ts', 'js', ...reactExtensions]
65
+ const isRoot = routeID === 'root'
66
+ const findFileByExtension = (prefix: string, filePaths: Array<string>) => {
67
+ const file = filePaths.find((file) =>
68
+ allExtensions.some((ext) => file === `${prefix}.${ext}`),
69
+ )
70
+ return file
71
+ }
72
+
73
+ if (isRoot) {
74
+ if (!fs.existsSync(appDir)) return
75
+ const filesInReactRouterPath = fs.readdirSync(appDir)
76
+ const rootFile = findFileByExtension('root', filesInReactRouterPath)
77
+ rootFile && openInEditor(path.join(appDir, rootFile), lineNum)
78
+ return
79
+ }
80
+
81
+ // If its not the root route, then we find the file or folder in the routes folder
82
+ // We know that the route ID is in the form of "routes/contact" or "routes/user.profile" when is not root
83
+ // so the ID already contains the "routes" segment, so we just need to find the file or folder in the routes folder
84
+ if (type === 'directory') {
85
+ const filesInFolderRoute = fs.readdirSync(validPath)
86
+ const routeFile = findFileByExtension('route', filesInFolderRoute)
87
+ routeFile && openInEditor(path.join(appDir, routeID, routeFile), lineNum)
88
+ return
89
+ }
90
+ return openInEditor(validPath, lineNum)
91
+ }
92
+ }
package/src/index.ts ADDED
@@ -0,0 +1,2 @@
1
+ export { devtools, defineDevtoolsConfig } from './plugin'
2
+ export type { TanStackDevtoolsViteConfig } from './plugin'
package/src/plugin.ts ADDED
@@ -0,0 +1,145 @@
1
+ import { normalizePath } from 'vite'
2
+ import chalk from 'chalk'
3
+ import { ServerEventBus } from '@tanstack/devtools-event-bus/server'
4
+ import { handleDevToolsViteRequest } from './utils'
5
+ import { DEFAULT_EDITOR_CONFIG, handleOpenSource } from './editor'
6
+ import type { EditorConfig } from './editor'
7
+ import type { ServerEventBusConfig } from '@tanstack/devtools-event-bus/server'
8
+ import type { Plugin } from 'vite'
9
+
10
+ export type TanStackDevtoolsViteConfig = {
11
+ /** The directory where the react router app is located. Defaults to the "./src" relative to where vite.config is being defined. */
12
+ appDir?: string
13
+ /**
14
+ * Configuration for the editor integration. Defaults to opening in VS code
15
+ */
16
+ editor?: EditorConfig
17
+ /**
18
+ * The configuration options for the server event bus
19
+ */
20
+ eventBusConfig?: ServerEventBusConfig
21
+ /**
22
+ * Configuration for enhanced logging.
23
+ */
24
+ enhancedLogs?: {
25
+ /**
26
+ * Whether to enable enhanced logging.
27
+ * @default true
28
+ */
29
+ enabled: boolean
30
+ }
31
+ }
32
+
33
+ export const defineDevtoolsConfig = (config: TanStackDevtoolsViteConfig) =>
34
+ config
35
+
36
+ export const devtools = (args?: TanStackDevtoolsViteConfig): Array<Plugin> => {
37
+ let port = 5173
38
+ const appDir = args?.appDir || './src'
39
+ const enhancedLogsConfig = args?.enhancedLogs ?? { enabled: true }
40
+ const bus = new ServerEventBus(args?.eventBusConfig)
41
+
42
+ return [
43
+ {
44
+ enforce: 'pre',
45
+ name: '@tanstack/devtools:custom-server',
46
+ apply(config) {
47
+ // Custom server is only needed in development for piping events to the client
48
+ return config.mode === 'development'
49
+ },
50
+ configureServer(server) {
51
+ bus.start()
52
+ server.middlewares.use((req, _res, next) => {
53
+ if (req.socket.localPort && req.socket.localPort !== port) {
54
+ port = req.socket.localPort
55
+ }
56
+ next()
57
+ })
58
+ if (server.config.server.port) {
59
+ port = server.config.server.port
60
+ }
61
+
62
+ server.httpServer?.on('listening', () => {
63
+ port = server.config.server.port
64
+ })
65
+
66
+ const editor = args?.editor ?? DEFAULT_EDITOR_CONFIG
67
+ const openInEditor = async (
68
+ path: string | undefined,
69
+ lineNum: string | undefined,
70
+ ) => {
71
+ if (!path) {
72
+ return
73
+ }
74
+ await editor.open(path, lineNum)
75
+ }
76
+ server.middlewares.use((req, res, next) =>
77
+ handleDevToolsViteRequest(req, res, next, (parsedData) => {
78
+ const { data, routine } = parsedData
79
+ if (routine === 'open-source') {
80
+ return handleOpenSource({
81
+ data: { type: data.type, data },
82
+ openInEditor,
83
+ appDir,
84
+ })
85
+ }
86
+ return
87
+ }),
88
+ )
89
+ },
90
+ },
91
+ {
92
+ name: '@tanstack/devtools:better-console-logs',
93
+ enforce: 'pre',
94
+ apply(config) {
95
+ return config.mode === 'development' && enhancedLogsConfig.enabled
96
+ },
97
+ transform(code, id) {
98
+ // Ignore anything external
99
+ if (
100
+ id.includes('node_modules') ||
101
+ id.includes('?raw') ||
102
+ id.includes('dist') ||
103
+ id.includes('build')
104
+ )
105
+ return code
106
+
107
+ if (!code.includes('console.')) {
108
+ return code
109
+ }
110
+ const lines = code.split('\n')
111
+ return lines
112
+ .map((line, lineNumber) => {
113
+ if (
114
+ line.trim().startsWith('//') ||
115
+ line.trim().startsWith('/**') ||
116
+ line.trim().startsWith('*')
117
+ ) {
118
+ return line
119
+ }
120
+ // Do not add for arrow functions or return statements
121
+ if (
122
+ line.replaceAll(' ', '').includes('=>console.') ||
123
+ line.includes('return console.')
124
+ ) {
125
+ return line
126
+ }
127
+
128
+ const column = line.indexOf('console.')
129
+ const location = `${id.replace(normalizePath(process.cwd()), '')}:${lineNumber + 1}:${column + 1}`
130
+ const logMessage = `'${chalk.magenta('LOG')} ${chalk.blueBright(`${location} - http://localhost:${port}/__tsd/open-source?source=${encodeURIComponent(id.replace(normalizePath(process.cwd()), ''))}&line=${lineNumber + 1}&column=${column + 1}`)}\\n → '`
131
+ if (line.includes('console.log(')) {
132
+ const newLine = `console.log(${logMessage},`
133
+ return line.replace('console.log(', newLine)
134
+ }
135
+ if (line.includes('console.error(')) {
136
+ const newLine = `console.error(${logMessage},`
137
+ return line.replace('console.error(', newLine)
138
+ }
139
+ return line
140
+ })
141
+ .join('\n')
142
+ },
143
+ },
144
+ ]
145
+ }
package/src/utils.ts ADDED
@@ -0,0 +1,70 @@
1
+ import { normalizePath } from 'vite'
2
+ import type { Connect } from 'vite'
3
+ import type { IncomingMessage, ServerResponse } from 'node:http'
4
+
5
+ export const handleDevToolsViteRequest = (
6
+ req: Connect.IncomingMessage,
7
+ res: ServerResponse<IncomingMessage>,
8
+ next: Connect.NextFunction,
9
+ cb: (data: any) => void,
10
+ ) => {
11
+ if (req.url?.includes('__tsd/open-source')) {
12
+ const searchParams = new URLSearchParams(req.url.split('?')[1])
13
+ const source = searchParams.get('source')
14
+ const line = searchParams.get('line')
15
+ const column = searchParams.get('column')
16
+ cb({
17
+ type: 'open-source',
18
+ routine: 'open-source',
19
+ data: {
20
+ source: source
21
+ ? normalizePath(`${process.cwd()}/${source}`)
22
+ : undefined,
23
+ line,
24
+ column,
25
+ },
26
+ })
27
+ res.setHeader('Content-Type', 'text/html')
28
+ res.write(`<script> window.close(); </script>`)
29
+ res.end()
30
+ return
31
+ }
32
+ if (!req.url?.includes('__tsd')) {
33
+ return next()
34
+ }
35
+
36
+ const chunks: Array<any> = []
37
+ req.on('data', (chunk) => {
38
+ chunks.push(chunk)
39
+ })
40
+ req.on('end', () => {
41
+ const dataToParse = Buffer.concat(chunks)
42
+ try {
43
+ const parsedData = JSON.parse(dataToParse.toString())
44
+ cb(parsedData)
45
+ } catch (e) {}
46
+ res.write('OK')
47
+ })
48
+ }
49
+
50
+ export async function checkPath(
51
+ routePath: string,
52
+ extensions = ['.tsx', '.jsx', '.ts', '.js'],
53
+ ) {
54
+ const fs = await import('node:fs')
55
+ // Check if the path exists as a directory
56
+ if (fs.existsSync(routePath) && fs.lstatSync(routePath).isDirectory()) {
57
+ return { validPath: routePath, type: 'directory' } as const
58
+ }
59
+
60
+ // Check if the path exists as a file with one of the given extensions
61
+ for (const ext of extensions) {
62
+ const filePath = `${routePath}${ext}`
63
+ if (fs.existsSync(filePath) && fs.lstatSync(filePath).isFile()) {
64
+ return { validPath: filePath, type: 'file' } as const
65
+ }
66
+ }
67
+
68
+ // If neither a file nor a directory is found
69
+ return null
70
+ }