@tanstack/devtools-vite 0.3.5 → 0.3.7

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.
@@ -0,0 +1,343 @@
1
+ import { readFileSync, writeFileSync } from 'node:fs'
2
+ import { gen, parse, t, trav } from './babel'
3
+ import type { PluginInjection } from '@tanstack/devtools-client'
4
+ import type { types as Babel } from '@babel/core'
5
+ import type { ParseResult } from '@babel/parser'
6
+
7
+ /**
8
+ * Detects if a file imports TanStack devtools packages
9
+ * Handles: import X from '@tanstack/react-devtools'
10
+ * import * as X from '@tanstack/react-devtools'
11
+ * import { TanStackDevtools } from '@tanstack/react-devtools'
12
+ */
13
+ const detectDevtoolsImport = (code: string): boolean => {
14
+ const devtoolsPackages = [
15
+ '@tanstack/react-devtools',
16
+ '@tanstack/solid-devtools',
17
+ '@tanstack/vue-devtools',
18
+ '@tanstack/svelte-devtools',
19
+ '@tanstack/angular-devtools',
20
+ ]
21
+
22
+ try {
23
+ const ast = parse(code, {
24
+ sourceType: 'module',
25
+ plugins: ['jsx', 'typescript'],
26
+ })
27
+
28
+ let hasDevtoolsImport = false
29
+
30
+ trav(ast, {
31
+ ImportDeclaration(path) {
32
+ const importSource = path.node.source.value
33
+ if (devtoolsPackages.includes(importSource)) {
34
+ hasDevtoolsImport = true
35
+ path.stop()
36
+ }
37
+ },
38
+ })
39
+
40
+ return hasDevtoolsImport
41
+ } catch (e) {
42
+ return false
43
+ }
44
+ }
45
+
46
+ /**
47
+ * Finds the TanStackDevtools component name in the file
48
+ * Handles renamed imports and namespace imports
49
+ */
50
+ export const findDevtoolsComponentName = (
51
+ ast: ParseResult<Babel.File>,
52
+ ): string | null => {
53
+ let componentName: string | null = null
54
+ const devtoolsPackages = [
55
+ '@tanstack/react-devtools',
56
+ '@tanstack/solid-devtools',
57
+ '@tanstack/vue-devtools',
58
+ '@tanstack/svelte-devtools',
59
+ '@tanstack/angular-devtools',
60
+ ]
61
+
62
+ trav(ast, {
63
+ ImportDeclaration(path) {
64
+ const importSource = path.node.source.value
65
+ if (devtoolsPackages.includes(importSource)) {
66
+ // Check for: import { TanStackDevtools } from '@tanstack/...'
67
+ const namedImport = path.node.specifiers.find(
68
+ (spec) =>
69
+ t.isImportSpecifier(spec) &&
70
+ t.isIdentifier(spec.imported) &&
71
+ spec.imported.name === 'TanStackDevtools',
72
+ )
73
+ if (namedImport && t.isImportSpecifier(namedImport)) {
74
+ componentName = namedImport.local.name
75
+ path.stop()
76
+ return
77
+ }
78
+
79
+ // Check for: import * as DevtoolsName from '@tanstack/...'
80
+ const namespaceImport = path.node.specifiers.find((spec) =>
81
+ t.isImportNamespaceSpecifier(spec),
82
+ )
83
+ if (namespaceImport && t.isImportNamespaceSpecifier(namespaceImport)) {
84
+ // For namespace imports, we need to look for DevtoolsName.TanStackDevtools
85
+ componentName = `${namespaceImport.local.name}.TanStackDevtools`
86
+ path.stop()
87
+ return
88
+ }
89
+ }
90
+ },
91
+ })
92
+
93
+ return componentName
94
+ }
95
+
96
+ export const transformAndInject = (
97
+ ast: ParseResult<Babel.File>,
98
+ injection: PluginInjection,
99
+ devtoolsComponentName: string,
100
+ ) => {
101
+ let didTransform = false
102
+
103
+ // Use pluginImport if provided, otherwise generate from package name
104
+ const importName = injection.pluginImport?.importName
105
+ const pluginType = injection.pluginImport?.type || 'jsx'
106
+ const displayName = injection.pluginName
107
+
108
+ if (!importName) {
109
+ return false
110
+ }
111
+ // Handle namespace imports like DevtoolsModule.TanStackDevtools
112
+ const isNamespaceImport = devtoolsComponentName.includes('.')
113
+
114
+ // Find and modify the TanStackDevtools JSX element
115
+ trav(ast, {
116
+ JSXOpeningElement(path) {
117
+ const elementName = path.node.name
118
+ let matches = false
119
+
120
+ if (isNamespaceImport) {
121
+ // Handle <DevtoolsModule.TanStackDevtools />
122
+ if (t.isJSXMemberExpression(elementName)) {
123
+ const fullName = `${t.isJSXIdentifier(elementName.object) ? elementName.object.name : ''}.${t.isJSXIdentifier(elementName.property) ? elementName.property.name : ''}`
124
+ matches = fullName === devtoolsComponentName
125
+ }
126
+ } else {
127
+ // Handle <TanStackDevtools /> or <RenamedDevtools />
128
+ matches =
129
+ t.isJSXIdentifier(elementName) &&
130
+ elementName.name === devtoolsComponentName
131
+ }
132
+
133
+ if (matches) {
134
+ // Find the plugins prop
135
+ const pluginsProp = path.node.attributes.find(
136
+ (attr) =>
137
+ t.isJSXAttribute(attr) &&
138
+ t.isJSXIdentifier(attr.name) &&
139
+ attr.name.name === 'plugins',
140
+ )
141
+ // plugins found
142
+ if (pluginsProp && t.isJSXAttribute(pluginsProp)) {
143
+ // Check if plugins prop has a value
144
+ if (
145
+ pluginsProp.value &&
146
+ t.isJSXExpressionContainer(pluginsProp.value)
147
+ ) {
148
+ const expression = pluginsProp.value.expression
149
+
150
+ // If it's an array expression, add our plugin to it
151
+ if (t.isArrayExpression(expression)) {
152
+ // Check if plugin already exists
153
+ const pluginExists = expression.elements.some((element) => {
154
+ if (!element) return false
155
+
156
+ // For function-based plugins, check if the function call exists
157
+ if (pluginType === 'function') {
158
+ return (
159
+ t.isCallExpression(element) &&
160
+ t.isIdentifier(element.callee) &&
161
+ element.callee.name === importName
162
+ )
163
+ }
164
+
165
+ // For JSX plugins, check object with name property
166
+ if (!t.isObjectExpression(element)) return false
167
+
168
+ return element.properties.some((prop) => {
169
+ if (
170
+ !t.isObjectProperty(prop) ||
171
+ !t.isIdentifier(prop.key) ||
172
+ prop.key.name !== 'name'
173
+ ) {
174
+ return false
175
+ }
176
+
177
+ return (
178
+ t.isStringLiteral(prop.value) &&
179
+ prop.value.value === displayName
180
+ )
181
+ })
182
+ })
183
+
184
+ if (!pluginExists) {
185
+ // For function-based plugins, add them directly as function calls
186
+ // For JSX plugins, wrap them in objects with name and render
187
+ if (pluginType === 'function') {
188
+ // Add directly: FormDevtoolsPlugin()
189
+ expression.elements.push(
190
+ t.callExpression(t.identifier(importName), []),
191
+ )
192
+ } else {
193
+ // Add as object: { name: "...", render: <Component /> }
194
+ const renderValue = t.jsxElement(
195
+ t.jsxOpeningElement(t.jsxIdentifier(importName), [], true),
196
+ null,
197
+ [],
198
+ true,
199
+ )
200
+
201
+ expression.elements.push(
202
+ t.objectExpression([
203
+ t.objectProperty(
204
+ t.identifier('name'),
205
+ t.stringLiteral(displayName),
206
+ ),
207
+ t.objectProperty(t.identifier('render'), renderValue),
208
+ ]),
209
+ )
210
+ }
211
+
212
+ didTransform = true
213
+ }
214
+ }
215
+ }
216
+ } else {
217
+ // No plugins prop exists, create one with our plugin
218
+ // For function-based plugins, add them directly as function calls
219
+ // For JSX plugins, wrap them in objects with name and render
220
+ let pluginElement
221
+ if (pluginType === 'function') {
222
+ // Add directly: plugins={[FormDevtoolsPlugin()]}
223
+ pluginElement = t.callExpression(t.identifier(importName), [])
224
+ } else {
225
+ // Add as object: plugins={[{ name: "...", render: <Component /> }]}
226
+ const renderValue = t.jsxElement(
227
+ t.jsxOpeningElement(t.jsxIdentifier(importName), [], true),
228
+ null,
229
+ [],
230
+ true,
231
+ )
232
+
233
+ pluginElement = t.objectExpression([
234
+ t.objectProperty(
235
+ t.identifier('name'),
236
+ t.stringLiteral(displayName),
237
+ ),
238
+ t.objectProperty(t.identifier('render'), renderValue),
239
+ ])
240
+ }
241
+
242
+ path.node.attributes.push(
243
+ t.jsxAttribute(
244
+ t.jsxIdentifier('plugins'),
245
+ t.jsxExpressionContainer(t.arrayExpression([pluginElement])),
246
+ ),
247
+ )
248
+
249
+ didTransform = true
250
+ }
251
+ }
252
+ },
253
+ })
254
+
255
+ // Add import at the top of the file if transform happened
256
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
257
+ if (didTransform) {
258
+ const importDeclaration = t.importDeclaration(
259
+ [t.importSpecifier(t.identifier(importName), t.identifier(importName))],
260
+ t.stringLiteral(injection.packageName),
261
+ )
262
+
263
+ // Find the last import declaration
264
+ let lastImportIndex = -1
265
+ ast.program.body.forEach((node, index) => {
266
+ if (t.isImportDeclaration(node)) {
267
+ lastImportIndex = index
268
+ }
269
+ })
270
+
271
+ // Insert after the last import or at the beginning
272
+ ast.program.body.splice(lastImportIndex + 1, 0, importDeclaration)
273
+ }
274
+
275
+ return didTransform
276
+ }
277
+
278
+ /**
279
+ * Detects if a file contains TanStack devtools import
280
+ */
281
+ export function detectDevtoolsFile(code: string): boolean {
282
+ return detectDevtoolsImport(code)
283
+ }
284
+
285
+ /**
286
+ * Injects a plugin into the TanStackDevtools component in a file
287
+ * Reads the file, transforms it, and writes it back
288
+ */
289
+ export function injectPluginIntoFile(
290
+ filePath: string,
291
+ injection: PluginInjection,
292
+ ): { success: boolean; error?: string } {
293
+ try {
294
+ // Read the file
295
+ const code = readFileSync(filePath, 'utf-8')
296
+
297
+ // Parse the code
298
+ const ast = parse(code, {
299
+ sourceType: 'module',
300
+ plugins: ['jsx', 'typescript'],
301
+ })
302
+
303
+ // Find the devtools component name (handles renamed imports)
304
+ const devtoolsComponentName = findDevtoolsComponentName(ast)
305
+ if (!devtoolsComponentName) {
306
+ return {
307
+ success: false,
308
+ error: 'Could not find TanStackDevtools import',
309
+ }
310
+ }
311
+
312
+ // Transform and inject
313
+ const didTransform = transformAndInject(
314
+ ast,
315
+ injection,
316
+ devtoolsComponentName,
317
+ )
318
+
319
+ if (!didTransform) {
320
+ return {
321
+ success: false,
322
+ error: 'Plugin already exists or no TanStackDevtools component found',
323
+ }
324
+ }
325
+
326
+ // Generate the new code
327
+ const result = gen(ast, {
328
+ sourceMaps: false,
329
+ retainLines: false,
330
+ })
331
+
332
+ // Write back to file
333
+ writeFileSync(filePath, result.code, 'utf-8')
334
+
335
+ return { success: true }
336
+ } catch (e) {
337
+ console.error('Error injecting plugin:', e)
338
+ return {
339
+ success: false,
340
+ error: e instanceof Error ? e.message : 'Unknown error',
341
+ }
342
+ }
343
+ }
@@ -0,0 +1,181 @@
1
+ import { exec } from 'node:child_process'
2
+ import { existsSync } from 'node:fs'
3
+ import { join } from 'node:path'
4
+ import { devtoolsEventClient } from '@tanstack/devtools-client'
5
+ import chalk from 'chalk'
6
+ import { readPackageJson, tryParseJson } from './utils'
7
+ import { injectPluginIntoFile } from './inject-plugin'
8
+ import type { OutdatedDeps } from '@tanstack/devtools-client'
9
+
10
+ /**
11
+ * Gets the outdated command for the detected package manager
12
+ */
13
+ const getOutdatedCommand = (packageManager: string): string => {
14
+ switch (packageManager) {
15
+ case 'yarn':
16
+ return 'yarn outdated --json'
17
+ case 'pnpm':
18
+ return 'pnpm outdated --format json'
19
+ case 'bun':
20
+ return 'bun outdated --json'
21
+ case 'npm':
22
+ default:
23
+ return 'npm outdated --json'
24
+ }
25
+ }
26
+
27
+ /**
28
+ * Adds a plugin to the devtools configuration file
29
+ */
30
+ export const addPluginToDevtools = (
31
+ devtoolsFileId: string | null,
32
+ packageName: string,
33
+ pluginName: string,
34
+ pluginImport?: { importName: string; type: 'jsx' | 'function' },
35
+ ): { success: boolean; error?: string } => {
36
+ // Check if we found the devtools file
37
+ if (!devtoolsFileId) {
38
+ const error = 'Devtools file not found'
39
+ console.log(
40
+ chalk.yellowBright(
41
+ `[@tanstack/devtools-vite] Could not add plugin. ${error}.`,
42
+ ),
43
+ )
44
+ return { success: false, error }
45
+ }
46
+
47
+ // Inject the plugin into the file
48
+ const result = injectPluginIntoFile(devtoolsFileId, {
49
+ packageName,
50
+ pluginName,
51
+ pluginImport,
52
+ })
53
+
54
+ if (result.success) {
55
+ console.log(
56
+ chalk.greenBright(
57
+ `[@tanstack/devtools-vite] Successfully added ${packageName} to devtools!`,
58
+ ),
59
+ )
60
+ } else {
61
+ console.log(
62
+ chalk.yellowBright(
63
+ `[@tanstack/devtools-vite] Could not add plugin: ${result.error}`,
64
+ ),
65
+ )
66
+ }
67
+
68
+ return result
69
+ }
70
+ /**
71
+ * Gets the install command for the detected package manager
72
+ */
73
+ const getInstallCommand = (
74
+ packageManager: string,
75
+ packageName: string,
76
+ ): string => {
77
+ switch (packageManager) {
78
+ case 'yarn':
79
+ return `yarn add -D ${packageName}`
80
+ case 'pnpm':
81
+ return `pnpm add -D ${packageName}`
82
+ case 'bun':
83
+ return `bun add -D ${packageName}`
84
+ case 'npm':
85
+ default:
86
+ return `npm install -D ${packageName}`
87
+ }
88
+ }
89
+
90
+ export const installPackage = async (
91
+ packageName: string,
92
+ ): Promise<{
93
+ success: boolean
94
+ error?: string
95
+ }> => {
96
+ return new Promise((resolve) => {
97
+ const packageManager = detectPackageManager()
98
+ const installCommand = getInstallCommand(packageManager, packageName)
99
+
100
+ console.log(
101
+ chalk.blueBright(
102
+ `[@tanstack/devtools-vite] Installing ${packageName}...`,
103
+ ),
104
+ )
105
+
106
+ exec(installCommand, async (installError) => {
107
+ if (installError) {
108
+ console.error(
109
+ chalk.redBright(
110
+ `[@tanstack/devtools-vite] Failed to install ${packageName}:`,
111
+ ),
112
+ installError.message,
113
+ )
114
+ resolve({
115
+ success: false,
116
+ error: installError.message,
117
+ })
118
+ return
119
+ }
120
+
121
+ console.log(
122
+ chalk.greenBright(
123
+ `[@tanstack/devtools-vite] Successfully installed ${packageName}`,
124
+ ),
125
+ )
126
+
127
+ // Read the updated package.json and emit the event
128
+ const updatedPackageJson = await readPackageJson()
129
+ devtoolsEventClient.emit('package-json-updated', {
130
+ packageJson: updatedPackageJson,
131
+ })
132
+
133
+ resolve({ success: true })
134
+ })
135
+ })
136
+ }
137
+
138
+ /**
139
+ * Detects the package manager used in the project by checking for lock files
140
+ */
141
+ const detectPackageManager = (): 'npm' | 'yarn' | 'pnpm' | 'bun' => {
142
+ const cwd = process.cwd()
143
+
144
+ // Check for lock files in order of specificity
145
+ if (existsSync(join(cwd, 'bun.lockb')) || existsSync(join(cwd, 'bun.lock'))) {
146
+ return 'bun'
147
+ }
148
+ if (existsSync(join(cwd, 'pnpm-lock.yaml'))) {
149
+ return 'pnpm'
150
+ }
151
+ if (existsSync(join(cwd, 'yarn.lock'))) {
152
+ return 'yarn'
153
+ }
154
+ if (existsSync(join(cwd, 'package-lock.json'))) {
155
+ return 'npm'
156
+ }
157
+
158
+ // Default to pnpm if no lock file is found
159
+ return 'pnpm'
160
+ }
161
+
162
+ export const emitOutdatedDeps = async () => {
163
+ return await new Promise<OutdatedDeps | null>((resolve) => {
164
+ const packageManager = detectPackageManager()
165
+ const outdatedCommand = getOutdatedCommand(packageManager)
166
+
167
+ exec(outdatedCommand, (_, stdout) => {
168
+ // outdated commands exit with code 1 if there are outdated packages, but still output valid JSON
169
+ if (stdout) {
170
+ const newOutdatedDeps = tryParseJson<OutdatedDeps>(stdout)
171
+ if (!newOutdatedDeps) {
172
+ return
173
+ }
174
+ devtoolsEventClient.emit('outdated-deps-read', {
175
+ outdatedDeps: newOutdatedDeps,
176
+ })
177
+ resolve(newOutdatedDeps)
178
+ }
179
+ })
180
+ })
181
+ }