@tanstack/devtools-vite 0.3.1 → 0.3.2

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.
@@ -4,9 +4,56 @@ const isTanStackDevtoolsImport = (source) => source === "@tanstack/react-devtool
4
4
  const getImportedNames = (importDecl) => {
5
5
  return importDecl.specifiers.map((spec) => spec.local.name);
6
6
  };
7
+ const getLeftoverImports = (node) => {
8
+ const finalReferences = [];
9
+ node.traverse({
10
+ JSXAttribute(path) {
11
+ const node2 = path.node;
12
+ const propName = typeof node2.name.name === "string" ? node2.name.name : node2.name.name.name;
13
+ if (propName === "plugins" && node2.value?.type === "JSXExpressionContainer" && node2.value.expression.type === "ArrayExpression") {
14
+ const elements = node2.value.expression.elements;
15
+ elements.forEach((el) => {
16
+ if (el?.type === "ObjectExpression") {
17
+ const props = el.properties;
18
+ const referencesToRemove = props.map((prop) => {
19
+ if (prop.type === "ObjectProperty" && prop.key.type === "Identifier" && prop.key.name === "render") {
20
+ const value = prop.value;
21
+ if (value.type === "JSXElement" && value.openingElement.name.type === "JSXIdentifier") {
22
+ const elementName = value.openingElement.name.name;
23
+ return elementName;
24
+ }
25
+ if (value.type === "ArrowFunctionExpression" || value.type === "FunctionExpression") {
26
+ const body = value.body;
27
+ if (body.type === "JSXElement" && body.openingElement.name.type === "JSXIdentifier") {
28
+ const elementName = body.openingElement.name.name;
29
+ return elementName;
30
+ }
31
+ }
32
+ if (value.type === "Identifier") {
33
+ const elementName = value.name;
34
+ return elementName;
35
+ }
36
+ if (value.type === "CallExpression" && value.callee.type === "Identifier") {
37
+ const elementName = value.callee.name;
38
+ return elementName;
39
+ }
40
+ return "";
41
+ }
42
+ return "";
43
+ }).filter(Boolean);
44
+ finalReferences.push(...referencesToRemove);
45
+ }
46
+ });
47
+ }
48
+ }
49
+ });
50
+ return finalReferences;
51
+ };
7
52
  const transform = (ast) => {
8
53
  let didTransform = false;
9
54
  const devtoolsComponentNames = /* @__PURE__ */ new Set();
55
+ const finalReferences = [];
56
+ const transformations = [];
10
57
  trav(ast, {
11
58
  ImportDeclaration(path) {
12
59
  const importSource = path.node.source.value;
@@ -14,22 +61,52 @@ const transform = (ast) => {
14
61
  getImportedNames(path.node).forEach(
15
62
  (name) => devtoolsComponentNames.add(name)
16
63
  );
17
- path.remove();
64
+ transformations.push(() => {
65
+ path.remove();
66
+ });
18
67
  didTransform = true;
19
68
  }
20
69
  },
21
70
  JSXElement(path) {
22
71
  const opening = path.node.openingElement;
23
72
  if (opening.name.type === "JSXIdentifier" && devtoolsComponentNames.has(opening.name.name)) {
24
- path.remove();
73
+ const refs = getLeftoverImports(path);
74
+ finalReferences.push(...refs);
75
+ transformations.push(() => {
76
+ path.remove();
77
+ });
25
78
  didTransform = true;
26
79
  }
27
80
  if (opening.name.type === "JSXMemberExpression" && opening.name.object.type === "JSXIdentifier" && devtoolsComponentNames.has(opening.name.object.name)) {
28
- path.remove();
81
+ const refs = getLeftoverImports(path);
82
+ finalReferences.push(...refs);
83
+ transformations.push(() => {
84
+ path.remove();
85
+ });
29
86
  didTransform = true;
30
87
  }
31
88
  }
32
89
  });
90
+ trav(ast, {
91
+ ImportDeclaration(path) {
92
+ const imports = path.node.specifiers;
93
+ for (const imported of imports) {
94
+ if (imported.type === "ImportSpecifier") {
95
+ if (finalReferences.includes(imported.local.name)) {
96
+ transformations.push(() => {
97
+ path.node.specifiers = path.node.specifiers.filter(
98
+ (spec) => spec !== imported
99
+ );
100
+ if (path.node.specifiers.length === 0) {
101
+ path.remove();
102
+ }
103
+ });
104
+ }
105
+ }
106
+ }
107
+ }
108
+ });
109
+ transformations.forEach((fn) => fn());
33
110
  return didTransform;
34
111
  };
