react-reinspect 0.1.1 → 0.1.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.
package/README.md
CHANGED
|
@@ -57,6 +57,21 @@ 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
|
+
|
|
60
75
|
|
|
61
76
|
## Example
|
|
62
77
|
|
|
@@ -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;AA4JD,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,CAgP7B;AAED,wBAAgB,2BAA2B,IAAI,MAAM,CA2CpD"}
|
|
@@ -0,0 +1,306 @@
|
|
|
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 isMemoForwardRefCall(node) {
|
|
51
|
+
const callee = unwrapExpression(node.callee);
|
|
52
|
+
if (t.isIdentifier(callee)) {
|
|
53
|
+
return callee.name === 'memo' || callee.name === 'forwardRef';
|
|
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 === 'memo' || callee.property.name === 'forwardRef';
|
|
60
|
+
}
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
function isWrappedInitializer(node) {
|
|
64
|
+
const expression = unwrapExpression(node);
|
|
65
|
+
if (!t.isCallExpression(expression)) {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
const callee = unwrapExpression(expression.callee);
|
|
69
|
+
return (t.isIdentifier(callee) &&
|
|
70
|
+
(callee.name === AUTO_WRAP_IMPORT_NAME || callee.name === 'withReinspect'));
|
|
71
|
+
}
|
|
72
|
+
function isComponentInitializer(node) {
|
|
73
|
+
const expression = unwrapExpression(node);
|
|
74
|
+
if (t.isArrowFunctionExpression(expression) || t.isFunctionExpression(expression)) {
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
if (t.isCallExpression(expression)) {
|
|
78
|
+
return isMemoForwardRefCall(expression);
|
|
79
|
+
}
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
function createAutoWrapCall(expression, componentName, scope, fallbackName) {
|
|
83
|
+
const metadataProperties = [
|
|
84
|
+
t.objectProperty(t.identifier('scope'), t.stringLiteral(scope)),
|
|
85
|
+
t.objectProperty(t.identifier('fallbackName'), t.stringLiteral(fallbackName)),
|
|
86
|
+
];
|
|
87
|
+
if (componentName) {
|
|
88
|
+
metadataProperties.unshift(t.objectProperty(t.identifier('componentName'), t.stringLiteral(componentName)));
|
|
89
|
+
}
|
|
90
|
+
return t.callExpression(t.identifier(AUTO_WRAP_IMPORT_NAME), [
|
|
91
|
+
expression,
|
|
92
|
+
t.objectExpression(metadataProperties),
|
|
93
|
+
]);
|
|
94
|
+
}
|
|
95
|
+
function createFunctionWrapAssignment(name, scope, fallbackName) {
|
|
96
|
+
return t.expressionStatement(t.assignmentExpression('=', t.identifier(name), createAutoWrapCall(t.identifier(name), name, scope, fallbackName)));
|
|
97
|
+
}
|
|
98
|
+
function functionDeclarationToExpression(node) {
|
|
99
|
+
const expression = t.functionExpression(node.id, node.params, node.body, node.generator, node.async);
|
|
100
|
+
expression.typeParameters = node.typeParameters;
|
|
101
|
+
expression.returnType = node.returnType;
|
|
102
|
+
return expression;
|
|
103
|
+
}
|
|
104
|
+
function isSupportedSourceFile(id) {
|
|
105
|
+
if (id.startsWith('\0') || id.startsWith('/@')) {
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
return SUPPORTED_FILE_PATTERN.test(id);
|
|
109
|
+
}
|
|
110
|
+
function isFirstPartyModule(id) {
|
|
111
|
+
return id.includes('/src/') && !id.includes('/node_modules/');
|
|
112
|
+
}
|
|
113
|
+
export function shouldSkipThirdPartyModule(id) {
|
|
114
|
+
return THIRD_PARTY_SKIP_PATTERNS.some((pattern) => id.includes(pattern));
|
|
115
|
+
}
|
|
116
|
+
export function transformModuleForAutoDiscover(code, fileId, scope) {
|
|
117
|
+
let ast;
|
|
118
|
+
try {
|
|
119
|
+
ast = parse(code, {
|
|
120
|
+
sourceType: 'module',
|
|
121
|
+
plugins: ['typescript', 'jsx'],
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
return { code, modified: false };
|
|
126
|
+
}
|
|
127
|
+
const program = ast.program;
|
|
128
|
+
const fallbackName = inferFallbackName(fileId);
|
|
129
|
+
let modified = false;
|
|
130
|
+
let autoWrapImportDeclaration;
|
|
131
|
+
let hasAutoWrapImport = false;
|
|
132
|
+
for (const statement of program.body) {
|
|
133
|
+
if (!t.isImportDeclaration(statement)) {
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
if (statement.source.value !== AUTO_WRAP_IMPORT_SOURCE) {
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
autoWrapImportDeclaration = statement;
|
|
140
|
+
hasAutoWrapImport = statement.specifiers.some((specifier) => t.isImportSpecifier(specifier) &&
|
|
141
|
+
t.isIdentifier(specifier.imported) &&
|
|
142
|
+
specifier.imported.name === AUTO_WRAP_IMPORT_NAME);
|
|
143
|
+
}
|
|
144
|
+
for (let index = 0; index < program.body.length; index += 1) {
|
|
145
|
+
const statement = program.body[index];
|
|
146
|
+
if (t.isFunctionDeclaration(statement) && statement.id) {
|
|
147
|
+
if (isPascalCaseIdentifier(statement.id.name)) {
|
|
148
|
+
program.body.splice(index + 1, 0, createFunctionWrapAssignment(statement.id.name, scope, fallbackName));
|
|
149
|
+
index += 1;
|
|
150
|
+
modified = true;
|
|
151
|
+
}
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
if (t.isExportNamedDeclaration(statement) &&
|
|
155
|
+
statement.declaration &&
|
|
156
|
+
t.isFunctionDeclaration(statement.declaration) &&
|
|
157
|
+
statement.declaration.id &&
|
|
158
|
+
isPascalCaseIdentifier(statement.declaration.id.name)) {
|
|
159
|
+
program.body.splice(index + 1, 0, createFunctionWrapAssignment(statement.declaration.id.name, scope, fallbackName));
|
|
160
|
+
index += 1;
|
|
161
|
+
modified = true;
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
const processVariableDeclaration = (declaration) => {
|
|
165
|
+
let declarationChanged = false;
|
|
166
|
+
for (const declarator of declaration.declarations) {
|
|
167
|
+
if (!t.isIdentifier(declarator.id) || !declarator.init) {
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
if (!isPascalCaseIdentifier(declarator.id.name)) {
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
if (!isComponentInitializer(declarator.init)) {
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
if (isWrappedInitializer(declarator.init)) {
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
declarator.init = createAutoWrapCall(declarator.init, declarator.id.name, scope, fallbackName);
|
|
180
|
+
declarationChanged = true;
|
|
181
|
+
}
|
|
182
|
+
return declarationChanged;
|
|
183
|
+
};
|
|
184
|
+
if (t.isVariableDeclaration(statement)) {
|
|
185
|
+
if (processVariableDeclaration(statement)) {
|
|
186
|
+
modified = true;
|
|
187
|
+
}
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
if (t.isExportNamedDeclaration(statement) &&
|
|
191
|
+
statement.declaration &&
|
|
192
|
+
t.isVariableDeclaration(statement.declaration)) {
|
|
193
|
+
if (processVariableDeclaration(statement.declaration)) {
|
|
194
|
+
modified = true;
|
|
195
|
+
}
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
if (!t.isExportDefaultDeclaration(statement)) {
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
const declaration = statement.declaration;
|
|
202
|
+
if (t.isFunctionDeclaration(declaration)) {
|
|
203
|
+
if (declaration.id && isPascalCaseIdentifier(declaration.id.name)) {
|
|
204
|
+
const localName = declaration.id.name;
|
|
205
|
+
const replacementStatements = [
|
|
206
|
+
declaration,
|
|
207
|
+
createFunctionWrapAssignment(localName, scope, fallbackName),
|
|
208
|
+
t.exportDefaultDeclaration(t.identifier(localName)),
|
|
209
|
+
];
|
|
210
|
+
program.body.splice(index, 1, ...replacementStatements);
|
|
211
|
+
index += replacementStatements.length - 1;
|
|
212
|
+
modified = true;
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
if (!declaration.id) {
|
|
216
|
+
const localIdentifier = t.identifier(DEFAULT_EXPORT_LOCAL_IDENTIFIER);
|
|
217
|
+
const wrappedValue = createAutoWrapCall(functionDeclarationToExpression(declaration), undefined, scope, fallbackName);
|
|
218
|
+
const replacementStatements = [
|
|
219
|
+
t.variableDeclaration('const', [
|
|
220
|
+
t.variableDeclarator(localIdentifier, wrappedValue),
|
|
221
|
+
]),
|
|
222
|
+
t.exportDefaultDeclaration(localIdentifier),
|
|
223
|
+
];
|
|
224
|
+
program.body.splice(index, 1, ...replacementStatements);
|
|
225
|
+
index += replacementStatements.length - 1;
|
|
226
|
+
modified = true;
|
|
227
|
+
}
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
if (!t.isExpression(declaration)) {
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
if (!isComponentInitializer(declaration) || isWrappedInitializer(declaration)) {
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
const localIdentifier = t.identifier(DEFAULT_EXPORT_LOCAL_IDENTIFIER);
|
|
237
|
+
const wrappedValue = createAutoWrapCall(declaration, undefined, scope, fallbackName);
|
|
238
|
+
const replacementStatements = [
|
|
239
|
+
t.variableDeclaration('const', [
|
|
240
|
+
t.variableDeclarator(localIdentifier, wrappedValue),
|
|
241
|
+
]),
|
|
242
|
+
t.exportDefaultDeclaration(localIdentifier),
|
|
243
|
+
];
|
|
244
|
+
program.body.splice(index, 1, ...replacementStatements);
|
|
245
|
+
index += replacementStatements.length - 1;
|
|
246
|
+
modified = true;
|
|
247
|
+
}
|
|
248
|
+
if (!modified) {
|
|
249
|
+
return { code, modified: false };
|
|
250
|
+
}
|
|
251
|
+
if (autoWrapImportDeclaration) {
|
|
252
|
+
if (!hasAutoWrapImport) {
|
|
253
|
+
autoWrapImportDeclaration.specifiers.push(t.importSpecifier(t.identifier(AUTO_WRAP_IMPORT_NAME), t.identifier(AUTO_WRAP_IMPORT_NAME)));
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
else {
|
|
257
|
+
let insertIndex = 0;
|
|
258
|
+
while (insertIndex < program.body.length &&
|
|
259
|
+
t.isImportDeclaration(program.body[insertIndex])) {
|
|
260
|
+
insertIndex += 1;
|
|
261
|
+
}
|
|
262
|
+
program.body.splice(insertIndex, 0, t.importDeclaration([
|
|
263
|
+
t.importSpecifier(t.identifier(AUTO_WRAP_IMPORT_NAME), t.identifier(AUTO_WRAP_IMPORT_NAME)),
|
|
264
|
+
], t.stringLiteral(AUTO_WRAP_IMPORT_SOURCE)));
|
|
265
|
+
}
|
|
266
|
+
const output = generate(ast, {
|
|
267
|
+
jsescOption: { minimal: true },
|
|
268
|
+
});
|
|
269
|
+
return {
|
|
270
|
+
code: output.code,
|
|
271
|
+
modified: true,
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
export function reinspectAutoDiscoverPlugin() {
|
|
275
|
+
return {
|
|
276
|
+
name: 'reinspect-auto-discover',
|
|
277
|
+
apply: 'serve',
|
|
278
|
+
enforce: 'pre',
|
|
279
|
+
transform(sourceCode, id) {
|
|
280
|
+
const normalizedId = normalizeModuleId(id);
|
|
281
|
+
if (!isSupportedSourceFile(normalizedId)) {
|
|
282
|
+
return null;
|
|
283
|
+
}
|
|
284
|
+
if (normalizedId.includes('/node_modules/react-reinspect/')) {
|
|
285
|
+
return null;
|
|
286
|
+
}
|
|
287
|
+
if (normalizedId.includes('/src/reinspect/')) {
|
|
288
|
+
return null;
|
|
289
|
+
}
|
|
290
|
+
const scope = isFirstPartyModule(normalizedId)
|
|
291
|
+
? 'first-party'
|
|
292
|
+
: 'third-party';
|
|
293
|
+
if (scope === 'third-party' && shouldSkipThirdPartyModule(normalizedId)) {
|
|
294
|
+
return null;
|
|
295
|
+
}
|
|
296
|
+
const transformedResult = transformModuleForAutoDiscover(sourceCode, normalizedId, scope);
|
|
297
|
+
if (!transformedResult.modified) {
|
|
298
|
+
return null;
|
|
299
|
+
}
|
|
300
|
+
return {
|
|
301
|
+
code: transformedResult.code,
|
|
302
|
+
map: null,
|
|
303
|
+
};
|
|
304
|
+
},
|
|
305
|
+
};
|
|
306
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-reinspect",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Visual runtime inspector for React components with render counters and style overrides.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"react",
|
|
@@ -19,10 +19,15 @@
|
|
|
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
|
+
},
|
|
22
26
|
"./style.css": "./dist/lib/style.css"
|
|
23
27
|
},
|
|
24
28
|
"files": [
|
|
25
29
|
"dist/lib",
|
|
30
|
+
"dist/plugin",
|
|
26
31
|
"README.md"
|
|
27
32
|
],
|
|
28
33
|
"sideEffects": [
|
|
@@ -32,11 +37,13 @@
|
|
|
32
37
|
"dev": "vite",
|
|
33
38
|
"build": "tsc -b && vite build",
|
|
34
39
|
"build:lib": "vite build -c vite.lib.config.ts && tsc -p tsconfig.lib.json",
|
|
40
|
+
"build:plugin": "tsc -p tsconfig.plugin.json",
|
|
41
|
+
"build:package": "pnpm build:lib && pnpm build:plugin",
|
|
35
42
|
"lint": "eslint .",
|
|
36
43
|
"preview": "vite preview",
|
|
37
44
|
"pack:check": "npm pack --dry-run --cache ./.cache/npm",
|
|
38
45
|
"publish:npm": "npm publish --access public --cache ./.cache/npm",
|
|
39
|
-
"prepublishOnly": "pnpm build:
|
|
46
|
+
"prepublishOnly": "pnpm build:package",
|
|
40
47
|
"test": "vitest run",
|
|
41
48
|
"test:watch": "vitest"
|
|
42
49
|
},
|
|
@@ -61,14 +68,14 @@
|
|
|
61
68
|
"eslint": "^9.39.4",
|
|
62
69
|
"eslint-plugin-react-hooks": "^7.0.1",
|
|
63
70
|
"eslint-plugin-react-refresh": "^0.5.2",
|
|
64
|
-
"globals": "^17.
|
|
71
|
+
"globals": "^17.5.0",
|
|
65
72
|
"jsdom": "^29.0.2",
|
|
66
73
|
"magic-string": "^0.30.21",
|
|
74
|
+
"react": "^19.2.5",
|
|
75
|
+
"react-dom": "^19.2.5",
|
|
67
76
|
"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"
|
|
77
|
+
"typescript-eslint": "^8.58.1",
|
|
78
|
+
"vite": "^8.0.8",
|
|
79
|
+
"vitest": "^4.1.4"
|
|
73
80
|
}
|
|
74
81
|
}
|