react-reinspect 0.1.1 → 0.1.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/README.md +79 -4
- package/dist/plugin/reinspectAutoDiscoverPlugin.d.ts +11 -0
- package/dist/plugin/reinspectAutoDiscoverPlugin.d.ts.map +1 -0
- package/dist/plugin/reinspectAutoDiscoverPlugin.js +383 -0
- package/dist/plugin/reinspectNextAutoDiscoverLoader.d.ts +11 -0
- package/dist/plugin/reinspectNextAutoDiscoverLoader.d.ts.map +1 -0
- package/dist/plugin/reinspectNextAutoDiscoverLoader.js +58 -0
- package/dist/plugin/reinspectNextPlugin.d.ts +32 -0
- package/dist/plugin/reinspectNextPlugin.d.ts.map +1 -0
- package/dist/plugin/reinspectNextPlugin.js +70 -0
- package/package.json +19 -8
package/README.md
CHANGED
|
@@ -57,6 +57,34 @@ If your app enforces strict CSP and blocks inline styles, import this once in yo
|
|
|
57
57
|
import 'react-reinspect/style.css'
|
|
58
58
|
```
|
|
59
59
|
|
|
60
|
+
Auto-discovery is compile-time and requires the Vite plugin:
|
|
61
|
+
|
|
62
|
+
```ts
|
|
63
|
+
// vite.config.ts
|
|
64
|
+
import { defineConfig } from 'vite'
|
|
65
|
+
import react from '@vitejs/plugin-react'
|
|
66
|
+
import { reinspectAutoDiscoverPlugin } from 'react-reinspect/vite-plugin'
|
|
67
|
+
|
|
68
|
+
export default defineConfig({
|
|
69
|
+
plugins: [reinspectAutoDiscoverPlugin(), react()],
|
|
70
|
+
})
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Without this plugin, only manually wrapped components (`withReinspect`) are inspectable.
|
|
74
|
+
|
|
75
|
+
Next.js (webpack mode) can use:
|
|
76
|
+
|
|
77
|
+
```ts
|
|
78
|
+
// next.config.ts
|
|
79
|
+
import { withReinspectAutoDiscover } from 'react-reinspect/next-plugin'
|
|
80
|
+
|
|
81
|
+
const nextConfig = {}
|
|
82
|
+
|
|
83
|
+
export default withReinspectAutoDiscover(nextConfig)
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
If your Next dev server runs with Turbopack, switch to webpack mode for this transform plugin.
|
|
87
|
+
|
|
60
88
|
|
|
61
89
|
## Example
|
|
62
90
|
|
|
@@ -73,9 +101,12 @@ You are a senior React code agent, your task is to integrate react-reinspect to
|
|
|
73
101
|
Do all of the following in one pass:
|
|
74
102
|
1) Install `react-reinspect` using this repo's package manager.
|
|
75
103
|
2) Turn it on in dev mode ONLY by wiring `ReinspectProvider` at app root.
|
|
76
|
-
3)
|
|
77
|
-
4)
|
|
78
|
-
5)
|
|
104
|
+
3) If this app uses Vite, add `reinspectAutoDiscoverPlugin` from `react-reinspect/vite-plugin` in `vite.config.*`.
|
|
105
|
+
4) If this app uses Next.js (webpack mode), wrap the Next config with `withReinspectAutoDiscover` from `react-reinspect/next-plugin`.
|
|
106
|
+
5) If Next.js is running with Turbopack, either switch dev to webpack for auto-discovery transforms or fall back to manual wrapping (`withReinspect`).
|
|
107
|
+
6) Keep production safe (e.g.: `enabled: import.meta.env.DEV` or however we manage dev/prod in this repo.).
|
|
108
|
+
7) Run validation (build/tests if available) and fix any issues.
|
|
109
|
+
8) Output a concise summary with changed files and how to use it.
|
|
79
110
|
|
|
80
111
|
Use this example as the baseline:
|
|
81
112
|
|
|
@@ -93,6 +124,23 @@ export function AppProviders({ children }: { children: React.ReactNode }) {
|
|
|
93
124
|
return <ReinspectProvider config={reinspectConfig}>{children}</ReinspectProvider>
|
|
94
125
|
}
|
|
95
126
|
|
|
127
|
+
For auto-discovery setup, apply one of these depending on framework:
|
|
128
|
+
|
|
129
|
+
// Vite
|
|
130
|
+
import { defineConfig } from 'vite'
|
|
131
|
+
import react from '@vitejs/plugin-react'
|
|
132
|
+
import { reinspectAutoDiscoverPlugin } from 'react-reinspect/vite-plugin'
|
|
133
|
+
|
|
134
|
+
export default defineConfig({
|
|
135
|
+
plugins: [reinspectAutoDiscoverPlugin(), react()],
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
// Next.js (webpack mode)
|
|
139
|
+
import { withReinspectAutoDiscover } from 'react-reinspect/next-plugin'
|
|
140
|
+
|
|
141
|
+
const nextConfig = {}
|
|
142
|
+
export default withReinspectAutoDiscover(nextConfig)
|
|
143
|
+
|
|
96
144
|
Output format I want from you:
|
|
97
145
|
- What changed (bullet list)
|
|
98
146
|
- Why this is safe in production
|
|
@@ -122,7 +170,34 @@ export function AppProviders({ children }: { children: React.ReactNode }) {
|
|
|
122
170
|
}
|
|
123
171
|
```
|
|
124
172
|
|
|
125
|
-
### 2)
|
|
173
|
+
### 2) Enable auto-discovery transforms (pick your framework)
|
|
174
|
+
|
|
175
|
+
Vite:
|
|
176
|
+
|
|
177
|
+
```ts
|
|
178
|
+
// vite.config.ts
|
|
179
|
+
import { defineConfig } from 'vite'
|
|
180
|
+
import react from '@vitejs/plugin-react'
|
|
181
|
+
import { reinspectAutoDiscoverPlugin } from 'react-reinspect/vite-plugin'
|
|
182
|
+
|
|
183
|
+
export default defineConfig({
|
|
184
|
+
plugins: [reinspectAutoDiscoverPlugin(), react()],
|
|
185
|
+
})
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
Next.js (webpack mode):
|
|
189
|
+
|
|
190
|
+
```ts
|
|
191
|
+
// next.config.ts
|
|
192
|
+
import { withReinspectAutoDiscover } from 'react-reinspect/next-plugin'
|
|
193
|
+
|
|
194
|
+
const nextConfig = {}
|
|
195
|
+
export default withReinspectAutoDiscover(nextConfig)
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
If your Next dev server runs with Turbopack, switch to webpack mode for auto-discovery transforms.
|
|
199
|
+
|
|
200
|
+
### 3) Use it in the browser
|
|
126
201
|
|
|
127
202
|
- Click `Reinspect settings` button.
|
|
128
203
|
- Right-click a wrapped component.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Plugin } from 'vite';
|
|
2
|
+
type AutoDiscoverScope = 'first-party' | 'third-party';
|
|
3
|
+
export interface AutoDiscoverTransformResult {
|
|
4
|
+
code: string;
|
|
5
|
+
modified: boolean;
|
|
6
|
+
}
|
|
7
|
+
export declare function shouldSkipThirdPartyModule(id: string): boolean;
|
|
8
|
+
export declare function transformModuleForAutoDiscover(code: string, fileId: string, scope: AutoDiscoverScope): AutoDiscoverTransformResult;
|
|
9
|
+
export declare function reinspectAutoDiscoverPlugin(): Plugin;
|
|
10
|
+
export {};
|
|
11
|
+
//# sourceMappingURL=reinspectAutoDiscoverPlugin.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reinspectAutoDiscoverPlugin.d.ts","sourceRoot":"","sources":["../../reinspectAutoDiscoverPlugin.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,MAAM,CAAA;AAElC,KAAK,iBAAiB,GAAG,aAAa,GAAG,aAAa,CAAA;AAgBtD,MAAM,WAAW,2BAA2B;IAC1C,IAAI,EAAE,MAAM,CAAA;IACZ,QAAQ,EAAE,OAAO,CAAA;CAClB;AAmQD,wBAAgB,0BAA0B,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAE9D;AAED,wBAAgB,8BAA8B,CAC5C,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,iBAAiB,GACvB,2BAA2B,CAwP7B;AAED,wBAAgB,2BAA2B,IAAI,MAAM,CA2CpD"}
|
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { parse } from '@babel/parser';
|
|
3
|
+
import { generate } from '@babel/generator';
|
|
4
|
+
import * as t from '@babel/types';
|
|
5
|
+
const AUTO_WRAP_IMPORT_SOURCE = 'react-reinspect';
|
|
6
|
+
const AUTO_WRAP_IMPORT_NAME = 'autoWrapInspectable';
|
|
7
|
+
const DEFAULT_EXPORT_LOCAL_IDENTIFIER = '__reinspect_default_component';
|
|
8
|
+
const SUPPORTED_FILE_PATTERN = /\.[cm]?[jt]sx?$/;
|
|
9
|
+
const THIRD_PARTY_SKIP_PATTERNS = [
|
|
10
|
+
'/node_modules/react/',
|
|
11
|
+
'/node_modules/react-dom/',
|
|
12
|
+
'/node_modules/scheduler/',
|
|
13
|
+
'/node_modules/@vite/',
|
|
14
|
+
'/node_modules/vite/',
|
|
15
|
+
'/node_modules/@react-refresh/',
|
|
16
|
+
];
|
|
17
|
+
function normalizeModuleId(id) {
|
|
18
|
+
const withoutQuery = id.split('?')[0];
|
|
19
|
+
return withoutQuery.split(path.sep).join('/');
|
|
20
|
+
}
|
|
21
|
+
function isPascalCaseIdentifier(name) {
|
|
22
|
+
return /^[A-Z][A-Za-z0-9]*$/.test(name);
|
|
23
|
+
}
|
|
24
|
+
function inferFallbackName(filePath) {
|
|
25
|
+
const baseName = path.basename(filePath).replace(/\.[^.]+$/, '');
|
|
26
|
+
const tokens = baseName
|
|
27
|
+
.replace(/[^A-Za-z0-9]+/g, ' ')
|
|
28
|
+
.trim()
|
|
29
|
+
.split(/\s+/)
|
|
30
|
+
.filter(Boolean);
|
|
31
|
+
if (tokens.length === 0) {
|
|
32
|
+
return 'Component';
|
|
33
|
+
}
|
|
34
|
+
return tokens
|
|
35
|
+
.map((token) => token[0].toUpperCase() + token.slice(1))
|
|
36
|
+
.join('');
|
|
37
|
+
}
|
|
38
|
+
function unwrapExpression(node) {
|
|
39
|
+
let currentNode = node;
|
|
40
|
+
while (t.isParenthesizedExpression(currentNode) ||
|
|
41
|
+
t.isTSAsExpression(currentNode) ||
|
|
42
|
+
t.isTSTypeAssertion(currentNode) ||
|
|
43
|
+
t.isTSNonNullExpression(currentNode) ||
|
|
44
|
+
t.isTSInstantiationExpression(currentNode) ||
|
|
45
|
+
t.isTypeCastExpression(currentNode)) {
|
|
46
|
+
currentNode = currentNode.expression;
|
|
47
|
+
}
|
|
48
|
+
return currentNode;
|
|
49
|
+
}
|
|
50
|
+
function isReactCreateElementCall(node) {
|
|
51
|
+
const callee = unwrapExpression(node.callee);
|
|
52
|
+
if (t.isIdentifier(callee)) {
|
|
53
|
+
return callee.name === 'createElement';
|
|
54
|
+
}
|
|
55
|
+
if (t.isMemberExpression(callee) &&
|
|
56
|
+
t.isIdentifier(callee.object) &&
|
|
57
|
+
callee.object.name === 'React' &&
|
|
58
|
+
t.isIdentifier(callee.property)) {
|
|
59
|
+
return callee.property.name === 'createElement';
|
|
60
|
+
}
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
function isAstNode(value) {
|
|
64
|
+
return (typeof value === 'object' &&
|
|
65
|
+
value !== null &&
|
|
66
|
+
'type' in value &&
|
|
67
|
+
typeof value.type === 'string');
|
|
68
|
+
}
|
|
69
|
+
function containsRenderableReactNode(node) {
|
|
70
|
+
if (!node) {
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
if (t.isJSXElement(node) || t.isJSXFragment(node)) {
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
if (t.isCallExpression(node) && isReactCreateElementCall(node)) {
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
for (const fieldValue of Object.values(node)) {
|
|
80
|
+
if (Array.isArray(fieldValue)) {
|
|
81
|
+
for (const value of fieldValue) {
|
|
82
|
+
if (isAstNode(value) && containsRenderableReactNode(value)) {
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
if (isAstNode(fieldValue) && containsRenderableReactNode(fieldValue)) {
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
function isComponentFunction(node) {
|
|
95
|
+
if (t.isBlockStatement(node.body)) {
|
|
96
|
+
return containsRenderableReactNode(node.body);
|
|
97
|
+
}
|
|
98
|
+
return containsRenderableReactNode(node.body);
|
|
99
|
+
}
|
|
100
|
+
function isMemoForwardRefCallee(callee) {
|
|
101
|
+
const expression = unwrapExpression(callee);
|
|
102
|
+
if (t.isIdentifier(expression)) {
|
|
103
|
+
return expression.name === 'memo' || expression.name === 'forwardRef';
|
|
104
|
+
}
|
|
105
|
+
if (t.isMemberExpression(expression) &&
|
|
106
|
+
t.isIdentifier(expression.object) &&
|
|
107
|
+
expression.object.name === 'React' &&
|
|
108
|
+
t.isIdentifier(expression.property)) {
|
|
109
|
+
return (expression.property.name === 'memo' ||
|
|
110
|
+
expression.property.name === 'forwardRef');
|
|
111
|
+
}
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
function isMemoForwardRefCall(node) {
|
|
115
|
+
const callee = unwrapExpression(node.callee);
|
|
116
|
+
if (!isMemoForwardRefCallee(callee)) {
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
if (node.arguments.length === 0) {
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
const firstArgument = node.arguments[0];
|
|
123
|
+
if (!t.isExpression(firstArgument)) {
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
const componentCandidate = unwrapExpression(firstArgument);
|
|
127
|
+
if (t.isArrowFunctionExpression(componentCandidate) ||
|
|
128
|
+
t.isFunctionExpression(componentCandidate)) {
|
|
129
|
+
return isComponentFunction(componentCandidate);
|
|
130
|
+
}
|
|
131
|
+
if (t.isIdentifier(componentCandidate)) {
|
|
132
|
+
return isPascalCaseIdentifier(componentCandidate.name);
|
|
133
|
+
}
|
|
134
|
+
return true;
|
|
135
|
+
}
|
|
136
|
+
function isWrappedInitializer(node) {
|
|
137
|
+
const expression = unwrapExpression(node);
|
|
138
|
+
if (!t.isCallExpression(expression)) {
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
const callee = unwrapExpression(expression.callee);
|
|
142
|
+
return (t.isIdentifier(callee) &&
|
|
143
|
+
(callee.name === AUTO_WRAP_IMPORT_NAME || callee.name === 'withReinspect'));
|
|
144
|
+
}
|
|
145
|
+
function isComponentInitializer(node) {
|
|
146
|
+
const expression = unwrapExpression(node);
|
|
147
|
+
if (t.isArrowFunctionExpression(expression) || t.isFunctionExpression(expression)) {
|
|
148
|
+
return isComponentFunction(expression);
|
|
149
|
+
}
|
|
150
|
+
if (t.isCallExpression(expression)) {
|
|
151
|
+
return isMemoForwardRefCall(expression);
|
|
152
|
+
}
|
|
153
|
+
return false;
|
|
154
|
+
}
|
|
155
|
+
function createAutoWrapCall(expression, componentName, scope, fallbackName) {
|
|
156
|
+
const metadataProperties = [
|
|
157
|
+
t.objectProperty(t.identifier('scope'), t.stringLiteral(scope)),
|
|
158
|
+
t.objectProperty(t.identifier('fallbackName'), t.stringLiteral(fallbackName)),
|
|
159
|
+
];
|
|
160
|
+
if (componentName) {
|
|
161
|
+
metadataProperties.unshift(t.objectProperty(t.identifier('componentName'), t.stringLiteral(componentName)));
|
|
162
|
+
}
|
|
163
|
+
return t.callExpression(t.identifier(AUTO_WRAP_IMPORT_NAME), [
|
|
164
|
+
expression,
|
|
165
|
+
t.objectExpression(metadataProperties),
|
|
166
|
+
]);
|
|
167
|
+
}
|
|
168
|
+
function createFunctionWrapAssignment(name, scope, fallbackName) {
|
|
169
|
+
return t.expressionStatement(t.assignmentExpression('=', t.identifier(name), createAutoWrapCall(t.identifier(name), name, scope, fallbackName)));
|
|
170
|
+
}
|
|
171
|
+
function functionDeclarationToExpression(node) {
|
|
172
|
+
const expression = t.functionExpression(node.id, node.params, node.body, node.generator, node.async);
|
|
173
|
+
expression.typeParameters = node.typeParameters;
|
|
174
|
+
expression.returnType = node.returnType;
|
|
175
|
+
return expression;
|
|
176
|
+
}
|
|
177
|
+
function isSupportedSourceFile(id) {
|
|
178
|
+
if (id.startsWith('\0') || id.startsWith('/@')) {
|
|
179
|
+
return false;
|
|
180
|
+
}
|
|
181
|
+
return SUPPORTED_FILE_PATTERN.test(id);
|
|
182
|
+
}
|
|
183
|
+
function isFirstPartyModule(id) {
|
|
184
|
+
return id.includes('/src/') && !id.includes('/node_modules/');
|
|
185
|
+
}
|
|
186
|
+
export function shouldSkipThirdPartyModule(id) {
|
|
187
|
+
return THIRD_PARTY_SKIP_PATTERNS.some((pattern) => id.includes(pattern));
|
|
188
|
+
}
|
|
189
|
+
export function transformModuleForAutoDiscover(code, fileId, scope) {
|
|
190
|
+
let ast;
|
|
191
|
+
try {
|
|
192
|
+
ast = parse(code, {
|
|
193
|
+
sourceType: 'module',
|
|
194
|
+
plugins: ['typescript', 'jsx'],
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
catch {
|
|
198
|
+
return { code, modified: false };
|
|
199
|
+
}
|
|
200
|
+
const program = ast.program;
|
|
201
|
+
const fallbackName = inferFallbackName(fileId);
|
|
202
|
+
let modified = false;
|
|
203
|
+
let autoWrapImportDeclaration;
|
|
204
|
+
let hasAutoWrapImport = false;
|
|
205
|
+
for (const statement of program.body) {
|
|
206
|
+
if (!t.isImportDeclaration(statement)) {
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
if (statement.source.value !== AUTO_WRAP_IMPORT_SOURCE) {
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
autoWrapImportDeclaration = statement;
|
|
213
|
+
hasAutoWrapImport = statement.specifiers.some((specifier) => t.isImportSpecifier(specifier) &&
|
|
214
|
+
t.isIdentifier(specifier.imported) &&
|
|
215
|
+
specifier.imported.name === AUTO_WRAP_IMPORT_NAME);
|
|
216
|
+
}
|
|
217
|
+
for (let index = 0; index < program.body.length; index += 1) {
|
|
218
|
+
const statement = program.body[index];
|
|
219
|
+
if (t.isFunctionDeclaration(statement) && statement.id) {
|
|
220
|
+
if (isPascalCaseIdentifier(statement.id.name) &&
|
|
221
|
+
isComponentFunction(statement)) {
|
|
222
|
+
program.body.splice(index + 1, 0, createFunctionWrapAssignment(statement.id.name, scope, fallbackName));
|
|
223
|
+
index += 1;
|
|
224
|
+
modified = true;
|
|
225
|
+
}
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
if (t.isExportNamedDeclaration(statement) &&
|
|
229
|
+
statement.declaration &&
|
|
230
|
+
t.isFunctionDeclaration(statement.declaration) &&
|
|
231
|
+
statement.declaration.id &&
|
|
232
|
+
isPascalCaseIdentifier(statement.declaration.id.name) &&
|
|
233
|
+
isComponentFunction(statement.declaration)) {
|
|
234
|
+
program.body.splice(index + 1, 0, createFunctionWrapAssignment(statement.declaration.id.name, scope, fallbackName));
|
|
235
|
+
index += 1;
|
|
236
|
+
modified = true;
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
const processVariableDeclaration = (declaration) => {
|
|
240
|
+
let declarationChanged = false;
|
|
241
|
+
for (const declarator of declaration.declarations) {
|
|
242
|
+
if (!t.isIdentifier(declarator.id) || !declarator.init) {
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
if (!isPascalCaseIdentifier(declarator.id.name)) {
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
if (!isComponentInitializer(declarator.init)) {
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
if (isWrappedInitializer(declarator.init)) {
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
declarator.init = createAutoWrapCall(declarator.init, declarator.id.name, scope, fallbackName);
|
|
255
|
+
declarationChanged = true;
|
|
256
|
+
}
|
|
257
|
+
return declarationChanged;
|
|
258
|
+
};
|
|
259
|
+
if (t.isVariableDeclaration(statement)) {
|
|
260
|
+
if (processVariableDeclaration(statement)) {
|
|
261
|
+
modified = true;
|
|
262
|
+
}
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
if (t.isExportNamedDeclaration(statement) &&
|
|
266
|
+
statement.declaration &&
|
|
267
|
+
t.isVariableDeclaration(statement.declaration)) {
|
|
268
|
+
if (processVariableDeclaration(statement.declaration)) {
|
|
269
|
+
modified = true;
|
|
270
|
+
}
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
if (!t.isExportDefaultDeclaration(statement)) {
|
|
274
|
+
continue;
|
|
275
|
+
}
|
|
276
|
+
const declaration = statement.declaration;
|
|
277
|
+
if (t.isFunctionDeclaration(declaration)) {
|
|
278
|
+
if (declaration.id &&
|
|
279
|
+
isPascalCaseIdentifier(declaration.id.name) &&
|
|
280
|
+
isComponentFunction(declaration)) {
|
|
281
|
+
const localName = declaration.id.name;
|
|
282
|
+
const replacementStatements = [
|
|
283
|
+
declaration,
|
|
284
|
+
createFunctionWrapAssignment(localName, scope, fallbackName),
|
|
285
|
+
t.exportDefaultDeclaration(t.identifier(localName)),
|
|
286
|
+
];
|
|
287
|
+
program.body.splice(index, 1, ...replacementStatements);
|
|
288
|
+
index += replacementStatements.length - 1;
|
|
289
|
+
modified = true;
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
if (!declaration.id && isComponentFunction(declaration)) {
|
|
293
|
+
const localIdentifier = t.identifier(DEFAULT_EXPORT_LOCAL_IDENTIFIER);
|
|
294
|
+
const wrappedValue = createAutoWrapCall(functionDeclarationToExpression(declaration), undefined, scope, fallbackName);
|
|
295
|
+
const replacementStatements = [
|
|
296
|
+
t.variableDeclaration('const', [
|
|
297
|
+
t.variableDeclarator(localIdentifier, wrappedValue),
|
|
298
|
+
]),
|
|
299
|
+
t.exportDefaultDeclaration(localIdentifier),
|
|
300
|
+
];
|
|
301
|
+
program.body.splice(index, 1, ...replacementStatements);
|
|
302
|
+
index += replacementStatements.length - 1;
|
|
303
|
+
modified = true;
|
|
304
|
+
}
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
307
|
+
if (!t.isExpression(declaration)) {
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
310
|
+
if (!isComponentInitializer(declaration) || isWrappedInitializer(declaration)) {
|
|
311
|
+
continue;
|
|
312
|
+
}
|
|
313
|
+
const localIdentifier = t.identifier(DEFAULT_EXPORT_LOCAL_IDENTIFIER);
|
|
314
|
+
const wrappedValue = createAutoWrapCall(declaration, undefined, scope, fallbackName);
|
|
315
|
+
const replacementStatements = [
|
|
316
|
+
t.variableDeclaration('const', [
|
|
317
|
+
t.variableDeclarator(localIdentifier, wrappedValue),
|
|
318
|
+
]),
|
|
319
|
+
t.exportDefaultDeclaration(localIdentifier),
|
|
320
|
+
];
|
|
321
|
+
program.body.splice(index, 1, ...replacementStatements);
|
|
322
|
+
index += replacementStatements.length - 1;
|
|
323
|
+
modified = true;
|
|
324
|
+
}
|
|
325
|
+
if (!modified) {
|
|
326
|
+
return { code, modified: false };
|
|
327
|
+
}
|
|
328
|
+
if (autoWrapImportDeclaration) {
|
|
329
|
+
if (!hasAutoWrapImport) {
|
|
330
|
+
autoWrapImportDeclaration.specifiers.push(t.importSpecifier(t.identifier(AUTO_WRAP_IMPORT_NAME), t.identifier(AUTO_WRAP_IMPORT_NAME)));
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
else {
|
|
334
|
+
let insertIndex = 0;
|
|
335
|
+
while (insertIndex < program.body.length &&
|
|
336
|
+
t.isImportDeclaration(program.body[insertIndex])) {
|
|
337
|
+
insertIndex += 1;
|
|
338
|
+
}
|
|
339
|
+
program.body.splice(insertIndex, 0, t.importDeclaration([
|
|
340
|
+
t.importSpecifier(t.identifier(AUTO_WRAP_IMPORT_NAME), t.identifier(AUTO_WRAP_IMPORT_NAME)),
|
|
341
|
+
], t.stringLiteral(AUTO_WRAP_IMPORT_SOURCE)));
|
|
342
|
+
}
|
|
343
|
+
const output = generate(ast, {
|
|
344
|
+
jsescOption: { minimal: true },
|
|
345
|
+
});
|
|
346
|
+
return {
|
|
347
|
+
code: output.code,
|
|
348
|
+
modified: true,
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
export function reinspectAutoDiscoverPlugin() {
|
|
352
|
+
return {
|
|
353
|
+
name: 'reinspect-auto-discover',
|
|
354
|
+
apply: 'serve',
|
|
355
|
+
enforce: 'pre',
|
|
356
|
+
transform(sourceCode, id) {
|
|
357
|
+
const normalizedId = normalizeModuleId(id);
|
|
358
|
+
if (!isSupportedSourceFile(normalizedId)) {
|
|
359
|
+
return null;
|
|
360
|
+
}
|
|
361
|
+
if (normalizedId.includes('/node_modules/react-reinspect/')) {
|
|
362
|
+
return null;
|
|
363
|
+
}
|
|
364
|
+
if (normalizedId.includes('/src/reinspect/')) {
|
|
365
|
+
return null;
|
|
366
|
+
}
|
|
367
|
+
const scope = isFirstPartyModule(normalizedId)
|
|
368
|
+
? 'first-party'
|
|
369
|
+
: 'third-party';
|
|
370
|
+
if (scope === 'third-party' && shouldSkipThirdPartyModule(normalizedId)) {
|
|
371
|
+
return null;
|
|
372
|
+
}
|
|
373
|
+
const transformedResult = transformModuleForAutoDiscover(sourceCode, normalizedId, scope);
|
|
374
|
+
if (!transformedResult.modified) {
|
|
375
|
+
return null;
|
|
376
|
+
}
|
|
377
|
+
return {
|
|
378
|
+
code: transformedResult.code,
|
|
379
|
+
map: null,
|
|
380
|
+
};
|
|
381
|
+
},
|
|
382
|
+
};
|
|
383
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
interface ReinspectNextLoaderOptions {
|
|
2
|
+
includeThirdParty?: boolean;
|
|
3
|
+
}
|
|
4
|
+
interface LoaderContextLike {
|
|
5
|
+
resourcePath: string;
|
|
6
|
+
getOptions?: () => ReinspectNextLoaderOptions;
|
|
7
|
+
async?: () => ((error: Error | null, code?: string) => void) | undefined;
|
|
8
|
+
}
|
|
9
|
+
export default function reinspectNextAutoDiscoverLoader(this: LoaderContextLike, sourceCode: string): string | void;
|
|
10
|
+
export {};
|
|
11
|
+
//# sourceMappingURL=reinspectNextAutoDiscoverLoader.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reinspectNextAutoDiscoverLoader.d.ts","sourceRoot":"","sources":["../../reinspectNextAutoDiscoverLoader.ts"],"names":[],"mappings":"AAQA,UAAU,0BAA0B;IAClC,iBAAiB,CAAC,EAAE,OAAO,CAAA;CAC5B;AAED,UAAU,iBAAiB;IACzB,YAAY,EAAE,MAAM,CAAA;IACpB,UAAU,CAAC,EAAE,MAAM,0BAA0B,CAAA;IAC7C,KAAK,CAAC,EAAE,MAAM,CAAC,CAAC,KAAK,EAAE,KAAK,GAAG,IAAI,EAAE,IAAI,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC,GAAG,SAAS,CAAA;CACzE;AA4DD,MAAM,CAAC,OAAO,UAAU,+BAA+B,CACrD,IAAI,EAAE,iBAAiB,EACvB,UAAU,EAAE,MAAM,GACjB,MAAM,GAAG,IAAI,CAyBf"}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { shouldSkipThirdPartyModule, transformModuleForAutoDiscover, } from './reinspectAutoDiscoverPlugin';
|
|
3
|
+
const SUPPORTED_FILE_PATTERN = /\.[cm]?[jt]sx?$/;
|
|
4
|
+
function normalizeModuleId(id) {
|
|
5
|
+
return id.split(path.sep).join('/');
|
|
6
|
+
}
|
|
7
|
+
function isSupportedSourceFile(id) {
|
|
8
|
+
if (!SUPPORTED_FILE_PATTERN.test(id)) {
|
|
9
|
+
return false;
|
|
10
|
+
}
|
|
11
|
+
if (id.includes('/node_modules/react-reinspect/')) {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
return true;
|
|
15
|
+
}
|
|
16
|
+
function resolveAutoDiscoverScope(normalizedId, includeThirdParty) {
|
|
17
|
+
const isThirdParty = normalizedId.includes('/node_modules/');
|
|
18
|
+
if (!isThirdParty) {
|
|
19
|
+
return 'first-party';
|
|
20
|
+
}
|
|
21
|
+
if (!includeThirdParty || shouldSkipThirdPartyModule(normalizedId)) {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
return 'third-party';
|
|
25
|
+
}
|
|
26
|
+
function runTransform(sourceCode, normalizedId, options) {
|
|
27
|
+
if (!isSupportedSourceFile(normalizedId)) {
|
|
28
|
+
return sourceCode;
|
|
29
|
+
}
|
|
30
|
+
const includeThirdParty = Boolean(options.includeThirdParty);
|
|
31
|
+
const scope = resolveAutoDiscoverScope(normalizedId, includeThirdParty);
|
|
32
|
+
if (!scope) {
|
|
33
|
+
return sourceCode;
|
|
34
|
+
}
|
|
35
|
+
const transformedResult = transformModuleForAutoDiscover(sourceCode, normalizedId, scope);
|
|
36
|
+
return transformedResult.modified ? transformedResult.code : sourceCode;
|
|
37
|
+
}
|
|
38
|
+
export default function reinspectNextAutoDiscoverLoader(sourceCode) {
|
|
39
|
+
const options = this.getOptions?.() ?? {};
|
|
40
|
+
const normalizedId = normalizeModuleId(this.resourcePath ?? '');
|
|
41
|
+
const callback = this.async?.();
|
|
42
|
+
try {
|
|
43
|
+
const transformedCode = runTransform(sourceCode, normalizedId, options);
|
|
44
|
+
if (callback) {
|
|
45
|
+
callback(null, transformedCode);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
return transformedCode;
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
const nextError = error instanceof Error ? error : new Error('Failed to transform module');
|
|
52
|
+
if (callback) {
|
|
53
|
+
callback(nextError);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
throw nextError;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export interface ReinspectNextPluginOptions {
|
|
2
|
+
includeThirdParty?: boolean;
|
|
3
|
+
enableInProduction?: boolean;
|
|
4
|
+
}
|
|
5
|
+
interface NextWebpackOptions {
|
|
6
|
+
dev: boolean;
|
|
7
|
+
}
|
|
8
|
+
interface NextWebpackUseEntry {
|
|
9
|
+
loader?: string;
|
|
10
|
+
options?: {
|
|
11
|
+
includeThirdParty?: boolean;
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
interface NextWebpackRule {
|
|
15
|
+
test?: RegExp;
|
|
16
|
+
enforce?: 'pre' | 'post';
|
|
17
|
+
exclude?: RegExp;
|
|
18
|
+
use?: NextWebpackUseEntry | NextWebpackUseEntry[];
|
|
19
|
+
}
|
|
20
|
+
interface NextWebpackModuleConfig {
|
|
21
|
+
rules?: NextWebpackRule[];
|
|
22
|
+
}
|
|
23
|
+
interface NextWebpackConfig {
|
|
24
|
+
module?: NextWebpackModuleConfig;
|
|
25
|
+
}
|
|
26
|
+
type NextWebpackFunction = (config: NextWebpackConfig, options: NextWebpackOptions) => NextWebpackConfig;
|
|
27
|
+
interface NextConfigLike {
|
|
28
|
+
webpack?: NextWebpackFunction;
|
|
29
|
+
}
|
|
30
|
+
export declare function withReinspectAutoDiscover(nextConfig?: NextConfigLike, pluginOptions?: ReinspectNextPluginOptions): NextConfigLike;
|
|
31
|
+
export {};
|
|
32
|
+
//# sourceMappingURL=reinspectNextPlugin.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reinspectNextPlugin.d.ts","sourceRoot":"","sources":["../../reinspectNextPlugin.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,0BAA0B;IACzC,iBAAiB,CAAC,EAAE,OAAO,CAAA;IAC3B,kBAAkB,CAAC,EAAE,OAAO,CAAA;CAC7B;AAED,UAAU,kBAAkB;IAC1B,GAAG,EAAE,OAAO,CAAA;CACb;AAED,UAAU,mBAAmB;IAC3B,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,OAAO,CAAC,EAAE;QACR,iBAAiB,CAAC,EAAE,OAAO,CAAA;KAC5B,CAAA;CACF;AAED,UAAU,eAAe;IACvB,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,OAAO,CAAC,EAAE,KAAK,GAAG,MAAM,CAAA;IACxB,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,GAAG,CAAC,EAAE,mBAAmB,GAAG,mBAAmB,EAAE,CAAA;CAClD;AAED,UAAU,uBAAuB;IAC/B,KAAK,CAAC,EAAE,eAAe,EAAE,CAAA;CAC1B;AAED,UAAU,iBAAiB;IACzB,MAAM,CAAC,EAAE,uBAAuB,CAAA;CACjC;AAED,KAAK,mBAAmB,GAAG,CACzB,MAAM,EAAE,iBAAiB,EACzB,OAAO,EAAE,kBAAkB,KACxB,iBAAiB,CAAA;AAEtB,UAAU,cAAc;IACtB,OAAO,CAAC,EAAE,mBAAmB,CAAA;CAC9B;AAqED,wBAAgB,yBAAyB,CACvC,UAAU,GAAE,cAAmB,EAC/B,aAAa,GAAE,0BAA+B,GAC7C,cAAc,CAqBhB"}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { fileURLToPath } from 'node:url';
|
|
2
|
+
function resolveNextLoaderPath() {
|
|
3
|
+
const loaderUrl = new URL('./reinspectNextAutoDiscoverLoader.js', import.meta.url);
|
|
4
|
+
if (loaderUrl.protocol === 'file:') {
|
|
5
|
+
return fileURLToPath(loaderUrl);
|
|
6
|
+
}
|
|
7
|
+
return loaderUrl.pathname;
|
|
8
|
+
}
|
|
9
|
+
function hasReinspectLoaderRule(rules) {
|
|
10
|
+
for (const rule of rules) {
|
|
11
|
+
const ruleUse = Array.isArray(rule.use)
|
|
12
|
+
? rule.use
|
|
13
|
+
: rule.use
|
|
14
|
+
? [rule.use]
|
|
15
|
+
: [];
|
|
16
|
+
if (ruleUse.some((entry) => entry.loader === resolveNextLoaderPath())) {
|
|
17
|
+
return true;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
function addReinspectLoaderRule(config, options) {
|
|
23
|
+
const moduleConfig = config.module ?? {};
|
|
24
|
+
const rules = moduleConfig.rules ?? [];
|
|
25
|
+
if (hasReinspectLoaderRule(rules)) {
|
|
26
|
+
config.module = {
|
|
27
|
+
...moduleConfig,
|
|
28
|
+
rules,
|
|
29
|
+
};
|
|
30
|
+
return config;
|
|
31
|
+
}
|
|
32
|
+
const nextLoaderPath = resolveNextLoaderPath();
|
|
33
|
+
const nextRules = [
|
|
34
|
+
{
|
|
35
|
+
test: /\.[cm]?[jt]sx?$/,
|
|
36
|
+
enforce: 'pre',
|
|
37
|
+
exclude: /node_modules/,
|
|
38
|
+
use: [
|
|
39
|
+
{
|
|
40
|
+
loader: nextLoaderPath,
|
|
41
|
+
options: {
|
|
42
|
+
includeThirdParty: Boolean(options.includeThirdParty),
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
],
|
|
46
|
+
},
|
|
47
|
+
...rules,
|
|
48
|
+
];
|
|
49
|
+
config.module = {
|
|
50
|
+
...moduleConfig,
|
|
51
|
+
rules: nextRules,
|
|
52
|
+
};
|
|
53
|
+
return config;
|
|
54
|
+
}
|
|
55
|
+
export function withReinspectAutoDiscover(nextConfig = {}, pluginOptions = {}) {
|
|
56
|
+
const originalWebpack = nextConfig.webpack;
|
|
57
|
+
return {
|
|
58
|
+
...nextConfig,
|
|
59
|
+
webpack(config, options) {
|
|
60
|
+
const resolvedConfig = typeof originalWebpack === 'function'
|
|
61
|
+
? originalWebpack(config, options) ?? config
|
|
62
|
+
: config;
|
|
63
|
+
const shouldEnable = options.dev || Boolean(pluginOptions.enableInProduction);
|
|
64
|
+
if (!shouldEnable) {
|
|
65
|
+
return resolvedConfig;
|
|
66
|
+
}
|
|
67
|
+
return addReinspectLoaderRule(resolvedConfig, pluginOptions);
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-reinspect",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "Visual runtime inspector for React components with render counters and style overrides.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"react",
|
|
@@ -19,10 +19,19 @@
|
|
|
19
19
|
"import": "./dist/lib/index.js",
|
|
20
20
|
"require": "./dist/lib/index.cjs"
|
|
21
21
|
},
|
|
22
|
+
"./vite-plugin": {
|
|
23
|
+
"types": "./dist/plugin/reinspectAutoDiscoverPlugin.d.ts",
|
|
24
|
+
"import": "./dist/plugin/reinspectAutoDiscoverPlugin.js"
|
|
25
|
+
},
|
|
26
|
+
"./next-plugin": {
|
|
27
|
+
"types": "./dist/plugin/reinspectNextPlugin.d.ts",
|
|
28
|
+
"import": "./dist/plugin/reinspectNextPlugin.js"
|
|
29
|
+
},
|
|
22
30
|
"./style.css": "./dist/lib/style.css"
|
|
23
31
|
},
|
|
24
32
|
"files": [
|
|
25
33
|
"dist/lib",
|
|
34
|
+
"dist/plugin",
|
|
26
35
|
"README.md"
|
|
27
36
|
],
|
|
28
37
|
"sideEffects": [
|
|
@@ -32,11 +41,13 @@
|
|
|
32
41
|
"dev": "vite",
|
|
33
42
|
"build": "tsc -b && vite build",
|
|
34
43
|
"build:lib": "vite build -c vite.lib.config.ts && tsc -p tsconfig.lib.json",
|
|
44
|
+
"build:plugin": "tsc -p tsconfig.plugin.json",
|
|
45
|
+
"build:package": "pnpm build:lib && pnpm build:plugin",
|
|
35
46
|
"lint": "eslint .",
|
|
36
47
|
"preview": "vite preview",
|
|
37
48
|
"pack:check": "npm pack --dry-run --cache ./.cache/npm",
|
|
38
49
|
"publish:npm": "npm publish --access public --cache ./.cache/npm",
|
|
39
|
-
"prepublishOnly": "pnpm build:
|
|
50
|
+
"prepublishOnly": "pnpm build:package",
|
|
40
51
|
"test": "vitest run",
|
|
41
52
|
"test:watch": "vitest"
|
|
42
53
|
},
|
|
@@ -61,14 +72,14 @@
|
|
|
61
72
|
"eslint": "^9.39.4",
|
|
62
73
|
"eslint-plugin-react-hooks": "^7.0.1",
|
|
63
74
|
"eslint-plugin-react-refresh": "^0.5.2",
|
|
64
|
-
"globals": "^17.
|
|
75
|
+
"globals": "^17.5.0",
|
|
65
76
|
"jsdom": "^29.0.2",
|
|
66
77
|
"magic-string": "^0.30.21",
|
|
78
|
+
"react": "^19.2.5",
|
|
79
|
+
"react-dom": "^19.2.5",
|
|
67
80
|
"typescript": "~6.0.2",
|
|
68
|
-
"typescript-eslint": "^8.58.
|
|
69
|
-
"vite": "^8.0.
|
|
70
|
-
"vitest": "^4.1.4"
|
|
71
|
-
"react": "^19.2.4",
|
|
72
|
-
"react-dom": "^19.2.4"
|
|
81
|
+
"typescript-eslint": "^8.58.1",
|
|
82
|
+
"vite": "^8.0.8",
|
|
83
|
+
"vitest": "^4.1.4"
|
|
73
84
|
}
|
|
74
85
|
}
|