35
112
  function removeDevtools(code, id) {
@@ -45,6 +122,7 @@ function removeDevtools(code, id) {
45
122
  }
46
123
  return gen(ast, {
47
124
  sourceMaps: true,
125
+ retainLines: true,
48
126
  filename: id,
49
127
  sourceFileName: filePath
50
128
  });
@@ -1 +1 @@
1
- {"version":3,"file":"remove-devtools.js","sources":["../../src/remove-devtools.ts"],"sourcesContent":["import { gen, parse, trav } from './babel'\nimport type { t } from './babel'\nimport type { types as Babel } from '@babel/core'\nimport type { ParseResult } from '@babel/parser'\n\nconst isTanStackDevtoolsImport = (source: string) =>\n source === '@tanstack/react-devtools' ||\n source === '@tanstack/devtools' ||\n source === '@tanstack/solid-devtools'\n\nconst getImportedNames = (importDecl: t.ImportDeclaration) => {\n return importDecl.specifiers.map((spec) => spec.local.name)\n}\n\nconst transform = (ast: ParseResult<Babel.File>) => {\n let didTransform = false\n const devtoolsComponentNames = new Set()\n\n trav(ast, {\n ImportDeclaration(path) {\n const importSource = path.node.source.value\n if (isTanStackDevtoolsImport(importSource)) {\n getImportedNames(path.node).forEach((name) =>\n devtoolsComponentNames.add(name),\n )\n path.remove()\n didTransform = true\n }\n },\n JSXElement(path) {\n const opening = path.node.openingElement\n if (\n opening.name.type === 'JSXIdentifier' &&\n devtoolsComponentNames.has(opening.name.name)\n ) {\n path.remove()\n didTransform = true\n }\n\n if (\n opening.name.type === 'JSXMemberExpression' &&\n opening.name.object.type === 'JSXIdentifier' &&\n devtoolsComponentNames.has(opening.name.object.name)\n ) {\n path.remove()\n didTransform = true\n }\n },\n })\n\n return didTransform\n}\n\nexport function removeDevtools(code: string, id: string) {\n const [filePath] = id.split('?')\n\n try {\n const ast = parse(code, {\n sourceType: 'module',\n plugins: ['jsx', 'typescript'],\n })\n const didTransform = transform(ast)\n if (!didTransform) {\n return { code }\n }\n return gen(ast, {\n sourceMaps: true,\n filename: id,\n sourceFileName: filePath,\n })\n } catch (e) {\n return { code }\n }\n}\n"],"names":[],"mappings":";;AAKA,MAAM,2BAA2B,CAAC,WAChC,WAAW,8BACX,WAAW,wBACX,WAAW;AAEb,MAAM,mBAAmB,CAAC,eAAoC;AAC5D,SAAO,WAAW,WAAW,IAAI,CAAC,SAAS,KAAK,MAAM,IAAI;AAC5D;AAEA,MAAM,YAAY,CAAC,QAAiC;AAClD,MAAI,eAAe;AACnB,QAAM,6CAA6B,IAAA;AAEnC,OAAK,KAAK;AAAA,IACR,kBAAkB,MAAM;AACtB,YAAM,eAAe,KAAK,KAAK,OAAO;AACtC,UAAI,yBAAyB,YAAY,GAAG;AAC1C,yBAAiB,KAAK,IAAI,EAAE;AAAA,UAAQ,CAAC,SACnC,uBAAuB,IAAI,IAAI;AAAA,QAAA;AAEjC,aAAK,OAAA;AACL,uBAAe;AAAA,MACjB;AAAA,IACF;AAAA,IACA,WAAW,MAAM;AACf,YAAM,UAAU,KAAK,KAAK;AAC1B,UACE,QAAQ,KAAK,SAAS,mBACtB,uBAAuB,IAAI,QAAQ,KAAK,IAAI,GAC5C;AACA,aAAK,OAAA;AACL,uBAAe;AAAA,MACjB;AAEA,UACE,QAAQ,KAAK,SAAS,yBACtB,QAAQ,KAAK,OAAO,SAAS,mBAC7B,uBAAuB,IAAI,QAAQ,KAAK,OAAO,IAAI,GACnD;AACA,aAAK,OAAA;AACL,uBAAe;AAAA,MACjB;AAAA,IACF;AAAA,EAAA,CACD;AAED,SAAO;AACT;AAEO,SAAS,eAAe,MAAc,IAAY;AACvD,QAAM,CAAC,QAAQ,IAAI,GAAG,MAAM,GAAG;AAE/B,MAAI;AACF,UAAM,MAAM,MAAM,MAAM;AAAA,MACtB,YAAY;AAAA,MACZ,SAAS,CAAC,OAAO,YAAY;AAAA,IAAA,CAC9B;AACD,UAAM,eAAe,UAAU,GAAG;AAClC,QAAI,CAAC,cAAc;AACjB,aAAO,EAAE,KAAA;AAAA,IACX;AACA,WAAO,IAAI,KAAK;AAAA,MACd,YAAY;AAAA,MACZ,UAAU;AAAA,MACV,gBAAgB;AAAA,IAAA,CACjB;AAAA,EACH,SAAS,GAAG;AACV,WAAO,EAAE,KAAA;AAAA,EACX;AACF;"}
1
+ {"version":3,"file":"remove-devtools.js","sources":["../../src/remove-devtools.ts"],"sourcesContent":["import { gen, parse, trav } from './babel'\nimport type { t } from './babel'\nimport type { types as Babel, NodePath } from '@babel/core'\nimport type { ParseResult } from '@babel/parser'\n\nconst isTanStackDevtoolsImport = (source: string) =>\n source === '@tanstack/react-devtools' ||\n source === '@tanstack/devtools' ||\n source === '@tanstack/solid-devtools'\n\nconst getImportedNames = (importDecl: t.ImportDeclaration) => {\n return importDecl.specifiers.map((spec) => spec.local.name)\n}\n\nconst getLeftoverImports = (node: NodePath<t.JSXElement>) => {\n const finalReferences: Array<string> = []\n node.traverse({\n JSXAttribute(path) {\n const node = path.node\n const propName =\n typeof node.name.name === 'string'\n ? node.name.name\n : node.name.name.name\n\n if (\n propName === 'plugins' &&\n node.value?.type === 'JSXExpressionContainer' &&\n node.value.expression.type === 'ArrayExpression'\n ) {\n const elements = node.value.expression.elements\n\n elements.forEach((el) => {\n if (el?.type === 'ObjectExpression') {\n // { name: \"something\", render: ()=> <Component /> }\n const props = el.properties\n const referencesToRemove = props\n .map((prop) => {\n if (\n prop.type === 'ObjectProperty' &&\n prop.key.type === 'Identifier' &&\n prop.key.name === 'render'\n ) {\n const value = prop.value\n // handle <ReactRouterPanel />\n if (\n value.type === 'JSXElement' &&\n value.openingElement.name.type === 'JSXIdentifier'\n ) {\n const elementName = value.openingElement.name.name\n return elementName\n }\n // handle () => <ReactRouterPanel /> or function() { return <ReactRouterPanel /> }\n if (\n value.type === 'ArrowFunctionExpression' ||\n value.type === 'FunctionExpression'\n ) {\n const body = value.body\n if (\n body.type === 'JSXElement' &&\n body.openingElement.name.type === 'JSXIdentifier'\n ) {\n const elementName = body.openingElement.name.name\n return elementName\n }\n }\n // handle render: SomeComponent\n if (value.type === 'Identifier') {\n const elementName = value.name\n return elementName\n }\n\n // handle render: someFunction()\n if (\n value.type === 'CallExpression' &&\n value.callee.type === 'Identifier'\n ) {\n const elementName = value.callee.name\n return elementName\n }\n\n return ''\n }\n return ''\n })\n .filter(Boolean)\n finalReferences.push(...referencesToRemove)\n }\n })\n }\n },\n })\n return finalReferences\n}\n\nconst transform = (ast: ParseResult<Babel.File>) => {\n let didTransform = false\n const devtoolsComponentNames = new Set()\n const finalReferences: Array<string> = []\n\n const transformations: Array<() => void> = []\n\n trav(ast, {\n ImportDeclaration(path) {\n const importSource = path.node.source.value\n if (isTanStackDevtoolsImport(importSource)) {\n getImportedNames(path.node).forEach((name) =>\n devtoolsComponentNames.add(name),\n )\n\n transformations.push(() => {\n path.remove()\n })\n\n didTransform = true\n }\n },\n JSXElement(path) {\n const opening = path.node.openingElement\n if (\n opening.name.type === 'JSXIdentifier' &&\n devtoolsComponentNames.has(opening.name.name)\n ) {\n const refs = getLeftoverImports(path)\n\n finalReferences.push(...refs)\n transformations.push(() => {\n path.remove()\n })\n didTransform = true\n }\n if (\n opening.name.type === 'JSXMemberExpression' &&\n opening.name.object.type === 'JSXIdentifier' &&\n devtoolsComponentNames.has(opening.name.object.name)\n ) {\n const refs = getLeftoverImports(path)\n finalReferences.push(...refs)\n transformations.push(() => {\n path.remove()\n })\n didTransform = true\n }\n },\n })\n\n trav(ast, {\n ImportDeclaration(path) {\n const imports = path.node.specifiers\n for (const imported of imports) {\n if (imported.type === 'ImportSpecifier') {\n if (finalReferences.includes(imported.local.name)) {\n transformations.push(() => {\n // remove the specifier\n path.node.specifiers = path.node.specifiers.filter(\n (spec) => spec !== imported,\n )\n // remove whole import if nothing is left\n if (path.node.specifiers.length === 0) {\n path.remove()\n }\n })\n }\n }\n }\n },\n })\n\n transformations.forEach((fn) => fn())\n\n return didTransform\n}\n\nexport function removeDevtools(code: string, id: string) {\n const [filePath] = id.split('?')\n\n try {\n const ast = parse(code, {\n sourceType: 'module',\n plugins: ['jsx', 'typescript'],\n })\n const didTransform = transform(ast)\n if (!didTransform) {\n return { code }\n }\n return gen(ast, {\n sourceMaps: true,\n retainLines: true,\n filename: id,\n sourceFileName: filePath,\n })\n } catch (e) {\n return { code }\n }\n}\n"],"names":["node"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanstack/devtools-vite",
3
- "version": "0.3.1",
3
+ "version": "0.3.2",
4
4
  "description": "TanStack Vite plugin used to enhance the core devtools with additional functionalities",
5
5
  "author": "Tanner Linsley",
6
6
  "license": "MIT",
@@ -57,22 +57,20 @@ export default function DevtoolsExample() {
57
57
  )
58
58
  expect(output).toBe(
59
59
  removeEmptySpace(`
60
- import { ReactQueryDevtoolsPanel } from '@tanstack/react-query-devtools';
61
- import { TanStackRouterDevtoolsPanel } from '@tanstack/react-router-devtools';
62
- import {
60
+ import {
63
61
  Link,
64
62
  Outlet,
65
63
  RouterProvider,
66
64
  createRootRoute,
67
65
  createRoute,
68
66
  createRouter
69
- } from '@tanstack/react-router';
67
+ } from '@tanstack/react-router';
70
68
 
71
69
 
72
70
  export default function DevtoolsExample() {
73
- return <>
71
+ return (<>
74
72
  <RouterProvider router={router} />
75
- </>;
73
+ </>);
76
74
 
77
75
  }
78
76
 
@@ -131,9 +129,7 @@ export default function DevtoolsExample() {
131
129
  )
132
130
  expect(output).toBe(
133
131
  removeEmptySpace(`
134
- import { ReactQueryDevtoolsPanel } from '@tanstack/react-query-devtools';
135
- import { TanStackRouterDevtoolsPanel } from '@tanstack/react-router-devtools';
136
- import {
132
+ import {
137
133
  Link,
138
134
  Outlet,
139
135
  RouterProvider,
@@ -144,9 +140,9 @@ export default function DevtoolsExample() {
144
140
 
145
141
 
146
142
  export default function DevtoolsExample() {
147
- return <>
143
+ return ( <>
148
144
  <RouterProvider router={router} />
149
- </>;
145
+ </>);
150
146
 
151
147
  }
152
148
 
@@ -205,9 +201,7 @@ export default function DevtoolsExample() {
205
201
  )
206
202
  expect(output).toBe(
207
203
  removeEmptySpace(`
208
- import { ReactQueryDevtoolsPanel } from '@tanstack/react-query-devtools';
209
- import { TanStackRouterDevtoolsPanel } from '@tanstack/react-router-devtools';
210
- import {
204
+ import {
211
205
  Link,
212
206
  Outlet,
213
207
  RouterProvider,
@@ -218,13 +212,350 @@ export default function DevtoolsExample() {
218
212
 
219
213
 
220
214
  export default function DevtoolsExample() {
221
- return <>
215
+ return (<>
222
216
  <RouterProvider router={router} />
223
- </>;
217
+ </>);
224
218
 
225
219
  }
226
220
 
227
221
  `),
228
222
  )
229
223
  })
224
+
225
+ test('it removes devtools and all possible variations of the plugins', () => {
226
+ const output = removeEmptySpace(
227
+ removeDevtools(
228
+ `
229
+ import { ReactQueryDevtoolsPanel } from '@tanstack/react-query-devtools'
230
+ import { TanStackRouterDevtoolsPanel } from '@tanstack/react-router-devtools'
231
+ import {
232
+ Link,
233
+ Outlet,
234
+ RouterProvider,
235
+ createRootRoute,
236
+ createRoute,
237
+ createRouter,
238
+ } from '@tanstack/react-router'
239
+ import * as Tools from '@tanstack/react-devtools'
240
+
241
+
242
+
243
+ export default function DevtoolsExample() {
244
+ return (
245
+ <>
246
+ <Tools.TanStackDevtools
247
+ eventBusConfig={{
248
+ connectToServerBus: true,
249
+ }}
250
+ plugins={[
251
+ {
252
+ name: 'TanStack Query',
253
+ render: <ReactQueryDevtoolsPanel />,
254
+ },
255
+ {
256
+ name: 'TanStack Query',
257
+ render: () => <ReactQueryDevtoolsPanel />,
258
+ },
259
+ {
260
+ name: 'TanStack Router',
261
+ render: TanStackRouterDevtoolsPanel,
262
+ },
263
+ some()
264
+ ]}
265
+ />
266
+ <RouterProvider router={router} />
267
+ </>
268
+ )
269
+ }`,
270
+ 'test.jsx',
271
+ ).code,
272
+ )
273
+
274
+ expect(output).toBe(
275
+ removeEmptySpace(`
276
+ import {
277
+ Link,
278
+ Outlet,
279
+ RouterProvider,
280
+ createRootRoute,
281
+ createRoute,
282
+ createRouter
283
+ } from '@tanstack/react-router' ;
284
+
285
+
286
+
287
+ export default function DevtoolsExample() {
288
+ return (
289
+ <>
290
+ <RouterProvider router={router} />
291
+ </>
292
+ );
293
+ }
294
+ `),
295
+ )
296
+ })
297
+
298
+ describe('removing plugin imports', () => {
299
+ test('it removes the plugin import from the import array if multiple import identifiers exist', () => {
300
+ const output = removeEmptySpace(
301
+ removeDevtools(
302
+ `
303
+ import { ReactQueryDevtoolsPanel, test } from '@tanstack/react-query-devtools'
304
+
305
+ import * as Tools from '@tanstack/react-devtools'
306
+
307
+
308
+
309
+ export default function DevtoolsExample() {
310
+ return (
311
+ <>
312
+ <Tools.TanStackDevtools
313
+ eventBusConfig={{
314
+ connectToServerBus: true,
315
+ }}
316
+ plugins={[
317
+ {
318
+ name: 'TanStack Query',
319
+ render: <ReactQueryDevtoolsPanel />,
320
+ }
321
+ ]}
322
+ />
323
+ <RouterProvider router={router} />
324
+ </>
325
+ )
326
+ }`,
327
+ 'test.jsx',
328
+ ).code,
329
+ )
330
+
331
+ expect(output).toBe(
332
+ removeEmptySpace(`
333
+ import { test } from '@tanstack/react-query-devtools';
334
+
335
+ export default function DevtoolsExample() {
336
+ return (
337
+ <>
338
+ <RouterProvider router={router} />
339
+ </>
340
+ );
341
+ }
342
+ `),
343
+ )
344
+ })
345
+
346
+ test("it doesn't remove the whole import if imported with * as", () => {
347
+ const output = removeEmptySpace(
348
+ removeDevtools(
349
+ `
350
+ import * as Stuff from '@tanstack/react-query-devtools'
351
+
352
+ import * as Tools from '@tanstack/react-devtools'
353
+
354
+
355
+
356
+ export default function DevtoolsExample() {
357
+ return (
358
+ <>
359
+ <Tools.TanStackDevtools
360
+ eventBusConfig={{
361
+ connectToServerBus: true,
362
+ }}
363
+ plugins={[
364
+ {
365
+ name: 'TanStack Query',
366
+ render: <Stuff.ReactQueryDevtoolsPanel />,
367
+ }
368
+ ]}
369
+ />
370
+ <RouterProvider router={router} />
371
+ </>
372
+ )
373
+ }`,
374
+ 'test.jsx',
375
+ ).code,
376
+ )
377
+
378
+ expect(output).toBe(
379
+ removeEmptySpace(`
380
+ import * as Stuff from '@tanstack/react-query-devtools';
381
+
382
+ export default function DevtoolsExample() {
383
+ return (
384
+ <>
385
+ <RouterProvider router={router} />
386
+ </>
387
+ );
388
+ }
389
+ `),
390
+ )
391
+ })
392
+
393
+ test('it removes the import completely if nothing is left', () => {
394
+ const output = removeEmptySpace(
395
+ removeDevtools(
396
+ `
397
+ import { ReactQueryDevtoolsPanel } from '@tanstack/react-query-devtools'
398
+ import * as Tools from '@tanstack/react-devtools'
399
+
400
+ export default function DevtoolsExample() {
401
+ return (
402
+ <>
403
+ <Tools.TanStackDevtools
404
+ eventBusConfig={{
405
+ connectToServerBus: true,
406
+ }}
407
+ plugins={[
408
+ {
409
+ name: 'TanStack Query',
410
+ render: <ReactQueryDevtoolsPanel />,
411
+ }
412
+ ]}
413
+ />
414
+ <RouterProvider router={router} />
415
+ </>
416
+ )
417
+ }`,
418
+ 'test.jsx',
419
+ ).code,
420
+ )
421
+
422
+ expect(output).toBe(
423
+ removeEmptySpace(`
424
+ export default function DevtoolsExample() {
425
+ return (
426
+ <>
427
+ <RouterProvider router={router} />
428
+ </>
429
+ );
430
+ }
431
+ `),
432
+ )
433
+ })
434
+
435
+ test('it removes the import completely even if used as a function instead of jsx', () => {
436
+ const output = removeEmptySpace(
437
+ removeDevtools(
438
+ `
439
+ import { plugin } from '@tanstack/react-query-devtools'
440
+ import * as Tools from '@tanstack/react-devtools'
441
+
442
+ export default function DevtoolsExample() {
443
+ return (
444
+ <>
445
+ <Tools.TanStackDevtools
446
+ eventBusConfig={{
447
+ connectToServerBus: true,
448
+ }}
449
+ plugins={[
450
+ {
451
+ name: 'TanStack Query',
452
+ render: plugin()
453
+ }
454
+ ]}
455
+ />
456
+ <RouterProvider router={router} />
457
+ </>
458
+ )
459
+ }`,
460
+ 'test.jsx',
461
+ ).code,
462
+ )
463
+
464
+ expect(output).toBe(
465
+ removeEmptySpace(`
466
+ export default function DevtoolsExample() {
467
+ return (
468
+ <>
469
+ <RouterProvider router={router} />
470
+ </>
471
+ );
472
+ }
473
+ `),
474
+ )
475
+ })
476
+
477
+ test('it removes the import completely even if used as a function inside of render', () => {
478
+ const output = removeEmptySpace(
479
+ removeDevtools(
480
+ `
481
+ import { ReactQueryDevtoolsPanel } from '@tanstack/react-query-devtools'
482
+ import * as Tools from '@tanstack/react-devtools'
483
+
484
+ export default function DevtoolsExample() {
485
+ return (
486
+ <>
487
+ <Tools.TanStackDevtools
488
+ eventBusConfig={{
489
+ connectToServerBus: true,
490
+ }}
491
+ plugins={[
492
+ {
493
+ name: 'TanStack Query',
494
+ render: () => <ReactQueryDevtoolsPanel />
495
+ }
496
+ ]}
497
+ />
498
+ <RouterProvider router={router} />
499
+ </>
500
+ )
501
+ }`,
502
+ 'test.jsx',
503
+ ).code,
504
+ )
505
+
506
+ expect(output).toBe(
507
+ removeEmptySpace(`
508
+ export default function DevtoolsExample() {
509
+ return (
510
+ <>
511
+ <RouterProvider router={router} />
512
+ </>
513
+ );
514
+ }
515
+ `),
516
+ )
517
+ })
518
+
519
+ test('it removes the import completely even if used as a reference inside of render', () => {
520
+ const output = removeEmptySpace(
521
+ removeDevtools(
522
+ `
523
+ import { ReactQueryDevtoolsPanel } from '@tanstack/react-query-devtools'
524
+ import * as Tools from '@tanstack/react-devtools'
525
+
526
+ export default function DevtoolsExample() {
527
+ return (
528
+ <>
529
+ <Tools.TanStackDevtools
530
+ eventBusConfig={{
531
+ connectToServerBus: true,
532
+ }}
533
+ plugins={[
534
+ {
535
+ name: 'TanStack Query',
536
+ render: ReactQueryDevtoolsPanel
537
+ }
538
+ ]}
539
+ />
540
+ <RouterProvider router={router} />
541
+ </>
542
+ )
543
+ }`,
544
+ 'test.jsx',
545
+ ).code,
546
+ )
547
+
548
+ expect(output).toBe(
549
+ removeEmptySpace(`
550
+ export default function DevtoolsExample() {
551
+ return (
552
+ <>
553
+ <RouterProvider router={router} />
554
+ </>
555
+ );
556
+ }
557
+ `),
558
+ )
559
+ })
560
+ })
230
561
  })
@@ -1,6 +1,6 @@
1
1
  import { gen, parse, trav } from './babel'
2
2
  import type { t } from './babel'
3
- import type { types as Babel } from '@babel/core'
3
+ import type { types as Babel, NodePath } from '@babel/core'
4
4
  import type { ParseResult } from '@babel/parser'
5
5
 
6
6
  const isTanStackDevtoolsImport = (source: string) =>
@@ -12,9 +12,92 @@ const getImportedNames = (importDecl: t.ImportDeclaration) => {
12
12
  return importDecl.specifiers.map((spec) => spec.local.name)
13
13
  }
14
14
 
15
+ const getLeftoverImports = (node: NodePath<t.JSXElement>) => {
16
+ const finalReferences: Array<string> = []
17
+ node.traverse({
18
+ JSXAttribute(path) {
19
+ const node = path.node
20
+ const propName =
21
+ typeof node.name.name === 'string'
22
+ ? node.name.name
23
+ : node.name.name.name
24
+
25
+ if (
26
+ propName === 'plugins' &&
27
+ node.value?.type === 'JSXExpressionContainer' &&
28
+ node.value.expression.type === 'ArrayExpression'
29
+ ) {
30
+ const elements = node.value.expression.elements
31
+
32
+ elements.forEach((el) => {
33
+ if (el?.type === 'ObjectExpression') {
34
+ // { name: "something", render: ()=> <Component /> }
35
+ const props = el.properties
36
+ const referencesToRemove = props
37
+ .map((prop) => {
38
+ if (
39
+ prop.type === 'ObjectProperty' &&
40
+ prop.key.type === 'Identifier' &&
41
+ prop.key.name === 'render'
42
+ ) {
43
+ const value = prop.value
44
+ // handle <ReactRouterPanel />
45
+ if (
46
+ value.type === 'JSXElement' &&
47
+ value.openingElement.name.type === 'JSXIdentifier'
48
+ ) {
49
+ const elementName = value.openingElement.name.name
50
+ return elementName
51
+ }
52
+ // handle () => <ReactRouterPanel /> or function() { return <ReactRouterPanel /> }
53
+ if (
54
+ value.type === 'ArrowFunctionExpression' ||
55
+ value.type === 'FunctionExpression'
56
+ ) {
57
+ const body = value.body
58
+ if (
59
+ body.type === 'JSXElement' &&
60
+ body.openingElement.name.type === 'JSXIdentifier'
61
+ ) {
62
+ const elementName = body.openingElement.name.name
63
+ return elementName
64
+ }
65
+ }
66
+ // handle render: SomeComponent
67
+ if (value.type === 'Identifier') {
68
+ const elementName = value.name
69
+ return elementName
70
+ }
71
+
72
+ // handle render: someFunction()
73
+ if (
74
+ value.type === 'CallExpression' &&
75
+ value.callee.type === 'Identifier'
76
+ ) {
77
+ const elementName = value.callee.name
78
+ return elementName
79
+ }
80
+
81
+ return ''
82
+ }
83
+ return ''
84
+ })
85
+ .filter(Boolean)
86
+ finalReferences.push(...referencesToRemove)
87
+ }
88
+ })
89
+ }
90
+ },
91
+ })
92
+ return finalReferences
93
+ }
94
+
15
95
  const transform = (ast: ParseResult<Babel.File>) => {
16
96
  let didTransform = false
17
97
  const devtoolsComponentNames = new Set()
98
+ const finalReferences: Array<string> = []
99
+
100
+ const transformations: Array<() => void> = []
18
101
 
19
102
  trav(ast, {
20
103
  ImportDeclaration(path) {
@@ -23,7 +106,11 @@ const transform = (ast: ParseResult<Babel.File>) => {
23
106
  getImportedNames(path.node).forEach((name) =>
24
107
  devtoolsComponentNames.add(name),
25
108
  )
26
- path.remove()
109
+
110
+ transformations.push(() => {
111
+ path.remove()
112
+ })
113
+
27
114
  didTransform = true
28
115
  }
29
116
  },
@@ -33,21 +120,53 @@ const transform = (ast: ParseResult<Babel.File>) => {
33
120
  opening.name.type === 'JSXIdentifier' &&
34
121
  devtoolsComponentNames.has(opening.name.name)
35
122
  ) {
36
- path.remove()
123
+ const refs = getLeftoverImports(path)
124
+
125
+ finalReferences.push(...refs)
126
+ transformations.push(() => {
127
+ path.remove()
128
+ })
37
129
  didTransform = true
38
130
  }
39
-
40
131
  if (
41
132
  opening.name.type === 'JSXMemberExpression' &&
42
133
  opening.name.object.type === 'JSXIdentifier' &&
43
134
  devtoolsComponentNames.has(opening.name.object.name)
44
135
  ) {
45
- path.remove()
136
+ const refs = getLeftoverImports(path)
137
+ finalReferences.push(...refs)
138
+ transformations.push(() => {
139
+ path.remove()
140
+ })
46
141
  didTransform = true
47
142
  }
48
143
  },
49
144
  })
50
145
 
146
+ trav(ast, {
147
+ ImportDeclaration(path) {
148
+ const imports = path.node.specifiers
149
+ for (const imported of imports) {
150
+ if (imported.type === 'ImportSpecifier') {
151
+ if (finalReferences.includes(imported.local.name)) {
152
+ transformations.push(() => {
153
+ // remove the specifier
154
+ path.node.specifiers = path.node.specifiers.filter(
155
+ (spec) => spec !== imported,
156
+ )
157
+ // remove whole import if nothing is left
158
+ if (path.node.specifiers.length === 0) {
159
+ path.remove()
160
+ }
161
+ })
162
+ }
163
+ }
164
+ }
165
+ },
166
+ })
167
+
168
+ transformations.forEach((fn) => fn())
169
+
51
170
  return didTransform
52
171
  }
53
172
 
@@ -65,6 +184,7 @@ export function removeDevtools(code: string, id: string) {
65
184
  }
66
185
  return gen(ast, {
67
186
  sourceMaps: true,
187
+ retainLines: true,
68
188
  filename: id,
69
189
  sourceFileName: filePath,
70
190
  })